├── src-shared
├── sass
│ ├── index.sass
│ └── profile.scss
└── cljs
│ └── cljs_webrepl
│ └── repl_thread.cljs
├── test
└── cljs
│ └── cljs_webrepl
│ ├── doo_runner.cljs
│ └── core_test.cljs
├── src-frontend
├── cljs-prod
│ └── cljs_webrepl
│ │ ├── prod.cljs
│ │ └── frontend.cljs
├── cljs-dev
│ └── cljs_webrepl
│ │ └── frontend.cljs
└── cljs
│ └── cljs_webrepl
│ ├── io.cljs
│ ├── editor.cljs
│ └── core.cljs
├── .gitignore
├── src-backend
├── cljs-dev
│ └── cljs_webrepl
│ │ └── backend.cljs
├── cljs-prod
│ └── cljs_webrepl
│ │ └── backend.cljs
└── cljs
│ └── cljs_webrepl
│ ├── io.cljs
│ └── repl.cljs
├── resources
└── public
│ ├── manifest.json
│ ├── index.html
│ ├── css
│ └── site.css
│ └── images
│ ├── cljs.svg
│ └── cljs-white.svg
├── README.md
├── .circleci
└── config.yml
├── deploy.sh
├── project.clj
└── LICENSE
/src-shared/sass/index.sass:
--------------------------------------------------------------------------------
1 | body
2 | background: tomato
3 | color: pink
4 |
--------------------------------------------------------------------------------
/src-shared/sass/profile.scss:
--------------------------------------------------------------------------------
1 | body {
2 | background: tomato;
3 | color: pink;
4 | }
5 |
--------------------------------------------------------------------------------
/test/cljs/cljs_webrepl/doo_runner.cljs:
--------------------------------------------------------------------------------
1 | (ns cljs-webrepl.doo-runner
2 | (:require [doo.runner :refer-macros [doo-tests]]
3 | [cljs-webrepl.core-test]))
4 |
5 | (doo-tests 'cljs-webrepl.core-test)
6 |
--------------------------------------------------------------------------------
/src-frontend/cljs-prod/cljs_webrepl/prod.cljs:
--------------------------------------------------------------------------------
1 | (ns cljs-webrepl.prod
2 | (:require [cljs-webrepl.core :as core]))
3 |
4 | ;;ignore println statements in prod
5 | (set! *print-fn* (fn [& _]))
6 |
7 | (core/init!)
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /classes
3 | /checkouts
4 | profiles.clj
5 | pom.xml
6 | pom.xml.asc
7 | *.jar
8 | *.class
9 | /.lein-*
10 | /.nrepl-port
11 | /resources/public/js
12 | /out
13 | /.repl
14 | *.log
15 | /.env
16 | /resources/public/css/site.min.css
17 |
--------------------------------------------------------------------------------
/src-frontend/cljs-prod/cljs_webrepl/frontend.cljs:
--------------------------------------------------------------------------------
1 | (ns cljs-webrepl.frontend
2 | (:require [cljs-webrepl.core :as core]
3 | [taoensso.timbre :as timbre
4 | :refer-macros (tracef debugf infof warnf errorf)]))
5 |
6 | ;;ignore println statements in prod
7 | ;;(set! *print-fn* (fn [& _]))
8 |
9 | (timbre/set-level! :info)
10 |
11 | (core/init!)
12 |
--------------------------------------------------------------------------------
/src-backend/cljs-dev/cljs_webrepl/backend.cljs:
--------------------------------------------------------------------------------
1 | (ns ^:figwheel-no-load cljs-webrepl.backend
2 | (:require
3 | [cljs-webrepl.repl-thread :as repl-thread]
4 | [cljs-webrepl.repl :as repl]
5 | [taoensso.timbre :as timbre
6 | :refer-macros (tracef debugf infof warnf errorf)]))
7 |
8 | (enable-console-print!)
9 | (timbre/set-level! :trace)
10 |
11 | (repl-thread/worker (repl/repl-chan-pair))
12 |
--------------------------------------------------------------------------------
/resources/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "lang": "en",
3 | "dir": "ltr",
4 | "name": "CLJS-WebREPL",
5 | "description": "ClojureScript REPL",
6 | "short_name": "CLJS-WebREPL",
7 | "icons": [{
8 | "src": "images/cljs-white.svg",
9 | "type": "image/svg"
10 | }],
11 | "scope": "/cljs-webrepl/",
12 | "start_url": "/cljs-webrepl/",
13 | "display": "fullscreen"
14 | }
15 |
--------------------------------------------------------------------------------
/src-backend/cljs-prod/cljs_webrepl/backend.cljs:
--------------------------------------------------------------------------------
1 | (ns cljs-webrepl.backend
2 | (:require
3 | [cljs-webrepl.repl-thread :as repl-thread]
4 | [cljs-webrepl.repl :as repl]
5 | [taoensso.timbre :as timbre
6 | :refer-macros (tracef debugf infof warnf errorf)]))
7 |
8 | ;;ignore println statements in prod
9 | ;;(set! *print-fn* (fn [& _]))
10 | (enable-console-print!)
11 | (timbre/set-level! :info)
12 |
13 | (repl-thread/worker (repl/repl-chan-pair))
14 |
--------------------------------------------------------------------------------
/src-frontend/cljs-dev/cljs_webrepl/frontend.cljs:
--------------------------------------------------------------------------------
1 | (ns ^:figwheel-no-load cljs-webrepl.frontend
2 | (:require
3 | [cljs-webrepl.core :as core]
4 | [figwheel.client :as figwheel :include-macros true]
5 | [taoensso.timbre :as timbre
6 | :refer-macros (tracef debugf infof warnf errorf)]))
7 |
8 | (enable-console-print!)
9 | (timbre/set-level! :trace)
10 |
11 | (figwheel/watch-and-reload
12 | :websocket-url "wss://figwheel.industrial.gt0.ca/figwheel-ws"
13 | :jsload-callback core/mount-root)
14 |
15 | (core/init!)
16 |
--------------------------------------------------------------------------------
/src-backend/cljs/cljs_webrepl/io.cljs:
--------------------------------------------------------------------------------
1 | (ns cljs-webrepl.io
2 | (:import goog.net.XhrIo))
3 |
4 | (defn fetch-file!
5 | "Very simple implementation of XMLHttpRequests that given a file path
6 | calls src-cb with the string fetched of nil in case of error.
7 | See doc at https://developers.google.com/closure/library/docs/xhrio"
8 | [file-url src-cb]
9 | (try
10 | (.send XhrIo file-url
11 | (fn [e]
12 | (if (.isSuccess (.-target e))
13 | (src-cb (.. e -target getResponseText))
14 | (src-cb nil))))
15 | (catch :default e
16 | (src-cb nil))))
17 |
--------------------------------------------------------------------------------
/src-frontend/cljs/cljs_webrepl/io.cljs:
--------------------------------------------------------------------------------
1 | (ns cljs-webrepl.io
2 | (:import goog.net.XhrIo))
3 |
4 | (defn fetch-file!
5 | "Very simple implementation of XMLHttpRequests that given a file path
6 | calls src-cb with the string fetched of nil in case of error.
7 | See doc at https://developers.google.com/closure/library/docs/xhrio"
8 | [file-url src-cb]
9 | (try
10 | (.send XhrIo file-url
11 | (fn [e]
12 | (if (.isSuccess (.-target e))
13 | (src-cb (.. e -target getResponseText))
14 | (src-cb nil))))
15 | (catch :default e
16 | (src-cb nil))))
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CLJS-WebREPL
2 |
3 | An attempt at a nice looking in browser REPL for ClojureScript. Try out the [live version](http://theasp.github.io/cljs-webrepl/)!
4 |
5 | # Thanks
6 | - Joel Martin and David Nolen for the cljs-bootstrap REPL bits:
7 | - https://github.com/kanaka/cljs-bootstrap
8 | - https://github.com/swannodette/cljs-bootstrap
9 | - Mike Fikes for the parts of the REPL taken from Planck
10 | - https://github.com/mfikes/planck
11 | - Dan Holmsand for the syntax hilighting, from the Reagent demo:
12 | - https://github.com/reagent-project/reagent/tree/master/demo/reagentdemo
13 |
14 | *PULL REQUESTS WELCOME!*
15 |
16 | # License
17 |
18 | Copyright © 2016 Andrew Phillips, Dan Holmsand, Mike Fikes, David Nolen, Rich Hickey, Joel Martin & Contributors
19 |
20 | Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.
21 |
--------------------------------------------------------------------------------
/test/cljs/cljs_webrepl/core_test.cljs:
--------------------------------------------------------------------------------
1 | (ns cljs-webrepl.core-test
2 | (:require [cljs.test :refer-macros [is are deftest testing use-fixtures]]
3 | [reagent.core :as reagent :refer [atom]]
4 | [cljs-webrepl.core :as rc]))
5 |
6 |
7 | (def isClient (not (nil? (try (.-document js/window)
8 | (catch js/Object e nil)))))
9 |
10 | (def rflush reagent/flush)
11 |
12 | (defn add-test-div [name]
13 | (let [doc js/document
14 | body (.-body js/document)
15 | div (.createElement doc "div")]
16 | (.appendChild body div)
17 | div))
18 |
19 | (defn with-mounted-component [comp f]
20 | (when isClient
21 | (let [div (add-test-div "_testreagent")]
22 | (let [comp (reagent/render-component comp div #(f comp div))]
23 | (reagent/unmount-component-at-node div)
24 | (reagent/flush)
25 | (.removeChild (.-body js/document) div)))))
26 |
27 |
28 | (defn found-in [re div]
29 | (let [res (.-innerHTML div)]
30 | (if (re-find re res)
31 | true
32 | (do (println "Not found: " res)
33 | false))))
34 |
35 |
36 | (deftest test-home
37 | (with-mounted-component (rc/home-page)
38 | (fn [c div]
39 | (is (found-in #"Welcome to" div)))))
40 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Clojure CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-clojure/ for more details
4 | #
5 | version: 2
6 | jobs:
7 | build:
8 | docker:
9 | # specify the version you desire here
10 | - image: circleci/clojure:lein-2.7.1
11 |
12 | # Specify service dependencies here if necessary
13 | # CircleCI maintains a library of pre-built images
14 | # documented at https://circleci.com/docs/2.0/circleci-images/
15 | # - image: circleci/postgres:9.4
16 |
17 | working_directory: ~/app
18 |
19 | environment:
20 | LEIN_ROOT: "true"
21 | # Customize the JVM maximum heap limit
22 | JVM_OPTS: -Xmx3200m
23 | LEIN_FAST_TRAMPOLINE: yes
24 |
25 | steps:
26 | - checkout
27 |
28 | # Download and cache dependencies
29 | - restore_cache:
30 | keys:
31 | - v1-dependencies-{{ checksum "project.clj" }}
32 | # fallback to using the latest cache if no exact match is found
33 | - v1-dependencies-
34 |
35 | - run: lein do deps
36 |
37 | - save_cache:
38 | paths:
39 | - ~/.m2
40 | - ~/app/node_modules
41 | key: v1-dependencies-{{ checksum "project.clj" }}
42 |
43 | - run: lein build-min
44 | - run: ./deploy.sh
45 |
--------------------------------------------------------------------------------
/resources/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
17 |
18 |
19 | Cljs-WebREPL
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -ex
4 |
5 | # Variables
6 | ORIGIN_URL=`git config --get remote.origin.url`
7 | GOOGLETAG="
8 |
9 |
11 |
16 | "
17 |
18 | # Set identity
19 | git config user.name "Automated Deployment"
20 | git config user.email "auto@example.com"
21 |
22 | # Delete existing gh-pages branch
23 | if git branch | grep -q gh-pages; then
24 | git branch -D gh-pages
25 | fi
26 |
27 | # Make new gh-pages branch
28 | git checkout --orphan gh-pages
29 |
30 | # Move everything to .dist
31 | rm -rf .dist
32 | mkdir .dist
33 | mv * .dist
34 |
35 | # Move what we want into place
36 | mv .dist/resources/public/* .
37 | mv .dist/target/cljsbuild/public/js .
38 | for i in js/*.min.js; do
39 | mv $i ${i%.min.js}.js
40 | done
41 |
42 | # Remove the old files
43 | git rm --cached -r .
44 | rm -rf .dist
45 |
46 | # Add google tag
47 | perl -p -i -e "s%%${GOOGLETAG}%" index.html
48 |
49 | # Push to gh-pages.
50 | git add -fA
51 | git commit --allow-empty -m "Automated Deployment [ci skip]"
52 | git push -f $ORIGIN_URL gh-pages
53 |
54 | # Move back to previous branch.
55 | git checkout -
56 |
57 | echo "Deployed Successfully!"
58 |
59 | exit 0
60 |
--------------------------------------------------------------------------------
/resources/public/css/site.css:
--------------------------------------------------------------------------------
1 | #app {
2 | width: 100vw;
3 | height: 100vh;
4 | }
5 |
6 | body {
7 | font-family: Roboto, Helvetica, Arial, sans-serif;
8 | /* background-color: #FAFAFA; */
9 | background-color: #F2F2F2;
10 | overflow-y: hidden; /* Disable pull down to refresh in chrome */
11 | }
12 |
13 | .mdl-dialog__wide {
14 | min-width: 280px;
15 | width: 280px;
16 | width: calc(50%);
17 | }
18 |
19 | .mdl-card__supporting-text {
20 | width: 90%; /* fallback if needed */
21 | width: calc(100% - 32px);
22 | }
23 |
24 | .mdl-card {
25 | width: 100%;
26 | min-height: 0px;
27 | }
28 |
29 | .mdl-card__menu > button {
30 | z-index: 2;
31 | }
32 |
33 |
34 | hr.border {
35 | margin: 0px;
36 | }
37 |
38 | .wide {
39 | width: 100%;
40 | }
41 |
42 | .tall {
43 | height: 100%;
44 | }
45 |
46 | .flex-v {
47 | display: flex;
48 | flex-direction: column;
49 | }
50 |
51 | .flex-h {
52 | display: flex;
53 | flex-direction: row;
54 | vertical-align: center;
55 | }
56 |
57 | .history {
58 | flex: auto;
59 | overflow-y: scroll;
60 | }
61 |
62 | .input {
63 | flex: none;
64 | }
65 |
66 | .input-field {
67 | flex: auto;
68 | }
69 |
70 | .run-button {
71 | flex: none;
72 | }
73 |
74 | .input textarea {
75 | resize: none;
76 | }
77 |
78 | .no-padding {
79 | padding: 0px;
80 | }
81 |
82 | .no-padding-top {
83 | padding-top: 0px;
84 | }
85 |
86 | .no-padding-bottom {
87 | padding-bottom: 0px;
88 | }
89 |
90 | .svg-size {
91 | height: 2.25em;
92 | }
93 |
94 | /* See: https://github.com/google/material-design-lite/issues/1407 */
95 | .mdl-card {
96 | overflow: visible;
97 | z-index: auto;
98 | }
99 |
100 | .white-bg {
101 | background-color: #ffffff;
102 | }
103 |
104 | .card-data, .padding-left {
105 | padding-left: 16px;
106 | }
107 |
108 | .error {
109 | color: #f00;
110 | }
111 |
112 | .card-data, .padding-right {
113 | padding-right: 16px;
114 | }
115 |
116 | .output, .result, .expression, .padding-top {
117 | padding-top: 16px;
118 | }
119 |
120 | .output, .result, .expression, .padding-bottom {
121 | padding-bottom: 16px;
122 | }
123 |
124 | .CodeMirror {
125 | height: auto;
126 | font-family: 'Roboto Mono', 'Courier New', monospace;
127 | }
128 |
129 | .CodeMirror-lines {
130 | padding: 0px;
131 | font-family: 'Roboto Mono', 'Courier New', monospace;
132 | }
133 |
--------------------------------------------------------------------------------
/src-backend/cljs/cljs_webrepl/repl.cljs:
--------------------------------------------------------------------------------
1 | (ns cljs-webrepl.repl
2 | (:require
3 | [clojure.string :as str :refer [blank? trim]]
4 | [cljs.core.async :refer [chan close! timeout put!]]
5 | [cljs-webrepl.io :as replumb-io]
6 | [replumb.core :as replumb]
7 | [replumb.repl :as replumb-repl]
8 | [taoensso.timbre :as timbre
9 | :refer-macros (tracef debugf infof warnf errorf)])
10 | (:require-macros
11 | [cljs.core.async.macros :refer [go go-loop]]))
12 |
13 | (def default-repl-opts
14 | (merge (replumb/options :browser
15 | ["/src/cljs" "/js/compiled/out"]
16 | replumb-io/fetch-file!)
17 | {:no-pr-str-on-value true
18 | :warning-as-error true
19 | :verbose false}))
20 |
21 | (def current-ns replumb-repl/current-ns)
22 |
23 | (defn native? [obj]
24 | (or (nil? obj) (boolean? obj) (string? obj) (number? obj) (keyword? obj) (coll? obj)))
25 |
26 |
27 | (defn obj->map* [obj acc key obj->map]
28 | (let [value (aget obj key)]
29 | (cond
30 | (fn? value) acc
31 | (native? value) (assoc acc (keyword key) value)
32 | (object? value) (obj->map obj)
33 | :else (assoc acc (keyword key) value))))
34 |
35 | (defn- obj->map
36 | "Workaround for `TypeError: Cannot convert object to primitive values`
37 | caused by `(js->clj (.-body exp-req) :keywordize-keys true)` apparently
38 | failing to correctly identify `(.-body exp-req)` as an object. Not sure
39 | what's causing this problem."
40 | [o]
41 | (when o
42 | (reduce #(obj->map* o %1 %2 obj->map) {} (js-keys o))))
43 |
44 | (defn add-cause [m cause]
45 | (if (some? cause)
46 | (assoc m :cause (str cause))
47 | m))
48 |
49 | (defn err->map [err]
50 | (when err
51 | (errorf "Evaluation: %s" err)
52 | (-> {:message (.-message err)
53 | :data (.-data err)}
54 | (add-cause (.-cause err)))))
55 |
56 | (defn fix-value [value]
57 | (cond (native? value) value
58 | (object? value) (obj->map value)
59 | :default (str value)))
60 |
61 | (defn on-repl-eval [[num expression] from-repl repl-opts]
62 | (debugf "on-repl-eval: %s %s" num expression)
63 | (put! from-repl [:repl/eval num (replumb-repl/current-ns) expression])
64 |
65 | (let [print-fn #(put! from-repl [:repl/print num %])
66 | result-fn #(put! from-repl [:repl/result num (replumb-repl/current-ns) %])]
67 | (binding [cljs.core/*print-newline* true
68 | cljs.core/*print-fn* print-fn]
69 | (replumb/read-eval-call repl-opts result-fn expression))))
70 |
71 | (defn repl-loop [from-repl to-repl repl-opts]
72 | (go
73 | (put! to-repl [:repl/eval nil "true"])
74 | (loop []
75 | (when-let [msg (
2 |
6 |
7 |
31 |
32 |
33 |
34 |
57 |
--------------------------------------------------------------------------------
/resources/public/images/cljs-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
31 |
32 |
46 |
--------------------------------------------------------------------------------
/src-frontend/cljs/cljs_webrepl/editor.cljs:
--------------------------------------------------------------------------------
1 | (ns cljs-webrepl.editor
2 | (:require
3 | [reagent.core :as r :refer [atom]]
4 | [clojure.string :as str]
5 | [cljsjs.codemirror]
6 | [cljsjs.codemirror.mode.clojure]
7 | [cljsjs.codemirror.keymap.emacs]
8 | [taoensso.timbre :as timbre
9 | :refer-macros (tracef debugf infof warnf errorf)]))
10 |
11 | (defn cm-options [options]
12 | (js-obj "readOnly" (:read-only? options false)
13 | "height" (name (:height options :auto))
14 | "autofocus" (:focus? options false)
15 | "lineWrapping" (:line-wrap? options false)
16 | "lineNumbers" (:line-numbers? options false)
17 | "mode" (name (:mode options :clojure))
18 | "indentUnit" (:indent options 2)
19 | "electricChars" (:electric-chars? options true)
20 | "viewportMargin" (:viewport-margin options js/Infinity)
21 | "extraKeys" (-> (:extra-keys options nil)
22 | (clj->js)
23 | (js/CodeMirror.normalizeKeyMap))))
24 |
25 | (defn cm-did-mount [node editor {:keys [on-input on-change on-key-down] :as options} text]
26 | (let [element (r/dom-node node)
27 | cm-opts (cm-options options)
28 | editor (reset! editor (js/CodeMirror.fromTextArea element cm-opts))]
29 | (.setValue editor text)
30 | (when on-change
31 | (.on editor "change" on-change))
32 | (when on-input
33 | (.on editor "input" on-input))))
34 |
35 | (defn cm-will-update [node editor [_ _ text]]
36 | (debugf "Update")
37 | (when-let [editor @editor]
38 | (.setValue editor text)
39 | (.refresh editor)))
40 |
41 | (defn cm-will-unmount [node editor]
42 | (when-let [editor @editor]
43 | true)
44 | (reset! editor nil))
45 |
46 | (defn cm-render [props text]
47 | [:textarea {:value text :read-only true}])
48 |
49 | (defn codemirror [props text]
50 | (let [editor (atom nil)]
51 | (r/create-class
52 | {:display-name "codemirror"
53 | :component-did-mount #(cm-did-mount %1 editor props text)
54 | :component-will-update #(cm-will-update %1 editor %2)
55 | :component-will-unmount #(cm-will-unmount %1 editor)
56 | :reagent-render cm-render})))
57 |
58 | (defn editor-update [{:keys [state] :as props} editor change]
59 | (swap! state assoc :input (.getValue editor)))
60 |
61 | (defn multi-line? [editor]
62 | (-> (.getValue editor)
63 | (str/includes? "\n")))
64 |
65 | (defn wrap-on-change [{:keys [on-change]}]
66 | (when on-change
67 | (fn [cm]
68 | (on-change (.getValue cm)))))
69 |
70 | (defn wrap-on-submit [{:keys [on-submit] :as props}]
71 | (fn [cm]
72 | (on-submit (.getValue cm))))
73 |
74 | (defn wrap-history-prev [{:keys [history-prev state] :as props}]
75 | (fn [cm]
76 | (.setValue cm (:input (history-prev)))
77 | (.refresh cm)))
78 |
79 | (defn wrap-history-next [{:keys [history-next state] :as props}]
80 | (fn [cm]
81 | (.setValue cm (:input (history-next)))
82 | (.refresh cm)))
83 |
84 | (defn wrap-ignore-multi [f]
85 | (fn [cm]
86 | (if (multi-line? cm)
87 | js/CodeMirror.Pass
88 | (f cm))))
89 |
90 | (defn insert-pair [cm pair]
91 | (doto cm
92 | (.replaceSelection pair)
93 | (.execCommand "goCharLeft")))
94 |
95 | (defn editor [props text]
96 | (let [on-change (wrap-on-change props)
97 | history-next (wrap-history-next props)
98 | history-prev (wrap-history-prev props)
99 | on-submit (wrap-on-submit props)
100 | extra-keys {:Up (-> history-prev wrap-ignore-multi)
101 | :Down (-> history-next wrap-ignore-multi)
102 | :Ctrl-Up history-prev
103 | :Ctrl-Down history-next
104 | :Enter (-> on-submit wrap-ignore-multi)
105 | :Ctrl-Enter on-submit
106 | :Shift-9 #(insert-pair % "()")
107 | "(" #(insert-pair % "()")
108 | "[" #(insert-pair % "[]")
109 | "{" #(insert-pair % "{}")
110 | "Shift-{" #(insert-pair % "{}")
111 | "Shift-'" #(insert-pair % "\"\"")}]
112 | (fn []
113 | [codemirror (-> {:editable? true
114 | :numbers? false
115 | :focus? true
116 | :extra-keys extra-keys
117 | :on-change on-change}
118 | (merge (dissoc props :on-change)))
119 | text])))
120 |
121 | (defn code [text]
122 | [codemirror {:read-only? true :mode "clojure"} text])
123 |
124 | (defn text [text]
125 | [codemirror {:read-only? true :mode "text"} text])
126 |
--------------------------------------------------------------------------------
/src-shared/cljs/cljs_webrepl/repl_thread.cljs:
--------------------------------------------------------------------------------
1 | (ns cljs-webrepl.repl-thread
2 | (:require
3 | [cljs.core.async :refer [chan close! timeout put! pipe]]
4 | [cognitect.transit :as transit]
5 | [cljs.tools.reader :as reader]
6 | [taoensso.timbre :as timbre
7 | :refer-macros (tracef debugf infof warnf errorf)])
8 | (:require-macros
9 | [cljs.core.async.macros :refer [go go-loop]]))
10 |
11 | (def script-name "js/backend.js")
12 |
13 | (def transit-writer (transit/writer :json))
14 | (def transit-reader (transit/reader :json))
15 |
16 | (defn write-transit [data]
17 | (transit/write transit-writer data))
18 |
19 | (defn read-transit [data]
20 | (transit/read transit-reader data))
21 |
22 | (defn write-edn [s]
23 | (binding [*print-level* nil
24 | *print-length* nil
25 | *print-dup* true]
26 | (pr-str s)))
27 |
28 | (extend-type js/Error
29 | IPrintWithWriter
30 | (-pr-writer [obj writer opts] (write-all writer "#error \"" (str obj) "\"")))
31 |
32 |
33 | (def edn-readers {'js #(clj->js %)
34 | 'uuid #(when (string? %)
35 | (uuid %))
36 | 'inst #(when (string? %)
37 | (js/Date. %))
38 | 'queue #(when (vector? %)
39 | (into cljs.core.PersistentQueue.EMPTY %))})
40 |
41 | (defn read-edn [s]
42 | (binding [reader/*default-data-reader-fn* (fn [tag value] value)
43 | reader/*data-readers* edn-readers]
44 | (reader/read-string s)))
45 |
46 | (defn worker? []
47 | (nil? js/self.document))
48 |
49 | (def thread-type (if (worker?)
50 | :worker
51 | :master))
52 |
53 | (defn read-transit-message [message]
54 | (let [message (aget message "content")]
55 | (try
56 | (read-transit message)
57 | (catch js/Error e
58 | (errorf "read-transit-message: %s %s: %s" thread-type (:message e) message)
59 | [:webworker/error nil e]))))
60 |
61 | (defn write-transit-message [message]
62 | (try
63 | (js-obj "format" "transit"
64 | "content" (write-transit message))
65 | (catch js/Error e nil)))
66 |
67 | (defn read-edn-message [message]
68 | (let [message (aget message "content")]
69 | (try
70 | (read-edn message)
71 | (catch js/Error e
72 | (errorf "read-edn-message: %s %s %s" thread-type e message)
73 | [:webworker/error nil e]))))
74 |
75 | (defn write-edn-message [message]
76 | (try
77 | (js-obj "format" "edn"
78 | "content" (write-edn message))
79 | (catch js/Error e nil)))
80 |
81 | (defn write-message [message]
82 | #_(debugf "write-message: %s %s" thread-type (pr-str message))
83 | (or (write-transit-message message)
84 | (write-edn-message message)
85 | (write-transit-message [:webworker/error nil (str "Unable to format message: " (pr message))])))
86 |
87 | (defn read-message [message]
88 | #_(debugf "read-message: %s %s" thread-type (pr-str message))
89 | (case (.-format message)
90 | "json" (read-transit-message message)
91 | "transit" (read-transit-message message)
92 | "edn" (read-edn-message message)
93 | [:webworker/error nil (str "Unknown message format: " (.-format message))]))
94 |
95 | (defn post-message [target message]
96 | (let [message (write-message message)]
97 | (debugf "post-message: %s %s" thread-type (pr-str message))
98 | (.postMessage target message)))
99 |
100 | (defn on-message [output-ch message]
101 | #_(debugf "on-message: %s %s" thread-type (pr-str message))
102 | (if message
103 | (put! output-ch (read-message message))
104 | (warnf "on-message: No message %s" thread-type)))
105 |
106 | (defn on-error [output-ch err]
107 | (errorf "on-error: %s %s" thread-type (pr-str err))
108 | (put! output-ch [:webworker/error nil (pr-str err)]))
109 |
110 | (defn- async-worker [& [target close-fn]]
111 | (let [is-worker? (not (some? target))
112 | target (or target js/self)
113 | input-ch (chan)
114 | output-ch (chan)
115 |
116 | finally-fn (fn []
117 | (debugf "Cleaning up WebWorker: %s" thread-type)
118 | (close! input-ch)
119 | (close! output-ch)
120 | (when close-fn
121 | (close-fn)))
122 | recv-fn (fn [event]
123 | (on-message output-ch (aget event "data")))
124 | error-fn (fn [err]
125 | (on-error output-ch err)
126 | (finally-fn))]
127 | (.addEventListener target "message" recv-fn)
128 | (.addEventListener target "error" error-fn)
129 | (go
130 | (loop []
131 | (when-let [message ( (.querySelector js/document (str "#" id))
46 | (.close)))
47 |
48 | (defn show-dialog [id]
49 | (when-let [dialog (.querySelector js/document (str "#" id))]
50 | (when-not (.-showModal dialog)
51 | (.registerDialog js/dialogPolyfill dialog))
52 | (.showModal dialog)))
53 |
54 | (defn show-reset-dialog []
55 | (show-dialog "reset-dialog"))
56 |
57 | (defn show-about-dialog []
58 | (show-dialog "about-dialog"))
59 |
60 | (defn pprint-str [data]
61 | (-> (with-out-str (fipp/pprint data))
62 | (str/trim-newline)))
63 |
64 | (defn unescape-string [data]
65 | (-> (println-str data)
66 | (str/trim-newline)))
67 |
68 | (defn trigger
69 | "Returns a reagent class that can be used to easily add triggers
70 | from the map in `props`, such as :component-did-mount. See
71 | `reagent.core/create-class` for more information."
72 | [props content]
73 | (r/create-class
74 | (-> {:display-name "trigger"}
75 | (merge props)
76 | (assoc :reagent-render (fn [_ content] content)))))
77 |
78 | (defn clipboard [child]
79 | (let [clipboard-atom (atom nil)]
80 | (r/create-class
81 | {:display-name "clipboard-button"
82 | :component-did-mount (fn [node]
83 | (let [clipboard (new js/Clipboard (r/dom-node node))]
84 | (reset! clipboard-atom clipboard)))
85 | :component-will-unmount (fn []
86 | (when-not (nil? @clipboard-atom)
87 | (.destroy @clipboard-atom)
88 | (reset! clipboard-atom nil)))
89 | :reagent-render (fn [child] child)})))
90 |
91 |
92 |
93 | (defn focus-node [node]
94 | (-> (r/dom-node node)
95 | (.focus)))
96 |
97 | (defn on-repl-eval [state num [ns expression]]
98 | (when num
99 | (swap! state update-in [:history num] assoc :ns ns :expression expression)))
100 |
101 | (defn on-repl-result [state num [ns result]]
102 | (if num
103 | (swap! state #(-> %
104 | (assoc :ns ns :ready? true)
105 | (update-in [:history num] assoc :result result)))
106 | (swap! state assoc :ns ns :ready? true)))
107 |
108 | (defn on-repl-print [state num [s]]
109 | (when num
110 | (swap! state update-in [:history num] update :output str s)))
111 |
112 | (defn on-repl-error [state num [err]]
113 | (when num
114 | (swap! state update-in [:history num] assoc :error err)))
115 |
116 | (defn on-repl-crash [state num [err]]
117 | (swap! state assoc :crashed? true :ready? false)
118 | (show-reset-dialog))
119 |
120 | (defn on-repl-event [state [name num & value]]
121 | (condp = name
122 | :repl/eval (on-repl-eval state num value)
123 | :repl/result (on-repl-result state num value)
124 | :repl/error (on-repl-error state num value)
125 | :repl/print (on-repl-print state num value)
126 | :webworker/error (on-repl-crash state num value)
127 | (warnf "Unknown repl event: %s %s" name value)))
128 |
129 | (defn repl-event-loop [state from-repl]
130 | (go-loop []
131 | (when-let [event ( %
154 | (merge default-state)
155 | (assoc :running? true :crashed? false :ready? false)
156 | (assoc :repl {:to-repl to-eval
157 | :from-repl from-repl})))
158 | (repl-event-loop state from-repl)
159 | (repl-eval-loop to-eval to-repl from-repl)))
160 |
161 | (defn eval-str! [expression]
162 | (let [{:keys [to-repl]} (:repl @state)
163 | expression (some-> expression str/trim)]
164 | (when (and (some? to-repl) (some? expression))
165 | (put! to-repl expression))))
166 |
167 | (defn history-prev [{:keys [cursor history] :as state}]
168 | (let [c (count history)
169 | new-cursor (inc cursor)]
170 | (if (<= new-cursor c)
171 | (assoc state
172 | :cursor new-cursor
173 | :input (:expression (get history (- c new-cursor))))
174 | state)))
175 |
176 | (defn history-next [{:keys [cursor history] :as state}]
177 | (let [c (count history)
178 | new-cursor (dec cursor)]
179 | (if (> new-cursor 0)
180 | (assoc state
181 | :cursor new-cursor
182 | :input (:expression (get history (- c new-cursor) "")))
183 | (assoc state :input ""))))
184 |
185 | (defn clear-input [state]
186 | (assoc state
187 | :cursor 0
188 | :input ""))
189 |
190 | (defn eval-input [state input]
191 | (let [expression (str/trim input)]
192 | (when-not (str/blank? expression)
193 | (eval-str! expression)
194 | (swap! state assoc :ready? false :input "" :cursor 0))))
195 |
196 | (defn input-on-change [state value]
197 | (assoc state :cursor 0 :input value))
198 |
199 | (defn scroll [node]
200 | (let [node (r/dom-node node)]
201 | (aset node "scrollTop" (.-scrollHeight node))))
202 |
203 | (defn scroll-on-update
204 | [child]
205 | (r/create-class
206 | {:display-name "scroll-on-update"
207 | :component-did-mount scroll
208 | :reagent-render identity}))
209 |
210 | (defn history-card-menu [props {:keys [num ns expression result output] :as history-item}]
211 | [mdl/upgrade
212 | [:div.mdl-card__menu
213 | [:button.mdl-button.mdl-js-button.mdl-button--icon.mdl-js-ripple-effect {:id (str "menu-" num)}
214 | [:i.material-icons "more_vert"]]
215 | [:ul.mdl-menu.mdl-menu--bottom-right.mdl-js-menu.mdl-js-ripple-effect
216 | {:for (str "menu-" num)}
217 | [:li.mdl-menu__item
218 | {:on-click #(eval-str! expression)}
219 | "Evaluate Again"]
220 | [clipboard
221 | [:li.mdl-menu__item
222 | {:data-clipboard-text expression}
223 | "Copy Expression"]]
224 | (if (some? output)
225 | [clipboard
226 | [:li.mdl-menu__item
227 | {:data-clipboard-text output}
228 | "Copy Output"]]
229 | [:li.mdl-menu__item
230 | {:disabled true}
231 | "Copy Output"])
232 | [clipboard
233 | [:li.mdl-menu__item
234 | {:data-clipboard-text (pr-str (:value result))}
235 | "Copy Result"]]]]])
236 |
237 | (defn history-card-expression [props ns expression]
238 | [:div.mdl-card__title
239 | [:div
240 | [:code.CodeMirror-lines (str ns "=>")]]
241 | [:div
242 | [editor/code expression]]])
243 |
244 | (defn history-card-output [props output]
245 | [:div
246 | [:div.card-data.output
247 | [editor/text (str/trim-newline output)]]
248 | [:hr.border]])
249 |
250 | (defn render-value [value]
251 | (cond
252 | (string? value)
253 | [editor/code (-> value unescape-string pprint-str)]
254 |
255 | (map? value)
256 | (case (:type value)
257 | :hiccup (:content value)
258 | [editor/code (pprint-str value)])
259 |
260 | :default
261 | [editor/code (pprint-str value)]))
262 |
263 | (defn render-error [error]
264 | [:pre.error
265 | (or (:stack error)
266 | (if (and (:message error) (not= "ERROR" (:message error)))
267 | (:message error)
268 | (str (:cause error))
269 | #_(or (str (:cause error)) (str error))))])
270 |
271 |
272 | (defn render-progress []
273 | [mdl/upgrade
274 | [:div.mdl-progress.mdl-js-progress.mdl-progress__indeterminate {:style {:width "100%"}}]])
275 |
276 | (defn history-card-result [props {:keys [success? value error] :as result}]
277 | [:div.card-data.result
278 | (if result
279 | (if success?
280 | [render-value value]
281 | [render-error error])
282 | [render-progress])])
283 |
284 | (defn history-card [{:keys [state columns] :as props} {:keys [ns expression result output] :as history-item}]
285 | [:div
286 | {:class (str "mdl-cell " (card-size-class columns))}
287 | [:div.mdl-card.mdl-shadow--2dp
288 | [history-card-expression props ns expression]
289 | [history-card-menu props history-item]
290 | [:hr.border]
291 | (when (seq output)
292 | [history-card-output props output])
293 | [history-card-result props result]]])
294 |
295 | (defn please-wait [props]
296 | [:div.history
297 | [:div.mdl-grid
298 | [:div.mdl-cell.mdl-cell--12-col
299 | [:p "REPL initializing..."]]]])
300 |
301 | (defn run-button [{:keys [state on-submit ready?] :as props}]
302 | (let [disabled? (or ready? (str/blank? (:input @state)))]
303 | [:div.padding-left
304 | ^{:key disabled?}
305 | [mdl/upgrade
306 | [:button.mdl-button.mdl-js-button.mdl-button--fab.mdl-js-ripple-effect.mdl-button--colored
307 | {:disabled disabled?
308 | :on-click #(on-submit (:input @state))}
309 | [:i.material-icons "send"]]]]))
310 |
311 | (defn repl-input [{:keys [state] :as props}]
312 | (let [on-change #(swap! state assoc :input %)
313 | props (assoc props :on-change on-change)]
314 | (fn []
315 | [mdl/upgrade
316 | [:div.input-field.mdl-textfield.mdl-js-textfield.mdl-textfield--floating-label
317 | [:div.mdl-textfield__label {:for "repl-input"}
318 | (str (:ns @state) "=>")]
319 | [:div.mdl-textfield__input {:id "repl-input"}
320 | [editor/editor props (:input @state)]]]])))
321 |
322 | (defn input-card [props num]
323 | [:div.mdl-cell.mdl-cell--12-col
324 | [:div.mdl-card.mdl-shadow--2dp
325 | [:div.card-data.expression
326 | [:div.flex-h
327 | [repl-input props]
328 | [run-button props]]]]])
329 |
330 |
331 | (defn history [props]
332 | ^{:key (count (:history @state))}
333 | [scroll-on-update
334 | [:div.history
335 | [:div.mdl-grid
336 | (doall
337 | (for [[num history-item] (:history @state)]
338 | ^{:key num}
339 | [history-card props (assoc history-item :num num)]))
340 | (when (:ready? @state)
341 | [input-card props])]]])
342 |
343 |
344 | (defn reset-dialog [{:keys [state]}]
345 | [:dialog.mdl-dialog {:id "reset-dialog"}
346 | (when (:crashed? @state)
347 | [:div.mdl-dialog__title
348 | [:p "The REPL has crashed!"]])
349 | [:div.mdl-dialog__content
350 | [:p "Reset REPL? All data will be lost."]]
351 | [:div.mdl-dialog__actions
352 | [:button.mdl-button
353 | {:on-click (fn []
354 | (close-dialog "reset-dialog")
355 | (reset-repl! state))}
356 | "Reset"]
357 | [:button.mdl-button
358 | {:on-click #(close-dialog "reset-dialog")}
359 | "Cancel"]]])
360 |
361 | (defn crash-dialog [{:keys [state]}]
362 | [:dialog.mdl-dialog {:id "crash-dialog"}
363 | [:div.mdl-dialog__content
364 | [:p "The REPL has crashed!"]]
365 | [:div.mdl-dialog__actions
366 | [:button.mdl-button
367 | {:on-click (fn []
368 | (close-dialog "crash-dialog")
369 | (reset-repl! state))}
370 | "Reset"]
371 | [:button.mdl-button
372 | {:on-click #(close-dialog "crash-dialog")}
373 | "Cancel"]]])
374 |
375 | (defn about-dialog [props]
376 | [:dialog.mdl-dialog.mdl-dialog__wide {:id "about-dialog"}
377 | [:h4.mdl-dialog__title (str "CLJS-WebREPL")]
378 | [:div.mdl-dialog__content
379 | [:p "A ClojureScript browser based REPL"]
380 | [:p (str "Using ClojureScript version " *clojurescript-version*)]
381 | [:p
382 | "Running: " (if (:running? @state) "Yes" "No") [:br]
383 | "Ready: " (if (:ready? @state) "Yes" "No") [:br]
384 | "Crashed: " (if (:crashed? @state) "Yes" "No")]
385 | [:h5 "License"]
386 | [:p
387 | "Copyright © 2016 Andrew Phillips, Dan Holmsand, Mike Fikes, David Nolen, Rich Hickey, Joel Martin & Contributors"]
388 | [:p
389 | "Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version."]]
390 | [:div.mdl-dialog__actions
391 | [:button.mdl-button
392 | {:on-click #(close-dialog "about-dialog")}
393 | "Ok"]]])
394 |
395 | (defn reset-repl-button [props]
396 | [:button.mdl-button.mdl-js-button.mdl-js-ripple-effect
397 | {:on-click show-reset-dialog
398 | :style {:color "#fff"}}
399 | [:i.material-icons "report"]
400 | [:span.mdl-cell--hide-phone "RESET"]])
401 |
402 | (defn more-columns-button [{:keys [more-columns]}]
403 | [:button.mdl-button.mdl-js-button.mdl-js-ripple-effect.mdl-cell--hide-phone
404 | {:on-click more-columns
405 | :style {:color "#fff"}}
406 | [:i.material-icons "view_column"]])
407 |
408 | (defn less-columns-button [{:keys [less-columns]}]
409 | [:button.mdl-button.mdl-js-button.mdl-js-ripple-effect.mdl-cell--hide-phone
410 | {:on-click less-columns
411 | :style {:color "#fff"}}
412 | [:i.material-icons "view_stream"]])
413 |
414 | (defn menu-button [id]
415 | [:button.mdl-button.mdl-js-button.mdl-button--icon.mdl-js-ripple-effect {:id "main-menu"}
416 | [:i.material-icons "more_vert"]])
417 |
418 | (defn more-columns-menu-item [{:keys [more-columns]}]
419 | [:li.mdl-menu__item {:on-click more-columns} "More Columns"])
420 |
421 | (defn less-columns-menu-item [{:keys [less-columns]}]
422 | [:li.mdl-menu__item {:on-click less-columns} "Less Columns"])
423 |
424 | (defn github-menu-item [{:keys [github]}]
425 | [:li.mdl-menu__item {:on-click github} "GitHub"])
426 |
427 | (defn about-menu-item [props]
428 | [:li.mdl-menu__item {:on-click show-about-dialog} "About"])
429 |
430 | (defn reset-menu-item [props]
431 | [:li.mdl-menu__item {:on-click show-reset-dialog} "Reset REPL"])
432 |
433 | (defn home-page-menu [props]
434 | [:ul.mdl-menu.mdl-menu--bottom-right.mdl-js-menu.mdl-js-ripple-effect
435 | {:for "main-menu"}
436 | [more-columns-menu-item props]
437 | [less-columns-menu-item props]
438 | [reset-menu-item props]
439 | [github-menu-item props]
440 | [about-menu-item props]])
441 |
442 | (defn home-page-title [{:keys [title title-icon] :as props}]
443 | [:span.mdl-layout-title
444 | [:img.svg-size {:src title-icon}]
445 | (str " " title)])
446 |
447 | (defn home-page-header [{:keys [reset-repl more-columns] :as props}]
448 | [:header.mdl-layout__header
449 | [:div.mdl-layout__header-row
450 | [home-page-title props]
451 | [:div.mdl-layout-spacer]
452 | [reset-repl-button props]
453 | [less-columns-button props]
454 | [more-columns-button props]
455 | [menu-button "main-menu"]
456 | [home-page-menu props]]])
457 |
458 | (defn home-page [{:keys [state] :as props}]
459 | [:div
460 | [about-dialog props]
461 | [reset-dialog props]
462 | [:div.tall
463 | [mdl/upgrade
464 | [:div.flex-v.tall.mdl-layout.mdl-js-layout.mdl-layout--fixed-header.mdl-layout--no-drawer-button
465 | [home-page-header props]
466 | (if (:ns @state)
467 | [history (assoc props :columns (:columns @state))]
468 | [please-wait props])]]]])
469 |
470 | (defn mount-root []
471 | (let [input (r/cursor state [:input])
472 | props {:state state
473 | :input input
474 | :on-submit #(eval-input state %)
475 | :history-prev #(swap! state history-prev)
476 | :history-next #(swap! state history-next)
477 | :more-columns #(swap! state more-columns)
478 | :less-columns #(swap! state less-columns)
479 | :github #(set! (.-location js/window) "https://github.com/theasp/cljs-webrepl")
480 | :title-icon "images/cljs-white.svg"
481 | :title "Cljs-WebREPL"}]
482 | (r/render [home-page props] (.getElementById js/document "app"))))
483 |
484 | (defn init! []
485 | (reset-repl! state)
486 | (mount-root))
487 |
--------------------------------------------------------------------------------