├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request_issue_template.md │ ├── issue_template.md │ ├── puppet_lint_issue_template.md │ └── syntax_color_issue_template.md ├── pull_request_template.md └── workflows │ ├── mend.yml │ ├── nightly.yml │ ├── release.yml │ ├── release_prep.yml │ └── vscode-ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── README_BUILD.md ├── assets ├── css │ └── main.css ├── icons │ ├── dark │ │ └── sync.svg │ └── light │ │ └── sync.svg └── js │ └── main.js ├── build.ps1 ├── images ├── Puppet-Logo-Amber-sm.png ├── puppet-dag-dark.svg ├── puppet-dag-light.svg ├── puppet-vscode-sm.png ├── puppet_logo_sm.svg └── syntax.png ├── languages ├── puppet-language-configuration.json └── puppetfile-language-configuration.json ├── package-lock.json ├── package.json ├── psakefile.ps1 ├── snippets ├── keywords.snippets.json ├── metadata.snippets.json └── puppetfile.snippets.json ├── src ├── configuration.ts ├── configuration │ ├── pathResolver.ts │ └── pdkResolver.ts ├── extension.ts ├── feature.ts ├── feature │ ├── DebuggingFeature.ts │ ├── FormatDocumentFeature.ts │ ├── PDKFeature.ts │ ├── PuppetModuleHoverFeature.ts │ ├── PuppetNodeGraphFeature.ts │ ├── PuppetResourceFeature.ts │ ├── PuppetStatusBarFeature.ts │ ├── PuppetfileCompletionFeature.ts │ ├── PuppetfileHoverFeature.ts │ └── UpdateConfigurationFeature.ts ├── forge.ts ├── handler.ts ├── handlers │ ├── stdio.ts │ └── tcp.ts ├── helpers │ └── commandHelper.ts ├── interfaces.ts ├── logging.ts ├── logging │ ├── file.ts │ ├── null.ts │ ├── outputchannel.ts │ └── stdout.ts ├── messages.ts ├── settings.ts ├── telemetry.ts ├── test │ ├── runtest.ts │ └── suite │ │ ├── configuration.test.ts │ │ ├── extension.test.ts │ │ ├── feature │ │ ├── DebuggingFeature.test.ts │ │ ├── FormatDocument.test.ts │ │ ├── PDKFeature.test.ts │ │ ├── PuppetModuleHoverFeature.test.ts │ │ ├── PuppetNodeGraphFeature.test.ts │ │ ├── PuppetResourceFeature.test.ts │ │ ├── PuppetStatusBarFeature.test.ts │ │ ├── PuppetfileCompletionFeature.test.ts │ │ ├── PuppetfileHoverFeature.test.ts │ │ └── UpdateConfigurationFeature.test.ts │ │ ├── forge.test.ts │ │ ├── handler.test.ts │ │ ├── index.ts │ │ ├── links.test.ts │ │ ├── paths.test.ts │ │ ├── pdkResolver.test.ts │ │ ├── settings.test.ts │ │ └── telemetry.test.ts └── views │ ├── facts.ts │ └── puppetfile.ts ├── syntaxes ├── .gitignore └── puppetfile.cson.json ├── tools ├── RELEASE.md ├── SyncProjects.ps1 └── release.ps1 └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | assets 2 | node_modules 3 | out 4 | src/test 5 | vendor 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | /* 2 | 👋 Hi! This file was autogenerated by tslint-to-eslint-config. 3 | https://github.com/typescript-eslint/tslint-to-eslint-config 4 | 5 | It represents the closest reasonable ESLint configuration to this 6 | project's original TSLint configuration. 7 | 8 | We recommend eventually switching this configuration to extend from 9 | the recommended rulesets in typescript-eslint. 10 | https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md 11 | 12 | Happy linting! 💖 13 | */ 14 | { 15 | "env": { "browser": true, "es6": true, "node": true }, 16 | "parser": "@typescript-eslint/parser", 17 | "parserOptions": { "sourceType": "module" }, 18 | "plugins": ["@typescript-eslint", "prettier"], 19 | "extends": [ 20 | "eslint:recommended", 21 | "plugin:@typescript-eslint/eslint-recommended", 22 | "plugin:@typescript-eslint/recommended", 23 | "plugin:prettier/recommended", 24 | "prettier" 25 | ], 26 | "rules": { 27 | "no-return-await": "error", 28 | "block-scoped-var": "error", 29 | "default-case": "error", 30 | "default-case-last": "error", 31 | "default-param-last": "error", 32 | "guard-for-in": "error", 33 | "no-extend-native": "error", 34 | "no-invalid-this": "error", 35 | "no-useless-call": "error", 36 | "no-void": "error", 37 | "wrap-iife": "error", 38 | "no-console": "error", 39 | "no-await-in-loop": "error", 40 | "no-template-curly-in-string": "error", 41 | "require-atomic-updates": "error", 42 | "no-useless-backreference": "error", 43 | "@typescript-eslint/no-unused-vars": "off", 44 | "@typescript-eslint/no-explicit-any": "off", 45 | "@typescript-eslint/naming-convention": "warn", 46 | "@typescript-eslint/member-delimiter-style": [ 47 | "warn", 48 | { 49 | "multiline": { "delimiter": "semi", "requireLast": true }, 50 | "singleline": { "delimiter": "semi", "requireLast": false } 51 | } 52 | ], 53 | "@typescript-eslint/no-unused-expressions": "warn", 54 | "@typescript-eslint/semi": ["warn", "always"], 55 | "@typescript-eslint/interface-name-prefix": "off", 56 | "curly": "warn", 57 | "eqeqeq": ["warn", "always"], 58 | "no-redeclare": "warn", 59 | "no-throw-literal": "warn", 60 | "prettier/prettier": 0 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request_issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request or new idea ✨ 3 | about: Suggest a new feature or something you would like see 4 | 5 | --- 6 | 7 | **Summary of the new feature** 8 | 9 | A clear and concise description of what the problem is that the new feature would solve. 10 | 11 | For example: 12 | 13 | --- 14 | **As a** user 15 | 16 | **I would like to** automatically format my puppet manifests 17 | 18 | **so that I** don't have to remember to do it myself. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 🐞 3 | about: Report an error or unexpected behavior 4 | 5 | --- 6 | 15 | ### What Versions are you running? 16 | 17 | OS Version: 18 | VSCode Version: 19 | Puppet Extension Version: 20 | PDK Version: 21 | 22 | 23 | 24 | ### What You Are Seeing? 25 | 26 | ### What is Expected? 27 | 28 | ### How Did You Get This To Happen? (Steps to Reproduce) 29 | 30 | 31 | 32 | ### Output Log 33 | 34 | 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/puppet_lint_issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Puppet lint bug report ⛔️ 3 | about: Puppet lint issues are tracked in a separate GitHub repo. 4 | 5 | --- 6 | 7 | * Please submit puppet lint issues to the [Puppet Lint](https://github.com/puppetlabs/puppet-lint) repo on GitHub. 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/syntax_color_issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Syntax Colorization bug report 🌈 3 | about: Puppet syntax colorization issues are tracked in a separate GitHub repo. 4 | 5 | --- 6 | 7 | * Please submit editor syntax colorization issues with Puppet files to the [Puppet Editor Syntax](https://github.com/lingua-pupuli/puppet-editor-syntax) repo on GitHub. 8 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | Provide a detailed description of all the changes present in this pull request. 3 | 4 | ## Additional Context 5 | Add any additional context about the problem here. 6 | - [ ] Root cause and the steps to reproduce. (If applicable) 7 | - [ ] Thought process behind the implementation. 8 | 9 | ## Related Issues (if any) 10 | Mention any related issues or pull requests. 11 | 12 | ## Checklist 13 | - [ ] 🟢 Spec tests. 14 | - [ ] 🟢 Acceptance tests. 15 | - [ ] Manually verified. 16 | -------------------------------------------------------------------------------- /.github/workflows/mend.yml: -------------------------------------------------------------------------------- 1 | name: mend 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | pull_request: 7 | branches: 8 | - "main" 9 | workflow_dispatch: 10 | 11 | jobs: 12 | mend: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: 'setup node: 18 platform: ${{ runner.os }}' 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 18 21 | 22 | - name: npm install 23 | run: | 24 | npm install -g vsce --silent; 25 | npm install --silent; 26 | 27 | - uses: "actions/setup-java@v4" 28 | with: 29 | distribution: "temurin" 30 | java-version: "17" 31 | 32 | - name: "download" 33 | run: curl -o wss-unified-agent.jar https://unified-agent.s3.amazonaws.com/wss-unified-agent.jar 34 | 35 | - name: "scan" 36 | run: java -jar wss-unified-agent.jar 37 | env: 38 | WS_APIKEY: ${{ secrets.MEND_API_KEY }} 39 | WS_WSS_URL: https://saas-eu.whitesourcesoftware.com/agent 40 | WS_USERKEY: ${{ secrets.MEND_TOKEN }} 41 | WS_PRODUCTNAME: "DevX" 42 | WS_PROJECTNAME: ${{ github.event.repository.name }} 43 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: nightly 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | node-version: [18] 14 | os: [ubuntu-latest, windows-latest, macos-latest] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: 'setup node: ${{ matrix.node-version }} platform: ${{ matrix.os }}' 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - name: setup linux 24 | if: runner.os == 'Linux' 25 | run: | 26 | /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & echo ">>> Started xvfb"; 27 | wget https://apt.puppetlabs.com/puppet8-release-jammy.deb; 28 | sudo dpkg -i puppet8-release-jammy.deb; 29 | sudo apt-get update -y; 30 | sudo apt-get install pdk -y; 31 | pdk --version; 32 | 33 | - name: setup macos 34 | if: runner.os == 'macOS' 35 | run: | 36 | export HOMEBREW_NO_AUTO_UPDATE=1; 37 | brew install --cask puppetlabs/puppet/pdk; 38 | /opt/puppetlabs/pdk/bin/pdk --version; 39 | 40 | - name: setup windows 41 | if: runner.os == 'Windows' 42 | run: | 43 | choco install pdk -y 44 | 45 | - name: Install psake 46 | shell: pwsh 47 | run: Install-Module psake -Force 48 | 49 | - name: npm install 50 | shell: pwsh 51 | run: | 52 | npm install -g vsce --silent; 53 | npm install --silent; 54 | 55 | - name: Run ESLint 56 | run: npm run lint 57 | 58 | - name: npm build 59 | shell: pwsh 60 | run: | 61 | invoke-psake -taskList 'build' 62 | 63 | - name: npm test 64 | env: 65 | BUILD_VERSION: '0.99.${{ github.event.number }}' 66 | VSCODE_BUILD_VERBOSE: true 67 | DISPLAY: ':99.0' 68 | shell: pwsh 69 | run: | 70 | npm test 71 | 72 | - name: vsce package 73 | if: runner.os == 'Linux' 74 | env: 75 | BUILD_VERSION: '0.99.${{ github.event.number }}' 76 | shell: pwsh 77 | run: | 78 | invoke-psake -properties @{ packageVersion = $env:BUILD_VERSION } -tasklist bump 79 | mkdir artifacts 80 | vsce package --out artifacts/puppet-vscode-$env:BUILD_VERSION.vsix 81 | 82 | - name: upload vsix 83 | if: runner.os == 'Linux' 84 | uses: actions/upload-artifact@master 85 | with: 86 | name: 'puppet-vscode' 87 | path: artifacts 88 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | NODE_VERSION: '18.x' 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Node ${{ env.NODE_VERSION }} 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: ${{ env.NODE_VERSION }} 18 | 19 | - name: Install psake 20 | shell: pwsh 21 | run: Install-Module psake -Force 22 | 23 | - name: Package vsix 24 | id: create_package 25 | shell: pwsh 26 | run: | 27 | npm install -g vsce --silent; 28 | npm install -g typescript --silent; 29 | npm install --silent; 30 | vsce package 31 | 32 | - name: Set vsix version 33 | id: vsce 34 | run: | 35 | echo "version=$(cat package.json | jq -r .version)" >> $GITHUB_OUTPUT 36 | 37 | - name: "Generate release notes" 38 | run: | 39 | export GH_HOST=github.com 40 | gh extension install chelnak/gh-changelog 41 | gh changelog get --latest > OUTPUT.md 42 | env: 43 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | - name: Create Github release 46 | run: | 47 | gh release create v${{ steps.vsce.outputs.version }} ./puppet-vscode-${{ steps.vsce.outputs.version }}.vsix --title v${{ steps.vsce.outputs.version }} -F OUTPUT.md 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | 51 | - name: Publish Extension 52 | id: publish-release-asset 53 | shell: pwsh 54 | run: | 55 | vsce publish -p ${{ secrets.VSCE_TOKEN }} --packagePath ./puppet-vscode-${{ steps.vsce.outputs.version }}.vsix 56 | 57 | - name: Publish Extension to OpenVSX 58 | id: publish-release-asset-openvsx 59 | shell: pwsh 60 | run: | 61 | npx ovsx publish ./puppet-vscode-${{ steps.vsce.outputs.version }}.vsix -p ${{ secrets.OPENVSX_TOKEN }} 62 | -------------------------------------------------------------------------------- /.github/workflows/release_prep.yml: -------------------------------------------------------------------------------- 1 | name: release_prep 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Version to released.' 8 | required: true 9 | default: '0.0.0' 10 | language-server-version: 11 | description: 'Version of language server for release to consume. In the format v0.0.0' 12 | required: false 13 | 14 | jobs: 15 | release_prep: 16 | name: "Release Prep" 17 | runs-on: "ubuntu-latest" 18 | env: 19 | NODE_VERSION: "18" 20 | 21 | steps: 22 | - name: "Checkout" 23 | uses: "actions/checkout@v4" 24 | with: 25 | clean: true 26 | fetch-depth: 0 27 | 28 | - name: Node ${{ env.NODE_VERSION }} 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: ${{ env.NODE_VERSION }} 32 | 33 | - name: "Update Version" 34 | run: | 35 | cat <<< $(jq '.version="${{ github.event.inputs.version }}"' package.json) > package.json 36 | 37 | - name: "Update Language Server Version" 38 | if: "${{ github.event.inputs.language-server-version != '' }}" 39 | run: | 40 | cat <<< $(jq '.editorComponents.editorServices.release="${{ github.event.inputs.language-server-version}}"' package.json) > package.json 41 | 42 | - name: "Generate package-lock.json" 43 | run: | 44 | npm install -g vsce --silent; 45 | npm install -g typescript --silent; 46 | npm install --silent; 47 | vsce package 48 | 49 | - name: "Generate changelog" 50 | run: | 51 | export GH_HOST=github.com 52 | gh extension install chelnak/gh-changelog 53 | gh changelog new --next-version v${{ github.event.inputs.version }} 54 | env: 55 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | 57 | - name: "Commit changes" 58 | run: | 59 | git config --local user.email "${{ github.repository_owner }}@users.noreply.github.com" 60 | git config --local user.name "GitHub Actions" 61 | git add . 62 | git commit -m "Release prep v${{ github.event.inputs.version }}" 63 | 64 | - name: "Create pull request" 65 | uses: "peter-evans/create-pull-request@v5" 66 | with: 67 | token: ${{ secrets.GITHUB_TOKEN }} 68 | commit-message: "Release prep v${{ github.event.inputs.version }}" 69 | branch: "release-prep" 70 | delete-branch: true 71 | title: "Release prep v${{ github.event.inputs.version }}" 72 | base: "main" 73 | body: | 74 | Automated release-prep from commit ${{ github.sha }}. 75 | labels: "maintenance" 76 | -------------------------------------------------------------------------------- /.github/workflows/vscode-ci.yml: -------------------------------------------------------------------------------- 1 | name: vscode-ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | branches: 9 | - "main" 10 | workflow_dispatch: 11 | 12 | env: 13 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 14 | 15 | jobs: 16 | build: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | matrix: 20 | node-version: [18] 21 | os: [ubuntu-latest, windows-latest, macos-latest] 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: 'setup node: ${{ matrix.node-version }} platform: ${{ matrix.os }}' 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | 30 | - name: setup linux 31 | if: runner.os == 'Linux' 32 | run: | 33 | /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & echo ">>> Started xvfb"; 34 | wget https://apt.puppetlabs.com/puppet8-release-jammy.deb; 35 | sudo dpkg -i puppet8-release-jammy.deb; 36 | sudo apt-get update -y; 37 | sudo apt-get install pdk -y; 38 | pdk --version; 39 | 40 | - name: setup macos 41 | if: runner.os == 'macOS' 42 | run: | 43 | export HOMEBREW_NO_AUTO_UPDATE=1; 44 | brew install --cask puppetlabs/puppet/pdk; 45 | /opt/puppetlabs/pdk/bin/pdk --version; 46 | 47 | - name: setup windows 48 | if: runner.os == 'Windows' 49 | run: | 50 | choco install pdk -y 51 | 52 | - name: Install psake 53 | shell: pwsh 54 | run: Install-Module -Name psake -RequiredVersion 4.9.0 -Force 55 | 56 | - name: npm install 57 | shell: pwsh 58 | run: | 59 | npm install -g vsce --silent; 60 | npm install --silent; 61 | 62 | - name: Run ESLint 63 | run: npm run lint 64 | 65 | - name: npm build 66 | shell: pwsh 67 | run: | 68 | invoke-psake -taskList 'build' 69 | 70 | - name: npm test 71 | env: 72 | BUILD_VERSION: '0.99.${{ github.event.number }}' 73 | VSCODE_BUILD_VERBOSE: true 74 | DISPLAY: ':99.0' 75 | run: | 76 | npm run test:coverage 77 | 78 | - name: Upload Coverage 79 | if: | 80 | runner.os == 'Linux' && 81 | env.CODECOV_TOKEN != '' 82 | uses: codecov/codecov-action@v4 83 | with: 84 | token: ${{ secrets.CODECOV_TOKEN }} 85 | files: ./coverage.xml 86 | fail_ci_if_error: true 87 | verbose: true 88 | 89 | - name: vsce package 90 | if: runner.os == 'Linux' 91 | env: 92 | BUILD_VERSION: '0.99.${{ github.event.number }}' 93 | shell: pwsh 94 | run: | 95 | invoke-psake -properties @{ packageVersion = $env:BUILD_VERSION } -tasklist bump 96 | mkdir artifacts 97 | vsce package --out artifacts/puppet-vscode-$env:BUILD_VERSION.vsix 98 | 99 | - name: upload vsix 100 | if: runner.os == 'Linux' 101 | uses: actions/upload-artifact@master 102 | with: 103 | name: 'puppet-vscode' 104 | path: artifacts 105 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | node_modules/ 3 | vendor/ 4 | tmp/ 5 | 6 | // Ignore generated module output 7 | *.vsix 8 | .vscode-test 9 | 10 | // Ignore temporary vendoring files 11 | editor_services.zip 12 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | languages 2 | snippets 3 | syntaxes 4 | out 5 | vendor 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "tabWidth": 2, 4 | "endOfLine": "lf", 5 | "printWidth": 120, 6 | "useTabs": false, 7 | "trailingComma": "all", 8 | "semi": true, 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "eamodio.gitlens", 5 | "esbenp.prettier-vscode", 6 | "wwm.better-align", 7 | "rebornix.Ruby" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 14 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 15 | "preLaunchTask": "npm: watch" 16 | }, 17 | { 18 | "name": "Extension Tests", 19 | "type": "extensionHost", 20 | "request": "launch", 21 | "runtimeExecutable": "${execPath}", 22 | "args": [ 23 | "--extensionDevelopmentPath=${workspaceFolder}", 24 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 25 | ], 26 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 27 | "preLaunchTask": "npm: watch" 28 | }, 29 | { 30 | "type": "node", 31 | "request": "launch", 32 | "name": "DebugAdapter", 33 | "cwd": "${workspaceFolder}", 34 | "program": "${workspaceFolder}/src/debugAdapter.ts", 35 | "args": ["--server=4711"], 36 | "outFiles": ["${workspaceFolder}/out/**/*.js"] 37 | }, 38 | { 39 | "type": "node", 40 | "request": "launch", 41 | "name": "Debug Gulp task", 42 | "program": "${workspaceFolder}/node_modules/gulp/bin/gulp.js", 43 | "args": [ 44 | // Add arguments here to debug gulp tasks 45 | // "bump", "--type", "minor" 46 | // "bump", "--specific", "99.99.0-appv.127" 47 | ] 48 | } 49 | ], 50 | "compounds": [ 51 | { 52 | "name": "Extension + DebugAdapter", 53 | "configurations": ["Extension", "DebugAdapter"] 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.formatOnType": true, 4 | "editor.insertSpaces": true, 5 | "editor.tabSize": 2, 6 | "editor.trimAutoWhitespace": true, 7 | "files.encoding": "utf8", 8 | "files.eol": "\n", 9 | "files.insertFinalNewline": true, 10 | "files.trimFinalNewlines": true, 11 | "files.trimTrailingWhitespace": true, 12 | "files.exclude": { 13 | "node_modules": true, 14 | "out": true 15 | }, 16 | "search.exclude": { 17 | "node_modules": true, 18 | "out": true 19 | }, 20 | "[javascript]": { 21 | "editor.defaultFormatter": "esbenp.prettier-vscode" 22 | }, 23 | "[json]": { 24 | "editor.defaultFormatter": "esbenp.prettier-vscode", 25 | "editor.quickSuggestions": { 26 | "strings": true 27 | }, 28 | "editor.suggest.insertMode": "replace" 29 | }, 30 | "[jsonc]": { 31 | "editor.defaultFormatter": "esbenp.prettier-vscode", 32 | "editor.quickSuggestions": { 33 | "strings": true 34 | }, 35 | "editor.suggest.insertMode": "replace" 36 | }, 37 | "typescript.tsc.autoDetect": "off", 38 | "typescript.format.enable": false, 39 | "[typescript]": { 40 | "editor.codeActionsOnSave": { 41 | "source.fixAll": true, 42 | "source.organizeImports": true 43 | }, 44 | "editor.defaultFormatter": "esbenp.prettier-vscode" 45 | }, 46 | "eslint.enable": true, 47 | "eslint.lintTask.enable": true, 48 | "eslint.alwaysShowStatus": true, 49 | "eslint.codeActionsOnSave.mode": "all", 50 | "eslint.run": "onType", 51 | "puppet.trace.server": { 52 | "verbosity": "verbose", 53 | "format": "json" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.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 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | test/** 5 | src/** 6 | **/*.map 7 | .gitignore 8 | tsconfig.json 9 | tmp/** 10 | logs/** 11 | sessions/** 12 | gulpfile.js 13 | docs 14 | .github 15 | .travis.yml 16 | appveyor.yml 17 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Setting ownership to the tooling team 2 | * @puppetlabs/devx 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at james.pogran@puppet.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Checklist (and a short version for the impatient) 2 | ================================================= 3 | 4 | * Commits: 5 | 6 | - Make commits of logical units. 7 | 8 | - Check for unnecessary whitespace with "git diff --check" before 9 | committing. 10 | 11 | - Commit using Unix line endings (check the settings around "crlf" in 12 | git-config(1)). 13 | 14 | - Do not check in commented out code or unneeded files. 15 | 16 | - The first line of the commit message should be a short 17 | description (50 characters is the soft limit, excluding ticket 18 | number(s)), and should skip the full stop. 19 | 20 | - Associate the issue in the message. The first line should include 21 | the issue number in the form "(GH-#XXXX) Rest of message". 22 | 23 | - The body should provide a meaningful commit message, which: 24 | 25 | - uses the imperative, present tense: "change", not "changed" or 26 | "changes". 27 | 28 | - includes motivation for the change, and contrasts its 29 | implementation with the previous behavior. 30 | 31 | - Make sure that you have tests for the bug you are fixing, or 32 | feature you are adding. 33 | 34 | - Make sure the test suites passes after your commit: 35 | `bundle exec rspec spec/acceptance` More information on [testing](#Testing) below 36 | 37 | - When introducing a new feature, make sure it is properly 38 | documented in the README.md 39 | 40 | * Submission: 41 | 42 | * Pre-requisites: 43 | 44 | - Make sure you have a [GitHub account](https://github.com/join) 45 | 46 | * Preferred method: 47 | 48 | - Fork the repository on GitHub. 49 | 50 | - Push your changes to a topic branch in your fork of the 51 | repository. (the format GH-1234-short_description_of_change is 52 | usually preferred for this project). 53 | 54 | - Submit a pull request to the repository. 55 | -------------------------------------------------------------------------------- /README_BUILD.md: -------------------------------------------------------------------------------- 1 | # Puppet Language Support for Visual Studio Code 2 | 3 | This extension provides Puppet Language support for [Visual Studio Code](https://code.visualstudio.com/) 4 | 5 | ## How to run the client and server for development 6 | 7 | ### Setup the client 8 | 9 | * Ensure nodejs is installed 10 | 11 | * Clone this repository 12 | 13 | ```bash 14 | > git clone https://github.com/puppetlabs/puppet-vscode.git 15 | 16 | > cd puppet-vscode 17 | ``` 18 | 19 | * Install the node modules 20 | 21 | ```bash 22 | > npm install 23 | ... 24 | 25 | > puppet-vscode@0.0.3 postinstall C:\Source\puppet-vscode\client 26 | > node ./node_modules/vscode/bin/install 27 | ... 28 | Detected VS Code engine version: ^1.10.0 29 | Found minimal version that qualifies engine range: 1.10.0 30 | Fetching vscode.d.ts from: https://raw.githubusercontent.com/Microsoft/vscode/1.10.0/src/vs/vscode.d.ts 31 | vscode.d.ts successfully installed! 32 | ``` 33 | 34 | ### Vendoring other resources 35 | 36 | The following resources are vendored into the extension; 37 | 38 | * Puppet Editor Services (`editor-services`) 39 | 40 | * Puppet Editor Syntax (`editorSyntax`) 41 | 42 | By default the extension will use the specified versions in the `package.json` file when vendoring resources. 43 | 44 | #### Example configuration 45 | 46 | The following examples use `editorServices`, however the configuration settings can be used on any resource. 47 | 48 | ##### By release tag 49 | 50 | To use version `0.10.0` of the Editor Services; 51 | 52 | ``` json 53 | { 54 | "editorComponents":{ 55 | "editorServices": { 56 | "release": "0.10.0" 57 | } 58 | } 59 | } 60 | ``` 61 | 62 | ##### Specific github repository or branch 63 | 64 | To use a specific GitHub repository that contains the Puppet Editor services, use the `githubref` configuration setting; for example to use the `puppet-editing` repository, owned by `Alice` with the `testing` branch 65 | 66 | ``` json 67 | { 68 | "editorComponents":{ 69 | "editorServices": { 70 | "githubuser": "Alice", 71 | "githubrepo": "puppet-editing", 72 | "githubref": "testing" 73 | } 74 | } 75 | } 76 | ``` 77 | 78 | Note - For `editorServices` the default the githubuser is `lingua-pupuli` and the githubrepo is `puppet-editor-services` 79 | 80 | Note - For `editorSyntax` the default the githubuser is `lingua-pupuli` and the githubrepo is `puppet-editor-syntax` 81 | 82 | Note - Use the full length commit SHA for `githubref`, not the abbreviated eight character SHA 83 | 84 | ##### Using a local directory 85 | 86 | To use a local directory that contains the Puppet Editor services, use the `directory` configuration setting; for example if the the editor services was located in `C:\puppet-editor-services` use the following; 87 | 88 | ``` json 89 | { 90 | "editor-services": { 91 | "directory": "C:\\puppet-editor-services" 92 | } 93 | } 94 | ``` 95 | 96 | Note - Backslashes in the path must be escaped. 97 | 98 | ### Vendoring the resources into the extension 99 | 100 | * Use psake to vendor the resources 101 | 102 | ```powershell 103 | > ./build.ps1 -task clean,vendor 104 | psake version 4.8.0 105 | Copyright (c) 2010-2018 James Kovacs & Contributors 106 | 107 | Executing clean 108 | Executing VendorEditorServices 109 | Executing VendorEditorSyntax 110 | 111 | psake succeeded executing C:\Users\james\src\lingua\client\psakefile.ps1 112 | 113 | ---------------------------------------------------------------------- 114 | Build Time Report 115 | ---------------------------------------------------------------------- 116 | Name Duration 117 | ---- -------- 118 | Clean 00:00:00.075 119 | VendorEditorServices 00:00:01.601 120 | VendorEditorSyntax 00:00:00.338 121 | Vendor 00:00:00.000 122 | Total: 00:00:02.023 123 | ``` 124 | 125 | * Start VS Code 126 | 127 | ```bash 128 | > code . 129 | ``` 130 | 131 | * Once VS Code is running, press `F5` to start a build and a new VS Code development instance should start 132 | 133 | * Open a Puppet file (.pp) and the client will start and connect to the Puppet Server 134 | 135 | > Other Puppet VS Code extensions may cause issues with the development instance. Ensure that you either uninstall or disable the other Puppet extensions prior. 136 | 137 | ## Issues 138 | 139 | Please raise issues for the Language Server or Extension using the GitHub [issue tracker](https://github.com/puppetlabs/puppet-vscode/issues/new). 140 | -------------------------------------------------------------------------------- /assets/css/main.css: -------------------------------------------------------------------------------- 1 | #cy { 2 | width: 100%; 3 | height: 100%; 4 | position: absolute; 5 | top: 0px; 6 | left: 0px; 7 | } 8 | 9 | 10 | body{ 11 | overflow-x: hidden; 12 | height: 100%; 13 | } 14 | -------------------------------------------------------------------------------- /assets/icons/dark/sync.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/icons/light/sync.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/js/main.js: -------------------------------------------------------------------------------- 1 | var cy; 2 | 3 | function generate_cy_stylesheet(cy) { 4 | // This is a bit of a hack but it works 5 | html_style = document.getElementsByTagName('html')[0].style; 6 | 7 | // From https://js.cytoscape.org/#core/style 8 | cy.style() 9 | .fromJson([ 10 | { 11 | selector: 'node', 12 | style: { 13 | label: 'data(id)', 14 | shape: 'round-rectangle', 15 | 'background-color': html_style.getPropertyValue('--vscode-button-background'), 16 | 'background-width': '90%', 17 | 'background-height': '90%', 18 | width: '228', 19 | height: '128', 20 | 'border-width': '0', 21 | }, 22 | }, 23 | { 24 | selector: 'label', 25 | style: { 26 | color: html_style.getPropertyValue('--vscode-button-foreground'), 27 | 'font-family': '"Segoe UI", Arial, Helvetica, sans-serif', 28 | 'font-size': '28vh', 29 | 'text-valign': 'center', 30 | 'text-halign': 'center', 31 | }, 32 | }, 33 | { 34 | selector: ':selected', 35 | style: { 36 | 'border-width': '4', 37 | 'border-color': html_style.getPropertyValue('--vscode-editor-hoverHighlightBackground'), 38 | 'background-color': html_style.getPropertyValue('--vscode-button-hoverBackground'), 39 | 'line-color': html_style.getPropertyValue('--vscode-minimap-errorHighlight'), 40 | 'target-arrow-color': html_style.getPropertyValue('--vscode-minimap-errorHighlight'), 41 | 'source-arrow-color': html_style.getPropertyValue('--vscode-minimap-errorHighlight'), 42 | }, 43 | }, 44 | { 45 | selector: 'edge', 46 | style: { 47 | 'target-arrow-shape': 'triangle', 48 | 'curve-style': 'bezier', 49 | 'control-point-step-size': 40, 50 | width: 10, 51 | }, 52 | }, 53 | ]) 54 | .update(); // indicate the end of your new stylesheet so that it can be updated on elements 55 | } 56 | 57 | function init() { 58 | vscode = acquireVsCodeApi(); 59 | 60 | cy = cytoscape({ 61 | container: document.getElementById('cy'), 62 | wheelSensitivity: 0.15, 63 | maxZoom: 5, 64 | minZoom: 0.2, 65 | selectionType: 'single', 66 | }); 67 | 68 | generate_cy_stylesheet(cy); 69 | 70 | vscode.postMessage({ command: 'initialized' }); 71 | } 72 | 73 | window.addEventListener('message', (event) => { 74 | const message = event.data; 75 | if (message.redraw == true) { 76 | cy.remove('*'); 77 | } 78 | 79 | nodeGraph = message.content; 80 | 81 | try { 82 | nodeGraph.vertices.forEach((element) => { 83 | cy.add({ 84 | data: { id: element.label }, 85 | }); 86 | }); 87 | 88 | nodeGraph.edges.forEach((element) => { 89 | cy.add({ 90 | data: { 91 | id: element.source + element.target, 92 | source: element.source, 93 | target: element.target, 94 | }, 95 | }); 96 | }); 97 | } catch (error) { 98 | vscode.postMessage({ 99 | command: 'error', 100 | errorMsg: `Error building node graph from json graph data: ${error}`, 101 | }); 102 | } 103 | 104 | cy.layout({ 105 | avoidOverlap: true, 106 | name: 'breadthfirst', 107 | circle: false, 108 | nodeDimensionsIncludeLabels: true, 109 | spacingFactor: 1.5, 110 | animate: true, 111 | }).run(); 112 | }); 113 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | [cmdletbinding()] 2 | param( 3 | [string[]]$Task = 'default', 4 | $properties 5 | ) 6 | 7 | if (!(Get-Module -Name psake -ListAvailable)) { 8 | Install-Module -Name psake -Scope CurrentUser -Force 9 | } 10 | 11 | Invoke-psake ` 12 | -buildFile "$PSScriptRoot\psakefile.ps1" ` 13 | -properties $properties ` 14 | -taskList $Task ` 15 | -Verbose:$VerbosePreference 16 | -------------------------------------------------------------------------------- /images/Puppet-Logo-Amber-sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puppetlabs/puppet-vscode/34b1c8f2b5a5a65993076ae7c0fb4b904c010145/images/Puppet-Logo-Amber-sm.png -------------------------------------------------------------------------------- /images/puppet-dag-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /images/puppet-dag-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /images/puppet-vscode-sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puppetlabs/puppet-vscode/34b1c8f2b5a5a65993076ae7c0fb4b904c010145/images/puppet-vscode-sm.png -------------------------------------------------------------------------------- /images/puppet_logo_sm.svg: -------------------------------------------------------------------------------- 1 | 3 | file_type_puppet 4 | 5 | 6 | -------------------------------------------------------------------------------- /images/syntax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puppetlabs/puppet-vscode/34b1c8f2b5a5a65993076ae7c0fb4b904c010145/images/syntax.png -------------------------------------------------------------------------------- /languages/puppet-language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | // symbol used for single line comment. Remove this entry if your language does not support line comments 4 | "lineComment": "#", 5 | // symbols used for start and end a block comment. Remove this entry if your language does not support block comments 6 | "blockComment": ["/*","*/"] 7 | }, 8 | // symbols used as brackets 9 | "brackets": [ 10 | ["{", "}"], 11 | ["[", "]"], 12 | ["(", ")"] 13 | ], 14 | // symbols that are auto closed when typing 15 | "autoClosingPairs": [ 16 | ["{", "}"], 17 | ["[", "]"], 18 | ["(", ")"], 19 | ["\"", "\""], 20 | ["'", "'"] 21 | ], 22 | // symbols that that can be used to surround a selection 23 | "surroundingPairs": [ 24 | ["{", "}"], 25 | ["[", "]"], 26 | ["(", ")"], 27 | ["\"", "\""], 28 | ["'", "'"] 29 | ], 30 | // support for region folding 31 | "folding": { 32 | "offSide": true, 33 | "markers": { 34 | "start": "^\\s*#\\s*region\\b", 35 | "end": "^\\s*#\\s*endregion\\b" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /languages/puppetfile-language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | // symbol used for single line comment. Remove this entry if your language does not support line comments 4 | "lineComment": "#", 5 | // symbols used for start and end a block comment. Remove this entry if your language does not support block comments 6 | "blockComment": ["/*","*/"] 7 | }, 8 | // symbols used as brackets 9 | "brackets": [ 10 | ["{", "}"], 11 | ["[", "]"], 12 | ["(", ")"] 13 | ], 14 | // symbols that are auto closed when typing 15 | "autoClosingPairs": [ 16 | ["{", "}"], 17 | ["[", "]"], 18 | ["(", ")"], 19 | ["\"", "\""], 20 | ["'", "'"] 21 | ], 22 | // symbols that that can be used to surround a selection 23 | "surroundingPairs": [ 24 | ["{", "}"], 25 | ["[", "]"], 26 | ["(", ")"], 27 | ["\"", "\""], 28 | ["'", "'"] 29 | ], 30 | // support for region folding 31 | "folding": { 32 | "offSide": true, 33 | "markers": { 34 | "start": "^\\s*#\\s*region\\b", 35 | "end": "^\\s*#\\s*endregion\\b" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /psakefile.ps1: -------------------------------------------------------------------------------- 1 | properties { 2 | $config = Get-Content (Join-Path $PSScriptRoot 'package.json') | ConvertFrom-Json 3 | $languageServerPath = (Join-Path $PSScriptRoot 'vendor/languageserver') 4 | $languageServerZip = Join-Path $PSScriptRoot 'editor_services.zip' 5 | $syntaxFilePath = Join-Path $PSScriptRoot 'syntaxes/puppet.tmLanguage' 6 | $packageVersion = '' 7 | } 8 | 9 | task Clean { 10 | $languageServerPath = (Join-Path $PSScriptRoot 'vendor') 11 | if (Test-Path -Path $languageServerPath) { 12 | Remove-Item -Path $languageServerPath -Recurse -Force 13 | } 14 | if (Test-Path -Path $syntaxFilePath) { 15 | Remove-Item -Path $syntaxFilePath -Force 16 | } 17 | } 18 | 19 | # "editorServices": { 20 | # "release": "0.26.0" 21 | # } 22 | # "editorServices": { 23 | # "release": "0.26.0" 24 | # "githubrepo": "puppet-editor-services", 25 | # "githubuser": "glennsarti" 26 | # } 27 | # "editorServices": { 28 | # "githubrepo": "puppet-editor-services", 29 | # "githubref": "glennsarti:spike-rearch-langserver" 30 | # }, 31 | task VendorEditorServices -precondition { !(Test-Path (Join-Path $PSScriptRoot 'vendor/languageserver')) } { 32 | $githubrepo = $config.editorComponents.editorServices.githubrepo ?? 'puppet-editor-services' 33 | $githubuser = $config.editorComponents.editorServices.githubuser ?? 'puppetlabs' 34 | 35 | if ($config.editorComponents.editorServices.release) { 36 | $releasenumber = $config.editorComponents.editorServices.release 37 | $uri = "https://github.com/${githubuser}/${githubrepo}/releases/download/${releasenumber}/puppet_editor_services_${releasenumber}.zip"; 38 | } 39 | else { 40 | $githubref = $config.editorComponents.editorServices.githubref; 41 | if ($githubref -notcontains ':') { 42 | throw "Invalid githubref. Must be in user:branch format like glennsarti:spike-rearch-langserver" 43 | } 44 | $githubuser = $githubref.split(":")[0] 45 | $githubbranch = $githubref.split(":")[1] 46 | $uri = "https://github.com/${githubuser}/${githubrepo}/archive/${githubbranch}.zip" 47 | } 48 | 49 | if ($config.editorComponents.editorServices.directory) { 50 | Copy-Item -Path $config.editorComponents.editorServices.directory -Destination $languageServerPath -Recurse -Force 51 | } 52 | elseif ($config.editorComponents.editorServices.release) { 53 | Invoke-RestMethod -Uri $uri -OutFile $languageServerZip -ErrorAction Stop 54 | Expand-Archive -Path $languageServerZip -DestinationPath $languageServerPath -ErrorAction Stop 55 | Remove-Item -Path $languageServerZip -Force 56 | } 57 | elseif ($config.editorComponents.editorServices.githubref) { 58 | Invoke-RestMethod -Uri $uri -OutFile $languageServerZip -ErrorAction Stop 59 | Expand-Archive -Path $languageServerZip -DestinationPath "$($languageServerPath)/tmp" -ErrorAction Stop 60 | Move-Item -Path (Join-Path $languageServerPath "tmp/$githubrepo-$githubref/*") -Destination $languageServerPath 61 | Remove-Item -Path $languageServerZip -Force 62 | Remove-Item -Path "$($languageServerPath)/tmp" -Force -Recurse 63 | } 64 | else { 65 | throw "Unable to vendor Editor Serices. Missing a release, directory, or git reference configuration item" 66 | } 67 | } 68 | 69 | task VendorEditorSyntax -precondition { !(Test-Path (Join-Path $PSScriptRoot 'syntaxes/puppet.tmLanguage')) } { 70 | $githubrepo = $config.editorComponents.editorSyntax.githubrepo ?? 'puppet-editor-syntax' 71 | $githubuser = $config.editorComponents.editorSyntax.githubuser ?? 'puppetlabs' 72 | 73 | if ($config.editorComponents.editorSyntax.directory) { 74 | $source = Join-Path ($config.editorComponents.editorSyntax.directory, 'syntaxes/puppet.tmLanguage') 75 | Copy-Item -Path $source -Destination $syntaxFilePath 76 | return 77 | } 78 | 79 | if ($config.editorComponents.editorSyntax.release) { 80 | $releasenumber = $config.editorComponents.editorSyntax.release 81 | $uri = "https://github.com/${githubuser}/${githubrepo}/releases/download/${releasenumber}/puppet.tmLanguage"; 82 | } 83 | elseif ($config.editorComponents.editorSyntax.githubref) { 84 | $githubref = $config.editorComponents.editorSyntax.githubref; 85 | $uri = "https://raw.githubusercontent.com/${githubuser}/${githubrepo}/${githubref}/syntaxes/puppet.tmLanguage" 86 | } 87 | else { 88 | throw "Unable to vendor Editor Syntax. Missing a release, directory, or git reference configuration item" 89 | } 90 | 91 | Invoke-RestMethod -Uri $uri -OutFile $syntaxFilePath -ErrorAction Stop 92 | } 93 | 94 | task VendorCytoscape -precondition { !(Test-Path (Join-Path $PSScriptRoot 'vendor\cytoscape')) } { 95 | $cyto = Join-Path $PSScriptRoot 'node_modules\cytoscape\dist' 96 | $vendorCytoPath = (Join-Path $PSScriptRoot 'vendor\cytoscape') 97 | Copy-Item -Path $cyto -Recurse -Destination $vendorCytoPath 98 | } 99 | 100 | task CompileTypeScript { 101 | exec { npm run compile } 102 | } 103 | 104 | task Bump { 105 | exec { npm version --no-git-tag-version $packageVersion } 106 | } 107 | 108 | task Npm -precondition { !(Test-Path (Join-Path $PSScriptRoot 'node_modules')) } { 109 | exec { npm install } 110 | } 111 | 112 | task Vendor -depends VendorEditorServices, VendorEditorSyntax, VendorCytoscape 113 | 114 | task Build -depends Npm, Vendor, CompileTypeScript 115 | 116 | task default -depends Build 117 | -------------------------------------------------------------------------------- /snippets/keywords.snippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "if": { 3 | "prefix": "if", 4 | "body": [ 5 | "if ${1:condition} {", 6 | "\t${2:# when true}", 7 | "}", 8 | "else {", 9 | "\t${3:# when false}", 10 | "}" 11 | ], 12 | "description": "Conditional statement" 13 | }, 14 | "unless": { 15 | "prefix": "unless", 16 | "body": [ 17 | "unless ${1:condition} {", 18 | "\t${2:# when false}", 19 | "}", 20 | "else {", 21 | "\t${2:# when true}", 22 | "}" 23 | ], 24 | "description": "Conditional statement" 25 | }, 26 | "case": { 27 | "prefix": "case", 28 | "body": [ 29 | "case ${1:condition} {", 30 | "\t'${2:value}': { } ", 31 | "\tdefault: { }", 32 | "}" 33 | ], 34 | "description": "Conditional case statement" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /snippets/metadata.snippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "blank metadata.json": { 3 | "prefix": "metadata.json", 4 | "body": [ 5 | "{", 6 | " \"name\": \"${1:author}-${2:module_name}\",", 7 | " \"version\": \"0.1.0\",", 8 | " \"author\": \"$1\",", 9 | " \"summary\": \"${3:brief summary}\",", 10 | " \"license\": \"Apache-2.0\",", 11 | " \"source\": \"${4:source location of the module}\",", 12 | " \"dependencies\": [", 13 | " {\"name\":\"puppetlabs-stdlib\",\"version_requirement\":\">= 1.0.0\"}", 14 | " ]", 15 | "}", 16 | "" 17 | ], 18 | "description": "Empty metadata.json file" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /snippets/puppetfile.snippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "Forge_module1": { 3 | "prefix": "Forge Module", 4 | "body": [ 5 | "mod '${1:author}-${2:name}', '${3:version}'" 6 | ], 7 | "description": "Install from the forge" 8 | }, 9 | "Forge_module2": { 10 | "prefix": "Latest Forge Module", 11 | "body": [ 12 | "mod '${1:author}-${2:name}', :latest" 13 | ], 14 | "description": "Install the latest version from the forge" 15 | }, 16 | "Git_module1": { 17 | "prefix": "Git Module by ref", 18 | "body": [ 19 | "mod '${1:name}', :git => '${2:repository}', :ref => '${3:reference}'" 20 | ], 21 | "description": "Install from a git repository specified by a git reference e.g. master" 22 | }, 23 | "Git_module2": { 24 | "prefix": "Git Module by commit", 25 | "body": [ 26 | "mod '${1:name}', :git => '${2:repository}', :commit => '${3:commit}'" 27 | ], 28 | "description": "Install from a git repository specified by a git commit e.g. 1c40e29" 29 | }, 30 | "Git_module3": { 31 | "prefix": "Git Module by tag", 32 | "body": [ 33 | "mod '${1:name}', :git => '${2:repository}', :tag => '${3:tag}'" 34 | ], 35 | "description": "Install from a git repository specified by a git tag e.g. v1.0.0" 36 | }, 37 | "Git_module4": { 38 | "prefix": "Git Module by branch", 39 | "body": [ 40 | "mod '${1:name}', :git => '${2:repository}', :branch => '${3:branch}'" 41 | ], 42 | "description": "Install from a git repository specified by a git branch e.g. release" 43 | }, 44 | "Svn_module1": { 45 | "prefix": "Svn Module by revision", 46 | "body": [ 47 | "mod '${1:name}', :svn => '${2:repository}', :revision => '${3:revision}'" 48 | ], 49 | "description": "Install from a SVN repository specified by a revision e.g. 154" 50 | }, 51 | "Local_module1": { 52 | "prefix": "Local Module", 53 | "body": [ 54 | "mod '${1:name}', :local => true" 55 | ], 56 | "description": "Install from the local Puppetfile module path" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/configuration/pathResolver.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | export class PathResolver { 5 | public static getprogramFiles(): string { 6 | switch (process.platform) { 7 | case 'win32': 8 | // eslint-disable-next-line no-case-declarations 9 | let programFiles = process.env['ProgramFiles'] || 'C:\\Program Files'; 10 | 11 | if (process.env['PROCESSOR_ARCHITEW6432'] === 'AMD64') { 12 | programFiles = process.env['ProgramW6432'] || 'C:\\Program Files'; 13 | } 14 | return programFiles; 15 | 16 | default: 17 | return '/opt'; 18 | } 19 | } 20 | 21 | public static resolveSubDirectory(rootDir: string, subDir: string) { 22 | const versionDir = path.join(rootDir, subDir); 23 | 24 | if (fs.existsSync(versionDir)) { 25 | return versionDir; 26 | } else { 27 | const subdir = PathResolver.getDirectories(rootDir)[1]; 28 | return subdir; 29 | } 30 | } 31 | 32 | public static getDirectories(parent: string) { 33 | return fs.readdirSync(parent).filter(function (file) { 34 | return fs.statSync(path.join(parent, file)).isDirectory(); 35 | }); 36 | } 37 | 38 | public static pathEnvSeparator() { 39 | if (process.platform === 'win32') { 40 | return ';'; 41 | } else { 42 | return ':'; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/configuration/pdkResolver.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | export interface IPDKRubyInstances { 5 | pdkDirectory: string; 6 | instances: IPDKRubyInstance[]; 7 | allPuppetVersions: string[]; 8 | latest: IPDKRubyInstance; 9 | 10 | /** 11 | * Finds the first PDK Instance that has Puppet version `puppetVersion` 12 | * @param puppetVersion The puppet version string to search for 13 | * @returns IPDKRubyInstance or undefined 14 | */ 15 | instanceForPuppetVersion(puppetVersion: string): IPDKRubyInstance; 16 | } 17 | 18 | export interface IPDKRubyInstance { 19 | rubyVerDir: string; 20 | rubyDir: string; 21 | rubyBinDir: string; 22 | gemVerDir: string; 23 | gemDir: string; 24 | 25 | puppetVersions: string[]; 26 | rubyVersion: string; 27 | valid: boolean; 28 | } 29 | 30 | // PDK Directory Layout 31 | // *nix - /opt/puppetlabs/pdk/ 32 | // Windows - C:\Program Files\Puppet Labs\DevelopmentKit 33 | // | private 34 | // | | puppet 35 | // | | | ruby 36 | // | | | 2.5.0 <---- pdkRubyVerDir (GEM_PATH #3) (contains puppet gems e.g. puppet, ffi, gettext) 37 | // | | ruby 38 | // | | 2.5.1 <---- pdkRubyDir 39 | // | | bin <---- pdkRubyBinDir 40 | // | | lib 41 | // | | ruby 42 | // | | gems 43 | // | | 2.5.0 <---- pdkGemVerDir (GEM_PATH #1) (contains base gem set e.g. bundler, rubygems) 44 | // | share 45 | // | cache 46 | // | ruby 47 | // | 2.5.0 <---- pdkGemDir (GEM_PATH #2, replaceSlashes) (contains all the ancillary gems e.g. puppet-lint, rspec) 48 | 49 | export function pdkInstances(pdkRootDirectory: string): IPDKRubyInstances { 50 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 51 | return new PDKRubyInstances(pdkRootDirectory); 52 | } 53 | 54 | export function emptyPDKInstance(): IPDKRubyInstance { 55 | return { 56 | rubyVerDir: undefined, 57 | rubyDir: undefined, 58 | rubyBinDir: undefined, 59 | gemVerDir: undefined, 60 | gemDir: undefined, 61 | puppetVersions: undefined, 62 | rubyVersion: undefined, 63 | valid: false, 64 | }; 65 | } 66 | 67 | class PDKRubyInstances implements IPDKRubyInstances { 68 | pdkDirectory: string; 69 | private rubyInstances: IPDKRubyInstance[] = undefined; 70 | private puppetVersions: string[] = undefined; 71 | 72 | constructor(pdkRootDirectory: string) { 73 | this.pdkDirectory = pdkRootDirectory; 74 | } 75 | 76 | get instances(): IPDKRubyInstance[] { 77 | if (this.rubyInstances !== undefined) { 78 | return this.rubyInstances; 79 | } 80 | this.rubyInstances = new Array(); 81 | if (this.pdkDirectory === undefined) { 82 | return this.rubyInstances; 83 | } 84 | if (!fs.existsSync(this.pdkDirectory)) { 85 | return this.rubyInstances; 86 | } 87 | 88 | const rubyDir = path.join(this.pdkDirectory, 'private', 'ruby'); 89 | if (!fs.existsSync(rubyDir)) { 90 | return this.rubyInstances; 91 | } 92 | 93 | fs.readdirSync(rubyDir).forEach((item) => { 94 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 95 | this.rubyInstances.push(new PDKRubyInstance(this.pdkDirectory, path.join(rubyDir, item))); 96 | }); 97 | 98 | return this.rubyInstances; 99 | } 100 | 101 | get allPuppetVersions(): string[] { 102 | if (this.puppetVersions !== undefined) { 103 | return this.puppetVersions; 104 | } 105 | this.puppetVersions = []; 106 | // This searching method isn't the most performant but as the list will probably 107 | // be very small (< 20 items) it's not a big deal and is cached anyway 108 | this.instances.forEach((instance) => { 109 | instance.puppetVersions.forEach((puppetVersion) => { 110 | if (this.puppetVersions.indexOf(puppetVersion) === -1) { 111 | this.puppetVersions.push(puppetVersion); 112 | } 113 | }); 114 | }); 115 | return this.puppetVersions; 116 | } 117 | 118 | public instanceForPuppetVersion(puppetVersion: string): IPDKRubyInstance { 119 | return this.instances.find((instance) => { 120 | return instance.puppetVersions.includes(puppetVersion); 121 | }); 122 | } 123 | 124 | // Override toString to make it look pretty 125 | toString(): string { 126 | return ( 127 | '[' + 128 | this.instances 129 | .map((item) => { 130 | return item.toString(); 131 | }) 132 | .join(', ') + 133 | ']' 134 | ); 135 | } 136 | 137 | get latest(): IPDKRubyInstance { 138 | let result = undefined; 139 | let lastVersion = '0.0.0'; 140 | 141 | this.instances.forEach((instance) => { 142 | // We don't have a real semver module so treat the strings as numbers and sort. 143 | if (instance.rubyVersion.localeCompare(lastVersion, undefined, { numeric: true }) > 0) { 144 | result = instance; 145 | lastVersion = instance.rubyVersion; 146 | } 147 | }); 148 | 149 | return result; 150 | } 151 | } 152 | 153 | class PDKRubyInstance implements IPDKRubyInstance { 154 | private _rubyVerDir: string; 155 | private _rubyDir: string; 156 | private _rubyBinDir: string; 157 | private _gemVerDir: string; 158 | private _gemDir: string; 159 | private _puppetVersions: string[]; 160 | 161 | private _rubyVersion: string; 162 | private _valid: boolean = undefined; 163 | 164 | // Directory Paths 165 | get rubyVerDir(): string { 166 | return this._rubyVerDir; 167 | } 168 | get rubyDir(): string { 169 | return this._rubyDir; 170 | } 171 | get rubyBinDir(): string { 172 | return this._rubyBinDir; 173 | } 174 | get gemVerDir(): string { 175 | return this._gemVerDir; 176 | } 177 | get gemDir(): string { 178 | return this._gemDir; 179 | } 180 | 181 | get rubyVersion(): string { 182 | return this._rubyVersion; 183 | } 184 | 185 | get valid(): boolean { 186 | if (this._valid !== undefined) { 187 | return this._valid; 188 | } 189 | // This instance is valid if these directories exist 190 | this._valid = 191 | fs.existsSync(this._rubyDir) && 192 | fs.existsSync(this._rubyBinDir) && 193 | fs.existsSync(this._rubyVerDir) && 194 | fs.existsSync(this._gemVerDir) && 195 | fs.existsSync(this._gemDir); 196 | return this._valid; 197 | } 198 | 199 | get puppetVersions(): string[] { 200 | if (this._puppetVersions !== undefined) { 201 | return this._puppetVersions; 202 | } 203 | this._puppetVersions = []; 204 | const gemdir = path.join(this._rubyVerDir, 'gems'); 205 | if (!fs.existsSync(gemdir)) { 206 | return this._puppetVersions; 207 | } 208 | 209 | // We could just call Ruby and ask it for all gems called puppet, but searching 210 | // the gem cache is just as easy and doesn't need to spawn a ruby process per 211 | // ruby version. 212 | fs.readdirSync(gemdir).forEach((item) => { 213 | const pathMatch = item.match(/^puppet-(\d+\.\d+\.\d+)(?:(-|$))/); 214 | if (pathMatch !== null) { 215 | this._puppetVersions.push(pathMatch[1]); 216 | } 217 | }); 218 | 219 | return this._puppetVersions; 220 | } 221 | 222 | // Override toString to make it look pretty 223 | toString(): string { 224 | return ( 225 | '{' + 226 | [ 227 | `rubyVersion: "${this._rubyVersion}"`, 228 | `rubyDir: "${this._rubyDir}"`, 229 | `rubyVerDir: "${this.rubyVerDir}"`, 230 | `gemVerDir: "${this.gemVerDir}"`, 231 | `gemDir: "${this.gemDir}"`, 232 | `gemDir: "${this.gemDir}"`, 233 | `puppetVersions: "${this.puppetVersions}"`, 234 | `valid: "${this.valid}"`, 235 | ].join(', ') + 236 | '}' 237 | ); 238 | } 239 | 240 | constructor(pdkDirectory: string, rubyDir: string) { 241 | this._rubyDir = rubyDir; 242 | this._rubyBinDir = path.join(rubyDir, 'bin'); 243 | this._rubyVersion = path.basename(rubyDir); 244 | // This is a little naive however there doesn't appear to be a native semver module 245 | // loaded in VS Code. The gem path is always the ..0 version of the 246 | // corresponding Ruby version 247 | const gemDirName = this._rubyVersion.replace(/\.\d+$/, '.0'); 248 | // Calculate gem paths 249 | this._rubyVerDir = path.join(pdkDirectory, 'private', 'puppet', 'ruby', gemDirName); 250 | this._gemVerDir = path.join(this._rubyDir, 'lib', 'ruby', 'gems', gemDirName); 251 | this._gemDir = path.join(pdkDirectory, 'share', 'cache', 'ruby', gemDirName); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/feature.ts: -------------------------------------------------------------------------------- 1 | import vscode = require('vscode'); 2 | 3 | export interface IFeature extends vscode.Disposable { 4 | dispose(); 5 | } 6 | -------------------------------------------------------------------------------- /src/feature/DebuggingFeature.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict'; 3 | 4 | import * as cp from 'child_process'; 5 | import * as vscode from 'vscode'; 6 | import { IAggregateConfiguration } from '../configuration'; 7 | import { IFeature } from '../feature'; 8 | import { CommandEnvironmentHelper } from '../helpers/commandHelper'; 9 | import { ILogger } from '../logging'; 10 | 11 | // Socket vs Exec DebugAdapter types 12 | // https://github.com/Microsoft/vscode/blob/2808feeaf6b24feaaa6ba49fb91ea165c4d5fb06/src/vs/workbench/parts/debug/node/debugger.ts#L58-L61 13 | // 14 | // DebugAdapterExecutable uses stdin/stdout 15 | // https://github.com/Microsoft/vscode/blob/2808feeaf6b24feaaa6ba49fb91ea165c4d5fb06/src/vs/workbench/parts/debug/node/debugAdapter.ts#L305 16 | // 17 | // DebugAdapterServer uses tcp 18 | // https://github.com/Microsoft/vscode/blob/2808feeaf6b24feaaa6ba49fb91ea165c4d5fb06/src/vs/workbench/parts/debug/node/debugAdapter.ts#L256 19 | 20 | export class DebugAdapterDescriptorFactory implements vscode.DebugAdapterDescriptorFactory, vscode.Disposable { 21 | readonly Context: vscode.ExtensionContext; 22 | readonly Config: IAggregateConfiguration; 23 | readonly Logger: ILogger; 24 | 25 | public ChildProcesses: cp.ChildProcess[] = []; 26 | 27 | constructor(context: vscode.ExtensionContext, config: IAggregateConfiguration, logger: ILogger) { 28 | this.Context = context; 29 | this.Config = config; 30 | this.Logger = logger; 31 | } 32 | 33 | public createDebugAdapterDescriptor( 34 | session: vscode.DebugSession, 35 | executable: vscode.DebugAdapterExecutable, 36 | ): vscode.ProviderResult { 37 | // Right now we don't care about session as we only have one type of adapter, which is launch. When 38 | // we add the ability to attach to a debugger remotely we'll need to switch scenarios based on `session` 39 | const thisFactory = this; 40 | 41 | return new Promise(function (resolve, reject) { 42 | const debugServer = CommandEnvironmentHelper.getDebugServerRubyEnvFromConfiguration( 43 | thisFactory.Context.asAbsolutePath(thisFactory.Config.ruby.debugServerPath), 44 | thisFactory.Config, 45 | ); 46 | 47 | const spawn_options: cp.SpawnOptions = {}; 48 | spawn_options.env = debugServer.options.env; 49 | spawn_options.stdio = 'pipe'; 50 | if (process.platform !== 'win32') { 51 | spawn_options.shell = true; 52 | } 53 | 54 | thisFactory.Logger.verbose( 55 | 'Starting the Debug Server with ' + debugServer.command + ' ' + debugServer.args.join(' '), 56 | ); 57 | const debugServerProc = cp.spawn(debugServer.command, debugServer.args, spawn_options); 58 | thisFactory.ChildProcesses.push(debugServerProc); 59 | 60 | let debugSessionRunning = false; 61 | debugServerProc.stdout.on('data', (data) => { 62 | thisFactory.Logger.debug('Debug Server STDOUT: ' + data.toString()); 63 | // If the debug client isn't already running and it's sent the trigger text, start up a client 64 | if (!debugSessionRunning && data.toString().match('DEBUG SERVER RUNNING') !== null) { 65 | debugSessionRunning = true; 66 | 67 | const p = data.toString().match(/DEBUG SERVER RUNNING (.*):(\d+)/); 68 | if (p === null) { 69 | reject('Debug Server started but unable to parse hostname and port'); 70 | } else { 71 | thisFactory.Logger.debug('Starting Debug Client connection to ' + p[1] + ':' + p[2]); 72 | resolve(new vscode.DebugAdapterServer(Number(p[2]), p[1])); 73 | } 74 | } 75 | }); 76 | debugServerProc.on('error', (data) => { 77 | thisFactory.Logger.error('Debug Srver errored with ' + data); 78 | reject('Spawning Debug Server failed with ' + data); 79 | }); 80 | debugServerProc.on('close', (exitCode) => { 81 | thisFactory.Logger.verbose('Debug Server exited with exitcode ' + exitCode); 82 | }); 83 | }); 84 | } 85 | 86 | public dispose(): any { 87 | this.ChildProcesses.forEach((item) => { 88 | item.kill('SIGHUP'); 89 | }); 90 | this.ChildProcesses = []; 91 | return undefined; 92 | } 93 | } 94 | 95 | export class DebugConfigurationProvider implements vscode.DebugConfigurationProvider { 96 | private debugType: string; 97 | private logger: ILogger; 98 | private context: vscode.ExtensionContext; 99 | 100 | constructor(debugType: string, logger: ILogger, context: vscode.ExtensionContext) { 101 | this.debugType = debugType; 102 | this.logger = logger; 103 | this.context = context; 104 | } 105 | 106 | public provideDebugConfigurations( 107 | folder: vscode.WorkspaceFolder | undefined, 108 | token?: vscode.CancellationToken, 109 | ): vscode.ProviderResult { 110 | return [this.createLaunchConfigFromContext(folder)]; 111 | } 112 | 113 | public resolveDebugConfiguration( 114 | folder: vscode.WorkspaceFolder | undefined, 115 | debugConfiguration: vscode.DebugConfiguration, 116 | token?: vscode.CancellationToken, 117 | ): vscode.ProviderResult { 118 | return debugConfiguration; 119 | } 120 | 121 | private createLaunchConfigFromContext(folder: vscode.WorkspaceFolder | undefined): vscode.DebugConfiguration { 122 | const config = { 123 | type: this.debugType, 124 | request: 'launch', 125 | name: 'Puppet Apply current file', 126 | manifest: '${file}', 127 | args: [], 128 | noop: true, 129 | cwd: '${file}', 130 | }; 131 | 132 | return config; 133 | } 134 | } 135 | 136 | export class DebuggingFeature implements IFeature { 137 | private factory: DebugAdapterDescriptorFactory; 138 | private provider: DebugConfigurationProvider; 139 | 140 | constructor(debugType: string, config: IAggregateConfiguration, context: vscode.ExtensionContext, logger: ILogger) { 141 | this.factory = new DebugAdapterDescriptorFactory(context, config, logger); 142 | this.provider = new DebugConfigurationProvider(debugType, logger, context); 143 | 144 | logger.debug('Registered DebugAdapterDescriptorFactory for ' + debugType); 145 | context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory(debugType, this.factory)); 146 | 147 | context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider(debugType, this.provider)); 148 | logger.debug('Registered DebugConfigurationProvider for ' + debugType); 149 | } 150 | 151 | public dispose(): any { 152 | if (this.factory !== null) { 153 | this.factory.dispose(); 154 | this.factory = null; 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/feature/FormatDocumentFeature.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as vscode from 'vscode'; 4 | import { IAggregateConfiguration } from '../configuration'; 5 | import { IFeature } from '../feature'; 6 | import { ConnectionHandler } from '../handler'; 7 | import { ConnectionStatus } from '../interfaces'; 8 | import { ILogger } from '../logging'; 9 | import * as messages from '../messages'; 10 | import { reporter } from '../telemetry'; 11 | 12 | class RequestParams implements messages.PuppetFixDiagnosticErrorsRequestParams { 13 | documentUri: string; 14 | alwaysReturnContent: boolean; 15 | } 16 | 17 | class FormatDocumentProvider { 18 | private connectionHandler: ConnectionHandler = undefined; 19 | 20 | constructor(connectionManager: ConnectionHandler) { 21 | this.connectionHandler = connectionManager; 22 | } 23 | 24 | public async formatTextEdits( 25 | document: vscode.TextDocument, 26 | options: vscode.FormattingOptions, 27 | ): Promise { 28 | if ( 29 | this.connectionHandler.status !== ConnectionStatus.RunningLoaded && 30 | this.connectionHandler.status !== ConnectionStatus.RunningLoading 31 | ) { 32 | vscode.window.showInformationMessage('Please wait and try again. The Puppet extension is still loading...'); 33 | return []; 34 | } 35 | 36 | if (reporter) { 37 | reporter.sendTelemetryEvent('puppet/FormatDocument'); 38 | } 39 | 40 | const requestParams = new RequestParams(); 41 | requestParams.documentUri = document.uri.toString(false); 42 | requestParams.alwaysReturnContent = false; 43 | 44 | const result = (await this.connectionHandler.languageClient.sendRequest( 45 | messages.PuppetFixDiagnosticErrorsRequest.type, 46 | requestParams, 47 | )) as messages.PuppetFixDiagnosticErrorsResponse; 48 | if (result.fixesApplied > 0 && result.newContent !== undefined) { 49 | return [vscode.TextEdit.replace(new vscode.Range(0, 0, document.lineCount, 0), result.newContent)]; 50 | } 51 | return []; 52 | } 53 | } 54 | 55 | export class FormatDocumentFeature implements IFeature { 56 | private provider: FormatDocumentProvider; 57 | 58 | constructor( 59 | langID: string, 60 | connectionManager: ConnectionHandler, 61 | config: IAggregateConfiguration, 62 | logger: ILogger, 63 | context: vscode.ExtensionContext, 64 | ) { 65 | this.provider = new FormatDocumentProvider(connectionManager); 66 | 67 | if (config.workspace.format.enable === true) { 68 | logger.debug('Registered Format Document provider'); 69 | context.subscriptions.push( 70 | vscode.languages.registerDocumentFormattingEditProvider(langID, { 71 | provideDocumentFormattingEdits: (document, options, token) => { 72 | return this.provider.formatTextEdits(document, options); 73 | }, 74 | }), 75 | ); 76 | } else { 77 | logger.debug('Format Document provider has not been registered'); 78 | } 79 | } 80 | 81 | public getProvider(): FormatDocumentProvider { 82 | return this.provider; 83 | } 84 | 85 | public dispose(): any { 86 | return undefined; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/feature/PDKFeature.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | import * as vscode from 'vscode'; 6 | import { IFeature } from '../feature'; 7 | import { ILogger } from '../logging'; 8 | import { PDKCommandStrings } from '../messages'; 9 | import { reporter } from '../telemetry'; 10 | 11 | export class PDKFeature implements IFeature { 12 | constructor(context: vscode.ExtensionContext, logger: ILogger) { 13 | context.subscriptions.push( 14 | vscode.commands.registerCommand(PDKCommandStrings.pdkNewModuleCommandId, () => { 15 | this.pdkNewModuleCommand(); 16 | }), 17 | ); 18 | logger.debug('Registered ' + PDKCommandStrings.pdkNewModuleCommandId + ' command'); 19 | 20 | // commands that require no user input 21 | [ 22 | { id: 'extension.pdkValidate', request: 'pdk validate', type: 'validate' }, 23 | { id: 'extension.pdkTestUnit', request: 'pdk test unit', type: 'test' }, 24 | ].forEach((command) => { 25 | context.subscriptions.push( 26 | vscode.commands.registerCommand(command.id, () => { 27 | this.getTerminal().sendText(command.request); 28 | this.getTerminal().show(); 29 | if (reporter) { 30 | reporter.sendTelemetryEvent(command.id); 31 | } 32 | }), 33 | ); 34 | logger.debug(`Registered ${command.id} command`); 35 | }); 36 | 37 | // commands that require user input 38 | [ 39 | { id: 'extension.pdkNewClass', request: 'pdk new class', type: 'Puppet class' }, 40 | { id: 'extension.pdkNewTask', request: 'pdk new task', type: 'Bolt task' }, 41 | { id: 'extension.pdkNewDefinedType', request: 'pdk new defined_type', type: 'Puppet defined_type' }, 42 | { id: 'extension.pdkNewFact', request: 'pdk new fact', type: 'Puppet Fact' }, 43 | { id: 'extension.pdkNewFunction', request: 'pdk new function', type: 'Puppet Function' }, 44 | ].forEach((command) => { 45 | context.subscriptions.push( 46 | vscode.commands.registerCommand(command.id, async () => { 47 | const name = await vscode.window.showInputBox({ 48 | prompt: `Enter a name for the new ${command.type}`, 49 | }); 50 | if (name === undefined) { 51 | vscode.window.showWarningMessage('No module name specifed. Exiting.'); 52 | return; 53 | } 54 | 55 | const request = `${command.request} ${name}`; 56 | this.getTerminal().sendText(request); 57 | this.getTerminal().show(); 58 | if (reporter) { 59 | reporter.sendTelemetryEvent(command.id); 60 | } 61 | }), 62 | ); 63 | logger.debug(`Registered ${command.id} command`); 64 | }); 65 | } 66 | 67 | private getTerminal(): vscode.Terminal { 68 | const existingTerm = vscode.window.terminals.find((tm) => tm.name === 'Puppet PDK'); 69 | return existingTerm === undefined ? vscode.window.createTerminal('Puppet PDK') : existingTerm; 70 | } 71 | 72 | public dispose(): void { 73 | this.getTerminal().dispose(); 74 | } 75 | 76 | private async pdkNewModuleCommand(): Promise { 77 | const name = await vscode.window.showInputBox({ 78 | prompt: 'Enter a name for the new Puppet module', 79 | }); 80 | if (!name) { 81 | vscode.window.showWarningMessage('No module name specifed. Exiting.'); 82 | return; 83 | } 84 | const directory = await vscode.window.showOpenDialog({ 85 | canSelectMany: false, 86 | canSelectFiles: false, 87 | canSelectFolders: true, 88 | openLabel: 'Choose the path for the new Puppet module', 89 | }); 90 | if (!directory) { 91 | vscode.window.showWarningMessage('No directory specifed. Exiting.'); 92 | return; 93 | } 94 | 95 | const p = path.join(directory[0].fsPath, name); 96 | 97 | this.getTerminal().sendText(`pdk new module --skip-interview ${name} ${p}`); 98 | this.getTerminal().show(); 99 | 100 | await new Promise((resolve) => { 101 | let count = 0; 102 | const handle = setInterval(() => { 103 | count++; 104 | if (count >= 30) { 105 | clearInterval(handle); 106 | resolve(); 107 | return; 108 | } 109 | 110 | if (fs.existsSync(p)) { 111 | resolve(); 112 | return; 113 | } 114 | }, 1000); 115 | }); 116 | 117 | const uri = vscode.Uri.file(p); 118 | await vscode.commands.executeCommand('vscode.openFolder', uri); 119 | 120 | if (reporter) { 121 | reporter.sendTelemetryEvent(PDKCommandStrings.pdkNewModuleCommandId); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/feature/PuppetModuleHoverFeature.ts: -------------------------------------------------------------------------------- 1 | import { getLocation } from 'jsonc-parser'; 2 | import * as vscode from 'vscode'; 3 | import { IFeature } from '../feature'; 4 | import { buildMarkdown, getModuleInfo } from '../forge'; 5 | import { ILogger } from '../logging'; 6 | import { reporter } from '../telemetry'; 7 | 8 | export class PuppetModuleHoverFeature implements IFeature { 9 | constructor(public context: vscode.ExtensionContext, public logger: ILogger) { 10 | const selector = [{ language: 'json', scheme: '*', pattern: '**/metadata.json' }]; 11 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 12 | context.subscriptions.push(vscode.languages.registerHoverProvider(selector, new PuppetModuleHoverProvider(logger))); 13 | } 14 | 15 | // eslint-disable-next-line @typescript-eslint/no-empty-function 16 | dispose() {} 17 | } 18 | 19 | export class PuppetModuleHoverProvider implements vscode.HoverProvider { 20 | constructor(public logger: ILogger) {} 21 | 22 | async provideHover( 23 | document: vscode.TextDocument, 24 | position: vscode.Position, 25 | token: vscode.CancellationToken, 26 | ): Promise | null { 27 | const offset = document.offsetAt(position); 28 | const location = getLocation(document.getText(), offset); 29 | 30 | if (location.isAtPropertyKey) { 31 | return; 32 | } 33 | 34 | if (location.path[0] !== 'dependencies') { 35 | return; 36 | } 37 | 38 | if (location.path[2] !== 'name') { 39 | return; 40 | } 41 | 42 | if (reporter) { 43 | reporter.sendTelemetryEvent('metadataJSON/Hover'); 44 | } 45 | const wordPattern = new RegExp(/[\w/-]+/); 46 | let range = document.getWordRangeAtPosition(position, wordPattern); 47 | 48 | // If the range does not include the full module name, adjust the range 49 | if (!range.contains(position)) { 50 | const lineText = document.lineAt(position.line).text; 51 | const quoteIndex = lineText.indexOf('"', position.character); 52 | if (quoteIndex !== -1) { 53 | const start = lineText.lastIndexOf('"', position.character) + 1; 54 | const end = quoteIndex; 55 | range = new vscode.Range(position.line, start, position.line, end); 56 | } 57 | } 58 | const word = document.getText(range); 59 | 60 | this.logger.debug('Metadata hover info found ' + word + ' module'); 61 | 62 | const name = word.replace('"', '').replace('"', '').replace('/', '-'); 63 | 64 | const info = await getModuleInfo(name, this.logger); 65 | const markdown = buildMarkdown(info); 66 | const hoverinfo = new vscode.Hover(markdown, range); 67 | return hoverinfo; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/feature/PuppetNodeGraphFeature.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as vscode from 'vscode'; 3 | import { IFeature } from '../feature'; 4 | import { ConnectionHandler } from '../handler'; 5 | import { ConnectionStatus } from '../interfaces'; 6 | import { ILogger } from '../logging'; 7 | import { PuppetNodeGraphRequest, PuppetNodeGraphResponse } from '../messages'; 8 | import { ISettings, settingsFromWorkspace } from '../settings'; 9 | import { reporter } from '../telemetry'; 10 | 11 | const puppetNodeGraphToTheSideCommandId = 'puppet.puppetShowNodeGraphToSide'; 12 | 13 | export class PuppetNodeGraphFeature implements IFeature { 14 | private providers: NodeGraphWebViewProvider[] = undefined; 15 | 16 | constructor( 17 | protected puppetLangID: string, 18 | protected handler: ConnectionHandler, 19 | protected logger: ILogger, 20 | protected context: vscode.ExtensionContext, 21 | ) { 22 | this.providers = []; 23 | 24 | context.subscriptions.push( 25 | vscode.commands.registerCommand(puppetNodeGraphToTheSideCommandId, () => { 26 | if (!vscode.window.activeTextEditor) { 27 | return; 28 | } 29 | if (vscode.window.activeTextEditor.document.languageId !== this.puppetLangID) { 30 | return; 31 | } 32 | if ( 33 | this.handler.status !== ConnectionStatus.RunningLoaded && 34 | this.handler.status !== ConnectionStatus.RunningLoading 35 | ) { 36 | vscode.window.showInformationMessage( 37 | 'The Puppet Node Graph Preview is not available as the Editor Service is not ready. Please try again.', 38 | ); 39 | return; 40 | } 41 | 42 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 43 | const provider = new NodeGraphWebViewProvider( 44 | vscode.window.activeTextEditor.document.uri, 45 | handler, 46 | logger, 47 | context, 48 | ); 49 | this.providers.push(provider); 50 | provider.show(); 51 | }), 52 | ); 53 | logger.debug('Registered command for node graph event handler'); 54 | 55 | context.subscriptions.push( 56 | vscode.workspace.onDidSaveTextDocument((document) => { 57 | // Subscribe to save events and fire updates 58 | this.providers.forEach((item) => { 59 | if (document.uri === vscode.window.activeTextEditor.document.uri) { 60 | item.show(true); 61 | } 62 | }); 63 | }), 64 | ); 65 | logger.debug('Registered onDidSaveTextDocument for node graph event handler'); 66 | } 67 | 68 | dispose() { 69 | this.providers.forEach((p) => { 70 | p.dispose(); 71 | }); 72 | } 73 | } 74 | 75 | class NodeGraphWebViewProvider implements vscode.Disposable { 76 | private panel: vscode.WebviewPanel = undefined; 77 | 78 | constructor( 79 | protected resource: vscode.Uri, 80 | protected connectionHandler: ConnectionHandler, 81 | protected logger: ILogger, 82 | protected context: vscode.ExtensionContext, 83 | ) { 84 | const fileName = path.basename(resource.fsPath); 85 | this.panel = vscode.window.createWebviewPanel( 86 | 'puppetNodeGraph', // Identifies the type of the webview. Used internally 87 | `Node Graph '${fileName}'`, // Title of the panel displayed to the user 88 | vscode.ViewColumn.Beside, // Editor column to show the new webview panel in. 89 | { enableScripts: true }, 90 | ); 91 | this.panel.webview.html = this.getHtml(this.context.extensionPath); 92 | // eslint-disable-next-line @typescript-eslint/no-empty-function 93 | this.panel.onDidDispose(() => {}); 94 | this.panel.webview.onDidReceiveMessage((message) => { 95 | switch (message.command) { 96 | case 'error': 97 | vscode.window.showErrorMessage(message.errorMsg); 98 | break; 99 | case 'warning': 100 | vscode.window.showWarningMessage(message.errorMsg); 101 | break; 102 | default: 103 | break; 104 | } 105 | }); 106 | } 107 | 108 | async show(redraw = false) { 109 | const notificationType = this.getNotificationType(); 110 | if (notificationType === undefined) { 111 | return this.connectionHandler.languageClient 112 | .sendRequest(PuppetNodeGraphRequest.type, { 113 | external: this.resource.toString(), 114 | }) 115 | .then((compileResult) => { 116 | this.getJsonContent(compileResult, redraw); 117 | }); 118 | } else { 119 | vscode.window.withProgress( 120 | { 121 | location: notificationType, 122 | title: 'Puppet', 123 | cancellable: false, 124 | }, 125 | (progress) => { 126 | progress.report({ message: 'Generating New Node Graph' }); 127 | 128 | return this.connectionHandler.languageClient 129 | .sendRequest(PuppetNodeGraphRequest.type, { 130 | external: this.resource.toString(), 131 | }) 132 | .then((compileResult) => { 133 | this.getJsonContent(compileResult, redraw); 134 | }); 135 | }, 136 | ); 137 | } 138 | } 139 | 140 | dispose() { 141 | this.panel.dispose(); 142 | } 143 | 144 | private getJsonContent(compileResult: PuppetNodeGraphResponse, redraw: boolean) { 145 | if (compileResult === undefined) { 146 | vscode.window.showErrorMessage('Invalid data returned from manifest. Cannot build node graph'); 147 | return; 148 | } 149 | 150 | if (compileResult.error) { 151 | vscode.window.showErrorMessage(compileResult.error); 152 | return; 153 | } 154 | 155 | if (reporter) { 156 | reporter.sendTelemetryEvent(puppetNodeGraphToTheSideCommandId); 157 | } 158 | 159 | this.panel.webview.postMessage({ 160 | content: compileResult, 161 | redraw: redraw, 162 | }); 163 | } 164 | 165 | private getNotificationType(): vscode.ProgressLocation { 166 | // Calculate where the progress message should go, if at all. 167 | const currentSettings: ISettings = settingsFromWorkspace(); 168 | 169 | let notificationType = vscode.ProgressLocation.Notification; 170 | 171 | if (currentSettings.notification !== undefined && currentSettings.notification.nodeGraph !== undefined) { 172 | switch (currentSettings.notification.nodeGraph.toLowerCase()) { 173 | case 'messagebox': 174 | notificationType = vscode.ProgressLocation.Notification; 175 | break; 176 | case 'statusbar': 177 | notificationType = vscode.ProgressLocation.Window; 178 | break; 179 | case 'none': 180 | notificationType = undefined; 181 | break; 182 | default: 183 | break; // Default is already set 184 | } 185 | } 186 | 187 | return notificationType; 188 | } 189 | 190 | private getHtml(extensionPath: string): string { 191 | const cytoPath = this.panel.webview.asWebviewUri( 192 | vscode.Uri.file(path.join(extensionPath, 'vendor', 'cytoscape', 'cytoscape.min.js')), 193 | ); 194 | const mainScript = this.panel.webview.asWebviewUri( 195 | vscode.Uri.file(path.join(extensionPath, 'assets', 'js', 'main.js')), 196 | ); 197 | const mainCss = this.panel.webview.asWebviewUri( 198 | vscode.Uri.file(path.join(extensionPath, 'assets', 'css', 'main.css')), 199 | ); 200 | 201 | const html = ` 202 | 203 | 204 | 205 | 206 | Puppet node graph 207 | 208 | 209 | 210 | 211 | 212 |
213 | 216 | 217 | 218 | `; 219 | return html; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/feature/PuppetResourceFeature.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as vscode from 'vscode'; 4 | import { IFeature } from '../feature'; 5 | import { ConnectionHandler } from '../handler'; 6 | import { ConnectionStatus } from '../interfaces'; 7 | import { ILogger } from '../logging'; 8 | import { 9 | PuppetCommandStrings, 10 | PuppetResourceRequest, 11 | PuppetResourceRequestParams, 12 | PuppetResourceResponse, 13 | } from '../messages'; 14 | import { ISettings, settingsFromWorkspace } from '../settings'; 15 | import { reporter } from '../telemetry'; 16 | 17 | export class PuppetResourceFeature implements IFeature { 18 | private _connectionHandler: ConnectionHandler; 19 | private logger: ILogger; 20 | 21 | // eslint-disable-next-line @typescript-eslint/no-empty-function 22 | dispose() {} 23 | 24 | constructor(context: vscode.ExtensionContext, connMgr: ConnectionHandler, logger: ILogger) { 25 | this.logger = logger; 26 | this._connectionHandler = connMgr; 27 | context.subscriptions.push( 28 | vscode.commands.registerCommand(PuppetCommandStrings.puppetResourceCommandId, () => { 29 | this.run(); 30 | }), 31 | ); 32 | } 33 | public run() { 34 | if (this._connectionHandler.status !== ConnectionStatus.RunningLoaded) { 35 | vscode.window.showInformationMessage('Puppet Resource is not available as the Language Server is not ready'); 36 | return; 37 | } 38 | 39 | this.pickPuppetResource().then((moduleName) => { 40 | if (moduleName) { 41 | const editor = vscode.window.activeTextEditor; 42 | if (!editor) { 43 | return; 44 | } 45 | 46 | const doc = editor.document; 47 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 48 | const requestParams = new RequestParams(); 49 | requestParams.typename = moduleName; 50 | 51 | // Calculate where the progress message should go, if at all. 52 | const currentSettings: ISettings = settingsFromWorkspace(); 53 | let notificationType = vscode.ProgressLocation.Notification; 54 | if (currentSettings.notification !== undefined && currentSettings.notification.puppetResource !== undefined) { 55 | switch (currentSettings.notification.puppetResource.toLowerCase()) { 56 | case 'messagebox': 57 | notificationType = vscode.ProgressLocation.Notification; 58 | break; 59 | case 'statusbar': 60 | notificationType = vscode.ProgressLocation.Window; 61 | break; 62 | case 'none': 63 | notificationType = undefined; 64 | break; 65 | default: 66 | break; // Default is already set 67 | } 68 | } 69 | 70 | if (notificationType !== undefined) { 71 | vscode.window.withProgress( 72 | { 73 | location: notificationType, 74 | title: 'Puppet', 75 | cancellable: false, 76 | }, 77 | (progress) => { 78 | progress.report({ message: `Gathering Puppet ${moduleName} Resources` }); 79 | return this._connectionHandler.languageClient 80 | .sendRequest(PuppetResourceRequest.type, requestParams) 81 | .then((resourceResult) => { 82 | this.respsonseToVSCodeEdit(resourceResult, editor, doc); 83 | }); 84 | }, 85 | ); 86 | } else { 87 | this._connectionHandler.languageClient 88 | .sendRequest(PuppetResourceRequest.type, requestParams) 89 | .then((resourceResult) => { 90 | this.respsonseToVSCodeEdit(resourceResult, editor, doc); 91 | }); 92 | } 93 | } 94 | }); 95 | } 96 | 97 | private respsonseToVSCodeEdit( 98 | resourceResult: PuppetResourceResponse, 99 | editor: vscode.TextEditor, 100 | doc: vscode.TextDocument, 101 | ) { 102 | if (resourceResult.error !== undefined && resourceResult.error.length > 0) { 103 | this.logger.error(resourceResult.error); 104 | return; 105 | } 106 | if (resourceResult.data === undefined || resourceResult.data.length === 0) { 107 | return; 108 | } 109 | 110 | if (!editor) { 111 | return; 112 | } 113 | 114 | let newPosition = new vscode.Position(0, 0); 115 | if (editor.selection.isEmpty) { 116 | const position = editor.selection.active; 117 | newPosition = position.with(position.line, 0); 118 | } 119 | 120 | this.editCurrentDocument(doc.uri, resourceResult.data, newPosition); 121 | if (reporter) { 122 | reporter.sendTelemetryEvent(PuppetCommandStrings.puppetResourceCommandId); 123 | } 124 | } 125 | 126 | private pickPuppetResource(): Thenable { 127 | const options: vscode.QuickPickOptions = { 128 | placeHolder: 'Enter a Puppet resource to interrogate', 129 | matchOnDescription: true, 130 | matchOnDetail: true, 131 | }; 132 | return vscode.window.showInputBox(options); 133 | } 134 | 135 | private editCurrentDocument(uri: vscode.Uri, text: string, position: vscode.Position) { 136 | const edit = new vscode.WorkspaceEdit(); 137 | edit.insert(uri, position, text); 138 | vscode.workspace.applyEdit(edit); 139 | } 140 | } 141 | 142 | class RequestParams implements PuppetResourceRequestParams { 143 | // tslint complains that these properties have 'no initializer and is not definitely assigned in the constructor.' 144 | // following any of the fixes suggested breaks the language server, so disabling the rule here 145 | // and will make a ticket to work on this with lang server 146 | typename: string; // tslint:disable-line 147 | title: string; // tslint:disable-line 148 | } 149 | -------------------------------------------------------------------------------- /src/feature/PuppetStatusBarFeature.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-use-before-define */ 2 | 'use strict'; 3 | 4 | import * as vscode from 'vscode'; 5 | import { IAggregateConfiguration } from '../configuration'; 6 | import { IFeature } from '../feature'; 7 | import { ConnectionStatus } from '../interfaces'; 8 | import { ILogger } from '../logging'; 9 | import { PuppetCommandStrings } from '../messages'; 10 | import { ProtocolType } from '../settings'; 11 | 12 | class PuppetStatusBarProvider { 13 | private statusBarItem: vscode.StatusBarItem; 14 | private logger: ILogger; 15 | private config: IAggregateConfiguration; 16 | 17 | constructor(langIDs: string[], config: IAggregateConfiguration, logger: ILogger) { 18 | this.logger = logger; 19 | this.config = config; 20 | this.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 1); 21 | this.statusBarItem.command = PuppetCommandStrings.puppetShowConnectionMenuCommandId; 22 | this.statusBarItem.show(); 23 | 24 | vscode.window.onDidChangeActiveTextEditor((textEditor) => { 25 | if (textEditor === undefined || langIDs.indexOf(textEditor.document.languageId) === -1) { 26 | this.statusBarItem.hide(); 27 | } else { 28 | this.statusBarItem.show(); 29 | } 30 | }); 31 | } 32 | 33 | public setConnectionStatus(statusText: string, status: ConnectionStatus, toolTip: string): void { 34 | // Icons are from https://octicons.github.com/ 35 | let statusIconText: string; 36 | let statusColor: string; 37 | 38 | switch (status) { 39 | case ConnectionStatus.RunningLoaded: 40 | statusIconText = '$(terminal) '; 41 | statusColor = '#affc74'; 42 | break; 43 | case ConnectionStatus.RunningLoading: 44 | // When the editor service is starting, it's functional but it may be missing 45 | // type/class/function/fact info. But language only features like format document 46 | // or document symbol, are available 47 | statusIconText = '$(sync~spin) '; 48 | statusColor = '#affc74'; 49 | break; 50 | case ConnectionStatus.Failed: 51 | statusIconText = '$(alert) '; 52 | statusColor = '#fcc174'; 53 | break; 54 | default: 55 | // ConnectionStatus.NotStarted 56 | // ConnectionStatus.Starting 57 | // ConnectionStatus.Stopping 58 | statusIconText = '$(gear) '; 59 | statusColor = '#f3fc74'; 60 | break; 61 | } 62 | 63 | statusIconText = (statusIconText + statusText).trim(); 64 | this.statusBarItem.color = statusColor; 65 | // Using a conditional here because resetting a $(sync~spin) will cause the animation to restart. Instead 66 | // Only change the status bar text if it has actually changed. 67 | if (this.statusBarItem.text !== statusIconText) { 68 | this.statusBarItem.text = statusIconText; 69 | } 70 | this.statusBarItem.tooltip = toolTip; // TODO: killme (new Date()).getUTCDate().toString() + "\nNewline\nWee!"; 71 | } 72 | 73 | public showConnectionMenu() { 74 | const menuItems: PuppetConnectionMenuItem[] = []; 75 | 76 | menuItems.push( 77 | new PuppetConnectionMenuItem('Show Puppet Session Logs', () => { 78 | vscode.commands.executeCommand(PuppetCommandStrings.puppetShowConnectionLogsCommandId); 79 | }), 80 | ); 81 | 82 | if ( 83 | this.config.ruby.pdkPuppetVersions !== undefined && 84 | this.config.ruby.pdkPuppetVersions.length > 0 && 85 | this.config.connection.protocol !== ProtocolType.TCP 86 | ) { 87 | // Add a static menu item to use the latest version 88 | menuItems.push( 89 | new PuppetConnectionMenuItem('Switch to latest Puppet version', () => { 90 | vscode.commands.executeCommand(PuppetCommandStrings.puppetUpdateConfigurationCommandId, { 91 | /* eslint-disable-next-line @typescript-eslint/naming-convention */ 92 | 'puppet.editorService.puppet.version': undefined, 93 | }); 94 | }), 95 | ); 96 | this.config.ruby.pdkPuppetVersions 97 | .sort((a, b) => b.localeCompare(a, undefined, { numeric: true })) // Reverse sort 98 | .forEach((puppetVersion) => { 99 | menuItems.push( 100 | new PuppetConnectionMenuItem('Switch to Puppet ' + puppetVersion.toString(), () => { 101 | vscode.commands.executeCommand(PuppetCommandStrings.puppetUpdateConfigurationCommandId, { 102 | /* eslint-disable-next-line @typescript-eslint/naming-convention */ 103 | 'puppet.editorService.puppet.version': puppetVersion, 104 | }); 105 | }), 106 | ); 107 | }); 108 | } 109 | 110 | vscode.window.showQuickPick(menuItems).then((selectedItem) => { 111 | if (selectedItem) { 112 | selectedItem.callback(); 113 | } 114 | }); 115 | } 116 | } 117 | 118 | class PuppetConnectionMenuItem implements vscode.QuickPickItem { 119 | public description = ''; 120 | 121 | // eslint-disable-next-line @typescript-eslint/no-empty-function 122 | constructor(public readonly label: string, public readonly callback: () => void = () => {}) {} 123 | } 124 | 125 | export interface IPuppetStatusBar { 126 | setConnectionStatus(statusText: string, status: ConnectionStatus, toolTip: string); 127 | } 128 | 129 | export class PuppetStatusBarFeature implements IFeature, IPuppetStatusBar { 130 | private provider: PuppetStatusBarProvider; 131 | 132 | constructor(langIDs: string[], config: IAggregateConfiguration, logger: ILogger, context: vscode.ExtensionContext) { 133 | context.subscriptions.push( 134 | vscode.commands.registerCommand(PuppetCommandStrings.puppetShowConnectionMenuCommandId, () => { 135 | this.provider.showConnectionMenu(); 136 | }), 137 | ); 138 | this.provider = new PuppetStatusBarProvider(langIDs, config, logger); 139 | } 140 | 141 | public setConnectionStatus(statusText: string, status: ConnectionStatus, toolTip: string): void { 142 | this.provider.setConnectionStatus(statusText, status, toolTip); 143 | } 144 | 145 | public dispose(): any { 146 | return undefined; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/feature/PuppetfileCompletionFeature.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { IFeature } from '../feature'; 3 | import { getPuppetModuleCompletion } from '../forge'; 4 | import { ILogger } from '../logging'; 5 | 6 | export class PuppetfileCompletionProvider implements vscode.CompletionItemProvider { 7 | constructor(public logger: ILogger) {} 8 | async provideCompletionItems( 9 | document: vscode.TextDocument, 10 | position: vscode.Position, 11 | token: vscode.CancellationToken, 12 | context: vscode.CompletionContext, 13 | ): Promise { 14 | // get all text until the `position` and check if it reads `mod.` 15 | const linePrefix = document.lineAt(position).text.substr(0, position.character); 16 | if (!linePrefix.startsWith('mod')) { 17 | return undefined; 18 | } 19 | 20 | const completionText = document.getText(document.getWordRangeAtPosition(position)); 21 | 22 | const data = await getPuppetModuleCompletion(completionText, this.logger); 23 | 24 | const l: vscode.CompletionList = new vscode.CompletionList(); 25 | data.modules.forEach((element) => { 26 | l.items.push(new vscode.CompletionItem(element, vscode.CompletionItemKind.Module)); 27 | }); 28 | 29 | return l; 30 | } 31 | 32 | resolveCompletionItem?( 33 | item: vscode.CompletionItem, 34 | token: vscode.CancellationToken, 35 | ): vscode.ProviderResult { 36 | throw new Error('Method not implemented.'); 37 | } 38 | } 39 | 40 | export class PuppetfileCompletionFeature implements IFeature { 41 | constructor(public context: vscode.ExtensionContext, public logger: ILogger) { 42 | const selector = [{ scheme: 'file', language: 'puppetfile' }]; 43 | context.subscriptions.push( 44 | vscode.languages.registerCompletionItemProvider(selector, new PuppetfileCompletionProvider(logger)), 45 | ); 46 | } 47 | 48 | // eslint-disable-next-line @typescript-eslint/no-empty-function 49 | dispose() {} 50 | } 51 | -------------------------------------------------------------------------------- /src/feature/PuppetfileHoverFeature.ts: -------------------------------------------------------------------------------- 1 | // const axios = require('axios'); 2 | import * as vscode from 'vscode'; 3 | import { IFeature } from '../feature'; 4 | import { buildMarkdown, getModuleInfo } from '../forge'; 5 | import { ILogger } from '../logging'; 6 | import { reporter } from '../telemetry'; 7 | 8 | class PuppetfileHoverProvider implements vscode.HoverProvider { 9 | constructor(public readonly logger: ILogger) {} 10 | 11 | async provideHover( 12 | document: vscode.TextDocument, 13 | position: vscode.Position, 14 | token: vscode.CancellationToken, 15 | ): Promise { 16 | if (token.isCancellationRequested) { 17 | return null; 18 | } 19 | 20 | const range = document.getWordRangeAtPosition(position); 21 | const line = document.lineAt(position); 22 | if (line.isEmptyOrWhitespace) { 23 | return null; 24 | } 25 | 26 | if (reporter) { 27 | reporter.sendTelemetryEvent('puppetfile/Hover'); 28 | } 29 | 30 | const text = line.text 31 | .replace(new RegExp('mod\\s+'), '') 32 | .replace(new RegExp(",\\s+'\\d.\\d.\\d\\'"), '') 33 | .replace(new RegExp(',\\s+:latest'), '') 34 | .replace("/", '-') 35 | .replace("'", '') 36 | .replace("'", ''); 37 | 38 | const info = await getModuleInfo(text, this.logger); 39 | const markdown = buildMarkdown(info); 40 | const hoverinfo = new vscode.Hover(markdown, range); 41 | return hoverinfo; 42 | } 43 | } 44 | 45 | export class PuppetfileHoverFeature implements IFeature { 46 | constructor(public context: vscode.ExtensionContext, public logger: ILogger) { 47 | context.subscriptions.push( 48 | vscode.languages.registerHoverProvider('puppetfile', new PuppetfileHoverProvider(logger)), 49 | ); 50 | } 51 | 52 | // eslint-disable-next-line @typescript-eslint/no-empty-function 53 | dispose(): void {} 54 | } 55 | -------------------------------------------------------------------------------- /src/feature/UpdateConfigurationFeature.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as vscode from 'vscode'; 4 | import { IFeature } from '../feature'; 5 | import { ILogger } from '../logging'; 6 | import { PuppetCommandStrings } from '../messages'; 7 | 8 | export class UpdateConfigurationFeature implements IFeature { 9 | private logger: ILogger; 10 | private settingsRequireRestart = ['puppet.editorService.puppet.version']; 11 | 12 | private async updateSettingsAsync(updateSettingsHash) { 13 | // If there are no workspace folders then we've just opened a puppet file. Therefore we can't updated the workspace folder settings, so we need to update 14 | // the global configuration instead. 15 | const configTarget = 16 | vscode.workspace.workspaceFolders === undefined || vscode.workspace.workspaceFolders.length === 0 17 | ? vscode.ConfigurationTarget.Global 18 | : null; 19 | let requiresRestart = false; 20 | 21 | await Object.keys(updateSettingsHash).forEach((key) => { 22 | requiresRestart = requiresRestart || this.settingsRequireRestart.includes(key); 23 | const value = updateSettingsHash[key]; 24 | const config = vscode.workspace.getConfiguration(); 25 | this.logger.debug('Updating configuration item ' + key + " to '" + value + "'"); 26 | config.update(key, value, configTarget); 27 | }); 28 | 29 | if (requiresRestart) { 30 | vscode.window 31 | .showInformationMessage( 32 | 'Puppet extensions needs to restart the editor. Would you like to do that now?', 33 | { modal: false }, 34 | ...['Yes', 'No'], 35 | ) 36 | .then((selection) => { 37 | if (selection === 'Yes') { 38 | vscode.commands.executeCommand('workbench.action.reloadWindow'); 39 | } 40 | }); 41 | } 42 | } 43 | 44 | constructor(logger: ILogger, context: vscode.ExtensionContext) { 45 | this.logger = logger; 46 | context.subscriptions.push( 47 | vscode.commands.registerCommand(PuppetCommandStrings.puppetUpdateConfigurationCommandId, (updateSettingsHash) => { 48 | this.updateSettingsAsync(updateSettingsHash); 49 | }), 50 | ); 51 | } 52 | 53 | public dispose(): any { 54 | return undefined; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/forge.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import axios from 'axios'; 3 | import { extensions, MarkdownString } from 'vscode'; 4 | import { ILogger } from './logging'; 5 | 6 | export interface PuppetForgeCompletionInfo { 7 | total: number; 8 | modules: string[]; 9 | } 10 | 11 | export interface PuppetForgeModuleInfo { 12 | uri: string; 13 | slug: string; 14 | name: string; 15 | downloads: number; 16 | score: number; 17 | created: Date; 18 | updated: Date; 19 | endorsement: string; 20 | owner: { slug: string; username: string }; 21 | forgeUrl: string; 22 | homepageUrl: string; 23 | version: number; 24 | summary: string; 25 | } 26 | 27 | function getVersion(): string { 28 | const pkg = extensions.getExtension('puppet.puppet-vscode'); 29 | return pkg.packageJSON.version; 30 | } 31 | 32 | export function buildMarkdown(info: PuppetForgeModuleInfo): MarkdownString { 33 | const message = `## ${info.name}\n 34 | ${info.summary}\n 35 | **Latest version:** ${info.version} (${info.updated.toDateString()})\n 36 | **Forge**: [${info.forgeUrl}](${info.forgeUrl})\n 37 | **Project**: [${info.homepageUrl}](${info.homepageUrl})\n 38 | **Owner:** ${info.owner.username}\n 39 | **Endorsement:** ${info.endorsement?.toLocaleUpperCase()}\n 40 | **Score:** ${info.score}\n 41 | `; 42 | return new MarkdownString(message); 43 | } 44 | 45 | export function getPDKVersion(logger: ILogger): Promise { 46 | return new Promise((resolve) => { 47 | return axios 48 | .get('https://s3.amazonaws.com/puppet-pdk/pdk/LATEST', { 49 | params: { 50 | exclude_fields: 'readme changelog license reference', 51 | }, 52 | headers: { 53 | 'Content-Type': 'application/json;charset=UTF-8', 54 | 'User-Agent': `puppet-vscode/${getVersion()}`, 55 | }, 56 | }) 57 | .then((response) => { 58 | if (response.status !== 200) { 59 | logger.error(`Error getting Puppet forge data. Status: ${response.status}:${response.statusText}`); 60 | resolve(undefined); 61 | } 62 | resolve(response.data); 63 | }); 64 | }); 65 | } 66 | 67 | export function getModuleInfo(title: string, logger: ILogger): Promise { 68 | return new Promise((resolve) => { 69 | return axios 70 | .get(`https://forgeapi.puppet.com/v3/modules/${title}`, { 71 | params: { 72 | exclude_fields: 'readme changelog license reference', 73 | }, 74 | headers: { 75 | 'Content-Type': 'application/json;charset=UTF-8', 76 | 'User-Agent': `puppet-vscode/${getVersion()}`, 77 | }, 78 | }) 79 | .then((response) => { 80 | if (response.status !== 200) { 81 | logger.error(`Error getting Puppet forge data. Status: ${response.status}:${response.statusText}`); 82 | resolve(undefined); 83 | } 84 | 85 | const info = response.data; 86 | const module = { 87 | uri: info.uri, 88 | slug: info.slug, 89 | name: info.name, 90 | downloads: info.downloads, 91 | score: info.feedback_score, 92 | created: new Date(info.created_at), 93 | updated: new Date(info.updated_at), 94 | endorsement: info.endorsement ?? '', 95 | forgeUrl: `https://forge.puppet.com/modules/${info.owner.username}/${info.name}`, 96 | homepageUrl: info.homepage_url ?? '', 97 | version: info.current_release.version, 98 | owner: { 99 | uri: info.owner.uri, 100 | slug: info.owner.slug, 101 | username: info.owner.username, 102 | gravatar: info.owner.gravatar_id, 103 | }, 104 | summary: info.current_release.metadata.summary, 105 | }; 106 | 107 | resolve(module); 108 | }) 109 | .catch((error) => { 110 | logger.error(`Error getting Puppet forge data: ${error}`); 111 | resolve(undefined); 112 | }); 113 | }); 114 | } 115 | 116 | export function getPuppetModuleCompletion(text: string, logger: ILogger): Promise { 117 | return new Promise((resolve) => { 118 | return axios 119 | .get(`https://forgeapi.puppet.com/private/modules?starts_with=${text}`, { 120 | params: { 121 | exclude_fields: 'readme changelog license reference', 122 | }, 123 | headers: { 124 | 'Content-Type': 'application/json;charset=UTF-8', 125 | 'User-Agent': `puppet-vscode/${getVersion()}`, 126 | }, 127 | }) 128 | .then((response) => { 129 | if (response.status !== 200) { 130 | logger.error(`Error getting Puppet forge data. Status: ${response.status}:${response.statusText}`); 131 | resolve(undefined); 132 | } 133 | 134 | const info = response.data; 135 | const results: string[] = info.results as string[]; 136 | const data = { 137 | total: parseInt(info.total), 138 | modules: results, 139 | }; 140 | 141 | resolve(data); 142 | }) 143 | .catch((error) => { 144 | logger.error(`Error getting Puppet forge data: ${error}`); 145 | resolve(undefined); 146 | }); 147 | }); 148 | } 149 | -------------------------------------------------------------------------------- /src/handler.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { 3 | LanguageClient, 4 | LanguageClientOptions, 5 | RevealOutputChannelOn, 6 | ServerOptions, 7 | } from 'vscode-languageclient/node'; 8 | 9 | import { IAggregateConfiguration } from './configuration'; 10 | import { IPuppetStatusBar } from './feature/PuppetStatusBarFeature'; 11 | import { ConnectionStatus } from './interfaces'; 12 | import { OutputChannelLogger } from './logging/outputchannel'; 13 | import { PuppetCommandStrings, PuppetVersionDetails, PuppetVersionRequest } from './messages'; 14 | import { ConnectionType, ProtocolType } from './settings'; 15 | import { reporter } from './telemetry'; 16 | 17 | export abstract class ConnectionHandler { 18 | private timeSpent: number; 19 | 20 | private _status: ConnectionStatus; 21 | public get status(): ConnectionStatus { 22 | return this._status; 23 | } 24 | 25 | private _languageClient: LanguageClient; 26 | public get languageClient(): LanguageClient { 27 | return this._languageClient; 28 | } 29 | 30 | abstract get connectionType(): ConnectionType; 31 | 32 | public get protocolType(): ProtocolType { 33 | return this.config.workspace.editorService.protocol; 34 | } 35 | 36 | protected constructor( 37 | protected context: vscode.ExtensionContext, 38 | protected statusBar: IPuppetStatusBar, 39 | protected logger: OutputChannelLogger, 40 | protected config: IAggregateConfiguration, 41 | protected readonly puppetLangID: string, 42 | protected readonly puppetFileLangID: string, 43 | ) { 44 | this.timeSpent = Date.now(); 45 | this.setConnectionStatus('Initializing', ConnectionStatus.Initializing); 46 | 47 | const documents = [ 48 | { scheme: 'file', language: puppetLangID }, 49 | { scheme: 'file', language: puppetFileLangID }, 50 | ]; 51 | 52 | this.logger.debug('Configuring language client options'); 53 | const clientOptions: LanguageClientOptions = { 54 | documentSelector: documents, 55 | outputChannel: this.logger.logChannel, 56 | revealOutputChannelOn: RevealOutputChannelOn.Info, 57 | }; 58 | 59 | this.logger.debug('Creating server options'); 60 | const serverOptions = this.createServerOptions(); 61 | 62 | this.logger.debug('Creating language client'); 63 | this._languageClient = new LanguageClient('PuppetVSCode', serverOptions, clientOptions); 64 | this._languageClient 65 | .start() 66 | .then( 67 | () => { 68 | this.languageClient.onTelemetry((event) => { 69 | const eventName = event.Name ? event.Name : 'PUPPET_LANGUAGESERVER_EVENT'; 70 | reporter.sendTelemetryEvent(eventName, event.Measurements, event.Properties); 71 | }); 72 | this.setConnectionStatus('Loading Puppet', ConnectionStatus.Starting); 73 | this.queryLanguageServerStatusWithProgress(); 74 | }, 75 | (reason) => { 76 | this.setConnectionStatus('Starting error', ConnectionStatus.Starting); 77 | this.languageClient.error(reason); 78 | reporter.sendTelemetryErrorEvent(reason); 79 | }, 80 | ) 81 | .catch((reason) => { 82 | this.setConnectionStatus('Failure', ConnectionStatus.Failed); 83 | reporter.sendTelemetryErrorEvent(reason); 84 | }); 85 | this.setConnectionStatus('Initialization Complete', ConnectionStatus.InitializationComplete); 86 | 87 | this.context.subscriptions.push( 88 | vscode.commands.registerCommand(PuppetCommandStrings.puppetShowConnectionLogsCommandId, () => { 89 | this.logger.show(); 90 | }), 91 | ); 92 | } 93 | 94 | abstract createServerOptions(): ServerOptions; 95 | abstract cleanup(): void; 96 | 97 | start(): void { 98 | this.setConnectionStatus('Starting languageserver', ConnectionStatus.Starting, ''); 99 | this.languageClient.start().then(() => { 100 | this.context.subscriptions.push({ 101 | dispose: () => this.languageClient.stop(), 102 | }); 103 | }); 104 | } 105 | 106 | stop(): void { 107 | this.setConnectionStatus('Stopping languageserver', ConnectionStatus.Stopping, ''); 108 | if (this.languageClient !== undefined) { 109 | this.timeSpent = Date.now() - this.timeSpent; 110 | this._languageClient.sendRequest(PuppetVersionRequest.type).then((versionDetails) => { 111 | reporter.sendTelemetryEvent('data', { 112 | timeSpent: this.timeSpent.toString(), 113 | puppetVersion: versionDetails.puppetVersion, 114 | facterVersion: versionDetails.facterVersion, 115 | languageServerVersion: versionDetails.languageServerVersion, 116 | }); 117 | }); 118 | this.languageClient.stop(); 119 | } 120 | 121 | this.logger.debug('Running cleanup'); 122 | this.cleanup(); 123 | this.setConnectionStatus('Stopped languageserver', ConnectionStatus.Stopped, ''); 124 | } 125 | 126 | public setConnectionStatus(message: string, status: ConnectionStatus, toolTip?: string) { 127 | this._status = status; 128 | this.statusBar.setConnectionStatus(message, status, toolTip); 129 | } 130 | 131 | private queryLanguageServerStatusWithProgress() { 132 | return new Promise((resolve, reject) => { 133 | let count = 0; 134 | let lastVersionResponse: PuppetVersionDetails; 135 | const handle = setInterval(() => { 136 | count++; 137 | 138 | // After 30 seonds timeout the progress 139 | if (count >= 30 || this._languageClient === undefined) { 140 | clearInterval(handle); 141 | this.setConnectionStatus(lastVersionResponse.puppetVersion, ConnectionStatus.RunningLoaded, ''); 142 | resolve(undefined); 143 | return; 144 | } 145 | 146 | this._languageClient.sendRequest(PuppetVersionRequest.type).then((versionDetails) => { 147 | lastVersionResponse = versionDetails; 148 | if ( 149 | versionDetails.factsLoaded && 150 | versionDetails.functionsLoaded && 151 | versionDetails.typesLoaded && 152 | versionDetails.classesLoaded 153 | ) { 154 | clearInterval(handle); 155 | this.setConnectionStatus(lastVersionResponse.puppetVersion, ConnectionStatus.RunningLoaded, ''); 156 | resolve(undefined); 157 | } else { 158 | let toolTip = ''; 159 | 160 | toolTip += versionDetails.classesLoaded ? '✔ Classes: Loaded\n' : '⏳ Classes: Loading...\n'; 161 | toolTip += versionDetails.factsLoaded ? '✔ Facts: Loaded\n' : '⏳ Facts: Loading...\n'; 162 | toolTip += versionDetails.functionsLoaded ? '✔ Functions: Loaded\n' : '⏳ Functions: Loading...\n'; 163 | toolTip += versionDetails.typesLoaded ? '✔ Types: Loaded' : '⏳ Types: Loading...'; 164 | 165 | this.setConnectionStatus(lastVersionResponse.puppetVersion, ConnectionStatus.RunningLoading, toolTip); 166 | } 167 | }); 168 | }, 1000); 169 | }); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/handlers/stdio.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Executable, ServerOptions } from 'vscode-languageclient/node'; 3 | import { IAggregateConfiguration } from '../configuration'; 4 | import { IPuppetStatusBar } from '../feature/PuppetStatusBarFeature'; 5 | import { ConnectionHandler } from '../handler'; 6 | import { CommandEnvironmentHelper } from '../helpers/commandHelper'; 7 | import { OutputChannelLogger } from '../logging/outputchannel'; 8 | import { ConnectionType, PuppetInstallType } from '../settings'; 9 | 10 | export class StdioConnectionHandler extends ConnectionHandler { 11 | get connectionType(): ConnectionType { 12 | return ConnectionType.Local; 13 | } 14 | 15 | constructor( 16 | context: vscode.ExtensionContext, 17 | statusBar: IPuppetStatusBar, 18 | logger: OutputChannelLogger, 19 | config: IAggregateConfiguration, 20 | puppetLangID: string, 21 | puppetFileLangID: string, 22 | ) { 23 | super(context, statusBar, logger, config, puppetLangID, puppetFileLangID); 24 | this.logger.debug(`Configuring ${ConnectionType[this.connectionType]}::${this.protocolType} connection handler`); 25 | this.start(); 26 | } 27 | 28 | createServerOptions(): ServerOptions { 29 | const exe: Executable = CommandEnvironmentHelper.getLanguageServerRubyEnvFromConfiguration( 30 | this.context.asAbsolutePath(this.config.ruby.languageServerPath), 31 | this.config, 32 | ); 33 | 34 | let logPrefix = ''; 35 | // eslint-disable-next-line default-case 36 | switch (this.config.workspace.installType) { 37 | case PuppetInstallType.PDK: 38 | logPrefix = '[getRubyEnvFromPDK] '; 39 | this.logger.debug(logPrefix + 'Using environment variable DEVKIT_BASEDIR=' + exe.options.env.DEVKIT_BASEDIR); 40 | this.logger.debug(logPrefix + 'Using environment variable GEM_HOME=' + exe.options.env.GEM_HOME); 41 | this.logger.debug(logPrefix + 'Using environment variable GEM_PATH=' + exe.options.env.GEM_PATH); 42 | break; 43 | case PuppetInstallType.PUPPET: 44 | logPrefix = '[getRubyExecFromPuppetAgent] '; 45 | this.logger.debug(logPrefix + 'Using environment variable SSL_CERT_FILE=' + exe.options.env.SSL_CERT_FILE); 46 | this.logger.debug(logPrefix + 'Using environment variable SSL_CERT_DIR=' + exe.options.env.SSL_CERT_DIR); 47 | break; 48 | } 49 | 50 | this.logger.debug(logPrefix + 'Using environment variable RUBY_DIR=' + exe.options.env.RUBY_DIR); 51 | this.logger.debug(logPrefix + 'Using environment variable RUBYLIB=' + exe.options.env.RUBYLIB); 52 | this.logger.debug(logPrefix + 'Using environment variable PATH=' + exe.options.env.PATH); 53 | this.logger.debug(logPrefix + 'Using environment variable RUBYOPT=' + exe.options.env.RUBYOPT); 54 | this.logger.debug(logPrefix + 'Editor Services will invoke with: ' + exe.command + ' ' + exe.args.join(' ')); 55 | 56 | const serverOptions: ServerOptions = { 57 | run: exe, 58 | debug: exe, 59 | }; 60 | 61 | return serverOptions; 62 | } 63 | 64 | cleanup(): void { 65 | this.logger.debug(`No cleanup needed for ${this.protocolType}`); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/handlers/tcp.ts: -------------------------------------------------------------------------------- 1 | import * as cp from 'child_process'; 2 | import * as net from 'net'; 3 | import * as vscode from 'vscode'; 4 | import { Executable, ServerOptions, StreamInfo } from 'vscode-languageclient/node'; 5 | import { IAggregateConfiguration } from '../configuration'; 6 | import { IPuppetStatusBar } from '../feature/PuppetStatusBarFeature'; 7 | import { ConnectionHandler } from '../handler'; 8 | import { CommandEnvironmentHelper } from '../helpers/commandHelper'; 9 | import { OutputChannelLogger } from '../logging/outputchannel'; 10 | import { ConnectionType, ProtocolType, PuppetInstallType } from '../settings'; 11 | 12 | export class TcpConnectionHandler extends ConnectionHandler { 13 | constructor( 14 | context: vscode.ExtensionContext, 15 | statusBar: IPuppetStatusBar, 16 | logger: OutputChannelLogger, 17 | config: IAggregateConfiguration, 18 | puppetLangID: string, 19 | puppetFileLangID: string, 20 | ) { 21 | super(context, statusBar, logger, config, puppetLangID, puppetFileLangID); 22 | this.logger.debug(`Configuring ${ConnectionType[this.connectionType]}::${this.protocolType} connection handler`); 23 | 24 | if (this.connectionType === ConnectionType.Local) { 25 | const exe: Executable = CommandEnvironmentHelper.getLanguageServerRubyEnvFromConfiguration( 26 | this.context.asAbsolutePath(this.config.ruby.languageServerPath), 27 | this.config, 28 | ); 29 | 30 | let logPrefix = ''; 31 | // eslint-disable-next-line default-case 32 | switch (this.config.workspace.installType) { 33 | case PuppetInstallType.PDK: 34 | logPrefix = '[getRubyEnvFromPDK] '; 35 | break; 36 | case PuppetInstallType.PUPPET: 37 | logPrefix = '[getRubyExecFromPuppetAgent] '; 38 | break; 39 | } 40 | 41 | this.logger.debug(logPrefix + 'Using environment variable RUBY_DIR=' + exe.options.env.RUBY_DIR); 42 | this.logger.debug(logPrefix + 'Using environment variable PATH=' + exe.options.env.PATH); 43 | this.logger.debug(logPrefix + 'Using environment variable RUBYLIB=' + exe.options.env.RUBYLIB); 44 | this.logger.debug(logPrefix + 'Using environment variable RUBYOPT=' + exe.options.env.RUBYOPT); 45 | this.logger.debug(logPrefix + 'Using environment variable SSL_CERT_FILE=' + exe.options.env.SSL_CERT_FILE); 46 | this.logger.debug(logPrefix + 'Using environment variable SSL_CERT_DIR=' + exe.options.env.SSL_CERT_DIR); 47 | this.logger.debug(logPrefix + 'Using environment variable GEM_PATH=' + exe.options.env.GEM_PATH); 48 | this.logger.debug(logPrefix + 'Using environment variable GEM_HOME=' + exe.options.env.GEM_HOME); 49 | 50 | const spawnOptions: cp.SpawnOptions = {}; 51 | const convertedOptions = Object.assign(spawnOptions, exe.options); 52 | 53 | this.logger.debug(logPrefix + 'Editor Services will invoke with: ' + exe.command + ' ' + exe.args.join(' ')); 54 | const proc = cp.spawn(exe.command, exe.args, convertedOptions); 55 | proc.stdout.on('data', (data) => { 56 | if (/LANGUAGE SERVER RUNNING/.test(data.toString())) { 57 | const p = data.toString().match(/LANGUAGE SERVER RUNNING.*:(\d+)/); 58 | config.workspace.editorService.tcp.port = Number(p[1]); 59 | this.start(); 60 | } 61 | }); 62 | proc.on('close', (exitCode) => { 63 | this.logger.debug('SERVER terminated with exit code: ' + exitCode); 64 | }); 65 | if (!proc || !proc.pid) { 66 | throw new Error(`Launching server using command ${exe.command} failed.`); 67 | } 68 | } else { 69 | this.start(); 70 | } 71 | } 72 | 73 | get connectionType(): ConnectionType { 74 | switch (this.config.workspace.editorService.protocol) { 75 | case ProtocolType.TCP: 76 | if ( 77 | this.config.workspace.editorService.tcp.address === '127.0.0.1' || 78 | this.config.workspace.editorService.tcp.address === 'localhost' || 79 | this.config.workspace.editorService.tcp.address === '' 80 | ) { 81 | return ConnectionType.Local; 82 | } else { 83 | return ConnectionType.Remote; 84 | } 85 | default: 86 | // In this case we have no idea what the type is 87 | return undefined; 88 | } 89 | } 90 | 91 | createServerOptions(): ServerOptions { 92 | this.logger.debug( 93 | `Starting language server client (host ${this.config.workspace.editorService.tcp.address} port ${this.config.workspace.editorService.tcp.port})`, 94 | ); 95 | 96 | const serverOptions = () => { 97 | const socket = new net.Socket(); 98 | 99 | socket.connect({ 100 | port: this.config.workspace.editorService.tcp.port, 101 | host: this.config.workspace.editorService.tcp.address, 102 | }); 103 | 104 | const result: StreamInfo = { 105 | writer: socket, 106 | reader: socket, 107 | }; 108 | 109 | return Promise.resolve(result); 110 | }; 111 | return serverOptions; 112 | } 113 | 114 | cleanup(): void { 115 | this.logger.debug(`No cleanup needed for ${this.protocolType}`); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Only add simple interfaces here. No import's allowed 4 | /* eslint-disable @typescript-eslint/naming-convention */ 5 | export enum ConnectionStatus { 6 | NotStarted, 7 | Starting, 8 | RunningLoading, 9 | RunningLoaded, 10 | Stopping, 11 | Failed, 12 | Stopped, 13 | Initializing, 14 | InitializationComplete, 15 | } 16 | -------------------------------------------------------------------------------- /src/logging.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-disable @typescript-eslint/naming-convention */ 3 | export enum LogLevel { 4 | Debug, 5 | Verbose, 6 | Normal, 7 | Warning, 8 | Error, 9 | } 10 | 11 | export interface ILogger { 12 | show(): void; 13 | verbose(message: string): void; 14 | debug(message: string): void; 15 | normal(message: string): void; 16 | warning(message: string): void; 17 | error(message: string): void; 18 | } 19 | -------------------------------------------------------------------------------- /src/logging/file.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as logging from '../logging'; 4 | import fs = require('fs'); 5 | 6 | export class FileLogger implements logging.ILogger { 7 | private logwriter: fs.WriteStream; 8 | // eslint-disable-next-line @typescript-eslint/no-empty-function 9 | public show() {} 10 | 11 | constructor(filename: string) { 12 | this.logwriter = fs.createWriteStream(filename); 13 | } 14 | 15 | public verbose(message: string) { 16 | this.emitMessage('VERBOSE', message); 17 | } 18 | public debug(message: string) { 19 | this.emitMessage('DEBUG', message); 20 | } 21 | public normal(message: string) { 22 | this.emitMessage('NORMAL', message); 23 | } 24 | public warning(message: string) { 25 | this.emitMessage('WARNING', message); 26 | } 27 | public error(message: string) { 28 | this.emitMessage('ERROR', message); 29 | } 30 | 31 | private emitMessage(severity: string, message: string) { 32 | if (this.logwriter.writable) { 33 | this.logwriter.write(severity + ': ' + message + '\n'); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/logging/null.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | 'use strict'; 3 | 4 | import * as logging from '../logging'; 5 | 6 | export class NullLogger implements logging.ILogger { 7 | public show() {} 8 | public verbose(message: string) {} 9 | public debug(message: string) {} 10 | public normal(message: string) {} 11 | public warning(message: string) {} 12 | public error(message: string) {} 13 | } 14 | -------------------------------------------------------------------------------- /src/logging/outputchannel.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as vscode from 'vscode'; 4 | import * as logging from '../logging'; 5 | import { ISettings } from '../settings'; 6 | 7 | export class OutputChannelLogger implements logging.ILogger { 8 | private _logChannel: vscode.OutputChannel; 9 | public get logChannel(): vscode.OutputChannel { 10 | return this._logChannel; 11 | } 12 | 13 | // Minimum log level that is shown to users on logChannel 14 | private minimumUserLogLevel: logging.LogLevel; 15 | 16 | constructor(logLevel: string) { 17 | this._logChannel = vscode.window.createOutputChannel('Puppet'); 18 | 19 | if (logLevel === undefined) { 20 | this.minimumUserLogLevel = logging.LogLevel.Normal; 21 | } else { 22 | this.minimumUserLogLevel = this.logLevelFromString(logLevel); 23 | } 24 | } 25 | 26 | public show() { 27 | this._logChannel.show(); 28 | } 29 | 30 | public verbose(message: string) { 31 | this.logWithLevel(logging.LogLevel.Verbose, message); 32 | } 33 | 34 | public debug(message: string) { 35 | this.logWithLevel(logging.LogLevel.Debug, message); 36 | } 37 | 38 | public normal(message: string) { 39 | this.logWithLevel(logging.LogLevel.Normal, message); 40 | } 41 | 42 | public warning(message: string) { 43 | this.logWithLevel(logging.LogLevel.Warning, message); 44 | } 45 | 46 | public error(message: string) { 47 | this.logWithLevel(logging.LogLevel.Error, message); 48 | } 49 | 50 | private logWithLevel(level: logging.LogLevel, message: string) { 51 | const logMessage = this.logLevelPrefixAsString(level) + new Date().toISOString() + ' ' + message; 52 | 53 | if (level >= this.minimumUserLogLevel) { 54 | this._logChannel.appendLine(logMessage); 55 | } 56 | } 57 | 58 | private logLevelFromString(logLevelName: string): logging.LogLevel { 59 | switch (logLevelName.toLowerCase()) { 60 | case 'verbose': 61 | return logging.LogLevel.Verbose; 62 | case 'debug': 63 | return logging.LogLevel.Debug; 64 | case 'normal': 65 | return logging.LogLevel.Normal; 66 | case 'warning': 67 | return logging.LogLevel.Warning; 68 | case 'error': 69 | return logging.LogLevel.Error; 70 | default: 71 | return logging.LogLevel.Error; 72 | } 73 | } 74 | 75 | private logLevelPrefixAsString(level: logging.LogLevel): string { 76 | switch (level) { 77 | case logging.LogLevel.Verbose: 78 | return 'VERBOSE: '; 79 | case logging.LogLevel.Debug: 80 | return 'DEBUG: '; 81 | case logging.LogLevel.Warning: 82 | return 'WARNING: '; 83 | case logging.LogLevel.Error: 84 | return 'ERROR: '; 85 | default: 86 | return ''; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/logging/stdout.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as logging from '../logging'; 4 | 5 | export class StandardOutLogger implements logging.ILogger { 6 | // eslint-disable-next-line @typescript-eslint/no-empty-function 7 | public show() {} 8 | 9 | public verbose(message: string) { 10 | this.emitMessage('VERBOSE', message); 11 | } 12 | public debug(message: string) { 13 | this.emitMessage('DEBUG', message); 14 | } 15 | public normal(message: string) { 16 | this.emitMessage('NORMAL', message); 17 | } 18 | public warning(message: string) { 19 | this.emitMessage('WARNING', message); 20 | } 21 | public error(message: string) { 22 | this.emitMessage('ERROR', message); 23 | } 24 | 25 | private emitMessage(severity: string, message: string) { 26 | if (process.stdout.writable) { 27 | process.stdout.write(severity + ': ' + message + '\n'); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/messages.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-namespace */ 2 | import { RequestType, RequestType0 } from 'vscode-languageclient/node'; 3 | 4 | export namespace PuppetVersionRequest { 5 | export const type = new RequestType0('puppet/getVersion'); 6 | } 7 | 8 | export interface PuppetVersionDetails { 9 | puppetVersion: string; 10 | facterVersion: string; 11 | languageServerVersion: string; 12 | factsLoaded: boolean; 13 | functionsLoaded: boolean; 14 | typesLoaded: boolean; 15 | classesLoaded: boolean; 16 | } 17 | 18 | export interface PuppetResourceRequestParams { 19 | typename: string; 20 | title: string; 21 | } 22 | 23 | export namespace PuppetResourceRequest { 24 | export const type = new RequestType('puppet/getResource'); 25 | } 26 | 27 | export interface PuppetResourceResponse { 28 | data: string; 29 | error: string; 30 | } 31 | 32 | export interface PuppetFixDiagnosticErrorsRequestParams { 33 | documentUri: string; 34 | alwaysReturnContent: boolean; 35 | } 36 | 37 | export namespace PuppetFixDiagnosticErrorsRequest { 38 | export const type = new RequestType('puppet/fixDiagnosticErrors'); 39 | } 40 | 41 | export interface PuppetFixDiagnosticErrorsResponse { 42 | documentUri: string; 43 | fixesApplied: number; 44 | newContent: string; 45 | } 46 | 47 | export namespace PuppetNodeGraphRequest { 48 | export const type = new RequestType('puppet/compileNodeGraph'); 49 | } 50 | 51 | export interface PuppetNodeGraphResponse { 52 | vertices: []; 53 | edges: []; 54 | error: string; 55 | } 56 | 57 | export namespace CompileNodeGraphRequest { 58 | export const type = new RequestType('puppet/compileNodeGraph'); 59 | } 60 | 61 | export interface CompileNodeGraphResponse { 62 | dotContent: string; 63 | error: string; 64 | data: string; 65 | } 66 | 67 | export class PuppetCommandStrings { 68 | static puppetResourceCommandId = 'extension.puppetResource'; 69 | static puppetShowConnectionMenuCommandId = 'extension.puppetShowConnectionMenu'; 70 | static puppetShowConnectionLogsCommandId = 'extension.puppetShowConnectionLogs'; 71 | static puppetUpdateConfigurationCommandId = 'extension.puppetUpdateConfiguration'; 72 | } 73 | 74 | export class PDKCommandStrings { 75 | static pdkNewModuleCommandId = 'extension.pdkNewModule'; 76 | static pdkNewClassCommandId = 'extension.pdkNewClass'; 77 | static pdkNewTaskCommandId = 'extension.pdkNewTask'; 78 | static pdkNewFactCommandId = 'extension.pdkNewFact'; 79 | static pdkNewFunctionCommandId = 'extension.pdkNewFunction'; 80 | static pdkNewDefinedTypeCommandId = 'extension.pdkNewDefinedType'; 81 | static pdkValidateCommandId = 'extension.pdkValidate'; 82 | static pdkTestUnitCommandId = 'extension.pdkTestUnit'; 83 | } 84 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import vscode = require('vscode'); 4 | /* eslint-disable @typescript-eslint/naming-convention */ 5 | export enum PuppetInstallType { 6 | PDK = 'pdk', 7 | PUPPET = 'agent', 8 | AUTO = 'auto', 9 | } 10 | 11 | export enum ProtocolType { 12 | UNKNOWN = '', 13 | STDIO = 'stdio', 14 | TCP = 'tcp', 15 | } 16 | 17 | export enum ConnectionType { 18 | Unknown, 19 | Local, 20 | Remote, 21 | } 22 | /* eslint-enable @typescript-eslint/naming-convention */ 23 | export interface IEditorServiceTCPSettings { 24 | address?: string; 25 | port?: number; 26 | } 27 | 28 | export interface IEditorServicePuppetSettings { 29 | confdir?: string; 30 | environment?: string; 31 | modulePath?: string; 32 | vardir?: string; 33 | version?: string; 34 | } 35 | 36 | export interface IEditorServiceSettings { 37 | debugFilePath?: string; 38 | enable?: boolean; 39 | featureFlags?: string[]; 40 | loglevel?: string; 41 | protocol?: ProtocolType; 42 | puppet?: IEditorServicePuppetSettings; 43 | tcp?: IEditorServiceTCPSettings; 44 | timeout?: number; 45 | } 46 | 47 | export interface IFormatSettings { 48 | enable?: boolean; 49 | } 50 | export interface IHoverSettings { 51 | showMetadataInfo?: boolean; 52 | // showPuppetfileInfo?: boolean; Future use 53 | } 54 | 55 | export interface ILintSettings { 56 | // Future Use 57 | enable?: boolean; // Future Use: Puppet Editor Services doesn't implement this yet. 58 | } 59 | 60 | export interface IPDKSettings { 61 | checkVersion?: boolean; 62 | } 63 | 64 | export interface INotificationSettings { 65 | nodeGraph?: string; 66 | puppetResource?: string; 67 | } 68 | 69 | export interface ISettings { 70 | editorService?: IEditorServiceSettings; 71 | format?: IFormatSettings; 72 | hover?: IHoverSettings; 73 | installDirectory?: string; 74 | installType?: PuppetInstallType; 75 | lint?: ILintSettings; 76 | notification?: INotificationSettings; 77 | pdk?: IPDKSettings; 78 | } 79 | 80 | const workspaceSectionName = 'puppet'; 81 | 82 | /** 83 | * Safely query the workspace configuration for a nested setting option. If it, or any part of the setting 84 | * path does not exist, return undefined 85 | * @param workspaceConfig The VScode workspace configuration to query 86 | * @param indexes An array of strings defining the setting path, e.g. The setting 'a.b.c' would pass indexes of ['a','b','c'] 87 | */ 88 | function getSafeWorkspaceConfig(workspaceConfig: vscode.WorkspaceConfiguration, indexes: string[]): any { 89 | if (indexes.length <= 0) { 90 | return undefined; 91 | } 92 | 93 | let index: string = indexes.shift(); 94 | let result: Record = workspaceConfig[index]; 95 | while (indexes.length > 0 && result !== undefined) { 96 | index = indexes.shift(); 97 | result = result[index]; 98 | } 99 | 100 | // A null settings is really undefined. 101 | if (result === null) { 102 | return undefined; 103 | } 104 | 105 | return result; 106 | } 107 | 108 | /** 109 | * Retrieves the list of "legacy" or deprecated setting names and their values 110 | */ 111 | export function legacySettings(): Map> { 112 | const workspaceConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(workspaceSectionName); 113 | 114 | const settings: Map> = new Map>(); 115 | const value: Record = undefined; 116 | 117 | // puppet.editorService.modulePath 118 | // value = getSafeWorkspaceConfig(workspaceConfig, ['editorService', 'modulePath']); 119 | // if (value !== undefined) { 120 | // settings.set('puppet.editorService.modulePath', value); 121 | // } 122 | 123 | return settings; 124 | } 125 | 126 | // Default settings 127 | export function defaultWorkspaceSettings(): ISettings { 128 | return { 129 | editorService: { 130 | enable: true, 131 | featureFlags: [], 132 | loglevel: 'normal', 133 | protocol: ProtocolType.STDIO, 134 | timeout: 10, 135 | }, 136 | format: { 137 | enable: true, 138 | }, 139 | hover: { 140 | showMetadataInfo: true, 141 | }, 142 | installDirectory: undefined, 143 | installType: PuppetInstallType.AUTO, 144 | lint: { 145 | enable: true, 146 | }, 147 | notification: { 148 | nodeGraph: 'messagebox', 149 | puppetResource: 'messagebox', 150 | }, 151 | pdk: { 152 | checkVersion: true, 153 | }, 154 | }; 155 | } 156 | 157 | export function settingsFromWorkspace(): ISettings { 158 | const workspaceConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(workspaceSectionName); 159 | const defaults: ISettings = defaultWorkspaceSettings(); 160 | 161 | // TODO: What if the wrong type is passed through? will it blow up? 162 | const settings = { 163 | editorService: workspaceConfig.get('editorService', defaults.editorService), 164 | format: workspaceConfig.get('format', defaults.format), 165 | hover: workspaceConfig.get('hover', defaults.hover), 166 | installDirectory: workspaceConfig.get('installDirectory', defaults.installDirectory), 167 | installType: workspaceConfig.get('installType', defaults.installType), 168 | lint: workspaceConfig.get('lint', defaults.lint), 169 | notification: workspaceConfig.get('notification', defaults.notification), 170 | pdk: workspaceConfig.get('pdk', defaults.pdk), 171 | }; 172 | 173 | if (settings.installDirectory && settings.installType === PuppetInstallType.AUTO) { 174 | const message = 175 | "Do not use 'installDirectory' and set 'installType' to auto. The 'installDirectory' setting" + 176 | ' is meant for custom installation directories that will not be discovered by the extension'; 177 | const title = 'Configuration Information'; 178 | const helpLink = 'https://puppet-vscode.github.io/docs/extension-settings'; 179 | vscode.window.showErrorMessage(message, { modal: false }, { title: title }).then((item) => { 180 | if (item === undefined) { 181 | return; 182 | } 183 | if (item.title === title) { 184 | vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(helpLink)); 185 | } 186 | }); 187 | } 188 | 189 | /** 190 | * Legacy Workspace Settings 191 | * 192 | * Retrieve deprecated settings and apply them to the settings. This is only needed as a helper and should be 193 | * removed a version or two later, after the setting is deprecated. 194 | */ 195 | 196 | // Ensure that object types needed for legacy settings exists 197 | if (settings.editorService === undefined) { 198 | settings.editorService = {}; 199 | } 200 | if (settings.editorService.featureFlags === undefined) { 201 | settings.editorService.featureFlags = []; 202 | } 203 | if (settings.editorService.puppet === undefined) { 204 | settings.editorService.puppet = {}; 205 | } 206 | if (settings.editorService.tcp === undefined) { 207 | settings.editorService.tcp = {}; 208 | } 209 | 210 | // Retrieve the legacy settings 211 | const oldSettings: Map> = legacySettings(); 212 | 213 | // Translate the legacy settings into the new setting names 214 | for (const [settingName, value] of oldSettings) { 215 | // eslint-disable-next-line no-empty 216 | switch ( 217 | settingName 218 | // case 'puppet.puppetAgentDir': // --> puppet.installDirectory 219 | // settings.installDirectory = value; 220 | // break; 221 | ) { 222 | } 223 | } 224 | 225 | return settings; 226 | } 227 | -------------------------------------------------------------------------------- /src/telemetry.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-use-before-define */ 2 | import TelemetryReporter from '@vscode/extension-telemetry'; 3 | import * as vscode from 'vscode'; 4 | 5 | export const reporter: TelemetryReporter = getTelemetryReporter(); 6 | 7 | function getTelemetryReporter() { 8 | const pkg = getPackageInfo(); 9 | const reporter: TelemetryReporter = new TelemetryReporter(pkg.aiKey); 10 | return reporter; 11 | } 12 | 13 | function getPackageInfo(): IPackageInfo { 14 | const pkg = vscode.extensions.getExtension('puppet.puppet-vscode'); 15 | return { 16 | aiKey: pkg.packageJSON.aiKey, 17 | }; 18 | } 19 | 20 | interface IPackageInfo { 21 | aiKey: string; 22 | } 23 | -------------------------------------------------------------------------------- /src/test/runtest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests'); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /src/test/suite/configuration.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { describe, it } from 'mocha'; 4 | import { createAggregrateConfiguration } from '../../configuration'; 5 | import { ISettings, PuppetInstallType, defaultWorkspaceSettings } from '../../settings'; 6 | 7 | describe('Configuration Tests', () => { 8 | var pdkBinDir = ''; 9 | var pdkPuppetBaseDir = ''; 10 | var puppetBaseDir = ''; 11 | 12 | switch (process.platform) { 13 | case 'win32': 14 | pdkPuppetBaseDir = 'C:\\Program Files\\Puppet Labs\\DevelopmentKit'; 15 | pdkBinDir = 'C:\\Program Files\\Puppet Labs\\DevelopmentKit\\bin'; 16 | puppetBaseDir = 'C:\\Program Files\\Puppet Labs\\Puppet'; 17 | break; 18 | default: 19 | pdkPuppetBaseDir = '/opt/puppetlabs/pdk'; 20 | pdkBinDir = '/opt/puppetlabs/pdk/bin'; 21 | puppetBaseDir = '/opt/puppetlabs'; 22 | break; 23 | } 24 | 25 | it('resolves pdkPuppetBaseDir as puppet with default installtype', () => { 26 | const settings: ISettings = defaultWorkspaceSettings(); 27 | var config = createAggregrateConfiguration(settings); 28 | assert.strictEqual(config.ruby.puppetBaseDir, pdkPuppetBaseDir); 29 | }); 30 | 31 | it('resolves puppetBaseDir as puppet with installtype eq puppet', () => { 32 | const settings: ISettings = defaultWorkspaceSettings(); 33 | settings.installType = PuppetInstallType.PUPPET; 34 | var config = createAggregrateConfiguration(settings); 35 | assert.strictEqual(config.ruby.puppetBaseDir, puppetBaseDir); 36 | }); 37 | 38 | it('resolves puppetBaseDir as pdk with installtype eq pdk', () => { 39 | const settings: ISettings = defaultWorkspaceSettings(); 40 | settings.installType = PuppetInstallType.PDK; 41 | var config = createAggregrateConfiguration(settings); 42 | assert.strictEqual(config.ruby.puppetBaseDir, pdkPuppetBaseDir); 43 | }); 44 | 45 | it('resolves pdkBinDir with installtype eq pdk', () => { 46 | const settings: ISettings = defaultWorkspaceSettings(); 47 | settings.installType = PuppetInstallType.PDK; 48 | var config = createAggregrateConfiguration(settings); 49 | assert.strictEqual(config.ruby.pdkBinDir, pdkBinDir); 50 | }); 51 | 52 | // Note that these integration tests REQUIRE the PDK to be installed locally 53 | // as the fileystem is queried for path information 54 | it('resolves latest PDK Instance with installtype eq pdk', () => { 55 | const settings: ISettings = defaultWorkspaceSettings(); 56 | settings.installType = PuppetInstallType.PDK; 57 | var config = createAggregrateConfiguration(settings); 58 | assert.notStrictEqual(config.ruby.pdkGemDir, undefined); 59 | }); 60 | 61 | it('resolves All Puppet Versions with installtype eq pdk', () => { 62 | const settings: ISettings = defaultWorkspaceSettings(); 63 | settings.installType = PuppetInstallType.PDK; 64 | var config = createAggregrateConfiguration(settings); 65 | assert.notStrictEqual(config.ruby.pdkPuppetVersions, undefined); 66 | assert.ok( 67 | config.ruby.pdkPuppetVersions.length > 0, 68 | 'config.ruby.pdkPuppetVersions.length should have at least one element', 69 | ); 70 | }); 71 | 72 | it('resolves a puppet version with installtype eq pdk', () => { 73 | // Find all of the available puppet settings 74 | let settings: ISettings = defaultWorkspaceSettings(); 75 | settings.installType = PuppetInstallType.PDK; 76 | let config = createAggregrateConfiguration(settings); 77 | // Use the first version available 78 | const puppetVersion = config.ruby.pdkPuppetVersions[0]; 79 | settings.editorService.puppet = { 80 | version: puppetVersion, 81 | }; 82 | // Generate the settings again 83 | config = createAggregrateConfiguration(settings); 84 | // Assert that pdk specifc information is still available 85 | // TODO: Should we test that version we ask is the version we get? 86 | assert.notStrictEqual(config.ruby.pdkGemDir, undefined); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { after, before, beforeEach, describe, it } from 'mocha'; 3 | import * as sinon from 'sinon'; 4 | import * as vscode from 'vscode'; 5 | import { provideCompletionItemMiddleware } from '../../extension'; 6 | import { PuppetStatusBarFeature } from '../../feature/PuppetStatusBarFeature'; 7 | import { ISettings, settingsFromWorkspace } from '../../settings'; 8 | 9 | describe('Extension Tests', () => { 10 | let vscodeCommandsRegisterCommandStub: sinon.SinonStub; 11 | let puppetStatusBarFeatureStub: sinon.SinonStub; 12 | let settings: ISettings; 13 | let context: vscode.ExtensionContext; 14 | let registerDebugAdapterDescriptorFactoryStub: sinon.SinonStub; 15 | let document: vscode.TextDocument; 16 | let position: vscode.Position; 17 | let completionContext: vscode.CompletionContext; 18 | let token: vscode.CancellationToken; 19 | let next: sinon.SinonStub 20 | const sandbox = sinon.createSandbox(); 21 | 22 | before(() => { 23 | vscodeCommandsRegisterCommandStub = sandbox.stub(vscode.commands, 'registerCommand'); 24 | settings = settingsFromWorkspace(); 25 | context = { 26 | subscriptions: [], 27 | asAbsolutePath: (relativePath: string) => { 28 | return `/absolute/path/to/${relativePath}`; 29 | }, 30 | globalState: { 31 | get: sandbox.stub(), 32 | } 33 | } as vscode.ExtensionContext; 34 | puppetStatusBarFeatureStub = sandbox.createStubInstance(PuppetStatusBarFeature); 35 | registerDebugAdapterDescriptorFactoryStub = sandbox.stub(vscode.debug, 'registerDebugAdapterDescriptorFactory'); 36 | }); 37 | 38 | beforeEach(() => { 39 | document = {} as vscode.TextDocument; 40 | position = new vscode.Position(0, 0); 41 | completionContext = {} as vscode.CompletionContext; 42 | token = new vscode.CancellationTokenSource().token; 43 | next = sandbox.stub(); 44 | }); 45 | 46 | after(() => { 47 | sandbox.restore(); 48 | }); 49 | 50 | it('should add command to completion items', async () => { 51 | const completionItems = [ 52 | new vscode.CompletionItem('item1', vscode.CompletionItemKind.Property), 53 | new vscode.CompletionItem('item2', vscode.CompletionItemKind.Text), 54 | ]; 55 | completionItems[0].detail = 'Property'; 56 | completionItems[1].detail = 'Text'; 57 | next.returns(completionItems); 58 | 59 | const result = await provideCompletionItemMiddleware.provideCompletionItem(document, position, context, token, next); 60 | 61 | assert.ok(Array.isArray(result)); 62 | assert.strictEqual(result.length, 2); 63 | assert.strictEqual(result[0].command?.command, 'editor.action.formatDocumentAndMoveCursor'); 64 | assert.strictEqual(result[1].command?.command, 'editor.action.formatDocument'); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/test/suite/feature/DebuggingFeature.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as cp from 'child_process'; 3 | import { EventEmitter } from 'events'; 4 | import { afterEach, beforeEach, describe, it } from 'mocha'; 5 | import * as sinon from 'sinon'; 6 | import * as vscode from 'vscode'; 7 | import { IAggregateConfiguration } from '../../../configuration'; 8 | import { DebugAdapterDescriptorFactory, DebuggingFeature } from '../../../feature/DebuggingFeature'; 9 | import { ILogger } from '../../../logging'; 10 | import * as index from '../index'; 11 | 12 | describe('DebuggingFeature', () => { 13 | let sandbox: sinon.SinonSandbox; 14 | let mockContext: vscode.ExtensionContext; 15 | let mockConfig: IAggregateConfiguration; 16 | let mockLogger: ILogger; 17 | let registerDebugAdapterDescriptorFactoryStub: sinon.SinonStub; 18 | let registerDebugConfigurationProviderStub: sinon.SinonStub; 19 | let debugType: string = 'debug'; 20 | const mockChildProcess = { 21 | stdout: new EventEmitter(), 22 | stderr: new EventEmitter(), 23 | on: function(event, handler) { 24 | this[event] = handler; 25 | }, 26 | } as any as cp.ChildProcess; 27 | 28 | beforeEach(() => { 29 | sandbox = sinon.createSandbox(); 30 | mockContext = index.extContext; 31 | mockConfig = index.configSettings; 32 | mockLogger = index.logger; 33 | // Stub the registerDebugAdapterDescriptorFactory and registerDebugConfigurationProvider methods 34 | registerDebugAdapterDescriptorFactoryStub = sandbox.stub(vscode.debug, 'registerDebugAdapterDescriptorFactory'); 35 | registerDebugConfigurationProviderStub = sandbox.stub(vscode.debug, 'registerDebugConfigurationProvider'); 36 | }); 37 | 38 | afterEach(() => { 39 | sandbox.restore(); 40 | }); 41 | 42 | it('DebuggingFeature constructor correctly initializes properties', () => { 43 | const debuggingFeature = new DebuggingFeature(debugType, mockConfig, mockContext, mockLogger); 44 | assert.strictEqual(debuggingFeature['factory'].Context, mockContext); 45 | assert.strictEqual(debuggingFeature['factory'].Config, mockConfig); 46 | assert.strictEqual(debuggingFeature['factory'].Logger, mockLogger); 47 | }); 48 | 49 | it('DebuggingFeature constructor registers DebugAdapterDescriptorFactory', () => { 50 | const debuggingFeature = new DebuggingFeature(debugType, mockConfig, mockContext, mockLogger); 51 | assert(registerDebugAdapterDescriptorFactoryStub.calledOnceWith(debugType, debuggingFeature['factory'])); 52 | }); 53 | 54 | it('createDebugAdapterDescriptor returns expected descriptor on successful scenario', async () => { 55 | const mockSession = {} as vscode.DebugSession; 56 | const mockExecutable = {} as vscode.DebugAdapterExecutable; 57 | const mockSpawn = sandbox.stub(cp, 'spawn'); 58 | mockSpawn.callsFake(() => { 59 | process.nextTick(() => mockChildProcess.stdout.emit('data', 'DEBUG SERVER RUNNING localhost:1234')); 60 | return mockChildProcess; 61 | }); 62 | 63 | const factory = new DebugAdapterDescriptorFactory(mockContext, mockConfig, mockLogger); 64 | const descriptor = await factory.createDebugAdapterDescriptor(mockSession, mockExecutable); 65 | 66 | sandbox.assert.match(descriptor, new vscode.DebugAdapterServer(1234, 'localhost')); 67 | }); 68 | 69 | // Test that DebugAdapterDescriptorFactory correctly handles 'error' event from debugServerProc 70 | it('DebugAdapterDescriptorFactory correctly handles \'error\' event from debugServerProc', async () => { 71 | const mockSession = {} as vscode.DebugSession; 72 | const mockExecutable = {} as vscode.DebugAdapterExecutable; 73 | 74 | const mockSpawn = sandbox.stub(cp, 'spawn'); 75 | mockSpawn.callsFake(() => { 76 | process.nextTick(() => mockChildProcess['error']('Test error')); 77 | return mockChildProcess; 78 | }); 79 | 80 | const factory = new DebugAdapterDescriptorFactory(mockContext, mockConfig, mockLogger); 81 | try { 82 | await factory.createDebugAdapterDescriptor(mockSession, mockExecutable); 83 | } catch (error) { 84 | assert.strictEqual(error, 'Spawning Debug Server failed with Test error'); 85 | } 86 | }); 87 | 88 | // Test that DebugAdapterDescriptorFactory correctly handles 'close' event from debugServerProc 89 | it('DebugAdapterDescriptorFactory correctly handles \'close\' event from debugServerProc', async () => { 90 | const mockSession = {} as vscode.DebugSession; 91 | const mockExecutable = {} as vscode.DebugAdapterExecutable; 92 | const mockLoggerVerbose = sandbox.stub(mockLogger, 'verbose'); 93 | 94 | const mockSpawn = sandbox.stub(cp, 'spawn'); 95 | mockSpawn.callsFake(() => { 96 | process.nextTick(() => { 97 | mockChildProcess.stdout.emit('data', 'DEBUG SERVER RUNNING localhost:1234'); 98 | mockChildProcess['close'](0); 99 | }); 100 | return mockChildProcess; 101 | }); 102 | 103 | const factory = new DebugAdapterDescriptorFactory(mockContext, mockConfig, mockLogger); 104 | await factory.createDebugAdapterDescriptor(mockSession, mockExecutable); 105 | sandbox.assert.calledWith(mockLoggerVerbose, 'Debug Server exited with exitcode 0'); 106 | }); 107 | 108 | it('dispose method empties ChildProcesses array', () => { 109 | const debuggingFeature = new DebuggingFeature(debugType, mockConfig, mockContext, mockLogger); 110 | debuggingFeature['factory'].dispose(); 111 | assert.strictEqual(debuggingFeature['factory'].ChildProcesses.length, 0); 112 | }); 113 | 114 | it('DebuggingFeature constructor registers DebugConfigurationProvider', () => { 115 | const debuggingFeature = new DebuggingFeature(debugType, mockConfig, mockContext, mockLogger); 116 | assert(registerDebugConfigurationProviderStub.calledOnceWith(debugType, debuggingFeature['provider'])); 117 | }); 118 | 119 | it('DebuggingFeature dispose method sets factory to null', () => { 120 | const debuggingFeature = new DebuggingFeature(debugType, mockConfig, mockContext, mockLogger); 121 | debuggingFeature.dispose(); 122 | assert.strictEqual(debuggingFeature['factory'], null); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /src/test/suite/feature/FormatDocument.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as fs from 'fs'; 3 | import { after, before, describe, it } from 'mocha'; 4 | import * as os from 'os'; 5 | import * as path from 'path'; 6 | import * as sinon from 'sinon'; 7 | import * as vscode from 'vscode'; 8 | import { FormatDocumentFeature } from '../../../feature/FormatDocumentFeature'; 9 | import { StdioConnectionHandler } from '../../../handlers/stdio'; 10 | import * as index from '../index'; 11 | 12 | describe('FormatDocumentFeature Test Suite', () => { 13 | const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-test-')); 14 | const tempFilePath = path.join(tempDir, 'manifest.pp'); 15 | let sandbox: sinon.Sandbox; 16 | let connectionHandler: StdioConnectionHandler; 17 | 18 | before(() => { 19 | sandbox = sinon.createSandbox(); 20 | connectionHandler = sandbox.createStubInstance(StdioConnectionHandler); 21 | }); 22 | 23 | after(() => { 24 | sandbox.restore(); 25 | }); 26 | 27 | it('Formats a document with linting errors', async () => { 28 | // Create a manifest with linting error (missing whitespace before the opening brace) 29 | const manifestContent = ` 30 | file{'/tmp/test': 31 | ensure => present, 32 | } 33 | `.split('\n').map(line => line.trim()).join('\n').trim(); 34 | // Write the manifest to a temporary file 35 | fs.writeFileSync(tempFilePath, manifestContent); 36 | 37 | // Create a new FormatDocumentFeature instance 38 | const feature = new FormatDocumentFeature(index.puppetLangID, connectionHandler, index.configSettings, index.logger, index.extContext); 39 | const document: vscode.TextDocument = await vscode.workspace.openTextDocument(tempFilePath); 40 | const range = new vscode.Range(new vscode.Position(0, 4), new vscode.Position(0, 5)); 41 | const mockTextEdits = [vscode.TextEdit.replace(range, ' {')]; // Add the missing whitespace, we arent testing puppet-lint here 42 | // Stub the formatTextEdits method to return the mockTextEdits 43 | const provider = feature.getProvider(); 44 | const formatTextEditsStub = sandbox.stub(provider, 'formatTextEdits'); 45 | formatTextEditsStub.returns(Promise.resolve(mockTextEdits)); 46 | 47 | // Format the document 48 | await vscode.window.showTextDocument(document); 49 | await vscode.commands.executeCommand('editor.action.formatDocument'); 50 | 51 | // Read the formatted document 52 | const formattedDocument = await vscode.workspace.openTextDocument(tempFilePath); 53 | const formattedContent = formattedDocument.getText(); 54 | 55 | // Assert that the document was formatted 56 | assert.notStrictEqual(formattedContent, manifestContent); 57 | 58 | assert.strictEqual(formattedContent, ` 59 | file {'/tmp/test': 60 | ensure => present, 61 | } 62 | `.split('\n').map(line => line.trim()).join('\n').trim()); 63 | 64 | formatTextEditsStub.restore(); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/test/suite/feature/PDKFeature.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import * as fs from 'fs'; 3 | import { afterEach, beforeEach, describe, it } from 'mocha'; 4 | import * as path from 'path'; 5 | import * as sinon from 'sinon'; 6 | import * as vscode from 'vscode'; 7 | import { PDKFeature } from '../../../feature/PDKFeature'; 8 | import { OutputChannelLogger } from '../../../logging/outputchannel'; 9 | import * as index from '../index'; 10 | 11 | describe('PDKFeature', () => { 12 | let sandbox: sinon.SinonSandbox; 13 | let pdkFeature: PDKFeature; 14 | let mockContext: vscode.ExtensionContext; 15 | let mockLogger: OutputChannelLogger; 16 | let registerCommandStub: sinon.SinonStub; 17 | const moduleName = 'testmodule'; 18 | const dir = '/path/to/directory'; 19 | 20 | beforeEach(() => { 21 | sandbox = sinon.createSandbox(); 22 | mockContext = index.extContext; 23 | mockLogger = index.logger; 24 | registerCommandStub = sandbox.stub(vscode.commands, 'registerCommand').returns(() => {}); 25 | pdkFeature = new PDKFeature(mockContext, mockLogger); 26 | }); 27 | 28 | afterEach(() => { 29 | sandbox.restore(); 30 | }); 31 | 32 | describe('constructor', () => { 33 | it('should register commands', () => { 34 | assert(registerCommandStub.called); 35 | }); 36 | }); 37 | 38 | describe('getTerminal', () => { 39 | it('should return an existing terminal if one exists', () => { 40 | const terminal = { name: 'Puppet PDK' }; 41 | sandbox.stub(vscode.window, 'terminals').value([terminal]); 42 | const result = (pdkFeature as any).getTerminal(); 43 | assert.strictEqual(result.name, terminal.name); 44 | }); 45 | 46 | it('should create a new terminal if none exists', () => { 47 | sandbox.stub(vscode.window, 'terminals').value([]); 48 | const createTerminalStub = sandbox.stub(vscode.window, 'createTerminal').returns({ name: 'Puppet PDK' } as vscode.Terminal); 49 | (pdkFeature as any).getTerminal(); 50 | assert(createTerminalStub.calledWith('Puppet PDK')); 51 | createTerminalStub.restore(); 52 | }); 53 | }); 54 | 55 | describe('pdkNewModuleCommand', () => { 56 | it('should send a command to the terminal', async () => { 57 | sandbox.stub(vscode.window, 'showInputBox').returns(Promise.resolve(moduleName)); 58 | sandbox.stub(vscode.window, 'showOpenDialog').returns(Promise.resolve([vscode.Uri.file(dir)])); 59 | const terminal = { sendText: sandbox.stub(), show: sandbox.stub() }; 60 | sandbox.stub(pdkFeature, 'getTerminal').returns(terminal); 61 | sandbox.stub(fs, 'existsSync').returns(true); 62 | sandbox.stub(vscode.commands, 'executeCommand'); 63 | 64 | await (pdkFeature as any).pdkNewModuleCommand(); 65 | assert(terminal.sendText.calledWith(`pdk new module --skip-interview ${moduleName} ${path.join(dir, moduleName)}`)); 66 | }); 67 | 68 | it('should show a warning if no module name is specified', async () => { 69 | const showWarningMessageStub = sandbox.stub(vscode.window, 'showWarningMessage'); 70 | sandbox.stub(vscode.window, 'showInputBox').returns(''); 71 | sandbox.stub(vscode.window, 'showOpenDialog').returns(Promise.resolve([vscode.Uri.file(dir)])); 72 | await (pdkFeature as any).pdkNewModuleCommand(); 73 | assert(showWarningMessageStub.calledWith('No module name specifed. Exiting.')); 74 | showWarningMessageStub.restore(); 75 | }); 76 | 77 | it('should show a warning if no directory is specified', async () => { 78 | const showWarningMessageStub = sandbox.stub(vscode.window, 'showWarningMessage'); 79 | sandbox.stub(vscode.window, 'showInputBox').returns(moduleName); 80 | sandbox.stub(vscode.window, 'showOpenDialog').returns(''); 81 | await (pdkFeature as any).pdkNewModuleCommand(); 82 | assert(showWarningMessageStub.calledWith('No directory specifed. Exiting.')); 83 | showWarningMessageStub.restore(); 84 | }); 85 | }); 86 | 87 | describe('dispose', () => { 88 | it('should dispose the terminal', () => { 89 | const terminal = { dispose: sandbox.stub() }; 90 | sandbox.stub(pdkFeature, 'getTerminal').returns(terminal); 91 | pdkFeature.dispose(); 92 | assert(terminal.dispose.called); 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /src/test/suite/feature/PuppetModuleHoverFeature.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import * as jsoncParser from 'jsonc-parser'; 3 | import { afterEach, beforeEach, describe, it } from 'mocha'; 4 | import * as sinon from 'sinon'; 5 | import * as vscode from 'vscode'; 6 | import { PuppetModuleHoverProvider } from '../../../feature/PuppetModuleHoverFeature'; 7 | import * as forge from '../../../forge'; 8 | import { OutputChannelLogger } from '../../../logging/outputchannel'; 9 | import * as telemetry from '../../../telemetry'; 10 | import * as index from '../index'; 11 | 12 | describe('PuppetModuleHoverProvider', () => { 13 | let sandbox: sinon.SinonSandbox; 14 | let logger: OutputChannelLogger; 15 | let document: vscode.TextDocument; 16 | let position: vscode.Position; 17 | let token: vscode.CancellationToken; 18 | let provider: PuppetModuleHoverProvider; 19 | const mockModuleInfo: forge.PuppetForgeModuleInfo = { 20 | uri: '/v3/modules/puppetlabs-stdlib', 21 | slug: 'puppetlabs-stdlib', 22 | name: 'puppetlabs/stdlib', 23 | downloads: 0, 24 | score: 0, 25 | created: new Date(), 26 | updated: new Date(), 27 | endorsement: 'supported', 28 | owner: { slug: 'puppetlabs', username: 'puppetlabs' }, 29 | forgeUrl: 'https://forge.puppet.com/modules/puppetlabs/stdlib', 30 | homepageUrl: 'https://github.com/puppetlabs/puppetlabs-stdlib', 31 | version: 0, 32 | summary: 'summary', 33 | }; 34 | 35 | beforeEach(() => { 36 | sandbox = sinon.createSandbox(); 37 | logger = index.logger; 38 | provider = new PuppetModuleHoverProvider(logger); 39 | document = { 40 | getText: sandbox.stub().returns(''), 41 | offsetAt: sandbox.stub().returns(0), 42 | getWordRangeAtPosition: sandbox.stub().returns(new vscode.Range(0, 0, 0, 0)) 43 | } as unknown as vscode.TextDocument; 44 | 45 | position = new vscode.Position(0, 0); 46 | token = new vscode.CancellationTokenSource().token; 47 | sandbox.stub(forge, 'getModuleInfo').returns(Promise.resolve(mockModuleInfo)); 48 | }); 49 | 50 | afterEach(() => { 51 | sandbox.restore(); 52 | }); 53 | 54 | it('should return null if cancellation token is already cancelled', async () => { 55 | // Set up token to be cancelled 56 | const result = await provider.provideHover(document, position, token); 57 | assert.isUndefined(result); 58 | }); 59 | 60 | it('should return null if location is at property key', async () => { 61 | sandbox.stub(jsoncParser, 'getLocation').returns({ isAtPropertyKey: true }); 62 | const result = await provider.provideHover(document, position, token); 63 | assert.isUndefined(result); 64 | }); 65 | 66 | it('should return null if the first element in the location path is not "dependencies"', async () => { 67 | sandbox.stub(jsoncParser, 'getLocation').returns({ path: ['not-dependencies'] }); 68 | const result = await provider.provideHover(document, position, token); 69 | assert.isUndefined(result); 70 | }); 71 | 72 | it('should return null if the third element in the location path is not "name"', async () => { 73 | sandbox.stub(jsoncParser, 'getLocation').returns({ path: ['dependencies', 'some-element', 'not-name'] }); 74 | const result = await provider.provideHover(document, position, token); 75 | assert.isUndefined(result); 76 | }); 77 | 78 | it('should send a telemetry event when a hover is provided', async () => { 79 | sandbox.stub(jsoncParser, 'getLocation').returns({ path: ['dependencies', 'some-element', 'name'] }); 80 | const sendTelemetryEvent = sandbox.stub(); 81 | sandbox.stub(telemetry, 'reporter').value({ sendTelemetryEvent }); 82 | await provider.provideHover(document, position, token); 83 | assert.isTrue(sendTelemetryEvent.calledWith('metadataJSON/Hover')); 84 | }); 85 | 86 | it('should return hover info with module information if getModuleInfo is successful', async () => { 87 | // Stub the jsoncParser.getLocation method to return a valid path 88 | sandbox.stub(jsoncParser, 'getLocation').returns({ path: ['dependencies', 'some-element', 'name'] }); 89 | 90 | const result = await provider.provideHover(document, position, token); 91 | const contents = result.contents[0] as vscode.MarkdownString; 92 | assert.include(contents.value, mockModuleInfo.name); 93 | assert.include(contents.value, mockModuleInfo.summary); 94 | assert.include(contents.value, mockModuleInfo.version.toString()); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /src/test/suite/feature/PuppetNodeGraphFeature.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { afterEach, before, beforeEach, describe, it } from 'mocha'; 3 | import * as sinon from 'sinon'; 4 | import * as vscode from 'vscode'; 5 | import { PuppetNodeGraphFeature } from '../../../feature/PuppetNodeGraphFeature'; 6 | import { StdioConnectionHandler } from '../../../handlers/stdio'; 7 | import { ConnectionStatus } from '../../../interfaces'; 8 | import * as index from '../index'; 9 | 10 | const mockContext: vscode.ExtensionContext = index.extContext; 11 | let sendRequestStub: sinon.SinonStub; 12 | let mockConnectionHandler: sinon.SinonStubbedInstance; 13 | let sandbox: sinon.SinonSandbox; 14 | let puppetNodeGraphFeature: PuppetNodeGraphFeature; 15 | 16 | describe('PuppetNodeGraphFeature', () => { 17 | before(() => { 18 | sandbox = sinon.createSandbox(); 19 | }); 20 | 21 | beforeEach(() => { 22 | sandbox.stub(vscode.commands, 'registerCommand'); 23 | mockConnectionHandler = sandbox.createStubInstance(StdioConnectionHandler); 24 | sandbox.stub(Object.getPrototypeOf(mockConnectionHandler), 'status').get(() => ConnectionStatus.RunningLoaded); 25 | sendRequestStub = sandbox.stub(); 26 | const mockLanguageClient = { 27 | sendRequest: sendRequestStub 28 | }; 29 | sandbox.stub(mockConnectionHandler, 'languageClient').get(() => mockLanguageClient); 30 | puppetNodeGraphFeature = new PuppetNodeGraphFeature(index.puppetLangID, mockConnectionHandler, index.logger, mockContext); 31 | }); 32 | 33 | afterEach(() => { 34 | puppetNodeGraphFeature.dispose(); 35 | sandbox.restore(); 36 | }); 37 | 38 | it('should construct PuppetNodeGraphFeature', () => { 39 | assert.ok(puppetNodeGraphFeature); 40 | }); 41 | 42 | it('should open webview panel when puppetShowNodeGraphToSide command is executed', async () => { 43 | const createWebviewPanelSpy = sandbox.spy(vscode.window, 'createWebviewPanel'); 44 | await vscode.commands.executeCommand('puppet.puppetShowNodeGraphToSide'); 45 | assert.ok(createWebviewPanelSpy.calledWith( 46 | 'puppetNodeGraph', 47 | 'Node Graph \'manifest.pp\'', 48 | vscode.ViewColumn.Beside, 49 | { enableScripts: true } 50 | )); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/test/suite/feature/PuppetResourceFeature.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { afterEach, before, beforeEach, describe, it } from 'mocha'; 3 | import * as sinon from 'sinon'; 4 | import * as vscode from 'vscode'; 5 | import { PuppetResourceFeature } from '../../../feature/PuppetResourceFeature'; 6 | import { ConnectionHandler } from '../../../handler'; 7 | import { StdioConnectionHandler } from '../../../handlers/stdio'; 8 | import { ConnectionStatus } from '../../../interfaces'; 9 | import { OutputChannelLogger } from '../../../logging/outputchannel'; 10 | import * as index from '../index'; 11 | 12 | describe('PuppetResourceFeature Test Suite', () => { 13 | let sandbox: sinon.SinonSandbox; 14 | let puppetResourceFeature: PuppetResourceFeature; 15 | let connectionHandler: ConnectionHandler; 16 | let logger: OutputChannelLogger = index.logger; 17 | let context: vscode.ExtensionContext = index.extContext; 18 | let editCurrentDocumentStub: sinon.SinonStub; 19 | let resolveDocumentChanged: () => void; 20 | let documentChanged: Promise; 21 | 22 | before(() => { 23 | sandbox = sinon.createSandbox(); 24 | }); 25 | 26 | beforeEach(() => { 27 | sandbox.stub(vscode.commands, 'registerCommand'); 28 | connectionHandler = sandbox.createStubInstance(StdioConnectionHandler); 29 | Object.defineProperty(connectionHandler, 'languageClient', { value: { sendRequest: () => { } } }); 30 | Object.defineProperty(connectionHandler, 'status', { writable: true, value: ConnectionStatus.RunningLoaded }); 31 | puppetResourceFeature = new PuppetResourceFeature(context, connectionHandler, logger); 32 | // define a promise that resolves when documentChanged is called 33 | documentChanged = new Promise(resolve => { 34 | resolveDocumentChanged = resolve; 35 | }); 36 | // Create a stub for the editCurrentDocument method that resolves documentChanged 37 | editCurrentDocumentStub = sandbox.stub(puppetResourceFeature, 'editCurrentDocument').callsFake(() => { 38 | resolveDocumentChanged(); 39 | }); 40 | }); 41 | 42 | afterEach(() => { 43 | puppetResourceFeature.dispose(); 44 | sandbox.restore(); 45 | }); 46 | 47 | it('run should show information message when language server is not ready', () => { 48 | Object.defineProperty(connectionHandler, 'status', { value: ConnectionStatus.NotStarted }); 49 | const showInformationMessageStub = sandbox.stub(vscode.window, 'showInformationMessage'); 50 | puppetResourceFeature.run(); 51 | assert.isTrue(showInformationMessageStub.calledWith('Puppet Resource is not available as the Language Server is not ready')); 52 | }); 53 | 54 | it('run should not proceed when no resource name is provided', () => { 55 | sandbox.stub(vscode.window, 'showInputBox').resolves(undefined); 56 | puppetResourceFeature.run(); 57 | assert.isTrue(editCurrentDocumentStub.notCalled); 58 | }); 59 | 60 | it('run should call editCurrentDocument when resourceResult.data is not empty', async () => { 61 | sandbox.stub(vscode.window, 'showInputBox').resolves('test-resource'); 62 | sandbox.stub(connectionHandler.languageClient, 'sendRequest').resolves({ data: 'test-data', error: undefined}); 63 | puppetResourceFeature.run(); 64 | await documentChanged; 65 | sandbox.assert.calledOnce(editCurrentDocumentStub); 66 | assert.include(editCurrentDocumentStub.args[0][1], 'test-data'); 67 | }); 68 | 69 | it('run should log error when resourceResult.error is not empty', () => { 70 | sandbox.stub(vscode.window, 'showInputBox').resolves('test-resource'); 71 | sandbox.stub(connectionHandler.languageClient, 'sendRequest').resolves({ data: undefined, error: 'test-error' }); 72 | puppetResourceFeature.run(); 73 | sandbox.assert.notCalled(editCurrentDocumentStub); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/test/suite/feature/PuppetStatusBarFeature.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { afterEach, beforeEach, describe, it } from 'mocha'; 3 | import * as sinon from 'sinon'; 4 | import * as vscode from 'vscode'; 5 | import { IAggregateConfiguration } from '../../../configuration'; 6 | import { PuppetStatusBarFeature } from '../../../feature/PuppetStatusBarFeature'; 7 | import { ConnectionStatus } from '../../../interfaces'; 8 | import { ILogger } from '../../../logging'; 9 | import * as index from '../index'; 10 | 11 | describe('PuppetStatusBarProvider', () => { 12 | let sandbox: sinon.SinonSandbox; 13 | let mockConfig: sinon.SinonStubbedInstance = index.configSettings; 14 | let mockLogger: sinon.SinonStubbedInstance = index.logger; 15 | let mockStatusBarItem: sinon.SinonStubbedInstance; 16 | let statusBarFeature: PuppetStatusBarFeature; 17 | 18 | beforeEach(() => { 19 | sandbox = sinon.createSandbox(); 20 | mockStatusBarItem = { 21 | alignment: vscode.StatusBarAlignment.Left, 22 | priority: undefined, 23 | text: '', 24 | tooltip: '', 25 | color: '', 26 | command: '', 27 | show: sandbox.stub(), 28 | hide: sandbox.stub(), 29 | dispose: sandbox.stub(), 30 | }; 31 | sandbox.stub(vscode.window, 'createStatusBarItem').returns(mockStatusBarItem); 32 | sandbox.stub(vscode.commands, 'registerCommand'); 33 | }); 34 | 35 | afterEach(() => { 36 | sandbox.restore(); 37 | }); 38 | 39 | it('should update status bar item when connection status changes', () => { 40 | statusBarFeature = new PuppetStatusBarFeature([index.puppetLangID], mockConfig, mockLogger, index.extContext) 41 | const newStatus = ConnectionStatus.RunningLoaded; 42 | const newStatusText = 'RunningLoaded'; 43 | const newToolTip = 'Connection is running and loaded'; 44 | statusBarFeature.setConnectionStatus(newStatusText, newStatus, newToolTip); 45 | 46 | sandbox.assert.calledOnce(mockStatusBarItem.show); 47 | assert.equal(mockStatusBarItem.text, `$(terminal) ${newStatusText}`); 48 | assert.equal(mockStatusBarItem.tooltip, newToolTip); 49 | }); 50 | 51 | it('should hide status bar item when language ID is not Puppet', () => { 52 | const mockNonPuppetEditor = { 53 | document: { 54 | languageId: 'javascript', // Use a language ID that is not 'puppet' 55 | }, 56 | } as vscode.TextEditor; 57 | const onDidChangeActiveTextEditorStub = sandbox.stub(vscode.window, 'onDidChangeActiveTextEditor'); 58 | // Create a new instance of PuppetStatusBarFeature to test the onDidChangeActiveTextEditor event 59 | statusBarFeature = new PuppetStatusBarFeature([index.puppetLangID], mockConfig, mockLogger, index.extContext) 60 | // Trigger the onDidChangeActiveTextEditor event, passing in a non-Puppet editor as if the user switched to a non-Puppet file 61 | onDidChangeActiveTextEditorStub.callArgWith(0, mockNonPuppetEditor); 62 | // Verify that the status bar item is hidden 63 | sandbox.assert.calledOnce(mockStatusBarItem.hide); 64 | }); 65 | 66 | it('should return undefined when disposed', () => { 67 | statusBarFeature = new PuppetStatusBarFeature([index.puppetLangID], mockConfig, mockLogger, index.extContext); 68 | const result = statusBarFeature.dispose(); 69 | assert.isUndefined(result); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/test/suite/feature/PuppetfileCompletionFeature.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import * as fs from 'fs'; 3 | import { after, before, describe, it } from 'mocha'; 4 | import * as os from 'os'; 5 | import * as path from 'path'; 6 | import * as sinon from 'sinon'; 7 | import * as vscode from 'vscode'; 8 | import { PuppetfileCompletionProvider } from '../../../feature/PuppetfileCompletionFeature'; 9 | import * as forge from '../../../forge'; 10 | import { StdioConnectionHandler } from '../../../handlers/stdio'; 11 | import * as index from '../index'; 12 | 13 | describe('PuppetfileCompletionFeature', () => { 14 | 15 | const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-test-')); 16 | const tempFilePath = path.join(tempDir, 'manifest.pp'); 17 | let sandbox: sinon.Sandbox; 18 | let connectionHandler: StdioConnectionHandler; 19 | let puppetfileCompletionProvider: PuppetfileCompletionProvider; 20 | let getPuppetModuleCompletionStub: sinon.SinonStub; 21 | 22 | before(() => { 23 | sandbox = sinon.createSandbox(); 24 | connectionHandler = sandbox.createStubInstance(StdioConnectionHandler); 25 | puppetfileCompletionProvider = new PuppetfileCompletionProvider(index.logger); 26 | // Create a stub for getPuppetModuleCompletion, so we dont make an actual api call 27 | getPuppetModuleCompletionStub = sandbox.stub(forge, 'getPuppetModuleCompletion'); 28 | getPuppetModuleCompletionStub.returns(Promise.resolve({ 29 | total: 3, 30 | modules: ['puppetlabs-stdlib', 'puppetlabs-concat', 'puppetlabs-apache'] 31 | }) 32 | ); 33 | }); 34 | 35 | after (() => { 36 | sandbox.restore(); 37 | }); 38 | 39 | it('provideCompletionItems returns expected results', async () => { 40 | // a simple Puppetfile with one module 41 | const puppetfileContent = ` 42 | forge 'https://forge.puppet.com' 43 | mod 'puppetlabs-' 44 | `.split('\n').map(line => line.trim()).join('\n').trim(); 45 | 46 | fs.writeFileSync(tempFilePath, puppetfileContent); 47 | 48 | const document: vscode.TextDocument = await vscode.workspace.openTextDocument(tempFilePath); 49 | const position = new vscode.Position(1, 'mod puppetlabs-'.length); 50 | const token = new vscode.CancellationTokenSource().token; 51 | const context = {} as vscode.CompletionContext; 52 | 53 | const result = await puppetfileCompletionProvider.provideCompletionItems(document, position, token, context); 54 | // Check if result is an instance of vscode.CompletionList 55 | if (result instanceof vscode.CompletionList) { 56 | for (const item of result.items) { 57 | assert.include(item.label, 'puppetlabs-'); 58 | assert.strictEqual(item.kind, vscode.CompletionItemKind.Module); 59 | } 60 | } 61 | }); 62 | 63 | it('provideCompletionItems returns undefined when line does not start with mod', async () => { 64 | // an invalid puppetfile 65 | const puppetfileContent = ` 66 | forge 'https://forge.puppet.com' 67 | 'puppetlabs-' 68 | `.split('\n').map(line => line.trim()).join('\n').trim(); 69 | 70 | fs.writeFileSync(tempFilePath, puppetfileContent); 71 | 72 | const document: vscode.TextDocument = await vscode.workspace.openTextDocument(tempFilePath); 73 | const position = new vscode.Position(1, 0); 74 | const token = new vscode.CancellationTokenSource().token; 75 | const context = {} as vscode.CompletionContext; 76 | 77 | const result = await puppetfileCompletionProvider.provideCompletionItems(document, position, token, context); 78 | assert.strictEqual(result, undefined); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/test/suite/feature/PuppetfileHoverFeature.test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import * as fs from 'fs'; 3 | import { after, before, describe, it } from 'mocha'; 4 | import * as os from 'os'; 5 | import * as path from 'path'; 6 | import * as sinon from 'sinon'; 7 | import * as vscode from 'vscode'; 8 | import { PuppetfileHoverFeature } from '../../../feature/PuppetfileHoverFeature'; 9 | import * as forge from '../../../forge'; 10 | import { ILogger } from '../../../logging'; 11 | import * as index from '../index'; 12 | 13 | describe('PuppetfileHoverFeature', () => { 14 | let context: vscode.ExtensionContext = index.extContext; 15 | let logger: ILogger = index.logger; 16 | let sandbox: sinon.SinonSandbox; 17 | let getModuleInfoStub: sinon.SinonStub; 18 | 19 | before(() => { 20 | sandbox = sinon.createSandbox(); 21 | // Stub the getModuleInfo function to return a known value, rather than make an actual api call 22 | getModuleInfoStub = sandbox.stub(forge, 'getModuleInfo'); 23 | getModuleInfoStub.returns(Promise.resolve({ 24 | uri: '/v3/modules/puppetlabs-stdlib', 25 | slug: 'puppetlabs-stdlib', 26 | name: 'puppetlabs/stdlib', 27 | downloads: 0, 28 | score: 0, 29 | created: new Date(), 30 | updated: new Date(), 31 | endorsement: 'supported', 32 | owner: { slug: 'puppetlabs', username: 'puppetlabs' }, 33 | forgeUrl: 'https://forge.puppet.com/modules/puppetlabs/stdlib', 34 | homepageUrl: 'https://github.com/puppetlabs/puppetlabs-stdlib', 35 | })); 36 | }); 37 | 38 | after(() => { 39 | sandbox.restore(); 40 | }); 41 | 42 | it('should register hover provider on construction', async () => { 43 | const feature = new PuppetfileHoverFeature(context, logger); 44 | // a simple Puppetfile with two modules 45 | const puppetfileContent = ` 46 | mod 'puppetlabs-stdlib' 47 | mod 'puppetlabs-concat' 48 | `.split('\n').map(line => line.trim()).join('\n').trim(); 49 | 50 | // Create a temporary file with the desired content 51 | const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-test-')); 52 | const tempFile = path.join(tempDir, 'Puppetfile'); 53 | fs.writeFileSync(tempFile, puppetfileContent); 54 | 55 | const document: vscode.TextDocument = await vscode.workspace.openTextDocument(tempFile); 56 | const position = new vscode.Position(1, 4); 57 | 58 | const hover = await vscode.commands.executeCommand('vscode.executeHoverProvider', document.uri, position); 59 | getModuleInfoStub.restore(); 60 | 61 | assert.instanceOf(hover, Array); 62 | assert.isNotEmpty(hover); 63 | // test that the hover contains the some of the expected content 64 | assert.include(hover[0].contents[0].value, '**Forge**: [https://forge.puppet.com/modules/puppetlabs/stdlib](https://forge.puppet.com/modules/puppetlabs/stdlib)'); 65 | assert.include(hover[0].contents[0].value, '**Project**: [https://github.com/puppetlabs/puppetlabs-stdlib](https://github.com/puppetlabs/puppetlabs-stdlib)'); 66 | }); 67 | 68 | it('should not throw on dispose', () => { 69 | const feature = new PuppetfileHoverFeature(context, logger); 70 | 71 | assert.doesNotThrow(() => feature.dispose()); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/test/suite/feature/UpdateConfigurationFeature.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { afterEach, before, describe, it } from 'mocha'; 3 | import * as sinon from 'sinon'; 4 | import * as vscode from 'vscode'; 5 | import { UpdateConfigurationFeature } from '../../../feature/UpdateConfigurationFeature'; 6 | import { ILogger } from '../../../logging'; 7 | import * as index from '../index'; 8 | 9 | describe('UpdateConfigurationFeature', () => { 10 | let updateConfigFeature: UpdateConfigurationFeature; 11 | let mockLogger: ILogger; 12 | let mockContext: vscode.ExtensionContext; 13 | let registerCommandStub: sinon.SinonStub; 14 | let getConfigurationSpy: sinon.SinonSpy; 15 | let sandbox: sinon.SinonSandbox; 16 | let showInformationMessageStub: sinon.SinonStub; 17 | 18 | before(() => { 19 | sandbox = sinon.createSandbox(); 20 | mockLogger = index.logger; 21 | mockContext = index.extContext; 22 | registerCommandStub = sandbox.stub(vscode.commands, 'registerCommand'); 23 | updateConfigFeature = new UpdateConfigurationFeature(mockLogger, mockContext); 24 | getConfigurationSpy = sandbox.stub(vscode.workspace, 'getConfiguration'); 25 | }); 26 | 27 | afterEach(() => { 28 | sandbox.restore(); 29 | }); 30 | 31 | it('Updates a configuration setting', async () => { 32 | const updateSettingsHash = { 'puppet.editorService.loglevel': 'debug' }; 33 | const mockConfig = { 34 | update: sandbox.stub(), 35 | }; 36 | getConfigurationSpy.returns(mockConfig); 37 | await (updateConfigFeature as any)['updateSettingsAsync'](updateSettingsHash); 38 | assert(getConfigurationSpy.calledOnce); 39 | assert(mockConfig.update.calledWith('puppet.editorService.loglevel', 'debug')); 40 | }); 41 | 42 | it('Prompts for restart if necessary', async () => { 43 | const updateSettingsHash = { 'puppet.editorService.puppet.version': '7.0.0' }; 44 | showInformationMessageStub = sandbox.stub(vscode.window, 'showInformationMessage').returns(Promise.resolve('Yes')); 45 | const executeCommandStub = sandbox.stub(vscode.commands, 'executeCommand'); 46 | await (updateConfigFeature as any)['updateSettingsAsync'](updateSettingsHash); 47 | assert(showInformationMessageStub.calledWith('Puppet extensions needs to restart the editor. Would you like to do that now?')); 48 | assert(executeCommandStub.calledWith('workbench.action.reloadWindow')); 49 | }); 50 | 51 | it('Does not prompt for restart if not necessary', async () => { 52 | const updateSettingsHash = { 'editor.tabSize': 2 }; 53 | showInformationMessageStub = sandbox.stub(vscode.window, 'showInformationMessage').returns(Promise.resolve('No')); 54 | await (updateConfigFeature as any)['updateSettingsAsync'](updateSettingsHash); 55 | assert(showInformationMessageStub.notCalled); 56 | }); 57 | 58 | it('Disposes correctly', () => { 59 | assert.doesNotThrow(() => { 60 | updateConfigFeature.dispose(); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/test/suite/forge.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { expect } from 'chai'; 3 | import { describe, it } from 'mocha'; 4 | import { createAggregrateConfiguration } from '../../configuration'; 5 | import * as forge from '../../forge'; 6 | import { OutputChannelLogger } from '../../logging/outputchannel'; 7 | import { settingsFromWorkspace } from '../../settings'; 8 | 9 | describe('Forge Tests', () => { 10 | 11 | const settings = settingsFromWorkspace(); 12 | const configSettings = createAggregrateConfiguration(settings); 13 | const logger = new OutputChannelLogger(configSettings.workspace.editorService.loglevel); 14 | 15 | it('Retrieves latest PDK version', () => { 16 | return forge.getPDKVersion(logger).then((version) => { 17 | let versionRegex = new RegExp('^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$'); 18 | assert.notStrictEqual(version, undefined); 19 | expect(versionRegex.test(version)).to.be.true; 20 | }); 21 | }); 22 | 23 | it('Fails to retrieve invalid module', () => { 24 | return forge.getModuleInfo('puppetlabs-somefakemodule-1', logger).then((info) => { 25 | assert.strictEqual(info, undefined); 26 | }); 27 | }); 28 | 29 | it('Retrieves module info', () => { 30 | return forge.getModuleInfo('puppetlabs-stdlib', logger).then((info) => { 31 | assert.notStrictEqual(info, undefined); 32 | assert.strictEqual(info.uri, '/v3/modules/puppetlabs-stdlib'); 33 | assert.strictEqual(info.slug, 'puppetlabs-stdlib'); 34 | assert.strictEqual(info.owner.username, 'puppetlabs'); 35 | assert.strictEqual(info.name, 'stdlib'); 36 | }) 37 | }); 38 | 39 | it('Builds valid markdown', () => { 40 | const info: forge.PuppetForgeModuleInfo = { 41 | uri: '/v3/modules/puppetlabs-stdlib', 42 | slug: 'puppetlabs-stdlib', 43 | name: 'puppetlabs/stdlib', 44 | downloads: 0, 45 | score: 0, 46 | created: new Date(), 47 | updated: new Date(), 48 | endorsement: 'supported', 49 | owner: { slug: 'puppetlabs', username: 'puppetlabs' }, 50 | forgeUrl: 'https://forge.puppet.com/modules/puppetlabs/stdlib', 51 | homepageUrl: 'https://github.com/puppetlabs/puppetlabs-stdlib', 52 | version: 0, 53 | summary: 'summary', 54 | }; 55 | const markdown = forge.buildMarkdown(info); 56 | assert.notStrictEqual(markdown, undefined); 57 | assert.strictEqual(`## puppetlabs/stdlib\n 58 | summary\n 59 | **Latest version:** 0 (${info.updated.toDateString()})\n 60 | **Forge**: [https://forge.puppet.com/modules/puppetlabs/stdlib](https://forge.puppet.com/modules/puppetlabs/stdlib)\n 61 | **Project**: [https://github.com/puppetlabs/puppetlabs-stdlib](https://github.com/puppetlabs/puppetlabs-stdlib)\n 62 | **Owner:** puppetlabs\n 63 | **Endorsement:** SUPPORTED\n 64 | **Score:** 0\n 65 | `, markdown.value); 66 | }); 67 | 68 | it('Returns an empty module completion list when passed invalid characters', () => { 69 | // module names cannot start with integers 70 | return forge.getPuppetModuleCompletion('12345612-', logger).then((info) => { 71 | expect(info.modules).to.eql([]); 72 | expect(info.total).to.eql(0); 73 | }); 74 | }); 75 | 76 | it('Retrieves module completion list', () => { 77 | return forge.getPuppetModuleCompletion('puppetlabs-', logger).then((info) => { 78 | assert.notStrictEqual(info, undefined); 79 | expect(info.total).to.be.greaterThan(0); 80 | expect(info.modules).to.include('puppetlabs-stdlib'); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/test/suite/handler.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { after, before, describe, it } from 'mocha'; 3 | import * as sinon from 'sinon'; 4 | import * as vscode from 'vscode'; 5 | import { PuppetStatusBarFeature } from '../../feature/PuppetStatusBarFeature'; 6 | import { ConnectionHandler } from '../../handler'; 7 | import { StdioConnectionHandler } from '../../handlers/stdio'; 8 | import { TcpConnectionHandler } from '../../handlers/tcp'; 9 | import { ConnectionStatus } from '../../interfaces'; 10 | import { ProtocolType } from '../../settings'; 11 | import * as index from './index'; 12 | 13 | let statusBar: PuppetStatusBarFeature; 14 | let stdioConnectionHandler: StdioConnectionHandler; 15 | let tcpConnectionHandler: TcpConnectionHandler; 16 | let setConnectionStatusSpy: sinon.SinonSpy; 17 | let registerCommandStub: sinon.SinonStub; 18 | let disposableStub: sinon.SinonStubbedInstance; 19 | let sandbox: sinon.SinonSandbox; 20 | 21 | describe('Stdio Handler Tests', () => { 22 | 23 | before(() => { 24 | sandbox = sinon.createSandbox(); 25 | statusBar = sandbox.createStubInstance(PuppetStatusBarFeature) 26 | setConnectionStatusSpy = sandbox.spy(ConnectionHandler.prototype, 'setConnectionStatus'); 27 | disposableStub = sandbox.createStubInstance(vscode.Disposable); 28 | registerCommandStub = sandbox.stub(vscode.commands, 'registerCommand').returns(disposableStub); 29 | }); 30 | 31 | after(() => { 32 | sandbox.restore(); 33 | }); 34 | 35 | it('StdioConnectionHandler is created', () => { 36 | stdioConnectionHandler = new StdioConnectionHandler(index.extContext, statusBar, index.logger, index.configSettings, index.puppetLangID, index.puppetFileLangID); 37 | assert.notStrictEqual(stdioConnectionHandler, undefined); 38 | assert.strictEqual(stdioConnectionHandler.connectionType, 1) // ConnectionType local = 1 i.e. local 39 | assert(setConnectionStatusSpy.calledWith('Initializing', ConnectionStatus.Initializing)); 40 | }); 41 | 42 | it('Stdio connection is established', () => { 43 | assert(stdioConnectionHandler.protocolType, 'stdio'); 44 | assert(setConnectionStatusSpy.calledWith('Initialization Complete', ConnectionStatus.InitializationComplete)); 45 | assert(stdioConnectionHandler.status, 'Initialization Complete'); 46 | }); 47 | 48 | it('Generates Server Options', () => { 49 | const serverOptions = stdioConnectionHandler.createServerOptions(); 50 | assert.notStrictEqual(serverOptions, undefined); 51 | }); 52 | }); 53 | 54 | 55 | describe('TCP Handler Tests', () => { 56 | 57 | before(() => { 58 | sandbox = sinon.createSandbox(); 59 | index.configSettings.workspace.editorService.protocol = ProtocolType.TCP; 60 | statusBar = sandbox.createStubInstance(PuppetStatusBarFeature); 61 | disposableStub = sandbox.createStubInstance(vscode.Disposable); 62 | registerCommandStub = sandbox.stub(vscode.commands, 'registerCommand').returns(disposableStub); 63 | }); 64 | 65 | after(() => { 66 | sandbox.restore(); 67 | }); 68 | 69 | it('TcpConnectionHandler is created', () => { 70 | tcpConnectionHandler = new TcpConnectionHandler(index.extContext, statusBar, index.logger, index.configSettings, index.puppetLangID, index.puppetFileLangID); 71 | assert.notStrictEqual(tcpConnectionHandler, undefined); 72 | assert.strictEqual(tcpConnectionHandler.connectionType, 1) // ConnectionType local = 1 i.e. Local 73 | assert(setConnectionStatusSpy.calledWith('Initializing', ConnectionStatus.Initializing)); 74 | }); 75 | 76 | it('TCP connection is established', () => { 77 | assert(tcpConnectionHandler.protocolType, 'tcp'); 78 | assert(setConnectionStatusSpy.calledWith('Initialization Complete', ConnectionStatus.InitializationComplete)); 79 | assert(tcpConnectionHandler.status, 'Initialization Complete'); 80 | }); 81 | 82 | it('Generates TCP Server Options', () => { 83 | const serverOptions = tcpConnectionHandler.createServerOptions(); 84 | assert.notStrictEqual(serverOptions, undefined); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as glob from 'glob'; 2 | import * as Mocha from 'mocha'; 3 | import * as path from 'path'; 4 | import * as sinon from 'sinon'; 5 | import * as vscode from 'vscode'; 6 | import { IAggregateConfiguration, createAggregrateConfiguration } from '../../configuration'; 7 | import { OutputChannelLogger } from '../../logging/outputchannel'; 8 | import { ISettings, defaultWorkspaceSettings, settingsFromWorkspace } from '../../settings'; 9 | 10 | 11 | export const puppetLangID = 'puppet'; 12 | export const puppetFileLangID = 'puppetfile'; 13 | export const defaultSettings: ISettings = defaultWorkspaceSettings(); 14 | export const workspaceSettings: ISettings = settingsFromWorkspace(); 15 | 16 | export let configSettings: IAggregateConfiguration = createAggregrateConfiguration(workspaceSettings); 17 | export let logger: OutputChannelLogger = new OutputChannelLogger(configSettings.workspace.editorService.loglevel); 18 | // create sinon sandbox to enable stubbing and mocking of extension context 19 | export const sandbox = sinon.createSandbox(); 20 | 21 | export const extContext: vscode.ExtensionContext = { 22 | extension: sandbox.stub(), 23 | asAbsolutePath: sandbox.stub(), 24 | storagePath: '/path/to/storage', 25 | globalStoragePath: '/path/to/global/storage', 26 | logPath: '/path/to/log', 27 | extensionUri: sandbox.stub(), 28 | environmentVariableCollection: sandbox.stub(), 29 | extensionMode: vscode.ExtensionMode.Production, 30 | globalStorageUri: sandbox.stub(), 31 | logUri: sandbox.stub(), 32 | storageUri: sandbox.stub(), 33 | subscriptions: [], 34 | globalState: sandbox.stub(), 35 | workspaceState: sandbox.stub(), 36 | secrets: sandbox.stub(), 37 | extensionPath: '/path/to/extension', 38 | }; 39 | 40 | export function run(): Promise { 41 | // Create the mocha test 42 | const mocha = new Mocha({ 43 | ui: 'tdd', 44 | color: true, 45 | }); 46 | 47 | const testsRoot = path.resolve(__dirname, '..'); 48 | 49 | return new Promise((c, e) => { 50 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 51 | if (err) { 52 | return e(err); 53 | } 54 | 55 | // Add files to the test suite 56 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 57 | 58 | try { 59 | // Run the mocha test 60 | mocha.run((failures) => { 61 | if (failures > 0) { 62 | e(new Error(`${failures} tests failed.`)); 63 | } else { 64 | c(); 65 | } 66 | }); 67 | } catch (err) { 68 | e(err); 69 | } 70 | }); 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /src/test/suite/links.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import axios from 'axios'; 3 | import * as fs from 'fs'; 4 | import { describe, it } from 'mocha'; 5 | import * as path from 'path'; 6 | import { pdkDownloadLink, releaseNotesLink, troubleShootingLink } from '../../extension'; 7 | describe('Vendored link checks', () => { 8 | let links: string[] = [pdkDownloadLink, releaseNotesLink, troubleShootingLink]; 9 | 10 | links.forEach((link) => { 11 | it(`Extension metadata: should return 200 for ${link}`, async () => { 12 | axios.get(link).then((response) => { 13 | assert.strictEqual(response.status, 200); 14 | }); 15 | }); 16 | }); 17 | 18 | // read the package.json and get the links from the jsonValidation section 19 | const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../../package.json'), 'utf-8')); 20 | links = packageJson.contributes.jsonValidation.map(element => element.url); 21 | links.forEach((link) => { 22 | it(`Validators: should return 200 for ${link}`, async () => { 23 | axios.get(link).then((response) => { 24 | assert.strictEqual(response.status, 200); 25 | }); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/test/suite/paths.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { describe, it } from 'mocha'; 4 | import { PathResolver } from '../../configuration/pathResolver'; 5 | 6 | describe('Path Resolution Tests', () => { 7 | it('resolves programfiles', () => { 8 | switch (process.platform) { 9 | case 'win32': 10 | assert.equal('C:\\Program Files', PathResolver.getprogramFiles()); 11 | break; 12 | default: 13 | assert.equal('/opt', PathResolver.getprogramFiles()); 14 | break; 15 | } 16 | }); 17 | 18 | it('resolves environment PATH seperator', () => { 19 | switch (process.platform) { 20 | case 'win32': 21 | assert.equal(';', PathResolver.pathEnvSeparator()); 22 | break; 23 | default: 24 | assert.equal(':', PathResolver.pathEnvSeparator()); 25 | break; 26 | } 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/test/suite/pdkResolver.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { describe, it } from 'mocha'; 4 | import * as pdk from '../../configuration/pdkResolver'; 5 | 6 | describe('configuration/pdkResolver Tests', () => { 7 | it('resolves directories that do not exist as an empty instances array', () => { 8 | const result = pdk.pdkInstances('/somedirectory/that/does/not/exist'); 9 | assert.equal(result.instances.length, 0); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/test/suite/settings.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as settings from '../../settings'; 3 | 4 | import { after, before, describe, it } from 'mocha'; 5 | import * as sinon from 'sinon'; 6 | import * as vscode from 'vscode'; 7 | import * as index from './index'; 8 | 9 | let sandbox: sinon.SinonSandbox; 10 | let workspaceConfigurationStub: sinon.SinonStubbedInstance; 11 | 12 | describe('Settings Tests', () => { 13 | 14 | before(() => { 15 | sandbox = sinon.createSandbox(); 16 | workspaceConfigurationStub = { 17 | get: sandbox.stub(), 18 | has: sandbox.stub(), 19 | inspect: sandbox.stub(), 20 | update: sandbox.stub(), 21 | }; 22 | sandbox.stub(vscode.workspace, 'getConfiguration').returns(workspaceConfigurationStub); 23 | }); 24 | 25 | after(() => { 26 | sandbox.restore(); 27 | }); 28 | 29 | it('Default settings are populated', () => { 30 | const defaultWorkspaceSettings = index.defaultSettings; 31 | assert.notStrictEqual(defaultWorkspaceSettings, undefined); 32 | }); 33 | 34 | it('Retrieves settings from workspace', () => { 35 | let editorServiceSettings: settings.IEditorServiceSettings = { 36 | enable: false, 37 | timeout: 50, 38 | }; 39 | let pdkSettings: settings.IPDKSettings = { 40 | checkVersion: false, 41 | }; 42 | workspaceConfigurationStub.get.withArgs('editorService').returns(editorServiceSettings); 43 | workspaceConfigurationStub.get.withArgs('pdk').returns(pdkSettings); 44 | const workspaceSettings = settings.settingsFromWorkspace(); 45 | assert.notStrictEqual(workspaceSettings, undefined); 46 | assert.strictEqual(workspaceSettings.editorService.enable, false); 47 | assert.strictEqual(workspaceSettings.editorService.timeout, 50); 48 | assert.strictEqual(workspaceSettings.pdk.checkVersion, false); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/test/suite/telemetry.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { describe, it } from 'mocha'; 3 | import * as telemetry from '../../telemetry'; 4 | 5 | describe('Telemetry Tests', () => { 6 | const reporter = telemetry.reporter; 7 | it('Telemetry is enabled', () => { 8 | assert.notStrictEqual(reporter, undefined); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/views/facts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | commands, 3 | Event, 4 | EventEmitter, 5 | ProviderResult, 6 | ThemeIcon, 7 | TreeDataProvider, 8 | TreeItem, 9 | TreeItemCollapsibleState, 10 | } from 'vscode'; 11 | import { RequestType0 } from 'vscode-languageclient/node'; 12 | import { ConnectionHandler } from '../handler'; 13 | import { PuppetVersionDetails } from '../messages'; 14 | import { reporter } from '../telemetry'; 15 | 16 | class PuppetFact extends TreeItem { 17 | constructor( 18 | public readonly label: string, 19 | private value: string, 20 | public readonly collapsibleState: TreeItemCollapsibleState, 21 | public readonly children?: Array<[string, PuppetFact]>, 22 | ) { 23 | super(label, collapsibleState); 24 | this.tooltip = `${this.label}-${this.value}`; 25 | this.description = this.value; 26 | if (children) { 27 | this.iconPath = ThemeIcon.Folder; 28 | } else { 29 | this.iconPath = ThemeIcon.File; 30 | } 31 | } 32 | } 33 | 34 | interface PuppetFactResponse { 35 | facts: string; 36 | error: string; 37 | } 38 | 39 | export class PuppetFactsProvider implements TreeDataProvider { 40 | private elements: Array<[string, PuppetFact]> = []; 41 | private _onDidChangeTreeData: EventEmitter = new EventEmitter(); 42 | readonly onDidChangeTreeData: Event; 43 | 44 | constructor(protected handler: ConnectionHandler) { 45 | this.onDidChangeTreeData = this._onDidChangeTreeData.event; 46 | commands.registerCommand('puppet.refreshFacts', () => this.refresh()); 47 | } 48 | 49 | refresh(): void { 50 | this._onDidChangeTreeData.fire(undefined); 51 | } 52 | 53 | getTreeItem(element: PuppetFact): TreeItem | Thenable { 54 | return element; 55 | } 56 | 57 | getChildren(element?: PuppetFact): Promise { 58 | if (element) { 59 | return Promise.resolve(element.children.map((e) => e[1])); 60 | } else { 61 | return this.getFactsFromLanguageServer(); 62 | } 63 | } 64 | 65 | private async getFactsFromLanguageServer(): Promise { 66 | /* 67 | this is problematic because we both store this and return the value 68 | but this allows us to cache the info for quick expands of the node. 69 | if we didn't cache, we would have to call out for each expand and getting 70 | facts is slow. 71 | */ 72 | await this.handler.languageClient.start(); 73 | 74 | const details = await this.handler.languageClient.sendRequest( 75 | new RequestType0('puppet/getVersion'), 76 | ); 77 | if (!details.factsLoaded) { 78 | // language server is ready, but hasn't loaded facts yet 79 | return new Promise((resolve) => { 80 | let count = 0; 81 | const handle = setInterval(async () => { 82 | count++; 83 | if (count >= 60) { 84 | clearInterval(handle); 85 | 86 | const results = await this.handler.languageClient.sendRequest( 87 | new RequestType0('puppet/getFacts'), 88 | ); 89 | this.elements = this.toList(results.facts); 90 | 91 | if (reporter) { 92 | reporter.sendTelemetryEvent('puppetFacts'); 93 | } 94 | 95 | resolve(this.elements.map((e) => e[1])); 96 | } 97 | 98 | const details = await this.handler.languageClient.sendRequest( 99 | new RequestType0('puppet/getVersion'), 100 | ); 101 | if (details.factsLoaded) { 102 | clearInterval(handle); 103 | 104 | const results = await this.handler.languageClient.sendRequest( 105 | new RequestType0('puppet/getFacts'), 106 | ); 107 | this.elements = this.toList(results.facts); 108 | 109 | if (reporter) { 110 | reporter.sendTelemetryEvent('puppetFacts'); 111 | } 112 | 113 | resolve(this.elements.map((e) => e[1])); 114 | } else { 115 | // not ready yet 116 | } 117 | }, 1000); 118 | }); 119 | } 120 | 121 | const results = await this.handler.languageClient.sendRequest( 122 | new RequestType0('puppet/getFacts'), 123 | ); 124 | this.elements = this.toList(results.facts); 125 | 126 | if (reporter) { 127 | reporter.sendTelemetryEvent('puppetFacts'); 128 | } 129 | 130 | return this.elements.map((e) => e[1]); 131 | } 132 | 133 | getParent?(element: PuppetFact): ProviderResult { 134 | throw new Error('Method not implemented.'); 135 | } 136 | 137 | toList(data: any): Array<[string, PuppetFact]> { 138 | const things: Array<[string, PuppetFact]> = []; 139 | 140 | for (const key of Object.keys(data)) { 141 | const value = data[key]; 142 | if (Object.prototype.toString.call(value) === '[object Object]') { 143 | const children = this.toList(value); 144 | const item = new PuppetFact(key, value, TreeItemCollapsibleState.Collapsed, children); 145 | things.push([key, item]); 146 | } else { 147 | things.push([key, new PuppetFact(key, value.toString(), TreeItemCollapsibleState.None)]); 148 | } 149 | } 150 | 151 | return things; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/views/puppetfile.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { 3 | commands, 4 | Event, 5 | EventEmitter, 6 | ProviderResult, 7 | ThemeIcon, 8 | TreeDataProvider, 9 | TreeItem, 10 | TreeItemCollapsibleState, 11 | Uri, 12 | ViewColumn, 13 | window, 14 | workspace, 15 | } from 'vscode'; 16 | import { RequestType } from 'vscode-languageclient'; 17 | import { ConnectionHandler } from '../handler'; 18 | import { reporter } from '../telemetry'; 19 | 20 | class PuppetfileDependencyItem extends TreeItem { 21 | constructor( 22 | public readonly name: string, 23 | public readonly version: string, 24 | public readonly startLine: number, 25 | public readonly endLine: number, 26 | public readonly collapsibleState: TreeItemCollapsibleState, 27 | public readonly children?: Array<[string, PuppetfileDependencyItem]>, 28 | ) { 29 | super(name, collapsibleState); 30 | this.tooltip = `${name}-${version}`; 31 | this.description = version; 32 | if (children) { 33 | this.iconPath = ThemeIcon.Folder; 34 | } else { 35 | this.iconPath = new ThemeIcon('package'); 36 | } 37 | } 38 | } 39 | 40 | class PuppetfileDependency { 41 | constructor( 42 | public readonly name: string, 43 | public readonly version: string, 44 | public readonly startLine: number, 45 | public readonly endLine: number, 46 | ) { 47 | // 48 | } 49 | } 50 | 51 | interface PuppetfileDependencyResponse { 52 | dependencies: PuppetfileDependency[]; 53 | error: string[]; 54 | } 55 | 56 | export class PuppetfileProvider implements TreeDataProvider { 57 | private _onDidChangeTreeData: EventEmitter = new EventEmitter< 58 | PuppetfileDependencyItem | undefined 59 | >(); 60 | readonly onDidChangeTreeData: Event; 61 | 62 | constructor(protected handler: ConnectionHandler) { 63 | this.onDidChangeTreeData = this._onDidChangeTreeData.event; 64 | commands.registerCommand('puppet.refreshPuppetfileDependencies', () => { 65 | reporter.sendTelemetryEvent('puppet.refreshPuppetfileDependencies'); 66 | this.refresh(); 67 | }); 68 | commands.registerCommand('puppet.goToPuppetfileDefinition', (puppetModule: PuppetfileDependencyItem) => { 69 | reporter.sendTelemetryEvent('puppet.goToPuppetfileDefinition'); 70 | 71 | const workspaceFolder = workspace.workspaceFolders[0].uri; 72 | const puppetfile = path.join(workspaceFolder.fsPath, 'Puppetfile'); 73 | workspace.openTextDocument(puppetfile).then((doc) => { 74 | const line = doc.lineAt(+puppetModule.startLine); 75 | window.showTextDocument(doc, { 76 | preserveFocus: true, 77 | preview: false, 78 | selection: line.range, 79 | viewColumn: ViewColumn.Active, 80 | }); 81 | }); 82 | }); 83 | } 84 | 85 | refresh(): void { 86 | this._onDidChangeTreeData.fire(null); 87 | } 88 | 89 | getTreeItem(element: PuppetfileDependencyItem): TreeItem | Thenable { 90 | return element; 91 | } 92 | 93 | getChildren(element?: PuppetfileDependencyItem): Promise { 94 | if (element) { 95 | return Promise.resolve(element.children.map((e) => e[1])); 96 | } else { 97 | return this.getPuppetfileDependenciesFromLanguageServer(); 98 | } 99 | } 100 | 101 | private async getPuppetfileDependenciesFromLanguageServer(): Promise { 102 | await this.handler.languageClient.start(); 103 | 104 | const fileUri = Uri.file(path.join(workspace.workspaceFolders[0].uri.fsPath, 'Puppetfile')); 105 | /* 106 | We use openTextDocument here because we need to parse whether or not a user has opened a 107 | Puppetfile or not. This triggers onDidOpen notification which sends the content of the Puppetfile 108 | to the Puppet Language Server which caches the content for puppetfile-resolver to parse 109 | */ 110 | return workspace.openTextDocument(fileUri).then(async () => { 111 | const results = await this.handler.languageClient.sendRequest( 112 | new RequestType('puppetfile/getDependencies'), 113 | { 114 | uri: fileUri.toString(), 115 | }, 116 | ); 117 | 118 | reporter.sendTelemetryEvent('puppetfileView'); 119 | 120 | if (results.error) { 121 | window.showErrorMessage(`${results.error}`); 122 | } 123 | 124 | const list = results.dependencies.map((d) => { 125 | return new PuppetfileDependencyItem(d.name, d.version, d.startLine, d.endLine, TreeItemCollapsibleState.None); 126 | }); 127 | 128 | return list; 129 | }); 130 | } 131 | 132 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 133 | getParent?(element: PuppetfileDependencyItem): ProviderResult { 134 | throw new Error('Method not implemented.'); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /syntaxes/.gitignore: -------------------------------------------------------------------------------- 1 | # The puppet.tmLanguage file is vendored and should not be checked in 2 | puppet.tmLanguage 3 | -------------------------------------------------------------------------------- /tools/RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Puppet Extension for VSCode 2 | 3 | ## Pre-release steps 4 | 5 | ### Vendor Latest Puppet Language Server and Syntax Files 6 | 7 | 1. Ensure [puppet-editor-services](https://github.com/puppetlabs/puppet-editor-services) has been tagged and release process followed. Note version. 8 | 1. Ensure [puppet-editor-syntax](https://github.com/puppetlabs/puppet-editor-syntax) has been tagged and release process followed. Note version. 9 | 1. `git clone https://github.com/puppetlabs/puppet-vscode` or `git clean -xfd` on working copy 10 | 1. Update `editor-components.json` with latest `puppet-editor-services` and `puppet-editor-syntax` versions. These can be seperate commits 11 | 1. Create PR 12 | 13 | ## Prepare VSCode extension 14 | 15 | 1. `git clone https://github.com/puppetlabs/puppet-vscode` or `git clean -xfd` on working copy 16 | 1. Update `CHANGELOG` with all tickets from release milestone for `puppet-vscode`, and any tickets from `puppet-editor-services` and `puppet-editor-syntax` that apply to this release. 17 | 1. Increment `version` field of `package.json` 18 | 1. Run `npm install` to update `package-lock.json` 19 | 1. `git commit -m '(maint) Release '` 20 | 1. Create release PR 21 | 22 | ## Package VSCode extension 23 | 24 | 1. `git clone https://github.com/puppetlabs/puppet-vscode` or `git clean -xfd` on working copy 25 | 1. `git tag -a '' -m '' ` 26 | 1. `git push ` 27 | 1. `git checkout ` 28 | 1. `git reset --hard ` 29 | 1. `mkdir 'output'` 30 | 1. `npm install` (this should produce no changes, but package-lock.json may be different, safe to ignore) 31 | 1. `npx vsce package` 32 | 1. `mv "puppet-vscode-*.vsix" 'output'` 33 | 1. `./tools/release.ps1 -releaseversion -guthubusername 'lingua-pupuli' -githubtoken ` 34 | 35 | ## Publish VSCode extnsion 36 | 37 | 1. Install personal access token from https://pogran.visualstudio.com/puppet-vscode ([instructions](https://code.visualstudio.com/api/working-with-extensions/publishing-extension#get-a-personal-access-token)) 38 | 1. `git clone https://github.com/puppetlabs/puppet-vscode` or `git clean -xfd` on working copy 39 | 1. `npm install` 40 | 1. `npmx vsce publish` 41 | 42 | ## Update the `puppet-vscode` docs website 43 | 44 | 1. Checkout `https://github.com/lingua-pupuli/docs` 45 | 1. Run the release update PowerShell script 46 | 47 | ``` powershell 48 | PS> docs/vscode/update-from-source.ps1 -ExtensionSourcePath C:\puppet-vscode 49 | ``` 50 | 51 | 1. Create a commit for the changes and raise a Pull Request 52 | -------------------------------------------------------------------------------- /tools/release.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [parameter(Mandatory = $true)] 3 | [String]$ReleaseVersion, 4 | 5 | [parameter(Mandatory = $true)] 6 | [String]$GitHubUsername, 7 | 8 | [parameter(Mandatory = $true)] 9 | [String]$GitHubToken 10 | ) 11 | 12 | $ErrorActionPreference = 'Stop' 13 | 14 | # Adapted from https://www.herebedragons.io/powershell-create-github-release-with-artifact 15 | function Update-GitHubRelease { 16 | param( 17 | $VersionNumber, 18 | $PreRelease, 19 | $ReleaseNotes, 20 | $ArtifactOutputDirectory, 21 | $GitHubUsername, 22 | $GitHubRepository, 23 | $GitHubApiUsername, 24 | $GitHubApiKey 25 | ) 26 | 27 | $draft = $false 28 | 29 | $auth = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($gitHubApiUsername + ':' + $gitHubApiKey)); 30 | 31 | # Github uses TLS 1.2 32 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 33 | 34 | # Find existing release 35 | $ReleaseDetails = $null 36 | $releaseParams = @{ 37 | Uri = "https://api.github.com/repos/$gitHubUsername/$gitHubRepository/releases/tags/$versionNumber"; 38 | Method = 'GET'; 39 | ContentType = 'application/json'; 40 | Headers = @{ 41 | Authorization = $auth; 42 | } 43 | } 44 | 45 | try { 46 | $ReleaseDetails = Invoke-RestMethod @releaseParams 47 | } 48 | catch { 49 | # Release is missing, create it 50 | $ReleaseDetails = $null 51 | } 52 | 53 | if ($ReleaseDetails -eq $null) { 54 | Write-Host "Creating release $versionNumber" 55 | # Create a release 56 | $releaseData = @{ 57 | tag_name = [string]::Format("{0}", $versionNumber); 58 | name = [string]::Format("{0}", $versionNumber); 59 | body = $releaseNotes; 60 | draft = $draft; 61 | prerelease = $preRelease; 62 | } 63 | $releaseParams = @{ 64 | ContentType = 'application/json' 65 | Uri = "https://api.github.com/repos/$gitHubUsername/$gitHubRepository/releases"; 66 | Method = 'POST'; 67 | Headers = @{ 68 | Authorization = $auth; 69 | } 70 | Body = (ConvertTo-Json $releaseData -Compress) 71 | } 72 | $ReleaseDetails = Invoke-RestMethod @releaseParams 73 | } 74 | else { 75 | Write-Host "Updating release $versionNumber" 76 | # Create a release 77 | $releaseData = @{ 78 | tag_name = [string]::Format("{0}", $versionNumber); 79 | name = [string]::Format("{0}", $versionNumber); 80 | body = $releaseNotes; 81 | draft = $draft; 82 | prerelease = $preRelease; 83 | } 84 | $releaseParams = @{ 85 | ContentType = 'application/json' 86 | Uri = "https://api.github.com/repos/$gitHubUsername/$gitHubRepository/releases/$($ReleaseDetails.id)"; 87 | Method = 'PATCH'; 88 | Headers = @{ 89 | Authorization = $auth; 90 | } 91 | Body = (ConvertTo-Json $releaseData -Compress) 92 | } 93 | $ReleaseDetails = Invoke-RestMethod @releaseParams 94 | } 95 | 96 | # Upload assets 97 | $uploadUri = $ReleaseDetails | Select-Object -ExpandProperty upload_url 98 | $uploadUri = $uploadUri -creplace '\{\?name,label\}' 99 | 100 | if (Test-Path -Path $artifactOutputDirectory) { 101 | Get-ChildItem -Path $artifactOutputDirectory | ForEach-Object { 102 | $filename = $_.Name 103 | $filepath = $_.Fullname 104 | Write-Host "Uploading $filename ..." 105 | 106 | $uploadParams = @{ 107 | Uri = $uploadUri; 108 | Method = 'POST'; 109 | Headers = @{ 110 | Authorization = $auth; 111 | } 112 | ContentType = 'application/text'; 113 | InFile = $filepath 114 | } 115 | 116 | if ($filename -match '\.zip$') { 117 | $uploadParams.ContentType = 'application/zip' 118 | } 119 | if ($filename -match '\.gz$') { 120 | $uploadParams.ContentType = 'application/tar+gzip' 121 | } 122 | $uploadParams.Uri += "?name=$filename" 123 | 124 | Invoke-RestMethod @uploadParams | Out-Null 125 | } 126 | } else { 127 | Write-Host "No assets to upload as '$artifactOutputDirectory' doesn't exist" 128 | } 129 | } 130 | 131 | function Get-ReleaseNotes { 132 | param($Version) 133 | 134 | Write-Host "Getting release notes for version $Version ..." 135 | 136 | $changelog = Join-Path -Path $PSScriptRoot -ChildPath '..\CHANGELOG.md' 137 | $releaseNotes = $null 138 | $inSection = $false 139 | 140 | Get-Content $changelog | ForEach-Object { 141 | $line = $_ 142 | 143 | if ($inSection) { 144 | if ($line -match "^## ") { 145 | $inSection = $false 146 | } 147 | else { 148 | $releaseNotes = $releaseNotes + "`n" + $line 149 | } 150 | } 151 | else { 152 | if (($line -match "^## \[${version}\] ") -or ($line -match "^## ${version} ")) { 153 | $releaseNotes = $line 154 | $inSection = $true 155 | } 156 | } 157 | } 158 | 159 | return ($releaseNotes -replace "\[${version}\]", $version) 160 | } 161 | 162 | $params = @{ 163 | VersionNumber = $releaseVersion 164 | PreRelease = $false 165 | ReleaseNotes = (Get-ReleaseNotes -Version $releaseVersion) 166 | ArtifactOutputDirectory = 'output' 167 | GitHubUsername = 'lingua-pupuli' 168 | GitHubRepository = 'puppet-vscode' 169 | GitHubApiUsername = $GitHubUsername 170 | GitHubApiKey = $GitHubToken 171 | } 172 | Update-GitHubRelease @params 173 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": ["dom", "es2016"], 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | /* Strict Type-Checking Option */ 10 | "strict": false /* enable all strict type-checking options */, 11 | /* Additional Checks */ 12 | "noUnusedLocals": false /* Report errors on unused locals. */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | }, 17 | "exclude": ["node_modules", ".vscode-test"] 18 | } 19 | --------------------------------------------------------------------------------