├── .gitignore ├── .tool-versions ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── config └── config.exs ├── jupyterlab-ielixir ├── .gitignore ├── README.md ├── package.json ├── src │ ├── codemirror-ielixir.ts │ └── index.ts ├── style │ └── index.css ├── tsconfig.json └── yarn.lock ├── lib ├── ielixir.ex └── ielixir │ ├── completion.ex │ ├── completion │ └── identifier_matcher.ex │ ├── display.ex │ ├── evaluator.ex │ ├── evaluator │ └── io_proxy.ex │ ├── init │ └── resources.ex │ ├── kernel │ ├── channels │ │ ├── control.ex │ │ ├── hb.ex │ │ ├── iopub.ex │ │ ├── shell.ex │ │ └── stdin.ex │ ├── command.ex │ ├── connection_file.ex │ ├── display.ex │ ├── history.ex │ ├── session.ex │ ├── socket │ │ ├── config.ex │ │ ├── pub.ex │ │ ├── rep.ex │ │ └── router.ex │ ├── supervisor.ex │ ├── wire.ex │ └── wire │ │ ├── message.ex │ │ ├── message_header.ex │ │ └── packet.ex │ ├── runtime.ex │ ├── runtime │ ├── elixir_standalone.ex │ ├── erl_dist.ex │ ├── erl_dist │ │ ├── evaluator_supervisor.ex │ │ ├── io_forward_gl.ex │ │ ├── logger_gl_backend.ex │ │ ├── node_manager.ex │ │ └── runtime_server.ex │ └── standalone_init.ex │ ├── util │ ├── crypto.ex │ ├── magic_command.ex │ ├── substring.ex │ └── tmp.done.ex │ ├── utils.ex │ └── utils │ ├── ansi.ex │ ├── emitter.ex │ ├── graph.ex │ └── time.ex ├── mix.exs ├── mix.lock ├── priv ├── kernel.js ├── kernel.json ├── logo-32x32.png └── logo-64x64.png ├── resources └── examples │ ├── Distributed portals with Elixir.ipynb │ ├── Plotting with VegaLite.ipynb │ ├── Welcome to IElixir.ipynb │ ├── images │ ├── portal-drop.jpeg │ └── portal-list.jpeg │ └── logo.png ├── shell.nix └── test └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /.idea 5 | erl_crash.dump 6 | *.ez 7 | *.iml 8 | /ielixir 9 | test_db.sqlite3 10 | .DS_store 11 | resources/ielixir/kernel.json 12 | 13 | .elixir_ls 14 | *.sqlite3 15 | envs 16 | 17 | **/.ipynb_checkpoints/ 18 | /*.ipynb 19 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 24.0 2 | elixir 1.12.0-otp-24 3 | python 3.9.7 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | https://t.me/proelixir. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM hexpm/elixir:1.12.3-erlang-24.0.6-ubuntu-xenial-20210114 2 | 3 | RUN apt-get update && apt-get install -y \ 4 | software-properties-common git curl \ 5 | && add-apt-repository ppa:deadsnakes/ppa \ 6 | && apt-get update && apt-get install -y \ 7 | python3.9 python3.9-distutils\ 8 | && curl -sL https://deb.nodesource.com/setup_12.x | bash - \ 9 | && apt-get install -y nodejs \ 10 | && rm -rf /var/lib/apt/lists/* \ 11 | # && ln -s /usr/bin/python3.9 /usr/bin/python \ 12 | && curl https://bootstrap.pypa.io/get-pip.py | python3.9 13 | 14 | 15 | RUN python3.9 -m pip install jupyterlab 16 | 17 | RUN jupyter labextension install jupyterlab-ielixir 18 | 19 | ARG time=0 20 | 21 | RUN mix local.hex --force && mix local.rebar --force \ 22 | && mix escript.install --force github spawnfest/ielixir \ 23 | && ln -s /root/.mix/escripts/ielixir /usr/bin/ielixir 24 | 25 | RUN ielixir install 26 | 27 | WORKDIR /workspace 28 | 29 | RUN mkdir -p /root/.jupyter/ && touch /root/.jupyter/jupyter_lab_config.py \ 30 | && printf "c.ServerApp.port = 8000\nc.ExtensionApp.open_browser = False\nc.ServerApp.ip = '0.0.0.0'\nc.ServerApp.open_browser = False\n" > /root/.jupyter/jupyter_lab_config.py 31 | 32 | EXPOSE 8000 33 | 34 | CMD [ "jupyter-lab", "--allow-root"] 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IElixir 2 | 3 | Jupyter's kernel for Elixir 4 | 5 | ## Acknowledgements, licenses, and disclaimers 6 | 7 | Implementation is inspired by [`IElixir`](https://github.com/pprzetacznik/IElixir), while all the codebase is totally rewritten to fit `escripts`, **Elixir**'s 1.12 `Mix.install/2` feature and kernel management introduced by [`livebook`](https://github.com/livebook-dev/livebook). 8 | 9 | All the code for node management is fully taken from [`livebook`](https://github.com/livebook-dev/livebook)'s codebase. 10 | 11 | This project is distributed under the `MIT` license, while node management parts are distributed under `Apache License 2.0`, inherited from [`livebook`](https://github.com/livebook-dev/livebook). 12 | 13 | ## Installation 14 | 15 | You must have: 16 | 17 | 1. One of the `jupyter`'s installations 18 | 1. **Elixir** 1.12 or higher 19 | 20 | ### Jupyter notebook 21 | 22 | To use this kernel, you need to have a `Jupyter` installed. You can find instructions on how to install it [here](https://jupyter.readthedocs.io/en/latest/install/notebook-classic.html). 23 | 24 | The simplest way to do this is to run: 25 | 26 | $ pip install jupyter 27 | 28 | Installing it with a virtual environment is recommended, but instructions to do it are out of this document's scope. 29 | 30 | ### Jupyter lab 31 | 32 | This frontend is a modern approach to work with **Jupyter**'s kernels, supports more features, and is recommended for been used for most cases. 33 | You can find instructions on how to install it [here](https://jupyterlab.readthedocs.io/en/stable/getting_started/installation.html). 34 | 35 | The simplest way to do this is to run: 36 | 37 | $ pip install jupyterlab 38 | 39 | This command will also install the original `jupyter notebook` under the hood. 40 | 41 | ### Elixir 42 | 43 | You need to have **Elixir** installed. You can find instructions on how to install it [here](https://elixir-lang.org/install.html). 44 | For sure you know how to do it if you are trying to use this project. 45 | 46 | ### IElixir kernel 47 | 48 | Installation is pretty simple: 49 | 50 | 1. Install the escript for this kernel: 51 | 52 | ```bash 53 | $ mix escript.install github spawnfest/ielixir 54 | ``` 55 | 56 | 1. Make the `jupyter` to _know_ our new kernel: 57 | 58 | ```bash 59 | $ ielixir install 60 | ``` 61 | 62 | 1. (_Optional_) If you want to use the `jupyterlab` frontend, you need to additional `ielixir` lablexicon: 63 | 64 | ```bash 65 | $ jupyter labextension install jupyterlab-ielixir 66 | ``` 67 | 68 | 69 | And you are all set! 70 | 71 | ## Running with docker 72 | 73 | 1. Build the docker image using presented `Dockerfile`: 74 | 75 | ```bash 76 | docker build . -t ielixir 77 | ``` 78 | 79 | 1. Run the image to get your server running on `8000` port: 80 | 81 | ```bash 82 | docker run -p 8000:8000 ielixir 83 | ``` 84 | 85 | 1. In docker's output you will see a link with defined token. Use it to open your `Jupyter Lab` in the browser. 86 | 87 | **NOTE!** Default workspace is empty! If you need to work with a workspace that exists on your host machine - 88 | you need to mount this folder inside containers `/workspace` folder. 89 | 90 | ```bash 91 | docker run --mount type=bind,source=path/to/workspace/,target=/workspace -p 8000:8000 ielixir 92 | ``` 93 | 94 | For example, to run examples from project's root use: 95 | 96 | ```bash 97 | docker run --mount type=bind,source="$(pwd)"/resources/examples,target=/workspace -p 8000:8000 ielixir 98 | ``` 99 | 100 | 101 | ## Roadmap 102 | 103 | - [x] `jupyter` messaging protocol 104 | - [ ] `Elixir` node for each kernel 105 | - [x] Standalone 106 | - [ ] `mix project` 107 | - [ ] `remote` 108 | - [x] Code highlighting for 109 | - [x] input 110 | - [x] output 111 | - [ ] History saving and exposing 112 | - [ ] Compatibility with 113 | - [x] `console` 114 | - [x] `notebook` 115 | - [x] `lab` 116 | - [ ] `vscode` extension 117 | - [x] Providing protocol for output decoration 118 | - [x] Client side 119 | - [x] kernel side 120 | - [x] Automaticly decorating 121 | - [x] pictures 122 | - [x] vega plots 123 | - [x] jsons 124 | - [ ] Autocompletion 125 | - [x] Intellisense 126 | - [ ] Perfect cursor positioning 127 | - [ ] Example notebooks 128 | - [x] Intro to Jupyter 129 | - [ ] Intro to Elixir inside IElixir 130 | - [ ] Intro to Elixir 131 | - [x] Example from Elixir's official syte 132 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | if Mix.env() == :prod do 4 | config :logger, 5 | level: :info 6 | else 7 | config :logger, 8 | level: :debug 9 | end 10 | -------------------------------------------------------------------------------- /jupyterlab-ielixir/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ -------------------------------------------------------------------------------- /jupyterlab-ielixir/README.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | ``` 4 | jupyter labextension install jupyterlab-ielixir 5 | ``` 6 | 7 | ## Development 8 | 9 | For a development install (requires npm version 4 or later), do the following in the repository directory: 10 | 11 | ``` 12 | npm install 13 | npm run build 14 | jupyter labextension link . 15 | ``` 16 | 17 | To rebuild the package and the JupyterLab app: 18 | 19 | ``` 20 | npm run build 21 | jupyter lab build 22 | ``` 23 | -------------------------------------------------------------------------------- /jupyterlab-ielixir/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jupyterlab-ielixir", 3 | "version": "0.0.1", 4 | "description": "adds ihaskell syntax highlighting to jupyterlab", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "ihaskell", 9 | "jupyterlab-extension" 10 | ], 11 | "homepage": "https://github.com/ilhub/ielixir/", 12 | "bugs": { 13 | "url": "https://github.com/ilhub/ielixir/issues" 14 | }, 15 | "license": "BSD-3-Clause", 16 | "author": "Virviil", 17 | "files": [ 18 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 19 | "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}" 20 | ], 21 | "main": "lib/index.js", 22 | "types": "lib/index.d.ts", 23 | "repository": { 24 | "type": "git", 25 | "url": "git@github.com:ilhub/ielixir.git" 26 | }, 27 | "scripts": { 28 | "build": "tsc", 29 | "clean": "rimraf lib", 30 | "watch": "tsc -w" 31 | }, 32 | "dependencies": { 33 | "@jupyterlab/application": ">=2.0.0", 34 | "@jupyterlab/apputils": ">=2.0.0", 35 | "@jupyterlab/docregistry": ">=2.0.0", 36 | "@jupyterlab/notebook": ">=2.0.0", 37 | "@jupyterlab/services": ">=3.0.1", 38 | "@lumino/disposable": "^1.1.2", 39 | "codemirror": "^5.61.0", 40 | "codemirror-mode-elixir": "^1.1.2" 41 | }, 42 | "devDependencies": { 43 | "rimraf": "^3.0.0", 44 | "typescript": "~3.7.3" 45 | }, 46 | "jupyterlab": { 47 | "extension": true 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /jupyterlab-ielixir/src/codemirror-ielixir.ts: -------------------------------------------------------------------------------- 1 | import * as CodeMirror from 'codemirror'; 2 | import "codemirror-mode-elixir"; 3 | 4 | CodeMirror.defineMode("ielixir", (config) => { 5 | let hmode = CodeMirror.getMode(config, "elixir"); 6 | return CodeMirror.multiplexingMode( 7 | hmode, 8 | { 9 | open: /:(?=!)/, // Matches : followed by !, but doesn't consume ! 10 | close: /^(?!!)/, // Matches start of line not followed by !, doesn't consume character 11 | mode: CodeMirror.getMode(config, "text/plain"), 12 | delimStyle: "delimit", 13 | }, 14 | { 15 | open: /\[r\||\[rprint\||\[rgraph\|/, 16 | close: /\|\]/, 17 | mode: CodeMirror.getMode(config, "text/x-rsrc"), 18 | delimStyle: "delimit", 19 | } 20 | ); 21 | }); 22 | 23 | CodeMirror.defineMIME("text/x-ielixir", "ielixir"); 24 | 25 | CodeMirror.modeInfo.push({ 26 | ext: ["ex"], 27 | mime: "text/x-ielixir", 28 | mode: "ielixir", 29 | name: "ielixir", 30 | }); 31 | -------------------------------------------------------------------------------- /jupyterlab-ielixir/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | JupyterFrontEnd, JupyterFrontEndPlugin 4 | } from '@jupyterlab/application'; 5 | 6 | import './codemirror-ielixir'; 7 | 8 | import '../style/index.css'; 9 | 10 | /** 11 | * Initialization data for the extension1 extension. 12 | */ 13 | const extension: JupyterFrontEndPlugin = { 14 | id: 'ielixir', 15 | autoStart: true, 16 | requires: [], 17 | activate: (app: JupyterFrontEnd) => 18 | { 19 | app.serviceManager.ready 20 | .then(() => {defineIElixir()}); 21 | } 22 | }; 23 | 24 | function defineIElixir() { 25 | console.log('ielixir codemirror activated'); 26 | } 27 | 28 | 29 | export default extension; 30 | -------------------------------------------------------------------------------- /jupyterlab-ielixir/style/index.css: -------------------------------------------------------------------------------- 1 | /* To be created custom styles */ -------------------------------------------------------------------------------- /jupyterlab-ielixir/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "noImplicitAny": false, 5 | "noEmitOnError": true, 6 | "noUnusedLocals": true, 7 | "allowSyntheticDefaultImports": true, 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "target": "es2015", 11 | "outDir": "./lib", 12 | "lib": ["dom", "es2015"], 13 | "types": [] 14 | }, 15 | "include": ["src/*"] 16 | } 17 | -------------------------------------------------------------------------------- /lib/ielixir.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir do 2 | require Logger 3 | 4 | alias IElixir.Init.Resources 5 | 6 | def main(["install" | _]) do 7 | at_tmp_folder(fn tmp_folder_path -> 8 | case which_jupyter() do 9 | {_, 0} -> 10 | Resources.generate_kernel(tmp_folder_path) 11 | case kernelspec(tmp_folder_path) do 12 | {_, 0} -> 13 | [:green, "All set! You are ready to start"] 14 | 15 | _ -> 16 | [:red, "Kernelspec is not valid (and it's probably a bug)! Kernel installation is failed"] 17 | end 18 | 19 | _ -> 20 | [:red, "Jupyter execution is not found! Kernel installation is failed"] 21 | end 22 | end) 23 | |> IO.ANSI.format() 24 | |> IO.puts() 25 | end 26 | 27 | def main(["serve", connection_file_path | _]) do 28 | # Dir where the kernel is started 29 | # Logger.debug(File.cwd()) 30 | Logger.debug("Starting IElixir kernel with connection_filed #{connection_file_path}") 31 | 32 | # Parsing connection file 33 | connection_data = 34 | connection_file_path 35 | |> IElixir.Kernel.ConnectionFile.parse() 36 | 37 | Logger.debug("Read connection data:\n#{inspect(connection_data)}") 38 | 39 | result = IElixir.Kernel.Supervisor.start_link(connection_data) 40 | Logger.debug("Supervision tree running results: #{inspect(result)}") 41 | 42 | :timer.sleep(:infinity) 43 | end 44 | 45 | defp kernelspec(tmp_folder_path) do 46 | System.cmd("jupyter", ["kernelspec", "install", "--user", "--replace", "--name=ielixir", tmp_folder_path]) 47 | end 48 | 49 | defp at_tmp_folder(fun) do 50 | case IElixir.Util.Tmp.mktemp() do 51 | {:ok, tmp_folder_path} -> 52 | try do 53 | fun.(tmp_folder_path) 54 | after 55 | File.rm_rf!(tmp_folder_path) 56 | end 57 | 58 | {:error, _} -> 59 | [:red, "Impossible to create temp folder! Kernel installation is failed"] 60 | end 61 | end 62 | 63 | defp which_jupyter() do 64 | System.cmd("which", ["jupyter"]) 65 | end 66 | 67 | end 68 | -------------------------------------------------------------------------------- /lib/ielixir/completion.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Completion do 2 | @moduledoc false 3 | 4 | # This module provides completion related operations 5 | # suitable for integration with a text editor. 6 | # 7 | # In a way, this provides the very basic features of a 8 | # language server that IElixir uses. 9 | 10 | alias IElixir.Completion.IdentifierMatcher 11 | 12 | # Configures width used for inspect and specs formatting. 13 | @line_length 30 14 | @extended_line_length 80 15 | 16 | @doc """ 17 | Resolves an completion request as defined by `IElixir.Runtime`. 18 | 19 | In practice this function simply dispatches the request to one of 20 | the other public functions in this module. 21 | """ 22 | @spec handle_request( 23 | IElixir.Runtime.completion_request(), 24 | Code.binding(), 25 | Macro.Env.t() 26 | ) :: IElixir.Runtime.completion_response() 27 | def handle_request(request, env, binding) 28 | 29 | def handle_request({:completion, hint}, binding, env) do 30 | items = get_completion_items(hint, binding, env) 31 | %{items: items} 32 | end 33 | 34 | def handle_request({:details, line, column}, binding, env) do 35 | get_details(line, column, binding, env) 36 | end 37 | 38 | def handle_request({:format, code}, _binding, _env) do 39 | case format_code(code) do 40 | {:ok, code} -> %{code: code} 41 | :error -> nil 42 | end 43 | end 44 | 45 | @doc """ 46 | Formats Elixir code. 47 | """ 48 | @spec format_code(String.t()) :: {:ok, String.t()} | :error 49 | def format_code(code) do 50 | try do 51 | formatted = 52 | code 53 | |> Code.format_string!() 54 | |> IO.iodata_to_binary() 55 | 56 | {:ok, formatted} 57 | rescue 58 | _ -> :error 59 | end 60 | end 61 | 62 | @doc """ 63 | Returns a list of completion suggestions for the given `hint`. 64 | """ 65 | @spec get_completion_items(String.t(), Code.binding(), Macro.Env.t()) :: 66 | list(IElixir.Runtime.completion_item()) 67 | def get_completion_items(hint, binding, env) do 68 | IdentifierMatcher.completion_identifiers(hint, binding, env) 69 | |> Enum.filter(&include_in_completion?/1) 70 | |> Enum.map(&format_completion_item/1) 71 | |> Enum.sort_by(&completion_item_priority/1) 72 | end 73 | 74 | defp include_in_completion?({:module, _module, _name, :hidden}), do: false 75 | defp include_in_completion?(_), do: true 76 | 77 | defp format_completion_item({:variable, name, value}), 78 | do: %{ 79 | label: name, 80 | kind: :variable, 81 | detail: "variable", 82 | documentation: value_snippet(value, @line_length), 83 | insert_text: name 84 | } 85 | 86 | defp format_completion_item({:map_field, name, value}), 87 | do: %{ 88 | label: name, 89 | kind: :field, 90 | detail: "field", 91 | documentation: value_snippet(value, @line_length), 92 | insert_text: name 93 | } 94 | 95 | defp format_completion_item({:module, module, name, doc_content}) do 96 | subtype = get_module_subtype(module) 97 | 98 | kind = 99 | case subtype do 100 | :protocol -> :interface 101 | :exception -> :struct 102 | :struct -> :struct 103 | :behaviour -> :interface 104 | _ -> :module 105 | end 106 | 107 | detail = Atom.to_string(subtype || :module) 108 | 109 | %{ 110 | label: name, 111 | kind: kind, 112 | detail: detail, 113 | documentation: format_doc_content(doc_content, :short), 114 | insert_text: String.trim_leading(name, ":") 115 | } 116 | end 117 | 118 | defp format_completion_item({:function, module, name, arity, doc_content, signatures, spec}), 119 | do: %{ 120 | label: "#{name}/#{arity}", 121 | kind: :function, 122 | detail: format_signatures(signatures, module), 123 | documentation: 124 | join_with_newlines([ 125 | format_doc_content(doc_content, :short), 126 | format_spec(spec, @line_length) |> code() 127 | ]), 128 | insert_text: name 129 | } 130 | 131 | defp format_completion_item({:type, _module, name, arity, doc_content}), 132 | do: %{ 133 | label: "#{name}/#{arity}", 134 | kind: :type, 135 | detail: "typespec", 136 | documentation: format_doc_content(doc_content, :short), 137 | insert_text: name 138 | } 139 | 140 | defp format_completion_item({:module_attribute, name, doc_content}), 141 | do: %{ 142 | label: name, 143 | kind: :variable, 144 | detail: "module attribute", 145 | documentation: format_doc_content(doc_content, :short), 146 | insert_text: name 147 | } 148 | 149 | defp completion_item_priority(completion_item) do 150 | {completion_item_kind_priority(completion_item.kind), completion_item.label} 151 | end 152 | 153 | @ordered_kinds [:field, :variable, :module, :struct, :interface, :function, :type] 154 | 155 | defp completion_item_kind_priority(kind) when kind in @ordered_kinds do 156 | Enum.find_index(@ordered_kinds, &(&1 == kind)) 157 | end 158 | 159 | @doc """ 160 | Returns detailed information about an identifier located 161 | in `column` in `line`. 162 | """ 163 | @spec get_details(String.t(), pos_integer(), Code.binding(), Macro.Env.t()) :: 164 | IElixir.Runtime.details() | nil 165 | def get_details(line, column, binding, env) do 166 | case IdentifierMatcher.locate_identifier(line, column, binding, env) do 167 | %{matches: []} -> 168 | nil 169 | 170 | %{matches: matches, range: range} -> 171 | contents = 172 | matches 173 | |> Enum.map(&format_details_item/1) 174 | |> Enum.uniq() 175 | 176 | %{range: range, contents: contents} 177 | end 178 | end 179 | 180 | defp format_details_item({:variable, name, value}) do 181 | join_with_divider([ 182 | code(name), 183 | value_snippet(value, @extended_line_length) 184 | ]) 185 | end 186 | 187 | defp format_details_item({:map_field, _name, value}) do 188 | join_with_divider([ 189 | value_snippet(value, @extended_line_length) 190 | ]) 191 | end 192 | 193 | defp format_details_item({:module, _module, name, doc_content}) do 194 | join_with_divider([ 195 | code(name), 196 | format_doc_content(doc_content, :all) 197 | ]) 198 | end 199 | 200 | defp format_details_item({:function, module, _name, _arity, doc_content, signatures, spec}) do 201 | join_with_divider([ 202 | format_signatures(signatures, module) |> code(), 203 | format_spec(spec, @extended_line_length) |> code(), 204 | format_doc_content(doc_content, :all) 205 | ]) 206 | end 207 | 208 | defp format_details_item({:type, _module, name, _arity, doc_content}) do 209 | join_with_divider([ 210 | code(name), 211 | format_doc_content(doc_content, :all) 212 | ]) 213 | end 214 | 215 | defp format_details_item({:module_attribute, name, doc_content}) do 216 | join_with_divider([ 217 | code("@" <> name), 218 | format_doc_content(doc_content, :all) 219 | ]) 220 | end 221 | 222 | defp get_module_subtype(module) do 223 | cond do 224 | module_has_function?(module, :__protocol__, 1) -> 225 | :protocol 226 | 227 | module_has_function?(module, :__impl__, 1) -> 228 | :implementation 229 | 230 | module_has_function?(module, :__struct__, 0) -> 231 | if module_has_function?(module, :exception, 1) do 232 | :exception 233 | else 234 | :struct 235 | end 236 | 237 | module_has_function?(module, :behaviour_info, 1) -> 238 | :behaviour 239 | 240 | true -> 241 | nil 242 | end 243 | end 244 | 245 | defp module_has_function?(module, func, arity) do 246 | Code.ensure_loaded?(module) and function_exported?(module, func, arity) 247 | end 248 | 249 | # Formatting helpers 250 | 251 | defp join_with_divider(strings), do: join_with(strings, "\n\n---\n\n") 252 | 253 | defp join_with_newlines(strings), do: join_with(strings, "\n\n") 254 | 255 | defp join_with(strings, joiner) do 256 | case Enum.reject(strings, &is_nil/1) do 257 | [] -> nil 258 | parts -> Enum.join(parts, joiner) 259 | end 260 | end 261 | 262 | defp code(nil), do: nil 263 | 264 | defp code(code) do 265 | """ 266 | ``` 267 | #{code} 268 | ```\ 269 | """ 270 | end 271 | 272 | defp value_snippet(value, line_length) do 273 | """ 274 | ``` 275 | #{inspect(value, pretty: true, width: line_length)} 276 | ```\ 277 | """ 278 | end 279 | 280 | defp format_signatures([], _module), do: nil 281 | 282 | defp format_signatures(signatures, module) do 283 | signatures_string = Enum.join(signatures, "\n") 284 | 285 | # Don't add module prefix to operator signatures 286 | if :binary.match(signatures_string, ["(", "/"]) != :nomatch do 287 | module_to_prefix(module) <> signatures_string 288 | else 289 | signatures_string 290 | end 291 | end 292 | 293 | defp module_to_prefix(mod) do 294 | case Atom.to_string(mod) do 295 | "Elixir." <> name -> name <> "." 296 | name -> ":" <> name <> "." 297 | end 298 | end 299 | 300 | defp format_spec(nil, _line_length), do: nil 301 | 302 | defp format_spec({{name, _arity}, spec_ast_list}, line_length) do 303 | spec_lines = 304 | Enum.map(spec_ast_list, fn spec_ast -> 305 | spec = 306 | Code.Typespec.spec_to_quoted(name, spec_ast) 307 | |> Macro.to_string() 308 | |> Code.format_string!(line_length: line_length) 309 | 310 | ["@spec ", spec] 311 | end) 312 | 313 | spec_lines 314 | |> Enum.intersperse("\n") 315 | |> IO.iodata_to_binary() 316 | end 317 | 318 | defp format_doc_content(doc, variant) 319 | 320 | defp format_doc_content(nil, _variant) do 321 | "No documentation available" 322 | end 323 | 324 | defp format_doc_content(:hidden, _variant) do 325 | "This is a private API" 326 | end 327 | 328 | defp format_doc_content({"text/markdown", markdown}, :short) do 329 | # Extract just the first paragraph 330 | markdown 331 | |> String.split("\n\n") 332 | |> hd() 333 | |> String.trim() 334 | end 335 | 336 | defp format_doc_content({"application/erlang+html", erlang_html}, :short) do 337 | # Extract just the first paragraph 338 | erlang_html 339 | |> Enum.find(&match?({:p, _, _}, &1)) 340 | |> case do 341 | nil -> nil 342 | paragraph -> erlang_html_to_md([paragraph]) 343 | end 344 | end 345 | 346 | defp format_doc_content({"text/markdown", markdown}, :all) do 347 | markdown 348 | end 349 | 350 | defp format_doc_content({"application/erlang+html", erlang_html}, :all) do 351 | erlang_html_to_md(erlang_html) 352 | end 353 | 354 | defp format_doc_content({format, _content}, _variant) do 355 | raise "unknown documentation format #{inspect(format)}" 356 | end 357 | 358 | # Erlang HTML AST 359 | # See https://erlang.org/doc/apps/erl_docgen/doc_storage.html#erlang-documentation-format 360 | 361 | def erlang_html_to_md(ast) do 362 | build_md([], ast) 363 | |> IO.iodata_to_binary() 364 | |> String.trim() 365 | end 366 | 367 | defp build_md(iodata, ast) 368 | 369 | defp build_md(iodata, []), do: iodata 370 | 371 | defp build_md(iodata, [string | ast]) when is_binary(string) do 372 | string |> append_inline(iodata) |> build_md(ast) 373 | end 374 | 375 | defp build_md(iodata, [{tag, _, content} | ast]) when tag in [:em, :i] do 376 | render_emphasis(content) |> append_inline(iodata) |> build_md(ast) 377 | end 378 | 379 | defp build_md(iodata, [{tag, _, content} | ast]) when tag in [:strong, :b] do 380 | render_strong(content) |> append_inline(iodata) |> build_md(ast) 381 | end 382 | 383 | defp build_md(iodata, [{:code, _, content} | ast]) do 384 | render_code_inline(content) |> append_inline(iodata) |> build_md(ast) 385 | end 386 | 387 | defp build_md(iodata, [{:a, attrs, content} | ast]) do 388 | render_link(content, attrs) |> append_inline(iodata) |> build_md(ast) 389 | end 390 | 391 | defp build_md(iodata, [{:br, _, []} | ast]) do 392 | render_line_break() |> append_inline(iodata) |> build_md(ast) 393 | end 394 | 395 | defp build_md(iodata, [{:p, _, content} | ast]) do 396 | render_paragraph(content) |> append_block(iodata) |> build_md(ast) 397 | end 398 | 399 | @headings ~w(h1 h2 h3 h4 h5 h6)a 400 | 401 | defp build_md(iodata, [{tag, _, content} | ast]) when tag in @headings do 402 | n = 1 + Enum.find_index(@headings, &(&1 == tag)) 403 | render_heading(n, content) |> append_block(iodata) |> build_md(ast) 404 | end 405 | 406 | defp build_md(iodata, [{:pre, _, [{:code, _, [content]}]} | ast]) do 407 | render_code_block(content, "erlang") |> append_block(iodata) |> build_md(ast) 408 | end 409 | 410 | defp build_md(iodata, [{:div, [{:class, class} | _], content} | ast]) do 411 | type = class |> to_string() |> String.upcase() 412 | 413 | render_blockquote([{:p, [], [{:strong, [], [type]}]} | content]) 414 | |> append_block(iodata) 415 | |> build_md(ast) 416 | end 417 | 418 | defp build_md(iodata, [{:ul, [{:class, "types"} | _], content} | ast]) do 419 | lines = Enum.map(content, fn {:li, _, line} -> line end) 420 | render_code_block(lines, "erlang") |> append_block(iodata) |> build_md(ast) 421 | end 422 | 423 | defp build_md(iodata, [{:ul, _, content} | ast]) do 424 | render_unordered_list(content) |> append_block(iodata) |> build_md(ast) 425 | end 426 | 427 | defp build_md(iodata, [{:ol, _, content} | ast]) do 428 | render_ordered_list(content) |> append_block(iodata) |> build_md(ast) 429 | end 430 | 431 | defp build_md(iodata, [{:dl, _, content} | ast]) do 432 | render_description_list(content) |> append_block(iodata) |> build_md(ast) 433 | end 434 | 435 | defp append_inline(md, iodata), do: [iodata, md] 436 | defp append_block(md, iodata), do: [iodata, "\n", md, "\n"] 437 | 438 | # Renderers 439 | 440 | defp render_emphasis(content) do 441 | ["*", build_md([], content), "*"] 442 | end 443 | 444 | defp render_strong(content) do 445 | ["**", build_md([], content), "**"] 446 | end 447 | 448 | defp render_code_inline(content) do 449 | ["`", build_md([], content), "`"] 450 | end 451 | 452 | defp render_link(content, attrs) do 453 | caption = build_md([], content) 454 | 455 | if href = attrs[:href] do 456 | ["[", caption, "](", href, ")"] 457 | else 458 | caption 459 | end 460 | end 461 | 462 | defp render_line_break(), do: "\\\n" 463 | 464 | defp render_paragraph(content), do: erlang_html_to_md(content) 465 | 466 | defp render_heading(n, content) do 467 | title = build_md([], content) 468 | [String.duplicate("#", n), " ", title] 469 | end 470 | 471 | defp render_code_block(content, language) do 472 | ["```", language, "\n", content, "\n```"] 473 | end 474 | 475 | defp render_blockquote(content) do 476 | inner = erlang_html_to_md(content) 477 | 478 | inner 479 | |> String.split("\n") 480 | |> Enum.map_intersperse("\n", &["> ", &1]) 481 | end 482 | 483 | defp render_unordered_list(content) do 484 | marker_fun = fn _index -> "* " end 485 | render_list(content, marker_fun, " ") 486 | end 487 | 488 | defp render_ordered_list(content) do 489 | marker_fun = fn index -> "#{index + 1}. " end 490 | render_list(content, marker_fun, " ") 491 | end 492 | 493 | defp render_list(items, marker_fun, indent) do 494 | spaced? = spaced_list_items?(items) 495 | item_separator = if(spaced?, do: "\n\n", else: "\n") 496 | 497 | items 498 | |> Enum.map(fn {:li, _, content} -> erlang_html_to_md(content) end) 499 | |> Enum.with_index() 500 | |> Enum.map(fn {inner, index} -> 501 | [first_line | lines] = String.split(inner, "\n") 502 | 503 | first_line = marker_fun.(index) <> first_line 504 | 505 | lines = 506 | Enum.map(lines, fn 507 | "" -> "" 508 | line -> indent <> line 509 | end) 510 | 511 | Enum.intersperse([first_line | lines], "\n") 512 | end) 513 | |> Enum.intersperse(item_separator) 514 | end 515 | 516 | defp spaced_list_items?([{:li, _, [{:p, _, _content} | _]} | _items]), do: true 517 | defp spaced_list_items?([_ | items]), do: spaced_list_items?(items) 518 | defp spaced_list_items?([]), do: false 519 | 520 | defp render_description_list(content) do 521 | # Rewrite description list as an unordered list with pseudo heading 522 | content 523 | |> Enum.chunk_every(2) 524 | |> Enum.map(fn [{:dt, _, dt}, {:dd, _, dd}] -> 525 | {:li, [], [{:p, [], [{:strong, [], dt}]}, {:p, [], dd}]} 526 | end) 527 | |> render_unordered_list() 528 | end 529 | end 530 | -------------------------------------------------------------------------------- /lib/ielixir/display.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Display do 2 | @moduledoc """ 3 | This module is to be propogated on target machine. 4 | """ 5 | 6 | def display({:ok, :"do not show this result in output"} = v), do: v 7 | 8 | def display({:ok, value}) do 9 | {:ok, IElixir.Displayable.display(value)} 10 | end 11 | 12 | def display(other), do: other 13 | 14 | end 15 | 16 | defprotocol IElixir.Displayable do 17 | @fallback_to_any true 18 | 19 | @spec display(t) :: IElixir.Display.t() | term 20 | def display(value) 21 | end 22 | 23 | defimpl IElixir.Displayable, for: Any do 24 | @spec display(any) :: IElixir.Display.t() | term 25 | # For the first run - do nothing with data. If user define something - he must return IElixir.Display.t() stuff 26 | def display(value), do: value 27 | end 28 | -------------------------------------------------------------------------------- /lib/ielixir/evaluator.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Evaluator do 2 | 3 | alias IElixir.Completion 4 | alias IElixir.Display 5 | alias IElixir.Evaluator.IOProxy 6 | 7 | @doc """ 8 | Starts the evaluator. 9 | """ 10 | def start_link(opts \\ []) do 11 | case :proc_lib.start_link(__MODULE__, :init, [opts]) do 12 | {:error, error} -> {:error, error} 13 | evaluator -> {:ok, evaluator.pid, evaluator} 14 | end 15 | end 16 | 17 | def evaluate_code(evaluator, send_to, code, cell, opts \\ []) do 18 | cast(evaluator, {:evaluate_code, send_to, code, cell, opts}) 19 | end 20 | 21 | def handle_completion(evaluator, send_to, code, cursor, cell) do 22 | cast(evaluator, {:handle_completion, send_to, code, cursor, cell}) 23 | end 24 | 25 | defp handle_cast({:evaluate_code, send_to, code, cell, opts}, %{context: context} = state) do 26 | IOProxy.configure(state.io_proxy, send_to, cell) 27 | file = Keyword.get(opts, :file, "nofile") 28 | 29 | context = put_in(context.env.file, file) 30 | start_time = System.monotonic_time() 31 | 32 | {context, response} = 33 | case eval(code, context) do 34 | {:ok, result, context} -> 35 | response = {:ok, result} 36 | {context, response} 37 | 38 | {:error, kind, error, stacktrace} -> 39 | response = {:error, kind, error, stacktrace} 40 | {context, response} 41 | end 42 | 43 | update_history(cell, response) 44 | send(send_to, {:log, :debug, "Evaluation response is #{inspect(response)}"}) 45 | 46 | evaluation_time_ms = get_execution_time_delta(start_time) 47 | 48 | IOProxy.flush(state.io_proxy) 49 | IOProxy.clear_input_buffers(state.io_proxy) 50 | 51 | output = Display.display(response) 52 | metadata = %{evaluation_time_ms: evaluation_time_ms} 53 | send(send_to, {:evaluation_response, cell, output, metadata}) 54 | 55 | {:noreply, %{state | context: context}} 56 | end 57 | 58 | defp handle_cast({:handle_completion, send_to, code, cursor, cell}, %{context: context} = state) do 59 | << code :: binary-size(cursor), _ :: binary() >> = code 60 | # Safely rescue from completion errors 61 | %{items: items} = 62 | try do 63 | Completion.handle_request({:completion, code}, context.binding, context.env) 64 | rescue 65 | error -> 66 | send(send_to, {:log, :error, Exception.format(:error, error, __STACKTRACE__)}) 67 | end 68 | 69 | matches = for %{insert_text: text} <- items, do: text 70 | send(send_to, {:completion_response, cell, matches, cursor, cursor}) 71 | 72 | {:noreply, state} 73 | end 74 | 75 | def child_spec(opts) do 76 | %{ 77 | id: __MODULE__, 78 | start: {__MODULE__, :start_link, [opts]}, 79 | type: :worker, 80 | restart: :temporary 81 | } 82 | end 83 | 84 | def init(opts) do 85 | {:ok, io_proxy} = IOProxy.start_link() 86 | 87 | # Use the dedicated IO device as the group leader, 88 | # so that it handles all :stdio operations. 89 | Process.group_leader(self(), io_proxy) 90 | 91 | evaluator_ref = make_ref() 92 | state = initial_state(evaluator_ref, io_proxy) 93 | evaluator = %{pid: self(), ref: evaluator_ref} 94 | 95 | :proc_lib.init_ack(evaluator) 96 | 97 | Process.put(:iex_history, IEx.History.init()) 98 | 99 | loop(state) 100 | end 101 | 102 | defp cast(evaluator, message) do 103 | send(evaluator.pid, {:__ielixir__cast__, evaluator.ref, message}) 104 | :ok 105 | end 106 | 107 | defp initial_state(evaluator_ref, io_proxy) do 108 | %{ 109 | evaluator_ref: evaluator_ref, 110 | io_proxy: io_proxy, 111 | context: initial_context() 112 | } 113 | end 114 | 115 | defp initial_context() do 116 | env = :elixir.env_for_eval([]) 117 | code = "import IEx.Helpers, except: [clear: 0, recompile: 0]" 118 | {:ok, _, context} = eval(code, %{binding: [], env: env}) 119 | context 120 | end 121 | 122 | defp loop(%{evaluator_ref: evaluator_ref} = state) do 123 | receive do 124 | # {:call, ^evaluator_ref, pid, ref, message} -> 125 | # {:reply, reply, state} = handle_call(message, pid, state) 126 | # send(pid, {ref, reply}) 127 | # loop(state) 128 | 129 | {:__ielixir__cast__, ^evaluator_ref, message} -> 130 | {:noreply, state} = handle_cast(message, state) 131 | loop(state) 132 | end 133 | end 134 | 135 | defp eval(code, %{binding: binding, env: env}) do 136 | try do 137 | quoted = Code.string_to_quoted!(code) 138 | {result, binding, env} = :elixir.eval_quoted(quoted, binding, env) 139 | {:ok, result, %{binding: binding, env: env}} 140 | catch 141 | kind, error -> 142 | {kind, error, stacktrace} = prepare_error(kind, error, __STACKTRACE__) 143 | {:error, kind, error, stacktrace} 144 | end 145 | end 146 | 147 | defp prepare_error(kind, error, stacktrace) do 148 | {error, stacktrace} = Exception.blame(kind, error, stacktrace) 149 | stacktrace = prune_stacktrace(stacktrace) 150 | {kind, error, stacktrace} 151 | end 152 | 153 | # Adapted from https://github.com/elixir-lang/elixir/blob/1c1654c88adfdbef38ff07fc30f6fbd34a542c07/lib/iex/lib/iex/evaluator.ex#L355-L372 154 | 155 | @elixir_internals [:elixir, :elixir_expand, :elixir_compiler, :elixir_module] ++ 156 | [:elixir_clauses, :elixir_lexical, :elixir_def, :elixir_map] ++ 157 | [:elixir_erl, :elixir_erl_clauses, :elixir_erl_pass] 158 | 159 | defp prune_stacktrace(stacktrace) do 160 | # The order in which each drop_while is listed is important. 161 | # For example, the user may call Code.eval_string/2 in their code 162 | # and if there is an error we should not remove erl_eval 163 | # and eval_bits information from the user stacktrace. 164 | stacktrace 165 | |> Enum.reverse() 166 | |> Enum.drop_while(&(elem(&1, 0) == :proc_lib)) 167 | |> Enum.drop_while(&(elem(&1, 0) == :gen_server)) 168 | |> Enum.drop_while(&(elem(&1, 0) == __MODULE__)) 169 | |> Enum.drop_while(&(elem(&1, 0) == :elixir)) 170 | |> Enum.drop_while(&(elem(&1, 0) in [:erl_eval, :eval_bits])) 171 | |> Enum.reverse() 172 | |> Enum.reject(&(elem(&1, 0) in @elixir_internals)) 173 | end 174 | 175 | defp get_execution_time_delta(started_at) do 176 | System.monotonic_time() 177 | |> Kernel.-(started_at) 178 | |> System.convert_time_unit(:native, :millisecond) 179 | end 180 | 181 | defp update_history(cell, response) do 182 | resp = 183 | case response do 184 | {:ok, resp} -> resp 185 | _ -> nil 186 | end 187 | 188 | iex_history = 189 | :iex_history 190 | |> Process.get() 191 | |> IEx.History.append({cell, resp}, 1000) 192 | 193 | Process.put(:iex_history, iex_history) 194 | end 195 | 196 | end 197 | -------------------------------------------------------------------------------- /lib/ielixir/evaluator/io_proxy.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Evaluator.IOProxy do 2 | @moduledoc false 3 | 4 | # An IO device process used by `Evaluator` as its `:stdio`. 5 | # 6 | # The process implements [The Erlang I/O Protocol](https://erlang.org/doc/apps/stdlib/io_protocol.html) 7 | # and can be thought of as a *virtual* IO device. 8 | # 9 | # Upon receiving an IO requests, the process sends a message 10 | # the `target` process specified during initialization. 11 | # Currently only output requests are supported. 12 | # 13 | # The implementation is based on the built-in `StringIO`, 14 | # so check it out for more reference. 15 | 16 | use GenServer 17 | 18 | alias IElixir.Evaluator 19 | 20 | ## API 21 | 22 | @doc """ 23 | Starts the IO device process. 24 | 25 | Make sure to use `configure/3` to actually proxy the requests. 26 | """ 27 | @spec start_link() :: GenServer.on_start() 28 | def start_link(opts \\ []) do 29 | GenServer.start_link(__MODULE__, opts) 30 | end 31 | 32 | @doc """ 33 | Sets IO proxy destination and the reference to be attached to all messages. 34 | 35 | For all supported requests a message is sent to `target`, 36 | so this device serves as a proxy. The given evaluation 37 | reference (`ref`) is also sent in all messages. 38 | 39 | The possible messages are: 40 | 41 | * `{:evaluation_output, ref, string}` - for output requests, 42 | where `ref` is the given evaluation reference and `string` is the output. 43 | """ 44 | @spec configure(pid(), pid(), Evaluator.ref()) :: :ok 45 | def configure(pid, target, ref) do 46 | GenServer.cast(pid, {:configure, target, ref}) 47 | end 48 | 49 | @doc """ 50 | Synchronously sends all buffer contents to the configured target process. 51 | """ 52 | @spec flush(pid()) :: :ok 53 | def flush(pid) do 54 | GenServer.call(pid, :flush) 55 | end 56 | 57 | @doc """ 58 | Asynchronously clears all buffered inputs, so next time they 59 | are requested again. 60 | """ 61 | @spec clear_input_buffers(pid()) :: :ok 62 | def clear_input_buffers(pid) do 63 | GenServer.cast(pid, :clear_input_buffers) 64 | end 65 | 66 | @doc """ 67 | Returns the accumulated widget pids and clears the accumulator. 68 | """ 69 | @spec flush_widgets(pid()) :: MapSet.t(pid()) 70 | def flush_widgets(pid) do 71 | GenServer.call(pid, :flush_widgets) 72 | end 73 | 74 | ## Callbacks 75 | 76 | @impl true 77 | def init(_opts) do 78 | {:ok, 79 | %{ 80 | encoding: :unicode, 81 | target: nil, 82 | ref: nil, 83 | buffer: [], 84 | input_buffers: %{}, 85 | widget_pids: MapSet.new() 86 | }} 87 | end 88 | 89 | @impl true 90 | def handle_cast({:configure, target, ref}, state) do 91 | {:noreply, %{state | target: target, ref: ref}} 92 | end 93 | 94 | def handle_cast(:clear_input_buffers, state) do 95 | {:noreply, %{state | input_buffers: %{}}} 96 | end 97 | 98 | @impl true 99 | def handle_call(:flush, _from, state) do 100 | {:reply, :ok, flush_buffer(state)} 101 | end 102 | 103 | def handle_call(:flush_widgets, _from, state) do 104 | {:reply, state.widget_pids, %{state | widget_pids: MapSet.new()}} 105 | end 106 | 107 | @impl true 108 | def handle_info({:io_request, from, reply_as, req}, state) do 109 | {reply, state} = io_request(req, state) 110 | io_reply(from, reply_as, reply) 111 | {:noreply, state} 112 | end 113 | 114 | def handle_info(:flush, state) do 115 | {:noreply, flush_buffer(state)} 116 | end 117 | 118 | defp io_request({:put_chars, chars} = req, state) do 119 | put_chars(:latin1, chars, req, state) 120 | end 121 | 122 | defp io_request({:put_chars, mod, fun, args} = req, state) do 123 | put_chars(:latin1, apply(mod, fun, args), req, state) 124 | end 125 | 126 | defp io_request({:put_chars, encoding, chars} = req, state) do 127 | put_chars(encoding, chars, req, state) 128 | end 129 | 130 | defp io_request({:put_chars, encoding, mod, fun, args} = req, state) do 131 | put_chars(encoding, apply(mod, fun, args), req, state) 132 | end 133 | 134 | defp io_request({:get_chars, prompt, count}, state) when count >= 0 do 135 | get_chars(:latin1, prompt, count, state) 136 | end 137 | 138 | defp io_request({:get_chars, encoding, prompt, count}, state) when count >= 0 do 139 | get_chars(encoding, prompt, count, state) 140 | end 141 | 142 | defp io_request({:get_line, prompt}, state) do 143 | get_line(:latin1, prompt, state) 144 | end 145 | 146 | defp io_request({:get_line, encoding, prompt}, state) do 147 | get_line(encoding, prompt, state) 148 | end 149 | 150 | defp io_request({:get_until, prompt, mod, fun, args}, state) do 151 | get_until(:latin1, prompt, mod, fun, args, state) 152 | end 153 | 154 | defp io_request({:get_until, encoding, prompt, mod, fun, args}, state) do 155 | get_until(encoding, prompt, mod, fun, args, state) 156 | end 157 | 158 | defp io_request({:get_password, _encoding}, state) do 159 | {{:error, :enotsup}, state} 160 | end 161 | 162 | defp io_request({:setopts, [encoding: encoding]}, state) when encoding in [:latin1, :unicode] do 163 | {:ok, %{state | encoding: encoding}} 164 | end 165 | 166 | defp io_request({:setopts, _opts}, state) do 167 | {{:error, :enotsup}, state} 168 | end 169 | 170 | defp io_request(:getopts, state) do 171 | {[binary: true, encoding: state.encoding], state} 172 | end 173 | 174 | defp io_request({:get_geometry, :columns}, state) do 175 | {{:error, :enotsup}, state} 176 | end 177 | 178 | defp io_request({:get_geometry, :rows}, state) do 179 | {{:error, :enotsup}, state} 180 | end 181 | 182 | defp io_request({:requests, reqs}, state) do 183 | io_requests(reqs, {:ok, state}) 184 | end 185 | 186 | defp io_request(_, state) do 187 | {{:error, :request}, state} 188 | end 189 | 190 | defp io_requests([req | rest], {:ok, state}) do 191 | io_requests(rest, io_request(req, state)) 192 | end 193 | 194 | defp io_requests(_, result) do 195 | result 196 | end 197 | 198 | defp put_chars(encoding, chars, req, state) do 199 | case :unicode.characters_to_binary(chars, encoding, state.encoding) do 200 | string when is_binary(string) -> 201 | if state.buffer == [] do 202 | Process.send_after(self(), :flush, 50) 203 | end 204 | 205 | {:ok, update_in(state.buffer, &buffer_append(&1, string))} 206 | 207 | {_, _, _} -> 208 | {{:error, req}, state} 209 | end 210 | rescue 211 | ArgumentError -> {{:error, req}, state} 212 | end 213 | 214 | defp get_line(encoding, prompt, state) do 215 | get_consume(encoding, prompt, state, fn input -> 216 | line_from_input(input) 217 | end) 218 | end 219 | 220 | defp get_chars(encoding, prompt, count, state) do 221 | get_consume(encoding, prompt, state, fn input -> 222 | chars_from_input(input, encoding, count) 223 | end) 224 | end 225 | 226 | defp get_until(encoding, prompt, mod, fun, args, state) do 227 | get_consume(encoding, prompt, state, fn input -> 228 | get_until_from_input(input, encoding, mod, fun, args) 229 | end) 230 | end 231 | 232 | defp get_consume(encoding, prompt, state, consume_fun) do 233 | prompt = :unicode.characters_to_binary(prompt, encoding, state.encoding) 234 | 235 | case get_input(prompt, state) do 236 | input when is_binary(input) -> 237 | {chars, rest} = consume_fun.(input) 238 | state = put_in(state.input_buffers[prompt], rest) 239 | {chars, state} 240 | 241 | error -> 242 | {error, state} 243 | end 244 | end 245 | 246 | defp get_input(prompt, state) do 247 | Map.get_lazy(state.input_buffers, prompt, fn -> 248 | request_input(prompt, state) 249 | end) 250 | end 251 | 252 | defp request_input(prompt, state) do 253 | send(state.target, {:evaluation_input, state.ref, self(), prompt}) 254 | 255 | ref = Process.monitor(state.target) 256 | 257 | receive do 258 | {:evaluation_input_reply, {:ok, string}} -> 259 | Process.demonitor(ref, [:flush]) 260 | string 261 | 262 | {:evaluation_input_reply, :error} -> 263 | Process.demonitor(ref, [:flush]) 264 | {:error, "no matching IElixir input found"} 265 | 266 | {:DOWN, ^ref, :process, _object, _reason} -> 267 | {:error, :terminated} 268 | end 269 | end 270 | 271 | defp line_from_input(""), do: {:eof, ""} 272 | 273 | defp line_from_input(input) do 274 | case :binary.match(input, ["\r\n", "\n"]) do 275 | :nomatch -> 276 | {input, ""} 277 | 278 | {pos, len} -> 279 | :erlang.split_binary(input, pos + len) 280 | end 281 | end 282 | 283 | defp chars_from_input("", _encoding, _count), do: {:eof, ""} 284 | 285 | defp chars_from_input(input, :unicode, count) do 286 | {:ok, count} = utf8_split_at(input, count) 287 | :erlang.split_binary(input, count) 288 | end 289 | 290 | defp chars_from_input(input, :latin1, count) do 291 | if byte_size(input) > count do 292 | :erlang.split_binary(input, count) 293 | else 294 | {input, ""} 295 | end 296 | end 297 | 298 | defp utf8_split_at(input, count), do: utf8_split_at(input, count, 0) 299 | 300 | defp utf8_split_at(_, 0, acc), do: {:ok, acc} 301 | 302 | defp utf8_split_at(<>, count, acc), 303 | do: utf8_split_at(t, count - 1, acc + byte_size(<>)) 304 | 305 | defp utf8_split_at(<<_, _::binary>>, _count, _acc), 306 | do: {:error, :invalid_unicode} 307 | 308 | defp utf8_split_at(<<>>, _count, acc), 309 | do: {:ok, acc} 310 | 311 | defp get_until_from_input(input, encoding, mod, fun, args) do 312 | {chars, rest} = get_until_from_input(input, encoding, mod, fun, args, []) 313 | {get_until_result(chars, encoding), rest} 314 | end 315 | 316 | defp get_until_from_input("", encoding, mod, fun, args, continuation) do 317 | case apply(mod, fun, [continuation, :eof | args]) do 318 | {:done, result, :eof} -> 319 | {result, ""} 320 | 321 | {:done, result, rest} -> 322 | {result, list_to_binary(rest, encoding)} 323 | 324 | {:more, next_continuation} -> 325 | get_until_from_input("", encoding, mod, fun, args, next_continuation) 326 | end 327 | end 328 | 329 | defp get_until_from_input(input, encoding, mod, fun, args, continuation) do 330 | {line, rest} = line_from_input(input) 331 | 332 | case apply(mod, fun, [continuation, binary_to_list(line, encoding) | args]) do 333 | {:done, result, :eof} -> 334 | {result, rest} 335 | 336 | {:done, result, extra} -> 337 | {result, list_to_binary(extra, encoding) <> rest} 338 | 339 | {:more, next_continuation} -> 340 | get_until_from_input(rest, encoding, mod, fun, args, next_continuation) 341 | end 342 | end 343 | 344 | defp binary_to_list(data, :unicode) when is_binary(data), do: String.to_charlist(data) 345 | defp binary_to_list(data, :latin1) when is_binary(data), do: :erlang.binary_to_list(data) 346 | 347 | defp list_to_binary(data, _) when is_binary(data), do: data 348 | defp list_to_binary(data, :unicode) when is_list(data), do: List.to_string(data) 349 | defp list_to_binary(data, :latin1) when is_list(data), do: :erlang.list_to_binary(data) 350 | 351 | # From https://erlang.org/doc/apps/stdlib/io_protocol.html - result can be any 352 | # Erlang term, but if it is a list(), the I/O server can convert it to a binary(). 353 | defp get_until_result(data, encoding) when is_list(data), do: list_to_binary(data, encoding) 354 | defp get_until_result(data, _), do: data 355 | 356 | defp io_reply(from, reply_as, reply) do 357 | send(from, {:io_reply, reply_as, reply}) 358 | end 359 | 360 | defp flush_buffer(state) do 361 | string = state.buffer |> Enum.reverse() |> Enum.join() 362 | 363 | if state.target != nil and string != "" do 364 | send(state.target, {:evaluation_output, state.ref, string}) 365 | end 366 | 367 | %{state | buffer: []} 368 | end 369 | 370 | defp buffer_append(buffer, text) do 371 | # Sometimes there are intensive outputs that use \r 372 | # to dynamically refresh the printd text. 373 | # Since we buffer the messages anyway, it makes 374 | # sense to send only the latest of these outputs. 375 | # Note that \r works per-line, so if there are newlines 376 | # we keep the buffer, but for \r-intensive operations 377 | # there are usually no newlines involved, so this optimisation works fine. 378 | if has_rewind?(text) and not has_newline?(text) and not Enum.any?(buffer, &has_newline?/1) do 379 | [text] 380 | else 381 | [text | buffer] 382 | end 383 | end 384 | 385 | # Checks for [\r][not \r] sequence in the given string. 386 | defp has_rewind?(<<>>), do: false 387 | defp has_rewind?(<>) when next != ?\r, do: true 388 | defp has_rewind?(<<_head, rest::binary>>), do: has_rewind?(rest) 389 | 390 | defp has_newline?(text), do: String.contains?(text, "\n") 391 | end 392 | -------------------------------------------------------------------------------- /lib/ielixir/init/resources.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Init.Resources do 2 | @external_resource "priv/kernel.js" 3 | @external_resource "priv/kernel.json" 4 | @external_resource "priv/logo-32x32.png" 5 | @external_resource "priv/logo-64x64.png" 6 | 7 | @kernel_folder_data [ 8 | {"kernel.js", File.read!("priv/kernel.js")}, 9 | {"kernel.json", File.read!("priv/kernel.json")}, 10 | {"logo-32x32.png", File.read!("priv/logo-32x32.png")}, 11 | {"logo-64x64.png", File.read!("priv/logo-64x64.png")} 12 | ] 13 | 14 | @spec generate_kernel(path_to_folder :: String.t()) :: :ok 15 | def generate_kernel(path_to_folder) do 16 | File.mkdir_p!(path_to_folder) 17 | 18 | Enum.each(@kernel_folder_data, fn {file_name, file_data} -> 19 | File.write!(Path.join(path_to_folder, file_name), file_data) 20 | end) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/ielixir/kernel/channels/control.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Kernel.Channels.Control do 2 | @moduledoc """ 3 | "Shell: this single ROUTER socket allows multiple incoming connections from 4 | frontends, and this is the socket where requests for code execution, object 5 | information, prompts, etc. are made to the kernel by any frontend. 6 | The communication on this socket is a sequence of request/reply actions from 7 | each frontend and the kernel." 8 | From https://ipython.org/ipython-doc/dev/development/messaging.html 9 | """ 10 | 11 | use IElixir.Kernel.Socket.Router, name: "control" 12 | 13 | alias IElixir.Kernel.Wire 14 | alias IElixir.Kernel.Wire.Packet 15 | alias IElixir.Kernel.Wire.Message 16 | alias IElixir.Kernel.Session 17 | 18 | @impl true 19 | @spec handle_packet(packet :: Packet.t(), channel :: Wire.channel()) :: 20 | Packet.t() | nil | {Packet.t() | nil, (() -> :ok)} 21 | 22 | def handle_packet( 23 | %Packet{ 24 | uuids: uuids, 25 | message: 26 | %Message{header: %{msg_type: "shutdown_request"}, content: %{"restart" => restart}} = 27 | parent_message 28 | } = _packet, 29 | _channel 30 | ) do 31 | Logger.debug("Received shutdown_request") 32 | 33 | { 34 | %Packet{ 35 | uuids: uuids, 36 | message: 37 | Message.from_parent( 38 | Session.get_session(), 39 | parent_message, 40 | "shutdown_reply", 41 | content: %{ 42 | status: "ok", 43 | restart: restart 44 | } 45 | ) 46 | }, 47 | &System.halt/0 48 | } 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/ielixir/kernel/channels/hb.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Kernel.Channels.Hb do 2 | alias IElixir.Kernel.Wire 3 | 4 | @moduledoc """ 5 | Represents HeartBeat channel 6 | """ 7 | use IElixir.Kernel.Socket.Rep, name: "hb" 8 | 9 | @impl true 10 | @spec handle_raw_packet(packet :: Wire.raw_packet(), socket :: Wire.channel()) :: 11 | Wire.raw_packet() 12 | def handle_raw_packet(packet, _socket) do 13 | # Basic idea here - message is sent as-is, so no need to parse it. 14 | Logger.debug("Got heartbeat packet: #{packet}") 15 | packet 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ielixir/kernel/channels/iopub.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Kernel.Channels.IOPub do 2 | use IElixir.Kernel.Socket.Pub, name: "iopub" 3 | 4 | alias IElixir.Kernel.Wire.Packet 5 | alias IElixir.Kernel.Wire.Message 6 | alias IElixir.Kernel.Session 7 | 8 | # Streams 9 | # https://jupyter-client.readthedocs.io/en/stable/messaging.html#streams-stdout-stderr-etc 10 | @spec stream( 11 | parent_packet :: IElixir.Kernel.Wire.Packet.t(), 12 | stream_name :: String.t(), 13 | stream_text :: String.t() 14 | ) :: :ok 15 | def stream( 16 | %Packet{message: parent_message, uuids: uuids} = _parent_packet, 17 | stream_name, 18 | stream_text 19 | ) do 20 | %Packet{ 21 | uuids: uuids, 22 | message: 23 | Message.from_parent( 24 | Session.get_session(), 25 | parent_message, 26 | "stream", 27 | content: %{ 28 | name: stream_name, 29 | text: stream_text 30 | } 31 | ) 32 | } 33 | |> publish() 34 | end 35 | 36 | # Display data 37 | # https://jupyter-client.readthedocs.io/en/stable/messaging.html#display-data 38 | @spec display_data( 39 | parent_packet :: IElixir.Kernel.Wire.Packet.t(), 40 | display :: IElixir.Kernel.Display.t() 41 | ) :: :ok 42 | def display_data( 43 | %Packet{message: parent_message, uuids: uuids} = _parent_packet, 44 | display 45 | ) do 46 | %Packet{ 47 | uuids: uuids, 48 | message: 49 | Message.from_parent( 50 | Session.get_session(), 51 | parent_message, 52 | "display_data", 53 | content: %{ 54 | data: display.data, 55 | metadata: display.metadata, 56 | transient: display.transient 57 | } 58 | ) 59 | } 60 | |> publish() 61 | end 62 | 63 | # Code inputs 64 | # https://jupyter-client.readthedocs.io/en/stable/messaging.html#code-inputs 65 | @spec execute_input( 66 | parent_packet :: IElixir.Kernel.Wire.Packet.t(), 67 | code :: String.t(), 68 | execution_count :: Integer 69 | ) :: :ok 70 | def execute_input( 71 | %Packet{message: parent_message, uuids: uuids} = _parent_packet, 72 | code, 73 | execution_count 74 | ) do 75 | %Packet{ 76 | uuids: uuids, 77 | message: 78 | Message.from_parent( 79 | Session.get_session(), 80 | parent_message, 81 | "display_data", 82 | content: %{ 83 | code: code, 84 | execution_count: execution_count 85 | } 86 | ) 87 | } 88 | |> publish() 89 | end 90 | 91 | # Execution results 92 | # https://jupyter-client.readthedocs.io/en/stable/messaging.html#id6 93 | @spec execute_result( 94 | parent_packet :: IElixir.Kernel.Wire.Packet.t(), 95 | display :: IElixir.Kernel.Display.t(), 96 | execution_count :: Integer 97 | ) :: :ok 98 | def execute_result( 99 | %Packet{message: parent_message, uuids: uuids} = _parent_packet, 100 | display, 101 | execution_count 102 | ) do 103 | %Packet{ 104 | uuids: uuids, 105 | message: 106 | Message.from_parent( 107 | Session.get_session(), 108 | parent_message, 109 | "execute_result", 110 | content: %{ 111 | data: display.data, 112 | metadata: display.metadata, 113 | transient: display.transient, 114 | execution_count: execution_count 115 | } 116 | ) 117 | } 118 | |> publish() 119 | end 120 | 121 | # Kernel status 122 | # https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-status 123 | 124 | @spec send_status(parent_packet :: Packet.t(), execution_state :: String.t()) :: :ok 125 | def send_status(%Packet{message: parent_message, uuids: uuids}, execution_state) do 126 | %Packet{ 127 | uuids: uuids, 128 | message: 129 | Message.from_parent( 130 | Session.get_session(), 131 | parent_message, 132 | "status", 133 | content: %{execution_state: execution_state} 134 | ) 135 | } 136 | |> publish() 137 | end 138 | 139 | @spec idle_notifier(parent_packet :: Packet.t()) :: (() -> :ok) 140 | def idle_notifier(parent_packet) do 141 | fn -> 142 | send_status(parent_packet, "idle") 143 | end 144 | end 145 | 146 | @spec busy_notifier(parent_packet :: Packet.t()) :: (() -> :ok) 147 | def busy_notifier(parent_packet) do 148 | fn -> 149 | send_status(parent_packet, "busy") 150 | end 151 | end 152 | 153 | # Clear output 154 | # https://jupyter-client.readthedocs.io/en/stable/messaging.html#clear-output 155 | def clear_output(_parent_packet) do 156 | raise "unimplemented" 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /lib/ielixir/kernel/channels/shell.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Kernel.Channels.Shell do 2 | @moduledoc """ 3 | "Shell: this single ROUTER socket allows multiple incoming connections from 4 | frontends, and this is the socket where requests for code execution, object 5 | information, prompts, etc. are made to the kernel by any frontend. 6 | The communication on this socket is a sequence of request/reply actions from 7 | each frontend and the kernel." 8 | From https://ipython.org/ipython-doc/dev/development/messaging.html 9 | """ 10 | 11 | use IElixir.Kernel.Socket.Router, name: "shell" 12 | 13 | alias IElixir.Kernel.Wire 14 | alias IElixir.Kernel.Wire.Packet 15 | alias IElixir.Kernel.Wire.Message 16 | alias IElixir.Kernel.Session 17 | 18 | alias IElixir.Kernel.Channels.IOPub 19 | 20 | @impl true 21 | @spec handle_packet(packet :: Packet.t(), channel :: Wire.channel()) :: 22 | Packet.t() | nil | {Packet.t() | nil, (() -> :ok)} 23 | def handle_packet( 24 | %Packet{ 25 | uuids: uuids, 26 | message: %Message{header: %{msg_type: "kernel_info_request"}} = parent_message 27 | } = packet, 28 | _channel 29 | ) do 30 | Logger.debug("Received kernel_info_request") 31 | IOPub.busy_notifier(packet).() 32 | 33 | { 34 | %Packet{ 35 | uuids: uuids, 36 | message: 37 | Message.from_parent( 38 | Session.get_session(), 39 | parent_message, 40 | "kernel_info_reply", 41 | content: %{ 42 | protocol_version: "5.3", 43 | implementation: "ielixir", 44 | implementation_version: "1.0", 45 | language_info: %{ 46 | "name" => "elixir", 47 | "version" => System.version(), 48 | "mimetype" => "text/x-ielixir", 49 | "file_extension" => "ex", 50 | "pygments_lexer" => "elixir", 51 | "codemirror_mode" => "ielixir", 52 | "nbconvert_exporter" => "" 53 | }, 54 | banner: "IElixir kernel: `#{File.cwd!()}`", 55 | help_links: [ 56 | %{ 57 | "text" => "Elixir Getting Started", 58 | "url" => "http://elixir-lang.org/getting-started/introduction.html" 59 | }, 60 | %{ 61 | "text" => "Elixir Documentation", 62 | "url" => "http://elixir-lang.org/docs.html" 63 | }, 64 | %{ 65 | "text" => "Elixir Sources", 66 | "url" => "https://github.com/elixir-lang/elixir" 67 | } 68 | ] 69 | } 70 | ) 71 | }, 72 | IOPub.idle_notifier(packet) 73 | } 74 | end 75 | 76 | def handle_packet( 77 | %Packet{ 78 | uuids: uuids, 79 | message: %Message{header: %{msg_type: "comm_info_request"}} = parent_message 80 | } = packet, 81 | _channel 82 | ) do 83 | Logger.debug("Received comm_info_request") 84 | IOPub.busy_notifier(packet).() 85 | 86 | { 87 | %Packet{ 88 | uuids: uuids, 89 | message: 90 | Message.from_parent( 91 | Session.get_session(), 92 | parent_message, 93 | "comm_info_reply", 94 | content: %{ 95 | comms: %{} 96 | } 97 | ) 98 | }, 99 | IOPub.idle_notifier(packet) 100 | } 101 | end 102 | 103 | def handle_packet( 104 | %Packet{ 105 | uuids: uuids, 106 | message: 107 | %Message{ 108 | header: %{msg_type: "complete_request"}, 109 | content: content 110 | } = parent_message 111 | } = packet, 112 | _channel 113 | ) do 114 | Logger.debug("Received complete_request") 115 | IOPub.busy_notifier(packet).() 116 | 117 | # random bytes - adhoc to workaround complete_request requirement in cell_id 118 | result = Session.complete_request(:crypto.strong_rand_bytes(20), content) 119 | Logger.debug("Returned complete_request: #{inspect(result)}") 120 | 121 | { 122 | %Packet{ 123 | uuids: uuids, 124 | message: 125 | Message.from_parent( 126 | Session.get_session(), 127 | parent_message, 128 | "complete_reply", 129 | content: %{ 130 | status: :ok, 131 | matches: result.matches, 132 | cursor_start: result.cursor_start, 133 | cursor_end: result.cursor_end 134 | } 135 | ) 136 | }, 137 | IOPub.idle_notifier(packet) 138 | } 139 | end 140 | 141 | def handle_packet( 142 | %Packet{ 143 | uuids: uuids, 144 | message: 145 | %Message{ 146 | header: %{msg_type: "execute_request"}, 147 | content: content, 148 | metadata: %{"cellId" => cell} 149 | } = parent_message 150 | } = packet, 151 | _channel 152 | ) do 153 | Logger.debug("Received execute_request") 154 | Logger.debug("Packet: #{inspect(packet)}") 155 | 156 | IOPub.busy_notifier(packet).() 157 | Session.increase_counter() 158 | result = Session.execute_request(cell, content) 159 | Logger.debug("Returned execute_request: #{inspect(result)}") 160 | 161 | result 162 | |> Map.get(:output, []) 163 | |> Enum.join("\n") 164 | |> case do 165 | "" -> nil 166 | output -> IOPub.stream(packet, "stdout", output) 167 | end 168 | 169 | case result.response do 170 | :ignored -> 171 | nil 172 | 173 | {:display, display} -> 174 | IOPub.execute_result(packet, display, Session.get_counter()) 175 | 176 | {:error, exception, :runtime_restart_required} -> 177 | IOPub.stream(packet, "stderr", exception) 178 | IOPub.stream(packet, "stderr", "Restart runtime pls") 179 | 180 | {:error, exception, _} -> 181 | IOPub.stream(packet, "stderr", exception) 182 | end 183 | 184 | { 185 | %Packet{ 186 | uuids: uuids, 187 | message: 188 | Message.from_parent( 189 | Session.get_session(), 190 | parent_message, 191 | "execute_reply", 192 | content: %{ 193 | status: "ok", 194 | execution_count: Session.get_counter(), 195 | user_expressions: %{}, 196 | payload: %{} 197 | } 198 | ) 199 | }, 200 | IOPub.idle_notifier(packet) 201 | } 202 | end 203 | end 204 | -------------------------------------------------------------------------------- /lib/ielixir/kernel/channels/stdin.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Kernel.Channels.StdIn do 2 | use IElixir.Kernel.Socket.Router, name: "stdin" 3 | 4 | # This function is a stub - it is not working 5 | def handle_packet(message_multipart, _channel) do 6 | Logger.info("StdIn message received #{inspect(message_multipart)}") 7 | {message_multipart, fn -> nil end} 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/ielixir/kernel/command.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Kernel.Command do 2 | defstruct code: [], magic: [] 3 | 4 | @type t() :: %__MODULE__{code: [], magic: []} 5 | 6 | @spec parse(binary) :: t() 7 | def parse(code) do 8 | code 9 | |> String.split(~r/\R/) 10 | |> Enum.reduce(%__MODULE__{}, &command_line_parser/2) 11 | |> then(fn %__MODULE__{code: code, magic: magic} -> 12 | %__MODULE__{code: Enum.reverse(code), magic: Enum.reverse(magic)} 13 | end) 14 | end 15 | 16 | defp command_line_parser(code_line, %__MODULE__{} = command) do 17 | case IElixir.Util.MagicCommand.parse(code_line) do 18 | {:ok, value} -> Map.update(command, :magic, [], &[value | &1]) 19 | {:error, value} -> Map.update(command, :code, [], &[value | &1]) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/ielixir/kernel/connection_file.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Kernel.ConnectionFile do 2 | @moduledoc """ 3 | This module provides operations with kernel connection file. 4 | It's protocol is defined here: 5 | https://jupyter-client.readthedocs.io/en/latest/kernels.html#connection-files 6 | """ 7 | 8 | require Logger 9 | 10 | @doc """ 11 | Parse connection file and return map with proper fields. 12 | 13 | ### Example 14 | 15 | iex> conn_info = IElixir.Utils.parse_connection_file("test/test_connection_file"); :ok 16 | :ok 17 | iex> conn_info["key"] 18 | "7534565f-e742-40f3-85b4-bf4e5f35390a" 19 | 20 | """ 21 | @spec parse(String.t()) :: map() 22 | def parse(connection_file) do 23 | # Caching contents of the connection file 24 | connection_file_contents = File.read!(connection_file) 25 | 26 | Logger.debug(fn -> 27 | "Parsing connection file #{connection_file}:\n#{connection_file_contents}" 28 | end) 29 | 30 | Jason.decode!(connection_file_contents) 31 | end 32 | 33 | @doc """ 34 | Function creates connection string that is acceptable for ZMQ client to open a connection 35 | 36 | Params: 37 | 38 | * connection_data - data from parsed connection file 39 | * channel_name - "control" | "shell" | "stdin" | "hb" | "iopub" 40 | """ 41 | @spec channel_connection_config(connection_data :: map(), channel_name :: String.t()) :: 42 | {atom(), list(), integer()} 43 | def channel_connection_config(connection_data, channel_name) do 44 | { 45 | :erlang.binary_to_atom(connection_data["transport"]), 46 | :erlang.binary_to_list(connection_data["ip"]), 47 | connection_data["#{channel_name}_port"] 48 | } 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/ielixir/kernel/display.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Kernel.Display do 2 | defstruct data: %{}, 3 | metadata: %{}, 4 | transient: %{} 5 | 6 | @type t() :: %__MODULE__{data: map(), metadata: map(), transient: map()} 7 | 8 | alias IElixir.Kernel.Displayable 9 | 10 | def display({:ok, :"do not show this result in output"}) do 11 | # Functions in the `IEx.Helpers` module return this specific value 12 | # to indicate no result should be printed in the iex shell, 13 | # so we respect that as well. 14 | :ignored 15 | end 16 | 17 | def display({:ok, value}) do 18 | {:display, Displayable.display(value)} 19 | end 20 | 21 | def display({:error, kind, error, stacktrace}) do 22 | formatted = Exception.format(kind, error, stacktrace) 23 | {:error, formatted, error_type(error)} 24 | end 25 | 26 | defp error_type(error) do 27 | cond do 28 | mix_install_vm_error?(error) -> :runtime_restart_required 29 | true -> :other 30 | end 31 | end 32 | 33 | defp mix_install_vm_error?(exception) do 34 | is_struct(exception, Mix.Error) and 35 | Exception.message(exception) =~ 36 | "Mix.install/2 can only be called with the same dependencies" 37 | end 38 | 39 | end 40 | 41 | defprotocol IElixir.Kernel.Displayable do 42 | @fallback_to_any true 43 | 44 | @spec display(term()) :: IElixir.Kernel.Display.t() 45 | def display(term) 46 | 47 | end 48 | 49 | defimpl IElixir.Kernel.Displayable, for: Any do 50 | 51 | # Note: we intentionally don't specify colors 52 | # for `:binary`, `:list`, `:map` and `:tuple` 53 | # and rely on these using the default text color. 54 | # This way we avoid a bunch of HTML tags for coloring commas, etc. 55 | @opts [ 56 | pretty: true, 57 | width: 100, 58 | syntax_colors: [ 59 | atom: :blue, 60 | # binary: :light_black, 61 | boolean: :magenta, 62 | # list: :light_black, 63 | # map: :light_black, 64 | number: :blue, 65 | nil: :magenta, 66 | regex: :red, 67 | string: :green, 68 | # tuple: :light_black, 69 | reset: :reset 70 | ] 71 | ] 72 | 73 | def display(term) do 74 | %IElixir.Kernel.Display{data: %{"text/plain": inspect(term, @opts)}} 75 | end 76 | 77 | end 78 | 79 | defimpl IElixir.Kernel.Displayable, for: BitString do 80 | 81 | # Basic implementation - for everything. 82 | @spec display(binary) :: IElixir.Kernel.Display.t() 83 | def display(term) do 84 | case ExImageInfo.info(term) do 85 | # This is not an image, or at least - not recognizable 86 | nil -> 87 | # Here we check that the data is valid json 88 | case Jason.decode(term) do 89 | {:ok, json_data} -> 90 | %IElixir.Kernel.Display{ 91 | data: %{ 92 | "text/plain": inspect(term), 93 | "application/json": json_data 94 | }, 95 | metadata: %{"application/json": %{expanded: true}}, 96 | transient: %{} 97 | } 98 | 99 | # This is nothing 100 | {:error, _reason} -> 101 | %IElixir.Kernel.Display{ 102 | data: %{"text/plain": inspect(term)}, 103 | metadata: %{}, 104 | transient: %{} 105 | } 106 | end 107 | 108 | {mime_type, width, height, _type_name} -> 109 | %IElixir.Kernel.Display{ 110 | data: %{mime_type => Base.encode64(term), "text/plain" => inspect(term)}, 111 | metadata: %{mime_type => %{width: width, height: height}}, 112 | transient: %{} 113 | } 114 | end 115 | end 116 | end 117 | 118 | defimpl IElixir.Kernel.Displayable, for: Map do 119 | def display( 120 | %{ 121 | "$schema" => "https://vega.github.io/schema/vega-lite/v5.json" 122 | } = vega_doc 123 | ) do 124 | %IElixir.Kernel.Display{ 125 | data: %{ 126 | "application/vnd.vegalite.v4+json": %{vega_doc | "$schema" => "https://vega.github.io/schema/vega-lite/v4.json"} 127 | } 128 | } 129 | end 130 | 131 | # Basic implementation - for everything. 132 | @spec display(binary) :: IElixir.Kernel.Display.t() 133 | def display(term) do 134 | IElixir.Kernel.Displayable.Any.display(term) 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /lib/ielixir/kernel/history.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Kernel.History do 2 | @moduledoc false 3 | 4 | require Logger 5 | 6 | defmodule Record do 7 | defstruct [ 8 | :session_id, 9 | :line, 10 | :source, 11 | :source_raw, 12 | :output 13 | ] 14 | end 15 | 16 | alias IElixir.Kernel.History.Record 17 | 18 | use GenServer 19 | 20 | def start_link(args) do 21 | GenServer.start_link(__MODULE__, args, name: __MODULE__) 22 | end 23 | 24 | def init(%{db_path: path}) do 25 | File.mkdir_p!(path) 26 | sessions_log = Path.join(path, "sessions.log") 27 | 28 | # This is ID of the current kernel 29 | session_id = 30 | :rand.uniform() |> Float.to_string() |> then(&:crypto.hash(:sha, &1)) |> Base.encode16() 31 | 32 | # Index of current session 33 | session_index = 34 | with true <- File.exists?(sessions_log), 35 | value when is_integer(value) <- 36 | Enum.find_index(File.stream!(sessions_log), &(String.trim(&1) == session_id)) do 37 | raise "Kernel's id conflict! Restarting kernel to generate new id" 38 | else 39 | _ -> 40 | # Part with appending kernel id to sessions log 41 | {:ok, file} = File.open(sessions_log, [:append]) 42 | IO.write(file, "#{session_id}\n") 43 | # Closing file to apply the appendance 44 | File.close(file) 45 | 46 | # Getting actual id 47 | File.stream!(sessions_log) 48 | |> Enum.find_index(&(String.trim(&1) == session_id)) 49 | end 50 | 51 | # Opening the file 52 | history_log = Path.join(path, "#{session_index}.log") 53 | {:ok, fd} = File.open(history_log, [:append]) 54 | 55 | # Finishing init 56 | {:ok, %{fd: fd, session_id: session_id, session_index: session_index}} 57 | end 58 | 59 | def insert(line, source, source_raw, output) do 60 | GenServer.call(__MODULE__, {:insert, line, source, source_raw, output}) 61 | end 62 | 63 | @spec get_session() :: integer() 64 | def get_session() do 65 | GenServer.call(__MODULE__, :get_session) 66 | end 67 | 68 | def handle_call({:insert, line, source, source_raw, output}, _from, %{fd: fd, session_id: session} = conn) do 69 | line_to_write = 70 | %Record{ 71 | session_id: session, 72 | line: line, 73 | source: source, 74 | source_raw: source_raw, 75 | output: output 76 | } 77 | |> :erlang.term_to_binary() 78 | |> Base.encode64() 79 | 80 | :file.write(fd, line_to_write <> "\n") 81 | 82 | {:reply, :ok, conn} 83 | end 84 | 85 | def handle_call(:get_session, _from, %{session_id: session_id} = conn) do 86 | {:reply, session_id, conn} 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/ielixir/kernel/session.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Kernel.Session do 2 | @moduledoc """ 3 | This module provides genserver that handles current shell session. 4 | It stores current kernel encryption so is able to compute signatures. 5 | Also it handles current session's key. 6 | """ 7 | 8 | require Logger 9 | use GenServer 10 | 11 | alias IElixir.Kernel.History 12 | alias IElixir.Kernel.Display 13 | alias IElixir.Util.Substring 14 | alias IElixir.Runtime 15 | alias IElixir.Util.Crypto 16 | 17 | @doc """ 18 | Start the session server 19 | 20 | IElixir.Kernel.Session.start_link(%{"signature_scheme" => "hmac-sha256", "key" => "7534565f-e742-40f3-85b4-bf4e5f35390a"}) 21 | 22 | ## Options 23 | 24 | "signature_scheme" and "key" options are required for proper work of HMAC server. 25 | """ 26 | @spec start_link(map) :: GenServer.on_start() 27 | def start_link(conn_info) do 28 | GenServer.start_link(__MODULE__, conn_info, name: __MODULE__) 29 | end 30 | 31 | def init(conn_info) do 32 | # TODO make default runtime selectable 33 | 34 | {:ok, runtime} = Runtime.ElixirStandalone.init() 35 | Runtime.connect(runtime) 36 | 37 | init_state = %{ 38 | session_id: History.get_session(), 39 | runtime: runtime, 40 | evaluating_cells: %{}, 41 | completing_cells: %{}, 42 | execution_count: 0 43 | } 44 | 45 | case String.split(conn_info["signature_scheme"], "-") do 46 | ["hmac", tail] -> 47 | {:ok, 48 | Map.merge(init_state, %{ 49 | signature_data: {String.to_atom(tail), conn_info["key"]} 50 | })} 51 | 52 | ["", _] -> 53 | {:ok, 54 | Map.merge(init_state, %{ 55 | signature_data: {nil, ""} 56 | })} 57 | 58 | scheme -> 59 | Logger.error("Invalid signature_scheme: #{inspect(scheme)}") 60 | {:error, "Invalid signature_scheme"} 61 | end 62 | end 63 | 64 | @doc """ 65 | Compute signature for provided message. 66 | Each argument must be valid UTF-8 string, because it is JSON decodable. 67 | 68 | ### Example 69 | 70 | iex> IElixir.HMAC.compute_signature("", "", "", "") 71 | "25eb8ea448d87f384f43c96960600c2ce1e713a364739674a6801585ae627958" 72 | 73 | """ 74 | @spec compute_signature(String.t(), String.t(), String.t(), String.t()) :: String.t() 75 | def compute_signature(header_raw, parent_header_raw, metadata_raw, content_raw) do 76 | GenServer.call( 77 | __MODULE__, 78 | {:compute_sig, [header_raw, parent_header_raw, metadata_raw, content_raw]} 79 | ) 80 | end 81 | 82 | @spec get_session :: String.t() 83 | def get_session() do 84 | GenServer.call(__MODULE__, :get_session) 85 | end 86 | 87 | @spec increase_counter :: :ok 88 | def increase_counter() do 89 | GenServer.cast(__MODULE__, :increase_counter) 90 | end 91 | 92 | @spec get_counter :: Integer.t() 93 | def get_counter() do 94 | GenServer.call(__MODULE__, :get_counter) 95 | end 96 | 97 | def execute_request(cell, content) do 98 | GenServer.call(__MODULE__, {:execute_request, cell, content}, :infinity) 99 | end 100 | 101 | def complete_request(cell, content) do 102 | GenServer.call(__MODULE__, {:complete_request, cell, content}, :infinity) 103 | end 104 | 105 | def handle_call( 106 | {:complete_request, cell, content}, 107 | from, 108 | %{completing_cells: completing_cells, runtime: runtime} = state 109 | ) do 110 | cursor = content["cursor_pos"] 111 | code = content["code"] 112 | Runtime.handle_completion(runtime, code, cursor, cell) 113 | 114 | completing_cells = Map.put(completing_cells, cell, {from, %{code: code, cursor: cursor}}) 115 | {:noreply, %{state | completing_cells: completing_cells}} 116 | end 117 | 118 | def handle_call( 119 | {:execute_request, cell, content}, 120 | from, 121 | %{evaluating_cells: evaluating_cells, runtime: runtime} = state 122 | ) do 123 | Runtime.evaluate_code(runtime, content["code"], cell, nil) 124 | evaluating_cells = Map.put(evaluating_cells, cell, {from, %{}}) 125 | {:noreply, %{state | evaluating_cells: evaluating_cells}} 126 | end 127 | 128 | def handle_call(:get_session, _from, %{session_id: session} = state) do 129 | {:reply, session, state} 130 | end 131 | 132 | def handle_call(:get_counter, _from, %{execution_count: execution_count} = state) do 133 | {:reply, execution_count, state} 134 | end 135 | 136 | def handle_call({:compute_sig, _parts}, _from, %{signature_data: {_, ""}} = state) do 137 | {:reply, "", state} 138 | end 139 | 140 | def handle_call({:compute_sig, parts}, _from, %{signature_data: {algo, key}} = state) do 141 | {:reply, Crypto.compute_signature(algo, key, parts), state} 142 | end 143 | 144 | def handle_cast(:increase_counter, state) do 145 | {:noreply, Map.update(state, :execution_count, 0, &(&1 + 1))} 146 | end 147 | 148 | def handle_info({:evaluation_output, cell, data}, %{evaluating_cells: evaluating_cells} = state) do 149 | evaluating_cells = 150 | Map.update!(evaluating_cells, cell, fn {from, cell_data} -> 151 | cell_data = Map.update(cell_data, :output, [data], &[data | &1]) 152 | {from, cell_data} 153 | end) 154 | 155 | {:noreply, %{state | evaluating_cells: evaluating_cells}} 156 | end 157 | 158 | def handle_info( 159 | {:evaluation_response, cell, data, metadata}, 160 | %{evaluating_cells: evaluating_cells} = state 161 | ) do 162 | {{from, cell_state}, evaluating_cells} = Map.pop(evaluating_cells, cell) 163 | 164 | data = Display.display(data) 165 | 166 | cell_state = 167 | cell_state 168 | |> Map.put(:response, data) 169 | |> Map.put(:response_metadata, metadata) 170 | 171 | GenServer.reply(from, cell_state) 172 | 173 | {:noreply, %{state | evaluating_cells: evaluating_cells}} 174 | end 175 | 176 | def handle_info( 177 | {:completion_response, cell, matches, _cursor_start, _cursor_end}, 178 | %{completing_cells: completing_cells} = state 179 | ) do 180 | {{from, cell_state}, completing_cells} = Map.pop(completing_cells, cell) 181 | 182 | cursor_start = Substring.calculate_substring_position(cell_state.code, matches) 183 | cursor = cell_state.cursor 184 | 185 | cell_state = 186 | cell_state 187 | |> Map.put(:matches, matches) 188 | |> Map.put(:cursor_start, cursor_start) 189 | |> Map.put(:cursor_end, cursor) 190 | 191 | GenServer.reply(from, cell_state) 192 | 193 | {:noreply, %{state | completing_cells: completing_cells}} 194 | end 195 | 196 | def handle_info({:log, level, message}, state) do 197 | Logger.log(level, message) 198 | {:noreply, state} 199 | end 200 | 201 | def handle_info(msg, state) do 202 | Logger.debug("Missed message: #{inspect(msg)}") 203 | {:noreply, state} 204 | end 205 | end 206 | -------------------------------------------------------------------------------- /lib/ielixir/kernel/socket/config.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Kernel.Socket.Config do 2 | @moduledoc false 3 | 4 | defstruct zmq_context: nil, 5 | connection_data: nil 6 | 7 | @type t() :: %__MODULE__{} 8 | end 9 | -------------------------------------------------------------------------------- /lib/ielixir/kernel/socket/pub.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Kernel.Socket.Pub do 2 | @moduledoc false 3 | 4 | defmacro __using__(opts) do 5 | channel_name = Keyword.get(opts, :name) 6 | 7 | quote do 8 | require Logger 9 | use GenServer 10 | 11 | def start_link(%IElixir.Kernel.Socket.Config{} = channel_config) do 12 | GenServer.start_link(__MODULE__, channel_config, name: __MODULE__) 13 | end 14 | 15 | # API 16 | def init(%IElixir.Kernel.Socket.Config{connection_data: connection_data}) do 17 | Logger.debug("Starting channel #{unquote(channel_name)} with PID: #{inspect(self())}") 18 | 19 | { 20 | :ok, 21 | %{ 22 | channel: 23 | IElixir.Kernel.Wire.make_channel( 24 | connection_data, 25 | unquote(channel_name), 26 | :pub 27 | ) 28 | } 29 | } 30 | end 31 | 32 | @spec publish(packet :: IElixir.Kernel.Wire.Packet.t()) :: :ok 33 | def publish(packet) do 34 | packet 35 | |> IElixir.Kernel.Wire.Packet.encode() 36 | |> publish_raw() 37 | end 38 | 39 | @spec publish_raw(raw_packet :: IElixir.Kernel.Connection.Wire.raw_packet()) :: :ok 40 | def publish_raw(raw_packet) when is_binary(raw_packet) do 41 | GenServer.call(__MODULE__, {:"$zmq_publish", raw_packet}) 42 | end 43 | 44 | def publish_raw(raw_packet) when is_list(raw_packet) do 45 | GenServer.call(__MODULE__, {:"$zmq_publish_multipart", raw_packet}) 46 | end 47 | 48 | def handle_call({:"$zmq_publish", raw_packet}, _from, %{channel: channel} = state) do 49 | { 50 | :reply, 51 | :chumak.send(channel, raw_packet), 52 | state 53 | } 54 | end 55 | 56 | def handle_call({:"$zmq_publish_multipart", raw_packet}, _from, %{channel: channel} = state) do 57 | { 58 | :reply, 59 | :chumak.send_multipart(channel, raw_packet), 60 | state 61 | } 62 | end 63 | 64 | def terminate(_reason, _) do 65 | Logger.debug("Shutdown #{unquote(channel_name)} channel") 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/ielixir/kernel/socket/rep.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Kernel.Socket.Rep do 2 | @moduledoc false 3 | 4 | alias IElixir.Kernel.Wire 5 | alias IElixir.Kernel.Wire.Packet 6 | 7 | @callback handle_packet(packet :: Packet.t(), channel :: Wire.channel()) :: Packet.t() 8 | @callback handle_raw_packet(packet :: Wire.raw_packet(), channel :: Wire.channel()) :: 9 | Wire.raw_packet() 10 | 11 | defmacro __using__(opts) do 12 | channel_name = Keyword.get(opts, :name) 13 | 14 | quote location: :keep do 15 | require Logger 16 | use GenServer 17 | @behaviour unquote(__MODULE__) 18 | 19 | def start_link(%IElixir.Kernel.Socket.Config{} = channel_config) do 20 | GenServer.start_link(__MODULE__, channel_config, name: __MODULE__) 21 | end 22 | 23 | # API 24 | def init(%IElixir.Kernel.Socket.Config{connection_data: connection_data}) do 25 | Logger.debug("Starting channel #{unquote(channel_name)} with PID: #{inspect(self())}") 26 | 27 | { 28 | :ok, 29 | %{ 30 | channel: 31 | IElixir.Kernel.Wire.make_channel( 32 | connection_data, 33 | unquote(channel_name), 34 | :rep 35 | ) 36 | }, 37 | {:continue, []} 38 | } 39 | end 40 | 41 | def handle_continue(_, %{channel: socket} = state) do 42 | get_req(socket) 43 | {:noreply, state} 44 | end 45 | 46 | def handle_info( 47 | {:zmq, :ok, packet}, 48 | %{ 49 | channel: channel 50 | } = state 51 | ) do 52 | Logger.debug("Got #{unquote(channel_name)} packet: #{packet}") 53 | reply = handle_raw_packet(packet, channel) 54 | :chumak.send(channel, reply) 55 | get_req(channel) 56 | {:noreply, state} 57 | end 58 | 59 | def handle_info(msg, state) do 60 | Logger.warn("Got unexpected packet on #{__MODULE__} process: #{inspect(msg)}") 61 | {:noreply, state} 62 | end 63 | 64 | def terminate(_reason, _) do 65 | Logger.debug("Shutdown #{unquote(channel_name)} channel") 66 | end 67 | 68 | defp get_req(socket) do 69 | parent = self() 70 | 71 | spawn_link(fn -> 72 | # This call is blocking 73 | case :chumak.recv(socket) do 74 | {:ok, data} -> Process.send(parent, {:zmq, :ok, data}, []) 75 | {:error, reason} -> Process.send(parent, {:zmq, :error, reason}, []) 76 | end 77 | end) 78 | end 79 | 80 | ## DEFAULT IMPLEMENTATIONS 81 | def handle_raw_packet(packet, channel) do 82 | case Packet.parse(packet) do 83 | {:ok, packet} -> 84 | handle_packet(packet, channel) 85 | |> Packet.encode() 86 | 87 | {:error, reason} -> 88 | Logger.warn("Failed to parse packet: #{inspect(packet)}") 89 | packet 90 | end 91 | end 92 | 93 | # Sending packet back. Should be overridden by user in most cases. 94 | def handle_packet(packet, channel), do: packet 95 | 96 | defoverridable handle_raw_packet: 2, handle_packet: 2 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/ielixir/kernel/socket/router.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Kernel.Socket.Router do 2 | @moduledoc false 3 | 4 | alias IElixir.Kernel.Wire 5 | alias IElixir.Kernel.Wire.Packet 6 | 7 | @callback handle_packet(packet :: Packet.t(), channel :: Wire.channel()) :: 8 | Packet.t() | nil | {Packet.t() | nil, (() -> :ok)} 9 | @callback handle_raw_packet(packet :: Wire.raw_packet(), channel :: pid()) :: 10 | Wire.raw_packet() | nil | {Wire.raw_packet() | nil, (() -> :ok)} 11 | 12 | defmacro __using__(opts) do 13 | channel_name = Keyword.get(opts, :name) 14 | 15 | quote do 16 | require Logger 17 | use GenServer 18 | @behaviour unquote(__MODULE__) 19 | 20 | def start_link(%IElixir.Kernel.Socket.Config{} = channel_config) do 21 | GenServer.start_link(__MODULE__, channel_config, name: __MODULE__) 22 | end 23 | 24 | # API 25 | def init(%IElixir.Kernel.Socket.Config{connection_data: connection_data}) do 26 | Logger.debug("Starting channel #{unquote(channel_name)} with PID: #{inspect(self())}") 27 | 28 | { 29 | :ok, 30 | %{ 31 | channel: 32 | IElixir.Kernel.Wire.make_channel( 33 | connection_data, 34 | unquote(channel_name), 35 | :router 36 | ) 37 | }, 38 | {:continue, []} 39 | } 40 | end 41 | 42 | def handle_continue(_, %{channel: socket} = state) do 43 | get_req(socket) 44 | {:noreply, state} 45 | end 46 | 47 | def handle_info( 48 | {:zmq, :ok, packet}, 49 | %{ 50 | channel: channel 51 | } = state 52 | ) do 53 | Logger.debug("Got #{unquote(channel_name)} packet: #{packet}") 54 | 55 | case handle_raw_packet(packet, channel) do 56 | nil -> 57 | :do_nothing 58 | 59 | {nil, after_callback} -> 60 | after_callback.() 61 | 62 | {reply, after_callback} -> 63 | :chumak.send_multipart(channel, reply) 64 | after_callback.() 65 | 66 | reply when is_list(reply) -> 67 | :chumak.send_multipart(channel, reply) 68 | # No callback 69 | end 70 | 71 | get_req(channel) 72 | {:noreply, state} 73 | end 74 | 75 | def handle_info(msg, state) do 76 | Logger.warn("Got unexpected packet on #{__MODULE__} process: #{inspect(msg)}") 77 | {:noreply, state} 78 | end 79 | 80 | def terminate(_reason, _) do 81 | Logger.debug("Shutdown #{unquote(channel_name)} channel") 82 | end 83 | 84 | defp get_req(socket) do 85 | parent = self() 86 | 87 | spawn_link(fn -> 88 | case :chumak.recv_multipart(socket) do 89 | {:ok, data} -> Process.send(parent, {:zmq, :ok, data}, []) 90 | {:error, reason} -> Process.send(parent, {:zmq, :error, reason}, []) 91 | end 92 | end) 93 | end 94 | 95 | ## DEFAULT IMPLEMENTATIONS 96 | @impl true 97 | 98 | @spec handle_raw_packet(packet :: Wire.raw_packet(), channel :: pid()) :: 99 | Wire.raw_packet() | nil 100 | def handle_raw_packet(packet, channel) do 101 | case Packet.parse(packet) do 102 | {:ok, packet} -> 103 | case handle_packet(packet, channel) do 104 | nil -> 105 | nil 106 | 107 | {nil, after_callback} -> 108 | {nil, after_callback} 109 | 110 | {packet, after_callback} -> 111 | {Packet.encode(packet), after_callback} 112 | 113 | packet -> 114 | Packet.encode(packet) 115 | end 116 | 117 | {:error, reason} -> 118 | Logger.warn("Failed to parse packet: #{inspect(packet)}") 119 | nil 120 | end 121 | end 122 | 123 | @impl true 124 | @spec handle_packet(packet :: Packet.t(), channel :: Wire.channel()) :: Packet.t() | nil 125 | def handle_packet(packet, channel), do: nil 126 | 127 | defoverridable handle_raw_packet: 2, handle_packet: 2 128 | end 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/ielixir/kernel/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Kernel.Supervisor do 2 | use Supervisor 3 | 4 | alias IElixir.Kernel 5 | alias IElixir.Kernel.Channels 6 | 7 | alias IElixir.Kernel.Socket.Config, as: SocketConfig 8 | import IElixir.Utils, only: [random_id: 0] 9 | 10 | def start_link(connection_data) do 11 | Supervisor.start_link(__MODULE__, connection_data, name: __MODULE__) 12 | end 13 | 14 | @impl true 15 | def init(connection_data) do 16 | :net_kernel.start([:"session-#{random_id()}@127.0.0.1", :longnames]) 17 | channel_starting_args = %SocketConfig{ 18 | connection_data: connection_data 19 | } 20 | 21 | children = [ 22 | # Starting supporting services 23 | {Kernel.History, %{db_path: db_path(), connection_data: connection_data}}, 24 | {Kernel.Session, connection_data}, 25 | 26 | # Starting channels 27 | {Channels.Hb, channel_starting_args}, 28 | {Channels.IOPub, channel_starting_args}, 29 | {Channels.StdIn, channel_starting_args}, 30 | {Channels.Shell, channel_starting_args} 31 | ] 32 | 33 | Supervisor.init(children, strategy: :one_for_one) 34 | end 35 | 36 | @doc """ 37 | Function calculates db path for installation. 38 | The path is located is users home director, following the convention from IPython 39 | """ 40 | def db_path do 41 | System.user_home!() 42 | |> Path.join(".ielixir") 43 | |> Path.join("history") 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/ielixir/kernel/wire.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Kernel.Wire do 2 | @type raw_packet :: binary() | list(binary()) 3 | @type channel :: pid() 4 | 5 | require Logger 6 | 7 | @doc false 8 | @spec make_channel( 9 | connection_data :: any(), 10 | channel_name :: String.t(), 11 | channel_type :: :chumak.socket_type() 12 | ) :: channel() 13 | def make_channel(connection_data, channel_name, channel_type) do 14 | sock = 15 | case :chumak.socket(channel_type, :erlang.binary_to_list(channel_name)) do 16 | {:ok, socket} -> socket 17 | {:error, {:already_started, socket}} -> socket 18 | end 19 | 20 | {transport, host, port} = 21 | channel_params = 22 | IElixir.Kernel.ConnectionFile.channel_connection_config(connection_data, channel_name) 23 | 24 | :chumak.bind(sock, transport, host, port) 25 | 26 | Logger.debug("Initializing #{channel_name} agent with params: #{inspect(channel_params)}") 27 | 28 | sock 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/ielixir/kernel/wire/message.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Kernel.Wire.Message do 2 | @moduledoc """ 3 | This is documentation for Message structure and some utils that helps in 4 | encoding, parsing, assembling and sending messages. 5 | 6 | Here are extracted functions which helps with messages management. 7 | """ 8 | 9 | alias IElixir.Kernel.Wire.MessageHeader 10 | 11 | require Logger 12 | 13 | defstruct header: %{}, 14 | parent_header: %{}, 15 | metadata: %{}, 16 | content: %{}, 17 | buffers: [] 18 | 19 | @type t() :: %__MODULE__{} 20 | 21 | @doc """ 22 | Function will make a new message. 23 | 24 | Params: 25 | 26 | * session_uuid - UUID that defines current kernel process 27 | * parent_message - parent message should be passed. Then, the message will be baked as a RESPONSE message for that parent 28 | * message_type - string that represents the type of the message 29 | * fields - keyword, that specifies optional params for the message: 30 | * :metadata 31 | * :content 32 | * :buffers 33 | """ 34 | @spec from_parent( 35 | session_uuid :: String.t(), 36 | parent_message :: __MODULE__.t(), 37 | message_type :: String.t(), 38 | fields :: Keyword.t() 39 | ) :: 40 | __MODULE__.t() 41 | def from_parent(session_uuid, parent_message, message_type, fields) do 42 | %__MODULE__{ 43 | # uuids: Keyword.get(fields, :parent, %{uuids: []}).uuids, 44 | header: MessageHeader.new(session_uuid, message_type), 45 | parent_header: parent_message.header, 46 | metadata: Keyword.get(fields, :metadata, %{}), 47 | content: Keyword.get(fields, :content, %{}), 48 | buffers: Keyword.get(fields, :buffers, []) 49 | } 50 | end 51 | 52 | def from_wire_parts(header_raw, parent_header_raw, metadata_raw, content_raw, buffers_raw) do 53 | %__MODULE__{ 54 | # uuids: Keyword.get(fields, :parent, %{uuids: []}).uuids, 55 | header: Jason.decode!(header_raw, keys: :atoms), 56 | parent_header: Jason.decode!(parent_header_raw, keys: :atoms), 57 | metadata: Jason.decode!(metadata_raw), 58 | content: Jason.decode!(content_raw), 59 | # TODO: Check if it should be parsed. It seems to be a list of binaries 60 | buffers: buffers_raw 61 | } 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/ielixir/kernel/wire/message_header.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Kernel.Wire.MessageHeader do 2 | @moduledoc """ 3 | NOTE: Session field differs for kernel and client messages. 4 | So CLIENT session identifies different clients, 5 | while KERNEL session identifies different KERNEL sessions. 6 | """ 7 | 8 | @doc """ 9 | Composes a new header. 10 | Message id is generated randomly, and the time is taken from the current time of this function call. 11 | """ 12 | def new(session_uuid, message_type) do 13 | %{ 14 | # Generating new random uuid 15 | msg_id: :uuid.uuid_to_string(:uuid.get_v4(), :binary_standard), 16 | # Username is general for the kernel 17 | username: "ielixir_kernel", 18 | session: session_uuid, 19 | msg_type: message_type, 20 | date: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now()), 21 | version: "5.3" 22 | } 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/ielixir/kernel/wire/packet.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Kernel.Wire.Packet do 2 | @moduledoc """ 3 | Module defines model for single packaet that comes from Wire Protocol, that is defined here 4 | https://jupyter-client.readthedocs.io/en/latest/messaging.html#the-wire-protocol 5 | """ 6 | 7 | @type t() :: %__MODULE__{} 8 | 9 | alias IElixir.Kernel.Wire.Message 10 | alias IElixir.Kernel.Session 11 | 12 | defstruct [ 13 | # List of uuids 14 | :uuids, 15 | # Message 16 | :message 17 | ] 18 | 19 | @spec parse(message_multipart :: list(binary())) :: {:ok, t()} | {:error, any()} 20 | def parse(message_multipart) do 21 | { 22 | # Here uuids - is a list of different ids, like ["0x123", "0x456", ...] 23 | uuids, 24 | [ 25 | "", 26 | baddad42, 27 | header, 28 | parent_header, 29 | metadata, 30 | # Blob is a list of binaries - raw buffers 31 | content | blob 32 | ] 33 | } = Enum.split_while(message_multipart, fn x -> x != "" end) 34 | 35 | packet = %__MODULE__{ 36 | uuids: uuids, 37 | message: 38 | Message.from_wire_parts( 39 | header, 40 | parent_header, 41 | metadata, 42 | content, 43 | blob 44 | ) 45 | } 46 | 47 | # Validating that this massage is from right frontend 48 | case Session.compute_signature(header, parent_header, metadata, content) do 49 | ^baddad42 -> {:ok, packet} 50 | _ -> {:error, {:unauthorized, packet}} 51 | end 52 | end 53 | 54 | def encode(%__MODULE__{ 55 | uuids: uuids, 56 | message: %Message{ 57 | header: header, 58 | parent_header: parent_header, 59 | metadata: metadata, 60 | content: content, 61 | buffers: buffers 62 | } 63 | }) do 64 | # Caching the data not to calculate twise for HMAC 65 | header = Jason.encode!(header) 66 | parent_header = Jason.encode!(parent_header) 67 | metadata = Jason.encode!(metadata) 68 | content = Jason.encode!(content) 69 | 70 | uuids ++ 71 | [ 72 | "", 73 | Session.compute_signature(header, parent_header, metadata, content), 74 | header, 75 | parent_header, 76 | metadata, 77 | content 78 | ] ++ 79 | buffers 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/ielixir/runtime.ex: -------------------------------------------------------------------------------- 1 | defprotocol IElixir.Runtime do 2 | @moduledoc false 3 | 4 | # This protocol defines an interface for evaluation backends. 5 | # 6 | # Usually a runtime involves a set of processes responsible 7 | # for evaluation, which could be running on a different node, 8 | # however the protocol does not require that. 9 | 10 | @typedoc """ 11 | An arbitrary term identifying an evaluation container. 12 | 13 | A container is an abstraction of an isolated group of evaluations. 14 | Containers are mostly independent and consequently can be evaluated 15 | concurrently if possible. 16 | 17 | Note that every evaluation can use the resulting environment 18 | and bindings of any previous evaluation, even from a different 19 | container. 20 | """ 21 | @type cell :: String.t() 22 | 23 | @typedoc """ 24 | Expected completion responses. 25 | 26 | Responding with `nil` indicates there is no relevant reply 27 | and effectively aborts the request, so it's suitable for 28 | error cases. 29 | """ 30 | @type completion_response :: term() 31 | 32 | @typedoc """ 33 | Looks up a list of identifiers that are suitable code 34 | completions for the given hint. 35 | """ 36 | @type completion_request :: {:completion, hint :: String.t()} 37 | 38 | @type completion_item :: %{ 39 | label: String.t(), 40 | kind: completion_item_kind(), 41 | detail: String.t() | nil, 42 | documentation: String.t() | nil, 43 | insert_text: String.t() 44 | } 45 | 46 | @type completion_item_kind :: 47 | :function | :module | :struct | :interface | :type | :variable | :field 48 | 49 | @typedoc """ 50 | Looks up more details about an identifier found in `column` in `line`. 51 | """ 52 | @type details_request :: {:details, line :: String.t(), column :: pos_integer()} 53 | 54 | @type details_response :: %{ 55 | range: %{ 56 | from: non_neg_integer(), 57 | to: non_neg_integer() 58 | }, 59 | contents: list(String.t()) 60 | } 61 | 62 | @typedoc """ 63 | Formats the given code snippet. 64 | """ 65 | @type format_request :: {:format, code :: String.t()} 66 | 67 | @type format_response :: %{ 68 | code: String.t() 69 | } 70 | 71 | @doc """ 72 | Sets the caller as runtime owner. 73 | 74 | It's advised for each runtime to have a leading process 75 | that is coupled to the lifetime of the underlying runtime 76 | resources. In this case the `connect` function may start 77 | monitoring that process and return the monitor reference. 78 | This way the caller is notified when the runtime goes down 79 | by listening to the :DOWN message. 80 | """ 81 | @spec connect(t()) :: reference() 82 | def connect(runtime) 83 | 84 | @doc """ 85 | Disconnects the current owner from runtime. 86 | 87 | This should cleanup the underlying node/processes. 88 | """ 89 | @spec disconnect(t()) :: :ok 90 | def disconnect(runtime) 91 | 92 | @doc """ 93 | Asynchronously parses and evaluates the given code. 94 | 95 | The given `cell` identifies the container where 96 | the code should be evaluated as well as the evaluation 97 | reference to store the resulting contxt under. 98 | 99 | Additionally, `prev_cell` points to a previous 100 | evaluation to be used as the starting point of this 101 | evaluation. If not applicable, the previous evaluation 102 | reference may be specified as `nil`. 103 | 104 | ## Communication 105 | 106 | Evaluation outputs are sent to the connected runtime owner. 107 | The messages should be of the form: 108 | 109 | * `{:evaluation_output, ref, output}` - output captured 110 | during evaluation 111 | 112 | * `{:evaluation_response, ref, output, metadata}` - final 113 | result of the evaluation. Recognised metadata entries 114 | are: `evaluation_time_ms` 115 | 116 | The evaluation may request user input by sending 117 | `{:evaluation_input, ref, reply_to, prompt}` to the runtime owner, 118 | which is supposed to reply with `{:evaluation_input_reply, reply}` 119 | where `reply` is either `{:ok, input}` or `:error` if no matching 120 | input can be found. 121 | 122 | In all of the above `ref` is the evaluation reference. 123 | 124 | If the evaluation state within a container is lost (for example 125 | a process goes down), the runtime may send `{:container_down, cell, message}` 126 | to notify the owner. 127 | 128 | ## Options 129 | 130 | * `:file` - file to which the evaluated code belongs. Most importantly, 131 | this has an impact on the value of `__DIR__`. 132 | """ 133 | @spec evaluate_code(t(), String.t(), cell(), cell(), keyword()) :: :ok 134 | def evaluate_code(runtime, code, cell, prev_cell, opts \\ []) 135 | 136 | @doc """ 137 | Disposes of an evaluation identified by the given cell. 138 | 139 | This can be used to cleanup resources related to an old evaluation 140 | if no longer needed. 141 | """ 142 | @spec forget_evaluation(t(), cell()) :: :ok 143 | def forget_evaluation(runtime, cell) 144 | 145 | @doc """ 146 | Disposes of an evaluation container identified by the given ref. 147 | 148 | This should be used to cleanup resources keeping track of the 149 | container all of its evaluations. 150 | """ 151 | @spec drop_container(t(), cell()) :: :ok 152 | def drop_container(runtime, cell) 153 | 154 | @doc """ 155 | Asynchronously handles an completion request. 156 | 157 | This part of runtime functionality is used to provide 158 | language and context specific completion features in 159 | the text editor. 160 | 161 | The response is sent to the `send_to` process as 162 | `{:completion_response, ref, request, response}`. 163 | 164 | The given `cell` idenfities an evaluation that may be used 165 | as context when resolving the request (if relevant). 166 | """ 167 | @spec handle_completion(t(), String.t(), Integer.t(), cell()) :: :ok 168 | def handle_completion(runtime, code, cursor, cell) 169 | 170 | @doc """ 171 | Synchronously starts a runtime of the same type with the 172 | same parameters. 173 | """ 174 | @spec duplicate(Runtime.t()) :: {:ok, Runtime.t()} | {:error, String.t()} 175 | def duplicate(runtime) 176 | end 177 | -------------------------------------------------------------------------------- /lib/ielixir/runtime/elixir_standalone.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Runtime.ElixirStandalone do 2 | defstruct [:node, :server_pid] 3 | 4 | # A runtime backed by a standalone Elixir node managed by IElixir. 5 | # 6 | # IElixir is responsible for starting and terminating the node. 7 | # Most importantly we have to make sure the started node doesn't 8 | # stay in the system when the session or the entire IElixir terminates. 9 | 10 | import IElixir.Runtime.StandaloneInit 11 | import IElixir.Utils, only: [random_id: 0, node_host: 0] 12 | 13 | @type t :: %__MODULE__{ 14 | node: node(), 15 | server_pid: pid() 16 | } 17 | 18 | @doc """ 19 | Starts a new Elixir node (i.e. a system process) and initializes 20 | it with IElixir-specific modules and processes. 21 | 22 | If no process calls `Runtime.connect/1` for a period of time, 23 | the node automatically terminates. Whoever connects, becomes the owner 24 | and as soon as it terminates, the node terminates as well. 25 | The node may also be terminated manually by using `Runtime.disconnect/1`. 26 | 27 | Note: to start the node it is required that `elixir` is a recognised 28 | executable within the system. 29 | """ 30 | @spec init() :: {:ok, t()} | {:error, String.t()} 31 | def init() do 32 | parent_node = node() 33 | child_node = :"child-#{random_id()}@#{node_host()}" 34 | argv = [parent_node] 35 | 36 | with( 37 | {:ok, elixir_path} <- find_elixir_executable(), 38 | port = start_elixir_node(elixir_path, child_node, child_node_eval_string(), argv), 39 | {:ok, server_pid} <- parent_init_sequence(child_node, port) 40 | )do 41 | runtime = %__MODULE__{ 42 | node: child_node, 43 | server_pid: server_pid 44 | } 45 | 46 | {:ok, runtime} 47 | end 48 | end 49 | 50 | defp start_elixir_node(elixir_path, node_name, eval, argv) do 51 | args = elixir_flags(node_name) ++ ["--eval", eval, "--" | Enum.map(argv, &to_string/1)] 52 | Port.open({:spawn_executable, elixir_path}, [:nouse_stdio, :hide, args: args]) 53 | end 54 | end 55 | 56 | defimpl IElixir.Runtime, for: IElixir.Runtime.ElixirStandalone do 57 | alias IElixir.Runtime.ErlDist 58 | 59 | def connect(runtime) do 60 | ErlDist.RuntimeServer.set_owner(runtime.server_pid, self()) 61 | Process.monitor(runtime.server_pid) 62 | end 63 | 64 | def disconnect(runtime) do 65 | ErlDist.RuntimeServer.stop(runtime.server_pid) 66 | end 67 | 68 | def evaluate_code(runtime, code, current_cell, _previous_cell, opts \\ []) do 69 | ErlDist.RuntimeServer.evaluate_code(runtime.server_pid, code, current_cell, opts) 70 | end 71 | 72 | def forget_evaluation(runtime, locator) do 73 | ErlDist.RuntimeServer.forget_evaluation(runtime.server_pid, locator) 74 | end 75 | 76 | def drop_container(runtime, container_ref) do 77 | ErlDist.RuntimeServer.drop_container(runtime.server_pid, container_ref) 78 | end 79 | 80 | def handle_completion(runtime, code, cursor, cell) do 81 | ErlDist.RuntimeServer.handle_completion(runtime.server_pid, code, cursor, cell) 82 | end 83 | 84 | def duplicate(_runtime) do 85 | IElixir.Runtime.ElixirStandalone.init() 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/ielixir/runtime/erl_dist.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Runtime.ErlDist do 2 | @moduledoc false 3 | 4 | # This module allows for initializing nodes connected using 5 | # Erlang Distribution with modules and processes necessary for evaluation. 6 | # 7 | # To ensure proper isolation between sessions, 8 | # code evaluation may take place in a separate Elixir runtime, 9 | # which also makes it easy to terminate the whole 10 | # evaluation environment without stopping IElixir. 11 | # This is what `Runtime.ElixirStandalone`, `Runtime.MixStandalone` 12 | # and `Runtime.Attached` do, so this module contains the shared 13 | # functionality they need. 14 | # 15 | # To work with a separate node, we have to inject the necessary 16 | # IElixir modules there and also start the relevant processes 17 | # related to evaluation. Fortunately Erlang allows us to send modules 18 | # binary representation to the other node and load them dynamically. 19 | # 20 | # For further details see `IElixir.Runtime.ErlDist.NodeManager`. 21 | 22 | alias IElixir.Runtime.ErlDist.NodeManager 23 | 24 | # Modules to load into the connected node. 25 | @required_modules [ 26 | IElixir.Evaluator, 27 | IElixir.Evaluator.IOProxy, 28 | IElixir.Display, 29 | IElixir.Displayable, 30 | IElixir.Displayable.Any, 31 | IElixir.Completion, 32 | IElixir.Completion.IdentifierMatcher, 33 | IElixir.Runtime.ErlDist, 34 | IElixir.Runtime.ErlDist.NodeManager, 35 | IElixir.Runtime.ErlDist.RuntimeServer, 36 | IElixir.Runtime.ErlDist.EvaluatorSupervisor, 37 | IElixir.Runtime.ErlDist.IOForwardGL, 38 | IElixir.Runtime.ErlDist.LoggerGLBackend 39 | ] 40 | 41 | @doc """ 42 | Starts a runtime server on the given node. 43 | 44 | If necessary, the required modules are loaded 45 | into the given node and the node manager process 46 | is started with `node_manager_opts`. 47 | """ 48 | @spec initialize(node(), keyword()) :: pid() 49 | def initialize(node, node_manager_opts \\ []) do 50 | unless modules_loaded?(node) do 51 | load_required_modules(node) 52 | end 53 | 54 | unless node_manager_started?(node) do 55 | start_node_manager(node, node_manager_opts) 56 | end 57 | 58 | start_runtime_server(node) 59 | end 60 | 61 | defp load_required_modules(node) do 62 | for module <- @required_modules do 63 | {_module, binary, filename} = :code.get_object_code(module) 64 | 65 | case :rpc.call(node, :code, :load_binary, [module, filename, binary]) do 66 | {:module, _} -> 67 | :ok 68 | 69 | {:error, reason} -> 70 | local_otp = :erlang.system_info(:otp_release) 71 | remote_otp = :rpc.call(node, :erlang, :system_info, [:otp_release]) 72 | 73 | if local_otp != remote_otp do 74 | raise RuntimeError, 75 | "failed to load #{inspect(module)} module into the remote node, potentially due to Erlang/OTP version mismatch, reason: #{inspect(reason)} (local #{local_otp} != remote #{remote_otp})" 76 | else 77 | raise RuntimeError, 78 | "failed to load #{inspect(module)} module into the remote node, reason: #{inspect(reason)}" 79 | end 80 | end 81 | end 82 | end 83 | 84 | defp start_node_manager(node, opts) do 85 | :rpc.call(node, NodeManager, :start, [opts]) 86 | end 87 | 88 | defp start_runtime_server(node) do 89 | NodeManager.start_runtime_server(node) 90 | end 91 | 92 | defp modules_loaded?(node) do 93 | :rpc.call(node, Code, :ensure_loaded?, [NodeManager]) 94 | end 95 | 96 | defp node_manager_started?(node) do 97 | case :rpc.call(node, Process, :whereis, [NodeManager]) do 98 | nil -> false 99 | _pid -> true 100 | end 101 | end 102 | 103 | @doc """ 104 | Unloads the previously loaded IElixir modules from the caller node. 105 | """ 106 | def unload_required_modules() do 107 | for module <- @required_modules do 108 | # If we attached, detached and attached again, there may still 109 | # be deleted module code, so purge it first. 110 | :code.purge(module) 111 | :code.delete(module) 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/ielixir/runtime/erl_dist/evaluator_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Runtime.ErlDist.EvaluatorSupervisor do 2 | @moduledoc false 3 | 4 | # Supervisor responsible for dynamically spawning 5 | # and terminating evaluator server processes. 6 | 7 | use DynamicSupervisor 8 | 9 | alias IElixir.Evaluator 10 | 11 | def start_link(opts \\ []) do 12 | DynamicSupervisor.start_link(__MODULE__, opts) 13 | end 14 | 15 | @impl true 16 | def init(_opts) do 17 | DynamicSupervisor.init(strategy: :one_for_one) 18 | end 19 | 20 | @doc """ 21 | Spawns a new evaluator. 22 | """ 23 | @spec start_evaluator(pid()) :: {:ok, Evaluator.t()} | {:error, any()} 24 | def start_evaluator(supervisor) do 25 | case DynamicSupervisor.start_child( 26 | supervisor, 27 | {Evaluator, []} 28 | ) do 29 | {:ok, _pid, evaluator} -> {:ok, evaluator} 30 | {:error, reason} -> {:error, reason} 31 | end 32 | end 33 | 34 | @doc """ 35 | Terminates the given evaluator. 36 | """ 37 | @spec terminate_evaluator(pid(), Evaluator.t()) :: :ok 38 | def terminate_evaluator(supervisor, evaluator) do 39 | DynamicSupervisor.terminate_child(supervisor, evaluator.pid) 40 | :ok 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/ielixir/runtime/erl_dist/io_forward_gl.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Runtime.ErlDist.IOForwardGL do 2 | @moduledoc false 3 | 4 | # An IO device process forwarding all requests to sender's group leader. 5 | # 6 | # We use this device as `:standard_error` on connected runtime node, 7 | # so that all evaluation warnings are treated as stdout. 8 | # 9 | # The process implements [The Erlang I/O Protocol](https://erlang.org/doc/apps/stdlib/io_protocol.html) 10 | # and can be thought of as a *virtual* IO device. 11 | 12 | use GenServer 13 | 14 | @type state :: %{(reply_as :: term()) => from :: pid()} 15 | 16 | ## API 17 | 18 | @doc """ 19 | Starts the IO device. 20 | 21 | ## Options 22 | 23 | * `:name` - the name to regsiter the process under. Optional. 24 | If the name is already used, it will be unregistered before 25 | starting the process and registered back when the server 26 | terminates. 27 | """ 28 | @spec start_link() :: GenServer.on_start() 29 | def start_link(opts \\ []) do 30 | name = opts[:name] 31 | 32 | if previous = name && Process.whereis(name) do 33 | Process.unregister(name) 34 | end 35 | 36 | GenServer.start_link(__MODULE__, {name, previous}, opts) 37 | end 38 | 39 | ## Callbacks 40 | 41 | @impl true 42 | def init({name, previous}) do 43 | Process.flag(:trap_exit, true) 44 | {:ok, %{previous: {name, previous}, replies: %{}}} 45 | end 46 | 47 | @impl true 48 | def handle_info({:io_request, from, reply_as, req}, state) do 49 | case Process.info(from, :group_leader) do 50 | {:group_leader, group_leader} -> 51 | # Forward the request to sender's group leader 52 | # and instruct it to get back to us. 53 | send(group_leader, {:io_request, self(), reply_as, req}) 54 | state = put_in(state.replies[reply_as], from) 55 | 56 | {:noreply, state} 57 | 58 | _ -> 59 | {:noreply, state} 60 | end 61 | end 62 | 63 | def handle_info({:io_reply, reply_as, reply}, state) do 64 | # Forward the reply from group leader to the original client. 65 | {initially_from, state} = pop_in(state.replies[reply_as]) 66 | send(initially_from, {:io_reply, reply_as, reply}) 67 | 68 | {:noreply, state} 69 | end 70 | 71 | @impl true 72 | def terminate(_, %{previous: {name, previous}}) do 73 | if name && previous do 74 | Process.unregister(name) 75 | Process.register(previous, name) 76 | end 77 | 78 | :ok 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/ielixir/runtime/erl_dist/logger_gl_backend.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Runtime.ErlDist.LoggerGLBackend do 2 | @moduledoc false 3 | 4 | # A logger backend used to forward logs to IElixir, 5 | # as with regular output. 6 | # 7 | # The backend is based on `Logger.Backends.Console`, 8 | # but instead of logging to the console, it sends 9 | # log output to the group leader of the soruce process, 10 | # provided the group leader is an instance of 11 | # `IElixir.Evaluator.IOProxy`. 12 | # 13 | # Basic configuration is taken from the console 14 | # logger configuration to match its formatting. 15 | 16 | @behaviour :gen_event 17 | 18 | @type state :: %{ 19 | colors: keyword(), 20 | format: binary(), 21 | level: atom(), 22 | metadata: list(atom()) 23 | } 24 | 25 | @impl true 26 | def init(__MODULE__) do 27 | config = Application.get_env(:logger, :console) 28 | {:ok, init_state(config)} 29 | end 30 | 31 | @impl true 32 | def handle_event({level, gl, {Logger, msg, ts, md}}, state) do 33 | %{level: log_level} = state 34 | 35 | if meet_level?(level, log_level) do 36 | log_event(level, msg, ts, md, gl, state) 37 | end 38 | 39 | {:ok, state} 40 | end 41 | 42 | def handle_event(_, state) do 43 | {:ok, state} 44 | end 45 | 46 | @impl true 47 | def code_change(_old_vsn, state, _extra) do 48 | {:ok, state} 49 | end 50 | 51 | @impl true 52 | def handle_call(_message, state) do 53 | {:ok, :ok, state} 54 | end 55 | 56 | @impl true 57 | def handle_info(_message, state) do 58 | {:ok, state} 59 | end 60 | 61 | @impl true 62 | def terminate(_reason, _state) do 63 | :ok 64 | end 65 | 66 | defp init_state(config) do 67 | level = Keyword.get(config, :level) 68 | format = Logger.Formatter.compile(Keyword.get(config, :format)) 69 | colors = configure_colors(config) 70 | metadata = Keyword.get(config, :metadata, []) |> configure_metadata() 71 | 72 | %{format: format, metadata: metadata, level: level, colors: colors} 73 | end 74 | 75 | defp configure_metadata(:all), do: :all 76 | defp configure_metadata(metadata), do: Enum.reverse(metadata) 77 | 78 | defp configure_colors(config) do 79 | colors = Keyword.get(config, :colors, []) 80 | 81 | %{ 82 | debug: Keyword.get(colors, :debug, :cyan), 83 | info: Keyword.get(colors, :info, :normal), 84 | warn: Keyword.get(colors, :warn, :yellow), 85 | error: Keyword.get(colors, :error, :red), 86 | enabled: Keyword.get(colors, :enabled, IO.ANSI.enabled?()) 87 | } 88 | end 89 | 90 | defp meet_level?(_lvl, nil), do: true 91 | 92 | defp meet_level?(lvl, min) do 93 | Logger.compare_levels(lvl, min) != :lt 94 | end 95 | 96 | defp log_event(level, msg, ts, md, gl, state) do 97 | if io_proxy?(gl) do 98 | output = format_event(level, msg, ts, md, state) 99 | async_io(gl, output) 100 | end 101 | end 102 | 103 | defp io_proxy?(pid) do 104 | info = Process.info(pid, [:dictionary]) 105 | info[:dictionary][:"$initial_call"] == {IElixir.Evaluator.IOProxy, :init, 1} 106 | end 107 | 108 | defp async_io(device, output) when is_pid(device) do 109 | send(device, {:io_request, self(), make_ref(), {:put_chars, :unicode, output}}) 110 | end 111 | 112 | defp format_event(level, msg, ts, md, state) do 113 | %{format: format, metadata: keys, colors: colors} = state 114 | 115 | format 116 | |> Logger.Formatter.format(level, msg, ts, take_metadata(md, keys)) 117 | |> color_event(level, colors, md) 118 | end 119 | 120 | defp take_metadata(metadata, :all) do 121 | metadata 122 | end 123 | 124 | defp take_metadata(metadata, keys) do 125 | Enum.reduce(keys, [], fn key, acc -> 126 | case Keyword.fetch(metadata, key) do 127 | {:ok, val} -> [{key, val} | acc] 128 | :error -> acc 129 | end 130 | end) 131 | end 132 | 133 | defp color_event(data, _level, %{enabled: false}, _md), do: data 134 | 135 | defp color_event(data, level, %{enabled: true} = colors, md) do 136 | color = md[:ansi_color] || Map.fetch!(colors, level) 137 | IO.ANSI.format([color, data]) 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /lib/ielixir/runtime/erl_dist/node_manager.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Runtime.ErlDist.NodeManager do 2 | @moduledoc false 3 | 4 | # The primary IElixir process started on a remote node. 5 | # 6 | # This process is responsible for initializing the node 7 | # with necessary runtime configuration and then starting 8 | # runtime server processes, one per runtime. 9 | # This approach allows for multiple runtimes connected 10 | # to the same node, while preserving the necessary 11 | # cleanup semantics. 12 | # 13 | # The manager process terminates as soon as the last runtime 14 | # server terminates. Upon termination the manager reverts the 15 | # runtime configuration back to the initial state. 16 | 17 | use GenServer 18 | 19 | alias IElixir.Runtime.ErlDist 20 | 21 | @name __MODULE__ 22 | 23 | @doc """ 24 | Starts the node manager. 25 | 26 | ## Options 27 | 28 | * `:unload_modules_on_termination` - whether to unload all 29 | IElixir related modules from the node on termination. 30 | Defaults to `true`. 31 | 32 | * `:anonymous` - configures whether manager should 33 | be registered under a global name or not. 34 | In most cases we enforce a single manager per node 35 | and identify it by a name, but this can be opted-out 36 | from by using this option. Defaults to `false`. 37 | """ 38 | def start(opts \\ []) do 39 | {opts, gen_opts} = split_opts(opts) 40 | GenServer.start(__MODULE__, opts, gen_opts) 41 | end 42 | 43 | @doc """ 44 | Starts the node manager with link. 45 | 46 | See `start/1` for available options. 47 | """ 48 | def start_link(opts \\ []) do 49 | {opts, gen_opts} = split_opts(opts) 50 | GenServer.start_link(__MODULE__, opts, gen_opts) 51 | end 52 | 53 | defp split_opts(opts) do 54 | {anonymous?, opts} = Keyword.pop(opts, :anonymous, false) 55 | 56 | gen_opts = [ 57 | name: if(anonymous?, do: nil, else: @name) 58 | ] 59 | 60 | {opts, gen_opts} 61 | end 62 | 63 | @doc """ 64 | Starts a new `IElixir.Runtime.ErlDist.RuntimeServer` for evaluation. 65 | """ 66 | @spec start_runtime_server(node() | pid()) :: pid() 67 | def start_runtime_server(node_or_pid) do 68 | GenServer.call(server(node_or_pid), :start_runtime_server) 69 | end 70 | 71 | defp server(pid) when is_pid(pid), do: pid 72 | defp server(node) when is_atom(node), do: {@name, node} 73 | 74 | @impl true 75 | def init(opts) do 76 | unload_modules_on_termination = Keyword.get(opts, :unload_modules_on_termination, true) 77 | 78 | ## Initialize the node 79 | 80 | Process.flag(:trap_exit, true) 81 | 82 | {:ok, server_supevisor} = DynamicSupervisor.start_link(strategy: :one_for_one) 83 | 84 | # Register our own standard error IO device that proxies 85 | # to sender's group leader. 86 | original_standard_error = Process.whereis(:standard_error) 87 | {:ok, io_forward_gl_pid} = ErlDist.IOForwardGL.start_link() 88 | Process.unregister(:standard_error) 89 | Process.register(io_forward_gl_pid, :standard_error) 90 | 91 | Logger.add_backend(IElixir.Runtime.ErlDist.LoggerGLBackend) 92 | 93 | # Set `ignore_module_conflict` only for the NodeManager lifetime. 94 | initial_ignore_module_conflict = Code.compiler_options()[:ignore_module_conflict] 95 | Code.compiler_options(ignore_module_conflict: true) 96 | 97 | {:ok, 98 | %{ 99 | unload_modules_on_termination: unload_modules_on_termination, 100 | server_supevisor: server_supevisor, 101 | runtime_servers: [], 102 | initial_ignore_module_conflict: initial_ignore_module_conflict, 103 | original_standard_error: original_standard_error 104 | }} 105 | end 106 | 107 | @impl true 108 | def terminate(_reason, state) do 109 | Code.compiler_options(ignore_module_conflict: state.initial_ignore_module_conflict) 110 | 111 | Process.unregister(:standard_error) 112 | Process.register(state.original_standard_error, :standard_error) 113 | 114 | Logger.remove_backend(IElixir.Runtime.ErlDist.LoggerGLBackend) 115 | 116 | if state.unload_modules_on_termination do 117 | ErlDist.unload_required_modules() 118 | end 119 | 120 | :ok 121 | end 122 | 123 | @impl true 124 | def handle_info({:DOWN, _, :process, pid, _}, state) do 125 | if pid in state.runtime_servers do 126 | case update_in(state.runtime_servers, &List.delete(&1, pid)) do 127 | %{runtime_servers: []} = state -> {:stop, :normal, state} 128 | state -> {:noreply, state} 129 | end 130 | else 131 | {:noreply, state} 132 | end 133 | end 134 | 135 | def handle_info(_message, state), do: {:noreply, state} 136 | 137 | @impl true 138 | def handle_call(:start_runtime_server, _from, state) do 139 | {:ok, server_pid} = 140 | DynamicSupervisor.start_child(state.server_supevisor, ErlDist.RuntimeServer) 141 | 142 | Process.monitor(server_pid) 143 | state = update_in(state.runtime_servers, &[server_pid | &1]) 144 | 145 | {:reply, server_pid, state} 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /lib/ielixir/runtime/erl_dist/runtime_server.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Runtime.ErlDist.RuntimeServer do 2 | @moduledoc false 3 | 4 | # A server process backing a specific runtime. 5 | # 6 | # This process handles `IElixir.Runtime` operations, 7 | # like evaluation and completion. It spawns/terminates 8 | # individual evaluators corresponding to evaluation 9 | # containers as necessary. 10 | # 11 | # Every runtime server must have an owner process, 12 | # to which the server lifetime is bound. 13 | # 14 | # For more specification see `IElixir.Runtime`. 15 | 16 | require Logger 17 | 18 | use GenServer, restart: :temporary 19 | 20 | alias IElixir.Evaluator 21 | alias IElixir.Runtime 22 | alias IElixir.Runtime.ErlDist.EvaluatorSupervisor 23 | 24 | @await_owner_timeout 5_000 25 | 26 | @doc """ 27 | Starts the manager. 28 | 29 | Note: make sure to call `set_owner` within #{@await_owner_timeout}ms 30 | or the runtime server assumes it's not needed and terminates. 31 | """ 32 | def start_link(opts \\ []) do 33 | GenServer.start_link(__MODULE__, opts) 34 | end 35 | 36 | @doc """ 37 | Sets the owner process. 38 | 39 | The owner process is monitored and as soon as it terminates, 40 | the server also terminates. All the evaluation results are 41 | send directly to the owner. 42 | """ 43 | @spec set_owner(pid(), pid()) :: :ok 44 | def set_owner(pid, owner) do 45 | GenServer.cast(pid, {:set_owner, owner}) 46 | end 47 | 48 | @doc """ 49 | Evaluates the given code using an `IElixir.Evaluator` 50 | process belonging to the given container and instructs 51 | it to send all the outputs to the owner process. 52 | 53 | If no evaluator exists for the given container, a new 54 | one is started. 55 | 56 | See `IElixir.Evaluator` for more details. 57 | """ 58 | def evaluate_code(pid, code, cell, opts \\ []) do 59 | GenServer.cast(pid, {:evaluate_code, code, cell, opts}) 60 | end 61 | 62 | @doc """ 63 | Removes the specified evaluation from the history. 64 | 65 | See `IElixir.Evaluator` for more details. 66 | """ 67 | @spec forget_evaluation(pid(), Runtime.locator()) :: :ok 68 | def forget_evaluation(pid, locator) do 69 | GenServer.cast(pid, {:forget_evaluation, locator}) 70 | end 71 | 72 | @doc """ 73 | Terminates the `IElixir.Evaluator` process that belongs 74 | to the given container. 75 | """ 76 | @spec drop_container(pid(), Runtime.cell()) :: :ok 77 | def drop_container(pid, cell) do 78 | GenServer.cast(pid, {:drop_container, cell}) 79 | end 80 | 81 | def handle_completion(pid, code, cursor, cell) do 82 | GenServer.cast(pid, {:handle_completion, self(), code, cursor, cell}) 83 | end 84 | 85 | @doc """ 86 | Stops the manager. 87 | 88 | This results in all IElixir-related modules being unloaded 89 | from the runtime node. 90 | """ 91 | @spec stop(pid()) :: :ok 92 | def stop(pid) do 93 | GenServer.stop(pid) 94 | end 95 | 96 | @impl true 97 | def init(_opts) do 98 | Process.send_after(self(), :check_owner, @await_owner_timeout) 99 | 100 | {:ok, evaluator_supervisor} = EvaluatorSupervisor.start_link() 101 | {:ok, completion_supervisor} = Task.Supervisor.start_link() 102 | {:ok, evaluator} = EvaluatorSupervisor.start_evaluator(evaluator_supervisor) 103 | Process.monitor(evaluator.pid) 104 | 105 | {:ok, 106 | %{ 107 | owner: nil, 108 | evaluator: evaluator, 109 | evaluator_supervisor: evaluator_supervisor, 110 | completion_supervisor: completion_supervisor 111 | }} 112 | end 113 | 114 | @impl true 115 | def handle_info(:check_owner, state) do 116 | # If not owner has been set within @await_owner_timeout 117 | # from the start, terminate the process. 118 | if state.owner do 119 | {:noreply, state} 120 | else 121 | {:stop, :no_owner, state} 122 | end 123 | end 124 | 125 | def handle_info({:DOWN, _, :process, owner, _}, %{owner: owner} = state) do 126 | {:stop, :normal, state} 127 | end 128 | 129 | def handle_info({:DOWN, _, :process, pid, reason}, %{evaluator_supervisor: evaluator_supervisor} = state) do 130 | Logger.error("Evaluator #{inspect pid} stopped with #{inspect reason}. Starting new evaluator") 131 | {:ok, evaluator} = EvaluatorSupervisor.start_evaluator(evaluator_supervisor) 132 | {:noreply, %{state | evaluator: evaluator}} 133 | end 134 | 135 | def handle_info(_message, state), do: {:noreply, state} 136 | 137 | @impl true 138 | def handle_cast({:set_owner, owner}, state) do 139 | Process.monitor(owner) 140 | {:noreply, %{state | owner: owner}} 141 | end 142 | 143 | def handle_cast( 144 | {:evaluate_code, code, current_cell, opts}, 145 | %{evaluator: evaluator} = state 146 | ) do 147 | Evaluator.evaluate_code( 148 | evaluator, 149 | state.owner, 150 | code, 151 | current_cell, 152 | opts 153 | ) 154 | 155 | {:noreply, state} 156 | end 157 | 158 | def handle_cast({:handle_completion, send_to, code, cursor, cell}, %{evaluator: evaluator} = state) do 159 | Evaluator.handle_completion(evaluator, send_to, code, cursor, cell) 160 | {:noreply, state} 161 | end 162 | 163 | end 164 | -------------------------------------------------------------------------------- /lib/ielixir/runtime/standalone_init.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Runtime.StandaloneInit do 2 | @moduledoc false 3 | 4 | # Generic functionality related to starting and setting up 5 | # a new Elixir system process. It's used by both ElixirStandalone 6 | # and MixStandalone runtimes. 7 | 8 | alias IElixir.Utils.Emitter 9 | alias IElixir.Runtime.ErlDist 10 | 11 | @doc """ 12 | Tries locating Elixir executable in PATH. 13 | """ 14 | @spec find_elixir_executable() :: {:ok, String.t()} | {:error, String.t()} 15 | def find_elixir_executable() do 16 | case System.find_executable("elixir") do 17 | nil -> {:error, "no Elixir executable found in PATH"} 18 | path -> {:ok, path} 19 | end 20 | end 21 | 22 | @doc """ 23 | A list of common flags used for spawned Elixir runtimes. 24 | """ 25 | @spec elixir_flags(node()) :: list() 26 | def elixir_flags(node_name) do 27 | [ 28 | "--name", 29 | to_string(node_name), 30 | "--erl", 31 | # Minimize schedulers busy wait threshold, 32 | # so that they go to sleep immediately after evaluation. 33 | # Enable ANSI escape codes as we handle them with HTML. 34 | # Disable stdin, so that the system process never tries to read 35 | # any input from the terminal. 36 | "+sbwt none +sbwtdcpu none +sbwtdio none -elixir ansi_enabled true -noinput", 37 | # Make the node hidden, so it doesn't automatically join the cluster 38 | "--hidden", 39 | # Use the cookie in IElixir 40 | "--cookie", 41 | Atom.to_string(Node.get_cookie()) 42 | ] 43 | end 44 | 45 | # --- 46 | # 47 | # Once the new node is spawned we need to establish a connection, 48 | # initialize it and make sure it correctly reacts to the parent node terminating. 49 | # 50 | # The procedure goes as follows: 51 | # 52 | # 1. The child sends {:node_initialized, ref} message to the parent 53 | # to communicate it's ready for initialization. 54 | # 55 | # 2. The parent initializes the child node - loads necessary modules, 56 | # starts the NodeManager process and a single RuntimeServer process. 57 | # 58 | # 3. The parent sends {:node_initialized, ref} message back to the child, 59 | # to communicate successful initialization. 60 | # 61 | # 4. The child starts monitoring the NodeManager process and freezes 62 | # until the NodeManager process terminates. The NodeManager process 63 | # serves as the leading remote process and represents the node from now on. 64 | # 65 | # The nodes either successfully go through this flow or return an error, 66 | # either if the other node dies or is not responding for too long. 67 | # 68 | # --- 69 | 70 | @doc """ 71 | Performs the parent side of the initialization contract. 72 | 73 | Should be called by the initializing process on the parent node. 74 | """ 75 | @spec parent_init_sequence(node(), port(), Emitter.t() | nil) :: 76 | {:ok, pid()} | {:error, String.t()} 77 | def parent_init_sequence(child_node, port, emitter \\ nil) do 78 | port_ref = Port.monitor(port) 79 | 80 | loop = fn loop -> 81 | receive do 82 | {:node_started, init_ref, ^child_node, primary_pid} -> 83 | Port.demonitor(port_ref) 84 | 85 | server_pid = ErlDist.initialize(child_node) 86 | 87 | send(primary_pid, {:node_initialized, init_ref}) 88 | 89 | {:ok, server_pid} 90 | 91 | {^port, {:data, output}} -> 92 | # Pass all the outputs through the given emitter. 93 | emitter && Emitter.emit(emitter, output) 94 | loop.(loop) 95 | 96 | {:DOWN, ^port_ref, :port, _object, _reason} -> 97 | {:error, "Elixir process terminated unexpectedly"} 98 | after 99 | 10_000 -> 100 | {:error, "connection timed out"} 101 | end 102 | end 103 | 104 | loop.(loop) 105 | end 106 | 107 | #TODO remove debug 108 | # Note Windows does not handle escaped quotes and newlines the same way as Unix, 109 | # so the string cannot have constructs newlines nor strings. That's why we pass 110 | # the parent node name as ARGV and write the code avoiding newlines. 111 | @child_node_eval_string """ 112 | [parent_node] = System.argv();\ 113 | parent_node = String.to_atom(parent_node);\ 114 | init_ref = make_ref();\ 115 | parent_process = {IElixir.Kernel.Session, parent_node};\ 116 | send(parent_process, {:node_started, init_ref, node(), self()});\ 117 | receive do {:node_initialized, ^init_ref} ->\ 118 | manager_ref = Process.monitor(IElixir.Runtime.ErlDist.NodeManager);\ 119 | receive do {:DOWN, ^manager_ref, :process, _object, _reason} -> :ok end;\ 120 | after 10_000 ->\ 121 | :timeout;\ 122 | end;\ 123 | """ 124 | 125 | if @child_node_eval_string =~ "\n" do 126 | raise "invalid @child_node_eval_string, newline found: #{inspect(@child_node_eval_string)}" 127 | end 128 | 129 | @doc """ 130 | Performs the child side of the initialization contract. 131 | 132 | This function returns AST that should be evaluated in primary 133 | process on the newly spawned child node. The executed code expects 134 | the parent_node on ARGV. The process on the parent node is assumed 135 | to have the same name as the child node. 136 | """ 137 | def child_node_eval_string(), do: @child_node_eval_string 138 | end 139 | -------------------------------------------------------------------------------- /lib/ielixir/util/crypto.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Util.Crypto do 2 | @spec compute_signature( 3 | algo :: atom(), 4 | key :: String.t(), 5 | packet_parts :: list(String.t()) 6 | ) :: bitstring() 7 | def compute_signature(algo, key, packet_parts) do 8 | ctx = 9 | Enum.reduce( 10 | packet_parts, 11 | :crypto.mac_init(:hmac, algo, key), 12 | &:crypto.mac_update(&2, &1) 13 | ) 14 | |> :crypto.mac_final() 15 | 16 | for <>, into: <<>>, do: <> 17 | end 18 | 19 | defp to_hex_char(i) when i >= 0 and i < 10, do: ?0 + i 20 | defp to_hex_char(i) when i >= 10 and i < 16, do: ?a + (i - 10) 21 | end 22 | -------------------------------------------------------------------------------- /lib/ielixir/util/magic_command.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Util.MagicCommand do 2 | @magic_command_regexp ~r/^%[a-z][a-zA-Z0-9]*.*$/ 3 | 4 | @spec parse(code :: String.t()) :: term() 5 | def parse(code) do 6 | case Regex.match?(@magic_command_regexp, code) do 7 | true -> 8 | {:ok, 9 | code 10 | |> String.trim_leading("%") 11 | |> String.split(~r/\W+/)} 12 | 13 | false -> 14 | {:error, code} 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ielixir/util/substring.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Util.Substring do 2 | @spec calculate_substring_position(string :: String.t(), substitutions :: list(String.t())) :: 3 | integer() 4 | def calculate_substring_position(string, [first_substitution | _] = _substitutions) do 5 | default_position = String.length(string) 6 | 7 | start_position = 8 | Enum.reduce_while((default_position - 1)..0, :not_found, fn grapheme_index, acc -> 9 | if String.starts_with?( 10 | first_substitution, 11 | String.slice(string, grapheme_index, default_position) 12 | ) do 13 | {:halt, grapheme_index} 14 | else 15 | {:cont, acc} 16 | end 17 | end) 18 | 19 | if String.starts_with?(first_substitution, string) do 20 | 0 21 | else 22 | case start_position do 23 | :not_found -> default_position 24 | value when is_integer(value) -> value 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/ielixir/util/tmp.done.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Util.Tmp do 2 | @doc """ 3 | Function is to create a temporary directory path where all the kernel initializtion files will be created 4 | """ 5 | @spec mktemp(binary) :: {:ok, String.t()} | {:error, :enoent} 6 | def mktemp(template \\ "XXXXXX") do 7 | case System.cmd("mktemp", ["-d", "-t", template], stderr_to_stdout: true) do 8 | {path, 0} -> {:ok, String.trim(path)} 9 | _ -> {:error, :enoent} 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/ielixir/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Utils do 2 | @moduledoc false 3 | 4 | @type id :: binary() 5 | 6 | @doc """ 7 | Generates a random binary id. 8 | """ 9 | @spec random_id() :: id() 10 | def random_id() do 11 | :crypto.strong_rand_bytes(20) |> Base.encode32(case: :lower) 12 | end 13 | 14 | @doc """ 15 | Returns the host part of a node. 16 | """ 17 | @spec node_host() :: binary() 18 | def node_host do 19 | [_, host] = node() |> Atom.to_string() |> :binary.split("@") 20 | host 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /lib/ielixir/utils/ansi.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Utils.ANSI.Modifier do 2 | @moduledoc false 3 | 4 | defmacro defmodifier(modifier, code, terminator \\ "m") do 5 | quote bind_quoted: [modifier: modifier, code: code, terminator: terminator] do 6 | defp ansi_prefix_to_modifier(unquote("[#{code}#{terminator}") <> rest) do 7 | {:ok, unquote(modifier), rest} 8 | end 9 | end 10 | end 11 | end 12 | 13 | defmodule IElixir.Utils.ANSI do 14 | @moduledoc false 15 | 16 | import IElixir.Utils.ANSI.Modifier 17 | 18 | @type modifier :: 19 | {:font_weight, :bold | :light} 20 | | {:font_style, :italic} 21 | | {:text_decoration, :underline | :line_through | :overline} 22 | | {:foreground_color, color()} 23 | | {:background_color, color()} 24 | 25 | @type color :: basic_color() | {:grayscale24, 0..23} | {:rgb6, 0..5, 0..5, 0..5} 26 | 27 | @type basic_color :: 28 | :black 29 | | :red 30 | | :green 31 | | :yellow 32 | | :blue 33 | | :magenta 34 | | :cyan 35 | | :white 36 | | :light_black 37 | | :light_red 38 | | :light_green 39 | | :light_yellow 40 | | :light_blue 41 | | :light_magenta 42 | | :light_cyan 43 | | :light_white 44 | 45 | @doc """ 46 | Takes a string with ANSI escape codes and parses it 47 | into a list of `{modifiers, string}` parts. 48 | """ 49 | @spec parse_ansi_string(String.t()) :: list({list(modifier()), String.t()}) 50 | def parse_ansi_string(string) do 51 | [head | ansi_prefixed_strings] = String.split(string, "\e") 52 | 53 | # Each part has the form of {modifiers, string} 54 | {tail_parts, _} = 55 | Enum.map_reduce(ansi_prefixed_strings, %{}, fn string, modifiers -> 56 | {modifiers, rest} = 57 | case ansi_prefix_to_modifier(string) do 58 | {:ok, modifier, rest} -> 59 | modifiers = add_modifier(modifiers, modifier) 60 | {modifiers, rest} 61 | 62 | {:error, _rest} -> 63 | {modifiers, "\e" <> string} 64 | end 65 | 66 | {{Map.to_list(modifiers), rest}, modifiers} 67 | end) 68 | 69 | parts = [{[], head} | tail_parts] 70 | 71 | parts 72 | |> Enum.reject(fn {_modifiers, string} -> string == "" end) 73 | |> merge_adjacent_parts([]) 74 | end 75 | 76 | defp merge_adjacent_parts([], acc), do: Enum.reverse(acc) 77 | 78 | defp merge_adjacent_parts([{modifiers, string1}, {modifiers, string2} | parts], acc) do 79 | merge_adjacent_parts([{modifiers, string1 <> string2} | parts], acc) 80 | end 81 | 82 | defp merge_adjacent_parts([part | parts], acc) do 83 | merge_adjacent_parts(parts, [part | acc]) 84 | end 85 | 86 | # Below goes a number of `ansi_prefix_to_modifier` function definitions, 87 | # that take a string like "[32msomething" (starting with ANSI code without the leading "\e") 88 | # and parse the prefix into the corresponding modifier. 89 | # The function returns either {:ok, modifier, rest} or {:error, rest} 90 | 91 | defmodifier(:reset, 0) 92 | 93 | # When the code is missing (i.e., "\e[m"), it is 0 for reset. 94 | defmodifier(:reset, "") 95 | 96 | @colors [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white] 97 | 98 | for {color, index} <- Enum.with_index(@colors) do 99 | defmodifier({:foreground_color, color}, 30 + index) 100 | defmodifier({:background_color, color}, 40 + index) 101 | defmodifier({:foreground_color, :"light_#{color}"}, 90 + index) 102 | defmodifier({:background_color, :"light_#{color}"}, 100 + index) 103 | end 104 | 105 | defmodifier({:foreground_color, :reset}, 39) 106 | defmodifier({:background_color, :reset}, 49) 107 | 108 | defmodifier({:font_weight, :bold}, 1) 109 | defmodifier({:font_weight, :light}, 2) 110 | defmodifier({:font_style, :italic}, 3) 111 | defmodifier({:text_decoration, :underline}, 4) 112 | defmodifier({:text_decoration, :line_through}, 9) 113 | defmodifier({:font_weight, :reset}, 22) 114 | defmodifier({:font_style, :reset}, 23) 115 | defmodifier({:text_decoration, :reset}, 24) 116 | defmodifier({:text_decoration, :overline}, 53) 117 | defmodifier({:text_decoration, :reset}, 55) 118 | 119 | defp ansi_prefix_to_modifier("[38;5;" <> string) do 120 | with {:ok, color, rest} <- bit8_prefix_to_color(string) do 121 | {:ok, {:foreground_color, color}, rest} 122 | end 123 | end 124 | 125 | defp ansi_prefix_to_modifier("[48;5;" <> string) do 126 | with {:ok, color, rest} <- bit8_prefix_to_color(string) do 127 | {:ok, {:background_color, color}, rest} 128 | end 129 | end 130 | 131 | # "\e(B" is RFC1468's switch to ASCII character set and can be ignored. This 132 | # can appear even when JIS character sets aren't in use. 133 | defp ansi_prefix_to_modifier("(B" <> rest) do 134 | {:ok, :ignored, rest} 135 | end 136 | 137 | defp bit8_prefix_to_color(string) do 138 | case Integer.parse(string) do 139 | {n, "m" <> rest} when n in 0..255 -> 140 | color = color_from_code(n) 141 | {:ok, color, rest} 142 | 143 | _ -> 144 | {:error, string} 145 | end 146 | end 147 | 148 | ignored_codes = [5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 25, 27, 51, 52, 54] 149 | 150 | for code <- ignored_codes do 151 | defmodifier(:ignored, code) 152 | end 153 | 154 | defmodifier(:ignored, 1, "A") 155 | defmodifier(:ignored, 1, "B") 156 | defmodifier(:ignored, 1, "C") 157 | defmodifier(:ignored, 1, "D") 158 | defmodifier(:ignored, 2, "J") 159 | defmodifier(:ignored, 2, "K") 160 | defmodifier(:ignored, "", "H") 161 | 162 | defp ansi_prefix_to_modifier(string), do: {:error, string} 163 | 164 | defp color_from_code(code) when code in 0..7 do 165 | Enum.at(@colors, code) 166 | end 167 | 168 | defp color_from_code(code) when code in 8..15 do 169 | color = Enum.at(@colors, code - 8) 170 | :"light_#{color}" 171 | end 172 | 173 | defp color_from_code(code) when code in 16..231 do 174 | rgb_code = code - 16 175 | b = rgb_code |> rem(6) 176 | g = rgb_code |> div(6) |> rem(6) 177 | r = rgb_code |> div(36) 178 | 179 | {:rgb6, r, g, b} 180 | end 181 | 182 | defp color_from_code(code) when code in 232..255 do 183 | level = code - 232 184 | {:grayscale24, level} 185 | end 186 | 187 | defp add_modifier(modifiers, :ignored), do: modifiers 188 | defp add_modifier(_modifiers, :reset), do: %{} 189 | defp add_modifier(modifiers, {key, :reset}), do: Map.delete(modifiers, key) 190 | defp add_modifier(modifiers, {key, value}), do: Map.put(modifiers, key, value) 191 | end 192 | -------------------------------------------------------------------------------- /lib/ielixir/utils/emitter.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Utils.Emitter do 2 | @moduledoc false 3 | 4 | # A wrapper struct for sending messages to the specified process. 5 | 6 | defstruct [:target_pid, :ref, :mapper] 7 | 8 | @type t :: %__MODULE__{ 9 | target_pid: pid(), 10 | ref: reference(), 11 | mapper: mapper() 12 | } 13 | 14 | @type mapper :: (term() -> term()) 15 | 16 | @doc """ 17 | Builds a new structure where `target_pid` represents 18 | the process that will receive all emitted items. 19 | """ 20 | @spec new(pid()) :: t() 21 | def new(target_pid) do 22 | %__MODULE__{target_pid: target_pid, ref: make_ref(), mapper: &Function.identity/1} 23 | end 24 | 25 | @doc """ 26 | Sends {:emitter, ref, item} message to the `target_pid`. 27 | 28 | Note that item may be transformed with emitter's `mapper` 29 | if there is one, see `Emitter.mapper/2`. 30 | """ 31 | @spec emit(t(), term()) :: :ok 32 | def emit(emitter, item) do 33 | message = {:emitter, emitter.ref, emitter.mapper.(item)} 34 | send(emitter.target_pid, message) 35 | :ok 36 | end 37 | 38 | @doc """ 39 | Returns a new emitter that maps all emitted items with `mapper`. 40 | """ 41 | @spec mapper(t(), mapper()) :: t() 42 | def mapper(emitter, mapper) do 43 | mapper = fn x -> mapper.(emitter.mapper.(x)) end 44 | %{emitter | mapper: mapper} 45 | end 46 | end 47 | 48 | defimpl Collectable, for: IElixir.Utils.Emitter do 49 | alias IElixir.Utils.Emitter 50 | 51 | def into(emitter) do 52 | collector_fun = fn 53 | :ok, {:cont, item} -> Emitter.emit(emitter, item) 54 | :ok, _ -> :ok 55 | end 56 | 57 | {:ok, collector_fun} 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/ielixir/utils/graph.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Utils.Graph do 2 | @moduledoc false 3 | 4 | @typedoc """ 5 | A bottom-up graph representation encoded as a map 6 | of child-to-parent entries. 7 | """ 8 | @type t() :: %{node_id => node_id | nil} 9 | 10 | @type t(node_id) :: %{node_id => node_id | nil} 11 | 12 | @type node_id :: term() 13 | 14 | @doc """ 15 | Finds a path between nodes `from_id` and `to_id`. 16 | 17 | If the path exists, a top-down list of nodes is 18 | returned including the extreme nodes. Otherwise, 19 | an empty list is returned. 20 | """ 21 | @spec find_path(t(), node_id(), node_id()) :: list(node_id()) 22 | def find_path(graph, from_id, to_id) do 23 | find_path(graph, from_id, to_id, []) 24 | end 25 | 26 | defp find_path(_graph, to_id, to_id, path), do: [to_id | path] 27 | defp find_path(_graph, nil, _to_id, _path), do: [] 28 | 29 | defp find_path(graph, from_id, to_id, path), 30 | do: find_path(graph, graph[from_id], to_id, [from_id | path]) 31 | 32 | @doc """ 33 | Finds grpah leave nodes, that is, nodes with 34 | no children. 35 | """ 36 | @spec leaves(t()) :: list(node_id()) 37 | def leaves(graph) do 38 | children = MapSet.new(graph, fn {key, _} -> key end) 39 | parents = MapSet.new(graph, fn {_, value} -> value end) 40 | MapSet.difference(children, parents) |> MapSet.to_list() 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/ielixir/utils/time.ex: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Utils.Time do 2 | @moduledoc false 3 | 4 | # A simplified version of https://gist.github.com/tlemens/88e9b08f62150ba6082f478a4a03ac52 5 | 6 | @doc """ 7 | Formats the given point in time relatively to present. 8 | """ 9 | @spec time_ago_in_words(NaiveDateTime.t()) :: String.t() 10 | def time_ago_in_words(naive_date_time) when is_struct(naive_date_time, NaiveDateTime) do 11 | now = NaiveDateTime.utc_now() 12 | 13 | if NaiveDateTime.compare(naive_date_time, now) == :gt do 14 | raise ArgumentError, "expected a datetime in the past, got: #{inspect(naive_date_time)}" 15 | end 16 | 17 | distance_of_time_in_words(naive_date_time, now) 18 | end 19 | 20 | @doc """ 21 | Formats time distance between `from_ndt` and `to_ndt` 22 | as a human-readable string. 23 | 24 | ## Examples 25 | 26 | iex> IElixir.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:15:04]) 27 | "less than 5 seconds" 28 | 29 | iex> IElixir.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:15:09]) 30 | "less than 10 seconds" 31 | 32 | iex> IElixir.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:15:19]) 33 | "less than 20 seconds" 34 | 35 | iex> IElixir.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:15:20]) 36 | "half a minute" 37 | 38 | iex> IElixir.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:15:39]) 39 | "half a minute" 40 | 41 | iex> IElixir.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:15:40]) 42 | "less than a minute" 43 | 44 | iex> IElixir.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:15:59]) 45 | "less than a minute" 46 | 47 | iex> IElixir.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:16:00]) 48 | "1 minute" 49 | 50 | iex> IElixir.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:16:29]) 51 | "1 minute" 52 | 53 | iex> IElixir.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:16:30]) 54 | "2 minutes" 55 | 56 | iex> IElixir.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:58:30]) 57 | "44 minutes" 58 | 59 | iex> IElixir.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 18:59:30]) 60 | "about 1 hour" 61 | 62 | iex> IElixir.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-20 19:59:30]) 63 | "about 2 hours" 64 | 65 | iex> IElixir.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-21 18:14:00]) 66 | "about 24 hours" 67 | 68 | iex> IElixir.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-21 18:15:00]) 69 | "1 day" 70 | 71 | iex> IElixir.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-06-22 18:15:00]) 72 | "2 days" 73 | 74 | iex> IElixir.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2020-07-22 18:15:00]) 75 | "about 1 month" 76 | 77 | iex> IElixir.Utils.Time.distance_of_time_in_words(~N[2020-06-20 18:15:00], ~N[2021-08-22 18:15:00]) 78 | "about 14 months" 79 | """ 80 | @spec distance_of_time_in_words(NaiveDateTime.t(), NaiveDateTime.t()) :: String.t() 81 | def distance_of_time_in_words(from_ndt, to_ndt) 82 | when is_struct(from_ndt, NaiveDateTime) and is_struct(to_ndt, NaiveDateTime) do 83 | duration_seconds = NaiveDateTime.diff(to_ndt, from_ndt) 84 | 85 | {:seconds, duration_seconds} 86 | |> maybe_convert_to_minutes() 87 | |> duration_in_words() 88 | end 89 | 90 | defp maybe_convert_to_minutes({:seconds, seconds}) when seconds > 59 do 91 | {:minutes, round(seconds / 60)} 92 | end 93 | 94 | defp maybe_convert_to_minutes(duration), do: duration 95 | 96 | defp duration_in_words({:seconds, seconds}) when seconds in 0..4 do 97 | "less than 5 seconds" 98 | end 99 | 100 | defp duration_in_words({:seconds, seconds}) when seconds in 5..9 do 101 | "less than 10 seconds" 102 | end 103 | 104 | defp duration_in_words({:seconds, seconds}) when seconds in 10..19 do 105 | "less than 20 seconds" 106 | end 107 | 108 | defp duration_in_words({:seconds, seconds}) when seconds in 20..39 do 109 | "half a minute" 110 | end 111 | 112 | defp duration_in_words({:seconds, seconds}) when seconds in 40..59 do 113 | "less than a minute" 114 | end 115 | 116 | defp duration_in_words({:minutes, minutes}) when minutes == 1 do 117 | "1 minute" 118 | end 119 | 120 | defp duration_in_words({:minutes, minutes}) when minutes in 2..44 do 121 | "#{minutes} minutes" 122 | end 123 | 124 | defp duration_in_words({:minutes, minutes}) when minutes in 45..89 do 125 | "about 1 hour" 126 | end 127 | 128 | # 90 mins up to 24 hours 129 | defp duration_in_words({:minutes, minutes}) when minutes in 90..1439 do 130 | "about #{round(minutes / 60)} hours" 131 | end 132 | 133 | # 24 hours up to 42 hours 134 | defp duration_in_words({:minutes, minutes}) when minutes in 1440..2519 do 135 | "1 day" 136 | end 137 | 138 | # 42 hours up to 30 days 139 | defp duration_in_words({:minutes, minutes}) when minutes in 2520..43_199 do 140 | "#{round(minutes / 1440)} days" 141 | end 142 | 143 | # 30 days up to 45 days 144 | defp duration_in_words({:minutes, minutes}) when minutes in 43_200..64_799 do 145 | "about 1 month" 146 | end 147 | 148 | # 45 days up to 60 days 149 | defp duration_in_words({:minutes, minutes}) when minutes in 64_800..86_399 do 150 | "about 2 months" 151 | end 152 | 153 | defp duration_in_words({:minutes, minutes}) do 154 | "about #{round(minutes / 43_200)} months" 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule IElixir.Mixfile do 2 | use Mix.Project 3 | 4 | @version "0.1.0" 5 | 6 | def project do 7 | [ 8 | app: :ielixir, 9 | escript: escript(), 10 | version: @version, 11 | source_url: "https://github.com/ilhub/ielixir", 12 | name: "IElixir", 13 | elixir: ">= 1.12.0", 14 | build_embedded: Mix.env() == :prod, 15 | start_permanent: Mix.env() == :prod, 16 | consolidate_protocols: false, 17 | deps: deps(), 18 | description: """ 19 | Jupyter's kernel for Elixir programming language 20 | """, 21 | package: package(), 22 | test_coverage: [tool: ExCoveralls], 23 | preferred_cli_env: [ 24 | coveralls: :test, 25 | "coveralls.detail": :test, 26 | "coveralls.post": :test, 27 | "coveralls.html": :test 28 | ] 29 | ] 30 | end 31 | 32 | def application do 33 | [ 34 | extra_applications: [:logger] 35 | ] 36 | end 37 | 38 | def escript() do 39 | [ 40 | main_module: IElixir 41 | ] 42 | end 43 | 44 | defp deps do 45 | [ 46 | {:chumak, "~> 1.4"}, 47 | {:jason, "~> 1.2"}, 48 | {:uuid, "~> 2.0", hex: :uuid_erl}, 49 | {:ex_image_info, "~> 0.2.4"}, 50 | {:vega_lite, "~> 0.1.1"}, 51 | 52 | # Docs dependencies 53 | {:ex_doc, ">= 0.0.0", only: :docs, runtime: false}, 54 | {:inch_ex, "~> 2.0.0", only: :docs}, 55 | 56 | # Test dependencies 57 | {:excoveralls, "~> 0.14", only: :test}, 58 | {:dialyxir, "~> 1.1", runtime: false, only: [:dev, :test]}, 59 | {:credo, "~> 1.5", runtime: false, only: [:dev, :test]} 60 | ] 61 | end 62 | 63 | defp package do 64 | [ 65 | files: [ 66 | "config", 67 | "lib", 68 | "priv", 69 | "mix.exs", 70 | "README.md" 71 | ], 72 | maintainers: ["Dmitry Rubinstein", "Georgy Sychev"], 73 | licenses: ["MIT", "Apache-2.0"] 74 | ] 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 3 | "certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"}, 4 | "chumak": {:hex, :chumak, "1.4.0", "79eb44ba2da1e2a072c06bca1c79016c936423c6b8f826d6a7c2e22e046a3d40", [:rebar3], [], "hexpm", "a3a618a2cae0e3f8e844752e7f6f56c6231c5daef1a8de498a5973baa202cc5c"}, 5 | "credo": {:hex, :credo, "1.5.6", "e04cc0fdc236fefbb578e0c04bd01a471081616e741d386909e527ac146016c6", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "4b52a3e558bd64e30de62a648518a5ea2b6e3e5d2b164ef5296244753fc7eb17"}, 6 | "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, 7 | "earmark_parser": {:hex, :earmark_parser, "1.4.15", "b29e8e729f4aa4a00436580dcc2c9c5c51890613457c193cc8525c388ccb2f06", [:mix], [], "hexpm", "044523d6438ea19c1b8ec877ec221b008661d3c27e3b848f4c879f500421ca5c"}, 8 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 9 | "ex_doc": {:hex, :ex_doc, "0.25.2", "4f1cae793c4d132e06674b282f1d9ea3bf409bcca027ddb2fe177c4eed6a253f", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5b0c172e87ac27f14dfd152d52a145238ec71a95efbf29849550278c58a393d6"}, 10 | "ex_image_info": {:hex, :ex_image_info, "0.2.4", "610002acba43520a9b1cf1421d55812bde5b8a8aeaf1fe7b1f8823e84e762adb", [:mix], [], "hexpm", "fd1a7e02664e3b14dfd3b231d22fdd48bd3dd694c4773e6272b3a6228f1106bc"}, 11 | "excoveralls": {:hex, :excoveralls, "0.14.2", "f9f5fd0004d7bbeaa28ea9606251bb643c313c3d60710bad1f5809c845b748f0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "ca6fd358621cb4d29311b29d4732c4d47dac70e622850979bc54ed9a3e50f3e1"}, 12 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 13 | "hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"}, 14 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 15 | "inch_ex": {:hex, :inch_ex, "2.0.0", "24268a9284a1751f2ceda569cd978e1fa394c977c45c331bb52a405de544f4de", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "96d0ec5ecac8cf63142d02f16b7ab7152cf0f0f1a185a80161b758383c9399a8"}, 16 | "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, 17 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 18 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"}, 19 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 20 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 21 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 22 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 23 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 24 | "quickrand": {:hex, :quickrand, "2.0.2", "1d73faa52e0c149fcbc72a63c26135ff68be8fa7870675c73645896788a7540c", [:rebar3], [], "hexpm", "e21c6c7f29ca995468662085ca54d7d09e861c180a9dfec2cf4a2e75364a16d6"}, 25 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 26 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 27 | "uuid": {:hex, :uuid_erl, "2.0.2", "0d1ca9d94ca3b058467bce20b7706cc816a2bdbbe0574dd008936ae97ea4ebe7", [:rebar3], [{:quickrand, "~> 2.0.2", [hex: :quickrand, repo: "hexpm", optional: false]}], "hexpm", "4866ca7b3bd0265bc371590dcd0fe2832fc08588a071b72d07e09e23f163d0d6"}, 28 | "vega_lite": {:hex, :vega_lite, "0.1.1", "ad4facf87a31dae37fafef062a7033728fde5bfe2c2cf9b363aa75d5f12619ac", [:mix], [], "hexpm", "9492b754e0e8ac7f4be51a69c1cf0e706608f6d7d7dd293f34064f79a6f6b42e"}, 29 | } 30 | -------------------------------------------------------------------------------- /priv/kernel.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "require", 3 | "codemirror/lib/codemirror", 4 | "codemirror/addon/mode/loadmode", 5 | "base/js/namespace", 6 | "base/js/events", 7 | "base/js/utils", 8 | ], function (require, CodeMirror, CodemirrorLoadmode, IPython, events, utils) { 9 | var onload = function () { 10 | console.log("Kernel elixir kernel.js is loading."); 11 | 12 | // add here logic that should be run once per **page load** 13 | // like adding specific UI, or changing the default value 14 | // of codecell highlight. 15 | enableElixirMode(CodeMirror) 16 | 17 | // Set tooltips to be triggered after 800ms 18 | IPython.tooltip.time_before_tooltip = 800; 19 | 20 | // IPython keycodes. 21 | var space = 32; 22 | var downArrow = 40; 23 | IPython.keyboard.keycodes.down = downArrow; // space 24 | 25 | IPython.CodeCell.options_default["cm_config"]["mode"] = "ielixir"; 26 | 27 | utils.requireCodeMirrorMode("elixir", function () { 28 | // Create a multiplexing mode that uses Elixir highlighting by default but 29 | // doesn't highlight command-line directives. 30 | CodeMirror.defineMode("ielixir", function (config) { 31 | return CodeMirror.multiplexingMode( 32 | CodeMirror.getMode(config, "elixir"), 33 | { 34 | open: /:(?=!)/, // Matches : followed by !, but doesn't consume ! 35 | close: /^(?!!)/, // Matches start of line not followed by !, doesn't consume character 36 | mode: CodeMirror.getMode(config, "text/plain"), 37 | delimStyle: "delimit", 38 | } 39 | ); 40 | }); 41 | 42 | cells = IPython.notebook.get_cells(); 43 | for (var i in cells) { 44 | c = cells[i]; 45 | if (c.cell_type === "code") { 46 | // Force the mode to be Elixir 47 | // This is necessary, otherwise sometimes highlighting just doesn't happen. 48 | // This may be an IPython bug. 49 | c.code_mirror.setOption("mode", "ielixir"); 50 | c.force_highlight("ielixir"); 51 | } 52 | } 53 | }); 54 | if (IPython.notebook.set_codemirror_mode) { 55 | IPython.notebook.set_codemirror_mode("ielixir"); 56 | } 57 | 58 | // Prevent the pager from surrounding everything with a
 59 |     IPython.Pager.prototype.append_text = function (text) {
 60 |       this.pager_element
 61 |         .find(".container")
 62 |         .append($("
").html(IPython.utils.autoLinkUrls(text))); 63 | }; 64 | 65 | events.on("shell_reply.Kernel", function () { 66 | // Add logic here that should be run once per reply. 67 | 68 | // Highlight things with a .highlight-code class 69 | // The id is the mode with with to highlight 70 | $(".highlight-code").each(function () { 71 | var $this = $(this), 72 | $code = $this.html(), 73 | $unescaped = $("
").html($code).text(); 74 | 75 | $this.empty(); 76 | 77 | // Never highlight this block again. 78 | this.className = ""; 79 | 80 | CodeMirror(this, { 81 | value: $unescaped, 82 | mode: this.id, 83 | lineNumbers: false, 84 | readOnly: true, 85 | }); 86 | }); 87 | }); 88 | console.log("IElixir kernel.js should have been loaded."); 89 | }; // end def of onload 90 | return { onload: onload }; 91 | }); 92 | 93 | var enableElixirMode = function (CodeMirror) { 94 | // Below code comes from CodeMirror Mode Elixir project (c) by Marijn 95 | // Haverbeke, Ian Walter, et al. available at 96 | // https://github.com/ianwalter/codemirror-mode-elixir 97 | 98 | // CodeMirror Mode Elixir, copyright (c) by Marijn Haverbeke, Ian Walter, and 99 | // others. Distributed under an MIT license: http://codemirror.net/LICENSE. 100 | CodeMirror.defineMode("elixir", (config) => { 101 | const wordObj = (words) => { 102 | let o = {}; 103 | for (var i = 0, e = words.length; i < e; ++i) o[words[i]] = true; 104 | return o; 105 | }; 106 | 107 | const keywords = wordObj([ 108 | "alias", 109 | "case", 110 | "cond", 111 | "def", 112 | "defmodule", 113 | "defp", 114 | "defstruct", 115 | "defprotocol", 116 | "defimpl", 117 | "defmacro", 118 | "quote", 119 | "unquote", 120 | "receive", 121 | "fn", 122 | "do", 123 | "else", 124 | "else if", 125 | "end", 126 | "false", 127 | "if", 128 | "in", 129 | "next", 130 | "rescue", 131 | "for", 132 | "true", 133 | "unless", 134 | "when", 135 | "nil", 136 | "raise", 137 | "throw", 138 | "try", 139 | "catch", 140 | "after", 141 | "with", 142 | "require", 143 | "use", 144 | "__MODULE__", 145 | "__FILE__", 146 | "__DIR__", 147 | "__ENV__", 148 | "__CALLER__", 149 | ]); 150 | const indentWords = wordObj([ 151 | "def", 152 | "defmodule", 153 | "defp", 154 | "case", 155 | "cond", 156 | "rescue", 157 | "try", 158 | "catch", 159 | "->", 160 | ]); 161 | const dedentWords = wordObj(["end"]); 162 | const matching = { "[": "]", "{": "}", "(": ")" }; 163 | 164 | let curPunc; 165 | 166 | const chain = (newtok, stream, state) => { 167 | state.tokenize.push(newtok); 168 | return newtok(stream, state); 169 | }; 170 | 171 | const tokenBase = (stream, state) => { 172 | if (stream.sol() && stream.match('"""') && stream.eol()) { 173 | state.tokenize.push(readBlockComment); 174 | return "comment"; 175 | } 176 | 177 | if (stream.eatSpace()) { 178 | return null; 179 | } 180 | 181 | let ch = stream.next(); 182 | let m; 183 | 184 | if (ch === "'" || ch === '"') { 185 | return chain(readQuoted(ch, "string", ch === '"'), stream, state); 186 | } else if (ch === "/") { 187 | let currentIndex = stream.current().length; 188 | if (stream.skipTo("/")) { 189 | let searchTill = stream.current().length; 190 | let balance = 0; // balance brackets 191 | 192 | stream.backUp(stream.current().length - currentIndex); 193 | 194 | while (stream.current().length < searchTill) { 195 | const chchr = stream.next(); 196 | if (chchr === "(") { 197 | balance += 1; 198 | } else if (chchr === ")") { 199 | balance -= 1; 200 | } 201 | if (balance < 0) { 202 | break; 203 | } 204 | } 205 | 206 | stream.backUp(stream.current().length - currentIndex); 207 | 208 | if (balance === 0) { 209 | return chain(readQuoted(ch, "string-2", true), stream, state); 210 | } 211 | } 212 | 213 | return "operator"; 214 | } else if (ch === "%") { 215 | let style = "string"; 216 | let embed = true; 217 | 218 | if (stream.eat("s")) { 219 | style = "atom"; 220 | } else if (stream.eat(/[WQ]/)) { 221 | style = "string"; 222 | } else if (stream.eat(/[r]/)) { 223 | style = "string-2"; 224 | } else if (stream.eat(/[wxq]/)) { 225 | style = "string"; 226 | embed = false; 227 | } 228 | 229 | let delim = stream.eat(/[^\w\s=]/); 230 | 231 | if (!delim) { 232 | return "operator"; 233 | } 234 | 235 | if (matching.propertyIsEnumerable(delim)) { 236 | delim = matching[delim]; 237 | } 238 | 239 | return chain(readQuoted(delim, style, embed, true), stream, state); 240 | } else if (ch === "#") { 241 | stream.skipToEnd(); 242 | return "comment"; 243 | } else if ( 244 | ch === "<" && 245 | (m = stream.match(/^<-?[\`\"\']?([a-zA-Z_?]\w*)[\`\"\']?(?:;|$)/)) 246 | ) { 247 | return chain(readHereDoc(m[1]), stream, state); 248 | } else if (ch === "0") { 249 | if (stream.eat("x")) { 250 | stream.eatWhile(/[\da-fA-F]/); 251 | } else if (stream.eat("b")) { 252 | stream.eatWhile(/[01]/); 253 | } else { 254 | stream.eatWhile(/[0-7]/); 255 | } 256 | return "number"; 257 | } else if (/\d/.test(ch)) { 258 | stream.match(/^[\d_]*(?:\.[\d_]+)?(?:[eE][+\-]?[\d_]+)?/); 259 | return "number"; 260 | } else if (ch === "?") { 261 | while (stream.match(/^\\[CM]-/)) {} 262 | 263 | if (stream.eat("\\")) { 264 | stream.eatWhile(/\w/); 265 | } else { 266 | stream.next(); 267 | } 268 | return "string"; 269 | } else if (ch === ":") { 270 | if (stream.eat("'")) { 271 | return chain(readQuoted("'", "atom", false), stream, state); 272 | } 273 | if (stream.eat('"')) { 274 | return chain(readQuoted('"', "atom", true), stream, state); 275 | } 276 | 277 | // :> :>> :< :<< are valid symbols 278 | if (stream.eat(/[\<\>]/)) { 279 | stream.eat(/[\<\>]/); 280 | return "atom"; 281 | } 282 | 283 | // :+ :- :/ :* :| :& :! are valid symbols 284 | if (stream.eat(/[\+\-\*\/\&\|\:\!]/)) { 285 | return "atom"; 286 | } 287 | 288 | // Symbols can't start by a digit 289 | if (stream.eat(/[a-zA-Z$@_\xa1-\uffff]/)) { 290 | stream.eatWhile(/[\w$\xa1-\uffff]/); 291 | // Only one ? ! = is allowed and only as the last character 292 | stream.eat(/[\?\!\=]/); 293 | return "atom"; 294 | } 295 | 296 | return "operator"; 297 | } else if (ch === "@" && stream.match(/^@?[a-zA-Z_\xa1-\uffff]/)) { 298 | stream.eat("@"); 299 | stream.eatWhile(/[\w\xa1-\uffff]/); 300 | return "variable-2"; 301 | } else if (ch === "$") { 302 | if (stream.eat(/[a-zA-Z_]/)) { 303 | stream.eatWhile(/[\w]/); 304 | } else if (stream.eat(/\d/)) { 305 | stream.eat(/\d/); 306 | } else { 307 | stream.next(); // Must be a special global like $: or $! 308 | } 309 | return "variable-3"; 310 | } else if (/[a-zA-Z_\xa1-\uffff]/.test(ch)) { 311 | stream.eatWhile(/[\w\xa1-\uffff]/); 312 | stream.eat(/[\?\!]/); 313 | if (stream.eat(":")) { 314 | return "atom"; 315 | } 316 | return "ident"; 317 | } else if ( 318 | ch === "|" && 319 | (state.varList || state.lastTok === "{" || state.lastTok === "do") 320 | ) { 321 | curPunc = "|"; 322 | return null; 323 | } else if (/[\(\)\[\]{}\\;]/.test(ch)) { 324 | curPunc = ch; 325 | return null; 326 | } else if (ch === "-" && stream.eat(">")) { 327 | return "arrow"; 328 | } else if (ch === "|" && stream.eat(">")) { 329 | return "pipe"; 330 | } else if (/[=+\-\/*:\.^%<>~|]/.test(ch)) { 331 | if (ch === "." && !stream.eatWhile(/[=+\-\/*:\.^%<>~|]/)) { 332 | curPunc = "."; 333 | } 334 | return "operator"; 335 | } else { 336 | return null; 337 | } 338 | }; 339 | 340 | const tokenBaseUntilBrace = (depth) => { 341 | if (!depth) { 342 | depth = 1; 343 | } 344 | 345 | return (stream, state) => { 346 | if (stream.peek() === "}") { 347 | if (depth === 1) { 348 | state.tokenize.pop(); 349 | return state.tokenize[state.tokenize.length - 1](stream, state); 350 | } else { 351 | state.tokenize[state.tokenize.length - 1] = tokenBaseUntilBrace( 352 | depth - 1 353 | ); 354 | } 355 | } else if (stream.peek() === "{") { 356 | state.tokenize[state.tokenize.length - 1] = tokenBaseUntilBrace( 357 | depth + 1 358 | ); 359 | } 360 | return tokenBase(stream, state); 361 | }; 362 | }; 363 | 364 | const tokenBaseOnce = () => { 365 | let alreadyCalled = false; 366 | return (stream, state) => { 367 | if (alreadyCalled) { 368 | state.tokenize.pop(); 369 | return state.tokenize[state.tokenize.length - 1](stream, state); 370 | } 371 | alreadyCalled = true; 372 | return tokenBase(stream, state); 373 | }; 374 | }; 375 | 376 | const readQuoted = (quote, style, embed, unescaped) => { 377 | return (stream, state) => { 378 | let escaped = false; 379 | let ch; 380 | 381 | if (state.context.type === "read-quoted-paused") { 382 | state.context = state.context.prev; 383 | stream.eat("}"); 384 | } 385 | 386 | while ((ch = stream.next()) != null) { 387 | // eslint-disable-line 388 | if (ch === quote && (unescaped || !escaped)) { 389 | state.tokenize.pop(); 390 | break; 391 | } 392 | 393 | if (embed && ch === "#" && !escaped) { 394 | if (stream.eat("{")) { 395 | if (quote === "}") { 396 | state.context = { 397 | prev: state.context, 398 | type: "read-quoted-paused", 399 | }; 400 | } 401 | state.tokenize.push(tokenBaseUntilBrace()); 402 | break; 403 | } else if (/[@\$]/.test(stream.peek())) { 404 | state.tokenize.push(tokenBaseOnce()); 405 | break; 406 | } 407 | } 408 | 409 | escaped = !escaped && ch === "\\"; 410 | } 411 | 412 | return style; 413 | }; 414 | }; 415 | 416 | const readHereDoc = (phrase) => { 417 | return (stream, state) => { 418 | if (stream.match(phrase)) { 419 | state.tokenize.pop(); 420 | } else { 421 | stream.skipToEnd(); 422 | } 423 | return "string"; 424 | }; 425 | }; 426 | 427 | const readBlockComment = (stream, state) => { 428 | if (stream.sol() && stream.match('"""') && stream.eol()) { 429 | state.tokenize.pop(); 430 | } 431 | stream.skipToEnd(); 432 | return "comment"; 433 | }; 434 | 435 | return { 436 | startState: () => { 437 | return { 438 | tokenize: [tokenBase], 439 | indented: 0, 440 | context: { type: "top", indented: -config.indentUnit }, 441 | continuedLine: false, 442 | lastTok: null, 443 | varList: false, 444 | }; 445 | }, 446 | token: (stream, state) => { 447 | curPunc = null; 448 | 449 | // if (stream.sol()) { 450 | // state.indented = stream.indentation() 451 | // } 452 | 453 | let style = state.tokenize[state.tokenize.length - 1](stream, state); 454 | let kwtype; 455 | let thisTok = curPunc; 456 | 457 | if (style === "ident") { 458 | let word = stream.current(); 459 | 460 | style = 461 | state.lastTok === "." 462 | ? "property" 463 | : keywords.propertyIsEnumerable(stream.current()) 464 | ? "keyword" 465 | : /^[A-Z]/.test(word) 466 | ? "tag" 467 | : state.lastTok === "def" || 468 | state.lastTok === "class" || 469 | state.varList 470 | ? "def" 471 | : "variable"; 472 | 473 | const isColumnIndent = stream.column() === stream.indentation(); 474 | if (style === "keyword") { 475 | thisTok = word; 476 | if (indentWords.propertyIsEnumerable(word)) { 477 | kwtype = "indent"; 478 | } else if (dedentWords.propertyIsEnumerable(word)) { 479 | kwtype = "dedent"; 480 | } else if ((word === "if" || word === "unless") && isColumnIndent) { 481 | kwtype = "indent"; 482 | } else if ( 483 | word === "do" && 484 | state.context.indented < state.indented 485 | ) { 486 | kwtype = "indent"; 487 | } 488 | } 489 | } 490 | 491 | if (curPunc || (style && style !== "comment")) { 492 | state.lastTok = thisTok; 493 | } 494 | 495 | if (curPunc === "|") { 496 | state.varList = !state.varList; 497 | } 498 | 499 | if (kwtype === "indent" || /[\(\[\{]/.test(curPunc)) { 500 | state.context = { 501 | prev: state.context, 502 | type: curPunc || style, 503 | indented: state.indented, 504 | }; 505 | } else if ( 506 | (kwtype === "dedent" || /[\)\]\}]/.test(curPunc)) && 507 | state.context.prev 508 | ) { 509 | state.context = state.context.prev; 510 | } 511 | 512 | if (stream.eol()) { 513 | state.continuedLine = curPunc === "\\" || style === "operator"; 514 | } 515 | 516 | return style; 517 | }, 518 | // indent: (state, textAfter) => { 519 | // if (state.tokenize[state.tokenize.length - 1] !== tokenBase) { 520 | // return 0 521 | // } 522 | // let firstChar = textAfter && textAfter.charAt(0) 523 | // let ct = state.context 524 | // let closing = ct.type === matching[firstChar] || 525 | // ct.type === 'keyword' && /^(?:end|until|else|else if|when|rescue)\b/.test(textAfter) 526 | // return ct.indented + (closing ? 0 : config.indentUnit) + 527 | // (state.continuedLine ? config.indentUnit : 0) 528 | // }, 529 | electricInput: /^\s*(?:end|rescue|else if|else|catch\})$/, 530 | lineComment: "#", 531 | }; 532 | }); 533 | 534 | CodeMirror.defineMIME("text/x-elixir", "elixir"); 535 | }; 536 | -------------------------------------------------------------------------------- /priv/kernel.json: -------------------------------------------------------------------------------- 1 | { 2 | "argv": [ 3 | "ielixir", 4 | "serve", 5 | "{connection_file}" 6 | ], 7 | "display_name": "Elixir", 8 | "language": "Elixir" 9 | } 10 | -------------------------------------------------------------------------------- /priv/logo-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spawnfest/ielixir/6ce728b7f98e79dbdbbc96489dcaf02a4faf24de/priv/logo-32x32.png -------------------------------------------------------------------------------- /priv/logo-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spawnfest/ielixir/6ce728b7f98e79dbdbbc96489dcaf02a4faf24de/priv/logo-64x64.png -------------------------------------------------------------------------------- /resources/examples/Welcome to IElixir.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Welcome to IElixir" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "## Introduction" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "We are happy you decided to give Jupyter with **IElixir kernel** a try, hopefully it empowers you to build great stuff! 🚀" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "metadata": {}, 27 | "source": [ 28 | "Jupyter is a tool for crafting interactive and collaborative code notebooks. It is primarily meant as a tool for rapid prototyping - think of it as an IEx session combined with your editor. You can also use it for authoring shareable articles that people can easily run and play around with. Package authors can write notebooks as interactive tutorials and include them in their repository, so that users can easily download and run them locally." 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "metadata": {}, 34 | "source": [ 35 | "## Basic usage" 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": {}, 41 | "source": [ 42 | "Each notebook consists of a number of cells, which serve as primary building blocks.\n", 43 | "There are **Markdown** cells (such as this one) that allow you to describe your work\n", 44 | "and **Elixir** cells to run your code!" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": null, 50 | "metadata": {}, 51 | "outputs": [], 52 | "source": [ 53 | "# This is an Elixir cell - as the name suggests that's where the code goes.\n", 54 | "# To evaluate this cell, you can either press `Shift + Enter` - to create a new cell right after this one\n", 55 | "# or use `Ctrl + Enter` - to just perform evaluation!\n", 56 | "\n", 57 | "message = \"hey, grab yourself a cup of 🍵\"" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "metadata": {}, 63 | "source": [ 64 | "Subsequent cells have access to the bindings you've defined:" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": null, 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "String.replace(message, \"🍵\", \"☕\")" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "Note however that bindings are global, so each cell execution sees all the stuff that has been created from the beginning of a Kernel session." 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "You can have more than one command inside a cell. But the output will be only the last:" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": null, 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [ 96 | "data = \"📔\"\n", 97 | "bad_data = \"🦠\"\n", 98 | "\n", 99 | "# Unfortunately, only bad data will be returned" 100 | ] 101 | }, 102 | { 103 | "cell_type": "markdown", 104 | "metadata": {}, 105 | "source": [ 106 | "At the same time, you can _print_ all the you want, and it will be printed as a separate output:" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": null, 112 | "metadata": {}, 113 | "outputs": [], 114 | "source": [ 115 | "# IO module outputs data to stdout, so we can use it for wide output\n", 116 | "\n", 117 | "IO.inspect(data)\n", 118 | "IO.puts(bad_data)\n", 119 | "\n", 120 | "# The result of puts - :ok, that's what we are getting as an execution result of this cell" 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "metadata": {}, 126 | "source": [ 127 | "## Kernel" 128 | ] 129 | }, 130 | { 131 | "cell_type": "markdown", 132 | "metadata": {}, 133 | "source": [ 134 | "Each time you run a notebook - new Kernel is started. Think about it as an **IEx** session.\n", 135 | "\n", 136 | "Same as there, near each executed cell the autoincremented nuber is appeared, to help you follow the order of cell execution:" 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": null, 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [ 145 | "\"Look <--- at the [ ] brackets on the left of the cell. Same are for the result\"\n", 146 | "# Yes, you can run empty string as inside IEx" 147 | ] 148 | }, 149 | { 150 | "cell_type": "markdown", 151 | "metadata": {}, 152 | "source": [ 153 | "Actually, that means that you can run a cell several times with different results, or even run previous cell, or skip a cell to run one after it and then to return:" 154 | ] 155 | }, 156 | { 157 | "cell_type": "code", 158 | "execution_count": null, 159 | "metadata": {}, 160 | "outputs": [], 161 | "source": [ 162 | "# Let's skip this cell, and then run it several times:\n", 163 | "important_list = tl(important_list)\n", 164 | "\n", 165 | "# Run it until you get an ArgumentError" 166 | ] 167 | }, 168 | { 169 | "cell_type": "code", 170 | "execution_count": null, 171 | "metadata": {}, 172 | "outputs": [], 173 | "source": [ 174 | "# This is the cell to be run first. \n", 175 | "\n", 176 | "important_list = [\"⚽️\", \"🏀\", \"🏈\"]" 177 | ] 178 | }, 179 | { 180 | "cell_type": "markdown", 181 | "metadata": {}, 182 | "source": [ 183 | "You can restart a kernel as many times as you want - if something goes wrong, you can start from the beginning.\n", 184 | "\n", 185 | "This can be done using `Kernel` menu tab in **Jupyter**'s menu bar. Let's try to do this now!" 186 | ] 187 | }, 188 | { 189 | "cell_type": "markdown", 190 | "metadata": {}, 191 | "source": [ 192 | "Please, don't forget that all your **aliases**, **defined modules** and **variables** will not be accessible for new Kernel untill you will execute a code to create them" 193 | ] 194 | }, 195 | { 196 | "cell_type": "code", 197 | "execution_count": null, 198 | "metadata": {}, 199 | "outputs": [], 200 | "source": [ 201 | "# Here let's try to access our variables again - and we'll see an error message that they are undefined:\n", 202 | "\n", 203 | "data" 204 | ] 205 | }, 206 | { 207 | "cell_type": "markdown", 208 | "metadata": {}, 209 | "source": [ 210 | "Every time a new Kernel starts - it starts to count execution cells from 1." 211 | ] 212 | }, 213 | { 214 | "cell_type": "markdown", 215 | "metadata": {}, 216 | "source": [ 217 | "## Notebook files" 218 | ] 219 | }, 220 | { 221 | "cell_type": "markdown", 222 | "metadata": {}, 223 | "source": [ 224 | "By default, notebooks are stored on disk from the beginning of there lifetime. They are stored with **.ipynb** file format. \n", 225 | "It's a Json, that can store data not about only the cells content, but also about Kernel metadata, output and so on.\n", 226 | "\n", 227 | "This format is not so good for version controll, but fortunately **Jupyter** is so popular nowadays, that every **VCS** and **IDE** has it's own plugins to work with it.\n", 228 | "\n", 229 | "For example **VSCode**, **Gitlab** and **Github** can display these files in notebook and not a JSON format, that is very handy." 230 | ] 231 | }, 232 | { 233 | "cell_type": "markdown", 234 | "metadata": {}, 235 | "source": [ 236 | "## Markdown" 237 | ] 238 | }, 239 | { 240 | "cell_type": "markdown", 241 | "metadata": {}, 242 | "source": [ 243 | "### Math" 244 | ] 245 | }, 246 | { 247 | "cell_type": "markdown", 248 | "metadata": {}, 249 | "source": [ 250 | "Jupyter uses $\\TeX$ syntax for math.\n", 251 | "It supports both inline math like $e^{\\pi i} + 1 = 0$, as well as display math:\n", 252 | "\n", 253 | "$$\n", 254 | "S(x) = \\frac{1}{1 + e^{-x}} = \\frac{e^{x}}{e^{x} + 1}\n", 255 | "$$\n", 256 | "\n", 257 | "You can explore all supported expressions [here](https://katex.org/docs/supported.html)." 258 | ] 259 | }, 260 | { 261 | "cell_type": "markdown", 262 | "metadata": {}, 263 | "source": [ 264 | "### Images" 265 | ] 266 | }, 267 | { 268 | "cell_type": "markdown", 269 | "metadata": {}, 270 | "source": [ 271 | "You can embed images inside your markdown like this (check it source code to see how it's working 🙂:\n", 272 | "\n", 273 | "![Elixir logo](./logo.png \"Elixir logo\")" 274 | ] 275 | }, 276 | { 277 | "cell_type": "markdown", 278 | "metadata": {}, 279 | "source": [ 280 | "## Next steps" 281 | ] 282 | }, 283 | { 284 | "cell_type": "markdown", 285 | "metadata": {}, 286 | "source": [ 287 | "That's our quick intro to IElixir! Where to go next?\n", 288 | "\n", 289 | "* If you are not familiar with Elixir, there is a fast paced\n", 290 | " introduction to the language in the [Distributed portals\n", 291 | " with Elixir](./Distributed%20portals%20with%20Elixir.ipynb)\n", 292 | "\n", 293 | "* Learn how Elixir integrates with Notebooks in the\n", 294 | " [Elixir and Jupyter](./Elixir%20and%20Jupyter.ipynb) notebook;\n", 295 | "\n", 296 | "Now go ahead and build something cool! 🚢" 297 | ] 298 | } 299 | ], 300 | "metadata": { 301 | "kernelspec": { 302 | "display_name": "Elixir", 303 | "language": "Elixir", 304 | "name": "ielixir" 305 | }, 306 | "language_info": { 307 | "codemirror_mode": "ielixir", 308 | "file_extension": "ex", 309 | "mimetype": "text/x-ielixir", 310 | "name": "elixir", 311 | "nbconvert_exporter": "", 312 | "pygments_lexer": "elixir", 313 | "version": "1.12.3" 314 | } 315 | }, 316 | "nbformat": 4, 317 | "nbformat_minor": 4 318 | } 319 | -------------------------------------------------------------------------------- /resources/examples/images/portal-drop.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spawnfest/ielixir/6ce728b7f98e79dbdbbc96489dcaf02a4faf24de/resources/examples/images/portal-drop.jpeg -------------------------------------------------------------------------------- /resources/examples/images/portal-list.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spawnfest/ielixir/6ce728b7f98e79dbdbbc96489dcaf02a4faf24de/resources/examples/images/portal-list.jpeg -------------------------------------------------------------------------------- /resources/examples/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spawnfest/ielixir/6ce728b7f98e79dbdbbc96489dcaf02a4faf24de/resources/examples/logo.png -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }: 2 | with pkgs; 3 | mkShell rec { 4 | nativeBuildInputs = with pkgs; [ 5 | erlangR24 elixir jupyter 6 | python39 python39Packages.jupyterlab 7 | ]; 8 | 9 | shellHook = '' 10 | PATH=$HOME/.mix/escripts:$PATH NIX_ENFORCE_PURITY=0 zsh 11 | ''; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(exclude: [:skip]) 2 | --------------------------------------------------------------------------------