├── .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"""