├── fixtures ├── 1.groq ├── 2.groq ├── 4.groq ├── 3.groq ├── 5.groq ├── 6.groq ├── 7.groq ├── 9.groq ├── test.md ├── 8.groq └── test.js ├── .npmignore ├── .gitignore ├── images └── icon.png ├── .vscodeignore ├── screenshots ├── codelenspreview.png └── previewofquery.png ├── .prettierrc ├── language └── language-configuration.json ├── src ├── resultView │ └── ResultView.tsx ├── query.ts ├── providers │ ├── content-provider.tsx │ └── groq-codelens-provider.ts ├── config │ └── findConfig.ts └── extension.ts ├── .vscode └── launch.json ├── release.config.mjs ├── grammars ├── groq.md.json ├── groq.js.json └── groq.json ├── CHANGELOG.md ├── tsconfig.json ├── LICENSE ├── .github └── workflows │ └── release.yml ├── README.md └── package.json /fixtures/1.groq: -------------------------------------------------------------------------------- 1 | * -------------------------------------------------------------------------------- /fixtures/2.groq: -------------------------------------------------------------------------------- 1 | *[_type == "movie"] -------------------------------------------------------------------------------- /fixtures/4.groq: -------------------------------------------------------------------------------- 1 | *[_type == "movie"][0...10] 2 | -------------------------------------------------------------------------------- /fixtures/3.groq: -------------------------------------------------------------------------------- 1 | { 2 | "movies": *[_type == "movie"] 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | fixtures/** 2 | .vscode/** 3 | .vscode-test/** 4 | .git/** 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode-test 3 | out 4 | *.vsix 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/vscode-sanity/HEAD/images/icon.png -------------------------------------------------------------------------------- /fixtures/5.groq: -------------------------------------------------------------------------------- 1 | *[_type == $type && title in ['a', "b", 'c']] | order(_createdAt desc) [0...10] { 2 | ..., 3 | } 4 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | .git/** 4 | .gitignore 5 | vsc-extension-quickstart.md 6 | fixtures/** 7 | -------------------------------------------------------------------------------- /screenshots/codelenspreview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/vscode-sanity/HEAD/screenshots/codelenspreview.png -------------------------------------------------------------------------------- /screenshots/previewofquery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/vscode-sanity/HEAD/screenshots/previewofquery.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 100, 4 | "bracketSpacing": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/6.groq: -------------------------------------------------------------------------------- 1 | *[title in ['a', "b", 'c']] | order(_createdAt desc) [0...10] { 2 | "names": ["espen", 'bjørge', ...["simen", "even"]], 3 | ..., 4 | } 5 | -------------------------------------------------------------------------------- /fixtures/7.groq: -------------------------------------------------------------------------------- 1 | *[_type == "book" && defined(author)] [0...10] { 2 | title, 3 | coverImage { 4 | asset->{url, mimeType}, 5 | crop 6 | }, 7 | 8 | // Expand the author reference 9 | author->, 10 | 11 | // Just need the title for the publisher 12 | "publisher": publisher->title, 13 | } 14 | -------------------------------------------------------------------------------- /fixtures/9.groq: -------------------------------------------------------------------------------- 1 | *[_type=='movie']{ 2 | ..., 3 | releaseDate >= '2018-06-01' => { 4 | "screenings": *[_type == 'screening' && movie._ref == ^._id], 5 | "news": *[_type == 'news' && movie._ref == ^._id], 6 | }, 7 | popularity > 20 && rating > 7.0 => { 8 | "featured": true, 9 | "awards": *[_type == 'award' && movie._ref == ^._id], 10 | }, 11 | } -------------------------------------------------------------------------------- /fixtures/test.md: -------------------------------------------------------------------------------- 1 | # Foo 2 | 3 | ```groq 4 | *[_type=='movie']{ 5 | ..., 6 | releaseDate >= '2018-06-01' => { 7 | "screenings": *[_type == 'screening' && movie._ref == ^._id], 8 | "news": *[_type == 'news' && movie._ref == ^._id], 9 | }, 10 | popularity > 20 && rating > 7.0 => { 11 | "featured": true, 12 | "awards": *[_type == 'award' && movie._ref == ^._id], 13 | }, 14 | } 15 | ``` 16 | -------------------------------------------------------------------------------- /language/language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "//" 4 | }, 5 | "brackets": [ 6 | ["{", "}"], 7 | ["[", "]"], 8 | ["(", ")"] 9 | ], 10 | "autoClosingPairs": [ 11 | ["{", "}"], 12 | ["[", "]"], 13 | ["(", ")"], 14 | ["\"", "\""], 15 | ["'", "'"] 16 | ], 17 | "surroundingPairs": [ 18 | ["{", "}"], 19 | ["[", "]"], 20 | ["(", ")"], 21 | ["\"", "\""], 22 | ["'", "'"] 23 | ] 24 | } -------------------------------------------------------------------------------- /src/resultView/ResultView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {ReactJason} from 'react-jason' 3 | 4 | export function ResultView({query, params, ms, result}: {query: string; params: Record; ms: number; result: any}) { 5 | return ( 6 |
7 |

Query result

8 |

Query: {query}

9 |

params: {JSON.stringify(params)}

10 |

Time: {ms}ms

11 | 12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /.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 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /release.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('semantic-release').GlobalConfig} 3 | */ 4 | export default { 5 | branches: ["main"], 6 | plugins: [ 7 | [ 8 | "@semantic-release/commit-analyzer", 9 | { 10 | releaseRules: [ 11 | { "type": "docs", "scope": "README", "release": "patch" }, 12 | ] 13 | } 14 | ], 15 | "@semantic-release/release-notes-generator", 16 | [ 17 | "semantic-release-vsce", 18 | { 19 | packageVsix: true 20 | } 21 | ], 22 | [ 23 | "@semantic-release/npm", 24 | { 25 | npmPublish: false 26 | } 27 | ], 28 | "@semantic-release/git", 29 | ] 30 | }; 31 | -------------------------------------------------------------------------------- /grammars/groq.md.json: -------------------------------------------------------------------------------- 1 | { 2 | "scopeName": "markdown.groq.codeblock", 3 | "fileTypes": [], 4 | "injectionSelector": "L:markup.fenced_code.block.markdown", 5 | "patterns": [ 6 | { 7 | "include": "#groq-code-block" 8 | } 9 | ], 10 | "repository": { 11 | "groq-code-block": { 12 | "begin": "(?<=[`~])groq(\\s+[^`~]*)?$", 13 | "end": "(^|\\G)(?=\\s*[`~]{3,}\\s*$)", 14 | "patterns": [ 15 | { 16 | "begin": "(^|\\G)(\\s*)(.*)", 17 | "while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)", 18 | "contentName": "meta.embedded.block.groq", 19 | "patterns": [ 20 | { 21 | "include": "source.groq" 22 | } 23 | ] 24 | } 25 | ] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes will be documented in this file. 4 | 5 | ## 0.2.0 - 2024-08-06 6 | 7 | - BREAKING CHANGE: Drop support for Sanity Studio v2 8 | - Support Sanity Studio v3 and new groq `defineQuery` method 9 | 10 | ## 0.1.4 - 2021-01-19 11 | 12 | - Fix Mac key binding for 'sanity.executeGroq' to not conflict with VSCode default keybindings 13 | 14 | ## 0.1.1 - 2020-10-13 15 | 16 | - Upgraded `@sanity/client` 17 | 18 | ## 0.1.0 - 2020-10-13 19 | 20 | - Added support for running GROQ-queries 21 | 22 | ## 0.0.3 - 2020-03-15 23 | 24 | - Added syntax highlighting for GROQ inside `.svelte`, `.php` files (#3) 25 | 26 | ## 0.0.2 - 2018-12-11 27 | 28 | - Update package description 29 | 30 | ## 0.0.1 - 2018-12-11 31 | 32 | - Initial release 33 | -------------------------------------------------------------------------------- /fixtures/8.groq: -------------------------------------------------------------------------------- 1 | *[_type == $type && title in ['a', "b", 'c']] | order(_createdAt desc) [0...10] { 2 | ..., 3 | 4 | author->, 5 | "tags": tags[]->, 6 | 7 | "even": index % 2 == 0, 8 | 'odd': index % 2 != 0, 9 | 10 | // Only fetch news for recent movies 11 | ...select(releaseDate >= $recentThreshold => { 12 | "news": *[_type == 'news' && movie._ref == ^._id], 13 | }), 14 | 15 | ...select(popularity > 20 && field == !false && rating >= 7.0 => { 16 | "awards": *[_type == 'award' && movie._ref == @._id], 17 | "related": *[_type == 'movie' && references(^._id)].title, 18 | "cast": castMembers[characterName in ['Ripley', 'Lambert']].person->{_id, name}, 19 | "featured": true, 20 | "count": count(*[_type == 'movie' && rating == 'R']) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /src/query.ts: -------------------------------------------------------------------------------- 1 | import {createClient} from '@sanity/client' 2 | import * as vscode from 'vscode' 3 | 4 | export async function executeGroq(options: { 5 | projectId: string 6 | dataset: string 7 | query: string 8 | params: Record 9 | useCdn: boolean 10 | token?: string 11 | }) { 12 | const {query, params, ...clientOptions} = options 13 | const {token, ...noTokenClientOptions} = clientOptions 14 | return createClient(clientOptions) 15 | .fetch(query, params, {filterResponse: false}) 16 | .catch((err) => { 17 | if (err.statusCode === 401) { 18 | vscode.window.showInformationMessage(err.message + '. Falling back to public dataset.') 19 | return createClient(noTokenClientOptions).fetch(query, params, {filterResponse: false}) 20 | } 21 | 22 | throw err 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "Node16", 4 | "target": "ES2022", 5 | "outDir": "out", 6 | "lib": ["ES2022", "esnext", "dom"], 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | /* Strict Type-Checking Option */ 10 | "strict": true /* enable all strict type-checking options */, 11 | /* Additional Checks */ 12 | "noUnusedLocals": true /* Report errors on unused locals. */, 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | "noImplicitAny": false, 17 | "esModuleInterop": true, 18 | "jsx": "react" 19 | }, 20 | "exclude": ["node_modules", ".vscode-test", "ts-graphql-plugin"] 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sanity.io 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 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: read # for checkout 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | outputs: 15 | new_release: ${{ steps.check_release.outputs.new_release }} 16 | permissions: 17 | contents: write # to be able to publish a GitHub release 18 | issues: write # to be able to comment on released issues 19 | pull-requests: write # to be able to comment on released pull requests 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | OVSX: ${{ secrets.OVSX_PAT }} 23 | VSCE: ${{ secrets.VSCE_PAT }} 24 | steps: 25 | - name: Set OVSX_PAT env var 26 | if: env.OVSX != '' 27 | run: echo "OVSX_PAT=${{ env.OVSX }}" >> $GITHUB_ENV 28 | - name: Set VSCE_PAT env var 29 | if: env.VSCE != '' 30 | run: echo "VSCE_PAT=${{ env.VSCE }}" >> $GITHUB_ENV 31 | - uses: actions/checkout@v4 32 | - uses: actions/setup-node@v4 33 | with: 34 | node-version: 22 35 | - run: npm ci 36 | - run: npm run lint 37 | - run: npm run release -------------------------------------------------------------------------------- /src/providers/content-provider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/server' 3 | import {type TextDocumentContentProvider, type Uri, type ProviderResult} from 'vscode' 4 | import {ResultView} from '../resultView/ResultView' 5 | 6 | export class GroqContentProvider implements TextDocumentContentProvider { 7 | private html: string = '' 8 | 9 | constructor(query: string, params: Record, ms: number, data: any) { 10 | this.html = ` 11 | 12 | 13 | GROQ result 14 | 18 | 19 | ${this.render(query, params, ms, data)} 20 | 21 | ` 22 | } 23 | 24 | provideTextDocumentContent(_: Uri): ProviderResult { 25 | return this.html 26 | } 27 | 28 | getCurrentHTML(): Promise { 29 | return new Promise((resolve) => { 30 | resolve(this.html) 31 | }) 32 | } 33 | 34 | render(query: string, params: Record, ms: number, result: any) { 35 | return ReactDOM.renderToStaticMarkup( 36 | <> 37 | 38 | 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /fixtures/test.js: -------------------------------------------------------------------------------- 1 | import {defineQuery} from 'groq' 2 | const groq = () => '' 3 | 4 | const query = groq`*[_type=='movie']{ 5 | ..., 6 | releaseDate >= '2018-06-01' => { 7 | "screenings": *[_type == 'screening' && movie._ref == ^._id], 8 | "news": *[_type == 'news' && movie._ref == ^._id], 9 | }, 10 | popularity > 20 && rating > 7.0 => { 11 | "featured": true, 12 | "awards": *[_type == 'award' && movie._ref == ^._id], 13 | }, 14 | } 15 | ` 16 | 17 | const query2 = /* groq */` 18 | *[_type=='movie']{ 19 | ..., 20 | releaseDate >= '2018-06-01' => { 21 | "screenings": *[_type == 'screening' && movie._ref == ^._id], 22 | "news": *[_type == 'news' && movie._ref == ^._id], 23 | }, 24 | popularity > 20 && rating > 7.0 => { 25 | "featured": true, 26 | "awards": *[_type == 'award' && movie._ref == ^._id], 27 | }, 28 | } 29 | ` 30 | 31 | const query3 = `// groq 32 | *[_type=='movie']{ 33 | ..., 34 | releaseDate >= '2018-06-01' => { 35 | "screenings": *[_type == 'screening' && movie._ref == ^._id], 36 | "news": *[_type == 'news' && movie._ref == ^._id], 37 | }, 38 | popularity > 20 && rating > 7.0 => { 39 | "featured": true, 40 | "awards": *[_type == 'award' && movie._ref == ^._id], 41 | }, 42 | } 43 | ` 44 | 45 | const query4 = defineQuery(`*[_type=='movie']{ 46 | ..., 47 | releaseDate >= '2018-06-01' => { 48 | "screenings": *[_type == 'screening' && movie._ref == ^._id], 49 | "news": *[_type == 'news' && movie._ref == ^._id], 50 | }, 51 | popularity > 20 && rating > 7.0 => { 52 | "featured": true, 53 | "awards": *[_type == 'award' && movie._ref == ^._id], 54 | }, 55 | }`) 56 | -------------------------------------------------------------------------------- /src/config/findConfig.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import * as path from 'path' 3 | import os from 'os' 4 | import {promises as fs} from 'fs' 5 | import osenv from 'osenv' 6 | import xdgBasedir from 'xdg-basedir' 7 | 8 | export interface Config { 9 | api: { 10 | projectId: string 11 | dataset: string 12 | token?: string 13 | } 14 | } 15 | 16 | export async function loadConfig(basePath: string): Promise { 17 | let dir = basePath 18 | while (!(await hasConfig(dir))) { 19 | const parent = path.dirname(dir) 20 | if (!dir || parent === dir) { 21 | // last ditch effort, check if we are in a studio monorepo 22 | const folders = vscode?.workspace?.workspaceFolders || [] 23 | dir = (folders.length && folders[0].uri.fsPath + '/studio') || '/' 24 | if (!(await hasConfig(dir))) { 25 | return false 26 | } 27 | } else { 28 | dir = parent 29 | } 30 | } 31 | 32 | const configContent = await fs.readFile(path.join(dir, 'sanity.json'), 'utf8') 33 | const config = parseJson(configContent) 34 | if (!config || !config.api || !config.api.projectId) { 35 | return false 36 | } 37 | 38 | const cliConfigContent = await fs.readFile(getGlobalConfigLocation(), 'utf8') 39 | const cliConfig = parseJson(cliConfigContent) 40 | 41 | return cliConfig ? {...config.api, token: cliConfig.authToken} : config.api 42 | } 43 | 44 | async function hasConfig(dir: string): Promise { 45 | return fs 46 | .stat(path.join(dir, 'sanity.json')) 47 | .then(() => true) 48 | .catch(() => false) 49 | } 50 | 51 | function parseJson(content: string) { 52 | try { 53 | return JSON.parse(content) 54 | } catch (err) { 55 | return false 56 | } 57 | } 58 | 59 | function getGlobalConfigLocation() { 60 | const user = (osenv.user() || 'user').replace(/\\/g, '') 61 | const configDir = xdgBasedir.config || path.join(os.tmpdir(), user, '.config') 62 | return path.join(configDir, 'sanity', 'config.json') 63 | } 64 | -------------------------------------------------------------------------------- /grammars/groq.js.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileTypes": [ 3 | "js", 4 | "jsx", 5 | "ts", 6 | "tsx", 7 | "vue" 8 | ], 9 | "injectionSelector": "L:source -string -comment", 10 | "patterns": [ 11 | { 12 | "contentName": "meta.embedded.block.groq", 13 | "begin": "\\s*+(groq|(/\\* groq \\*/))\\s*(`)", 14 | "beginCaptures": { 15 | "1": { 16 | "name": "entity.name.function.tagged-template.js" 17 | }, 18 | "2": { 19 | "name": "comment.groq.js" 20 | }, 21 | "3": { 22 | "name": "punctuation.definition.string.template.begin.js" 23 | } 24 | }, 25 | "end": "`", 26 | "endCaptures": { 27 | "0": { 28 | "name": "punctuation.definition.string.template.end.js" 29 | } 30 | }, 31 | "patterns": [ 32 | { 33 | "include": "source.groq" 34 | } 35 | ] 36 | }, 37 | { 38 | "name": "taggedTemplates", 39 | "contentName": "meta.embedded.block.groq", 40 | "begin": "(`)(//\\s*groq)", 41 | "beginCaptures": { 42 | "1": { 43 | "name": "punctuation.definition.string.template.begin.js" 44 | }, 45 | "2": { 46 | "name": "comment.line.groq.js" 47 | } 48 | }, 49 | "end": "`", 50 | "endCaptures": { 51 | "0": { 52 | "name": "punctuation.definition.string.template.end.js" 53 | } 54 | }, 55 | "patterns": [ 56 | { 57 | "include": "source.groq" 58 | } 59 | ] 60 | }, 61 | { 62 | "name": "meta.embedded.block.defineQuery", 63 | "begin": "(defineQuery)\\s*\\(\\s*(['\"`])", 64 | "contentName": "meta.embedded.block.groq", 65 | "beginCaptures": { 66 | "1": { 67 | "name": "keyword.other.js" 68 | }, 69 | "2": { 70 | "name": "punctuation.definition.string.begin.js" 71 | } 72 | }, 73 | "end": "(['\"`])\\s*\\)", 74 | "endCaptures": { 75 | "1": { 76 | "name": "punctuation.definition.string.end.js" 77 | } 78 | }, 79 | "patterns": [ 80 | { 81 | "include": "source.groq" 82 | } 83 | ] 84 | } 85 | ], 86 | "scopeName": "inline.groq" 87 | } 88 | -------------------------------------------------------------------------------- /src/providers/groq-codelens-provider.ts: -------------------------------------------------------------------------------- 1 | import {type CodeLensProvider, type TextDocument, type CancellationToken, CodeLens, Range, Position} from 'vscode' 2 | 3 | interface ExtractedQuery { 4 | content: string 5 | uri: string 6 | position: Position 7 | } 8 | 9 | function extractAllTemplateLiterals(document: TextDocument): ExtractedQuery[] { 10 | const documents: ExtractedQuery[] = [] 11 | const text = document.getText() 12 | const regExpGQL = new RegExp('groq\\s*`([\\s\\S]+?)`', 'mg') 13 | 14 | let prevIndex = 0 15 | let result 16 | while ((result = regExpGQL.exec(text)) !== null) { 17 | const content = result[1] 18 | const queryPosition = text.indexOf(content, prevIndex) 19 | documents.push({ 20 | content: content, 21 | uri: document.uri.path, 22 | position: document.positionAt(queryPosition), 23 | }) 24 | prevIndex = queryPosition + 1 25 | } 26 | return documents 27 | } 28 | 29 | function extractAllDefineQuery(document: TextDocument): ExtractedQuery[] { 30 | const documents: ExtractedQuery[] = [] 31 | const text = document.getText() 32 | const pattern = '(\\s*defineQuery\\((["\'`])([\\s\\S]*?)\\2\\))' 33 | const regexp = new RegExp(pattern, 'g'); 34 | 35 | let prevIndex = 0 36 | let result 37 | while ((result = regexp.exec(text)) !== null) { 38 | const content = result[3] 39 | const queryPosition = text.indexOf(result[1], prevIndex) 40 | documents.push({ 41 | content: content, 42 | uri: document.uri.path, 43 | position: document.positionAt(queryPosition), 44 | }) 45 | prevIndex = queryPosition + 1 46 | } 47 | return documents 48 | } 49 | 50 | export class GROQCodeLensProvider implements CodeLensProvider { 51 | constructor() {} 52 | 53 | public provideCodeLenses(document: TextDocument, _token: CancellationToken): CodeLens[] { 54 | if (document.languageId === 'groq') { 55 | return [ 56 | new CodeLens(new Range(new Position(0, 0), new Position(0, 0)), { 57 | title: 'Execute Query', 58 | command: 'sanity.executeGroq', 59 | arguments: [document.getText()], 60 | }), 61 | ] 62 | } 63 | 64 | // find all lines where "groq" exists 65 | const queries: ExtractedQuery[] = [...extractAllTemplateLiterals(document), ...extractAllDefineQuery(document)] 66 | 67 | // add a button above each line that has groq 68 | return queries.map((def) => { 69 | return new CodeLens( 70 | new Range(new Position(def.position.line, 0), new Position(def.position.line, 0)), 71 | { 72 | title: 'Execute Query', 73 | command: 'sanity.executeGroq', 74 | arguments: [def.content], 75 | } 76 | ) 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vscode-sanity 2 | 3 | Extension for [Visual Studio Code](https://code.visualstudio.com/) / [Cursor](https://cursor.com/) that makes developing applications for [Sanity.io](https://www.sanity.io/) that much more awesome. 4 | 5 | ## Features 6 | 7 | ### GROQ syntax highlighting 8 | 9 | Syntax highlighting for the GROQ query language is available in the following situations: 10 | 11 | - Files with the `.groq` extension 12 | - Fenced code blocks in Markdown with the `groq` tag 13 | - Tagged template literals with the `groq` tag 14 | - Queries using the `defineQuery` method 15 | - Template literals prefixed with the `/* groq */` comment 16 | - Template literals starting with a `// groq` comment 17 | 18 | ### Execute GROQ-queries 19 | 20 | When GROQ-queries are detected, the extension will allow you to run the query and displays the result as JSON in a separate tab. 21 | 22 | The project ID and dataset used is determined by finding `sanity.cli.ts` in the workspace. If multiple files are found, the extension will prompt you to select one. 23 | 24 | If the GROQ file/query has any variables, then extension asks for a relative filename of a JSON-file containing an object of key-value mappings. It autofills the param filename based on the current file with a `.json` extension, if it exists. 25 | 26 | ![Execute GROQ](https://raw.githubusercontent.com/sanity-io/vscode-sanity/main/screenshots/previewofquery.png) 27 | 28 | ## Usage 29 | 30 | Install the extension for [VSCode](https://marketplace.visualstudio.com/items?itemName=sanity-io.vscode-sanity) or [Cursor](https://open-vsx.org/extension/sanity-io/vscode-sanity) by searching for `sanity`. This extension adds syntax highlighting for GROQ-files and `groq` tags. 31 | 32 | ## Development 33 | 34 | 1. Clone the repository - https://github.com/sanity-io/vscode-sanity 35 | 2. `npm install` 36 | 3. Open it in VSCode 37 | 4. Go to the debugging section and run the launch program "Extension" 38 | 5. This will open another VSCode instance with extension enabled 39 | 6. Open a file that should be syntax highlighted 40 | 7. Make changes to the extension code, then press (`Ctrl+R` or `Cmd+R` on Mac) in the syntax highlighted file to test the changes 41 | 42 | > We follow the principles of semantic versioning and conventional commits, meaning that the commits are used as the basis for determining if it's a patch/minor/major when building and releasing new version, as well as for generating the release notes. A good explanation of what commit messages translate to what version bumps can be found in the [`semantic release`](https://github.com/semantic-release/semantic-release?tab=readme-ov-file#commit-message-format) docs. 43 | 44 | If you want to build/inspect the vsix file, you can do `npm run package`. It can then be "installed from location" in either Cursor or VS Code. 45 | 46 | ## Publishing 47 | 48 | The extension is built whenever new code is pushed to the `main` branch of the repo. The release notes and tagging is done automatically in CI by `semantic-release` and the extension is pushed to Open VSX registry and [Visual Studio marketplace](https://marketplace.visualstudio.com/items?itemName=sanity-io.vscode-sanity). 49 | 50 | If you want to dry run the release, you can do 👇 to have `semantic-release` parse the git history and see if everything is ok. Since you're running locally it'll skip the actual steps of packaging and pushing etc. You need to set `GITHUB_TOKEN` + `OVSX_PAT` and/or `VSCE_PAT` env vars for the dry run release to complete. 51 | 52 | ```sh 53 | # you can dry-run using 54 | npm run release:dryrun 55 | 56 | # and when all is good you do 57 | npm run release 58 | ``` 59 | 60 | > ⚠️ The `release` script will not publish from your local machine (unless you do `--no-ci`). The publishing should be handled in CI – if you need to do it, see the workflow file for details on what ENV vars you need to provide. 61 | 62 | ## License 63 | 64 | MIT 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-sanity", 3 | "displayName": "Sanity.io", 4 | "version": "0.2.4", 5 | "license": "MIT", 6 | "description": "Developer tools for applications powered by Sanity.io", 7 | "author": "Sanity.io ", 8 | "publisher": "sanity-io", 9 | "engines": { 10 | "vscode": "^1.91.0" 11 | }, 12 | "keywords": [ 13 | "vscode", 14 | "visual studio code", 15 | "sanity", 16 | "groq" 17 | ], 18 | "categories": [ 19 | "Programming Languages" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/sanity-io/vscode-sanity.git" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/sanity-io/vscode-sanity/issues" 27 | }, 28 | "homepage": "https://github.com/sanity-io/vscode-sanity/blob/main/README.md", 29 | "icon": "images/icon.png", 30 | "activationEvents": [ 31 | "*" 32 | ], 33 | "main": "./out/extension.js", 34 | "contributes": { 35 | "configuration": { 36 | "title": "Sanity", 37 | "properties": { 38 | "sanity.useCodelens": { 39 | "type": "boolean", 40 | "default": true, 41 | "description": "Enable GROQ codelens." 42 | }, 43 | "sanity.useCDN": { 44 | "type": "boolean", 45 | "default": false, 46 | "description": "Use APICDN for GROQ queries." 47 | }, 48 | "sanity.openJSONFile": { 49 | "type": "boolean", 50 | "default": false, 51 | "description": "Open query results in a new tab as an editable JSON file." 52 | } 53 | } 54 | }, 55 | "commands": [ 56 | { 57 | "command": "sanity.executeGroq", 58 | "title": "Execute GROQ query" 59 | } 60 | ], 61 | "menus": { 62 | "commandPalette": [ 63 | { 64 | "command": "sanity.executeGroq", 65 | "when": "editorLangId == groq || editorLangId == plaintext" 66 | } 67 | ] 68 | }, 69 | "languages": [ 70 | { 71 | "id": "groq", 72 | "aliases": [ 73 | "GROQ", 74 | "groq" 75 | ], 76 | "extensions": [ 77 | ".groq" 78 | ], 79 | "configuration": "./language/language-configuration.json" 80 | } 81 | ], 82 | "grammars": [ 83 | { 84 | "language": "groq", 85 | "scopeName": "source.groq", 86 | "path": "./grammars/groq.json" 87 | }, 88 | { 89 | "injectTo": [ 90 | "source.js", 91 | "source.ts", 92 | "source.js.jsx", 93 | "source.tsx", 94 | "source.vue", 95 | "source.svelte", 96 | "source.php", 97 | "source.astro" 98 | ], 99 | "scopeName": "inline.groq", 100 | "path": "./grammars/groq.js.json", 101 | "embeddedLanguages": { 102 | "meta.embedded.block.groq": "groq" 103 | } 104 | }, 105 | { 106 | "scopeName": "markdown.groq.codeblock", 107 | "path": "./grammars/groq.md.json", 108 | "injectTo": [ 109 | "text.html.markdown" 110 | ], 111 | "embeddedLanguages": { 112 | "meta.embedded.block.groq": "javascript" 113 | } 114 | } 115 | ], 116 | "keybindings": [ 117 | { 118 | "command": "sanity.executeGroq", 119 | "key": "ctrl+shift+g", 120 | "mac": "ctrl+shift+g", 121 | "when": "editorTextFocus" 122 | } 123 | ] 124 | }, 125 | "dependencies": { 126 | "@sanity/client": "^6.21.1", 127 | "groq-js": "^1.12.0", 128 | "line-number": "^0.1.0", 129 | "osenv": "^0.1.5", 130 | "react": "^16.13.1", 131 | "react-dom": "^16.13.1", 132 | "react-jason": "^1.1.2", 133 | "ts-node": "^10.9.2", 134 | "xdg-basedir": "^4.0.0" 135 | }, 136 | "devDependencies": { 137 | "@semantic-release/git": "^10.0.1", 138 | "@semantic-release/npm": "^13.1.1", 139 | "@types/node": "^20.14.14", 140 | "@types/react": "^16.9.52", 141 | "@types/react-dom": "^16.9.8", 142 | "@types/vscode": "^1.91.0", 143 | "@vscode/vsce": "^3.6.2", 144 | "ovsx": "^0.10.6", 145 | "prettier": "^2.1.2", 146 | "semantic-release-vsce": "^6.0.12", 147 | "typescript": "5.5" 148 | }, 149 | "scripts": { 150 | "lint": "tsc --noEmit", 151 | "test": "npm run compile", 152 | "compile": "tsc -p ./", 153 | "watch": "tsc -watch -p ./", 154 | "env:source": "export $(cat .envrc | xargs)", 155 | "package": "npm run compile && vsce package --allow-star-activation", 156 | "release": "npm run compile && semantic-release", 157 | "release:dryrun": "npm run compile && semantic-release --dry-run", 158 | "upgrade-interactive": "npx npm-check -u" 159 | }, 160 | "volta": { 161 | "node": "22.20.0" 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as vscode from 'vscode' 3 | import {promises as fs, constants as fsconstants} from 'fs' 4 | import {parse} from 'groq-js' 5 | import {register as registerTsNode} from 'ts-node' 6 | import {Config} from './config/findConfig' 7 | import {GroqContentProvider} from './providers/content-provider' 8 | import {GROQCodeLensProvider} from './providers/groq-codelens-provider' 9 | import {executeGroq} from './query' 10 | 11 | export function activate(context: vscode.ExtensionContext) { 12 | // needed to load sanity.cli.ts 13 | registerTsNode() 14 | 15 | // Assigned by `readConfig()` 16 | let codelens: vscode.Disposable | undefined 17 | let useCodelens 18 | let openJSONFile 19 | let useCDN 20 | 21 | // Read and listen for configuration updates 22 | readConfig() 23 | vscode.workspace.onDidChangeConfiguration(() => readConfig()) 24 | 25 | let resultPanel: vscode.WebviewPanel | undefined 26 | let disposable = vscode.commands.registerCommand('sanity.executeGroq', async (groqQuery) => { 27 | let config: Config 28 | let query: string = groqQuery 29 | let params: Record = {} 30 | try { 31 | config = await loadSanityConfig() 32 | if (config === null) { 33 | return 34 | } 35 | 36 | if (!query) { 37 | query = await loadGroqFromFile() 38 | } 39 | const variables = findVariablesInQuery(query) 40 | if (variables.length > 0) { 41 | params = await readParams(variables) 42 | } 43 | 44 | vscode.window.showInformationMessage(`Executing GROQ query: ${query}`) 45 | // FIXME: Throw error object in webview? 46 | const {ms, result} = await executeGroq({ 47 | ...config.api, 48 | query, 49 | params, 50 | useCdn: config.api.token ? false : useCDN, 51 | }) 52 | 53 | vscode.window.setStatusBarMessage( 54 | `Query took ${ms}ms` + (useCDN ? ' with cdn' : ' without cdn'), 55 | 10000 56 | ) 57 | 58 | if (!openJSONFile && !resultPanel) { 59 | resultPanel = vscode.window.createWebviewPanel( 60 | 'executionResultsWebView', 61 | 'GROQ Execution Result', 62 | vscode.ViewColumn.Beside, 63 | {} 64 | ) 65 | 66 | resultPanel.onDidDispose(() => { 67 | resultPanel = undefined 68 | }) 69 | } 70 | 71 | if (openJSONFile) { 72 | await openInUntitled(result, 'json') 73 | } else if (resultPanel) { 74 | const contentProvider = await registerContentProvider( 75 | context, 76 | query, 77 | params, 78 | ms, 79 | result || [] 80 | ) 81 | const html = await contentProvider.getCurrentHTML() 82 | resultPanel.webview.html = html 83 | } 84 | } catch (err) { 85 | vscode.window.showErrorMessage(getErrorMessage(err)) 86 | return 87 | } 88 | }) 89 | context.subscriptions.push(disposable) 90 | 91 | function readConfig() { 92 | const settings = vscode.workspace.getConfiguration('sanity') 93 | openJSONFile = settings.get('openJSONFile', false) 94 | useCodelens = settings.get('useCodelens', true) 95 | useCDN = settings.get('useCDN', false) 96 | 97 | if (useCodelens && !codelens) { 98 | codelens = vscode.languages.registerCodeLensProvider( 99 | ['javascript', 'typescript', 'javascriptreact', 'typescriptreact', 'groq'], 100 | new GROQCodeLensProvider() 101 | ) 102 | 103 | context.subscriptions.push(codelens) 104 | } else if (!useCodelens && codelens) { 105 | const subIndex = context.subscriptions.indexOf(codelens) 106 | context.subscriptions.splice(subIndex, 1) 107 | codelens.dispose() 108 | codelens = undefined 109 | } 110 | } 111 | } 112 | 113 | async function loadSanityConfig() { 114 | const configFiles = await vscode.workspace.findFiles('**/sanity.cli.ts', '**/node_modules/**') 115 | if (configFiles.length === 0) { 116 | throw new Error('Could not resolve sanity.cli.ts configuration file') 117 | } 118 | let configFilePath: string | undefined = configFiles[0].fsPath 119 | 120 | // if there are multiple files, ask the user to pick one 121 | if (configFiles.length > 1) { 122 | const values = configFiles.map((value) => { 123 | const workspacePath = vscode.workspace.getWorkspaceFolder(value) 124 | const label = path.relative(workspacePath?.uri.fsPath || '', value.fsPath) 125 | return {label, value} 126 | }) 127 | 128 | configFilePath = await vscode.window 129 | .showQuickPick(values, {}) 130 | .then((selected) => selected?.value.fsPath) 131 | } 132 | 133 | // the user canceled the quick pick 134 | if (!configFilePath) { 135 | return null 136 | } 137 | 138 | const exists = await checkFileExists(configFilePath) 139 | if (!exists) { 140 | throw new Error('Could not resolve sanity.cli.ts configuration file') 141 | } 142 | 143 | // clear require cache to ensure we get the latest version 144 | delete require.cache[require.resolve(configFilePath)] 145 | 146 | const config = require(configFilePath) 147 | return config.default 148 | } 149 | 150 | async function loadGroqFromFile() { 151 | const activeTextEditor = vscode.window.activeTextEditor 152 | if (!activeTextEditor) { 153 | throw new Error('Nothing to execute') 154 | } 155 | 156 | return activeTextEditor.document.getText() 157 | } 158 | 159 | async function registerContentProvider( 160 | context: vscode.ExtensionContext, 161 | query: string, 162 | params: Record, 163 | ms: number, 164 | result: any 165 | ): Promise { 166 | const contentProvider = new GroqContentProvider(query, params, ms, result) 167 | const registration = vscode.workspace.registerTextDocumentContentProvider('groq', contentProvider) 168 | context.subscriptions.push(registration) 169 | return contentProvider 170 | } 171 | 172 | function getActiveFileName(): string { 173 | return vscode.window.activeTextEditor?.document.fileName || '' 174 | } 175 | 176 | async function checkFileExists(file) { 177 | return fs 178 | .access(file, fsconstants.F_OK) 179 | .then(() => true) 180 | .catch(() => false) 181 | } 182 | 183 | function findVariablesInQuery(query: string): string[] { 184 | return findVariables(parse(query), []) 185 | } 186 | 187 | function findVariables(node: any, found: string[]): string[] { 188 | if (node && node.type === 'Parameter' && typeof node.name === 'string') { 189 | return found.concat(node.name) 190 | } 191 | 192 | if (Array.isArray(node)) { 193 | return node.reduce((acc, child) => findVariables(child, acc), found) 194 | } 195 | 196 | if (typeof node !== 'object') { 197 | return found 198 | } 199 | 200 | return Object.keys(node).reduce((acc, key) => findVariables(node[key], acc), found) 201 | } 202 | 203 | async function readParamsFile(): Promise> { 204 | const activeFile = getActiveFileName() 205 | if (activeFile && activeFile !== '') { 206 | var pos = activeFile.lastIndexOf('.') 207 | const absoluteParamFile = activeFile.substring(0, pos < 0 ? activeFile.length : pos) + '.json' 208 | if (await checkFileExists(absoluteParamFile)) { 209 | try { 210 | const content = await fs.readFile(absoluteParamFile) 211 | return JSON.parse(content.toString()) 212 | } catch (err) { 213 | vscode.window.showErrorMessage(`Failed to read parameter file: ${getErrorMessage(err)}`) 214 | } 215 | } 216 | } 217 | 218 | return {} 219 | } 220 | 221 | async function readParams(variables: string[]): Promise> { 222 | const values: Record = await readParamsFile() 223 | const missing = variables.filter((variable) => !values[variable]) 224 | for (const variable of missing) { 225 | let value = await vscode.window.showInputBox({title: `value for "${variable}"`, value: ''}) 226 | if (!value) { 227 | continue 228 | } 229 | 230 | try { 231 | value = JSON.parse(value) 232 | } catch (err) { 233 | // noop 234 | } 235 | values[variable] = value 236 | } 237 | 238 | return values 239 | } 240 | 241 | async function openInUntitled(content: string, language?: string) { 242 | const cs = JSON.stringify(content) 243 | await vscode.workspace.openTextDocument({content: cs}).then((document) => { 244 | vscode.window.showTextDocument(document, {viewColumn: vscode.ViewColumn.Beside}) 245 | vscode.languages.setTextDocumentLanguage(document, language || 'json') 246 | }) 247 | } 248 | 249 | function getErrorMessage(err: unknown): string { 250 | if (err instanceof Error) { 251 | return err.message 252 | } 253 | 254 | if (typeof err === 'string') { 255 | return err 256 | } 257 | 258 | return 'An error occurred' 259 | } 260 | -------------------------------------------------------------------------------- /grammars/groq.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "GROQ", 4 | "scopeName": "source.groq", 5 | "patterns": [ 6 | { 7 | "include": "#query" 8 | }, 9 | { 10 | "include": "#value" 11 | }, 12 | { 13 | "include": "#pair" 14 | } 15 | ], 16 | "repository": { 17 | "query": { 18 | "patterns": [ 19 | { 20 | "include": "#nullary-access-operator" 21 | }, 22 | { 23 | "include": "#arraylike" 24 | }, 25 | { 26 | "include": "#pipe" 27 | }, 28 | { 29 | "include": "#sort-order" 30 | }, 31 | { 32 | "include": "#filter" 33 | } 34 | ] 35 | }, 36 | "variable": { 37 | "match": "\\$[_A-Za-z][_0-9A-Za-z]*", 38 | "name": "variable.other.groq" 39 | }, 40 | "keyword": { 41 | "match": "\\b(asc|desc|in|match)\\b", 42 | "name": "keyword.other.groq" 43 | }, 44 | "comparison": { 45 | "match": "(==|!=|>=|<=|(?|<)", 46 | "name": "keyword.operator.comparison.groq" 47 | }, 48 | "operator": { 49 | "match": "(\\+|-|\\*{1,2}|/|%)", 50 | "name": "keyword.operator.arithmetic.groq" 51 | }, 52 | "pipe": { 53 | "match": "\\|", 54 | "name": "keyword.operator.pipe.groq" 55 | }, 56 | "logical": { 57 | "match": "(!|&&|(\\|\\|))", 58 | "name": "keyword.operator.logical.groq" 59 | }, 60 | "reference": { 61 | "match": "\\->", 62 | "name": "keyword.operator.reference.groq" 63 | }, 64 | "pair": { 65 | "patterns": [ 66 | { 67 | "include": "#identifier" 68 | }, 69 | { 70 | "include": "#value" 71 | }, 72 | { 73 | "include": "#filter" 74 | }, 75 | { 76 | "match": "=>", 77 | "name": "keyword.operator.pair.groq" 78 | } 79 | ] 80 | }, 81 | "arraylike": { 82 | "begin": "\\[", 83 | "beginCaptures": { 84 | "0": { 85 | "name": "punctuation.definition.bracket.begin.groq" 86 | } 87 | }, 88 | "end": "\\](\\s*\\.)?", 89 | "endCaptures": { 90 | "0": { 91 | "name": "punctuation.definition.bracket.end.groq" 92 | }, 93 | "1": { 94 | "name": "keyword.operator.descendant.groq" 95 | } 96 | }, 97 | "patterns": [ 98 | { 99 | "include": "#range" 100 | }, 101 | { 102 | "include": "#filter" 103 | }, 104 | { 105 | "include": "#array-values" 106 | } 107 | ] 108 | }, 109 | "array": { 110 | "name": "meta.structure.array.groq", 111 | "begin": "\\[", 112 | "beginCaptures": { 113 | "0": { 114 | "name": "punctuation.definition.bracket.begin.groq" 115 | } 116 | }, 117 | "end": "\\]", 118 | "endCaptures": { 119 | "0": { 120 | "name": "punctuation.definition.bracket.end.groq" 121 | } 122 | }, 123 | "patterns": [ 124 | { 125 | "include": "#array-values" 126 | } 127 | ] 128 | }, 129 | "range": { 130 | "name": "meta.structure.range.groq", 131 | "match": "\\s*(\\d+)\\s*(\\.{2,3})\\s*(\\d+)\\s*", 132 | "captures": { 133 | "1": { 134 | "name": "constant.numeric.groq" 135 | }, 136 | "2": { 137 | "name": "keyword.operator.range.groq" 138 | }, 139 | "3": { 140 | "name": "constant.numeric.groq" 141 | } 142 | } 143 | }, 144 | "spread": { 145 | "name": "meta.structure.spread.groq", 146 | "begin": "\\.\\.\\.", 147 | "beginCaptures": { 148 | "0": { 149 | "name": "punctuation.definition.spread.begin.groq" 150 | } 151 | }, 152 | "end": "(?=.)", 153 | "applyEndPatternLast": 1, 154 | "endCaptures": { 155 | "0": { 156 | "name": "punctuation.definition.spread.end.groq" 157 | } 158 | }, 159 | "patterns": [ 160 | { 161 | "include": "#array" 162 | }, 163 | { 164 | "include": "#function-call" 165 | }, 166 | { 167 | "include": "#projection" 168 | } 169 | ] 170 | }, 171 | "array-values": { 172 | "name": "meta.structure.array-values.groq", 173 | "patterns": [ 174 | { 175 | "include": "#value" 176 | }, 177 | { 178 | "include": "#spread" 179 | }, 180 | { 181 | "match": ",", 182 | "name": "punctuation.separator.array.groq" 183 | }, 184 | { 185 | "match": "[^\\s\\]]", 186 | "name": "invalid.illegal.expected-array-separator.groq" 187 | } 188 | ] 189 | }, 190 | "filter": { 191 | "name": "meta.structure.filter.groq", 192 | "patterns": [ 193 | { 194 | "include": "#function-call" 195 | }, 196 | { 197 | "include": "#keyword" 198 | }, 199 | { 200 | "include": "#constant" 201 | }, 202 | { 203 | "include": "#identifier" 204 | }, 205 | { 206 | "include": "#value" 207 | }, 208 | { 209 | "include": "#comparison" 210 | }, 211 | { 212 | "include": "#operator" 213 | }, 214 | { 215 | "include": "#logical" 216 | } 217 | ] 218 | }, 219 | "comments": { 220 | "patterns": [ 221 | { 222 | "name": "comment.line.double-slash.js", 223 | "match": "(//).*$\\n?", 224 | "captures": { 225 | "1": { 226 | "name": "punctuation.definition.comment.groq" 227 | } 228 | } 229 | } 230 | ] 231 | }, 232 | "nullary-access-operator": { 233 | "match": "[*@^]", 234 | "name": "constant.language.groq" 235 | }, 236 | "constant": { 237 | "match": "\\b(?:true|false|null)\\b", 238 | "name": "constant.language.groq" 239 | }, 240 | "number": { 241 | "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", 242 | "name": "constant.numeric.groq" 243 | }, 244 | "named-projection": { 245 | "patterns": [ 246 | { 247 | "include": "#identifier" 248 | }, 249 | { 250 | "include": "#objectkey" 251 | }, 252 | { 253 | "include": "#projection" 254 | } 255 | ] 256 | }, 257 | "projection": { 258 | "begin": "\\{", 259 | "beginCaptures": { 260 | "0": { 261 | "name": "punctuation.definition.projection.begin.groq" 262 | } 263 | }, 264 | "end": "\\}", 265 | "endCaptures": { 266 | "0": { 267 | "name": "punctuation.definition.projection.end.groq" 268 | } 269 | }, 270 | "name": "meta.structure.projection.groq", 271 | "patterns": [ 272 | { 273 | "include": "#identifier" 274 | }, 275 | { 276 | "include": "#objectkey" 277 | }, 278 | { 279 | "include": "#named-projection" 280 | }, 281 | { 282 | "include": "#comments" 283 | }, 284 | { 285 | "include": "#spread" 286 | }, 287 | { 288 | "include": "#pair" 289 | }, 290 | { 291 | "begin": ":", 292 | "beginCaptures": { 293 | "0": { 294 | "name": "punctuation.separator.projection.key-value.groq" 295 | } 296 | }, 297 | "end": "(,)|(?=\\})", 298 | "endCaptures": { 299 | "1": { 300 | "name": "punctuation.separator.projection.pair.groq" 301 | } 302 | }, 303 | "name": "meta.structure.projection.value.groq", 304 | "patterns": [ 305 | { 306 | "include": "#nullary-access-operator" 307 | }, 308 | { 309 | "include": "#arraylike" 310 | }, 311 | { 312 | "include": "#value" 313 | }, 314 | { 315 | "include": "#spread" 316 | }, 317 | { 318 | "include": "#identifier" 319 | }, 320 | { 321 | "include": "#operator" 322 | }, 323 | { 324 | "include": "#comparison" 325 | }, 326 | { 327 | "include": "#pair" 328 | }, 329 | { 330 | "match": "[^\\s,]", 331 | "name": "invalid.illegal.expected-projection-separator.groq" 332 | } 333 | ] 334 | }, 335 | { 336 | "match": "[^\\s\\},]", 337 | "name": "invalid.illegal.expected-projection-separator.groq" 338 | } 339 | ] 340 | }, 341 | "string": { 342 | "name": "string.quoted.groq", 343 | "patterns": [ 344 | { 345 | "include": "#single-string" 346 | }, 347 | { 348 | "include": "#double-string" 349 | } 350 | ] 351 | }, 352 | "double-string": { 353 | "name": "string.quoted.double.groq", 354 | "begin": "\"", 355 | "beginCaptures": { 356 | "0": { 357 | "name": "punctuation.definition.string.begin.groq" 358 | } 359 | }, 360 | "end": "\"", 361 | "endCaptures": { 362 | "0": { 363 | "name": "punctuation.definition.string.end.groq" 364 | } 365 | }, 366 | "patterns": [ 367 | { 368 | "include": "#stringcontent" 369 | } 370 | ] 371 | }, 372 | "single-string": { 373 | "name": "string.quoted.single.groq", 374 | "begin": "'", 375 | "beginCaptures": { 376 | "0": { 377 | "name": "punctuation.definition.string.single.begin.groq" 378 | } 379 | }, 380 | "end": "'", 381 | "endCaptures": { 382 | "0": { 383 | "name": "punctuation.definition.string.single.end.groq" 384 | } 385 | }, 386 | "patterns": [ 387 | { 388 | "include": "#stringcontent" 389 | } 390 | ] 391 | }, 392 | "objectkey": { 393 | "name": "string.groq support.type.property-name.groq", 394 | "patterns": [ 395 | { 396 | "include": "#string" 397 | } 398 | ] 399 | }, 400 | "stringcontent": { 401 | "patterns": [ 402 | { 403 | "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", 404 | "name": "constant.character.escape.groq" 405 | }, 406 | { 407 | "match": "\\\\.", 408 | "name": "invalid.illegal.unrecognized-string-escape.groq" 409 | } 410 | ] 411 | }, 412 | "sort-pair": { 413 | "name": "attribute.sortpair.groq", 414 | "patterns": [ 415 | { 416 | "match": "([_A-Za-z][_0-9A-Za-z]*)(?:\\s*(asc|desc))?", 417 | "captures": { 418 | "1": { 419 | "name": "variable.other.readwrite.groq" 420 | }, 421 | "2": { 422 | "name": "keyword.other.groq" 423 | } 424 | } 425 | }, 426 | { 427 | "begin": "(@)(\\[)", 428 | "beginCaptures": { 429 | "1": { 430 | "name": "constant.language.groq" 431 | }, 432 | "2": { 433 | "name": "punctuation.definition.bracket.begin.groq" 434 | } 435 | }, 436 | "end": "(\\])(?:\\s*(asc|desc))?", 437 | "endCaptures": { 438 | "1": { 439 | "name": "punctuation.definition.bracket.begin.groq" 440 | }, 441 | "2": { 442 | "name": "keyword.other.groq" 443 | } 444 | }, 445 | "patterns": [ 446 | { 447 | "include": "#string" 448 | } 449 | ] 450 | } 451 | ] 452 | }, 453 | "sort-order": { 454 | "name": "support.function.sortorder.groq", 455 | "begin": "\\b(order)\\s*\\(", 456 | "beginCaptures": { 457 | "0": { 458 | "name": "support.function.sortorder.begin.groq" 459 | } 460 | }, 461 | "end": "\\)", 462 | "endCaptures": { 463 | "0": { 464 | "name": "support.function.sortorder.end.groq" 465 | } 466 | }, 467 | "patterns": [ 468 | { 469 | "include": "#sort-pair" 470 | }, 471 | { 472 | "match": ",", 473 | "name": "punctuation.separator.array.groq" 474 | }, 475 | { 476 | "match": "[^\\s\\]]", 477 | "name": "invalid.illegal.expected-sort-separator.groq" 478 | } 479 | ] 480 | }, 481 | "function-call": { 482 | "patterns": [ 483 | { 484 | "include": "#function-var-arg" 485 | }, 486 | { 487 | "include": "#function-single-arg" 488 | }, 489 | { 490 | "include": "#function-round" 491 | } 492 | ] 493 | }, 494 | "function-var-arg": { 495 | "name": "support.function.vararg.groq", 496 | "begin": "\\b(coalesce|select)\\s*\\(", 497 | "beginCaptures": { 498 | "0": { 499 | "name": "support.function.vararg.begin.groq" 500 | } 501 | }, 502 | "end": "\\)", 503 | "endCaptures": { 504 | "0": { 505 | "name": "support.function.vararg.end.groq" 506 | } 507 | }, 508 | "patterns": [ 509 | { 510 | "include": "#value" 511 | }, 512 | { 513 | "include": "#identifier" 514 | }, 515 | { 516 | "include": "#filter" 517 | }, 518 | { 519 | "include": "#pair" 520 | }, 521 | { 522 | "match": ",", 523 | "name": "punctuation.separator.array.groq" 524 | } 525 | ] 526 | }, 527 | "function-single-arg": { 528 | "name": "support.function.singlearg.groq", 529 | "begin": "\\b(count|defined|length|path|references)\\s*\\(", 530 | "beginCaptures": { 531 | "0": { 532 | "name": "support.function.singlearg.begin.groq" 533 | } 534 | }, 535 | "end": "\\)", 536 | "endCaptures": { 537 | "0": { 538 | "name": "support.function.singlearg.end.groq" 539 | } 540 | }, 541 | "patterns": [ 542 | { 543 | "include": "#query" 544 | }, 545 | { 546 | "include": "#identifier" 547 | }, 548 | { 549 | "include": "#value" 550 | }, 551 | { 552 | "include": "#pair" 553 | } 554 | ] 555 | }, 556 | "identifier": { 557 | "patterns": [ 558 | { 559 | "match": "([_A-Za-z][_0-9A-Za-z]*)\\s*(\\[\\s*\\])?\\s*(\\->)", 560 | "captures": { 561 | "1": { 562 | "name": "variable.other.readwrite.groq" 563 | }, 564 | "2": { 565 | "name": "punctuation.definition.block.js" 566 | }, 567 | "3": { 568 | "name": "keyword.operator.reference.groq" 569 | } 570 | } 571 | }, 572 | { 573 | "match": "(?:([_A-Za-z][_0-9A-Za-z]*)|([@^]))\\s*(\\[\\s*\\])?\\s*(\\.)", 574 | "captures": { 575 | "1": { 576 | "name": "variable.other.readwrite.groq" 577 | }, 578 | "2": { 579 | "name": "constant.language.groq" 580 | }, 581 | "3": { 582 | "name": "punctuation.definition.block.js" 583 | }, 584 | "4": { 585 | "name": "keyword.operator.descendant.groq" 586 | } 587 | } 588 | }, 589 | { 590 | "match": "[_A-Za-z][_0-9A-Za-z]*", 591 | "name": "variable.other.readwrite.groq" 592 | } 593 | ] 594 | }, 595 | "value": { 596 | "patterns": [ 597 | { 598 | "include": "#constant" 599 | }, 600 | { 601 | "include": "#number" 602 | }, 603 | { 604 | "include": "#string" 605 | }, 606 | { 607 | "include": "#array" 608 | }, 609 | { 610 | "include": "#variable" 611 | }, 612 | { 613 | "include": "#projection" 614 | }, 615 | { 616 | "include": "#comments" 617 | }, 618 | { 619 | "include": "#function-call" 620 | } 621 | ] 622 | } 623 | } 624 | } 625 | --------------------------------------------------------------------------------