diff --git a/.gitignore b/.gitignore index f7fcc1b..fe689c4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ pom.xml.asc /.repl *.log /.env +/resources/public/css/site.min.css diff --git a/env/dev/cljs/cljs_webrepl/dev.cljs b/env/dev/cljs/cljs_webrepl/dev.cljs index a328e7c..340615a 100644 --- a/env/dev/cljs/cljs_webrepl/dev.cljs +++ b/env/dev/cljs/cljs_webrepl/dev.cljs @@ -5,7 +5,7 @@ (enable-console-print!) (figwheel/watch-and-reload - :websocket-url "ws://localhost:3449/figwheel-ws" + :websocket-url "wss://figwheel.industrial.gt0.ca/figwheel-ws" :jsload-callback core/mount-root) (core/init!) diff --git a/env/dev/cljs/cljs_webrepl/repl_thread_dev.cljs b/env/dev/cljs/cljs_webrepl/repl_thread_dev.cljs new file mode 100644 index 0000000..f71284e --- /dev/null +++ b/env/dev/cljs/cljs_webrepl/repl_thread_dev.cljs @@ -0,0 +1,6 @@ +(ns ^:figwheel-no-load cljs-webrepl.repl-thread-dev + (:require [cljs-webrepl.repl-thread :as repl-thread])) + +(enable-console-print!) + +(repl-thread/worker) diff --git a/env/prod/cljs/cljs_webrepl/repl_thread_prod.cljs b/env/prod/cljs/cljs_webrepl/repl_thread_prod.cljs new file mode 100644 index 0000000..578517e --- /dev/null +++ b/env/prod/cljs/cljs_webrepl/repl_thread_prod.cljs @@ -0,0 +1,7 @@ +(ns cljs-webrepl.repl-thread-prod + (:require [cljs-webrepl.repl-thread :as repl-thread])) + +;;ignore println statements in prod +(set! *print-fn* (fn [& _])) + +(repl-thread/worker) diff --git a/project.clj b/project.clj index 16f441d..a69161e 100644 --- a/project.clj +++ b/project.clj @@ -5,11 +5,10 @@ :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.8.0"] - [org.clojure/clojurescript "1.9.229"] - [cljsjs/clipboard "1.5.9-0"] - [cljsjs/material "1.2.1-0"] + [org.clojure/clojurescript "1.9.293"] [com.cognitect/transit-cljs "0.8.239"] [com.taoensso/timbre "4.7.4"] + [cljsjs/material "1.2.1-0"] [environ "1.1.0"] [figwheel "0.5.8"] [fipp "0.6.6"] @@ -34,15 +33,25 @@ :minify-assets {:assets {"resources/public/css/site.min.css" "resources/public/css/site.css"}} - :cljsbuild {:builds - {:app {:source-paths ["src/cljs"] - :compiler {:output-to "target/cljsbuild/public/js/app.js" - :output-dir "target/cljsbuild/public/js/out" - :asset-path "js/out" - :main cljs-webrepl.prod - :static-fns true - :optimizations :none - :pretty-print true}}}} + :cljsbuild {:builds {:app {:source-paths ["src/cljs"] + :compiler {:output-to "target/cljsbuild/public/js/app.js" + :output-dir "target/cljsbuild/public/js/app" + :asset-path "js/app" + :main cljs-webrepl.prod + :static-fns true + :optimizations :none + :pretty-print true + :parallel-build true}} + + :repl-thread {:source-paths ["src/cljs"] + :compiler {:output-to "target/cljsbuild/public/js/repl-thread.js" + :output-dir "target/cljsbuild/public/js/repl-thread" + :asset-path "js/repl-thread" + :main cljs-webrepl.repl-thread-prod + :static-fns true + :optimizations :simple + :pretty-print true + :parallel-build true}}}} :profiles {:dev {:plugins [[lein-figwheel "0.5.8"] @@ -52,31 +61,33 @@ :figwheel {:http-server-root "public" :server-port 3449 :nrepl-port 7001 - :css-dirs ["resources/public/css"]} + :css-dirs ["resources/public/css"] + :load-all-builds true} :env {:dev true} - :cljsbuild {:builds {:app - {:source-paths ["src/cljs" "env/dev/cljs"] - :compiler {:source-map true - :main cljs-webrepl.dev}} - :test - {:source-paths ["src/cljs" "test/cljs" "env/dev/cljs"] - :compiler {:output-to "target/test.js" - :main cljs-webrepl.doo-runner - :optimizations :whitespace - :pretty-print true}}}}} + :cljsbuild {:builds {:app {:source-paths ["src/cljs" "env/dev/cljs"] + :compiler {:source-map true + :main cljs-webrepl.dev}} + :repl-thread {:source-paths ["src/cljs" "env/dev/cljs"] + :compiler {:source-map "target/cljsbuild/public/js/repl-thread.js.map" + :main cljs-webrepl.repl-thread-dev}} + :test {:source-paths ["src/cljs" "test/cljs" "env/dev/cljs"] + :compiler {:output-to "target/test.js" + :main cljs-webrepl.doo-runner + :optimizations :whitespace + :pretty-print true}}}}} :prod {:hooks [minify-assets.plugin/hooks] - :prep-tasks ["cljsbuild" "once"] :env {:production true} :omit-source true :cljsbuild - {:builds {:app - {:source-paths ["src/cljs" "env/prod/cljs"] - :compiler - {:optimizations :none - :pretty-print false}}}}} + {:builds {:app {:source-paths ["src/cljs" "env/prod/cljs"] + :compiler {:optimizations :advanced + :pretty-print false}} + :repl-thread {:source-paths ["src/cljs" "env/prod/cljs"] + :compiler {:optimizations :simple + :pretty-print false}}}}} :uberjar {:hooks [minify-assets.plugin/hooks] :prep-tasks ["cljsbuild" "once"] @@ -84,8 +95,9 @@ :omit-source true :cljsbuild {:jar true - :builds {:app - {:source-paths ["src/cljs" "env/prod/cljs"] - :compiler - {:optimizations :none - :pretty-print false}}}}}}) + :builds {:app {:source-paths ["src/cljs" "env/prod/cljs"] + :compiler {:optimizations :advanced + :pretty-print false}} + :repl-thread {:source-paths ["src/cljs" "env/prod/cljs"] + :compiler {:optimizations :simple + :pretty-print false}}}}}}) diff --git a/src/cljs/cljs_webrepl/core.cljs b/src/cljs/cljs_webrepl/core.cljs index 2fa5a88..2e1c1fe 100644 --- a/src/cljs/cljs_webrepl/core.cljs +++ b/src/cljs/cljs_webrepl/core.cljs @@ -4,9 +4,9 @@ [cljs.core.async :refer [chan close! timeout put!]] [reagent.core :as r :refer [atom]] [reagent.session :as session] - [cljsjs.clipboard :as clipboard] [fipp.edn :as fipp] [cljs-webrepl.repl :as repl] + [cljs-webrepl.repl-thread :as repl-thread] [cljs-webrepl.mdl :as mdl] [cljs-webrepl.syntax :refer [syntaxify]] [taoensso.timbre :as timbre @@ -14,56 +14,87 @@ (:require-macros [cljs.core.async.macros :refer [go go-loop]])) -(defonce state - (atom {:ns "unknown" - :input "(+ 1 2)" - :cursor 0 - :history (sorted-map)})) +(def default-state + {:ns nil + :input "" + :cursor 0 + :history (sorted-map)}) + +(defonce state (atom default-state)) (defn pprint-str [data] (with-out-str (fipp/pprint data))) -(defn repl-init-event [state num [ns expression]] +(defn on-repl-eval [state num [ns expression]] (when num (swap! state update-in [:history num] assoc :ns ns :expression expression))) -(defn repl-result-event [state num [ns result]] +(defn on-repl-result [state num [ns result]] (if num (swap! state #(-> % (assoc :ns ns) (update-in [:history num] assoc :result result))) (swap! state assoc :ns ns))) -(defn repl-output-event [state num [s]] +(defn on-repl-print [state num [s]] (when num (swap! state update-in [:history num] update :output conj s))) -(defn repl-event [state [name num & value]] +(defn on-repl-error [state num [err]] + (when num + (swap! state update-in [:history num] assoc :error err))) + +(defn on-repl-event [state [name num & value]] (condp = name - :init (repl-init-event state num value) - :result (repl-result-event state num value) - :print (repl-output-event state num value) + :repl/eval (on-repl-eval state num value) + :repl/result (on-repl-result state num value) + :repl/error (on-repl-error state num value) + :repl/print (on-repl-print state num value) (warnf "Unknown repl event: %s %s" name value))) +(defn repl-event-loop [state from-repl] + (go-loop [] + (when-let [event ( % + (merge default-state) + (assoc :repl {:to-repl to-eval + :from-repl from-repl}))))) + + + (defn eval-str! [expression] (let [{:keys [to-repl]} (:repl @state) expression (some-> expression trim)] (when (and (some? to-repl) (some? expression)) (put! to-repl expression)))) -(defn clipboard [child] - (let [clipboard-atom (atom nil)] - (r/create-class - {:display-name "clipboard-button" - :component-did-mount - #(let [clipboard (new js/Clipboard (r/dom-node %))] - (reset! clipboard-atom clipboard)) - :component-will-unmount - #(when-not (nil? @clipboard-atom) - (.destroy @clipboard-atom) - (reset! clipboard-atom nil)) - :reagent-render - (fn [child] child)}))) +(defn copy-to-clipboard [txt] + (->> (js-obj "dataType" "text/plain" "data" txt) + (new js/ClipboardEvent "copy") + (js/document.dispatchEvent))) (defn history-prev [{:keys [cursor history] :as state}] (let [c (count history) @@ -71,7 +102,7 @@ (if (<= new-cursor c) (assoc state :cursor new-cursor - :input (:expression (nth history (- c new-cursor)))) + :input (:expression (get history (- c new-cursor)))) state))) (defn history-next [{:keys [cursor history] :as state}] @@ -80,7 +111,7 @@ (if (> new-cursor 0) (assoc state :cursor new-cursor - :input (:expression (nth history (- c new-cursor)))) + :input (:expression (get history (- c new-cursor)))) state))) (defn clear-input [state] @@ -138,7 +169,7 @@ (pprint-str) (syntaxify))) -(defn history-card-menu [props {:keys [ns num expression result output] :as history-item}] +(defn history-card-menu [props num {:keys [ns expression result output] :as history-item}] [mdl/upgrade [:div.mdl-card__menu [:button.mdl-button.mdl-js-button.mdl-button--icon.mdl-js-ripple-effect {:id (str "menu-" num)} @@ -148,25 +179,22 @@ [:li.mdl-menu__item {:on-click #(eval-str! expression)} "Evaluate Again"] - [clipboard - [:li.mdl-menu__item - {:data-clipboard-text expression} - "Copy Expression"]] + [:li.mdl-menu__item + {:on-click #(copy-to-clipboard (pprint-str output))} + "Copy Expression"] (if (seq output) - [clipboard - [:li.mdl-menu__item - {:data-clipboard-text "WHOOPS!"} - "Copy Output"]] + [:li.mdl-menu__item + {:data-clipboard-text "WHOOPS!"} + "Copy Output"] [:li.mdl-menu__item {:disabled true} "Copy Output"]) - [clipboard - [:li.mdl-menu__item - {:data-clipboard-text (:value result)} - "Copy Result"]]]]]) + [:li.mdl-menu__item + {:on-click #(copy-to-clipboard (if (string? (:value result)) (:value result) (pprint-str (:value result))))} + "Copy Result"]]]]) (defn history-card-output [props output] - [:div.card-outpu + [:div.card-output [:div.card-data (into [:pre.line] (for [line output] @@ -191,7 +219,7 @@ [{:keys [state] :as props} num {:keys [ns expression result output] :as history-item}] [:div.mdl-cell.mdl-cell--12-col [:div.mdl-card.mdl-shadow--2dp - [history-card-menu props history-item] + [history-card-menu props num history-item] [:div.card-data.expression [:code (str num " " ns "=> ") @@ -203,41 +231,54 @@ [history-card-result props result]]]) -(defn history [props] - (let [state (:state props)] - ^{:key (count (:history @state))} - [scroll-on-update - [:div.history - [:div.mdl-grid - (doall - (for [[num history-item] (:history @state)] - ^{:key num} - [history-card props num history-item]))]]])) +(defn please-wait [props] + [:div.history + [:div.mdl-grid + [:div.mdl-cell.mdl-cell--12-col + [:p "REPL initializing..."]]]]) + +(defn history [{:keys [state] :as props}] + ^{:key (count (:history @state))} + [scroll-on-update + [:div.history + [:div.mdl-grid + (doall + (for [[num history-item] (:history @state)] + ^{:key num} + [history-card props num history-item]))]]]) (defn input-field [props] - (let [state (:state props)] + (let [state (:state props) + {:keys [ns input]} @state + is-init? (some? ns) + ns (or ns "unknown")] [:div.input-field - [:form {:action "#" :autoComplete "off"} + [:form {:action "#" "autoComplete" "off"} [mdl/upgrade [:div.wide.mdl-textfield.mdl-js-textfield.mdl-textfield--floating-label [:input.wide.mdl-textfield__input {:type :text :id "input" :autocomplete "off" - :value (:input @state) + :value input + :disabled (not is-init?) :on-change #(swap! state input-on-change %) :on-key-down #(input-key-down state %)}] + ^{:key is-init?} [:label.mdl-textfield__label {:for "input"} - (str (:ns @state) "=>")]]]]])) + (str ns "=>")]]]]])) (defn run-button [props] - (let [state (:state props) - is-blank? (blank? (:input @state))] + (let [state (:state props) + {:keys [ns input]} @state + is-init? (some? ns) + is-blank? (blank? input) + is-disabled? (or (not is-init?) is-blank?)] [:div.padding-left - ^{:key is-blank?} + ^{:key is-disabled?} [mdl/upgrade [:button.mdl-button.mdl-js-button.mdl-button--fab.mdl-js-ripple-effect.mdl-button--colored - {:disabled is-blank? + {:disabled is-disabled? :on-click #(eval-input state)} [:i.material-icons "send"]]]])) @@ -298,23 +339,23 @@ [:i.material-icons "more_vert"]] [:ul.mdl-menu.mdl-menu--bottom-right.mdl-js-menu.mdl-js-ripple-effect {:for "main-menu"} + [:li.mdl-menu__item + {:on-click #(reset-repl! state)} + "Reset REPL"] [:li.mdl-menu__item {:on-click #(set! (.-location js/window) "https://github.com/theasp/cljs-webrepl")} "GitHub"] [:li.mdl-menu__item {:on-click show-about-dialog} "About"]]]] - [history props] + (if (:ns @state) + [history props] + [please-wait props]) [input props]]]]])) (defn mount-root [] (r/render [home-page] (.getElementById js/document "app"))) (defn init! [] - (let [{:keys [to-repl from-repl] :as repl} (repl/repl-chan-pair)] - (go-loop [] - (when-let [event (map [err] + (when err + (errorf "Evaluation: %s" err) + {:message (.-message err) + :data (.-data err)})) + +(defn fix-result [result] + (-> result + (update :error err->map))) + +(defn on-repl-eval [[num expression] from-repl repl-opts] + (debugf "About to eval: %s %s %s" num expression from-repl) + + (put! from-repl [:repl/eval num (replumb-repl/current-ns) expression]) + + (let [print-fn #(put! from-repl [:repl/print num %]) + result-fn #(put! from-repl [:repl/result num (replumb-repl/current-ns) (fix-result %)])] + (binding [cljs.core/*print-newline* true + cljs.core/*print-fn* print-fn] + ;; Use the 3rd argument to put! so the :init event is sent first + (replumb/read-eval-call repl-opts result-fn expression)))) + +(defn repl-loop [from-repl to-repl repl-opts] + (go + (put! to-repl [:repl/eval nil "true"]) + (loop [] + (when-let [msg (> (.-data.msg msg) + (t/read reader))) + +(defn- serialzie [writer msg] + (let [msg (t/write writer msg)] + (js-obj "msg" msg))) + +(defn- async-worker [& [target close-fn]] + (let [is-worker? (not (some? target)) + target (or target js/self) + input-ch (chan) + output-ch (chan) + reader (t/reader :json) + writer (t/writer :json) + + finally-fn (fn [] + (debugf "Cleaning up WebWorker (thread? %s)" (worker?)) + (close! input-ch) + (close! output-ch) + (when close-fn + (close-fn))) + + recv-fn (fn [msg] + (when-let [msg (deserialize reader msg)] + (put! output-ch msg))) + + error-fn (fn [err] + (let [err {:message (.-message err) + :file (.-filename err) + :line (.-lineno err) + :worker? (worker?)}] + (errorf "WebWorker: %s" err) + (put! output-ch [:webworker/error nil err]) + (finally-fn)))] + (.addEventListener target "message" recv-fn) + (.addEventListener target "error" error-fn) + (go + (loop [] + (when-let [msg (