From f42ffda3751297151ceca18cfb6d6dc7d023b075 Mon Sep 17 00:00:00 2001 From: RandomFractals Date: Fri, 17 Feb 2023 09:07:25 -0600 Subject: [PATCH 01/32] add new ViewContext string enum for the prql view context keys with new ActivePrqlDocumentUri key we'll use to save prql doc. reference to restore sql preview --- src/constants.ts | 3 --- src/sql_output/index.ts | 5 +++-- src/views/viewContext.ts | 11 +++++++++++ 3 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 src/views/viewContext.ts diff --git a/src/constants.ts b/src/constants.ts index 69c34f7..295bc92 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -24,8 +24,5 @@ export const ViewSettings = `${ExtensionId}.viewSettings`; export const AddCompilerSignatureComment = 'addCompilerSignatureComment'; export const AddTargetDialectToSqlFilenames = 'addTargetDialectToSqlFilenames'; -// PRQL context keys -export const SqlPreviewActive = `${ExtensionId}.sqlPreviewActive`; - // VSCodes actions and commands export const WorkbenchActionOpenSettings = 'workbench.action.openSettings'; diff --git a/src/sql_output/index.ts b/src/sql_output/index.ts index fc087af..fe24038 100644 --- a/src/sql_output/index.ts +++ b/src/sql_output/index.ts @@ -21,6 +21,7 @@ import { normalizeThemeName, } from './utils'; +import { ViewContext } from '../views/viewContext'; import { isPrqlDocument } from '../utils'; import { compile } from '../compiler'; import * as constants from '../constants'; @@ -96,7 +97,7 @@ async function compilePrql( } function clearSqlContext(context: ExtensionContext) { - commands.executeCommand('setContext', constants.SqlPreviewActive, false); + commands.executeCommand('setContext', ViewContext.SqlPreviewActive, false); context.workspaceState.update('prql.sql', undefined); } @@ -114,7 +115,7 @@ function sendText(context: ExtensionContext, panel: WebviewPanel) { panel.webview.postMessage(result); // set sql preview flag and update sql output - commands.executeCommand('setContext', constants.SqlPreviewActive, true); + commands.executeCommand('setContext', ViewContext.SqlPreviewActive, true); context.workspaceState.update('prql.sql', result.sql); }); } diff --git a/src/views/viewContext.ts b/src/views/viewContext.ts new file mode 100644 index 0000000..505f6a5 --- /dev/null +++ b/src/views/viewContext.ts @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +/** + * PRQL view context keys enum for when clauses and PRQL view menu commands. + * + * @see https://code.visualstudio.com/api/references/when-clause-contexts#add-a-custom-when-clause-context + * @see https://code.visualstudio.com/api/references/when-clause-contexts#inspect-context-keys-utility + */ +export const enum ViewContext { + SqlPreviewActive = 'prql.sqlPreviewActive', + ActivePrqlDocumentUri = 'prql.activePrqlDocumentUri' +} From 54ebeb4e0faf28511fc433cd4297f3e8a38445b0 Mon Sep 17 00:00:00 2001 From: RandomFractals Date: Fri, 17 Feb 2023 09:12:07 -0600 Subject: [PATCH 02/32] set active prql document uri context key on active prql editor document changes and clear it when sql preview is no longer active, so we can use it for sql preview restore on vscode reload later --- src/sql_output/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/sql_output/index.ts b/src/sql_output/index.ts index fe24038..2b95826 100644 --- a/src/sql_output/index.ts +++ b/src/sql_output/index.ts @@ -96,8 +96,15 @@ async function compilePrql( }; } +/** + * Clears active SQL Preview context and view state. + * + * @param context Extension context. + */ function clearSqlContext(context: ExtensionContext) { commands.executeCommand('setContext', ViewContext.SqlPreviewActive, false); + commands.executeCommand('setContext', + ViewContext.ActivePrqlDocumentUri, undefined); context.workspaceState.update('prql.sql', undefined); } @@ -116,6 +123,8 @@ function sendText(context: ExtensionContext, panel: WebviewPanel) { // set sql preview flag and update sql output commands.executeCommand('setContext', ViewContext.SqlPreviewActive, true); + commands.executeCommand('setContext', + ViewContext.ActivePrqlDocumentUri, editor.document.uri); context.workspaceState.update('prql.sql', result.sql); }); } From 46bf190f34a021420232bbb7e608917cf4171fb5 Mon Sep 17 00:00:00 2001 From: RandomFractals Date: Fri, 17 Feb 2023 09:23:19 -0600 Subject: [PATCH 03/32] rename active prql document uri context key and don't clear it for sql preview restore to work on vscode reload --- src/sql_output/index.ts | 4 +--- src/views/viewContext.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/sql_output/index.ts b/src/sql_output/index.ts index 2b95826..47a4069 100644 --- a/src/sql_output/index.ts +++ b/src/sql_output/index.ts @@ -103,8 +103,6 @@ async function compilePrql( */ function clearSqlContext(context: ExtensionContext) { commands.executeCommand('setContext', ViewContext.SqlPreviewActive, false); - commands.executeCommand('setContext', - ViewContext.ActivePrqlDocumentUri, undefined); context.workspaceState.update('prql.sql', undefined); } @@ -124,7 +122,7 @@ function sendText(context: ExtensionContext, panel: WebviewPanel) { // set sql preview flag and update sql output commands.executeCommand('setContext', ViewContext.SqlPreviewActive, true); commands.executeCommand('setContext', - ViewContext.ActivePrqlDocumentUri, editor.document.uri); + ViewContext.LastActivePrqlDocumentUri, editor.document.uri); context.workspaceState.update('prql.sql', result.sql); }); } diff --git a/src/views/viewContext.ts b/src/views/viewContext.ts index 505f6a5..fe817f9 100644 --- a/src/views/viewContext.ts +++ b/src/views/viewContext.ts @@ -7,5 +7,5 @@ */ export const enum ViewContext { SqlPreviewActive = 'prql.sqlPreviewActive', - ActivePrqlDocumentUri = 'prql.activePrqlDocumentUri' + LastActivePrqlDocumentUri = 'prql.lastActivePrqlDocumentUri' } From 700735975fdc54f0c7fd35a49e49a8556d954889 Mon Sep 17 00:00:00 2001 From: RandomFractals Date: Fri, 17 Feb 2023 09:31:18 -0600 Subject: [PATCH 04/32] move sql preview impl. and utils to new src/views folder - rename it to sqlPreview.ts - and delete old src/sql_ouput folder --- src/extension.ts | 2 +- src/{sql_output/index.ts => views/sqlPreview.ts} | 2 +- src/{sql_output => views}/utils.ts | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/{sql_output/index.ts => views/sqlPreview.ts} (99%) rename src/{sql_output => views}/utils.ts (100%) diff --git a/src/extension.ts b/src/extension.ts index 456bfbb..76be706 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,5 +1,5 @@ import { ExtensionContext } from 'vscode'; -import { activateSqlPreviewPanel } from './sql_output'; +import { activateSqlPreviewPanel } from './views/sqlPreview'; import { activateDiagnostics } from './diagnostics'; import { registerCommands } from './commands'; diff --git a/src/sql_output/index.ts b/src/views/sqlPreview.ts similarity index 99% rename from src/sql_output/index.ts rename to src/views/sqlPreview.ts index 47a4069..9877b1f 100644 --- a/src/sql_output/index.ts +++ b/src/views/sqlPreview.ts @@ -21,7 +21,7 @@ import { normalizeThemeName, } from './utils'; -import { ViewContext } from '../views/viewContext'; +import { ViewContext } from './viewContext'; import { isPrqlDocument } from '../utils'; import { compile } from '../compiler'; import * as constants from '../constants'; diff --git a/src/sql_output/utils.ts b/src/views/utils.ts similarity index 100% rename from src/sql_output/utils.ts rename to src/views/utils.ts From 897baaecc067b20c65e13734d00d2ed3250e9c38 Mon Sep 17 00:00:00 2001 From: RandomFractals Date: Fri, 17 Feb 2023 09:51:53 -0600 Subject: [PATCH 05/32] reformat all ts code to rollback prior prettier formatting changes and make extension code imports, multi-line statements, function args, etc. more compact and readable --- src/commands.ts | 33 +++++++++++---------------------- src/compiler.ts | 16 ++++++++++------ src/diagnostics.ts | 5 +++-- src/views/sqlPreview.ts | 37 +++++++++++++++---------------------- src/views/utils.ts | 5 ++++- 5 files changed, 43 insertions(+), 53 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 31a8e35..68ea538 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -22,7 +22,6 @@ import { TextEncoder } from 'util'; export function registerCommands(context: ExtensionContext) { registerCommand(context, constants.GenerateSqlFile, generateSqlFile); registerCommand(context, constants.ViewSettings, viewPrqlSettings); - registerCommand(context, constants.CopySqlToClipboard, () => { const sql: string | undefined = context.workspaceState.get('prql.sql'); if (sql) { @@ -45,21 +44,21 @@ function registerCommand( context: ExtensionContext, commandId: string, callback: (...args: any[]) => any, - thisArg?: any -): void { + thisArg?: any): void { + const command: Disposable = commands.registerCommand( commandId, async (...args) => { try { await callback(...args); - } catch (e: unknown) { + } + catch (e: unknown) { window.showErrorMessage(String(e)); console.error(e); } }, thisArg ); - context.subscriptions.push(command); } @@ -67,10 +66,7 @@ function registerCommand( * Opens vscode Settings panel with PRQL settings. */ async function viewPrqlSettings() { - await commands.executeCommand( - constants.WorkbenchActionOpenSettings, - constants.ExtensionId - ); + await commands.executeCommand(constants.WorkbenchActionOpenSettings, constants.ExtensionId); } /** @@ -93,29 +89,22 @@ async function generateSqlFile() { if (Array.isArray(result)) { window.showErrorMessage(`PRQL Compile \ ${result[0].display ?? result[0].reason}`); - } else { + } + else { const prqlDocumentUri: Uri = editor.document.uri; const prqlFilePath = path.parse(prqlDocumentUri.fsPath); const prqlSettings = workspace.getConfiguration('prql'); const target = prqlSettings.get('target'); - const addTargetDialectToSqlFilenames = ( - prqlSettings.get(constants.AddTargetDialectToSqlFilenames) - ); + const addTargetDialectToSqlFilenames = + prqlSettings.get(constants.AddTargetDialectToSqlFilenames); let sqlFilenameSuffix = ''; - if ( - addTargetDialectToSqlFilenames && - target !== 'Generic' && - target !== 'None' - ) { + if (addTargetDialectToSqlFilenames && target !== 'Generic' && target !== 'None') { sqlFilenameSuffix = `.${target.toLowerCase()}`; } // create sql filename based on prql file path, name, and current settings - const sqlFilePath = path.join( - prqlFilePath.dir, - `${prqlFilePath.name}${sqlFilenameSuffix}.sql` - ); + const sqlFilePath = path.join(prqlFilePath.dir, `${prqlFilePath.name}${sqlFilenameSuffix}.sql`); // create sql file const sqlFileUri: Uri = Uri.file(sqlFilePath); diff --git a/src/compiler.ts b/src/compiler.ts index 3b162b9..cbc9d6b 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -1,4 +1,8 @@ -import { workspace, WorkspaceConfiguration } from 'vscode'; +import { + workspace, + WorkspaceConfiguration +} from 'vscode'; + import * as prql from 'prql-js'; import * as constants from './constants'; @@ -7,9 +11,7 @@ export function compile(prqlString: string): string | ErrorMessage[] { const prqlSettings: WorkspaceConfiguration = workspace.getConfiguration('prql'); const target = prqlSettings.get('target'); - const addCompilerInfo = ( - prqlSettings.get(constants.AddCompilerSignatureComment) - ); + const addCompilerInfo = prqlSettings.get(constants.AddCompilerSignatureComment); // create compile options from prql workspace settings const compileOptions = new prql.CompileOptions(); @@ -19,12 +21,14 @@ export function compile(prqlString: string): string | ErrorMessage[] { try { // run prql compile return prql.compile(prqlString, compileOptions) as string; - } catch (error) { + } + catch (error) { if ((error as any)?.message) { try { const errorMessages = JSON.parse((error as any).message); return errorMessages.inner as ErrorMessage[]; - } catch (ignored) { + } + catch (ignored) { throw error; } } diff --git a/src/diagnostics.ts b/src/diagnostics.ts index 60fa6af..5a6083f 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -23,7 +23,6 @@ function getRange(location: SourceLocation | null): Range { location.end[1] ); } - return new Range(new Position(0, 0), new Position(0, 0)); } @@ -36,13 +35,15 @@ function updateLineDiagnostics(diagnosticCollection: DiagnosticCollection) { if (!Array.isArray(result)) { diagnosticCollection.set(editor.document.uri, []); - } else { + } + else { const range = getRange(result[0].location); const diagnostic = new Diagnostic( range, result[0].reason ?? 'Syntax Error', DiagnosticSeverity.Error ); + diagnosticCollection.set(editor.document.uri, [diagnostic]); } } diff --git a/src/views/sqlPreview.ts b/src/views/sqlPreview.ts index 9877b1f..f40071d 100644 --- a/src/views/sqlPreview.ts +++ b/src/views/sqlPreview.ts @@ -26,19 +26,15 @@ import { isPrqlDocument } from '../utils'; import { compile } from '../compiler'; import * as constants from '../constants'; -function getCompiledTemplate( - context: ExtensionContext, - webview: Webview -): string { +function getCompiledTemplate(context: ExtensionContext, webview: Webview): string { + // load webview html template, view script, and stylesheet const template = readFileSync( - getResourceUri(context, 'sql_output.html').fsPath, - 'utf-8' - ); + getResourceUri(context, 'sql_output.html').fsPath, 'utf-8'); const templateJS = getResourceUri(context, 'sql_output.js'); const templateCss = getResourceUri(context, 'sql_output.css'); - return (template as string) - .replace(/##CSP_SOURCE##/g, webview.cspSource) + // inject web resource urls in the loaded webview html template + return template.replace(/##CSP_SOURCE##/g, webview.cspSource) .replace('##JS_URI##', webview.asWebviewUri(templateJS).toString()) .replace('##CSS_URI##', webview.asWebviewUri(templateCss).toString()); } @@ -70,10 +66,8 @@ async function getHighlighter(): Promise { return (highlighter = await shiki.getHighlighter({ theme: getThemeName() })); } -async function compilePrql( - text: string, - lastOkHtml: string | undefined -): Promise { +async function compilePrql(text: string, + lastOkHtml: string | undefined): Promise { const result = compile(text); if (Array.isArray(result)) { @@ -136,10 +130,9 @@ function sendThemeChanged(panel: WebviewPanel) { panel.webview.postMessage({ status: 'theme-changed' }); } -function createWebviewPanel( - context: ExtensionContext, - onDidDispose: () => any -): WebviewPanel { +function createWebviewPanel(context: ExtensionContext, + onDidDispose: () => any): WebviewPanel { + const panel = window.createWebviewPanel( constants.SqlPreviewPanel, constants.SqlPreviewTitle, @@ -153,6 +146,7 @@ function createWebviewPanel( localResourceRoots: [Uri.joinPath(context.extensionUri, 'resources')], } ); + panel.webview.html = getCompiledTemplate(context, panel.webview); panel.iconPath = getResourceUri(context, 'favicon.ico'); @@ -171,6 +165,7 @@ function createWebviewPanel( ); let lastEditor: TextEditor | undefined = undefined; + disposables.push( window.onDidChangeActiveTextEditor((editor) => { if (editor && editor !== lastEditor) { @@ -190,8 +185,7 @@ function createWebviewPanel( }) ); - panel.onDidDispose( - () => { + panel.onDidDispose(() => { clearSqlContext(context); disposables.forEach((d) => d.dispose()); onDidDispose(); @@ -201,18 +195,17 @@ function createWebviewPanel( ); sendText(context, panel); - return panel; } export function activateSqlPreviewPanel(context: ExtensionContext) { let panel: WebviewPanel | undefined = undefined; let panelViewColumn: ViewColumn | undefined = undefined; - const command = commands.registerCommand(constants.OpenSqlPreview, () => { if (panel) { panel.reveal(panelViewColumn, true); - } else { + } + else { panel = createWebviewPanel(context, () => (panel = undefined)); panelViewColumn = panel?.viewColumn; } diff --git a/src/views/utils.ts b/src/views/utils.ts index c7941a1..043700e 100644 --- a/src/views/utils.ts +++ b/src/views/utils.ts @@ -1,4 +1,7 @@ -import { ExtensionContext, Uri } from 'vscode'; +import { + ExtensionContext, + Uri +} from 'vscode'; export interface CompilationResult { status: 'ok' | 'error'; From b28b9c5d655c8cc01b91f06840e4971fc3593962 Mon Sep 17 00:00:00 2001 From: RandomFractals Date: Fri, 17 Feb 2023 12:39:27 -0600 Subject: [PATCH 06/32] first draft of new sql preview webview setup - rename sql_ouput webview files to sql preivew - add new SqlPreviewSerializer implementation - create new SqlPreview class with most of the setup for multi-instance sql document previews - add view init, refresh and update view state to upated sqlPreview.js --- resources/{sql_output.css => sql-preview.css} | 0 .../{sql_output.html => sql-preview.html} | 8 +- resources/sqlPreview.js | 82 ++++++ resources/sql_output.js | 37 --- src/views/sqlPreview.ts | 275 ++++++++++++++++-- src/views/sqlPreviewSerializer.ts | 47 +++ 6 files changed, 390 insertions(+), 59 deletions(-) rename resources/{sql_output.css => sql-preview.css} (100%) rename resources/{sql_output.html => sql-preview.html} (80%) create mode 100644 resources/sqlPreview.js delete mode 100644 resources/sql_output.js create mode 100644 src/views/sqlPreviewSerializer.ts diff --git a/resources/sql_output.css b/resources/sql-preview.css similarity index 100% rename from resources/sql_output.css rename to resources/sql-preview.css diff --git a/resources/sql_output.html b/resources/sql-preview.html similarity index 80% rename from resources/sql_output.html rename to resources/sql-preview.html index 38b825e..8148b28 100644 --- a/resources/sql_output.html +++ b/resources/sql-preview.html @@ -3,9 +3,11 @@ - diff --git a/resources/sqlPreview.js b/resources/sqlPreview.js new file mode 100644 index 0000000..c444397 --- /dev/null +++ b/resources/sqlPreview.js @@ -0,0 +1,82 @@ +// initialize vscode api +const vscode = acquireVsCodeApi(); + +// prql document vars and view state +let documentUrl = ''; +let viewState = {documentUrl: documentUrl}; + +// add page load handler +window.addEventListener('load', initializeView); + +/** + * Initializes sql preview webview. + */ +function initializeView() { + // restore previous view state + viewState = vscode.getState(); + if (viewState && viewState.documentUrl) { + // get last previewed prql document url + documentUrl = viewState.documentUrl; + } + else { + // create new empty view config + viewState = {}; + viewState.documentUrl = documentUrl; + vscode.setState(viewState); + } + + // request initial sql preview load + vscode.postMessage({ command: 'refresh' }); +} + +// add view update handler +window.addEventListener('message', (event) => { + const { status } = event.data; + const template = document.getElementById(`status-${status}`).content.cloneNode(true); + + const el = (name) => template.getElementById(name); + + switch (status) { + case 'ok': + template.lastElementChild.innerHTML = event.data.html; + break; + case 'error': + const { + error: { message }, + last_html: lastHtml, + } = event.data; + + if (lastHtml) { + el('last-html').innerHTML = lastHtml; + el('error-container').classList.add('error-container-fixed'); + } + + if (message.length > 0) { + el('error-message').innerHTML = message; + el('error-container').style.display = 'block'; + } + break; + case 'theme-changed': + // Content already in the template + break; + case 'refresh': + updateViewState(event.data); + break; + default: + throw new Error('unknown message'); + } + + document.getElementById('result').replaceChildren(template); +}); + +/** + * Updates Sql Preview view state on initial view load and refresh. + * + * @param {*} prqlInfo Prql document info from webview. + */ +function updateViewState(prqlInfo) { + // get and save prql document url in view state + documentUrl = prqlInfo.documentUrl; + viewState.documentUrl = documentUrl; + vscode.setState(viewState); +} diff --git a/resources/sql_output.js b/resources/sql_output.js deleted file mode 100644 index 44e4792..0000000 --- a/resources/sql_output.js +++ /dev/null @@ -1,37 +0,0 @@ -window.addEventListener('message', (event) => { - const { status } = event.data; - const template = document - .getElementById(`status-${status}`) - .content.cloneNode(true); - - const el = (name) => template.getElementById(name); - - switch (status) { - case 'ok': - template.lastElementChild.innerHTML = event.data.html; - break; - case 'error': - const { - error: { message }, - last_html: lastHtml, - } = event.data; - - if (lastHtml) { - el('last-html').innerHTML = lastHtml; - el('error-container').classList.add('error-container-fixed'); - } - - if (message.length > 0) { - el('error-message').innerHTML = message; - el('error-container').style.display = 'block'; - } - break; - case 'theme-changed': - // Content already in the template - break; - default: - throw new Error('unknown message'); - } - - document.getElementById('result').replaceChildren(template); -}); diff --git a/src/views/sqlPreview.ts b/src/views/sqlPreview.ts index f40071d..3c51c77 100644 --- a/src/views/sqlPreview.ts +++ b/src/views/sqlPreview.ts @@ -5,6 +5,7 @@ import { ViewColumn, Webview, WebviewPanel, + WebviewPanelOnDidChangeViewStateEvent, Uri, commands, window, @@ -13,6 +14,7 @@ import { import * as shiki from 'shiki'; import { readFileSync } from 'node:fs'; +import * as path from 'path'; import { CompilationResult, @@ -26,28 +28,261 @@ import { isPrqlDocument } from '../utils'; import { compile } from '../compiler'; import * as constants from '../constants'; +/** + * Defines Sql Preview class for managing state and behaviour of Sql Preview webview panel(s). + */ +export class SqlPreview { + + // view tracking vars + public static currentView: SqlPreview | undefined; + private static _views: Map = new Map(); + + // view instance vars + private readonly _webviewPanel: WebviewPanel; + private readonly _extensionUri: Uri; + private readonly _documentUri: Uri; + private readonly _viewUri: Uri; + private _viewConfig: any = {}; + private _disposables: Disposable[] = []; + + /** + * Reveals current sql preview webview + * or creates new sql preview webview panel + * for the given PRQL document Uri from and active editor. + * + * @param context Extension context. + * @param documentUri PRQL document Uri. + * @param webviewPanel Optional webview panel instance. + * @param viewConfig View config to restore. + */ + public static render(context: ExtensionContext, documentUri: Uri, + webviewPanel?: WebviewPanel, viewConfig?: any) { + + // create view Uri + const viewUri: Uri = documentUri.with({ scheme: 'prql' }); + + // check for open sql preview + const sqlPreview: SqlPreview | undefined = SqlPreview._views.get(viewUri.toString(true)); // skip encoding + if (sqlPreview) { + // show loaded webview panel in the active editor view column + sqlPreview.reveal(); + SqlPreview.currentView = sqlPreview; + } + else { + if (!webviewPanel) { + // create new webview panel for the prql document sql preview + webviewPanel = SqlPreview.createWebviewPanel(documentUri); + } + else { + // enable scripts for existing webview panel + webviewPanel.webview.options = { + enableScripts: true, + enableCommandUris: true + }; + } + + if (webviewPanel) { + // set custom sql preview panel icon + webviewPanel.iconPath = Uri.file( + path.join(context.extensionUri.fsPath, './resources/icons/prql-logo.png')); + } + + // set as current sql preview + SqlPreview.currentView = new SqlPreview(context, webviewPanel, documentUri, viewConfig); + } + + // update table view context values + commands.executeCommand('setContext', ViewContext.SqlPreviewActive, true); + commands.executeCommand('setContext', ViewContext.LastActivePrqlDocumentUri, documentUri); + } + + /** + * Creates new webview panel for the given prql source document Uri. + * + * @param documentUri PRQL source document Uri. + * @returns New webview panel instance. + */ + private static createWebviewPanel(documentUri: Uri): WebviewPanel { + // create new webview panel for sql preview + const fileName = path.basename(documentUri.path, '.prql'); + return window.createWebviewPanel( + constants.SqlPreviewPanel, // webview panel view type + `${constants.SqlPreviewTitle}: ${fileName}`, // webview panel title + { + viewColumn: ViewColumn.Beside, // use active view column for display + preserveFocus: true + }, + { // webview panel options + enableScripts: true, // enable JavaScript in webview + enableCommandUris: true, + enableFindWidget: true, + retainContextWhenHidden: true + } + ); + } + + /** + * Creates new SqlPreivew webview panel instance. + * + * @param context Extension context. + * @param webviewPanel Reference to the webview panel. + * @param documentUri PRQL document Uri. + * @param tableConfig Optional view config to restore. + */ + private constructor(context: ExtensionContext, + webviewPanel: WebviewPanel, + documentUri: Uri, viewConfig?: any) { + + // save webview panel and extension uri + this._webviewPanel = webviewPanel; + this._extensionUri = context.extensionUri; + this._documentUri = documentUri; + this._viewUri = documentUri.with({ scheme: 'prql' }); + + if (viewConfig) { + // save view config to restore + this._viewConfig = viewConfig; + } + + // configure webview panel + this.configure(context); + + // add it to the tracked sql preview webviews + SqlPreview._views.set(this._viewUri.toString(true), this); + + // update view context values on webview state change + this._webviewPanel.onDidChangeViewState( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + (viewChangeEvent: WebviewPanelOnDidChangeViewStateEvent) => { + if (this._webviewPanel.active) { + // update view context values + commands.executeCommand('setContext', ViewContext.SqlPreviewActive, true); + commands.executeCommand('setContext', ViewContext.LastActivePrqlDocumentUri, documentUri); + SqlPreview.currentView = this; + } + else { + // clear sql preview context + commands.executeCommand('etContext', ViewContext.SqlPreviewActive, false); + SqlPreview.currentView = undefined; + } + }); + + // dispose view resources when thi webview panel is closed by the user or via vscode apis + this._webviewPanel.onDidDispose(this.dispose, this, this._disposables); + } + + /** + * Disposes Sql Preview webview resources when webview panel is closed. + */ + public dispose() { + SqlPreview.currentView = undefined; + SqlPreview._views.delete(this._viewUri.toString(true)); // skip encoding + while (this._disposables.length) { + const disposable: Disposable | undefined = this._disposables.pop(); + if (disposable) { + disposable.dispose(); + } + } + + // clear active view context value + commands.executeCommand('setContext', ViewContext.SqlPreviewActive, false); + } + + /** + * Reveals loaded Sql Preview and sets it as active in vscode editor panel. + */ + public reveal() { + const viewColumn: ViewColumn = ViewColumn.Active ? ViewColumn.Active : ViewColumn.One; + this.webviewPanel.reveal(viewColumn); + + // update table view context values + commands.executeCommand('setContext', ViewContext.SqlPreviewActive, true); + commands.executeCommand('setContext', ViewContext.LastActivePrqlDocumentUri, this.documentUri); + } + + /** + * Configures webview html for the Sql Preview display, + * and registers webview message request handlers for updates. + * + * @param context Extension context. + * @param viewConfig Sql Preview config. + */ + private configure(context: ExtensionContext): void { + // set view html content for the webview panel + this.webviewPanel.webview.html = getCompiledTemplate(context, this.webviewPanel.webview); + // this.getWebviewContent(this.webviewPanel.webview, this._extensionUri, viewConfig); + + // process webview messages + this.webviewPanel.webview.onDidReceiveMessage((message: any) => { + const command: string = message.command; + switch (command) { + case 'refresh': + // reload data view and config + this.refresh(); + break; + } + }, undefined, this._disposables); + } + + /** + * Reloads table view on data save changes or vscode IDE realod. + */ + public async refresh(): Promise { + // update view state + this.webviewPanel.webview.postMessage({ + command: 'refresh', + documentUrl: this.documentUri.fsPath + }); + } + + /** + * Gets the underlying webview panel instance for this view. + */ + get webviewPanel(): WebviewPanel { + return this._webviewPanel; + } + + /** + * Gets view panel visibility status. + */ + get visible(): boolean { + return this._webviewPanel.visible; + } + + + /** + * Gets the source data uri for this view. + */ + get documentUri(): Uri { + return this._documentUri; + } + + /** + * Gets the view uri to load on tabular data view command triggers or vscode IDE reload. + */ + get viewUri(): Uri { + return this._viewUri; + } +} + function getCompiledTemplate(context: ExtensionContext, webview: Webview): string { - // load webview html template, view script, and stylesheet - const template = readFileSync( - getResourceUri(context, 'sql_output.html').fsPath, 'utf-8'); - const templateJS = getResourceUri(context, 'sql_output.js'); - const templateCss = getResourceUri(context, 'sql_output.css'); - - // inject web resource urls in the loaded webview html template - return template.replace(/##CSP_SOURCE##/g, webview.cspSource) - .replace('##JS_URI##', webview.asWebviewUri(templateJS).toString()) - .replace('##CSS_URI##', webview.asWebviewUri(templateCss).toString()); + // load webview html template, sql preview script and stylesheet + const htmlTemplate = readFileSync( + getResourceUri(context, 'sql-preview.html').fsPath, 'utf-8'); + const sqlPreviewScriptUri: Uri = getResourceUri(context, 'sqlPreview.js'); + const sqlPreviewStylesheetUri: Uri = getResourceUri(context, 'sql-preview.css'); + + // inject web resource urls into the loaded webview html template + return htmlTemplate.replace(/##CSP_SOURCE##/g, webview.cspSource) + .replace('##JS_URI##', webview.asWebviewUri(sqlPreviewScriptUri).toString()) + .replace('##CSS_URI##', webview.asWebviewUri(sqlPreviewStylesheetUri).toString()); } function getThemeName(): string { - const currentThemeName = workspace - .getConfiguration('workbench') + const currentThemeName = workspace.getConfiguration('workbench') .get('colorTheme', 'dark-plus'); - for (const themeName of [ - currentThemeName, - normalizeThemeName(currentThemeName), - ]) { + for (const themeName of [currentThemeName, normalizeThemeName(currentThemeName)]) { if (shiki.BUNDLED_THEMES.includes(themeName as shiki.Theme)) { return themeName; } @@ -130,8 +365,8 @@ function sendThemeChanged(panel: WebviewPanel) { panel.webview.postMessage({ status: 'theme-changed' }); } -function createWebviewPanel(context: ExtensionContext, - onDidDispose: () => any): WebviewPanel { +export function createWebviewPanel(context: ExtensionContext, + onDidDispose?: () => any): WebviewPanel { const panel = window.createWebviewPanel( constants.SqlPreviewPanel, @@ -188,7 +423,9 @@ function createWebviewPanel(context: ExtensionContext, panel.onDidDispose(() => { clearSqlContext(context); disposables.forEach((d) => d.dispose()); - onDidDispose(); + if (onDidDispose !== undefined) { + onDidDispose(); + } }, undefined, context.subscriptions diff --git a/src/views/sqlPreviewSerializer.ts b/src/views/sqlPreviewSerializer.ts new file mode 100644 index 0000000..d7c7830 --- /dev/null +++ b/src/views/sqlPreviewSerializer.ts @@ -0,0 +1,47 @@ +import { + Disposable, + ExtensionContext, + WebviewPanel, + WebviewPanelSerializer, + Uri, + window, +} from 'vscode'; + +import { SqlPreview } from './sqlPreview'; +import * as constants from '../constants'; + +/** + * Sql Preview webview panel serializer for restoring active Sql Preview on vscode reload. + */ +export class SqlPreviewSerializer implements WebviewPanelSerializer { + + /** + * Registers Sql Preview serializer. + * + * @param context Extension context. + * @returns Disposable object for this webview panel serializer. + */ + public static register(context: ExtensionContext): Disposable { + return window.registerWebviewPanelSerializer( + constants.SqlPreviewPanel, new SqlPreviewSerializer(context)); + } + + /** + * Creates new Sql Preview webview serializer. + * + * @param extensionUri Extension directory Uri. + */ + constructor(private readonly context: ExtensionContext) { + } + + /** + * Restores last active Sql Preview webview panel on vscode reload. + * + * @param webviewPanel Webview panel to restore. + * @param state Saved web view panel state with preview PRQL document Url. + */ + async deserializeWebviewPanel(webviewPanel: WebviewPanel, state: any) { + const documentUri: Uri = Uri.parse(state.documentUrl); + SqlPreview.render(this.context, documentUri, webviewPanel, state); + } +} From 45bc01cd588e2482480a8c9d70991b40f1238823 Mon Sep 17 00:00:00 2001 From: RandomFractals Date: Fri, 17 Feb 2023 12:51:16 -0600 Subject: [PATCH 07/32] minor docs updates for the new SqlPreview class and methods --- src/views/sqlPreview.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/views/sqlPreview.ts b/src/views/sqlPreview.ts index 3c51c77..d3879da 100644 --- a/src/views/sqlPreview.ts +++ b/src/views/sqlPreview.ts @@ -46,9 +46,10 @@ export class SqlPreview { private _disposables: Disposable[] = []; /** - * Reveals current sql preview webview - * or creates new sql preview webview panel - * for the given PRQL document Uri from and active editor. + * Reveals current Sql Preview webview + * or creates new Sql Preview webview panel + * for the given PRQL document Uri + * from an open and active PRQL document editor. * * @param context Extension context. * @param documentUri PRQL document Uri. @@ -83,15 +84,16 @@ export class SqlPreview { if (webviewPanel) { // set custom sql preview panel icon + // TODO: create smaller prql.svg icon instead of using large prql ext. logo png webviewPanel.iconPath = Uri.file( - path.join(context.extensionUri.fsPath, './resources/icons/prql-logo.png')); + path.join(context.extensionUri.fsPath, './resources/prql-logo.png')); } // set as current sql preview SqlPreview.currentView = new SqlPreview(context, webviewPanel, documentUri, viewConfig); } - // update table view context values + // update sql preview context values commands.executeCommand('setContext', ViewContext.SqlPreviewActive, true); commands.executeCommand('setContext', ViewContext.LastActivePrqlDocumentUri, documentUri); } @@ -109,7 +111,7 @@ export class SqlPreview { constants.SqlPreviewPanel, // webview panel view type `${constants.SqlPreviewTitle}: ${fileName}`, // webview panel title { - viewColumn: ViewColumn.Beside, // use active view column for display + viewColumn: ViewColumn.Beside, // display it on the side preserveFocus: true }, { // webview panel options @@ -127,13 +129,13 @@ export class SqlPreview { * @param context Extension context. * @param webviewPanel Reference to the webview panel. * @param documentUri PRQL document Uri. - * @param tableConfig Optional view config to restore. + * @param viewConfig Optional view config to restore. */ private constructor(context: ExtensionContext, webviewPanel: WebviewPanel, documentUri: Uri, viewConfig?: any) { - // save webview panel and extension uri + // save view context info this._webviewPanel = webviewPanel; this._extensionUri = context.extensionUri; this._documentUri = documentUri; @@ -167,7 +169,7 @@ export class SqlPreview { } }); - // dispose view resources when thi webview panel is closed by the user or via vscode apis + // dispose view resources when this webview panel is closed by the user or via vscode apis this._webviewPanel.onDidDispose(this.dispose, this, this._disposables); } @@ -225,7 +227,7 @@ export class SqlPreview { } /** - * Reloads table view on data save changes or vscode IDE realod. + * Reloads Sql Preivew for the PRQL document Uri or on vscode IDE realod. */ public async refresh(): Promise { // update view state From a2f5a75892b2b73e41248c1ac57ed591657c5ba0 Mon Sep 17 00:00:00 2001 From: RandomFractals Date: Fri, 17 Feb 2023 13:25:26 -0600 Subject: [PATCH 08/32] activat prql extension when prql.sqlPreviewPanel is loaded or deserialized --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index b734f27..ade2840 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "main": "out/extension.js", "activationEvents": [ "onLanguage:prql", + "onWebviewPanel:prql.sqlPreviewPanel", "onCommand:prql.openSqlPreview", "onCommand:prql.generateSqlFile", "onCommand:prql.copySqlToClipboard", From c27d7b312a17f061910a80c98d4e8a0067b24eaa Mon Sep 17 00:00:00 2001 From: RandomFractals Date: Fri, 17 Feb 2023 13:29:50 -0600 Subject: [PATCH 09/32] register sql preview serializer to reopen sql preview after vscode reload --- src/extension.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/extension.ts b/src/extension.ts index 76be706..22a92c9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,4 +1,5 @@ import { ExtensionContext } from 'vscode'; +import { SqlPreviewSerializer } from './views/sqlPreviewSerializer'; import { activateSqlPreviewPanel } from './views/sqlPreview'; import { activateDiagnostics } from './diagnostics'; import { registerCommands } from './commands'; @@ -15,4 +16,7 @@ export function activate(context: ExtensionContext) { activateDiagnostics(context); activateSqlPreviewPanel(context); registerCommands(context); + + // register sql preview serializer for restore on vscode reload + context.subscriptions.push(SqlPreviewSerializer.register(context)); } From dc8d1b1c98ecc3a100905117d7280fc56a330f1c Mon Sep 17 00:00:00 2001 From: RandomFractals Date: Fri, 17 Feb 2023 15:15:01 -0600 Subject: [PATCH 10/32] move all the old sql preview functions to new SqlPreview class - add open sql preview command to commands - use new SqlPreview.render to render those views --- src/commands.ts | 12 ++ src/extension.ts | 2 - src/views/sqlPreview.ts | 342 +++++++++++++++++----------------------- 3 files changed, 157 insertions(+), 199 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 68ea538..e5684ba 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -13,6 +13,7 @@ import * as constants from './constants'; import { compile } from './compiler'; import { TextEncoder } from 'util'; +import { SqlPreview } from './views/sqlPreview'; /** * Registers PRQL extension commands. @@ -22,6 +23,17 @@ import { TextEncoder } from 'util'; export function registerCommands(context: ExtensionContext) { registerCommand(context, constants.GenerateSqlFile, generateSqlFile); registerCommand(context, constants.ViewSettings, viewPrqlSettings); + + registerCommand(context, constants.OpenSqlPreview, (documentUri: Uri) => { + if (!documentUri && window.activeTextEditor) { + // use active text editor document Uri + documentUri = window.activeTextEditor.document.uri; + } + + // render Sql Preview for the requested PRQL document + SqlPreview.render(context, documentUri); + }); + registerCommand(context, constants.CopySqlToClipboard, () => { const sql: string | undefined = context.workspaceState.get('prql.sql'); if (sql) { diff --git a/src/extension.ts b/src/extension.ts index 22a92c9..7caf3af 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,5 @@ import { ExtensionContext } from 'vscode'; import { SqlPreviewSerializer } from './views/sqlPreviewSerializer'; -import { activateSqlPreviewPanel } from './views/sqlPreview'; import { activateDiagnostics } from './diagnostics'; import { registerCommands } from './commands'; @@ -14,7 +13,6 @@ import { registerCommands } from './commands'; */ export function activate(context: ExtensionContext) { activateDiagnostics(context); - activateSqlPreviewPanel(context); registerCommands(context); // register sql preview serializer for restore on vscode reload diff --git a/src/views/sqlPreview.ts b/src/views/sqlPreview.ts index d3879da..14e7974 100644 --- a/src/views/sqlPreview.ts +++ b/src/views/sqlPreview.ts @@ -45,6 +45,10 @@ export class SqlPreview { private _viewConfig: any = {}; private _disposables: Disposable[] = []; + private _highlighter: shiki.Highlighter | undefined; + private _lastEditor: TextEditor | undefined = undefined; + private _lastSqlHtml: string | undefined; + /** * Reveals current Sql Preview webview * or creates new Sql Preview webview panel @@ -72,7 +76,7 @@ export class SqlPreview { else { if (!webviewPanel) { // create new webview panel for the prql document sql preview - webviewPanel = SqlPreview.createWebviewPanel(documentUri); + webviewPanel = SqlPreview.createWebviewPanel(context, documentUri); } else { // enable scripts for existing webview panel @@ -84,9 +88,8 @@ export class SqlPreview { if (webviewPanel) { // set custom sql preview panel icon - // TODO: create smaller prql.svg icon instead of using large prql ext. logo png webviewPanel.iconPath = Uri.file( - path.join(context.extensionUri.fsPath, './resources/prql-logo.png')); + path.join(context.extensionUri.fsPath, './resources/favicon.ico')); } // set as current sql preview @@ -101,15 +104,16 @@ export class SqlPreview { /** * Creates new webview panel for the given prql source document Uri. * + * @param context Extension context. * @param documentUri PRQL source document Uri. * @returns New webview panel instance. */ - private static createWebviewPanel(documentUri: Uri): WebviewPanel { + private static createWebviewPanel(context: ExtensionContext, documentUri: Uri): WebviewPanel { // create new webview panel for sql preview const fileName = path.basename(documentUri.path, '.prql'); return window.createWebviewPanel( constants.SqlPreviewPanel, // webview panel view type - `${constants.SqlPreviewTitle}: ${fileName}`, // webview panel title + `${constants.SqlPreviewTitle}: ${fileName}.sql`, // webview panel title { viewColumn: ViewColumn.Beside, // display it on the side preserveFocus: true @@ -118,7 +122,8 @@ export class SqlPreview { enableScripts: true, // enable JavaScript in webview enableCommandUris: true, enableFindWidget: true, - retainContextWhenHidden: true + retainContextWhenHidden: true, + localResourceRoots: [Uri.joinPath(context.extensionUri, 'resources')], } ); } @@ -169,24 +174,54 @@ export class SqlPreview { } }); - // dispose view resources when this webview panel is closed by the user or via vscode apis - this._webviewPanel.onDidDispose(this.dispose, this, this._disposables); + // add prql text document change handler + [workspace.onDidOpenTextDocument, workspace.onDidChangeTextDocument].forEach( + (event) => { + this._disposables.push( + event( + debounce(() => { + this.sendText(context, this._webviewPanel); + }, 10) + ) + ); + } + ); + + // add active text editor change handler + this._disposables.push( + window.onDidChangeActiveTextEditor((editor) => { + if (editor && editor !== this._lastEditor) { + this._lastEditor = editor; + this._lastSqlHtml = undefined; + this.clearSqlContext(context); + this.sendText(context, this._webviewPanel); + } + }) + ); + + // add color theme change handler + this._disposables.push( + window.onDidChangeActiveColorTheme(() => { + this._highlighter = undefined; + this._lastSqlHtml = undefined; + this.sendThemeChanged(this._webviewPanel); + }) + ); + + // add dispose resources handler + this._webviewPanel.onDidDispose(() => this.dispose(context)); } /** * Disposes Sql Preview webview resources when webview panel is closed. */ - public dispose() { + public dispose(context: ExtensionContext) { SqlPreview.currentView = undefined; SqlPreview._views.delete(this._viewUri.toString(true)); // skip encoding - while (this._disposables.length) { - const disposable: Disposable | undefined = this._disposables.pop(); - if (disposable) { - disposable.dispose(); - } - } + this._disposables.forEach((d) => d.dispose()); // clear active view context value + this.clearSqlContext(context); commands.executeCommand('setContext', ViewContext.SqlPreviewActive, false); } @@ -211,7 +246,7 @@ export class SqlPreview { */ private configure(context: ExtensionContext): void { // set view html content for the webview panel - this.webviewPanel.webview.html = getCompiledTemplate(context, this.webviewPanel.webview); + this.webviewPanel.webview.html = this.getCompiledTemplate(context, this.webviewPanel.webview); // this.getWebviewContent(this.webviewPanel.webview, this._extensionUri, viewConfig); // process webview messages @@ -224,6 +259,8 @@ export class SqlPreview { break; } }, undefined, this._disposables); + + this.sendText(context, this._webviewPanel); } /** @@ -237,6 +274,88 @@ export class SqlPreview { }); } + private sendText(context: ExtensionContext, panel: WebviewPanel) { + const editor = window.activeTextEditor; + + if (panel.visible && editor && isPrqlDocument(editor)) { + const text = editor.document.getText(); + this.compilePrql(text, this._lastSqlHtml).then((result) => { + if (result.status === 'ok') { + this._lastSqlHtml = result.html; + } + panel.webview.postMessage(result); + + // set sql preview flag and update sql output + commands.executeCommand('setContext', ViewContext.SqlPreviewActive, true); + commands.executeCommand('setContext', + ViewContext.LastActivePrqlDocumentUri, editor.document.uri); + context.workspaceState.update('prql.sql', result.sql); + }); + } + + if (!panel.visible || !panel.active) { + this.clearSqlContext(context); + } + } + + private async sendThemeChanged(panel: WebviewPanel) { + panel.webview.postMessage({ status: 'theme-changed' }); + } + + private async compilePrql(text: string, + lastOkHtml: string | undefined): Promise { + const result = compile(text); + + if (Array.isArray(result)) { + return { + status: 'error', + error: { + message: result[0].display ?? result[0].reason, + }, + lastHtml: lastOkHtml, + }; + } + + const highlighter = await this.getHighlighter(); + const highlighted = highlighter.codeToHtml(result, { lang: 'sql' }); + + return { + status: 'ok', + html: highlighted, + sql: result, + }; + } + + /** + * Clears active SQL Preview context and view state. + * + * @param context Extension context. + */ + private async clearSqlContext(context: ExtensionContext) { + commands.executeCommand('setContext', ViewContext.SqlPreviewActive, false); + context.workspaceState.update('prql.sql', undefined); + } + + private async getHighlighter(): Promise { + if (this._highlighter) { + return Promise.resolve(this._highlighter); + } + return (this._highlighter = await shiki.getHighlighter({theme: this.themeName})); + } + + get themeName(): string { + const currentThemeName = workspace.getConfiguration('workbench') + .get('colorTheme', 'dark-plus'); + + for (const themeName of [currentThemeName, normalizeThemeName(currentThemeName)]) { + if (shiki.BUNDLED_THEMES.includes(themeName as shiki.Theme)) { + return themeName; + } + } + + return 'css-variables'; + } + /** * Gets the underlying webview panel instance for this view. */ @@ -265,189 +384,18 @@ export class SqlPreview { get viewUri(): Uri { return this._viewUri; } -} -function getCompiledTemplate(context: ExtensionContext, webview: Webview): string { - // load webview html template, sql preview script and stylesheet - const htmlTemplate = readFileSync( - getResourceUri(context, 'sql-preview.html').fsPath, 'utf-8'); - const sqlPreviewScriptUri: Uri = getResourceUri(context, 'sqlPreview.js'); - const sqlPreviewStylesheetUri: Uri = getResourceUri(context, 'sql-preview.css'); - - // inject web resource urls into the loaded webview html template - return htmlTemplate.replace(/##CSP_SOURCE##/g, webview.cspSource) - .replace('##JS_URI##', webview.asWebviewUri(sqlPreviewScriptUri).toString()) - .replace('##CSS_URI##', webview.asWebviewUri(sqlPreviewStylesheetUri).toString()); -} - -function getThemeName(): string { - const currentThemeName = workspace.getConfiguration('workbench') - .get('colorTheme', 'dark-plus'); - - for (const themeName of [currentThemeName, normalizeThemeName(currentThemeName)]) { - if (shiki.BUNDLED_THEMES.includes(themeName as shiki.Theme)) { - return themeName; - } - } - - return 'css-variables'; -} - -let highlighter: shiki.Highlighter | undefined; - -async function getHighlighter(): Promise { - if (highlighter) { - return Promise.resolve(highlighter); - } - - return (highlighter = await shiki.getHighlighter({ theme: getThemeName() })); -} -async function compilePrql(text: string, - lastOkHtml: string | undefined): Promise { - const result = compile(text); + private getCompiledTemplate(context: ExtensionContext, webview: Webview): string { + // load webview html template, sql preview script and stylesheet + const htmlTemplate = readFileSync( + getResourceUri(context, 'sql-preview.html').fsPath, 'utf-8'); + const sqlPreviewScriptUri: Uri = getResourceUri(context, 'sqlPreview.js'); + const sqlPreviewStylesheetUri: Uri = getResourceUri(context, 'sql-preview.css'); - if (Array.isArray(result)) { - return { - status: 'error', - error: { - message: result[0].display ?? result[0].reason, - }, - lastHtml: lastOkHtml, - }; + // inject web resource urls into the loaded webview html template + return htmlTemplate.replace(/##CSP_SOURCE##/g, webview.cspSource) + .replace('##JS_URI##', webview.asWebviewUri(sqlPreviewScriptUri).toString()) + .replace('##CSS_URI##', webview.asWebviewUri(sqlPreviewStylesheetUri).toString()); } - - const highlighter = await getHighlighter(); - const highlighted = highlighter.codeToHtml(result, { lang: 'sql' }); - - return { - status: 'ok', - html: highlighted, - sql: result, - }; -} - -/** - * Clears active SQL Preview context and view state. - * - * @param context Extension context. - */ -function clearSqlContext(context: ExtensionContext) { - commands.executeCommand('setContext', ViewContext.SqlPreviewActive, false); - context.workspaceState.update('prql.sql', undefined); -} - -let lastOkHtml: string | undefined; - -function sendText(context: ExtensionContext, panel: WebviewPanel) { - const editor = window.activeTextEditor; - - if (panel.visible && editor && isPrqlDocument(editor)) { - const text = editor.document.getText(); - compilePrql(text, lastOkHtml).then((result) => { - if (result.status === 'ok') { - lastOkHtml = result.html; - } - panel.webview.postMessage(result); - - // set sql preview flag and update sql output - commands.executeCommand('setContext', ViewContext.SqlPreviewActive, true); - commands.executeCommand('setContext', - ViewContext.LastActivePrqlDocumentUri, editor.document.uri); - context.workspaceState.update('prql.sql', result.sql); - }); - } - - if (!panel.visible || !panel.active) { - clearSqlContext(context); - } -} - -function sendThemeChanged(panel: WebviewPanel) { - panel.webview.postMessage({ status: 'theme-changed' }); -} - -export function createWebviewPanel(context: ExtensionContext, - onDidDispose?: () => any): WebviewPanel { - - const panel = window.createWebviewPanel( - constants.SqlPreviewPanel, - constants.SqlPreviewTitle, - { - viewColumn: ViewColumn.Beside, - preserveFocus: true, - }, - { - enableFindWidget: false, - enableScripts: true, - localResourceRoots: [Uri.joinPath(context.extensionUri, 'resources')], - } - ); - - panel.webview.html = getCompiledTemplate(context, panel.webview); - panel.iconPath = getResourceUri(context, 'favicon.ico'); - - const disposables: Disposable[] = []; - - [workspace.onDidOpenTextDocument, workspace.onDidChangeTextDocument].forEach( - (event) => { - disposables.push( - event( - debounce(() => { - sendText(context, panel); - }, 10) - ) - ); - } - ); - - let lastEditor: TextEditor | undefined = undefined; - - disposables.push( - window.onDidChangeActiveTextEditor((editor) => { - if (editor && editor !== lastEditor) { - lastEditor = editor; - lastOkHtml = undefined; - clearSqlContext(context); - sendText(context, panel); - } - }) - ); - - disposables.push( - window.onDidChangeActiveColorTheme(() => { - highlighter = undefined; - lastOkHtml = undefined; - sendThemeChanged(panel); - }) - ); - - panel.onDidDispose(() => { - clearSqlContext(context); - disposables.forEach((d) => d.dispose()); - if (onDidDispose !== undefined) { - onDidDispose(); - } - }, - undefined, - context.subscriptions - ); - - sendText(context, panel); - return panel; -} - -export function activateSqlPreviewPanel(context: ExtensionContext) { - let panel: WebviewPanel | undefined = undefined; - let panelViewColumn: ViewColumn | undefined = undefined; - const command = commands.registerCommand(constants.OpenSqlPreview, () => { - if (panel) { - panel.reveal(panelViewColumn, true); - } - else { - panel = createWebviewPanel(context, () => (panel = undefined)); - panelViewColumn = panel?.viewColumn; - } - }); - context.subscriptions.push(command); } From ae3b5e3d886506eb90ddcb404bda17a0aeb2616d Mon Sep 17 00:00:00 2001 From: RandomFractals Date: Sat, 18 Feb 2023 05:18:01 -0600 Subject: [PATCH 11/32] clarify docs in sql preview serializer --- src/views/sqlPreviewSerializer.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/views/sqlPreviewSerializer.ts b/src/views/sqlPreviewSerializer.ts index d7c7830..abbf01f 100644 --- a/src/views/sqlPreviewSerializer.ts +++ b/src/views/sqlPreviewSerializer.ts @@ -11,7 +11,7 @@ import { SqlPreview } from './sqlPreview'; import * as constants from '../constants'; /** - * Sql Preview webview panel serializer for restoring active Sql Preview on vscode reload. + * Sql Preview webview panel serializer for restoring open Sql Previews on vscode reload. */ export class SqlPreviewSerializer implements WebviewPanelSerializer { @@ -29,13 +29,13 @@ export class SqlPreviewSerializer implements WebviewPanelSerializer { /** * Creates new Sql Preview webview serializer. * - * @param extensionUri Extension directory Uri. + * @param extensionUri Extension context. */ constructor(private readonly context: ExtensionContext) { } /** - * Restores last active Sql Preview webview panel on vscode reload. + * Restores open Sql Preview webview panel on vscode reload. * * @param webviewPanel Webview panel to restore. * @param state Saved web view panel state with preview PRQL document Url. From 5779ff7b1674f7b7aa39d9df997f5733025c8afb Mon Sep 17 00:00:00 2001 From: RandomFractals Date: Sat, 18 Feb 2023 05:21:30 -0600 Subject: [PATCH 12/32] delete utils.ts with one one liner method --- src/diagnostics.ts | 3 +-- src/utils.ts | 5 ----- src/views/sqlPreview.ts | 3 +-- 3 files changed, 2 insertions(+), 9 deletions(-) delete mode 100644 src/utils.ts diff --git a/src/diagnostics.ts b/src/diagnostics.ts index 5a6083f..1ddc74a 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -11,7 +11,6 @@ import { workspace, } from 'vscode'; -import { isPrqlDocument } from './utils'; import { SourceLocation, compile } from './compiler'; function getRange(location: SourceLocation | null): Range { @@ -29,7 +28,7 @@ function getRange(location: SourceLocation | null): Range { function updateLineDiagnostics(diagnosticCollection: DiagnosticCollection) { const editor = window.activeTextEditor; - if (editor && isPrqlDocument(editor)) { + if (editor && editor.document.languageId === 'prql') { const text = editor.document.getText(); const result = compile(text); diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 12018c6..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { TextEditor } from 'vscode'; - -export function isPrqlDocument(editor: TextEditor): boolean { - return editor.document.languageId === 'prql'; -} diff --git a/src/views/sqlPreview.ts b/src/views/sqlPreview.ts index 14e7974..f885118 100644 --- a/src/views/sqlPreview.ts +++ b/src/views/sqlPreview.ts @@ -24,7 +24,6 @@ import { } from './utils'; import { ViewContext } from './viewContext'; -import { isPrqlDocument } from '../utils'; import { compile } from '../compiler'; import * as constants from '../constants'; @@ -277,7 +276,7 @@ export class SqlPreview { private sendText(context: ExtensionContext, panel: WebviewPanel) { const editor = window.activeTextEditor; - if (panel.visible && editor && isPrqlDocument(editor)) { + if (panel.visible && editor && editor.document.languageId === 'prql') { const text = editor.document.getText(); this.compilePrql(text, this._lastSqlHtml).then((result) => { if (result.status === 'ok') { From 180a70f6c9c40e1ef7d9d3074ceef3581510947d Mon Sep 17 00:00:00 2001 From: RandomFractals Date: Sat, 18 Feb 2023 05:30:19 -0600 Subject: [PATCH 13/32] move CompilationResult interface to separate ts file --- src/views/compilationResult.ts | 12 ++++++++++++ src/views/sqlPreview.ts | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 src/views/compilationResult.ts diff --git a/src/views/compilationResult.ts b/src/views/compilationResult.ts new file mode 100644 index 0000000..1c627fd --- /dev/null +++ b/src/views/compilationResult.ts @@ -0,0 +1,12 @@ +/** + * PRQL compilation resust for sql preview and display. + */ +export interface CompilationResult { + status: 'ok' | 'error'; + html?: string; + sql?: string; + error?: { + message: string; + }; + lastHtml?: string | undefined; +} diff --git a/src/views/sqlPreview.ts b/src/views/sqlPreview.ts index f885118..eece0e3 100644 --- a/src/views/sqlPreview.ts +++ b/src/views/sqlPreview.ts @@ -17,13 +17,13 @@ import { readFileSync } from 'node:fs'; import * as path from 'path'; import { - CompilationResult, debounce, getResourceUri, normalizeThemeName, } from './utils'; import { ViewContext } from './viewContext'; +import { CompilationResult } from './compilationResult'; import { compile } from '../compiler'; import * as constants from '../constants'; From c27fbf382b70beac428c8b1bcbc95a9a763511b0 Mon Sep 17 00:00:00 2001 From: RandomFractals Date: Sat, 18 Feb 2023 05:50:45 -0600 Subject: [PATCH 14/32] refactor webview resource uri and html template construction - remove getResourceUri() from src/views/utils.ts - and document html template and resource uri methods --- src/views/sqlPreview.ts | 54 ++++++++++++++++++++++++++++++----------- src/views/utils.ts | 23 ------------------ 2 files changed, 40 insertions(+), 37 deletions(-) diff --git a/src/views/sqlPreview.ts b/src/views/sqlPreview.ts index eece0e3..0499aeb 100644 --- a/src/views/sqlPreview.ts +++ b/src/views/sqlPreview.ts @@ -18,8 +18,6 @@ import * as path from 'path'; import { debounce, - getResourceUri, - normalizeThemeName, } from './utils'; import { ViewContext } from './viewContext'; @@ -245,7 +243,7 @@ export class SqlPreview { */ private configure(context: ExtensionContext): void { // set view html content for the webview panel - this.webviewPanel.webview.html = this.getCompiledTemplate(context, this.webviewPanel.webview); + this.webviewPanel.webview.html = this.getHtmlTemplate(context, this.webviewPanel.webview); // this.getWebviewContent(this.webviewPanel.webview, this._extensionUri, viewConfig); // process webview messages @@ -342,16 +340,26 @@ export class SqlPreview { return (this._highlighter = await shiki.getHighlighter({theme: this.themeName})); } + /** + * Gets shiki highlighter theme name that matches current vscode color theme + * to use it as the UI theme for this Sql Preview webview. + */ get themeName(): string { - const currentThemeName = workspace.getConfiguration('workbench') - .get('colorTheme', 'dark-plus'); + // get current vscode color UI theme name + let colorTheme = workspace.getConfiguration('workbench') + .get('colorTheme', 'dark-plus'); // default - for (const themeName of [currentThemeName, normalizeThemeName(currentThemeName)]) { - if (shiki.BUNDLED_THEMES.includes(themeName as shiki.Theme)) { - return themeName; - } + if (shiki.BUNDLED_THEMES.includes(colorTheme as shiki.Theme)) { + return colorTheme; + } + + // try normalized color theme name + colorTheme = colorTheme.toLowerCase().replace('theme', '').replace(/\s+/g, '-'); + if (shiki.BUNDLED_THEMES.includes(colorTheme as shiki.Theme)) { + return colorTheme; } + // ??? not sure what this means return 'css-variables'; } @@ -385,16 +393,34 @@ export class SqlPreview { } - private getCompiledTemplate(context: ExtensionContext, webview: Webview): string { + /** + * Loads and creates html template for Sql Preview webview. + * + * @param context Extension context. + * @param webview Sql Preview webview. + * @returns Html template to use for Sql Preview webview. + */ + private getHtmlTemplate(context: ExtensionContext, webview: Webview): string { // load webview html template, sql preview script and stylesheet const htmlTemplate = readFileSync( - getResourceUri(context, 'sql-preview.html').fsPath, 'utf-8'); - const sqlPreviewScriptUri: Uri = getResourceUri(context, 'sqlPreview.js'); - const sqlPreviewStylesheetUri: Uri = getResourceUri(context, 'sql-preview.css'); + this.getResourceUri(context, 'sql-preview.html').fsPath, 'utf-8'); + const sqlPreviewScriptUri: Uri = this.getResourceUri(context, 'sqlPreview.js'); + const sqlPreviewStylesheetUri: Uri = this.getResourceUri(context, 'sql-preview.css'); - // inject web resource urls into the loaded webview html template + // inject webview resource urls into the loaded webview html template return htmlTemplate.replace(/##CSP_SOURCE##/g, webview.cspSource) .replace('##JS_URI##', webview.asWebviewUri(sqlPreviewScriptUri).toString()) .replace('##CSS_URI##', webview.asWebviewUri(sqlPreviewStylesheetUri).toString()); } + + /** + * Gets webview resource Uri from extension directory. + * + * @param context Extension context. + * @param filename Resource filename to create resource Uri. + * @returns Webview resource Uri. + */ + private getResourceUri(context: ExtensionContext, fileName: string) { + return Uri.joinPath(context.extensionUri, 'resources', fileName); + } } diff --git a/src/views/utils.ts b/src/views/utils.ts index 043700e..94d7cb1 100644 --- a/src/views/utils.ts +++ b/src/views/utils.ts @@ -1,26 +1,3 @@ -import { - ExtensionContext, - Uri -} from 'vscode'; - -export interface CompilationResult { - status: 'ok' | 'error'; - html?: string; - sql?: string; - error?: { - message: string; - }; - lastHtml?: string | undefined; -} - -export function getResourceUri(context: ExtensionContext, filename: string) { - return Uri.joinPath(context.extensionUri, 'resources', filename); -} - -export function normalizeThemeName(currentTheme: string): string { - return currentTheme.toLowerCase().replace('theme', '').replace(/\s+/g, '-'); -} - export function debounce(fn: () => any, timeout: number) { let timer: NodeJS.Timeout | undefined; return () => { From 91a02dd3054ec9bb5c01aa25bfe9b823454b85e4 Mon Sep 17 00:00:00 2001 From: RandomFractals Date: Sat, 18 Feb 2023 05:54:40 -0600 Subject: [PATCH 15/32] move debounce to sqlPreview.ts and delete utils.ts only sql preview uses it --- src/views/sqlPreview.ts | 23 ++++++++++++++++++----- src/views/utils.ts | 9 --------- 2 files changed, 18 insertions(+), 14 deletions(-) delete mode 100644 src/views/utils.ts diff --git a/src/views/sqlPreview.ts b/src/views/sqlPreview.ts index 0499aeb..25738cf 100644 --- a/src/views/sqlPreview.ts +++ b/src/views/sqlPreview.ts @@ -16,10 +16,6 @@ import * as shiki from 'shiki'; import { readFileSync } from 'node:fs'; import * as path from 'path'; -import { - debounce, -} from './utils'; - import { ViewContext } from './viewContext'; import { CompilationResult } from './compilationResult'; import { compile } from '../compiler'; @@ -176,7 +172,7 @@ export class SqlPreview { (event) => { this._disposables.push( event( - debounce(() => { + this.debounce(() => { this.sendText(context, this._webviewPanel); }, 10) ) @@ -209,6 +205,23 @@ export class SqlPreview { this._webviewPanel.onDidDispose(() => this.dispose(context)); } + /** + * Debounce for sql preview updates on prql text changes. + * + * @param fn + * @param timeout + * @returns + */ + private debounce(fn: () => any, timeout: number) { + let timer: NodeJS.Timeout | undefined; + return () => { + clearTimeout(timer); + timer = setTimeout(() => { + fn(); + }, timeout); + }; + } + /** * Disposes Sql Preview webview resources when webview panel is closed. */ diff --git a/src/views/utils.ts b/src/views/utils.ts deleted file mode 100644 index 94d7cb1..0000000 --- a/src/views/utils.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function debounce(fn: () => any, timeout: number) { - let timer: NodeJS.Timeout | undefined; - return () => { - clearTimeout(timer); - timer = setTimeout(() => { - fn(); - }, timeout); - }; -} From f4c574d7f92493d322b4f08a924d5745f3d26f3e Mon Sep 17 00:00:00 2001 From: RandomFractals Date: Sat, 18 Feb 2023 08:03:40 -0600 Subject: [PATCH 16/32] finish webview refactor for multi-instance sql preview --- resources/sqlPreview.js | 17 ++-- src/views/sqlPreview.ts | 182 ++++++++++++++++++++++++---------------- 2 files changed, 119 insertions(+), 80 deletions(-) diff --git a/resources/sqlPreview.js b/resources/sqlPreview.js index c444397..b332584 100644 --- a/resources/sqlPreview.js +++ b/resources/sqlPreview.js @@ -26,7 +26,7 @@ function initializeView() { } // request initial sql preview load - vscode.postMessage({ command: 'refresh' }); + vscode.postMessage({command: 'refresh'}); } // add view update handler @@ -34,10 +34,9 @@ window.addEventListener('message', (event) => { const { status } = event.data; const template = document.getElementById(`status-${status}`).content.cloneNode(true); - const el = (name) => template.getElementById(name); - switch (status) { case 'ok': + // show updated sql html template.lastElementChild.innerHTML = event.data.html; break; case 'error': @@ -47,17 +46,17 @@ window.addEventListener('message', (event) => { } = event.data; if (lastHtml) { - el('last-html').innerHTML = lastHtml; - el('error-container').classList.add('error-container-fixed'); + document.getElementById('last-html').innerHTML = lastHtml; + document.getElementById('error-container').classList.add('error-container-fixed'); } if (message.length > 0) { - el('error-message').innerHTML = message; - el('error-container').style.display = 'block'; + document.getElementById('error-message').innerHTML = message; + document.getElementById('error-container').style.display = 'block'; } break; - case 'theme-changed': - // Content already in the template + case 'themeChanged': + // content already in the template: do nothing ??? break; case 'refresh': updateViewState(event.data); diff --git a/src/views/sqlPreview.ts b/src/views/sqlPreview.ts index 25738cf..b9a1cfb 100644 --- a/src/views/sqlPreview.ts +++ b/src/views/sqlPreview.ts @@ -1,23 +1,28 @@ import { + commands, + window, + workspace, Disposable, + Event, ExtensionContext, + TextDocument, + TextDocumentChangeEvent, TextEditor, ViewColumn, Webview, WebviewPanel, WebviewPanelOnDidChangeViewStateEvent, - Uri, - commands, - window, - workspace, + Uri } from 'vscode'; import * as shiki from 'shiki'; + import { readFileSync } from 'node:fs'; import * as path from 'path'; import { ViewContext } from './viewContext'; import { CompilationResult } from './compilationResult'; + import { compile } from '../compiler'; import * as constants from '../constants'; @@ -35,6 +40,7 @@ export class SqlPreview { private readonly _extensionUri: Uri; private readonly _documentUri: Uri; private readonly _viewUri: Uri; + private _viewConfig: any = {}; private _disposables: Disposable[] = []; @@ -43,26 +49,26 @@ export class SqlPreview { private _lastSqlHtml: string | undefined; /** - * Reveals current Sql Preview webview - * or creates new Sql Preview webview panel - * for the given PRQL document Uri - * from an open and active PRQL document editor. - * - * @param context Extension context. - * @param documentUri PRQL document Uri. - * @param webviewPanel Optional webview panel instance. - * @param viewConfig View config to restore. - */ + * Reveals current Sql Preview webview + * or creates new Sql Preview webview panel + * for the given PRQL document Uri + * from an open and active PRQL document editor. + * + * @param context Extension context. + * @param documentUri PRQL document Uri. + * @param webviewPanel Optional webview panel instance. + * @param viewConfig View config to restore. + */ public static render(context: ExtensionContext, documentUri: Uri, webviewPanel?: WebviewPanel, viewConfig?: any) { // create view Uri - const viewUri: Uri = documentUri.with({ scheme: 'prql' }); + const viewUri: Uri = documentUri.with({scheme: 'prql'}); - // check for open sql preview + // check for an open sql preview const sqlPreview: SqlPreview | undefined = SqlPreview._views.get(viewUri.toString(true)); // skip encoding if (sqlPreview) { - // show loaded webview panel in the active editor view column + // show loaded webview panel sqlPreview.reveal(); SqlPreview.currentView = sqlPreview; } @@ -82,7 +88,7 @@ export class SqlPreview { if (webviewPanel) { // set custom sql preview panel icon webviewPanel.iconPath = Uri.file( - path.join(context.extensionUri.fsPath, './resources/favicon.ico')); + path.join(context.extensionUri.fsPath, 'resources', 'favicon.ico')); } // set as current sql preview @@ -103,7 +109,8 @@ export class SqlPreview { */ private static createWebviewPanel(context: ExtensionContext, documentUri: Uri): WebviewPanel { // create new webview panel for sql preview - const fileName = path.basename(documentUri.path, '.prql'); + const fileName = path.basename(documentUri.path, '.prql'); // strip out prql file ext. + return window.createWebviewPanel( constants.SqlPreviewPanel, // webview panel view type `${constants.SqlPreviewTitle}: ${fileName}.sql`, // webview panel title @@ -122,7 +129,7 @@ export class SqlPreview { } /** - * Creates new SqlPreivew webview panel instance. + * Creates new SqlPreview webview panel instance. * * @param context Extension context. * @param webviewPanel Reference to the webview panel. @@ -137,7 +144,7 @@ export class SqlPreview { this._webviewPanel = webviewPanel; this._extensionUri = context.extensionUri; this._documentUri = documentUri; - this._viewUri = documentUri.with({ scheme: 'prql' }); + this._viewUri = documentUri.with({scheme: 'prql'}); if (viewConfig) { // save view config to restore @@ -162,18 +169,18 @@ export class SqlPreview { } else { // clear sql preview context - commands.executeCommand('etContext', ViewContext.SqlPreviewActive, false); + commands.executeCommand('setContext', ViewContext.SqlPreviewActive, false); SqlPreview.currentView = undefined; } }); // add prql text document change handler [workspace.onDidOpenTextDocument, workspace.onDidChangeTextDocument].forEach( - (event) => { + (event: Event | Event) => { this._disposables.push( event( this.debounce(() => { - this.sendText(context, this._webviewPanel); + this.update(context); }, 10) ) ); @@ -183,11 +190,16 @@ export class SqlPreview { // add active text editor change handler this._disposables.push( window.onDidChangeActiveTextEditor((editor) => { - if (editor && editor !== this._lastEditor) { + if (editor && editor.document.uri.fsPath === this.documentUri.fsPath) { + // reset PRQL editor reference and sql html output this._lastEditor = editor; this._lastSqlHtml = undefined; - this.clearSqlContext(context); - this.sendText(context, this._webviewPanel); + + // clear sql preview context and recompile prql + // from the linked and active PRQL editor + // for the webview's PRQL source document + this.clearSqlPreviewContext(context); + this.update(context); } }) ); @@ -197,7 +209,7 @@ export class SqlPreview { window.onDidChangeActiveColorTheme(() => { this._highlighter = undefined; this._lastSqlHtml = undefined; - this.sendThemeChanged(this._webviewPanel); + webviewPanel.webview.postMessage({status: 'themeChanged'}); }) ); @@ -229,10 +241,7 @@ export class SqlPreview { SqlPreview.currentView = undefined; SqlPreview._views.delete(this._viewUri.toString(true)); // skip encoding this._disposables.forEach((d) => d.dispose()); - - // clear active view context value - this.clearSqlContext(context); - commands.executeCommand('setContext', ViewContext.SqlPreviewActive, false); + this.clearSqlPreviewContext(context); } /** @@ -242,13 +251,13 @@ export class SqlPreview { const viewColumn: ViewColumn = ViewColumn.Active ? ViewColumn.Active : ViewColumn.One; this.webviewPanel.reveal(viewColumn); - // update table view context values + // update sql preview view context values commands.executeCommand('setContext', ViewContext.SqlPreviewActive, true); commands.executeCommand('setContext', ViewContext.LastActivePrqlDocumentUri, this.documentUri); } /** - * Configures webview html for the Sql Preview display, + * Configures webview html for Sql Preview display, * and registers webview message request handlers for updates. * * @param context Extension context. @@ -270,11 +279,12 @@ export class SqlPreview { } }, undefined, this._disposables); - this.sendText(context, this._webviewPanel); + // send initial prql compile result to webview + this.update(context); } /** - * Reloads Sql Preivew for the PRQL document Uri or on vscode IDE realod. + * Reloads Sql Preivew for the active PRQL document Uri or on vscode IDE realod. */ public async refresh(): Promise { // update view state @@ -284,37 +294,71 @@ export class SqlPreview { }); } - private sendText(context: ExtensionContext, panel: WebviewPanel) { + /** + * Updates Sql Preview with new PRQL compilation results + * from the active PRQL text editor. + * + * @param context Extension context. + */ + private update(context: ExtensionContext) { + // check active text editor const editor = window.activeTextEditor; - - if (panel.visible && editor && editor.document.languageId === 'prql') { - const text = editor.document.getText(); - this.compilePrql(text, this._lastSqlHtml).then((result) => { - if (result.status === 'ok') { - this._lastSqlHtml = result.html; + if (this.webviewPanel.visible && editor && + editor.document.languageId === 'prql' && + editor.document.uri.fsPath === this.documentUri.fsPath) { + + // get updated prql code + const prqlCode = editor.document.getText(); + this.compilePrql(prqlCode, this._lastSqlHtml).then((compilationResult) => { + if (compilationResult.status === 'ok') { + // save last valid sql html output to show when errors occur later + this._lastSqlHtml = compilationResult.html; } - panel.webview.postMessage(result); + this.webviewPanel.webview.postMessage(compilationResult); // set sql preview flag and update sql output - commands.executeCommand('setContext', ViewContext.SqlPreviewActive, true); - commands.executeCommand('setContext', - ViewContext.LastActivePrqlDocumentUri, editor.document.uri); - context.workspaceState.update('prql.sql', result.sql); + this.resetSqlPreviewContext(); + context.workspaceState.update('prql.sql', compilationResult.sql); }); } - if (!panel.visible || !panel.active) { - this.clearSqlContext(context); + if (!this.webviewPanel.visible || !this.webviewPanel.active) { + this.clearSqlPreviewContext(context); } } - private async sendThemeChanged(panel: WebviewPanel) { - panel.webview.postMessage({ status: 'theme-changed' }); + /** + * Resets current/active SQL Preview context and view state. + * + * @param context Extension context. + */ + private async resetSqlPreviewContext() { + commands.executeCommand('setContext', ViewContext.SqlPreviewActive, true); + commands.executeCommand('setContext', + ViewContext.LastActivePrqlDocumentUri, this.documentUri); + } + + /** + * Clears SQL Preview context and view state. + * + * @param context Extension context. + */ + private async clearSqlPreviewContext(context: ExtensionContext) { + commands.executeCommand('setContext', ViewContext.SqlPreviewActive, false); + context.workspaceState.update('prql.sql', undefined); } - private async compilePrql(text: string, - lastOkHtml: string | undefined): Promise { - const result = compile(text); + /** + * Compiles prql code and returns generated sql, + * and formatted html sql compilation result. + * + * @param prqlCode PRQL code to compile. + * @param lastSqlHtml Last valid sql html output. + * @returns Compilation result in sql and html formats. + */ + private async compilePrql(prqlCode: string, + lastSqlHtml: string | undefined): Promise { + const result = compile(prqlCode); if (Array.isArray(result)) { return { @@ -322,30 +366,26 @@ export class SqlPreview { error: { message: result[0].display ?? result[0].reason, }, - lastHtml: lastOkHtml, + lastHtml: lastSqlHtml, }; } + // create html to display for the generated sql const highlighter = await this.getHighlighter(); - const highlighted = highlighter.codeToHtml(result, { lang: 'sql' }); + const sqlHtml = highlighter.codeToHtml(result, {lang: 'sql'}); return { status: 'ok', - html: highlighted, + html: sqlHtml, sql: result, }; } /** - * Clears active SQL Preview context and view state. + * Gets shiki code highlighter instance to create html formatted sql output. * - * @param context Extension context. + * @returns Shiki highlighter instance with UI theme matching vscode color theme. */ - private async clearSqlContext(context: ExtensionContext) { - commands.executeCommand('setContext', ViewContext.SqlPreviewActive, false); - context.workspaceState.update('prql.sql', undefined); - } - private async getHighlighter(): Promise { if (this._highlighter) { return Promise.resolve(this._highlighter); @@ -360,7 +400,7 @@ export class SqlPreview { get themeName(): string { // get current vscode color UI theme name let colorTheme = workspace.getConfiguration('workbench') - .get('colorTheme', 'dark-plus'); // default + .get('colorTheme', 'dark-plus'); // default to dark plus if (shiki.BUNDLED_THEMES.includes(colorTheme as shiki.Theme)) { return colorTheme; @@ -372,7 +412,9 @@ export class SqlPreview { return colorTheme; } - // ??? not sure what this means + // ??? not sure what this means, or does. + // Does it use the loaded vscode CSS vars + // when no color theme is set? return 'css-variables'; } @@ -390,22 +432,20 @@ export class SqlPreview { return this._webviewPanel.visible; } - /** - * Gets the source data uri for this view. + * Gets the source document uri for this view. */ get documentUri(): Uri { return this._documentUri; } /** - * Gets the view uri to load on tabular data view command triggers or vscode IDE reload. + * Gets the view uri to load on sql preview command triggers or vscode IDE reload. */ get viewUri(): Uri { return this._viewUri; } - /** * Loads and creates html template for Sql Preview webview. * From 562496b1e1c14544e9f729d8f55b354b64cd2114 Mon Sep 17 00:00:00 2001 From: RandomFractals Date: Sat, 18 Feb 2023 08:42:36 -0600 Subject: [PATCH 17/32] sql preview changes to load prql from file after vscode reload when there is no active prql text editor and sql preview is being restored on vscode reload --- src/views/sqlPreview.ts | 65 ++++++++++++++++++++----------- src/views/sqlPreviewSerializer.ts | 3 +- 2 files changed, 45 insertions(+), 23 deletions(-) diff --git a/src/views/sqlPreview.ts b/src/views/sqlPreview.ts index b9a1cfb..84c1531 100644 --- a/src/views/sqlPreview.ts +++ b/src/views/sqlPreview.ts @@ -263,7 +263,7 @@ export class SqlPreview { * @param context Extension context. * @param viewConfig Sql Preview config. */ - private configure(context: ExtensionContext): void { + private async configure(context: ExtensionContext) { // set view html content for the webview panel this.webviewPanel.webview.html = this.getHtmlTemplate(context, this.webviewPanel.webview); // this.getWebviewContent(this.webviewPanel.webview, this._extensionUri, viewConfig); @@ -279,8 +279,15 @@ export class SqlPreview { } }, undefined, this._disposables); + // load initial prql code from file + console.log(this.documentUri.fsPath); + const prqlContent:Uint8Array = await workspace.fs.readFile( + Uri.file(this.documentUri.fsPath)); + const textDecoder = new TextDecoder('utf8'); + const prqlCode = textDecoder.decode(prqlContent); + // send initial prql compile result to webview - this.update(context); + this.update(context, prqlCode); } /** @@ -299,27 +306,21 @@ export class SqlPreview { * from the active PRQL text editor. * * @param context Extension context. + * @param prqlCode Optional prql code overwrite to use + * instead of text from the active vscode PRQL editor. */ - private update(context: ExtensionContext) { + private update(context: ExtensionContext, prqlCode?: string) { // check active text editor const editor = window.activeTextEditor; if (this.webviewPanel.visible && editor && editor.document.languageId === 'prql' && editor.document.uri.fsPath === this.documentUri.fsPath) { - - // get updated prql code + // get updated prql code from the active PRQL text editor const prqlCode = editor.document.getText(); - this.compilePrql(prqlCode, this._lastSqlHtml).then((compilationResult) => { - if (compilationResult.status === 'ok') { - // save last valid sql html output to show when errors occur later - this._lastSqlHtml = compilationResult.html; - } - this.webviewPanel.webview.postMessage(compilationResult); - - // set sql preview flag and update sql output - this.resetSqlPreviewContext(); - context.workspaceState.update('prql.sql', compilationResult.sql); - }); + this.processPrql(context, prqlCode); + } + else if (prqlCode) { + this.processPrql(context, prqlCode); } if (!this.webviewPanel.visible || !this.webviewPanel.active) { @@ -348,6 +349,28 @@ export class SqlPreview { context.workspaceState.update('prql.sql', undefined); } + /** + * Processes given prql code. + * + * @param context Extension context. + * @param prqlCode PRQL code to process. + */ + private async processPrql(context: ExtensionContext, prqlCode: string) { + this.compilePrql(prqlCode, this._lastSqlHtml).then((compilationResult) => { + if (compilationResult.status === 'ok') { + // save last valid sql html output to show when errors occur later + this._lastSqlHtml = compilationResult.html; + } + + // update webview + this.webviewPanel.webview.postMessage(compilationResult); + + // reset active sql preview context and update last sql in workspace state + this.resetSqlPreviewContext(); + context.workspaceState.update('prql.sql', compilationResult.sql); + }); + } + /** * Compiles prql code and returns generated sql, * and formatted html sql compilation result. @@ -358,9 +381,11 @@ export class SqlPreview { */ private async compilePrql(prqlCode: string, lastSqlHtml: string | undefined): Promise { - const result = compile(prqlCode); + // compile given prql code + const result = compile(prqlCode); if (Array.isArray(result)) { + // return last valid sql html ouput with new error info return { status: 'error', error: { @@ -374,11 +399,7 @@ export class SqlPreview { const highlighter = await this.getHighlighter(); const sqlHtml = highlighter.codeToHtml(result, {lang: 'sql'}); - return { - status: 'ok', - html: sqlHtml, - sql: result, - }; + return {status: 'ok', html: sqlHtml, sql: result}; } /** diff --git a/src/views/sqlPreviewSerializer.ts b/src/views/sqlPreviewSerializer.ts index abbf01f..fc28bb4 100644 --- a/src/views/sqlPreviewSerializer.ts +++ b/src/views/sqlPreviewSerializer.ts @@ -41,7 +41,8 @@ export class SqlPreviewSerializer implements WebviewPanelSerializer { * @param state Saved web view panel state with preview PRQL document Url. */ async deserializeWebviewPanel(webviewPanel: WebviewPanel, state: any) { - const documentUri: Uri = Uri.parse(state.documentUrl); + const documentUri: Uri = Uri.file(state.documentUrl); + console.debug(`prql: ${state.documentUrl}`); SqlPreview.render(this.context, documentUri, webviewPanel, state); } } From bae92ff8697d512ba4e6eaeb2b9577e8191e461c Mon Sep 17 00:00:00 2001 From: RandomFractals Date: Sat, 18 Feb 2023 10:50:26 -0600 Subject: [PATCH 18/32] finalize webview reload and rework compilation results display - add new command field to all the webview messages - finish wring refresh with storing of document url in view state for the view restore on vscode reload - check if prql file exists prior to loading it in restored sql previews --- resources/sql-preview.html | 2 +- resources/sqlPreview.js | 35 ++++++++----------- src/views/compilationResult.ts | 4 +-- src/views/sqlPreview.ts | 57 +++++++++++++++++++------------ src/views/sqlPreviewSerializer.ts | 1 - 5 files changed, 53 insertions(+), 46 deletions(-) diff --git a/resources/sql-preview.html b/resources/sql-preview.html index 8148b28..48905ef 100644 --- a/resources/sql-preview.html +++ b/resources/sql-preview.html @@ -15,7 +15,7 @@