import { Connection } from '@salesforce/core' ; import { JsonMap } from '@salesforce/ts-types' ; import { debounce } from 'debounce' ; import { DescribeSObjectResult , QueryResult } from 'jsforce' ; import * as vscode from 'vscode' ; import { trackErrorWithTelemetry } from '../commonUtils' ; import { nls } from '../messages' ; import { QueryDataViewService as QueryDataView } from '../queryDataView/queryDataViewService' ; import { channelService , isDefaultOrgSet , onOrgChange , retrieveSObject , retrieveSObjects , workspaceContext } from '../sfdx' ; import { TelemetryModelJson } from '../telemetry' ; import { QueryRunner } from './queryRunner' ; export interface SoqlEditorEvent { type : string ; payload ? : string | string [ ] | JsonMap ; } export enum MessageType { UI_ACTIVATED = 'ui_activated' , UI_SOQL_CHANGED = 'ui_soql_changed' , UI_TELEMETRY = 'ui_telemetry' , SOBJECT_METADATA_REQUEST = 'sobject_metadata_request' , SOBJECT_METADATA_RESPONSE = 'sobject_metadata_response' , SOBJECTS_REQUEST = 'sobjects_request' , SOBJECTS_RESPONSE = 'sobjects_response' , TEXT_SOQL_CHANGED = 'text_soql_changed' , RUN_SOQL_QUERY = 'run_query' , CONNECTION_CHANGED = 'connection_changed' , RUN_SOQL_QUERY_DONE = 'run_query_done' } class ConnectionChangedListener { protected editorInstances : SOQLEditorInstance [ ] ; protected static instance : ConnectionChangedListener ; protected constructor ( ) { onOrgChange ( async ( orgInfo ) => { await this . connectionChanged ( ) ; } ) ; this . editorInstances = [ ] ; } public static getInstance ( ) { if ( ! ConnectionChangedListener . instance ) { ConnectionChangedListener . instance = new ConnectionChangedListener ( ) ; } return ConnectionChangedListener . instance ; } public addSoqlEditor ( editor ) { this . editorInstances . push ( editor ) ; } public removeSoqlEditor ( editor ) { this . editorInstances = this . editorInstances . filter ( instance => instance !== editor ) ; } public async connectionChanged ( ) { this . editorInstances . forEach ( editor => editor . onConnectionChanged ( ) ) ; } } export class SOQLEditorInstance { public subscriptions : vscode . Disposable [ ] = [ ] ; protected lastIncomingSoqlStatement = '' ; protected disposedCallback : ( ( instance : SOQLEditorInstance ) => void ) | undefined ; constructor ( protected document , protected webviewPanel , protected _token ) { vscode . workspace . onDidChangeTextDocument ( debounce ( this . onDocumentChangeHandler , 1000 ) , this , this . subscriptions ) ; webviewPanel . webview . onDidReceiveMessage ( this . onDidRecieveMessageHandler , this , this . subscriptions ) ; ConnectionChangedListener . getInstance ( ) . addSoqlEditor ( this ) ; webviewPanel . onDidDispose ( this . dispose , this , this . subscriptions ) ; } protected sendMessageToUi ( type , payload ? ) { this . webviewPanel . webview . postMessage ( { type , payload } ) . then ( undefined , ( err ) => { const message = nls . localize ( 'error_unknown_error' , 'web_view_post_message' ) ; channelService . appendLine ( message ) ; trackErrorWithTelemetry ( type , err ) ; } ) ; } protected updateWebview ( document ) { const newSoqlStatement = document . getText ( ) ; if ( this . lastIncomingSoqlStatement !== newSoqlStatement ) { this . sendMessageToUi ( MessageType . TEXT_SOQL_CHANGED , newSoqlStatement ) ; } this . lastIncomingSoqlStatement = '' ; } protected updateSObjects ( sobjectNames ) { this . sendMessageToUi ( MessageType . SOBJECTS_RESPONSE , sobjectNames ) ; } protected updateSObjectMetadata ( sobject ) { this . sendMessageToUi ( MessageType . SOBJECT_METADATA_RESPONSE , sobject ) ; } protected onDocumentChangeHandler ( e ) { if ( e . document . uri . toString ( ) === this . document . uri . toString ( ) ) { this . updateWebview ( this . document ) ; } } protected onDidRecieveMessageHandler ( event ) { switch ( event . type ) { case MessageType . UI_ACTIVATED : { this . updateWebview ( this . document ) ; break ; } case MessageType . UI_SOQL_CHANGED : { const soql = event . payload as string ; this . lastIncomingSoqlStatement = soql ; this . updateTextDocument ( this . document , soql ) ; break ; } case MessageType . UI_TELEMETRY : { const { unsupported } = event . payload as TelemetryModelJson ; const hasUnsupported = Array . isArray ( unsupported ) ? unsupported . length : unsupported ; if ( hasUnsupported ) { trackErrorWithTelemetry ( 'syntax_unsupported' , JSON . stringify ( event . payload ) ) . catch ( console . error ) ; const message = nls . localize ( 'info_syntax_unsupported' ) ; channelService . appendLine ( message ) ; } break ; } case MessageType . SOBJECT_METADATA_REQUEST : { retrieveSObject ( event . payload as string ) . then ( sobject => this . updateSObjectMetadata ( sobject ) ) . catch ( ( ) => { const message = nls . localize ( 'error_sobject_metadata_request' , event . payload ) ; channelService . appendLine ( message ) ; } ) ; break ; } case MessageType . SOBJECTS_REQUEST : { retrieveSObjects ( ) . then ( sobjectNames => this . updateSObjects ( sobjectNames ) ) . catch ( ( ) => { const message = nls . localize ( 'error_sobjects_request' ) ; channelService . appendLine ( message ) ; } ) ; break ; } case MessageType . RUN_SOQL_QUERY : { vscode . window . withProgress ( { cancellable : false , location : vscode . ProgressLocation . Notification , title : nls . localize ( 'progress_running_query' ) } , ( ) => this . handleRunQuery ( ) ) . then ( undefined , err => { const message = nls . localize ( 'error_run_soql_query' , err . message ) ; channelService . appendLine ( message ) ; this . runQueryDone ( ) ; } ) ; break ; } default : { const message = nls . localize ( 'error_unknown_error' , event . type ) ; channelService . appendLine ( message ) ; trackErrorWithTelemetry ( 'message_unknown' , event . type ) . catch ( console . error ) ; } } } protected async handleRunQuery ( ) { if ( ! isDefaultOrgSet ( ) ) { const message = nls . localize ( 'info_no_default_org' ) ; channelService . appendLine ( message ) ; vscode . window . showInformationMessage ( message ) ; this . runQueryDone ( ) ; return Promise . resolve ( ) ; } const queryText = this . document . getText ( ) ; const conn = await workspaceContext . getConnection ( ) ; const queryData = await new QueryRunner ( ( conn as unknown ) as Connection ) . runQuery ( queryText ) ; this . openQueryDataView ( queryData ) ; this . runQueryDone ( ) ; } protected runQueryDone ( ) { this . webviewPanel . webview . postMessage ( { type : MessageType . RUN_SOQL_QUERY_DONE } ) ; } protected openQueryDataView ( queryData ) { const webview = new QueryDataView ( this . subscriptions , queryData , this . document ) ; webview . createOrShowWebView ( ) ; } protected updateTextDocument ( document , soqlQuery ) { const edit = new vscode . WorkspaceEdit ( ) ; edit . replace ( document . uri , new vscode . Range ( 0 , 0 , document . lineCount , 0 ) , soqlQuery ) ; return vscode . workspace . applyEdit ( edit ) ; } protected dispose ( ) { ConnectionChangedListener . getInstance ( ) . removeSoqlEditor ( this ) ; this . subscriptions . forEach ( dispposable => dispposable . dispose ( ) ) ; if ( this . disposedCallback ) { this . disposedCallback ( this ) ; } } public onDispose ( callback ) { this . disposedCallback = callback ; } public onConnectionChanged ( ) { this . sendMessageToUi ( MessageType . CONNECTION_CHANGED ) ; } }