├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── pr-gated.yml ├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── LICENSE ├── README.md ├── SECURITY.md ├── client ├── .eslintignore ├── .eslintrc.js ├── .vscode-test.js ├── .vscode │ └── extensions.json ├── package-lock.json ├── package.json ├── src │ ├── commands.ts │ ├── constants.ts │ ├── dataflowModel.ts │ ├── extension.ts │ ├── funcUtils.ts │ ├── librarySymbolClient.ts │ ├── librarySymbolManager.ts │ ├── librarySymbolUtils.ts │ ├── powerQueryApi.ts │ ├── subscriptions │ │ ├── documentSemanticTokensProvider.ts │ │ └── index.ts │ └── test │ │ ├── suite │ │ ├── completion.test.ts │ │ ├── completionUtils.ts │ │ ├── dataflowExtractCommand.test.ts │ │ ├── diagnostics.test.ts │ │ ├── documentSymbolUtils.ts │ │ ├── documentSymbols.test.ts │ │ ├── encodingCommands.test.ts │ │ ├── extension.test.ts │ │ ├── librarySymbolManager.test.ts │ │ ├── librarySymbolUtils.test.ts │ │ ├── sectionCompletion.test.ts │ │ └── testUtils.ts │ │ └── testFixture │ │ ├── .vscode │ │ └── settings.json │ │ ├── Diagnostics.ExternalLibrarySymbol.pq │ │ ├── Diagnostics.NoErrors.pq │ │ ├── Diagnostics.TableIsEmpty.Error.pq │ │ ├── DocumentSymbols.pq │ │ ├── ExtensionTest.json │ │ ├── completion.pq │ │ ├── dataflow.json │ │ ├── diagnostics.pq │ │ └── section.pq ├── tsconfig.json ├── tsconfig.webpack.json └── webpack.config.js ├── imgs ├── PQIcon_256.png ├── formatDocument.gif ├── fuzzyAutocomplete.gif ├── hover.png ├── jsonDecodeEncode.png └── parameterHints.png ├── language-configuration.json ├── package-lock.json ├── package.json ├── scripts ├── .eslintignore ├── .eslintrc.js ├── package-lock.json ├── package.json ├── readme.md ├── src │ └── benchmarkFile.ts └── tsconfig.json ├── server ├── .eslintignore ├── .eslintrc.js ├── .mocharc.json ├── .vscode │ └── extensions.json ├── mochaReporterConfig.json ├── package-lock.json ├── package.json ├── src │ ├── cancellationToken │ │ ├── cancellationTokenAdapter.ts │ │ ├── cancellationTokenUtils.ts │ │ └── index.ts │ ├── errorUtils.ts │ ├── eventHandlerUtils.ts │ ├── index.ts │ ├── library │ │ ├── externalLibraryUtils.ts │ │ ├── index.ts │ │ ├── librarySymbolUtils.ts │ │ ├── libraryTypeResolver.ts │ │ ├── libraryUtils.ts │ │ ├── moduleLibraryUtils.ts │ │ ├── sdk │ │ │ └── sdk-enUs.json │ │ └── standard │ │ │ └── standard-enUs.json │ ├── server.ts │ ├── settings │ │ ├── index.ts │ │ ├── settings.ts │ │ └── settingsUtils.ts │ ├── test │ │ ├── errorUtils.test.ts │ │ └── standardLibrary.test.ts │ └── traceManagerUtils.ts ├── tsconfig.json ├── tsconfig.webpack.json └── webpack.config.js ├── syntaxes ├── .prettierrc └── powerquery.tmLanguage.json └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | overrides: [ 4 | { 5 | files: "**/*.ts", 6 | parser: "@typescript-eslint/parser", 7 | parserOptions: { project: "tsconfig.json" }, 8 | plugins: ["@typescript-eslint", "prettier", "promise", "security"], 9 | extends: [ 10 | "eslint:recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:prettier/recommended", 13 | "plugin:security/recommended", 14 | ], 15 | rules: { 16 | "@typescript-eslint/await-thenable": "error", 17 | "@typescript-eslint/consistent-type-assertions": ["warn", { assertionStyle: "as" }], 18 | "@typescript-eslint/explicit-function-return-type": "error", 19 | "@typescript-eslint/no-floating-promises": "error", 20 | "@typescript-eslint/no-inferrable-types": "off", 21 | "@typescript-eslint/no-namespace": "error", 22 | "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], 23 | "@typescript-eslint/prefer-namespace-keyword": "error", 24 | "@typescript-eslint/space-infix-ops": "error", 25 | "@typescript-eslint/switch-exhaustiveness-check": "error", 26 | "@typescript-eslint/typedef": [ 27 | "error", 28 | { 29 | arrayDestructuring: true, 30 | arrowParameter: true, 31 | memberVariableDeclaration: true, 32 | objectDestructuring: true, 33 | parameter: true, 34 | propertyDeclaration: true, 35 | variableDeclaration: true, 36 | }, 37 | ], 38 | "@typescript-eslint/unified-signatures": "error", 39 | "array-callback-return": "error", 40 | "arrow-body-style": ["error", "as-needed"], 41 | "constructor-super": "error", 42 | curly: ["error", "all"], 43 | "max-len": [ 44 | "warn", 45 | { 46 | code: 120, 47 | ignorePattern: "^(?!.*/(/|\\*) .* .*).*$", 48 | }, 49 | ], 50 | "no-async-promise-executor": "error", 51 | "no-await-in-loop": "error", 52 | "no-class-assign": "error", 53 | "no-compare-neg-zero": "error", 54 | "no-cond-assign": "error", 55 | "no-constant-condition": "error", 56 | "no-dupe-class-members": "error", 57 | "no-dupe-else-if": "error", 58 | "no-dupe-keys": "error", 59 | "no-duplicate-imports": "error", 60 | "no-restricted-globals": "error", 61 | "no-eval": "error", 62 | "no-extra-boolean-cast": "error", 63 | "no-fallthrough": "error", 64 | "no-func-assign": "error", 65 | "no-global-assign": "error", 66 | "no-implicit-coercion": "error", 67 | "no-implicit-globals": "error", 68 | "no-implied-eval": "error", 69 | "no-invalid-this": "error", 70 | "no-irregular-whitespace": "error", 71 | "no-lone-blocks": "error", 72 | "no-lonely-if": "error", 73 | "no-loss-of-precision": "error", 74 | "no-nested-ternary": "error", 75 | "no-plusplus": "error", 76 | "no-self-assign": "error", 77 | "no-self-compare": "error", 78 | "no-sparse-arrays": "error", 79 | "no-this-before-super": "error", 80 | "no-unreachable": "error", 81 | "no-unsafe-optional-chaining": "error", 82 | "no-unused-private-class-members": "error", 83 | "no-useless-backreference": "error", 84 | "no-useless-catch": "error", 85 | "no-useless-computed-key": "error", 86 | "no-useless-concat": "error", 87 | "no-useless-rename": "error", 88 | "no-useless-return": "error", 89 | "object-shorthand": ["error", "always"], 90 | "one-var": ["error", "never"], 91 | "padding-line-between-statements": [ 92 | "warn", 93 | { 94 | blankLine: "always", 95 | prev: "*", 96 | next: [ 97 | "class", 98 | "do", 99 | "for", 100 | "function", 101 | "if", 102 | "multiline-block-like", 103 | "multiline-const", 104 | "multiline-expression", 105 | "multiline-let", 106 | "multiline-var", 107 | "switch", 108 | "try", 109 | "while", 110 | ], 111 | }, 112 | { 113 | blankLine: "always", 114 | prev: [ 115 | "class", 116 | "do", 117 | "for", 118 | "function", 119 | "if", 120 | "multiline-block-like", 121 | "multiline-const", 122 | "multiline-expression", 123 | "multiline-let", 124 | "multiline-var", 125 | "switch", 126 | "try", 127 | "while", 128 | ], 129 | next: "*", 130 | }, 131 | { 132 | blankLine: "always", 133 | prev: "*", 134 | next: ["continue", "return"], 135 | }, 136 | ], 137 | "prefer-template": "error", 138 | "prettier/prettier": "error", 139 | "promise/prefer-await-to-then": "error", 140 | "require-atomic-updates": "error", 141 | "require-await": "warn", 142 | "security/detect-non-literal-fs-filename": "off", 143 | "security/detect-object-injection": "off", 144 | "spaced-comment": ["warn", "always"], 145 | "sort-imports": ["error", { allowSeparatedGroups: true, ignoreCase: true }], 146 | "valid-typeof": "error", 147 | }, 148 | }, 149 | ], 150 | }; 151 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: JordanBoltonMN 7 | --- 8 | 9 | **Expected behavior** 10 | A clear and concise description of what you expected to happen. 11 | 12 | **Actual behavior** 13 | A clear and concise description of the description of what the bug is. 14 | 15 | **To Reproduce** 16 | Please include the following: 17 | 18 | - (Required) The Power Query script that triggers the issue. 19 | - (Required) Any non-default settings used in the API call(s) which trigger the issue. 20 | 21 | **Additional context** 22 | Add any other context about the problem here. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Enhancement]" 5 | labels: enhancement 6 | assignees: JordanBoltonMN 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/pr-gated.yml: -------------------------------------------------------------------------------- 1 | name: Gated pull request 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | branches: [master] 6 | jobs: 7 | build-and-test: 8 | runs-on: windows-latest 9 | steps: 10 | - name: checkout 11 | uses: actions/checkout@v4.1.2 12 | - name: setup node 13 | uses: actions/setup-node@v4.0.2 14 | with: 15 | node-version: "18.17" 16 | - run: node -v 17 | - run: npm ci 18 | - run: npm run audit 19 | - run: npm run build 20 | - run: npm run test:server 21 | - run: npm run vsix 22 | - name: retry 5 times client UI tests 23 | uses: nick-fields/retry@v3 24 | with: 25 | timeout_minutes: 10 26 | max_attempts: 5 27 | command: npm run test:client 28 | - name: upload VSIX to artifactory 29 | uses: actions/upload-artifact@v4.3.1 30 | with: 31 | name: vsix-artifact 32 | path: | 33 | *.vsix 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | out/ 64 | lib/ 65 | scratch/ 66 | .vscode-test/ 67 | **/tsconfig.tsbuildinfo 68 | client/lib/ 69 | *.vsix 70 | dist/ 71 | *.zip 72 | .vs/ 73 | test-results.xml 74 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "endOfLine": "auto", 4 | "printWidth": 120, 5 | "tabWidth": 4, 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["amodio.tsl-problem-matcher", "esbenp.prettier-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "name": "Launch Client", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--disable-extensions", "--extensionDevelopmentPath=${workspaceRoot}"], 11 | "outFiles": ["${workspaceRoot}/client/dist/extension.js"], 12 | "preLaunchTask": "Webpack Client" 13 | }, 14 | { 15 | "type": "node", 16 | "request": "attach", 17 | "name": "Attach to Server", 18 | "port": 6009, 19 | "restart": true, 20 | "sourceMaps": true, 21 | "outFiles": ["${workspaceRoot}/server/dist/server.js"] 22 | }, 23 | { 24 | "name": "Language UI Test", 25 | "type": "extensionHost", 26 | "request": "launch", 27 | "testConfiguration": "${workspaceFolder}/client/.vscode-test.js", 28 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 29 | "sourceMaps": true, 30 | "outFiles": ["${workspaceRoot}/client/lib/**/*.js"], 31 | "internalConsoleOptions": "openOnSessionStart", 32 | "preLaunchTask": "Run Watchers" 33 | }, 34 | { 35 | "type": "node", 36 | "request": "launch", 37 | "name": "Run server unit tests", 38 | "program": "${workspaceFolder}/server/node_modules/mocha/bin/_mocha", 39 | "cwd": "${workspaceFolder}/server", 40 | "args": ["--inspect", "--colors", "--timeout", "999999"], 41 | "internalConsoleOptions": "openOnSessionStart" 42 | } 43 | ], 44 | "compounds": [ 45 | { 46 | "name": "Client + Server", 47 | "configurations": ["Launch Client", "Attach to Server"], 48 | "preLaunchTask": "Webpack Server" 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": false, 3 | "typescript.tsc.autoDetect": "off", 4 | "typescript.preferences.quoteStyle": "single", 5 | "typescript.tsdk": "node_modules\\typescript\\lib", 6 | "editor.formatOnSave": true, 7 | "sarif-viewer.connectToGithubCodeScanning": "off" 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "webpack-dev", 7 | "path": "server/", 8 | "group": "build", 9 | "isBackground": true, 10 | "problemMatcher": ["$ts-webpack-watch", "$tslint-webpack-watch"], 11 | "label": "Webpack Server" 12 | }, 13 | { 14 | "type": "npm", 15 | "script": "webpack-dev", 16 | "path": "client/", 17 | "group": "build", 18 | "isBackground": true, 19 | "problemMatcher": ["$ts-webpack-watch", "$tslint-webpack-watch"], 20 | "label": "Webpack Client" 21 | }, 22 | { 23 | "type": "npm", 24 | "script": "watch", 25 | "path": "client/", 26 | "group": "build", 27 | "isBackground": true, 28 | "problemMatcher": [ 29 | { 30 | "base": "$tsc-watch", 31 | "fileLocation": ["relative", "${workspaceFolder}/client/"] 32 | } 33 | ], 34 | "label": "Build UI Tests" 35 | }, 36 | { 37 | "type": "npm", 38 | "script": "watch", 39 | "path": "server/", 40 | "group": "build", 41 | "isBackground": true, 42 | "problemMatcher": [ 43 | { 44 | "base": "$tsc-watch", 45 | "fileLocation": ["relative", "${workspaceFolder}/server/"] 46 | } 47 | ], 48 | "label": "Watch server unit tests" 49 | }, 50 | { 51 | "label": "Run Watchers", 52 | "dependsOn": ["Webpack Server", "Webpack Client", "Build UI Tests", "Watch server unit tests"] 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | **/* 2 | !language-configuration.json 3 | !LICENSE 4 | !package.json 5 | !README.md 6 | !client/dist/**/*.js 7 | !imgs/PQIcon_256.png 8 | !server/dist/**/*.js 9 | !syntaxes/*.json 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 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 | [![](https://vsmarketplacebadges.dev/version-short/PowerQuery.vscode-powerquery.png)](https://marketplace.visualstudio.com/items?itemName=PowerQuery.vscode-powerquery) 2 | [![](https://vsmarketplacebadges.dev/installs-short/PowerQuery.vscode-powerquery.png)](https://marketplace.visualstudio.com/items?itemName=PowerQuery.vscode-powerquery) 3 | 4 | # Power Query language service for VS Code 5 | 6 | Available in the [Visual Studio Code Marketplace](https://marketplace.visualstudio.com/items?itemName=PowerQuery.vscode-powerquery). Provides a language service for the [Power Query / M formula language](https://powerquery.microsoft.com/) with the following capabilities: 7 | 8 | ## Fuzzy autocomplete 9 | 10 | Suggests keywords, local variables, and the standard Power Query library. 11 | 12 | ![Fuzzy autocomplete](imgs/fuzzyAutocomplete.gif) 13 | 14 | ## Hover 15 | 16 | ![On hover](imgs/hover.png) 17 | 18 | ## Function hints 19 | 20 | Displays function documentation if it exists, and validates the types for function arguments. 21 | 22 | ![Parameter hints](imgs/parameterHints.png) 23 | 24 | ## Code formatting 25 | 26 | Provides a formatter for the "Format Document" (Alt + Shift + F) command. 27 | 28 | ![Format Document](imgs/formatDocument.gif) 29 | 30 | ## Commands 31 | 32 | ### String encoding/decoding 33 | 34 | These commands can be used to add/remove M and JSON string formatting to/from the currently selected text. This can be helpful when you need to encode an embedded SQL (or other) query in an M expression, or when you're working with files that contain embedded M expressions, such as Power BI Dataflow's [model.json](https://docs.microsoft.com/en-us/common-data-model/model-json) file, and Power Query traces. There is a `powerquery.editor.transformTarget` setting in the extension to choose the target for the operation. `inPlace` (the default) replaces the currently selected text with the updated value. `clipboard` does not change the currently selected text, and puts the transformed text on the clipboard. 35 | 36 | These commands require one or more text selections in the active editor window. 37 | 38 | ![Decode/Encode JSON string](imgs/jsonDecodeEncode.png) 39 | 40 | | Command | Label | 41 | | --------------------------- | ------------------------------------------ | 42 | | powerquery.jsonEscapeText | Encode selection as JSON string | 43 | | powerquery.jsonUnescapeText | Remove JSON string encoding from selection | 44 | | powerquery.mEscapeText | Encode selection as an M text value | 45 | | powerquery.mUnescapeText | Remove M text encoding from selection | 46 | 47 | A more specialized version of this command will extract the M Document from an entire model.json/dataflow.json document. This command requires the active document to be recognized as JSON. The result is a new PowerQuery document. 48 | 49 | | Command | Label | 50 | | ---------------------------------- | ---------------------------------- | 51 | | powerquery.extractDataflowDocument | Extract M document from model.json | 52 | 53 | ## Related projects 54 | 55 | - [powerquery-parser](https://github.com/microsoft/powerquery-parser): A lexer + parser for Power Query. Also contains features such as type validation. 56 | - [powerquery-formatter](https://github.com/microsoft/powerquery-formatter): A code formatter for Power Query which is bundled in the VSCode extension. 57 | - [powerquery-language-services](https://github.com/microsoft/powerquery-language-services): A high level library that wraps the parser for external projects, such as the VSCode extension. Includes features such as Intellisense. 58 | 59 | ## How to build 60 | 61 | Install dependencies: 62 | 63 | ```cmd 64 | npm install 65 | ``` 66 | 67 | Build the project: 68 | 69 | ```cmd 70 | npm run build 71 | ``` 72 | 73 | Generate vsix package: 74 | 75 | ```cmd 76 | npm run vsix 77 | ``` 78 | 79 | The .vsix can be installed into VS Code from the commandline: 80 | 81 | ```cmd 82 | code --install-extension vscode-powerquery-*.vsix 83 | ``` 84 | 85 | ## Testing 86 | 87 | There are two test suites: 88 | 89 | 1. Server unit tests - `mocha` based, no build dependency. 90 | 2. Client UI test - `vscode/test-electron` based, requires webpacked test suite. 91 | 92 | ### Running tests from the command line 93 | 94 | To run all tests: 95 | 96 | ```cmd 97 | npm run webpack-prod 98 | npm run test 99 | ``` 100 | 101 | To run server unit tests only: 102 | 103 | ```cmd 104 | npm run test:server 105 | ``` 106 | 107 | ### Running tests from VS Code 108 | 109 | Run one of the following Debug/Launch profiles: 110 | 111 | 1. Run server unit tests 112 | 2. Language UI Test 113 | 114 | > If you receive errors related to missing problem matchers, please ensure you have the [TypeScript + Webpack Problem Matchers](https://marketplace.visualstudio.com/items?itemName=amodio.tsl-problem-matcher) vscode extension installed. 115 | 116 | ## Contributing 117 | 118 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 119 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 120 | the rights to use your contribution. For details, visit . 121 | 122 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 123 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 124 | provided by the bot. You will only need to do this once across all repos using our CLA. 125 | 126 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 127 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 128 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 129 | 130 | ## Trademarks 131 | 132 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 133 | trademarks or logos is subject to and must follow 134 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). 135 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 136 | Any use of third-party trademarks or logos are subject to those third-party's policies. 137 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /client/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | **/*.js -------------------------------------------------------------------------------- /client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: false, 3 | }; 4 | -------------------------------------------------------------------------------- /client/.vscode-test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | // Guidance from https://code.visualstudio.com/api/working-with-extensions/testing-extension 5 | const { defineConfig } = require('@vscode/test-cli'); 6 | 7 | module.exports = defineConfig([ 8 | { 9 | label: "UI Tests", 10 | files: "lib/test/**/*.test.js", 11 | workspaceFolder: "src/test/testFixture", 12 | extensionDevelopmentPath: "..", 13 | launchArgs: ["--profile-temp", "--disable-extensions"], 14 | 15 | mocha: { 16 | color: true, 17 | ui: "tdd", 18 | timeout: 20000, 19 | slow: 10000, 20 | // TODO: Using mocha-multi-reporters breaks the VS Code test runner. All tests start reporting "Test process exited unexpectedly". 21 | // reporter: "mocha-multi-reporters", 22 | // reporterOptions: { 23 | // reporterEnabled: "spec, mocha-junit-reporter", 24 | // mochaJunitReporterReporterOptions: { 25 | // mochaFile: "test-results.xml", 26 | // }, 27 | // } 28 | } 29 | } 30 | ]); -------------------------------------------------------------------------------- /client/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "recommendations": [ 4 | "esbenp.prettier-vscode" 5 | ], 6 | "unwantedRecommendations": [] 7 | } 8 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-powerquery-client", 3 | "version": "0.0.61", 4 | "description": "VS Code part of language server", 5 | "author": "Microsoft Corporation", 6 | "license": "MIT", 7 | "homepage": "https://github.com/microsoft/vscode-powerquery#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/microsoft/vscode-powerquery.git", 11 | "directory": "client" 12 | }, 13 | "issues": { 14 | "url": "https://github.com/microsoft/vscode-powerquery/issues" 15 | }, 16 | "scripts": { 17 | "init": "npm install-clean", 18 | "build": ".\\node_modules\\.bin\\tsc", 19 | "watch": ".\\node_modules\\.bin\\tsc -watch", 20 | "test": "vscode-test", 21 | "link:start": "npm link && npm uninstall @microsoft/powerquery-parser @microsoft/powerquery-language-services && git clean -xdf && npm install && npm link @microsoft/powerquery-parser @microsoft/powerquery-language-services", 22 | "link:stop": "npm unlink @microsoft/powerquery-parser @microsoft/powerquery-language-services && git clean -xdf && npm install && npm install @microsoft/powerquery-parser@latest @microsoft/powerquery-language-services@latest --save-exact", 23 | "lint": "eslint src --ext ts", 24 | "webpack-prod": "node_modules\\.bin\\webpack --mode production", 25 | "webpack-dev": "node_modules\\.bin\\webpack --watch --mode development" 26 | }, 27 | "main": "lib\\extension", 28 | "engines": { 29 | "node": ">=18.17.0", 30 | "vscode": "^1.87.0" 31 | }, 32 | "dependencies": { 33 | "@microsoft/powerquery-language-services": "0.10.1", 34 | "@microsoft/powerquery-parser": "0.15.10", 35 | "vscode-languageclient": "9.0.1" 36 | }, 37 | "devDependencies": { 38 | "@types/chai": "4.3.1", 39 | "@types/glob": "7.2.0", 40 | "@types/mocha": "10.0.6", 41 | "@types/node": "20.12.12", 42 | "@types/vscode": "1.87.0", 43 | "@vscode/test-cli": "^0.0.9", 44 | "@vscode/test-electron": "^2.3.10", 45 | "chai": "4.3.6", 46 | "eslint": "8.15.0", 47 | "eslint-config-prettier": "8.5.0", 48 | "eslint-plugin-prettier": "4.0.0", 49 | "eslint-plugin-security": "1.5.0", 50 | "glob": "8.0.3", 51 | "mocha": "10.4.0", 52 | "mocha-junit-reporter": "2.2.1", 53 | "mocha-multi-reporters": "1.5.1", 54 | "npm-check-updates": "16.1.0", 55 | "prettier": "2.6.2", 56 | "ts-loader": "9.5.1", 57 | "ts-node": "10.9.2", 58 | "typescript": "5.4.5", 59 | "webpack": "5.95.0", 60 | "webpack-cli": "5.1.4" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /client/src/commands.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQP from "@microsoft/powerquery-parser"; 5 | import * as vscode from "vscode"; 6 | import path = require("path"); 7 | import { DataflowModel } from "./dataflowModel"; 8 | 9 | function getSetting(config: string, setting: string): string { 10 | const value: string | undefined = vscode.workspace.getConfiguration(config).get(setting); 11 | 12 | if (value == undefined) { 13 | return ""; 14 | } 15 | 16 | return value; 17 | } 18 | 19 | async function processText( 20 | textEditor: vscode.TextEditor, 21 | edit: vscode.TextEditorEdit, 22 | processingFunction: (selection: string) => string, 23 | ): Promise { 24 | const selectionSeparator: string = "\n-----------------------------\n"; 25 | const target: string = getSetting("powerquery.editor", "transformTarget"); 26 | 27 | try { 28 | let textForClipboard: string = ""; 29 | 30 | switch (target) { 31 | case "clipboard": 32 | textEditor.selections.forEach(async (selection: vscode.Selection) => { 33 | try { 34 | const replacement: string = processingFunction(textEditor.document.getText(selection)); 35 | 36 | if (textForClipboard.length > 0) { 37 | textForClipboard += selectionSeparator; 38 | } 39 | 40 | textForClipboard += replacement; 41 | } catch (err) { 42 | await vscode.window.showErrorMessage(`Failed to transform text. Error: ${err}`); 43 | } 44 | }); 45 | 46 | await vscode.env.clipboard.writeText(textForClipboard); 47 | break; 48 | case "inPlace": 49 | default: 50 | textEditor.selections.forEach(async (selection: vscode.Selection) => { 51 | try { 52 | const replacement: string = processingFunction(textEditor.document.getText(selection)); 53 | edit.replace(selection, replacement); 54 | } catch (err) { 55 | await vscode.window.showErrorMessage(`Failed to transform text. Error: ${err}`); 56 | } 57 | }); 58 | } 59 | } catch (err) { 60 | await vscode.window.showErrorMessage(`Failed to transform text to ${target}. Error: ${err}`); 61 | } 62 | } 63 | 64 | // https://docs.microsoft.com/en-us/powerquery-m/m-spec-lexical-structure#character-escape-sequences 65 | 66 | // TODO: Support arbitrary escape sequence lists: #(cr,cr,cr) 67 | export async function escapeMText(textEditor: vscode.TextEditor, edit: vscode.TextEditorEdit): Promise { 68 | await processText(textEditor, edit, PQP.Language.TextUtils.escape); 69 | } 70 | 71 | export async function unescapeMText(textEditor: vscode.TextEditor, edit: vscode.TextEditorEdit): Promise { 72 | await processText(textEditor, edit, PQP.Language.TextUtils.unescape); 73 | } 74 | 75 | export async function escapeJsonText(textEditor: vscode.TextEditor, edit: vscode.TextEditorEdit): Promise { 76 | await processText(textEditor, edit, JSON.stringify); 77 | } 78 | 79 | export async function unescapeJsonText(textEditor: vscode.TextEditor, edit: vscode.TextEditorEdit): Promise { 80 | await processText(textEditor, edit, removeJsonEncoding); 81 | } 82 | 83 | export async function extractDataflowDocument(): Promise { 84 | const textEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; 85 | 86 | if (!textEditor) { 87 | return undefined; 88 | } 89 | 90 | let dataflow: DataflowModel; 91 | 92 | try { 93 | dataflow = JSON.parse(textEditor.document.getText()); 94 | } catch (err) { 95 | await vscode.window.showErrorMessage(`Failed to parse document. Error: ${JSON.stringify(err)}`); 96 | 97 | return undefined; 98 | } 99 | 100 | if (!dataflow || !dataflow["pbi:mashup"]?.document) { 101 | await vscode.window.showErrorMessage(`Failed to parse document as a dataflow.json model`); 102 | 103 | return undefined; 104 | } 105 | 106 | const mashupDocument: string = dataflow["pbi:mashup"].document; 107 | 108 | const headerComments: string[] = [ 109 | `// name: ${dataflow.name}`, 110 | `// dataflowId: ${dataflow["ppdf:dataflowId"]}`, 111 | `// modifiedTime: ${dataflow.modifiedTime}`, 112 | ]; 113 | 114 | const content: string = `${headerComments.join("\r\n")}\r\n${mashupDocument}`; 115 | 116 | const workspaceRoot: string = path.dirname(textEditor.document.fileName); 117 | 118 | const currentEditorFileName: string = path.basename( 119 | textEditor.document.fileName, 120 | path.extname(textEditor.document.fileName), 121 | ); 122 | 123 | const newFileUri: vscode.Uri = vscode.Uri.parse( 124 | `untitled:${path.join(workspaceRoot, `${currentEditorFileName}.pq`)}`, 125 | ); 126 | 127 | const document: vscode.TextDocument = await vscode.workspace.openTextDocument(newFileUri); 128 | const contentEdit: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); 129 | contentEdit.insert(document.uri, new vscode.Position(0, 0), content); 130 | await vscode.workspace.applyEdit(contentEdit); 131 | 132 | // TODO: Can this be read from user settings/preferences? 133 | // The format command returns an error if we don't pass in any options. 134 | const formattingOptions: vscode.FormattingOptions = { 135 | tabSize: 4, 136 | insertSpaces: true, 137 | }; 138 | 139 | const textEdits: vscode.TextEdit[] = await vscode.commands.executeCommand( 140 | "vscode.executeFormatDocumentProvider", 141 | document.uri, 142 | formattingOptions, 143 | ); 144 | 145 | const formatEdit: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); 146 | formatEdit.set(document.uri, textEdits as vscode.TextEdit[]); 147 | 148 | await vscode.workspace.applyEdit(formatEdit); 149 | 150 | await vscode.window.showTextDocument(document); 151 | 152 | return document.uri; 153 | } 154 | 155 | function removeJsonEncoding(text: string): string { 156 | return JSON.parse(text); 157 | } 158 | -------------------------------------------------------------------------------- /client/src/constants.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | export const enum CommandConstant { 5 | ExtractDataflowDocument = "powerquery.extractDataflowDocument", 6 | EscapeJsonText = "powerquery.jsonEscapeText", 7 | EscapeMText = "powerquery.mEscapeText", 8 | UnescapeJsonText = "powerquery.jsonUnescapeText", 9 | UnescapeMText = "powerquery.mUnescapeText", 10 | } 11 | 12 | export const enum ConfigurationConstant { 13 | AdditionalSymbolsDirectories = "additionalSymbolsDirectories", 14 | BasePath = "powerquery.client", 15 | } 16 | -------------------------------------------------------------------------------- /client/src/dataflowModel.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | // Stripped down presentation of the dataflow.json format 5 | 6 | export interface DataflowModel { 7 | name: string; 8 | "ppdf:dataflowId": string; 9 | culture: string; 10 | modifiedTime: Date; 11 | "pbi:mashup": Mashup; 12 | } 13 | 14 | export interface Mashup { 15 | document: string; 16 | } 17 | -------------------------------------------------------------------------------- /client/src/extension.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as LC from "vscode-languageclient/node"; 5 | import * as path from "path"; 6 | import * as vscode from "vscode"; 7 | 8 | import * as CommandFn from "./commands"; 9 | import * as Subscriptions from "./subscriptions"; 10 | import { CommandConstant, ConfigurationConstant } from "./constants"; 11 | import { LibrarySymbolClient } from "./librarySymbolClient"; 12 | import { LibrarySymbolManager } from "./librarySymbolManager"; 13 | import { PowerQueryApi } from "./powerQueryApi"; 14 | 15 | let client: LC.LanguageClient; 16 | let librarySymbolClient: LibrarySymbolClient; 17 | let librarySymbolManager: LibrarySymbolManager; 18 | 19 | export async function activate(context: vscode.ExtensionContext): Promise { 20 | // Register commands 21 | context.subscriptions.push( 22 | vscode.commands.registerCommand(CommandConstant.ExtractDataflowDocument, CommandFn.extractDataflowDocument), 23 | vscode.commands.registerTextEditorCommand(CommandConstant.EscapeJsonText, CommandFn.escapeJsonText), 24 | vscode.commands.registerTextEditorCommand(CommandConstant.EscapeMText, CommandFn.escapeMText), 25 | vscode.commands.registerTextEditorCommand(CommandConstant.UnescapeJsonText, CommandFn.unescapeJsonText), 26 | vscode.commands.registerTextEditorCommand(CommandConstant.UnescapeMText, CommandFn.unescapeMText), 27 | ); 28 | 29 | // The server is implemented in node 30 | const serverModule: string = context.asAbsolutePath(path.join("server", "dist", "server.js")); 31 | // The debug options for the server 32 | // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging 33 | const debugOptions: LC.ForkOptions = { execArgv: ["--nolazy", "--inspect=6009"] }; 34 | 35 | // If the extension is launched in debug mode then the debug server options are used 36 | // Otherwise the run options are used 37 | const serverOptions: LC.ServerOptions = { 38 | run: { module: serverModule, transport: LC.TransportKind.ipc }, 39 | debug: { 40 | module: serverModule, 41 | transport: LC.TransportKind.ipc, 42 | options: debugOptions, 43 | }, 44 | }; 45 | 46 | // Options to control the language client 47 | const clientOptions: LC.LanguageClientOptions = { 48 | // Register the server for plain text documents 49 | documentSelector: [ 50 | { scheme: "file", language: "powerquery" }, 51 | { scheme: "untitled", language: "powerquery" }, 52 | ], 53 | }; 54 | 55 | // Create the language client and start the client. 56 | client = new LC.LanguageClient("powerquery", "Power Query", serverOptions, clientOptions); 57 | 58 | // Start the client. This will also launch the server. 59 | await client.start(); 60 | 61 | // TODO: Move this to the LSP based API. 62 | context.subscriptions.push( 63 | vscode.languages.registerDocumentSemanticTokensProvider( 64 | { language: "powerquery" }, 65 | Subscriptions.createDocumentSemanticTokensProvider(client), 66 | Subscriptions.SemanticTokensLegend, 67 | ), 68 | ); 69 | 70 | librarySymbolClient = new LibrarySymbolClient(client); 71 | librarySymbolManager = new LibrarySymbolManager(librarySymbolClient, client); 72 | 73 | await configureSymbolDirectories(); 74 | 75 | context.subscriptions.push( 76 | vscode.workspace.onDidChangeConfiguration(async (event: vscode.ConfigurationChangeEvent) => { 77 | const symbolDirs: string = ConfigurationConstant.BasePath.concat( 78 | ".", 79 | ConfigurationConstant.AdditionalSymbolsDirectories, 80 | ); 81 | 82 | if (event.affectsConfiguration(symbolDirs)) { 83 | await configureSymbolDirectories(); 84 | } 85 | }), 86 | ); 87 | 88 | return Object.freeze(librarySymbolClient); 89 | } 90 | 91 | export function deactivate(): Thenable | undefined { 92 | return client?.stop(); 93 | } 94 | 95 | async function configureSymbolDirectories(): Promise { 96 | const config: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(ConfigurationConstant.BasePath); 97 | 98 | const additionalSymbolsDirectories: string[] | undefined = config.get( 99 | ConfigurationConstant.AdditionalSymbolsDirectories, 100 | ); 101 | 102 | // TODO: Should we fix/remove invalid and malformed directory path values? 103 | // For example, a quoted path "c:\path\to\file" will be considered invalid and reported as an error. 104 | // We could modify values and write them back to the original config locations. 105 | 106 | await librarySymbolManager.refreshSymbolDirectories(additionalSymbolsDirectories ?? []); 107 | 108 | // TODO: Configure file system watchers to detect library file changes. 109 | } 110 | -------------------------------------------------------------------------------- /client/src/funcUtils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | export function debounce void>(fn: F, timeout: number): F; 5 | 6 | export function debounce( 7 | fn: (this: This, ...args: Parameters) => void, 8 | timeout: number, 9 | ): (this: This, ...args: Parameters) => void { 10 | let _args: Parameters; 11 | let _this: This; 12 | 13 | let triggerId: ReturnType; 14 | 15 | return startTimer; 16 | 17 | function startTimer(this: This, ...args: Parameters): void { 18 | _args = args; 19 | // eslint-disable-next-line no-invalid-this, @typescript-eslint/no-this-alias 20 | _this = this; 21 | clearTimeout(triggerId); 22 | triggerId = setTimeout(execute, timeout); 23 | } 24 | 25 | function execute(): void { 26 | fn.apply(_this, _args); 27 | } 28 | } 29 | 30 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 31 | type NoInfer = [T][T extends any ? 0 : never]; 32 | 33 | export function partitionFn( 34 | fnGenerator: (this: NoInfer, ...args: NoInfer) => (this: This, ...args: Parameters) => ReturnType, 35 | keyGenerator: (this: This, ...args: Parameters) => unknown, 36 | ): (this: This, ...args: Parameters) => ReturnType { 37 | const cache: Map ReturnType> = new Map< 38 | unknown, 39 | (this: This, ...args: Parameters) => ReturnType 40 | >(); 41 | 42 | return function (this: This, ...args: Parameters): ReturnType { 43 | // eslint-disable-next-line no-invalid-this 44 | const key: unknown = keyGenerator.apply(this, args); 45 | 46 | // Attempt to reuse a pre-generated function 47 | const fn: ((this: This, ...args: Parameters) => ReturnType) | undefined = cache.get(key); 48 | 49 | if (fn) { 50 | // eslint-disable-next-line no-invalid-this 51 | return fn.apply(this, args); 52 | } else { 53 | // Otherwise, make a new one and cache it 54 | // eslint-disable-next-line no-invalid-this 55 | const fn: (this: This, ...args: Parameters) => ReturnType = fnGenerator.apply(this, args); 56 | cache.set(key, fn); 57 | 58 | // eslint-disable-next-line no-invalid-this 59 | return fn.apply(this, args); 60 | } 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /client/src/librarySymbolClient.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as LC from "vscode-languageclient/node"; 5 | import * as vscode from "vscode"; 6 | 7 | import { LibraryJson, PowerQueryApi } from "./powerQueryApi"; 8 | 9 | // Minimal implementation to faciliate unit testing 10 | export type MinimalPowerQueryLanguageServiceClient = Pick< 11 | LC.BaseLanguageClient, 12 | "sendRequest" | "isRunning" | "info" | "error" 13 | >; 14 | 15 | // We might need to rename/refactor this in the future if the exported API has functions unrelated to symbols. 16 | export class LibrarySymbolClient implements PowerQueryApi { 17 | constructor(private lsClient: MinimalPowerQueryLanguageServiceClient) {} 18 | 19 | public onModuleLibraryUpdated(workspaceUriPath: string, library: LibraryJson): void { 20 | if (this.lsClient.isRunning()) { 21 | this.lsClient.info("Calling powerquery/moduleLibraryUpdated"); 22 | 23 | void this.lsClient.sendRequest("powerquery/moduleLibraryUpdated", { 24 | workspaceUriPath, 25 | library, 26 | }); 27 | } else { 28 | this.lsClient.error("Received moduleLibraryUpdated call but client is not running.", undefined, false); 29 | } 30 | } 31 | 32 | public async addLibrarySymbols( 33 | librarySymbols: ReadonlyMap, 34 | token?: vscode.CancellationToken, 35 | ): Promise { 36 | if (this.lsClient.isRunning()) { 37 | // The JSON-RPC libraries don't support sending maps, so we convert it to a tuple array. 38 | const librarySymbolsTuples: ReadonlyArray<[string, LibraryJson | null]> = Array.from( 39 | librarySymbols.entries(), 40 | ); 41 | 42 | await this.lsClient.sendRequest( 43 | "powerquery/addLibrarySymbols", 44 | { 45 | librarySymbols: librarySymbolsTuples, 46 | }, 47 | token, 48 | ); 49 | } else { 50 | this.lsClient.error("Received addLibrarySymbols call but client is not running.", undefined, false); 51 | } 52 | } 53 | 54 | public async removeLibrarySymbols( 55 | librariesToRemove: ReadonlyArray, 56 | token?: vscode.CancellationToken, 57 | ): Promise { 58 | if (this.lsClient.isRunning()) { 59 | await this.lsClient.sendRequest( 60 | "powerquery/removeLibrarySymbols", 61 | { 62 | librariesToRemove, 63 | }, 64 | token, 65 | ); 66 | } else { 67 | this.lsClient.error("Received removeLibrarySymbols call but client is not running.", undefined, false); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /client/src/librarySymbolManager.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as LC from "vscode-languageclient/node"; 5 | import * as path from "path"; 6 | import * as vscode from "vscode"; 7 | 8 | import * as LibrarySymbolUtils from "./librarySymbolUtils"; 9 | 10 | import { LibraryJson, PowerQueryApi } from "./powerQueryApi"; 11 | 12 | export type MinimalClientTrace = Pick; 13 | export type MinimalFileSystem = Pick; 14 | 15 | export class LibrarySymbolManager { 16 | private static readonly ErrorMessagePrefix: string = 17 | "Error processing symbol directory path. Please update your configuration."; 18 | private static readonly SymbolFileExtension: string = ".json"; 19 | private static readonly SymbolFileEncoding: string = "utf-8"; 20 | 21 | private readonly fs: vscode.FileSystem; 22 | private readonly registeredSymbolModules: string[] = []; 23 | 24 | constructor( 25 | private librarySymbolClient: PowerQueryApi, 26 | private clientTrace?: MinimalClientTrace, 27 | fs?: vscode.FileSystem, 28 | ) { 29 | this.fs = fs ?? vscode.workspace.fs; 30 | } 31 | 32 | public async refreshSymbolDirectories(directories: ReadonlyArray): Promise> { 33 | await this.clearAllRegisteredSymbolModules(); 34 | 35 | if (!directories || directories.length === 0) { 36 | return []; 37 | } 38 | 39 | // Fetch the full list of files to process. 40 | const fileDiscoveryActions: Promise>[] = []; 41 | 42 | const normalizedDirectoryUris: ReadonlyArray = directories.map((directory: string) => { 43 | const normalized: string = path.normalize(directory); 44 | 45 | if (directory !== normalized) { 46 | this.clientTrace?.info(`Normalized symbol file path '${directory}' => '${normalized}'`); 47 | } 48 | 49 | return vscode.Uri.file(normalized); 50 | }); 51 | 52 | const dedupedDirectoryUris: ReadonlyArray = Array.from(new Set(normalizedDirectoryUris)); 53 | 54 | for (const uri of dedupedDirectoryUris) { 55 | fileDiscoveryActions.push(this.getSymbolFilesFromDirectory(uri)); 56 | } 57 | 58 | // TODO: check for duplicate module file names and only take the last one. 59 | // This would allow a connector developer to override a symbol library generated 60 | // with an older version of their connector. 61 | const symbolFileActions: Promise<[vscode.Uri, LibraryJson] | undefined>[] = []; 62 | const files: ReadonlyArray = (await Promise.all(fileDiscoveryActions)).flat(); 63 | 64 | for (const fileUri of files) { 65 | symbolFileActions.push(this.processSymbolFile(fileUri)); 66 | } 67 | 68 | if (symbolFileActions.length === 0) { 69 | this.clientTrace?.info( 70 | `No symbol files (${LibrarySymbolManager.SymbolFileExtension}) found in symbol file directories.`, 71 | ); 72 | 73 | return []; 74 | } 75 | 76 | // Process all symbol files, filtering out any that failed to load. 77 | const allSymbolFiles: ReadonlyArray<[vscode.Uri, LibraryJson]> = (await Promise.all(symbolFileActions)).filter( 78 | (value: [vscode.Uri, LibraryJson] | undefined) => value !== undefined, 79 | ) as ReadonlyArray<[vscode.Uri, LibraryJson]>; 80 | 81 | const validSymbolLibraries: Map = new Map(); 82 | 83 | for (const [uri, library] of allSymbolFiles) { 84 | const moduleName: string = LibrarySymbolManager.getModuleNameFromFileUri(uri); 85 | validSymbolLibraries.set(moduleName, library); 86 | } 87 | 88 | this.clientTrace?.info(`Registering symbol files. Total file count: ${validSymbolLibraries.size}`); 89 | 90 | if (validSymbolLibraries.size > 0) { 91 | await this.librarySymbolClient 92 | .addLibrarySymbols(validSymbolLibraries) 93 | .then(() => this.registeredSymbolModules.push(...validSymbolLibraries.keys())); 94 | } 95 | 96 | return this.registeredSymbolModules; 97 | } 98 | 99 | public async getSymbolFilesFromDirectory(directory: vscode.Uri): Promise> { 100 | let isDirectoryValid: boolean = false; 101 | 102 | try { 103 | const stat: vscode.FileStat = await this.fs.stat(directory); 104 | 105 | if (stat.type !== vscode.FileType.Directory) { 106 | this.clientTrace?.error( 107 | `${LibrarySymbolManager.ErrorMessagePrefix} '${directory.toString()}' is not a directory.`, 108 | JSON.stringify(stat), 109 | ); 110 | } else { 111 | isDirectoryValid = true; 112 | } 113 | } catch (error: unknown) { 114 | this.clientTrace?.error( 115 | `${LibrarySymbolManager.ErrorMessagePrefix} Exception while processing '${directory.toString()}'.`, 116 | error, 117 | ); 118 | } 119 | 120 | if (!isDirectoryValid) { 121 | return []; 122 | } 123 | 124 | const files: [string, vscode.FileType][] = await this.fs.readDirectory(directory); 125 | 126 | // We only want .json files. 127 | return files 128 | .map((value: [string, vscode.FileType]): vscode.Uri | undefined => { 129 | const fileName: string = value[0]; 130 | 131 | if ( 132 | value[1] === vscode.FileType.File && 133 | fileName.toLocaleLowerCase().endsWith(LibrarySymbolManager.SymbolFileExtension) 134 | ) { 135 | return vscode.Uri.joinPath(directory, fileName); 136 | } 137 | 138 | return undefined; 139 | }) 140 | .filter((value: vscode.Uri | undefined) => value !== undefined) as vscode.Uri[]; 141 | } 142 | 143 | public async processSymbolFile(fileUri: vscode.Uri): Promise<[vscode.Uri, LibraryJson] | undefined> { 144 | try { 145 | const contents: Uint8Array = await this.fs.readFile(fileUri); 146 | const text: string = new TextDecoder(LibrarySymbolManager.SymbolFileEncoding).decode(contents); 147 | 148 | const library: LibraryJson = LibrarySymbolUtils.parseLibraryJson(text); 149 | 150 | this.clientTrace?.debug(`Loaded symbol file '${fileUri.toString()}'. Symbol count: ${library.length}`); 151 | 152 | return [fileUri, library]; 153 | } catch (error: unknown) { 154 | this.clientTrace?.error( 155 | `${ 156 | LibrarySymbolManager.ErrorMessagePrefix 157 | } Error processing '${fileUri.toString()}' as symbol library.`, 158 | error, 159 | ); 160 | } 161 | 162 | return undefined; 163 | } 164 | 165 | private static getModuleNameFromFileUri(fileUri: vscode.Uri): string { 166 | return path.basename(fileUri.fsPath, LibrarySymbolManager.SymbolFileExtension); 167 | } 168 | 169 | private async clearAllRegisteredSymbolModules(): Promise { 170 | if (this.registeredSymbolModules.length === 0) { 171 | return; 172 | } 173 | 174 | await this.librarySymbolClient 175 | .removeLibrarySymbols(this.registeredSymbolModules) 176 | .then(() => (this.registeredSymbolModules.length = 0)); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /client/src/librarySymbolUtils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import { 5 | LibraryJson, 6 | LibrarySymbol, 7 | LibrarySymbolDocumentation, 8 | LibrarySymbolFunctionParameter, 9 | LibrarySymbolRecordField, 10 | } from "./powerQueryApi"; 11 | 12 | export function parseLibraryJson(json: string): LibraryJson { 13 | const parsed: unknown = JSON.parse(json); 14 | assertLibraryJson(parsed); 15 | 16 | return parsed; 17 | } 18 | 19 | function assertLibraryJson(json: unknown): asserts json is LibraryJson { 20 | assertIsArray(json); 21 | json.forEach(assertLibrarySymbol); 22 | } 23 | 24 | function assertLibrarySymbol(symbol: unknown): asserts symbol is LibrarySymbol { 25 | assertIsObject(symbol); 26 | assertHasProperty(symbol, "name", "string"); 27 | assertHasProperty(symbol, "documentation", "object", true); 28 | assertHasProperty(symbol, "functionParameters", "object", true); 29 | assertHasProperty(symbol, "completionItemKind", "number"); 30 | assertHasProperty(symbol, "isDataSource", "boolean"); 31 | assertHasProperty(symbol, "type", "string"); 32 | 33 | const librarySymbol: LibrarySymbol = symbol as LibrarySymbol; 34 | 35 | if (librarySymbol.documentation !== null && librarySymbol.documentation !== undefined) { 36 | assertLibrarySymbolDocumentation(librarySymbol.documentation); 37 | } 38 | 39 | if (librarySymbol.functionParameters !== null && librarySymbol.functionParameters !== undefined) { 40 | assertIsArray(librarySymbol.functionParameters); 41 | librarySymbol.functionParameters.forEach(assertLibrarySymbolFunctionParameter); 42 | } 43 | } 44 | 45 | function assertLibrarySymbolDocumentation(doc: unknown): asserts doc is LibrarySymbolDocumentation { 46 | assertIsObject(doc); 47 | assertHasProperty(doc, "description", "string", true); 48 | assertHasProperty(doc, "longDescription", "string", true); 49 | } 50 | 51 | function assertLibrarySymbolFunctionParameter(param: unknown): asserts param is LibrarySymbolFunctionParameter { 52 | assertIsObject(param); 53 | assertHasProperty(param, "name", "string"); 54 | assertHasProperty(param, "type", "string"); 55 | assertHasProperty(param, "isRequired", "boolean"); 56 | assertHasProperty(param, "isNullable", "boolean"); 57 | assertHasProperty(param, "caption", "string", true); 58 | assertHasProperty(param, "description", "string", true); 59 | assertHasProperty(param, "sampleValues", "object", true); 60 | assertHasProperty(param, "allowedValues", "object", true); 61 | assertHasProperty(param, "defaultValue", "object", true); 62 | assertHasProperty(param, "fields", "object", true); 63 | assertHasProperty(param, "enumNames", "object", true); 64 | assertHasProperty(param, "enumCaptions", "object", true); 65 | 66 | const functionParam: LibrarySymbolFunctionParameter = param as LibrarySymbolFunctionParameter; 67 | 68 | if (functionParam.sampleValues !== null && functionParam.sampleValues !== undefined) { 69 | assertIsArray(functionParam.sampleValues); 70 | } 71 | 72 | if (functionParam.allowedValues !== null && functionParam.allowedValues !== undefined) { 73 | assertIsArray(functionParam.allowedValues); 74 | } 75 | 76 | if (functionParam.fields !== null && functionParam.fields !== undefined) { 77 | assertIsArray(functionParam.fields); 78 | functionParam.fields.forEach(assertLibrarySymbolRecordField); 79 | } 80 | 81 | if (functionParam.enumNames !== null && functionParam.enumNames !== undefined) { 82 | assertIsArray(functionParam.enumNames); 83 | } 84 | 85 | if (functionParam.enumCaptions !== null && functionParam.enumCaptions !== undefined) { 86 | assertIsArray(functionParam.enumCaptions); 87 | } 88 | } 89 | 90 | function assertLibrarySymbolRecordField(field: unknown): asserts field is LibrarySymbolRecordField { 91 | assertIsObject(field); 92 | assertHasProperty(field, "name", "string"); 93 | assertHasProperty(field, "type", "string"); 94 | assertHasProperty(field, "isRequired", "boolean"); 95 | assertHasProperty(field, "caption", "string", true); 96 | assertHasProperty(field, "description", "string", true); 97 | } 98 | 99 | // Helper functions 100 | function assertIsArray(value: unknown): asserts value is Array { 101 | if (!Array.isArray(value)) { 102 | throw new TypeError("Expected an array"); 103 | } 104 | } 105 | 106 | function assertIsObject(value: unknown): asserts value is object { 107 | if (typeof value !== "object" || value === null) { 108 | throw new TypeError("Expected an object"); 109 | } 110 | } 111 | 112 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 113 | function assertHasProperty(obj: any, propName: string, type: string, optional: boolean = false): void { 114 | if (!(propName in obj)) { 115 | if (!optional) { 116 | throw new TypeError(`Missing property: ${propName}`); 117 | } 118 | } else if (typeof obj[propName] !== type && obj[propName] !== null && obj[propName] !== undefined) { 119 | throw new TypeError(`Expected property type ${type} for property ${propName}`); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /client/src/powerQueryApi.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import type * as PQLS from "@microsoft/powerquery-language-services"; 5 | 6 | export type LibrarySymbol = PQLS.LibrarySymbol.LibrarySymbol; 7 | export type LibrarySymbolDocumentation = PQLS.LibrarySymbol.LibrarySymbolDocumentation; 8 | export type LibrarySymbolFunctionParameter = PQLS.LibrarySymbol.LibrarySymbolFunctionParameter; 9 | export type LibrarySymbolRecordField = PQLS.LibrarySymbol.LibrarySymbolRecordField; 10 | export type LibraryJson = ReadonlyArray; 11 | 12 | export interface PowerQueryApi { 13 | readonly onModuleLibraryUpdated: (workspaceUriPath: string, library: LibraryJson) => void; 14 | readonly addLibrarySymbols: (librarySymbols: ReadonlyMap) => Promise; 15 | readonly removeLibrarySymbols: (librariesToRemove: ReadonlyArray) => Promise; 16 | } 17 | -------------------------------------------------------------------------------- /client/src/subscriptions/documentSemanticTokensProvider.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as LC from "vscode-languageclient/node"; 5 | import * as PQLS from "@microsoft/powerquery-language-services"; 6 | import * as vscode from "vscode"; 7 | 8 | import * as FuncUtils from "../funcUtils"; 9 | import { CancellationToken, TextDocument } from "vscode"; 10 | import { SemanticTokenModifiers, SemanticTokenTypes } from "vscode-languageclient/node"; 11 | 12 | export function createDocumentSemanticTokensProvider(client: LC.LanguageClient): vscode.DocumentSemanticTokensProvider { 13 | return { 14 | provideDocumentSemanticTokens: ( 15 | textDocument: TextDocument, 16 | cancellationToken: CancellationToken, 17 | ): Promise => debouncedSemanticTokenRequester(client, textDocument, cancellationToken), 18 | }; 19 | } 20 | 21 | const semanticTokenTypes: SemanticTokenTypes[] = [ 22 | SemanticTokenTypes.function, 23 | SemanticTokenTypes.keyword, 24 | SemanticTokenTypes.number, 25 | SemanticTokenTypes.operator, 26 | SemanticTokenTypes.parameter, 27 | SemanticTokenTypes.string, 28 | SemanticTokenTypes.type, 29 | SemanticTokenTypes.variable, 30 | ]; 31 | 32 | const semanticTokenModifiers: SemanticTokenModifiers[] = [ 33 | SemanticTokenModifiers.declaration, 34 | SemanticTokenModifiers.defaultLibrary, 35 | ]; 36 | 37 | export const SemanticTokensLegend: vscode.SemanticTokensLegend = new vscode.SemanticTokensLegend( 38 | semanticTokenTypes, 39 | semanticTokenModifiers, 40 | ); 41 | 42 | const debouncedSemanticTokenRequester: ( 43 | this: unknown, 44 | client: LC.LanguageClient, 45 | textDocument: TextDocument, 46 | cancellationToken: CancellationToken, 47 | ) => Promise = FuncUtils.partitionFn( 48 | () => FuncUtils.debounce(semanticTokenRequester, 250), 49 | (_client: LC.LanguageClient, textDocument: TextDocument, _cancellationToken: CancellationToken) => 50 | textDocument.uri.toString(), 51 | ); 52 | 53 | async function semanticTokenRequester( 54 | client: LC.LanguageClient, 55 | textDocument: TextDocument, 56 | cancellationToken: CancellationToken, 57 | ): Promise { 58 | const semanticTokens: PQLS.PartialSemanticToken[] = await client.sendRequest( 59 | "powerquery/semanticTokens", 60 | { 61 | textDocumentUri: textDocument.uri.toString(), 62 | cancellationToken, 63 | }, 64 | ); 65 | 66 | const tokenBuilder: vscode.SemanticTokensBuilder = new vscode.SemanticTokensBuilder(SemanticTokensLegend); 67 | 68 | for (const partialSemanticToken of semanticTokens) { 69 | tokenBuilder.push( 70 | new vscode.Range( 71 | new vscode.Position(partialSemanticToken.range.start.line, partialSemanticToken.range.start.character), 72 | new vscode.Position(partialSemanticToken.range.end.line, partialSemanticToken.range.end.character), 73 | ), 74 | partialSemanticToken.tokenType, 75 | partialSemanticToken.tokenModifiers, 76 | ); 77 | } 78 | 79 | return tokenBuilder.build(); 80 | } 81 | -------------------------------------------------------------------------------- /client/src/subscriptions/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | export * from "./documentSemanticTokensProvider"; 5 | -------------------------------------------------------------------------------- /client/src/test/suite/completion.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as vscode from "vscode"; 5 | 6 | import * as CompletionUtils from "./completionUtils"; 7 | import * as TestUtils from "./testUtils"; 8 | 9 | // See https://code.visualstudio.com/api/references/commands for full list of commands. 10 | 11 | // TODO: Add test mechanism that uses | notation and uses testUtils.setTestContent 12 | // TODO: Add test case for identifier with trailing. ex - "Access.|" 13 | 14 | suite("Access.Dat completion", () => { 15 | const docUri: vscode.Uri = TestUtils.getDocUri("completion.pq"); 16 | 17 | suiteSetup(async () => await TestUtils.closeFileIfOpen(docUri)); 18 | 19 | test("Simple completion item test", async () => 20 | await CompletionUtils.testCompletion( 21 | docUri, 22 | new vscode.Position(0, 9), 23 | { 24 | items: [{ label: "Access.Database", kind: vscode.CompletionItemKind.Function }], 25 | }, 26 | CompletionUtils.VertificationType.Contains, 27 | )); 28 | }); 29 | -------------------------------------------------------------------------------- /client/src/test/suite/completionUtils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as assert from "assert"; 5 | import * as vscode from "vscode"; 6 | 7 | import * as TestUtils from "./testUtils"; 8 | import { Commands } from "./testUtils"; 9 | 10 | export enum VertificationType { 11 | Exact, 12 | Ordered, 13 | Contains, 14 | } 15 | 16 | export async function testCompletion( 17 | docUri: vscode.Uri, 18 | position: vscode.Position, 19 | expectedCompletionList: vscode.CompletionList, 20 | vertification: VertificationType, 21 | ): Promise { 22 | await vscode.workspace.openTextDocument(docUri); 23 | 24 | const actualCompletionList: vscode.CompletionList | undefined = await testCompletionBase(docUri, position); 25 | 26 | if (actualCompletionList === undefined) { 27 | throw new Error("CompletionList is undefined"); 28 | } 29 | 30 | if (vertification === VertificationType.Exact) { 31 | assert.equal( 32 | actualCompletionList.items.length, 33 | expectedCompletionList.items.length, 34 | "expected item counts don't match", 35 | ); 36 | } else { 37 | assert( 38 | actualCompletionList.items.length >= expectedCompletionList.items.length, 39 | `received fewer items (${actualCompletionList.items.length}) than expected (${expectedCompletionList.items.length})`, 40 | ); 41 | } 42 | 43 | if (vertification === VertificationType.Exact || vertification === VertificationType.Ordered) { 44 | expectedCompletionList.items.forEach((expectedItem: vscode.CompletionItem, index: number) => { 45 | const actualItem: vscode.CompletionItem | undefined = actualCompletionList.items.at(index); 46 | assert.equal(actualItem?.label, expectedItem.label); 47 | assert.equal(actualItem?.kind, expectedItem.kind); 48 | }); 49 | } else { 50 | expectedCompletionList.items.forEach((expectedItem: vscode.CompletionItem) => { 51 | const filteredItems: vscode.CompletionItem[] = actualCompletionList.items.filter( 52 | (item: vscode.CompletionItem) => item.label === expectedItem.label, 53 | ); 54 | 55 | assert.equal(filteredItems.length, 1, `expected to find one item with label '${expectedItem.label}'`); 56 | 57 | assert.equal( 58 | filteredItems[0].kind, 59 | expectedItem.kind, 60 | `item kind mismatch. Label: ${expectedItem.label} Expected: ${expectedItem.kind} Actual: ${filteredItems[0].kind}`, 61 | ); 62 | }); 63 | } 64 | } 65 | 66 | async function testCompletionBase( 67 | docUri: vscode.Uri, 68 | position: vscode.Position, 69 | ): Promise { 70 | await TestUtils.activate(docUri); 71 | 72 | // Executing the command `vscode.executeCompletionItemProvider` to simulate triggering completion 73 | return vscode.commands.executeCommand(Commands.CompletionItems, docUri, position); 74 | } 75 | -------------------------------------------------------------------------------- /client/src/test/suite/dataflowExtractCommand.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as assert from "assert"; 5 | import * as vscode from "vscode"; 6 | import { CommandConstant } from "../../constants"; 7 | import { expect } from "chai"; 8 | 9 | import * as TestUtils from "./testUtils"; 10 | 11 | // TODO: We could add command unit tests that use mocks to avoid UI based tests. 12 | suite("Dataflow Extract Command", () => { 13 | const docUri: vscode.Uri = TestUtils.getDocUri("dataflow.json"); 14 | 15 | suiteSetup(async () => { 16 | await TestUtils.activateExtension(); 17 | 18 | return await TestUtils.closeFileIfOpen(docUri); 19 | }); 20 | 21 | test("Command is registered", async () => { 22 | const commands: string[] = [CommandConstant.ExtractDataflowDocument]; 23 | 24 | const pqCommands: string[] = (await vscode.commands.getCommands(/* filterInternal */ true)).filter( 25 | (cmd: string) => cmd.startsWith("powerquery."), 26 | ); 27 | 28 | commands.forEach((cmd: string) => assert(pqCommands.includes(cmd), `Command not found: ${cmd}`)); 29 | }); 30 | 31 | test("Extract command", async () => { 32 | const doc: vscode.TextDocument = await vscode.workspace.openTextDocument(docUri); 33 | 34 | await vscode.window.showTextDocument(doc); 35 | 36 | const newDocUri: vscode.Uri | undefined = await vscode.commands.executeCommand( 37 | CommandConstant.ExtractDataflowDocument, 38 | ); 39 | 40 | expect(newDocUri !== undefined, "command did not return new document URI"); 41 | 42 | return await TestUtils.closeFileIfOpen(newDocUri as vscode.Uri); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /client/src/test/suite/diagnostics.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as assert from "assert"; 5 | import * as vscode from "vscode"; 6 | 7 | import * as TestUtils from "./testUtils"; 8 | import { LibrarySymbol, PowerQueryApi } from "../../powerQueryApi"; 9 | 10 | type PartialDiagnostic = Partial & { severity: vscode.DiagnosticSeverity }; 11 | 12 | suite("Diagnostics: Simple", () => { 13 | const docUri: vscode.Uri = TestUtils.getDocUri("diagnostics.pq"); 14 | 15 | suiteSetup(async () => await TestUtils.closeFileIfOpen(docUri)); 16 | 17 | test("Simple diagnostics test", async () => 18 | await testDiagnostics(docUri, [ 19 | { 20 | message: 21 | "Expected to find a equal operator <'='>, but a not equal to operator ('<>') was found instead", 22 | range: new vscode.Range(0, 9, 0, 12), 23 | severity: vscode.DiagnosticSeverity.Error, 24 | }, 25 | ])); 26 | }); 27 | 28 | suite("Diagnostics: No errors", () => { 29 | const docUri: vscode.Uri = TestUtils.getDocUri("Diagnostics.NoErrors.pq"); 30 | 31 | suiteSetup(async () => await TestUtils.closeFileIfOpen(docUri)); 32 | 33 | test("No errors", async () => await testDiagnostics(docUri, [])); 34 | }); 35 | 36 | suite("Diagnostics: External Library Symbols", () => { 37 | const docUri: vscode.Uri = TestUtils.getDocUri("Diagnostics.ExternalLibrarySymbol.pq"); 38 | const testLibraryName: string = "TestLibrary"; 39 | 40 | const expectedDiagnostic: PartialDiagnostic = { 41 | message: "Cannot find the name 'TestSymbol.ShouldNotExistOrMatchExisting'.", 42 | range: new vscode.Range(0, 0, 0, 22), 43 | severity: vscode.DiagnosticSeverity.Error, 44 | }; 45 | 46 | let extensionApi: PowerQueryApi; 47 | 48 | suiteSetup(async () => { 49 | extensionApi = await TestUtils.activateExtension(); 50 | }); 51 | 52 | // Closing the file after every test ensures no state conflicts. 53 | teardown(async () => await TestUtils.closeFileIfOpen(docUri)); 54 | 55 | test("Missing symbol", async () => { 56 | await testDiagnostics(docUri, [expectedDiagnostic]); 57 | }); 58 | 59 | test("Add symbol", async () => { 60 | const extensionApi: PowerQueryApi = await TestUtils.activateExtension(); 61 | 62 | const symbol: LibrarySymbol = { 63 | name: "TestSymbol.ShouldNotExistOrMatchExisting", 64 | documentation: null, 65 | completionItemKind: 3, 66 | functionParameters: null, 67 | isDataSource: false, 68 | type: "any", 69 | }; 70 | 71 | const symbolMap: ReadonlyMap = new Map([[testLibraryName, [symbol]]]); 72 | 73 | await extensionApi.addLibrarySymbols(symbolMap); 74 | 75 | await testDiagnostics(docUri, []); 76 | }); 77 | 78 | test("Remove symbol", async () => { 79 | await extensionApi.removeLibrarySymbols([testLibraryName]); 80 | 81 | await testDiagnostics(docUri, [expectedDiagnostic]); 82 | }); 83 | }); 84 | 85 | suite("Diagnostics: Experimental", () => { 86 | const docUri: vscode.Uri = TestUtils.getDocUri("Diagnostics.TableIsEmpty.Error.pq"); 87 | 88 | suiteSetup(async () => await TestUtils.closeFileIfOpen(docUri)); 89 | 90 | test("No error reported with default settings", async () => { 91 | await testDiagnostics(docUri, []); 92 | }); 93 | 94 | // TODO: Tests that change the local configuration settings. 95 | // Investigate support for scoped settings / Document Settings. 96 | }); 97 | 98 | async function testDiagnostics(docUri: vscode.Uri, expectedDiagnostics: PartialDiagnostic[]): Promise { 99 | const editor: vscode.TextEditor = await TestUtils.activate(docUri); 100 | 101 | // Add a short delay to ensure the diagnostics are computed. 102 | // Diagnostics tests can be flaky without this delay. 103 | await TestUtils.delay(10); 104 | const actualDiagnostics: vscode.Diagnostic[] = vscode.languages.getDiagnostics(editor.document.uri); 105 | 106 | // Special handling for common case 107 | if (expectedDiagnostics.length === 0 && actualDiagnostics.length !== 0) { 108 | assert.fail(`Expected 0 diagnostics but received: ${JSON.stringify(actualDiagnostics, undefined, 2)}`); 109 | } 110 | 111 | assert.equal( 112 | actualDiagnostics.length, 113 | expectedDiagnostics.length, 114 | `Expected ${expectedDiagnostics.length} diagnostics by received ${actualDiagnostics.length}`, 115 | ); 116 | 117 | expectedDiagnostics.forEach((expectedDiagnostic: PartialDiagnostic, index: number) => { 118 | const actualDiagnostic: vscode.Diagnostic | undefined = actualDiagnostics[index]; 119 | 120 | assert.equal(actualDiagnostic.severity, expectedDiagnostic.severity); 121 | 122 | if (expectedDiagnostic.message !== undefined) { 123 | assert.equal(actualDiagnostic?.message, expectedDiagnostic.message); 124 | } 125 | 126 | if (expectedDiagnostic.range === undefined) { 127 | assert.deepEqual(actualDiagnostic?.range, expectedDiagnostic.range); 128 | } 129 | }); 130 | } 131 | -------------------------------------------------------------------------------- /client/src/test/suite/documentSymbolUtils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as vscode from "vscode"; 5 | import { assert, expect } from "chai"; 6 | 7 | import * as TestUtils from "./testUtils"; 8 | import { Commands } from "./testUtils"; 9 | 10 | export interface ExpectedDocumentSymbol { 11 | readonly name: string; 12 | readonly kind: vscode.SymbolKind; 13 | readonly children?: ExpectedDocumentSymbol[]; 14 | } 15 | 16 | export async function testDocumentSymbols( 17 | docUri: vscode.Uri, 18 | expectedSymbols: ExpectedDocumentSymbol[], 19 | ): Promise { 20 | await vscode.workspace.openTextDocument(docUri); 21 | 22 | const documentSymbols: vscode.DocumentSymbol[] | undefined = await documentSymbolsBase(docUri); 23 | 24 | if (documentSymbols === undefined) { 25 | assert.fail("documentSymbols undefined"); 26 | } 27 | 28 | const actualSymbols: ExpectedDocumentSymbol[] = documentSymbolArrayToExpectedSymbols(documentSymbols); 29 | expect(actualSymbols).deep.equals(expectedSymbols, "Expected document symbols to match."); 30 | } 31 | 32 | async function documentSymbolsBase(docUri: vscode.Uri): Promise { 33 | await TestUtils.activate(docUri); 34 | 35 | return vscode.commands.executeCommand(Commands.DocumentSymbols, docUri); 36 | } 37 | 38 | function documentSymbolArrayToExpectedSymbols(documentSymbols: vscode.DocumentSymbol[]): ExpectedDocumentSymbol[] { 39 | const expectedSymbols: ExpectedDocumentSymbol[] = []; 40 | 41 | documentSymbols.forEach((element: vscode.DocumentSymbol) => { 42 | let children: ExpectedDocumentSymbol[] | undefined; 43 | 44 | if (element.children && element.children.length > 0) { 45 | children = documentSymbolArrayToExpectedSymbols(element.children); 46 | expectedSymbols.push({ name: element.name, kind: element.kind, children }); 47 | } else { 48 | expectedSymbols.push({ name: element.name, kind: element.kind }); 49 | } 50 | }); 51 | 52 | return expectedSymbols; 53 | } 54 | -------------------------------------------------------------------------------- /client/src/test/suite/documentSymbols.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as vscode from "vscode"; 5 | 6 | import * as DocumentSymbolUtils from "./documentSymbolUtils"; 7 | import * as TestUtils from "./testUtils"; 8 | 9 | suite("DocumentSymbols", () => { 10 | const docUri: vscode.Uri = TestUtils.getDocUri("DocumentSymbols.pq"); 11 | 12 | suiteSetup(async () => await TestUtils.closeFileIfOpen(docUri)); 13 | 14 | test("DocumentSymbols.pq", async () => 15 | await DocumentSymbolUtils.testDocumentSymbols(docUri, [ 16 | { name: "firstMember", kind: vscode.SymbolKind.Number }, 17 | { name: "secondMember", kind: vscode.SymbolKind.String }, 18 | { name: "thirdMember", kind: vscode.SymbolKind.Function }, 19 | { 20 | name: "letMember", 21 | kind: vscode.SymbolKind.Variable, 22 | children: [ 23 | { name: "a", kind: vscode.SymbolKind.Number }, 24 | { name: "b", kind: vscode.SymbolKind.Number }, 25 | { name: "c", kind: vscode.SymbolKind.Number }, 26 | ], 27 | }, 28 | ])); 29 | }); 30 | -------------------------------------------------------------------------------- /client/src/test/suite/encodingCommands.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as assert from "assert"; 5 | import * as vscode from "vscode"; 6 | import { CommandConstant } from "../../constants"; 7 | import { expect } from "chai"; 8 | 9 | import * as TestUtils from "./testUtils"; 10 | 11 | // TODO: We could add command unit tests that use mocks to avoid UI based tests. 12 | suite("Encode/Decode Commands", () => { 13 | const mToEncode: string = 'let\r\n #"id" = "m text"" here"\r\nin\r\n #"id"'; 14 | const jsonToDecode: string = `"let\\r\\n #\\"id\\" = \\"m text\\"\\" here\\"\\r\\nin\\r\\n #\\"id\\""`; 15 | 16 | suiteSetup(async () => await TestUtils.activateExtension()); 17 | 18 | test("Commands are registered", async () => { 19 | const commands: string[] = [ 20 | CommandConstant.EscapeMText, 21 | CommandConstant.UnescapeMText, 22 | CommandConstant.EscapeJsonText, 23 | CommandConstant.UnescapeJsonText, 24 | ]; 25 | 26 | const pqCommands: string[] = (await vscode.commands.getCommands(/* filterInternal */ true)).filter( 27 | (cmd: string) => cmd.startsWith("powerquery."), 28 | ); 29 | 30 | commands.forEach((cmd: string) => assert(pqCommands.includes(cmd), `Command not found: ${cmd}`)); 31 | }); 32 | 33 | test("M Escape", async () => { 34 | const content: string = 'Encode \t\t and \r\n and "quotes" but not this #(tab)'; 35 | const expected: string = 'Encode #(tab)#(tab) and #(cr,lf) and ""quotes"" but not this #(#)(tab)'; 36 | 37 | return await runEncodeTest(content, expected, CommandConstant.EscapeMText); 38 | }); 39 | 40 | test("M Unescape", async () => { 41 | const content: string = 'Encode #(tab)#(tab) and #(cr)#(lf) and ""quotes"" but not this #(#)(tab)'; 42 | const expected: string = 'Encode \t\t and \r\n and "quotes" but not this #(tab)'; 43 | 44 | return await runEncodeTest(content, expected, CommandConstant.UnescapeMText); 45 | }); 46 | 47 | test("JSON Escape", async () => await runEncodeTest(mToEncode, jsonToDecode, CommandConstant.EscapeJsonText)); 48 | 49 | test("JSON Unescape (existing quotes)", async () => { 50 | const content: string = '"let\\r\\n #\\"id\\" = \\"m text\\"\\" here\\"\\r\\nin\\r\\n #\\"id\\""'; 51 | const expected: string = 'let\r\n #"id" = "m text"" here"\r\nin\r\n #"id"'; 52 | 53 | return await runEncodeTest(content, expected, CommandConstant.UnescapeJsonText); 54 | }); 55 | 56 | test("JSON Unescape (no quotes)", async () => 57 | await runEncodeTest(jsonToDecode, mToEncode, CommandConstant.UnescapeJsonText)); 58 | }); 59 | 60 | async function runEncodeTest(original: string, expected: string, command: string): Promise { 61 | const doc: vscode.TextDocument = await vscode.workspace.openTextDocument({ 62 | language: "powerquery", 63 | content: original, 64 | }); 65 | 66 | // Use a large range to select the entire document 67 | const editor: vscode.TextEditor = await vscode.window.showTextDocument(doc); 68 | editor.selection = new vscode.Selection(0, 0, 9999, 9999); 69 | 70 | await vscode.commands.executeCommand(command); 71 | 72 | const currentText: string = doc.getText(); 73 | await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); 74 | 75 | expect(expected).to.equal(currentText); 76 | } 77 | -------------------------------------------------------------------------------- /client/src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as assert from "assert"; 5 | import * as vscode from "vscode"; 6 | 7 | import * as TestUtils from "./testUtils"; 8 | 9 | import { PowerQueryApi } from "../../powerQueryApi"; 10 | 11 | suite("Extension Tests", () => { 12 | test("extension loads", () => { 13 | assert.ok(vscode.extensions.getExtension(TestUtils.extensionId)); 14 | }); 15 | 16 | test("should be able to activate", async () => { 17 | const api: PowerQueryApi = await TestUtils.activateExtension(); 18 | assert.ok(api); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /client/src/test/suite/librarySymbolManager.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as assert from "assert"; 5 | import * as fs from "fs"; 6 | import * as vscode from "vscode"; 7 | 8 | import * as TestUtils from "./testUtils"; 9 | 10 | import { LibraryJson, PowerQueryApi } from "../../powerQueryApi"; 11 | import { LibrarySymbolManager } from "../../librarySymbolManager"; 12 | 13 | class MockLibararySymbolClient implements PowerQueryApi { 14 | public registeredSymbols: Map = new Map(); 15 | 16 | onModuleLibraryUpdated(_workspaceUriPath: string, _library: LibraryJson): void { 17 | throw new Error("Function not implemented."); 18 | } 19 | 20 | addLibrarySymbols(librarySymbols: ReadonlyMap): Promise { 21 | for (const [key, value] of librarySymbols) { 22 | this.registeredSymbols.set(key, value); 23 | } 24 | 25 | return Promise.resolve(); 26 | } 27 | 28 | removeLibrarySymbols(librariesToRemove: ReadonlyArray): Promise { 29 | for (const library of librariesToRemove) { 30 | this.registeredSymbols.delete(library); 31 | } 32 | 33 | return Promise.resolve(); 34 | } 35 | 36 | reset(): void { 37 | this.registeredSymbols.clear(); 38 | } 39 | } 40 | 41 | const mockClient: MockLibararySymbolClient = new MockLibararySymbolClient(); 42 | const librarySymbolManager: LibrarySymbolManager = new LibrarySymbolManager(mockClient); 43 | 44 | suite("LibrarySymbolManager.processSymbolFile", () => { 45 | test("Valid", async () => { 46 | const fileUri: vscode.Uri = TestUtils.getDocUri("ExtensionTest.json"); 47 | 48 | const res: [vscode.Uri, LibraryJson] | undefined = await librarySymbolManager.processSymbolFile(fileUri); 49 | 50 | assert(res !== undefined, "Expected result"); 51 | 52 | assert.equal(res[0], fileUri, "uri should match"); 53 | assert.ok(res[1], "library should be defined"); 54 | assert.equal(res[1].length, 1, "Expected one symbol"); 55 | assert.equal(res[1][0].name, "ExtensionTest.Contents"); 56 | }); 57 | 58 | test("Not a symbol file", async () => { 59 | const fileUri: vscode.Uri = TestUtils.getDocUri("dataflow.json"); 60 | 61 | const res: [vscode.Uri, LibraryJson] | undefined = await librarySymbolManager.processSymbolFile(fileUri); 62 | assert(res === undefined, "Expected library to be undefined"); 63 | }); 64 | 65 | test("Not json", async () => { 66 | const fileUri: vscode.Uri = TestUtils.getDocUri("index.js"); 67 | 68 | const res: [vscode.Uri, LibraryJson] | undefined = await librarySymbolManager.processSymbolFile(fileUri); 69 | assert(res === undefined, "Expected library to be undefined"); 70 | }); 71 | 72 | test("Not a file", async () => { 73 | const fileUri: vscode.Uri = vscode.Uri.file(TestUtils.getTestFixturePath()); 74 | 75 | const res: [vscode.Uri, LibraryJson] | undefined = await librarySymbolManager.processSymbolFile(fileUri); 76 | assert(res === undefined, "Expected library to be undefined"); 77 | }); 78 | }); 79 | 80 | suite("LibrarySymbolManager.refreshSymbolDirectories", () => { 81 | test("Refresh with valid file", async () => { 82 | const modules: ReadonlyArray = await librarySymbolManager.refreshSymbolDirectories([ 83 | TestUtils.getTestFixturePath(), 84 | ]); 85 | 86 | assert.equal(modules.length, 1, "Expected one result"); 87 | assert.equal(modules[0], "ExtensionTest"); 88 | 89 | assert.ok(mockClient.registeredSymbols, "call should have been made"); 90 | assert.equal(mockClient.registeredSymbols.size, 1, "Expected one element in the symbols call"); 91 | 92 | const entry: LibraryJson | undefined = mockClient.registeredSymbols.get("ExtensionTest"); 93 | assert(entry !== undefined, "Expected ExtensionTest to in the results"); 94 | assert.equal(entry.length, 1, "Expected one library in the result"); 95 | assert.equal(entry[0].name, "ExtensionTest.Contents"); 96 | 97 | const resetModules: ReadonlyArray = await librarySymbolManager.refreshSymbolDirectories([]); 98 | assert.equal(resetModules.length, 0, "Expected empty string array"); 99 | assert.equal(mockClient.registeredSymbols.size, 0, "Expected registered symbols to be cleared"); 100 | }); 101 | }); 102 | 103 | suite("LibrarySymbolManager.getSymbolFilesFromDirectory", () => { 104 | test("Two files", async () => await runDirectoryTest(TestUtils.getTestFixturePath(), 2)); 105 | test("Does not exist", async () => await runDirectoryTest(TestUtils.randomDirName(), 0)); 106 | test("Invalid dir name: symbols", async () => await runDirectoryTest("@@$$%!!~~!!!", 0)); 107 | 108 | test("No files", async () => { 109 | const tmpDir: string = fs.mkdtempSync(TestUtils.randomDirName()); 110 | 111 | try { 112 | const dirUri: vscode.Uri = vscode.Uri.file(tmpDir); 113 | const res: ReadonlyArray = await librarySymbolManager.getSymbolFilesFromDirectory(dirUri); 114 | assert.equal(res.length, 0); 115 | } finally { 116 | try { 117 | if (tmpDir) { 118 | fs.rmSync(tmpDir, { recursive: true }); 119 | } 120 | } catch (e) { 121 | console.error(`An error has occurred while removing the temp folder ${tmpDir}. Error: ${e}`); 122 | } 123 | } 124 | }); 125 | }); 126 | 127 | async function runDirectoryTest(path: string, count: number): Promise { 128 | const dirUri: vscode.Uri = vscode.Uri.file(path); 129 | const result: ReadonlyArray = await librarySymbolManager.getSymbolFilesFromDirectory(dirUri); 130 | assert.equal(result.length, count, "Expected file count did not match"); 131 | } 132 | -------------------------------------------------------------------------------- /client/src/test/suite/librarySymbolUtils.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as assert from "assert"; 5 | import * as fs from "fs"; 6 | import * as vscode from "vscode"; 7 | import { expect } from "chai"; 8 | 9 | import * as LibrarySymbolUtils from "../../librarySymbolUtils"; 10 | import * as TestUtils from "./testUtils"; 11 | import { LibraryJson } from "../../powerQueryApi"; 12 | 13 | suite("LibrarySymbolUtils", () => { 14 | suite("parseLibraryJson", () => { 15 | test("Empty", () => { 16 | expect(() => LibrarySymbolUtils.parseLibraryJson("")).to.throw(); 17 | }); 18 | 19 | test("Empty JSON object", () => { 20 | expect(() => LibrarySymbolUtils.parseLibraryJson("{}")).to.throw("Expected an array"); 21 | }); 22 | 23 | test("Empty root array", () => { 24 | const library: LibraryJson = LibrarySymbolUtils.parseLibraryJson("[]"); 25 | assert.equal(library.length, 0); 26 | }); 27 | 28 | test("Invalid symbol", () => { 29 | expect(() => LibrarySymbolUtils.parseLibraryJson(`[{"not": "a", "symbol": [] }]`)).to.throw( 30 | "Missing property: name", 31 | ); 32 | }); 33 | 34 | test("Valid from file", () => { 35 | const fileUri: vscode.Uri = TestUtils.getDocUri("ExtensionTest.json"); 36 | const contents: string = fs.readFileSync(fileUri.fsPath, "utf-8"); 37 | const library: LibraryJson = LibrarySymbolUtils.parseLibraryJson(contents); 38 | 39 | assert.equal(library.length, 1); 40 | assert.equal(library[0].name, "ExtensionTest.Contents"); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /client/src/test/suite/sectionCompletion.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as vscode from "vscode"; 5 | 6 | import * as CompletionUtils from "./completionUtils"; 7 | import * as TestUtils from "./testUtils"; 8 | 9 | suite("Section completion tests", () => { 10 | const docUri: vscode.Uri = TestUtils.getDocUri("section.pq"); 11 | 12 | suiteSetup(async () => await TestUtils.closeFileIfOpen(docUri)); 13 | 14 | test("Keywords", async () => { 15 | await CompletionUtils.testCompletion( 16 | docUri, 17 | new vscode.Position(3, 14), 18 | { 19 | items: [ 20 | { label: "if", kind: vscode.CompletionItemKind.Keyword }, 21 | { label: "let", kind: vscode.CompletionItemKind.Keyword }, 22 | { label: "not", kind: vscode.CompletionItemKind.Keyword }, 23 | { label: "true", kind: vscode.CompletionItemKind.Keyword }, 24 | ], 25 | }, 26 | CompletionUtils.VertificationType.Contains, 27 | ); 28 | }); 29 | 30 | test("Section members", async () => { 31 | await CompletionUtils.testCompletion( 32 | docUri, 33 | new vscode.Position(11, 12), 34 | { 35 | items: [ 36 | { label: "firstMember", kind: vscode.CompletionItemKind.Variable }, 37 | { label: "secondMember", kind: vscode.CompletionItemKind.Variable }, 38 | { label: "thirdMember", kind: vscode.CompletionItemKind.Variable }, 39 | ], 40 | }, 41 | CompletionUtils.VertificationType.Contains, 42 | ); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /client/src/test/suite/testUtils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as assert from "assert"; 5 | import * as os from "os"; 6 | import * as path from "path"; 7 | import * as vscode from "vscode"; 8 | 9 | import { PowerQueryApi } from "../../powerQueryApi"; 10 | 11 | const testFixurePath: string = "../../../src/test/testFixture"; 12 | 13 | export const extensionId: string = "powerquery.vscode-powerquery"; 14 | 15 | export async function activate(docUri: vscode.Uri): Promise { 16 | await activateExtension(); 17 | 18 | try { 19 | const doc: vscode.TextDocument = await vscode.workspace.openTextDocument(docUri); 20 | 21 | return await vscode.window.showTextDocument(doc); 22 | } catch (e) { 23 | console.error(e); 24 | assert.fail(`Failed to open ${docUri}`); 25 | } 26 | } 27 | 28 | export async function activateExtension(): Promise { 29 | // The extensionId is `publisher.name` from package.json 30 | const ext: vscode.Extension | undefined = vscode.extensions.getExtension(extensionId); 31 | 32 | if (!ext) { 33 | throw new Error("Failed to load extension."); 34 | } 35 | 36 | if (ext.isActive) { 37 | return ext.exports; 38 | } 39 | 40 | return await ext.activate(); 41 | } 42 | 43 | export async function closeFileIfOpen(file: vscode.Uri): Promise { 44 | const tabs: vscode.Tab[] = vscode.window.tabGroups.all.map((tg: vscode.TabGroup) => tg.tabs).flat(); 45 | 46 | const tab: vscode.Tab | undefined = tabs.find( 47 | (tab: vscode.Tab) => tab.input instanceof vscode.TabInputText && tab.input.uri.path === file.path, 48 | ); 49 | 50 | if (tab) { 51 | await vscode.window.tabGroups.close(tab); 52 | } 53 | } 54 | 55 | export function delay(ms: number): Promise { 56 | return new Promise((resolve: () => void) => setTimeout(resolve, ms)); 57 | } 58 | 59 | export function getTestFixturePath(): string { 60 | return path.resolve(__dirname, testFixurePath); 61 | } 62 | 63 | export const getDocPath: (p: string) => string = (p: string): string => path.resolve(getTestFixturePath(), p); 64 | 65 | export const getDocUri: (p: string) => vscode.Uri = (p: string): vscode.Uri => vscode.Uri.file(getDocPath(p)); 66 | 67 | export async function setTestContent( 68 | doc: vscode.TextDocument, 69 | editor: vscode.TextEditor, 70 | content: string, 71 | ): Promise { 72 | const all: vscode.Range = new vscode.Range(doc.positionAt(0), doc.positionAt(doc.getText().length)); 73 | 74 | return await editor.edit((eb: vscode.TextEditorEdit) => eb.replace(all, content)); 75 | } 76 | 77 | export enum Commands { 78 | CompletionItems = "vscode.executeCompletionItemProvider", 79 | DocumentSymbols = "vscode.executeDocumentSymbolProvider", 80 | Format = "vscode.executeFormatDocumentProvider", 81 | Hover = "vscode.executeHoverProvider", 82 | SignatureHelp = "vscode.executeSignatureHelpProvider ", 83 | } 84 | 85 | export const randomDirName: (length?: number) => string = (length: number = 8): string => 86 | path.resolve(os.tmpdir(), Math.random().toString(16).substring(2, length)); 87 | -------------------------------------------------------------------------------- /client/src/test/testFixture/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.enabled": false 3 | } 4 | -------------------------------------------------------------------------------- /client/src/test/testFixture/Diagnostics.ExternalLibrarySymbol.pq: -------------------------------------------------------------------------------- 1 | TestSymbol.ShouldNotExistOrMatchExisting() 2 | -------------------------------------------------------------------------------- /client/src/test/testFixture/Diagnostics.NoErrors.pq: -------------------------------------------------------------------------------- 1 | section testDocument; 2 | 3 | shared Exported.Member = 1; 4 | 5 | SimpleFunction = () => 2; -------------------------------------------------------------------------------- /client/src/test/testFixture/Diagnostics.TableIsEmpty.Error.pq: -------------------------------------------------------------------------------- 1 | Table.IsEmpty("abc") -------------------------------------------------------------------------------- /client/src/test/testFixture/DocumentSymbols.pq: -------------------------------------------------------------------------------- 1 | [Version = "1.0.0"] 2 | section sectionTest; 3 | 4 | firstMember = 1; 5 | secondMember = "static string"; 6 | thirdMember = () => true; 7 | 8 | shared letMember = let a = 1, b = 2, c = 3 in c; 9 | -------------------------------------------------------------------------------- /client/src/test/testFixture/ExtensionTest.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "ExtensionTest.Contents", 4 | "documentation": null, 5 | "completionItemKind": 3, 6 | "functionParameters": [ 7 | { 8 | "name": "message", 9 | "type": "nullable text", 10 | "isRequired": false, 11 | "isNullable": true, 12 | "caption": null, 13 | "description": null, 14 | "sampleValues": null, 15 | "allowedValues": null, 16 | "defaultValue": null, 17 | "fields": null, 18 | "enumNames": null, 19 | "enumCaptions": null 20 | } 21 | ], 22 | "isDataSource": true, 23 | "type": "any" 24 | } 25 | ] -------------------------------------------------------------------------------- /client/src/test/testFixture/completion.pq: -------------------------------------------------------------------------------- 1 | Access.Dat 2 | -------------------------------------------------------------------------------- /client/src/test/testFixture/dataflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MyTestDataflow", 3 | "ppdf:dataflowId": "12345678-2e1a-4af9-8b92-98152e0ba2da", 4 | "ppdf:owner": {}, 5 | "version": "1.0", 6 | "culture": "en-US", 7 | "modifiedTime": "2023-01-25T18:11:13.6841777+00:00", 8 | "pbi:mashup": { 9 | "fastCombine": false, 10 | "allowNativeQueries": false, 11 | "queriesMetadata": {}, 12 | "document": "section Section1;\r\nshared Query1 = let\r\n Source = Table.FromRecords({[textColumn=\"123\"]}),\r\nSource2 = Table.FromRecords({[textColumn=\"123\"]}),\r\nSource3 = Table.FromRecords({[textColumn=\"123\"]}),\r\nSource4 = Table.FromRecords({[textColumn=\"123\"]})\r\nin\r\nSource;\r\n", 13 | "connectionOverrides": [] 14 | }, 15 | "entities": [] 16 | } -------------------------------------------------------------------------------- /client/src/test/testFixture/diagnostics.pq: -------------------------------------------------------------------------------- 1 | let this not be M 2 | -------------------------------------------------------------------------------- /client/src/test/testFixture/section.pq: -------------------------------------------------------------------------------- 1 | [Version="1.0.0"] 2 | section sectionTest; 3 | 4 | firstMember = 1; 5 | secondMember = "static string"; 6 | thirdMember = () => true; 7 | 8 | shared letMember = 9 | let 10 | a = 1, 11 | b = 2, 12 | c = 3 13 | in 14 | c; 15 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src" 6 | }, 7 | "include": ["src/**/*.ts"], 8 | "exclude": ["node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /client/tsconfig.webpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "exclude": ["node_modules", "src/test"] 4 | } 5 | -------------------------------------------------------------------------------- /client/webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | "use strict"; 4 | 5 | const path = require("path"); 6 | 7 | /**@type {import('webpack').Configuration}*/ 8 | const config = { 9 | target: "node", 10 | entry: "./src/extension.ts", 11 | output: { 12 | path: path.resolve(__dirname, "dist"), 13 | filename: "extension.js", 14 | libraryTarget: "commonjs2", 15 | devtoolModuleFilenameTemplate: "../[resource-path]", 16 | }, 17 | devtool: "source-map", 18 | externals: { 19 | vscode: "commonjs vscode", 20 | }, 21 | infrastructureLogging: { 22 | level: "log", 23 | }, 24 | resolve: { 25 | extensions: [".ts", ".js"], 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.ts$/, 31 | exclude: /node_modules/, 32 | use: [ 33 | { 34 | loader: "ts-loader", 35 | options: { 36 | configFile: "tsconfig.webpack.json", 37 | }, 38 | }, 39 | ], 40 | }, 41 | ], 42 | }, 43 | }; 44 | module.exports = config; 45 | -------------------------------------------------------------------------------- /imgs/PQIcon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-powerquery/51dd91670c11956633702865c96f27a29212273d/imgs/PQIcon_256.png -------------------------------------------------------------------------------- /imgs/formatDocument.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-powerquery/51dd91670c11956633702865c96f27a29212273d/imgs/formatDocument.gif -------------------------------------------------------------------------------- /imgs/fuzzyAutocomplete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-powerquery/51dd91670c11956633702865c96f27a29212273d/imgs/fuzzyAutocomplete.gif -------------------------------------------------------------------------------- /imgs/hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-powerquery/51dd91670c11956633702865c96f27a29212273d/imgs/hover.png -------------------------------------------------------------------------------- /imgs/jsonDecodeEncode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-powerquery/51dd91670c11956633702865c96f27a29212273d/imgs/jsonDecodeEncode.png -------------------------------------------------------------------------------- /imgs/parameterHints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-powerquery/51dd91670c11956633702865c96f27a29212273d/imgs/parameterHints.png -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | // symbol used for single line comment 4 | "lineComment": "//", 5 | // symbols used for start and end a block comment 6 | "blockComment": ["/*", "*/"] 7 | }, 8 | // symbols used as brackets 9 | "brackets": [["{", "}"], ["[", "]"], ["(", ")"]], 10 | // symbols that are auto closed when typing 11 | "autoClosingPairs": [ 12 | ["{", "}"], 13 | ["[", "]"], 14 | ["(", ")"], 15 | ["\"", "\""], 16 | ["'", "'"] 17 | ], 18 | // symbols that that can be used to surround a selection 19 | "surroundingPairs": [ 20 | ["{", "}"], 21 | ["[", "]"], 22 | ["(", ")"], 23 | ["\"", "\""], 24 | ["'", "'"] 25 | ], 26 | // (built-in identifier)|(quoted identifier)|(each record field)|(number.float)|(number.hex)|(number)|(identifier) 27 | "wordPattern": "(#\\w+)|(#\".+?\")|(_?\\[\\w+\\])|(-?\\d*\\.\\d+(?:[eE][\\-+]?\\d+)?)|(-?0[xX][0-9a-fA-F]+)|(-?\\d+(?:[eE][\\-+]?\\d+)?)|([^\\[\\]\\`\\~\\!\\@\\%\\^\\&\\*\\(\\)\\-\\=\\+\\{\\}\\\\\\|\\;\\:\\'\\\"\\,\\<\\>\\/\\?\\s]+)" 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-powerquery", 3 | "version": "0.1.61", 4 | "displayName": "Power Query / M Language", 5 | "description": "Language service for the Power Query / M formula language", 6 | "author": "Microsoft Corporation", 7 | "license": "MIT", 8 | "homepage": "https://github.com/microsoft/vscode-powerquery#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/microsoft/vscode-powerquery.git" 12 | }, 13 | "issues": { 14 | "url": "https://github.com/microsoft/vscode-powerquery/issues" 15 | }, 16 | "scripts": { 17 | "postinstall": "npm run install:client && npm run install:server && npm run install:scripts", 18 | "install:client": "cd client && npm install-clean", 19 | "install:server": "cd server && npm install-clean", 20 | "install:scripts": "cd scripts && npm install-clean", 21 | "audit": "npm audit fix && npm run audit:client && npm run audit:server && npm run audit:scripts", 22 | "audit:client": "cd client && npm audit", 23 | "audit:server": "cd server && npm audit", 24 | "audit:scripts": "cd scripts && npm audit", 25 | "audit:fix": "npm audit fix && npm run audit:fix:client && npm run audit:fix:server && npm run audit:fix:scripts", 26 | "audit:fix:client": "cd client && npm audit fix", 27 | "audit:fix:server": "cd server && npm audit fix", 28 | "audit:fix:scripts": "cd scripts && npm audit fix", 29 | "build": "npm run build:client && npm run build:server && npm run build:scripts", 30 | "build:client": "cd client && npm run build", 31 | "build:server": "cd server && npm run build", 32 | "build:scripts": "cd scripts && npm run build", 33 | "link:start": "npm run link:start:client && npm run link:start:server && npm run link:start:scripts", 34 | "link:start:client": "cd client && npm run link:start", 35 | "link:start:server": "cd server && npm run link:start", 36 | "link:start:scripts": "cd scripts && npm run link:start", 37 | "link:stop": "npm run link:stop:client && npm run link:stop:server && npm run link:stop:scripts", 38 | "link:stop:client": "cd client && npm run link:stop", 39 | "link:stop:server": "cd server && npm run link:stop", 40 | "link:stop:scripts": "cd scripts && npm run link:stop", 41 | "lint": "npm run lint:client && npm run lint:server", 42 | "lint:client": "eslint client/src --ext ts", 43 | "lint:server": "eslint server/src --ext ts", 44 | "lint:scripts": "eslint scripts --ext ts", 45 | "test": "npm run test:server && npm run test:client", 46 | "test:client": "cd client && npm run test", 47 | "test:server": "cd server && npm run test", 48 | "webpack-dev": "npm run webpack-dev:client && npm run webpack-dev:server", 49 | "webpack-dev:client": "cd client && npm run webpack-dev", 50 | "webpack-dev:server:": "cd server && npm run webpack-dev", 51 | "webpack-prod": "npm run webpack-prod:server && npm run webpack-prod:client", 52 | "webpack-prod:client": "cd client && npm run webpack-prod", 53 | "webpack-prod:server": "cd server && npm run webpack-prod", 54 | "version": "npm version patch && npm run version:client && npm run version:server && npm run version:scripts", 55 | "version:client": "cd client && npm version patch", 56 | "version:server": "cd server && npm version patch", 57 | "version:scripts": "cd scripts && npm version patch", 58 | "vscode:prepublish": "npm run webpack-prod", 59 | "vsix": "npx @vscode/vsce@latest package" 60 | }, 61 | "icon": "imgs/PQIcon_256.png", 62 | "main": "./client/dist/extension", 63 | "types": "./client/dist/extension.d.ts", 64 | "engines": { 65 | "node": ">= 20", 66 | "vscode": "^1.87.0" 67 | }, 68 | "capabilities": { 69 | "untrustedWorkspaces": { 70 | "supported": true 71 | } 72 | }, 73 | "categories": [ 74 | "Programming Languages" 75 | ], 76 | "activationEvents": [], 77 | "publisher": "PowerQuery", 78 | "contributes": { 79 | "commands": [ 80 | { 81 | "command": "powerquery.extractDataflowDocument", 82 | "title": "Extract document from dataflow.json", 83 | "category": "powerquery" 84 | }, 85 | { 86 | "command": "powerquery.mEscapeText", 87 | "title": "Encode selection as an M text value", 88 | "category": "powerquery" 89 | }, 90 | { 91 | "command": "powerquery.mUnescapeText", 92 | "title": "Remove M text encoding from selection", 93 | "category": "powerquery" 94 | }, 95 | { 96 | "command": "powerquery.jsonUnescapeText", 97 | "title": "Remove JSON string encoding from selection", 98 | "category": "powerquery" 99 | }, 100 | { 101 | "command": "powerquery.jsonEscapeText", 102 | "title": "Encode selection as a JSON value", 103 | "category": "powerquery" 104 | } 105 | ], 106 | "menus": { 107 | "commandPalette": [ 108 | { 109 | "command": "powerquery.extractDataflowDocument", 110 | "when": "editorIsOpen && editorLangId == json" 111 | }, 112 | { 113 | "command": "powerquery.mEscapeText", 114 | "when": "editorHasSelection" 115 | }, 116 | { 117 | "command": "powerquery.mUnescapeText", 118 | "when": "editorHasSelection" 119 | }, 120 | { 121 | "command": "powerquery.jsonUnescapeText", 122 | "when": "editorHasSelection" 123 | }, 124 | { 125 | "command": "powerquery.jsonEscapeText", 126 | "when": "editorHasSelection" 127 | } 128 | ] 129 | }, 130 | "configuration": { 131 | "type": "object", 132 | "title": "Power Query", 133 | "properties": { 134 | "powerquery.benchmark.enable": { 135 | "scope": "window", 136 | "type": "boolean", 137 | "default": false, 138 | "description": "Recommended always off. Enables benchmark traces to be generated for the extension." 139 | }, 140 | "powerquery.client.additionalSymbolsDirectories": { 141 | "scope": "machine-overridable", 142 | "type": "array", 143 | "items": { 144 | "type": "string" 145 | }, 146 | "examples": [ 147 | "c:\\PowerQuerySymbols\\" 148 | ], 149 | "markdownDescription": "One or more absolute file system paths to directories containing M language symbols in json format." 150 | }, 151 | "powerquery.diagnostics.isWorkspaceCacheAllowed": { 152 | "scope": "window", 153 | "type": "boolean", 154 | "default": true, 155 | "description": "Recommended always on. Toggles internal caching causing performance degregation when off. Used to find hot paths in the extension." 156 | }, 157 | "powerquery.diagnostics.typeStrategy": { 158 | "scope": "window", 159 | "type": "string", 160 | "default": "Primitive", 161 | "description": "Sets what strategy is used by the type analysis. Extended is useful for small scripts but can hang on larger, complicated files. If performance isn't acceptable then fallback to Primitive.", 162 | "enum": [ 163 | "Extended", 164 | "Primitive" 165 | ] 166 | }, 167 | "powerquery.general.experimental": { 168 | "scope": "window", 169 | "type": "boolean", 170 | "default": false, 171 | "description": "Specifies whether to enable experimental features." 172 | }, 173 | "powerquery.general.locale": { 174 | "scope": "window", 175 | "type": "string", 176 | "description": "Locale to use for errors and other messages returned by the language parser.", 177 | "enum": [ 178 | "bg-BG", 179 | "ca-EZ", 180 | "cs-CZ", 181 | "da-DK", 182 | "de-DE", 183 | "el-GR", 184 | "en-US", 185 | "es-ES", 186 | "et-EE", 187 | "eu-ES", 188 | "fi-FI", 189 | "fr-FR", 190 | "gl-ES", 191 | "hi-IN", 192 | "hr-HR", 193 | "hu-HU", 194 | "id-ID", 195 | "it-IT", 196 | "ja-JP", 197 | "kk-KZ", 198 | "ko-KR", 199 | "lt-LT", 200 | "lv-LV", 201 | "ms-MY", 202 | "nb-NO", 203 | "nl-NL", 204 | "pl-PL", 205 | "pt-BR", 206 | "pt-PT", 207 | "ro-RO", 208 | "ru-RU", 209 | "sk-SK", 210 | "sl-SI", 211 | "sr-Cyrl-RS", 212 | "sr-Latn-RS", 213 | "sv-SE", 214 | "th-TH", 215 | "tr-TR", 216 | "uk-UA", 217 | "vi-VN", 218 | "zh-CN", 219 | "zh-TW" 220 | ], 221 | "default": "en-US" 222 | }, 223 | "powerquery.general.mode": { 224 | "scope": "window", 225 | "type": "string", 226 | "default": "Power Query", 227 | "description": "Changes what library functions are available.", 228 | "enum": [ 229 | "Power Query", 230 | "SDK" 231 | ] 232 | }, 233 | "powerquery.timeout.symbolTimeoutInMs": { 234 | "scope": "window", 235 | "type": "number", 236 | "default": 2000, 237 | "description": "Symbol provider timeout in milliseconds." 238 | }, 239 | "powerquery.trace.server": { 240 | "scope": "window", 241 | "type": "string", 242 | "enum": [ 243 | "off", 244 | "messages", 245 | "verbose" 246 | ], 247 | "default": "off", 248 | "description": "Traces the communication between VS Code and the language server." 249 | }, 250 | "powerquery.editor.transformTarget": { 251 | "scope": "window", 252 | "type": "string", 253 | "enum": [ 254 | "inPlace", 255 | "clipboard" 256 | ], 257 | "default": "inPlace", 258 | "description": "Default target for text transformation operations - allows the choice of in place (replacing the currently selected text) or storing the results on the clipboard." 259 | } 260 | } 261 | }, 262 | "languages": [ 263 | { 264 | "id": "powerquery", 265 | "aliases": [ 266 | "Power Query Formula Language", 267 | "Power Query/M", 268 | "Power Query", 269 | "powerquery", 270 | "pq", 271 | "M" 272 | ], 273 | "extensions": [ 274 | ".pq", 275 | ".pqout", 276 | ".pqm", 277 | ".m", 278 | ".mout" 279 | ], 280 | "configuration": "./language-configuration.json" 281 | } 282 | ], 283 | "grammars": [ 284 | { 285 | "language": "powerquery", 286 | "scopeName": "source.powerquery", 287 | "path": "./syntaxes/powerquery.tmLanguage.json" 288 | } 289 | ] 290 | }, 291 | "devDependencies": { 292 | "@typescript-eslint/eslint-plugin": "5.24.0", 293 | "@typescript-eslint/parser": "5.24.0", 294 | "eslint": "8.15.0", 295 | "eslint-config-prettier": "8.5.0", 296 | "eslint-plugin-prettier": "4.0.0", 297 | "eslint-plugin-promise": "6.0.0", 298 | "eslint-plugin-security": "1.5.0", 299 | "prettier": "2.6.2", 300 | "typescript": "4.6.4" 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /scripts/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | **/*.js -------------------------------------------------------------------------------- /scripts/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: false, 3 | }; 4 | -------------------------------------------------------------------------------- /scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-powerquery-scripts", 3 | "version": "0.0.61", 4 | "description": "Scripts for vscode-powerquery repository", 5 | "author": "Microsoft Corporation", 6 | "license": "MIT", 7 | "homepage": "https://github.com/microsoft/vscode-powerquery#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/microsoft/vscode-powerquery.git", 11 | "directory": "server" 12 | }, 13 | "issues": { 14 | "url": "https://github.com/microsoft/vscode-powerquery/issues" 15 | }, 16 | "scripts": { 17 | "init": "npm install-clean", 18 | "build": ".\\node_modules\\.bin\\tsc", 19 | "watch": ".\\node_modules\\.bin\\tsc -watch", 20 | "test": "mocha --reporter mocha-multi-reporters --reporter-options configFile=src/test/mochaConfig.json -r ts-node/register src/test/**/*.ts", 21 | "link:start": "npm link && npm uninstall @microsoft/powerquery-parser @microsoft/powerquery-language-services && git clean -xdf && npm install && npm link @microsoft/powerquery-parser @microsoft/powerquery-language-services", 22 | "link:stop": "npm unlink @microsoft/powerquery-parser @microsoft/powerquery-language-services && git clean -xdf && npm install && npm install @microsoft/powerquery-parser@latest @microsoft/powerquery-language-services@latest --save-exact", 23 | "lint": "eslint src --ext ts", 24 | "webpack-prod": "node_modules\\.bin\\webpack --mode production", 25 | "webpack-dev": "node_modules\\.bin\\webpack --watch --mode development" 26 | }, 27 | "engines": { 28 | "node": ">=18.17.0" 29 | }, 30 | "dependencies": { 31 | "@microsoft/powerquery-language-services": "0.10.1", 32 | "@microsoft/powerquery-parser": "0.15.10", 33 | "vscode-languageserver-textdocument": "1.0.4" 34 | }, 35 | "devDependencies": { 36 | "@types/mocha": "10.0.6", 37 | "@types/node": "20.12.12", 38 | "eslint": "8.15.0", 39 | "eslint-config-prettier": "8.5.0", 40 | "eslint-plugin-prettier": "4.0.0", 41 | "eslint-plugin-promise": "6.0.0", 42 | "eslint-plugin-security": "1.5.0", 43 | "mocha": "10.4.0", 44 | "prettier": "2.6.2", 45 | "ts-loader": "9.3.0", 46 | "ts-node": "10.7.0", 47 | "typescript": "5.4.5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /scripts/readme.md: -------------------------------------------------------------------------------- 1 | # Scripts 2 | 3 | ## BenchmarkFile 4 | 5 | Runs an lex/parse/inspection on a given file at the given location. Position is given in the form of `lineNumber:columnNumber`, where the first possible position is `0:0`. 6 | 7 | ### Example 8 | 9 | `.\node_modules\.bin\ts-node .\src\benchmarkFile.ts C:\git\vscode-powerquery\scripts\foo.pq 12:45` 10 | -------------------------------------------------------------------------------- /scripts/src/benchmarkFile.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import fs = require("fs"); 5 | import * as PQLS from "@microsoft/powerquery-language-services"; 6 | import * as PQP from "@microsoft/powerquery-parser"; 7 | import { InspectionSettings } from "@microsoft/powerquery-language-services"; 8 | 9 | import { LibrarySymbolUtils, LibraryUtils } from "../../server/src/library"; 10 | 11 | const args: ReadonlyArray = process.argv; 12 | 13 | function throwUnexpectedArgLength(): void { 14 | throw new Error(`Expected 4 args, found ${args.length} instead`); 15 | } 16 | 17 | function throwInvalidPosition(): void { 18 | throw new Error("The Position argument isn't in the form of lineNumberInteger,characterNumberInteger"); 19 | } 20 | 21 | function parsePosition(raw: string): PQLS.Position { 22 | const components: ReadonlyArray = raw.split(":").map((value: string) => value.trim()); 23 | 24 | if (components.length !== 2) { 25 | throwInvalidPosition(); 26 | } 27 | 28 | const chunk1: number = Number(components[0]); 29 | const chunk2: number = Number(components[1]); 30 | 31 | if (!Number.isInteger(chunk1) || !Number.isInteger(chunk2)) { 32 | throwInvalidPosition(); 33 | } 34 | 35 | return { 36 | line: Number.parseInt(components[0]), 37 | character: Number.parseInt(components[1]), 38 | }; 39 | } 40 | 41 | if (args.length < 4) { 42 | throwUnexpectedArgLength(); 43 | } else if (!fs.existsSync(args[2])) { 44 | throw new Error(`Expected the 3rd argument to be a filepath to an existing file. Received ${args[2]}`); 45 | } 46 | 47 | let contents: string = ""; 48 | 49 | const fileContents: string = fs.readFileSync(args[2], "utf8").replace(/^\uFEFF/, ""); 50 | const position: PQLS.Position = parsePosition(args[3]); 51 | 52 | const library: PQLS.Library.ILibrary = LibraryUtils.createLibrary( 53 | [LibrarySymbolUtils.getSymbolsForLocaleAndMode(PQP.Locale.en_US, "Power Query")], 54 | [], 55 | ); 56 | 57 | const inspectionSettings: InspectionSettings = PQLS.InspectionUtils.inspectionSettings( 58 | { 59 | ...PQP.DefaultSettings, 60 | traceManager: new PQP.Trace.BenchmarkTraceManager((message: string) => (contents += message)), 61 | }, 62 | { 63 | isWorkspaceCacheAllowed: true, 64 | library, 65 | }, 66 | ); 67 | 68 | const triedInspect: Promise< 69 | PQP.Result, PQP.Lexer.LexError.TLexError | PQP.Parser.ParseError.TParseError> 70 | > = PQLS.Inspection.tryInspect(inspectionSettings, fileContents, position, undefined); 71 | 72 | triedInspect 73 | .then( 74 | ( 75 | result: PQP.Result< 76 | Promise, 77 | PQP.Lexer.LexError.TLexError | PQP.Parser.ParseError.TParseError 78 | >, 79 | ) => { 80 | console.log(`isOk: ${PQP.ResultUtils.isOk(result)}`); 81 | }, 82 | ) 83 | .catch((error: unknown) => { 84 | throw error; 85 | }) 86 | .finally(() => console.log(contents)); 87 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "rootDirs": ["src", "../server"] 5 | }, 6 | "include": ["src/**/*.ts", "../server/**/*.ts"], 7 | "exclude": ["node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /server/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | **/*.js -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: false, 3 | }; 4 | -------------------------------------------------------------------------------- /server/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": ["ts-node/register"], 3 | "extensions": ["ts"], 4 | "spec": "src/test/**/*.test.ts" 5 | } 6 | -------------------------------------------------------------------------------- /server/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "recommendations": [ 4 | "esbenp.prettier-vscode" 5 | ], 6 | "unwantedRecommendations": [] 7 | } 8 | -------------------------------------------------------------------------------- /server/mochaReporterConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporterEnabled": "spec, mocha-junit-reporter" 3 | } 4 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-powerquery-server", 3 | "version": "0.0.61", 4 | "description": "Power Query language server implementation.", 5 | "author": "Microsoft Corporation", 6 | "license": "MIT", 7 | "homepage": "https://github.com/microsoft/vscode-powerquery#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/microsoft/vscode-powerquery.git", 11 | "directory": "server" 12 | }, 13 | "issues": { 14 | "url": "https://github.com/microsoft/vscode-powerquery/issues" 15 | }, 16 | "scripts": { 17 | "init": "npm install-clean", 18 | "build": ".\\node_modules\\.bin\\tsc", 19 | "watch": ".\\node_modules\\.bin\\tsc -watch", 20 | "test": "mocha --no-package --reporter mocha-multi-reporters --reporter-options configFile=mochaReporterConfig.json", 21 | "link:start": "npm link && npm uninstall @microsoft/powerquery-parser @microsoft/powerquery-formatter @microsoft/powerquery-language-services && git clean -xdf && npm install && npm link @microsoft/powerquery-parser @microsoft/powerquery-formatter @microsoft/powerquery-language-services", 22 | "link:stop": "npm unlink @microsoft/powerquery-parser @microsoft/powerquery-formatter @microsoft/powerquery-language-services && git clean -xdf && npm install && npm install @microsoft/powerquery-parser@latest @microsoft/powerquery-formatter@latest @microsoft/powerquery-language-services@latest --save-exact", 23 | "lint": "eslint src --ext ts", 24 | "webpack-prod": "node_modules\\.bin\\webpack --mode production", 25 | "webpack-dev": "node_modules\\.bin\\webpack --watch --mode development" 26 | }, 27 | "main": "lib\\server.d.ts", 28 | "types": "lib\\server.d.ts", 29 | "engines": { 30 | "node": ">=18.17.0" 31 | }, 32 | "dependencies": { 33 | "@microsoft/powerquery-formatter": "0.3.14", 34 | "@microsoft/powerquery-language-services": "0.10.1", 35 | "@microsoft/powerquery-parser": "0.15.10", 36 | "vscode-languageserver": "9.0.1", 37 | "vscode-languageserver-textdocument": "1.0.11", 38 | "vscode-languageserver-types": "3.17.5" 39 | }, 40 | "devDependencies": { 41 | "@types/chai": "4.3.1", 42 | "@types/mocha": "10.0.6", 43 | "@types/node": "20.12.12", 44 | "@types/vscode": "1.87.0", 45 | "chai": "4.3.6", 46 | "cross-env": "7.0.3", 47 | "eslint": "8.15.0", 48 | "eslint-config-prettier": "8.5.0", 49 | "eslint-plugin-prettier": "4.0.0", 50 | "eslint-plugin-security": "1.5.0", 51 | "mocha": "10.4.0", 52 | "mocha-junit-reporter": "2.2.1", 53 | "mocha-multi-reporters": "1.5.1", 54 | "prettier": "2.6.2", 55 | "ts-loader": "9.3.0", 56 | "ts-node": "10.7.0", 57 | "typescript": "5.4.5", 58 | "webpack": "5.95.0", 59 | "webpack-cli": "5.1.4" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /server/src/cancellationToken/cancellationTokenAdapter.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as LS from "vscode-languageserver/node"; 5 | import * as PQP from "@microsoft/powerquery-parser"; 6 | 7 | export class CancellationTokenAdapter implements PQP.ICancellationToken { 8 | private cancelReason: string | undefined; 9 | 10 | constructor( 11 | protected readonly parserCancellationToken: PQP.ICancellationToken, 12 | protected readonly languageServerCancellationToken: LS.CancellationToken, 13 | ) {} 14 | 15 | public isCancelled(): boolean { 16 | return ( 17 | this.parserCancellationToken.isCancelled() || this.languageServerCancellationToken.isCancellationRequested 18 | ); 19 | } 20 | 21 | public throwIfCancelled(): void { 22 | if (this.isCancelled()) { 23 | this.parserCancellationToken.cancel(this.cancelReason ?? "Language server cancellation requested"); 24 | this.parserCancellationToken.throwIfCancelled(); 25 | } 26 | } 27 | 28 | public cancel(reason: string): void { 29 | this.cancelReason = reason; 30 | this.parserCancellationToken.cancel(reason); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/src/cancellationToken/cancellationTokenUtils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as LS from "vscode-languageserver/node"; 5 | import * as PQP from "@microsoft/powerquery-parser"; 6 | import { CancellationTokenAdapter } from "./cancellationTokenAdapter"; 7 | 8 | export function createAdapterOrTimedCancellation( 9 | cancellationToken: LS.CancellationToken | undefined, 10 | timeoutInMs: number, 11 | ): PQP.ICancellationToken { 12 | return cancellationToken ? createAdapter(cancellationToken, timeoutInMs) : createTimedCancellation(timeoutInMs); 13 | } 14 | 15 | function createAdapter(cancellationToken: LS.CancellationToken, timeoutInMs: number): CancellationTokenAdapter { 16 | return new CancellationTokenAdapter(createTimedCancellation(timeoutInMs), cancellationToken); 17 | } 18 | 19 | function createTimedCancellation(timeoutInMs: number): PQP.TimedCancellationToken { 20 | return new PQP.TimedCancellationToken(timeoutInMs); 21 | } 22 | -------------------------------------------------------------------------------- /server/src/cancellationToken/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | export * as CancellationTokenUtils from "./cancellationTokenUtils"; 5 | export * from "./cancellationTokenAdapter"; 6 | -------------------------------------------------------------------------------- /server/src/errorUtils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as LS from "vscode-languageserver/node"; 5 | import * as PQP from "@microsoft/powerquery-parser"; 6 | import { Trace, TraceManager } from "@microsoft/powerquery-parser/lib/powerquery-parser/common/trace"; 7 | 8 | export interface FormatErrorMetadata { 9 | readonly child: FormatErrorMetadata | undefined; 10 | readonly topOfStack: string | undefined; 11 | readonly message: string | undefined; 12 | readonly name: string; 13 | } 14 | 15 | export function handleError( 16 | connection: LS.Connection, 17 | value: unknown, 18 | action: string, 19 | traceManager: TraceManager, 20 | ): void { 21 | const trace: Trace = traceManager.entry("handleError", action, undefined); 22 | let vscodeMessage: string; 23 | 24 | if (PQP.CommonError.isCommonError(value) && value.innerError instanceof PQP.CommonError.CancellationError) { 25 | vscodeMessage = `CancellationError during ${action}.`; 26 | connection.console.info(vscodeMessage); 27 | } else if (PQP.CommonError.isCommonError(value) && value.innerError instanceof PQP.CommonError.InvariantError) { 28 | vscodeMessage = `InvariantError during ${action}.`; 29 | connection.console.warn(vscodeMessage); 30 | connection.console.warn(formatError(value.innerError)); 31 | } else if (value instanceof Error) { 32 | const error: Error = value; 33 | vscodeMessage = `Unexpected Error during ${action}.`; 34 | connection.console.error(vscodeMessage); 35 | connection.console.error(formatError(error)); 36 | } else { 37 | vscodeMessage = `unknown error value '${value}' during ${action}.`; 38 | connection.console.warn(vscodeMessage); 39 | } 40 | 41 | trace.exit({ vscodeMessage }); 42 | } 43 | 44 | export function formatError(error: Error): string { 45 | return JSON.stringify(formatErrorMetadata(error), null, 4); 46 | } 47 | 48 | function formatErrorMetadata(error: Error): FormatErrorMetadata { 49 | let child: FormatErrorMetadata | undefined; 50 | 51 | if ( 52 | error instanceof PQP.CommonError.CommonError || 53 | error instanceof PQP.Lexer.LexError.LexError || 54 | error instanceof PQP.Parser.ParseError.ParseError 55 | ) { 56 | child = formatErrorMetadata(error.innerError); 57 | } 58 | 59 | const splitLines: ReadonlyArray | undefined = error.stack?.split("\n"); 60 | 61 | return { 62 | child, 63 | topOfStack: splitLines?.slice(0, 4).join("\n"), 64 | message: error.message, 65 | name: error.constructor.name, 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /server/src/eventHandlerUtils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import { 5 | CancellationToken, 6 | Disposable, 7 | ErrorCodes, 8 | GenericRequestHandler, 9 | LSPErrorCodes, 10 | ResponseError, 11 | } from "vscode-languageserver/node"; 12 | 13 | interface RuntimeEnvironment { 14 | readonly timer: { 15 | readonly setImmediate: (callback: (...args: unknown[]) => void, ...args: unknown[]) => Disposable; 16 | readonly setTimeout: (callback: (...args: unknown[]) => void, ms: number, ...args: unknown[]) => Disposable; 17 | }; 18 | } 19 | 20 | const environment: RuntimeEnvironment = { 21 | timer: { 22 | setImmediate(callback: (...args: unknown[]) => void, ...args: unknown[]): Disposable { 23 | const handle: NodeJS.Timeout = setTimeout(callback, 0, ...args); 24 | 25 | return { dispose: () => clearTimeout(handle) }; 26 | }, 27 | setTimeout(callback: (...args: unknown[]) => void, ms: number, ...args: unknown[]): Disposable { 28 | const handle: NodeJS.Timeout = setTimeout(callback, ms, ...args); 29 | 30 | return { dispose: () => clearTimeout(handle) }; 31 | }, 32 | }, 33 | }; 34 | 35 | export function genericRequestHandler(func: (params: T) => R): GenericRequestHandler { 36 | const handler: GenericRequestHandler = (params: T): R | ResponseError => { 37 | try { 38 | return func(params); 39 | } catch (error: unknown) { 40 | return new ResponseError( 41 | ErrorCodes.InternalError, 42 | error instanceof Error ? error.message : "An unknown error occurred", 43 | error, 44 | ); 45 | } 46 | }; 47 | 48 | return handler; 49 | } 50 | 51 | export function runSafeAsync( 52 | func: () => Thenable, 53 | errorVal: T, 54 | errorMessage: string, 55 | token: CancellationToken, 56 | ): Thenable> { 57 | return new Promise>((resolve: (value: T | ResponseError) => void) => { 58 | environment.timer.setImmediate(() => { 59 | if (token.isCancellationRequested) { 60 | resolve(cancelValue()); 61 | 62 | return; 63 | } 64 | 65 | // eslint-disable-next-line promise/prefer-await-to-then 66 | return func().then( 67 | (result: T) => { 68 | if (token.isCancellationRequested) { 69 | resolve(cancelValue()); 70 | } else { 71 | resolve(result); 72 | } 73 | }, 74 | (e: Error) => { 75 | // TODO: Should we be passing through tracemanager? 76 | console.error(formatError(errorMessage, e)); 77 | resolve(errorVal); 78 | }, 79 | ); 80 | }); 81 | }); 82 | } 83 | 84 | function cancelValue(): ResponseError { 85 | return new ResponseError(LSPErrorCodes.RequestCancelled, "Request cancelled"); 86 | } 87 | 88 | function formatError(message: string, err: unknown): string { 89 | if (err instanceof Error) { 90 | const error: Error = err as Error; 91 | 92 | return `formatError: ${message}: ${error.message}\n${error.stack}`; 93 | } else if (typeof err === "string") { 94 | return `formatError: ${message}: ${err}`; 95 | } else if (err && typeof err === "object" && "toString" in err) { 96 | return `formatError: ${message}: ${err.toString()}`; 97 | } 98 | 99 | return message; 100 | } 101 | -------------------------------------------------------------------------------- /server/src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | export * from "./server"; 5 | export * from "./library"; 6 | -------------------------------------------------------------------------------- /server/src/library/externalLibraryUtils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQLS from "@microsoft/powerquery-language-services"; 5 | 6 | export type ExternalSymbolLibrary = ReadonlyArray; 7 | 8 | export function getSymbols(): ReadonlyArray { 9 | return Array.from(externalLibraryByName.values()); 10 | } 11 | 12 | export function addLibaries(symbols: ReadonlyMap): void { 13 | for (const [key, value] of symbols) { 14 | externalLibraryByName.set(key, value); 15 | } 16 | } 17 | 18 | export function removeLibraries(libraryNames: ReadonlyArray): void { 19 | for (const libraryName of libraryNames) { 20 | externalLibraryByName.delete(libraryName); 21 | } 22 | } 23 | 24 | const externalLibraryByName: Map = new Map(); 25 | -------------------------------------------------------------------------------- /server/src/library/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | export * as ExternalLibraryUtils from "./externalLibraryUtils"; 5 | export * as LibrarySymbolUtils from "./librarySymbolUtils"; 6 | export * as LibraryUtils from "./libraryUtils"; 7 | export * as ModuleLibraryUtils from "./moduleLibraryUtils"; 8 | -------------------------------------------------------------------------------- /server/src/library/librarySymbolUtils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQLS from "@microsoft/powerquery-language-services"; 5 | import * as PQP from "@microsoft/powerquery-parser"; 6 | 7 | import * as SdkLibrarySymbolsEnUs from "./sdk/sdk-enUs.json"; 8 | import * as StandardLibrarySymbolsEnUs from "./standard/standard-enUs.json"; 9 | 10 | export function getSymbolsForLocaleAndMode( 11 | locale: string, 12 | mode: "Power Query" | "SDK", 13 | ): ReadonlyArray { 14 | switch (mode) { 15 | case "Power Query": 16 | return StandardLibrarySymbolByLocale.get(locale) ?? StandardLibrarySymbolsEnUs; 17 | 18 | case "SDK": 19 | return SdkLibrarySymbols.get(locale) ?? SdkLibrarySymbolsEnUs; 20 | 21 | default: 22 | throw new PQP.CommonError.InvariantError(`Unknown mode: ${mode}`); 23 | } 24 | } 25 | 26 | export function toLibraryDefinitions( 27 | librarySymbols: ReadonlyArray, 28 | ): ReadonlyMap { 29 | const libraryDefinitionsResult: PQP.PartialResult< 30 | ReadonlyMap, 31 | PQLS.LibrarySymbolUtils.IncompleteLibraryDefinitions, 32 | ReadonlyArray 33 | > = PQLS.LibrarySymbolUtils.createLibraryDefinitions(librarySymbols); 34 | 35 | let libraryDefinitions: ReadonlyMap; 36 | let invalidSymbols: ReadonlyArray; 37 | 38 | if (PQP.PartialResultUtils.isOk(libraryDefinitionsResult)) { 39 | libraryDefinitions = libraryDefinitionsResult.value; 40 | invalidSymbols = []; 41 | } else if (PQP.PartialResultUtils.isIncomplete(libraryDefinitionsResult)) { 42 | libraryDefinitions = libraryDefinitionsResult.partial.libraryDefinitions; 43 | invalidSymbols = libraryDefinitionsResult.partial.invalidSymbols; 44 | } else { 45 | libraryDefinitions = new Map(); 46 | invalidSymbols = libraryDefinitionsResult.error; 47 | } 48 | 49 | if (invalidSymbols.length) { 50 | console.warn(`Failed to convert library symbols: ${JSON.stringify(invalidSymbols)}`); 51 | } 52 | 53 | return libraryDefinitions; 54 | } 55 | 56 | const StandardLibrarySymbolByLocale: ReadonlyMap> = new Map([ 57 | [PQP.Locale.en_US, StandardLibrarySymbolsEnUs], 58 | ]); 59 | 60 | const SdkLibrarySymbols: ReadonlyMap> = new Map([ 61 | [PQP.Locale.en_US, SdkLibrarySymbolsEnUs], 62 | ]); 63 | -------------------------------------------------------------------------------- /server/src/library/libraryTypeResolver.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | // Contains smart type resolvers for library functions, 5 | // such as Table.AddColumn(...) returning a DefinedTable. 6 | 7 | import * as PQP from "@microsoft/powerquery-parser"; 8 | import { Type, TypeUtils } from "@microsoft/powerquery-parser/lib/powerquery-parser/language"; 9 | import { ExternalType } from "@microsoft/powerquery-language-services"; 10 | 11 | // Wraps an external type resolver with smart type resolvers for invocation requests. 12 | export function wrapSmartTypeResolver( 13 | externalTypeResolver: ExternalType.TExternalTypeResolverFn, 14 | ): ExternalType.TExternalTypeResolverFn { 15 | return (request: ExternalType.TExternalTypeRequest): PQP.Language.Type.TPowerQueryType | undefined => { 16 | const type: PQP.Language.Type.TPowerQueryType | undefined = externalTypeResolver(request); 17 | 18 | if (!type || request.kind !== ExternalType.ExternalTypeRequestKind.Invocation) { 19 | return type; 20 | } 21 | 22 | const key: string = TypeUtils.nameOf(type, PQP.Trace.NoOpTraceManagerInstance, undefined); 23 | const maybeSmartTypeResolverFn: SmartTypeResolverFn | undefined = SmartTypeResolverFns.get(key); 24 | 25 | if (maybeSmartTypeResolverFn) { 26 | const typeChecked: TypeUtils.CheckedInvocation = TypeUtils.typeCheckInvocation( 27 | request.args, 28 | // If it's an invocation type then it's assumed we 29 | // already confirmed the request is about a DefinedFunction. 30 | TypeUtils.assertAsDefinedFunction(type), 31 | PQP.Trace.NoOpTraceManagerInstance, 32 | undefined, 33 | ); 34 | 35 | if (isValidInvocation(typeChecked)) { 36 | return maybeSmartTypeResolverFn(request.args); 37 | } 38 | } 39 | 40 | return type; 41 | }; 42 | } 43 | 44 | type SmartTypeResolverFn = (args: ReadonlyArray) => Type.TPowerQueryType | undefined; 45 | 46 | function isValidInvocation(typeChecked: TypeUtils.CheckedInvocation): boolean { 47 | return !typeChecked.extraneous.length && !typeChecked.invalid.size && !typeChecked.missing.length; 48 | } 49 | 50 | function resolveTableAddColumn(args: ReadonlyArray): Type.TPowerQueryType | undefined { 51 | const table: Type.TPowerQueryType = TypeUtils.assertAsTable(PQP.Assert.asDefined(args[0])); 52 | const columnName: Type.TText = TypeUtils.assertAsText(PQP.Assert.asDefined(args[1])); 53 | const columnGenerator: Type.TFunction = TypeUtils.assertAsFunction(PQP.Assert.asDefined(args[2])); 54 | 55 | const maybeColumnType: Type.TPowerQueryType | undefined = 56 | args.length === 4 ? TypeUtils.assertAsType(PQP.Assert.asDefined(args[3])) : undefined; 57 | 58 | // We can't mutate the given table without being able to resolve columnName to a literal. 59 | if (!TypeUtils.isTextLiteral(columnName)) { 60 | return undefined; 61 | } 62 | 63 | let columnType: Type.TPowerQueryType; 64 | 65 | if (maybeColumnType !== undefined) { 66 | columnType = maybeColumnType; 67 | } else if (TypeUtils.isDefinedFunction(columnGenerator)) { 68 | columnType = columnGenerator.returnType; 69 | } else { 70 | columnType = Type.AnyInstance; 71 | } 72 | 73 | const normalizedColumnName: string = PQP.Language.TextUtils.normalizeIdentifier(columnName.literal.slice(1, -1)); 74 | 75 | if (TypeUtils.isDefinedTable(table)) { 76 | // We can't overwrite an existing key. 77 | if (table.fields.has(normalizedColumnName)) { 78 | return Type.NoneInstance; 79 | } 80 | 81 | return TypeUtils.definedTable( 82 | table.isNullable, 83 | new PQP.OrderedMap([...table.fields.entries(), [normalizedColumnName, columnType]]), 84 | table.isOpen, 85 | ); 86 | } else { 87 | return TypeUtils.definedTable(table.isNullable, new PQP.OrderedMap([[normalizedColumnName, columnType]]), true); 88 | } 89 | } 90 | 91 | // We don't have a way to know when the library has a behavioral change. 92 | // The best we can do is check if a type signature changed by using TypeUtils.nameOf(invoked function). 93 | const SmartTypeResolverFns: ReadonlyMap = new Map([ 94 | [ 95 | "(table: table, newColumnName: text, columnGenerator: function, columnType: optional nullable type) => table", 96 | resolveTableAddColumn, 97 | ], 98 | ]); 99 | -------------------------------------------------------------------------------- /server/src/library/libraryUtils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQLS from "@microsoft/powerquery-language-services"; 5 | import * as PQP from "@microsoft/powerquery-parser"; 6 | 7 | import * as SdkLibrarySymbolsEnUs from "./sdk/sdk-enUs.json"; 8 | import * as StandardLibrarySymbolsEnUs from "./standard/standard-enUs.json"; 9 | import { LibrarySymbolUtils } from "."; 10 | import { wrapSmartTypeResolver } from "./libraryTypeResolver"; 11 | 12 | export const StandardLibrarySymbolByLocale: ReadonlyMap< 13 | string, 14 | ReadonlyArray 15 | > = new Map([[PQP.Locale.en_US, StandardLibrarySymbolsEnUs]]); 16 | 17 | export const SdkLibrarySymbols: ReadonlyMap> = new Map([ 18 | [PQP.Locale.en_US, SdkLibrarySymbolsEnUs], 19 | ]); 20 | 21 | export function clearCache(): void { 22 | libraryByCacheKey.clear(); 23 | } 24 | 25 | export function createCacheKey(locale: string, mode: string): string { 26 | return `${locale};${mode}`; 27 | } 28 | 29 | export function createLibrary( 30 | staticLibraryDefinitionCollection: ReadonlyArray>, 31 | dynamicLibraryDefinitionCollection: ReadonlyArray<() => ReadonlyMap>, 32 | ): PQLS.Library.ILibrary { 33 | const staticLibraryDefinitions: Map = new Map(); 34 | 35 | for (const collection of staticLibraryDefinitionCollection) { 36 | for (const [key, value] of LibrarySymbolUtils.toLibraryDefinitions(collection).entries()) { 37 | staticLibraryDefinitions.set(key, value); 38 | } 39 | } 40 | 41 | const dynamicLibraryDefinitions: () => ReadonlyMap = () => { 42 | const result: Map = new Map(); 43 | 44 | for (const collection of dynamicLibraryDefinitionCollection) { 45 | for (const [key, value] of collection()) { 46 | result.set(key, value); 47 | } 48 | } 49 | 50 | return result; 51 | }; 52 | 53 | const library: PQLS.Library.ILibrary = { 54 | externalTypeResolver: wrapSmartTypeResolver( 55 | PQLS.LibraryDefinitionUtils.externalTypeResolver({ 56 | staticLibraryDefinitions, 57 | dynamicLibraryDefinitions, 58 | }), 59 | ), 60 | libraryDefinitions: { 61 | staticLibraryDefinitions, 62 | dynamicLibraryDefinitions, 63 | }, 64 | }; 65 | 66 | return library; 67 | } 68 | 69 | export function createLibraryAndSetCache( 70 | cacheKey: string, 71 | staticLibraryDefinitionCollection: ReadonlyArray>, 72 | dynamicLibraryDefinitionCollection: ReadonlyArray<() => ReadonlyMap>, 73 | ): PQLS.Library.ILibrary { 74 | const library: PQLS.Library.ILibrary = createLibrary( 75 | staticLibraryDefinitionCollection, 76 | dynamicLibraryDefinitionCollection, 77 | ); 78 | 79 | libraryByCacheKey.set(cacheKey, library); 80 | 81 | return library; 82 | } 83 | 84 | export function getLibrary(cacheKey: string): PQLS.Library.ILibrary | undefined { 85 | return libraryByCacheKey.get(cacheKey); 86 | } 87 | 88 | const libraryByCacheKey: Map = new Map(); 89 | -------------------------------------------------------------------------------- /server/src/library/moduleLibraryUtils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQLS from "@microsoft/powerquery-language-services"; 5 | 6 | import { LibrarySymbolUtils } from "."; 7 | 8 | export function clearCache(): void { 9 | moduleLibraryByUri.clear(); 10 | } 11 | 12 | export function getAsDynamicLibraryDefinitions( 13 | uri: string, 14 | ): () => ReadonlyMap { 15 | return () => { 16 | for (const [connectorUri, libraryDefinitions] of moduleLibraryByUri.entries()) { 17 | if (uri.startsWith(connectorUri)) { 18 | return libraryDefinitions; 19 | } 20 | } 21 | 22 | return new Map(); 23 | }; 24 | } 25 | 26 | export function getModuleCount(): number { 27 | return moduleLibraryByUri.size; 28 | } 29 | 30 | export function onModuleAdded(uri: string, moduleSymbols: ReadonlyArray): void { 31 | moduleLibraryByUri.set(uri, LibrarySymbolUtils.toLibraryDefinitions(moduleSymbols)); 32 | } 33 | 34 | export function onModuleRemoved(uri: string): void { 35 | moduleLibraryByUri.delete(uri); 36 | } 37 | 38 | const moduleLibraryByUri: Map> = new Map(); 39 | -------------------------------------------------------------------------------- /server/src/server.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as LS from "vscode-languageserver/node"; 5 | import * as PQF from "@microsoft/powerquery-formatter"; 6 | import * as PQLS from "@microsoft/powerquery-language-services"; 7 | import * as PQP from "@microsoft/powerquery-parser"; 8 | import { TextDocument } from "vscode-languageserver-textdocument"; 9 | 10 | import * as ErrorUtils from "./errorUtils"; 11 | import * as EventHandlerUtils from "./eventHandlerUtils"; 12 | import * as TraceManagerUtils from "./traceManagerUtils"; 13 | import { ExternalLibraryUtils, LibraryUtils, ModuleLibraryUtils } from "./library"; 14 | import { SettingsUtils } from "./settings"; 15 | 16 | type LibraryJson = ReadonlyArray; 17 | 18 | interface SemanticTokenParams { 19 | readonly textDocumentUri: string; 20 | readonly cancellationToken: LS.CancellationToken; 21 | } 22 | 23 | interface ModuleLibraryUpdatedParams { 24 | readonly workspaceUriPath: string; 25 | readonly library: LibraryJson; 26 | } 27 | 28 | interface AddLibrarySymbolsParams { 29 | readonly librarySymbols: ReadonlyArray<[string, LibraryJson]>; 30 | } 31 | 32 | interface RemoveLibrarySymbolsParams { 33 | readonly librariesToRemove: ReadonlyArray; 34 | } 35 | 36 | // Create a connection for the server. The connection uses Node's IPC as a transport. 37 | // Also include all preview / proposed LSP features. 38 | const connection: LS.Connection = LS.createConnection(LS.ProposedFeatures.all); 39 | const documents: LS.TextDocuments = new LS.TextDocuments(TextDocument); 40 | 41 | connection.onCompletion( 42 | async ( 43 | params: LS.TextDocumentPositionParams, 44 | cancellationToken: LS.CancellationToken, 45 | ): Promise => { 46 | const document: TextDocument | undefined = documents.get(params.textDocument.uri); 47 | 48 | if (document === undefined) { 49 | return []; 50 | } 51 | 52 | const pqpCancellationToken: PQP.ICancellationToken = SettingsUtils.createCancellationToken(cancellationToken); 53 | 54 | const traceManager: PQP.Trace.TraceManager = TraceManagerUtils.createTraceManager( 55 | params.textDocument.uri, 56 | "onCompletion", 57 | params.position, 58 | ); 59 | 60 | const analysis: PQLS.Analysis = createAnalysis(document, traceManager); 61 | 62 | const result: PQP.Result = 63 | await analysis.getAutocompleteItems(params.position, pqpCancellationToken); 64 | 65 | if (PQP.ResultUtils.isOk(result)) { 66 | return result.value ?? []; 67 | } else { 68 | ErrorUtils.handleError(connection, result.error, "onCompletion", traceManager); 69 | 70 | return []; 71 | } 72 | }, 73 | ); 74 | 75 | connection.onDefinition(async (params: LS.DefinitionParams, cancellationToken: LS.CancellationToken) => { 76 | const document: TextDocument | undefined = documents.get(params.textDocument.uri); 77 | 78 | if (document === undefined) { 79 | return undefined; 80 | } 81 | 82 | const pqpCancellationToken: PQP.ICancellationToken = SettingsUtils.createCancellationToken(cancellationToken); 83 | 84 | const traceManager: PQP.Trace.TraceManager = TraceManagerUtils.createTraceManager( 85 | params.textDocument.uri, 86 | "onDefinition", 87 | params.position, 88 | ); 89 | 90 | const analysis: PQLS.Analysis = createAnalysis(document, traceManager); 91 | 92 | const result: PQP.Result = await analysis.getDefinition( 93 | params.position, 94 | pqpCancellationToken, 95 | ); 96 | 97 | if (PQP.ResultUtils.isOk(result)) { 98 | return result.value ?? []; 99 | } else { 100 | ErrorUtils.handleError(connection, result.error, "onComplection", traceManager); 101 | 102 | return []; 103 | } 104 | }); 105 | 106 | connection.onDidChangeConfiguration(async () => { 107 | await SettingsUtils.initializeServerSettings(connection); 108 | connection.languages.diagnostics.refresh(); 109 | }); 110 | 111 | documents.onDidClose(async (event: LS.TextDocumentChangeEvent) => { 112 | // Clear any errors associated with this file 113 | await connection.sendDiagnostics({ 114 | uri: event.document.uri, 115 | version: event.document.version, 116 | diagnostics: [], 117 | }); 118 | }); 119 | 120 | connection.onFoldingRanges(async (params: LS.FoldingRangeParams, cancellationToken: LS.CancellationToken) => { 121 | const document: TextDocument | undefined = documents.get(params.textDocument.uri); 122 | 123 | if (document === undefined) { 124 | return []; 125 | } 126 | 127 | const pqpCancellationToken: PQP.ICancellationToken = SettingsUtils.createCancellationToken(cancellationToken); 128 | const traceManager: PQP.Trace.TraceManager = TraceManagerUtils.createTraceManager(document.uri, "onFoldingRanges"); 129 | const analysis: PQLS.Analysis = createAnalysis(document, traceManager); 130 | 131 | const result: PQP.Result = 132 | await analysis.getFoldingRanges(pqpCancellationToken); 133 | 134 | if (PQP.ResultUtils.isOk(result)) { 135 | return result.value ?? []; 136 | } else { 137 | ErrorUtils.handleError(connection, result.error, "onFoldingRanges", traceManager); 138 | 139 | return []; 140 | } 141 | }); 142 | 143 | connection.onDocumentSymbol(documentSymbols); 144 | 145 | const emptyHover: LS.Hover = { 146 | range: undefined, 147 | contents: [], 148 | }; 149 | 150 | // eslint-disable-next-line require-await 151 | connection.onHover(async (params: LS.TextDocumentPositionParams, cancellationToken: LS.CancellationToken) => 152 | EventHandlerUtils.runSafeAsync( 153 | async () => { 154 | const document: TextDocument | undefined = documents.get(params.textDocument.uri); 155 | 156 | if (document === undefined) { 157 | return emptyHover; 158 | } 159 | 160 | const pqpCancellationToken: PQP.ICancellationToken = 161 | SettingsUtils.createCancellationToken(cancellationToken); 162 | 163 | const traceManager: PQP.Trace.TraceManager = TraceManagerUtils.createTraceManager( 164 | params.textDocument.uri, 165 | "onHover", 166 | params.position, 167 | ); 168 | 169 | const analysis: PQLS.Analysis = createAnalysis(document, traceManager); 170 | 171 | const result: PQP.Result = await analysis.getHover( 172 | params.position, 173 | pqpCancellationToken, 174 | ); 175 | 176 | if (PQP.ResultUtils.isOk(result)) { 177 | return result.value ?? emptyHover; 178 | } else { 179 | ErrorUtils.handleError(connection, result.error, "onHover", traceManager); 180 | 181 | return emptyHover; 182 | } 183 | }, 184 | emptyHover, 185 | `Error while computing hover for ${params.textDocument.uri}`, 186 | cancellationToken, 187 | ), 188 | ); 189 | 190 | connection.onInitialize((params: LS.InitializeParams) => { 191 | const capabilities: LS.ServerCapabilities = { 192 | completionProvider: { 193 | resolveProvider: false, 194 | }, 195 | definitionProvider: true, 196 | diagnosticProvider: { 197 | interFileDependencies: false, 198 | workspaceDiagnostics: false, 199 | }, 200 | documentFormattingProvider: true, 201 | documentSymbolProvider: { 202 | workDoneProgress: false, 203 | }, 204 | hoverProvider: true, 205 | renameProvider: true, 206 | signatureHelpProvider: { 207 | triggerCharacters: ["(", ","], 208 | }, 209 | textDocumentSync: LS.TextDocumentSyncKind.Incremental, 210 | workspace: { 211 | // TODO: Disabling until we've fully tested support for multiple workspace folders 212 | workspaceFolders: { 213 | supported: false, 214 | }, 215 | }, 216 | }; 217 | 218 | SettingsUtils.setHasConfigurationCapability(Boolean(params.capabilities.workspace?.configuration)); 219 | 220 | return { 221 | capabilities, 222 | }; 223 | }); 224 | 225 | connection.onInitialized(async () => { 226 | if (SettingsUtils.getHasConfigurationCapability()) { 227 | await connection.client.register(LS.DidChangeConfigurationNotification.type, undefined); 228 | } 229 | 230 | await SettingsUtils.initializeServerSettings(connection); 231 | }); 232 | 233 | connection.onRenameRequest(async (params: LS.RenameParams, cancellationToken: LS.CancellationToken) => { 234 | const document: TextDocument | undefined = documents.get(params.textDocument.uri.toString()); 235 | 236 | if (document === undefined) { 237 | return undefined; 238 | } 239 | 240 | const pqpCancellationToken: PQP.ICancellationToken = SettingsUtils.createCancellationToken(cancellationToken); 241 | const traceManager: PQP.Trace.TraceManager = TraceManagerUtils.createTraceManager(document.uri, "onRenameRequest"); 242 | const analysis: PQLS.Analysis = createAnalysis(document, traceManager); 243 | 244 | const result: PQP.Result = await analysis.getRenameEdits( 245 | params.position, 246 | params.newName, 247 | pqpCancellationToken, 248 | ); 249 | 250 | if (PQP.ResultUtils.isOk(result)) { 251 | return result.value ? { changes: { [document.uri]: result.value } } : undefined; 252 | } else { 253 | ErrorUtils.handleError(connection, result.error, "onRenameRequest", traceManager); 254 | 255 | return undefined; 256 | } 257 | }); 258 | 259 | connection.onRequest("powerquery/semanticTokens", async (params: SemanticTokenParams) => { 260 | const document: TextDocument | undefined = documents.get(params.textDocumentUri); 261 | 262 | if (document === undefined) { 263 | return []; 264 | } 265 | 266 | const pqpCancellationToken: PQP.ICancellationToken = SettingsUtils.createCancellationToken(undefined); 267 | const traceManager: PQP.Trace.TraceManager = TraceManagerUtils.createTraceManager(document.uri, "semanticTokens"); 268 | const analysis: PQLS.Analysis = createAnalysis(document, traceManager); 269 | 270 | const result: PQP.Result = 271 | await analysis.getPartialSemanticTokens(pqpCancellationToken); 272 | 273 | if (PQP.ResultUtils.isOk(result)) { 274 | return result.value ?? []; 275 | } else { 276 | ErrorUtils.handleError(connection, result.error, "semanticTokens", traceManager); 277 | 278 | return []; 279 | } 280 | }); 281 | 282 | connection.onRequest( 283 | "powerquery/moduleLibraryUpdated", 284 | EventHandlerUtils.genericRequestHandler((params: ModuleLibraryUpdatedParams) => { 285 | ModuleLibraryUtils.onModuleAdded(params.workspaceUriPath, params.library); 286 | LibraryUtils.clearCache(); 287 | connection.languages.diagnostics.refresh(); 288 | }), 289 | ); 290 | 291 | connection.onRequest( 292 | "powerquery/addLibrarySymbols", 293 | EventHandlerUtils.genericRequestHandler((params: AddLibrarySymbolsParams) => { 294 | // JSON-RPC doesn't support sending Maps, so we have to convert from tuple array. 295 | const symbolMaps: ReadonlyMap = new Map(params.librarySymbols); 296 | ExternalLibraryUtils.addLibaries(symbolMaps); 297 | LibraryUtils.clearCache(); 298 | connection.languages.diagnostics.refresh(); 299 | }), 300 | ); 301 | 302 | connection.onRequest( 303 | "powerquery/removeLibrarySymbols", 304 | EventHandlerUtils.genericRequestHandler((params: RemoveLibrarySymbolsParams) => { 305 | ExternalLibraryUtils.removeLibraries(params.librariesToRemove); 306 | LibraryUtils.clearCache(); 307 | connection.languages.diagnostics.refresh(); 308 | }), 309 | ); 310 | 311 | connection.onSignatureHelp( 312 | async ( 313 | params: LS.TextDocumentPositionParams, 314 | cancellationToken: LS.CancellationToken, 315 | ): Promise => { 316 | const emptySignatureHelp: LS.SignatureHelp = { 317 | signatures: [], 318 | activeParameter: undefined, 319 | activeSignature: 0, 320 | }; 321 | 322 | const document: TextDocument | undefined = documents.get(params.textDocument.uri); 323 | 324 | if (document === undefined) { 325 | return emptySignatureHelp; 326 | } 327 | 328 | const pqpCancellationToken: PQP.ICancellationToken = SettingsUtils.createCancellationToken(cancellationToken); 329 | 330 | const traceManager: PQP.Trace.TraceManager = TraceManagerUtils.createTraceManager( 331 | params.textDocument.uri, 332 | "onSignatureHelp", 333 | params.position, 334 | ); 335 | 336 | const analysis: PQLS.Analysis = createAnalysis(document, traceManager); 337 | 338 | const result: PQP.Result = 339 | await analysis.getSignatureHelp(params.position, pqpCancellationToken); 340 | 341 | if (PQP.ResultUtils.isOk(result)) { 342 | return result.value ?? emptySignatureHelp; 343 | } else { 344 | ErrorUtils.handleError(connection, result.error, "onSignatureHelp", traceManager); 345 | 346 | return emptySignatureHelp; 347 | } 348 | }, 349 | ); 350 | 351 | connection.onDocumentFormatting( 352 | async (params: LS.DocumentFormattingParams, cancellationToken: LS.CancellationToken): Promise => { 353 | const document: TextDocument | undefined = documents.get(params.textDocument.uri); 354 | 355 | if (document === undefined) { 356 | return []; 357 | } 358 | 359 | const traceManager: PQP.Trace.TraceManager = TraceManagerUtils.createTraceManager( 360 | params.textDocument.uri, 361 | "onDocumentFormatting", 362 | undefined, 363 | ); 364 | 365 | const result: PQP.Result = await PQLS.tryFormat( 366 | document, 367 | { 368 | ...PQP.DefaultSettings, 369 | ...PQF.DefaultSettings, 370 | cancellationToken: SettingsUtils.createCancellationToken(cancellationToken), 371 | traceManager, 372 | }, 373 | ); 374 | 375 | if (PQP.ResultUtils.isOk(result)) { 376 | return result.value ?? []; 377 | } else { 378 | ErrorUtils.handleError(connection, result.error, "onDocumentFormatting", traceManager); 379 | 380 | return []; 381 | } 382 | }, 383 | ); 384 | 385 | connection.languages.diagnostics.on( 386 | // eslint-disable-next-line require-await 387 | async (params: LS.DocumentDiagnosticParams, cancellationToken: LS.CancellationToken) => 388 | EventHandlerUtils.runSafeAsync( 389 | async () => { 390 | const document: TextDocument | undefined = documents.get(params.textDocument.uri); 391 | 392 | if (document === undefined) { 393 | return { 394 | kind: LS.DocumentDiagnosticReportKind.Full, 395 | items: [], 396 | }; 397 | } 398 | 399 | const diagnostics: LS.Diagnostic[] = await getDocumentDiagnostics(document, cancellationToken); 400 | 401 | return { 402 | kind: LS.DocumentDiagnosticReportKind.Full, 403 | items: diagnostics, 404 | }; 405 | }, 406 | { kind: LS.DocumentDiagnosticReportKind.Full, items: [] }, 407 | `Error while computing diagnostics for ${params.textDocument.uri}`, 408 | cancellationToken, 409 | ), 410 | ); 411 | 412 | // Make the text document manager listen on the connection 413 | // for open, change and close text document events 414 | documents.listen(connection); 415 | 416 | // Listen on the connection 417 | connection.listen(); 418 | 419 | function createAnalysis(document: TextDocument, traceManager: PQP.Trace.TraceManager): PQLS.Analysis { 420 | const localizedLibrary: PQLS.Library.ILibrary = SettingsUtils.getLibrary(document.uri); 421 | 422 | return PQLS.AnalysisUtils.analysis(document, SettingsUtils.createAnalysisSettings(localizedLibrary, traceManager)); 423 | } 424 | 425 | async function documentSymbols( 426 | params: LS.DocumentSymbolParams, 427 | cancellationToken: LS.CancellationToken, 428 | ): Promise { 429 | const document: TextDocument | undefined = documents.get(params.textDocument.uri); 430 | 431 | if (document === undefined) { 432 | return undefined; 433 | } 434 | 435 | const pqpCancellationToken: PQP.ICancellationToken = SettingsUtils.createCancellationToken(cancellationToken); 436 | 437 | const traceManager: PQP.Trace.TraceManager = TraceManagerUtils.createTraceManager( 438 | params.textDocument.uri, 439 | "onDocumentSymbol", 440 | ); 441 | 442 | const analysis: PQLS.Analysis = createAnalysis(document, traceManager); 443 | 444 | const triedParseState: PQP.Result = 445 | await analysis.getParseState(); 446 | 447 | if (PQP.ResultUtils.isError(triedParseState)) { 448 | ErrorUtils.handleError(connection, triedParseState.error, "onDocumentSymbol", traceManager); 449 | 450 | return undefined; 451 | } 452 | 453 | if (triedParseState.value === undefined) { 454 | return undefined; 455 | } 456 | 457 | try { 458 | return PQLS.getDocumentSymbols(triedParseState.value.contextState.nodeIdMapCollection, pqpCancellationToken); 459 | } catch (error: unknown) { 460 | ErrorUtils.handleError(connection, error, "onDocumentSymbol", traceManager); 461 | 462 | return undefined; 463 | } 464 | } 465 | 466 | async function getDocumentDiagnostics( 467 | document: TextDocument, 468 | cancellationToken: LS.CancellationToken, 469 | ): Promise { 470 | const traceManager: PQP.Trace.TraceManager = TraceManagerUtils.createTraceManager( 471 | document.uri, 472 | "getDocumentDiagnostics", 473 | ); 474 | 475 | const localizedLibrary: PQLS.Library.ILibrary = SettingsUtils.getLibrary(document.uri); 476 | 477 | const analysisSettings: PQLS.AnalysisSettings = SettingsUtils.createAnalysisSettings( 478 | localizedLibrary, 479 | traceManager, 480 | ); 481 | 482 | const validationSettings: PQLS.ValidationSettings = SettingsUtils.createValidationSettings( 483 | localizedLibrary, 484 | traceManager, 485 | SettingsUtils.createCancellationToken(cancellationToken), 486 | ); 487 | 488 | const result: PQP.Result = await PQLS.validate( 489 | document, 490 | analysisSettings, 491 | validationSettings, 492 | ); 493 | 494 | if (PQP.ResultUtils.isOk(result) && result.value) { 495 | return result.value.diagnostics; 496 | } else { 497 | ErrorUtils.handleError(connection, result, "getDocumentDiagnostics", traceManager); 498 | 499 | return []; 500 | } 501 | } 502 | -------------------------------------------------------------------------------- /server/src/settings/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | export * as SettingsUtils from "./settingsUtils"; 5 | export * from "./settings"; 6 | -------------------------------------------------------------------------------- /server/src/settings/settings.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQLS from "@microsoft/powerquery-language-services"; 5 | import * as PQP from "@microsoft/powerquery-parser"; 6 | 7 | export interface ServerSettings { 8 | checkForDuplicateIdentifiers: boolean; 9 | checkInvokeExpressions: boolean; 10 | experimental: boolean; 11 | isBenchmarksEnabled: boolean; 12 | isWorkspaceCacheAllowed: boolean; 13 | locale: string; 14 | mode: "Power Query" | "SDK"; 15 | symbolTimeoutInMs: number; 16 | typeStrategy: PQLS.TypeStrategy; 17 | } 18 | 19 | export const DefaultServerSettings: ServerSettings = { 20 | checkForDuplicateIdentifiers: true, 21 | checkInvokeExpressions: false, 22 | experimental: false, 23 | isBenchmarksEnabled: false, 24 | isWorkspaceCacheAllowed: true, 25 | locale: PQP.DefaultLocale, 26 | mode: "Power Query", 27 | symbolTimeoutInMs: 4000, 28 | typeStrategy: PQLS.TypeStrategy.Primitive, 29 | }; 30 | -------------------------------------------------------------------------------- /server/src/settings/settingsUtils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as LS from "vscode-languageserver/node"; 5 | import * as PQLS from "@microsoft/powerquery-language-services"; 6 | import * as PQP from "@microsoft/powerquery-parser"; 7 | 8 | import { DefaultServerSettings, ServerSettings } from "./settings"; 9 | import { ExternalLibraryUtils, LibrarySymbolUtils, LibraryUtils, ModuleLibraryUtils } from "../library"; 10 | import { CancellationTokenUtils } from "../cancellationToken"; 11 | 12 | const LanguageId: string = "powerquery"; 13 | 14 | let serverSettings: ServerSettings = DefaultServerSettings; 15 | let hasConfigurationCapability: boolean = false; 16 | 17 | export async function initializeServerSettings(connection: LS.Connection): Promise { 18 | serverSettings = await fetchConfigurationSettings(connection); 19 | } 20 | 21 | export function createAnalysisSettings( 22 | library: PQLS.Library.ILibrary, 23 | traceManager: PQP.Trace.TraceManager, 24 | ): PQLS.AnalysisSettings { 25 | return { 26 | inspectionSettings: createInspectionSettings(library, traceManager), 27 | isWorkspaceCacheAllowed: serverSettings.isWorkspaceCacheAllowed, 28 | traceManager, 29 | initialCorrelationId: undefined, 30 | }; 31 | } 32 | 33 | export function createCancellationToken(cancellationToken: LS.CancellationToken | undefined): PQP.ICancellationToken { 34 | return CancellationTokenUtils.createAdapterOrTimedCancellation(cancellationToken, serverSettings.symbolTimeoutInMs); 35 | } 36 | 37 | export function createInspectionSettings( 38 | library: PQLS.Library.ILibrary, 39 | traceManager: PQP.Trace.TraceManager, 40 | ): PQLS.InspectionSettings { 41 | return PQLS.InspectionUtils.inspectionSettings( 42 | { 43 | ...PQP.DefaultSettings, 44 | locale: serverSettings.locale, 45 | traceManager, 46 | }, 47 | { 48 | library, 49 | isWorkspaceCacheAllowed: serverSettings.isWorkspaceCacheAllowed, 50 | typeStrategy: serverSettings.typeStrategy, 51 | }, 52 | ); 53 | } 54 | 55 | export function createValidationSettings( 56 | library: PQLS.Library.ILibrary, 57 | traceManager: PQP.Trace.TraceManager, 58 | cancellationToken: PQP.ICancellationToken | undefined, 59 | ): PQLS.ValidationSettings { 60 | return PQLS.ValidationSettingsUtils.createValidationSettings( 61 | createInspectionSettings(library, traceManager), 62 | LanguageId, 63 | { 64 | cancellationToken, 65 | checkForDuplicateIdentifiers: serverSettings.checkForDuplicateIdentifiers, 66 | checkInvokeExpressions: serverSettings.checkInvokeExpressions, 67 | }, 68 | ); 69 | } 70 | 71 | export async function fetchConfigurationSettings(connection: LS.Connection): Promise { 72 | if (!hasConfigurationCapability) { 73 | return DefaultServerSettings; 74 | } 75 | 76 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 77 | const config: any = await connection.workspace.getConfiguration({ section: "powerquery" }); 78 | const typeStrategy: PQLS.TypeStrategy | undefined = config?.diagnostics?.typeStrategy; 79 | const experimental: boolean = config?.general?.experimental; 80 | 81 | return { 82 | checkForDuplicateIdentifiers: true, 83 | checkInvokeExpressions: false, 84 | experimental, 85 | isBenchmarksEnabled: config?.benchmark?.enable ?? false, 86 | isWorkspaceCacheAllowed: config?.diagnostics?.isWorkspaceCacheAllowed ?? true, 87 | locale: config?.general?.locale ?? PQP.DefaultLocale, 88 | mode: deriveMode(config?.general?.mode), 89 | symbolTimeoutInMs: config?.timeout?.symbolTimeoutInMs, 90 | typeStrategy: typeStrategy ? deriveTypeStrategy(typeStrategy) : PQLS.TypeStrategy.Primitive, 91 | }; 92 | } 93 | 94 | export function getServerSettings(): ServerSettings { 95 | return serverSettings; 96 | } 97 | 98 | export function getLibrary(uri: string): PQLS.Library.ILibrary { 99 | const cacheKey: string = LibraryUtils.createCacheKey(serverSettings.locale, serverSettings.mode); 100 | const result: PQLS.Library.ILibrary | undefined = LibraryUtils.getLibrary(cacheKey); 101 | 102 | if (result) { 103 | return result; 104 | } 105 | 106 | return LibraryUtils.createLibraryAndSetCache( 107 | cacheKey, 108 | [ 109 | LibrarySymbolUtils.getSymbolsForLocaleAndMode(serverSettings.locale, serverSettings.mode), 110 | ExternalLibraryUtils.getSymbols().flat(), 111 | ], 112 | [ModuleLibraryUtils.getAsDynamicLibraryDefinitions(uri)], 113 | ); 114 | } 115 | 116 | export function getHasConfigurationCapability(): boolean { 117 | return hasConfigurationCapability; 118 | } 119 | 120 | export function setHasConfigurationCapability(value: boolean): void { 121 | hasConfigurationCapability = value; 122 | } 123 | 124 | function deriveMode(value: string | undefined): "Power Query" | "SDK" { 125 | switch (value) { 126 | case "SDK": 127 | case "Power Query": 128 | return value; 129 | 130 | default: 131 | return "Power Query"; 132 | } 133 | } 134 | 135 | function deriveTypeStrategy(value: string): PQLS.TypeStrategy { 136 | switch (value) { 137 | case PQLS.TypeStrategy.Extended: 138 | case PQLS.TypeStrategy.Primitive: 139 | return value; 140 | 141 | default: 142 | throw new PQP.CommonError.InvariantError(`could not derive typeStrategy`); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /server/src/test/errorUtils.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQP from "@microsoft/powerquery-parser"; 5 | import { expect } from "chai"; 6 | 7 | import { formatError, FormatErrorMetadata } from "../errorUtils"; 8 | 9 | type AbridgedFormatErrorMetadata = Pick & { 10 | readonly child: AbridgedFormatErrorMetadata | undefined; 11 | }; 12 | 13 | function abridgedFormattedError(text: string): AbridgedFormatErrorMetadata { 14 | const metadata: FormatErrorMetadata = JSON.parse(text); 15 | 16 | return stripTopOfStack(metadata); 17 | } 18 | 19 | function stripTopOfStack(obj: FormatErrorMetadata): AbridgedFormatErrorMetadata { 20 | return { 21 | child: obj.child ? stripTopOfStack(obj.child) : undefined, 22 | message: obj.message, 23 | name: obj.name, 24 | }; 25 | } 26 | 27 | describe(`errorUtils`, () => { 28 | describe(`formatError`, () => { 29 | it(`unknown error`, () => { 30 | const actual: AbridgedFormatErrorMetadata = abridgedFormattedError(formatError(new Error("foobar"))); 31 | 32 | const expected: AbridgedFormatErrorMetadata = { 33 | child: undefined, 34 | message: "foobar", 35 | name: Error.name, 36 | }; 37 | 38 | expect(actual).to.deep.equal(expected); 39 | }); 40 | 41 | it(`InvariantError`, () => { 42 | const actual: AbridgedFormatErrorMetadata = abridgedFormattedError( 43 | formatError(new PQP.CommonError.CommonError(new PQP.CommonError.InvariantError("1 <> 2"))), 44 | ); 45 | 46 | const expected: AbridgedFormatErrorMetadata = { 47 | child: { 48 | child: undefined, 49 | message: "InvariantError: 1 <> 2", 50 | name: PQP.CommonError.InvariantError.name, 51 | }, 52 | message: "InvariantError: 1 <> 2", 53 | name: PQP.CommonError.CommonError.name, 54 | }; 55 | 56 | expect(actual).to.deep.equal(expected); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /server/src/test/standardLibrary.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQLS from "@microsoft/powerquery-language-services"; 5 | import * as PQP from "@microsoft/powerquery-parser"; 6 | import { AnalysisSettings, Hover, Position, SignatureHelp } from "@microsoft/powerquery-language-services"; 7 | import { assert, expect } from "chai"; 8 | import { Assert, ResultUtils } from "@microsoft/powerquery-parser"; 9 | import { ExternalLibraryUtils, LibrarySymbolUtils, LibraryUtils, ModuleLibraryUtils } from "../library"; 10 | import { MarkupContent, ParameterInformation, SignatureInformation } from "vscode-languageserver"; 11 | import { SettingsUtils } from "../settings"; 12 | 13 | const defaultLibrary: PQLS.Library.ILibrary = LibraryUtils.createLibrary( 14 | [LibrarySymbolUtils.getSymbolsForLocaleAndMode(PQP.Locale.en_US, "Power Query")], 15 | [], 16 | ); 17 | 18 | class NoOpCancellationToken implements PQP.ICancellationToken { 19 | isCancelled: () => boolean = () => false; 20 | // eslint-disable-next-line @typescript-eslint/no-empty-function 21 | throwIfCancelled: () => void = () => {}; 22 | // eslint-disable-next-line @typescript-eslint/no-empty-function 23 | cancel: () => void = () => {}; 24 | } 25 | 26 | const NoOpCancellationTokenInstance: NoOpCancellationToken = new NoOpCancellationToken(); 27 | 28 | async function assertGetHover(text: string, library?: PQLS.Library.ILibrary): Promise { 29 | const [analysis, position]: [PQLS.Analysis, Position] = createAnalysis(text, library); 30 | 31 | const result: PQP.Result = await analysis.getHover( 32 | position, 33 | NoOpCancellationTokenInstance, 34 | ); 35 | 36 | ResultUtils.assertIsOk(result); 37 | 38 | return result.value; 39 | } 40 | 41 | async function assertGetSignatureHelp(text: string): Promise { 42 | const [analysis, position]: [PQLS.Analysis, Position] = createAnalysis(text); 43 | 44 | const result: PQP.Result = await analysis.getSignatureHelp( 45 | position, 46 | NoOpCancellationTokenInstance, 47 | ); 48 | 49 | ResultUtils.assertIsOk(result); 50 | 51 | return ( 52 | result.value ?? { 53 | signatures: [], 54 | activeSignature: undefined, 55 | activeParameter: undefined, 56 | } 57 | ); 58 | } 59 | 60 | async function assertHoverContentEquals( 61 | text: string, 62 | expected: string, 63 | library?: PQLS.Library.ILibrary, 64 | ): Promise { 65 | const hover: PQLS.Hover | undefined = await assertGetHover(text, library); 66 | assert(hover !== undefined, "expected hover to be defined"); 67 | const markupContent: MarkupContent = assertAsMarkupContent(hover.contents); 68 | expect(markupContent.value).to.equal(expected); 69 | } 70 | 71 | const assertIsFunction: ( 72 | definition: PQLS.Library.TLibraryDefinition, 73 | ) => asserts definition is PQLS.Library.LibraryFunction = PQLS.LibraryDefinitionUtils.assertIsFunction; 74 | 75 | function assertAsMarkupContent(value: Hover["contents"]): MarkupContent { 76 | assertIsMarkupContent(value); 77 | 78 | return value; 79 | } 80 | 81 | function assertIsMarkupContent(value: Hover["contents"]): asserts value is MarkupContent { 82 | if (!MarkupContent.is(value)) { 83 | throw new Error(`expected value to be MarkupContent`); 84 | } 85 | } 86 | 87 | function createAnalysis(textWithPipe: string, library?: PQLS.Library.ILibrary): [PQLS.Analysis, Position] { 88 | const text: string = textWithPipe.replace("|", ""); 89 | 90 | const position: Position = { 91 | character: textWithPipe.indexOf("|"), 92 | line: 0, 93 | }; 94 | 95 | const analysisSettings: AnalysisSettings = { 96 | inspectionSettings: PQLS.InspectionUtils.inspectionSettings(PQP.DefaultSettings, { 97 | library: library ?? defaultLibrary, 98 | }), 99 | isWorkspaceCacheAllowed: false, 100 | traceManager: PQP.Trace.NoOpTraceManagerInstance, 101 | initialCorrelationId: undefined, 102 | }; 103 | 104 | return [PQLS.AnalysisUtils.analysis(PQLS.textDocument(textWithPipe, 1, text), analysisSettings), position]; 105 | } 106 | 107 | describe(`StandardLibrary`, () => { 108 | describe(`simple`, () => { 109 | it("index const by name", () => { 110 | const definitionKey: string = "BinaryOccurrence.Required"; 111 | 112 | const libraryDefinition: PQLS.Library.TLibraryDefinition | undefined = 113 | defaultLibrary.libraryDefinitions.staticLibraryDefinitions.get(definitionKey); 114 | 115 | if (libraryDefinition === undefined) { 116 | throw new Error(`expected constant '${definitionKey}' was not found`); 117 | } 118 | 119 | expect(libraryDefinition.label).eq(definitionKey, "unexpected label"); 120 | expect(libraryDefinition.description.length).greaterThan(0, "summary should not be empty"); 121 | expect(libraryDefinition.kind).eq(PQLS.Library.LibraryDefinitionKind.Constant); 122 | expect(libraryDefinition.asPowerQueryType.kind).eq(PQP.Language.Type.TypeKind.Number); 123 | }); 124 | 125 | it("index function by name", () => { 126 | const exportKey: string = "List.Distinct"; 127 | 128 | const libraryDefinition: PQLS.Library.TLibraryDefinition | undefined = 129 | defaultLibrary.libraryDefinitions.staticLibraryDefinitions.get(exportKey); 130 | 131 | if (libraryDefinition === undefined) { 132 | throw new Error(`expected constant '${exportKey}' was not found`); 133 | } 134 | 135 | assertIsFunction(libraryDefinition); 136 | 137 | expect(libraryDefinition.label !== null); 138 | expect(libraryDefinition.parameters[0].typeKind).eq(PQP.Language.Type.TypeKind.List); 139 | expect(libraryDefinition.parameters[1].typeKind).eq(PQP.Language.Type.TypeKind.Any); 140 | }); 141 | 142 | it("#date constructor", () => { 143 | const exportKey: string = "#date"; 144 | 145 | const libraryDefinition: PQLS.Library.TLibraryDefinition | undefined = 146 | defaultLibrary.libraryDefinitions.staticLibraryDefinitions.get(exportKey); 147 | 148 | if (libraryDefinition === undefined) { 149 | throw new Error(`expected constant '${exportKey}' was not found`); 150 | } 151 | 152 | assertIsFunction(libraryDefinition); 153 | 154 | expect(libraryDefinition.label !== null); 155 | expect(libraryDefinition.kind).eq(PQLS.Library.LibraryDefinitionKind.Function); 156 | expect(libraryDefinition.parameters.length).eq(3, "expecting 3 parameters in first signature"); 157 | expect(libraryDefinition.parameters[0].label).eq("year"); 158 | expect(libraryDefinition.parameters[0].typeKind).eq(PQP.Language.Type.TypeKind.Number); 159 | }); 160 | }); 161 | 162 | describe(`StandardLibrary`, () => { 163 | describe(`Table.AddColumn`, () => { 164 | it(`getHover`, async () => { 165 | const expression: string = `let foo = Table.AddColumn(1 as table, "bar", each 1) in fo|o`; 166 | const expected: string = "[let-variable] foo: table [bar: 1, ...]"; 167 | await assertHoverContentEquals(expression, expected); 168 | }); 169 | 170 | it("getSignatureHelp", async () => { 171 | const signatureHelp: SignatureHelp = await assertGetSignatureHelp("Table.AddColumn(|"); 172 | 173 | expect(signatureHelp.activeParameter).to.equal(0); 174 | expect(signatureHelp.activeSignature).to.equal(0); 175 | expect(signatureHelp.signatures.length).to.equal(1); 176 | 177 | const signature: SignatureInformation = PQP.Assert.asDefined(signatureHelp.signatures[0]); 178 | Assert.isDefined(signature.documentation); 179 | 180 | expect(signature.documentation).to.equal( 181 | `Adds a column with the specified name. The value is computed using the specified selection function with each row taken as an input.`, 182 | ); 183 | 184 | Assert.isDefined(signature.parameters); 185 | expect(signature.parameters.length).to.equal(4); 186 | const parameters: ReadonlyArray = signature.parameters; 187 | 188 | const firstParameter: ParameterInformation = PQP.Assert.asDefined(parameters[0]); 189 | expect(firstParameter.label).to.equal(PQP.Language.Constant.PrimitiveTypeConstant.Table); 190 | 191 | const secondParameter: ParameterInformation = PQP.Assert.asDefined(parameters[1]); 192 | expect(secondParameter.label).to.equal("newColumnName"); 193 | 194 | const thirdParameter: ParameterInformation = PQP.Assert.asDefined(parameters[2]); 195 | expect(thirdParameter.label).to.equal("columnGenerator"); 196 | 197 | const fourthParameter: ParameterInformation = PQP.Assert.asDefined(parameters[3]); 198 | expect(fourthParameter.label).to.equal("columnType"); 199 | }); 200 | }); 201 | }); 202 | }); 203 | 204 | // TODO: This is hardcoded for testing purposes but needs to be kept in sync with the actual library symbol format. 205 | const additionalSymbolJsonStr: string = `[{"name":"ExtensionTest.Contents","documentation":null,"completionItemKind":3,"functionParameters":[{"name":"message","type":"nullable text","isRequired":false,"isNullable":true,"caption":null,"description":null,"sampleValues":null,"allowedValues":null,"defaultValue":null,"fields":null,"enumNames":null,"enumCaptions":null}],"isDataSource":true,"type":"any"}]`; 206 | 207 | describe(`moduleLibraryUpdated`, () => { 208 | describe(`single export`, () => { 209 | it(`SDK call format`, () => { 210 | const libraryJson: PQLS.LibrarySymbol.LibrarySymbol[] = JSON.parse(additionalSymbolJsonStr); 211 | expect(libraryJson.length).to.equal(1, "expected 1 export"); 212 | expect(libraryJson[0].name).to.equal("ExtensionTest.Contents"); 213 | }); 214 | 215 | it(`ModuleLibraries`, () => { 216 | ModuleLibraryUtils.clearCache(); 217 | expect(ModuleLibraryUtils.getModuleCount()).to.equal(0); 218 | 219 | const libraryJson: PQLS.LibrarySymbol.LibrarySymbol[] = JSON.parse(additionalSymbolJsonStr); 220 | ModuleLibraryUtils.onModuleAdded("testing", libraryJson); 221 | expect(ModuleLibraryUtils.getModuleCount()).to.equal(1); 222 | 223 | ModuleLibraryUtils.clearCache(); 224 | expect(ModuleLibraryUtils.getModuleCount()).to.equal(0); 225 | }); 226 | }); 227 | }); 228 | 229 | describe(`setLibrarySymbols`, () => { 230 | const libraryKey: string = "Testing"; 231 | 232 | const testLibrary: ReadonlyMap = new Map([ 233 | [libraryKey, JSON.parse(additionalSymbolJsonStr)], 234 | ]); 235 | 236 | before(() => { 237 | ExternalLibraryUtils.addLibaries(testLibrary); 238 | }); 239 | 240 | it(`Library registered`, () => { 241 | const symbols: ReadonlyArray = ExternalLibraryUtils.getSymbols(); 242 | expect(symbols.length).to.equal(1, "expected 1 library"); 243 | expect(symbols[0].length).to.equal(1, "expected 1 symbol"); 244 | }); 245 | 246 | it(`hover for external symbol`, async () => { 247 | const expression: string = `let foo = ExtensionTest.Con|tents("hello") in foo`; 248 | const expected: string = "[library function] ExtensionTest.Contents: (message: optional nullable text) => any"; 249 | const library: PQLS.Library.ILibrary = SettingsUtils.getLibrary("test"); 250 | await assertHoverContentEquals(expression, expected, library); 251 | }); 252 | 253 | after(() => { 254 | ExternalLibraryUtils.removeLibraries([libraryKey]); 255 | expect(ExternalLibraryUtils.getSymbols().length).to.equal(0, "expected 0 libraries"); 256 | }); 257 | }); 258 | -------------------------------------------------------------------------------- /server/src/traceManagerUtils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as fs from "fs"; 5 | import * as path from "path"; 6 | import * as PQP from "@microsoft/powerquery-parser"; 7 | import { NoOpTraceManagerInstance } from "@microsoft/powerquery-parser/lib/powerquery-parser/common/trace"; 8 | import { Position } from "vscode-languageserver-textdocument"; 9 | 10 | import { SettingsUtils } from "./settings"; 11 | 12 | export function createTraceManager( 13 | uri: string | undefined, 14 | sourceAction: string, 15 | position?: Position, 16 | ): PQP.Trace.TraceManager { 17 | if (SettingsUtils.getServerSettings().isBenchmarksEnabled) { 18 | return createBenchmarkTraceManager(uri, sourceAction, position) ?? NoOpTraceManagerInstance; 19 | } else { 20 | return NoOpTraceManagerInstance; 21 | } 22 | } 23 | 24 | function createBenchmarkTraceManager( 25 | uri: string | undefined, 26 | sourceAction: string, 27 | position?: Position, 28 | ): PQP.Trace.BenchmarkTraceManager | undefined { 29 | if (!uri) { 30 | return undefined; 31 | } 32 | 33 | let source: string = path.parse(uri).name; 34 | 35 | // If untitled document 36 | if (uri.startsWith("untitled:")) { 37 | source = source.slice("untitled:".length); 38 | } 39 | // Else expect it to be a file 40 | else { 41 | source = path.parse(uri).name; 42 | } 43 | 44 | if (!source) { 45 | return undefined; 46 | } 47 | 48 | if (position) { 49 | sourceAction += `L${position.line}C${position.character}`; 50 | } 51 | 52 | const logDirectory: string = path.join(process.cwd(), "vscode-powerquery-logs"); 53 | 54 | if (!fs.existsSync(logDirectory)) { 55 | fs.mkdirSync(logDirectory, { recursive: true }); 56 | } 57 | 58 | let benchmarkUri: string; 59 | 60 | // TODO: make this not O(n) 61 | for (let iteration: number = 0; iteration < 1000; iteration += 1) { 62 | benchmarkUri = path.join(logDirectory, `${source}_${sourceAction}_${iteration}.log`); 63 | 64 | if (!fs.existsSync(benchmarkUri)) { 65 | const writeStream: fs.WriteStream = fs.createWriteStream(benchmarkUri, { flags: "w" }); 66 | 67 | return new PQP.Trace.BenchmarkTraceManager((message: string) => writeStream.write(message)); 68 | } 69 | } 70 | 71 | // TODO: handle fallback if all iterations are taken 72 | return undefined; 73 | } 74 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src" 6 | }, 7 | "include": ["src/**/*.ts", "src/library/standardLibrary.json"], 8 | "exclude": ["node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /server/tsconfig.webpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "exclude": ["node_modules", "src/test"] 4 | } 5 | -------------------------------------------------------------------------------- /server/webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | "use strict"; 4 | 5 | const path = require("path"); 6 | 7 | /**@type {import('webpack').Configuration}*/ 8 | const config = { 9 | target: "node", 10 | entry: "./src/server.ts", 11 | output: { 12 | path: path.resolve(__dirname, "dist"), 13 | filename: "server.js", 14 | libraryTarget: "commonjs2", 15 | devtoolModuleFilenameTemplate: "../[resource-path]", 16 | }, 17 | devtool: "source-map", 18 | externals: { 19 | vscode: "commonjs vscode", 20 | }, 21 | infrastructureLogging: { 22 | level: "log", 23 | }, 24 | resolve: { 25 | extensions: [".ts", ".js"], 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.ts$/, 31 | exclude: /node_modules/, 32 | use: [ 33 | { 34 | loader: "ts-loader", 35 | options: { 36 | configFile: "tsconfig.webpack.json", 37 | }, 38 | }, 39 | ], 40 | }, 41 | ], 42 | }, 43 | }; 44 | module.exports = config; 45 | -------------------------------------------------------------------------------- /syntaxes/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2 3 | } 4 | -------------------------------------------------------------------------------- /syntaxes/powerquery.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "powerquery", 3 | "scopeName": "source.powerquery", 4 | "fileTypes": ["pq", "pqm"], 5 | "uuid": "41968B57-12E6-4AC5-92A4-A837010E8B0A", 6 | "patterns": [ 7 | { 8 | "include": "#Noise" 9 | }, 10 | { 11 | "include": "#LiteralExpression" 12 | }, 13 | { 14 | "include": "#Keywords" 15 | }, 16 | { 17 | "include": "#ImplicitVariable" 18 | }, 19 | { 20 | "include": "#IntrinsicVariable" 21 | }, 22 | { 23 | "include": "#Operators" 24 | }, 25 | { 26 | "include": "#DotOperators" 27 | }, 28 | { 29 | "include": "#TypeName" 30 | }, 31 | { 32 | "include": "#RecordExpression" 33 | }, 34 | { 35 | "include": "#Punctuation" 36 | }, 37 | { 38 | "include": "#QuotedIdentifier" 39 | }, 40 | { 41 | "include": "#Identifier" 42 | } 43 | ], 44 | "repository": { 45 | "Keywords": { 46 | "match": "\\b(?:(and|or|not)|(if|then|else)|(try|catch|otherwise)|(as|each|in|is|let|meta|type|error)|(section|shared))\\b", 47 | "captures": { 48 | "1": { 49 | "name": "keyword.operator.word.logical.powerquery" 50 | }, 51 | "2": { 52 | "name": "keyword.control.conditional.powerquery" 53 | }, 54 | "3": { 55 | "name": "keyword.control.exception.powerquery" 56 | }, 57 | "4": { 58 | "name": "keyword.other.powerquery" 59 | }, 60 | "5": { 61 | "name": "keyword.powerquery" 62 | } 63 | } 64 | }, 65 | "TypeName": { 66 | "match": "\\b(?:(optional|nullable)|(action|any|anynonnull|binary|date|datetime|datetimezone|duration|function|list|logical|none|null|number|record|table|text|time|type))\\b", 67 | "captures": { 68 | "1": { 69 | "name": "storage.modifier.powerquery" 70 | }, 71 | "2": { 72 | "name": "storage.type.powerquery" 73 | } 74 | } 75 | }, 76 | "LiteralExpression": { 77 | "patterns": [ 78 | { 79 | "include": "#String" 80 | }, 81 | { 82 | "include": "#NumericConstant" 83 | }, 84 | { 85 | "include": "#LogicalConstant" 86 | }, 87 | { 88 | "include": "#NullConstant" 89 | }, 90 | { 91 | "include": "#FloatNumber" 92 | }, 93 | { 94 | "include": "#DecimalNumber" 95 | }, 96 | { 97 | "include": "#HexNumber" 98 | }, 99 | { 100 | "include": "#IntNumber" 101 | } 102 | ] 103 | }, 104 | "Noise": { 105 | "patterns": [ 106 | { 107 | "include": "#BlockComment" 108 | }, 109 | { 110 | "include": "#LineComment" 111 | }, 112 | { 113 | "include": "#Whitespace" 114 | } 115 | ] 116 | }, 117 | "Whitespace": { 118 | "match": "\\s+" 119 | }, 120 | "BlockComment": { 121 | "begin": "/\\*", 122 | "end": "\\*/", 123 | "name": "comment.block.powerquery" 124 | }, 125 | "LineComment": { 126 | "match": "//.*", 127 | "name": "comment.line.double-slash.powerquery" 128 | }, 129 | "String": { 130 | "begin": "\"", 131 | "beginCaptures": { 132 | "0": { 133 | "name": "punctuation.definition.string.begin.powerquery" 134 | } 135 | }, 136 | "end": "\"(?!\")", 137 | "endCaptures": { 138 | "0": { 139 | "name": "punctuation.definition.string.end.powerquery" 140 | } 141 | }, 142 | "patterns": [ 143 | { 144 | "match": "\"\"", 145 | "name": "constant.character.escape.quote.powerquery" 146 | }, 147 | { 148 | "include": "#EscapeSequence" 149 | } 150 | ], 151 | "name": "string.quoted.double.powerquery" 152 | }, 153 | "QuotedIdentifier": { 154 | "begin": "#\"", 155 | "beginCaptures": { 156 | "0": { 157 | "name": "punctuation.definition.quotedidentifier.begin.powerquery" 158 | } 159 | }, 160 | "end": "\"(?!\")", 161 | "endCaptures": { 162 | "0": { 163 | "name": "punctuation.definition.quotedidentifier.end.powerquery" 164 | } 165 | }, 166 | "patterns": [ 167 | { 168 | "match": "\"\"", 169 | "name": "constant.character.escape.quote.powerquery" 170 | }, 171 | { 172 | "include": "#EscapeSequence" 173 | } 174 | ], 175 | "name": "entity.name.powerquery" 176 | }, 177 | "EscapeSequence": { 178 | "begin": "#\\(", 179 | "beginCaptures": { 180 | "0": { 181 | "name": "punctuation.definition.escapesequence.begin.powerquery" 182 | } 183 | }, 184 | "end": "\\)", 185 | "endCaptures": { 186 | "0": { 187 | "name": "punctuation.definition.escapesequence.end.powerquery" 188 | } 189 | }, 190 | "patterns": [ 191 | { 192 | "match": "(#|\\h{4}|\\h{8}|cr|lf|tab)(?:,(#|\\h{4}|\\h{8}|cr|lf|tab))*" 193 | }, 194 | { 195 | "match": "[^\\)]", 196 | "name": "invalid.illegal.escapesequence.powerquery" 197 | } 198 | ], 199 | "name": "constant.character.escapesequence.powerquery" 200 | }, 201 | "LogicalConstant": { 202 | "match": "\\b(true|false)\\b", 203 | "name": "constant.language.logical.powerquery" 204 | }, 205 | "NullConstant": { 206 | "match": "\\b(null)\\b", 207 | "name": "constant.language.null.powerquery" 208 | }, 209 | "NumericConstant": { 210 | "match": "(?)|(=)|(<>|<|>|<=|>=)|(&)|(\\+|-|\\*|\\/)|(!)|(\\?)", 258 | "captures": { 259 | "1": { 260 | "name": "keyword.operator.function.powerquery" 261 | }, 262 | "2": { 263 | "name": "keyword.operator.assignment-or-comparison.powerquery" 264 | }, 265 | "3": { 266 | "name": "keyword.operator.comparison.powerquery" 267 | }, 268 | "4": { 269 | "name": "keyword.operator.combination.powerquery" 270 | }, 271 | "5": { 272 | "name": "keyword.operator.arithmetic.powerquery" 273 | }, 274 | "6": { 275 | "name": "keyword.operator.sectionaccess.powerquery" 276 | }, 277 | "7": { 278 | "name": "keyword.operator.optional.powerquery" 279 | } 280 | } 281 | }, 282 | "DotOperators": { 283 | "match": "(?