├── .gitignore ├── .gitmodules ├── Readme.md ├── build.boot ├── screenshot.png ├── src ├── parinfer_codemirror └── reepl │ ├── code_mirror.cljs │ ├── completions.cljs │ ├── core.cljs │ ├── example.cljs │ ├── handlers.cljs │ ├── helpers.cljs │ ├── parinferize.cljs │ ├── repl_items.cljs │ ├── replumb.cljs │ ├── show_devtools.cljs │ ├── show_function.cljs │ ├── show_value.cljs │ ├── subs.cljs │ └── timers.cljs └── static ├── index.html └── main.css /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | build 3 | pages 4 | replumb 5 | jared 6 | .nrepl-port 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "parinfer"] 2 | path = parinfer 3 | url = git@github.com:jaredly/codemirror-parinfer.git 4 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Reepl 2 | The cljs Read-eval-print-loop that really understands you 3 | 4 |  5 | 6 | ## [Try it](https://jaredforsyth.com/reepl) 7 | 8 | ### Features 9 | 10 | - auto-completion 11 | - rich formatting of cljs objects 12 | - parinfer 13 | 14 | `src/reepl/example.cljs` is the `main` on that page, and you can see how this lib can be used. 15 | 16 | ## Building yourself 17 | Make sure you `git submodule update --init`. 18 | 19 | Grab the latest `boot` (version 2.5.5 at the time of writing), run `boot dev`, and then open `http://localhost:3002`. 20 | 21 | ## Things you can configure 22 | 23 | - how statements are eval'd (replumb-based setup available for your convenience) 24 | - how completion works (replumb-based fn used in the example has completion for `js/` as well!) 25 | - how documentation is gotten (the example uses a custom impl based on cljs source) 26 | - how values are displayed (the example uses [cljs-devtools](https://github.com/binaryage/cljs-devtools) for formatting most things) 27 | 28 | ## Extra Dependencies 29 | 30 | - codemirror: you can see in `build.boot` how to get the css imported correctly 31 | - parinfer-codemirror: check out the `build.boot` for this too, and I'm currently using a [custom fork](https://github.com/jaredly/codemirror-parinfer) 32 | 33 | ## Powered By 34 | 35 | - [Clojurescript](https://github.com/clojure/clojurescript) 36 | - [Boot](http://boot-clj.com/) 37 | - [Reagent](http://reagent-project.github.io/) (which uses [React](https://facebook.github.io/react/)) 38 | - [Cljs-Devtools](https://github.com/binaryage/cljs-devtools/) 39 | - [Parinfer](https://shaunlebron.github.io/parinfer/) 40 | - [Replumb](https://github.com/ScalaConsultants/replumb/) (optional) 41 | -------------------------------------------------------------------------------- /build.boot: -------------------------------------------------------------------------------- 1 | (set-env! 2 | :source-paths #{"src"} 3 | :resource-paths #{"html"} 4 | :dependencies 5 | '[ 6 | [adzerk/boot-cljs "1.7.48-5" :scope "test"] 7 | [adzerk/boot-cljs-repl "0.3.0" :scope "test"] 8 | [adzerk/boot-reload "0.4.9" :scope "test"] 9 | [pandeiro/boot-http "0.6.3" :scope "test"] 10 | [crisptrutski/boot-cljs-test "0.2.1" :scope "test"] 11 | [zilti/boot-typed "0.1.1" :scope "test"] 12 | [adzerk/bootlaces "0.1.13"] 13 | 14 | [replumb "0.1.5-SNAPSHOT"] 15 | [parinfer "0.2.3"] 16 | [reagent "0.5.1"] 17 | [re-frame "0.6.0"] 18 | [binaryage/devtools "0.4.1"] 19 | [cljsjs/codemirror "5.10.0-0"] 20 | [quil "2.3.0"] 21 | 22 | [ajchemist/boot-figwheel "0.5.0-0"] ;; latest release 23 | [com.cemerick/piggieback "0.2.1" :scope "test"] 24 | [figwheel-sidecar "0.5.0-2" :scope "test"] 25 | 26 | [weasel "0.7.0" :scope "test"] 27 | [org.clojure/tools.nrepl "0.2.12" :scope "test"] 28 | 29 | [org.clojure/clojure "1.8.0"] 30 | [org.clojure/core.typed "0.3.18"] 31 | [org.clojure/clojurescript "1.9.14"] 32 | ]) 33 | 34 | (require 35 | '[adzerk.bootlaces :refer :all] 36 | '[adzerk.boot-cljs :refer [cljs]] 37 | '[adzerk.boot-cljs-repl :refer [cljs-repl start-repl]] 38 | '[adzerk.boot-reload :refer [reload]] 39 | '[crisptrutski.boot-cljs-test :refer [test-cljs]] 40 | '[pandeiro.boot-http :refer [serve]]) 41 | 42 | (require 'boot-figwheel) 43 | (refer 'boot-figwheel :rename '{cljs-repl fw-cljs-repl}) 44 | 45 | (def +version+ "1.0.1") 46 | (bootlaces! +version+) 47 | 48 | (task-options! 49 | pom {:project 'reepl 50 | :version +version+ 51 | :description "A configurable in-browser clojurescript REPL" 52 | :url "https://github.com/jaredly/reepl" 53 | :scm {:url "https://github.com/adzerk/bootlaces"} 54 | :license {"ISC License" "https://opensource.org/licenses/ISC"}} 55 | figwheel {:build-ids ["dev"] 56 | :all-builds [{:id "dev" 57 | :compiler {:main 'reepl.example 58 | :output-to "example.js"} 59 | :figwheel {:build-id "dev" 60 | :on-jsload "reepl.example/main" 61 | :heads-up-display true 62 | :autoload true 63 | :debug false}}] 64 | :figwheel-options {:repl true 65 | :http-server-root "target" 66 | :css-dirs ["target"] 67 | :open-file-command "emacsclient"}}) 68 | 69 | (deftask auto-test [] 70 | (set-env! :source-paths #{"src" "test"}) 71 | (comp (watch) 72 | (speak) 73 | (test-cljs))) 74 | 75 | (def foreign-libs 76 | [{:file "parinfer/resources/public/codemirror/mode/clojure/clojure-parinfer.js" 77 | :provides ["parinfer.codemirror.mode.clojure.clojure-parinfer"]}]) 78 | 79 | (deftask dev [] 80 | (set-env! :source-paths #{"src"}) 81 | (set-env! :asset-paths #{"static"}) 82 | (comp 83 | (target :dir #{"target"}) 84 | (serve :dir "target" :port 3002) 85 | (watch) 86 | ;;(speak) 87 | (reload :on-jsload 'reepl.example/main) 88 | (cljs-repl) 89 | (cljs :source-map true 90 | :compiler-options {:foreign-libs foreign-libs} 91 | :optimizations :none) 92 | (sift :add-jar 93 | {'cljsjs/codemirror 94 | #"cljsjs/codemirror/development/codemirror.css"}) 95 | (sift :move 96 | {#"cljsjs/codemirror/development/codemirror.css" 97 | "vendor/codemirror/codemirror.css"}) 98 | (target :dir #{"target"}) 99 | )) 100 | 101 | (deftask devfw [] 102 | (set-env! :source-paths #(into % ["src"])) 103 | (comp (repl) (figwheel))) 104 | 105 | (deftask build [] 106 | (set-env! :source-paths #{"src"}) 107 | (set-env! :asset-paths #{"static"}) 108 | (comp 109 | (target :dir #{"build"}) 110 | (cljs :source-map true 111 | :compiler-options {;;:asset-path "target/out" 112 | :foreign-libs foreign-libs} 113 | :optimizations :simple) 114 | (sift :add-jar {'cljsjs/codemirror #"cljsjs/codemirror/development/codemirror.css"}) 115 | (sift :move {#"cljsjs/codemirror/development/codemirror.css" 116 | "vendor/codemirror/codemirror.css"}) 117 | (target :dir #{"build"}) 118 | )) 119 | 120 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredly/reepl/96a8979c574b3979a7aeeed27e57a2ec4d357350/screenshot.png -------------------------------------------------------------------------------- /src/parinfer_codemirror: -------------------------------------------------------------------------------- 1 | ../parinfer/src/parinfer_codemirror -------------------------------------------------------------------------------- /src/reepl/code_mirror.cljs: -------------------------------------------------------------------------------- 1 | (ns reepl.code-mirror 2 | (:require [clojure.string :as str] 3 | [reagent.core :as r] 4 | [cljsjs.codemirror] 5 | [cljsjs.codemirror.addon.edit.closebrackets] 6 | [cljsjs.codemirror.addon.edit.matchbrackets] 7 | [cljsjs.codemirror.addon.hint.show-hint] 8 | [cljsjs.codemirror.addon.runmode.runmode] 9 | [cljsjs.codemirror.addon.runmode.colorize] 10 | [cljsjs.codemirror.mode.clojure] 11 | [cljsjs.codemirror.mode.javascript] 12 | [cljsjs.codemirror.keymap.vim] 13 | [cljs.pprint :as pprint])) 14 | 15 | ;; TODO can we avoid the global state modification here? 16 | #_(js/CodeMirror.registerHelper 17 | "wordChars" 18 | "clojure" 19 | #"[^\s\(\)\[\]\{\},`']") 20 | 21 | (def wordChars 22 | "[^\\s\\(\\)\\[\\]\\{\\},`']*") 23 | 24 | (defn word-in-line 25 | [line lno cno] 26 | (let [ 27 | back (get (.match (.slice line 0 cno) (js/RegExp. (str wordChars "$"))) 0) 28 | forward (get (.match (.slice line cno) (js/RegExp. (str "^" wordChars))) 0) 29 | ] 30 | {:start #js {:line lno 31 | :ch (- cno (count back))} 32 | :end #js {:line lno 33 | :ch (+ cno (count forward))} 34 | })) 35 | 36 | (defn cm-current-word 37 | "Find the current 'word' according to CodeMirror's `wordChars' list" 38 | [cm] 39 | (let [pos (.getCursor cm) 40 | lno (.-line pos) 41 | cno (.-ch pos) 42 | line (.getLine cm lno) 43 | ] 44 | ;; findWordAt doesn't work w/ clojure-parinfer mode 45 | ;; (.findWordAt cm back) 46 | (word-in-line line lno cno) 47 | )) 48 | 49 | (defn repl-hint 50 | "Get a new completion state." 51 | [complete-word cm options] 52 | (let [range (cm-current-word cm) 53 | text (.getRange cm 54 | (:start range) 55 | (:end range)) 56 | words (when (not (empty? text)) 57 | (vec (complete-word text)))] 58 | (when-not (empty? words) 59 | {:list words 60 | :num (count words) 61 | :active (= (get (first words) 2) text) 62 | :show-all false 63 | :initial-text text 64 | :pos 0 65 | :from (:start range) 66 | :to (:end range)}))) 67 | 68 | (defn cycle-pos 69 | "Cycle through positions. Returns [active? new-pos]. 70 | 71 | count 72 | total number of completions 73 | current 74 | current position 75 | go-back? 76 | should we be going in reverse 77 | initial-active 78 | if false, then we return not-active when wrapping around" 79 | [count current go-back? initial-active] 80 | (if go-back? 81 | (if (>= 0 current) 82 | (if initial-active 83 | [true (dec count)] 84 | [false 0]) 85 | [true (dec current)]) 86 | (if (>= current (dec count)) 87 | (if initial-active 88 | [true 0] 89 | [false 0]) 90 | [true (inc current)]))) 91 | 92 | (defn cycle-completions 93 | "Cycle through completions, changing the codemirror text accordingly. Returns 94 | a new state map. 95 | 96 | state 97 | the current completion state 98 | go-back? 99 | whether to cycle in reverse (generally b/c shift is pressed) 100 | cm 101 | the codemirror instance 102 | evt 103 | the triggering event. it will be `.preventDefault'd if there are completions 104 | to cycle through." 105 | [{:keys [num pos active from to list initial-text] :as state} 106 | go-back? cm evt] 107 | (when (and state (or (< 1 (count list)) 108 | (and (< 0 (count list)) 109 | (not (= initial-text (get (first list) 2)))))) 110 | (.preventDefault evt) 111 | (let [initial-active (= initial-text (get (first list) 2)) 112 | [active pos] (if active 113 | (cycle-pos num pos go-back? initial-active) 114 | [true (if go-back? (dec num) pos)]) 115 | text (if active 116 | (get (get list pos) 2) 117 | initial-text)] 118 | ;; TODO don't replaceRange here, instead watch the state atom and react to 119 | ;; that. 120 | (.replaceRange cm text from to) 121 | (assoc state 122 | :pos pos 123 | :active active 124 | :to #js {:line (.-line from) 125 | :ch (+ (count text) 126 | (.-ch from))})))) 127 | 128 | ;; TODO refactor this to be smaller 129 | (defn code-mirror 130 | "Create a code-mirror editor that knows a fair amount about being a good 131 | repl. The parameters: 132 | 133 | value-atom (reagent atom) 134 | when this changes, the editor will update to reflect it. 135 | 136 | options (TODO finish documenting) 137 | 138 | :style (reagent style map) 139 | will be applied to the container element 140 | 141 | :on-change (fn [text] -> nil) 142 | :on-eval (fn [text] -> nil) 143 | :on-up (fn [] -> nil) 144 | :on-down (fn [] -> nil) 145 | :should-go-up 146 | :should-go-down 147 | :should-eval 148 | 149 | :js-cm-opts 150 | options passed into the CodeMirror constructor 151 | 152 | :on-cm-init (fn [cm] -> nil) 153 | called with the CodeMirror instance, for whatever extra fiddling you want to do." 154 | [value-atom {:keys [style 155 | on-change 156 | on-eval 157 | on-up 158 | on-down 159 | complete-atom 160 | complete-word 161 | should-go-up 162 | should-go-down 163 | should-eval 164 | js-cm-opts 165 | on-cm-init]}] 166 | 167 | (let [cm (atom nil)] 168 | (r/create-class 169 | {:component-did-mount 170 | (fn [this] 171 | (let [el (r/dom-node this) 172 | ;; On Escape, should we revert to the pre-completion-text? 173 | cancel-keys #{13 27} 174 | cmp-ignore #{9 16 17 18 91 93} 175 | cmp-show #{17 18 91 93} 176 | inst (js/CodeMirror. 177 | el 178 | (clj->js 179 | (merge 180 | {:lineNumbers false 181 | :viewportMargin js/Infinity 182 | :matchBrackets true 183 | :autofocus true 184 | :extraKeys #js {"Shift-Enter" "newlineAndIndent"} 185 | :value @value-atom 186 | :autoCloseBrackets true 187 | :mode "clojure"} 188 | js-cm-opts)))] 189 | 190 | (reset! cm inst) 191 | (.on inst "change" 192 | (fn [] 193 | (let [value (.getValue inst)] 194 | (when-not (= value @value-atom) 195 | (on-change value))))) 196 | 197 | (.on inst "keyup" 198 | (fn [inst evt] 199 | (if (cancel-keys (.-keyCode evt)) 200 | (reset! complete-atom nil) 201 | (if (cmp-show (.-keyCode evt)) 202 | (swap! complete-atom assoc :show-all false) 203 | (when-not (cmp-ignore (.-keyCode evt)) 204 | (reset! complete-atom (repl-hint complete-word inst nil)) 205 | ))) 206 | )) 207 | 208 | (.on inst "keydown" 209 | (fn [inst evt] 210 | (case (.-keyCode evt) 211 | (17 18 91 93) 212 | (swap! complete-atom assoc :show-all true) 213 | ;; tab 214 | 9 (do 215 | ;; TODO: do I ever want to use TAB normally? 216 | ;; Maybe if there are no completions... 217 | ;; Then I'd move this into cycle-completions? 218 | (swap! complete-atom 219 | cycle-completions 220 | (.-shiftKey evt) 221 | inst 222 | evt)) 223 | ;; enter 224 | 13 (let [source (.getValue inst)] 225 | (when (should-eval source inst evt) 226 | (.preventDefault evt) 227 | (on-eval source))) 228 | ;; up 229 | 38 (let [source (.getValue inst)] 230 | (when (and (not (.-shiftKey evt)) 231 | (should-go-up source inst)) 232 | (.preventDefault evt) 233 | (on-up))) 234 | ;; down 235 | 40 (let [source (.getValue inst)] 236 | (when (and (not (.-shiftKey evt)) 237 | (should-go-down source inst)) 238 | (.preventDefault evt) 239 | (on-down))) 240 | :none) 241 | )) 242 | (when on-cm-init 243 | (on-cm-init inst)) 244 | )) 245 | 246 | :component-did-update 247 | (fn [this old-argv] 248 | (when-not (= @value-atom (.getValue @cm)) 249 | (.setValue @cm @value-atom) 250 | (let [last-line (.lastLine @cm) 251 | last-ch (count (.getLine @cm last-line))] 252 | (.setCursor @cm last-line last-ch)))) 253 | 254 | :reagent-render 255 | (fn [_ _ _] 256 | @value-atom 257 | [:div {:style style}])}))) 258 | 259 | (defn colored-text [text style] 260 | (r/create-class 261 | {:component-did-mount 262 | (fn [this] 263 | (let [node (r/dom-node this)] 264 | ((aget js/CodeMirror "colorize") #js[node] "clojure"))) 265 | :reagent-render 266 | (fn [_] 267 | [:pre {:style (merge {:padding 0 :margin 0} style)} 268 | text])})) 269 | -------------------------------------------------------------------------------- /src/reepl/completions.cljs: -------------------------------------------------------------------------------- 1 | (ns reepl.completions 2 | (:require [re-frame.core :refer [dispatch 3 | dispatch-sync 4 | subscribe]] 5 | [clojure.string :as str] 6 | [cljs.reader] 7 | [cljs.tools.reader] 8 | 9 | [reagent.core :as r] 10 | [reepl.code-mirror :as code-mirror] 11 | [reepl.show-value :refer [show-value]] 12 | [reepl.repl-items :refer [repl-items]] 13 | 14 | [reepl.handlers :as handlers] 15 | [reepl.subs :as subs] 16 | [reepl.helpers :as helpers] 17 | ) 18 | (:require-macros 19 | [reagent.ratom :refer [reaction]])) 20 | 21 | (def styles 22 | {:completion-container {:position :relative 23 | :font-size 12} 24 | :completion-list {:flex-direction :row 25 | :overflow :hidden 26 | :height 20} 27 | :completion-empty {:color "#ccc" 28 | ;;:font-weight :bold 29 | :padding "3px 10px"} 30 | 31 | :completion-show-all {:position :absolute 32 | :top 0 33 | :left 0 34 | :right 0 35 | :z-index 1000 36 | :flex-direction :row 37 | :background-color "#eef" 38 | :flex-wrap :wrap} 39 | :completion-item {;; :cursor :pointer TODO make these clickable 40 | :padding "3px 5px 3px"} 41 | :completion-selected {:background-color "#eee"} 42 | :completion-active {:background-color "#aaa"} 43 | }) 44 | 45 | (def view (partial helpers/view styles)) 46 | (def text (partial helpers/text styles)) 47 | (def button (partial helpers/button styles)) 48 | 49 | (def canScrollIfNeeded 50 | (not (nil? (.-scrollIntoViewIfNeeded js/document.body)))) 51 | 52 | (defn completion-item [text is-selected is-active set-active] 53 | (r/create-class 54 | {:component-did-update 55 | (fn [this [_ _ old-is-selected]] 56 | (let [[_ _ is-selected] (r/argv this)] 57 | (if (and (not old-is-selected) 58 | is-selected) 59 | (if canScrollIfNeeded 60 | (.scrollIntoViewIfNeeded (r/dom-node this) false) 61 | (.scrollIntoView (r/dom-node this)))))) 62 | :reagent-render 63 | (fn [text is-selected is-active set-active] 64 | [view {:on-click set-active 65 | :style [:completion-item 66 | (and is-selected 67 | (if is-active 68 | :completion-active 69 | :completion-selected))]} 70 | text])})) 71 | 72 | (defn completion-list [{:keys [pos list active show-all]} set-active] 73 | (let [items (map-indexed 74 | #(-> [completion-item 75 | (get %2 2) 76 | (= %1 pos) 77 | active 78 | (partial set-active %1)]) 79 | list) 80 | ] 81 | [view :completion-container 82 | (if show-all 83 | (into [view :completion-show-all] items)) 84 | (if (empty? items) 85 | [view :completion-empty "This is where completions show up"] 86 | (into 87 | [view :completion-list] 88 | items)) 89 | ])) 90 | -------------------------------------------------------------------------------- /src/reepl/core.cljs: -------------------------------------------------------------------------------- 1 | (ns reepl.core 2 | (:require [re-frame.core :refer [dispatch 3 | dispatch-sync 4 | subscribe]] 5 | [clojure.string :as str] 6 | [cljs.reader] 7 | [cljs.tools.reader] 8 | 9 | [reagent.core :as r] 10 | [reepl.code-mirror :as code-mirror] 11 | [reepl.show-value :refer [show-value]] 12 | [reepl.repl-items :refer [repl-items]] 13 | [reepl.completions :refer [completion-list]] 14 | 15 | [reepl.handlers :as handlers] 16 | [reepl.subs :as subs] 17 | [reepl.helpers :as helpers] 18 | ) 19 | (:require-macros 20 | [reagent.ratom :refer [reaction]])) 21 | 22 | (def styles 23 | {:container {:font-family "monospace" 24 | :flex 1 25 | :display :flex 26 | :white-space "pre-wrap"} 27 | 28 | :intro-message {:padding "10px 20px" 29 | :line-height 1.5 30 | :border-bottom "1px solid #aaa" 31 | :flex-direction :row 32 | :margin-bottom 10} 33 | :intro-code {:background-color "#eee" 34 | :padding "0 5px"} 35 | 36 | :completion-container {:position :relative 37 | :font-size 12} 38 | :completion-list {:flex-direction :row 39 | :overflow :hidden 40 | :height 20} 41 | :completion-empty {:color "#ccc" 42 | ;;:font-weight :bold 43 | :padding "3px 10px"} 44 | 45 | :completion-show-all {:position :absolute 46 | :top 0 47 | :left 0 48 | :right 0 49 | :z-index 1000 50 | :flex-direction :row 51 | :background-color "#eef" 52 | :flex-wrap :wrap} 53 | :completion-item {;; :cursor :pointer TODO make these clickable 54 | :padding "3px 5px 3px"} 55 | :completion-selected {:background-color "#eee"} 56 | :completion-active {:background-color "#aaa"} 57 | 58 | :docs {:height 200 59 | :overflow :auto 60 | :padding "5px 10px"} 61 | :docs-empty {:color "#ccc" 62 | :padding "5px 10px"} 63 | 64 | :input-container {:flex-direction :row 65 | :border-top "2px solid #eee" 66 | :border-bottom "2px solid #eee"} 67 | :main-caret {:padding "8px 5px 8px 10px" 68 | :margin-right 0 69 | :flex-direction :row} 70 | 71 | :input-caret {:color "#55f" 72 | :margin-right 10} 73 | }) 74 | 75 | (def view (partial helpers/view styles)) 76 | (def text (partial helpers/text styles)) 77 | (def button (partial helpers/button styles)) 78 | 79 | (defn is-valid-cljs? [source] 80 | (try 81 | (do 82 | (cljs.tools.reader/read-string source) 83 | true) 84 | (catch js/Error _ 85 | false))) 86 | 87 | ;; TODO these should probably go inside code-mirror.cljs? They are really 88 | ;; coupled to CodeMirror.... 89 | (def default-cm-opts 90 | {:should-go-up 91 | (fn [source inst] 92 | (let [pos (.getCursor inst)] 93 | (= 0 (.-line pos))) 94 | ) 95 | 96 | :should-go-down 97 | (fn [source inst] 98 | (let [pos (.getCursor inst) 99 | last-line (.lastLine inst)] 100 | (= last-line (.-line pos)))) 101 | 102 | ;; TODO if the cursor is inside a list, and the function doesn't have enought 103 | ;; arguments yet, then return false 104 | ;; e.g. (map |) <- map needs at least one argument. 105 | :should-eval 106 | (fn [source inst evt] 107 | (if (.-shiftKey evt) 108 | false 109 | (if (.-metaKey evt) 110 | true 111 | (let [lines (.lineCount inst) 112 | in-place (or (= 1 lines) 113 | (let [pos (.getCursor inst) 114 | last-line (dec lines)] 115 | (and 116 | (= last-line (.-line pos)) 117 | (= (.-ch pos) 118 | (count (.getLine inst last-line))))))] 119 | (and in-place 120 | (is-valid-cljs? source)))))) 121 | }) 122 | 123 | (defn repl-input [state submit cm-opts] 124 | {:pre [(every? (comp not nil?) 125 | (map cm-opts 126 | [:on-up 127 | :on-down 128 | :complete-atom 129 | :complete-word 130 | :on-change]))]} 131 | (let [{:keys [pos count text]} @state] 132 | [view :input-container 133 | [view {:style [:input-caret :main-caret]} 134 | "[" (inc pos) "/" count "]>"] 135 | ^{:key (str (hash (:js-cm-opts cm-opts)))} 136 | [code-mirror/code-mirror (reaction (:text @state)) 137 | (merge 138 | default-cm-opts 139 | {:style {:height "auto" 140 | :font-size 16 141 | :flex 1 142 | :padding "2px"} 143 | :on-eval submit} 144 | cm-opts)]])) 145 | 146 | (defn docs-view [docs] 147 | [view :docs 148 | (or docs [view :docs-empty "This is where docs show up"])]) 149 | 150 | (defn set-print! [log] 151 | (set! cljs.core/*print-newline* false) 152 | (set! cljs.core/*print-err-fn* 153 | (fn [& args] 154 | (if (= 1 (count args)) 155 | (log (first args)) 156 | (log args)))) 157 | (set! cljs.core/*print-fn* 158 | (fn [& args] 159 | (if (= 1 (count args)) 160 | (log (first args)) 161 | (log args))))) 162 | 163 | (def initial-state 164 | {:items [] 165 | :hist-pos 0 166 | :history ["{:a 2 {:b 3} 4}"]}) 167 | 168 | ;; TODO is there a macro or something that could do this cleaner? 169 | (defn make-handlers [state] 170 | {:add-input (partial swap! state handlers/add-input) 171 | :add-result (partial swap! state handlers/add-result) 172 | :go-up (partial swap! state handlers/go-up) 173 | :go-down (partial swap! state handlers/go-down) 174 | :clear-items (partial swap! state handlers/clear-items) 175 | :set-text (partial swap! state handlers/set-text) 176 | :add-log (partial swap! state handlers/add-log)}) 177 | 178 | (defn repl [& {:keys [execute 179 | complete-word 180 | get-docs 181 | state 182 | show-value-opts 183 | js-cm-opts 184 | on-cm-init]}] 185 | (let [state (or state (r/atom initial-state)) 186 | {:keys 187 | [add-input 188 | add-result 189 | go-up 190 | go-down 191 | clear-items 192 | set-text 193 | add-log]} (make-handlers state) 194 | 195 | items (subs/items state) 196 | complete-atom (r/atom nil) 197 | docs (reaction 198 | (let [{:keys [pos list] :as state} @complete-atom] 199 | (when state 200 | (let [sym (first (get list pos))] 201 | (when (symbol? sym) 202 | (get-docs sym)))))) 203 | submit (fn [text] 204 | (if (= ":cljs/clear" (.trim text)) 205 | (do 206 | (clear-items) 207 | (set-text "")) 208 | (when (< 0 (count (.trim text))) 209 | (set-text text) 210 | (add-input text) 211 | (execute text #(add-result (not %1) %2)))))] 212 | 213 | (set-print! add-log) 214 | 215 | (fn [& {:keys [execute 216 | complete-word 217 | get-docs 218 | state 219 | show-value-opts 220 | js-cm-opts 221 | on-cm-init]}] 222 | [view :container 223 | [repl-items @items (assoc show-value-opts 224 | :set-text set-text)] 225 | [repl-input 226 | (subs/current-text state) 227 | submit 228 | {:complete-word complete-word 229 | :on-up go-up 230 | :on-down go-down 231 | :complete-atom complete-atom 232 | :on-change set-text 233 | :js-cm-opts js-cm-opts 234 | :on-cm-init on-cm-init 235 | }] 236 | [completion-list 237 | @complete-atom 238 | ;; TODO this should also replace the text.... 239 | identity 240 | #_(swap! complete-atom assoc :pos % :active true)] 241 | [docs-view 242 | @docs]]))) 243 | -------------------------------------------------------------------------------- /src/reepl/example.cljs: -------------------------------------------------------------------------------- 1 | (ns reepl.example 2 | (:require [cljs.js :as jsc] 3 | ;; [cljs.analyzer :as ana] 4 | [reepl.core :as reepl] 5 | [reepl.helpers :as helpers] 6 | [reagent.core :as r] 7 | 8 | [reepl.replumb :as replumb] 9 | [reepl.show-function :as show-function] 10 | [reepl.show-devtools :as show-devtools] 11 | 12 | [parinfer-codemirror.editor :as parinfer] 13 | [parinfer.codemirror.mode.clojure.clojure-parinfer] 14 | [devtools.core :as devtools] 15 | 16 | ;; Libs I want to be available to the repl 17 | [cljs.tools.reader] 18 | [quil.core :as q :include-macros true]) 19 | (:import goog.net.XhrIo)) 20 | 21 | ;; Used to make the repl reload-tolerant 22 | (defonce repl-state 23 | (r/atom reepl/initial-state)) 24 | 25 | (def styles 26 | {:main {:justify-content :center 27 | :align-items :center 28 | :align-self :stretch 29 | :margin-top 100 30 | :margin-bottom 100 31 | :flex 1} 32 | :box {:width 700 33 | :border-radius 5 34 | :background-color "white" 35 | :flex 1} 36 | :bottom {:flex-direction :row 37 | :align-items :center 38 | :color "#ddd"} 39 | :label {:margin "10px 5px" 40 | :display :flex 41 | :flex-direction :row 42 | :align-items :center 43 | :font-size ".8em" 44 | :cursor :pointer 45 | } 46 | :checkbox {:margin-right 5} 47 | :link {:color "#aaa" 48 | :text-decoration :none 49 | :margin "0 20px"} 50 | }) 51 | 52 | (def view (partial helpers/view styles)) 53 | 54 | (defn maybe-fn-docs [fn] 55 | (let [doc (replumb/doc-from-sym fn)] 56 | (when (:forms doc) 57 | (with-out-str 58 | (replumb/print-doc doc))))) 59 | 60 | (def default-settings 61 | {:vim false 62 | :warning-as-error true 63 | :parinfer true}) 64 | 65 | (defn get-settings [] 66 | (let [val js/localStorage.reeplSettings] 67 | (if-not val 68 | default-settings 69 | (try 70 | (merge default-settings 71 | (js->clj (js/JSON.parse val) :keywordize-keys true)) 72 | (catch js/Error _ 73 | default-settings))))) 74 | 75 | (defn save-settings [settings] 76 | (let [str (js/JSON.stringify (clj->js settings))] 77 | (aset js/localStorage "reeplSettings" str))) 78 | 79 | (defonce 80 | settings (r/atom (get-settings))) 81 | 82 | (add-watch settings :settings #(save-settings %4)) 83 | 84 | (def pi-count (atom 0)) 85 | 86 | (def my-st (jsc/empty-state)) 87 | 88 | (defn jsc-run [source cb] 89 | (jsc/eval-str my-st 90 | source 91 | 'stuff 92 | {:eval jsc/js-eval 93 | :ns 'cljs.user 94 | :context :statement 95 | :def-emits-var true 96 | } 97 | (fn [result] 98 | (if (contains? result :error) 99 | (cb false (:error result)) 100 | (cb true (:value result)))))) 101 | 102 | (defn main-view [] 103 | [view :main 104 | [view :box 105 | [reepl/repl 106 | :execute #(replumb/run-repl %1 {:warning-as-error (:warning-as-error @settings)} %2) 107 | :complete-word replumb/process-apropos 108 | :get-docs replumb/process-doc 109 | :state repl-state 110 | ;; TODO change name 111 | :show-value-opts 112 | {:showers [show-devtools/show-devtools 113 | (partial show-function/show-fn-with-docs maybe-fn-docs)]} 114 | :js-cm-opts {:mode (if (:parinfer @settings) 115 | "clojure-parinfer" 116 | "clojure") 117 | :keyMap (if (:vim @settings) "vim" "default") 118 | :showCursorWhenSelecting true} 119 | :on-cm-init #(when (:parinfer @settings) 120 | (parinfer/parinferize! % (swap! pi-count inc) 121 | :indent-mode (.getValue %)))] 122 | ] 123 | [view :bottom 124 | [:label 125 | {:style (:label styles)} 126 | [:input { 127 | :type "checkbox" 128 | :checked (:vim @settings) 129 | :style (:checkbox styles) 130 | :on-change #(swap! settings update :vim not) 131 | }] 132 | "Vim"] 133 | [:label 134 | {:style (:label styles)} 135 | [:input {:checked (:parinfer @settings) 136 | :type "checkbox" 137 | :style (:checkbox styles) 138 | :on-change #(swap! settings update :parinfer not) 139 | }] 140 | "Parinfer"] 141 | [:label 142 | {:style (:label styles)} 143 | [:input {:checked (:warning-as-error @settings) 144 | :type "checkbox" 145 | :style (:checkbox styles) 146 | :on-change #(swap! settings update :warning-as-error not) 147 | }] 148 | "Warning as error"] 149 | [:a 150 | {:href "https://github.com/jaredly/reepl" 151 | :target :_blank 152 | :style (:link styles)} 153 | "Github"] 154 | ]]) 155 | 156 | (defn main [] 157 | (js/console.log "reload!") 158 | (r/render [main-view] (js/document.getElementById "container"))) 159 | 160 | (devtools/install!) 161 | 162 | (swap! jsc/*loaded* conj 163 | 'quil.core 164 | 'reepl.core 165 | 'reepl.show-value 166 | 'reepl.show-value 167 | 'clojure.string 168 | 'cljs.reader 169 | 'cljs.tools.reader) 170 | 171 | (replumb/run-repl "(require '[quil.core :as q])" identity) 172 | (replumb/run-repl "(require '[clojure.string :as str])" identity) 173 | (replumb/run-repl "(require '[reepl.core :as reepl])" identity) 174 | (replumb/run-repl "(require '[reepl.show-value])" identity) 175 | (replumb/run-repl "(require '[cljs.reader])" identity) 176 | (replumb/run-repl "(require '[cljs.tools.reader])" identity) 177 | 178 | (main) 179 | 180 | ;; :((( why doesn't parinfer support reloading?? 181 | (defonce -initing 182 | (parinfer/start-editor-sync!)) 183 | -------------------------------------------------------------------------------- /src/reepl/handlers.cljs: -------------------------------------------------------------------------------- 1 | (ns reepl.handlers 2 | (:require [reagent.core :as r] 3 | [reepl.helpers :as helpers]) 4 | (:require-macros 5 | [reagent.ratom :refer [reaction]])) 6 | 7 | (defn clear-items [db] 8 | (assoc db :items [])) 9 | 10 | (defn init [db data] 11 | (merge db data)) 12 | 13 | (defn add-item [db item] 14 | (update db :items conj item)) 15 | 16 | (defn add-items [db items] 17 | (update db :items concat items)) 18 | 19 | (defn add-input [db input] 20 | (let [inum (count (:history db))] 21 | (-> db 22 | (assoc :hist-pos 0) 23 | (update :history conj "") 24 | (update :items conj {:type :input :text input :num inum})))) 25 | 26 | (defn add-result [db error? value] 27 | (update db :items conj {:type (if error? :error :output) 28 | :value value})) 29 | 30 | (defn add-log [db val] 31 | (update db :items conj {:type :log :value val})) 32 | 33 | (defn set-text [db text] 34 | (let [history (:history db) 35 | pos (:hist-pos db) 36 | idx (- (count history) pos 1)] 37 | (-> db 38 | (assoc :hist-pos 0) 39 | (assoc :history 40 | (if (= pos 0) 41 | (assoc history idx text) 42 | (if (= "" (last history)) 43 | (assoc history (dec (count history)) text) 44 | (conj history text))))))) 45 | 46 | (defn go-up [db] 47 | (let [pos (:hist-pos db) 48 | len (count (:history db)) 49 | new-pos (if (>= pos (dec len)) 50 | pos 51 | (inc pos))] 52 | (assoc db :hist-pos new-pos))) 53 | 54 | (defn go-down [db] 55 | (let [pos (:hist-pos db) 56 | new-pos (if (<= pos 0) 57 | 0 58 | (dec pos)) 59 | ] 60 | (assoc db :hist-pos new-pos))) 61 | -------------------------------------------------------------------------------- /src/reepl/helpers.cljs: -------------------------------------------------------------------------------- 1 | (ns reepl.helpers 2 | (:require [reagent.core :as r])) 3 | 4 | (def text-style {:display "inline-block" 5 | :flex-shrink 0 6 | :box-sizing "border-box"}) 7 | 8 | (def view-style {:display "flex" 9 | :flex-direction "column" 10 | :min-height 0 11 | :flex-shrink 0 12 | :box-sizing "border-box"}) 13 | 14 | (def button-style {:display "inline-block" 15 | :flex-shrink 0 16 | :box-sizing "border-box" 17 | :cursor "pointer" 18 | :background-color "transparent" 19 | :border "1px solid" 20 | :border-radius 5}) 21 | 22 | (defn get-styles [styles style-prop] 23 | (cond 24 | (not style-prop) {} 25 | (keyword? style-prop) (styles style-prop) 26 | (sequential? style-prop) (reduce (fn [a b] (merge a (get-styles styles b))) {} style-prop) 27 | :default style-prop)) 28 | 29 | (defn parse-props [styles default-style props] 30 | (if (keyword? props) 31 | (parse-props styles default-style {:style props}) 32 | (merge {:style (merge default-style (get-styles styles (:style props)))} 33 | (dissoc props :style))) 34 | #_(if (keyword? props) 35 | {:style (merge default-style (props styles))} 36 | (let [style-prop (:style props) 37 | style (if (keyword? style-prop) 38 | (styles style-prop) 39 | style) 40 | style (merge default-style style) 41 | props (merge {:style style} (dissoc props :style))] 42 | props))) 43 | 44 | (defn better-el [dom-el default-style styles props & children] 45 | (let [[props children] 46 | (if (or (keyword? props) (map? props)) 47 | [props children] 48 | [nil (concat [props] children)] 49 | )] 50 | (vec (concat [dom-el (parse-props styles default-style props)] children)))) 51 | 52 | (def view (partial better-el :div view-style)) 53 | (def text (partial better-el :span text-style)) 54 | ; TODO have the button also stop-propagation 55 | (def button (partial better-el :button button-style)) 56 | 57 | (defn hoverable [config & children] 58 | (let [hovered (r/atom false)] 59 | (fn [{:keys [style hover-style el props] 60 | :or {:el :div}} & children] 61 | (into 62 | [el (assoc props 63 | :style (if @hovered 64 | (merge style hover-style) 65 | style) 66 | :on-mouse-over #(do (reset! hovered true) nil) 67 | :on-mouse-out #(do (reset! hovered false) nil) 68 | ) 69 | ] children)))) 70 | -------------------------------------------------------------------------------- /src/reepl/parinferize.cljs: -------------------------------------------------------------------------------- 1 | (ns reepl.parinferize 2 | (:require [clojure.string :refer [join]] 3 | [parinfer-codemirror.state :refer [state 4 | empty-editor-state]] 5 | [parinfer-codemirror.editor-support :refer [update-cursor! 6 | fix-text! 7 | cm-key 8 | IEditor 9 | get-prev-state 10 | frame-updated? 11 | set-frame-updated!]])) 12 | 13 | ;;---------------------------------------------------------------------- 14 | ;; Life Cycle events 15 | ;;---------------------------------------------------------------------- 16 | 17 | ;; NOTE: 18 | ;; Text is either updated after a change in text or 19 | ;; a cursor movement, but not both. 20 | ;; 21 | ;; When typing, on-change is called, then on-cursor-activity. 22 | ;; So we prevent updating the text twice by using an update flag. 23 | 24 | (def frame-updates (atom {})) 25 | 26 | (defn before-change 27 | "Called before any change is applied to the editor." 28 | [cm change] 29 | ;; keep CodeMirror from reacting to a change from "setValue" 30 | ;; if it is not a new value. 31 | (when (and (= "setValue" (.-origin change)) 32 | (= (.getValue cm) (join "\n" (.-text change)))) 33 | (.cancel change))) 34 | 35 | (defn on-change 36 | "Called after any change is applied to the editor." 37 | [cm change] 38 | (when (not= "setValue" (.-origin change)) 39 | (fix-text! cm :change change) 40 | (update-cursor! cm change) 41 | (set-frame-updated! cm true))) 42 | 43 | (defn on-cursor-activity 44 | "Called after the cursor moves in the editor." 45 | [cm] 46 | (when-not (frame-updated? cm) 47 | (fix-text! cm)) 48 | (set-frame-updated! cm false)) 49 | 50 | (defn on-tab 51 | "Indent selection or insert two spaces when tab is pressed. 52 | from: https://github.com/codemirror/CodeMirror/issues/988#issuecomment-14921785" 53 | [cm] 54 | (if (.somethingSelected cm) 55 | (.indentSelection cm) 56 | (let [n (.getOption cm "indentUnit") 57 | spaces (apply str (repeat n " "))] 58 | (.replaceSelection cm spaces)))) 59 | 60 | ;;---------------------------------------------------------------------- 61 | ;; Setup 62 | ;;---------------------------------------------------------------------- 63 | 64 | (def editor-opts 65 | {:mode "clojure-parinfer" 66 | :matchBrackets true 67 | :extraKeys {:Tab on-tab}}) 68 | 69 | (aset js/CodeMirror "keyMap" "default" "Shift-Tab" "indentLess") 70 | 71 | (defn parinferize! 72 | "Add parinfer goodness to a codemirror editor" 73 | ([cm key- parinfer-mode] 74 | (when-not (get @state key-) 75 | (let [initial-state (assoc empty-editor-state 76 | :mode parinfer-mode) 77 | prev-editor-state (atom nil)] 78 | 79 | ;; (set! (.-id wrapper) (str "cm-" element-id)) 80 | 81 | (when-not (get @state key-) 82 | (swap! frame-updates assoc key- {})) 83 | 84 | (swap! state update-in [key-] 85 | #(-> (or % initial-state) 86 | (assoc :cm cm))) 87 | 88 | ;; Extend the code mirror object with some utility methods. 89 | (specify! cm 90 | IEditor 91 | (get-prev-state [this] prev-editor-state) 92 | (cm-key [this] key-) 93 | (frame-updated? [this] (get-in @frame-updates [key- :frame-updated?])) 94 | (set-frame-updated! [this value] (swap! frame-updates assoc-in [key- :frame-updated?] value))) 95 | 96 | ;; handle code mirror events 97 | (.on cm "change" on-change) 98 | (.on cm "beforeChange" before-change) 99 | (.on cm "cursorActivity" on-cursor-activity) 100 | 101 | cm)))) 102 | 103 | ;;---------------------------------------------------------------------- 104 | ;; Setup 105 | ;;---------------------------------------------------------------------- 106 | 107 | (defn on-state-change 108 | "Called everytime the state changes to sync the code editor." 109 | [_ _ old-state new-state] 110 | (doseq [[k {:keys [cm text]}] new-state] 111 | (let [changed? (not= text (.getValue cm))] 112 | (when changed? 113 | (.setValue cm text))))) 114 | 115 | (defn force-editor-sync! [] 116 | (doseq [[k {:keys [cm text]}] @state] 117 | (.setValue cm text))) 118 | 119 | (defn start-editor-sync! [] 120 | ;; sync state changes to the editor 121 | (add-watch state :editor-updater on-state-change) 122 | (force-editor-sync!)) 123 | -------------------------------------------------------------------------------- /src/reepl/repl_items.cljs: -------------------------------------------------------------------------------- 1 | (ns reepl.repl-items 2 | (:require [re-frame.core :refer [dispatch 3 | dispatch-sync 4 | subscribe]] 5 | [clojure.string :as str] 6 | [cljs.reader] 7 | [cljs.tools.reader] 8 | 9 | [reagent.core :as r] 10 | [reepl.show-value :refer [show-value]] 11 | [reepl.code-mirror :as code-mirror] 12 | 13 | [reepl.helpers :as helpers] 14 | ) 15 | (:require-macros 16 | [reagent.ratom :refer [reaction]])) 17 | 18 | (def styles 19 | {:repl-items {:flex 1 20 | :overflow :auto 21 | :flex-basis 0 22 | :flex-shrink 1} 23 | :repl-item {:flex-direction :row 24 | :padding "3px 5px"} 25 | 26 | :intro-message {:padding "10px 20px" 27 | :line-height 1.5 28 | :border-bottom "1px solid #aaa" 29 | :flex-direction :row 30 | :margin-bottom 10} 31 | 32 | :input-item {} 33 | :output-item {} 34 | :error-item {:color :red 35 | :padding "5px 10px"} 36 | :underlying-error {:margin-left 10} 37 | :caret {:color "#aaf" 38 | :font-weight "bold" 39 | :margin-right 5 40 | :margin-left 5 41 | :font-size 11 42 | :padding-top 2 43 | :flex-direction :row 44 | } 45 | :input-caret {:color "#55f" 46 | :margin-right 10} 47 | :input-text {:flex 1 48 | :cursor :pointer 49 | :word-wrap :break-word} 50 | :output-caret {} 51 | :output-value {:flex 1 52 | :word-wrap :break-word} 53 | }) 54 | 55 | (def view (partial helpers/view styles)) 56 | (def text (partial helpers/text styles)) 57 | (def button (partial helpers/button styles)) 58 | 59 | (defmulti repl-item (fn [item opts] (:type item))) 60 | 61 | (defmethod repl-item :input 62 | [{:keys [num text]} opts] 63 | [view {:style [:repl-item :input-item]} 64 | [view {:style [:caret :input-caret]} "[" num "]>"] 65 | [view {:style :input-text 66 | :on-click (partial (:set-text opts) text)} 67 | [code-mirror/colored-text text]]]) 68 | 69 | (defmethod repl-item :log 70 | [{:keys [value]} opts] 71 | [view {:style [:repl-item :log-item]} 72 | [show-value value nil opts]]) 73 | 74 | (defmethod repl-item :error 75 | [{:keys [value]} opts] 76 | (let [message (.-message value) 77 | underlying (.-cause value)] 78 | [view {:style [:repl-item :output-item :error-item]} 79 | message 80 | (when underlying 81 | ;; TODO also show stack? 82 | [text :underlying-error (.-message underlying)]) 83 | ])) 84 | 85 | (defmethod repl-item :output 86 | [{:keys [value]} opts] 87 | [view {:style [:repl-item :output-item]} 88 | [view {:style [:caret :output-caret]} "<"] 89 | [view :output-value [show-value value nil opts]]]) 90 | 91 | (def intro-message 92 | [text :intro-message 93 | [text {:style {:font-weight :bold 94 | :font-size "1.2em"}} 95 | "Reepl: "] 96 | "the cljs Read-eval-print-loop that really understands you. 97 | Type " 98 | [text :intro-code ":cljs/clear"] 99 | " to clear the history"]) 100 | 101 | (defn repl-items [_] 102 | (r/create-class 103 | {:component-did-update 104 | (fn [this] 105 | (let [el (r/dom-node this)] 106 | (set! (.-scrollTop el) (.-scrollHeight el)))) 107 | :reagent-render 108 | (fn [items opts] 109 | (into 110 | [view :repl-items 111 | intro-message] 112 | (map #(repl-item % opts) items)))})) 113 | -------------------------------------------------------------------------------- /src/reepl/replumb.cljs: -------------------------------------------------------------------------------- 1 | (ns reepl.replumb 2 | (:require [cljs.js :as jsc] 3 | [cljs.analyzer :as ana] 4 | [reepl.core :as reepl] 5 | [reepl.helpers :as helpers] 6 | [devtools.core :as devtools] 7 | [cljs.pprint :as pprint] 8 | [reagent.core :as r] 9 | [quil.core :as q :include-macros true] 10 | [cljs.tools.reader] 11 | [clojure.string :as str] 12 | 13 | [replumb.core :as replumb] 14 | [replumb.repl] 15 | [replumb.ast :as ast] 16 | [replumb.doc-maps :as docs] 17 | [cljs.repl :as repl] 18 | [parinfer-codemirror.editor :as parinfer] 19 | [parinfer.codemirror.mode.clojure.clojure-parinfer] 20 | 21 | [cljs.tools.reader.reader-types :refer [string-push-back-reader]] 22 | [cljs.tools.reader] 23 | [cljs.tagged-literals :as tags] 24 | 25 | [quil.middleware :as m]) 26 | (:import goog.net.XhrIo)) 27 | 28 | (defn fetch-file! 29 | "Very simple implementation of XMLHttpRequests that given a file path 30 | calls src-cb with the string fetched of nil in case of error. 31 | See doc at https://developers.google.com/closure/library/docs/xhrio" 32 | [file-url src-cb] 33 | (try 34 | (.send XhrIo file-url 35 | (fn [e] 36 | (if (.isSuccess (.-target e)) 37 | (src-cb (.. e -target getResponseText)) 38 | (src-cb nil)))) 39 | (catch :default e 40 | (src-cb nil)))) 41 | 42 | (def replumb-opts 43 | (merge (replumb/browser-options 44 | ["/main.out" "/main.out"] 45 | ;; TODO figure out file loading 46 | #_(fn [& a] nil) 47 | fetch-file!) 48 | {:warning-as-error true 49 | ;; :verbose true 50 | :no-pr-str-on-value true})) 51 | 52 | (defn find-last-expr-pos [text] 53 | ;; parse #js {} correctly 54 | (binding [cljs.tools.reader/*data-readers* tags/*cljs-data-readers*] 55 | (let [rr (string-push-back-reader text) 56 | ;; get a unique js object as a sigil 57 | eof (js-obj) 58 | read #(cljs.tools.reader/read {:eof eof} rr) 59 | ] 60 | (loop [last-pos 0 second-pos 0 last-form nil second-form nil] 61 | (let [form (read) 62 | new-pos (.-s-pos (.-rdr rr))] 63 | (if (identical? eof form) 64 | second-pos;; second-form] 65 | (recur new-pos last-pos form last-form))))))) 66 | 67 | (defn make-last-expr-set-val [text js-name] 68 | (let [last-pos (find-last-expr-pos text)] 69 | (js/console.log last-pos text) 70 | (when-not (= last-pos 0) 71 | (str 72 | (.slice text 0 last-pos) 73 | "(aset js/window \"" js-name "\" " 74 | (.slice text last-pos) 75 | ")" 76 | )))) 77 | 78 | (defn jsc-run [source cb] 79 | (jsc/eval-str replumb.repl/st 80 | source 81 | 'stuff 82 | {:eval (fn a [& b] 83 | (js/console.log "eval source" b) 84 | (apply jsc/js-eval b)) 85 | :ns (replumb.repl/current-ns) 86 | :context :statement 87 | :def-emits-var true} 88 | (fn [result] 89 | (swap! replumb.repl/app-env assoc :current-ns (:ns result)) 90 | (if (contains? result :error) 91 | (cb false (:error result)) 92 | (cb true (aget js/window "last_repl_value")))))) 93 | 94 | (defn get-first-form [text] 95 | ;; parse #js {} correctly 96 | (binding [cljs.tools.reader/*data-readers* tags/*cljs-data-readers*] 97 | (let [rr (string-push-back-reader text) 98 | form (cljs.tools.reader/read rr) 99 | ;; TODO this is a bit dependent on tools.reader internals... 100 | s-pos (.-s-pos (.-rdr rr))] 101 | [form s-pos]))) 102 | 103 | (defn run-repl-multi [text opts cb] 104 | (let [text (.trim text) 105 | [form pos] (get-first-form text) 106 | source (.slice text 0 pos) 107 | remainder (.trim (.slice text pos)) 108 | has-more? (not (empty? remainder))] 109 | (js/console.log [text form pos source remainder has-more?]) 110 | (replumb/read-eval-call 111 | opts 112 | #(let [success? (replumb/success? %) 113 | result (replumb/unwrap-result %)] 114 | (js/console.log "evaled" [success? result has-more?]) 115 | (if-not success? 116 | (cb success? result) 117 | ;; TODO should I log the result if it's not the end? 118 | (if has-more? 119 | (run-repl-multi remainder opts cb) 120 | (cb success? result)) 121 | )) 122 | source))) 123 | 124 | ;; Trying to get expressions + statements to play well together 125 | ;; TODO is this a better way? The `do' stuff seems to work alright ... although 126 | ;; it won't work if there are other `ns' statements inside there... 127 | (defn run-repl-experimental* [text opts cb] 128 | (let [fixed (make-last-expr-set-val text "last_repl_value")] 129 | (if fixed 130 | (jsc-run fixed cb) 131 | (replumb/read-eval-call 132 | opts #(cb (replumb/success? %) (replumb/unwrap-result %)) text)))) 133 | 134 | (defn fix-ns-do [text] 135 | ;; parse #js {} correctly 136 | (binding [cljs.tools.reader/*data-readers* tags/*cljs-data-readers*] 137 | (let [rr (string-push-back-reader text) 138 | form (cljs.tools.reader/read rr) 139 | is-ns (and (sequential? form) 140 | (= 'ns (first form))) 141 | ;; TODO this is a bit dependent on tools.reader internals... 142 | s-pos (.-s-pos (.-rdr rr))] 143 | (js/console.log is-ns form s-pos) 144 | (if-not is-ns 145 | (str "(do " text ")") 146 | (str 147 | (.slice text 0 s-pos) 148 | "(do " 149 | (.slice text s-pos) 150 | ")" 151 | ))))) 152 | 153 | (defn run-repl* [text opts cb] 154 | (replumb/read-eval-call 155 | opts 156 | #(cb 157 | (replumb/success? %) 158 | (replumb/unwrap-result %)) 159 | (fix-ns-do text))) 160 | 161 | (defn run-repl 162 | ([text cb] (run-repl-multi text replumb-opts cb)) 163 | ([text opts cb] (run-repl-multi text (merge replumb-opts opts) cb))) 164 | 165 | (defn compare-completion 166 | "The comparison algo for completions 167 | 168 | 1. if one is exactly the text, then it goes first 169 | 2. if one *starts* with the text, then it goes first 170 | 3. otherwise leave in current order" 171 | [text a b] 172 | (cond 173 | (and (= text a) 174 | (= text b)) 0 175 | (= text a) -1 176 | (= text b) 1 177 | :else 178 | (let [a-starts (= 0 (.indexOf a text)) 179 | b-starts (= 0 (.indexOf b text))] 180 | (cond 181 | (and a-starts b-starts) 0 182 | a-starts -1 183 | b-starts 1 184 | :default 0)))) 185 | 186 | (defn compare-ns 187 | "Sorting algo for namespaces 188 | 189 | The current ns comes first, then cljs.core, then anything else 190 | alphabetically" 191 | [current ns1 ns2] 192 | (cond 193 | (= ns1 current) -1 194 | (= ns2 current) 1 195 | (= ns1 'cljs.core) -1 196 | (= ns2 'cljs.core) 1 197 | :default (compare ns1 ns2))) 198 | 199 | (defn get-from-js-ns 200 | "Use js introspection to get a list of interns in a namespaces 201 | 202 | This is pretty dependent on cljs runtime internals, so it may break in the 203 | future (although I think it's fairly unlikely). It takes advantage of the fact 204 | that the ns `something.other.thing' is available as an object on 205 | `window.something.other.thing', and Object.keys gets all the variables in that 206 | namespace." 207 | [ns] 208 | 209 | (let [parts (map munge (.split (str ns) ".")) 210 | ns (reduce aget js/window parts)] 211 | (if-not ns 212 | [] 213 | (map demunge (js/Object.keys ns))))) 214 | 215 | (defn dedup-requires 216 | "Takes a map of {require-name ns-name} and dedups multiple keys that have the 217 | same ns-name value." 218 | [requires] 219 | (first 220 | (reduce (fn [[result seen] [k v]] 221 | (if (seen v) 222 | [result seen] 223 | [(assoc result k v) (conj seen v)])) [{} #{}] requires))) 224 | 225 | (defn get-matching-ns-interns [[name ns] matches? only-ns] 226 | (let [ns-name (str ns) 227 | publics (keys (ast/ns-publics @replumb.repl/st ns)) 228 | publics (if (empty? publics) 229 | (get-from-js-ns ns) 230 | publics)] 231 | (if-not (or (nil? only-ns) 232 | (= only-ns ns-name)) 233 | [] 234 | (sort (map #(symbol name (str %)) 235 | (filter matches? 236 | publics)))))) 237 | 238 | (defn js-attrs [obj] 239 | (if-not obj 240 | [] 241 | (let [constructor (.-constructor obj) 242 | proto (js/Object.getPrototypeOf obj)] 243 | (concat (js/Object.keys obj) 244 | (when-not (= proto obj) 245 | (js-attrs proto)))))) 246 | 247 | (defn js-completion 248 | [text] 249 | (let [parts (vec (.split text ".")) 250 | completing (or (last parts) "") 251 | prefix #(str "js/" (str/join "." (conj (vec (butlast parts)) %))) 252 | possibles (js-attrs (reduce aget js/window (butlast parts)))] 253 | (->> possibles 254 | (filter #(not (= -1 (.indexOf % completing)))) 255 | (sort (partial compare-completion text)) 256 | (map #(-> [nil (prefix %) (prefix %)]))))) 257 | 258 | ;; TODO fuzzy-match if there are no normal matches 259 | (defn cljs-completion 260 | "Tab completion. Copied w/ extensive modifications from replumb.repl/process-apropos." 261 | [text] 262 | (let [[only-ns text] (if-not (= -1 (.indexOf text "/")) 263 | (.split text "/") 264 | [nil text]) 265 | matches? #(and 266 | ;; TODO find out what these t_cljs$core things are... seem to be nil 267 | (= -1 (.indexOf (str %) "t_cljs$core")) 268 | (< -1 (.indexOf (str %) text))) 269 | current-ns (replumb.repl/current-ns) 270 | replace-name (fn [sym] 271 | (if (or 272 | (= (namespace sym) "cljs.core") 273 | (= (namespace sym) (str current-ns))) 274 | (name sym) 275 | (str sym))) 276 | requires (:requires 277 | (ast/namespace @replumb.repl/st current-ns)) 278 | only-ns (when only-ns 279 | (or (str (get requires (symbol only-ns))) 280 | only-ns)) 281 | requires (concat 282 | [[nil current-ns] 283 | [nil 'cljs.core]] 284 | (dedup-requires (vec requires))) 285 | names (set (apply concat requires)) 286 | defs (->> requires 287 | (sort-by second (partial compare-ns current-ns)) 288 | (mapcat #(get-matching-ns-interns % matches? only-ns)) 289 | ;; [qualified symbol, show text, replace text] 290 | (map #(-> [% (str %) (replace-name %) (name %)])) 291 | (sort-by #(get % 3) (partial compare-completion text)))] 292 | (vec (concat 293 | ;; TODO make this configurable 294 | (take 75 defs) 295 | (map 296 | #(-> [% (str %) (str %)]) 297 | (filter matches? names)))))) 298 | 299 | (defn process-apropos [text] 300 | (if (= 0 (.indexOf text "js/")) 301 | (js-completion (.slice text 3)) 302 | (cljs-completion text) 303 | )) 304 | 305 | (defn get-forms [m] 306 | (cond 307 | (:forms m) (:forms m) 308 | (:arglists m) (let [arglists (:arglists m)] 309 | (if (or (:macro m) 310 | (:repl-special-function m)) 311 | arglists 312 | (if (= 'quote (first arglists)) 313 | (second arglists) 314 | arglists))))) 315 | 316 | ;; Copied & modified from cljs.repl/print-doc 317 | (defn get-doc [m] 318 | (merge {:name (str (when-let [ns (:ns m)] (str ns "/")) (:name m)) 319 | :type (cond 320 | (:protocol m) :protocol 321 | (:special-form m) :special-form 322 | (:macro m) :macro 323 | (:repl-special-function m) :repl-special-function 324 | :else :normal) 325 | :forms (get-forms m) 326 | :doc (:doc m)} 327 | (if (:special-form m) 328 | {:please-see (if (contains? m :url) 329 | (when (:url m) 330 | (str "http://clojure.org/" (:url m))) 331 | (str "http://clojure.org/special_forms#" (:name m)))} 332 | (when (:protocol m) 333 | {:protocol-methods (:methods m)})))) 334 | 335 | (defn doc-from-sym [sym] 336 | (cond 337 | (docs/special-doc-map sym) (get-doc (docs/special-doc sym)) 338 | (docs/repl-special-doc-map sym) (get-doc (docs/repl-special-doc sym)) 339 | (ast/namespace 340 | @replumb.repl/st sym) (get-doc 341 | (select-keys 342 | (ast/namespace @replumb.repl/st sym) 343 | [:name :doc])) 344 | :else (get-doc 345 | (replumb.repl/get-var 346 | nil 347 | (replumb.repl/empty-analyzer-env) sym)))) 348 | 349 | (def type-name 350 | {:protocol "Protocol" 351 | :special-form "Special Form" 352 | :macro "Macro" 353 | :repl-special-function "REPL Special Function"}) 354 | 355 | ;; Copied & modified from cljs.repl/print-doc 356 | (defn print-doc [doc] 357 | (println (:name doc)) 358 | (if-not (= :normal (:type doc)) 359 | (println (type-name (:type doc)))) 360 | (when (:forms doc) 361 | (prn (:forms doc))) 362 | (when (:please-see doc) 363 | (println (str "\n Please see " (:please-see doc)))) 364 | (when (:doc doc) 365 | (println (:doc doc))) 366 | (when (:methods doc) 367 | (doseq [[name {:keys [doc arglists]}] (:methods doc)] 368 | (println) 369 | (println " " name) 370 | (println " " arglists) 371 | (when doc 372 | (println " " doc))))) 373 | 374 | (defn process-doc 375 | "Get the documentation for a symbol. Copied & modified from replumb." 376 | [sym] 377 | (when sym 378 | (with-out-str 379 | (print-doc (doc-from-sym sym))))) 380 | -------------------------------------------------------------------------------- /src/reepl/show_devtools.cljs: -------------------------------------------------------------------------------- 1 | (ns reepl.show-devtools 2 | (:require [clojure.string :as str] 3 | [reagent.core :as r] 4 | 5 | [devtools.format :as devtools] 6 | [cljs.pprint :as pprint] 7 | [reepl.helpers :as helpers]) 8 | (:require-macros 9 | [reagent.ratom :refer [reaction]])) 10 | 11 | (def styles 12 | {:value-head {:flex-direction :row} 13 | :value-toggle {:font-size 9 14 | :padding 4 15 | :cursor :pointer}}) 16 | 17 | (def view (partial helpers/view styles)) 18 | (def text (partial helpers/text styles)) 19 | (def button (partial helpers/button styles)) 20 | 21 | (defn str? [val] 22 | (= js/String (type val))) 23 | 24 | (defn pprint-str [val] 25 | (pprint/write val :stream nil)) 26 | 27 | (defn js-array? [val] 28 | (= js/Array (type val))) 29 | 30 | (defn parse-style [raw] 31 | (into {} 32 | (map (fn [line] 33 | (let [[k v] (str/split line ":")] 34 | [(keyword k) v]))(str/split raw ";")))) 35 | 36 | (defn show-el [val show-value] 37 | (let [type (first val) 38 | opts (second val) 39 | children (drop 2 val)] 40 | (if (= "object" type) 41 | [show-value (.-object opts) (.-config opts)] 42 | (into 43 | [(keyword type) {:style (when opts (parse-style (.-style opts)))}] 44 | (map #(if-not (js-array? %) % (show-el % show-value)) children)) 45 | ))) 46 | 47 | (defn openable [header val config show-value] 48 | (let [open (r/atom false)] 49 | (fn [_ _] 50 | (let [is-open @open] 51 | [view :value-with-body 52 | [view :value-head 53 | [view {:on-click #(swap! open not) 54 | :style :value-toggle} 55 | (if is-open "▼" "▶")] 56 | (show-el header show-value)] 57 | (when is-open 58 | (show-el (devtools/body-api-call val config) show-value)) 59 | ])))) 60 | 61 | ;; see https://docs.google.com/document/d/1FTascZXT9cxfetuPRT2eXPQKXui4nWFivUnS_335T3U/preview 62 | (defn show-devtools [val config show-value] 63 | (if (var? val) 64 | nil 65 | (let [header (try 66 | (devtools/header-api-call val config) 67 | (catch js/Error e 68 | e))] 69 | (cond 70 | (not header) 71 | nil 72 | (instance? js/Error header) 73 | [view :inline-value "Error expanding lazy value"] 74 | :else 75 | (if-not (devtools/has-body-api-call val config) 76 | [view :inline-value (show-el header show-value)] 77 | [openable header val config show-value] 78 | ))))) 79 | -------------------------------------------------------------------------------- /src/reepl/show_function.cljs: -------------------------------------------------------------------------------- 1 | (ns reepl.show-function 2 | (:require [clojure.string :as str] 3 | [reagent.core :as r] 4 | 5 | [devtools.format :as devtools] 6 | [cljs.pprint :as pprint] 7 | [reepl.helpers :as helpers])) 8 | 9 | (def styles 10 | {:function {:color "#00a"} 11 | }) 12 | 13 | (def view (partial helpers/view styles)) 14 | (def text (partial helpers/text styles)) 15 | (def button (partial helpers/button styles)) 16 | 17 | (def cljs-fn-prefix 18 | "cljs$core$IFn$_invoke$arity$") 19 | 20 | (defn recover-cljs-name [parts] 21 | (-> (str/join \. (butlast parts)) 22 | (str \/ (last parts)) 23 | demunge)) 24 | 25 | (defn get-cljs-arities [fn] 26 | (map 27 | #(aget fn %) 28 | (filter #(.startsWith % cljs-fn-prefix) (js->clj (js/Object.keys fn))))) 29 | 30 | (defn get-fn-summary [fn] 31 | (let [source (str fn) 32 | args (second (re-find #"\(([^\)]+)\)" source))] 33 | (map demunge 34 | (str/split args \,)))) 35 | 36 | (defn get-function-forms [fn] 37 | (let [arities (get-cljs-arities fn) 38 | arities (if (empty? arities) 39 | [fn] 40 | arities)] 41 | (map get-fn-summary 42 | arities))) 43 | 44 | (defn get-fn-name [fn] 45 | (let [parts (.split (.-name fn) \$)] 46 | (cond 47 | (empty? (.-name fn)) "*anonymous*" 48 | (= 1 (count parts)) (.-name fn) 49 | :else (recover-cljs-name parts)))) 50 | 51 | (defn str-fn-forms [forms] 52 | (str 53 | \[ (str/join "] [" (map (partial str/join " ") forms)) \])) 54 | 55 | (defn show-fn-with-docs [get-doc fn _ _] 56 | (when (= js/Function (type fn)) 57 | (let [docs (get-doc (symbol (get-fn-name fn))) 58 | is-native-fn (.match (str fn) #"\{ \[native code\] \}$" )] 59 | (if docs 60 | [view 61 | :function 62 | [text :function-docs 63 | docs]] 64 | [view 65 | :function 66 | [text :function-head "fn " (get-fn-name fn)] 67 | [text :function-arities (str-fn-forms (get-function-forms fn))] 68 | [text :function-body 69 | (when is-native-fn "[native code]")]])))) 70 | 71 | (defn show-fn [f config show-value] 72 | (show-fn-with-docs (fn [_] nil) f config show-value)) 73 | -------------------------------------------------------------------------------- /src/reepl/show_value.cljs: -------------------------------------------------------------------------------- 1 | (ns reepl.show-value 2 | (:require [clojure.string :as str] 3 | [reagent.core :as r] 4 | 5 | [devtools.format :as devtools] 6 | [cljs.pprint :as pprint] 7 | [reepl.helpers :as helpers]) 8 | (:require-macros 9 | [reagent.ratom :refer [reaction]])) 10 | 11 | (def styles 12 | {:value-head {:flex-direction :row} 13 | :inline-value {:display :inline-flex} 14 | :value-toggle {:font-size 9 15 | :padding 4 16 | :cursor :pointer} 17 | :function {:color "#00a"} 18 | }) 19 | 20 | (def view (partial helpers/view styles)) 21 | 22 | (defn str? [val] 23 | (= js/String (type val))) 24 | 25 | (defn pprint-str [val] 26 | (pprint/write val :stream nil)) 27 | 28 | (defn show-str [val] 29 | (if (str? val) 30 | val 31 | (pprint-str val))) 32 | 33 | (defn show-value- [val config showers] 34 | (loop [shower-list showers] 35 | (if (empty? shower-list) 36 | (throw (js/Error. (str "No shower for value " val))) 37 | (let [res ((first shower-list) val config #(show-value- %1 %2 showers))] 38 | (if res 39 | [view :inline-value res] 40 | (recur (rest shower-list))))))) 41 | 42 | (defn show-value [val opts show-opts] 43 | (show-value- val opts (conj (vec (:showers show-opts)) show-str))) 44 | -------------------------------------------------------------------------------- /src/reepl/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns reepl.subs 2 | (:require [reagent.core :as r]) 3 | (:require-macros 4 | [reagent.ratom :refer [reaction]])) 5 | 6 | (defn items [db] 7 | (reaction (:items @db))) 8 | 9 | (defn current-text [db] 10 | (let [idx (reaction (:hist-pos @db)) 11 | history (reaction (:history @db))] 12 | (reaction (let [items @history 13 | pos (- (count items) @idx 1)] 14 | {:pos pos 15 | :count (count items) 16 | :text (get items pos)})))) 17 | -------------------------------------------------------------------------------- /src/reepl/timers.cljs: -------------------------------------------------------------------------------- 1 | (ns reepl.timers) 2 | 3 | (defn num-timers [state] 4 | (count (::timers state))) 5 | 6 | (defn new-timer [time handler] 7 | {:start (js/Date.now) 8 | :time time 9 | :handler handler}) 10 | 11 | (defn add-timer [state time handler] 12 | (update-in state [::timers] 13 | (fnil conj []) 14 | (new-timer time handler))) 15 | 16 | (defn add-ival [state name time handler] 17 | (update-in state [::ivals] 18 | assoc name 19 | (new-timer time handler))) 20 | 21 | (defn remove-ival [state name] 22 | (update-in state [::ivals] 23 | dissoc name)) 24 | 25 | (defn timer-ready [timer] 26 | (>= (- (js/Date.now) (:start timer)) (:time timer))) 27 | 28 | (defn check-timer [state timer] 29 | (if-not (timer-ready timer) 30 | (update-in state [::timers] conj timer) 31 | (or ((:handler timer) state) state))) 32 | 33 | (defn check-ival [state [name timer]] 34 | (if-not (timer-ready timer) 35 | state 36 | (or ((:handler timer) state) state))) 37 | 38 | (defn update-timers [state] 39 | (if (empty? (::timers state)) 40 | state ; TODO it'd be nice not to need to remove the ivals first... 41 | (reduce check-timer (assoc state ::timers []) (::timers state)))) 42 | 43 | (defn update-ivals [state] 44 | (if (empty? (::ivals state)) 45 | state 46 | (reduce check-ival state (::ivals state)))) 47 | 48 | (defn update-fn [orig state] 49 | (-> state 50 | orig 51 | update-timers 52 | update-ivals)) 53 | 54 | (defn middleware [options] 55 | (assoc options :update #(update-fn (:update options) %))) 56 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |