From e245283efbd6d3029aca0fc74f1c552bc2dcd9a3 Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Mon, 17 Apr 2023 16:33:18 +0200 Subject: [PATCH 01/38] Expose window in SCI env and remove from render --- src/nextjournal/clerk/render.cljs | 4 ---- src/nextjournal/clerk/sci_env.cljs | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index 284fd6d7f..0c73c8ff5 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -16,7 +16,6 @@ [nextjournal.clerk.render.hooks :as hooks] [nextjournal.clerk.render.localstorage :as localstorage] [nextjournal.clerk.render.navbar :as navbar] - [nextjournal.clerk.render.window :as window] [nextjournal.clerk.viewer :as viewer] [nextjournal.markdown.transform :as md.transform] [reagent.core :as r] @@ -599,9 +598,6 @@ (swap! !state update :desc viewer/merge-presentations more fetch-opts))))} [inspect-presented (:desc @!state)]])) -(defn show-window [& content] - [window/show content]) - (defn root [] [:<> [inspect-presented @!doc] diff --git a/src/nextjournal/clerk/sci_env.cljs b/src/nextjournal/clerk/sci_env.cljs index 214590641..a69f576b1 100644 --- a/src/nextjournal/clerk/sci_env.cljs +++ b/src/nextjournal/clerk/sci_env.cljs @@ -19,6 +19,7 @@ [nextjournal.clerk.render.context :as view-context] [nextjournal.clerk.render.hooks] [nextjournal.clerk.render.navbar] + [nextjournal.clerk.render.window] [nextjournal.clerk.trim-image] [nextjournal.clerk.viewer :as viewer] [nextjournal.clojure-mode.commands] @@ -143,6 +144,7 @@ 'nextjournal.clerk.render.code 'nextjournal.clerk.render.hooks 'nextjournal.clerk.render.navbar + 'nextjournal.clerk.render.window 'nextjournal.clojure-mode.keymap 'nextjournal.clojure-mode.commands From f7029eba2710f21af7a2021ec8202ff94d386c1b Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Mon, 17 Apr 2023 16:33:37 +0200 Subject: [PATCH 02/38] Fix shrinking header --- src/nextjournal/clerk/render/window.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextjournal/clerk/render/window.cljs b/src/nextjournal/clerk/render/window.cljs index 90466904a..89035346a 100644 --- a/src/nextjournal/clerk/render/window.cljs +++ b/src/nextjournal/clerk/render/window.cljs @@ -63,7 +63,7 @@ (js/addEventListener "mousemove" handle-mouse-move)) #(js/removeEventListener "mousemove" handle-mouse-move))) [!mouse-down on-drag]) - [:div.bg-slate-100.hover:bg-slate-200.dark:bg-slate-800.dark:hover:bg-slate-700.cursor-move.w-full.rounded-t-lg + [:div.bg-slate-100.hover:bg-slate-200.dark:bg-slate-800.dark:hover:bg-slate-700.cursor-move.w-full.rounded-t-lg.flex-shrink-0 {:class "h-[14px]" :on-mouse-down (fn [event] (on-drag-start) From ec933d5bbe0b7c3dfb55a09d431ae129ed77eec2 Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Mon, 17 Apr 2023 16:36:44 +0200 Subject: [PATCH 03/38] Add tap window poc --- notebooks/tap_window.clj | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 notebooks/tap_window.clj diff --git a/notebooks/tap_window.clj b/notebooks/tap_window.clj new file mode 100644 index 000000000..981bfb1f3 --- /dev/null +++ b/notebooks/tap_window.clj @@ -0,0 +1,30 @@ +;; # 🪲Debug +(ns notebook.tap-window + {:nextjournal.clerk/visibility {:code :hide :result :hide} + :nextjournal.clerk/no-cache true} + (:require [nextjournal.clerk :as clerk] + [nextjournal.clerk.viewer :as v])) + +(def window-viewer + {:render-fn '(fn [{:keys [vals]} opts] + [nextjournal.clerk.render.window/show + (into [:div] + (map (fn [v] + [:div.mb-4.pb-4.border-b + [nextjournal.clerk.render/inspect-presented v]])) + (:nextjournal/value vals))]) + :transform-fn v/mark-preserve-keys}) + +(defonce !taps (atom '())) + +(defonce taps-setup (add-tap (fn [x] + (swap! !taps conj x) + (clerk/recompute!)))) + +^{::clerk/visibility {:result :show}} +(clerk/with-viewer window-viewer + {:vals @!taps}) + +(comment + (tap> (clerk/html [:div.w-8.h-8.bg-green-500])) + (tap> (clerk/plotly {:data [{:x [1 2 3 4]}]}))) From 78d417b51e9d1fd29f560ac7c34cf3bc48a62779 Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Mon, 17 Apr 2023 17:13:44 +0200 Subject: [PATCH 04/38] Draft for a global stateful clerk/window API --- src/nextjournal/clerk.clj | 6 ++++++ src/nextjournal/clerk/render.cljs | 13 ++++++++++++- src/nextjournal/clerk/webserver.clj | 3 +++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index d58b4e89c..46a37ed7a 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -80,6 +80,12 @@ #_(recompute!) +(defn window! [id content] (webserver/update-window! id content)) +(defn destroy-window! [id content] (webserver/destroy-window! id)) + +#_(window! ::my-window-2 (table [[1 2] [3 4]])) +#_(destroy-window! ::my-window-2 (table [[1 2] [3 4]])) + (defn ^:private supported-file? "Returns whether `path` points to a file that should be shown." [path] diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index 0c73c8ff5..a86a1bb93 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -16,6 +16,7 @@ [nextjournal.clerk.render.hooks :as hooks] [nextjournal.clerk.render.localstorage :as localstorage] [nextjournal.clerk.render.navbar :as navbar] + [nextjournal.clerk.render.window :as window] [nextjournal.clerk.viewer :as viewer] [nextjournal.markdown.transform :as md.transform] [reagent.core :as r] @@ -560,6 +561,7 @@ [:span.cmt-meta tag] (when space? nbsp) value])) (defonce !doc (ratom/atom nil)) +(defonce !windows (r/atom {})) (defonce !error (ratom/atom nil)) (defonce !viewers viewer/!viewers) @@ -608,7 +610,11 @@ [exec-status status])] (when @!error [:div.fixed.top-0.left-0.w-full.h-full - [inspect-presented @!error]])]) + [inspect-presented @!error]]) + (into [:<>] + (map (fn [[_id {:keys [title presented-value]}]] + [window/show [inspect-presented presented-value]])) + @!windows)]) (declare mount) @@ -700,9 +706,14 @@ error (reject error))) (js/console.warn :process-eval-reply!/not-found :eval-id eval-id :keys (keys @!pending-clerk-eval-replies)))) +(defn set-window-state! [{:keys [id state]}] (swap! !windows assoc id state)) +(defn destroy-window! [{:keys [id]}] (swap! !windows dissoc id)) + (defn ^:export dispatch [{:as msg :keys [type]}] (let [dispatch-fn (get {:patch-state! patch-state! :set-state! set-state! + :set-window-state! set-window-state! + :destroy-window! destroy-window! :eval-reply process-eval-reply!} type (fn [_] diff --git a/src/nextjournal/clerk/webserver.clj b/src/nextjournal/clerk/webserver.clj index c8a1e5a0c..9d66c5091 100644 --- a/src/nextjournal/clerk/webserver.clj +++ b/src/nextjournal/clerk/webserver.clj @@ -191,6 +191,9 @@ #_(update-doc! (help-doc)) +(defn update-window! [id content] + (broadcast! {:type :set-window-state! :id id :state {:presented-value (v/present content)}})) +(defn destroy-window! [id] (broadcast! {:type :destroy-window! :id id})) (defn broadcast-status! [status] From 5dcb35d81ce4d3ac5aefc346eed84b51ac92a5b9 Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Mon, 17 Apr 2023 17:45:58 +0200 Subject: [PATCH 05/38] Draft of window-based global taps --- src/nextjournal/clerk.clj | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index 46a37ed7a..2d80e324c 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -80,11 +80,27 @@ #_(recompute!) -(defn window! [id content] (webserver/update-window! id content)) -(defn destroy-window! [id content] (webserver/destroy-window! id)) - +(defn window! + ([id] (case id ::taps (webserver/update-window! id (col @!taps)))) + ([id content] (webserver/update-window! id content))) + +(defn destroy-window! [id] (webserver/destroy-window! id)) + +(defonce !taps (atom ())) +(defn tapped [x] + (do (swap! !taps conj x) + (window! ::taps))) +(defonce taps-setup (add-tap tapped)) + +#_ (doseq [f @@(resolve 'clojure.core/tapset)] (remove-tap f)) +#_ (reset! !taps ()) +#_ (tap> 1) +#_ (window! ::taps) +#_(destroy-window! ::taps) +#_ (tap> (html [:h1 "Ahoi"])) +#_ (tap> (table [[1 2] [3 4]])) #_(window! ::my-window-2 (table [[1 2] [3 4]])) -#_(destroy-window! ::my-window-2 (table [[1 2] [3 4]])) +#_(destroy-window! ::my-window-2) (defn ^:private supported-file? "Returns whether `path` points to a file that should be shown." From 9997dfa06f2b932e721e71e01aae972ccc66803b Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Mon, 17 Apr 2023 17:57:15 +0200 Subject: [PATCH 06/38] Pass options to clerk/window --- src/nextjournal/clerk.clj | 8 +- src/nextjournal/clerk/render.cljs | 4 +- src/nextjournal/clerk/render/window.cljs | 134 ++++++++++++----------- src/nextjournal/clerk/webserver.clj | 3 +- 4 files changed, 76 insertions(+), 73 deletions(-) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index 2d80e324c..482c5f0cb 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -81,8 +81,10 @@ #_(recompute!) (defn window! - ([id] (case id ::taps (webserver/update-window! id (col @!taps)))) - ([id content] (webserver/update-window! id content))) + ([id] (case id ::taps (webserver/update-window! id {:title "🚰Taps"} (col @!taps)))) + ([id content] (window! id {} content)) + ([id opts content] + (webserver/update-window! id (merge opts {:presented-value (v/present content)})))) (defn destroy-window! [id] (webserver/destroy-window! id)) @@ -99,7 +101,7 @@ #_(destroy-window! ::taps) #_ (tap> (html [:h1 "Ahoi"])) #_ (tap> (table [[1 2] [3 4]])) -#_(window! ::my-window-2 (table [[1 2] [3 4]])) +#_(window! ::my-window-3 {:title "Ahoi"} (table [[1 2] [3 4]])) #_(destroy-window! ::my-window-2) (defn ^:private supported-file? diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index a86a1bb93..98fdd9fff 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -612,8 +612,8 @@ [:div.fixed.top-0.left-0.w-full.h-full [inspect-presented @!error]]) (into [:<>] - (map (fn [[_id {:keys [title presented-value]}]] - [window/show [inspect-presented presented-value]])) + (map (fn [[id {:as opts :keys [presented-value]}]] + [window/show [inspect-presented presented-value] (dissoc opts :presented-value)])) @!windows)]) (declare mount) diff --git a/src/nextjournal/clerk/render/window.cljs b/src/nextjournal/clerk/render/window.cljs index 89035346a..2ded2ccaa 100644 --- a/src/nextjournal/clerk/render/window.cljs +++ b/src/nextjournal/clerk/render/window.cljs @@ -49,7 +49,7 @@ {:on-mouse-down #(handle-mouse-down :left) :class "w-[4px]"}]])) -(defn header [{:keys [on-drag on-drag-start on-drag-end] :or {on-drag-start #() on-drag-end #()}}] +(defn header [{:keys [title on-drag on-drag-start on-drag-end] :or {on-drag-start #() on-drag-end #()}}] (let [!mouse-down (hooks/use-state false)] (hooks/use-effect (fn [] (let [handle-mouse-up (fn [] @@ -67,7 +67,7 @@ {:class "h-[14px]" :on-mouse-down (fn [event] (on-drag-start) - (reset! !mouse-down {:start-x (.-screenX event) :start-y (.-screenY event)}))}])) + (reset! !mouse-down {:start-x (.-screenX event) :start-y (.-screenY event)}))} title])) (defn resize-top [panel {:keys [top height]} dy] (j/assoc-in! panel [:style :height] (str (- height dy) "px")) @@ -111,67 +111,69 @@ (j/assoc-in! [:style :top] "5px") (j/assoc-in! [:style :left] "5px"))) -(defn show [& content] - (let [!panel-ref (hooks/use-ref nil) - !dragging? (hooks/use-state nil) - !dockable-at (hooks/use-state nil) - !docking-ref (hooks/use-ref nil)] - [:<> - [:div.fixed.border-2.border-dashed.border-indigo-600.border-opacity-70.bg-indigo-600.bg-opacity-30.pointer-events-none.transition-all.rounded-lg - {:class (str "z-[999] " (if-let [side @!dockable-at] - (str "opacity-100 " (case side - :top "left-[5px] top-[5px] right-[5px] h-[33vh]" - :left "left-[5px] top-[5px] bottom-[5px] w-[33vw]" - :bottom "left-[5px] bottom-[5px] right-[5px] h-[33vh]" - :right "right-[5px] top-[5px] bottom-[5px] w-[33vw]")) - "opacity-0 "))}] - [:div.fixed.bg-white.dark:bg-slate-900.shadow-xl.text-slate-800.dark:text-slate-100.rounded-lg.flex.flex-col.hover:ring-2 - {:class (str "z-[1000] " (if @!dragging? "ring-indigo-600 select-none ring-2 " "ring-slate-300 dark:ring-slate-700 ring-1 ")) - :ref !panel-ref - :style {:top 30 :right 30 :width 400 :height 400}} - [resizer {:on-resize (fn [dir dx dy] - (when-let [panel @!panel-ref] - (let [rect (j/lookup (.getBoundingClientRect panel))] - (case dir - :top-left (do (resize-top panel rect dy) - (resize-left panel rect dx)) - :top (resize-top panel rect dy) - :top-right (do (resize-top panel rect dy) - (resize-right panel rect dx)) - :right (resize-right panel rect dx) - :bottom-right (do (resize-bottom panel rect dy) - (resize-right panel rect dx)) - :bottom (resize-bottom panel rect dy) - :bottom-left (do (resize-bottom panel rect dy) - (resize-left panel rect dx)) - :left (resize-left panel rect dx))))) - :on-resize-start #(reset! !dragging? true) - :on-resize-end #(reset! !dragging? false)}] - [header {:on-drag (fn [{:keys [x y dx dy]}] - (when-let [panel @!panel-ref] - (let [{:keys [left top width]} (j/lookup (.getBoundingClientRect panel)) - x-edge-offset 20 - y-edge-offset 10 - vw js/innerWidth - vh js/innerHeight] - (reset! !dockable-at (cond - (zero? x) :left - (>= x (- vw 2)) :right - (<= y 0) :top - (>= y (- vh 2)) :bottom - :else nil)) - (reset! !docking-ref @!dockable-at) - (j/assoc-in! panel [:style :left] (str (min (- vw x-edge-offset) (max (+ x-edge-offset (- width)) (+ left dx))) "px")) - (j/assoc-in! panel [:style :top] (str (min (- vh y-edge-offset) (max y-edge-offset (+ top dy))) "px"))))) - :on-drag-start #(reset! !dragging? true) - :on-drag-end (fn [] - (when-let [side @!docking-ref] - (let [panel @!panel-ref] - (case side - :top (dock-at-top panel) - :right (dock-at-right panel) - :bottom (dock-at-bottom panel) - :left (dock-at-left panel)))) - (reset! !dockable-at nil) - (reset! !docking-ref nil))}] - (into [:div.p-3.flex-auto.overflow-auto] content)]])) +(defn show + ([content] (show content {})) + ([content opts] + (let [!panel-ref (hooks/use-ref nil) + !dragging? (hooks/use-state nil) + !dockable-at (hooks/use-state nil) + !docking-ref (hooks/use-ref nil)] + [:<> + [:div.fixed.border-2.border-dashed.border-indigo-600.border-opacity-70.bg-indigo-600.bg-opacity-30.pointer-events-none.transition-all.rounded-lg + {:class (str "z-[999] " (if-let [side @!dockable-at] + (str "opacity-100 " (case side + :top "left-[5px] top-[5px] right-[5px] h-[33vh]" + :left "left-[5px] top-[5px] bottom-[5px] w-[33vw]" + :bottom "left-[5px] bottom-[5px] right-[5px] h-[33vh]" + :right "right-[5px] top-[5px] bottom-[5px] w-[33vw]")) + "opacity-0 "))}] + [:div.fixed.bg-white.dark:bg-slate-900.shadow-xl.text-slate-800.dark:text-slate-100.rounded-lg.flex.flex-col.hover:ring-2 + {:class (str "z-[1000] " (if @!dragging? "ring-indigo-600 select-none ring-2 " "ring-slate-300 dark:ring-slate-700 ring-1 ")) + :ref !panel-ref + :style {:top 30 :right 30 :width 400 :height 400}} + [resizer {:on-resize (fn [dir dx dy] + (when-let [panel @!panel-ref] + (let [rect (j/lookup (.getBoundingClientRect panel))] + (case dir + :top-left (do (resize-top panel rect dy) + (resize-left panel rect dx)) + :top (resize-top panel rect dy) + :top-right (do (resize-top panel rect dy) + (resize-right panel rect dx)) + :right (resize-right panel rect dx) + :bottom-right (do (resize-bottom panel rect dy) + (resize-right panel rect dx)) + :bottom (resize-bottom panel rect dy) + :bottom-left (do (resize-bottom panel rect dy) + (resize-left panel rect dx)) + :left (resize-left panel rect dx))))) + :on-resize-start #(reset! !dragging? true) + :on-resize-end #(reset! !dragging? false)}] + [header (merge {:on-drag (fn [{:keys [x y dx dy]}] + (when-let [panel @!panel-ref] + (let [{:keys [left top width]} (j/lookup (.getBoundingClientRect panel)) + x-edge-offset 20 + y-edge-offset 10 + vw js/innerWidth + vh js/innerHeight] + (reset! !dockable-at (cond + (zero? x) :left + (>= x (- vw 2)) :right + (<= y 0) :top + (>= y (- vh 2)) :bottom + :else nil)) + (reset! !docking-ref @!dockable-at) + (j/assoc-in! panel [:style :left] (str (min (- vw x-edge-offset) (max (+ x-edge-offset (- width)) (+ left dx))) "px")) + (j/assoc-in! panel [:style :top] (str (min (- vh y-edge-offset) (max y-edge-offset (+ top dy))) "px"))))) + :on-drag-start #(reset! !dragging? true) + :on-drag-end (fn [] + (when-let [side @!docking-ref] + (let [panel @!panel-ref] + (case side + :top (dock-at-top panel) + :right (dock-at-right panel) + :bottom (dock-at-bottom panel) + :left (dock-at-left panel)))) + (reset! !dockable-at nil) + (reset! !docking-ref nil))} opts)] + [:div.p-3.flex-auto.overflow-auto content]]]))) diff --git a/src/nextjournal/clerk/webserver.clj b/src/nextjournal/clerk/webserver.clj index 9d66c5091..36d6a04e7 100644 --- a/src/nextjournal/clerk/webserver.clj +++ b/src/nextjournal/clerk/webserver.clj @@ -191,8 +191,7 @@ #_(update-doc! (help-doc)) -(defn update-window! [id content] - (broadcast! {:type :set-window-state! :id id :state {:presented-value (v/present content)}})) +(defn update-window! [id state] (broadcast! {:type :set-window-state! :id id :state state})) (defn destroy-window! [id] (broadcast! {:type :destroy-window! :id id})) From 02f416164391b17bdcf6e78c458c9c3011ce979c Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Mon, 17 Apr 2023 18:04:32 +0200 Subject: [PATCH 07/38] Fix compile --- src/nextjournal/clerk.clj | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index 482c5f0cb..49631d8db 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -80,6 +80,10 @@ #_(recompute!) +(defonce !taps (atom ())) + +(declare col) +;; TODO: drop `col` in favour of a suitable viewer (defn window! ([id] (case id ::taps (webserver/update-window! id {:title "🚰Taps"} (col @!taps)))) ([id content] (window! id {} content)) @@ -88,7 +92,6 @@ (defn destroy-window! [id] (webserver/destroy-window! id)) -(defonce !taps (atom ())) (defn tapped [x] (do (swap! !taps conj x) (window! ::taps))) @@ -101,7 +104,8 @@ #_(destroy-window! ::taps) #_ (tap> (html [:h1 "Ahoi"])) #_ (tap> (table [[1 2] [3 4]])) -#_(window! ::my-window-3 {:title "Ahoi"} (table [[1 2] [3 4]])) +#_(window! ::my-window {:title "Ahoi"} (table [[1 2] [3 4]])) +#_(window! ::my-window {:title "Ahoi"} (plotly {:data [{:y [1 2 3]}]})) #_(destroy-window! ::my-window-2) (defn ^:private supported-file? From e0399f909580ee6d62f00ead0e841a0226eb7de8 Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Mon, 17 Apr 2023 18:11:49 +0200 Subject: [PATCH 08/38] Fix taps --- src/nextjournal/clerk.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index 49631d8db..694e9c0e3 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -85,7 +85,7 @@ (declare col) ;; TODO: drop `col` in favour of a suitable viewer (defn window! - ([id] (case id ::taps (webserver/update-window! id {:title "🚰Taps"} (col @!taps)))) + ([id] (case id ::taps (window! id {:title "🚰Taps"} (col @!taps)))) ([id content] (window! id {} content)) ([id opts content] (webserver/update-window! id (merge opts {:presented-value (v/present content)})))) From d3956174a7aaa6a75e2aa44aecd1889541132fde Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Mon, 17 Apr 2023 18:58:59 +0200 Subject: [PATCH 09/38] Lint --- src/nextjournal/clerk.clj | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index 694e9c0e3..82ed7017c 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -92,9 +92,7 @@ (defn destroy-window! [id] (webserver/destroy-window! id)) -(defn tapped [x] - (do (swap! !taps conj x) - (window! ::taps))) +(defn tapped [x] (swap! !taps conj x) (window! ::taps)) (defonce taps-setup (add-tap tapped)) #_ (doseq [f @@(resolve 'clojure.core/tapset)] (remove-tap f)) From 7be982b5a2b5b7d2d6f11411b786edf7b6e75a96 Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Mon, 17 Apr 2023 19:02:36 +0200 Subject: [PATCH 10/38] Consistency --- src/nextjournal/clerk/render.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index 98fdd9fff..9d5c5083a 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -561,7 +561,7 @@ [:span.cmt-meta tag] (when space? nbsp) value])) (defonce !doc (ratom/atom nil)) -(defonce !windows (r/atom {})) +(defonce !windows (ratom/atom {})) (defonce !error (ratom/atom nil)) (defonce !viewers viewer/!viewers) From a2c143811d2ce5cf9e4d2a29a878859d646a3a8c Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Tue, 18 Apr 2023 10:01:06 +0200 Subject: [PATCH 11/38] Toward fixing pagination --- src/nextjournal/clerk.clj | 6 ++++-- src/nextjournal/clerk/render.cljs | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index 82ed7017c..dcffda659 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -88,7 +88,9 @@ ([id] (case id ::taps (window! id {:title "🚰Taps"} (col @!taps)))) ([id content] (window! id {} content)) ([id opts content] - (webserver/update-window! id (merge opts {:presented-value (v/present content)})))) + (webserver/update-window! id (merge opts {:nextjournal/presented (v/present content) + :nextjournal/fetch-opts {:blob-id id} + :nextjournal/blob-id id})))) (defn destroy-window! [id] (webserver/destroy-window! id)) @@ -97,7 +99,7 @@ #_ (doseq [f @@(resolve 'clojure.core/tapset)] (remove-tap f)) #_ (reset! !taps ()) -#_ (tap> 1) +#_(tap> (range 30)) #_ (window! ::taps) #_(destroy-window! ::taps) #_ (tap> (html [:h1 "Ahoi"])) diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index 9d5c5083a..7c6db067a 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -612,8 +612,8 @@ [:div.fixed.top-0.left-0.w-full.h-full [inspect-presented @!error]]) (into [:<>] - (map (fn [[id {:as opts :keys [presented-value]}]] - [window/show [inspect-presented presented-value] (dissoc opts :presented-value)])) + (map (fn [[id state]] + [window/show [render-result state {}] (dissoc state :nextjournal/presented)])) @!windows)]) (declare mount) From a2427a6297308188127427e40a648d0a70e12845 Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Tue, 18 Apr 2023 10:16:40 +0200 Subject: [PATCH 12/38] Fix changing same window values --- src/nextjournal/clerk.clj | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index dcffda659..c88be7540 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -89,8 +89,9 @@ ([id content] (window! id {} content)) ([id opts content] (webserver/update-window! id (merge opts {:nextjournal/presented (v/present content) - :nextjournal/fetch-opts {:blob-id id} - :nextjournal/blob-id id})))) + :nextjournal/hash (gensym) + :nextjournal/fetch-opts {:blob-id (gensym)} + :nextjournal/blob-id (gensym)})))) (defn destroy-window! [id] (webserver/destroy-window! id)) @@ -100,13 +101,14 @@ #_ (doseq [f @@(resolve 'clojure.core/tapset)] (remove-tap f)) #_ (reset! !taps ()) #_(tap> (range 30)) -#_ (window! ::taps) +#_(window! ::taps) #_(destroy-window! ::taps) #_ (tap> (html [:h1 "Ahoi"])) #_ (tap> (table [[1 2] [3 4]])) #_(window! ::my-window {:title "Ahoi"} (table [[1 2] [3 4]])) +#_(window! ::my-window {:title "Ahoi"} (range 40)) #_(window! ::my-window {:title "Ahoi"} (plotly {:data [{:y [1 2 3]}]})) -#_(destroy-window! ::my-window-2) +#_(destroy-window! ::my-window) (defn ^:private supported-file? "Returns whether `path` points to a file that should be shown." From f0217a2619230e999afbb99c1222d2103702135c Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Tue, 18 Apr 2023 10:29:42 +0200 Subject: [PATCH 13/38] Fix pagination inside windows --- src/nextjournal/clerk.clj | 12 ++++++------ src/nextjournal/clerk/webserver.clj | 11 ++++++++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index c88be7540..e36f6fdbd 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -90,21 +90,21 @@ ([id opts content] (webserver/update-window! id (merge opts {:nextjournal/presented (v/present content) :nextjournal/hash (gensym) - :nextjournal/fetch-opts {:blob-id (gensym)} - :nextjournal/blob-id (gensym)})))) + :nextjournal/fetch-opts {:blob-id (str id)} + :nextjournal/blob-id (str id)})))) (defn destroy-window! [id] (webserver/destroy-window! id)) (defn tapped [x] (swap! !taps conj x) (window! ::taps)) (defonce taps-setup (add-tap tapped)) -#_ (doseq [f @@(resolve 'clojure.core/tapset)] (remove-tap f)) -#_ (reset! !taps ()) +#_(doseq [f @@(resolve 'clojure.core/tapset)] (remove-tap f)) +#_(reset! !taps ()) #_(tap> (range 30)) #_(window! ::taps) #_(destroy-window! ::taps) -#_ (tap> (html [:h1 "Ahoi"])) -#_ (tap> (table [[1 2] [3 4]])) +#_(tap> (html [:h1 "Ahoi"])) +#_(tap> (table [[1 2] [3 4]])) #_(window! ::my-window {:title "Ahoi"} (table [[1 2] [3 4]])) #_(window! ::my-window {:title "Ahoi"} (range 40)) #_(window! ::my-window {:title "Ahoi"} (plotly {:data [{:y [1 2 3]}]})) diff --git a/src/nextjournal/clerk/webserver.clj b/src/nextjournal/clerk/webserver.clj index 36d6a04e7..5cb1b9cb8 100644 --- a/src/nextjournal/clerk/webserver.clj +++ b/src/nextjournal/clerk/webserver.clj @@ -24,6 +24,7 @@ (defonce !clients (atom #{})) (defonce !doc (atom nil)) +(defonce !window (atom nil)) (defonce !error (atom nil)) (defonce !last-sender-ch (atom nil)) @@ -77,8 +78,9 @@ (into {} (comp (filter #(and (map? %) (v/get-safe % :nextjournal/blob-id) (v/get-safe % :nextjournal/presented))) (map (juxt :nextjournal/blob-id :nextjournal/presented))) - (tree-seq coll? seq - (:nextjournal/value presented-doc)))) + (cons @!window + (tree-seq coll? seq + (:nextjournal/value presented-doc))))) #_(blob->presented (meta @!doc)) @@ -191,7 +193,10 @@ #_(update-doc! (help-doc)) -(defn update-window! [id state] (broadcast! {:type :set-window-state! :id id :state state})) +(defn update-window! [id state] + (reset! !window state) + (broadcast! {:type :set-window-state! :id id :state state})) + (defn destroy-window! [id] (broadcast! {:type :destroy-window! :id id})) From 7882b716b86997821e09f4edf658855bea2dfc2f Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Tue, 18 Apr 2023 10:42:22 +0200 Subject: [PATCH 14/38] Fix pagination across windows --- src/nextjournal/clerk.clj | 7 ++++--- src/nextjournal/clerk/webserver.clj | 10 +++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index e36f6fdbd..f0b00ad1e 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -105,9 +105,10 @@ #_(destroy-window! ::taps) #_(tap> (html [:h1 "Ahoi"])) #_(tap> (table [[1 2] [3 4]])) -#_(window! ::my-window {:title "Ahoi"} (table [[1 2] [3 4]])) -#_(window! ::my-window {:title "Ahoi"} (range 40)) -#_(window! ::my-window {:title "Ahoi"} (plotly {:data [{:y [1 2 3]}]})) +#_(window! ::my-window {:title "🔭 Rear Window"} (table [[1 2] [3 4]])) +#_(window! ::my-window {:title "🔭 Rear Window"} (range 30)) +#_(window! ::my-window {:title "🔭 Rear Window"} (plotly {:data [{:y [1 2 3]}]})) +#_(window! ::my-window-2 {:title "🪟"} (range 100)) #_(destroy-window! ::my-window) (defn ^:private supported-file? diff --git a/src/nextjournal/clerk/webserver.clj b/src/nextjournal/clerk/webserver.clj index 5cb1b9cb8..ab5789292 100644 --- a/src/nextjournal/clerk/webserver.clj +++ b/src/nextjournal/clerk/webserver.clj @@ -24,7 +24,7 @@ (defonce !clients (atom #{})) (defonce !doc (atom nil)) -(defonce !window (atom nil)) +(defonce !windows (atom {})) (defonce !error (atom nil)) (defonce !last-sender-ch (atom nil)) @@ -78,9 +78,9 @@ (into {} (comp (filter #(and (map? %) (v/get-safe % :nextjournal/blob-id) (v/get-safe % :nextjournal/presented))) (map (juxt :nextjournal/blob-id :nextjournal/presented))) - (cons @!window - (tree-seq coll? seq - (:nextjournal/value presented-doc))))) + (concat (vals @!windows) + (tree-seq coll? seq + (:nextjournal/value presented-doc))))) #_(blob->presented (meta @!doc)) @@ -194,7 +194,7 @@ #_(update-doc! (help-doc)) (defn update-window! [id state] - (reset! !window state) + (swap! !windows assoc id state) (broadcast! {:type :set-window-state! :id id :state state})) (defn destroy-window! [id] (broadcast! {:type :destroy-window! :id id})) From 1c17fcf1ba04ea763e1f5d4cd514dc7a329650a2 Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Tue, 18 Apr 2023 11:09:19 +0200 Subject: [PATCH 15/38] Window titles and close button --- notebooks/tap_window.clj | 22 ++-------------------- src/nextjournal/clerk.clj | 2 +- src/nextjournal/clerk/render.cljs | 6 +++++- src/nextjournal/clerk/render/window.cljs | 24 ++++++++++++++++++------ 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/notebooks/tap_window.clj b/notebooks/tap_window.clj index 981bfb1f3..82e8a0268 100644 --- a/notebooks/tap_window.clj +++ b/notebooks/tap_window.clj @@ -1,29 +1,11 @@ -;; # 🪲Debug +;; # 🪟 Windows (ns notebook.tap-window {:nextjournal.clerk/visibility {:code :hide :result :hide} :nextjournal.clerk/no-cache true} (:require [nextjournal.clerk :as clerk] [nextjournal.clerk.viewer :as v])) -(def window-viewer - {:render-fn '(fn [{:keys [vals]} opts] - [nextjournal.clerk.render.window/show - (into [:div] - (map (fn [v] - [:div.mb-4.pb-4.border-b - [nextjournal.clerk.render/inspect-presented v]])) - (:nextjournal/value vals))]) - :transform-fn v/mark-preserve-keys}) - -(defonce !taps (atom '())) - -(defonce taps-setup (add-tap (fn [x] - (swap! !taps conj x) - (clerk/recompute!)))) - -^{::clerk/visibility {:result :show}} -(clerk/with-viewer window-viewer - {:vals @!taps}) +(clerk/window! ::clerk/taps) (comment (tap> (clerk/html [:div.w-8.h-8.bg-green-500])) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index f0b00ad1e..96605dc56 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -85,7 +85,7 @@ (declare col) ;; TODO: drop `col` in favour of a suitable viewer (defn window! - ([id] (case id ::taps (window! id {:title "🚰Taps"} (col @!taps)))) + ([id] (case id ::taps (window! id {:title "🚰 Taps"} (col @!taps)))) ([id content] (window! id {} content)) ([id opts content] (webserver/update-window! id (merge opts {:nextjournal/presented (v/present content) diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index 7c6db067a..899c853df 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -613,7 +613,11 @@ [inspect-presented @!error]]) (into [:<>] (map (fn [[id state]] - [window/show [render-result state {}] (dissoc state :nextjournal/presented)])) + [window/show + [render-result state {}] + (-> state + (assoc :id id :on-close #(swap! !windows dissoc id)) + (dissoc :nextjournal/presented))])) @!windows)]) (declare mount) diff --git a/src/nextjournal/clerk/render/window.cljs b/src/nextjournal/clerk/render/window.cljs index 2ded2ccaa..6ecba340a 100644 --- a/src/nextjournal/clerk/render/window.cljs +++ b/src/nextjournal/clerk/render/window.cljs @@ -49,8 +49,9 @@ {:on-mouse-down #(handle-mouse-down :left) :class "w-[4px]"}]])) -(defn header [{:keys [title on-drag on-drag-start on-drag-end] :or {on-drag-start #() on-drag-end #()}}] - (let [!mouse-down (hooks/use-state false)] +(defn header [{:keys [id title on-drag on-drag-start on-drag-end on-close] :or {on-drag-start #() on-drag-end #()}}] + (let [!mouse-down (hooks/use-state false) + name (or title id)] (hooks/use-effect (fn [] (let [handle-mouse-up (fn [] (on-drag-end) @@ -63,11 +64,21 @@ (js/addEventListener "mousemove" handle-mouse-move)) #(js/removeEventListener "mousemove" handle-mouse-move))) [!mouse-down on-drag]) - [:div.bg-slate-100.hover:bg-slate-200.dark:bg-slate-800.dark:hover:bg-slate-700.cursor-move.w-full.rounded-t-lg.flex-shrink-0 - {:class "h-[14px]" + [:div.bg-slate-100.hover:bg-slate-200.dark:bg-slate-800.dark:hover:bg-slate-700.cursor-move.w-full.rounded-t-lg.flex-shrink-0.leading-none.flex.items-center.justify-between + {:class (if name "h-[20px] " "h-[14px] ") :on-mouse-down (fn [event] (on-drag-start) - (reset! !mouse-down {:start-x (.-screenX event) :start-y (.-screenY event)}))} title])) + (reset! !mouse-down {:start-x (.-screenX event) :start-y (.-screenY event)}))} + (when name + [:span.font-sans.font-medium.text-slate-700 + {:class "text-[11px] ml-[8px] "} + (or title id)]) + (when on-close + [:button.text-slate-600.hover:text-slate-900.hover:bg-slate-300.rounded-tr-lg.flex.items-center.justify-center + {:on-click on-close + :class "w-[20px] h-[20px]"} + [:svg {:xmlns "http://www.w3.org/2000/svg" :fill "none" :viewBox "0 0 24 24" :stroke-width "1.5" :stroke "currentColor" :class "w-3 h-3"} + [:path {:stroke-linecap "round" :stroke-linejoin "round" :d "M6 18L18 6M6 6l12 12"}]]])])) (defn resize-top [panel {:keys [top height]} dy] (j/assoc-in! panel [:style :height] (str (- height dy) "px")) @@ -175,5 +186,6 @@ :bottom (dock-at-bottom panel) :left (dock-at-left panel)))) (reset! !dockable-at nil) - (reset! !docking-ref nil))} opts)] + (reset! !docking-ref nil))} + opts)] [:div.p-3.flex-auto.overflow-auto content]]]))) From 1bdea94d7c2761040f3181f1807aab3df968764b Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Tue, 18 Apr 2023 11:20:09 +0200 Subject: [PATCH 16/38] Reset default result padding in window --- src/nextjournal/clerk.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index 96605dc56..280a64771 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -88,7 +88,7 @@ ([id] (case id ::taps (window! id {:title "🚰 Taps"} (col @!taps)))) ([id content] (window! id {} content)) ([id opts content] - (webserver/update-window! id (merge opts {:nextjournal/presented (v/present content) + (webserver/update-window! id (merge opts {:nextjournal/presented (assoc (v/present content) :nextjournal/css-class ["px-0"]) :nextjournal/hash (gensym) :nextjournal/fetch-opts {:blob-id (str id)} :nextjournal/blob-id (str id)})))) From 75e5743b90c0acf2b759428df62e84f2afa97556 Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Tue, 18 Apr 2023 11:26:01 +0200 Subject: [PATCH 17/38] Also respect already existing css-classes --- src/nextjournal/clerk.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index 280a64771..91c8d2948 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -88,7 +88,7 @@ ([id] (case id ::taps (window! id {:title "🚰 Taps"} (col @!taps)))) ([id content] (window! id {} content)) ([id opts content] - (webserver/update-window! id (merge opts {:nextjournal/presented (assoc (v/present content) :nextjournal/css-class ["px-0"]) + (webserver/update-window! id (merge opts {:nextjournal/presented (update (v/present content) :nextjournal/css-class #(or % ["px-0"])) :nextjournal/hash (gensym) :nextjournal/fetch-opts {:blob-id (str id)} :nextjournal/blob-id (str id)})))) From 655549f9bc03b3429f79e9c6ed5d16524b878ccd Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Tue, 18 Apr 2023 12:13:14 +0200 Subject: [PATCH 18/38] Re-use tap viewers and state for taps window, add window ns --- notebooks/tap_window.clj | 3 +- src/nextjournal/clerk.clj | 31 ---------------- src/nextjournal/clerk/render/window.cljs | 4 +-- src/nextjournal/clerk/tap.clj | 7 ++-- src/nextjournal/clerk/window.clj | 46 ++++++++++++++++++++++++ 5 files changed, 53 insertions(+), 38 deletions(-) create mode 100644 src/nextjournal/clerk/window.clj diff --git a/notebooks/tap_window.clj b/notebooks/tap_window.clj index 82e8a0268..28ed819cc 100644 --- a/notebooks/tap_window.clj +++ b/notebooks/tap_window.clj @@ -5,7 +5,8 @@ (:require [nextjournal.clerk :as clerk] [nextjournal.clerk.viewer :as v])) -(clerk/window! ::clerk/taps) +#_(clerk/window! ::clerk/taps) +(clerk/window! :my-window (clerk/html [:div.w-8.h-8.bg-green-500])) (comment (tap> (clerk/html [:div.w-8.h-8.bg-green-500])) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index 91c8d2948..d58b4e89c 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -80,37 +80,6 @@ #_(recompute!) -(defonce !taps (atom ())) - -(declare col) -;; TODO: drop `col` in favour of a suitable viewer -(defn window! - ([id] (case id ::taps (window! id {:title "🚰 Taps"} (col @!taps)))) - ([id content] (window! id {} content)) - ([id opts content] - (webserver/update-window! id (merge opts {:nextjournal/presented (update (v/present content) :nextjournal/css-class #(or % ["px-0"])) - :nextjournal/hash (gensym) - :nextjournal/fetch-opts {:blob-id (str id)} - :nextjournal/blob-id (str id)})))) - -(defn destroy-window! [id] (webserver/destroy-window! id)) - -(defn tapped [x] (swap! !taps conj x) (window! ::taps)) -(defonce taps-setup (add-tap tapped)) - -#_(doseq [f @@(resolve 'clojure.core/tapset)] (remove-tap f)) -#_(reset! !taps ()) -#_(tap> (range 30)) -#_(window! ::taps) -#_(destroy-window! ::taps) -#_(tap> (html [:h1 "Ahoi"])) -#_(tap> (table [[1 2] [3 4]])) -#_(window! ::my-window {:title "🔭 Rear Window"} (table [[1 2] [3 4]])) -#_(window! ::my-window {:title "🔭 Rear Window"} (range 30)) -#_(window! ::my-window {:title "🔭 Rear Window"} (plotly {:data [{:y [1 2 3]}]})) -#_(window! ::my-window-2 {:title "🪟"} (range 100)) -#_(destroy-window! ::my-window) - (defn ^:private supported-file? "Returns whether `path` points to a file that should be shown." [path] diff --git a/src/nextjournal/clerk/render/window.cljs b/src/nextjournal/clerk/render/window.cljs index 6ecba340a..fc912f098 100644 --- a/src/nextjournal/clerk/render/window.cljs +++ b/src/nextjournal/clerk/render/window.cljs @@ -124,7 +124,7 @@ (defn show ([content] (show content {})) - ([content opts] + ([content {:as opts :keys [css-class]}] (let [!panel-ref (hooks/use-ref nil) !dragging? (hooks/use-state nil) !dockable-at (hooks/use-state nil) @@ -188,4 +188,4 @@ (reset! !dockable-at nil) (reset! !docking-ref nil))} opts)] - [:div.p-3.flex-auto.overflow-auto content]]]))) + [:div {:class (str "flex-auto " (or css-class "p-3 overflow-auto"))} content]]]))) diff --git a/src/nextjournal/clerk/tap.clj b/src/nextjournal/clerk/tap.clj index cd1c58fec..73a298d1c 100644 --- a/src/nextjournal/clerk/tap.clj +++ b/src/nextjournal/clerk/tap.clj @@ -28,7 +28,6 @@ ^{::clerk/sync true ::clerk/viewer switch-view ::clerk/visibility {:result :show}} (defonce !view (atom :stream)) - (defonce !taps (atom ())) (defn reset-taps! [] @@ -45,9 +44,9 @@ (def tap-viewer {:pred (v/get-safe ::val) :render-fn '(fn [{::keys [val tapped-at]} opts] - [:div.border-t.relative.py-3.mt-2 - [:span.absolute.rounded-full.px-2.bg-gray-300.font-mono.top-0 - {:class "left-1/2 -translate-x-1/2 -translate-y-1/2 py-[1px] text-[9px]"} (:nextjournal/value tapped-at)] + [:div.w-full + [:div.font-sans.bg-slate-50.px-2 + {:class "py-[2px] text-[9px]"} (:nextjournal/value tapped-at)] [:div.overflow-x-auto [nextjournal.clerk.render/inspect-presented val]]]) :transform-fn (fn [{:as wrapped-value :nextjournal/keys [value]}] (-> wrapped-value diff --git a/src/nextjournal/clerk/window.clj b/src/nextjournal/clerk/window.clj new file mode 100644 index 000000000..a0aa7c524 --- /dev/null +++ b/src/nextjournal/clerk/window.clj @@ -0,0 +1,46 @@ +(ns nextjournal.clerk.window + (:require [nextjournal.clerk.tap :as tap] + [nextjournal.clerk.viewer :as v] + [nextjournal.clerk.webserver :as webserver])) + +(def taps-viewer + {:render-fn '(fn [taps opts] + (into [:div] + (nextjournal.clerk.viewer/inspect-children opts) + taps))}) + +(defn window! + ([id] + (case id + ::taps (window! id {:title "🚰 Taps" :css-class "p-0"} + (v/with-viewers (v/add-viewers [tap/tap-viewer]) + (v/with-viewer taps-viewer @tap/!taps))))) + ([id content] (window! id {} content)) + ([id opts content] + (webserver/update-window! id (merge opts {:nextjournal/presented (update (v/present content) :nextjournal/css-class #(or % ["px-0"])) + :nextjournal/hash (gensym) + :nextjournal/fetch-opts {:blob-id (str id)} + :nextjournal/blob-id (str id)})))) + +(defn destroy-window! [id] (webserver/destroy-window! id)) + +(doseq [w (keys @webserver/!windows)] + (destroy-window! w)) + +#_(window! ::taps) + +#_(defn tapped [x] (swap! !taps conj x) (window! ::taps)) +#_(defonce taps-setup (add-tap tapped)) + +#_(doseq [f @@(resolve 'clojure.core/tapset)] (remove-tap f)) +#_(reset! !taps ()) +#_(tap> (range 30)) +#_(window! ::taps) +#_(destroy-window! ::taps) +#_(tap> (v/html [:h1 "Ahoi"])) +#_(tap> (v/table [[1 2] [3 4]])) +#_(window! ::my-window {:title "🔭 Rear Window"} (table [[1 2] [3 4]])) +#_(window! ::my-window {:title "🔭 Rear Window"} (range 30)) +#_(window! ::my-window {:title "🔭 Rear Window"} (plotly {:data [{:y [1 2 3]}]})) +#_(window! ::my-window-2 {:title "🪟"} (range 100)) +#_(destroy-window! ::my-window) From ff404b1447fdbac28a21c25b4d0afc6f9be9a02d Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Tue, 18 Apr 2023 12:29:03 +0200 Subject: [PATCH 19/38] Restore top-level API / rename --- notebooks/tap_window.clj | 3 +-- src/nextjournal/clerk.clj | 4 ++++ src/nextjournal/clerk/window.clj | 34 +++++++++++++++++--------------- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/notebooks/tap_window.clj b/notebooks/tap_window.clj index 28ed819cc..4baa21b9c 100644 --- a/notebooks/tap_window.clj +++ b/notebooks/tap_window.clj @@ -1,11 +1,10 @@ ;; # 🪟 Windows -(ns notebook.tap-window +(ns tap-window {:nextjournal.clerk/visibility {:code :hide :result :hide} :nextjournal.clerk/no-cache true} (:require [nextjournal.clerk :as clerk] [nextjournal.clerk.viewer :as v])) -#_(clerk/window! ::clerk/taps) (clerk/window! :my-window (clerk/html [:div.w-8.h-8.bg-green-500])) (comment diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index d58b4e89c..3a054b393 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -13,6 +13,7 @@ [nextjournal.clerk.parser :as parser] [nextjournal.clerk.view :as view] [nextjournal.clerk.viewer :as v] + [nextjournal.clerk.window :as window] [nextjournal.clerk.webserver :as webserver])) (defonce ^:private !show-filter-fn (atom nil)) @@ -70,6 +71,9 @@ #_(show! "https://raw.githubusercontent.com/nextjournal/clerk-demo/main/notebooks/rule_30.clj") #_(show! (java.io.StringReader. ";; # In Memory Notebook 👋\n(+ 41 1)")) +(defn window! "todo" [& args] (apply window/open! args)) +(defn destroy-window! "todo" [id] (window/destroy! id)) + (defn recompute! "Recomputes the currently visible doc, without parsing it." [] diff --git a/src/nextjournal/clerk/window.clj b/src/nextjournal/clerk/window.clj index a0aa7c524..5a244d494 100644 --- a/src/nextjournal/clerk/window.clj +++ b/src/nextjournal/clerk/window.clj @@ -9,38 +9,40 @@ (nextjournal.clerk.viewer/inspect-children opts) taps))}) -(defn window! +(defn open! ([id] (case id - ::taps (window! id {:title "🚰 Taps" :css-class "p-0"} - (v/with-viewers (v/add-viewers [tap/tap-viewer]) + ::taps (open! id {:title "🚰 Taps" :css-class "p-0"} + (v/with-viewers (v/add-viewers [tap/tap-viewer]) (v/with-viewer taps-viewer @tap/!taps))))) - ([id content] (window! id {} content)) + ([id content] (open! id {} content)) ([id opts content] (webserver/update-window! id (merge opts {:nextjournal/presented (update (v/present content) :nextjournal/css-class #(or % ["px-0"])) :nextjournal/hash (gensym) :nextjournal/fetch-opts {:blob-id (str id)} :nextjournal/blob-id (str id)})))) -(defn destroy-window! [id] (webserver/destroy-window! id)) +(defn destroy! [id] (webserver/destroy-window! id)) -(doseq [w (keys @webserver/!windows)] - (destroy-window! w)) +(defn destroy-all! [] + (doseq [w (keys @webserver/!windows)] + (destroy! w))) -#_(window! ::taps) +#_(open! ::taps) -#_(defn tapped [x] (swap! !taps conj x) (window! ::taps)) +#_(defn tapped [x] (swap! !taps conj x) (open! ::taps)) #_(defonce taps-setup (add-tap tapped)) #_(doseq [f @@(resolve 'clojure.core/tapset)] (remove-tap f)) #_(reset! !taps ()) #_(tap> (range 30)) -#_(window! ::taps) -#_(destroy-window! ::taps) +#_(open! ::taps) +#_(destroy! ::taps) #_(tap> (v/html [:h1 "Ahoi"])) #_(tap> (v/table [[1 2] [3 4]])) -#_(window! ::my-window {:title "🔭 Rear Window"} (table [[1 2] [3 4]])) -#_(window! ::my-window {:title "🔭 Rear Window"} (range 30)) -#_(window! ::my-window {:title "🔭 Rear Window"} (plotly {:data [{:y [1 2 3]}]})) -#_(window! ::my-window-2 {:title "🪟"} (range 100)) -#_(destroy-window! ::my-window) +#_(open! ::my-window {:title "🔭 Rear Window"} (v/table [[1 2] [3 4]])) +#_(open! ::my-window {:title "🔭 Rear Window"} (range 30)) +#_(open! ::my-window {:title "🔭 Rear Window"} (v/plotly {:data [{:y [1 2 3]}]})) +#_(open! ::my-window-2 {:title "🪟"} (range 100)) +#_(destroy! ::my-window) +#_(destroy-all!) From 5620687139330060849491a1ac1ee6ef142709d1 Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Tue, 18 Apr 2023 12:41:43 +0200 Subject: [PATCH 20/38] Fix showing new taps --- src/nextjournal/clerk/window.clj | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/nextjournal/clerk/window.clj b/src/nextjournal/clerk/window.clj index 5a244d494..7279be28a 100644 --- a/src/nextjournal/clerk/window.clj +++ b/src/nextjournal/clerk/window.clj @@ -17,11 +17,14 @@ (v/with-viewer taps-viewer @tap/!taps))))) ([id content] (open! id {} content)) ([id opts content] + ;; TODO: consider calling v/transform-result (webserver/update-window! id (merge opts {:nextjournal/presented (update (v/present content) :nextjournal/css-class #(or % ["px-0"])) :nextjournal/hash (gensym) :nextjournal/fetch-opts {:blob-id (str id)} :nextjournal/blob-id (str id)})))) +(add-watch tap/!taps ::tap-watcher (fn [_ _ _ _] (open! ::taps))) + (defn destroy! [id] (webserver/destroy-window! id)) (defn destroy-all! [] @@ -29,14 +32,9 @@ (destroy! w))) #_(open! ::taps) - -#_(defn tapped [x] (swap! !taps conj x) (open! ::taps)) -#_(defonce taps-setup (add-tap tapped)) - #_(doseq [f @@(resolve 'clojure.core/tapset)] (remove-tap f)) #_(reset! !taps ()) #_(tap> (range 30)) -#_(open! ::taps) #_(destroy! ::taps) #_(tap> (v/html [:h1 "Ahoi"])) #_(tap> (v/table [[1 2] [3 4]])) From c7a57e9ba412f629a9fb465a1ebfdcedafb3e896 Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Tue, 18 Apr 2023 12:46:09 +0200 Subject: [PATCH 21/38] Show usage --- notebooks/tap_window.clj | 9 +++++++-- src/nextjournal/clerk.clj | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/notebooks/tap_window.clj b/notebooks/tap_window.clj index 4baa21b9c..df532e089 100644 --- a/notebooks/tap_window.clj +++ b/notebooks/tap_window.clj @@ -3,10 +3,15 @@ {:nextjournal.clerk/visibility {:code :hide :result :hide} :nextjournal.clerk/no-cache true} (:require [nextjournal.clerk :as clerk] - [nextjournal.clerk.viewer :as v])) + [nextjournal.clerk.tap :as tap] + [nextjournal.clerk.window :as window])) (clerk/window! :my-window (clerk/html [:div.w-8.h-8.bg-green-500])) (comment + (clerk/destroy-window! :my-window) + (clerk/destroy-all!) + (clerk/window! ::window/taps) (tap> (clerk/html [:div.w-8.h-8.bg-green-500])) - (tap> (clerk/plotly {:data [{:x [1 2 3 4]}]}))) + (tap> (clerk/plotly {:data [{:x [1 2 3 4]}]})) + (tap/reset-taps!)) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index 3a054b393..d4a416182 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -72,7 +72,8 @@ #_(show! (java.io.StringReader. ";; # In Memory Notebook 👋\n(+ 41 1)")) (defn window! "todo" [& args] (apply window/open! args)) -(defn destroy-window! "todo" [id] (window/destroy! id)) +(def destroy-window! "todo" window/destroy!) +(def destroy-all-windows! "todo" window/destroy-all!) (defn recompute! "Recomputes the currently visible doc, without parsing it." From 3f698f4d99d47e681975291fdb223e3a2085034c Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Tue, 18 Apr 2023 12:54:58 +0200 Subject: [PATCH 22/38] Remove destroyed windows from state --- src/nextjournal/clerk/webserver.clj | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/nextjournal/clerk/webserver.clj b/src/nextjournal/clerk/webserver.clj index ab5789292..b92ec6172 100644 --- a/src/nextjournal/clerk/webserver.clj +++ b/src/nextjournal/clerk/webserver.clj @@ -197,7 +197,9 @@ (swap! !windows assoc id state) (broadcast! {:type :set-window-state! :id id :state state})) -(defn destroy-window! [id] (broadcast! {:type :destroy-window! :id id})) +(defn destroy-window! [id] + (swap! !windows dissoc id) + (broadcast! {:type :destroy-window! :id id})) (defn broadcast-status! [status] From 61c5504114e45d79cc83d0b22592c13a6a01832f Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Tue, 18 Apr 2023 13:23:54 +0200 Subject: [PATCH 23/38] Fix cyclic deps --- src/nextjournal/clerk.clj | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index d4a416182..18e9bf963 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -13,7 +13,6 @@ [nextjournal.clerk.parser :as parser] [nextjournal.clerk.view :as view] [nextjournal.clerk.viewer :as v] - [nextjournal.clerk.window :as window] [nextjournal.clerk.webserver :as webserver])) (defonce ^:private !show-filter-fn (atom nil)) @@ -71,9 +70,9 @@ #_(show! "https://raw.githubusercontent.com/nextjournal/clerk-demo/main/notebooks/rule_30.clj") #_(show! (java.io.StringReader. ";; # In Memory Notebook 👋\n(+ 41 1)")) -(defn window! "todo" [& args] (apply window/open! args)) -(def destroy-window! "todo" window/destroy!) -(def destroy-all-windows! "todo" window/destroy-all!) +(defn window! "todo" [& args] (apply (requiring-resolve 'nextjournal.clerk.window/open!) args)) +(defn destroy-window! "todo" [id] ((requiring-resolve 'nextjournal.clerk.window/destroy!) id)) +(defn destroy-all-windows! "todo" [] ((requiring-resolve 'nextjournal.clerk.window/destroy-all!))) (defn recompute! "Recomputes the currently visible doc, without parsing it." From fbe650928120b4893ab5be3c88be4ac41e28cd57 Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Tue, 18 Apr 2023 13:26:59 +0200 Subject: [PATCH 24/38] Reimplement switch view for taps window --- src/nextjournal/clerk/window.clj | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/nextjournal/clerk/window.clj b/src/nextjournal/clerk/window.clj index 7279be28a..0fcda9362 100644 --- a/src/nextjournal/clerk/window.clj +++ b/src/nextjournal/clerk/window.clj @@ -5,9 +5,20 @@ (def taps-viewer {:render-fn '(fn [taps opts] - (into [:div] - (nextjournal.clerk.viewer/inspect-children opts) - taps))}) + (let [!view (nextjournal.clerk.render.hooks/use-state :stream)] + [:div + [:div.flex.justify-between.items-center + (into [:div.flex.items-center.font-sans.text-xs.mb-3 [:span.text-slate-500.mr-2 "View-as:"]] + (map (fn [choice] + [:button.px-3.py-1.font-medium.hover:bg-indigo-50.rounded-full.hover:text-indigo-600.transition + {:class (if (= @!view choice) "bg-indigo-100 text-indigo-600" "text-slate-500") + :on-click #(reset! !view choice)} + choice]) [:stream :latest])) + [:button.text-xs.rounded-full.px-3.py-1.border-2.font-sans.hover:bg-slate-100.cursor-pointer + {:on-click #(nextjournal.clerk.render/clerk-eval `(reset-taps!))} "Clear"]] + (into [:div] + (nextjournal.clerk.viewer/inspect-children opts) + (cond->> taps (= :latest @!view) (take 1)))]))}) (defn open! ([id] From c490f3eb39b12e2c00dd0cfeb662c70c85619d03 Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Tue, 18 Apr 2023 13:44:35 +0200 Subject: [PATCH 25/38] Allow to clear taps --- src/nextjournal/clerk/window.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextjournal/clerk/window.clj b/src/nextjournal/clerk/window.clj index 0fcda9362..d525e080e 100644 --- a/src/nextjournal/clerk/window.clj +++ b/src/nextjournal/clerk/window.clj @@ -15,7 +15,7 @@ :on-click #(reset! !view choice)} choice]) [:stream :latest])) [:button.text-xs.rounded-full.px-3.py-1.border-2.font-sans.hover:bg-slate-100.cursor-pointer - {:on-click #(nextjournal.clerk.render/clerk-eval `(reset-taps!))} "Clear"]] + {:on-click #(nextjournal.clerk.render/clerk-eval `(tap/reset-taps!))} "Clear"]] (into [:div] (nextjournal.clerk.viewer/inspect-children opts) (cond->> taps (= :latest @!view) (take 1)))]))}) From 943237c6c76c34e5f23619422e4132b1308875e4 Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Tue, 18 Apr 2023 14:05:25 +0200 Subject: [PATCH 26/38] Persist taps window view choice --- src/nextjournal/clerk/window.clj | 39 +++++++++++++++++--------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/nextjournal/clerk/window.clj b/src/nextjournal/clerk/window.clj index d525e080e..cac414e28 100644 --- a/src/nextjournal/clerk/window.clj +++ b/src/nextjournal/clerk/window.clj @@ -3,29 +3,33 @@ [nextjournal.clerk.viewer :as v] [nextjournal.clerk.webserver :as webserver])) +(declare open!) +(defonce !taps-view (atom :stream)) +(defn set-view! [x] (reset! !taps-view x) (open! ::taps)) + (def taps-viewer - {:render-fn '(fn [taps opts] - (let [!view (nextjournal.clerk.render.hooks/use-state :stream)] - [:div - [:div.flex.justify-between.items-center - (into [:div.flex.items-center.font-sans.text-xs.mb-3 [:span.text-slate-500.mr-2 "View-as:"]] - (map (fn [choice] - [:button.px-3.py-1.font-medium.hover:bg-indigo-50.rounded-full.hover:text-indigo-600.transition - {:class (if (= @!view choice) "bg-indigo-100 text-indigo-600" "text-slate-500") - :on-click #(reset! !view choice)} - choice]) [:stream :latest])) - [:button.text-xs.rounded-full.px-3.py-1.border-2.font-sans.hover:bg-slate-100.cursor-pointer - {:on-click #(nextjournal.clerk.render/clerk-eval `(tap/reset-taps!))} "Clear"]] - (into [:div] - (nextjournal.clerk.viewer/inspect-children opts) - (cond->> taps (= :latest @!view) (take 1)))]))}) + {:render-fn '(fn [taps {:as opts :keys [taps-view]}] + [:div + [:div.flex.justify-between.items-center + (into [:div.flex.items-center.font-sans.text-xs.mb-3 [:span.text-slate-500.mr-2 "View-as:"]] + (map (fn [choice] + [:button.px-3.py-1.font-medium.hover:bg-indigo-50.rounded-full.hover:text-indigo-600.transition + {:class (if (= taps-view choice) "bg-indigo-100 text-indigo-600" "text-slate-500") + :on-click #(nextjournal.clerk.render/clerk-eval `(set-view! ~choice))} + choice]) [:stream :latest])) + [:button.text-xs.rounded-full.px-3.py-1.border-2.font-sans.hover:bg-slate-100.cursor-pointer + {:on-click #(nextjournal.clerk.render/clerk-eval `(tap/reset-taps!))} "Clear"]] + (into [:div] + (nextjournal.clerk.viewer/inspect-children opts) + (cond->> taps (= :latest taps-view) (take 1)))])}) (defn open! ([id] (case id ::taps (open! id {:title "🚰 Taps" :css-class "p-0"} (v/with-viewers (v/add-viewers [tap/tap-viewer]) - (v/with-viewer taps-viewer @tap/!taps))))) + (v/with-viewer taps-viewer {:nextjournal/opts {:taps-view @!taps-view}} + @tap/!taps))))) ([id content] (open! id {} content)) ([id opts content] ;; TODO: consider calling v/transform-result @@ -44,10 +48,9 @@ #_(open! ::taps) #_(doseq [f @@(resolve 'clojure.core/tapset)] (remove-tap f)) -#_(reset! !taps ()) #_(tap> (range 30)) #_(destroy! ::taps) -#_(tap> (v/html [:h1 "Ahoi"])) +#_(tap> (v/plotly {:data [{:y [1 2 3]}]})) #_(tap> (v/table [[1 2] [3 4]])) #_(open! ::my-window {:title "🔭 Rear Window"} (v/table [[1 2] [3 4]])) #_(open! ::my-window {:title "🔭 Rear Window"} (range 30)) From 7e46979b813e833c70408e90e3773e8ac6fa1768 Mon Sep 17 00:00:00 2001 From: Andrea Amantini Date: Tue, 18 Apr 2023 14:22:30 +0200 Subject: [PATCH 27/38] Fix --- notebooks/tap_window.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebooks/tap_window.clj b/notebooks/tap_window.clj index df532e089..817ad869b 100644 --- a/notebooks/tap_window.clj +++ b/notebooks/tap_window.clj @@ -10,7 +10,7 @@ (comment (clerk/destroy-window! :my-window) - (clerk/destroy-all!) + (clerk/destroy-all-windows!) (clerk/window! ::window/taps) (tap> (clerk/html [:div.w-8.h-8.bg-green-500])) (tap> (clerk/plotly {:data [{:x [1 2 3 4]}]})) From bc7a3e2aa12c304f2a4a9221390c6c66a91af407 Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Tue, 18 Apr 2023 14:33:12 +0200 Subject: [PATCH 28/38] More work on tap window design --- src/nextjournal/clerk/tap.clj | 8 +++++--- src/nextjournal/clerk/window.clj | 31 +++++++++++++++++++------------ 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/nextjournal/clerk/tap.clj b/src/nextjournal/clerk/tap.clj index 73a298d1c..b948a2646 100644 --- a/src/nextjournal/clerk/tap.clj +++ b/src/nextjournal/clerk/tap.clj @@ -45,9 +45,11 @@ {:pred (v/get-safe ::val) :render-fn '(fn [{::keys [val tapped-at]} opts] [:div.w-full - [:div.font-sans.bg-slate-50.px-2 - {:class "py-[2px] text-[9px]"} (:nextjournal/value tapped-at)] - [:div.overflow-x-auto [nextjournal.clerk.render/inspect-presented val]]]) + [:div.font-sans.bg-slate-100.py-1 + {:class "px-[8px] text-[11px]"} (:nextjournal/value tapped-at)] + [:div.overflow-x-auto.py-2 + {:class "px-[8px]"} + [nextjournal.clerk.render/inspect-presented val]]]) :transform-fn (fn [{:as wrapped-value :nextjournal/keys [value]}] (-> wrapped-value v/mark-preserve-keys diff --git a/src/nextjournal/clerk/window.clj b/src/nextjournal/clerk/window.clj index cac414e28..a51e230a9 100644 --- a/src/nextjournal/clerk/window.clj +++ b/src/nextjournal/clerk/window.clj @@ -9,27 +9,34 @@ (def taps-viewer {:render-fn '(fn [taps {:as opts :keys [taps-view]}] - [:div - [:div.flex.justify-between.items-center - (into [:div.flex.items-center.font-sans.text-xs.mb-3 [:span.text-slate-500.mr-2 "View-as:"]] + [:div.flex.flex-col + [:div.flex.justify-between.items-center.font-sans.border-b.border-t.shadow.z-1 + {:class "text-[11px] height-[24px] px-[8px]"} + (into [:div.flex.items-center] (map (fn [choice] - [:button.px-3.py-1.font-medium.hover:bg-indigo-50.rounded-full.hover:text-indigo-600.transition - {:class (if (= taps-view choice) "bg-indigo-100 text-indigo-600" "text-slate-500") + [:button.transition-all.mr-2.relative + {:class (str "h-[24px] border-b-2 " + (if (= taps-view choice) + "text-indigo-600 border-indigo-600 font-bold " + "text-slate-500 border-transparent hover:text-indigo-600 ")) :on-click #(nextjournal.clerk.render/clerk-eval `(set-view! ~choice))} - choice]) [:stream :latest])) - [:button.text-xs.rounded-full.px-3.py-1.border-2.font-sans.hover:bg-slate-100.cursor-pointer - {:on-click #(nextjournal.clerk.render/clerk-eval `(tap/reset-taps!))} "Clear"]] - (into [:div] + [:span.relative {:class "-bottom-[2px]"} (clojure.string/capitalize (name choice))]]) + [:stream :latest])) + [:button.text-slate-500.hover:text-indigo-600 + {:on-click #(nextjournal.clerk.render/clerk-eval `(tap/reset-taps!))} + "Clear"]] + (into [:div.overflow-auto + {:style {:height "calc(100% - 40px)"}}] (nextjournal.clerk.viewer/inspect-children opts) (cond->> taps (= :latest taps-view) (take 1)))])}) (defn open! ([id] (case id - ::taps (open! id {:title "🚰 Taps" :css-class "p-0"} + ::taps (open! id {:title "🚰 Taps" :css-class "p-0 relative overflow-auto"} (v/with-viewers (v/add-viewers [tap/tap-viewer]) - (v/with-viewer taps-viewer {:nextjournal/opts {:taps-view @!taps-view}} - @tap/!taps))))) + (v/with-viewer taps-viewer {:nextjournal/opts {:taps-view @!taps-view}} + @tap/!taps))))) ([id content] (open! id {} content)) ([id opts content] ;; TODO: consider calling v/transform-result From 34fc068147464ec17c8556daa138c374aaf69c30 Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Tue, 18 Apr 2023 14:34:49 +0200 Subject: [PATCH 29/38] Cleaner switcher --- notebooks/tap_window.clj | 20 ++++++++++---------- src/nextjournal/clerk/window.clj | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/notebooks/tap_window.clj b/notebooks/tap_window.clj index 817ad869b..dc8b101ba 100644 --- a/notebooks/tap_window.clj +++ b/notebooks/tap_window.clj @@ -3,15 +3,15 @@ {:nextjournal.clerk/visibility {:code :hide :result :hide} :nextjournal.clerk/no-cache true} (:require [nextjournal.clerk :as clerk] - [nextjournal.clerk.tap :as tap] - [nextjournal.clerk.window :as window])) + [nextjournal.clerk.window :as window] + [nextjournal.clerk.tap :as tap])) -(clerk/window! :my-window (clerk/html [:div.w-8.h-8.bg-green-500])) +#_(clerk/window! :my-window (clerk/html [:div.w-8.h-8.bg-green-500])) -(comment - (clerk/destroy-window! :my-window) - (clerk/destroy-all-windows!) - (clerk/window! ::window/taps) - (tap> (clerk/html [:div.w-8.h-8.bg-green-500])) - (tap> (clerk/plotly {:data [{:x [1 2 3 4]}]})) - (tap/reset-taps!)) +#_(comment + (clerk/destroy-window! :my-window) + (clerk/destroy-all-windows!) + (clerk/window! ::window/taps) + (tap> (clerk/html [:div.w-8.h-8.bg-green-500])) + (tap> (clerk/plotly {:data [{:x [1 2 3 4]}]})) + (tap/reset-taps!)) diff --git a/src/nextjournal/clerk/window.clj b/src/nextjournal/clerk/window.clj index a51e230a9..478a89dca 100644 --- a/src/nextjournal/clerk/window.clj +++ b/src/nextjournal/clerk/window.clj @@ -15,12 +15,12 @@ (into [:div.flex.items-center] (map (fn [choice] [:button.transition-all.mr-2.relative - {:class (str "h-[24px] border-b-2 " + {:class (str "h-[24px] " (if (= taps-view choice) - "text-indigo-600 border-indigo-600 font-bold " - "text-slate-500 border-transparent hover:text-indigo-600 ")) + "text-indigo-600 font-bold " + "text-slate-500 hover:text-indigo-600 ")) :on-click #(nextjournal.clerk.render/clerk-eval `(set-view! ~choice))} - [:span.relative {:class "-bottom-[2px]"} (clojure.string/capitalize (name choice))]]) + (clojure.string/capitalize (name choice))]) [:stream :latest])) [:button.text-slate-500.hover:text-indigo-600 {:on-click #(nextjournal.clerk.render/clerk-eval `(tap/reset-taps!))} From 25cf8b6286ada5fed11fa2e58a34a291e8f1c3a9 Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Tue, 18 Apr 2023 15:12:56 +0200 Subject: [PATCH 30/38] Naming: close-window, close-all-windows, ::clerk/taps; use clerk-eval to close --- notebooks/tap_window.clj | 6 ++--- scratch_window.clj | 36 +++++++++++++++++++++++++++++ src/nextjournal/clerk.clj | 4 ++-- src/nextjournal/clerk/render.cljs | 8 ++++--- src/nextjournal/clerk/webserver.clj | 4 ++-- src/nextjournal/clerk/window.clj | 21 +++++++++-------- 6 files changed, 59 insertions(+), 20 deletions(-) create mode 100644 scratch_window.clj diff --git a/notebooks/tap_window.clj b/notebooks/tap_window.clj index dc8b101ba..c1b1ec831 100644 --- a/notebooks/tap_window.clj +++ b/notebooks/tap_window.clj @@ -9,9 +9,9 @@ #_(clerk/window! :my-window (clerk/html [:div.w-8.h-8.bg-green-500])) #_(comment - (clerk/destroy-window! :my-window) - (clerk/destroy-all-windows!) - (clerk/window! ::window/taps) + (clerk/close-window! :my-window) + (clerk/close-all-windows!) + (clerk/window! ::clerk/taps) (tap> (clerk/html [:div.w-8.h-8.bg-green-500])) (tap> (clerk/plotly {:data [{:x [1 2 3 4]}]})) (tap/reset-taps!)) diff --git a/scratch_window.clj b/scratch_window.clj new file mode 100644 index 000000000..b47aca4fe --- /dev/null +++ b/scratch_window.clj @@ -0,0 +1,36 @@ +;; # 🪲Debug +(ns scratch-window + {:nextjournal.clerk/visibility {:code :hide :result :hide} + :nextjournal.clerk/no-cache true} + (:require [nextjournal.clerk :as clerk] + [nextjournal.clerk.viewer :as v])) + +(def window-viewer + {:render-fn '(fn [{:keys [vals]} opts] + [nextjournal.clerk.render.window/show + (into [:div] + (map (fn [v] + [:div.mb-4.pb-4.border-b + [nextjournal.clerk.render/inspect-presented v]])) + (:nextjournal/value vals))]) + :transform-fn v/mark-preserve-keys}) + +(defonce !taps (atom '())) + +(defonce taps-setup (add-tap (fn [x] + (swap! !taps conj x) + (clerk/recompute!)))) + +^{::clerk/visibility {:result :show}} +(clerk/with-viewer window-viewer + {:vals @!taps}) + +(comment + (tap> (clerk/html [:div.w-8.h-8.bg-green-500])) + (tap> (clerk/plotly {:data [{:x [1 2 3 4]}]}))) + +(clerk/window! ::clerk/taps) +(clerk/destroy-window ::clerk/taps) +(clerk/list-windows) + +(clerk/window! :test (range 100)) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index 18e9bf963..114f42d82 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -71,8 +71,8 @@ #_(show! (java.io.StringReader. ";; # In Memory Notebook 👋\n(+ 41 1)")) (defn window! "todo" [& args] (apply (requiring-resolve 'nextjournal.clerk.window/open!) args)) -(defn destroy-window! "todo" [id] ((requiring-resolve 'nextjournal.clerk.window/destroy!) id)) -(defn destroy-all-windows! "todo" [] ((requiring-resolve 'nextjournal.clerk.window/destroy-all!))) +(defn close-window! "todo" [id] ((requiring-resolve 'nextjournal.clerk.window/close!) id)) +(defn close-all-windows! "todo" [] ((requiring-resolve 'nextjournal.clerk.window/close-all!))) (defn recompute! "Recomputes the currently visible doc, without parsing it." diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index 899c853df..6810e8e21 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -600,6 +600,8 @@ (swap! !state update :desc viewer/merge-presentations more fetch-opts))))} [inspect-presented (:desc @!state)]])) +(declare clerk-eval) + (defn root [] [:<> [inspect-presented @!doc] @@ -616,7 +618,7 @@ [window/show [render-result state {}] (-> state - (assoc :id id :on-close #(swap! !windows dissoc id)) + (assoc :id id :on-close #(clerk-eval `(nextjournal.clerk.window/close! ~id))) (dissoc :nextjournal/presented))])) @!windows)]) @@ -711,13 +713,13 @@ (js/console.warn :process-eval-reply!/not-found :eval-id eval-id :keys (keys @!pending-clerk-eval-replies)))) (defn set-window-state! [{:keys [id state]}] (swap! !windows assoc id state)) -(defn destroy-window! [{:keys [id]}] (swap! !windows dissoc id)) +(defn close-window! [{:keys [id]}] (swap! !windows dissoc id)) (defn ^:export dispatch [{:as msg :keys [type]}] (let [dispatch-fn (get {:patch-state! patch-state! :set-state! set-state! :set-window-state! set-window-state! - :destroy-window! destroy-window! + :close-window! close-window! :eval-reply process-eval-reply!} type (fn [_] diff --git a/src/nextjournal/clerk/webserver.clj b/src/nextjournal/clerk/webserver.clj index b92ec6172..45f61c35d 100644 --- a/src/nextjournal/clerk/webserver.clj +++ b/src/nextjournal/clerk/webserver.clj @@ -197,9 +197,9 @@ (swap! !windows assoc id state) (broadcast! {:type :set-window-state! :id id :state state})) -(defn destroy-window! [id] +(defn close-window! [id] (swap! !windows dissoc id) - (broadcast! {:type :destroy-window! :id id})) + (broadcast! {:type :close-window! :id id})) (defn broadcast-status! [status] diff --git a/src/nextjournal/clerk/window.clj b/src/nextjournal/clerk/window.clj index 478a89dca..3ad29e9c6 100644 --- a/src/nextjournal/clerk/window.clj +++ b/src/nextjournal/clerk/window.clj @@ -1,5 +1,6 @@ (ns nextjournal.clerk.window - (:require [nextjournal.clerk.tap :as tap] + (:require [nextjournal.clerk :as clerk] + [nextjournal.clerk.tap :as tap] [nextjournal.clerk.viewer :as v] [nextjournal.clerk.webserver :as webserver])) @@ -33,7 +34,7 @@ (defn open! ([id] (case id - ::taps (open! id {:title "🚰 Taps" :css-class "p-0 relative overflow-auto"} + ::clerk/taps (open! id {:title "🚰 Taps" :css-class "p-0 relative overflow-auto"} (v/with-viewers (v/add-viewers [tap/tap-viewer]) (v/with-viewer taps-viewer {:nextjournal/opts {:taps-view @!taps-view}} @tap/!taps))))) @@ -45,23 +46,23 @@ :nextjournal/fetch-opts {:blob-id (str id)} :nextjournal/blob-id (str id)})))) -(add-watch tap/!taps ::tap-watcher (fn [_ _ _ _] (open! ::taps))) +(add-watch tap/!taps ::tap-watcher (fn [_ _ _ _] (open! ::clerk/taps))) -(defn destroy! [id] (webserver/destroy-window! id)) +(defn close! [id] (webserver/close-window! id)) -(defn destroy-all! [] +(defn close-all! [] (doseq [w (keys @webserver/!windows)] - (destroy! w))) + (close! w))) -#_(open! ::taps) +#_(open! ::clerk/taps) #_(doseq [f @@(resolve 'clojure.core/tapset)] (remove-tap f)) #_(tap> (range 30)) -#_(destroy! ::taps) +#_(close! ::clerk/taps) #_(tap> (v/plotly {:data [{:y [1 2 3]}]})) #_(tap> (v/table [[1 2] [3 4]])) #_(open! ::my-window {:title "🔭 Rear Window"} (v/table [[1 2] [3 4]])) #_(open! ::my-window {:title "🔭 Rear Window"} (range 30)) #_(open! ::my-window {:title "🔭 Rear Window"} (v/plotly {:data [{:y [1 2 3]}]})) #_(open! ::my-window-2 {:title "🪟"} (range 100)) -#_(destroy! ::my-window) -#_(destroy-all!) +#_(close! ::my-window) +#_(close-all!) From 462cbd750a19f0bfca90e5afcedcf6c32dd47dd6 Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Tue, 18 Apr 2023 15:21:30 +0200 Subject: [PATCH 31/38] =?UTF-8?q?Key=20windows=20with=20id=20so=20they=20d?= =?UTF-8?q?on=E2=80=99t=20take=20over=20themselves?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/nextjournal/clerk/render.cljs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index 6810e8e21..da93c2f94 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -615,6 +615,7 @@ [inspect-presented @!error]]) (into [:<>] (map (fn [[id state]] + ^{:key id} [window/show [render-result state {}] (-> state From 456fae4ad4decf2bcfb992dc4e2df69ad9db30d9 Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Tue, 18 Apr 2023 15:25:56 +0200 Subject: [PATCH 32/38] Fix switching --- src/nextjournal/clerk/window.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextjournal/clerk/window.clj b/src/nextjournal/clerk/window.clj index 3ad29e9c6..3e213507b 100644 --- a/src/nextjournal/clerk/window.clj +++ b/src/nextjournal/clerk/window.clj @@ -6,7 +6,7 @@ (declare open!) (defonce !taps-view (atom :stream)) -(defn set-view! [x] (reset! !taps-view x) (open! ::taps)) +(defn set-view! [x] (reset! !taps-view x) (open! ::clerk/taps)) (def taps-viewer {:render-fn '(fn [taps {:as opts :keys [taps-view]}] From 985ff1b11b6411c0d808ee0e9633ac7fbd19ab8b Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Tue, 18 Apr 2023 15:33:32 +0200 Subject: [PATCH 33/38] Keep windows open when reloading page or showing different notebooks --- src/nextjournal/clerk/webserver.clj | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/nextjournal/clerk/webserver.clj b/src/nextjournal/clerk/webserver.clj index 45f61c35d..a884d5510 100644 --- a/src/nextjournal/clerk/webserver.clj +++ b/src/nextjournal/clerk/webserver.clj @@ -126,8 +126,22 @@ #_(pr-str (read-msg "#viewer-eval (resolve 'clojure.core/inc)")) +(defn update-window! [id state] + (swap! !windows assoc id state) + (broadcast! {:type :set-window-state! :id id :state state})) + +(defn update-windows! [] + (doseq [[id state] @!windows] + (update-window! id state))) + +(defn close-window! [id] + (swap! !windows dissoc id) + (broadcast! {:type :close-window! :id id})) + (def ws-handlers - {:on-open (fn [ch] (swap! !clients conj ch)) + {:on-open (fn [ch] + (swap! !clients conj ch) + (update-windows!)) :on-close (fn [ch _reason] (swap! !clients disj ch)) :on-receive (fn [sender-ch edn-string] (binding [*ns* (or (:ns @!doc) @@ -193,15 +207,6 @@ #_(update-doc! (help-doc)) -(defn update-window! [id state] - (swap! !windows assoc id state) - (broadcast! {:type :set-window-state! :id id :state state})) - -(defn close-window! [id] - (swap! !windows dissoc id) - (broadcast! {:type :close-window! :id id})) - - (defn broadcast-status! [status] ;; avoid editscript diff but use manual patch to just replace `:status` in doc (broadcast! {:type :patch-state! :patch [[[:status] :r status]]})) From 45aa8b50ab6b6cd47dbe67ee47ebbb3d8ae891a4 Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Wed, 19 Apr 2023 11:50:24 +0200 Subject: [PATCH 34/38] Add intro to windows --- notebooks/tap_window.clj | 49 +++++++++++++++++++----- src/nextjournal/clerk/render/window.cljs | 6 +-- src/nextjournal/clerk/tap.clj | 2 +- src/nextjournal/clerk/window.clj | 2 +- 4 files changed, 45 insertions(+), 14 deletions(-) diff --git a/notebooks/tap_window.clj b/notebooks/tap_window.clj index c1b1ec831..36da71267 100644 --- a/notebooks/tap_window.clj +++ b/notebooks/tap_window.clj @@ -3,15 +3,46 @@ {:nextjournal.clerk/visibility {:code :hide :result :hide} :nextjournal.clerk/no-cache true} (:require [nextjournal.clerk :as clerk] - [nextjournal.clerk.window :as window] [nextjournal.clerk.tap :as tap])) -#_(clerk/window! :my-window (clerk/html [:div.w-8.h-8.bg-green-500])) +{::clerk/visibility {:code :show}} -#_(comment - (clerk/close-window! :my-window) - (clerk/close-all-windows!) - (clerk/window! ::clerk/taps) - (tap> (clerk/html [:div.w-8.h-8.bg-green-500])) - (tap> (clerk/plotly {:data [{:x [1 2 3 4]}]})) - (tap/reset-taps!)) +;; Clerk windows are draggable, resizable and dockable containers that are floating on top of other content. Windows make it easy to show arbitary content, independent of a notebook, while still getting all the benefits of Clerk viewers. This can be nice for debugging. For example you could use it to inspect a data structure in one window and show the same data structure as a graph in a second window. + +;; Windows have identity. In order to spawn one, you have to call something like: + +(clerk/window! :my-window {:foo (vec (repeat 2 {:baz (range 30) :fooze (range 40)})) :bar (range 20)}) + +;; This creates a window with a `:my-window` id. The id makes the window addressable and, as such, allows to update its contents from the REPL. For example, you can call … + +(clerk/window! :my-window {:title "A debug window"} (zipmap (range 1000) (map #(* % %) (range 1000)))) + +;; … to replace the contens of `:my-window`. The window itself will not be reinstantiated. The example also shows that `window!` takes an optional second `opts` argument that can be used to give it a custom title. + +;; Windows have a dedicated close button but you can also use the id to close it from the REPL, e.g. + +(clerk/close-window! :my-window) + +;; Finally, there's also special `::clerk/taps` window that doesn't require you to set any content. Instead, it will show you a stream of taps (independant of the notebooks you are working in). So, whenever you `tap>` something, the Taps window will show it when it's open: + +(comment + (clerk/window! ::clerk/taps)) + +;; Mind that windows live outside notebooks and once you spawn one, it shows until you close it again, even if you reload the page or show a different notebook! + +(comment + (clerk/window! :test {:title "My Super-Duper Window"} (range 100)) + (clerk/window! :test (clerk/html [:div.w-8.h-8.bg-green-500])) + (clerk/close-window! :test) + (clerk/close-all-windows!) + (clerk/window! ::clerk/taps) + (tap> (clerk/html [:div.w-8.h-8.bg-green-500])) + (tap> (clerk/vl {:description "A simple bar chart with embedded data." + :data {:values [{:a "A" :b 28} {:a "B" :b 55} {:a "C" :b 43} + {:a "D" :b 91} {:a "E" :b 81} {:a "F" :b 53} + {:a "G" :b 19} {:a "H" :b 87} {:a "I" :b 52}]} + :mark "bar" + :encoding {:x {:field "a" :type "nominal" :axis {:labelAngle 0}} + :y {:field "b" :type "quantitative"}}})) + (tap> 1) + (tap/reset-taps!)) diff --git a/src/nextjournal/clerk/render/window.cljs b/src/nextjournal/clerk/render/window.cljs index fc912f098..c72f1715b 100644 --- a/src/nextjournal/clerk/render/window.cljs +++ b/src/nextjournal/clerk/render/window.cljs @@ -65,18 +65,18 @@ #(js/removeEventListener "mousemove" handle-mouse-move))) [!mouse-down on-drag]) [:div.bg-slate-100.hover:bg-slate-200.dark:bg-slate-800.dark:hover:bg-slate-700.cursor-move.w-full.rounded-t-lg.flex-shrink-0.leading-none.flex.items-center.justify-between - {:class (if name "h-[20px] " "h-[14px] ") + {:class (if name "h-[24px] " "h-[14px] ") :on-mouse-down (fn [event] (on-drag-start) (reset! !mouse-down {:start-x (.-screenX event) :start-y (.-screenY event)}))} (when name [:span.font-sans.font-medium.text-slate-700 - {:class "text-[11px] ml-[8px] "} + {:class "text-[12px] ml-[8px] "} (or title id)]) (when on-close [:button.text-slate-600.hover:text-slate-900.hover:bg-slate-300.rounded-tr-lg.flex.items-center.justify-center {:on-click on-close - :class "w-[20px] h-[20px]"} + :class "w-[24px] h-[24px]"} [:svg {:xmlns "http://www.w3.org/2000/svg" :fill "none" :viewBox "0 0 24 24" :stroke-width "1.5" :stroke "currentColor" :class "w-3 h-3"} [:path {:stroke-linecap "round" :stroke-linejoin "round" :d "M6 18L18 6M6 6l12 12"}]]])])) diff --git a/src/nextjournal/clerk/tap.clj b/src/nextjournal/clerk/tap.clj index b948a2646..5d9eb4b38 100644 --- a/src/nextjournal/clerk/tap.clj +++ b/src/nextjournal/clerk/tap.clj @@ -45,7 +45,7 @@ {:pred (v/get-safe ::val) :render-fn '(fn [{::keys [val tapped-at]} opts] [:div.w-full - [:div.font-sans.bg-slate-100.py-1 + [:div.font-sans.bg-slate-50.py-1.text-slate-600.tracking-wide.border-t.border-b {:class "px-[8px] text-[11px]"} (:nextjournal/value tapped-at)] [:div.overflow-x-auto.py-2 {:class "px-[8px]"} diff --git a/src/nextjournal/clerk/window.clj b/src/nextjournal/clerk/window.clj index 3e213507b..e5cc83c2f 100644 --- a/src/nextjournal/clerk/window.clj +++ b/src/nextjournal/clerk/window.clj @@ -11,7 +11,7 @@ (def taps-viewer {:render-fn '(fn [taps {:as opts :keys [taps-view]}] [:div.flex.flex-col - [:div.flex.justify-between.items-center.font-sans.border-b.border-t.shadow.z-1 + [:div.flex.justify-between.items-center.font-sans.border-t.shadow.z-1 {:class "text-[11px] height-[24px] px-[8px]"} (into [:div.flex.items-center] (map (fn [choice] From 0bf559b060a9705d4ee6692797092cb1dc31d327 Mon Sep 17 00:00:00 2001 From: Philippa Markovics Date: Tue, 20 Jun 2023 18:13:00 +0200 Subject: [PATCH 35/38] sci repl window WIP --- notebooks/tap_window.clj | 3 +- src/nextjournal/clerk/render/window.cljs | 72 +++++++++- .../clerk/sci_env/completions.cljs | 127 ++++++++++++++++++ src/nextjournal/clerk/window.clj | 9 +- 4 files changed, 205 insertions(+), 6 deletions(-) create mode 100644 src/nextjournal/clerk/sci_env/completions.cljs diff --git a/notebooks/tap_window.clj b/notebooks/tap_window.clj index 36da71267..9ff71bb4e 100644 --- a/notebooks/tap_window.clj +++ b/notebooks/tap_window.clj @@ -45,4 +45,5 @@ :encoding {:x {:field "a" :type "nominal" :axis {:labelAngle 0}} :y {:field "b" :type "quantitative"}}})) (tap> 1) - (tap/reset-taps!)) + (tap/reset-taps!) + (clerk/window! ::clerk/sci-repl)) diff --git a/src/nextjournal/clerk/render/window.cljs b/src/nextjournal/clerk/render/window.cljs index c72f1715b..e4c65f6aa 100644 --- a/src/nextjournal/clerk/render/window.cljs +++ b/src/nextjournal/clerk/render/window.cljs @@ -1,6 +1,16 @@ (ns nextjournal.clerk.render.window - (:require [applied-science.js-interop :as j] - [nextjournal.clerk.render.hooks :as hooks])) + (:require ["@codemirror/view" :as cm-view :refer [keymap highlightActiveLine]] + [applied-science.js-interop :as j] + [clojure.string :as str] + [nextjournal.clerk.render.code :as code] + [nextjournal.clerk.render.hooks :as hooks] + [nextjournal.clerk.sci-env.completions :as completions] + [nextjournal.clojure-mode.extensions.eval-region :as eval-region] + [sci.core :as sci] + [sci.ctx-store])) + +(defn inspect-fn [] + @(resolve 'nextjournal.clerk.render/inspect)) (defn resizer [{:keys [on-resize on-resize-start on-resize-end] :or {on-resize-start #() on-resize-end #()}}] (let [!direction (hooks/use-state nil) @@ -122,6 +132,64 @@ (j/assoc-in! [:style :top] "5px") (j/assoc-in! [:style :left] "5px"))) +(defn eval-string + ([source] (sci/eval-string* (sci.ctx-store/get-ctx) source)) + ([ctx source] + (when-some [code (not-empty (str/trim source))] + (try {:result (sci/eval-string* ctx code)} + (catch js/Error e + {:error (str (.-message e))}))))) + +(j/defn eval-at-cursor [on-result ^:js {:keys [state]}] + (some->> (eval-region/cursor-node-string state) + (eval-string) + (on-result)) + true) + +(j/defn eval-top-level [on-result ^:js {:keys [state]}] + (some->> (eval-region/top-level-string state) + (eval-string) + (on-result)) + true) + +(j/defn eval-cell [on-result ^:js {:keys [state]}] + (-> (.-doc state) + (str) + (eval-string) + (on-result)) + true) + +(defn sci-extension [{:keys [modifier on-result]}] + (.of cm-view/keymap + (j/lit + [{:key "Mod-Enter" + :run (partial eval-cell on-result)} + {:key (str modifier "-Enter") + :shift (partial eval-top-level on-result) + :run (partial eval-at-cursor on-result)}]))) + +(defn sci-repl [] + (let [!code-str (hooks/use-state "") + !results (hooks/use-state ())] + [:div.flex.flex-col.bg-gray-50 + [:div.w-full.border-t.border-b.border-slate-300.shadow-inner.px-2.py-1.bg-slate-100 + [code/editor !code-str {:extensions #js [(.of keymap nextjournal.clojure-mode.keymap/paredit) + completions/completion-source + (sci-extension {:modifier "Alt" + :on-result #(swap! !results conj {:result % + :evaled-at (js/Date.) + :react-key (gensym)})})]}]] + (into + [:div.w-full.flex-auto.overflow-auto] + (map (fn [{:as r :keys [result evaled-at react-key]}] + ^{:key react-key} + [:div.border-b.px-2.py-2.text-xs.font-mono + [:div.font-mono.text-slate-40.flex-shrink-0.text-right + {:class "text-[9px]"} + (str (first (.. evaled-at toTimeString (split " "))) ":" (.getMilliseconds evaled-at))] + [(inspect-fn) result]])) + @!results)])) + (defn show ([content] (show content {})) ([content {:as opts :keys [css-class]}] diff --git a/src/nextjournal/clerk/sci_env/completions.cljs b/src/nextjournal/clerk/sci_env/completions.cljs new file mode 100644 index 000000000..cad16c02e --- /dev/null +++ b/src/nextjournal/clerk/sci_env/completions.cljs @@ -0,0 +1,127 @@ +(ns nextjournal.clerk.sci-env.completions + (:require ["@codemirror/autocomplete" :as cm-autocomplete :refer [CompletionContext]] + ["@codemirror/language" :as cm-lang] + [clojure.string :as str] + [goog.object :as gobject] + [sci.core :as sci] + [sci.ctx-store])) + +(defn format [fmt-str x] + (str/replace fmt-str "%s" x)) + +(defn fully-qualified-syms [ctx ns-sym] + (let [syms (sci/eval-string* ctx (format "(keys (ns-map '%s))" ns-sym)) + sym-strs (map #(str "`" %) syms) + sym-expr (str "[" (str/join " " sym-strs) "]") + syms (sci/eval-string* ctx sym-expr) + syms (remove #(str/starts-with? (str %) "nbb.internal") syms)] + syms)) + +(defn- ns-imports->completions [ctx query-ns query] + (let [[_ns-part name-part] (str/split query #"/") + resolved (sci/eval-string* ctx + (pr-str `(let [resolved# (resolve '~query-ns)] + (when-not (var? resolved#) + resolved#))))] + (when resolved + (when-let [[prefix imported] (if name-part + (let [ends-with-dot? (str/ends-with? name-part ".") + fields (str/split name-part #"\.") + fields (if ends-with-dot? + fields + (butlast fields))] + [(str query-ns "/" (when (seq fields) + (let [joined (str/join "." fields)] + (str joined ".")))) + (apply gobject/getValueByKeys resolved + fields)]) + [(str query-ns "/") resolved])] + (let [props (loop [obj imported + props []] + (if obj + (recur (js/Object.getPrototypeOf obj) + (into props (js/Object.getOwnPropertyNames obj))) + props)) + completions (map (fn [k] + [nil (str prefix k)]) props)] + completions))))) + +(defn- match [_alias->ns ns->alias query [sym-ns sym-name qualifier]] + (let [pat (re-pattern query)] + (or (when (and (= :unqualified qualifier) (re-find pat sym-name)) + [sym-ns sym-name]) + (when sym-ns + (or (when (re-find pat (str (get ns->alias (symbol sym-ns)) "/" sym-name)) + [sym-ns (str (get ns->alias (symbol sym-ns)) "/" sym-name)]) + (when (re-find pat (str sym-ns "/" sym-name)) + [sym-ns (str sym-ns "/" sym-name)])))))) + +(defn completions [{:keys [ctx] + ns-str :ns + :as request}] + (js/console.log "request" request) + (try + (let [sci-ns (when ns-str + (sci/find-ns ctx (symbol ns-str)))] + (sci/binding [sci/ns (or sci-ns @sci/ns)] + (if-let [query (or (:symbol request) + (:prefix request))] + (let [has-namespace? (str/includes? query "/") + query-ns (when has-namespace? (some-> (str/split query #"/") + first symbol)) + from-current-ns (fully-qualified-syms ctx (sci/eval-string* ctx "(ns-name *ns*)")) + from-current-ns (map (fn [sym] + [(namespace sym) (name sym) :unqualified]) + from-current-ns) + alias->ns (sci/eval-string* ctx "(let [m (ns-aliases *ns*)] (zipmap (keys m) (map ns-name (vals m))))") + ns->alias (zipmap (vals alias->ns) (keys alias->ns)) + from-aliased-nss (doall (mapcat + (fn [alias] + (let [ns (get alias->ns alias) + syms (sci/eval-string* ctx (format "(keys (ns-publics '%s))" ns))] + (map (fn [sym] + [(str ns) (str sym) :qualified]) + syms))) + (keys alias->ns))) + all-namespaces (->> (sci/eval-string* ctx "(all-ns)") + (map (fn [ns] + [(str ns) nil :qualified]))) + from-imports (when has-namespace? (ns-imports->completions ctx query-ns query)) + fully-qualified-names (when-not from-imports + (when has-namespace? + (let [ns (get alias->ns query-ns query-ns) + syms (sci/eval-string* ctx (format "(and (find-ns '%s) + (keys (ns-publics '%s)))" + ns))] + (map (fn [sym] + [(str ns) (str sym) :qualified]) + syms)))) + svs (concat from-current-ns from-aliased-nss all-namespaces fully-qualified-names) + completions (keep (fn [entry] + (match alias->ns ns->alias query entry)) + svs) + completions (concat completions from-imports) + completions (->> (map (fn [[namespace name]] + (cond-> {"candidate" (str name)} + namespace (assoc "ns" (str namespace)))) + completions) + distinct vec)] + {:completions completions + :status ["done"]}) + {:status ["done"]}))) + (catch :default e + (js/console.error "ERROR" e) + {:completions [] + :status ["done"]}))) + +(defn autocomplete [^js context] + (let [node-before (.. (cm-lang/syntaxTree (.-state context)) (resolveInner (.-pos context) -1)) + text-before (.. context -state (sliceDoc (.-from node-before) (.-pos context)))] + #js {:from (.-from node-before) + :options (clj->js (map + (fn [{:strs [candidate]}] + (doto {:label candidate} prn)) + (:completions (completions {:ctx (sci.ctx-store/get-ctx) :ns "user" :symbol text-before}))))})) + +(def completion-source + (cm-autocomplete/autocompletion #js {:override #js [autocomplete]})) diff --git a/src/nextjournal/clerk/window.clj b/src/nextjournal/clerk/window.clj index e5cc83c2f..6489521cf 100644 --- a/src/nextjournal/clerk/window.clj +++ b/src/nextjournal/clerk/window.clj @@ -35,9 +35,12 @@ ([id] (case id ::clerk/taps (open! id {:title "🚰 Taps" :css-class "p-0 relative overflow-auto"} - (v/with-viewers (v/add-viewers [tap/tap-viewer]) - (v/with-viewer taps-viewer {:nextjournal/opts {:taps-view @!taps-view}} - @tap/!taps))))) + (v/with-viewers (v/add-viewers [tap/tap-viewer]) + (v/with-viewer taps-viewer {:nextjournal/opts {:taps-view @!taps-view}} + @tap/!taps))) + ::clerk/sci-repl (open! id {:title "SCI REPL" :css-class "p-0 relative overflow-auto"} + (v/with-viewer {:render-fn 'nextjournal.clerk.render.window/sci-repl + :transform-fn clerk/mark-presented} nil)))) ([id content] (open! id {} content)) ([id opts content] ;; TODO: consider calling v/transform-result From cc90f9034ec68415f1d69e3f11bd155d49ee964a Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Wed, 21 Jun 2023 08:51:58 +0200 Subject: [PATCH 36/38] Fix lint warnings --- src/nextjournal/clerk/render/window.cljs | 5 +++-- src/nextjournal/clerk/sci_env/completions.cljs | 2 +- src/nextjournal/clerk/window.clj | 14 +++++++------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/nextjournal/clerk/render/window.cljs b/src/nextjournal/clerk/render/window.cljs index e4c65f6aa..bda933c12 100644 --- a/src/nextjournal/clerk/render/window.cljs +++ b/src/nextjournal/clerk/render/window.cljs @@ -1,10 +1,11 @@ (ns nextjournal.clerk.render.window - (:require ["@codemirror/view" :as cm-view :refer [keymap highlightActiveLine]] + (:require ["@codemirror/view" :as cm-view :refer [keymap]] [applied-science.js-interop :as j] [clojure.string :as str] [nextjournal.clerk.render.code :as code] [nextjournal.clerk.render.hooks :as hooks] [nextjournal.clerk.sci-env.completions :as completions] + [nextjournal.clojure-mode.keymap :as clojure-mode.keymap] [nextjournal.clojure-mode.extensions.eval-region :as eval-region] [sci.core :as sci] [sci.ctx-store])) @@ -173,7 +174,7 @@ !results (hooks/use-state ())] [:div.flex.flex-col.bg-gray-50 [:div.w-full.border-t.border-b.border-slate-300.shadow-inner.px-2.py-1.bg-slate-100 - [code/editor !code-str {:extensions #js [(.of keymap nextjournal.clojure-mode.keymap/paredit) + [code/editor !code-str {:extensions #js [(.of keymap clojure-mode.keymap/paredit) completions/completion-source (sci-extension {:modifier "Alt" :on-result #(swap! !results conj {:result % diff --git a/src/nextjournal/clerk/sci_env/completions.cljs b/src/nextjournal/clerk/sci_env/completions.cljs index cad16c02e..0262ed523 100644 --- a/src/nextjournal/clerk/sci_env/completions.cljs +++ b/src/nextjournal/clerk/sci_env/completions.cljs @@ -1,5 +1,5 @@ (ns nextjournal.clerk.sci-env.completions - (:require ["@codemirror/autocomplete" :as cm-autocomplete :refer [CompletionContext]] + (:require ["@codemirror/autocomplete" :as cm-autocomplete] ["@codemirror/language" :as cm-lang] [clojure.string :as str] [goog.object :as gobject] diff --git a/src/nextjournal/clerk/window.clj b/src/nextjournal/clerk/window.clj index 6489521cf..a608836c9 100644 --- a/src/nextjournal/clerk/window.clj +++ b/src/nextjournal/clerk/window.clj @@ -34,13 +34,13 @@ (defn open! ([id] (case id - ::clerk/taps (open! id {:title "🚰 Taps" :css-class "p-0 relative overflow-auto"} - (v/with-viewers (v/add-viewers [tap/tap-viewer]) - (v/with-viewer taps-viewer {:nextjournal/opts {:taps-view @!taps-view}} - @tap/!taps))) - ::clerk/sci-repl (open! id {:title "SCI REPL" :css-class "p-0 relative overflow-auto"} - (v/with-viewer {:render-fn 'nextjournal.clerk.render.window/sci-repl - :transform-fn clerk/mark-presented} nil)))) + :nextjournal.clerk/taps (open! id {:title "🚰 Taps" :css-class "p-0 relative overflow-auto"} + (v/with-viewers (v/add-viewers [tap/tap-viewer]) + (v/with-viewer taps-viewer {:nextjournal/opts {:taps-view @!taps-view}} + @tap/!taps))) + :nextjournal.clerk/sci-repl (open! id {:title "SCI REPL" :css-class "p-0 relative overflow-auto"} + (v/with-viewer {:render-fn 'nextjournal.clerk.render.window/sci-repl + :transform-fn v/mark-presented} nil)))) ([id content] (open! id {} content)) ([id opts content] ;; TODO: consider calling v/transform-result From f9fd783867a010d70a4b29a488436cfd4edefcc0 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Wed, 21 Jun 2023 09:01:35 +0200 Subject: [PATCH 37/38] Resolve circular dep and avoid requring-resolve --- src/nextjournal/clerk.clj | 9 +++++---- src/nextjournal/clerk/tap.clj | 34 +++++++++++++++----------------- src/nextjournal/clerk/window.clj | 11 +++++------ 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/nextjournal/clerk.clj b/src/nextjournal/clerk.clj index 4e601b6ec..9d113b294 100644 --- a/src/nextjournal/clerk.clj +++ b/src/nextjournal/clerk.clj @@ -12,7 +12,8 @@ [nextjournal.clerk.eval :as eval] [nextjournal.clerk.parser :as parser] [nextjournal.clerk.viewer :as v] - [nextjournal.clerk.webserver :as webserver])) + [nextjournal.clerk.webserver :as webserver] + [nextjournal.clerk.window :as window])) (defonce ^:private !show-filter-fn (atom nil)) (defonce ^:private !last-file (atom nil)) @@ -78,9 +79,9 @@ #_(show! "https://raw.githubusercontent.com/nextjournal/clerk-demo/main/notebooks/rule_30.clj") #_(show! (java.io.StringReader. ";; # In Memory Notebook 👋\n(+ 41 1)")) -(defn window! "todo" [& args] (apply (requiring-resolve 'nextjournal.clerk.window/open!) args)) -(defn close-window! "todo" [id] ((requiring-resolve 'nextjournal.clerk.window/close!) id)) -(defn close-all-windows! "todo" [] ((requiring-resolve 'nextjournal.clerk.window/close-all!))) +(defn window! [& args] (apply window/open! args)) +(defn close-window! [id] (window/close! id)) +(defn close-all-windows! [] (window/close-all!)) (defn recompute! "Recomputes the currently visible doc, without parsing it." diff --git a/src/nextjournal/clerk/tap.clj b/src/nextjournal/clerk/tap.clj index 9c750a922..96eb3c5bc 100644 --- a/src/nextjournal/clerk/tap.clj +++ b/src/nextjournal/clerk/tap.clj @@ -1,9 +1,7 @@ ;; # 🚰 Tap Inspector (ns nextjournal.clerk.tap {:nextjournal.clerk/visibility {:code :hide :result :hide}} - (:require [clojure.core :as core] - [nextjournal.clerk :as clerk] - [nextjournal.clerk.viewer :as v]) + (:require [nextjournal.clerk.viewer :as v]) (:import (java.time Instant LocalTime ZoneId))) (defn inst->local-time-str [inst] (str (LocalTime/ofInstant inst (ZoneId/systemDefault)))) @@ -25,18 +23,18 @@ [:button.text-xs.rounded-full.px-3.py-1.border-2.font-sans.hover:bg-slate-100.cursor-pointer {:on-click #(nextjournal.clerk.render/clerk-eval `(reset-taps!))} "Clear"]])))) -^{::clerk/sync true ::clerk/viewer switch-view ::clerk/visibility {:result :show}} +^{:nextjournal.clerk/sync true :nextjournal.clerk/viewer switch-view :nextjournal.clerk/visibility {:result :show}} (defonce !view (atom :stream)) (defonce !taps (atom ())) (defn reset-taps! [] (reset! !taps ()) - (clerk/recompute!)) + ((resolve 'nextjournal.clerk/recompute!))) (defn tapped [x] (swap! !taps conj (record-tap x)) - (clerk/recompute!)) + ((resolve 'nextjournal.clerk/recompute!))) (defonce tap-setup (add-tap (fn [x] ((resolve `tapped) x)))) @@ -58,25 +56,25 @@ (update-in [:nextjournal/value ::tapped-at] inst->local-time-str)))}) -^{::clerk/visibility {:result :show} - ::clerk/viewers (v/add-viewers [tap-viewer])} -(clerk/fragment (cond->> @!taps - (= :latest @!view) (take 1))) +^{:nextjournal.clerk/visibility {:result :show} + :nextjournal.clerk/viewers (v/add-viewers [tap-viewer])} +(v/fragment (cond->> @!taps + (= :latest @!view) (take 1))) (comment (last @!taps) (dotimes [_i 5] (tap> (rand-int 1000))) (tap> (shuffle (range (+ 20 (rand-int 200))))) - (tap> (clerk/md "> The purpose of visualization is **insight**, not pictures.")) + (tap> (v/md "> The purpose of visualization is **insight**, not pictures.")) (tap> (v/plotly {:data [{:z [[1 2 3] [3 2 1]] :type "surface"}]})) - (tap> (clerk/html {::clerk/width :full} [:h1.w-full.border-2.border-amber-500.bg-amber-500.h-10])) - (tap> (clerk/table {::clerk/width :full} [[1 2] [3 4]])) - (tap> (clerk/plotly {::clerk/width :full} {:data [{:y [3 1 2]}]})) - (tap> (clerk/image "trees.png")) + (tap> (v/html {:nextjournal.clerk/width :full} [:h1.w-full.border-2.border-amber-500.bg-amber-500.h-10])) + (tap> (v/table {:nextjournal.clerk/width :full} [[1 2] [3 4]])) + (tap> (v/plotly {:nextjournal.clerk/width :full} {:data [{:y [3 1 2]}]})) + (tap> (v/image "trees.png")) (do (require 'rule-30) - (tap> (clerk/with-viewers (clerk/add-viewers rule-30/viewers) rule-30/rule-30))) - (tap> (clerk/with-viewers (clerk/add-viewers rule-30/viewers) rule-30/board)) - (tap> (clerk/html [:h1 "Fin. 👋"])) + (tap> (v/with-viewers (v/add-viewers rule-30/viewers) rule-30/rule-30))) + (tap> (v/with-viewers (v/add-viewers rule-30/viewers) rule-30/board)) + (tap> (v/html [:h1 "Fin. 👋"])) (tap> (reduce (fn [acc _] (vector acc)) :fin (range 200))) (reset-taps!)) diff --git a/src/nextjournal/clerk/window.clj b/src/nextjournal/clerk/window.clj index a608836c9..96ef431c8 100644 --- a/src/nextjournal/clerk/window.clj +++ b/src/nextjournal/clerk/window.clj @@ -1,12 +1,11 @@ (ns nextjournal.clerk.window - (:require [nextjournal.clerk :as clerk] - [nextjournal.clerk.tap :as tap] + (:require [nextjournal.clerk.tap :as tap] [nextjournal.clerk.viewer :as v] [nextjournal.clerk.webserver :as webserver])) (declare open!) (defonce !taps-view (atom :stream)) -(defn set-view! [x] (reset! !taps-view x) (open! ::clerk/taps)) +(defn set-view! [x] (reset! !taps-view x) (open! :nextjournal.clerk/taps)) (def taps-viewer {:render-fn '(fn [taps {:as opts :keys [taps-view]}] @@ -49,7 +48,7 @@ :nextjournal/fetch-opts {:blob-id (str id)} :nextjournal/blob-id (str id)})))) -(add-watch tap/!taps ::tap-watcher (fn [_ _ _ _] (open! ::clerk/taps))) +(add-watch tap/!taps ::tap-watcher (fn [_ _ _ _] (open! :nextjournal.clerk/taps))) (defn close! [id] (webserver/close-window! id)) @@ -57,10 +56,10 @@ (doseq [w (keys @webserver/!windows)] (close! w))) -#_(open! ::clerk/taps) +#_(open! :nextjournal.clerk/taps) #_(doseq [f @@(resolve 'clojure.core/tapset)] (remove-tap f)) #_(tap> (range 30)) -#_(close! ::clerk/taps) +#_(close! :nextjournal.clerk/taps) #_(tap> (v/plotly {:data [{:y [1 2 3]}]})) #_(tap> (v/table [[1 2] [3 4]])) #_(open! ::my-window {:title "🔭 Rear Window"} (v/table [[1 2] [3 4]])) From 1c793df3a1d5e8b654b525f015f67bce02d50106 Mon Sep 17 00:00:00 2001 From: Martin Kavalar Date: Wed, 21 Jun 2023 16:27:54 +0200 Subject: [PATCH 38/38] Start refactoring from window to generic session --- src/nextjournal/clerk/render.cljs | 22 ++-- src/nextjournal/clerk/sci_env.cljs | 13 ++- src/nextjournal/clerk/tap.clj | 1 - src/nextjournal/clerk/view.clj | 4 +- src/nextjournal/clerk/webserver.clj | 155 ++++++++++++++++++---------- src/nextjournal/clerk/window.clj | 9 +- 6 files changed, 129 insertions(+), 75 deletions(-) diff --git a/src/nextjournal/clerk/render.cljs b/src/nextjournal/clerk/render.cljs index 5705b64cc..9344c6ef6 100644 --- a/src/nextjournal/clerk/render.cljs +++ b/src/nextjournal/clerk/render.cljs @@ -17,7 +17,7 @@ [nextjournal.clerk.render.hooks :as hooks] [nextjournal.clerk.render.localstorage :as localstorage] [nextjournal.clerk.render.navbar :as navbar] - [nextjournal.clerk.render.window :as window] + #_[nextjournal.clerk.render.window :as window] [nextjournal.clerk.viewer :as viewer] [reagent.core :as r] [reagent.ratom :as ratom] @@ -138,6 +138,7 @@ (assoc :fragment (subs (.-hash url) 1)))))))) (defn history-push-state [{:as opts :keys [path fragment replace?]}] + (js/console.log :history-push-state opts) (when (not= path (some-> js/history .-state .-path)) (j/call js/history (if replace? :replaceState :pushState) (clj->js opts) "" (str (.. js/document -location -origin) "/" path (when fragment (str "#" fragment)))))) @@ -602,15 +603,16 @@ (when-let [error (get-in @!doc [:nextjournal/value :error])] [:div.fixed.top-0.left-0.w-full.h-full [inspect-presented error]]) - (into [:<>] - (map (fn [[id state]] - ^{:key id} - [window/show - [render-result state {}] - (-> state - (assoc :id id :on-close #(clerk-eval `(nextjournal.clerk.window/close! ~id))) - (dissoc :nextjournal/presented))])) - @!windows)]) + #_(when-not (:nextjournal/window-id @!doc) + (into [:<>] + (map (fn [[id state]] + ^{:key id} + [window/show + [render-result state {}] + (-> state + (assoc :id id :on-close #(clerk-eval `(nextjournal.clerk.window/close! ~id))) + (dissoc :nextjournal/presented))])) + @!windows))]) (declare mount) diff --git a/src/nextjournal/clerk/sci_env.cljs b/src/nextjournal/clerk/sci_env.cljs index 3f28a2d05..abe42cda2 100644 --- a/src/nextjournal/clerk/sci_env.cljs +++ b/src/nextjournal/clerk/sci_env.cljs @@ -185,12 +185,17 @@ (defn reconnect-timeout [failed-connection-attempts] (get [0 0 100 500 5000] failed-connection-attempts 10000)) -(defn ^:export connect [ws-url] +(defn ^:export connect [ws-url window-edn] (when (::failed-attempts @render/!doc) (swap! render/!doc assoc ::connection-status "Reconnecting…")) - (let [ws (js/WebSocket. ws-url)] + (let [window (when window-edn + (read-string window-edn)) + ws (js/WebSocket. ws-url)] (set! (.-onmessage ws) onmessage) - (set! (.-onopen ws) (fn [e] (swap! render/!doc dissoc ::connection-status ::failed-attempts))) + (set! (.-onopen ws) (fn [e] + (when window + (.send ws {:type :set-window! :window window})) + (swap! render/!doc dissoc ::connection-status ::failed-attempts))) (set! (.-onclose ws) (fn [e] (let [timeout (reconnect-timeout (::failed-attempts @render/!doc 0))] (swap! render/!doc @@ -200,7 +205,7 @@ (str "Disconnected, reconnecting in " timeout "ms…") "Reconnecting…")) (update ::failed-attempts (fnil inc 0))))) - (js/setTimeout #(connect ws-url) timeout)))) + (js/setTimeout #(connect ws-url window-edn) timeout)))) (set! (.-clerk_ws ^js goog/global) ws) (set! (.-ws_send ^js goog/global) (fn [msg] (.send ws msg))))) diff --git a/src/nextjournal/clerk/tap.clj b/src/nextjournal/clerk/tap.clj index 96eb3c5bc..8745d0646 100644 --- a/src/nextjournal/clerk/tap.clj +++ b/src/nextjournal/clerk/tap.clj @@ -55,7 +55,6 @@ (assoc-in [:nextjournal/render-opts :id] (::key value)) ;; assign custom react key (update-in [:nextjournal/value ::tapped-at] inst->local-time-str)))}) - ^{:nextjournal.clerk/visibility {:result :show} :nextjournal.clerk/viewers (v/add-viewers [tap-viewer])} (v/fragment (cond->> @!taps diff --git a/src/nextjournal/clerk/view.clj b/src/nextjournal/clerk/view.clj index 6f4ea108e..00a7d89b9 100644 --- a/src/nextjournal/clerk/view.clj +++ b/src/nextjournal/clerk/view.clj @@ -50,7 +50,7 @@ ;; https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions (str/replace s "" "")) -(defn ->html [{:as state :keys [conn-ws? current-path html exclude-js?]}] +(defn ->html [{:as state :keys [conn-ws? current-path html exclude-js? window-id]}] (hiccup/html5 [:head [:meta {:charset "UTF-8"}] @@ -66,4 +66,4 @@ let state = " (-> state v/->edn escape-closing-script-tag pr-str) ".replaceAll('nextjournal.clerk.view/escape-closing-script-tag', 'script') viewer.init(viewer.read_string(state))\n" (when conn-ws? - "viewer.connect(document.location.origin.replace(/^http/, 'ws') + '/_ws')")])])) + (format "viewer.connect(document.location.origin.replace(/^http/, 'ws') + '/_ws', '%s')" (pr-str window-id)))])])) diff --git a/src/nextjournal/clerk/webserver.clj b/src/nextjournal/clerk/webserver.clj index aa19ac0a9..0d54488d2 100644 --- a/src/nextjournal/clerk/webserver.clj +++ b/src/nextjournal/clerk/webserver.clj @@ -23,9 +23,12 @@ :visibility {:code :hide, :result :show} :result {:nextjournal/value (v/html (help-hiccup))}}]}) -(defonce !clients (atom #{})) +(defonce !clients->sessions (atom {})) (defonce !doc (atom nil)) -(defonce !windows (atom {})) + +(defonce !sessions (atom {:default !doc})) +#_(reset! !sessions {:default !doc}) + (defonce !last-sender-ch (atom nil)) #_(view/doc->viewer @!doc) @@ -36,15 +39,21 @@ (defn send! [ch msg] (httpkit/send! ch (v/->edn msg))) -(defn broadcast! [msg] - (doseq [ch @!clients] +(defn broadcast! [session msg] + (doseq [ch (keep (fn [[ch ch-session]] + (when (= ch-session session) + ch)) + @!clients->sessions)] (when (not= @!last-sender-ch *sender-ch*) (send! ch {:type :patch-state! :patch [] :effects [(v/->ViewerEval (list 'nextjournal.clerk.render/set-reset-sync-atoms! (not= *sender-ch* ch)))]})) + (let [edn (v/->edn msg)] + (when (str/includes? edn "get-safe/fn--14073") + (spit "msg.edn" (with-out-str (clojure.pprint/pprint edn))))) (httpkit/send! ch (v/->edn msg))) (reset! !last-sender-ch *sender-ch*)) -#_(broadcast! [{:random (rand-int 10000) :range (range 100)}]) +#_(broadcast! nil [{:random (rand-int 10000) :range (range 100)}]) (defn ^:private percent-decode [s] (java.net.URLDecoder/decode s java.nio.charset.StandardCharsets/UTF_8)) @@ -71,7 +80,7 @@ (into {} (comp (filter #(and (map? %) (v/get-safe % :nextjournal/blob-id) (v/get-safe % :nextjournal/presented))) (map (juxt :nextjournal/blob-id :nextjournal/presented))) - (concat (vals @!windows) + (concat (vals @!sessions) (tree-seq coll? seq (:nextjournal/value presented-doc))))) @@ -113,7 +122,7 @@ (when-let [scheduled-send-status-future (-> doc meta ::!send-status-future)] (future-cancel scheduled-send-status-future))) -(defn present+reset! [doc] +(defn present+reset! [!doc doc] (let [presented (view/doc->viewer doc) sync-vars-old (v/extract-sync-atom-vars @!doc) sync-vars (v/extract-sync-atom-vars doc)] @@ -125,19 +134,26 @@ (reset! !doc (with-meta doc presented)) presented)) -(defn update-doc! [{:as doc :keys [nav-path fragment skip-history?]}] - (broadcast! (if (and (:ns @!doc) (= (:ns @!doc) (:ns doc))) - {:type :patch-state! :patch (editscript/get-edits (editscript/diff (meta @!doc) (present+reset! doc) {:algo :quick}))} - (cond-> {:type :set-state! - :doc (present+reset! doc)} - (and nav-path (not skip-history?)) - (assoc :effects [(v/->ViewerEval (list 'nextjournal.clerk.render/history-push-state - (cond-> {:path nav-path} fragment (assoc :fragment fragment))))]))))) - +(defn update-doc! + ([doc] (update-doc! :default doc)) + ([session {:as doc :keys [nav-path fragment skip-history?]}] + (let [!doc (get @!sessions session)] + (broadcast! session + (if (and (:ns @!doc) (= (:ns @!doc) (:ns doc))) + {:type :patch-state! :patch (editscript/get-edits (editscript/diff (meta @!doc) (present+reset! !doc doc) {:algo :quick}))} + (cond-> {:type :set-state! + :doc (present+reset! !doc doc)} + (and nav-path (not skip-history?)) + (assoc :effects [(v/->ViewerEval (list 'nextjournal.clerk.render/history-push-state + (cond-> {:path nav-path} fragment (assoc :fragment fragment))))]))))))) + +#_(show! {} 'nextjournal.clerk.tap) +#_(show! {} 'nextjournal.clerk.home) #_(update-doc! (help-doc)) -(defn update-error! [ex] - (update-doc! (assoc @!doc :error ex))) +(defn update-error! + ([ex] (update-error! :default ex)) + ([session ex] (update-doc! session (assoc (get @!sessions session) :error ex)))) (defn read-msg [s] (binding [*data-readers* v/data-readers] @@ -150,40 +166,47 @@ #_(pr-str (read-msg "#viewer-eval (resolve 'clojure.core/inc)")) -(defn update-window! [id state] +(defn update-window! [id content] + #_#_ (swap! !windows assoc id state) - (broadcast! {:type :set-window-state! :id id :state state})) + (broadcast! nil {:type :set-window-state! :id id :state state})) (defn update-windows! [] + #_ (doseq [[id state] @!windows] - (update-window! id state))) + (update-window! nil state))) (defn close-window! [id] + #_#_ (swap! !windows dissoc id) - (broadcast! {:type :close-window! :id id})) + (broadcast! nil {:type :close-window! :id id})) (def ws-handlers {:on-open (fn [ch] - (swap! !clients conj ch) + (swap! !clients->sessions assoc ch :default) (update-windows!)) - :on-close (fn [ch _reason] (swap! !clients disj ch)) + :on-close (fn [ch _reason] (swap! !clients->sessions dissoc ch)) :on-receive (fn [sender-ch edn-string] - (binding [*ns* (or (:ns @!doc) - (create-ns 'user))] - (let [{:as msg :keys [type recompute?]} (read-msg edn-string)] - (case type - :eval (do (send! sender-ch (merge {:type :eval-reply :eval-id (:eval-id msg)} - (try {:reply (eval (:form msg))} - (catch Exception e - {:error (Throwable->map e)})))) - (when recompute? - (eval '(nextjournal.clerk/recompute!)))) - :swap! (when-let [var (resolve (:var-name msg))] - (try - (binding [*sender-ch* sender-ch] - (apply swap! @var (eval (:args msg)))) - (catch Exception ex - (throw (doto (ex-info (str "Clerk cannot `swap!` synced var `" (:var-name msg) "`.") msg ex) update-error!)))))))))}) + (let [session (or (get @!clients->sessions sender-ch) + :default) + !doc (get @!sessions session)] + (binding [*ns* (or (:ns @!doc) + (create-ns 'user))] + (let [{:as msg :keys [type recompute?]} (read-msg edn-string)] + (case type + :set-window! (swap! !clients->sessions assoc sender-ch (:window msg)) + :eval (do (send! sender-ch (merge {:type :eval-reply :eval-id (:eval-id msg)} + (try {:reply (eval (:form msg))} + (catch Exception e + {:error (Throwable->map e)})))) + (when recompute? + (eval '(nextjournal.clerk/recompute!)))) + :swap! (when-let [var (resolve (:var-name msg))] + (try + (binding [*sender-ch* sender-ch] + (apply swap! @var (eval (:args msg)))) + (catch Exception ex + (throw (doto (ex-info (str "Clerk cannot `swap!` synced var `" (:var-name msg) "`.") msg ex) update-error!))))))))))}) #_(do (apply swap! nextjournal.clerk.atom/my-state (eval '[update :counter inc])) @@ -214,13 +237,22 @@ (defn show! [opts file-or-ns] ((resolve 'nextjournal.clerk/show!) opts file-or-ns)) +#_(show! {} 'nextjournal.clerk.home) + (defn navigate! [{:as opts :keys [nav-path]}] (show! opts (->file-or-ns nav-path))) (defn prefetch-request? [req] (= "prefetch" (-> req :headers (get "purpose")))) +(defn extract-window [{:as req :keys [query-string]}] + (some-> (query-string->map query-string) :window edn/read-string)) + +(defn get-doc [req] + (get @!sessions (or (extract-window req) :default))) + (defn serve-notebook [{:as req :keys [uri]}] - (let [nav-path (subs uri 1)] + (let [nav-path (subs uri 1) + !doc (get-doc req)] (cond (prefetch-request? req) {:status 404} @@ -242,42 +274,61 @@ :headers {"Content-Type" "text/plain"} :body (format "Could not find notebook at %s." (pr-str nav-path))})))) +(defn serve-window [{:as req :keys [uri]}] + (if-let [!doc (get @!sessions (extract-window req))] + (do (present+reset! !doc @!doc) + {:status 200 + :headers {"Content-Type" "text/html" "Cache-Control" "no-store"} + :body (view/->html {:doc (meta @!doc) + :window-id (extract-window req) + :resource->url @config/!resource->url + :conn-ws? true})}) + {:status 404 + :body "no-window"})) + +#_(do (swap! !sessions assoc 'tap (atom (nextjournal.clerk.eval/eval-file (clojure.java.io/resource "nextjournal/clerk/tap.clj")))) :done) + + (defn app [{:as req :keys [uri]}] (if (:websocket? req) (httpkit/as-channel req ws-handlers) (try (case (get (re-matches #"/([^/]*).*" uri) 1) - "_blob" (serve-blob @!doc (extract-blob-opts req)) + "_blob" (serve-blob @(get-doc req) (extract-blob-opts req)) ("build" "js" "css") (serve-file uri (str "public" uri)) ("_fs") (serve-file uri (str/replace uri "/_fs/" "")) "_ws" {:status 200 :body "upgrading..."} "favicon.ico" {:status 404} - (serve-notebook req)) + (if (extract-window req) + (serve-window req) + (serve-notebook req))) (catch Throwable e {:status 500 :body (with-out-str (pprint/pprint (Throwable->map e)))})))) #_(nextjournal.clerk/serve! {}) -(defn broadcast-status! [status] +(defn broadcast-status! [session status] ;; avoid editscript diff but use manual patch to just replace `:status` in doc - (broadcast! {:type :patch-state! :patch [[[:status] :r status]]})) + (broadcast! session {:type :patch-state! :patch [[[:status] :r status]]})) (defn broadcast-status-debounced! "Schedules broadcasting a status update after 50 ms. Cancels previously scheduled broadcast, if it exists." - [old-future status] + [old-future session status] (when old-future (future-cancel old-future)) (future (Thread/sleep 50) - (broadcast-status! status))) - -(defn set-status! [status] - (swap! !doc (fn [doc] (-> (or doc (help-doc)) - (vary-meta assoc :status status) - (vary-meta update ::!send-status-future broadcast-status-debounced! status))))) + (broadcast-status! session status))) + +(defn set-status! + ([status] (set-status! :default status)) + ([session status] + (swap! (get @!sessions session) (fn [doc] (-> (or doc (help-doc)) + (vary-meta assoc :status status) + (vary-meta update ::!send-status-future broadcast-status-debounced! session status)))))) #_(clojure.java.browse/browse-url "http://localhost:7777") diff --git a/src/nextjournal/clerk/window.clj b/src/nextjournal/clerk/window.clj index 96ef431c8..01ec5caef 100644 --- a/src/nextjournal/clerk/window.clj +++ b/src/nextjournal/clerk/window.clj @@ -43,18 +43,15 @@ ([id content] (open! id {} content)) ([id opts content] ;; TODO: consider calling v/transform-result - (webserver/update-window! id (merge opts {:nextjournal/presented (update (v/present content) :nextjournal/css-class #(or % ["px-0"])) - :nextjournal/hash (gensym) - :nextjournal/fetch-opts {:blob-id (str id)} - :nextjournal/blob-id (str id)})))) + (webserver/update-window! id content))) (add-watch tap/!taps ::tap-watcher (fn [_ _ _ _] (open! :nextjournal.clerk/taps))) (defn close! [id] (webserver/close-window! id)) (defn close-all! [] - (doseq [w (keys @webserver/!windows)] - (close! w))) + #_(doseq [w (keys @webserver/!windows)] + (close! w))) #_(open! :nextjournal.clerk/taps) #_(doseq [f @@(resolve 'clojure.core/tapset)] (remove-tap f))