├── .eslintrc.json ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── JSONataLanguage.code-workspace ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── extension.ts ├── kernel │ ├── functions.ts │ ├── kernel.ts │ └── loader │ │ ├── json.ts │ │ └── xml.ts ├── language │ ├── CompletionProvider.ts │ ├── diagnostics.ts │ ├── formatter.ts │ └── functions.ts └── notebook │ ├── notebookKernel.ts │ └── notebookSerializer.ts ├── syntaxes ├── jsonata-configuration.json └── jsonata.tmLanguage.json ├── test ├── cos.jsonata ├── order.jsonata ├── packagejson.jsonata-book ├── selector.jsonata └── test.jsonata ├── tsconfig.json └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "extends": [ 5 | "airbnb-base" 6 | ], 7 | "parserOptions": { 8 | "ecmaVersion": 6, 9 | "sourceType": "module" 10 | }, 11 | "plugins": [ 12 | "@typescript-eslint" 13 | ], 14 | "rules": { 15 | "@typescript-eslint/naming-convention": "warn", 16 | "@typescript-eslint/semi": "warn", 17 | "curly": "warn", 18 | "eqeqeq": "warn", 19 | "no-throw-literal": "warn", 20 | "semi": "off", 21 | "import/no-unresolved": "off", 22 | "import/extensions": "off" 23 | }, 24 | "ignorePatterns": [ 25 | "out", 26 | "dist", 27 | "**/*.d.ts" 28 | ] 29 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: NodeJS with Webpack 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [14.x, 16.x, 18.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | 25 | - name: Build 26 | run: | 27 | npm install 28 | npm run package 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | .vscode-test-web/ 6 | *.vsix 7 | test/*.tmp -------------------------------------------------------------------------------- /.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 | "eamodio.tsl-problem-matcher" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/dist/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/test/**/*.js" 30 | ], 31 | "preLaunchTask": "npm: test-watch" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off" 13 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": [ 10 | "$ts-webpack-watch", 11 | "$tslint-webpack-watch" 12 | ], 13 | "isBackground": true, 14 | "presentation": { 15 | "reveal": "never" 16 | }, 17 | "group": { 18 | "kind": "build", 19 | "isDefault": true 20 | } 21 | }, 22 | { 23 | "type": "npm", 24 | "script": "test-watch", 25 | "problemMatcher": "$tsc-watch", 26 | "isBackground": true, 27 | "presentation": { 28 | "reveal": "never" 29 | }, 30 | "group": "build" 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | .vscode-test-web/** 4 | out/** 5 | node_modules/** 6 | src/** 7 | .gitignore 8 | .yarnrc 9 | vsc-extension-quickstart.md 10 | **/tsconfig.json 11 | **/.eslintrc.json 12 | **/*.map 13 | **/*.ts 14 | *.code-workspace -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "vscode-language-jsonata" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [Unreleased] 8 | - Support Error Diagnostics in JSONata files and notebooks 9 | 10 | ## [1.0.0] 11 | - Support block comments (#2) 12 | - Update JSONata to 2.0.3 13 | - Fix parsing string quotes (#5) 14 | - Change of evaluation behavior (Promises are evaluated during JSONata evaluation) 15 | - Implement new functions: 16 | - $parseString() 17 | - $loadUrl() 18 | - $readFile() 19 | - $readUrl() 20 | - $eval() 21 | - $import() 22 | - $writeFile() 23 | 24 | ## [0.4.0] 25 | - Update JSONata to version 2.0.2 26 | 27 | ## [0.3.1] 28 | - Fix issue where VS Code does not recognize the mime type `application/json` as notebook renderer anymore 29 | 30 | ## [0.3.0] 31 | 32 | - Support importing XML files in `$loadFile()` 33 | ## [0.2.1] 34 | 35 | - Better error handling for loading files with `$loadFile()` 36 | ## [0.2.0] 37 | 38 | - Enable web extension (currently only syntax highlighting works - there is no renderer for the JSON output) 39 | - Code Completion for defined functions and vars 40 | 41 | ## [0.1.1] 42 | 43 | - Add test file 44 | - Extend Readme 45 | ## [0.1.0] 46 | 47 | - Initial release 48 | - Supports the grammar from try.jsonata.org 49 | - Supports notebooks -------------------------------------------------------------------------------- /JSONataLanguage.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ] 7 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT license 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vscode-language-jsonata 2 | 3 | This extension brings the JSONata query engine to Visual Studio Code. 4 | 5 | ## Features 6 | 7 | This extension enables JSONata language support to Visual Studio Code. The extension is activated for files with the *.jsonata extension. 8 | 9 | We also support JSONata Notebooks. They are automatically activated for files with the *.jsonata-book extension. 10 | 11 | Within notebooks several additional functions are available: 12 | 13 | - `$parseString($content[, $type])` takes up to two arguments. It parses `$content` into an JSON object. This happens based on the variable `$type` with a JSON or an XML parser. JSON is the default parser. 14 | - `$loadFile($file[, $type])` takes up to two arguments. `$file` represents the filename to be loaded. With the optional argument `$type` one can specify how this file should be loaded. At the moment only `json` and `xml` can be chosen whereas json is the standard if `$type` is missing. 15 | - `$loadUrl($url[, $type])` works just like `$loadFile()` but with URLs instead of files. 16 | 17 | - `$readFile($file)` reads a file and returns the result as a string. 18 | - `$writeFile($file, $content)` writes the data of `$content` into the file `$file`. The content can be an object and is stringified as a JSON string. The indentation is set to `2`. 19 | - `$readUrl($url)` reads a url and returns the result as a string. 20 | 21 | - `$eval($content)` evaluates a string using the JSONata compiler. 22 | - `$import($file)` imports a file and evaluates it. This is useful for defining often used functions in a separate file and then include it in a notebook. If several functions shall be imported from a single file, then export them as a object. This could look like this: 23 | ```json 24 | { 25 | $hello := function() { 26 | "hello" 27 | }; 28 | $world := function() { 29 | "world" 30 | } 31 | } 32 | ``` 33 | In the notebook you can then use this use this code: 34 | ```json 35 | ( 36 | $api = $import("test.jsonata"); 37 | $api.hello(); 38 | $api.world() 39 | ) 40 | ``` 41 | 42 | 43 | Each code cell of the notebook can access the result of the most recent executed cell by using `$ans`. 44 | 45 | A good documentation for the JSONata language can be found [here](https://docs.jsonata.org/overview.html). 46 | 47 | Screenshot 2022-02-20 at 18 19 28 48 | 49 | ## Requirements 50 | 51 | None 52 | 53 | ## Extension Settings 54 | 55 | None 56 | 57 | ## Known Issues 58 | 59 | - None 60 | 61 | ## Release Notes 62 | 63 | ## 1.0.0 64 | - Support block comments (#2) 65 | - Fix parsing string quotes (#5) 66 | - Change of evaluation behavior (Promises are evaluated during JSONata evaluation) 67 | - Implement new functions: 68 | - $parseString() 69 | - $loadUrl() 70 | - $readFile() 71 | - $readUrl() 72 | - $eval() 73 | - $import() 74 | - $writeFile() 75 | 76 | ## 0.4.0 77 | - Update JSONata to version 2.0.2 78 | 79 | ## 0.3.1 80 | - Fix issue where VS Code does not recognize the mime type `application/json` as notebook renderer anymore 81 | 82 | ## 0.3.0 83 | 84 | - Support importing XML files in `$loadFile()` 85 | 86 | ## 0.2.1 87 | 88 | - Better error handling for loading files with `$loadFile()` 89 | 90 | ## 0.2.0 91 | 92 | - Enable web extension (currently only syntax highlighting works - there is no renderer for the JSON output) 93 | - Code Completion for defined functions and vars 94 | 95 | ### 0.1.0 96 | 97 | - Initial release 98 | - Supports the grammar from try.jsonata.org 99 | - Supports notebooks 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-language-jsonata", 3 | "displayName": "VSCode Language JSONata", 4 | "description": "Language support for JSONata", 5 | "version": "1.0.0", 6 | "engines": { 7 | "vscode": "^1.64.0" 8 | }, 9 | "repository": { 10 | "url": "https://github.com/bigbug/vscode-language-jsonata" 11 | }, 12 | "author": { 13 | "name": "@bigbug", 14 | "url": "https://github.com/bigbug" 15 | }, 16 | "publisher": "bigbug", 17 | "categories": [ 18 | "Other" 19 | ], 20 | "activationEvents": [ 21 | "onNotebook:jsonata-book", 22 | "onLanguage:jsonata" 23 | ], 24 | "main": "./dist/extension.js", 25 | "browser": "./dist/extension.js", 26 | "contributes": { 27 | "languages": [ 28 | { 29 | "id": "jsonata", 30 | "extensions": [ 31 | ".jsonata" 32 | ], 33 | "configuration": "./syntaxes/jsonata-configuration.json" 34 | } 35 | ], 36 | "grammars": [ 37 | { 38 | "language": "jsonata", 39 | "scopeName": "source.jsonata", 40 | "path": "./syntaxes/jsonata.tmLanguage.json" 41 | } 42 | ], 43 | "notebooks": [ 44 | { 45 | "id": "jsonata-book", 46 | "type": "jsonata-book", 47 | "displayName": "JSONata Book", 48 | "selector": [ 49 | { 50 | "filenamePattern": "*.jsonata-book" 51 | } 52 | ] 53 | } 54 | ] 55 | }, 56 | "scripts": { 57 | "vscode:prepublish": "npm run package", 58 | "compile": "webpack", 59 | "watch": "webpack --watch", 60 | "open-in-browser": "vscode-test-web --extensionDevelopmentPath=. .", 61 | "package": "webpack --mode production --devtool hidden-source-map", 62 | "test-compile": "tsc -p . --outDir out", 63 | "pretest": "npm run test-compile && npm run compile && npm run lint", 64 | "lint": "eslint src --ext ts", 65 | "test": "node ./out/test/runTest.js" 66 | }, 67 | "devDependencies": { 68 | "@types/glob": "^7.1.4", 69 | "@types/jsonata": "^1.5.1", 70 | "@types/mocha": "^9.0.0", 71 | "@types/node": "14.x", 72 | "@types/vscode": "^1.64.0", 73 | "@typescript-eslint/eslint-plugin": "^4.31.1", 74 | "@typescript-eslint/parser": "^4.31.1", 75 | "@vscode/test-electron": "^1.6.2", 76 | "@vscode/test-web": "*", 77 | "eslint": "^7.32.0", 78 | "eslint-config-airbnb-base": "^15.0.0", 79 | "glob": "^7.1.7", 80 | "mocha": "^9.1.1", 81 | "ts-loader": "^9.2.5", 82 | "typescript": "^4.4.3", 83 | "webpack": "^5.52.1", 84 | "webpack-cli": "^4.8.0" 85 | }, 86 | "dependencies": { 87 | "@types/lodash": "^4.14.178", 88 | "json-stringify-safe": "^5.0.1", 89 | "jsonata": "^2.0.3", 90 | "lodash": "^4.17.21", 91 | "node-fetch": "^3.3.2", 92 | "vscode-uri": "^3.0.3", 93 | "xml2js": "^0.6.2" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | // Import the module and reference it with the alias vscode in your code below 3 | import * as vscode from 'vscode'; 4 | import CompletionProvider from './language/CompletionProvider'; 5 | import NotebookKernel from './notebook/notebookKernel'; 6 | import NotebookSerializer from './notebook/notebookSerializer'; 7 | import subscribeToDocumentChanges from './language/diagnostics'; 8 | import JSONataDocumentFormatter from './language/formatter'; 9 | 10 | // this method is called when your extension is activated 11 | // your extension is activated the very first time the command is executed 12 | export function activate(context: vscode.ExtensionContext) { 13 | context.subscriptions.push(new NotebookKernel()); 14 | context.subscriptions.push(vscode.workspace.registerNotebookSerializer('jsonata-book', new NotebookSerializer(), { 15 | transientOutputs: false, 16 | transientCellMetadata: { 17 | inputCollapsed: true, 18 | outputCollapsed: true, 19 | }, 20 | })); 21 | 22 | context.subscriptions.push(vscode.languages.registerCompletionItemProvider( 23 | ['jsonata'], 24 | new CompletionProvider(), 25 | '$', 26 | )); 27 | 28 | const jsonataDiagnostics = vscode.languages.createDiagnosticCollection('jsonata'); 29 | context.subscriptions.push(jsonataDiagnostics); 30 | 31 | subscribeToDocumentChanges(context, jsonataDiagnostics); 32 | context.subscriptions.push(vscode.languages.registerDocumentFormattingEditProvider( 33 | ['jsonata'], 34 | new JSONataDocumentFormatter(), 35 | )); 36 | } 37 | 38 | // this method is called when your extension is deactivated 39 | export function deactivate() {} 40 | -------------------------------------------------------------------------------- /src/kernel/functions.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-promise-reject-errors */ 2 | // eslint-disable-next-line max-classes-per-file 3 | import * as vscode from 'vscode'; 4 | import { Utils } from 'vscode-uri'; 5 | import fetch from 'node-fetch'; 6 | import loadJSON from './loader/json'; 7 | import loadXML from './loader/xml'; 8 | 9 | const jsonata = require('jsonata'); 10 | 11 | // NEEDED Declaration to silence errors 12 | declare class TextDecoder { 13 | decode(data: Uint8Array): string; 14 | } 15 | 16 | declare class TextEncoder { 17 | encode(data: string): Uint8Array; 18 | } 19 | 20 | export function parseString(content: string, type?: string) { 21 | if (!type || type === 'json') { 22 | return loadJSON(content); 23 | } if (type === 'xml') { 24 | return loadXML(content); 25 | } 26 | return Promise.reject('unknown file handler!'); 27 | } 28 | 29 | export function readFile(filename: string) { 30 | if (!vscode.workspace.workspaceFolders) { return Promise.reject('No workspace loaded!'); } 31 | const folderUri = vscode.workspace.workspaceFolders[0].uri; 32 | const fileUri = Utils.joinPath(folderUri, filename); 33 | 34 | return vscode.workspace.fs.readFile(fileUri) 35 | .then((data) => { 36 | const string = new TextDecoder().decode(data); 37 | return Promise.resolve(string); 38 | }) as Promise; 39 | } 40 | 41 | export function writeFile(filename: string, content: any) { 42 | if (!vscode.workspace.workspaceFolders) { return Promise.reject('No workspace loaded!'); } 43 | const folderUri = vscode.workspace.workspaceFolders[0].uri; 44 | const fileUri = Utils.joinPath(folderUri, filename); 45 | 46 | const data = JSON.stringify(content, undefined, 2); 47 | return vscode.workspace.fs.writeFile(fileUri, new TextEncoder().encode(data)) 48 | .then(() => Promise.resolve(undefined)) as Promise; 49 | } 50 | 51 | export function readUrl(url: string) { 52 | return fetch(url).then((res) => res.text()); 53 | } 54 | 55 | export function loadFile(filename: string, type?: string) { 56 | return readFile(filename) 57 | .then((content) => parseString(content, type)); 58 | } 59 | 60 | export function evaluate(content: string, data: unknown, bindings: {[name:string] : unknown}) { 61 | const jsonataObject = jsonata(content); 62 | return jsonataObject.evaluate(data, bindings); 63 | } 64 | 65 | export function importFile(filename: string, data: unknown, bindings: {[name:string] : unknown}) { 66 | return readFile(filename) 67 | .then((content) => evaluate(content, data, bindings)); 68 | } 69 | 70 | export function loadUrl(filename: string, type?: string) { 71 | return readUrl(filename).then((content) => parseString(content, type)); 72 | } 73 | -------------------------------------------------------------------------------- /src/kernel/kernel.ts: -------------------------------------------------------------------------------- 1 | import jsonata = require('jsonata'); 2 | import { 3 | evaluate, importFile, loadFile, loadUrl, parseString, readUrl, readFile, writeFile, 4 | } from './functions'; 5 | 6 | export default class JSONataKernel { 7 | private data : unknown = undefined; 8 | 9 | private bindings : {[name:string] : unknown} = {}; 10 | 11 | public async restart() { 12 | this.data = undefined; 13 | this.bindings = {}; 14 | } 15 | 16 | private wrapLoader(func: (...args: any[]) => Promise) : 17 | (...args: any[]) => Promise { 18 | return (...args: any[]) => func(...args).then((res: unknown) => { 19 | this.data = res; 20 | return res; 21 | }); 22 | } 23 | 24 | private registerFunctions(obj: jsonata.Expression) { 25 | const funcs : { 26 | name: string, 27 | pointer: (...args: any[]) => Promise | PromiseLike, 28 | parameters: string, 29 | }[] = [ 30 | { 31 | name: 'parseString', 32 | pointer: parseString, 33 | parameters: '', 34 | }, 35 | { 36 | name: 'loadFile', 37 | pointer: this.wrapLoader(loadFile), 38 | parameters: '', 39 | }, 40 | { 41 | name: 'loadUrl', 42 | pointer: this.wrapLoader(loadUrl), 43 | parameters: '', 44 | }, 45 | { 46 | name: 'readFile', 47 | pointer: readFile, 48 | parameters: '', 49 | }, 50 | { 51 | name: 'writeFile', 52 | pointer: writeFile, 53 | parameters: '', 54 | }, 55 | { 56 | name: 'readUrl', 57 | pointer: readUrl, 58 | parameters: '', 59 | }, 60 | { 61 | name: 'eval', 62 | pointer: evaluate, 63 | parameters: '', 64 | }, 65 | { 66 | name: 'import', 67 | pointer: importFile, 68 | parameters: '', 69 | }, 70 | ]; 71 | funcs.forEach( 72 | (a) => obj.registerFunction(a.name, (...b: any[]) => a.pointer(...b), a.parameters), 73 | ); 74 | } 75 | 76 | public async run(code: string) { 77 | const obj = jsonata(code); 78 | this.registerFunctions(obj); 79 | const result = await obj.evaluate(this.data, this.bindings); 80 | const match = code.trim().match(/^\$(\w+)\s*:=/); 81 | if (match) { 82 | this.bindings[match[1]] = result; 83 | } 84 | this.bindings.ans = result; 85 | return result; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/kernel/loader/json.ts: -------------------------------------------------------------------------------- 1 | export default function loadJSON(fileString: string) : Promise { 2 | try { 3 | return Promise.resolve(JSON.parse(fileString)); 4 | } catch(e) { 5 | return Promise.reject(e); 6 | } 7 | } -------------------------------------------------------------------------------- /src/kernel/loader/xml.ts: -------------------------------------------------------------------------------- 1 | const xml2js = require('xml2js'); 2 | export default function loadXML(fileString: string) : Promise { 3 | let parser = new xml2js.Parser(/* options */); 4 | return parser.parseStringPromise(fileString); 5 | } -------------------------------------------------------------------------------- /src/language/CompletionProvider.ts: -------------------------------------------------------------------------------- 1 | import { uniq } from 'lodash'; 2 | import { 3 | CompletionItem, 4 | CompletionItemKind, 5 | CompletionItemProvider, NotebookCell, Position, SnippetString, TextDocument, 6 | } from 'vscode'; 7 | import functions from './functions'; 8 | 9 | export default class CompletionProvider implements CompletionItemProvider { 10 | // eslint-disable-next-line class-methods-use-this 11 | provideCompletionItems( 12 | document: TextDocument, 13 | position: Position, 14 | // token: CancellationToken, 15 | // context: CompletionContext 16 | ) : CompletionItem[] | undefined { 17 | const line = document.lineAt(position.line).text.substring(0, position.character); 18 | 19 | const reg = [ 20 | { 21 | regex: /\$$/, 22 | // eslint-disable-next-line no-unused-vars 23 | func: (_a: any) => functions.split(',').map((f) => { 24 | const item = new CompletionItem(`$${f}()`, CompletionItemKind.Function); 25 | item.insertText = new SnippetString(`${f}($1)`); 26 | return item; 27 | }), 28 | }, 29 | { 30 | regex: /\$$/, 31 | // eslint-disable-next-line no-unused-vars 32 | func: (_a: any) => { 33 | let text = document.getText(); 34 | if ((document as any).notebook) { 35 | const cells : NotebookCell[] = (document as any).notebook.getCells(); 36 | text = cells.map((cell) => cell.document.getText()).join(';\n'); 37 | } 38 | const matches = [...text.matchAll(/\$(\w+)\s*:=\s*(function\s*\(|λ\s*\()?/g)]; 39 | 40 | const functionMatches = uniq(matches.filter((i) => i[2]).map((i) => i[1])); 41 | const varMatches = uniq(matches.filter((i) => !i[2]).map((i) => i[1])); 42 | return [ 43 | ...functionMatches.map((f) => { 44 | const item = new CompletionItem(`$${f}()`, CompletionItemKind.Function); 45 | item.insertText = new SnippetString(`${f}($1)`); 46 | return item; 47 | }), 48 | ...varMatches.map((f) => { 49 | const item = new CompletionItem(`$${f}`, CompletionItemKind.Function); 50 | item.insertText = new SnippetString(f); 51 | return item; 52 | }), 53 | ]; 54 | }, 55 | }, 56 | ]; 57 | 58 | let suggestions: CompletionItem[] = []; 59 | 60 | reg.forEach((el) => { 61 | const result = line.match(el.regex); 62 | if (result) { 63 | suggestions = suggestions.concat(el.func(result)); 64 | } 65 | }); 66 | 67 | return suggestions; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/language/diagnostics.ts: -------------------------------------------------------------------------------- 1 | import jsonata = require('jsonata'); 2 | import { isNumber, isObject } from 'lodash'; 3 | import * as vscode from 'vscode'; 4 | 5 | /** 6 | * Analyzes the text document for problems. 7 | * This demo diagnostic problem provider finds all mentions of 'emoji'. 8 | * @param doc text document to analyze 9 | * @param jsonataDiagnostics diagnostic collection 10 | */ 11 | export function refreshDiagnostics( 12 | doc: vscode.TextDocument, 13 | jsonataDiagnostics: vscode.DiagnosticCollection, 14 | ): void { 15 | const diagnostics: vscode.Diagnostic[] = []; 16 | 17 | const code = doc.getText(); 18 | 19 | try { 20 | jsonata(code); 21 | } catch (e: any) { 22 | // @ts-ignore 23 | if (!e || !isObject(e) || !e.position || !isNumber(e.position)) { 24 | diagnostics.push(new vscode.Diagnostic( 25 | new vscode.Range(new vscode.Position(0, 0), new vscode.Position(999999, 999999)), 26 | 'Unknown JSONata error!', 27 | vscode.DiagnosticSeverity.Error, 28 | )); 29 | } else { 30 | // @ts-ignore 31 | const { position, message } = e; 32 | const lines = code.slice(0, position).split('\n'); 33 | const line = lines.length - 1; 34 | diagnostics.push(new vscode.Diagnostic( 35 | new vscode.Range( 36 | new vscode.Position(line, lines[lines.length - 1].length), 37 | new vscode.Position(line, 99999), 38 | ), 39 | message, 40 | vscode.DiagnosticSeverity.Error, 41 | )); 42 | } 43 | diagnostics.push(); 44 | } 45 | jsonataDiagnostics.set(doc.uri, diagnostics); 46 | } 47 | 48 | export default function subscribeToDocumentChanges( 49 | context: vscode.ExtensionContext, 50 | jsonataDiagnostics: vscode.DiagnosticCollection, 51 | ): void { 52 | if (vscode.window.activeTextEditor) { 53 | refreshDiagnostics(vscode.window.activeTextEditor.document, jsonataDiagnostics); 54 | } 55 | context.subscriptions.push( 56 | vscode.window.onDidChangeActiveTextEditor((editor) => { 57 | if (editor) { 58 | refreshDiagnostics(editor.document, jsonataDiagnostics); 59 | } 60 | }), 61 | ); 62 | 63 | context.subscriptions.push( 64 | vscode.workspace.onDidChangeTextDocument( 65 | (e) => refreshDiagnostics(e.document, jsonataDiagnostics), 66 | ), 67 | ); 68 | 69 | vscode.workspace.onDidChangeNotebookDocument( 70 | (e) => { 71 | e.notebook.getCells().forEach((cell) => { 72 | refreshDiagnostics(cell.document, jsonataDiagnostics); 73 | }); 74 | }, 75 | ); 76 | 77 | context.subscriptions.push( 78 | vscode.workspace.onDidCloseTextDocument( 79 | (doc) => jsonataDiagnostics.delete(doc.uri), 80 | ), 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /src/language/formatter.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line max-classes-per-file 2 | import jsonata = require('jsonata'); 3 | import { 4 | DocumentFormattingEditProvider, Position, ProviderResult, Range, TextDocument, TextEdit, window, 5 | } from 'vscode'; 6 | 7 | class Formatter { 8 | private indent = 0; 9 | 10 | private indentStep = 2; 11 | 12 | private formattedCode: string = ''; 13 | 14 | constructor(code: string) { 15 | const obj = jsonata(code).ast(); 16 | this.evaluate(obj); 17 | 18 | if (this.strip(code) !== this.strip(this.formattedCode)) { 19 | // window.showErrorMessage('Error on formatting! Input and output are different!'); 20 | // throw new Error('Error on formatting! Input and output are different!'); 21 | } 22 | } 23 | 24 | // eslint-disable-next-line class-methods-use-this 25 | private strip(code: string) { 26 | let res = code; 27 | res = res.replace(/[ \s\t\n]/g, ''); 28 | return res; 29 | } 30 | 31 | private evaluate(obj: jsonata.ExprNode) { 32 | // this.rest(obj); 33 | // return; 34 | if (obj.type === 'unary' && obj.value === '{') { 35 | this.evaluteObj(obj); 36 | } else if (obj.type === 'unary' && obj.value === '[') { 37 | this.evaluateArray(obj); 38 | } else if (obj.type === 'unary' && obj.value === '-') { 39 | this.evaluateMinus(obj); 40 | } else if (obj.type === 'string') { 41 | this.evaluateString(obj); 42 | } else if (obj.type === 'number') { 43 | this.evaluateNumber(obj); 44 | } else if (obj.type === 'lambda') { 45 | this.evaluateLambda(obj); 46 | } else if (obj.type === 'variable') { 47 | this.evaluateVariable(obj); 48 | } else if (obj.type === 'binary') { 49 | this.evaluateBinary(obj); 50 | } else if (obj.type === 'name') { 51 | this.evaluateName(obj); 52 | } else if (obj.type === 'path') { 53 | this.evaluatePath(obj); 54 | } else if (obj.type === 'block') { 55 | this.evaluateBlock(obj); 56 | } else if (obj.type === 'wildcard' || obj.type === 'descendant') { 57 | this.evaluateWildcard(obj); 58 | } else if (obj.type === 'parent') { 59 | this.evaluateParent(obj); 60 | } else if (obj.type === 'apply') { 61 | this.evaluateApply(obj); 62 | } else if (obj.type === 'bind') { 63 | this.evaluateBind(obj); 64 | } else if (obj.type === 'condition') { 65 | this.evaluateCondition(obj); 66 | } else if (obj.type === 'function') { 67 | this.evaluateFunction(obj); 68 | } else if (obj.type === 'regex') { 69 | this.evaluateRegex(obj); 70 | } else if (obj.type === 'filter') { 71 | this.evaluateFilter(obj); 72 | } else { 73 | this.rest(obj); 74 | } 75 | } 76 | 77 | private rest(obj: jsonata.ExprNode) { 78 | this.p(JSON.stringify(obj, undefined, 4)); 79 | } 80 | 81 | private p(code: string) { 82 | this.formattedCode += code.replace('\n', `\n${' '.repeat(this.indent)}`); 83 | } 84 | 85 | private i() { 86 | this.indent += this.indentStep; 87 | } 88 | 89 | private d() { 90 | this.indent -= this.indentStep; 91 | } 92 | 93 | private evaluteObj(obj: jsonata.ExprNode) { 94 | this.p('{'); 95 | this.i(); 96 | obj.lhs?.forEach((e, i, a) => { 97 | this.p('\n'); 98 | // @ts-ignore 99 | this.evaluate(e[0]); 100 | this.p(': '); 101 | // @ts-ignore 102 | this.evaluate(e[1]); 103 | if (i + 1 !== a.length) this.p(','); 104 | }); 105 | this.d(); 106 | this.p('\n'); 107 | this.p('}'); 108 | } 109 | 110 | private evaluateArray(obj:jsonata.ExprNode) { 111 | if (obj.expressions?.length === 1) { 112 | this.p('['); 113 | this.evaluate(obj.expressions[0]); 114 | this.p(']'); 115 | return; 116 | } 117 | this.i(); 118 | this.p('['); 119 | obj.expressions?.forEach((e, i, a) => { 120 | this.p('\n'); 121 | this.evaluate(e); 122 | if (i + 1 !== a.length) this.p(','); 123 | }); 124 | this.d(); 125 | this.p('\n]'); 126 | } 127 | 128 | private evaluateBlock(obj: jsonata.ExprNode) { 129 | if (obj.expressions?.length === 1) { 130 | this.p('('); 131 | this.evaluate(obj.expressions[0]); 132 | this.p(')'); 133 | return; 134 | } 135 | this.i(); 136 | this.p('(\n'); 137 | obj.expressions?.forEach((e, i, a) => { 138 | this.evaluate(e); 139 | if (i + 1 !== a.length) this.p(';\n'); 140 | }); 141 | this.d(); 142 | this.p('\n)'); 143 | } 144 | 145 | private evaluateCondition(obj: jsonata.ExprNode) { 146 | // @ts-ignore 147 | this.evaluate(obj.condition); 148 | this.i(); 149 | this.p('\n? '); 150 | // @ts-ignore 151 | this.evaluate(obj.then); 152 | this.p('\n: '); 153 | // @ts-ignore 154 | this.evaluate(obj.else); 155 | this.d(); 156 | } 157 | 158 | private evaluateBind(obj: jsonata.ExprNode) { 159 | // @ts-ignore 160 | this.evaluate(obj.lhs); 161 | this.p(` ${obj.value} `); 162 | // @ts-ignore 163 | this.evaluate(obj.rhs); 164 | } 165 | 166 | private evaluateString(obj: jsonata.ExprNode) { 167 | this.p(`"${obj.value}"`); 168 | } 169 | 170 | private evaluateMinus(obj: jsonata.ExprNode) { 171 | this.p('-'); 172 | // @ts-ignore 173 | this.evaluate(obj.expression); 174 | } 175 | 176 | private evaluateNumber(obj: jsonata.ExprNode) { 177 | this.p(`${obj.value}`); 178 | } 179 | 180 | private evaluateArguments(obj: jsonata.ExprNode) { 181 | obj.arguments?.forEach((arg, index, a) => { 182 | this.evaluate(arg); 183 | if (index + 1 !== a.length) { 184 | this.p(', '); 185 | } 186 | }); 187 | } 188 | 189 | private evaluateLambda(obj: jsonata.ExprNode) { 190 | // @ts-ignore 191 | if (Object.keys(obj).includes('thunk') && obj.thunk) { 192 | // @ts-ignore 193 | this.evaluate(obj.body); 194 | return; 195 | } 196 | this.p('function('); 197 | this.evaluateArguments(obj); 198 | this.i(); 199 | this.p(') {\n'); 200 | // @ts-ignore 201 | this.evaluate(obj.body); 202 | this.d(); 203 | this.p('\n}'); 204 | } 205 | 206 | private evaluateFunction(obj: jsonata.ExprNode) { 207 | // @ts-ignore 208 | this.evaluate(obj.procedure); 209 | this.p('('); 210 | this.evaluateArguments(obj); 211 | this.p(')'); 212 | } 213 | 214 | private evaluateVariable(obj: jsonata.ExprNode) { 215 | this.p(`$${obj.value}`); 216 | } 217 | 218 | private evaluateWildcard(obj: jsonata.ExprNode) { 219 | this.p(obj.value); 220 | } 221 | 222 | // eslint-disable-next-line no-unused-vars 223 | private evaluateParent(obj: jsonata.ExprNode) { 224 | this.p('%'); 225 | } 226 | 227 | private evaluateRegex(obj: jsonata.ExprNode) { 228 | this.p(obj.value.toString()); 229 | } 230 | 231 | private evaluateBinary(obj: jsonata.ExprNode) { 232 | // @ts-ignore 233 | this.evaluate(obj.lhs); 234 | this.p(` ${obj.value} `); 235 | // @ts-ignore 236 | this.evaluate(obj.rhs); 237 | } 238 | 239 | private evaluatePath(obj: jsonata.ExprNode) { 240 | let i = 0; 241 | // @ts-ignore 242 | if (obj.steps[0].type === 'variable' && obj.steps[0].value === '') { 243 | i = 1; 244 | this.p('$'); 245 | } 246 | // @ts-ignore 247 | for (i; i < obj.steps?.length; i += 1) { 248 | if (i !== 0) this.p('.'); 249 | // @ts-ignore 250 | this.evaluate(obj.steps[i]); 251 | 252 | // @ts-ignore 253 | if (obj.steps[i].stages) { 254 | // @ts-ignore 255 | obj.steps[i].stages?.forEach((e) => this.evaluate(e)); 256 | } 257 | } 258 | // @ts-ignore 259 | if (obj.group) { 260 | // @ts-ignore 261 | this.evaluteObj(obj.group); 262 | } 263 | } 264 | 265 | private evaluateFilter(obj: jsonata.ExprNode) { 266 | this.p('['); 267 | // @ts-ignore 268 | this.evaluate(obj.expr); 269 | this.p(']'); 270 | } 271 | 272 | private evaluateApply(obj:jsonata.ExprNode) { 273 | // @ts-ignore 274 | this.evaluate(obj.lhs); 275 | this.p(` ${obj.value} `); 276 | // @ts-ignore 277 | this.evaluate(obj.rhs); 278 | } 279 | 280 | private evaluateName(obj: jsonata.ExprNode) { 281 | const value = obj.value as string; 282 | if (value.includes(' ')) { 283 | this.p(`\`${value}\``); 284 | return; 285 | } 286 | this.p(value); 287 | } 288 | 289 | public code() { 290 | return this.formattedCode; 291 | } 292 | } 293 | 294 | export default class JSONataDocumentFormatter implements DocumentFormattingEditProvider { 295 | // eslint-disable-next-line class-methods-use-this 296 | provideDocumentFormattingEdits( 297 | document: TextDocument, 298 | // options: FormattingOptions, 299 | // token: CancellationToken, 300 | ): ProviderResult { 301 | try { 302 | const code = document.getText(); 303 | const formatted = new Formatter(code).code(); 304 | console.log(formatted); 305 | 306 | const edit: TextEdit[] = []; 307 | edit.push(new TextEdit( 308 | new Range( 309 | new Position(0, 0), 310 | new Position(document.lineCount - 1, document.lineAt(document.lineCount - 1).text.length), 311 | ), 312 | formatted, 313 | )); 314 | return edit; 315 | } catch (e: any) { 316 | console.log(e); 317 | // (parser error) don't bubble up as a pot. unhandled thenable promise; 318 | // explicitly return "no change" instead. 319 | // show error message 320 | window.showErrorMessage(`${e.name} (@${e.location.start.line}:${e.location.start.column}): ${e.message}`); 321 | return undefined; 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/language/functions.ts: -------------------------------------------------------------------------------- 1 | const functions = 'sum,count,max,min,average,string,substring,substringBefore,substringAfter,lowercase,uppercase,length,trim,pad,match,contains,replace,split,join,formatNumber,formatBase,number,floor,ceil,round,abs,sqrt,power,random,boolean,not,map,zip,filter,single,foldLeft,sift,keys,lookup,append,exists,spread,merge,reverse,each,error,assert,type,sort,shuffle,distinct,base64encode,base64decode,encodeUrlComponent,encodeUrl,decodeUrlComponent,decodeUrl'; 2 | export default functions; 3 | -------------------------------------------------------------------------------- /src/notebook/notebookKernel.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import JSONataKernel from '../kernel/kernel'; 3 | 4 | const stringify = require('json-stringify-safe'); 5 | 6 | // const MIME_TYPE = "x-application/jsonata"; 7 | 8 | export default class NotebookKernel implements vscode.Disposable { 9 | readonly id = 'jsonata-book-kernel'; 10 | 11 | readonly notebookType = 'jsonata-book'; 12 | 13 | readonly label = 'JSONata Book'; 14 | 15 | readonly supportedLanguages = ['jsonata']; 16 | 17 | private readonly controller: vscode.NotebookController; 18 | 19 | private executionOrder = 0; 20 | 21 | private kernel: JSONataKernel; 22 | 23 | constructor() { 24 | this.kernel = new JSONataKernel(); 25 | this.controller = vscode.notebooks.createNotebookController( 26 | this.id, 27 | this.notebookType, 28 | this.label, 29 | ); 30 | 31 | this.controller.supportedLanguages = this.supportedLanguages; 32 | this.controller.supportsExecutionOrder = false; 33 | this.controller.description = 'A notebook for making JSONata queries.'; 34 | this.controller.executeHandler = this.executeAll.bind(this); 35 | } 36 | 37 | dispose(): void { 38 | this.controller.dispose(); 39 | } 40 | 41 | public async restartKernel() { 42 | await vscode.commands.executeCommand('notebook.clearAllCellsOutputs'); 43 | this.kernel.restart(); 44 | } 45 | 46 | private executeAll( 47 | cells: vscode.NotebookCell[], 48 | // eslint-disable-next-line no-unused-vars 49 | _notebook: vscode.NotebookDocument, 50 | // eslint-disable-next-line no-unused-vars 51 | _controller: vscode.NotebookController, 52 | ): void { 53 | let run = Promise.resolve(); 54 | // eslint-disable-next-line no-restricted-syntax 55 | for (const cell of cells) { 56 | run = run.then(() => this.doExecution(cell)); 57 | } 58 | } 59 | 60 | private async doExecution(cell: vscode.NotebookCell): Promise { 61 | const execution = this.controller.createNotebookCellExecution(cell); 62 | // eslint-disable-next-line no-plusplus 63 | execution.executionOrder = ++this.executionOrder; 64 | execution.start(Date.now()); 65 | 66 | const query = cell.document.getText(); 67 | try { 68 | const result = await this.kernel.run(query); 69 | execution.replaceOutput([new vscode.NotebookCellOutput([ 70 | vscode.NotebookCellOutputItem.json(result, 'text/x-json'), 71 | ])]); 72 | 73 | execution.end(true, Date.now()); 74 | return Promise.resolve(); 75 | } catch (e) { 76 | execution.replaceOutput([ 77 | new vscode.NotebookCellOutput([ 78 | vscode.NotebookCellOutputItem.error({ 79 | name: (e instanceof Error && e.name) || 'error', 80 | message: (e instanceof Error && e.message) || stringify(e, undefined, 4), 81 | }), 82 | ]), 83 | ]); 84 | execution.end(false, Date.now()); 85 | return Promise.reject(); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/notebook/notebookSerializer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | import * as vscode from 'vscode'; 3 | 4 | // NEEDED Declaration to silence errors 5 | declare class TextDecoder { 6 | decode(data: Uint8Array): string; 7 | } 8 | 9 | declare class TextEncoder { 10 | encode(data: string): Uint8Array; 11 | } 12 | 13 | const stringify = require('json-stringify-safe'); 14 | 15 | interface RawCellOutput { 16 | mime: string; 17 | value: any; 18 | } 19 | interface RawNotebookCell { 20 | language: string; 21 | value: string; 22 | kind: vscode.NotebookCellKind; 23 | editable?: boolean; 24 | outputs: RawCellOutput[]; 25 | } 26 | 27 | export default class NotebookSerializer implements vscode.NotebookSerializer { 28 | // eslint-disable-next-line class-methods-use-this 29 | async deserializeNotebook( 30 | content: Uint8Array, 31 | // eslint-disable-next-line no-unused-vars 32 | _token: vscode.CancellationToken, 33 | ): Promise { 34 | const contents = new TextDecoder().decode(content); // convert to String to make JSON object 35 | 36 | // Read file contents 37 | let raw: RawNotebookCell[]; 38 | try { 39 | raw = JSON.parse(contents); 40 | } catch { 41 | raw = []; 42 | } 43 | 44 | function convertRawOutputToBytes(cell: RawNotebookCell) { 45 | const result: vscode.NotebookCellOutputItem[] = []; 46 | 47 | // eslint-disable-next-line no-restricted-syntax 48 | for (const output of cell.outputs) { 49 | const data = new TextEncoder().encode(stringify(output.value)); 50 | result.push(new vscode.NotebookCellOutputItem(data, output.mime)); 51 | } 52 | 53 | return result; 54 | } 55 | 56 | // Create array of Notebook cells for the VS Code API from file contents 57 | const cells = raw.map((item) => new vscode.NotebookCellData( 58 | item.kind, 59 | item.value, 60 | item.language, 61 | )); 62 | 63 | for (let i = 0; i < cells.length; i += 1) { 64 | const cell = cells[i]; 65 | cell.outputs = []; 66 | if (raw[i].outputs) { 67 | cell.outputs = [new vscode.NotebookCellOutput(convertRawOutputToBytes(raw[i]))]; 68 | } 69 | } 70 | 71 | // Pass read and formatted Notebook Data to VS Code to display Notebook with saved cells 72 | return new vscode.NotebookData( 73 | cells, 74 | ); 75 | } 76 | 77 | // eslint-disable-next-line class-methods-use-this 78 | async serializeNotebook( 79 | data: vscode.NotebookData, 80 | // eslint-disable-next-line no-unused-vars 81 | _token: vscode.CancellationToken, 82 | ): Promise { 83 | // function to take output renderer data to a format to save to the file 84 | function asRawOutput(cell: vscode.NotebookCellData): RawCellOutput[] { 85 | const result: RawCellOutput[] = []; 86 | // eslint-disable-next-line no-restricted-syntax 87 | for (const output of cell.outputs ?? []) { 88 | // eslint-disable-next-line no-restricted-syntax 89 | for (const item of output.items) { 90 | let outputContents = ''; 91 | outputContents = new TextDecoder().decode(item.data); 92 | 93 | try { 94 | const outputData = JSON.parse(outputContents); 95 | result.push({ mime: item.mime, value: outputData }); 96 | } catch { 97 | result.push({ mime: item.mime, value: outputContents }); 98 | } 99 | } 100 | } 101 | return result; 102 | } 103 | 104 | // Map the Notebook data into the format we want to save the Notebook data as 105 | 106 | const contents: RawNotebookCell[] = []; 107 | 108 | // eslint-disable-next-line no-restricted-syntax 109 | for (const cell of data.cells) { 110 | contents.push({ 111 | kind: cell.kind, 112 | language: cell.languageId, 113 | value: cell.value, 114 | outputs: asRawOutput(cell), 115 | }); 116 | } 117 | 118 | // Give a string of all the data to save and VS Code will handle the rest 119 | return new TextEncoder().encode(stringify(contents)); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /syntaxes/jsonata-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "comments": { 4 | // symbol used for single line comment. Remove this entry if your language does not support line comments 5 | "lineComment": "//", 6 | // symbols used for start and end a block comment. Remove this entry if your language does not support block comments 7 | "blockComment": [ 8 | "/*", 9 | "*/" 10 | ] 11 | }, 12 | "brackets": [ 13 | [ 14 | "(", 15 | ")" 16 | ], 17 | [ 18 | "[", 19 | "]" 20 | ], 21 | [ 22 | "{", 23 | "}" 24 | ] 25 | ], 26 | "autoClosingPairs": [ 27 | { 28 | "open": "(", 29 | "close": ")" 30 | }, 31 | { 32 | "open": "[", 33 | "close": "]" 34 | }, 35 | { 36 | "open": "{", 37 | "close": "}" 38 | }, 39 | { 40 | "open": "\"", 41 | "close": "\"" 42 | }, 43 | { 44 | "open": "'", 45 | "close": "'" 46 | }, 47 | { 48 | "open": "`", 49 | "close": "`" 50 | } 51 | ], 52 | "surroundingPairs": [ 53 | { 54 | "open": "(", 55 | "close": ")" 56 | }, 57 | { 58 | "open": "[", 59 | "close": "]" 60 | }, 61 | { 62 | "open": "{", 63 | "close": "}" 64 | }, 65 | { 66 | "open": "\"", 67 | "close": "\"" 68 | }, 69 | { 70 | "open": "'", 71 | "close": "'" 72 | }, 73 | { 74 | "open": "`", 75 | "close": "`" 76 | } 77 | ], 78 | "indentationRules": { 79 | "decreaseIndentPattern": "/^((?!.*?\\/\\*).*\\*\\/)?\\s*[}\\])].*$/", 80 | "increaseIndentPattern": "/^((?!\\/\\/).)*(\\{[^}\"'`]*|\\([^)\"'`]*|\\[[^\\]\"'`]*)$/" 81 | }, 82 | "insertSpaces": true, 83 | "tabSize": 2 84 | } -------------------------------------------------------------------------------- /syntaxes/jsonata.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "scopeName": "source.jsonata", 4 | "foldingStartMarker": "\\{\\s*$", 5 | "foldingStopMarker": "^\\s*\\}", 6 | "patterns": [ 7 | { 8 | "include": "#value" 9 | } 10 | ], 11 | "repository": { 12 | "expression": { 13 | "name": "meta.selector.jsonata", 14 | "patterns": [ 15 | { 16 | "match": "(\\$|\\.)", 17 | "name": "keyword.other.selector.jsonata" 18 | }, 19 | { 20 | "match": "(\\+|-|\\*|/|&)", 21 | "name": "keyword.other.math.jsonata" 22 | } 23 | ] 24 | }, 25 | "array": { 26 | "begin": "\\[", 27 | "beginCaptures": { 28 | "0": { 29 | "name": "punctuation.definition.array.begin.jsonata" 30 | } 31 | }, 32 | "end": "\\]", 33 | "endCaptures": { 34 | "0": { 35 | "name": "punctuation.definition.array.end.jsonata" 36 | } 37 | }, 38 | "name": "meta.structure.array.jsonata", 39 | "patterns": [ 40 | { 41 | "include": "#value" 42 | }, 43 | { 44 | "match": ",", 45 | "name": "punctuation.separator.array.jsonata" 46 | }, 47 | { 48 | "match": "[^\\s\\]]", 49 | "name": "invalid.illegal.expected-array-separator.jsonata" 50 | } 51 | ] 52 | }, 53 | "comments": { 54 | "patterns": [ 55 | { 56 | "begin": "/\\*\\*(?!/)", 57 | "captures": { 58 | "0": { 59 | "name": "punctuation.definition.comment.jsonata" 60 | } 61 | }, 62 | "end": "\\*/", 63 | "name": "comment.block.documentation.jsonata" 64 | }, 65 | { 66 | "begin": "/\\*", 67 | "captures": { 68 | "0": { 69 | "name": "punctuation.definition.comment.jsonata" 70 | } 71 | }, 72 | "end": "\\*/", 73 | "name": "comment.block.jsonata" 74 | }, 75 | { 76 | "captures": { 77 | "1": { 78 | "name": "punctuation.definition.comment.jsonata" 79 | } 80 | }, 81 | "match": "(//).*$\\n?", 82 | "name": "comment.line.double-slash.js" 83 | } 84 | ] 85 | }, 86 | "constant": { 87 | "match": "\\b(?:true|false|null)\\b", 88 | "name": "constant.language.jsonata" 89 | }, 90 | "number": { 91 | "match": "(?x) # turn on extended mode\n -? # an optional minus\n (?:\n 0 # a zero\n | # ...or...\n [1-9] # a 1-9 character\n \\d* # followed by zero or more digits\n )\n (?:\n (?:\n \\. # a period\n \\d+ # followed by one or more digits\n )?\n (?:\n [eE] # an e character\n [+-]? # followed by an option +/-\n \\d+ # followed by one or more digits\n )? # make exponent optional\n )? # make decimal portion optional", 92 | "name": "constant.numeric.jsonata" 93 | }, 94 | "object": { 95 | "begin": "\\{", 96 | "beginCaptures": { 97 | "0": { 98 | "name": "punctuation.definition.dictionary.begin.jsonata" 99 | } 100 | }, 101 | "end": "\\}", 102 | "endCaptures": { 103 | "0": { 104 | "name": "punctuation.definition.dictionary.end.jsonata" 105 | } 106 | }, 107 | "name": "meta.structure.dictionary.jsonata", 108 | "patterns": [ 109 | { 110 | "comment": "the JSON object key", 111 | "include": "#objectkey" 112 | }, 113 | { 114 | "include": "#comments" 115 | }, 116 | { 117 | "begin": ":", 118 | "beginCaptures": { 119 | "0": { 120 | "name": "punctuation.separator.dictionary.key-value.jsonata" 121 | } 122 | }, 123 | "end": "(,)|(?=\\})", 124 | "endCaptures": { 125 | "1": { 126 | "name": "punctuation.separator.dictionary.pair.jsonata" 127 | } 128 | }, 129 | "name": "meta.structure.dictionary.value.jsonata", 130 | "patterns": [ 131 | { 132 | "comment": "the JSON object value", 133 | "include": "#value" 134 | } 135 | ] 136 | }, 137 | { 138 | "match": "[^\\s\\}]", 139 | "name": "invalid.illegal.expected-dictionary-separator.jsonata" 140 | } 141 | ] 142 | }, 143 | "string": { 144 | "begin": "\"", 145 | "beginCaptures": { 146 | "0": { 147 | "name": "punctuation.definition.string.begin.jsonata" 148 | } 149 | }, 150 | "end": "\"", 151 | "endCaptures": { 152 | "0": { 153 | "name": "punctuation.definition.string.end.jsonata" 154 | } 155 | }, 156 | "name": "string.quoted.double.jsonata", 157 | "patterns": [ 158 | { 159 | "include": "#stringcontent" 160 | } 161 | ] 162 | }, 163 | "objectkey": { 164 | "begin": "\"", 165 | "beginCaptures": { 166 | "0": { 167 | "name": "punctuation.support.type.property-name.begin.jsonata" 168 | } 169 | }, 170 | "end": "\"", 171 | "endCaptures": { 172 | "0": { 173 | "name": "punctuation.support.type.property-name.end.jsonata" 174 | } 175 | }, 176 | "name": "string.jsonata support.type.property-name.jsonata", 177 | "patterns": [ 178 | { 179 | "include": "#stringcontent" 180 | }, 181 | { 182 | "include": "#expression" 183 | } 184 | ] 185 | }, 186 | "stringcontent": { 187 | "patterns": [ 188 | { 189 | "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", 190 | "name": "constant.character.escape.jsonata" 191 | }, 192 | { 193 | "match": "\\\\.", 194 | "name": "invalid.illegal.unrecognized-string-escape.jsonata" 195 | } 196 | ] 197 | }, 198 | "value": { 199 | "patterns": [ 200 | { 201 | "include": "#comments" 202 | }, 203 | { 204 | "include": "#constant" 205 | }, 206 | { 207 | "include": "#number" 208 | }, 209 | { 210 | "include": "#string" 211 | }, 212 | { 213 | "include": "#array" 214 | }, 215 | { 216 | "include": "#object" 217 | }, 218 | { 219 | "include": "#expression" 220 | } 221 | ] 222 | } 223 | } 224 | } -------------------------------------------------------------------------------- /test/cos.jsonata: -------------------------------------------------------------------------------- 1 | $cos := function($x){ /* Derive cosine by expanding Maclaurin series */ 2 | $x > $pi ? $cos($x - 2 * $pi) : $x < -$pi ? $cos($x + 2 * $pi) : 3 | $sum([0..12].($power(-1, $) * $power($x, 2*$) / $factorial(2*$))) 4 | } -------------------------------------------------------------------------------- /test/order.jsonata: -------------------------------------------------------------------------------- 1 | Order[OrderID = "order104"].Product.{ 2 | "Account": $AccName(), 3 | "SKU-" & $string(ProductID): $.`Product Name` 4 | } -------------------------------------------------------------------------------- /test/packagejson.jsonata-book: -------------------------------------------------------------------------------- 1 | [{"kind":2,"language":"jsonata","value":"$loadFile(\"../package.json\")","outputs":[{"mime":"text/x-json","value":{"name":"vscode-language-jsonata","displayName":"VSCode Language JSONata","description":"Language support for JSONata","version":"1.0.0","engines":{"vscode":"^1.64.0"},"repository":{"url":"https://github.com/bigbug/vscode-language-jsonata"},"author":{"name":"@bigbug","url":"https://github.com/bigbug"},"publisher":"bigbug","categories":["Other"],"activationEvents":["onNotebook:jsonata-book","onLanguage:jsonata"],"main":"./dist/extension.js","browser":"./dist/extension.js","contributes":{"languages":[{"id":"jsonata","extensions":[".jsonata"],"configuration":"./syntaxes/jsonata-configuration.json"}],"grammars":[{"language":"jsonata","scopeName":"source.jsonata","path":"./syntaxes/jsonata.tmLanguage.json"}],"notebooks":[{"id":"jsonata-book","type":"jsonata-book","displayName":"JSONata Book","selector":[{"filenamePattern":"*.jsonata-book"}]}]},"scripts":{"vscode:prepublish":"npm run package","compile":"webpack","watch":"webpack --watch","open-in-browser":"vscode-test-web --extensionDevelopmentPath=. .","package":"webpack --mode production --devtool hidden-source-map","test-compile":"tsc -p . --outDir out","pretest":"npm run test-compile && npm run compile && npm run lint","lint":"eslint src --ext ts","test":"node ./out/test/runTest.js"},"devDependencies":{"@types/glob":"^7.1.4","@types/jsonata":"^1.5.1","@types/mocha":"^9.0.0","@types/node":"14.x","@types/vscode":"^1.64.0","@typescript-eslint/eslint-plugin":"^4.31.1","@typescript-eslint/parser":"^4.31.1","@vscode/test-electron":"^1.6.2","@vscode/test-web":"*","eslint":"^7.32.0","glob":"^7.1.7","mocha":"^9.1.1","ts-loader":"^9.2.5","typescript":"^4.4.3","webpack":"^5.52.1","webpack-cli":"^4.8.0"},"dependencies":{"@types/lodash":"^4.14.178","json-stringify-safe":"^5.0.1","jsonata":"^2.0.3","lodash":"^4.17.21","node-fetch":"^3.3.2","vscode-uri":"^3.0.3","xml2js":"^0.4.23"}}}]},{"kind":2,"language":"jsonata","value":"**.vscode","outputs":[{"mime":"text/x-json","value":"^1.64.0"}]},{"kind":2,"language":"jsonata","value":"$","outputs":[{"mime":"text/x-json","value":{"name":"vscode-language-jsonata","displayName":"VSCode Language JSONata","description":"Language support for JSONata","version":"1.0.0","engines":{"vscode":"^1.64.0"},"repository":{"url":"https://github.com/bigbug/vscode-language-jsonata"},"author":{"name":"@bigbug","url":"https://github.com/bigbug"},"publisher":"bigbug","categories":["Other"],"activationEvents":["onNotebook:jsonata-book","onLanguage:jsonata"],"main":"./dist/extension.js","browser":"./dist/extension.js","contributes":{"languages":[{"id":"jsonata","extensions":[".jsonata"],"configuration":"./syntaxes/jsonata-configuration.json"}],"grammars":[{"language":"jsonata","scopeName":"source.jsonata","path":"./syntaxes/jsonata.tmLanguage.json"}],"notebooks":[{"id":"jsonata-book","type":"jsonata-book","displayName":"JSONata Book","selector":[{"filenamePattern":"*.jsonata-book"}]}]},"scripts":{"vscode:prepublish":"npm run package","compile":"webpack","watch":"webpack --watch","open-in-browser":"vscode-test-web --extensionDevelopmentPath=. .","package":"webpack --mode production --devtool hidden-source-map","test-compile":"tsc -p . --outDir out","pretest":"npm run test-compile && npm run compile && npm run lint","lint":"eslint src --ext ts","test":"node ./out/test/runTest.js"},"devDependencies":{"@types/glob":"^7.1.4","@types/jsonata":"^1.5.1","@types/mocha":"^9.0.0","@types/node":"14.x","@types/vscode":"^1.64.0","@typescript-eslint/eslint-plugin":"^4.31.1","@typescript-eslint/parser":"^4.31.1","@vscode/test-electron":"^1.6.2","@vscode/test-web":"*","eslint":"^7.32.0","glob":"^7.1.7","mocha":"^9.1.1","ts-loader":"^9.2.5","typescript":"^4.4.3","webpack":"^5.52.1","webpack-cli":"^4.8.0"},"dependencies":{"@types/lodash":"^4.14.178","json-stringify-safe":"^5.0.1","jsonata":"^2.0.3","lodash":"^4.17.21","node-fetch":"^3.3.2","vscode-uri":"^3.0.3","xml2js":"^0.4.23"}}}]},{"kind":2,"language":"jsonata","value":"$.author","outputs":[{"mime":"text/x-json","value":{"name":"@bigbug","url":"https://github.com/bigbug"}}]},{"kind":2,"language":"jsonata","value":"$ans.name","outputs":[{"mime":"text/x-json","value":"@bigbug"}]},{"kind":2,"language":"jsonata","value":"$eval('{\n \"hello\": function() {\n \"world\"\n },\n \"echo\": function($t) {\n \"echo \" & $t\n }\n}')","outputs":[{"mime":"text/x-json","value":{"hello":{"_jsonata_lambda":true,"input":{},"environment":{"timestamp":"2023-12-26T23:08:57.126Z","async":false,"isParallelCall":true,"global":{"ancestry":[null]}},"arguments":[],"body":{"value":"world","type":"string","position":37}},"echo":{"_jsonata_lambda":true,"input":{},"environment":{"timestamp":"2023-12-26T23:08:57.126Z","async":false,"isParallelCall":true,"global":{"ancestry":[null]}},"arguments":[{"value":"t","type":"variable","position":64}],"body":{"type":"binary","value":"&","position":81,"lhs":{"value":"echo ","type":"string","position":79},"rhs":{"value":"t","type":"variable","position":84}}}}}]},{"kind":2,"language":"jsonata","value":"$test := $import(\"test.jsonata\")","outputs":[{"mime":"text/x-json","value":{"hello":{"_jsonata_lambda":true,"input":{},"environment":{"timestamp":"2023-12-26T23:08:57.297Z","async":false,"isParallelCall":false,"global":{"ancestry":[null]}},"arguments":[],"body":{"type":"binary","value":"&","position":45,"lhs":{"type":"binary","value":"&","position":39,"lhs":{"value":"hello","type":"string","position":37},"rhs":{"value":" ","type":"string","position":43}},"rhs":{"value":"world","type":"string","position":53}}}}}]},{"kind":2,"language":"jsonata","value":"$test.hello(\"Test\")","outputs":[{"mime":"text/x-json","value":"hello world"}]},{"kind":2,"language":"jsonata","value":"(\n $abc := $loadUrl(\"https://raw.githubusercontent.com/jsonata-js/jsonata/master/package.json\").repository;\n $writeFile(\"githubdata.json.tmp\", $abc)\n)","outputs":[{"mime":"text/x-json","value":"undefined"}]}] -------------------------------------------------------------------------------- /test/selector.jsonata: -------------------------------------------------------------------------------- 1 | Account.Order.Product{ 2 | `Product Name`: { 3 | "Price": Price, 4 | "Qty": Quantity 5 | } 6 | } -------------------------------------------------------------------------------- /test/test.jsonata: -------------------------------------------------------------------------------- 1 | { 2 | "hello": function() { 3 | ( 4 | "hello" & " " & "world"; 5 | %.%.a.def; 6 | [ 7 | Address, 8 | Other.Alternative.Address 9 | ].City; 10 | Product.(Price * Quantity); 11 | Account.Order.Product { 12 | `Product Name`: {"Price": Price, "Qty": Quantity} 13 | }; 14 | $sum(Account.Order.Product.(Price*Quantity)); 15 | $cos := function($x){ /* Derive cosine by expanding Maclaurin series */ 16 | $x > $pi ? $cos($x - 2 * $pi) : $x < -$pi ? $cos($x + 2 * $pi) : 17 | $sum([0..12].($power(-1, $) * $power($x, 2*$) / $factorial(2*$))) 18 | }; 19 | $twice := function($f) { function($x){ $f($f($x)) } }; 20 | Account.( 21 | $AccName := function() { $.'Account Name' }; 22 | 23 | Order[OrderID = 'order104'].Product.{ 24 | 'Account': $AccName(), 25 | 'SKU-' & $string(ProductID): $.'Product Name' 26 | } 27 | ); 28 | $matcher := /[a-z]*an[a-z]*/i; 29 | library.loans@$l.books@$b[$l.isbn=$b.isbn].{ 30 | 'title': $b.title, 31 | 'customer': $l.customer 32 | }; 33 | [1..$count(Items)].("Item " & $); 34 | "world" in ["hello", "world"]; 35 | library.books[price < 10 or section="diy"].title; 36 | Customer.Email ~> $substringAfter("@") ~> $substringBefore(".") ~> $uppercase(); 37 | ) 38 | } 39 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "lib": [ 6 | "ES2020" 7 | ], 8 | "rootDir": "src", 9 | "strict": true /* enable all strict type-checking options */ 10 | /* Additional Checks */ 11 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 12 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 13 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 14 | }, 15 | "exclude": [ 16 | "node_modules", 17 | ".vscode-test" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | //@ts-check 8 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 9 | 10 | /** @type WebpackConfig */ 11 | const extensionConfig = { 12 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 13 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 14 | 15 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 16 | output: { 17 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 18 | path: path.resolve(__dirname, 'dist'), 19 | filename: 'extension.js', 20 | libraryTarget: 'commonjs2' 21 | }, 22 | externals: { 23 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 24 | // modules added here also need to be added in the .vsceignore file 25 | }, 26 | resolve: { 27 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 28 | extensions: ['.ts', '.js'] 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.ts$/, 34 | exclude: /node_modules/, 35 | use: [ 36 | { 37 | loader: 'ts-loader' 38 | } 39 | ] 40 | } 41 | ] 42 | }, 43 | devtool: 'nosources-source-map' 44 | }; 45 | module.exports = [ extensionConfig ]; --------------------------------------------------------------------------------