├── dev-resources
└── .gitignore
├── figwheel-main.edn
├── docs
├── fonts
│ ├── FiraCode-Bold.otf
│ ├── FiraCode-Light.otf
│ ├── FiraCode-Medium.otf
│ ├── FiraCode-Retina.otf
│ ├── FiraCode-Regular.otf
│ ├── glyphicons-halflings-regular.eot
│ ├── glyphicons-halflings-regular.ttf
│ ├── glyphicons-halflings-regular.woff
│ ├── glyphicons-halflings-regular.woff2
│ └── glyphicons-halflings-regular.svg
├── style.css
├── paren-soup-dark.css
└── paren-soup-light.css
├── .gitignore
├── dev.clj
├── dev.cljs.edn
├── project.clj
├── deps.edn
├── src
└── parinferish
│ ├── examples.cljs
│ └── core.cljc
├── UNLICENSE
├── README.md
├── prod.clj
├── test.clj
└── test-resources
├── smart-mode.md
├── paren-mode.md
└── indent-mode.md
/dev-resources/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/figwheel-main.edn:
--------------------------------------------------------------------------------
1 | {:open-url "http://localhost:5000/cljs/parinferish.core"
2 | :clean-outputs true}
3 |
--------------------------------------------------------------------------------
/docs/fonts/FiraCode-Bold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oakes/parinferish/HEAD/docs/fonts/FiraCode-Bold.otf
--------------------------------------------------------------------------------
/docs/fonts/FiraCode-Light.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oakes/parinferish/HEAD/docs/fonts/FiraCode-Light.otf
--------------------------------------------------------------------------------
/docs/fonts/FiraCode-Medium.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oakes/parinferish/HEAD/docs/fonts/FiraCode-Medium.otf
--------------------------------------------------------------------------------
/docs/fonts/FiraCode-Retina.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oakes/parinferish/HEAD/docs/fonts/FiraCode-Retina.otf
--------------------------------------------------------------------------------
/docs/fonts/FiraCode-Regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oakes/parinferish/HEAD/docs/fonts/FiraCode-Regular.otf
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | data_readers.clj
2 | hs_err_pid*.log
3 | pom.xml
4 | pom.xml.asc
5 | **/gen
6 | **/target
7 | .*
8 | !.gitignore
9 |
--------------------------------------------------------------------------------
/docs/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oakes/parinferish/HEAD/docs/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/docs/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oakes/parinferish/HEAD/docs/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/docs/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oakes/parinferish/HEAD/docs/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/docs/fonts/glyphicons-halflings-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oakes/parinferish/HEAD/docs/fonts/glyphicons-halflings-regular.woff2
--------------------------------------------------------------------------------
/dev.clj:
--------------------------------------------------------------------------------
1 | (require
2 | '[figwheel.main :as figwheel]
3 | '[dynadoc.core :as dynadoc])
4 |
5 | (dynadoc/-main "--port" "5000")
6 | (figwheel/-main "--build" "dev")
7 |
8 |
--------------------------------------------------------------------------------
/dev.cljs.edn:
--------------------------------------------------------------------------------
1 | {:main parinferish.examples
2 | :optimizations :simple
3 | :output-to "dev-resources/dynadoc-extend/main.js"
4 | :output-dir "dev-resources/dynadoc-extend/main.out"
5 | :asset-path "/main.out"}
6 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject parinferish "0.9.0-SNAPSHOT"
2 | :description "A library for parsing and optionally applying parinfer(ish) to Clojure code"
3 | :url "https://github.com/oakes/parinferish"
4 | :license {:name "Public Domain"
5 | :url "http://unlicense.org/UNLICENSE"}
6 | :repositories [["clojars" {:url "https://clojars.org/repo"
7 | :sign-releases false}]])
8 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src"]
2 | :aliases {:dev {:extra-deps {org.clojure/clojurescript {:mvn/version "1.10.764"}
3 | dynadoc {:mvn/version "RELEASE"}
4 | com.bhauman/figwheel-main {:mvn/version "0.2.11"}}
5 | :extra-paths ["dev-resources"]
6 | :main-opts ["dev.clj"]}
7 | :prod {:extra-deps {leiningen {:mvn/version "2.9.0"}}
8 | :main-opts ["prod.clj"]}
9 | :test {:extra-paths ["test-resources"]
10 | :main-opts ["test.clj"]}}}
11 |
--------------------------------------------------------------------------------
/src/parinferish/examples.cljs:
--------------------------------------------------------------------------------
1 | (ns parinferish.examples
2 | (:require parinferish.core
3 | dynadoc.core)
4 | (:require-macros [dynadoc.example :refer [defexample defexamples]]))
5 |
6 | (defexample parinferish.core/parse
7 | (parse "(+ 1 2)"))
8 |
9 | (defexamples parinferish.core/flatten
10 | ["Run indent mode"
11 | (parinferish.core/flatten (parinferish.core/parse "(foo a" {:mode :indent}))]
12 | ["Run paren mode"
13 | (parinferish.core/flatten (parinferish.core/parse "(foo\na)" {:mode :paren}))]
14 | ["Run smart mode"
15 | (parinferish.core/flatten (parinferish.core/parse "([1\n 2\n 3]" {:mode :smart :cursor-line 0 :cursor-column 1}))])
16 |
17 | (defexample parinferish.core/diff
18 | (parinferish.core/diff (parinferish.core/parse "([1\n 2\n 3]" {:mode :smart :cursor-line 0 :cursor-column 1})))
19 |
20 |
--------------------------------------------------------------------------------
/UNLICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://clojars.org/parinferish)
2 |
3 | ## Introduction
4 |
5 | A Clojure and ClojureScript library that parses code and optionally applies parinfer(ish) to it. It works pretty much like parinfer and implements the three modes (indent, paren, and smart) but I'm not necessarily trying to make it behave the exact same way.
6 |
7 | ### [Try the interactive docs!](https://oakes.github.io/parinferish/)
8 |
9 | Compared to the real parinfer, parinferish...
10 |
11 | 1. Acts as a general purpose clojure parser, because its `parse` function returns the code a hiccup-style data structure, not merely a string. In fact, adjusting parens/indentation is just an optional step.
12 | 2. Doesn't ever change code when only the cursor position changes (and only smart mode requires the cursor position at all)
13 | 3. Has no basis in math, logic, or sanity...the only basis for any behavior is my own preferences and bugs.
14 |
15 | See [html-soup](https://github.com/oakes/html-soup) for an example of a library that uses parinferish as a parser.
16 |
17 | ## Licensing
18 |
19 | All files that originate from this project are dedicated to the public domain. I would love pull requests, and will assume that they are also dedicated to the public domain.
20 |
--------------------------------------------------------------------------------
/docs/style.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Fira Code';
3 | src: url('fonts/FiraCode-Regular.otf') format('opentype');
4 | }
5 | html, body, .paren-soup {
6 | -webkit-font-feature-settings: "liga" on, "calt" on;
7 | -webkit-font-smoothing: antialiased;
8 | text-rendering: optimizeLegibility;
9 | font-family: 'Fira Code';
10 | font-size: 18px;
11 | height: 100%;
12 | margin: 0;
13 | }
14 | .nses {
15 | position: fixed;
16 | width: 300px;
17 | left: 0px;
18 | top: 50px;
19 | bottom: 0px;
20 | margin: 5px;
21 | overflow: auto;
22 | white-space: nowrap;
23 | }
24 | .vars {
25 | position: fixed;
26 | top: 0px;
27 | left: 300px;
28 | right: 0px;
29 | bottom: 0px;
30 | margin: 10px;
31 | padding: 5px;
32 | overflow: auto;
33 | }
34 | .var-info {
35 | margin-top: 30px;
36 | margin-bottom: 30px;
37 | }
38 | .doc {
39 | white-space: pre;
40 | }
41 | .section {
42 | margin-top: 10px;
43 | margin-bottom: 10px;
44 | }
45 | .tag {
46 | float: left;
47 | margin-right: 5px;
48 | border-radius: 5px;
49 | background-color: lightgreen;
50 | }
51 | .search {
52 | position: fixed;
53 | left: 0px;
54 | top: 0px;
55 | width: 300px;
56 | height: 40px;
57 | margin: 5px;
58 | font-size: 30px;
59 | }
60 | .var {
61 | margin-left: 30px;
62 | }
63 | .footer {
64 | text-align: center;
65 | margin-top: 30px;
66 | font-size: 14px;
67 | }
68 | .export {
69 | text-align: right;
70 | float: right;
71 | font-size: 20px;
72 | }
73 | .card {
74 | width: 100%;
75 | height: 200px;
76 | border: 1px solid black;
77 | margin-top: 10px;
78 | margin-bottom: 10px;
79 | }
80 | canvas {
81 | width: 100%;
82 | height: 100%;
83 | }
84 |
--------------------------------------------------------------------------------
/prod.clj:
--------------------------------------------------------------------------------
1 | (defmulti task first)
2 |
3 | (defmethod task :default
4 | [_]
5 | (let [all-tasks (-> task methods (dissoc :default) keys sort)
6 | interposed (->> all-tasks (interpose ", ") (apply str))]
7 | (println "Unknown or missing task. Choose one of:" interposed)
8 | (System/exit 1)))
9 |
10 | (require
11 | '[clojure.string :as str]
12 | '[leiningen.core.project :as p :refer [defproject]]
13 | '[leiningen.install :refer [install]]
14 | '[leiningen.deploy :refer [deploy]])
15 |
16 | (defn read-project-clj []
17 | (p/ensure-dynamic-classloader)
18 | (-> "project.clj" load-file var-get))
19 |
20 | (defn read-deps-edn [aliases-to-include]
21 | (let [{:keys [paths deps aliases]} (-> "deps.edn" slurp clojure.edn/read-string)
22 | deps (->> (select-keys aliases aliases-to-include)
23 | vals
24 | (mapcat :extra-deps)
25 | (into deps)
26 | (map (fn parse-coord [coord]
27 | (let [[artifact info] coord
28 | s (str artifact)]
29 | (if-let [i (str/index-of s "$")]
30 | [(symbol (subs s 0 i))
31 | (assoc info :classifier (subs s (inc i)))]
32 | coord))))
33 | (reduce
34 | (fn [deps [artifact info]]
35 | (if-let [version (:mvn/version info)]
36 | (conj deps
37 | (transduce cat conj [artifact version]
38 | (select-keys info [:exclusions :classifier])))
39 | deps))
40 | []))
41 | paths (->> (select-keys aliases aliases-to-include)
42 | vals
43 | (mapcat :extra-paths)
44 | (into paths))]
45 | {:dependencies deps
46 | :source-paths []
47 | :resource-paths paths}))
48 |
49 | (defmethod task "install"
50 | [_]
51 | (-> (read-project-clj)
52 | (merge (read-deps-edn []))
53 | p/init-project
54 | install)
55 | (System/exit 0))
56 |
57 | (defmethod task "deploy"
58 | [_]
59 | (-> (read-project-clj)
60 | (merge (read-deps-edn []))
61 | p/init-project
62 | (deploy "clojars"))
63 | (System/exit 0))
64 |
65 | ;; entry point
66 |
67 | (task *command-line-args*)
68 |
69 |
--------------------------------------------------------------------------------
/docs/paren-soup-dark.css:
--------------------------------------------------------------------------------
1 | html, body, .paren-soup {
2 | background-color: #272b30;
3 | }
4 |
5 | button {
6 | color: white;
7 | }
8 |
9 | .paren-soup {
10 | font-family: monospace;
11 | color: lightgray;
12 | }
13 |
14 | .paren-soup .error-text {
15 | position: fixed;
16 | background-color: #293134;
17 | padding-left: 10px;
18 | }
19 |
20 | .paren-soup .instarepl {
21 | float: left;
22 | position: relative;
23 | display: list-item;
24 | padding-right: 5px;
25 | min-width: 200px;
26 | max-width: 200px;
27 | }
28 |
29 | .paren-soup .instarepl .result {
30 | position: absolute;
31 | overflow: hidden;
32 | background-color: darkgreen;
33 | outline: 1px solid;
34 | max-width: inherit;
35 | word-wrap: break-word;
36 | right: 0px;
37 | opacity: 0.7;
38 | white-space: break-spaces;
39 | }
40 |
41 | .paren-soup .instarepl .result:hover {
42 | cursor: pointer;
43 | height: auto !important;
44 | z-index: 1;
45 | opacity: 1;
46 | }
47 |
48 | .paren-soup .instarepl .error {
49 | background-color: darkred;
50 | }
51 |
52 | .paren-soup .numbers {
53 | float: left;
54 | padding: 0px 5px;
55 | text-align: right;
56 | opacity: 0.7;
57 | border-right: 1px solid #ddd;
58 | }
59 |
60 | .paren-soup .content {
61 | margin: 0px;
62 | padding: 0px 5px;
63 | outline: 0px solid transparent;
64 | white-space: pre;
65 | word-wrap: normal;
66 | overflow: scroll;
67 | }
68 |
69 | .paren-soup .content .symbol {
70 | color: white;
71 | }
72 |
73 | .paren-soup .content .number {
74 | color: gold;
75 | }
76 |
77 | .paren-soup .content .string {
78 | color: darksalmon;
79 | }
80 |
81 | .paren-soup .content .keyword {
82 | color: dodgerblue;
83 | }
84 |
85 | .paren-soup .content .nil {
86 | color: lightblue;
87 | }
88 |
89 | .paren-soup .content .boolean {
90 | color: lightblue;
91 | }
92 |
93 | .paren-soup .content .error {
94 | display: none;
95 | width: 0.9em;
96 | height: 0.9em;
97 | background-color: red;
98 | border-radius: 0.3em;
99 | }
100 |
101 | .paren-soup .content .rainbow-0 {
102 | color: aqua;
103 | }
104 |
105 | .paren-soup .content .rainbow-1 {
106 | color: orange;
107 | }
108 |
109 | .paren-soup .content .rainbow-2 {
110 | color: cornflowerblue;
111 | }
112 |
113 | .paren-soup .content .rainbow-3 {
114 | color: fuchsia;
115 | }
116 |
117 | .paren-soup .content .rainbow-4 {
118 | color: lime;
119 | }
120 |
--------------------------------------------------------------------------------
/docs/paren-soup-light.css:
--------------------------------------------------------------------------------
1 | html, body, .paren-soup {
2 | background-color: #f7f7f7;
3 | }
4 |
5 | .paren-soup {
6 | font-size: 18px;
7 | font-family: monospace;
8 | color: gray;
9 | caret-color: black;
10 | }
11 |
12 | .paren-soup .error-text {
13 | position: fixed;
14 | background-color: white;
15 | padding-left: 10px;
16 | }
17 |
18 | .paren-soup .instarepl {
19 | float: left;
20 | position: relative;
21 | display: list-item;
22 | padding-right: 5px;
23 | min-width: 200px;
24 | max-width: 200px;
25 | }
26 |
27 | .paren-soup .instarepl .result {
28 | position: absolute;
29 | overflow: hidden;
30 | background-color: lightgreen;
31 | outline: 1px solid;
32 | max-width: inherit;
33 | word-wrap: break-word;
34 | right: 0px;
35 | opacity: 0.7;
36 | white-space: break-spaces;
37 | }
38 |
39 | .paren-soup .instarepl .result:hover {
40 | cursor: pointer;
41 | height: auto !important;
42 | z-index: 1;
43 | opacity: 1;
44 | }
45 |
46 | .paren-soup .instarepl .error {
47 | background-color: pink;
48 | }
49 |
50 | .paren-soup .numbers {
51 | float: left;
52 | padding: 0px 5px;
53 | text-align: right;
54 | opacity: 0.7;
55 | border-right: 1px solid #ddd;
56 | }
57 |
58 | .paren-soup .content {
59 | margin: 0px;
60 | padding: 0px 5px;
61 | outline: 0px solid transparent;
62 | white-space: pre;
63 | word-wrap: normal;
64 | overflow: scroll;
65 | background-color: white;
66 | }
67 |
68 | .paren-soup .content .symbol {
69 | color: black;
70 | }
71 |
72 | .paren-soup .content .number {
73 | color: gold;
74 | }
75 |
76 | .paren-soup .content .string {
77 | color: red;
78 | }
79 |
80 | .paren-soup .content .keyword {
81 | color: blue;
82 | }
83 |
84 | .paren-soup .content .nil {
85 | color: lightblue;
86 | }
87 |
88 | .paren-soup .content .boolean {
89 | color: lightblue;
90 | }
91 |
92 | .paren-soup .content .error {
93 | display: none;
94 | width: 0.9em;
95 | height: 0.9em;
96 | background-color: red;
97 | border-radius: 0.3em;
98 | }
99 |
100 | .paren-soup .content .rainbow-0 {
101 | color: aqua;
102 | }
103 |
104 | .paren-soup .content .rainbow-1 {
105 | color: orange;
106 | }
107 |
108 | .paren-soup .content .rainbow-2 {
109 | color: cornflowerblue;
110 | }
111 |
112 | .paren-soup .content .rainbow-3 {
113 | color: fuchsia;
114 | }
115 |
116 | .paren-soup .content .rainbow-4 {
117 | color: lime;
118 | }
119 |
--------------------------------------------------------------------------------
/test.clj:
--------------------------------------------------------------------------------
1 | (require
2 | '[clojure.string :as str]
3 | '[clojure.java.io :as io]
4 | '[parinferish.core :as ps])
5 |
6 | (defn parse-cases [mode]
7 | (-> (str (name mode) "-mode.md")
8 | io/resource
9 | slurp
10 | (str/replace #"\n.*\^ error:.*" "")
11 | (str/split #"```")
12 | (->> (filter #(or (str/starts-with? % "in\n")
13 | (str/starts-with? % "out\n")))
14 | (map str/trim)
15 | (partition 2)
16 | (map (fn [[in out :as pair]]
17 | (when-not (str/starts-with? in "in")
18 | (throw (ex-info (str "Expected \"in\":" \newline in) {})))
19 | (when-not (str/starts-with? out "out")
20 | (throw (ex-info (str "Expected \"out\":" \newline out) {})))
21 | (let [[new-in new-out] (mapv (fn [s]
22 | (subs s (inc (str/index-of s "\n"))))
23 | pair)
24 | ret {:in new-in :out new-out :opts {:mode mode}}]
25 | (if (= :smart mode)
26 | (let [pipe-pos (or (str/index-of new-in "|")
27 | (throw (ex-info (str "No pipe found:" \newline new-in) {})))
28 | until-pipe (subs new-in 0 pipe-pos)]
29 | (update ret :opts assoc
30 | :cursor-line (->> until-pipe
31 | (re-seq #"\n")
32 | count)
33 | :cursor-column (-> until-pipe
34 | (subs (or (some-> (str/last-index-of until-pipe "\n") inc)
35 | 0))
36 | count)))
37 | ret)))))))
38 |
39 | (doseq [mode [:indent :paren :smart]]
40 | (println "Testing" (name mode) "mode")
41 | (let [*success-count (atom 0)]
42 | (doseq [{:keys [in out opts]} (parse-cases mode)]
43 | (let [in (str/replace in #"\|" "")
44 | out (str/replace out #"\|" "")
45 | res (ps/flatten (ps/parse in opts))]
46 | (if (= res out)
47 | (swap! *success-count inc)
48 | (do
49 | (println "Test failed")
50 | (println "Options: " opts)
51 | (println "Input: " (pr-str in))
52 | (println "Output: " (pr-str res))
53 | (println "Expected:" (pr-str out))
54 | (println)))))
55 | (println @*success-count "tests passed")))
56 |
57 |
--------------------------------------------------------------------------------
/test-resources/smart-mode.md:
--------------------------------------------------------------------------------
1 | # Smart Mode
2 |
3 | ## Leading Close-Parens
4 |
5 | Leading close-parens can cause many problems that can be fixed by paren mode,
6 | so we exit to paren mode when they are detected.
7 |
8 | For example, it is convenient to keep trailing parens in front of the cursor
9 | after pressing enter or after deleting everything behind them:
10 |
11 | ```in
12 | (let [a 1
13 | |])
14 | ```
15 |
16 | ```out
17 | (let [a 1
18 | |])
19 | ```
20 |
21 | Moving the cursor away:
22 |
23 | ```in-disable
24 | (let [a 1
25 | ]); <-- spaces
26 | ```
27 |
28 | ```out-disable
29 | (let [a 1])
30 | ; <-- spaces
31 | ```
32 |
33 | **NOTE: Parinferish behaves differently.** It does not change any code when the cursor moves:
34 |
35 | But we also need safety from inadvertent AST breakage. For example,
36 | Indent Mode should allow this intermediate state:
37 |
38 | ```in
39 | (let [a 1
40 | |] (+ a 2))
41 | ```
42 |
43 | ```out
44 | (let [a 1
45 | |] (+ a 2))
46 | ```
47 |
48 | Moving the cursor away will cause Indent Mode to still detect the leading
49 | close-paren, exit to Paren Mode, then fix the spacing to prevent inadvertent
50 | breakage.
51 |
52 | ```in-disable
53 | (let [a 1
54 | ] (+ a 2))
55 | ```
56 |
57 | ```out-disable
58 | (let [a 1]
59 | (+ a 2))
60 | ```
61 |
62 | **NOTE: Parinferish behaves differently.** It does not change any code when the cursor moves:
63 |
64 | To prevent weird things, indentation needs to be locked to respect
65 | the leading close-paren. Exiting to Paren Mode allows this and prevents further
66 | AST breakage.
67 |
68 | ```in
69 | (let [a 1
70 | |] (+ a 2))
71 | ```
72 |
73 | ```out-disable
74 | (let [a 1
75 | |] (+ a 2))
76 | ```
77 |
78 | **NOTE: Parinferish behaves differently.** It will not allow the end bracket to be there:
79 |
80 | ```out
81 | (let [a 1]
82 | |(+ a 2))
83 | ```
84 |
85 | Moving cursor to the right progressively moves leading close-parens behind it
86 | to their normal positions:
87 |
88 | ```in
89 | (let [a 1
90 | ]|)
91 | ```
92 |
93 | ```out-disable
94 | (let [a 1]
95 | |)
96 | ```
97 |
98 | **NOTE: Parinferish behaves differently.** It does not move the end bracket:
99 |
100 | ```out
101 | (let [a 1
102 | ]|)
103 | ```
104 |
105 | When in Paren Mode we must abide by its rules to stay balanced.
106 |
107 | As a courtesy, unmatched close-parens in a paren trail at the beginning of a
108 | line are auto-removed (only when paren mode is triggered from smart mode).
109 |
110 | ```in
111 | |)
112 | ```
113 |
114 | ```out
115 | |
116 | ```
117 |
118 | ```in
119 | (foo
120 | |))
121 | ```
122 |
123 | ```out
124 | (foo
125 | |)
126 | ```
127 |
128 | ```in
129 | (foo
130 | }|)
131 | ```
132 |
133 | ```out
134 | (foo
135 | |)
136 | ```
137 |
138 | Likewise:
139 |
140 | ```in
141 | (foo
142 | ) foo} bar|
143 | ```
144 |
145 | ```out-disable
146 | (foo
147 | ) foo} bar|
148 | ^ error: unmatched-close-paren
149 | ```
150 |
151 | **NOTE: Parinferish behaves differently.** It removes the invalid end bracket:
152 |
153 | ```out
154 | (foo
155 | ) foo bar|
156 | ```
157 |
158 | ```in
159 | (foo
160 | ) (bar|
161 | ```
162 |
163 | ```out-disable
164 | (foo
165 | ) (bar|
166 | ^ error: unclosed-paren
167 | ```
168 |
169 | **NOTE: Parinferish behaves differently.** It adds the necessary paren:
170 |
171 | ```out
172 | (foo
173 | ) (bar|)
174 | ```
175 |
176 |
177 | ## Changes
178 |
179 | Indent a single-line expression to enter a sibling:
180 |
181 | ```in
182 | (foo (bar)
183 | |baz)
184 | ```
185 |
186 | ```out
187 | (foo (bar
188 | |baz))
189 | ```
190 |
191 | Dedent multi-line expression to leave its parent:
192 |
193 | ```in
194 | (foo
195 | |{:a 1
196 | :b 2})
197 | ```
198 |
199 | ```out-disable
200 | (foo)
201 | {:a 1
202 | :b 2}
203 | ```
204 |
205 | **NOTE: Parinferish behaves differently.** It does not maintain relative indentation:
206 |
207 | ```out
208 | (foo)
209 | {:a 1
210 | :b 2}
211 | ```
212 |
213 | Indent multi-line expression to enter new parent:
214 |
215 | ```in
216 | (foo)
217 | |{:a 1
218 | :b 2}
219 | ```
220 |
221 | ```out
222 | (foo
223 | {:a 1
224 | :b 2})
225 | ```
226 |
227 | Dedenting an inner line makes it leave parent:
228 |
229 | ```in
230 | (foo
231 | {:a 1
232 | |:b 2})
233 | ```
234 |
235 | ```out
236 | (foo
237 | {:a 1})
238 | :b 2
239 | ```
240 |
241 | Dedenting a collection will adopt a former sibling line below it:
242 |
243 | ```in
244 | (defn foo
245 | |[a b]
246 | bar)
247 | ```
248 |
249 | ```out-disable
250 | (defn foo)
251 | [a b
252 | bar]
253 | ```
254 |
255 | **NOTE: Parinferish behaves differently.** It doesn't perform indent mode on collections starting after the cursor:
256 |
257 | ```out
258 | (defn foo)
259 | [a b]
260 | bar
261 | ```
262 |
263 | But dedenting a top-level form should not cause a child to adopt a sibling:
264 |
265 | ```in
266 | |(defn foo
267 | [a b]
268 | bar)
269 | ```
270 |
271 | ```out-disable
272 | (defn foo
273 | [a b]
274 | bar)
275 | ```
276 |
277 | **NOTE: Parinferish behaves differently.** It does not maintain relative indentation:
278 |
279 | ```out
280 | (defn foo
281 | [a b]
282 | bar)
283 | ```
284 |
285 | Indented comments move with expressions:
286 |
287 | ```in
288 | |(defn foo
289 | [a b]
290 | ; comment 1
291 | bar)
292 | ; comment 2
293 | ```
294 |
295 | ```out-disable
296 | (defn foo
297 | [a b]
298 | ; comment 1
299 | bar)
300 | ; comment 2
301 | ```
302 |
303 | **NOTE: Parinferish behaves differently.** It does not maintain relative indentation:
304 |
305 | ```out
306 | (defn foo
307 | [a b]
308 | ; comment 1
309 | bar)
310 | ; comment 2
311 | ```
312 |
313 | ## Cursor temporarily preventing sibling adoption
314 |
315 | To prevent undesirable sibling adoption when dedenting, we temporarily keep
316 | a close-paren from moving when the cursor is to the left of its open-paren.
317 |
318 | ```in
319 | (defn foo
320 | |[a b
321 | c d]
322 | bar
323 | baz)
324 | ```
325 |
326 | ```out-disable
327 | (defn foo)
328 | |[a b
329 | c d]
330 | bar
331 | baz
332 | ```
333 |
334 | **NOTE: Parinferish behaves differently.** It does not maintain relative indentation:
335 |
336 | ```out
337 | (defn foo)
338 | |[a b
339 | c d]
340 | bar
341 | baz
342 | ```
343 |
344 | ```in
345 | (defn foo)
346 | |[a b
347 | c d]
348 | bar
349 | baz
350 | ```
351 |
352 | ```out
353 | (defn foo)
354 | |[a b
355 | c d]
356 | bar
357 | baz
358 | ```
359 |
360 | ## Multiple Changes
361 |
362 | ```in-disable
363 | (my-fnfoo (if some-condition
364 | -----+++
365 | println) my-funfoo {:foo 1
366 | ------+++
367 | :bar 2})
368 | ```
369 |
370 | ```out-disable
371 | (foo (if some-condition
372 | println) foo {:foo 1
373 | :bar 2})
374 | ```
375 |
376 | ## Resolving Precarious Paren After Dedent
377 |
378 | In the example below, we expect `4` to not be adopted
379 | by any collection inside `(((1 2 3)))`.
380 |
381 | ```in
382 | (|(((1
383 | 2
384 | 3)))
385 | 4)
386 | ```
387 |
388 | ```out-disable
389 | (|(((1
390 | 2
391 | 3)))
392 | 4)
393 | ```
394 |
395 | **NOTE: Parinferish behaves differently.** It does not maintain relative indentation:
396 |
397 | ```out
398 | (|(((1
399 | 2
400 | 3)))
401 | 4)
402 | ```
403 |
404 | When cursor is removed, the precarious parens are resolved by preserving structure
405 | and correcting indentation.
406 |
407 | ```in
408 | (|(((1
409 | 2
410 | 3)))
411 | 4)
412 | ```
413 |
414 | ```out-disable
415 | ((((1
416 | 2
417 | 3)))
418 | 4)
419 | ```
420 |
421 | **NOTE: Parinferish behaves differently.** It doesn't perform paren mode on collections starting before the cursor:
422 |
423 | ```out
424 | ((((1
425 | 2
426 | 3)))
427 | 4)
428 | ```
429 |
430 | ```in
431 | ((|((1
432 | 2
433 | 3)))
434 | 4)
435 | ```
436 |
437 | ```out-disable
438 | ((|((1
439 | 2
440 | 3)))
441 | 4)
442 | ```
443 |
444 | **NOTE: Parinferish behaves differently.** It doesn't perform paren mode on collections starting before the cursor:
445 |
446 | ```out
447 | ((|((1
448 | 2
449 | 3))
450 | 4))
451 | ```
452 |
453 | ## Indenting Selected Lines
454 |
455 | Indent only the first line:
456 |
457 | ```in
458 | |(foo
459 | (bar
460 | baz))
461 | ```
462 |
463 | ```out-disable
464 | (foo
465 | (bar
466 | baz))
467 | ```
468 |
469 | **NOTE: Parinferish behaves differently.** It only corrects indentation with the minimum number of spaces:
470 |
471 | ```out
472 | (foo
473 | (bar
474 | baz))
475 | ```
476 |
477 | **NOTE: The rest of the tests below are currently disabled for Parinferish.**
478 |
479 | Indent first two lines:
480 |
481 | ```in-disable
482 | (foo
483 | ++
484 | (bar
485 | ++
486 | baz))
487 | ```
488 |
489 | ```out-disable
490 | (foo
491 | (bar
492 | baz))
493 | ```
494 |
495 | Indent last two lines:
496 |
497 | ```in-disable
498 | (foo
499 | (bar
500 | ++
501 | baz))
502 | ++
503 | ```
504 |
505 | ```out-disable
506 | (foo
507 | (bar
508 | baz))
509 | ```
510 |
511 |
512 | Indent only the first line:
513 |
514 | ```in-disable
515 | |(foo
516 | bar
517 | baz)
518 | ```
519 |
520 | ```out-disable
521 | (foo
522 | bar
523 | baz)
524 | ```
525 |
526 | Indent first two lines:
527 |
528 | ```in-disable
529 | (foo
530 | ++
531 | bar
532 | ++
533 | baz)
534 | ```
535 |
536 | ```out-disable
537 | (foo
538 | bar
539 | baz)
540 | ```
541 |
542 | Indent last two lines:
543 |
544 | ```in-disable
545 | (foo
546 | bar
547 | ++
548 | baz)
549 | ++
550 | ```
551 |
552 | ```out-disable
553 | (foo
554 | bar
555 | baz)
556 | ```
557 |
558 | ## Multi-change Bug
559 |
560 | [Issue #173](https://github.com/shaunlebron/parinfer/issues/173)
561 |
562 | ```in-disable
563 | ((reduce-kv (fn [m k v]
564 | +
565 | {}
566 | +
567 | {}))
568 | +
569 | ```
570 |
571 | ```in-disable
572 | ((reduce-kv (fn [m k v]
573 | {}
574 | {})))
575 | +
576 | ```
577 |
578 | ```out-disable
579 | ((reduce-kv (fn [m k v])
580 | {}
581 | {}))
582 | ```
583 |
584 | [Issue #176](https://github.com/shaunlebron/parinfer/issues/176)
585 |
586 | ```in-disable
587 | (let [a 1]
588 | (
589 | +
590 | (foo))
591 | ++
592 | ```
593 |
594 | ```in-disable
595 | (let [a 1]
596 | (
597 | (foo)))
598 | +
599 | ```
600 |
601 | ```out-disable
602 | (let [a 1]
603 | (
604 | (foo)))
605 | ```
606 |
607 | [Issue #177](https://github.com/shaunlebron/parinfer/issues/177)
608 |
609 | ```in-disable
610 | (let [a 1]
611 |
612 | (foo))
613 | ```
614 |
615 | ```in-disable
616 | (let [a 1]
617 | (let [a 1]
618 | +++++++++++
619 | (foo))
620 | ++++++++
621 | (foo))
622 | ```
623 |
624 | ```in-disable
625 | (let [a 1]
626 | (let [a 1]
627 | (foo))
628 | ++
629 | (foo))
630 | ```
631 |
632 | ```out-disable
633 | (let [a 1]
634 | (let [a 1]
635 | (foo))
636 | (foo))
637 | ```
638 |
639 | [Issue #179](https://github.com/shaunlebron/parinfer/issues/179)
640 |
641 | ```in-disable
642 | {:a {:b (Integer/valueOf (-> ""
643 | ----------------
644 | (.length)))}}
645 | ```
646 |
647 | ```in-disable
648 | {:a {:b (Integer/valueOf (-> ""
649 | -------------
650 | (.length)))}}
651 | -----------------------------
652 | ```
653 |
654 | ```out-disable
655 | {:a {:b (Integer/valueOf (-> ""
656 | (.length)))}}
657 | ```
658 |
--------------------------------------------------------------------------------
/test-resources/paren-mode.md:
--------------------------------------------------------------------------------
1 | # Paren Mode
2 |
3 | ## Indentation Threshold Clamping
4 |
5 | clamp left
6 |
7 | ```in
8 | (let [foo 1]
9 | foo)
10 | ```
11 |
12 | ```out
13 | (let [foo 1]
14 | foo)
15 | ```
16 |
17 | clamp right
18 |
19 | ```in
20 | (let [foo 1]
21 | foo)
22 | ```
23 |
24 | ```out
25 | (let [foo 1]
26 | foo)
27 | ```
28 |
29 | ```in
30 | (let [foo {:a 1}]
31 | foo)
32 | ```
33 |
34 | ```out
35 | (let [foo {:a 1}]
36 | foo)
37 | ```
38 |
39 | sanity check to apply rules to multiple expressions
40 |
41 | ```in
42 | (let [foo 1]
43 | foo)
44 |
45 | (let [foo 1]
46 | foo)
47 | ```
48 |
49 | ```out
50 | (let [foo 1]
51 | foo)
52 |
53 | (let [foo 1]
54 | foo)
55 | ```
56 |
57 | ## Relative Indentation
58 |
59 | keep relative indentation of child expressions:
60 |
61 | ```in
62 | (let [foo [1 2 3]]
63 | (-> foo
64 | (map inc)))
65 | ```
66 |
67 | ```out-disable
68 | (let [foo [1 2 3]]
69 | (-> foo
70 | (map inc)))
71 | ```
72 |
73 | **NOTE: Parinferish behaves differently.** It does not maintain relative indentation:
74 |
75 | ```out
76 | (let [foo [1 2 3]]
77 | (-> foo
78 | (map inc)))
79 | ```
80 |
81 | ## Leading Close-Paren
82 |
83 |
84 | Close-parens at start of line are moved to end of previous line. Note that the
85 | spaces before the close-paren are not removed.
86 |
87 | ```in
88 | (let [foo 1
89 | ]; <-- spaces
90 | foo)
91 | ```
92 |
93 | ```out-disable
94 | (let [foo 1]
95 | ; <-- spaces
96 | foo)
97 | ```
98 |
99 | **NOTE: Parinferish behaves differently.** It does not move the paren:
100 |
101 | ```out
102 | (let [foo 1
103 | ]; <-- spaces
104 | foo)
105 | ```
106 |
107 | ```in
108 | (let [foo 1
109 | bar 2
110 |
111 | ] (+ foo bar
112 | ); <-- spaces
113 | )
114 | ```
115 |
116 | ```out-disable
117 | (let [foo 1
118 | bar 2]
119 |
120 | (+ foo bar))
121 | ; <-- spaces
122 |
123 | ```
124 |
125 | **NOTE: Parinferish behaves differently.** It only changes indentation:
126 |
127 | ```out
128 | (let [foo 1
129 | bar 2
130 |
131 | ] (+ foo bar
132 | ); <-- spaces
133 | )
134 | ```
135 |
136 | ## Inside the Indentation Treshold
137 |
138 | extra indent is fine (won't cause parinfer to restructure it)
139 |
140 | ```in
141 | (def x [1 2 3 4
142 | 5 6 7 8])
143 | ```
144 |
145 | ```out
146 | (def x [1 2 3 4
147 | 5 6 7 8])
148 | ```
149 |
150 | doesn't try to align siblings.
151 |
152 | ```in
153 | (assoc x
154 | :foo 1
155 | :bar 2)
156 | ```
157 |
158 | ```out
159 | (assoc x
160 | :foo 1
161 | :bar 2)
162 | ```
163 |
164 | ## Unclosed Parens
165 |
166 | ```in
167 | (foo
168 | ```
169 |
170 | ```out
171 | (foo
172 | ^ error: unclosed-paren
173 | ```
174 |
175 | ```in
176 | (defn foo
177 | [arg arg2
178 | bar
179 | ```
180 |
181 | ```out
182 | (defn foo
183 | [arg arg2
184 | ^ error: unclosed-paren
185 | bar
186 | ```
187 |
188 | ## Unmatched close-parens
189 |
190 | ```in
191 | (foo})
192 | ```
193 |
194 | ```out
195 | (foo})
196 | ^ error: unmatched-close-paren
197 | ```
198 |
199 | ```in
200 | (foo
201 | })
202 | ```
203 |
204 | ```out
205 | (foo
206 | })
207 | ^ error: unmatched-close-paren
208 | ```
209 |
210 | ```in
211 | (defn foo
212 | [arg
213 | bar)
214 | ```
215 |
216 | ```out
217 | (defn foo
218 | [arg
219 | bar)
220 | ^ error: unmatched-close-paren
221 | ```
222 |
223 | ## Backslash cases
224 |
225 | escape character in comment untouched:
226 |
227 | ```in
228 | ; hello \n world
229 | ```
230 |
231 | ```out
232 | ; hello \n world
233 | ```
234 |
235 | escaped whitespace
236 |
237 | ```in
238 | (def foo \,)
239 | (def bar \ )
240 | ```
241 |
242 | ```out
243 | (def foo \,)
244 | (def bar \ )
245 | ```
246 |
247 | Hanging backslash at end of line is invalid and causes processing to be abandoned.
248 |
249 | ```in
250 | (foo [a b]\
251 | c)
252 | ```
253 |
254 | ```out
255 | (foo [a b]\
256 | ^ error: eol-backslash
257 | c)
258 | ```
259 |
260 | ## Unclosed Quotes
261 |
262 | ```in
263 | (def foo
264 | "hello
265 | bar)
266 | ```
267 |
268 | ```out
269 | (def foo
270 | "hello
271 | ^ error: unclosed-quote
272 | bar)
273 | ```
274 |
275 | ## Dangerous Quotes
276 |
277 | odd number of quotes not allowed in a comment, so it remains unprocessed:
278 |
279 | ```in
280 | (def foo [a b]
281 | ; "my string
282 | ret)
283 | ```
284 |
285 | ```out-disable
286 | (def foo [a b]
287 | ; "my string
288 | ^ error: quote-danger
289 | ret)
290 | ```
291 |
292 | **NOTE: Parinferish behaves differently.** It does not turn itself off:
293 |
294 | ```out
295 | (def foo [a b]
296 | ; "my string
297 | ret)
298 | ```
299 |
300 | balanced quotes allowed across contiguous comments:
301 |
302 | ```in
303 | (def foo [a b]
304 | ; "my multiline
305 | ; docstring."
306 | ret)
307 | ```
308 |
309 | ```out
310 | (def foo [a b]
311 | ; "my multiline
312 | ; docstring."
313 | ret)
314 | ```
315 |
316 | ## Multiline Strings
317 |
318 | A line ending inside a string will not have a definable Paren Trail. This
319 | minimal test case will fail if the close-paren is treated as a Paren Trail.
320 |
321 | ```in
322 | ( )"
323 | "
324 | ```
325 |
326 | ```out
327 | ( )"
328 | "
329 | ```
330 |
331 | ## Spaces in Paren Trail
332 |
333 | Preserve spaces in paren trail for the cursor line. This allows the user
334 | to insert things between close-parens more easily.
335 |
336 | ```in
337 | (foo |)
338 | ```
339 |
340 | ```out
341 | (foo |)
342 | ```
343 |
344 | ```in
345 | (foo [1 2 3 |] )
346 | ```
347 |
348 | ```out
349 | (foo [1 2 3 |] )
350 | ```
351 |
352 | But get rid of spaces in paren trail if no cursor is present on the line:
353 |
354 | ```in
355 | (foo )
356 | ```
357 |
358 | ```out-disable
359 | (foo)
360 | ```
361 |
362 | **NOTE: Parinferish behaves differently.** It preserves the whitespace:
363 |
364 | ```out
365 | (foo )
366 | ```
367 |
368 | ```in
369 | (foo [1 2 3 ] )
370 | ```
371 |
372 | ```out-disable
373 | (foo [1 2 3])
374 | ```
375 |
376 | **NOTE: Parinferish behaves differently.** It preserves the whitespace:
377 |
378 | ```out
379 | (foo [1 2 3 ] )
380 | ```
381 |
382 | ## Cursor Behavior
383 |
384 | Pressing enter before a close-paren.
385 |
386 | ```in
387 | (foo [a b
388 | |])
389 | ```
390 |
391 | ```out
392 | (foo [a b
393 | |])
394 | ```
395 |
396 | Cursor pushed forward when a form is balanced and indented.
397 |
398 | ```in
399 | (foo [1 2 3
400 | 4 5 6
401 | 7 8 9])|
402 | ```
403 |
404 | ```out
405 | (foo [1 2 3
406 | 4 5 6
407 | 7 8 9])|
408 | ```
409 |
410 | ## Changes
411 |
412 | When backspacing, preserve the indentation of the child lines.
413 |
414 | ```in-disable
415 | (let [foo 1
416 | ----
417 | ; comment 1
418 | bar 2
419 | baz 3])
420 | ; comment 2
421 | ```
422 |
423 | ```out-disable
424 | (let [foo 1
425 | ; comment 1
426 | bar 2
427 | baz 3])
428 | ; comment 2
429 | ```
430 |
431 | **NOTE: Parinferish behaves differently.** It does not preserve the relative indentation:
432 |
433 | ```in
434 | (let [foo 1
435 | ; comment 1
436 | bar 2
437 | baz 3])
438 | ; comment 2
439 | ```
440 |
441 | ```out
442 | (let [foo 1
443 | ; comment 1
444 | bar 2
445 | baz 3])
446 | ; comment 2
447 | ```
448 |
449 | ```in-disable
450 | (def foo
451 | ---
452 | ; comment 1
453 | bar)
454 | ; comment 2
455 | ```
456 |
457 | ```out-disable
458 | (def foo
459 | ; comment 1
460 | bar)
461 | ; comment 2
462 | ```
463 |
464 | **NOTE: Parinferish behaves differently.** It does not preserve the relative indentation:
465 |
466 | ```in
467 | (def foo
468 | ; comment 1
469 | bar)
470 | ; comment 2
471 | ```
472 |
473 | ```out
474 | (def foo
475 | ; comment 1
476 | bar)
477 | ; comment 2
478 | ```
479 |
480 | When typing before an open-paren, preserve the indentation of the child lines.
481 |
482 | ```in-disable
483 | (def foo (bar
484 | ++++
485 | 4 5 6
486 | ; comment 1
487 | 7 8 9))
488 | ; comment 2
489 | ```
490 |
491 | ```out-disable
492 | (def foo (bar
493 | 4 5 6
494 | ; comment 1
495 | 7 8 9))
496 | ; comment 2
497 | ```
498 |
499 | **NOTE: Parinferish behaves differently.** It does not preserve the relative indentation:
500 |
501 | ```in
502 | (def foo (bar
503 | 4 5 6
504 | ; comment 1
505 | 7 8 9))
506 | ; comment 2
507 | ```
508 |
509 | ```out
510 | (def foo (bar
511 | 4 5 6
512 | ; comment 1
513 | 7 8 9))
514 | ; comment 2
515 | ```
516 |
517 | __Multiple changes__:
518 |
519 | ```in-disable
520 | (my-fnfoo (if some-condition
521 | -----+++
522 | println) my-funfoo {:foo 1
523 | ------+++
524 | :bar 2})
525 | ```
526 |
527 | ```out-disable
528 | (foo (if some-condition
529 | println) foo {:foo 1
530 | :bar 2})
531 | ```
532 |
533 | **NOTE: Parinferish behaves differently.** It does not preserve the relative indentation:
534 |
535 | ```in
536 | (foo (if some-condition
537 | println) foo {:foo 1
538 | :bar 2})
539 | ```
540 |
541 | ```out
542 | (foo (if some-condition
543 | println) foo {:foo 1
544 | :bar 2})
545 | ```
546 |
547 | ## Extending indentation constraints
548 |
549 | Child lines should never fall to the right of a sibling list somewhere above it.
550 | This promotes more structural stability in Indent Mode—allowing child lines
551 | to be deleted without affecting structure of subsequent lines.
552 |
553 | In the example below, the original Paren Mode would dedent `1 2 3`, but would
554 | leave `4 5 6` unchanged. Subsequently, if the user deletes `1 2 3` in Indent
555 | Mode, `4 5 6` would immediately be adopted into the `[bar baz]` collection,
556 | which is likely an unintended side effect.
557 |
558 | This places a harder constraint on indentation in Paren Mode than Indent Mode,
559 | making the invariant no longer equivalent between them.
560 |
561 | ```in
562 | (foo [bar baz]
563 | 1 2 3
564 | 4 5 6)
565 | ```
566 |
567 | ```out
568 | (foo [bar baz]
569 | 1 2 3
570 | 4 5 6)
571 | ```
572 |
573 | ```in
574 | (foo [bar baz
575 | ]; <-- spaces
576 | 1 2 3
577 | 4 5 6)
578 | ```
579 |
580 | ```out-disable
581 | (foo [bar baz]
582 | ; <-- spaces
583 | 1 2 3
584 | 4 5 6)
585 | ```
586 |
587 | **NOTE: Parinferish behaves differently.** It does not move the bracket:
588 |
589 | ```out
590 | (foo [bar baz
591 | ]; <-- spaces
592 | 1 2 3
593 | 4 5 6)
594 | ```
595 |
596 | ## Paren Trails
597 |
598 | We return non-empty Paren Trails so plugins can dim them with markers:
599 |
600 |
601 | ```in-disable
602 | (defn foo
603 | "hello, this is a docstring"
604 | [a b]
605 | (let [sum (+ a b)
606 | prod (* a b)]
607 | {:sum sum
608 | :prod prod}))
609 | ```
610 |
611 | ```out-disable
612 | (defn foo
613 | "hello, this is a docstring"
614 | [a b]
615 | ^ parenTrail
616 | (let [sum (+ a b)
617 | ^ parenTrail
618 | prod (* a b)]
619 | ^^ parenTrail
620 | {:sum sum
621 | :prod prod}))
622 | ^^^ parenTrail
623 | ```
624 |
625 | ## Indenting Selected Lines
626 |
627 | Indent only the first line:
628 |
629 | ```in-disable
630 | (foo
631 | ++
632 | (bar
633 | baz))
634 | ```
635 |
636 | ```out-disable
637 | (foo
638 | (bar
639 | baz))
640 | ```
641 |
642 | Indent first two lines:
643 |
644 | ```in-disable
645 | (foo
646 | ++
647 | (bar
648 | ++
649 | baz))
650 | ```
651 |
652 | ```out-disable
653 | (foo
654 | (bar
655 | baz))
656 | ```
657 |
658 | Indent last two lines:
659 |
660 | ```in-disable
661 | (foo
662 | (bar
663 | ++
664 | baz))
665 | ++
666 | ```
667 |
668 | ```out-disable
669 | (foo
670 | (bar
671 | baz))
672 | ```
673 |
674 |
675 | Indent only the first line:
676 |
677 | ```in-disable
678 | (foo
679 | ++
680 | bar
681 | baz)
682 | ```
683 |
684 | ```out-disable
685 | (foo
686 | bar
687 | baz)
688 | ```
689 |
690 | Indent first two lines:
691 |
692 | ```in-disable
693 | (foo
694 | ++
695 | bar
696 | ++
697 | baz)
698 | ```
699 |
700 | ```out-disable
701 | (foo
702 | bar
703 | baz)
704 | ```
705 |
706 | Indent last two lines:
707 |
708 | ```in-disable
709 | (foo
710 | bar
711 | ++
712 | baz)
713 | ++
714 | ```
715 |
716 | ```out-disable
717 | (foo
718 | bar
719 | baz)
720 | ```
721 |
722 | ## Tab Stops
723 |
724 | > __NOTE__: should be copied from same section in Indent Mode
725 |
726 | We can return the positions of the open-parens whose structure would be
727 | affected by the indentation of the current cursor line. This allows editors to
728 | use them to create tab stops for smart indentation snapping.
729 |
730 | ```in-disable
731 | (def x [1 2 3])
732 | (def y 2)
733 | |
734 | ```
735 |
736 | ```out-disable
737 | (def x [1 2 3])
738 | (def y 2)
739 | ^ > tabStops
740 | |
741 | ```
742 |
743 | The `>` means the position of the first arg after an open-paren, because some styles
744 | use it for alignment.
745 |
746 | ```in-disable
747 | (foo bar
748 | (baz boo))
749 | |
750 | ```
751 |
752 | ```out-disable
753 | (foo bar
754 | (baz boo))
755 | ^ ^ > tabStops
756 | |
757 | ```
758 |
759 | ```in-disable
760 | (let [a {:foo 1}
761 | |
762 | bar [1 2 3]]
763 | bar)
764 | ```
765 |
766 | ```out-disable
767 | (let [a {:foo 1}
768 | ^ ^ ^ > tabStops
769 | |
770 | bar [1 2 3]]
771 | bar)
772 | ```
773 |
774 | ```in-disable
775 | (let [a {:foo 1}
776 | bar (func 1 2 3)]
777 | |
778 | bar)
779 | ```
780 |
781 | ```out-disable
782 | (let [a {:foo 1}
783 | bar (func 1 2 3)]
784 | ^ ^ ^ > tabStops
785 | |
786 | bar)
787 | ```
788 |
--------------------------------------------------------------------------------
/src/parinferish/core.cljc:
--------------------------------------------------------------------------------
1 | (ns parinferish.core
2 | (:require [clojure.string :as str])
3 | (:refer-clojure :exclude [flatten]))
4 |
5 | (def ^:private regexes
6 | [[:newline-and-indent #"^\n[ ]*"]
7 | [:whitespace #"^[ \t\r,]+"]
8 | [:special-char #"^['`~^@]"]
9 | [:delimiter #"^[\[\]{}()]"]
10 | [:delimiter #"^#\{"]
11 | ;; the next regex only matches the beginning of a string.
12 | ;; the rest is read by the parse-string function below.
13 | ;; parsing entire strings via regex was causing a stack
14 | ;; overflow error, so i'm doing it manually instead
15 | [:string #"^\""]
16 | [:character #"^\\\S"]
17 | [:backslash #"^\\"]
18 | [:comment #"^;.*"]
19 | [:number #"^[+-]?\d+[/\.]?[a-zA-Z\d]*"]
20 | [:symbol #"^[^\s\[\]{}('\"`,;)\\]+"]])
21 |
22 | (def ^:private whitespace?
23 | #{:newline-and-indent :whitespace :comment
24 | ;; a lone backslash is not actually whitespace,
25 | ;; but it's here to prevent it from being immediately
26 | ;; followed by an end paren, since that would cause it
27 | ;; to be escaped and turned into a character
28 | :backslash})
29 |
30 | (def ^:private open-delims #{"#{" "(" "[" "{"})
31 | (def ^:private close-delims #{"}" ")" "]"})
32 | (def ^:private delims {"#{" "}"
33 | "(" ")"
34 | "[" "]"
35 | "{" "}"})
36 |
37 | (defn- count-newlines [s]
38 | (let [*counter (volatile! 0)]
39 | (doseq [ch s]
40 | (when (= ch \newline)
41 | (vswap! *counter inc)))
42 | @*counter))
43 |
44 | (defn- count-last-line [s]
45 | (when-let [index (str/last-index-of s \newline)]
46 | (count (subs s (inc index)))))
47 |
48 | (defn- read-token [group-name token *error *line *column *indent last-token]
49 | (let [token-data [(if (and (= group-name :symbol)
50 | (str/starts-with? token ":"))
51 | :keyword
52 | group-name)
53 | token]
54 | line (cond
55 | (= group-name :newline-and-indent) (vswap! *line inc)
56 | (= group-name :string) (vswap! *line + (count-newlines token))
57 | :else @*line)
58 | start-column (if (= group-name :newline-and-indent)
59 | -1
60 | @*column)
61 | end-column (cond
62 | (= group-name :newline-and-indent)
63 | (vreset! *column (dec (count token)))
64 | (= group-name :string)
65 | ;; multiline strings need to reset the column number correctly
66 | (if-let [n (count-last-line token)]
67 | (vreset! *column n)
68 | (vswap! *column + (count token)))
69 | :else
70 | (vswap! *column + (count token)))
71 | indent (cond
72 | (and (= :delimiter group-name)
73 | (open-delims token))
74 | (vreset! *indent end-column)
75 | (= group-name :newline-and-indent)
76 | (vreset! *indent (dec (count token)))
77 | :else
78 | @*indent)
79 | token-data (vary-meta token-data assoc
80 | :line line
81 | :column start-column
82 | :indent indent)]
83 | (cond-> token-data
84 | (whitespace? group-name)
85 | (vary-meta assoc :whitespace? true)
86 | (and (= group-name :string)
87 | (not (str/ends-with? token "\"")))
88 | (vary-meta assoc :error-message (vreset! *error "Unbalanced quote"))
89 | (and (= group-name :newline-and-indent)
90 | (= (first last-token) :backslash))
91 | (vary-meta assoc :error-message (vreset! *error "Backslash at end of line")))))
92 |
93 | (declare read-structured-token)
94 |
95 | (defn- wrap-coll [data]
96 | (let [first-meta (-> data first meta)]
97 | (vary-meta (into [:collection] data)
98 | assoc :indent (:indent first-meta))))
99 |
100 | (defn- insert-token [data token-data]
101 | (conj data (vary-meta token-data assoc :action :insert)))
102 |
103 | (defn- remove-token [data token-data]
104 | (conj data (vary-meta token-data assoc :action :remove)))
105 |
106 | (defn- insert-delim [data end-delim]
107 | (let [first-meta (-> data first meta)
108 | token-data (vary-meta [:delimiter end-delim] assoc
109 | :indent (:indent first-meta)
110 | :action :insert)
111 | [last-data first-data]
112 | (->> data
113 | reverse
114 | (split-with #(-> % first whitespace?))
115 | (mapv reverse)
116 | (mapv vec))]
117 | (into (conj first-data token-data)
118 | last-data)))
119 |
120 | (defn- read-next-tokens-with-indent [flat-tokens {:keys [*index] :as opts}]
121 | (loop [data []
122 | ignore-data []
123 | last-index @*index
124 | new-line? false]
125 | (if-let [[group _ :as token-data] (read-structured-token flat-tokens opts)]
126 | (cond
127 | (= :delimiter group)
128 | (recur
129 | data
130 | (conj ignore-data (vary-meta token-data assoc :action :remove))
131 | last-index
132 | new-line?)
133 | (whitespace? group)
134 | (recur
135 | data
136 | (conj ignore-data token-data)
137 | last-index
138 | (or new-line? (= group :newline-and-indent)))
139 | :else
140 | (if new-line?
141 | (recur
142 | (-> data
143 | (into ignore-data)
144 | (conj token-data))
145 | []
146 | @*index
147 | new-line?)
148 | (recur
149 | data
150 | (conj ignore-data token-data)
151 | last-index
152 | new-line?)))
153 | (do
154 | (vreset! *index last-index)
155 | data))))
156 |
157 | (defn- read-coll-indent-mode [flat-tokens [_ delim :as token-data] {:keys [*index] :as opts}]
158 | (let [end-delim (delims delim)
159 | indent (-> token-data meta :indent)
160 | opts (assoc opts :min-indent indent)]
161 | (loop [data [token-data]
162 | whitespace-data []
163 | last-index @*index]
164 | (if-let [[group token :as token-data] (read-structured-token flat-tokens opts)]
165 | (cond
166 | (whitespace? group)
167 | (recur data (conj whitespace-data token-data) last-index)
168 | (< (-> token-data meta :indent) indent)
169 | (do
170 | (vreset! *index last-index)
171 | (-> data
172 | (insert-delim end-delim)
173 | wrap-coll))
174 | (= :delimiter group)
175 | (if (= token end-delim)
176 | (let [tokens-to-move (read-next-tokens-with-indent flat-tokens opts)]
177 | (if (seq tokens-to-move)
178 | (-> data
179 | (into whitespace-data)
180 | (remove-token token-data)
181 | (into tokens-to-move)
182 | (insert-token token-data)
183 | wrap-coll)
184 | (-> data
185 | (into whitespace-data)
186 | (conj token-data)
187 | wrap-coll)))
188 | (recur
189 | (-> data
190 | (into whitespace-data)
191 | (remove-token token-data))
192 | []
193 | @*index))
194 | :else
195 | (recur
196 | (-> data
197 | (into whitespace-data)
198 | (conj token-data))
199 | []
200 | @*index))
201 | (do
202 | (vreset! *index last-index)
203 | (-> data
204 | (insert-delim end-delim)
205 | wrap-coll))))))
206 |
207 | (defn- read-coll-paren-mode [flat-tokens [_ delim :as token-data] {:keys [indent-change *error] :as opts}]
208 | (let [end-delim (delims delim)
209 | min-indent (cond-> (-> token-data meta :indent)
210 | indent-change
211 | (+ indent-change))
212 | opts (dissoc opts :min-indent)]
213 | (loop [data [token-data]
214 | max-indent nil
215 | indent-change (or indent-change 0)]
216 | (if-let [[group token :as token-data] (read-structured-token flat-tokens
217 | (assoc opts :indent-change indent-change))]
218 | (cond
219 | (= :newline-and-indent group)
220 | (let [current-indent (-> token-data meta :indent)
221 | indent-change (cond
222 | (< current-indent min-indent)
223 | (- min-indent current-indent)
224 | (and max-indent (> current-indent max-indent))
225 | (- max-indent current-indent)
226 | :else
227 | 0)]
228 | (recur
229 | (cond
230 | (pos? indent-change)
231 | (-> data
232 | (conj token-data)
233 | (conj (vary-meta [:whitespace (str/join (repeat indent-change " "))]
234 | assoc :action :insert :whitespace? true)))
235 | (neg? indent-change)
236 | (-> data
237 | (conj (update token-data 1 subs 0 (+ (count token) indent-change)))
238 | (conj (vary-meta [:whitespace (str/join (repeat (* -1 indent-change) " "))]
239 | assoc :action :remove :whitespace? true)))
240 | :else
241 | (conj data token-data))
242 | max-indent
243 | indent-change))
244 | (= :collection group)
245 | (recur
246 | (conj data token-data)
247 | (cond-> (-> token-data meta :indent dec)
248 | max-indent
249 | (min max-indent))
250 | indent-change)
251 | (= :delimiter group)
252 | (cond-> (wrap-coll (conj data token-data))
253 | (not= token end-delim)
254 | (vary-meta assoc :error-message (vreset! *error "Unmatched delimiter")))
255 | :else
256 | (recur (conj data token-data) max-indent indent-change))
257 | (vary-meta (wrap-coll data)
258 | assoc :error-message (vreset! *error "EOF while reading"))))))
259 |
260 | (defn- read-coll [flat-tokens [_ delim :as token-data] {:keys [*index *error] :as opts}]
261 | (let [end-delim (delims delim)]
262 | (loop [data [token-data]]
263 | (if-let [[group token :as token-data] (read-structured-token flat-tokens opts)]
264 | (if (= :delimiter group)
265 | (cond-> (wrap-coll (conj data token-data))
266 | (not= token end-delim)
267 | (vary-meta assoc :error-message (vreset! *error "Unmatched delimiter")))
268 | (recur (conj data token-data)))
269 | (vary-meta (wrap-coll data)
270 | assoc :error-message (vreset! *error "EOF while reading"))))))
271 |
272 | (defn- read-structured-token [flat-tokens {:keys [*index mode min-indent] :as opts}]
273 | (when-let [[group token :as token-data] (get flat-tokens (vswap! *index inc))]
274 | (when (or (nil? min-indent)
275 | (whitespace? group)
276 | (-> token-data meta :column (>= min-indent)))
277 | (if (and (= :delimiter group)
278 | (open-delims token))
279 | (case mode
280 | :indent (read-coll-indent-mode flat-tokens token-data opts)
281 | :paren (read-coll-paren-mode flat-tokens token-data opts)
282 | :smart (let [{:keys [cursor-line cursor-column]} opts
283 | {:keys [line column]} (meta token-data)]
284 | (if (or (< line cursor-line)
285 | (and (= line cursor-line)
286 | (< column cursor-column)))
287 | (read-coll-indent-mode flat-tokens token-data opts)
288 | (read-coll-paren-mode flat-tokens token-data opts)))
289 | nil (read-coll flat-tokens token-data opts))
290 | token-data))))
291 |
292 | (defn- read-useful-token [flat-tokens {:keys [mode *error] :as opts}]
293 | (when-let [[_ token :as token-data] (read-structured-token flat-tokens opts)]
294 | (cond
295 | (close-delims token)
296 | (if (#{:indent :smart} mode)
297 | (vary-meta token-data assoc :action :remove)
298 | (vary-meta token-data assoc :error-message (vreset! *error "Unmatched delimiter")))
299 | :else
300 | token-data)))
301 |
302 | (defn- parse-string [token input-str]
303 | (loop [token token
304 | input-str (subs input-str (count token))]
305 | (if-let [ch (first input-str)]
306 | (case ch
307 | \\
308 | (if (> (count input-str) 1)
309 | (recur (str token (subs input-str 0 2)) (subs input-str 2))
310 | (str token ch))
311 | \"
312 | (str token ch)
313 | (recur (str token ch) (subs input-str 1)))
314 | token)))
315 |
316 | (defn parse
317 | "Returns a hierarcichal (I can never spell that goddamn word) vector of tuples
318 | representing each distinct Clojure token from the input string. Takes an options
319 | map that allows you to apply parinferish via the :mode option (:indent, :paren, :smart).
320 | Note that if you use parinfer, the return value will include both the inserted and removed
321 | tokens. The tokens parinfer wants to remove will be removed by the `flatten` function."
322 | ([input]
323 | (parse input {}))
324 | ([input opts]
325 | (when (and (= :smart (:mode opts))
326 | (or (nil? (:cursor-line opts))
327 | (nil? (:cursor-column opts))))
328 | (throw (ex-info "Smart mode requires :cursor-line and :cursor-column" {})))
329 | (let [*error (volatile! nil)
330 | *line (volatile! 0)
331 | *column (volatile! 0)
332 | *indent (volatile! 0)
333 | tokens (loop [input-str input
334 | tokens (transient [])
335 | last-token nil]
336 | (if-let [[group-name token]
337 | (some (fn [[group-name regex]]
338 | (when-let [match (re-find regex input-str)]
339 | [group-name match]))
340 | regexes)]
341 | (let [token (if (= group-name :string)
342 | (parse-string token input-str)
343 | token)
344 | token-data (read-token group-name token *error *line *column *indent last-token)]
345 | (recur (subs input-str (count token)) (conj! tokens token-data) token-data))
346 | (persistent! tokens)))
347 | opts (assoc opts
348 | :*error *error
349 | :*index (volatile! -1))
350 | opts (cond-> opts @*error (dissoc :mode))]
351 | (loop [structured-tokens []]
352 | (if-let [token-data (read-useful-token tokens opts)]
353 | (recur (conj structured-tokens token-data))
354 | (let [{:keys [mode]} opts]
355 | (vary-meta structured-tokens
356 | assoc :mode mode :error? (some? @*error))))))))
357 |
358 | (defn- node-iter [node-fn nodes node disable-parinfer?]
359 | (if (= (first node) :collection)
360 | (->> (reduce
361 | (fn [v child]
362 | (node-iter node-fn v child disable-parinfer?))
363 | []
364 | (rest node))
365 | (into (with-meta [:collection] (meta node)))
366 | node-fn
367 | (conj nodes))
368 | (if (if disable-parinfer?
369 | (-> node meta :action (= :insert))
370 | (-> node meta :action (= :remove)))
371 | nodes
372 | (conj nodes (node-fn node)))))
373 |
374 | (defn flatten
375 | "Takes the result of `parse` and flattens it into a string. Optionally takes a
376 | `node-fn` which will receive each token and return whatever it wants, so you can
377 | have more control of what format it outputs to."
378 | ([parsed-code]
379 | (->> parsed-code
380 | (flatten #(-> % rest str/join))
381 | str/join))
382 | ([node-fn parsed-code]
383 | (let [m (meta parsed-code)
384 | disable-parinfer? (and (= :paren (:mode m))
385 | (:error? m))]
386 | (reduce
387 | (fn [v code]
388 | (into v (node-iter node-fn [] code disable-parinfer?)))
389 | []
390 | parsed-code))))
391 |
392 | (defn- diff-node [*line *column *diff parent-node node]
393 | (if (vector? node)
394 | (let [[type & children] node]
395 | (cond
396 | (= type :newline-and-indent)
397 | (do
398 | (vswap! *line inc)
399 | (vreset! *column -1))
400 | (= type :string)
401 | (vswap! *line + (count-newlines (first children))))
402 | (run! (partial diff-node *line *column *diff node) children))
403 | (let [line @*line
404 | column @*column
405 | parent-meta (meta parent-node)]
406 | (if (= :string (first parent-node))
407 | ;; multiline strings need to reset the column number correctly
408 | (if-let [n (count-last-line node)]
409 | (vreset! *column n)
410 | (vswap! *column + (count node)))
411 | (vswap! *column + (count node)))
412 | (when-let [action (:action parent-meta)]
413 | (let [last-diff (last @*diff)]
414 | ;; if we are removing the thing we just inserted,
415 | ;; the actions cancel each other out
416 | ;; so just remove both from the diff
417 | (if (and last-diff
418 | (= action :remove)
419 | (= (:action last-diff) :insert)
420 | (= line (:line last-diff))
421 | (= 1 (- column (:column last-diff)))
422 | (= node (:content last-diff)))
423 | (vswap! *diff pop)
424 | (vswap! *diff conj {:line line
425 | :column column
426 | :content node
427 | :action action
428 | :type (first parent-node)})))
429 | (when (= action :remove)
430 | (vswap! *column - (count node)))))))
431 |
432 | (defn diff
433 | "Takes the result of `parse` and returns a vector of maps describing
434 | each insertion or removal made by parinferish."
435 | [parsed-code]
436 | (let [*line (volatile! 0)
437 | *column (volatile! 0)
438 | *diff (volatile! [])
439 | m (meta parsed-code)
440 | disable-parinfer? (and (= :paren (:mode m))
441 | (:error? m))]
442 | (when-not disable-parinfer?
443 | (run! (partial diff-node *line *column *diff nil) parsed-code))
444 | @*diff))
445 |
446 |
--------------------------------------------------------------------------------
/test-resources/indent-mode.md:
--------------------------------------------------------------------------------
1 | # Indent Mode
2 |
3 | ## No Closers
4 |
5 | Most basic behavior can be described by leaving out close-parens.
6 |
7 | ```in
8 | (defn foo
9 | [arg
10 | ret
11 | ```
12 |
13 | ```out
14 | (defn foo
15 | [arg]
16 | ret)
17 | ```
18 |
19 | indenting line 3:
20 |
21 | ```in
22 | (defn foo
23 | [arg
24 | ret
25 | ```
26 |
27 | ```out
28 | (defn foo
29 | [arg
30 | ret])
31 | ```
32 |
33 | dedenting line 2:
34 |
35 | ```in
36 | (defn foo
37 | [arg
38 | ret
39 | ```
40 |
41 | ```out
42 | (defn foo)
43 | [arg
44 | ret]
45 | ```
46 |
47 | dedenting line 2 and 3:
48 |
49 | ```in
50 | (defn foo
51 | [arg
52 | ret
53 | ```
54 |
55 | ```out
56 | (defn foo)
57 | [arg]
58 | ret
59 | ```
60 |
61 | multiple functions:
62 |
63 | ```in
64 | (defn foo
65 | [arg
66 | ret
67 |
68 | (defn foo
69 | [arg
70 | ret
71 | ```
72 |
73 | ```out
74 | (defn foo
75 | [arg]
76 | ret)
77 |
78 | (defn foo
79 | [arg]
80 | ret)
81 | ```
82 |
83 | ## Bad Closers
84 |
85 | close-paren at end-of-line with no open-paren:
86 |
87 | ```in
88 | bar)
89 | ```
90 |
91 | ```out
92 | bar
93 | ```
94 |
95 | close-paren at end-of-line is wrong type:
96 |
97 | ```in
98 | (def foo [a b]]
99 | ```
100 |
101 | ```out
102 | (def foo [a b])
103 | ```
104 |
105 | insert missing close-paren inside another when at end-of-line:
106 |
107 | ```in
108 | (let [x {:foo 1 :bar 2]
109 | x)
110 | ```
111 |
112 | ```out
113 | (let [x {:foo 1 :bar 2}]
114 | x)
115 | ```
116 |
117 | unmatched close-parens _inside_ a line are removed:
118 |
119 | ```in
120 | (foo [a (|b] c)
121 | ```
122 | ```out-disable
123 | (foo [a (|b] c)
124 | ^ error: unmatched-close-paren
125 | ```
126 |
127 | **NOTE: Parinferish behaves differently.** It does not turn itself off:
128 |
129 | ```out
130 | (foo [a (|b c)])
131 | ```
132 |
133 | ## Strings
134 |
135 | No close-parens are inserted when a string is unclosed.
136 |
137 | ```in
138 | (def foo "as
139 | ```
140 |
141 | ```out
142 | (def foo "as
143 | ^ error: unclosed-quote
144 | ```
145 |
146 | even if close-parens are quoted out, do not do anything.
147 |
148 | ```in
149 | (defn foo [a "])
150 | ```
151 |
152 | ```out
153 | (defn foo [a "])
154 | ^ error: unclosed-quote
155 | ```
156 |
157 | Multiline strings are supported:
158 |
159 | ```in
160 | (defn foo
161 | "This is docstring.
162 | Line 2 here."
163 | ret
164 | ```
165 |
166 | ```out
167 | (defn foo
168 | "This is docstring.
169 | Line 2 here."
170 | ret)
171 | ```
172 |
173 | Indentation inside multiline strings does not trigger Parinfer's indentation rules.
174 |
175 | ```in
176 | (let [a "Hello
177 | World"
178 | b 2
179 | ret
180 | ```
181 |
182 | ```out
183 | (let [a "Hello
184 | World"
185 | b 2]
186 | ret)
187 | ```
188 |
189 | Close-parens are ignored when inside strings.
190 |
191 | ```in
192 | (let [a "])"
193 | b 2
194 | ```
195 |
196 | ```out
197 | (let [a "])"
198 | b 2])
199 | ```
200 |
201 | Escaped quotes are handled correctly.
202 |
203 | ```in
204 | (def foo "\""
205 | ```
206 |
207 | ```out
208 | (def foo "\"")
209 | ```
210 |
211 | A line ending inside a string will not have a definable Paren Trail. This
212 | minimal test case will fail if the close-paren is treated as a Paren Trail.
213 |
214 | ```in
215 | ()"
216 | "
217 | ```
218 |
219 | ```out
220 | ()"
221 | "
222 | ```
223 |
224 | ## Unbalanced Quotes
225 |
226 | __NOTE:__ The pipe `|` represents the cursor, and its character is removed from
227 | input. We use it here to suggest that the user has just typed the character to
228 | the left of the cursor.
229 |
230 | Typing a quote before another string does not corrupt it (i.e. turn it inside
231 | out, causing Parinfer to treat its contents as code).
232 |
233 | ```in
234 | "|"foo"
235 | ```
236 |
237 | ```out
238 | "|"foo"
239 | ^ error: unclosed-quote
240 | ```
241 |
242 | Another case:
243 |
244 | ```in
245 | (def foo
246 | "|
247 | "(a b)
248 | c")
249 | ```
250 |
251 | ```out
252 | (def foo
253 | "|
254 | "(a b)
255 | c")
256 | ^ error: unclosed-quote
257 | ```
258 |
259 | ## Unbalanced Quotes in Comments
260 |
261 | Unbalanced quotes can be accidentally rebalanced by comments containing an odd number of quotes,
262 | so we do not want to process if any comments meet this critera.
263 |
264 | Notice that the following code is correctly balanced, quite accidentally, but
265 | Parinfer does not process it because the last line contains a comment with an
266 | odd number of quotes (one):
267 |
268 | ```in
269 | (for [col columns]
270 | "|
271 | [:div.td {:style "max-width: 500px;"}])
272 | ```
273 |
274 | ```out-disable
275 | (for [col columns]
276 | "|
277 | [:div.td {:style "max-width: 500px;"}])
278 | ^ error: quote-danger
279 | ```
280 |
281 | **NOTE: Parinferish behaves differently.** It does not turn itself off:
282 |
283 | ```out
284 | (for [col columns]
285 | "|
286 | [:div.td {:style "max-width: 500px);"}])
287 | ```
288 |
289 | But a comment can contain an odd number of quotes if it is in a contiguous group of comments
290 | which contain an even number of them. This allows commenting out a multiline string without
291 | any problems:
292 |
293 | ```in
294 | (def foo [a b]
295 | ; "my multiline
296 | ; docstring."
297 | ret)
298 | ```
299 |
300 | ```out
301 | (def foo [a b])
302 | ; "my multiline
303 | ; docstring."
304 | ret
305 | ```
306 |
307 | Escaped strings are not counted when determining odd number of quotes in a comment.
308 |
309 | ```in
310 | (def foo [a b]
311 | ; ""\"
312 | ret)
313 | ```
314 |
315 | ```out
316 | (def foo [a b])
317 | ; ""\"
318 | ret
319 | ```
320 |
321 | ## Character syntax
322 |
323 | Correctly handle escaped parens as literal characters.
324 |
325 | ```in
326 | (defn foo [a b
327 | \[
328 | ret
329 | ```
330 |
331 | ```out
332 | (defn foo [a b]
333 | \[
334 | ret)
335 | ```
336 |
337 | ```in
338 | (defn foo [a b]
339 | ret\)
340 | ```
341 |
342 | ```out
343 | (defn foo [a b]
344 | ret\))
345 | ```
346 |
347 | ```in
348 | {:tag-open \[ :tag-close \]}
349 | {:tag-open \[ :tag-close \]}
350 | ```
351 |
352 | ```out
353 | {:tag-open \[ :tag-close \]}
354 | {:tag-open \[ :tag-close \]}
355 | ```
356 |
357 | Correctly handle escaped semicolons as characters instead of comments.
358 | Otherwise, the inferred close-parens would be inserted before them.
359 |
360 | ```in
361 | (def foo \;
362 | ```
363 |
364 | ```out
365 | (def foo \;)
366 | ```
367 |
368 | Inferred close-parens are inserted after escaped whitespace.
369 |
370 | ```in
371 | (def foo \,
372 | (def bar \ ; <-- space
373 | ```
374 |
375 | ```out-disable
376 | (def foo \,)
377 | (def bar \ ); <-- space
378 | ```
379 |
380 | **NOTE: Parinferish behaves differently.** It inserts the end paren before the backslash:
381 |
382 | ```out
383 | (def foo \,)
384 | (def bar) \ ; <-- space
385 | ```
386 |
387 | Hanging backslash at end of line is invalid and causes processing to be abandoned.
388 |
389 | ```in
390 | (foo [a b\
391 | c)
392 | ```
393 |
394 | ```out
395 | (foo [a b\
396 | ^ error: eol-backslash
397 | c)
398 | ```
399 |
400 | ## Comments
401 |
402 | When commenting-out an inferred close-paren, a new one should be inserted
403 | before it.
404 |
405 | ```in
406 | (def foo ;)
407 | ```
408 |
409 | ```out
410 | (def foo) ;)
411 | ```
412 |
413 | Commenting-out a line containing inferred close-parens should cause new ones to
414 | be inserted at the previous non-empty line.
415 |
416 | ```in
417 | (let [a 1
418 | b 2
419 | c {:foo 1
420 | ;; :bar 2}]
421 | ret)
422 | ```
423 |
424 | ```out
425 | (let [a 1
426 | b 2
427 | c {:foo 1}]
428 | ;; :bar 2}]
429 | ret)
430 | ```
431 |
432 | Inferred close-parens are inserted before comments.
433 |
434 | ```in
435 | (let [a 1 ;; a comment
436 | ret)
437 | ```
438 |
439 | ```out
440 | (let [a 1] ;; a comment
441 | ret)
442 | ```
443 |
444 | escape character in comment untouched:
445 |
446 | ```in
447 | ; hello \n world
448 | ```
449 |
450 | ```out
451 | ; hello \n world
452 | ```
453 |
454 | ---
455 |
456 | ## Cursor Padding in Paren Trail
457 |
458 | __NOTE__: the pipe `|` represents the cursor, but the character is removed from the input.
459 |
460 | Inferred close-parens can only be inserted to the right of the cursor (if it is present).
461 | This allows us to insert a space before typing a new token.
462 |
463 | ```in
464 | (def b |)
465 | ```
466 |
467 | ```out
468 | (def b |)
469 | ```
470 |
471 | Once the cursor leaves the line, the space is removed.
472 |
473 | ```in
474 | (def b )
475 | ```
476 |
477 | ```out-disable
478 | (def b)
479 | ```
480 |
481 | **NOTE: Parinferish behaves differently.** The space is preserved:
482 |
483 | ```out
484 | (def b )
485 | ```
486 |
487 | Another example with more close-parens:
488 |
489 | ```in
490 | (def b [[c d] |])
491 | ```
492 |
493 | ```out
494 | (def b [[c d] |])
495 | ```
496 |
497 | Once the cursor leaves the line, the space is removed.
498 |
499 | ```in
500 | (def b [[c d] ])
501 | ```
502 |
503 | ```out-disable
504 | (def b [[c d]])
505 | ```
506 |
507 | **NOTE: Parinferish behaves differently.** The space is preserved:
508 |
509 | ```out
510 | (def b [[c d] ])
511 | ```
512 |
513 | ## Cursor Blocking displacement of Paren Trail
514 |
515 | Inferred close-parens before the cursor are never removed, which may
516 | cause indented lines below to be ignored. This is to allow inserting a token
517 | after such a close-paren.
518 |
519 | For example, without the cursor on the first line, this is expected:
520 |
521 | ```in
522 | (let [a 1])
523 | ret)
524 | ```
525 |
526 | ```out
527 | (let [a 1]
528 | ret)
529 | ```
530 |
531 | With the cursor at the end of the first line, the indented line below does not affect it.
532 |
533 | ```in
534 | (let [a 1])|
535 | ret)
536 | ```
537 |
538 | ```out-disable
539 | (let [a 1])|
540 | ret
541 | ```
542 |
543 | **NOTE: Parinferish behaves differently.** It does not take your cursor into account in indent mode:
544 |
545 | ```out
546 | (let [a 1]|
547 | ret)
548 | ```
549 |
550 | If this was not allowed, we would not be able to reach this valid state from
551 | the previous state:
552 |
553 | ```in
554 | (let [a 1]) 2
555 | ret
556 | ```
557 |
558 | ```out-disable
559 | (let [a 1]) 2
560 | ret
561 | ```
562 |
563 | **NOTE: Parinferish behaves differently.** It moves the paren if possible:
564 |
565 | ```out
566 | (let [a 1] 2
567 | ret)
568 | ```
569 |
570 | But if the cursor is before such a close-paren, we are not in a position to
571 | insert a token after it, thus indentation can affect it again:
572 |
573 | ```in
574 | (let [a 1]|)
575 | ret)
576 | ```
577 |
578 | ```out
579 | (let [a 1]|
580 | ret)
581 | ```
582 |
583 | If the cursor is in a comment after such a close-paren, we can safely move it:
584 |
585 | ```in
586 | (let [a 1]) ;|
587 | ret
588 | ```
589 |
590 | ```out
591 | (let [a 1] ;|
592 | ret)
593 | ```
594 |
595 | The cursor should not keep unmatched close-parens in the trail:
596 |
597 | ```in
598 | (foo)}}}}|
599 | ```
600 |
601 | ```out
602 | (foo)|
603 | ```
604 |
605 | ```in
606 | (foo}}}}|)
607 | ```
608 |
609 | ```out
610 | (foo|)
611 | ```
612 |
613 | ## Leading close-parens
614 |
615 | Leading close-parens can cause many problems that we can ignore by simply removing
616 | them, which is what we do when `forceBalance` is enabled. (currently can't enable
617 | these in tests).
618 |
619 | If `forceBalance` is not on, we suspend Indent Mode.
620 |
621 | ```in
622 | (foo
623 | ) bar
624 | ```
625 |
626 | ```out
627 | (foo
628 | ) bar
629 | ^ error: leading-close-paren
630 | ```
631 |
632 | It is allowed if the leading parens are also in paren trail:
633 |
634 | ```in
635 | (foo
636 | ); comment
637 | ```
638 |
639 | ```out-disable
640 | (foo)
641 | ; comment
642 | ```
643 |
644 | **NOTE: Parinferish behaves differently.** It preserves the whitespace:
645 |
646 | ```out
647 | (foo
648 | ); comment
649 | ```
650 |
651 | If there's more than one, point to the first one.
652 |
653 | ```in
654 | [(foo
655 | )] bar
656 | ```
657 |
658 | ```out
659 | [(foo
660 | )] bar
661 | ^ error: leading-close-paren
662 | ```
663 |
664 | ## Unmatched close-parens
665 |
666 | I've tried some inference algorithms to resolve unmatched close-parens (see
667 | [#131]), but they didn't work out due to reasons stated in the issue. The
668 | following cases demonstrate many edge cases that would have to be resolved if
669 | ever approached again.
670 |
671 | [#131]:https://github.com/shaunlebron/parinfer/issues/131
672 |
673 | Inserting a `(` inside a nested vector:
674 |
675 | ```in
676 | (foo [bar (|...] baz)
677 | ```
678 |
679 | ```out-disable
680 | (foo [bar (|...] baz)
681 | ^ error: unmatched-close-paren
682 | ```
683 |
684 | **NOTE: Parinferish behaves differently.** It does not turn itself off:
685 |
686 | ```out
687 | (foo [bar (|... baz)])
688 | ```
689 |
690 | Inserting a `]` inside a nested list:
691 |
692 | ```in
693 | (foo [bar (]| baz)])
694 | ```
695 |
696 | ```out-disable
697 | (foo [bar (]| baz)])
698 | ^ error: unmatched-close-paren
699 | ```
700 |
701 | **NOTE: Parinferish behaves differently.** It does not turn itself off:
702 |
703 | ```out
704 | (foo [bar (| baz)])
705 | ```
706 |
707 | Inserting a `]` ahead of another inside a list (maybe to "barf" the end of the
708 | vector).
709 |
710 | ```in
711 | [... (foo [bar ]| baz] ...)]
712 | ```
713 |
714 | ```out-disable
715 | [... (foo [bar ]| baz] ...)]
716 | ^ error: unmatched-close-paren
717 | ```
718 |
719 | **NOTE: Parinferish behaves differently.** It does not turn itself off:
720 |
721 | ```out
722 | [... (foo [bar ]| baz ...)]
723 | ```
724 |
725 | Suppose you just backspaced a `[` below:
726 |
727 | ```in
728 | (let [{:keys |foo bar]} my-map])
729 | ```
730 |
731 | ```out-disable
732 | (let [{:keys |foo bar]} my-map])
733 | ^ error: unmatched-close-paren
734 | ```
735 |
736 | **NOTE: Parinferish behaves differently.** It does not turn itself off:
737 |
738 | ```out
739 | (let [{:keys |foo bar} my-map])
740 | ```
741 |
742 | Inserting a matched `)` inside nested expressions sometimes works out:
743 |
744 | ```in
745 | (a (b (c))| d) e)
746 | ```
747 |
748 | ```out
749 | (a (b (c))| d) e
750 | ```
751 |
752 | Inserting a matched `(` inside nested expressions sometimes works out too:
753 |
754 | ```in
755 | (a (b (c(|) d) e)
756 | ```
757 |
758 | ```out
759 | (a (b (c(|) d) e))
760 | ```
761 |
762 | But all it takes is one different kind of a paren to keep it from working:
763 |
764 | ```in
765 | (f [x (a (b c(|) d) y] g)
766 | ```
767 |
768 | ```out-disable
769 | (f [x (a (b c(|) d) y] g)
770 | ^ error: unmatched-close-paren
771 | ```
772 |
773 | **NOTE: Parinferish behaves differently.** It does not turn itself off:
774 |
775 | ```out
776 | (f [x (a (b c(|) d) y g)])
777 | ```
778 |
779 | Unmatched close-parens on indented lines present similar issues.
780 | For example, inserting a `)` below:
781 |
782 | ```in
783 | (foo
784 | bar)| baz) qux
785 | ```
786 |
787 | ```out-disable
788 | (foo
789 | bar)| baz) qux
790 | ^ error: unmatched-close-paren
791 | ```
792 |
793 | **NOTE: Parinferish behaves differently.** It does not turn itself off:
794 |
795 | ```out
796 | (foo
797 | bar)| baz qux
798 | ```
799 |
800 | ```in
801 | (foo
802 | [bar
803 | bar)| baz
804 | bar])
805 | ```
806 |
807 | ```out-disable
808 | (foo
809 | [bar
810 | bar)| baz
811 | ^ error: unmatched-close-paren
812 | bar])
813 | ```
814 |
815 | **NOTE: Parinferish behaves differently.** It does not turn itself off:
816 |
817 | ```out
818 | (foo
819 | [bar
820 | bar| baz
821 | bar])
822 | ```
823 |
824 | Or when dedenting a line makes an inner close-paren unmatched:
825 |
826 | ```in
827 | (foo
828 | [bar]
829 | |bar) baz
830 | ```
831 |
832 | ```out-disable
833 | (foo
834 | [bar]
835 | |bar) baz
836 | ^ error: unmatched-close-paren
837 | ```
838 |
839 | **NOTE: Parinferish behaves differently.** It does not turn itself off:
840 |
841 | ```out
842 | (foo
843 | [bar])
844 | |bar baz
845 | ```
846 |
847 | In the same example, a different similar problem emerges when indenting a line
848 | makes the same inner close-paren unmatched:
849 |
850 | ```in
851 | (foo
852 | [bar]
853 | |bar) baz
854 | ```
855 |
856 | ```out-disable
857 | (foo
858 | [bar]
859 | |bar) baz
860 | ^ error: unmatched-close-paren
861 | ```
862 |
863 | **NOTE: Parinferish behaves differently.** It does not turn itself off:
864 |
865 | ```out
866 | (foo
867 | [bar
868 | |bar baz])
869 | ```
870 |
871 | The same problem demonstrated for another dedenting example:
872 |
873 | ```in
874 | (foo
875 | [bar
876 | bar]) baz
877 | ```
878 |
879 | ```out-disable
880 | (foo
881 | [bar
882 | bar]) baz
883 | ^ error: unmatched-close-paren
884 | ```
885 |
886 | **NOTE: Parinferish behaves differently.** It does not turn itself off:
887 |
888 | ```out
889 | (foo
890 | [bar]
891 | bar) baz
892 | ```
893 |
894 | ## Cursor Shifting
895 |
896 | Commenting an inferred close-paren
897 |
898 | ```in
899 | (foo bar ;|)
900 | ```
901 |
902 | ```out
903 | (foo bar) ;|)
904 | ```
905 |
906 | Commenting multiple inferred close-parens
907 |
908 | ```in
909 | (let [x 1
910 | y 2;|])
911 | ```
912 |
913 | ```out
914 | (let [x 1
915 | y 2]);|])
916 | ```
917 |
918 | When typing an open-paren, a close-paren should come after the cursor:
919 |
920 | ```in
921 | (|
922 | ```
923 |
924 | ```out
925 | (|)
926 | ```
927 |
928 | ## Tab Stops
929 |
930 | We can return the positions of the open-parens whose structure would be
931 | affected by the indentation of the current cursor line. This allows editors to
932 | use them to create tab stops for smart indentation snapping.
933 |
934 | ```in-disable
935 | (def x [1 2 3])
936 | (def y 2)
937 | |
938 | ```
939 |
940 | ```out-disable
941 | (def x [1 2 3])
942 | (def y 2)
943 | ^ > tabStops
944 | |
945 | ```
946 |
947 | The `>` means the position of the first arg after an open-paren, because some styles
948 | use it for alignment.
949 |
950 | ```in-disable
951 | (foo bar
952 | (baz boo))
953 | |
954 | ```
955 |
956 | ```out-disable
957 | (foo bar
958 | (baz boo))
959 | ^ ^ > tabStops
960 | |
961 | ```
962 |
963 | ```in-disable
964 | (let [a {:foo 1}
965 | |
966 | bar [1 2 3]]
967 | bar)
968 | ```
969 |
970 | ```out-disable
971 | (let [a {:foo 1}
972 | ^ ^ ^ > tabStops
973 | |
974 | bar [1 2 3]]
975 | bar)
976 | ```
977 |
978 |
979 | ```in-disable
980 | (let [a {:foo 1}
981 | bar (func 1 2 3)]
982 | |
983 | bar)
984 | ```
985 |
986 | ```out-disable
987 | (let [a {:foo 1}
988 | bar (func 1 2 3)]
989 | ^ ^ ^ > tabStops
990 | |
991 | bar)
992 | ```
993 |
994 | ## Paren Trails
995 |
996 | We return non-empty Paren Trails so plugins can dim them with markers:
997 |
998 | ```in-disable
999 | (defn foo
1000 | "hello, this is a docstring"
1001 | [a b]
1002 | (let [sum (+ a b)
1003 | prod (* a b)]
1004 | {:sum sum
1005 | :prod prod}))
1006 | ```
1007 |
1008 | ```out-disable
1009 | (defn foo
1010 | "hello, this is a docstring"
1011 | [a b]
1012 | ^ parenTrail
1013 | (let [sum (+ a b)
1014 | ^ parenTrail
1015 | prod (* a b)]
1016 | ^^ parenTrail
1017 | {:sum sum
1018 | :prod prod}))
1019 | ^^^ parenTrail
1020 | ```
1021 |
1022 | **NOTE: This is a fix added by Parinferish.**
1023 |
1024 | Mismatched parens following multiline strings are corrected regardless of indentation.
1025 |
1026 | ```in
1027 | (def shader
1028 | '{:inputs {a_position vec2}
1029 | :outputs {v_color vec4}
1030 | :functions "
1031 | void main()
1032 | {
1033 | gl_Position = vec4(a_position.x, a_position.y, 0.0, 1.0);
1034 | v_color = gl_Position * 0.5 + 0.5;
1035 | }")
1036 | ```
1037 |
1038 | ```out
1039 | (def shader
1040 | '{:inputs {a_position vec2}
1041 | :outputs {v_color vec4}
1042 | :functions "
1043 | void main()
1044 | {
1045 | gl_Position = vec4(a_position.x, a_position.y, 0.0, 1.0);
1046 | v_color = gl_Position * 0.5 + 0.5;
1047 | }"})
1048 | ```
1049 |
--------------------------------------------------------------------------------
/docs/fonts/glyphicons-halflings-regular.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------