├── .editorconfig ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── enhancement.md ├── pull_request_template.md └── workflows │ ├── CI.yaml │ └── release.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── .yarnrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DCO ├── Jenkinsfile ├── LICENSE ├── README.md ├── USAGE_DATA.md ├── build └── polyfills │ └── yamlFormatter.js ├── custom-settings.json ├── icon └── icon128.png ├── images └── demo.gif ├── language-configuration.json ├── package.json ├── scripts ├── check-dependencies.js └── e2e.sh ├── src ├── extension.ts ├── extensionConflicts.ts ├── json-schema-cache.ts ├── json-schema-content-provider.ts ├── node │ └── yamlClientMain.ts ├── paths.ts ├── recommendation │ ├── handler.ts │ ├── handlerImpl.ts │ ├── index.ts │ └── openShiftToolkit.ts ├── schema-extension-api.ts ├── schema-status-bar-item.ts ├── telemetry.ts └── webworker │ └── yamlClientMain.ts ├── syntaxes └── yaml.tmLanguage.json ├── test ├── completion.test.ts ├── helper.ts ├── index.ts ├── json-schema-cache.test.ts ├── json-schema-selection.test.ts ├── json-shema-content-provider.test.ts ├── schemaModification.test.ts ├── schemaProvider.test.ts ├── telemetry.test.ts ├── testFixture │ ├── completion │ │ ├── .travis.yml │ │ ├── completion.yaml │ │ ├── enum_completion.yaml │ │ └── schemaProvider.yaml │ ├── hover │ │ └── basic.yaml │ ├── schemas │ │ └── basic_completion_schema.json │ └── validation │ │ └── schemaProvider.yaml ├── testRunner.ts └── ui-test │ ├── allTestsSuite.ts │ ├── autocompletionTest.ts │ ├── common │ └── YAMLConstants.ts │ ├── contentAssistTest.ts │ ├── customTagsTest.ts │ ├── extensionUITest.ts │ ├── schemaIsSetTest.ts │ └── util │ └── utility.ts ├── thirdpartynotices.txt ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Tab indentation 2 | [*] 3 | indent_style = space 4 | indent_size = 2 5 | trim_trailing_whitespace = true 6 | 7 | [{.travis.yml,npm-shrinkwrap.json,package.json}] 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | env: { 5 | node: true, 6 | es6: true, 7 | }, 8 | parserOptions: { 9 | sourceType: 'module', 10 | }, 11 | plugins: ['@typescript-eslint', 'prettier'], 12 | extends: [ 13 | 'eslint:recommended', 14 | 'plugin:@typescript-eslint/eslint-recommended', 15 | 'plugin:@typescript-eslint/recommended', 16 | 'prettier', 17 | ], 18 | rules: { 19 | 'prettier/prettier': 'error', 20 | '@typescript-eslint/no-use-before-define': ['error', { functions: false, classes: false }], 21 | '@typescript-eslint/no-unused-vars': ['warn'], 22 | '@typescript-eslint/explicit-function-return-type': [1, { allowExpressions: true }], 23 | 'eol-last': ['error'], 24 | 'space-infix-ops': ['error', { int32Hint: false }], 25 | 'no-multi-spaces': ['error', { ignoreEOLComments: true }], 26 | 'keyword-spacing': ['error'], 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Global Owners 2 | * @evidolob 3 | * @msivasubramaniaan 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 🐞 3 | about: Report a bug found in VSCode-YAML 4 | title: '' 5 | labels: 'kind/bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | 20 | 21 | ## Expected Behavior 22 | 23 | 24 | ## Current Behavior 25 | 26 | 27 | ## Steps to Reproduce 28 | 29 | 1. 30 | 2. 31 | 3. 32 | 33 | ## Environment 34 | - [ ] Windows 35 | - [ ] Mac 36 | - [ ] Linux 37 | - [ ] other (please specify) 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser 3 | blank_issues_enabled: false # default is true 4 | contact_links: 5 | - name: Discussions 6 | url: https://github.com/redhat-developer/vscode-yaml/discussions 7 | about: > 8 | Any kind of questions should go onto discussions. 9 | We recommend using this even if you suspect a bug or have 10 | a feature request, so the core team can keep the issue tracker 11 | clean. Also, note that schema specific bugs should go to their 12 | repositories as our extension does not contain any schema. 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement 💡 3 | about: Suggest an enhancement for VSCode-YAML 4 | title: '' 5 | labels: 'kind/enhancement' 6 | assignees: '' 7 | 8 | --- 9 | ### Is your enhancement related to a problem? Please describe. 10 | 11 | 12 | ### Describe the solution you would like 13 | 14 | 15 | ### Describe alternatives you have considered 16 | 17 | 18 | ### Additional context 19 | 20 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### What does this PR do? 2 | 3 | 4 | ### What issues does this PR fix or reference? 5 | 6 | 7 | ### Is it tested? How? 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/CI.yaml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the main branch 7 | on: 8 | push: 9 | branches: [main] 10 | pull_request: 11 | branches: [main] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | # The type of runner that the job will run on 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | os: [macos-latest, windows-latest, ubuntu-latest] 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v4 27 | 28 | # Set up Node 29 | - name: Use Node 20 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: 20 33 | 34 | # Run install dependencies 35 | - name: Install dependencies 36 | run: yarn 37 | 38 | # Build extension 39 | - name: Run build 40 | run: yarn build 41 | 42 | # Run tests 43 | - name: Run Test 44 | uses: coactions/setup-xvfb@b6b4fcfb9f5a895edadc3bc76318fae0ac17c8b3 #v1.0.1 45 | with: 46 | run: yarn test 47 | 48 | # Run UI tests 49 | - name: Run UI Test 50 | uses: coactions/setup-xvfb@b6b4fcfb9f5a895edadc3bc76318fae0ac17c8b3 #v1.0.1 51 | with: 52 | run: yarn run ui-test 53 | options: -screen 0 1920x1080x24 54 | 55 | #Package vsix 56 | - name: Build .vsix package on Linux 57 | if: matrix.os == 'ubuntu-latest' 58 | run: | 59 | VERSION=$(node -p "require('./package.json').version") 60 | npx vsce package -o vscode-yaml-${VERSION}-${GITHUB_RUN_ID}-${GITHUB_RUN_NUMBER}.vsix 61 | 62 | #Upload vsix 63 | - name: Upload linux-built vsix 64 | if: matrix.os == 'ubuntu-latest' 65 | uses: actions/upload-artifact@v4 66 | with: 67 | name: vscode-yaml 68 | path: vscode-yaml*.vsix 69 | 70 | # Archive test results 71 | - name: Archiving test artifacts 72 | uses: actions/upload-artifact@v4 73 | with: 74 | name: ${{ matrix.os }}-artifacts 75 | path: | 76 | test-resources/screenshots/*.png 77 | retention-days: 2 78 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | publishPreRelease: 7 | description: 'Publish a pre-release ?' 8 | required: true 9 | type: choice 10 | options: 11 | - 'true' 12 | - 'false' 13 | default: 'false' 14 | publishToMarketPlace: 15 | description: 'Publish to VS Code Marketplace ?' 16 | required: true 17 | type: choice 18 | options: 19 | - 'true' 20 | - 'false' 21 | default: 'true' 22 | publishToOVSX: 23 | description: 'Publish to OpenVSX Registry ?' 24 | required: true 25 | type: choice 26 | options: 27 | - 'true' 28 | - 'false' 29 | default: 'true' 30 | 31 | jobs: 32 | packaging-job: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Checkout vscode-yaml 36 | uses: actions/checkout@v4 37 | - name: Use Node.js 38 | uses: actions/setup-node@v4 39 | with: 40 | path: release 41 | node-version: 20 42 | - name: Install dependencies 43 | run: | 44 | npm install -g typescript "yarn" "@vscode/vsce" "ovsx" 45 | echo "EXT_VERSION=$(cat package.json | jq -r .version)" >> $GITHUB_ENV 46 | - name: Build vscode-yaml 47 | uses: coactions/setup-xvfb@b6b4fcfb9f5a895edadc3bc76318fae0ac17c8b3 #v1.0.1 48 | with: 49 | run: | 50 | yarn install 51 | yarn run build 52 | yarn run check-dependencies 53 | - name: Run Unit Tests 54 | uses: coactions/setup-xvfb@b6b4fcfb9f5a895edadc3bc76318fae0ac17c8b3 #v1.0.1 55 | with: 56 | run: yarn test --silent 57 | - name: Package 58 | run: | 59 | vsce package -o vscode-yaml-${{ env.EXT_VERSION }}-${GITHUB_RUN_NUMBER}.vsix 60 | sha256sum *.vsix > vscode-yaml-${{ env.EXT_VERSION }}-${GITHUB_RUN_NUMBER}.vsix.sha256 61 | ls -lash *.vsix *.sha256 62 | - name: Upload VSIX Artifacts 63 | uses: actions/upload-artifact@v4 64 | with: 65 | name: vscode-yaml 66 | path: vscode-yaml-${{ env.EXT_VERSION }}-${{github.run_number}}*.vsix 67 | if-no-files-found: error 68 | - name: Publish to GH Release Tab 69 | if: ${{ inputs.publishToMarketPlace == 'true' && inputs.publishToOVSX == 'true' }} 70 | uses: "marvinpinto/action-automatic-releases@919008cf3f741b179569b7a6fb4d8860689ab7f0" 71 | with: 72 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 73 | automatic_release_tag: "${{ env.EXT_VERSION }}" 74 | draft: true 75 | files: | 76 | vscode-yaml-${{ env.EXT_VERSION }}-${{github.run_number}}*.vsix 77 | vscode-yaml-${{ env.EXT_VERSION }}-${{github.run_number}}*.sha256 78 | 79 | release-job: 80 | if: ${{ inputs.publishToMarketPlace == 'true' || inputs.publishToOVSX == 'true' }} 81 | environment: ${{ (inputs.publishToMarketPlace == 'true' || inputs.publishToOVSX == 'true') && 'release' || 'pre-release' }} 82 | runs-on: ubuntu-latest 83 | needs: packaging-job 84 | steps: 85 | - name: Checkout vscode-yaml 86 | uses: actions/checkout@v4 87 | - name: Use Node.js 88 | uses: actions/setup-node@v4 89 | with: 90 | node-version: 20 91 | - name: Install dependencies 92 | run: | 93 | npm install -g typescript "yarn" "@vscode/vsce" "ovsx" 94 | echo "EXT_VERSION=$(cat package.json | jq -r .version)" >> $GITHUB_ENV 95 | - name: Download VSIX Artifacts 96 | uses: actions/download-artifact@v4 97 | - name: Publish to VS Code Marketplace 98 | if: ${{ github.event_name == 'schedule' || inputs.publishToMarketPlace == 'true' || inputs.publishPreRelease == 'true' }} 99 | run: | 100 | vsce publish -p ${{ secrets.VSCODE_MARKETPLACE_TOKEN }} --packagePath vscode-yaml/vscode-yaml-${{ env.EXT_VERSION }}-${GITHUB_RUN_NUMBER}.vsix 101 | - name: Publish to OpenVSX Registry 102 | if: ${{ github.event_name == 'schedule' || inputs.publishToOVSX == 'true' || inputs.publishPreRelease == 'true' }} 103 | run: | 104 | ovsx publish -p ${{ secrets.OVSX_MARKETPLACE_TOKEN }} --packagePath vscode-yaml/vscode-yaml-${{ env.EXT_VERSION }}-${GITHUB_RUN_NUMBER}.vsix 105 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | out/ 3 | dist/ 4 | .vscode-test/ 5 | .vscode-test-web/ 6 | test/testFixture/.vscode 7 | *.vsix 8 | .DS_Store 9 | test-resources 10 | yarn-error.log -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode-test 2 | out 3 | syntaxes/ 4 | dist 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "printWidth": 130, 4 | "singleQuote": true, 5 | "trailingComma": "es5", 6 | "endOfLine": "auto" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 6 | "dbaeumer.vscode-eslint", 7 | "EditorConfig.EditorConfig" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // A launch configuration that compiles the extension and then opens it inside a new window 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 11 | "sourceMaps": true, 12 | "outFiles": ["${workspaceRoot}/dist/**/*.js"], 13 | "preLaunchTask": "compile typescript", 14 | "env": { 15 | "DEBUG_VSCODE_YAML":"true", 16 | "VSCODE_REDHAT_TELEMETRY_DEBUG":"true" 17 | } 18 | }, 19 | { 20 | "name": "Extension Tests", 21 | "type": "extensionHost", 22 | "request": "launch", 23 | "runtimeExecutable": "${execPath}", 24 | "preLaunchTask": "compile test", 25 | "args": [ 26 | "--disable-extension=ms-kubernetes-tools.vscode-kubernetes-tools", 27 | "--extensionDevelopmentPath=${workspaceFolder}", 28 | "--extensionTestsPath=${workspaceFolder}/out/test", 29 | "${workspaceRoot}/test/testFixture" 30 | ], 31 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 32 | "env": { 33 | "DEBUG_VSCODE_YAML":"true" 34 | } 35 | }, 36 | { 37 | "name": "Launch Web Extension", 38 | "type": "extensionHost", 39 | "debugWebWorkerHost": true, 40 | "request": "launch", 41 | "args": [ 42 | "--extensionDevelopmentPath=${workspaceFolder}", 43 | "--extensionDevelopmentKind=web" 44 | ], 45 | "env": { 46 | "VSCODE_REDHAT_TELEMETRY_DEBUG":"true" 47 | }, 48 | "outFiles": ["${workspaceRoot}/dist/**/*.js"], 49 | "preLaunchTask": "compile webpack" 50 | }, 51 | { 52 | "name": "Debug UI Tests", 53 | "type": "node", 54 | "request": "launch", 55 | "program": "${workspaceFolder}/node_modules/.bin/extest", 56 | "args": [ 57 | "setup-and-run", 58 | "${workspaceFolder}/out/test/ui-test/allTestsSuite.js", 59 | "-o", 60 | "${workspaceFolder}/custom-settings.json", 61 | "--mocha_config", 62 | "${workspaceFolder}/src/ui-test/.mocharc-debug.js" 63 | ], 64 | "console": "integratedTerminal", 65 | "internalConsoleOptions": "neverOpen" 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | "typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version 10 | "editor.quickSuggestions": { 11 | "other": true, 12 | "comments": true, 13 | "strings": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | // A task runner that calls a custom npm script that compiles the extension. 7 | { 8 | "label": "compile typescript", 9 | // the command is a shell script 10 | "type": "shell", 11 | // we run the custom npm script "compile" as defined in package.json 12 | "command": "yarn run compile", 13 | // show the output window only if unrecognized errors occur. 14 | "presentation": { 15 | "reveal": "never" 16 | }, 17 | // The tsc compiler is started in watching mode 18 | "isBackground": true, 19 | // use the standard tsc in watch mode problem matcher to find compile problems in the output. 20 | "problemMatcher": "$tsc-watch" 21 | }, 22 | { 23 | "label": "compile webpack", 24 | "type": "npm", 25 | "script": "compile", 26 | "group": "build", 27 | "isBackground": true, 28 | "problemMatcher": ["$ts-webpack-watch"] 29 | }, 30 | { 31 | "label": "compile test", 32 | // the command is a shell script 33 | "type": "shell", 34 | // we run the custom npm script "compile" as defined in package.json 35 | "command": "yarn test-compile", 36 | // show the output window only if unrecognized errors occur. 37 | "presentation": { 38 | "reveal": "never" 39 | }, 40 | // The tsc compiler is started in watching mode 41 | "isBackground": true, 42 | // use the standard tsc in watch mode problem matcher to find compile problems in the output. 43 | "problemMatcher": "$tsc-watch" 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | typings/** 3 | out/** 4 | test/** 5 | src/** 6 | images/** 7 | node_modules/** 8 | scripts/** 9 | build/** 10 | Jenkinsfile 11 | prettier* 12 | **/*.map 13 | .gitignore 14 | .github/** 15 | tsconfig.json 16 | vsc-extension-quickstart.md 17 | undefined/** 18 | CONTRIBUTING.md 19 | .vscode-test/** 20 | .vscode-test-web/** 21 | **/**.vsix 22 | **/**.tar.gz 23 | !node_modules/prettier/index.js 24 | !node_modules/prettier/third-party.js 25 | !node_modules/prettier/parser-yaml.js 26 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | --ignore-engines true 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Developer Support 4 | 5 | All contributions are welcome! 6 | 7 | ### Getting started 8 | 9 | 1. Install prerequisites: 10 | 11 | * latest [Visual Studio Code](https://code.visualstudio.com/) 12 | * [Node.js](https://nodejs.org/) v14.0.0 or higher 13 | 14 | 2. Fork and clone this repository and go into the folder 15 | 16 | ```bash 17 | $ cd vscode-yaml 18 | ``` 19 | 20 | 3. Install the dependencies 21 | 22 | ```bash 23 | $ npm install 24 | ``` 25 | 26 | 4. Compile the Typescript to Javascript 27 | 28 | ```bash 29 | $ npm run compile 30 | ``` 31 | 32 | ##### Developing the client side 33 | 34 | 1. Open the client in vscode 35 | 2. Make changes as neccessary and the run the code using F5 36 | 37 | ##### Developing the client and server together 38 | 39 | 1. Download both the [Yaml Language Server](https://github.com/redhat-developer/yaml-language-server) and this VSCode Yaml Client. 40 | 41 | 2. Create a project with the directories in the following structure. 42 | 43 | ``` 44 | ParentFolder/ 45 | ├──── vscode-yaml/ 46 | ├──── yaml-language-server/ 47 | ``` 48 | 49 | 3. Open the `vscode-yaml` folder in VSCode, and then add the `yaml-language-server` project to the workspace using `File -> Add Folder to Workspace...`. 50 | 51 | 4. Run `yarn install` in both directories to initialize `node_modules` dependencies. 52 | 53 | 5. To run the language server in VSCode, click `View -> Debug`, then from the drop down menu beside the green arrow select `Launch Extension (vscode-yaml)`, click the arrow, and a new VSCode window should load with the YAML LS running. 54 | 55 | 6. To debug the language server in VSCode, from the same drop down menu 56 | select 57 | `Attach (yaml-language-server)`, and click the green arrow to start. 58 | Ensure you've opened a YAML file or else the server would have not yet 59 | started. 60 | 61 | **Notes:** 62 | * Disable or remove any existing implementations of the YAML Language server from VSCode or there will be conflicts. 63 | * If you still have issues you can also try changing the debug port for the language server. To do this change the port in the `Attach to server` configuration to another value in `yaml-language-server/.vscode/launch.json`, then change update the port in `debugOptions` (`'--inspect=6009'`) to the new port in the file `vscode-yaml/src/node/yamlClientMain.ts`. 64 | 65 | ##### Developing the server side 66 | 67 | 1. To develop the language server visit https://github.com/redhat-developer/yaml-language-server 68 | 69 | Refer to VS Code [documentation](https://code.visualstudio.com/docs/extensions/debugging-extensions) on how to run and debug the extension 70 | 71 | ### Installation from GitHub Release 72 | 73 | To obtain and install the latest release from GitHub you can: 74 | 75 | * First download the latest *.vsix file from [GitHub Releases section](https://github.com/redhat-developer/vscode-yaml/releases) 76 | * Inside of VSCode navigate to the extension tab and click the three elipses (...). 77 | * Click install from VSIX and provide the location of the *.vsix that was downloaded 78 | 79 | ### Certificate of Origin 80 | 81 | By contributing to this project you agree to the Developer Certificate of 82 | Origin (DCO). This document was created by the Linux Kernel community and is a 83 | simple statement that you, as a contributor, have the legal right to make the 84 | contribution. See the [DCO](DCO) file for details. 85 | -------------------------------------------------------------------------------- /DCO: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 1 Letterman Drive 6 | Suite D4700 7 | San Francisco, CA, 94129 8 | 9 | Everyone is permitted to copy and distribute verbatim copies of this 10 | license document, but changing it is not allowed. 11 | 12 | 13 | Developer's Certificate of Origin 1.1 14 | 15 | By making a contribution to this project, I certify that: 16 | 17 | (a) The contribution was created in whole or in part by me and I 18 | have the right to submit it under the open source license 19 | indicated in the file; or 20 | 21 | (b) The contribution is based upon previous work that, to the best 22 | of my knowledge, is covered under an appropriate open source 23 | license and I have the right under that license to submit that 24 | work with modifications, whether created in whole or in part 25 | by me, under the same open source license (unless I am 26 | permitted to submit under a different license), as indicated 27 | in the file; or 28 | 29 | (c) The contribution was provided directly to me by some other 30 | person who certified (a), (b) or (c) and I have not modified 31 | it. 32 | 33 | (d) I understand and agree that this project and the contribution 34 | are public and that a record of the contribution (including all 35 | personal information I submit with it, including my sign-off) is 36 | maintained indefinitely and may be redistributed consistent with 37 | this project or the open source license(s) involved. 38 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | def installBuildRequirements(){ 4 | def nodeHome = tool 'nodejs-lts' 5 | env.PATH="${env.PATH}:${nodeHome}/bin" 6 | sh "npm install --global yarn" 7 | sh "npm install --global vsce" 8 | } 9 | 10 | def buildVscodeExtension(){ 11 | sh "yarn install" 12 | sh "yarn run vscode:prepublish" 13 | } 14 | 15 | node('rhel8'){ 16 | 17 | stage 'Checkout vscode-yaml code' 18 | deleteDir() 19 | git branch: 'release', url: 'https://github.com/redhat-developer/vscode-yaml.git' 20 | 21 | stage 'install vscode-yaml build requirements' 22 | installBuildRequirements() 23 | 24 | stage 'Build vscode-yaml' 25 | sh "yarn install" 26 | sh "yarn run build" 27 | sh "yarn run check-dependencies" 28 | 29 | stage 'Test vscode-yaml for staging' 30 | wrap([$class: 'Xvnc']) { 31 | sh "yarn test --silent" 32 | } 33 | 34 | stage "Package vscode-yaml" 35 | def packageJson = readJSON file: 'package.json' 36 | sh "vsce package -o yaml-${packageJson.version}-${env.BUILD_NUMBER}.vsix" 37 | 38 | stage 'Upload vscode-yaml to staging' 39 | def vsix = findFiles(glob: '**.vsix') 40 | sh "sftp -C ${UPLOAD_LOCATION}/snapshots/vscode-yaml/ <<< \$'put -p \"${vsix[0].path}\"'" 41 | stash name:'vsix', includes:vsix[0].path 42 | } 43 | 44 | node('rhel8'){ 45 | if(publishPreRelease.equals('true')){ 46 | stage "publish generic version" 47 | withCredentials([[$class: 'StringBinding', credentialsId: 'vscode_java_marketplace', variable: 'TOKEN']]) { 48 | sh 'vsce publish --pre-release -p ${TOKEN}' 49 | } 50 | 51 | stage "publish specific version" 52 | // for pre-release versions, vsixs are not stashed and kept in project folder 53 | withCredentials([[$class: 'StringBinding', credentialsId: 'vscode_java_marketplace', variable: 'TOKEN']]) { 54 | def platformVsixes = findFiles(glob: '**.vsix') 55 | for(platformVsix in platformVsixes){ 56 | sh 'vsce publish -p ${TOKEN}' + " --packagePath ${platformVsix.path}" 57 | } 58 | } 59 | } else if(publishToMarketPlace.equals('true')){ 60 | timeout(time:5, unit:'DAYS') { 61 | input message:'Approve deployment?', submitter: 'yvydolob, msivasub' 62 | } 63 | 64 | stage "Publish to Marketplaces" 65 | unstash 'vsix'; 66 | def vsix = findFiles(glob: '**.vsix') 67 | // VS Code Marketplace 68 | withCredentials([[$class: 'StringBinding', credentialsId: 'vscode_java_marketplace', variable: 'TOKEN']]) { 69 | sh 'vsce publish -p ${TOKEN} --packagePath' + " ${vsix[0].path}" 70 | } 71 | 72 | // Open-vsx Marketplace 73 | sh "npm install -g ovsx" 74 | withCredentials([[$class: 'StringBinding', credentialsId: 'open-vsx-access-token', variable: 'OVSX_TOKEN']]) { 75 | sh 'ovsx publish -p ${OVSX_TOKEN}' + " ${vsix[0].path}" 76 | } 77 | archive includes:"**.vsix" 78 | 79 | stage ("Promote the build to stable") { 80 | vsix = findFiles(glob: '**.vsix') 81 | sh "sftp -C ${UPLOAD_LOCATION}/stable/vscode-yaml/ <<< \$'put -p \"${vsix[0].path}\"'" 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Red Hat Inc. and others. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Visual Studio Marketplace](https://img.shields.io/visual-studio-marketplace/v/redhat.vscode-yaml?style=for-the-badge&label=VS%20Marketplace&logo=visual-studio-code)](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) 2 | [![Installs](https://img.shields.io/visual-studio-marketplace/i/redhat.vscode-yaml?style=for-the-badge&logo=microsoft)](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) 3 | [![Build Status](https://img.shields.io/github/actions/workflow/status/redhat-developer/vscode-yaml/CI.yaml?branch=main&style=for-the-badge&logo=github)](https://github.com/redhat-developer/vscode-yaml/actions?query=workflow:CI) 4 | [![License](https://img.shields.io/github/license/redhat-developer/vscode-yaml?style=for-the-badge)](https://github.com/redhat-developer/vscode-yaml/blob/master/LICENSE) 5 | [![OpenVSX Registry](https://img.shields.io/open-vsx/dt/redhat/vscode-yaml?color=purple&label=OpenVSX%20Downloads&style=for-the-badge)](https://open-vsx.org/extension/redhat/vscode-yaml) 6 | 7 | # YAML Language Support by Red Hat 8 | Provides comprehensive YAML Language support to [Visual Studio Code](https://code.visualstudio.com/), via the [yaml-language-server](https://github.com/redhat-developer/yaml-language-server), with built-in Kubernetes syntax support. 9 | 10 | ## Features 11 | ![screencast](https://raw.githubusercontent.com/redhat-developer/vscode-yaml/main/images/demo.gif) 12 | 13 | 1. YAML validation: 14 | * Detects whether the entire file is valid yaml 15 | * Detects errors such as: 16 | * Node is not found 17 | * Node has an invalid key node type 18 | * Node has an invalid type 19 | * Node is not a valid child node 20 | 2. Document Outlining (Ctrl + Shift + O): 21 | * Provides the document outlining of all completed nodes in the file 22 | 3. Auto completion (Ctrl + Space): 23 | * Auto completes on all commands 24 | * Scalar nodes autocomplete to schema's defaults if they exist 25 | 4. Hover support: 26 | * Hovering over a node shows description *if provided by schema* 27 | 5. Formatter: 28 | * Allows for formatting the current file 29 | * On type formatting auto indent for array items 30 | 31 | *Auto completion and hover support are provided by the schema. Please refer to Language Server Settings to setup a schema* 32 | 33 | ## YAML version support 34 | Starting from `1.0.0` the extension uses [eemeli/yaml](https://github.com/eemeli/yaml) as the new YAML parser, which strictly enforces the specified YAML spec version. 35 | Default YAML spec version is `1.2`, it can be changed with `yaml.yamlVersion` setting. 36 | 37 | ## Extension Settings 38 | 39 | The following settings are supported: 40 | * `yaml.yamlVersion`: Set default YAML spec version (`1.2` or `1.1`) 41 | * `yaml.format.enable`: Enable/disable default YAML formatter (requires restart) 42 | * `yaml.format.singleQuote`: Use single quotes instead of double quotes 43 | * `yaml.format.bracketSpacing`: Print spaces between brackets in objects 44 | * `yaml.format.proseWrap`: Always: wrap prose if it exceeds the print width, Never: never wrap the prose, Preserve: wrap prose as-is 45 | * `yaml.format.printWidth`: Specify the line length that the printer will wrap on 46 | * `yaml.format.trailingComma`: Specify if trailing commas should be used in JSON-like segments of the YAML 47 | * `yaml.validate`: Enable/disable validation feature 48 | * `yaml.hover`: Enable/disable hover 49 | * `yaml.completion`: Enable/disable autocompletion 50 | * `yaml.schemas`: Helps you associate schemas with files in a glob pattern 51 | * `yaml.schemaStore.enable`: When set to true, the YAML language server will pull in all available schemas from [JSON Schema Store](http://schemastore.org/json/) 52 | * `yaml.schemaStore.url`: URL of a schema store catalog to use when downloading schemas. 53 | * `yaml.customTags`: Array of custom tags that the parser will validate against. It has two ways to be used. Either an item in the array is a custom tag such as "!Ref" and it will automatically map !Ref to a scalar, or you can specify the type of the object !Ref should be, e.g. "!Ref sequence". The type of object can be either scalar (for strings and booleans), sequence (for arrays), mapping (for objects). 54 | * `yaml.maxItemsComputed`: The maximum number of outline symbols and folding regions computed (limited for performance reasons). 55 | * `yaml.disableDefaultProperties`: Disable adding not required properties with default values into completion text (default is false). 56 | * `yaml.suggest.parentSkeletonSelectedFirst`: If true, the user must select some parent skeleton first before autocompletion starts to suggest the rest of the properties. When the YAML object is not empty, autocompletion ignores this setting and returns all properties and skeletons. 57 | 58 | - `[yaml]`: VSCode-YAML adds default configuration for all YAML files. More specifically, it converts tabs to spaces to ensure valid YAML, sets the tab size, allows live typing autocompletion and formatting, and also allows code lens. These settings can be modified via the corresponding settings inside the `[yaml]` section in the settings: 59 | - `editor.tabSize` 60 | - `editor.formatOnType` 61 | - `editor.codeLens` 62 | 63 | * `http.proxy`: The URL of the proxy server that will be used when attempting to download a schema. If it is not set or it is undefined no proxy server will be used. 64 | 65 | - `http.proxyStrictSSL`: If true the proxy server certificate should be verified against the list of supplied CAs. Default is false. 66 | - `yaml.style.flowMapping` : Forbids flow style mappings if set to `forbid` 67 | - `yaml.style.flowSequence` : Forbids flow style sequences if set to `forbid` 68 | - `yaml.keyOrdering` : Enforces alphabetical ordering of keys in mappings when set to `true`. Default is `false` 69 | - `yaml.extension.recommendations` : Enable extension recommendations for YAML files. Default is `true` 70 | 71 | ## Adding custom tags 72 | 73 | To use the custom tags in your YAML file, you need to first specify the custom tags in the setting of your code editor. For example, you can have the following custom tags: 74 | 75 | ```YAML 76 | "yaml.customTags": [ 77 | "!Scalar-example scalar", 78 | "!Seq-example sequence", 79 | "!Mapping-example mapping" 80 | ] 81 | ``` 82 | 83 | The !Scalar-example would map to a scalar custom tag, the !Seq-example would map to a sequence custom tag, the !Mapping-example would map to a mapping custom tag. 84 | 85 | You can then use the newly defined custom tags inside the YAML file: 86 | 87 | ```YAML 88 | some_key: !Scalar-example some_value 89 | some_sequence: !Seq-example 90 | - some_seq_key_1: some_seq_value_1 91 | - some_seq_key_2: some_seq_value_2 92 | some_mapping: !Mapping-example 93 | some_mapping_key_1: some_mapping_value_1 94 | some_mapping_key_2: some_mapping_value_2 95 | ``` 96 | 97 | ## Associating schemas 98 | 99 | YAML Language support uses [JSON Schemas](https://json-schema.org/) to understand the shape of a YAML file, including its value sets, defaults and descriptions. The schema support is shipped with JSON Schema Draft 7. 100 | 101 | We support schemas provided through [JSON Schema Store](http://schemastore.org/json/). However, schemas can also be defined in a workspace. 102 | 103 | The association of a YAML file to a schema can be done either in the YAML file itself using a modeline or in the User or Workspace [settings](https://code.visualstudio.com/docs/getstarted/settings) under the property `yaml.schemas`. 104 | 105 | ### Associating a schema in the YAML file 106 | 107 | It is possible to specify a yaml schema using a modeline. Schema url can be a relative path. If a relative path is specified, it is calculated from yaml file path, not from workspace root path 108 | 109 | ```yaml 110 | # yaml-language-server: $schema= 111 | ``` 112 | 113 | ### Associating a schema to a glob pattern via yaml.schemas: 114 | 115 | `yaml.schemas` applies a schema to a file. In other words, the schema (placed on the left) is applied to the glob pattern on the right. Your schema can be local or online. Your schema must be a relative path and not an absolute path. The entrance point for `yaml.schemas` is a location in [user and workspace settings](https://code.visualstudio.com/docs/getstarted/settings#_creating-user-and-workspace-settings) 116 | 117 | When associating a schema it should follow the format below 118 | ```JSON 119 | "yaml.schemas": { 120 | "url": "globPattern", 121 | "Kubernetes": "globPattern" 122 | } 123 | ``` 124 | 125 | e.g. 126 | 127 | ```json 128 | yaml.schemas: { 129 | "https://json.schemastore.org/composer": "/*" 130 | } 131 | ``` 132 | 133 | e.g. 134 | 135 | ```json 136 | yaml.schemas: { 137 | "kubernetes": "/myYamlFile.yaml" 138 | } 139 | ``` 140 | 141 | e.g. 142 | 143 | ```json 144 | yaml.schemas: { 145 | "https://json.schemastore.org/composer": "/*", 146 | "kubernetes": "/myYamlFile.yaml" 147 | } 148 | ``` 149 | 150 | On Windows with full path: 151 | 152 | ```json 153 | yaml.schemas: { 154 | "C:\\Users\\user\\Documents\\custom_schema.json": "someFilePattern.yaml", 155 | "file:///C:/Users/user/Documents/custom_schema.json": "someFilePattern.yaml", 156 | } 157 | ``` 158 | 159 | On Mac/Linux with full path: 160 | 161 | ```json 162 | yaml.schemas: { 163 | "/home/user/custom_schema.json": "someFilePattern.yaml", 164 | } 165 | ``` 166 | 167 | Since `0.11.0` YAML Schemas can be used for validation: 168 | 169 | ```json 170 | "/home/user/custom_schema.yaml": "someFilePattern.yaml" 171 | ``` 172 | 173 | A schema can be associated with multiple globs using a json array, e.g. 174 | 175 | ```json 176 | yaml.schemas: { 177 | "kubernetes": ["filePattern1.yaml", "filePattern2.yaml"] 178 | } 179 | ``` 180 | 181 | e.g. 182 | 183 | ```json 184 | "yaml.schemas": { 185 | "http://json.schemastore.org/composer": ["/*"], 186 | "file:///home/johnd/some-schema.json": ["some.yaml"], 187 | "../relative/path/schema.json": ["/config*.yaml"], 188 | "/Users/johnd/some-schema.json": ["some.yaml"], 189 | } 190 | ``` 191 | 192 | e.g. 193 | 194 | ```JSON 195 | "yaml.schemas": { 196 | "kubernetes": ["/myYamlFile.yaml"] 197 | } 198 | ``` 199 | 200 | e.g. 201 | ```json 202 | "yaml.schemas": { 203 | "http://json.schemastore.org/composer": ["/*"], 204 | "kubernetes": ["/myYamlFile.yaml"] 205 | } 206 | ``` 207 | 208 | #### Multi root schema association: 209 | 210 | You can also use relative paths when working with multi-root workspaces. 211 | 212 | Suppose you have a multi-root workspace that is laid out like: 213 | 214 | ```shell 215 | My_first_project: 216 | test.yaml 217 | my_schema.json 218 | My_second_project: 219 | test2.yaml 220 | my_schema2.json 221 | ``` 222 | 223 | You must then associate schemas relative to the root of the multi root workspace project. 224 | 225 | ```json 226 | yaml.schemas: { 227 | "My_first_project/my_schema.json": "test.yaml", 228 | "My_second_project/my_schema2.json": "test2.yaml" 229 | } 230 | ``` 231 | 232 | `yaml.schemas` allows you to specify JSON schemas that you want to validate against the YAML you write. *Kubernetes* is a reserved keyword field. It does not require a URL, as the language server will provide that. You need the keyword `kubernetes` and a glob pattern. 233 | 234 | ### Mapping a schema in an extension 235 | 236 | - Supports `yamlValidation` point, which allows you to contribute a schema for a specific type of YAML file (Similar to [jsonValidation](https://code.visualstudio.com/docs/extensionAPI/extension-points#_contributesjsonvalidation)) 237 | e.g. 238 | ```JSON 239 | { 240 | "contributes": { 241 | "yamlValidation": [ 242 | { 243 | "fileMatch": "yourfile.yml", 244 | "url": "./schema.json" 245 | } 246 | ] 247 | } 248 | } 249 | ``` 250 | ## Feedback & Questions 251 | 252 | If you discover an issue please file a bug and we will fix it as soon as possible. 253 | * File a bug in [GitHub Issues](https://github.com/redhat-developer/vscode-yaml/issues). 254 | * Open a [Discussion on GitHub](https://github.com/redhat-developer/vscode-yaml/discussions). 255 | 256 | ## License 257 | 258 | MIT, See [LICENSE](LICENSE) for more information. 259 | ## Data and Telemetry 260 | 261 | The `vscode-yaml` extension collects anonymous [usage data](USAGE_DATA.md) and sends it to Red Hat servers to help improve our products and services. Read our [privacy statement](https://developers.redhat.com/article/tool-data-collection) to learn more. This extension respects the `redhat.telemetry.enabled` setting, which you can learn more about at https://github.com/redhat-developer/vscode-redhat-telemetry#how-to-disable-telemetry-reporting 262 | 263 | ## How to contribute 264 | 265 | The instructions are available in the [contribution guide](CONTRIBUTING.md). 266 | -------------------------------------------------------------------------------- /USAGE_DATA.md: -------------------------------------------------------------------------------- 1 | # Data collection 2 | 3 | vscode-yaml has opt-in telemetry collection, provided by [vscode-redhat-telemetry](https://github.com/redhat-developer/vscode-redhat-telemetry). 4 | 5 | ## What's included in the vscode-yaml telemetry data 6 | 7 | - yaml-language-server start 8 | - errors during yaml-language-server start 9 | - any errors from LSP requests 10 | - `kubernetes` schema usage 11 | 12 | ## What's included in the general telemetry data 13 | 14 | Please see the 15 | [vscode-redhat-telemetry data collection information](https://github.com/redhat-developer/vscode-redhat-telemetry/blob/HEAD/USAGE_DATA.md) 16 | for information on what data it collects. 17 | 18 | ## How to opt-in or out 19 | 20 | Use the `redhat.telemetry.enabled` setting to enable or disable telemetry collection. 21 | 22 | Note that this extension abides by Visual Studio Code's telemetry level: if `telemetry.telemetryLevel` is set to off, then no telemetry events will be sent to Red Hat, even if `redhat.telemetry.enabled` is set to true. If `telemetry.telemetryLevel` is set to `error` or `crash`, only events containing an error or errors property will be sent to Red Hat. 23 | -------------------------------------------------------------------------------- /build/polyfills/yamlFormatter.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-function-return-type */ 2 | /* eslint-disable @typescript-eslint/no-empty-function */ 3 | class YAMLFormatter { 4 | constructor() {} 5 | configure() {} 6 | format() { 7 | return []; 8 | } 9 | } 10 | exports.YAMLFormatter = YAMLFormatter; 11 | -------------------------------------------------------------------------------- /custom-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "yaml.schemas": { 3 | "https://json.schemastore.org/kustomization.json": "/*" 4 | } 5 | } -------------------------------------------------------------------------------- /icon/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer/vscode-yaml/cc15df699148c614db963c53a880aa4590030ec8/icon/icon128.png -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer/vscode-yaml/cc15df699148c614db963c53a880aa4590030ec8/images/demo.gif -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "#" 4 | }, 5 | "brackets": [ 6 | ["{", "}"], 7 | ["[", "]"], 8 | ["(", ")"] 9 | ], 10 | "autoClosingPairs": [ 11 | ["{", "}"], 12 | ["[", "]"], 13 | ["(", ")"], 14 | ["\"", "\""], 15 | ["'", "'"], 16 | ["`", "`"] 17 | ], 18 | "surroundingPairs": [ 19 | ["{", "}"], 20 | ["[", "]"], 21 | ["(", ")"], 22 | ["\"", "\""], 23 | ["'", "'"], 24 | ["`", "`"] 25 | ], 26 | "folding": { 27 | "offSide": true, 28 | "markers": { 29 | "start": "^\\s*#\\s*region\\b", 30 | "end": "^\\s*#\\s*endregion\\b" 31 | } 32 | }, 33 | "indentationRules": { 34 | "increaseIndentPattern": "^\\s*.*(:|-) ?(&\\w+)?(\\{[^}\"']*|\\([^)\"']*)?$", 35 | "decreaseIndentPattern": "^\\s+\\}$" 36 | }, 37 | "wordPattern": "(^.?[^\\s]+)+|([^\\s\n={[][\\w\\-\\.\/$%&*:\"']+)" 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-yaml", 3 | "displayName": "YAML", 4 | "description": "YAML Language Support by Red Hat, with built-in Kubernetes syntax support", 5 | "author": "Red Hat", 6 | "contributors": [ 7 | { 8 | "name": "Joshua Pinkney", 9 | "email": "jpinkney@redhat.com" 10 | }, 11 | { 12 | "name": "Yevhen Vydolob", 13 | "email": "yvydolob@redhat.com" 14 | } 15 | ], 16 | "license": "MIT", 17 | "version": "1.19.0", 18 | "publisher": "redhat", 19 | "bugs": "https://github.com/redhat-developer/vscode-yaml/issues", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/redhat-developer/vscode-yaml" 23 | }, 24 | "icon": "icon/icon128.png", 25 | "engines": { 26 | "npm": ">=7.0.0", 27 | "vscode": "^1.63.0" 28 | }, 29 | "categories": [ 30 | "Programming Languages", 31 | "Linters", 32 | "Snippets", 33 | "Formatters" 34 | ], 35 | "capabilities": { 36 | "untrustedWorkspaces": { 37 | "supported": true 38 | }, 39 | "virtualWorkspaces": true 40 | }, 41 | "activationEvents": [ 42 | "onLanguage:yaml", 43 | "onLanguage:dockercompose", 44 | "onLanguage:github-actions-workflow" 45 | ], 46 | "keywords": [ 47 | "kubernetes", 48 | "yaml", 49 | "autocompletion", 50 | "validation" 51 | ], 52 | "main": "./dist/extension", 53 | "browser": "./dist/extension-web", 54 | "contributes": { 55 | "languages": [ 56 | { 57 | "id": "yaml", 58 | "aliases": [ 59 | "YAML", 60 | "yaml" 61 | ], 62 | "extensions": [ 63 | ".yml", 64 | ".eyaml", 65 | ".eyml", 66 | ".yaml" 67 | ], 68 | "filenames": [ 69 | ".clang-format", 70 | "_clang-format", 71 | ".clang-tidy" 72 | ], 73 | "firstLine": "^#cloud-config", 74 | "configuration": "./language-configuration.json" 75 | } 76 | ], 77 | "grammars": [ 78 | { 79 | "language": "yaml", 80 | "scopeName": "source.yaml", 81 | "path": "./syntaxes/yaml.tmLanguage.json" 82 | } 83 | ], 84 | "configuration": { 85 | "title:": "YAML", 86 | "properties": { 87 | "redhat.telemetry.enabled": { 88 | "type": "boolean", 89 | "default": null, 90 | "markdownDescription": "Enable usage data and errors to be sent to Red Hat servers. Read our [privacy statement](https://developers.redhat.com/article/tool-data-collection).", 91 | "scope": "window", 92 | "tags": [ 93 | "telemetry", 94 | "usesOnlineServices" 95 | ] 96 | }, 97 | "yaml.yamlVersion": { 98 | "type": "string", 99 | "default": "1.2", 100 | "enum": [ 101 | "1.1", 102 | "1.2" 103 | ], 104 | "markdownDescription": "Default YAML spec version" 105 | }, 106 | "yaml.trace.server": { 107 | "type": "string", 108 | "enum": [ 109 | "off", 110 | "messages", 111 | "verbose" 112 | ], 113 | "default": "off", 114 | "description": "Traces the communication between VSCode and the YAML language service." 115 | }, 116 | "yaml.schemas": { 117 | "type": "object", 118 | "default": {}, 119 | "description": "Associate schemas to YAML files in the current workspace" 120 | }, 121 | "yaml.format.enable": { 122 | "type": "boolean", 123 | "default": true, 124 | "description": "Enable/disable default YAML formatter" 125 | }, 126 | "yaml.format.singleQuote": { 127 | "type": "boolean", 128 | "default": false, 129 | "description": "Use single quotes instead of double quotes" 130 | }, 131 | "yaml.format.bracketSpacing": { 132 | "type": "boolean", 133 | "default": true, 134 | "description": "Print spaces between brackets in objects" 135 | }, 136 | "yaml.format.proseWrap": { 137 | "type": "string", 138 | "default": "preserve", 139 | "enum": [ 140 | "preserve", 141 | "never", 142 | "always" 143 | ], 144 | "description": "Always: wrap prose if it exceeds the print width, Never: never wrap the prose, Preserve: wrap prose as-is" 145 | }, 146 | "yaml.format.printWidth": { 147 | "type": "integer", 148 | "default": 80, 149 | "description": "Specify the line length that the printer will wrap on" 150 | }, 151 | "yaml.format.trailingComma": { 152 | "type": "boolean", 153 | "default": true, 154 | "description": "Specify if trailing commas should be used in JSON-like segments of the YAML" 155 | }, 156 | "yaml.validate": { 157 | "type": "boolean", 158 | "default": true, 159 | "description": "Enable/disable validation feature" 160 | }, 161 | "yaml.hover": { 162 | "type": "boolean", 163 | "default": true, 164 | "description": "Enable/disable hover feature" 165 | }, 166 | "yaml.completion": { 167 | "type": "boolean", 168 | "default": true, 169 | "description": "Enable/disable completion feature" 170 | }, 171 | "yaml.customTags": { 172 | "type": "array", 173 | "default": [], 174 | "description": "Custom tags for the parser to use" 175 | }, 176 | "yaml.schemaStore.enable": { 177 | "type": "boolean", 178 | "default": true, 179 | "description": "Automatically pull available YAML schemas from JSON Schema Store" 180 | }, 181 | "yaml.schemaStore.url": { 182 | "type": "string", 183 | "default": "https://www.schemastore.org/api/json/catalog.json", 184 | "description": "URL of schema store catalog to use" 185 | }, 186 | "yaml.disableAdditionalProperties": { 187 | "type": "boolean", 188 | "default": false, 189 | "description": "Globally set additionalProperties to false for all objects. So if its true, no extra properties are allowed inside yaml." 190 | }, 191 | "yaml.disableDefaultProperties": { 192 | "type": "boolean", 193 | "default": false, 194 | "description": "Disable adding not required properties with default values into completion text." 195 | }, 196 | "yaml.maxItemsComputed": { 197 | "type": "integer", 198 | "default": 5000, 199 | "description": "The maximum number of outline symbols and folding regions computed (limited for performance reasons)." 200 | }, 201 | "yaml.suggest.parentSkeletonSelectedFirst": { 202 | "type": "boolean", 203 | "default": false, 204 | "description": "If true, the user must select some parent skeleton first before autocompletion starts to suggest the rest of the properties. When yaml object is not empty, autocompletion ignores this setting and returns all properties and skeletons" 205 | }, 206 | "yaml.style.flowMapping": { 207 | "type": "string", 208 | "enum": [ 209 | "allow", 210 | "forbid" 211 | ], 212 | "default": "allow", 213 | "description": "Forbid flow style mappings" 214 | }, 215 | "yaml.style.flowSequence": { 216 | "type": "string", 217 | "enum": [ 218 | "allow", 219 | "forbid" 220 | ], 221 | "default": "allow", 222 | "description": "Forbid flow style sequences" 223 | }, 224 | "yaml.keyOrdering": { 225 | "type": "boolean", 226 | "default": false, 227 | "description": "Enforces alphabetical ordering of keys in mappings when set to true" 228 | }, 229 | "yaml.extension.recommendations": { 230 | "type": "boolean", 231 | "default": "true", 232 | "description": "Suggest additional extensions based on YAML usage." 233 | } 234 | } 235 | }, 236 | "configurationDefaults": { 237 | "[yaml]": { 238 | "editor.insertSpaces": true, 239 | "editor.tabSize": 2, 240 | "editor.quickSuggestions": { 241 | "other": true, 242 | "comments": false, 243 | "strings": true 244 | }, 245 | "editor.autoIndent": "keep" 246 | } 247 | } 248 | }, 249 | "extensionDependencies": [], 250 | "scripts": { 251 | "build": "yarn run clean && yarn run lint && yarn run vscode:prepublish", 252 | "check-dependencies": "node ./scripts/check-dependencies.js", 253 | "clean": "rimraf out && rimraf dist", 254 | "compile": "webpack --mode none", 255 | "format": "prettier --write .", 256 | "lint": "eslint -c .eslintrc.js --ext .ts src test", 257 | "test": "yarn test-compile && sh scripts/e2e.sh", 258 | "ui-test": "yarn test-compile && extest setup-and-run -y out/test/ui-test/allTestsSuite.js -c 1.76.2", 259 | "vscode:prepublish": "webpack --mode production", 260 | "watch": "webpack --mode development --watch --info-verbosity verbose", 261 | "test-compile": "yarn clean && tsc -p ./ && webpack --mode development", 262 | "run-in-chromium": "npm run compile && vscode-test-web --browserType=chromium --extensionDevelopmentPath=. ." 263 | }, 264 | "devDependencies": { 265 | "@types/chai": "^4.2.12", 266 | "@types/fs-extra": "^9.0.6", 267 | "@types/mocha": "^2.2.48", 268 | "@types/node": "^12.12.6", 269 | "@types/sinon": "^10.0.6", 270 | "@types/sinon-chai": "^3.2.5", 271 | "@types/vscode": "^1.63.0", 272 | "@types/webpack": "^4.4.10", 273 | "@typescript-eslint/eslint-plugin": "^7.11.0", 274 | "@typescript-eslint/parser": "^7.11.0", 275 | "@vscode/test-electron": "^2.4.0", 276 | "@vscode/test-web": "0.0.11", 277 | "buffer": "^6.0.3", 278 | "chai": "^4.2.0", 279 | "crypto-browserify": "^3.12.0", 280 | "eslint": "^8.57.0", 281 | "eslint-config-prettier": "^6.11.0", 282 | "eslint-plugin-prettier": "^3.1.4", 283 | "glob": "^7.1.6", 284 | "mocha": "^9.1.2", 285 | "path-browserify": "^1.0.1", 286 | "prettier": "2.2.1", 287 | "process": "^0.11.10", 288 | "rimraf": "^3.0.2", 289 | "sinon": "^12.0.1", 290 | "sinon-chai": "^3.7.0", 291 | "ts-loader": "^9.2.5", 292 | "ts-node": "^3.3.0", 293 | "typescript": "^5.4.5", 294 | "umd-compat-loader": "^2.1.2", 295 | "url": "^0.11.0", 296 | "util": "^0.12.5", 297 | "vscode-extension-tester": "^5.3.0", 298 | "webpack": "^5.76.1", 299 | "webpack-cli": "^5.0.1" 300 | }, 301 | "dependencies": { 302 | "@redhat-developer/vscode-redhat-telemetry": "^0.8.0", 303 | "fs-extra": "^9.1.0", 304 | "request-light": "^0.5.7", 305 | "vscode-languageclient": "7.0.0", 306 | "vscode-nls": "^3.2.1", 307 | "vscode-uri": "^2.0.3", 308 | "whatwg-fetch": "^3.6.2", 309 | "yaml-language-server": "next" 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /scripts/check-dependencies.js: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | //check package.json do not have dependency with 'next' version 7 | 8 | /* eslint-disable @typescript-eslint/no-var-requires */ 9 | 10 | const exit = require('process').exit; 11 | const dependencies = require('../package.json').dependencies; 12 | 13 | for (const dep in dependencies) { 14 | if (Object.prototype.hasOwnProperty.call(dependencies, dep)) { 15 | const version = dependencies[dep]; 16 | if (version === 'next') { 17 | console.error(`Dependency ${dep} has "${version}" version, please change it to fixed version`); 18 | exit(1); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scripts/e2e.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | node "$(pwd)/out/test/testRunner" 3 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Copyright (c) Adam Voss. All rights reserved. 4 | * Copyright (c) Microsoft Corporation. All rights reserved. 5 | * Licensed under the MIT License. See License.txt in the project root for license information. 6 | *--------------------------------------------------------------------------------------------*/ 7 | 'use strict'; 8 | 9 | import { workspace, ExtensionContext, extensions, window, commands, Uri } from 'vscode'; 10 | import { 11 | CommonLanguageClient, 12 | LanguageClientOptions, 13 | NotificationType, 14 | RequestType, 15 | RevealOutputChannelOn, 16 | } from 'vscode-languageclient'; 17 | import { CUSTOM_SCHEMA_REQUEST, CUSTOM_CONTENT_REQUEST, SchemaExtensionAPI } from './schema-extension-api'; 18 | import { joinPath } from './paths'; 19 | import { getJsonSchemaContent, IJSONSchemaCache, JSONSchemaDocumentContentProvider } from './json-schema-content-provider'; 20 | import { getConflictingExtensions, showUninstallConflictsNotification } from './extensionConflicts'; 21 | import { TelemetryErrorHandler, TelemetryOutputChannel } from './telemetry'; 22 | import { TextDecoder } from 'util'; 23 | import { createJSONSchemaStatusBarItem } from './schema-status-bar-item'; 24 | import { initializeRecommendation } from './recommendation'; 25 | 26 | export interface ISchemaAssociations { 27 | [pattern: string]: string[]; 28 | } 29 | 30 | export interface ISchemaAssociation { 31 | fileMatch: string[]; 32 | uri: string; 33 | } 34 | 35 | // eslint-disable-next-line @typescript-eslint/no-namespace 36 | namespace SettingIds { 37 | export const maxItemsComputed = 'yaml.maxItemsComputed'; 38 | } 39 | 40 | // eslint-disable-next-line @typescript-eslint/no-namespace 41 | namespace StorageIds { 42 | export const maxItemsExceededInformation = 'yaml.maxItemsExceededInformation'; 43 | } 44 | 45 | // eslint-disable-next-line @typescript-eslint/no-namespace 46 | namespace SchemaAssociationNotification { 47 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 48 | export const type: NotificationType = new NotificationType( 49 | 'json/schemaAssociations' 50 | ); 51 | } 52 | 53 | // eslint-disable-next-line @typescript-eslint/no-namespace 54 | namespace VSCodeContentRequestRegistration { 55 | // eslint-disable-next-line @typescript-eslint/ban-types 56 | export const type: NotificationType<{}> = new NotificationType('yaml/registerContentRequest'); 57 | } 58 | 59 | // eslint-disable-next-line @typescript-eslint/no-namespace 60 | namespace VSCodeContentRequest { 61 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 62 | export const type: RequestType = new RequestType('vscode/content'); 63 | } 64 | 65 | // eslint-disable-next-line @typescript-eslint/no-namespace 66 | namespace FSReadFile { 67 | // eslint-disable-next-line @typescript-eslint/ban-types 68 | export const type: RequestType = new RequestType('fs/readFile'); 69 | } 70 | 71 | // eslint-disable-next-line @typescript-eslint/no-namespace 72 | namespace DynamicCustomSchemaRequestRegistration { 73 | // eslint-disable-next-line @typescript-eslint/ban-types 74 | export const type: NotificationType<{}> = new NotificationType('yaml/registerCustomSchemaRequest'); 75 | } 76 | 77 | // eslint-disable-next-line @typescript-eslint/no-namespace 78 | namespace ResultLimitReachedNotification { 79 | // eslint-disable-next-line @typescript-eslint/ban-types 80 | export const type: NotificationType = new NotificationType('yaml/resultLimitReached'); 81 | } 82 | 83 | // eslint-disable-next-line @typescript-eslint/no-namespace 84 | export namespace SchemaSelectionRequests { 85 | export const type: NotificationType = new NotificationType('yaml/supportSchemaSelection'); 86 | export const schemaStoreInitialized: NotificationType = new NotificationType('yaml/schema/store/initialized'); 87 | } 88 | 89 | let client: CommonLanguageClient; 90 | 91 | const lsName = 'YAML Support'; 92 | 93 | export type LanguageClientConstructor = ( 94 | name: string, 95 | description: string, 96 | clientOptions: LanguageClientOptions 97 | ) => CommonLanguageClient; 98 | 99 | export interface RuntimeEnvironment { 100 | readonly telemetry: TelemetryService; 101 | readonly schemaCache: IJSONSchemaCache; 102 | } 103 | 104 | export interface TelemetryService { 105 | send(arg: { name: string; properties?: unknown }): Promise; 106 | sendStartupEvent(): Promise; 107 | } 108 | 109 | export function startClient( 110 | context: ExtensionContext, 111 | newLanguageClient: LanguageClientConstructor, 112 | runtime: RuntimeEnvironment 113 | ): SchemaExtensionAPI { 114 | const telemetryErrorHandler = new TelemetryErrorHandler(runtime.telemetry, lsName, 4); 115 | const outputChannel = window.createOutputChannel(lsName); 116 | // Options to control the language client 117 | const clientOptions: LanguageClientOptions = { 118 | // Register the server for on disk and newly created YAML documents 119 | documentSelector: [ 120 | { language: 'yaml' }, 121 | { language: 'dockercompose' }, 122 | { language: 'github-actions-workflow' }, 123 | { pattern: '*.y(a)ml' }, 124 | ], 125 | synchronize: { 126 | // Notify the server about file changes to YAML and JSON files contained in the workspace 127 | fileEvents: [workspace.createFileSystemWatcher('**/*.?(e)y?(a)ml'), workspace.createFileSystemWatcher('**/*.json')], 128 | }, 129 | revealOutputChannelOn: RevealOutputChannelOn.Never, 130 | errorHandler: telemetryErrorHandler, 131 | outputChannel: new TelemetryOutputChannel(outputChannel, runtime.telemetry), 132 | }; 133 | 134 | // Create the language client and start it 135 | client = newLanguageClient('yaml', lsName, clientOptions); 136 | 137 | const disposable = client.start(); 138 | 139 | const schemaExtensionAPI = new SchemaExtensionAPI(client); 140 | 141 | // Push the disposable to the context's subscriptions so that the 142 | // client can be deactivated on extension deactivation 143 | context.subscriptions.push(disposable); 144 | context.subscriptions.push( 145 | workspace.registerTextDocumentContentProvider( 146 | 'json-schema', 147 | new JSONSchemaDocumentContentProvider(runtime.schemaCache, schemaExtensionAPI) 148 | ) 149 | ); 150 | 151 | context.subscriptions.push( 152 | client.onTelemetry((e) => { 153 | runtime.telemetry.send(e); 154 | }) 155 | ); 156 | 157 | findConflicts(); 158 | client 159 | .onReady() 160 | .then(() => { 161 | // Send a notification to the server with any YAML schema associations in all extensions 162 | client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociations()); 163 | 164 | // If the extensions change, fire this notification again to pick up on any association changes 165 | extensions.onDidChange(() => { 166 | client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociations()); 167 | findConflicts(); 168 | }); 169 | // Tell the server that the client is ready to provide custom schema content 170 | client.sendNotification(DynamicCustomSchemaRequestRegistration.type); 171 | // Tell the server that the client supports schema requests sent directly to it 172 | client.sendNotification(VSCodeContentRequestRegistration.type); 173 | // Tell the server that the client supports schema selection requests 174 | client.sendNotification(SchemaSelectionRequests.type); 175 | // If the server asks for custom schema content, get it and send it back 176 | client.onRequest(CUSTOM_SCHEMA_REQUEST, (resource: string) => { 177 | return schemaExtensionAPI.requestCustomSchema(resource); 178 | }); 179 | client.onRequest(CUSTOM_CONTENT_REQUEST, (uri: string) => { 180 | return schemaExtensionAPI.requestCustomSchemaContent(uri); 181 | }); 182 | client.onRequest(VSCodeContentRequest.type, (uri: string) => { 183 | return getJsonSchemaContent(uri, runtime.schemaCache); 184 | }); 185 | client.onRequest(FSReadFile.type, (fsPath: string) => { 186 | return workspace.fs.readFile(Uri.file(fsPath)).then((uint8array) => new TextDecoder().decode(uint8array)); 187 | }); 188 | 189 | sendStartupTelemetryEvent(runtime.telemetry, true); 190 | // Adapted from: 191 | // https://github.com/microsoft/vscode/blob/94c9ea46838a9a619aeafb7e8afd1170c967bb55/extensions/json-language-features/client/src/jsonClient.ts#L305-L318 192 | client.onNotification(ResultLimitReachedNotification.type, async (message) => { 193 | const shouldPrompt = context.globalState.get(StorageIds.maxItemsExceededInformation) !== false; 194 | if (shouldPrompt) { 195 | const ok = 'Ok'; 196 | const openSettings = 'Open Settings'; 197 | const neverAgain = "Don't Show Again"; 198 | const pick = await window.showInformationMessage( 199 | `${message}\nUse setting '${SettingIds.maxItemsComputed}' to configure the limit.`, 200 | ok, 201 | openSettings, 202 | neverAgain 203 | ); 204 | if (pick === neverAgain) { 205 | await context.globalState.update(StorageIds.maxItemsExceededInformation, false); 206 | } else if (pick === openSettings) { 207 | await commands.executeCommand('workbench.action.openSettings', SettingIds.maxItemsComputed); 208 | } 209 | } 210 | }); 211 | 212 | client.onNotification(SchemaSelectionRequests.schemaStoreInitialized, () => { 213 | createJSONSchemaStatusBarItem(context, client); 214 | }); 215 | 216 | initializeRecommendation(context); 217 | }) 218 | .catch((err) => { 219 | sendStartupTelemetryEvent(runtime.telemetry, false, err); 220 | }); 221 | 222 | return schemaExtensionAPI; 223 | } 224 | 225 | /** 226 | * Finds extensions that conflict with VSCode-YAML. 227 | * If one or more conflicts are found then show an uninstall notification 228 | * If no conflicts are found then do nothing 229 | */ 230 | function findConflicts(): void { 231 | const conflictingExtensions = getConflictingExtensions(); 232 | if (conflictingExtensions.length > 0) { 233 | showUninstallConflictsNotification(conflictingExtensions); 234 | } 235 | } 236 | 237 | function getSchemaAssociations(): ISchemaAssociation[] { 238 | const associations: ISchemaAssociation[] = []; 239 | extensions.all.forEach((extension) => { 240 | const packageJSON = extension.packageJSON; 241 | if (packageJSON && packageJSON.contributes && packageJSON.contributes.yamlValidation) { 242 | const yamlValidation = packageJSON.contributes.yamlValidation; 243 | if (Array.isArray(yamlValidation)) { 244 | yamlValidation.forEach((jv) => { 245 | // eslint-disable-next-line prefer-const 246 | let { fileMatch, url } = jv; 247 | if (typeof fileMatch === 'string') { 248 | fileMatch = [fileMatch]; 249 | } 250 | if (Array.isArray(fileMatch) && typeof url === 'string') { 251 | let uri: string = url; 252 | if (uri[0] === '.' && uri[1] === '/') { 253 | uri = joinPath(extension.extensionUri, uri).toString(); 254 | } 255 | fileMatch = fileMatch.map((fm) => { 256 | if (fm[0] === '%') { 257 | fm = fm.replace(/%APP_SETTINGS_HOME%/, '/User'); 258 | fm = fm.replace(/%MACHINE_SETTINGS_HOME%/, '/Machine'); 259 | fm = fm.replace(/%APP_WORKSPACES_HOME%/, '/Workspaces'); 260 | } else if (!fm.match(/^(\w+:\/\/|\/|!)/)) { 261 | fm = '/' + fm; 262 | } 263 | return fm; 264 | }); 265 | associations.push({ fileMatch, uri }); 266 | } 267 | }); 268 | } 269 | } 270 | }); 271 | return associations; 272 | } 273 | 274 | async function sendStartupTelemetryEvent(telemetry: TelemetryService, initialized: boolean, err?: Error): Promise { 275 | const startUpEvent = { 276 | name: 'startup', 277 | properties: { 278 | 'yaml.server.initialized': initialized, 279 | }, 280 | }; 281 | if (err?.message) { 282 | startUpEvent.properties['error'] = err.message; 283 | } 284 | await telemetry.send(startUpEvent); 285 | } 286 | 287 | export function logToExtensionOutputChannel(message: string): void { 288 | client.outputChannel.appendLine(message); 289 | } 290 | -------------------------------------------------------------------------------- /src/extensionConflicts.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved.. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import { commands, Extension, extensions, window } from 'vscode'; 6 | 7 | // A set of VSCode extension ID's that conflict with VSCode-YAML 8 | const azureDeploy = 'ms-vscode-deploy-azure.azure-deploy'; 9 | const conflictingIDs = new Set(['vscoss.vscode-ansible', azureDeploy, 'sysninja.vscode-ansible-mod', 'haaaad.ansible']); 10 | 11 | // A set of VSCode extension ID's that are currently uninstalling 12 | const uninstallingIDs = new Set(); 13 | 14 | /** 15 | * Get all of the installed extensions that currently conflict with VSCode-YAML 16 | */ 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | export function getConflictingExtensions(): Extension[] { 19 | const conflictingExtensions = []; 20 | conflictingIDs.forEach((extension) => { 21 | const ext = extensions.getExtension(extension); 22 | if (ext && !uninstallingIDs.has(ext.id)) { 23 | conflictingExtensions.push(ext); 24 | } 25 | }); 26 | return conflictingExtensions; 27 | } 28 | 29 | /** 30 | * Display the uninstall conflicting extension notification if there are any conflicting extensions currently installed 31 | */ 32 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 33 | export function showUninstallConflictsNotification(conflictingExts: Extension[]): void { 34 | // Add all available conflicting extensions to the uninstalling IDs map 35 | for (const extIndex in conflictingExts) { 36 | const ext = conflictingExts[extIndex]; 37 | uninstallingIDs.add(ext.id); 38 | } 39 | 40 | const uninstallMsg = 'Uninstall'; 41 | 42 | // Gather all the conflicting display names 43 | let conflictMsg = ''; 44 | if (conflictingExts.length === 1) { 45 | conflictMsg = `${conflictingExts[0].packageJSON.displayName} extension is incompatible with VSCode-YAML. Please uninstall it.`; 46 | } else { 47 | const extNames = []; 48 | conflictingExts.forEach((ext) => { 49 | extNames.push(ext.packageJSON.displayName); 50 | }); 51 | conflictMsg = `The ${extNames.join(', ')} extensions are incompatible with VSCode-YAML. Please uninstall them.`; 52 | } 53 | 54 | if (conflictingExts.length > 0) { 55 | window.showInformationMessage(conflictMsg, uninstallMsg).then((clickedMsg) => { 56 | if (clickedMsg === uninstallMsg) { 57 | conflictingExts.forEach((ext) => { 58 | commands.executeCommand('workbench.extensions.uninstallExtension', ext.id); 59 | uninstallingIDs.delete(ext.id); 60 | }); 61 | 62 | // The azure deploy extension must be reloaded in order to be completely uninstalled 63 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 64 | if (conflictingExts.findIndex((ext: any) => ext.id === azureDeploy) !== -1) { 65 | commands.executeCommand('workbench.action.reloadWindow'); 66 | } 67 | } 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/json-schema-cache.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as fs from 'fs-extra'; 7 | import * as path from 'path'; 8 | import * as crypto from 'crypto'; 9 | import { Memento } from 'vscode'; 10 | import { logToExtensionOutputChannel } from './extension'; 11 | import { IJSONSchemaCache } from './json-schema-content-provider'; 12 | 13 | const CACHE_DIR = 'schemas_cache'; 14 | const CACHE_KEY = 'json-schema-key'; 15 | 16 | interface CacheEntry { 17 | eTag: string; 18 | schemaPath: string; 19 | } 20 | 21 | interface SchemaCache { 22 | [uri: string]: CacheEntry; 23 | } 24 | 25 | export class JSONSchemaCache implements IJSONSchemaCache { 26 | private readonly cachePath: string; 27 | private readonly cache: SchemaCache; 28 | 29 | private isInitialized = false; 30 | 31 | constructor(globalStoragePath: string, private memento: Memento) { 32 | this.cachePath = path.join(globalStoragePath, CACHE_DIR); 33 | this.cache = memento.get(CACHE_KEY, {}); 34 | } 35 | 36 | private async init(): Promise { 37 | await fs.ensureDir(this.cachePath); 38 | const cachedFiles = await fs.readdir(this.cachePath); 39 | // clean up memento if cached files was deleted from fs 40 | const cachedValues = cachedFiles.map((it) => path.join(this.cachePath, it)); 41 | for (const key in this.cache) { 42 | if (Object.prototype.hasOwnProperty.call(this.cache, key)) { 43 | const cacheEntry = this.cache[key]; 44 | if (!cachedValues.includes(cacheEntry.schemaPath)) { 45 | delete this.cache[key]; 46 | } 47 | } 48 | } 49 | await this.memento.update(CACHE_KEY, this.cache); 50 | this.isInitialized = true; 51 | } 52 | 53 | private getCacheFilePath(uri: string): string { 54 | const hash = crypto.createHash('MD5'); 55 | hash.update(uri); 56 | const hashedURI = hash.digest('hex'); 57 | return path.join(this.cachePath, hashedURI); 58 | } 59 | 60 | getETag(schemaUri: string): string | undefined { 61 | if (!this.isInitialized) { 62 | return undefined; 63 | } 64 | return this.cache[schemaUri]?.eTag; 65 | } 66 | 67 | async putSchema(schemaUri: string, eTag: string, schemaContent: string): Promise { 68 | if (!this.isInitialized) { 69 | await this.init(); 70 | } 71 | if (!this.cache[schemaUri]) { 72 | this.cache[schemaUri] = { eTag, schemaPath: this.getCacheFilePath(schemaUri) }; 73 | } else { 74 | this.cache[schemaUri].eTag = eTag; 75 | } 76 | try { 77 | const cacheFile = this.cache[schemaUri].schemaPath; 78 | await fs.writeFile(cacheFile, schemaContent); 79 | 80 | await this.memento.update(CACHE_KEY, this.cache); 81 | } catch (err) { 82 | delete this.cache[schemaUri]; 83 | logToExtensionOutputChannel(err); 84 | } 85 | } 86 | 87 | async getSchema(schemaUri: string): Promise { 88 | if (!this.isInitialized) { 89 | await this.init(); 90 | } 91 | const cacheFile = this.cache[schemaUri]?.schemaPath; 92 | if (await fs.pathExists(cacheFile)) { 93 | return await fs.readFile(cacheFile, { encoding: 'UTF8' }); 94 | } 95 | 96 | return undefined; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/json-schema-content-provider.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { TextDocumentContentProvider, Uri, workspace, window } from 'vscode'; 7 | import { xhr, configure as configureHttpRequests, getErrorStatusDescription, XHRResponse } from 'request-light'; 8 | import { SchemaExtensionAPI } from './schema-extension-api'; 9 | 10 | export interface IJSONSchemaCache { 11 | getETag(schemaUri: string): string | undefined; 12 | putSchema(schemaUri: string, eTag: string, schemaContent: string): Promise; 13 | getSchema(schemaUri: string): Promise; 14 | } 15 | 16 | export class JSONSchemaDocumentContentProvider implements TextDocumentContentProvider { 17 | constructor(private readonly schemaCache: IJSONSchemaCache, private readonly schemaApi: SchemaExtensionAPI) {} 18 | async provideTextDocumentContent(uri: Uri): Promise { 19 | if (uri.fragment) { 20 | const origUri = uri.fragment; 21 | const schemaUri = Uri.parse(origUri); 22 | // handle both 'http' and 'https' 23 | if (origUri.startsWith('http')) { 24 | return getJsonSchemaContent(origUri, this.schemaCache); 25 | } else if (this.schemaApi.hasProvider(schemaUri.scheme)) { 26 | let content = this.schemaApi.requestCustomSchemaContent(origUri); 27 | 28 | content = await Promise.resolve(content); 29 | // prettify JSON 30 | if (content.indexOf('\n') === -1) { 31 | content = JSON.stringify(JSON.parse(content), null, 2); 32 | } 33 | 34 | return content; 35 | } else { 36 | window.showErrorMessage(`Cannot Load content for: ${origUri}. Unknown schema: '${schemaUri.scheme}'`); 37 | return null; 38 | } 39 | } else { 40 | window.showErrorMessage(`Cannot Load content for: '${uri.toString()}' `); 41 | return null; 42 | } 43 | } 44 | } 45 | 46 | export async function getJsonSchemaContent(uri: string, schemaCache: IJSONSchemaCache): Promise { 47 | const cachedETag = schemaCache.getETag(uri); 48 | 49 | const httpSettings = workspace.getConfiguration('http'); 50 | configureHttpRequests(httpSettings.proxy, httpSettings.proxyStrictSSL); 51 | 52 | const headers: { [key: string]: string } = { 'Accept-Encoding': 'gzip, deflate' }; 53 | if (cachedETag) { 54 | headers['If-None-Match'] = cachedETag; 55 | } 56 | return xhr({ url: uri, followRedirects: 5, headers }) 57 | .then(async (response) => { 58 | // cache only if server supports 'etag' header 59 | const etag = response.headers['etag']; 60 | if (typeof etag === 'string') { 61 | await schemaCache.putSchema(uri, etag, response.responseText); 62 | } 63 | return response.responseText; 64 | }) 65 | .then((text) => { 66 | return text; 67 | }) 68 | .catch(async (error: XHRResponse) => { 69 | // content not changed, return cached 70 | if (error.status === 304) { 71 | const content = await schemaCache.getSchema(uri); 72 | // ensure that we return content even if cache doesn't have it 73 | if (content === undefined) { 74 | console.error(`Cannot read cached content for: ${uri}, trying to load again`); 75 | delete headers['If-None-Match']; 76 | return xhr({ url: uri, followRedirects: 5, headers }) 77 | .then((response) => { 78 | return response.responseText; 79 | }) 80 | .catch((err: XHRResponse) => { 81 | return createReject(err); 82 | }); 83 | } 84 | return content; 85 | } 86 | // in case of some error, like internet connection issue, check if cached version exist and return it 87 | if (schemaCache.getETag(uri)) { 88 | const content = schemaCache.getSchema(uri); 89 | if (content) { 90 | return content; 91 | } 92 | } 93 | return createReject(error); 94 | }); 95 | } 96 | 97 | function createReject(error: XHRResponse): Promise { 98 | return Promise.reject(error.responseText || getErrorStatusDescription(error.status) || error.toString()); 99 | } 100 | -------------------------------------------------------------------------------- /src/node/yamlClientMain.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { ExtensionContext } from 'vscode'; 7 | import { startClient, LanguageClientConstructor, RuntimeEnvironment } from '../extension'; 8 | import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node'; 9 | 10 | import { SchemaExtensionAPI } from '../schema-extension-api'; 11 | 12 | import { getRedHatService } from '@redhat-developer/vscode-redhat-telemetry'; 13 | import { JSONSchemaCache } from '../json-schema-cache'; 14 | 15 | // this method is called when vs code is activated 16 | export async function activate(context: ExtensionContext): Promise { 17 | // Create Telemetry Service 18 | const telemetry = await (await getRedHatService(context)).getTelemetryService(); 19 | 20 | let serverModule: string; 21 | if (startedFromSources()) { 22 | serverModule = context.asAbsolutePath('../yaml-language-server/out/server/src/server.js'); 23 | } else { 24 | // The YAML language server is implemented in node 25 | serverModule = context.asAbsolutePath('./dist/languageserver.js'); 26 | } 27 | 28 | // The debug options for the server 29 | const debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] }; 30 | 31 | // If the extension is launched in debug mode then the debug server options are used 32 | // Otherwise the run options are used 33 | const serverOptions: ServerOptions = { 34 | run: { module: serverModule, transport: TransportKind.ipc }, 35 | debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }, 36 | }; 37 | 38 | const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { 39 | return new LanguageClient(id, name, serverOptions, clientOptions); 40 | }; 41 | 42 | const runtime: RuntimeEnvironment = { 43 | telemetry, 44 | schemaCache: new JSONSchemaCache(context.globalStorageUri.fsPath, context.globalState), 45 | }; 46 | 47 | return startClient(context, newLanguageClient, runtime); 48 | } 49 | 50 | function startedFromSources(): boolean { 51 | return process.env['DEBUG_VSCODE_YAML'] === 'true'; 52 | } 53 | -------------------------------------------------------------------------------- /src/paths.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Copyright (c) Microsoft Corporation. All rights reserved. 4 | * Licensed under the MIT License. See License.txt in the project root for license information. 5 | *--------------------------------------------------------------------------------------------*/ 6 | import { Uri } from 'vscode'; 7 | 8 | const Dot = '.'.charCodeAt(0); 9 | 10 | export function normalizePath(parts: string[]): string { 11 | const newParts: string[] = []; 12 | for (const part of parts) { 13 | if (part.length === 0 || (part.length === 1 && part.charCodeAt(0) === Dot)) { 14 | // ignore 15 | } else if (part.length === 2 && part.charCodeAt(0) === Dot && part.charCodeAt(1) === Dot) { 16 | newParts.pop(); 17 | } else { 18 | newParts.push(part); 19 | } 20 | } 21 | if (parts.length > 1 && parts[parts.length - 1].length === 0) { 22 | newParts.push(''); 23 | } 24 | let res = newParts.join('/'); 25 | if (parts[0].length === 0) { 26 | res = '/' + res; 27 | } 28 | return res; 29 | } 30 | 31 | export function joinPath(uri: Uri, ...paths: string[]): Uri { 32 | const parts = uri.path.split('/'); 33 | for (const path of paths) { 34 | parts.push(...path.split('/')); 35 | } 36 | return uri.with({ path: normalizePath(parts) }); 37 | } 38 | -------------------------------------------------------------------------------- /src/recommendation/handler.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | 'use strict'; 7 | 8 | export interface IHandler { 9 | handle(extName: string, message: string): Promise; 10 | 11 | canRecommendExtension(extName: string): boolean; 12 | } 13 | -------------------------------------------------------------------------------- /src/recommendation/handlerImpl.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | 'use strict'; 7 | 8 | import * as vscode from 'vscode'; 9 | import { IHandler } from './handler'; 10 | 11 | const KEY_RECOMMENDATION_USER_CHOICE_MAP = 'recommendationUserChoice'; 12 | 13 | async function installExtensionCmdHandler(extensionName: string, displayName: string): Promise { 14 | return vscode.window 15 | .withProgress( 16 | { location: vscode.ProgressLocation.Notification, title: `Installing ${displayName || extensionName}...` }, 17 | () => { 18 | return vscode.commands.executeCommand('workbench.extensions.installExtension', extensionName); 19 | } 20 | ) 21 | .then(() => { 22 | vscode.window.showInformationMessage(`Successfully installed ${displayName || extensionName}.`); 23 | }); 24 | } 25 | 26 | enum UserChoice { 27 | install = 'Install', 28 | never = 'Never', 29 | later = 'Later', 30 | } 31 | 32 | export class HandlerImpl implements IHandler { 33 | userChoice: () => unknown; 34 | storeUserChoice: (choice: unknown) => void; 35 | constructor(context: vscode.ExtensionContext) { 36 | this.userChoice = () => { 37 | return context.globalState.get(KEY_RECOMMENDATION_USER_CHOICE_MAP, {}); 38 | }; 39 | 40 | this.storeUserChoice = (choice: unknown) => { 41 | context.globalState.update(KEY_RECOMMENDATION_USER_CHOICE_MAP, choice); 42 | }; 43 | } 44 | 45 | isExtensionInstalled(extName: string): boolean { 46 | return !!vscode.extensions.getExtension(extName); 47 | } 48 | 49 | canRecommendExtension(extName: string): boolean { 50 | return this.userChoice()[extName] !== UserChoice.never && !this.isExtensionInstalled(extName); 51 | } 52 | 53 | async handle(extName: string, message: string): Promise { 54 | if (this.isExtensionInstalled(extName)) { 55 | return; 56 | } 57 | 58 | const choice = this.userChoice(); 59 | if (choice[extName] === UserChoice.never) { 60 | return; 61 | } 62 | 63 | const actions: Array = Object.values(UserChoice); 64 | const answer = await vscode.window.showInformationMessage(message, ...actions); 65 | if (answer === UserChoice.install) { 66 | await installExtensionCmdHandler(extName, extName); 67 | } 68 | 69 | choice[extName] = answer; 70 | this.storeUserChoice(choice); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/recommendation/index.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | 'use strict'; 7 | 8 | import * as vscode from 'vscode'; 9 | import { HandlerImpl } from './handlerImpl'; 10 | import { initializeRecommendation as initOpenShiftToolkit } from './openShiftToolkit'; 11 | 12 | export function initializeRecommendation(context: vscode.ExtensionContext): void { 13 | const handler = new HandlerImpl(context); 14 | initOpenShiftToolkit(context, handler); 15 | } 16 | -------------------------------------------------------------------------------- /src/recommendation/openShiftToolkit.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | 'use strict'; 7 | 8 | import * as fs from 'fs'; 9 | import * as path from 'path'; 10 | import * as vscode from 'vscode'; 11 | import { IHandler } from './handler'; 12 | 13 | const EXTENSION_NAME = 'redhat.vscode-openshift-connector'; 14 | const GH_ORG_URL = `https://github.com/redhat-developer/vscode-openshift-tools`; 15 | const RECOMMENDATION_MESSAGE = `The workspace has a devfile.yaml. Install [OpenShift Toolkit](${GH_ORG_URL}) extension for assistance with deploying to a cluster?`; 16 | const YAML_EXTENSION_RECOMMENDATIONS = 'yaml.extension.recommendations'; 17 | 18 | function isDevfileYAML(uri: vscode.Uri): boolean { 19 | try { 20 | if (fs.lstatSync(uri.fsPath).isDirectory()) { 21 | const devFileYamlPath = path.join(uri.fsPath, 'devfile.yaml'); 22 | return fs.existsSync(devFileYamlPath); 23 | } 24 | } catch (error) { 25 | return false; 26 | } 27 | return !!uri.path && path.basename(uri.path).toLowerCase() === 'devfile.yaml'; 28 | } 29 | 30 | export function initializeRecommendation(context: vscode.ExtensionContext, handler: IHandler): void { 31 | const show = vscode.workspace.getConfiguration().get(YAML_EXTENSION_RECOMMENDATIONS); 32 | if (!show) { 33 | return; 34 | } 35 | if (!handler.canRecommendExtension(EXTENSION_NAME)) { 36 | return; 37 | } 38 | context.subscriptions.push( 39 | vscode.workspace.onDidOpenTextDocument((e) => { 40 | if (isDevfileYAML(e.uri)) { 41 | handler.handle(EXTENSION_NAME, RECOMMENDATION_MESSAGE); 42 | } 43 | }) 44 | ); 45 | 46 | const isdevfileYAMLOpened = vscode.workspace.workspaceFolders?.findIndex((workspace) => isDevfileYAML(workspace.uri)) > -1; 47 | if (isdevfileYAMLOpened) { 48 | handler.handle(EXTENSION_NAME, RECOMMENDATION_MESSAGE); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/schema-extension-api.ts: -------------------------------------------------------------------------------- 1 | import { URI } from 'vscode-uri'; 2 | import { CommonLanguageClient as LanguageClient, RequestType } from 'vscode-languageclient/node'; 3 | import { workspace } from 'vscode'; 4 | import { logToExtensionOutputChannel } from './extension'; 5 | 6 | interface SchemaContributorProvider { 7 | readonly requestSchema: (resource: string) => string; 8 | readonly requestSchemaContent: (uri: string) => Promise | string; 9 | readonly label?: string; 10 | } 11 | 12 | export enum MODIFICATION_ACTIONS { 13 | 'delete', 14 | 'add', 15 | } 16 | 17 | export interface SchemaAdditions { 18 | schema: string; 19 | action: MODIFICATION_ACTIONS.add; 20 | path: string; 21 | key: string; 22 | content: unknown; 23 | } 24 | 25 | export interface SchemaDeletions { 26 | schema: string; 27 | action: MODIFICATION_ACTIONS.delete; 28 | path: string; 29 | key: string; 30 | } 31 | 32 | // eslint-disable-next-line @typescript-eslint/no-namespace 33 | namespace SchemaModificationNotification { 34 | // eslint-disable-next-line @typescript-eslint/ban-types 35 | export const type: RequestType = new RequestType('json/schema/modify'); 36 | } 37 | 38 | export interface ExtensionAPI { 39 | registerContributor( 40 | schema: string, 41 | requestSchema: (resource: string) => string, 42 | requestSchemaContent: (uri: string) => Promise | string, 43 | label?: string 44 | ): boolean; 45 | modifySchemaContent(schemaModifications: SchemaAdditions | SchemaDeletions): Promise; 46 | } 47 | 48 | class SchemaExtensionAPI implements ExtensionAPI { 49 | private _customSchemaContributors: { [index: string]: SchemaContributorProvider } = {}; 50 | private _yamlClient: LanguageClient; 51 | 52 | constructor(client: LanguageClient) { 53 | this._yamlClient = client; 54 | } 55 | 56 | /** 57 | * Register a custom schema provider 58 | * 59 | * @param {string} the provider's name 60 | * @param requestSchema the requestSchema function 61 | * @param requestSchemaContent the requestSchemaContent function 62 | * @param label the content label, yaml key value pair, like 'apiVersion:some.api/v1' 63 | * @returns {boolean} 64 | */ 65 | public registerContributor( 66 | schema: string, 67 | requestSchema: (resource: string) => string, 68 | requestSchemaContent: (uri: string) => Promise | string, 69 | label?: string 70 | ): boolean { 71 | if (this._customSchemaContributors[schema]) { 72 | return false; 73 | } 74 | 75 | if (!requestSchema) { 76 | throw new Error('Illegal parameter for requestSchema.'); 77 | } 78 | 79 | if (label) { 80 | const [first, second] = label.split(':'); 81 | if (first && second) { 82 | label = second.trim(); 83 | label = label.replaceAll('.', '\\.'); 84 | label = `${first}:[\t ]+${label}`; 85 | } 86 | } 87 | this._customSchemaContributors[schema] = { 88 | requestSchema, 89 | requestSchemaContent, 90 | label, 91 | }; 92 | 93 | return true; 94 | } 95 | 96 | /** 97 | * Call requestSchema for each provider and finds all matches. 98 | * 99 | * @param {string} resource 100 | * @returns {string} the schema uri 101 | */ 102 | public requestCustomSchema(resource: string): string[] { 103 | const matches = []; 104 | for (const customKey of Object.keys(this._customSchemaContributors)) { 105 | try { 106 | const contributor = this._customSchemaContributors[customKey]; 107 | let uri: string; 108 | if (contributor.label && workspace.textDocuments) { 109 | const labelRegexp = new RegExp(contributor.label, 'g'); 110 | for (const doc of workspace.textDocuments) { 111 | if (doc.uri.toString() === resource) { 112 | if (labelRegexp.test(doc.getText())) { 113 | uri = contributor.requestSchema(resource); 114 | return [uri]; 115 | } 116 | } 117 | } 118 | } 119 | 120 | uri = contributor.requestSchema(resource); 121 | 122 | if (uri) { 123 | matches.push(uri); 124 | } 125 | } catch (error) { 126 | logToExtensionOutputChannel( 127 | `Error thrown while requesting schema "${error}" when calling the registered contributor "${customKey}"` 128 | ); 129 | } 130 | } 131 | return matches; 132 | } 133 | 134 | /** 135 | * Call requestCustomSchemaContent for named provider and get the schema content. 136 | * 137 | * @param {string} uri the schema uri returned from requestSchema. 138 | * @returns {string} the schema content 139 | */ 140 | public requestCustomSchemaContent(uri: string): Promise | string { 141 | if (uri) { 142 | const _uri = URI.parse(uri); 143 | 144 | if ( 145 | _uri.scheme && 146 | this._customSchemaContributors[_uri.scheme] && 147 | this._customSchemaContributors[_uri.scheme].requestSchemaContent 148 | ) { 149 | return this._customSchemaContributors[_uri.scheme].requestSchemaContent(uri); 150 | } 151 | } 152 | } 153 | 154 | public async modifySchemaContent(schemaModifications: SchemaAdditions | SchemaDeletions): Promise { 155 | return this._yamlClient.sendRequest(SchemaModificationNotification.type, schemaModifications); 156 | } 157 | 158 | public hasProvider(schema: string): boolean { 159 | return this._customSchemaContributors[schema] !== undefined; 160 | } 161 | } 162 | 163 | // constants 164 | export const CUSTOM_SCHEMA_REQUEST = 'custom/schema/request'; 165 | export const CUSTOM_CONTENT_REQUEST = 'custom/schema/content'; 166 | 167 | export { SchemaExtensionAPI }; 168 | -------------------------------------------------------------------------------- /src/schema-status-bar-item.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import { 6 | ExtensionContext, 7 | window, 8 | commands, 9 | StatusBarAlignment, 10 | TextEditor, 11 | StatusBarItem, 12 | QuickPickItem, 13 | ThemeColor, 14 | workspace, 15 | } from 'vscode'; 16 | import { CommonLanguageClient, RequestType } from 'vscode-languageclient/node'; 17 | 18 | type FileUri = string; 19 | type SchemaVersions = { [version: string]: string }; 20 | interface JSONSchema { 21 | name?: string; 22 | description?: string; 23 | uri: string; 24 | versions?: SchemaVersions; 25 | } 26 | 27 | interface MatchingJSONSchema extends JSONSchema { 28 | usedForCurrentFile: boolean; 29 | fromStore: boolean; 30 | } 31 | 32 | interface SchemaItem extends QuickPickItem { 33 | schema?: MatchingJSONSchema; 34 | } 35 | 36 | interface SchemaVersionItem extends QuickPickItem { 37 | version: string; 38 | url: string; 39 | } 40 | 41 | // eslint-disable-next-line @typescript-eslint/ban-types 42 | const getJSONSchemasRequestType: RequestType = new RequestType('yaml/get/all/jsonSchemas'); 43 | 44 | // eslint-disable-next-line @typescript-eslint/ban-types 45 | const getSchemaRequestType: RequestType = new RequestType('yaml/get/jsonSchema'); 46 | 47 | export let statusBarItem: StatusBarItem; 48 | 49 | let client: CommonLanguageClient; 50 | let versionSelection: SchemaItem = undefined; 51 | 52 | const selectVersionLabel = 'Select Different Version'; 53 | 54 | export function createJSONSchemaStatusBarItem(context: ExtensionContext, languageclient: CommonLanguageClient): void { 55 | if (statusBarItem) { 56 | updateStatusBar(window.activeTextEditor); 57 | return; 58 | } 59 | const commandId = 'yaml.select.json.schema'; 60 | client = languageclient; 61 | commands.registerCommand(commandId, () => { 62 | return showSchemaSelection(); 63 | }); 64 | statusBarItem = window.createStatusBarItem(StatusBarAlignment.Right); 65 | statusBarItem.command = commandId; 66 | context.subscriptions.push(statusBarItem); 67 | 68 | context.subscriptions.push(window.onDidChangeActiveTextEditor(updateStatusBar)); 69 | 70 | updateStatusBar(window.activeTextEditor); 71 | } 72 | 73 | async function updateStatusBar(editor: TextEditor): Promise { 74 | if (editor && editor.document.languageId === 'yaml') { 75 | versionSelection = undefined; 76 | // get schema info there 77 | const schema = await client.sendRequest(getSchemaRequestType, editor.document.uri.toString()); 78 | if (!schema || schema.length === 0) { 79 | statusBarItem.text = 'No JSON Schema'; 80 | statusBarItem.tooltip = 'Select JSON Schema'; 81 | statusBarItem.backgroundColor = undefined; 82 | } else if (schema.length === 1) { 83 | statusBarItem.text = schema[0].name ?? schema[0].uri; 84 | let version; 85 | if (schema[0].versions) { 86 | version = findUsedVersion(schema[0].versions, schema[0].uri); 87 | } else { 88 | const schemas = await client.sendRequest(getJSONSchemasRequestType, window.activeTextEditor.document.uri.toString()); 89 | let versionSchema: JSONSchema; 90 | const schemaStoreItem = findSchemaStoreItem(schemas, schema[0].uri); 91 | if (schemaStoreItem) { 92 | [version, versionSchema] = schemaStoreItem; 93 | (versionSchema as MatchingJSONSchema).usedForCurrentFile = true; 94 | versionSchema.uri = schema[0].uri; 95 | versionSelection = createSelectVersionItem(version, versionSchema as MatchingJSONSchema); 96 | } 97 | } 98 | if (version && !statusBarItem.text.includes(version)) { 99 | statusBarItem.text += `(${version})`; 100 | } 101 | statusBarItem.tooltip = 'Select JSON Schema'; 102 | statusBarItem.backgroundColor = undefined; 103 | } else { 104 | statusBarItem.text = 'Multiple JSON Schemas...'; 105 | statusBarItem.tooltip = 'Multiple JSON Schema used to validate this file, click to select one'; 106 | statusBarItem.backgroundColor = new ThemeColor('statusBarItem.warningBackground'); 107 | } 108 | 109 | statusBarItem.show(); 110 | } else { 111 | statusBarItem.hide(); 112 | } 113 | } 114 | 115 | async function showSchemaSelection(): Promise { 116 | const schemas = await client.sendRequest(getJSONSchemasRequestType, window.activeTextEditor.document.uri.toString()); 117 | const schemasPick = window.createQuickPick(); 118 | let pickItems: SchemaItem[] = []; 119 | 120 | for (const val of schemas) { 121 | if (val.usedForCurrentFile && val.versions) { 122 | const item = createSelectVersionItem(findUsedVersion(val.versions, val.uri), val); 123 | pickItems.unshift(item); 124 | } 125 | const item = { 126 | label: val.name ?? val.uri, 127 | description: val.description, 128 | detail: val.usedForCurrentFile ? 'Used for current file$(check)' : '', 129 | alwaysShow: val.usedForCurrentFile, 130 | schema: val, 131 | }; 132 | pickItems.push(item); 133 | } 134 | if (versionSelection) { 135 | pickItems.unshift(versionSelection); 136 | } 137 | 138 | pickItems = pickItems.sort((a, b) => { 139 | if (a.schema?.usedForCurrentFile && a.schema?.versions) { 140 | return -1; 141 | } 142 | if (b.schema?.usedForCurrentFile && b.schema?.versions) { 143 | return 1; 144 | } 145 | if (a.schema?.usedForCurrentFile) { 146 | return -1; 147 | } 148 | if (b.schema?.usedForCurrentFile) { 149 | return 1; 150 | } 151 | return a.label.localeCompare(b.label); 152 | }); 153 | 154 | schemasPick.items = pickItems; 155 | schemasPick.placeholder = 'Search JSON schema'; 156 | schemasPick.title = 'Select JSON schema'; 157 | schemasPick.onDidHide(() => schemasPick.dispose()); 158 | 159 | schemasPick.onDidChangeSelection((selection) => { 160 | try { 161 | if (selection.length > 0) { 162 | if (selection[0].label === selectVersionLabel) { 163 | handleSchemaVersionSelection(selection[0].schema); 164 | } else if (selection[0].schema) { 165 | writeSchemaUriMapping(selection[0].schema.uri); 166 | } 167 | } 168 | } catch (err) { 169 | console.error(err); 170 | } 171 | schemasPick.hide(); 172 | }); 173 | schemasPick.show(); 174 | } 175 | 176 | function deleteExistingFilePattern(settings: Record, fileUri: string): unknown { 177 | for (const key in settings) { 178 | if (Object.prototype.hasOwnProperty.call(settings, key)) { 179 | const element = settings[key]; 180 | 181 | if (Array.isArray(element)) { 182 | const filePatterns = element.filter((val) => val !== fileUri); 183 | settings[key] = filePatterns; 184 | } 185 | 186 | if (element === fileUri) { 187 | delete settings[key]; 188 | } 189 | } 190 | } 191 | 192 | return settings; 193 | } 194 | 195 | function createSelectVersionItem(version: string, schema: MatchingJSONSchema): SchemaItem { 196 | return { 197 | label: selectVersionLabel, 198 | detail: `Current: ${version}`, 199 | alwaysShow: true, 200 | schema: schema, 201 | }; 202 | } 203 | function findSchemaStoreItem(schemas: JSONSchema[], url: string): [string, JSONSchema] | undefined { 204 | for (const schema of schemas) { 205 | if (schema.versions) { 206 | for (const version in schema.versions) { 207 | if (url === schema.versions[version]) { 208 | return [version, schema]; 209 | } 210 | } 211 | } 212 | } 213 | } 214 | 215 | function writeSchemaUriMapping(schemaUrl: string): void { 216 | const settings: Record = workspace.getConfiguration('yaml').get('schemas'); 217 | const fileUri = window.activeTextEditor.document.uri.toString(); 218 | const newSettings = Object.assign({}, settings); 219 | deleteExistingFilePattern(newSettings, fileUri); 220 | const schemaSettings = newSettings[schemaUrl]; 221 | if (schemaSettings) { 222 | if (Array.isArray(schemaSettings)) { 223 | (schemaSettings as Array).push(fileUri); 224 | } else if (typeof schemaSettings === 'string') { 225 | newSettings[schemaUrl] = [schemaSettings, fileUri]; 226 | } 227 | } else { 228 | newSettings[schemaUrl] = fileUri; 229 | } 230 | workspace.getConfiguration('yaml').update('schemas', newSettings); 231 | } 232 | 233 | function handleSchemaVersionSelection(schema: MatchingJSONSchema): void { 234 | const versionPick = window.createQuickPick(); 235 | const versionItems: SchemaVersionItem[] = []; 236 | const usedVersion = findUsedVersion(schema.versions, schema.uri); 237 | for (const version in schema.versions) { 238 | versionItems.push({ 239 | label: version + (usedVersion === version ? '$(check)' : ''), 240 | url: schema.versions[version], 241 | version: version, 242 | }); 243 | } 244 | 245 | versionPick.items = versionItems; 246 | versionPick.title = `Select JSON Schema version for ${schema.name}`; 247 | versionPick.placeholder = 'Version'; 248 | versionPick.onDidHide(() => versionPick.dispose()); 249 | 250 | versionPick.onDidChangeSelection((items) => { 251 | if (items && items.length === 1) { 252 | writeSchemaUriMapping(items[0].url); 253 | } 254 | versionPick.hide(); 255 | }); 256 | versionPick.show(); 257 | } 258 | 259 | function findUsedVersion(versions: SchemaVersions, uri: string): string { 260 | for (const version in versions) { 261 | const versionUri = versions[version]; 262 | if (versionUri === uri) { 263 | return version; 264 | } 265 | } 266 | return 'latest'; 267 | } 268 | -------------------------------------------------------------------------------- /src/telemetry.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { TelemetryService } from './extension'; 7 | import { CloseAction, ErrorAction, ErrorHandler, Message } from 'vscode-languageclient'; 8 | import * as vscode from 'vscode'; 9 | 10 | export class TelemetryErrorHandler implements ErrorHandler { 11 | private restarts: number[] = []; 12 | constructor( 13 | private readonly telemetry: TelemetryService, 14 | private readonly name: string, 15 | private readonly maxRestartCount: number 16 | ) {} 17 | 18 | error(error: Error, message: Message, count: number): ErrorAction { 19 | this.telemetry.send({ name: 'yaml.lsp.error', properties: { jsonrpc: message.jsonrpc, error: error.message } }); 20 | if (count && count <= 3) { 21 | return ErrorAction.Continue; 22 | } 23 | return ErrorAction.Shutdown; 24 | } 25 | closed(): CloseAction { 26 | this.restarts.push(Date.now()); 27 | if (this.restarts.length <= this.maxRestartCount) { 28 | return CloseAction.Restart; 29 | } else { 30 | const diff = this.restarts[this.restarts.length - 1] - this.restarts[0]; 31 | if (diff <= 3 * 60 * 1000) { 32 | vscode.window.showErrorMessage( 33 | `The ${this.name} server crashed ${ 34 | this.maxRestartCount + 1 35 | } times in the last 3 minutes. The server will not be restarted.` 36 | ); 37 | return CloseAction.DoNotRestart; 38 | } else { 39 | this.restarts.shift(); 40 | return CloseAction.Restart; 41 | } 42 | } 43 | } 44 | } 45 | 46 | const errorMassagesToSkip = [{ text: 'Warning: Setting the NODE_TLS_REJECT_UNAUTHORIZED', contains: true }]; 47 | 48 | export class TelemetryOutputChannel implements vscode.OutputChannel { 49 | private errors: string[] | undefined; 50 | private throttleTimeout: vscode.Disposable | undefined; 51 | constructor(private readonly delegate: vscode.OutputChannel, private readonly telemetry: TelemetryService) {} 52 | 53 | get name(): string { 54 | return this.delegate.name; 55 | } 56 | append(value: string): void { 57 | this.checkError(value); 58 | this.delegate.append(value); 59 | } 60 | appendLine(value: string): void { 61 | this.checkError(value); 62 | this.delegate.appendLine(value); 63 | } 64 | replace(value: string): void { 65 | this.checkError(value); 66 | this.delegate.replace(value); 67 | } 68 | private checkError(value: string): void { 69 | if (value.startsWith('[Error') || value.startsWith(' Message: Request')) { 70 | if (this.isNeedToSkip(value)) { 71 | return; 72 | } 73 | if (!this.errors) { 74 | this.errors = []; 75 | } 76 | if (this.throttleTimeout) { 77 | this.throttleTimeout.dispose(); 78 | } 79 | this.errors.push(value); 80 | const timeoutHandle = setTimeout(() => { 81 | this.telemetry.send({ name: 'yaml.server.error', properties: { error: this.createErrorMessage() } }); 82 | this.errors = undefined; 83 | }, 50); 84 | this.throttleTimeout = new vscode.Disposable(() => clearTimeout(timeoutHandle)); 85 | } 86 | } 87 | 88 | private isNeedToSkip(value: string): boolean { 89 | for (const skip of errorMassagesToSkip) { 90 | if (skip.contains) { 91 | if (value.includes(skip.text)) { 92 | return true; 93 | } 94 | } else { 95 | const starts = value.startsWith(skip.text); 96 | if (starts) { 97 | return true; 98 | } 99 | } 100 | } 101 | 102 | return false; 103 | } 104 | 105 | private createErrorMessage(): string { 106 | const result = []; 107 | for (const value of this.errors) { 108 | if (value.startsWith('[Error')) { 109 | result.push(value.substr(value.indexOf(']') + 1, value.length).trim()); 110 | } else { 111 | result.push(value); 112 | } 113 | } 114 | 115 | return result.join('\n'); 116 | } 117 | 118 | clear(): void { 119 | this.delegate.clear(); 120 | } 121 | show(preserveFocus?: boolean): void; 122 | show(column?: vscode.ViewColumn, preserveFocus?: boolean): void; 123 | show(column?: never, preserveFocus?: boolean): void { 124 | this.delegate.show(column, preserveFocus); 125 | } 126 | hide(): void { 127 | this.delegate.hide(); 128 | } 129 | dispose(): void { 130 | this.delegate.dispose(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/webworker/yamlClientMain.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-function-return-type */ 2 | /*--------------------------------------------------------------------------------------------- 3 | * Copyright (c) Microsoft Corporation. All rights reserved. 4 | * Licensed under the MIT License. See License.txt in the project root for license information. 5 | *--------------------------------------------------------------------------------------------*/ 6 | 7 | import { ExtensionContext } from 'vscode'; 8 | import { LanguageClientOptions } from 'vscode-languageclient'; 9 | import { startClient, LanguageClientConstructor, RuntimeEnvironment } from '../extension'; 10 | import { LanguageClient } from 'vscode-languageclient/browser'; 11 | import { SchemaExtensionAPI } from '../schema-extension-api'; 12 | import { IJSONSchemaCache } from '../json-schema-content-provider'; 13 | import { getRedHatService } from '@redhat-developer/vscode-redhat-telemetry/lib/webworker'; 14 | // this method is called when vs code is activated 15 | export async function activate(context: ExtensionContext): Promise { 16 | const extensionUri = context.extensionUri; 17 | const serverMain = extensionUri.with({ 18 | path: extensionUri.path + '/dist/languageserver-web.js', 19 | }); 20 | try { 21 | const worker = new Worker(serverMain.toString()); 22 | const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { 23 | return new LanguageClient(id, name, clientOptions, worker); 24 | }; 25 | 26 | const schemaCache: IJSONSchemaCache = { 27 | getETag: () => undefined, 28 | getSchema: async () => undefined, 29 | putSchema: () => Promise.resolve(), 30 | }; 31 | const telemetry = await (await getRedHatService(context)).getTelemetryService(); 32 | const runtime: RuntimeEnvironment = { 33 | telemetry, 34 | schemaCache, 35 | }; 36 | return startClient(context, newLanguageClient, runtime); 37 | } catch (e) { 38 | console.log(e); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /syntaxes/yaml.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "information_for_contributors": [ 3 | "This file has been converted from https://github.com/textmate/yaml.tmbundle/blob/master/Syntaxes/YAML.tmLanguage", 4 | "If you want to provide a fix or improvement, please create a pull request against the original repository.", 5 | "Once accepted there, we are happy to receive an update request." 6 | ], 7 | "version": "https://github.com/textmate/yaml.tmbundle/commit/e54ceae3b719506dba7e481a77cea4a8b576ae46", 8 | "name": "YAML", 9 | "scopeName": "source.yaml", 10 | "patterns": [ 11 | { 12 | "include": "#comment" 13 | }, 14 | { 15 | "include": "#property" 16 | }, 17 | { 18 | "include": "#directive" 19 | }, 20 | { 21 | "match": "^---", 22 | "name": "entity.other.document.begin.yaml" 23 | }, 24 | { 25 | "match": "^\\.{3}", 26 | "name": "entity.other.document.end.yaml" 27 | }, 28 | { 29 | "include": "#node" 30 | } 31 | ], 32 | "repository": { 33 | "block-collection": { 34 | "patterns": [ 35 | { 36 | "include": "#block-sequence" 37 | }, 38 | { 39 | "include": "#block-mapping" 40 | } 41 | ] 42 | }, 43 | "block-mapping": { 44 | "patterns": [ 45 | { 46 | "include": "#block-pair" 47 | } 48 | ] 49 | }, 50 | "block-node": { 51 | "patterns": [ 52 | { 53 | "include": "#prototype" 54 | }, 55 | { 56 | "include": "#block-scalar" 57 | }, 58 | { 59 | "include": "#block-collection" 60 | }, 61 | { 62 | "include": "#flow-scalar-plain-out" 63 | }, 64 | { 65 | "include": "#flow-node" 66 | } 67 | ] 68 | }, 69 | "block-pair": { 70 | "patterns": [ 71 | { 72 | "begin": "\\?", 73 | "beginCaptures": { 74 | "1": { 75 | "name": "punctuation.definition.key-value.begin.yaml" 76 | } 77 | }, 78 | "end": "(?=\\?)|^ *(:)|(:)", 79 | "endCaptures": { 80 | "1": { 81 | "name": "punctuation.separator.key-value.mapping.yaml" 82 | }, 83 | "2": { 84 | "name": "invalid.illegal.expected-newline.yaml" 85 | } 86 | }, 87 | "name": "meta.block-mapping.yaml", 88 | "patterns": [ 89 | { 90 | "include": "#block-node" 91 | } 92 | ] 93 | }, 94 | { 95 | "begin": "(?x)\n (?=\n (?x:\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] \\S\n )\n (\n [^\\s:]\n | : \\S\n | \\s+ (?![#\\s])\n )*\n \\s*\n :\n\t\t\t\t\t\t\t(\\s|$)\n )\n ", 96 | "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n )\n ", 97 | "patterns": [ 98 | { 99 | "include": "#flow-scalar-plain-out-implicit-type" 100 | }, 101 | { 102 | "begin": "(?x)\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] \\S\n ", 103 | "beginCaptures": { 104 | "0": { 105 | "name": "entity.name.tag.yaml" 106 | } 107 | }, 108 | "contentName": "entity.name.tag.yaml", 109 | "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n )\n ", 110 | "name": "string.unquoted.plain.out.yaml" 111 | } 112 | ] 113 | }, 114 | { 115 | "match": ":(?=\\s|$)", 116 | "name": "punctuation.separator.key-value.mapping.yaml" 117 | } 118 | ] 119 | }, 120 | "block-scalar": { 121 | "begin": "(?:(\\|)|(>))([1-9])?([-+])?(.*\\n?)", 122 | "beginCaptures": { 123 | "1": { 124 | "name": "keyword.control.flow.block-scalar.literal.yaml" 125 | }, 126 | "2": { 127 | "name": "keyword.control.flow.block-scalar.folded.yaml" 128 | }, 129 | "3": { 130 | "name": "constant.numeric.indentation-indicator.yaml" 131 | }, 132 | "4": { 133 | "name": "storage.modifier.chomping-indicator.yaml" 134 | }, 135 | "5": { 136 | "patterns": [ 137 | { 138 | "include": "#comment" 139 | }, 140 | { 141 | "match": ".+", 142 | "name": "invalid.illegal.expected-comment-or-newline.yaml" 143 | } 144 | ] 145 | } 146 | }, 147 | "end": "^(?=\\S)|(?!\\G)", 148 | "patterns": [ 149 | { 150 | "begin": "^([ ]+)(?! )", 151 | "end": "^(?!\\1|\\s*$)", 152 | "name": "string.unquoted.block.yaml" 153 | } 154 | ] 155 | }, 156 | "block-sequence": { 157 | "match": "(-)(?!\\S)", 158 | "name": "punctuation.definition.block.sequence.item.yaml" 159 | }, 160 | "comment": { 161 | "begin": "(?:(^[ \\t]*)|[ \\t]+)(?=#\\p{Print}*$)", 162 | "beginCaptures": { 163 | "1": { 164 | "name": "punctuation.whitespace.comment.leading.yaml" 165 | } 166 | }, 167 | "end": "(?!\\G)", 168 | "patterns": [ 169 | { 170 | "begin": "#", 171 | "beginCaptures": { 172 | "0": { 173 | "name": "punctuation.definition.comment.yaml" 174 | } 175 | }, 176 | "end": "\\n", 177 | "name": "comment.line.number-sign.yaml" 178 | } 179 | ] 180 | }, 181 | "directive": { 182 | "begin": "^%", 183 | "beginCaptures": { 184 | "0": { 185 | "name": "punctuation.definition.directive.begin.yaml" 186 | } 187 | }, 188 | "end": "(?=$|[ \\t]+($|#))", 189 | "name": "meta.directive.yaml", 190 | "patterns": [ 191 | { 192 | "captures": { 193 | "1": { 194 | "name": "keyword.other.directive.yaml.yaml" 195 | }, 196 | "2": { 197 | "name": "constant.numeric.yaml-version.yaml" 198 | } 199 | }, 200 | "match": "\\G(YAML)[ \\t]+(\\d+\\.\\d+)" 201 | }, 202 | { 203 | "captures": { 204 | "1": { 205 | "name": "keyword.other.directive.tag.yaml" 206 | }, 207 | "2": { 208 | "name": "storage.type.tag-handle.yaml" 209 | }, 210 | "3": { 211 | "name": "support.type.tag-prefix.yaml" 212 | } 213 | }, 214 | "match": "(?x)\n \\G\n (TAG)\n (?:[ \\t]+\n ((?:!(?:[0-9A-Za-z\\-]*!)?))\n (?:[ \\t]+ (\n ! (?x: %[0-9A-Fa-f]{2} | [0-9A-Za-z\\-#;/?:@&=+$,_.!~*'()\\[\\]] )*\n | (?![,!\\[\\]{}]) (?x: %[0-9A-Fa-f]{2} | [0-9A-Za-z\\-#;/?:@&=+$,_.!~*'()\\[\\]] )+\n )\n )?\n )?\n " 215 | }, 216 | { 217 | "captures": { 218 | "1": { 219 | "name": "support.other.directive.reserved.yaml" 220 | }, 221 | "2": { 222 | "name": "string.unquoted.directive-name.yaml" 223 | }, 224 | "3": { 225 | "name": "string.unquoted.directive-parameter.yaml" 226 | } 227 | }, 228 | "match": "(?x) \\G (\\w+) (?:[ \\t]+ (\\w+) (?:[ \\t]+ (\\w+))? )?" 229 | }, 230 | { 231 | "match": "\\S+", 232 | "name": "invalid.illegal.unrecognized.yaml" 233 | } 234 | ] 235 | }, 236 | "flow-alias": { 237 | "captures": { 238 | "1": { 239 | "name": "keyword.control.flow.alias.yaml" 240 | }, 241 | "2": { 242 | "name": "punctuation.definition.alias.yaml" 243 | }, 244 | "3": { 245 | "name": "variable.other.alias.yaml" 246 | }, 247 | "4": { 248 | "name": "invalid.illegal.character.anchor.yaml" 249 | } 250 | }, 251 | "match": "((\\*))([^\\s\\[\\]\/{\/},]+)([^\\s\\]},]\\S*)?" 252 | }, 253 | "flow-collection": { 254 | "patterns": [ 255 | { 256 | "include": "#flow-sequence" 257 | }, 258 | { 259 | "include": "#flow-mapping" 260 | } 261 | ] 262 | }, 263 | "flow-mapping": { 264 | "begin": "\\{", 265 | "beginCaptures": { 266 | "0": { 267 | "name": "punctuation.definition.mapping.begin.yaml" 268 | } 269 | }, 270 | "end": "\\}", 271 | "endCaptures": { 272 | "0": { 273 | "name": "punctuation.definition.mapping.end.yaml" 274 | } 275 | }, 276 | "name": "meta.flow-mapping.yaml", 277 | "patterns": [ 278 | { 279 | "include": "#prototype" 280 | }, 281 | { 282 | "match": ",", 283 | "name": "punctuation.separator.mapping.yaml" 284 | }, 285 | { 286 | "include": "#flow-pair" 287 | } 288 | ] 289 | }, 290 | "flow-node": { 291 | "patterns": [ 292 | { 293 | "include": "#prototype" 294 | }, 295 | { 296 | "include": "#flow-alias" 297 | }, 298 | { 299 | "include": "#flow-collection" 300 | }, 301 | { 302 | "include": "#flow-scalar" 303 | } 304 | ] 305 | }, 306 | "flow-pair": { 307 | "patterns": [ 308 | { 309 | "begin": "\\?", 310 | "beginCaptures": { 311 | "0": { 312 | "name": "punctuation.definition.key-value.begin.yaml" 313 | } 314 | }, 315 | "end": "(?=[},\\]])", 316 | "name": "meta.flow-pair.explicit.yaml", 317 | "patterns": [ 318 | { 319 | "include": "#prototype" 320 | }, 321 | { 322 | "include": "#flow-pair" 323 | }, 324 | { 325 | "include": "#flow-node" 326 | }, 327 | { 328 | "begin": ":(?=\\s|$|[\\[\\]{},])", 329 | "beginCaptures": { 330 | "0": { 331 | "name": "punctuation.separator.key-value.mapping.yaml" 332 | } 333 | }, 334 | "end": "(?=[},\\]])", 335 | "patterns": [ 336 | { 337 | "include": "#flow-value" 338 | } 339 | ] 340 | } 341 | ] 342 | }, 343 | { 344 | "begin": "(?x)\n (?=\n (?:\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] [^\\s[\\[\\]{},]]\n )\n (\n [^\\s:[\\[\\]{},]]\n | : [^\\s[\\[\\]{},]]\n | \\s+ (?![#\\s])\n )*\n \\s*\n :\n\t\t\t\t\t\t\t(\\s|$)\n )\n ", 345 | "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n | \\s* : [\\[\\]{},]\n | \\s* [\\[\\]{},]\n )\n ", 346 | "name": "meta.flow-pair.key.yaml", 347 | "patterns": [ 348 | { 349 | "include": "#flow-scalar-plain-in-implicit-type" 350 | }, 351 | { 352 | "begin": "(?x)\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] [^\\s[\\[\\]{},]]\n ", 353 | "beginCaptures": { 354 | "0": { 355 | "name": "entity.name.tag.yaml" 356 | } 357 | }, 358 | "contentName": "entity.name.tag.yaml", 359 | "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n | \\s* : [\\[\\]{},]\n | \\s* [\\[\\]{},]\n )\n ", 360 | "name": "string.unquoted.plain.in.yaml" 361 | } 362 | ] 363 | }, 364 | { 365 | "include": "#flow-node" 366 | }, 367 | { 368 | "begin": ":(?=\\s|$|[\\[\\]{},])", 369 | "captures": { 370 | "0": { 371 | "name": "punctuation.separator.key-value.mapping.yaml" 372 | } 373 | }, 374 | "end": "(?=[},\\]])", 375 | "name": "meta.flow-pair.yaml", 376 | "patterns": [ 377 | { 378 | "include": "#flow-value" 379 | } 380 | ] 381 | } 382 | ] 383 | }, 384 | "flow-scalar": { 385 | "patterns": [ 386 | { 387 | "include": "#flow-scalar-double-quoted" 388 | }, 389 | { 390 | "include": "#flow-scalar-single-quoted" 391 | }, 392 | { 393 | "include": "#flow-scalar-plain-in" 394 | } 395 | ] 396 | }, 397 | "flow-scalar-double-quoted": { 398 | "begin": "\"", 399 | "beginCaptures": { 400 | "0": { 401 | "name": "punctuation.definition.string.begin.yaml" 402 | } 403 | }, 404 | "end": "\"", 405 | "endCaptures": { 406 | "0": { 407 | "name": "punctuation.definition.string.end.yaml" 408 | } 409 | }, 410 | "name": "string.quoted.double.yaml", 411 | "patterns": [ 412 | { 413 | "match": "\\\\([0abtnvfre \"/\\\\N_Lp]|x\\d\\d|u\\d{4}|U\\d{8})", 414 | "name": "constant.character.escape.yaml" 415 | }, 416 | { 417 | "match": "\\\\\\n", 418 | "name": "constant.character.escape.double-quoted.newline.yaml" 419 | } 420 | ] 421 | }, 422 | "flow-scalar-plain-in": { 423 | "patterns": [ 424 | { 425 | "include": "#flow-scalar-plain-in-implicit-type" 426 | }, 427 | { 428 | "begin": "(?x)\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] [^\\s[\\[\\]{},]]\n ", 429 | "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n | \\s* : [\\[\\]{},]\n | \\s* [\\[\\]{},]\n )\n ", 430 | "name": "string.unquoted.plain.in.yaml" 431 | } 432 | ] 433 | }, 434 | "flow-scalar-plain-in-implicit-type": { 435 | "patterns": [ 436 | { 437 | "captures": { 438 | "1": { 439 | "name": "constant.language.null.yaml" 440 | }, 441 | "2": { 442 | "name": "constant.language.boolean.yaml" 443 | }, 444 | "3": { 445 | "name": "constant.numeric.integer.yaml" 446 | }, 447 | "4": { 448 | "name": "constant.numeric.float.yaml" 449 | }, 450 | "5": { 451 | "name": "constant.other.timestamp.yaml" 452 | }, 453 | "6": { 454 | "name": "constant.language.value.yaml" 455 | }, 456 | "7": { 457 | "name": "constant.language.merge.yaml" 458 | } 459 | }, 460 | "match": "(?x)\n (?x:\n (null|Null|NULL|~)\n | (y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)\n | (\n (?:\n [-+]? 0b [0-1_]+ # (base 2)\n | [-+]? 0 [0-7_]+ # (base 8)\n | [-+]? (?: 0|[1-9][0-9_]*) # (base 10)\n | [-+]? 0x [0-9a-fA-F_]+ # (base 16)\n | [-+]? [1-9] [0-9_]* (?: :[0-5]?[0-9])+ # (base 60)\n )\n )\n | (\n (?x:\n [-+]? (?: [0-9] [0-9_]*)? \\. [0-9.]* (?: [eE] [-+] [0-9]+)? # (base 10)\n | [-+]? [0-9] [0-9_]* (?: :[0-5]?[0-9])+ \\. [0-9_]* # (base 60)\n | [-+]? \\. (?: inf|Inf|INF) # (infinity)\n | \\. (?: nan|NaN|NAN) # (not a number)\n )\n )\n | (\n (?x:\n \\d{4} - \\d{2} - \\d{2} # (y-m-d)\n | \\d{4} # (year)\n - \\d{1,2} # (month)\n - \\d{1,2} # (day)\n (?: [Tt] | [ \\t]+) \\d{1,2} # (hour)\n : \\d{2} # (minute)\n : \\d{2} # (second)\n (?: \\.\\d*)? # (fraction)\n (?:\n (?:[ \\t]*) Z\n | [-+] \\d{1,2} (?: :\\d{1,2})?\n )? # (time zone)\n )\n )\n | (=)\n | (<<)\n )\n (?:\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n | \\s* : [\\[\\]{},]\n | \\s* [\\[\\]{},]\n )\n )\n " 461 | } 462 | ] 463 | }, 464 | "flow-scalar-plain-out": { 465 | "patterns": [ 466 | { 467 | "include": "#flow-scalar-plain-out-implicit-type" 468 | }, 469 | { 470 | "begin": "(?x)\n [^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] \\S\n ", 471 | "end": "(?x)\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n )\n ", 472 | "name": "string.unquoted.plain.out.yaml" 473 | } 474 | ] 475 | }, 476 | "flow-scalar-plain-out-implicit-type": { 477 | "patterns": [ 478 | { 479 | "captures": { 480 | "1": { 481 | "name": "constant.language.null.yaml" 482 | }, 483 | "2": { 484 | "name": "constant.language.boolean.yaml" 485 | }, 486 | "3": { 487 | "name": "constant.numeric.integer.yaml" 488 | }, 489 | "4": { 490 | "name": "constant.numeric.float.yaml" 491 | }, 492 | "5": { 493 | "name": "constant.other.timestamp.yaml" 494 | }, 495 | "6": { 496 | "name": "constant.language.value.yaml" 497 | }, 498 | "7": { 499 | "name": "constant.language.merge.yaml" 500 | } 501 | }, 502 | "match": "(?x)\n (?x:\n (null|Null|NULL|~)\n | (y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)\n | (\n (?:\n [-+]? 0b [0-1_]+ # (base 2)\n | [-+]? 0 [0-7_]+ # (base 8)\n | [-+]? (?: 0|[1-9][0-9_]*) # (base 10)\n | [-+]? 0x [0-9a-fA-F_]+ # (base 16)\n | [-+]? [1-9] [0-9_]* (?: :[0-5]?[0-9])+ # (base 60)\n )\n )\n | (\n (?x:\n [-+]? (?: [0-9] [0-9_]*)? \\. [0-9.]* (?: [eE] [-+] [0-9]+)? # (base 10)\n | [-+]? [0-9] [0-9_]* (?: :[0-5]?[0-9])+ \\. [0-9_]* # (base 60)\n | [-+]? \\. (?: inf|Inf|INF) # (infinity)\n | \\. (?: nan|NaN|NAN) # (not a number)\n )\n )\n | (\n (?x:\n \\d{4} - \\d{2} - \\d{2} # (y-m-d)\n | \\d{4} # (year)\n - \\d{1,2} # (month)\n - \\d{1,2} # (day)\n (?: [Tt] | [ \\t]+) \\d{1,2} # (hour)\n : \\d{2} # (minute)\n : \\d{2} # (second)\n (?: \\.\\d*)? # (fraction)\n (?:\n (?:[ \\t]*) Z\n | [-+] \\d{1,2} (?: :\\d{1,2})?\n )? # (time zone)\n )\n )\n | (=)\n | (<<)\n )\n (?x:\n (?=\n \\s* $\n | \\s+ \\#\n | \\s* : (\\s|$)\n )\n )\n " 503 | } 504 | ] 505 | }, 506 | "flow-scalar-single-quoted": { 507 | "begin": "'", 508 | "beginCaptures": { 509 | "0": { 510 | "name": "punctuation.definition.string.begin.yaml" 511 | } 512 | }, 513 | "end": "'(?!')", 514 | "endCaptures": { 515 | "0": { 516 | "name": "punctuation.definition.string.end.yaml" 517 | } 518 | }, 519 | "name": "string.quoted.single.yaml", 520 | "patterns": [ 521 | { 522 | "match": "''", 523 | "name": "constant.character.escape.single-quoted.yaml" 524 | } 525 | ] 526 | }, 527 | "flow-sequence": { 528 | "begin": "\\[", 529 | "beginCaptures": { 530 | "0": { 531 | "name": "punctuation.definition.sequence.begin.yaml" 532 | } 533 | }, 534 | "end": "\\]", 535 | "endCaptures": { 536 | "0": { 537 | "name": "punctuation.definition.sequence.end.yaml" 538 | } 539 | }, 540 | "name": "meta.flow-sequence.yaml", 541 | "patterns": [ 542 | { 543 | "include": "#prototype" 544 | }, 545 | { 546 | "match": ",", 547 | "name": "punctuation.separator.sequence.yaml" 548 | }, 549 | { 550 | "include": "#flow-pair" 551 | }, 552 | { 553 | "include": "#flow-node" 554 | } 555 | ] 556 | }, 557 | "flow-value": { 558 | "patterns": [ 559 | { 560 | "begin": "\\G(?![},\\]])", 561 | "end": "(?=[},\\]])", 562 | "name": "meta.flow-pair.value.yaml", 563 | "patterns": [ 564 | { 565 | "include": "#flow-node" 566 | } 567 | ] 568 | } 569 | ] 570 | }, 571 | "node": { 572 | "patterns": [ 573 | { 574 | "include": "#block-node" 575 | } 576 | ] 577 | }, 578 | "property": { 579 | "begin": "(?=!|&)", 580 | "end": "(?!\\G)", 581 | "name": "meta.property.yaml", 582 | "patterns": [ 583 | { 584 | "captures": { 585 | "1": { 586 | "name": "keyword.control.property.anchor.yaml" 587 | }, 588 | "2": { 589 | "name": "punctuation.definition.anchor.yaml" 590 | }, 591 | "3": { 592 | "name": "entity.name.type.anchor.yaml" 593 | }, 594 | "4": { 595 | "name": "invalid.illegal.character.anchor.yaml" 596 | } 597 | }, 598 | "match": "\\G((&))([^\\s\\[\\]/{/},]+)(\\S+)?" 599 | }, 600 | { 601 | "match": "(?x)\n \\G\n (?:\n ! < (?: %[0-9A-Fa-f]{2} | [0-9A-Za-z\\-#;/?:@&=+$,_.!~*'()\\[\\]] )+ >\n | (?:!(?:[0-9A-Za-z\\-]*!)?) (?: %[0-9A-Fa-f]{2} | [0-9A-Za-z\\-#;/?:@&=+$_.~*'()] )+\n | !\n )\n (?=\\ |\\t|$)\n ", 602 | "name": "storage.type.tag-handle.yaml" 603 | }, 604 | { 605 | "match": "\\S+", 606 | "name": "invalid.illegal.tag-handle.yaml" 607 | } 608 | ] 609 | }, 610 | "prototype": { 611 | "patterns": [ 612 | { 613 | "include": "#comment" 614 | }, 615 | { 616 | "include": "#property" 617 | } 618 | ] 619 | } 620 | } 621 | } 622 | -------------------------------------------------------------------------------- /test/completion.test.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import * as vscode from 'vscode'; 7 | import { getDocUri, activate, testCompletion, updateSettings, testCompletionNotEmpty, resetSettings } from './helper'; 8 | import * as path from 'path'; 9 | 10 | describe('Completion should work in multiple different scenarios', () => { 11 | const docUri = getDocUri(path.join('completion', 'completion.yaml')); 12 | const travisUri = getDocUri(path.join('completion', '.travis.yml')); 13 | 14 | afterEach(async () => { 15 | await resetSettings('schemas', {}); 16 | await resetSettings('schemaStore.enable', true); 17 | }); 18 | 19 | it('completion works with local schema', async () => { 20 | await activate(docUri); 21 | const schemaPath = path.join(__dirname, '..', '..', 'test', 'testFixture', 'schemas', 'basic_completion_schema.json'); 22 | await updateSettings('schemas', { 23 | [vscode.Uri.file(schemaPath).toString()]: 'completion.yaml', 24 | }); 25 | await testCompletion(docUri, new vscode.Position(0, 0), { 26 | items: [ 27 | { 28 | label: 'my_key', 29 | kind: 9, 30 | }, 31 | ], 32 | }); 33 | }); 34 | 35 | it('completion works with external schema', async () => { 36 | await activate(docUri); 37 | await updateSettings('schemas', { 38 | 'https://gist.githubusercontent.com/JPinkney/4c4a43977932402c2a09a677f29287c3/raw/4d4f638b37ddeda84fb27e6b2cf14d3dc0793029/a.yaml': 39 | 'completion.yaml', 40 | }); 41 | await testCompletion(docUri, new vscode.Position(0, 0), { 42 | items: [ 43 | { 44 | label: 'version', 45 | kind: 9, 46 | }, 47 | ], 48 | }); 49 | }); 50 | 51 | it('completion works with schema store schema', async () => { 52 | await activate(travisUri); 53 | await updateSettings('schemaStore.enable', true); 54 | await testCompletionNotEmpty(travisUri, new vscode.Position(0, 0)); 55 | }); 56 | 57 | it('completion does not work with schema store disabled and no schemas set', async () => { 58 | await activate(travisUri); 59 | await updateSettings('schemaStore.enable', false); 60 | await testCompletion(travisUri, new vscode.Position(0, 0), { 61 | items: [ 62 | { 63 | label: 'Inline schema', 64 | kind: 0, 65 | }, 66 | ], 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/helper.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Copyright (c) Microsoft Corporation. All rights reserved. 4 | * Licensed under the MIT License. See License.txt in the project root for license information. 5 | * ------------------------------------------------------------------------------------------ */ 6 | 7 | import * as vscode from 'vscode'; 8 | import * as path from 'path'; 9 | import assert = require('assert'); 10 | import { CommonLanguageClient } from 'vscode-languageclient/lib/common/commonClient'; 11 | import { MessageTransports, ProtocolRequestType, ProtocolRequestType0, RequestType, RequestType0 } from 'vscode-languageclient'; 12 | 13 | export let doc: vscode.TextDocument; 14 | export let editor: vscode.TextEditor; 15 | export let documentEol: string; 16 | export let platformEol: string; 17 | 18 | /** 19 | * Activates the redhat.vscode-yaml extension 20 | */ 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | export async function activate(docUri: vscode.Uri): Promise { 23 | const ext = vscode.extensions.getExtension('redhat.vscode-yaml'); 24 | const activation = await ext.activate(); 25 | try { 26 | doc = await vscode.workspace.openTextDocument(docUri); 27 | editor = await vscode.window.showTextDocument(doc); 28 | 29 | await sleep(2000); // Wait for server activation 30 | return activation; 31 | } catch (e) { 32 | console.error(e); 33 | } 34 | } 35 | 36 | export async function sleep(ms: number): Promise { 37 | return new Promise((resolve) => setTimeout(resolve, ms)); 38 | } 39 | 40 | export const getDocPath = (p: string): string => { 41 | return path.resolve(__dirname, path.join('..', '..', 'test', 'testFixture', p)); 42 | }; 43 | 44 | export const getDocUri = (p: string): vscode.Uri => { 45 | return vscode.Uri.file(getDocPath(p)); 46 | }; 47 | 48 | export const updateSettings = (setting: string, value: unknown): Thenable => { 49 | const yamlConfiguration = vscode.workspace.getConfiguration('yaml', null); 50 | return yamlConfiguration.update(setting, value, false); 51 | }; 52 | 53 | export const resetSettings = (setting: string, value: unknown): Thenable => { 54 | const yamlConfiguration = vscode.workspace.getConfiguration('yaml', null); 55 | return yamlConfiguration.update(setting, value, false); 56 | }; 57 | 58 | export async function setTestContent(content: string): Promise { 59 | const all = new vscode.Range(doc.positionAt(0), doc.positionAt(doc.getText().length)); 60 | return editor.edit((eb) => eb.replace(all, content)); 61 | } 62 | 63 | export async function testCompletion( 64 | docUri: vscode.Uri, 65 | position: vscode.Position, 66 | expectedCompletionList: vscode.CompletionList 67 | ): Promise { 68 | // Executing the command `vscode.executeCompletionItemProvider` to simulate triggering completion 69 | const actualCompletionList = (await vscode.commands.executeCommand( 70 | 'vscode.executeCompletionItemProvider', 71 | docUri, 72 | position 73 | )) as vscode.CompletionList; 74 | 75 | const sortedActualCompletionList = actualCompletionList.items.sort((a, b) => (a.label > b.label ? 1 : -1)); 76 | assert.equal( 77 | actualCompletionList.items.length, 78 | expectedCompletionList.items.length, 79 | "Completion List doesn't have expected size" 80 | ); 81 | expectedCompletionList.items 82 | .sort((a, b) => (a.label > b.label ? 1 : -1)) 83 | .forEach((expectedItem, i) => { 84 | const actualItem = sortedActualCompletionList[i]; 85 | assert.equal(actualItem.label, expectedItem.label); 86 | assert.equal(actualItem.kind, expectedItem.kind); 87 | }); 88 | } 89 | 90 | export async function testCompletionNotEmpty(docUri: vscode.Uri, position: vscode.Position): Promise { 91 | // Executing the command `vscode.executeCompletionItemProvider` to simulate triggering completion 92 | const actualCompletionList = (await vscode.commands.executeCommand( 93 | 'vscode.executeCompletionItemProvider', 94 | docUri, 95 | position 96 | )) as vscode.CompletionList; 97 | 98 | assert.notEqual(actualCompletionList.items.length, 0); 99 | } 100 | 101 | export async function testHover(docUri: vscode.Uri, position: vscode.Position, expectedHover: vscode.Hover[]): Promise { 102 | // Executing the command `vscode.executeCompletionItemProvider` to simulate triggering completion 103 | const actualHoverResults = (await vscode.commands.executeCommand( 104 | 'vscode.executeHoverProvider', 105 | docUri, 106 | position 107 | )) as vscode.Hover[]; 108 | 109 | assert.equal(actualHoverResults.length, expectedHover.length); 110 | expectedHover.forEach((expectedItem, i) => { 111 | const actualItem = actualHoverResults[i]; 112 | assert.equal((actualItem.contents[i] as vscode.MarkdownString).value, expectedItem.contents[i]); 113 | }); 114 | } 115 | 116 | export async function testDiagnostics(docUri: vscode.Uri, expectedDiagnostics: vscode.Diagnostic[]): Promise { 117 | const actualDiagnostics = vscode.languages.getDiagnostics(docUri); 118 | 119 | assert.equal(actualDiagnostics.length, expectedDiagnostics.length); 120 | 121 | expectedDiagnostics.forEach((expectedDiagnostic, i) => { 122 | const actualDiagnostic = actualDiagnostics[i]; 123 | assert.equal(actualDiagnostic.message, expectedDiagnostic.message); 124 | assert.deepEqual(actualDiagnostic.range, expectedDiagnostic.range); 125 | assert.equal(actualDiagnostic.severity, expectedDiagnostic.severity); 126 | }); 127 | } 128 | 129 | export class TestMemento implements vscode.Memento { 130 | keys(): readonly string[] { 131 | throw new Error('Method not implemented.'); 132 | } 133 | get(key: string): T; 134 | get(key: string, defaultValue: T): T; 135 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 136 | get(key: string, defaultValue?: T): T | undefined { 137 | throw new Error('Method not implemented.'); 138 | } 139 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 140 | update(key: string, value: unknown): Thenable { 141 | throw new Error('Method not implemented.'); 142 | } 143 | } 144 | 145 | export class TestLanguageClient extends CommonLanguageClient { 146 | constructor() { 147 | super('test', 'test', {}); 148 | } 149 | protected getLocale(): string { 150 | throw new Error('Method not implemented.'); 151 | } 152 | protected createMessageTransports(): Promise { 153 | throw new Error('Method not implemented.'); 154 | } 155 | 156 | sendRequest(type: ProtocolRequestType0, token?: vscode.CancellationToken): Promise; 157 | sendRequest( 158 | type: ProtocolRequestType, 159 | params: P, 160 | token?: vscode.CancellationToken 161 | ): Promise; 162 | sendRequest(type: RequestType0, token?: vscode.CancellationToken): Promise; 163 | sendRequest(type: RequestType, params: P, token?: vscode.CancellationToken): Promise; 164 | sendRequest(method: string, token?: vscode.CancellationToken): Promise; 165 | /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-unused-vars*/ 166 | sendRequest(method: string, param: any, token?: vscode.CancellationToken): Promise; 167 | sendRequest( 168 | method: any, 169 | param?: any, 170 | token?: any 171 | ): Promise | Promise | Promise | Promise | Promise | Promise { 172 | return Promise.resolve(void 0); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import * as path from 'path'; 7 | import * as Mocha from 'mocha'; 8 | import * as glob from 'glob'; 9 | 10 | export function run(): Promise { 11 | // Create the mocha test 12 | const mocha = new Mocha({ 13 | ui: 'bdd', 14 | timeout: 10000, 15 | }); 16 | 17 | const testsRoot = path.resolve(__dirname, '..'); 18 | 19 | return new Promise((c, e) => { 20 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 21 | if (err) { 22 | return e(err); 23 | } 24 | 25 | // Add files to the test suite 26 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 27 | 28 | try { 29 | // Run the mocha test 30 | mocha.run((failures) => { 31 | if (failures > 0) { 32 | e(new Error(`${failures} tests failed.`)); 33 | } else { 34 | c(); 35 | } 36 | }); 37 | } catch (err) { 38 | console.error(err); 39 | e(err); 40 | } 41 | }); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /test/json-schema-cache.test.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as sinon from 'sinon'; 7 | import * as sinonChai from 'sinon-chai'; 8 | import * as chai from 'chai'; 9 | import * as vscode from 'vscode'; 10 | import * as fs from 'fs-extra'; 11 | import { JSONSchemaCache } from '../src/json-schema-cache'; 12 | import { TestMemento } from './helper'; 13 | 14 | const expect = chai.expect; 15 | chai.use(sinonChai); 16 | describe('JSON Schema Cache Tests', () => { 17 | const sandbox = sinon.createSandbox(); 18 | let memento: sinon.SinonStubbedInstance; 19 | let ensureDirStub: sinon.SinonStub; 20 | let readdirStub: sinon.SinonStub; 21 | let pathExistsStub: sinon.SinonStub; 22 | let readFileStub: sinon.SinonStub; 23 | 24 | afterEach(() => { 25 | sandbox.restore(); 26 | }); 27 | 28 | beforeEach(() => { 29 | memento = sandbox.stub(new TestMemento()); 30 | ensureDirStub = sandbox.stub(fs, 'ensureDir'); 31 | readdirStub = sandbox.stub(fs, 'readdir'); 32 | pathExistsStub = sandbox.stub(fs, 'pathExists'); 33 | readFileStub = sandbox.stub(fs, 'readFile'); 34 | }); 35 | 36 | it('should clean up cache if there are no schema file', async () => { 37 | memento.get.returns({ somePath: { schemaPath: '/foo/path/' } }); 38 | memento.update.resolves(); 39 | 40 | ensureDirStub.resolves(); 41 | readdirStub.resolves([]); 42 | 43 | pathExistsStub.resolves(false); 44 | readFileStub.resolves(); 45 | 46 | const cache = new JSONSchemaCache('/some/path/', (memento as unknown) as vscode.Memento); 47 | const result = await cache.getSchema('/some/uri'); 48 | expect(result).is.undefined; 49 | expect(memento.update).calledOnceWith('json-schema-key', {}); 50 | }); 51 | 52 | it('should check cache', async () => { 53 | const mementoData = { somePath: { schemaPath: '/some/path/foo.json' } }; 54 | memento.get.returns(mementoData); 55 | memento.update.resolves(); 56 | 57 | ensureDirStub.resolves(); 58 | readdirStub.resolves(['foo.json']); 59 | 60 | pathExistsStub.resolves(false); 61 | readFileStub.resolves(); 62 | 63 | const cache = new JSONSchemaCache('/some/path/', (memento as unknown) as vscode.Memento); 64 | const result = await cache.getSchema('/some/uri'); 65 | expect(result).is.undefined; 66 | expect(memento.update).calledOnceWith('json-schema-key', mementoData); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/json-schema-selection.test.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as sinon from 'sinon'; 7 | import * as sinonChai from 'sinon-chai'; 8 | import * as chai from 'chai'; 9 | import { createJSONSchemaStatusBarItem } from '../src/schema-status-bar-item'; 10 | import { CommonLanguageClient } from 'vscode-languageclient'; 11 | import * as vscode from 'vscode'; 12 | import { TestLanguageClient } from './helper'; 13 | import * as jsonStatusBar from '../src/schema-status-bar-item'; 14 | 15 | const expect = chai.expect; 16 | chai.use(sinonChai); 17 | 18 | describe('Status bar should work in multiple different scenarios', () => { 19 | const sandbox = sinon.createSandbox(); 20 | let clcStub: sinon.SinonStubbedInstance; 21 | let registerCommandStub: sinon.SinonStub; 22 | let createStatusBarItemStub: sinon.SinonStub; 23 | let onDidChangeActiveTextEditorStub: sinon.SinonStub; 24 | 25 | beforeEach(() => { 26 | clcStub = sandbox.stub(new TestLanguageClient()); 27 | registerCommandStub = sandbox.stub(vscode.commands, 'registerCommand'); 28 | createStatusBarItemStub = sandbox.stub(vscode.window, 'createStatusBarItem'); 29 | onDidChangeActiveTextEditorStub = sandbox.stub(vscode.window, 'onDidChangeActiveTextEditor'); 30 | sandbox.stub(vscode.window, 'activeTextEditor').returns(undefined); 31 | sandbox.stub(jsonStatusBar, 'statusBarItem').returns(undefined); 32 | }); 33 | 34 | afterEach(() => { 35 | sandbox.restore(); 36 | }); 37 | 38 | it('Should create status bar item for JSON Schema', async () => { 39 | const context: vscode.ExtensionContext = { 40 | subscriptions: [], 41 | } as vscode.ExtensionContext; 42 | const statusBar = ({ show: sandbox.stub() } as unknown) as vscode.StatusBarItem; 43 | createStatusBarItemStub.returns(statusBar); 44 | clcStub.sendRequest.resolves([]); 45 | 46 | createJSONSchemaStatusBarItem(context, (clcStub as unknown) as CommonLanguageClient); 47 | 48 | expect(registerCommandStub).calledOnceWith('yaml.select.json.schema'); 49 | expect(createStatusBarItemStub).calledOnceWith(vscode.StatusBarAlignment.Right); 50 | expect(context.subscriptions).has.length(2); 51 | }); 52 | 53 | it('Should update status bar on editor change', async () => { 54 | const context: vscode.ExtensionContext = { 55 | subscriptions: [], 56 | } as vscode.ExtensionContext; 57 | const statusBar = ({ show: sandbox.stub() } as unknown) as vscode.StatusBarItem; 58 | createStatusBarItemStub.returns(statusBar); 59 | onDidChangeActiveTextEditorStub.returns({}); 60 | clcStub.sendRequest.resolves([{ uri: 'https://foo.com/bar.json', name: 'bar schema' }]); 61 | 62 | createJSONSchemaStatusBarItem(context, (clcStub as unknown) as CommonLanguageClient); 63 | const callBackFn = onDidChangeActiveTextEditorStub.firstCall.firstArg; 64 | await callBackFn({ document: { languageId: 'yaml', uri: vscode.Uri.parse('/foo.yaml') } }); 65 | 66 | expect(statusBar.text).to.equal('bar schema'); 67 | expect(statusBar.tooltip).to.equal('Select JSON Schema'); 68 | expect(statusBar.backgroundColor).to.be.undefined; 69 | expect(statusBar.show).calledTwice; 70 | }); 71 | 72 | it('Should inform if there are no schema', async () => { 73 | const context: vscode.ExtensionContext = { 74 | subscriptions: [], 75 | } as vscode.ExtensionContext; 76 | const statusBar = ({ show: sandbox.stub() } as unknown) as vscode.StatusBarItem; 77 | createStatusBarItemStub.returns(statusBar); 78 | onDidChangeActiveTextEditorStub.returns({}); 79 | clcStub.sendRequest.resolves([]); 80 | 81 | createJSONSchemaStatusBarItem(context, (clcStub as unknown) as CommonLanguageClient); 82 | const callBackFn = onDidChangeActiveTextEditorStub.firstCall.firstArg; 83 | await callBackFn({ document: { languageId: 'yaml', uri: vscode.Uri.parse('/foo.yaml') } }); 84 | 85 | expect(statusBar.text).to.equal('No JSON Schema'); 86 | expect(statusBar.tooltip).to.equal('Select JSON Schema'); 87 | expect(statusBar.backgroundColor).to.be.undefined; 88 | expect(statusBar.show).calledTwice; 89 | }); 90 | 91 | it('Should inform if there are more than one schema', async () => { 92 | const context: vscode.ExtensionContext = { 93 | subscriptions: [], 94 | } as vscode.ExtensionContext; 95 | const statusBar = ({ show: sandbox.stub() } as unknown) as vscode.StatusBarItem; 96 | createStatusBarItemStub.returns(statusBar); 97 | onDidChangeActiveTextEditorStub.returns({}); 98 | clcStub.sendRequest.resolves([{}, {}]); 99 | 100 | createJSONSchemaStatusBarItem(context, (clcStub as unknown) as CommonLanguageClient); 101 | const callBackFn = onDidChangeActiveTextEditorStub.firstCall.firstArg; 102 | await callBackFn({ document: { languageId: 'yaml', uri: vscode.Uri.parse('/foo.yaml') } }); 103 | 104 | expect(statusBar.text).to.equal('Multiple JSON Schemas...'); 105 | expect(statusBar.tooltip).to.equal('Multiple JSON Schema used to validate this file, click to select one'); 106 | expect(statusBar.backgroundColor).to.eql({ id: 'statusBarItem.warningBackground' }); 107 | expect(statusBar.show).calledTwice; 108 | }); 109 | 110 | it('Should show JSON Schema Store schema version', async () => { 111 | const context: vscode.ExtensionContext = { 112 | subscriptions: [], 113 | } as vscode.ExtensionContext; 114 | const statusBar = ({ show: sandbox.stub() } as unknown) as vscode.StatusBarItem; 115 | createStatusBarItemStub.returns(statusBar); 116 | onDidChangeActiveTextEditorStub.returns({ document: { uri: vscode.Uri.parse('/foo.yaml') } }); 117 | clcStub.sendRequest 118 | .withArgs(sinon.match.has('method', 'yaml/get/jsonSchema'), sinon.match('/foo.yaml')) 119 | .resolves([{ uri: 'https://foo.com/bar.json', name: 'bar schema' }]); 120 | clcStub.sendRequest 121 | .withArgs(sinon.match.has('method', 'yaml/get/all/jsonSchemas'), sinon.match.any) 122 | .resolves([{ versions: { '1.0.0': 'https://foo.com/bar.json' } }]); 123 | 124 | createJSONSchemaStatusBarItem(context, (clcStub as unknown) as CommonLanguageClient); 125 | const callBackFn = onDidChangeActiveTextEditorStub.firstCall.firstArg; 126 | await callBackFn({ document: { languageId: 'yaml', uri: vscode.Uri.parse('/foo.yaml') } }); 127 | 128 | expect(statusBar.text).to.equal('bar schema(1.0.0)'); 129 | expect(statusBar.tooltip).to.equal('Select JSON Schema'); 130 | expect(statusBar.backgroundColor).to.be.undefined; 131 | expect(statusBar.show).calledTwice; 132 | }); 133 | 134 | it('Should show JSON Schema Store schema version, dont include version', async () => { 135 | const context: vscode.ExtensionContext = { 136 | subscriptions: [], 137 | } as vscode.ExtensionContext; 138 | const statusBar = ({ show: sandbox.stub() } as unknown) as vscode.StatusBarItem; 139 | createStatusBarItemStub.returns(statusBar); 140 | onDidChangeActiveTextEditorStub.returns({ document: { uri: vscode.Uri.parse('/foo.yaml') } }); 141 | clcStub.sendRequest 142 | .withArgs(sinon.match.has('method', 'yaml/get/jsonSchema'), sinon.match('/foo.yaml')) 143 | .resolves([{ uri: 'https://foo.com/bar.json', name: 'bar schema(1.0.0)' }]); 144 | clcStub.sendRequest 145 | .withArgs(sinon.match.has('method', 'yaml/get/all/jsonSchemas'), sinon.match.any) 146 | .resolves([{ versions: { '1.0.0': 'https://foo.com/bar.json' } }]); 147 | 148 | createJSONSchemaStatusBarItem(context, (clcStub as unknown) as CommonLanguageClient); 149 | const callBackFn = onDidChangeActiveTextEditorStub.firstCall.firstArg; 150 | await callBackFn({ document: { languageId: 'yaml', uri: vscode.Uri.parse('/foo.yaml') } }); 151 | 152 | expect(statusBar.text).to.equal('bar schema(1.0.0)'); 153 | expect(statusBar.tooltip).to.equal('Select JSON Schema'); 154 | expect(statusBar.backgroundColor).to.be.undefined; 155 | expect(statusBar.show).calledTwice; 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /test/json-shema-content-provider.test.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import * as vscode from 'vscode'; 6 | import { getDocUri, activate } from './helper'; 7 | import * as assert from 'assert'; 8 | 9 | describe('Tests for JSON Schema content provider', () => { 10 | const SCHEMA = 'myschema'; 11 | const schemaJSON = JSON.stringify({ 12 | type: 'object', 13 | properties: { 14 | version: { 15 | type: 'string', 16 | description: 'A stringy string string', 17 | enum: ['test'], 18 | }, 19 | }, 20 | }); 21 | 22 | function onRequestSchema1URI(resource: string): string | undefined { 23 | if (resource.endsWith('completion.yaml') || resource.endsWith('basic.yaml')) { 24 | return `${SCHEMA}://schema/porter`; 25 | } 26 | return undefined; 27 | } 28 | 29 | function onRequestSchema1Content(): string | undefined { 30 | return schemaJSON; 31 | } 32 | 33 | it('should handle "json-schema" url', async () => { 34 | const docUri = getDocUri('completion/completion.yaml'); 35 | const client = await activate(docUri); 36 | client._customSchemaContributors = {}; 37 | client.registerContributor(SCHEMA, onRequestSchema1URI, onRequestSchema1Content); 38 | const customUri = vscode.Uri.parse(`json-schema://some/url/schema.json#${SCHEMA}://some/path/schema.json`); 39 | const doc = await vscode.workspace.openTextDocument(customUri); 40 | const editor = await vscode.window.showTextDocument(doc); 41 | assert.strictEqual(editor.document.getText(), JSON.stringify(JSON.parse(schemaJSON), null, 2)); 42 | client._customSchemaContributors = {}; 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/schemaModification.test.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import * as vscode from 'vscode'; 7 | import { getDocUri, activate, testCompletion, updateSettings, resetSettings } from './helper'; 8 | import { ExtensionAPI, MODIFICATION_ACTIONS } from '../src/schema-extension-api'; 9 | 10 | describe('Schema sections can be modified in memory', () => { 11 | const completionUri = getDocUri('completion/enum_completion.yaml'); 12 | 13 | afterEach(async () => { 14 | await resetSettings('schemas', {}); 15 | await resetSettings('schemaStore.enable', true); 16 | }); 17 | 18 | it('Modified schema gets correct results', async () => { 19 | const extensionAPI: ExtensionAPI = await activate(completionUri); 20 | 21 | // Insert the original schema 22 | await updateSettings('schemas', { 23 | 'https://gist.githubusercontent.com/JPinkney/00b908178a64d3a6274e2c9523b39521/raw/53042c011c089b13ef0a42b4b037ea2431bbba8d/basic_completion_schema.json': 24 | 'enum_completion.yaml', 25 | }); 26 | 27 | // Test that the schema was correctly loaded into memory 28 | await testCompletion(completionUri, new vscode.Position(0, 10), { 29 | items: [ 30 | { 31 | label: 'my_value', 32 | kind: 11, 33 | }, 34 | ], 35 | }); 36 | 37 | // Modify the schema 38 | await extensionAPI.modifySchemaContent({ 39 | action: MODIFICATION_ACTIONS.add, 40 | path: 'properties/my_key', 41 | key: 'enum', 42 | content: ['my_apple', 'my_banana', 'my_carrot'], 43 | schema: 44 | 'https://gist.githubusercontent.com/JPinkney/00b908178a64d3a6274e2c9523b39521/raw/53042c011c089b13ef0a42b4b037ea2431bbba8d/basic_completion_schema.json', 45 | }); 46 | 47 | await testCompletion(completionUri, new vscode.Position(0, 9), { 48 | items: [ 49 | { 50 | label: 'my_apple', 51 | kind: 11, 52 | }, 53 | { 54 | label: 'my_banana', 55 | kind: 11, 56 | }, 57 | { 58 | label: 'my_carrot', 59 | kind: 11, 60 | }, 61 | ], 62 | }); 63 | }); 64 | 65 | it('Deleted schema gets correct results', async () => { 66 | const extensionAPI: ExtensionAPI = await activate(completionUri); 67 | 68 | // Insert the original schema 69 | await updateSettings('schemas', { 70 | 'https://gist.githubusercontent.com/JPinkney/ee1caa73523b8e0574b9e9b241e2991e/raw/9569ef35a76ce5165b3c1b35abe878c44e861b33/sample.json': 71 | 'enum_completion.yaml', 72 | }); 73 | 74 | // Test that the schema was correctly loaded into memory 75 | await testCompletion(completionUri, new vscode.Position(0, 10), { 76 | items: [ 77 | { 78 | label: 'my_test1', 79 | kind: 11, 80 | }, 81 | { 82 | label: 'my_test2', 83 | kind: 11, 84 | }, 85 | ], 86 | }); 87 | 88 | // Modify the schema 89 | await extensionAPI.modifySchemaContent({ 90 | action: MODIFICATION_ACTIONS.delete, 91 | path: 'properties/my_key', 92 | schema: 93 | 'https://gist.githubusercontent.com/JPinkney/ee1caa73523b8e0574b9e9b241e2991e/raw/9569ef35a76ce5165b3c1b35abe878c44e861b33/sample.json', 94 | key: 'enum', 95 | }); 96 | 97 | await testCompletion(completionUri, new vscode.Position(0, 9), { 98 | items: [ 99 | { 100 | label: 'my_test2', 101 | kind: 11, 102 | }, 103 | ], 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /test/schemaProvider.test.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import * as vscode from 'vscode'; 7 | import { getDocUri, activate, testCompletion, testHover, testDiagnostics, sleep } from './helper'; 8 | 9 | const SCHEMA = 'myschema'; 10 | const SCHEMA2 = 'myschema2'; 11 | const SCHEMA3 = 'myschema3'; 12 | 13 | describe('Tests for schema provider feature', () => { 14 | const docUri = getDocUri('completion/completion.yaml'); 15 | const hoverUri = getDocUri('hover/basic.yaml'); 16 | const schemaProviderUri = getDocUri('completion/schemaProvider.yaml'); 17 | 18 | it('completion, hover, and validation work with registered contributor schema', async () => { 19 | const client = await activate(docUri); 20 | client._customSchemaContributors = {}; 21 | client.registerContributor(SCHEMA, onRequestSchema1URI, onRequestSchema1Content); 22 | await testCompletion(docUri, new vscode.Position(0, 0), { 23 | items: [ 24 | { 25 | label: 'version', 26 | kind: 9, 27 | }, 28 | ], 29 | }); 30 | 31 | await vscode.window.showTextDocument(hoverUri); 32 | await testHover(hoverUri, new vscode.Position(0, 3), [ 33 | { 34 | contents: ['A stringy string string\n\nAllowed Values:\n\n* `test`\n'], 35 | }, 36 | ]); 37 | 38 | await sleep(2000); // Wait for the diagnostics to compute on this file 39 | await testDiagnostics(hoverUri, [ 40 | { 41 | message: 'Value is not accepted. Valid values: "test".', 42 | range: new vscode.Range(new vscode.Position(0, 9), new vscode.Position(0, 14)), 43 | severity: 0, 44 | }, 45 | ]); 46 | }); 47 | 48 | it('Validation occurs automatically with registered contributor schema', async () => { 49 | const client = await activate(hoverUri); 50 | client._customSchemaContributors = {}; 51 | client.registerContributor(SCHEMA, onRequestSchema1URI, onRequestSchema1Content); 52 | 53 | await sleep(2000); // Wait for the diagnostics to compute on this file 54 | await testDiagnostics(hoverUri, [ 55 | { 56 | message: 'Value is not accepted. Valid values: "test".', 57 | range: new vscode.Range(new vscode.Position(0, 9), new vscode.Position(0, 14)), 58 | severity: 0, 59 | }, 60 | ]); 61 | }); 62 | 63 | it('Multiple contributors can match one file', async () => { 64 | const client = await activate(docUri); 65 | client._customSchemaContributors = {}; 66 | client.registerContributor(SCHEMA2, onRequestSchema2URI, onRequestSchema2Content, 'apple: tastes_good'); 67 | client.registerContributor(SCHEMA3, onRequestSchema3URI, onRequestSchema3Content); 68 | 69 | await testCompletion(docUri, new vscode.Position(0, 0), { 70 | items: [ 71 | { 72 | label: 'apple', 73 | kind: 9, 74 | documentation: 'An apple', 75 | }, 76 | { 77 | label: 'version', 78 | kind: 9, 79 | documentation: 'A stringy string string', 80 | }, 81 | ], 82 | }); 83 | }); 84 | 85 | it('Multiple contributors with one label matches', async () => { 86 | const client = await activate(schemaProviderUri); 87 | client._customSchemaContributors = {}; 88 | client.registerContributor(SCHEMA2, onRequestSchema2URI, onRequestSchema2Content, 'apple: tastes_good'); 89 | client.registerContributor(SCHEMA3, onRequestSchema3URI, onRequestSchema3Content); 90 | 91 | await testCompletion(schemaProviderUri, new vscode.Position(0, 0), { 92 | items: [ 93 | { 94 | label: 'apple', 95 | kind: 9, 96 | documentation: 'An apple', 97 | }, 98 | ], 99 | }); 100 | }); 101 | 102 | it('Multiple contributors with labels but only one label matches', async () => { 103 | const client = await activate(schemaProviderUri); 104 | client._customSchemaContributors = {}; 105 | client.registerContributor(SCHEMA2, onRequestSchema2URI, onRequestSchema2Content, 'apple: tastes_good'); 106 | client.registerContributor(SCHEMA3, onRequestSchema3URI, onRequestSchema3Content, 'apple: bad'); 107 | 108 | await testCompletion(schemaProviderUri, new vscode.Position(0, 0), { 109 | items: [ 110 | { 111 | label: 'apple', 112 | kind: 9, 113 | documentation: 'An apple', 114 | }, 115 | ], 116 | }); 117 | }); 118 | 119 | it('Multiple contributors with labels but no label matches', async () => { 120 | const client = await activate(schemaProviderUri); 121 | client._customSchemaContributors = {}; 122 | client.registerContributor(SCHEMA2, onRequestSchema2URI, onRequestSchema2Content, 'apple: not_bad'); 123 | client.registerContributor(SCHEMA3, onRequestSchema3URI, onRequestSchema3Content, 'apple: bad'); 124 | 125 | await testCompletion(schemaProviderUri, new vscode.Position(0, 0), { 126 | items: [ 127 | { 128 | label: 'apple', 129 | kind: 9, 130 | documentation: 'An apple', 131 | }, 132 | { 133 | label: 'version', 134 | kind: 9, 135 | documentation: 'A stringy string string', 136 | }, 137 | ], 138 | }); 139 | }); 140 | 141 | it('Multiple contributors with one throwing an error', async () => { 142 | const client = await activate(docUri); 143 | client._customSchemaContributors = {}; 144 | client.registerContributor(SCHEMA2, onRequestSchema2URI, onRequestSchema2Content); 145 | client.registerContributor('schemathrowingerror', onRequestSchemaURIThrowError, onRequestSchemaContentThrowError); 146 | 147 | await testCompletion(docUri, new vscode.Position(0, 0), { 148 | items: [ 149 | { 150 | label: 'apple', 151 | kind: 9, 152 | documentation: 'An apple', 153 | }, 154 | ], 155 | }); 156 | }); 157 | }); 158 | 159 | const schemaJSON = JSON.stringify({ 160 | type: 'object', 161 | properties: { 162 | version: { 163 | type: 'string', 164 | description: 'A stringy string string', 165 | enum: ['test'], 166 | }, 167 | }, 168 | }); 169 | 170 | function onRequestSchema1URI(resource: string): string | undefined { 171 | if (resource.endsWith('completion.yaml') || resource.endsWith('basic.yaml')) { 172 | return `${SCHEMA}://schema/porter`; 173 | } 174 | return undefined; 175 | } 176 | 177 | function onRequestSchema1Content(): string | undefined { 178 | return schemaJSON; 179 | } 180 | 181 | function onRequestSchemaURIThrowError(): string | undefined { 182 | throw new Error('test what happens when an error is thrown and not caught'); 183 | } 184 | 185 | function onRequestSchemaContentThrowError(): string | undefined { 186 | throw new Error('test what happens when an error is thrown and not caught'); 187 | } 188 | 189 | const schemaJSON2 = JSON.stringify({ 190 | type: 'object', 191 | properties: { 192 | apple: { 193 | type: 'string', 194 | description: 'An apple', 195 | }, 196 | }, 197 | }); 198 | 199 | function onRequestSchema2URI(): string | undefined { 200 | return `${SCHEMA2}://schema/porter`; 201 | } 202 | 203 | function onRequestSchema2Content(): string | undefined { 204 | return schemaJSON2; 205 | } 206 | 207 | function onRequestSchema3URI(): string | undefined { 208 | return `${SCHEMA3}://schema/porter`; 209 | } 210 | 211 | function onRequestSchema3Content(): string | undefined { 212 | return schemaJSON; 213 | } 214 | -------------------------------------------------------------------------------- /test/telemetry.test.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | import * as sinon from 'sinon'; 6 | import * as sinonChai from 'sinon-chai'; 7 | import * as chai from 'chai'; 8 | import * as vscode from 'vscode'; 9 | import { TelemetryErrorHandler, TelemetryOutputChannel } from '../src/telemetry'; 10 | import { TelemetryEvent, TelemetryService } from '@redhat-developer/vscode-redhat-telemetry'; 11 | 12 | const expect = chai.expect; 13 | chai.use(sinonChai); 14 | class TelemetryStub implements TelemetryService { 15 | sendStartupEvent(): Promise { 16 | throw new Error('Method not implemented.'); 17 | } 18 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 19 | send(event: TelemetryEvent): Promise { 20 | throw new Error('Method not implemented.'); 21 | } 22 | sendShutdownEvent(): Promise { 23 | throw new Error('Method not implemented.'); 24 | } 25 | flushQueue(): Promise { 26 | throw new Error('Method not implemented.'); 27 | } 28 | dispose(): Promise { 29 | throw new Error('Method not implemented.'); 30 | } 31 | } 32 | // skip this suite as `useFakeTimers` hung's vscode and CI newer finish build 33 | describe.skip('Telemetry Test', () => { 34 | const sandbox = sinon.createSandbox(); 35 | const testOutputChannel = vscode.window.createOutputChannel('YAML_TEST'); 36 | afterEach(() => { 37 | sandbox.restore(); 38 | }); 39 | describe('TelemetryOutputChannel', () => { 40 | let telemetryChannel: TelemetryOutputChannel; 41 | let outputChannel: sinon.SinonStubbedInstance; 42 | let telemetry: sinon.SinonStubbedInstance; 43 | let clock: sinon.SinonFakeTimers; 44 | 45 | beforeEach(() => { 46 | outputChannel = sandbox.stub(testOutputChannel); 47 | telemetry = sandbox.stub(new TelemetryStub()); 48 | telemetryChannel = new TelemetryOutputChannel( 49 | (outputChannel as unknown) as vscode.OutputChannel, 50 | (telemetry as unknown) as TelemetryService 51 | ); 52 | clock = sinon.useFakeTimers(); 53 | }); 54 | 55 | afterEach(() => { 56 | clock.restore(); 57 | }); 58 | 59 | it('should delegate "append" method', () => { 60 | telemetryChannel.append('Some'); 61 | expect(outputChannel.append).calledOnceWith('Some'); 62 | }); 63 | 64 | it('should delegate "appendLine" method', () => { 65 | telemetryChannel.appendLine('Some'); 66 | expect(outputChannel.appendLine).calledOnceWith('Some'); 67 | }); 68 | 69 | it('should delegate "clear" method', () => { 70 | telemetryChannel.clear(); 71 | expect(outputChannel.clear).calledOnce; 72 | }); 73 | 74 | it('should delegate "dispose" method', () => { 75 | telemetryChannel.dispose(); 76 | expect(outputChannel.dispose).calledOnce; 77 | }); 78 | 79 | it('should delegate "hide" method', () => { 80 | telemetryChannel.hide(); 81 | expect(outputChannel.hide).calledOnce; 82 | }); 83 | 84 | it('should delegate "show" method', () => { 85 | telemetryChannel.show(); 86 | expect(outputChannel.show).calledOnce; 87 | }); 88 | 89 | it('should send telemetry if log error in "append"', () => { 90 | telemetryChannel.append('[Error] Some'); 91 | clock.tick(51); 92 | expect(telemetry.send).calledOnceWith({ name: 'yaml.server.error', properties: { error: 'Some' } }); 93 | }); 94 | 95 | it('should send telemetry if log error on "appendLine"', () => { 96 | telemetryChannel.appendLine('[Error] Some error'); 97 | clock.tick(51); 98 | expect(telemetry.send).calledOnceWith({ name: 'yaml.server.error', properties: { error: 'Some error' } }); 99 | }); 100 | 101 | it("shouldn't send telemetry if error should be skipped", () => { 102 | telemetryChannel.append( 103 | "[Error - 15:10:33] (node:25052) Warning: Setting the NODE_TLS_REJECT_UNAUTHORIZED environment variable to '0' makes TLS connections and HTTPS requests insecure by disabling certificate verification." 104 | ); 105 | clock.tick(51); 106 | expect(telemetry.send).not.called; 107 | }); 108 | 109 | it('should throttle send telemetry if "append" called multiple times', () => { 110 | telemetryChannel.append('[Error] Some'); 111 | telemetryChannel.append('[Error] Second Error'); 112 | clock.tick(51); 113 | expect(telemetry.send).calledOnceWith({ name: 'yaml.server.error', properties: { error: 'Some\nSecond Error' } }); 114 | }); 115 | 116 | it('should throttle send telemetry if "appendLine" called multiple times', () => { 117 | telemetryChannel.appendLine('[Error] Some'); 118 | telemetryChannel.appendLine('[Error] Second Error'); 119 | telemetryChannel.appendLine('[Error] Third Error'); 120 | clock.tick(51); 121 | expect(telemetry.send).calledOnceWith({ 122 | name: 'yaml.server.error', 123 | properties: { error: 'Some\nSecond Error\nThird Error' }, 124 | }); 125 | }); 126 | }); 127 | 128 | describe('TelemetryErrorHandler', () => { 129 | let telemetry: sinon.SinonStubbedInstance; 130 | let errorHandler: TelemetryErrorHandler; 131 | 132 | beforeEach(() => { 133 | telemetry = sandbox.stub(new TelemetryStub()); 134 | errorHandler = new TelemetryErrorHandler(telemetry, 'YAML LS', 3); 135 | }); 136 | 137 | it('should log telemetry on error', () => { 138 | errorHandler.error(new Error('Some'), { jsonrpc: 'Error message' }, 3); 139 | expect(telemetry.send).calledOnceWith({ 140 | name: 'yaml.lsp.error', 141 | properties: { jsonrpc: 'Error message', error: 'Some' }, 142 | }); 143 | }); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /test/testFixture/completion/.travis.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer/vscode-yaml/cc15df699148c614db963c53a880aa4590030ec8/test/testFixture/completion/.travis.yml -------------------------------------------------------------------------------- /test/testFixture/completion/completion.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redhat-developer/vscode-yaml/cc15df699148c614db963c53a880aa4590030ec8/test/testFixture/completion/completion.yaml -------------------------------------------------------------------------------- /test/testFixture/completion/enum_completion.yaml: -------------------------------------------------------------------------------- 1 | my_key: my_ 2 | -------------------------------------------------------------------------------- /test/testFixture/completion/schemaProvider.yaml: -------------------------------------------------------------------------------- 1 | apple: tastes_good 2 | -------------------------------------------------------------------------------- /test/testFixture/hover/basic.yaml: -------------------------------------------------------------------------------- 1 | version: apple 2 | -------------------------------------------------------------------------------- /test/testFixture/schemas/basic_completion_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "my_key": { 5 | "type": "string", 6 | "description": "My string", 7 | "enum": ["my_value"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/testFixture/validation/schemaProvider.yaml: -------------------------------------------------------------------------------- 1 | version: False 2 | -------------------------------------------------------------------------------- /test/testRunner.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Red Hat, Inc. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | import * as path from 'path'; 6 | import * as cp from 'child_process'; 7 | import { runTests, downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath } from '@vscode/test-electron'; 8 | 9 | async function main(): Promise { 10 | try { 11 | const executable = await downloadAndUnzipVSCode(); 12 | const cliPath = resolveCliPathFromVSCodeExecutablePath(executable); 13 | const dependencies = []; 14 | for (const dep of dependencies) { 15 | const installLog = cp.execSync(`"${cliPath}" --install-extension ${dep}`); 16 | console.log(installLog.toString()); 17 | } 18 | // The folder containing the Extension Manifest package.json 19 | // Passed to `--extensionDevelopmentPath` 20 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 21 | 22 | // The path to test runner 23 | // Passed to --extensionTestsPath 24 | const extensionTestsPath = path.resolve(__dirname, './index'); 25 | 26 | // Download VS Code, unzip it and run the integration test 27 | await runTests({ 28 | vscodeExecutablePath: executable, 29 | extensionDevelopmentPath, 30 | extensionTestsPath, 31 | launchArgs: ['--disable-extension=ms-kubernetes-tools.vscode-kubernetes-tools', '.'], 32 | }); 33 | } catch (err) { 34 | console.error('Failed to run tests'); 35 | process.exit(1); 36 | } 37 | } 38 | 39 | main(); 40 | -------------------------------------------------------------------------------- /test/ui-test/allTestsSuite.ts: -------------------------------------------------------------------------------- 1 | import { extensionUIAssetsTest } from './extensionUITest'; 2 | import { contentAssistSuggestionTest } from './contentAssistTest'; 3 | import { customTagsTest } from './customTagsTest'; 4 | import { schemaIsSetTest } from './schemaIsSetTest'; 5 | import { autocompletionTest } from './autocompletionTest'; 6 | 7 | describe('VSCode YAML - UI tests', () => { 8 | extensionUIAssetsTest(); 9 | contentAssistSuggestionTest(); 10 | customTagsTest(); 11 | schemaIsSetTest(); 12 | autocompletionTest(); 13 | }); 14 | -------------------------------------------------------------------------------- /test/ui-test/autocompletionTest.ts: -------------------------------------------------------------------------------- 1 | import os = require('os'); 2 | import path = require('path'); 3 | import { expect } from 'chai'; 4 | import { WebDriver, TextEditor, EditorView, VSBrowser, ContentAssist } from 'vscode-extension-tester'; 5 | import { getSchemaLabel, deleteFileInHomeDir, createCustomFile } from './util/utility'; 6 | 7 | /** 8 | * @author Zbynek Cervinka 9 | * @author Ondrej Dockal 10 | */ 11 | export function autocompletionTest(): void { 12 | describe('Verify autocompletion completes what should be completed', () => { 13 | let driver: WebDriver; 14 | const yamlFileName = 'kustomization.yaml'; 15 | const homeDir = os.homedir(); 16 | const yamlFilePath = path.join(homeDir, yamlFileName); 17 | let editor: TextEditor; 18 | 19 | before(async function setup() { 20 | this.timeout(20000); 21 | driver = VSBrowser.instance.driver; 22 | await createCustomFile(yamlFilePath); 23 | await driver.wait(async () => { 24 | return await getSchemaLabel(yamlFileName); 25 | }, 18000); 26 | }); 27 | 28 | it('Autocompletion works as expected', async function () { 29 | this.timeout(30000); 30 | 31 | editor = new TextEditor(); 32 | await editor.typeTextAt(1, 1, 'api'); 33 | const contentAssist = (await editor.toggleContentAssist(true)) as ContentAssist; 34 | if (contentAssist.hasItem('apiVersion')) { 35 | await (await contentAssist.getItem('apiVersion')).click(); 36 | } 37 | const text = await editor.getText(); 38 | 39 | if (text != 'apiVersion: ') { 40 | expect.fail("The 'apiVersion: ' string has not been autocompleted."); 41 | } 42 | }); 43 | 44 | after(async function () { 45 | this.timeout(5000); 46 | await editor.save(); 47 | await new EditorView().closeAllEditors(); 48 | deleteFileInHomeDir(yamlFileName); 49 | }); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /test/ui-test/common/YAMLConstants.ts: -------------------------------------------------------------------------------- 1 | export class YamlConstants { 2 | // General 3 | public static readonly YES = 'Yes'; 4 | public static readonly NO = 'No'; 5 | public static readonly LICENSE_EDITOR = 'workflow.license'; 6 | 7 | public static readonly YAML_NAME = 'YAML'; 8 | } 9 | -------------------------------------------------------------------------------- /test/ui-test/contentAssistTest.ts: -------------------------------------------------------------------------------- 1 | import os = require('os'); 2 | import path = require('path'); 3 | import { expect } from 'chai'; 4 | import { WebDriver, VSBrowser, ContentAssist, EditorView, TextEditor } from 'vscode-extension-tester'; 5 | import { createCustomFile, deleteFileInHomeDir, getSchemaLabel } from './util/utility'; 6 | /** 7 | * @author Zbynek Cervinka 8 | * @author Ondrej Dockal 9 | */ 10 | export function contentAssistSuggestionTest(): void { 11 | describe('Verify content assist suggests right sugestion', () => { 12 | let driver: WebDriver; 13 | let editor: TextEditor; 14 | const yamlFileName = 'kustomization.yaml'; 15 | const homeDir = os.homedir(); 16 | const yamlFilePath = path.join(homeDir, yamlFileName); 17 | 18 | before(async function setup() { 19 | this.timeout(20000); 20 | driver = VSBrowser.instance.driver; 21 | editor = await createCustomFile(yamlFilePath); 22 | await driver.wait(async () => { 23 | return await getSchemaLabel(yamlFileName); 24 | }, 18000); 25 | }); 26 | 27 | it('Content assist suggests right suggestion', async function () { 28 | this.timeout(15000); 29 | editor = new TextEditor(); 30 | await editor.setText('api'); 31 | const contentAssist = await editor.toggleContentAssist(true); 32 | 33 | // find if an item with given label is present 34 | if (contentAssist instanceof ContentAssist) { 35 | const hasItem = await contentAssist.hasItem('apiVersion'); 36 | if (!hasItem) { 37 | expect.fail("The 'apiVersion' string did not appear in the content assist's suggestion list."); 38 | } 39 | } else { 40 | expect.fail("The 'apiVersion' string did not appear in the content assist's suggestion list."); 41 | } 42 | }); 43 | 44 | after(async function () { 45 | this.timeout(5000); 46 | await editor.save(); 47 | await new EditorView().closeAllEditors(); 48 | deleteFileInHomeDir(yamlFileName); 49 | }); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /test/ui-test/customTagsTest.ts: -------------------------------------------------------------------------------- 1 | import os = require('os'); 2 | import path = require('path'); 3 | import { expect } from 'chai'; 4 | import { By, WebDriver, TextEditor, Workbench, ContentAssist, EditorView, VSBrowser } from 'vscode-extension-tester'; 5 | import { createCustomFile, deleteFileInHomeDir, getSchemaLabel, hardDelay } from './util/utility'; 6 | 7 | /** 8 | * @author Zbynek Cervinka 9 | * @author Ondrej Dockal 10 | */ 11 | export function customTagsTest(): void { 12 | describe("Verify extension's custom tags", () => { 13 | let driver: WebDriver; 14 | const yamlFileName = 'kustomization.yaml'; 15 | const homeDir = os.homedir(); 16 | const yamlFilePath = path.join(homeDir, yamlFileName); 17 | let editor: TextEditor; 18 | let editorView: EditorView; 19 | 20 | before(async function setup() { 21 | this.timeout(20000); 22 | driver = VSBrowser.instance.driver; 23 | editorView = new EditorView(); 24 | await createCustomFile(yamlFilePath); 25 | await driver.wait(async () => { 26 | return await getSchemaLabel(yamlFileName); 27 | }, 18000); 28 | }); 29 | 30 | it('YAML custom tags works as expected', async function () { 31 | this.timeout(30000); 32 | 33 | const settingsEditor = await new Workbench().openSettings(); 34 | const setting = await settingsEditor.findSetting('Custom Tags', 'Yaml'); 35 | await setting.findElement(By.className('edit-in-settings-button')).click(); 36 | 37 | await hardDelay(2000); 38 | const textSettingsEditor = (await editorView.openEditor('settings.json')) as TextEditor; 39 | if (process.platform === 'darwin') { 40 | await driver.actions().sendKeys(' "customTag1"').perform(); 41 | } else { 42 | const coor = await textSettingsEditor.getCoordinates(); 43 | await textSettingsEditor.typeTextAt(coor[0], coor[1], ' "customTag1"'); 44 | } 45 | await textSettingsEditor.save(); 46 | await hardDelay(1_000); 47 | 48 | editor = (await editorView.openEditor(yamlFileName)) as TextEditor; 49 | await editor.setText('custom'); 50 | await editor.save(); 51 | 52 | const contentAssist = await editor.toggleContentAssist(true); 53 | 54 | // find if an item with given label is present in the content assist 55 | if (contentAssist instanceof ContentAssist) { 56 | const hasItem = await contentAssist.hasItem('customTag1'); 57 | if (!hasItem) { 58 | expect.fail("The 'customTag1' custom tag did not appear in the content assist's suggestion list."); 59 | } 60 | } else { 61 | expect.fail("The 'customTag1' custom tag did not appear in the content assist's suggestion list."); 62 | } 63 | }); 64 | 65 | after(async function () { 66 | this.timeout(5000); 67 | await new EditorView().closeAllEditors(); 68 | deleteFileInHomeDir(yamlFileName); 69 | }); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /test/ui-test/extensionUITest.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { YamlConstants } from './common/YAMLConstants'; 3 | import { 4 | ActivityBar, 5 | ExtensionsViewItem, 6 | ExtensionsViewSection, 7 | SideBarView, 8 | VSBrowser, 9 | ViewControl, 10 | WebDriver, 11 | } from 'vscode-extension-tester'; 12 | 13 | /** 14 | * @author Ondrej Dockal 15 | */ 16 | export function extensionUIAssetsTest(): void { 17 | describe("Verify extension's base assets available after install", () => { 18 | let driver: WebDriver; 19 | let sideBar: SideBarView; 20 | let view: ViewControl; 21 | let section: ExtensionsViewSection; 22 | let yamlItem: ExtensionsViewItem; 23 | 24 | before(async function () { 25 | this.timeout(20000); 26 | driver = VSBrowser.instance.driver; 27 | view = await new ActivityBar().getViewControl('Extensions'); 28 | sideBar = await view.openView(); 29 | driver.wait( 30 | async () => !(await sideBar.getContent().hasProgress()), 31 | 5000, 32 | "Progress bar hasn't been hidden within the timeout" 33 | ); 34 | section = (await sideBar.getContent().getSection('Installed')) as ExtensionsViewSection; 35 | await section.expand(); 36 | yamlItem = await driver.wait( 37 | async () => { 38 | return await section.findItem(`@installed ${YamlConstants.YAML_NAME}`); 39 | }, 40 | 5000, 41 | 'There were not visible items available under installed section' 42 | ); 43 | }); 44 | 45 | it('YAML extension is installed', async function () { 46 | this.timeout(5000); 47 | expect(yamlItem).not.undefined; 48 | let author: string; 49 | let name: string; 50 | try { 51 | name = await yamlItem.getTitle(); 52 | author = await yamlItem.getAuthor(); 53 | } catch (error) { 54 | if ((error as Error).name === 'StaleElementReferenceError') { 55 | yamlItem = await section.findItem(`@installed ${YamlConstants.YAML_NAME}`); 56 | name = await yamlItem.getTitle(); 57 | author = await yamlItem.getAuthor(); 58 | } 59 | throw error; 60 | } 61 | expect(name).to.equal(YamlConstants.YAML_NAME); 62 | expect(author).to.equal('Red Hat'); 63 | }); 64 | 65 | after(async () => { 66 | if (sideBar && (await sideBar.isDisplayed()) && view) { 67 | await view.closeView(); 68 | } 69 | }); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /test/ui-test/schemaIsSetTest.ts: -------------------------------------------------------------------------------- 1 | import os = require('os'); 2 | import path = require('path'); 3 | import { WebDriver, VSBrowser, EditorView, WebElement } from 'vscode-extension-tester'; 4 | import { createCustomFile, deleteFileInHomeDir, getSchemaLabel } from './util/utility'; 5 | import { expect } from 'chai'; 6 | 7 | /** 8 | * @author Zbynek Cervinka 9 | * @author Ondrej Dockal 10 | */ 11 | export function schemaIsSetTest(): void { 12 | describe('Verify that the right JSON schema has been selected', () => { 13 | let driver: WebDriver; 14 | const yamlFileName = 'kustomization.yaml'; 15 | const homeDir = os.homedir(); 16 | const yamlFilePath = path.join(homeDir, yamlFileName); 17 | let schema: WebElement; 18 | 19 | before(async function setup() { 20 | this.timeout(20000); 21 | driver = VSBrowser.instance.driver; 22 | await createCustomFile(yamlFilePath); 23 | schema = await driver.wait(async () => { 24 | return await getSchemaLabel(yamlFileName); 25 | }, 18000); 26 | }); 27 | 28 | it('The right JSON schema has been selected', async function () { 29 | this.timeout(5000); 30 | expect(schema).not.undefined; 31 | expect(await schema.getText()).to.include('kustomization'); 32 | }); 33 | 34 | after(async function () { 35 | this.timeout(5000); 36 | await new EditorView().closeAllEditors(); 37 | deleteFileInHomeDir(yamlFileName); 38 | }); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/ui-test/util/utility.ts: -------------------------------------------------------------------------------- 1 | import os = require('os'); 2 | import path = require('path'); 3 | import { StatusBar, By, WebElement, InputBox, TextEditor, Workbench } from 'vscode-extension-tester'; 4 | 5 | /** 6 | * @author Zbynek Cervinka 7 | * @author Ondrej Dockal 8 | */ 9 | 10 | export async function createCustomFile(path: string): Promise { 11 | await new Workbench().openCommandPrompt(); 12 | 13 | let input = await InputBox.create(); 14 | await input.setText('>new file'); 15 | await input.confirm(); 16 | await input.confirm(); 17 | const editor = new TextEditor(); 18 | editor.save(); 19 | input = await InputBox.create(); 20 | await input.setText(path); 21 | await input.confirm(); 22 | return editor; 23 | } 24 | 25 | export function deleteFileInHomeDir(filename: string): void { 26 | const homeDir = os.homedir(); 27 | const pathtofile = path.join(homeDir, filename); 28 | 29 | // eslint-disable-next-line @typescript-eslint/no-var-requires 30 | const fs = require('fs'); 31 | if (fs.existsSync(pathtofile)) { 32 | fs.rmSync(pathtofile, { recursive: true, force: true }); 33 | } 34 | } 35 | 36 | export async function getSchemaLabel(text: string): Promise { 37 | const schemalabel = await new StatusBar().findElements(By.xpath('.//a[@aria-label="' + text + ', Select JSON Schema"]')); 38 | return schemalabel[0]; 39 | } 40 | 41 | export async function hardDelay(milliseconds: number): Promise { 42 | return new Promise((resolve) => setTimeout(resolve, milliseconds)); 43 | } 44 | -------------------------------------------------------------------------------- /thirdpartynotices.txt: -------------------------------------------------------------------------------- 1 | redhat-developer/vscode-yaml 2 | 3 | THIRD-PARTY SOFTWARE NOTICES AND INFORMATION 4 | Do Not Translate or Localize 5 | 6 | This project incorporates components from the projects listed below. The original copyright notices and the licenses under which Red Hat received such components are set forth below. Red Hat reserves all rights not expressly granted herein, whether by implication, estoppel or otherwise. 7 | 8 | 1. textmate/yaml.tmbundle (https://github.com/textmate/yaml.tmbundle) 9 | 2. microsoft/vscode (https://github.com/Microsoft/vscode) 10 | 11 | %% textmate/yaml.tmbundle NOTICES AND INFORMATION BEGIN HERE 12 | ========================================= 13 | Copyright (c) 2015 FichteFoll 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 31 | ========================================= 32 | END OF textmate/yaml.tmbundle NOTICES AND INFORMATION 33 | 34 | %% vscode NOTICES AND INFORMATION BEGIN HERE 35 | ========================================= 36 | MIT License 37 | 38 | Copyright (c) 2015 - present Microsoft Corporation 39 | 40 | All rights reserved. 41 | 42 | Permission is hereby granted, free of charge, to any person obtaining a copy 43 | of this software and associated documentation files (the "Software"), to deal 44 | in the Software without restriction, including without limitation the rights 45 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 46 | copies of the Software, and to permit persons to whom the Software is 47 | furnished to do so, subject to the following conditions: 48 | 49 | The above copyright notice and this permission notice shall be included in all 50 | copies or substantial portions of the Software. 51 | 52 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 53 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 54 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 55 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 56 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 57 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 58 | SOFTWARE. 59 | ========================================= 60 | END OF vscode NOTICES AND INFORMATION -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2022","WebWorker"], 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "out", 7 | "skipLibCheck": true, 8 | "sourceMap": true, 9 | "target": "es2022" 10 | }, 11 | "exclude": ["node_modules", "server", "src/webworker"], 12 | "include": ["src", "test"] 13 | } 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | //@ts-check 5 | 6 | 'use strict'; 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-var-requires 9 | const path = require('path'); 10 | // eslint-disable-next-line @typescript-eslint/no-var-requires 11 | const webpack = require('webpack'); 12 | 13 | /**@type {import('webpack').Configuration}*/ 14 | const config = { 15 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 16 | node: { 17 | __dirname: false, 18 | __filename: false, 19 | }, 20 | entry: { 21 | extension: './src/node/yamlClientMain.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 22 | languageserver: './node_modules/yaml-language-server/out/server/src/server.js', 23 | }, 24 | output: { 25 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 26 | path: path.resolve(__dirname, 'dist'), 27 | filename: '[name].js', 28 | libraryTarget: 'commonjs2', 29 | devtoolModuleFilenameTemplate: '../[resource-path]', 30 | }, 31 | devtool: 'source-map', 32 | externals: { 33 | vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 34 | prettier: 'commonjs prettier', 35 | }, 36 | resolve: { 37 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 38 | extensions: ['.ts', '.js'], 39 | }, 40 | module: { 41 | rules: [ 42 | { 43 | test: /\.ts$/, 44 | exclude: /node_modules/, 45 | use: [ 46 | { 47 | loader: 'ts-loader', 48 | }, 49 | ], 50 | }, 51 | { 52 | test: /node_modules[\\|/](vscode-json-languageservice)/, 53 | use: { loader: 'umd-compat-loader' }, 54 | }, 55 | ], 56 | }, 57 | }; 58 | 59 | /**@type {import('webpack').Configuration}*/ 60 | const clientWeb = { 61 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 62 | target: 'webworker', // extensions run in a webworker context 63 | entry: { 64 | 'extension-web': './src/webworker/yamlClientMain.ts', 65 | }, 66 | output: { 67 | filename: 'extension-web.js', 68 | path: path.join(__dirname, './dist'), 69 | libraryTarget: 'commonjs', 70 | devtoolModuleFilenameTemplate: '../[resource-path]', 71 | }, 72 | resolve: { 73 | mainFields: ['module', 'main'], 74 | extensions: ['.ts', '.js'], // support ts-files and js-files 75 | alias: { 76 | 'node-fetch': 'whatwg-fetch', 77 | 'object-hash': 'object-hash/dist/object_hash.js', 78 | }, 79 | fallback: { 80 | path: require.resolve('path-browserify'), 81 | 'node-fetch': require.resolve('whatwg-fetch'), 82 | util: require.resolve('util'), 83 | fs: false, 84 | }, 85 | }, 86 | module: { 87 | rules: [ 88 | { 89 | test: /\.ts$/, 90 | exclude: /node_modules/, 91 | use: [ 92 | { 93 | // configure TypeScript loader: 94 | // * enable sources maps for end-to-end source maps 95 | loader: 'ts-loader', 96 | options: { 97 | compilerOptions: { 98 | sourceMap: true, 99 | declaration: false, 100 | }, 101 | }, 102 | }, 103 | ], 104 | }, 105 | ], 106 | }, 107 | plugins: [ 108 | new webpack.ProvidePlugin({ 109 | process: path.resolve(path.join(__dirname, 'node_modules/process/browser.js')), // provide a shim for the global `process` variable 110 | }), 111 | ], 112 | externals: { 113 | vscode: 'commonjs vscode', // ignored because it doesn't exist 114 | }, 115 | performance: { 116 | hints: false, 117 | }, 118 | devtool: 'nosources-source-map', 119 | }; 120 | 121 | /**@type {import('webpack').Configuration}*/ 122 | const serverWeb = { 123 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 124 | target: 'webworker', // extensions run in a webworker context 125 | entry: { 126 | 'languageserver-web': './node_modules/yaml-language-server/lib/esm/webworker/yamlServerMain', 127 | }, 128 | output: { 129 | filename: 'languageserver-web.js', 130 | path: path.join(__dirname, './dist'), 131 | libraryTarget: 'var', 132 | library: 'serverExportVar', 133 | devtoolModuleFilenameTemplate: '../[resource-path]', 134 | }, 135 | resolve: { 136 | mainFields: ['browser', 'module', 'main'], 137 | extensions: ['.ts', '.js'], // support ts-files and js-files 138 | alias: { 139 | './services/yamlFormatter': path.resolve(__dirname, './build/polyfills/yamlFormatter.js'), // not supported for now. prettier can run in the web, but it's a bit more work. 140 | 'vscode-json-languageservice/lib/umd': 'vscode-json-languageservice/lib/esm', 141 | }, 142 | fallback: { 143 | path: require.resolve('path-browserify/'), 144 | url: require.resolve('url/'), 145 | buffer: require.resolve('buffer/'), 146 | fs: false, 147 | }, 148 | }, 149 | plugins: [ 150 | new webpack.ProvidePlugin({ 151 | process: path.resolve(path.join(__dirname, 'node_modules/process/browser.js')), // provide a shim for the global `process` variable 152 | }), 153 | ], 154 | module: {}, 155 | externals: {}, 156 | performance: { 157 | hints: false, 158 | }, 159 | 160 | devtool: 'nosources-source-map', 161 | }; 162 | 163 | module.exports = [config, clientWeb, serverWeb]; 164 | --------------------------------------------------------------------------------