This is an automated email from the ASF dual-hosted git repository.

hdalsania pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/daffodil-vscode.git


The following commit(s) were added to refs/heads/main by this push:
     new f512836b Implement step over and step out
f512836b is described below

commit f512836b13d378f3dd46f9f4d839cfc7fa3f3acd
Author: Shane Dell <[email protected]>
AuthorDate: Tue Sep 23 15:17:33 2025 -0400

    Implement step over and step out
    
    - Implemented step over and step out logic.
    - Step in is currently doing the proper thing, fixed in a different PR.
    
    Closes #5
    Closes #1089
---
 .vscode/settings.json                              |   2 +-
 build/yarn-scripts.ts                              |  38 +++++
 .../org.apache.daffodil.debugger.dap/Parse.scala   | 175 ++++++++++++++++-----
 package.json                                       |   1 +
 src/infoset.ts                                     |  17 ++
 5 files changed, 189 insertions(+), 44 deletions(-)

diff --git a/.vscode/settings.json b/.vscode/settings.json
index e8e7ff2b..daa47889 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -27,7 +27,7 @@
     // Turn off tsc task auto detection since we have the necessary tasks as 
npm scripts
     "typescript.tsc.autoDetect": "off",
     "files.watcherExclude": {
-        "**/target": true
+        "**/target/**": true
     },
     "[css]": {
         "editor.defaultFormatter": "esbenp.prettier-vscode",
diff --git a/build/yarn-scripts.ts b/build/yarn-scripts.ts
index 1acaf664..5a3d8093 100644
--- a/build/yarn-scripts.ts
+++ b/build/yarn-scripts.ts
@@ -22,6 +22,12 @@ const fs = require('fs')
 const { glob } = require('glob')
 const concurrently = require('concurrently')
 const child_process = require('child_process')
+const jsoncParse = require('jsonc-parser').parse
+
+const packageData = jsoncParse(
+  fs.readFileSync(path.resolve('package.json'), 'utf8')
+)
+const pkg_version = packageData['version']
 
 function rmFileOrDirectory(path) {
   if (fs.existsSync(path)) fs.rmSync(path, { recursive: true })
@@ -325,6 +331,37 @@ function packageVsix() {
   process.exit(result.status === null ? 1 : result.status)
 }
 
+function getScalaVersions() {
+  const scalaVersions = ['2.12', '2.13']
+
+  // The scala 3 version of the debugger should only exist if JDK >= 17 is 
being used
+  if (fs.existsSync(`debugger/target/jvm-3/universal/stage`)) {
+    scalaVersions.push('3')
+  }
+
+  return scalaVersions
+}
+
+function moveDebuggers() {
+  getScalaVersions().forEach(async (scalaVersion) => {
+    const serverPackage = `daffodil-debugger-${scalaVersion}-${pkg_version}`
+    const jvmFolderName = `jvm-${scalaVersion}`
+    const stageFilePath = path.resolve(
+      `debugger/target/${jvmFolderName}/universal/stage`
+    )
+
+    const serverPackageFolder = path.join('dist/debuggers', serverPackage)
+
+    // remove debugger package folder if exists
+    if (fs.existsSync(serverPackageFolder)) {
+      fs.rmSync(serverPackageFolder, { recursive: true, force: true })
+    }
+
+    // Copy staged debugger files to desired location
+    fs.cpSync(stageFilePath, serverPackageFolder, { recursive: true })
+  })
+}
+
 /* START SECTION: Update version */
 // helper function to get the version passed in
 function parseArgs() {
@@ -487,4 +524,5 @@ module.exports = {
   packageVsix: packageVsix,
   checkMissingLicenseData: checkMissingLicenseData,
   checkLicenseCompatibility: checkLicenseCompatibility,
+  moveDebuggers: moveDebuggers,
 }
diff --git 
a/debugger/src/main/scala/org.apache.daffodil.debugger.dap/Parse.scala 
b/debugger/src/main/scala/org.apache.daffodil.debugger.dap/Parse.scala
index 9bf80a0f..f295edb6 100644
--- a/debugger/src/main/scala/org.apache.daffodil.debugger.dap/Parse.scala
+++ b/debugger/src/main/scala/org.apache.daffodil.debugger.dap/Parse.scala
@@ -818,8 +818,13 @@ object Parse {
                 )
               _ <- data.send(newState.data)
             } yield newState
-          case (state, Parse.Event.EndElement(_)) => IO.pure(state.copy(data = 
state.data.pop()))
-          case (state, _: Parse.Event.Fini.type)  => IO.pure(state)
+          case (state, e: Parse.Event.EndElement) =>
+            val newState = state.copy(
+              data = state.data.pop(),
+              infoset = e.infoset // Update the state with the new infoset
+            )
+            data.send(newState.data).as(newState)
+          case (state, _: Parse.Event.Fini.type)                             
=> IO.pure(state)
           case (state, Event.Control(DAPodil.Debugee.State.Stopped(reason))) =>
             val events =
               List(
@@ -1092,7 +1097,7 @@ object Parse {
           infoset
         )
     }
-    case class EndElement(state: StateForDebugger) extends Event
+    case class EndElement(state: StateForDebugger, infoset: 
Option[InfosetEvent]) extends Event
     case object Fini extends Event
     case class Control(state: DAPodil.Debugee.State) extends Event
     case class Error(message: String) extends 
Events.DebugEvent("daffodil.parseError")
@@ -1256,7 +1261,18 @@ object Parse {
     /** Blocks if the current state is stopped, unblocking when a `step` or 
`continue` happens. Returns true if blocking
       * happened, false otherwise.
       */
-    def await(): IO[Boolean]
+    // def await(): IO[Boolean]
+    def await(onStop: IO[Unit] = IO.unit): IO[Boolean] // Add onStop here
+
+    /** Update the control with the current element depth (set by the parser 
thread before awaiting). Depth is used to
+      * implement step/stepOut semantics.
+      */
+    def setCurrentDepth(depth: Int): IO[Unit]
+
+    /** Indicate the kind of the upcoming await (e.g. "start" or "end"). 
Parser hooks should set this before calling
+      * await().
+      */
+    def setAwaitingKind(kind: String): IO[Unit]
 
     /** Start running. */
     def continue(): IO[Unit]
@@ -1294,26 +1310,61 @@ object Parse {
       for {
         waiterArrived <- Deferred[IO, Unit]
         state <- Ref[IO].of[State](AwaitingFirstAwait(waiterArrived))
+        currentDepth <- Ref[IO].of[Int](0)
+        awaitingKind <- Ref[IO].of[String]("")
+        stopTarget <- Ref[IO].of[Option[(Int, StepType)]](None)
       } yield new Control {
-        def await(): IO[Boolean] =
+        def await(onStop: IO[Unit] = IO.unit): IO[Boolean] =
           for {
             nextContinue <- Deferred[IO, Unit]
             nextAwaitStarted <- Deferred[IO, Unit]
             awaited <- state.modify {
               case AwaitingFirstAwait(waiterArrived) =>
-                Stopped(nextContinue, nextAwaitStarted) -> waiterArrived
-                  .complete(()) *> nextContinue.get.as(true)
+                // Stopped(nextContinue, nextAwaitStarted) -> waiterArrived
+                //   .complete(()) *> nextContinue.get.as(true)
+                Stopped(nextContinue, nextAwaitStarted) -> (waiterArrived
+                  .complete(()) *> onStop *> 
nextAwaitStarted.complete(()).void *> nextContinue.get.as(true))
               case Running                                      => Running -> 
IO.pure(false)
               case s @ Stopped(whenContinued, nextAwaitStarted) =>
-                s -> nextAwaitStarted.complete(()) *> // signal next await 
happened
-                  whenContinued.get.as(true) // block
+                s -> stopTarget.get.flatMap {
+                  case None =>
+                    // No step target, pause immediately and signal that we've 
stopped
+                    onStop *> nextAwaitStarted.complete(()).void *> 
whenContinued.get.as(true)
+                  case Some((targetDepth, mode)) =>
+                    for {
+                      cur <- currentDepth.get
+                      kind <- awaitingKind.get
+                      res <- mode match {
+                        // stepIn: Stop at the very next 'start' event, 
completely ignoring 'end' events
+                        case StepType.StepIn =>
+                          if (kind == "start")
+                            stopTarget.set(None) *> onStop *> 
nextAwaitStarted.complete(()).void *> whenContinued.get
+                              .as(true)
+                          else IO.pure(false)
+
+                        // stepOver / stepOut: Run invisibly until we hit a 
'start' event at the target depth or shallower
+                        case StepType.StepOver | StepType.StepOut =>
+                          if (kind == "start" && cur <= targetDepth)
+                            stopTarget.set(None) *> onStop *> 
nextAwaitStarted.complete(()).void *> whenContinued.get
+                              .as(true)
+                          else
+                            IO.pure(false) // Ignore ALL 'end' events and any 
'start' events deeper than our target
+
+                        // Block once and clear the stopTarget so subsequent 
awaits don't re-trigger
+                        case null =>
+                          stopTarget.set(None) *> onStop *> 
nextAwaitStarted.complete(()).void *> whenContinued.get.as(
+                            true
+                          )
+                      }
+                    } yield res
+                }
             }.flatten
           } yield awaited
 
-        def performStep(stepType: StepType): IO[Unit] =
+        def performStep(stepType: StepType, addedDepth: Int): IO[Unit] =
           for {
             nextContinue <- Deferred[IO, Unit]
-            nextAwaitStarted <- Deferred[IO, Unit]
+            newAwaitStarted <- Deferred[IO, Unit]
             _ <- state.modify {
               case s @ AwaitingFirstAwait(waiterArrived) =>
                 s -> waiterArrived.get *> (stepType match {
@@ -1323,35 +1374,46 @@ object Parse {
                 })
               case Running                   => Running -> IO.unit
               case Stopped(whenContinued, _) =>
-                Stopped(nextContinue, nextAwaitStarted) -> (
-                  whenContinued.complete(()) *> // wake up await-ers
-                    nextAwaitStarted.get // block until next await is invoked
+                Stopped(nextContinue, newAwaitStarted) -> (
+                  for {
+                    d <- currentDepth.get
+                    _ <- stopTarget.set(Some((d + addedDepth, stepType)))
+                    _ <- whenContinued.complete(()) // Unblock the parser to 
continue running invisibly
+                    _ <- newAwaitStarted.get // WAIT here until the parser 
hits the target 'start' event and stops
+                  } yield ()
                 )
             }.flatten
           } yield ()
 
-        def stepOver(): IO[Unit] = performStep(StepType.StepOver)
-        def stepIn(): IO[Unit] = performStep(StepType.StepIn)
-        def stepOut(): IO[Unit] = performStep(StepType.StepOut)
+        def stepOver(): IO[Unit] = performStep(StepType.StepOver, 0)
+        def stepIn(): IO[Unit] = performStep(StepType.StepIn, 1)
+        def stepOut(): IO[Unit] = performStep(StepType.StepOut, -1)
+
+        // Helper functions for stepping
+        def setCurrentDepth(depth: Int): IO[Unit] = currentDepth.set(depth)
+        def setAwaitingKind(kind: String): IO[Unit] = awaitingKind.set(kind)
 
         def continue(): IO[Unit] =
           state.modify {
             case s @ AwaitingFirstAwait(waiterArrived) =>
               s -> waiterArrived.get *> continue()
-            case Running                   => Running -> IO.unit
-            case Stopped(whenContinued, _) =>
-              Running -> whenContinued.complete(()).void // wake up await-ers
+            case Running                                  => Running -> IO.unit
+            case Stopped(whenContinued, nextAwaitStarted) =>
+              Running -> (stopTarget.set(None) *> 
nextAwaitStarted.complete(()).void *> whenContinued.complete(())).void
           }.flatten
 
         def pause(): IO[Unit] =
           for {
             nextContinue <- Deferred[IO, Unit]
-            nextAwaitStarted <- Deferred[IO, Unit]
-            _ <- state.update {
-              case Running               => Stopped(nextContinue, 
nextAwaitStarted)
-              case s: AwaitingFirstAwait => s
-              case s: Stopped            => s
-            }
+            newAwaitStarted <- Deferred[IO, Unit]
+            _ <- state.modify {
+              case Running =>
+                Stopped(nextContinue, newAwaitStarted) -> IO.unit
+              case s: AwaitingFirstAwait            => s -> IO.unit
+              case s @ Stopped(_, nextAwaitStarted) =>
+                // If we are hit by a breakpoint during a step, clear target 
and unblock performStep
+                s -> (stopTarget.set(None) *> 
nextAwaitStarted.complete(()).void)
+            }.flatten
           } yield ()
       }
   }
@@ -1371,6 +1433,34 @@ object Parse {
   ) extends Debugger {
     implicit val logger: Logger[IO] = Slf4jLogger.getLogger
 
+    // Helper function to get the infoset
+    private def getFullInfoset(pstate: PState): Option[InfosetEvent] = {
+      var node = pstate.infoset
+      while (node.diParent != null) node = node.diParent
+      node match {
+        case d: DIDocument if d.numChildren == 0 => None
+        case _                                   => 
Some(InfosetEvent(infosetFormat, node))
+      }
+    }
+
+    // Helper function to calculate the current depth
+    private def calculateDepth(pstate: PState): Int =
+      Convert
+        .daffodilMaybeToOption(pstate.currentNode)
+        .map { node =>
+          var d = 0
+          var n = node
+          while (n.diParent != null) {
+            n = n.diParent
+            // Only count actual elements and document root, ignore hidden 
DIArray wrappers
+            if (n.isInstanceOf[DIElement] || n.isInstanceOf[DIDocument]) {
+              d += 1
+            }
+          }
+          d
+        }
+        .getOrElse(0)
+
     override def init(pstate: PState, processor: Parser): Unit =
       dispatcher.unsafeRunSync {
         events.send(Event.Init(pstate.copyStateForDebugger)).void
@@ -1385,26 +1475,17 @@ object Parse {
 
     override def startElement(pstate: PState, processor: Parser): Unit =
       dispatcher.unsafeRunSync {
-        // Generating the infoset requires a PState, not a StateForDebugger, 
so we can't generate it later from the Event.StartElement (which contains the 
StateForDebugger).
-        lazy val infoset = {
-          var node = pstate.infoset
-          while (node.diParent != null) node = node.diParent
-          node match {
-            case d: DIDocument if d.numChildren == 0 => None
-            case _                                   => 
Some(InfosetEvent(infosetFormat, node))
-          }
-        }
-
         for {
           _ <- logger.debug("pre-control await")
-          isStepping <- control.await() // may block until external control 
says to unblock, for stepping behavior
+          _ <- control.setCurrentDepth(calculateDepth(pstate))
+          _ <- control.setAwaitingKind("start")
+          isStepping <- control.await(
+            onStop = events.send(new Event.StartElement(pstate, 
getFullInfoset(pstate))).void
+          )
           _ <- logger.debug("post-control await")
           location = createLocation(pstate.schemaFileLocation)
           shouldBreak <- breakpoints.shouldBreak(location)
-          startElement =
-            if (isStepping || shouldBreak) new Event.StartElement(pstate, 
infoset)
-            else new Event.StartElement(pstate, None)
-          _ <- events.send(startElement)
+          _ <- events.send(new Event.StartElement(pstate, 
None)).unlessA(isStepping)
           _ <- onBreakpointHit(location).whenA(shouldBreak)
         } yield ()
       }
@@ -1430,8 +1511,16 @@ object Parse {
 
     override def endElement(pstate: PState, processor: Parser): Unit =
       dispatcher.unsafeRunSync {
-        control.await() *> // ensure no events while debugger is paused
-          events.send(Event.EndElement(pstate.copyStateForDebugger)).void
+        for {
+          _ <- logger.debug("pre-control await")
+          _ <- control.setCurrentDepth(calculateDepth(pstate))
+          _ <- control.setAwaitingKind("end")
+          isStepping <- control.await(
+            onStop = events.send(new Event.EndElement(pstate, 
getFullInfoset(pstate))).void
+          )
+          _ <- logger.debug("post-control await")
+          _ <- events.send(new Event.EndElement(pstate, 
None)).unlessA(isStepping)
+        } yield ()
       }
   }
 
diff --git a/package.json b/package.json
index 33962680..bf8fdc67 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
     "test": "sbt test && yarn test:svelte && node 
./out/tests/omegaEditServerLifecycle.js && node ./out/tests/runTest.js",
     "test:svelte": "mocha --import=tsx ./src/svelte/tests/**/*.test.ts",
     "sbt": "sbt Universal/stage",
+    "move-debuggers": "run-func build/yarn-scripts.ts moveDebuggers",
     "svelte:check": "svelte-check --tsconfig ./src/svelte/tsconfig.json",
     "svelte:build": "cd src/svelte && vite build --config ./vite.config.mjs 
--mode production --emptyOutDir",
     "update-version": "run-func build/yarn-scripts.ts updateVersion",
diff --git a/src/infoset.ts b/src/infoset.ts
index 50afc90f..dbeb59f3 100644
--- a/src/infoset.ts
+++ b/src/infoset.ts
@@ -82,6 +82,23 @@ export async function activate(ctx: vscode.ExtensionContext) 
{
       }
       sid = undefined
       await openInfosetFilePrompt()
+
+      const tabs: vscode.Tab[] = vscode.window.tabGroups.all
+        .map((tg) => tg.tabs)
+        .flat()
+
+      // Find the tab that matches the file Uri
+      const foundTab = tabs.find(
+        (tab) =>
+          tab.input instanceof vscode.TabInputText &&
+          tab.input.uri.path === doc?.fileName
+      )
+
+      // If the tab is found, close it
+      if (foundTab) {
+        // The close method can take a single tab or an array of tabs
+        await vscode.window.tabGroups.close(foundTab)
+      }
     })
   )
 

Reply via email to