├── .gitignore ├── keymaps └── clojure-party-repl.cson ├── lib └── clojure-party-repl.js ├── license ├── menus └── clojure-party-repl.cson ├── package.json ├── project.clj ├── readme.md ├── resources └── unrepl │ └── blob.clj ├── screenshot.png ├── shadow-cljs.edn ├── src └── clojure_party_repl │ ├── bencode.cljs │ ├── common.cljs │ ├── connection_panel.cljs │ ├── core.cljs │ ├── execution.cljs │ ├── guest.cljs │ ├── host.cljs │ ├── local_repl.cljs │ ├── nrepl.cljs │ ├── remote_repl.cljs │ ├── repl.cljs │ ├── strings.cljs │ └── unrepl.cljs └── styles └── clojure-party-repl.less /.gitignore: -------------------------------------------------------------------------------- 1 | /resources/public/js/compiled/** 2 | .DS_Store 3 | pom.xml 4 | *.jar 5 | /lib/cljs-runtime 6 | classes/ 7 | out/ 8 | /target/ 9 | .lein-deps-sum 10 | .lein-repl-history 11 | .lein-plugins/ 12 | .shadow-cljs/ 13 | .repl 14 | .nrepl-* 15 | node_modules 16 | /package-lock.json 17 | -------------------------------------------------------------------------------- /keymaps/clojure-party-repl.cson: -------------------------------------------------------------------------------- 1 | # macOS keybindings: 2 | '.platform-darwin atom-workspace atom-text-editor:not([mini])': 3 | 'ctrl-alt-l': 'clojure-party-repl:startLocalRepl' 4 | 'ctrl-alt-r': 'clojure-party-repl:connectToRemoteRepl' 5 | 'cmd-enter': 'clojure-party-repl:sendToRepl' 6 | 7 | '.platform-darwin atom-workspace atom-text-editor:not([mini]).repl-entry': 8 | 'cmd-up': 'clojure-party-repl:showOlderHistory' 9 | 'cmd-down': 'clojure-party-repl:showNewerHistory' 10 | 11 | 12 | # Linux keybindings: 13 | '.platform-linux atom-workspace atom-text-editor:not([mini])': 14 | 'ctrl-alt-l': 'clojure-party-repl:startLocalRepl' 15 | 'ctrl-alt-r': 'clojure-party-repl:connectToRemoteRepl' 16 | 'ctrl-enter': 'clojure-party-repl:sendToRepl' 17 | 18 | '.platform-linux atom-workspace atom-text-editor:not([mini]).repl-entry': 19 | 'ctrl-up': 'clojure-party-repl:showOlderHistory' 20 | 'ctrl-down': 'clojure-party-repl:showNewerHistory' 21 | -------------------------------------------------------------------------------- /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' from 19 | a Contributor if it was added to the Program by such Contributor itself or 20 | anyone acting on such Contributor's behalf. Contributions do not include 21 | additions to the Program which: (i) are separate modules of software 22 | distributed in conjunction with the Program under their own license 23 | 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 Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement, 34 | including all Contributors. 35 | 36 | 2. GRANT OF RIGHTS 37 | a) Subject to the terms of this Agreement, each Contributor hereby grants 38 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 39 | reproduce, prepare derivative works of, publicly display, publicly perform, 40 | distribute and sublicense the Contribution of such Contributor, if any, and 41 | such derivative works, in source code and object code form. 42 | b) Subject to the terms of this Agreement, each Contributor hereby grants 43 | Recipient a non-exclusive, worldwide, royalty-free patent license under 44 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 45 | transfer the Contribution of such Contributor, if any, in source code and 46 | object code form. This patent license shall apply to the combination of the 47 | Contribution and the Program if, at the time the Contribution is added by 48 | the Contributor, such addition of the Contribution causes such combination 49 | to be covered by the Licensed Patents. The patent license shall not apply 50 | to any other combinations which include the Contribution. No hardware per 51 | se is licensed hereunder. 52 | c) Recipient understands that although each Contributor grants the licenses to 53 | its Contributions set forth herein, no assurances are provided by any 54 | Contributor that the Program does not infringe the patent or other 55 | intellectual property rights of any other entity. Each Contributor 56 | disclaims any liability to Recipient for claims brought by any other entity 57 | based on infringement of intellectual property rights or otherwise. As a 58 | condition to exercising the rights and licenses granted hereunder, each 59 | Recipient hereby assumes sole responsibility to secure any other 60 | intellectual property rights needed, if any. For example, if a third party 61 | patent license is required to allow Recipient to distribute the Program, it 62 | is Recipient's responsibility to acquire that license before distributing 63 | the Program. 64 | d) Each Contributor represents that to its knowledge it has sufficient 65 | copyright rights in its Contribution, if any, to grant the copyright 66 | license set forth in this Agreement. 67 | 68 | 3. REQUIREMENTS 69 | 70 | A Contributor may choose to distribute the Program in object code form under its 71 | own license agreement, provided that: 72 | 73 | a) it complies with the terms and conditions of this Agreement; and 74 | b) its license agreement: 75 | i) effectively disclaims on behalf of all Contributors all warranties and 76 | conditions, express and implied, including warranties or conditions of 77 | title and non-infringement, and implied warranties or conditions of 78 | merchantability and fitness for a particular purpose; 79 | ii) effectively excludes on behalf of all Contributors all liability for 80 | damages, including direct, indirect, special, incidental and 81 | consequential damages, such as lost profits; 82 | iii) states that any provisions which differ from this Agreement are offered 83 | by that Contributor alone and not by any other party; and 84 | iv) states that source code for the Program is available from such 85 | Contributor, and informs licensees how to obtain it in a reasonable 86 | manner on or through a medium customarily used for software exchange. 87 | 88 | When the Program is made available in source code form: 89 | 90 | a) it must be made available under this Agreement; and 91 | b) a copy of this Agreement must be included with each copy of the Program. 92 | Contributors may not remove or alter any copyright notices contained within 93 | the Program. 94 | 95 | Each Contributor must identify itself as the originator of its Contribution, if 96 | any, in a manner that reasonably allows subsequent Recipients to identify the 97 | originator of the Contribution. 98 | 99 | 4. COMMERCIAL DISTRIBUTION 100 | 101 | Commercial distributors of software may accept certain responsibilities with 102 | respect to end users, business partners and the like. While this license is 103 | intended to facilitate the commercial use of the Program, the Contributor who 104 | includes the Program in a commercial product offering should do so in a manner 105 | which does not create potential liability for other Contributors. Therefore, if 106 | a Contributor includes the Program in a commercial product offering, such 107 | Contributor ("Commercial Contributor") hereby agrees to defend and indemnify 108 | every other Contributor ("Indemnified Contributor") against any losses, damages 109 | and costs (collectively "Losses") arising from claims, lawsuits and other legal 110 | actions brought by a third party against the Indemnified Contributor to the 111 | extent caused by the acts or omissions of such Commercial Contributor in 112 | connection with its distribution of the Program in a commercial product 113 | offering. The obligations in this section do not apply to any claims or Losses 114 | relating to any actual or alleged intellectual property infringement. In order 115 | to qualify, an Indemnified Contributor must: a) promptly notify the Commercial 116 | Contributor in writing of such claim, and b) allow the Commercial Contributor to 117 | control, and cooperate with the Commercial Contributor in, the defense and any 118 | related settlement negotiations. The Indemnified Contributor may participate in 119 | any such claim at its own expense. 120 | 121 | For example, a Contributor might include the Program in a commercial product 122 | offering, Product X. That Contributor is then a Commercial Contributor. If that 123 | Commercial Contributor then makes performance claims, or offers warranties 124 | related to Product X, those performance claims and warranties are such 125 | Commercial Contributor's responsibility alone. Under this section, the 126 | Commercial Contributor would have to defend claims against the other 127 | Contributors related to those performance claims and warranties, and if a court 128 | requires any other Contributor to pay any damages as a result, the Commercial 129 | Contributor must pay those damages. 130 | 131 | 5. NO WARRANTY 132 | 133 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN 134 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 135 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, 136 | NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each 137 | Recipient is solely responsible for determining the appropriateness of using and 138 | distributing the Program and assumes all risks associated with its exercise of 139 | rights under this Agreement , including but not limited to the risks and costs 140 | of program errors, compliance with applicable laws, damage to or loss of data, 141 | programs or equipment, and unavailability or interruption of operations. 142 | 143 | 6. DISCLAIMER OF LIABILITY 144 | 145 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 146 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 147 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 148 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 149 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 150 | OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS 151 | GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 152 | 153 | 7. GENERAL 154 | 155 | If any provision of this Agreement is invalid or unenforceable under applicable 156 | law, it shall not affect the validity or enforceability of the remainder of the 157 | terms of this Agreement, and without further action by the parties hereto, such 158 | provision shall be reformed to the minimum extent necessary to make such 159 | provision valid and enforceable. 160 | 161 | If Recipient institutes patent litigation against any entity (including a 162 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 163 | (excluding combinations of the Program with other software or hardware) 164 | infringes such Recipient's patent(s), then such Recipient's rights granted under 165 | Section 2(b) shall terminate as of the date such litigation is filed. 166 | 167 | All Recipient's rights under this Agreement shall terminate if it fails to 168 | comply with any of the material terms or conditions of this Agreement and does 169 | not cure such failure in a reasonable period of time after becoming aware of 170 | such noncompliance. If all Recipient's rights under this Agreement terminate, 171 | Recipient agrees to cease use and distribution of the Program as soon as 172 | reasonably practicable. However, Recipient's obligations under this Agreement 173 | and any licenses granted by Recipient relating to the Program shall continue and 174 | survive. 175 | 176 | Everyone is permitted to copy and distribute copies of this Agreement, but in 177 | order to avoid inconsistency the Agreement is copyrighted and may only be 178 | modified in the following manner. The Agreement Steward reserves the right to 179 | publish new versions (including revisions) of this Agreement from time to time. 180 | No one other than the Agreement Steward has the right to modify this Agreement. 181 | The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation 182 | may assign the responsibility to serve as the Agreement Steward to a suitable 183 | separate entity. Each new version of the Agreement will be given a 184 | distinguishing version number. The Program (including Contributions) may always 185 | be distributed subject to the version of the Agreement under which it was 186 | received. In addition, after a new version of the Agreement is published, 187 | Contributor may elect to distribute the Program (including its Contributions) 188 | under the new version. Except as expressly stated in Sections 2(a) and 2(b) 189 | above, Recipient receives no rights or licenses to the intellectual property of 190 | any Contributor under this Agreement, whether expressly, by implication, 191 | estoppel or otherwise. All rights in the Program not expressly granted under 192 | this Agreement are reserved. 193 | 194 | This Agreement is governed by the laws of the State of New York and the 195 | intellectual property laws of the United States of America. No party to this 196 | Agreement will bring a legal action under this Agreement more than one year 197 | after the cause of action arose. Each party waives its rights to a jury trial in 198 | any resulting litigation. 199 | -------------------------------------------------------------------------------- /menus/clojure-party-repl.cson: -------------------------------------------------------------------------------- 1 | 'context-menu': 2 | 'atom-text-editor': [ 3 | { 4 | 'label': 'Execute Code with Party REPL' 5 | 'command': 'clojure-party-repl:sendToRepl' 6 | } 7 | ] 8 | 'menu': [ 9 | { 10 | 'label': 'Packages' 11 | 'submenu': [ 12 | 'label': 'Clojure Party Repl' 13 | 'submenu': [ 14 | { 15 | 'label': 'Start Local Lein REPL' 16 | 'command': 'clojure-party-repl:startLocalRepl' 17 | }, 18 | { 19 | 'label': 'Connect to Remote REPL Server' 20 | 'command': 'clojure-party-repl:connectToRemoteRepl' 21 | }, 22 | { 23 | 'label': 'Execute Code' 24 | 'command': 'clojure-party-repl:sendToRepl' 25 | } 26 | ] 27 | ] 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clojure-party-repl", 3 | "main": "./lib/clojure-party-repl", 4 | "version": "1.1.0", 5 | "description": "Clojure REPL for Atom with Teletype support", 6 | "keywords": [], 7 | "repository": "https://github.com/party-repl/clojure-party-repl", 8 | "license": "Eclipse Public License", 9 | "engines": { 10 | "atom": ">=1.0.0 <2.0.0" 11 | }, 12 | "dependencies": { 13 | "bencode": "~2.0.0" 14 | }, 15 | "consumedServices": { 16 | "autosave": { 17 | "versions": { 18 | "1.0.0": "consumeAutosave" 19 | } 20 | } 21 | }, 22 | "devDependencies": { 23 | "shadow-cljs": "^2.2.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject clojure-party-repl "1.0.0" 2 | 3 | :license {:name "Eclipse Public License" 4 | :url "http://www.eclipse.org/legal/epl-v10.html"} 5 | 6 | :dependencies [[org.clojure/clojure "1.9.0"] 7 | [org.clojure/clojurescript "1.9.946"] 8 | [org.clojure/core.async "0.4.474"] 9 | [binaryage/oops "0.6.1"]] 10 | 11 | 12 | :source-paths ["src"] 13 | :profiles {:dev {:dependencies [[thheller/shadow-cljs "2.2.3"]]}}) 14 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Clojure Party REPL 2 | 3 | Clojure REPL for Atom written in ClojureScript with full Teletype support for pair programming. Let's party! 😄 4 | 5 | ![Screenshot](screenshot.png) 6 | 7 | 8 | ## How to use this 9 | Open or add your project folder inside Atom, and follow one of the methods below: 10 | 11 | Start a local nREPL server with Leiningen: 12 | 1. Open a file in the project folder, and focus on the file so it has a blinking cursor. 13 | 14 | 2. Go to Atom's menu bar and select Packages -> Clojure Party Repl -> Start Local REPL 15 | 16 | 3. Make sure `lein` command is available and there's `project.clj` in the root directory. 17 | 18 | Connect to a remote nREPL server: 19 | 1. Have a running nREPL ready to be connected. 20 | 21 | 2. Go to Atom's menu bar and select Packages -> Clojure Party Repl -> Connect to Remote REPL server 22 | 23 | 3. Fill out the pop out panel and hit an Enter key to connect. 24 | 25 | Connect to a remote [Unrepl](https://github.com/Unrepl/unrepl) server: 26 | 1. Have a running Clojure Socket REPL ready to be connected. Socket REPL can be launched in a few different ways: 27 | - Use Leiningen project by adding `:jvm-opts ["-Dclojure.server.repl={:port 9999 :accept clojure.core.server/repl}"]` to your `project.clj` and start your repl normally. 28 | - Use Clojure jar in the terminal by typing in `java -Dclojure.server.myrepl="{:port 9999,:accept,clojure.core.server/repl}" -jar ~/.m2/repository/org/clojure/clojure/1.8.0/clojure-1.8.0.jar`. 29 | 30 | 31 | 2. Go to Atom's menu bar and select Packages -> Clojure Party Repl -> Connect to Remote REPL server 32 | 33 | 3. Fill out the pop out panel and connect. It will upgrade your Socket REPL to unrepl! 34 | 35 | Once it's connected to the REPL, there should be two new tabs called `Clojure Party REPL History` and `Clojure Party REPL Entry`. The History tab shows all the output from the REPL, and the Entry tab is where you can type in code to send over to the REPL. 36 | 37 | When you're done with the REPL, simply close the tab and the REPL will be disconnected. 38 | 39 | 40 | Execute code in REPL using the following methods: 41 | - Type in code inside the `Clojure Party REPL Entry` tab and hit Command-Enter keys. 42 | - Move a cursor in any part of the code in a file inside a project and hit Command-Enter keys. 43 | - Select code in a file that's inside a project and hit Command-Enter keys. 44 | - Send code from an unsaved/untitled file and hit Command-Enter keys. 45 | 46 | If Command-Enter keys aren't working, other packages' keybindings could be conflicting with Party Repl's. Check if your keybindings for Atom is working right. Go to Atom -> Keymap... to open `keymap.cson` and follow the instructions. 47 | 48 | 49 | ## Pair programming with shared REPLs 50 | Install teletype and share both `Clojure Party REPL History` and `Clojure Party REPL Entry` tabs. That's it, and now everybody shares the state of the running project! 51 | 52 | 53 | ## How it works 54 | TODO 55 | 56 | 57 | ## Development 58 | Wanna help polish this turd? Sweet! This package is built using shadow-cljs to make development easier supporting both: 59 | 60 | - Hot code reloading. As you change the plugin's code, it will be dynamically reloaded so there's no need to refresh Atom. 61 | - A full ClojureScript REPL into Atom, so you can REPL into the package and even inspect objects inside Atom. 62 | 63 | 64 | ### Install Shadow CJLS 65 | First, install the Shadow CLJS node package somewhere on your $PATH: 66 | 67 | ``` 68 | npm install shadow-cljs 69 | ``` 70 | 71 | 72 | ### Compiling our CLJS with auto code-reloading 73 | Compile using one of the methods below: 74 | 75 | To compile with an auto code-reloading loop with REPL, use: 76 | 77 | ``` 78 | shadow-cljs watch app 79 | ``` 80 | 81 | To compile without a code-reloading loop, use: 82 | 83 | ``` 84 | shadow-cljs compile app 85 | ``` 86 | 87 | To compile for release with `:simple` optimizations, use: 88 | 89 | ``` 90 | shadow-cljs release app 91 | ``` 92 | 93 | The first time you run shadow-cljs it will ask you to install stuff into the 94 | project directory. This is ok, it's all blocked by `.gitignore`. 95 | 96 | After running this you should see compiled JS dropped into the standard `lib` folder. 97 | 98 | When using the shadow-cljs' `watch` mode, most changes will be automatically 99 | reloaded such as changes to ClojureScript functions and vars, however changes to menus and 100 | things on Atom's side will still require a refresh with View -> Developer -> Reload Window. 101 | 102 | 103 | ### Linking the plugin to Atom 104 | After you compiled the plugin, run: 105 | 106 | ``` 107 | apm install 108 | apm link 109 | ``` 110 | 111 | Next, restart Atom so it will notice the newly linked package. We just created a symlink 112 | from `~/.atom/packages/clojure-party-repl` to your project path. 113 | 114 | After restarting Atom, you should see the clojure-party-repl plugin installed and be able to use it. 115 | 116 | 117 | ### REPLing into the running project 118 | Now that the package is installed, check the `shadow-cljs watch app` output and look for a message saying 119 | `"JS runtime connected"`. This means it made contact with our package running in Atom. Now we can 120 | REPL in. This can be finicky, if you don't see the message, try reloading Atom again. 121 | 122 | Next, while shadow-cljs is running, in another terminal run: 123 | 124 | ``` 125 | $ shadow-cljs cljs-repl app 126 | ``` 127 | 128 | This will connect you directly into the plugin so you can live develop it. You can exit the REPL by typing `:repl/quit` 129 | 130 | Once we add CLJS support, you'll be able to REPL into the package using itself. 🐵 131 | 132 | 133 | ### Testing and hacking on the clojure-party-repl 134 | Now that we've got dynamic code-reloading and a ClojureScript REPL into Atom, 135 | let's take this thing for a spin. 136 | 137 | The easiest way to work on the plugin is to open a new Clojure project 138 | in Atom in Dev Mode. If you don't have a dummy project around `lein new app test-project` 139 | will make one. Open the new project by going to View -> Developer -> Open In Dev Mode, 140 | and then open the development console View -> Developer -> Toggle Developer Tools to 141 | show any possible errors preventing the plugin from starting. 142 | 143 | 144 | ### Adding new functionality to clojure-party-repl 145 | The best place to start hacking is `core.clj`. This is where all 146 | the public functions that do interesting stuff are defined and exported. 147 | 148 | If you want to add something cool, follow the pattern in `core.clj` and 149 | add your function to `shadow-cljs.edn` too. 150 | 151 | Exported functions can be linked to keybindings and menu items, checkout the standard 152 | `keymaps` and `menus` directories. 153 | 154 | 155 | ## Roadmap 156 | 157 | ### Core features 158 | - [x] Write new nREPL client in ClojureScript 159 | - [x] Support for Leiningen 160 | - [ ] Support for Boot 161 | - [x] Support for remote nREPLs 162 | - [ ] Support for Socket REPLs (clojure.core style REPLs) 163 | - [x] unRepl support 164 | - [ ] Add line number and file metadata to stacktraces. 165 | - [ ] ClojureScript support 166 | 167 | 168 | ### Pair programming / Teletype support 169 | - [ ] Use Teletype to send messages between the host and guest. 170 | - [ ] Sync the REPL history between the host and the guest. 171 | - [ ] Find a better way to link the REPL editors between the host and guest. Right now they are just linked by the title like “REPL Entry”, but this limits us to just 1 REPL over Teletype at a time. 172 | - [ ] Find a way to gracefully open both the input-editor and output-editor (history) editors at once on the guest’s side when the host clicks in either one. For example trick Teletype to sending both editors over to the guest at the same time. 173 | 174 | 175 | ### Using multiple REPLs simultaneously support 176 | - [x] When executing code from a file, find which project the file is in, and run the code in the REPL that corresponds to that project. Otherwise, fall back to the last used REPL. 177 | - [x] Add the project name to the title of the REPL editors 178 | - [x] When an user sends code from an untitled/unsaved file, it should be sent to the current REPL. 179 | 180 | 181 | ### User interface improvements 182 | - [ ] Integrate with Atom IDE 183 | - [x] When executing code, temporarily highlight the top-level form or the selected code being sent to the REPL. Likewise, somehow indicate when there's a syntax error preventing a top-level form from being found. 184 | - [ ] Create a new kind of Pane which holds both the REPL input-editor and output-editor. 185 | - [ ] Add buttons to REPL Pane like "Execute" and "Clear history". 186 | - [ ] Make the "Execute" button dynamically switch to "Cancel" when a long running command is executed (this should work seamlessly on the guest side too). 187 | - [ ] Make the output-editor (history) read-only. 188 | 189 | 190 | ### OS support 191 | - [x] macOS support 192 | - [x] Linux support 193 | - [ ] Windows support, probably not going to do it. PRs welcome. 194 | 195 | 196 | ### Polish 197 | - [x] Add support for multiple Atom projects. When starting a REPL either start it in the project for the current file, or prompt the user and ask which file. 198 | - [x] Add support for multiple remote REPLs. When starting a REPL prompt the user and ask which project. 199 | - [ ] Update our namespace detection regex to allow for all valid characters in a namespace: include $=<>_ 200 | - [ ] Only allow one command to execute at a time. If we execute code in the REPL, either from a file or directly from the input-editor, it should show a spinner in the REPL UI and ignore the command. 201 | - [ ] Compile out a release version with advanced optimizations and all of the REPL/dynamic reloading shadow-cljs features turned off. 202 | 203 | 204 | ### Errors 205 | - [ ] Fix error “Pane has been destroyed at Pane.activate” when REPL TextEditor is the only item in a Pane. 206 | - [ ] Fix error “TypeError: Patch does not apply at DefaultHistoryProvider.module.exports.DefaultHistoryProvider.getSnapshot <- TextBuffer.module.exports.TextBuffer.getHistory” 207 | 208 | 209 | ### Possible future ideas 210 | - [ ] Stop multiplexing stdout/stderr and REPL results through the output-editor (history). Add a 3rd collapsable editor which 211 | is dedicated to stdout/stderr. Additionally, add an option to 212 | hook into all stdout/err streams on a remote socket REPL, so 213 | all output can be seen even from other threads. 214 | - [ ] The ability to add breakpoints or wrap seamless print statements around blocks of code. These would be a first-class UI feature, imagine a purple box wrapped around code that would print it out, every time 215 | - [ ] Create a custom Atom grammar for the REPL history editor, ideally this would draw horizontal lines between each REPL entry. 216 | 217 | 218 | ## Contributors (alphabetical) 219 | - [Aaron Brooks](https://github.com/abrooks) 220 | - [Chris Houser](https://github.com/Chouser) 221 | - [Hans Livingstone](https://hanslivingstone.com) 222 | - Tomomi Livingstone 223 | 224 | 225 | ## License 226 | Copyright 2018 Tomomi Livingstone. 227 | 228 | Distributed under the Eclipse Public License, the same as Clojure. 229 | 230 | Balloon icon made by [Freepik](https://www.freepik.com) from [Flaticon](https://www.flaticon.com), licensed under [CC 3.0 BY](https://creativecommons.org/licenses/by/3.0). 231 | -------------------------------------------------------------------------------- /resources/unrepl/blob.clj: -------------------------------------------------------------------------------- 1 | (clojure.core/let [prefix__641__auto__ (clojure.core/name (clojure.core/gensym)) code__642__auto__ (.replaceAll "(ns unrepl.print\n (:require [clojure.string :as str]\n [clojure.edn :as edn]\n [clojure.main :as main]))\n\n(defprotocol MachinePrintable\n (-print-on [x write rem-depth]))\n\n(defn print-on [write x rem-depth]\n (let [rem-depth (dec rem-depth)]\n (if (and (neg? rem-depth) (or (nil? *print-length*) (pos? *print-length*)))\n (binding [*print-length* 0]\n (print-on write x 0))\n (do\n (when (and *print-meta* (meta x))\n (write \"#unrepl/meta [\")\n (-print-on (meta x) write rem-depth)\n (write \" \"))\n (-print-on x write rem-depth)\n (when (and *print-meta* (meta x))\n (write \"]\"))))))\n\n(defn base64-encode [^java.io.InputStream in]\n (let [table \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\"\n sb (StringBuilder.)]\n (loop [shift 4 buf 0]\n (let [got (.read in)]\n (if (neg? got)\n (do\n (when-not (= shift 4)\n (let [n (bit-and (bit-shift-right buf 6) 63)]\n (.append sb (.charAt table n))))\n (cond\n (= shift 2) (.append sb \"==\")\n (= shift 0) (.append sb \\=))\n (str sb))\n (let [buf (bit-or buf (bit-shift-left got shift))\n n (bit-and (bit-shift-right buf 6) 63)]\n (.append sb (.charAt table n))\n (let [shift (- shift 2)]\n (if (neg? shift)\n (do\n (.append sb (.charAt table (bit-and buf 63)))\n (recur 4 0))\n (recur shift (bit-shift-left buf 6))))))))))\n\n(defn base64-decode [^String s]\n (let [table \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\"\n in (java.io.StringReader. s)\n bos (java.io.ByteArrayOutputStream.)]\n (loop [bits 0 buf 0]\n (let [got (.read in)]\n (when-not (or (neg? got) (= 61 #_\\= got))\n (let [buf (bit-or (.indexOf table got) (bit-shift-left buf 6))\n bits (+ bits 6)]\n (if (<= 8 bits)\n (let [bits (- bits 8)]\n (.write bos (bit-shift-right buf bits))\n (recur bits (bit-and 63 buf)))\n (recur bits buf))))))\n (.toByteArray bos)))\n\n(def ^:dynamic *elide*\n \"Function of 1 argument which returns the elision.\"\n (constantly nil))\n\n(def ^:dynamic *string-length* 80)\n\n(def ^:dynamic *realize-on-print*\n \"Set to false to avoid realizing lazy sequences.\"\n true)\n\n(defmacro ^:private blame-seq [& body]\n `(try (seq ~@body)\n (catch Throwable t#\n (list (tagged-literal 'unrepl/lazy-error t#)))))\n\n(defn- may-print? [s]\n (or *realize-on-print* (not (instance? clojure.lang.IPending s)) (realized? s)))\n\n(declare ->ElidedKVs)\n\n(defn- print-kvs\n [write kvs rem-depth]\n (let [print-length *print-length*]\n (loop [kvs kvs i 0]\n (if (< i print-length)\n (when-some [[[k v] & kvs] (seq kvs)]\n (when (pos? i) (write \", \"))\n (print-on write k rem-depth)\n (write \" \")\n (print-on write v rem-depth)\n (recur kvs (inc i)))\n (when (seq kvs)\n (when (pos? i) (write \", \"))\n (write \"#unrepl/... nil \")\n (print-on write (tagged-literal 'unrepl/... (*elide* (->ElidedKVs kvs))) rem-depth))))))\n\n(defn- print-vs\n [write vs rem-depth]\n (let [print-length *print-length*]\n (loop [vs vs i 0]\n (if (and (< i print-length) (may-print? vs))\n (when-some [[v :as vs] (blame-seq vs)]\n (when (pos? i) (write \" \"))\n (if (and (tagged-literal? v) (= (:tag v) 'unrepl/lazy-error))\n (print-on write v rem-depth)\n (do\n (print-on write v rem-depth)\n (recur (rest vs) (inc i)))))\n (do\n (when (pos? i) (write \" \"))\n (print-on write (tagged-literal 'unrepl/... (*elide* vs)) rem-depth))))))\n\n(defrecord ElidedKVs [s]\n MachinePrintable\n (-print-on [_ write rem-depth]\n (write \"{\")\n (print-kvs write s rem-depth)\n (write \"}\")))\n\n(def atomic? (some-fn nil? true? false? char? string? symbol? keyword? #(and (number? %) (not (ratio? %)))))\n\n(defn- as-str\n \"Like pr-str but escapes all ASCII control chars.\"\n [x]\n ;hacky\n (cond\n (string? x) (str/replace (pr-str x) #\"\\p{Cntrl}\"\n #(format \"\\\\u%04x\" (int (.charAt ^String % 0))))\n (char? x) (str/replace (pr-str x) #\"\\p{Cntrl}\"\n #(format \"u%04x\" (int (.charAt ^String % 0))))\n :else (pr-str x)))\n\n(defmacro ^:private latent-fn [& fn-body]\n `(let [d# (delay (binding [*ns* (find-ns '~(ns-name *ns*))] (eval '(fn ~@fn-body))))]\n (fn\n ([] (@d#))\n ([x#] (@d# x#))\n ([x# & xs#] (apply @d# x# xs#)))))\n\n(defrecord MimeContent [mk-in]\n MachinePrintable\n (-print-on [_ write rem-depth]\n (with-open [in (mk-in)]\n (write \"#unrepl/base64 \\\"\")\n (write (base64-encode in))\n (write \"\\\"\"))))\n\n(defn- mime-content [mk-in]\n (when-some [e (*elide* (MimeContent. mk-in))]\n {:content (tagged-literal 'unrepl/... e)}))\n\n(def ^:dynamic *object-representations*\n \"map of classes to functions returning their representation component (3rd item in #unrepl/object [class id rep])\"\n {clojure.lang.IDeref\n (fn [x]\n (let [pending? (and (instance? clojure.lang.IPending x) ; borrowed from https://github.com/brandonbloom/fipp/blob/8df75707e355c1a8eae5511b7d73c1b782f57293/src/fipp/ednize.clj#L37-L51\n (not (.isRealized ^clojure.lang.IPending x)))\n [ex val] (when-not pending?\n (try [false @x]\n (catch Throwable e\n [true e])))\n failed? (or ex (and (instance? clojure.lang.Agent x)\n (agent-error x)))\n status (cond\n failed? :failed\n pending? :pending\n :else :ready)]\n {:unrepl.ref/status status :unrepl.ref/val val}))\n \n clojure.lang.AFn\n (fn [x]\n (-> x class .getName main/demunge))\n \n java.io.File (fn [^java.io.File f]\n (into {:path (.getPath f)}\n (when (.isFile f)\n {:attachment (tagged-literal 'unrepl/mime\n (into {:content-type \"application/octet-stream\"\n :content-length (.length f)}\n (mime-content #(java.io.FileInputStream. f))))})))\n \n java.awt.Image (latent-fn [^java.awt.Image img]\n (let [w (.getWidth img nil)\n h (.getHeight img nil)]\n (into {:width w, :height h}\n {:attachment\n (tagged-literal 'unrepl/mime\n (into {:content-type \"image/png\"}\n (mime-content #(let [bos (java.io.ByteArrayOutputStream.)]\n (when (javax.imageio.ImageIO/write\n (doto (java.awt.image.BufferedImage. w h java.awt.image.BufferedImage/TYPE_INT_ARGB)\n (-> .getGraphics (.drawImage img 0 0 nil)))\n \"png\" bos)\n (java.io.ByteArrayInputStream. (.toByteArray bos)))))))})))\n \n Object (fn [x]\n (if (-> x class .isArray)\n (seq x)\n (str x)))})\n\n(defn- object-representation [x] \n (reduce-kv (fn [_ class f]\n (when (instance? class x) (reduced (f x)))) nil *object-representations*)) ; todo : cache\n\n(defn- class-form [^Class x]\n (if (.isArray x) [(-> x .getComponentType class-form)] (symbol (.getName x))))\n\n(def unreachable (tagged-literal 'unrepl/... nil))\n\n(defn- print-tag-lit-on [write tag form rem-depth]\n (write (str \"#\" tag \" \"))\n (print-on write form rem-depth))\n\n(extend-protocol MachinePrintable\n clojure.lang.TaggedLiteral\n (-print-on [x write rem-depth]\n \n (case (:tag x)\n unrepl/... (binding ; don't elide the elision \n [*print-length* Long/MAX_VALUE\n *print-level* Long/MAX_VALUE\n *string-length* Long/MAX_VALUE]\n (write (str \"#\" (:tag x) \" \"))\n (print-on write (:form x) Long/MAX_VALUE))\n (print-tag-lit-on write (:tag x) (:form x) rem-depth)))\n\n clojure.lang.Ratio\n (-print-on [x write rem-depth]\n (print-tag-lit-on write \"unrepl/ratio\"\n [(.numerator x) (.denominator x)] rem-depth))\n \n clojure.lang.Var\n (-print-on [x write rem-depth]\n (print-tag-lit-on write \"clojure/var\"\n (when-some [ns (:ns (meta x))] ; nil when local var\n (symbol (name (ns-name ns)) (name (:name (meta x)))))\n rem-depth))\n \n Throwable \n (-print-on [t write rem-depth]\n (print-tag-lit-on write \"error\" (Throwable->map t) rem-depth))\n \n Class\n (-print-on [x write rem-depth]\n (print-tag-lit-on write \"unrepl.java/class\" (class-form x) rem-depth))\n \n java.util.Date (-print-on [x write rem-depth] (write (pr-str x)))\n java.util.Calendar (-print-on [x write rem-depth] (write (pr-str x)))\n java.sql.Timestamp (-print-on [x write rem-depth] (write (pr-str x)))\n clojure.lang.Namespace\n (-print-on [x write rem-depth]\n (print-tag-lit-on write \"unrepl/ns\" (ns-name x) rem-depth))\n java.util.regex.Pattern\n (-print-on [x write rem-depth]\n (print-tag-lit-on write \"unrepl/pattern\" (str x) rem-depth))\n String\n (-print-on [x write rem-depth]\n (if (<= (count x) *string-length*)\n (write (as-str x))\n (let [i (if (and (Character/isHighSurrogate (.charAt ^String x (dec *string-length*)))\n (Character/isLowSurrogate (.charAt ^String x *string-length*)))\n (inc *string-length*) *string-length*)\n prefix (subs x 0 i)\n rest (subs x i)]\n (if (= rest \"\")\n (write (as-str x))\n (do\n (write \"#unrepl/string [\")\n (write (as-str prefix))\n (write \" \")\n (print-on write (tagged-literal 'unrepl/... (*elide* rest)) rem-depth)\n (write \"]\")))))))\n\n(defn- print-coll [open close write x rem-depth]\n (write open)\n (print-vs write x rem-depth)\n (write close))\n\n(extend-protocol MachinePrintable\n nil\n (-print-on [_ write _] (write \"nil\"))\n Object\n (-print-on [x write rem-depth]\n (cond\n (atomic? x) (write (as-str x))\n (map? x)\n (do\n (when (record? x)\n (write \"#\") (write (.getName (class x))) (write \" \"))\n (write \"{\")\n (print-kvs write x rem-depth)\n (write \"}\"))\n (vector? x) (print-coll \"[\" \"]\" write x rem-depth)\n (seq? x) (print-coll \"(\" \")\" write x rem-depth)\n (set? x) (print-coll \"#{\" \"}\" write x rem-depth)\n :else\n (print-tag-lit-on write \"unrepl/object\"\n [(class x) (format \"0x%x\" (System/identityHashCode x)) (object-representation x)\n {:bean {unreachable (tagged-literal 'unrepl/... (*elide* (ElidedKVs. (bean x))))}}]\n rem-depth))))\n\n(defn edn-str [x]\n (let [out (java.io.StringWriter.)\n write (fn [^String s] (.write out s))]\n (binding [*print-readably* true\n *print-length* (or *print-length* 10)\n *print-level* (or *print-level* 8)\n *string-length* (or *string-length* 72)]\n (print-on write x (or *print-level* 8))\n (str out))))\n\n(defn full-edn-str [x]\n (binding [*print-length* Long/MAX_VALUE\n *print-level* Long/MAX_VALUE\n *string-length* Integer/MAX_VALUE]\n (edn-str x)))\n(ns unrepl.repl\n (:require [clojure.main :as m]\n [unrepl.print :as p]\n [clojure.edn :as edn]\n [clojure.java.io :as io]))\n\n(defn classloader\n \"Creates a classloader that obey standard delegating policy.\n Takes two arguments: a parent classloader and a function which\n takes a keyword (:resource or :class) and a string (a resource or a class name) and returns an array of bytes\n or nil.\"\n [parent f]\n (let [define-class (doto (.getDeclaredMethod ClassLoader \"defineClass\" (into-array [String (Class/forName \"[B\") Integer/TYPE Integer/TYPE]))\n (.setAccessible true))]\n (proxy [ClassLoader] [parent]\n (findResource [name]\n (when-some [bytes (f :resource name)]\n (let [file (doto (java.io.File/createTempFile \"unrepl-sideload-\" (str \"-\" (re-find #\"[^/]*$\" name)))\n .deleteOnExit)]\n (io/copy bytes file)\n (-> file .toURI .toURL))))\n (findClass [name]\n (if-some [bytes (f :class name)]\n (.invoke define-class this (to-array name bytes 0 (count bytes)))\n (throw (ClassNotFoundException. name)))))))\n\n(defn ^java.io.Writer tagging-writer\n ([write]\n (proxy [java.io.Writer] []\n (close []) ; do not cascade\n (flush []) ; atomic always flush\n (write\n ([x]\n (write (cond \n (string? x) x\n (integer? x) (str (char x))\n :else (String. ^chars x))))\n ([string-or-chars off len]\n (when (pos? len)\n (write (subs (if (string? string-or-chars) string-or-chars (String. ^chars string-or-chars))\n off (+ off len))))))))\n ([tag write]\n (tagging-writer (fn [s] (write [tag s]))))\n ([tag group-id write]\n (tagging-writer (fn [s] (write [tag s group-id])))))\n\n(defn blame-ex [phase ex]\n (if (::phase (ex-data ex))\n ex\n (ex-info (str \"Exception during \" (name phase) \" phase.\")\n {::ex ex ::phase phase} ex)))\n\n(defmacro blame [phase & body]\n `(try ~@body\n (catch Throwable t#\n (throw (blame-ex ~phase t#)))))\n\n(defn atomic-write [^java.io.Writer w]\n (fn [x]\n (let [s (blame :print (p/edn-str x))] ; was pr-str, must occur outside of the locking form to avoid deadlocks\n (locking w\n (.write w s)\n (.write w \"\\n\")\n (.flush w)))))\n\n(defn fuse-write [awrite]\n (fn [x]\n (when-some [w @awrite]\n (try\n (w x)\n (catch Throwable t\n (reset! awrite nil))))))\n\n(def ^:dynamic write)\n\n(defn unrepl-reader [^java.io.Reader r before-read]\n (let [offset (atom 0)\n offset! #(swap! offset + %)]\n (proxy [clojure.lang.LineNumberingPushbackReader clojure.lang.ILookup] [r]\n (valAt\n ([k] (get this k nil))\n ([k not-found] (case k :offset @offset not-found)))\n (read\n ([]\n (before-read)\n (let [c (proxy-super read)]\n (when-not (neg? c) (offset! 1))\n c))\n ([cbuf]\n (before-read)\n (let [n (proxy-super read cbuf)]\n (when (pos? n) (offset! n))\n n))\n ([cbuf off len]\n (before-read)\n (let [n (proxy-super read cbuf off len)]\n (when (pos? n) (offset! n))\n n)))\n (unread\n ([c-or-cbuf]\n (if (integer? c-or-cbuf)\n (when-not (neg? c-or-cbuf) (offset! -1))\n (offset! (- (alength c-or-cbuf))))\n (proxy-super unread c-or-cbuf))\n ([cbuf off len]\n (offset! (- len))\n (proxy-super unread cbuf off len)))\n (skip [n]\n (let [n (proxy-super skip n)]\n (offset! n)\n n))\n (readLine []\n (when-some [s (proxy-super readLine)]\n (offset! (count s))\n s)))))\n\n(defn- close-socket! [x]\n ; hacky way because the socket is not exposed by clojure.core.server\n (loop [x x]\n (if (= \"java.net.SocketInputStream\" (.getName (class x)))\n (do (.close x) true)\n (when-some [^java.lang.reflect.Field field \n (->> x class (iterate #(.getSuperclass %)) (take-while identity)\n (mapcat #(.getDeclaredFields %))\n (some #(when (#{\"in\" \"sd\"} (.getName ^java.lang.reflect.Field %)) %)))]\n (recur (.get (doto field (.setAccessible true)) x))))))\n\n(defn soft-store [make-action not-found]\n (let [ids-to-refs (atom {})\n refs-to-ids (atom {})\n refq (java.lang.ref.ReferenceQueue.)\n NULL (Object.)]\n (.start (Thread. (fn []\n (let [ref (.remove refq)]\n (let [id (@refs-to-ids ref)]\n (swap! refs-to-ids dissoc ref)\n (swap! ids-to-refs dissoc id)))\n (recur))))\n {:put (fn [x]\n (let [x (if (nil? x) NULL x)\n id (keyword (gensym))\n ref (java.lang.ref.SoftReference. x refq)]\n (swap! refs-to-ids assoc ref id)\n (swap! ids-to-refs assoc id ref)\n {:get (make-action id)}))\n :get (fn [id]\n (if-some [x (some-> @ids-to-refs ^java.lang.ref.Reference (get id) .get)]\n (if (= NULL x) nil x)\n not-found))}))\n\n(defonce ^:private sessions (atom {}))\n\n(defonce ^:private elision-store (soft-store #(list `fetch %) p/unreachable))\n(defn fetch [id] \n (let [x ((:get elision-store) id)]\n (cond\n (= p/unreachable x) x\n (instance? unrepl.print.ElidedKVs x) x\n (string? x) x\n (instance? unrepl.print.MimeContent x) x\n :else (seq x))))\n\n(defn session [id]\n (some-> @sessions (get id) deref))\n\n(defn interrupt! [session-id eval]\n (let [{:keys [^Thread thread eval-id promise]}\n (some-> session-id session :current-eval)]\n (when (and (= eval eval-id)\n (deliver promise\n {:ex (doto (ex-info \"Evaluation interrupted\" {::phase :eval})\n (.setStackTrace (.getStackTrace thread)))\n :bindings {}}))\n (.stop thread)\n true)))\n\n(defn background! [session-id eval]\n (let [{:keys [eval-id promise future]}\n (some-> session-id session :current-eval)]\n (boolean\n (and\n (= eval eval-id)\n (deliver promise\n {:eval future\n :bindings {}})))))\n\n(defn exit! [session-id] ; too violent\n (some-> session-id session :in close-socket!))\n\n(defn reattach-outs! [session-id]\n (some-> session-id session :write-atom \n (reset!\n (if (bound? #'write)\n write\n (let [out *out*]\n (fn [x]\n (binding [*out* out\n *print-readably* true]\n (prn x))))))))\n\n(defn attach-sideloader! [session-id]\n (prn '[:unrepl.jvm.side-loader/hello])\n (some-> session-id session :side-loader \n (reset!\n (let [out *out*\n in *in*]\n (fn self [k name]\n (binding [*out* out]\n (locking self\n (prn [k name])\n (some-> (edn/read {:eof nil} in) p/base64-decode)))))))\n (let [o (Object.)] (locking o (.wait o))))\n\n(defn set-file-line-col [session-id file line col]\n (when-some [^java.lang.reflect.Field field \n (->> clojure.lang.LineNumberingPushbackReader\n .getDeclaredFields\n (some #(when (= \"_columnNumber\" (.getName ^java.lang.reflect.Field %)) %)))]\n (doto field (.setAccessible true)) ; sigh\n (when-some [in (some-> session-id session :in)]\n (set! *file* file)\n (set! *source-path* file)\n (.setLineNumber in line)\n (.set field in (int col)))))\n\n(defn- writers-flushing-repo [max-latency-ms]\n (let [writers (java.util.WeakHashMap.)\n flush-them-all #(locking writers\n (doseq [^java.io.Writer w (.keySet writers)]\n (.flush w)))]\n (.scheduleAtFixedRate\n (java.util.concurrent.Executors/newScheduledThreadPool 1)\n flush-them-all\n max-latency-ms max-latency-ms java.util.concurrent.TimeUnit/MILLISECONDS)\n (fn [w]\n (locking writers (.put writers w nil)))))\n\n(defmacro ^:private flushing [bindings & body]\n `(binding ~bindings\n (try ~@body\n (finally ~@(for [v (take-nth 2 bindings)]\n `(.flush ~(vary-meta v assoc :tag 'java.io.Writer)))))))\n\n(defn start []\n (with-local-vars [in-eval false\n unrepl false\n eval-id 0\n prompt-vars #{#'*ns* #'*warn-on-reflection*}\n current-eval-future nil]\n (let [session-id (keyword (gensym \"session\"))\n raw-out *out*\n aw (atom (atomic-write raw-out))\n write-here (fuse-write aw)\n schedule-writer-flush! (writers-flushing-repo 50) ; 20 fps (flushes per second)\n scheduled-writer (fn [& args]\n (-> (apply tagging-writer args)\n java.io.BufferedWriter.\n (doto schedule-writer-flush!)))\n edn-out (scheduled-writer :out (fn [x] (binding [p/*string-length* Integer/MAX_VALUE] (write-here x))))\n ensure-raw-repl (fn []\n (when (and @in-eval @unrepl) ; reading from eval!\n (var-set unrepl false)\n (write [:bye {:reason :upgrade :actions {}}])\n (flush)\n ; (reset! aw (blocking-write))\n (set! *out* raw-out)))\n in (unrepl-reader *in* ensure-raw-repl)\n session-state (atom {:current-eval {}\n :in in\n :write-atom aw\n :log-eval (fn [msg]\n (when (bound? eval-id)\n (write [:log msg @eval-id])))\n :log-all (fn [msg]\n (write [:log msg nil]))\n :side-loader (atom nil)\n :prompt-vars #{#'*ns* #'*warn-on-reflection*}})\n current-eval-thread+promise (atom nil)\n ensure-unrepl (fn []\n (when-not @unrepl\n (var-set unrepl true)\n (flush)\n (set! *out* edn-out)\n (binding [*print-length* Long/MAX_VALUE\n *print-level* Long/MAX_VALUE\n p/*string-length* Long/MAX_VALUE]\n (write [:unrepl/hello {:session session-id\n :actions (into\n {:exit `(exit! ~session-id)\n :start-aux `(start-aux ~session-id)\n :log-eval\n `(some-> ~session-id session :log-eval)\n :log-all\n `(some-> ~session-id session :log-all)\n :print-limits\n `(let [bak# {:unrepl.print/string-length p/*string-length*\n :unrepl.print/coll-length *print-length*\n :unrepl.print/nesting-depth *print-level*}]\n (some->> ~(tagged-literal 'unrepl/param :unrepl.print/string-length) (set! p/*string-length*))\n (some->> ~(tagged-literal 'unrepl/param :unrepl.print/coll-length) (set! *print-length*))\n (some->> ~(tagged-literal 'unrepl/param :unrepl.print/nesting-depth) (set! *print-level*))\n bak#)\n :set-source\n `(unrepl/do\n (set-file-line-col ~session-id\n ~(tagged-literal 'unrepl/param :unrepl/sourcename)\n ~(tagged-literal 'unrepl/param :unrepl/line)\n ~(tagged-literal 'unrepl/param :unrepl/column)))\n :unrepl.jvm/start-side-loader\n `(attach-sideloader! ~session-id)}\n {})}]))))\n \n interruptible-eval\n (fn [form]\n (try\n (let [original-bindings (get-thread-bindings)\n p (promise)\n f\n (future\n (swap! session-state update :current-eval\n assoc :thread (Thread/currentThread))\n (with-bindings original-bindings\n (try\n (write [:started-eval\n {:actions \n {:interrupt (list `interrupt! session-id @eval-id)\n :background (list `background! session-id @eval-id)}}\n @eval-id])\n (let [v (with-bindings {in-eval true}\n (blame :eval (eval form)))]\n (deliver p {:eval v :bindings (get-thread-bindings)})\n v)\n (catch Throwable t\n (deliver p {:ex t :bindings (get-thread-bindings)})\n (throw t)))))]\n (swap! session-state update :current-eval\n into {:eval-id @eval-id :promise p :future f})\n (let [{:keys [ex eval bindings]} @p]\n (doseq [[var val] bindings\n :when (not (identical? val (original-bindings var)))]\n (var-set var val))\n (if ex\n (throw ex)\n eval)))\n (finally\n (swap! session-state assoc :current-eval {}))))\n cl (.getContextClassLoader (Thread/currentThread))\n slcl (classloader cl\n (fn [k x]\n (when-some [f (some-> session-state deref :side-loader deref)]\n (f k x))))]\n (swap! session-state assoc :class-loader slcl)\n (swap! sessions assoc session-id session-state)\n (binding [*out* raw-out\n *err* (tagging-writer :err write)\n *in* in\n *file* \"unrepl-session\"\n *source-path* \"unrepl-session\"\n p/*elide* (:put elision-store)\n p/*string-length* p/*string-length* \n write write-here]\n (.setContextClassLoader (Thread/currentThread) slcl)\n (with-bindings {clojure.lang.Compiler/LOADER slcl}\n (try\n (m/repl\n :prompt (fn []\n (ensure-unrepl)\n (write [:prompt (into {:file *file*\n :line (.getLineNumber *in*)\n :column (.getColumnNumber *in*)\n :offset (:offset *in*)}\n (map (fn [v]\n (let [m (meta v)]\n [(symbol (name (ns-name (:ns m))) (name (:name m))) @v])))\n (:prompt-vars @session-state))]))\n :read (fn [request-prompt request-exit]\n (blame :read (let [line+col [(.getLineNumber *in*) (.getColumnNumber *in*)]\n offset (:offset *in*)\n r (m/repl-read request-prompt request-exit)\n line+col' [(.getLineNumber *in*) (.getColumnNumber *in*)]\n offset' (:offset *in*)\n len (- offset' offset)\n id (when-not (#{request-prompt request-exit} r)\n (var-set eval-id (inc @eval-id)))]\n (write [:read {:from line+col :to line+col'\n :offset offset\n :len (- offset' offset)}\n id])\n (if (and (seq? r) (= (first r) 'unrepl/do))\n (let [id @eval-id\n write #(binding [p/*string-length* Integer/MAX_VALUE] (write %))]\n (flushing [*err* (tagging-writer :err id write)\n *out* (scheduled-writer :out id write)]\n (eval (cons 'do (next r))))\n request-prompt)\n r))))\n :eval (fn [form]\n (let [id @eval-id\n write #(binding [p/*string-length* Integer/MAX_VALUE] (write %))]\n (flushing [*err* (tagging-writer :err id write)\n *out* (scheduled-writer :out id write)]\n (interruptible-eval form))))\n :print (fn [x]\n (ensure-unrepl)\n (write [:eval x @eval-id]))\n :caught (fn [e]\n (ensure-unrepl)\n (let [{:keys [::ex ::phase]\n :or {ex e phase :repl}} (ex-data e)]\n (write [:exception {:ex ex :phase phase} @eval-id]))))\n (finally\n (.setContextClassLoader (Thread/currentThread) cl))))\n (write [:bye {:reason :disconnection\n :outs :muted\n :actions {:reattach-outs `(reattach-outs! ~session-id)}}])))))\n\n(defn start-aux [session-id]\n (let [cl (.getContextClassLoader (Thread/currentThread))]\n (try\n (some->> session-id session :class-loader (.setContextClassLoader (Thread/currentThread)))\n (start)\n (finally\n (.setContextClassLoader (Thread/currentThread) cl)))))\n\n;; WIP for extensions\n\n(defmacro ensure-ns [[fully-qualified-var-name & args :as expr]]\n `(do\n (require '~(symbol (namespace fully-qualified-var-name)))\n ~expr))\n(ns user)\n(unrepl.repl/start)" "(? code__642__auto__ java.io.StringReader. clojure.lang.LineNumberingPushbackReader.)] (try (clojure.core/binding [clojure.core/*ns* clojure.core/*ns*] (clojure.core/loop [ret__644__auto__ nil] (clojure.core/let [form__645__auto__ (clojure.core/read rdr__643__auto__ false (quote eof__646__auto__))] (if (clojure.core/= (quote eof__646__auto__) form__645__auto__) ret__644__auto__ (recur (clojure.core/eval form__645__auto__)))))) (catch java.lang.Throwable t__647__auto__ (clojure.core/println "[:unrepl.upgrade/failed]") (throw t__647__auto__)))) 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/party-repl/clojure-party-repl/6dd1028f702c149916c9d1ba66a308247fa5a53f/screenshot.png -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:dependencies [] 2 | :lein true 3 | :nrepl {:port 5555} 4 | :builds {:app {:target :node-library 5 | :exports {:config clojure-party-repl.core/config 6 | :activate clojure-party-repl.core/activate 7 | :deactivate clojure-party-repl.core/deactivate 8 | :connectToRemoteRepl clojure-party-repl.core/connect-to-remote-repl 9 | :startLocalRepl clojure-party-repl.core/start-local-repl 10 | :sendToRepl clojure-party-repl.core/send-to-repl 11 | :showNewerHistory clojure-party-repl.core/show-newer-repl-history 12 | :showOlderHistory clojure-party-repl.core/show-older-repl-history 13 | :consumeAutosave clojure-party-repl.core/consume-autosave} 14 | :output-to "lib/clojure-party-repl.js" 15 | :devtools {:before-load clojure-party-repl.core/stop 16 | :after-load clojure-party-repl.core/start}}}} 17 | -------------------------------------------------------------------------------- /src/clojure_party_repl/bencode.cljs: -------------------------------------------------------------------------------- 1 | (ns clojure-party-repl.bencode 2 | (:require [cljs.nodejs :as node] 3 | [oops.core :refer [oget oset!]] 4 | [clojure-party-repl.common :refer [console-log]])) 5 | 6 | (def bencode (node/require "bencode")) 7 | 8 | (defn reset-decode-data [] 9 | (oset! (.-decode bencode) "data" nil) 10 | (oset! (.-decode bencode) "encoding" nil) 11 | (oset! (.-decode bencode) "position" 0)) 12 | 13 | (defn ^:private decode-next 14 | "Returns a decoded data when it succeeds to decode. Returns nil when there's 15 | no more data to be decoded or when there's only partial data." 16 | [] 17 | (let [previous-position (.-position (.-decode bencode))] 18 | (try 19 | (.next (.-decode bencode)) 20 | (catch js/Error e 21 | (when (> (.-position (.-decode bencode)) 22 | (.-length (.-data (.-decode bencode)))) 23 | (oset! (.-decode bencode) "position" previous-position)) 24 | (console-log "Caught " e))))) 25 | 26 | (defn ^:private decode-all 27 | "Returns a vector of decoded data that was possible to decode as far as 28 | it could." 29 | [] 30 | (loop [all-data []] 31 | (if-let [decoded-data (decode-next)] 32 | (do 33 | (recur (conj all-data decoded-data))) 34 | all-data))) 35 | 36 | (defn ^:private concat-data-and-decode 37 | "Returns a vector of decoded-data after concatinating the new data onto the 38 | previous data." 39 | [data] 40 | (console-log "Concat and decode...") 41 | (let [new-data (.concat js/Buffer (js/Array. (.-data (.-decode bencode)) (js/Buffer. data)))] 42 | (oset! (.-decode bencode) "data" new-data) 43 | (decode-all))) 44 | 45 | (defn decoded-all? [] 46 | (console-log "Decoded All?: " (when (.-data (.-decode bencode)) 47 | (.-length (.-data (.-decode bencode)))) 48 | (when (.-data (.-decode bencode)) 49 | (.-position (.-decode bencode)))) 50 | (or (nil? (.-data (.-decode bencode))) 51 | (= (.-length (.-data (.-decode bencode))) 52 | (.-position (.-decode bencode))))) 53 | 54 | (defn get-decode-data [] 55 | {:data (oget (.-decode bencode) "data") 56 | :position (oget (.-decode bencode) "position") 57 | :encoding (oget (.-decode bencode) "encoding")}) 58 | 59 | (defn apply-decode-data 60 | "Sets the given decode data as the bencode state if the data has not been 61 | fully decoded." 62 | [{:keys [data position encoding]}] 63 | (console-log "Applying decode data...") 64 | (when (and data position (not= (.-length data) position)) 65 | (oset! (.-decode bencode) "data" data) 66 | (oset! (.-decode bencode) "position" position) 67 | (oset! (.-decode bencode) "encoding" encoding) 68 | (console-log "Applied data: " (.-length (.-data (.-decode bencode))) (.-position (.-decode bencode))))) 69 | 70 | (defn decode 71 | "Returns a vector of decoded data in case the encoded data includes multiple 72 | data chunks. It needs to be in a try-catch block because it can throw when 73 | the given data is empty, partial data, or invalid data. 74 | 75 | 'decode' object holds two states: the data to be decoded and the position to 76 | start decoding next. The position gets updated when decode.next() succeeds. 77 | Because of these states, we need to handle two different cases: 78 | 1. bencode.decode() is called the first time or when all the previous data 79 | has been decoded (meaning the position is at the last index), so we can 80 | override the previous data and just call bencode.decode() again. 81 | 2. The previous data is only partially decoded, so the new data needs 82 | to be concatinated onto the previous data before decoding." 83 | [data] 84 | (if (decoded-all?) 85 | (try 86 | (do 87 | (apply-decode-data {:data data 88 | :position 0 89 | :encoding "utf8"}) 90 | (decode-all)) 91 | (catch js/Error e 92 | (console-log "Caught Error during decoding: " e))) 93 | (concat-data-and-decode data))) 94 | 95 | (defn encode 96 | "Encodes the given data using bencode." 97 | [data] 98 | (try 99 | (.encode bencode data "binary") 100 | (catch js/Error e 101 | (console-log "Failed encoding because of " e)))) 102 | -------------------------------------------------------------------------------- /src/clojure_party_repl/common.cljs: -------------------------------------------------------------------------------- 1 | (ns clojure-party-repl.common 2 | (:require [cljs.nodejs :as node] 3 | [cljs.pprint :refer [pprint]] 4 | [clojure.string :as string])) 5 | 6 | (def node-atom (node/require "atom")) 7 | (def fs (node/require "fs")) 8 | (def CompositeDisposable (.-CompositeDisposable node-atom)) 9 | 10 | (def max-history-count 100) 11 | 12 | ;; A map with project-name to TextEditor 13 | (def repls (atom {})) 14 | 15 | ;; Template for repl's state 16 | (def repl-state 17 | {:current-working-directory "" 18 | :process-env nil 19 | :repl-process nil 20 | :connection nil 21 | :session nil 22 | :host "localhost" 23 | :port nil 24 | :current-ns "user" 25 | :init-code nil 26 | :repl-type nil 27 | :subscriptions nil 28 | :process nil 29 | :host-input-editor nil 30 | :host-output-editor nil 31 | :guest-input-editor nil 32 | :guest-output-editor nil 33 | :repl-history (list) 34 | :current-history-index -1}) 35 | 36 | (def state 37 | (atom {:disposables [] 38 | :lein-path "" 39 | :most-recent-repl-project-name nil})) 40 | 41 | ;; NOTE: When this is true, all output will be printed to the Console. In order 42 | ;; to turn this on, change it to true and recompile. You can also change it 43 | ;; through the ClojureScript REPL for the plugin. 44 | (def in-dev-mode? (atom true)) 45 | 46 | ;; TODO: Add a :force-print true arg? to output even in development mode 47 | (defn console-log 48 | "Used for development. The output can be viewed in the Atom's Console when in 49 | Dev Mode." 50 | [& output] 51 | (when (true? @in-dev-mode?) 52 | (apply (.-log js/console) output))) 53 | 54 | (defn show-error [& error] 55 | (apply (.-error js/console) error) 56 | (.addError (.-notifications js/atom) (apply str error))) 57 | 58 | (defn add-subscription 59 | "This should be wrapped whenever adding any subscriptions in order to dispose 60 | them later." 61 | [project-name disposable] 62 | (.add (get-in @repls [project-name :subscriptions]) disposable)) 63 | 64 | (defn add-repl-history [project-name code] 65 | (when (= max-history-count (count (get-in @repls [project-name :repl-history]))) 66 | (swap! repls update project-name #(update % :repl-history butlast))) 67 | (swap! repls update project-name #(update % :repl-history (fn [history] (conj history code)))) 68 | (swap! repls update project-name #(assoc % :current-history-index -1))) 69 | 70 | ;; TODO: Warn user when project.clj doesn't exist in the project. 71 | (defn get-project-clj [project-path] 72 | (let [project-clj-path (str project-path "/project.clj")] 73 | (console-log "Looking for project.clj at " project-clj-path " - " (.existsSync fs project-clj-path)) 74 | (.existsSync fs project-clj-path))) 75 | 76 | (defn get-project-directory-from-path [root-project-path file-path] 77 | (loop [directories (butlast (string/split file-path #"/")) 78 | project-path root-project-path] 79 | (if (get-project-clj project-path) 80 | project-path 81 | (when (coll? directories) 82 | (recur (next directories) (str project-path "/" (first directories))))))) 83 | 84 | ;; TODO: Support having nested project folders. Right now it assumes that each 85 | ;; project folder that's opened in Atom is an independent project. It 86 | ;; should, however, allow user to open one big folder that contains 87 | ;; multiple projects. 88 | (defn ^:private get-project-path 89 | "Returns the project path of the given editor or nil if there is no project 90 | associated with the editor." 91 | ([] 92 | (get-project-path (.getActiveTextEditor (.-workspace js/atom)))) 93 | ([text-editor] 94 | (when text-editor 95 | (let [path (.getPath (.getBuffer text-editor)) 96 | [directory-path, relative-path] (.relativizePath (.-project js/atom) path)] 97 | (when directory-path 98 | (console-log "----Project---->" directory-path " - " relative-path) 99 | (get-project-directory-from-path directory-path relative-path)))))) 100 | 101 | (defn get-active-project-path 102 | "Returns the path of the project that corresponds to the active editor or nil 103 | if no project is associated with the active text editor." 104 | [] 105 | (get-project-path)) 106 | 107 | (defn get-project-name-from-path 108 | "Returns the project name from the given path or nil." 109 | [project-path] 110 | (when project-path 111 | (last (string/split project-path #"/")))) 112 | 113 | (defn get-active-project-name 114 | [] 115 | (get-project-name-from-path (get-active-project-path))) 116 | 117 | (defn get-project-name-from-text-editor 118 | "Returns the project name from the path of the text editor." 119 | [editor] 120 | (when-let [project-path (get-project-path editor)] 121 | (get-project-name-from-path project-path))) 122 | 123 | (defn get-project-name-from-input-editor 124 | "Returns the project name if there's a reference to the input editor." 125 | [editor] 126 | (some (fn [project-name] 127 | (console-log "Checking if repl exists for the project: " project-name) 128 | (when (or (= editor (get-in @repls [project-name :guest-input-editor])) 129 | (= editor (get-in @repls [project-name :host-input-editor]))) 130 | project-name)) 131 | (keys @repls))) 132 | 133 | (defn get-project-name-from-most-recent-repl 134 | "Returns a project name for the most recently used repl if it still exists." 135 | [] 136 | (when-let [project-name (get @state :most-recent-repl-project-name)] 137 | (when (or (get-in @repls [project-name :host-input-editor]) 138 | (get-in @repls [project-name :guest-input-editor])) 139 | project-name))) 140 | 141 | (defn visible-repl? [text-editor] 142 | (when (and text-editor (.-element text-editor)) 143 | (not= "none" (.-display (.-style (.-element text-editor)))))) 144 | 145 | (defn get-project-name-from-visible-repl [] 146 | (some #(when (or (visible-repl? (get-in @repls [% :host-input-editor])) 147 | (visible-repl? (get-in @repls [% :guest-input-editor]))) 148 | %) 149 | (keys @repls))) 150 | 151 | (defn add-repl [project-name & options] 152 | (swap! repls assoc project-name (-> (apply assoc repl-state options) 153 | (assoc :subscriptions (CompositeDisposable.))))) 154 | 155 | ;; TODO: Support destroying multiple editors with a shared buffer. 156 | (defn ^:private close-editor 157 | "Searches through all the panes for the editor and destroys it." 158 | [editor] 159 | (doseq [pane (.getPanes (.-workspace js/atom))] 160 | (when (some #(= editor %) (.getItems pane)) 161 | (.destroyItem pane editor)))) 162 | 163 | (defn destroy-editor 164 | "Destroys an editor defined in the state." 165 | [project-name editor-keyword] 166 | (when-let [editor (get-in @repls [project-name editor-keyword])] 167 | (close-editor editor) 168 | (swap! repls update project-name #(assoc % editor-keyword nil)))) 169 | 170 | (defn dispose-project-if-empty 171 | "Remove the project state if there's no running repls for the project name." 172 | [project-name] 173 | (when-not (or (get-in @repls [project-name :host-input-editor]) 174 | (get-in @repls [project-name :guest-input-editor])) 175 | (swap! repls dissoc project-name) 176 | (when (= project-name (get @state :most-recent-repl-project-name)) 177 | (swap! state assoc :most-recent-repl-project-name nil)))) 178 | 179 | ;; TODO: Pretty print results 180 | (defn append-to-editor 181 | "Appends text at the end of the editor. Always append a newline following the 182 | text unless specified not to. If the last line only contains whitespaces, 183 | delete all whitespaces of the line in order to ignore them. Otherwise, any 184 | text appended after that gets indented to the right." 185 | [editor output & {:keys [add-newline?] :or {add-newline? true}}] 186 | (when editor 187 | (let [text (if add-newline? 188 | (str output "\n") 189 | output)] 190 | (.moveToBottom editor) 191 | (when (re-find #"^\s+$" (.getLastLine (.getBuffer editor))) 192 | (.deleteToBeginningOfLine editor)) 193 | (.insertText editor text (js-obj "bypassReadOnly" true)) 194 | (.scrollToBottom (.-element editor)) 195 | (.moveToBottom editor)))) 196 | -------------------------------------------------------------------------------- /src/clojure_party_repl/connection_panel.cljs: -------------------------------------------------------------------------------- 1 | (ns clojure-party-repl.connection-panel 2 | "Creates a model connection panel for prompting the user when connecting 3 | to a remote repl." 4 | (:require-macros [cljs.core.async.macros :refer [go]]) 5 | (:require [cljs.core.async :refer [chan (.getModel element) 28 | (.getText) 29 | (string/trim))] 30 | (if-not (string/blank? text) 31 | text 32 | (.getAttribute element "placeholder-text"))) 33 | (.-innerText element))) 34 | 35 | (defn ^:private get-option-value 36 | "Returns the selected value in the select element." 37 | [select-element] 38 | (.-value (aget (.-options select-element) (.-selectedIndex select-element)))) 39 | 40 | (defn ^:private set-text 41 | "Sets the text value for an element or an atom-text-editor." 42 | [element text] 43 | (if (= (string/lower-case (.-tagName element)) 44 | "atom-text-editor") 45 | (.setText (.getModel element) text) 46 | (set! (.-innerText element) text))) 47 | 48 | (defn ^:private set-text-color 49 | "Sets the text color style on an element" 50 | [element value] 51 | (set! (.-color (.-style element)) value)) 52 | 53 | (defn get-all-project-names [] 54 | (->> (.getPaths (.-project js/atom)) 55 | (map common/get-project-name-from-path))) 56 | 57 | (defn ^:private create-drop-down [] 58 | (let [project-select (doto (.createElement js/document "select") 59 | (.setAttribute "tabindex" -1) 60 | (set-text-color "black"))] 61 | (doseq [project-name (get-all-project-names)] 62 | (.appendChild project-select 63 | (doto (.createElement js/document "option") 64 | (.setAttribute "value" project-name) 65 | (set-text project-name)))) 66 | project-select)) 67 | 68 | (defn ^:private create-connection-panel-dom 69 | "Builds the DOM for the modal panel and returns a map of UI components to be 70 | stored in the ui-components atom." 71 | [] 72 | (let [default-host "localhost" 73 | default-port "" 74 | container (.createElement js/document "section") 75 | header (doto (.createElement js/document "h4") 76 | (.setAttribute "class" "icon icon-clob")) 77 | project-container (doto (.createElement js/document "div") 78 | (.setAttribute "class" "clojure-party-repl container control-group")) 79 | project-label (doto (.createElement js/document "label") 80 | (.setAttribute "class" "control-label")) 81 | project-title (doto (.createElement js/document "div") 82 | (.setAttribute "class" "setting-title")) 83 | project-subview (.createElement js/document "subview") 84 | project-select (doto (.createElement js/document "select") 85 | (.setAttribute "tabindex" -1) 86 | (.setAttribute "class" "form-control") 87 | (set-text-color "black")) 88 | repl-type-container (doto (.createElement js/document "div") 89 | (.setAttribute "class" "clojure-party-repl container control-group")) 90 | repl-type-label (doto (.createElement js/document "label") 91 | (.setAttribute "class" "control-label")) 92 | repl-type-title (doto (.createElement js/document "div") 93 | (.setAttribute "class" "setting-title")) 94 | repl-type-select (doto (.createElement js/document "select") 95 | (.setAttribute "tabindex" -1) 96 | (.setAttribute "class" "form-control") 97 | (set-text-color "black")) 98 | host-container (doto (.createElement js/document "div") 99 | (.setAttribute "class" "block")) 100 | host-label (.createElement js/document "div") 101 | host-subview (.createElement js/document "subview") 102 | host-editor (doto (.createElement js/document "atom-text-editor") 103 | (.setAttribute "mini" true) 104 | (.setAttribute "placeholder-text" default-host) 105 | (.setAttribute "tabindex" -1)) 106 | port-container (doto (.createElement js/document "div") 107 | (.setAttribute "class" "clojure-party-repl container")) 108 | port-label (.createElement js/document "div") 109 | port-subview (.createElement js/document "subview") 110 | port-editor (doto (.createElement js/document "atom-text-editor") 111 | (.setAttribute "mini" true) 112 | (.setAttribute "placeholder-text" default-port) 113 | (.setAttribute "tabindex" -1)) 114 | button-container (doto (.createElement js/document "div") 115 | (.setAttribute "class" "clojure-party-repl container button-container")) 116 | cancel-button (doto (.createElement js/document "button") 117 | (.setAttribute "class" "btn clojure-party-repl cancel-button")) 118 | proceed-button (doto (.createElement js/document "button") 119 | (.setAttribute "class" "btn btn-primary clojure-party-repl proceed-button"))] 120 | (set-text project-title strings/connection-panel-project) 121 | (set-text repl-type-title strings/connection-panel-repl-type) 122 | (set-text host-label strings/connection-panel-host) 123 | (set-text port-label strings/connection-panel-port) 124 | (set-text cancel-button strings/cancel-button) 125 | (set-text proceed-button strings/connect-to-repl-button) 126 | (.appendChild container header) 127 | (.appendChild container project-container) 128 | (.appendChild project-container project-label) 129 | (.appendChild project-label project-title) 130 | (.appendChild project-container project-subview) 131 | (.appendChild project-subview project-select) 132 | (.appendChild container repl-type-container) 133 | (.appendChild repl-type-container repl-type-label) 134 | (.appendChild repl-type-label repl-type-title) 135 | (.appendChild repl-type-container repl-type-select) 136 | (.appendChild container host-container) 137 | (.appendChild host-container host-label) 138 | (.appendChild host-container host-subview) 139 | (.appendChild host-subview host-editor) 140 | (.appendChild container port-container) 141 | (.appendChild port-container port-label) 142 | (.appendChild port-container port-subview) 143 | (.appendChild port-subview port-editor) 144 | (.appendChild container button-container) 145 | (.appendChild button-container cancel-button) 146 | (.appendChild button-container proceed-button) 147 | {:container container 148 | :header header 149 | :project-select project-select 150 | :repl-type-select repl-type-select 151 | :host-editor host-editor 152 | :port-editor port-editor 153 | :cancel-button cancel-button 154 | :proceed-button proceed-button})) 155 | 156 | (defn ^:private update-repl-type-select 157 | "TODO: Add more options for nrepl type." 158 | [repl-type-select] 159 | (set! (.-innerHTML repl-type-select) "") ; The most performant method for removing all children 160 | (.appendChild repl-type-select (doto (.createElement js/document "option") 161 | (.setAttribute "value" "lein") 162 | (set-text strings/leiningen-name))) 163 | (.appendChild repl-type-select (doto (.createElement js/document "option") 164 | (.setAttribute "value" "unrepl") 165 | (set-text strings/unrepl-name)))) 166 | 167 | (defn ^:private update-project-select 168 | "Clears all of the children from the project-select dropdown and fills it with 169 | all of the currently open projects, selecting the project for the currently 170 | active editor." 171 | [project-select] 172 | (set! (.-innerHTML project-select) "") ; The most performant method for removing all children 173 | (let [active-project-name (common/get-active-project-name)] 174 | (doseq [project-name (get-all-project-names)] 175 | (let [option (doto (.createElement js/document "option") 176 | (.setAttribute "value" project-name) 177 | (set-text project-name))] 178 | (when (= project-name active-project-name) 179 | (.setAttribute option "selected" true)) 180 | (.appendChild project-select option))))) 181 | 182 | (defn ^:private add-connection-panel-commands 183 | "Adds Atom commands to listen to the enter and escape keys and 184 | button clicks. 185 | 186 | When enter is pressed or proceed button is clicked, reads the 187 | host and port values and writes them to the async channel 188 | for the caller to get." 189 | [components] 190 | (let [{:keys [panel container host-editor port-editor 191 | repl-type-select project-select 192 | cancel-button proceed-button]} components 193 | confirm (fn [event] 194 | (.hide panel) 195 | (async/put! address-channel 196 | {:host (get-text host-editor) 197 | :port (int (get-text port-editor)) 198 | :repl-type (keyword (get-option-value repl-type-select)) 199 | :project-name (get-option-value project-select)})) 200 | cancel (fn [event] 201 | (.hide panel) 202 | (async/put! address-channel false))] 203 | (-> (.-commands js/atom) 204 | (.add container "core:confirm" confirm)) 205 | (-> (.-commands js/atom) 206 | (.add container "core:cancel" cancel)) 207 | (.addEventListener cancel-button "click" cancel) 208 | (.addEventListener proceed-button "click" confirm))) 209 | 210 | (defn ^:private add-connection-panel-tab-listeners 211 | "Adds a keydown listener to intercept Atom's default behavior 212 | and switch between the inputs. Since there are only two 213 | inputs, we don't need to worry about behavior for shift-tab 214 | since it's identical. 215 | 216 | An altenative way to implement this (how the find-and-replace package 217 | does it), would be to export new functions and create a keymap with a 218 | selector which specifically targets these inputs." 219 | [components] 220 | (let [{:keys [host-editor port-editor]} components 221 | keydown (fn [event] 222 | (when (= (.-key event) "Tab") 223 | (if (.hasFocus host-editor) 224 | (.focus port-editor) 225 | (.focus host-editor)) 226 | (.stopPropagation event) 227 | (.preventDefault event)))] 228 | (.addEventListener host-editor "keydown" keydown) 229 | (.addEventListener port-editor "keydown" keydown))) 230 | 231 | ;; TODO: Read the port from a .nrepl-port file in the current project if it exists 232 | (defn create-connection-panel 233 | "Creates the connection panel and leaves it hidden until 234 | the user is prompted." 235 | [] 236 | (let [{:keys [container] :as components} (create-connection-panel-dom) 237 | panel (-> (.-workspace js/atom) 238 | (.addModalPanel (js-obj "item" container 239 | "visible" false))) 240 | components (assoc components :panel panel)] 241 | (add-connection-panel-commands components) 242 | (add-connection-panel-tab-listeners components) 243 | (reset! ui-components components))) 244 | 245 | (defn prompt-connection-panel 246 | "Interupts the user with a modal connection panel asking for 247 | a socket address to connect to, returning the result through 248 | an async channel. 249 | 250 | Returns false if the user cancels the prompt." 251 | [message] 252 | (let [{:keys [panel header host-editor port-editor 253 | repl-type-select project-select]} @ui-components] 254 | (go 255 | (if-not (.-visible panel) 256 | (do 257 | (set-text header message) 258 | (set-text host-editor "") 259 | (set-text port-editor "") 260 | (update-repl-type-select repl-type-select) 261 | (update-project-select project-select) 262 | (.show panel) 263 | (.focus host-editor) 264 | ( (count (get-in @repls [project-name :repl-history])) 80 | (get-in @repls [project-name :current-history-index])) 81 | (show-current-history project-name editor))))) 82 | 83 | (defn show-newer-repl-history 84 | "Exported plugin command. Replaces the content of the input-editor with a 85 | newer history item." 86 | [event] 87 | (let [editor (.getActiveTextEditor (.-workspace js/atom)) 88 | project-name (get-project-name-from-input-editor editor)] 89 | (when (and project-name 90 | (or (= editor (get-in @repls [project-name :guest-input-editor])) 91 | (= editor (get-in @repls [project-name :host-input-editor])))) 92 | (when (>= (get-in @repls [project-name :current-history-index]) 0) 93 | (swap! repls update project-name #(update % :current-history-index dec))) 94 | (if (> 0 (get-in @repls [project-name :current-history-index])) 95 | (.setText editor "") 96 | (show-current-history project-name editor))))) 97 | 98 | (defn ^:private add-commands 99 | "Exports commands and makes them available in Atom. Exported commands also 100 | need to be added to shadow-cljs.edn." 101 | [] 102 | (swap! state update :disposables concat 103 | [(.add commands "atom-workspace" (str package-namespace ":startLocalRepl") start-local-repl) 104 | (.add commands "atom-workspace" (str package-namespace ":connectToRemoteRepl") connect-to-remote-repl) 105 | (.add commands "atom-workspace" (str package-namespace ":sendToRepl") send-to-repl) 106 | (.add commands "atom-text-editor.repl-entry" (str package-namespace ":showNewerHistory") show-newer-repl-history) 107 | (.add commands "atom-text-editor.repl-entry" (str package-namespace ":showOlderHistory") show-older-repl-history)])) 108 | 109 | (defn ^:private observe-setting 110 | [name callback] 111 | (.observe (.-config js/atom) name callback)) 112 | 113 | (defn ^:private observe-settings-changes 114 | [] 115 | (swap! state update :disposables concat 116 | [(observe-setting (str package-namespace ".lein-path") #(cond 117 | (= % "") 118 | (swap! state assoc :lein-path "") 119 | (string/ends-with? % "/") 120 | (swap! state assoc :lein-path %) 121 | :else 122 | (swap! state assoc :lein-path (str % "/"))))])) 123 | 124 | (defn ^:private dispose-repls 125 | "Disposes all the existing guest and host REPLs." 126 | [] 127 | (doseq [project-name (keys @repls)] 128 | (guest/dispose project-name) 129 | (host/dispose project-name))) 130 | 131 | (defn consume-autosave 132 | "Consumes the Services API provided by Atom's autosave package to prevent 133 | our editors from getting autosaved into the project. The hook for this is 134 | defined in package.json." 135 | [info] 136 | (let [dont-save-if (oget info "dontSaveIf")] 137 | (dont-save-if (fn [pane-item] 138 | (some #(string/includes? (.getPath pane-item) %1) 139 | [output-editor-title input-editor-title]))))) 140 | 141 | (def config 142 | "Config Settings for Atom. This will be shown in the Settings section along with the Readme." 143 | (clj->js 144 | {:lein-path 145 | {:title "Path to Leiningen" 146 | :description "If your Leiningen is not placed in one of your System's $PATH, specify where your `lein` is installed." 147 | :type "string" 148 | :default ""}})) 149 | 150 | (defn activate 151 | "Initializes the plugin, called automatically by Atom, during startup or if 152 | the plugin was just installed or re-enabled." 153 | [] 154 | (console-log "Activating clojure-party-repl...") 155 | (add-commands) 156 | (observe-settings-changes) 157 | (panel/create-connection-panel) 158 | (guest/look-for-teletyped-repls)) 159 | 160 | (defn deactivate 161 | "Shuts down the plugin, called automatically by Atom if the plugin is 162 | disabled or uninstalled." 163 | [] 164 | (console-log "Deactivating clojure-party-repl...") 165 | (dispose-repls) 166 | (reset! repls {}) 167 | (doseq [disposable (get @state :disposables)] 168 | (.dispose disposable)) 169 | (swap! state assoc :disposables [])) 170 | 171 | (def start 172 | "Activates the plugin, used for development." 173 | activate) 174 | 175 | (def stop 176 | "Deactivates the plugin, used for development." 177 | deactivate) 178 | -------------------------------------------------------------------------------- /src/clojure_party_repl/execution.cljs: -------------------------------------------------------------------------------- 1 | (ns clojure-party-repl.execution 2 | (:require [clojure.string :as string] 3 | [cljs.nodejs :as node] 4 | [clojure-party-repl.repl :as repl] 5 | [clojure-party-repl.strings :refer [execute-comment]] 6 | [clojure-party-repl.common :as common :refer [append-to-editor 7 | console-log 8 | show-error 9 | repls 10 | visible-repl?]] 11 | [cljs.core.async :as async :refer [timeout = (.indexOf scopes "string.quoted.double.clojure") 0) 27 | (>= (.indexOf scopes "comment.line.semicolon.clojure") 0) 28 | (>= (.indexOf scopes "string.regexp.clojure") 0)))) 29 | 30 | (defn find-all-namespace-declarations 31 | "Searches through the entire buffer for all namespace declarations and 32 | collects the ranges." 33 | [editor range] 34 | (let [ranges (transient []) 35 | regex (js/RegExp. "\\s*\\(\\s*ns\\s*([A-Za-z\\*\\+\\!\\-\\_\\'\\?]?[A-Za-z0-9\\.\\*\\+\\!\\-\\_\\'\\?\\:]*)" "gm")] 36 | (.backwardsScanInBufferRange editor 37 | regex 38 | range (fn [result] 39 | (when-not (inside-string-or-comment? editor (.-start (.-range result))) 40 | (let [match-string (str (second (.-match result)))] 41 | (conj! ranges [(.-start (.-range result)) match-string]))))) 42 | (console-log "Namespaces " ranges) 43 | (persistent! ranges))) 44 | 45 | ;; TODO: Warn user if the namespace isn't declared in the repl. Currently, 46 | ;; repl simply won't return any results when we send code to undeclared 47 | ;; namespaces. 48 | (defn find-namespace-for-range 49 | "Finds a namespace where the code range is declared at." 50 | [editor range] 51 | (let [search-range ((.-Range node-atom) 0 (.-start range)) 52 | namespaces (find-all-namespace-declarations editor search-range)] 53 | (some (fn [[point namespace]] 54 | (console-log "Namespace " namespace " " (.isGreaterThan (.-start range) point)) 55 | (when (.isGreaterThan (.-start range) point) 56 | namespace)) 57 | namespaces))) 58 | 59 | ;; TODO: When both host and guest input editors exist and are visible, 60 | ;; ask the user with a pop up which one to use. 61 | (defn execute-on-host-or-guest 62 | "Execute code on the editor that exists and is visible when there're both 63 | host and guest REPLs, otherwise, execute code on the editor that exists." 64 | [project-name code & [options]] 65 | (let [find-repl (if (and (get-in @repls [project-name :host-input-editor]) 66 | (get-in @repls [project-name :guest-input-editor])) 67 | #(and (some? (get-in @repls [%2 %1])) 68 | (visible-repl? (get-in @repls [%2 %1]))) 69 | #(some? (get-in @repls [%2 %1])))] 70 | (condp find-repl project-name 71 | :host-input-editor (execute project-name code options) 72 | :guest-input-editor (append-to-editor (get-in @repls [project-name :guest-input-editor]) 73 | (str code execute-comment) 74 | :add-newline? false) 75 | (show-error "No running REPL or the REPL isn't visible for the project: " project-name)))) 76 | 77 | (defn flash-range 78 | "Temporary highlight the range to provide visual feedback for users, so 79 | they can see what code has been executed in the file." 80 | [editor range] 81 | (let [marker (.markBufferRange editor range)] 82 | (.decorateMarker editor marker (js-obj "type" "highlight" 83 | "class" "executed-top-level-form")) 84 | (go 85 | ( (.-workspace js/atom) 37 | (.open (str output-editor-title " " project-name) (js-obj "split" "right")) 38 | (.then (fn [editor] 39 | (set! (.-isModified editor) (fn [] false)) 40 | (set! (.-isModified (.getBuffer editor)) (fn [] false)) 41 | (.setSoftWrapped editor true) 42 | (.add (.-classList (.-element editor)) "repl-history") 43 | (set-grammar editor) 44 | (.moveToBottom editor) 45 | (swap! repls update project-name #(assoc % :host-output-editor editor)) 46 | (.setPlaceholderText editor output-editor-placeholder) 47 | (add-subscription project-name 48 | (.onDidDestroy editor (fn [event] 49 | (swap! repls update project-name #(assoc % :host-output-editor nil)) 50 | (repl/stop-process project-name) 51 | (dispose project-name) 52 | (dispose-project-if-empty project-name)))))))) 53 | 54 | (defn find-non-blank-last-row [buffer] 55 | (let [last-row (.getLastRow buffer)] 56 | (if (.isRowBlank buffer last-row) 57 | (.previousNonBlankRow buffer last-row) 58 | last-row))) 59 | 60 | ;; TODO: Set a placeholder text to notify user when repl is ready. 61 | (defn create-input-editor 62 | "Opens a text editor for simulating repl's entry area. Adds a listener 63 | onDidStopChanging to look for execute-comment entered by guest side using 64 | teletype in the entry, so that it can detect when to execute the code." 65 | [project-name] 66 | (-> (.-workspace js/atom) 67 | (.open (str input-editor-title " " project-name) (js-obj "split" "down")) 68 | (.then (fn [editor] 69 | (set! (.-isModified editor) (fn [] false)) 70 | (set! (.-isModified (.getBuffer editor)) (fn [] false)) 71 | (.setSoftWrapped editor true) 72 | (.add (.-classList (.-element editor)) "repl-entry") 73 | (set-grammar editor) 74 | (swap! repls update project-name #(assoc % :host-input-editor editor)) 75 | (add-subscription project-name 76 | (.onDidStopChanging editor (fn [event] 77 | (let [buffer (.getBuffer editor) 78 | non-blank-row (find-non-blank-last-row buffer) 79 | last-text (.lineForRow buffer non-blank-row)] 80 | (when (ends-with? (trim last-text) execute-comment) 81 | (.deleteRows buffer (inc non-blank-row) (inc (.getLastRow buffer))) 82 | (execution/execute-entered-text project-name editor)))))) 83 | (add-subscription project-name 84 | (.onDidDestroy editor (fn [event] 85 | (swap! repls update project-name #(assoc % :host-input-editor nil)) 86 | (repl/stop-process project-name) 87 | (dispose project-name) 88 | (dispose-project-if-empty project-name)))))))) 89 | 90 | ;; TODO: Make sure to create input editor after output editor has been created. 91 | (defn create-editors [project-name] 92 | (create-output-editor project-name) 93 | (create-input-editor project-name)) 94 | -------------------------------------------------------------------------------- /src/clojure_party_repl/local_repl.cljs: -------------------------------------------------------------------------------- 1 | (ns clojure-party-repl.local-repl 2 | (:require [cljs.nodejs :as node] 3 | [clojure.string :as string] 4 | [oops.core :refer [oset!]] 5 | [clojure-party-repl.repl :as repl :refer [stop-process 6 | append-to-output-editor]] 7 | [clojure-party-repl.nrepl :as nrepl] 8 | [clojure-party-repl.common :as common :refer [console-log 9 | show-error 10 | get-project-path 11 | get-project-name-from-path 12 | destroy-editor 13 | repls 14 | state]])) 15 | 16 | ;; TODO: Switch to unrepl 17 | ;; TODO: Support sending multiple messages to repl 18 | ;; TODO: Support exiting repl by Control+D or (exit) or (quit) just like the 19 | ;; Leiningen doc says. 20 | 21 | (def process (node/require "process")) 22 | (def child-process (node/require "child_process")) 23 | 24 | (def ^:private lein-exec (string/split "lein repl" #" ")) 25 | 26 | (defn ^:private look-for-port 27 | "Searches for a port that nRepl server started on." 28 | [project-name data-string] 29 | (when (nil? (get-in @repls [project-name :port])) 30 | (when-let [[_ port] (re-find #"nREPL server started on port (\d+)" data-string)] 31 | (console-log "Port found from " data-string) 32 | (swap! repls update project-name #(assoc % :port port)) 33 | (nrepl/connect-to-nrepl {:project-name project-name 34 | :host (get-in @repls [project-name :host]) 35 | :port port})))) 36 | 37 | (defn ^:private look-for-ns 38 | "Searches for a namespace that's currently set in the repl." 39 | [project-name data-string] 40 | (when-let [match (re-find #"(\S+)=>" data-string)] 41 | (console-log "Namespace found!!! " match " from " data-string) 42 | (swap! repls update project-name #(assoc % :current-ns (second match))))) 43 | 44 | (defn ^:private look-for-repl-info [project-name data-string] 45 | (look-for-port project-name data-string) 46 | (look-for-ns project-name data-string)) 47 | 48 | (defn ^:private setup-process 49 | "Adding callbacks to all messages that lein process recieves." 50 | [project-name repl-process] 51 | (console-log "Setting up process...") 52 | (.on (.-stdout repl-process) "data" (fn [data] 53 | (let [data-string (.toString data)] 54 | (look-for-repl-info project-name data-string) 55 | (append-to-output-editor project-name data-string :add-newline? false)))) 56 | (.on (.-stderr repl-process) "data" (fn [data] 57 | (append-to-output-editor project-name (.toString data) :add-newline? false))) 58 | (.on repl-process "error" (fn [error] 59 | (cond 60 | (string/ends-with? (.toString error) "lein ENOENT") 61 | (show-error error " Please change the path for Leiningen in the Settings.") 62 | (string/includes? (.toString error) "no such file or directory") 63 | (show-error error " Party Repl couldn't find your Leiningen. Please specify where your `lein` command is in the Settings.") 64 | :else 65 | (show-error "Lein process error: " error)))) 66 | (.on repl-process "close" (fn [code] 67 | (console-log "Closing process... " code) 68 | (stop-process project-name))) 69 | (.on repl-process "exit" (fn [code signal] 70 | (console-log "Exiting repl... " code " " signal) 71 | (swap! repls update project-name #(assoc % :repl-process nil))))) 72 | 73 | (defn ^:private start-repl-process 74 | "Starts a lein repl process on project-path." 75 | [env project-path & args] 76 | (console-log "Starting lein process...") 77 | (let [project-name (get-project-name-from-path project-path) 78 | process-env (js-obj "cwd" project-path 79 | "env" (goog.object.set env "PWD" project-path)) 80 | lein-command (str (:lein-path @state) (first lein-exec)) 81 | repl-process (.spawn child-process lein-command (clj->js (next lein-exec)) process-env)] 82 | (swap! repls update project-name #(assoc % :current-working-directory project-path 83 | :process-env process-env 84 | :repl-process repl-process 85 | :repl-type :repl-type/nrepl)) 86 | (setup-process project-name repl-process))) 87 | 88 | (defn ^:private get-env 89 | "Setup the environment that lein process will run in." 90 | [] 91 | (let [env (goog.object.clone (.-env process))] 92 | (doseq [k ["PWD" "ATOM_HOME" "ATOM_SHELL_INTERNAL_RUN_AS_NODE" "GOOGLE_API_KEY" "NODE_ENV" "NODE_PATH" "userAgent" "taskPath"]] 93 | (goog.object.remove env k)) 94 | env)) 95 | 96 | (defn start-local-repl [project-path] 97 | (start-repl-process (get-env) project-path)) 98 | -------------------------------------------------------------------------------- /src/clojure_party_repl/nrepl.cljs: -------------------------------------------------------------------------------- 1 | (ns clojure-party-repl.nrepl 2 | (:require [cljs.nodejs :as node] 3 | [clojure.string :as string] 4 | [oops.core :refer [oget oset! oset!+ ocall]] 5 | [clojure-party-repl.common :refer [console-log repls add-repl-history]] 6 | [clojure-party-repl.bencode :as bencode] 7 | [clojure-party-repl.repl :as repl] 8 | [cljs.core.async :as async :refer [chan timeout close! ! alts!]]) 9 | (:require-macros [cljs.core.async.macros :refer [go go-loop]])) 10 | 11 | (def net (node/require "net")) 12 | (def process (node/require "process")) 13 | 14 | (def timeout-msec 5000) 15 | 16 | (defn timeout-chan [] 17 | (timeout (or timeout-msec (.-MAX_SAFE_INTEGER js/Number)))) 18 | 19 | (defn ^:private apply-decode-data [connection] 20 | (bencode/apply-decode-data @(:decode connection))) 21 | 22 | (defn ^:private cache-decode-data [connection] 23 | (let [{:keys [data position encoding]} (bencode/get-decode-data)] 24 | (when-not (bencode/decoded-all?) 25 | (swap! (:decode connection) assoc :data data 26 | :position position 27 | :encoding encoding)))) 28 | 29 | (defn ^:private swap-decode-data [project-name connection] 30 | (let [previous-project-name (repl/get-most-recent-repl)] 31 | (when (and previous-project-name 32 | (not= previous-project-name project-name)) 33 | (cache-decode-data (get-in @repls [previous-project-name :connection])) 34 | (apply-decode-data connection)))) 35 | 36 | ;; TODO: Manage decode data with message id. 37 | (defn send 38 | "Writes a message onto the socket." 39 | [connection message callback] 40 | (let [id (or (get message "id") 41 | (.-uuid (random-uuid))) 42 | message-js (clj->js (assoc message "id" id))] 43 | (swap! (:queued-messages connection) assoc id []) 44 | (swap! (:callbacks connection) assoc id callback) 45 | (apply-decode-data connection) 46 | (.write (:socket-connection connection) (bencode/encode message-js)))) 47 | 48 | (defn eval [connection code options callback] 49 | (send connection 50 | (assoc options "op" "eval" 51 | "code" code) 52 | callback)) 53 | 54 | (defn clone-connection 55 | ([connection callback] 56 | (send connection {"op" "clone"} callback)) 57 | ([connection session callback] 58 | (send connection {"op" "clone" "session" session} callback))) 59 | 60 | ;; Use defmulti/defmethod on these 61 | ; (comment (send []) 62 | ; (clone []) 63 | ; (close []) 64 | ; (describe []) 65 | ; (eval []) 66 | ; (intrupt []) 67 | ; (loadFile []) 68 | ; (stdin [])) 69 | 70 | (def connection-template 71 | {:socket-connection nil 72 | :output-chan nil 73 | :queued-messages nil 74 | :callbacks nil 75 | :decode {:data nil 76 | :encoding nil 77 | :position 0}}) 78 | 79 | (defn ^:private has-done-message? [messages] 80 | (and (coll? messages) 81 | (last messages) 82 | (.-status (last messages)) 83 | (some #(= % "done") (.-status (last messages))))) 84 | 85 | (defn ^:private callback-with-queued-messages 86 | "Calls a callback associated with messages with the id. The messages should 87 | include a 'done' message, indicating the end of the messages." 88 | [connection id] 89 | (when-let [messages (get @(:queued-messages connection) id)] 90 | (when (has-done-message? messages) 91 | (when-let [callback (get @(:callbacks connection) id)] 92 | (callback nil messages) 93 | (swap! (:queued-messages connection) dissoc id) 94 | (swap! (:callbacks connection) dissoc id))))) 95 | 96 | (defn ^:private consume-all-data 97 | "Keeps reading data from the socket until it's been depleated. All data is 98 | decoded and put into a channel. The channel is closed when no more data 99 | can be read from the socket." 100 | [project-name connection message-chan] 101 | (go-loop [] 102 | (console-log "Consuming data...") 103 | (if-let [chunk (.read (:socket-connection connection))] 104 | (do 105 | (swap-decode-data project-name connection) 106 | (let [messages (bencode/decode chunk)] 107 | (when-not (empty? messages) 108 | (console-log "Decoded data...") 109 | (>! message-chan messages) 110 | (recur)))) 111 | (close! message-chan)))) 112 | 113 | (defn ^:private read-data 114 | "Called when socket dispatches readable event, having data available 115 | to be read. This consumes all available data on socket and put them in 116 | output-channel after converting them into messages. message-chan will 117 | eventually takes nil out and breaks out of loop-recur when it's been closed." 118 | [project-name connection] 119 | (console-log "Reading data... ") 120 | (let [message-chan (chan)] 121 | (go-loop [] 122 | (if-let [messages (! (:output-chan connection) messages) 126 | (doseq [message-js messages] 127 | (let [id (.-id message-js)] 128 | (swap! (:queued-messages connection) update id conj message-js) 129 | (callback-with-queued-messages connection id))) 130 | (recur)) 131 | (cache-decode-data connection))) 132 | (consume-all-data project-name connection message-chan))) 133 | 134 | ;; TODO: Support type ahead by creating a new session to send the code. 135 | 136 | ;; Messages can be one of these types: 137 | ;; 1. General REPL's messages, especially for printing the initial description of the REPL -> session-id 138 | ;; 2. Replies that come back from the message sent to the REPL -> message-id 139 | 140 | ;; TODO: Handle clone error 141 | (defn ^:private get-new-session-message 142 | "Reads the first message containing session id received after socket 143 | connection has been established." 144 | [project-name connection] 145 | (let [message-chan (chan)] 146 | (go 147 | (when-let [messages ( ") :add-newline? false)) 230 | 231 | (defn ^:private output-message 232 | "Outputs the message contents onto the output editor. If it contains a 233 | namespace, updates the current namespace for the project." 234 | [project-name message-js] 235 | (when (.-ns message-js) 236 | (swap! repls update project-name #(assoc % :current-ns (.-ns message-js)))) 237 | (if (.-out message-js) 238 | (repl/append-to-output-editor project-name (string/trim (.-out message-js))) 239 | (if (.-value message-js) 240 | (repl/append-to-output-editor project-name (.-value message-js)) 241 | (when (.-err message-js) 242 | (repl/append-to-output-editor project-name (.-err message-js)))))) 243 | 244 | (defn ^:private filter-by-session [session messages] 245 | (filter #(= (.-session %) session) messages)) 246 | 247 | (defn ^:private handle-messages 248 | "Outputs messages as they come into the output channel." 249 | [project-name connection] 250 | (console-log "Handler called...") 251 | (let [output-chan (:output-chan connection)] 252 | (go-loop [] 253 | (when-let [messages (! alts!]]) 13 | (:require-macros [cljs.core.async.macros :refer [go go-loop]])) 14 | 15 | (def node-atom (node/require "atom")) 16 | (def process (node/require "process")) 17 | (def net (node/require "net")) 18 | (def fs (node/require "fs")) 19 | 20 | (def atom-home-directory (oget process ["env" "ATOM_HOME"])) 21 | 22 | (fs.readFile (str atom-home-directory "/packages/clojure-party-repl/resources/unrepl/blob.clj") 23 | (fn [err data] 24 | (when err (console-log err)) 25 | (def unrepl-blob data))) 26 | 27 | (defn send [connection code] 28 | (.write connection code)) 29 | 30 | (def ^:private ellipsis "…more") 31 | (def ^:private elision-key :get) 32 | 33 | (defn ^:private has-elisions? 34 | "Elision is expressed as either `#unrepl/... nil` or 35 | `#unrepl/... {:get continuation-fn}` at the end of a collection. Elision allows 36 | us to have control over how much and when we want to show the details. 37 | 38 | There are a few places it could appear: 39 | 1. Lazy sequence (0 1 2 3 4 5 6 7 8 9 {:get (continuation-fn :id)}) 40 | 2. Long string [prefix {:get (continuation-fn :id)}] 41 | 3. Stacktraces [[], [], [], {:get (continuation-fn :id)}] 42 | 4. Map {:a 0, :b 1, :c 2, ... {:get (continuation-fn :id)}} 43 | 44 | The cutoff lengths are set as defaults inside unrepl.printer." 45 | [coll] 46 | (if (and (coll? coll) 47 | (or (and (satisfies? PersistentArrayMap coll) 48 | (every? elision-key (keys coll))) 49 | (contains? (last coll) elision-key))) 50 | true 51 | false)) 52 | 53 | (defn ^:private find-matching-marker-range 54 | "Returns a marker if the cursor is positioned inside the marker." 55 | [cursor-position [marker continuation-fn]] 56 | (when-let [range (.getBufferRange marker)] 57 | (when (and (.isGreaterThanOrEqual cursor-position (.-start range)) 58 | (.isLessThanOrEqual cursor-position (.-end range))) 59 | (console-log "Marker clicked!" marker continuation-fn) 60 | [marker continuation-fn]))) 61 | 62 | (defn ^:private on-elision-click 63 | "Sends the continuation function code for the corresponding elision when the 64 | cursor moves inside the elision marker." 65 | [project-name event] 66 | (when-not (.-textChanged event) 67 | (let [cursor-position (.-newBufferPosition event) 68 | [clicked-marker continuation-fn] (some (partial find-matching-marker-range cursor-position) 69 | (get-in @repls [project-name :connection :elisions]))] 70 | (console-log "Output editor clicked!" cursor-position) 71 | (when (and clicked-marker continuation-fn) 72 | (swap! repls update-in [project-name :connection] #(assoc % :pending-elision-range (.getBufferRange clicked-marker))) 73 | (send (get-in @repls [project-name :connection :socket-connection]) (str continuation-fn "\n")) 74 | (.destroy clicked-marker) 75 | (swap! repls update-in [project-name :connection :elisions] #(dissoc % clicked-marker)))))) 76 | 77 | (defn ^:private add-elision-click-handler 78 | "Watches the cursor position inside the output editor to trigger the elision 79 | to expand." 80 | [project-name] 81 | (let [output-editor (get-in @repls [project-name :host-output-editor])] 82 | (add-subscription project-name 83 | (.onDidChangeCursorPosition output-editor (partial on-elision-click project-name))) 84 | (add-subscription project-name 85 | (.onDidAddCursor output-editor (fn [cursor] (console-log "====Cursor===>" cursor)))))) 86 | 87 | (defn ^:private look-for-elisions 88 | "Searches inside the range for elisions and replaces them with ellipsis. It 89 | also decorates the ellipsis with markers and keeps the reference to them with 90 | the continuation function. 91 | 92 | Some examples to show output with elisions: 93 | (zipmap (range) (map char (range 0 200))) 94 | (flatten [0 1 2 [3 4 5 [6 7] [8 9 10]] 11 12 [13 [14 15 16] 17 18 [19]] 20]) 95 | (str \"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s.\")" 96 | [project-name changed-range] 97 | (let [{:keys [host-output-editor] {:keys [elision-regex elision-marker-layer]} :connection} (get-in @repls [project-name])] 98 | (.scanInBufferRange host-output-editor 99 | elision-regex 100 | changed-range 101 | (fn [result] 102 | (let [match (.-match result) 103 | range (.-range result) 104 | replace-text (.-replace result) 105 | marker (.markBufferRange elision-marker-layer range (js-obj "invalidate" "never")) 106 | continuation-fn (str (second match))] 107 | (replace-text ellipsis) 108 | (swap! repls update-in [project-name :connection :elisions] #(assoc % marker continuation-fn))))))) 109 | 110 | 111 | (defn ^:private on-elision-append 112 | "Searches for elisions inside the appended buffer. Since the change event 113 | gives us the range for each change that happened, we only need to look inside 114 | the range for elisions." 115 | [project-name event] 116 | (doseq [change (.-changes event)] 117 | (when-let [new-range (.-newRange change)] 118 | (look-for-elisions project-name new-range)))) 119 | 120 | (defn ^:private add-elision-append-handler 121 | "Watches any changes that happens to the output editor to check if any 122 | elisions have been appended." 123 | [project-name] 124 | (let [output-editor (get-in @repls [project-name :host-output-editor])] 125 | (add-subscription project-name 126 | (.onDidStopChanging output-editor (partial on-elision-append project-name))))) 127 | 128 | (defn ^:private add-elision-marker-layer [project-name] 129 | (let [{:keys [host-output-editor]} (get-in @repls [project-name]) 130 | marker-layer (.addMarkerLayer host-output-editor (js-obj "maintainHistory" true))] 131 | (.decorateMarkerLayer host-output-editor 132 | marker-layer 133 | (js-obj "type" "highlight" 134 | "class" "clojure-party-repl elisions")) 135 | (swap! repls update-in [project-name :connection] #(assoc % :elision-marker-layer marker-layer)))) 136 | 137 | (defmulti handle-unrepl-tuple 138 | (fn [project-name tuple] 139 | (if-not (vector? tuple) 140 | ::not-a-tuple 141 | (let [[tag payload group-id] tuple] 142 | tag)))) 143 | 144 | (defmethod handle-unrepl-tuple ::not-a-tuple [project-name thing] 145 | (repl/append-to-output-editor (pr-str thing) :add-newline? true)) 146 | 147 | (defmethod handle-unrepl-tuple :default [project-name [tag payload group-id :as tuple]] 148 | (console-log "Unhandled unrepl tuple:" (pr-str tuple))) 149 | 150 | (defmethod handle-unrepl-tuple :read [project-name [tag payload group-id :as tuple]] 151 | (console-log "Noop unrepl tuple:" (pr-str tuple))) 152 | 153 | (defmethod handle-unrepl-tuple :started-eval [project-name [tag payload group-id :as tuple]] 154 | (let [{{:keys [interrupt background]} :actions} payload] 155 | (console-log "Noop unrepl tuple:" (pr-str tuple)))) 156 | 157 | ;; TODO: Handle elision expansion for exception too. 158 | ;; NOTE: For string payload, it adds unnecessary space from joining the string 159 | ;; when the prefix doesn't end with a whitespace and the expanded string 160 | ;; doesn't start with a whitespace. 161 | (defmethod handle-unrepl-tuple :eval [project-name [tag payload group-id]] 162 | (if-let [elision-range (get-in @repls [project-name :connection :pending-elision-range])] 163 | (if (or (seq? payload) (vector? payload) (set? payload)) 164 | (repl/append-to-output-editor-at project-name (apply pr-str payload) elision-range :add-newline? false) 165 | (if (coll? payload) 166 | (repl/append-to-output-editor-at project-name (apply pr-str (flatten (into [] payload))) elision-range :add-newline? false) 167 | (when (string? payload) 168 | (repl/append-to-output-editor-at project-name (if (string/starts-with? (str payload) " ") 169 | (subs (str payload) 1) 170 | (str payload)) 171 | elision-range :add-newline? false)))) 172 | (repl/append-to-output-editor project-name (pr-str payload) :add-newline? true))) 173 | 174 | (defmethod handle-unrepl-tuple :bye [project-name [tag payload :as tuple]] 175 | (let [{:keys [reason outs actions]} payload] 176 | (console-log "Bye for now because of" reason))) 177 | 178 | (defmethod handle-unrepl-tuple :out [project-name [tag payload group-id :as tuple]] 179 | (console-log "Noop unrepl tuple:" (pr-str tuple))) 180 | 181 | (defmethod handle-unrepl-tuple :err [project-name [tag payload group-id :as tuple]] 182 | (console-log "Noop unrepl tuple:" (pr-str tuple))) 183 | 184 | ;; The exception has a shape of: 185 | ;; {:ex {:cause String 186 | ;; :via [{:type Exception 187 | ;; :message String 188 | ;; :at [StackTrace, String, at, {:get (continuation-fn)}]}, 189 | ;; ..., 190 | ;; {:get (continuation-fn)}] 191 | ;; :trace [[], [], ..., {:get (continuation-fn)}]}} 192 | ;; :phase :eval/} 193 | (defmethod handle-unrepl-tuple :exception [project-name [tag payload group-id :as tuple]] 194 | (let [{:keys [ex phase]} payload 195 | {:keys [cause via trace]} ex 196 | [{:keys [type _ _]} _] via] 197 | (repl/append-to-output-editor project-name 198 | (string/join " " [type cause (-> trace 199 | (get 0 []) 200 | (get 2 ""))]) 201 | :add-newline? true) 202 | (doseq [[_ _ at _] (vec (butlast (next trace)))] 203 | (when-not (coll? at) 204 | (repl/append-to-output-editor project-name 205 | (str \tab at) 206 | :add-newline? true))))) 207 | 208 | (defmethod handle-unrepl-tuple :prompt [project-name [tag payload]] 209 | (if-let [elision-range (get-in @repls [project-name :connection :pending-elision-range])] 210 | (swap! repls update-in [project-name :connection] #(assoc % :pending-elision-range nil)) 211 | (repl/append-to-output-editor project-name 212 | (str (get payload 'clojure.core/*ns*) 213 | "> ") 214 | :add-newline? false))) 215 | 216 | (defmethod handle-unrepl-tuple :unrepl.upgrade/failed [project-name [tag]] 217 | (repl/append-to-output-editor project-name "Unable to upgrade your REPL to Unrepl.")) 218 | 219 | (defmethod handle-unrepl-tuple :unrepl/hello [project-name [tag payload]] 220 | (let [{:keys [session actions]} payload 221 | {:keys [start-aux exit set-source :unrepl.jvm/start-side-loader]} actions 222 | {:keys [connection port host]} (get @repls project-name) 223 | unrepl-ns (.-ns (first exit))] 224 | (console-log "Upgraded to Unrepl: " session) 225 | (console-log "Available commands are: " (string/join " " [start-aux exit set-source start-side-loader])) 226 | (swap! repls update-in [project-name :connection] 227 | #(assoc % :actions {:exit (str exit) 228 | :set-source (str set-source)} 229 | :unrepl-ns unrepl-ns 230 | :unrepl-session (name session) 231 | :elisions {} 232 | :pending-elision-range nil 233 | :elision-regex (js/RegExp. (str "(?:\\{?\\.\\.\\.\\s+)?\\{\\:get\\s+(\\(" 234 | (string/escape unrepl-ns {\_ "\\_" \. "\\."}) 235 | "\\/fetch\\s+\\:[A-Za-z]+\\_\\_[0-9]+\\))\\}") "gm"))) 236 | (add-elision-click-handler project-name) 237 | (add-elision-append-handler project-name) 238 | (add-elision-marker-layer project-name))) 239 | 240 | (defn read-clojure-var [v] 241 | (symbol (str "#'" v))) 242 | 243 | (defn read-string 244 | "When string is too long, it's represented as a tuple containing the prefix 245 | and elisions. Elisions can be expanded by calling the continuation function." 246 | [string] 247 | (cond 248 | (string? string) (identity string) 249 | (vector? string) (let [[prefix elisions] string] 250 | (if (string/ends-with? prefix " ") 251 | (string/join "" string) 252 | (string/join " " string))))) 253 | 254 | (defn read-ratio [[a b]] 255 | (symbol (str a "/" b))) 256 | 257 | (defn read-object [[class id representation :as object]] 258 | (identity object)) 259 | 260 | (defn read-elision 261 | "Elision can either be nil or a map with the continuation function associated 262 | to the :get key." 263 | [elisions] 264 | (if (nil? elisions) 265 | (symbol (str "...")) 266 | (identity elisions))) 267 | 268 | (defn read-lazy-error 269 | "An exception #unrepl/lazy-error is inlined in a sequence when 270 | realization of a lazy sequence throws. 271 | 272 | For example, as stated in unrepl spec: 273 | (map #(/ %) (iterate dec 3)) 274 | will return 275 | (#unrepl/ratio [1 3] #unrepl/ratio [1 2] 1 #unrepl/lazy-error #error {:cause \"Divide by zero\", :via [{:type #unrepl.java/class java.lang.ArithmeticException, :message \"Divide by zero\", :at #unrepl/object [#unrepl.java/class java.lang.StackTraceElement \"0x272298a\" \"clojure.lang.Numbers.divide(Numbers.java:158)\"]}], :trace [#unrepl/... nil]}) 276 | 277 | TODO: Also show a stacktrace below the result?" 278 | [{:keys [cause via trace]}] 279 | (let [[{:keys [type message _]} _] via 280 | first-trace (-> trace 281 | (get 0 []) 282 | (get 2 ""))] 283 | (apply console-log cause) 284 | (identity [type message trace]))) 285 | 286 | (defn ^:private read-unrepl-stream 287 | "Read and process all the unrepl tuples in the given data string. 288 | 289 | For each supported tagged literal, we need to provide a reader function. If 290 | not provided, \"No reader function for tag error.\" will be thrown." 291 | [project-name data] 292 | (let [reader (string-push-back-reader (str data))] 293 | (loop [] 294 | (let [msg (read {:eof ::eof 295 | :readers {'clojure/var read-clojure-var 296 | 'unrepl/param identity 297 | 'unrepl/ns identity 298 | 'unrepl/string read-string 299 | 'unrepl/ratio read-ratio 300 | 'unrepl/meta identity 301 | 'unrepl/pattern identity 302 | 'unrepl/object read-object 303 | 'unrepl/mime identity 304 | 'unrepl.java/class identity 305 | 'unrepl/... read-elision 306 | 'unrepl/lazy-error read-lazy-error 307 | 'error identity}} 308 | reader)] 309 | (console-log (prn-str msg)) 310 | (when (not= msg ::eof) 311 | (handle-unrepl-tuple project-name msg) 312 | (recur)))))) 313 | 314 | (defn ^:private upgrade-connection-to-unrepl [project-name] 315 | (swap! repls update project-name #(assoc % :repl-type :repl-type/unrepl)) 316 | (send (get-in @repls [project-name :connection :socket-connection]) unrepl-blob)) 317 | 318 | (defn wrap-code-with-namespace 319 | "This removes the need to send a separate blob to check if the namespace 320 | exists or not. 321 | 322 | TODO: Use this for nrepl too?" 323 | [code namespace] 324 | (str "(do " 325 | "(when (clojure.core/find-ns '" namespace ")" 326 | "(ns " namespace "))" 327 | code ")\n")) 328 | 329 | (defmethod repl/execute-code :repl-type/unrepl 330 | [project-name code & [{:keys [namespace line column]}]] 331 | (let [{:keys [connection session current-ns]} (get @repls project-name) 332 | {:keys [socket-connection actions]} connection 333 | {:keys [set-source exit]} actions] 334 | (repl/append-to-output-editor project-name code :add-newline? true) 335 | (add-repl-history project-name code) 336 | (send socket-connection (string/replace set-source 337 | #":unrepl/sourcename|:unrepl/line|:unrepl/column" 338 | (if namespace 339 | {":unrepl/sourcename" (str "\"" namespace "\"") 340 | ":unrepl/line" line 341 | ":unrepl/column" column} 342 | {":unrepl/sourcename" (str "\"" "repl-entry" "\"") 343 | ":unrepl/line" 1 344 | ":unrepl/column" 1}))) 345 | (send socket-connection (if namespace 346 | (wrap-code-with-namespace code namespace) 347 | (str code "\n"))))) 348 | 349 | (defmethod repl/stop-process :repl-type/unrepl 350 | [project-name] 351 | (let [{:keys [connection repl-process]} (get @repls project-name) 352 | {:keys [socket-connection elision-marker-layer]} connection] 353 | (when socket-connection 354 | (.end socket-connection)) 355 | (when elision-marker-layer 356 | (.destroy elision-marker-layer)))) 357 | 358 | (defn connect-to-remote-plain-repl [{:keys [project-name host port]}] 359 | (let [conn (net.Socket.)] 360 | (.connect conn port host 361 | (fn [] 362 | (swap! repls update project-name 363 | #(assoc % :repl-type :repl-type/plain 364 | :connection {:socket-connection conn} 365 | :host host 366 | :port port)) 367 | (upgrade-connection-to-unrepl project-name))) 368 | (.on conn "data" 369 | (fn [data] 370 | (if (= :repl-type/unrepl (get-in @repls [project-name :repl-type])) 371 | (read-unrepl-stream project-name data) 372 | (repl/append-to-output-editor project-name (str data) :add-newline? false)))) 373 | (.on conn "error" 374 | (fn [error] 375 | (show-error error " Cannot connect to Socket Repl. Please make sure Socket Repl is running at port " port "."))) 376 | (.on conn "close" 377 | (fn [] (console-log "Socket connection closed"))))) 378 | -------------------------------------------------------------------------------- /styles/clojure-party-repl.less: -------------------------------------------------------------------------------- 1 | /** 2 | * The ui-variables file is provided by base themes provided by Atom. 3 | * 4 | * See https://github.com/atom/atom-dark-ui/blob/master/styles/ui-variables.less 5 | * for a full listing of what's available. 6 | */ 7 | @import "ui-variables"; 8 | 9 | .clojure-party-repl { 10 | 11 | } 12 | 13 | .repl-entry { 14 | 15 | } 16 | 17 | .repl-history { 18 | 19 | } 20 | 21 | .highlight.executed-top-level-form .region { 22 | background-color: rgba(100, 148, 237, 0.6); 23 | } 24 | 25 | .highlight.clojure-party-repl.elisions .region { 26 | background-color: rgba(100, 148, 237, 0.6); 27 | border-radius: 3px; 28 | cursor: pointer; 29 | } 30 | 31 | .clojure-party-repl.container { 32 | padding-bottom: @component-padding; 33 | } 34 | 35 | .clojure-party-repl.button-container { 36 | position: relative; 37 | height: @component-line-height; 38 | } 39 | 40 | .clojure-party-repl.cancel-button { 41 | position: absolute; 42 | left: 0; 43 | } 44 | 45 | .clojure-party-repl.proceed-button { 46 | position: absolute; 47 | right: 0; 48 | } 49 | --------------------------------------------------------------------------------