Merge vscode 1.67 (#20883)

* Fix initial build breaks from 1.67 merge (#2514)

* Update yarn lock files

* Update build scripts

* Fix tsconfig

* Build breaks

* WIP

* Update yarn lock files

* Misc breaks

* Updates to package.json

* Breaks

* Update yarn

* Fix breaks

* Breaks

* Build breaks

* Breaks

* Breaks

* Breaks

* Breaks

* Breaks

* Missing file

* Breaks

* Breaks

* Breaks

* Breaks

* Breaks

* Fix several runtime breaks (#2515)

* Missing files

* Runtime breaks

* Fix proxy ordering issue

* Remove commented code

* Fix breaks with opening query editor

* Fix post merge break

* Updates related to setup build and other breaks (#2516)

* Fix bundle build issues

* Update distro

* Fix distro merge and update build JS files

* Disable pipeline steps

* Remove stats call

* Update license name

* Make new RPM dependencies a warning

* Fix extension manager version checks

* Update JS file

* Fix a few runtime breaks

* Fixes

* Fix runtime issues

* Fix build breaks

* Update notebook tests (part 1)

* Fix broken tests

* Linting errors

* Fix hygiene

* Disable lint rules

* Bump distro

* Turn off smoke tests

* Disable integration tests

* Remove failing "activate" test

* Remove failed test assertion

* Disable other broken test

* Disable query history tests

* Disable extension unit tests

* Disable failing tasks
This commit is contained in:
Karl Burtram
2022-10-19 19:13:18 -07:00
committed by GitHub
parent 33c6daaea1
commit 8a3d08f0de
3738 changed files with 192313 additions and 107208 deletions

View File

@@ -5,9 +5,8 @@
import { ExtensionContext, Uri } from 'vscode';
import { LanguageClientOptions } from 'vscode-languageclient';
import { startClient, LanguageClientConstructor } from '../jsonClient';
import { startClient, LanguageClientConstructor, SchemaRequestService } from '../jsonClient';
import { LanguageClient } from 'vscode-languageclient/browser';
import { RequestService } from '../requests';
declare const Worker: {
new(stringUrl: string): any;
@@ -24,7 +23,7 @@ export function activate(context: ExtensionContext) {
return new LanguageClient(id, name, clientOptions, worker);
};
const http: RequestService = {
const schemaRequests: SchemaRequestService = {
getContent(uri: string) {
return fetch(uri, { mode: 'cors' })
.then(function (response: any) {
@@ -32,7 +31,8 @@ export function activate(context: ExtensionContext) {
});
}
};
startClient(context, newLanguageClient, { http });
startClient(context, newLanguageClient, { schemaRequests });
} catch (e) {
console.log(e);

View File

@@ -6,6 +6,8 @@ import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export type JSONLanguageStatus = { schemas: string[] };
import {
workspace, window, languages, commands, ExtensionContext, extensions, Uri,
Diagnostic, StatusBarAlignment, TextEditor, TextDocument, FormattingOptions, CancellationToken,
@@ -18,20 +20,25 @@ import {
} from 'vscode-languageclient';
import { hash } from './utils/hash';
import { RequestService, joinPath } from './requests';
import { createLanguageStatusItem } from './languageStatus';
namespace VSCodeContentRequest {
export const type: RequestType<string, string, any> = new RequestType('vscode/content');
}
namespace SchemaContentChangeNotification {
export const type: NotificationType<string> = new NotificationType('json/schemaContent');
export const type: NotificationType<string | string[]> = new NotificationType('json/schemaContent');
}
namespace ForceValidateRequest {
export const type: RequestType<string, Diagnostic[], any> = new RequestType('json/validate');
}
namespace LanguageStatusRequest {
export const type: RequestType<string, JSONLanguageStatus, any> = new RequestType('json/languageStatus');
}
export interface ISchemaAssociations {
[pattern: string]: string[];
}
@@ -52,7 +59,8 @@ namespace ResultLimitReachedNotification {
interface Settings {
json?: {
schemas?: JSONSchemaSettings[];
format?: { enable: boolean; };
format?: { enable?: boolean };
validate?: { enable?: boolean };
resultLimit?: number;
};
http?: {
@@ -61,7 +69,7 @@ interface Settings {
};
}
interface JSONSchemaSettings {
export interface JSONSchemaSettings {
fileMatch?: string[];
url?: string;
schema?: any;
@@ -69,6 +77,7 @@ interface JSONSchemaSettings {
namespace SettingIds {
export const enableFormatter = 'json.format.enable';
export const enableValidation = 'json.validate.enable';
export const enableSchemaDownload = 'json.schemaDownload.enable';
export const maxItemsComputed = 'json.maxItemsComputed';
}
@@ -88,17 +97,23 @@ export interface TelemetryReporter {
export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => CommonLanguageClient;
export interface Runtime {
http: RequestService;
telemetry?: TelemetryReporter
schemaRequests: SchemaRequestService;
telemetry?: TelemetryReporter;
}
export interface SchemaRequestService {
getContent(uri: string): Promise<string>;
clearCache?(): Promise<string[]>;
}
export const languageServerDescription = localize('jsonserver.name', 'JSON Language Server');
export function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime) {
const toDispose = context.subscriptions;
let rangeFormatting: Disposable | undefined = undefined;
const documentSelector = ['json', 'jsonc'];
const schemaResolutionErrorStatusBarItem = window.createStatusBarItem('status.json.resolveError', StatusBarAlignment.Right, 0);
@@ -109,6 +124,16 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua
const fileSchemaErrors = new Map<string, string>();
let schemaDownloadEnabled = true;
let isClientReady = false;
toDispose.push(commands.registerCommand('json.clearCache', async () => {
if (isClientReady && runtime.schemaRequests.clearCache) {
const cachedSchemas = await runtime.schemaRequests.clearCache();
await client.sendNotification(SchemaContentChangeNotification.type, cachedSchemas);
}
window.showInformationMessage(localize('json.clearCache.completed', "JSON schema cache cleared."));
}));
// Options to control the language client
const clientOptions: LanguageClientOptions = {
// Register the server for json documents
@@ -190,12 +215,14 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua
};
// Create the language client and start the client.
const client = newLanguageClient('json', localize('jsonserver.name', 'JSON Language Server'), clientOptions);
const client = newLanguageClient('json', languageServerDescription, clientOptions);
client.registerProposedFeatures();
const disposable = client.start();
toDispose.push(disposable);
client.onReady().then(() => {
isClientReady = true;
const schemaDocuments: { [uri: string]: boolean } = {};
// handle content request
@@ -220,7 +247,7 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua
*/
runtime.telemetry.sendTelemetryEvent('json.schema', { schemaURL: uriPath });
}
return runtime.http.getContent(uriPath).catch(e => {
return runtime.schemaRequests.getContent(uriPath).catch(e => {
return Promise.reject(new ResponseError(4, e.toString()));
});
} else {
@@ -281,9 +308,9 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua
client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociations(context));
extensions.onDidChange(_ => {
toDispose.push(extensions.onDidChange(_ => {
client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociations(context));
});
}));
// manually register / deregister format provider based on the `json.format.enable` setting avoiding issues with late registration. See #71652.
updateFormatterRegistration();
@@ -302,7 +329,7 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua
client.onNotification(ResultLimitReachedNotification.type, async message => {
const shouldPrompt = context.globalState.get<boolean>(StorageIds.maxItemsExceededInformation) !== false;
if (shouldPrompt) {
const ok = localize('ok', "Ok");
const ok = localize('ok', "OK");
const openSettings = localize('goToSetting', 'Open Settings');
const neverAgain = localize('yes never again', "Don't Show Again");
const pick = await window.showInformationMessage(`${message}\n${localize('configureLimit', 'Use setting \'{0}\' to configure the limit.', SettingIds.maxItemsComputed)}`, ok, openSettings, neverAgain);
@@ -314,6 +341,8 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua
}
});
toDispose.push(createLanguageStatusItem(documentSelector, (uri: string) => client.sendRequest(LanguageStatusRequest.type, uri)));
function updateFormatterRegistration() {
const formatEnabled = workspace.getConfiguration().get(SettingIds.enableFormatter);
if (!formatEnabled && rangeFormatting) {
@@ -376,7 +405,7 @@ function getSchemaAssociations(_context: ExtensionContext): ISchemaAssociation[]
if (Array.isArray(fileMatch) && typeof url === 'string') {
let uri: string = url;
if (uri[0] === '.' && uri[1] === '/') {
uri = joinPath(extension.extensionUri, uri).toString();
uri = Uri.joinPath(extension.extensionUri, uri).toString();
}
fileMatch = fileMatch.map(fm => {
if (fm[0] === '%') {
@@ -398,6 +427,7 @@ function getSchemaAssociations(_context: ExtensionContext): ISchemaAssociation[]
}
function getSettings(): Settings {
const configuration = workspace.getConfiguration();
const httpSettings = workspace.getConfiguration('http');
const resultLimit: number = Math.trunc(Math.max(0, Number(workspace.getConfiguration().get(SettingIds.maxItemsComputed)))) || 5000;
@@ -408,6 +438,8 @@ function getSettings(): Settings {
proxyStrictSSL: httpSettings.get('proxyStrictSSL')
},
json: {
validate: { enable: configuration.get(SettingIds.enableValidation) },
format: { enable: configuration.get(SettingIds.enableFormatter) },
schemas: [],
resultLimit
}
@@ -497,7 +529,7 @@ function getSchemaId(schema: JSONSchemaSettings, folderUri?: Uri): string | unde
url = schema.schema.id || `vscode://schemas/custom/${encodeURIComponent(hash(schema.schema).toString(16))}`;
}
} else if (folderUri && (url[0] === '.' || url[0] === '/')) {
url = joinPath(folderUri, url).toString();
url = Uri.joinPath(folderUri, url).toString();
}
return url;
}

View File

@@ -0,0 +1,219 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { window, languages, Uri, LanguageStatusSeverity, Disposable, commands, QuickPickItem, extensions, workspace, Extension, WorkspaceFolder, QuickPickItemKind, ThemeIcon } from 'vscode';
import { JSONLanguageStatus, JSONSchemaSettings } from './jsonClient';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
type ShowSchemasInput = {
schemas: string[];
uri: string;
};
interface ShowSchemasItem extends QuickPickItem {
uri?: Uri;
buttonCommands?: (() => void)[];
}
function getExtensionSchemaAssociations() {
const associations: { fullUri: string; extension: Extension<any>; label: string }[] = [];
for (const extension of extensions.all) {
const jsonValidations = extension.packageJSON?.contributes?.jsonValidation;
if (Array.isArray(jsonValidations)) {
for (const jsonValidation of jsonValidations) {
let uri = jsonValidation.url;
if (typeof uri === 'string') {
if (uri[0] === '.' && uri[1] === '/') {
uri = Uri.joinPath(extension.extensionUri, uri).toString(false);
}
associations.push({ fullUri: uri, extension, label: jsonValidation.url });
}
}
}
}
return {
findExtension(uri: string): ShowSchemasItem | undefined {
for (const association of associations) {
if (association.fullUri === uri) {
return {
label: association.label,
detail: localize('schemaFromextension', 'Configured by extension: {0}', association.extension.id),
uri: Uri.parse(association.fullUri),
buttons: [{ iconPath: new ThemeIcon('extensions'), tooltip: localize('openExtension', 'Open Extension') }],
buttonCommands: [() => commands.executeCommand('workbench.extensions.action.showExtensionsWithIds', [[association.extension.id]])]
};
}
}
return undefined;
}
};
}
//
function getSettingsSchemaAssociations(uri: string) {
const resourceUri = Uri.parse(uri);
const workspaceFolder = workspace.getWorkspaceFolder(resourceUri);
const settings = workspace.getConfiguration('json', resourceUri).inspect<JSONSchemaSettings[]>('schemas');
const associations: { fullUri: string; workspaceFolder: WorkspaceFolder | undefined; label: string }[] = [];
const folderSettingSchemas = settings?.workspaceFolderValue;
if (workspaceFolder && Array.isArray(folderSettingSchemas)) {
for (const setting of folderSettingSchemas) {
const uri = setting.url;
if (typeof uri === 'string') {
let fullUri = uri;
if (uri[0] === '.' && uri[1] === '/') {
fullUri = Uri.joinPath(workspaceFolder.uri, uri).toString(false);
}
associations.push({ fullUri, workspaceFolder, label: uri });
}
}
}
const userSettingSchemas = settings?.globalValue;
if (Array.isArray(userSettingSchemas)) {
for (const setting of userSettingSchemas) {
const uri = setting.url;
if (typeof uri === 'string') {
let fullUri = uri;
if (workspaceFolder && uri[0] === '.' && uri[1] === '/') {
fullUri = Uri.joinPath(workspaceFolder.uri, uri).toString(false);
}
associations.push({ fullUri, workspaceFolder: undefined, label: uri });
}
}
}
return {
findSetting(uri: string): ShowSchemasItem | undefined {
for (const association of associations) {
if (association.fullUri === uri) {
return {
label: association.label,
detail: association.workspaceFolder ? localize('schemaFromFolderSettings', 'Configured in workspace settings') : localize('schemaFromUserSettings', 'Configured in user settings'),
uri: Uri.parse(association.fullUri),
buttons: [{ iconPath: new ThemeIcon('gear'), tooltip: localize('openSettings', 'Open Settings') }],
buttonCommands: [() => commands.executeCommand(association.workspaceFolder ? 'workbench.action.openWorkspaceSettingsFile' : 'workbench.action.openSettingsJson', ['json.schemas'])]
};
}
}
return undefined;
}
};
}
function showSchemaList(input: ShowSchemasInput) {
const extensionSchemaAssocations = getExtensionSchemaAssociations();
const settingsSchemaAssocations = getSettingsSchemaAssociations(input.uri);
const extensionEntries = [];
const settingsEntries = [];
const otherEntries = [];
for (const schemaUri of input.schemas) {
const extensionEntry = extensionSchemaAssocations.findExtension(schemaUri);
if (extensionEntry) {
extensionEntries.push(extensionEntry);
continue;
}
const settingsEntry = settingsSchemaAssocations.findSetting(schemaUri);
if (settingsEntry) {
settingsEntries.push(settingsEntry);
continue;
}
otherEntries.push({ label: schemaUri, uri: Uri.parse(schemaUri) });
}
const items: ShowSchemasItem[] = [...extensionEntries, ...settingsEntries, ...otherEntries];
if (items.length === 0) {
items.push({
label: localize('schema.noSchema', 'No schema configured for this file'),
buttons: [{ iconPath: new ThemeIcon('gear'), tooltip: localize('openSettings', 'Open Settings') }],
buttonCommands: [() => commands.executeCommand('workbench.action.openSettingsJson', ['json.schemas'])]
});
}
items.push({ label: '', kind: QuickPickItemKind.Separator });
items.push({ label: localize('schema.showdocs', 'Learn more about JSON schema configuration...'), uri: Uri.parse('https://code.visualstudio.com/docs/languages/json#_json-schemas-and-settings') });
const quickPick = window.createQuickPick<ShowSchemasItem>();
quickPick.title = localize('schemaPicker.title', 'JSON Schemas used for {0}', input.uri);
// quickPick.placeholder = items.length ? localize('schemaPicker.placeholder', 'Select the schema to open') : undefined;
quickPick.items = items;
quickPick.show();
quickPick.onDidAccept(() => {
const uri = quickPick.selectedItems[0].uri;
if (uri) {
commands.executeCommand('vscode.open', uri);
quickPick.dispose();
}
});
quickPick.onDidTriggerItemButton(b => {
const index = b.item.buttons?.indexOf(b.button);
if (index !== undefined && index >= 0 && b.item.buttonCommands && b.item.buttonCommands[index]) {
b.item.buttonCommands[index]();
}
});
}
export function createLanguageStatusItem(documentSelector: string[], statusRequest: (uri: string) => Promise<JSONLanguageStatus>): Disposable {
const statusItem = languages.createLanguageStatusItem('json.projectStatus', documentSelector);
statusItem.name = localize('statusItem.name', "JSON Validation Status");
statusItem.severity = LanguageStatusSeverity.Information;
const showSchemasCommand = commands.registerCommand('_json.showAssociatedSchemaList', showSchemaList);
const activeEditorListener = window.onDidChangeActiveTextEditor(() => {
updateLanguageStatus();
});
async function updateLanguageStatus() {
const document = window.activeTextEditor?.document;
if (document && documentSelector.indexOf(document.languageId) !== -1) {
try {
statusItem.text = '$(loading~spin)';
statusItem.detail = localize('pending.detail', 'Loading JSON info');
statusItem.command = undefined;
const schemas = (await statusRequest(document.uri.toString())).schemas;
statusItem.detail = undefined;
if (schemas.length === 0) {
statusItem.text = localize('status.noSchema.short', "No Schema Validation");
statusItem.detail = localize('status.noSchema', 'No JSON schema configured.');
} else if (schemas.length === 1) {
statusItem.text = localize('status.withSchema.short', "Schema Validated");
statusItem.detail = localize('status.singleSchema', 'JSON schema configured.');
} else {
statusItem.text = localize('status.withSchemas.short', "Schema Validated");
statusItem.detail = localize('status.multipleSchema', 'Multiple JSON schemas configured.');
}
statusItem.command = {
command: '_json.showAssociatedSchemaList',
title: localize('status.openSchemasLink', 'Show Schemas'),
arguments: [{ schemas, uri: document.uri.toString() } as ShowSchemasInput]
};
} catch (e) {
statusItem.text = localize('status.error', 'Unable to compute used schemas');
statusItem.detail = undefined;
statusItem.command = undefined;
}
} else {
statusItem.text = localize('status.notJSON', 'Not a JSON editor');
statusItem.detail = undefined;
statusItem.command = undefined;
}
}
updateLanguageStatus();
return Disposable.from(statusItem, activeEditorListener, showSchemasCommand);
}

View File

@@ -3,24 +3,26 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ExtensionContext } from 'vscode';
import { startClient, LanguageClientConstructor } from '../jsonClient';
import { ExtensionContext, OutputChannel, window, workspace } from 'vscode';
import { startClient, LanguageClientConstructor, SchemaRequestService, languageServerDescription } from '../jsonClient';
import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node';
import * as fs from 'fs';
import { xhr, XHRResponse, getErrorStatusDescription } from 'request-light';
import { promises as fs } from 'fs';
import * as path from 'path';
import { xhr, XHRResponse, getErrorStatusDescription, Headers } from 'request-light';
import TelemetryReporter from 'vscode-extension-telemetry';
import { RequestService } from '../requests';
import TelemetryReporter from '@vscode/extension-telemetry';
import { JSONSchemaCache } from './schemaCache';
let telemetry: TelemetryReporter | undefined;
// this method is called when vs code is activated
export function activate(context: ExtensionContext) {
const clientPackageJSON = getPackageInfo(context);
export async function activate(context: ExtensionContext) {
const clientPackageJSON = await getPackageInfo(context);
telemetry = new TelemetryReporter(clientPackageJSON.name, clientPackageJSON.version, clientPackageJSON.aiKey);
const outputChannel = window.createOutputChannel(languageServerDescription);
const serverMain = `./server/${clientPackageJSON.main.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/jsonServerMain`;
const serverModule = context.asAbsolutePath(serverMain);
@@ -35,10 +37,15 @@ export function activate(context: ExtensionContext) {
};
const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => {
clientOptions.outputChannel = outputChannel;
return new LanguageClient(id, name, serverOptions, clientOptions);
};
const log = getLog(outputChannel);
context.subscriptions.push(log);
startClient(context, newLanguageClient, { http: getHTTPRequestService(), telemetry });
const schemaRequests = await getSchemaRequestService(context, log);
startClient(context, newLanguageClient, { schemaRequests, telemetry });
}
export function deactivate(): Promise<any> {
@@ -52,23 +59,100 @@ interface IPackageInfo {
main: string;
}
function getPackageInfo(context: ExtensionContext): IPackageInfo {
async function getPackageInfo(context: ExtensionContext): Promise<IPackageInfo> {
const location = context.asAbsolutePath('./package.json');
try {
return JSON.parse(fs.readFileSync(location).toString());
return JSON.parse((await fs.readFile(location)).toString());
} catch (e) {
console.log(`Problems reading ${location}: ${e}`);
return { name: '', version: '', aiKey: '', main: '' };
}
}
function getHTTPRequestService(): RequestService {
interface Log {
trace(message: string): void;
isTrace(): boolean;
dispose(): void;
}
const traceSetting = 'json.trace.server';
function getLog(outputChannel: OutputChannel): Log {
let trace = workspace.getConfiguration().get(traceSetting) === 'verbose';
const configListener = workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(traceSetting)) {
trace = workspace.getConfiguration().get(traceSetting) === 'verbose';
}
});
return {
getContent(uri: string, _encoding?: string): Promise<string> {
const headers = { 'Accept-Encoding': 'gzip, deflate' };
return xhr({ url: uri, followRedirects: 5, headers }).then(response => {
return response.responseText;
}, (error: XHRResponse) => {
trace(message: string) {
if (trace) {
outputChannel.appendLine(message);
}
},
isTrace() {
return trace;
},
dispose: () => configListener.dispose()
};
}
const retryTimeoutInHours = 2 * 24; // 2 days
async function getSchemaRequestService(context: ExtensionContext, log: Log): Promise<SchemaRequestService> {
let cache: JSONSchemaCache | undefined = undefined;
const globalStorage = context.globalStorageUri;
let clearCache: (() => Promise<string[]>) | undefined;
if (globalStorage.scheme === 'file') {
const schemaCacheLocation = path.join(globalStorage.fsPath, 'json-schema-cache');
await fs.mkdir(schemaCacheLocation, { recursive: true });
const schemaCache = new JSONSchemaCache(schemaCacheLocation, context.globalState);
log.trace(`[json schema cache] initial state: ${JSON.stringify(schemaCache.getCacheInfo(), null, ' ')}`);
cache = schemaCache;
clearCache = async () => {
const cachedSchemas = await schemaCache.clearCache();
log.trace(`[json schema cache] cache cleared. Previously cached schemas: ${cachedSchemas.join(', ')}`);
return cachedSchemas;
};
}
const isXHRResponse = (error: any): error is XHRResponse => typeof error?.status === 'number';
const request = async (uri: string, etag?: string): Promise<string> => {
const headers: Headers = { 'Accept-Encoding': 'gzip, deflate' };
if (etag) {
headers['If-None-Match'] = etag;
}
try {
log.trace(`[json schema cache] Requesting schema ${uri} etag ${etag}...`);
const response = await xhr({ url: uri, followRedirects: 5, headers });
if (cache) {
const etag = response.headers['etag'];
if (typeof etag === 'string') {
log.trace(`[json schema cache] Storing schema ${uri} etag ${etag} in cache`);
await cache.putSchema(uri, etag, response.responseText);
} else {
log.trace(`[json schema cache] Response: schema ${uri} no etag`);
}
}
return response.responseText;
} catch (error: unknown) {
if (isXHRResponse(error)) {
if (error.status === 304 && etag && cache) {
log.trace(`[json schema cache] Response: schema ${uri} unchanged etag ${etag}`);
const content = await cache.getSchema(uri, etag, true);
if (content) {
log.trace(`[json schema cache] Get schema ${uri} etag ${etag} from cache`);
return content;
}
return request(uri);
}
let status = getErrorStatusDescription(error.status);
if (status && error.responseText) {
status = `${status}\n${error.responseText.substring(0, 200)}`;
@@ -76,8 +160,28 @@ function getHTTPRequestService(): RequestService {
if (!status) {
status = error.toString();
}
return Promise.reject(status);
});
log.trace(`[json schema cache] Respond schema ${uri} error ${status}`);
throw status;
}
throw error;
}
};
return {
getContent: async (uri: string) => {
if (cache && /^https?:\/\/json\.schemastore\.org\//.test(uri)) {
const content = await cache.getSchemaIfUpdatedSince(uri, retryTimeoutInHours);
if (content) {
if (log.isTrace()) {
log.trace(`[json schema cache] Schema ${uri} from cache without request (last accessed ${cache.getLastUpdatedInHours(uri)} hours ago)`);
}
return content;
}
}
return request(uri, cache?.getETag(uri));
},
clearCache
};
}

View File

@@ -0,0 +1,147 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { promises as fs } from 'fs';
import * as path from 'path';
import { createHash } from 'crypto';
import { Memento } from 'vscode';
interface CacheEntry {
etag: string;
fileName: string;
updateTime: number;
}
interface CacheInfo {
[schemaUri: string]: CacheEntry;
}
const MEMENTO_KEY = 'json-schema-cache';
export class JSONSchemaCache {
private cacheInfo: CacheInfo;
constructor(private readonly schemaCacheLocation: string, private readonly globalState: Memento) {
const infos = globalState.get<CacheInfo>(MEMENTO_KEY, {}) as CacheInfo;
const validated: CacheInfo = {};
for (const schemaUri in infos) {
const { etag, fileName, updateTime } = infos[schemaUri];
if (typeof etag === 'string' && typeof fileName === 'string' && typeof updateTime === 'number') {
validated[schemaUri] = { etag, fileName, updateTime };
}
}
this.cacheInfo = validated;
}
getETag(schemaUri: string): string | undefined {
return this.cacheInfo[schemaUri]?.etag;
}
getLastUpdatedInHours(schemaUri: string): number | undefined {
const updateTime = this.cacheInfo[schemaUri]?.updateTime;
if (updateTime !== undefined) {
return (new Date().getTime() - updateTime) / 1000 / 60 / 60;
}
return undefined;
}
async putSchema(schemaUri: string, etag: string, schemaContent: string): Promise<void> {
try {
const fileName = getCacheFileName(schemaUri);
await fs.writeFile(path.join(this.schemaCacheLocation, fileName), schemaContent);
const entry: CacheEntry = { etag, fileName, updateTime: new Date().getTime() };
this.cacheInfo[schemaUri] = entry;
} catch (e) {
delete this.cacheInfo[schemaUri];
} finally {
await this.updateMemento();
}
}
async getSchemaIfUpdatedSince(schemaUri: string, expirationDurationInHours: number): Promise<string | undefined> {
const lastUpdatedInHours = this.getLastUpdatedInHours(schemaUri);
if (lastUpdatedInHours !== undefined && (lastUpdatedInHours < expirationDurationInHours)) {
return this.loadSchemaFile(schemaUri, this.cacheInfo[schemaUri], false);
}
return undefined;
}
async getSchema(schemaUri: string, etag: string, etagValid: boolean): Promise<string | undefined> {
const cacheEntry = this.cacheInfo[schemaUri];
if (cacheEntry) {
if (cacheEntry.etag === etag) {
return this.loadSchemaFile(schemaUri, cacheEntry, etagValid);
} else {
this.deleteSchemaFile(schemaUri, cacheEntry);
}
}
return undefined;
}
private async loadSchemaFile(schemaUri: string, cacheEntry: CacheEntry, isUpdated: boolean): Promise<string | undefined> {
const cacheLocation = path.join(this.schemaCacheLocation, cacheEntry.fileName);
try {
const content = (await fs.readFile(cacheLocation)).toString();
if (isUpdated) {
cacheEntry.updateTime = new Date().getTime();
}
return content;
} catch (e) {
delete this.cacheInfo[schemaUri];
return undefined;
} finally {
await this.updateMemento();
}
}
private async deleteSchemaFile(schemaUri: string, cacheEntry: CacheEntry): Promise<void> {
const cacheLocation = path.join(this.schemaCacheLocation, cacheEntry.fileName);
delete this.cacheInfo[schemaUri];
await this.updateMemento();
try {
await fs.rm(cacheLocation);
} catch (e) {
// ignore
}
}
// for debugging
public getCacheInfo() {
return this.cacheInfo;
}
private async updateMemento() {
try {
await this.globalState.update(MEMENTO_KEY, this.cacheInfo);
} catch (e) {
// ignore
}
}
public async clearCache(): Promise<string[]> {
const uris = Object.keys(this.cacheInfo);
try {
const files = await fs.readdir(this.schemaCacheLocation);
for (const file of files) {
try {
await fs.unlink(path.join(this.schemaCacheLocation, file));
} catch (_e) {
// ignore
}
}
} catch (e) {
// ignore
} finally {
this.cacheInfo = {};
await this.updateMemento();
}
return uris;
}
}
function getCacheFileName(uri: string): string {
return `${createHash('MD5').update(uri).digest('hex')}.schema.json`;
}

View File

@@ -1,68 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Uri } from 'vscode';
export interface RequestService {
getContent(uri: string, encoding?: string): Promise<string>;
}
export function getScheme(uri: string) {
return uri.substr(0, uri.indexOf(':'));
}
export function dirname(uri: string) {
const lastIndexOfSlash = uri.lastIndexOf('/');
return lastIndexOfSlash !== -1 ? uri.substr(0, lastIndexOfSlash) : '';
}
export function basename(uri: string) {
const lastIndexOfSlash = uri.lastIndexOf('/');
return uri.substr(lastIndexOfSlash + 1);
}
const Slash = '/'.charCodeAt(0);
const Dot = '.'.charCodeAt(0);
export function isAbsolutePath(path: string) {
return path.charCodeAt(0) === Slash;
}
export function resolvePath(uri: Uri, path: string): Uri {
if (isAbsolutePath(path)) {
return uri.with({ path: normalizePath(path.split('/')) });
}
return joinPath(uri, path);
}
export function normalizePath(parts: string[]): string {
const newParts: string[] = [];
for (const part of parts) {
if (part.length === 0 || part.length === 1 && part.charCodeAt(0) === Dot) {
// ignore
} else if (part.length === 2 && part.charCodeAt(0) === Dot && part.charCodeAt(1) === Dot) {
newParts.pop();
} else {
newParts.push(part);
}
}
if (parts.length > 1 && parts[parts.length - 1].length === 0) {
newParts.push('');
}
let res = newParts.join('/');
if (parts[0].length === 0) {
res = '/' + res;
}
return res;
}
export function joinPath(uri: Uri, ...paths: string[]): Uri {
const parts = uri.path.split('/');
for (let path of paths) {
parts.push(...path.split('/'));
}
return uri.with({ path: normalizePath(parts) });
}

View File

@@ -1,7 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../../../src/vs/vscode.d.ts'/>
/// <reference path="../../../../../src/vs/vscode.proposed.d.ts" />