import * as path from 'path' ; import * as fs from 'fs' ; import * as nls from 'vscode-nls' ; import { xhr , XHRResponse , getErrorStatusDescription } from 'request-light' ; const localize = nls . loadMessageBundle ( ) ; import { workspace , window , languages , commands , ExtensionContext , extensions , Uri , LanguageConfiguration , Diagnostic , StatusBarAlignment , TextEditor , TextDocument , FormattingOptions , CancellationToken , ProviderResult , TextEdit , Range , Disposable } from 'vscode' ; import { LanguageClient , LanguageClientOptions , RequestType , ServerOptions , TransportKind , NotificationType , DidChangeConfigurationNotification , HandleDiagnosticsSignature , ResponseError , DocumentRangeFormattingParams , DocumentRangeFormattingRequest } from 'vscode-languageclient' ; import TelemetryReporter from 'vscode-extension-telemetry' ; import { hash } from './utils/hash' ; namespace VSCodeContentRequest { export const type = new RequestType ( 'vscode/content' ) ; } namespace SchemaContentChangeNotification { export const type = new NotificationType ( 'json/schemaContent' ) ; } namespace ForceValidateRequest { export const type = new RequestType ( 'json/validate' ) ; } export interface ISchemaAssociations { [ pattern ] : string [ ] ; } namespace SchemaAssociationNotification { export const type = new NotificationType ( 'json/schemaAssociations' ) ; } interface IPackageInfo { name : string ; version : string ; aiKey : string ; } interface Settings { json ? : { schemas ? : JSONSchemaSettings [ ] ; format ? : { enable : boolean ; } ; } ; http ? : { proxy ? : string ; proxyStrictSSL ? : boolean ; } ; } interface JSONSchemaSettings { fileMatch ? : string [ ] ; url ? : string ; schema ? : any ; } let telemetryReporter ; export function activate ( context ) { let toDispose = context . subscriptions ; let rangeFormatting = undefined ; let packageInfo = getPackageInfo ( context ) ; telemetryReporter = packageInfo && new TelemetryReporter ( packageInfo . name , packageInfo . version , packageInfo . aiKey ) ; let serverMain = readJSONFile ( context . asAbsolutePath ( './server/package.json' ) ) . main ; let serverModule = context . asAbsolutePath ( path . join ( 'server' , serverMain ) ) ; let debugOptions = { execArgv : [ '--nolazy' , '--inspect=' + ( 9000 + Math . round ( Math . random ( ) * 10000 ) ) ] } ; let serverOptions = { run : { module : serverModule , transport : TransportKind . ipc } , debug : { module : serverModule , transport : TransportKind . ipc , options : debugOptions } } ; let documentSelector = [ 'json' , 'jsonc' ] ; let schemaResolutionErrorStatusBarItem = window . createStatusBarItem ( { id : 'status.json.resolveError' , name : localize ( 'json.resolveError' , "JSON: Schema Resolution Error" ) , alignment : StatusBarAlignment . Right , priority : 0 } ) ; schemaResolutionErrorStatusBarItem . command = '_json.retryResolveSchema' ; schemaResolutionErrorStatusBarItem . tooltip = localize ( 'json.schemaResolutionErrorMessage' , 'Unable to resolve schema.' ) + ' ' + localize ( 'json.clickToRetry' , 'Click to retry.' ) ; schemaResolutionErrorStatusBarItem . text = '$(alert)' ; toDispose . push ( schemaResolutionErrorStatusBarItem ) ; let fileSchemaErrors = new Map < string , string > ( ) ; let clientOptions = { documentSelector , initializationOptions : { handledSchemaProtocols : [ 'file' ] , provideFormatter : false } , synchronize : { configurationSection : [ 'json' , 'http' ] , fileEvents : workspace . createFileSystemWatcher ( '**/*.json' ) } , middleware : { workspace : { didChangeConfiguration : ( ) => client . sendNotification ( DidChangeConfigurationNotification . type , { settings : getSettings ( ) } ) } , handleDiagnostics : ( uri , diagnostics , next ) => { const schemaErrorIndex = diagnostics . findIndex ( candidate => candidate . code === 0x300 ) ; if ( schemaErrorIndex === - 1 ) { fileSchemaErrors . delete ( uri . toString ( ) ) ; return next ( uri , diagnostics ) ; } const schemaResolveDiagnostic = diagnostics [ schemaErrorIndex ] ; fileSchemaErrors . set ( uri . toString ( ) , schemaResolveDiagnostic . message ) ; if ( window . activeTextEditor && window . activeTextEditor . document . uri . toString ( ) === uri . toString ( ) ) { schemaResolutionErrorStatusBarItem . show ( ) ; } next ( uri , diagnostics ) ; } } } ; let client = new LanguageClient ( 'json' , localize ( 'jsonserver.name' , 'JSON Language Server' ) , serverOptions , clientOptions ) ; client . registerProposedFeatures ( ) ; let disposable = client . start ( ) ; toDispose . push ( disposable ) ; client . onReady ( ) . then ( ( ) => { disposable = client . onTelemetry ( e => { if ( telemetryReporter ) { telemetryReporter . sendTelemetryEvent ( e . key , e . data ) ; } } ) ; const schemaDocuments = { } ; client . onRequest ( VSCodeContentRequest . type , ( uriPath ) => { let uri = Uri . parse ( uriPath ) ; if ( uri . scheme !== 'http' && uri . scheme !== 'https' ) { return workspace . openTextDocument ( uri ) . then ( doc => { schemaDocuments [ uri . toString ( ) ] = true ; return doc . getText ( ) ; } , error => { return Promise . reject ( error ) ; } ) ; } else { const headers = { 'Accept-Encoding' : 'gzip, deflate' } ; return xhr ( { url : uriPath , followRedirects : 5 , headers } ) . then ( response => { return response . responseText ; } , ( error ) => { return Promise . reject ( new ResponseError ( error . status , error . responseText || getErrorStatusDescription ( error . status ) || error . toString ( ) ) ) ; } ) ; } } ) ; let handleContentChange = ( uriString ) => { if ( schemaDocuments [ uriString ] ) { client . sendNotification ( SchemaContentChangeNotification . type , uriString ) ; return true ; } return false ; } ; let handleActiveEditorChange = ( activeEditor ? ) => { if ( ! activeEditor ) { return ; } const activeDocUri = activeEditor . document . uri . toString ( ) ; if ( activeDocUri && fileSchemaErrors . has ( activeDocUri ) ) { schemaResolutionErrorStatusBarItem . show ( ) ; } else { schemaResolutionErrorStatusBarItem . hide ( ) ; } } ; toDispose . push ( workspace . onDidChangeTextDocument ( e => handleContentChange ( e . document . uri . toString ( ) ) ) ) ; toDispose . push ( workspace . onDidCloseTextDocument ( d => { const uriString = d . uri . toString ( ) ; if ( handleContentChange ( uriString ) ) { delete schemaDocuments [ uriString ] ; } fileSchemaErrors . delete ( uriString ) ; } ) ) ; toDispose . push ( window . onDidChangeActiveTextEditor ( handleActiveEditorChange ) ) ; let handleRetryResolveSchemaCommand = ( ) => { if ( window . activeTextEditor ) { schemaResolutionErrorStatusBarItem . text = '$(watch)' ; const activeDocUri = window . activeTextEditor . document . uri . toString ( ) ; client . sendRequest ( ForceValidateRequest . type , activeDocUri ) . then ( ( diagnostics ) => { const schemaErrorIndex = diagnostics . findIndex ( candidate => candidate . code === 0x300 ) ; if ( schemaErrorIndex !== - 1 ) { const schemaResolveDiagnostic = diagnostics [ schemaErrorIndex ] ; fileSchemaErrors . set ( activeDocUri , schemaResolveDiagnostic . message ) ; } else { schemaResolutionErrorStatusBarItem . hide ( ) ; } schemaResolutionErrorStatusBarItem . text = '$(alert)' ; } ) ; } } ; toDispose . push ( commands . registerCommand ( '_json.retryResolveSchema' , handleRetryResolveSchemaCommand ) ) ; client . sendNotification ( SchemaAssociationNotification . type , getSchemaAssociation ( context ) ) ; extensions . onDidChange ( _ => { client . sendNotification ( SchemaAssociationNotification . type , getSchemaAssociation ( context ) ) ; } ) ; updateFormatterRegistration ( ) ; toDispose . push ( { dispose : ( ) => rangeFormatting && rangeFormatting . dispose ( ) } ) ; toDispose . push ( workspace . onDidChangeConfiguration ( e => e . affectsConfiguration ( 'html.format.enable' ) && updateFormatterRegistration ( ) ) ) ; } ) ; let languageConfiguration = { wordPattern : /("(?:[^\\\"]*(?:\\.)?)*"?)|[^\s{}\[\],:]+/ , indentationRules : { increaseIndentPattern : /({+(?=([^"]*"[^"]*")*[^"}]*$))|(\[+(?=([^"]*"[^"]*")*[^"\]]*$))/ , decreaseIndentPattern : /^\s*[}\]],?\s*$/ } } ; languages . setLanguageConfiguration ( 'json' , languageConfiguration ) ; languages . setLanguageConfiguration ( 'jsonc' , languageConfiguration ) ; function updateFormatterRegistration ( ) { const formatEnabled = workspace . getConfiguration ( ) . get ( 'json.format.enable' ) ; if ( ! formatEnabled && rangeFormatting ) { rangeFormatting . dispose ( ) ; rangeFormatting = undefined ; } else if ( formatEnabled && ! rangeFormatting ) { rangeFormatting = languages . registerDocumentRangeFormattingEditProvider ( documentSelector , { provideDocumentRangeFormattingEdits ( document , range , options , token ) { let params = { textDocument : client . code2ProtocolConverter . asTextDocumentIdentifier ( document ) , range : client . code2ProtocolConverter . asRange ( range ) , options : client . code2ProtocolConverter . asFormattingOptions ( options ) } ; return client . sendRequest ( DocumentRangeFormattingRequest . type , params , token ) . then ( client . protocol2CodeConverter . asTextEdits , ( error ) => { client . logFailedRequest ( DocumentRangeFormattingRequest . type , error ) ; return Promise . resolve ( [ ] ) ; } ) ; } } ) ; } } } export function deactivate ( ) { return telemetryReporter ? telemetryReporter . dispose ( ) : Promise . resolve ( null ) ; } function getSchemaAssociation ( _context ) { let associations = { } ; extensions . all . forEach ( extension => { let packageJSON = extension . packageJSON ; if ( packageJSON && packageJSON . contributes && packageJSON . contributes . jsonValidation ) { let jsonValidation = packageJSON . contributes . jsonValidation ; if ( Array . isArray ( jsonValidation ) ) { jsonValidation . forEach ( jv => { let { fileMatch , url } = jv ; if ( fileMatch && url ) { if ( url [ 0 ] === '.' && url [ 1 ] === '/' ) { url = Uri . file ( path . join ( extension . extensionPath , url ) ) . toString ( ) ; } if ( fileMatch [ 0 ] === '%' ) { fileMatch = fileMatch . replace ( /%APP_SETTINGS_HOME%/ , '/User' ) ; fileMatch = fileMatch . replace ( /%MACHINE_SETTINGS_HOME%/ , '/Machine' ) ; fileMatch = fileMatch . replace ( /%APP_WORKSPACES_HOME%/ , '/Workspaces' ) ; } else if ( fileMatch . charAt ( 0 ) !== '/' && ! fileMatch . match ( /\w+:\/\// ) ) { fileMatch = '/' + fileMatch ; } let association = associations [ fileMatch ] ; if ( ! association ) { association = [ ] ; associations [ fileMatch ] = association ; } association . push ( url ) ; } } ) ; } } } ) ; return associations ; } function getSettings ( ) { let httpSettings = workspace . getConfiguration ( 'http' ) ; let settings = { http : { proxy : httpSettings . get ( 'proxy' ) , proxyStrictSSL : httpSettings . get ( 'proxyStrictSSL' ) } , json : { schemas : [ ] , } } ; let schemaSettingsById = Object . create ( null ) ; let collectSchemaSettings = ( schemaSettings , rootPath ? , fileMatchPrefix ? ) => { for ( let setting of schemaSettings ) { let url = getSchemaId ( setting , rootPath ) ; if ( ! url ) { continue ; } let schemaSetting = schemaSettingsById [ url ] ; if ( ! schemaSetting ) { schemaSetting = schemaSettingsById [ url ] = { url , fileMatch : [ ] } ; settings . json ! . schemas ! . push ( schemaSetting ) ; } let fileMatches = setting . fileMatch ; let resultingFileMatches = schemaSetting . fileMatch ! ; if ( Array . isArray ( fileMatches ) ) { if ( fileMatchPrefix ) { for ( let fileMatch of fileMatches ) { if ( fileMatch [ 0 ] === '/' ) { resultingFileMatches . push ( fileMatchPrefix + fileMatch ) ; resultingFileMatches . push ( fileMatchPrefix + '/*' + fileMatch ) ; } else { resultingFileMatches . push ( fileMatchPrefix + '/' + fileMatch ) ; resultingFileMatches . push ( fileMatchPrefix + '/*/' + fileMatch ) ; } } } else { resultingFileMatches . push ( ... fileMatches ) ; } } if ( setting . schema ) { schemaSetting . schema = setting . schema ; } } } ; let globalSettings = workspace . getConfiguration ( 'json' , null ) . get < JSONSchemaSettings [ ] > ( 'schemas' ) ; if ( Array . isArray ( globalSettings ) ) { collectSchemaSettings ( globalSettings , workspace . rootPath ) ; } let folders = workspace . workspaceFolders ; if ( folders ) { for ( let folder of folders ) { let folderUri = folder . uri ; let schemaConfigInfo = workspace . getConfiguration ( 'json' , folderUri ) . inspect < JSONSchemaSettings [ ] > ( 'schemas' ) ; let folderSchemas = schemaConfigInfo ! . workspaceFolderValue ; if ( Array . isArray ( folderSchemas ) ) { let folderPath = folderUri . toString ( ) ; if ( folderPath [ folderPath . length - 1 ] === '/' ) { folderPath = folderPath . substr ( 0 , folderPath . length - 1 ) ; } collectSchemaSettings ( folderSchemas , folderUri . fsPath , folderPath ) ; } } } return settings ; } function getSchemaId ( schema , rootPath ? ) { let url = schema . url ; if ( ! url ) { if ( schema . schema ) { url = schema . schema . id || `vscode://schemas/custom/${ encodeURIComponent ( hash ( schema . schema ) . toString ( 16 ) ) }` ; } } else if ( rootPath && ( url [ 0 ] === '.' || url [ 0 ] === '/' ) ) { url = Uri . file ( path . normalize ( path . join ( rootPath , url ) ) ) . toString ( ) ; } return url ; } function getPackageInfo ( context ) { let extensionPackage = readJSONFile ( context . asAbsolutePath ( './package.json' ) ) ; if ( extensionPackage ) { return { name : extensionPackage . name , version : extensionPackage . version , aiKey : extensionPackage . aiKey } ; } return undefined ; } function readJSONFile ( location ) { try { return JSON . parse ( fs . readFileSync ( location ) . toString ( ) ) ; } catch ( e ) { console . log ( `Problems reading ${ location }: ${ e }` ) ; return { } ; } }