diff --git a/README.md b/README.md index 95015bb..1a0cd15 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,8 @@ See the [examples](./examples) for examples including: * Terminal creation and control * Fontsize manipulation * Calva Structural Editing enhancements +* Opening and showing project files +* Workspace activation script ## Support and feedback diff --git a/doc/api.md b/doc/api.md index f31b0ba..c570919 100644 --- a/doc/api.md +++ b/doc/api.md @@ -1,21 +1,32 @@ # APIs -There are two different APIs here: +The Joyride API consist of: 1. The Joyride *Scripting API* + * Scripting life-cycle management + * Included clojure library namespaces 1. The Joyride *Extension API* Please note that Joyride's *Extension API* is also available to *Joyride scripts*. ## Scripting API +### Scripting life-cycle + +You can make some code run when Joyride activates, by naming the scripts `activate.cljs`. The activations script will be run in the order: + +1. `/activate.cljs` +1. `/activate.cljs` + +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 + In addition to `clojure.core`, `clojure.set`, `clojure.edn`, `clojure.string`, `clojure.walk`, `clojure.data`, Joyride exposes the following namespaces: -## Joyride - -### `joyride.core` +#### `joyride.core` - `*file*`: dynamic var representing the currently executing file - `get-extension-context`: function returning the Joyride [extension context](https://code.visualstudio.com/api/references/vscode-api#ExtensionContext) object @@ -28,15 +39,13 @@ NB: While using `*file*` bare works, it will probably stop working soon. Always ...)) ``` -## Promesa +#### promesa.core See [promesa docs](https://cljdoc.org/d/funcool/promesa/6.0.2/doc/user-guide). -### `promesa.core` - -`p/->>`, `p/->`, `p/all`, `p/any`, `p/catch`, `p/chain`, `p/create`, `p/deferred`, `p/delay`, `p/do`, `p/do`, `p/done`, `p/finally`, `p/let`, `p/map`, `p/mapcat`, `p/pending`, `p/promise`, `p/promise`, `p/race`, `p/rejected`, `p/rejected`, `p/resolved`, `p/resolved`, `p/run`, `p/then`, `p/thenable`, `p/with`, and `p/wrap` +**``**: `p/->>`, `p/->`, `p/all`, `p/any`, `p/catch`, `p/chain`, `p/create`, `p/deferred`, `p/delay`, `p/do`, `p/do`, `p/done`, `p/finally`, `p/let`, `p/map`, `p/mapcat`, `p/pending`, `p/promise`, `p/promise`, `p/race`, `p/rejected`, `p/rejected`, `p/resolved`, `p/resolved`, `p/run`, `p/then`, `p/thenable`, `p/with`, and `p/wrap` -## Possibly coming additions +### Possibly coming additions We want to add `clojure.test` and `clojure.pprint` as well in the near future. How near/if depends on things like how much time we can spend on it, and how easy/hard it will be to port this over from [nbb](https://github.com/babashka/nbb). diff --git a/examples/.joyride/scripts/activate.cljs b/examples/.joyride/scripts/activate.cljs new file mode 100644 index 0000000..308868b --- /dev/null +++ b/examples/.joyride/scripts/activate.cljs @@ -0,0 +1,41 @@ +(ns activate + (:require [joyride.core :as joyride] + ["vscode" :as vscode] + [promesa.core :as p])) + +(println "Hello World, from Workspace activate.cljs script") + +(defonce !db (atom {:disposables []})) + +;; To make the activation script re-runnable we dispose of +;; event handlers and such that we might have registered +;; in previous runs. +(defn- clear-disposables! [] + (run! (fn [disposable] + (.dispose disposable)) + (:disposables @!db)) + (swap! !db assoc :disposables [])) + +;; Pushing the disposables on the extension context's +;; subscriptions will make VS Code dispose of them when the +;; Joyride extension is deactivated. +(defn- push-disposable [disposable] + (swap! !db update :disposables conj disposable) + (-> (joyride/get-extension-context) + .-subscriptions + (.push disposable))) + +(defn- my-main [] + (clear-disposables!) + (push-disposable + ;; It might surprise you to see how often and when this happens, + ;; and when it doesn't happen. + (vscode/workspace.onDidOpenTextDocument + (fn [doc] + (println "[Joyride example]" + (.-languageId doc) + "document opened:" + (.-fileName doc)))))) + +(when true + (my-main)) diff --git a/examples/.joyride/scripts/joyride_api.cljs b/examples/.joyride/scripts/joyride_api.cljs index 54e7004..def74fa 100644 --- a/examples/.joyride/scripts/joyride_api.cljs +++ b/examples/.joyride/scripts/joyride_api.cljs @@ -34,9 +34,9 @@ ;; Joyride scripts can also reach the Joyride extension ;; through `joyride.core` (require '[joyride.core :as joyride]) + (require '[clojure.repl :refer [doc]]) (-> (joyride/get-extension-context) .-extension .-exports) - (require '[clojure.repl :refer [doc]]) (doc joyride/get-extension-context) - ) \ No newline at end of file + ) diff --git a/examples/README.md b/examples/README.md index 552e578..1d9dc1c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,6 +6,13 @@ Demonstrating some ways to [Joyride VS Code](https://marketplace.visualstudio.co In the `.joyride/scripts` folder you'll find mostly small examples: +## Activation script + +A Workspace [activate.cljs] script that registers a `vscode/workspace.onDidOpenTextDocument` event handler. Demonstrates: + +* Using the `joyride.core/get-extension-context` to push disposables on its `subscriptions` array. Making VS Code dispose of them when Joyride is deactivated. +* A re-runnable recipe to avoid re-registering the event handler. (By disposing it and then re-register.) + ## Create a Webview `webview/example.cljs` diff --git a/src/main/joyride/db.cljs b/src/main/joyride/db.cljs new file mode 100644 index 0000000..b002bde --- /dev/null +++ b/src/main/joyride/db.cljs @@ -0,0 +1,3 @@ +(ns joyride.db) + +(defonce !app-db (atom {})) \ No newline at end of file diff --git a/src/main/joyride/extension.cljs b/src/main/joyride/extension.cljs index ef0e846..dfce40d 100644 --- a/src/main/joyride/extension.cljs +++ b/src/main/joyride/extension.cljs @@ -1,52 +1,27 @@ (ns joyride.extension (:require ["path" :as path] ["vscode" :as vscode] - [clojure.pprint :as pprint] [joyride.config :as conf] - [joyride.when-contexts :as when-contexts] + [joyride.db :as db] + [joyride.life-cycle :as life-cycle] [joyride.nrepl :as nrepl] [joyride.sci :as jsci] [joyride.scripts-menu :refer [show-script-picker+]] - [joyride.utils :refer [info vscode-read-uri+ jsify]] + [joyride.utils :as utils :refer [info jsify vscode-read-uri+]] + [joyride.when-contexts :as when-contexts] [promesa.core :as p] [sci.core :as sci])) -(defonce !db (atom {})) - (defn- register-command! [^js context command-id var] (let [disposable (vscode/commands.registerCommand command-id var)] - (swap! !db update :disposables conj disposable) + (swap! db/!app-db update :disposables conj disposable) (.push (.-subscriptions context) disposable))) (defn- clear-disposables! [] - (swap! !db assoc :disposables []) + (swap! db/!app-db assoc :disposables []) (p/run! (fn [^js disposable] (.dispose disposable)) - (:disposables @!db))) - -(def ^{:dynamic true - :doc "Should the Joyride output channel be revealed after `say`? - Default: `true`"} - *show-when-said?* true) - -(defn say [message] - (let [channel ^js (:output-channel @!db)] - (.appendLine channel message) - (when *show-when-said?* - (.show channel true)))) - -(defn say-error [message] - (say (str "ERROR: " message))) - -(defn say-result - ([result] - (say-result nil result)) - ([message result] - (let [prefix (if (empty? message) - "=> " - (str message "\n=> "))] - (.append ^js (:output-channel @!db) prefix) - (say (with-out-str (pprint/pprint result)))))) + (:disposables @db/!app-db))) (defn run-code ([] @@ -58,7 +33,7 @@ (run-code input)))) ([code] (p/let [result (jsci/eval-string code)] - (say-result result)))) + (utils/say-result result)))) (defn choose-file [default-uri] (vscode/window.showOpenDialog #js {:canSelectMany false @@ -81,9 +56,9 @@ (p/handle (fn [result error] (if error (do - (say-error (str title " Failed: " script-path " " (.-message error))) + (utils/say-error (str title " Failed: " script-path " " (.-message error))) (js/console.error title "Failed: " script-path (.-message error) error)) - (do (say-result (str script-path " evaluated.") result) + (do (utils/say-result (str script-path " evaluated.") result) result))))))) (def run-workspace-script-args ["Run Workspace Script" @@ -115,12 +90,17 @@ (defn ^:export activate [^js context] (when context - (reset! !db {:output-channel (vscode/window.createOutputChannel "Joyride") + (reset! db/!app-db {:output-channel (vscode/window.createOutputChannel "Joyride") :extension-context context :disposables []}) - (vreset! jsci/!extension-context context) - (say "🟢 Joyride VS Code with Clojure. 🚗")) - (let [{:keys [extension-context]} @!db] + (utils/say "🟢 Joyride VS Code with Clojure. 🚗") + (p/-> (life-cycle/maybe-run-init-script+ run-user-script+ + (:user life-cycle/init-scripts)) + (p/then + (fn [_result] + (life-cycle/maybe-run-init-script+ run-workspace-script+ + (:workspace life-cycle/init-scripts)))))) + (let [{:keys [extension-context]} @db/!app-db] (register-command! extension-context "joyride.runCode" #'run-code) (register-command! extension-context "joyride.runWorkspaceScript" #'run-workspace-script+) (register-command! extension-context "joyride.runUserScript" #'run-user-script+) diff --git a/src/main/joyride/life_cycle.cljs b/src/main/joyride/life_cycle.cljs new file mode 100644 index 0000000..f479f5a --- /dev/null +++ b/src/main/joyride/life_cycle.cljs @@ -0,0 +1,36 @@ +(ns joyride.life-cycle + (:require ["path" :as path] + ["vscode" :as vscode] + [joyride.config :as conf] + [joyride.utils :as utils] + [promesa.core :as p])) + +(def user-init-script "activate.cljs") +(def user-init-script-path (path/join conf/user-config-path + conf/user-scripts-path + user-init-script)) + +(def workspace-init-script "activate.cljs") +(def workspace-init-script-path (path/join conf/workspace-scripts-path + workspace-init-script)) +(def workspace-init-script-abs-path (path/join vscode/workspace.rootPath + workspace-init-script-path)) + +(def init-scripts {:user {:label "User activate" + :script user-init-script + :script-path user-init-script-path + :script-abs-path user-init-script-path} + :workspace {:label "Workspace activate" + :script user-init-script + :script-path workspace-init-script-path + :script-abs-path workspace-init-script-abs-path}}) + +(defn maybe-run-init-script+ [run-fn {:keys [label script script-path script-abs-path]}] + (utils/say (str label " script: " script-path)) + (-> (utils/path-exists?+ script-abs-path) + (p/then (fn [exists?] + (if exists? + (do + (utils/say (str " Running...")) + (run-fn script)) + (utils/say (str " No " label " script present"))))))) \ No newline at end of file diff --git a/src/main/joyride/sci.cljs b/src/main/joyride/sci.cljs index 4c8a804..f72b85b 100644 --- a/src/main/joyride/sci.cljs +++ b/src/main/joyride/sci.cljs @@ -3,6 +3,7 @@ ["path" :as path] ["vscode" :as vscode] [clojure.string :as str] + [joyride.db :as db] [joyride.config :refer [workspace-scripts-path]] [joyride.utils :as utils] [sci-configs.funcool.promesa :as pconfig] @@ -10,13 +11,11 @@ (sci/alter-var-root sci/print-fn (constantly *print-fn*)) -(def !extension-context (volatile! nil)) - (defn get-extension-context "Returns the Joyride extension context object. See: https://code.visualstudio.com/api/references/vscode-api#ExtensionContext" [] - @!extension-context) + (:extension-context @db/!app-db)) (def joyride-ns (sci/create-ns 'joyride.core nil)) diff --git a/src/main/joyride/utils.cljs b/src/main/joyride/utils.cljs index 0f5b0b5..86c273c 100644 --- a/src/main/joyride/utils.cljs +++ b/src/main/joyride/utils.cljs @@ -1,7 +1,9 @@ (ns joyride.utils (:require ["vscode" :as vscode] - [promesa.core :as p] - [clojure.string :as str])) + [clojure.pprint :as pprint] + [clojure.string :as str] + [joyride.db :as db] + [promesa.core :as p])) (defn jsify [clj-thing] (clj->js clj-thing)) @@ -9,6 +11,14 @@ (defn cljify [js-thing] (js->clj js-thing :keywordize-keys true)) +(defn path-exists?+ [path] + (-> (p/let [uri (vscode/Uri.file path)] + (vscode/workspace.fs.stat uri) + true) + (p/catch + (fn [_e] + false)))) + (defn vscode-read-uri+ [^js uri-or-path] (let [uri (if (string? uri-or-path) (vscode/Uri.file uri-or-path) @@ -33,3 +43,27 @@ (defn error [& xs] (vscode/window.showErrorMessage (str/join " " (mapv str xs)))) + +(def ^{:dynamic true + :doc "Should the Joyride output channel be revealed after `say`? + Default: `true`"} + *show-when-said?* true) + +(defn say [message] + (let [channel ^js (:output-channel @db/!app-db)] + (.appendLine channel message) + (when *show-when-said?* + (.show channel true)))) + +(defn say-error [message] + (say (str "ERROR: " message))) + +(defn say-result + ([result] + (say-result nil result)) + ([message result] + (let [prefix (if (empty? message) + "=> " + (str message "\n=> "))] + (.append ^js (:output-channel @db/!app-db) prefix) + (say (with-out-str (pprint/pprint result)))))) \ No newline at end of file