diff --git a/rascal-vscode-extension/package.json b/rascal-vscode-extension/package.json index 373589c3..1da0c5c1 100644 --- a/rascal-vscode-extension/package.json +++ b/rascal-vscode-extension/package.json @@ -47,8 +47,28 @@ { "command": "rascalmpl.importModule", "title": "Start Rascal Terminal and Import this module" + }, + { + "command": "rascalmpl.startDebuggerForRepl", + "title": "Start Rascal debugger for REPL", + "icon": "$(debug)" } ], + "menus": { + "commandPalette": [ + { + "command": "rascalmpl.startDebuggerForRepl", + "when": "false" + } + ], + "view/item/context": [ + { + "command": "rascalmpl.startDebuggerForRepl", + "when": "view == rascalmpl-debugger-view && viewItem == 'canStartDebugging'", + "group": "inline" + } + ] + }, "languages": [ { "id": "rascalmpl", @@ -89,12 +109,24 @@ "icon": "./assets/images/rascal-logo-v2.1.svg", "visibility": "collapsed" } + ], + "debug": [ + { + "id": "rascalmpl-debugger-view", + "name": "Debug Rascal Terminal", + "icon": "$(debug)", + "visibility": "visible" + } ] }, "viewsWelcome": [ { "view": "rascalmpl-configuration-view", "contents": "No Rascal Projects found in the workspace" + }, + { + "view": "rascalmpl-debugger-view", + "contents": "No active Rascal REPLs found" } ], "breakpoints": [ diff --git a/rascal-vscode-extension/src/RascalExtension.ts b/rascal-vscode-extension/src/RascalExtension.ts index 4220f8b3..4479f70a 100644 --- a/rascal-vscode-extension/src/RascalExtension.ts +++ b/rascal-vscode-extension/src/RascalExtension.ts @@ -36,6 +36,7 @@ import { RascalTerminalLinkProvider } from './RascalTerminalLinkProvider'; import { VSCodeUriResolverServer } from './fs/VSCodeURIResolver'; import { RascalLibraryProvider } from './ux/LibraryNavigator'; import { FileType } from 'vscode'; +import { RascalDebugViewProvider } from './dap/RascalDebugView'; export class RascalExtension implements vscode.Disposable { private readonly vfsServer: VSCodeUriResolverServer; @@ -55,6 +56,7 @@ export class RascalExtension implements vscode.Disposable { checkForJVMUpdate(); vscode.window.registerTreeDataProvider('rascalmpl-configuration-view', new RascalLibraryProvider(this.rascal.rascalClient)); + vscode.window.registerTreeDataProvider('rascalmpl-debugger-view', new RascalDebugViewProvider(this.rascal.rascalDebugClient, context)); vscode.window.registerTerminalLinkProvider(new RascalTerminalLinkProvider(this.rascal.rascalClient)); } diff --git a/rascal-vscode-extension/src/dap/RascalDebugClient.ts b/rascal-vscode-extension/src/dap/RascalDebugClient.ts index 9d74a47d..127ff9a1 100644 --- a/rascal-vscode-extension/src/dap/RascalDebugClient.ts +++ b/rascal-vscode-extension/src/dap/RascalDebugClient.ts @@ -24,7 +24,7 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ -import { debug, DebugConfiguration, DebugSession, Terminal, window } from "vscode"; +import { debug, DebugConfiguration, DebugSession, Terminal, window, EventEmitter } from "vscode"; import { RascalDebugAdapterDescriptorFactory } from "./RascalDebugAdapterDescriptorFactory"; import { RascalDebugConfigurationProvider } from "./RascalDebugConfigurationProvider"; @@ -37,7 +37,8 @@ export class RascalDebugClient { debugSocketServersPorts: Map; // Terminal processID -> socket server port for debug runningDebugSessionsPorts: Set; // Stores all running debug session server ports - + private portEventEmitter = new EventEmitter<{processId: number, serverPort: number}>(); + readonly portRegistrationEvent = this.portEventEmitter.event; constructor(){ this.rascalDescriptorFactory = new RascalDebugAdapterDescriptorFactory(); @@ -77,13 +78,11 @@ export class RascalDebugClient { registerDebugServerPort(processID: number, serverPort: number){ this.debugSocketServersPorts.set(processID, serverPort); + this.portEventEmitter.fire({"processId": processID, "serverPort": serverPort}); } - getServerPort(processID: number | undefined){ - if(processID !== undefined && this.debugSocketServersPorts.has(processID)){ - return this.debugSocketServersPorts.get(processID); - } - return undefined; + getServerPort(processId: number){ + return this.debugSocketServersPorts.get(processId); } isConnectedToDebugServer(serverPort: number){ diff --git a/rascal-vscode-extension/src/dap/RascalDebugConfigurationProvider.ts b/rascal-vscode-extension/src/dap/RascalDebugConfigurationProvider.ts index 266137b8..c546f158 100644 --- a/rascal-vscode-extension/src/dap/RascalDebugConfigurationProvider.ts +++ b/rascal-vscode-extension/src/dap/RascalDebugConfigurationProvider.ts @@ -59,6 +59,9 @@ export class RascalDebugConfigurationProvider implements DebugConfigurationProvi if (!debugConfiguration['serverPort']){ const terminalProcessID = await window.activeTerminal?.processId; + if (terminalProcessID === undefined) { + throw Error("Active terminal has no associated process ID!"); + } const port = this.debugClient.getServerPort(terminalProcessID); if(port === undefined) { throw Error("Active terminal has not a debug server port registered !"); diff --git a/rascal-vscode-extension/src/dap/RascalDebugView.ts b/rascal-vscode-extension/src/dap/RascalDebugView.ts new file mode 100644 index 00000000..b3700ffe --- /dev/null +++ b/rascal-vscode-extension/src/dap/RascalDebugView.ts @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +import * as vscode from 'vscode'; +import { RascalDebugClient } from './RascalDebugClient'; + +export class RascalDebugViewProvider implements vscode.TreeDataProvider { + private changeEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this.changeEmitter.event; + + constructor(private readonly rascalDebugClient: RascalDebugClient, readonly context: vscode.ExtensionContext) { + const fireEmitter = (_: vscode.Terminal | vscode.DebugSession | {processId: number, serverPort: number} | undefined) : void => { + this.changeEmitter.fire(undefined); + }; + + vscode.window.onDidOpenTerminal(fireEmitter, this, context.subscriptions); + vscode.window.onDidCloseTerminal(fireEmitter, this, context.subscriptions); + vscode.window.onDidChangeActiveTerminal(fireEmitter, this, context.subscriptions); + vscode.debug.onDidStartDebugSession(fireEmitter, this, context.subscriptions); + vscode.debug.onDidTerminateDebugSession(fireEmitter, this, context.subscriptions); + + this.rascalDebugClient.portRegistrationEvent(fireEmitter, this, context.subscriptions); + + this.context.subscriptions.push( + vscode.commands.registerCommand("rascalmpl.startDebuggerForRepl", (replNode: RascalReplNode) => { + if (replNode.serverPort !== undefined) { + this.rascalDebugClient.startDebuggingSession(replNode.serverPort); + } + }, this) + ); + } + + getTreeItem(element: RascalReplNode): vscode.TreeItem | Thenable { + return element; + } + getChildren(element?: RascalReplNode | undefined): vscode.ProviderResult { + if (element === undefined) { + return this.updateRascalDebugView(); + } + return []; + } + getParent?(_element: RascalReplNode): vscode.ProviderResult { + //The Rascal debug view gives a flat list of the opened Rascal terminals. As such, only root items exit in this view. + return undefined; + } + resolveTreeItem?(item: vscode.TreeItem, _element: RascalReplNode, _token: vscode.CancellationToken): vscode.ProviderResult { + return item; + } + + makeLabel(label: string, isActiveTerminal : boolean) : string | vscode.TreeItemLabel { + if (isActiveTerminal) { + return {label: label, highlights : [[0, label.length]]}; + } + return label; + } + + async updateRascalDebugView() : Promise { + const result : RascalReplNode[] = []; + const activeTerminalProcessId = await vscode.window.activeTerminal?.processId; + for (const terminal of vscode.window.terminals) { + const processId = await terminal.processId; + if (processId === undefined) { + continue; + } + if (terminal.name.includes("Rascal terminal")) { + const label = this.makeLabel(terminal.name, processId === activeTerminalProcessId); + const serverPort = this.rascalDebugClient.getServerPort(processId); + const isDebugging = serverPort !== undefined && this.rascalDebugClient.isConnectedToDebugServer(serverPort); + const replNode = new RascalReplNode(label, serverPort, isDebugging); + if (serverPort !== undefined && !isDebugging) { + replNode.contextValue = "canStartDebugging"; + } + result.push(replNode); + } + } + return result; + } + +} + +export class RascalReplNode extends vscode.TreeItem { + constructor(label : string | vscode.TreeItemLabel, readonly serverPort : number | undefined, isDebugging : boolean) { + super(label, vscode.TreeItemCollapsibleState.None); + this.iconPath = new vscode.ThemeIcon(isDebugging ? "debug" : "terminal"); + } +}