├── .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 |
4 |
--------------------------------------------------------------------------------
/assets/icons/light/sync.svg:
--------------------------------------------------------------------------------
1 |
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 |
14 |
--------------------------------------------------------------------------------
/images/puppet-dag-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
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 |
--------------------------------------------------------------------------------