import { createConnection , IConnection , TextDocuments , TextDocument , InitializeParams , InitializeResult , NotificationType , RequestType , DocumentRangeFormattingRequest , Disposable , ServerCapabilities , Diagnostic } from 'vscode-languageserver' ; import { xhr , XHRResponse , configure as configureHttpRequests , getErrorStatusDescription } from 'request-light' ; import * as fs from 'fs' ; import { URI } from 'vscode-uri' ; import * as URL from 'url' ; import { formatError , runSafe , runSafeAsync } from './utils/runner' ; import { JSONDocument , JSONSchema , getLanguageService , DocumentLanguageSettings , SchemaConfiguration , ClientCapabilities } from 'vscode-json-languageservice' ; import { getLanguageModelCache } from './languageModelCache' ; interface ISchemaAssociations { [ pattern ] : string [ ] ; } namespace SchemaAssociationNotification { export const type = new NotificationType ( 'json/schemaAssociations' ) ; } 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' ) ; } const connection = createConnection ( ) ; process . on ( 'unhandledRejection' , ( e ) => { console . error ( formatError ( `Unhandled exception` , e ) ) ; } ) ; process . on ( 'uncaughtException' , ( e ) => { console . error ( formatError ( `Unhandled exception` , e ) ) ; } ) ; console . log = connection . console . log . bind ( connection . console ) ; console . error = connection . console . error . bind ( connection . console ) ; const workspaceContext = { resolveRelativePath : ( relativePath , resource ) => { return URL . resolve ( resource , relativePath ) ; } } ; function getSchemaRequestService ( handledSchemas ) { return ( uri ) : Thenable < string > => { const protocol = uri . substr ( 0 , uri . indexOf ( ':' ) ) ; if ( ! handledSchemas || handledSchemas [ protocol ] ) { if ( protocol === 'file' ) { const fsPath = URI . parse ( uri ) . fsPath ; return new Promise < string > ( ( c , e ) => { fs . readFile ( fsPath , 'UTF-8' , ( err , result ) => { err ? e ( err . message || err . toString ( ) ) : c ( result . toString ( ) ) ; } ) ; } ) ; } else if ( protocol === 'http' || protocol === 'https' ) { if ( uri . indexOf ( '//schema.management.azure.com/' ) !== - 1 ) { connection . telemetry . logEvent ( { key : 'json.schema' , value : { schemaURL : uri } } ) ; } const headers = { 'Accept-Encoding' : 'gzip, deflate' } ; return xhr ( { url : uri , followRedirects : 5 , headers } ) . then ( response => { return response . responseText ; } , ( error ) => { return Promise . reject ( error . responseText || getErrorStatusDescription ( error . status ) || error . toString ( ) ) ; } ) ; } } return connection . sendRequest ( VSCodeContentRequest . type , uri ) . then ( responseText => { return responseText ; } , error => { return Promise . reject ( error . message ) ; } ) ; } ; } let languageService = getLanguageService ( { workspaceContext , contributions : [ ] , clientCapabilities : ClientCapabilities . LATEST } ) ; const documents = new TextDocuments ( ) ; documents . listen ( connection ) ; let clientSnippetSupport = false ; let dynamicFormatterRegistration = false ; let foldingRangeLimit = Number . MAX_VALUE ; let hierarchicalDocumentSymbolSupport = false ; connection . onInitialize ( ( params ) : InitializeResult => { const handledProtocols = params . initializationOptions && params . initializationOptions [ 'handledSchemaProtocols' ] ; languageService = getLanguageService ( { schemaRequestService : getSchemaRequestService ( handledProtocols ) , workspaceContext , contributions : [ ] , clientCapabilities : params . capabilities } ) ; function getClientCapability < T > ( name , def ) { const keys = name . split ( '.' ) ; let c = params . capabilities ; for ( let i = 0 ; c && i < keys . length ; i ++ ) { if ( ! c . hasOwnProperty ( keys [ i ] ) ) { return def ; } c = c [ keys [ i ] ] ; } return c ; } clientSnippetSupport = getClientCapability ( 'textDocument.completion.completionItem.snippetSupport' , false ) ; dynamicFormatterRegistration = getClientCapability ( 'textDocument.rangeFormatting.dynamicRegistration' , false ) && ( typeof params . initializationOptions . provideFormatter !== 'boolean' ) ; foldingRangeLimit = getClientCapability ( 'textDocument.foldingRange.rangeLimit' , Number . MAX_VALUE ) ; hierarchicalDocumentSymbolSupport = getClientCapability ( 'textDocument.documentSymbol.hierarchicalDocumentSymbolSupport' , false ) ; const capabilities = { textDocumentSync : documents . syncKind , completionProvider : clientSnippetSupport ? { resolveProvider : true , triggerCharacters : [ '"' , ':' ] } : undefined , hoverProvider : true , documentSymbolProvider : true , documentRangeFormattingProvider : params . initializationOptions . provideFormatter === true , colorProvider : { } , foldingRangeProvider : true , selectionRangeProvider : true } ; return { capabilities } ; } ) ; interface Settings { json : { schemas : JSONSchemaSettings [ ] ; format : { enable : boolean ; } ; } ; http : { proxy : string ; proxyStrictSSL : boolean ; } ; } interface JSONSchemaSettings { fileMatch ? : string [ ] ; url ? : string ; schema ? : JSONSchema ; } let jsonConfigurationSettings = undefined ; let schemaAssociations = undefined ; let formatterRegistration = null ; connection . onDidChangeConfiguration ( ( change ) => { let settings = < Settings > change . settings ; configureHttpRequests ( settings . http && settings . http . proxy , settings . http && settings . http . proxyStrictSSL ) ; jsonConfigurationSettings = settings . json && settings . json . schemas ; updateConfiguration ( ) ; if ( dynamicFormatterRegistration ) { const enableFormatter = settings && settings . json && settings . json . format && settings . json . format . enable ; if ( enableFormatter ) { if ( ! formatterRegistration ) { formatterRegistration = connection . client . register ( DocumentRangeFormattingRequest . type , { documentSelector : [ { language : 'json' } , { language : 'jsonc' } ] } ) ; } } else if ( formatterRegistration ) { formatterRegistration . then ( r => r . dispose ( ) ) ; formatterRegistration = null ; } } } ) ; connection . onNotification ( SchemaAssociationNotification . type , associations => { schemaAssociations = associations ; updateConfiguration ( ) ; } ) ; connection . onNotification ( SchemaContentChangeNotification . type , uri => { languageService . resetSchema ( uri ) ; } ) ; connection . onRequest ( ForceValidateRequest . type , uri => { return new Promise < Diagnostic [ ] > ( resolve => { const document = documents . get ( uri ) ; if ( document ) { updateConfiguration ( ) ; validateTextDocument ( document , diagnostics => { resolve ( diagnostics ) ; } ) ; } else { resolve ( [ ] ) ; } } ) ; } ) ; function updateConfiguration ( ) { const languageSettings = { validate : true , allowComments : true , schemas : new Array < SchemaConfiguration > ( ) } ; if ( schemaAssociations ) { for ( const pattern in schemaAssociations ) { const association = schemaAssociations [ pattern ] ; if ( Array . isArray ( association ) ) { association . forEach ( uri => { languageSettings . schemas . push ( { uri , fileMatch : [ pattern ] } ) ; } ) ; } } } if ( jsonConfigurationSettings ) { jsonConfigurationSettings . forEach ( ( schema , index ) => { let uri = schema . url ; if ( ! uri && schema . schema ) { uri = schema . schema . id || `vscode://schemas/custom/${ index }` ; } if ( uri ) { languageSettings . schemas . push ( { uri , fileMatch : schema . fileMatch , schema : schema . schema } ) ; } } ) ; } languageService . configure ( languageSettings ) ; documents . all ( ) . forEach ( triggerValidation ) ; } documents . onDidChangeContent ( ( change ) => { triggerValidation ( change . document ) ; } ) ; documents . onDidClose ( event => { cleanPendingValidation ( event . document ) ; connection . sendDiagnostics ( { uri : event . document . uri , diagnostics : [ ] } ) ; } ) ; const pendingValidationRequests = { } ; const validationDelayMs = 500 ; function cleanPendingValidation ( textDocument ) { const request = pendingValidationRequests [ textDocument . uri ] ; if ( request ) { clearTimeout ( request ) ; delete pendingValidationRequests [ textDocument . uri ] ; } } function triggerValidation ( textDocument ) { cleanPendingValidation ( textDocument ) ; pendingValidationRequests [ textDocument . uri ] = setTimeout ( ( ) => { delete pendingValidationRequests [ textDocument . uri ] ; validateTextDocument ( textDocument ) ; } , validationDelayMs ) ; } function validateTextDocument ( textDocument , callback ? ) { const respond = ( diagnostics ) => { connection . sendDiagnostics ( { uri : textDocument . uri , diagnostics } ) ; if ( callback ) { callback ( diagnostics ) ; } } ; if ( textDocument . getText ( ) . length === 0 ) { respond ( [ ] ) ; return ; } const jsonDocument = getJSONDocument ( textDocument ) ; const version = textDocument . version ; const documentSettings = textDocument . languageId === 'jsonc' ? { comments : 'ignore' , trailingCommas : 'warning' } : { comments : 'error' , trailingCommas : 'error' } ; languageService . doValidation ( textDocument , jsonDocument , documentSettings ) . then ( diagnostics => { setTimeout ( ( ) => { const currDocument = documents . get ( textDocument . uri ) ; if ( currDocument && currDocument . version === version ) { respond ( diagnostics ) ; } } , 100 ) ; } , error => { connection . console . error ( formatError ( `Error while validating ${ textDocument . uri }` , error ) ) ; } ) ; } connection . onDidChangeWatchedFiles ( ( change ) => { let hasChanges = false ; change . changes . forEach ( c => { if ( languageService . resetSchema ( c . uri ) ) { hasChanges = true ; } } ) ; if ( hasChanges ) { documents . all ( ) . forEach ( triggerValidation ) ; } } ) ; const jsonDocuments = getLanguageModelCache < JSONDocument > ( 10 , 60 , document => languageService . parseJSONDocument ( document ) ) ; documents . onDidClose ( e => { jsonDocuments . onDocumentRemoved ( e . document ) ; } ) ; connection . onShutdown ( ( ) => { jsonDocuments . dispose ( ) ; } ) ; function getJSONDocument ( document ) { return jsonDocuments . get ( document ) ; } connection . onCompletion ( ( textDocumentPosition , token ) => { return runSafeAsync ( async ( ) => { const document = documents . get ( textDocumentPosition . textDocument . uri ) ; if ( document ) { const jsonDocument = getJSONDocument ( document ) ; return languageService . doComplete ( document , textDocumentPosition . position , jsonDocument ) ; } return null ; } , null , `Error while computing completions for ${ textDocumentPosition . textDocument . uri }` , token ) ; } ) ; connection . onCompletionResolve ( ( completionItem , token ) => { return runSafeAsync ( ( ) => { return languageService . doResolve ( completionItem ) ; } , completionItem , `Error while resolving completion proposal` , token ) ; } ) ; connection . onHover ( ( textDocumentPositionParams , token ) => { return runSafeAsync ( async ( ) => { const document = documents . get ( textDocumentPositionParams . textDocument . uri ) ; if ( document ) { const jsonDocument = getJSONDocument ( document ) ; return languageService . doHover ( document , textDocumentPositionParams . position , jsonDocument ) ; } return null ; } , null , `Error while computing hover for ${ textDocumentPositionParams . textDocument . uri }` , token ) ; } ) ; connection . onDocumentSymbol ( ( documentSymbolParams , token ) => { return runSafe ( ( ) => { const document = documents . get ( documentSymbolParams . textDocument . uri ) ; if ( document ) { const jsonDocument = getJSONDocument ( document ) ; if ( hierarchicalDocumentSymbolSupport ) { return languageService . findDocumentSymbols2 ( document , jsonDocument ) ; } else { return languageService . findDocumentSymbols ( document , jsonDocument ) ; } } return [ ] ; } , [ ] , `Error while computing document symbols for ${ documentSymbolParams . textDocument . uri }` , token ) ; } ) ; connection . onDocumentRangeFormatting ( ( formatParams , token ) => { return runSafe ( ( ) => { const document = documents . get ( formatParams . textDocument . uri ) ; if ( document ) { return languageService . format ( document , formatParams . range , formatParams . options ) ; } return [ ] ; } , [ ] , `Error while formatting range for ${ formatParams . textDocument . uri }` , token ) ; } ) ; connection . onDocumentColor ( ( params , token ) => { return runSafeAsync ( async ( ) => { const document = documents . get ( params . textDocument . uri ) ; if ( document ) { const jsonDocument = getJSONDocument ( document ) ; return languageService . findDocumentColors ( document , jsonDocument ) ; } return [ ] ; } , [ ] , `Error while computing document colors for ${ params . textDocument . uri }` , token ) ; } ) ; connection . onColorPresentation ( ( params , token ) => { return runSafe ( ( ) => { const document = documents . get ( params . textDocument . uri ) ; if ( document ) { const jsonDocument = getJSONDocument ( document ) ; return languageService . getColorPresentations ( document , jsonDocument , params . color , params . range ) ; } return [ ] ; } , [ ] , `Error while computing color presentations for ${ params . textDocument . uri }` , token ) ; } ) ; connection . onFoldingRanges ( ( params , token ) => { return runSafe ( ( ) => { const document = documents . get ( params . textDocument . uri ) ; if ( document ) { return languageService . getFoldingRanges ( document , { rangeLimit : foldingRangeLimit } ) ; } return null ; } , null , `Error while computing folding ranges for ${ params . textDocument . uri }` , token ) ; } ) ; connection . onSelectionRanges ( ( params , token ) => { return runSafe ( ( ) => { const document = documents . get ( params . textDocument . uri ) ; if ( document ) { const jsonDocument = getJSONDocument ( document ) ; return languageService . getSelectionRanges ( document , params . positions , jsonDocument ) ; } return [ ] ; } , [ ] , `Error while computing selection ranges for ${ params . textDocument . uri }` , token ) ; } ) ; connection . listen ( ) ;