Skip to content

Commit f512836

Browse files
shanedellhdalsania
authored andcommitted
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
1 parent 6d65313 commit f512836

5 files changed

Lines changed: 189 additions & 44 deletions

File tree

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
2828
"typescript.tsc.autoDetect": "off",
2929
"files.watcherExclude": {
30-
"**/target": true
30+
"**/target/**": true
3131
},
3232
"[css]": {
3333
"editor.defaultFormatter": "esbenp.prettier-vscode",

build/yarn-scripts.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ const fs = require('fs')
2222
const { glob } = require('glob')
2323
const concurrently = require('concurrently')
2424
const child_process = require('child_process')
25+
const jsoncParse = require('jsonc-parser').parse
26+
27+
const packageData = jsoncParse(
28+
fs.readFileSync(path.resolve('package.json'), 'utf8')
29+
)
30+
const pkg_version = packageData['version']
2531

2632
function rmFileOrDirectory(path) {
2733
if (fs.existsSync(path)) fs.rmSync(path, { recursive: true })
@@ -325,6 +331,37 @@ function packageVsix() {
325331
process.exit(result.status === null ? 1 : result.status)
326332
}
327333

334+
function getScalaVersions() {
335+
const scalaVersions = ['2.12', '2.13']
336+
337+
// The scala 3 version of the debugger should only exist if JDK >= 17 is being used
338+
if (fs.existsSync(`debugger/target/jvm-3/universal/stage`)) {
339+
scalaVersions.push('3')
340+
}
341+
342+
return scalaVersions
343+
}
344+
345+
function moveDebuggers() {
346+
getScalaVersions().forEach(async (scalaVersion) => {
347+
const serverPackage = `daffodil-debugger-${scalaVersion}-${pkg_version}`
348+
const jvmFolderName = `jvm-${scalaVersion}`
349+
const stageFilePath = path.resolve(
350+
`debugger/target/${jvmFolderName}/universal/stage`
351+
)
352+
353+
const serverPackageFolder = path.join('dist/debuggers', serverPackage)
354+
355+
// remove debugger package folder if exists
356+
if (fs.existsSync(serverPackageFolder)) {
357+
fs.rmSync(serverPackageFolder, { recursive: true, force: true })
358+
}
359+
360+
// Copy staged debugger files to desired location
361+
fs.cpSync(stageFilePath, serverPackageFolder, { recursive: true })
362+
})
363+
}
364+
328365
/* START SECTION: Update version */
329366
// helper function to get the version passed in
330367
function parseArgs() {
@@ -487,4 +524,5 @@ module.exports = {
487524
packageVsix: packageVsix,
488525
checkMissingLicenseData: checkMissingLicenseData,
489526
checkLicenseCompatibility: checkLicenseCompatibility,
527+
moveDebuggers: moveDebuggers,
490528
}

debugger/src/main/scala/org.apache.daffodil.debugger.dap/Parse.scala

Lines changed: 132 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -818,8 +818,13 @@ object Parse {
818818
)
819819
_ <- data.send(newState.data)
820820
} yield newState
821-
case (state, Parse.Event.EndElement(_)) => IO.pure(state.copy(data = state.data.pop()))
822-
case (state, _: Parse.Event.Fini.type) => IO.pure(state)
821+
case (state, e: Parse.Event.EndElement) =>
822+
val newState = state.copy(
823+
data = state.data.pop(),
824+
infoset = e.infoset // Update the state with the new infoset
825+
)
826+
data.send(newState.data).as(newState)
827+
case (state, _: Parse.Event.Fini.type) => IO.pure(state)
823828
case (state, Event.Control(DAPodil.Debugee.State.Stopped(reason))) =>
824829
val events =
825830
List(
@@ -1092,7 +1097,7 @@ object Parse {
10921097
infoset
10931098
)
10941099
}
1095-
case class EndElement(state: StateForDebugger) extends Event
1100+
case class EndElement(state: StateForDebugger, infoset: Option[InfosetEvent]) extends Event
10961101
case object Fini extends Event
10971102
case class Control(state: DAPodil.Debugee.State) extends Event
10981103
case class Error(message: String) extends Events.DebugEvent("daffodil.parseError")
@@ -1256,7 +1261,18 @@ object Parse {
12561261
/** Blocks if the current state is stopped, unblocking when a `step` or `continue` happens. Returns true if blocking
12571262
* happened, false otherwise.
12581263
*/
1259-
def await(): IO[Boolean]
1264+
// def await(): IO[Boolean]
1265+
def await(onStop: IO[Unit] = IO.unit): IO[Boolean] // Add onStop here
1266+
1267+
/** Update the control with the current element depth (set by the parser thread before awaiting). Depth is used to
1268+
* implement step/stepOut semantics.
1269+
*/
1270+
def setCurrentDepth(depth: Int): IO[Unit]
1271+
1272+
/** Indicate the kind of the upcoming await (e.g. "start" or "end"). Parser hooks should set this before calling
1273+
* await().
1274+
*/
1275+
def setAwaitingKind(kind: String): IO[Unit]
12601276

12611277
/** Start running. */
12621278
def continue(): IO[Unit]
@@ -1294,26 +1310,61 @@ object Parse {
12941310
for {
12951311
waiterArrived <- Deferred[IO, Unit]
12961312
state <- Ref[IO].of[State](AwaitingFirstAwait(waiterArrived))
1313+
currentDepth <- Ref[IO].of[Int](0)
1314+
awaitingKind <- Ref[IO].of[String]("")
1315+
stopTarget <- Ref[IO].of[Option[(Int, StepType)]](None)
12971316
} yield new Control {
1298-
def await(): IO[Boolean] =
1317+
def await(onStop: IO[Unit] = IO.unit): IO[Boolean] =
12991318
for {
13001319
nextContinue <- Deferred[IO, Unit]
13011320
nextAwaitStarted <- Deferred[IO, Unit]
13021321
awaited <- state.modify {
13031322
case AwaitingFirstAwait(waiterArrived) =>
1304-
Stopped(nextContinue, nextAwaitStarted) -> waiterArrived
1305-
.complete(()) *> nextContinue.get.as(true)
1323+
// Stopped(nextContinue, nextAwaitStarted) -> waiterArrived
1324+
// .complete(()) *> nextContinue.get.as(true)
1325+
Stopped(nextContinue, nextAwaitStarted) -> (waiterArrived
1326+
.complete(()) *> onStop *> nextAwaitStarted.complete(()).void *> nextContinue.get.as(true))
13061327
case Running => Running -> IO.pure(false)
13071328
case s @ Stopped(whenContinued, nextAwaitStarted) =>
1308-
s -> nextAwaitStarted.complete(()) *> // signal next await happened
1309-
whenContinued.get.as(true) // block
1329+
s -> stopTarget.get.flatMap {
1330+
case None =>
1331+
// No step target, pause immediately and signal that we've stopped
1332+
onStop *> nextAwaitStarted.complete(()).void *> whenContinued.get.as(true)
1333+
case Some((targetDepth, mode)) =>
1334+
for {
1335+
cur <- currentDepth.get
1336+
kind <- awaitingKind.get
1337+
res <- mode match {
1338+
// stepIn: Stop at the very next 'start' event, completely ignoring 'end' events
1339+
case StepType.StepIn =>
1340+
if (kind == "start")
1341+
stopTarget.set(None) *> onStop *> nextAwaitStarted.complete(()).void *> whenContinued.get
1342+
.as(true)
1343+
else IO.pure(false)
1344+
1345+
// stepOver / stepOut: Run invisibly until we hit a 'start' event at the target depth or shallower
1346+
case StepType.StepOver | StepType.StepOut =>
1347+
if (kind == "start" && cur <= targetDepth)
1348+
stopTarget.set(None) *> onStop *> nextAwaitStarted.complete(()).void *> whenContinued.get
1349+
.as(true)
1350+
else
1351+
IO.pure(false) // Ignore ALL 'end' events and any 'start' events deeper than our target
1352+
1353+
// Block once and clear the stopTarget so subsequent awaits don't re-trigger
1354+
case null =>
1355+
stopTarget.set(None) *> onStop *> nextAwaitStarted.complete(()).void *> whenContinued.get.as(
1356+
true
1357+
)
1358+
}
1359+
} yield res
1360+
}
13101361
}.flatten
13111362
} yield awaited
13121363

1313-
def performStep(stepType: StepType): IO[Unit] =
1364+
def performStep(stepType: StepType, addedDepth: Int): IO[Unit] =
13141365
for {
13151366
nextContinue <- Deferred[IO, Unit]
1316-
nextAwaitStarted <- Deferred[IO, Unit]
1367+
newAwaitStarted <- Deferred[IO, Unit]
13171368
_ <- state.modify {
13181369
case s @ AwaitingFirstAwait(waiterArrived) =>
13191370
s -> waiterArrived.get *> (stepType match {
@@ -1323,35 +1374,46 @@ object Parse {
13231374
})
13241375
case Running => Running -> IO.unit
13251376
case Stopped(whenContinued, _) =>
1326-
Stopped(nextContinue, nextAwaitStarted) -> (
1327-
whenContinued.complete(()) *> // wake up await-ers
1328-
nextAwaitStarted.get // block until next await is invoked
1377+
Stopped(nextContinue, newAwaitStarted) -> (
1378+
for {
1379+
d <- currentDepth.get
1380+
_ <- stopTarget.set(Some((d + addedDepth, stepType)))
1381+
_ <- whenContinued.complete(()) // Unblock the parser to continue running invisibly
1382+
_ <- newAwaitStarted.get // WAIT here until the parser hits the target 'start' event and stops
1383+
} yield ()
13291384
)
13301385
}.flatten
13311386
} yield ()
13321387

1333-
def stepOver(): IO[Unit] = performStep(StepType.StepOver)
1334-
def stepIn(): IO[Unit] = performStep(StepType.StepIn)
1335-
def stepOut(): IO[Unit] = performStep(StepType.StepOut)
1388+
def stepOver(): IO[Unit] = performStep(StepType.StepOver, 0)
1389+
def stepIn(): IO[Unit] = performStep(StepType.StepIn, 1)
1390+
def stepOut(): IO[Unit] = performStep(StepType.StepOut, -1)
1391+
1392+
// Helper functions for stepping
1393+
def setCurrentDepth(depth: Int): IO[Unit] = currentDepth.set(depth)
1394+
def setAwaitingKind(kind: String): IO[Unit] = awaitingKind.set(kind)
13361395

13371396
def continue(): IO[Unit] =
13381397
state.modify {
13391398
case s @ AwaitingFirstAwait(waiterArrived) =>
13401399
s -> waiterArrived.get *> continue()
1341-
case Running => Running -> IO.unit
1342-
case Stopped(whenContinued, _) =>
1343-
Running -> whenContinued.complete(()).void // wake up await-ers
1400+
case Running => Running -> IO.unit
1401+
case Stopped(whenContinued, nextAwaitStarted) =>
1402+
Running -> (stopTarget.set(None) *> nextAwaitStarted.complete(()).void *> whenContinued.complete(())).void
13441403
}.flatten
13451404

13461405
def pause(): IO[Unit] =
13471406
for {
13481407
nextContinue <- Deferred[IO, Unit]
1349-
nextAwaitStarted <- Deferred[IO, Unit]
1350-
_ <- state.update {
1351-
case Running => Stopped(nextContinue, nextAwaitStarted)
1352-
case s: AwaitingFirstAwait => s
1353-
case s: Stopped => s
1354-
}
1408+
newAwaitStarted <- Deferred[IO, Unit]
1409+
_ <- state.modify {
1410+
case Running =>
1411+
Stopped(nextContinue, newAwaitStarted) -> IO.unit
1412+
case s: AwaitingFirstAwait => s -> IO.unit
1413+
case s @ Stopped(_, nextAwaitStarted) =>
1414+
// If we are hit by a breakpoint during a step, clear target and unblock performStep
1415+
s -> (stopTarget.set(None) *> nextAwaitStarted.complete(()).void)
1416+
}.flatten
13551417
} yield ()
13561418
}
13571419
}
@@ -1371,6 +1433,34 @@ object Parse {
13711433
) extends Debugger {
13721434
implicit val logger: Logger[IO] = Slf4jLogger.getLogger
13731435

1436+
// Helper function to get the infoset
1437+
private def getFullInfoset(pstate: PState): Option[InfosetEvent] = {
1438+
var node = pstate.infoset
1439+
while (node.diParent != null) node = node.diParent
1440+
node match {
1441+
case d: DIDocument if d.numChildren == 0 => None
1442+
case _ => Some(InfosetEvent(infosetFormat, node))
1443+
}
1444+
}
1445+
1446+
// Helper function to calculate the current depth
1447+
private def calculateDepth(pstate: PState): Int =
1448+
Convert
1449+
.daffodilMaybeToOption(pstate.currentNode)
1450+
.map { node =>
1451+
var d = 0
1452+
var n = node
1453+
while (n.diParent != null) {
1454+
n = n.diParent
1455+
// Only count actual elements and document root, ignore hidden DIArray wrappers
1456+
if (n.isInstanceOf[DIElement] || n.isInstanceOf[DIDocument]) {
1457+
d += 1
1458+
}
1459+
}
1460+
d
1461+
}
1462+
.getOrElse(0)
1463+
13741464
override def init(pstate: PState, processor: Parser): Unit =
13751465
dispatcher.unsafeRunSync {
13761466
events.send(Event.Init(pstate.copyStateForDebugger)).void
@@ -1385,26 +1475,17 @@ object Parse {
13851475

13861476
override def startElement(pstate: PState, processor: Parser): Unit =
13871477
dispatcher.unsafeRunSync {
1388-
// Generating the infoset requires a PState, not a StateForDebugger, so we can't generate it later from the Event.StartElement (which contains the StateForDebugger).
1389-
lazy val infoset = {
1390-
var node = pstate.infoset
1391-
while (node.diParent != null) node = node.diParent
1392-
node match {
1393-
case d: DIDocument if d.numChildren == 0 => None
1394-
case _ => Some(InfosetEvent(infosetFormat, node))
1395-
}
1396-
}
1397-
13981478
for {
13991479
_ <- logger.debug("pre-control await")
1400-
isStepping <- control.await() // may block until external control says to unblock, for stepping behavior
1480+
_ <- control.setCurrentDepth(calculateDepth(pstate))
1481+
_ <- control.setAwaitingKind("start")
1482+
isStepping <- control.await(
1483+
onStop = events.send(new Event.StartElement(pstate, getFullInfoset(pstate))).void
1484+
)
14011485
_ <- logger.debug("post-control await")
14021486
location = createLocation(pstate.schemaFileLocation)
14031487
shouldBreak <- breakpoints.shouldBreak(location)
1404-
startElement =
1405-
if (isStepping || shouldBreak) new Event.StartElement(pstate, infoset)
1406-
else new Event.StartElement(pstate, None)
1407-
_ <- events.send(startElement)
1488+
_ <- events.send(new Event.StartElement(pstate, None)).unlessA(isStepping)
14081489
_ <- onBreakpointHit(location).whenA(shouldBreak)
14091490
} yield ()
14101491
}
@@ -1430,8 +1511,16 @@ object Parse {
14301511

14311512
override def endElement(pstate: PState, processor: Parser): Unit =
14321513
dispatcher.unsafeRunSync {
1433-
control.await() *> // ensure no events while debugger is paused
1434-
events.send(Event.EndElement(pstate.copyStateForDebugger)).void
1514+
for {
1515+
_ <- logger.debug("pre-control await")
1516+
_ <- control.setCurrentDepth(calculateDepth(pstate))
1517+
_ <- control.setAwaitingKind("end")
1518+
isStepping <- control.await(
1519+
onStop = events.send(new Event.EndElement(pstate, getFullInfoset(pstate))).void
1520+
)
1521+
_ <- logger.debug("post-control await")
1522+
_ <- events.send(new Event.EndElement(pstate, None)).unlessA(isStepping)
1523+
} yield ()
14351524
}
14361525
}
14371526

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"test": "sbt test && yarn test:svelte && node ./out/tests/omegaEditServerLifecycle.js && node ./out/tests/runTest.js",
4848
"test:svelte": "mocha --import=tsx ./src/svelte/tests/**/*.test.ts",
4949
"sbt": "sbt Universal/stage",
50+
"move-debuggers": "run-func build/yarn-scripts.ts moveDebuggers",
5051
"svelte:check": "svelte-check --tsconfig ./src/svelte/tsconfig.json",
5152
"svelte:build": "cd src/svelte && vite build --config ./vite.config.mjs --mode production --emptyOutDir",
5253
"update-version": "run-func build/yarn-scripts.ts updateVersion",

src/infoset.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,23 @@ export async function activate(ctx: vscode.ExtensionContext) {
8282
}
8383
sid = undefined
8484
await openInfosetFilePrompt()
85+
86+
const tabs: vscode.Tab[] = vscode.window.tabGroups.all
87+
.map((tg) => tg.tabs)
88+
.flat()
89+
90+
// Find the tab that matches the file Uri
91+
const foundTab = tabs.find(
92+
(tab) =>
93+
tab.input instanceof vscode.TabInputText &&
94+
tab.input.uri.path === doc?.fileName
95+
)
96+
97+
// If the tab is found, close it
98+
if (foundTab) {
99+
// The close method can take a single tab or an array of tabs
100+
await vscode.window.tabGroups.close(foundTab)
101+
}
85102
})
86103
)
87104

0 commit comments

Comments
 (0)