├── .devcontainer └── devcontainer.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── config.yml ├── dependabot.yml └── workflows │ ├── build.yml │ ├── linter.yml │ └── release.yml ├── .gitignore ├── .markdownlint.yml ├── .prettierrc.json ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── CITATION.cff ├── LICENSE ├── README.md ├── assets ├── images │ ├── editor-context.png │ ├── explorer-context.png │ ├── explorer-view.png │ ├── install-extensions.png │ └── use-template.png ├── logo │ ├── logo-social.png │ ├── logo.png │ └── logo.svg └── templates │ ├── julia.qmd │ ├── python.qmd │ └── r.qmd ├── eslint.config.mjs ├── markdownlint-rules └── 001-blanks-around-fenced-divs.js ├── package-lock.json ├── package.json ├── src ├── commands │ ├── installQuartoExtension.ts │ └── newQuartoReprex.ts ├── constants.ts ├── extension.ts ├── ui │ ├── extensionsInstalled.ts │ └── extensionsQuickPick.ts └── utils │ ├── activate.ts │ ├── ask.ts │ ├── extensionDetails.ts │ ├── extensions.ts │ ├── handleUri.ts │ ├── hash.ts │ ├── lint.ts │ ├── log.ts │ ├── network.ts │ ├── quarto.ts │ ├── reprex.ts │ └── workspace.ts ├── tsconfig.json └── webpack.config.js /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Quarto Wizard", 3 | "image": "buildpack-deps:noble-curl", 4 | "remoteUser": "vscode", 5 | "features": { 6 | "ghcr.io/devcontainers/features/common-utils:2": { 7 | "installZsh": "true", 8 | "username": "vscode", 9 | "userUid": "1001", 10 | "userGid": "1001", 11 | "upgradePackages": "true" 12 | }, 13 | "ghcr.io/devcontainers/features/git:1": { 14 | "version": "latest", 15 | "ppa": "false" 16 | }, 17 | "ghcr.io/devcontainers/features/node:1": { 18 | "nodeGypDependencies": true, 19 | "version": "latest", 20 | "pnpmVersion": "latest", 21 | "nvmVersion": "latest" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mcanouil # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report an error or unexpected behaviour 3 | labels: 4 | - "Type: Bug :bug:" 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Welcome to the Quarto Wizard GitHub repository! 11 | We are always happy to hear feedback from our users. 12 | 13 | - If you want to ask for a feature, please use the [Feature Requests GitHub Discussions](https://github.com/mcanouil/quarto-wizard/discussions/categories/feature-requests). 14 | - If you want to ask for help, please use the [Q&A GitHub Discussions](https://github.com/mcanouil/quarto-wizard/discussions/categories/q-a). 15 | - If you're reporting an issue with Quarto CLI, please visit [Quarto CLI GitHub repository](https://github.com/quarto-dev/quarto-cli). 16 | 17 | Thank you for using Quarto Wizard! 18 | 19 | - type: textarea 20 | attributes: 21 | label: Bug description 22 | description: Description of the bug. 23 | placeholder: Please describe the bug here. 24 | 25 | - type: textarea 26 | attributes: 27 | label: Steps to reproduce 28 | description: | 29 | Tell us how to reproduce this bug. 30 | 31 | - type: textarea 32 | attributes: 33 | label: Actual behaviour 34 | description: Tell us what happens instead. 35 | 36 | - type: textarea 37 | attributes: 38 | label: Expected behaviour 39 | description: Tell us what should happen. 40 | 41 | - type: dropdown 42 | id: ide 43 | attributes: 44 | label: What IDE are you seeing the problem on? 45 | description: Consider upgrading to the latest version before submitting a bug report. If "Other", please specify. 46 | multiple: true 47 | options: 48 | - Visual Studio Code 49 | - Positron 50 | - VSCodium 51 | - Other 52 | 53 | - type: input 54 | id: quarto-cli-version 55 | attributes: 56 | label: What version of Quarto CLI are you using? 57 | description: Please document the Quarto CLI version being installed in your environment (_e.g._, 1.3.450, 1.4.557, 1.5.57, 1.6.40, _etc._). 58 | placeholder: 1.3.450, 1.4.557, 1.5.57, 1.6.40, ... 59 | 60 | - type: input 61 | id: os 62 | attributes: 63 | label: What operating system are you seeing the problem on? 64 | description: Please document the operating system you're running (_e.g., MacOS Ventura 13.4, Windows 11, Linux Debian 11, _etc._). 65 | placeholder: MacOS Ventura 13.4, Windows 11, Linux Debian 11, ... 66 | 67 | - type: textarea 68 | attributes: 69 | label: Your environment (if necessary) 70 | description: | 71 | If necessary, please document any relevant information regarding your environment. 72 | 73 | - type: markdown 74 | attributes: 75 | value: "_Thanks for submitting this bug report!_" 76 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Ask a question 4 | url: https://github.com/mcanouil/quarto-wizard/discussions/categories/q-a 5 | about: Please ask and answer questions in our Discussion board 6 | - name: Share an idea, a missing feature or anything else 7 | url: https://github.com/mcanouil/quarto-wizard/discussions/categories/feature-requests 8 | about: Please give us any feedback in our Discussion board 9 | - name: Report a Quarto CLI bug 10 | url: https://github.com/quarto-dev/quarto-cli 11 | about: Please report any issue with Quarto CLI in its repository 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | labels: 13 | - "Type: Dependencies :arrow_up:" 14 | - package-ecosystem: "github-actions" 15 | directory: "/" 16 | schedule: 17 | interval: "monthly" 18 | labels: 19 | - "Type: Dependencies :arrow_up:" 20 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2025 Mickaël CANOUIL 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | name: Build Extension 24 | 25 | on: 26 | workflow_dispatch: 27 | pull_request: 28 | types: 29 | - synchronize 30 | - ready_for_review 31 | branches: 32 | - main 33 | paths: 34 | - "src/**" 35 | - "package.json" 36 | - "package-lock.json" 37 | 38 | concurrency: 39 | group: ${{ github.workflow }}-${{ github.action }}-${{ github.ref }}-${{ github.event_name }} 40 | cancel-in-progress: true 41 | 42 | permissions: read-all 43 | 44 | jobs: 45 | build: 46 | runs-on: ubuntu-latest 47 | if: github.event.pull_request.draft == false 48 | steps: 49 | - name: Checkout repository 50 | uses: actions/checkout@v4 51 | 52 | - name: Set up Node.js 53 | uses: actions/setup-node@v4 54 | 55 | - name: Install dependencies 56 | shell: bash 57 | run: npm install 58 | 59 | - name: Install Visual Studio Code Extension Manager 60 | shell: bash 61 | run: npm install -g @vscode/vsce 62 | 63 | - name: Build extension 64 | shell: bash 65 | run: vsce package --pre-release --out quarto-wizard.vsix 66 | 67 | - name: Upload VSIX as workflow artifact 68 | uses: actions/upload-artifact@v4 69 | with: 70 | name: quarto-wizard-${{ github.sha }} 71 | path: quarto-wizard.vsix 72 | retention-days: 10 73 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: "Super Linter" 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.action }}-${{ github.ref }}-${{ github.event_name }} 8 | cancel-in-progress: true 9 | 10 | permissions: read-all 11 | 12 | jobs: 13 | linter: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: read 18 | statuses: write 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | - name: Super-Linter 25 | uses: super-linter/super-linter@v7 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | DEFAULT_BRANCH: main 29 | IGNORE_GITIGNORED_FILES: true 30 | VALIDATE_ALL_CODEBASE: false 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2025 Mickaël CANOUIL 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | name: Release and Publish VS Code Extension 24 | 25 | on: 26 | workflow_dispatch: 27 | inputs: 28 | type: 29 | type: choice 30 | description: Type 31 | options: 32 | - release 33 | - pre-release 34 | default: release 35 | date: 36 | type: string 37 | description: 'Date ("YYYY-MM-DD" or "today")' 38 | default: today 39 | version: 40 | type: string 41 | description: "Version" 42 | default: "minor" 43 | 44 | jobs: 45 | update-changelog: 46 | runs-on: ubuntu-latest 47 | 48 | permissions: 49 | contents: write 50 | pull-requests: write 51 | 52 | env: 53 | BRANCH: ci/update-changelog-release 54 | GITHUB_TOKEN: ${{ secrets.github_token }} 55 | 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@v4 59 | 60 | - name: Check for "unreleased" in CHANGELOG.md 61 | id: check_unreleased 62 | shell: bash 63 | run: | 64 | if grep -q "Unreleased" CHANGELOG.md; then 65 | echo "UNRELEASED_FOUND=true" >> $GITHUB_OUTPUT 66 | else 67 | echo "UNRELEASED_FOUND=false" >> $GITHUB_OUTPUT 68 | fi 69 | 70 | - name: Set up Node.js 71 | if: ${{ steps.check_unreleased.outputs.UNRELEASED_FOUND == 'true' }} 72 | uses: actions/setup-node@v4 73 | 74 | - name: Install dependencies 75 | if: ${{ steps.check_unreleased.outputs.UNRELEASED_FOUND == 'true' }} 76 | shell: bash 77 | run: npm install 78 | 79 | - name: Install Visual Studio Code Extension Manager 80 | if: ${{ steps.check_unreleased.outputs.UNRELEASED_FOUND == 'true' }} 81 | shell: bash 82 | run: npm install -g @vscode/vsce 83 | 84 | - name: Bump Version / Commit / Push CHANGELOG.md 85 | if: ${{ steps.check_unreleased.outputs.UNRELEASED_FOUND == 'true' }} 86 | env: 87 | GH_TOKEN: ${{ secrets.github_token }} 88 | COMMIT: "ci: bump version for release :rocket:" 89 | shell: bash 90 | run: | 91 | git config --local user.name github-actions[bot] 92 | git config --local user.email 41898282+github-actions[bot]@users.noreply.github.com 93 | if git show-ref --quiet refs/heads/${BRANCH}; then 94 | echo "Branch ${BRANCH} already exists." 95 | git branch -D "${BRANCH}" 96 | git push origin --delete "${BRANCH}" 97 | fi 98 | git checkout -b "${BRANCH}" 99 | if [ "${{ github.event.inputs.date }}" = "today" ]; then 100 | DATE=$(date +%Y-%m-%d) 101 | else 102 | DATE=${{ github.event.inputs.date }} 103 | fi 104 | vsce package ${{ github.event.inputs.version }} -m "${COMMIT}" 105 | VERSION=$(jq -r .version package.json) 106 | RELEASE_DATE="${VERSION} (${DATE})" 107 | sed -i "s/Unreleased/${RELEASE_DATE}/" CHANGELOG.md 108 | sed -i "s/^version:.*/version: ${VERSION}/" CITATION.cff 109 | sed -i "s/^date-released:.*/date-released: \"${DATE}\"/" CITATION.cff 110 | git add CHANGELOG.md || echo "No changes to add" 111 | git add CITATION.cff || echo "No changes to add" 112 | git commit -m "${COMMIT}" || echo "No changes to commit" 113 | git push --force origin ${BRANCH} || echo "No changes to push" 114 | 115 | - name: Create Pull Request 116 | if: ${{ steps.check_unreleased.outputs.UNRELEASED_FOUND == 'true' }} 117 | shell: bash 118 | run: | 119 | sleep 30 120 | gh pr create --fill-first --base "main" --head "${BRANCH}" --label "Type: CI/CD :robot:" 121 | 122 | - name: Merge Pull Request 123 | if: ${{ steps.check_unreleased.outputs.UNRELEASED_FOUND == 'true' }} 124 | shell: bash 125 | run: | 126 | sleep 30 127 | gh pr merge --auto --squash --delete-branch 128 | sleep 10 129 | 130 | release-publish: 131 | runs-on: ubuntu-latest 132 | 133 | needs: update-changelog 134 | 135 | permissions: 136 | contents: write 137 | id-token: write 138 | attestations: write 139 | 140 | steps: 141 | - name: Checkout repository 142 | uses: actions/checkout@v4 143 | 144 | - name: Update branch 145 | run: | 146 | git fetch --all 147 | git checkout main 148 | git pull origin main 149 | 150 | - name: Set up Node.js 151 | uses: actions/setup-node@v4 152 | 153 | - name: Install dependencies 154 | shell: bash 155 | run: npm install 156 | 157 | - name: Install Visual Studio Code Extension Manager 158 | shell: bash 159 | run: npm install -g @vscode/vsce 160 | 161 | - name: Set version 162 | shell: bash 163 | run: | 164 | current_version=$(jq -r .version package.json) 165 | version_line=$(grep -n '"version"' package.json | cut -d: -f1) 166 | echo "VERSION=${current_version}" >> $GITHUB_ENV 167 | echo "::notice file=package.json,line=${version_line}::${current_version}" 168 | 169 | - name: Set changelog 170 | env: 171 | VERSION: ${{ env.VERSION }} 172 | shell: bash 173 | run: | 174 | awk -v version="^## ${VERSION}.*" ' 175 | $0 ~ version {flag=1; next} 176 | /^## / && flag {flag=0} 177 | flag 178 | ' CHANGELOG.md >"CHANGELOG-${VERSION}.md" 179 | echo "CHANGELOG=CHANGELOG-${VERSION}.md" >> $GITHUB_ENV 180 | 181 | - name: Package extension 182 | shell: bash 183 | run: | 184 | if [ "${{ github.event.inputs.type }}" = "pre-release" ]; then 185 | vsce package --pre-release 186 | else 187 | vsce package 188 | fi 189 | 190 | - name: Generate artifact attestation 191 | uses: actions/attest-build-provenance@v2 192 | with: 193 | subject-path: "${{ github.workspace }}/quarto-wizard-${{ env.VERSION }}.vsix" 194 | 195 | - name: Release extension on GitHub 196 | env: 197 | GH_TOKEN: ${{ secrets.github_token }} 198 | VERSION: ${{ env.VERSION }} 199 | CHANGELOG: ${{ env.CHANGELOG }} 200 | shell: bash 201 | run: | 202 | if [ "${{ github.event.inputs.type }}" = "pre-release" ]; then 203 | gh release create ${VERSION} ./quarto-wizard-${VERSION}.vsix --prerelease --title ${VERSION} --notes-file ${CHANGELOG} --generate-notes 204 | else 205 | gh release create ${VERSION} ./quarto-wizard-${VERSION}.vsix --title ${VERSION} --notes-file ${CHANGELOG} --generate-notes 206 | fi 207 | 208 | - name: Publish extension to Visual Studio Marketplace 209 | env: 210 | VS_MARKETPLACE_TOKEN: ${{ secrets.VS_MARKETPLACE_TOKEN }} 211 | shell: bash 212 | run: | 213 | if [ "${{ github.event.inputs.type }}" = "pre-release" ]; then 214 | vsce publish --pre-release --pat ${VS_MARKETPLACE_TOKEN} 215 | else 216 | vsce publish --pat ${VS_MARKETPLACE_TOKEN} 217 | fi 218 | 219 | - name: Publish extension to Open VSX Registry 220 | env: 221 | OPEN_VSX_REGISTRY_TOKEN: ${{ secrets.OPEN_VSX_REGISTRY_TOKEN }} 222 | shell: bash 223 | run: | 224 | npm install --global ovsx 225 | if [ "${{ github.event.inputs.type }}" = "pre-release" ]; then 226 | npx ovsx publish --pre-release --pat ${OPEN_VSX_REGISTRY_TOKEN} 227 | else 228 | npx ovsx publish --pat ${OPEN_VSX_REGISTRY_TOKEN} 229 | fi 230 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist 3 | node_modules 4 | *.vsix 5 | /assets/videos 6 | -------------------------------------------------------------------------------- /.markdownlint.yml: -------------------------------------------------------------------------------- 1 | default: true 2 | 3 | MD013: false 4 | 5 | MD033: 6 | allowed_elements: ["img", "p", "video"] 7 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "printWidth": 120 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 10 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 11 | "preLaunchTask": "npm: webpack" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Package Quarto Wizard", 8 | "type": "shell", 9 | "command": "npx @vscode/vsce package --pre-release", 10 | "group": "build" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | .devcontainer 4 | .vscode 5 | .vscode-test 6 | .gitignore 7 | .DS_Store 8 | 9 | tsconfig.json 10 | eslint.config.mjs 11 | webpack.config.js 12 | .markdownlint.yml 13 | 14 | src 15 | dist/**/*.map 16 | **/*.vsix 17 | node_modules 18 | 19 | assets/logo/logo-social.png 20 | assets/videos/** 21 | CITATION.cff 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.18.6 (2025-05-02) 4 | 5 | - fix: ensure update check occurs after installing, updating, or removing an extension. 6 | 7 | ## 0.18.5 (2025-04-25) 8 | 9 | - feat: use error code from Quarto CLI >= 1.7.23 when installing an extension. 10 | 11 | ## 0.18.4 (2025-04-17) 12 | 13 | - fix: handle undefined source in repository field for extension tree 14 | 15 | ## 0.18.3 (2025-04-12) 16 | 17 | - chore: no usrer-facing changes. 18 | 19 | ## 0.18.2 (2025-04-12) 20 | 21 | - fix: proper handling of extension source with default tag 22 | 23 | ## 0.18.1 (2025-04-03) 24 | 25 | - fix: strip tag from source URL in `/use?repo=` handler. 26 | 27 | ## 0.18.0 (2025-04-03) 28 | 29 | - feat: add `/use?repo=` URI handler for installing Quarto extensions templates from the browser. 30 | - feat: enhance URI handling for Quarto extensions. 31 | - feat: enhance Quarto extension installation and template handling. 32 | - feat: documentation for Quarto commands. 33 | - feat: support extension tags in default Quarto installation. 34 | 35 | ## 0.17.0 (2025-03-31) 36 | 37 | - feat: support installing/using Quarto extensions templates. 38 | - fix: correct typo in short title of the `quartoWizard.installExtension` command. 39 | 40 | ## 0.16.2 (2025-03-22) 41 | 42 | - refactor: update to reflect changes in . 43 | 44 | ## 0.16.1 (2025-03-17) 45 | 46 | - fix: GitHub icon / "open source" visibility for tree elements without source. 47 | 48 | ## 0.16.0 (2025-03-15) 49 | 50 | ## 0.15.2 (2025-03-12) 51 | 52 | - feat: add a welcome view in the Quarto Wizard Explorer. 53 | - feat: handle empty workspace(s) in the explorer view. 54 | - feat: add workspace folder install context command. 55 | - fix: disable "blanks-around-fenced-divs" rule by default. 56 | - refactor(src/utils/workspace.ts): use `vscode.WorkspaceFolderPickOptions()` for workspace folder selection. 57 | - docs: add a note about the "blanks-around-fenced-divs" rule in the README. 58 | 59 | ## 0.15.1 (2025-03-10) 60 | 61 | - fix: tweak `markdownlint` extension activation and trigger to avoid linting issues. 62 | 63 | ## 0.15.0 (2025-03-09) 64 | 65 | - feat: add support for multi-root workspaces ([#102](https://github.com/mcanouil/quarto-wizard/issues/102)). 66 | - feat: implement workspace folder selection utility ([#100](https://github.com/mcanouil/quarto-wizard/issues/100), [#101](https://github.com/mcanouil/quarto-wizard/issues/101)). 67 | 68 | ## 0.14.2 (2025-03-06) 69 | 70 | - feat: add custom "markdownlint" rules: "blanks-around-fenced-divs". 71 | - fix: set "markdownlint" linting to "on type" by default. 72 | - chore: remove a `console.log()` statement. 73 | 74 | ## 0.14.1 (2025-02-23) 75 | 76 | - feat: improve the SVG icon. 77 | 78 | ## 0.14.0 (2025-02-22) 79 | 80 | - feat: add URI handler for installing Quarto extensions from the browser. 81 | `vscode://mcanouil.quarto-wizard/install?repo=mcanouil/quarto-iconify` 82 | 83 | ## 0.13.0 (2025-02-22) 84 | 85 | - feat: allow to disable the automatic markdown linting via `markdownlint` with a "never" option. 86 | - refactor(lintOnEvent): use `switch` instead of `if`. 87 | - refactor: Use truthy checks instead for better readability.. 88 | 89 | ## 0.12.0 (2025-02-21) 90 | 91 | - feat: automatic markdown linting via `markdownlint`. 92 | - feat: lazy extension dependencies. 93 | 94 | ## 0.11.0 (2025-02-20) 95 | 96 | - feat: first release of the Quarto Wizard extension, but not yet 1.0.0. 97 | 98 | ## 0.10.1 (2025-02-20) 99 | 100 | - chore: no user-facing changes. 101 | 102 | ## 0.10.0 (2025-02-20) 103 | 104 | - refactor: no longer use `quarto remove` to uninstall an extension. 105 | 106 | ## 0.9.0 (2025-02-20) 107 | 108 | - refactor: use a pre-fetched list of Quarto extensions for the QuickPick UI. 109 | - refactor: drop GitHub authentication requirement for the extension details. 110 | - refactor: change log level for cached extensions messages. 111 | - refactor: implement debounced logging for extension fetching via fetchExtensions() 112 | - feat: add activation log message for Quarto Wizard. 113 | 114 | ## 0.8.1 (2025-02-20) 115 | 116 | - fix: prevent error notification when extension details cannot be retrieved. 117 | 118 | ## 0.8.0 (2025-02-18) 119 | 120 | - feat: retrieve and display extensions details from GitHub API. 121 | - feat: add more details in QuickPick UI for extensions. 122 | - feat: set `log` to `true` for output channel, allowing colouring. 123 | - feat: add Quarto extensions update check. 124 | - fix: activate `quarto-wizard-explorer` view only in a workspace. 125 | - refactor: use constants variables for cache name and expiration time. 126 | - refactor: use `logMessage` function to log messages. 127 | - refactor: add a log level parameter to `logMessage` function. 128 | - docs: add GitHub account authentication as a requirement. 129 | - docs: update README.md with new features and usage instructions. 130 | - chore(CITATION.cff): add citation file. 131 | - chore: update TypeScript configuration settings. 132 | - chore: add basic Dev Container setup. 133 | - ci: bump version via GitHub Actions input. 134 | - docs: add JSDoc comments for utility functions and commands. 135 | 136 | ## 0.7.2 (2025-02-06) 137 | 138 | - fix(src/ui/extensionsQuickPick.ts): broken extensions install by updating description handling in QuickPick UI. 139 | - deps: update dependencies to latest. 140 | 141 | ## 0.7.1 (2025-02-02) 142 | 143 | - feat: add "activationEvents" to `package.json` to avoid unnecessary activation. 144 | - fix: GitHub button in QuickPick UI opens again the repository in the default browser. 145 | 146 | ## 0.7.0 (2025-01-30) 147 | 148 | - refactor(src/utils/network.ts): internal logging. 149 | - refactor(src/utils/extensions.ts): externalise Quick Pick UI tools. 150 | - refactor: use constants for log messages target. 151 | - fix(src/utils/extensions.ts): caching of the list of available Quarto extensions. 152 | - fix(src/utils/extensions.ts): update cache expiration time for extensions list and display in log as ISO string. 153 | - fix: harmonise log and notification messages. 154 | - chore: use webpack to bundle the extension. 155 | 156 | ## 0.6.0 (2025-01-24) 157 | 158 | - feat(package.json): add a "Quarto Wizard" menu in Explorer and Editor context menus. 159 | - fix(src/utils/reprex.ts): `Quarto Wizard: Quarto Reproducible Document` command no longer set filename. 160 | - fix(README.md): update commands and usage instructions. 161 | 162 | ## 0.5.5 (2025-01-21) 163 | 164 | - chore: no changes. 165 | 166 | ## 0.5.4 (2025-01-21) 167 | 168 | - chore: no changes. 169 | 170 | ## 0.5.3 (2025-01-21) 171 | 172 | - fix: duplication of recently installed extensions in search results. 173 | - fix: add information and error notifications when updating and removing an extension. 174 | - refactor: add `showLogsCommand()` function to display a link to the output log in the notification. 175 | 176 | ## 0.5.2 (2025-01-19) 177 | 178 | - fix: add source after updating an extension. 179 | - refactor: add `installQuartoExtensionSource` to contain the logic to install an extension and add the source. 180 | 181 | ## 0.5.1 (2025-01-19) 182 | 183 | - fix(.vscodeignore): remove wrong entry. 184 | 185 | ## 0.5.0 (2025-01-19) 186 | 187 | - feat: add view to display and to manage the Quarto extensions installed. 188 | - feat(checkQuartoPath): better check for the Quarto CLI path. 189 | - refactor(utils/extensions.ts): externalise user prompts to a separate module (`utils/ask.ts`). 190 | - refactor: update and correct trust authors and confirm installations prompts option value, _i.e._, `Yes, always trust`. 191 | - refactor(extension.ts): don't use temporary variables for commands. 192 | 193 | ## 0.4.2 (2025-01-05) 194 | 195 | - feat: add command `Quarto Wizard: New Reproducible Document` to create a new Quarto document in "new File" menu. 196 | - refactor: use "category" instead of hardcoding `Quarto Wizard:` in the command title. 197 | 198 | ## 0.4.1 (2025-01-04) 199 | 200 | - feat(assets/templates): add bibliography reference to the templates. 201 | 202 | ## 0.4.0 (2025-01-04) 203 | 204 | - feat(README.md): add usage instructions for `Quarto Wizard: New Reproducible Document`. 205 | - feat: add settings to specify the Quarto CLI path (`quartoWizard.quarto.path`). 206 | - feat: add settings to control user prompts for trusting authors and confirming installations (`quartoWizard.ask.trustAuthors`, `quartoWizard.ask.confirmInstall`). 207 | - feat: introduce prompts for users to trust authors and confirm installations, with a "Never ask again" option to update settings accordingly. 208 | - feat: enhance the extension installation process to update `_extension.yml` with the source repository (i.e., `source: `), ensuring future updates. 209 | - fix(README.md): remove duplicated command prefix. 210 | - style: format code with Prettier. 211 | - ci: allow anything after version number header in CHANGELOG.md. 212 | 213 | ## 0.3.0 (2024-11-19) 214 | 215 | - feat: add command `Quarto Wizard: New Reproducible Document` to create a new Quarto document. ([#6](https://github.com/mcanouil/quarto-wizard/pull/6)) 216 | - refactor: replace "see details" with "show logs" in notification messages. ([#5](https://github.com/mcanouil/quarto-wizard/pull/5)) 217 | 218 | ## 0.2.1 (2024-11-18) 219 | 220 | - docs: update README.md with updated usage instructions. 221 | 222 | ## 0.2.0 (2024-11-18) 223 | 224 | - feat: cache the list of available Quarto extensions (CSV) for twelve hours. 225 | - feat: add command `Quarto Wizard: Clear Recently Installed Extensions` to remove the list of recently installed extensions. 226 | - fix: `Quarto Wizard: Install Extension(s)` errors if no workspace/folder is open. ([#4](https://github.com/mcanouil/quarto-wizard/pull/4)) 227 | - refactor: split `extension.ts` into multiple files. 228 | - refactor: rename `quartoExtensions` to `quartoWizard`. 229 | - refactor: update commands prefix from `Quarto` to `Quarto Wizard`. 230 | - ci: publish to Open VSX Registry. ([#3](https://github.com/mcanouil/quarto-wizard/pull/3)) 231 | 232 | ## 0.1.0 (2024-11-16) 233 | 234 | - Initial release of Quarto Wizard extension. 235 | - feat: add command `Quarto: Install Extension(s)` to open the extension installer interface. 236 | - feat: add command `Quarto: Show Quarto Wizard Output` to display the output log for the extension. 237 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | title: "Quarto Wizard: A Visual Studio Code Extension to Manage Quarto Projects" 3 | message: "If you use this project, please cite it as below." 4 | type: software 5 | authors: 6 | - family-names: Canouil 7 | given-names: Mickaël 8 | orcid: "https://orcid.org/0000-0002-3396-4549" 9 | repository-code: "https://github.com/mcanouil/quarto-wizard" 10 | url: "https://github.com/mcanouil/quarto-wizard" 11 | abstract: >- 12 | Quarto Wizard is a Visual Studio Code extension that helps 13 | you manage your [Quarto](https://quarto.org) projects. 14 | It allows you to easily install Quarto extensions directly from the [Quarto Extensions](http://m.canouil.dev/quarto-extensions/) listing repository. 15 | This extension provides a user-friendly interface to browse, select, and install Quarto extensions, enhancing your Quarto development experience. 16 | keywords: 17 | - quarto 18 | - vscode 19 | - positron 20 | - codium 21 | - extension 22 | license: MIT 23 | commit: 455f0784f291c69a6c1331364f150d4fa1fb2289 24 | version: 0.18.6 25 | date-released: "2025-05-02" 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Mickaël Canouil 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quarto Wizard A cartoon-style illustration of a dog dressed as a wizard, holding a glowing wand. The dog is wearing a pointed hat and a robe with red accents, set against a background filled with magical symbols. 2 | 3 | ![GitHub Release](https://img.shields.io/github/v/release/mcanouil/quarto-wizard?style=flat-square&include_prereleases&label=Version) 4 | [![Visual Studio Marketplace Downloads](https://img.shields.io/visual-studio-marketplace/d/mcanouil.quarto-wizard?style=flat-square&color=333333&label=Visual%20Studio%20Marketplace)](https://marketplace.visualstudio.com/items?itemName=mcanouil.quarto-wizard) 5 | [![Open VSX Downloads](https://img.shields.io/open-vsx/dt/mcanouil/quarto-wizard?style=flat-square&color=333333&label=Open%20VSX)](https://open-vsx.org/extension/mcanouil/quarto-wizard) 6 | [![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/mcanouil/quarto-wizard/total?style=flat-square&color=333333&label=GitHub)](https://github.com/mcanouil/quarto-wizard/releases/latest) 7 | 8 | ## Overview 9 | 10 | **Quarto Wizard** is a Visual Studio Code extension that helps you manage your [Quarto](https://quarto.org) projects. 11 | It allows you to easily install Quarto extensions directly from the [Quarto Extensions](https://github.com/mcanouil/quarto-extensions) listing repository. 12 | This extension provides a user-friendly interface to browse, select, and install Quarto extensions, enhancing your Quarto development experience. 13 | Additionally, it offers a set of commands to create new Quarto documents that you can use for as a starting point for your bug reports, feature requests, or any other Quarto-related content. 14 | Finally, it provides an automatic Markdown linting feature to help you write better Markdown documents. 15 | 16 | ## Requirements 17 | 18 | - **Check Internet Connection**: Ensure you have an active internet connection before installing extensions. 19 | - **Check Quarto Installation**: Verify that Quarto is installed and available in your system's PATH. 20 | 21 | ## Commands 22 | 23 | - `Quarto Wizard: Install Extension(s)`: Opens the extension installer interface. 24 | - **Browse Extensions**: View a list of available Quarto extensions. 25 |

This image displays a search results interface for Quarto extensions authored by the user "mcanouil". It lists various extensions, including their names, version numbers, star ratings, and brief descriptions. The search highlights extensions such as Animate, Div Reuse, Elevator, Github, Highlight Text, Iconify, Invoice, and Letter, showcasing diverse functionalities ranging from animated content to document styling and templates for invoices and letters. The purpose of the image is to present a concise overview of available extensions along with their popularity and license information for Quarto users.

26 | - **Install Extensions**: Install selected Quarto extensions with a single click. 27 | - `Quarto Wizard: Use Template`: Opens the template installer interface. 28 | - **Browse Templates**: View a list of available Quarto templates from Quarto Extensions. 29 |

This image showcases a menu of Quarto extension templates available for selection. It lists templates like "LETTER," "ACADEMIC TYPST," "ACM," "ACS," and others, each with details such as version, number of stars, repository link, and license type. The "LETTER" template is highlighted, suggesting recent usage. This visual serves as a practical guide for users looking to choose and apply specific Quarto templates effectively.

30 | - **Install Templates**: Install selected Quarto template with a single click. 31 | - `Quarto Wizard: Clear Recently Installed Extensions`: Clears the list of recently installed extensions. 32 | - `Quarto Wizard: Show Quarto Wizard Log Output`: Displays the output log for the extension installer. 33 | - `Quarto Wizard: Quarto Reproducible Document`: Creates a new Quarto document. 34 | - [`R`](/assets/templates/r.qmd) 35 | - [`Python`](assets/templates/python.qmd) 36 | - [`Julia`](assets/templates/julia.qmd) 37 | - `Quarto Wizard: Focus on Extensions Installed View`: Opens the Quarto Wizard view to display and manage the Quarto extensions installed. 38 | 39 | ## Usage 40 | 41 | ### Quarto Wizard Explorer View 42 | 43 | 1. Open the Command Palette (`Ctrl+Shift+P` or `Cmd+Shift+P` on macOS). 44 | 2. Type `Quarto Wizard: Focus on Extensions Installed View` and select it. 45 | Or click on the Quarto Wizard icon in the Activity Bar. 46 |

This image showcases the Quarto Wizard extension interface within Visual Studio Code. It highlights features like the workspace view, extension management options for adding, removing, or updating extensions, as well as template usage and GitHub repository access. The interface also illustrates the Quarto Wizard Explorer section, with annotations using colored arrows and text to explain specific functionalities. Two workspaces are displayed: one with installed extensions labeled wizard-dev, and another without installed extensions labeled quarto-playground. This visual guide serves users looking to manage Quarto extensions in Visual Studio Code effectively.

47 | 48 | Or click on the Quarto Wizard icon in the Activity Bar. 49 | 50 | _Quarto Wizard Explorer View in action:_ 51 | 52 |

53 | 54 | > [!IMPORTANT] 55 | > Quarto extensions can only be updated if installed by Quarto Wizard (_i.e._, if `source: /` is present in `_extension.yml`). 56 | > You can manually add the source to the extension's `_extension.yml` file to enable updates. 57 | 58 | ### Explorer/Editor Context Menu 59 | 60 | - Right-click in the Explorer or Editor to access the following commands: 61 | - `Install Extension(s)`. 62 | - `Use Template`. 63 | - `Quarto Reproducible Document`. 64 | - `Show Quarto Wizard Log Output`. 65 | - `Clear Recently Installed Extensions`. 66 | 67 |

This image presents a context menu within Visual Studio Code. The menu displays options such as "Install Extension(s)," "Use Template", "Quarto Reproducible Document", and more. The "Quarto Wizard" option is highlighted. This visual aids users in navigating and utilising Quarto tools effectively within their workspace.

68 | 69 | ### Install Quarto Extensions 70 | 71 | 1. Open the Command Palette (`Ctrl+Shift+P` or `Cmd+Shift+P` on macOS). 72 | 2. Type `Quarto Wizard: Install Extension(s)` and select it. 73 | 3. Browse the list of available Quarto extensions. 74 | 4. Select the Quarto extension(s) you want to install. 75 | 5. Answer the prompts to confirm the installation. 76 | 77 | > [!NOTE] 78 | > Quarto Wizard can only display available informations, _i.e._, if the author of an extension has not provided a description, license, and/or used tags for release versions, these fields will be populated with `none`. 79 | 80 | ### Use Quarto Templates 81 | 82 | 1. Open the Command Palette (`Ctrl+Shift+P` or `Cmd+Shift+P` on macOS). 83 | 2. Type `Quarto Wizard: Use Template` and select it. 84 | 3. Browse the list of available Quarto templates. 85 | 4. Select the Quarto template you want to use. 86 | 5. Answer the prompts to confirm the installation. 87 | 88 | ### Create a New Reproducible Document 89 | 90 | 1. Open the Command Palette (`Ctrl+Shift+P` or `Cmd+Shift+P` on macOS). 91 | 2. Type `Quarto Wizard: Quarto Reproducible Document` and select it. 92 | 3. Choose the template for the new Quarto document. 93 | 94 | ### Show Quarto Wizard Output 95 | 96 | 1. Open the Command Palette (`Ctrl+Shift+P` or `Cmd+Shift+P` on macOS). 97 | 2. Type `Quarto Wizard: Show Quarto Wizard Log Output` and select it. 98 | 3. View the output log for the Quarto Wizard extension. 99 | 4. Use the output log to troubleshoot any issues. 100 | 101 | ### Markdown Linting 102 | 103 | The Quarto Wizard extension provides automatic Markdown linting to help you write better Markdown documents. 104 | This is achieved by using the [`markdownlint`](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint) extension. 105 | 106 | In the context of Quarto, it's recommended to disable the following rule in your `settings.json` file: 107 | 108 | ```json 109 | { 110 | "markdownlint.config": { 111 | "first-line-h1": false, //MD041 112 | "single-h1": false // MD025 113 | } 114 | } 115 | ``` 116 | 117 | See the [`markdownlint` README](https://github.com/DavidAnson/vscode-markdownlint?tab=readme-ov-file) for more information. 118 | 119 | #### Custom Markdown Linting Rules 120 | 121 | - `QMD001` / `blanks-around-fenced-divs`: Ensure there are blank lines around [fenced divs](https://pandoc.org/MANUAL.html#extension-fenced_divs) delimiters. 122 | 123 | ```json 124 | { 125 | "markdownlint.config": { 126 | "blanks-around-fenced-divs": true 127 | } 128 | } 129 | ``` 130 | 131 | ## Verifying Release Asset Build Provenance 132 | 133 | To ensure the authenticity and integrity of the release asset, use GitHub CLI to verify its build provenance using GitHub CLI. 134 | 135 | ```bash 136 | gh attestation verify quarto-wizard-.vsix --repo mcanouil/quarto-wizard 137 | ``` 138 | 139 | ## Development 140 | 141 | 1. Clone the repository: 142 | 143 | ```sh 144 | git clone https://github.com/mcanouil/quarto-wizard 145 | ``` 146 | 147 | 2. Open the project in Visual Studio Code. 148 | 149 | 3. Install the dependencies: 150 | 151 | ```sh 152 | npm install 153 | ``` 154 | 155 | 4. Launch the extension: 156 | 157 | - Press `F5` to open a new Visual Studio Code window with the extension loaded. 158 | 159 | ## Contributing 160 | 161 | Contributions are welcome! Please open an issue or submit a pull request on the [GitHub repository](https://github.com/mcanouil/quarto-wizard). 162 | 163 | ## License 164 | 165 | This project is licensed under the MIT License. 166 | See the [LICENSE](LICENSE) file for details. 167 | 168 | ## Disclaimer 169 | 170 | This extension is not affiliated with or endorsed by [Quarto](https://quarto.org) or its maintainers. 171 | -------------------------------------------------------------------------------- /assets/images/editor-context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcanouil/quarto-wizard/66003a9d1f761535ec6bd208fa429d8c3fda1e2c/assets/images/editor-context.png -------------------------------------------------------------------------------- /assets/images/explorer-context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcanouil/quarto-wizard/66003a9d1f761535ec6bd208fa429d8c3fda1e2c/assets/images/explorer-context.png -------------------------------------------------------------------------------- /assets/images/explorer-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcanouil/quarto-wizard/66003a9d1f761535ec6bd208fa429d8c3fda1e2c/assets/images/explorer-view.png -------------------------------------------------------------------------------- /assets/images/install-extensions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcanouil/quarto-wizard/66003a9d1f761535ec6bd208fa429d8c3fda1e2c/assets/images/install-extensions.png -------------------------------------------------------------------------------- /assets/images/use-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcanouil/quarto-wizard/66003a9d1f761535ec6bd208fa429d8c3fda1e2c/assets/images/use-template.png -------------------------------------------------------------------------------- /assets/logo/logo-social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcanouil/quarto-wizard/66003a9d1f761535ec6bd208fa429d8c3fda1e2c/assets/logo/logo-social.png -------------------------------------------------------------------------------- /assets/logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcanouil/quarto-wizard/66003a9d1f761535ec6bd208fa429d8c3fda1e2c/assets/logo/logo.png -------------------------------------------------------------------------------- /assets/logo/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 37 | 39 | 51 | 63 | 75 | 87 | 93 | 100 | 104 | 108 | 115 | 122 | 123 | 124 | 131 | 134 | 141 | 148 | 149 | 150 | 153 | 157 | 158 | 165 | 168 | 173 | 175 | 180 | 187 | 195 | 203 | 210 | 211 | 219 | 220 | 221 | 222 | 223 | 227 | 231 | 233 | 237 | 242 | 246 | 250 | 254 | 258 | 263 | 268 | 273 | 278 | 279 | 280 | 281 | 282 | -------------------------------------------------------------------------------- /assets/templates/julia.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Reproducible Quarto Document" 3 | format: html 4 | engine: julia 5 | references: 6 | - id: canouil_jointly_2018 7 | author: 8 | - family: Canouil 9 | given: Mickaël 10 | - family: Balkau 11 | given: Beverley 12 | - family: Roussel 13 | given: Ronan 14 | - family: Froguel 15 | given: Philippe 16 | - family: Rocheleau 17 | given: Ghislain 18 | container-title: Frontiers in Genetics 19 | doi: 10.3389/fgene.2018.00210 20 | issued: 2018 21 | title: "Jointly Modelling Single Nucleotide Polymorphisms With 22 | Longitudinal and Time-to-Event Trait: An Application to Type 2 23 | Diabetes and Fasting Plasma Glucose" 24 | type: article-journal 25 | journal-title: "Frontiers in Genetics*" 26 | --- 27 | 28 | ```{julia} 29 | #| include: false 30 | using Pkg 31 | Pkg.add("Plots") 32 | ``` 33 | 34 | This is a reproducible Quarto document with references [@canouil_jointly_2018]. 35 | 36 | ```{julia} 37 | using Plots 38 | plot(sin, x -> sin(2x), 0, 2) 39 | ``` 40 | 41 | ![An image]({{< placeholder 600 400 >}}){#fig-placeholder} 42 | 43 | {{< lipsum 1 >}} 44 | 45 | The end after @fig-placeholder. 46 | -------------------------------------------------------------------------------- /assets/templates/python.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Reproducible Quarto Document" 3 | format: html 4 | engine: jupyter 5 | references: 6 | - id: canouil_jointly_2018 7 | author: 8 | - family: Canouil 9 | given: Mickaël 10 | - family: Balkau 11 | given: Beverley 12 | - family: Roussel 13 | given: Ronan 14 | - family: Froguel 15 | given: Philippe 16 | - family: Rocheleau 17 | given: Ghislain 18 | container-title: Frontiers in Genetics 19 | doi: 10.3389/fgene.2018.00210 20 | issued: 2018 21 | title: "Jointly Modelling Single Nucleotide Polymorphisms With 22 | Longitudinal and Time-to-Event Trait: An Application to Type 2 23 | Diabetes and Fasting Plasma Glucose" 24 | type: article-journal 25 | journal-title: "Frontiers in Genetics*" 26 | --- 27 | 28 | This is a reproducible Quarto document with references [@canouil_jointly_2018]. 29 | 30 | ```{python} 31 | import matplotlib.pyplot as plt 32 | 33 | x = [1, 2, 3, 4, 5] 34 | y = [1, 4, 9, 16, 25] 35 | 36 | plt.plot(x, y) 37 | plt.show() 38 | ``` 39 | 40 | ![An image]({{< placeholder 600 400 >}}){#fig-placeholder} 41 | 42 | {{< lipsum 1 >}} 43 | 44 | The end after @fig-placeholder. 45 | -------------------------------------------------------------------------------- /assets/templates/r.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Reproducible Quarto Document" 3 | format: html 4 | engine: knitr 5 | references: 6 | - id: canouil_jointly_2018 7 | author: 8 | - family: Canouil 9 | given: Mickaël 10 | - family: Balkau 11 | given: Beverley 12 | - family: Roussel 13 | given: Ronan 14 | - family: Froguel 15 | given: Philippe 16 | - family: Rocheleau 17 | given: Ghislain 18 | container-title: Frontiers in Genetics 19 | doi: 10.3389/fgene.2018.00210 20 | issued: 2018 21 | title: "Jointly Modelling Single Nucleotide Polymorphisms With 22 | Longitudinal and Time-to-Event Trait: An Application to Type 2 23 | Diabetes and Fasting Plasma Glucose" 24 | type: article-journal 25 | journal-title: "Frontiers in Genetics*" 26 | --- 27 | 28 | This is a reproducible Quarto document with references [@canouil_jointly_2018]. 29 | 30 | ```{r} 31 | x <- c(1, 2, 3, 4, 5) 32 | y <- c(1, 4, 9, 16, 25) 33 | 34 | plot(x, y) 35 | ``` 36 | 37 | ![An image]({{< placeholder 600 400 >}}){#fig-placeholder} 38 | 39 | {{< lipsum 1 >}} 40 | 41 | The end after @fig-placeholder. 42 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js'; 4 | import tseslint from 'typescript-eslint'; 5 | 6 | export default tseslint.config( 7 | eslint.configs.recommended, 8 | tseslint.configs.recommended, 9 | tseslint.configs.stylistic, 10 | ); 11 | -------------------------------------------------------------------------------- /markdownlint-rules/001-blanks-around-fenced-divs.js: -------------------------------------------------------------------------------- 1 | /** @type {import("markdownlint").Rule} */ 2 | module.exports = { 3 | "names": ["QMD001", "blanks-around-fenced-divs"], 4 | "description": "Fenced Divs markers should be surrounded by blank lines", 5 | "tags": ["fenced_divs", "blank_lines"], 6 | "parser": "none", 7 | "function": function QMD001(params, onError) { 8 | const lines = params.lines; 9 | 10 | for (let i = 0; i < lines.length; i++) { 11 | const line = lines[i].trim(); 12 | 13 | if (line.match(/^:{3,}/)) { 14 | const isMissingTopBlank = i === 0 || lines[i - 1].trim().length > 0; 15 | const isMissingBottomBlank = i === lines.length - 1 || lines[i + 1].trim().length > 0; 16 | if (isMissingTopBlank || isMissingBottomBlank) { 17 | onError({ 18 | lineNumber: i + 1, 19 | detail: `Before: ${isMissingTopBlank ? "missing" : "present"}, After: ${isMissingBottomBlank ? "missing" : "present"}`, 20 | }); 21 | } 22 | } 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quarto-wizard", 3 | "displayName": "Quarto Wizard", 4 | "description": "A Visual Studio Code extension that helps you manage Quarto projects.", 5 | "version": "0.18.6", 6 | "publisher": "mcanouil", 7 | "author": { 8 | "name": "Mickaël CANOUIL", 9 | "url": "https://mickael.canouil.fr" 10 | }, 11 | "license": "MIT", 12 | "icon": "assets/logo/logo.png", 13 | "galleryBanner": { 14 | "color": "#333333", 15 | "theme": "dark" 16 | }, 17 | "keywords": [ 18 | "quarto", 19 | "markdown", 20 | "pandoc", 21 | "extension", 22 | "lua", 23 | "latex", 24 | "reveal.js", 25 | "html", 26 | "typst" 27 | ], 28 | "categories": [ 29 | "Programming Languages", 30 | "Data Science", 31 | "Machine Learning", 32 | "Notebooks" 33 | ], 34 | "pricing": "Free", 35 | "homepage": "https://github.com/mcanouil/quarto-wizard", 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/mcanouil/quarto-wizard" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/mcanouil/quarto-wizard/issues" 42 | }, 43 | "sponsor": { 44 | "url": "https://github.com/sponsors/mcanouil" 45 | }, 46 | "engines": { 47 | "vscode": "^1.96.0" 48 | }, 49 | "activationEvents": [ 50 | "onLanguage:quarto", 51 | "workspaceContains:**/*.{qmd,rmd}", 52 | "workspaceContains:**/_quarto.{yml,yaml}", 53 | "workspaceContains:**/_brand.{yml,yaml}", 54 | "workspaceContains:**/_extension.{yml,yaml}", 55 | "onUri" 56 | ], 57 | "main": "./dist/extension", 58 | "scripts": { 59 | "vscode:prepublish": "webpack --mode production", 60 | "webpack": "webpack --mode development --stats-error-details", 61 | "watch": "webpack --watch --mode development --stats-error-details", 62 | "test-compile": "tsc -p ./", 63 | "lint": "eslint --fix --cache --format unix \"src/**/*.{ts,tsx}\"" 64 | }, 65 | "devDependencies": { 66 | "@eslint/js": "^9.23.0", 67 | "@types/js-yaml": "^4.0.9", 68 | "@types/node": "^22.13.17", 69 | "@types/semver": "^7.7.0", 70 | "@types/vscode": "^1.96.0", 71 | "@vscode/vsce": "^3.3.2", 72 | "eslint": "^9.25.1", 73 | "eslint-plugin-tsdoc": "^0.4.0", 74 | "ts-loader": "^9.5.2", 75 | "typescript": "^5.8.3", 76 | "typescript-eslint": "^8.31.1", 77 | "webpack": "^5.98.0", 78 | "webpack-cli": "^6.0.1" 79 | }, 80 | "dependencies": { 81 | "@types/lodash": "^4.17.16", 82 | "js-yaml": "^4.1.0", 83 | "lodash": "^4.17.21", 84 | "semver": "^7.7.1" 85 | }, 86 | "contributes": { 87 | "commands": [ 88 | { 89 | "command": "quartoWizard.installExtension", 90 | "title": "Install Extension(s)", 91 | "shortTitle": "Install", 92 | "icon": "$(add)", 93 | "category": "Quarto Wizard" 94 | }, 95 | { 96 | "command": "quartoWizard.extensionsInstalled.install", 97 | "title": "Install Extension(s)", 98 | "shortTitle": "Install", 99 | "icon": "$(add)", 100 | "category": "Quarto Wizard" 101 | }, 102 | { 103 | "command": "quartoWizard.useTemplate", 104 | "title": "Use Template", 105 | "shortTitle": "Quarto Use Template", 106 | "icon": "$(new-file)", 107 | "category": "Quarto Wizard" 108 | }, 109 | { 110 | "command": "quartoWizard.extensionsInstalled.useTemplate", 111 | "title": "Use Template", 112 | "shortTitle": "Use Template", 113 | "icon": "$(new-file)", 114 | "category": "Quarto Wizard" 115 | }, 116 | { 117 | "command": "quartoWizard.showOutput", 118 | "title": "Show Quarto Wizard Log Output", 119 | "shortTitle": "Show Log", 120 | "icon": "$(output)", 121 | "category": "Quarto Wizard" 122 | }, 123 | { 124 | "command": "quartoWizard.clearRecent", 125 | "title": "Clear Recently Installed Extensions", 126 | "shortTitle": "Clear Recent", 127 | "icon": "$(clear-all)", 128 | "category": "Quarto Wizard" 129 | }, 130 | { 131 | "command": "quartoWizard.newQuartoReprex", 132 | "title": "Quarto Reproducible Document", 133 | "icon": "$(new-file)", 134 | "category": "Quarto Wizard" 135 | }, 136 | { 137 | "command": "quartoWizard.getExtensionsDetails", 138 | "title": "Get Extensions Details", 139 | "category": "Quarto Wizard" 140 | }, 141 | { 142 | "command": "quartoWizard.extensionsInstalled.refresh", 143 | "title": "Refresh Installed Extensions", 144 | "shortTitle": "Refresh", 145 | "icon": "$(refresh)", 146 | "category": "Quarto Wizard" 147 | }, 148 | { 149 | "command": "quartoWizard.extensionsInstalled.openSource", 150 | "title": "Open Extension Source", 151 | "shortTitle": "Open Source", 152 | "icon": "$(github)", 153 | "category": "Quarto Wizard" 154 | }, 155 | { 156 | "command": "quartoWizard.extensionsInstalled.update", 157 | "title": "Update Extension", 158 | "shortTitle": "Update", 159 | "icon": "$(cloud-download)", 160 | "category": "Quarto Wizard" 161 | }, 162 | { 163 | "command": "quartoWizard.extensionsInstalled.remove", 164 | "title": "Remove Extension", 165 | "shortTitle": "Remove", 166 | "icon": "$(remove)", 167 | "category": "Quarto Wizard" 168 | } 169 | ], 170 | "submenus": [ 171 | { 172 | "id": "quartoWizard.menu", 173 | "label": "Quarto Wizard" 174 | } 175 | ], 176 | "menus": { 177 | "file/newFile": [ 178 | { 179 | "command": "quartoWizard.newQuartoReprex", 180 | "group": "Quarto" 181 | }, 182 | { 183 | "command": "quartoWizard.useTemplate", 184 | "group": "Quarto" 185 | } 186 | ], 187 | "explorer/context": [ 188 | { 189 | "submenu": "quartoWizard.menu", 190 | "group": "2_execution" 191 | } 192 | ], 193 | "editor/context": [ 194 | { 195 | "submenu": "quartoWizard.menu", 196 | "group": "quartoWizard" 197 | } 198 | ], 199 | "quartoWizard.menu": [ 200 | { 201 | "command": "quartoWizard.installExtension", 202 | "group": "quartoWizard@1" 203 | }, 204 | { 205 | "command": "quartoWizard.useTemplate", 206 | "group": "quartoWizard@2" 207 | }, 208 | { 209 | "command": "quartoWizard.newQuartoReprex", 210 | "group": "quartoWizard@3" 211 | }, 212 | { 213 | "command": "quartoWizard.showOutput", 214 | "group": "quartoWizard@4" 215 | }, 216 | { 217 | "command": "quartoWizard.clearRecent", 218 | "group": "quartoWizard@5" 219 | } 220 | ], 221 | "view/title": [ 222 | { 223 | "command": "quartoWizard.installExtension", 224 | "when": "view == quartoWizard.extensionsInstalled", 225 | "group": "navigation@1" 226 | }, 227 | { 228 | "command": "quartoWizard.useTemplate", 229 | "when": "view == quartoWizard.extensionsInstalled", 230 | "group": "navigation@2" 231 | }, 232 | { 233 | "command": "quartoWizard.extensionsInstalled.refresh", 234 | "when": "view == quartoWizard.extensionsInstalled", 235 | "group": "navigation@3" 236 | } 237 | ], 238 | "view/item/context": [ 239 | { 240 | "command": "quartoWizard.extensionsInstalled.install", 241 | "when": "view == quartoWizard.extensionsInstalled && viewItem == quartoExtensionWorkspaceFolder", 242 | "group": "inline@1" 243 | }, 244 | { 245 | "command": "quartoWizard.extensionsInstalled.useTemplate", 246 | "when": "view == quartoWizard.extensionsInstalled && viewItem == quartoExtensionWorkspaceFolder", 247 | "group": "inline@2" 248 | }, 249 | { 250 | "command": "quartoWizard.extensionsInstalled.update", 251 | "when": "view == quartoWizard.extensionsInstalled && viewItem == quartoExtensionItemOutdated", 252 | "group": "inline@3" 253 | }, 254 | { 255 | "command": "quartoWizard.extensionsInstalled.remove", 256 | "when": "view == quartoWizard.extensionsInstalled && (viewItem == quartoExtensionItem || viewItem == quartoExtensionItemOutdated || viewItem == quartoExtensionItemNoSource)", 257 | "group": "inline@4" 258 | }, 259 | { 260 | "command": "quartoWizard.extensionsInstalled.openSource", 261 | "when": "view == quartoWizard.extensionsInstalled && (viewItem == quartoExtensionItem || viewItem == quartoExtensionItemOutdated) && viewItem != quartoExtensionItemNoSource", 262 | "group": "inline@5" 263 | }, 264 | { 265 | "command": "quartoWizard.extensionsInstalled.update", 266 | "when": "view == quartoWizard.extensionsInstalled && viewItem == quartoExtensionItemOutdated", 267 | "group": "navigation@1" 268 | }, 269 | { 270 | "command": "quartoWizard.extensionsInstalled.remove", 271 | "when": "view == quartoWizard.extensionsInstalled && (viewItem == quartoExtensionItem || viewItem == quartoExtensionItemOutdated || viewItem == quartoExtensionItemNoSource)", 272 | "group": "navigation@2" 273 | }, 274 | { 275 | "command": "quartoWizard.extensionsInstalled.openSource", 276 | "when": "view == quartoWizard.extensionsInstalled && (viewItem == quartoExtensionItem || viewItem == quartoExtensionItemOutdated) && viewItem != quartoExtensionItemNoSource", 277 | "group": "navigation@3" 278 | } 279 | ], 280 | "commandPalette": [ 281 | { 282 | "command": "quartoWizard.extensionsInstalled.openSource", 283 | "when": "false" 284 | }, 285 | { 286 | "command": "quartoWizard.extensionsInstalled.install", 287 | "when": "false" 288 | }, 289 | { 290 | "command": "quartoWizard.extensionsInstalled.useTemplate", 291 | "when": "false" 292 | }, 293 | { 294 | "command": "quartoWizard.extensionsInstalled.update", 295 | "when": "false" 296 | }, 297 | { 298 | "command": "quartoWizard.extensionsInstalled.remove", 299 | "when": "false" 300 | }, 301 | { 302 | "command": "quartoWizard.extensionsInstalled.refresh", 303 | "when": "false" 304 | }, 305 | { 306 | "command": "quartoWizard.getExtensionsDetails", 307 | "when": "false" 308 | } 309 | ] 310 | }, 311 | "viewsContainers": { 312 | "activitybar": [ 313 | { 314 | "id": "quarto-wizard-explorer", 315 | "title": "Quarto Wizard", 316 | "icon": "assets/logo/logo.svg", 317 | "contextualTitle": "Quarto Wizard", 318 | "when": "workspaceFolderCount >= 1" 319 | } 320 | ] 321 | }, 322 | "viewsWelcome": [ 323 | { 324 | "view": "quartoWizard.extensionsInstalled", 325 | "contents": "No Quarto extensions installed ([browse extensions](https://m.canouil.dev/quarto-extensions/)).\n[Install Extension(s)](command:quartoWizard.installExtension)\nTo learn more about Quarto and Quarto extensions [read the documentation](https://quarto.org/)." 326 | } 327 | ], 328 | "views": { 329 | "quarto-wizard-explorer": [ 330 | { 331 | "id": "quartoWizard.extensionsInstalled", 332 | "type": "tree", 333 | "name": "Extensions Installed", 334 | "icon": "assets/logo/logo.svg", 335 | "contextualTitle": "Quarto Wizard Extensions Installed", 336 | "visibility": "visible", 337 | "when": "workspaceFolderCount >= 1" 338 | } 339 | ] 340 | }, 341 | "configuration": { 342 | "title": "Quarto Wizard", 343 | "properties": { 344 | "quartoWizard.quarto.path": { 345 | "order": 1, 346 | "scope": "window", 347 | "type": "string", 348 | "default": null, 349 | "markdownDescription": "A path to the Quarto CLI executable. By default, the extension looks for Quarto CLI in the `PATH` and in `#quarto.path#`, but if set, will use the path specified instead." 350 | }, 351 | "quartoWizard.ask.trustAuthors": { 352 | "order": 2, 353 | "scope": "resource", 354 | "type": "string", 355 | "enum": [ 356 | "never", 357 | "ask" 358 | ], 359 | "default": "ask", 360 | "markdownDescription": "Ask for confirmation before trusting an extension author. `ask` to ask for confirmation, `never` to always confirm and never ask again." 361 | }, 362 | "quartoWizard.ask.confirmInstall": { 363 | "order": 3, 364 | "scope": "resource", 365 | "type": "string", 366 | "enum": [ 367 | "never", 368 | "ask" 369 | ], 370 | "default": "ask", 371 | "markdownDescription": "Ask for confirmation before installing an extension. `ask` to ask for confirmation, `never` to always confirm and never ask again." 372 | }, 373 | "quartoWizard.lint.trigger": { 374 | "order": 4, 375 | "scope": "resource", 376 | "type": "string", 377 | "enum": [ 378 | "save", 379 | "type", 380 | "never" 381 | ], 382 | "default": "type", 383 | "markdownDescription": "Run the markdown linter on save (`save`), on type (`type`), or never (`never`)." 384 | }, 385 | "quartoWizard.log.level": { 386 | "order": 20, 387 | "scope": "resource", 388 | "type": "string", 389 | "enum": [ 390 | "error", 391 | "warn", 392 | "info", 393 | "debug" 394 | ], 395 | "default": "info", 396 | "markdownDescription": "The level of logging to use. `error` to only log errors, `warn` to log warnings and errors, `info` to log info, warnings, and errors, `debug` to log everything." 397 | } 398 | } 399 | }, 400 | "configurationDefaults": { 401 | "markdownlint.config": { 402 | "blanks-around-fenced-divs": false 403 | }, 404 | "markdownlint.customRules": [ 405 | "{mcanouil.quarto-wizard}/markdownlint-rules/001-blanks-around-fenced-divs.js" 406 | ] 407 | } 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /src/commands/installQuartoExtension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { QW_RECENTLY_INSTALLED, QW_RECENTLY_USED } from "../constants"; 3 | import { showLogsCommand, logMessage } from "../utils/log"; 4 | import { checkInternetConnection } from "../utils/network"; 5 | import { getQuartoPath, checkQuartoPath, installQuartoExtensionSource } from "../utils/quarto"; 6 | import { askTrustAuthors, askConfirmInstall } from "../utils/ask"; 7 | import { getExtensionsDetails } from "../utils/extensionDetails"; 8 | import { ExtensionQuickPickItem, showExtensionQuickPick } from "../ui/extensionsQuickPick"; 9 | import { selectWorkspaceFolder } from "../utils/workspace"; 10 | 11 | /** 12 | * Installs the selected Quarto extensions. 13 | * 14 | * @param selectedExtensions - The extensions selected by the user for installation. 15 | * @param workspaceFolder - The workspace folder where the extensions will be installed. 16 | */ 17 | async function installQuartoExtensions(selectedExtensions: readonly ExtensionQuickPickItem[], workspaceFolder: string) { 18 | const mutableSelectedExtensions: ExtensionQuickPickItem[] = [...selectedExtensions]; 19 | 20 | if ((await askTrustAuthors()) !== 0) return; 21 | if ((await askConfirmInstall()) !== 0) return; 22 | 23 | await vscode.window.withProgress( 24 | { 25 | location: vscode.ProgressLocation.Notification, 26 | title: `Installing selected extension(s) (${showLogsCommand()})`, 27 | cancellable: true, 28 | }, 29 | async (progress, token) => { 30 | token.onCancellationRequested(() => { 31 | const message = "Operation cancelled by the user."; 32 | logMessage(message, "info"); 33 | vscode.window.showInformationMessage(`${message} ${showLogsCommand()}.`); 34 | }); 35 | 36 | const installedExtensions: string[] = []; 37 | const failedExtensions: string[] = []; 38 | const totalExtensions = mutableSelectedExtensions.length; 39 | let installedCount = 0; 40 | 41 | for (const selectedExtension of mutableSelectedExtensions) { 42 | if (!selectedExtension.id) { 43 | continue; 44 | } 45 | progress.report({ 46 | message: `(${installedCount} / ${totalExtensions}) ${selectedExtension.label} ...`, 47 | increment: (1 / (totalExtensions + 1)) * 100, 48 | }); 49 | 50 | let extensionSource = selectedExtension.id; 51 | if (selectedExtension.tag && selectedExtension.tag !== "none") { 52 | extensionSource = `${selectedExtension.id}@${selectedExtension.tag}`; 53 | } 54 | 55 | const success = await installQuartoExtensionSource(extensionSource, workspaceFolder); 56 | // Once source is supported in _extension.yml, the above line can be replaced with the following line 57 | // const success = await installQuartoExtension(extension); 58 | if (success) { 59 | installedExtensions.push(selectedExtension.id); 60 | } else { 61 | failedExtensions.push(selectedExtension.id); 62 | } 63 | 64 | installedCount++; 65 | } 66 | progress.report({ 67 | message: `(${totalExtensions} / ${totalExtensions}) extensions processed.`, 68 | increment: (1 / (totalExtensions + 1)) * 100, 69 | }); 70 | 71 | if (installedExtensions.length > 0) { 72 | logMessage(`Successfully installed extension${installedExtensions.length > 1 ? "s" : ""}:`, "info"); 73 | installedExtensions.forEach((ext) => { 74 | logMessage(` - ${ext}`, "info"); 75 | }); 76 | } 77 | 78 | if (failedExtensions.length > 0) { 79 | logMessage(`Failed to install extension${failedExtensions.length > 1 ? "s" : ""}:`, "error"); 80 | failedExtensions.forEach((ext) => { 81 | logMessage(` - ${ext}`, "error"); 82 | }); 83 | const message = [ 84 | "The following extension", 85 | failedExtensions.length > 1 ? "s were" : " was", 86 | " not installed, try installing ", 87 | failedExtensions.length > 1 ? "them" : "it", 88 | " manually with `quarto add `:", 89 | ].join(""); 90 | vscode.window.showErrorMessage(`${message} ${failedExtensions.join(", ")}. ${showLogsCommand()}.`); 91 | } else { 92 | const message = [installedCount, " extension", installedCount > 1 ? "s" : "", " installed successfully."].join( 93 | "" 94 | ); 95 | logMessage(message, "info"); 96 | vscode.window.showInformationMessage(`${message} ${showLogsCommand()}.`); 97 | } 98 | } 99 | ); 100 | } 101 | 102 | /** 103 | * Command to install Quarto extensions in a specified workspace folder. 104 | * Prompts the user to select extensions, installs them, and optionally handles templates. 105 | * 106 | * @param context - The extension context. 107 | * @param workspaceFolder - The target workspace folder for extension installation. 108 | * @param template - Whether to filter for and handle template extensions. 109 | */ 110 | export async function installQuartoExtensionFolderCommand( 111 | context: vscode.ExtensionContext, 112 | workspaceFolder: string, 113 | template = false 114 | ) { 115 | const isConnected = await checkInternetConnection("https://github.com/"); 116 | if (!isConnected) { 117 | return; 118 | } 119 | await checkQuartoPath(getQuartoPath()); 120 | 121 | let extensionsList = await getExtensionsDetails(context); 122 | if (template) { 123 | extensionsList = extensionsList.filter((ext) => ext.template); 124 | } 125 | const recentKey = template ? QW_RECENTLY_USED : QW_RECENTLY_INSTALLED; 126 | const recentExtensions: string[] = context.globalState.get(recentKey, []); 127 | const selectedExtensions = await showExtensionQuickPick(extensionsList, recentExtensions, template); 128 | 129 | if (selectedExtensions.length > 0) { 130 | await installQuartoExtensions(selectedExtensions, workspaceFolder); 131 | if (template) { 132 | await useQuartoTemplate(selectedExtensions); 133 | } 134 | const selectedIDs = selectedExtensions.map((ext) => ext.id); 135 | const updatedRecentExtensions = [...selectedIDs, ...recentExtensions.filter((ext) => !selectedIDs.includes(ext))]; 136 | await context.globalState.update(recentKey, updatedRecentExtensions.slice(0, 5)); 137 | } 138 | } 139 | 140 | /** 141 | * Command handler for installing Quarto extensions. 142 | * Prompts the user to select a workspace folder and then calls installQuartoExtensionFolderCommand. 143 | * 144 | * @param context - The extension context. 145 | */ 146 | export async function installQuartoExtensionCommand(context: vscode.ExtensionContext) { 147 | const workspaceFolder = await selectWorkspaceFolder(); 148 | if (!workspaceFolder) { 149 | return; 150 | } 151 | installQuartoExtensionFolderCommand(context, workspaceFolder, false); 152 | } 153 | 154 | /** 155 | * Opens a Quarto template in the editor. 156 | * 157 | * @param id - The ID of the extension containing the template 158 | * @param templateContent - The base64-encoded template content 159 | * @returns A Promise that resolves to a boolean indicating success or failure 160 | */ 161 | export async function openTemplate(id: string, templateContent: string): Promise { 162 | try { 163 | const decodedContent = Buffer.from(templateContent, "base64").toString("utf-8"); 164 | await vscode.workspace.openTextDocument({ content: decodedContent, language: "quarto" }).then((document) => { 165 | vscode.window.showTextDocument(document); 166 | }); 167 | const message = `Template from "${id}" opened successfully.`; 168 | logMessage(message, "info"); 169 | return true; 170 | } catch (error) { 171 | const message = 172 | error instanceof Error 173 | ? `Failed to open the template from "${id}": ${error.message}.` 174 | : `An unknown error occurred retrieving the template content from "${id}".`; 175 | logMessage(message, "error"); 176 | vscode.window.showErrorMessage(`${message} ${showLogsCommand()}.`); 177 | return false; 178 | } 179 | } 180 | 181 | /** 182 | * Use the selected Quarto extension template. 183 | * 184 | * @param selectedExtension - The extension template selected by the user for use. 185 | */ 186 | async function useQuartoTemplate(selectedExtension: readonly ExtensionQuickPickItem[]) { 187 | if (selectedExtension.length === 0 || !selectedExtension[0].templateContent) { 188 | const message = "No template content found for the selected extension."; 189 | logMessage(message, "error"); 190 | vscode.window.showErrorMessage(`${message} ${showLogsCommand()}.`); 191 | return; 192 | } 193 | 194 | const extensionId = selectedExtension[0].id; 195 | const extensionTemplate = selectedExtension[0].templateContent; 196 | if (!extensionId || !extensionTemplate) { 197 | const message = "Invalid extension ID or template content."; 198 | logMessage(message, "error"); 199 | vscode.window.showErrorMessage(`${message} ${showLogsCommand()}.`); 200 | return; 201 | } 202 | await openTemplate(extensionId, extensionTemplate); 203 | } 204 | 205 | /** 206 | * Executes the command to use a Quarto template. 207 | * This function prompts the user to select a workspace folder, then installs a Quarto extension configured as a template. 208 | * 209 | * @param context - The VS Code extension context 210 | * @returns A Promise that resolves when the operation is complete, or void if the user cancels folder selection 211 | */ 212 | export async function useQuartoTemplateCommand(context: vscode.ExtensionContext) { 213 | const workspaceFolder = await selectWorkspaceFolder(); 214 | if (!workspaceFolder) { 215 | return; 216 | } 217 | installQuartoExtensionFolderCommand(context, workspaceFolder, true); 218 | } 219 | -------------------------------------------------------------------------------- /src/commands/newQuartoReprex.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { showLogsCommand, logMessage } from "../utils/log"; 3 | import { newQuartoReprex } from "../utils/reprex"; 4 | 5 | /** 6 | * Command to create a new Quarto REPRoducible EXample (reprex). 7 | * Prompts the user to select a computing language (R, Python, or Julia) and then creates a reprex for the selected language. 8 | * If no language is selected, displays an error message and aborts the operation. 9 | * 10 | * @param context - The extension context. 11 | */ 12 | export async function newQuartoReprexCommand(context: vscode.ExtensionContext) { 13 | const languages = ["R", "Python", "Julia"]; 14 | const selectedLanguage = await vscode.window.showQuickPick(languages, { 15 | placeHolder: "Select the computing language", 16 | }); 17 | 18 | if (selectedLanguage) { 19 | newQuartoReprex(selectedLanguage, context); 20 | } else { 21 | const message = `No computing language selected. Aborting.`; 22 | logMessage(message, "error"); 23 | vscode.window.showErrorMessage(`${message} ${showLogsCommand()}.`); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | /** 4 | * Output channel for Quarto Wizard logs. 5 | */ 6 | export const QW_LOG = vscode.window.createOutputChannel("Quarto Wizard", { log: true }); 7 | 8 | /** 9 | * Key for storing recently installed extensions. 10 | */ 11 | export const QW_RECENTLY_INSTALLED = "recentlyInstalledExtensions"; 12 | 13 | /** 14 | * Key for storing recently used templates. 15 | */ 16 | export const QW_RECENTLY_USED = "recentlyUsedTemplates"; 17 | 18 | /** 19 | * URL to the Quarto extensions CSV file. 20 | */ 21 | export const QW_EXTENSIONS = 22 | "https://raw.githubusercontent.com/mcanouil/quarto-extensions/refs/heads/quarto-wizard/quarto-extensions.json"; 23 | 24 | /** 25 | * Key for caching the Quarto extensions JSON. 26 | */ 27 | export const QW_EXTENSIONS_CACHE = "quarto_wizard_extensions"; 28 | 29 | /** 30 | * Cache duration for the Quarto extensions JSON (default to 1 hour). 31 | */ 32 | export const QW_EXTENSIONS_CACHE_TIME = 0 * 60 * 60 * 1000; 33 | 34 | /** 35 | * Markdown Lint extension identifier. 36 | */ 37 | export const kMarkDownLintExtension = "DavidAnson.vscode-markdownlint"; 38 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { QW_LOG, QW_RECENTLY_INSTALLED, QW_RECENTLY_USED } from "./constants"; 3 | import { showLogsCommand, logMessage } from "./utils/log"; 4 | import { installQuartoExtensionCommand, useQuartoTemplateCommand } from "./commands/installQuartoExtension"; 5 | import { newQuartoReprexCommand } from "./commands/newQuartoReprex"; 6 | import { ExtensionsInstalled } from "./ui/extensionsInstalled"; 7 | import { getExtensionsDetails } from "./utils/extensionDetails"; 8 | import { lint } from "./utils/lint"; 9 | import { handleUri } from "./utils/handleUri"; 10 | 11 | /** 12 | * This method is called when the extension is activated. 13 | * It registers various commands and initialises the ExtensionsInstalled class that defines the installed extensions view. 14 | * It also registers a URI handler for extension protocol links. 15 | * 16 | * @param context - The context in which the extension is running. 17 | */ 18 | export function activate(context: vscode.ExtensionContext) { 19 | context.subscriptions.push(vscode.commands.registerCommand("quartoWizard.showOutput", () => QW_LOG.show())); 20 | QW_LOG.appendLine("Quarto Wizard, your magical assistant, is now active!"); 21 | 22 | lint(context); 23 | 24 | context.subscriptions.push( 25 | vscode.commands.registerCommand("quartoWizard.clearRecent", () => { 26 | context.globalState.update(QW_RECENTLY_INSTALLED, []); 27 | context.globalState.update(QW_RECENTLY_USED, []); 28 | const message = "Recently installed Quarto extensions have been cleared."; 29 | logMessage(message, "info"); 30 | vscode.window.showInformationMessage(`${message} ${showLogsCommand()}.`); 31 | }) 32 | ); 33 | 34 | context.subscriptions.push( 35 | vscode.commands.registerCommand("quartoWizard.installExtension", async () => installQuartoExtensionCommand(context)) 36 | ); 37 | context.subscriptions.push( 38 | vscode.commands.registerCommand("quartoWizard.useTemplate", async () => useQuartoTemplateCommand(context)) 39 | ); 40 | 41 | context.subscriptions.push( 42 | vscode.commands.registerCommand("quartoWizard.newQuartoReprex", () => newQuartoReprexCommand(context)) 43 | ); 44 | 45 | context.subscriptions.push( 46 | vscode.commands.registerCommand("quartoWizard.getExtensionsDetails", () => getExtensionsDetails(context)) 47 | ); 48 | 49 | new ExtensionsInstalled(context); 50 | 51 | vscode.window.registerUriHandler({ 52 | handleUri: (uri: vscode.Uri) => handleUri(uri, context), 53 | }); 54 | } 55 | 56 | /** 57 | * This method is called when the extension is deactivated. 58 | * Currently, it does not perform any specific action. 59 | */ 60 | export function deactivate() { 61 | // No cleanup necessary 62 | } 63 | -------------------------------------------------------------------------------- /src/ui/extensionsInstalled.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as path from "path"; 3 | import * as fs from "fs"; 4 | import * as semver from "semver"; 5 | import { debounce } from "lodash"; 6 | import { logMessage, showLogsCommand } from "../utils/log"; 7 | import { ExtensionData, findQuartoExtensions, readExtensions } from "../utils/extensions"; 8 | import { removeQuartoExtension, installQuartoExtensionSource } from "../utils/quarto"; 9 | import { getExtensionsDetails } from "../utils/extensionDetails"; 10 | import { installQuartoExtensionFolderCommand } from "../commands/installQuartoExtension"; 11 | 12 | /** 13 | * Represents a tree item for a workspace folder. 14 | */ 15 | class WorkspaceFolderTreeItem extends vscode.TreeItem { 16 | public workspaceFolder: string; 17 | 18 | constructor(public readonly label: string, public readonly folderPath: string) { 19 | super(label, vscode.TreeItemCollapsibleState.Expanded); 20 | this.contextValue = "quartoExtensionWorkspaceFolder"; 21 | this.iconPath = new vscode.ThemeIcon("folder"); 22 | this.tooltip = folderPath; 23 | this.workspaceFolder = folderPath; 24 | } 25 | } 26 | 27 | /** 28 | * Represents a tree item for a Quarto extension. 29 | */ 30 | class ExtensionTreeItem extends vscode.TreeItem { 31 | public latestVersion?: string; 32 | public workspaceFolder: string; 33 | 34 | constructor( 35 | public readonly label: string, 36 | public readonly collapsibleState: vscode.TreeItemCollapsibleState, 37 | public readonly workspacePath: string, 38 | public readonly data?: ExtensionData, 39 | icon?: string, 40 | latestVersion?: string 41 | ) { 42 | super(label, collapsibleState); 43 | const needsUpdate = latestVersion !== undefined; 44 | const baseContextValue = "quartoExtensionItem"; 45 | let contextValue = baseContextValue; 46 | if (needsUpdate) { 47 | contextValue = baseContextValue + "Outdated"; 48 | } else if (data && !this.data?.source) { 49 | contextValue = baseContextValue + "NoSource"; 50 | } 51 | this.tooltip = `${this.label}`; 52 | this.description = this.data ? `${this.data.version}${needsUpdate ? ` (latest: ${latestVersion})` : ""}` : ""; 53 | this.contextValue = this.data ? contextValue : "quartoExtensionItemDetails"; 54 | if (icon) { 55 | this.iconPath = new vscode.ThemeIcon(icon); 56 | } 57 | this.latestVersion = latestVersion !== "unknown" ? `@${latestVersion}` : ""; 58 | this.workspaceFolder = workspacePath; 59 | } 60 | } 61 | 62 | /** 63 | * Provides data for the Quarto extensions tree view. 64 | */ 65 | class QuartoExtensionTreeDataProvider implements vscode.TreeDataProvider { 66 | private _onDidChangeTreeData: vscode.EventEmitter = 67 | new vscode.EventEmitter(); 68 | readonly onDidChangeTreeData: vscode.Event = 69 | this._onDidChangeTreeData.event; 70 | 71 | private extensionsDataByFolder: Record> = {}; 72 | private latestVersionsByFolder: Record> = {}; 73 | 74 | constructor(private workspaceFolders: readonly vscode.WorkspaceFolder[]) { 75 | this.refreshAllExtensionsData(); 76 | } 77 | 78 | getTreeItem(element: WorkspaceFolderTreeItem | ExtensionTreeItem): vscode.TreeItem { 79 | return element; 80 | } 81 | 82 | getChildren( 83 | element?: WorkspaceFolderTreeItem | ExtensionTreeItem 84 | ): Thenable<(WorkspaceFolderTreeItem | ExtensionTreeItem)[]> { 85 | if (!element) { 86 | if (this.workspaceFolders.length === 0) { 87 | return Promise.resolve([ 88 | new ExtensionTreeItem( 89 | "No workspace folders open.", 90 | vscode.TreeItemCollapsibleState.None, 91 | "", 92 | undefined, 93 | "info" 94 | ), 95 | ]); 96 | } 97 | return Promise.resolve(this.getWorkspaceFolderItems()); 98 | } 99 | 100 | if (element instanceof WorkspaceFolderTreeItem) { 101 | return Promise.resolve(this.getExtensionItems(element.folderPath)); 102 | } 103 | 104 | return Promise.resolve(this.getExtensionDetailItems(element)); 105 | } 106 | 107 | private getWorkspaceFolderItems(): WorkspaceFolderTreeItem[] { 108 | if (this.workspaceFolders.length > 1) { 109 | return this.workspaceFolders.map((folder) => { 110 | return new WorkspaceFolderTreeItem(folder.name, folder.uri.fsPath); 111 | }); 112 | } 113 | return this.workspaceFolders 114 | .filter((folder) => { 115 | const folderData = this.extensionsDataByFolder[folder.uri.fsPath] || {}; 116 | return Object.keys(folderData).length > 0; 117 | }) 118 | .map((folder) => new WorkspaceFolderTreeItem(folder.name, folder.uri.fsPath)); 119 | } 120 | 121 | private getExtensionItems(workspacePath: string): ExtensionTreeItem[] { 122 | const folderData = this.extensionsDataByFolder[workspacePath] || {}; 123 | if (Object.keys(folderData).length === 0) { 124 | return [ 125 | new ExtensionTreeItem( 126 | "No extensions installed.", 127 | vscode.TreeItemCollapsibleState.None, 128 | workspacePath, 129 | undefined, 130 | "info" 131 | ), 132 | ]; 133 | } 134 | 135 | return Object.keys(folderData).map( 136 | (ext) => 137 | new ExtensionTreeItem( 138 | ext, 139 | vscode.TreeItemCollapsibleState.Collapsed, 140 | workspacePath, 141 | folderData[ext], 142 | "package", 143 | this.latestVersionsByFolder[workspacePath]?.[ext] 144 | ) 145 | ); 146 | } 147 | 148 | private getExtensionDetailItems(element: ExtensionTreeItem): ExtensionTreeItem[] { 149 | const data = element.data; 150 | if (!data) { 151 | return []; 152 | } 153 | return [ 154 | new ExtensionTreeItem(`Title: ${data.title}`, vscode.TreeItemCollapsibleState.None, element.workspaceFolder), 155 | new ExtensionTreeItem(`Author: ${data.author}`, vscode.TreeItemCollapsibleState.None, element.workspaceFolder), 156 | new ExtensionTreeItem(`Version: ${data.version}`, vscode.TreeItemCollapsibleState.None, element.workspaceFolder), 157 | new ExtensionTreeItem( 158 | `Contributes: ${data.contributes}`, 159 | vscode.TreeItemCollapsibleState.None, 160 | element.workspaceFolder 161 | ), 162 | new ExtensionTreeItem( 163 | `Repository: ${data.repository}`, 164 | vscode.TreeItemCollapsibleState.None, 165 | element.workspaceFolder 166 | ), 167 | new ExtensionTreeItem(`Source: ${data.source}`, vscode.TreeItemCollapsibleState.None, element.workspaceFolder), 168 | ]; 169 | } 170 | 171 | /** 172 | * Refreshes the tree data with a debounce. 173 | */ 174 | refresh = debounce((): void => { 175 | this.refreshAllExtensionsData(); 176 | this._onDidChangeTreeData.fire(); 177 | }, 300); // Debounce refresh calls with a 300ms delay 178 | 179 | /** 180 | * Forces a refresh of the tree data. 181 | */ 182 | forceRefresh(): void { 183 | this.refresh(); 184 | this.refresh.flush(); 185 | } 186 | 187 | private refreshAllExtensionsData(): void { 188 | this.extensionsDataByFolder = {}; 189 | 190 | for (const folder of this.workspaceFolders) { 191 | const workspaceFolder = folder.uri.fsPath; 192 | let extensionsList: string[] = []; 193 | 194 | if (fs.existsSync(path.join(workspaceFolder, "_extensions"))) { 195 | extensionsList = findQuartoExtensions(path.join(workspaceFolder, "_extensions")); 196 | } 197 | 198 | this.extensionsDataByFolder[workspaceFolder] = readExtensions(workspaceFolder, extensionsList); 199 | } 200 | } 201 | 202 | /** 203 | * Checks for updates to the installed extensions. 204 | * @param {vscode.ExtensionContext} context - The extension context. 205 | * @param {vscode.TreeView} [view] - The tree view. 206 | * @param {boolean} [silent=true] - Whether to show update messages. 207 | * @returns {Promise} - The number of updates available. 208 | */ 209 | async checkUpdate( 210 | context: vscode.ExtensionContext, 211 | view?: vscode.TreeView, 212 | silent = true 213 | ): Promise { 214 | const extensionsDetails = await getExtensionsDetails(context); 215 | const updatesAvailable: string[] = []; 216 | this.latestVersionsByFolder = {}; 217 | let totalUpdates = 0; 218 | 219 | for (const folder of this.workspaceFolders) { 220 | const workspacePath = folder.uri.fsPath; 221 | const folderData = this.extensionsDataByFolder[workspacePath] || {}; 222 | this.latestVersionsByFolder[workspacePath] = {}; 223 | 224 | for (const ext of Object.keys(folderData)) { 225 | const extensionData = folderData[ext]; 226 | const matchingDetail = extensionsDetails.find((detail) => detail.id === extensionData.repository); 227 | 228 | if (!extensionData.version || extensionData.version === "none") { 229 | continue; 230 | } 231 | 232 | if (matchingDetail?.version === "none") { 233 | this.latestVersionsByFolder[workspacePath][ext] = "unknown"; 234 | continue; 235 | } 236 | 237 | if (matchingDetail && semver.lt(extensionData.version, matchingDetail.version)) { 238 | updatesAvailable.push(`${folder.name}/${ext}`); 239 | this.latestVersionsByFolder[workspacePath][ext] = matchingDetail.version; 240 | totalUpdates++; 241 | } 242 | } 243 | } 244 | 245 | if (updatesAvailable.length > 0 && !silent) { 246 | const message = `Updates available for the following extensions: ${updatesAvailable.join(", ")}.`; 247 | logMessage(message, "info"); 248 | } 249 | 250 | if (view) { 251 | view.badge = { 252 | value: totalUpdates, 253 | tooltip: `${totalUpdates} update${totalUpdates === 1 ? "" : "s"} available`, 254 | }; 255 | } 256 | 257 | return totalUpdates; 258 | } 259 | } 260 | 261 | /** 262 | * Manages the installed Quarto extensions. 263 | * Sets up the tree data provider and registers the necessary commands. 264 | */ 265 | export class ExtensionsInstalled { 266 | private treeDataProvider!: QuartoExtensionTreeDataProvider; 267 | 268 | /** 269 | * Initialises the extensions view and sets up the tree data provider and commands. 270 | * @param {vscode.ExtensionContext} context - The extension context. 271 | */ 272 | private initialise(context: vscode.ExtensionContext) { 273 | const workspaceFolders = vscode.workspace.workspaceFolders || []; 274 | if (workspaceFolders.length === 0) { 275 | const message = `Please open a workspace/folder to install Quarto extensions.`; 276 | logMessage(message, "error"); 277 | vscode.window.showErrorMessage(`${message} ${showLogsCommand()}.`); 278 | return; 279 | } 280 | 281 | this.treeDataProvider = new QuartoExtensionTreeDataProvider(workspaceFolders); 282 | const view = vscode.window.createTreeView("quartoWizard.extensionsInstalled", { 283 | treeDataProvider: this.treeDataProvider, 284 | showCollapseAll: true, 285 | }); 286 | 287 | this.treeDataProvider.checkUpdate(context, view, false); 288 | this.treeDataProvider.refresh(); 289 | 290 | view.onDidChangeVisibility((e) => { 291 | if (e.visible) { 292 | this.treeDataProvider.checkUpdate(context, view); 293 | this.treeDataProvider.refresh(); 294 | } 295 | }); 296 | // view.onDidChangeSelection((e) => { 297 | // if (e.selection) { 298 | // this.treeDataProvider.checkUpdate(context, view); 299 | // this.treeDataProvider.refresh(); 300 | // } 301 | // }); 302 | 303 | context.subscriptions.push(view); 304 | context.subscriptions.push( 305 | vscode.commands.registerCommand("quartoWizard.extensionsInstalled.refresh", () => { 306 | this.treeDataProvider.forceRefresh(); 307 | this.treeDataProvider.checkUpdate(context, view); 308 | this.treeDataProvider.forceRefresh(); 309 | }) 310 | ); 311 | 312 | context.subscriptions.push( 313 | vscode.commands.registerCommand("quartoWizard.extensionsInstalled.openSource", (item: ExtensionTreeItem) => { 314 | if (item.data?.repository) { 315 | const url = `https://github.com/${item.data?.repository}`; 316 | vscode.env.openExternal(vscode.Uri.parse(url)); 317 | } 318 | }) 319 | ); 320 | 321 | context.subscriptions.push( 322 | vscode.commands.registerCommand("quartoWizard.extensionsInstalled.install", async (item: ExtensionTreeItem) => { 323 | installQuartoExtensionFolderCommand(context, item.workspaceFolder, false); 324 | }) 325 | ); 326 | 327 | context.subscriptions.push( 328 | vscode.commands.registerCommand( 329 | "quartoWizard.extensionsInstalled.useTemplate", 330 | async (item: ExtensionTreeItem) => { 331 | installQuartoExtensionFolderCommand(context, item.workspaceFolder, true); 332 | } 333 | ) 334 | ); 335 | 336 | /** 337 | * Updates a Quarto extension to the latest version. 338 | * Uses the source repository information from the extension manifest. 339 | */ 340 | context.subscriptions.push( 341 | vscode.commands.registerCommand("quartoWizard.extensionsInstalled.update", async (item: ExtensionTreeItem) => { 342 | const success = await installQuartoExtensionSource( 343 | `${item.data?.repository ?? item.label}${item.latestVersion}`, 344 | item.workspaceFolder 345 | ); 346 | // Once source is supported in _extension.yml, the above line can be replaced with the following line 347 | // const success = await installQuartoExtension(item.data?.repository ?? item.label); 348 | if (success) { 349 | vscode.window.showInformationMessage(`Extension "${item.label}" updated successfully.`); 350 | this.treeDataProvider.checkUpdate(context, view); 351 | this.treeDataProvider.forceRefresh(); 352 | } else { 353 | if (!item.data?.repository) { 354 | vscode.window.showErrorMessage( 355 | `Failed to update extension "${item.label}". ` + 356 | `Source not found in extension manifest. ` + 357 | `${showLogsCommand()}.` 358 | ); 359 | } else { 360 | vscode.window.showErrorMessage(`Failed to update extension ${item.label}. ${showLogsCommand()}.`); 361 | } 362 | } 363 | }) 364 | ); 365 | 366 | /** 367 | * Removes a Quarto extension from the workspace. 368 | * Deletes the extension directory and refreshes the view. 369 | */ 370 | context.subscriptions.push( 371 | vscode.commands.registerCommand("quartoWizard.extensionsInstalled.remove", async (item: ExtensionTreeItem) => { 372 | const success = await removeQuartoExtension(item.label, item.workspaceFolder); 373 | if (success) { 374 | vscode.window.showInformationMessage(`Extension "${item.label}" removed successfully.`); 375 | this.treeDataProvider.checkUpdate(context, view); 376 | this.treeDataProvider.forceRefresh(); 377 | } else { 378 | vscode.window.showErrorMessage(`Failed to remove extension "${item.label}". ${showLogsCommand()}.`); 379 | } 380 | }) 381 | ); 382 | } 383 | 384 | constructor(context: vscode.ExtensionContext) { 385 | this.initialise(context); 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /src/ui/extensionsQuickPick.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { ExtensionDetails } from "../utils/extensionDetails"; 3 | 4 | /** 5 | * Interface representing a QuickPick item for an extension. 6 | */ 7 | export interface ExtensionQuickPickItem extends vscode.QuickPickItem { 8 | url?: string; 9 | id?: string; 10 | tag?: string; 11 | template?: boolean; 12 | templateContent?: string; 13 | } 14 | 15 | /** 16 | * Creates QuickPick items from extension details. 17 | * @param {ExtensionDetails[]} extensions - The list of extension details. 18 | * @returns {ExtensionQuickPickItem[]} - An array of QuickPick items. 19 | */ 20 | export function createExtensionItems(extensions: ExtensionDetails[]): ExtensionQuickPickItem[] { 21 | return extensions.map((ext) => ({ 22 | label: ext.name, 23 | description: `$(tag) ${ext.version} $(star) ${ext.stars.toString()} $(repo) ${ext.full_name} $(law) ${ext.license}`, 24 | detail: `${ext.description}`, 25 | buttons: [ 26 | { 27 | iconPath: new vscode.ThemeIcon("github"), 28 | tooltip: "Open GitHub Repository", 29 | }, 30 | ], 31 | url: ext.html_url, 32 | id: ext.id, 33 | tag: ext.tag, 34 | template: ext.template, 35 | templateContent: ext.templateContent, 36 | })); 37 | } 38 | 39 | /** 40 | * Shows a QuickPick for selecting Quarto extensions. 41 | * @param {ExtensionDetails[]} extensionsList - The list of extension details. 42 | * @param {string[]} recentlyInstalled - The list of recently installed or used extensions. 43 | * @param {boolean} [template=false] - Whether this is for template selection. If true, only one template can be selected. 44 | * @returns {Promise} - A promise that resolves to the selected QuickPick items. 45 | */ 46 | export async function showExtensionQuickPick( 47 | extensionsList: ExtensionDetails[], 48 | recentlyInstalled: string[], 49 | template = false 50 | ): Promise { 51 | const groupedExtensions: ExtensionQuickPickItem[] = [ 52 | { 53 | label: template ? "Recently Used" : "Recently Installed", 54 | kind: vscode.QuickPickItemKind.Separator, 55 | }, 56 | ...createExtensionItems(extensionsList.filter((ext) => recentlyInstalled.includes(ext.id))), 57 | { 58 | label: "All Extensions", 59 | kind: vscode.QuickPickItemKind.Separator, 60 | }, 61 | ...createExtensionItems(extensionsList.filter((ext) => !recentlyInstalled.includes(ext.id))).sort((a, b) => 62 | a.label.localeCompare(b.label) 63 | ), 64 | ]; 65 | 66 | const quickPick = vscode.window.createQuickPick(); 67 | quickPick.items = groupedExtensions; 68 | quickPick.placeholder = template ? "Select Quarto extension template to use" : "Select Quarto extensions to install"; 69 | quickPick.canSelectMany = !template; 70 | quickPick.matchOnDescription = true; 71 | quickPick.onDidTriggerItemButton((e) => { 72 | const url = e.item.url; 73 | if (url) { 74 | vscode.env.openExternal(vscode.Uri.parse(url)); 75 | } 76 | }); 77 | 78 | return new Promise((resolve) => { 79 | quickPick.onDidAccept(() => { 80 | resolve(quickPick.selectedItems); 81 | quickPick.hide(); 82 | }); 83 | quickPick.show(); 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /src/utils/activate.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { QW_LOG } from "../constants"; 3 | 4 | /** 5 | * Prompts the user to install a specified extension if it is not already installed. 6 | * The user's choice is stored in the global state to avoid prompting repeatedly. 7 | * 8 | * @param extensionId - The ID of the extension to be installed. 9 | * @param context - The extension context which provides access to the global state. 10 | * @returns A promise that resolves when the prompt handling is complete. 11 | * 12 | * The function performs the following actions based on the user's choice: 13 | * - "Install Now": Initiates the installation of the extension. 14 | * - "Maybe Later": Logs the user's choice and sets a flag to prompt again later. 15 | * - "Don't Ask Again": Logs the user's choice and sets a flag to avoid future prompts. 16 | */ 17 | async function promptInstallExtension(extensionId: string, context: vscode.ExtensionContext): Promise { 18 | const kPromptInstallExtension = "PromptInstallExtension"; 19 | const prompt = context.globalState.get(`${kPromptInstallExtension}.${extensionId}`); 20 | if (prompt === false) { 21 | return; 22 | } 23 | const choice = await vscode.window.showInformationMessage( 24 | `Extension '${extensionId}' is not installed. Would you like to install it?`, 25 | "Install Now", 26 | "Maybe Later", 27 | "Don't Ask Again" 28 | ); 29 | switch (choice) { 30 | case "Install Now": 31 | await vscode.commands.executeCommand("workbench.extensions.installExtension", extensionId); 32 | QW_LOG.appendLine(`${extensionId} installation initiated.`); 33 | break; 34 | case "Maybe Later": 35 | QW_LOG.appendLine(`User chose to install ${extensionId} later.`); 36 | context.globalState.update(`${kPromptInstallExtension}.${extensionId}`, true); 37 | break; 38 | case "Don't Ask Again": 39 | QW_LOG.appendLine(`User chose not to be asked again about ${extensionId}.`); 40 | context.globalState.update(`${kPromptInstallExtension}.${extensionId}`, false); 41 | break; 42 | } 43 | } 44 | 45 | /** 46 | * Activates a list of VS Code extensions. 47 | * For each extension in the list, the function attempts to: 48 | * - Get the extension from the VS Code API 49 | * - Activate it if it exists but is not already active 50 | * - Prompt the user to install it if it does not exist 51 | * 52 | * @param extensions - An array of extension IDs to activate. 53 | * @param context - The VS Code extension context. 54 | * @returns A promise that resolves when all extensions have been processed. 55 | */ 56 | export async function activateExtensions(extensions: string[], context: vscode.ExtensionContext): Promise { 57 | extensions.forEach(async (extensionId) => { 58 | const extension = await vscode.extensions.getExtension(extensionId); 59 | if (extension) { 60 | if (!extension.isActive) { 61 | await extension.activate(); 62 | QW_LOG.appendLine(`${extensionId} activated.`); 63 | } 64 | } else { 65 | QW_LOG.appendLine(`Failed to activate ${extensionId}.`); 66 | await promptInstallExtension(extensionId, context); 67 | } 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/ask.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { showLogsCommand, logMessage } from "../utils/log"; 3 | 4 | /** 5 | * Prompts the user to trust the authors of the selected extensions when the trustAuthors setting is set to "ask". 6 | * @returns {Promise} - Returns 0 if the authors are trusted or if the setting is updated to "never", otherwise returns 1. 7 | */ 8 | export async function askTrustAuthors(): Promise { 9 | const config = vscode.workspace.getConfiguration("quartoWizard.ask", null); 10 | const configTrustAuthors = config.get("trustAuthors"); 11 | 12 | if (configTrustAuthors === "ask") { 13 | const trustAuthors = await vscode.window.showQuickPick( 14 | [ 15 | { label: "Yes", description: "Trust authors." }, 16 | { label: "No", description: "Do not trust authors." }, 17 | { label: "Yes, always trust", description: "Change setting to always trust." }, 18 | ], 19 | { 20 | placeHolder: "Do you trust the authors of the selected extension(s)?", 21 | } 22 | ); 23 | if (trustAuthors?.label === "Yes, always trust") { 24 | await config.update("trustAuthors", "never", vscode.ConfigurationTarget.Global); 25 | return 0; 26 | } else if (trustAuthors?.label !== "Yes") { 27 | const message = "Operation cancelled because the authors are not trusted."; 28 | logMessage(message, "info"); 29 | vscode.window.showInformationMessage(`${message} ${showLogsCommand()}.`); 30 | return 1; 31 | } 32 | } 33 | return 0; 34 | } 35 | 36 | /** 37 | * Prompts the user to confirm the installation of the selected extensions when the confirmInstall setting is set to "ask". 38 | * @returns {Promise} - Returns 0 if the installation is confirmed or if the setting is updated to "never", otherwise returns 1. 39 | */ 40 | export async function askConfirmInstall(): Promise { 41 | const config = vscode.workspace.getConfiguration("quartoWizard.ask", null); 42 | const configConfirmInstall = config.get("confirmInstall"); 43 | 44 | if (configConfirmInstall === "ask") { 45 | const installWorkspace = await vscode.window.showQuickPick( 46 | [ 47 | { label: "Yes", description: "Install extensions." }, 48 | { label: "No", description: "Do not install extensions." }, 49 | { label: "Yes, always trust", description: "Change setting to always trust." }, 50 | ], 51 | { 52 | placeHolder: "Do you want to install the selected extension(s)?", 53 | } 54 | ); 55 | if (installWorkspace?.label === "Yes, always trust") { 56 | await config.update("confirmInstall", "never", vscode.ConfigurationTarget.Global); 57 | return 0; 58 | } else if (installWorkspace?.label !== "Yes") { 59 | const message = "Operation cancelled by the user."; 60 | logMessage(message, "info"); 61 | vscode.window.showInformationMessage(`${message} ${showLogsCommand()}.`); 62 | return 1; 63 | } 64 | } 65 | return 0; 66 | } 67 | 68 | /** 69 | * Prompts the user to confirm the removal of the selected extensions when the confirmRemove setting is set to "always". 70 | * @returns {Promise} - Returns 0 if the removal is confirmed or if the setting is updated to "never", otherwise returns 1. 71 | */ 72 | export async function askConfirmRemove(): Promise { 73 | const config = vscode.workspace.getConfiguration("quartoWizard.ask"); 74 | const configConfirmRemove = config.get("confirmRemove"); 75 | 76 | if (configConfirmRemove === "always") { 77 | const removeWorkspace = await vscode.window.showQuickPick( 78 | [ 79 | { label: "Yes", description: "Remove extensions." }, 80 | { label: "No", description: "Do not remove extensions." }, 81 | { label: "Yes, always trust", description: "Change setting to always trust." }, 82 | ], 83 | { 84 | placeHolder: "Do you want to remove the selected extension(s)?", 85 | } 86 | ); 87 | if (removeWorkspace?.label === "Yes, always trust") { 88 | await config.update("confirmRemove", "never", vscode.ConfigurationTarget.Global); 89 | return 0; 90 | } else if (removeWorkspace?.label !== "Yes") { 91 | const message = "Operation cancelled by the user."; 92 | logMessage(message, "info"); 93 | vscode.window.showInformationMessage(`${message} ${showLogsCommand()}.`); 94 | return 1; 95 | } 96 | } 97 | return 0; 98 | } 99 | -------------------------------------------------------------------------------- /src/utils/extensionDetails.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { QW_EXTENSIONS, QW_EXTENSIONS_CACHE, QW_EXTENSIONS_CACHE_TIME } from "../constants"; 3 | import { logMessage, debouncedLogMessage } from "./log"; 4 | import { generateHashKey } from "./hash"; 5 | 6 | /** 7 | * Interface representing the details of a Quarto extension. 8 | */ 9 | export interface ExtensionDetails { 10 | id: string; // Unique identifier for the extension 11 | name: string; // Display name of the extension 12 | full_name: string; // "owner/repo" format 13 | owner: string; // Owner/organisation name 14 | description: string; // Extension description 15 | stars: number; // GitHub star count 16 | license: string; // Licence information 17 | html_url: string; // GitHub repository URL 18 | version: string; // Current version (without 'v' prefix) 19 | tag: string; // Release tag 20 | template: boolean; // Whether this extension is a template 21 | templateContent: string; // Content of the template if applicable 22 | } 23 | 24 | /** 25 | * Fetches the list of Quarto extensions, using cached data if available. 26 | * @param {vscode.ExtensionContext} context - The extension context used for caching. 27 | * @returns {Promise} - A promise that resolves to an array of extension details or empty array on error. 28 | */ 29 | async function fetchExtensions(context: vscode.ExtensionContext): Promise { 30 | const url = QW_EXTENSIONS; 31 | const cacheKey = `${QW_EXTENSIONS_CACHE}_${generateHashKey(url)}`; 32 | const cachedData = context.globalState.get<{ data: ExtensionDetails[]; timestamp: number }>(cacheKey); 33 | 34 | if (cachedData && Date.now() - cachedData.timestamp < QW_EXTENSIONS_CACHE_TIME) { 35 | debouncedLogMessage(`Using cached extensions: ${new Date(cachedData.timestamp).toISOString()}`, "info"); 36 | return cachedData.data; 37 | } 38 | 39 | debouncedLogMessage(`Fetching extensions: ${url}`, "info"); 40 | let message = `Error fetching list of extensions from ${url}.`; 41 | try { 42 | const response: Response = await fetch(url); 43 | if (!response.ok) { 44 | message = `${message}. ${response.statusText}`; 45 | throw new Error(`Failed to fetch ${url}: ${response.statusText}`); 46 | } 47 | const data = await response.text(); 48 | const extensionsDetailsList = await parseExtensionsDetails(data); 49 | await context.globalState.update(cacheKey, { data: extensionsDetailsList, timestamp: Date.now() }); 50 | return extensionsDetailsList; 51 | } catch (error) { 52 | logMessage(`${message} ${error}`, "error"); 53 | return []; 54 | } 55 | } 56 | 57 | /** 58 | * Parses the details of Quarto extensions from JSON data. 59 | * @param {string} data - The extensions details as JSON with extension keys and metadata. 60 | * @returns {Promise} - A promise that resolves to an array of extension details or empty array on error. 61 | */ 62 | async function parseExtensionsDetails(data: string): Promise { 63 | try { 64 | const parsedData = JSON.parse(data); 65 | const extensionDetailsList: ExtensionDetails[] = Object.keys(parsedData).map((key) => { 66 | const extension = parsedData[key]; 67 | return { 68 | id: key, 69 | name: extension.title, 70 | full_name: extension.nameWithOwner, 71 | owner: extension.owner, 72 | description: extension.description, 73 | stars: extension.stargazerCount, 74 | license: extension.licenseInfo, 75 | html_url: extension.url, 76 | version: extension.latestRelease.replace(/^v/, ""), 77 | tag: extension.latestRelease, 78 | template: extension.template, 79 | templateContent: extension.templateContent, 80 | }; 81 | }); 82 | return extensionDetailsList; 83 | } catch (error) { 84 | const message = "Error parsing extension details."; 85 | logMessage(`${message} ${error}`, "error"); 86 | return []; 87 | } 88 | } 89 | 90 | /** 91 | * Fetches the details of all valid Quarto extensions and filters out any undefined entries. 92 | * @param {vscode.ExtensionContext} context - The extension context used for caching. 93 | * @returns {Promise} - A promise that resolves to an array of validated extension details. 94 | */ 95 | export async function getExtensionsDetails(context: vscode.ExtensionContext): Promise { 96 | const extensions = await fetchExtensions(context); 97 | 98 | return extensions.filter((extension): extension is ExtensionDetails => extension !== undefined); 99 | } 100 | -------------------------------------------------------------------------------- /src/utils/extensions.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import * as yaml from "js-yaml"; 4 | import { logMessage } from "./log"; 5 | 6 | /** 7 | * Recursively finds Quarto extension files in a directory. 8 | * @param {string} directory - The directory to search. 9 | * @returns {string[]} - An array of file paths to the found extension files. 10 | */ 11 | function findQuartoExtensionsRecurse(directory: string): string[] { 12 | let results: string[] = []; 13 | const list = fs.readdirSync(directory); 14 | list.forEach((file) => { 15 | const filePath = path.join(directory, file); 16 | const stat = fs.statSync(filePath); 17 | if (stat && stat.isDirectory() && path.basename(filePath) !== "_extensions") { 18 | results = results.concat(findQuartoExtensionsRecurse(filePath)); 19 | } else if (file.endsWith("_extension.yml") || file.endsWith("_extension.yaml")) { 20 | results.push(filePath); 21 | } 22 | }); 23 | return results; 24 | } 25 | 26 | /** 27 | * Finds Quarto extensions in a directory. 28 | * @param {string} directory - The directory to search. 29 | * @returns {string[]} - An array of relative paths to the found extensions. 30 | */ 31 | export function findQuartoExtensions(directory: string): string[] { 32 | return findQuartoExtensionsRecurse(directory).map((filePath) => path.relative(directory, path.dirname(filePath))); 33 | } 34 | 35 | /** 36 | * Gets the modification times of Quarto extensions in a directory. 37 | * @param {string} directory - The directory to search. 38 | * @returns {{ [key: string]: Date }} - An object mapping extension paths to their modification times. 39 | */ 40 | export function getMtimeExtensions(directory: string): Record { 41 | if (!fs.existsSync(directory)) { 42 | return {}; 43 | } 44 | const extensions = findQuartoExtensions(directory); 45 | const extensionsMtimeDict: Record = {}; 46 | extensions.forEach((extension) => { 47 | extensionsMtimeDict[extension] = fs.statSync(path.join(directory, extension)).mtime; 48 | }); 49 | return extensionsMtimeDict; 50 | } 51 | 52 | /** 53 | * Finds modified Quarto extensions in a directory. 54 | * @param {{ [key: string]: Date }} extensions - An object mapping extension paths to their previous modification times. 55 | * @param {string} directory - The directory to search. 56 | * @returns {string[]} - An array of relative paths to the modified extensions. 57 | */ 58 | export function findModifiedExtensions(extensions: Record, directory: string): string[] { 59 | if (!fs.existsSync(directory)) { 60 | return []; 61 | } 62 | const modifiedExtensions: string[] = []; 63 | const currentExtensions = findQuartoExtensions(directory); 64 | currentExtensions.forEach((extension) => { 65 | const extensionPath = path.join(directory, extension); 66 | const extensionMtime = fs.statSync(extensionPath).mtime; 67 | if (!extensions[extension] || extensions[extension] < extensionMtime) { 68 | modifiedExtensions.push(extension); 69 | } 70 | }); 71 | return modifiedExtensions; 72 | } 73 | 74 | /** 75 | * Interface representing the data of a Quarto extension. 76 | */ 77 | export interface ExtensionData { 78 | title?: string; 79 | author?: string; 80 | version?: string; 81 | contributes?: string; 82 | source?: string; 83 | repository?: string; 84 | } 85 | 86 | /** 87 | * Reads a YAML file and returns its data as an ExtensionData object. 88 | * @param {string} filePath - The path to the YAML file. 89 | * @returns {ExtensionData | null} - The parsed data or null if the file does not exist. 90 | */ 91 | function readYamlFile(filePath: string): ExtensionData | null { 92 | if (!fs.existsSync(filePath)) { 93 | return null; 94 | } 95 | const fileContent = fs.readFileSync(filePath, "utf8"); 96 | const data = yaml.load(fileContent) as any; // eslint-disable-line @typescript-eslint/no-explicit-any 97 | return { 98 | title: data.title, 99 | author: data.author, 100 | version: data.version, 101 | contributes: Object.keys(data.contributes).join(", "), 102 | source: data.source, 103 | repository: data.source ? data.source.replace(/@.*$/, "") : undefined, 104 | }; 105 | } 106 | 107 | /** 108 | * Reads Quarto extensions data from a workspace folder. 109 | * @param {string} workspaceFolder - The workspace folder to search. 110 | * @param {string[]} extensions - An array of extension names to read. 111 | * @returns {Record} - An object mapping extension names to their data. 112 | */ 113 | export function readExtensions(workspaceFolder: string, extensions: string[]): Record { 114 | const extensionsData: Record = {}; 115 | for (const ext of extensions) { 116 | let filePath = path.join(workspaceFolder, "_extensions", ext, "_extension.yml"); 117 | if (!fs.existsSync(filePath)) { 118 | filePath = path.join(workspaceFolder, "_extensions", ext, "_extension.yaml"); 119 | } 120 | const extData = readYamlFile(filePath); 121 | if (extData) { 122 | extensionsData[ext] = extData; 123 | } 124 | } 125 | return extensionsData; 126 | } 127 | 128 | /** 129 | * Removes a specified extension and its parent directories if they become empty. 130 | * 131 | * @param extension - The name of the extension to remove. 132 | * @param root - The root directory where the extension is located. 133 | * @returns {boolean} - Status (true for success, false for failure). 134 | */ 135 | export async function removeExtension(extension: string, root: string): Promise { 136 | const extensionPath = path.join(root, extension); 137 | if (fs.existsSync(extensionPath)) { 138 | try { 139 | fs.rmSync(extensionPath, { recursive: true, force: true }); 140 | 141 | const ownerPath = path.dirname(extensionPath); 142 | if (fs.readdirSync(ownerPath).length === 0) { 143 | fs.rmdirSync(ownerPath); 144 | } 145 | 146 | if (fs.readdirSync(root).length === 0) { 147 | fs.rmdirSync(root); 148 | } 149 | return true; 150 | } catch (error) { 151 | logMessage(`Failed to remove extension: ${error}`); 152 | return false; 153 | } 154 | } else { 155 | logMessage(`Extension path does not exist: ${extensionPath}`); 156 | return false; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/utils/handleUri.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { installQuartoExtensionSource } from "./quarto"; 3 | import { showLogsCommand, logMessage } from "../utils/log"; 4 | import { selectWorkspaceFolder } from "../utils/workspace"; 5 | import { ExtensionDetails, getExtensionsDetails } from "../utils/extensionDetails"; 6 | import { openTemplate } from "../commands/installQuartoExtension"; 7 | 8 | /** 9 | * Handle the URI passed to the extension. 10 | * 11 | * @param uri - The URI passed to the extension. 12 | * @param context - The extension context used for accessing extension resources and state. 13 | * 14 | * Supported URI formats: 15 | * vscode://mcanouil.quarto-wizard/install?repo=/ 16 | * vscode://mcanouil.quarto-wizard/use?repo=/ 17 | */ 18 | export async function handleUri(uri: vscode.Uri, context: vscode.ExtensionContext) { 19 | switch (uri.path) { 20 | case "/install": 21 | handleUriInstall(uri); 22 | break; 23 | case "/use": 24 | handleUriUse(uri, context); 25 | break; 26 | default: 27 | logMessage(`Unsupported path: ${uri.path}`, "warn"); 28 | break; 29 | } 30 | } 31 | 32 | /** 33 | * Handles the installation of a Quarto extension from a repository URI. 34 | * 35 | * @param uri - The VS Code URI containing query parameters, expected to have a "repo" parameter 36 | * specifying the repository to install. 37 | * 38 | * @returns A Promise that resolves when the installation is complete or canceled. 39 | * The function doesn't return any value but may show information messages to the user 40 | * and perform the extension installation if confirmed. 41 | */ 42 | export async function handleUriInstall(uri: vscode.Uri) { 43 | const repo = new URLSearchParams(uri.query).get("repo"); 44 | const workspaceFolder = await selectWorkspaceFolder(); 45 | if (!repo || !workspaceFolder) { 46 | return; 47 | } 48 | const installWorkspace = await vscode.window.showInformationMessage( 49 | `Do you confirm the installation of "${repo}" extension?`, 50 | { modal: true }, 51 | "Yes", 52 | "No" 53 | ); 54 | 55 | if (installWorkspace === "No") { 56 | const message = "Operation cancelled by the user."; 57 | logMessage(message, "info"); 58 | vscode.window.showInformationMessage(`${message} ${showLogsCommand()}.`); 59 | return; 60 | } 61 | await installQuartoExtensionSource(repo, workspaceFolder); 62 | } 63 | 64 | /** 65 | * Handles the installation and immediate use of a Quarto extension from a repository URI. 66 | * 67 | * @param uri - The VS Code URI containing query parameters, expected to have a "repo" parameter 68 | * specifying the repository to install and use. 69 | * @param context - The extension context used to access extension resources and state. 70 | * 71 | * @returns A Promise that resolves when the installation and template opening is complete or canceled. 72 | * The function installs the specified extension and then opens its template for immediate use. 73 | */ 74 | export async function handleUriUse(uri: vscode.Uri, context: vscode.ExtensionContext) { 75 | await handleUriInstall(uri); 76 | const extensionsList = await getExtensionsDetails(context); 77 | const repoSource = new URLSearchParams(uri.query).get("repo"); 78 | const repo = repoSource?.replace(/@.*$/, ""); 79 | const matchingExtension = extensionsList.find((ext: ExtensionDetails) => ext.id === repo); 80 | if (!matchingExtension) { 81 | const message = `Extension "${repo}" not found.`; 82 | logMessage(message, "error"); 83 | vscode.window.showErrorMessage(`${message} ${showLogsCommand()}.`); 84 | return; 85 | } 86 | const extensionId = matchingExtension.id; 87 | const extensionTemplate = matchingExtension.templateContent; 88 | if (!extensionId || !extensionTemplate) { 89 | const message = "Invalid extension ID or template content."; 90 | logMessage(message, "error"); 91 | vscode.window.showErrorMessage(`${message} ${showLogsCommand()}.`); 92 | return; 93 | } 94 | await openTemplate(matchingExtension.id, matchingExtension.templateContent); 95 | } 96 | -------------------------------------------------------------------------------- /src/utils/hash.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "crypto"; 2 | 3 | /** 4 | * Generates a hash key for a given string. 5 | * 6 | * @param {string} object - The string to be hashed. 7 | * @returns {string} - The generated hash key in hexadecimal format. 8 | */ 9 | export function generateHashKey(object: string): string { 10 | return crypto.createHash("md5").update(object).digest("hex"); 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/lint.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { QW_LOG, kMarkDownLintExtension } from "../constants"; 3 | import { activateExtensions } from "./activate"; 4 | 5 | /** 6 | * Toggle markdown linting for the currently active text editor. 7 | */ 8 | export function toggleLinting() { 9 | vscode.commands.executeCommand("markdownlint.toggleLinting"); 10 | vscode.commands.executeCommand("markdownlint.toggleLinting"); // Toggle twice to ensure linting is enabled 11 | } 12 | 13 | /** 14 | * Lints the currently active text editor if the document language is "quarto". 15 | * 16 | * This function performs the following steps: 17 | * 1. Checks if there is an active text editor and if the document language is "quarto". 18 | * 2. Activates the "DavidAnson.vscode-markdownlint" extension. 19 | * 3. Changes the document language to "markdown". 20 | * 4. Toggles markdown linting twice to ensure it is enabled. 21 | * 5. Changes the document language back to "quarto". 22 | */ 23 | function triggerLint() { 24 | if (!vscode.extensions.getExtension(kMarkDownLintExtension)) { 25 | QW_LOG.appendLine(`The '${kMarkDownLintExtension}' extension is not installed.`); 26 | return; 27 | } 28 | const editor = vscode.window.activeTextEditor; 29 | if (editor && editor.document.languageId === "quarto") { 30 | vscode.languages 31 | .setTextDocumentLanguage(editor.document, "markdown") 32 | .then(toggleLinting) 33 | .then(() => { 34 | vscode.languages.setTextDocumentLanguage(editor.document, "quarto"); 35 | }); 36 | } 37 | } 38 | 39 | /** 40 | * Registers event listeners to trigger the linting process based on the user's configuration. 41 | * 42 | * The function reads the `quartoWizard.lint.trigger` configuration setting to determine when to trigger linting. 43 | * It supports two triggers: 44 | * - "save": Linting is triggered when a Quarto document is saved. 45 | * - "type": Linting is triggered when a Quarto document is modified. 46 | * 47 | * Depending on the trigger setting, the appropriate event listener is registered: 48 | * - For "save", it listens to the `onDidSaveTextDocument` event. 49 | * - For "type", it listens to the `onDidChangeTextDocument` event. 50 | * 51 | * When the specified event occurs, and the document's language ID is "quarto", the `quartoWizard.lint` command is executed. 52 | */ 53 | function lintOnEvent(lintOn: string) { 54 | if (!vscode.extensions.getExtension(kMarkDownLintExtension)) { 55 | QW_LOG.appendLine(`The '${kMarkDownLintExtension}' extension is not installed.`); 56 | return; 57 | } 58 | switch (lintOn) { 59 | case "save": 60 | vscode.workspace.onDidSaveTextDocument((document) => { 61 | if (document.languageId === "quarto") { 62 | triggerLint(); 63 | } 64 | }); 65 | break; 66 | case "type": 67 | vscode.workspace.onDidChangeTextDocument((event) => { 68 | if (event.document.languageId === "quarto") { 69 | triggerLint(); 70 | } 71 | }); 72 | break; 73 | default: 74 | QW_LOG.appendLine(`Unsupported lint trigger: ${lintOn}`); 75 | } 76 | } 77 | 78 | /** 79 | * Lints the current Quarto document based on the user's configuration. 80 | * 81 | * @param context - The extension context provided by VS Code. 82 | */ 83 | export function lint(context: vscode.ExtensionContext) { 84 | const config = vscode.workspace.getConfiguration("quartoWizard.lint", null); 85 | const lintOn = config.get("trigger") || "never"; 86 | if (vscode.window.activeTextEditor?.document.languageId === "quarto" && lintOn !== "never") { 87 | activateExtensions([kMarkDownLintExtension], context); 88 | toggleLinting(); 89 | lintOnEvent(lintOn); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/utils/log.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { debounce } from "lodash"; 3 | import { QW_LOG } from "../constants"; 4 | 5 | /** 6 | * Returns a command string to show the logs. 7 | * 8 | * @returns {string} The command string to show the logs. 9 | */ 10 | export function showLogsCommand(): string { 11 | return "[Show logs](command:quartoWizard.showOutput)"; 12 | } 13 | 14 | /** 15 | * Logs a message to the Quarto Wizard log output if the message type 16 | * is at or below the configured log level. 17 | * 18 | * @param {string} message - The message to log. 19 | * @param {string} [type="info"] - The type of log message (e.g., "error", "warn", "info", "debug"). 20 | */ 21 | export function logMessage(message: string, type = "info"): void { 22 | const levels = ["error", "warn", "info", "debug"]; 23 | const config = vscode.workspace.getConfiguration("quartoWizard.log", null); 24 | const logLevel = config.get("level") ?? "info"; 25 | 26 | if (levels.indexOf(type) <= levels.indexOf(logLevel)) { 27 | QW_LOG.appendLine(message); 28 | } 29 | } 30 | 31 | /** 32 | * Debounced version of logMessage that limits how frequently messages are logged. 33 | * Waits 1000ms before logging the message to prevent excessive logging. 34 | * 35 | * @param {string} message - The message to log. 36 | * @param {string} [type="info"] - The type of log message. 37 | */ 38 | export const debouncedLogMessage = debounce(logMessage, 1000); 39 | -------------------------------------------------------------------------------- /src/utils/network.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { showLogsCommand, logMessage } from "./log"; 3 | 4 | /** 5 | * Checks if there is an active internet connection by attempting to fetch a URL. 6 | * 7 | * @param {string} [url="https://github.com/"] - The URL to check the internet connection against. 8 | * @returns {Promise} - A promise that resolves to true if the internet connection is active, otherwise false. 9 | */ 10 | export async function checkInternetConnection(url = "https://github.com/"): Promise { 11 | try { 12 | const response: Response = await fetch(url); 13 | if (response.ok) { 14 | return true; 15 | } else { 16 | const message = `No internet connection. Please check your network settings.`; 17 | logMessage(message, "error"); 18 | vscode.window.showErrorMessage(`${message} ${showLogsCommand()}.`); 19 | return false; 20 | } 21 | } catch { 22 | const message = `No internet connection. Please check your network settings.`; 23 | logMessage(message, "error"); 24 | vscode.window.showErrorMessage(`${message} ${showLogsCommand()}.`); 25 | return false; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/quarto.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { exec } from "child_process"; 3 | import * as path from "path"; 4 | import * as fs from "fs"; 5 | import { logMessage } from "./log"; 6 | import { findModifiedExtensions, getMtimeExtensions, removeExtension } from "./extensions"; 7 | 8 | let cachedQuartoPath: string | undefined; 9 | 10 | /** 11 | * Retrieves the Quarto path from the configuration. 12 | * 13 | * @returns {string} - The Quarto path. 14 | */ 15 | export function getQuartoPath(): string { 16 | if (cachedQuartoPath) { 17 | return cachedQuartoPath; 18 | } 19 | const config = vscode.workspace.getConfiguration("quartoWizard.quarto"); 20 | let quartoPath = config.get("path"); 21 | if (!quartoPath && quartoPath !== "") { 22 | const fallbackConfig = vscode.workspace.getConfiguration("quarto"); 23 | quartoPath = fallbackConfig.get("path"); 24 | } 25 | if (!quartoPath && quartoPath !== "") { 26 | quartoPath = "quarto"; 27 | } 28 | cachedQuartoPath = quartoPath || "quarto"; 29 | return cachedQuartoPath; 30 | } 31 | 32 | vscode.workspace.onDidChangeConfiguration(async (e) => { 33 | if (e.affectsConfiguration("quartoWizard.quarto.path") || e.affectsConfiguration("quarto.path")) { 34 | cachedQuartoPath = undefined; 35 | await checkQuartoPath(getQuartoPath()); 36 | } 37 | }); 38 | 39 | /** 40 | * Checks if the Quarto path is valid and updates the cached path if necessary. 41 | * 42 | * @param {string | undefined} quartoPath - The Quarto path to check. 43 | * @returns {Promise} - A promise that resolves to true if the Quarto path is valid, otherwise false. 44 | */ 45 | export async function checkQuartoPath(quartoPath: string | undefined): Promise { 46 | return new Promise((resolve) => { 47 | if (!quartoPath) { 48 | vscode.window.showErrorMessage("Quarto path is not set."); 49 | resolve(false); 50 | } else if (!checkQuartoVersion(quartoPath)) { 51 | vscode.window.showErrorMessage(`Quarto path '${quartoPath}' does not exist.`); 52 | resolve(false); 53 | } else { 54 | resolve(true); 55 | } 56 | }); 57 | } 58 | 59 | /** 60 | * Checks if the Quarto version is valid by executing the version command. 61 | * 62 | * @param {string | undefined} quartoPath - The Quarto path to check the version for. 63 | * @returns {Promise} - A promise that resolves to true if the Quarto version is valid, otherwise false. 64 | */ 65 | export async function checkQuartoVersion(quartoPath: string | undefined): Promise { 66 | return new Promise((resolve) => { 67 | exec(`${quartoPath} --version`, (error, stdout, stderr) => { 68 | if (error || stderr) { 69 | resolve(false); 70 | } else { 71 | resolve(stdout.trim().length > 0); 72 | } 73 | }); 74 | }); 75 | } 76 | 77 | /** 78 | * Installs a Quarto extension. 79 | * 80 | * @param {string} extension - The name of the extension to install. 81 | * @param {string} workspaceFolder - The workspace folder path. 82 | * @returns {Promise} - A promise that resolves to true if the extension is installed successfully, otherwise false. 83 | */ 84 | export async function installQuartoExtension(extension: string, workspaceFolder: string): Promise { 85 | logMessage(`Installing ${extension} ...`, "info"); 86 | return new Promise((resolve) => { 87 | if (!workspaceFolder) { 88 | return; 89 | } 90 | const quartoPath = getQuartoPath(); 91 | checkQuartoPath(quartoPath); 92 | const command = `${quartoPath} add ${extension} --no-prompt`; 93 | 94 | exec(command, { cwd: workspaceFolder }, (error, stdout, stderr) => { 95 | let isInstalled = false; 96 | if (error) { 97 | logMessage(`Error installing extension: ${error}`, "error"); 98 | if (stderr) { 99 | logMessage(`${stderr}`, "error"); 100 | } 101 | } else if (stderr) { 102 | isInstalled = stderr.includes("Extension installation complete"); 103 | if (!isInstalled) { 104 | logMessage(`${stderr}`, "error"); 105 | } 106 | } else { 107 | isInstalled = true; 108 | } 109 | 110 | if (isInstalled) { 111 | vscode.commands.executeCommand("quartoWizard.extensionsInstalled.refresh"); 112 | resolve(true); 113 | } else { 114 | resolve(false); 115 | } 116 | }); 117 | }); 118 | } 119 | 120 | // Update _extension.yml file with source, i.e., GitHub username/repo 121 | // This is needed for the extension to be updated in the future 122 | // To be removed when Quarto supports source records in the _extension.yml file or elsewhere 123 | // See https://github.com/quarto-dev/quarto-cli/issues/11468 124 | /** 125 | * Updates the _extension.yml file with the source information for the installed extension. 126 | * 127 | * @param {string} extension - The name of the extension to install. 128 | * @param {string} workspaceFolder - The workspace folder path. 129 | * @returns {Promise} - A promise that resolves to true if the extension source is updated successfully, otherwise false. 130 | */ 131 | export async function installQuartoExtensionSource(extension: string, workspaceFolder: string): Promise { 132 | const extensionsDirectory = path.join(workspaceFolder, "_extensions"); 133 | const existingExtensions = getMtimeExtensions(extensionsDirectory); 134 | 135 | const success = await installQuartoExtension(extension, workspaceFolder); 136 | 137 | const newExtension = findModifiedExtensions(existingExtensions, extensionsDirectory); 138 | const fileNames = ["_extension.yml", "_extension.yaml"]; 139 | const filePath = fileNames 140 | .map((name) => path.join(extensionsDirectory, ...newExtension, name)) 141 | .find((fullPath) => fs.existsSync(fullPath)); 142 | if (filePath) { 143 | const fileContent = fs.readFileSync(filePath, "utf-8"); 144 | const updatedContent = fileContent.includes("source: ") 145 | ? fileContent.replace(/source: .*/, `source: ${extension}`) 146 | : `${fileContent.trim()}\nsource: ${extension}`; 147 | fs.writeFileSync(filePath, updatedContent); 148 | } 149 | vscode.commands.executeCommand("quartoWizard.extensionsInstalled.refresh"); 150 | return success; 151 | } 152 | 153 | /** 154 | * Removes a Quarto extension. 155 | * 156 | * @param {string} extension - The name of the extension to remove. 157 | * @param {string} workspaceFolder - The workspace folder path. 158 | * @returns {Promise} - A promise that resolves to true if the extension is removed successfully, otherwise false. 159 | */ 160 | export async function removeQuartoExtension(extension: string, workspaceFolder: string): Promise { 161 | logMessage(`Removing ${extension} ...`, "info"); 162 | if (!workspaceFolder) { 163 | return false; 164 | } 165 | const status = await removeExtension(extension, path.join(workspaceFolder, "_extensions")); 166 | return status; 167 | } 168 | /** Quarto command to remove an extension. 169 | * 170 | * @param extension - The name of the extension to remove. 171 | * @param root - The root directory where the extension is located. 172 | * @returns {boolean} - Status (true for success, false for failure). 173 | * 174 | * @deprecated Use removeExtension from extensions.ts instead. 175 | */ 176 | // export async function removeExtension(extension: string, root: string): Promise { 177 | // return new Promise((resolve) => { 178 | // const quartoPath = getQuartoPath(); 179 | // checkQuartoPath(quartoPath); 180 | // const command = `${quartoPath} remove ${extension} --no-prompt`; 181 | // exec(command, { cwd: root }, (error, stdout, stderr) => { 182 | // if (stderr) { 183 | // logMessage(`${stderr}`, "error"); 184 | // const isRemoved = stderr.includes("Extension removed"); 185 | // if (isRemoved) { 186 | // resolve(true); 187 | // } else { 188 | // resolve(false); 189 | // return; 190 | // } 191 | // } 192 | // vscode.commands.executeCommand("quartoWizard.extensionsInstalled.refresh"); 193 | // resolve(true); 194 | // }); 195 | // }); 196 | // } 197 | -------------------------------------------------------------------------------- /src/utils/reprex.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import { showLogsCommand, logMessage } from "./log"; 5 | 6 | /** 7 | * Creates a new Quarto reprex (REPRoducible EXample) file based on the specified language. 8 | * 9 | * @param {string} language - The programming language for the reprex (e.g., "R", "Julia", "Python"). 10 | * @param {vscode.ExtensionContext} context - The extension context. 11 | */ 12 | export async function newQuartoReprex(language: string, context: vscode.ExtensionContext) { 13 | let templateFile = ""; 14 | 15 | switch (language) { 16 | case "R": 17 | templateFile = "r.qmd"; 18 | break; 19 | case "Julia": 20 | templateFile = "julia.qmd"; 21 | break; 22 | case "Python": 23 | templateFile = "python.qmd"; 24 | break; 25 | default: { 26 | const message = `Unsupported language: ${language}.`; 27 | logMessage(message, "error"); 28 | vscode.window.showErrorMessage(`${message} ${showLogsCommand()}.`); 29 | return; 30 | } 31 | } 32 | 33 | const filePath = path.join(context.extensionPath, "assets", "templates", templateFile); 34 | fs.readFile(filePath, "utf8", (err, data) => { 35 | if (err) { 36 | const message = `Failed to read the template file: ${err.message}.`; 37 | logMessage(message, "error"); 38 | vscode.window.showErrorMessage(`${message} ${showLogsCommand()}.`); 39 | return; 40 | } 41 | 42 | vscode.workspace.openTextDocument({ content: data, language: "quarto" }).then((document) => { 43 | vscode.window.showTextDocument(document); 44 | }); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /src/utils/workspace.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | /** 4 | * Prompts the user to select a workspace folder if multiple workspace folders are detected. 5 | * 6 | * @returns {Promise} - The selected workspace folder path or undefined if no selection is made. 7 | */ 8 | export async function selectWorkspaceFolder(): Promise { 9 | const workspaceFolders = vscode.workspace.workspaceFolders; 10 | if (!workspaceFolders || workspaceFolders.length === 0) { 11 | return undefined; 12 | } 13 | 14 | if (workspaceFolders.length === 1) { 15 | return workspaceFolders[0].uri.fsPath; 16 | } 17 | 18 | const options: vscode.WorkspaceFolderPickOptions = { 19 | placeHolder: "Select a workspace folder", 20 | ignoreFocusOut: true, 21 | }; 22 | 23 | const selectedFolder = await vscode.window.showWorkspaceFolderPick(options); 24 | 25 | return selectedFolder?.uri.fsPath; 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "out", 4 | "rootDir": "src", 5 | "module": "NodeNext", // Overwritten by webpack.config.js 6 | "target": "ES2024", 7 | "lib": ["dom", "ES2024"], 8 | "sourceMap": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noImplicitAny": true 12 | }, 13 | "exclude": ["node_modules", ".vscode-test"] 14 | } 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* --------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * --------------------------------------------------------------------------------------------*/ 5 | 6 | //@ts-check 7 | 8 | 'use strict'; 9 | 10 | const path = require('path'); 11 | 12 | /**@type {import('webpack').Configuration}*/ 13 | const config = { 14 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 15 | 16 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 17 | output: { // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 18 | path: path.resolve(__dirname, 'dist'), 19 | filename: 'extension.js', 20 | libraryTarget: "commonjs2", 21 | devtoolModuleFilenameTemplate: "../[resource-path]", 22 | }, 23 | devtool: 'source-map', 24 | externals: { 25 | vscode: "commonjs vscode" // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 26 | }, 27 | resolve: { // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 28 | extensions: ['.ts', '.js'] 29 | }, 30 | module: { 31 | rules: [{ 32 | test: /\.ts$/, 33 | exclude: /node_modules/, 34 | use: [{ 35 | loader: 'ts-loader', 36 | options: { 37 | compilerOptions: { 38 | "module": "ES2024", 39 | "moduleResolution": "bundler", 40 | } 41 | } 42 | }] 43 | }] 44 | }, 45 | } 46 | 47 | module.exports = config; 48 | --------------------------------------------------------------------------------