Browse Source

feat: Proper breakpoint management for debug adapter

Matthias Ladkau 3 years ago
parent
commit
805cbd2232

+ 2 - 0
cli/ecal.go

@@ -23,6 +23,8 @@ import (
 TODO:
 - create executable binary (pack into single binary)
 - debug server support (vscode)
+- debug support break on start
+- debug support errors
 - pretty printer
 */
 

+ 12 - 16
ecal-support/.eslintrc.js

@@ -1,24 +1,20 @@
 module.exports = {
   env: {
     browser: true,
-    es2021: true
+    es2021: true,
   },
-  extends: [
-    'standard'
-  ],
-  parser: '@typescript-eslint/parser',
+  extends: ["standard"],
+  parser: "@typescript-eslint/parser",
   parserOptions: {
     ecmaVersion: 12,
-    sourceType: 'module'
+    sourceType: "module",
   },
-  plugins: [
-    '@typescript-eslint'
-  ],
+  plugins: ["@typescript-eslint"],
   rules: {
-    "eqeqeq": [2, "allow-null"],
-    "indent": [2, 4],
-    "quotes": [2, "single"],
-    "semi": [2, "always"],
-    "no-console": 0
-  }
-}
+    eqeqeq: [2, "allow-null"],
+    indent: [2, 4],
+    quotes: [2, "single"],
+    semi: [2, "always"],
+    "no-console": 0,
+  },
+};

+ 1 - 0
ecal-support/.prettierignore

@@ -0,0 +1 @@
+out

+ 1 - 0
ecal-support/.prettierrc.json

@@ -0,0 +1 @@
+{}

+ 9 - 11
ecal-support/.vscode/launch.json

@@ -1,14 +1,12 @@
 // A launch configuration that launches the extension inside a new window
 {
-    "version": "0.2.0",
-    "configurations": [
-        {
-            "name": "ECAL Extension",
-            "type": "extensionHost",
-            "request": "launch",
-            "args": [
-                "--extensionDevelopmentPath=${workspaceFolder}"
-            ]
-        }
-    ]
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "name": "ECAL Extension",
+      "type": "extensionHost",
+      "request": "launch",
+      "args": ["--extensionDevelopmentPath=${workspaceFolder}"]
+    }
+  ]
 }

+ 4 - 4
ecal-support/README.md

@@ -2,9 +2,9 @@
 
 ## Folder content
 
-* `package.json` - manifest file
-* `syntaxes/ecal.tmLanguage.json` - Text mate grammar file
-* `language-configuration.json` - language configuration for VSCode
+- `package.json` - manifest file
+- `syntaxes/ecal.tmLanguage.json` - Text mate grammar file
+- `language-configuration.json` - language configuration for VSCode
 
 ## Build the extention
 
@@ -45,4 +45,4 @@ https://devt.de/krotik/ecal/releases
 
 ## Developing the extension
 
-In VSCode the extention can be launched and debugged using the included launch configuration. Press F5 to start a VS Code instance with ECAL support extention form the development code.
+In VSCode the extention can be launched and debugged using the included launch configuration. Press F5 to start a VS Code instance with ECAL support extention form the development code.

+ 26 - 26
ecal-support/language-configuration.json

@@ -1,28 +1,28 @@
 {
-    "comments": {
-        "lineComment": "#",
-        "blockComment": [ "/*", "*/" ]
-    },
-    // symbols used as brackets
-    "brackets": [
-        ["{", "}"],
-        ["[", "]"],
-        ["(", ")"]
-    ],
-    // symbols that are auto closed when typing
-    "autoClosingPairs": [
-        ["{", "}"],
-        ["[", "]"],
-        ["(", ")"],
-        ["\"", "\""],
-        ["'", "'"]
-    ],
-    // symbols that can be used to surround a selection
-    "surroundingPairs": [
-        ["{", "}"],
-        ["[", "]"],
-        ["(", ")"],
-        ["\"", "\""],
-        ["'", "'"]
-    ]
+  "comments": {
+    "lineComment": "#",
+    "blockComment": ["/*", "*/"]
+  },
+  // symbols used as brackets
+  "brackets": [
+    ["{", "}"],
+    ["[", "]"],
+    ["(", ")"]
+  ],
+  // symbols that are auto closed when typing
+  "autoClosingPairs": [
+    ["{", "}"],
+    ["[", "]"],
+    ["(", ")"],
+    ["\"", "\""],
+    ["'", "'"]
+  ],
+  // symbols that can be used to surround a selection
+  "surroundingPairs": [
+    ["{", "}"],
+    ["[", "]"],
+    ["(", ")"],
+    ["\"", "\""],
+    ["'", "'"]
+  ]
 }

+ 133 - 131
ecal-support/package.json

@@ -1,137 +1,139 @@
 {
-    "name": "ecal-support",
-    "displayName": "ECAL Support",
-    "version": "0.9.1",
-    "publisher": "krotik",
-    "description": "Extension to support the development of ECAL scripts in VS Code.",
-    "author": {
-        "name": "Matthias Ladkau",
-        "email": "github@ladkau.de"
-    },
-    "license": "MIT",
-    "engines": {
-        "vscode": "^1.50.0"
-    },
-    "icon": "images/logo.png",
-    "categories": [
-        "Programming Languages"
-    ],
-    "repository": {
-        "type": "git",
-        "url": "https://devt.de/krotik/ecal.git"
-    },
-    "scripts": {
-        "compile": "tsc",
-        "watch": "tsc -w",
-        "package": "vsce package",
-        "lint": "eslint 'src/**/*.{js,ts,tsx}' --quiet --fix"
-    },
-    "dependencies": {
-        "@jpwilliams/waitgroup": "1.0.1",
-        "vscode-debugadapter": "^1.42.1",
-        "promise-socket": "^7.0.0",
-        "readline-promise": "^1.0.4",
-        "async-lock": "^1.2.4"
-    },
-    "devDependencies": {
-        "@types/node": "^14.14.2",
-        "@types/vscode": "^1.50.0",
-        "@typescript-eslint/eslint-plugin": "^4.5.0",
-        "@typescript-eslint/parser": "^4.5.0",
-        "eslint": "^7.12.0",
-        "eslint-config-standard": "^15.0.0",
-        "eslint-plugin-import": "^2.22.1",
-        "eslint-plugin-node": "^11.1.0",
-        "eslint-plugin-promise": "^4.2.1",
-        "eslint-plugin-standard": "^4.0.2",
-        "typescript": "^4.0.3",
-        "vsce": "^1.81.1"
-    },
-    "main": "./out/extension.js",
-    "activationEvents": [
-        "onDebug"
-    ],
-    "contributes": {
-        "languages": [
-            {
-                "id": "ecal",
-                "aliases": [
-                    "Event Condition Action Language",
-                    "ecal"
-                ],
-                "extensions": [
-                    ".ecal"
-                ],
-                "configuration": "./language-configuration.json"
-            }
+  "name": "ecal-support",
+  "displayName": "ECAL Support",
+  "version": "0.9.1",
+  "publisher": "krotik",
+  "description": "Extension to support the development of ECAL scripts in VS Code.",
+  "author": {
+    "name": "Matthias Ladkau",
+    "email": "github@ladkau.de"
+  },
+  "license": "MIT",
+  "engines": {
+    "vscode": "^1.50.0"
+  },
+  "icon": "images/logo.png",
+  "categories": [
+    "Programming Languages"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "https://devt.de/krotik/ecal.git"
+  },
+  "scripts": {
+    "compile": "tsc",
+    "watch": "tsc -w",
+    "package": "vsce package",
+    "lint": "eslint 'src/**/*.{js,ts,tsx}' --quiet",
+    "pretty": "prettier --write ."
+  },
+  "dependencies": {
+    "@jpwilliams/waitgroup": "1.0.1",
+    "vscode-debugadapter": "^1.42.1",
+    "promise-socket": "^7.0.0",
+    "readline-promise": "^1.0.4",
+    "async-lock": "^1.2.4"
+  },
+  "devDependencies": {
+    "@types/node": "^14.14.2",
+    "@types/vscode": "^1.50.0",
+    "@typescript-eslint/eslint-plugin": "^4.5.0",
+    "@typescript-eslint/parser": "^4.5.0",
+    "eslint": "^7.12.0",
+    "eslint-config-standard": "^15.0.0",
+    "eslint-plugin-import": "^2.22.1",
+    "eslint-plugin-node": "^11.1.0",
+    "eslint-plugin-promise": "^4.2.1",
+    "eslint-plugin-standard": "^4.0.2",
+    "prettier": "2.1.2",
+    "typescript": "^4.0.3",
+    "vsce": "^1.81.1"
+  },
+  "main": "./out/extension.js",
+  "activationEvents": [
+    "onDebug"
+  ],
+  "contributes": {
+    "languages": [
+      {
+        "id": "ecal",
+        "aliases": [
+          "Event Condition Action Language",
+          "ecal"
         ],
-        "grammars": [
-            {
-                "language": "ecal",
-                "scopeName": "source.ecal",
-                "path": "./syntaxes/ecal.tmLanguage.json"
-            }
+        "extensions": [
+          ".ecal"
         ],
-        "breakpoints": [
-            {
-                "language": "ecal"
-            }
-        ],
-        "debuggers": [
-            {
-                "type": "ecaldebug",
-                "label": "ECAL Debug",
-                "program": "./out/ecalDebugAdapter.js",
-                "runtime": "node",
-                "configurationAttributes": {
-                    "launch": {
-                        "required": [
-                            "host",
-                            "port",
-                            "dir"
-                        ],
-                        "properties": {
-                            "host": {
-                                "type": "string",
-                                "description": "Host of the ECAL debug server.",
-                                "default": "localhost:43806"
-                            },
-                            "port": {
-                                "type": "number",
-                                "description": "Port of the ECAL debug server.",
-                                "default": "localhost:43806"
-                            },
-                            "dir": {
-                                "type": "string",
-                                "description": "Root directory for ECAL debug server.",
-                                "default": "${workspaceFolder}"
-                            },
-                            "executeOnEntry": {
-                                "type": "boolean",
-                                "description": "Execute the current edited ECAL script on entry. If this is set to false then code needs to be manually started from the ECAL debug server console.",
-                                "default": true
-                            },
-                            "trace": {
-                                "type": "boolean",
-                                "description": "Enable logging of the Debug Adapter Protocol.",
-                                "default": false
-                            }
-                        }
-                    }
-                },
-                "initialConfigurations": [
-                    {
-                        "type": "ecaldebug",
-                        "request": "launch",
-                        "name": "Debug ECAL script with ECAL Debug Server",
-                        "host": "localhost",
-                        "port": 33274,
-                        "dir": "${workspaceFolder}",
-                        "executeOnEntry": true,
-                        "trace": false
-                    }
-                ]
+        "configuration": "./language-configuration.json"
+      }
+    ],
+    "grammars": [
+      {
+        "language": "ecal",
+        "scopeName": "source.ecal",
+        "path": "./syntaxes/ecal.tmLanguage.json"
+      }
+    ],
+    "breakpoints": [
+      {
+        "language": "ecal"
+      }
+    ],
+    "debuggers": [
+      {
+        "type": "ecaldebug",
+        "label": "ECAL Debug",
+        "program": "./out/ecalDebugAdapter.js",
+        "runtime": "node",
+        "configurationAttributes": {
+          "launch": {
+            "required": [
+              "host",
+              "port",
+              "dir"
+            ],
+            "properties": {
+              "host": {
+                "type": "string",
+                "description": "Host of the ECAL debug server.",
+                "default": "localhost:43806"
+              },
+              "port": {
+                "type": "number",
+                "description": "Port of the ECAL debug server.",
+                "default": "localhost:43806"
+              },
+              "dir": {
+                "type": "string",
+                "description": "Root directory for ECAL debug server.",
+                "default": "${workspaceFolder}"
+              },
+              "executeOnEntry": {
+                "type": "boolean",
+                "description": "Execute the current edited ECAL script on entry. If this is set to false then code needs to be manually started from the ECAL debug server console.",
+                "default": true
+              },
+              "trace": {
+                "type": "boolean",
+                "description": "Enable logging of the Debug Adapter Protocol.",
+                "default": false
+              }
             }
+          }
+        },
+        "initialConfigurations": [
+          {
+            "type": "ecaldebug",
+            "request": "launch",
+            "name": "Debug ECAL script with ECAL Debug Server",
+            "host": "localhost",
+            "port": 33274,
+            "dir": "${workspaceFolder}",
+            "executeOnEntry": true,
+            "trace": false
+          }
         ]
-    }
+      }
+    ]
+  }
 }

+ 461 - 254
ecal-support/src/ecalDebugAdapter.ts

@@ -6,13 +6,21 @@
  */
 
 import {
-    logger, Logger, LoggingDebugSession, InitializedEvent,
-    Thread, Breakpoint
-} from 'vscode-debugadapter';
-import { DebugProtocol } from 'vscode-debugprotocol';
-import { WaitGroup } from '@jpwilliams/waitgroup';
-import { ECALDebugClient } from './ecalDebugClient';
-import * as vscode from 'vscode';
+  logger,
+  Logger,
+  LoggingDebugSession,
+  Thread,
+  Source,
+  Breakpoint,
+  InitializedEvent,
+  BreakpointEvent,
+  StoppedEvent,
+} from "vscode-debugadapter";
+import { DebugProtocol } from "vscode-debugprotocol";
+import { WaitGroup } from "@jpwilliams/waitgroup";
+import { ECALDebugClient } from "./ecalDebugClient";
+import * as vscode from "vscode";
+import { ClientBreakEvent, DebugStatus } from "./types";
 
 /**
  * ECALDebugArguments are the arguments which VSCode can pass to the debug adapter.
@@ -20,11 +28,11 @@ import * as vscode from 'vscode';
  * debug adapter from a lauch configuration ('.vscode/launch.json') in a project folder.
  */
 interface ECALDebugArguments extends DebugProtocol.LaunchRequestArguments {
-    host: string; // Host of the ECAL debug server
-    port: number; // Port of the ECAL debug server
-    dir: string; // Root directory for ECAL interpreter
-    executeOnEntry?: boolean; // Flag if the debugged script should be executed when the debug session is started
-    trace?: boolean; // Flag to enable verbose logging of the adapter protocol
+  host: string; // Host of the ECAL debug server
+  port: number; // Port of the ECAL debug server
+  dir: string; // Root directory for ECAL interpreter
+  executeOnEntry?: boolean; // Flag if the debugged script should be executed when the debug session is started
+  trace?: boolean; // Flag to enable verbose logging of the adapter protocol
 }
 
 /**
@@ -36,331 +44,530 @@ interface ECALDebugArguments extends DebugProtocol.LaunchRequestArguments {
  * https://microsoft.github.io/debug-adapter-protocol/overview#How_it_works
  */
 export class ECALDebugSession extends LoggingDebugSession {
-    /**
-     * WaitGroup to wait the finish of the configuration sequence
-     */
-    private wgConfig = new WaitGroup();
+  /**
+   * Client to the ECAL debug server
+   */
+  private client: ECALDebugClient;
 
-    private client: ECALDebugClient;
+  /**
+   * Output channel for log messages
+   */
+  private extout: vscode.OutputChannel = vscode.window.createOutputChannel(
+    "ECAL Debug Session"
+  );
 
-    private extout : vscode.OutputChannel = vscode.window.createOutputChannel('ECAL Debug Session');
+  /**
+   * WaitGroup to wait the finish of the configuration sequence
+   */
+  private wgConfig = new WaitGroup();
 
-    private config :ECALDebugArguments = {} as ECALDebugArguments;
+  private config: ECALDebugArguments = {} as ECALDebugArguments;
 
-    /**
-     * Create a new debug adapter which is used for one debug session.
-     */
-    public constructor () {
-        super('mock-debug.txt');
+  private unconfirmedBreakpoints: DebugProtocol.Breakpoint[] = [];
 
-        this.extout.appendLine('Creating Debug Session');
-        this.client = new ECALDebugClient(new LogChannelAdapter(this.extout));
+  private bpCount: number = 1;
+  private bpIds: Record<string, number> = {};
 
-        // Add event handlers
+  public sendEvent(event: DebugProtocol.Event): void {
+    super.sendEvent(event);
+    console.error("#### Sending event:", event);
+  }
 
-        this.client.on('pauseOnBreakpoint', e => {
-            console.log("event:", e)
-        })
+  /**
+   * Create a new debug adapter which is used for one debug session.
+   */
+  public constructor() {
+    super("mock-debug.txt");
+
+    this.extout.appendLine("Creating Debug Session");
+    this.client = new ECALDebugClient(new LogChannelAdapter(this.extout));
+
+    // Add event handlers
+
+    this.client.on("pauseOnBreakpoint", (e: ClientBreakEvent) => {
+      console.log("#### send StoppedEvent event:", e.tid, typeof e.tid);
+      this.sendEvent(new StoppedEvent("breakpoint", e.tid));
+    });
+
+    this.client.on("status", (e: DebugStatus) => {
+      try {
+        if (this.unconfirmedBreakpoints.length > 0) {
+          for (const toConfirm of this.unconfirmedBreakpoints) {
+            for (const [breakpointString, ok] of Object.entries(
+              e.breakpoints
+            )) {
+              const line = parseInt(breakpointString.split(":")[1]);
+              if (ok) {
+                if (
+                  toConfirm.line === line &&
+                  toConfirm.source?.name === breakpointString
+                ) {
+                  console.log("Confirmed breakpoint:", breakpointString);
+                  toConfirm.verified = true;
+                  this.sendEvent(new BreakpointEvent("changed", toConfirm));
+                }
+              }
+            }
+          }
+          this.unconfirmedBreakpoints = [];
+        }
+      } catch (e) {
+        console.error(e);
+      }
+    });
 
-        // Lines and columns start at 1
-        this.setDebuggerLinesStartAt1(true);
-        this.setDebuggerColumnsStartAt1(true);
+    // Lines and columns start at 1
+    this.setDebuggerLinesStartAt1(true);
+    this.setDebuggerColumnsStartAt1(true);
 
-        // Increment the config WaitGroup counter for configurationDoneRequest()
-        this.wgConfig.add(1);
-    }
+    // Increment the config WaitGroup counter for configurationDoneRequest()
+    this.wgConfig.add(1);
+  }
 
-    /**
-     * Called as the first step in the DAP. The client (e.g. VSCode)
-     * interrogates the debug adapter on the features which it provides.
-     */
-    protected initializeRequest (response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void {
-        console.log('##### initializeRequest:', args);
+  /**
+   * Called as the first step in the DAP. The client (e.g. VSCode)
+   * interrogates the debug adapter on the features which it provides.
+   */
+  protected initializeRequest(
+    response: DebugProtocol.InitializeResponse,
+    args: DebugProtocol.InitializeRequestArguments
+  ): void {
+    console.log("##### initializeRequest:", args);
 
-        response.body = response.body || {};
+    response.body = response.body || {};
 
-        // The adapter implements the configurationDoneRequest.
-        response.body.supportsConfigurationDoneRequest = true;
+    // The adapter implements the configurationDoneRequest.
+    response.body.supportsConfigurationDoneRequest = true;
 
-        this.sendResponse(response);
+    // make VS Code to send cancelRequests
+    response.body.supportsCancelRequest = true;
 
-        this.sendEvent(new InitializedEvent());
-    }
+    // make VS Code send the breakpointLocations request
+    response.body.supportsBreakpointLocationsRequest = true;
 
-    /**
-     * Called as part of the "configuration Done" step in the DAP. The client (e.g. VSCode) has
-     * finished the initialization of the debug adapter.
-     */
-    protected configurationDoneRequest (response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments): void {
-        console.log('##### configurationDoneRequest');
+    // make VS Code provide "Step in Target" functionality
+    response.body.supportsStepInTargetsRequest = true;
 
-        super.configurationDoneRequest(response, args);
-        this.wgConfig.done();
-    }
+    this.sendResponse(response);
 
-    /**
-     * The client (e.g. VSCode) asks the debug adapter to start the debuggee communication.
-     */
-    protected async launchRequest (response: DebugProtocol.LaunchResponse, args: ECALDebugArguments) {
-        console.log('##### launchRequest:', args);
+    this.sendEvent(new InitializedEvent());
+  }
 
-        this.config = args; // Store the configuration
+  /**
+   * Called as part of the "configuration Done" step in the DAP. The client (e.g. VSCode) has
+   * finished the initialization of the debug adapter.
+   */
+  protected configurationDoneRequest(
+    response: DebugProtocol.ConfigurationDoneResponse,
+    args: DebugProtocol.ConfigurationDoneArguments
+  ): void {
+    console.log("##### configurationDoneRequest");
+
+    super.configurationDoneRequest(response, args);
+    this.wgConfig.done();
+  }
 
-        // Setup logging either verbose or just on errors
+  /**
+   * The client (e.g. VSCode) asks the debug adapter to start the debuggee communication.
+   */
+  protected async launchRequest(
+    response: DebugProtocol.LaunchResponse,
+    args: ECALDebugArguments
+  ) {
+    console.log("##### launchRequest:", args);
 
-        logger.setup(args.trace ? Logger.LogLevel.Verbose : Logger.LogLevel.Error, false);
+    this.config = args; // Store the configuration
 
-        await this.wgConfig.wait(); // Wait for configuration sequence to finish
+    // Setup logging either verbose or just on errors
 
-        this.extout.appendLine(`Configuration loaded: ${JSON.stringify(args)}`);
+    logger.setup(
+      args.trace ? Logger.LogLevel.Verbose : Logger.LogLevel.Error,
+      false
+    );
 
-        await this.client.conect(args.host, args.port);
+    await this.wgConfig.wait(); // Wait for configuration sequence to finish
 
-        this.sendResponse(response);
-    }
+    this.extout.appendLine(`Configuration loaded: ${JSON.stringify(args)}`);
 
-    protected async setBreakPointsRequest (response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): Promise<void> {
-        console.error('##### setBreakPointsRequest:', args);
+    await this.client.conect(args.host, args.port);
 
-        const breakpoints:DebugProtocol.Breakpoint[] = [];
+    console.log("##### launchRequest result:", response.body);
 
-        if (args.source.path?.indexOf(this.config.dir) === 0) {
-            const source = args.source.path.slice(this.config.dir.length + 1);
+    this.sendResponse(response);
+  }
 
-            // Clear all breakpoints of the file
+  protected async setBreakPointsRequest(
+    response: DebugProtocol.SetBreakpointsResponse,
+    args: DebugProtocol.SetBreakpointsArguments
+  ): Promise<void> {
+    console.log("##### setBreakPointsRequest:", args);
 
-            await this.client.clearBreakpoints(source);
+    let breakpoints: DebugProtocol.Breakpoint[] = [];
 
-            // Set all breakpoints
+    if (args.source.path?.indexOf(this.config.dir) === 0) {
+      const sourcePath = args.source.path.slice(this.config.dir.length + 1);
 
-            for (const line of args.lines || []) {
-                await this.client.setBreakpoint(`${source}:${line}`);
-            }
+      // Clear all breakpoints of the file
 
-            // Confirm that the breakpoints have been set
+      await this.client.clearBreakpoints(sourcePath);
 
-            const status = await this.client.status();
-            if (status) {
-                for (const [k, v] of Object.entries(status.breakpoints)) {
-                    if (v) {
-                        const line = parseInt(k.split(':')[1]);
-                        this.extout.appendLine(`Setting breakpoint for ${args.source.name}: ${line}`);
-                        breakpoints.push(new Breakpoint(true, line));
-                    }
-                }
-            }
-        }
+      // Send all breakpoint requests to the debug server
+
+      for (const sbp of args.breakpoints || []) {
+        await this.client.setBreakpoint(`${sourcePath}:${sbp.line}`);
+      }
 
-        response.body = {
-            breakpoints
-        };
+      // Confirm that the breakpoints have been set
 
-        this.sendResponse(response);
+      const status = await this.client.status();
+
+      if (status) {
+        breakpoints = (args.lines || []).map((line) => {
+          const breakpointString = `${sourcePath}:${line}`;
+          const bp: DebugProtocol.Breakpoint = new Breakpoint(
+            status.breakpoints[breakpointString],
+            line,
+            undefined,
+            new Source(breakpointString, args.source.path)
+          );
+          bp.id = this.getBreakPointId(breakpointString);
+          return bp;
+        });
+      } else {
+        for (const sbp of args.breakpoints || []) {
+          const breakpointString = `${sourcePath}:${sbp.line}`;
+          const bp: DebugProtocol.Breakpoint = new Breakpoint(
+            false,
+            sbp.line,
+            undefined,
+            new Source(breakpointString, args.source.path)
+          );
+          bp.id = this.getBreakPointId(breakpointString);
+          breakpoints.push(bp);
+        }
+        this.unconfirmedBreakpoints = breakpoints;
+        console.log(
+          "Breakpoints to be confirmed:",
+          this.unconfirmedBreakpoints
+        );
+      }
     }
 
-    protected async threadsRequest (response: DebugProtocol.ThreadsResponse): Promise<void> {
-        console.log('##### threadsRequest');
+    response.body = {
+      breakpoints,
+    };
+
+    console.error("##### setBreakPointsRequest result:", response.body);
 
-        const status = await this.client.status();
-        const threads = [];
+    this.sendResponse(response);
+  }
 
-        if (status) {
-            for (const tid in Object.keys(status.threads)) {
-                threads.push(new Thread(parseInt(tid), `Thread ${tid}`));
+  protected async breakpointLocationsRequest(
+    response: DebugProtocol.BreakpointLocationsResponse,
+    args: DebugProtocol.BreakpointLocationsArguments
+  ) {
+    let breakpoints: DebugProtocol.BreakpointLocation[] = [];
+
+    if (args.source.path?.indexOf(this.config.dir) === 0) {
+      const sourcePath = args.source.path.slice(this.config.dir.length + 1);
+      const status = await this.client.status();
+
+      if (status) {
+        for (const [breakpointString, v] of Object.entries(
+          status.breakpoints
+        )) {
+          if (v) {
+            const line = parseInt(breakpointString.split(":")[1]);
+            if (`${sourcePath}:${line}` === breakpointString) {
+              breakpoints.push({
+                line,
+              });
             }
-        } else {
-            threads.push(new Thread(1, 'Thread 1'));
+          }
         }
+      }
+    }
+    response.body = {
+      breakpoints,
+    };
 
-        response.body = {
-            threads
-        };
+    this.sendResponse(response);
+  }
 
-        this.sendResponse(response);
-    }
+  protected async threadsRequest(
+    response: DebugProtocol.ThreadsResponse
+  ): Promise<void> {
+    console.log("##### threadsRequest");
 
-    protected stackTraceRequest (response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void {
-        console.error('##### stackTraceRequest:', args);
+    const status = await this.client.status();
+    const threads = [];
 
-        response.body = {
-            stackFrames: []
-        };
-        this.sendResponse(response);
+    if (status) {
+      for (const tid of Object.keys(status.threads)) {
+        threads.push(new Thread(parseInt(tid), `Thread ${tid}`));
+      }
+    } else {
+      threads.push(new Thread(1, "Thread 1"));
     }
 
-    protected scopesRequest (response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void {
-        console.error('##### scopesRequest:', args);
+    response.body = {
+      threads,
+    };
 
-        response.body = {
-            scopes: []
-        };
-        this.sendResponse(response);
-    }
+    console.log("##### threadsRequest result:", response.body);
 
-    protected async variablesRequest (response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments, request?: DebugProtocol.Request) {
-        console.error('##### variablesRequest', args, request);
+    this.sendResponse(response);
+  }
 
-        response.body = {
-            variables: []
-        };
-        this.sendResponse(response);
-    }
+  protected stackTraceRequest(
+    response: DebugProtocol.StackTraceResponse,
+    args: DebugProtocol.StackTraceArguments
+  ): void {
+    console.error("##### stackTraceRequest:", args);
 
-    protected continueRequest (response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void {
-        console.error('##### continueRequest', args);
-        this.sendResponse(response);
-    }
+    response.body = {
+      stackFrames: [],
+    };
+    this.sendResponse(response);
+  }
 
-    protected reverseContinueRequest (response: DebugProtocol.ReverseContinueResponse, args: DebugProtocol.ReverseContinueArguments): void {
-        console.error('##### reverseContinueRequest', args);
-        this.sendResponse(response);
-    }
+  protected scopesRequest(
+    response: DebugProtocol.ScopesResponse,
+    args: DebugProtocol.ScopesArguments
+  ): void {
+    console.error("##### scopesRequest:", args);
 
-    protected nextRequest (response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void {
-        console.error('##### nextRequest', args);
-        this.sendResponse(response);
-    }
+    response.body = {
+      scopes: [],
+    };
+    this.sendResponse(response);
+  }
 
-    protected stepBackRequest (response: DebugProtocol.StepBackResponse, args: DebugProtocol.StepBackArguments): void {
-        console.error('##### stepBackRequest', args);
-        this.sendResponse(response);
-    }
+  protected async variablesRequest(
+    response: DebugProtocol.VariablesResponse,
+    args: DebugProtocol.VariablesArguments,
+    request?: DebugProtocol.Request
+  ) {
+    console.error("##### variablesRequest", args, request);
+
+    response.body = {
+      variables: [],
+    };
+    this.sendResponse(response);
+  }
 
-    protected stepInTargetsRequest (response: DebugProtocol.StepInTargetsResponse, args: DebugProtocol.StepInTargetsArguments) {
-        console.error('##### stepInTargetsRequest', args);
-        response.body = {
-            targets: []
-        };
-        this.sendResponse(response);
-    }
+  protected continueRequest(
+    response: DebugProtocol.ContinueResponse,
+    args: DebugProtocol.ContinueArguments
+  ): void {
+    console.error("##### continueRequest", args);
+    this.sendResponse(response);
+  }
 
-    protected stepInRequest (response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments): void {
-        console.error('##### stepInRequest', args);
-        this.sendResponse(response);
-    }
+  protected reverseContinueRequest(
+    response: DebugProtocol.ReverseContinueResponse,
+    args: DebugProtocol.ReverseContinueArguments
+  ): void {
+    console.error("##### reverseContinueRequest", args);
+    this.sendResponse(response);
+  }
 
-    protected stepOutRequest (response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments): void {
-        console.error('##### stepOutRequest', args);
-        this.sendResponse(response);
-    }
+  protected nextRequest(
+    response: DebugProtocol.NextResponse,
+    args: DebugProtocol.NextArguments
+  ): void {
+    console.error("##### nextRequest", args);
+    this.sendResponse(response);
+  }
 
-    protected async evaluateRequest (response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): Promise<void> {
-        let result: any;
+  protected stepBackRequest(
+    response: DebugProtocol.StepBackResponse,
+    args: DebugProtocol.StepBackArguments
+  ): void {
+    console.error("##### stepBackRequest", args);
+    this.sendResponse(response);
+  }
 
-        try {
-            result = await this.client.sendCommandString(`${args.expression}\r\n`);
+  protected stepInTargetsRequest(
+    response: DebugProtocol.StepInTargetsResponse,
+    args: DebugProtocol.StepInTargetsArguments
+  ) {
+    console.error("##### stepInTargetsRequest", args);
+    response.body = {
+      targets: [],
+    };
+    this.sendResponse(response);
+  }
 
-            if (typeof (result) !== 'string') {
-                result = JSON.stringify(result, null, '  ');
-            }
-        } catch (e) {
-            result = String(e);
-        }
+  protected stepInRequest(
+    response: DebugProtocol.StepInResponse,
+    args: DebugProtocol.StepInArguments
+  ): void {
+    console.error("##### stepInRequest", args);
+    this.sendResponse(response);
+  }
 
-        response.body = {
-            result,
-            variablesReference: 0
-        };
+  protected stepOutRequest(
+    response: DebugProtocol.StepOutResponse,
+    args: DebugProtocol.StepOutArguments
+  ): void {
+    console.error("##### stepOutRequest", args);
+    this.sendResponse(response);
+  }
+
+  protected async evaluateRequest(
+    response: DebugProtocol.EvaluateResponse,
+    args: DebugProtocol.EvaluateArguments
+  ): Promise<void> {
+    let result: any;
 
-        this.sendResponse(response);
+    try {
+      result = await this.client.sendCommandString(`${args.expression}\r\n`);
+
+      if (typeof result !== "string") {
+        result = JSON.stringify(result, null, "  ");
+      }
+    } catch (e) {
+      result = String(e);
     }
 
-    protected dataBreakpointInfoRequest (response: DebugProtocol.DataBreakpointInfoResponse, args: DebugProtocol.DataBreakpointInfoArguments): void {
-        console.error('##### dataBreakpointInfoRequest', args);
+    response.body = {
+      result,
+      variablesReference: 0,
+    };
 
-        response.body = {
-            dataId: null,
-            description: 'cannot break on data access',
-            accessTypes: undefined,
-            canPersist: false
-        };
+    this.sendResponse(response);
+  }
 
-        this.sendResponse(response);
-    }
+  protected dataBreakpointInfoRequest(
+    response: DebugProtocol.DataBreakpointInfoResponse,
+    args: DebugProtocol.DataBreakpointInfoArguments
+  ): void {
+    console.error("##### dataBreakpointInfoRequest", args);
 
-    protected setDataBreakpointsRequest (response: DebugProtocol.SetDataBreakpointsResponse, args: DebugProtocol.SetDataBreakpointsArguments): void {
-        console.error('##### setDataBreakpointsRequest', args);
+    response.body = {
+      dataId: null,
+      description: "cannot break on data access",
+      accessTypes: undefined,
+      canPersist: false,
+    };
 
-        response.body = {
-            breakpoints: []
-        };
+    this.sendResponse(response);
+  }
 
-        this.sendResponse(response);
-    }
+  protected setDataBreakpointsRequest(
+    response: DebugProtocol.SetDataBreakpointsResponse,
+    args: DebugProtocol.SetDataBreakpointsArguments
+  ): void {
+    console.error("##### setDataBreakpointsRequest", args);
 
-    protected completionsRequest (response: DebugProtocol.CompletionsResponse, args: DebugProtocol.CompletionsArguments): void {
-        console.error('##### completionsRequest', args);
-
-        response.body = {
-            targets: [
-                {
-                    label: 'item 10',
-                    sortText: '10'
-                },
-                {
-                    label: 'item 1',
-                    sortText: '01'
-                },
-                {
-                    label: 'item 2',
-                    sortText: '02'
-                },
-                {
-                    label: 'array[]',
-                    selectionStart: 6,
-                    sortText: '03'
-                },
-                {
-                    label: 'func(arg)',
-                    selectionStart: 5,
-                    selectionLength: 3,
-                    sortText: '04'
-                }
-            ]
-        };
-        this.sendResponse(response);
-    }
+    response.body = {
+      breakpoints: [],
+    };
 
-    protected cancelRequest (response: DebugProtocol.CancelResponse, args: DebugProtocol.CancelArguments) {
-        console.error('##### cancelRequest', args);
-        this.sendResponse(response);
-    }
+    this.sendResponse(response);
+  }
+
+  protected completionsRequest(
+    response: DebugProtocol.CompletionsResponse,
+    args: DebugProtocol.CompletionsArguments
+  ): void {
+    console.error("##### completionsRequest", args);
+
+    response.body = {
+      targets: [
+        {
+          label: "item 10",
+          sortText: "10",
+        },
+        {
+          label: "item 1",
+          sortText: "01",
+        },
+        {
+          label: "item 2",
+          sortText: "02",
+        },
+        {
+          label: "array[]",
+          selectionStart: 6,
+          sortText: "03",
+        },
+        {
+          label: "func(arg)",
+          selectionStart: 5,
+          selectionLength: 3,
+          sortText: "04",
+        },
+      ],
+    };
+    this.sendResponse(response);
+  }
 
-    protected customRequest (command: string, response: DebugProtocol.Response, args: any) {
-        console.error('##### customRequest', args);
+  protected cancelRequest(
+    response: DebugProtocol.CancelResponse,
+    args: DebugProtocol.CancelArguments
+  ) {
+    console.error("##### cancelRequest", args);
+    this.sendResponse(response);
+  }
 
-        if (command === 'toggleFormatting') {
-            this.sendResponse(response);
-        } else {
-            super.customRequest(command, response, args);
-        }
+  protected customRequest(
+    command: string,
+    response: DebugProtocol.Response,
+    args: any
+  ) {
+    console.error("##### customRequest", args);
+
+    if (command === "toggleFormatting") {
+      this.sendResponse(response);
+    } else {
+      super.customRequest(command, response, args);
     }
+  }
 
-    public shutdown () {
-        console.log('#### Shutdown');
-        this.client?.shutdown().then(() => {
-            this.extout.appendLine('Debug Session has finished');
-        }).catch(e => {
-            this.extout.appendLine(`Debug Session has finished with an error: ${e}`);
-        });
+  public shutdown() {
+    console.log("#### Shutdown");
+    this.client
+      ?.shutdown()
+      .then(() => {
+        this.extout.appendLine("Debug Session has finished");
+      })
+      .catch((e) => {
+        this.extout.appendLine(
+          `Debug Session has finished with an error: ${e}`
+        );
+      });
+  }
+
+  /**
+   * Map a given breakpoint string to a breakpoint ID.
+   */
+  private getBreakPointId(breakpointString: string): number {
+    let id = this.bpIds[breakpointString];
+    if (!id) {
+      id = this.bpCount++;
+      this.bpIds[breakpointString] = id;
     }
+    return id;
+  }
 }
 
 class LogChannelAdapter {
-  private out: vscode.OutputChannel
+  private out: vscode.OutputChannel;
 
-  constructor (out: vscode.OutputChannel) {
-      this.out = out;
+  constructor(out: vscode.OutputChannel) {
+    this.out = out;
   }
 
-  log (value: string): void {
-      this.out.appendLine(value);
+  log(value: string): void {
+    this.out.appendLine(value);
   }
 
-  error (value: string): void {
-      this.out.appendLine(`Error: ${value}`);
-      setTimeout(() => {
-          this.out.show(true);
-      }, 500);
+  error(value: string): void {
+    this.out.appendLine(`Error: ${value}`);
+    setTimeout(() => {
+      this.out.show(true);
+    }, 500);
   }
 }

+ 136 - 130
ecal-support/src/ecalDebugClient.ts

@@ -2,183 +2,189 @@
  * Debug client implementation for the ECAL debugger.
  */
 
-import * as net from 'net';
-import { EventEmitter } from 'events';
-import PromiseSocket from 'promise-socket';
-import { LogOutputStream, DebugStatus, ThreadInspection } from './types';
+import * as net from "net";
+import { EventEmitter } from "events";
+import PromiseSocket from "promise-socket";
+import { LogOutputStream, DebugStatus, ThreadInspection } from "./types";
 
 interface BacklogCommand {
-  cmd:string,
-  args? :string[],
+  cmd: string;
+  args?: string[];
 }
 
 /**
  * Debug client for ECAL debug server.
  */
 export class ECALDebugClient extends EventEmitter {
-  private socket : PromiseSocket<net.Socket>;
-  private socketLock : any;
-  private connected : boolean = false
-  private backlog : BacklogCommand[] = []
-  private threadInspection : Record<string, ThreadInspection> = {}
+  private socket: PromiseSocket<net.Socket>;
+  private socketLock: any;
+  private connected: boolean = false;
+  private backlog: BacklogCommand[] = [];
+  private threadInspection: Record<number, ThreadInspection> = {};
 
   /**
-     * Create a new debug client.
-     */
-  public constructor (
-        protected out: LogOutputStream
-  ) {
-    super()
+   * Create a new debug client.
+   */
+  public constructor(protected out: LogOutputStream) {
+    super();
     this.socket = new PromiseSocket(new net.Socket());
 
-      const AsyncLock = require('async-lock');
-      this.socketLock = new AsyncLock();
+    const AsyncLock = require("async-lock");
+    this.socketLock = new AsyncLock();
   }
 
-  public async conect (host: string, port: number) {
-      try {
-          this.out.log(`Connecting to: ${host}:${port}`);
-          await this.socket.connect({ port, host });
-          // this.socket.setTimeout(2000);
-          this.connected = true;
-          this.pollEvents(); // Start emitting events
-      } catch (e) {
-          this.out.error(`Could not connect to debug server: ${e}`);
-      }
+  public async conect(host: string, port: number) {
+    try {
+      this.out.log(`Connecting to: ${host}:${port}`);
+      await this.socket.connect({ port, host });
+      // this.socket.setTimeout(2000);
+      this.connected = true;
+      this.pollEvents(); // Start emitting events
+    } catch (e) {
+      this.out.error(`Could not connect to debug server: ${e}`);
+    }
   }
 
-  public async status () : Promise<DebugStatus | null> {
-      try {
-          return await this.sendCommand('status') as DebugStatus;
-      } catch (e) {
-          this.out.error(`Could not query for status: ${e}`);
-          return null;
-      }
+  public async status(): Promise<DebugStatus | null> {
+    try {
+      return (await this.sendCommand("status")) as DebugStatus;
+    } catch (e) {
+      this.out.error(`Could not query for status: ${e}`);
+      return null;
+    }
   }
 
-  public async inspect (tid:string) : Promise<ThreadInspection | null> {
-      try {
-          return await this.sendCommand('inspect', [tid]) as ThreadInspection;
-      } catch (e) {
-          this.out.error(`Could not inspect thread ${tid}: ${e}`);
-          return null;
-      }
+  public async inspect(tid: number): Promise<ThreadInspection | null> {
+    try {
+      return (await this.sendCommand("inspect", [
+        String(tid),
+      ])) as ThreadInspection;
+    } catch (e) {
+      this.out.error(`Could not inspect thread ${tid}: ${e}`);
+      return null;
+    }
   }
 
-  public async setBreakpoint (breakpoint: string) {
-      try {
-          await this.sendCommand(`break ${breakpoint}`) as DebugStatus;
-      } catch (e) {
-          this.out.error(`Could not set breakpoint ${breakpoint}: ${e}`);
-      }
+  public async setBreakpoint(breakpoint: string) {
+    try {
+      (await this.sendCommand(`break ${breakpoint}`)) as DebugStatus;
+    } catch (e) {
+      this.out.error(`Could not set breakpoint ${breakpoint}: ${e}`);
+    }
   }
 
-  public async clearBreakpoints (source: string) {
-      try {
-          await this.sendCommand('rmbreak', [source]) as DebugStatus;
-      } catch (e) {
-          this.out.error(`Could not remove breakpoints for ${source}: ${e}`);
-      }
+  public async clearBreakpoints(source: string) {
+    try {
+      (await this.sendCommand("rmbreak", [source])) as DebugStatus;
+    } catch (e) {
+      this.out.error(`Could not remove breakpoints for ${source}: ${e}`);
+    }
   }
 
-  public async shutdown () {
-      this.connected = false;
-      await this.socket.destroy();
+  public async shutdown() {
+    this.connected = false;
+    await this.socket.destroy();
   }
 
   /**
    * PollEvents is the polling loop for debug events.
    */
-  private async pollEvents () {
-      let nextLoop = 1000;
-      try {
-          const status = await this.status();
-
-          for (const [tid, thread] of Object.entries(status?.threads || [])) {
-              if (thread.threadRunning === false && !this.threadInspection[tid]) {
+  private async pollEvents() {
+    let nextLoop = 1000;
+    try {
+      const status = await this.status();
 
-                console.log("#### Thread was stopped!!")
+      this.emit("status", status);
 
-                // A thread was stopped inspect it
+      for (const [tidString, thread] of Object.entries(status?.threads || [])) {
+        const tid = parseInt(tidString);
 
-                let inspection : ThreadInspection = {
-                    callstack: [],
-                    threadRunning: false
-                }
+        if (thread.threadRunning === false && !this.threadInspection[tid]) {
+          console.log("#### Thread was stopped!!");
 
-                try {
-                    inspection = await this.sendCommand('describe', [tid]) as ThreadInspection;
-                } catch (e) {
-                    this.out.error(`Could not get description for ${tid}: ${e}`);
-                }
+          // A thread was stopped inspect it
 
-                this.threadInspection[tid] = inspection;
+          let inspection: ThreadInspection = {
+            callstack: [],
+            threadRunning: false,
+          };
 
-                console.log("#### Description result:", inspection)
-
-                this.emit('pauseOnBreakpoint', tid);
-            }
+          try {
+            inspection = (await this.sendCommand("describe", [
+              String(tid),
+            ])) as ThreadInspection;
+          } catch (e) {
+            this.out.error(`Could not get description for ${tid}: ${e}`);
           }
 
-      } catch (e) {
-          this.out.error(`Error during event loop: ${e}`);
-          nextLoop = 5000;
-      }
+          this.threadInspection[tid] = inspection;
 
-      if (this.connected) {
-          setTimeout(this.pollEvents.bind(this), nextLoop);
-      } else {
-          this.out.log('Stop emitting events' + nextLoop);
+          this.emit("pauseOnBreakpoint", { tid, inspection });
+        }
       }
+    } catch (e) {
+      this.out.error(`Error during event loop: ${e}`);
+      nextLoop = 5000;
+    }
+
+    if (this.connected) {
+      setTimeout(this.pollEvents.bind(this), nextLoop);
+    } else {
+      this.out.log("Stop emitting events" + nextLoop);
+    }
   }
 
-  public async sendCommand (cmd:string, args? :string[]): Promise<any> {
-      // Create or process the backlog depending on the connection status
-
-      if (!this.connected) {
-          this.backlog.push({
-              cmd,
-              args
-          });
-          return null;
-      } else if (this.backlog.length > 0) {
-          const backlog = this.backlog;
-          this.backlog = [];
-          for (const item of backlog) {
-              await this.sendCommand(item.cmd, item.args);
-          }
+  public async sendCommand(cmd: string, args?: string[]): Promise<any> {
+    // Create or process the backlog depending on the connection status
+
+    if (!this.connected) {
+      this.backlog.push({
+        cmd,
+        args,
+      });
+      return null;
+    } else if (this.backlog.length > 0) {
+      const backlog = this.backlog;
+      this.backlog = [];
+      for (const item of backlog) {
+        await this.sendCommand(item.cmd, item.args);
       }
+    }
 
-      return await this.sendCommandString(`##${cmd} ${args ? args.join(' ') : ''}\r\n`);
+    return await this.sendCommandString(
+      `##${cmd} ${args ? args.join(" ") : ""}\r\n`
+    );
   }
 
-  public async sendCommandString (cmdString:string): Promise<any> {
-      // Socket needs to be locked. Reading and writing to the socket is seen
-      // by the interpreter as async (i/o bound) code. Separate calls to
-      // sendCommand will be executed in different event loops. Without the lock
-      // the different sendCommand calls would mix their responses.
+  public async sendCommandString(cmdString: string): Promise<any> {
+    // Socket needs to be locked. Reading and writing to the socket is seen
+    // by the interpreter as async (i/o bound) code. Separate calls to
+    // sendCommand will be executed in different event loops. Without the lock
+    // the different sendCommand calls would mix their responses.
 
-      return await this.socketLock.acquire('socket', async () => {
-          await this.socket.write(cmdString, 'utf8');
+    return await this.socketLock.acquire("socket", async () => {
+      await this.socket.write(cmdString, "utf8");
 
-          let text = '';
-          while (!text.endsWith('\n\n')) {
-              text += await this.socket.read(1);
-          }
+      let text = "";
+      while (!text.endsWith("\n\n")) {
+        text += await this.socket.read(1);
+      }
 
-          let res : any = {};
-          try {
-              res = JSON.parse(text);
-          } catch (e) {
-              throw (new Error(`Could not parse response: ${text} - error:${e}`));
-          }
-          if (res?.DebuggerError) {
-              throw (new Error(`Unexpected internal error for command "${cmdString}": ${res.DebuggerError}`));
-          }
-          if (res?.EncodedOutput !== undefined) {
-              res = Buffer.from(res.EncodedOutput, 'base64').toString('utf8');
-          }
-          return res;
-      });
+      let res: any = {};
+      try {
+        res = JSON.parse(text);
+      } catch (e) {
+        throw new Error(`Could not parse response: ${text} - error:${e}`);
+      }
+      if (res?.DebuggerError) {
+        throw new Error(
+          `Unexpected internal error for command "${cmdString}": ${res.DebuggerError}`
+        );
+      }
+      if (res?.EncodedOutput !== undefined) {
+        res = Buffer.from(res.EncodedOutput, "base64").toString("utf8");
+      }
+      return res;
+    });
   }
 }

+ 20 - 14
ecal-support/src/extension.ts

@@ -1,19 +1,25 @@
-import * as vscode from 'vscode';
-import { ProviderResult } from 'vscode';
-import { ECALDebugSession } from './ecalDebugAdapter';
+import * as vscode from "vscode";
+import { ProviderResult } from "vscode";
+import { ECALDebugSession } from "./ecalDebugAdapter";
 
-export function activate (context: vscode.ExtensionContext) {
-    context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory(
-        'ecaldebug', new InlineDebugAdapterFactory()));
+export function activate(context: vscode.ExtensionContext) {
+  context.subscriptions.push(
+    vscode.debug.registerDebugAdapterDescriptorFactory(
+      "ecaldebug",
+      new InlineDebugAdapterFactory()
+    )
+  );
 }
 
-export function deactivate () {
-}
+export function deactivate() {}
 
-class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory {
-    createDebugAdapterDescriptor (_session: vscode.DebugSession): ProviderResult<vscode.DebugAdapterDescriptor> {
-        // Declare the ECALDebugSession as an DebugAdapterInlineImplementation so extention and adapter can
-        // run in-process (making it possible to easily debug the adapter)
-        return new vscode.DebugAdapterInlineImplementation(new ECALDebugSession());
-    }
+class InlineDebugAdapterFactory
+  implements vscode.DebugAdapterDescriptorFactory {
+  createDebugAdapterDescriptor(
+    _session: vscode.DebugSession
+  ): ProviderResult<vscode.DebugAdapterDescriptor> {
+    // Declare the ECALDebugSession as an DebugAdapterInlineImplementation so extention and adapter can
+    // run in-process (making it possible to easily debug the adapter)
+    return new vscode.DebugAdapterInlineImplementation(new ECALDebugSession());
+  }
 }

+ 31 - 10
ecal-support/src/types.ts

@@ -1,24 +1,45 @@
+export interface EcalAstNode {
+  allowescapes: boolean;
+  children: EcalAstNode[];
+  id: number;
+  identifier: boolean;
+  line: number;
+  linepos: number;
+  name: string;
+  pos: number;
+  source: string;
+  value: any;
+}
+
 export interface ThreadInspection {
-    callstack: string[]
-    threadRunning: boolean
+  callstack: string[];
+  threadRunning: boolean;
+  code?: string;
+  node?: EcalAstNode;
+  vs?: any;
+}
+
+export interface ClientBreakEvent {
+  tid: number;
+  inspection: ThreadInspection;
 }
 
 export interface ThreadStatus {
-    callstack: string[]
-    threadRunning?: boolean
+  callstack: string[];
+  threadRunning?: boolean;
 }
 
 export interface DebugStatus {
-    breakonstart: boolean,
-    breakpoints: any,
-    sources: string[],
-    threads: Record<number, ThreadStatus>
+  breakonstart: boolean;
+  breakpoints: Record<string, boolean>;
+  sources: string[];
+  threads: Record<number, ThreadStatus>;
 }
 
 /**
  * Log output stream for this client.
  */
 export interface LogOutputStream {
-    log(value: string): void;
-    error(value: string): void;
+  log(value: string): void;
+  error(value: string): void;
 }

+ 137 - 112
ecal-support/syntaxes/ecal.tmLanguage.json

@@ -1,113 +1,138 @@
 {
-	"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
-	"name": "Event Condition Action Language",
-	"patterns": [
-		{
-			"include": "#keywords"
-		}, {
-			"include": "#identifiers"
-		}, {
-			"include": "#strings"
-		}, {
-			"include": "#comments"
-		}
-	],
-	"repository": {
-		"keywords": {
-			"patterns": [{
-				"name": "keyword.control.import.ecal",
-				"match": "\\b(import|as)\\b"
-			}, {
-				"name": "keyword.control.sink.ecal",
-				"match": "\\b(sink|kindmatch|scopematch|statematch|priority|suppresses)\\b"
-			}, {
-				"name": "keyword.control.function.ecal",
-				"match": "\\b(func|return)\\b"
-			}, {
-				"name": "keyword.operator.boolean.ecal",
-				"match": "\\b(and|or|not)\\b"
-			}, {
-				"name": "keyword.operator.string.ecal",
-				"match": "\\b(like|hasprefix|hassuffic)\\b"
-			}, {
-				"name": "keyword.operator.list.ecal",
-				"match": "\\b(in|notin)\\b"
-			}, {
-				"name": "constant.language.terminals.ecal",
-				"match": "\\b(false|true|null)\\b"
-			}, {
-				"name": "keyword.control.conditional.ecal",
-				"match": "\\b(if|elif|else)\\b"
-			}, {
-				"name": "keyword.control.loop.ecal",
-				"match": "\\b(for|break|continue)\\b"
-			}, {
-				"name": "keyword.control.try.ecal",
-				"match": "\\b(try|except|finally)\\b"
-			}]
-		},
-		"identifiers": {
-			"patterns": [{
-				"match": "([a-zA-Z]+)\\(",
-				"captures": {
-					"1": {
-						"name": "entity.name.function.ecal"
-					}
-				}
-			}, {
-				"match": "([a-zA-Z]+) :?=",
-				"captures": {
-					"1": {
-						"name": "storage.type.var.ecal"
-					}
-				}
-			}]
-		},
-		"strings": {
-			"patterns": [{
-				"name": "string.quoted.double.ecal",
-				"begin": "r?\"",
-				"end": "\"",
-				"patterns": [{
-					"include" : "#escapes"
-				}]
-			}, {
-				"name": "string.quoted.single.ecal",
-				"begin": "r?'",
-				"end": "'",
-				"patterns": [{
-					"include" : "#escapes"
-				}]
-			}],
-			"repository" : {
-				"escapes" : {
-					"patterns": [
-						{
-							"name": "constant.character.escape.ecal",
-							"match": "\\\\."
-						}, {
-							"name": "constant.character.escape.ecal",
-							"begin": "{{",
-							"end": "}}"
-						}
-					]
-				}
-			}
-		},
-		"comments": {
-			"patterns": [
-				{
-					"name": "comment.block.ecal",
-					"begin": "/\\*",
-					"end": "\\*/"
-				},
-				{
-					"name": "comment.line.ecal",
-					"begin": "#",
-					"end": "\\n"
-				}
-			]
-		}
-	},
-	"scopeName": "source.ecal"
-}
+  "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
+  "name": "Event Condition Action Language",
+  "patterns": [
+    {
+      "include": "#keywords"
+    },
+    {
+      "include": "#identifiers"
+    },
+    {
+      "include": "#strings"
+    },
+    {
+      "include": "#comments"
+    }
+  ],
+  "repository": {
+    "keywords": {
+      "patterns": [
+        {
+          "name": "keyword.control.import.ecal",
+          "match": "\\b(import|as)\\b"
+        },
+        {
+          "name": "keyword.control.sink.ecal",
+          "match": "\\b(sink|kindmatch|scopematch|statematch|priority|suppresses)\\b"
+        },
+        {
+          "name": "keyword.control.function.ecal",
+          "match": "\\b(func|return)\\b"
+        },
+        {
+          "name": "keyword.operator.boolean.ecal",
+          "match": "\\b(and|or|not)\\b"
+        },
+        {
+          "name": "keyword.operator.string.ecal",
+          "match": "\\b(like|hasprefix|hassuffic)\\b"
+        },
+        {
+          "name": "keyword.operator.list.ecal",
+          "match": "\\b(in|notin)\\b"
+        },
+        {
+          "name": "constant.language.terminals.ecal",
+          "match": "\\b(false|true|null)\\b"
+        },
+        {
+          "name": "keyword.control.conditional.ecal",
+          "match": "\\b(if|elif|else)\\b"
+        },
+        {
+          "name": "keyword.control.loop.ecal",
+          "match": "\\b(for|break|continue)\\b"
+        },
+        {
+          "name": "keyword.control.try.ecal",
+          "match": "\\b(try|except|finally)\\b"
+        }
+      ]
+    },
+    "identifiers": {
+      "patterns": [
+        {
+          "match": "([a-zA-Z]+)\\(",
+          "captures": {
+            "1": {
+              "name": "entity.name.function.ecal"
+            }
+          }
+        },
+        {
+          "match": "([a-zA-Z]+) :?=",
+          "captures": {
+            "1": {
+              "name": "storage.type.var.ecal"
+            }
+          }
+        }
+      ]
+    },
+    "strings": {
+      "patterns": [
+        {
+          "name": "string.quoted.double.ecal",
+          "begin": "r?\"",
+          "end": "\"",
+          "patterns": [
+            {
+              "include": "#escapes"
+            }
+          ]
+        },
+        {
+          "name": "string.quoted.single.ecal",
+          "begin": "r?'",
+          "end": "'",
+          "patterns": [
+            {
+              "include": "#escapes"
+            }
+          ]
+        }
+      ],
+      "repository": {
+        "escapes": {
+          "patterns": [
+            {
+              "name": "constant.character.escape.ecal",
+              "match": "\\\\."
+            },
+            {
+              "name": "constant.character.escape.ecal",
+              "begin": "{{",
+              "end": "}}"
+            }
+          ]
+        }
+      }
+    },
+    "comments": {
+      "patterns": [
+        {
+          "name": "comment.block.ecal",
+          "begin": "/\\*",
+          "end": "\\*/"
+        },
+        {
+          "name": "comment.line.ecal",
+          "begin": "#",
+          "end": "\\n"
+        }
+      ]
+    }
+  },
+  "scopeName": "source.ecal"
+}

+ 20 - 24
ecal-support/tsconfig.json

@@ -1,26 +1,22 @@
 {
-	"compilerOptions": {
-		"module": "commonjs",
-		"target": "es6",
-		"outDir": "out",
-		"lib": [
-			"es6"
-		],
-		"sourceMap": true,
-		"rootDir": "src",
-		"strict": true,
-		"noImplicitReturns": true,
-		"noFallthroughCasesInSwitch": true,
-		"noUnusedParameters": true,
-		"noImplicitAny": true,
-		"removeComments": true,
-		"noUnusedLocals": true,
-		"noImplicitThis": true,
-		"inlineSourceMap": false,
-		"preserveConstEnums": true,
-		"strictNullChecks": true
-	},
-	"exclude": [
-		"node_modules", "tmp"
-	]
+  "compilerOptions": {
+    "module": "commonjs",
+    "target": "es6",
+    "outDir": "out",
+    "lib": ["es6"],
+    "sourceMap": true,
+    "rootDir": "src",
+    "strict": true,
+    "noImplicitReturns": true,
+    "noFallthroughCasesInSwitch": true,
+    "noUnusedParameters": true,
+    "noImplicitAny": true,
+    "removeComments": true,
+    "noUnusedLocals": true,
+    "noImplicitThis": true,
+    "inlineSourceMap": false,
+    "preserveConstEnums": true,
+    "strictNullChecks": true
+  },
+  "exclude": ["node_modules", "tmp"]
 }