├── .gitignore ├── LICENSE ├── README.md ├── boot.properties ├── build.boot ├── demo └── re_console │ ├── example.cljs │ ├── example │ ├── handlers.cljs │ └── subs.cljs │ ├── io.cljs │ └── replumb_proxy.cljs ├── html ├── css │ ├── main.css │ └── re-console.scss ├── index.html └── js │ └── clojure-parinfer.js ├── screenshot.gif ├── src └── re_console │ ├── app.cljs │ ├── common.cljs │ ├── core.cljs │ ├── editor.cljs │ ├── handlers.cljs │ ├── parinfer.cljs │ ├── subs.cljs │ └── utils.cljs ├── test └── re_console │ ├── app_test.cljs │ └── example_test.cljs └── version.properties /.gitignore: -------------------------------------------------------------------------------- 1 | ### BOOT ####################################################################### 2 | 3 | /.boot/ 4 | 5 | ### LEININGEN ################################################################## 6 | 7 | .lein-* 8 | 9 | ### MAVEN (include pom.xml in /base) ########################################### 10 | 11 | *.jar 12 | *.war 13 | pom.xml 14 | pom.xml.asc 15 | 16 | ### NREPL ###################################################################### 17 | 18 | .repl-* 19 | .nrepl-* 20 | 21 | ### JAVA ####################################################################### 22 | 23 | /hs_err_pid*.log 24 | 25 | ### OSX ######################################################################## 26 | 27 | .DS_Store 28 | 29 | ### EMACS ###################################################################### 30 | 31 | [#]*[#] 32 | 33 | ### VIM ######################################################################## 34 | 35 | *.swn 36 | *.swo 37 | *.swp 38 | 39 | ### PROJECT #################################################################### 40 | 41 | /target/ 42 | /out -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 4 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 5 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial code and documentation 12 | distributed under this Agreement, and 13 | b) in the case of each subsequent Contributor: 14 | i) changes to the Program, and 15 | ii) additions to the Program; 16 | 17 | where such changes and/or additions to the Program originate from and are 18 | distributed by that particular Contributor. A Contribution 'originates' 19 | from a Contributor if it was added to the Program by such Contributor 20 | itself or anyone acting on such Contributor's behalf. Contributions do not 21 | include additions to the Program which: (i) are separate modules of 22 | software distributed in conjunction with the Program under their own 23 | license agreement, and (ii) are not derivative works of the Program. 24 | 25 | "Contributor" means any person or entity that distributes the Program. 26 | 27 | "Licensed Patents" mean patent claims licensable by a Contributor which are 28 | necessarily infringed by the use or sale of its Contribution alone or when 29 | combined with the Program. 30 | 31 | "Program" means the Contributions distributed in accordance with this 32 | Agreement. 33 | 34 | "Recipient" means anyone who receives the Program under this Agreement, 35 | including all Contributors. 36 | 37 | 2. GRANT OF RIGHTS 38 | a) Subject to the terms of this Agreement, each Contributor hereby grants 39 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 40 | reproduce, prepare derivative works of, publicly display, publicly 41 | perform, distribute and sublicense the Contribution of such Contributor, 42 | if any, and such derivative works, in source code and object code form. 43 | b) Subject to the terms of this Agreement, each Contributor hereby grants 44 | Recipient a non-exclusive, worldwide, royalty-free patent license under 45 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 46 | transfer the Contribution of such Contributor, if any, in source code and 47 | object code form. This patent license shall apply to the combination of 48 | the Contribution and the Program if, at the time the Contribution is 49 | added by the Contributor, such addition of the Contribution causes such 50 | combination to be covered by the Licensed Patents. The patent license 51 | shall not apply to any other combinations which include the Contribution. 52 | No hardware per se is licensed hereunder. 53 | c) Recipient understands that although each Contributor grants the licenses 54 | to its Contributions set forth herein, no assurances are provided by any 55 | Contributor that the Program does not infringe the patent or other 56 | intellectual property rights of any other entity. Each Contributor 57 | disclaims any liability to Recipient for claims brought by any other 58 | entity based on infringement of intellectual property rights or 59 | otherwise. As a condition to exercising the rights and licenses granted 60 | hereunder, each Recipient hereby assumes sole responsibility to secure 61 | any other intellectual property rights needed, if any. For example, if a 62 | third party patent license is required to allow Recipient to distribute 63 | the Program, it is Recipient's responsibility to acquire that license 64 | before distributing the Program. 65 | d) Each Contributor represents that to its knowledge it has sufficient 66 | copyright rights in its Contribution, if any, to grant the copyright 67 | license set forth in this Agreement. 68 | 69 | 3. REQUIREMENTS 70 | 71 | A Contributor may choose to distribute the Program in object code form under 72 | its own license agreement, provided that: 73 | 74 | a) it complies with the terms and conditions of this Agreement; and 75 | b) its license agreement: 76 | i) effectively disclaims on behalf of all Contributors all warranties 77 | and conditions, express and implied, including warranties or 78 | conditions of title and non-infringement, and implied warranties or 79 | conditions of merchantability and fitness for a particular purpose; 80 | ii) effectively excludes on behalf of all Contributors all liability for 81 | damages, including direct, indirect, special, incidental and 82 | consequential damages, such as lost profits; 83 | iii) states that any provisions which differ from this Agreement are 84 | offered by that Contributor alone and not by any other party; and 85 | iv) states that source code for the Program is available from such 86 | Contributor, and informs licensees how to obtain it in a reasonable 87 | manner on or through a medium customarily used for software exchange. 88 | 89 | When the Program is made available in source code form: 90 | 91 | a) it must be made available under this Agreement; and 92 | b) a copy of this Agreement must be included with each copy of the Program. 93 | Contributors may not remove or alter any copyright notices contained 94 | within the Program. 95 | 96 | Each Contributor must identify itself as the originator of its Contribution, 97 | if 98 | any, in a manner that reasonably allows subsequent Recipients to identify the 99 | originator of the Contribution. 100 | 101 | 4. COMMERCIAL DISTRIBUTION 102 | 103 | Commercial distributors of software may accept certain responsibilities with 104 | respect to end users, business partners and the like. While this license is 105 | intended to facilitate the commercial use of the Program, the Contributor who 106 | includes the Program in a commercial product offering should do so in a manner 107 | which does not create potential liability for other Contributors. Therefore, 108 | if a Contributor includes the Program in a commercial product offering, such 109 | Contributor ("Commercial Contributor") hereby agrees to defend and indemnify 110 | every other Contributor ("Indemnified Contributor") against any losses, 111 | damages and costs (collectively "Losses") arising from claims, lawsuits and 112 | other legal actions brought by a third party against the Indemnified 113 | Contributor to the extent caused by the acts or omissions of such Commercial 114 | Contributor in connection with its distribution of the Program in a commercial 115 | product offering. The obligations in this section do not apply to any claims 116 | or Losses relating to any actual or alleged intellectual property 117 | infringement. In order to qualify, an Indemnified Contributor must: 118 | a) promptly notify the Commercial Contributor in writing of such claim, and 119 | b) allow the Commercial Contributor to control, and cooperate with the 120 | Commercial Contributor in, the defense and any related settlement 121 | negotiations. The Indemnified Contributor may participate in any such claim at 122 | its own expense. 123 | 124 | For example, a Contributor might include the Program in a commercial product 125 | offering, Product X. That Contributor is then a Commercial Contributor. If 126 | that Commercial Contributor then makes performance claims, or offers 127 | warranties related to Product X, those performance claims and warranties are 128 | such Commercial Contributor's responsibility alone. Under this section, the 129 | Commercial Contributor would have to defend claims against the other 130 | Contributors related to those performance claims and warranties, and if a 131 | court requires any other Contributor to pay any damages as a result, the 132 | Commercial Contributor must pay those damages. 133 | 134 | 5. NO WARRANTY 135 | 136 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN 137 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 138 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each 140 | Recipient is solely responsible for determining the appropriateness of using 141 | and distributing the Program and assumes all risks associated with its 142 | exercise of rights under this Agreement , including but not limited to the 143 | risks and costs of program errors, compliance with applicable laws, damage to 144 | or loss of data, programs or equipment, and unavailability or interruption of 145 | operations. 146 | 147 | 6. DISCLAIMER OF LIABILITY 148 | 149 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 150 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 151 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 152 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 153 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 154 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 155 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 156 | OF SUCH DAMAGES. 157 | 158 | 7. GENERAL 159 | 160 | If any provision of this Agreement is invalid or unenforceable under 161 | applicable law, it shall not affect the validity or enforceability of the 162 | remainder of the terms of this Agreement, and without further action by the 163 | parties hereto, such provision shall be reformed to the minimum extent 164 | necessary to make such provision valid and enforceable. 165 | 166 | If Recipient institutes patent litigation against any entity (including a 167 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 168 | (excluding combinations of the Program with other software or hardware) 169 | infringes such Recipient's patent(s), then such Recipient's rights granted 170 | under Section 2(b) shall terminate as of the date such litigation is filed. 171 | 172 | All Recipient's rights under this Agreement shall terminate if it fails to 173 | comply with any of the material terms or conditions of this Agreement and does 174 | not cure such failure in a reasonable period of time after becoming aware of 175 | such noncompliance. If all Recipient's rights under this Agreement terminate, 176 | Recipient agrees to cease use and distribution of the Program as soon as 177 | reasonably practicable. However, Recipient's obligations under this Agreement 178 | and any licenses granted by Recipient relating to the Program shall continue 179 | and survive. 180 | 181 | Everyone is permitted to copy and distribute copies of this Agreement, but in 182 | order to avoid inconsistency the Agreement is copyrighted and may only be 183 | modified in the following manner. The Agreement Steward reserves the right to 184 | publish new versions (including revisions) of this Agreement from time to 185 | time. No one other than the Agreement Steward has the right to modify this 186 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 187 | Eclipse Foundation may assign the responsibility to serve as the Agreement 188 | Steward to a suitable separate entity. Each new version of the Agreement will 189 | be given a distinguishing version number. The Program (including 190 | Contributions) may always be distributed subject to the version of the 191 | Agreement under which it was received. In addition, after a new version of the 192 | Agreement is published, Contributor may elect to distribute the Program 193 | (including its Contributions) under the new version. Except as expressly 194 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 195 | licenses to the intellectual property of any Contributor under this Agreement, 196 | whether expressly, by implication, estoppel or otherwise. All rights in the 197 | Program not expressly granted under this Agreement are reserved. 198 | 199 | This Agreement is governed by the laws of the State of New York and the 200 | intellectual property laws of the United States of America. No party to this 201 | Agreement will bring a legal action under this Agreement more than one year 202 | after the cause of action arose. Each party waives its rights to a jury trial in 203 | any resulting litigation. 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # re-console 2 | 3 | [![Clojars Project](https://img.shields.io/clojars/v/re-console.svg)](https://clojars.org/re-console) 4 | 5 | ## Overview 6 | re-console is an interactive terminal-like REPL. It is implemented using a 7 | [CodeMirror](https://codemirror.net/) 8 | component for user input and 9 | [re-frame](https://github.com/Day8/re-frame) for state management. It 10 | provides a user-friendly interface which allows to easily embed it in a web page 11 | and integrate it with an evaluation library (e.g. 12 | [replumb](https://github.com/Lambda-X/replumb)). 13 | 14 | ## Demo 15 | 16 | The sources are located in `src/re_console`, 17 | while the interactive demo is located in `demo/re_console`. 18 | It uses `replumb` as evaluation library. 19 | 20 | To start the demo run `boot dev` and browse to 21 | [localhost:3000](http://localhost:3000). 22 | 23 | ![screenshot](screenshot.gif) 24 | 25 | ## Usage 26 | 27 | Using re-console is as simple as rendering the `console` component, which 28 | accepts two parameters: a unique key and a map of options. 29 | 30 | ```clojure 31 | (reagent/render [console/console :key 32 | {:eval-opts (replumb-proxy/eval-opts false ["/js/compiled/out"]) 33 | :mode-line? true}] 34 | (.getElementById js/document "app")) 35 | ``` 36 | 37 | The unique key is used to identify the console and the map of options 38 | should contain the following keys: 39 | 40 | * `:mode`: the input mode, one of `#{:indent-mode, :paren-mode, :none}`, defaults 41 | to `:none`. 42 | * `:mode-line?`: if `true`, an Emacs-like modeline will be displayed under the 43 | console. 44 | * `:eval-opts`: a map of evaluation options, which in turn contains: 45 | 46 | * `:get-prompt`: a zero-arity function, returns a string (the prompt 47 | displayed after each evaluation). 48 | * `:should-eval`: a predicate function that takes the source as input (the 49 | expression to evaluate) and returns true or false depending on whether 50 | it can be evaluated. If `false`, the cursor will be placed on a new line. 51 | * `to-str-fn`: a one-arity function that converts the result to a string. 52 | If the result is already a string simply use `identity`. 53 | * `evaluate`: the main evaluation function, takes as arguments a callback 54 | function (called after evaluation) and the source to evaluate. 55 | Will be called if `should-eval` returns `true`. 56 | 57 | The callback function will be called with a map as parameter containing the 58 | following keys: 59 | 60 | * `success?`: if `true` the evaluation has succeded 61 | * `result`: the result of the evaluation (as it is), can be a valid result or an 62 | error 63 | * `prev-ns`: a string indicating the namespace the evaluation took place in 64 | * `source`: the original source 65 | 66 | The whole state is kept under the `:consoles` keyword in the global state and 67 | you can nicely interact with it via re-frame 68 | [handlers](https://github.com/Lambda-X/re-console/blob/0.1.3/src/re_console/handlers.cljs) 69 | (for example for changing the evaluation options or clearing the history). 70 | 71 | ## Tests 72 | 73 | Run `boot test` or `boot auto-test`. 74 | 75 | ## CSS styling 76 | 77 | The re-console component needs a custom CSS file in order to be displayed 78 | correctly. Be sure to add it to your application. A template is found in 79 | [the demo](https://github.com/Lambda-X/re-console/blob/0.1.3/html/css/re-console.scss) 80 | It's a `scss` file but it's easily convertible to CSS manually or via 81 | [boot](https://github.com/Lambda-X/re-console/blob/0.1.3/build.boot#L80). 82 | 83 | ## Parinfer 84 | 85 | In order to take advantage of [parinfer](https://shaunlebron.github.io/parinfer/) 86 | you need to do two things: 87 | 88 | * include the appropriate JavaScript file in the the compilation set. To do this 89 | add the [:foreign-libs](https://github.com/Lambda-X/re-console/blob/0.1.3/build.boot#L69) 90 | value. 91 | 92 | * Switch to parinfer by invokating the `:set-console-mode` handler, passing as 93 | value `:indent-mode`. To disable parinfer, call the same handler with value `:none`. 94 | 95 | # Community 96 | 97 | Many thanks to jaredly's 98 | [reepl](https://github.com/jaredly/reepl), amasad's 99 | [jq-console](https://github.com/replit/jq-console) and hiram-madelaine's 100 | [codemirror-parinfer](https://github.com/hiram-madelaine/codemirror-parinfer) 101 | for inspiration. 102 | 103 | # License 104 | 105 | Copyright © 2016 106 | 107 | Distributed under the Eclipse Public License. 108 | -------------------------------------------------------------------------------- /boot.properties: -------------------------------------------------------------------------------- 1 | BOOT_CLOJURE_NAME=org.clojure/clojure 2 | BOOT_CLOJURE_VERSION=1.7.0 3 | BOOT_VERSION=2.5.5 4 | BOOT_EMIT_TARGET=no 5 | -------------------------------------------------------------------------------- /build.boot: -------------------------------------------------------------------------------- 1 | (set-env! 2 | :resource-paths #{"html" "demo" "src"} 3 | :dependencies '[[adzerk/boot-cljs "1.7.228-1" :scope "test"] 4 | [pandeiro/boot-http "0.7.1-SNAPSHOT" :scope "test"] 5 | [adzerk/boot-reload "0.4.5" :scope "test"] 6 | [degree9/boot-semver "1.2.4" :scope "test"] 7 | [adzerk/boot-cljs-repl "0.3.0" :scope "test"] 8 | [com.cemerick/piggieback "0.2.1" :scope "test"] 9 | [weasel "0.7.0" :scope "test"] 10 | [org.clojure/tools.nrepl "0.2.12" :scope "test"] 11 | [crisptrutski/boot-cljs-test "0.2.2-SNAPSHOT" :scope "test"] 12 | [org.clojars.stumitchell/clairvoyant "0.1.0-SNAPSHOT" :scope "test"] 13 | [day8/re-frame-tracer "0.1.0-SNAPSHOT" :scope "test"] 14 | [deraen/boot-sass "0.2.1" :scope "test"] 15 | [adzerk/bootlaces "0.1.13" :scope "test"] 16 | [org.clojure/clojure "1.7.0"] 17 | [org.clojure/clojurescript "1.7.228"] 18 | [reagent "0.5.0"] 19 | [re-frame "0.5.0"] 20 | [replumb/replumb "0.2.1"] 21 | [cljsjs/codemirror "5.10.0-0"] 22 | [parinfer "0.2.3"]]) 23 | 24 | (require 25 | '[adzerk.boot-cljs :refer [cljs]] 26 | '[adzerk.boot-cljs-repl :refer [cljs-repl start-repl]] 27 | '[adzerk.boot-reload :refer [reload]] 28 | '[crisptrutski.boot-cljs-test :refer [test-cljs]] 29 | '[pandeiro.boot-http :refer [serve]] 30 | '[deraen.boot-sass :refer [sass]] 31 | '[boot-semver.core :refer :all] 32 | '[adzerk.bootlaces :refer :all]) 33 | 34 | (def +version+ (get-version)) 35 | 36 | (bootlaces! +version+) 37 | 38 | (task-options! pom {:project 're-console 39 | :version +version+ 40 | :url "https://github.com/Lambda-X/re-console" 41 | :description "A reactive console based on re-frame"} 42 | test-cljs {:js-env :phantom 43 | :out-file "phantom-tests.js"}) 44 | 45 | ;; This prevents a name collision WARNING between the test task and 46 | ;; clojure.core/test, a function that nobody really uses or cares 47 | ;; about. 48 | (ns-unmap 'boot.user 'test) 49 | 50 | (deftask version-file 51 | "A task that includes the version.properties file in the fileset." 52 | [] 53 | (with-pre-wrap [fileset] 54 | (boot.util/info "Add version.properties...\n") 55 | (-> fileset 56 | (add-resource (java.io.File. ".") :include #{#"^version\.properties$"}) 57 | commit!))) 58 | 59 | (deftask test [] 60 | (merge-env! :source-paths #{"test"}) 61 | (comp (speak) 62 | (test-cljs))) 63 | 64 | (deftask auto-test [] 65 | (merge-env! :source-paths #{"test"}) 66 | (comp (watch) 67 | (test))) 68 | 69 | (def foreign-libs 70 | [{:file "html/js/clojure-parinfer.js" 71 | :provides ["parinfer.codemirror.mode.clojure.clojure-parinfer"]}]) 72 | 73 | (deftask dev [] 74 | (comp (version-file) 75 | (serve) 76 | (watch) 77 | (speak) 78 | (cljs-repl) 79 | (reload :on-jsload 're-console.example/main) 80 | (sass) 81 | (cljs :optimizations :none 82 | :source-map true 83 | :compiler-options {:source-map-timestamp true 84 | :foreign-libs foreign-libs}))) 85 | 86 | (deftask build [] 87 | (merge-env! :source-paths #{"src" "demo"} :resource-paths #{"html"}) 88 | (comp (version-file) 89 | (sass) 90 | (cljs :optimizations :advanced 91 | :compiler-options {:foreign-libs foreign-libs}))) 92 | -------------------------------------------------------------------------------- /demo/re_console/example.cljs: -------------------------------------------------------------------------------- 1 | (ns re-console.example 2 | (:require [re-console.replumb-proxy :as replumb-proxy] 3 | [re-console.core :as console] 4 | [re-frame.core :refer [dispatch subscribe]] 5 | [reagent.core :as reagent] 6 | [cljsjs.codemirror] 7 | [cljsjs.codemirror.addon.edit.matchbrackets] 8 | [cljsjs.codemirror.addon.runmode.runmode] 9 | [cljsjs.codemirror.addon.runmode.colorize] 10 | [cljsjs.codemirror.mode.clojure] 11 | [re-console.parinfer :as parinfer] 12 | [parinfer.codemirror.mode.clojure.clojure-parinfer])) 13 | 14 | (defonce console-key :cljs-console) 15 | 16 | (defn toggle-verbose [] 17 | (let [verbose? (subscribe [:get-console-verbose])] 18 | (fn [] 19 | [:div 20 | [:input.button-element 21 | {:type "button" 22 | :value "Toggle verbose" 23 | :on-click #(do (dispatch [:toggle-verbose]) 24 | (dispatch [:set-console-eval-opts console-key 25 | (replumb-proxy/eval-opts (not @verbose?) ["/js/compiled/out"])]))}] 26 | [:span 27 | "Now is " [:strong (if (false? @verbose?) "false" "true")]]]))) 28 | 29 | (defn toggle-parinfer [] 30 | (let [mode (subscribe [:get-console-mode console-key])] 31 | (fn [] 32 | [:div 33 | [:input.button-element 34 | {:type "button" 35 | :value "Toggle parinfer" 36 | :on-click #(let [new-mode (if (= @mode :none) :indent-mode :none)] 37 | (dispatch [:set-console-mode console-key new-mode]))}] 38 | [:span 39 | "Now is " [:strong (name @mode)]]]))) 40 | 41 | (defn buttons 42 | [] 43 | [:div.buttons-container 44 | [toggle-verbose] 45 | [toggle-parinfer]]) 46 | 47 | (defn ^:export main [] 48 | (enable-console-print!) 49 | (dispatch [:init-options]) 50 | (reagent/render [console/console console-key {:eval-opts (replumb-proxy/eval-opts false ["/js/compiled/out"]) 51 | :mode-line? true}] 52 | (.getElementById js/document "app")) 53 | (reagent/render [buttons] 54 | (.getElementById js/document "buttons"))) 55 | -------------------------------------------------------------------------------- /demo/re_console/example/handlers.cljs: -------------------------------------------------------------------------------- 1 | (ns re-console.example.handlers 2 | (:require [re-frame.core :refer [register-handler dispatch]] 3 | [clairvoyant.core :refer-macros [trace-forms]] 4 | [re-frame-tracer.core :refer [tracer]])) 5 | 6 | ;; (trace-forms {:tracer (tracer :color "green")} 7 | 8 | (register-handler 9 | :init-options 10 | (fn init-verbose [db [_]] 11 | (assoc-in db [:options :verbose] false))) 12 | 13 | 14 | (register-handler 15 | :toggle-verbose 16 | (fn toggle-verbose [db [_]] 17 | (update-in db [:options :verbose] not))) 18 | 19 | ;; ) 20 | -------------------------------------------------------------------------------- /demo/re_console/example/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns re-console.example.subs 2 | (:require 3 | [re-frame.core :refer [register-sub]] 4 | [clairvoyant.core :refer-macros [trace-forms]] 5 | [re-frame-tracer.core :refer [tracer]]) 6 | (:require-macros 7 | [reagent.ratom :refer [reaction]])) 8 | 9 | ;; (trace-forms {:tracer (tracer :color "brown")} 10 | 11 | (register-sub 12 | :get-console-verbose 13 | (fn [db [_]] 14 | (reaction (get-in @db [:options :verbose])))) 15 | 16 | ;; ) 17 | -------------------------------------------------------------------------------- /demo/re_console/io.cljs: -------------------------------------------------------------------------------- 1 | (ns re-console.io 2 | (:import [goog.events EventType] 3 | [goog.net XhrIo])) 4 | 5 | (defn fetch-file! 6 | "Very simple implementation of XMLHttpRequests that given a file path 7 | calls src-cb with the string fetched of nil in case of error. 8 | 9 | See doc at https://developers.google.com/closure/library/docs/xhrio" 10 | [file-url src-cb] 11 | (try 12 | (.send XhrIo file-url 13 | (fn [e] 14 | (if (.isSuccess (.-target e)) 15 | (src-cb (.. e -target getResponseText)) 16 | (src-cb nil)))) 17 | (catch :default e 18 | (src-cb nil)))) 19 | -------------------------------------------------------------------------------- /demo/re_console/replumb_proxy.cljs: -------------------------------------------------------------------------------- 1 | (ns re-console.replumb-proxy 2 | (:require [replumb.core :as replumb] 3 | [replumb.repl :as replumb-repl] 4 | [re-console.io :as io])) 5 | 6 | (defn replumb-options 7 | [verbose? src-paths] 8 | (merge (replumb/options :browser src-paths io/fetch-file!) 9 | {:warning-as-error true 10 | :verbose verbose?})) 11 | 12 | (defn read-eval-call [opts cb source] 13 | (let [ns (replumb-repl/current-ns)] 14 | (replumb/read-eval-call opts 15 | #(cb {:success? (replumb/success? %) 16 | :result % 17 | :prev-ns ns 18 | :source source}) 19 | source))) 20 | 21 | (defn multiline? 22 | [input] 23 | (try 24 | (replumb-repl/read-string {} input) 25 | false 26 | (catch :default e 27 | (= "EOF" (subs (.-message e) 0 3))))) 28 | 29 | (defn eval-opts 30 | [verbose src-path] 31 | {:get-prompt replumb/get-prompt 32 | :should-eval (complement multiline?) 33 | :to-str-fn (partial replumb/result->string false true) 34 | :evaluate (partial read-eval-call 35 | (replumb-options verbose src-path))}) 36 | -------------------------------------------------------------------------------- /html/css/main.css: -------------------------------------------------------------------------------- 1 | /* workaround for refresh issues in webkit-based browsers */ 2 | @-webkit-keyframes androidBugfix {from { padding: 0; } to { padding: 0; }} 3 | body { -webkit-animation: androidBugfix infinite 1s; } 4 | /* end workaround */ 5 | 6 | body, html { 7 | background-color: #E0E0E0; 8 | } 9 | 10 | .buttons-container { 11 | padding: 10px; 12 | } 13 | 14 | .button-element { 15 | margin-right: 5px; 16 | color: #fff !important; 17 | word-spacing: 0.25em; 18 | font-family: 'Open Sans', sans-serif; 19 | text-transform: uppercase; 20 | border: none; 21 | line-height: 22px; 22 | padding: 12px 13px 11px; 23 | text-align: center; 24 | display: inline-block; 25 | margin-top: 5px; 26 | margin-bottom: 18px; 27 | text-decoration: none; 28 | } 29 | 30 | #main { 31 | width: 80%; 32 | margin: 40px auto 0; 33 | } 34 | 35 | -------------------------------------------------------------------------------- /html/css/re-console.scss: -------------------------------------------------------------------------------- 1 | $background-color: #FFF; 2 | $font-size: 15px; 3 | $font-family: Menlo,Monaco,Consolas,"Courier New",monospace; 4 | 5 | /* For status bar a the bottom *inside* see 6 | http://jsfiddle.net/AwH9S/ */ 7 | /* .re-console-wrapper { */ 8 | /* position: relative; */ 9 | /* } */ 10 | 11 | .re-console-container { 12 | background-color: $background-color; 13 | overflow-y: scroll; 14 | height: 400px; 15 | padding: 10px; 16 | border-top-left-radius: 10px; 17 | border-top-right-radius: 10px; 18 | -webkit-transform: translateZ(0); 19 | -webkit-mask-image: -webkit-radial-gradient(circle, white 100%, black 100%); 20 | } 21 | 22 | .re-console { 23 | 24 | .CodeMirror { 25 | height: auto; 26 | font-size: $font-size; 27 | font-family: $font-family; 28 | margin-bottom: 20px; 29 | } 30 | 31 | .CodeMirror-code { 32 | background-color: $background-color; 33 | } 34 | } 35 | 36 | .re-console .CodeMirror-lines, .re-console .CodeMirror pre { 37 | padding: 0; 38 | } 39 | 40 | .re-console-item, .re-console-item pre, .re-console-item-error pre { 41 | font-family: $font-family; 42 | font-size: $font-size; 43 | opacity: 0.7; 44 | background: transparent; 45 | border: none; 46 | margin: 0; 47 | padding: 0px; 48 | white-space: pre-wrap; /* CSS 3 */ 49 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ 50 | white-space: -pre-wrap; /* Opera 4-6 */ 51 | white-space: -o-pre-wrap; /* Opera 7 */ 52 | word-wrap: break-word; 53 | } 54 | 55 | .re-console-item-error { 56 | color: #e60000; 57 | } 58 | 59 | .re-console-mode-line { 60 | font-family: $font-family; 61 | font-size: 10px; 62 | text-align: center; 63 | background-color: #414141; 64 | color: white; 65 | padding: 10px; 66 | border-bottom-left-radius: 10px; 67 | border-bottom-right-radius: 10px; 68 | -webkit-transform: translateZ(0); 69 | -webkit-mask-image: -webkit-radial-gradient(circle, white 100%, black 100%); 70 | white-space: pre-wrap; /* CSS 3 */ 71 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ 72 | white-space: -pre-wrap; /* Opera 4-6 */ 73 | white-space: -o-pre-wrap; /* Opera 7 */ 74 | word-wrap: break-word; 75 | } 76 | 77 | -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | re-console 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 | 15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /html/js/clojure-parinfer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * To Parinfer developers, 3 | * 4 | * This is a syntax-highlighting mode for Clojure, copied from CodeMirror. 5 | * We modify it for Parinfer so that it dims the inferred parens at the end of a line. 6 | * (Search for Parinfer below for the relevant edit) 7 | * 8 | * For the purpose of extra-highlighting, we also modify it by tracking a previousToken 9 | * so we can highlight def'd symbols and symbols that are called to. Example: 10 | * 11 | * (def foo 123) (bar 123) 12 | * ^^^ ^^^ 13 | * |------------------|------------- highlighted as 'def' token type 14 | * 15 | * This Clojure mode also has logic for where to indent the cursor when pressing enter. 16 | * We do not modify this. 17 | * 18 | */ 19 | 20 | 21 | 22 | 23 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 24 | // Distributed under an MIT license: http://codemirror.net/LICENSE 25 | 26 | /** 27 | * Author: Hans Engel 28 | * Branched from CodeMirror's Scheme mode (by Koh Zi Han, based on implementation by Koh Zi Chun) 29 | */ 30 | 31 | (function(mod) { 32 | if (typeof exports == "object" && typeof module == "object") // CommonJS 33 | mod(require("../../lib/codemirror")); 34 | else if (typeof define == "function" && define.amd) // AMD 35 | define(["../../lib/codemirror"], mod); 36 | else // Plain browser env 37 | mod(CodeMirror); 38 | })(function(CodeMirror) { 39 | "use strict"; 40 | 41 | CodeMirror.defineMode("clojure-parinfer", function (options) { 42 | var BUILTIN = "builtin", COMMENT = "comment", STRING = "string", CHARACTER = "string-2", 43 | ATOM = "atom", NUMBER = "number", BRACKET = "bracket", KEYWORD = "keyword", VAR = "variable", 44 | SOL = "sol", EOL = "eol", MOL = "mol", // close-paren styles 45 | DEF = "def"; 46 | var INDENT_WORD_SKIP = options.indentUnit || 2; 47 | var NORMAL_INDENT_UNIT = options.indentUnit || 2; 48 | 49 | function makeKeywords(str) { 50 | var obj = {}, words = str.split(" "); 51 | for (var i = 0; i < words.length; ++i) obj[words[i]] = true; 52 | return obj; 53 | } 54 | 55 | var atoms = makeKeywords("true false nil"); 56 | 57 | var defs = makeKeywords( 58 | "defn defn- def defonce defmulti defmethod defmacro defstruct deftype ns"); 59 | 60 | var keywords = makeKeywords( 61 | "defn defn- def def- defonce defmulti defmethod defmacro defstruct deftype defprotocol defrecord defproject deftest slice defalias defhinted defmacro- defn-memo defnk defnk defonce- defunbound defunbound- defvar defvar- let letfn do case cond condp for loop recur when when-not when-let when-first if if-let if-not . .. -> ->> doto and or dosync doseq dotimes dorun doall load import unimport ns in-ns refer try catch finally throw with-open with-local-vars binding gen-class gen-and-load-class gen-and-save-class handler-case handle"); 62 | 63 | var builtins = makeKeywords( 64 | "* *' *1 *2 *3 *agent* *allow-unresolved-vars* *assert* *clojure-version* *command-line-args* *compile-files* *compile-path* *compiler-options* *data-readers* *e *err* *file* *flush-on-newline* *fn-loader* *in* *math-context* *ns* *out* *print-dup* *print-length* *print-level* *print-meta* *print-readably* *read-eval* *source-path* *unchecked-math* *use-context-classloader* *verbose-defrecords* *warn-on-reflection* + +' - -' -> ->> ->ArrayChunk ->Vec ->VecNode ->VecSeq -cache-protocol-fn -reset-methods .. / < <= = == > >= EMPTY-NODE accessor aclone add-classpath add-watch agent agent-error agent-errors aget alength alias all-ns alter alter-meta! alter-var-root amap ancestors and apply areduce array-map aset aset-boolean aset-byte aset-char aset-double aset-float aset-int aset-long aset-short assert assoc assoc! assoc-in associative? atom await await-for await1 bases bean bigdec bigint biginteger binding bit-and bit-and-not bit-clear bit-flip bit-not bit-or bit-set bit-shift-left bit-shift-right bit-test bit-xor boolean boolean-array booleans bound-fn bound-fn* bound? butlast byte byte-array bytes case cast char char-array char-escape-string char-name-string char? chars chunk chunk-append chunk-buffer chunk-cons chunk-first chunk-next chunk-rest chunked-seq? class class? clear-agent-errors clojure-version coll? comment commute comp comparator compare compare-and-set! compile complement concat cond condp conj conj! cons constantly construct-proxy contains? count counted? create-ns create-struct cycle dec dec' decimal? declare default-data-readers definline definterface defmacro defmethod defmulti defn defn- defonce defprotocol defrecord defstruct deftype delay delay? deliver denominator deref derive descendants destructure disj disj! dissoc dissoc! distinct distinct? doall dorun doseq dosync dotimes doto double double-array doubles drop drop-last drop-while empty empty? ensure enumeration-seq error-handler error-mode eval even? every-pred every? ex-data ex-info extend extend-protocol extend-type extenders extends? false? ffirst file-seq filter filterv find find-keyword find-ns find-protocol-impl find-protocol-method find-var first flatten float float-array float? floats flush fn fn? fnext fnil for force format frequencies future future-call future-cancel future-cancelled? future-done? future? gen-class gen-interface gensym get get-in get-method get-proxy-class get-thread-bindings get-validator group-by hash hash-combine hash-map hash-set identical? identity if-let if-not ifn? import in-ns inc inc' init-proxy instance? int int-array integer? interleave intern interpose into into-array ints io! isa? iterate iterator-seq juxt keep keep-indexed key keys keyword keyword? last lazy-cat lazy-seq let letfn line-seq list list* list? load load-file load-reader load-string loaded-libs locking long long-array longs loop macroexpand macroexpand-1 make-array make-hierarchy map map-indexed map? mapcat mapv max max-key memfn memoize merge merge-with meta method-sig methods min min-key mod munge name namespace namespace-munge neg? newline next nfirst nil? nnext not not-any? not-empty not-every? not= ns ns-aliases ns-imports ns-interns ns-map ns-name ns-publics ns-refers ns-resolve ns-unalias ns-unmap nth nthnext nthrest num number? numerator object-array odd? or parents partial partition partition-all partition-by pcalls peek persistent! pmap pop pop! pop-thread-bindings pos? pr pr-str prefer-method prefers primitives-classnames print print-ctor print-dup print-method print-simple print-str printf println println-str prn prn-str promise proxy proxy-call-with-super proxy-mappings proxy-name proxy-super push-thread-bindings pvalues quot rand rand-int rand-nth range ratio? rational? rationalize re-find re-groups re-matcher re-matches re-pattern re-seq read read-line read-string realized? reduce reduce-kv reductions ref ref-history-count ref-max-history ref-min-history ref-set refer refer-clojure reify release-pending-sends rem remove remove-all-methods remove-method remove-ns remove-watch repeat repeatedly replace replicate require reset! reset-meta! resolve rest restart-agent resultset-seq reverse reversible? rseq rsubseq satisfies? second select-keys send send-off seq seq? seque sequence sequential? set set-error-handler! set-error-mode! set-validator! set? short short-array shorts shuffle shutdown-agents slurp some some-fn sort sort-by sorted-map sorted-map-by sorted-set sorted-set-by sorted? special-symbol? spit split-at split-with str string? struct struct-map subs subseq subvec supers swap! symbol symbol? sync take take-last take-nth take-while test the-ns thread-bound? time to-array to-array-2d trampoline transient tree-seq true? type unchecked-add unchecked-add-int unchecked-byte unchecked-char unchecked-dec unchecked-dec-int unchecked-divide-int unchecked-double unchecked-float unchecked-inc unchecked-inc-int unchecked-int unchecked-long unchecked-multiply unchecked-multiply-int unchecked-negate unchecked-negate-int unchecked-remainder-int unchecked-short unchecked-subtract unchecked-subtract-int underive unquote unquote-splicing update-in update-proxy use val vals var-get var-set var? vary-meta vec vector vector-of vector? when when-first when-let when-not while with-bindings with-bindings* with-in-str with-loading-context with-local-vars with-meta with-open with-out-str with-precision with-redefs with-redefs-fn xml-seq zero? zipmap *default-data-reader-fn* as-> cond-> cond->> reduced reduced? send-via set-agent-send-executor! set-agent-send-off-executor! some-> some->>"); 65 | 66 | var indentKeys = makeKeywords( 67 | // Built-ins 68 | "ns fn def defn defmethod bound-fn if if-not case condp when while when-not when-first do future comment doto locking proxy with-open with-precision reify deftype defrecord defprotocol extend extend-protocol extend-type try catch " + 69 | 70 | // Binding forms 71 | "let letfn binding loop for doseq dotimes when-let if-let " + 72 | 73 | // Data structures 74 | "defstruct struct-map assoc " + 75 | 76 | // clojure.test 77 | "testing deftest " + 78 | 79 | // contrib 80 | "handler-case handle dotrace deftrace"); 81 | 82 | var tests = { 83 | digit: /\d/, 84 | digit_or_colon: /[\d:]/, 85 | hex: /[0-9a-f]/i, 86 | sign: /[+-]/, 87 | exponent: /e/i, 88 | keyword_char: /[^\s\(\[\;\)\]]/, 89 | symbol: /[\w*+!\-\._?:<>\/\xa1-\uffff]/ 90 | }; 91 | 92 | function stateStack(indent, type, prev) { // represents a state stack object 93 | this.indent = indent; 94 | this.type = type; 95 | this.prev = prev; 96 | } 97 | 98 | function pushStack(state, indent, type) { 99 | state.indentStack = new stateStack(indent, type, state.indentStack); 100 | } 101 | 102 | function popStack(state) { 103 | state.indentStack = state.indentStack.prev; 104 | } 105 | 106 | function isNumber(ch, stream){ 107 | // hex 108 | if ( ch === '0' && stream.eat(/x/i) ) { 109 | stream.eatWhile(tests.hex); 110 | return true; 111 | } 112 | 113 | // leading sign 114 | if ( ( ch == '+' || ch == '-' ) && ( tests.digit.test(stream.peek()) ) ) { 115 | stream.eat(tests.sign); 116 | ch = stream.next(); 117 | } 118 | 119 | if ( tests.digit.test(ch) ) { 120 | stream.eat(ch); 121 | stream.eatWhile(tests.digit); 122 | 123 | if ( '.' == stream.peek() ) { 124 | stream.eat('.'); 125 | stream.eatWhile(tests.digit); 126 | } 127 | 128 | if ( stream.eat(tests.exponent) ) { 129 | stream.eat(tests.sign); 130 | stream.eatWhile(tests.digit); 131 | } 132 | 133 | return true; 134 | } 135 | 136 | return false; 137 | } 138 | 139 | // Eat character that starts after backslash \ 140 | function eatCharacter(stream) { 141 | var first = stream.next(); 142 | // Read special literals: backspace, newline, space, return. 143 | // Just read all lowercase letters. 144 | if (first && first.match(/[a-z]/) && stream.match(/[a-z]+/, true)) { 145 | return; 146 | } 147 | // Read unicode character: \u1000 \uA0a1 148 | if (first === "u") { 149 | stream.match(/[0-9a-z]{4}/i, true); 150 | } 151 | } 152 | 153 | return { 154 | startState: function () { 155 | return { 156 | previousToken: null, 157 | indentStack: null, 158 | indentation: 0, 159 | mode: false, 160 | atStart: false 161 | }; 162 | }, 163 | 164 | token: function (stream, state) { 165 | 166 | if (stream.sol()) { 167 | state.atStart = (state.mode != "string"); 168 | } 169 | 170 | if (state.indentStack == null && stream.sol()) { 171 | // update indentation, but only if indentStack is empty 172 | state.indentation = stream.indentation(); 173 | } 174 | 175 | // skip spaces 176 | if (stream.eatSpace()) { 177 | return null; 178 | } 179 | var returnType = null; 180 | var previousToken = null; 181 | 182 | switch(state.mode){ 183 | case "string": // multi-line string parsing mode 184 | var next, escaped = false; 185 | while ((next = stream.next()) != null) { 186 | if (next == "\"" && !escaped) { 187 | 188 | state.mode = false; 189 | break; 190 | } 191 | escaped = !escaped && next == "\\"; 192 | } 193 | returnType = STRING; // continue on in string mode 194 | break; 195 | default: // default parsing mode 196 | var ch = stream.next(); 197 | 198 | if (ch == "\"") { 199 | state.mode = "string"; 200 | returnType = STRING; 201 | } else if (ch == "\\") { 202 | eatCharacter(stream); 203 | returnType = CHARACTER; 204 | } else if (ch == "'" && !( tests.digit_or_colon.test(stream.peek()) )) { 205 | returnType = ATOM; 206 | } else if (ch == ";") { // comment 207 | stream.skipToEnd(); // rest of the line is a comment 208 | returnType = COMMENT; 209 | } else if (isNumber(ch,stream)){ 210 | returnType = NUMBER; 211 | } else if (ch == "(" || ch == "[" || ch == "{" ) { 212 | if (ch == "(") { 213 | previousToken = "("; 214 | } 215 | 216 | var keyWord = '', indentTemp = stream.column(), letter; 217 | /** 218 | Either 219 | (indent-word .. 220 | (non-indent-word .. 221 | (;something else, bracket, etc. 222 | */ 223 | 224 | if (ch == "(") while ((letter = stream.eat(tests.keyword_char)) != null) { 225 | keyWord += letter; 226 | } 227 | 228 | if (keyWord.length > 0 && (indentKeys.propertyIsEnumerable(keyWord) || 229 | /^(?:def|with)/.test(keyWord))) { // indent-word 230 | pushStack(state, indentTemp + INDENT_WORD_SKIP, ch); 231 | } else { // non-indent word 232 | // we continue eating the spaces 233 | stream.eatSpace(); 234 | if (stream.eol() || stream.peek() == ";") { 235 | // nothing significant after 236 | // we restart indentation the user defined spaces after 237 | pushStack(state, indentTemp + NORMAL_INDENT_UNIT, ch); 238 | } else { 239 | pushStack(state, indentTemp + stream.current().length, ch); // else we match 240 | } 241 | } 242 | stream.backUp(stream.current().length - 1); // undo all the eating 243 | 244 | returnType = BRACKET; 245 | } else if (ch == ")" || ch == "]" || ch == "}") { 246 | returnType = BRACKET; 247 | 248 | // Parinfer: (style trailing delimiters) 249 | stream.eatWhile(/[\s,\]})]/); 250 | if (stream.eol() || stream.peek() == ";") { 251 | returnType += " " + EOL; 252 | } else if (state.atStart) { 253 | returnType += " " + SOL; 254 | } else { 255 | returnType += " " + MOL; 256 | } 257 | stream.backUp(stream.current().length - 1); 258 | 259 | if (state.indentStack != null && state.indentStack.type == (ch == ")" ? "(" : (ch == "]" ? "[" :"{"))) { 260 | popStack(state); 261 | } 262 | } else if ( ch == ":" ) { 263 | stream.eatWhile(tests.symbol); 264 | return ATOM; 265 | } else { 266 | stream.eatWhile(tests.symbol); 267 | 268 | if (keywords && keywords.propertyIsEnumerable(stream.current())) { 269 | returnType = KEYWORD; 270 | } else if (builtins && builtins.propertyIsEnumerable(stream.current())) { 271 | returnType = BUILTIN; 272 | } else if (atoms && atoms.propertyIsEnumerable(stream.current())) { 273 | returnType = ATOM; 274 | } else if (state.previousToken == "def" || state.previousToken == "(") { 275 | returnType = DEF; 276 | } else { 277 | returnType = VAR; 278 | } 279 | 280 | if (state.previousToken == "(" && defs && defs.propertyIsEnumerable(stream.current())) { 281 | previousToken = "def"; 282 | } 283 | } 284 | 285 | if (!(ch == ")" || ch == "]" || ch == "}")) { 286 | state.atStart = false; 287 | } 288 | } 289 | 290 | state.previousToken = previousToken; 291 | 292 | return returnType; 293 | }, 294 | 295 | indent: function (state) { 296 | if (state.indentStack == null) return state.indentation; 297 | return state.indentStack.indent; 298 | }, 299 | 300 | closeBrackets: {pairs: "()[]{}\"\""}, 301 | lineComment: ";;" 302 | }; 303 | }); 304 | 305 | CodeMirror.defineMIME("text/x-clojure", "clojure"); 306 | 307 | }); 308 | -------------------------------------------------------------------------------- /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lambda-X/re-console/a7a31046ba5066163d46ac586072c92b7414c52b/screenshot.gif -------------------------------------------------------------------------------- /src/re_console/app.cljs: -------------------------------------------------------------------------------- 1 | (ns re-console.app) 2 | 3 | (def initial-console-state {:items [] 4 | :hist-pos 0 5 | :queued-forms [] 6 | :history [""] 7 | :cm-instance nil 8 | :eval-opts {} 9 | :mode :none 10 | ;; parinfer technical details 11 | :frame-updated? false 12 | :on-before-change nil 13 | :on-after-change nil}) 14 | 15 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 16 | ;;; Getters ;;; 17 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 18 | 19 | (defn console 20 | [db k] 21 | (get-in db [:consoles (name k) :cm-instance])) 22 | 23 | (def console-created? 24 | "Was the console created? Returns a truey or falsey value." 25 | (comp not nil? console)) 26 | 27 | (defn console-items 28 | [db k] 29 | (get-in db [:consoles (name k) :items])) 30 | 31 | (defn console-mode 32 | [db k] 33 | (get-in db [:consoles (name k) :mode])) 34 | 35 | (defn console-frame-updated 36 | [db k] 37 | (get-in db [:consoles (name k) :frame-updated?])) 38 | 39 | (defn console-history 40 | [db k] 41 | (get-in db [:consoles (name k) :history])) 42 | 43 | (defn console-history-pos 44 | [db k] 45 | (get-in db [:consoles (name k) :hist-pos])) 46 | 47 | (defn queued-forms 48 | [db k] 49 | (get-in db [:consoles (name k) :queued-forms])) 50 | 51 | (defn console-eval-opts 52 | [db k] 53 | (get-in db [:consoles (name k) :eval-opts])) 54 | 55 | (defn console-full-text 56 | [db k] 57 | (let [items (console-items db k) 58 | to-str-fn (:to-str-fn (console-eval-opts db k))] 59 | (apply str (interpose \newline (map (fn [item] 60 | (if-let [text (:text item)] 61 | (str (:ns item) "=> " text) 62 | (to-str-fn (:value item)))) 63 | items))))) 64 | 65 | (defn console-on-before-change 66 | [db k] 67 | (get-in db [:consoles (name k) :on-before-change])) 68 | 69 | (defn console-on-after-change 70 | [db k] 71 | (get-in db [:consoles (name k) :on-after-change])) 72 | 73 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 74 | ;;; State modifiers ;;; 75 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 76 | 77 | (defn init-console 78 | [db k initial-user-state] 79 | (assoc-in db [:consoles (name k)] (merge initial-console-state initial-user-state))) 80 | 81 | (defn add-console-instance 82 | [db k instance] 83 | (assoc-in db [:consoles (name k) :cm-instance] instance)) 84 | 85 | (defn set-console-eval-opts 86 | [db k eval-opts] 87 | (assoc-in db [:consoles (name k) :eval-opts] eval-opts)) 88 | 89 | (defn set-console-mode [db console-key mode] 90 | (assoc-in db [:consoles (name console-key) :mode] mode)) 91 | 92 | (defn set-console-frame-updated [db console-key value] 93 | (assoc-in db [:consoles (name console-key) :frame-updated?] value)) 94 | 95 | (defn update-console-history 96 | [db k idx pos text] 97 | (update-in db [:consoles (name k) :history] 98 | (fn [current-history] 99 | (if (= pos 0) 100 | (assoc current-history idx text) 101 | (filterv seq (conj current-history text)))))) 102 | 103 | (defn set-console-history-position 104 | [db k new-pos] 105 | (assoc-in db [:consoles (name k) :hist-pos] new-pos)) 106 | 107 | (defn set-console-text 108 | [db k text] 109 | (let [history (console-history db k) 110 | pos (console-history-pos db k) 111 | idx (- (count history) pos 1)] 112 | (-> db 113 | (set-console-history-position k 0) 114 | (update-console-history k idx pos text)))) 115 | 116 | (defn clear-console-items 117 | [db k] 118 | (-> db 119 | (assoc-in [:consoles (name k) :items] []) 120 | (set-console-text k ""))) 121 | 122 | (defn reset-console-items 123 | [db k] 124 | (update-in db [:consoles (name k)] 125 | (fn [current-state] 126 | (merge current-state 127 | (select-keys initial-console-state [:items :hist-pos :history]))))) 128 | 129 | (defn add-console-item 130 | [db k item] 131 | (update-in db [:consoles (name k) :items] conj item)) 132 | 133 | (defn add-console-items 134 | [db k items] 135 | (update-in db [:consoles (name k) :items] concat items)) 136 | 137 | (defn add-console-history-item 138 | [db k item] 139 | (update-in db [:consoles (name k) :history] conj item)) 140 | 141 | (defn add-console-input-item 142 | [db k inum input ns] 143 | (update-in db [:consoles (name k) :items] conj {:type :input 144 | :text input 145 | :num inum 146 | :ns ns})) 147 | 148 | (defn add-console-input 149 | [db k input ns] 150 | (let [inum (count (console-history db k))] 151 | (add-console-input-item db k inum input ns))) 152 | 153 | (defn add-console-result 154 | [db k error? value] 155 | (update-in db [:consoles (name k) :items] conj {:type (if error? :error :output) 156 | :value value})) 157 | 158 | (defn add-console-log 159 | [db k item] 160 | (update-in db [:consoles (name k) :items] conj {:type :log 161 | :value item})) 162 | 163 | (defn console-go-up 164 | [db k] 165 | (let [pos (console-history-pos db k) 166 | len (count (console-history db k)) 167 | new-pos (if (>= pos (dec len)) 168 | pos 169 | (inc pos))] 170 | (set-console-history-position db k new-pos))) 171 | 172 | (defn console-go-down 173 | [db k] 174 | (update-in db [:consoles (name k) :hist-pos] 175 | (fn [pos] (if (<= pos 0) 176 | 0 177 | (dec pos))))) 178 | 179 | (defn clear-console-queued-forms 180 | [db k] 181 | (-> db 182 | (assoc-in [:consoles (name k) :queued-forms] []) 183 | (set-console-text k ""))) 184 | 185 | (defn set-console-queued-forms 186 | [db k forms] 187 | (-> db 188 | (set-console-text k (first forms)) 189 | (assoc-in [:consoles (name k) :queued-forms] (rest forms)))) 190 | 191 | (defn drop-first-queued-form 192 | [db k] 193 | (update-in db [:consoles (name k) :queued-forms] (partial drop 1))) 194 | 195 | (defn set-next-queued-form-if-any 196 | [db k] 197 | (if-let [form (first (queued-forms db k))] 198 | (-> db 199 | (set-console-text k form) 200 | (drop-first-queued-form k)) 201 | db)) 202 | 203 | (defn on-eval-complete 204 | [db k {:keys [prev-ns source success? result]}] 205 | (let [db (if (seq source) 206 | (-> db 207 | (set-console-text k source) 208 | (set-console-history-position k 0) 209 | (add-console-history-item k "")) 210 | db)] 211 | (-> db 212 | (add-console-input k source prev-ns) 213 | (add-console-result k (not success?) result) 214 | (set-next-queued-form-if-any k)))) 215 | 216 | (defn set-console-on-before-change 217 | [db k on-before-change] 218 | (assoc-in db [:consoles (name k) :on-before-change] on-before-change)) 219 | 220 | (defn set-console-on-after-change 221 | [db k on-after-change] 222 | (assoc-in db [:consoles (name k) :on-after-change] on-after-change)) 223 | 224 | -------------------------------------------------------------------------------- /src/re_console/common.cljs: -------------------------------------------------------------------------------- 1 | (ns re-console.common) 2 | 3 | (defn beginning-of-source 4 | [exp] 5 | (let [idx (.indexOf exp "=> ")] 6 | (if (not= -1 idx) 7 | (+ 3 (.indexOf exp "=> ")) 8 | 0))) 9 | 10 | (defn source-without-prompt 11 | [exp] 12 | exp 13 | (subs exp (beginning-of-source exp))) 14 | 15 | (defn scroll-to-el-bottom! 16 | [el] 17 | (set! (.-scrollTop el) (.-scrollHeight el))) 18 | -------------------------------------------------------------------------------- /src/re_console/core.cljs: -------------------------------------------------------------------------------- 1 | (ns re-console.core 2 | (:require [reagent.core :as reagent :refer [atom]] 3 | [re-frame.core :refer [subscribe dispatch dispatch-sync]] 4 | [re-console.handlers :as handlers] 5 | [re-console.subs :as subs] 6 | [re-console.editor :as editor] 7 | [re-console.common :as common] 8 | [re-console.utils :as utils])) 9 | 10 | ;;; many parts are taken from jaredly's reepl 11 | ;;; https://github.com/jaredly/reepl 12 | 13 | (defn display-console-output-item 14 | ([console-key value] 15 | (display-console-output-item console-key value false)) 16 | ([console-key value error?] 17 | [:div 18 | {:on-click #(dispatch [:focus-console-editor console-key])} 19 | [:div [:pre {:style {:margin 0 :padding 0} 20 | :class (str "re-console-item" (when error? " re-console-item-error"))} 21 | value]]])) 22 | 23 | (defn display-console-item 24 | [console-key to-str-fn item] 25 | (if-let [text (:text item)] 26 | [:div.re-console-item {:on-click #(do (dispatch [:console-set-text console-key text]) 27 | (dispatch [:focus-console-editor console-key]))} 28 | [utils/colored-text (str (:ns item) "=> " text)]] 29 | (display-console-output-item console-key 30 | (to-str-fn (:value item)) 31 | (= :error (:type item))))) 32 | 33 | (defn console-items 34 | ([console-key items] 35 | (console-items console-key items identity)) 36 | ([console-key items to-str-fn] 37 | (into [:div {:on-click #(.stopPropagation %)}] 38 | (map (partial display-console-item console-key to-str-fn) items)))) 39 | 40 | (defn mode-line 41 | [console-key] 42 | (let [mode (subscribe [:get-console-mode console-key]) 43 | queued-forms-count (subscribe [:queued-forms-count console-key])] 44 | [:div.re-console-mode-line 45 | (str "Current mode: " (name @mode) " | " 46 | @queued-forms-count " form(s) in the evaluation queue")])) 47 | 48 | (defn console [console-key opts] 49 | (let [items (subscribe [:get-console-items console-key]) 50 | text (subscribe [:get-console-current-text console-key])] 51 | (dispatch-sync [:init-console console-key opts]) 52 | (reagent/create-class 53 | {:reagent-render 54 | (fn [] 55 | [:div 56 | [:div.re-console-container 57 | {:on-click #(dispatch [:focus-console-editor console-key])} 58 | [:div.re-console 59 | [console-items console-key @items (-> opts :eval-opts :to-str-fn)] 60 | [editor/console-editor console-key text]]] 61 | (when (:mode-line? opts) 62 | [mode-line console-key])]) 63 | :component-did-update 64 | (fn [this] 65 | (common/scroll-to-el-bottom! (.-firstChild (reagent/dom-node this))))}))) 66 | -------------------------------------------------------------------------------- /src/re_console/editor.cljs: -------------------------------------------------------------------------------- 1 | (ns re-console.editor 2 | (:require [reagent.core :as reagent] 3 | [re-frame.core :refer [dispatch subscribe dispatch-sync]] 4 | [re-console.common :as common] 5 | [re-console.parinfer :as parinfer] 6 | [cljsjs.codemirror] 7 | [cljsjs.codemirror.addon.edit.matchbrackets] 8 | [cljsjs.codemirror.addon.runmode.runmode] 9 | [cljsjs.codemirror.addon.runmode.colorize] 10 | [cljsjs.codemirror.mode.clojure] 11 | [parinfer.codemirror.mode.clojure.clojure-parinfer])) 12 | 13 | (defn default-cm-options 14 | [initial-prompt mode] 15 | {:lineNumbers false 16 | :viewportMargin js/Infinity 17 | :showCursorWhenSelecting true 18 | :lineWrapping true 19 | :matchBrackets true 20 | :autofocus true 21 | :extraKeys #js {"Shift-Enter" "newlineAndIndent"} 22 | :value initial-prompt 23 | :mode (if (= mode :none) "clojure" "clojure-parinfer")}) 24 | 25 | (def default-cm-handlers 26 | {:should-go-up 27 | (fn [source inst] 28 | (let [pos (.getCursor inst)] 29 | (= 0 (.-line pos)))) 30 | 31 | :should-go-down 32 | (fn [source inst] 33 | (let [pos (.getCursor inst) 34 | last-line (.lastLine inst)] 35 | (= last-line (.-line pos))))}) 36 | 37 | (defn handlers [console-key] 38 | {:add-input #(dispatch [:add-console-input console-key %1 %2]) 39 | :add-result #(dispatch [:add-console-result console-key %1 %2]) 40 | :go-up #(dispatch [:console-go-up console-key %]) 41 | :go-down #(dispatch [:console-go-down console-key %]) 42 | :clear-items #(dispatch [:clear-console-items console-key %]) 43 | :set-text #(dispatch [:console-set-text console-key %1]) 44 | :add-log #(dispatch [:add-console-log console-key %])}) 45 | 46 | (defn move-to-end 47 | [cm] 48 | (let [last-line (.lastLine cm) 49 | last-ch (count (.getLine cm last-line))] 50 | (.setCursor cm last-line last-ch))) 51 | 52 | (defn modifying-prompt? 53 | [inst key] 54 | (let [pos-to (.getCursor inst "to") 55 | pos-from (.getCursor inst "from") 56 | lno (.-line pos-to) 57 | cno-to (.-ch pos-to) 58 | cno-from (.-ch pos-from) 59 | compare-position-fn (if (= 8 key) <= <) 60 | begin-source (common/beginning-of-source (.getValue inst)) 61 | same-pos (= pos-to pos-from)] 62 | (and ((complement #{37 38 39 40}) key) 63 | (zero? lno) 64 | (or (compare-position-fn cno-to begin-source) 65 | (and (not same-pos) 66 | (compare-position-fn cno-from (dec begin-source))))))) 67 | 68 | (defn on-viewport-change [this] 69 | (fn [] 70 | (let [el (reagent/dom-node this) 71 | re-console (.-parentElement el) 72 | box (.-parentElement re-console)] 73 | (common/scroll-to-el-bottom! box)))) 74 | 75 | (defn on-change [inst console-key text {:keys [set-text]} mode] 76 | (let [on-after-change (subscribe [:get-console-on-after-change console-key]) 77 | on-before-change (subscribe [:get-console-on-before-change console-key])] 78 | (fn [inst evt] 79 | (when (= :none @mode) 80 | (let [value (common/source-without-prompt (.getValue inst))] 81 | (when-not (= value @text) 82 | (when @on-before-change 83 | ( @on-before-change inst evt)) 84 | (set-text value) 85 | (when @on-after-change 86 | (@on-after-change inst evt)))))))) 87 | 88 | (defn on-keydown [console-key 89 | {:keys [should-go-up should-go-down go-up go-down]} 90 | eval-opts] 91 | (fn [inst evt] 92 | (if (modifying-prompt? inst (.-keyCode evt)) 93 | (.preventDefault evt) 94 | (case (.-keyCode evt) 95 | ;; enter 96 | 13 (let [source (common/source-without-prompt (.getValue inst))] 97 | (when (and (not (.-shiftKey evt)) ((:should-eval @eval-opts) source)) 98 | (.preventDefault evt) 99 | ((:evaluate @eval-opts) #(dispatch [:on-eval-complete console-key %]) source))) 100 | ;; up 101 | 38 (let [source (common/source-without-prompt (.getValue inst))] 102 | (when (and (not (.-shiftKey evt)) 103 | (should-go-up source inst)) 104 | (.preventDefault evt) 105 | (go-up))) 106 | ;; down 107 | 40 (let [source (common/source-without-prompt (.getValue inst))] 108 | (when (and (not (.-shiftKey evt)) 109 | (should-go-down source inst)) 110 | (.preventDefault evt) 111 | (go-down))) 112 | :none)))) 113 | 114 | (defn console-editor 115 | [console-key text] 116 | (let [cm (subscribe [:get-console console-key]) 117 | handlers (merge (handlers console-key) default-cm-handlers) 118 | eval-opts (subscribe [:get-console-eval-opts console-key]) 119 | mode (subscribe [:get-console-mode console-key]) 120 | on-before-change (subscribe [:get-console-on-before-change console-key])] 121 | (reagent/create-class 122 | {:component-did-mount 123 | (fn [this] 124 | (let [el (reagent/dom-node this) 125 | inst (js/CodeMirror. 126 | el 127 | (clj->js (default-cm-options (str ((:get-prompt @eval-opts)) @text) @mode)))] 128 | (dispatch-sync [:add-console-instance console-key inst]) 129 | (move-to-end inst) 130 | (.on inst "viewportChange" (on-viewport-change this)) 131 | (.on inst "change" (on-change inst console-key text handlers mode)) 132 | (.on inst "keydown" (on-keydown console-key handlers eval-opts)) 133 | (parinfer/parinferize! inst console-key))) 134 | :component-did-update 135 | (fn [this old-argv] 136 | (when-not (= @text (common/source-without-prompt (.getValue @cm))) 137 | (let [cursor (.getCursor @cm) 138 | line-idx (.-line cursor) 139 | cursor-idx (.-ch cursor)] 140 | (.setValue @cm (str ((:get-prompt @eval-opts)) @text)) 141 | (let [current-line (.getLine @cm (min (dec (.lineCount @cm)) line-idx)) 142 | last-idx (loop [exclude-chars #{\( \[ \{ \) \] \} \space}] 143 | (if (seq exclude-chars) 144 | (let [char (first exclude-chars) 145 | char-index (.indexOf current-line char cursor-idx)] 146 | (if (= char-index -1) 147 | (recur (rest exclude-chars)) 148 | char-index)) 149 | (count current-line)))] 150 | (.setCursor @cm line-idx last-idx))))) 151 | :reagent-render 152 | (fn [] 153 | @text 154 | [:div])}))) 155 | 156 | -------------------------------------------------------------------------------- /src/re_console/handlers.cljs: -------------------------------------------------------------------------------- 1 | (ns re-console.handlers 2 | (:require [re-frame.core :refer [register-handler dispatch]] 3 | [clairvoyant.core :refer-macros [trace-forms]] 4 | [re-frame-tracer.core :refer [tracer]] 5 | [re-console.app :as app])) 6 | 7 | ;; (trace-forms {:tracer (tracer :color "green")} 8 | 9 | (register-handler 10 | :init-console 11 | (fn add-console [db [_ console-key eval-opts]] 12 | (app/init-console db console-key eval-opts))) 13 | 14 | (register-handler 15 | :add-console-instance 16 | (fn add-console [db [_ console-key instance]] 17 | (app/add-console-instance db console-key instance))) 18 | 19 | (register-handler 20 | :focus-console-editor 21 | (fn focus-console-editor [db [_ console-key]] 22 | (when-let [instance (app/console db console-key)] 23 | (.focus instance)) 24 | db)) 25 | 26 | (register-handler 27 | :set-console-theme 28 | (fn set-console-theme [db [_ console-key theme]] 29 | (when-let [instance (app/console db console-key)] 30 | (.setOption instance "theme" theme)) 31 | db)) 32 | 33 | (register-handler 34 | :clear-console-items 35 | (fn clear-console-items [db [_ console-key]] 36 | (dispatch [:focus-console-editor console-key]) 37 | (app/clear-console-items db console-key))) 38 | 39 | (register-handler 40 | :reset-console-items 41 | (fn reset-console-items [db [_ console-key]] 42 | (dispatch [:focus-console-editor console-key]) 43 | (app/reset-console-items db console-key))) 44 | 45 | (register-handler 46 | :add-console-item 47 | (fn add-console-item [db [_ console-key item]] 48 | (app/add-console-item db console-key item))) 49 | 50 | (register-handler 51 | :add-console-items 52 | (fn add-console-items [db [_ console-key items]] 53 | (app/add-console-item db console-key items))) 54 | 55 | (register-handler 56 | :add-console-input 57 | (fn add-console-input [db [_ console-key input ns]] 58 | (app/add-console-input db console-key input ns))) 59 | 60 | (register-handler 61 | :add-console-result 62 | (fn add-console-result [db [_ console-key error? value]] 63 | (app/add-console-result db console-key error? value))) 64 | 65 | (register-handler 66 | :add-console-log 67 | (fn add-console-log [db [_ console-key item]] 68 | (app/add-console-log db console-key item))) 69 | 70 | (register-handler 71 | :console-set-text 72 | (fn console-set-text [db [_ console-key text]] 73 | (app/set-console-text db console-key text))) 74 | 75 | (register-handler 76 | :console-go-up 77 | (fn console-go-up [db [_ console-key]] 78 | (app/console-go-up db console-key))) 79 | 80 | (register-handler 81 | :console-go-down 82 | (fn console-go-down [db [_ console-key]] 83 | (app/console-go-down db console-key))) 84 | 85 | (register-handler 86 | :set-console-queued-forms 87 | (fn set-queued-forms [db [_ console-key forms]] 88 | (dispatch [:focus-console-editor console-key]) 89 | (app/set-console-queued-forms db console-key forms))) 90 | 91 | (register-handler 92 | :clear-console-queued-forms 93 | (fn clear-console-queued-forms [db [_ console-key]] 94 | (dispatch [:focus-console-editor console-key]) 95 | (app/clear-console-queued-forms db console-key))) 96 | 97 | (register-handler 98 | :on-eval-complete 99 | (fn on-eval-complete [db [_ console-key result-map]] 100 | (app/on-eval-complete db console-key result-map))) 101 | 102 | (register-handler 103 | :set-console-eval-opts 104 | (fn set-console-eval-opts [db [_ console-key eval-opts]] 105 | (app/set-console-eval-opts db console-key eval-opts))) 106 | 107 | (register-handler 108 | :set-console-mode 109 | (fn set-console-mode [db [_ console-key mode]] 110 | (dispatch [:focus-console-editor console-key]) 111 | (app/set-console-mode db console-key mode))) 112 | 113 | (register-handler 114 | :set-console-frame-updated 115 | (fn set-console-frame-updated [db [_ console-key value]] 116 | (app/set-console-frame-updated db console-key value))) 117 | 118 | (register-handler 119 | :set-console-on-before-change 120 | (fn set-console-on-before-change [db [_ console-key on-before-change]] 121 | (app/set-console-on-before-change db console-key on-before-change))) 122 | 123 | (register-handler 124 | :set-console-on-after-change 125 | (fn set-console-on-after-change [db [_ console-key on-after-change]] 126 | (app/set-console-on-after-change db console-key on-after-change))) 127 | 128 | ;; ) 129 | -------------------------------------------------------------------------------- /src/re_console/parinfer.cljs: -------------------------------------------------------------------------------- 1 | (ns re-console.parinfer 2 | "Glues Parinfer's formatter to a CodeMirror editor" 3 | (:require [clojure.string :refer [join]] 4 | [re-frame.core :refer [subscribe dispatch dispatch-sync]] 5 | [parinfer.indent-mode :as indent-mode] 6 | [parinfer.paren-mode :as paren-mode] 7 | [re-console.common :as common])) 8 | 9 | ;;; Editor support 10 | 11 | (defn update-cursor-fn 12 | "Correctly position cursor after text that was just typed. 13 | We need this since reformatting the text can shift things forward past our cursor." 14 | [console-key] 15 | (let [cm (subscribe [:get-console console-key])] 16 | (fn [change] 17 | (when (= "+input" (.-origin change)) 18 | (let [selection? (.somethingSelected @cm) 19 | text (join "\n" (.-text change)) 20 | from-x (.. change -from -ch) 21 | line-no (.. change -from -line) 22 | line (.getLine @cm line-no) 23 | insert-x (.indexOf line text from-x) 24 | after-x (+ insert-x (count text))] 25 | (cond 26 | ;; something is selected, don't touch the cursor 27 | selection? 28 | nil 29 | 30 | ;; pressing return, keep current position then. 31 | (= text "\n") 32 | nil 33 | 34 | ;; only move the semicolon ahead since it can be pushed forward by 35 | ;; commenting out inferred parens meaning they are immediately 36 | ;; reinserted behind it. 37 | (= text ";") 38 | (.setCursor @cm line-no after-x) 39 | 40 | ;; typed character not found where expected it, we probably prevented it. keep cursor where it was. 41 | (or (= -1 insert-x) 42 | (> insert-x from-x)) 43 | (.setCursor @cm line-no from-x) 44 | 45 | :else nil)))))) 46 | 47 | (defn compute-cursor-dx 48 | [cursor change] 49 | (when change 50 | (let [;; This is a hack for codemirror. 51 | ;; For some reason codemirror triggers an "+input" change after the 52 | ;; indent spaces are already applied. So I modified codemirror to 53 | ;; label these changes as +indenthack so we can ignore them. 54 | ignore? (= "+indenthack" (.-origin change))] 55 | (if ignore? 56 | 0 57 | (let [start-x (.. change -to -ch) 58 | new-lines (.. change -text) 59 | len-last-line (count (last new-lines)) 60 | end-x (if (> (count new-lines) 1) 61 | len-last-line 62 | (+ len-last-line (.. change -from -ch)))] 63 | (- end-x start-x)))))) 64 | 65 | (defn compute-cm-change 66 | [cm change options prev-state] 67 | (let [{:keys [start-line end-line num-new-lines]} 68 | (if change 69 | {:start-line (.. change -from -line) 70 | :end-line (inc (.. change -to -line)) 71 | :num-new-lines (alength (.-text change))} 72 | 73 | (let [start (:cursor-line prev-state) 74 | end (inc start)] 75 | {:start-line start 76 | :end-line end 77 | :num-new-lines (- end start)})) 78 | 79 | lines (for [i (range start-line (+ start-line num-new-lines))] 80 | (.getLine cm i))] 81 | {:line-no [start-line end-line] 82 | :new-line lines})) 83 | 84 | (defn fix-text-fn 85 | "Correctly format the text from the given editor." 86 | [console-key] 87 | (let [cm (subscribe [:get-console console-key]) 88 | mode (subscribe [:get-console-mode console-key]) 89 | eval-opts (subscribe [:get-console-eval-opts console-key]) 90 | prev-state (atom nil)] 91 | (fn [& {:keys [change use-cache?] 92 | :or {change nil, use-cache? false}}] 93 | (let [current-text (.getValue @cm) 94 | selection? (.somethingSelected @cm) 95 | selections (.listSelections @cm) 96 | cursor (.getCursor @cm) 97 | scroller (.getScrollerElement @cm) 98 | scroll-x (.-scrollLeft scroller) 99 | scroll-y (.-scrollTop scroller) 100 | options {:cursor-line (.-line cursor) 101 | :cursor-x (.-ch cursor) 102 | :cursor-dx (compute-cursor-dx cursor change)} 103 | new-text (case @mode 104 | :indent-mode 105 | (let [result (if (and use-cache? @prev-state) 106 | (indent-mode/format-text-change 107 | current-text 108 | @prev-state 109 | (compute-cm-change @cm change options @prev-state) 110 | options) 111 | (indent-mode/format-text current-text options))] 112 | (when (:valid? result) 113 | (reset! prev-state (:state result))) 114 | (:text result)) 115 | :paren-mode 116 | (let [result (paren-mode/format-text current-text options)] 117 | (:text result)) 118 | nil)] 119 | 120 | (dispatch [:console-set-text console-key (common/source-without-prompt new-text)]) 121 | (.setValue @cm (str ((:get-prompt @eval-opts)) (common/source-without-prompt new-text))) 122 | 123 | ;; restore the selection, cursor, and scroll 124 | ;; since these are reset when overwriting codemirror's value. 125 | (if selection? 126 | (.setSelections @cm selections) 127 | (.setCursor @cm cursor)) 128 | (.scrollTo @cm scroll-x scroll-y))))) 129 | 130 | ;; NOTE: 131 | ;; Text is either updated after a change in text or 132 | ;; a cursor movement, but not both. 133 | ;; When typing, on-change is called, then on-cursor-activity. 134 | ;; So we prevent updating the text twice by using an update flag. 135 | 136 | (defn before-change 137 | "Called before any change is applied to the editor." 138 | [cm change] 139 | ;; keep CodeMirror from reacting to a change from "setValue" 140 | ;; if it is not a new value. 141 | (when (and (= "setValue" (.-origin change)) 142 | (= (.getValue cm) (join "\n" (.-text change)))) 143 | (.cancel change))) 144 | 145 | (defn on-change 146 | "Called after any change is applied to the editor." 147 | [console-key] 148 | (let [fix-text! (fix-text-fn console-key) 149 | update-cursor! (update-cursor-fn console-key) 150 | mode (subscribe [:get-console-mode console-key]) 151 | on-before-change (subscribe [:get-console-on-before-change console-key]) 152 | on-after-change (subscribe [:get-console-on-after-change console-key])] 153 | (fn [inst change] 154 | (when-not (= :none @mode) 155 | (when (not= "setValue" (.-origin change)) 156 | (when @on-before-change 157 | (@on-before-change inst change)) 158 | (fix-text! :change change) 159 | (update-cursor! change) 160 | (dispatch-sync [:set-console-frame-updated console-key true]) 161 | (when @on-after-change 162 | (@on-after-change inst change))))))) 163 | 164 | (defn on-cursor-activity 165 | "Called after the cursor moves in the editor." 166 | [console-key] 167 | (let [frame-updated? (subscribe [:get-console-frame-updated console-key]) 168 | fix-text! (fix-text-fn console-key) 169 | mode (subscribe [:get-console-mode console-key]) 170 | on-before-change (subscribe [:get-console-on-before-change console-key]) 171 | on-after-change (subscribe [:get-console-on-after-change console-key])] 172 | (fn [inst evt] 173 | (when-not (= :none @mode) 174 | (when-not @frame-updated? 175 | (when @on-before-change 176 | (@on-before-change inst evt)) 177 | (fix-text!) 178 | (when @on-after-change 179 | (@on-after-change inst evt))) 180 | (dispatch-sync [:set-console-frame-updated console-key false]))))) 181 | 182 | (defn parinferize! 183 | "Add parinfer goodness to a codemirror editor" 184 | [cm console-key] 185 | (.on cm "change" (on-change console-key)) 186 | (.on cm "beforeChange" before-change)) 187 | -------------------------------------------------------------------------------- /src/re_console/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns re-console.subs 2 | (:require 3 | [re-frame.core :refer [register-sub]] 4 | [clairvoyant.core :refer-macros [trace-forms]] 5 | [re-frame-tracer.core :refer [tracer]] 6 | [re-console.app :as app]) 7 | (:require-macros 8 | [reagent.ratom :refer [reaction]])) 9 | 10 | ;; (trace-forms {:tracer (tracer :color "brown")} 11 | 12 | (register-sub 13 | :console-created? 14 | (fn [db [_ console-key]] 15 | (reaction (app/console-created? @db console-key)))) 16 | 17 | (register-sub 18 | :get-console-items 19 | (fn [db [_ console-key]] 20 | (reaction (app/console-items @db console-key)))) 21 | 22 | (register-sub 23 | :get-console-mode 24 | (fn [db [_ console-key]] 25 | (reaction (app/console-mode @db console-key)))) 26 | 27 | (register-sub 28 | :get-console-frame-updated 29 | (fn [db [_ console-key]] 30 | (reaction (app/console-frame-updated @db console-key)))) 31 | 32 | (register-sub 33 | :get-console-current-text 34 | (fn [db [_ console-key]] 35 | (let [idx (reaction (get-in @db [:consoles (name console-key) :hist-pos])) 36 | history (reaction (get-in @db [:consoles (name console-key) :history]))] 37 | (reaction (let [items @history 38 | pos (- (count items) @idx 1)] 39 | (get items pos)))))) 40 | 41 | (register-sub 42 | :get-console 43 | (fn [db [_ console-key]] 44 | (reaction (app/console @db console-key)))) 45 | 46 | (register-sub 47 | :queued-forms-count 48 | (fn [db [_ console-key]] 49 | (reaction (count (app/queued-forms @db console-key))))) 50 | 51 | (register-sub 52 | :queued-forms-empty? 53 | (fn [db [_ console-key]] 54 | (reaction (not (empty? (app/queued-forms @db console-key)))))) 55 | 56 | (register-sub 57 | :get-console-eval-opts 58 | (fn [db [_ console-key]] 59 | (reaction (app/console-eval-opts @db console-key)))) 60 | 61 | (register-sub 62 | :get-console-on-before-change 63 | (fn [db [_ console-key]] 64 | (reaction (app/console-on-before-change @db console-key)))) 65 | 66 | (register-sub 67 | :get-console-on-after-change 68 | (fn [db [_ console-key]] 69 | (reaction (app/console-on-after-change @db console-key)))) 70 | 71 | (register-sub 72 | :get-console-full-text 73 | (fn [db [_ console-key]] 74 | (reaction (app/console-full-text @db console-key)))) 75 | 76 | ;; ) 77 | -------------------------------------------------------------------------------- /src/re_console/utils.cljs: -------------------------------------------------------------------------------- 1 | (ns re-console.utils 2 | (:require [reagent.core :as reagent])) 3 | 4 | (defn colored-text [text style] 5 | (reagent/create-class 6 | {:component-did-mount 7 | (fn [this] 8 | (let [node (reagent/dom-node this)] 9 | ((aget js/CodeMirror "colorize") #js[node] "clojure"))) 10 | :reagent-render 11 | (fn [_] 12 | [:pre {:style (merge {:padding 0 :margin 0} style)} 13 | text])})) 14 | -------------------------------------------------------------------------------- /test/re_console/app_test.cljs: -------------------------------------------------------------------------------- 1 | (ns re-console.app-test 2 | (:require [re-console.app :as app] 3 | [cljs.test :refer-macros [deftest is async]] 4 | [re-console.replumb-proxy :as replumb-proxy])) 5 | 6 | ;; Or doo will exit with an error, see: 7 | ;; https://github.com/bensu/doo/issues/83#issuecomment-165498172 8 | (set! (.-error js/console) (fn [x] (.log js/console x))) 9 | 10 | (def db (atom {:consoles {}})) 11 | 12 | (def console-key :cljs-console) 13 | 14 | (def eval-opts (replumb-proxy/eval-opts false ["/js/compiled/out"])) 15 | 16 | (defn submit 17 | [db k source] 18 | (let [evaluate (:evaluate eval-opts)] 19 | (evaluate (partial app/on-eval-complete db k) 20 | source))) 21 | 22 | (deftest initial-configuration 23 | (let [created? (app/console-created? @db console-key)] 24 | (is (= false created?) "The console should be nil")) 25 | 26 | (let [db (-> @db 27 | (app/init-console console-key eval-opts) 28 | (app/add-console-instance console-key {:placeholder "js-object"})) 29 | created? (app/console-created? db console-key) 30 | items (app/console-items db console-key) 31 | queued-forms (app/queued-forms db console-key) 32 | history (app/console-history db console-key) 33 | hist-pos (app/console-history-pos db console-key)] 34 | (is (= true created?) "The console should be created") 35 | (is (empty? items) "The items should be empty") 36 | (is (empty? queued-forms) "The queued forms should be empty") 37 | (is (= (-> history count) 1) "The history should contain an empty string") 38 | (is (= (-> history first) "") "The first and only item should be an empty string") 39 | (is (= 0 hist-pos) "The history position should be 0"))) 40 | 41 | (deftest evaluate 42 | (let [db (-> @db 43 | (app/init-console console-key eval-opts) 44 | (app/add-console-instance console-key {:placeholder "js-object"}) 45 | (submit console-key "123") 46 | (submit console-key "(inc 10)")) 47 | items (app/console-items db console-key) 48 | history (app/console-history db console-key) 49 | hist-pos (app/console-history-pos db console-key)] 50 | (is (= 4 (-> items count)) "The items should contain 4 elements (2 input, 2 output)") 51 | (is (= 3 (-> history count)) "The history should contain 3 items (2 previous, 1 current)") 52 | (is (= (-> items first :type) :input) "The first item should be of type :input") 53 | (is (= (-> items second :type) :output) "The second item should be of type :output"))) 54 | -------------------------------------------------------------------------------- /test/re_console/example_test.cljs: -------------------------------------------------------------------------------- 1 | (ns re-console.example-test 2 | (:require [cljs.test :refer-macros [deftest testing is]])) 3 | 4 | (deftest silly-test 5 | (is (= (+ 1 1) 6 | 2))) 7 | -------------------------------------------------------------------------------- /version.properties: -------------------------------------------------------------------------------- 1 | VERSION=0.1.4-SNAPSHOT 2 | --------------------------------------------------------------------------------