├── .gitattributes ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── .vscodeignore ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── assets ├── folding.gif ├── formatting.gif ├── highlighting.png ├── overview.gif └── sorting.gif ├── icon.png ├── language-configuration.json ├── last-upstream-hash ├── package.json ├── package.nls.json ├── src ├── client │ ├── browser │ │ └── clientMain.ts │ ├── client.ts │ ├── languageParticipants.ts │ ├── languageStatus.ts │ ├── node │ │ ├── clientMain.ts │ │ └── schemaCache.ts │ └── utils │ │ └── hash.ts └── server │ ├── browser │ ├── serverMain.ts │ └── serverWorkerMain.ts │ ├── languageModelCache.ts │ ├── node │ ├── serverMain.ts │ └── serverNodeMain.ts │ ├── server.ts │ └── utils │ ├── runner.ts │ ├── strings.ts │ └── validation.ts ├── syntaxes └── json5.tmLanguage.json ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically normalize line endings. 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.vsix 3 | better-json5 4 | .yarn 5 | dist 6 | .vscode-test-web 7 | test.json5 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "amodio.tsl-problem-matcher" 7 | ] 8 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--disable-extensions", 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ] 16 | }, 17 | { 18 | "name": "Run Web Extension ", 19 | "type": "extensionHost", 20 | "debugWebWorkerHost": true, 21 | "request": "launch", 22 | "args": [ 23 | "--extensionDevelopmentPath=${workspaceFolder}", 24 | "--extensionDevelopmentKind=web" 25 | ], 26 | "outFiles": [ 27 | "${workspaceFolder}/dist/browser/*.js" 28 | ], 29 | "preLaunchTask": "${defaultBuildTask}" 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "compile-web", 9 | "group": { 10 | "kind": "build", 11 | "isDefault": true 12 | }, 13 | "problemMatcher": [ 14 | "$ts-webpack", 15 | "$tslint-webpack" 16 | ] 17 | }, 18 | { 19 | "type": "npm", 20 | "script": "watch-web", 21 | "group": "build", 22 | "isBackground": true, 23 | "problemMatcher": [ 24 | "$ts-webpack-watch", 25 | "$tslint-webpack-watch" 26 | ] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | .gitignore 4 | vsc-extension-quickstart.md 5 | node_modules 6 | *.webpack.config.js 7 | tsconfig.json 8 | .yarn -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 1.4.0 4 | 5 | - Colorization for numerics now can be selected with `numbers` in `editor.tokenColorCustomizations` settings. ([#11](https://github.com/BlueGlassBlock/better-json5/issues/11)) 6 | - TextMate scopes for numerics are now `constant.numeric.hex.json5` and `constant.numeric.dec.json5` for hexadecimal and decimal numbers respectively. 7 | 8 | 9 | 10 | ## 1.3.0 11 | 12 | - Suggestions now respect quote of current input, instead of forcing users to use `json5.format.keyQuotes` and `json5.format.stringQuotes` settings all the time. ([#9](https://github.com/BlueGlassBlock/better-json5/issues/9)) 13 | 14 | ## 1.2.1 15 | 16 | - Fixed a minor issue that caused keys with only one character to be flagged as false-positive. 17 | 18 | ## 1.2.0 19 | 20 | - Removed telemetry code that came with the fork. 21 | 22 | - Addressed [vscode#232647](https://github.com/microsoft/vscode/issues/232647). 23 | 24 | - Supported `${workspaceFolder}`, `${workspaceFolderBasename}`, `${pathSeparator}`, `${env:VARIABLE_NAME}` and `${config:CONFIG_NAME}` in `fileMatch` and `url` properties of `json5.schema` settings. ([#4](https://github.com/BlueGlassBlock/better-json5/issues/4)) 25 | 26 | > Please note that using variable substitution in `fileMatch` may not work as you expected since the pattern is identified as a **glob pattern**, and it doesn't like path separators that may come with variables. 27 | 28 | ## 1.1.2 29 | 30 | - Optimized packaging method 31 | 32 | ## 1.1.1 33 | 34 | - Fixed [#7](https://github.com/BlueGlassBlock/better-json5/issues/7) 35 | - Fixed [#8](https://github.com/BlueGlassBlock/better-json5/issues/8) 36 | 37 | ## 1.1.0 38 | 39 | - Fixed [#6](https://github.com/BlueGlassBlock/better-json5/issues/6): Multiline strings are getting false-positive errors 40 | - Fixed TextMate grammar for floating point numbers like `3.` 41 | - Implemented multiline string folding 42 | 43 | ## 1.0.1 44 | 45 | - Fixed [#3](https://github.com/BlueGlassBlock/better-json5/issues/3): Extension looksup schema configuration in wrong section 46 | 47 | ## 1.0.0 48 | 49 | - Make suggestions respect quote settings 50 | 51 | ## 0.0.3 52 | 53 | - Implemented `json5.format.trailingCommas` setting 54 | - Implemented `json5.format.keyQuotes` and `json5.format.stringQuotes` settings 55 | - Published to [#2](https://github.com/BlueGlassBlock/better-json5/issues/2) 56 | 57 | ## 0.0.2 58 | 59 | - Fixed [#1](https://github.com/BlueGlassBlock/better-json5/issues/1): Extension failed to load in 60 | 61 | ## 0.0.1 62 | 63 | - Initial release -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Nyuan Zhang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | JSON Language Features 24 | 25 | The MIT License (MIT) 26 | 27 | Copyright (c) Microsoft 28 | 29 | Permission is hereby granted, free of charge, to any person obtaining a copy 30 | of this software and associated documentation files (the "Software"), to deal 31 | in the Software without restriction, including without limitation the rights 32 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 33 | copies of the Software, and to permit persons to whom the Software is 34 | furnished to do so, subject to the following conditions: 35 | 36 | The above copyright notice and this permission notice shall be included in all 37 | copies or substantial portions of the Software. 38 | 39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 40 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 41 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 42 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 43 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 44 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 45 | SOFTWARE. 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # better-json5 4 | 5 | > JSON5 support for Visual Studio Code, done right ⚡ 6 | 7 | ## Features 8 | 9 | #### Syntax highlighting with correctly colored keys, with your favorite theme support out-of-the-box 10 | ![Syntax Highlighting](./assets/highlighting.png) 11 | 12 | #### JSON Schema based validation and intellisense 13 | ![Overview](./assets/overview.gif) 14 | 15 | #### Completely configurable formatting 16 | 17 | ![Formatting](./assets/formatting.gif) 18 | 19 | #### Sorting Command 20 | 21 | ![Sorting](./assets/sorting.gif) 22 | 23 | #### Proper folding for objects, arrays and multiline strings 24 | 25 | ![Folding](./assets/folding.gif) 26 | 27 |
28 | 29 | ## Extension Settings 30 | 31 | - `json5.schemas`: Associate schemas to JSON5 files in the current project. 32 | - `json5.validate.enable`: Enable/disable validation. 33 | - `json5.format.enable`: Enable/disable formatting. 34 | - `json5.format.keepLines`: Keep all existing new lines when formatting. 35 | - `json5.format.trailingCommas`: Control the occurrence of trailing commas in objects and arrays. 36 | - `json5.format.keyQuotes`: Control the usage of quotes for object keys. 37 | - `json5.format.stringQuotes`: Control the usage of quotes for string values in objects and arrays. 38 | - `json5.tracing`: Traces the communication between VS Code and the JSON5 language server. 39 | 40 | ## Credits 41 | 42 | This extension is heavily based on the [JSON Language Features](https://github.com/microsoft/vscode/tree/main/extensions/json-language-features) extension by Microsoft. 43 | 44 | ## Changelog 45 | 46 | See [CHANGELOG.md](CHANGELOG.md) 47 | 48 | ## License 49 | 50 | [MIT](LICENSE.md) -------------------------------------------------------------------------------- /assets/folding.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueGlassBlock/better-json5/a78dd70a5502bb6a4cae746459ecfa13759e0d40/assets/folding.gif -------------------------------------------------------------------------------- /assets/formatting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueGlassBlock/better-json5/a78dd70a5502bb6a4cae746459ecfa13759e0d40/assets/formatting.gif -------------------------------------------------------------------------------- /assets/highlighting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueGlassBlock/better-json5/a78dd70a5502bb6a4cae746459ecfa13759e0d40/assets/highlighting.png -------------------------------------------------------------------------------- /assets/overview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueGlassBlock/better-json5/a78dd70a5502bb6a4cae746459ecfa13759e0d40/assets/overview.gif -------------------------------------------------------------------------------- /assets/sorting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueGlassBlock/better-json5/a78dd70a5502bb6a4cae746459ecfa13759e0d40/assets/sorting.gif -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlueGlassBlock/better-json5/a78dd70a5502bb6a4cae746459ecfa13759e0d40/icon.png -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | // symbol used for single line comment. Remove this entry if your language does not support line comments 4 | "lineComment": "//", 5 | // symbols used for start and end a block comment. Remove this entry if your language does not support block comments 6 | "blockComment": [ "/*", "*/" ] 7 | }, 8 | // symbols used as brackets 9 | "brackets": [ 10 | ["{", "}"], 11 | ["[", "]"] 12 | ], 13 | // symbols that can be used to surround a selection 14 | "surroundingPairs": [ 15 | ["{", "}"], 16 | ["[", "]"], 17 | ["\"", "\""], 18 | ["'", "'"] 19 | ], 20 | "colorizedBracketPairs": [ 21 | ["{", "}"], 22 | ["[", "]"] 23 | ], 24 | "autoClosingPairs": [ 25 | { "open": "{", "close": "}", "notIn": ["string"] }, 26 | { "open": "[", "close": "]", "notIn": ["string"] }, 27 | { "open": "(", "close": ")", "notIn": ["string"] }, 28 | { "open": "'", "close": "'", "notIn": ["string"] }, 29 | { "open": "\"", "close": "\"", "notIn": ["string", "comment"] }, 30 | ], 31 | "folding": { 32 | "markers": { 33 | "start": "^\\s*// region:\\b", 34 | "end": "^\\s*// endregion\\b" 35 | } 36 | }, 37 | "indentationRules": { 38 | "increaseIndentPattern": "({+(?=((\\\\.|[^\"\\\\])*\"(\\\\.|[^\"\\\\])*\")*[^\"}]*)$)|(\\[+(?=((\\\\.|[^\"\\\\])*\"(\\\\.|[^\"\\\\])*\")*[^\"\\]]*)$)", 39 | "decreaseIndentPattern": "^\\s*[}\\]],?\\s*$" 40 | } 41 | } -------------------------------------------------------------------------------- /last-upstream-hash: -------------------------------------------------------------------------------- 1 | 8713346d2bd6c256506eb74d97e727f7a93beee4 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "better-json5", 3 | "displayName": "Better JSON5", 4 | "description": "JSON5 support for Visual Studio Code, done right ⚡", 5 | "version": "1.4.0", 6 | "engines": { 7 | "vscode": "^1.93.0" 8 | }, 9 | "publisher": "BlueGlassBlock", 10 | "repository": { 11 | "url": "https://github.com/BlueGlassBlock/better-json5" 12 | }, 13 | "main": "./dist/node/clientMain.js", 14 | "browser": "./dist/browser/clientMain.js", 15 | "scripts": { 16 | "compile-dbg": "webpack --mode development", 17 | "compile": "webpack --mode production", 18 | "package": "npm run compile && vsce package --no-dependencies", 19 | "release": "npm run package && vsce publish --no-dependencies && ovsx publish --no-dependencies" 20 | }, 21 | "icon": "./icon.png", 22 | "capabilities": { 23 | "virtualWorkspaces": true, 24 | "untrustedWorkspaces": { 25 | "supported": true 26 | } 27 | }, 28 | "activationEvents": [ 29 | "onLanguage:json5" 30 | ], 31 | "categories": [ 32 | "Programming Languages", 33 | "Formatters", 34 | "Linters" 35 | ], 36 | "contributes": { 37 | "languages": [ 38 | { 39 | "id": "json5", 40 | "aliases": [ 41 | "JSON5", 42 | "json5" 43 | ], 44 | "extensions": [ 45 | ".json5" 46 | ], 47 | "configuration": "./language-configuration.json" 48 | } 49 | ], 50 | "grammars": [ 51 | { 52 | "language": "json5", 53 | "scopeName": "source.json5", 54 | "path": "./syntaxes/json5.tmLanguage.json" 55 | } 56 | ], 57 | "configuration": { 58 | "id": "json5", 59 | "order": 20, 60 | "type": "object", 61 | "title": "JSON5", 62 | "properties": { 63 | "json5.schemas": { 64 | "type": "array", 65 | "scope": "resource", 66 | "description": "%json5.schemas.desc%", 67 | "items": { 68 | "type": "object", 69 | "default": { 70 | "fileMatch": [ 71 | "/myfile" 72 | ], 73 | "url": "schemaURL" 74 | }, 75 | "properties": { 76 | "url": { 77 | "type": "string", 78 | "default": "/user.schema.json", 79 | "description": "%json5.schemas.url.desc%" 80 | }, 81 | "fileMatch": { 82 | "type": "array", 83 | "items": { 84 | "type": "string", 85 | "default": "MyFile.json", 86 | "description": "%json5.schemas.fileMatch.item.desc%" 87 | }, 88 | "minItems": 1, 89 | "description": "%json5.schemas.fileMatch.desc%" 90 | }, 91 | "schema": { 92 | "$ref": "http://json-schema.org/draft-07/schema#", 93 | "description": "%json5.schemas.schema.desc%", 94 | "default": {} 95 | } 96 | } 97 | } 98 | }, 99 | "json5.validate.enable": { 100 | "type": "boolean", 101 | "scope": "window", 102 | "default": true, 103 | "description": "%json5.validate.enable.desc%" 104 | }, 105 | "json5.format.enable": { 106 | "type": "boolean", 107 | "scope": "window", 108 | "default": true, 109 | "description": "%json5.format.enable.desc%" 110 | }, 111 | "json5.format.keepLines": { 112 | "type": "boolean", 113 | "scope": "window", 114 | "default": false, 115 | "description": "%json5.format.keepLines.desc%" 116 | }, 117 | "json5.format.trailingCommas": { 118 | "type": "string", 119 | "scope": "window", 120 | "enum": [ 121 | "keep", 122 | "none", 123 | "all" 124 | ], 125 | "default": "keep", 126 | "description": "%json5.format.trailingCommas.desc%" 127 | }, 128 | "json5.format.keyQuotes": { 129 | "type": "string", 130 | "scope": "window", 131 | "enum": [ 132 | "keep", 133 | "double", 134 | "single", 135 | "none-double", 136 | "none-single" 137 | ], 138 | "default": "keep", 139 | "description": "%json5.format.keyQuotes.desc%" 140 | }, 141 | "json5.format.stringQuotes": { 142 | "type": "string", 143 | "scope": "window", 144 | "enum": [ 145 | "keep", 146 | "double", 147 | "single" 148 | ], 149 | "default": "keep", 150 | "description": "%json5.format.stringQuotes.desc%" 151 | }, 152 | "json5.trace.server": { 153 | "type": "string", 154 | "scope": "window", 155 | "enum": [ 156 | "off", 157 | "messages", 158 | "verbose" 159 | ], 160 | "default": "off", 161 | "description": "%json5.tracing.desc%" 162 | }, 163 | "json5.colorDecorators.enable": { 164 | "type": "boolean", 165 | "scope": "window", 166 | "default": true, 167 | "description": "%json5.colorDecorators.enable.desc%", 168 | "deprecationMessage": "%json5.colorDecorators.enable.deprecationMessage%" 169 | }, 170 | "json5.maxItemsComputed": { 171 | "type": "number", 172 | "default": 5000, 173 | "description": "%json5.maxItemsComputed.desc%" 174 | }, 175 | "json5.schemaDownload.enable": { 176 | "type": "boolean", 177 | "default": true, 178 | "description": "%json5.enableSchemaDownload.desc%", 179 | "tags": [ 180 | "usesOnlineServices" 181 | ] 182 | } 183 | } 184 | }, 185 | "configurationDefaults": { 186 | "[json5]": { 187 | "editor.quickSuggestions": { 188 | "strings": true 189 | }, 190 | "editor.suggest.insertMode": "replace" 191 | } 192 | }, 193 | "jsonValidation": [ 194 | { 195 | "fileMatch": "*.schema.json5", 196 | "url": "http://json-schema.org/draft-07/schema#" 197 | } 198 | ], 199 | "commands": [ 200 | { 201 | "command": "json5.clearCache", 202 | "title": "%json5.command.clearCache%", 203 | "category": "JSON5" 204 | }, 205 | { 206 | "command": "json5.sort", 207 | "title": "%json5.command.sort%", 208 | "category": "JSON5" 209 | } 210 | ] 211 | }, 212 | "dependencies": { 213 | "@blueglassblock/json5-kit": "0.2.3", 214 | "@blueglassblock/json5-languageservice": "^0.5.1", 215 | "@vscode/l10n": "^0.0.18", 216 | "request-light": "^0.8.0", 217 | "vscode-languageclient": "^10.0.0-next.13", 218 | "vscode-languageserver": "^10.0.0-next.11", 219 | "vscode-uri": "^3.0.8" 220 | }, 221 | "devDependencies": { 222 | "@types/node": "20.x", 223 | "@types/vscode": "^1.93.0", 224 | "@vscode/test-web": "^0.0.60", 225 | "@vscode/vsce": "^3.2.1", 226 | "assert": "^2.1.0", 227 | "ovsx": "^0.9.5", 228 | "process": "^0.11.10", 229 | "ts-loader": "^9.5.1", 230 | "typescript": "^5.6.2", 231 | "webpack": "^5.95.0", 232 | "webpack-cli": "^5.1.4" 233 | }, 234 | "packageManager": "yarn@4.5.0" 235 | } 236 | -------------------------------------------------------------------------------- /package.nls.json: -------------------------------------------------------------------------------- 1 | { 2 | "json5.schemas.desc": "Associate schemas to JSON5 files in the current project.", 3 | "json5.schemas.url.desc": "A URL or absolute file path to a schema. Can be a relative path (starting with './') in workspace and workspace folder settings.", 4 | "json5.schemas.fileMatch.desc": "An array of file patterns to match against when resolving JSON5 files to schemas. `*` and '**' can be used as a wildcard. Exclusion patterns can also be defined and start with '!'. A file matches when there is at least one matching pattern and the last matching pattern is not an exclusion pattern.", 5 | "json5.schemas.fileMatch.item.desc": "A file pattern that can contain '*' and '**' to match against when resolving JSON5 files to schemas. When beginning with '!', it defines an exclusion pattern.", 6 | "json5.schemas.schema.desc": "The schema definition for the given URL. The schema only needs to be provided to avoid accesses to the schema URL.", 7 | "json5.format.enable.desc": "Enable/disable default JSON5 formatter", 8 | "json5.format.keepLines.desc": "Keep all existing new lines when formatting.", 9 | "json5.format.trailingCommas.desc": "Controls whether trailing commas are added when formatting.", 10 | "json5.format.keyQuotes.desc": "Controls which quotes are used for object keys when formatting.", 11 | "json5.format.stringQuotes.desc": "Controls which quotes are used for strings other than keys when formatting.", 12 | "json5.validate.enable.desc": "Enable/disable JSON5 validation.", 13 | "json5.tracing.desc": "Traces the communication between VS Code and the JSON5 language server.", 14 | "json5.colorDecorators.enable.desc": "Enables or disables color decorators", 15 | "json5.colorDecorators.enable.deprecationMessage": "The setting `json.colorDecorators.enable` has been deprecated in favor of `editor.colorDecorators`.", 16 | "json5.schemaResolutionErrorMessage": "Unable to resolve schema.", 17 | "json5.clickToRetry": "Click to retry.", 18 | "json5.maxItemsComputed.desc": "The maximum number of outline symbols and folding regions computed (limited for performance reasons).", 19 | "json5.maxItemsExceededInformation.desc": "Show notification when exceeding the maximum number of outline symbols and folding regions.", 20 | "json5.enableSchemaDownload.desc": "When enabled, JSON schemas can be fetched from http and https locations.", 21 | "json5.command.clearCache": "Clear Schema Cache", 22 | "json5.command.sort": "Sort Document" 23 | } -------------------------------------------------------------------------------- /src/client/browser/clientMain.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { Disposable, ExtensionContext, Uri, l10n, window } from 'vscode'; 7 | import { LanguageClientOptions } from 'vscode-languageclient'; 8 | import { startClient, LanguageClientConstructor, SchemaRequestService, AsyncDisposable, languageServerDescription } from '../client'; 9 | import { LanguageClient } from 'vscode-languageclient/browser'; 10 | 11 | let client: AsyncDisposable | undefined; 12 | 13 | // this method is called when vs code is activated 14 | export async function activate(context: ExtensionContext) { 15 | const serverMain = Uri.joinPath(context.extensionUri, 'dist/browser/serverMain.js'); 16 | try { 17 | const worker = new Worker(serverMain.toString()); 18 | worker.postMessage({ i10lLocation: l10n.uri?.toString(false) ?? '' }); 19 | 20 | const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { 21 | return new LanguageClient(id, name, worker, clientOptions); 22 | }; 23 | 24 | const schemaRequests: SchemaRequestService = { 25 | getContent(uri: string) { 26 | return fetch(uri, { mode: 'cors' }) 27 | .then(function (response: any) { 28 | return response.text(); 29 | }); 30 | } 31 | }; 32 | 33 | const timer = { 34 | setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable { 35 | const handle = setTimeout(callback, ms, ...args); 36 | return { dispose: () => clearTimeout(handle) }; 37 | } 38 | }; 39 | 40 | const logOutputChannel = window.createOutputChannel(languageServerDescription, { log: true }); 41 | context.subscriptions.push(logOutputChannel); 42 | 43 | client = await startClient(context, newLanguageClient, { schemaRequests, timer, logOutputChannel }); 44 | 45 | } catch (e) { 46 | console.log(e); 47 | } 48 | } 49 | 50 | export async function deactivate(): Promise { 51 | if (client) { 52 | await client.dispose(); 53 | client = undefined; 54 | } 55 | } -------------------------------------------------------------------------------- /src/client/client.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export type JSONLanguageStatus = { schemas: string[] }; 7 | 8 | import { 9 | workspace, window, languages, commands, LogOutputChannel, ExtensionContext, extensions, Uri, ColorInformation, 10 | Diagnostic, StatusBarAlignment, TextEditor, TextDocument, FormattingOptions, CancellationToken, FoldingRange, 11 | ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation, l10n, 12 | RelativePattern 13 | } from 'vscode'; 14 | import { 15 | LanguageClientOptions, RequestType, NotificationType, FormattingOptions as LSPFormattingOptions, DocumentDiagnosticReportKind, 16 | DidChangeConfigurationNotification, HandleDiagnosticsSignature, ResponseError, DocumentRangeFormattingParams, 17 | DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, ProvideHoverSignature, BaseLanguageClient, ProvideFoldingRangeSignature, ProvideDocumentSymbolsSignature, ProvideDocumentColorsSignature 18 | } from 'vscode-languageclient'; 19 | 20 | import { hash } from './utils/hash'; 21 | import { createDocumentSymbolsLimitItem, createLanguageStatusItem, createLimitStatusItem } from './languageStatus'; 22 | import { getLanguageParticipants, LanguageParticipants } from './languageParticipants'; 23 | 24 | namespace VSCodeContentRequest { 25 | export const type: RequestType = new RequestType('vscode/content'); 26 | } 27 | 28 | namespace SchemaContentChangeNotification { 29 | export const type: NotificationType = new NotificationType('json5/schemaContent'); 30 | } 31 | 32 | namespace ForceValidateRequest { 33 | export const type: RequestType = new RequestType('json5/validate'); 34 | } 35 | 36 | namespace LanguageStatusRequest { 37 | export const type: RequestType = new RequestType('json5/languageStatus'); 38 | } 39 | 40 | interface SortOptions extends LSPFormattingOptions { 41 | } 42 | 43 | interface DocumentSortingParams { 44 | /** 45 | * The uri of the document to sort. 46 | */ 47 | readonly uri: string; 48 | /** 49 | * The sort options 50 | */ 51 | readonly options: SortOptions; 52 | } 53 | 54 | namespace DocumentSortingRequest { 55 | export interface ITextEdit { 56 | range: { 57 | start: { line: number; character: number }; 58 | end: { line: number; character: number }; 59 | }; 60 | newText: string; 61 | } 62 | export const type: RequestType = new RequestType('json5/sort'); 63 | } 64 | 65 | export interface ISchemaAssociations { 66 | [pattern: string]: string[]; 67 | } 68 | 69 | export interface ISchemaAssociation { 70 | fileMatch: string[]; 71 | uri: string; 72 | } 73 | 74 | namespace SchemaAssociationNotification { 75 | export const type: NotificationType = new NotificationType('json5/schemaAssociations'); 76 | } 77 | 78 | type Settings = { 79 | json5?: { 80 | schemas?: JSONSchemaSettings[]; 81 | format?: { 82 | enable?: boolean, 83 | trailingCommas?: 'keep' | 'none' | 'all', 84 | keyQuotes?: 'keep' | 'single' | 'double' | 'none-single' | 'none-double', 85 | stringQuotes?: 'keep' | 'single' | 'double', 86 | }; 87 | keepLines?: { enable?: boolean }; 88 | validate?: { enable?: boolean }; 89 | resultLimit?: number; 90 | foldingLimit?: number; 91 | colorDecorationLimit?: number; 92 | }; 93 | http?: { 94 | proxy?: string; 95 | proxyStrictSSL?: boolean; 96 | }; 97 | }; 98 | 99 | export type JSONSchemaSettings = { 100 | fileMatch?: string[]; 101 | url?: string; 102 | schema?: any; 103 | folderUri?: string; 104 | }; 105 | 106 | export namespace SettingIds { 107 | export const enableFormatter = 'json5.format.enable'; 108 | export const enableKeepLines = 'json5.format.keepLines'; 109 | export const trailingCommas = 'json5.format.trailingCommas'; 110 | export const keyQuotes = 'json5.format.keyQuotes'; 111 | export const stringQuotes = 'json5.format.stringQuotes'; 112 | export const enableValidation = 'json5.validate.enable'; 113 | export const enableSchemaDownload = 'json5.schemaDownload.enable'; 114 | export const maxItemsComputed = 'json5.maxItemsComputed'; 115 | export const editorFoldingMaximumRegions = 'editor.foldingMaximumRegions'; 116 | export const editorColorDecoratorsLimit = 'editor.colorDecoratorsLimit'; 117 | 118 | export const editorSection = 'editor'; 119 | export const foldingMaximumRegions = 'foldingMaximumRegions'; 120 | export const colorDecoratorsLimit = 'colorDecoratorsLimit'; 121 | } 122 | 123 | export interface TelemetryReporter { 124 | sendTelemetryEvent(eventName: string, properties?: { 125 | [key: string]: string; 126 | }, measurements?: { 127 | [key: string]: number; 128 | }): void; 129 | } 130 | 131 | export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient; 132 | 133 | export interface Runtime { 134 | schemaRequests: SchemaRequestService; 135 | readonly timer: { 136 | setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable; 137 | }; 138 | logOutputChannel: LogOutputChannel; 139 | } 140 | 141 | export interface SchemaRequestService { 142 | getContent(uri: string): Promise; 143 | clearCache?(): Promise; 144 | } 145 | 146 | export const languageServerDescription = l10n.t('JSON5 Language Server'); 147 | 148 | let resultLimit = 5000; 149 | let foldingLimit = 5000; 150 | let colorDecoratorLimit = 5000; 151 | 152 | export interface AsyncDisposable { 153 | dispose(): Promise; 154 | } 155 | 156 | export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise { 157 | const languageParticipants = getLanguageParticipants(); 158 | context.subscriptions.push(languageParticipants); 159 | 160 | let client: Disposable | undefined = await startClientWithParticipants(context, languageParticipants, newLanguageClient, runtime); 161 | 162 | let restartTrigger: Disposable | undefined; 163 | languageParticipants.onDidChange(() => { 164 | if (restartTrigger) { 165 | restartTrigger.dispose(); 166 | } 167 | restartTrigger = runtime.timer.setTimeout(async () => { 168 | if (client) { 169 | runtime.logOutputChannel.info('Extensions have changed, restarting JSON5 server...'); 170 | runtime.logOutputChannel.info(''); 171 | const oldClient = client; 172 | client = undefined; 173 | await oldClient.dispose(); 174 | client = await startClientWithParticipants(context, languageParticipants, newLanguageClient, runtime); 175 | } 176 | }, 2000); 177 | }); 178 | 179 | return { 180 | dispose: async () => { 181 | restartTrigger?.dispose(); 182 | await client?.dispose(); 183 | } 184 | }; 185 | } 186 | 187 | async function startClientWithParticipants(context: ExtensionContext, languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise { 188 | 189 | const toDispose: Disposable[] = []; 190 | 191 | let rangeFormatting: Disposable | undefined = undefined; 192 | 193 | const documentSelector = languageParticipants.documentSelector; 194 | 195 | const schemaResolutionErrorStatusBarItem = window.createStatusBarItem('status.json5.resolveError', StatusBarAlignment.Right, 0); 196 | schemaResolutionErrorStatusBarItem.name = l10n.t('JSON5: Schema Resolution Error'); 197 | schemaResolutionErrorStatusBarItem.text = '$(alert)'; 198 | toDispose.push(schemaResolutionErrorStatusBarItem); 199 | 200 | const fileSchemaErrors = new Map(); 201 | let schemaDownloadEnabled = true; 202 | 203 | let isClientReady = false; 204 | 205 | const documentSymbolsLimitStatusbarItem = createLimitStatusItem((limit: number) => createDocumentSymbolsLimitItem(documentSelector, SettingIds.maxItemsComputed, limit)); 206 | toDispose.push(documentSymbolsLimitStatusbarItem); 207 | 208 | toDispose.push(commands.registerCommand('json5.clearCache', async () => { 209 | client.sendNotification(DidChangeConfigurationNotification.type, { settings: getSettings() }); // Push latest settings just in case 210 | if (isClientReady && runtime.schemaRequests.clearCache) { 211 | const cachedSchemas = await runtime.schemaRequests.clearCache(); 212 | await client.sendNotification(SchemaContentChangeNotification.type, cachedSchemas); 213 | window.showInformationMessage(l10n.t('JSON5 schema cache fully cleared.')); 214 | } 215 | else { 216 | window.showErrorMessage(l10n.t('Unable to purge cache, triggered settings update instead.')); 217 | } 218 | })); 219 | 220 | 221 | toDispose.push(commands.registerCommand('json5.sort', async () => { 222 | 223 | if (isClientReady) { 224 | const textEditor = window.activeTextEditor; 225 | if (textEditor) { 226 | const documentOptions = textEditor.options; 227 | const textEdits = await getSortTextEdits(textEditor.document, documentOptions.tabSize, documentOptions.insertSpaces); 228 | const success = await textEditor.edit(mutator => { 229 | for (const edit of textEdits) { 230 | mutator.replace(client.protocol2CodeConverter.asRange(edit.range), edit.newText); 231 | } 232 | }); 233 | if (!success) { 234 | window.showErrorMessage(l10n.t('Failed to sort the JSONC document, please consider opening an issue.')); 235 | } 236 | } 237 | } 238 | })); 239 | 240 | function filterSchemaErrorDiagnostics(uri: Uri, diagnostics: Diagnostic[]): Diagnostic[] { 241 | const schemaErrorIndex = diagnostics.findIndex(isSchemaResolveError); 242 | if (schemaErrorIndex !== -1) { 243 | const schemaResolveDiagnostic = diagnostics[schemaErrorIndex]; 244 | fileSchemaErrors.set(uri.toString(), schemaResolveDiagnostic.message); 245 | if (!schemaDownloadEnabled) { 246 | diagnostics = diagnostics.filter(d => !isSchemaResolveError(d)); 247 | } 248 | if (window.activeTextEditor && window.activeTextEditor.document.uri.toString() === uri.toString()) { 249 | schemaResolutionErrorStatusBarItem.show(); 250 | } 251 | } 252 | return diagnostics; 253 | } 254 | 255 | // Options to control the language client 256 | const clientOptions: LanguageClientOptions = { 257 | // Register the server for json5 documents 258 | documentSelector, 259 | initializationOptions: { 260 | handledSchemaProtocols: ['file'], // language server only loads file-URI. Fetching schemas with other protocols ('http'...) are made on the client. 261 | provideFormatter: false, // tell the server to not provide formatting capability and ignore the `json5.format.enable` setting. 262 | customCapabilities: { rangeFormatting: { editLimit: 10000 } } 263 | }, 264 | synchronize: { 265 | // Synchronize the setting section 'json5' to the server 266 | configurationSection: ['json5', 'http'], 267 | fileEvents: workspace.createFileSystemWatcher('**/*.json') 268 | }, 269 | middleware: { 270 | workspace: { 271 | didChangeConfiguration: () => client.sendNotification(DidChangeConfigurationNotification.type, { settings: getSettings() }) 272 | }, 273 | provideDiagnostics: async (uriOrDoc, previousResolutId, token, next) => { 274 | const diagnostics = await next(uriOrDoc, previousResolutId, token); 275 | if (diagnostics && diagnostics.kind === DocumentDiagnosticReportKind.Full) { 276 | const uri = uriOrDoc instanceof Uri ? uriOrDoc : uriOrDoc.uri; 277 | diagnostics.items = filterSchemaErrorDiagnostics(uri, diagnostics.items); 278 | } 279 | return diagnostics; 280 | }, 281 | handleDiagnostics: (uri: Uri, diagnostics: Diagnostic[], next: HandleDiagnosticsSignature) => { 282 | diagnostics = filterSchemaErrorDiagnostics(uri, diagnostics); 283 | next(uri, diagnostics); 284 | }, 285 | // testing the replace / insert mode 286 | provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult { 287 | function update(item: CompletionItem) { 288 | const range = item.range; 289 | if (range instanceof Range && range.end.isAfter(position) && range.start.isBeforeOrEqual(position)) { 290 | item.range = { inserting: new Range(range.start, position), replacing: range }; 291 | } 292 | if (item.documentation instanceof MarkdownString) { 293 | item.documentation = updateMarkdownString(item.documentation); 294 | } 295 | 296 | } 297 | function updateProposals(r: CompletionItem[] | CompletionList | null | undefined): CompletionItem[] | CompletionList | null | undefined { 298 | if (r) { 299 | (Array.isArray(r) ? r : r.items).forEach(update); 300 | } 301 | return r; 302 | } 303 | 304 | const r = next(document, position, context, token); 305 | if (isThenable(r)) { 306 | return r.then(updateProposals); 307 | } 308 | return updateProposals(r); 309 | }, 310 | provideHover(document: TextDocument, position: Position, token: CancellationToken, next: ProvideHoverSignature) { 311 | function updateHover(r: Hover | null | undefined): Hover | null | undefined { 312 | if (r && Array.isArray(r.contents)) { 313 | r.contents = r.contents.map(h => h instanceof MarkdownString ? updateMarkdownString(h) : h); 314 | } 315 | return r; 316 | } 317 | const r = next(document, position, token); 318 | if (isThenable(r)) { 319 | return r.then(updateHover); 320 | } 321 | return updateHover(r); 322 | }, 323 | provideFoldingRanges(document: TextDocument, context: FoldingContext, token: CancellationToken, next: ProvideFoldingRangeSignature) { 324 | const r = next(document, context, token); 325 | if (isThenable(r)) { 326 | return r; 327 | } 328 | return r; 329 | }, 330 | provideDocumentColors(document: TextDocument, token: CancellationToken, next: ProvideDocumentColorsSignature) { 331 | const r = next(document, token); 332 | if (isThenable(r)) { 333 | return r; 334 | } 335 | return r; 336 | }, 337 | provideDocumentSymbols(document: TextDocument, token: CancellationToken, next: ProvideDocumentSymbolsSignature) { 338 | type T = SymbolInformation[] | DocumentSymbol[]; 339 | function countDocumentSymbols(symbols: DocumentSymbol[]): number { 340 | return symbols.reduce((previousValue, s) => previousValue + 1 + countDocumentSymbols(s.children), 0); 341 | } 342 | function isDocumentSymbol(r: T): r is DocumentSymbol[] { 343 | return r[0] instanceof DocumentSymbol; 344 | } 345 | function checkLimit(r: T | null | undefined): T | null | undefined { 346 | if (Array.isArray(r) && (isDocumentSymbol(r) ? countDocumentSymbols(r) : r.length) > resultLimit) { 347 | documentSymbolsLimitStatusbarItem.update(document, resultLimit); 348 | } else { 349 | documentSymbolsLimitStatusbarItem.update(document, false); 350 | } 351 | return r; 352 | } 353 | const r = next(document, token); 354 | if (isThenable(r)) { 355 | return r.then(checkLimit); 356 | } 357 | return checkLimit(r); 358 | } 359 | } 360 | }; 361 | 362 | clientOptions.outputChannel = runtime.logOutputChannel; 363 | // Create the language client and start the client. 364 | const client = newLanguageClient('json5', languageServerDescription, clientOptions); 365 | client.registerProposedFeatures(); 366 | 367 | const schemaDocuments: { [uri: string]: boolean } = {}; 368 | 369 | // handle content request 370 | client.onRequest(VSCodeContentRequest.type, async (uriPath: string) => { 371 | const uri = Uri.parse(uriPath); 372 | const uriString = uri.toString(); 373 | if (uri.scheme === 'untitled') { 374 | throw new ResponseError(3, l10n.t('Unable to load {0}', uriString)); 375 | } 376 | if (uri.scheme === 'vscode') { 377 | try { 378 | runtime.logOutputChannel.info('read schema from vscode: ' + uriString); 379 | ensureFilesystemWatcherInstalled(uri); 380 | const content = await workspace.fs.readFile(uri); 381 | return new TextDecoder().decode(content); 382 | } catch (e) { 383 | throw new ResponseError(5, e.toString(), e); 384 | } 385 | } else if (uri.scheme !== 'http' && uri.scheme !== 'https') { 386 | try { 387 | const document = await workspace.openTextDocument(uri); 388 | schemaDocuments[uriString] = true; 389 | return document.getText(); 390 | } catch (e) { 391 | throw new ResponseError(2, e.toString(), e); 392 | } 393 | } else if (schemaDownloadEnabled) { 394 | try { 395 | return await runtime.schemaRequests.getContent(uriString); 396 | } catch (e) { 397 | throw new ResponseError(4, e.toString()); 398 | } 399 | } else { 400 | throw new ResponseError(1, l10n.t('Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload)); 401 | } 402 | }); 403 | 404 | await client.start(); 405 | 406 | isClientReady = true; 407 | 408 | const handleContentChange = (uriString: string) => { 409 | if (schemaDocuments[uriString]) { 410 | client.sendNotification(SchemaContentChangeNotification.type, uriString); 411 | return true; 412 | } 413 | return false; 414 | }; 415 | const handleActiveEditorChange = (activeEditor?: TextEditor) => { 416 | if (!activeEditor) { 417 | return; 418 | } 419 | 420 | const activeDocUri = activeEditor.document.uri.toString(); 421 | 422 | if (activeDocUri && fileSchemaErrors.has(activeDocUri)) { 423 | schemaResolutionErrorStatusBarItem.show(); 424 | } else { 425 | schemaResolutionErrorStatusBarItem.hide(); 426 | } 427 | }; 428 | const handleContentClosed = (uriString: string) => { 429 | if (handleContentChange(uriString)) { 430 | delete schemaDocuments[uriString]; 431 | } 432 | fileSchemaErrors.delete(uriString); 433 | }; 434 | 435 | const watchers: Map = new Map(); 436 | toDispose.push(new Disposable(() => { 437 | for (const d of watchers.values()) { 438 | d.dispose(); 439 | } 440 | })); 441 | 442 | 443 | const ensureFilesystemWatcherInstalled = (uri: Uri) => { 444 | 445 | const uriString = uri.toString(); 446 | if (!watchers.has(uriString)) { 447 | try { 448 | const watcher = workspace.createFileSystemWatcher(new RelativePattern(uri, '*')); 449 | const handleChange = (uri: Uri) => { 450 | runtime.logOutputChannel.info('schema change detected ' + uri.toString()); 451 | client.sendNotification(SchemaContentChangeNotification.type, uriString); 452 | }; 453 | const createListener = watcher.onDidCreate(handleChange); 454 | const changeListener = watcher.onDidChange(handleChange); 455 | const deleteListener = watcher.onDidDelete(() => { 456 | const watcher = watchers.get(uriString); 457 | if (watcher) { 458 | watcher.dispose(); 459 | watchers.delete(uriString); 460 | } 461 | }); 462 | watchers.set(uriString, Disposable.from(watcher, createListener, changeListener, deleteListener)); 463 | } catch { 464 | runtime.logOutputChannel.info('Problem installing a file system watcher for ' + uriString); 465 | } 466 | } 467 | }; 468 | 469 | toDispose.push(workspace.onDidChangeTextDocument(e => handleContentChange(e.document.uri.toString()))); 470 | toDispose.push(workspace.onDidCloseTextDocument(d => handleContentClosed(d.uri.toString()))); 471 | 472 | toDispose.push(window.onDidChangeActiveTextEditor(handleActiveEditorChange)); 473 | 474 | const handleRetryResolveSchemaCommand = () => { 475 | if (window.activeTextEditor) { 476 | schemaResolutionErrorStatusBarItem.text = '$(watch)'; 477 | const activeDocUri = window.activeTextEditor.document.uri.toString(); 478 | client.sendRequest(ForceValidateRequest.type, activeDocUri).then((diagnostics) => { 479 | const schemaErrorIndex = diagnostics.findIndex(isSchemaResolveError); 480 | if (schemaErrorIndex !== -1) { 481 | // Show schema resolution errors in status bar only; ref: #51032 482 | const schemaResolveDiagnostic = diagnostics[schemaErrorIndex]; 483 | fileSchemaErrors.set(activeDocUri, schemaResolveDiagnostic.message); 484 | } else { 485 | schemaResolutionErrorStatusBarItem.hide(); 486 | } 487 | schemaResolutionErrorStatusBarItem.text = '$(alert)'; 488 | }); 489 | } 490 | }; 491 | 492 | toDispose.push(commands.registerCommand('_json5.retryResolveSchema', handleRetryResolveSchemaCommand)); 493 | 494 | client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociations(context)); 495 | 496 | toDispose.push(extensions.onDidChange(_ => { 497 | client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociations(context)); 498 | })); 499 | 500 | // manually register / deregister format provider based on the `json5.format.enable` setting avoiding issues with late registration. See #71652. 501 | updateFormatterRegistration(); 502 | toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() }); 503 | 504 | updateSchemaDownloadSetting(); 505 | 506 | toDispose.push(workspace.onDidChangeConfiguration(e => { 507 | if (e.affectsConfiguration(SettingIds.enableFormatter)) { 508 | updateFormatterRegistration(); 509 | } else if (e.affectsConfiguration(SettingIds.enableSchemaDownload)) { 510 | updateSchemaDownloadSetting(); 511 | } 512 | client.sendNotification(DidChangeConfigurationNotification.type, { settings: getSettings() }); 513 | })); 514 | 515 | toDispose.push(createLanguageStatusItem(documentSelector, (uri: string) => client.sendRequest(LanguageStatusRequest.type, uri))); 516 | 517 | function updateFormatterRegistration() { 518 | const formatEnabled = workspace.getConfiguration().get(SettingIds.enableFormatter); 519 | if (!formatEnabled && rangeFormatting) { 520 | rangeFormatting.dispose(); 521 | rangeFormatting = undefined; 522 | } else if (formatEnabled && !rangeFormatting) { 523 | rangeFormatting = languages.registerDocumentRangeFormattingEditProvider(documentSelector, { 524 | provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult { 525 | const filesConfig = workspace.getConfiguration('files', document); 526 | const fileFormattingOptions = { 527 | trimTrailingWhitespace: filesConfig.get('trimTrailingWhitespace'), 528 | trimFinalNewlines: filesConfig.get('trimFinalNewlines'), 529 | insertFinalNewline: filesConfig.get('insertFinalNewline'), 530 | }; 531 | const params: DocumentRangeFormattingParams = { 532 | textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), 533 | range: client.code2ProtocolConverter.asRange(range), 534 | options: client.code2ProtocolConverter.asFormattingOptions(options, fileFormattingOptions) 535 | }; 536 | 537 | return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then( 538 | client.protocol2CodeConverter.asTextEdits, 539 | (error) => { 540 | client.handleFailedRequest(DocumentRangeFormattingRequest.type, undefined, error, []); 541 | return Promise.resolve([]); 542 | } 543 | ); 544 | } 545 | }); 546 | } 547 | } 548 | 549 | function updateSchemaDownloadSetting() { 550 | schemaDownloadEnabled = workspace.getConfiguration().get(SettingIds.enableSchemaDownload) !== false; 551 | if (schemaDownloadEnabled) { 552 | schemaResolutionErrorStatusBarItem.tooltip = l10n.t('Unable to resolve schema. Click to retry.'); 553 | schemaResolutionErrorStatusBarItem.command = '_json5.retryResolveSchema'; 554 | handleRetryResolveSchemaCommand(); 555 | } else { 556 | schemaResolutionErrorStatusBarItem.tooltip = l10n.t('Downloading schemas is disabled. Click to configure.'); 557 | schemaResolutionErrorStatusBarItem.command = { command: 'workbench.action.openSettings', arguments: [SettingIds.enableSchemaDownload], title: '' }; 558 | } 559 | } 560 | 561 | async function getSortTextEdits(document: TextDocument, tabSize: string | number = 4, insertSpaces: string | boolean = true): Promise { 562 | const filesConfig = workspace.getConfiguration('files', document); 563 | const options: SortOptions = { 564 | tabSize: Number(tabSize), 565 | insertSpaces: Boolean(insertSpaces), 566 | trimTrailingWhitespace: filesConfig.get('trimTrailingWhitespace'), 567 | trimFinalNewlines: filesConfig.get('trimFinalNewlines'), 568 | insertFinalNewline: filesConfig.get('insertFinalNewline'), 569 | }; 570 | const params: DocumentSortingParams = { 571 | uri: document.uri.toString(), 572 | options 573 | }; 574 | const edits = await client.sendRequest(DocumentSortingRequest.type, params); 575 | // Here we convert the JSON objects to real TextEdit objects 576 | return edits.map((edit) => { 577 | return new TextEdit( 578 | new Range(edit.range.start.line, edit.range.start.character, edit.range.end.line, edit.range.end.character), 579 | edit.newText 580 | ); 581 | }); 582 | } 583 | 584 | return { 585 | dispose: async () => { 586 | await client.stop(); 587 | toDispose.forEach(d => d.dispose()); 588 | rangeFormatting?.dispose(); 589 | } 590 | }; 591 | } 592 | 593 | function getSchemaAssociations(_context: ExtensionContext): ISchemaAssociation[] { 594 | const associations: ISchemaAssociation[] = []; 595 | extensions.all.forEach(extension => { 596 | const packageJSON = extension.packageJSON; 597 | if (packageJSON && packageJSON.contributes && packageJSON.contributes.jsonValidation) { 598 | const jsonValidation = packageJSON.contributes.jsonValidation; 599 | if (Array.isArray(jsonValidation)) { 600 | jsonValidation.forEach(jv => { 601 | let { fileMatch, url } = jv; 602 | if (typeof fileMatch === 'string') { 603 | fileMatch = [fileMatch]; 604 | } 605 | if (Array.isArray(fileMatch) && typeof url === 'string') { 606 | let uri: string = url; 607 | if (uri[0] === '.' && uri[1] === '/') { 608 | uri = Uri.joinPath(extension.extensionUri, uri).toString(); 609 | } 610 | fileMatch = fileMatch.map(fm => { 611 | if (fm[0] === '%') { 612 | fm = fm.replace(/%APP_SETTINGS_HOME%/, '/User'); 613 | fm = fm.replace(/%MACHINE_SETTINGS_HOME%/, '/Machine'); 614 | fm = fm.replace(/%APP_WORKSPACES_HOME%/, '/Workspaces'); 615 | } else if (!fm.match(/^(\w+:\/\/|\/|!)/)) { 616 | fm = '/' + fm; 617 | } 618 | return fm; 619 | }); 620 | associations.push({ fileMatch, uri }); 621 | } 622 | }); 623 | } 624 | } 625 | }); 626 | return associations; 627 | } 628 | 629 | function getSettings(): Settings { 630 | const configuration = workspace.getConfiguration(); 631 | const httpSettings = workspace.getConfiguration('http'); 632 | 633 | const normalizeLimit = (settingValue: any) => Math.trunc(Math.max(0, Number(settingValue))) || 5000; 634 | 635 | resultLimit = normalizeLimit(workspace.getConfiguration().get(SettingIds.maxItemsComputed)); 636 | const editorSettings = workspace.getConfiguration(SettingIds.editorSection, { languageId: 'json5' }); 637 | 638 | foldingLimit = normalizeLimit(editorSettings.get(SettingIds.foldingMaximumRegions)); 639 | colorDecoratorLimit = normalizeLimit(editorSettings.get(SettingIds.colorDecoratorsLimit)); 640 | 641 | const schemas: JSONSchemaSettings[] = []; 642 | 643 | const settings: Settings = { 644 | http: { 645 | proxy: httpSettings.get('proxy'), 646 | proxyStrictSSL: httpSettings.get('proxyStrictSSL') 647 | }, 648 | json5: { 649 | validate: { enable: configuration.get(SettingIds.enableValidation) }, 650 | format: { enable: configuration.get(SettingIds.enableFormatter), trailingCommas: configuration.get(SettingIds.trailingCommas), keyQuotes: configuration.get(SettingIds.keyQuotes), stringQuotes: configuration.get(SettingIds.stringQuotes), }, 651 | keepLines: { enable: configuration.get(SettingIds.enableKeepLines) }, 652 | schemas, 653 | resultLimit: resultLimit + 1, // ask for one more so we can detect if the limit has been exceeded 654 | foldingLimit: foldingLimit + 1, 655 | colorDecorationLimit: colorDecoratorLimit + 1, 656 | } 657 | }; 658 | 659 | /* 660 | * Add schemas from the settings 661 | * folderUri to which folder the setting is scoped to. `undefined` means global (also external files) 662 | * settingsLocation against which path relative schema URLs are resolved 663 | */ 664 | const collectSchemaSettings = (schemaSettings: JSONSchemaSettings[] | undefined, folderUri: string | undefined, settingsLocation: Uri | undefined) => { 665 | function substituteVariables(path: string): string { 666 | return path.replace(/\$\{(.*?)\}/g, (match, variable: string) => { 667 | if (variable.startsWith('env:')) { 668 | return process.env[variable.substring('env:'.length)] || ''; 669 | } 670 | if (variable.startsWith('config:')) { 671 | const res = workspace.getConfiguration().get(variable.substring('config:'.length)); 672 | return typeof res === 'string' ? res : ''; 673 | } 674 | folderUri = folderUri || workspace.workspaceFolders?.[0]?.uri.toString(); 675 | switch (variable) { 676 | case 'workspaceFolder': 677 | return settingsLocation ? settingsLocation.fsPath : ''; 678 | case 'workspaceFolderBasename': 679 | return settingsLocation?.path.split('/').pop() || ''; 680 | case 'pathSeparator': 681 | return process.platform === 'win32' ? '\\' : '/'; 682 | } 683 | return match; 684 | }); 685 | } 686 | 687 | function urlify(path: string): string { 688 | if (!path.match(/^(\w+:\/\/|\/|!)/)) { 689 | return Uri.file(path).toString(); 690 | } 691 | return path; 692 | } 693 | 694 | if (schemaSettings) { 695 | for (const setting of schemaSettings) { 696 | const url = getSchemaId(setting, settingsLocation); 697 | if (url) { 698 | const schemaSetting: JSONSchemaSettings = { url: urlify(substituteVariables(url)), fileMatch: setting.fileMatch?.map(substituteVariables), folderUri, schema: setting.schema }; 699 | schemas.push(schemaSetting); 700 | } 701 | } 702 | } 703 | }; 704 | 705 | const folders = workspace.workspaceFolders ?? []; 706 | 707 | const schemaConfigInfo = workspace.getConfiguration('json5', null).inspect('schemas'); 708 | if (schemaConfigInfo) { 709 | // settings in user config 710 | collectSchemaSettings(schemaConfigInfo.globalValue, undefined, undefined); 711 | if (workspace.workspaceFile) { 712 | if (schemaConfigInfo.workspaceValue) { 713 | const settingsLocation = Uri.joinPath(workspace.workspaceFile, '..'); 714 | // settings in the workspace configuration file apply to all files (also external files) 715 | collectSchemaSettings(schemaConfigInfo.workspaceValue, undefined, settingsLocation); 716 | } 717 | for (const folder of folders) { 718 | const folderUri = folder.uri; 719 | const folderSchemaConfigInfo = workspace.getConfiguration('json5', folderUri).inspect('schemas'); 720 | collectSchemaSettings(folderSchemaConfigInfo?.workspaceFolderValue, folderUri.toString(false), folderUri); 721 | } 722 | } else { 723 | if (schemaConfigInfo.workspaceValue && folders.length === 1) { 724 | // single folder workspace: settings apply to all files (also external files) 725 | collectSchemaSettings(schemaConfigInfo.workspaceValue, undefined, folders[0].uri); 726 | } 727 | } 728 | } 729 | return settings; 730 | } 731 | 732 | function getSchemaId(schema: JSONSchemaSettings, settingsLocation?: Uri): string | undefined { 733 | let url = schema.url; 734 | if (!url) { 735 | if (schema.schema) { 736 | url = schema.schema.id || `vscode://schemas/custom/${encodeURIComponent(hash(schema.schema).toString(16))}`; 737 | } 738 | } else if (settingsLocation && (url[0] === '.' || url[0] === '/')) { 739 | url = Uri.joinPath(settingsLocation, url).toString(false); 740 | } 741 | return url; 742 | } 743 | 744 | function isThenable(obj: ProviderResult): obj is Thenable { 745 | return obj && (obj)['then']; 746 | } 747 | 748 | function updateMarkdownString(h: MarkdownString): MarkdownString { 749 | const n = new MarkdownString(h.value, true); 750 | n.isTrusted = h.isTrusted; 751 | return n; 752 | } 753 | 754 | function isSchemaResolveError(d: Diagnostic) { 755 | return d.code === /* SchemaResolveError */ 0x300; 756 | } -------------------------------------------------------------------------------- /src/client/languageParticipants.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { Event, EventEmitter, extensions } from 'vscode'; 7 | 8 | /** 9 | * JSON language participant contribution. 10 | */ 11 | interface LanguageParticipantContribution { 12 | /** 13 | * The id of the language which participates with the JSON language server. 14 | */ 15 | languageId: string; 16 | /** 17 | * true if the language allows comments and false otherwise. 18 | * TODO: implement server side setting 19 | */ 20 | comments?: boolean; 21 | } 22 | 23 | export interface LanguageParticipants { 24 | readonly onDidChange: Event; 25 | readonly documentSelector: string[]; 26 | hasLanguage(languageId: string): boolean; 27 | useComments(languageId: string): boolean; 28 | dispose(): void; 29 | } 30 | 31 | export function getLanguageParticipants(): LanguageParticipants { 32 | const onDidChangeEmmiter = new EventEmitter(); 33 | let languages = new Set(); 34 | let comments = new Set(); 35 | 36 | function update() { 37 | const oldLanguages = languages, oldComments = comments; 38 | 39 | languages = new Set(); 40 | languages.add('json5'); 41 | comments = new Set(); 42 | comments.add('json5'); 43 | 44 | for (const extension of extensions.all) { 45 | const jsonLanguageParticipants = extension.packageJSON?.contributes?.jsonLanguageParticipants as LanguageParticipantContribution[]; 46 | if (Array.isArray(jsonLanguageParticipants)) { 47 | for (const jsonLanguageParticipant of jsonLanguageParticipants) { 48 | const languageId = jsonLanguageParticipant.languageId; 49 | if (typeof languageId === 'string') { 50 | languages.add(languageId); 51 | if (jsonLanguageParticipant.comments === true) { 52 | comments.add(languageId); 53 | } 54 | } 55 | } 56 | } 57 | } 58 | return !isEqualSet(languages, oldLanguages) || !isEqualSet(comments, oldComments); 59 | } 60 | update(); 61 | 62 | const changeListener = extensions.onDidChange(_ => { 63 | if (update()) { 64 | onDidChangeEmmiter.fire(); 65 | } 66 | }); 67 | 68 | return { 69 | onDidChange: onDidChangeEmmiter.event, 70 | get documentSelector() { return Array.from(languages); }, 71 | hasLanguage(languageId: string) { return languages.has(languageId); }, 72 | useComments(languageId: string) { return comments.has(languageId); }, 73 | dispose: () => changeListener.dispose() 74 | }; 75 | } 76 | 77 | function isEqualSet(s1: Set, s2: Set) { 78 | if (s1.size !== s2.size) { 79 | return false; 80 | } 81 | for (const e of s1) { 82 | if (!s2.has(e)) { 83 | return false; 84 | } 85 | } 86 | return true; 87 | } -------------------------------------------------------------------------------- /src/client/languageStatus.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { 7 | window, languages, Uri, Disposable, commands, QuickPickItem, 8 | extensions, workspace, Extension, WorkspaceFolder, QuickPickItemKind, 9 | ThemeIcon, TextDocument, LanguageStatusSeverity, l10n, DocumentSelector 10 | } from 'vscode'; 11 | import { JSONLanguageStatus, JSONSchemaSettings } from './client'; 12 | 13 | type ShowSchemasInput = { 14 | schemas: string[]; 15 | uri: string; 16 | }; 17 | 18 | interface ShowSchemasItem extends QuickPickItem { 19 | uri?: Uri; 20 | buttonCommands?: (() => void)[]; 21 | } 22 | 23 | function getExtensionSchemaAssociations() { 24 | const associations: { fullUri: string; extension: Extension; label: string }[] = []; 25 | 26 | for (const extension of extensions.all) { 27 | const jsonValidations = extension.packageJSON?.contributes?.jsonValidation; 28 | if (Array.isArray(jsonValidations)) { 29 | for (const jsonValidation of jsonValidations) { 30 | let uri = jsonValidation.url; 31 | if (typeof uri === 'string') { 32 | if (uri[0] === '.' && uri[1] === '/') { 33 | uri = Uri.joinPath(extension.extensionUri, uri).toString(false); 34 | } 35 | associations.push({ fullUri: uri, extension, label: jsonValidation.url }); 36 | } 37 | } 38 | } 39 | } 40 | return { 41 | findExtension(uri: string): ShowSchemasItem | undefined { 42 | for (const association of associations) { 43 | if (association.fullUri === uri) { 44 | return { 45 | label: association.label, 46 | detail: l10n.t('Configured by extension: {0}', association.extension.id), 47 | uri: Uri.parse(association.fullUri), 48 | buttons: [{ iconPath: new ThemeIcon('extensions'), tooltip: l10n.t('Open Extension') }], 49 | buttonCommands: [() => commands.executeCommand('workbench.extensions.action.showExtensionsWithIds', [[association.extension.id]])] 50 | }; 51 | } 52 | } 53 | return undefined; 54 | } 55 | }; 56 | } 57 | 58 | // 59 | 60 | function getSettingsSchemaAssociations(uri: string) { 61 | const resourceUri = Uri.parse(uri); 62 | const workspaceFolder = workspace.getWorkspaceFolder(resourceUri); 63 | 64 | const settings = workspace.getConfiguration('json5', resourceUri).inspect('schemas'); 65 | 66 | const associations: { fullUri: string; workspaceFolder: WorkspaceFolder | undefined; label: string }[] = []; 67 | 68 | const folderSettingSchemas = settings?.workspaceFolderValue; 69 | if (workspaceFolder && Array.isArray(folderSettingSchemas)) { 70 | for (const setting of folderSettingSchemas) { 71 | const uri = setting.url; 72 | if (typeof uri === 'string') { 73 | let fullUri = uri; 74 | if (uri[0] === '.' && uri[1] === '/') { 75 | fullUri = Uri.joinPath(workspaceFolder.uri, uri).toString(false); 76 | } 77 | associations.push({ fullUri, workspaceFolder, label: uri }); 78 | } 79 | } 80 | } 81 | const userSettingSchemas = settings?.globalValue; 82 | if (Array.isArray(userSettingSchemas)) { 83 | for (const setting of userSettingSchemas) { 84 | const uri = setting.url; 85 | if (typeof uri === 'string') { 86 | let fullUri = uri; 87 | if (workspaceFolder && uri[0] === '.' && uri[1] === '/') { 88 | fullUri = Uri.joinPath(workspaceFolder.uri, uri).toString(false); 89 | } 90 | associations.push({ fullUri, workspaceFolder: undefined, label: uri }); 91 | } 92 | } 93 | } 94 | return { 95 | findSetting(uri: string): ShowSchemasItem | undefined { 96 | for (const association of associations) { 97 | if (association.fullUri === uri) { 98 | return { 99 | label: association.label, 100 | detail: association.workspaceFolder ? l10n.t('Configured in workspace settings') : l10n.t('Configured in user settings'), 101 | uri: Uri.parse(association.fullUri), 102 | buttons: [{ iconPath: new ThemeIcon('gear'), tooltip: l10n.t('Open Settings') }], 103 | buttonCommands: [() => commands.executeCommand(association.workspaceFolder ? 'workbench.action.openWorkspaceSettingsFile' : 'workbench.action.openSettingsJson', ['json5.schemas'])] 104 | }; 105 | } 106 | } 107 | return undefined; 108 | } 109 | }; 110 | } 111 | 112 | function showSchemaList(input: ShowSchemasInput) { 113 | 114 | const extensionSchemaAssocations = getExtensionSchemaAssociations(); 115 | const settingsSchemaAssocations = getSettingsSchemaAssociations(input.uri); 116 | 117 | const extensionEntries = []; 118 | const settingsEntries = []; 119 | const otherEntries = []; 120 | 121 | for (const schemaUri of input.schemas) { 122 | const extensionEntry = extensionSchemaAssocations.findExtension(schemaUri); 123 | if (extensionEntry) { 124 | extensionEntries.push(extensionEntry); 125 | continue; 126 | } 127 | const settingsEntry = settingsSchemaAssocations.findSetting(schemaUri); 128 | if (settingsEntry) { 129 | settingsEntries.push(settingsEntry); 130 | continue; 131 | } 132 | otherEntries.push({ label: schemaUri, uri: Uri.parse(schemaUri) }); 133 | } 134 | 135 | const items: ShowSchemasItem[] = [...extensionEntries, ...settingsEntries, ...otherEntries]; 136 | if (items.length === 0) { 137 | items.push({ 138 | label: l10n.t('No schema configured for this file'), 139 | buttons: [{ iconPath: new ThemeIcon('gear'), tooltip: l10n.t('Open Settings') }], 140 | buttonCommands: [() => commands.executeCommand('workbench.action.openSettingsJson', ['json5.schemas'])] 141 | }); 142 | } 143 | 144 | items.push({ label: '', kind: QuickPickItemKind.Separator }); 145 | items.push({ label: l10n.t('Learn more about JSON schema configuration...'), uri: Uri.parse('https://code.visualstudio.com/docs/languages/json#_json-schemas-and-settings') }); 146 | 147 | const quickPick = window.createQuickPick(); 148 | quickPick.placeholder = items.length ? l10n.t('Select the schema to use for {0}', input.uri) : undefined; 149 | quickPick.items = items; 150 | quickPick.show(); 151 | quickPick.onDidAccept(() => { 152 | const uri = quickPick.selectedItems[0].uri; 153 | if (uri) { 154 | commands.executeCommand('vscode.open', uri); 155 | quickPick.dispose(); 156 | } 157 | }); 158 | quickPick.onDidTriggerItemButton(b => { 159 | const index = b.item.buttons?.indexOf(b.button); 160 | if (index !== undefined && index >= 0 && b.item.buttonCommands && b.item.buttonCommands[index]) { 161 | b.item.buttonCommands[index](); 162 | } 163 | }); 164 | } 165 | 166 | export function createLanguageStatusItem(documentSelector: DocumentSelector, statusRequest: (uri: string) => Promise): Disposable { 167 | const statusItem = languages.createLanguageStatusItem('json5.projectStatus', documentSelector); 168 | statusItem.name = l10n.t('JSON5 Validation Status'); 169 | statusItem.severity = LanguageStatusSeverity.Information; 170 | 171 | const showSchemasCommand = commands.registerCommand('_json5.showAssociatedSchemaList', showSchemaList); 172 | 173 | const activeEditorListener = window.onDidChangeActiveTextEditor(() => { 174 | updateLanguageStatus(); 175 | }); 176 | 177 | async function updateLanguageStatus() { 178 | const document = window.activeTextEditor?.document; 179 | if (document) { 180 | try { 181 | statusItem.text = '$(loading~spin)'; 182 | statusItem.detail = l10n.t('Loading JSON5 info'); 183 | statusItem.command = undefined; 184 | 185 | const schemas = (await statusRequest(document.uri.toString())).schemas; 186 | statusItem.detail = undefined; 187 | if (schemas.length === 0) { 188 | statusItem.text = l10n.t('No Schema Validation'); 189 | statusItem.detail = l10n.t('no JSON schema configured'); 190 | } else if (schemas.length === 1) { 191 | statusItem.text = l10n.t('Schema Validated'); 192 | statusItem.detail = l10n.t('JSON schema configured'); 193 | } else { 194 | statusItem.text = l10n.t('Schema Validated'); 195 | statusItem.detail = l10n.t('multiple JSON schemas configured'); 196 | } 197 | statusItem.command = { 198 | command: '_json5.showAssociatedSchemaList', 199 | title: l10n.t('Show Schemas'), 200 | arguments: [{ schemas, uri: document.uri.toString() } satisfies ShowSchemasInput] 201 | }; 202 | } catch (e) { 203 | statusItem.text = l10n.t('Unable to compute used schemas: {0}', e.message); 204 | statusItem.detail = undefined; 205 | statusItem.command = undefined; 206 | } 207 | } else { 208 | statusItem.text = l10n.t('Unable to compute used schemas: No document'); 209 | statusItem.detail = undefined; 210 | statusItem.command = undefined; 211 | } 212 | } 213 | 214 | updateLanguageStatus(); 215 | 216 | return Disposable.from(statusItem, activeEditorListener, showSchemasCommand); 217 | } 218 | 219 | export function createLimitStatusItem(newItem: (limit: number) => Disposable) { 220 | let statusItem: Disposable | undefined; 221 | const activeLimits: Map = new Map(); 222 | 223 | const toDispose: Disposable[] = []; 224 | toDispose.push(window.onDidChangeActiveTextEditor(textEditor => { 225 | statusItem?.dispose(); 226 | statusItem = undefined; 227 | const doc = textEditor?.document; 228 | if (doc) { 229 | const limit = activeLimits.get(doc); 230 | if (limit !== undefined) { 231 | statusItem = newItem(limit); 232 | } 233 | } 234 | })); 235 | toDispose.push(workspace.onDidCloseTextDocument(document => { 236 | activeLimits.delete(document); 237 | })); 238 | 239 | function update(document: TextDocument, limitApplied: number | false) { 240 | if (limitApplied === false) { 241 | activeLimits.delete(document); 242 | if (statusItem && document === window.activeTextEditor?.document) { 243 | statusItem.dispose(); 244 | statusItem = undefined; 245 | } 246 | } else { 247 | activeLimits.set(document, limitApplied); 248 | if (document === window.activeTextEditor?.document) { 249 | if (!statusItem || limitApplied !== activeLimits.get(document)) { 250 | statusItem?.dispose(); 251 | statusItem = newItem(limitApplied); 252 | } 253 | } 254 | } 255 | } 256 | return { 257 | update, 258 | dispose() { 259 | statusItem?.dispose(); 260 | toDispose.forEach(d => d.dispose()); 261 | toDispose.length = 0; 262 | statusItem = undefined; 263 | activeLimits.clear(); 264 | } 265 | }; 266 | } 267 | 268 | const openSettingsCommand = 'workbench.action.openSettings'; 269 | const configureSettingsLabel = l10n.t('Configure'); 270 | 271 | export function createDocumentSymbolsLimitItem(documentSelector: DocumentSelector, settingId: string, limit: number): Disposable { 272 | const statusItem = languages.createLanguageStatusItem('json5.documentSymbolsStatus', documentSelector); 273 | statusItem.name = l10n.t('JSON Outline Status'); 274 | statusItem.severity = LanguageStatusSeverity.Warning; 275 | statusItem.text = l10n.t('Outline'); 276 | statusItem.detail = l10n.t('only {0} document symbols shown for performance reasons', limit); 277 | statusItem.command = { command: openSettingsCommand, arguments: [settingId], title: configureSettingsLabel }; 278 | return Disposable.from(statusItem); 279 | } 280 | 281 | -------------------------------------------------------------------------------- /src/client/node/clientMain.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { Disposable, ExtensionContext, LogOutputChannel, window, l10n, env, LogLevel } from 'vscode'; 7 | import { startClient, LanguageClientConstructor, SchemaRequestService, languageServerDescription, AsyncDisposable } from '../client'; 8 | import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node'; 9 | 10 | import { promises as fs } from 'fs'; 11 | import * as path from 'path'; 12 | import { xhr, XHRResponse, getErrorStatusDescription, Headers } from 'request-light'; 13 | 14 | import { JSONSchemaCache } from './schemaCache'; 15 | 16 | let client: AsyncDisposable | undefined; 17 | 18 | // this method is called when vs code is activated 19 | export async function activate(context: ExtensionContext) { 20 | const clientPackageJSON = await getPackageInfo(context); 21 | 22 | const logOutputChannel = window.createOutputChannel(languageServerDescription, { log: true }); 23 | context.subscriptions.push(logOutputChannel); 24 | 25 | const serverMain = `./${clientPackageJSON.main.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/serverMain`; 26 | const serverModule = context.asAbsolutePath(serverMain); 27 | 28 | // The debug options for the server 29 | const debugOptions = { execArgv: ['--nolazy', '--inspect=' + (6000 + Math.round(Math.random() * 999))] }; 30 | 31 | // If the extension is launch in debug mode the debug server options are use 32 | // Otherwise the run options are used 33 | const serverOptions: ServerOptions = { 34 | run: { module: serverModule, transport: TransportKind.ipc }, 35 | debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } 36 | }; 37 | 38 | const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { 39 | return new LanguageClient(id, name, serverOptions, clientOptions); 40 | }; 41 | 42 | const timer = { 43 | setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable { 44 | const handle = setTimeout(callback, ms, ...args); 45 | return { dispose: () => clearTimeout(handle) }; 46 | } 47 | }; 48 | 49 | // pass the location of the localization bundle to the server 50 | process.env['VSCODE_L10N_BUNDLE_LOCATION'] = l10n.uri?.toString() ?? ''; 51 | 52 | const schemaRequests = await getSchemaRequestService(context, logOutputChannel); 53 | 54 | client = await startClient(context, newLanguageClient, { schemaRequests, timer, logOutputChannel }); 55 | } 56 | 57 | export async function deactivate(): Promise { 58 | if (client) { 59 | await client.dispose(); 60 | client = undefined; 61 | } 62 | } 63 | 64 | interface IPackageInfo { 65 | name: string; 66 | version: string; 67 | aiKey: string; 68 | main: string; 69 | } 70 | 71 | async function getPackageInfo(context: ExtensionContext): Promise { 72 | const location = context.asAbsolutePath('./package.json'); 73 | try { 74 | return JSON.parse((await fs.readFile(location)).toString()); 75 | } catch (e) { 76 | console.log(`Problems reading ${location}: ${e}`); 77 | return { name: '', version: '', aiKey: '', main: '' }; 78 | } 79 | } 80 | 81 | const retryTimeoutInHours = 2 * 24; // 2 days 82 | 83 | async function getSchemaRequestService(context: ExtensionContext, log: LogOutputChannel): Promise { 84 | let cache: JSONSchemaCache | undefined = undefined; 85 | const globalStorage = context.globalStorageUri; 86 | 87 | let clearCache: (() => Promise) | undefined; 88 | if (globalStorage.scheme === 'file') { 89 | const schemaCacheLocation = path.join(globalStorage.fsPath, 'json-schema-cache'); 90 | await fs.mkdir(schemaCacheLocation, { recursive: true }); 91 | 92 | const schemaCache = new JSONSchemaCache(schemaCacheLocation, context.globalState); 93 | log.trace(`[json schema cache] initial state: ${JSON.stringify(schemaCache.getCacheInfo(), null, ' ')}`); 94 | cache = schemaCache; 95 | clearCache = async () => { 96 | const cachedSchemas = await schemaCache.clearCache(); 97 | log.trace(`[json schema cache] cache cleared. Previously cached schemas: ${cachedSchemas.join(', ')}`); 98 | return cachedSchemas; 99 | }; 100 | } 101 | 102 | 103 | const isXHRResponse = (error: any): error is XHRResponse => typeof error?.status === 'number'; 104 | 105 | const request = async (uri: string, etag?: string): Promise => { 106 | const headers: Headers = { 107 | 'Accept-Encoding': 'gzip, deflate', 108 | 'User-Agent': `${env.appName} (${env.appHost})` 109 | }; 110 | if (etag) { 111 | headers['If-None-Match'] = etag; 112 | } 113 | try { 114 | log.trace(`[json schema cache] Requesting schema ${uri} etag ${etag}...`); 115 | 116 | const response = await xhr({ url: uri, followRedirects: 5, headers }); 117 | if (cache) { 118 | const etag = response.headers['etag']; 119 | if (typeof etag === 'string') { 120 | log.trace(`[json schema cache] Storing schema ${uri} etag ${etag} in cache`); 121 | await cache.putSchema(uri, etag, response.responseText); 122 | } else { 123 | log.trace(`[json schema cache] Response: schema ${uri} no etag`); 124 | } 125 | } 126 | return response.responseText; 127 | } catch (error: unknown) { 128 | if (isXHRResponse(error)) { 129 | if (error.status === 304 && etag && cache) { 130 | 131 | log.trace(`[json schema cache] Response: schema ${uri} unchanged etag ${etag}`); 132 | 133 | const content = await cache.getSchema(uri, etag, true); 134 | if (content) { 135 | log.trace(`[json schema cache] Get schema ${uri} etag ${etag} from cache`); 136 | return content; 137 | } 138 | return request(uri); 139 | } 140 | 141 | let status = getErrorStatusDescription(error.status); 142 | if (status && error.responseText) { 143 | status = `${status}\n${error.responseText.substring(0, 200)}`; 144 | } 145 | if (!status) { 146 | status = error.toString(); 147 | } 148 | log.trace(`[json schema cache] Respond schema ${uri} error ${status}`); 149 | 150 | throw status; 151 | } 152 | throw error; 153 | } 154 | }; 155 | 156 | return { 157 | getContent: async (uri: string) => { 158 | if (cache && /^https?:\/\/json\.schemastore\.org\//.test(uri)) { 159 | const content = await cache.getSchemaIfUpdatedSince(uri, retryTimeoutInHours); 160 | if (content) { 161 | if (log.logLevel === LogLevel.Trace) { 162 | log.trace(`[json schema cache] Schema ${uri} from cache without request (last accessed ${cache.getLastUpdatedInHours(uri)} hours ago)`); 163 | } 164 | 165 | return content; 166 | } 167 | } 168 | return request(uri, cache?.getETag(uri)); 169 | }, 170 | clearCache 171 | }; 172 | } -------------------------------------------------------------------------------- /src/client/node/schemaCache.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { promises as fs } from 'fs'; 7 | import * as path from 'path'; 8 | import { createHash } from 'crypto'; 9 | import { Memento } from 'vscode'; 10 | 11 | interface CacheEntry { 12 | etag: string; 13 | fileName: string; 14 | updateTime: number; 15 | } 16 | 17 | interface CacheInfo { 18 | [schemaUri: string]: CacheEntry; 19 | } 20 | 21 | const MEMENTO_KEY = 'json-schema-cache'; 22 | 23 | export class JSONSchemaCache { 24 | private cacheInfo: CacheInfo; 25 | 26 | constructor(private readonly schemaCacheLocation: string, private readonly globalState: Memento) { 27 | const infos = globalState.get(MEMENTO_KEY, {}) as CacheInfo; 28 | const validated: CacheInfo = {}; 29 | for (const schemaUri in infos) { 30 | const { etag, fileName, updateTime } = infos[schemaUri]; 31 | if (typeof etag === 'string' && typeof fileName === 'string' && typeof updateTime === 'number') { 32 | validated[schemaUri] = { etag, fileName, updateTime }; 33 | } 34 | } 35 | this.cacheInfo = validated; 36 | } 37 | 38 | getETag(schemaUri: string): string | undefined { 39 | return this.cacheInfo[schemaUri]?.etag; 40 | } 41 | 42 | getLastUpdatedInHours(schemaUri: string): number | undefined { 43 | const updateTime = this.cacheInfo[schemaUri]?.updateTime; 44 | if (updateTime !== undefined) { 45 | return (new Date().getTime() - updateTime) / 1000 / 60 / 60; 46 | } 47 | return undefined; 48 | } 49 | 50 | async putSchema(schemaUri: string, etag: string, schemaContent: string): Promise { 51 | try { 52 | const fileName = getCacheFileName(schemaUri); 53 | await fs.writeFile(path.join(this.schemaCacheLocation, fileName), schemaContent); 54 | const entry: CacheEntry = { etag, fileName, updateTime: new Date().getTime() }; 55 | this.cacheInfo[schemaUri] = entry; 56 | } catch (e) { 57 | delete this.cacheInfo[schemaUri]; 58 | } finally { 59 | await this.updateMemento(); 60 | } 61 | } 62 | 63 | async getSchemaIfUpdatedSince(schemaUri: string, expirationDurationInHours: number): Promise { 64 | const lastUpdatedInHours = this.getLastUpdatedInHours(schemaUri); 65 | if (lastUpdatedInHours !== undefined && (lastUpdatedInHours < expirationDurationInHours)) { 66 | return this.loadSchemaFile(schemaUri, this.cacheInfo[schemaUri], false); 67 | } 68 | return undefined; 69 | } 70 | 71 | async getSchema(schemaUri: string, etag: string, etagValid: boolean): Promise { 72 | const cacheEntry = this.cacheInfo[schemaUri]; 73 | if (cacheEntry) { 74 | if (cacheEntry.etag === etag) { 75 | return this.loadSchemaFile(schemaUri, cacheEntry, etagValid); 76 | } else { 77 | this.deleteSchemaFile(schemaUri, cacheEntry); 78 | } 79 | } 80 | return undefined; 81 | } 82 | 83 | private async loadSchemaFile(schemaUri: string, cacheEntry: CacheEntry, isUpdated: boolean): Promise { 84 | const cacheLocation = path.join(this.schemaCacheLocation, cacheEntry.fileName); 85 | try { 86 | const content = (await fs.readFile(cacheLocation)).toString(); 87 | if (isUpdated) { 88 | cacheEntry.updateTime = new Date().getTime(); 89 | } 90 | return content; 91 | } catch (e) { 92 | delete this.cacheInfo[schemaUri]; 93 | return undefined; 94 | } finally { 95 | await this.updateMemento(); 96 | } 97 | } 98 | 99 | private async deleteSchemaFile(schemaUri: string, cacheEntry: CacheEntry): Promise { 100 | const cacheLocation = path.join(this.schemaCacheLocation, cacheEntry.fileName); 101 | delete this.cacheInfo[schemaUri]; 102 | await this.updateMemento(); 103 | try { 104 | await fs.rm(cacheLocation); 105 | } catch (e) { 106 | // ignore 107 | } 108 | } 109 | 110 | 111 | // for debugging 112 | public getCacheInfo() { 113 | return this.cacheInfo; 114 | } 115 | 116 | private async updateMemento() { 117 | try { 118 | await this.globalState.update(MEMENTO_KEY, this.cacheInfo); 119 | } catch (e) { 120 | // ignore 121 | } 122 | } 123 | 124 | public async clearCache(): Promise { 125 | const uris = Object.keys(this.cacheInfo); 126 | try { 127 | const files = await fs.readdir(this.schemaCacheLocation); 128 | for (const file of files) { 129 | try { 130 | await fs.unlink(path.join(this.schemaCacheLocation, file)); 131 | } catch (_e) { 132 | // ignore 133 | } 134 | } 135 | } catch (e) { 136 | // ignore 137 | } finally { 138 | 139 | this.cacheInfo = {}; 140 | await this.updateMemento(); 141 | } 142 | return uris; 143 | } 144 | } 145 | function getCacheFileName(uri: string): string { 146 | return `${createHash('sha256').update(uri).digest('hex')}.schema.json`; 147 | } -------------------------------------------------------------------------------- /src/client/utils/hash.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | /** 7 | * Return a hash value for an object. 8 | */ 9 | export function hash(obj: any, hashVal = 0): number { 10 | switch (typeof obj) { 11 | case 'object': 12 | if (obj === null) { 13 | return numberHash(349, hashVal); 14 | } else if (Array.isArray(obj)) { 15 | return arrayHash(obj, hashVal); 16 | } 17 | return objectHash(obj, hashVal); 18 | case 'string': 19 | return stringHash(obj, hashVal); 20 | case 'boolean': 21 | return booleanHash(obj, hashVal); 22 | case 'number': 23 | return numberHash(obj, hashVal); 24 | case 'undefined': 25 | return 937 * 31; 26 | default: 27 | return numberHash(obj, 617); 28 | } 29 | } 30 | 31 | function numberHash(val: number, initialHashVal: number): number { 32 | return (((initialHashVal << 5) - initialHashVal) + val) | 0; // hashVal * 31 + ch, keep as int32 33 | } 34 | 35 | function booleanHash(b: boolean, initialHashVal: number): number { 36 | return numberHash(b ? 433 : 863, initialHashVal); 37 | } 38 | 39 | function stringHash(s: string, hashVal: number) { 40 | hashVal = numberHash(149417, hashVal); 41 | for (let i = 0, length = s.length; i < length; i++) { 42 | hashVal = numberHash(s.charCodeAt(i), hashVal); 43 | } 44 | return hashVal; 45 | } 46 | 47 | function arrayHash(arr: any[], initialHashVal: number): number { 48 | initialHashVal = numberHash(104579, initialHashVal); 49 | return arr.reduce((hashVal, item) => hash(item, hashVal), initialHashVal); 50 | } 51 | 52 | function objectHash(obj: any, initialHashVal: number): number { 53 | initialHashVal = numberHash(181387, initialHashVal); 54 | return Object.keys(obj).sort().reduce((hashVal, key) => { 55 | hashVal = stringHash(key, hashVal); 56 | return hash(obj[key], hashVal); 57 | }, initialHashVal); 58 | } -------------------------------------------------------------------------------- /src/server/browser/serverMain.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { createConnection, BrowserMessageReader, BrowserMessageWriter, Disposable } from 'vscode-languageserver/browser'; 7 | import { RuntimeEnvironment, startServer } from '../server'; 8 | 9 | 10 | const messageReader = new BrowserMessageReader(self); 11 | const messageWriter = new BrowserMessageWriter(self); 12 | 13 | const connection = createConnection(messageReader, messageWriter); 14 | 15 | console.log = connection.console.log.bind(connection.console); 16 | console.error = connection.console.error.bind(connection.console); 17 | 18 | const runtime: RuntimeEnvironment = { 19 | timer: { 20 | setImmediate(callback: (...args: any[]) => void, ...args: any[]): Disposable { 21 | const handle = setTimeout(callback, 0, ...args); 22 | return { dispose: () => clearTimeout(handle) }; 23 | }, 24 | setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable { 25 | const handle = setTimeout(callback, ms, ...args); 26 | return { dispose: () => clearTimeout(handle) }; 27 | } 28 | } 29 | }; 30 | 31 | startServer(connection, runtime); -------------------------------------------------------------------------------- /src/server/browser/serverWorkerMain.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as l10n from '@vscode/l10n'; 7 | 8 | let initialized = false; 9 | const pendingMessages: any[] = []; 10 | const messageHandler = async (e: any) => { 11 | if (!initialized) { 12 | const l10nLog: string[] = []; 13 | initialized = true; 14 | const i10lLocation = e.data.i10lLocation; 15 | if (i10lLocation) { 16 | try { 17 | await l10n.config({ uri: i10lLocation }); 18 | l10nLog.push(`l10n: Configured to ${i10lLocation.toString()}.`); 19 | } catch (e) { 20 | l10nLog.push(`l10n: Problems loading ${i10lLocation.toString()} : ${e}.`); 21 | } 22 | } else { 23 | l10nLog.push(`l10n: No bundle configured.`); 24 | } 25 | await import('./serverMain.js'); 26 | if (self.onmessage !== messageHandler) { 27 | pendingMessages.forEach(msg => self.onmessage?.(msg)); 28 | pendingMessages.length = 0; 29 | } 30 | l10nLog.forEach(console.log); 31 | } else { 32 | pendingMessages.push(e); 33 | } 34 | }; 35 | self.onmessage = messageHandler; -------------------------------------------------------------------------------- /src/server/languageModelCache.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { TextDocument } from 'vscode-languageserver'; 7 | 8 | export interface LanguageModelCache { 9 | get(document: TextDocument): T; 10 | onDocumentRemoved(document: TextDocument): void; 11 | dispose(): void; 12 | } 13 | 14 | export function getLanguageModelCache(maxEntries: number, cleanupIntervalTimeInSec: number, parse: (document: TextDocument) => T): LanguageModelCache { 15 | let languageModels: { [uri: string]: { version: number; languageId: string; cTime: number; languageModel: T } } = {}; 16 | let nModels = 0; 17 | 18 | let cleanupInterval: NodeJS.Timeout | undefined = undefined; 19 | if (cleanupIntervalTimeInSec > 0) { 20 | cleanupInterval = setInterval(() => { 21 | const cutoffTime = Date.now() - cleanupIntervalTimeInSec * 1000; 22 | const uris = Object.keys(languageModels); 23 | for (const uri of uris) { 24 | const languageModelInfo = languageModels[uri]; 25 | if (languageModelInfo.cTime < cutoffTime) { 26 | delete languageModels[uri]; 27 | nModels--; 28 | } 29 | } 30 | }, cleanupIntervalTimeInSec * 1000); 31 | } 32 | 33 | return { 34 | get(document: TextDocument): T { 35 | const version = document.version; 36 | const languageId = document.languageId; 37 | const languageModelInfo = languageModels[document.uri]; 38 | if (languageModelInfo && languageModelInfo.version === version && languageModelInfo.languageId === languageId) { 39 | languageModelInfo.cTime = Date.now(); 40 | return languageModelInfo.languageModel; 41 | } 42 | const languageModel = parse(document); 43 | languageModels[document.uri] = { languageModel, version, languageId, cTime: Date.now() }; 44 | if (!languageModelInfo) { 45 | nModels++; 46 | } 47 | 48 | if (nModels === maxEntries) { 49 | let oldestTime = Number.MAX_VALUE; 50 | let oldestUri = null; 51 | for (const uri in languageModels) { 52 | const languageModelInfo = languageModels[uri]; 53 | if (languageModelInfo.cTime < oldestTime) { 54 | oldestUri = uri; 55 | oldestTime = languageModelInfo.cTime; 56 | } 57 | } 58 | if (oldestUri) { 59 | delete languageModels[oldestUri]; 60 | nModels--; 61 | } 62 | } 63 | return languageModel; 64 | 65 | }, 66 | onDocumentRemoved(document: TextDocument) { 67 | const uri = document.uri; 68 | if (languageModels[uri]) { 69 | delete languageModels[uri]; 70 | nModels--; 71 | } 72 | }, 73 | dispose() { 74 | if (typeof cleanupInterval !== 'undefined') { 75 | clearInterval(cleanupInterval); 76 | cleanupInterval = undefined; 77 | languageModels = {}; 78 | nModels = 0; 79 | } 80 | } 81 | }; 82 | } -------------------------------------------------------------------------------- /src/server/node/serverMain.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { createConnection, Connection, Disposable } from 'vscode-languageserver/node'; 7 | import { formatError } from '../utils/runner'; 8 | import { RequestService, RuntimeEnvironment, startServer } from '../server'; 9 | 10 | import { xhr, XHRResponse, configure as configureHttpRequests, getErrorStatusDescription } from 'request-light'; 11 | import { URI as Uri } from 'vscode-uri'; 12 | import * as fs from 'fs'; 13 | 14 | // Create a connection for the server. 15 | const connection: Connection = createConnection(); 16 | 17 | console.log = connection.console.log.bind(connection.console); 18 | console.error = connection.console.error.bind(connection.console); 19 | 20 | process.on('unhandledRejection', (e: any) => { 21 | connection.console.error(formatError(`Unhandled exception`, e)); 22 | }); 23 | 24 | function getHTTPRequestService(): RequestService { 25 | return { 26 | getContent(uri: string, _encoding?: string) { 27 | const headers = { 'Accept-Encoding': 'gzip, deflate' }; 28 | return xhr({ url: uri, followRedirects: 5, headers }).then(response => { 29 | return response.responseText; 30 | }, (error: XHRResponse) => { 31 | return Promise.reject(error.responseText || getErrorStatusDescription(error.status) || error.toString()); 32 | }); 33 | } 34 | }; 35 | } 36 | 37 | function getFileRequestService(): RequestService { 38 | return { 39 | getContent(location: string, encoding?: BufferEncoding) { 40 | return new Promise((c, e) => { 41 | const uri = Uri.parse(location); 42 | fs.readFile(uri.fsPath, encoding, (err, buf) => { 43 | if (err) { 44 | return e(err); 45 | } 46 | c(buf.toString()); 47 | }); 48 | }); 49 | } 50 | }; 51 | } 52 | 53 | const runtime: RuntimeEnvironment = { 54 | timer: { 55 | setImmediate(callback: (...args: any[]) => void, ...args: any[]): Disposable { 56 | const handle = setImmediate(callback, ...args); 57 | return { dispose: () => clearImmediate(handle) }; 58 | }, 59 | setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable { 60 | const handle = setTimeout(callback, ms, ...args); 61 | return { dispose: () => clearTimeout(handle) }; 62 | } 63 | }, 64 | file: getFileRequestService(), 65 | http: getHTTPRequestService(), 66 | configureHttpRequests 67 | }; 68 | 69 | 70 | 71 | startServer(connection, runtime); -------------------------------------------------------------------------------- /src/server/node/serverNodeMain.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import * as l10n from '@vscode/l10n'; 6 | 7 | async function setupMain() { 8 | const l10nLog: string[] = []; 9 | 10 | const i10lLocation = process.env['VSCODE_L10N_BUNDLE_LOCATION']; 11 | if (i10lLocation) { 12 | try { 13 | await l10n.config({ uri: i10lLocation }); 14 | l10nLog.push(`l10n: Configured to ${i10lLocation.toString()}`); 15 | } catch (e) { 16 | l10nLog.push(`l10n: Problems loading ${i10lLocation.toString()} : ${e}`); 17 | } 18 | } 19 | await import('./serverMain.js'); 20 | l10nLog.forEach(console.log); 21 | } 22 | setupMain(); -------------------------------------------------------------------------------- /src/server/server.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { 7 | Connection, 8 | TextDocuments, InitializeParams, InitializeResult, NotificationType, RequestType, 9 | DocumentRangeFormattingRequest, Disposable, ServerCapabilities, TextDocumentSyncKind, TextEdit, DocumentFormattingRequest, TextDocumentIdentifier, Diagnostic, CodeAction, CodeActionKind 10 | } from 'vscode-languageserver'; 11 | 12 | import { runSafe, runSafeAsync } from './utils/runner'; 13 | import { DiagnosticsSupport, registerDiagnosticsPullSupport, registerDiagnosticsPushSupport } from './utils/validation'; 14 | import { TextDocument, JSONDocument, JSONSchema, getLanguageService, DocumentLanguageSettings, SchemaConfiguration, ClientCapabilities, Range, Position, SortOptions, FormattingOptions } from '@blueglassblock/json5-languageservice'; 15 | import { getLanguageModelCache } from './languageModelCache'; 16 | import { Utils, URI } from 'vscode-uri'; 17 | import * as l10n from '@vscode/l10n'; 18 | 19 | type ISchemaAssociations = Record; 20 | 21 | type JSONLanguageStatus = { schemas: string[] }; 22 | 23 | namespace SchemaAssociationNotification { 24 | export const type: NotificationType = new NotificationType('json5/schemaAssociations'); 25 | } 26 | 27 | namespace VSCodeContentRequest { 28 | export const type: RequestType = new RequestType('vscode/content'); 29 | } 30 | 31 | namespace SchemaContentChangeNotification { 32 | export const type: NotificationType = new NotificationType('json5/schemaContent'); 33 | } 34 | 35 | namespace ForceValidateRequest { 36 | export const type: RequestType = new RequestType('json5/validate'); 37 | } 38 | 39 | namespace LanguageStatusRequest { 40 | export const type: RequestType = new RequestType('json5/languageStatus'); 41 | } 42 | 43 | export interface DocumentSortingParams { 44 | /** 45 | * The uri of the document to sort. 46 | */ 47 | uri: string; 48 | /** 49 | * The sort options 50 | */ 51 | options: SortOptions; 52 | } 53 | 54 | namespace DocumentSortingRequest { 55 | export const type: RequestType = new RequestType('json5/sort'); 56 | } 57 | 58 | const workspaceContext = { 59 | resolveRelativePath: (relativePath: string, resource: string) => { 60 | const base = resource.substring(0, resource.lastIndexOf('/') + 1); 61 | return Utils.resolvePath(URI.parse(base), relativePath).toString(); 62 | } 63 | }; 64 | 65 | export interface RequestService { 66 | getContent(uri: string): Promise; 67 | } 68 | 69 | export interface RuntimeEnvironment { 70 | file?: RequestService; 71 | http?: RequestService; 72 | configureHttpRequests?(proxy: string | undefined, strictSSL: boolean): void; 73 | readonly timer: { 74 | setImmediate(callback: (...args: any[]) => void, ...args: any[]): Disposable; 75 | setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable; 76 | }; 77 | } 78 | 79 | export function startServer(connection: Connection, runtime: RuntimeEnvironment) { 80 | 81 | function getSchemaRequestService(handledSchemas: string[] = ['https', 'http', 'file']) { 82 | const builtInHandlers: { [protocol: string]: RequestService | undefined } = {}; 83 | for (const protocol of handledSchemas) { 84 | if (protocol === 'file') { 85 | builtInHandlers[protocol] = runtime.file; 86 | } else if (protocol === 'http' || protocol === 'https') { 87 | builtInHandlers[protocol] = runtime.http; 88 | } 89 | } 90 | return (uri: string): Thenable => { 91 | const protocol = uri.substr(0, uri.indexOf(':')); 92 | 93 | const builtInHandler = builtInHandlers[protocol]; 94 | if (builtInHandler) { 95 | return builtInHandler.getContent(uri); 96 | } 97 | return connection.sendRequest(VSCodeContentRequest.type, uri).then(responseText => { 98 | return responseText; 99 | }, error => { 100 | return Promise.reject(error.message); 101 | }); 102 | }; 103 | } 104 | 105 | // create the JSON language service 106 | let languageService = getLanguageService({ 107 | workspaceContext, 108 | contributions: [], 109 | clientCapabilities: ClientCapabilities.LATEST 110 | }); 111 | 112 | // Create a text document manager. 113 | const documents = new TextDocuments(TextDocument); 114 | 115 | // Make the text document manager listen on the connection 116 | // for open, change and close text document events 117 | documents.listen(connection); 118 | 119 | let clientSnippetSupport = false; 120 | let dynamicFormatterRegistration = false; 121 | let hierarchicalDocumentSymbolSupport = false; 122 | 123 | let foldingRangeLimitDefault = Number.MAX_VALUE; 124 | let resultLimit = Number.MAX_VALUE; 125 | let foldingRangeLimit = Number.MAX_VALUE; 126 | let colorDecoratorLimit = Number.MAX_VALUE; 127 | 128 | let formatterMaxNumberOfEdits = Number.MAX_VALUE; 129 | let diagnosticsSupport: DiagnosticsSupport | undefined; 130 | 131 | 132 | // After the server has started the client sends an initialize request. The server receives 133 | // in the passed params the rootPath of the workspace plus the client capabilities. 134 | connection.onInitialize((params: InitializeParams): InitializeResult => { 135 | 136 | const initializationOptions = params.initializationOptions as any || {}; 137 | 138 | const handledProtocols = initializationOptions?.handledSchemaProtocols; 139 | 140 | languageService = getLanguageService({ 141 | schemaRequestService: getSchemaRequestService(handledProtocols), 142 | workspaceContext, 143 | contributions: [], 144 | clientCapabilities: params.capabilities 145 | }); 146 | 147 | function getClientCapability(name: string, def: T) { 148 | const keys = name.split('.'); 149 | let c: any = params.capabilities; 150 | for (let i = 0; c && i < keys.length; i++) { 151 | if (!c.hasOwnProperty(keys[i])) { 152 | return def; 153 | } 154 | c = c[keys[i]]; 155 | } 156 | return c; 157 | } 158 | 159 | clientSnippetSupport = getClientCapability('textDocument.completion.completionItem.snippetSupport', false); 160 | dynamicFormatterRegistration = getClientCapability('textDocument.rangeFormatting.dynamicRegistration', false) && (typeof initializationOptions.provideFormatter !== 'boolean'); 161 | foldingRangeLimitDefault = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE); 162 | hierarchicalDocumentSymbolSupport = getClientCapability('textDocument.documentSymbol.hierarchicalDocumentSymbolSupport', false); 163 | formatterMaxNumberOfEdits = initializationOptions.customCapabilities?.rangeFormatting?.editLimit || Number.MAX_VALUE; 164 | 165 | const supportsDiagnosticPull = getClientCapability('textDocument.diagnostic', undefined); 166 | if (supportsDiagnosticPull === undefined) { 167 | diagnosticsSupport = registerDiagnosticsPushSupport(documents, connection, runtime, validateTextDocument); 168 | } else { 169 | diagnosticsSupport = registerDiagnosticsPullSupport(documents, connection, runtime, validateTextDocument); 170 | } 171 | 172 | const capabilities: ServerCapabilities = { 173 | textDocumentSync: TextDocumentSyncKind.Incremental, 174 | completionProvider: clientSnippetSupport ? { 175 | resolveProvider: false, // turn off resolving as the current language service doesn't do anything on resolve. Also fixes #91747 176 | triggerCharacters: ['"', "'", ':'] 177 | } : undefined, 178 | hoverProvider: true, 179 | documentSymbolProvider: true, 180 | documentRangeFormattingProvider: initializationOptions.provideFormatter === true, 181 | documentFormattingProvider: initializationOptions.provideFormatter === true, 182 | colorProvider: {}, 183 | foldingRangeProvider: true, 184 | selectionRangeProvider: true, 185 | documentLinkProvider: {}, 186 | diagnosticProvider: { 187 | documentSelector: null, 188 | interFileDependencies: false, 189 | workspaceDiagnostics: false 190 | }, 191 | codeActionProvider: true 192 | }; 193 | 194 | return { capabilities }; 195 | }); 196 | 197 | 198 | 199 | // The settings interface describes the server relevant settings part 200 | interface Settings { 201 | json5?: { 202 | schemas?: JSONSchemaSettings[]; 203 | format?: { 204 | enable?: boolean, 205 | trailingCommas?: 'keep' | 'none' | 'all', 206 | keyQuotes?: 'keep' | 'single' | 'double' | 'none-single' | 'none-double', 207 | stringQuotes?: 'keep' | 'single' | 'double', 208 | }; 209 | keepLines?: { enable?: boolean }; 210 | validate?: { enable?: boolean }; 211 | resultLimit?: number; 212 | foldingLimit?: number; 213 | colorDecoratorLimit?: number; 214 | }; 215 | http?: { 216 | proxy?: string; 217 | proxyStrictSSL?: boolean; 218 | }; 219 | } 220 | 221 | interface JSONSchemaSettings { 222 | fileMatch?: string[]; 223 | url?: string; 224 | schema?: JSONSchema; 225 | folderUri?: string; 226 | } 227 | 228 | 229 | 230 | let jsonConfigurationSettings: JSONSchemaSettings[] | undefined = undefined; 231 | let schemaAssociations: ISchemaAssociations | SchemaConfiguration[] | undefined = undefined; 232 | let formatterRegistrations: Thenable[] | null = null; 233 | let validateEnabled = true; 234 | let keepLinesEnabled = false; 235 | let trailingCommasOption: undefined | 'none' | 'all' = undefined; 236 | let keyQuotesOption: undefined | 'single' | 'double' | 'none-single' | 'none-double' = undefined; 237 | let stringQuotesOption: undefined | 'single' | 'double' = undefined; 238 | 239 | // The settings have changed. Is sent on server activation as well. 240 | connection.onDidChangeConfiguration((change) => { 241 | const settings = change.settings; 242 | runtime.configureHttpRequests?.(settings?.http?.proxy, !!settings.http?.proxyStrictSSL); 243 | jsonConfigurationSettings = settings.json5?.schemas; 244 | validateEnabled = !!settings.json5?.validate?.enable; 245 | keepLinesEnabled = settings.json5?.keepLines?.enable || false; 246 | trailingCommasOption = settings.json5?.format?.trailingCommas === 'keep' ? undefined : settings.json5?.format?.trailingCommas; 247 | keyQuotesOption = settings.json5?.format?.keyQuotes === 'keep' ? undefined : settings.json5?.format?.keyQuotes; 248 | stringQuotesOption = settings.json5?.format?.stringQuotes === 'keep' ? undefined : settings.json5?.format?.stringQuotes; 249 | updateConfiguration(); 250 | 251 | const sanitizeLimitSetting = (settingValue: any) => Math.trunc(Math.max(settingValue, 0)); 252 | resultLimit = sanitizeLimitSetting(settings.json5?.resultLimit || Number.MAX_VALUE); 253 | foldingRangeLimit = sanitizeLimitSetting(settings.json5?.foldingLimit || foldingRangeLimitDefault); 254 | colorDecoratorLimit = sanitizeLimitSetting(settings.json5?.colorDecoratorLimit || Number.MAX_VALUE); 255 | 256 | // dynamically enable & disable the formatter 257 | if (dynamicFormatterRegistration) { 258 | const enableFormatter = settings.json5?.format?.enable; 259 | if (enableFormatter) { 260 | if (!formatterRegistrations) { 261 | const documentSelector = [{ language: 'json5' }]; 262 | formatterRegistrations = [ 263 | connection.client.register(DocumentRangeFormattingRequest.type, { documentSelector }), 264 | connection.client.register(DocumentFormattingRequest.type, { documentSelector }) 265 | ]; 266 | } 267 | } else if (formatterRegistrations) { 268 | formatterRegistrations.forEach(p => p.then(r => r.dispose())); 269 | formatterRegistrations = null; 270 | } 271 | } 272 | }); 273 | 274 | // The jsonValidation extension configuration has changed 275 | connection.onNotification(SchemaAssociationNotification.type, associations => { 276 | schemaAssociations = associations; 277 | updateConfiguration(); 278 | }); 279 | 280 | // A schema has changed 281 | connection.onNotification(SchemaContentChangeNotification.type, uriOrUris => { 282 | let needsRevalidation = false; 283 | if (Array.isArray(uriOrUris)) { 284 | for (const uri of uriOrUris) { 285 | if (languageService.resetSchema(uri)) { 286 | needsRevalidation = true; 287 | } 288 | } 289 | } else { 290 | needsRevalidation = languageService.resetSchema(uriOrUris); 291 | } 292 | if (needsRevalidation) { 293 | diagnosticsSupport?.requestRefresh(); 294 | } 295 | }); 296 | 297 | // Retry schema validation on all open documents 298 | connection.onRequest(ForceValidateRequest.type, async uri => { 299 | const document = documents.get(uri); 300 | if (document) { 301 | updateConfiguration(); 302 | return await validateTextDocument(document); 303 | } 304 | return []; 305 | }); 306 | 307 | connection.onRequest(LanguageStatusRequest.type, async uri => { 308 | const document = documents.get(uri); 309 | if (document) { 310 | const jsonDocument = getJSONDocument(document); 311 | return languageService.getLanguageStatus(document, jsonDocument); 312 | } else { 313 | return { schemas: [] }; 314 | } 315 | }); 316 | 317 | connection.onRequest(DocumentSortingRequest.type, async params => { 318 | const uri = params.uri; 319 | const options = params.options; 320 | const document = documents.get(uri); 321 | if (document) { 322 | return languageService.sort(document, options); 323 | } 324 | return []; 325 | }); 326 | 327 | function updateConfiguration() { 328 | const languageSettings = { 329 | validate: validateEnabled, 330 | schemas: new Array(), 331 | keyQuotes: keyQuotesOption, 332 | stringQuotes: stringQuotesOption, 333 | }; 334 | if (schemaAssociations) { 335 | if (Array.isArray(schemaAssociations)) { 336 | Array.prototype.push.apply(languageSettings.schemas, schemaAssociations); 337 | } else { 338 | for (const pattern in schemaAssociations) { 339 | const association = schemaAssociations[pattern]; 340 | if (Array.isArray(association)) { 341 | association.forEach(uri => { 342 | languageSettings.schemas.push({ uri, fileMatch: [pattern] }); 343 | }); 344 | } 345 | } 346 | } 347 | } 348 | if (jsonConfigurationSettings) { 349 | jsonConfigurationSettings.forEach((schema, index) => { 350 | let uri = schema.url; 351 | if (!uri && schema.schema) { 352 | uri = schema.schema.id || `vscode://schemas/custom/${index}`; 353 | } 354 | if (uri) { 355 | languageSettings.schemas.push({ uri, fileMatch: schema.fileMatch, schema: schema.schema, folderUri: schema.folderUri }); 356 | } 357 | }); 358 | } 359 | languageService.configure(languageSettings); 360 | 361 | diagnosticsSupport?.requestRefresh(); 362 | } 363 | 364 | async function validateTextDocument(textDocument: TextDocument): Promise { 365 | if (textDocument.getText().length === 0) { 366 | return []; // ignore empty documents 367 | } 368 | const jsonDocument = getJSONDocument(textDocument); 369 | const documentSettings: DocumentLanguageSettings = {}; 370 | return await languageService.doValidation(textDocument, jsonDocument, documentSettings); 371 | } 372 | 373 | connection.onDidChangeWatchedFiles((change) => { 374 | // Monitored files have changed in VSCode 375 | let hasChanges = false; 376 | change.changes.forEach(c => { 377 | if (languageService.resetSchema(c.uri)) { 378 | hasChanges = true; 379 | } 380 | }); 381 | if (hasChanges) { 382 | diagnosticsSupport?.requestRefresh(); 383 | } 384 | }); 385 | 386 | const jsonDocuments = getLanguageModelCache(10, 60, document => languageService.parseJSONDocument(document)); 387 | documents.onDidClose(e => { 388 | jsonDocuments.onDocumentRemoved(e.document); 389 | }); 390 | connection.onShutdown(() => { 391 | jsonDocuments.dispose(); 392 | }); 393 | 394 | function getJSONDocument(document: TextDocument): JSONDocument { 395 | return jsonDocuments.get(document); 396 | } 397 | 398 | connection.onCompletion((textDocumentPosition, token) => { 399 | return runSafeAsync(runtime, async () => { 400 | const document = documents.get(textDocumentPosition.textDocument.uri); 401 | if (document) { 402 | const jsonDocument = getJSONDocument(document); 403 | return languageService.doComplete(document, textDocumentPosition.position, jsonDocument); 404 | } 405 | return null; 406 | }, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`, token); 407 | }); 408 | 409 | connection.onHover((textDocumentPositionParams, token) => { 410 | return runSafeAsync(runtime, async () => { 411 | const document = documents.get(textDocumentPositionParams.textDocument.uri); 412 | if (document) { 413 | const jsonDocument = getJSONDocument(document); 414 | return languageService.doHover(document, textDocumentPositionParams.position, jsonDocument); 415 | } 416 | return null; 417 | }, null, `Error while computing hover for ${textDocumentPositionParams.textDocument.uri}`, token); 418 | }); 419 | 420 | connection.onDocumentSymbol((documentSymbolParams, token) => { 421 | return runSafe(runtime, () => { 422 | const document = documents.get(documentSymbolParams.textDocument.uri); 423 | if (document) { 424 | const jsonDocument = getJSONDocument(document); 425 | if (hierarchicalDocumentSymbolSupport) { 426 | return languageService.findDocumentSymbols2(document, jsonDocument, { resultLimit }); 427 | } else { 428 | return languageService.findDocumentSymbols(document, jsonDocument, { resultLimit }); 429 | } 430 | } 431 | return []; 432 | }, [], `Error while computing document symbols for ${documentSymbolParams.textDocument.uri}`, token); 433 | }); 434 | 435 | connection.onCodeAction((codeActionParams, token) => { 436 | return runSafeAsync(runtime, async () => { 437 | const document = documents.get(codeActionParams.textDocument.uri); 438 | if (document) { 439 | const sortCodeAction = CodeAction.create('Sort JSON5', CodeActionKind.Source.concat('.sort', '.json5')); 440 | sortCodeAction.command = { 441 | command: 'json5.sort', 442 | title: l10n.t('Sort JSON5') 443 | }; 444 | return [sortCodeAction]; 445 | } 446 | return []; 447 | }, [], `Error while computing code actions for ${codeActionParams.textDocument.uri}`, token); 448 | }); 449 | 450 | function onFormat(textDocument: TextDocumentIdentifier, range: Range | undefined, options: FormattingOptions): TextEdit[] { 451 | 452 | options.keepLines = keepLinesEnabled; 453 | options.trailingCommas = trailingCommasOption; 454 | options.keyQuotes = keyQuotesOption; 455 | options.stringQuotes = stringQuotesOption; 456 | const document = documents.get(textDocument.uri); 457 | if (document) { 458 | const edits = languageService.format(document, range ?? getFullRange(document), options); 459 | if (edits.length > formatterMaxNumberOfEdits) { 460 | const newText = TextDocument.applyEdits(document, edits); 461 | return [TextEdit.replace(getFullRange(document), newText)]; 462 | } 463 | return edits; 464 | } 465 | return []; 466 | } 467 | 468 | connection.onDocumentRangeFormatting((formatParams, token) => { 469 | return runSafe(runtime, () => onFormat(formatParams.textDocument, formatParams.range, formatParams.options), [], `Error while formatting range for ${formatParams.textDocument.uri}`, token); 470 | }); 471 | 472 | connection.onDocumentFormatting((formatParams, token) => { 473 | return runSafe(runtime, () => onFormat(formatParams.textDocument, undefined, formatParams.options), [], `Error while formatting ${formatParams.textDocument.uri}`, token); 474 | }); 475 | 476 | connection.onDocumentColor((params, token) => { 477 | return runSafeAsync(runtime, async () => { 478 | const document = documents.get(params.textDocument.uri); 479 | if (document) { 480 | 481 | const jsonDocument = getJSONDocument(document); 482 | const resultLimit = colorDecoratorLimit; 483 | return languageService.findDocumentColors(document, jsonDocument, { resultLimit }); 484 | } 485 | return []; 486 | }, [], `Error while computing document colors for ${params.textDocument.uri}`, token); 487 | }); 488 | 489 | connection.onColorPresentation((params, token) => { 490 | return runSafe(runtime, () => { 491 | const document = documents.get(params.textDocument.uri); 492 | if (document) { 493 | const jsonDocument = getJSONDocument(document); 494 | return languageService.getColorPresentations(document, jsonDocument, params.color, params.range); 495 | } 496 | return []; 497 | }, [], `Error while computing color presentations for ${params.textDocument.uri}`, token); 498 | }); 499 | 500 | connection.onFoldingRanges((params, token) => { 501 | return runSafe(runtime, () => { 502 | const document = documents.get(params.textDocument.uri); 503 | if (document) { 504 | const rangeLimit = foldingRangeLimit; 505 | return languageService.getFoldingRanges(document, { rangeLimit }); 506 | } 507 | return null; 508 | }, null, `Error while computing folding ranges for ${params.textDocument.uri}`, token); 509 | }); 510 | 511 | 512 | connection.onSelectionRanges((params, token) => { 513 | return runSafe(runtime, () => { 514 | const document = documents.get(params.textDocument.uri); 515 | if (document) { 516 | const jsonDocument = getJSONDocument(document); 517 | return languageService.getSelectionRanges(document, params.positions, jsonDocument); 518 | } 519 | return []; 520 | }, [], `Error while computing selection ranges for ${params.textDocument.uri}`, token); 521 | }); 522 | 523 | connection.onDocumentLinks((params, token) => { 524 | return runSafeAsync(runtime, async () => { 525 | const document = documents.get(params.textDocument.uri); 526 | if (document) { 527 | const jsonDocument = getJSONDocument(document); 528 | return languageService.findLinks(document, jsonDocument); 529 | } 530 | return []; 531 | }, [], `Error while computing links for ${params.textDocument.uri}`, token); 532 | }); 533 | 534 | // Listen on the connection 535 | connection.listen(); 536 | } 537 | 538 | function getFullRange(document: TextDocument): Range { 539 | return Range.create(Position.create(0, 0), document.positionAt(document.getText().length)); 540 | } -------------------------------------------------------------------------------- /src/server/utils/runner.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { CancellationToken, ResponseError, LSPErrorCodes } from 'vscode-languageserver'; 7 | import { RuntimeEnvironment } from '../server'; 8 | 9 | export function formatError(message: string, err: any): string { 10 | if (err instanceof Error) { 11 | const error = err; 12 | return `${message}: ${error.message}\n${error.stack}`; 13 | } else if (typeof err === 'string') { 14 | return `${message}: ${err}`; 15 | } else if (err) { 16 | return `${message}: ${err.toString()}`; 17 | } 18 | return message; 19 | } 20 | 21 | export function runSafeAsync(runtime: RuntimeEnvironment, func: () => Thenable, errorVal: T, errorMessage: string, token: CancellationToken): Thenable> { 22 | return new Promise>((resolve) => { 23 | runtime.timer.setImmediate(() => { 24 | if (token.isCancellationRequested) { 25 | resolve(cancelValue()); 26 | return; 27 | } 28 | return func().then(result => { 29 | if (token.isCancellationRequested) { 30 | resolve(cancelValue()); 31 | return; 32 | } else { 33 | resolve(result); 34 | } 35 | }, e => { 36 | console.error(formatError(errorMessage, e)); 37 | resolve(errorVal); 38 | }); 39 | }); 40 | }); 41 | } 42 | 43 | export function runSafe(runtime: RuntimeEnvironment, func: () => T, errorVal: T, errorMessage: string, token: CancellationToken): Thenable> { 44 | return new Promise>((resolve) => { 45 | runtime.timer.setImmediate(() => { 46 | if (token.isCancellationRequested) { 47 | resolve(cancelValue()); 48 | } else { 49 | try { 50 | const result = func(); 51 | if (token.isCancellationRequested) { 52 | resolve(cancelValue()); 53 | return; 54 | } else { 55 | resolve(result); 56 | } 57 | 58 | } catch (e) { 59 | console.error(formatError(errorMessage, e)); 60 | resolve(errorVal); 61 | } 62 | } 63 | }); 64 | }); 65 | } 66 | 67 | function cancelValue() { 68 | console.log('cancelled'); 69 | return new ResponseError(LSPErrorCodes.RequestCancelled, 'Request cancelled'); 70 | } -------------------------------------------------------------------------------- /src/server/utils/strings.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | /** 7 | * Determines if haystack ends with needle. 8 | */ 9 | export function endsWith(haystack: string, needle: string): boolean { 10 | const diff = haystack.length - needle.length; 11 | if (diff > 0) { 12 | return haystack.lastIndexOf(needle) === diff; 13 | } else if (diff === 0) { 14 | return haystack === needle; 15 | } else { 16 | return false; 17 | } 18 | } 19 | 20 | export function convertSimple2RegExpPattern(pattern: string): string { 21 | return pattern.replace(/[\-\\\{\}\+\?\|\^\$\.\,\[\]\(\)\#\s]/g, '\\$&').replace(/[\*]/g, '.*'); 22 | } -------------------------------------------------------------------------------- /src/server/utils/validation.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { CancellationToken, Connection, Diagnostic, Disposable, DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentDiagnosticReportKind, TextDocuments } from 'vscode-languageserver'; 7 | import { TextDocument } from '@blueglassblock/json5-languageservice'; 8 | import { formatError, runSafeAsync } from './runner'; 9 | import { RuntimeEnvironment } from '../server'; 10 | 11 | export type Validator = (textDocument: TextDocument) => Promise; 12 | export type DiagnosticsSupport = { 13 | dispose(): void; 14 | requestRefresh(): void; 15 | }; 16 | 17 | export function registerDiagnosticsPushSupport(documents: TextDocuments, connection: Connection, runtime: RuntimeEnvironment, validate: Validator): DiagnosticsSupport { 18 | 19 | const pendingValidationRequests: { [uri: string]: Disposable } = {}; 20 | const validationDelayMs = 500; 21 | 22 | const disposables: Disposable[] = []; 23 | 24 | // The content of a text document has changed. This event is emitted 25 | // when the text document first opened or when its content has changed. 26 | documents.onDidChangeContent(change => { 27 | triggerValidation(change.document); 28 | }, undefined, disposables); 29 | 30 | // a document has closed: clear all diagnostics 31 | documents.onDidClose(event => { 32 | cleanPendingValidation(event.document); 33 | connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); 34 | }, undefined, disposables); 35 | 36 | function cleanPendingValidation(textDocument: TextDocument): void { 37 | const request = pendingValidationRequests[textDocument.uri]; 38 | if (request) { 39 | request.dispose(); 40 | delete pendingValidationRequests[textDocument.uri]; 41 | } 42 | } 43 | 44 | function triggerValidation(textDocument: TextDocument): void { 45 | cleanPendingValidation(textDocument); 46 | const request = pendingValidationRequests[textDocument.uri] = runtime.timer.setTimeout(async () => { 47 | if (request === pendingValidationRequests[textDocument.uri]) { 48 | try { 49 | const diagnostics = await validate(textDocument); 50 | if (request === pendingValidationRequests[textDocument.uri]) { 51 | connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); 52 | } 53 | delete pendingValidationRequests[textDocument.uri]; 54 | } catch (e) { 55 | connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e)); 56 | } 57 | } 58 | }, validationDelayMs); 59 | } 60 | 61 | return { 62 | requestRefresh: () => { 63 | documents.all().forEach(triggerValidation); 64 | }, 65 | dispose: () => { 66 | disposables.forEach(d => d.dispose()); 67 | disposables.length = 0; 68 | const keys = Object.keys(pendingValidationRequests); 69 | for (const key of keys) { 70 | pendingValidationRequests[key].dispose(); 71 | delete pendingValidationRequests[key]; 72 | } 73 | } 74 | }; 75 | } 76 | 77 | export function registerDiagnosticsPullSupport(documents: TextDocuments, connection: Connection, runtime: RuntimeEnvironment, validate: Validator): DiagnosticsSupport { 78 | 79 | function newDocumentDiagnosticReport(diagnostics: Diagnostic[]): DocumentDiagnosticReport { 80 | return { 81 | kind: DocumentDiagnosticReportKind.Full, 82 | items: diagnostics 83 | }; 84 | } 85 | 86 | const registration = connection.languages.diagnostics.on(async (params: DocumentDiagnosticParams, token: CancellationToken) => { 87 | return runSafeAsync(runtime, async () => { 88 | const document = documents.get(params.textDocument.uri); 89 | if (document) { 90 | return newDocumentDiagnosticReport(await validate(document)); 91 | } 92 | return newDocumentDiagnosticReport([]); 93 | 94 | }, newDocumentDiagnosticReport([]), `Error while computing diagnostics for ${params.textDocument.uri}`, token); 95 | }); 96 | 97 | function requestRefresh(): void { 98 | connection.languages.diagnostics.refresh(); 99 | } 100 | 101 | return { 102 | requestRefresh, 103 | dispose: () => { 104 | registration.dispose(); 105 | } 106 | }; 107 | 108 | } -------------------------------------------------------------------------------- /syntaxes/json5.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "scopeName": "source.json5", 3 | "fileTypes": [ 4 | "json5" 5 | ], 6 | "name": "JSON5", 7 | "patterns": [ 8 | { 9 | "include": "#comments" 10 | }, 11 | { 12 | "include": "#value" 13 | } 14 | ], 15 | "repository": { 16 | "array": { 17 | "begin": "\\[", 18 | "beginCaptures": { 19 | "0": { 20 | "name": "punctuation.definition.array.begin.json5 punctuation.definition.array.begin.json.five" 21 | } 22 | }, 23 | "end": "\\]", 24 | "endCaptures": { 25 | "0": { 26 | "name": "punctuation.definition.array.end.json5 punctuation.definition.array.end.json.five" 27 | } 28 | }, 29 | "name": "meta.structure.array.json5 meta.structure.array.json.five", 30 | "patterns": [ 31 | { 32 | "include": "#comments" 33 | }, 34 | { 35 | "include": "#value" 36 | }, 37 | { 38 | "match": ",", 39 | "name": "punctuation.separator.array.json5 punctuation.separator.array.json.five" 40 | }, 41 | { 42 | "match": "[^\\s\\]]", 43 | "name": "invalid.illegal.expected-array-separator.json5 invalid.illegal.expected-array-separator.json.five" 44 | } 45 | ] 46 | }, 47 | "constant": { 48 | "match": "\\b(?:true|false|null|Infinity|NaN)\\b", 49 | "name": "constant.language.json5 constant.language.json.five" 50 | }, 51 | "infinity": { 52 | "match": "(-)*\\b(?:Infinity|NaN)\\b", 53 | "name": "constant.language.json5 constant.language.json.five" 54 | }, 55 | "number": { 56 | "patterns": [ 57 | { 58 | "comment": "handles hexadecimal numbers", 59 | "match": "(0x)[0-9a-fA-f]*", 60 | "name": "constant.numeric.hex.json5 constant.numeric.hex.json.five" 61 | }, 62 | { 63 | "comment": "handles integer and decimal numbers", 64 | "match": "[+-.]?(?=[1-9]|0(?!\\d))\\d+(\\.\\d*)?([eE][+-]?\\d+)?", 65 | "name": "constant.numeric.dec.json5 constant.numeric.dec.json.five" 66 | } 67 | ] 68 | }, 69 | "object": { 70 | "begin": "\\{", 71 | "beginCaptures": { 72 | "0": { 73 | "name": "punctuation.definition.dictionary.begin.json5 punctuation.definition.dictionary.begin.json.five" 74 | } 75 | }, 76 | "comment": "a json5 object", 77 | "end": "\\}", 78 | "endCaptures": { 79 | "0": { 80 | "name": "punctuation.definition.dictionary.end.json5 punctuation.definition.dictionary.end.json.five" 81 | } 82 | }, 83 | "name": "meta.structure.dictionary.json5 meta.structure.dictionary.json.five", 84 | "patterns": [ 85 | { 86 | "include": "#comments" 87 | }, 88 | { 89 | "comment": "the json5 object key", 90 | "include": "#key" 91 | }, 92 | { 93 | "begin": ":", 94 | "beginCaptures": { 95 | "0": { 96 | "name": "punctuation.separator.dictionary.key-value.json5 punctuation.separator.dictionary.key-value.json.five" 97 | } 98 | }, 99 | "end": "(,)|(?=\\})", 100 | "endCaptures": { 101 | "1": { 102 | "name": "punctuation.separator.dictionary.pair.json5 punctuation.separator.dictionary.pair.json.five" 103 | } 104 | }, 105 | "name": "meta.structure.dictionary.value.json5 meta.structure.dictionary.value.json.five", 106 | "patterns": [ 107 | { 108 | "comment": "the json5 object value", 109 | "include": "#value" 110 | }, 111 | { 112 | "match": "[^\\s,]", 113 | "name": "invalid.illegal.expected-dictionary-separator.json5 invalid.illegal.expected-dictionary-separator.json.five" 114 | } 115 | ] 116 | }, 117 | { 118 | "match": "[^\\s\\}]", 119 | "name": "invalid.illegal.expected-dictionary-separator.json5 invalid.illegal.expected-dictionary-separator.json.five" 120 | } 121 | ] 122 | }, 123 | "stringSingle": { 124 | "begin": "[']", 125 | "beginCaptures": { 126 | "0": { 127 | "name": "punctuation.definition.string.begin.json5 punctuation.definition.string.begin.json.five" 128 | } 129 | }, 130 | "end": "[']", 131 | "endCaptures": { 132 | "0": { 133 | "name": "punctuation.definition.string.end.json5 punctuation.definition.string.end.json.five" 134 | } 135 | }, 136 | "name": "string.quoted.single.json5 string.quoted.single.json.five", 137 | "patterns": [ 138 | { 139 | "match": "(?x: # turn on extended mode\n \\\\ # a literal backslash\n (?: # ...followed by...\n [\"\\\\/bfnrt] # one of these characters\n | # ...or...\n u # a u\n [0-9a-fA-F]{4} # and four hex digits\n )\n )", 140 | "name": "constant.character.escape.json5 constant.character.escape.json.five" 141 | }, 142 | { 143 | "match": "\\\\.", 144 | "name": "invalid.illegal.unrecognized-string-escape.json5 invalid.illegal.unrecognized-string-escape.json.five" 145 | } 146 | ] 147 | }, 148 | "stringDouble": { 149 | "begin": "[\"]", 150 | "beginCaptures": { 151 | "0": { 152 | "name": "punctuation.definition.string.begin.json5 punctuation.definition.string.begin.json.five" 153 | } 154 | }, 155 | "end": "[\"]", 156 | "endCaptures": { 157 | "0": { 158 | "name": "punctuation.definition.string.end.json5 punctuation.definition.string.end.json.five" 159 | } 160 | }, 161 | "name": "string.quoted.double.json5 string.quoted.double.json.five", 162 | "patterns": [ 163 | { 164 | "match": "(?x: # turn on extended mode\n \\\\ # a literal backslash\n (?: # ...followed by...\n [\"\\\\/bfnrt] # one of these characters\n | # ...or...\n u # a u\n [0-9a-fA-F]{4} # and four hex digits\n )\n )", 165 | "name": "constant.character.escape.json5 constant.character.escape.json.five" 166 | }, 167 | { 168 | "match": "\\\\.", 169 | "name": "invalid.illegal.unrecognized-string-escape.json5 invalid.illegal.unrecognized-string-escape.json.five" 170 | } 171 | ] 172 | }, 173 | "key": { 174 | "patterns": [ 175 | { 176 | "begin": "[']", 177 | "beginCaptures": { 178 | "0": { 179 | "name": "punctuation.definition.string.begin.json5 punctuation.definition.string.begin.json.five" 180 | } 181 | }, 182 | "end": "[']", 183 | "endCaptures": { 184 | "0": { 185 | "name": "punctuation.definition.string.end.json5 punctuation.definition.string.end.json.five" 186 | } 187 | }, 188 | "name": "string.quoted.single.json5 string.quoted.single.json.five support.type.property-name.json5 support.type.property-name.json.five", 189 | "patterns": [ 190 | { 191 | "match": "(?x: # turn on extended mode\n \\\\ # a literal backslash\n (?: # ...followed by...\n [\"\\\\/bfnrt] # one of these characters\n | # ...or...\n u # a u\n [0-9a-fA-F]{4} # and four hex digits\n )\n )", 192 | "name": "constant.character.escape.json5 constant.character.escape.json.five" 193 | }, 194 | { 195 | "match": "\\\\.", 196 | "name": "invalid.illegal.unrecognized-string-escape.json5 invalid.illegal.unrecognized-string-escape.json.five" 197 | } 198 | ] 199 | }, 200 | { 201 | "begin": "[\"]", 202 | "beginCaptures": { 203 | "0": { 204 | "name": "punctuation.definition.string.begin.json5 punctuation.definition.string.begin.json.five" 205 | } 206 | }, 207 | "end": "[\"]", 208 | "endCaptures": { 209 | "0": { 210 | "name": "punctuation.definition.string.end.json5 punctuation.definition.string.end.json.five" 211 | } 212 | }, 213 | "name": "string.quoted.double.json5 string.quoted.double.json.five support.type.property-name.json5 support.type.property-name.json.five", 214 | "patterns": [ 215 | { 216 | "match": "(?x: # turn on extended mode\n \\\\ # a literal backslash\n (?: # ...followed by...\n [\"\\\\/bfnrt] # one of these characters\n | # ...or...\n u # a u\n [0-9a-fA-F]{4} # and four hex digits\n )\n )", 217 | "name": "constant.character.escape.json5 constant.character.escape.json.five" 218 | }, 219 | { 220 | "match": "\\\\.", 221 | "name": "invalid.illegal.unrecognized-string-escape.json5 invalid.illegal.unrecognized-string-escape.json.five" 222 | } 223 | ] 224 | }, 225 | { 226 | "match": "([a-zA-Z$_]|[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312E\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEA\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE83\uDE86-\uDE89\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D])([a-zA-Z$_\u200C\u200D0-9]|[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u08D4-\u08E1\u08E3-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u09FC\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0AF9-\u0AFF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C80-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D00-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D54-\u0D57\u0D5F-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19D9\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1CD0-\u1CD2\u1CD4-\u1CF9\u1D00-\u1DF9\u1DFB-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312E\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEA\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C5\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA8FD\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDDFD\uDE80-\uDE9C\uDEA0-\uDED0\uDEE0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF7A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE38-\uDE3A\uDE3F\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE6\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC00-\uDC46\uDC66-\uDC6F\uDC7F-\uDCBA\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD00-\uDD34\uDD36-\uDD3F\uDD50-\uDD73\uDD76\uDD80-\uDDC4\uDDCA-\uDDCC\uDDD0-\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE37\uDE3E\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEEA\uDEF0-\uDEF9\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3C-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC00-\uDC4A\uDC50-\uDC59\uDC80-\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDB5\uDDB8-\uDDC0\uDDD8-\uDDDD\uDE00-\uDE40\uDE44\uDE50-\uDE59\uDE80-\uDEB7\uDEC0-\uDEC9\uDF00-\uDF19\uDF1D-\uDF2B\uDF30-\uDF39]|\uD806[\uDCA0-\uDCE9\uDCFF\uDE00-\uDE3E\uDE47\uDE50-\uDE83\uDE86-\uDE99\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC36\uDC38-\uDC40\uDC50-\uDC59\uDC72-\uDC8F\uDC92-\uDCA7\uDCA9-\uDCB6\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD36\uDD3A\uDD3C\uDD3D\uDD3F-\uDD47\uDD50-\uDD59]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDEF0-\uDEF4\uDF00-\uDF36\uDF40-\uDF43\uDF50-\uDF59\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50-\uDF7E\uDF8F-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A]|\uD83A[\uDC00-\uDCC4\uDCD0-\uDCD6\uDD00-\uDD4A\uDD50-\uDD59]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uDB40[\uDD00-\uDDEF])*", 227 | "name": "string.key.json5 string.key.json.five support.type.property-name.json5 support.type.property-name.json.five" 228 | } 229 | ] 230 | }, 231 | "value": { 232 | "comment": "the 'value' diagram at http://json.org", 233 | "patterns": [ 234 | { 235 | "include": "#constant" 236 | }, 237 | { 238 | "include": "#infinity" 239 | }, 240 | { 241 | "include": "#number" 242 | }, 243 | { 244 | "include": "#stringSingle" 245 | }, 246 | { 247 | "include": "#stringDouble" 248 | }, 249 | { 250 | "include": "#array" 251 | }, 252 | { 253 | "include": "#object" 254 | } 255 | ] 256 | }, 257 | "comments": { 258 | "patterns": [ 259 | { 260 | "match": "/{2}.*", 261 | "name": "comment.single.json5 comment.single.json.five" 262 | }, 263 | { 264 | "begin": "/\\*\\*(?!/)", 265 | "captures": { 266 | "0": { 267 | "name": "punctuation.definition.comment.json5 punctuation.definition.comment.json.five" 268 | } 269 | }, 270 | "end": "\\*/", 271 | "name": "comment.block.documentation.json5 comment.block.documentation.json.five" 272 | }, 273 | { 274 | "begin": "/\\*", 275 | "captures": { 276 | "0": { 277 | "name": "punctuation.definition.comment.json5 punctuation.definition.comment.json.five" 278 | } 279 | }, 280 | "end": "\\*/", 281 | "name": "comment.block.json5 comment.block.json.five" 282 | } 283 | ] 284 | } 285 | } 286 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "Node16", 4 | "target": "ES2020", 5 | "outDir": "dist", 6 | "lib": [ 7 | "ES2020", "WebWorker" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true, /* enable all strict type-checking options */ 12 | "exactOptionalPropertyTypes": false, 13 | "useUnknownInCatchVariables": false, 14 | "alwaysStrict": true, 15 | "noImplicitAny": true, 16 | "noImplicitReturns": true, 17 | "noImplicitOverride": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "forceConsistentCasingInFileNames": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | //@ts-check 7 | 'use strict'; 8 | 9 | //@ts-check 10 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 11 | 12 | const path = require('path'); 13 | const webpack = require('webpack'); 14 | 15 | /** @type WebpackConfig */ 16 | const clientBrowserConfig = { 17 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 18 | target: 'webworker', // extensions run in a webworker context 19 | entry: { 20 | 'extension': './src/client/browser/clientMain.ts', 21 | }, 22 | output: { 23 | filename: 'clientMain.js', 24 | path: path.join(__dirname, './dist/browser'), 25 | libraryTarget: 'commonjs' 26 | }, 27 | resolve: { 28 | mainFields: ['browser', 'module', 'main'], // look for `browser` entry point in imported node modules 29 | extensions: ['.ts', '.js'], // support ts-files and js-files 30 | alias: { 31 | // provides alternate implementation for node module and source files 32 | }, 33 | fallback: { 34 | // Webpack 5 no longer polyfills Node.js core modules automatically. 35 | // see https://webpack.js.org/configuration/resolve/#resolvefallback 36 | // for the list of Node.js core module polyfills. 37 | 'assert': require.resolve('assert') 38 | }, 39 | extensionAlias: { 40 | '.js': ['.js', '.ts'] 41 | } 42 | }, 43 | module: { 44 | rules: [{ 45 | test: /\.ts$/, 46 | exclude: /node_modules/, 47 | use: [{ 48 | loader: 'ts-loader' 49 | }] 50 | }] 51 | }, 52 | plugins: [ 53 | new webpack.optimize.LimitChunkCountPlugin({ 54 | maxChunks: 1 // disable chunks by default since web extensions must be a single bundle 55 | }) 56 | ], 57 | externals: { 58 | 'vscode': 'commonjs vscode', // ignored because it doesn't exist 59 | }, 60 | performance: { 61 | hints: false 62 | }, 63 | devtool: 'nosources-source-map', // create a source map that points to the original source file 64 | infrastructureLogging: { 65 | level: "log", // enables logging required for problem matchers 66 | }, 67 | }; 68 | 69 | /** @type WebpackConfig */ 70 | const clientNodeConfig = { 71 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 72 | target: 'node', 73 | entry: { 74 | 'extension': './src/client/node/clientMain.ts', 75 | }, 76 | output: { 77 | filename: 'clientMain.js', 78 | path: path.join(__dirname, './dist/node'), 79 | libraryTarget: 'commonjs', 80 | devtoolModuleFilenameTemplate: '../../[resource-path]' 81 | }, 82 | resolve: { 83 | conditionNames: ['import', 'require', 'node-addons', 'node'], 84 | mainFields: ['module', 'main'], 85 | extensions: ['.ts', '.js'], // support ts-files and js-files 86 | extensionAlias: { 87 | '.js': ['.js', '.ts'] 88 | } 89 | }, 90 | module: { 91 | rules: [{ 92 | test: /\.ts$/, 93 | exclude: /node_modules/, 94 | use: [{ 95 | loader: 'ts-loader', 96 | options: { 97 | compilerOptions: { 98 | 'sourceMap': true, 99 | }, 100 | onlyCompileBundledFiles: true, 101 | } 102 | }] 103 | }] 104 | }, 105 | plugins: [ 106 | new webpack.optimize.LimitChunkCountPlugin({ 107 | maxChunks: 1 // disable chunks by default since web extensions must be a single bundle 108 | }), 109 | ], 110 | externals: { 111 | 'vscode': 'commonjs vscode', // ignored because it doesn't exist 112 | }, 113 | performance: { 114 | hints: false 115 | }, 116 | devtool: 'nosources-source-map', // create a source map that points to the original source file 117 | infrastructureLogging: { 118 | level: "log", // enables logging required for problem matchers 119 | }, 120 | }; 121 | 122 | /** @type WebpackConfig */ 123 | const serverBrowserConfig = { 124 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 125 | target: 'webworker', // extensions run in a webworker context 126 | entry: { 127 | 'extension': './src/server/browser/serverWorkerMain.ts', 128 | }, 129 | output: { 130 | filename: 'serverMain.js', 131 | path: path.join(__dirname, './dist/browser'), 132 | library: 'serverExportVar', 133 | libraryTarget: 'var' 134 | }, 135 | resolve: { 136 | mainFields: ['browser', 'module', 'main'], // look for `browser` entry point in imported node modules 137 | extensions: ['.ts', '.js'], // support ts-files and js-files 138 | extensionAlias: { 139 | '.js': ['.js', '.ts'] 140 | }, 141 | fallback: { 142 | // Webpack 5 no longer polyfills Node.js core modules automatically. 143 | // see https://webpack.js.org/configuration/resolve/#resolvefallback 144 | // for the list of Node.js core module polyfills. 145 | 'assert': require.resolve('assert') 146 | } 147 | }, 148 | module: { 149 | rules: [{ 150 | test: /\.ts$/, 151 | exclude: /node_modules/, 152 | use: [{ 153 | loader: 'ts-loader' 154 | }] 155 | }] 156 | }, 157 | plugins: [ 158 | new webpack.optimize.LimitChunkCountPlugin({ 159 | maxChunks: 1 // disable chunks by default since web extensions must be a single bundle 160 | }) 161 | ], 162 | externals: { 163 | 'vscode': 'commonjs vscode', // ignored because it doesn't exist 164 | }, 165 | performance: { 166 | hints: false 167 | }, 168 | devtool: 'nosources-source-map', // create a source map that points to the original source file 169 | infrastructureLogging: { 170 | level: "log", // enables logging required for problem matchers 171 | } 172 | }; 173 | 174 | /** @type WebpackConfig */ 175 | const serverNodeConfig = { 176 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 177 | target: 'node', 178 | entry: { 179 | 'extension': './src/server/node/serverNodeMain.ts', 180 | }, 181 | output: { 182 | filename: 'serverMain.js', 183 | path: path.join(__dirname, './dist/node'), 184 | libraryTarget: 'commonjs', 185 | devtoolModuleFilenameTemplate: '../../[resource-path]' 186 | }, 187 | resolve: { 188 | conditionNames: ['import', 'require', 'node-addons', 'node'], 189 | mainFields: ['module', 'main'], 190 | extensions: ['.ts', '.js'], // support ts-files and js-files 191 | extensionAlias: { 192 | '.js': ['.js', '.ts'] 193 | }, 194 | }, 195 | module: { 196 | rules: [{ 197 | test: /\.ts$/, 198 | exclude: /node_modules/, 199 | use: [{ 200 | loader: 'ts-loader' 201 | }] 202 | }] 203 | }, 204 | plugins: [ 205 | new webpack.optimize.LimitChunkCountPlugin({ 206 | maxChunks: 1 // disable chunks by default since web extensions must be a single bundle 207 | }) 208 | ], 209 | externals: { 210 | 'vscode': 'commonjs vscode', // ignored because it doesn't exist 211 | }, 212 | performance: { 213 | hints: false 214 | }, 215 | devtool: 'nosources-source-map', // create a source map that points to the original source file 216 | infrastructureLogging: { 217 | level: "log", // enables logging required for problem matchers 218 | }, 219 | }; 220 | 221 | module.exports = [clientBrowserConfig, clientNodeConfig, serverBrowserConfig, serverNodeConfig ]; --------------------------------------------------------------------------------