Skip to content

Commit 87dd429

Browse files
sci repl window WIP
1 parent 8222d5a commit 87dd429

File tree

4 files changed

+206
-7
lines changed

4 files changed

+206
-7
lines changed

notebooks/tap_window.clj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,5 @@
4545
:encoding {:x {:field "a" :type "nominal" :axis {:labelAngle 0}}
4646
:y {:field "b" :type "quantitative"}}}))
4747
(tap> 1)
48-
(tap/reset-taps!))
48+
(tap/reset-taps!)
49+
(clerk/window! ::clerk/sci-repl))

src/nextjournal/clerk/render/window.cljs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
(ns nextjournal.clerk.render.window
2-
(:require [applied-science.js-interop :as j]
3-
[nextjournal.clerk.render.hooks :as hooks]))
2+
(:require ["@codemirror/view" :as cm-view :refer [keymap highlightActiveLine]]
3+
[applied-science.js-interop :as j]
4+
[clojure.string :as str]
5+
[nextjournal.clerk.render.code :as code]
6+
[nextjournal.clerk.render.hooks :as hooks]
7+
[nextjournal.clerk.sci-env.completions :as completions]
8+
[nextjournal.clojure-mode.extensions.eval-region :as eval-region]
9+
[sci.core :as sci]
10+
[sci.ctx-store]))
11+
12+
(defn inspect-fn []
13+
@(resolve 'nextjournal.clerk.render/inspect))
414

515
(defn resizer [{:keys [on-resize on-resize-start on-resize-end] :or {on-resize-start #() on-resize-end #()}}]
616
(let [!direction (hooks/use-state nil)
@@ -122,6 +132,64 @@
122132
(j/assoc-in! [:style :top] "5px")
123133
(j/assoc-in! [:style :left] "5px")))
124134

135+
(defn eval-string
136+
([source] (sci/eval-string* (sci.ctx-store/get-ctx) source))
137+
([ctx source]
138+
(when-some [code (not-empty (str/trim source))]
139+
(try {:result (sci/eval-string* ctx code)}
140+
(catch js/Error e
141+
{:error (str (.-message e))})))))
142+
143+
(j/defn eval-at-cursor [on-result ^:js {:keys [state]}]
144+
(some->> (eval-region/cursor-node-string state)
145+
(eval-string)
146+
(on-result))
147+
true)
148+
149+
(j/defn eval-top-level [on-result ^:js {:keys [state]}]
150+
(some->> (eval-region/top-level-string state)
151+
(eval-string)
152+
(on-result))
153+
true)
154+
155+
(j/defn eval-cell [on-result ^:js {:keys [state]}]
156+
(-> (.-doc state)
157+
(str)
158+
(eval-string)
159+
(on-result))
160+
true)
161+
162+
(defn sci-extension [{:keys [modifier on-result]}]
163+
(.of cm-view/keymap
164+
(j/lit
165+
[{:key "Mod-Enter"
166+
:run (partial eval-cell on-result)}
167+
{:key (str modifier "-Enter")
168+
:shift (partial eval-top-level on-result)
169+
:run (partial eval-at-cursor on-result)}])))
170+
171+
(defn sci-repl []
172+
(let [!code-str (hooks/use-state "")
173+
!results (hooks/use-state ())]
174+
[:div.flex.flex-col.bg-gray-50
175+
[:div.w-full.border-t.border-b.border-slate-300.shadow-inner.px-2.py-1.bg-slate-100
176+
[code/editor !code-str {:extensions #js [(.of keymap nextjournal.clojure-mode.keymap/paredit)
177+
completions/completion-source
178+
(sci-extension {:modifier "Alt"
179+
:on-result #(swap! !results conj {:result %
180+
:evaled-at (js/Date.)
181+
:react-key (gensym)})})]}]]
182+
(into
183+
[:div.w-full.flex-auto.overflow-auto]
184+
(map (fn [{:as r :keys [result evaled-at react-key]}]
185+
^{:key react-key}
186+
[:div.border-b.px-2.py-2.text-xs.font-mono
187+
[:div.font-mono.text-slate-40.flex-shrink-0.text-right
188+
{:class "text-[9px]"}
189+
(str (first (.. evaled-at toTimeString (split " "))) ":" (.getMilliseconds evaled-at))]
190+
[(inspect-fn) result]]))
191+
@!results)]))
192+
125193
(defn show
126194
([content] (show content {}))
127195
([content {:as opts :keys [css-class]}]
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
(ns nextjournal.clerk.sci-env.completions
2+
(:require ["@codemirror/autocomplete" :as cm-autocomplete :refer [CompletionContext]]
3+
["@codemirror/language" :as cm-lang]
4+
[clojure.string :as str]
5+
[goog.object :as gobject]
6+
[sci.core :as sci]
7+
[sci.ctx-store]))
8+
9+
(defn format [fmt-str x]
10+
(str/replace fmt-str "%s" x))
11+
12+
(defn fully-qualified-syms [ctx ns-sym]
13+
(let [syms (sci/eval-string* ctx (format "(keys (ns-map '%s))" ns-sym))
14+
sym-strs (map #(str "`" %) syms)
15+
sym-expr (str "[" (str/join " " sym-strs) "]")
16+
syms (sci/eval-string* ctx sym-expr)
17+
syms (remove #(str/starts-with? (str %) "nbb.internal") syms)]
18+
syms))
19+
20+
(defn- ns-imports->completions [ctx query-ns query]
21+
(let [[_ns-part name-part] (str/split query #"/")
22+
resolved (sci/eval-string* ctx
23+
(pr-str `(let [resolved# (resolve '~query-ns)]
24+
(when-not (var? resolved#)
25+
resolved#))))]
26+
(when resolved
27+
(when-let [[prefix imported] (if name-part
28+
(let [ends-with-dot? (str/ends-with? name-part ".")
29+
fields (str/split name-part #"\.")
30+
fields (if ends-with-dot?
31+
fields
32+
(butlast fields))]
33+
[(str query-ns "/" (when (seq fields)
34+
(let [joined (str/join "." fields)]
35+
(str joined "."))))
36+
(apply gobject/getValueByKeys resolved
37+
fields)])
38+
[(str query-ns "/") resolved])]
39+
(let [props (loop [obj imported
40+
props []]
41+
(if obj
42+
(recur (js/Object.getPrototypeOf obj)
43+
(into props (js/Object.getOwnPropertyNames obj)))
44+
props))
45+
completions (map (fn [k]
46+
[nil (str prefix k)]) props)]
47+
completions)))))
48+
49+
(defn- match [_alias->ns ns->alias query [sym-ns sym-name qualifier]]
50+
(let [pat (re-pattern query)]
51+
(or (when (and (= :unqualified qualifier) (re-find pat sym-name))
52+
[sym-ns sym-name])
53+
(when sym-ns
54+
(or (when (re-find pat (str (get ns->alias (symbol sym-ns)) "/" sym-name))
55+
[sym-ns (str (get ns->alias (symbol sym-ns)) "/" sym-name)])
56+
(when (re-find pat (str sym-ns "/" sym-name))
57+
[sym-ns (str sym-ns "/" sym-name)]))))))
58+
59+
(defn completions [{:keys [ctx]
60+
ns-str :ns
61+
:as request}]
62+
(js/console.log "request" request)
63+
(try
64+
(let [sci-ns (when ns-str
65+
(sci/find-ns ctx (symbol ns-str)))]
66+
(sci/binding [sci/ns (or sci-ns @sci/ns)]
67+
(if-let [query (or (:symbol request)
68+
(:prefix request))]
69+
(let [has-namespace? (str/includes? query "/")
70+
query-ns (when has-namespace? (some-> (str/split query #"/")
71+
first symbol))
72+
from-current-ns (fully-qualified-syms ctx (sci/eval-string* ctx "(ns-name *ns*)"))
73+
from-current-ns (map (fn [sym]
74+
[(namespace sym) (name sym) :unqualified])
75+
from-current-ns)
76+
alias->ns (sci/eval-string* ctx "(let [m (ns-aliases *ns*)] (zipmap (keys m) (map ns-name (vals m))))")
77+
ns->alias (zipmap (vals alias->ns) (keys alias->ns))
78+
from-aliased-nss (doall (mapcat
79+
(fn [alias]
80+
(let [ns (get alias->ns alias)
81+
syms (sci/eval-string* ctx (format "(keys (ns-publics '%s))" ns))]
82+
(map (fn [sym]
83+
[(str ns) (str sym) :qualified])
84+
syms)))
85+
(keys alias->ns)))
86+
all-namespaces (->> (sci/eval-string* ctx "(all-ns)")
87+
(map (fn [ns]
88+
[(str ns) nil :qualified])))
89+
from-imports (when has-namespace? (ns-imports->completions ctx query-ns query))
90+
fully-qualified-names (when-not from-imports
91+
(when has-namespace?
92+
(let [ns (get alias->ns query-ns query-ns)
93+
syms (sci/eval-string* ctx (format "(and (find-ns '%s)
94+
(keys (ns-publics '%s)))"
95+
ns))]
96+
(map (fn [sym]
97+
[(str ns) (str sym) :qualified])
98+
syms))))
99+
svs (concat from-current-ns from-aliased-nss all-namespaces fully-qualified-names)
100+
completions (keep (fn [entry]
101+
(match alias->ns ns->alias query entry))
102+
svs)
103+
completions (concat completions from-imports)
104+
completions (->> (map (fn [[namespace name]]
105+
(cond-> {"candidate" (str name)}
106+
namespace (assoc "ns" (str namespace))))
107+
completions)
108+
distinct vec)]
109+
{:completions completions
110+
:status ["done"]})
111+
{:status ["done"]})))
112+
(catch :default e
113+
(js/console.error "ERROR" e)
114+
{:completions []
115+
:status ["done"]})))
116+
117+
(defn autocomplete [^js context]
118+
(let [node-before (.. (cm-lang/syntaxTree (.-state context)) (resolveInner (.-pos context) -1))
119+
text-before (.. context -state (sliceDoc (.-from node-before) (.-pos context)))]
120+
#js {:from (.-from node-before)
121+
:options (clj->js (map
122+
(fn [{:strs [candidate]}]
123+
(doto {:label candidate} prn))
124+
(:completions (completions {:ctx (sci.ctx-store/get-ctx) :ns "user" :symbol text-before}))))}))
125+
126+
(def completion-source
127+
(cm-autocomplete/autocompletion #js {:override #js [autocomplete]}))

src/nextjournal/clerk/window.clj

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,13 @@
3333
(defn open!
3434
([id]
3535
(case id
36-
:nextjournal.clerk/taps (open! id {:title "🚰 Taps" :css-class "p-0 relative overflow-auto"}
37-
(v/with-viewers (v/add-viewers [tap/tap-viewer])
38-
(v/with-viewer taps-viewer {:nextjournal/opts {:taps-view @!taps-view}}
39-
@tap/!taps)))))
36+
::clerk/taps (open! id {:title "🚰 Taps" :css-class "p-0 relative overflow-auto"}
37+
(v/with-viewers (v/add-viewers [tap/tap-viewer])
38+
(v/with-viewer taps-viewer {:nextjournal/opts {:taps-view @!taps-view}}
39+
@tap/!taps)))
40+
::clerk/sci-repl (open! id {:title "SCI REPL" :css-class "p-0 relative overflow-auto"}
41+
(v/with-viewer {:render-fn 'nextjournal.clerk.render.window/sci-repl
42+
:transform-fn clerk/mark-presented} nil))))
4043
([id content] (open! id {} content))
4144
([id opts content]
4245
;; TODO: consider calling v/transform-result

0 commit comments

Comments
 (0)