| name | joyride |
| description | Write effective Joyride scripts and source files in ClojureScript. Covers VS Code API interop, async/promise handling, data-oriented functional design, REPL-driven development, disposable lifecycle, Flares (WebViews), and extension API access. Use when: writing or editing .cljs files for Joyride, automating VS Code, building interactive tools with the VS Code API, creating Flare WebViews, fixing or debugging Joyride scripts, troubleshooting async/promise issues, working with joyride.core or promesa.core, or doing REPL-driven ClojureScript development in VS Code. REPL: use joyride_evaluate_code, not the Backseat Driver clojure_evaluate_code — the Joyride REPL is a separate runtime. |
Joyride — Effective Scripting
Joyride runs SCI ClojureScript in VS Code's Extension Host with full access to the VS Code API. Scripts and source files automate VS Code in user space — no extension authoring required.
When to Use This Skill
- Writing or editing any
.cljs file intended for Joyride
- Using the VS Code API from ClojureScript
- Building interactive tools, commands, or UI (quick picks, status bar items, WebViews)
- Working with async operations, promises, or the Joyride REPL
- Designing data-oriented, functional solutions for VS Code automation
Related Skills (Cross-References)
| Skill | Scope |
|---|
| joyride-user-scripts | User-global scripts/source in the Joyride User directory — activation, classpath, keybindings |
| joyride-workspace-automation | Workspace .joyride/ directory — project-local scripts, activation, workspace-specific commands |
This skill covers how to write effective Joyride code. The related skills cover where code lives and how it's organized.
Principles
Data-Oriented, Functional Design
Look for Prior Art
Before building something new, check the user's existing Joyride scripts and source for patterns, utilities, and solutions. The Joyride User directory (joyride/user-joyride-dir) contains scripts/ and src/ with working code that may already solve part of the problem — or demonstrate project-specific conventions to follow.
;; Find existing .cljs files in the User Joyride directory
(p/let [files (vscode/workspace.findFiles
(str (joyride/user-joyride-dir) "/**/*.cljs"))]
(mapv #(.-fsPath %) files))
Reuse existing namespace requires, utility functions, and data patterns rather than reinventing them.
REPL-First Development
Always use the Joyride REPL tool (joyride_evaluate_code) for evaluating Joyride code — not a general-purpose Clojure REPL. The Joyride REPL runs inside VS Code's Extension Host where it has access to the VS Code API, handles promises correctly via awaitResult, and shares state with running scripts.
Before writing to files, develop in the REPL:
- Start small — evaluate tiny sub-expressions to understand APIs
- Build up — compose working pieces into larger expressions
- Verify each step — every expression should return the expected result
- Only then write files — once the solution is proven in the REPL
Evaluating sub-expressions is preferred over println / js/console.log. Directly inspect values instead of printing them.
Namespace Targeting
When evaluating in the REPL, always target the correct namespace. Functions defined in the wrong namespace (e.g., user instead of your intended ns) become unavailable where expected.
Gather → Transform → Act
Structure code as a data pipeline: gather data from the VS Code world, transform it as pure Clojure data, then act with side effects.
;; Gather: pull data from VS Code into Clojure
(p/let [editor vscode/window.activeTextEditor]
(let [;; Transform: flat map with namespaced keys — pure data
doc-data {:doc/uri (-> editor .-document .-uri .-fsPath)
:doc/lang (-> editor .-document .-languageId)
:cursor/line (-> editor .-selection .-active .-line)}]
;; Act: decision is pure, side effect is thin
(when (= (:doc/lang doc-data) "clojure")
(vscode/window.showInformationMessage
(str "Clojure file at line " (:cursor/line doc-data))))))
The data map IS the testable unit. Print it, filter it, assert on it — all in the REPL.
VS Code API Interop
Access VS Code APIs via JS interop. Use vscode/api.method for functions and members. Use #js {} literals for plain JS objects — never reify or deftype to implement JS interfaces.
(require '["vscode" :as vscode])
;; Functions and methods
(vscode/window.showInformationMessage "Hello!")
(vscode/commands.executeCommand "workbench.action.files.save")
;; Quick pick (returns a promise)
(vscode/window.showQuickPick #js ["Option A" "Option B"])
;; Input box (returns a promise)
(vscode/window.showInputBox #js {:prompt "Enter a value:"})
;; Plain JS objects for options
#js {:placeHolder "Search..." :matchOnDescription true}
Implementing JS Interfaces
VS Code APIs often expect objects implementing interfaces (e.g., TreeDataProvider, TextDocumentContentProvider). In SCI, reify with Object fails (Unable to resolve symbol: Object) and deftype only supports built-in methods like toString — not arbitrary interface methods.
Always use #js {} with function values:
;; Implementing a VS Code interface
(def provider
#js {:provideTextDocumentContent (fn [uri] "content here")
:onDidChange js/undefined})
;; Registering with VS Code
(vscode/workspace.registerTextDocumentContentProvider "my-scheme" provider)
Property Access
;; Read properties with .-
(.-uri vscode/window.activeTextEditor.document)
;; Nested property access
(-> vscode/window.activeTextEditor .-document .-uri .-fsPath)
Async / Promise Handling
Joyride uses promesa.core for async operations. Many VS Code APIs return promises.
(require '[promesa.core :as p])
;; p/let chains async steps — each binding awaits the previous
(p/let [result (vscode/window.showInputBox #js {:prompt "Name?"})
files (vscode/workspace.findFiles "**/*.cljs")]
(when result
{:input result :file-count (count files)}))
Capturing Async Results for REPL Exploration
;; Bind async results to vars for later inspection
(p/let [files (vscode/workspace.findFiles "**/*.cljs")]
(def found-files files))
;; Now evaluate `found-files` to explore the data
When to Await
| Scenario | Await? |
|---|
| User input (showInputBox, showQuickPick) | Yes — need the response |
| File operations (findFiles, readFile) | Yes — need the data |
| Extension API calls returning promises | Yes — need the result |
| Fire-and-forget info messages | No |
| Side-effect-only async ops | No |
Joyride Core API
(require '[joyride.core :as joyride])
joyride/*file* ; Path of the current file
(joyride/invoked-script) ; Script being run (nil in REPL)
(joyride/extension-context) ; VS Code ExtensionContext
(joyride/output-channel) ; Joyride's OutputChannel
joyride/user-joyride-dir ; User joyride directory path
;; Async file operations
(joyride/slurp "path/to/file") ; Returns promise — like clojure.core/slurp but async
(joyride/load-file "path/to.cljs"); Returns promise — loads and evaluates a file
Note: load-file is the Joyride async version. Clojure's load-file is not implemented in SCI.
Script Execution Guard
Scripts that should only execute when run directly (not when loaded via require in other code or the REPL):
(when (= (joyride/invoked-script) joyride/*file*)
(main))
This is Joyride's equivalent of Python's if __name__ == "__main__".
Disposable Lifecycle
VS Code uses disposables for event subscriptions, status bar items, commands, and other UI elements. Always manage their lifecycle.
Registering with Extension Context
For items that should live as long as Joyride is active:
(let [disposable (vscode/workspace.onDidOpenTextDocument handler)]
(.push (.-subscriptions (joyride/extension-context)) disposable))
Holding References for Dynamic UI
For items you want to modify or dispose at will (status bar buttons, decorations):
;; Hold a reference so you can update/dispose later
(defonce !status-item (atom nil))
(defn create-status-item! []
(when-let [old @!status-item] (.dispose old))
(let [item (vscode/window.createStatusBarItem
vscode/StatusBarAlignment.Left 0)]
(set! (.-text item) "$(rocket) My Tool")
(set! (.-command item) "my.command")
(.show item)
(reset! !status-item item)
item))
Extension API Access
Safely access other extensions:
(defn get-extension-api [extension-id]
(when-let [ext (vscode/extensions.getExtension extension-id)]
(when (.-isActive ext)
(.-exports ext))))
;; Example: Git extension
(when-let [git-api (some-> (vscode/extensions.getExtension "vscode.git")
.-exports
(.getAPI 1))]
(.-repositories git-api))
Always check availability and activation state before use.
Flares — WebView Creation
Flares provide a convenient way to create WebView panels and sidebar views using Hiccup syntax.
(require '[joyride.flare :as flare])
;; Panel with Hiccup
(flare/flare!+ {:html [:div [:h1 "Hello"] [:p "World"]]
:title "My Panel"
:key "my-panel"})
;; Sidebar slot (1-5 available)
(flare/flare!+ {:html [:div [:h2 "Sidebar"]]
:key :sidebar-1})
;; From file
(flare/flare!+ {:file "assets/view.html" :key "file-view"})
;; External URL
(flare/flare!+ {:url "https://example.com" :title "Docs"})
;; Management
(flare/ls) ; List open flares
(flare/close! "key") ; Close by key
(flare/close-all!) ; Close all
flare!+ returns a promise. Hiccup :style attributes use maps: {:color :red :margin "10px"}.
For bidirectional communication, use :message-handler and post-message!+.
Fetching Web Resources
Joyride can fetch web content directly — use this to pull documentation, API specs, raw files from GitHub, or any text resource:
(p/let [response (js/fetch "https://raw.githubusercontent.com/user/repo/main/README.md")
text (.text response)]
(def readme-content text))
This is especially useful for:
- Pulling up-to-date documentation or examples before writing code
- Fetching configuration or data files from remote sources
- Accessing raw content from GitHub repos (use
raw.githubusercontent.com URLs)
Keybinding Pattern
Document keybindings in source files for discoverability:
;; "Install" by placing this file in the Joyride User src/ directory
;; and adding this keybinding to keybindings.json:
;; {
;; "key": "ctrl+alt+j ctrl+alt+x",
;; "command": "joyride.runCode",
;; "args": "(require '[my-ns :as m] :reload) (m/my-command!+)"
;; }
The :reload ensures the latest code is executed.
Anti-Patterns and Corrections
| Anti-pattern | Correction |
|---|
Using println / js/console.log to inspect | Evaluate sub-expressions directly in the REPL |
Top-level side effects outside defn | Wrap in functions; use script execution guard |
load-file (Clojure built-in) | Use joyride/load-file (async, returns promise) |
Using reify or deftype for JS interfaces | Use #js {} with function values — reify Object fails in SCI |
| Forgetting to dispose UI elements | Hold references; register with extension context |
Reading @atom in pure functions | Pass data as function arguments |
| Deep nested maps for domain data | Flat maps with namespaced keywords |
| Forward declaring functions | Define before use — rearrange file order |
| Hardcoded fallback configs hiding errors | Fail fast with clear error messages |
| Mixing business logic with side effects | Pure functions for decisions; thin side-effect layer |
| Starting from scratch without checking existing code | Look for prior art in the User Joyride directory first |
SCI / Scittle Async Gotchas
- Use
^:async + await for async functions in SCI
js-await is Squint-specific — fails in SCI with "Unable to resolve symbol"
- Top-level
await is unsupported — await must be inside an ^:async function
- Use
promesa.core/let (p/let) as the primary async coordination tool
Troubleshooting
| Issue | Solution |
|---|
| Function not found after REPL eval | Check namespace targeting — may have ended up in user |
Promise result is #object[Promise] | Use p/let to unwrap, or awaitResult: true in eval tool |
| Extension API returns nil | Check isActive — extension may not be activated yet |
| Status bar item not showing | Call .show and verify it's not disposed |
Script runs on require when it shouldn't | Add script execution guard |
load-file not working | Use joyride.core/load-file (async version) |
Testing
Pure-function-first design enables testing in the REPL with cljs.test. Tests reside in src/test/ with namespaces starting with test.. Run all tests:
(do (require 'run-all-tests :reload) (run-all-tests/run!+))
Develop tests interactively in the REPL before committing them to files.
References