feat: show image sources automatically in VS Code
Browse the image's writable virtual source filesystem in an Explorer tree without changing workspace folders. Wire source context actions and ensure Spawn with State prompts for JSON when VS Code passes menu selection arguments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
89794b08dd
commit
7332aef585
|
|
@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- Show the image's editable virtual `source` filesystem automatically in the
|
||||||
|
VS Code Explorer without adding a workspace folder.
|
||||||
- Require an explicit `runa-cli` subcommand: `repl` starts the interactive
|
- Require an explicit `runa-cli` subcommand: `repl` starts the interactive
|
||||||
session, and each image operation is also available as a one-shot command.
|
session, and each image operation is also available as a one-shot command.
|
||||||
- Rename the REPL actor listing command from `actors` to `ps`, remove its
|
- Rename the REPL actor listing command from `actors` to `ps`, remove its
|
||||||
|
|
@ -13,6 +15,8 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Always prompt for JSON state when Spawn with State is invoked from a source
|
||||||
|
context menu, including multi-selection menu arguments.
|
||||||
- Close extension connections with a WebSocket handshake on disconnect,
|
- Close extension connections with a WebSocket handshake on disconnect,
|
||||||
reconnect, and deactivation instead of terminating the transport immediately.
|
reconnect, and deactivation instead of terminating the transport immediately.
|
||||||
- Exit the REPL on Ctrl+C and complete the WebSocket close handshake instead
|
- Exit the REPL on Ctrl+C and complete the WebSocket close handshake instead
|
||||||
|
|
|
||||||
|
|
@ -173,10 +173,11 @@ actors created through the REPL or another client. Click an actor to inspect
|
||||||
it. Server notifications automatically refresh the tree and open snapshots;
|
it. Server notifications automatically refresh the tree and open snapshots;
|
||||||
**Runa: Refresh Image** is also available on demand.
|
**Runa: Refresh Image** is also available on demand.
|
||||||
|
|
||||||
The extension mounts a **source** workspace directory backed by the image,
|
The **Runa Image Sources** Explorer view automatically shows the image's virtual
|
||||||
including `actors/counter.rn`. Save edits to update the working file without
|
filesystem beneath **source**, including `actors/counter.rn`, without adding a
|
||||||
|
workspace folder. Open a file and save edits to update the image without
|
||||||
committing. Right-click a source file to **Spawn** (null state) or **Spawn with
|
committing. Right-click a source file to **Spawn** (null state) or **Spawn with
|
||||||
State**, or to commit, browse history, and roll back. Historical revisions
|
State** (which prompts for JSON), or to commit, browse history, and roll back. Historical revisions
|
||||||
remain read-only.
|
remain read-only.
|
||||||
|
|
||||||
Use **Runa: Spawn Actor** to choose a source path and supply JSON initial state.
|
Use **Runa: Spawn Actor** to choose a source path and supply JSON initial state.
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,8 @@ The stdio transport and child-process client have been removed.
|
||||||
|
|
||||||
The TypeScript VS Code client issues `list_actors`, `inspect_actor`,
|
The TypeScript VS Code client issues `list_actors`, `inspect_actor`,
|
||||||
source file/version-control operations, `spawn`, `send`, `tick`, and `system`.
|
source file/version-control operations, `spawn`, `send`, `tick`, and `system`.
|
||||||
VS Code mounts writable image working files under a `source` workspace folder,
|
VS Code automatically displays writable image files under a `source` directory
|
||||||
|
in its image-source Explorer tree, without modifying workspace folders,
|
||||||
and caches read-only commit documents by connection, path, and revision.
|
and caches read-only commit documents by connection, path, and revision.
|
||||||
It preserves i64 inputs and bounds the complete outgoing envelope as well as
|
It preserves i64 inputs and bounds the complete outgoing envelope as well as
|
||||||
incoming messages. It enforces loopback endpoints, bounded UTF-8 text messages,
|
incoming messages. It enforces loopback endpoints, bounded UTF-8 text messages,
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,10 @@ In that host:
|
||||||
|
|
||||||
1. Run **Runa: Connect to Local Image**. The default is `127.0.0.1:3737`;
|
1. Run **Runa: Connect to Local Image**. The default is `127.0.0.1:3737`;
|
||||||
`runa.address` changes the suggested endpoint.
|
`runa.address` changes the suggested endpoint.
|
||||||
2. Open the image's `source` folder (see [Image source files](#image-source-files)).
|
2. The **Runa Image Sources** Explorer view automatically shows the image's
|
||||||
Right-click a `.rn` file and choose **Spawn Actor** (initial state `null`) or
|
`source` directory (see [Image source files](#image-source-files)).
|
||||||
**Spawn Actor with State** (enter JSON, for example `0` for
|
Right-click a `.rn` file and choose **Spawn** (initial state `null`) or
|
||||||
|
**Spawn with State...** (enter JSON in the input field, for example `0` for
|
||||||
`actors/counter`). **Runa: Spawn Actor** in **Runa Actors** prompts for a source
|
`actors/counter`). **Runa: Spawn Actor** in **Runa Actors** prompts for a source
|
||||||
path and state. The command reports and returns the new actor reference.
|
path and state. The command reports and returns the new actor reference.
|
||||||
Mailbox capacity is 64.
|
Mailbox capacity is 64.
|
||||||
|
|
@ -97,11 +98,12 @@ in the Runa output channel and an error notification; commands are never retried
|
||||||
|
|
||||||
## Image source files
|
## Image source files
|
||||||
|
|
||||||
The image owns actor source text. On connect, the extension mounts it as a
|
The image owns actor source text. On connect, **Runa Image Sources** automatically
|
||||||
writable workspace folder named `source` (`runa-image://<endpoint>/source`).
|
shows its virtual filesystem beneath `source` (`runa-image://<endpoint>/source`).
|
||||||
In a saved multi-root workspace this happens automatically; otherwise accept the
|
No workspace changes, folder prompt, or extension restart is required.
|
||||||
prompt or run **Runa: Add Image Source Folder** (VS Code may restart extensions
|
Opening a file reads it from the image; saving writes back to the image.
|
||||||
when a single-folder window becomes multi-root). Directories are derived from
|
The optional **Runa: Add Source Folder to Workspace** command is still available
|
||||||
|
if a regular workspace mount is desired. Directories are derived from
|
||||||
source paths: `actors/counter` appears as `source/actors/counter.rn`. Source
|
source paths: `actors/counter` appears as `source/actors/counter.rn`. Source
|
||||||
paths are canonical and exclude the `source/` prefix and `.rn` suffix.
|
paths are canonical and exclude the `source/` prefix and `.rn` suffix.
|
||||||
|
|
||||||
|
|
@ -113,6 +115,8 @@ paths are canonical and exclude the `source/` prefix and `.rn` suffix.
|
||||||
- **Spawn** uses the latest saved working source, committed or not. If the
|
- **Spawn** uses the latest saved working source, committed or not. If the
|
||||||
editor has unsaved changes, spawn and commit ask to save first; cancelling
|
editor has unsaved changes, spawn and commit ask to save first; cancelling
|
||||||
does nothing. Unsaved editor text is never sent implicitly.
|
does nothing. Unsaved editor text is never sent implicitly.
|
||||||
|
- **Spawn with State...** opens a JSON input field; cancelling the field does
|
||||||
|
not spawn an actor.
|
||||||
- **Runa: Commit Source** records the saved working source as a new revision
|
- **Runa: Commit Source** records the saved working source as a new revision
|
||||||
and reports its revision string.
|
and reports its revision string.
|
||||||
- **Runa: Source History** lists revisions for the file and opens the selected
|
- **Runa: Source History** lists revisions for the file and opens the selected
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
import { control, spawnCounter, startImage } from "./server";
|
import { control, spawnCounter, startImage } from "./server";
|
||||||
import { RevisionDocuments } from "../src/sourceProvider";
|
import { ImageSourceTree, RevisionDocuments } from "../src/sourceProvider";
|
||||||
|
|
||||||
export async function run(): Promise<void> {
|
export async function run(): Promise<void> {
|
||||||
const image = await startImage();
|
const image = await startImage();
|
||||||
|
const folders = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.toString());
|
||||||
try {
|
try {
|
||||||
await vscode.commands.executeCommand("runa.connect", image.address);
|
await vscode.commands.executeCommand("runa.connect", image.address);
|
||||||
|
assert.deepEqual(
|
||||||
|
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.toString()),
|
||||||
|
folders,
|
||||||
|
);
|
||||||
await editSources(image.address);
|
await editSources(image.address);
|
||||||
const spawned = await vscode.commands.executeCommand<unknown>("runa.spawn", {
|
const spawned = await vscode.commands.executeCommand<unknown>("runa.spawn", {
|
||||||
path: "actors/counter",
|
path: "actors/counter",
|
||||||
|
|
@ -134,16 +139,24 @@ export async function run(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function editSources(address: string): Promise<void> {
|
async function editSources(address: string): Promise<void> {
|
||||||
const folder = vscode.workspace.workspaceFolders?.find(
|
const extension = vscode.extensions.all.find(
|
||||||
(candidate) => candidate.uri.scheme === "runa-image",
|
(candidate) => candidate.packageJSON.name === "runa",
|
||||||
);
|
);
|
||||||
assert.ok(folder, "connect mounts the image source folder");
|
assert.ok(extension);
|
||||||
assert.equal(folder.name, "source");
|
const { sourceTree } = extension.exports as { sourceTree: ImageSourceTree };
|
||||||
assert.equal(folder.uri.toString(), `runa-image://${address}/source`);
|
const [root] = sourceTree.getChildren();
|
||||||
assert.deepEqual(await vscode.workspace.fs.readDirectory(folder.uri), [
|
assert.ok(root);
|
||||||
|
assert.equal(root.uri.toString(), `runa-image://${address}/source`);
|
||||||
|
assert.deepEqual(await vscode.workspace.fs.readDirectory(root.uri), [
|
||||||
["actors", vscode.FileType.Directory],
|
["actors", vscode.FileType.Directory],
|
||||||
]);
|
]);
|
||||||
const actors = vscode.Uri.joinPath(folder.uri, "actors");
|
const [directory] = sourceTree.getChildren(root);
|
||||||
|
assert.ok(directory);
|
||||||
|
assert.equal(directory.directory, true);
|
||||||
|
const [file] = sourceTree.getChildren(directory);
|
||||||
|
assert.ok(file);
|
||||||
|
assert.equal(sourceTree.getTreeItem(file).contextValue, "runa.sourceFile");
|
||||||
|
const actors = vscode.Uri.joinPath(root.uri, "actors");
|
||||||
assert.deepEqual(await vscode.workspace.fs.readDirectory(actors), [
|
assert.deepEqual(await vscode.workspace.fs.readDirectory(actors), [
|
||||||
["counter.rn", vscode.FileType.File],
|
["counter.rn", vscode.FileType.File],
|
||||||
]);
|
]);
|
||||||
|
|
@ -204,11 +217,28 @@ async function editSources(address: string): Promise<void> {
|
||||||
await control(address, { operation: "inspect_actor", parameters: { actor_ref: nullActor } }),
|
await control(address, { operation: "inspect_actor", parameters: { actor_ref: nullActor } }),
|
||||||
);
|
);
|
||||||
assert.match(reply, /"state":\{"type":"null"\}/);
|
assert.match(reply, /"state":\{"type":"null"\}/);
|
||||||
const actor = await vscode.commands.executeCommand<string>(
|
const inputWindow = vscode.window as { showInputBox: (...args: unknown[]) => unknown };
|
||||||
"runa.spawnSourceWithState",
|
const originalInput = inputWindow.showInputBox;
|
||||||
uri,
|
const inputs: unknown[][] = [];
|
||||||
"40",
|
let stateAnswer: string | undefined;
|
||||||
|
inputWindow.showInputBox = async (...args: unknown[]) => {
|
||||||
|
inputs.push(args);
|
||||||
|
return stateAnswer;
|
||||||
|
};
|
||||||
|
let actor: string | undefined;
|
||||||
|
try {
|
||||||
|
assert.equal(
|
||||||
|
await vscode.commands.executeCommand("runa.spawnSourceWithState", file, [file]),
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
|
stateAnswer = "40";
|
||||||
|
actor = await vscode.commands.executeCommand<string>("runa.spawnSourceWithState", file, [file]);
|
||||||
|
assert.equal(inputs.length, 2, "context menus always prompt for state");
|
||||||
|
assert.match(JSON.stringify(inputs[1]), /Initial state for actors\/counter/);
|
||||||
|
} finally {
|
||||||
|
inputWindow.showInputBox = originalInput;
|
||||||
|
}
|
||||||
|
assert.match(String(actor), /^[A-Za-z0-9_-]{21}$/);
|
||||||
await vscode.commands.executeCommand("runa.send", { actor_ref: actor }, "null");
|
await vscode.commands.executeCommand("runa.send", { actor_ref: actor }, "null");
|
||||||
await vscode.commands.executeCommand("runa.tick", 10);
|
await vscode.commands.executeCommand("runa.tick", 10);
|
||||||
reply = JSON.stringify(
|
reply = JSON.stringify(
|
||||||
|
|
@ -278,7 +308,7 @@ async function editSources(address: string): Promise<void> {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
vscode.workspace.workspaceFolders?.filter((candidate) => candidate.uri.scheme === "runa-image")
|
vscode.workspace.workspaceFolders?.filter((candidate) => candidate.uri.scheme === "runa-image")
|
||||||
.length,
|
.length,
|
||||||
1,
|
0,
|
||||||
);
|
);
|
||||||
const reopened = await vscode.commands.executeCommand<string>(
|
const reopened = await vscode.commands.executeCommand<string>(
|
||||||
"runa.sourceHistory",
|
"runa.sourceHistory",
|
||||||
|
|
@ -303,7 +333,7 @@ async function editSources(address: string): Promise<void> {
|
||||||
revisions.dispose();
|
revisions.dispose();
|
||||||
}
|
}
|
||||||
console.log(
|
console.log(
|
||||||
"Runa sources: mounted folder, save, uncommitted spawn, commit, history, rollback, session isolation passed",
|
"Runa sources: automatic tree, state input, save, uncommitted spawn, commit, history, rollback, session isolation passed",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"main": "./out/src/extension.js",
|
"main": "./out/src/extension.js",
|
||||||
"activationEvents": [
|
"activationEvents": [
|
||||||
"onView:runa.actors",
|
"onView:runa.actors",
|
||||||
|
"onView:runa.sources",
|
||||||
"onFileSystem:runa-image"
|
"onFileSystem:runa-image"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -163,10 +164,19 @@
|
||||||
{
|
{
|
||||||
"id": "runa.actors",
|
"id": "runa.actors",
|
||||||
"name": "Runa Actors"
|
"name": "Runa Actors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "runa.sources",
|
||||||
|
"name": "Runa Image Sources"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"viewsWelcome": [
|
"viewsWelcome": [
|
||||||
|
{
|
||||||
|
"view": "runa.sources",
|
||||||
|
"contents": "Connect to browse and edit the image's virtual filesystem.\n[Connect](command:runa.connect)",
|
||||||
|
"when": "!runa.connected"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"view": "runa.actors",
|
"view": "runa.actors",
|
||||||
"contents": "Connect to a trusted local Runa image.\n[Connect](command:runa.connect)",
|
"contents": "Connect to a trusted local Runa image.\n[Connect](command:runa.connect)",
|
||||||
|
|
@ -175,6 +185,16 @@
|
||||||
],
|
],
|
||||||
"menus": {
|
"menus": {
|
||||||
"view/title": [
|
"view/title": [
|
||||||
|
{
|
||||||
|
"command": "runa.connect",
|
||||||
|
"when": "view == runa.sources && !runa.connected",
|
||||||
|
"group": "navigation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "runa.refresh",
|
||||||
|
"when": "view == runa.sources && runa.connected",
|
||||||
|
"group": "navigation"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "runa.spawn",
|
"command": "runa.spawn",
|
||||||
"when": "view == runa.actors && runa.connected",
|
"when": "view == runa.actors && runa.connected",
|
||||||
|
|
@ -200,6 +220,31 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"view/item/context": [
|
"view/item/context": [
|
||||||
|
{
|
||||||
|
"command": "runa.spawnSource",
|
||||||
|
"when": "view == runa.sources && viewItem == runa.sourceFile && runa.connected",
|
||||||
|
"group": "runa_1_spawn@1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "runa.spawnSourceWithState",
|
||||||
|
"when": "view == runa.sources && viewItem == runa.sourceFile && runa.connected",
|
||||||
|
"group": "runa_1_spawn@2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "runa.commitSource",
|
||||||
|
"when": "view == runa.sources && viewItem == runa.sourceFile && runa.connected",
|
||||||
|
"group": "runa_2_source@1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "runa.sourceHistory",
|
||||||
|
"when": "view == runa.sources && viewItem == runa.sourceFile && runa.connected",
|
||||||
|
"group": "runa_2_source@2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "runa.rollbackSource",
|
||||||
|
"when": "view == runa.sources && viewItem == runa.sourceFile && runa.connected",
|
||||||
|
"group": "runa_2_source@3"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "runa.send",
|
"command": "runa.send",
|
||||||
"when": "view == runa.actors && viewItem == runa.actor",
|
"when": "view == runa.actors && viewItem == runa.actor",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { RefreshQueue } from "./refresh";
|
||||||
import { parseValue } from "./values";
|
import { parseValue } from "./values";
|
||||||
import {
|
import {
|
||||||
ImageSourceFileSystem,
|
ImageSourceFileSystem,
|
||||||
|
ImageSourceTree,
|
||||||
RevisionDocuments,
|
RevisionDocuments,
|
||||||
REVISION_SCHEME,
|
REVISION_SCHEME,
|
||||||
SOURCE_SCHEME,
|
SOURCE_SCHEME,
|
||||||
|
|
@ -130,12 +131,14 @@ class SnapshotDocuments implements vscode.TextDocumentContentProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SourceTarget = vscode.Uri | { path: string } | string | undefined;
|
type SourceTarget = vscode.Uri | { path: string } | { uri: vscode.Uri } | string | undefined;
|
||||||
|
|
||||||
export function activate(context: vscode.ExtensionContext): void {
|
export function activate(context: vscode.ExtensionContext): { sourceTree: ImageSourceTree } {
|
||||||
const tree = new ActorTree();
|
const tree = new ActorTree();
|
||||||
const documents = new SnapshotDocuments();
|
const documents = new SnapshotDocuments();
|
||||||
const sources = new ImageSourceFileSystem();
|
const sources = new ImageSourceFileSystem();
|
||||||
|
const sourceTree = new ImageSourceTree(sources);
|
||||||
|
const sourceView = vscode.window.createTreeView("runa.sources", { treeDataProvider: sourceTree });
|
||||||
const revisions = new RevisionDocuments();
|
const revisions = new RevisionDocuments();
|
||||||
const view = vscode.window.createTreeView("runa.actors", { treeDataProvider: tree });
|
const view = vscode.window.createTreeView("runa.actors", { treeDataProvider: tree });
|
||||||
const output = vscode.window.createOutputChannel("Runa");
|
const output = vscode.window.createOutputChannel("Runa");
|
||||||
|
|
@ -176,6 +179,8 @@ export function activate(context: vscode.ExtensionContext): void {
|
||||||
status.command = connected ? "runa.refresh" : "runa.connect";
|
status.command = connected ? "runa.refresh" : "runa.connect";
|
||||||
view.description = connected ? endpoint : "Disconnected";
|
view.description = connected ? endpoint : "Disconnected";
|
||||||
view.message = connected ? undefined : "Not connected to an image.";
|
view.message = connected ? undefined : "Not connected to an image.";
|
||||||
|
sourceView.description = connected ? endpoint : "Disconnected";
|
||||||
|
sourceTree.refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
const disconnect = () => {
|
const disconnect = () => {
|
||||||
|
|
@ -201,6 +206,7 @@ export function activate(context: vscode.ExtensionContext): void {
|
||||||
await sources.store.refresh();
|
await sources.store.refresh();
|
||||||
if (current !== client || currentSession !== session) return;
|
if (current !== client || currentSession !== session) return;
|
||||||
tree.replace(actors);
|
tree.replace(actors);
|
||||||
|
sourceTree.refresh();
|
||||||
documents.refresh(session, endpoint, actors);
|
documents.refresh(session, endpoint, actors);
|
||||||
view.message = actors.length
|
view.message = actors.length
|
||||||
? undefined
|
? undefined
|
||||||
|
|
@ -232,20 +238,6 @@ export function activate(context: vscode.ExtensionContext): void {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const offerSourceFolder = async (address: string) => {
|
|
||||||
const uri = sourceFolderUri(address);
|
|
||||||
if (vscode.workspace.workspaceFolders?.some((f) => f.uri.toString() === uri.toString())) return;
|
|
||||||
if (vscode.workspace.workspaceFile !== undefined) {
|
|
||||||
await addSourceFolder(address);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const choice = await vscode.window.showInformationMessage(
|
|
||||||
"Runa: Add the image's source folder to the workspace? VS Code restarts extensions when a window becomes a multi-root workspace; reconnect afterwards.",
|
|
||||||
"Add Source Folder",
|
|
||||||
);
|
|
||||||
if (choice && client && endpoint === address) await addSourceFolder(address);
|
|
||||||
};
|
|
||||||
|
|
||||||
const connect = async (target?: string) => {
|
const connect = async (target?: string) => {
|
||||||
const address =
|
const address =
|
||||||
target ??
|
target ??
|
||||||
|
|
@ -285,6 +277,7 @@ export function activate(context: vscode.ExtensionContext): void {
|
||||||
tree.replace([]);
|
tree.replace([]);
|
||||||
documents.disconnect(currentSession);
|
documents.disconnect(currentSession);
|
||||||
sources.store.detach();
|
sources.store.detach();
|
||||||
|
sourceTree.refresh();
|
||||||
connectionStatus(false);
|
connectionStatus(false);
|
||||||
output.appendLine(`Disconnected from ${address}`);
|
output.appendLine(`Disconnected from ${address}`);
|
||||||
if (error) report(error);
|
if (error) report(error);
|
||||||
|
|
@ -314,7 +307,6 @@ export function activate(context: vscode.ExtensionContext): void {
|
||||||
? undefined
|
? undefined
|
||||||
: "No actors. Spawn a .rn file from the source folder.";
|
: "No actors. Spawn a .rn file from the source folder.";
|
||||||
output.appendLine(`Connected to ${address} (protocol v4)`);
|
output.appendLine(`Connected to ${address} (protocol v4)`);
|
||||||
await offerSourceFolder(address);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const inspect = async (selected?: ActorSnapshot) => {
|
const inspect = async (selected?: ActorSnapshot) => {
|
||||||
|
|
@ -410,6 +402,7 @@ export function activate(context: vscode.ExtensionContext): void {
|
||||||
if (uri?.scheme === SOURCE_SCHEME) selected = uri;
|
if (uri?.scheme === SOURCE_SCHEME) selected = uri;
|
||||||
}
|
}
|
||||||
if (selected === undefined) return pickSource(placeHolder);
|
if (selected === undefined) return pickSource(placeHolder);
|
||||||
|
if (typeof selected === "object" && "uri" in selected) selected = selected.uri;
|
||||||
if (selected instanceof vscode.Uri) {
|
if (selected instanceof vscode.Uri) {
|
||||||
if (selected.authority !== connectionEndpoint) {
|
if (selected.authority !== connectionEndpoint) {
|
||||||
throw new Error(`${selected.toString(true)} belongs to another image; connect to it first`);
|
throw new Error(`${selected.toString(true)} belongs to another image; connect to it first`);
|
||||||
|
|
@ -486,7 +479,8 @@ export function activate(context: vscode.ExtensionContext): void {
|
||||||
const connection = active();
|
const connection = active();
|
||||||
const path = await sourceTarget(target, connection.endpoint, "Select the source to spawn");
|
const path = await sourceTarget(target, connection.endpoint, "Select the source to spawn");
|
||||||
if (path === undefined) return;
|
if (path === undefined) return;
|
||||||
const state = stateText ?? (await jsonInput(`Initial state for ${path}`));
|
const state =
|
||||||
|
typeof stateText === "string" ? stateText : await jsonInput(`Initial state for ${path}`);
|
||||||
if (state === undefined) return;
|
if (state === undefined) return;
|
||||||
return spawnPath(path, state, connection);
|
return spawnPath(path, state, connection);
|
||||||
};
|
};
|
||||||
|
|
@ -647,6 +641,8 @@ export function activate(context: vscode.ExtensionContext): void {
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
tree,
|
tree,
|
||||||
sources,
|
sources,
|
||||||
|
sourceTree,
|
||||||
|
sourceView,
|
||||||
revisions,
|
revisions,
|
||||||
documents,
|
documents,
|
||||||
view,
|
view,
|
||||||
|
|
@ -689,4 +685,5 @@ export function activate(context: vscode.ExtensionContext): void {
|
||||||
);
|
);
|
||||||
connectionStatus(false);
|
connectionStatus(false);
|
||||||
status.show();
|
status.show();
|
||||||
|
return { sourceTree };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,51 @@ export function sourcePathOf(uri: vscode.Uri): string {
|
||||||
return entry.path;
|
return entry.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SourceEntry {
|
||||||
|
uri: vscode.Uri;
|
||||||
|
directory: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ImageSourceTree implements vscode.TreeDataProvider<SourceEntry>, vscode.Disposable {
|
||||||
|
private readonly changes = new vscode.EventEmitter<SourceEntry | undefined>();
|
||||||
|
readonly onDidChangeTreeData = this.changes.event;
|
||||||
|
|
||||||
|
constructor(private readonly files: ImageSourceFileSystem) {}
|
||||||
|
|
||||||
|
refresh(): void {
|
||||||
|
this.changes.fire(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildren(parent?: SourceEntry): SourceEntry[] {
|
||||||
|
const endpoint = this.files.store.image;
|
||||||
|
if (!this.files.store.connected || endpoint === undefined) return [];
|
||||||
|
if (!parent) return [{ uri: sourceFolderUri(endpoint), directory: true }];
|
||||||
|
return this.files.readDirectory(parent.uri).map(([name, type]) => ({
|
||||||
|
uri: vscode.Uri.joinPath(parent.uri, name),
|
||||||
|
directory: type === vscode.FileType.Directory,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
getTreeItem(entry: SourceEntry): vscode.TreeItem {
|
||||||
|
const item = new vscode.TreeItem(
|
||||||
|
entry.uri,
|
||||||
|
entry.directory
|
||||||
|
? vscode.TreeItemCollapsibleState.Expanded
|
||||||
|
: vscode.TreeItemCollapsibleState.None,
|
||||||
|
);
|
||||||
|
item.id = entry.uri.toString();
|
||||||
|
item.contextValue = entry.directory ? "runa.sourceDirectory" : "runa.sourceFile";
|
||||||
|
if (!entry.directory) {
|
||||||
|
item.command = { command: "vscode.open", title: "Open Source", arguments: [entry.uri] };
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
this.changes.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Writable working files of the connected image under `runa-image://<endpoint>/source`. */
|
/** Writable working files of the connected image under `runa-image://<endpoint>/source`. */
|
||||||
export class ImageSourceFileSystem implements vscode.FileSystemProvider {
|
export class ImageSourceFileSystem implements vscode.FileSystemProvider {
|
||||||
private readonly changes = new vscode.EventEmitter<vscode.FileChangeEvent[]>();
|
private readonly changes = new vscode.EventEmitter<vscode.FileChangeEvent[]>();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue