3
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
4
*--------------------------------------------------------------------------------------------*/
5
5
6
- import { ExtensionContext } from 'vscode' ;
7
- import { startClient , LanguageClientConstructor } from '../jsonClient' ;
6
+ import { ExtensionContext , OutputChannel , window , workspace } from 'vscode' ;
7
+ import { startClient , LanguageClientConstructor , SchemaRequestService , languageServerDescription } from '../jsonClient' ;
8
8
import { ServerOptions , TransportKind , LanguageClientOptions , LanguageClient } from 'vscode-languageclient/node' ;
9
9
10
- import * as fs from 'fs' ;
11
- import { xhr , XHRResponse , getErrorStatusDescription } from 'request-light' ;
10
+ import { promises as fs } from 'fs' ;
11
+ import * as path from 'path' ;
12
+ import { xhr , XHRResponse , getErrorStatusDescription , Headers } from 'request-light' ;
12
13
13
14
import TelemetryReporter from 'vscode-extension-telemetry' ;
14
- import { RequestService } from '../requests ' ;
15
+ import { JSONSchemaCache } from './schemaCache ' ;
15
16
16
17
let telemetry : TelemetryReporter | undefined ;
17
18
18
19
// this method is called when vs code is activated
19
- export function activate ( context : ExtensionContext ) {
20
-
21
- const clientPackageJSON = getPackageInfo ( context ) ;
20
+ export async function activate ( context : ExtensionContext ) {
21
+ const clientPackageJSON = await getPackageInfo ( context ) ;
22
22
telemetry = new TelemetryReporter ( clientPackageJSON . name , clientPackageJSON . version , clientPackageJSON . aiKey ) ;
23
23
24
+ const outputChannel = window . createOutputChannel ( languageServerDescription ) ;
25
+
24
26
const serverMain = `./server/${ clientPackageJSON . main . indexOf ( '/dist/' ) !== - 1 ? 'dist' : 'out' } /node/jsonServerMain` ;
25
27
const serverModule = context . asAbsolutePath ( serverMain ) ;
26
28
@@ -35,10 +37,15 @@ export function activate(context: ExtensionContext) {
35
37
} ;
36
38
37
39
const newLanguageClient : LanguageClientConstructor = ( id : string , name : string , clientOptions : LanguageClientOptions ) => {
40
+ clientOptions . outputChannel = outputChannel ;
38
41
return new LanguageClient ( id , name , serverOptions , clientOptions ) ;
39
42
} ;
43
+ const log = getLog ( outputChannel ) ;
44
+ context . subscriptions . push ( log ) ;
40
45
41
- startClient ( context , newLanguageClient , { http : getHTTPRequestService ( ) , telemetry } ) ;
46
+ const schemaRequests = await getSchemaRequestService ( context , log ) ;
47
+
48
+ startClient ( context , newLanguageClient , { schemaRequests, telemetry } ) ;
42
49
}
43
50
44
51
export function deactivate ( ) : Promise < any > {
@@ -52,32 +59,113 @@ interface IPackageInfo {
52
59
main : string ;
53
60
}
54
61
55
- function getPackageInfo ( context : ExtensionContext ) : IPackageInfo {
62
+ async function getPackageInfo ( context : ExtensionContext ) : Promise < IPackageInfo > {
56
63
const location = context . asAbsolutePath ( './package.json' ) ;
57
64
try {
58
- return JSON . parse ( fs . readFileSync ( location ) . toString ( ) ) ;
65
+ return JSON . parse ( ( await fs . readFile ( location ) ) . toString ( ) ) ;
59
66
} catch ( e ) {
60
67
console . log ( `Problems reading ${ location } : ${ e } ` ) ;
61
68
return { name : '' , version : '' , aiKey : '' , main : '' } ;
62
69
}
63
70
}
64
71
65
- function getHTTPRequestService ( ) : RequestService {
72
+ interface Log {
73
+ trace ( message : string ) : void ;
74
+ dispose ( ) : void ;
75
+ }
76
+
77
+ const traceSetting = 'json.trace.server' ;
78
+ function getLog ( outputChannel : OutputChannel ) : Log {
79
+ let trace = workspace . getConfiguration ( ) . get ( traceSetting ) === 'verbose' ;
80
+ const configListener = workspace . onDidChangeConfiguration ( e => {
81
+ if ( e . affectsConfiguration ( traceSetting ) ) {
82
+ trace = workspace . getConfiguration ( ) . get ( traceSetting ) === 'verbose' ;
83
+ }
84
+ } ) ;
66
85
return {
67
- getContent ( uri : string , _encoding ?: string ) : Promise < string > {
68
- const headers = { 'Accept-Encoding' : 'gzip, deflate' } ;
69
- return xhr ( { url : uri , followRedirects : 5 , headers } ) . then ( response => {
70
- return response . responseText ;
71
- } , ( error : XHRResponse ) => {
86
+ trace ( message : string ) {
87
+ if ( trace ) {
88
+ outputChannel . appendLine ( message ) ;
89
+ }
90
+ } ,
91
+ dispose : ( ) => configListener . dispose ( )
92
+ } ;
93
+ }
94
+
95
+ const retryTimeoutInDays = 2 ; // 2 days
96
+ const retryTimeoutInMs = retryTimeoutInDays * 24 * 60 * 60 * 1000 ;
97
+
98
+ async function getSchemaRequestService ( context : ExtensionContext , log : Log ) : Promise < SchemaRequestService > {
99
+ let cache : JSONSchemaCache | undefined = undefined ;
100
+ const globalStorage = context . globalStorageUri ;
101
+ if ( globalStorage . scheme === 'file' ) {
102
+ const schemaCacheLocation = path . join ( globalStorage . fsPath , 'json-schema-cache' ) ;
103
+ await fs . mkdir ( schemaCacheLocation , { recursive : true } ) ;
104
+
105
+ cache = new JSONSchemaCache ( schemaCacheLocation , context . globalState ) ;
106
+ log . trace ( `[json schema cache] initial state: ${ JSON . stringify ( cache . getCacheInfo ( ) , null , ' ' ) } ` ) ;
107
+ }
108
+
109
+ const isXHRResponse = ( error : any ) : error is XHRResponse => typeof error ?. status === 'number' ;
110
+
111
+ const request = async ( uri : string , etag ?: string ) : Promise < string > => {
112
+ const headers : Headers = { 'Accept-Encoding' : 'gzip, deflate' } ;
113
+ if ( etag ) {
114
+ headers [ 'If-None-Match' ] = etag ;
115
+ }
116
+ try {
117
+ log . trace ( `[json schema cache] Requesting schema ${ uri } etag ${ etag } ...` ) ;
118
+
119
+ const response = await xhr ( { url : uri , followRedirects : 5 , headers } ) ;
120
+ if ( cache ) {
121
+ const etag = response . headers [ 'etag' ] ;
122
+ if ( typeof etag === 'string' ) {
123
+ log . trace ( `[json schema cache] Storing schema ${ uri } etag ${ etag } in cache` ) ;
124
+ await cache . putSchema ( uri , etag , response . responseText ) ;
125
+ } else {
126
+ log . trace ( `[json schema cache] Response: schema ${ uri } no etag` ) ;
127
+ }
128
+ }
129
+ return response . responseText ;
130
+ } catch ( error : unknown ) {
131
+ if ( isXHRResponse ( error ) ) {
132
+ if ( error . status === 304 && etag && cache ) {
133
+
134
+ log . trace ( `[json schema cache] Response: schema ${ uri } unchanged etag ${ etag } ` ) ;
135
+
136
+ const content = await cache . getSchema ( uri , etag ) ;
137
+ if ( content ) {
138
+ log . trace ( `[json schema cache] Get schema ${ uri } etag ${ etag } from cache` ) ;
139
+ return content ;
140
+ }
141
+ return request ( uri ) ;
142
+ }
143
+
72
144
let status = getErrorStatusDescription ( error . status ) ;
73
145
if ( status && error . responseText ) {
74
146
status = `${ status } \n${ error . responseText . substring ( 0 , 200 ) } ` ;
75
147
}
76
148
if ( ! status ) {
77
149
status = error . toString ( ) ;
78
150
}
79
- return Promise . reject ( status ) ;
80
- } ) ;
151
+ log . trace ( `[json schema cache] Respond schema ${ uri } error ${ status } ` ) ;
152
+
153
+ throw status ;
154
+ }
155
+ throw error ;
156
+ }
157
+ } ;
158
+
159
+ return {
160
+ getContent : async ( uri : string ) => {
161
+ if ( cache && / ^ h t t p s ? : \/ \/ j s o n \. s c h e m a s t o r e \. o r g \/ / . test ( uri ) ) {
162
+ const content = await cache . getSchemaIfAccessedSince ( uri , retryTimeoutInMs ) ;
163
+ if ( content ) {
164
+ log . trace ( `[json schema cache] Schema ${ uri } from cache without request (last accessed less than ${ retryTimeoutInDays } days ago)` ) ;
165
+ return content ;
166
+ }
167
+ }
168
+ return request ( uri , cache ?. getETag ( uri ) ) ;
81
169
}
82
170
} ;
83
171
}
0 commit comments