import * as vsc from 'vscode' ; import { Disposable , disposeAll } from './dispose' ; import { WebviewCollection } from './util' ; interface SQLiteEdit { readonly data : Uint8Array ; } interface SQLiteDocumentDelegate { getFileData ( ) : Promise < Uint8Array > ; } class SQLiteDocument extends Disposable implements vsc . CustomDocument { static async create ( uri , backupId , delegate ) { const dataFile = typeof backupId === 'string' ? vsc . Uri . parse ( backupId ) : uri ; const fileData = await SQLiteDocument . readFile ( dataFile ) ; return new SQLiteDocument ( uri , fileData , delegate ) ; } private static async readFile ( uri ) { if ( uri . scheme === 'untitled' ) { return new Uint8Array ( ) ; } return vsc . workspace . fs . readFile ( uri ) ; } private readonly _uri : vsc . Uri ; private _documentData : Uint8Array ; private _edits : Array < SQLiteEdit > = [ ] ; private _savedEdits : Array < SQLiteEdit > = [ ] ; private readonly _delegate : SQLiteDocumentDelegate ; private constructor ( uri , initialContent , delegate ) { super ( ) ; this . _uri = uri ; this . _documentData = initialContent ; this . _delegate = delegate ; } public get uri ( ) { return this . _uri ; } private pathRegExp = /(?<dirname>.*)\/(?<filename>(?<basename>.*)(?<extname>\.[^.]+))$/ ; public get uriParts ( ) { const { dirname , filename , basename , extname } = this . _uri . toString ( ) . match ( this . pathRegExp ) ?. groups ?? { } ; return { dirname , filename , basename , extname } ; } public get documentData ( ) : Uint8Array { return this . _documentData ; } private readonly _onDidDispose = this . _register ( new vsc . EventEmitter < void > ( ) ) ; /**
     * Fired when the document is disposed of.
     */ public readonly onDidDispose = this . _onDidDispose . event ; private readonly _onDidChangeDocument = this . _register ( new vsc . EventEmitter < { readonly content ? : Uint8Array ; readonly edits : readonly SQLiteEdit [ ] ; } > ( ) ) ; /**
     * Fired to notify webviews that the document has changed.
     */ public readonly onDidChangeContent = this . _onDidChangeDocument . event ; private readonly _onDidChange = this . _register ( new vsc . EventEmitter < { readonly label : string ; undo ( ) : void ; redo ( ) : void ; } > ( ) ) ; /**
     * Fired to tell VS Code that an edit has occurred in the document.
     *
     * This updates the document's dirty indicator.
     */ public readonly onDidChange = this . _onDidChange . event ; /**
     * Called by VS Code when there are no more references to the document.
     *
     * This happens when all editors for it have been closed.
     */ dispose ( ) { this . _onDidDispose . fire ( ) ; super . dispose ( ) ; } /**
     * Called when the user edits the document in a webview.
     *
     * This fires an event to notify VS Code that the document has been edited.
     */ makeEdit ( edit ) { this . _edits . push ( edit ) ; this . _onDidChange . fire ( { label : 'Stroke' , undo : async ( ) => { this . _edits . pop ( ) ; this . _onDidChangeDocument . fire ( { edits : this . _edits , } ) ; } , redo : async ( ) => { this . _edits . push ( edit ) ; this . _onDidChangeDocument . fire ( { edits : this . _edits , } ) ; } } ) ; } /**
     * Called by VS Code when the user saves the document.
     */ async save ( cancellation ) { await this . saveAs ( this . uri , cancellation ) ; this . _savedEdits = Array . from ( this . _edits ) ; } /**
     * Called by VS Code when the user saves the document to a new location.
     */ async saveAs ( targetResource , cancellation ) { const fileData = await this . _delegate . getFileData ( ) ; if ( cancellation . isCancellationRequested ) { return ; } await vsc . workspace . fs . writeFile ( targetResource , fileData ) ; } /**
     * Called by VS Code when the user calls `revert` on a document.
     */ async revert ( _cancellation ) { const diskContent = await SQLiteDocument . readFile ( this . uri ) ; this . _documentData = diskContent ; this . _edits = this . _savedEdits ; this . _onDidChangeDocument . fire ( { content : diskContent , edits : this . _edits , } ) ; } async refresh ( _cancellation ? ) { const diskContent = await SQLiteDocument . readFile ( this . uri ) ; this . _documentData = diskContent ; this . _edits = [ ] ; this . _onDidChangeDocument . fire ( { content : diskContent , edits : [ ] , } ) ; } /**
     * Called by VS Code to backup the edited document.
     *
     * These backups are used to implement hot exit.
     */ async backup ( destination , cancellation ) { await this . saveAs ( destination , cancellation ) ; return { id : destination . toString ( ) , delete : async ( ) => { try { await vsc . workspace . fs . delete ( destination ) ; } catch { } } } ; } } const $default = 'default-src' ; const $script = 'script-src' ; const $style = 'style-src' ; const $img = 'img-src' ; const $font = 'font-src' ; const $child = 'child-src' ; const $self = "'self'" ; const $vscode = 'vscode-resource: qwtel.vscode-unpkg.net' ; const $data = 'data:' ; const $blob = 'blob:' ; const $inlineStyle = "'unsafe-inline'" ; const $unsafeEval = "'unsafe-eval'" ; const buildCSP = ( cspObj ) => Object . entries ( cspObj ) . map ( ( [ k , vs ] ) => `${ k } ${ vs . join ( ' ' ) };` ) . join ( ' ' ) ; class SQLiteEditorProvider implements vsc . CustomEditorProvider < SQLiteDocument > { private readonly webviews = new WebviewCollection ( ) ; constructor ( private readonly _context ) { } async openCustomDocument ( uri , openContext , _token ) { const document = await SQLiteDocument . create ( uri , openContext . backupId , { getFileData : async ( ) => { const webviewsForDocument = [ ... this . webviews . get ( document . uri ) ] ; if ( ! webviewsForDocument . length ) { throw new Error ( 'Could not find webview to save for' ) ; } const panel = webviewsForDocument [ 0 ] ; const response = await this . postMessageWithResponse < number [ ] > ( panel , 'getFileData' , { } ) ; return new Uint8Array ( response ) ; } } ) ; const listeners = [ ] ; listeners . push ( document . onDidChange ( e => { this . _onDidChangeCustomDocument . fire ( { document , ... e , } ) ; } ) ) ; listeners . push ( document . onDidChangeContent ( e => { for ( const webviewPanel of this . webviews . get ( document . uri ) ) { const { filename } = document . uriParts ; const { buffer , byteOffset , byteLength } = document . documentData ; const value = { buffer , byteOffset , byteLength } ; this . postMessage ( webviewPanel , 'update' , { filename , value , editable : false , } , [ buffer ] ) ; } } ) ) ; document . onDidDispose ( ( ) => disposeAll ( listeners ) ) ; return document ; } async resolveCustomEditor ( document , webviewPanel , token ) { this . webviews . add ( document . uri , webviewPanel ) ; webviewPanel . webview . options = { enableScripts : true , } ; webviewPanel . webview . html = await this . getHtmlForWebview ( webviewPanel . webview ) ; webviewPanel . webview . onDidReceiveMessage ( e => this . onMessage ( document , e ) ) ; webviewPanel . webview . onDidReceiveMessage ( e => { if ( e . type === 'ready' && this . webviews . has ( document . uri ) ) { if ( document . uri . scheme === 'untitled' ) { this . postMessage ( webviewPanel , 'init' , { filename : 'untitled' , untitled : true , editable : false , } ) ; } else { const editable = false ; const { buffer , byteOffset , byteLength } = document . documentData ; const value = { buffer , byteOffset , byteLength } ; const { filename } = document . uriParts ; this . postMessage ( webviewPanel , 'init' , { filename , value , editable , } , [ buffer ] ) ; } } } ) ; } private readonly _onDidChangeCustomDocument = new vsc . EventEmitter < vsc . CustomDocumentEditEvent < SQLiteDocument > > ( ) ; public readonly onDidChangeCustomDocument = this . _onDidChangeCustomDocument . event ; public saveCustomDocument ( document , cancellation ) { return document . save ( cancellation ) ; } public saveCustomDocumentAs ( document , destination , cancellation ) { return document . saveAs ( destination , cancellation ) ; } public revertCustomDocument ( document , cancellation ) { return document . revert ( cancellation ) ; } public backupCustomDocument ( document , context , cancellation ) { return document . backup ( context . destination , cancellation ) ; } private async getHtmlForWebview ( webview ) { const publicUri = vsc . Uri . joinPath ( this . _context . extensionUri , 'sqlite-viewer-app' , 'public' ) ; const codiconsUri = vsc . Uri . joinPath ( this . _context . extensionUri , 'node_modules' , '@vscode/codicons' , 'dist' , 'codicon.css' ) ; const html = new TextDecoder ( ) . decode ( await vsc . workspace . fs . readFile ( vsc . Uri . joinPath ( publicUri , 'vscode.html' ) ) ) ; const PUBLIC_URL = webview . asWebviewUri ( vsc . Uri . joinPath ( this . _context . extensionUri , 'sqlite-viewer-app' , 'public' ) ) . toString ( ) ; const csp = { [ $default ] : [ $self , $vscode ] , [ $script ] : [ $self , $vscode , $unsafeEval ] , [ $style ] : [ $self , $vscode , $inlineStyle ] , [ $img ] : [ $self , $vscode , $data ] , [ $font ] : [ $self , $vscode ] , [ $child ] : [ $blob ] , } ; const prepHtml = html . replaceAll ( '%PUBLIC_URL%' , PUBLIC_URL ) . replaceAll ( '%REACT_APP_CSP%' , buildCSP ( csp ) ) . replace ( '<!--HEAD-->' , `
        <link rel="stylesheet" href="${ webview . asWebviewUri ( vsc . Uri . joinPath ( publicUri , 'bundle.css' ) ) }"/>
        <link rel="stylesheet" href="${ webview . asWebviewUri ( codiconsUri ) }"/>
      ` ) . replace ( '<!--BODY-->' , `
        <script src="${ webview . asWebviewUri ( vsc . Uri . joinPath ( publicUri , 'bundle.js' ) ) }"></script>
      ` ) ; return prepHtml ; } private _requestId = 1 ; private readonly _callbacks = new Map < number , ( response : any ) => void > ( ) ; private postMessageWithResponse < R = unknown > ( panel , type , body ) { const requestId = this . _requestId ++ ; const p = new Promise < R > ( resolve => this . _callbacks . set ( requestId , resolve ) ) ; panel . webview . postMessage ( { type , requestId , body } ) ; return p ; } private postMessage ( panel , type , body , transfer ? ) { panel . webview . postMessage ( { type , body } , transfer ) ; } private async onMessage ( document , message ) { switch ( message . type ) { case 'refresh' : if ( document . uri . scheme !== 'untitled' ) { document . refresh ( ) ; } return ; case 'blob' : const { data , download , metaKey } = message ; const { dirname } = document . uriParts ; const dlUri = vsc . Uri . parse ( `${ dirname }/${ download }` ) ; await vsc . workspace . fs . writeFile ( dlUri , data ) ; if ( ! metaKey ) await vsc . commands . executeCommand ( 'vscode.open' , dlUri ) ; return ; case 'response' : { const callback = this . _callbacks . get ( message . requestId ) ; callback ?. ( message . body ) ; return ; } } } } const registerOptions = { webviewOptions : { retainContextWhenHidden : true , } , supportsMultipleEditorsPerDocument : false , } ; export class SQLiteEditorDefaultProvider extends SQLiteEditorProvider { static viewType = 'sqlite-viewer.view' ; public static register ( context ) { return vsc . window . registerCustomEditorProvider ( SQLiteEditorDefaultProvider . viewType , new SQLiteEditorDefaultProvider ( context ) , registerOptions ) ; } } export class SQLiteEditorOptionProvider extends SQLiteEditorProvider { static viewType = 'sqlite-viewer.option' ; public static register ( context ) { return vsc . window . registerCustomEditorProvider ( SQLiteEditorOptionProvider . viewType , new SQLiteEditorOptionProvider ( context ) , registerOptions ) ; } }