diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c491696..09e63a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,10 +19,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 20.x cache: npm @@ -37,10 +37,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 20.x cache: npm @@ -56,10 +56,10 @@ jobs: needs: [lint, unit-tests] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 20.x cache: npm diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1ad8870..7b38481 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,10 +17,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: '20.x' cache: npm @@ -43,10 +43,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: '20.x' cache: npm diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 0f2ef8d..ddf66cf 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -1,4 +1,15 @@ { "$schema": "https://json.schemastore.org/mcp-config", - "servers": {} + "servers": { + "sysml-v2-local": { + "type": "stdio", + "command": "node", + "args": [ + "./node_modules/sysml-v2-lsp/dist/server/mcpServer.js" + ], + "tools": [ + "*" + ] + } + } } diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 755729b..e84fdc4 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -109,8 +109,8 @@ A VS Code extension providing SysML v2.0 language support with interactive visua │ │ v v ┌─────────────┐ ┌───────────────┐ - │ Model Tree │ │ Visualization │ - │ Explorer │ │ Webview │ + │ Model Tree │ │ Visualization│ + │ Explorer │ │ Webview │ └─────────────┘ └───────────────┘ ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index ae9c74e..af8d475 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,29 @@ All notable changes to the SysML v2.0 Language Support extension will be documen The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.26.0] + +### Added + +- Model Explorer package context menu action: **View Model Dashboard** +- Model Dashboard now support workspaces + +### Changed + +- LSP updated to 0.7.0 + - Improved semantic validation + - New quick fix actions + - Model Complexity Index calculation tweaks + - Model Dashboard workspace targeting UI: file/semantic mode toggle with filtered target dropdown + - Updated to OMG 2026-02 - SysML v2 Release + +### Fixed + +- Visualizer filter reliability across diagram data sources (including non-standard element collections) +- State transition rendering for qualified names and selected-machine transition mapping +- Webview disposal race conditions in visualizer updates/postMessage paths +- Model Dashboard auto-refresh on file changes while open + ## [0.25.0] ### Added diff --git a/Makefile b/Makefile index 723aeb9..53bb138 100644 --- a/Makefile +++ b/Makefile @@ -32,6 +32,7 @@ help: @echo " $(GREEN)watch$(NC) - Watch and compile on changes" @echo " $(GREEN)test$(NC) - Run unit tests (fast, no VS Code required)" @echo " $(GREEN)test-integration$(NC) - Run full integration tests (requires VS Code)" + @echo " $(GREEN)test-integration-parallel$(NC) - Run integration tests in 2 parallel shards" @echo " $(GREEN)test-syntax$(NC) - Test syntax and compilation (no VS Code required)" @echo " $(GREEN)install-test-deps$(NC) - Install system dependencies for testing" @echo " $(GREEN)lint$(NC) - Run linting" @@ -39,7 +40,7 @@ help: @echo " $(GREEN)coverage$(NC) - Generate coverage report" @echo " $(GREEN)package$(NC) - Create VSIX package" @echo " $(GREEN)clean$(NC) - Clean build artifacts" - @echo " $(GREEN)clean-all$(NC) - Clean everything including node_modules" + @echo " $(GREEN)clean-all$(NC) - Clean + deterministic reinstall + refresh local file deps" @echo " $(GREEN)dev$(NC) - Start development environment" @echo " $(GREEN)debug$(NC) - Prepare for debugging (then press F5 in VS Code)" @echo " $(GREEN)debug-watch$(NC) - Launch watch mode for debugging with auto-recompile" @@ -83,7 +84,7 @@ test: compile .PHONY: test-integration test-integration: compile @echo "$(YELLOW)Running integration tests...$(NC)" - @if npm run test; then \ + @if node ./out/test/runTest.js; then \ echo "$(GREEN)Integration tests completed successfully!$(NC)"; \ else \ exit_code=$$?; \ @@ -100,6 +101,27 @@ test-integration: compile exit $$exit_code; \ fi +# Run integration tests in two shards in parallel. This is opt-in because +# some environments may prefer deterministic single-host execution. +.PHONY: test-integration-parallel +test-integration-parallel: compile + @echo "$(YELLOW)Running integration tests in parallel (2 shards)...$(NC)" + @set +e; \ + SHARD1="integration.test.js,visualizationPanel.test.js,modelExplorerIntegration.test.js,modelDashboardPanel.test.js,performance.test.js"; \ + SHARD2="codeActions.test.js,codeLens.test.js,diagramButtons.test.js,editingFeatures.test.js,exportFunctionality.test.js,exportScaleLogic.test.js,featureExplorerProvider.test.js,keywordDiagnostics.test.js,lspClient.test.js,mcpServer.test.js,modelExplorerProvider.test.js,sysRunnerGame.test.js"; \ + SYSML_TEST_FILES="$$SHARD1" node ./out/test/runTest.js > /tmp/sysml-test-shard1.log 2>&1 & pid1=$$!; \ + SYSML_TEST_FILES="$$SHARD2" node ./out/test/runTest.js > /tmp/sysml-test-shard2.log 2>&1 & pid2=$$!; \ + wait $$pid1; code1=$$?; \ + wait $$pid2; code2=$$?; \ + echo "$(BLUE)=== Shard 1 output ===$(NC)"; cat /tmp/sysml-test-shard1.log; \ + echo "$(BLUE)=== Shard 2 output ===$(NC)"; cat /tmp/sysml-test-shard2.log; \ + rm -f /tmp/sysml-test-shard1.log /tmp/sysml-test-shard2.log; \ + if [ $$code1 -ne 0 ] || [ $$code2 -ne 0 ]; then \ + echo "$(RED)Parallel integration tests failed (shard1=$$code1, shard2=$$code2)$(NC)"; \ + exit 1; \ + fi; \ + echo "$(GREEN)Parallel integration tests completed successfully!$(NC)" + # Test syntax and compilation without VS Code .PHONY: test-syntax test-syntax: compile lint @@ -209,15 +231,22 @@ clean: rm -f tsconfig.tsbuildinfo @echo "$(GREEN)Build artifacts cleaned!$(NC)" -# Clean everything including dependencies, then reinstall and recompile +# Clean everything including dependencies, then reinstall and recompile. +# Keeps package-lock for deterministic installs and force-refreshes local +# file: dependencies (for example local .tgz packages) to avoid stale npm cache. .PHONY: clean-all clean-all: clean @echo "$(YELLOW)Cleaning all files including dependencies...$(NC)" rm -rf $(NODE_MODULES) - rm -f package-lock.json @echo "$(GREEN)All files cleaned!$(NC)" + @echo "$(YELLOW)Clearing npm cache to avoid stale local package artifacts...$(NC)" + npm cache clean --force + @echo "$(YELLOW)Refreshing package-lock for local file: dependencies (if any)...$(NC)" + @node -e "const cp=require('child_process');const pkg=require('./package.json');const deps={...(pkg.dependencies||{}),...(pkg.devDependencies||{})};const local=Object.entries(deps).filter(([,ref])=>typeof ref==='string'&&ref.startsWith('file:'));if(local.length===0){console.log('No local file: dependencies found.');process.exit(0);}for(const [name,ref] of local){console.log('Updating lock entry for '+name+' from '+ref);cp.execFileSync('npm',['install',name+'@'+ref,'--package-lock-only','--save-exact','--ignore-scripts','--force'],{stdio:'inherit'});}" @echo "$(YELLOW)Reinstalling dependencies...$(NC)" npm install + @echo "$(YELLOW)Refreshing local file: dependencies (if any)...$(NC)" + @node -e "const cp=require('child_process');const pkg=require('./package.json');const deps={...(pkg.dependencies||{}),...(pkg.devDependencies||{})};const local=Object.entries(deps).filter(([,ref])=>typeof ref==='string'&&ref.startsWith('file:'));if(local.length===0){console.log('No local file: dependencies found.');process.exit(0);}for(const [name,ref] of local){console.log('Refreshing '+name+' from '+ref);cp.execFileSync('npm',['install',name+'@'+ref,'--force'],{stdio:'inherit'});}" @echo "$(YELLOW)Recompiling...$(NC)" npm run compile @echo "$(GREEN)Clean rebuild complete - ready for F5 debugging!$(NC)" diff --git a/README.md b/README.md index 64e8f2e..5d6327d 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,8 @@ Right-click a package node in the **SysML Model Explorer** → **Visualize Packa npm install && npm run compile && npm test ``` +Note for when packaged as a VSIX, the extension registers its MCP server from the extension install path at activation time. A workspace `.vscode/mcp.json` is only a local development override (for example, to pin Copilot chat to a specific local server build). + ## License MIT diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..f24e1f0 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,48 @@ +# Roadmap + +This is a hobby project built for the SysML community and maintained in my spare time. + +I welcome feature requests, suggestions, and bug reports. I review all feedback and will address items as time allows. + +## Project Goals + +- Provide practical, reliable SysML authoring support in VS Code. +- Improve modeling workflows with useful diagnostics, navigation, and visualizations. +- Keep the project approachable for contributors and users. + +## Maintenance Model + +- This project is maintained on a best-effort basis. +- Response and fix timelines are not guaranteed. +- Issues that affect stability, correctness, or data safety are prioritized. + +## Community Feedback + +Please open an issue for: + +- Bug reports +- Feature requests +- UX suggestions +- Documentation improvements + +Helpful issue reports include: + +- Steps to reproduce +- Expected vs actual behavior +- Sample SysML snippet or file +- Environment details (OS, VS Code version, extension version) + +## Near-Term Focus + +- Improve reliability and test coverage +- Continue refining model visualization and dashboard capabilities +- Reduce false positives in diagnostics +- Improve performance for larger models + +## Long-Term Direction + +- Grow community-driven improvements through feedback and contributions + +## Thank You + +Thanks for using the extension and helping improve it through feedback and testing. diff --git a/package-lock.json b/package-lock.json index 87a4297..b2f0ee5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "sysml-v2-support", - "version": "0.24.0", + "version": "0.26.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sysml-v2-support", - "version": "0.24.0", + "version": "0.26.0", "license": "MIT", "dependencies": { "elkjs": "^0.11.0", - "sysml-v2-lsp": "^0.6.0", + "sysml-v2-lsp": "^0.7.0", "vscode-languageclient": "^9.0.1" }, "devDependencies": { @@ -302,15 +302,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -374,9 +374,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", - "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { @@ -387,7 +387,7 @@ "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.3", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -463,9 +463,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.3", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", - "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -986,17 +986,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", - "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", + "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/type-utils": "8.56.1", - "@typescript-eslint/utils": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/type-utils": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -1009,22 +1009,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.56.1", + "@typescript-eslint/parser": "^8.57.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", - "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", + "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3" }, "engines": { @@ -1040,14 +1040,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", - "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", + "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.1", - "@typescript-eslint/types": "^8.56.1", + "@typescript-eslint/tsconfig-utils": "^8.57.0", + "@typescript-eslint/types": "^8.57.0", "debug": "^4.4.3" }, "engines": { @@ -1062,14 +1062,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", - "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", + "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1" + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1080,9 +1080,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", - "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", + "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", "dev": true, "license": "MIT", "engines": { @@ -1097,15 +1097,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", - "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", + "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -1122,9 +1122,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", - "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", + "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", "dev": true, "license": "MIT", "engines": { @@ -1136,16 +1136,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", - "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", + "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.56.1", - "@typescript-eslint/tsconfig-utils": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/project-service": "8.57.0", + "@typescript-eslint/tsconfig-utils": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1164,16 +1164,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", - "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", + "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1" + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1188,13 +1188,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", - "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", + "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -3042,25 +3042,25 @@ } }, "node_modules/eslint": { - "version": "9.39.3", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", - "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.3", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -3079,7 +3079,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -3453,9 +3453,9 @@ } }, "node_modules/flatted": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", - "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", "dev": true, "license": "ISC" }, @@ -6122,9 +6122,9 @@ } }, "node_modules/sysml-v2-lsp": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/sysml-v2-lsp/-/sysml-v2-lsp-0.6.0.tgz", - "integrity": "sha512-eq24XpxLMpWqMmGBEbLuIywXpZeYgoqlGIawPpu05m+ZYhujRIgwHWo3COz9lnth487tzgs/JbqDOIHmltALaw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/sysml-v2-lsp/-/sysml-v2-lsp-0.7.0.tgz", + "integrity": "sha512-cJ2JINpe7Ez1gyMO020gEokoTLmqRFv+KTcNqGq+10PI3mE0q74anHLxFzbrjIx2wRDeoacQcMg3v9XVkvuWHw==", "hasInstallScript": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index b99debb..d2f2762 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "sysml-v2-support", "displayName": "SysML v2.0 Language Support", "description": "Complete SysML v2.0 support with visualiser, model explorer, model dashboard, feature inspector, including syntax highlighting, formatting, validation, navigation, and interactive views", - "version": "0.25.0", + "version": "0.26.0", "publisher": "JamieD", "license": "MIT", "icon": "icon.png", @@ -221,10 +221,20 @@ "when": "view == sysmlModelExplorer && viewItem == sysmlPackage", "group": "inline" }, + { + "command": "sysml.showModelDashboard", + "when": "view == sysmlModelExplorer && viewItem == sysmlPackage", + "group": "inline" + }, { "command": "sysml.visualizePackage", "when": "view == sysmlModelExplorer && viewItem == sysmlPackage", "group": "sysml@1" + }, + { + "command": "sysml.showModelDashboard", + "when": "view == sysmlModelExplorer && viewItem == sysmlPackage", + "group": "sysml@2" } ], "editor/title": [ @@ -487,7 +497,7 @@ }, "dependencies": { "elkjs": "^0.11.0", - "sysml-v2-lsp": "^0.6.0", + "sysml-v2-lsp": "^0.7.0", "vscode-languageclient": "^9.0.1" }, "overrides": { diff --git a/samples/.vscode/mcp.json b/samples/.vscode/mcp.json index 0f2ef8d..e8d1f55 100644 --- a/samples/.vscode/mcp.json +++ b/samples/.vscode/mcp.json @@ -1,4 +1,15 @@ { "$schema": "https://json.schemastore.org/mcp-config", - "servers": {} + "servers": { + "sysml-v2-local": { + "type": "stdio", + "command": "node", + "args": [ + "../../node_modules/sysml-v2-lsp/dist/server/mcpServer.js" + ], + "tools": [ + "*" + ] + } + } } diff --git a/samples/.vscode/settings.json b/samples/.vscode/settings.json index 2c63c08..8689158 100644 --- a/samples/.vscode/settings.json +++ b/samples/.vscode/settings.json @@ -1,2 +1,3 @@ { + "sysml.diagnostics.filterMode": "semantic" } diff --git a/samples/Camera Example/camera-bdd.sysml b/samples/Camera Example/camera-bdd.sysml index f949df8..251b43c 100644 --- a/samples/Camera Example/camera-bdd.sysml +++ b/samples/Camera Example/camera-bdd.sysml @@ -73,4 +73,6 @@ package CameraTestBDD { part spareLens : LensAssembly; part battery : PowerModule; } -} + + attribute def Resolution; +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index b61f480..095d388 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1031,8 +1031,68 @@ export function activate(context: vscode.ExtensionContext) { // ─── Model Dashboard ──────────────────────────────────────────── context.subscriptions.push( - vscode.commands.registerCommand('sysml.showModelDashboard', async (fileUri?: vscode.Uri) => { - ModelDashboardPanel.createOrShow(context.extensionUri, lspModelProvider, fileUri); + vscode.commands.registerCommand('sysml.showModelDashboard', async (target?: vscode.Uri | ModelTreeItem) => { + let fileUri: vscode.Uri | undefined; + let packageName: string | undefined; + + if (target instanceof vscode.Uri) { + fileUri = target; + } else if (target && typeof target === 'object' && 'elementUri' in target) { + // Tree item from Model Explorer package nodes. + const treeItem = target as ModelTreeItem; + fileUri = treeItem.elementUri; + if (treeItem.element?.type === 'package') { + packageName = treeItem.element.name; + } + } + + ModelDashboardPanel.createOrShow(context.extensionUri, lspModelProvider, fileUri, packageName); + }) + ); + + context.subscriptions.push( + vscode.commands.registerCommand('sysml.openProblemForUri', async (uriLike?: string | vscode.Uri) => { + let targetUri: vscode.Uri | undefined; + if (typeof uriLike === 'string') { + try { targetUri = vscode.Uri.parse(uriLike); } catch { /* ignore */ } + } else if (uriLike && typeof (uriLike as vscode.Uri).toString === 'function') { + targetUri = uriLike as vscode.Uri; + } + + if (!targetUri) { + const editor = vscode.window.activeTextEditor; + if (editor && editor.document.languageId === 'sysml') { + targetUri = editor.document.uri; + } + } + + if (!targetUri) { + await vscode.commands.executeCommand('workbench.actions.view.problems'); + return; + } + + const diagnostics = vscode.languages.getDiagnostics(targetUri) + .filter(d => d.severity === vscode.DiagnosticSeverity.Error || d.severity === vscode.DiagnosticSeverity.Warning) + .sort((a, b) => { + if (a.severity !== b.severity) { + return a.severity - b.severity; // Error first + } + if (a.range.start.line !== b.range.start.line) { + return a.range.start.line - b.range.start.line; + } + return a.range.start.character - b.range.start.character; + }); + + if (diagnostics.length > 0) { + const doc = await vscode.workspace.openTextDocument(targetUri); + const editor = await vscode.window.showTextDocument(doc, { preview: false, preserveFocus: false }); + const pos = diagnostics[0].range.start; + const sel = new vscode.Selection(pos, pos); + editor.selection = sel; + editor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.InCenterIfOutsideViewport); + } + + await vscode.commands.executeCommand('workbench.actions.view.problems'); }) ); @@ -1240,6 +1300,9 @@ export function activate(context: vscode.ExtensionContext) { // Re-parse for model explorer / visualization only. // Language features are handled by the LSP server. parseSysMLDocument(event.document); + + // Keep the model dashboard in sync while it's open. + ModelDashboardPanel.currentPanel?.notifyFileChanged(event.document.uri); } }) ); @@ -1266,10 +1329,11 @@ export function activate(context: vscode.ExtensionContext) { // external changes. context.subscriptions.push( vscode.workspace.onDidSaveTextDocument(document => { - if (document.languageId === 'sysml' && VisualizationPanel.currentPanel) { - // Short delay gives the LSP server time to process the save + if (document.languageId === 'sysml') { + // Short delay gives the LSP server time to process the save. setTimeout(() => { VisualizationPanel.currentPanel?.notifyFileChanged(document.uri); + ModelDashboardPanel.currentPanel?.notifyFileChanged(document.uri); }, 500); } }) @@ -1286,6 +1350,7 @@ export function activate(context: vscode.ExtensionContext) { if (VisualizationPanel.currentPanel) { VisualizationPanel.currentPanel.notifyFileChanged(uri); } + ModelDashboardPanel.currentPanel?.notifyFileChanged(uri); }) ); @@ -1295,6 +1360,7 @@ export function activate(context: vscode.ExtensionContext) { if (VisualizationPanel.currentPanel) { VisualizationPanel.currentPanel.notifyFileChanged(uri); } + ModelDashboardPanel.currentPanel?.notifyFileChanged(uri); }) ); @@ -1304,6 +1370,7 @@ export function activate(context: vscode.ExtensionContext) { if (VisualizationPanel.currentPanel) { VisualizationPanel.currentPanel.notifyFileChanged(uri); } + ModelDashboardPanel.currentPanel?.notifyFileChanged(uri); }) ); } diff --git a/src/panels/modelDashboardPanel.ts b/src/panels/modelDashboardPanel.ts index 639024f..f1e6dd6 100644 --- a/src/panels/modelDashboardPanel.ts +++ b/src/panels/modelDashboardPanel.ts @@ -39,10 +39,18 @@ interface DashboardData { }; resolvedTypes: Record; fileName: string; + fileUri: string; diagnosticErrors: number; diagnosticWarnings: number; } +interface DashboardTargetOption { + value: string; + label: string; +} + +type DashboardTargetMode = 'file' | 'semantic'; + export class ModelDashboardPanel { public static currentPanel: ModelDashboardPanel | undefined; @@ -51,18 +59,60 @@ export class ModelDashboardPanel { private _lspModelProvider: LspModelProvider | undefined; /** Cached dashboard data for instant re-renders. */ private _lastData: DashboardData | undefined; + /** Cache key for last successful model fetch: `${uri}|${documentVersion}`. */ + private _lastDataKey: string | undefined; + /** Prevent duplicate concurrent refreshes from triggering parallel LSP calls. */ + private _refreshInFlight: Promise | undefined; + /** True while dashboard is refreshing data in the background. */ + private _isUpdating = false; /** Explicit document URI, used when no text editor is active (e.g. opened from visualizer). */ private _documentUri: vscode.Uri | undefined; + /** Debounces rapid change/save/fs events into a single refresh. */ + private _refreshTimer: ReturnType | undefined; + /** Workspace-mode dropdown options (files/packages). */ + private _targetOptions: DashboardTargetOption[] = []; + /** Workspace-mode file options. */ + private _fileOptions: DashboardTargetOption[] = []; + /** Workspace-mode semantic/package options. */ + private _semanticOptions: DashboardTargetOption[] = []; + /** Package name -> source file URIs that contain the package. */ + private _packageSources = new Map(); + /** Whether dropdown options should be recomputed. */ + private _targetOptionsDirty = true; + /** Current target mode, matching model explorer concepts. */ + private _targetMode: DashboardTargetMode = 'semantic'; + /** Optional selected package label for workspace package selection. */ + private _selectedPackageName: string | undefined; private constructor( panel: vscode.WebviewPanel, lspModelProvider: LspModelProvider | undefined, documentUri?: vscode.Uri, + packageName?: string, ) { this._panel = panel; this._lspModelProvider = lspModelProvider; this._documentUri = documentUri; + this._selectedPackageName = packageName; + if (packageName) { + this._targetMode = 'semantic'; + } this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + this._panel.webview.onDidReceiveMessage( + (message: unknown) => { + if (!message || typeof message !== 'object') { + return; + } + const msg = message as { command?: string; value?: unknown }; + if (msg.command === 'selectDashboardTarget' && typeof msg.value === 'string') { + void this._handleTargetSelection(msg.value); + } else if (msg.command === 'setDashboardTargetMode' && typeof msg.value === 'string') { + void this._handleTargetModeSelection(msg.value); + } + }, + null, + this._disposables, + ); } /** Create or reveal the Model Dashboard panel. */ @@ -70,6 +120,7 @@ export class ModelDashboardPanel { extensionUri: vscode.Uri, lspModelProvider: LspModelProvider | undefined, documentUri?: vscode.Uri, + packageName?: string, ): Promise { if (ModelDashboardPanel.currentPanel) { ModelDashboardPanel.currentPanel._panel.reveal(vscode.ViewColumn.Active); @@ -77,6 +128,10 @@ export class ModelDashboardPanel { if (documentUri) { ModelDashboardPanel.currentPanel._documentUri = documentUri; } + if (packageName) { + ModelDashboardPanel.currentPanel._selectedPackageName = packageName; + ModelDashboardPanel.currentPanel._targetMode = 'semantic'; + } // Show cached data instantly, then refresh in background if (ModelDashboardPanel.currentPanel._lastData) { ModelDashboardPanel.currentPanel._panel.webview.html = @@ -90,10 +145,10 @@ export class ModelDashboardPanel { 'sysmlModelDashboard', 'SysML Model Dashboard', vscode.ViewColumn.Active, - { enableScripts: false, localResourceRoots: [] }, + { enableScripts: true, enableCommandUris: true, localResourceRoots: [] }, ); - const dashboard = new ModelDashboardPanel(panel, lspModelProvider, documentUri); + const dashboard = new ModelDashboardPanel(panel, lspModelProvider, documentUri, packageName); ModelDashboardPanel.currentPanel = dashboard; // Show a loading skeleton immediately so the panel isn't blank dashboard._panel.webview.html = dashboard._loadingHtml(); @@ -111,8 +166,42 @@ export class ModelDashboardPanel { await this._refresh(); } + /** + * Notify dashboard that a SysML file changed. + * If the changed file is the file currently shown by the dashboard, + * schedule a debounced refresh. + */ + notifyFileChanged(uri: vscode.Uri): void { + const inWorkspaceMode = !!vscode.workspace.workspaceFile; + const affectsCurrentTarget = this._isCurrentDashboardUri(uri); + + if (!affectsCurrentTarget && !inWorkspaceMode) { + return; + } + + if (inWorkspaceMode) { + // Keep dropdown entries fresh when workspace files/packages change. + this._targetOptionsDirty = true; + } + // Invalidate version-key cache so the next refresh re-queries model data. + this._lastDataKey = undefined; + + if (this._refreshTimer) { + clearTimeout(this._refreshTimer); + } + + this._refreshTimer = setTimeout(() => { + this._refreshTimer = undefined; + void this._refresh(); + }, 300); + } + dispose(): void { ModelDashboardPanel.currentPanel = undefined; + if (this._refreshTimer) { + clearTimeout(this._refreshTimer); + this._refreshTimer = undefined; + } this._panel.dispose(); for (const d of this._disposables) { d.dispose(); @@ -122,33 +211,97 @@ export class ModelDashboardPanel { // ─── Private ─────────────────────────────────────────────────── private async _refresh(): Promise { + if (this._refreshInFlight) { + await this._refreshInFlight; + return; + } + + this._setUpdating(true); + + this._refreshInFlight = this._refreshCore(); + try { + await this._refreshInFlight; + } finally { + this._refreshInFlight = undefined; + this._setUpdating(false); + } + } + + private _setUpdating(isUpdating: boolean): void { + if (this._isUpdating === isUpdating) { + return; + } + this._isUpdating = isUpdating; + if (this._lastData) { + this._panel.webview.html = this._buildHtml(this._lastData); + } + } + + private async _refreshCore(): Promise { if (!this._lspModelProvider) { + this._lastDataKey = undefined; this._panel.webview.html = this._emptyHtml( 'Model Dashboard requires an LSP model provider', ); return; } - // Prefer the active text editor; fall back to the explicitly provided URI - // (e.g. when opened from the visualizer where no text editor is active). const editor = vscode.window.activeTextEditor; let uri: string; let fileName: string; let diagnosticUri: vscode.Uri; + let documentVersion = -1; if (editor && editor.document.languageId === 'sysml') { uri = editor.document.uri.toString(); fileName = editor.document.fileName.split('/').pop() ?? 'unknown'; diagnosticUri = editor.document.uri; + documentVersion = editor.document.version; + if (!this._documentUri) { + this._documentUri = editor.document.uri; + } } else if (this._documentUri) { uri = this._documentUri.toString(); fileName = this._documentUri.path.split('/').pop() ?? 'unknown'; diagnosticUri = this._documentUri; + const openDoc = vscode.workspace.textDocuments?.find(d => d.uri.toString() === uri); + documentVersion = openDoc?.version ?? -1; } else { + this._lastDataKey = undefined; this._panel.webview.html = this._emptyHtml('Open a SysML file to see the dashboard'); return; } + await this._refreshTargetOptions(uri); + + if (this._targetMode === 'semantic' && this._selectedPackageName) { + const sourceUris = this._packageSources.get(this._selectedPackageName); + const semanticUri = sourceUris && sourceUris.length > 0 ? sourceUris[0] : undefined; + if (semanticUri) { + uri = semanticUri; + diagnosticUri = vscode.Uri.parse(semanticUri); + fileName = diagnosticUri.path.split('/').pop() ?? 'unknown'; + const openDoc = vscode.workspace.textDocuments?.find(d => d.uri.toString() === semanticUri); + documentVersion = openDoc?.version ?? -1; + this._documentUri = diagnosticUri; + } + } + + const dataKey = documentVersion >= 0 ? `${uri}|${documentVersion}` : undefined; + if (this._lastData && dataKey && dataKey === this._lastDataKey) { + const { diagnosticErrors, diagnosticWarnings } = this._countDiagnostics(diagnosticUri); + if (this._lastData.diagnosticErrors !== diagnosticErrors || this._lastData.diagnosticWarnings !== diagnosticWarnings) { + this._lastData = { + ...this._lastData, + diagnosticErrors, + diagnosticWarnings, + fileName, + }; + } + this._panel.webview.html = this._buildHtml(this._lastData); + return; + } + try { const result = await this._lspModelProvider.getModel(uri, ['resolvedTypes']); const rawStats = result.stats ?? { @@ -163,34 +316,258 @@ export class ModelDashboardPanel { complexity: rawStats.complexity as ComplexityData | undefined, }; - const diagnostics = vscode.languages.getDiagnostics(diagnosticUri); - const diagnosticErrors = diagnostics.filter( - d => d.severity === vscode.DiagnosticSeverity.Error, - ).length; - const diagnosticWarnings = diagnostics.filter( - d => d.severity === vscode.DiagnosticSeverity.Warning, - ).length; + const { diagnosticErrors, diagnosticWarnings } = this._countDiagnostics(diagnosticUri); const data: DashboardData = { stats, resolvedTypes: result.resolvedTypes ?? {}, fileName, + fileUri: diagnosticUri.toString(), diagnosticErrors, diagnosticWarnings, }; this._lastData = data; + this._lastDataKey = dataKey; this._panel.webview.html = this._buildHtml(data); } catch { this._panel.webview.html = this._emptyHtml('Failed to fetch model data from LSP server'); } } + private async _refreshTargetOptions(currentUri: string): Promise { + if (!vscode.workspace.workspaceFile) { + this._targetOptions = []; + this._fileOptions = []; + this._semanticOptions = []; + this._packageSources.clear(); + this._targetOptionsDirty = false; + return; + } + + if (!this._targetOptionsDirty && this._targetOptions.length > 0) { + return; + } + + const files = await vscode.workspace.findFiles('**/*.sysml', '**/node_modules/**'); + const fileOptions: DashboardTargetOption[] = []; + const packageSources = new Map(); + + for (const file of files) { + const fileUri = file.toString(); + const rel = vscode.workspace.asRelativePath(file, false); + fileOptions.push({ + value: `file:${encodeURIComponent(fileUri)}`, + label: rel, + }); + + const packageNames = await this._extractPackageNames(file); + for (const pkg of packageNames) { + const existing = packageSources.get(pkg) ?? []; + if (!existing.includes(fileUri)) { + existing.push(fileUri); + } + packageSources.set(pkg, existing); + } + } + + const currentFileValue = `file:${encodeURIComponent(currentUri)}`; + if (!fileOptions.some(o => o.value === currentFileValue)) { + fileOptions.unshift({ + value: currentFileValue, + label: vscode.workspace.asRelativePath(vscode.Uri.parse(currentUri), false), + }); + } + + const semanticOptions: DashboardTargetOption[] = [...packageSources.keys()] + .sort((a, b) => a.localeCompare(b)) + .map(pkgName => { + const occurrences = packageSources.get(pkgName)?.length ?? 0; + const suffix = occurrences > 1 ? ` (${occurrences} files)` : ''; + return { + value: `pkg:${encodeURIComponent(pkgName)}`, + label: `${pkgName}${suffix}`, + }; + }); + + this._fileOptions = fileOptions; + this._semanticOptions = semanticOptions; + this._packageSources = packageSources; + this._targetOptions = this._targetMode === 'semantic' ? this._semanticOptions : this._fileOptions; + this._targetOptionsDirty = false; + + if (this._targetMode === 'semantic') { + const selectedPackageName = this._selectedPackageName; + if (selectedPackageName && !this._semanticOptions.some(o => o.value === this._toPackageOptionValue(selectedPackageName))) { + this._selectedPackageName = undefined; + } + if (!this._selectedPackageName && this._semanticOptions.length > 0) { + const first = this._semanticOptions[0].value; + if (first.startsWith('pkg:')) { + this._selectedPackageName = decodeURIComponent(first.slice(4)); + } + } + } + } + + private async _extractPackageNames(uri: vscode.Uri): Promise { + try { + const existingDoc = vscode.workspace.textDocuments?.find(d => d.uri.toString() === uri.toString()); + const doc = existingDoc ?? await vscode.workspace.openTextDocument(uri); + const text = doc.getText(); + const re = /\bpackage\s+([A-Za-z_][A-Za-z0-9_:]*)/g; + const names = new Set(); + let m: RegExpExecArray | null; + while ((m = re.exec(text)) !== null) { + names.add(m[1]); + } + return [...names]; + } catch { + return []; + } + } + + private _toPackageOptionValue(packageName: string): string { + return `pkg:${encodeURIComponent(packageName)}`; + } + + private _selectedTargetValue(fileUri: string): string { + if (this._targetMode === 'semantic') { + const selectedPackageName = this._selectedPackageName; + if (selectedPackageName) { + const pkgValue = this._toPackageOptionValue(selectedPackageName); + if (this._targetOptions.some(o => o.value === pkgValue)) { + return pkgValue; + } + } + return this._targetOptions[0]?.value ?? ''; + } + return `file:${encodeURIComponent(fileUri)}`; + } + + private async _handleTargetSelection(value: string): Promise { + if (!value.startsWith('file:') && !value.startsWith('pkg:')) { + return; + } + + if (value.startsWith('file:')) { + const encodedUri = value.slice(5); + let decodedUri: string; + try { + decodedUri = decodeURIComponent(encodedUri); + } catch { + return; + } + this._selectedPackageName = undefined; + this._targetMode = 'file'; + this._targetOptions = this._fileOptions; + + try { + this._documentUri = vscode.Uri.parse(decodedUri); + } catch { + return; + } + } else { + const encodedPkg = value.slice(4); + try { + this._selectedPackageName = decodeURIComponent(encodedPkg); + } catch { + this._selectedPackageName = undefined; + } + this._targetMode = 'semantic'; + this._targetOptions = this._semanticOptions; + + const sourceUris = this._selectedPackageName + ? this._packageSources.get(this._selectedPackageName) + : undefined; + const firstSource = sourceUris && sourceUris.length > 0 ? sourceUris[0] : undefined; + if (firstSource) { + this._documentUri = vscode.Uri.parse(firstSource); + } + } + + this._lastDataKey = undefined; + this._setUpdating(true); + if (this._lastData) { + this._panel.webview.html = this._buildHtml(this._lastData); + } + void this._refresh(); + } + + private async _handleTargetModeSelection(mode: string): Promise { + if (mode !== 'file' && mode !== 'semantic') { + return; + } + + const nextMode = mode as DashboardTargetMode; + if (this._targetMode === nextMode && this._targetOptions.length > 0) { + return; + } + + this._targetMode = nextMode; + this._targetOptions = this._targetMode === 'semantic' ? this._semanticOptions : this._fileOptions; + + if (this._targetMode === 'semantic') { + if (!this._selectedPackageName && this._targetOptions.length > 0) { + const first = this._targetOptions[0].value; + if (first.startsWith('pkg:')) { + this._selectedPackageName = decodeURIComponent(first.slice(4)); + } + } + const sourceUris = this._selectedPackageName + ? this._packageSources.get(this._selectedPackageName) + : undefined; + const firstSource = sourceUris && sourceUris.length > 0 ? sourceUris[0] : undefined; + if (firstSource) { + this._documentUri = vscode.Uri.parse(firstSource); + } + } else { + this._selectedPackageName = undefined; + const selectedFileValue = this._targetOptions[0]?.value; + if (selectedFileValue && selectedFileValue.startsWith('file:')) { + const decodedUri = decodeURIComponent(selectedFileValue.slice(5)); + this._documentUri = vscode.Uri.parse(decodedUri); + } + } + + this._lastDataKey = undefined; + this._setUpdating(true); + if (this._lastData) { + this._panel.webview.html = this._buildHtml(this._lastData); + } + void this._refresh(); + } + + private _isCurrentDashboardUri(uri: vscode.Uri): boolean { + const target = uri.toString(); + + if (this._lastData?.fileUri) { + return this._lastData.fileUri === target; + } + + if (this._documentUri) { + return this._documentUri.toString() === target; + } + + const editor = vscode.window.activeTextEditor; + return !!editor && editor.document.languageId === 'sysml' && editor.document.uri.toString() === target; + } + + private _countDiagnostics(uri: vscode.Uri): { diagnosticErrors: number; diagnosticWarnings: number } { + let diagnosticErrors = 0; + let diagnosticWarnings = 0; + for (const d of vscode.languages.getDiagnostics(uri)) { + if (d.severity === vscode.DiagnosticSeverity.Error) diagnosticErrors++; + else if (d.severity === vscode.DiagnosticSeverity.Warning) diagnosticWarnings++; + } + return { diagnosticErrors, diagnosticWarnings }; + } + /** Loading skeleton shown while the LSP request is in flight. */ private _loadingHtml(): string { return ` - +