Skip to content

Commit ef7850a

Browse files
committed
add initial support for loopback mode
via cljs_eval: https://clojureverse.org/t/status-update-inspect-cljs-eval/6074
1 parent a42bd8a commit ef7850a

File tree

6 files changed

+138
-16
lines changed

6 files changed

+138
-16
lines changed

docs/faq.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,18 @@
186186
> [this commit](https://github.com/binaryage/dirac-sample/commit/3f6b149eca7bac6efc6ffd77f29d25bdc1606d3c) how you could
187187
> potentially implement something like this for your own project.
188188
189+
#### What is loopback mode?
190+
191+
> Shadow-cljs exposed experimental cljs_eval API. Please read [the announcement](https://clojureverse.org/t/status-update-inspect-cljs-eval/6074).
192+
> We can make use of this in shadow-cljs scenarios without any prior Dirac setup.
193+
> If Dirac REPL is opened on a page which has no Dirac Runtime but has this API available, we can at least offer sugar
194+
> on top of cljs_eval. When you type "some cljs code" into Dirac REPL and hit ENTER, it will be effectively turned into `await cljs_eval("some cljs code")`
195+
> Shadow-cljs will then do the compilation, evaluates generated javascript and you should see some results back in the console.
196+
> To switch current namespace enter `(in-ns 'your.new.namespace)`, this is handled specially by Dirac REPL.
197+
> I refer to this mode of operation as "loopback REPL". Because there is no Dirac nREPL backend or runtime support.
198+
> Dirac talks to page's context to do cljs evaluations. Some Dirac features are not supported.
199+
> When this mode is active, you should see purple "loopback" indicator when the REPL prompt is empty.
200+
189201
#### I have a great idea for contribution! How to hack on Dirac itself?
190202

191203
> Please refer to [hacking.md](hacking.md).

resources/unpacked/devtools/front_end/console/ConsoleView.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1740,8 +1740,10 @@ export class ConsoleView extends UI.Widget.VBox {
17401740
const data =
17411741
/** @type {{result: ?SDK.RemoteObject.RemoteObject, commandMessage: !SDK.ConsoleModel.ConsoleMessage, exceptionDetails: (!Protocol.Runtime.ExceptionDetails|undefined)}} */
17421742
(event.data);
1743-
this._prompt.history().pushHistoryItem(data.commandMessage.messageText);
1744-
this._consoleHistorySetting.set(this._prompt.history().historyData().slice(-persistedHistorySize));
1743+
if (!data.commandMessage.skipHistory) {
1744+
this._prompt.history().pushHistoryItem(data.commandMessage.messageText);
1745+
this._consoleHistorySetting.set(this._prompt.history().historyData().slice(-persistedHistorySize));
1746+
}
17451747
this._printResult(data.result, data.commandMessage, data.exceptionDetails);
17461748
}
17471749

resources/unpacked/devtools/front_end/dirac/dirac.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,9 @@ console.log("DJS imported!");
240240
return loadLazyDirac().then(() => window.dirac.addConsoleMessageToMainTarget(...args));
241241
}
242242

243+
function evaluateCommandInConsole(...args) {
244+
return loadLazyDirac().then(() => window.dirac.evaluateCommandInConsole(...args));
245+
}
243246
function registerDiracLinkAction(...args) {
244247
return loadLazyDirac().then(() => window.dirac.registerDiracLinkAction(...args));
245248
}
@@ -294,6 +297,7 @@ console.log("DJS imported!");
294297
subscribeDebuggerEvents: subscribeDebuggerEvents,
295298
unsubscribeDebuggerEvents: unsubscribeDebuggerEvents,
296299
addConsoleMessageToMainTarget: addConsoleMessageToMainTarget,
300+
evaluateCommandInConsole: evaluateCommandInConsole,
297301
startListeningForWorkspaceChanges: startListeningForWorkspaceChanges,
298302
stopListeningForWorkspaceChanges: stopListeningForWorkspaceChanges,
299303
extractScopeInfoFromScopeChainAsync: extractScopeInfoFromScopeChainAsync,

resources/unpacked/devtools/front_end/dirac_lazy/dirac_lazy.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,18 @@ Object.assign(window.dirac, (function() {
197197
SDK.consoleModel.addMessage(msg);
198198
}
199199

200+
function evaluateCommandInConsole(contextName, code) {
201+
let context = contextName === "current" ? lookupCurrentContext() : lookupDefaultContext();
202+
if (!context) {
203+
console.warn("evaluateCommandInConsole got null '" +contextName+ "' context:", code);
204+
return;
205+
}
206+
const commandMessage = new SDK.ConsoleMessage(context.runtimeModel, SDK.ConsoleMessage.MessageSource.JS, null, code, SDK.ConsoleMessage.MessageType.Command);
207+
commandMessage.setExecutionContextId(context.id);
208+
commandMessage.skipHistory = true;
209+
SDK.consoleModel.evaluateCommandInConsole(context, commandMessage, code, false);
210+
}
211+
200212
// --- scope info -------------------------------------------------------------------------------------------------------
201213

202214
function getScopeTitle(scope) {
@@ -919,6 +931,7 @@ Object.assign(window.dirac, (function() {
919931
subscribeDebuggerEvents: subscribeDebuggerEvents,
920932
unsubscribeDebuggerEvents: unsubscribeDebuggerEvents,
921933
addConsoleMessageToMainTarget: addConsoleMessageToMainTarget,
934+
evaluateCommandInConsole: evaluateCommandInConsole,
922935
startListeningForWorkspaceChanges: startListeningForWorkspaceChanges,
923936
stopListeningForWorkspaceChanges: stopListeningForWorkspaceChanges,
924937
extractScopeInfoFromScopeChainAsync: extractScopeInfoFromScopeChainAsync,

src/implant/dirac/implant/eval.cljs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,16 @@
8383
:default (oget dirac "evalInDefaultContext")
8484
:current (oget dirac "evalInCurrentContext")))
8585

86+
(defn get-context-name [context]
87+
(case context
88+
:default "default"
89+
:current "current"))
90+
91+
(defn eval-command-in-console! [context js-code]
92+
(let [context-name (get-context-name context)]
93+
(log (str "eval-command-in-console! [" context-name " context] > ") js-code)
94+
(ocall (get-dirac) "evaluateCommandInConsole" context-name js-code)))
95+
8696
(defn get-current-time []
8797
(.getTime (js/Date.)))
8898

@@ -155,6 +165,9 @@
155165
(defn ^:dynamic console-log-template [method & args]
156166
(str "console." method "(" (string/join (interpose "," (map code-as-string args))) ")"))
157167

168+
(defn ^:dynamic cljs-eval-template [code ns]
169+
(str "await cljs_eval(" (code-as-string code) ", {'ns':" (code-as-string ns) "})"))
170+
158171
(defn ^:dynamic prepare-install-playground-runtime-template [before includes after]
159172
(let [template (str before ";" (emit-install-playground-runtime-template))
160173
js-quote (fn [url] (str "'" url "'"))
@@ -484,3 +497,20 @@
484497
(if-not (= (first res) ::ok)
485498
(error "Failed to install playground runtime" res)
486499
(second res))))))
500+
501+
; -- loopback REPL support --------------------------------------------------------------------------------------------------
502+
503+
(defn go-ask-is-cljs-eval-present? []
504+
(go
505+
(let [res (<! (go-call-eval-with-timeout! :default "typeof cljs_eval" 2000 true))]
506+
(if-not (= (first res) ::ok)
507+
(do
508+
(error "Failed to detect cljs_eval presence" (pr-str res))
509+
false)
510+
(do
511+
(log "loopback available" (second res))
512+
(= (second res) "function"))))))
513+
514+
(defn eval-cljs-in-console! [code ns]
515+
(let [js-code (cljs-eval-template code ns)]
516+
(eval-command-in-console! :current js-code)))

src/implant/dirac/implant/intercom.cljs

Lines changed: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
[dirac.shared.async :refer [<! close! go go-channel go-wait put!]]
1616
[dirac.shared.utils :as utils]
1717
[goog.functions :as gfns]
18-
[oops.core :refer [oapply ocall oget gget]])
18+
[oops.core :refer [oapply gcall ocall oget gget]])
1919
(:import goog.net.WebSocket.ErrorEvent))
2020

2121
(def required-repl-api-version 9)
@@ -27,10 +27,12 @@
2727
(defonce ^:dynamic *last-session-id* nil)
2828
(defonce ^:dynamic *last-connect-fn-id* 0)
2929
(defonce ^:dynamic *ignore-next-client-change* false)
30+
(defonce ^:dynamic *loopback-repl-ns* nil)
3031

3132
(def dirac-agent-help-url "https://github.com/binaryage/dirac/blob/master/docs/installation.md#start-dirac-agent")
3233
(def dirac-runtime-help-url "https://github.com/binaryage/dirac/blob/master/docs/installation.md#install-the-dirac-runtime")
3334
(def dirac-upgrading-help-url "https://github.com/binaryage/dirac/blob/master/docs/upgrading.md")
35+
(def dirac-loopback-mode-url "https://github.com/binaryage/dirac/blob/master/docs/faq.md#what-is-loopback-mode")
3436

3537
(defn ^:dynamic repl-api-mismatch-msg [current-api required-api]
3638
(str "Dirac REPL API version mismatch detected.\n"
@@ -109,6 +111,9 @@
109111
"Normally we would inject playground project to get ad-hoc REPL working here "
110112
"but it is currently not supported with shadow-cljs."))
111113

114+
(defn ^:dynamic explain-loopback-mode-msg []
115+
(str "Dirac REPL is running in loopback mode. See " dirac-loopback-mode-url "."))
116+
112117
(defn check-agent-version! [agent-version]
113118
(let [our-version implant-version/version]
114119
(when-not (= agent-version our-version)
@@ -164,6 +169,22 @@
164169
(set! *repl-connected* false)
165170
(set! *last-connect-fn-id* 0))
166171

172+
(defn loopback-repl? []
173+
(= *last-connection-url* "loopback-repl"))
174+
175+
(defn update-loopback-repl! []
176+
(console/set-prompt-ns! *loopback-repl-ns*)
177+
(console/set-prompt-compiler! "loopback" ""))
178+
179+
(defn connect-to-loopback-repl! []
180+
(set! *last-connection-url* "loopback-repl")
181+
(set! *last-session-id* "loopback-session")
182+
(set! *repl-bootstrapped* true)
183+
(set! *repl-connected* true)
184+
(set! *loopback-repl-ns* "cljs.user")
185+
(update-repl-mode!)
186+
(update-loopback-repl!))
187+
167188
(defn on-client-change [_key _ref old new]
168189
(when-not *ignore-next-client-change*
169190
(if (some? new)
@@ -263,15 +284,47 @@
263284
(js->clj scope-info-js :keywordize-keys true)
264285
{}))
265286

287+
(def dirac-special-re #"\(?dirac!?.*\)?")
288+
(def in-ns-re #"\(in-ns\s+'?(.*)\)")
289+
290+
(defn is-dirac-special? [code]
291+
(some? (re-matches dirac-special-re code)))
292+
293+
(defn is-in-ns-call? [code]
294+
(some? (re-matches in-ns-re code)))
295+
296+
(defn handle-dirac-special! []
297+
(gcall "dirac.addConsoleMessageToMainTarget" "log" "info" (explain-loopback-mode-msg))
298+
::command-handled)
299+
300+
(defn handle-in-ns-call! [code]
301+
(let [m (re-matches in-ns-re code)
302+
ns (second m)]
303+
(when (some? ns)
304+
(set! *loopback-repl-ns* ns)
305+
(update-loopback-repl!)
306+
::command-handled)))
307+
308+
(defn attempt-to-handle-loopback-repl-specials! [code]
309+
(cond
310+
(is-dirac-special? code) (handle-dirac-special!)
311+
(is-in-ns-call? code) (handle-in-ns-call! code)))
312+
313+
(defn do-loopback-repl-eval! [code]
314+
(if-not (= ::command-handled (attempt-to-handle-loopback-repl-specials! code))
315+
(eval/eval-cljs-in-console! code *loopback-repl-ns*)))
316+
266317
(defn send-eval-request! [job-id code scope-info]
267-
(when (repl-ready?)
268-
(console/announce-job-start! job-id (str "eval: " code))
269-
(let [message {:op "eval"
270-
:dirac "short-circuit-presentation"
271-
:id job-id
272-
:code code
273-
:scope-info (prepare-scope-info scope-info)}]
274-
(nrepl-tunnel-client/tunnel-message! (utils/compact message)))))
318+
(if-not (loopback-repl?)
319+
(when (repl-ready?)
320+
(console/announce-job-start! job-id (str "eval: " code))
321+
(let [message {:op "eval"
322+
:dirac "short-circuit-presentation"
323+
:id job-id
324+
:code code
325+
:scope-info (prepare-scope-info scope-info)}]
326+
(nrepl-tunnel-client/tunnel-message! (utils/compact message))))
327+
(do-loopback-repl-eval! code)))
275328

276329
(defn ws-url [host port]
277330
(str "ws://" host ":" port))
@@ -311,6 +364,12 @@
311364
(register-bootstrap-done-hook! enter-playground!)
312365
(<! (go-init-repl! true))))
313366

367+
(defn go-start-loopback-repl! []
368+
(feedback/post! "start-loopback-repl!")
369+
(go
370+
(connect-to-loopback-repl!)
371+
(<! (go-init-repl! true))))
372+
314373
(declare go-react-on-global-object-cleared!)
315374

316375
(defn on-debugger-event [type & args]
@@ -342,11 +401,13 @@
342401
(if (<! (eval/go-ask-is-runtime-repl-enabled?))
343402
(<! (go-start-repl!))
344403
(display-prompt-status (repl-support-not-enabled-msg)))
345-
(if (or no-playground? (hosted?))
346-
(display-prompt-status (missing-runtime-msg runtime-present?))
347-
(if (<! (eval/go-ask-is-shadow-present?))
348-
(display-prompt-status (no-playground-due-to-shadow-msg))
349-
(<! (go-start-playground-repl!)))))))))
404+
(if (<! (eval/go-ask-is-cljs-eval-present?))
405+
(<! (go-start-loopback-repl!))
406+
(if (or no-playground? (hosted?))
407+
(display-prompt-status (missing-runtime-msg runtime-present?))
408+
(if (<! (eval/go-ask-is-shadow-present?))
409+
(display-prompt-status (no-playground-due-to-shadow-msg))
410+
(<! (go-start-playground-repl!))))))))))
350411

351412
(defn go-react-on-global-object-cleared! []
352413
(reset-repl-state!)

0 commit comments

Comments
 (0)