diff --git a/CHANGELOG.md b/CHANGELOG.md index f568fbc..9ec27ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes to Joyride ## [Unreleased] +- [Less boilerplate-y require of APIs from VS Code extensions](https://github.com/BetterThanTomorrow/joyride/issues/71) + ## [0.0.12] - 2022-05-18 - [Enable access to more `promesa.core` vars](https://github.com/BetterThanTomorrow/joyride/issues/68) diff --git a/README.md b/README.md index 18e24f8..268c06f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ https://user-images.githubusercontent.com/30010/165934412-ffeb5729-07be-4aa5-b29 Joyride is Powered by [SCI](https://github.com/babashka/sci) (Small Clojure Interpreter). +See [doc/api.md](https://github.com/BetterThanTomorrow/joyride/blob/master/doc/api.md) for documentation about the Joyride API. + ## WIP You are entering a construction yard. Things are going to change and break your configs while we are searching for good APIs and UI/Ux. diff --git a/deps.edn b/deps.edn index bbf2c74..6c7563e 100644 --- a/deps.edn +++ b/deps.edn @@ -4,7 +4,7 @@ org.babashka/sci #_{:local/root "../babashka/sci"} {:git/url "https://github.com/babashka/sci" - :git/sha "07e21bdab3c1365f7b2a06377e1c56fa09e95a15"} + :git/sha "37722fb5e49970fe428781b287f478d8310fd3df"} org.babashka/sci-configs #_{:local/root "../sci.configs"} {:git/url "https://github.com/babashka/sci-configs" diff --git a/doc/api.md b/doc/api.md index fbf4236..fe7c583 100644 --- a/doc/api.md +++ b/doc/api.md @@ -22,7 +22,33 @@ You can make some code run when Joyride activates, by naming the scripts `activa Look at the [Activation example](../examples/.joyride/scripts/activate.cljs) script for a way to use this, and for a way to make the script re-runnable. -### Namespaces +### NPM modules + +TBD. + +### VS Code, and Extension ”namespaces” + +Joyride exposes its `vscode` module for scripts to consume. You require it like so: + +```clojure +(ns joyride-api + (:require ... + ["vscode" :as vscode] + ...)) +``` + +VS Code Extensions that export an API can be required using the `ext://` prefix followed by the extension identifier. For instance, to require [Calva's Extension API](https://calva.io/api/) use `"ext://betterthantomorrow.calva"`. Optionally you can specify any submodules in the exported API by suffixing the namespace with a `$` followed by the dotted path of the submodule. You can also `refer` objects in the API and submodules. Like so: + +```clojure +(ns z-joylib.calva-api + (:require ... + ["ext://betterthantomorrow.calva$v0" :as calva :refer [repl ranges]]) + ...) + +(def current-form-text (second (ranges.currentForm))) +``` + +### ClojureScript Namespaces In addition to `clojure.core`, `clojure.set`, `clojure.edn`, `clojure.string`, `clojure.walk`, `clojure.data`, Joyride exposes diff --git a/examples/.joyride/scripts/joyride_api.cljs b/examples/.joyride/scripts/joyride_api.cljs index e56f95e..757e58c 100644 --- a/examples/.joyride/scripts/joyride_api.cljs +++ b/examples/.joyride/scripts/joyride_api.cljs @@ -1,14 +1,14 @@ (ns joyride-api (:require ["vscode" :as vscode] + ["ext://betterthantomorrow.joyride" :as joy-api] [promesa.core :as p] [joyride.core :as joyride])) (def joyrideExt (vscode/extensions.getExtension "betterthantomorrow.joyride")) -(def joyApi (.-exports joyrideExt)) (comment ;; Starting the nREPL server - (-> (.startNReplServer joyApi) + (-> (joy-api/startNReplServer) (p/catch (fn [e] (println (.-message e) e)))) ;; (Oh, yes, it's already started, of course.) ;; Try first stopping the server? That will not help, @@ -18,13 +18,13 @@ (comment ;; Getting contexts - (.getContextValue joyApi "joyride.isNReplServerRunning") - + (joy-api/getContextValue "joyride.isNReplServerRunning") + ;; Non-Joyride context keys returns `nil` - (.getContextValue joyApi "foo.bar") + (joy-api/getContextValue "foo.bar") ;; NB: Use the extension instance for this! - (.getContextValue joyApi "joyride.isActive") + (joy-api/getContextValue "joyride.isActive") ;; Like so: (.-isActive joyrideExt) ;; (Not that it matters, you can't deactivate Joyride, @@ -38,8 +38,7 @@ .-extension .-exports) (require '[clojure.repl :refer [doc]]) - (doc joyride/extension-context) - ) + (doc joyride/extension-context)) ;; in addition to the extension context, joyride.core also has: ;; * *file* - the absolute path of the file where an diff --git a/examples/.joyride/scripts/z_joylib/calva_api.cljs b/examples/.joyride/scripts/z_joylib/calva_api.cljs index 783c1e3..e2ec49c 100644 --- a/examples/.joyride/scripts/z_joylib/calva_api.cljs +++ b/examples/.joyride/scripts/z_joylib/calva_api.cljs @@ -1,26 +1,19 @@ (ns z-joylib.calva-api (:require ["vscode" :as vscode] + ["ext://betterthantomorrow.calva$v0" :as calva] [joyride.core :as joyride] [promesa.core :as p] [z-joylib.editor-utils :as editor-utils])) (def oc (joyride.core/output-channel)) -(def calva (vscode/extensions.getExtension "betterthantomorrow.calva")) -(def calvaApi (-> calva - .-exports - .-v0 - (js->clj :keywordize-keys true))) - -(defn text-for-ranges-key [ranges-key] - (second ((get-in calvaApi [:ranges ranges-key])))) (defn evaluate-in-session+ [session-key code] - (p/let [result ((get-in [:repl :evaluateCode] calvaApi) + (p/let [result (calva/repl.evaluateCode session-key code #js {:stdout #(.append oc %) :stderr #(.append oc (str "Error: " %))})] - (.-result result))) + (.-result result))) (defn clj-evaluate+ [code] (evaluate-in-session+ "clj" code)) @@ -31,7 +24,7 @@ (defn evaluate+ "Evaluates `code` in whatever the current session is." [code] - (evaluate-in-session+ ((get-in calvaApi [:repl :currentSessionKey])) code)) + (evaluate-in-session+ (calva/repl.currentSessionKey) code)) (defn evaluate-selection+ [] (p/let [code (editor-utils/current-selection-text) @@ -41,10 +34,10 @@ ;; Utils for REPL-ing Joyride code, when connected to a project REPL. (defn joyride-eval-current-form+ [] - (vscode/commands.executeCommand "joyride.runCode" (text-for-ranges-key :currentForm))) + (vscode/commands.executeCommand "joyride.runCode" (second (calva/ranges.currentForm)))) (defn joyride-eval-top-level-form+ [] - (vscode/commands.executeCommand "joyride.runCode" (text-for-ranges-key :currentTopLevelForm))) + (vscode/commands.executeCommand "joyride.runCode" (second (calva/ranges.currentTopLevelForm)))) ;; Bind to some nice keyboard shortcuts, e.g. like so: ;; { @@ -62,4 +55,4 @@ (defn restart-clojure-lsp [] (p/do (vscode/commands.executeCommand "calva.clojureLsp.stop") - (vscode/commands.executeCommand "calva.clojureLsp.start"))) \ No newline at end of file + (vscode/commands.executeCommand "calva.clojureLsp.start"))) diff --git a/src/joyride/sci.cljs b/src/joyride/sci.cljs index ca1d8ed..afa9ca9 100644 --- a/src/joyride/sci.cljs +++ b/src/joyride/sci.cljs @@ -3,6 +3,7 @@ ["path" :as path] ["vscode" :as vscode] [clojure.string :as str] + [goog.object :as gobject] [joyride.db :as db] [joyride.config :as conf] [sci.configs.funcool.promesa :as pconfig] @@ -31,6 +32,27 @@ {:file ns-path :source (str (fs/readFileSync path-to-load))}))) +(def ^:private extension-namespace-prefix "ext://") + +(defn- extract-extension-name [namespace] + (subs namespace (count extension-namespace-prefix))) + +(defn- active-extension? [namespace] + (when (.startsWith namespace extension-namespace-prefix) + (let [[extension-name _module-name] (.split (extract-extension-name namespace) "$") + extension (vscode/extensions.getExtension extension-name)] + (and extension + (.-isActive extension))))) + +(defn- extension-module [namespace] + (let [[extension-name module-name] (.split (extract-extension-name namespace) "$") + extension (vscode/extensions.getExtension extension-name)] + (when extension + (when-let [exports (.-exports extension)] + (if module-name + (gobject/getValueByKeys exports (.split module-name ".")) + exports))))) + (def !ctx (volatile! (sci/init {:classes {'js goog/global @@ -41,19 +63,36 @@ 'extension-context (sci/copy-var db/extension-context joyride-ns) 'invoked-script (sci/copy-var db/invoked-script joyride-ns) 'output-channel (sci/copy-var db/output-channel joyride-ns)}) - :load-fn (fn [{:keys [namespace opts]}] + :load-fn (fn [{:keys [ns libname opts]}] (cond - (symbol? namespace) - (source-script-by-ns namespace) - (string? namespace) ;; node built-in or npm library - (if (= "vscode" namespace) + (symbol? libname) + (source-script-by-ns libname) + (string? libname) ;; node built-in or npm library + (cond + (= "vscode" libname) (do (sci/add-class! @!ctx 'vscode vscode) - (sci/add-import! @!ctx (symbol (str @sci/ns)) 'vscode (:as opts)) + (sci/add-import! @!ctx ns 'vscode (:as opts)) {:handled true}) - (let [mod (js/require namespace) - ns-sym (symbol namespace)] + + (active-extension? libname) + (let [module (extension-module libname) + munged-ns (symbol (munge libname)) + refer (:refer opts)] + (sci/add-class! @!ctx munged-ns module) + (sci/add-import! @!ctx ns munged-ns (:as opts)) + (when refer + (doseq [sym refer] + (let [prop (gobject/get module sym) + sub-sym (symbol (str munged-ns "$" sym))] + (sci/add-class! @!ctx sub-sym prop) + (sci/add-import! @!ctx ns sub-sym sym)))) + {:handled true}) + + :else + (let [mod (js/require libname) + ns-sym (symbol libname)] (sci/add-class! @!ctx ns-sym mod) - (sci/add-import! @!ctx (symbol (str @sci/ns)) ns-sym + (sci/add-import! @!ctx ns ns-sym (or (:as opts) ns-sym)) {:handled true}))))})))