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)
+ }
})
)