Skip to content

Commit 6694eff

Browse files
committed
Merge branch dev into published
2 parents 1b601d8 + f6ce177 commit 6694eff

File tree

8 files changed

+214
-38
lines changed

8 files changed

+214
-38
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ Changes to Calva.
44

55
## [Unreleased]
66

7+
## [2.0.530] - 2025-09-22
8+
9+
- [Make shadow-cljs runtimes status update with connection status](https://github.com/BetterThanTomorrow/calva/issues/2928)
10+
711
## [2.0.529] - 2025-09-21
812

913
- Fix: [No `node-repl` and `browser-repl` builds available with **deps.edn + shadow-cljs** project types](https://github.com/BetterThanTomorrow/calva/issues/2929)

docs/site/shadow-cljs.md

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ search:
55
boost: 7
66
---
77

8-
Calva supports most any JVM hosted ClojureScript environment (and some others, including SCI based, too), but shadow-cljs gets some special treatment to try make it extra convenient to use.
9-
10-
With many shadow-cljs projects, Calva's _connect project type_ **shadow-cljs**, is the right choice. Projects that use Leiningen or deps.edn can be used both with the **Leiningen/deps.edn** _and_ **shadow-cljs** type, depending on configuration see [below](#shadow-cljs-in-full-stack-projects) for more on this.
8+
Calva supports most any JVM hosted ClojureScript environment (and some others, including SCI based, too), but [shadow-cljs](https://github.com/thheller/shadow-cljs) gets some special treatment to try to make it extra convenient to use.
119

10+
With many shadow-cljs projects, Calva's _connect project type_ **shadow-cljs**, is the right choice. Projects that use Leiningen or deps.edn can be used both with the **Leiningen/deps.edn** _and_ **shadow-cljs** type, depending on configuration, see [below](#shadow-cljs-in-full-stack-projects) for more on this.
1211

1312
# shadow-cljs - browser quickstart
1413

@@ -29,30 +28,69 @@ Here's how you start a shadow-cljs ClojureScript REPL and connect Calva with the
2928
1. Open http://localhost:8020/ in the browser
3029
1. Open browser.cljs file and load it in the REPL: **Calva: Load/Evaluate Current File and Dependencies**
3130

32-
Now you can should be able to evaluate forms, e.g.:
31+
Now you should be able to evaluate forms, e.g.:
3332

3433
* The current form or selection with <kbd>ctrl</kbd>+<kbd>enter</kbd>, or
3534
* Top-level forms with <kbd>alt/option</kbd>+<kbd>enter</kbd>.
3635

3736
(See [Code Evaluation](evaluation.md))
3837

39-
# Runtime Selection
38+
# shadow-cljs Runtimes
39+
40+
A shadow-cljs Runtime is an instance of your app that connects to shadow-cljs. When Calva connects to a runtime, that's where your ClojureScript REPL evaluations run.
41+
42+
```
43+
Calva <-talks to-> shadow-cljs server <-talks-to-> App (Runtime)
44+
```
4045

41-
When working with shadow-cljs projects you often have multiple runtimes running in different browser tabs, browsers, and phones, simultaneously. Calva will automatically connect to one of these (whichever runtime shadow-cljs connects when selecting a build). And you can then also switch between different JavaScript runtimes without reconnecting the REPL.
46+
We call the **App (Runtime)** in this diagram *the currently connected runtime*, and sometimes *the active runtime*.
4247

43-
## Available Runtimes
48+
When working with shadow-cljs projects you often have multiple runtimes running simultaneously across different browsers, tabs, and devices. Calva automatically connects to one runtime initially and lets you switch between them. It also detects when runtimes connect or disconnect.
4449

45-
Use the command **Calva: Select Shadow CLJS Runtime** to see all currently connected runtimes, presented in a VS Code quick-pick menu. The status bar will also display the active runtime when connected to a shadow-cljs project, and clicking this indicator will open the runtimes menu.
50+
## Available runtimes
4651

47-
## Switching Runtimes
52+
Use the command **Calva: Select Shadow CLJS Runtime** to see all runtimes that are currently connected to shadow-cljs. The runtimes will be presented in a VS Code quick-pick menu. The status bar will also display the active runtime, and clicking this indicator will open the runtimes menu.
53+
54+
## Switching runtimes
4855

4956
The **Calva: Select Shadow CLJS Runtime** menu will also let you select which runtime the `cljs` repl is connected to.
5057

5158
![shadow-cljs-runtimes-menu](/images/shadow-cljs/runtimes-menu.png)
5259

60+
## Automatic tracking of current runtime
61+
62+
Calva reacts to notifications from shadow-cljs about runtimes that connect to the server:
63+
64+
If no runtime is connected to Calva and a new runtime connects to shadow-cljs:
65+
66+
* *Calva will connect to this new runtime*
67+
68+
If Calva is connected to a runtime that disconnects from shadow-cljs:
69+
70+
* *Calva will have no connected runtime*. Even if there are more runtimes connected to shadow-cljs, you'll need to initiate a connection and select a new runtime manually.
71+
72+
!!! Note "Reload scenarios supported"
73+
74+
Note that this means that if you have the app running in browser tabs **A**, **B**, and **C**, with Calva connected to **A**, and you reload browser tab **A**:
75+
76+
* *Calva will “reconnect” to **A***
77+
78+
But if you just close **A**:
79+
80+
* *Calva will not automatically connect to **B**, nor **C***
81+
82+
And if you, while disconnected, start the app on the iPhone **D**:
83+
84+
* *Calva will connect **D***
85+
86+
!!! Note "Code hot reload"
87+
88+
Also note that the connection we're talking about here is Calva's connection to the app's REPL. shadow-cljs hot reloading of code when a file is saved always happens in all runtimes for all apps that are built from the code of that file. Calva is not involved in this mechanism.
89+
90+
5391
# shadow-cljs in full stack projects
5492

55-
**shadow-cljs** is a bit special in regards to Calva REPL connection. Mainly because you can start **shadow-cljs** and it's nREPL server in two ways:
93+
**shadow-cljs** is a bit special in regards to Calva REPL connection. Mainly because you can start **shadow-cljs** and its nREPL server in two ways:
5694

5795
1. Using the **shadow-cljs** npm executable
5896
2. Via the Clojure REPL in your Leiningen or **deps.edn** project
@@ -62,7 +100,7 @@ These options show up as **project types** when connecting or jacking in:
62100
1. Project type: **shadow-cljs**
63101
2. Project type: **deps.edn + shadow-cljs** or **Leiningen + shadow-cljs**
64102

65-
The technical difference here is wether you let **shadow-cljs** start **clojure**/**Leiningen** (the first option) or if you let Calva do it (the second option). If you let Calva do it, Calva will then start the **shadow-cljs** watcher from the Clojure process. From a usage perspective the two approaches will result in different channeling of **shadow-cljs** output, e.g. test runner results. With the first option (the **shadow-cljs** project type), **shadow-cljs** output will be channeled to the **Jack-in** terminal. With the **deps.edn**/**Leiningen** option, that output will be channeled to the Output/REPL window.
103+
The technical difference here is whether you let **shadow-cljs** start **clojure**/**Leiningen** (the first option) or if you let Calva do it (the second option). If you let Calva do it, Calva will then start the **shadow-cljs** watcher from the Clojure process. From a usage perspective the two approaches will result in different channeling of **shadow-cljs** output, e.g. test runner results. With the first option (the **shadow-cljs** project type), **shadow-cljs** output will be channeled to the **Jack-in** terminal. With the **deps.edn**/**Leiningen** option, that output will be channeled to the Output/REPL window.
66104

67105
See [shadow-cljs + Clojure with Calva: The basics](https://blog.agical.se/en/posts/shadow-cljs-clojure-cljurescript-calva-nrepl-basics/) for some more discussion on how the REPL connection works.
68106

package-lock.json

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "Calva: Clojure & ClojureScript Interactive Programming",
44
"description": "Integrated REPL, formatter, Paredit, and more. Powered by cider-nrepl and clojure-lsp.",
55
"icon": "assets/calva.png",
6-
"version": "2.0.529",
6+
"version": "2.0.530",
77
"publisher": "betterthantomorrow",
88
"author": {
99
"name": "Better Than Tomorrow",

src/connector.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as open from 'open';
77
import status from './status';
88
import * as projectTypes from './nrepl/project-types';
99
import { NReplClient, NReplSession } from './nrepl';
10-
import { detectInitialRuntime } from './shadow-cljs-runtime';
10+
import * as shadowCljsRuntime from './shadow-cljs-runtime';
1111
import {
1212
CljsTypeConfig,
1313
ReplConnectSequence,
@@ -154,6 +154,12 @@ async function connectToHost(hostname: string, port: number, connectSequence: Re
154154
const cljsType: CljsTypeConfig = isBuiltinType
155155
? getDefaultCljsType(connectSequence.cljsType as string)
156156
: (connectSequence.cljsType as CljsTypeConfig);
157+
158+
// Initialize shadow-cljs remote notifications if we're connecting to shadow-cljs
159+
if (isShadowCljsReplType(cljsType)) {
160+
await shadowCljsRuntime.initializeShadowRemoteNotifications();
161+
}
162+
157163
translatedReplType = createCLJSReplType(
158164
cljsType,
159165
projectTypes.getCljsTypeName(connectSequence),
@@ -513,7 +519,7 @@ function createCLJSReplType(
513519
}
514520
const runtimesConnected = await waitForShadowCljsRuntimes();
515521
if (runtimesConnected) {
516-
await detectInitialRuntime();
522+
await shadowCljsRuntime.detectInitialRuntime();
517523
}
518524
return runtimesConnected;
519525
} else {

src/nrepl/index.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { getConfig } from '../config';
2020
import { log, Direction } from './logging';
2121
import * as string from '../util/string';
2222
import * as output from '../results-output/output';
23+
import { handleShadowRemoteMessage } from '../shadow-cljs-runtime';
2324

2425
function hasStatus(res: any, status: string): boolean {
2526
return res.status && res.status.indexOf(status) > -1;
@@ -293,6 +294,11 @@ export class NReplSession {
293294
}
294295

295296
_defaultMessageHandler(msgData: any) {
297+
if (msgData.op === 'shadow-remote-msg') {
298+
void handleShadowRemoteMessage(msgData);
299+
return;
300+
}
301+
296302
if (msgData['repl-type']) {
297303
this.replType = msgData['repl-type'];
298304
}
@@ -972,6 +978,38 @@ export class NReplSession {
972978
}
973979
});
974980
}
981+
982+
shadowCljsRemoteInit() {
983+
return new Promise<any>((resolve, reject) => {
984+
const id = this.client.nextId;
985+
const msg = {
986+
op: 'shadow-remote-init',
987+
id: id,
988+
session: this.sessionId,
989+
'data-type': 'edn',
990+
};
991+
// shadow-cljs remote messages do not respond with an acknowledging response
992+
// so we can't bind a messagehandler the usual way. Fire-and-forget!
993+
this.client.write(msg);
994+
resolve(null);
995+
});
996+
}
997+
998+
shadowCljsRemoteRegisterNotify() {
999+
return new Promise<any>((resolve, reject) => {
1000+
const id = this.client.nextId;
1001+
const msg = {
1002+
op: 'shadow-remote-msg',
1003+
id: id,
1004+
session: this.sessionId,
1005+
data: '{:op :request-clients :notify true :query [:eq :type :runtime]}',
1006+
};
1007+
// shadow-cljs remote messages do not respond with an acknowledging response
1008+
// so we can't bind a messagehandler the usual way. Fire-and-forget!
1009+
this.client.write(msg);
1010+
resolve(null);
1011+
});
1012+
}
9751013
}
9761014

9771015
/**

0 commit comments

Comments
 (0)