├── .prettierrc.yaml ├── .gitignore ├── .vscodeignore ├── .gitattributes ├── resources ├── favicon.ico ├── prql-logo.png ├── sql-preview.html ├── sql-preview.css └── sqlPreview.js ├── docs └── images │ ├── prql-vscode.gif │ ├── prql-vscode.png │ ├── prql-settings.gif │ ├── prql-settings.png │ └── prql-vscode-features.png ├── examples ├── loop.prql ├── fibonacci-numbers.prql ├── .vscode │ └── settings.json ├── fibonacci-numbers.sql ├── loop.sql ├── pi.prql └── pi.sql ├── tsconfig.json ├── src ├── views │ ├── compilationResult.ts │ ├── viewContext.ts │ ├── sqlPreviewSerializer.ts │ └── sqlPreview.ts ├── constants.ts ├── extension.ts ├── diagnostics.ts ├── compiler.ts └── commands.ts ├── .github ├── dependabot.yml └── workflows │ ├── release.yaml │ └── pull-request.yaml ├── .vscode └── launch.json ├── language-configuration.json ├── .pre-commit-config.yaml ├── syntaxes ├── inline-prql.json └── prql.tmLanguage.json ├── eslint.config.mjs ├── CHANGELOG.md ├── package.json ├── README.md └── LICENSE /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.vsix 3 | out 4 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | .gitignore 4 | 5 | docs/images/** 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically normalize line endings. 2 | * text=auto 3 | -------------------------------------------------------------------------------- /resources/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PRQL/prql-vscode/HEAD/resources/favicon.ico -------------------------------------------------------------------------------- /resources/prql-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PRQL/prql-vscode/HEAD/resources/prql-logo.png -------------------------------------------------------------------------------- /docs/images/prql-vscode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PRQL/prql-vscode/HEAD/docs/images/prql-vscode.gif -------------------------------------------------------------------------------- /docs/images/prql-vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PRQL/prql-vscode/HEAD/docs/images/prql-vscode.png -------------------------------------------------------------------------------- /docs/images/prql-settings.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PRQL/prql-vscode/HEAD/docs/images/prql-settings.gif -------------------------------------------------------------------------------- /docs/images/prql-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PRQL/prql-vscode/HEAD/docs/images/prql-settings.png -------------------------------------------------------------------------------- /examples/loop.prql: -------------------------------------------------------------------------------- 1 | from_text format:json '[{"n": 1 }]' 2 | loop ( 3 | select n = n+1 4 | filter n<=3 5 | ) 6 | -------------------------------------------------------------------------------- /docs/images/prql-vscode-features.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PRQL/prql-vscode/HEAD/docs/images/prql-vscode-features.png -------------------------------------------------------------------------------- /examples/fibonacci-numbers.prql: -------------------------------------------------------------------------------- 1 | from_text format:json '[{"a":1, "b":1}]' 2 | loop ( 3 | derive b_new = a + b 4 | select {a=b, b=b_new} 5 | ) 6 | take 7 7 | -------------------------------------------------------------------------------- /examples/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "sqltools.connections": [ 3 | { 4 | "previewLimit": 50, 5 | "driver": "DuckDB", 6 | "name": "memory.duckdb", 7 | "database": ":memory:" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2020", 5 | "lib": ["es2020"], 6 | "outDir": "out", 7 | "sourceMap": true, 8 | "strict": true, 9 | "rootDir": "src" 10 | }, 11 | "exclude": ["node_modules"] 12 | } 13 | -------------------------------------------------------------------------------- /src/views/compilationResult.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * PRQL compilation resust for sql preview and display. 3 | */ 4 | export interface CompilationResult { 5 | status: 'ok' | 'error'; 6 | sql?: string; 7 | sqlHtml?: string; 8 | error?: { 9 | message: string; 10 | }; 11 | lastSqlHtml?: string | undefined; 12 | } 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | commit-message: 8 | prefix: 'chore: ' 9 | # We exclude labels throughout because of https://github.com/dependabot/dependabot-core/issues/7645#issuecomment-1986212847 10 | labels: [] 11 | 12 | - package-ecosystem: github-actions 13 | directory: '/' 14 | schedule: 15 | interval: daily 16 | commit-message: 17 | prefix: 'chore: ' 18 | labels: [] 19 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [released] 4 | workflow_dispatch: 5 | 6 | name: Publish extension 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v6 12 | - uses: actions/setup-node@v6 13 | with: 14 | node-version: 20 15 | - run: npm ci 16 | - uses: HaaLeo/publish-vscode-extension@v2 17 | with: 18 | pat: ${{ secrets.VS_MARKETPLACE_TOKEN }} 19 | registryUrl: https://marketplace.visualstudio.com 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"] 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/views/viewContext.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | /** 3 | * PRQL view context keys enum for when clauses and PRQL view menu commands. 4 | * 5 | * @see https://code.visualstudio.com/api/references/when-clause-contexts#add-a-custom-when-clause-context 6 | * @see https://code.visualstudio.com/api/references/when-clause-contexts#inspect-context-keys-utility 7 | */ 8 | export const enum ViewContext { 9 | SqlPreviewActive = 'prql.sqlPreviewActive', 10 | LastActivePrqlDocumentUri = 'prql.lastActivePrqlDocumentUri', 11 | } 12 | -------------------------------------------------------------------------------- /examples/fibonacci-numbers.sql: -------------------------------------------------------------------------------- 1 | WITH table_0 AS ( 2 | SELECT 3 | 1 AS a, 4 | 1 AS b 5 | ), 6 | table_4 AS ( 7 | WITH RECURSIVE loop AS ( 8 | SELECT 9 | a, 10 | b 11 | FROM 12 | table_0 AS table_1 13 | UNION 14 | ALL 15 | SELECT 16 | b AS _expr_0, 17 | a + b 18 | FROM 19 | loop AS table_2 20 | ) 21 | SELECT 22 | * 23 | FROM 24 | loop 25 | ) 26 | SELECT 27 | a, 28 | b 29 | FROM 30 | table_4 AS table_3 31 | LIMIT 32 | 7 33 | 34 | -- Generated by PRQL compiler version:0.6.1 target:sql.duckdb (https://prql-lang.org) 35 | -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "#" 4 | }, 5 | // symbols used as brackets 6 | "brackets": [ 7 | ["[", "]"], 8 | ["(", ")"], 9 | ["{", "}"] 10 | ], 11 | // symbols that are auto closed when typing 12 | "autoClosingPairs": [ 13 | ["[", "]"], 14 | ["(", ")"], 15 | ["{", "}"], 16 | ["\"", "\""], 17 | ["'", "'"] 18 | ], 19 | // symbols that can be used to surround a selection 20 | "surroundingPairs": [ 21 | ["[", "]"], 22 | ["(", ")"], 23 | ["{", "}"], 24 | ["\"", "\""], 25 | ["'", "'"] 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /examples/loop.sql: -------------------------------------------------------------------------------- 1 | WITH table_0 AS ( 2 | SELECT 3 | 1 AS n 4 | ), 5 | table_6 AS ( 6 | WITH RECURSIVE loop AS ( 7 | SELECT 8 | n 9 | FROM 10 | table_0 AS table_1 11 | UNION 12 | ALL 13 | SELECT 14 | _expr_0 15 | FROM 16 | ( 17 | SELECT 18 | n + 1 AS _expr_0 19 | FROM 20 | loop AS table_2 21 | ) AS table_3 22 | WHERE 23 | _expr_0 <= 3 24 | ) 25 | SELECT 26 | * 27 | FROM 28 | loop 29 | ) 30 | SELECT 31 | n 32 | FROM 33 | table_6 AS table_5 34 | 35 | -- Generated by PRQL compiler version:0.6.1 target:sql.duckdb (https://prql-lang.org) 36 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: mixed-line-ending 9 | - repo: https://github.com/crate-ci/typos 10 | rev: v1 11 | hooks: 12 | - id: typos 13 | # https://github.com/crate-ci/typos/issues/347 14 | pass_filenames: false 15 | - repo: https://github.com/pre-commit/mirrors-prettier 16 | rev: v4.0.0-alpha.8 17 | hooks: 18 | - id: prettier 19 | additional_dependencies: 20 | - prettier 21 | ci: 22 | # Currently network access isn't supported in the CI product. 23 | autoupdate_commit_msg: 'chore: pre-commit autoupdate' 24 | -------------------------------------------------------------------------------- /resources/sql-preview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | PRQL SQL Preview 16 | 17 | 18 |
Waiting for a PRQL file...
19 |
20 |

21 |       
22 |

23 |       
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yaml: -------------------------------------------------------------------------------- 1 | # Inspired by https://github.com/prql/prql/blob/663af5e1f166d9058ff88977cd7531b56b0ded1c/.github/workflows/pull-request.yaml 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, synchronize, labeled] 6 | push: 7 | branches: 8 | - main 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }}-${{ github.job }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v6 20 | - uses: actions/setup-node@v6 21 | with: 22 | node-version: 20 23 | - run: npm ci 24 | - run: npm run compile 25 | 26 | automerge-dependabot: 27 | runs-on: ubuntu-latest 28 | 29 | permissions: 30 | pull-requests: write 31 | contents: write 32 | 33 | steps: 34 | - uses: fastify/github-action-merge-dependabot@v3 35 | with: 36 | github-token: ${{ github.token }} 37 | use-github-auto-merge: true 38 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | 3 | /** 4 | * PRQL extension constants. 5 | */ 6 | 7 | // Extension constants 8 | export const PublisherId = 'PRQL-lang'; 9 | export const ExtensionName = 'prql-vscode'; 10 | export const ExtensionId = 'prql'; 11 | export const ExtensionDisplayName = 'PRQL'; 12 | 13 | // PRQL webview constants 14 | export const SqlPreviewPanel = `${ExtensionId}.sqlPreviewPanel`; 15 | export const SqlPreviewTitle = 'SQL Preview'; 16 | 17 | // PRQL extension command constants 18 | export const OpenSqlPreview = `${ExtensionId}.openSqlPreview`; 19 | export const GenerateSqlFile = `${ExtensionId}.generateSqlFile`; 20 | export const CopySqlToClipboard = `${ExtensionId}.copySqlToClipboard`; 21 | export const ViewSettings = `${ExtensionId}.viewSettings`; 22 | 23 | // PRQL extension setting keys 24 | export const AddCompilerSignatureComment = 'addCompilerSignatureComment'; 25 | export const AddTargetDialectToSqlFilenames = 'addTargetDialectToSqlFilenames'; 26 | 27 | // VSCodes actions and commands 28 | export const WorkbenchActionOpenSettings = 'workbench.action.openSettings'; 29 | -------------------------------------------------------------------------------- /syntaxes/inline-prql.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileTypes": [], 3 | "injectionSelector": "L:source.js -comment -(string -meta.embedded), L:source.jsx -comment -(string -meta.embedded), L:source.js.jsx -comment -(string -meta.embedded), L:source.ts -comment -(string -meta.embedded), L:source.tsx -comment -(string -meta.embedded)", 4 | "patterns": [ 5 | { 6 | "name": "string.js.taggedTemplate.commentTaggedTemplate.prql", 7 | "contentName": "meta.embedded.block.prql", 8 | "begin": "(?x)(\\b(?:\\w+\\.)*(?:prql)\\s*)(`)", 9 | "beginCaptures": { 10 | "1": { 11 | "name": "entity.name.function.tagged-template.js" 12 | }, 13 | "2": { 14 | "name": "punctuation.definition.string.template.begin.js" 15 | } 16 | }, 17 | "end": "(`)", 18 | "endCaptures": { 19 | "0": { 20 | "name": "string.js" 21 | }, 22 | "1": { 23 | "name": "punctuation.definition.string.template.end.js" 24 | } 25 | }, 26 | "patterns": [ 27 | { 28 | "include": "source.prql" 29 | }, 30 | { 31 | "match": "." 32 | } 33 | ] 34 | } 35 | ], 36 | "scopeName": "inline.prql" 37 | } 38 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import { window, ExtensionContext } from 'vscode'; 2 | 3 | import { SqlPreviewSerializer } from './views/sqlPreviewSerializer'; 4 | import { activateDiagnostics } from './diagnostics'; 5 | import { registerCommands } from './commands'; 6 | import { SqlPreview } from './views/sqlPreview'; 7 | 8 | /** 9 | * Activates PRQL extension, 10 | * enables PRQL text document diagnostics, 11 | * registers Open SQL Preview and other 12 | * PRQL extension commands. 13 | * 14 | * @param context Extension context. 15 | */ 16 | export function activate(context: ExtensionContext) { 17 | activateDiagnostics(context); 18 | registerCommands(context); 19 | 20 | // register sql preview serializer for restore on vscode reload 21 | context.subscriptions.push(SqlPreviewSerializer.register(context)); 22 | 23 | // add active text editor change handler 24 | context.subscriptions.push( 25 | window.onDidChangeActiveTextEditor((editor) => { 26 | if (editor && editor.document.languageId === 'prql') { 27 | // reveal the corresponding sql preview, if already open 28 | SqlPreview.reveal(context, editor.document.uri); 29 | } else { 30 | SqlPreview.clearActiveSqlPreviewContext(context); 31 | } 32 | }), 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from '@typescript-eslint/eslint-plugin'; 2 | import tsParser from '@typescript-eslint/parser'; 3 | import path from 'node:path'; 4 | import { fileURLToPath } from 'node:url'; 5 | import js from '@eslint/js'; 6 | import { FlatCompat } from '@eslint/eslintrc'; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | const compat = new FlatCompat({ 11 | baseDirectory: __dirname, 12 | recommendedConfig: js.configs.recommended, 13 | allConfig: js.configs.all, 14 | }); 15 | 16 | export default [ 17 | { 18 | ignores: ['**/out', '**/resources', 'eslint.config.mjs'], 19 | }, 20 | ...compat.extends( 21 | 'eslint:recommended', 22 | 'plugin:@typescript-eslint/recommended', 23 | ), 24 | { 25 | plugins: { 26 | '@typescript-eslint': typescriptEslint, 27 | }, 28 | 29 | languageOptions: { 30 | parser: tsParser, 31 | ecmaVersion: 5, 32 | sourceType: 'module', 33 | }, 34 | 35 | rules: { 36 | '@typescript-eslint/no-explicit-any': 'off', 37 | 'eol-last': ['error', 'always'], 38 | '@typescript-eslint/naming-convention': 'warn', 39 | curly: 'warn', 40 | eqeqeq: 'warn', 41 | 'no-throw-literal': 'warn', 42 | '@typescript-eslint/no-unused-vars': [ 43 | 'error', 44 | { 45 | argsIgnorePattern: '^_', 46 | }, 47 | ], 48 | }, 49 | }, 50 | ]; 51 | -------------------------------------------------------------------------------- /src/views/sqlPreviewSerializer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Disposable, 3 | ExtensionContext, 4 | WebviewPanel, 5 | WebviewPanelSerializer, 6 | Uri, 7 | window, 8 | } from 'vscode'; 9 | 10 | import { SqlPreview } from './sqlPreview'; 11 | import * as constants from '../constants'; 12 | 13 | /** 14 | * Sql Preview webview panel serializer for restoring open Sql Previews on vscode reload. 15 | */ 16 | export class SqlPreviewSerializer implements WebviewPanelSerializer { 17 | /** 18 | * Registers Sql Preview serializer. 19 | * 20 | * @param context Extension context. 21 | * @returns Disposable object for this webview panel serializer. 22 | */ 23 | public static register(context: ExtensionContext): Disposable { 24 | return window.registerWebviewPanelSerializer( 25 | constants.SqlPreviewPanel, 26 | new SqlPreviewSerializer(context), 27 | ); 28 | } 29 | 30 | /** 31 | * Creates new Sql Preview webview serializer. 32 | * 33 | * @param extensionUri Extension context. 34 | */ 35 | constructor(private readonly context: ExtensionContext) {} 36 | 37 | /** 38 | * Restores open Sql Preview webview panel on vscode reload. 39 | * 40 | * @param webviewPanel Webview panel to restore. 41 | * @param state Saved web view panel state with preview PRQL document Url. 42 | */ 43 | async deserializeWebviewPanel(webviewPanel: WebviewPanel, state: any) { 44 | const documentUri: Uri = Uri.file(state.documentUrl); 45 | SqlPreview.render(this.context, documentUri, webviewPanel); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /resources/sql-preview.css: -------------------------------------------------------------------------------- 1 | @media (prefers-color-scheme: light) { 2 | .shiki.dark-plus { 3 | display: none; 4 | } 5 | } 6 | 7 | @media (prefers-color-scheme: dark) { 8 | .shiki.light-plus { 9 | display: none; 10 | } 11 | } 12 | 13 | :root { 14 | --one-color: var(--vscode-focusBorder); 15 | --shiki-color-text: var(--vscode-editor-foreground); 16 | --shiki-color-background: var(--vscode-editor-background); 17 | --shiki-token-constant: var(--one-color); 18 | --shiki-token-string: var(--one-color); 19 | --shiki-token-comment: var(--one-color); 20 | --shiki-token-keyword: var(--one-color); 21 | --shiki-token-parameter: var(--one-color); 22 | --shiki-token-function: var(--one-color); 23 | --shiki-token-string-expression: var(--one-color); 24 | --shiki-token-punctuation: var(--one-color); 25 | --shiki-token-link: var(--one-color); 26 | } 27 | 28 | body, 29 | code { 30 | font-family: var(--vscode-editor-font-family) !important; 31 | font-size: var(--vscode-editor-font-size) !important; 32 | font-weight: var(--vscode-editor-font-weight) !important; 33 | } 34 | 35 | body { 36 | margin: 0px; 37 | padding: 10px; 38 | } 39 | 40 | h3 { 41 | margin: 10px 0 0 10px; 42 | color: var(--vscode-editorError-foreground); 43 | } 44 | 45 | .error { 46 | padding: 0px; 47 | margin: 0px; 48 | } 49 | 50 | #result { 51 | margin: 0px; 52 | padding: 0px; 53 | } 54 | 55 | #last-html { 56 | overflow-y: scroll; 57 | max-height: 80vh; 58 | margin: 0px; 59 | padding: 0px; 60 | } 61 | 62 | #error-container { 63 | overflow-x: scroll; 64 | color: var(--vscode-editorError-foreground); 65 | background-color: var(--vscode-editor-background); 66 | } 67 | 68 | .error-container-fixed { 69 | left: 10px; 70 | right: 10px; 71 | bottom: 0; 72 | position: fixed; 73 | border-top: 2px solid var(--vscode-editorError-foreground); 74 | } 75 | 76 | #error-message { 77 | padding: 0px; 78 | } 79 | -------------------------------------------------------------------------------- /src/diagnostics.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Diagnostic, 3 | DiagnosticCollection, 4 | DiagnosticSeverity, 5 | ExtensionContext, 6 | Position, 7 | Range, 8 | TextDocument, 9 | languages, 10 | window, 11 | workspace, 12 | } from 'vscode'; 13 | 14 | import { SourceLocation, compile } from './compiler'; 15 | 16 | function getRange(location: SourceLocation | null): Range { 17 | if (location) { 18 | return new Range( 19 | location.start[0], 20 | location.start[1], 21 | location.end[0], 22 | location.end[1], 23 | ); 24 | } 25 | return new Range(new Position(0, 0), new Position(0, 0)); 26 | } 27 | 28 | function updateLineDiagnostics(diagnosticCollection: DiagnosticCollection) { 29 | const editor = window.activeTextEditor; 30 | 31 | if (editor && editor.document.languageId === 'prql') { 32 | const text = editor.document.getText(); 33 | const result = compile(text); 34 | 35 | if (!Array.isArray(result)) { 36 | // success, clear the errors 37 | diagnosticCollection.set(editor.document.uri, []); 38 | } else { 39 | const diagnostics = result 40 | // don't report errors for missing main pipeline 41 | .filter((e) => e.code !== 'E0001') 42 | .map((e) => { 43 | return new Diagnostic( 44 | getRange(e.location), 45 | e.reason, 46 | DiagnosticSeverity.Error, 47 | ); 48 | }); 49 | 50 | diagnosticCollection.set(editor.document.uri, diagnostics); 51 | } 52 | } 53 | } 54 | 55 | export function activateDiagnostics(context: ExtensionContext) { 56 | const diagnosticCollection = languages.createDiagnosticCollection('prql'); 57 | context.subscriptions.push(diagnosticCollection); 58 | 59 | workspace.onDidCloseTextDocument((document: TextDocument) => 60 | diagnosticCollection.set(document.uri, []), 61 | ); 62 | 63 | [ 64 | workspace.onDidOpenTextDocument, 65 | workspace.onDidChangeTextDocument, 66 | window.onDidChangeActiveTextEditor, 67 | ].forEach((event) => { 68 | context.subscriptions.push( 69 | event(() => updateLineDiagnostics(diagnosticCollection)), 70 | ); 71 | }); 72 | 73 | updateLineDiagnostics(diagnosticCollection); 74 | } 75 | -------------------------------------------------------------------------------- /src/compiler.ts: -------------------------------------------------------------------------------- 1 | import { workspace, WorkspaceConfiguration } from 'vscode'; 2 | 3 | import * as prql from 'prqlc'; 4 | import * as constants from './constants'; 5 | 6 | export function compile(prqlString: string): string | ErrorMessage[] { 7 | // get prql settings 8 | const prqlSettings: WorkspaceConfiguration = 9 | workspace.getConfiguration('prql'); 10 | const target = prqlSettings.get('target'); 11 | const addCompilerInfo = ( 12 | prqlSettings.get(constants.AddCompilerSignatureComment) 13 | ); 14 | 15 | // create compile options from prql workspace settings 16 | const compileOptions = new prql.CompileOptions(); 17 | compileOptions.signature_comment = addCompilerInfo; 18 | if (target !== 'Any') { 19 | compileOptions.target = `sql.${target.toLowerCase()}`; 20 | } else { 21 | compileOptions.target = 'sql.any'; 22 | } 23 | 24 | try { 25 | // run prql compile 26 | return prql.compile(prqlString, compileOptions) as string; 27 | } catch (error) { 28 | if ((error as any)?.message) { 29 | try { 30 | const errorMessages = JSON.parse((error as any).message); 31 | console.log(errorMessages); 32 | return errorMessages.inner as ErrorMessage[]; 33 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 34 | } catch (_error) { 35 | throw error; 36 | } 37 | } 38 | throw error; 39 | } 40 | } 41 | 42 | export interface ErrorMessage { 43 | /// Message kind. Currently only Error is implemented. 44 | kind: 'Error' | 'Warning' | 'Lint'; 45 | /// Machine-readable identifier of the error 46 | code: string | null; 47 | /// Plain text of the error 48 | reason: string; 49 | /// A list of suggestions of how to fix the error 50 | hint: string | null; 51 | /// Character offset of error origin within a source file 52 | span: [number, number] | null; 53 | 54 | /// Annotated code, containing cause and hints. 55 | display: string | null; 56 | /// Line and column number of error origin within a source file 57 | location: SourceLocation | null; 58 | } 59 | 60 | /// Location within the source file. 61 | /// Tuples contain: 62 | /// - line number (0-based), 63 | /// - column number within that line (0-based), 64 | export interface SourceLocation { 65 | start: [number, number]; 66 | end: [number, number]; 67 | } 68 | -------------------------------------------------------------------------------- /examples/pi.prql: -------------------------------------------------------------------------------- 1 | prql target:sql.duckdb 2 | 3 | let config = ( 4 | from_text format:json '[{"num_digits":50}]' 5 | derive [ 6 | array_len = (10*num_digits)/3, 7 | calc_len = 1+4, 8 | loop_len = array_len + calc_len, 9 | ] 10 | ) 11 | 12 | func loop_steps step_0 step_i step_1 step_2 step_3 other -> case [ 13 | k==0 => step_0, 14 | 1 <= k and k <= array_len => step_i, 15 | k==array_len+1 => step_1, 16 | k==array_len+2 => step_2, 17 | k==array_len+3 => step_3, 18 | true => other, 19 | ] 20 | 21 | func q_steps step_q9 step_q10 step_j2 step_jg2 other -> case [ 22 | q==9 => step_q9, 23 | q==10 => step_q10, 24 | j==2 => step_j2, 25 | j>2 => step_jg2, 26 | true => other, 27 | ] 28 | 29 | 30 | from config 31 | select [ 32 | num_digits, 33 | array_len, 34 | loop_len, 35 | j = 0, 36 | k = 0, 37 | q = 0, 38 | a = s"[2 for i in range({array_len})]", 39 | nines = 0, 40 | predigit = 0, 41 | output = '', 42 | ] 43 | loop ( 44 | filter j < num_digits + 1 45 | derive [ 46 | j_new = case [k==0 => j+1, true => j], 47 | k_new = (k+1) % loop_len, 48 | q_step_i = (10*s"{a}[{k}]"+q*(array_len-k+1))/(2*(array_len-k)+1), 49 | q_new = loop_steps 0 q_step_i (q/10) q q q, 50 | 51 | a_step_i = s"[CASE WHEN i=={k} THEN (10*{a}[i]+{q}*({array_len}-i+1))%(2*({array_len}-i)+1) ELSE {a}[i] END for i in generate_series(1,{array_len})]", 52 | a_step_1 = s"[CASE WHEN i=={array_len} THEN {q}%10 ELSE {a}[i] END for i in generate_series(1,{array_len})]", 53 | a_new = loop_steps a a_step_i a_step_1 a a a, 54 | 55 | nines_new = loop_steps nines nines nines (q_steps (nines+1) 0 nines nines nines) (case [q!=9 and q!=10 and nines!=0 => 0, true => nines]) nines, 56 | predigit_new = loop_steps predigit predigit predigit (q_steps predigit 0 q q q) predigit predigit, 57 | 58 | output_step_2 = (q_steps '' s"({predigit}+1)::string || repeat('0', {nines})" s"{predigit}::string || '.'" s"{predigit}::string" ''), 59 | output_step_3 = (case [q!=9 and q!=10 and nines!=0 => s"repeat('9', {nines})", true => '']), 60 | output_new = loop_steps '' '' '' output_step_2 output_step_3 '', 61 | ] 62 | select [ 63 | num_digits, 64 | array_len, 65 | loop_len, 66 | j = j_new, 67 | k = k_new, 68 | q = q_new, 69 | a = a_new, 70 | nines = nines_new, 71 | predigit = predigit_new, 72 | output = output_new, 73 | ] 74 | ) 75 | aggregate [pi=s"string_agg({output}, '')"] 76 | -------------------------------------------------------------------------------- /resources/sqlPreview.js: -------------------------------------------------------------------------------- 1 | // initialize vscode api 2 | const vscode = acquireVsCodeApi(); 3 | 4 | // prql document vars and view state 5 | let documentUrl = ''; 6 | let viewState = { documentUrl: documentUrl }; 7 | 8 | // add page load handler 9 | window.addEventListener('load', initializeView); 10 | 11 | /** 12 | * Initializes sql preview webview. 13 | */ 14 | function initializeView() { 15 | // restore previous view state 16 | viewState = vscode.getState(); 17 | if (viewState && viewState.documentUrl) { 18 | // get last previewed prql document url 19 | documentUrl = viewState.documentUrl; 20 | } else { 21 | // create new empty view config 22 | viewState = {}; 23 | viewState.documentUrl = documentUrl; 24 | vscode.setState(viewState); 25 | } 26 | 27 | // request initial sql preview load 28 | vscode.postMessage({ command: 'refresh' }); 29 | } 30 | 31 | // add view update handler 32 | window.addEventListener('message', (event) => { 33 | switch (event.data.command) { 34 | case 'update': 35 | // show updated sql preview content 36 | update(event.data.result); 37 | break; 38 | case 'changeTheme': 39 | // do nothing: this webview html is UI theme neutral, 40 | // and will update sql html content on the next edit 41 | break; 42 | case 'refresh': 43 | updateViewState(event.data); 44 | break; 45 | default: 46 | throw new Error('unknown message'); 47 | } 48 | }); 49 | 50 | /** 51 | * Updates Sql Preview view state on initial view load and refresh. 52 | * 53 | * @param {*} prqlInfo Prql document info from webview. 54 | */ 55 | function updateViewState(prqlInfo) { 56 | // get and save prql document url in view state 57 | documentUrl = prqlInfo.documentUrl; 58 | viewState.documentUrl = documentUrl; 59 | vscode.setState(viewState); 60 | } 61 | 62 | /** 63 | * Displays updated sql html from compiled PRQL result. 64 | * 65 | * @param {*} compilationResult PRQL compilation result. 66 | */ 67 | function update(compilationResult) { 68 | // show updated sql preview content 69 | const result = compilationResult; 70 | const errorContainer = document.getElementById('error-container'); 71 | if (result.status === 'ok') { 72 | document.getElementById('result').innerHTML = result.sqlHtml; 73 | } else if (result.lastSqlHtml) { 74 | document.getElementById('last-html').innerHTML = result.lastSqlHtml; 75 | document 76 | .getElementById('error-container') 77 | .classList.add('error-container-fixed'); 78 | } 79 | 80 | if (result.status === 'error' && result.error.message.length > 0) { 81 | // show errors 82 | document.getElementById('error-message').innerHTML = result.error.message; 83 | errorContainer.style.display = 'block'; 84 | } else { 85 | // hide error container 86 | errorContainer.style.display = 'none'; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /examples/pi.sql: -------------------------------------------------------------------------------- 1 | WITH table_0 AS ( 2 | SELECT 3 | 50 AS num_digits 4 | ), 5 | config AS ( 6 | SELECT 7 | num_digits, 8 | 10 * num_digits / 3 AS array_len, 9 | 5 AS calc_len, 10 | 10 * num_digits / 3 + 5 AS loop_len 11 | FROM 12 | table_0 AS table_1 13 | ), 14 | table_6 AS ( 15 | WITH RECURSIVE loop AS ( 16 | SELECT 17 | num_digits, 18 | array_len, 19 | loop_len, 20 | 0 AS _expr_0, 21 | 0 AS _expr_1, 22 | 0 AS _expr_2, 23 | [2 for i in range(array_len)] AS _expr_3, 24 | 0 AS _expr_4, 25 | 0 AS _expr_5, 26 | '' AS _expr_6 27 | FROM 28 | config 29 | UNION 30 | ALL 31 | SELECT 32 | num_digits, 33 | array_len, 34 | loop_len, 35 | _expr_12 AS _expr_15, 36 | _expr_11 AS _expr_16, 37 | _expr_10 AS _expr_17, 38 | _expr_9 AS _expr_18, 39 | _expr_8 AS _expr_19, 40 | _expr_7 AS _expr_20, 41 | CASE 42 | WHEN _expr_1 = 0 THEN '' 43 | WHEN 1 <= _expr_1 44 | AND _expr_1 <= array_len THEN '' 45 | WHEN _expr_1 = array_len + 1 THEN '' 46 | WHEN _expr_1 = array_len + 2 THEN _expr_13 47 | WHEN _expr_1 = array_len + 3 THEN _expr_14 48 | ELSE '' 49 | END 50 | FROM 51 | ( 52 | SELECT 53 | num_digits, 54 | array_len, 55 | loop_len, 56 | CASE 57 | WHEN _expr_1 = 0 THEN _expr_5 58 | WHEN 1 <= _expr_1 59 | AND _expr_1 <= array_len THEN _expr_5 60 | WHEN _expr_1 = array_len + 1 THEN _expr_5 61 | WHEN _expr_1 = array_len + 2 THEN CASE 62 | WHEN _expr_2 = 9 THEN _expr_5 63 | WHEN _expr_2 = 10 THEN 0 64 | WHEN _expr_0 = 2 THEN _expr_2 65 | WHEN _expr_0 > 2 THEN _expr_2 66 | ELSE _expr_2 67 | END 68 | WHEN _expr_1 = array_len + 3 THEN _expr_5 69 | ELSE _expr_5 70 | END AS _expr_7, 71 | CASE 72 | WHEN _expr_1 = 0 THEN _expr_4 73 | WHEN 1 <= _expr_1 74 | AND _expr_1 <= array_len THEN _expr_4 75 | WHEN _expr_1 = array_len + 1 THEN _expr_4 76 | WHEN _expr_1 = array_len + 2 THEN CASE 77 | WHEN _expr_2 = 9 THEN _expr_4 + 1 78 | WHEN _expr_2 = 10 THEN 0 79 | WHEN _expr_0 = 2 THEN _expr_4 80 | WHEN _expr_0 > 2 THEN _expr_4 81 | ELSE _expr_4 82 | END 83 | WHEN _expr_1 = array_len + 3 THEN CASE 84 | WHEN _expr_2 <> 9 85 | AND _expr_2 <> 10 86 | AND _expr_4 <> 0 THEN 0 87 | ELSE _expr_4 88 | END 89 | ELSE _expr_4 90 | END AS _expr_8, 91 | CASE 92 | WHEN _expr_1 = 0 THEN _expr_3 93 | WHEN 1 <= _expr_1 94 | AND _expr_1 <= array_len THEN [CASE WHEN i==_expr_1 THEN (10*_expr_3[i] + _expr_2 *(array_len - i + 1) 95 | ) %(2 *(array_len - i) + 1) 96 | ELSE _expr_3 [i] 97 | END for i in generate_series(1, array_len) ] 98 | WHEN _expr_1 = array_len + 1 THEN [CASE WHEN i==array_len THEN _expr_2%10 ELSE _expr_3[i] 99 | END for i in generate_series(1, array_len) ] 100 | WHEN _expr_1 = array_len + 2 THEN _expr_3 101 | WHEN _expr_1 = array_len + 3 THEN _expr_3 102 | ELSE _expr_3 103 | END AS _expr_9, 104 | CASE 105 | WHEN _expr_1 = 0 THEN 0 106 | WHEN 1 <= _expr_1 107 | AND _expr_1 <= array_len THEN ( 108 | 10 * _expr_3 [_expr_1] + _expr_2 * (array_len - _expr_1 + 1) 109 | ) / (2 * (array_len - _expr_1) + 1) 110 | WHEN _expr_1 = array_len + 1 THEN _expr_2 / 10 111 | WHEN _expr_1 = array_len + 2 THEN _expr_2 112 | WHEN _expr_1 = array_len + 3 THEN _expr_2 113 | ELSE _expr_2 114 | END AS _expr_10, 115 | (_expr_1 + 1) % loop_len AS _expr_11, 116 | CASE 117 | WHEN _expr_1 = 0 THEN _expr_0 + 1 118 | ELSE _expr_0 119 | END AS _expr_12, 120 | _expr_1, 121 | CASE 122 | WHEN _expr_2 = 9 THEN '' 123 | WHEN _expr_2 = 10 THEN (_expr_5 + 1) :: string || repeat('0', _expr_4) 124 | WHEN _expr_0 = 2 THEN _expr_5 :: string || '.' 125 | WHEN _expr_0 > 2 THEN _expr_5 :: string 126 | ELSE '' 127 | END AS _expr_13, 128 | CASE 129 | WHEN _expr_2 <> 9 130 | AND _expr_2 <> 10 131 | AND _expr_4 <> 0 THEN repeat('9', _expr_4) 132 | ELSE '' 133 | END AS _expr_14, 134 | _expr_2, 135 | _expr_4 136 | FROM 137 | loop AS table_2 138 | WHERE 139 | _expr_0 < num_digits + 1 140 | ) AS table_3 141 | ) 142 | SELECT 143 | * 144 | FROM 145 | loop 146 | ) 147 | SELECT 148 | string_agg(_expr_6, '') AS pi 149 | FROM 150 | table_6 AS table_5 151 | 152 | -- Generated by PRQL compiler version:0.6.1 target:sql.duckdb (https://prql-lang.org) 153 | -------------------------------------------------------------------------------- /syntaxes/prql.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "PRQL", 4 | "scopeName": "source.prql", 5 | "fileTypes": ["prql"], 6 | "patterns": [ 7 | { 8 | "include": "#unicode-bidi" 9 | }, 10 | { 11 | "include": "#docblock" 12 | }, 13 | { 14 | "include": "#comment" 15 | }, 16 | { 17 | "include": "#constants" 18 | }, 19 | { 20 | "include": "#datatypes" 21 | }, 22 | { 23 | "include": "#keywords" 24 | }, 25 | { 26 | "include": "#keyword-operator" 27 | }, 28 | { 29 | "include": "#named-args" 30 | }, 31 | { 32 | "include": "#assigns" 33 | }, 34 | { 35 | "include": "#function-call" 36 | }, 37 | { 38 | "include": "#type-def" 39 | }, 40 | { 41 | "include": "#interpolation-strings" 42 | }, 43 | { 44 | "include": "#string-quoted-raw-single" 45 | }, 46 | { 47 | "include": "#string-quoted-raw-double" 48 | }, 49 | { 50 | "include": "#string-quoted-triple" 51 | }, 52 | { 53 | "include": "#string-quoted-single" 54 | }, 55 | { 56 | "include": "#string-quoted-double" 57 | }, 58 | { 59 | "include": "#time-units" 60 | }, 61 | { 62 | "include": "#ident" 63 | } 64 | ], 65 | "repository": { 66 | "docblock": { 67 | "name": "comment.block.documentation", 68 | "match": "#!.*$" 69 | }, 70 | "comment": { 71 | "name": "comment.line.number-sign", 72 | "match": "#.*$" 73 | }, 74 | "constants": { 75 | "name": "constant.language", 76 | "match": "true|false|null" 77 | }, 78 | "datatypes": { 79 | "name": "storage.type", 80 | "match": "(bool|int8|int16|int32|int64|int128|int|float|text|set)\\b" 81 | }, 82 | "escape": { 83 | "name": "constant.character.escape", 84 | "match": "\\\\." 85 | }, 86 | "keywords": { 87 | "patterns": [ 88 | { 89 | "name": "keyword.control.prql", 90 | "match": "\\b(let|into|case|prql|type|module|internal|from|from_text|select|derive|filter|take|sort|join|aggregate|group|null|true|false)\\b" 91 | } 92 | ] 93 | }, 94 | "keyword-operator": { 95 | "name": "keyword.operator", 96 | "match": "==|~=|\\+|-|\\*|!=|->|=>|<=|>=|&&|<|>" 97 | }, 98 | "time-units": { 99 | "name": "keyword.other.unit", 100 | "match": "years|months|weeks|days|hours|minutes|seconds|milliseconds|microseconds" 101 | }, 102 | "named-args": { 103 | "match": "(\\w+)\\s*:", 104 | "captures": { 105 | "1": { "name": "entity.name.tag" } 106 | } 107 | }, 108 | "assigns": { 109 | "match": "(\\w+)\\s*=(?!=)", 110 | "captures": { 111 | "1": { "name": "variable.name" } 112 | } 113 | }, 114 | "function-call": { 115 | "match": "\\b(\\w+)\\b(\\s+(\\w|[.])+)+(\\s|$|,|]|\\))", 116 | "captures": { 117 | "1": { 118 | "name": "support.function" 119 | }, 120 | "2": { 121 | "name": "variable.parameter" 122 | } 123 | } 124 | }, 125 | "type-def": { 126 | "name": "support.type", 127 | "match": "<\\w+>" 128 | }, 129 | "ident": { 130 | "name": "variable", 131 | "match": "\\b(\\w+)\\b" 132 | }, 133 | "interpolation-strings": { 134 | "name": "string.interpolated", 135 | "begin": "(s|f)\"", 136 | "end": "\"", 137 | "patterns": [ 138 | { 139 | "name": "keyword.operator.new", 140 | "match": "\\{[^}]*}" 141 | }, 142 | { 143 | "include": "#escape" 144 | } 145 | ] 146 | }, 147 | "unicode-bidi": { 148 | "name": "invalid.illegal", 149 | "match": "(\u202A|\u202B|\u202D|\u202E|\u2066|\u2067|\u2068|\u202C|\u2069)" 150 | }, 151 | "string-quoted-raw-single": { 152 | "name": "string.quoted", 153 | "begin": "r'", 154 | "end": "'" 155 | }, 156 | "string-quoted-raw-double": { 157 | "name": "string.quoted", 158 | "begin": "r\"", 159 | "end": "\"" 160 | }, 161 | "string-quoted-single": { 162 | "name": "string.quoted", 163 | "begin": "'", 164 | "end": "'", 165 | "patterns": [ 166 | { 167 | "include": "#escape" 168 | } 169 | ] 170 | }, 171 | "string-quoted-double": { 172 | "name": "string.quoted", 173 | "begin": "\"", 174 | "end": "\"", 175 | "patterns": [ 176 | { 177 | "include": "#escape" 178 | } 179 | ] 180 | }, 181 | "string-quoted-triple": { 182 | "name": "string.quoted", 183 | "begin": "\"\"\"", 184 | "end": "\"\"\"", 185 | "patterns": [ 186 | { 187 | "include": "#escape" 188 | } 189 | ] 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/commands.ts: -------------------------------------------------------------------------------- 1 | import { 2 | commands, 3 | env, 4 | window, 5 | workspace, 6 | Disposable, 7 | ExtensionContext, 8 | Uri, 9 | } from 'vscode'; 10 | 11 | import * as fs from 'fs'; 12 | import * as path from 'path'; 13 | 14 | import { SqlPreview } from './views/sqlPreview'; 15 | import { TextEncoder } from 'util'; 16 | import { compile } from './compiler'; 17 | import * as constants from './constants'; 18 | 19 | /** 20 | * Registers PRQL extension commands. 21 | * 22 | * @param context Extension context. 23 | */ 24 | export function registerCommands(context: ExtensionContext) { 25 | registerCommand(context, constants.ViewSettings, viewPrqlSettings); 26 | 27 | registerCommand(context, constants.GenerateSqlFile, (documentUri: Uri) => { 28 | const editor = window.activeTextEditor; 29 | if (!documentUri && editor && editor.document.languageId === 'prql') { 30 | // use prql from the active prql text editor 31 | generateSqlFile(editor.document.uri, editor.document.getText()); 32 | } else if ( 33 | documentUri && 34 | fs.existsSync(documentUri.fsPath) && 35 | documentUri.fsPath.endsWith('.prql') 36 | ) { 37 | // load prql code from the local prql file 38 | const prqlCode: string = fs.readFileSync(documentUri.fsPath, 'utf8'); 39 | generateSqlFile(documentUri, prqlCode); 40 | } 41 | }); 42 | 43 | registerCommand(context, constants.OpenSqlPreview, (documentUri: Uri) => { 44 | if (!documentUri && window.activeTextEditor) { 45 | // use active text editor document Uri 46 | documentUri = window.activeTextEditor.document.uri; 47 | } 48 | 49 | // render Sql Preview for the requested PRQL document 50 | SqlPreview.render(context, documentUri); 51 | }); 52 | 53 | registerCommand(context, constants.CopySqlToClipboard, () => { 54 | // get last generated prql sql content from workspace state 55 | let sql: string | undefined = context.workspaceState.get('prql.sql'); 56 | 57 | let sqlFileName = 'SQL'; 58 | if (SqlPreview.currentView) { 59 | // get sql filename and content fromn sql preview 60 | sqlFileName = `prql://${path.basename( 61 | SqlPreview.currentView.documentUri.path, 62 | '.prql', 63 | )}.sql`; 64 | sql = SqlPreview.currentView.lastCompilationResult?.sql; 65 | } 66 | 67 | if (sql !== undefined) { 68 | // write the last active sql preview sql code to vscode clipboard 69 | env.clipboard.writeText(sql); 70 | window.showInformationMessage(`Copied ${sqlFileName} to Clipboard.`); 71 | } 72 | }); 73 | } 74 | 75 | /** 76 | * Registers vscode extension command. 77 | * 78 | * @param context Extension context. 79 | * @param commandId Command identifier. 80 | * @param callback Command handler. 81 | * @param thisArg The `this` context used when invoking command handler. 82 | */ 83 | function registerCommand( 84 | context: ExtensionContext, 85 | commandId: string, 86 | callback: (...args: any[]) => any, 87 | thisArg?: any, 88 | ): void { 89 | const command: Disposable = commands.registerCommand( 90 | commandId, 91 | async (...args) => { 92 | try { 93 | await callback(...args); 94 | } catch (e: unknown) { 95 | window.showErrorMessage(String(e)); 96 | console.error(e); 97 | } 98 | }, 99 | thisArg, 100 | ); 101 | context.subscriptions.push(command); 102 | } 103 | 104 | /** 105 | * Opens vscode Settings panel with PRQL settings. 106 | */ 107 | async function viewPrqlSettings() { 108 | await commands.executeCommand( 109 | constants.WorkbenchActionOpenSettings, 110 | constants.ExtensionId, 111 | ); 112 | } 113 | 114 | /** 115 | * Compiles PRQL text for the given PRQL document uri, and creates 116 | * or updates the corresponding SQL file with PRQL compiler output. 117 | * 118 | * Opens generated SQL file in text editor for code formatting, 119 | * or running generated SQL statements with available 120 | * vscode database extensions and sql tools. 121 | * 122 | * @param prqlDocumentUri PRQL source document Uri. 123 | * @param prqlCode PRQL source code. 124 | */ 125 | async function generateSqlFile(prqlDocumentUri: Uri, prqlCode: string) { 126 | // compile given prql source code 127 | const sqlCode = compile(prqlCode); 128 | 129 | if (Array.isArray(sqlCode)) { 130 | // display prql compilation errors 131 | window.showErrorMessage(`PRQL Compile \ 132 | ${sqlCode[0].display ?? sqlCode[0].reason}`); 133 | } else { 134 | // get sql file generation prql settings 135 | const prqlSettings = workspace.getConfiguration('prql'); 136 | const target = prqlSettings.get('target'); 137 | const addTargetDialectToSqlFilenames = ( 138 | prqlSettings.get(constants.AddTargetDialectToSqlFilenames) 139 | ); 140 | 141 | // create sql filename based on prql file path, name, and current settings 142 | const prqlFilePath = path.parse(prqlDocumentUri.fsPath); 143 | let sqlFilenameSuffix = ''; 144 | if ( 145 | addTargetDialectToSqlFilenames && 146 | target !== 'Generic' && 147 | target !== 'Any' 148 | ) { 149 | sqlFilenameSuffix = `.${target.toLowerCase()}`; 150 | } 151 | const sqlFilePath = path.join( 152 | prqlFilePath.dir, 153 | `${prqlFilePath.name}${sqlFilenameSuffix}.sql`, 154 | ); 155 | 156 | // create sql file 157 | const sqlFileUri: Uri = Uri.file(sqlFilePath); 158 | const textEncoder: TextEncoder = new TextEncoder(); 159 | const sqlContent: Uint8Array = textEncoder.encode(sqlCode); 160 | await workspace.fs.writeFile(sqlFileUri, sqlContent); 161 | 162 | // show generated sql file 163 | await window.showTextDocument(sqlFileUri); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Small changes and releases to include recent PRQL versions are not recorded 4 | here. Instead see [releases](https://github.com/PRQL/prql-vscode/releases) for a 5 | brief summary of added extension features, extension source code zip archive, 6 | and `prql-vscode-x.x.x.vsix` extension package download. 7 | 8 | ## 0.13.0 - 2024-07-25 9 | 10 | - Bump to 0.13.0 11 | 12 | ## 0.12.2 - 2024-06-11 13 | 14 | - Bump to 0.12.2 15 | - Change dependency name to `prqlc` (from `prql-js`) 16 | 17 | ## 0.11.3 - 2024-02-12 18 | 19 | - Bump to 0.11.3 20 | 21 | ## 0.9.0 22 | 23 | ### Breaking changes 24 | 25 | - The `Hive` target option is removed, and the `None` target option is renamed to `Any`. 26 | 27 | ## 0.6.0 28 | 29 | - Refactor SQL Preview webview implementation 30 | ([#60](https://github.com/PRQL/prql-vscode/issues/60)) 31 | - Add VS Code marketplace badges to README.md 32 | ([#87](https://github.com/PRQL/prql-vscode/issues/87)) 33 | - Add PRQL Settings shortcut menu button to SQL Preview and PRQL Editor titlebar 34 | ([#90](https://github.com/PRQL/prql-vscode/issues/90)) 35 | - Remove prql-example.png from /resources 36 | ([#91](https://github.com/PRQL/prql-vscode/issues/91)) 37 | - Add PRQL compiler signature comment boolean setting to extension config 38 | ([#94](https://github.com/PRQL/prql-vscode/issues/94)) 39 | - Document new PRQL Settings under Configuration section in README.md 40 | ([#97](https://github.com/PRQL/prql-vscode/issues/97)) 41 | - Change prql.target extension setting default to Generic and add None option 42 | ([#98](https://github.com/PRQL/prql-vscode/issues/98)) 43 | - Set PRQL Settings order to show Target setting first 44 | ([#99](https://github.com/PRQL/prql-vscode/issues/99)) 45 | - Implement SQL Preview webview deserialize to show it after VS Code reload 46 | ([#102](https://github.com/PRQL/prql-vscode/issues/102)) 47 | - Add boolean prql.addTargetDialectToSqlFilenames setting for the generated SQL filenames 48 | ([#103](https://github.com/PRQL/prql-vscode/issues/103)) 49 | - Create and use separate SQL Preview webview for multiple open PRQL documents 50 | ([#108](https://github.com/PRQL/prql-vscode/issues/108)) 51 | - Display virtual sql filename in clipboard copy notification message 52 | ([#109](https://github.com/PRQL/prql-vscode/issues/109)) 53 | - Document new Sql Preview update release v0.6.0 features 54 | ([#111](https://github.com/PRQL/prql-vscode/issues/111)) 55 | - Allow to open Sql Preview for a .prql file from a menu in built-in vscode file explorer 56 | ([#113](https://github.com/PRQL/prql-vscode/issues/113)) 57 | - Allow to generate SQL file from a PRQL document in vscode file explorer 58 | ([#115](https://github.com/PRQL/prql-vscode/issues/115)) 59 | - Add Copy Sql to Clipboard menu option to PRQL editor and Sql Preview editor/title/context menus 60 | ([#116](https://github.com/PRQL/prql-vscode/issues/116)) 61 | - Add PRQL Settings menu to PRQL text editor/title/context menus 62 | ([#117](https://github.com/PRQL/prql-vscode/issues/117)) 63 | - Update CHANGELOG.md for the v0.6.0 release 64 | ([#120](https://github.com/PRQL/prql-vscode/issues/120)) 65 | 66 | ## 0.5.0 67 | 68 | - Use PRQL logo icon for `.prql` file extensions and display in file explorer 69 | and editor title bar ([#39](https://github.com/PRQL/prql-vscode/issues/39)) 70 | - Add PRQL to SQL context menus to PRQL editor title 71 | ([#41](https://github.com/PRQL/prql-vscode/issues/41)) 72 | - Add Generate SQL File command 73 | ([#42](https://github.com/PRQL/prql-vscode/issues/42)) 74 | - Rename PRQL - SQL Output panel to SQL Preview 75 | ([#46](https://github.com/PRQL/prql-vscode/issues/46)) 76 | - Add prql.target setting and use it to compile PRQL to SQL 77 | ([#48](https://github.com/PRQL/prql-vscode/issues/48)) 78 | - Provide Copy to Clipboard feature in Sql Preview 79 | ([#55](https://github.com/PRQL/prql-vscode/issues/55)) 80 | - Update prql-js to v0.5.0 and use new CompileOptions for the target 81 | ([#65](https://github.com/PRQL/prql-vscode/issues/65)) 82 | - Update CHANGELOG.md for v0.5.0 release 83 | ([#68](https://github.com/PRQL/prql-vscode/issues/68)) 84 | - Add PRQL extension to Data Science and Formatters categories 85 | ([#70](https://github.com/PRQL/prql-vscode/issues/70)) 86 | - Create and use new docs/images folder for extension features images in docs 87 | ([#72](https://github.com/PRQL/prql-vscode/issues/72)) 88 | - Update README.md with new features and settings in 0.5.0 version release 89 | ([#76](https://github.com/PRQL/prql-vscode/issues/76)) 90 | 91 | ## 0.4.2 92 | 93 | - Update [`prql-js`](https://github.com/PRQL/prql/tree/main/prql-js) compiler to 94 | version [0.4.2](https://github.com/PRQL/prql/releases/tag/0.4.2) 95 | 96 | ## 0.4.0 97 | 98 | - Upgrade the underlying compiler to 99 | [PRQL 0.4.0](https://github.com/PRQL/prql/releases/tag/0.4.0) 100 | - Detect PRQL based on language ID rather than file extension by 101 | [@jiripospisil](https://github.com/jiripospisil) 102 | ([#25](https://github.com/PRQL/prql-vscode/pull/25)) 103 | - Upgrade prql-js to 0.4.0 by [@aljazerzen](https://github.com/aljazerzen) 104 | ([#29](https://github.com/PRQL/prql-vscode/pull/29)) 105 | - Release on releases rather than tags by 106 | [@max-sixty](https://github.com/max-sixty) 107 | ([#23](https://github.com/PRQL/prql-vscode/pull/23)) 108 | 109 | ## 0.3.4 110 | 111 | - Bump `prql-js` to 0.3.0 112 | 113 | ## 0.3.3 114 | 115 | - Bump `prql-js` to 0.2.11 116 | 117 | ## 0.3.2 118 | 119 | - Provide PRQL diagnostics 120 | - Rename repo to `prql-vscode`, from `prql-code` 121 | - Add GitHub Action to test on each PR 122 | - Add GitHub Action to release on each tag 123 | 124 | ## 0.3.0 125 | 126 | - Live transpiling from PRQL to SQL in a side panel 127 | 128 | ## 0.2.0 129 | 130 | - Update grammar to PRQL version 0.2 131 | 132 | ## 0.1.0 133 | 134 | - Initial release 135 | - Add syntax highlighting for PRQL 136 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prql-vscode", 3 | "displayName": "PRQL", 4 | "description": "PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement", 5 | "repository": { 6 | "url": "https://github.com/prql/prql-vscode.git" 7 | }, 8 | "publisher": "prql-lang", 9 | "version": "0.13.0", 10 | "icon": "resources/prql-logo.png", 11 | "engines": { 12 | "vscode": "^1.65.0" 13 | }, 14 | "categories": [ 15 | "Data Science", 16 | "Formatters", 17 | "Programming Languages" 18 | ], 19 | "keywords": [ 20 | "data tools", 21 | "sql tools" 22 | ], 23 | "source": "src/extension.ts", 24 | "main": "out/extension.js", 25 | "activationEvents": [ 26 | "onLanguage:prql", 27 | "onWebviewPanel:prql.sqlPreviewPanel", 28 | "onCommand:prql.openSqlPreview", 29 | "onCommand:prql.generateSqlFile", 30 | "onCommand:prql.copySqlToClipboard", 31 | "onCommand:prql.viewSettings" 32 | ], 33 | "contributes": { 34 | "languages": [ 35 | { 36 | "id": "prql", 37 | "aliases": [ 38 | "PRQL", 39 | "prql" 40 | ], 41 | "extensions": [ 42 | ".prql" 43 | ], 44 | "configuration": "./language-configuration.json", 45 | "icon": { 46 | "dark": "./resources/prql-logo.png", 47 | "light": "./resources/prql-logo.png" 48 | } 49 | } 50 | ], 51 | "grammars": [ 52 | { 53 | "language": "prql", 54 | "scopeName": "source.prql", 55 | "path": "./syntaxes/prql.tmLanguage.json" 56 | }, 57 | { 58 | "injectTo": [ 59 | "source.js", 60 | "source.jsx", 61 | "source.ts", 62 | "source.tsx" 63 | ], 64 | "scopeName": "inline.prql", 65 | "path": "./syntaxes/inline-prql.json", 66 | "embeddedLanguages": { 67 | "meta.embedded.block.prql": "prql" 68 | } 69 | } 70 | ], 71 | "commands": [ 72 | { 73 | "command": "prql.openSqlPreview", 74 | "title": "Open SQL Preview", 75 | "category": "PRQL", 76 | "icon": "$(open-preview)" 77 | }, 78 | { 79 | "command": "prql.generateSqlFile", 80 | "title": "Generate SQL File", 81 | "category": "PRQL", 82 | "icon": "$(database)" 83 | }, 84 | { 85 | "command": "prql.copySqlToClipboard", 86 | "title": "Copy SQL to Clipboard", 87 | "category": "PRQL", 88 | "icon": "$(copy)" 89 | }, 90 | { 91 | "command": "prql.viewSettings", 92 | "title": "View PRQL Settings", 93 | "category": "PRQL", 94 | "icon": "$(gear)" 95 | } 96 | ], 97 | "menus": { 98 | "explorer/context": [ 99 | { 100 | "command": "prql.openSqlPreview", 101 | "when": "resourceLangId == prql", 102 | "group": "prql" 103 | }, 104 | { 105 | "command": "prql.generateSqlFile", 106 | "when": "resourceLangId == prql", 107 | "group": "prql" 108 | } 109 | ], 110 | "editor/title": [ 111 | { 112 | "command": "prql.openSqlPreview", 113 | "when": "resourceLangId == prql", 114 | "group": "navigation" 115 | }, 116 | { 117 | "command": "prql.generateSqlFile", 118 | "when": "resourceLangId == prql", 119 | "group": "navigation" 120 | }, 121 | { 122 | "command": "prql.viewSettings", 123 | "when": "resourceLangId == prql", 124 | "group": "navigation" 125 | }, 126 | { 127 | "command": "prql.copySqlToClipboard", 128 | "when": "prql.sqlPreviewActive || resourceLangId == prql", 129 | "group": "navigation" 130 | } 131 | ], 132 | "editor/title/context": [ 133 | { 134 | "command": "prql.openSqlPreview", 135 | "when": "resourceLangId == prql", 136 | "group": "prql" 137 | }, 138 | { 139 | "command": "prql.generateSqlFile", 140 | "when": "resourceLangId == prql", 141 | "group": "prql" 142 | }, 143 | { 144 | "command": "prql.viewSettings", 145 | "when": "resourceLangId == prql", 146 | "group": "prql" 147 | }, 148 | { 149 | "command": "prql.copySqlToClipboard", 150 | "when": "prql.sqlPreviewActive || resourceLangId == prql", 151 | "group": "prql" 152 | } 153 | ] 154 | }, 155 | "configuration": { 156 | "title": "PRQL", 157 | "type": "object", 158 | "properties": { 159 | "prql.target": { 160 | "type": "string", 161 | "enum": [ 162 | "Ansi", 163 | "BigQuery", 164 | "ClickHouse", 165 | "DuckDb", 166 | "Generic", 167 | "MsSql", 168 | "MySql", 169 | "Postgres", 170 | "SQLite", 171 | "Snowflake", 172 | "Any" 173 | ], 174 | "default": "Generic", 175 | "order": 0, 176 | "description": "PRQL compiler target dialect to use when generating SQL from pipeline definition files." 177 | }, 178 | "prql.addCompilerSignatureComment": { 179 | "type": "boolean", 180 | "default": true, 181 | "order": 1, 182 | "description": "Add PRQL compiler signature comment with SQL target dialect and compiler version to generated SQL." 183 | }, 184 | "prql.addTargetDialectToSqlFilenames": { 185 | "type": "boolean", 186 | "default": false, 187 | "order": 2, 188 | "description": "Add target dialect suffix to the generated SQL filenames." 189 | } 190 | } 191 | } 192 | }, 193 | "scripts": { 194 | "vscode:prepublish": "npm run compile", 195 | "compile": "eslint --max-warnings 0 . && tsc -p ./", 196 | "watch": "tsc -w -p ./" 197 | }, 198 | "devDependencies": { 199 | "@eslint/eslintrc": "^3.3.3", 200 | "@eslint/js": "^9.39.1", 201 | "@types/node": "^25.0.1", 202 | "@typescript-eslint/eslint-plugin": "^8.49.0", 203 | "@typescript-eslint/parser": "^8.49.0", 204 | "eslint": "^9.39.1", 205 | "typescript": "^5.9.3" 206 | }, 207 | "dependencies": { 208 | "@types/vscode": "^1.107.0", 209 | "prqlc": "^0.13.7", 210 | "shiki": "^0.14.7" 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PRQL extension for Visual Studio Code 2 | 3 | [![Apache-2.0 License](https://img.shields.io/badge/license-Apache2-brightgreen.svg)](http://opensource.org/licenses/Apache-2.0) 4 | [![Version](https://vsmarketplacebadges.dev/version-short/PRQL-lang.prql-vscode.svg?color=orange)](https://marketplace.visualstudio.com/items?itemName=PRQL-lang.prql-vscode) 5 | [![Installs](https://vsmarketplacebadges.dev/installs-short/PRQL-lang.prql-vscode.svg?color=orange)](https://marketplace.visualstudio.com/items?itemName=PRQL-lang.prql-vscode) 6 | [![Downloads](https://vsmarketplacebadges.dev/downloads-short/PRQL-lang.prql-vscode.svg?color=orange)](https://marketplace.visualstudio.com/items?itemName=PRQL-lang.prql-vscode) 7 | [![Rating](https://vsmarketplacebadges.dev/rating-short/PRQL-lang.prql-vscode.svg?color=orange)](https://marketplace.visualstudio.com/items?itemName=PRQL-lang.prql-vscode) 8 | 9 | PRQL is a modern language for transforming data — a simple, powerful, pipelined 10 | SQL replacement. 11 | 12 | This extension adds [PRQL](https://prql-lang.org/) support to VS Code. 13 | 14 | ![PRQL Editor and SQL Preview](https://github.com/PRQL/prql-vscode/blob/main/docs/images/prql-vscode.png?raw=true) 15 | 16 | ## Features 17 | 18 | - [PRQL](https://prql-lang.org/) language support and syntax highlighting 19 | - SQL Previews with Problems diagnostics and PRQL errors display updated on every keystroke as you type PRQL 20 | - Dedicated SQL Previews linked to open PRQL documents in VS Code editor 21 | - Restore open SQL Previews on VS Code reload 22 | - Copy SQL from an open SQL Preview to VS Code Clipboard 23 | - Generate SQL File PRQL editor context menu shortcut 24 | - View PRQL Settings editor context menu shortcut 25 | - PRQL compile target setting for the generated SQL dialect 26 | - Multi-target SQL generation and file naming options 27 | - Optional PRQL compiler signature comment append in generated SQL 28 | 29 | ![PRQL Features](https://github.com/PRQL/prql-vscode/blob/main/docs/images/prql-vscode.gif?raw=true) 30 | 31 | ### Feature Contributions 32 | 33 | PRQL extension contributes the following Settings, Commands, Languages and Activation Events to the VS Code: 34 | 35 | ![PRQL VS Code Feature Contributions](https://github.com/PRQL/prql-vscode/blob/main/docs/images/prql-vscode-features.png?raw=true) 36 | 37 | ## Configuration 38 | 39 | Modify 40 | [User or Workspace Settings](https://code.visualstudio.com/docs/getstarted/settings#_creating-user-and-workspace-settings) 41 | in VS Code to change the default PRQL extension Settings globally or only for the open project workspace. 42 | 43 | ![PRQL Extension Settings](https://github.com/PRQL/prql-vscode/blob/main/docs/images/prql-settings.png?raw=true) 44 | 45 | You can use new `View PRQL Settings` PRQL editor context menu shortcut to access and modify PRQL extension Settings: 46 | 47 | ![View PRQL Settings](https://github.com/PRQL/prql-vscode/blob/main/docs/images/prql-settings.gif?raw=true) 48 | 49 | ### PRQL Settings 50 | 51 | PRQL extension Settings allow you to customize PRQL [compiler options](https://github.com/PRQL/prql/tree/main/prqlc/bindings/js#usage) and filenames of the generated SQL files. Use the ⚙️ PRQL Settings shortcut from the open PRQL document editor context menu to access and change these configuration options. 52 | 53 | | Setting | Description | 54 | | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 55 | | `prql.target` | Specifies the default PRQL compiler target dialect to use when generating SQL from pipeline definition files (`.prql`) globally or in an open vscode project workspace. Defaults to `Generic`. | 56 | | `prql.addCompilerSignatureComment` | Adds `Generated by PRQL compiler version ...` signature comment with SQL target dialect info used to create SQL from PRQL document. Defaults to `true`. Set this setting to `false` to stop PRQL compiler from adding `Generated by ...` line to the end of the created SQL. | 57 | | `prql.addTargetDialectToSqlFilenames` | Adds target dialect suffix to the generated SQL filenames when `Generate Sql File` PRQL document command is used. Defaults to `false`. Set this setting to `true` when targeting multiple database systems with different SQL flavors. For example, projects using [`PostgreSQL`](https://www.postgresql.org/) transaction database and [`DuckDB`](https://duckdb.org/) OLAP database management system for analytics can use this option to generate different SQL from PRQL query documents. PRQL extension will save generated SQL documents as `*.postgre.sql` and `*.duckdb.sql` when using `Generate SQL File` command with the currently selected `prql.target` in PRQL Settings set to `Postgre` or `DuckDB`. | 58 | 59 | ### PRQL Target 60 | 61 | PRQL extension and the underlying [`prqlc-js`](https://github.com/PRQL/prql/tree/main/prqlc/bindings/js#usage) compiler used by this extension supports the following PRQL target dialect options: `Ansi`, `BigQuery`, `ClickHouse`, `DuckDb`, `Generic`, `MsSql`, `MySql`, `Postgres`, `SQLite`, `Snowflake`, and `Any`. 62 | 63 | The `prql.target` extension setting default option value is `Generic`, which will produce SQL that should work with most database management systems. We recommend you set it to the target DB you are working with in your project [workspace settings](https://code.visualstudio.com/docs/getstarted/settings#_creating-user-and-workspace-settings). 64 | 65 | You can also disable this PRQL compiler option in vscode extension by setting `prql.target` to `Any`. When `prql.target` is set to `Any`, PRQL compiler will read the target SQL dialect from `.prql` file header as described in [PRQL Language Book](https://prql-lang.org/book/project/target.html). For example, setting `prql.target` to `Any` and adding `prql target:sql.postgres` on the first line of your `.prql` query file will produce SQL for `PostgreSQL` database. Otherwise, `Generic` SQL flavor will be used for the generated SQL. 66 | 67 | ## Deploying the Extension 68 | 69 | This repo has the machinery to update the VSCode extension to the Microsoft Marketplace. 70 | 71 | When there is a new version of `prqlc` in `npm`, dependabot will PR an update. 72 | Once per day, the _.github/dependabot.yml_ file checks NPM and compares the `dependencies.prqlc` property in _package.json_ to the latest version in NPM. If they differ, dependabot creates a PR for _package.json_. 73 | 74 | Once that has been merged, the following manual steps will publish an update for the extension: 75 | 76 | - Update the [_CHANGELOG.md_](CHANGELOG.md) file, as needed 77 | 78 | - In _package.json_, update the `version` to match. This sets the version number of the extension itself. 79 | 80 | - Run `npm install` to update the `package-lock.json` 81 | 82 | - Create a new release from Github. This will start a workflow to release the current version to the VS Code Marketplace. 83 | 84 | - NB: From time to time, check the `node-version` in the files*.github/workflows/pull-request.yaml* and _.github/workflows/release.yml_. We track Node.js LTS - version 20 in June 2024. 85 | 86 | ## Developing the Extension 87 | 88 | - Clone the repository and install dependencies: 89 | 90 | ```sh 91 | git clone git@github.com:prql/prql-vscode.git 92 | cd prql-vscode && npm install 93 | ``` 94 | 95 | - Open the project in VS Code and start the TypeScript compilation task via 96 | `Command Palette` -> `Tasks: Run build task` -> `npm: watch`. Alternatively, 97 | you can run the compilation in your terminal directly: 98 | 99 | ```sh 100 | npm run watch 101 | ``` 102 | 103 | - Launch the extension in the Run and Debug panel. If you need to develop 104 | against a local version of `prql-js`, use `npm link` and restart the 105 | compilation task: 106 | 107 | ```sh 108 | npm link ../prql/prql-js 109 | ``` 110 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /src/views/sqlPreview.ts: -------------------------------------------------------------------------------- 1 | import { 2 | commands, 3 | window, 4 | workspace, 5 | Disposable, 6 | Event, 7 | ExtensionContext, 8 | TextDocument, 9 | TextDocumentChangeEvent, 10 | ViewColumn, 11 | Webview, 12 | WebviewPanel, 13 | WebviewPanelOnDidChangeViewStateEvent, 14 | Uri, 15 | } from 'vscode'; 16 | 17 | import * as shiki from 'shiki'; 18 | 19 | import { readFileSync } from 'node:fs'; 20 | import * as fs from 'fs'; 21 | import * as path from 'path'; 22 | 23 | import { ViewContext } from './viewContext'; 24 | import { CompilationResult } from './compilationResult'; 25 | 26 | import { compile } from '../compiler'; 27 | import * as constants from '../constants'; 28 | 29 | /** 30 | * Defines Sql Preview class for managing state and behaviour of Sql Preview webview panel(s). 31 | */ 32 | export class SqlPreview { 33 | // view tracking vars 34 | public static currentView: SqlPreview | undefined; 35 | private static _views: Map = new Map< 36 | string, 37 | SqlPreview 38 | >(); 39 | 40 | // view instance vars 41 | private readonly _webviewPanel: WebviewPanel; 42 | private readonly _documentUri: Uri; 43 | private readonly _viewUri: Uri; 44 | 45 | private _disposables: Disposable[] = []; 46 | private _highlighter: shiki.Highlighter | undefined; 47 | private _lastCompilationResult: CompilationResult | undefined; 48 | 49 | /** 50 | * Reveals current Sql Preview webview 51 | * or creates new Sql Preview webview panel 52 | * for the given PRQL document Uri 53 | * from an open and active PRQL document editor. 54 | * 55 | * @param context Extension context. 56 | * @param documentUri PRQL document Uri. 57 | * @param webviewPanel Optional webview panel instance. 58 | * @param viewConfig View config to restore. 59 | */ 60 | public static render( 61 | context: ExtensionContext, 62 | documentUri: Uri, 63 | webviewPanel?: WebviewPanel, 64 | ) { 65 | // attempt to reveal an open sql preview 66 | const sqlPreview: SqlPreview | undefined = SqlPreview.reveal( 67 | context, 68 | documentUri, 69 | ); 70 | 71 | if (sqlPreview === undefined) { 72 | if (!webviewPanel) { 73 | // create new webview panel for the prql document sql preview 74 | webviewPanel = SqlPreview.createWebviewPanel(context, documentUri); 75 | } else { 76 | // enable scripts for existing webview panel 77 | webviewPanel.webview.options = { 78 | enableScripts: true, 79 | enableCommandUris: true, 80 | }; 81 | } 82 | 83 | // create new sql peview and set it as current view 84 | SqlPreview.currentView = new SqlPreview( 85 | context, 86 | webviewPanel, 87 | documentUri, 88 | ); 89 | } 90 | 91 | sqlPreview?.updateActiveSqlPreviewContext(context); 92 | } 93 | 94 | /** 95 | * Reveals an open Sql Preview for a given PRQL document URI. 96 | * 97 | * @param context Extension context. 98 | * @param documentUri PRQL document Uri. 99 | * @returns The corresponding open Sql Preview for a PRQL document URI, or undefined. 100 | */ 101 | public static reveal( 102 | context: ExtensionContext, 103 | documentUri: Uri, 104 | ): SqlPreview | undefined { 105 | // create view Uri 106 | const viewUri: Uri = documentUri.with({ scheme: 'prql' }); 107 | 108 | // get an open sql preview 109 | const sqlPreview: SqlPreview | undefined = SqlPreview._views.get( 110 | viewUri.toString(true), 111 | ); // skip encoding 112 | 113 | if (sqlPreview !== undefined) { 114 | // show loaded webview panel 115 | sqlPreview.reveal(context); 116 | SqlPreview.currentView = sqlPreview; 117 | return sqlPreview; 118 | } else { 119 | SqlPreview.clearActiveSqlPreviewContext(context); 120 | } 121 | 122 | return undefined; 123 | } 124 | 125 | /** 126 | * clears active sql preview, context values and prql.sql in workspace state. 127 | * 128 | * @param context Extension context. 129 | */ 130 | public static clearActiveSqlPreviewContext(context: ExtensionContext) { 131 | SqlPreview.currentView = undefined; 132 | commands.executeCommand('setContext', ViewContext.SqlPreviewActive, false); 133 | commands.executeCommand( 134 | 'setContext', 135 | ViewContext.LastActivePrqlDocumentUri, 136 | undefined, 137 | ); 138 | context.workspaceState.update('prql.sql', undefined); 139 | } 140 | 141 | /** 142 | * Creates new webview panel for the given prql source document Uri. 143 | * 144 | * @param context Extension context. 145 | * @param documentUri PRQL source document Uri. 146 | * @returns New webview panel instance. 147 | */ 148 | private static createWebviewPanel( 149 | context: ExtensionContext, 150 | documentUri: Uri, 151 | ): WebviewPanel { 152 | // create sql preview filename for the webview panel title display 153 | const fileName = path.basename(documentUri.path, '.prql'); // strip out prql file ext. 154 | 155 | // create new sql preview webview panel 156 | const webviewPanel = window.createWebviewPanel( 157 | constants.SqlPreviewPanel, // webview panel view type 158 | `${fileName}.sql`, // webview panel title 159 | { 160 | viewColumn: ViewColumn.Beside, // display it on the side 161 | preserveFocus: true, 162 | }, 163 | { 164 | // webview panel options 165 | enableScripts: true, // enable JavaScript in webview 166 | enableCommandUris: true, 167 | enableFindWidget: true, 168 | retainContextWhenHidden: true, 169 | localResourceRoots: [Uri.joinPath(context.extensionUri, 'resources')], 170 | }, 171 | ); 172 | 173 | // set custom sql preview panel icon 174 | webviewPanel.iconPath = Uri.file( 175 | path.join(context.extensionUri.fsPath, 'resources', 'prql-logo.png'), 176 | ); 177 | 178 | return webviewPanel; 179 | } 180 | 181 | /** 182 | * Creates new SqlPreview webview panel instance. 183 | * 184 | * @param context Extension context. 185 | * @param webviewPanel Reference to the webview panel. 186 | * @param documentUri PRQL document Uri. 187 | */ 188 | private constructor( 189 | context: ExtensionContext, 190 | webviewPanel: WebviewPanel, 191 | documentUri: Uri, 192 | ) { 193 | // save view context info 194 | this._webviewPanel = webviewPanel; 195 | this._documentUri = documentUri; 196 | this._viewUri = documentUri.with({ scheme: 'prql' }); 197 | 198 | // configure webview panel 199 | this.configure(context); 200 | 201 | // add it to the tracked sql preview webviews 202 | SqlPreview._views.set(this._viewUri.toString(true), this); 203 | 204 | // update view context values on webview state change 205 | this._webviewPanel.onDidChangeViewState( 206 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 207 | (viewChangeEvent: WebviewPanelOnDidChangeViewStateEvent) => { 208 | if (this._webviewPanel.active) { 209 | // update view context values 210 | commands.executeCommand( 211 | 'setContext', 212 | ViewContext.SqlPreviewActive, 213 | true, 214 | ); 215 | commands.executeCommand( 216 | 'setContext', 217 | ViewContext.LastActivePrqlDocumentUri, 218 | documentUri, 219 | ); 220 | SqlPreview.currentView = this; 221 | } else { 222 | // clear sql preview context 223 | commands.executeCommand( 224 | 'setContext', 225 | ViewContext.SqlPreviewActive, 226 | false, 227 | ); 228 | SqlPreview.currentView = undefined; 229 | } 230 | }, 231 | ); 232 | 233 | // add prql text document change handler 234 | [ 235 | workspace.onDidOpenTextDocument, 236 | workspace.onDidChangeTextDocument, 237 | ].forEach((event: Event | Event) => { 238 | this._disposables.push( 239 | event( 240 | this.debounce(() => { 241 | this.update(context); 242 | }, 10), 243 | ), 244 | ); 245 | }); 246 | 247 | // add active text editor change handler 248 | this._disposables.push( 249 | window.onDidChangeActiveTextEditor((editor) => { 250 | if (editor && editor.document.uri.fsPath === this.documentUri.fsPath) { 251 | // clear sql preview context and recompile prql 252 | // from the linked and active PRQL editor 253 | // for the webview's PRQL source document 254 | this.clearSqlPreviewContext(context); 255 | this.update(context); 256 | } 257 | }), 258 | ); 259 | 260 | // add color theme change handler 261 | this._disposables.push( 262 | window.onDidChangeActiveColorTheme(() => { 263 | // reset highlighter 264 | this._highlighter = undefined; 265 | 266 | // notify webview 267 | webviewPanel.webview.postMessage({ command: 'changeTheme' }); 268 | }), 269 | ); 270 | 271 | // add dispose resources handler 272 | this._webviewPanel.onDidDispose(() => this.dispose(context)); 273 | } 274 | 275 | /** 276 | * Debounce for sql preview updates on prql text changes. 277 | * 278 | * @param fn 279 | * @param timeout 280 | * @returns 281 | */ 282 | private debounce(fn: () => any, timeout: number) { 283 | let timer: NodeJS.Timeout | undefined; 284 | return () => { 285 | clearTimeout(timer); 286 | timer = setTimeout(() => { 287 | fn(); 288 | }, timeout); 289 | }; 290 | } 291 | 292 | /** 293 | * Disposes Sql Preview webview resources when webview panel is closed. 294 | */ 295 | public dispose(context: ExtensionContext) { 296 | SqlPreview.currentView = undefined; 297 | SqlPreview._views.delete(this._viewUri.toString(true)); // skip encoding 298 | this._disposables.forEach((d) => d.dispose()); 299 | this.clearSqlPreviewContext(context); 300 | } 301 | 302 | /** 303 | * Reveals loaded Sql Preview and sets it as active in vscode editor panel. 304 | * 305 | * @param context Extension context. 306 | */ 307 | public reveal(context: ExtensionContext) { 308 | const viewColumn: ViewColumn = ViewColumn.Active 309 | ? ViewColumn.Active 310 | : ViewColumn.One; 311 | this.webviewPanel.reveal(viewColumn, true); // preserve current active editor focus 312 | this.updateActiveSqlPreviewContext(context); 313 | } 314 | 315 | /** 316 | * Configures webview html for Sql Preview display, 317 | * and registers webview message request handlers for updates. 318 | * 319 | * @param context Extension context. 320 | * @param viewConfig Sql Preview config. 321 | */ 322 | private async configure(context: ExtensionContext) { 323 | // set view html content for the webview panel 324 | this.webviewPanel.webview.html = this.getHtmlTemplate( 325 | context, 326 | this.webviewPanel.webview, 327 | ); 328 | 329 | // process webview messages 330 | this.webviewPanel.webview.onDidReceiveMessage( 331 | (message: any) => { 332 | const command: string = message.command; 333 | switch (command) { 334 | case 'refresh': 335 | // reload data view and config 336 | this.refresh(); 337 | break; 338 | } 339 | }, 340 | undefined, 341 | this._disposables, 342 | ); 343 | 344 | let prqlCode = undefined; 345 | if ( 346 | fs.existsSync(this.documentUri.fsPath) && 347 | this.documentUri.fsPath.endsWith('.prql') 348 | ) { 349 | // load initial prql code from file 350 | const prqlContent: Uint8Array = await workspace.fs.readFile( 351 | Uri.file(this.documentUri.fsPath), 352 | ); 353 | 354 | const textDecoder = new TextDecoder('utf8'); 355 | prqlCode = textDecoder.decode(prqlContent); 356 | } 357 | 358 | // update webview 359 | this.refresh(); 360 | this.update(context, prqlCode); 361 | } 362 | 363 | /** 364 | * Loads and creates html template for Sql Preview webview. 365 | * 366 | * @param context Extension context. 367 | * @param webview Sql Preview webview. 368 | * @returns Html template to use for Sql Preview webview. 369 | */ 370 | private getHtmlTemplate(context: ExtensionContext, webview: Webview): string { 371 | // load webview html template, stylesheet and sql preview script 372 | const htmlTemplate = readFileSync( 373 | this.getResourceUri(context, 'sql-preview.html').fsPath, 374 | 'utf-8', 375 | ); 376 | const stylesheetUri: Uri = this.getResourceUri(context, 'sql-preview.css'); 377 | const scriptUri: Uri = this.getResourceUri(context, 'sqlPreview.js'); 378 | 379 | // inject webview resource urls into the loaded webview html template, 380 | // add webview CSP source, and convert css and js Uris to webview resource Uris 381 | return htmlTemplate 382 | .replace(/##CSP_SOURCE##/g, webview.cspSource) 383 | .replace('##CSS_URI##', webview.asWebviewUri(stylesheetUri).toString()) 384 | .replace('##JS_URI##', webview.asWebviewUri(scriptUri).toString()); 385 | } 386 | 387 | /** 388 | * Gets webview resource Uri from extension directory. 389 | * 390 | * @param context Extension context. 391 | * @param filename Resource filename to create resource Uri. 392 | * @returns Webview resource Uri. 393 | */ 394 | private getResourceUri(context: ExtensionContext, fileName: string) { 395 | return Uri.joinPath(context.extensionUri, 'resources', fileName); 396 | } 397 | 398 | /** 399 | * Reloads Sql Preview for the active PRQL document Uri or on vscode IDE reload. 400 | */ 401 | public async refresh(): Promise { 402 | // update view state 403 | this.webviewPanel.webview.postMessage({ 404 | command: 'refresh', 405 | documentUrl: this.documentUri.fsPath, 406 | }); 407 | } 408 | 409 | /** 410 | * Updates Sql Preview with new PRQL compilation results 411 | * from the active PRQL text editor. 412 | * 413 | * @param context Extension context. 414 | * @param prqlCode Optional prql code overwrite to use 415 | * instead of text from the active vscode PRQL editor. 416 | */ 417 | private update(context: ExtensionContext, prqlCode?: string) { 418 | // check active text editor 419 | const editor = window.activeTextEditor; 420 | if ( 421 | this.webviewPanel.visible && 422 | editor && 423 | editor.document.languageId === 'prql' && 424 | editor.document.uri.fsPath === this.documentUri.fsPath 425 | ) { 426 | // get updated prql code from the active PRQL text editor 427 | const prqlCode = editor.document.getText(); 428 | this.processPrql(context, prqlCode); 429 | } else if (prqlCode) { 430 | this.processPrql(context, prqlCode); 431 | } 432 | } 433 | 434 | /** 435 | * Updates current/active SQL Preview context and view state. 436 | * 437 | * @param context Extension context. 438 | */ 439 | private async updateActiveSqlPreviewContext(context: ExtensionContext) { 440 | commands.executeCommand('setContext', ViewContext.SqlPreviewActive, true); 441 | commands.executeCommand( 442 | 'setContext', 443 | ViewContext.LastActivePrqlDocumentUri, 444 | this.documentUri, 445 | ); 446 | // update active sql preview sql text in workspace state 447 | context.workspaceState.update('prql.sql', this._lastCompilationResult?.sql); 448 | } 449 | 450 | /** 451 | * Clears SQL Preview context and view state. 452 | * 453 | * @param context Extension context. 454 | */ 455 | private async clearSqlPreviewContext(context: ExtensionContext) { 456 | commands.executeCommand('setContext', ViewContext.SqlPreviewActive, false); 457 | context.workspaceState.update('prql.sql', undefined); 458 | } 459 | 460 | /** 461 | * Processes given prql code. 462 | * 463 | * @param context Extension context. 464 | * @param prqlCode PRQL code to process. 465 | */ 466 | private async processPrql(context: ExtensionContext, prqlCode: string) { 467 | this.compilePrql(prqlCode, this._lastCompilationResult?.lastSqlHtml).then( 468 | (compilationResult) => { 469 | if (compilationResult.status === 'ok') { 470 | // save the last valid compilation result 471 | // to show it when errors occur later, 472 | // or on sql preview panel reveal 473 | this._lastCompilationResult = compilationResult; 474 | } 475 | 476 | // update webview 477 | this.webviewPanel.webview.postMessage({ 478 | command: 'update', 479 | result: compilationResult, 480 | }); 481 | 482 | this.updateActiveSqlPreviewContext(context); 483 | }, 484 | ); 485 | } 486 | 487 | /** 488 | * Compiles prql code and returns generated sql, 489 | * and formatted html sql compilation result. 490 | * 491 | * @param prqlCode PRQL code to compile. 492 | * @param lastSqlHtml Last valid sql html output. 493 | * @returns Compilation result in sql and html formats. 494 | */ 495 | private async compilePrql( 496 | prqlCode: string, 497 | lastSqlHtml: string | undefined, 498 | ): Promise { 499 | // compile given prql code 500 | const sqlCode = compile(prqlCode); 501 | if (Array.isArray(sqlCode)) { 502 | // return last valid sql html output with new error info 503 | return { 504 | status: 'error', 505 | error: { 506 | message: sqlCode[0].display ?? sqlCode[0].reason, 507 | }, 508 | lastSqlHtml: lastSqlHtml, 509 | }; 510 | } 511 | 512 | // create html to display for the generated sql 513 | const highlighter = await this.getHighlighter(); 514 | const sqlHtml = highlighter.codeToHtml(sqlCode, { lang: 'sql' }); 515 | 516 | return { status: 'ok', sqlHtml: sqlHtml, sql: sqlCode }; 517 | } 518 | 519 | /** 520 | * Gets shiki code highlighter instance to create html formatted sql output. 521 | * 522 | * @returns Shiki highlighter instance with UI theme matching vscode color theme. 523 | */ 524 | private async getHighlighter(): Promise { 525 | if (this._highlighter) { 526 | return Promise.resolve(this._highlighter); 527 | } 528 | return (this._highlighter = await shiki.getHighlighter({ 529 | theme: this.themeName, 530 | })); 531 | } 532 | 533 | /** 534 | * Gets shiki highlighter theme name that matches current vscode color theme 535 | * to use it as the UI theme for this Sql Preview webview. 536 | */ 537 | get themeName(): string { 538 | // get current vscode color UI theme name 539 | let colorTheme = workspace 540 | .getConfiguration('workbench') 541 | .get('colorTheme', 'dark-plus'); // default to dark plus 542 | 543 | if (shiki.BUNDLED_THEMES.includes(colorTheme as shiki.Theme)) { 544 | return colorTheme; 545 | } 546 | 547 | // try normalized color theme name 548 | colorTheme = colorTheme 549 | .toLowerCase() 550 | .replace('theme', '') 551 | .replace(/\s+/g, '-'); 552 | if (shiki.BUNDLED_THEMES.includes(colorTheme as shiki.Theme)) { 553 | return colorTheme; 554 | } 555 | 556 | // ??? not sure what this means, or does. 557 | // Does it use the loaded vscode CSS vars 558 | // when no color theme is set? 559 | return 'css-variables'; 560 | } 561 | 562 | /** 563 | * Gets the last valid compilation result for this sql preview. 564 | */ 565 | get lastCompilationResult(): CompilationResult | undefined { 566 | return this._lastCompilationResult; 567 | } 568 | 569 | /** 570 | * Gets the underlying webview panel instance for this view. 571 | */ 572 | get webviewPanel(): WebviewPanel { 573 | return this._webviewPanel; 574 | } 575 | 576 | /** 577 | * Gets view panel visibility status. 578 | */ 579 | get visible(): boolean { 580 | return this._webviewPanel.visible; 581 | } 582 | 583 | /** 584 | * Gets the source document uri for this view. 585 | */ 586 | get documentUri(): Uri { 587 | return this._documentUri; 588 | } 589 | 590 | /** 591 | * Gets the view uri to load on sql preview command triggers or vscode IDE reload. 592 | */ 593 | get viewUri(): Uri { 594 | return this._viewUri; 595 | } 596 | } 597 | --------------------------------------------------------------------------------