eae5d8c807
These conflicts will be resolved in the following commits. We do it this way so that PR review is possible.
532 lines
19 KiB
TypeScript
532 lines
19 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
import * as nls from 'vscode-nls';
|
|
|
|
const localize = nls.loadMessageBundle();
|
|
|
|
import {
|
|
workspace, window, languages, commands, ExtensionContext, extensions, Uri, LanguageConfiguration,
|
|
Diagnostic, StatusBarAlignment, TextEditor, TextDocument, FormattingOptions, CancellationToken,
|
|
ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString,
|
|
} from 'vscode';
|
|
import {
|
|
LanguageClientOptions, RequestType, NotificationType,
|
|
DidChangeConfigurationNotification, HandleDiagnosticsSignature, ResponseError, DocumentRangeFormattingParams,
|
|
DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, ProvideHoverSignature, CommonLanguageClient
|
|
} from 'vscode-languageclient';
|
|
|
|
import { hash } from './utils/hash';
|
|
import { RequestService, joinPath } from './requests';
|
|
|
|
namespace VSCodeContentRequest {
|
|
export const type: RequestType<string, string, any> = new RequestType('vscode/content');
|
|
}
|
|
|
|
namespace SchemaContentChangeNotification {
|
|
export const type: NotificationType<string> = new NotificationType('json/schemaContent');
|
|
}
|
|
|
|
namespace ForceValidateRequest {
|
|
export const type: RequestType<string, Diagnostic[], any> = new RequestType('json/validate');
|
|
}
|
|
|
|
export interface ISchemaAssociations {
|
|
[pattern: string]: string[];
|
|
}
|
|
|
|
export interface ISchemaAssociation {
|
|
fileMatch: string[];
|
|
uri: string;
|
|
}
|
|
|
|
namespace SchemaAssociationNotification {
|
|
export const type: NotificationType<ISchemaAssociations | ISchemaAssociation[]> = new NotificationType('json/schemaAssociations');
|
|
}
|
|
|
|
namespace ResultLimitReachedNotification {
|
|
export const type: NotificationType<string> = new NotificationType('json/resultLimitReached');
|
|
}
|
|
|
|
interface Settings {
|
|
json?: {
|
|
schemas?: JSONSchemaSettings[];
|
|
format?: { enable: boolean; };
|
|
resultLimit?: number;
|
|
};
|
|
http?: {
|
|
proxy?: string;
|
|
proxyStrictSSL?: boolean;
|
|
};
|
|
}
|
|
|
|
interface JSONSchemaSettings {
|
|
fileMatch?: string[];
|
|
url?: string;
|
|
schema?: any;
|
|
}
|
|
|
|
namespace SettingIds {
|
|
export const enableFormatter = 'json.format.enable';
|
|
export const enableSchemaDownload = 'json.schemaDownload.enable';
|
|
export const maxItemsComputed = 'json.maxItemsComputed';
|
|
}
|
|
|
|
namespace StorageIds {
|
|
export const maxItemsExceededInformation = 'json.maxItemsExceededInformation';
|
|
}
|
|
|
|
export interface TelemetryReporter {
|
|
sendTelemetryEvent(eventName: string, properties?: {
|
|
[key: string]: string;
|
|
}, measurements?: {
|
|
[key: string]: number;
|
|
}): void;
|
|
}
|
|
|
|
export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => CommonLanguageClient;
|
|
|
|
export interface Runtime {
|
|
http: RequestService;
|
|
telemetry?: TelemetryReporter
|
|
}
|
|
|
|
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({
|
|
id: 'status.json.resolveError',
|
|
name: localize('json.resolveError', "JSON: Schema Resolution Error"),
|
|
alignment: StatusBarAlignment.Right,
|
|
priority: 0,
|
|
});
|
|
schemaResolutionErrorStatusBarItem.text = '$(alert)';
|
|
toDispose.push(schemaResolutionErrorStatusBarItem);
|
|
|
|
const fileSchemaErrors = new Map<string, string>();
|
|
let schemaDownloadEnabled = true;
|
|
|
|
// Options to control the language client
|
|
const clientOptions: LanguageClientOptions = {
|
|
// Register the server for json documents
|
|
documentSelector,
|
|
initializationOptions: {
|
|
handledSchemaProtocols: ['file'], // language server only loads file-URI. Fetching schemas with other protocols ('http'...) are made on the client.
|
|
provideFormatter: false, // tell the server to not provide formatting capability and ignore the `json.format.enable` setting.
|
|
customCapabilities: { rangeFormatting: { editLimit: 10000 } }
|
|
},
|
|
synchronize: {
|
|
// Synchronize the setting section 'json' to the server
|
|
configurationSection: ['json', 'http'],
|
|
fileEvents: workspace.createFileSystemWatcher('**/*.json')
|
|
},
|
|
middleware: {
|
|
workspace: {
|
|
didChangeConfiguration: () => client.sendNotification(DidChangeConfigurationNotification.type, { settings: getSettings() })
|
|
},
|
|
handleDiagnostics: (uri: Uri, diagnostics: Diagnostic[], next: HandleDiagnosticsSignature) => {
|
|
const schemaErrorIndex = diagnostics.findIndex(isSchemaResolveError);
|
|
|
|
if (schemaErrorIndex === -1) {
|
|
fileSchemaErrors.delete(uri.toString());
|
|
return next(uri, diagnostics);
|
|
}
|
|
|
|
const schemaResolveDiagnostic = diagnostics[schemaErrorIndex];
|
|
fileSchemaErrors.set(uri.toString(), schemaResolveDiagnostic.message);
|
|
|
|
if (!schemaDownloadEnabled) {
|
|
diagnostics = diagnostics.filter(d => !isSchemaResolveError(d));
|
|
}
|
|
|
|
if (window.activeTextEditor && window.activeTextEditor.document.uri.toString() === uri.toString()) {
|
|
schemaResolutionErrorStatusBarItem.show();
|
|
}
|
|
|
|
next(uri, diagnostics);
|
|
},
|
|
// testing the replace / insert mode
|
|
provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult<CompletionItem[] | CompletionList> {
|
|
function update(item: CompletionItem) {
|
|
const range = item.range;
|
|
if (range instanceof Range && range.end.isAfter(position) && range.start.isBeforeOrEqual(position)) {
|
|
item.range = { inserting: new Range(range.start, position), replacing: range };
|
|
}
|
|
if (item.documentation instanceof MarkdownString) {
|
|
item.documentation = updateMarkdownString(item.documentation);
|
|
}
|
|
|
|
}
|
|
function updateProposals(r: CompletionItem[] | CompletionList | null | undefined): CompletionItem[] | CompletionList | null | undefined {
|
|
if (r) {
|
|
(Array.isArray(r) ? r : r.items).forEach(update);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
const r = next(document, position, context, token);
|
|
if (isThenable<CompletionItem[] | CompletionList | null | undefined>(r)) {
|
|
return r.then(updateProposals);
|
|
}
|
|
return updateProposals(r);
|
|
},
|
|
provideHover(document: TextDocument, position: Position, token: CancellationToken, next: ProvideHoverSignature) {
|
|
function updateHover(r: Hover | null | undefined): Hover | null | undefined {
|
|
if (r && Array.isArray(r.contents)) {
|
|
r.contents = r.contents.map(h => h instanceof MarkdownString ? updateMarkdownString(h) : h);
|
|
}
|
|
return r;
|
|
}
|
|
const r = next(document, position, token);
|
|
if (isThenable<Hover | null | undefined>(r)) {
|
|
return r.then(updateHover);
|
|
}
|
|
return updateHover(r);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Create the language client and start the client.
|
|
const client = newLanguageClient('json', localize('jsonserver.name', 'JSON Language Server'), clientOptions);
|
|
client.registerProposedFeatures();
|
|
|
|
const disposable = client.start();
|
|
toDispose.push(disposable);
|
|
client.onReady().then(() => {
|
|
const schemaDocuments: { [uri: string]: boolean } = {};
|
|
|
|
// handle content request
|
|
client.onRequest(VSCodeContentRequest.type, (uriPath: string) => {
|
|
const uri = Uri.parse(uriPath);
|
|
if (uri.scheme === 'untitled') {
|
|
return Promise.reject(new ResponseError(3, localize('untitled.schema', 'Unable to load {0}', uri.toString())));
|
|
}
|
|
if (uri.scheme !== 'http' && uri.scheme !== 'https') {
|
|
return workspace.openTextDocument(uri).then(doc => {
|
|
schemaDocuments[uri.toString()] = true;
|
|
return doc.getText();
|
|
}, error => {
|
|
return Promise.reject(new ResponseError(2, error.toString()));
|
|
});
|
|
} else if (schemaDownloadEnabled) {
|
|
if (runtime.telemetry && uri.authority === 'schema.management.azure.com') {
|
|
/* __GDPR__
|
|
"json.schema" : {
|
|
"schemaURL" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
|
}
|
|
*/
|
|
runtime.telemetry.sendTelemetryEvent('json.schema', { schemaURL: uriPath });
|
|
}
|
|
return runtime.http.getContent(uriPath);
|
|
} else {
|
|
return Promise.reject(new ResponseError(1, localize('schemaDownloadDisabled', 'Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload)));
|
|
}
|
|
});
|
|
|
|
const handleContentChange = (uriString: string) => {
|
|
if (schemaDocuments[uriString]) {
|
|
client.sendNotification(SchemaContentChangeNotification.type, uriString);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const handleActiveEditorChange = (activeEditor?: TextEditor) => {
|
|
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));
|
|
|
|
const 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(isSchemaResolveError);
|
|
if (schemaErrorIndex !== -1) {
|
|
// Show schema resolution errors in status bar only; ref: #51032
|
|
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, getSchemaAssociations(context));
|
|
|
|
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();
|
|
toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() });
|
|
|
|
updateSchemaDownloadSetting();
|
|
|
|
toDispose.push(workspace.onDidChangeConfiguration(e => {
|
|
if (e.affectsConfiguration(SettingIds.enableFormatter)) {
|
|
updateFormatterRegistration();
|
|
} else if (e.affectsConfiguration(SettingIds.enableSchemaDownload)) {
|
|
updateSchemaDownloadSetting();
|
|
}
|
|
}));
|
|
|
|
client.onNotification(ResultLimitReachedNotification.type, async message => {
|
|
const shouldPrompt = context.globalState.get<boolean>(StorageIds.maxItemsExceededInformation) !== false;
|
|
if (shouldPrompt) {
|
|
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);
|
|
if (pick === neverAgain) {
|
|
await context.globalState.update(StorageIds.maxItemsExceededInformation, false);
|
|
} else if (pick === openSettings) {
|
|
await commands.executeCommand('workbench.action.openSettings', SettingIds.maxItemsComputed);
|
|
}
|
|
}
|
|
});
|
|
|
|
function updateFormatterRegistration() {
|
|
const formatEnabled = workspace.getConfiguration().get(SettingIds.enableFormatter);
|
|
if (!formatEnabled && rangeFormatting) {
|
|
rangeFormatting.dispose();
|
|
rangeFormatting = undefined;
|
|
} else if (formatEnabled && !rangeFormatting) {
|
|
rangeFormatting = languages.registerDocumentRangeFormattingEditProvider(documentSelector, {
|
|
provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult<TextEdit[]> {
|
|
const filesConfig = workspace.getConfiguration('files', document);
|
|
const fileFormattingOptions = {
|
|
trimTrailingWhitespace: filesConfig.get<boolean>('trimTrailingWhitespace'),
|
|
trimFinalNewlines: filesConfig.get<boolean>('trimFinalNewlines'),
|
|
insertFinalNewline: filesConfig.get<boolean>('insertFinalNewline'),
|
|
};
|
|
const params: DocumentRangeFormattingParams = {
|
|
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
|
|
range: client.code2ProtocolConverter.asRange(range),
|
|
options: client.code2ProtocolConverter.asFormattingOptions(options, fileFormattingOptions)
|
|
};
|
|
|
|
return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then(
|
|
client.protocol2CodeConverter.asTextEdits,
|
|
(error) => {
|
|
client.handleFailedRequest(DocumentRangeFormattingRequest.type, error, []);
|
|
return Promise.resolve([]);
|
|
}
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function updateSchemaDownloadSetting() {
|
|
schemaDownloadEnabled = workspace.getConfiguration().get(SettingIds.enableSchemaDownload) !== false;
|
|
if (schemaDownloadEnabled) {
|
|
schemaResolutionErrorStatusBarItem.tooltip = localize('json.schemaResolutionErrorMessage', 'Unable to resolve schema. Click to retry.');
|
|
schemaResolutionErrorStatusBarItem.command = '_json.retryResolveSchema';
|
|
handleRetryResolveSchemaCommand();
|
|
} else {
|
|
schemaResolutionErrorStatusBarItem.tooltip = localize('json.schemaResolutionDisabledMessage', 'Downloading schemas is disabled. Click to configure.');
|
|
schemaResolutionErrorStatusBarItem.command = { command: 'workbench.action.openSettings', arguments: [SettingIds.enableSchemaDownload], title: '' };
|
|
}
|
|
}
|
|
|
|
});
|
|
|
|
const languageConfiguration: LanguageConfiguration = {
|
|
wordPattern: /("(?:[^\\\"]*(?:\\.)?)*"?)|[^\s{}\[\],:]+/,
|
|
indentationRules: {
|
|
increaseIndentPattern: /({+(?=([^"]*"[^"]*")*[^"}]*$))|(\[+(?=([^"]*"[^"]*")*[^"\]]*$))/,
|
|
decreaseIndentPattern: /^\s*[}\]],?\s*$/
|
|
}
|
|
};
|
|
languages.setLanguageConfiguration('json', languageConfiguration);
|
|
languages.setLanguageConfiguration('jsonc', languageConfiguration);
|
|
|
|
}
|
|
|
|
function getSchemaAssociations(_context: ExtensionContext): ISchemaAssociation[] {
|
|
const associations: ISchemaAssociation[] = [];
|
|
extensions.all.forEach(extension => {
|
|
const packageJSON = extension.packageJSON;
|
|
if (packageJSON && packageJSON.contributes && packageJSON.contributes.jsonValidation) {
|
|
const jsonValidation = packageJSON.contributes.jsonValidation;
|
|
if (Array.isArray(jsonValidation)) {
|
|
jsonValidation.forEach(jv => {
|
|
let { fileMatch, url } = jv;
|
|
if (typeof fileMatch === 'string') {
|
|
fileMatch = [fileMatch];
|
|
}
|
|
if (Array.isArray(fileMatch) && typeof url === 'string') {
|
|
let uri: string = url;
|
|
if (uri[0] === '.' && uri[1] === '/') {
|
|
uri = joinPath(extension.extensionUri, uri).toString();
|
|
}
|
|
fileMatch = fileMatch.map(fm => {
|
|
if (fm[0] === '%') {
|
|
fm = fm.replace(/%APP_SETTINGS_HOME%/, '/User');
|
|
fm = fm.replace(/%MACHINE_SETTINGS_HOME%/, '/Machine');
|
|
fm = fm.replace(/%APP_WORKSPACES_HOME%/, '/Workspaces');
|
|
} else if (!fm.match(/^(\w+:\/\/|\/|!)/)) {
|
|
fm = '/' + fm;
|
|
}
|
|
return fm;
|
|
});
|
|
associations.push({ fileMatch, uri });
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
return associations;
|
|
}
|
|
|
|
function getSettings(): Settings {
|
|
const httpSettings = workspace.getConfiguration('http');
|
|
|
|
const resultLimit: number = Math.trunc(Math.max(0, Number(workspace.getConfiguration().get(SettingIds.maxItemsComputed)))) || 5000;
|
|
|
|
const settings: Settings = {
|
|
http: {
|
|
proxy: httpSettings.get('proxy'),
|
|
proxyStrictSSL: httpSettings.get('proxyStrictSSL')
|
|
},
|
|
json: {
|
|
schemas: [],
|
|
resultLimit
|
|
}
|
|
};
|
|
const schemaSettingsById: { [schemaId: string]: JSONSchemaSettings } = Object.create(null);
|
|
const collectSchemaSettings = (schemaSettings: JSONSchemaSettings[], folderUri?: Uri, isMultiRoot?: boolean) => {
|
|
|
|
let fileMatchPrefix = undefined;
|
|
if (folderUri && isMultiRoot) {
|
|
fileMatchPrefix = folderUri.toString();
|
|
if (fileMatchPrefix[fileMatchPrefix.length - 1] === '/') {
|
|
fileMatchPrefix = fileMatchPrefix.substr(0, fileMatchPrefix.length - 1);
|
|
}
|
|
}
|
|
for (const setting of schemaSettings) {
|
|
const url = getSchemaId(setting, folderUri);
|
|
if (!url) {
|
|
continue;
|
|
}
|
|
let schemaSetting = schemaSettingsById[url];
|
|
if (!schemaSetting) {
|
|
schemaSetting = schemaSettingsById[url] = { url, fileMatch: [] };
|
|
settings.json!.schemas!.push(schemaSetting);
|
|
}
|
|
const fileMatches = setting.fileMatch;
|
|
if (Array.isArray(fileMatches)) {
|
|
const resultingFileMatches = schemaSetting.fileMatch || [];
|
|
schemaSetting.fileMatch = resultingFileMatches;
|
|
const addMatch = (pattern: string) => { // filter duplicates
|
|
if (resultingFileMatches.indexOf(pattern) === -1) {
|
|
resultingFileMatches.push(pattern);
|
|
}
|
|
};
|
|
for (const fileMatch of fileMatches) {
|
|
if (fileMatchPrefix) {
|
|
if (fileMatch[0] === '/') {
|
|
addMatch(fileMatchPrefix + fileMatch);
|
|
addMatch(fileMatchPrefix + '/*' + fileMatch);
|
|
} else {
|
|
addMatch(fileMatchPrefix + '/' + fileMatch);
|
|
addMatch(fileMatchPrefix + '/*/' + fileMatch);
|
|
}
|
|
} else {
|
|
addMatch(fileMatch);
|
|
}
|
|
}
|
|
}
|
|
if (setting.schema && !schemaSetting.schema) {
|
|
schemaSetting.schema = setting.schema;
|
|
}
|
|
}
|
|
};
|
|
|
|
const folders = workspace.workspaceFolders;
|
|
|
|
// merge global and folder settings. Qualify all file matches with the folder path.
|
|
const globalSettings = workspace.getConfiguration('json', null).get<JSONSchemaSettings[]>('schemas');
|
|
if (Array.isArray(globalSettings)) {
|
|
if (!folders) {
|
|
collectSchemaSettings(globalSettings);
|
|
}
|
|
}
|
|
if (folders) {
|
|
const isMultiRoot = folders.length > 1;
|
|
for (const folder of folders) {
|
|
const folderUri = folder.uri;
|
|
|
|
const schemaConfigInfo = workspace.getConfiguration('json', folderUri).inspect<JSONSchemaSettings[]>('schemas');
|
|
|
|
const folderSchemas = schemaConfigInfo!.workspaceFolderValue;
|
|
if (Array.isArray(folderSchemas)) {
|
|
collectSchemaSettings(folderSchemas, folderUri, isMultiRoot);
|
|
}
|
|
if (Array.isArray(globalSettings)) {
|
|
collectSchemaSettings(globalSettings, folderUri, isMultiRoot);
|
|
}
|
|
|
|
}
|
|
}
|
|
return settings;
|
|
}
|
|
|
|
function getSchemaId(schema: JSONSchemaSettings, folderUri?: Uri): string | undefined {
|
|
let url = schema.url;
|
|
if (!url) {
|
|
if (schema.schema) {
|
|
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();
|
|
}
|
|
return url;
|
|
}
|
|
|
|
function isThenable<T>(obj: ProviderResult<T>): obj is Thenable<T> {
|
|
return obj && (<any>obj)['then'];
|
|
}
|
|
|
|
function updateMarkdownString(h: MarkdownString): MarkdownString {
|
|
const n = new MarkdownString(h.value, true);
|
|
n.isTrusted = h.isTrusted;
|
|
return n;
|
|
}
|
|
|
|
function isSchemaResolveError(d: Diagnostic) {
|
|
return d.code === /* SchemaResolveError */ 0x300;
|
|
}
|