Skip to content

Commit 4821704

Browse files
mkmhuebert
authored andcommitted
highlight evaluated form
* highlight after eval (partial impl) * derive eval-region from selection + modifier state * Make eval form/at-cursor accessible through command palette * Keymap * Fix demo after eval-region refactoring * Adjust key mapping based on user agent platform * Fix title * docs * fix bug when no form is at cursor, make cursor and top-level searches more precise Co-authored-by: Matthew Huebert <[email protected]> Co-authored-by: Matthew Huebert <[email protected]> Co-authored-by: Matthew Huebert <[email protected]> Co-authored-by: Matthew Huebert <[email protected]>
1 parent 9330351 commit 4821704

File tree

8 files changed

+203
-232
lines changed

8 files changed

+203
-232
lines changed

public/index.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html>
33
<head>
44
<meta charset="utf8">
5-
<title>lezer-clojure</title>
5+
<title>Clojure/Script mode for CodeMirror 6</title>
66
<link href="https://cdn.nextjournal.com/data/QmW53nJSRrRao5FZ9sZ2pwQ4Gd4mK4nZcvhrATVdiabPkc?filename=tailwind-a4c8a6fe636b6d528505c30cb68526a024f446a7.css&content-type=text/css" rel="stylesheet">
77
<link href="https://cdn.nextjournal.com/data/QmSaHZCU6U2DeNohfW2PuXDHkayw7w21uvUWL5oEqVWKwH?filename=viewer-1c61aac61ffa4da89b828d538c5e4eff188e7b56.css&content-type=text/css" rel="stylesheet">
88
<link href="https://cdn.nextjournal.com/data/QmZZpjcdZDa8WT27QpcepDfqwuGik6Y3Ueyxaxs1Gqpk9w?filename=nextjournal-c81d440c5a7312046bbc5a2c3f2c5567d9ea9131.css&content-type=text/css" rel="stylesheet">
@@ -15,7 +15,7 @@
1515
<div class="landing-page">
1616
<div class="hero pb-4">
1717
<div>
18-
<h1>Clojure/Script mode for <a href="https://codemirror.net/6/">codemirror.next</a></h1>
18+
<h1>Clojure/Script mode for <a href="https://codemirror.net/6/">CodeMirror 6</a></h1>
1919
<p>
2020
Enable a decent Clojure/Script editor experience in the browser.
2121
Implemented as a <a href="https://lezer.codemirror.net">lezer</a> grammar.
@@ -29,6 +29,7 @@ <h1>Clojure/Script mode for <a href="https://codemirror.net/6/">codemirror.next<
2929
🤹‍♀️ Try in Nextjournal
3030
</a>
3131
</div>
32+
3233
</div>
3334
</div>
3435
</div>

src/demo/codemirror_next/clojure/demo.cljs

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@
4343
:$cursor {:visibility "hidden"}
4444
"$$focused $cursor" {:visibility "visible"}})))
4545

46-
(defonce extensions #js[(.-lineWrapping EditorView)
47-
theme
46+
(defonce extensions #js[theme
4847
(history)
4948
highlight/defaultHighlightStyle
5049
(view/drawSelection)
@@ -72,7 +71,7 @@
7271
:on-result (partial reset! last-result)})] source)
7372
:parent el)))))]
7473
[:div
75-
[:div {:class "mt-4 rounded-md border mb-0 text-sm monospace overflow-auto relative shadow-md bg-white"
74+
[:div {:class "mt-4 rounded-md mb-0 text-sm monospace overflow-auto relative shadow-sm bg-white"
7675
:ref mount!}]
7776
[:div.mt-3.mv-4.pl-6 {:style {:white-space "pre-wrap" :font-family "monospace"}}
7877
(prn-str @last-result)]]
@@ -81,20 +80,51 @@
8180

8281
(defn samples []
8382
(into [:<>]
84-
(for [source ["(rand-nth (range 1000))"
85-
"(defn greeting [first-name] \n (str \"Hello, \" first-name))"
86-
"(greeting \"fido\")"]]
83+
(for [source ["(comment ;; try evaluating those with alt-enter
84+
(fizz-buzz 1)
85+
(fizz-buzz 3)
86+
(fizz-buzz 5)
87+
(fizz-buzz 15)
88+
(fizz-buzz 17)
89+
(fizz-buzz 42))
90+
91+
(defn fizz-buzz [n]
92+
(condp (fn [a b] (zero? (mod b a))) n
93+
15 \"fizzbuzz\"
94+
3 \"fizz\"
95+
5 \"buzz\"
96+
n))"]]
8797
[editor source])))
8898

8999
(defn tag [tag & s]
90100
(let [[opts s] (if (map? (first s)) [(first s) (rest s)] [nil s])]
91101
(str "<" (name tag) (reduce-kv #(str %1 " " (name %2) "=" "'" %3 "'") "" opts) ">" (apply str s) "</" (name tag) ">")))
92102

103+
(defn mac? []
104+
(some? (re-find #"Mac" js/navigator.platform)))
105+
106+
(defn key-mapping []
107+
(cond-> {"ArrowUp" ""
108+
"ArrowDown" ""
109+
"ArrowRight" ""
110+
"ArrowLeft" ""}
111+
(mac?)
112+
(merge {"Alt" ""
113+
"Shift" ""
114+
"Enter" ""
115+
"Ctrl" ""})))
116+
117+
(defn render-key [key]
118+
(let [keys (into [] (map #(get ((memoize key-mapping)) % %) (str/split key #"-")))]
119+
(tag :span
120+
(str/join (tag :span "+") (map (partial tag :kdb {:class "bg-gray monospace m-1" :style "border-radius: 5px; padding: 1px 4px; border: 1px solid black;"}) keys)))))
121+
93122
(defn ^:dev/after-load render []
94123
(rdom/render [samples] (js/document.getElementById "editor"))
95124
(j/assoc! (js/document.getElementById "docs")
96125
:innerHTML
97126
(tag :div
127+
(tag :h3 {:class "m-3" } "Keybindings")
98128
(tag :table {:cellpadding 0 :class "w-full"}
99129
(->> keymap/paredit-keymap*
100130
(sort-by first)
@@ -103,14 +133,14 @@
103133
(tag :tr
104134
{:class "border-t even:bg-gray-100"}
105135
(tag :td {:class "px-3 py-1 align-top"} (tag :b (name command)))
106-
(tag :td {:class "px-3 py-1 align-top monospace text-sm"} key)
136+
(tag :td {:class "px-3 py-1 align-top text-sm"} (render-key key))
107137
(tag :td {:class "px-3 py-1 align-top"} doc))
108138
(when shift
109139
(tag :tr
110140
{:class "border-t even:bg-gray-100"}
111141
(tag :td {:class "px-3 py-1 align-top"} (tag :b (name shift)))
112-
(tag :td {:class "px-3 py-1 align-top monospace text-sm"}
113-
(str "Shift-" key))
142+
(tag :td {:class "px-3 py-1 align-top text-sm"}
143+
(render-key (str "Shift-" key)))
114144
(tag :td {:class "px-3 py-1 align-top"} ""))))) ""))
115145
"</table>"))))
116146

src/demo/codemirror_next/clojure/demo/sci.cljs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[applied-science.js-interop :as j]
44
[sci.core :as sci]
55
[codemirror-next.clojure.node :as n]
6-
[codemirror-next.clojure.extensions.temp-selection :as temp-selection]
6+
[codemirror-next.clojure.extensions.eval-region :as eval-region]
77
[codemirror-next.clojure.util :as u]))
88

99
(defonce context (sci/init {}))
@@ -13,13 +13,19 @@
1313
(catch js/Error e
1414
(str e))))
1515

16-
(j/defn eval-current-range [on-result ^:js {:as view :keys [state]}]
17-
(some->> (temp-selection/current-string state)
16+
(j/defn eval-at-cursor [on-result ^:js {:keys [state]}]
17+
(some->> (eval-region/cursor-node-string state)
1818
(eval-string)
1919
(on-result))
2020
true)
2121

22-
(j/defn eval-cell [on-result ^:js {:as view :keys [state]}]
22+
(j/defn eval-top-level [on-result ^:js {:keys [state]}]
23+
(some->> (eval-region/top-level-string state)
24+
(eval-string)
25+
(on-result))
26+
true)
27+
28+
(j/defn eval-cell [on-result ^:js {:keys [state]}]
2329
(-> (str "(do " (.-doc state) " )")
2430
(eval-string)
2531
(on-result))
@@ -32,5 +38,5 @@
3238
[{:key "Mod-Enter"
3339
:run (partial eval-cell on-result)}
3440
{:key (str modifier "-Enter")
35-
:shift (partial eval-current-range on-result)
36-
:run (partial eval-current-range on-result)}])))
41+
:shift (partial eval-top-level on-result)
42+
:run (partial eval-at-cursor on-result)}])))

src/main/codemirror_next/clojure.cljs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
[codemirror-next.clojure.extensions.match-brackets :as match-brackets]
1313
[codemirror-next.clojure.extensions.formatting :as format]
1414
[codemirror-next.clojure.extensions.selection-history :as sel-history]
15-
[codemirror-next.clojure.extensions.temp-selection :as temp-selection]
15+
[codemirror-next.clojure.extensions.eval-region :as eval-region]
1616
[codemirror-next.clojure.keymap :as keymap]
1717
[codemirror-next.clojure.node :as n]
1818
[codemirror-next.clojure.selections :as sel]
@@ -79,7 +79,7 @@
7979
(sel-history/extension)
8080
(format/ext-format-changed-lines)
8181
(.-lineWrapping EditorView)
82-
(temp-selection/as-decoration {:modifier "Alt"})])
82+
(eval-region/extension {:modifier "Alt"})])
8383

8484
(comment
8585

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
(ns codemirror-next.clojure.extensions.eval-region
2+
(:require
3+
["@codemirror/next/state" :as state :refer [StateEffect StateField Facet precedence]]
4+
["@codemirror/next/view" :as view :refer [EditorView Decoration keymap]]
5+
["w3c-keyname" :refer [keyName]]
6+
[applied-science.js-interop :as j]
7+
[codemirror-next.clojure.util :as u]
8+
[codemirror-next.clojure.node :as n]
9+
[clojure.string :as str]))
10+
11+
(defn uppermost-edge-here
12+
"Returns node or its highest ancestor that starts or ends at the cursor position."
13+
[pos node]
14+
(or (->> (iterate n/up node)
15+
(take-while (every-pred (complement n/top?)
16+
#(or (= pos (n/end %) (n/end node))
17+
(= pos (n/start %) (n/start node)))))
18+
(last))
19+
node))
20+
21+
(defn primary-selection [state]
22+
(-> (j/call-in state [:selection :asSingle])
23+
(j/get-in [:ranges 0])))
24+
25+
(defn node-at-cursor
26+
([state] (node-at-cursor state (j/get (primary-selection state) :from)))
27+
([^js state from]
28+
(some->> (n/nearest-touching state from -1)
29+
(#(when (and (or (n/terminal-type? (n/type %))
30+
(= (n/start %) from)
31+
(= (n/end %) from))
32+
(not (n/top? %))) %))
33+
(uppermost-edge-here from)
34+
(n/balanced-range state))))
35+
36+
(defn top-level-node [state]
37+
(->> (n/nearest-touching state (j/get (primary-selection state) :from) -1)
38+
(iterate n/up)
39+
(take-while (every-pred identity (complement n/top?)))
40+
last))
41+
42+
;; Modifier field
43+
(defonce modifier-effect (.define StateEffect))
44+
(defonce modifier-field
45+
(.define StateField
46+
(j/lit {:create (constantly {})
47+
:update (fn [value ^js tr]
48+
(or (some-> (first (filter #(.is ^js % modifier-effect) (.-effects tr)))
49+
(j/get :value))
50+
value))})))
51+
52+
(defn get-modifier-field [^js state] (.field state modifier-field))
53+
54+
(j/defn set-modifier-field! [^:js {:as view :keys [dispatch state]} value]
55+
(dispatch #js{:effects (.of modifier-effect value)}))
56+
57+
(j/defn mark [spec ^:js {:keys [from to]}]
58+
(-> (.mark Decoration spec)
59+
(.range from to)))
60+
61+
(defn single-mark [spec range]
62+
(.set Decoration #js[(mark spec range)]))
63+
64+
(defonce mark-spec (j/lit {:attributes {:style "background-color: rgba(0, 243, 255, 0.14);"}}))
65+
(defonce mark-spec-highlight (j/lit {:attributes {:style "background-color: rgba(0, 243, 255, 0.35);"}}))
66+
67+
(defn cursor-range [^js state]
68+
(if (.. state -selection -primary -empty)
69+
(node-at-cursor state)
70+
(.. state -selection -primary)))
71+
72+
(defonce region-field
73+
(.define StateField
74+
(j/lit
75+
{:create (constantly (.-none Decoration))
76+
:update (j/fn [_value ^:js {:keys [state]}]
77+
(let [{:strs [Alt Shift Enter]} (get-modifier-field state)
78+
spec (if Enter mark-spec-highlight mark-spec)]
79+
(if-let [range (cond (and Alt Shift) (top-level-node state)
80+
Alt (or (u/guard (primary-selection state)
81+
#(not (j/get % :empty)))
82+
(cursor-range state)))]
83+
(single-mark spec range)
84+
(.-none Decoration))))
85+
:provide [(.-decorations EditorView)]})))
86+
87+
88+
(defn get-region-field [^js state] (.field state region-field))
89+
90+
(defn current-range [^js state]
91+
(or (some-> (get-region-field state)
92+
(j/call :iter)
93+
(u/guard #(j/get % :value)))
94+
(.. state -selection -primary)))
95+
96+
(defn modifier-extension
97+
"Maintains modifier-state-field, containing a map of {<modifier> true}, including Enter."
98+
[modifier]
99+
(let [handle-enter (j/fn handle-enter [^:js {:as view :keys [state]}]
100+
(set-modifier-field! view (assoc (get-modifier-field state) "Enter" true))
101+
nil)
102+
handle-key-event (j/fn [^:js {:as event :keys [altKey shiftKey metaKey controlKey type]}
103+
^:js {:as view :keys [state]}]
104+
(let [prev (get-modifier-field state)
105+
next (cond-> {}
106+
altKey (assoc "Alt" true)
107+
shiftKey (assoc "Shift" true)
108+
metaKey (assoc "Meta" true)
109+
controlKey (assoc "Control" true)
110+
(and (= "keydown" type)
111+
(= "Enter" (keyName event)))
112+
(assoc "Enter" true))]
113+
(when (not= prev next)
114+
(set-modifier-field! view next))))
115+
handle-backspace (j/fn [^:js {:as view :keys [state dispatch]}]
116+
(j/let [^:js {:keys [from to]} (current-range state)]
117+
(when (not= from to)
118+
(dispatch (j/lit {:changes {:from from :to to :insert ""}
119+
:annotations (u/user-event-annotation "delete")})))
120+
true))]
121+
#js[modifier-field
122+
(precedence (keymap
123+
(j/lit [{:key (str modifier "-Enter")
124+
:shift handle-enter
125+
:run handle-enter}
126+
{:key (str modifier "-Backspace")
127+
:run handle-backspace
128+
:shift handle-backspace}])) "override")
129+
(.domEventHandlers view/EditorView
130+
#js{:keydown handle-key-event
131+
:keyup handle-key-event})]))
132+
133+
(defn extension [{:keys [modifier]
134+
:or {modifier "Alt"}}]
135+
#js[(modifier-extension modifier)
136+
region-field])
137+
138+
(defn cursor-node-string [^js state]
139+
(u/guard (some->> (node-at-cursor state)
140+
(u/range-str state))
141+
(complement str/blank?)))
142+
143+
(defn top-level-string [^js state]
144+
(u/guard (some->> (top-level-node state)
145+
(u/range-str state))
146+
(complement str/blank?)))

src/main/codemirror_next/clojure/extensions/formatting.cljs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@
159159
"pointerselection"
160160
"cut"
161161
"noformat"
162-
"tempselection") nil
162+
"evalregion") nil
163163
"format-selections" (format-selection (.-state tr))
164164
(let [state (.-state tr)
165165
context (make-indent-context state)]

0 commit comments

Comments
 (0)