From 403a16dc8ab43dcfe4b435802cd7883b21d6ab38 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Mon, 1 Sep 2025 11:28:38 +0200 Subject: [PATCH 1/3] Initial version of Process controlled launching of sketches --- client/src/extension.ts | 2 + client/src/setupCommands.ts | 140 +++++++++++++++++----------------- client/src/setupConsole.ts | 148 ++++++++++++++++++++++++++++++++++++ client/tsconfig.tsbuildinfo | 2 +- package-lock.json | 20 ++++- package.json | 20 +++++ 6 files changed, 257 insertions(+), 75 deletions(-) create mode 100644 client/src/setupConsole.ts diff --git a/client/src/extension.ts b/client/src/extension.ts index f1a3c86..85af2e1 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -9,6 +9,7 @@ import { setupSidebar } from './setupSidebar'; import { setupDecorators } from './setupDecorators'; import { setupPDEFiles } from './setupPDEFiles'; import { EventEmitter } from 'stream'; +import setupConsole from './setupConsole'; export interface ProcessingVersion { @@ -38,6 +39,7 @@ export async function activate(context: ExtensionContext) { setupCommands(context); setupLanguageServer(); setupSidebar(context); + setupConsole(context); setupDecorators(context); setupPDEFiles(); } diff --git a/client/src/setupCommands.ts b/client/src/setupCommands.ts index c73bd12..79f16da 100644 --- a/client/src/setupCommands.ts +++ b/client/src/setupCommands.ts @@ -1,77 +1,77 @@ import { basename, dirname, join } from 'path'; import { ExtensionContext, commands, Uri, window, workspace } from 'vscode'; -import { state } from './extension'; +// import { state } from './extension'; -const sketchNumber = 0; +// const sketchNumber = 0; export function setupCommands(context: ExtensionContext) { - const runSketch = commands.registerCommand('processing.sketch.run', (resource: Uri) => { - // TODO: Use VScode contexts to highlight run button when sketch is running, blocked until we do not run the sketch in a terminal - // https://code.visualstudio.com/api/references/when-clause-contexts - - const autosave = workspace - .getConfiguration('processing') - .get('autosave'); - if (autosave === true) { - // Save all files before running the sketch - commands.executeCommand('workbench.action.files.saveAll'); - } - if (resource == undefined) { - const editor = window.activeTextEditor; - if (editor) { - resource = editor.document.uri; - } - } - - if (!resource) { - return; - } - - // TODO: Give feedback if the sketch is starting - - let terminal = state.terminal; - // Create a new terminal - if (terminal === undefined || terminal.exitStatus) { - window.terminals.forEach(t => { - if (t.name === "Sketch") { - t.dispose(); - } - }); - state.terminal = window.createTerminal("Sketch"); - terminal = state.terminal; - // Show the terminal panel the first time - terminal.show(true); - } else { - // Send the command to the terminal - terminal.sendText('\x03', false); - } - - // clear the terminal - terminal.sendText("clear", true); - - let path = state.selectedVersion.path; - if (process.platform === "win32") { - // on windows we need to escape spaces - path = `& "${path}"`; - } - - let cmd = `${path} cli --sketch="${dirname(resource.fsPath)}" --run`; - if (process.platform === "win32") { - // on windows we need to pipe stderr to stdout and convert to string - cmd += ` 2>&1`; - } - - terminal.sendText(cmd, true); - }); - - const stopSketch = commands.registerCommand('processing.sketch.stop', () => { - if (state.terminal === undefined) { - return; - } - - // Send the command to the terminal - state.terminal.sendText('\x03', false); - }); + // const runSketch = commands.registerCommand('processing.sketch.run', (resource: Uri) => { + // // TODO: Use VScode contexts to highlight run button when sketch is running, blocked until we do not run the sketch in a terminal + // // https://code.visualstudio.com/api/references/when-clause-contexts + + // const autosave = workspace + // .getConfiguration('processing') + // .get('autosave'); + // if (autosave === true) { + // // Save all files before running the sketch + // commands.executeCommand('workbench.action.files.saveAll'); + // } + // if (resource == undefined) { + // const editor = window.activeTextEditor; + // if (editor) { + // resource = editor.document.uri; + // } + // } + + // if (!resource) { + // return; + // } + + // return; + + // let terminal = state.terminal; + // // Create a new terminal + // if (terminal === undefined || terminal.exitStatus) { + // window.terminals.forEach(t => { + // if (t.name === "Sketch") { + // t.dispose(); + // } + // }); + // state.terminal = window.createTerminal("Sketch"); + // terminal = state.terminal; + // // Show the terminal panel the first time + // terminal.show(true); + // } else { + // // Send the command to the terminal + // terminal.sendText('\x03', false); + // } + + // // clear the terminal + // terminal.sendText("clear", true); + + // let path = state.selectedVersion.path; + // if (process.platform === "win32") { + // // on windows we need to escape spaces + // path = `& "${path}"`; + // } + + // let cmd = `${path} cli --sketch="${dirname(resource.fsPath)}" --run`; + // if (process.platform === "win32") { + // // on windows we need to pipe stderr to stdout and convert to string + // cmd += ` 2>&1`; + // } + + // terminal.sendText(cmd, true); + // }); + + // const stopSketch = commands.registerCommand('processing.sketch.stop', () => { + // if (state.terminal === undefined) { + // return; + // } + + // // Send the command to the terminal + // state.terminal.sendText('\x03', false); + // }); const openSketch = commands.registerCommand('processing.sketch.open', async (folder: string, isReadOnly: boolean) => { if (!folder) { @@ -161,7 +161,7 @@ export function setupCommands(context: ExtensionContext) { // TODO: Add command to select Processing version and set the setting - context.subscriptions.push(runSketch, stopSketch, openSketch, newSketch); + context.subscriptions.push(openSketch, newSketch); } // Helper function to convert a number to alphabetical (e.g., 0 = a, 1 = b, ..., 25 = z, 26 = aa, etc.) diff --git a/client/src/setupConsole.ts b/client/src/setupConsole.ts new file mode 100644 index 0000000..0847894 --- /dev/null +++ b/client/src/setupConsole.ts @@ -0,0 +1,148 @@ +import { ChildProcess, spawn } from 'child_process'; +import { commands, ExtensionContext, Uri, ViewColumn, WebviewView, WebviewViewProvider, WebviewViewResolveContext, window, workspace } from 'vscode'; +import { state } from './extension'; +import { dirname } from 'path'; +import treeKill = require('tree-kill'); + +export default function setupConsole(context: ExtensionContext) { + // Convert to array to allow for waiting on the process to end + let sketchProcess: ChildProcess | undefined = undefined; + + const provider = new ProcessingConsoleViewProvider(); + + const register = window.registerWebviewViewProvider('processingConsoleView', provider); + + const startSketch = commands.registerCommand('processing.sketch.run', (resource: Uri) => { + const autosave = workspace + .getConfiguration('processing') + .get('autosave'); + if (autosave === true) { + // Save all files before running the sketch + commands.executeCommand('workbench.action.files.saveAll'); + } + if (resource == undefined) { + const editor = window.activeTextEditor; + if (editor) { + resource = editor.document.uri; + } + } + + if (!resource) { + return; + } + commands.executeCommand('processingConsoleView.focus'); + commands.executeCommand('processing.sketch.stop'); + + const proc = spawn( + state.selectedVersion.path, + ['cli', `--sketch=${dirname(resource.fsPath)}`, '--run'], + { + shell: false, + } + ); + proc.stdout.on("data", (data) => { + provider.webview?.webview.postMessage({ type: 'stdout', value: data?.toString() }); + }); + proc.stderr.on("data", (data) => { + provider.webview?.webview.postMessage({ type: 'stderr', value: data?.toString() }); + // TODO: Handle and highlight errors in the editor + }); + proc.on('close', (code) => { + provider.webview?.webview.postMessage({ type: 'close', value: code?.toString() }); + sketchProcess = undefined; + }); + provider.webview?.show?.(true); + provider.webview?.webview.postMessage({ type: 'clear'}); + sketchProcess = proc; + }); + + const stopSketch = commands.registerCommand('processing.sketch.stop', () => { + if (sketchProcess === undefined) { + return; + } + treeKill(sketchProcess?.pid as number); + }); + + context.subscriptions.push( + register, + startSketch, + stopSketch + ); +} + +// TODO: Add setting for timestamps +// TODO: Add setting for collapsing similar messages +// TODO: Add option to enable/disable stdout and stderr +class ProcessingConsoleViewProvider implements WebviewViewProvider { + public webview?: WebviewView; + + public resolveWebviewView(webviewView: WebviewView, context: WebviewViewResolveContext): Thenable | void { + webviewView.webview.options = { enableScripts: true }; + webviewView.webview.html = ` + + + + + + + `; + webviewView.onDidDispose(() => { + commands.executeCommand("processing.sketch.stop"); + }); + this.webview = webviewView; + } + +} \ No newline at end of file diff --git a/client/tsconfig.tsbuildinfo b/client/tsconfig.tsbuildinfo index 2671e2f..33286b7 100644 --- a/client/tsconfig.tsbuildinfo +++ b/client/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/compareversions.ts","./src/extension.ts","./src/setupcommands.ts","./src/setupdecorators.ts","./src/setuplanguageserver.ts","./src/setuppdefiles.ts","./src/setupselectedversion.ts","./src/setupsidebar.ts"],"version":"5.8.3"} \ No newline at end of file +{"root":["./src/compareversions.ts","./src/extension.ts","./src/setupcommands.ts","./src/setupconsole.ts","./src/setupdecorators.ts","./src/setuplanguageserver.ts","./src/setuppdefiles.ts","./src/setupselectedversion.ts","./src/setupsidebar.ts"],"version":"5.8.3"} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fdbac11..6d513da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,17 @@ { - "name": "processing4-vscode-extension", - "version": "1.0.6", + "name": "processing-vscode-extension", + "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "processing4-vscode-extension", - "version": "1.0.6", + "name": "processing-vscode-extension", + "version": "0.0.0", "hasInstallScript": true, "license": "LGPL-2.1-or-later", + "dependencies": { + "tree-kill": "^1.2.2" + }, "devDependencies": { "@eslint/js": "^9.13.0", "@stylistic/eslint-plugin": "^2.9.0", @@ -1797,6 +1800,15 @@ "node": ">=8.0" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", diff --git a/package.json b/package.json index fc04b7f..7985963 100644 --- a/package.json +++ b/package.json @@ -141,16 +141,33 @@ "title": "Processing", "icon": "media/processing-flat-color.svg" } + ], + "panel": [ + { + "id": "processingConsole", + "title": "Processing Console", + "icon": "media/processing-flat-color.svg" + } ] }, "views": { + "processingConsole": [ + { + "type": "webview", + "id": "processingConsoleView", + "name": "Console", + "icon": "media/processing-flat-color.svg" + } + ], "processingSidebar": [ { + "type": "tree", "id": "processingSidebarSketchbookView", "name": "Sketchbook", "icon": "media/processing-flat-color.svg" }, { + "type": "tree", "id": "processingSidebarExamplesView", "name": "Examples", "icon": "media/processing-flat-color.svg" @@ -194,5 +211,8 @@ "mocha": "^10.3.0", "typescript": "^5.8.2", "typescript-eslint": "^8.26.0" + }, + "dependencies": { + "tree-kill": "^1.2.2" } } From c94cb225bc94b29e3cccb1c2357b3620890508b7 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Mon, 1 Sep 2025 11:55:45 +0200 Subject: [PATCH 2/3] Added stateful command --- client/src/setupConsole.ts | 7 +++++++ media/restart.svg | 5 +++++ package.json | 12 +++++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 media/restart.svg diff --git a/client/src/setupConsole.ts b/client/src/setupConsole.ts index 0847894..5ecab8e 100644 --- a/client/src/setupConsole.ts +++ b/client/src/setupConsole.ts @@ -50,10 +50,16 @@ export default function setupConsole(context: ExtensionContext) { proc.on('close', (code) => { provider.webview?.webview.postMessage({ type: 'close', value: code?.toString() }); sketchProcess = undefined; + commands.executeCommand('setContext', 'processing.sketch.running', false); }); provider.webview?.show?.(true); provider.webview?.webview.postMessage({ type: 'clear'}); sketchProcess = proc; + commands.executeCommand('setContext', 'processing.sketch.running', true); + }); + + const restartSketch = commands.registerCommand('processing.sketch.restart', (resource: Uri) => { + commands.executeCommand('processing.sketch.run', resource); }); const stopSketch = commands.registerCommand('processing.sketch.stop', () => { @@ -66,6 +72,7 @@ export default function setupConsole(context: ExtensionContext) { context.subscriptions.push( register, startSketch, + restartSketch, stopSketch ); } diff --git a/media/restart.svg b/media/restart.svg new file mode 100644 index 0000000..536d921 --- /dev/null +++ b/media/restart.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/package.json b/package.json index 7985963..6bb895f 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,11 @@ "title": "Run the Processing Sketch", "icon": "media/start.svg" }, + { + "command": "processing.sketch.restart", + "title": "Restart the Processing Sketch", + "icon": "media/restart.svg" + }, { "command": "processing.sketch.stop", "title": "Stop the Processing Sketch", @@ -83,7 +88,12 @@ "editor/title": [ { "command": "processing.sketch.run", - "when": "editorLangId == Processing || editorLangId == java", + "when": "!processing.sketch.running && (editorLangId == Processing || editorLangId == java)", + "group": "navigation" + }, + { + "command": "processing.sketch.restart", + "when": "processing.sketch.running && (editorLangId == Processing || editorLangId == java)", "group": "navigation" }, { From 3e29e931c61ce702e418e3ee0f1a735bfb9ffe07 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Mon, 1 Sep 2025 11:59:32 +0200 Subject: [PATCH 3/3] Refactor sketch process management to use an array for multiple processes and improve cleanup on stop command --- client/src/setupConsole.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/client/src/setupConsole.ts b/client/src/setupConsole.ts index 5ecab8e..8c320fd 100644 --- a/client/src/setupConsole.ts +++ b/client/src/setupConsole.ts @@ -1,12 +1,11 @@ import { ChildProcess, spawn } from 'child_process'; -import { commands, ExtensionContext, Uri, ViewColumn, WebviewView, WebviewViewProvider, WebviewViewResolveContext, window, workspace } from 'vscode'; +import { commands, ExtensionContext, Uri, WebviewView, WebviewViewProvider, WebviewViewResolveContext, window, workspace } from 'vscode'; import { state } from './extension'; import { dirname } from 'path'; -import treeKill = require('tree-kill'); +import * as treeKill from 'tree-kill'; export default function setupConsole(context: ExtensionContext) { - // Convert to array to allow for waiting on the process to end - let sketchProcess: ChildProcess | undefined = undefined; + const sketchProcesses: ChildProcess[] = []; const provider = new ProcessingConsoleViewProvider(); @@ -49,12 +48,12 @@ export default function setupConsole(context: ExtensionContext) { }); proc.on('close', (code) => { provider.webview?.webview.postMessage({ type: 'close', value: code?.toString() }); - sketchProcess = undefined; + sketchProcesses.splice(sketchProcesses.indexOf(proc), 1); commands.executeCommand('setContext', 'processing.sketch.running', false); }); provider.webview?.show?.(true); provider.webview?.webview.postMessage({ type: 'clear'}); - sketchProcess = proc; + sketchProcesses.push(proc); commands.executeCommand('setContext', 'processing.sketch.running', true); }); @@ -63,10 +62,9 @@ export default function setupConsole(context: ExtensionContext) { }); const stopSketch = commands.registerCommand('processing.sketch.stop', () => { - if (sketchProcess === undefined) { - return; + for (const proc of sketchProcesses) { + treeKill(proc.pid as number); } - treeKill(sketchProcess?.pid as number); }); context.subscriptions.push(