diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..4114e81 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,47 @@ +name: Bug report +description: Something in SIMAI Framework IntelliSense does not work as expected. +labels: [bug] +body: + - type: markdown + attributes: + value: | + Use this form for problems with the VS Code extension itself: completions, + hovers, colour swatches, diagnostics or packaging. If a SIMAI Framework + component, class or token is wrong in the Framework, report it to + [simai/ui-source](https://github.com/simai/ui-source/issues) instead. + - type: input + id: version + attributes: + label: Extension version + description: Shown in the Extensions view, for example 0.2.0. + validations: + required: true + - type: input + id: vscode + attributes: + label: VS Code version and OS + description: From Help → About, for example 1.142.0 on macOS 26. + validations: + required: true + - type: input + id: language + attributes: + label: File language + description: For example HTML, TSX, Vue, CSS, Blade or PHP. + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: A minimal snippet and where the cursor was. + placeholder: | + 1. Open a .html file + 2. Type > "$GITHUB_OUTPUT" + elif git ls-remote --exit-code --heads origin "$BRANCH" > /dev/null; then + echo "::notice::Branch ${BRANCH} already exists; not opening another pull request." + echo "stale=false" >> "$GITHUB_OUTPUT" + else + echo "stale=true" >> "$GITHUB_OUTPUT" + fi + echo "latest=${LATEST}" >> "$GITHUB_OUTPUT" + echo "current=${CURRENT}" >> "$GITHUB_OUTPUT" + echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT" + + - if: steps.compare.outputs.stale == 'true' + uses: actions/checkout@v4 + with: + repository: simai/ui + path: .framework/ui + fetch-depth: 0 + - if: steps.compare.outputs.stale == 'true' + uses: actions/checkout@v4 + with: + repository: simai/ui-source + path: .framework/ui-source + fetch-depth: 0 + + - name: Vendor the new release + if: steps.compare.outputs.stale == 'true' + run: | + npm ci + npm run vendor:framework -- --ui .framework/ui --ui-source .framework/ui-source \ + --release "${{ steps.compare.outputs.latest }}" + npm run generate + + - name: Gates + id: gates + if: steps.compare.outputs.stale == 'true' + continue-on-error: true + run: | + set -o pipefail + { + npm run pretest && npm test && npm run generate:check \ + && xvfb-run -a npm run test:extension \ + && npx vsce ls | node out/generate/checkPackage.js + } 2>&1 | tail -n 60 | tee gates.log + + - name: Open a pull request + if: steps.compare.outputs.stale == 'true' + env: + GH_TOKEN: ${{ github.token }} + LATEST: ${{ steps.compare.outputs.latest }} + CURRENT: ${{ steps.compare.outputs.current }} + BRANCH: ${{ steps.compare.outputs.branch }} + GATES: ${{ steps.gates.outcome }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git switch -c "$BRANCH" + git add contracts data + git commit -m "Repin to SIMAI Framework release ${LATEST}" + git push origin "$BRANCH" + DRAFT="" + [ "$GATES" = "success" ] || DRAFT="--draft" + { + echo "Vendored ${LATEST} (was ${CURRENT}) with \`npm run vendor:framework\`." + echo + echo "Gates in the freshness run: **${GATES}**. Update the pinned counts in the" + echo "tests and the CHANGELOG, and check the diff of data/ before merging." + echo + echo "Pull requests opened with the workflow token do not start other workflows;" + echo "push a commit or re-run CI to get the CI checks." + echo + echo '```' + tail -n 40 gates.log + echo '```' + } > body.md + gh pr create --base main --head "$BRANCH" $DRAFT \ + --title "Repin to SIMAI Framework release ${LATEST}" --body-file body.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..bb59980 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,71 @@ +name: Release + +on: + push: + tags: ['v*'] + +permissions: + contents: read + +jobs: + version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Tag matches package.json version + run: | + VERSION=$(node -p "require('./package.json').version") + if [ "${GITHUB_REF_NAME}" != "v${VERSION}" ]; then + echo "::error::Tag ${GITHUB_REF_NAME} does not match package.json version ${VERSION}" + exit 1 + fi + + gate: + needs: version + uses: ./.github/workflows/ci.yml + + release: + needs: gate + runs-on: ubuntu-latest + permissions: + contents: write + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + OVSX_PAT: ${{ secrets.OVSX_PAT }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - name: Package + id: package + run: | + VSIX="sf-vscode-${GITHUB_REF_NAME#v}.vsix" + npx vsce package -o "$VSIX" + sha256sum "$VSIX" > "$VSIX.sha256" + echo "vsix=$VSIX" >> "$GITHUB_OUTPUT" + echo "sha256=$(cut -d' ' -f1 "$VSIX.sha256")" >> "$GITHUB_OUTPUT" + - name: GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "$GITHUB_REF_NAME" \ + "${{ steps.package.outputs.vsix }}" "${{ steps.package.outputs.vsix }}.sha256" \ + --title "$GITHUB_REF_NAME" \ + --notes "SIMAI Framework IntelliSense ${GITHUB_REF_NAME}. See CHANGELOG.md. + + sha256 of ${{ steps.package.outputs.vsix }}: \`${{ steps.package.outputs.sha256 }}\`" + - name: Publish to the VS Code Marketplace + if: env.VSCE_PAT != '' + run: npx vsce publish --packagePath "${{ steps.package.outputs.vsix }}" --pat "$VSCE_PAT" + - name: Skip the VS Code Marketplace + if: env.VSCE_PAT == '' + run: echo "::notice::VSCE_PAT is not set; skipped publishing to the VS Code Marketplace." + - name: Publish to Open VSX + if: env.OVSX_PAT != '' + run: npx --yes ovsx@1.3.0 publish "${{ steps.package.outputs.vsix }}" --pat "$OVSX_PAT" + - name: Skip Open VSX + if: env.OVSX_PAT == '' + run: echo "::notice::OVSX_PAT is not set; skipped publishing to Open VSX." diff --git a/.vscodeignore b/.vscodeignore index 07baf4d..81c7d92 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -8,6 +8,7 @@ !LICENSE !images/icon.png !data/*.json +!snippets/*.json !out/*.js !out/providers/*.js !out/registry/*.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 187a794..067db21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,37 @@ All notable changes to SIMAI Framework IntelliSense are listed here. -## [0.1.0] - Unreleased +## [0.2.0] - Unreleased + +### Added + +- Diagnostics for deprecated `sf-` tags and attributes (Warning) and for likely + misspellings of Framework tokens, tags and attributes (Information), with + Quick Fixes to the nearest valid name. Severity is configurable with + `sf.diagnostics.*`. +- Blade and PHP templates: class attributes, `sf-` tags and attributes, and + tokens in `style` attributes and `\n', + 'editor.action.showHover', + 2500, + ); + fs.rmSync(root, { recursive: true, force: true }); +} diff --git a/src/test/suite/completion.ext.test.ts b/src/test/suite/completion.ext.test.ts index a5f2d03..f40dcd7 100644 --- a/src/test/suite/completion.ext.test.ts +++ b/src/test/suite/completion.ext.test.ts @@ -11,6 +11,7 @@ const extensions: Record = { vue: 'vue', typescript: 'ts', typescriptreact: 'tsx', + php: 'php', }; const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'sf-docs-')); let fixtureCount = 0; @@ -162,7 +163,7 @@ suite('hover provider', () => { assert.match(text, /--sf-primary/); assert.match(text, /\| light \| #005bbe \|/); assert.match(text, /\| dark \| #acc7ff \|/); - assert.match(text, /ui-a9636b60d3e3/); + assert.match(text, /ui-3521193e0c6b/); }); test('sizing tokens show mobile and desktop values', async () => { @@ -210,3 +211,92 @@ suite('colour provider', () => { assert.ok(presentations.some(item => item.label === '--sf-primary')); }); }); + +async function diagnosticsFor(language: string, content: string): Promise<{ + document: vscode.TextDocument; + diagnostics: vscode.Diagnostic[]; +}> { + const { document } = await open(language, `|${content}`); + for (let attempt = 0; attempt < 50; attempt += 1) { + const diagnostics = vscode.languages.getDiagnostics(document.uri) + .filter(item => item.source === 'SIMAI Framework'); + if (diagnostics.length > 0) return { document, diagnostics }; + await new Promise(resolve => setTimeout(resolve, 100)); + } + return { document, diagnostics: [] }; +} + +suite('diagnostics and quick fixes', () => { + test('a misspelt token is reported and fixed to the nearest name', async () => { + const { document, diagnostics } = await diagnosticsFor('css', '.a { color: var(--sf-primay); }'); + assert.equal(diagnostics.length, 1); + assert.equal(diagnostics[0].severity, vscode.DiagnosticSeverity.Information); + assert.equal(diagnostics[0].code, 'unknown-token'); + const actions = await vscode.commands.executeCommand( + 'vscode.executeCodeActionProvider', + document.uri, + diagnostics[0].range, + vscode.CodeActionKind.QuickFix.value, + ); + const fix = actions.find(action => action.title === 'Replace with --sf-primary'); + assert.ok(fix?.edit, 'quick fix missing'); + assert.ok(await vscode.workspace.applyEdit(fix.edit)); + assert.equal(document.getText(), '.a { color: var(--sf-primary); }'); + }); + + test('deprecated attributes are warnings; misspelt attributes are fixed', async () => { + const { document, diagnostics } = await diagnosticsFor( + 'html', + '\n', + ); + const deprecated = diagnostics.find(item => item.code === 'deprecated-attribute')!; + assert.equal(deprecated.severity, vscode.DiagnosticSeverity.Warning); + assert.deepEqual(deprecated.tags, [vscode.DiagnosticTag.Deprecated]); + const typo = diagnostics.find(item => item.code === 'unknown-attribute')!; + const actions = await vscode.commands.executeCommand( + 'vscode.executeCodeActionProvider', + document.uri, + typo.range, + vscode.CodeActionKind.QuickFix.value, + ); + assert.ok(actions.some(action => action.title === 'Replace with appearance')); + }); + + test('valid and project-owned names produce no diagnostics', async () => { + const { document } = await open('html', '|'); + await new Promise(resolve => setTimeout(resolve, 800)); + assert.deepEqual( + vscode.languages.getDiagnostics(document.uri).filter(item => item.source === 'SIMAI Framework'), + [], + ); + }); +}); + +suite('PHP templates, snippets and documentation links', () => { + test('PHP templates complete tags, attributes and classes', async () => { + assert.ok(find(await completions('php', '\n
{ + const items = await completions('html', 'sf-fie|'); + const snippet = items.find(item => item.kind === vscode.CompletionItemKind.Snippet && labelOf(item) === 'sf-field'); + assert.ok(snippet, 'sf-field snippet missing'); + }); + + test('hovers link to the configured documentation site', async () => { + const config = vscode.workspace.getConfiguration('sf'); + assert.doesNotMatch(await hoverText('html', ''), /SIMAI Framework docs/); + await config.update('docs.baseUrl', 'https://docs.example', vscode.ConfigurationTarget.Global); + try { + assert.match( + await hoverText('html', ''), + /\[SIMAI Framework docs\]\(https:\/\/docs\.example\/smart\.buttons\)/, + ); + } finally { + await config.update('docs.baseUrl', undefined, vscode.ConfigurationTarget.Global); + } + }); +});