├── .python-version ├── messages.json ├── messages ├── 2.8.0.txt ├── 3.4.0.txt ├── 2.9.0.txt ├── 3.7.0.txt ├── 3.0.0.txt ├── 4.0.0.txt ├── 2.4.0.txt ├── 3.6.0.txt └── install.txt ├── cljfmt.edn ├── ClojureSymbols.tmPreferences ├── Comment.tmPreferences ├── cs_warn.py ├── LICENSE.txt ├── Main.sublime-menu ├── test_scheme ├── demo.clj └── color_scheme.clj ├── Clojure Sublimed.sublime-settings ├── Default (OSX).sublime-keymap ├── Default (Linux).sublime-keymap ├── Default (Windows).sublime-keymap ├── cs_progress.py ├── cs_eval_status.py ├── cs_colors.py ├── docs └── protocol_socket.md ├── Default.sublime-commands ├── cs_cljfmt.py ├── cs_comment.py ├── cs_conn_shadow_cljs.py ├── src_clojure └── clojure_sublimed │ ├── middleware.clj │ ├── socket_repl.clj │ └── core.clj ├── cs_printer.py ├── cs_bencode.py ├── cs_conn_nrepl_jvm.py ├── test_comment ├── comment_reversible.txt └── comment.txt ├── cs_conn_nrepl_raw.py ├── cs_watch.py ├── Clojure Sublimed Dark.sublime-color-scheme ├── cs_conn_socket_repl.py ├── test_indent └── indent.txt ├── Clojure Sublimed Light.sublime-color-scheme ├── cs_conn.py ├── cs_indent.py ├── cs_common.py └── CHANGELOG.md /.python-version: -------------------------------------------------------------------------------- 1 | 3.8 -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "messages/install.txt", 3 | "2.4.0": "messages/2.4.0.txt" 4 | } -------------------------------------------------------------------------------- /messages/2.8.0.txt: -------------------------------------------------------------------------------- 1 | Clojure Sublimed now includes support for Shadow CLJS thanks to @sainadh-d. To use it, select `Clojure Sublimed: Connect shadow-cljs` command -------------------------------------------------------------------------------- /cljfmt.edn: -------------------------------------------------------------------------------- 1 | {:indents {#re ".*" [[:inner 0]]} 2 | :remove-surrounding-whitespace? false 3 | :remove-trailing-whitespace? false 4 | :remove-consecutive-blank-lines? false} -------------------------------------------------------------------------------- /messages/3.4.0.txt: -------------------------------------------------------------------------------- 1 | Clojure Sublimed now supports: 2 | 3 | - a separate connection per window (was one global connection) 4 | - `.repl-port` files for Socket REPL, similar to `.nrepl-port` for nREPL -------------------------------------------------------------------------------- /messages/2.9.0.txt: -------------------------------------------------------------------------------- 1 | Clojure Sublimed now includes support for UNIX domain sockets for nREPL, thanks to @tribals. To use it, select `Clojure Sublimed: Connect` command and specify absolute filesystem path to the socket file instead of host:port pair -------------------------------------------------------------------------------- /ClojureSymbols.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | name 5 | Clojure Sublimed Symbols 6 | scope 7 | entity.name.clojure 8 | settings 9 | 10 | showInIndexedSymbolList 11 | 1 12 | showInSymbolList 13 | 1 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Comment.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | name 5 | Comment 6 | scope 7 | source.clojure 8 | settings 9 | 10 | shellVariables 11 | 12 | 13 | name 14 | TM_COMMENT_START 15 | value 16 | ; 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /cs_warn.py: -------------------------------------------------------------------------------- 1 | import sublime, sublime_plugin 2 | from . import cs_common 3 | 4 | status_key = 'clojure-sublimed-warn-status' 5 | 6 | def add_warning(window): 7 | state = cs_common.get_state(window) 8 | state.warnings += 1 9 | suffix = 's' if state.warnings > 0 else '' 10 | cs_common.set_status(window, status_key, f'⚠️ {state.warnings} warning{suffix}') 11 | 12 | def reset_warnings(window): 13 | state = cs_common.get_state(window) 14 | state.warnings = 0 15 | cs_common.set_status(window, status_key, None) -------------------------------------------------------------------------------- /messages/3.7.0.txt: -------------------------------------------------------------------------------- 1 | New feature in Clojure Sublimed: Watches! 2 | 3 | Watches are great alternative to debug prints: they allow you to monitor intermediate values during function execution right in the editor. 4 | 5 | This is how they work: 6 | 7 | - Select a right-hand expression 8 | - Run `Clojure Sublimed: Add Watch` command 9 | - Now every time function is executed, for any reason, watched expressions will display values they evaluate to, in real time. 10 | 11 | For now watches are only supported in Socket REPL. 12 | 13 | Enjoy! 14 | -------------------------------------------------------------------------------- /messages/3.0.0.txt: -------------------------------------------------------------------------------- 1 | Clojure Sublimed just updated to 3.0.0! It’s a huge rewrite with many exciting features: 2 | 3 | - REPL doesn’t depend on syntax highlighting anymore. 4 | - On top of JVM nREPL and ShadowCLJS nREPL, we now support Raw nREPL (no extra middlewares, so less quality, but should work anywhere) and JVM Socket REPL (works on core Clojure with 0 dependencies). 5 | - It is now much easier to add new REPLs. Contributions welcome :) 6 | - Pretty-printer now works client-side, same on every REPL. 7 | - Indenter and formatter work much faster now and do not require setting `Clojure (Sublimed)` syntax. 8 | 9 | Let me know if anything breaks. If you were using JVM nREPL, I recommend switching to Socket REPL as it has better support in Clojure Sublimed, faster startup and brighter future. 10 | 11 | Happy Clojure-ing! 12 | 13 | Best, 14 | Nikita. -------------------------------------------------------------------------------- /messages/4.0.0.txt: -------------------------------------------------------------------------------- 1 | Clojure Sublimed 4.0.0 2 | ---------------------- 3 | 4 | Two major new features: 5 | 6 | # Code formatting 7 | 8 | By default, Clojure Sublimed uses [Better Clojure Formatting style](https://tonsky.me/blog/clojurefmt/). 9 | 10 | Starting with 4.0.0, you can switch to `cljftm` instead: 11 | 12 | - Download `cljfmt` binary from `https://github.com/weavejester/cljfmt/releases/latest` 13 | - Add `cljfmt` to `$PATH` 14 | - In Sublime Text, open `Preferences: Settings` 15 | - Add `"clojure_sublimed_formatter": "cljfmt"` 16 | 17 | # Color scheme 18 | 19 | 4.0.0 ships major improvement in syntax definitons for Clojure. 20 | 21 | To make best use of them, we now offer color scheme that makes use of many of these features. 22 | 23 | - Cmd/Ctrl + Shift + P (Command Palette) 24 | - `UI: Select Color Scheme` 25 | - Select `Auto` -> `Clojure Sublimed Light` -> `Clojure Sublimed Dark` 26 | 27 | These color schemes will work for other languages, too. -------------------------------------------------------------------------------- /messages/2.4.0.txt: -------------------------------------------------------------------------------- 1 | Clojure Sublimed now includes optional support for [Simple Clojure Formatting rules](https://tonsky.me/blog/clojurefmt/). It doesn’t require nREPL connection but does require `Clojure (Sublimed)` syntax to be selected for buffer. 2 | 3 | To reformat whole file, run `Clojure Sublimed: Reindent Buffer`. 4 | 5 | To reindent only current line(s), run `Clojure Sublimed: Reindent Lines`. 6 | 7 | To enable correct indentations as you type code, rebind `Enter` to `Clojure Sublimed: Insert Newline`: 8 | 9 | ``` 10 | {"keys": ["enter"], 11 | "command": "clojure_sublimed_insert_newline", 12 | "context": [{"key": "selector", "operator": "equal", "operand": "source.edn | source.clojure"}, 13 | {"key": "auto_complete_visible", "operator": "equal", "operand": false}, 14 | {"key": "panel_has_focus", "operator": "equal", "operand": false}]} 15 | ``` 16 | 17 | Best way to do it is through running `Preferences: Clojure Sublimed Key Bindings`. 18 | 19 | For full changelog, see https://github.com/tonsky/Clojure-Sublimed/blob/master/CHANGELOG.md -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-2021 Nikita Prokopov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { "id": "preferences", 3 | "children": [ 4 | { "caption": "Package Settings", 5 | "mnemonic": "P", 6 | "id": "package-settings", 7 | "children": [ 8 | { "caption": "Clojure Sublimed", 9 | "children": [ 10 | { "caption": "README", 11 | "command": "open_file", 12 | "args": { 13 | "file": "${packages}/Clojure Sublimed/README.md" 14 | } 15 | }, 16 | { "caption": "-" }, 17 | { "caption": "Settings", 18 | "command": "edit_settings", 19 | "args": { 20 | "base_file": "${packages}/Clojure Sublimed/Clojure Sublimed.sublime-settings", 21 | "default": "{\n\t$0\n}\n" 22 | } 23 | }, 24 | { "caption": "Key Bindings", 25 | "command": "edit_settings", 26 | "args": { 27 | "base_file": "${packages}/Clojure Sublimed/Default (${platform}).sublime-keymap", 28 | "default": "[\n\t$0\n]\n" 29 | } 30 | }, 31 | ] 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /test_scheme/demo.clj: -------------------------------------------------------------------------------- 1 | ;; Clojure Sublimed 2 | ;; ---------------- 3 | 4 | ;; Nested parentheses 5 | ((())) ([{}]) [#{} #?(:clj) #()] 6 | 7 | ;; Reader comments 8 | (let [x 1 #_(throw (ex-info "" {})) 9 | y 2 10 | ;; Including stacking 11 | #_#_ z (/ 1 0)]) 12 | 13 | ;; Block comments 14 | (comment 15 | (/ 1 0)) 16 | 17 | ;; Unused symbols 18 | (defn fn [a _unused & _]) 19 | 20 | ;; Namespaces 21 | clojure.string/index-of 22 | 23 | ;; Metadata 24 | ^{:doc "abc"} x 25 | 26 | ;; Quoting 27 | ... '[a b c] ... 28 | 29 | ;; And unquoting 30 | `(let [x# ~(gensym x)] 31 | ...) 32 | 33 | ;; Trickiy edge cases 34 | (defn #_c ^int fn "doc" {:attr :map} []) 35 | '^int sym 36 | ^'tag sym 37 | #_#_()()() 38 | 39 | ;; Error detection 40 | "\u221 \x" #".* \E) \y" 41 | #inst "1985-" \aa 1/0 09 #123 nnil truee 42 | :kv: :kv/ :/kv :/ :kv/ab: :kv/ab/ 43 | 44 | 45 | 46 | 47 | (defn reverse 48 | "Returns a seq of \"items\" in reverse order." 49 | {:added "1.0" 50 | :static true} 51 | [coll] 52 | (reduce1 conj () coll)) 53 | 54 | ;; math stuff 55 | (defn ^:private nary-inline 56 | ([op] 57 | (nary-inline op op)) 58 | ([op unchecked-op] 59 | (fn [x y & more] 60 | (let [op (if *unchecked-math* 61 | unchecked-op op)] 62 | (reduce1 63 | (fn [a b] 64 | `(. clojure.lang.Numbers (~op ~a ~b))) 65 | `(. clojure.lang.Numbers (~op ~x ~y)) 66 | more)))))) 67 | 68 | (defn ^:private >1? [n] 69 | (clojure.lang.Numbers/gt n 1)) 70 | 71 | (defn ^:private >0? [n] 72 | (clojure.lang.Numbers/gt n 0)) 73 | 74 | 75 | -------------------------------------------------------------------------------- /messages/3.6.0.txt: -------------------------------------------------------------------------------- 1 | Clojure Sublimed now allows you to create custom commands that transform code before sending it to eval. 2 | 3 | For example, this will pretty-print result of your evaluation to stdout: 4 | 5 | ``` 6 | {"keys": ["ctrl+p"], 7 | "command": "clojure_sublimed_eval", 8 | "args": {"transform": "(doto %code clojure.pprint/pprint)"}} 9 | ``` 10 | 11 | `transform` is a format string that takes selected form, formats it according to described rules and then sends resulting code to evaluation. 12 | 13 | If you now press `ctrl+p` on a form like `(+ 1 2)`, the actual eval sent to REPL will be: 14 | 15 | ``` 16 | (doto (+ 1 2) clojure.pprint/pprint) 17 | ``` 18 | 19 | Which will pretty-print evaluation result to stdout. This pattern might be useful for large results that don’t fit inline. 20 | 21 | Another use-case might be “eval to buffer”: 22 | 23 | ``` 24 | {"keys": ["ctrl+b"], 25 | "command": "chain", 26 | "args": { 27 | "commands": [ 28 | ["clojure_sublimed_eval", {"transform": "(with-open [w (clojure.java.io/writer \"/tmp/sublimed_output.edn\")] (doto %code (clojure.pprint/pprint w)))"}], 29 | ["open_file", {"file": "/tmp/sublimed_output.edn"}] 30 | ] 31 | } 32 | } 33 | ``` 34 | 35 | Inside `transform` you can also use `%ns` (current ns) and `%symbol` (if selected form is `def`-something, it will be replaced with defined name, otherwise `nil`). 36 | 37 | This allows us to implement “run test under cursor”: 38 | 39 | ``` 40 | {"keys": ["super+shift+t"], 41 | "command": "clojure_sublimed_eval", 42 | "args": {"transform": "(clojure.test/run-test-var #'%symbol)"}} 43 | ``` 44 | 45 | Enjoy! 46 | -------------------------------------------------------------------------------- /Clojure Sublimed.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // Enable debug logging in Sublime Text console 3 | "debug": false, 4 | 5 | // If evaluation takes longer than this, print elapsed time 6 | // Set to null to disable 7 | "elapsed_threshold_ms": 100, 8 | 9 | // Animation to display while waiting for evaluation to finish. 10 | // 11 | // Some ideas: 12 | // 13 | // ["\\", "|", "/", "-"] 14 | // 15 | // ["[=----]", "[-=---]", "[--=--]", "[---=-]", 16 | // "[----=]", "[---=-]", "[--=--]", "[-=---]"] 17 | // 18 | // ["▓░░░░", "░▓░░░", "░░▓░░", "░░░▓░", 19 | // "░░░░▓", "░░░▓░", "░░▓░░", "░▓░░░"] 20 | // 21 | // ["⠏", "⠛", "⠹", "⢸", "⣰", "⣤", "⣆", "⡇"] 22 | // 23 | // ["▁▂▃▄▅", "▂▁▂▃▄", "▃▂▁▂▃", "▄▃▂▁▂", "▅▄▃▂▁", 24 | // "▆▅▄▃▂", "▇▆▅▄▃", "█▇▆▅▄", "▇█▇▆▅", "▆▇█▇▆", 25 | // "▅▆▇█▇", "▄▅▆▇█", "▃▄▅▆▇", "▂▃▄▅▆"] 26 | // 27 | // Set to 1-element array to disable animation: 28 | // 29 | // ["..."] 30 | // 31 | "progress_phases": ["🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚", "🕛"], 32 | 33 | // how often to update animation 34 | "progress_interval_ms": 100, 35 | 36 | // values larger than this will be truncated. Set to 0 to disable truncation 37 | "print_quota": 4096, 38 | 39 | // When true, all evals will happen in a single session. This makes 40 | // dynamic vars like `*e` or `*warn-on-reflection*` persistent, but also 41 | // makes all evaluations strictly sequential (new eval will not start until 42 | // all the previous ones have finished). 43 | // 44 | // False by default (enables parallel evals). 45 | "eval_in_session": false, 46 | 47 | // A form to be evaluated in shared session and inherited by all evals 48 | // E.g. (set! *warn-on-reflection* true) 49 | "eval_shared": "", 50 | 51 | // formatter, "sublimed" or "cljfmt". Latter requires `cljfmt` to be on $PATH 52 | "formatter": "sublimed", 53 | 54 | // reformat file on save, false by default 55 | "format_on_save": false 56 | } -------------------------------------------------------------------------------- /messages/install.txt: -------------------------------------------------------------------------------- 1 | Hi there! Thank you for installing Clojure Sublimed. 2 | 3 | How to get started: 4 | 5 | 6 | Associate syntax with *.clj* files (only needs to be done once) 7 | --------------------------------------------------------------- 8 | 9 | For each file type (.clj, .cljs, .cljc, .edn) do: 10 | 11 | - Open any file with that extension 12 | - Go to menu -> `View` → `Syntax` → `Open all with current extension as...` 13 | - Select `Clojure (Sublimed)` 14 | 15 | 16 | Key bindings 17 | ------------ 18 | 19 | Sublime has no good way to ship optional key bindings with plugin. So 20 | 21 | - Cmd/Ctrl + Shift + P (Command Palette) 22 | - `Preferences: Clojure Sublimed Key Bindings` 23 | - Copy examples from the left to your config on the right 24 | 25 | I recomment at least: 26 | 27 | - Evaluate 28 | - Evaluate Buffer 29 | - Interrupt Pending Evaluations 30 | - Clear Evaluation Results 31 | - Reindent 32 | - Insert New Line 33 | 34 | 35 | Code formatting 36 | --------------- 37 | 38 | By default, Clojure Sublimed uses [Better Clojure Formatting style](https://tonsky.me/blog/clojurefmt/). 39 | 40 | If you want to use `cljftm`: 41 | 42 | - Download `cljfmt` binary from `https://github.com/weavejester/cljfmt/releases/latest` 43 | - Add `cljfmt` to `$PATH` 44 | - In Sublime Text, open `Preferences: Settings` 45 | - Add `"clojure_sublimed_formatter": "cljfmt"` 46 | 47 | Color scheme 48 | ------------ 49 | 50 | If you want to try our color scheme: 51 | 52 | - Cmd/Ctrl + Shift + P (Command Palette) 53 | - `UI: Select Color Scheme` 54 | - Select `Auto` -> `Clojure Sublimed Light` -> `Clojure Sublimed Dark` 55 | 56 | These color schemes will work for other languages, too. 57 | 58 | 59 | Running your app 60 | ---------------- 61 | 62 | Clojure Sublimed will not run your app for you. A few alternatives instead: 63 | 64 | - Use separate terminal app 65 | - Terminus plugin 66 | - Sublime Executor plugin 67 | 68 | 69 | Connecting to REPLs 70 | ------------------- 71 | 72 | Depending on what type of REPL your run, use following commands: 73 | 74 | - JVM nREPL → `Clojure Sublimed: Connect to nREPL JVM` 75 | - Shadow CLJS client-side → `Clojure Sublimed: Connect to shadow-cljs` 76 | - Shadow CLJS server-side → `Clojure Sublimed: Connect to raw nREPL` 77 | - Socket REPL on JVM → `Clojure Sublimed: Connect to Socket REPL` 78 | - Any other nREPL (babashka, sci, ...) → `Clojure Sublimed: Connect to raw nREPL` 79 | 80 | Clojure Sublimed can only run one REPL connection per window. 81 | 82 | Minimal supported nREPL version is 0.9. 83 | 84 | Read more at https://github.com/tonsky/Clojure-Sublimed/blob/master/README.md -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | // // Evaluate 3 | // {"keys": ["ctrl+enter"], 4 | // "command": "clojure_sublimed_eval", 5 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 6 | 7 | // // Evaluate Buffer 8 | // {"keys": ["ctrl+b"], 9 | // "command": "clojure_sublimed_eval_buffer", 10 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 11 | 12 | // // Interrupt Pending Evaluations 13 | // {"keys": ["ctrl+c"], 14 | // "command": "clojure_sublimed_interrupt_eval", 15 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 16 | 17 | // // Toggle Info 18 | // {"keys": ["ctrl+i"], 19 | // "command": "clojure_sublimed_toggle_info", 20 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 21 | 22 | // // Clear Evaluation Results 23 | // {"keys": ["ctrl+l"], 24 | // "command": "clojure_sublimed_clear_evals", 25 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 26 | 27 | // // Extras 28 | 29 | // // Look for .nrepl-port file and try connection to port in it 30 | // {"keys": ["ctrl+j"], 31 | // "command": "clojure_sublimed_connect_nrepl_jvm", 32 | // "args": {"address": "auto"}}, 33 | 34 | // // Toggle Stacktrace 35 | // {"keys": ["ctrl+e"], 36 | // "command": "clojure_sublimed_toggle_trace", 37 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 38 | 39 | // // Toggle Symbol Info 40 | // {"keys": ["ctrl+d"], 41 | // "command": "clojure_sublimed_toggle_symbol", 42 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 43 | 44 | // // Copy Evaluation Result 45 | // {"keys": ["super+c"], 46 | // "command": "clojure_sublimed_copy", 47 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 48 | 49 | // // Reindent 50 | // {"keys": ["ctrl+f"], 51 | // "command": "clojure_sublimed_reindent", 52 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 53 | 54 | // // Insert New Line 55 | // {"keys": ["enter"], 56 | // "command": "clojure_sublimed_insert_newline", 57 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}, 58 | // {"key": "auto_complete_visible", "operator": "equal", "operand": false}, 59 | // {"key": "panel_has_focus", "operator": "equal", "operand": false}]}, 60 | 61 | // // Toggle Comment 62 | // {"keys": ["super+forward_slash"], 63 | // "command": "clojure_sublimed_toggle_comment", 64 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 65 | ] -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | // // Evaluate 3 | // {"keys": ["ctrl+alt+enter"], 4 | // "command": "clojure_sublimed_eval", 5 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 6 | 7 | // // Evaluate Buffer 8 | // {"keys": ["ctrl+alt+b"], 9 | // "command": "clojure_sublimed_eval_buffer", 10 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 11 | 12 | // // Interrupt Pending Evaluations 13 | // {"keys": ["ctrl+alt+c"], 14 | // "command": "clojure_sublimed_interrupt_eval", 15 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 16 | 17 | // // Toggle Info 18 | // {"keys": ["ctrl+alt+i"], 19 | // "command": "clojure_sublimed_toggle_info", 20 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 21 | 22 | // // Clear Evaluation Results 23 | // {"keys": ["ctrl+alt+l"], 24 | // "command": "clojure_sublimed_clear_evals", 25 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 26 | 27 | // // Extras 28 | 29 | // // Look for .nrepl-port file and try connection to port in it 30 | // {"keys": ["ctrl+alt+j"], 31 | // "command": "clojure_sublimed_connect_nrepl_jvm", 32 | // "args": {"address": "auto"}}, 33 | 34 | // // Toggle Stacktrace 35 | // {"keys": ["ctrl+alt+e"], 36 | // "command": "clojure_sublimed_toggle_trace", 37 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 38 | 39 | // // Toggle Symbol Info 40 | // {"keys": ["ctrl+alt+d"], 41 | // "command": "clojure_sublimed_toggle_symbol", 42 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 43 | 44 | // // Copy Evaluation Result 45 | // {"keys": ["ctrl+c"], 46 | // "command": "clojure_sublimed_copy", 47 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 48 | 49 | // // Reindent 50 | // {"keys": ["ctrl+alt+f"], 51 | // "command": "clojure_sublimed_reindent", 52 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 53 | 54 | // // Insert New Line 55 | // {"keys": ["enter"], 56 | // "command": "clojure_sublimed_insert_newline", 57 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}, 58 | // {"key": "auto_complete_visible", "operator": "equal", "operand": false}, 59 | // {"key": "panel_has_focus", "operator": "equal", "operand": false}]}, 60 | 61 | // // Toggle Comment 62 | // {"keys": ["ctrl+/"], 63 | // "command": "clojure_sublimed_toggle_comment", 64 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 65 | ] -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | // // Evaluate 3 | // {"keys": ["ctrl+alt+enter"], 4 | // "command": "clojure_sublimed_eval", 5 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 6 | 7 | // // Evaluate Buffer 8 | // {"keys": ["ctrl+alt+b"], 9 | // "command": "clojure_sublimed_eval_buffer", 10 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 11 | 12 | // // Interrupt Pending Evaluations 13 | // {"keys": ["ctrl+alt+c"], 14 | // "command": "clojure_sublimed_interrupt_eval", 15 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 16 | 17 | // // Toggle Info 18 | // {"keys": ["ctrl+alt+i"], 19 | // "command": "clojure_sublimed_toggle_info", 20 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 21 | 22 | // // Clear Evaluation Results 23 | // {"keys": ["ctrl+alt+l"], 24 | // "command": "clojure_sublimed_clear_evals", 25 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 26 | 27 | // // Extras 28 | 29 | // // Look for .nrepl-port file and try connection to port in it 30 | // {"keys": ["ctrl+alt+j"], 31 | // "command": "clojure_sublimed_connect_nrepl_jvm", 32 | // "args": {"address": "auto"}}, 33 | 34 | // // Toggle Stacktrace 35 | // {"keys": ["ctrl+alt+e"], 36 | // "command": "clojure_sublimed_toggle_trace", 37 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 38 | 39 | // // Toggle Symbol Info 40 | // {"keys": ["ctrl+alt+d"], 41 | // "command": "clojure_sublimed_toggle_symbol", 42 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 43 | 44 | // // Copy Evaluation Result 45 | // {"keys": ["ctrl+c"], 46 | // "command": "clojure_sublimed_copy", 47 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 48 | 49 | // // Reindent 50 | // {"keys": ["ctrl+alt+f"], 51 | // "command": "clojure_sublimed_reindent", 52 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 53 | 54 | // // Insert New Line 55 | // {"keys": ["enter"], 56 | // "command": "clojure_sublimed_insert_newline", 57 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}, 58 | // {"key": "auto_complete_visible", "operator": "equal", "operand": false}, 59 | // {"key": "panel_has_focus", "operator": "equal", "operand": false}]}, 60 | 61 | // // Toggle Comment 62 | // {"keys": ["ctrl+/"], 63 | // "command": "clojure_sublimed_toggle_comment", 64 | // "context": [{"key": "selector", "operator": "equal", "operand": "source.clojure"}]}, 65 | ] -------------------------------------------------------------------------------- /cs_progress.py: -------------------------------------------------------------------------------- 1 | import sublime, sublime_plugin, threading, time 2 | from . import cs_common, cs_eval 3 | 4 | class ProgressThread: 5 | """ 6 | Thread that updates all pending evals spinners. 7 | Singleton, always running, but if no pending evals are present, sleeps 8 | """ 9 | def __init__(self): 10 | self.running = False 11 | self.condition = threading.Condition() 12 | self.phases = ["🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚", "🕛"] 13 | self.phase_idx = 0 14 | self.interval = 100 15 | 16 | def update_phases(self, phases, interval): 17 | if phases is not None: 18 | self.phases = phases 19 | self.phase_idx = 0 20 | if interval is not None: 21 | self.interval = interval 22 | if len(phases) > 1: 23 | self.start() 24 | else: 25 | self.stop() 26 | 27 | def phase(self): 28 | return self.phases[self.phase_idx] 29 | 30 | def run_loop(self): 31 | thread.update_phases(cs_common.setting("progress_phases"), cs_common.setting("progress_interval_ms")) 32 | while True: 33 | if not self.running: 34 | break 35 | time.sleep(self.interval / 1000.0) 36 | updated = False 37 | if (window := sublime.active_window()) and (view := window.active_view()): 38 | for eval in cs_eval.by_status(view, 'pending'): 39 | eval.update(eval.status, self.phase()) 40 | updated = True 41 | if updated: 42 | self.phase_idx = (self.phase_idx + 1) % len(self.phases) 43 | else: 44 | with self.condition: 45 | self.condition.wait() 46 | 47 | def start(self): 48 | if not self.running: 49 | self.running = True 50 | threading.Thread(daemon=True, target=self.run_loop).start() 51 | 52 | def wake(self): 53 | if self.running: 54 | with self.condition: 55 | self.condition.notify_all() 56 | 57 | def stop(self): 58 | self.running = False 59 | with self.condition: 60 | self.condition.notify_all() 61 | 62 | thread = ProgressThread() 63 | 64 | def phase(): 65 | return thread.phase() 66 | 67 | def wake(): 68 | thread.wake() 69 | 70 | class EventListener(sublime_plugin.EventListener): 71 | def on_activated_async(self, view): 72 | """ 73 | On active view change 74 | """ 75 | thread.wake() 76 | 77 | def on_settings_change(): 78 | thread.update_phases(cs_common.setting("progress_phases"), cs_common.setting("progress_interval_ms")) 79 | 80 | def plugin_loaded(): 81 | cs_common.on_settings_change(__name__, on_settings_change) 82 | 83 | def plugin_unloaded(): 84 | thread.stop() 85 | cs_common.clear_settings_change(__name__) 86 | -------------------------------------------------------------------------------- /cs_eval_status.py: -------------------------------------------------------------------------------- 1 | import sublime, sublime_plugin 2 | from . import cs_common, cs_conn, cs_eval, cs_parser, cs_progress 3 | 4 | status_key = 'clojure-sublimed-eval-status' 5 | 6 | class StatusEval: 7 | """ 8 | Displays 'eval_code' command results in status bar 9 | """ 10 | def __init__(self, code, id = None, batch_id = None): 11 | self.window = sublime.active_window() 12 | state = cs_common.get_state(self.window) 13 | 14 | if state.status_eval: 15 | state.status_eval.erase() 16 | 17 | self.id = id or cs_eval.Eval.next_id() 18 | self.batch_id = batch_id or self.id 19 | self.code = code 20 | self.session = None 21 | self.ex_source = None 22 | self.ex_line = None 23 | self.ex_column = None 24 | self.trace = None 25 | self.on_finish = None 26 | 27 | state.status_eval = self 28 | 29 | self.update('pending', cs_progress.phase()) 30 | cs_progress.wake() 31 | 32 | def update(self, status, value, time_taken = None): 33 | self.status = status 34 | self.value = value 35 | if status in {"pending", "interrupt"}: 36 | cs_common.set_status(self.window, status_key, "⏳ " + self.code) 37 | elif "success" == status: 38 | if time := cs_common.format_time_taken(time_taken): 39 | value = time + ' ' + value 40 | cs_common.set_status(self.window, status_key, "✅ " + value) 41 | elif "failure" == status: 42 | if time := cs_common.format_time_taken(time_taken): 43 | value = time + ' ' + value 44 | cs_common.set_status(self.window, status_key, "❌ " + value) 45 | elif "exception" == status: 46 | if time := cs_common.format_time_taken(time_taken): 47 | value = time + ' ' + value 48 | msg = "❌ " + value 49 | if self.ex_source: 50 | msg += ", at " + self.ex_source 51 | if self.ex_line: 52 | msg += ":" + str(self.ex_line) 53 | if self.ex_column: 54 | msg += ":" + str(self.ex_column) 55 | cs_common.set_status(self.window, status_key, msg) 56 | 57 | def erase(self, interrupt = True): 58 | state = cs_common.get_state(self.window) 59 | cs_common.set_status(self.window, status_key, None) 60 | state.status_eval = None 61 | if interrupt and self.status == "pending" and self.session: 62 | state.conn.interrupt(self.id) 63 | 64 | class ClojureSublimedEvalCodeCommand(sublime_plugin.WindowCommand): 65 | def run(self, code, ns = None): 66 | if (not ns) and (view := cs_common.active_view()): 67 | ns = cs_parser.namespace(view, view.size()) 68 | state = cs_common.get_state(self.window) 69 | state.conn.eval_status(code, ns or 'user') 70 | 71 | def is_enabled(self): 72 | if not cs_conn.ready(self.window): 73 | return False 74 | state = cs_common.get_state(self.window) 75 | if state.status_eval and state.status_eval.status in {'pending', 'interrupt'}: 76 | return False 77 | return True 78 | -------------------------------------------------------------------------------- /cs_colors.py: -------------------------------------------------------------------------------- 1 | import re, sublime 2 | 3 | RE_REPLACE_GLOB = re.compile(r"\*\*|[\*\?\.\(\)\[\]\{\}\$\^\+\|]") 4 | 5 | region_id = 0 6 | 7 | # Colors 8 | FG_ANSI = { 9 | 30: 'black', 10 | 31: 'red', 11 | 32: 'green', 12 | 33: 'brown', 13 | 34: 'blue', 14 | 35: 'magenta', 15 | 36: 'cyan', 16 | 37: 'white', 17 | 39: 'default', 18 | 90: 'light_black', 19 | 91: 'light_red', 20 | 92: 'light_green', 21 | 93: 'light_brown', 22 | 94: 'light_blue', 23 | 95: 'light_magenta', 24 | 96: 'light_cyan', 25 | 97: 'light_white' 26 | } 27 | 28 | BG_ANSI = { 29 | 40: 'black', 30 | 41: 'red', 31 | 42: 'green', 32 | 43: 'brown', 33 | 44: 'blue', 34 | 45: 'magenta', 35 | 46: 'cyan', 36 | 47: 'white', 37 | 49: 'default', 38 | 100: 'light_black', 39 | 101: 'light_red', 40 | 102: 'light_green', 41 | 103: 'light_brown', 42 | 104: 'light_blue', 43 | 105: 'light_magenta', 44 | 106: 'light_cyan', 45 | 107: 'light_white' 46 | } 47 | 48 | SCOPES = { 49 | 'red': 'redish', 50 | 'green': 'greenish', 51 | 'brown': 'orangish', 52 | 'blue': 'bluish', 53 | 'magenta': 'pinkish', # purplish 54 | 'cyan': 'cyanish', 55 | 'light_red': 'redish', 56 | 'light_green': 'greenish', 57 | 'light_brown': 'orangish', 58 | 'light_blue': 'bluish', 59 | 'light_magenta': 'pinkish', 60 | 'light_cyan': 'cyanish' 61 | } 62 | 63 | RE_UNKNOWN_ESCAPES = re.compile(r"\x1b[^a-zA-Z]*[a-zA-Z]") 64 | RE_COLOR_ESCAPES = re.compile(r"\x1b\[((?:;?\d+)*)m") 65 | RE_NOTSPACE = re.compile(r"[^\s]+") 66 | 67 | def write(view, characters): 68 | decolorized = "" 69 | original_pos = 0 70 | decolorized_pos = 0 71 | fg = "default" 72 | bg = "default" 73 | regions = [] 74 | def iteration(start, end, group): 75 | nonlocal decolorized, original_pos, decolorized_pos, fg, bg, regions 76 | text = characters[original_pos:start] 77 | text = RE_UNKNOWN_ESCAPES.sub("", text) 78 | decolorized += text 79 | if len(text) > 0 and (fg != "default" or bg != "default"): 80 | regions.append({"text": text, 81 | "start": decolorized_pos, 82 | "end": decolorized_pos + len(text), 83 | "fg": fg, 84 | "bg": bg}) 85 | digits = re.findall(r"\d+", group) or ["0"] 86 | for digit in digits: 87 | digit = int(digit) 88 | if digit in FG_ANSI: 89 | fg = FG_ANSI[digit] 90 | if digit in BG_ANSI: 91 | bg = BG_ANSI[digit] 92 | if digit == 0: 93 | fg = 'default' 94 | bg = 'default' 95 | original_pos = end 96 | decolorized_pos += len(text) 97 | 98 | for m in RE_COLOR_ESCAPES.finditer(characters): 99 | iteration(m.start(), m.end(), m.group(1)) 100 | iteration(len(characters), len(characters), "") 101 | 102 | insertion_point = view.size() 103 | view.run_command('append', {'characters': decolorized, 'force': True, 'scroll_to_end': True}) 104 | 105 | global region_id 106 | for region in regions: 107 | if scope := SCOPES.get(region['bg'], None) or SCOPES.get(region['fg'], None): 108 | for m in RE_NOTSPACE.finditer(region['text']): 109 | start = insertion_point + region['start'] + m.start() 110 | end = start + len(m.group(0)) 111 | region_id += 1 112 | view.add_regions( 113 | "executor#{}".format(region_id), 114 | [sublime.Region(start, end)], 115 | 'region.' + scope) 116 | -------------------------------------------------------------------------------- /test_scheme/color_scheme.clj: -------------------------------------------------------------------------------- 1 | 2 | ; Constants 3 | nil true false \c \tab 1 1.0 1/2 #inst "1985-01-25" 4 | 5 | ; Symbols 6 | abc ab/cd _abc 7 | 8 | ; Keywords 9 | :a :a.b/c.d ::ab ::a.b/c.d 10 | 11 | ; Strings 12 | "" "abc" "\" \u221e \x" 13 | 14 | ; Regexps 15 | #"re \n \uFFFF \p{L} \Qabc\E) \y" 16 | 17 | ; Top-level parens 18 | () [] {} #() #{} #?() #?@() 19 | 20 | ; Nested parens 21 | (() [] {} #() #{} #?() #?@()) 22 | [() [] {} #() #{} #?() #?@()] 23 | {() [] {} #() #{} #?() #?@()} 24 | #{() [] {} #() #{} #?() #?@()} 25 | #(() [] {} #() #{} #?() #?@()) 26 | ([{}]) 27 | (((((((((()))))))))) [[[[[[[[[[]]]]]]]]]] {{{{{{{{{{}}}}}}}}}} 28 | 29 | ; Definitions 30 | (def xyz) 31 | (def xyz xyz) 32 | (def xyz 123) 33 | (def 123 xyz) 34 | (def 35 | xyz) 36 | (do (def xyz)) 37 | 38 | ; Punctuation 39 | , 40 | 41 | ; Meta 42 | ^{:s sym 43 | :v "str\n" 44 | :n 123 45 | :re #"\p{L}) \y" 46 | :l ([{}]) 47 | :q '(abc) 48 | :sq `(abc ~def) 49 | :m ^int i 50 | :d @ref 51 | :v #'var 52 | :c #?(:clj) 53 | :df (def x 1) 54 | ; linecomment 55 | #_#_reader comment 56 | (comment form))))} sym 57 | 58 | ; Quotes 59 | '{:symbols [name/space _ _abc] 60 | :strings ["str\n\x" '"str"] 61 | :regexp #"\p{L}) \y" 62 | :number 123.456 63 | :keyword :key/word 64 | :parens (() #() #?(:clj :cljs) [] {} #{}) 65 | :quoted ('abc `(x ~y)) 66 | :meta ^int i 67 | :vars [@ref #'var] 68 | :defs (def x 1) 69 | ; linecomment 70 | #_#_reader comment 71 | (comment form))))} 72 | 73 | ; Syntax quotes 74 | `{:symbols [name/space _ _abc] 75 | :strings ["str\n\x" '"str"] 76 | :regexp #"\p{L}) \y" 77 | :number 123.456 78 | :keyword :key/word 79 | :parens (() #() #?(:clj :cljs) [] {} #{}) 80 | :quoted ('abc `(x ~y)) 81 | :meta ^int i 82 | :vars [@ref #'var] 83 | :defs (def x 1) 84 | ; linecomment 85 | #_#_reader comment 86 | (comment form))))} 87 | 88 | `{:symbols [name/space _ _abc] 89 | :strings ["str\n\x" '"str"] 90 | :regexp #"\p{L}) \y" 91 | :number 123.456 92 | :keyword :key/word 93 | :parens (() #() #?(:clj :cljs) [] {} #{}) 94 | :quoted ('abc `(x ~y)) 95 | :meta ^int i 96 | :vars [@ref #'var] 97 | :defs (def x 1) 98 | ; linecomment 99 | #_#_reader comment 100 | (comment form))))} 101 | 102 | `~{:symbols [name/space _ _abc] 103 | :strings ["str\n\x" '"str"] 104 | :regexp #"\p{L}) \y" 105 | :number 123.456 106 | :keyword :key/word 107 | :parens (() #() #?(:clj :cljs) [] {} #{}) 108 | :quoted ('abc `(x ~y)) 109 | :meta ^int i 110 | :vars [@ref #'var] 111 | :defs (def x 1) 112 | ; linecomment 113 | #_#_reader comment 114 | (comment form))))} 115 | 116 | ; Line comments 117 | ; {:symbols [name/space _ _abc] 118 | ; :strings ["str\n\x" '"str"] 119 | ; :regexp #"\p{L}) \y" 120 | ; :number 123.456 121 | ; :keyword :key/word 122 | ; :parens (() #() #?(:clj :cljs) [] {} #{}) 123 | ; :quoted ('abc `(x ~y)) 124 | ; :meta ^int i 125 | ; :vars [@ref #'var] 126 | ; :defs (def x 1) 127 | ; ; linecomment 128 | ; #_#_reader comment 129 | ; (comment form))))} 130 | 131 | ; Reader comments 132 | #_{:symbols [name/space _ _abc] 133 | :strings ["str\n\x" '"str"] 134 | :regexp #"\p{L}) \y" 135 | :number 123.456 136 | :keyword :key/word 137 | :parens (() #() #?(:clj :cljs) [] {} #{}) 138 | :quoted ('abc `(x ~y)) 139 | :meta ^int i 140 | :vars [@ref #'var] 141 | :defs (def x 1) 142 | ; linecomment 143 | #_#_reader comment 144 | (comment form))))} 145 | 146 | ; Form comments 147 | (comment 148 | {:symbols [name/space _ _abc] 149 | :strings ["str\n\x" '"str"] 150 | :regexp #"\p{L}) \y" 151 | :number 123.456 152 | :keyword :key/word 153 | :parens (() #() #?(:clj :cljs) [] {} #{}) 154 | :quoted ('abc `(x ~y)) 155 | :meta ^int i 156 | :vars [@ref #'var] 157 | :defs (def x 1) 158 | ; linecomment 159 | #_#_reader comment 160 | (comment form))))}) 161 | -------------------------------------------------------------------------------- /docs/protocol_socket.md: -------------------------------------------------------------------------------- 1 | # Upgraded Socket REPL protocol 2 | 3 | All messages are: 4 | 5 | - EDN-formatted, 6 | - No keywords or symbols, only strings and ints, 7 | - '\n' inside strings escaped, 8 | - no newlines inside messages, 9 | - newline after each message. 10 | 11 | --- 12 | 13 | ``` 14 | RCV {"tag" "started"} 15 | ``` 16 | 17 | When client receives this message, REPL has finished upgrading and is ready to accept commands. 18 | 19 | --- 20 | 21 | ``` 22 | SND {"op" => "eval" 23 | "id" => any, id 24 | "code" => string. Code to evaluate 25 | "ns" => string, optional. Namespace name. Defaults to user 26 | "file" => string, optional. File name where this code comes from. Defaults to NO_SOURCE_FILE 27 | "line" => int, optional. Line in file 28 | "column" => int, optional. Column position of first code character, 0-based} 29 | ``` 30 | 31 | Evaluate form. If `code` contains multiple top-level forms, they are evaluated sequentially. After each successful evaluation, you get this: 32 | 33 | ``` 34 | RCV {"tag" => "ret" 35 | "id" => any, id 36 | "idx" => int, sequential number of form in original `code` 37 | "val" => string, pr-str value 38 | "time" => int, execution time, ms 39 | "form" => string, form that being evaluated 40 | "from_line" => int, line at the beginning of the form 41 | "from_column" => int, column at the beginning of the form 42 | "to_line" => int, line at the end of the form 43 | "to_column" => int, column at the end of the form} 44 | ``` 45 | 46 | Value string will be truncated after 1024 characters, so don’t rely on it be valid readable Clojure. 47 | 48 | If some form fails, you get this and batch execution stops: 49 | 50 | ``` 51 | RCV {"tag" => "ex" 52 | "id" => any. Id 53 | "val" => string. Error messasge 54 | "trace" => multiline string. Stacktrace 55 | "source" => string, if known. File name 56 | "line" => int, if known 57 | "column" => int, 0-based, if known 58 | "form", "from_"/"to_" "_line"/"_column" => same as in "ret"} 59 | ``` 60 | 61 | Finally, success of failure, you’ll always recieve this: 62 | 63 | ``` 64 | {"tag" => "done" 65 | "id" => any. Id} 66 | ``` 67 | 68 | --- 69 | 70 | To interrupt evaluation, send this: 71 | 72 | ``` 73 | SND {"op" => "interrupt" 74 | "id" => any} 75 | ``` 76 | 77 | Whole batch will be stopped by throwing an exception. You’ll receive :ex and :done after this 78 | 79 | --- 80 | 81 | To look up a symbol, send this: 82 | 83 | ``` 84 | SND {"op" => "lookup" 85 | "id" => any 86 | "symbol" => string 87 | "ns" => string, optional, defaults to user} 88 | ``` 89 | 90 | To which you’ll get either 91 | 92 | ``` 93 | RCV {"tag" => "lookup" 94 | "id" => any 95 | "val" => map, description} 96 | ``` 97 | 98 | Val map looks like this for functions: 99 | 100 | ``` 101 | {"ns" "clojure.core" 102 | "name" "str" 103 | "arglists" "([] [x] [x & ys])" 104 | "doc" "With no args, returns the empty string. With one arg x, returns\n x.toString(). (str nil) returns the empty string. With more than\n one arg, returns the concatenation of the str values of the args." 105 | "file" "clojure/core.clj" 106 | "line" "546" 107 | "column" "1" 108 | "added" "1.0"} 109 | ``` 110 | 111 | for vars: 112 | 113 | ``` 114 | {"ns" "clojure.core" 115 | "name" "*warn-on-reflection*" 116 | "doc" "When set to true, the compiler will emit warnings when reflection is\n needed to resolve Java method calls or field accesses.\n\n Defaults to false." 117 | "added" "1.0"} 118 | ``` 119 | 120 | and for special forms: 121 | 122 | ``` 123 | {"ns" "clojure.core" 124 | "name" "do" 125 | "forms" "[(do exprs*)]" 126 | "doc" "Evaluates the expressions in order and returns the value of\n the last. If no expressions are supplied, returns nil." 127 | "file" "clojure/core.clj" 128 | "special-form" "true"} 129 | ``` 130 | 131 | If symbol can’t be found, you’ll get: 132 | 133 | ``` 134 | RCV {"tag" => "ex" 135 | "id" => any, id 136 | "val" => string, message} 137 | ``` 138 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Clojure Sublimed: Connect to nREPL JVM", 4 | "command": "clojure_sublimed_connect_nrepl_jvm" 5 | }, 6 | { 7 | "caption": "Clojure Sublimed: Connect to raw nREPL", 8 | "command": "clojure_sublimed_connect_nrepl_raw" 9 | }, 10 | { 11 | "caption": "Clojure Sublimed: Toggle output panel", 12 | "command": "clojure_sublimed_toggle_output_panel" 13 | }, 14 | 15 | { 16 | "caption": "Clojure Sublimed: Connect to shadow-cljs", 17 | "command": "clojure_sublimed_connect_shadow_cljs" 18 | }, 19 | { 20 | "caption": "Clojure Sublimed: Connect to Socket REPL", 21 | "command": "clojure_sublimed_connect_socket_repl" 22 | }, 23 | { 24 | "caption": "Clojure Sublimed: Disconnect", 25 | "command": "clojure_sublimed_disconnect" 26 | }, 27 | { 28 | "caption": "Clojure Sublimed: Reconnect", 29 | "command": "clojure_sublimed_reconnect" 30 | }, 31 | { 32 | "caption": "Clojure Sublimed: Evaluate", 33 | "command": "clojure_sublimed_eval" 34 | }, 35 | { 36 | "caption": "Clojure Sublimed: Evaluate Buffer", 37 | "command": "clojure_sublimed_eval_buffer" 38 | }, 39 | { 40 | "caption": "Clojure Sublimed: Evaluate Previous Form at Current Level", 41 | "command": "clojure_sublimed_eval_previous_form" 42 | }, 43 | { 44 | "caption": "Clojure Sublimed: Interrupt Pending Evaluations", 45 | "command": "clojure_sublimed_interrupt_eval" 46 | }, 47 | { 48 | "caption": "Clojure Sublimed: Copy Evaluation Result", 49 | "command": "clojure_sublimed_copy" 50 | }, 51 | { 52 | "caption": "Clojure Sublimed: Toggle Stacktrace", 53 | "command": "clojure_sublimed_toggle_trace" 54 | }, 55 | { 56 | "caption": "Clojure Sublimed: Toggle Symbol Info", 57 | "command": "clojure_sublimed_toggle_symbol" 58 | }, 59 | { 60 | "caption": "Clojure Sublimed: Toggle Info", 61 | "command": "clojure_sublimed_toggle_info" 62 | }, 63 | { 64 | "caption": "Clojure Sublimed: Clear Evaluation Results", 65 | "command": "clojure_sublimed_clear_evals" 66 | }, 67 | { 68 | "caption": "Clojure Sublimed: Reindent", 69 | "command": "clojure_sublimed_reindent" 70 | }, 71 | { 72 | "caption": "Clojure Sublimed: Reindent Lines", 73 | "command": "clojure_sublimed_reindent_lines" 74 | }, 75 | { 76 | "caption": "Clojure Sublimed: Reindent Buffer", 77 | "command": "clojure_sublimed_reindent_buffer" 78 | }, 79 | { 80 | "caption": "Clojure Sublimed: Pretty-print selection", 81 | "command": "clojure_sublimed_pretty_print" 82 | }, 83 | { 84 | "caption": "Clojure Sublimed: Select topmost form", 85 | "command": "clojure_sublimed_select_topmost_form" 86 | }, 87 | { 88 | "caption": "Clojure Sublimed: Toggle comment", 89 | "command": "clojure_sublimed_toggle_comment" 90 | }, 91 | { 92 | "caption": "Clojure Sublimed: Insert Newline", 93 | "command": "clojure_sublimed_insert_newline" 94 | }, 95 | { 96 | "caption": "Clojure Sublimed: Add Watch", 97 | "command": "clojure_sublimed_add_watch" 98 | }, 99 | { 100 | "caption": "Clojure Sublimed: Align Cursors", 101 | "command": "clojure_sublimed_align_cursors" 102 | }, 103 | { 104 | "caption": "Preferences: Clojure Sublimed Settings", 105 | "command": "edit_settings", 106 | "args": { 107 | "base_file": "${packages}/Clojure Sublimed/Clojure Sublimed.sublime-settings", 108 | "default": "// Clojure Sublimed Settings - User\n{\n\t$0\n}\n" 109 | } 110 | }, 111 | { 112 | "caption": "Preferences: Clojure Sublimed Key Bindings", 113 | "command": "edit_settings", 114 | "args": { 115 | "base_file": "${packages}/Clojure Sublimed/Default (${platform}).sublime-keymap", 116 | "user_file": "${packages}/User/Default (${platform}).sublime-keymap" 117 | } 118 | }, 119 | ] -------------------------------------------------------------------------------- /cs_cljfmt.py: -------------------------------------------------------------------------------- 1 | import difflib, os, re, sublime, subprocess 2 | from . import cs_common, cs_parser 3 | 4 | def format_string(text, view = None, cwd = None): 5 | try: 6 | cmd = 'cljfmt.exe' if 'windows' == sublime.platform() else 'cljfmt' 7 | if not cwd: 8 | if file := view.file_name(): 9 | cwd = os.path.dirname(file) 10 | elif folders := view.window().folders(): 11 | cwd = folders[0] 12 | 13 | proc = subprocess.run([cmd, 'fix', '-'], 14 | input = text, 15 | text = True, 16 | capture_output = True, 17 | check = True, 18 | cwd = cwd) 19 | except FileNotFoundError: 20 | sublime.error_message(f'`{cmd}` is not on $PATH') 21 | raise 22 | if 'Failed' not in proc.stderr: 23 | return proc.stdout 24 | 25 | def indent_lines(view, regions, edit): 26 | regions = [region for region in regions if not region.empty()] 27 | if not regions: 28 | regions = [sublime.Region(0, view.size())] 29 | replacements = [] 30 | for region in regions: 31 | text = view.substr(region) 32 | if text_formatted := format_string(text, view = view): 33 | pos = region.begin() 34 | diff = difflib.ndiff(text.splitlines(keepends=True), text_formatted.splitlines(keepends=True)) 35 | for line in diff: 36 | if line[:2] == '- ': 37 | replacements.append((sublime.Region(pos, pos + len(line) - 2), '')) 38 | pos = pos + len(line) - 2 39 | elif line[:2] == '+ ': 40 | replacements.append((sublime.Region(pos, pos), line[2:])) 41 | elif line[:2] == ' ': 42 | pos = pos + len(line) - 2 43 | elif line[:2] == '? ': 44 | pass 45 | if replacements: 46 | selections_before = [(view.rowcol(r.a), view.rowcol(r.b)) for r in view.sel()] 47 | delta = 0 48 | for region, string in replacements: 49 | transformed_region = sublime.Region(region.a + delta, region.b + delta) 50 | view.replace(edit, transformed_region, string) 51 | delta = delta - region.size() + len(string) 52 | 53 | view.sel().clear() 54 | for ((rowa, cola), (rowb, colb)) in selections_before: 55 | a = view.text_point(rowa, cola) 56 | b = view.text_point(rowb, colb) 57 | view.sel().add(sublime.Region(a, b)) 58 | 59 | def newline_indent(view, point): 60 | text = view.substr(sublime.Region(0, point)) 61 | parsed = cs_parser.parse(text) 62 | to_close = [] 63 | node = parsed 64 | start = node.children[-1].start if node.children else 0 65 | while node: 66 | if 'string' == node.name and node.open and not node.close: 67 | to_close.insert(0, '"') 68 | elif 'parens' == node.name and node.open and not node.close: 69 | to_close.insert(0, ')') 70 | elif 'braces' == node.name and node.open and not node.close: 71 | to_close.insert(0, '}') 72 | elif 'brackets' == node.name and node.open and not node.close: 73 | to_close.insert(0, ']') 74 | node = node.children[-1] if node.children else None 75 | if to_close and '"' == to_close[0]: 76 | return None 77 | 78 | ns = None 79 | for child in parsed.children: 80 | if child.end >= start: 81 | break 82 | if child.name == 'meta': 83 | child = child.body.children[0] 84 | if child.name == 'parens': 85 | body = child.body 86 | if body and len(body.children) >= 2: 87 | first_form = body.children[0] 88 | if first_form.name == 'token' and first_form.text == 'ns': 89 | ns = child 90 | 91 | excerpt = '' 92 | if ns: 93 | excerpt = text[ns.start:ns.end] + '\n' 94 | 95 | excerpt = excerpt + text[start:] + "\nCLOJURE_SUBLIMED_SYM" + "".join(to_close) 96 | formatted = format_string(excerpt, view = view) 97 | last_line = formatted.splitlines()[-1] 98 | indent = re.match(r"^\s*", last_line)[0] 99 | return len(indent) 100 | -------------------------------------------------------------------------------- /cs_comment.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sublime, sublime_plugin 3 | from . import cs_parser 4 | 5 | def search_point(node, pos): 6 | # no children 7 | if node.nested() is None: 8 | if node.start <= pos and pos <= node.end: 9 | return node 10 | else: 11 | return None 12 | 13 | # uncomment 14 | if node.is_terminal() and node.start <= pos and pos <= node.end: 15 | return node 16 | 17 | prev_child = None 18 | res = None 19 | for child in node.nested(): 20 | # has children: between two (() () | ()) 21 | # has children: before first ( | () () ()) 22 | if child.start > pos: 23 | res = prev_child 24 | break 25 | 26 | # has children: at the start (() |() ()) 27 | # has children: inside one (() (|) ()) 28 | # has children: at the end (() ()| ()) 29 | if child.start <= pos and pos <= child.end: 30 | res = search_point(child, pos) 31 | break 32 | 33 | prev_child = child 34 | 35 | if res: 36 | return res 37 | 38 | if node.start <= pos and pos <= node.end: 39 | return node 40 | 41 | def search_range(node, start, end): 42 | if node.nested() is not None and node.start <= start and end <= node.end: 43 | if start <= node.start and node.end <= end: 44 | return [node] 45 | res = [] 46 | for child in node.nested(): 47 | if child.nested() is not None and child.start <= start and end <= child.end: 48 | return search_range(child, start, end) 49 | # (...)...[...] - no 50 | # (...[...)...] 51 | # [...(...)...] 52 | # [...(...]...) 53 | # [...]...(...) - no 54 | # (...[...]...) 55 | elif not child.end <= start and not child.start >= end: 56 | res.append(child) 57 | return res or [node] 58 | 59 | class ClojureSublimedToggleCommentCommand(sublime_plugin.TextCommand): 60 | def run(self, edit): 61 | view = self.view 62 | parsed = cs_parser.parse_tree(view) 63 | sel = [] 64 | offset = 0 65 | regions = [r for r in view.sel()] 66 | regions.sort(key = lambda r: r.begin()) 67 | for region in regions: 68 | if region.empty(): 69 | nodes = [search_point(parsed, region.begin())] 70 | else: 71 | nodes = search_range(parsed, region.begin(), region.end()) 72 | a = region.a + offset 73 | b = region.b + offset 74 | if all(node.name == "comment" for node in nodes): 75 | # uncomment line comments 76 | for node in nodes: 77 | m = re.match(r'^;+\s*', node.text) 78 | r = sublime.Region(node.start + offset, node.start + offset + len(m[0])) 79 | view.replace(edit, r, "") 80 | a = a if a < r.begin() else r.begin() if a < r.end() else a - r.size() 81 | b = b if b < r.begin() else r.begin() if b < r.end() else b - r.size() 82 | offset -= r.size() 83 | elif all(node.name in ["discard", "comment"] for node in nodes): 84 | # uncomment discards only 85 | for node in nodes: 86 | if node.name == "discard": 87 | r = sublime.Region(node.start + offset, node.body.start + offset) 88 | view.replace(edit, r, "") 89 | a = a if a < r.begin() else r.begin() if a < r.end() else a - r.size() 90 | b = b if b < r.begin() else r.begin() if b < r.end() else b - r.size() 91 | offset -= r.size() 92 | else: 93 | for node in nodes: 94 | if node.name not in ["discard", "comment"]: 95 | # comment 96 | r = sublime.Region(node.start + offset, node.start + offset) 97 | view.replace(edit, r, "#_") 98 | a = a if a < r.begin() else a + 2 99 | b = b if b < r.begin() else b + 2 100 | offset += 2 101 | sel.append(sublime.Region(a, b)) 102 | view.sel().clear() 103 | view.sel().add_all(sel) 104 | -------------------------------------------------------------------------------- /cs_conn_shadow_cljs.py: -------------------------------------------------------------------------------- 1 | import os, re, sublime, sublime_plugin 2 | from . import cs_common, cs_conn, cs_conn_nrepl_raw, cs_eval 3 | 4 | class ConnectionShadowCljs(cs_conn_nrepl_raw.ConnectionNreplRaw): 5 | """ 6 | Shadow CLJS connnection. Requires an additional argument: build 7 | """ 8 | def __init__(self, addr, build): 9 | super().__init__(addr) 10 | self.build = build 11 | 12 | def handle_connect(self, msg): 13 | if 1 == msg.get('id') and 'new-session' in msg: 14 | self.session = msg['new-session'] 15 | self.set_status(2, 'Upgrading REPL') 16 | 17 | if self.build == 'node-repl': 18 | code = '(shadow.cljs.devtools.api/node-repl)' 19 | elif self.build == 'browser-repl': 20 | code = '(shadow.cljs.devtools.api/browser-repl)' 21 | else: 22 | code = f'(shadow.cljs.devtools.api/repl {self.build})' 23 | self.send({'id': 2, 24 | 'session': self.session, 25 | 'op': 'eval', 26 | 'code': code}) 27 | 28 | return True 29 | elif 2 == msg.get('id') and msg.get('status') == ['done']: 30 | self.set_status(4, self.get_addr()) 31 | return True 32 | 33 | def handle_value(self, msg): 34 | if 'value' in msg and (id := msg.get('id')): 35 | eval = cs_eval.by_id(id) 36 | value = msg.get('value') 37 | if eval and eval.status == 'exception' and ('nil' == value or value.startswith(':repl/')): 38 | pass 39 | else: 40 | cs_eval.on_success(id, msg.get('value')) 41 | return True 42 | 43 | def handle_err(self, msg): 44 | if 'err' in msg and (id := msg.get('id')): 45 | eval = cs_eval.by_id(id) 46 | trace = msg['err'] 47 | error = re.sub(r'\s*------+\s*', '', trace) 48 | if eval and eval.status == 'exception': 49 | trace = eval.trace + '\n' + trace 50 | error = eval.value + '\n' + error 51 | cs_eval.on_exception(id, error, trace = trace) 52 | return True 53 | 54 | def load_file_impl(self, id, file, path): 55 | msg = {'id': id, 56 | 'session': self.session, 57 | 'op': 'load-file', 58 | 'file': file, 59 | 'file-name': os.path.basename(path) if path else "NO_SOURCE_FILE.cljc"} 60 | if path: 61 | msg['file-path'] = path 62 | self.send(msg) 63 | 64 | def load_file(self, view): 65 | if view.file_name(): 66 | super().load_file(view) 67 | else: 68 | self.eval(view, [sublime.Region(0, view.size())]) 69 | 70 | 71 | class BuildInputHandler(sublime_plugin.TextInputHandler): 72 | def initial_text(self): 73 | return ':app' 74 | 75 | def preview(self, text): 76 | return sublime.Html(""" 77 | 78 | 79 | Provide the cljs build for shadow to watch. 80 |
81 | Valid options are node-repl, browser-repl or the build defined in shadow-cljs.edn / project.clj 82 | For more info check Shadow Documentation 83 | 84 | 85 | """) 86 | 87 | class ClojureSublimedConnectShadowCljsCommand(sublime_plugin.WindowCommand): 88 | def run(self, address, build, timeout = 0): 89 | state = cs_common.get_state(self.window) 90 | state.last_conn = ('clojure_sublimed_connect_shadow_cljs', {'address': address, 'build': build}) 91 | ConnectionShadowCljs(address, build).try_connect(timeout = timeout) 92 | 93 | def input(self, args): 94 | if 'address' in args and 'build' in args: 95 | pass 96 | elif 'address' in args: 97 | return BuildInputHandler() 98 | elif 'build' in args: 99 | return cs_conn.AddressInputHandler(port_files = ['.nrepl-port', '.shadow-cljs/nrepl.port']) 100 | else: 101 | return cs_conn.AddressInputHandler(port_files = ['.nrepl-port', '.shadow-cljs/nrepl.port'], next_input = BuildInputHandler()) 102 | 103 | def is_enabled(self): 104 | state = cs_common.get_state(self.window) 105 | return state.conn is None 106 | -------------------------------------------------------------------------------- /src_clojure/clojure_sublimed/middleware.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-sublimed.middleware 2 | (:require 3 | [clojure.string :as str] 4 | [clojure-sublimed.core :as core] 5 | [nrepl.middleware :as middleware] 6 | [nrepl.middleware.print :as print] 7 | [nrepl.middleware.caught :as caught] 8 | [nrepl.middleware.session :as session] 9 | [nrepl.transport :as transport]) 10 | (:import 11 | [nrepl.transport Transport])) 12 | 13 | (defn on-send [{:keys [transport] :as msg} on-send] 14 | (assoc msg :transport 15 | (reify Transport 16 | (recv [this] 17 | (transport/recv transport)) 18 | (recv [this timeout] 19 | (transport/recv transport timeout)) 20 | (send [this resp] 21 | (when-some [resp' (on-send resp)] 22 | (transport/send transport resp')) 23 | this)))) 24 | 25 | (defn after-send [{:keys [transport] :as msg} after-send] 26 | (assoc msg :transport 27 | (reify Transport 28 | (recv [this] 29 | (transport/recv transport)) 30 | (recv [this timeout] 31 | (transport/recv transport timeout)) 32 | (send [this resp] 33 | (transport/send transport resp) 34 | (after-send resp) 35 | this)))) 36 | 37 | (defn print-root-trace [^Throwable t] 38 | (println (core/trace-str t))) 39 | 40 | (defn- populate-caught [{t ::caught/throwable :as resp}] 41 | (let [root ^Throwable (core/root-cause t) 42 | {:clojure.error/keys [source line column]} (ex-data root) 43 | cause ^Throwable (or (some-> root .getCause) root) 44 | data (ex-data cause) 45 | resp' (cond-> resp 46 | cause (assoc 47 | ::caught/throwable root 48 | ::root-ex-msg (.getMessage cause) 49 | ::root-ex-class (.getSimpleName (class cause)) 50 | ::trace (core/trace-str root)) 51 | source (assoc ::source source) 52 | line (assoc ::line line) 53 | column (assoc ::column column) 54 | data (update ::print/keys (fnil conj []) ::root-ex-data) 55 | data (assoc ::root-ex-data data))] 56 | resp')) 57 | 58 | (defn wrap-errors [handler] 59 | (fn [msg] 60 | (handler (-> msg (on-send populate-caught))))) 61 | 62 | (middleware/set-descriptor! 63 | #'wrap-errors 64 | {:requires #{#'caught/wrap-caught} ;; run inside wrap-caught 65 | :expects #{"eval"} ;; but outside of "eval" 66 | :handles {}}) 67 | 68 | 69 | (defn- redirect-output [resp] 70 | (when-some [out (:out resp)] 71 | (.print System/out out) 72 | (.flush System/out)) 73 | (when-some [err (:err resp)] 74 | (.print System/err err) 75 | (.flush System/err)) 76 | resp) 77 | 78 | (defn wrap-output [handler] 79 | (fn [msg] 80 | (handler (-> msg (on-send redirect-output))))) 81 | 82 | (middleware/set-descriptor! 83 | #'wrap-output 84 | {:requires #{} 85 | :expects #{"eval"} ;; run outside of "eval" 86 | :handles {}}) 87 | 88 | 89 | (defn time-eval [handler] 90 | (fn [{:keys [op] :as msg}] 91 | (if (= "eval" op) 92 | (let [start (System/nanoTime)] 93 | (-> msg 94 | (on-send #(cond-> % (contains? % :value) (assoc ::time-taken (- (System/nanoTime) start)))) 95 | (handler))) 96 | (handler msg)))) 97 | 98 | (middleware/set-descriptor! 99 | #'time-eval 100 | {:requires #{#'wrap-output #'wrap-errors #'print/wrap-print} 101 | :expects #{"eval"} 102 | :handles {}}) 103 | 104 | 105 | (defn clone-and-eval [handler] 106 | (fn [{:keys [id op session transport] :as msg}] 107 | (if (= op "clone-eval-close") 108 | (let [*new-session (promise)] 109 | (-> {:id id :op "clone" :session session :transport transport} 110 | (on-send #(do (deliver *new-session (:new-session %)) %)) 111 | (handler)) 112 | (-> msg 113 | (assoc :session @*new-session :op "eval") 114 | (after-send (fn [resp] 115 | (when (and 116 | (= (:id resp) id) 117 | (= (:session resp) @*new-session) 118 | (contains? (:status resp) :done)) 119 | (future ((session/session handler) {:id id :op "close" :session @*new-session :transport transport}))))) 120 | (handler))) 121 | (handler msg)))) 122 | 123 | (middleware/set-descriptor! 124 | #'clone-and-eval 125 | {:requires #{} 126 | :expects #{"eval" "clone"} 127 | :handles {"clone-and-eval" 128 | {:doc "Clones current session, evals given code in the cloned session"}}}) 129 | -------------------------------------------------------------------------------- /cs_printer.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | def safe_get(l, i, default = None): 4 | """ 5 | Like dict.get(), but for lists 6 | """ 7 | if i < len(l): 8 | return l[i] 9 | else: 10 | return default 11 | 12 | def format_map(text, node, indent, limit): 13 | """ 14 | Puts key-value pairs on separate line each. Aligns keys by longest one: 15 | {:a 1 :bbb 2 :cc 3} => {:a 1 16 | :bbb 2 17 | :cc 3} 18 | """ 19 | res = node.open.text 20 | indent_keys = indent + len(node.open.text) * ' ' 21 | keys = [] 22 | vals = [] 23 | if node.body: 24 | idxs = range(0, len(node.body.children), 2) 25 | keys = [node.body.children[i] for i in idxs] 26 | vals = [safe_get(node.body.children, i + 1) for i in idxs] 27 | key_strings = [format(text, k, indent_keys, limit) for k in keys] 28 | longest_key = max(len(ks) for ks in key_strings) if key_strings else 0 29 | indent_vals = indent_keys + longest_key * ' ' + ' ' 30 | for i, ks, v in zip(range(0, len(keys)), key_strings, vals): 31 | if i > 0: 32 | res += '\n' + indent_keys 33 | res += ks 34 | if v is not None: 35 | vs = format(text, v, indent_keys, limit) 36 | if '\n' in vs: 37 | res += '\n' + indent_keys + vs 38 | elif len(indent_keys) + longest_key + 1 + len(vs) <= limit: 39 | res += (longest_key - len(ks)) * ' ' + ' ' + vs 40 | elif len(indent_keys) + len(ks) + 1 + len(vs) <= limit: 41 | res += ' ' + vs 42 | else: 43 | res += '\n' + indent_keys + vs 44 | if node.close: 45 | res += node.close.text 46 | return res 47 | 48 | def format_list(text, node, indent, limit): 49 | """ 50 | Everythin list-like: (...), [...], #{...} 51 | Puts as many children as it can on a line, then starts new one. 52 | """ 53 | indent_children = indent + (len(node.open.text) * ' ') 54 | res = node.open.text 55 | force_newline = False 56 | is_first = True 57 | if node.body: 58 | for i, child in enumerate(node.body.children): 59 | if force_newline: 60 | res += '\n' + indent_children 61 | is_first = True 62 | 63 | child_str = format(text, child, indent_children, limit) 64 | if '\n' in child_str or child.name in {'brackets', 'parens', 'braces'} or len(child_str) > limit / 3: 65 | if not is_first: 66 | res += '\n' + indent_children 67 | res += child_str 68 | force_newline = True 69 | is_first = True 70 | continue 71 | last_line = res[res.rfind('\n') + 1:] 72 | separator = '' if is_first else ' ' 73 | if len(last_line) + len(separator) + len(child_str) > limit: 74 | res += '\n' + indent_children + child_str 75 | else: 76 | res += separator + child_str 77 | force_newline = False 78 | is_first = False 79 | close = node.close.text if node.close else '' 80 | res += close 81 | return res 82 | 83 | def format_tagged(text, node, indent, limit): 84 | """ 85 | #tag 86 | """ 87 | tag_string = format(text, node.tag, indent, limit) 88 | res = '#' + tag_string 89 | if node.body: 90 | value_indent = indent + ' ' + len(tag_string) * ' ' + ' ' 91 | res += ' ' + format(text, node.body.children[0], value_indent, limit) 92 | return res 93 | 94 | def wrap_string(s, limit = 80, indent = ''): 95 | space = limit - len(indent) 96 | length = len(s) 97 | if length <= space: 98 | return s 99 | if space < 10: 100 | return s 101 | res = "" 102 | for start in range(0, length, space): 103 | end = min(start + space, length) 104 | if start > 0: 105 | res += '\n' + indent 106 | res += s[start:end] 107 | return res 108 | 109 | def format(text, node, indent = '', limit = 80): 110 | """ 111 | Given text and its parsed AST as node, returns formatted (pretty-printed) string of that node 112 | """ 113 | if node.name == 'source': 114 | return '\n'.join(format(text, n, '', limit) for n in node.children) 115 | elif node.name == 'braces' and node.open.text != '#{': 116 | return format_map(text, node, indent, limit) 117 | elif node.name in {'parens', 'brackets', 'braces'}: 118 | return format_list(text, node, indent, limit) 119 | elif node.name == 'tagged': 120 | return format_tagged(text, node, indent, limit) 121 | else: 122 | str = text[node.start:node.end] 123 | str = re.sub("(? 0 else None 19 | 20 | def update_region(self): 21 | regions = self.view.get_regions(self.region_key()) 22 | if regions and len(regions) >= 1: 23 | self.region = regions[0] 24 | 25 | def __init__(self, view, region): 26 | self.id = Watch.next_id() 27 | self.view = view 28 | self.region = region 29 | self.values = collections.deque(maxlen = 10) 30 | self.phantom_id = None 31 | watches[self.id] = self 32 | watches_by_view[view.id()][self.id] = self 33 | self.update(recursive = False) 34 | 35 | def __lt__(self, other): 36 | return self.region.begin() < other.region.begin() 37 | 38 | def update(self, value = None, recursive = True): 39 | view = self.view 40 | if value is not None: 41 | self.values.append(value) 42 | scope, color = cs_common.scope_color(self.view, 'watch') 43 | view.erase_regions(self.region_key()) 44 | 45 | line = view.line(self.region) 46 | same_line_watches = list(w for w in watches_by_view[view.id()].values() if view.line(w.region) == line) 47 | same_line_watches.sort() 48 | if same_line_watches[0] == self: 49 | display = " · ".join(cs_common.escape(w.value()) for w in same_line_watches if w.value()) 50 | self.view.add_regions( 51 | key = self.region_key(), 52 | regions = [self.region], 53 | scope = scope, 54 | flags = sublime.DRAW_NO_FILL + sublime.NO_UNDO, 55 | annotations = [display] if display else [], 56 | annotation_color = color if display else '' 57 | ) 58 | else: 59 | self.view.add_regions( 60 | key = self.region_key(), 61 | regions = [self.region], 62 | scope = scope, 63 | flags = sublime.DRAW_NO_FILL + sublime.NO_UNDO 64 | ) 65 | if recursive: 66 | same_line_watches[0].update() 67 | 68 | def erase(self): 69 | self.view.erase_regions(self.region_key()) 70 | if self.phantom_id: 71 | self.view.erase_phantom_by_id(self.phantom_id) 72 | del watches[self.id] 73 | del watches_by_view[self.view.id()][self.id] 74 | 75 | def toggle(self): 76 | if self.value() is None: 77 | return 78 | 79 | if self.phantom_id: 80 | self.view.erase_phantom_by_id(self.phantom_id) 81 | self.phantom_id = None 82 | return 83 | 84 | limit = cs_common.wrap_width(self.view) 85 | 86 | string = "" 87 | for index, value in enumerate(reversed(self.values)): 88 | node = cs_parser.parse(value) 89 | prefix = f" i-{index}" if index > 0 else "last" 90 | string += f"{prefix}: {cs_printer.format(value, node, limit = limit)}\n" 91 | 92 | styles = """ 93 | .light body { background-color: hsl(285, 100%, 90%); } 94 | .dark body { background-color: hsl(285, 100%, 10%); } 95 | """ 96 | if phantom_styles := cs_common.phantom_styles(self.view, "phantom_success"): 97 | styles += f".light body, .dark body {{ { phantom_styles }; border: 4px solid #CC33CC; }}" 98 | 99 | body = f""" 100 | { cs_common.basic_styles(self.view) } 101 | { styles } 102 | """ 103 | 104 | for line in string.splitlines(): 105 | line = cs_printer.wrap_string(line, limit = limit) 106 | line = cs_common.escape(line) 107 | body += "

" + line + "

" 108 | body += "" 109 | point = self.view.line(self.region.end()).begin() 110 | self.phantom_id = self.view.add_phantom( 111 | key = self.region_key(), 112 | region = sublime.Region(point, point), 113 | content = body, 114 | layout = sublime.LAYOUT_BLOCK 115 | ) 116 | 117 | def on_watch(id, value): 118 | if w := watches.get(id): 119 | w.update(value) 120 | 121 | def erase_watches(predicate = lambda x: True, view = None): 122 | if view: 123 | to_erase = list(w for w in watches_by_view[view.id()].values() if predicate(w)) 124 | else: 125 | to_erase = list(w for w in watches.values() if predicate(w)) 126 | for w in to_erase: 127 | w.erase() 128 | 129 | def by_region(view, region): 130 | for w in watches_by_view[view.id()].values(): 131 | if cs_common.regions_touch(w.region, region): 132 | return w 133 | 134 | def transform(view): 135 | def transform_impl(code, **kwargs): 136 | region = kwargs['eval_region'] 137 | watches = list(w for w in watches_by_view[view.id()].values() if region.contains(w.region)) 138 | watches.sort() 139 | pos = region.begin() 140 | res = '' 141 | for w in watches: 142 | w_region = w.region 143 | res += view.substr(sublime.Region(pos, w_region.begin())) 144 | res += "(clojure-sublimed.socket-repl/watch " + str(w.id) + " " 145 | res += view.substr(w_region) 146 | res += ")" 147 | pos = w_region.end() 148 | res += view.substr(sublime.Region(pos, region.end())) 149 | return res 150 | return transform_impl 151 | 152 | class ClojureSublimedAddWatchCommand(sublime_plugin.TextCommand): 153 | def run(self, edit): 154 | view = self.view 155 | window = view.window() 156 | sel = view.sel()[0] 157 | if ws := list(w for w in watches_by_view[view.id()].values() if w.region == sel): 158 | ws[0].erase() 159 | else: 160 | erase_watches(lambda w: w.region.intersects(sel), view) 161 | top = cs_parser.topmost_form(view, sel.begin()) 162 | state = cs_common.get_state(window) 163 | watch = Watch(view, sel) 164 | state.conn.eval(view, [top]) 165 | 166 | def is_enabled(self): 167 | view = self.view 168 | window = view.window() 169 | sel = view.sel()[0] 170 | state = cs_common.get_state(window) 171 | return bool(state.conn \ 172 | and state.conn.ready() \ 173 | and type(state.conn).__name__ == 'ConnectionSocketRepl' \ 174 | and len(view.sel()) == 1 \ 175 | and not sel.empty()) 176 | 177 | class EventListener(sublime_plugin.EventListener): 178 | def on_pre_close(self, view): 179 | erase_watches(view = view) 180 | 181 | class TextChangeListener(sublime_plugin.TextChangeListener): 182 | def on_text_changed_async(self, changes): 183 | view = self.buffer.primary_view() 184 | changed = [sublime.Region(x.a.pt, x.b.pt) for x in changes] 185 | 186 | def should_erase(w): 187 | return any(w.region.intersects(r) for r in changed) 188 | erase_watches(should_erase, view) 189 | 190 | lines = list(view.line(r) for r in changed) 191 | def should_update(w): 192 | return any(r.contains(w.region.begin()) for r in lines) 193 | need_update = list(w for w in watches_by_view[view.id()].values() if should_update(w)) 194 | 195 | for w in watches_by_view[view.id()].values(): 196 | w.update_region() 197 | 198 | for w in need_update: 199 | w.update(recursive = False) 200 | 201 | def plugin_unloaded(): 202 | erase_watches() 203 | -------------------------------------------------------------------------------- /Clojure Sublimed Dark.sublime-color-scheme: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Clojure Sublimed Dark", 3 | "author": "Nikita Prokopov", 4 | "variables": 5 | { 6 | "active": "#00BFFF", 7 | "fg": "#CECECE", 8 | "bg": "#0E1415", 9 | "blue": "#71ADE7", 10 | "green": "#95CB82", 11 | "green-bg": "#203028", 12 | "red": "#FF6060", 13 | "red-bg": "#2B1D1E", 14 | "magenta": "#CC8BC9", 15 | "yellow": "#FFF080", 16 | // "orange": "#FFBC5D", 17 | "gray": "#606060", 18 | }, 19 | "globals": 20 | { 21 | "foreground": "var(fg)", 22 | "background": "var(bg)", 23 | "caret": "var(active)", 24 | "line_highlight": "#ffffff10", 25 | "misspelling": "#ff0000", 26 | "selection": "#293334", 27 | "inactive_selection": "#ffffff10", 28 | "selection_border_width": "0", 29 | "selection_corner_radius": "2", 30 | "highlight": "var(active)", 31 | "find_highlight_foreground": "#000", 32 | "find_highlight": "var(active)", 33 | "brackets_options": "underline", 34 | "brackets_foreground": "var(active)", 35 | "bracket_contents_options": "underline", 36 | "bracket_contents_foreground": "var(active)", 37 | "tags_options": "underline", 38 | "tags_foreground": "var(active)", 39 | "gutter": "#121819", 40 | "gutter_foreground": "#282828", 41 | "gutter_foreground_active": "#282828", 42 | }, 43 | "rules": 44 | [ 45 | {"name": "Strings", 46 | "scope": "string - meta.metadata, meta.quoted string - meta.metadata - comment", 47 | "foreground": "var(green)"}, 48 | 49 | {"name": "Escapes", 50 | "scope": "constant.character.escape - meta.metadata, constant.other.placeholder - meta.metadata", 51 | "background": "var(green-bg)"}, 52 | 53 | {"name": "Constants", 54 | "scope": "constant - constant.character.escape, punctuation.definition.constant, support.type, source.sql keyword", 55 | "foreground": "var(magenta)"}, 56 | 57 | {"name": "Definitions", 58 | "scope": "entity.name - entity.name.tag - meta.metadata", 59 | "foreground": "var(blue)"}, 60 | 61 | {"name": "Symbol namespaces", 62 | "scope": "meta.namespace.symbol, source.symbol punctuation.definition.namespace", 63 | "foreground": "var(gray)"}, 64 | 65 | {"name": "Unused symbol", 66 | "scope": "source.symbol.unused", 67 | "foreground": "var(gray)"}, 68 | 69 | {"name": "Punctuation", 70 | "scope": "punctuation - punctuation.section - punctuation.definition - punctuation.accessor", 71 | "foreground": "var(gray)"}, 72 | 73 | {"name": "Comma", 74 | "scope": "punctuation.definition.comma", 75 | "foreground": "var(gray)"}, 76 | 77 | {"name": "Line Comments", 78 | "scope": "comment - comment.reader - comment.form, invalid comment, meta.quoted comment.line, meta.quoted comment.line punctuation.definition.comment, meta.metadata comment.line punctuation.definition.comment", 79 | "foreground": "var(yellow)" 80 | }, 81 | 82 | {"name": "Reader comments", 83 | "scope": "comment.reader, comment.reader keyword.operator, comment.reader string, comment.reader constant, comment.reader punctuation.definition.constant, comment.reader punctuation, comment.reader constant.character.escape, comment.reader invalid.illegal.escape, comment.reader string invalid, comment.reader string invalid punctuation, comment.reader entity.name, comment.reader meta.quoted", 84 | "foreground": "var(gray)", 85 | "background": "var(bg)"}, 86 | 87 | {"name": "Form comments", 88 | "scope": "comment.form, comment.form keyword.operator, comment.form string, comment.form constant, comment.form punctuation.definition.constant, comment.form punctuation, comment.form constant.character.escape, comment.form invalid.illegal.escape, comment.form string invalid, comment.form string invalid punctuation, comment.form entity.name, comment.form meta.quoted", 89 | "foreground": "var(gray)", 90 | "background": "var(bg)"}, 91 | 92 | {"name": "Metadata", 93 | "scope": "meta.metadata, meta.metadata keyword.operator, meta.metadata string, meta.metadata constant, meta.metadata punctuation.definition.constant, meta.metadata punctuation", 94 | "foreground": "var(gray)"}, 95 | 96 | {"scope": "meta.metadata meta.quoted", 97 | "background": "var(bg)"}, 98 | 99 | {"name": "Quoted", 100 | "scope": "meta.quoted - meta.quoted meta.unquoted, meta.quoted meta.unquoted meta.quoted - meta.quoted meta.unquoted meta.quoted meta.unquoted, meta.quoted meta.unquoted meta.quoted meta.unquoted meta.quoted - meta.quoted meta.unquoted meta.quoted meta.unquoted meta.quoted meta.unquoted, meta.quoted comment.reader, meta.quoted punctuation.definition.comment, meta.quoted comment.form, meta.quoted comment.form punctuation", 101 | "background": "#FFFFFF10"}, 102 | 103 | {"name": "Inner brackets", 104 | "scope": "meta.parens meta.parens punctuation.section, meta.parens meta.brackets punctuation.section, meta.parens meta.braces punctuation.section, meta.brackets meta.parens punctuation.section, meta.brackets meta.brackets punctuation.section, meta.brackets meta.braces punctuation.section, meta.braces meta.parens punctuation.section, meta.braces meta.brackets punctuation.section, meta.braces meta.braces punctuation.section", 105 | "foreground": "var(gray)"}, 106 | 107 | {"name": "Mistakes", 108 | "scope": "invalid, invalid string, invalid constant, invalid entity.name, invalid punctuation, invalid source.symbol", 109 | "foreground": "var(red)", 110 | "background": "var(red-bg)"}, 111 | 112 | // MARKUP 113 | 114 | {"scope": "markup.inserted", 115 | "foreground": "var(green)"}, 116 | 117 | {"scope": "markup.deleted", 118 | "foreground": "hsl(2, 65%, 50%)"}, 119 | 120 | {"scope": "markup.changed", 121 | "foreground": "hsl(30, 85%, 50%)"}, 122 | 123 | {"scope": "markup.ignored", 124 | "foreground": "#aaa"}, 125 | 126 | {"scope": "markup.untracked", 127 | "foreground": "#aaa"}, 128 | 129 | // REGIONS 130 | 131 | {"scope": "region.eval.success", 132 | "foreground": "var(green)"}, 133 | 134 | {"scope": "region.eval.exception", 135 | "foreground": "var(red)"}, 136 | 137 | {"scope": "region.eval.pending", 138 | "foreground": "var(gray)"}, 139 | 140 | {"scope": "region.watch", 141 | "foreground": "var(magenta)"}, 142 | 143 | {"scope": "region.redish", 144 | "background": "#F04F5080"}, 145 | 146 | {"scope": "region.orangish", 147 | "background": "#FF935680"}, 148 | 149 | {"scope": "region.yellowish", 150 | "background": "#FFBC5D80"}, 151 | 152 | {"scope": "region.greenish", 153 | "background": "#60CB0080"}, 154 | 155 | {"scope": "region.cyanish", 156 | "background": "#00AACB80"}, 157 | 158 | {"scope": "region.bluish", 159 | "background": "#017ACC80"}, 160 | 161 | {"scope": "region.purplish", 162 | "background": "#C171FF80"}, 163 | 164 | {"scope": "region.pinkish", 165 | "background": "#E64CE680"}, 166 | 167 | {"scope": "region.greyish", 168 | "background": "#FFFFFF10"}, 169 | 170 | // {"scope": "region.eval.lookup", 171 | // "foreground": "hsl(208, 100%, 50%)"}, 172 | ] 173 | } 174 | -------------------------------------------------------------------------------- /cs_conn_socket_repl.py: -------------------------------------------------------------------------------- 1 | import json, os, re, sublime, sublime_plugin, threading 2 | from . import cs_common, cs_conn, cs_eval, cs_eval_status, cs_parser, cs_warn, cs_watch 3 | 4 | def lines(socket): 5 | buffer = b'' 6 | while True: 7 | more = socket.recv(4096) 8 | if more: 9 | buffer += more 10 | while b'\n' in buffer: 11 | (line, buffer) = buffer.split(b'\n', 1) 12 | yield line.decode() 13 | if not more: 14 | break 15 | if buffer: 16 | yield buffer.decode() 17 | 18 | def escape(s): 19 | return s.replace('\\', '\\\\').replace('"', '\\"') 20 | 21 | class ConnectionSocketRepl(cs_conn.Connection): 22 | """ 23 | Upgraded Socket REPL: does what nREPL JVM does, but without extra dependencies 24 | """ 25 | def __init__(self, addr): 26 | super().__init__() 27 | self.addr = addr 28 | self.socket = None 29 | self.reader = None 30 | self.closing = False 31 | 32 | def connect_impl(self): 33 | self.set_status(0, 'Connecting to {}', self.get_addr()) 34 | self.socket = cs_common.socket_connect(self.get_addr()) 35 | self.reader = threading.Thread(daemon=True, target=self.read_loop) 36 | self.reader.start() 37 | 38 | def disconnect_impl(self): 39 | cs_watch.erase_watches(lambda w: w.view.window() == self.window) 40 | if self.socket: 41 | self.socket.close() 42 | self.socket = None 43 | 44 | def read_loop(self): 45 | try: 46 | self.set_status(1, 'Upgrading REPL') 47 | self.send(cs_common.clojure_source('core.clj')) 48 | self.send(cs_common.clojure_source('socket_repl.clj')) 49 | if shared := cs_common.setting('eval_shared'): 50 | self.send(shared) 51 | self.send("(repl)\n") 52 | started = False 53 | for line in lines(self.socket): 54 | cs_common.debug('RCV {}', line) 55 | if started: 56 | msg = cs_parser.parse_as_dict(line) 57 | self.handle_msg(msg) 58 | else: 59 | if '{"tag" "started"}' in line: 60 | self.set_status(4, self.get_addr()) 61 | started = True 62 | except OSError: 63 | pass 64 | self.disconnect() 65 | 66 | def send(self, msg): 67 | cs_common.debug('SND {}', msg) 68 | self.socket.sendall(msg.encode()) 69 | 70 | def eval_impl(self, form): 71 | msg = f'{{' + \ 72 | f'"id" {form.id}, ' + \ 73 | f'"op" "eval", ' + \ 74 | f'"ns" "{form.ns}", ' 75 | 76 | if form.print_quota is not None: 77 | msg += f'"print_quota" {form.print_quota}, ' 78 | 79 | msg += f'"code" "{escape(form.code)}"' 80 | 81 | if form.file: 82 | msg += f', "file" "{escape(form.file)}"' 83 | 84 | if form.line is not None: 85 | msg += f', "line" {form.line}' 86 | 87 | if form.column is not None: 88 | msg += f', "column" {form.column}' 89 | 90 | msg += f'}}' 91 | self.send(msg) 92 | 93 | def eval(self, view, sel, transform_fn = None, print_quota = None, on_finish = None): 94 | cs_warn.reset_warnings(self.window) 95 | for selected_region in sel: 96 | # find regions to eval 97 | eval_region = self.eval_region(selected_region, view) 98 | 99 | # extracting code 100 | transform_fn = transform_fn or cs_watch.transform(view) 101 | (code, ns, forms) = self.code(view, selected_region, eval_region, transform_fn) 102 | 103 | # create evals 104 | start = eval_region.begin() 105 | batch_id = cs_eval.Eval.next_id() 106 | for idx, form in enumerate(forms): 107 | region = sublime.Region(start + form.start, start + form.end) 108 | eval = cs_eval.Eval(view, region, id = f'{batch_id}.{idx}', batch_id = batch_id, on_finish=on_finish) 109 | 110 | # send msg 111 | (line, column) = view.rowcol_utf16(eval_region.begin()) 112 | form = cs_common.Form( 113 | id = batch_id, 114 | code = code, 115 | ns = ns, 116 | line = line + 1, 117 | column = column, 118 | file = view.file_name(), 119 | print_quota = print_quota if print_quota is not None else cs_common.setting('print_quota') 120 | ) 121 | self.eval_impl(form) 122 | 123 | def eval_status(self, code, ns): 124 | cs_warn.reset_warnings(self.window) 125 | batch_id = cs_eval.Eval.next_id() 126 | eval = cs_eval_status.StatusEval(code, id = f'{batch_id}.0', batch_id = batch_id) 127 | form = cs_common.Form(id = batch_id, code = code, ns = ns) 128 | self.eval_impl(form) 129 | 130 | def load_file(self, view): 131 | self.eval(view, [sublime.Region(0, view.size())]) 132 | 133 | def lookup_impl(self, id, symbol, ns): 134 | msg = f'{{"id" {id}, "op" "lookup", "symbol" "{symbol}", "ns" "{ns}"}}' 135 | self.send(msg) 136 | 137 | def interrupt_impl(self, batch_id, id): 138 | msg = f'{{"id" {batch_id}, "op" "interrupt"}}' 139 | self.send(msg) 140 | 141 | def handle_value(self, msg): 142 | if 'ret' == msg['tag']: 143 | id = msg.get('id') 144 | idx = msg.get('idx') 145 | val = msg.get('val') 146 | time = msg.get('time') 147 | cs_eval.on_success(f'{id}.{idx}', val, time = time) 148 | return True 149 | 150 | def handle_watch(self, msg): 151 | if 'watch' == msg['tag']: 152 | id = msg.get('watch_id') 153 | val = msg.get('val') 154 | cs_watch.on_watch(id, val) 155 | return True 156 | 157 | def handle_exception(self, msg): 158 | if 'ex' == msg['tag']: 159 | id = msg.get('id') 160 | idx = msg.get('idx') 161 | val = msg.get('val') 162 | source = msg.get('source') 163 | line = msg.get('line') 164 | column = msg.get('column') 165 | trace = msg.get('trace') 166 | eval_id = f'{id}.{idx}' if idx is not None else id 167 | cs_eval.on_exception(eval_id, val, source = source, line = line, column = column, trace = trace) 168 | return True 169 | 170 | def handle_done(self, msg): 171 | if 'done' == msg['tag']: 172 | batch_id = msg.get('id') 173 | cs_eval.on_done(batch_id) 174 | return True 175 | 176 | def handle_lookup(self, msg): 177 | if 'lookup' == msg['tag']: 178 | id = msg.get('id') 179 | val = cs_parser.parse_as_dict(msg['val']) 180 | cs_eval.on_lookup(id, val) 181 | return True 182 | 183 | def handle_err(self, msg): 184 | if 'err' == msg['tag']: 185 | if msg['val'].startswith("Reflection warning"): 186 | cs_warn.add_warning(self.window) 187 | return True 188 | 189 | def handle_msg(self, msg): 190 | # cs_common.debug('MSG {}', msg) 191 | self.handle_value(msg) \ 192 | or self.handle_exception(msg) \ 193 | or self.handle_done(msg) \ 194 | or self.handle_watch(msg) \ 195 | or self.handle_lookup(msg) \ 196 | or self.handle_err(msg) 197 | 198 | class ClojureSublimedConnectSocketReplCommand(sublime_plugin.WindowCommand): 199 | def run(self, address, timeout = 0): 200 | state = cs_common.get_state(self.window) 201 | state.last_conn = ('clojure_sublimed_connect_socket_repl', {'address': address}) 202 | if address == 'auto': 203 | address = lambda: self.input({}).initial_text() 204 | ConnectionSocketRepl(address).try_connect(timeout = timeout) 205 | 206 | def input(self, args): 207 | if 'address' not in args: 208 | return cs_conn.AddressInputHandler(port_files = ['.repl-port', '.shadow-cljs/socket-repl.port']) 209 | 210 | def is_enabled(self): 211 | state = cs_common.get_state(self.window) 212 | return state.conn is None 213 | -------------------------------------------------------------------------------- /test_indent/indent.txt: -------------------------------------------------------------------------------- 1 | ================================================================================ 2 | Empty list 3 | ================================================================================ 4 | 5 | ( 6 | ) 7 | ( 8 | ) 9 | 10 | -------------------------------------------------------------------------------- 11 | 12 | ( 13 | ) 14 | ( 15 | ) 16 | 17 | ================================================================================ 18 | List starting with symbol 19 | ================================================================================ 20 | 21 | (abc 22 | ) 23 | (abc 24 | def) 25 | (abc def 26 | ghi) 27 | 28 | -------------------------------------------------------------------------------- 29 | 30 | (abc 31 | ) 32 | (abc 33 | def) 34 | (abc def 35 | ghi) 36 | 37 | ================================================================================ 38 | List starting with number 39 | ================================================================================ 40 | 41 | (123 42 | ) 43 | (123 44 | 456) 45 | '(123 456 46 | 789) 47 | '(1 2 3 4 48 | 5 6 7 8) 49 | 50 | -------------------------------------------------------------------------------- 51 | 52 | (123 53 | ) 54 | (123 55 | 456) 56 | '(123 456 57 | 789) 58 | '(1 2 3 4 59 | 5 6 7 8) 60 | 61 | ================================================================================ 62 | List starting with keyword 63 | ================================================================================ 64 | 65 | (:abc 66 | ) 67 | (:abc 68 | :def) 69 | (:abc :def 70 | :ghi) 71 | 72 | -------------------------------------------------------------------------------- 73 | 74 | (:abc 75 | ) 76 | (:abc 77 | :def) 78 | (:abc :def 79 | :ghi) 80 | 81 | ================================================================================ 82 | Many args 83 | ================================================================================ 84 | 85 | (defn many-args [a b c 86 | d e f]) 87 | 88 | -------------------------------------------------------------------------------- 89 | 90 | (defn many-args [a b c 91 | d e f]) 92 | 93 | ================================================================================ 94 | Multi-arity 95 | ================================================================================ 96 | 97 | (defn multi-arity 98 | ([x] 99 | body) 100 | ([x y] 101 | body)) 102 | 103 | -------------------------------------------------------------------------------- 104 | 105 | (defn multi-arity 106 | ([x] 107 | body) 108 | ([x y] 109 | body)) 110 | 111 | ================================================================================ 112 | Vectors 113 | ================================================================================ 114 | 115 | [] 116 | [ 117 | ] 118 | [a] 119 | [a 120 | ] 121 | [ 122 | a] 123 | [a b] 124 | [a b 125 | ] 126 | [a 127 | b 128 | ] 129 | [ 130 | a 131 | b 132 | ] 133 | [a b c] 134 | [a b c 135 | ] 136 | [a b 137 | c 138 | ] 139 | [a 140 | b 141 | c 142 | ] 143 | [ 144 | a 145 | b 146 | c 147 | ] 148 | 149 | -------------------------------------------------------------------------------- 150 | 151 | [] 152 | [ 153 | ] 154 | [a] 155 | [a 156 | ] 157 | [ 158 | a] 159 | [a b] 160 | [a b 161 | ] 162 | [a 163 | b 164 | ] 165 | [ 166 | a 167 | b 168 | ] 169 | [a b c] 170 | [a b c 171 | ] 172 | [a b 173 | c 174 | ] 175 | [a 176 | b 177 | c 178 | ] 179 | [ 180 | a 181 | b 182 | c 183 | ] 184 | 185 | ================================================================================ 186 | Maps 187 | ================================================================================ 188 | 189 | {} 190 | { 191 | } 192 | {a b} 193 | {a b 194 | } 195 | {a 196 | b 197 | } 198 | { 199 | a 200 | b 201 | } 202 | {a b c d} 203 | {a b c d 204 | } 205 | {a b c 206 | d} 207 | {a b 208 | c d 209 | } 210 | {a 211 | b c d} 212 | {a 213 | b c 214 | d} 215 | 216 | -------------------------------------------------------------------------------- 217 | 218 | {} 219 | { 220 | } 221 | {a b} 222 | {a b 223 | } 224 | {a 225 | b 226 | } 227 | { 228 | a 229 | b 230 | } 231 | {a b c d} 232 | {a b c d 233 | } 234 | {a b c 235 | d} 236 | {a b 237 | c d 238 | } 239 | {a 240 | b c d} 241 | {a 242 | b c 243 | d} 244 | 245 | ================================================================================ 246 | Sets 247 | ================================================================================ 248 | 249 | #{} 250 | #{ 251 | } 252 | #{a} 253 | #{a 254 | } 255 | #{ 256 | a} 257 | #{a b} 258 | #{a b 259 | } 260 | #{a 261 | b 262 | } 263 | #{ 264 | a 265 | b 266 | } 267 | #{a b c} 268 | #{a b c 269 | } 270 | #{a b 271 | c 272 | } 273 | #{a 274 | b 275 | c 276 | } 277 | #{ 278 | a 279 | b 280 | c 281 | } 282 | 283 | -------------------------------------------------------------------------------- 284 | 285 | #{} 286 | #{ 287 | } 288 | #{a} 289 | #{a 290 | } 291 | #{ 292 | a} 293 | #{a b} 294 | #{a b 295 | } 296 | #{a 297 | b 298 | } 299 | #{ 300 | a 301 | b 302 | } 303 | #{a b c} 304 | #{a b c 305 | } 306 | #{a b 307 | c 308 | } 309 | #{a 310 | b 311 | c 312 | } 313 | #{ 314 | a 315 | b 316 | c 317 | } 318 | 319 | ================================================================================ 320 | Reader conditional 321 | ================================================================================ 322 | 323 | #?(:clj 1 324 | :cljs 2) 325 | 326 | #?@(:clj [1] 327 | :cljs [2]) 328 | 329 | -------------------------------------------------------------------------------- 330 | 331 | #?(:clj 1 332 | :cljs 2) 333 | 334 | #?@(:clj [1] 335 | :cljs [2]) 336 | 337 | ================================================================================ 338 | Multiline strings 339 | ================================================================================ 340 | 341 | "asdasd 342 | asdas 343 | aksjdlkj 344 | lkjdlk" 345 | (def x 346 | "asdasd 347 | asdas 348 | aksjdlkj 349 | lkjdlk") 350 | (def y [ 351 | "asdas" 352 | "adasd" 353 | "adasd" 354 | ]) 355 | 356 | -------------------------------------------------------------------------------- 357 | 358 | "asdasd 359 | asdas 360 | aksjdlkj 361 | lkjdlk" 362 | (def x 363 | "asdasd 364 | asdas 365 | aksjdlkj 366 | lkjdlk") 367 | (def y [ 368 | "asdas" 369 | "adasd" 370 | "adasd" 371 | ]) 372 | 373 | ================================================================================ 374 | Requires 375 | ================================================================================ 376 | 377 | (ns abc 378 | (:requires 379 | [a.b.c :as c] 380 | [d.e.f :as f])) 381 | (ns abc 382 | (:requires [a.b.c :as c] 383 | [d.e.f :as f])) 384 | 385 | -------------------------------------------------------------------------------- 386 | 387 | (ns abc 388 | (:requires 389 | [a.b.c :as c] 390 | [d.e.f :as f])) 391 | (ns abc 392 | (:requires [a.b.c :as c] 393 | [d.e.f :as f])) 394 | 395 | ================================================================================ 396 | Or 397 | ================================================================================ 398 | 399 | (or cond-1 cond-2) 400 | (or cond-1 401 | cond-2) 402 | (or 403 | cond-1 404 | cond-2) 405 | 406 | -------------------------------------------------------------------------------- 407 | 408 | (or cond-1 cond-2) 409 | (or cond-1 410 | cond-2) 411 | (or 412 | cond-1 413 | cond-2) 414 | 415 | ================================================================================ 416 | Threading 417 | ================================================================================ 418 | 419 | (-> a b c) 420 | (-> a b 421 | c) 422 | (-> a 423 | b 424 | c) 425 | (-> 426 | a 427 | b 428 | c) 429 | 430 | -------------------------------------------------------------------------------- 431 | 432 | (-> a b c) 433 | (-> a b 434 | c) 435 | (-> a 436 | b 437 | c) 438 | (-> 439 | a 440 | b 441 | c) 442 | 443 | ================================================================================ 444 | Threading 445 | ================================================================================ 446 | 447 | (do a b c) 448 | (do a b 449 | c) 450 | (do a 451 | b 452 | c) 453 | (do 454 | a 455 | b 456 | c) 457 | 458 | -------------------------------------------------------------------------------- 459 | 460 | (do a b c) 461 | (do a b 462 | c) 463 | (do a 464 | b 465 | c) 466 | (do 467 | a 468 | b 469 | c) 470 | 471 | ================================================================================ 472 | Everything 473 | ================================================================================ 474 | 475 | (letfn [(square [x] 476 | (* x x)) 477 | (sum [x y] 478 | (+ x y))] 479 | (let [x 3 480 | y 4] 481 | (sum (square x) 482 | (square y)))) 483 | 484 | -------------------------------------------------------------------------------- 485 | 486 | (letfn [(square [x] 487 | (* x x)) 488 | (sum [x y] 489 | (+ x y))] 490 | (let [x 3 491 | y 4] 492 | (sum (square x) 493 | (square y)))) 494 | -------------------------------------------------------------------------------- /Clojure Sublimed Light.sublime-color-scheme: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Clojure Sublimed Light", 3 | "author": "Nikita Prokopov", 4 | "variables": 5 | { 6 | "active": "#43bef4", 7 | "fg": "#000", 8 | "bg": "#fff", 9 | "gray": "#A0A0A0", 10 | }, 11 | "globals": 12 | { 13 | "foreground": "var(fg)", 14 | "background": "var(bg)", 15 | "caret": "var(active)", 16 | "line_highlight": "#00000008", 17 | "misspelling": "#f00", 18 | "selection": "#B4D8FD", 19 | "inactive_selection": "#E0E0E0", 20 | "selection_border_width": "0", 21 | "selection_corner_radius": "2", 22 | "highlight": "#FFBC5D", 23 | "find_highlight": "#FFBC5D", 24 | "find_highlight_foreground": "#000", 25 | "brackets_options": "underline", 26 | "brackets_foreground": "var(active)", 27 | "bracket_contents_options": "underline", 28 | "bracket_contents_foreground": "var(active)", 29 | "tags_options": "underline", 30 | "tags_foreground": "var(active)", 31 | // "gutter": "#E6E6E6", 32 | "gutter_foreground": "#CCC", 33 | "gutter_foreground_highlight": "#CCC", 34 | }, 35 | "rules": 36 | [ {"name": "Strings", 37 | "scope": "string - meta.metadata, meta.quoted string - meta.metadata - comment", 38 | "background": "#eefbd9", 39 | "foreground": "#3c5c00"}, 40 | 41 | {"name": "Escapes", 42 | "scope": "constant.character.escape - meta.metadata, constant.other.placeholder - meta.metadata", 43 | "background": "#DBECB6"}, 44 | 45 | {"name": "Constants", 46 | "scope": "constant - constant.character.escape, punctuation.definition.constant, support.type, source.sql keyword", 47 | "foreground": "#8a3eb5"}, 48 | 49 | {"name": "Definitions", 50 | "scope": "entity.name - entity.name.tag - meta.metadata", 51 | "background": "#DBF1FF", 52 | "foreground": "#195b7c"}, 53 | 54 | {"name": "Symbol namespaces", 55 | "scope": "meta.namespace.symbol, source.symbol punctuation.definition.namespace", 56 | "foreground": "var(gray)"}, 57 | 58 | {"name": "Unused symbol", 59 | "scope": "source.symbol.unused", 60 | "foreground": "var(gray)"}, 61 | 62 | {"name": "Punctuation", 63 | "scope": "punctuation - punctuation.section - punctuation.definition - punctuation.accessor", 64 | "foreground": "var(gray)"}, 65 | 66 | {"name": "Comma", 67 | "scope": "punctuation.definition.comma", 68 | "foreground": "var(gray)"}, 69 | 70 | {"name": "Line Comments", 71 | "scope": "comment - comment.reader - comment.form, invalid comment, meta.quoted comment.line, meta.quoted comment.line punctuation.definition.comment, meta.metadata comment.line punctuation.definition.comment", 72 | "foreground": "#6d6607", 73 | "background": "#FFFABC"}, 74 | 75 | {"name": "Reader comments", 76 | "scope": "comment.reader, comment.reader keyword.operator, comment.reader string, comment.reader constant, comment.reader punctuation.definition.constant, comment.reader punctuation, comment.reader constant.character.escape, comment.reader invalid.illegal.escape, comment.reader string invalid, comment.reader string invalid punctuation, comment.reader entity.name, comment.reader meta.quoted", 77 | "foreground": "var(gray)", 78 | "background": "var(bg)"}, 79 | 80 | {"name": "Form comments", 81 | "scope": "comment.form, comment.form keyword.operator, comment.form string, comment.form constant, comment.form punctuation.definition.constant, comment.form punctuation, comment.form constant.character.escape, comment.form invalid.illegal.escape, comment.form string invalid, comment.form string invalid punctuation, comment.form entity.name, comment.form meta.quoted", 82 | "foreground": "var(gray)", 83 | "background": "var(bg)"}, 84 | 85 | {"name": "Metadata", 86 | "scope": "meta.metadata, meta.metadata keyword.operator, meta.metadata string, meta.metadata constant, meta.metadata punctuation.definition.constant, meta.metadata punctuation", 87 | "foreground": "var(gray)"}, 88 | 89 | {"scope": "meta.metadata meta.quoted", 90 | "background": "var(bg)"}, 91 | 92 | {"name": "Quoted", 93 | "scope": "meta.quoted - meta.quoted meta.unquoted, meta.quoted meta.unquoted meta.quoted - meta.quoted meta.unquoted meta.quoted meta.unquoted, meta.quoted meta.unquoted meta.quoted meta.unquoted meta.quoted - meta.quoted meta.unquoted meta.quoted meta.unquoted meta.quoted meta.unquoted, meta.quoted comment.reader, meta.quoted punctuation.definition.comment, meta.quoted comment.form, meta.quoted comment.form punctuation", 94 | "background": "#00000010"}, 95 | 96 | {"name": "JSX", 97 | "scope": "meta.jsx - meta.jsx source.js.embedded, meta.jsx source.js.embedded meta.jsx - meta.jsx source.js.embedded meta.jsx source.js.embedded, meta.jsx source.js.embedded meta.jsx source.js.embedded meta.jsx - meta.jsx source.js.embedded meta.jsx source.js.embedded meta.jsx source.js.embedded", 98 | "background": "#00000010"}, 99 | 100 | {"name": "Inner brackets", 101 | "scope": "meta.parens meta.parens punctuation.section, meta.parens meta.brackets punctuation.section, meta.parens meta.braces punctuation.section, meta.brackets meta.parens punctuation.section, meta.brackets meta.brackets punctuation.section, meta.brackets meta.braces punctuation.section, meta.braces meta.parens punctuation.section, meta.braces meta.brackets punctuation.section, meta.braces meta.braces punctuation.section", 102 | "foreground": "var(gray)"}, 103 | 104 | // {"name": "Parens level 0", 105 | // "scope": "meta.parens punctuation.section", 106 | // "foreground": "hsl(0, 50%, 50%)"}, 107 | 108 | // {"name": "Parens level 1", 109 | // "scope": "meta.parens meta.parens punctuation.section", 110 | // "foreground": "hsl(60, 50%, 50%)"}, 111 | 112 | // {"name": "Parens level 2", 113 | // "scope": "meta.parens meta.parens meta.parens punctuation.section", 114 | // "foreground": "hsl(120, 50%, 50%)"}, 115 | 116 | // {"name": "Parens level 3", 117 | // "scope": "meta.parens meta.parens meta.parens meta.parens punctuation.section", 118 | // "foreground": "hsl(180, 50%, 50%)"}, 119 | 120 | // {"name": "Parens level 4", 121 | // "scope": "meta.parens meta.parens meta.parens meta.parens meta.parens punctuation.section", 122 | // "foreground": "hsl(240, 50%, 50%)"}, 123 | 124 | // {"name": "Parens level 5", 125 | // "scope": "meta.parens meta.parens meta.parens meta.parens meta.parens meta.parens punctuation.section", 126 | // "foreground": "hsl(300, 50%, 50%)"}, 127 | 128 | {"name": "Mistakes", 129 | "scope": "invalid, invalid string, invalid constant, invalid entity.name, invalid punctuation, invalid source.symbol", 130 | "foreground": "#c33", 131 | "background": "#FFE0E0"}, 132 | 133 | // MARKUP 134 | 135 | {"scope": "markup.inserted", 136 | "foreground": "hsl(100, 50%, 50%)"}, 137 | 138 | {"scope": "markup.deleted", 139 | "foreground": "hsl(2, 65%, 50%)"}, 140 | 141 | {"scope": "markup.changed", 142 | "foreground": "hsl(30, 85%, 50%)"}, 143 | 144 | {"scope": "markup.ignored", 145 | "foreground": "#aaa"}, 146 | 147 | {"scope": "markup.untracked", 148 | "foreground": "#aaa"}, 149 | 150 | // REGION 151 | 152 | {"scope": "region.eval.success", 153 | "foreground": "hsl(100, 50%, 50%)"}, 154 | 155 | {"scope": "region.eval.exception", 156 | "foreground": "hsl(2, 65%, 50%)"}, 157 | 158 | {"scope": "region.eval.pending", 159 | "foreground": "#CCCCCC"}, 160 | 161 | {"scope": "region.watch", 162 | "foreground": "hsl(285, 50%, 50%)"}, 163 | 164 | {"scope": "region.redish", 165 | "background": "#F04F5080"}, 166 | 167 | {"scope": "region.orangish", 168 | "background": "#FF935680"}, 169 | 170 | {"scope": "region.yellowish", 171 | "background": "#FFBC5D80"}, 172 | 173 | {"scope": "region.greenish", 174 | "background": "#60CB0080"}, 175 | 176 | {"scope": "region.cyanish", 177 | "background": "#00AACB80"}, 178 | 179 | {"scope": "region.bluish", 180 | "background": "#017ACC80"}, 181 | 182 | {"scope": "region.purplish", 183 | "background": "#C171FF80"}, 184 | 185 | {"scope": "region.pinkish", 186 | "background": "#E64CE680"}, 187 | 188 | {"scope": "region.greyish", 189 | "background": "#00000010"}, 190 | 191 | // {"scope": "region.eval.lookup", 192 | // "foreground": "hsl(208, 100%, 50%)"}, 193 | ] 194 | } -------------------------------------------------------------------------------- /cs_conn.py: -------------------------------------------------------------------------------- 1 | import os, re, stat, sublime, sublime_plugin, threading, time 2 | from . import cs_common, cs_eval, cs_eval_status, cs_parser, cs_warn 3 | 4 | status_key = 'clojure-sublimed-conn' 5 | phases = ['🌑', '🌒', '🌓', '🌔', '🌕'] 6 | 7 | def ready(window = None): 8 | """ 9 | When connection is fully initialized 10 | """ 11 | state = cs_common.get_state(window) 12 | return bool(state.conn and state.conn.ready()) 13 | 14 | class Connection: 15 | def __init__(self): 16 | self.status = None 17 | self.disconnecting = False 18 | self.window = sublime.active_window() 19 | 20 | def get_addr(self): 21 | return self.addr() if callable(self.addr) else self.addr 22 | 23 | def connect_impl(self): 24 | pass 25 | 26 | def connect(self): 27 | """ 28 | Connect to address specified during construction 29 | """ 30 | state = cs_common.get_state() 31 | try: 32 | self.connect_impl() 33 | state.conn = self 34 | except Exception as e: 35 | cs_common.error('Connection failed') 36 | self.disconnect() 37 | if window := sublime.active_window(): 38 | window.status_message(f'Connection failed') 39 | 40 | def try_connect_impl(self, timeout): 41 | state = cs_common.get_state(self.window) 42 | t0 = time.time() 43 | attempt = 1 44 | while time.time() - t0 <= timeout: 45 | time.sleep(0.25) 46 | try: 47 | cs_common.debug('Connection attempt #{} to {}', attempt, self.get_addr()) 48 | self.connect_impl() 49 | state.conn = self 50 | return 51 | except Exception as e: 52 | attempt += 1 53 | cs_common.error('Giving up after {} sec connecting to {}', round(time.time() - t0, 2), self.get_addr()) 54 | self.disconnect() 55 | if window := sublime.active_window(): 56 | window.status_message(f'Connection failed') 57 | 58 | def try_connect(self, timeout = 0): 59 | state = cs_common.get_state(self.window) 60 | if timeout: 61 | threading.Thread(target = self.try_connect_impl, args=(timeout,)).start() 62 | else: 63 | self.connect() 64 | 65 | def ready(self): 66 | return bool(self.status and self.status[0] == phases[4]) 67 | 68 | def eval_impl(self, form): 69 | pass 70 | 71 | def eval_region(self, region, view): 72 | if region.empty(): 73 | if eval := cs_eval.by_region(view, region): 74 | return eval.region() 75 | return cs_parser.topmost_form(view, region.begin()) 76 | return region 77 | 78 | def code(self, view, selected_region, eval_region, transform_fn = None): 79 | code = view.substr(eval_region) 80 | ns = cs_parser.namespace(view, eval_region.begin()) or 'user' 81 | parsed = cs_parser.parse(view.substr(eval_region)) 82 | forms = [child for child in parsed.children if child.name not in {'comment', 'discard'}] 83 | 84 | if transform_fn: 85 | symbol = cs_parser.defsym(forms[0]) if len(forms) == 1 else None 86 | kwargs = {'selected_region': selected_region, 87 | 'eval_region': eval_region, 88 | 'ns': ns, 89 | 'symbol': symbol} 90 | code = transform_fn(code, **kwargs) 91 | 92 | return (code, ns, forms) 93 | 94 | 95 | def eval(self, view, sel, transform_fn = None, print_quota = None, on_finish = None): 96 | """ 97 | Eval code and call `cs_eval.on_success(id, value)` or `cs_eval.on_exception(id, value, trace)` 98 | """ 99 | for selected_region in sel: 100 | eval_region = self.eval_region(selected_region, view) 101 | eval = cs_eval.Eval(view, eval_region, on_finish = on_finish) 102 | (line, column) = view.rowcol_utf16(eval_region.begin()) 103 | line = line + 1 104 | 105 | (code, ns, forms) = self.code(view, selected_region, eval_region, transform_fn) 106 | 107 | form = cs_common.Form( 108 | id = eval.id, 109 | code = code, 110 | ns = ns, 111 | line = line, 112 | column = column, 113 | file = view.file_name(), 114 | print_quota = print_quota) 115 | self.eval_impl(form) 116 | 117 | def eval_status(self, code, ns): 118 | eval = cs_eval_status.StatusEval(code) 119 | form = cs_common.Form(id = eval.id, code = code, ns = ns) 120 | self.eval_impl(form) 121 | 122 | def load_file_impl(self, id, file, path): 123 | pass 124 | 125 | def load_file(self, view): 126 | """ 127 | Load whole file (~load-file nREPL command). Same callbacks as `eval` 128 | """ 129 | region = sublime.Region(0, view.size()) 130 | eval = cs_eval.Eval(view, region) 131 | self.load_file_impl(eval.id, view.substr(region), view.file_name()) 132 | 133 | def lookup_impl(self, id, symbol, ns): 134 | pass 135 | 136 | def lookup(self, view, region): 137 | """ 138 | Look symbol up and call `cs_eval.on_lookup(id, value)` 139 | """ 140 | symbol = view.substr(region) 141 | ns = cs_parser.namespace(view, region.begin()) or 'user' 142 | eval = cs_eval.Eval(view, region) 143 | self.lookup_impl(eval.id, symbol, ns) 144 | 145 | def interrupt_impl(self, batch_id, id): 146 | pass 147 | 148 | def interrupt(self, batch_id, id): 149 | """ 150 | Interrupt currently executing eval with id = id. 151 | Will probably call `cs_eval.on_exception(id, value, trace)` on interruption 152 | """ 153 | self.interrupt_impl(batch_id, id) 154 | 155 | def disconnect_impl(self): 156 | pass 157 | 158 | def disconnect(self): 159 | """ 160 | Disconnect from REPL 161 | """ 162 | if self.disconnecting: 163 | return 164 | self.disconnecting = True 165 | self.disconnect_impl() 166 | state = cs_common.get_state() 167 | state.conn = None 168 | cs_common.set_status(self.window, status_key, None) 169 | cs_eval.erase_evals(lambda eval: eval.window == self.window) 170 | cs_warn.reset_warnings(self.window) 171 | 172 | def set_status(self, phase, message, *args): 173 | status = phases[phase] + ' ' + message.format(*args) 174 | self.status = status 175 | cs_common.set_status(self.window, status_key, status) 176 | 177 | def is_socket(path): 178 | return stat.S_ISSOCK(os.stat(path).st_mode) 179 | 180 | class AddressInputHandler(sublime_plugin.TextInputHandler): 181 | def __init__(self, port_files = [], next_input = None): 182 | self.port_files = port_files 183 | self.next = next_input 184 | 185 | """ 186 | Reusable InputHandler that remembers last address and can also look for .nrepl-port file 187 | """ 188 | def placeholder(self): 189 | return "host:port or /path/to/nrepl.sock" 190 | 191 | def initial_text(self): 192 | # .nrepl-port file present 193 | if self.port_files: 194 | for port_file in self.port_files: 195 | if path := cs_common.find_in_folders(name = port_file): 196 | with open(path, "rt") as f: 197 | content = f.read(10).strip() 198 | if re.fullmatch(r'[1-9][0-9]*', content): 199 | return f'localhost:{content}' 200 | if path := cs_common.find_in_folders(pred = is_socket): 201 | return path 202 | state = cs_common.get_state() 203 | return state.last_conn[1]['address'] if state.last_conn else 'localhost:' 204 | 205 | def initial_selection(self): 206 | text = self.initial_text() 207 | end = len(text) 208 | if ':' in text: 209 | return [(text.rfind(':') + 1, end)] 210 | elif '/' in text: 211 | return [(text.rfind('/') + 1, end)] 212 | 213 | def preview(self, text): 214 | if not self.validate(text): 215 | return 'Expected : or ' 216 | 217 | def validate(self, text): 218 | text = text.strip() 219 | if not text: 220 | return False 221 | elif 'auto' == text: 222 | return True 223 | elif match := re.fullmatch(r'([a-zA-Z0-9\.]+):(\d{1,5})', text): 224 | _, port = match.groups() 225 | return 1 <= int(port) and int(port) < 65536 226 | else: 227 | path = cs_common.find_in_folders(name = text) 228 | return bool(path and is_socket(path)) 229 | 230 | def next_input(self, args): 231 | return self.next 232 | 233 | class ClojureSublimedReconnectCommand(sublime_plugin.WindowCommand): 234 | def run(self): 235 | state = cs_common.get_state(self.window) 236 | if state.conn: 237 | self.window.run_command('clojure_sublimed_disconnect', {}) 238 | self.window.run_command(*state.last_conn) 239 | 240 | def is_enabled(self): 241 | state = cs_common.get_state(self.window) 242 | return state.last_conn is not None 243 | 244 | class ClojureSublimedDisconnectCommand(sublime_plugin.WindowCommand): 245 | def run(self): 246 | state = cs_common.get_state(self.window) 247 | state.conn.disconnect() 248 | 249 | def is_enabled(self): 250 | state = cs_common.get_state(self.window) 251 | return state.conn is not None 252 | 253 | def plugin_unloaded(): 254 | for state in cs_common.states.values(): 255 | if state.conn: 256 | state.conn.disconnect() 257 | -------------------------------------------------------------------------------- /test_comment/comment.txt: -------------------------------------------------------------------------------- 1 | ================================================================================ 2 | Partial comment 3 | ================================================================================ 4 | 5 | (def m 6 | {→:x #_1 7 | #_:y 2←}) 8 | 9 | -------------------------------------------------------------------------------- 10 | 11 | (def m 12 | {#_→:x #_1 13 | #_:y #_2←}) 14 | 15 | ================================================================================ 16 | Partial comment (reverse) 17 | ================================================================================ 18 | 19 | (def m 20 | {#_→:x #_1 21 | #_:y #_2←}) 22 | 23 | -------------------------------------------------------------------------------- 24 | 25 | (def m 26 | {→:x 1 27 | :y 2←}) 28 | 29 | ================================================================================ 30 | [ BROKEN ] Uncomment line comments 1 31 | ================================================================================ 32 | 33 | (def m 34 | {:x 1 35 | | ;; :y 2 36 | :z 3}) 37 | 38 | -------------------------------------------------------------------------------- 39 | 40 | (def m 41 | {:x 1 42 | | :y 2 43 | :z 3}) 44 | 45 | ================================================================================ 46 | Uncomment line comments 2 47 | ================================================================================ 48 | 49 | (def m 50 | {:x 1 51 | |;; :y 2 52 | :z 3}) 53 | 54 | -------------------------------------------------------------------------------- 55 | 56 | (def m 57 | {:x 1 58 | |:y 2 59 | :z 3}) 60 | 61 | ================================================================================ 62 | Uncomment line comments 3 63 | ================================================================================ 64 | 65 | (def m 66 | {:x 1 67 | ;|; :y 2 68 | :z 3}) 69 | 70 | -------------------------------------------------------------------------------- 71 | 72 | (def m 73 | {:x 1 74 | |:y 2 75 | :z 3}) 76 | 77 | ================================================================================ 78 | Uncomment line comments 4 79 | ================================================================================ 80 | 81 | (def m 82 | {:x 1 83 | ;;| :y 2 84 | :z 3}) 85 | 86 | -------------------------------------------------------------------------------- 87 | 88 | (def m 89 | {:x 1 90 | |:y 2 91 | :z 3}) 92 | 93 | ================================================================================ 94 | Uncomment line comments 5 95 | ================================================================================ 96 | 97 | (def m 98 | {:x 1 99 | ;; |:y 2 100 | :z 3}) 101 | 102 | -------------------------------------------------------------------------------- 103 | 104 | (def m 105 | {:x 1 106 | |:y 2 107 | :z 3}) 108 | 109 | ================================================================================ 110 | Uncomment line comments 6 111 | ================================================================================ 112 | 113 | (def m 114 | {:x 1 115 | ;; :y |2 116 | :z 3}) 117 | 118 | -------------------------------------------------------------------------------- 119 | 120 | (def m 121 | {:x 1 122 | :y |2 123 | :z 3}) 124 | 125 | ================================================================================ 126 | Uncomment line comments 7 127 | ================================================================================ 128 | 129 | (def m 130 | {:x 1 131 | ;; :y 2| 132 | :z 3}) 133 | 134 | -------------------------------------------------------------------------------- 135 | 136 | (def m 137 | {:x 1 138 | :y 2| 139 | :z 3}) 140 | 141 | ================================================================================ 142 | Uncomment with space 1 143 | ================================================================================ 144 | 145 | (defn |#_ abc [] 146 | ...) 147 | 148 | -------------------------------------------------------------------------------- 149 | 150 | (defn |abc [] 151 | ...) 152 | 153 | ================================================================================ 154 | Uncomment with space 2 155 | ================================================================================ 156 | 157 | (defn #|_ abc [] 158 | ...) 159 | 160 | -------------------------------------------------------------------------------- 161 | 162 | (defn |abc [] 163 | ...) 164 | 165 | ================================================================================ 166 | Uncomment with space 3 167 | ================================================================================ 168 | 169 | (defn #_| abc [] 170 | ...) 171 | 172 | -------------------------------------------------------------------------------- 173 | 174 | (defn |abc [] 175 | ...) 176 | 177 | ================================================================================ 178 | Uncomment with space 4 179 | ================================================================================ 180 | 181 | (defn #_ | abc [] 182 | ...) 183 | 184 | -------------------------------------------------------------------------------- 185 | 186 | (defn |abc [] 187 | ...) 188 | 189 | ================================================================================ 190 | Uncomment with space 5 191 | ================================================================================ 192 | 193 | (defn #_ |abc [] 194 | ...) 195 | 196 | -------------------------------------------------------------------------------- 197 | 198 | (defn |abc [] 199 | ...) 200 | 201 | ================================================================================ 202 | Uncomment with space 6 203 | ================================================================================ 204 | 205 | (defn #_ a|bc [] 206 | ...) 207 | 208 | -------------------------------------------------------------------------------- 209 | 210 | (defn a|bc [] 211 | ...) 212 | 213 | ================================================================================ 214 | Uncomment with space 7 215 | ================================================================================ 216 | 217 | (defn #_ abc| [] 218 | ...) 219 | 220 | -------------------------------------------------------------------------------- 221 | 222 | (defn abc| [] 223 | ...) 224 | 225 | ================================================================================ 226 | Uncomment with space 8 227 | ================================================================================ 228 | 229 | (defn #_ abc | [] 230 | ...) 231 | 232 | -------------------------------------------------------------------------------- 233 | 234 | (defn abc | [] 235 | ...) 236 | 237 | ================================================================================ 238 | Uncomment deep 1 239 | ================================================================================ 240 | 241 | (defn |#_ (let [x 1 242 | y {:a [1 2 3]}]) 243 | (println)) 244 | 245 | -------------------------------------------------------------------------------- 246 | 247 | (defn |(let [x 1 248 | y {:a [1 2 3]}]) 249 | (println)) 250 | 251 | ================================================================================ 252 | Uncomment deep 2 253 | ================================================================================ 254 | 255 | (defn #|_ (let [x 1 256 | y {:a [1 2 3]}]) 257 | (println)) 258 | 259 | -------------------------------------------------------------------------------- 260 | 261 | (defn |(let [x 1 262 | y {:a [1 2 3]}]) 263 | (println)) 264 | 265 | ================================================================================ 266 | Uncomment deep 3 267 | ================================================================================ 268 | 269 | (defn #_| (let [x 1 270 | y {:a [1 2 3]}]) 271 | (println)) 272 | 273 | -------------------------------------------------------------------------------- 274 | 275 | (defn |(let [x 1 276 | y {:a [1 2 3]}]) 277 | (println)) 278 | 279 | ================================================================================ 280 | Uncomment deep 4 281 | ================================================================================ 282 | 283 | (defn #_ |(let [x 1 284 | y {:a [1 2 3]}]) 285 | (println)) 286 | 287 | -------------------------------------------------------------------------------- 288 | 289 | (defn |(let [x 1 290 | y {:a [1 2 3]}]) 291 | (println)) 292 | 293 | ================================================================================ 294 | Uncomment deep 5 295 | ================================================================================ 296 | 297 | (defn #_ (let [x 1 298 | y {:a [1| 2 3]}]) 299 | (println)) 300 | 301 | -------------------------------------------------------------------------------- 302 | 303 | (defn (let [x 1 304 | y {:a [1| 2 3]}]) 305 | (println)) 306 | 307 | ================================================================================ 308 | Uncomment deep 6 309 | ================================================================================ 310 | 311 | (defn #_ (let [x 1 312 | y {:a [1 2 3]}]|) 313 | (println)) 314 | 315 | -------------------------------------------------------------------------------- 316 | 317 | (defn (let [x 1 318 | y {:a [1 2 3]}]|) 319 | (println)) 320 | 321 | ================================================================================ 322 | Uncomment deep 7 323 | ================================================================================ 324 | 325 | (defn #_ (let [x 1 326 | y {:a [1 2 3]}])| 327 | (println)) 328 | 329 | -------------------------------------------------------------------------------- 330 | 331 | (defn (let [x 1 332 | y {:a [1 2 3]}])| 333 | (println)) 334 | 335 | -------------------------------------------------------------------------------- /src_clojure/clojure_sublimed/socket_repl.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-sublimed.socket-repl 2 | (:require 3 | [clojure.string :as str] 4 | [clojure.walk :as walk] 5 | [clojure-sublimed.core :as core]) 6 | (:import 7 | [java.io FilterWriter Reader StringReader Writer] 8 | [java.lang.reflect Field] 9 | [clojure.lang Compiler Compiler$CompilerException LineNumberingPushbackReader LispReader LispReader$ReaderException RT TaggedLiteral])) 10 | 11 | (defonce ^:dynamic *out-fn* 12 | prn) 13 | 14 | (defonce *out-fns 15 | (atom #{})) 16 | 17 | (defonce ^:dynamic *context* 18 | nil) 19 | 20 | (defonce *evals 21 | (atom {})) 22 | 23 | (defn stop! [] 24 | (throw (ex-info "Stop" {::stop true}))) 25 | 26 | (defn read-command [in] 27 | (let [[form s] (read+string {:eof ::eof, :read-cond :allow} in)] 28 | (when (= ::eof form) 29 | (stop!)) 30 | 31 | ; (vswap! *context* assoc :form s) 32 | 33 | (when-not (map? form) 34 | (throw (Exception. (str "Unexpected form: " (pr-str form))))) 35 | 36 | form)) 37 | 38 | (defn report-throwable [^Throwable t] 39 | (let [{:clojure.error/keys [source line column]} (ex-data t)] 40 | (*out-fn* 41 | {"tag" "ex" 42 | "val" (core/error-str t) 43 | "trace" (core/trace-str t) 44 | "source" source 45 | "line" line 46 | "column" (some-> column inc)}))) 47 | 48 | (defn reader ^LineNumberingPushbackReader [code line column] 49 | (let [reader (LineNumberingPushbackReader. (StringReader. code))] 50 | (when line 51 | (.setLineNumber reader (int line))) 52 | (when column 53 | (when-some [field (.getDeclaredField LineNumberingPushbackReader "_columnNumber")] 54 | (doto ^Field field 55 | (.setAccessible true) 56 | (.set reader (int column))))) 57 | reader)) 58 | 59 | (defn consume-ws [^LineNumberingPushbackReader reader] 60 | (loop [ch (.read reader)] 61 | (if (or (Character/isWhitespace ch) (= (int \,) ch)) 62 | (recur (.read reader)) 63 | (when-not (neg? ch) 64 | (.unread reader ch))))) 65 | 66 | (defn eval-code [form] 67 | (let [{:strs [code ns line column file]} form 68 | name (or (some-> file (str/split #"[/\\]") last) "NO_SOURCE_FILE") 69 | ns (symbol (or ns "user")) 70 | ns-obj (or 71 | (find-ns ns) 72 | (do 73 | (require ns) 74 | (find-ns ns))) 75 | ;; Adapted from clojure.lang.Compiler/load 76 | ;; Does not bind *uncheked-math*, *warn-on-reflection* and *data-readers* 77 | eof (Object.) 78 | opts {:eof eof 79 | :read-cond :allow} 80 | reader (reader code line column) 81 | _ (consume-ws reader) 82 | _ (push-thread-bindings 83 | {Compiler/LOADER (RT/makeClassLoader) 84 | #'*file* file 85 | #'*source-path* name 86 | Compiler/METHOD nil 87 | Compiler/LOCAL_ENV nil 88 | Compiler/LOOP_LOCALS nil 89 | Compiler/NEXT_LOCAL_NUM 0 90 | #'*read-eval* true 91 | #'*ns* ns-obj 92 | Compiler/LINE_BEFORE (.getLineNumber reader) 93 | Compiler/COLUMN_BEFORE (.getColumnNumber reader) 94 | Compiler/LINE_AFTER (.getLineNumber reader) 95 | Compiler/COLUMN_AFTER (.getColumnNumber reader) 96 | #'*e nil 97 | #'*1 nil 98 | #'*2 nil 99 | #'*3 nil}) 100 | ret (try 101 | (loop [idx 0] 102 | (vswap! *context* assoc "idx" idx) 103 | (let [[obj obj-str] (read+string opts reader)] 104 | (when-not (identical? obj eof) 105 | (.set Compiler/LINE_AFTER (.getLineNumber reader)) 106 | (.set Compiler/COLUMN_AFTER (.getColumnNumber reader)) 107 | (vswap! *context* assoc 108 | "from_line" (.get Compiler/LINE_BEFORE) 109 | "from_column" (.get Compiler/COLUMN_BEFORE) 110 | "to_line" (.get Compiler/LINE_AFTER) 111 | "to_column" (.get Compiler/COLUMN_AFTER) 112 | "form" obj-str) 113 | (let [start (System/nanoTime) 114 | ret (Compiler/eval obj false)] 115 | (*out-fn* 116 | {"tag" "ret" 117 | "val" (core/bounded-pr-str ret) 118 | "time" (-> (System/nanoTime) (- start) (quot 1000000))}) 119 | (consume-ws reader) 120 | (.set Compiler/LINE_BEFORE (.getLineNumber reader)) 121 | (.set Compiler/COLUMN_BEFORE (.getColumnNumber reader)) 122 | (recur (inc idx)))))) 123 | (catch LispReader$ReaderException e 124 | (throw (Compiler$CompilerException. 125 | file 126 | (.-line e) 127 | (.-column e) 128 | nil 129 | Compiler$CompilerException/PHASE_READ 130 | (.getCause e)))) 131 | (catch Throwable e 132 | (if (instance? Compiler$CompilerException e) 133 | (throw e) 134 | (throw (Compiler$CompilerException. 135 | file 136 | (.deref Compiler/LINE_BEFORE) 137 | (.deref Compiler/COLUMN_BEFORE) 138 | nil 139 | Compiler$CompilerException/PHASE_EXECUTION 140 | e)))) 141 | (finally 142 | (pop-thread-bindings)))])) 143 | 144 | (defn fork-eval [{:strs [id print_quota] :as form}] 145 | (swap! *evals assoc id 146 | (future 147 | (binding [core/*print-quota* (or print_quota core/*print-quota*)] 148 | (try 149 | (core/track-vars 150 | (eval-code form)) 151 | (catch Throwable t 152 | (try 153 | (report-throwable t) 154 | (catch Throwable t 155 | :ignore))) 156 | (finally 157 | (swap! *evals dissoc id) 158 | (vswap! *context* dissoc "idx" "from_line" "from_column" "to_line" "to_column" "form") 159 | (*out-fn* 160 | {"tag" "done"}))))))) 161 | 162 | (defn interrupt [{:strs [id]}] 163 | (when-some [f (@*evals id)] 164 | (future-cancel f))) 165 | 166 | (def safe-meta? 167 | #{:ns :name :doc :file :arglists :forms :macro :special-form :protocol :line :column :added :deprecated :resource}) 168 | 169 | (defn lookup-symbol [form] 170 | (let [{:strs [id op symbol ns]} form 171 | ns (clojure.core/symbol (or ns "user")) 172 | symbol (clojure.core/symbol symbol) 173 | meta (if (special-symbol? symbol) 174 | (assoc ((requiring-resolve 'clojure.repl/special-doc) symbol) 175 | :ns 'clojure.core 176 | :file "clojure/core.clj" 177 | :special-form true) 178 | (meta (ns-resolve ns symbol)))] 179 | (*out-fn* 180 | (if meta 181 | (let [meta' (reduce-kv 182 | (fn [m k v] 183 | (if (safe-meta? k) 184 | (assoc m (name k) (str v)) ; stringify to match nREPL 185 | m)) 186 | nil 187 | meta)] 188 | {"tag" "lookup" 189 | "val" meta'}) 190 | {"tag" "ex" 191 | "val" (str "Symbol '" symbol " not found in ns '" ns)})))) 192 | 193 | (defmacro watch [id form] 194 | `(let [res# ~form 195 | msg# {"tag" "watch" 196 | "val" (core/bounded-pr-str res#) 197 | "watch_id" ~id}] 198 | (doseq [out-fn# @*out-fns] 199 | (out-fn# msg#)) 200 | res#)) 201 | 202 | (defn out-fn [out] 203 | (let [lock (Object.)] 204 | #(locking lock 205 | (binding [*out* out 206 | *print-readably* true] 207 | (prn (merge (sorted-map) (some-> *context* deref) %)))))) 208 | 209 | (defn repl [] 210 | (let [out-fn (out-fn *out*)] 211 | (try 212 | (swap! *out-fns conj out-fn) 213 | (binding [*out-fn* out-fn 214 | *out* (core/duplicate-writer (.getRawRoot #'*out*) "out" out-fn) 215 | *err* (core/duplicate-writer (.getRawRoot #'*err*) "err" out-fn) 216 | core/*changed-vars (atom {})] 217 | (out-fn {"tag" "started"}) 218 | (loop [] 219 | (when 220 | (binding [*context* (volatile! {})] 221 | (try 222 | (let [form (read-command *in*)] 223 | (core/set-changed-vars!) 224 | (when-some [id (form "id")] 225 | (vswap! *context* assoc "id" id)) 226 | (case (get form "op") 227 | "eval" (fork-eval form) 228 | "interrupt" (interrupt form) 229 | "lookup" (lookup-symbol form) 230 | (throw (Exception. (str "Unknown op: " (get form "op"))))) 231 | true) 232 | (catch Throwable t 233 | (when-not (-> t ex-data ::stop) 234 | (report-throwable t) 235 | true)))) 236 | (recur))) 237 | (doseq [[id f] @*evals] 238 | (future-cancel f))) 239 | (finally 240 | (swap! *out-fns disj out-fn))))) 241 | -------------------------------------------------------------------------------- /cs_indent.py: -------------------------------------------------------------------------------- 1 | import collections, re 2 | import sublime, sublime_plugin 3 | from . import cs_cljfmt, cs_common, cs_parser, cs_printer 4 | 5 | def search_path(node, pos): 6 | """ 7 | Looks for the deepest node that wraps pos (start < pos < end). 8 | Returns full path to that node from the top 9 | """ 10 | res = [node] 11 | for child in node.children: 12 | if child.start < pos < child.end: 13 | res += search_path(child, pos) 14 | elif pos < child.start: 15 | break 16 | return res 17 | 18 | def indent(view, point, parsed = None): 19 | """ 20 | Given point, returns (tag, row, indent) for that line, where indent 21 | is a correct indent based on the last unclosed paren before point. 22 | 23 | Tag could be 'string' (don't change anything, we're inside string), 24 | 'top-level' (set to 0, we are at top level) or 'indent' (normal behaviour) 25 | 26 | Row is row number of the token for which this indent is based on (row of open paren) 27 | """ 28 | parsed = parsed or cs_parser.parse(view.substr(sublime.Region(0, point)) + ' ') 29 | if path := search_path(parsed, point): 30 | node = None 31 | first_form = None 32 | 33 | # try finding unmatched open paren 34 | for child in path[-1].children: 35 | if child.start >= point: 36 | break 37 | if child.name == 'error' and child.text in ['(', '[', '{', '"']: 38 | node = child 39 | first_form = None 40 | elif first_form is None: 41 | first_form = child 42 | 43 | # try indent relative to wrapping paren 44 | if not node: 45 | for n in reversed(path): 46 | if n.name in ['string', 'parens', 'braces', 'brackets']: 47 | node = n 48 | first_form = node.body.children[0] if node.body and node.body.children else None 49 | break 50 | 51 | # top level 52 | if not node: 53 | row, _ = view.rowcol(point) 54 | return ('top-level', row, 0) 55 | 56 | row, col = view.rowcol(node.open.end if node.open else node.end) 57 | offset = 0 58 | if node.name == 'string': 59 | return ('string', row, col) 60 | elif node.name == 'parens' or (node.name == 'error' and node.text == '('): 61 | if first_form and cs_parser.is_symbol(first_form): 62 | offset = 1 63 | else: 64 | offset = 0 65 | return ('indent', row, col + offset) 66 | 67 | def newline_indent(view, point): 68 | return indent(view, point)[2] 69 | 70 | def skip_spaces(view, point): 71 | """ 72 | Starting from point, skips as much spaces as it can without going to the new line, 73 | and returns new point 74 | """ 75 | def is_space(point): 76 | s = view.substr(sublime.Region(point, point + 1)) 77 | return s.isspace() and s not in ['\n', '\r'] 78 | while point < view.size() and is_space(point): 79 | point = point + 1 80 | return point 81 | 82 | def indent_lines(view, selections, edit): 83 | """ 84 | Given set of sorted ranges (`selections`), indents all lines touched by those selections 85 | """ 86 | # Calculate all replacements first 87 | parsed = cs_parser.parse(view.substr(sublime.Region(0, view.size())) + ' ') 88 | replacements = {} # row -> (begin, delta_i) 89 | for sel in selections: 90 | for line in view.lines(sel): 91 | begin = line.begin() 92 | end = skip_spaces(view, begin) 93 | # do not touch empty lines 94 | if end == line.end(): 95 | continue 96 | row, _ = view.rowcol(begin) 97 | type, base_row, i = indent(view, begin, parsed) 98 | # do not re-indent multiline strings 99 | if type == 'string': 100 | continue 101 | # if we moved line before and depend on it, take that into account 102 | _, base_delta_i = replacements.get(base_row, (0, 0)) 103 | delta_i = i - (end - begin) + base_delta_i 104 | if delta_i != 0: 105 | replacements[row] = (begin, delta_i) 106 | 107 | # Now apply all replacements, recalculating begins as we go 108 | delta_total = 0 109 | for row in replacements: 110 | begin, delta_i = replacements[row] 111 | begin = begin + delta_total 112 | delta_total += delta_i 113 | if delta_i < 0: 114 | view.replace(edit, sublime.Region(begin, begin - delta_i), "") 115 | else: 116 | view.replace(edit, sublime.Region(begin, begin), " " * delta_i) 117 | 118 | class ClojureSublimedReindentBufferOnSave(sublime_plugin.EventListener): 119 | def on_pre_save(self, view): 120 | if cs_common.setting("format_on_save", False) and ('Clojure' in view.syntax().name or 'EDN' in view.syntax().name): 121 | view.run_command('clojure_sublimed_reindent_buffer') 122 | 123 | class ClojureSublimedReindentBufferCommand(sublime_plugin.TextCommand): 124 | def run(self, edit): 125 | view = self.view 126 | with cs_common.Measure("Reindent Buffer {} chars", view.size()): 127 | if 'cljfmt' == cs_common.setting('formatter', view = view): 128 | cs_cljfmt.indent_lines(view, [sublime.Region(0, view.size())], edit) 129 | else: 130 | indent_lines(view, [sublime.Region(0, view.size())], edit) 131 | 132 | class ClojureSublimedReindentLinesCommand(sublime_plugin.TextCommand): 133 | def run(self, edit): 134 | view = self.view 135 | with cs_common.Measure("Reindent Lines {} chars", sum([r.size() for r in view.sel()])): 136 | if 'cljfmt' == cs_common.setting('formatter', view = view): 137 | cs_cljfmt.indent_lines(view, view.sel(), edit) 138 | else: 139 | indent_lines(view, view.sel(), edit) 140 | 141 | class ClojureSublimedReindentCommand(sublime_plugin.TextCommand): 142 | def run(self, edit): 143 | view = self.view 144 | if all(r.empty() for r in view.sel()): 145 | view.run_command('clojure_sublimed_reindent_buffer') 146 | else: 147 | view.run_command('clojure_sublimed_reindent_lines') 148 | 149 | class ClojureSublimedPrettyPrintCommand(sublime_plugin.TextCommand): 150 | def run(self, edit): 151 | view = self.view 152 | change_id = view.change_id() 153 | for region in [r for r in view.sel()]: 154 | region = view.transform_region_from(region, change_id) 155 | if region.empty(): 156 | region = cs_parser.topmost_form(view, region.begin()) 157 | form = view.substr(region) 158 | node = cs_parser.parse(form) 159 | formatted = cs_printer.format(form, node, limit = cs_common.wrap_width(view)) 160 | view.replace(edit, region, formatted) 161 | 162 | class ClojureSublimedSelectTopmostFormCommand(sublime_plugin.TextCommand): 163 | def run(self, edit): 164 | view = self.view 165 | sel = view.sel() 166 | for region in [r for r in sel]: 167 | sel.add(cs_parser.topmost_form(view, region.begin())) 168 | 169 | def cljfmt_indent(view, point): 170 | i = None 171 | try: 172 | i = cs_cljfmt.newline_indent(view, point) 173 | except: 174 | pass 175 | return newline_indent(view, point) if i is None else i 176 | 177 | class ClojureSublimedInsertNewlineCommand(sublime_plugin.TextCommand): 178 | def run(self, edit): 179 | view = self.view 180 | newline_indent_fn = cljfmt_indent if 'cljfmt' == cs_common.setting('formatter', view = view) else newline_indent 181 | 182 | # Calculate all replacements first 183 | replacements = [] 184 | for sel in view.sel(): 185 | end = skip_spaces(view, sel.end()) 186 | i = newline_indent_fn(view, sel.begin()) 187 | replacements.append((sublime.Region(sel.begin(), end), "\n" + " " * i)) 188 | 189 | # Now apply them all at once 190 | change_id_sel = view.change_id() 191 | view.sel().clear() 192 | for region, string in replacements: 193 | region = view.transform_region_from(region, change_id_sel) 194 | point = region.begin() + len(string) 195 | view.replace(edit, region, string) 196 | # Add selection at the end of newly inserted region 197 | view.sel().add(sublime.Region(point, point)) 198 | 199 | view.show(view.sel(), show_surrounds = False) 200 | 201 | class ClojureSublimedAlignCursorsCommand(sublime_plugin.TextCommand): 202 | def run(self, edit): 203 | view = self.view 204 | by_row = collections.defaultdict(list) 205 | for region in view.sel(): 206 | row, col = view.rowcol(region.a) 207 | by_row[row].append(region) 208 | # print('by_row', by_row) 209 | cols = max(len(line_regions) for line_regions in by_row.values()) 210 | # print('cols', cols) 211 | change_id = view.change_id() 212 | # upd = lambda r: view.transform_region_from(r, change_id) 213 | for col in range(0, cols): 214 | col_regions = [line_regions[col] for line_regions in by_row.values() if len(line_regions) > col] 215 | col_regions = [view.transform_region_from(r, change_id) for r in col_regions] 216 | col_regions.sort(key = lambda r: view.rowcol(r.a)[0]) 217 | # print('col', col, col_regions) 218 | max_col = max(view.rowcol(r.a)[1] for r in col_regions) 219 | max_len = max(r.size() for r in col_regions) 220 | # print("max_col", max_col, "max_len", max_len) 221 | change_id_2 = view.change_id() 222 | for r in col_regions: 223 | r = view.transform_region_from(r, change_id_2) 224 | _, col = view.rowcol(r.begin()) 225 | length = r.size() 226 | prepend = max_col - col 227 | append = max_len - length 228 | # print("r", r, "col", col, "len", length, "left", max_col - col, "right", max_len - length) 229 | view.replace(edit, sublime.Region(r.begin()), ' ' * prepend) 230 | # r = view.transform_region_from(r, change_id) 231 | # print("new r", r) 232 | view.replace(edit, sublime.Region(r.end() + prepend), ' ' * append) 233 | 234 | 235 | # change_id = view.change_id() 236 | # for region in list(view.sel()): 237 | # region = view.transform_region_from(region, change_id) 238 | # _, col = view.rowcol(region.a) 239 | # view.replace(edit, region, ' ' * (max_col - col)) 240 | -------------------------------------------------------------------------------- /src_clojure/clojure_sublimed/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-sublimed.core 2 | (:require 3 | [clojure.spec.alpha :as spec] 4 | [clojure.string :as str]) 5 | (:import 6 | [clojure.lang Compiler Compiler$CompilerException ExceptionInfo LispReader$ReaderException] 7 | [java.io BufferedWriter OutputStream OutputStreamWriter PrintWriter StringWriter Writer])) 8 | 9 | (def ^:dynamic *print-quota* 10 | 4096) 11 | 12 | (def quota-marker 13 | {}) 14 | 15 | (defn- to-char-array ^chars [x] 16 | (cond 17 | (string? x) (.toCharArray ^String x) 18 | (integer? x) (char-array [(char x)]) 19 | :else x)) 20 | 21 | ;; modified from nrepl.middleware.print/with-quota-writer 22 | (defn bounded-writer 23 | "java.io.Writer that wraps throws once it has written more than `quota` bytes" 24 | ^Writer [^Writer writer quota] 25 | (let [total (volatile! 0)] 26 | (proxy [Writer] [] 27 | (toString [] 28 | (.toString writer)) 29 | (write 30 | ([x] 31 | (let [cbuf (to-char-array x)] 32 | (.write ^Writer this cbuf (int 0) (count cbuf)))) 33 | ([x off len] 34 | (locking total 35 | (let [cbuf (to-char-array x) 36 | rem (- quota @total)] 37 | (vswap! total + len) 38 | (.write writer cbuf ^int off ^int (min len rem)) 39 | (when (neg? (- rem len)) 40 | (throw (ex-info "Quota Exceeded" quota-marker))))))) 41 | (flush [] 42 | (.flush writer)) 43 | (close [] 44 | (.close writer))))) 45 | 46 | (defn bounded-pr-str [x] 47 | (let [writer (if (> *print-quota* 0) 48 | (bounded-writer (StringWriter.) *print-quota*) 49 | (StringWriter.))] 50 | (try 51 | (binding [*out* writer] 52 | (pr x)) 53 | (str writer) 54 | (catch ExceptionInfo e 55 | (if (identical? quota-marker (ex-data e)) 56 | (str writer "...") 57 | (throw e)))))) 58 | 59 | (defn duplicate-writer ^Writer [^Writer writer tag out-fn] 60 | (let [sb (StringBuffer.) 61 | proxy (proxy [Writer] [] 62 | (flush [] 63 | (.flush writer) 64 | (let [len (.length sb)] 65 | (when (pos? len) 66 | (out-fn {"tag" tag, "val" (str sb)}) 67 | (.delete sb 0 len)))) 68 | (close [] 69 | (.close writer)) 70 | (write 71 | ([x] 72 | (let [cbuf (to-char-array x)] 73 | (.write writer cbuf) 74 | (.append sb cbuf))) 75 | ([x off len] 76 | (let [cbuf (to-char-array x)] 77 | (.write writer cbuf ^int off ^int len) 78 | (.append sb cbuf ^int off ^int len)))))] 79 | (PrintWriter. proxy true))) 80 | 81 | ;; errors 82 | 83 | (defn- noise? [^StackTraceElement el] 84 | (let [class (.getClassName el)] 85 | (#{"clojure.lang.RestFn" "clojure.lang.AFn"} class))) 86 | 87 | (defn- duplicate? [^StackTraceElement prev-el ^StackTraceElement el] 88 | (and 89 | (= (.getClassName prev-el) (.getClassName el)) 90 | (= (.getFileName prev-el) (.getFileName el)) 91 | (#{"invokeStatic"} (.getMethodName prev-el)) 92 | (#{"invoke" "doInvoke" "invokePrim"} (.getMethodName el)))) 93 | 94 | (defn- clear-duplicates [els] 95 | (for [[prev-el el] (map vector (cons nil els) els) 96 | :when (or (nil? prev-el) (not (duplicate? prev-el el)))] 97 | el)) 98 | 99 | (defn- trace-element [^StackTraceElement el] 100 | (let [file (.getFileName el) 101 | line (.getLineNumber el) 102 | cls (.getClassName el) 103 | method (.getMethodName el) 104 | clojure? (if file 105 | (or (.endsWith file ".clj") (.endsWith file ".cljc") (= file "NO_SOURCE_FILE")) 106 | (#{"invoke" "doInvoke" "invokePrim" "invokeStatic"} method)) 107 | 108 | [ns separator method] 109 | (cond 110 | (not clojure?) 111 | [(-> cls (str/split #"\.") last) "." method] 112 | 113 | (#{"invoke" "doInvoke" "invokeStatic"} method) 114 | (let [[ns method] (str/split (Compiler/demunge cls) #"/" 2) 115 | method (-> method 116 | (str/replace #"eval\d{3,}" "eval") 117 | (str/replace #"--\d{3,}" ""))] 118 | [ns "/" method]) 119 | 120 | :else 121 | [(Compiler/demunge cls) "/" (Compiler/demunge method)])] 122 | {:element el 123 | :file (if (= "NO_SOURCE_FILE" file) nil file) 124 | :line line 125 | :ns ns 126 | :separator separator 127 | :method method})) 128 | 129 | (defn- get-trace [^Throwable t] 130 | (->> (.getStackTrace t) 131 | (take-while 132 | (fn [^StackTraceElement el] 133 | (and 134 | (not= "clojure.lang.Compiler" (.getClassName el)) 135 | (not= "clojure.lang.LispReader" (.getClassName el)) 136 | (not (str/starts-with? (.getClassName el) "clojure_sublimed"))))) 137 | (remove noise?) 138 | (clear-duplicates) 139 | (mapv trace-element))) 140 | 141 | (defn datafy-throwable [^Throwable t] 142 | (let [trace (get-trace t) 143 | common (when-some [prev-t (.getCause t)] 144 | (let [prev-trace (get-trace prev-t)] 145 | (loop [m (dec (count trace)) 146 | n (dec (count prev-trace))] 147 | (if (and (>= m 0) (>= n 0) (= (nth trace m) (nth prev-trace n))) 148 | (recur (dec m) (dec n)) 149 | (- (dec (count trace)) m)))))] 150 | {:message (.getMessage t) 151 | :class (class t) 152 | :data (ex-data t) 153 | :trace trace 154 | :common (or common 0) 155 | :cause (some-> (.getCause t) datafy-throwable)})) 156 | 157 | (defmacro write [w & args] 158 | (list* 'do 159 | (for [arg args] 160 | (if (or (string? arg) (= String (:tag (meta arg)))) 161 | `(Writer/.write ~w ~arg) 162 | `(Writer/.write ~w (str ~arg)))))) 163 | 164 | (defn- pad [ch ^long len] 165 | (when (pos? len) 166 | (let [sb (StringBuilder. len)] 167 | (dotimes [_ len] 168 | (.append sb (char ch))) 169 | (str sb)))) 170 | 171 | (defn- split-file [s] 172 | (if-some [[_ name ext] (re-matches #"(.*)(\.[^.]+)" s)] 173 | [name ext] 174 | [s ""])) 175 | 176 | (defn- linearize [key xs] 177 | (->> xs (iterate key) (take-while some?))) 178 | 179 | (defn- longest-method [indent ts] 180 | (reduce max 0 181 | (for [[t depth] (map vector ts (range)) 182 | el (:trace t)] 183 | (+ (* depth indent) (count (:ns el)) (count (:separator el)) (count (:method el)))))) 184 | 185 | (defn print-humanly [^Writer w ^Throwable t] 186 | (let [ts (linearize :cause (datafy-throwable t)) 187 | max-len (longest-method 0 ts) 188 | indent " "] 189 | (doseq [[idx t] (map vector (range) ts) 190 | :let [{:keys [class message data trace common]} t]] 191 | ;; class 192 | (write w (when (pos? idx) "\nCaused by: ") (.getSimpleName ^Class class)) 193 | 194 | ;; message 195 | (when message 196 | (write w ": ") 197 | (print-method message w)) 198 | 199 | ;; data 200 | (when data 201 | (write w " ") 202 | (print-method data w)) 203 | 204 | ;; trace 205 | (doseq [el (drop-last common trace) 206 | :let [{:keys [ns separator method file line]} el 207 | right-pad (pad \space (- max-len (count ns) (count separator) (count method)))]] 208 | (write w "\n" indent) 209 | 210 | ;; method 211 | (write w ns separator method) 212 | 213 | ;; locaiton 214 | (cond 215 | (= -2 line) 216 | (write w right-pad " " "Native Method") 217 | 218 | file 219 | (write w right-pad " " file " " line))) 220 | 221 | ;; ... common elements 222 | (when (pos? common) 223 | (write w "\n" indent "... " common " common elements"))))) 224 | 225 | (defn compiler-err-str [^Throwable t] 226 | (when (and 227 | (instance? Compiler$CompilerException t) 228 | (not (= :execution (:clojure.error/phase (ex-data t)))) 229 | (str/starts-with? (.getMessage t) "Syntax error") 230 | (.getCause t) 231 | (instance? RuntimeException t)) 232 | (let [cause (.getCause t) 233 | {:clojure.error/keys [source line column]} (ex-data t) 234 | source (some-> source (str/split #"/") last)] 235 | (str (.getMessage cause) " (" source ":" line ":" (some-> column inc) ")")))) 236 | 237 | (defn root-cause ^Throwable [^Throwable t] 238 | (when t 239 | (if-some [cause (.getCause t)] 240 | (recur cause) 241 | t))) 242 | 243 | (defn error-str [^Throwable t] 244 | (or 245 | (compiler-err-str t) 246 | (let [cause (root-cause t) 247 | data (ex-data cause) 248 | class (.getSimpleName (class cause)) 249 | msg (.getMessage cause)] 250 | (cond-> (str class ": " msg) 251 | data (str " " (bounded-pr-str data)))))) 252 | 253 | (defn trace-str [^Throwable t] 254 | (or 255 | (compiler-err-str t) 256 | (let [w (StringWriter.) 257 | t (if (and 258 | (instance? Compiler$CompilerException t) 259 | (= :execution (:clojure.error/phase (ex-data t)))) 260 | (.getCause t) 261 | t)] 262 | (print-humanly w t) 263 | (str w)))) 264 | 265 | ;; Allow dynamic vars to be set in root thread when changed in spawned threads 266 | 267 | (def settable-vars 268 | [#'*ns* 269 | #'*warn-on-reflection* 270 | #'*math-context* 271 | #'*print-meta* 272 | #'*print-length* 273 | #'*print-level* 274 | #'*print-namespace-maps* 275 | #'*data-readers* 276 | #'*default-data-reader-fn* 277 | #'*compile-path* 278 | #'*command-line-args* 279 | #'*unchecked-math* 280 | #'*assert* 281 | #'spec/*explain-out*]) 282 | 283 | (def ^:dynamic *changed-vars) 284 | 285 | (defn track-vars* [vars on-change body] 286 | (let [before (persistent! 287 | (reduce #(assoc! %1 %2 @%2) 288 | (transient {}) 289 | vars))] 290 | (push-thread-bindings before) 291 | (try 292 | (body) 293 | (finally 294 | (doseq [var vars 295 | :let [val @var] 296 | :when (not= val (before var))] 297 | (on-change var val)) 298 | (pop-thread-bindings))))) 299 | 300 | (defmacro track-vars [& body] 301 | `(track-vars* 302 | settable-vars 303 | (fn [var# val#] 304 | (swap! *changed-vars assoc var# val#)) 305 | (fn [] ~@body))) 306 | 307 | (defn set-changed-vars! [] 308 | (let [[vars _] (reset-vals! *changed-vars {})] 309 | (doseq [[var val] vars] 310 | (.set ^clojure.lang.Var var val)))) 311 | -------------------------------------------------------------------------------- /cs_common.py: -------------------------------------------------------------------------------- 1 | import collections, html, math, os, re, socket, sublime, sublime_plugin, time, traceback 2 | from typing import Any, Dict, Tuple 3 | 4 | ns = 'clojure-sublimed' 5 | 6 | package = None 7 | 8 | class State: 9 | def __init__(self): 10 | self.statuses = {} 11 | self.last_view = None 12 | self.conn = None 13 | self.last_conn = None 14 | self.warnings = 0 15 | self.status_eval = None 16 | self.watches = {} 17 | 18 | states = collections.defaultdict(lambda: State()) 19 | 20 | def get_state(window = None): 21 | if window is None: 22 | window = sublime.active_window() 23 | return states[window.id()] 24 | 25 | class Form: 26 | def __init__(self, id = None, code = None, ns = 'user', line = None, column = None, file = None, print_quota = None): 27 | self.id = id 28 | self.code = code 29 | self.ns = ns 30 | self.line = line 31 | self.column = column 32 | self.file = file 33 | self.print_quota = print_quota 34 | 35 | def main_settings(view = None): 36 | if view := view or sublime.active_window().active_view(): 37 | return view.settings() 38 | return sublime.load_settings("Preferences.sublime-settings") 39 | 40 | def settings(): 41 | """ 42 | Plugin settings 43 | """ 44 | return sublime.load_settings("Clojure Sublimed.sublime-settings") 45 | 46 | def setting(key, default = None, view = None): 47 | """ 48 | Shortcut to get value of a particular plugin setting 49 | """ 50 | s = main_settings(view = view) 51 | if s and (res := s.get("clojure_sublimed_" + key)) is not None: 52 | return res 53 | s = settings() 54 | if s and (res := s.get(key)) is not None: 55 | return res 56 | return default 57 | 58 | def on_settings_change(tag, callback): 59 | """ 60 | Subscribe to settings change 61 | """ 62 | main_settings().add_on_change(tag, callback) 63 | settings().add_on_change(tag, callback) 64 | callback() 65 | 66 | def clear_settings_change(tag): 67 | """ 68 | Unsubscribe from settings change 69 | """ 70 | main_settings().clear_on_change(tag) 71 | settings().clear_on_change(tag) 72 | 73 | def wrap_width(view): 74 | if (w := setting('wrap_width')): 75 | return w 76 | if not view: 77 | return 80 78 | return math.floor(view.viewport_extent()[0] / view.em_width()) - 3 79 | 80 | def debug(format, *args): 81 | """ 82 | Print to console if 'debug' is set to True. Format as in `str.format` 83 | """ 84 | if setting('debug'): 85 | print('[ Clojure Sublimed ]', format.format(*args)) 86 | 87 | def error(format, *args): 88 | """ 89 | Print error and stacktrace to console. Format as in `str.format` 90 | """ 91 | print('[ Clojure Sublimed ] ERROR:', format.format(*args)) 92 | traceback.print_exc() 93 | 94 | class Measure: 95 | """ 96 | Measure and print (if debug) execution time of with block. Format as in `str.format` 97 | """ 98 | def __init__(self, format, *args): 99 | self.format = "{:.2f} ms " + format 100 | self.args = args 101 | 102 | def __enter__(self): 103 | self.time = time.time() 104 | 105 | def __exit__(self, exc_type, exc_value, exc_tb): 106 | debug(self.format, (time.time() - self.time) * 1000, *self.args) 107 | 108 | def format_time_taken(time_taken): 109 | """ 110 | Human-readable time taken (ms or sec) 111 | """ 112 | threshold = setting("elapsed_threshold_ms" ,100) 113 | if threshold != None and time_taken != None: 114 | elapsed = time_taken / 1000 115 | if elapsed * 1000 >= threshold: 116 | if elapsed >= 10: 117 | return f"({'{:,.0f}'.format(elapsed)} sec)" 118 | elif elapsed >= 1: 119 | return f"({'{:.1f}'.format(elapsed)} sec)" 120 | elif elapsed >= 0.005: 121 | return f"({'{:.0f}'.format(elapsed * 1000)} ms)" 122 | else: 123 | return f"({'{:.2f}'.format(elapsed * 1000)} ms)" 124 | 125 | def regions_touch(r1, r2): 126 | """ 127 | True iff regions intersect or touch 128 | """ 129 | return r1 != None and r2 != None and not r1.end() < r2.begin() and not r1.begin() > r2.end() 130 | 131 | def basic_styles(view): 132 | """ 133 | Used to format phantoms, to achieve ~line height as in the main editor 134 | """ 135 | settings = view.settings() 136 | top = settings.get('line_padding_top', 0) 137 | bottom = settings.get('line_padding_bottom', 0) 138 | return f"""