From caecb020830d17351b4a74294578b41f5d17f217 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 14 Jun 2026 10:42:18 -0700 Subject: [PATCH 1/7] keyring-style dart installation rather than holding keys --- tool/gh_codespaces/install_dart.sh | 21 ++- tool/gh_codespaces/pubkeys/dart.pub | 267 ---------------------------- 2 files changed, 10 insertions(+), 278 deletions(-) delete mode 100644 tool/gh_codespaces/pubkeys/dart.pub diff --git a/tool/gh_codespaces/install_dart.sh b/tool/gh_codespaces/install_dart.sh index abbe39a0c..3fc47fcd7 100755 --- a/tool/gh_codespaces/install_dart.sh +++ b/tool/gh_codespaces/install_dart.sh @@ -11,21 +11,20 @@ set -euo pipefail -# Add Dart repository key. - -declare -r input_pubkey_file='tool/gh_codespaces/pubkeys/dart.pub' -declare -r output_pubkey_file='/usr/share/keyrings/dart.gpg' - -sudo gpg --output ${output_pubkey_file} --dearmor ${input_pubkey_file} +sudo apt-get update +sudo apt-get install -y wget gpg apt-transport-https -# Add Dart repository. +sudo mkdir -p /usr/share/keyrings +wget -qO- https://dl-ssl.google.com/linux/linux_signing_key.pub \ + | gpg --dearmor \ + | sudo tee /usr/share/keyrings/dart.gpg >/dev/null -declare -r dart_repository_url='https://storage.googleapis.com/download.dartlang.org/linux/debian' -declare -r dart_repository_file='/etc/apt/sources.list.d/dart.list' +# Add Dart repository key. -echo "deb [signed-by=${output_pubkey_file}] ${dart_repository_url} stable main" | sudo tee ${dart_repository_file} +echo "deb [signed-by=/usr/share/keyrings/dart.gpg] https://storage.googleapis.com/download.dartlang.org/linux/debian stable main" \ + | sudo tee /etc/apt/sources.list.d/dart_stable.list # Install Dart. sudo apt-get update -sudo apt-get install dart +sudo apt-get install -y dart diff --git a/tool/gh_codespaces/pubkeys/dart.pub b/tool/gh_codespaces/pubkeys/dart.pub deleted file mode 100644 index 0366239cb..000000000 --- a/tool/gh_codespaces/pubkeys/dart.pub +++ /dev/null @@ -1,267 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.2.2 (GNU/Linux) - -mQGiBEXwb0YRBADQva2NLpYXxgjNkbuP0LnPoEXruGmvi3XMIxjEUFuGNCP4Rj/a -kv2E5VixBP1vcQFDRJ+p1puh8NU0XERlhpyZrVMzzS/RdWdyXf7E5S8oqNXsoD1z -fvmI+i9b2EhHAA19Kgw7ifV8vMa4tkwslEmcTiwiw8lyUl28Wh4Et8SxzwCggDcA -feGqtn3PP5YAdD0km4S4XeMEAJjlrqPoPv2Gf//tfznY2UyS9PUqFCPLHgFLe80u -QhI2U5jt6jUKN4fHauvR6z3seSAsh1YyzyZCKxJFEKXCCqnrFSoh4WSJsbFNc4PN -b0V0SqiTCkWADZyLT5wll8sWuQ5ylTf3z1ENoHf+G3um3/wk/+xmEHvj9HCTBEXP -78X0A/0Tqlhc2RBnEf+AqxWvM8sk8LzJI/XGjwBvKfXe+l3rnSR2kEAvGzj5Sg0X -4XmfTg4Jl8BNjWyvm2Wmjfet41LPmYJKsux3g0b8yzQxeOA4pQKKAU3Z4+rgzGmf -HdwCG5MNT2A5XxD/eDd+L4fRx0HbFkIQoAi1J3YWQSiTk15fw7RMR29vZ2xlLCBJ -bmMuIExpbnV4IFBhY2thZ2UgU2lnbmluZyBLZXkgPGxpbnV4LXBhY2thZ2VzLWtl -eW1hc3RlckBnb29nbGUuY29tPohjBBMRAgAjAhsDBgsJCAcDAgQVAggDBBYCAwEC -HgECF4AFAkYVdn8CGQEACgkQoECDD3+sWZHKSgCfdq3HtNYJLv+XZleb6HN4zOcF -AJEAniSFbuv8V5FSHxeRimHx25671az+uQINBEXwb0sQCACuA8HT2nr+FM5y/kzI -A51ZcC46KFtIDgjQJ31Q3OrkYP8LbxOpKMRIzvOZrsjOlFmDVqitiVc7qj3lYp6U -rgNVaFv6Qu4bo2/ctjNHDDBdv6nufmusJUWq/9TwieepM/cwnXd+HMxu1XBKRVk9 -XyAZ9SvfcW4EtxVgysI+XlptKFa5JCqFM3qJllVohMmr7lMwO8+sxTWTXqxsptJo -pZeKz+UBEEqPyw7CUIVYGC9ENEtIMFvAvPqnhj1GS96REMpry+5s9WKuLEaclWpd -K3krttbDlY1NaeQUCRvBYZ8iAG9YSLHUHMTuI2oea07Rh4dtIAqPwAX8xn36JAYG -2vgLAAMFB/wKqaycjWAZwIe98Yt0qHsdkpmIbarD9fGiA6kfkK/UxjL/k7tmS4Vm -CljrrDZkPSQ/19mpdRcGXtb0NI9+nyM5trweTvtPw+HPkDiJlTaiCcx+izg79Fj9 -KcofuNb3lPdXZb9tzf5oDnmm/B+4vkeTuEZJ//IFty8cmvCpzvY+DAz1Vo9rA+Zn -cpWY1n6z6oSS9AsyT/IFlWWBZZ17SpMHu+h4Bxy62+AbPHKGSujEGQhWq8ZRoJAT -G0KSObnmZ7FwFWu1e9XFoUCt0bSjiJWTIyaObMrWu/LvJ3e9I87HseSJStfw6fki -5og9qFEkMrIrBCp3QGuQWBq/rTdMuwNFiEkEGBECAAkFAkXwb0sCGwwACgkQoECD -D3+sWZF/WACfeNAu1/1hwZtUo1bR+MWiCjpvHtwAnA1R3IHqFLQ2X3xJ40XPuAyY -/FJG -=Quqp ------END PGP PUBLIC KEY BLOCK----- ------BEGIN PGP PUBLIC KEY BLOCK----- - -mQINBFcMjNMBEAC6Wr5QuLIFgz1V1EFPlg8ty2TsjQEl4VWftUAqWlMevJFWvYEx -BOsOZ6kNFfBfjAxgJNWTkxZrHzDl74R7KW/nUx6X57bpFjUyRaB8F3/NpWKSeIGS -pJT+0m2SgUNhLAn1WY/iNJGNaMl7lgUnaP+/ZsSNT9hyTBiH3Ev5VvAtMGhVI/u8 -P0EtTjXp4o2U+VqFTBGmZ6PJVhCFjZUeRByloHw8dGOshfXKgriebpioHvU8iQ2U -GV3WNIirB2Rq1wkKxXJ/9Iw+4l5m4GmXMs7n3XaYQoBj28H86YA1cYWSm5LR5iU2 -TneI1fJ3vwF2vpSXVBUUDk67PZhg6ZwGRT7GFWskC0z8PsWd5jwK20mA8EVKq0vN -BFmMK6i4fJU+ux17Rgvnc9tDSCzFZ1/4f43EZ41uTmmNXIDsaPCqwjvSS5ICadt2 -xeqTWDlzONUpOs5yBjF1cfJSdVxsfshvln2JXUwgIdKl4DLbZybuNFXnPffNLb2v -PtRJHO48O2UbeXS8n27PcuMoLRd7+r7TsqG2vBH4t/cB/1vsvWMbqnQlaJ5VsjeW -Tp8Gv9FJiKuU8PKiWsF4EGR/kAFyCB8QbJeQ6HrOT0CXLOaYHRu2TvJ4taY9doXn -98TgU03XTLcYoSp49cdkkis4K+9hd2dUqARVCG7UVd9PY60VVCKi47BVKQARAQAB -tFRHb29nbGUgSW5jLiAoTGludXggUGFja2FnZXMgU2lnbmluZyBBdXRob3JpdHkp -IDxsaW51eC1wYWNrYWdlcy1rZXltYXN0ZXJAZ29vZ2xlLmNvbT6JAk4EEwEIADgC -GwMCHgECF4AWIQTrTBv9TwQvbd3M7JF3IfY704tHlgUCVwyM0wULCQgHAgYVCgkI -CwIEFgIDAQAKCRB3IfY704tHlkGrD/9aIOPxoABbhHDa+GbM1XHSeV99q2UOIsYc -A5Jg3k2+Vbjr/006cL9Kk+rdbruZJtERo2z+HVVhkJisvySbsd0UbWfiY5AdHzNP -azpitbX9cNYi0ghDZsD5UgP3cWdx21BJPO0v9PBG9U4z1TQ+pmsQphtNzMC4tK+A -H/7WTXnVPzKXTYziIEIPgHeassSj7Yfwa8kLiBR5tAehHDNNMi/mMf4d6a+wO46x -hhRx/BLjoaIxsZw9f5VxDAqGbCrW8IccwJX8vTc89y+6vpzSurdqYrplZWGpcnfT -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 -bVtIZ+JHTbuH+tg0EoRNcCbz -=JIbr ------END PGP PUBLIC KEY BLOCK----- From 089faf88031f1cb74bc03e0e1e756f001fc1a2ca Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 14 Jun 2026 11:36:04 -0700 Subject: [PATCH 2/7] new dart analyzer failure with bad override --- doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart b/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart index 5765f08bf..18efb5a2d 100644 --- a/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart +++ b/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart @@ -7,7 +7,6 @@ import '../../chapter_3/answers/helper.dart'; import '../../chapter_5/answers/full_subtractor.dart'; class FullSubtractorComb extends FullSubtractor { - @override FullSubtractorComb(super.a, super.b, super.borrowIn) { // Declare input and output final a = input('a'); From 1232afbe490bbf246590886e2adae8765aba699d Mon Sep 17 00:00:00 2001 From: Desmond Kirkpatrick Date: Mon, 15 Jun 2026 06:04:29 -0700 Subject: [PATCH 3/7] Potential fix for pull request finding Clarify comment Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tool/gh_codespaces/install_dart.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tool/gh_codespaces/install_dart.sh b/tool/gh_codespaces/install_dart.sh index 3fc47fcd7..f170dc247 100755 --- a/tool/gh_codespaces/install_dart.sh +++ b/tool/gh_codespaces/install_dart.sh @@ -19,7 +19,7 @@ wget -qO- https://dl-ssl.google.com/linux/linux_signing_key.pub \ | gpg --dearmor \ | sudo tee /usr/share/keyrings/dart.gpg >/dev/null -# Add Dart repository key. +# Add Dart repository. echo "deb [signed-by=/usr/share/keyrings/dart.gpg] https://storage.googleapis.com/download.dartlang.org/linux/debian stable main" \ | sudo tee /etc/apt/sources.list.d/dart_stable.list From 5c8fc4db135be4858dd042a80b65269cab635ab1 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 20 Jun 2026 06:24:51 -0700 Subject: [PATCH 4/7] Limit ROHD extension branch scope --- analysis_options.yaml | 1 + .../answers/exercise_2_n_bit_subtractor.dart | 122 ++--- rohd_extension/.markdownlint.json | 4 + rohd_extension/Makefile | 98 ++++ rohd_extension/README.md | 287 ++++++++++ rohd_extension/dart/analysis_options.yaml | 1 + rohd_extension/dart/lib/dtd_service.dart | 147 +++++ rohd_extension/dart/lib/flc_data.dart | 429 +++++++++++++++ .../dart/lib/rohd_source_navigator.dart | 11 + rohd_extension/dart/lib/source_navigator.dart | 217 ++++++++ rohd_extension/dart/pubspec.yaml | 17 + rohd_extension/dart/test/flc_data_test.dart | 405 ++++++++++++++ rohd_extension/package.json | 102 ++++ rohd_extension/resources/icon.png | Bin 0 -> 404 bytes rohd_extension/snippets/rohd.json | 415 ++++++++++++++ rohd_extension/src/conditional_completions.ts | 493 +++++++++++++++++ rohd_extension/src/debug_tracker.ts | 384 +++++++++++++ rohd_extension/src/dtd_bridge.ts | 397 ++++++++++++++ rohd_extension/src/extension.ts | 129 +++++ rohd_extension/src/flc_service.ts | 515 ++++++++++++++++++ rohd_extension/src/source_navigator.ts | 482 ++++++++++++++++ rohd_extension/src/uri_forwarder.ts | 295 ++++++++++ rohd_extension/tool/install.sh | 58 ++ rohd_extension/tsconfig.json | 15 + 24 files changed, 4958 insertions(+), 66 deletions(-) create mode 100644 rohd_extension/.markdownlint.json create mode 100644 rohd_extension/Makefile create mode 100644 rohd_extension/README.md create mode 100644 rohd_extension/dart/analysis_options.yaml create mode 100644 rohd_extension/dart/lib/dtd_service.dart create mode 100644 rohd_extension/dart/lib/flc_data.dart create mode 100644 rohd_extension/dart/lib/rohd_source_navigator.dart create mode 100644 rohd_extension/dart/lib/source_navigator.dart create mode 100644 rohd_extension/dart/pubspec.yaml create mode 100644 rohd_extension/dart/test/flc_data_test.dart create mode 100644 rohd_extension/package.json create mode 100644 rohd_extension/resources/icon.png create mode 100644 rohd_extension/snippets/rohd.json create mode 100644 rohd_extension/src/conditional_completions.ts create mode 100644 rohd_extension/src/debug_tracker.ts create mode 100644 rohd_extension/src/dtd_bridge.ts create mode 100644 rohd_extension/src/extension.ts create mode 100644 rohd_extension/src/flc_service.ts create mode 100644 rohd_extension/src/source_navigator.ts create mode 100644 rohd_extension/src/uri_forwarder.ts create mode 100755 rohd_extension/tool/install.sh create mode 100644 rohd_extension/tsconfig.json diff --git a/analysis_options.yaml b/analysis_options.yaml index 2b2098177..2158fae07 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -9,6 +9,7 @@ analyzer: exclude: - doc/tutorials/chapter_9/rohd_vf_example - rohd_devtools_extension + - rohd_extension/dart # keep up to date, matching https://dart.dev/tools/linter-rules/all # some lints are not yet available, so disabled and marked with [not currently recognized] diff --git a/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart b/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart index 5765f08bf..d6f3eb17f 100644 --- a/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart +++ b/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart @@ -7,7 +7,6 @@ import '../../chapter_3/answers/helper.dart'; import '../../chapter_5/answers/full_subtractor.dart'; class FullSubtractorComb extends FullSubtractor { - @override FullSubtractorComb(super.a, super.b, super.borrowIn) { // Declare input and output final a = input('a'); @@ -19,39 +18,19 @@ class FullSubtractorComb extends FullSubtractor { final borrowOut = addOutput('borrow_comb'); Combinational([ - Case([a, b, borrow].swizzle(), [ - CaseItem(Const(bin('000'), width: 3), [ - diff < 0, - borrowOut < 0, - ]), - CaseItem(Const(bin('001'), width: 3), [ - diff < 1, - borrowOut < 1, - ]), - CaseItem(Const(bin('010'), width: 3), [ - diff < 1, - borrowOut < 1, - ]), - CaseItem(Const(bin('011'), width: 3), [ - diff < 0, - borrowOut < 1, - ]), - CaseItem(Const(bin('100'), width: 3), [ - diff < 1, - borrowOut < 0, - ]), - CaseItem(Const(bin('101'), width: 3), [ - diff < 0, - borrowOut < 0, - ]), - CaseItem(Const(bin('110'), width: 3), [ - diff < 0, - borrowOut < 0, - ]) - ], defaultItem: [ - diff < 1, - borrowOut < 1 - ]) + Case( + [a, b, borrow].swizzle(), + [ + CaseItem(Const(bin('000'), width: 3), [diff < 0, borrowOut < 0]), + CaseItem(Const(bin('001'), width: 3), [diff < 1, borrowOut < 1]), + CaseItem(Const(bin('010'), width: 3), [diff < 1, borrowOut < 1]), + CaseItem(Const(bin('011'), width: 3), [diff < 0, borrowOut < 1]), + CaseItem(Const(bin('100'), width: 3), [diff < 1, borrowOut < 0]), + CaseItem(Const(bin('101'), width: 3), [diff < 0, borrowOut < 0]), + CaseItem(Const(bin('110'), width: 3), [diff < 0, borrowOut < 0]), + ], + defaultItem: [diff < 1, borrowOut < 1], + ), ]); } @@ -92,41 +71,49 @@ class NBitFullSubtractor extends Module { Future main() async { group('Full Subtractor', () { - test('should return True when cas conditionals matched truth table', - () async { - final a = Logic(); - final b = Logic(); - final bIn = Logic(); - - final fsComb = FullSubtractorComb(a, b, bIn); - await fsComb.build(); - - for (var i = 0; i <= 1; i++) { - for (var j = 0; j <= 1; j++) { - for (var k = 0; k <= 1; k++) { - a.put(i); - b.put(j); - bIn.put(k); - - final res = fsTruthTable(i, j, k); - - final actualDiff = fsComb.fsResult.diff.value.toInt(); - final actualBOut = fsComb.fsResult.borrow.value.toInt(); - - final expectedDiff = res.diff; - final expectedBOut = res.borrowOut; - - expect(actualDiff, expectedDiff, + test( + 'should return True when cas conditionals matched truth table', + () async { + final a = Logic(); + final b = Logic(); + final bIn = Logic(); + + final fsComb = FullSubtractorComb(a, b, bIn); + await fsComb.build(); + + for (var i = 0; i <= 1; i++) { + for (var j = 0; j <= 1; j++) { + for (var k = 0; k <= 1; k++) { + a.put(i); + b.put(j); + bIn.put(k); + + final res = fsTruthTable(i, j, k); + + final actualDiff = fsComb.fsResult.diff.value.toInt(); + final actualBOut = fsComb.fsResult.borrow.value.toInt(); + + final expectedDiff = res.diff; + final expectedBOut = res.borrowOut; + + expect( + actualDiff, + expectedDiff, reason: 'a: $a, b: $b, bIn: $bIn' - ' actualDiff: $actualDiff, expectedDiff: $expectedDiff'); + ' actualDiff: $actualDiff, expectedDiff: $expectedDiff', + ); - expect(actualBOut, expectedBOut, + expect( + actualBOut, + expectedBOut, reason: 'a: $a, b: $b, bIn: $bIn' - ' actualBOut: $actualBOut, expectedBOut: $expectedBOut'); + ' actualBOut: $actualBOut, expectedBOut: $expectedBOut', + ); + } } } - } - }); + }, + ); }); test( @@ -145,7 +132,10 @@ Future main() async { a.put(randA); b.put(randB); - expect(mod.result.value.toInt(), equals(minusResult), - reason: 'randA: $randA, randB: $randB, addResult: $minusResult'); + expect( + mod.result.value.toInt(), + equals(minusResult), + reason: 'randA: $randA, randB: $randB, addResult: $minusResult', + ); }); } diff --git a/rohd_extension/.markdownlint.json b/rohd_extension/.markdownlint.json new file mode 100644 index 000000000..fe1bf1caa --- /dev/null +++ b/rohd_extension/.markdownlint.json @@ -0,0 +1,4 @@ +{ + "MD013": { "tables": false, "code_blocks": false }, + "MD060": false +} diff --git a/rohd_extension/Makefile b/rohd_extension/Makefile new file mode 100644 index 000000000..53f5d297b --- /dev/null +++ b/rohd_extension/Makefile @@ -0,0 +1,98 @@ +# Makefile for the ROHD VS Code Extension (rohd_extension) +# +# Targets: +# compile - Compile TypeScript sources to out/ +# install - Compile and install to remote VS Code Server (default) +# install-local - Compile and install to local VS Code +# install-remote - Compile and install to remote VS Code Server +# clean - Remove compiled output and packaged .vsix files +# real-clean - clean + remove node_modules, lock files, .dart_tool +# help - Show this help + +ROOT := $(shell pwd) +TS_SOURCES := $(shell find $(ROOT)/src -name '*.ts' 2>/dev/null) + +# Read name/version from package.json +PKG_NAME := $(shell node -p "require('./package.json').name") +PKG_VERSION := $(shell node -p "require('./package.json').version") +PKG_PUBLISHER := $(shell node -p "require('./package.json').publisher") + +# VS Code Server extension directory (remote dev container) +REMOTE_EXT_DIR := $(HOME)/.vscode-server/extensions/$(PKG_PUBLISHER).$(PKG_NAME)-$(PKG_VERSION) +# Local VS Code extension directory +LOCAL_EXT_DIR := $(HOME)/.vscode/extensions/$(PKG_PUBLISHER).$(PKG_NAME)-$(PKG_VERSION) + +# Stamp file to track last successful compile +COMPILE_STAMP := out/.compile-stamp + +.PHONY: all help compile install install-local install-remote clean real-clean + +all: install + +help: + @echo "ROHD VS Code Extension - Build Targets" + @echo "" + @echo " compile - Compile TypeScript to out/" + @echo " install - Compile and install to remote server (default)" + @echo " install-local - Compile and install to local VS Code" + @echo " install-remote - Compile and install to remote VS Code Server" + @echo " clean - Remove compiled output and .vsix files" + @echo " real-clean - clean + node_modules, lock files, .dart_tool" + @echo "" + @echo "Extension: $(PKG_PUBLISHER).$(PKG_NAME) v$(PKG_VERSION)" + @echo "Remote: $(REMOTE_EXT_DIR)" + +# --------------------------------------------------------------------------- +# Compile +# --------------------------------------------------------------------------- + +$(COMPILE_STAMP): $(TS_SOURCES) tsconfig.json package.json + @echo "Compiling TypeScript..." + @npx tsc -p ./ + @touch $(COMPILE_STAMP) + +compile: $(COMPILE_STAMP) + +# --------------------------------------------------------------------------- +# Install +# --------------------------------------------------------------------------- + +install-remote: compile + @if [ -d "$(REMOTE_EXT_DIR)" ]; then \ + echo "Installing to $(REMOTE_EXT_DIR)/out/..."; \ + cp out/*.js out/*.js.map "$(REMOTE_EXT_DIR)/out/"; \ + echo "Done. Reload the VS Code window to pick up changes."; \ + else \ + echo "ERROR: Extension not found at $(REMOTE_EXT_DIR)"; \ + echo "Install the extension first via VS Code, then use this target to update."; \ + exit 1; \ + fi + +install-local: compile + @if [ -d "$(LOCAL_EXT_DIR)" ]; then \ + echo "Installing to $(LOCAL_EXT_DIR)/out/..."; \ + cp out/*.js out/*.js.map "$(LOCAL_EXT_DIR)/out/"; \ + echo "Done. Reload the VS Code window to pick up changes."; \ + else \ + echo "ERROR: Extension not found at $(LOCAL_EXT_DIR)"; \ + echo "Install the extension first via VS Code, then use this target to update."; \ + exit 1; \ + fi + +install: install-remote + +# --------------------------------------------------------------------------- +# Clean +# --------------------------------------------------------------------------- + +clean: + @echo "Cleaning compiled output and .vsix files..." + @rm -rf out/ + @rm -f *.vsix + +real-clean: clean + @echo "Removing node_modules, lock files, and Dart build artifacts..." + @rm -rf node_modules/ + @rm -f package-lock.json + @rm -rf dart/.dart_tool/ + @rm -f dart/pubspec.lock diff --git a/rohd_extension/README.md b/rohd_extension/README.md new file mode 100644 index 000000000..59c02aae8 --- /dev/null +++ b/rohd_extension/README.md @@ -0,0 +1,287 @@ +# ROHD VS Code Extension + +A VS Code extension for the [ROHD](https://github.com/intel/rohd) hardware +design framework. It provides context-aware Dart code snippets, cross-probe +source navigation from ROHD viewers (schematic, waveform), and automatic +port-forwarded URI display for the Dart Tooling Daemon (DTD) and VM Service. + +## Features + +- **Context-aware ROHD snippets** — conditional constructs (`If`, `Iff`, + `ElseIf`, `Else`, `Case`, `CaseZ`) only appear when the cursor is inside + a `Combinational` or `Sequential` block. Top-level patterns (`Module`, + `Sequential`, `Combinational`, `FSM`, etc.) are always available. +- **Cross-probe source navigation** — click a signal or module in an ROHD + viewer and jump directly to the corresponding Dart source (FLC — **F**ile, + **L**ine, **C**olumn — crossprobing). +- **Debug adapter tracking** — registers a `DebugAdapterTrackerFactory` for + Dart sessions to automatically capture DTD and VM Service URIs with + port-forwarding awareness. +- **DTD bridge** — registers `rohd.goToSource` / `rohd.resolveFrames` + services on the Dart Tooling Daemon so the DevTools extension can navigate + the editor remotely. + +On activation the extension prints: + +```text +════════════════════════════════════════════════════════════ +ROHD 0.3.0: Extension loaded for FLC crossprobing. + +DTD: + URI: ws://127.0.0.1:44123/token + Fwd: ws://localhost:58201/token ← only shown if port differs +VM: + URI: ws://127.0.0.1:40699/TOKEN=/ws + Fwd: ws://localhost:61969/TOKEN=/ws ← only shown if port differs +════════════════════════════════════════════════════════════ +``` + +## Snippets + +### Always available (top-level) + +| Prefix | Expands to | Description | +|--------|-----------|-------------| +| `mod`, `Module` | `class … extends Module { … }` | Module scaffold with `addInput`/`addOutput` | +| `seq`, `Sequential` | `Sequential(clk, [ If(reset, …) ])` | `always_ff` block with reset pattern | +| `comb`, `Combinational` | `Combinational([ … ])` | `always_comb` block | +| `assign` | `out <= expr;` | Continuous assignment (outside `_Always`) | +| `sim`, `Simulator` | Clock, reset, `WaveDumper`, `Simulator.run()` | Simulation / testbench boilerplate | +| `fsm`, `FSM` | `FiniteStateMachine` scaffold | FSM with states, events, actions | +| `example`, `counter` | Full counter module | ROHD counter reference example | +| `vf`, `tb`, `testbench` | `rohd_vf` testbench | Agent / Driver / Monitor / Sequencer template | + +### Context-aware — file scope + +These appear only at file/top level (not inside a function or class body). + +| Prefix | Expands to | Description | +|--------|-----------|-------------| +| `FSM`, `fsm` | enum + `class extends Module` + `FiniteStateMachine` | Full FSM scaffold at file level | +| `Module`, `mod` | `class extends Module { addInput/addOutput … }` | Module scaffold | +| `Interface`, `intf` | enum + `class extends Interface` + `clone()` | Classic Interface with direction enum, `setPorts`, and `clone()` | +| `PairInterface`, `pairintf` | `class extends PairInterface { … clone() }` | PairInterface with provider/consumer roles | + +### Context-aware — module body scope + +These appear when the cursor is inside a `class … extends Module` body. + +| Prefix | Expands to | Description | +|--------|-----------|-------------| +| `Pipeline`, `pipe` | `Pipeline(clk, stages: [(p) => […]])` | Pipelined datapath | +| `ReadyValidPipeline`, `rvpipe` | `ReadyValidPipeline(clk, stages: …, valid, ready)` | Pipeline with flow control | +| `Sequential`, `seq` | `Sequential(clk, [If(reset, …)])` | `always_ff` block | +| `Combinational`, `comb` | `Combinational([…])` | `always_comb` block | + +### Context-aware — inside `Combinational` or `Sequential` + +These snippets only appear when the cursor is inside a `Combinational([…])` +or `Sequential(clk, […])` block, matching ROHD's requirement that +conditionals live inside an `_Always` block. + +| Prefix | Expands to | Description | +|--------|-----------|-------------| +| `If` | `If(cond, then: […], orElse: […])` | Inline if/else (most common) | +| `ifthen` | `If(cond, then: […])` | Simple conditional guard | +| `ifnested`, `iforelse` | `If(a, then: …, orElse: [If(b, …)])` | Nested if / else-if / else chain | +| `If.block`, `ifblock` | `If.block([Iff(…), ElseIf(…), Else(…)])` | Flat if/else-if/else block chain | +| `Iff`, `iff` | `Iff(cond, […])` | First clause in `If.block` (two f's) | +| `ElseIf`, `elseif` | `ElseIf(cond, […])` | Middle clause in `If.block` | +| `Else`, `else` | `Else([…])` | Final clause in `If.block` | +| `Case` | `Case(expr, [CaseItem(…)], …)` | `case` / `unique case` / `priority case` | +| `CaseZ`, `casez` | `CaseZ(expr, [CaseItem(…)])` | Don't-care matching with `z` syntax | +| `CaseItem`, `caseitem` | `CaseItem(value, […])` | Single arm inside `Case`/`CaseZ` | +| `assign` | `out < expr,` | Conditional assignment (inside `_Always`) | + +> **Note:** `Iff` (two f's) is *not* `If` — it is the first entry in an +> `If.block([…])` chain. The double-f distinguishes it from `If(cond, +> then: …, orElse: …)` which is a self-contained conditional. + +## FLC Cross-Probing + +FLC (**F**ile, **L**ine, **C**olumn) data maps every signal and submodule +in the generated output back to the Dart source location where it was +constructed. The extension uses FLC data to navigate from a schematic or +waveform viewer directly to the ROHD Dart source. + +### FLC JSON Format (v2) + +An `.flc.json` file has a shared file table and per-module signal/instance +entries: + +```json +{ + "version": 2, + "files": [ + "lib/src/my_module.dart", + "lib/src/modules/gates.dart" + ], + "modules": { + "Top": { + "svFile": "Top.sv", + "signals": { + "a": { "sv": "2:19", "src": ["0:15:9"] }, + "b": { "sv": "3:20", "src": ["0:16:15"] } + }, + "instances": { + "inner": { "sv": "7:1", "src": ["0:6:20", "0:17:17"] } + } + } + } +} +``` + +- **`files`** — array of workspace-relative paths, indexed by the first + number in each trace entry. +- **`"0:15:9"`** — file index 0, line 15, column 9. +- **`sv`** — optional line:column in the generated `.sv` file. +- **`src`** — one or more Dart source locations (multiple entries represent + the construction call stack, innermost first). +- A signal may use the compact form (bare array `["0:15:9"]`) when there + is no SV mapping, or the enriched form (object with `sv`/`src`). + +## Commands + +| Command | Title | +|---------|-------| +| `rohd.openSourceLocation` | Go to Source Location | +| `rohd.openSourceLocations` | Go to Source Locations (multi-frame) | +| `rohd.nextSourceLocation` | Next Source Frame | +| `rohd.prevSourceLocation` | Previous Source Frame | +| `rohd.connectDtd` | Connect to Dart Tooling Daemon | +| `rohd.showForwardedUris` | Show Forwarded DTD/VM URIs | + +## Settings + +| Setting | Default | Description | +|---------|---------|-------------| +| `rohd.enableCompletions` | *(unset)* | Enable context-aware ROHD completions. On first activation you are prompted; the choice is saved globally. Set `true`/`false` to skip the prompt. | +| `rohd.dtdUri` | `""` | WebSocket URI of the Dart Tooling Daemon. Leave empty for auto-discovery. | + +## Prerequisites + +- **Node.js ≥ 18** (use nvm if the container ships an older version): + + ```bash + export PATH="$HOME/.nvm/versions/node/v20.19.6/bin:$PATH" + node --version # v18+ or v20+ + ``` + +- **npm** (comes with Node) + +## Build + +```bash +cd rohd_extension +npm install +npm run compile # produces out/extension.js +``` + +## Local Installation + +### Package as VSIX + +```bash +cd rohd_extension +yes | npx @vscode/vsce package --allow-missing-repository +``` + +This produces `rohd-0.3.0.vsix`. + +### Install + +```bash +code --install-extension rohd-0.3.0.vsix --force +``` + +Then reload the VS Code window (**Developer: Reload Window**). + +### One-liner (build + install) + +```bash +export PATH="$HOME/.nvm/versions/node/v20.19.6/bin:$PATH" \ + && cd rohd_extension \ + && npm install \ + && npm run compile \ + && rm -f *.vsix \ + && yes | npx @vscode/vsce package --allow-missing-repository \ + && code --install-extension rohd-0.3.0.vsix --force \ + && echo "Done — reload the VS Code window to activate." +``` + +## Remote Installation (Dev Containers / SSH) + +Extensions that interact with the Dart debug adapter must be installed on the +**remote** side (inside the container or on the SSH host). + +### Option 1: devcontainer.json (recommended) + +Place the extension source in your repo and build it on container creation: + +```jsonc +// .devcontainer/devcontainer.json +{ + "postCreateCommand": "cd rohd_extension && export PATH=\"$HOME/.nvm/versions/node/v20.19.6/bin:$PATH\" && npm install && npm run compile && rm -f *.vsix && yes | npx @vscode/vsce package --allow-missing-repository && code --install-extension rohd-0.2.0.vsix --force" +} +``` + +### Option 2: Pre-built VSIX + +Build the `.vsix` on your host or in CI, then install at container start: + +```jsonc +{ + "postStartCommand": "code --install-extension rohd_extension/rohd-0.2.0.vsix --force" +} +``` + +### Option 3: Install via CLI while connected + +```bash +code --install-extension rohd-0.2.0.vsix --force +``` + +The `code` CLI inside a remote session targets the VS Code Server +automatically. + +### Option 4: Install via VS Code UI + +1. Connect to the remote host / container. +2. Open Extensions (`Ctrl+Shift+X`). +3. Click `...` → **Install from VSIX...** and select the `.vsix` file. +4. Reload the window. + +### Option 5: Copy directly into `.vscode-server/extensions/` + +If the `code` CLI is not available (e.g. in a Dockerfile `RUN` step): + +```bash +mkdir -p ~/.vscode-server/extensions/rohd.rohd-0.2.0 +cp -r rohd_extension/{package.json,out,snippets,resources} \ + ~/.vscode-server/extensions/rohd.rohd-0.2.0/ +``` + +The directory name must follow the pattern `.-`. + +## File Structure + +```text +rohd_extension/ +├── package.json # Extension manifest +├── tsconfig.json # TypeScript configuration +├── src/ +│ ├── extension.ts # Entry point — activates all modules +│ ├── source_navigator.ts # Cross-probe → editor navigation +│ ├── dtd_bridge.ts # DTD JSON-RPC bridge +│ ├── debug_tracker.ts # Debug adapter tracker (DTD/VM URIs) +│ └── conditional_completions.ts # Context-aware conditional snippets +├── out/ # Compiled JS (generated) +├── snippets/ +│ └── rohd.json # ROHD Dart snippets +└── resources/ + └── icon.png # Extension icon +``` + +## License + +BSD-3-Clause — see the repository root LICENSE file. diff --git a/rohd_extension/dart/analysis_options.yaml b/rohd_extension/dart/analysis_options.yaml new file mode 100644 index 000000000..7c1aca46c --- /dev/null +++ b/rohd_extension/dart/analysis_options.yaml @@ -0,0 +1 @@ +# include: package:lints/recommended.yaml diff --git a/rohd_extension/dart/lib/dtd_service.dart b/rohd_extension/dart/lib/dtd_service.dart new file mode 100644 index 000000000..47a540b72 --- /dev/null +++ b/rohd_extension/dart/lib/dtd_service.dart @@ -0,0 +1,147 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// dtd_service.dart +// DTD service handler for receiving cross-probe source navigation +// requests from the ROHD DevTools extension. +// +// Registers a `rohd.goToSource` service on the Dart Tooling Daemon so +// that the DevTools extension can send resolved SourceFrame lists for +// navigation in the VS Code editor. +// +// 2026 April 27 +// Author: Desmond Kirkpatrick + +import 'dart:async'; +import 'dart:convert'; + +import 'package:json_rpc_2/json_rpc_2.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +import 'source_navigator.dart'; + +/// Callback invoked when the DTD service receives a goToSource request. +/// +/// The TS shell provides this callback to bridge DTD requests into +/// VS Code command execution. +typedef GoToSourceCallback = Future Function(List frames, + {int startIndex}); + +/// Manages the DTD connection and service registration for source +/// navigation. +class DtdService { + final GoToSourceCallback _onGoToSource; + Peer? _peer; + WebSocketChannel? _channel; + bool _disposed = false; + + /// Creates a DTD service that calls [onGoToSource] when a + /// `rohd.goToSource` request arrives. + DtdService({required GoToSourceCallback onGoToSource}) + : _onGoToSource = onGoToSource; + + /// Connect to the DTD at [uri] and register the `rohd.goToSource` + /// service. + /// + /// Returns `true` if connection and registration succeeded. + Future connect(String uri) async { + if (_disposed) return false; + + try { + _channel = WebSocketChannel.connect(Uri.parse(uri)); + await _channel!.ready; + _peer = Peer(_channel!.cast()); + + // Register the service method. + _peer!.registerMethod('rohd.goToSource', _handleGoToSource); + + // Start listening (non-blocking). + unawaited( + _peer!.listen().then((_) { + // Connection closed. + _peer = null; + }), + ); + + return true; + } on Exception catch (e) { + _peer = null; + _channel = null; + // ignore: avoid_print + print('[DtdService] Failed to connect to DTD at $uri: $e'); + return false; + } + } + + /// Whether the DTD connection is active. + bool get isConnected => _peer != null && !_peer!.isClosed; + + /// Disconnect from DTD and clean up. + Future dispose() async { + _disposed = true; + await _peer?.close(); + _peer = null; + await _channel?.sink.close(); + _channel = null; + } + + // --------------------------------------------------------------------------- + // RPC handler + // --------------------------------------------------------------------------- + + /// Handle an incoming `rohd.goToSource` request. + /// + /// Expected parameters: + /// ```json + /// { + /// "frames": [ + /// {"file": "lib/src/foo.dart", "line": 42, "col": 5, "type": "rohd"}, + /// {"file": "Foo.sv", "line": 10, "col": 1, "type": "sv"} + /// ], + /// "index": 0 // optional starting frame + /// } + /// ``` + Future> _handleGoToSource(Parameters params) async { + try { + final framesRaw = params['frames'].asList; + final startIndex = params['index'].asIntOr(0); + + final frames = framesRaw.map((raw) { + final map = raw as Map; + return SourceFrame.fromJson(map); + }).toList(); + + if (frames.isEmpty) { + return {'status': 'error', 'message': 'No frames provided'}; + } + + await _onGoToSource(frames, startIndex: startIndex); + return {'status': 'ok', 'navigated': frames.length}; + } on Exception catch (e) { + return {'status': 'error', 'message': e.toString()}; + } + } +} + +// --------------------------------------------------------------------------- +// Convenience: encode/decode frames for stdio-based communication +// --------------------------------------------------------------------------- + +/// Encode a goToSource request as a JSON string for stdio transport. +String encodeGoToSourceRequest(List frames, {int index = 0}) => + jsonEncode({ + 'method': 'rohd.goToSource', + 'frames': frames.map((f) => f.toJson()).toList(), + 'index': index, + }); + +/// Decode a goToSource request from a JSON string. +(List, int) decodeGoToSourceRequest(String json) { + final map = jsonDecode(json) as Map; + final framesRaw = map['frames'] as List; + final index = (map['index'] as int?) ?? 0; + final frames = framesRaw + .map((raw) => SourceFrame.fromJson(raw as Map)) + .toList(); + return (frames, index); +} diff --git a/rohd_extension/dart/lib/flc_data.dart b/rohd_extension/dart/lib/flc_data.dart new file mode 100644 index 000000000..010196836 --- /dev/null +++ b/rohd_extension/dart/lib/flc_data.dart @@ -0,0 +1,429 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// flc_data.dart +// FLC (File/Line/Column) data model for cross-probing from schematic +// signals to their ROHD Dart source locations. +// +// Parses trace data embedded in Yosys JSON module attributes under the +// `rohd.src_trace` key, as produced by `SourceTraceRegistry`. +// +// 2026 April +// Author: Desmond Kirkpatrick + +/// A single source location frame from an FLC trace. +class FlcFrame { + /// File path (package-relative, e.g. `lib/src/foo.dart`). + final String file; + + /// 1-based line number. + final int line; + + /// 1-based column number. + final int column; + + /// Frame type: `'rohd'` for ROHD Dart source, `'sv'` for SystemVerilog. + final String type; + + const FlcFrame({ + required this.file, + required this.line, + required this.column, + this.type = 'rohd', + }); + + @override + String toString() => '$file:$line:$column [$type]'; +} + +/// Trace entry for a single signal or instance — one or more stack frames +/// ordered innermost → outermost, plus output-language source locations. +class FlcEntry { + /// Stack frames for this signal/instance (ROHD Dart source). + final List frames; + + /// Output-language source locations (e.g. SystemVerilog, SystemC). + /// + /// Each frame's [FlcFrame.type] identifies the language (`'sv'`, `'sc'`, + /// etc.). Multiple frames per language are allowed (e.g. when a signal + /// appears in both a declaration and an assignment). + final List outputFrames; + + /// Original name before Namer disambiguation (e.g. `sum` before it + /// became `sum_0`). Null if the name was not renamed. + final String? origName; + + const FlcEntry({ + required this.frames, + this.outputFrames = const [], + this.origName, + }); + + /// First SystemVerilog output frame, or `null` if none. + /// + /// Convenience accessor for backward compatibility — equivalent to + /// `outputFrames.where((f) => f.type == 'sv').firstOrNull`. + FlcFrame? get svFrame => outputFrames.cast().firstWhere( + (f) => f!.type == 'sv', + orElse: () => null, + ); + + /// All frames: output-language frames first, then ROHD src frames. + List get allFrames => [...outputFrames, ...frames]; +} + +/// FLC data parsed from a v5 trie-based FLC hierarchy JSON file. +class FlcData { + /// Global file table (index → path). + final List files; + + /// Module name → signal name → FlcEntry. + final Map> _signals; + + /// Module name → instance name → FlcEntry. + final Map> _instances; + + FlcData._({ + required this.files, + required Map> signals, + required Map> instances, + }) : _signals = signals, + _instances = instances; + + /// Whether any FLC data was found. + bool get isEmpty => _signals.isEmpty && _instances.isEmpty; + + /// Parse FLC data from a v5/v6 trie-based hierarchy JSON. + /// + /// v5 and v6 share the trie structure; v6 adds multi-position support + /// (comma-separated entries per language) and list-per-language + /// outputFiles. Any other version is rejected and returns empty FLC data. + factory FlcData.fromJson(Map json) { + final version = json['version']; + if (version != null && version != 5 && version != 6) { + return FlcData._(files: [], signals: {}, instances: {}); + } + final files = + (json['files'] as List?)?.cast() ?? []; + final rawModules = json['modules']; + final modules = + rawModules is Map ? Map.from(rawModules) : null; + if (modules == null) { + return FlcData._(files: files, signals: {}, instances: {}); + } + + final signals = >{}; + final instances = >{}; + + for (final modEntry in modules.entries) { + final moduleName = modEntry.key; + final modMap = modEntry.value as Map?; + if (modMap == null) continue; + + final svFile = modMap['svFile'] as String?; + // Output files map. + // v5: {"sv": "Foo.sv", "sc": "Foo.h"} (string per language) + // v6: {"sv": ["Foo.sv"], "sc": ["Foo.h"]} (list per language) + // Falls back to legacy "svFile" field. For lookup we use the first + // file in each language list (the canonical output). + final rawOutputFiles = modMap['outputFiles']; + final outputFiles = { + if (svFile != null) 'sv': svFile, + }; + if (rawOutputFiles is Map) { + for (final e in rawOutputFiles.entries) { + final v = e.value; + if (v is String) { + outputFiles[e.key as String] = v; + } else if (v is List && v.isNotEmpty && v.first is String) { + outputFiles[e.key as String] = v.first as String; + } + } + } + final tree = modMap['tree'] as List?; + if (tree == null) continue; + + final modSignals = {}; + final modInstances = {}; + + /// Walk a trie node, collecting frames along the path. + /// Frames are accumulated outermost-first; we reverse at the leaf + /// to match the innermost-first convention of FlcEntry.frames. + void walkNode(List node, List path) { + if (node.isEmpty) return; + final frame = node[0] as String; + final currentPath = [...path, frame]; + + for (var i = 1; i < node.length; i++) { + final elem = node[i]; + if (elem is List) { + // Child trie node. + walkNode(elem.cast(), currentPath); + } else if (elem is String) { + // String-encoded leaf symbol. + final parsed = _parseSymbolString(elem); + final name = parsed.name; + final isInstance = parsed.isInstance; + final origName = parsed.origName; + + // Build ROHD source frames (reverse to innermost-first). + final rohdFrames = []; + for (final f in currentPath.reversed) { + final parts = f.split(':'); + if (parts.length < 2) continue; + final fi = int.tryParse(parts[0]); + if (fi == null || fi >= files.length) continue; + final line = int.tryParse(parts[1]) ?? 1; + final col = parts.length > 2 ? (int.tryParse(parts[2]) ?? 1) : 1; + rohdFrames.add( + FlcFrame(file: files[fi], line: line, column: col), + ); + } + + // Build output-language frames from parsed positions. + final outFrames = []; + for (final pos in parsed.outputPositions) { + final file = outputFiles[pos.type]; + if (file == null) continue; + outFrames.add( + FlcFrame( + file: file, + line: pos.line, + column: pos.column, + type: pos.type, + ), + ); + } + + if (rohdFrames.isNotEmpty || outFrames.isNotEmpty) { + final entry = FlcEntry( + frames: rohdFrames, + outputFrames: outFrames, + origName: origName, + ); + if (isInstance) { + modInstances[name] = entry; + } else { + modSignals[name] = entry; + } + } + } + } + } + + // tree is a list of root trie nodes. + for (final rootNode in tree) { + if (rootNode is List) { + walkNode(rootNode.cast(), []); + } + } + + if (modSignals.isNotEmpty) signals[moduleName] = modSignals; + if (modInstances.isNotEmpty) instances[moduleName] = modInstances; + } + + return FlcData._(files: files, signals: signals, instances: instances); + } + + /// Parse a v5 string-encoded symbol. + /// + /// Format: `[*]name[@positions][~origName]` + /// + /// Positions are semicolon-separated language groups; within each group + /// entries are comma-separated. Each entry is optionally prefixed with a + /// language tag (only the group's first entry carries the tag): + /// - `sv:L:C` — SystemVerilog at line L, column C + /// - `sc:L:C` — SystemC at line L, column C + /// - `L:C` — legacy shorthand, treated as `sv:L:C` + /// + /// Examples: + /// - `clk@2:13` (legacy single SV position) + /// - `clk@sv:2:13` (explicit SV position) + /// - `clk@sv:2:13;sc:10:5` (SV + SystemC) + /// - `clk@sv:2:13,5:7;sc:10:5` (v6: multi-entry within a language) + static _SymbolInfo _parseSymbolString(String s) { + final isInstance = s.startsWith('*'); + var rest = isInstance ? s.substring(1) : s; + + String? origName; + final tildeIdx = rest.indexOf('~'); + if (tildeIdx >= 0) { + origName = rest.substring(tildeIdx + 1); + rest = rest.substring(0, tildeIdx); + } + + final outputPositions = <_OutputPos>[]; + final atIdx = rest.indexOf('@'); + if (atIdx >= 0) { + final posStr = rest.substring(atIdx + 1); + rest = rest.substring(0, atIdx); + + for (final group in posStr.split(';')) { + if (group.isEmpty) continue; + // A group is `[lang:]entry(,entry)*` where each entry is `[F:]L:C`. + final entries = group.split(','); + String? groupLang; + for (var i = 0; i < entries.length; i++) { + var part = entries[i]; + if (part.isEmpty) continue; + // Only the first entry of a group may carry a language tag. + if (i == 0) { + final segments = part.split(':'); + final firstIsTag = + segments.length >= 3 && int.tryParse(segments[0]) == null; + if (firstIsTag) { + groupLang = segments[0]; + part = segments.sublist(1).join(':'); + } + } + final type = groupLang ?? 'sv'; + final segments = part.split(':'); + final lineStr = segments.length >= 2 + ? segments[segments.length - 2] + : segments[0]; + final colStr = + segments.length >= 2 ? segments[segments.length - 1] : null; + final line = int.tryParse(lineStr) ?? 1; + final column = colStr != null ? (int.tryParse(colStr) ?? 1) : 1; + outputPositions + .add(_OutputPos(type: type, line: line, column: column)); + } + } + } + + return _SymbolInfo( + name: rest, + isInstance: isInstance, + outputPositions: outputPositions, + origName: origName, + ); + } + + /// Create empty FLC data (no trace information available). + factory FlcData.empty() => FlcData._(files: [], signals: {}, instances: {}); + + /// Look up FLC frames for a signal in a given module. + /// + /// Returns null if no trace data exists for this signal. + /// Falls back to matching by [origName] if the canonical name isn't found. + List? lookupSignal(String moduleName, String signalName) => + lookupSignalEntry(moduleName, signalName)?.frames; + + /// Look up the full [FlcEntry] for a signal (includes SV frame if present). + FlcEntry? lookupSignalEntry(String moduleName, String signalName) { + final modSignals = _signals[moduleName]; + if (modSignals == null) return null; + + // Direct match. + final direct = modSignals[signalName]; + if (direct != null) return direct; + + // Fallback: search by origName. + for (final entry in modSignals.values) { + if (entry.origName != null && entry.origName == signalName) { + return entry; + } + } + return null; + } + + /// Look up FLC frames for an instance (submodule) in a given module. + List? lookupInstance(String moduleName, String instanceName) => + lookupInstanceEntry(moduleName, instanceName)?.frames; + + /// Look up the full [FlcEntry] for an instance (includes SV frame if present). + FlcEntry? lookupInstanceEntry(String moduleName, String instanceName) { + final modInstances = _instances[moduleName]; + if (modInstances == null) return null; + + final direct = modInstances[instanceName]; + if (direct != null) return direct; + + // Fallback: search by origName. + for (final entry in modInstances.values) { + if (entry.origName != null && entry.origName == instanceName) { + return entry; + } + } + return null; + } + + /// All module names that have FLC data. + Set get moduleNames => {..._signals.keys, ..._instances.keys}; + + /// Signal names recorded for [moduleName], or empty if none. + Set signalNamesFor(String moduleName) => + _signals[moduleName]?.keys.toSet() ?? {}; + + /// Instance names recorded for [moduleName], or empty if none. + Set instanceNamesFor(String moduleName) => + _instances[moduleName]?.keys.toSet() ?? {}; + + /// Reverse lookup: find all (moduleName, signalName, entry) tuples whose + /// ROHD source frames include the given [fileSuffix] and [line]. + /// + /// [fileSuffix] is matched against the end of each frame's file path + /// (e.g. `'serializer.dart'` matches `'lib/src/serialization/serializer.dart'`). + /// When [line] is non-null only frames on that exact line match. + List<({String module, String signal, FlcEntry entry})> lookupByRohdLine( + String fileSuffix, { + int? line, + }) { + final results = <({String module, String signal, FlcEntry entry})>[]; + for (final modEntry in _signals.entries) { + for (final sigEntry in modEntry.value.entries) { + for (final frame in sigEntry.value.frames) { + if (frame.file.endsWith(fileSuffix) && + (line == null || frame.line == line)) { + results.add(( + module: modEntry.key, + signal: sigEntry.key, + entry: sigEntry.value, + )); + break; // one match per signal is enough + } + } + } + } + for (final modEntry in _instances.entries) { + for (final instEntry in modEntry.value.entries) { + for (final frame in instEntry.value.frames) { + if (frame.file.endsWith(fileSuffix) && + (line == null || frame.line == line)) { + results.add(( + module: modEntry.key, + signal: instEntry.key, + entry: instEntry.value, + )); + break; + } + } + } + } + return results; + } +} + +/// Parsed v5 symbol string info. +class _SymbolInfo { + final String name; + final bool isInstance; + final List<_OutputPos> outputPositions; + final String? origName; + + const _SymbolInfo({ + required this.name, + required this.isInstance, + this.outputPositions = const [], + this.origName, + }); +} + +/// A parsed output-language position from a symbol string. +class _OutputPos { + final String type; // e.g. 'sv', 'sc' + final int line; + final int column; + + const _OutputPos({required this.type, required this.line, this.column = 1}); +} diff --git a/rohd_extension/dart/lib/rohd_source_navigator.dart b/rohd_extension/dart/lib/rohd_source_navigator.dart new file mode 100644 index 000000000..ea77ef85a --- /dev/null +++ b/rohd_extension/dart/lib/rohd_source_navigator.dart @@ -0,0 +1,11 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// rohd_source_navigator.dart +// Library exports for the ROHD source navigator Dart package. + +library; + +export 'dtd_service.dart'; +export 'flc_data.dart'; +export 'source_navigator.dart'; diff --git a/rohd_extension/dart/lib/source_navigator.dart b/rohd_extension/dart/lib/source_navigator.dart new file mode 100644 index 000000000..f6d96c972 --- /dev/null +++ b/rohd_extension/dart/lib/source_navigator.dart @@ -0,0 +1,217 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// source_navigator.dart +// Platform-independent source navigation logic — path normalisation, +// frame cycling, and candidate path generation. +// +// This is the Dart port of the core logic from source_navigator.ts. +// VS Code-specific APIs (editor, decorations, status bar) remain in the +// thin TypeScript shell. +// +// 2026 April 27 +// Author: Desmond Kirkpatrick + +/// A single source location frame. +class SourceFrame { + /// File path (package-relative, e.g. `lib/src/foo.dart`). + final String file; + + /// 1-based line number. + final int line; + + /// 1-based column number. + final int col; + + /// Optional description (e.g. function name from stack trace). + final String? desc; + + /// Frame type: `'sv'` for SystemVerilog, `'rohd'` for ROHD Dart source. + final String type; + + const SourceFrame({ + required this.file, + required this.line, + required this.col, + this.desc, + this.type = 'rohd', + }); + + /// Create from JSON map (as received over DTD). + factory SourceFrame.fromJson(Map json) => SourceFrame( + file: json['file'] as String, + line: json['line'] as int, + col: json['col'] as int, + desc: json['desc'] as String?, + type: (json['type'] as String?) ?? 'rohd', + ); + + /// Serialize to JSON for transmission. + Map toJson() => { + 'file': file, + 'line': line, + 'col': col, + if (desc != null) 'desc': desc, + 'type': type, + }; + + /// Short display name for status bar. + String get shortFile => file.split('/').last; + + /// Type tag for display: 'SV', 'ROHD', or 'Source'. + String get typeTag { + switch (type) { + case 'sv': + return 'SV'; + case 'rohd': + return 'ROHD'; + default: + return 'Source'; + } + } +} + +/// Manages frame cycling state for multi-frame source navigation. +class FrameCycler { + List _frames = []; + int _index = 0; + + /// The current list of frames. + List get frames => _frames; + + /// The current frame index. + int get index => _index; + + /// Whether there are multiple frames to cycle through. + bool get hasMultipleFrames => _frames.length > 1; + + /// Whether there are any frames at all. + bool get isEmpty => _frames.isEmpty; + + /// The current frame, or null if empty. + SourceFrame? get current => _frames.isEmpty ? null : _frames[_index]; + + /// Set frames for a single source location (no cycling). + void setSingle(SourceFrame frame) { + _frames = [frame]; + _index = 0; + } + + /// Set frames for multi-frame navigation. + void setMultiple(List frames, {int startIndex = 0}) { + _frames = frames; + _index = startIndex.clamp(0, frames.length - 1); + } + + /// Advance to the next frame (wrapping). + SourceFrame? next() { + if (_frames.isEmpty) return null; + _index = (_index + 1) % _frames.length; + return _frames[_index]; + } + + /// Go back to the previous frame (wrapping). + SourceFrame? prev() { + if (_frames.isEmpty) return null; + _index = (_index - 1 + _frames.length) % _frames.length; + return _frames[_index]; + } + + /// Clear all frames. + void clear() { + _frames = []; + _index = 0; + } + + /// Status bar text for the current frame. + /// + /// Format: `"TYPE 1/3: file.dart:42 desc"` + String get statusText { + if (_frames.isEmpty) return ''; + final f = _frames[_index]; + final desc = f.desc != null ? ' ${f.desc}' : ''; + return '${f.typeTag} ${_index + 1}/${_frames.length}: ' + '${f.shortFile}:${f.line}$desc'; + } + + /// Returns the first frame of each unique type (for opening + /// both ROHD and SV files simultaneously). + List firstOfEachType() { + final seen = {}; + final result = []; + for (final f in _frames) { + if (seen.add(f.type)) { + result.add(f); + } + } + return result; + } +} + +/// Normalize a file path by collapsing `.` and `..` segments. +/// +/// FLC paths from SourceTraceRegistry often contain `.dart_tool/../lib/...` +/// which needs collapsing before resolution. +String normalizePath(String filePath) { + final isAbsolute = filePath.startsWith('/'); + final parts = filePath.split('/'); + final resolved = []; + for (final part in parts) { + if (part == '.' || part.isEmpty) { + continue; + } else if (part == '..' && resolved.isNotEmpty && resolved.last != '..') { + resolved.removeLast(); + } else { + resolved.add(part); + } + } + final joined = resolved.join('/'); + return isAbsolute ? '/$joined' : joined; +} + +/// Generate candidate paths for a package-relative file path. +/// +/// Given a list of workspace root paths, produces candidates (in order): +/// 1. Normalized path relative to each workspace root +/// 2. Normalized path relative to parent directories (up to [parentLevels]) +/// 3. Absolute path (if applicable) +/// 4. Original un-normalized path (fallback) +/// +/// Returns relative candidate strings; the caller (TS shell) converts +/// them to URIs. +List resolveCandidatePaths( + String filePath, { + List workspaceRoots = const [], + int parentLevels = 4, +}) { + final normalized = normalizePath(filePath); + final candidates = []; + + for (final root in workspaceRoots) { + // Direct: workspace root + normalized path + candidates.add('$root/$normalized'); + + // Walk up parent directories. + var parent = root; + for (var i = 0; i < parentLevels; i++) { + final lastSlash = parent.lastIndexOf('/'); + if (lastSlash <= 0) break; + parent = parent.substring(0, lastSlash); + candidates.add('$parent/$normalized'); + } + } + + // Absolute path fallback. + if (normalized.startsWith('/')) { + candidates.add(normalized); + } + + // Try original un-normalized path if it differs. + if (normalized != filePath) { + for (final root in workspaceRoots) { + candidates.add('$root/$filePath'); + } + } + + return candidates; +} diff --git a/rohd_extension/dart/pubspec.yaml b/rohd_extension/dart/pubspec.yaml new file mode 100644 index 000000000..cd0129ffe --- /dev/null +++ b/rohd_extension/dart/pubspec.yaml @@ -0,0 +1,17 @@ +name: rohd_source_navigator +description: > + Dart implementation of the ROHD source navigator — path normalisation, + frame cycling, and DTD service for cross-probe source navigation. +version: 0.1.0 + +environment: + sdk: ^3.0.0 + +dependencies: + json_rpc_2: ^3.0.0 + stream_channel: ^2.1.0 + web_socket_channel: ^3.0.0 + +dev_dependencies: + lints: ^5.0.0 + test: ^1.24.0 diff --git a/rohd_extension/dart/test/flc_data_test.dart b/rohd_extension/dart/test/flc_data_test.dart new file mode 100644 index 000000000..9c33e078f --- /dev/null +++ b/rohd_extension/dart/test/flc_data_test.dart @@ -0,0 +1,405 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// flc_data_test.dart +// Unit tests for FlcData model: v5 trie-based FLC JSON parsing and +// signal lookup. + +import 'package:rohd_source_navigator/flc_data.dart'; +import 'package:test/test.dart'; + +void main() { + group('FlcData.fromJson (v5 trie)', () { + test('parses single-frame signal', () { + final json = { + 'version': 5, + 'files': ['lib/src/foo.dart', 'lib/src/bar.dart'], + 'modules': { + 'TopModule': { + 'tree': [ + ['0:10:5', 'clk'], + [ + '0:20:3', + ['1:30:1', 'data'] + ], + ['1:50:7', '*sub0'], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + expect(flc.isEmpty, isFalse); + expect(flc.files, ['lib/src/foo.dart', 'lib/src/bar.dart']); + expect(flc.moduleNames, contains('TopModule')); + + // Single-frame signal. + final clkFrames = flc.lookupSignal('TopModule', 'clk'); + expect(clkFrames, isNotNull); + expect(clkFrames!.length, 1); + expect(clkFrames[0].file, 'lib/src/foo.dart'); + expect(clkFrames[0].line, 10); + expect(clkFrames[0].column, 5); + + // Multi-frame signal (outermost frame first in trie, reversed to + // innermost-first in FlcEntry.frames). + final dataFrames = flc.lookupSignal('TopModule', 'data'); + expect(dataFrames, isNotNull); + expect(dataFrames!.length, 2); + // Innermost first after reversal. + expect(dataFrames[0].file, 'lib/src/bar.dart'); + expect(dataFrames[0].line, 30); + expect(dataFrames[1].file, 'lib/src/foo.dart'); + expect(dataFrames[1].line, 20); + + // Instance lookup. + final sub0Frames = flc.lookupInstance('TopModule', 'sub0'); + expect(sub0Frames, isNotNull); + expect(sub0Frames!.length, 1); + expect(sub0Frames[0].file, 'lib/src/bar.dart'); + expect(sub0Frames[0].line, 50); + }); + + test('parses signal with origName', () { + final json = { + 'version': 5, + 'files': ['lib/src/adder.dart'], + 'modules': { + 'Adder': { + 'tree': [ + ['0:42:5', 'sum_0~sum'], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + + // Direct match by canonical name. + final directFrames = flc.lookupSignal('Adder', 'sum_0'); + expect(directFrames, isNotNull); + expect(directFrames![0].line, 42); + + // Fallback match by origName. + final origFrames = flc.lookupSignal('Adder', 'sum'); + expect(origFrames, isNotNull); + expect(origFrames![0].line, 42); + }); + + test('parses signal with SV position (legacy svFile)', () { + final json = { + 'version': 5, + 'files': ['lib/src/foo.dart'], + 'modules': { + 'FilterBank': { + 'svFile': 'FilterBank.sv', + 'tree': [ + ['0:868:11', 'clk@2:13'], + ['0:869:13', 'reset@3:13'], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + expect(flc.isEmpty, isFalse); + + final clkEntry = flc.lookupSignalEntry('FilterBank', 'clk'); + expect(clkEntry, isNotNull); + + // SV frame via backward-compat getter. + expect(clkEntry!.svFrame, isNotNull); + expect(clkEntry.svFrame!.file, 'FilterBank.sv'); + expect(clkEntry.svFrame!.line, 2); + expect(clkEntry.svFrame!.column, 13); + expect(clkEntry.svFrame!.type, 'sv'); + + // outputFrames list. + expect(clkEntry.outputFrames.length, 1); + expect(clkEntry.outputFrames[0].type, 'sv'); + + // ROHD src frames. + expect(clkEntry.frames.length, 1); + expect(clkEntry.frames[0].file, 'lib/src/foo.dart'); + expect(clkEntry.frames[0].line, 868); + expect(clkEntry.frames[0].column, 11); + expect(clkEntry.frames[0].type, 'rohd'); + + // allFrames returns output frames first, then ROHD. + final allFrames = clkEntry.allFrames; + expect(allFrames.length, 2); + expect(allFrames[0].type, 'sv'); + expect(allFrames[1].type, 'rohd'); + }); + + test('parses signal with outputFiles map', () { + final json = { + 'version': 5, + 'files': ['lib/src/foo.dart'], + 'modules': { + 'FilterBank': { + 'outputFiles': {'sv': 'FilterBank.sv', 'sc': 'FilterBank.cpp'}, + 'tree': [ + ['0:868:11', 'clk@sv:2:13;sc:10:5'], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + final clkEntry = flc.lookupSignalEntry('FilterBank', 'clk'); + expect(clkEntry, isNotNull); + + // Two output frames. + expect(clkEntry!.outputFrames.length, 2); + expect(clkEntry.outputFrames[0].type, 'sv'); + expect(clkEntry.outputFrames[0].file, 'FilterBank.sv'); + expect(clkEntry.outputFrames[0].line, 2); + expect(clkEntry.outputFrames[0].column, 13); + expect(clkEntry.outputFrames[1].type, 'sc'); + expect(clkEntry.outputFrames[1].file, 'FilterBank.cpp'); + expect(clkEntry.outputFrames[1].line, 10); + expect(clkEntry.outputFrames[1].column, 5); + + // Backward-compat svFrame returns the first SV frame. + expect(clkEntry.svFrame, isNotNull); + expect(clkEntry.svFrame!.type, 'sv'); + expect(clkEntry.svFrame!.line, 2); + + // allFrames: output frames first (2), then ROHD (1). + expect(clkEntry.allFrames.length, 3); + }); + + test('parses multiple SV positions for same signal', () { + final json = { + 'version': 5, + 'files': ['lib/src/foo.dart'], + 'modules': { + 'Top': { + 'outputFiles': {'sv': 'Top.sv'}, + 'tree': [ + ['0:10:3', 'sig@sv:5:1;sv:20:3'], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + final entry = flc.lookupSignalEntry('Top', 'sig'); + expect(entry, isNotNull); + expect(entry!.outputFrames.length, 2); + expect(entry.outputFrames[0].line, 5); + expect(entry.outputFrames[1].line, 20); + // svFrame returns the first one. + expect(entry.svFrame!.line, 5); + }); + + test('sv frame is null when no svFile in module', () { + final json = { + 'version': 5, + 'files': ['lib/src/foo.dart'], + 'modules': { + 'Combinational': { + 'tree': [ + ['0:10:3', 'out@5:1'], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + final outEntry = flc.lookupSignalEntry('Combinational', 'out'); + expect(outEntry, isNotNull); + // No svFile/outputFiles -> output frames should be empty. + expect(outEntry!.svFrame, isNull); + expect(outEntry.outputFrames, isEmpty); + expect(outEntry.frames.length, 1); + expect(outEntry.frames[0].type, 'rohd'); + }); + + test('returns null for missing signals', () { + final json = { + 'version': 5, + 'files': ['lib/src/top.dart'], + 'modules': { + 'Top': { + 'tree': [ + ['0:1:1', 'a'], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + expect(flc.lookupSignal('Top', 'nonexistent'), isNull); + expect(flc.lookupSignal('NonexistentModule', 'a'), isNull); + expect(flc.lookupInstance('Top', 'a'), isNull); + }); + + test('returns empty FlcData when modules is null', () { + final flc = FlcData.fromJson({'version': 5, 'files': []}); + expect(flc.isEmpty, isTrue); + expect(flc.files, isEmpty); + }); + + test('returns empty FlcData when modules is empty', () { + final flc = FlcData.fromJson({'version': 5, 'files': [], 'modules': {}}); + expect(flc.isEmpty, isTrue); + }); + + test('instance with SV position and origName', () { + final json = { + 'version': 5, + 'files': ['lib/src/top.dart'], + 'modules': { + 'Top': { + 'svFile': 'Top.sv', + 'tree': [ + ['0:42:3', '*sub0@20:5~origSub'], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + final entry = flc.lookupInstanceEntry('Top', 'sub0'); + expect(entry, isNotNull); + expect(entry!.frames.length, 1); + expect(entry.frames[0].line, 42); + expect(entry.svFrame, isNotNull); + expect(entry.svFrame!.line, 20); + expect(entry.outputFrames.length, 1); + expect(entry.outputFrames[0].type, 'sv'); + expect(entry.origName, 'origSub'); + + // Fallback by origName. + final byOrig = flc.lookupInstanceEntry('Top', 'origSub'); + expect(byOrig, isNotNull); + }); + }); + + group('FlcData.empty', () { + test('creates empty instance', () { + final flc = FlcData.empty(); + expect(flc.isEmpty, isTrue); + expect(flc.files, isEmpty); + expect(flc.moduleNames, isEmpty); + expect(flc.lookupSignal('any', 'thing'), isNull); + }); + }); + + group('FlcData multi-module', () { + test('handles multiple modules with shared files', () { + final json = { + 'version': 5, + 'files': ['lib/src/shared.dart', 'lib/src/b_only.dart'], + 'modules': { + 'ModA': { + 'tree': [ + ['0:10:1', 'a'], + ], + }, + 'ModB': { + 'tree': [ + ['1:20:1', 'b'], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + expect( + flc.files, + containsAll(['lib/src/shared.dart', 'lib/src/b_only.dart']), + ); + + final aFrames = flc.lookupSignal('ModA', 'a'); + expect(aFrames, isNotNull); + expect(aFrames![0].file, 'lib/src/shared.dart'); + + final bFrames = flc.lookupSignal('ModB', 'b'); + expect(bFrames, isNotNull); + expect(bFrames![0].file, 'lib/src/b_only.dart'); + }); + }); + + group('FlcFrame edge cases', () { + test('handles frame with only file:line (no column)', () { + final json = { + 'version': 5, + 'files': ['lib/x.dart'], + 'modules': { + 'M': { + 'tree': [ + ['0:99', 's'], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + final frames = flc.lookupSignal('M', 's'); + expect(frames, isNotNull); + expect(frames![0].line, 99); + expect(frames[0].column, 1); // defaults to 1 + }); + + test('skips malformed frame strings', () { + final json = { + 'version': 5, + 'files': ['lib/x.dart'], + 'modules': { + 'M': { + 'tree': [ + [ + 'bad', + ['0:10:5', 's'] + ], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + final frames = flc.lookupSignal('M', 's'); + expect(frames, isNotNull); + // 'bad' frame is skipped since file index parse fails. + expect(frames!.length, 1); + expect(frames[0].line, 10); + }); + + test('shared trie prefix produces correct frames', () { + final json = { + 'version': 5, + 'files': ['lib/src/top.dart', 'lib/src/inner.dart'], + 'modules': { + 'Top': { + 'tree': [ + [ + '0:100:1', // shared outer frame + ['1:10:5', 'sig1'], + ['1:20:3', 'sig2'], + ], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + + // Both signals share the outer frame 0:100:1. + final sig1 = flc.lookupSignal('Top', 'sig1'); + expect(sig1, isNotNull); + expect(sig1!.length, 2); // inner + outer + // Innermost first after reversal. + expect(sig1[0].line, 10); + expect(sig1[1].line, 100); + + final sig2 = flc.lookupSignal('Top', 'sig2'); + expect(sig2, isNotNull); + expect(sig2!.length, 2); + expect(sig2[0].line, 20); + expect(sig2[1].line, 100); + }); + }); +} diff --git a/rohd_extension/package.json b/rohd_extension/package.json new file mode 100644 index 000000000..4ff066cdd --- /dev/null +++ b/rohd_extension/package.json @@ -0,0 +1,102 @@ +{ + "name": "rohd", + "displayName": "ROHD", + "description": "ROHD extension for VS Code — snippets, cross-probe source navigation, and language support for the ROHD hardware design framework.", + "version": "0.3.0", + "publisher": "rohd", + "license": "BSD-3-Clause", + "repository": { + "type": "git", + "url": "https://github.com/intel/rohd" + }, + "icon": "resources/icon.png", + "engines": { + "vscode": "^1.80.0" + }, + "keywords": [ + "rohd", + "hardware", + "rtl", + "hdl", + "cross-probe" + ], + "categories": [ + "Snippets", + "Other" + ], + "activationEvents": [ + "onCommand:rohd.openSourceLocation", + "onCommand:rohd.openSourceLocations", + "onDebugResolve:dart", + "onDebug:dart", + "onStartupFinished" + ], + "main": "./out/extension.js", + "contributes": { + "commands": [ + { + "command": "rohd.openSourceLocation", + "title": "ROHD: Go to Source Location" + }, + { + "command": "rohd.openSourceLocations", + "title": "ROHD: Go to Source Locations (multi-frame)" + }, + { + "command": "rohd.nextSourceLocation", + "title": "ROHD: Next Source Frame" + }, + { + "command": "rohd.prevSourceLocation", + "title": "ROHD: Previous Source Frame" + }, + { + "command": "rohd.connectDtd", + "title": "ROHD: Connect to Dart Tooling Daemon" + }, + { + "command": "rohd.showForwardedUris", + "title": "ROHD: Show Forwarded DTD/VM URIs" + } + ], + "configuration": { + "title": "ROHD", + "properties": { + "rohd.enableCompletions": { + "type": "boolean", + "default": null, + "description": "Enable context-aware ROHD code completions (If, Case, Pipeline, Module, etc.). Set to true/false, or leave unset to be prompted on first activation." + }, + "rohd.dtdUri": { + "type": "string", + "default": "", + "description": "WebSocket URI of the Dart Tooling Daemon (ws://...). Leave empty for auto-discovery." + } + } + }, + "snippets": [ + { + "language": "dart", + "path": "./snippets/rohd.json" + } + ] + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "lint": "eslint src --ext ts" + }, + "dependencies": { + "ws": "^8.14.0" + }, + "devDependencies": { + "@types/node": "^20.2.5", + "@types/vscode": "^1.80.0", + "@types/ws": "^8.5.5", + "@typescript-eslint/eslint-plugin": "^5.59.8", + "@typescript-eslint/parser": "^5.59.8", + "eslint": "^8.41.0", + "typescript": "^5.9.3" + } +} diff --git a/rohd_extension/resources/icon.png b/rohd_extension/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5c266214de54eb7e197607aa458735407445ac64 GIT binary patch literal 404 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^V2t*3aSW-L^Y)x0FM|OGgW(lF x-9>7P`;}KVoZDkNN&d)B$2p?}Sz)lH)^&4B>BMh49e|?>k#!dhL literal 0 HcmV?d00001 diff --git a/rohd_extension/snippets/rohd.json b/rohd_extension/snippets/rohd.json new file mode 100644 index 000000000..eabfc9d8b --- /dev/null +++ b/rohd_extension/snippets/rohd.json @@ -0,0 +1,415 @@ +{ + "ROHD: Create Module": { + "prefix": ["mod", "module", "Mod", "Module"], + "body": [ + "class $1 extends Module {", + "\t$1(Logic ${2:a}) {", + "\t// Add to Input Port", + "\t$2 = addInput('$2', $2);", + "\tfinal $3 = addOutput('${3:b}');", + "\t// Example Logic of assign a to b", + "\tb <= a;", + "\t}", + "}" + ], + "description": "ROHD: Create Module." + }, + "ROHD: Create Sequential Logic": { + "prefix": ["seq", "sequential", "Seq", "Sequential"], + "body": [ + "Sequential(${1:clk}, [", + "\tIf(${2:reset}, then: [", + "\t\t${3:out} < 0,", + "\t], orElse: [", + "\t\t${3:out} < ${4:nextVal},", + "\t]),", + "]);" + ], + "description": "Sequential(clk, [...]) — always_ff block triggered on clock edge. Contains conditional assignments using <." + }, + "ROHD: Combinational Logic": { + "prefix": ["comb", "combinational", "Comb", "Combinational"], + "body": [ + "Combinational([", + "\t${1:out} < ${2:expression},", + "]);" + ], + "description": "Combinational([...]) — always_comb block. Contains conditional assignments using <." + }, + "ROHD: Simple assign <=": { + "prefix": ["assign"], + "body": [ + "${1:srcLogic} <= ${2:destLogic};" + ], + "description": "Assignment Operator <= which used outside of the Sequential or Combinational conditional operator." + }, + "ROHD: Generate Simulation snippet.": { + "prefix": ["sim", "Simulator", "simulation"], + "body": [ + "final clk = SimpleClockGenerator(10).clk;", + "final reset = Logic(name: 'reset');", + "WaveDumper(module, outputPath: 'wavedumpername.vcd');", + "Simulator.setMaxSimTime(100);", + "unawaited(Simulator.run());", + "", + "// reset flow", + "reset.inject(0);", + "await clk.nextNegedge;", + "reset.inject(1);", + "await clk.nextNegedge;", + "await clk.nextNegedge;", + "await clk.nextNegedge;", + "reset.inject(0);", + "await clk.nextNegedge;", + "", + "// Stimulus and checking here", + "await clk.nextNegedge;", + "", + "Simulator.endSimulation();", + "await Simulator.simulationEnded;" + ], + "description": "Add simulation snippet. Normally used in performing testing." + }, + "ROHD: Counter example.": { + "prefix": ["example", "rohd", "counter"], + "body": [ + "// Import the ROHD package", + "import 'package:rohd/rohd.dart';", + "// Define a class Counter that extends ROHD's abstract Module class", + "class Counter extends Module {", + "\t// For convenience, map interesting outputs to ", + "\t// short variable names for consumers of this module", + "\tLogic get val => output('val');\n", + "\t// This counter supports any width, determined at run-time", + "\tfinal int width;", + "\tCounter(Logic en, Logic reset, Logic clk, {this.width=8, String name='counter'}) ", + "\t\t\t: super(name: name) {\n", + "\t\t// Register inputs and outputs of the module in the constructor.", + "\t\t// Module logic must consume registered inputs and output to registered outputs.", + "\t\ten = addInput('en', en);", + "\t\treset = addInput('reset', reset);", + "\t\tclk = addInput('clk', clk);", + "\t\tvar val = addOutput('val', width: width);\n", + "\t\t// A local signal named 'nextVal'", + "\t\tvar nextVal = Logic(name: 'nextVal', width: width);", + "\t\t// Assignment statement of nextVal to be val+1 (<= is the assignment operator)", + "\t\tnextVal <= val + 1;\n", + "\t\t// `Sequential` is like SystemVerilog's always_ff, ", + "\t\t// in this case trigger on the positive edge of clk", + "\t\tSequential(clk, [", + "\t\t\t// `If` is a conditional if statement, ", + "\t\t\t// like `if` in SystemVerilog always blocks", + "\t\t\tIf(reset, then:[", + "\t\t\t\t// the '<' operator is a conditional assignment", + "\t\t\t\tval < 0", + "\t\t\t], orElse: [If(en, then: [", + "\t\t\t\tval < nextVal", + "\t\t\t])])", + "\t\t]);", + "\t}", + "}" + ], + "description": "A counter example build using ROHD. Can be use as a reference or simple template to build a module. Visit https://intel.github.io/rohd-website/docs/sample-example/ for details." + }, + "ROHD: Finite State Machine (FSM)": { + "prefix": ["fsm", "FSM"], + "body": [ + "// Change stateA, stateB and stateC to your respective state", + "enum FSMState { ${1:stateA}, ${2:stateB}, ${3:stateC} }\n", + "class SampleFSMModule extends Module {", + "\tlate FiniteStateMachine _state;\n", + "\t// Modified Logics inputs based on your needs", + "\tSampleFSMModule(Logic clk, Logic reset, Logic a, Logic b)", + "\t\t\t: super(name: 'fsm_module_name') {", + "\t\tclk = addInput('clk', clk);", + "\t\treset = addInput(reset.name, reset);", + "\t\ta = addInput(a.name, a);\n", + "\t\t// The output of the fsm, modified this based on your needs", + "\t\tfinal c = addOutput('output_pin');\n", + "\t\t// Below is the example of the state transition, ", + "\t\t// modified according to your needs", + "\t\tfinal states = [", + "\t\t\t// In the state of ${1:stateA}", + "\t\t\tState(FSMState.${1:stateA}, events: {", + "\t\t\t\t// If signal b is present, go to state of ${2:stateB}", + "\t\t\t\tb: FSMState.${2:stateB},", + "\t\t\t}, actions: [", + "\t\t\t\t// You can add your respective logic output here", + "\t\t\t\t// For example, in this state, c is still 0", + "\t\t\t\t// you can add more logic here", + "\t\t\t\tc < 0,", + "\t\t\t]),", + "\t\t\t// In the state of ${2:stateB}", + "\t\t\tState(FSMState.${2:stateB}, events: {", + "\t\t\t\t// if signal a is present, go to state of ${3:stateC}", + "\t\t\t\ta: FSMState.${3:stateC}", + "\t\t\t}, actions: [", + "\t\t\t\t// You can add your respective logic output here", + "\t\t\t\t// For example, in this state, c is still 0", + "\t\t\t\t// you can add more logic here", + "\t\t\t\tc < 0,", + "\t\t\t]),", + "\t\t\t// In the state of ${3:stateC}", + "\t\t\tState(FSMState.${3:stateC}, events: {", + "\t\t\t\t// Go back to ${1:stateA}", + "\t\t\t\tConst(1): FSMState.${1:stateA}", + "\t\t\t}, actions: [", + "\t\t\t\t// You can add your respective logic output here", + "\t\t\t\t// For example, in this state, c is change to 1", + "\t\t\t\t// you can add more logic here", + "\t\t\t\tc < 1,", + "\t\t\t]),", + "\t\t];\n", + "\t\t_state = FiniteStateMachine(clk, reset, FSMState.${1:stateA}, states);\n", + "\t\t// generate state diagram", + "\t\t_state.generateDiagram(outputPath: 'sample_fsm_diagram.md');", + "\t}", + "}" + ], + "description": "An abstraction of the FSM build in ROHD. The API doc for state can be found at https://intel.github.io/rohd/rohd/FiniteStateMachine-class.html." + }, + "ROHD Testbench": { + "prefix": ["vf", "tb", "testbench"], + "body": [ + "import 'dart:async';", + "import 'dart:collection';", + "import 'package:logging/logging.dart';", + "import 'package:rohd/rohd.dart';", + "import 'package:rohd_vf/rohd_vf.dart';", + "", + "/// Main function entry point to execute this testbench.", + "Future main({Level loggerLevel = Level.FINER}) async {", + " // Set the logger level", + " Logger.root.level = loggerLevel;", + "", + " // Create the testbench", + " final tb = TopTB();", + "", + " // Build the DUT", + " await tb.dut.build();", + "", + " // Attach a waveform dumper to the DUT", + " WaveDumper(tb.dut);", + "", + " // Set a maximum simulation time so it doesn't run forever", + " Simulator.setMaxSimTime(300);", + "", + " // Create and start the test!", + " final test = DUTTest(tb.intf);", + " await test.start();", + "}", + "", + "class TopTB {", + " // TODO: Create an instance of the DUT (The Module you want to test)", + " late final ${1:dutModule} dut;", + "", + " // TODO: Build an instance of the interface for the DUT", + " final ${2:dutInterface} intf = ${2:dutInterface}();", + "", + " TopTB() {", + " // TODO(Optional): Initialized your pin here", + " // Example: intf.clk <= SimpleClockGenerator(10).clk;", + "", + " // Create the DUT, passing it our interface", + " dut = ${1:dutModule}(intf);", + " }", + "}", + "// A Test is like a top-level testing entity that contains the top testbench", + "// Env and kicks off Sequences. Only one Test should be running at a time. The", + "// Test also contains a central Random object to be used for randomization in", + "// a reproducible way.", + "class DUTTest extends Test {", + " // Interface of DUT", + " final ${2:dutInterface} intf;", + "", + " late final DUTEnv env;", + "", + " late final DUTSequencer _dutSequencer;", + "", + " DUTTest(this.intf, {String name = 'dutTest'}) : super(name) {", + " env = DUTEnv(intf, this);", + " _dutSequencer = env.agent.sequencer;", + " }", + "", + "", + " @override", + " Future run(Phase phase) async {", + " unawaited(super.run(phase));", + " final obj = phase.raiseObjection('dut_test');", + "", + " logger.info('Running the test...');", + "", + " // TODO: Register your test action with Simulator here, you can", + " // change the Simulation time.", + " // Simulator.registerAction(1, () {", + " // Example: intf.reset.put(0);", + " // });", + "", + " // TODO: Add sequenceItem to the sequencer for initialization", + " // Example: _dutSequencer.add(DUTSeqItem(false));", + "", + " // TODO: Kick start the Sequencer with n number of DUTSequence repetition", + " // Example: await _dutSequencer.start(DUTSequence(5));", + "", + " logger.info('Done adding stimulus to the sequencer');", + "", + " obj.drop();", + " }", + "}", + "", + "class DUTEnv extends Env {", + " // Interface of DUT", + " final ${2:dutInterface} intf;", + "", + " /// The agent that communicates with the DUT.", + " late final DUTAgent agent;", + "", + "", + " DUTEnv(this.intf, Component parent, {String name = 'dutEnv'})", + " : super(name, parent) {", + " agent = DUTAgent(intf, this);", + " }", + "", + " @override", + " Future run(Phase phase) async {", + " unawaited(super.run(phase));", + "", + " // TODO: You can add a listener to the output of the monitor for some logging", + " // Example:", + " // agent.valueMonitor.stream.listen((event) {", + " // logger.finer('');", + " // });", + " }", + "}", + "", + "/// An agent to bundle the sequencer, driver, and monitors for one DUT.", + "class DUTAgent extends Agent {", + " final ${2:dutInterface} intf;", + " late final DUTSequencer sequencer;", + " late final DUTDriver driver;", + " late final DUTValueMonitor valueMonitor;", + "", + " DUTAgent(this.intf, Component parent, {String name = 'dutAgent'})", + " : super(name, parent) {", + " sequencer = DUTSequencer(this);", + " driver = DUTDriver(intf, sequencer, this);", + " valueMonitor = DUTValueMonitor(intf, this);", + " }", + "}", + "", + "/// A basic [Sequencer] for the DUT.", + "class DUTSequencer extends Sequencer {", + " DUTSequencer(Component parent, {String name = 'dutSequencer'})", + " : super(name, parent);", + "}", + "", + "// A driver responisble for converting SequenceItem into signal transitions", + "// on a hardware interface.", + "class DUTDriver extends Driver {", + " final ${2:dutInterface} intf;", + "", + " final Queue _pendingItems = Queue();", + "", + " Objection? _driverObjection;", + "", + " DUTDriver(this.intf, DUTSequencer sequencer, Component parent,", + " {String name = 'dutDriver'})", + " : super(name, parent, sequencer: sequencer);", + "", + " @override", + " Future run(Phase phase) async {", + " unawaited(super.run(phase));", + "", + " // Listen to new items coming from the sequencer, and add them to a queue", + " sequencer.stream.listen((newItem) {", + " _driverObjection ??= phase.raiseObjection('dut_driver')", + " ..dropped.then((value) => logger.fine('Driver objection dropped'));", + " _pendingItems.add(newItem);", + " });", + "", + " // Every clock negative edge, drive the next pending item if it exists", + " intf.clk.negedge.listen((args) {", + " if (_pendingItems.isNotEmpty) {", + " final nextItem = _pendingItems.removeFirst();", + " drive(nextItem);", + " if (_pendingItems.isEmpty) {", + " _driverObjection?.drop();", + " _driverObjection = null;", + " }", + " }", + " });", + " }", + "", + " // Translate a SequenceItem into pin wiggles", + " // TODO: Map sequence item to respective pin", + " void drive(DUTSeqItem? item) {", + " // Example:", + " // if (item == null) {", + " // intf.en.inject(0);", + " // } else {", + " // intf.en.inject(item.en);", + " // }", + " }", + "}", + "", + "/// A monitor is responsible for watching an interface and reporting out", + "/// interesting events onto an output stream.", + "class DUTValueMonitor extends Monitor {", + " final ${2:dutInterface} intf;", + "", + " DUTValueMonitor(this.intf, Component parent,", + " {String name = 'dutValueMonitor'})", + " : super(name, parent);", + "", + " @override", + " Future run(Phase phase) async {", + " unawaited(super.run(phase));", + " await intf.reset.nextNegedge;", + "", + " intf.clk.posedge.listen((event) {", + "", + " // TODO: Add the output pin that you want to monitor here", + " // Example: add(intf.val.value);", + " });", + " }", + "}", + "", + "", + "// A Sequence is a modular object which has instructions for how to send", + "// SequenceItems to a Sequencer. A typical use case would be sending a", + "// collection of SequenceItems in a specific order.", + "class DUTSequence extends Sequence {", + " final int numRepeat;", + "", + " DUTSequence(this.numRepeat, {String name = 'dutSequence'}) : super(name);", + "", + " @override", + " Future body(Sequencer sequencer) async {", + " final dutSequencer = sequencer as DUTSequencer;", + " for (var i = 0; i < numRepeat; i++) {", + " // TODO: Add sequenceItem to the Sequence that we want to send to test", + " // Example: dutSequencer", + " // ..add(DUTSeqItem(true))", + " // ..add(DUTSeqItem(false));", + " }", + " }", + "}", + "", + "// A SequenceItem represents a collection of information to transmit across", + "// an interface. A typical use case would be an object representing", + "// a transaction to be driven over a standardized hardware interface.", + "class DUTSeqItem extends SequenceItem {", + " // TODO: Add your sequence Item", + " // Example: final bool _enable;", + "", + " // TODO: Register your input variable to the constructor", + " // Example: DUTSeqItem(this._enable);", + "", + " // TODO: Create a getter for monitoring purposes", + " // int get en => _enable ? 1 : 0;", + "}", + "" + ], + "description": "Dart Testbench Template" + } +} \ No newline at end of file diff --git a/rohd_extension/src/conditional_completions.ts b/rohd_extension/src/conditional_completions.ts new file mode 100644 index 000000000..f8aed5e26 --- /dev/null +++ b/rohd_extension/src/conditional_completions.ts @@ -0,0 +1,493 @@ +/* --------------------------------------------------------------------------- + * Copyright (C) 2026 Intel Corporation. + * SPDX-License-Identifier: BSD-3-Clause + * + * conditional_completions.ts + * Context-aware CompletionItemProvider for ROHD constructs. + * + * Three scopes: + * + * 1. FILE SCOPE — FSM (enum + class extends Module), Module scaffold + * only appear at file/top level, not inside a function like main(). + * + * 2. MODULE BODY — Pipeline, Sequential, Combinational appear when the + * cursor is inside a class that extends Module. + * + * 3. INSIDE _ALWAYS — If, If.block, Iff, ElseIf, Else, Case, CaseZ, + * CaseItem, and conditional assignment (<) only appear when the + * cursor is inside a Combinational or Sequential block. + * + * Author: Desmond Kirkpatrick + * --------------------------------------------------------------------------- */ + +import * as vscode from 'vscode'; + +// --------------------------------------------------------------------------- +// Context detection +// --------------------------------------------------------------------------- + +/** + * Determine the ROHD context at the cursor position. + * + * Returns a set of active scopes: 'file', 'module', 'always'. + */ +function detectContext( + document: vscode.TextDocument, + position: vscode.Position, +): Set { + const textBefore = document.getText( + new vscode.Range(new vscode.Position(0, 0), position), + ); + + const scopes = new Set(); + + // --- Pass 1: find brace positions for class-extends-Module and functions --- + + const classModulePattern = /class\s+\w+\s+extends\s+\w*Module\w*[^{]*\{/g; + let m: RegExpExecArray | null; + const classModuleBraces = new Set(); + while ((m = classModulePattern.exec(textBefore)) !== null) { + classModuleBraces.add(m.index + m[0].length - 1); + } + + const funcPattern = /(?:void|Future|int|bool|String|dynamic|var|final|async|static)\s+\w+\s*(?:<[^>]*>)?\s*\([^)]*\)\s*(?:async\s*)?\{/g; + const funcBraces = new Set(); + while ((m = funcPattern.exec(textBefore)) !== null) { + funcBraces.add(m.index + m[0].length - 1); + } + + // Also catch constructors: `ClassName(...) : super(...) {` or `ClassName(...) {` + const ctorPattern = /\b\w+\s*\([^)]*\)\s*(?::\s*super\([^)]*\)\s*)?\{/g; + while ((m = ctorPattern.exec(textBefore)) !== null) { + const bracePos = m.index + m[0].length - 1; + if (!classModuleBraces.has(bracePos)) { + funcBraces.add(bracePos); + } + } + + // --- Pass 2: walk through text tracking brace depth + always detection --- + + let inString = false; + let stringChar = ''; + + interface BraceFrame { type: 'module' | 'function' | 'other' } + const braceStack: BraceFrame[] = []; + + let parenDepth = 0; + let alwaysParenDepth = -1; + + for (let i = 0; i < textBefore.length; i++) { + const ch = textBefore[i]; + + // String literals. + if (inString) { + if (ch === '\\') { i++; continue; } + if (ch === stringChar) { inString = false; } + continue; + } + if (ch === "'" || ch === '"') { + inString = true; + stringChar = ch; + continue; + } + + // Line comments. + if (ch === '/' && i + 1 < textBefore.length && textBefore[i + 1] === '/') { + i += 2; + while (i < textBefore.length && textBefore[i] !== '\n') { i++; } + continue; + } + + // Block comments. + if (ch === '/' && i + 1 < textBefore.length && textBefore[i + 1] === '*') { + i += 2; + while (i + 1 < textBefore.length && + !(textBefore[i] === '*' && textBefore[i + 1] === '/')) { i++; } + i++; + continue; + } + + // Braces. + if (ch === '{') { + let type: 'module' | 'function' | 'other' = 'other'; + if (classModuleBraces.has(i)) { + type = 'module'; + } else if (funcBraces.has(i)) { + type = 'function'; + } + braceStack.push({ type }); + } else if (ch === '}') { + if (braceStack.length > 0) { + braceStack.pop(); + } + } + + // Parentheses — track Combinational/Sequential. + if (ch === '(') { + const lookback = textBefore.substring(Math.max(0, i - 30), i); + if (/(?:Combinational|Sequential)\s*$/.test(lookback)) { + alwaysParenDepth = parenDepth; + } + parenDepth++; + } else if (ch === ')') { + parenDepth--; + if (alwaysParenDepth >= 0 && parenDepth <= alwaysParenDepth) { + alwaysParenDepth = -1; + } + } + } + + // --- Determine scopes --- + + const insideAlways = alwaysParenDepth >= 0; + const insideModule = braceStack.some(f => f.type === 'module'); + const insideFunction = braceStack.some(f => f.type === 'function'); + + if (insideAlways) { scopes.add('always'); } + if (insideModule) { scopes.add('module'); } + if (!insideFunction && !insideModule) { scopes.add('file'); } + + return scopes; +} + +// --------------------------------------------------------------------------- +// Snippet definitions +// --------------------------------------------------------------------------- + +interface SnippetDef { + label: string; + prefixes: string[]; + body: string; + detail: string; + documentation: string; + sortOrder: string; +} + +// ---- ALWAYS scope ------------------------------------------------------- + +const ALWAYS_SNIPPETS: SnippetDef[] = [ + { + label: 'If (then/orElse)', + prefixes: ['If', 'if'], + body: 'If(${1:condition}, then: [\n\t${2:out} < ${3:value},\n], orElse: [\n\t${2:out} < ${4:defaultValue},\n]),', + detail: 'If(cond, then: [...], orElse: [...])', + documentation: 'Inline conditional. Maps to if/else in SystemVerilog.', + sortOrder: '0a', + }, + { + label: 'If (then only)', + prefixes: ['If', 'if', 'ifthen'], + body: 'If(${1:condition}, then: [\n\t${2:out} < ${3:value},\n]),', + detail: 'If(cond, then: [...])', + documentation: 'Simple conditional guard — no else branch.', + sortOrder: '0b', + }, + { + label: 'If nested (then/orElse chain)', + prefixes: ['If', 'if', 'ifnested', 'iforelse'], + body: 'If(${1:condA}, then: [\n\t${4:out} < ${5:valueA},\n], orElse: [If(${2:condB}, then: [\n\t${4:out} < ${6:valueB},\n], orElse: [\n\t${4:out} < ${7:defaultValue},\n])]),', + detail: 'If(a, ..., orElse: [If(b, ...)])', + documentation: 'Nested if / else-if / else chain using orElse nesting.', + sortOrder: '0c', + }, + { + label: 'If.block (Iff/ElseIf/Else)', + prefixes: ['If.block', 'ifblock', 'IfBlock'], + body: 'If.block([\n\tIff(${1:condition}, [\n\t\t${4:out} < ${5:valueA},\n\t]),\n\tElseIf(${2:condition}, [\n\t\t${4:out} < ${6:valueB},\n\t]),\n\tElse([\n\t\t${4:out} < ${7:defaultValue},\n\t]),\n]),', + detail: 'If.block([Iff(...), ElseIf(...), Else(...)])', + documentation: 'Flat if/else-if/else chain. First entry must be Iff (two f\'s).', + sortOrder: '0d', + }, + { + label: 'If.block (Iff/Else only)', + prefixes: ['If.block', 'ifblock', 'IfBlock'], + body: 'If.block([\n\tIff(${1:condition}, [\n\t\t${2:out} < ${3:value},\n\t]),\n\tElse([\n\t\t${2:out} < ${4:defaultValue},\n\t]),\n]),', + detail: 'If.block([Iff(...), Else(...)])', + documentation: 'Simple if/else using block style.', + sortOrder: '0e', + }, + { + label: 'Iff (first clause in If.block)', + prefixes: ['Iff', 'iff'], + body: 'Iff(${1:condition}, [\n\t${2:out} < ${3:value},\n]),', + detail: 'Iff(cond, [...])', + documentation: 'First clause in an If.block chain. Note: two f\'s.', + sortOrder: '1a', + }, + { + label: 'ElseIf', + prefixes: ['ElseIf', 'elseif', 'elif'], + body: 'ElseIf(${1:condition}, [\n\t${2:out} < ${3:value},\n]),', + detail: 'ElseIf(cond, [...])', + documentation: 'Middle clause in an If.block chain.', + sortOrder: '1b', + }, + { + label: 'Else', + prefixes: ['Else', 'else'], + body: 'Else([\n\t${1:out} < ${2:value},\n]),', + detail: 'Else([...])', + documentation: 'Final clause in an If.block chain.', + sortOrder: '1c', + }, + { + label: 'Case', + prefixes: ['Case', 'case'], + body: 'Case(${1:expression}, [\n\tCaseItem(${2:value1}, [\n\t\t${5:out} < ${6:result1},\n\t]),\n\tCaseItem(${3:value2}, [\n\t\t${5:out} < ${7:result2},\n\t]),\n], defaultItem: [\n\t${5:out} < ${8:defaultResult},\n], conditionalType: ConditionalType.${4|none,unique,priority|}\n),', + detail: 'Case(expr, [CaseItem(...)], ...)', + documentation: 'Case statement — maps to case/unique case/priority case in SystemVerilog.', + sortOrder: '2a', + }, + { + label: 'CaseZ', + prefixes: ['CaseZ', 'caseZ', 'casez'], + body: 'CaseZ(${1:expression}, [\n\tCaseItem(Const(LogicValue.ofString(\'${2:z1}\')), [\n\t\t${4:out} < ${5:result1},\n\t]),\n\tCaseItem(Const(LogicValue.ofString(\'${3:10}\')), [\n\t\t${4:out} < ${6:result2},\n\t]),\n], defaultItem: [\n\t${4:out} < ${7:defaultResult},\n], conditionalType: ConditionalType.${8|none,unique,priority|}\n),', + detail: 'CaseZ(expr, [CaseItem(...)])', + documentation: 'Like Case but with \'z\' don\'t-care matching.', + sortOrder: '2b', + }, + { + label: 'CaseItem', + prefixes: ['CaseItem', 'caseitem'], + body: 'CaseItem(${1:value}, [\n\t${2:out} < ${3:result},\n]),', + detail: 'CaseItem(value, [...])', + documentation: 'A single arm inside a Case or CaseZ.', + sortOrder: '2c', + }, + { + label: 'Conditional assign (<)', + prefixes: ['assign'], + body: '${1:out} < ${2:value},', + detail: 'out < value', + documentation: 'Conditional assignment using < operator.', + sortOrder: '3a', + }, +]; + +// ---- MODULE scope ------------------------------------------------------- + +const MODULE_SNIPPETS: SnippetDef[] = [ + { + label: 'Pipeline', + prefixes: ['Pipeline', 'pipeline', 'pipe'], + body: 'final ${1:pipeline} = Pipeline(${2:clk},\n\tstages: [\n\t\t(p) => [p.get(${3:a}) < p.get(${3:a}) + 1],\n\t\t(p) => [p.get(${3:a}) < p.get(${3:a}) + 1],\n\t],\n\t${4:reset: reset,}\n);\n${5:out} <= ${1:pipeline}.get(${3:a});', + detail: 'Pipeline(clk, stages: [(p) => [...], ...])', + documentation: 'Pipelined logic — each stage is a `List Function(PipelineStageInfo p)`. Use `p.get(signal)` to access pipelined values. Stages use the same conditional syntax as Combinational.', + sortOrder: '0a', + }, + { + label: 'ReadyValidPipeline', + prefixes: ['ReadyValidPipeline', 'rvpipe', 'rvpipeline'], + body: 'final ${1:pipeline} = ReadyValidPipeline(${2:clk},\n\tstages: [\n\t\t(p) => [p.get(${3:a}) < p.get(${3:a}) + 1],\n\t\t(p) => [p.get(${3:a}) < p.get(${3:a}) + 1],\n\t],\n\tvalidPipeIn: ${4:validIn},\n\treadyPipeOut: ${5:readyOut},\n);\n${6:out} <= ${1:pipeline}.get(${3:a});', + detail: 'ReadyValidPipeline(clk, stages: [...], valid, ready)', + documentation: 'Pipeline with ready/valid flow control. Same stage syntax as Pipeline, plus validPipeIn and readyPipeOut for backpressure.', + sortOrder: '0b', + }, + { + label: 'Sequential', + prefixes: ['Sequential', 'sequential', 'seq'], + body: 'Sequential(${1:clk}, [\n\tIf(${2:reset}, then: [\n\t\t${3:out} < 0,\n\t], orElse: [\n\t\t${3:out} < ${4:nextVal},\n\t]),\n]);', + detail: 'Sequential(clk, [...])', + documentation: 'always_ff block triggered on clock edge.', + sortOrder: '0c', + }, + { + label: 'Combinational', + prefixes: ['Combinational', 'combinational', 'comb'], + body: 'Combinational([\n\t${1:out} < ${2:expression},\n]);', + detail: 'Combinational([...])', + documentation: 'always_comb block.', + sortOrder: '0d', + }, +]; + +// ---- FILE scope --------------------------------------------------------- + +const FILE_SNIPPETS: SnippetDef[] = [ + { + label: 'Finite State Machine (FSM)', + prefixes: ['FSM', 'fsm'], + body: [ + 'enum ${1:MyState} { ${2:idle}, ${3:active}, ${4:done} }', + '', + 'class ${5:MyFSMModule} extends Module {', + '\tlate FiniteStateMachine<${1:MyState}> _fsm;', + '', + '\t${5:MyFSMModule}(Logic clk, Logic reset, Logic ${6:input})', + '\t\t\t: super(name: \'${7:fsm_module}\') {', + '\t\tclk = addInput(\'clk\', clk);', + '\t\treset = addInput(\'reset\', reset);', + '\t\t${6:input} = addInput(\'${6:input}\', ${6:input});', + '', + '\t\tfinal ${8:output} = addOutput(\'${8:output}\');', + '', + '\t\tfinal states = [', + '\t\t\tState(${1:MyState}.${2:idle}, events: {', + '\t\t\t\t${6:input}: ${1:MyState}.${3:active},', + '\t\t\t}, actions: [', + '\t\t\t\t${8:output} < 0,', + '\t\t\t]),', + '\t\t\tState(${1:MyState}.${3:active}, events: {', + '\t\t\t\t${6:input}: ${1:MyState}.${4:done},', + '\t\t\t}, actions: [', + '\t\t\t\t${8:output} < 1,', + '\t\t\t]),', + '\t\t\tState(${1:MyState}.${4:done}, events: {', + '\t\t\t\tConst(1): ${1:MyState}.${2:idle},', + '\t\t\t}, actions: [', + '\t\t\t\t${8:output} < 0,', + '\t\t\t]),', + '\t\t];', + '', + '\t\t_fsm = FiniteStateMachine(clk, reset, ${1:MyState}.${2:idle}, states);', + '\t}', + '}', + ].join('\n'), + detail: 'enum + class extends Module with FiniteStateMachine', + documentation: 'FSM scaffold — generates the enum and Module class at file scope.', + sortOrder: '0a', + }, + { + label: 'Module', + prefixes: ['Module', 'module', 'mod'], + body: [ + 'class ${1:MyModule} extends Module {', + '\tLogic get ${2:out} => output(\'${2:out}\');', + '', + '\t${1:MyModule}(Logic ${3:a}, {super.name = \'${4:my_module}\'}) {', + '\t\t${3:a} = addInput(\'${3:a}\', ${3:a}, width: ${3:a}.width);', + '\t\tfinal ${2:out} = addOutput(\'${2:out}\', width: ${3:a}.width);', + '', + '\t\t${2:out} <= ${3:a};', + '\t}', + '}', + ].join('\n'), + detail: 'class MyModule extends Module { ... }', + documentation: 'ROHD Module scaffold with addInput/addOutput.', + sortOrder: '0b', + }, + { + label: 'Interface (enum + setPorts + clone)', + prefixes: ['Interface', 'interface', 'intf'], + body: [ + 'enum ${1:MyDirection} { ${2:inward}, ${3:outward} }', + '', + 'class ${4:MyInterface} extends Interface<${1:MyDirection}> {', + '\tLogic get ${5:dataIn} => port(\'${5:dataIn}\');', + '\tLogic get ${6:dataOut} => port(\'${6:dataOut}\');', + '\tLogic get ${7:clk} => port(\'${7:clk}\');', + '', + '\tfinal int ${8:width};', + '\t${4:MyInterface}({this.${8:width} = 8}) {', + '\t\tsetPorts([', + '\t\t\tLogic.port(\'${5:dataIn}\', ${8:width}),', + '\t\t\tLogic.port(\'${7:clk}\'),', + '\t\t], [', + '\t\t\t${1:MyDirection}.${2:inward},', + '\t\t]);', + '', + '\t\tsetPorts([', + '\t\t\tLogic.port(\'${6:dataOut}\', ${8:width}),', + '\t\t], [', + '\t\t\t${1:MyDirection}.${3:outward},', + '\t\t]);', + '\t}', + '', + '\t@override', + '\t${4:MyInterface} clone() => ${4:MyInterface}(${8:width}: ${8:width});', + '}', + ].join('\n'), + detail: 'enum + class extends Interface with clone()', + documentation: 'Classic ROHD Interface with direction enum, setPorts grouping, port getters, and clone(). Use with Module.addInterfacePorts(intf, inputTags: {...}, outputTags: {...}).', + sortOrder: '0c', + }, + { + label: 'PairInterface (provider/consumer)', + prefixes: ['PairInterface', 'pairinterface', 'pairintf'], + body: [ + 'class ${1:MyPairInterface} extends PairInterface {', + '\tLogic get ${2:clk} => port(\'${2:clk}\');', + '\tLogic get ${3:req} => port(\'${3:req}\');', + '\tLogic get ${4:rsp} => port(\'${4:rsp}\');', + '', + '\t${1:MyPairInterface}()', + '\t\t\t: super(', + '\t\t\t\t\tportsFromProvider: [Logic.port(\'${3:req}\')],', + '\t\t\t\t\tportsFromConsumer: [Logic.port(\'${4:rsp}\')],', + '\t\t\t\t\tsharedInputPorts: [Logic.port(\'${2:clk}\')],', + '\t\t\t\t);', + '', + '\t${1:MyPairInterface}.clone(${1:MyPairInterface} super.otherInterface)', + '\t\t\t: super.clone();', + '}', + ].join('\n'), + detail: 'class extends PairInterface { ... clone() }', + documentation: 'PairInterface with provider/consumer roles and shared inputs. Use with Module.addPairInterfacePorts(intf, PairRole.provider) or PairRole.consumer.', + sortOrder: '0d', + }, +]; + +// --------------------------------------------------------------------------- +// Build completion items +// --------------------------------------------------------------------------- + +function buildItems(snippets: SnippetDef[]): vscode.CompletionItem[] { + const items: vscode.CompletionItem[] = []; + for (const snippet of snippets) { + for (const prefix of snippet.prefixes) { + const item = new vscode.CompletionItem( + prefix, + vscode.CompletionItemKind.Snippet, + ); + item.detail = `${snippet.label} — ${snippet.detail}`; + item.documentation = new vscode.MarkdownString(snippet.documentation); + item.insertText = new vscode.SnippetString(snippet.body); + item.sortText = snippet.sortOrder + prefix; + items.push(item); + } + } + return items; +} + +const alwaysItems = buildItems(ALWAYS_SNIPPETS); +const moduleItems = buildItems(MODULE_SNIPPETS); +const fileItems = buildItems(FILE_SNIPPETS); + +// --------------------------------------------------------------------------- +// Completion provider +// --------------------------------------------------------------------------- + +class RohdContextCompletionProvider + implements vscode.CompletionItemProvider +{ + provideCompletionItems( + document: vscode.TextDocument, + position: vscode.Position, + _token: vscode.CancellationToken, + _context: vscode.CompletionContext, + ): vscode.CompletionItem[] | undefined { + const scopes = detectContext(document, position); + const items: vscode.CompletionItem[] = []; + + if (scopes.has('always')) { items.push(...alwaysItems); } + if (scopes.has('module')) { items.push(...moduleItems); } + if (scopes.has('file')) { items.push(...fileItems); } + + return items.length > 0 ? items : undefined; + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +export function activate(_context: vscode.ExtensionContext): vscode.Disposable { + const provider = new RohdContextCompletionProvider(); + + return vscode.languages.registerCompletionItemProvider( + { language: 'dart', scheme: 'file' }, + provider, + 'I', 'i', 'E', 'e', 'C', 'c', 'S', 's', + 'P', 'p', 'R', 'r', 'F', 'f', 'M', 'm', 'a', + ); +} diff --git a/rohd_extension/src/debug_tracker.ts b/rohd_extension/src/debug_tracker.ts new file mode 100644 index 000000000..189cfd7cd --- /dev/null +++ b/rohd_extension/src/debug_tracker.ts @@ -0,0 +1,384 @@ +/* --------------------------------------------------------------------------- + * Copyright (C) 2026 Intel Corporation. + * SPDX-License-Identifier: BSD-3-Clause + * + * debug_tracker.ts + * Debug Adapter Tracker for the ROHD VS Code extension. + * + * Registers with VS Code's debug infrastructure via + * `registerDebugAdapterTrackerFactory` to automatically intercept Dart + * debug sessions. Extracts the VM Service URI from DAP messages and + * obtains the DTD URI from the Dart extension API — no manual + * configuration required. + * + * Author: Desmond Kirkpatrick + * --------------------------------------------------------------------------- */ + +import * as vscode from 'vscode'; +import * as dtdBridge from './dtd_bridge'; + +const output = vscode.window.createOutputChannel('ROHD Debug Tracker'); + +// --------------------------------------------------------------------------- +// State +// --------------------------------------------------------------------------- + +/** Per-session tracking data. */ +interface SessionInfo { + vmServiceUri?: string; + vmServiceForwardedUri?: string; + dtdUri?: string; + dtdForwardedUri?: string; +} + +const sessions = new Map(); + +/** The DTD URI obtained from the Dart extension API. */ +let dartExtDtdUri: string | undefined; +let dartExtDtdForwardedUri: string | undefined; + +// --------------------------------------------------------------------------- +// URI helpers (shared logic with uri_forwarder.ts) +// --------------------------------------------------------------------------- + +async function resolveForwardedUri( + rawWsUri: string, +): Promise { + try { + const httpUri = rawWsUri + .replace(/^ws:\/\//, 'http://') + .replace(/^wss:\/\//, 'https://'); + + const parsed = vscode.Uri.parse(httpUri); + const resolved = await vscode.env.asExternalUri(parsed); + + const scheme = resolved.scheme === 'https' ? 'wss' : 'ws'; + const forwarded = `${scheme}://${resolved.authority}${parsed.path}`; + + if (resolved.authority !== parsed.authority) { + return forwarded; + } + return undefined; + } catch { + return undefined; + } +} + +function normalizeWsUri(uri: string, ensureWsSuffix: boolean): string { + let u = uri; + if (u.startsWith('http://')) { + u = u.replace('http://', 'ws://'); + } else if (u.startsWith('https://')) { + u = u.replace('https://', 'wss://'); + } + if (ensureWsSuffix && !u.endsWith('/ws')) { + u = u.replace(/\/?$/, '/ws'); + } + return u; +} + +// --------------------------------------------------------------------------- +// Dart extension DTD API +// --------------------------------------------------------------------------- + +interface DartExtensionApi { + dtdUri?: string; + onDtdUriChanged?: ( + listener: (uri: string | undefined) => void, + thisArgs?: unknown, + disposables?: vscode.Disposable[], + ) => vscode.Disposable; +} + +function watchDtdFromDartExtension(context: vscode.ExtensionContext): void { + const dartExt = vscode.extensions.getExtension('dart-code.dart-code'); + if (!dartExt) { + output.appendLine('[DTD] Dart extension not found — will rely on DAP events only.'); + return; + } + + async function processDtd(rawUri: string): Promise { + const wsUri = normalizeWsUri(rawUri, false); + dartExtDtdUri = wsUri; + output.appendLine(`[DTD] From Dart extension API: ${wsUri}`); + + const forwarded = await resolveForwardedUri(wsUri); + dartExtDtdForwardedUri = forwarded; + if (forwarded) { + output.appendLine(`[DTD] Forwarded: ${forwarded}`); + } + + // Feed the original (non-forwarded) DTD URI to the bridge. + // The bridge runs on the same host as the DTD daemon, so it uses + // the local port. The forwarded port is only for remote clients. + dtdBridge.connectIfNeeded(wsUri).catch((err) => { + output.appendLine(`[DTD] Bridge connect failed: ${err}`); + }); + } + + function handleApi(api: DartExtensionApi): void { + if (api.dtdUri) { + processDtd(api.dtdUri).catch((err) => { + output.appendLine(`[DTD] Error processing DTD URI: ${err}`); + }); + } + if (api.onDtdUriChanged) { + const disposable = api.onDtdUriChanged((uri) => { + if (uri) { + processDtd(uri).catch((err) => { + output.appendLine(`[DTD] Error on DTD URI change: ${err}`); + }); + } + }); + if (disposable) { + context.subscriptions.push(disposable); + } + } + } + + if (dartExt.isActive) { + handleApi(dartExt.exports as DartExtensionApi); + } else { + dartExt.activate().then( + (api) => handleApi(api as DartExtensionApi), + (err) => output.appendLine(`[DTD] Dart extension activation failed: ${err}`), + ); + } +} + +// --------------------------------------------------------------------------- +// Banner output +// --------------------------------------------------------------------------- + +const EXTENSION_VERSION = '0.3.0'; + +async function printSessionBanner( + session: vscode.DebugSession, + info: SessionInfo, +): Promise { + const lines: string[] = []; + + lines.push(`ROHD ${EXTENSION_VERSION}: Extension loaded for FLC crossprobing.`); + lines.push(''); + + // DTD: prefer session-specific, fall back to Dart extension API + const dtdOrig = info.dtdUri ?? dartExtDtdUri; + const dtdFwd = info.dtdForwardedUri ?? dartExtDtdForwardedUri; + if (dtdOrig) { + lines.push('DTD:'); + lines.push(` URI: ${dtdOrig}`); + if (dtdFwd) { + lines.push(` Fwd: ${dtdFwd}`); + } + } + + if (info.vmServiceUri) { + lines.push('VM:'); + lines.push(` URI: ${info.vmServiceUri}`); + if (info.vmServiceForwardedUri) { + lines.push(` Fwd: ${info.vmServiceForwardedUri}`); + } + } + + const banner = '═'.repeat(60); + const block = [banner, ...lines, banner].join('\n'); + + // Debug console + vscode.debug.activeDebugConsole.appendLine(''); + vscode.debug.activeDebugConsole.appendLine(block); + + // Output channel (persists across sessions) + output.appendLine(''); + output.appendLine(block); + + // Popup with DTD URI for quick copy + const popupUri = dtdFwd ?? dtdOrig; + if (popupUri) { + const action = await vscode.window.showInformationMessage( + `ROHD DTD: ${popupUri}`, + 'Copy', + ); + if (action === 'Copy') { + await vscode.env.clipboard.writeText(popupUri); + vscode.window.showInformationMessage('DTD URI copied to clipboard.'); + } + } +} + +// --------------------------------------------------------------------------- +// Debug Adapter Tracker +// --------------------------------------------------------------------------- + +class RohdDebugAdapterTracker implements vscode.DebugAdapterTracker { + private readonly session: vscode.DebugSession; + + constructor(session: vscode.DebugSession) { + this.session = session; + } + + onWillStartSession(): void { + output.appendLine( + `[Tracker] Debug session starting: "${this.session.name}" (${this.session.id})`, + ); + sessions.set(this.session.id, {}); + } + + onDidSendMessage(message: unknown): void { + // The Dart debug adapter sends an "event" message with + // event === "dart.debuggerUris" containing the VM Service URI. + const msg = message as Record; + if (msg.type !== 'event') { return; } + + if (msg.event === 'dart.debuggerUris') { + const body = msg.body as Record | undefined; + const vmUri = body?.vmServiceUri as string | undefined; + if (vmUri) { + this.processVmServiceUri(vmUri); + } + } + } + + onWillStopSession(): void { + output.appendLine( + `[Tracker] Debug session ending: "${this.session.name}" (${this.session.id})`, + ); + sessions.delete(this.session.id); + } + + onError(error: Error): void { + output.appendLine(`[Tracker] Error in session "${this.session.name}": ${error.message}`); + } + + private async processVmServiceUri(rawUri: string): Promise { + const vmUri = normalizeWsUri(rawUri, true); + const info = sessions.get(this.session.id) ?? {}; + info.vmServiceUri = vmUri; + + output.appendLine(`[Tracker] VM Service URI: ${vmUri}`); + + const forwarded = await resolveForwardedUri(vmUri); + info.vmServiceForwardedUri = forwarded; + if (forwarded) { + output.appendLine(`[Tracker] VM Service Forwarded: ${forwarded}`); + } + + // Copy DTD info from Dart extension API into session info + if (dartExtDtdUri) { + info.dtdUri = dartExtDtdUri; + info.dtdForwardedUri = dartExtDtdForwardedUri; + + // Ensure the DTD bridge is connected now that we have a session. + // Use the original (non-forwarded) URI — bridge is local. + dtdBridge.connectIfNeeded(dartExtDtdUri).catch((err) => { + output.appendLine(`[Tracker] Bridge connect failed: ${err}`); + }); + } + + sessions.set(this.session.id, info); + await printSessionBanner(this.session, info); + } +} + +class RohdDebugAdapterTrackerFactory implements vscode.DebugAdapterTrackerFactory { + createDebugAdapterTracker( + session: vscode.DebugSession, + ): vscode.ProviderResult { + return new RohdDebugAdapterTracker(session); + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** Get session info for the active debug session. */ +export function getActiveSessionInfo(): SessionInfo | undefined { + const session = vscode.debug.activeDebugSession; + if (!session) { return undefined; } + return sessions.get(session.id); +} + +/** Get the DTD URI (forwarded preferred, original fallback). */ +export function getDtdUri(): string | undefined { + const info = getActiveSessionInfo(); + return info?.dtdForwardedUri ?? info?.dtdUri ?? dartExtDtdForwardedUri ?? dartExtDtdUri; +} + +/** Get the VM Service URI (forwarded preferred, original fallback). */ +export function getVmServiceUri(): string | undefined { + const info = getActiveSessionInfo(); + return info?.vmServiceForwardedUri ?? info?.vmServiceUri; +} + +export function activate(context: vscode.ExtensionContext): void { + output.appendLine('[Debug Tracker] Activating...'); + + // Watch for DTD URI from the Dart extension API + watchDtdFromDartExtension(context); + + // Register the tracker factory for Dart debug sessions + context.subscriptions.push( + vscode.debug.registerDebugAdapterTrackerFactory( + 'dart', + new RohdDebugAdapterTrackerFactory(), + ), + ); + + // Also register for generic debug types in case Dart sessions + // use a different type identifier + context.subscriptions.push( + vscode.debug.registerDebugAdapterTrackerFactory( + '*', + { + createDebugAdapterTracker(session: vscode.DebugSession) { + // Only track Dart-related sessions + if (session.type === 'dart' || session.type === 'flutter') { + // Already tracked by the 'dart' factory above; skip duplication. + return undefined; + } + return undefined; + }, + }, + ), + ); + + // Command to show forwarded URIs for the active session + context.subscriptions.push( + vscode.commands.registerCommand('rohd.showForwardedUris', () => { + const info = getActiveSessionInfo(); + if (!info) { + vscode.window.showWarningMessage('No active Dart debug session.'); + return; + } + + const lines: string[] = []; + if (info.dtdUri) { + lines.push(`DTD: ${info.dtdUri}`); + if (info.dtdForwardedUri) { lines.push(`DTD Fwd: ${info.dtdForwardedUri}`); } + } else if (dartExtDtdUri) { + lines.push(`DTD: ${dartExtDtdUri}`); + if (dartExtDtdForwardedUri) { lines.push(`DTD Fwd: ${dartExtDtdForwardedUri}`); } + } + if (info.vmServiceUri) { + lines.push(`VM: ${info.vmServiceUri}`); + if (info.vmServiceForwardedUri) { lines.push(`VM Fwd: ${info.vmServiceForwardedUri}`); } + } + + if (lines.length === 0) { + vscode.window.showWarningMessage('URIs not yet available. Start a debug session first.'); + return; + } + + output.appendLine(lines.join('\n')); + output.show(true); + }), + ); + + output.appendLine('[Debug Tracker] Activated — tracking Dart debug sessions.'); +} + +export function deactivate(): void { + sessions.clear(); + dartExtDtdUri = undefined; + dartExtDtdForwardedUri = undefined; +} diff --git a/rohd_extension/src/dtd_bridge.ts b/rohd_extension/src/dtd_bridge.ts new file mode 100644 index 000000000..2b0fa4b6a --- /dev/null +++ b/rohd_extension/src/dtd_bridge.ts @@ -0,0 +1,397 @@ +/* --------------------------------------------------------------------------- + * Copyright (C) 2026 Intel Corporation. + * SPDX-License-Identifier: BSD-3-Clause + * + * dtd_bridge.ts + * DTD (Dart Tooling Daemon) bridge for receiving cross-probe source + * navigation requests from the ROHD DevTools extension. + * + * Registers a `rohd.goToSource` service method on the DTD so that the + * DevTools extension can send resolved SourceFrame lists for navigation. + * + * Author: Desmond Kirkpatrick + * --------------------------------------------------------------------------- */ + +import * as vscode from 'vscode'; +import { openSourceLocations, resolveFrames } from './source_navigator'; +import * as flcService from './flc_service'; + +const output = vscode.window.createOutputChannel('ROHD DTD Bridge'); + +// --------------------------------------------------------------------------- +// Minimal JSON-RPC 2.0 server over WebSocket +// --------------------------------------------------------------------------- + +let ws: import('ws').WebSocket | undefined; +let nextId = 1; +const pendingRequests = new Map void; + reject: (error: Error) => void; +}>(); + +type RpcHandler = (params: Record) => Promise; +const methods = new Map(); + +function dtdResult(result: Record): Record { + return { type: 'Success', ...result }; +} + +function sendJsonRpc(data: Record): void { + if (ws?.readyState === 1 /* OPEN */) { + ws.send(JSON.stringify(data)); + } +} + +function handleMessage(raw: string): void { + let msg: Record; + try { + msg = JSON.parse(raw); + } catch { + return; + } + + // Response to a request we sent. + if ('id' in msg && ('result' in msg || 'error' in msg)) { + const id = msg.id as number; + const pending = pendingRequests.get(id); + if (pending) { + pendingRequests.delete(id); + if ('error' in msg) { + pending.reject(new Error(JSON.stringify(msg.error))); + } else { + pending.resolve(msg.result); + } + } + return; + } + + // Incoming request or notification. + if ('method' in msg) { + const method = msg.method as string; + const params = (msg.params ?? {}) as Record; + const handler = methods.get(method); + + if (handler && 'id' in msg) { + // Request — send response. + handler(params) + .then(result => sendJsonRpc({ jsonrpc: '2.0', id: msg.id, result })) + .catch(err => + sendJsonRpc({ + jsonrpc: '2.0', + id: msg.id, + error: { code: -32000, message: String(err) }, + }), + ); + } else if (handler) { + // Notification — fire and forget. + handler(params).catch(() => {}); + } + } +} + +/** Send a JSON-RPC request and wait for the response. */ +async function rpcRequest( + method: string, + params?: Record, +): Promise { + return new Promise((resolve, reject) => { + const id = nextId++; + pendingRequests.set(id, { resolve, reject }); + sendJsonRpc({ jsonrpc: '2.0', id, method, params: params ?? {} }); + + // Timeout after 10 seconds. + setTimeout(() => { + if (pendingRequests.has(id)) { + pendingRequests.delete(id); + reject(new Error(`RPC timeout: ${method}`)); + } + }, 10000); + }); +} + +// --------------------------------------------------------------------------- +// Service registration +// --------------------------------------------------------------------------- + +/** + * Register the `rohd.goToSource` handler. + * + * When the DevTools extension calls this service via DTD, the handler + * parses the SourceFrame list and delegates to the existing + * `openSourceLocations` command. + */ +function registerGoToSourceHandler(): void { + methods.set('rohd.goToSource', async (params) => { + const framesRaw = params.frames; + if (!Array.isArray(framesRaw) || framesRaw.length === 0) { + return dtdResult({ status: 'error', message: 'No frames provided' }); + } + + const index = typeof params.index === 'number' ? params.index : 0; + + output.appendLine( + `[DTD] goToSource: ${framesRaw.length} frame(s), index=${index}`, + ); + + // Delegate to the existing source navigator. + await openSourceLocations({ frames: framesRaw, index }); + return dtdResult({ status: 'ok', navigated: framesRaw.length }); + }); + + methods.set('rohd.resolveFrames', async (params) => { + const framesRaw = params.frames; + if (!Array.isArray(framesRaw) || framesRaw.length === 0) { + return dtdResult({ status: 'error', message: 'No frames provided' }); + } + + output.appendLine( + `[DTD] resolveFrames: ${framesRaw.length} frame(s)`, + ); + + const enriched = await resolveFrames(framesRaw); + return dtdResult({ status: 'ok', frames: enriched }); + }); + + // Query which source formats are available for a module. + // params: { flcPath: string, module: string | null } + methods.set('rohd.queryModule', async (params) => { + const flcPath = params.flcPath as string | undefined; + const moduleName = (params.module as string | null) ?? null; + + if (!flcPath) { + return dtdResult({ status: 'error', message: 'flcPath is required' }); + } + + output.appendLine(`[DTD] queryModule: module=${moduleName ?? '(any)'}, flcPath=${flcPath}`); + const info = flcService.queryModule(flcPath, moduleName); + return dtdResult({ status: 'ok', ...info }); + }); + + // Look up signal source frames from a persisted .flc.json file. + // params: { flcPath: string, module: string | null, signal: string, format?: string } + methods.set('rohd.lookupSignal', async (params) => { + const flcPath = params.flcPath as string | undefined; + const moduleName = (params.module as string | null) ?? null; + const signalName = params.signal as string | undefined; + const format = params.format as string | undefined; + + if (!flcPath || !signalName) { + return dtdResult({ status: 'error', message: 'flcPath and signal are required' }); + } + + output.appendLine( + `[DTD] lookupSignal: signal=${signalName}, module=${moduleName ?? '(any)'}, format=${format ?? 'all'}`, + ); + const frames = flcService.lookupSignal(flcPath, moduleName, signalName, format); + return dtdResult({ status: 'ok', frames }); + }); +} + +// --------------------------------------------------------------------------- +// DTD connection lifecycle +// --------------------------------------------------------------------------- + +let reconnectTimer: ReturnType | undefined; +let reconnectAttempts = 0; +let dtdUri: string | undefined; + +/** + * Connect to the DTD and register services. + * + * @param uri WebSocket URI of the Dart Tooling Daemon. + */ +async function connectToDtd(uri: string): Promise { + dtdUri = uri; + + try { + // Dynamic import — ws is a Node.js dependency. + const WebSocket = (await import('ws')).default; + + ws = new WebSocket(uri); + + return new Promise((resolve) => { + ws!.on('open', () => { + output.appendLine(`[DTD] Connected to ${uri}`); + reconnectAttempts = 0; + + registerGoToSourceHandler(); + + // Register ourselves as a service on the DTD. + rpcRequest('registerService', { + service: 'rohd', + method: 'goToSource', + }).then(() => { + output.appendLine('[DTD] Registered rohd.goToSource service'); + }).catch((err) => { + output.appendLine(`[DTD] Service registration note: ${err}`); + }); + + rpcRequest('registerService', { + service: 'rohd', + method: 'resolveFrames', + }).then(() => { + output.appendLine('[DTD] Registered rohd.resolveFrames service'); + }).catch((err) => { + output.appendLine(`[DTD] resolveFrames registration note: ${err}`); + }); + + rpcRequest('registerService', { + service: 'rohd', + method: 'queryModule', + }).then(() => { + output.appendLine('[DTD] Registered rohd.queryModule service'); + }).catch((err) => { + output.appendLine(`[DTD] queryModule registration note: ${err}`); + }); + + rpcRequest('registerService', { + service: 'rohd', + method: 'lookupSignal', + }).then(() => { + output.appendLine('[DTD] Registered rohd.lookupSignal service'); + }).catch((err) => { + output.appendLine(`[DTD] lookupSignal registration note: ${err}`); + }); + + resolve(true); + }); + + ws!.on('message', (data: Buffer | string) => { + handleMessage(data.toString()); + }); + + ws!.on('close', () => { + output.appendLine('[DTD] Connection closed'); + ws = undefined; + scheduleReconnect(); + }); + + ws!.on('error', (err: Error) => { + output.appendLine(`[DTD] Connection error: ${err.message}`); + ws = undefined; + resolve(false); + }); + }); + } catch (err) { + output.appendLine(`[DTD] Failed to connect: ${err}`); + return false; + } +} + +function scheduleReconnect(): void { + if (!dtdUri || reconnectTimer) return; + + // Exponential backoff: 2s, 4s, 8s, 16s, 32s max. + const delay = Math.min(2000 * 2 ** reconnectAttempts, 32000); + reconnectAttempts++; + + output.appendLine(`[DTD] Reconnecting in ${delay / 1000}s...`); + reconnectTimer = setTimeout(async () => { + reconnectTimer = undefined; + if (dtdUri) { + await connectToDtd(dtdUri); + } + }, delay); +} + +// --------------------------------------------------------------------------- +// DTD URI discovery +// --------------------------------------------------------------------------- + +/** + * Try to discover the DTD URI from known sources. + * + * Order of precedence: + * 1. `DART_TOOLING_DAEMON_URI` environment variable + * 2. VS Code setting `rohd.dtdUri` (for manual override) + * + * Note: The Dart extension API DTD URI is discovered asynchronously by + * debug_tracker.ts and fed to connectIfNeeded() when available. + */ +function discoverDtdUri(): string | undefined { + // Environment variable (set by IDE or debug launcher). + const envUri = process.env.DART_TOOLING_DAEMON_URI; + if (envUri) { + output.appendLine(`[DTD] URI from env: ${envUri}`); + return envUri; + } + + // VS Code setting. + const config = vscode.workspace.getConfiguration('rohd'); + const configUri = config.get('dtdUri'); + if (configUri) { + output.appendLine(`[DTD] URI from setting: ${configUri}`); + return configUri; + } + + output.appendLine('[DTD] No DTD URI discovered (debug_tracker will push later)'); + return undefined; +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** Whether the DTD bridge is connected. */ +export function isConnected(): boolean { + return ws?.readyState === 1; +} + +/** + * Connect to the DTD if not already connected. + * Called by debug_tracker when the DTD URI is discovered from the Dart + * extension API (which resolves after activation). + */ +export async function connectIfNeeded(uri: string): Promise { + if (isConnected()) { + return; // Already connected — nothing to do. + } + output.appendLine(`[DTD] Connecting via debug tracker: ${uri}`); + await connectToDtd(uri); +} + +/** + * Activate the DTD bridge. + * + * Discovers the DTD URI and connects. If the URI is not available + * at activation time, the bridge remains dormant and can be connected + * later via the `rohd.connectDtd` command. + */ +export async function activate(context: vscode.ExtensionContext): Promise { + // Register a command for manual DTD connection. + context.subscriptions.push( + vscode.commands.registerCommand('rohd.connectDtd', async () => { + const uri = await vscode.window.showInputBox({ + prompt: 'Enter the Dart Tooling Daemon WebSocket URI', + placeHolder: 'ws://127.0.0.1:...', + value: dtdUri ?? '', + }); + if (uri) { + await connectToDtd(uri); + } + }), + ); + + // Try automatic discovery. + const uri = discoverDtdUri(); + if (uri) { + await connectToDtd(uri); + } +} + +/** Clean up the DTD bridge. */ +export async function dispose(): Promise { + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = undefined; + } + dtdUri = undefined; + + if (ws) { + ws.close(); + ws = undefined; + } + + pendingRequests.clear(); + methods.clear(); +} diff --git a/rohd_extension/src/extension.ts b/rohd_extension/src/extension.ts new file mode 100644 index 000000000..0957ff572 --- /dev/null +++ b/rohd_extension/src/extension.ts @@ -0,0 +1,129 @@ +/* --------------------------------------------------------------------------- + * Copyright (C) 2026 Intel Corporation. + * SPDX-License-Identifier: BSD-3-Clause + * + * extension.ts + * Main entry point for the ROHD VS Code extension. + * + * Provides: + * - ROHD Dart code snippets (carried forward from v0.0.5) + * - Cross-probe source navigation commands for ROHD viewer extensions + * + * Original snippets by: Yao Jing Quek + * Cross-probe navigation by: Desmond Kirkpatrick + * --------------------------------------------------------------------------- */ + +import * as vscode from 'vscode'; +import * as sourceNavigator from './source_navigator'; +import * as dtdBridge from './dtd_bridge'; +import * as debugTracker from './debug_tracker'; +import * as conditionalCompletions from './conditional_completions'; +import * as flcService from './flc_service'; + +export function activate(context: vscode.ExtensionContext): void { + console.log('ROHD extension is now active (v0.3.0)'); + + // Initialise the FLC service with the extension path so it can locate + // the compiled flc_lookup binary. + flcService.initialize(context.extensionPath); + + // Register cross-probe → editor navigation commands. + // These are invoked by ROHD viewer extensions (schematic, wave) via + // vscode.commands.executeCommand('rohd.openSourceLocation', ...). + sourceNavigator.registerCommands(context); + + // ── FLC commands — viewer extensions delegate here ────────────────────── + + // rohd.queryModule: resolve available source formats for a module. + // Args: { flcPath: string, module: string | null } + // Returns: ModuleInfo (extensionAvailable, module, formats, ...) + context.subscriptions.push( + vscode.commands.registerCommand( + 'rohd.queryModule', + (args: { flcPath: string; module: string | null }) => + flcService.queryModule(args.flcPath, args.module), + ), + ); + + // rohd.lookupSignal: resolve source frames for a signal. + // Args: { flcPath: string, module: string | null, signal: string, format?: string } + // Returns: SourceFrame[] + context.subscriptions.push( + vscode.commands.registerCommand( + 'rohd.lookupSignal', + (args: { flcPath: string; module: string | null; signal: string; format?: string }) => + flcService.lookupSignal(args.flcPath, args.module, args.signal, args.format), + ), + ); + + // rohd.resolveFlcPath: find the .flc.json sidecar for a document path. + // Args: { documentFsPath: string } + // Returns: string | null + context.subscriptions.push( + vscode.commands.registerCommand( + 'rohd.resolveFlcPath', + (args: { documentFsPath: string }) => + flcService.resolveFlcPath(args.documentFsPath), + ), + ); + + // Activate the DTD bridge for receiving cross-probe requests from + // the ROHD DevTools extension running in a remote iframe. + dtdBridge.activate(context); + + // Register with the debug adapter to automatically intercept Dart + // debug sessions, capture VM Service + DTD URIs, and print the + // consolidated banner. Replaces the old passive uri_forwarder. + debugTracker.activate(context); + + // Register context-aware ROHD completions if the user has opted in. + activateCompletionsIfEnabled(context); + + // Re-check when the setting changes at runtime. + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('rohd.enableCompletions')) { + activateCompletionsIfEnabled(context); + } + }), + ); +} + +let completionsDisposable: vscode.Disposable | undefined; + +async function activateCompletionsIfEnabled( + context: vscode.ExtensionContext, +): Promise { + const config = vscode.workspace.getConfiguration('rohd'); + let enabled: boolean | undefined = config.get('enableCompletions') ?? undefined; + + if (enabled === undefined) { + // First time — ask the user. + const choice = await vscode.window.showInformationMessage( + 'Enable ROHD context-aware code completions?', + 'Yes', 'No', + ); + if (choice === 'Yes') { + enabled = true; + } else if (choice === 'No') { + enabled = false; + } else { + return; // Dismissed — ask again next time. + } + await config.update('enableCompletions', enabled, vscode.ConfigurationTarget.Global); + } + + if (enabled && !completionsDisposable) { + completionsDisposable = conditionalCompletions.activate(context); + context.subscriptions.push(completionsDisposable); + } else if (!enabled && completionsDisposable) { + completionsDisposable.dispose(); + completionsDisposable = undefined; + } +} + +export function deactivate(): void { + debugTracker.deactivate(); + dtdBridge.dispose(); + sourceNavigator.dispose(); +} diff --git a/rohd_extension/src/flc_service.ts b/rohd_extension/src/flc_service.ts new file mode 100644 index 000000000..ff46245ea --- /dev/null +++ b/rohd_extension/src/flc_service.ts @@ -0,0 +1,515 @@ +/* --------------------------------------------------------------------------- + * Copyright (C) 2026 Intel Corporation. + * SPDX-License-Identifier: BSD-3-Clause + * + * flc_service.ts + * Centralised FLC (File-Location Cache) service for rohd_extension. + * + * Owns: + * - Resolving the .flc.json sidecar path from any document path + * - Querying available source formats (rohd, sv, …) for a module + * - Looking up signal/instance source frames from v5/v6 FLC JSON + * + * All FLC parsing logic that was previously duplicated across + * rohd-schematic-viewer/extension.js and rohd-wave-viewer/extension.ts + * now lives here. Those extensions delegate via VS Code commands: + * rohd.queryModule – returns ModuleInfo + * rohd.lookupSignal – returns SourceFrame[] + * + * Author: Desmond Kirkpatrick + * --------------------------------------------------------------------------- */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; + +const output = vscode.window.createOutputChannel('ROHD FLC'); + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface FormatInfo { + available: boolean; + fileFound: boolean; + path: string | null; +} + +export interface ModuleFormats { + rohd?: FormatInfo; + sv?: FormatInfo; + [key: string]: FormatInfo | undefined; +} + +export interface ModuleInfo { + extensionAvailable: boolean; + module: string | null; + formats: ModuleFormats; + error?: string; + fstLoading: boolean; +} + +export interface SourceFrame { + file: string; + line: number; + col: number; + desc?: string; + type: string; +} + +interface FlcOutputPos { + type: string; + line: number; + col: number; +} + +interface FlcSymbolInfo { + name: string; + isInstance: boolean; + outputPositions: FlcOutputPos[]; + origName: string | null; +} + +// --------------------------------------------------------------------------- +// Initialisation +// --------------------------------------------------------------------------- + +let _extensionPath: string | undefined; + +/** Must be called from extension activate() before any other API. */ +export function initialize(extensionPath: string): void { + _extensionPath = extensionPath; + output.appendLine('[FlcService] Initialised. extensionPath=' + extensionPath); +} + +// --------------------------------------------------------------------------- +// Sidecar resolution +// --------------------------------------------------------------------------- + +/** + * Resolve the .flc.json sidecar for any document path. + * + * Conventions supported: + * Foo.rohd.json → Foo.flc.json (schematic viewer) + * Foo.vcd → Foo.flc.json (wave viewer) + * Foo.fst → Foo.flc.json + * Foo.ghw → Foo.flc.json + * + * Returns the absolute path if the sidecar exists, otherwise null. + */ +export function resolveFlcPath(documentFsPath: string): string | null { + const dir = path.dirname(documentFsPath); + const base = path.basename(documentFsPath); + + // .rohd.json → .flc.json + const fromRohdJson = base.replace(/\.rohd\.json$/i, '.flc.json'); + if (fromRohdJson !== base) { + const p = path.join(dir, fromRohdJson); + return fs.existsSync(p) ? p : null; + } + + // .vcd / .fst / .ghw → .flc.json + const fromWave = base.replace(/\.(vcd|fst|ghw)$/i, '.flc.json'); + if (fromWave !== base) { + const p = path.join(dir, fromWave); + return fs.existsSync(p) ? p : null; + } + + return null; +} + +// --------------------------------------------------------------------------- +// Module format query +// --------------------------------------------------------------------------- + +/** + * Return which source formats are available for [moduleName] in [flcPath]. + * + * Reads and parses the FLC JSON directly (no subprocess needed for metadata). + * Performs case-insensitive module name matching. + */ +export function queryModule(flcPath: string, moduleName: string | null): ModuleInfo { + if (!fs.existsSync(flcPath)) { + return { + extensionAvailable: true, + module: moduleName, + formats: {}, + error: 'FLC file not found: ' + flcPath, + fstLoading: false, + }; + } + + try { + const raw = fs.readFileSync(flcPath, 'utf8'); + const flcJson = JSON.parse(raw) as Record; + const modules = (flcJson['modules'] ?? {}) as Record; + const docDir = path.dirname(flcPath); + + // `files` entries (ROHD Dart sources) are stored relative to the package + // root (e.g. `.dart_tool/../lib/src/...`), NOT relative to the directory + // that holds the `.flc.json` (which is typically a `build/` output dir). + // Resolve against `packageRoot` when present, then fall back to `docDir`. + const packageRoot = + typeof flcJson['packageRoot'] === 'string' + ? (flcJson['packageRoot'] as string) + : null; + const resolveSourcePath = (relPath: string): string => { + const bases = packageRoot ? [packageRoot, docDir] : [docDir]; + for (const base of bases) { + const candidate = path.resolve(base, relPath); + if (fs.existsSync(candidate)) { + return candidate; + } + } + // None existed — return the best-guess canonical path (packageRoot-based + // when available) so the reported path is meaningful. + return path.resolve(bases[0], relPath); + }; + + // Accept exact match or case-insensitive prefix match. + let modData: Record | null = null; + if (moduleName && modules[moduleName]) { + modData = modules[moduleName] as Record; + } else if (moduleName) { + const lc = moduleName.toLowerCase(); + for (const [k, v] of Object.entries(modules)) { + if (k.toLowerCase() === lc || k.toLowerCase().startsWith(lc + '_')) { + modData = v as Record; + break; + } + } + } + + const formats: ModuleFormats = {}; + + if (modData) { + // ROHD Dart source: trie tree non-empty + at least one global .dart file. + const tree = modData['tree']; + const hasRohdTree = Array.isArray(tree) && tree.length > 0; + const globalFiles = (flcJson['files'] ?? []) as string[]; + const hasRohd = hasRohdTree && globalFiles.length > 0; + + if (hasRohd) { + const rohdFile = globalFiles.find(f => f.endsWith('.dart')); + const rohdPath = rohdFile ? resolveSourcePath(rohdFile) : null; + formats['rohd'] = { + available: true, + fileFound: rohdPath ? fs.existsSync(rohdPath) : false, + path: rohdPath, + }; + } + + // Output-language files (sv, sc, …) from outputFiles map. + // v6: Record (list per language). + // v5 legacy: Record. + const rawOutputFiles = + (modData['outputFiles'] ?? {}) as Record; + const outputFiles: Record = {}; + for (const [lang, val] of Object.entries(rawOutputFiles)) { + if (Array.isArray(val)) { + if (val.length > 0 && typeof val[0] === 'string') { + outputFiles[lang] = val[0] as string; + } + } else if (typeof val === 'string') { + outputFiles[lang] = val; + } + } + // Legacy single svFile field. + if (!outputFiles['sv'] && modData['svFile']) { + outputFiles['sv'] = modData['svFile'] as string; + } + for (const [lang, relPath] of Object.entries(outputFiles)) { + const absPath = path.resolve(docDir, relPath); + formats[lang] = { + available: true, + fileFound: fs.existsSync(absPath), + path: absPath, + }; + } + } else { + output.appendLine( + '[FlcService] queryModule: "' + moduleName + + '" not found. Available: ' + Object.keys(modules).slice(0, 10).join(', '), + ); + } + + return { + extensionAvailable: true, + module: moduleName, + formats, + fstLoading: false, + }; + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + output.appendLine('[FlcService] queryModule error: ' + msg); + return { + extensionAvailable: true, + module: moduleName, + formats: {}, + error: 'Error reading FLC: ' + msg, + fstLoading: false, + }; + } +} + +// --------------------------------------------------------------------------- +// Signal lookup +// --------------------------------------------------------------------------- + +/** + * Look up source frames for [signalName] in [moduleName]. + * + * When [format] is provided, only frames of that type ('rohd', 'sv', 'sc', ...) + * are returned. Pass null/undefined to return all formats. + */ +export function lookupSignal( + flcPath: string, + moduleName: string | null, + signalName: string, + format?: string, +): SourceFrame[] { + if (!fs.existsSync(flcPath)) { + output.appendLine('[FlcService] lookupSignal: FLC not found: ' + flcPath); + return []; + } + + output.appendLine( + '[FlcService] lookupSignal: ' + flcPath + + ' module=' + (moduleName ?? '(any)') + + ' signal=' + signalName + + ' format=' + (format ?? 'all'), + ); + + try { + const raw = fs.readFileSync(flcPath, 'utf8'); + const flcJson = JSON.parse(raw) as Record; + const frames = lookupSignalInJson(flcJson, path.dirname(flcPath), moduleName, signalName); + const filtered = format ? frames.filter(f => f.type === format) : frames; + output.appendLine( + '[FlcService] lookupSignal: ' + filtered.length + ' frame(s)' + + (format ? ' after ' + format + ' filter' : ''), + ); + if (filtered.length === 0 && frames.length > 0 && format) { + output.appendLine( + '[FlcService] lookupSignal: available frame types=' + + Array.from(new Set(frames.map(f => f.type))).join(', '), + ); + } + return filtered; + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + output.appendLine('[FlcService] lookupSignal failed: ' + msg); + return []; + } +} + +function lookupSignalInJson( + flcJson: Record, + flcDir: string, + moduleName: string | null, + signalName: string, +): SourceFrame[] { + const modules = asRecord(flcJson['modules']); + if (!modules) { return []; } + + const moduleNames = moduleName ? matchingModuleNames(modules, moduleName) : Object.keys(modules); + for (const modName of moduleNames) { + const modData = asRecord(modules[modName]); + if (!modData) { continue; } + const frames = lookupSignalInModule(flcJson, flcDir, modData, signalName); + if (frames.length > 0) { + return frames; + } + } + + if (moduleName) { + output.appendLine( + '[FlcService] lookupSignal: no match for ' + moduleName + '/' + signalName + + '. Available modules: ' + Object.keys(modules).slice(0, 10).join(', '), + ); + } + return []; +} + +function lookupSignalInModule( + flcJson: Record, + flcDir: string, + modData: Record, + signalName: string, +): SourceFrame[] { + const tree = modData['tree']; + if (!Array.isArray(tree)) { return []; } + + const files = Array.isArray(flcJson['files']) + ? (flcJson['files'] as unknown[]).filter((f): f is string => typeof f === 'string') + : []; + const outputFiles = getOutputFiles(modData); + + let origNameMatch: SourceFrame[] = []; + + const walkNode = (node: unknown[], pathFrames: string[]): SourceFrame[] => { + if (node.length === 0 || typeof node[0] !== 'string') { return []; } + const currentPath = [...pathFrames, node[0] as string]; + + for (let i = 1; i < node.length; i++) { + const elem = node[i]; + if (Array.isArray(elem)) { + const found = walkNode(elem, currentPath); + if (found.length > 0) { return found; } + } else if (typeof elem === 'string') { + const parsed = parseSymbolString(elem); + const frames = entryToFrames(parsed, currentPath, files, outputFiles, flcDir, signalName); + if (parsed.name === signalName) { + return frames; + } + if (parsed.origName === signalName && origNameMatch.length === 0) { + origNameMatch = frames; + } + } + } + + return []; + }; + + for (const rootNode of tree) { + if (!Array.isArray(rootNode)) { continue; } + const found = walkNode(rootNode, []); + if (found.length > 0) { return found; } + } + return origNameMatch; +} + +function entryToFrames( + symbol: FlcSymbolInfo, + pathFrames: string[], + files: string[], + outputFiles: Record, + flcDir: string, + signalName: string, +): SourceFrame[] { + const frames: SourceFrame[] = []; + + for (const frame of [...pathFrames].reverse()) { + const parts = frame.split(':'); + if (parts.length < 2) { continue; } + const fileIndex = Number.parseInt(parts[0], 10); + if (!Number.isInteger(fileIndex) || fileIndex < 0 || fileIndex >= files.length) { continue; } + frames.push({ + file: files[fileIndex], + line: Number.parseInt(parts[1], 10) || 1, + col: parts.length > 2 ? (Number.parseInt(parts[2], 10) || 1) : 1, + desc: signalName + ' [ROHD]', + type: 'rohd', + }); + } + + for (const pos of symbol.outputPositions) { + const outputFile = outputFiles[pos.type]; + if (!outputFile) { continue; } + frames.push({ + file: path.resolve(flcDir, outputFile), + line: pos.line, + col: pos.col, + desc: signalName + ' [' + pos.type.toUpperCase() + ']', + type: pos.type, + }); + } + + return frames; +} + +function getOutputFiles(modData: Record): Record { + const outputFiles: Record = {}; + const svFile = modData['svFile']; + if (typeof svFile === 'string') { + outputFiles['sv'] = svFile; + } + + const rawOutputFiles = asRecord(modData['outputFiles']); + if (!rawOutputFiles) { return outputFiles; } + + for (const [lang, val] of Object.entries(rawOutputFiles)) { + if (typeof val === 'string') { + outputFiles[lang] = val; + } else if (Array.isArray(val)) { + const first = val.find((item): item is string => typeof item === 'string'); + if (first) { + outputFiles[lang] = first; + } + } + } + return outputFiles; +} + +function parseSymbolString(symbol: string): FlcSymbolInfo { + const isInstance = symbol.startsWith('*'); + let rest = isInstance ? symbol.substring(1) : symbol; + + let origName: string | null = null; + const tildeIdx = rest.indexOf('~'); + if (tildeIdx >= 0) { + origName = rest.substring(tildeIdx + 1); + rest = rest.substring(0, tildeIdx); + } + + const outputPositions: FlcOutputPos[] = []; + const atIdx = rest.indexOf('@'); + if (atIdx >= 0) { + const positions = rest.substring(atIdx + 1); + rest = rest.substring(0, atIdx); + outputPositions.push(...parseOutputPositions(positions)); + } + + return { name: rest, isInstance, outputPositions, origName }; +} + +function parseOutputPositions(positions: string): FlcOutputPos[] { + const result: FlcOutputPos[] = []; + for (const group of positions.split(';')) { + if (!group) { continue; } + const entries = group.split(','); + let groupLang: string | null = null; + for (let i = 0; i < entries.length; i++) { + let part = entries[i]; + if (!part) { continue; } + if (i === 0) { + const segments = part.split(':'); + const firstIsTag = segments.length >= 3 && Number.isNaN(Number.parseInt(segments[0], 10)); + if (firstIsTag) { + groupLang = segments[0]; + part = segments.slice(1).join(':'); + } + } + const type = groupLang ?? 'sv'; + const segments = part.split(':'); + const lineText = segments.length >= 2 ? segments[segments.length - 2] : segments[0]; + const colText = segments.length >= 2 ? segments[segments.length - 1] : undefined; + result.push({ + type, + line: Number.parseInt(lineText, 10) || 1, + col: colText ? (Number.parseInt(colText, 10) || 1) : 1, + }); + } + } + return result; +} + +function matchingModuleNames(modules: Record, moduleName: string): string[] { + if (Object.prototype.hasOwnProperty.call(modules, moduleName)) { + return [moduleName]; + } + + const lower = moduleName.toLowerCase(); + const matches = Object.keys(modules).filter((name) => { + const candidate = name.toLowerCase(); + return candidate === lower || candidate.startsWith(lower + '_'); + }); + return matches; +} + +function asRecord(value: unknown): Record | null { + if (value && typeof value === 'object' && !Array.isArray(value)) { + return value as Record; + } + return null; +} diff --git a/rohd_extension/src/source_navigator.ts b/rohd_extension/src/source_navigator.ts new file mode 100644 index 000000000..dc727e774 --- /dev/null +++ b/rohd_extension/src/source_navigator.ts @@ -0,0 +1,482 @@ +/* --------------------------------------------------------------------------- + * Copyright (C) 2026 Intel Corporation. + * SPDX-License-Identifier: BSD-3-Clause + * + * source_navigator.ts + * Cross-probe → VS Code editor navigation module. + * + * Receives source location requests from ROHD viewer extensions (schematic, + * wave) and navigates the VS Code editor to the corresponding file/line/col. + * Supports multi-frame stack traces with cycling. + * + * Author: Desmond Kirkpatrick + * --------------------------------------------------------------------------- */ + +import * as vscode from 'vscode'; + +const output = vscode.window.createOutputChannel('ROHD Source Navigator'); + +/** A single source location frame. */ +export interface SourceFrame { + /** File path (package-relative, e.g. `lib/src/foo.dart`). */ + file: string; + /** 1-based line number. */ + line: number; + /** 1-based column number. */ + col: number; + /** Optional description (e.g. function name from stack trace). */ + desc?: string; + /** Frame type: 'sv' for SystemVerilog, 'rohd' for ROHD Dart source. */ + type?: string; +} + +// --------------------------------------------------------------------------- +// Frame cycling state +// --------------------------------------------------------------------------- + +let currentFrames: SourceFrame[] = []; +let currentFrameIndex = 0; +let statusBarItem: vscode.StatusBarItem | undefined; +let statusBarTimeout: ReturnType | undefined; + +// Highlight decoration for the target symbol (yellow flash). +const highlightDecoration = vscode.window.createTextEditorDecorationType({ + backgroundColor: 'rgba(255, 213, 79, 0.35)', +}); +let highlightTimeout: ReturnType | undefined; + +// --------------------------------------------------------------------------- +// Public API — called via vscode.commands.executeCommand() +// --------------------------------------------------------------------------- + +/** + * Navigate to a single source location. + * + * @param args `{ file: string, line: number, col: number }` + */ +export async function openSourceLocation( + args: { file: string; line: number; col: number }, +): Promise { + if (!args || !args.file) { + vscode.window.showWarningMessage('ROHD: No source location provided.'); + return; + } + // Clear any previous frame cycling state. + currentFrames = [{ file: args.file, line: args.line, col: args.col }]; + currentFrameIndex = 0; + hideStatusBar(); + await navigateToFrame(currentFrames[0]); +} + +/** + * Navigate to the first of multiple source location frames and enable + * cycling through them. + * + * @param args `{ frames: SourceFrame[], index?: number }` + */ +export async function openSourceLocations( + args: { frames: SourceFrame[]; index?: number }, +): Promise { + if (!args || !args.frames || args.frames.length === 0) { + vscode.window.showWarningMessage('ROHD: No source locations provided.'); + return; + } + currentFrames = args.frames; + currentFrameIndex = args.index ?? 0; + if (currentFrameIndex < 0 || currentFrameIndex >= currentFrames.length) { + currentFrameIndex = 0; + } + updateStatusBar(); + + // Navigate to the primary frame. + await navigateToFrame(currentFrames[currentFrameIndex]); + + // Also open the first frame of each other type (e.g. SV alongside ROHD) + // so both files are visible simultaneously. + const openedTypes = new Set([currentFrames[currentFrameIndex].type || 'rohd']); + for (const frame of currentFrames) { + const t = frame.type || 'rohd'; + if (!openedTypes.has(t)) { + openedTypes.add(t); + await navigateToFrame(frame, true); + } + } +} + +/** Advance to the next frame in the current frame list. */ +export async function nextSourceLocation(): Promise { + if (currentFrames.length === 0) { return; } + currentFrameIndex = (currentFrameIndex + 1) % currentFrames.length; + updateStatusBar(); + await navigateToFrame(currentFrames[currentFrameIndex]); +} + +/** Go back to the previous frame in the current frame list. */ +export async function prevSourceLocation(): Promise { + if (currentFrames.length === 0) { return; } + currentFrameIndex = + (currentFrameIndex - 1 + currentFrames.length) % currentFrames.length; + updateStatusBar(); + await navigateToFrame(currentFrames[currentFrameIndex]); +} + +// --------------------------------------------------------------------------- +// Navigation implementation +// --------------------------------------------------------------------------- + +/** + * Open (or reuse) an editor tab for the given frame and scroll to the + * target line. Tries multiple candidate paths until one succeeds. + */ +async function navigateToFrame(frame: SourceFrame, preserveFocus = false): Promise { + const candidates = resolveCandidates(frame.file); + if (candidates.length === 0) { + vscode.window.showWarningMessage( + `ROHD: Could not resolve file: ${frame.file}`, + ); + return; + } + + // 0-based position from 1-based FLC data. + const line = Math.max(0, frame.line - 1); + const col = Math.max(0, frame.col - 1); + const pos = new vscode.Position(line, col); + const range = new vscode.Range(pos, pos); + + for (const uri of candidates) { + try { + const doc = await vscode.workspace.openTextDocument(uri); + const docUriStr = doc.uri.toString(); + + // Reuse an existing editor tab for this document if one is already + // open, instead of always opening a new split. Compare against + // the document's canonical URI (not the candidate URI which may + // contain unresolved `..` segments). + let viewColumn = vscode.ViewColumn.Beside; + for (const tab of vscode.window.tabGroups.all.flatMap(g => g.tabs)) { + const tabInput = tab.input; + if (tabInput instanceof vscode.TabInputText && + tabInput.uri.toString() === docUriStr) { + viewColumn = tab.group.viewColumn; + break; + } + } + + const editor = await vscode.window.showTextDocument(doc, { + viewColumn, + preserveFocus, + selection: range, + }); + + editor.revealRange( + new vscode.Range(pos, pos), + vscode.TextEditorRevealType.InCenterIfOutsideViewport, + ); + + // Flash-highlight the symbol at the target position. + flashHighlight(editor, line, col); + + output.appendLine( + `Navigated to ${frame.file}:${frame.line}:${frame.col} (resolved: ${uri.fsPath})`, + ); + return; // Success — stop trying candidates. + } catch { + // This candidate didn't work; try the next one. + continue; + } + } + + // All candidates failed. + vscode.window.showWarningMessage( + `ROHD: Could not find file: ${frame.file}`, + ); + output.appendLine( + `Failed to resolve ${frame.file} (tried ${candidates.length} candidates)`, + ); +} + +/** + * Normalize a file path by collapsing `.` and `..` segments. + * + * FLC paths from SourceTraceRegistry often contain `.dart_tool/../lib/...` + * which needs collapsing before resolution. + */ +function normalizePath(filePath: string): string { + const isAbsolute = filePath.startsWith('/'); + const parts = filePath.split('/'); + const resolved: string[] = []; + for (const part of parts) { + if (part === '.' || part === '') { + continue; + } else if (part === '..' && resolved.length > 0 && resolved[resolved.length - 1] !== '..') { + resolved.pop(); + } else { + resolved.push(part); + } + } + const joined = resolved.join('/'); + return isAbsolute ? '/' + joined : joined; +} + +/** + * Generate candidate URIs for a package-relative file path. + * + * Tries (in order): + * 1. Normalized path relative to each workspace folder + * 2. Normalized path relative to parent directories of each workspace + * folder (up to 4 levels — covers typical layouts where the ROHD + * package root is above the extension subdirectory) + * 3. As an absolute path + */ +function resolveCandidates(filePath: string): vscode.Uri[] { + const normalized = normalizePath(filePath); + const candidates: vscode.Uri[] = []; + const folders = vscode.workspace.workspaceFolders; + + if (folders) { + for (const folder of folders) { + // Direct: workspace root + normalized path + candidates.push(vscode.Uri.joinPath(folder.uri, normalized)); + + // SV files are often generated into a build/ directory. + candidates.push(vscode.Uri.joinPath(folder.uri, 'build', normalized)); + + // Walk up parent directories (the ROHD package root may be above + // the workspace folder, e.g. merged/ vs merged/rohd_devtools_extension/rohd-schematic-viewer/) + let parent = folder.uri; + for (let i = 0; i < 4; i++) { + parent = vscode.Uri.joinPath(parent, '..'); + candidates.push(vscode.Uri.joinPath(parent, normalized)); + candidates.push(vscode.Uri.joinPath(parent, 'build', normalized)); + } + } + } + + // Absolute path fallback. + if (normalized.startsWith('/')) { + candidates.push(vscode.Uri.file(normalized)); + } + + // Also try the original (un-normalized) path in case it's already correct. + if (normalized !== filePath && folders) { + for (const folder of folders) { + candidates.push(vscode.Uri.joinPath(folder.uri, filePath)); + } + } + + return candidates; +} + +// --------------------------------------------------------------------------- +// Status bar +// --------------------------------------------------------------------------- + +function updateStatusBar(): void { + if (currentFrames.length <= 1) { + hideStatusBar(); + return; + } + if (!statusBarItem) { + statusBarItem = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Right, + 100, + ); + statusBarItem.command = 'rohd.nextSourceLocation'; + statusBarItem.tooltip = 'Click to cycle through source frames'; + } + const frame = currentFrames[currentFrameIndex]; + const shortFile = frame.file.split('/').pop() ?? frame.file; + const typeTag = frame.type === 'sv' ? 'SV' : frame.type === 'rohd' ? 'ROHD' : 'Source'; + const desc = frame.desc ? ` ${frame.desc}` : ''; + statusBarItem.text = `$(source-control) ${typeTag} ${currentFrameIndex + 1}/${currentFrames.length}: ${shortFile}:${frame.line}${desc}`; + statusBarItem.show(); + + // Auto-hide after 15 seconds. + if (statusBarTimeout) { clearTimeout(statusBarTimeout); } + statusBarTimeout = setTimeout(() => hideStatusBar(), 15000); +} + +function hideStatusBar(): void { + statusBarItem?.hide(); + if (statusBarTimeout) { + clearTimeout(statusBarTimeout); + statusBarTimeout = undefined; + } +} + +// --------------------------------------------------------------------------- +// Highlight flash +// --------------------------------------------------------------------------- + +function flashHighlight(editor: vscode.TextEditor, line: number, col: number): void { + if (highlightTimeout) { clearTimeout(highlightTimeout); } + + const doc = editor.document; + const pos = new vscode.Position(line, col); + + // Try to get the word range at the column position (symbol highlight). + const wordRange = doc.getWordRangeAtPosition(pos); + + // Use the word range if found, otherwise highlight from column to end of line. + const highlightRange = wordRange + ?? new vscode.Range(pos, doc.lineAt(line).range.end); + + editor.setDecorations(highlightDecoration, [highlightRange]); + + // Remove highlight after 60 seconds. + highlightTimeout = setTimeout(() => { + editor.setDecorations(highlightDecoration, []); + }, 60000); +} + +// --------------------------------------------------------------------------- +// Lifecycle +// --------------------------------------------------------------------------- + +/** Register all commands. Call from extension activate(). */ +export function registerCommands(context: vscode.ExtensionContext): void { + context.subscriptions.push( + vscode.commands.registerCommand('rohd.openSourceLocation', openSourceLocation), + vscode.commands.registerCommand('rohd.openSourceLocations', openSourceLocations), + vscode.commands.registerCommand('rohd.nextSourceLocation', nextSourceLocation), + vscode.commands.registerCommand('rohd.prevSourceLocation', prevSourceLocation), + ); + output.appendLine('ROHD Source Navigator commands registered.'); +} + +/** Clean up. Call from extension deactivate(). */ +export function dispose(): void { + hideStatusBar(); + statusBarItem?.dispose(); + highlightDecoration.dispose(); +} + +// --------------------------------------------------------------------------- +// Frame enrichment — resolve enclosing method names via Document Symbols +// --------------------------------------------------------------------------- + +/** A frame enriched with its enclosing method/class names. */ +export interface EnrichedFrame extends SourceFrame { + /** Enclosing method/function name (e.g. `"build"`). */ + methodName?: string; + /** Enclosing class name (e.g. `"Serializer"`). */ + className?: string; + /** Human-readable label: `"Serializer.build() — serializer.dart:55"`. */ + label?: string; +} + +/** + * Resolve the enclosing method name for a source location using the + * VS Code Document Symbol Provider (backed by the Dart language server). + */ +async function resolveEnclosingSymbol( + uri: vscode.Uri, + line: number, + col: number, +): Promise<{ methodName?: string; className?: string }> { + try { + const symbols = await vscode.commands.executeCommand( + 'vscode.executeDocumentSymbolProvider', + uri, + ); + if (!symbols || symbols.length === 0) { + return {}; + } + + const pos = new vscode.Position(Math.max(0, line - 1), Math.max(0, col - 1)); + let methodName: string | undefined; + let className: string | undefined; + + // Walk the symbol tree depth-first, tracking the innermost containing + // function/method and its parent class. + function walk(syms: vscode.DocumentSymbol[], parentClass?: string): void { + for (const sym of syms) { + if (!sym.range.contains(pos)) { continue; } + + if (sym.kind === vscode.SymbolKind.Class || + sym.kind === vscode.SymbolKind.Enum) { + className = sym.name; + walk(sym.children, sym.name); + } else if ( + sym.kind === vscode.SymbolKind.Method || + sym.kind === vscode.SymbolKind.Function || + sym.kind === vscode.SymbolKind.Constructor + ) { + methodName = sym.name; + if (parentClass) { className = parentClass; } + // Continue into children in case there's a nested function. + walk(sym.children, parentClass); + } else { + walk(sym.children, parentClass); + } + } + } + + walk(symbols); + return { methodName, className }; + } catch { + return {}; + } +} + +/** + * Enrich a list of frames with enclosing method/class names. + * + * Opens each document (lazily — no visible editor tab) to query the + * Document Symbol Provider. Returns an enriched copy of each frame. + */ +export async function resolveFrames( + frames: SourceFrame[], +): Promise { + const enriched: EnrichedFrame[] = []; + + for (const frame of frames) { + const candidates = resolveCandidates(frame.file); + let methodName: string | undefined; + let className: string | undefined; + let resolved = false; + + for (const uri of candidates) { + try { + // Open the document without showing it — this triggers the + // language server to analyse it if not already cached. + await vscode.workspace.openTextDocument(uri); + const result = await resolveEnclosingSymbol(uri, frame.line, frame.col); + methodName = result.methodName; + className = result.className; + resolved = true; + break; + } catch { + continue; + } + } + + // Build human-readable label. + const shortFile = frame.file.split('/').pop() ?? frame.file; + let label: string; + if (className && methodName) { + label = `${className}.${methodName}() — ${shortFile}:${frame.line}`; + } else if (methodName) { + label = `${methodName}() — ${shortFile}:${frame.line}`; + } else if (className) { + label = `${className} — ${shortFile}:${frame.line}`; + } else { + label = `${shortFile}:${frame.line}`; + } + + enriched.push({ + ...frame, + methodName, + className, + label, + }); + + if (!resolved) { + output.appendLine( + `[resolveFrames] Could not resolve symbols for ${frame.file}:${frame.line}`, + ); + } + } + + return enriched; +} diff --git a/rohd_extension/src/uri_forwarder.ts b/rohd_extension/src/uri_forwarder.ts new file mode 100644 index 000000000..f070da501 --- /dev/null +++ b/rohd_extension/src/uri_forwarder.ts @@ -0,0 +1,295 @@ +/* --------------------------------------------------------------------------- + * Copyright (C) 2026 Intel Corporation. + * SPDX-License-Identifier: BSD-3-Clause + * + * uri_forwarder.ts + * Resolves and displays DTD and VM Service URIs with port-forwarding + * awareness. When running inside a dev container or remote session, + * VS Code's `asExternalUri` may map container-internal ports to + * host-visible ports. This module detects that and shows both the + * original and forwarded URIs so they can be pasted into the ROHD + * DevTools GUI. + * + * Author: Desmond Kirkpatrick + * --------------------------------------------------------------------------- */ + +import * as vscode from 'vscode'; + +const EXTENSION_VERSION = '0.2.0'; + +let outputChannel: vscode.OutputChannel; + +/** Map of session id → last known VM Service URI (container-internal). */ +const sessionVmUris = new Map(); + +/** Latest original DTD URI (container-internal). */ +let originalDtdUri: string | undefined; + +/** Latest forwarded DTD URI, undefined if no forwarding occurred. */ +let forwardedDtdUri: string | undefined; + +// --------------------------------------------------------------------------- +// URI helpers +// --------------------------------------------------------------------------- + +/** + * Given a raw WS URI, resolve via `vscode.env.asExternalUri` to get the + * port-forwarded equivalent. Returns the forwarded URI string, or + * `undefined` if no forwarding occurred (i.e. authority is unchanged). + */ +async function resolveForwardedUri( + rawWsUri: string, +): Promise { + try { + const httpUri = rawWsUri + .replace(/^ws:\/\//, 'http://') + .replace(/^wss:\/\//, 'https://'); + + const parsed = vscode.Uri.parse(httpUri); + const resolved = await vscode.env.asExternalUri(parsed); + + const scheme = resolved.scheme === 'https' ? 'wss' : 'ws'; + const forwarded = `${scheme}://${resolved.authority}${parsed.path}`; + + if (resolved.authority !== parsed.authority) { + return forwarded; + } + return undefined; + } catch { + return undefined; + } +} + +/** Normalize a URI to ws:// scheme; optionally ensure a /ws suffix. */ +function normalizeWsUri(uri: string, ensureWsSuffix: boolean): string { + let u = uri; + if (u.startsWith('http://')) { + u = u.replace('http://', 'ws://'); + } else if (u.startsWith('https://')) { + u = u.replace('https://', 'wss://'); + } + if (ensureWsSuffix && !u.endsWith('/ws')) { + u = u.replace(/\/?$/, '/ws'); + } + return u; +} + +// --------------------------------------------------------------------------- +// Dart extension API typings (subset) +// --------------------------------------------------------------------------- + +interface DartExtensionApi { + dtdUri?: string; + onDtdUriChanged?: ( + listener: (uri: string | undefined) => void, + thisArgs?: unknown, + disposables?: vscode.Disposable[], + ) => vscode.Disposable; +} + +// --------------------------------------------------------------------------- +// DTD URI handling +// --------------------------------------------------------------------------- + +async function processDtdUri(rawUri: string): Promise { + const wsUri = normalizeWsUri(rawUri, false); + originalDtdUri = wsUri; + outputChannel.appendLine(`[DTD] Original: ${wsUri}`); + + const forwarded = await resolveForwardedUri(wsUri); + forwardedDtdUri = forwarded; + if (forwarded) { + outputChannel.appendLine(`[DTD] Forwarded: ${forwarded}`); + } +} + +function watchDtdUri(context: vscode.ExtensionContext): void { + const dartExt = vscode.extensions.getExtension('dart-code.dart-code'); + if (!dartExt) { + outputChannel.appendLine( + '[DTD] Dart extension not found — DTD URI detection disabled.', + ); + return; + } + + function handleApi(api: DartExtensionApi): void { + if (api.dtdUri) { + processDtdUri(api.dtdUri).catch((err) => { + outputChannel.appendLine(`[DTD] Error: ${err}`); + }); + } + if (api.onDtdUriChanged) { + const disposable = api.onDtdUriChanged((uri) => { + if (uri) { + processDtdUri(uri).catch((err) => { + outputChannel.appendLine(`[DTD] Error: ${err}`); + }); + } + }); + if (disposable) { + context.subscriptions.push(disposable); + } + } + } + + if (dartExt.isActive) { + handleApi(dartExt.exports as DartExtensionApi); + } else { + dartExt.activate().then( + (api) => handleApi(api as DartExtensionApi), + (err) => + outputChannel.appendLine( + `[DTD] Dart extension activation failed: ${err}`, + ), + ); + } +} + +// --------------------------------------------------------------------------- +// Consolidated output +// --------------------------------------------------------------------------- + +/** + * Print a formatted block with the ROHD version, FLC info, and the + * DTD / VM Service URIs (original + forwarded when port differs). + */ +async function printConsolidatedBlock( + vmOriginal: string, + vmForwarded: string | undefined, + session: vscode.DebugSession, +): Promise { + const lines: string[] = []; + + lines.push(`ROHD ${EXTENSION_VERSION}: Extension loaded for FLC crossprobing.`); + lines.push(''); + + if (originalDtdUri) { + lines.push('DTD:'); + lines.push(` URI: ${originalDtdUri}`); + if (forwardedDtdUri) { + lines.push(` Fwd: ${forwardedDtdUri}`); + } + } + + lines.push('VM:'); + lines.push(` URI: ${vmOriginal}`); + if (vmForwarded) { + lines.push(` Fwd: ${vmForwarded}`); + } + + const banner = '═'.repeat(60); + const block = [banner, ...lines, banner].join('\n'); + + // Debug console + vscode.debug.activeDebugConsole.appendLine(''); + vscode.debug.activeDebugConsole.appendLine(block); + + // Output channel (persists across sessions) + outputChannel.appendLine(''); + outputChannel.appendLine(block); + + // Show the most useful forwarded URI in a popup for quick copy + const popupUri = forwardedDtdUri ?? originalDtdUri; + if (popupUri) { + const action = await vscode.window.showInformationMessage( + `DTD: ${popupUri}`, + 'Copy', + ); + if (action === 'Copy') { + await vscode.env.clipboard.writeText(popupUri); + vscode.window.showInformationMessage('DTD URI copied to clipboard.'); + } + } +} + +// --------------------------------------------------------------------------- +// VM Service URI handling +// --------------------------------------------------------------------------- + +async function processVmServiceUri( + rawUri: string, + session: vscode.DebugSession, +): Promise { + const vmUri = normalizeWsUri(rawUri, true); + sessionVmUris.set(session.id, vmUri); + + outputChannel.appendLine(`[VM] Original: ${vmUri}`); + + const forwarded = await resolveForwardedUri(vmUri); + await printConsolidatedBlock(vmUri, forwarded, session); +} + +// --------------------------------------------------------------------------- +// Public API — called from extension.ts +// --------------------------------------------------------------------------- + +export function activate(context: vscode.ExtensionContext): void { + outputChannel = vscode.window.createOutputChannel('ROHD URI Forwarder'); + + // Start watching for the DTD URI early — it resolves before the + // debug session starts so it will be ready when we print. + watchDtdUri(context); + + // Listen for the DAP custom event "dart.debuggerUris" + context.subscriptions.push( + vscode.debug.onDidReceiveDebugSessionCustomEvent((e) => { + if (e.event !== 'dart.debuggerUris') { + return; + } + + const uri = e.body?.vmServiceUri as string | undefined; + if (!uri) { + outputChannel.appendLine( + '[URI Forwarder] dart.debuggerUris event received but no vmServiceUri in body.', + ); + return; + } + + outputChannel.appendLine( + `[URI Forwarder] Received dart.debuggerUris for session "${e.session.name}".`, + ); + + processVmServiceUri(uri, e.session).catch((err) => { + outputChannel.appendLine(`[URI Forwarder] Error: ${err}`); + }); + }), + ); + + // Clean up stored URIs when sessions end + context.subscriptions.push( + vscode.debug.onDidTerminateDebugSession((session) => { + sessionVmUris.delete(session.id); + }), + ); + + // Manual command: re-resolve and re-print for the active session + context.subscriptions.push( + vscode.commands.registerCommand('rohd.showForwardedUris', async () => { + const session = vscode.debug.activeDebugSession; + if (!session) { + vscode.window.showWarningMessage('No active debug session.'); + return; + } + + const uri = sessionVmUris.get(session.id); + if (!uri) { + vscode.window.showWarningMessage( + 'VM Service URI not yet available for this session. ' + + 'It will be displayed automatically once the Dart VM starts.', + ); + return; + } + + await processVmServiceUri(uri, session); + }), + ); + + outputChannel.appendLine('[URI Forwarder] Activated.'); +} + +export function deactivate(): void { + sessionVmUris.clear(); + originalDtdUri = undefined; + forwardedDtdUri = undefined; + outputChannel?.dispose(); +} diff --git a/rohd_extension/tool/install.sh b/rohd_extension/tool/install.sh new file mode 100755 index 000000000..bb2f6d235 --- /dev/null +++ b/rohd_extension/tool/install.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# --------------------------------------------------------------------------- +# install.sh — Build and install the ROHD VS Code extension. +# +# Usage: +# ./tool/install.sh # build + install +# ./tool/install.sh --skip-build # install existing .vsix only +# +# Requires: node ≥ 18, npm, code CLI +# --------------------------------------------------------------------------- +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EXT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +VERSION=$(node -p "require('$EXT_DIR/package.json').version") +VSIX="$EXT_DIR/rohd-${VERSION}.vsix" + +# ── Ensure Node ≥ 18 is available ── +if ! command -v node &>/dev/null; then + if [[ -d "$HOME/.nvm/versions/node" ]]; then + NODE_DIR=$(ls -d "$HOME/.nvm/versions/node"/v2* 2>/dev/null | sort -V | tail -1) + [[ -z "$NODE_DIR" ]] && NODE_DIR=$(ls -d "$HOME/.nvm/versions/node"/v1[89]* 2>/dev/null | sort -V | tail -1) + if [[ -n "$NODE_DIR" ]]; then + export PATH="$NODE_DIR/bin:$PATH" + echo "Using node from $NODE_DIR" + fi + fi +fi + +node_ver=$(node --version 2>/dev/null || echo "none") +echo "Node: $node_ver" + +# ── Build ── +if [[ "${1:-}" != "--skip-build" ]]; then + echo "── Installing npm dependencies ──" + cd "$EXT_DIR" + npm install --no-audit --no-fund + + echo "── Compiling TypeScript ──" + npx tsc + + echo "── Packaging VSIX ──" + rm -f "$EXT_DIR"/rohd-*.vsix + echo y | npx @vscode/vsce package --no-dependencies +fi + +# ── Install ── +if [[ ! -f "$VSIX" ]]; then + echo "ERROR: $VSIX not found. Run without --skip-build first." >&2 + exit 1 +fi + +echo "── Installing $VSIX ──" +code --install-extension "$VSIX" --force + +echo "" +echo "Done — ROHD extension v${VERSION} installed." +echo "Reload the VS Code window to activate." diff --git a/rohd_extension/tsconfig.json b/rohd_extension/tsconfig.json new file mode 100644 index 000000000..ec2434dd0 --- /dev/null +++ b/rohd_extension/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "out", + "lib": ["ES2020"], + "sourceMap": true, + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "exclude": ["node_modules", "out"] +} From dad48a1a8e98993a01cd2b8733829c4b273087d9 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 20 Jun 2026 06:33:27 -0700 Subject: [PATCH 5/7] Clean DevTools extension analysis --- .../lib/rohd_devtools/services/tree_service.dart | 3 ++- .../lib/rohd_devtools/ui/module_tree_card.dart | 5 +++-- .../lib/rohd_devtools/ui/module_tree_details_navbar.dart | 2 +- .../lib/rohd_devtools/ui/signal_details_card.dart | 4 ++-- rohd_devtools_extension/pubspec.yaml | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/rohd_devtools_extension/lib/rohd_devtools/services/tree_service.dart b/rohd_devtools_extension/lib/rohd_devtools/services/tree_service.dart index 578134c52..d84b5fe20 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/services/tree_service.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/services/tree_service.dart @@ -10,6 +10,7 @@ import 'dart:convert'; import 'package:devtools_app_shared/service.dart'; +import 'package:flutter/foundation.dart'; import 'package:rohd_devtools_extension/rohd_devtools/models/tree_model.dart'; class TreeService { @@ -28,7 +29,7 @@ class TreeService { final treeObj = jsonDecode(treeInstance.valueAsString ?? '') as Map; if (treeObj['status'] == 'fail') { - print('error'); + debugPrint('error'); return null; } else { diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_card.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_card.dart index 40f1e72de..a12b25697 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_card.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_card.dart @@ -74,8 +74,9 @@ class _ModuleTreeCardState extends State { children: [ Container( decoration: BoxDecoration( - color: - isSelected ? Colors.blue.withOpacity(0.2) : Colors.transparent, + color: isSelected + ? Colors.blue.withValues(alpha: 0.2) + : Colors.transparent, borderRadius: BorderRadius.circular(4.0), ), padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_details_navbar.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_details_navbar.dart index f84835e5e..a94935179 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_details_navbar.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_details_navbar.dart @@ -20,7 +20,7 @@ class ModuleTreeDetailsNavbar extends StatelessWidget { type: BottomNavigationBarType.fixed, backgroundColor: const Color(0x1B1B1FEE), selectedItemColor: Colors.white, - unselectedItemColor: Colors.white.withOpacity(.60), + unselectedItemColor: Colors.white.withValues(alpha: .60), selectedFontSize: 10, unselectedFontSize: 10, onTap: (value) { diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_details_card.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_details_card.dart index 0d3fdeb3a..442bb2fac 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_details_card.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_details_card.dart @@ -17,9 +17,9 @@ class SignalDetailsCard extends StatefulWidget { final TreeModel? module; const SignalDetailsCard({ - Key? key, + super.key, this.module, - }) : super(key: key); + }); @override SignalDetailsCardState createState() => SignalDetailsCardState(); diff --git a/rohd_devtools_extension/pubspec.yaml b/rohd_devtools_extension/pubspec.yaml index 0aa366e78..8b5bb226f 100644 --- a/rohd_devtools_extension/pubspec.yaml +++ b/rohd_devtools_extension/pubspec.yaml @@ -29,7 +29,7 @@ dev_dependencies: flutter_lints: ^3.0.1 build_runner: ^2.4.7 mocktail: ^1.0.2 - bloc_lint: ^0.1.0 + bloc_lint: ^0.3.7 flutter: uses-material-design: true From 35394dec29918e5ca1b25667e3643be7b5fd9561 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 20 Jun 2026 07:08:00 -0700 Subject: [PATCH 6/7] Keep DevTools extension changes on owning branches --- .../lib/rohd_devtools/services/tree_service.dart | 3 +-- .../lib/rohd_devtools/ui/module_tree_card.dart | 5 ++--- .../lib/rohd_devtools/ui/module_tree_details_navbar.dart | 2 +- .../lib/rohd_devtools/ui/signal_details_card.dart | 4 ++-- rohd_devtools_extension/pubspec.yaml | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/rohd_devtools_extension/lib/rohd_devtools/services/tree_service.dart b/rohd_devtools_extension/lib/rohd_devtools/services/tree_service.dart index d84b5fe20..578134c52 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/services/tree_service.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/services/tree_service.dart @@ -10,7 +10,6 @@ import 'dart:convert'; import 'package:devtools_app_shared/service.dart'; -import 'package:flutter/foundation.dart'; import 'package:rohd_devtools_extension/rohd_devtools/models/tree_model.dart'; class TreeService { @@ -29,7 +28,7 @@ class TreeService { final treeObj = jsonDecode(treeInstance.valueAsString ?? '') as Map; if (treeObj['status'] == 'fail') { - debugPrint('error'); + print('error'); return null; } else { diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_card.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_card.dart index a12b25697..40f1e72de 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_card.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_card.dart @@ -74,9 +74,8 @@ class _ModuleTreeCardState extends State { children: [ Container( decoration: BoxDecoration( - color: isSelected - ? Colors.blue.withValues(alpha: 0.2) - : Colors.transparent, + color: + isSelected ? Colors.blue.withOpacity(0.2) : Colors.transparent, borderRadius: BorderRadius.circular(4.0), ), padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_details_navbar.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_details_navbar.dart index a94935179..f84835e5e 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_details_navbar.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_details_navbar.dart @@ -20,7 +20,7 @@ class ModuleTreeDetailsNavbar extends StatelessWidget { type: BottomNavigationBarType.fixed, backgroundColor: const Color(0x1B1B1FEE), selectedItemColor: Colors.white, - unselectedItemColor: Colors.white.withValues(alpha: .60), + unselectedItemColor: Colors.white.withOpacity(.60), selectedFontSize: 10, unselectedFontSize: 10, onTap: (value) { diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_details_card.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_details_card.dart index 442bb2fac..0d3fdeb3a 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_details_card.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_details_card.dart @@ -17,9 +17,9 @@ class SignalDetailsCard extends StatefulWidget { final TreeModel? module; const SignalDetailsCard({ - super.key, + Key? key, this.module, - }); + }) : super(key: key); @override SignalDetailsCardState createState() => SignalDetailsCardState(); diff --git a/rohd_devtools_extension/pubspec.yaml b/rohd_devtools_extension/pubspec.yaml index 8b5bb226f..0aa366e78 100644 --- a/rohd_devtools_extension/pubspec.yaml +++ b/rohd_devtools_extension/pubspec.yaml @@ -29,7 +29,7 @@ dev_dependencies: flutter_lints: ^3.0.1 build_runner: ^2.4.7 mocktail: ^1.0.2 - bloc_lint: ^0.3.7 + bloc_lint: ^0.1.0 flutter: uses-material-design: true From a87fa5c20118f6813c1f354731e34d6634060c08 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 21 Jun 2026 09:26:53 -0700 Subject: [PATCH 7/7] added back pubkeys, and made a wget a fallback solution with loud warning --- tool/gh_codespaces/install_dart.sh | 80 +++++++- tool/gh_codespaces/pubkeys/dart.pub | 305 ++++++++++++++++++++++++++++ 2 files changed, 380 insertions(+), 5 deletions(-) create mode 100644 tool/gh_codespaces/pubkeys/dart.pub diff --git a/tool/gh_codespaces/install_dart.sh b/tool/gh_codespaces/install_dart.sh index f170dc247..d0bfdfe91 100755 --- a/tool/gh_codespaces/install_dart.sh +++ b/tool/gh_codespaces/install_dart.sh @@ -8,21 +8,91 @@ # # 2023 February 5 # Author: Chykon +# +# 2026 June 21 +# Updated to add fallback logic for fetching the latest Dart repository key from Google if the locally cached key fails verification (e.g. due to key rotation). +# Author: Desmond A. Kirkpatrick set -euo pipefail +declare -r cached_pubkey_file="$(dirname "${BASH_SOURCE[0]}")/pubkeys/dart.pub" +declare -r keyring_file='/usr/share/keyrings/dart.gpg' +declare -r dart_repository_file='/etc/apt/sources.list.d/dart_stable.list' +declare -r dart_repository_url='https://storage.googleapis.com/download.dartlang.org/linux/debian' +declare -r google_signing_key_url='https://dl-ssl.google.com/linux/linux_signing_key.pub' + sudo apt-get update sudo apt-get install -y wget gpg apt-transport-https sudo mkdir -p /usr/share/keyrings -wget -qO- https://dl-ssl.google.com/linux/linux_signing_key.pub \ - | gpg --dearmor \ - | sudo tee /usr/share/keyrings/dart.gpg >/dev/null # Add Dart repository. -echo "deb [signed-by=/usr/share/keyrings/dart.gpg] https://storage.googleapis.com/download.dartlang.org/linux/debian stable main" \ - | sudo tee /etc/apt/sources.list.d/dart_stable.list +echo "deb [signed-by=${keyring_file}] ${dart_repository_url} stable main" \ + | sudo tee "${dart_repository_file}" + +# Install the repository key from the locally cached, ASCII-armored public key. +install_key_from_file() { + sudo gpg --yes --output "${keyring_file}" --dearmor "${1}" +} + +# Install the repository key by fetching the latest key from Google. +install_key_from_google() { + wget -qO- "${google_signing_key_url}" \ + | gpg --dearmor \ + | sudo tee "${keyring_file}" >/dev/null +} + +# Emit a prominent warning that stands out in CI logs (and as a GitHub Actions +# annotation when available) without failing the build. +warn_loudly() { + local message="${1}" + { + echo '' + echo '################################################################################' + echo '## install_dart WARNING' + echo "## ${message}" + echo '################################################################################' + echo '' + } >&2 + # Surface a GitHub Actions warning annotation (non-fatal) when running in CI. + if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + echo "::warning title=install_dart cached key bypassed::${message}" + fi +} + +# Verify that the installed keyring can authenticate the Dart repository by +# refreshing only the Dart sources list and checking for signature/key errors. +dart_repository_verified() { + local update_log + if ! update_log=$(sudo apt-get update \ + -o Dir::Etc::sourcelist="${dart_repository_file}" \ + -o Dir::Etc::sourceparts="-" \ + -o APT::Get::List-Cleanup="0" 2>&1); then + return 1 + fi + if echo "${update_log}" \ + | grep -Eiq 'NO_PUBKEY|EXPKEYSIG|REVKEYSIG|BADSIG|not signed|could.?n.?t be verified'; then + return 1 + fi + return 0 +} + +# Prefer the locally cached key. If it can no longer authenticate the repository +# (e.g. the key has been rotated), fall back to fetching the latest key from +# Google so the install can still proceed. +install_key_from_file "${cached_pubkey_file}" + +if dart_repository_verified; then + echo 'install_dart: using locally cached Dart repository key.' +else + install_key_from_google + if ! dart_repository_verified; then + echo 'install_dart: Dart repository key verification failed even after fetching the latest key from Google.' >&2 + exit 1 + fi + warn_loudly "Cached Dart repository key (${cached_pubkey_file}) failed verification and was bypassed; installed using the latest key fetched from Google. Please refresh the cached key." +fi # Install Dart. diff --git a/tool/gh_codespaces/pubkeys/dart.pub b/tool/gh_codespaces/pubkeys/dart.pub new file mode 100644 index 000000000..839f8a235 --- /dev/null +++ b/tool/gh_codespaces/pubkeys/dart.pub @@ -0,0 +1,305 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=CkA1 +-----END PGP PUBLIC KEY BLOCK-----