Joyride
repository·master·Indexed 20 days ago
https://github.com/betterthantomorrow/joyrideA VS Code extension that makes the editor hackable by allowing users to extend and script it using ClojureScript or JavaScript via the Small Clojure Interpreter (SCI). It provides direct access to the VS Code API for interactive customization and automation through User and Workspace scopes, activation scripts, and a convenience API for Webviews called Flares.
What's inside joyride
- Joyride is a VS Code extension that enables runtime extensibility using ClojureScript and JavaScript. It allows users to execute code via a REPL or keyboard shortcuts, providing full access to the VS Code API. It follows an Emacs-inspired model where users can automate workflows and extend the editor without writing traditional VS Code extensions.
Understand Joyride Activation Scripts
masterJoyride uses activation scripts to run code at different scopes within VS Code. There are two primary types:
- User-level (
user_activate.cljs): Runs for all workspaces. This is the place to register global behaviors, such as custom keybindings, global output channels, or universal command palettes. - Workspace-level (
workspace_activate.cljs): Runs only for the current workspace. This is ideal for project-specific automation, such as registering event handlers for specific file types or workspace-specific tools.
Key Patterns:
- Disposables: Use
joyride.core/extension-contextto push disposables onto itssubscriptionsarray. This ensures VS Code automatically cleans up your registered events/commands when Joyride is deactivated. - Re-runnability: When writing scripts that register event handlers (like
vscode/workspace.onDidOpenTextDocument), always dispose of the previous registration before re-registering to avoid duplicate handlers when the script is re-evaluated.
- User-level (
Protect script side-effects with a Script Execution Guard
masterWhen developing scripts, you often load namespaces in a REPL. To prevent the main logic of a script from executing every time the file is loaded (which would trigger unwanted side effects), wrap your main entry point in a guard that checks if the file was invoked as a script.
(when (= (joyride/invoked-script) joyride/*file*) (main))Configure script activation lifecycle
masterYou can execute code automatically when Joyride activates by providing activation scripts. These scripts are run in the following order:
<User scripts directory>/user_activate.cljs<Workspace scripts directory>/workspace_activate.cljs
Implement VS Code and Node.js interfaces in Joyride SCI
masterBecause Joyride uses SCI, you cannot use
deftypeto implement interfaces. Instead, use a plain JavaScript object (#js) containing the required functions. Use these objects wherever an instance of that interface is expected.;; Instead of: ;; (deftype Foo [] (bar [x y] ...)) ;; Use: #js {:bar (fn [x y] ...)}JavaScript Interop: Objects and Destructuring
masterSince Joyride runs in a JS environment, you frequently interact with JS objects and interfaces.
Creating JS Objects: Use the
#jsliteral to create native JavaScript objects. This is required for passing options to VS Code APIs that expect JS objects.#js {:onClick (fn [event] (println "Clicked!"))}Destructuring JS Objects: To work with JS objects in a Clojure-idiomatic way, use
js->cljwith:keywordize-keys trueto convert them to Clojure maps.;; Creating JS objects #js {:onClick (fn [event] (println "Clicked!")) :onHover (fn [event] (println "Hovered!"))} ;; Destructuring JS Objects (let [{:keys [uri fsPath]} (js->clj workspace-folder :keywordize-keys true)] (println "Workspace at:" fsPath))Understand the Joyride classpath
masterJoyride searches for your source files in a fixed order of directories. The first match found is used. The search order is:
<workspace-root>/.joyride/src<workspace-root>/.joyride/scripts<user-home>/.config/joyride/src<user-home>/.config/joyride/scripts
Organize User and Workspace scripts
masterJoyride automatically discovers scripts based on their location in your file system. Use the following directory structures to organize your scripts:
User Scripts
Store global scripts in your user configuration directory:
<user home>/.config/joyride/scripts/**/*.cljsWorkspace Scripts
Store project-specific scripts within your workspace root:
.joyride/scripts/**/*.cljsManage state and disposables in Joyride
masterJoyride uses a central application state and a disposable pattern to manage resources and cleanup.
- Central State: The application state is managed via the
!app-dbatom, which tracks:disposables,:extension-context,:workspace-root-path, and the:invoked-script. - Disposables: To prevent resource leaks, use
push-disposable!to register cleanup tasks andclear-disposables!to execute them.
;; Central app state (defonce !app-db (atom {:disposables [] :extension-context nil :workspace-root-path nil :invoked-script nil}))- Central State: The application state is managed via the
Communicate between Extension and Webview
masterJoyride supports bidirectional communication between your extension logic and the flare's webview.
- Extension $\rightarrow$ Webview: Use
post-message!+to send a serialized JSON message to a specific flare. - Webview $\rightarrow$ Extension: Provide a
:message-handlerfunction when callingflare!+. This function will be invoked whenever the webview sends a message.
;; Extension $\rightarrow$ Webview (post-message!+ :my-flare {:action "update" :data {...}}) ;; Webview $\rightarrow$ Extension (flare!+ {:key :my-flare :html "..." :message-handler (fn [msg] (js/console.log "Received:" msg))})- Extension $\rightarrow$ Webview: Use
Manage resources and disposables in reloadable scripts
masterTo prevent event handlers and UI elements (like status bar items) from piling up every time you re-run a script, use a pattern for managing disposables. This ensures that old resources are cleaned up before new ones are created.
Recommended Pattern:
- Maintain an atom of
!disposables. - Create a
clear-disposables!function to iterate and call.dispose()on everything in the atom. - Create a
register-disposable!function that both adds the item to your atom and pushes it to thejoyride/extension-contextsubscriptions. - Call
clear-disposables!at the start of yourmainfunction.
;; Pattern for reloadable scripts that create disposables (defonce !disposables (atom [])) (defn clear-disposables! [] "Dispose all existing disposables and clear the list" (run! #(.dispose %) @!disposables) (reset! !disposables [])) (defn register-disposable! [disposable] "Register a disposable for cleanup and with VS Code" (swap! !disposables conj disposable) (.push (.-subscriptions (joyride/extension-context)) disposable)) (defn main [] ;; Clear any existing disposables first (makes script reloadable) (clear-disposables!) ;; Now create new disposables (register-disposable! (vscode/workspace.onDidOpenTextDocument my-handler)) ;; Create status bar button (will be recreated on re-run) (register-disposable! (doto (vscode/window.createStatusBarItem vscode/StatusBarAlignment.Left) (aset "text" "My Button") (aset "command" "my.command") (.show)))) ;; Use in activation scripts and regular scripts (when (= (joyride/invoked-script) joyride/*file*) (main))- Maintain an atom of
Workspace vs User: Decision Guide
masterDecide where to place your Joyride code based on its scope and intended audience:
Criterion Workspace ( .joyride/)User ( ~/.config/joyride/)Applies to This project only All workspaces Shareable Yes — commit to repo No — personal setup Overrides Wins over user-level code Provides defaults Typical use Project tooling, build helpers Personal editor customizations Activation workspace_activate.cljsuser_activate.cljsRule of thumb: If it's useful in every workspace, put it in user. If it's project-specific or team-shareable, put it in workspace.