diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0301b00 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/.vscode/settings.json b/.vscode/settings.json index 1dc255b..a311c44 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,7 @@ "**/node_modules": true, "**/release": true, "**/out": true - } + }, + "javascript.preferences.quoteStyle": "single", + "typescript.preferences.quoteStyle": "single" } \ No newline at end of file diff --git a/package.json b/package.json index 560d264..df57d23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "monaco-yaml", - "version": "1.3.1", + "version": "2.0.0", "description": "YAML plugin for the Monaco Editor", "scripts": { "compile": "rimraf ./out && yarn compile:umd && yarn compile:esm", @@ -35,7 +35,7 @@ "rimraf": "^2.6.2", "typescript": "^3.1.6", "uglify-es": "^3.3.9", - "vscode-json-languageservice": "^3.1.6", - "vscode-languageserver-types": "3.12.0" + "vscode-languageserver-types": "3.12.0", + "yaml-language-server": "^0.1.0" } } diff --git a/scripts/bundle.js b/scripts/bundle.js index 51ff186..a639b60 100644 --- a/scripts/bundle.js +++ b/scripts/bundle.js @@ -46,10 +46,7 @@ function bundleOne(moduleId, exclude) { name: 'jsonc-parser', location: path.join(REPO_ROOT, 'node_modules/jsonc-parser/lib/umd'), main: 'main' - }, { - name: 'vscode-json-languageservice/lib', - location: path.join(REPO_ROOT, 'node_modules/vscode-json-languageservice/lib/umd') - }, + }, { name: 'vscode-languageserver-types', diff --git a/scripts/release.js b/scripts/release.js index 593140c..773ea31 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -22,7 +22,6 @@ helpers.packageESM({ "jsonc-parser/lib/esm": "jsonc-parser", "vscode-languageserver-types/lib/esm": "vscode-languageserver-types", "vscode-uri/lib/esm": "vscode-uri", - "vscode-json-languageservice/lib/esm": "vscode-json-languageservice", // "js-yaml/dist": "js-yaml" } }); diff --git a/src/languageFeatures.ts b/src/languageFeatures.ts index d8a9d9a..caf155b 100644 --- a/src/languageFeatures.ts +++ b/src/languageFeatures.ts @@ -1,432 +1,479 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { LanguageServiceDefaultsImpl } from './monaco.contribution'; -import { YAMLWorker } from './yamlWorker'; - -import * as ls from 'vscode-languageserver-types'; - -import Uri = monaco.Uri; -import Position = monaco.Position; -import Range = monaco.Range; -import IRange = monaco.IRange; -import Thenable = monaco.Thenable; -import CancellationToken = monaco.CancellationToken; -import IDisposable = monaco.IDisposable; - - -export interface WorkerAccessor { - (...more: Uri[]): Thenable -} - -// --- diagnostics --- --- - -export class DiagnosticsAdapter { - - private _disposables: IDisposable[] = []; - private _listener: { [uri: string]: IDisposable } = Object.create(null); - - constructor(private _languageId: string, private _worker: WorkerAccessor, defaults: LanguageServiceDefaultsImpl) { - const onModelAdd = (model: monaco.editor.IModel): void => { - let modeId = model.getModeId(); - if (modeId !== this._languageId) { - return; - } - - let handle: NodeJS.Timer; - this._listener[model.uri.toString()] = model.onDidChangeContent(() => { - clearTimeout(handle); - handle = setTimeout(() => this._doValidate(model.uri, modeId), 500); - }); - - this._doValidate(model.uri, modeId); - }; - - const onModelRemoved = (model: monaco.editor.IModel): void => { - monaco.editor.setModelMarkers(model, this._languageId, []); - let uriStr = model.uri.toString(); - let listener = this._listener[uriStr]; - if (listener) { - listener.dispose(); - delete this._listener[uriStr]; - } - }; - - this._disposables.push(monaco.editor.onDidCreateModel(onModelAdd)); - this._disposables.push(monaco.editor.onWillDisposeModel(model => { - onModelRemoved(model); - this._resetSchema(model.uri); - })); - this._disposables.push(monaco.editor.onDidChangeModelLanguage(event => { - onModelRemoved(event.model); - onModelAdd(event.model); - this._resetSchema(event.model.uri); - })); - - this._disposables.push(defaults.onDidChange(_ => { - monaco.editor.getModels().forEach(model => { - if (model.getModeId() === this._languageId) { - onModelRemoved(model); - onModelAdd(model); - } - }); - })); - - this._disposables.push({ - dispose: () => { - monaco.editor.getModels().forEach(onModelRemoved); - for (let key in this._listener) { - this._listener[key].dispose(); - } - } - }); - - monaco.editor.getModels().forEach(onModelAdd); - } - - public dispose(): void { - this._disposables.forEach(d => d && d.dispose()); - this._disposables = []; - } - - private _resetSchema(resource: Uri): void { - this._worker().then(worker => { - worker.resetSchema(resource.toString()); - }); - } - - private _doValidate(resource: Uri, languageId: string): void { - this._worker(resource).then(worker => { - return worker.doValidation(resource.toString()).then(diagnostics => { - const markers = diagnostics.map(d => toDiagnostics(resource, d)); - let model = monaco.editor.getModel(resource); - if (model.getModeId() === languageId) { - monaco.editor.setModelMarkers(model, languageId, markers); - } - }); - }).then(undefined, err => { - console.error(err); - }); - } -} - - -function toSeverity(lsSeverity: number): monaco.MarkerSeverity { - switch (lsSeverity) { - case ls.DiagnosticSeverity.Error: return monaco.MarkerSeverity.Error; - case ls.DiagnosticSeverity.Warning: return monaco.MarkerSeverity.Warning; - case ls.DiagnosticSeverity.Information: return monaco.MarkerSeverity.Info; - case ls.DiagnosticSeverity.Hint: return monaco.MarkerSeverity.Hint; - default: - return monaco.MarkerSeverity.Info; - } -} - -function toDiagnostics(resource: Uri, diag: ls.Diagnostic): monaco.editor.IMarkerData { - let code = typeof diag.code === 'number' ? String(diag.code) : diag.code; - - return { - severity: toSeverity(diag.severity), - startLineNumber: diag.range.start.line + 1, - startColumn: diag.range.start.character + 1, - endLineNumber: diag.range.end.line + 1, - endColumn: diag.range.end.character + 1, - message: diag.message, - code: code, - source: diag.source - }; -} - -// --- completion ------ - -function fromPosition(position: Position): ls.Position { - if (!position) { - return void 0; - } - return { character: position.column - 1, line: position.lineNumber - 1 }; -} - -function fromRange(range: IRange): ls.Range { - if (!range) { - return void 0; - } - return { start: { line: range.startLineNumber - 1, character: range.startColumn - 1 }, end: { line: range.endLineNumber - 1, character: range.endColumn - 1 } }; -} -function toRange(range: ls.Range): Range { - if (!range) { - return void 0; - } - return new Range(range.start.line + 1, range.start.character + 1, range.end.line + 1, range.end.character + 1); -} - -function toCompletionItemKind(kind: number): monaco.languages.CompletionItemKind { - let mItemKind = monaco.languages.CompletionItemKind; - - switch (kind) { - case ls.CompletionItemKind.Text: return mItemKind.Text; - case ls.CompletionItemKind.Method: return mItemKind.Method; - case ls.CompletionItemKind.Function: return mItemKind.Function; - case ls.CompletionItemKind.Constructor: return mItemKind.Constructor; - case ls.CompletionItemKind.Field: return mItemKind.Field; - case ls.CompletionItemKind.Variable: return mItemKind.Variable; - case ls.CompletionItemKind.Class: return mItemKind.Class; - case ls.CompletionItemKind.Interface: return mItemKind.Interface; - case ls.CompletionItemKind.Module: return mItemKind.Module; - case ls.CompletionItemKind.Property: return mItemKind.Property; - case ls.CompletionItemKind.Unit: return mItemKind.Unit; - case ls.CompletionItemKind.Value: return mItemKind.Value; - case ls.CompletionItemKind.Enum: return mItemKind.Enum; - case ls.CompletionItemKind.Keyword: return mItemKind.Keyword; - case ls.CompletionItemKind.Snippet: return mItemKind.Snippet; - case ls.CompletionItemKind.Color: return mItemKind.Color; - case ls.CompletionItemKind.File: return mItemKind.File; - case ls.CompletionItemKind.Reference: return mItemKind.Reference; - } - return mItemKind.Property; -} - -function fromCompletionItemKind(kind: monaco.languages.CompletionItemKind): ls.CompletionItemKind { - let mItemKind = monaco.languages.CompletionItemKind; - - switch (kind) { - case mItemKind.Text: return ls.CompletionItemKind.Text; - case mItemKind.Method: return ls.CompletionItemKind.Method; - case mItemKind.Function: return ls.CompletionItemKind.Function; - case mItemKind.Constructor: return ls.CompletionItemKind.Constructor; - case mItemKind.Field: return ls.CompletionItemKind.Field; - case mItemKind.Variable: return ls.CompletionItemKind.Variable; - case mItemKind.Class: return ls.CompletionItemKind.Class; - case mItemKind.Interface: return ls.CompletionItemKind.Interface; - case mItemKind.Module: return ls.CompletionItemKind.Module; - case mItemKind.Property: return ls.CompletionItemKind.Property; - case mItemKind.Unit: return ls.CompletionItemKind.Unit; - case mItemKind.Value: return ls.CompletionItemKind.Value; - case mItemKind.Enum: return ls.CompletionItemKind.Enum; - case mItemKind.Keyword: return ls.CompletionItemKind.Keyword; - case mItemKind.Snippet: return ls.CompletionItemKind.Snippet; - case mItemKind.Color: return ls.CompletionItemKind.Color; - case mItemKind.File: return ls.CompletionItemKind.File; - case mItemKind.Reference: return ls.CompletionItemKind.Reference; - } - return ls.CompletionItemKind.Property; -} - -function toTextEdit(textEdit: ls.TextEdit): monaco.editor.ISingleEditOperation { - if (!textEdit) { - return void 0; - } - return { - range: toRange(textEdit.range), - text: textEdit.newText - } -} - -export class CompletionAdapter implements monaco.languages.CompletionItemProvider { - - constructor(private _worker: WorkerAccessor) { - } - - public get triggerCharacters(): string[] { - return [' ', ':']; - } - - provideCompletionItems(model: monaco.editor.IReadOnlyModel, position: Position, context: monaco.languages.CompletionContext, token: CancellationToken): Thenable { - const wordInfo = model.getWordUntilPosition(position); - const resource = model.uri; - - return this._worker(resource).then(worker => { - return worker.doComplete(resource.toString(), fromPosition(position)); - }).then(info => { - if (!info) { - return; - } - let items: monaco.languages.CompletionItem[] = info.items.map(entry => { - let item: monaco.languages.CompletionItem = { - label: entry.label, - insertText: entry.insertText || entry.label, - sortText: entry.sortText, - filterText: entry.filterText, - documentation: entry.documentation, - detail: entry.detail, - kind: toCompletionItemKind(entry.kind), - }; - if (entry.textEdit) { - item.range = toRange(entry.textEdit.range); - item.insertText = entry.textEdit.newText; - } - if (entry.additionalTextEdits) { - item.additionalTextEdits = entry.additionalTextEdits.map(toTextEdit) - } - if (entry.insertTextFormat === ls.InsertTextFormat.Snippet) { - item.insertTextRules = monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet; - } - return item; - }); - - return { - isIncomplete: info.isIncomplete, - suggestions: items - }; - }); - } -} - -function isMarkupContent(thing: any): thing is ls.MarkupContent { - return thing && typeof thing === 'object' && typeof (thing).kind === 'string'; -} - -function toMarkdownString(entry: ls.MarkupContent | ls.MarkedString): monaco.IMarkdownString { - if (typeof entry === 'string') { - return { - value: entry - }; - } - if (isMarkupContent(entry)) { - if (entry.kind === 'plaintext') { - return { - value: entry.value.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&') - }; - } - return { - value: entry.value - }; - } - - return { value: '```' + entry.language + '\n' + entry.value + '\n```\n' }; -} - -function toMarkedStringArray(contents: ls.MarkupContent | ls.MarkedString | ls.MarkedString[]): monaco.IMarkdownString[] { - if (!contents) { - return void 0; - } - if (Array.isArray(contents)) { - return contents.map(toMarkdownString); - } - return [toMarkdownString(contents)]; -} - - -// --- hover ------ - -export class HoverAdapter implements monaco.languages.HoverProvider { - - constructor(private _worker: WorkerAccessor) {} - - provideHover(model: monaco.editor.IReadOnlyModel, position: Position, token: CancellationToken): Thenable { - let resource = model.uri; - - return this._worker(resource).then(worker => { - return worker.doHover(resource.toString(), fromPosition(position)); - }).then(info => { - if (!info) { - return; - } - return { - range: toRange(info.range), - contents: toMarkedStringArray(info.contents) - }; - }); - } -} - -// --- document symbols ------ - -function toSymbolKind(kind: ls.SymbolKind): monaco.languages.SymbolKind { - let mKind = monaco.languages.SymbolKind; - - switch (kind) { - case ls.SymbolKind.File: return mKind.Array; - case ls.SymbolKind.Module: return mKind.Module; - case ls.SymbolKind.Namespace: return mKind.Namespace; - case ls.SymbolKind.Package: return mKind.Package; - case ls.SymbolKind.Class: return mKind.Class; - case ls.SymbolKind.Method: return mKind.Method; - case ls.SymbolKind.Property: return mKind.Property; - case ls.SymbolKind.Field: return mKind.Field; - case ls.SymbolKind.Constructor: return mKind.Constructor; - case ls.SymbolKind.Enum: return mKind.Enum; - case ls.SymbolKind.Interface: return mKind.Interface; - case ls.SymbolKind.Function: return mKind.Function; - case ls.SymbolKind.Variable: return mKind.Variable; - case ls.SymbolKind.Constant: return mKind.Constant; - case ls.SymbolKind.String: return mKind.String; - case ls.SymbolKind.Number: return mKind.Number; - case ls.SymbolKind.Boolean: return mKind.Boolean; - case ls.SymbolKind.Array: return mKind.Array; - } - return mKind.Function; -} - - -export class DocumentSymbolAdapter implements monaco.languages.DocumentSymbolProvider { - - constructor(private _worker: WorkerAccessor) { - } - - public provideDocumentSymbols(model: monaco.editor.IReadOnlyModel, token: CancellationToken): Thenable { - const resource = model.uri; - - return this._worker(resource).then(worker => worker.findDocumentSymbols(resource.toString())).then(items => { - if (!items) { - return; - } - return items.map(item => ({ - name: item.name, - detail: '', - containerName: item.containerName, - kind: toSymbolKind(item.kind), - range: toRange(item.location.range), - selectionRange: toRange(item.location.range) - })); - }); - } -} - - -function fromFormattingOptions(options: monaco.languages.FormattingOptions): ls.FormattingOptions { - return { - tabSize: options.tabSize, - insertSpaces: options.insertSpaces - }; -} - -export class DocumentFormattingEditProvider implements monaco.languages.DocumentFormattingEditProvider { - - constructor(private _worker: WorkerAccessor) { - } - - public provideDocumentFormattingEdits(model: monaco.editor.IReadOnlyModel, options: monaco.languages.FormattingOptions, token: CancellationToken): Thenable { - const resource = model.uri; - - return this._worker(resource).then(worker => { - return worker.format(resource.toString(), null, fromFormattingOptions(options)).then(edits => { - if (!edits || edits.length === 0) { - return; - } - return edits.map(toTextEdit); - }); - }); - } -} - -export class DocumentRangeFormattingEditProvider implements monaco.languages.DocumentRangeFormattingEditProvider { - - constructor(private _worker: WorkerAccessor) { - } - - public provideDocumentRangeFormattingEdits(model: monaco.editor.IReadOnlyModel, range: Range, options: monaco.languages.FormattingOptions, token: CancellationToken): Thenable { - const resource = model.uri; - - return this._worker(resource).then(worker => { - return worker.format(resource.toString(), fromRange(range), fromFormattingOptions(options)).then(edits => { - if (!edits || edits.length === 0) { - return; - } - return edits.map(toTextEdit); - }); - }); - } -} +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { LanguageServiceDefaultsImpl } from './monaco.contribution'; +import { YAMLWorker } from './yamlWorker'; + +import * as ls from 'vscode-languageserver-types'; + +import Uri = monaco.Uri; +import Position = monaco.Position; +import Range = monaco.Range; +import IRange = monaco.IRange; +import Thenable = monaco.Thenable; +import CancellationToken = monaco.CancellationToken; +import IDisposable = monaco.IDisposable; + + +export interface WorkerAccessor { + (...more: Uri[]): Thenable +} + +// --- diagnostics --- --- + +export class DiagnosticsAdapter { + + private _disposables: IDisposable[] = []; + private _listener: { [uri: string]: IDisposable } = Object.create(null); + + constructor(private _languageId: string, private _worker: WorkerAccessor, defaults: LanguageServiceDefaultsImpl) { + const onModelAdd = (model: monaco.editor.IModel): void => { + let modeId = model.getModeId(); + if (modeId !== this._languageId) { + return; + } + + let handle: NodeJS.Timer; + this._listener[model.uri.toString()] = model.onDidChangeContent(() => { + clearTimeout(handle); + handle = setTimeout(() => this._doValidate(model.uri, modeId), 500); + }); + + this._doValidate(model.uri, modeId); + }; + + const onModelRemoved = (model: monaco.editor.IModel): void => { + monaco.editor.setModelMarkers(model, this._languageId, []); + let uriStr = model.uri.toString(); + let listener = this._listener[uriStr]; + if (listener) { + listener.dispose(); + delete this._listener[uriStr]; + } + }; + + this._disposables.push(monaco.editor.onDidCreateModel(onModelAdd)); + this._disposables.push(monaco.editor.onWillDisposeModel(model => { + onModelRemoved(model); + this._resetSchema(model.uri); + })); + this._disposables.push(monaco.editor.onDidChangeModelLanguage(event => { + onModelRemoved(event.model); + onModelAdd(event.model); + this._resetSchema(event.model.uri); + })); + + this._disposables.push(defaults.onDidChange(_ => { + monaco.editor.getModels().forEach(model => { + if (model.getModeId() === this._languageId) { + onModelRemoved(model); + onModelAdd(model); + } + }); + })); + + this._disposables.push({ + dispose: () => { + monaco.editor.getModels().forEach(onModelRemoved); + for (let key in this._listener) { + this._listener[key].dispose(); + } + } + }); + + monaco.editor.getModels().forEach(onModelAdd); + } + + public dispose(): void { + this._disposables.forEach(d => d && d.dispose()); + this._disposables = []; + } + + private _resetSchema(resource: Uri): void { + this._worker().then(worker => { + worker.resetSchema(resource.toString()); + }); + } + + private _doValidate(resource: Uri, languageId: string): void { + this._worker(resource).then(worker => { + return worker.doValidation(resource.toString()).then(diagnostics => { + const markers = diagnostics.map(d => toDiagnostics(resource, d)); + let model = monaco.editor.getModel(resource); + if (model.getModeId() === languageId) { + monaco.editor.setModelMarkers(model, languageId, markers); + } + }); + }).then(undefined, err => { + console.error(err); + }); + } +} + + +function toSeverity(lsSeverity: number): monaco.MarkerSeverity { + switch (lsSeverity) { + case ls.DiagnosticSeverity.Error: return monaco.MarkerSeverity.Error; + case ls.DiagnosticSeverity.Warning: return monaco.MarkerSeverity.Warning; + case ls.DiagnosticSeverity.Information: return monaco.MarkerSeverity.Info; + case ls.DiagnosticSeverity.Hint: return monaco.MarkerSeverity.Hint; + default: + return monaco.MarkerSeverity.Info; + } +} + +function toDiagnostics(resource: Uri, diag: ls.Diagnostic): monaco.editor.IMarkerData { + let code = typeof diag.code === 'number' ? String(diag.code) : diag.code; + + return { + severity: toSeverity(diag.severity), + startLineNumber: diag.range.start.line + 1, + startColumn: diag.range.start.character + 1, + endLineNumber: diag.range.end.line + 1, + endColumn: diag.range.end.character + 1, + message: diag.message, + code: code, + source: diag.source + }; +} + +// --- completion ------ + +function fromPosition(position: Position): ls.Position { + if (!position) { + return void 0; + } + return { character: position.column - 1, line: position.lineNumber - 1 }; +} + +function fromRange(range: IRange): ls.Range { + if (!range) { + return void 0; + } + return { start: { line: range.startLineNumber - 1, character: range.startColumn - 1 }, end: { line: range.endLineNumber - 1, character: range.endColumn - 1 } }; +} +function toRange(range: ls.Range): Range { + if (!range) { + return void 0; + } + return new Range(range.start.line + 1, range.start.character + 1, range.end.line + 1, range.end.character + 1); +} + +function toCompletionItemKind(kind: number): monaco.languages.CompletionItemKind { + let mItemKind = monaco.languages.CompletionItemKind; + + switch (kind) { + case ls.CompletionItemKind.Text: return mItemKind.Text; + case ls.CompletionItemKind.Method: return mItemKind.Method; + case ls.CompletionItemKind.Function: return mItemKind.Function; + case ls.CompletionItemKind.Constructor: return mItemKind.Constructor; + case ls.CompletionItemKind.Field: return mItemKind.Field; + case ls.CompletionItemKind.Variable: return mItemKind.Variable; + case ls.CompletionItemKind.Class: return mItemKind.Class; + case ls.CompletionItemKind.Interface: return mItemKind.Interface; + case ls.CompletionItemKind.Module: return mItemKind.Module; + case ls.CompletionItemKind.Property: return mItemKind.Property; + case ls.CompletionItemKind.Unit: return mItemKind.Unit; + case ls.CompletionItemKind.Value: return mItemKind.Value; + case ls.CompletionItemKind.Enum: return mItemKind.Enum; + case ls.CompletionItemKind.Keyword: return mItemKind.Keyword; + case ls.CompletionItemKind.Snippet: return mItemKind.Snippet; + case ls.CompletionItemKind.Color: return mItemKind.Color; + case ls.CompletionItemKind.File: return mItemKind.File; + case ls.CompletionItemKind.Reference: return mItemKind.Reference; + } + return mItemKind.Property; +} + +function fromCompletionItemKind(kind: monaco.languages.CompletionItemKind): ls.CompletionItemKind { + let mItemKind = monaco.languages.CompletionItemKind; + + switch (kind) { + case mItemKind.Text: return ls.CompletionItemKind.Text; + case mItemKind.Method: return ls.CompletionItemKind.Method; + case mItemKind.Function: return ls.CompletionItemKind.Function; + case mItemKind.Constructor: return ls.CompletionItemKind.Constructor; + case mItemKind.Field: return ls.CompletionItemKind.Field; + case mItemKind.Variable: return ls.CompletionItemKind.Variable; + case mItemKind.Class: return ls.CompletionItemKind.Class; + case mItemKind.Interface: return ls.CompletionItemKind.Interface; + case mItemKind.Module: return ls.CompletionItemKind.Module; + case mItemKind.Property: return ls.CompletionItemKind.Property; + case mItemKind.Unit: return ls.CompletionItemKind.Unit; + case mItemKind.Value: return ls.CompletionItemKind.Value; + case mItemKind.Enum: return ls.CompletionItemKind.Enum; + case mItemKind.Keyword: return ls.CompletionItemKind.Keyword; + case mItemKind.Snippet: return ls.CompletionItemKind.Snippet; + case mItemKind.Color: return ls.CompletionItemKind.Color; + case mItemKind.File: return ls.CompletionItemKind.File; + case mItemKind.Reference: return ls.CompletionItemKind.Reference; + } + return ls.CompletionItemKind.Property; +} + +function toTextEdit(textEdit: ls.TextEdit): monaco.editor.ISingleEditOperation { + if (!textEdit) { + return void 0; + } + return { + range: toRange(textEdit.range), + text: textEdit.newText + } +} + +export class CompletionAdapter implements monaco.languages.CompletionItemProvider { + + constructor(private _worker: WorkerAccessor) { + } + + public get triggerCharacters(): string[] { + return [' ', ':']; + } + + provideCompletionItems(model: monaco.editor.IReadOnlyModel, position: Position, context: monaco.languages.CompletionContext, token: CancellationToken): Thenable { + const wordInfo = model.getWordUntilPosition(position); + const resource = model.uri; + + return this._worker(resource).then(worker => { + return worker.doComplete(resource.toString(), fromPosition(position)); + }).then(info => { + if (!info) { + return; + } + let items: monaco.languages.CompletionItem[] = info.items.map(entry => { + let item: monaco.languages.CompletionItem = { + label: entry.label, + insertText: entry.insertText || entry.label, + sortText: entry.sortText, + filterText: entry.filterText, + documentation: entry.documentation, + detail: entry.detail, + kind: toCompletionItemKind(entry.kind), + }; + if (entry.textEdit) { + item.range = toRange(entry.textEdit.range); + item.insertText = entry.textEdit.newText; + } + if (entry.additionalTextEdits) { + item.additionalTextEdits = entry.additionalTextEdits.map(toTextEdit) + } + if (entry.insertTextFormat === ls.InsertTextFormat.Snippet) { + item.insertTextRules = monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet; + } + return item; + }); + + return { + isIncomplete: info.isIncomplete, + suggestions: items + }; + }); + } +} + +function isMarkupContent(thing: any): thing is ls.MarkupContent { + return thing && typeof thing === 'object' && typeof (thing).kind === 'string'; +} + +function toMarkdownString(entry: ls.MarkupContent | ls.MarkedString): monaco.IMarkdownString { + if (typeof entry === 'string') { + return { + value: entry + }; + } + if (isMarkupContent(entry)) { + if (entry.kind === 'plaintext') { + return { + value: entry.value.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&') + }; + } + return { + value: entry.value + }; + } + + return { value: '```' + entry.language + '\n' + entry.value + '\n```\n' }; +} + +function toMarkedStringArray(contents: ls.MarkupContent | ls.MarkedString | ls.MarkedString[]): monaco.IMarkdownString[] { + if (!contents) { + return void 0; + } + if (Array.isArray(contents)) { + return contents.map(toMarkdownString); + } + return [toMarkdownString(contents)]; +} + + +// --- hover ------ + +export class HoverAdapter implements monaco.languages.HoverProvider { + + constructor(private _worker: WorkerAccessor) { } + + provideHover(model: monaco.editor.IReadOnlyModel, position: Position, token: CancellationToken): Thenable { + let resource = model.uri; + + return this._worker(resource).then(worker => { + return worker.doHover(resource.toString(), fromPosition(position)); + }).then(info => { + if (!info) { + return; + } + return { + range: toRange(info.range), + contents: toMarkedStringArray(info.contents) + }; + }); + } +} + +// --- document symbols ------ + +function toSymbolKind(kind: ls.SymbolKind): monaco.languages.SymbolKind { + let mKind = monaco.languages.SymbolKind; + + switch (kind) { + case ls.SymbolKind.File: return mKind.Array; + case ls.SymbolKind.Module: return mKind.Module; + case ls.SymbolKind.Namespace: return mKind.Namespace; + case ls.SymbolKind.Package: return mKind.Package; + case ls.SymbolKind.Class: return mKind.Class; + case ls.SymbolKind.Method: return mKind.Method; + case ls.SymbolKind.Property: return mKind.Property; + case ls.SymbolKind.Field: return mKind.Field; + case ls.SymbolKind.Constructor: return mKind.Constructor; + case ls.SymbolKind.Enum: return mKind.Enum; + case ls.SymbolKind.Interface: return mKind.Interface; + case ls.SymbolKind.Function: return mKind.Function; + case ls.SymbolKind.Variable: return mKind.Variable; + case ls.SymbolKind.Constant: return mKind.Constant; + case ls.SymbolKind.String: return mKind.String; + case ls.SymbolKind.Number: return mKind.Number; + case ls.SymbolKind.Boolean: return mKind.Boolean; + case ls.SymbolKind.Array: return mKind.Array; + } + return mKind.Function; +} + + +export class DocumentSymbolAdapter implements monaco.languages.DocumentSymbolProvider { + + constructor(private _worker: WorkerAccessor) { + } + + public provideDocumentSymbols(model: monaco.editor.IReadOnlyModel, token: CancellationToken): Thenable { + const resource = model.uri; + + return this._worker(resource).then(worker => worker.findDocumentSymbols(resource.toString())).then(items => { + if (!items) { + return; + } + return items.map(item => toDocumentSymbol(item)); + }); + } +} + +function toDocumentSymbol(item: ls.DocumentSymbol): monaco.languages.DocumentSymbol { + return { + detail: '', + range: toRange(item.range), + name: item.name, + kind: toSymbolKind(item.kind), + selectionRange: toRange(item.selectionRange), + children: item.children.map(child => toDocumentSymbol(child)), + } +} + + +function fromFormattingOptions(options: monaco.languages.FormattingOptions): ls.FormattingOptions { + return { + tabSize: options.tabSize, + insertSpaces: options.insertSpaces + }; +} + +export class DocumentFormattingEditProvider implements monaco.languages.DocumentFormattingEditProvider { + + constructor(private _worker: WorkerAccessor) { + } + + public provideDocumentFormattingEdits(model: monaco.editor.IReadOnlyModel, options: monaco.languages.FormattingOptions, token: CancellationToken): Thenable { + const resource = model.uri; + + return this._worker(resource).then(worker => { + return worker.format(resource.toString(), null, fromFormattingOptions(options)).then(edits => { + if (!edits || edits.length === 0) { + return; + } + return edits.map(toTextEdit); + }); + }); + } +} + +export class DocumentRangeFormattingEditProvider implements monaco.languages.DocumentRangeFormattingEditProvider { + + constructor(private _worker: WorkerAccessor) { + } + + public provideDocumentRangeFormattingEdits(model: monaco.editor.IReadOnlyModel, range: Range, options: monaco.languages.FormattingOptions, token: CancellationToken): Thenable { + const resource = model.uri; + + return this._worker(resource).then(worker => { + return worker.format(resource.toString(), fromRange(range), fromFormattingOptions(options)).then(edits => { + if (!edits || edits.length === 0) { + return; + } + return edits.map(toTextEdit); + }); + }); + } +} + + +export class DocumentColorAdapter implements monaco.languages.DocumentColorProvider { + + constructor(private _worker: WorkerAccessor) { + } + + public provideDocumentColors(model: monaco.editor.IReadOnlyModel, token: CancellationToken): Thenable { + const resource = model.uri; + + return this._worker(resource).then(worker => worker.findDocumentColors(resource.toString())).then(infos => { + if (!infos) { + return; + } + return infos.map(item => ({ + color: item.color, + range: toRange(item.range) + })); + }); + } + + public provideColorPresentations(model: monaco.editor.IReadOnlyModel, info: monaco.languages.IColorInformation, token: CancellationToken): Thenable { + const resource = model.uri; + + return this._worker(resource).then(worker => worker.getColorPresentations(resource.toString(), info.color, fromRange(info.range))).then(presentations => { + if (!presentations) { + return; + } + return presentations.map(presentation => { + let item: monaco.languages.IColorPresentation = { + label: presentation.label, + }; + if (presentation.textEdit) { + item.textEdit = toTextEdit(presentation.textEdit) + } + if (presentation.additionalTextEdits) { + item.additionalTextEdits = presentation.additionalTextEdits.map(toTextEdit) + } + return item; + }); + }); + } +} diff --git a/src/languageservice/jsonContributions.ts b/src/languageservice/jsonContributions.ts index f11bcd0..8157554 100644 --- a/src/languageservice/jsonContributions.ts +++ b/src/languageservice/jsonContributions.ts @@ -5,7 +5,8 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import {Thenable, MarkedString, CompletionItem} from 'vscode-json-languageservice'; +import { Thenable } from './jsonLanguageTypes'; +import { MarkedString, CompletionItem } from 'vscode-languageserver-types'; export interface JSONWorkerContribution { getInfoContribution(uri: string, location: JSONPath): Thenable; @@ -23,4 +24,4 @@ export interface CompletionsCollector { log(message: string): void; setAsIncomplete(): void; getNumberOfProposals(): number; -} \ No newline at end of file +} diff --git a/src/languageservice/jsonLanguageTypes.ts b/src/languageservice/jsonLanguageTypes.ts new file mode 100644 index 0000000..3d892ee --- /dev/null +++ b/src/languageservice/jsonLanguageTypes.ts @@ -0,0 +1,258 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { JSONWorkerContribution, JSONPath, Segment, CompletionsCollector } from './jsonContributions'; +import { JSONSchema } from './jsonSchema'; +import { Range, TextEdit, Color, ColorInformation, ColorPresentation, FoldingRange, FoldingRangeKind, MarkupKind } from 'vscode-languageserver-types'; + +export { + Range, TextEdit, JSONSchema, JSONWorkerContribution, JSONPath, Segment, CompletionsCollector, + Color, ColorInformation, ColorPresentation, FoldingRange, FoldingRangeKind +}; + +/** + * Error codes used by diagnostics + */ +export enum ErrorCode { + Undefined = 0, + EnumValueMismatch = 1, + UnexpectedEndOfComment = 0x101, + UnexpectedEndOfString = 0x102, + UnexpectedEndOfNumber = 0x103, + InvalidUnicode = 0x104, + InvalidEscapeCharacter = 0x105, + InvalidCharacter = 0x106, + PropertyExpected = 0x201, + CommaExpected = 0x202, + ColonExpected = 0x203, + ValueExpected = 0x204, + CommaOrCloseBacketExpected = 0x205, + CommaOrCloseBraceExpected = 0x206, + TrailingComma = 0x207, + DuplicateKey = 0x208, + CommentNotPermitted = 0x209, + SchemaResolveError = 0x300 +} + +export type ASTNode = ObjectASTNode | PropertyASTNode | ArrayASTNode | StringASTNode | NumberASTNode | BooleanASTNode | NullASTNode; + +export interface BaseASTNode { + readonly type: 'object' | 'array' | 'property' | 'string' | 'number' | 'boolean' | 'null'; + readonly parent?: ASTNode; + readonly offset: number; + readonly length: number; + readonly children?: ASTNode[]; + readonly value?: string | boolean | number | null; +} +export interface ObjectASTNode extends BaseASTNode { + readonly type: 'object'; + readonly properties: PropertyASTNode[]; + readonly children: ASTNode[]; +} +export interface PropertyASTNode extends BaseASTNode { + readonly type: 'property'; + readonly keyNode: StringASTNode; + readonly valueNode?: ASTNode; + readonly colonOffset?: number; + readonly children: ASTNode[]; +} +export interface ArrayASTNode extends BaseASTNode { + readonly type: 'array'; + readonly items: ASTNode[]; + readonly children: ASTNode[]; +} +export interface StringASTNode extends BaseASTNode { + readonly type: 'string'; + readonly value: string; +} +export interface NumberASTNode extends BaseASTNode { + readonly type: 'number'; + readonly value: number; + readonly isInteger: boolean; +} +export interface BooleanASTNode extends BaseASTNode { + readonly type: 'boolean'; + readonly value: boolean; +} +export interface NullASTNode extends BaseASTNode { + readonly type: 'null'; + readonly value: null; +} + +export interface LanguageSettings { + /** + * If set, the validator will return syntax and semantic errors. + */ + validate?: boolean; + /** + * Defines whether comments are allowed or not. If set to false, comments will be reported as errors. + * DocumentLanguageSettings.allowComments will override this setting. + */ + allowComments?: boolean; + + /** + * A list of known schemas and/or associations of schemas to file names. + */ + schemas?: SchemaConfiguration[]; +} + +export type SeverityLevel = 'error' | 'warning' | 'ignore'; + +export interface DocumentLanguageSettings { + /** + * The severity of reported comments. If not set, 'LanguageSettings.allowComments' defines wheter comments are ignored or reported as errors. + */ + comments?: SeverityLevel; + + /** + * The severity of reported trailing commas. If not set, trailing commas will be reported as errors. + */ + trailingCommas?: SeverityLevel; +} + +export interface SchemaConfiguration { + /** + * The URI of the schema, which is also the identifier of the schema. + */ + uri: string; + /** + * A list of file names that are associated to the schema. The '*' wildcard can be used. For example '*.schema.json', 'package.json' + */ + fileMatch?: string[]; + /** + * The schema for the given URI. + * If no schema is provided, the schema will be fetched with the schema request service (if available). + */ + schema?: JSONSchema; +} + +export interface WorkspaceContextService { + resolveRelativePath(relativePath: string, resource: string): string; +} +/** + * The schema request service is used to fetch schemas. The result should the schema file comment, or, + * in case of an error, a displayable error string + */ +export interface SchemaRequestService { + (uri: string): Thenable; +} + +export interface PromiseConstructor { + /** + * Creates a new Promise. + * @param executor A callback used to initialize the promise. This callback is passed two arguments: + * a resolve callback used resolve the promise with a value or the result of another promise, + * and a reject callback used to reject the promise with a provided reason or error. + */ + new (executor: (resolve: (value?: T | Thenable) => void, reject: (reason?: any) => void) => void): Thenable; + + /** + * Creates a Promise that is resolved with an array of results when all of the provided Promises + * resolve, or rejected when any Promise is rejected. + * @param values An array of Promises. + * @returns A new Promise. + */ + all(values: Array>): Thenable; + /** + * Creates a new rejected promise for the provided reason. + * @param reason The reason the promise was rejected. + * @returns A new rejected Promise. + */ + reject(reason: any): Thenable; + + /** + * Creates a new resolved promise for the provided value. + * @param value A promise. + * @returns A promise whose internal state matches the provided promise. + */ + resolve(value: T | Thenable): Thenable; + +} + +export interface Thenable { + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: (value: R) => TResult | Thenable, onrejected?: (reason: any) => TResult | Thenable): Thenable; + then(onfulfilled?: (value: R) => TResult | Thenable, onrejected?: (reason: any) => void): Thenable; +} + +export interface LanguageServiceParams { + /** + * The schema request service is used to fetch schemas. The result should the schema file comment, or, + * in case of an error, a displayable error string + */ + schemaRequestService?: SchemaRequestService; + /** + * The workspace context is used to resolve relative paths for relative schema references. + */ + workspaceContext?: WorkspaceContextService; + /** + * An optional set of completion and hover participants. + */ + contributions?: JSONWorkerContribution[]; + /** + * A promise constructor. If not set, the ES5 Promise will be used. + */ + promiseConstructor?: PromiseConstructor; + /** + * Describes the LSP capabilities the client supports. + */ + clientCapabilities?: ClientCapabilities; +} + +/** + * Describes what LSP capabilities the client supports + */ +export interface ClientCapabilities { + /** + * The text document client capabilities + */ + textDocument?: { + /** + * Capabilities specific to completions. + */ + completion?: { + /** + * The client supports the following `CompletionItem` specific + * capabilities. + */ + completionItem?: { + /** + * Client supports the follow content formats for the documentation + * property. The order describes the preferred format of the client. + */ + documentationFormat?: MarkupKind[]; + }; + + }; + /** + * Capabilities specific to hovers. + */ + hover?: { + /** + * Client supports the follow content formats for the content + * property. The order describes the preferred format of the client. + */ + contentFormat?: MarkupKind[]; + }; + }; +} + +export namespace ClientCapabilities { + export const LATEST: ClientCapabilities = { + textDocument: { + completion: { + completionItem: { + documentationFormat: [MarkupKind.Markdown, MarkupKind.PlainText] + } + } + } + }; +} diff --git a/src/languageservice/jsonSchema.ts b/src/languageservice/jsonSchema.ts index 0052129..63ffe19 100644 --- a/src/languageservice/jsonSchema.ts +++ b/src/languageservice/jsonSchema.ts @@ -1,53 +1,76 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (c) Red Hat, Inc. All rights reserved. * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; +export type JSONSchemaRef = JSONSchema | boolean; + export interface JSONSchema { id?: string; + $id?: string; $schema?: string; type?: string | string[]; title?: string; default?: any; - definitions?: JSONSchemaMap; + definitions?: { [name: string]: JSONSchema }; description?: string; properties?: JSONSchemaMap; patternProperties?: JSONSchemaMap; - additionalProperties?: any; + additionalProperties?: boolean | JSONSchemaRef; minProperties?: number; maxProperties?: number; - dependencies?: JSONSchemaMap | string[]; - items?: any; + dependencies?: JSONSchemaMap | { [prop: string]: string[] }; + items?: JSONSchemaRef | JSONSchemaRef[]; minItems?: number; maxItems?: number; uniqueItems?: boolean; - additionalItems?: boolean; + additionalItems?: boolean | JSONSchemaRef; pattern?: string; minLength?: number; maxLength?: number; minimum?: number; maximum?: number; - exclusiveMinimum?: boolean; - exclusiveMaximum?: boolean; + exclusiveMinimum?: boolean | number; + exclusiveMaximum?: boolean | number; multipleOf?: number; required?: string[]; $ref?: string; - anyOf?: JSONSchema[]; - allOf?: JSONSchema[]; - oneOf?: JSONSchema[]; - not?: JSONSchema; + anyOf?: JSONSchemaRef[]; + allOf?: JSONSchemaRef[]; + oneOf?: JSONSchemaRef[]; + not?: JSONSchemaRef; enum?: any[]; format?: string; + + // schema draft 06 + const?: any; + contains?: JSONSchemaRef; + propertyNames?: JSONSchemaRef; + examples?: any[]; + + // schema draft 07 + $comment?: string; + if?: JSONSchemaRef; + then?: JSONSchemaRef; + else?: JSONSchemaRef; + + // VSCode extensions + + defaultSnippets?: { label?: string; description?: string; markdownDescription?: string; body?: any; bodyText?: string; }[]; // VSCode extension: body: a object that will be converted to a JSON string. bodyText: text with \t and \n errorMessage?: string; // VSCode extension patternErrorMessage?: string; // VSCode extension deprecationMessage?: string; // VSCode extension enumDescriptions?: string[]; // VSCode extension - schemaSequence?: JSONSchema[]; // extension for multiple schemas related to multiple documents in single yaml file + markdownEnumDescriptions?: string[]; // VSCode extension + markdownDescription?: string; // VSCode extension + doNotSuggest?: boolean; // VSCode extension + allowComments?: boolean; // VSCode extension + + schemaSequence?: JSONSchema[]; // extension for multiple schemas related to multiple documents in single yaml file "x-kubernetes-group-version-kind"?; //Kubernetes extension } export interface JSONSchemaMap { - [name: string]:JSONSchema; + [name: string]: JSONSchemaRef; } diff --git a/src/languageservice/parser/jsonParser.ts b/src/languageservice/parser/jsonParser.ts index b523f97..83dcdbb 100644 --- a/src/languageservice/parser/jsonParser.ts +++ b/src/languageservice/parser/jsonParser.ts @@ -1,1051 +1,1293 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (c) Red Hat, Inc. All rights reserved. * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import * as Json from 'jsonc-parser'; -import { JSONSchema } from '../jsonSchema'; -import * as objects from '../utils/objects'; +import { JSONSchema, JSONSchemaRef } from '../jsonSchema'; +import { isNumber, equals, isBoolean, isString, isDefined } from '../utils/objects'; +import { ASTNode, ObjectASTNode, ArrayASTNode, BooleanASTNode, NumberASTNode, StringASTNode, NullASTNode, PropertyASTNode, JSONPath, ErrorCode } from '../jsonLanguageTypes'; import * as nls from 'vscode-nls'; -import { LanguageSettings } from '../yamlLanguageService'; +import Uri from 'vscode-uri'; +import { TextDocument, Diagnostic, DiagnosticSeverity, Range } from 'vscode-languageserver-types'; + const localize = nls.loadMessageBundle(); export interface IRange { - start: number; - end: number; + offset: number; + length: number; } -export enum ErrorCode { - Undefined = 0, - EnumValueMismatch = 1, - CommentsNotAllowed = 2 -} - -export enum ProblemSeverity { - Error, Warning -} +const colorHexPattern = /^#([0-9A-Fa-f]{3,4}|([0-9A-Fa-f]{2}){3,4})$/; +const emailPattern = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; export interface IProblem { - location: IRange; - severity: ProblemSeverity; - code?: ErrorCode; - message: string; + location: IRange; + severity: DiagnosticSeverity; + code?: ErrorCode; + message: string; } -export class ASTNode { - public start: number; - public end: number; - public type: string; - public parent: ASTNode; - public parserSettings: LanguageSettings; - public location: Json.Segment; +export abstract class ASTNodeImpl { - constructor(parent: ASTNode, type: string, location: Json.Segment, start: number, end?: number) { - this.type = type; - this.location = location; - this.start = start; - this.end = end; - this.parent = parent; - this.parserSettings = { - isKubernetes: false - }; - } + public readonly abstract type: 'object' | 'property' | 'array' | 'number' | 'boolean' | 'null' | 'string'; - public setParserSettings(parserSettings: LanguageSettings){ - this.parserSettings = parserSettings; - } + public offset: number; + public length: number; + public readonly parent: ASTNode; - public getPath(): Json.JSONPath { - let path = this.parent ? this.parent.getPath() : []; - if (this.location !== null) { - path.push(this.location); - } - return path; - } + constructor(parent: ASTNode, offset: number, length?: number) { + this.offset = offset; + this.length = length; + this.parent = parent; + } + public get children(): ASTNode[] { + return []; + } - public getChildNodes(): ASTNode[] { - return []; - } - - public getLastChild(): ASTNode { - return null; - } - - public getValue(): any { - // override in children - return; - } - - public contains(offset: number, includeRightBound: boolean = false): boolean { - return offset >= this.start && offset < this.end || includeRightBound && offset === this.end; - } - - public toString(): string { - return 'type: ' + this.type + ' (' + this.start + '/' + this.end + ')' + (this.parent ? ' parent: {' + this.parent.toString() + '}' : ''); - } - - public visit(visitor: (node: ASTNode) => boolean): boolean { - return visitor(this); - } - - public getNodeFromOffset(offset: number): ASTNode { - let findNode = (node: ASTNode): ASTNode => { - if (offset >= node.start && offset < node.end) { - let children = node.getChildNodes(); - for (let i = 0; i < children.length && children[i].start <= offset; i++) { - let item = findNode(children[i]); - if (item) { - return item; - } - } - return node; - } - return null; - }; - return findNode(this); - } - - public getNodeCollectorCount(offset: number): Number { - let collector = []; - let findNode = (node: ASTNode): ASTNode => { - let children = node.getChildNodes(); - for (let i = 0; i < children.length; i++) { - let item = findNode(children[i]); - if (item && item.type === "property") { - collector.push(item); - } - } - return node; - }; - let foundNode = findNode(this); - return collector.length; - } - - public getNodeFromOffsetEndInclusive(offset: number): ASTNode { - let collector = []; - let findNode = (node: ASTNode): ASTNode => { - if (offset >= node.start && offset <= node.end) { - let children = node.getChildNodes(); - for (let i = 0; i < children.length && children[i].start <= offset; i++) { - let item = findNode(children[i]); - if (item) { - collector.push(item); - } - } - return node; - } - return null; - }; - let foundNode = findNode(this); - let currMinDist = Number.MAX_VALUE; - let currMinNode = null; - for(let possibleNode in collector){ - let currNode = collector[possibleNode]; - let minDist = (currNode.end - offset) + (offset - currNode.start); - if(minDist < currMinDist){ - currMinNode = currNode; - currMinDist = minDist; - } - } - return currMinNode || foundNode; - } - - public validate(schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { - if (!matchingSchemas.include(this)) { - return; - } - - if (Array.isArray(schema.type)) { - if ((schema.type).indexOf(this.type) === -1) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: schema.errorMessage || localize('typeArrayMismatchWarning', 'Incorrect type. Expected one of {0}.', (schema.type).join(', ')) - }); - } - } - else if (schema.type) { - if (this.type !== schema.type) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: schema.errorMessage || localize('typeMismatchWarning', 'Incorrect type. Expected "{0}".', schema.type) - }); - } - } - if (Array.isArray(schema.allOf)) { - schema.allOf.forEach((subSchema) => { - this.validate(subSchema, validationResult, matchingSchemas); - }); - } - if (schema.not) { - let subValidationResult = new ValidationResult(); - let subMatchingSchemas = matchingSchemas.newSub(); - this.validate(schema.not, subValidationResult, subMatchingSchemas); - if (!subValidationResult.hasProblems()) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('notSchemaWarning', "Matches a schema that is not allowed.") - }); - } - subMatchingSchemas.schemas.forEach((ms) => { - ms.inverted = !ms.inverted; - matchingSchemas.add(ms); - }); - } - - let testAlternatives = (alternatives: JSONSchema[], maxOneMatch: boolean) => { - let matches = []; - - // remember the best match that is used for error messages - let bestMatch: { schema: JSONSchema; validationResult: ValidationResult; matchingSchemas: ISchemaCollector; } = null; - alternatives.forEach((subSchema) => { - let subValidationResult = new ValidationResult(); - let subMatchingSchemas = matchingSchemas.newSub(); - - this.validate(subSchema, subValidationResult, subMatchingSchemas); - if (!subValidationResult.hasProblems()) { - matches.push(subSchema); - } - if (!bestMatch) { - bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas }; - } else if(this.parserSettings.isKubernetes) { - bestMatch = alternativeComparison(subValidationResult, bestMatch, subSchema, subMatchingSchemas); - } else { - bestMatch = genericComparison(maxOneMatch, subValidationResult, bestMatch, subSchema, subMatchingSchemas); - } - }); - - if (matches.length > 1 && maxOneMatch && !this.parserSettings.isKubernetes) { - validationResult.problems.push({ - location: { start: this.start, end: this.start + 1 }, - severity: ProblemSeverity.Warning, - message: localize('oneOfWarning', "Matches multiple schemas when only one must validate.") - }); - } - if (bestMatch !== null) { - validationResult.merge(bestMatch.validationResult); - validationResult.propertiesMatches += bestMatch.validationResult.propertiesMatches; - validationResult.propertiesValueMatches += bestMatch.validationResult.propertiesValueMatches; - matchingSchemas.merge(bestMatch.matchingSchemas); - } - return matches.length; - }; - if (Array.isArray(schema.anyOf)) { - testAlternatives(schema.anyOf, false); - } - if (Array.isArray(schema.oneOf)) { - testAlternatives(schema.oneOf, true); - } - - if (Array.isArray(schema.enum)) { - let val = this.getValue(); - let enumValueMatch = false; - for (let e of schema.enum) { - if (objects.equals(val, e)) { - enumValueMatch = true; - break; - } - } - validationResult.enumValues = schema.enum; - validationResult.enumValueMatch = enumValueMatch; - if (!enumValueMatch) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - code: ErrorCode.EnumValueMismatch, - message: schema.errorMessage || localize('enumWarning', 'Value is not accepted. Valid values: {0}.', schema.enum.map(v => JSON.stringify(v)).join(', ')) - }); - } - } - - if (schema.deprecationMessage && this.parent) { - validationResult.problems.push({ - location: { start: this.parent.start, end: this.parent.end }, - severity: ProblemSeverity.Warning, - message: schema.deprecationMessage - }); - } - matchingSchemas.add({ node: this, schema: schema }); - } + public toString(): string { + return 'type: ' + this.type + ' (' + this.offset + '/' + this.length + ')' + (this.parent ? ' parent: {' + this.parent.toString() + '}' : ''); + } } -export class NullASTNode extends ASTNode { +export class NullASTNodeImpl extends ASTNodeImpl implements NullASTNode { - constructor(parent: ASTNode, name: Json.Segment, start: number, end?: number) { - super(parent, 'null', name, start, end); - } - - public getValue(): any { - return null; - } + public type: 'null' = 'null'; + public value: null = null; + constructor(parent: ASTNode, offset: number, length?: number) { + super(parent, offset, length); + } } -export class BooleanASTNode extends ASTNode { +export class BooleanASTNodeImpl extends ASTNodeImpl implements BooleanASTNode { - private value: boolean | string; + public type: 'boolean' = 'boolean'; + public value: boolean; - constructor(parent: ASTNode, name: Json.Segment, value: boolean | string, start: number, end?: number) { - super(parent, 'boolean', name, start, end); - this.value = value; - } + constructor(parent: ASTNode, boolValue: boolean, offset: number, length?: number) { + super(parent, offset, length); + this.value = boolValue; + } +} - public getValue(): any { - return this.value; - } +export class ArrayASTNodeImpl extends ASTNodeImpl implements ArrayASTNode { + + public type: 'array' = 'array'; + public items: ASTNode[]; + + constructor(parent: ASTNode, offset: number, length?: number) { + super(parent, offset, length); + this.items = []; + } + + public get children(): ASTNode[] { + return this.items; + } +} + +export class NumberASTNodeImpl extends ASTNodeImpl implements NumberASTNode { + + public type: 'number' = 'number'; + public isInteger: boolean; + public value: number; + + constructor(parent: ASTNode, offset: number, length?: number) { + super(parent, offset, length); + this.isInteger = true; + this.value = Number.NaN; + } +} + +export class StringASTNodeImpl extends ASTNodeImpl implements StringASTNode { + public type: 'string' = 'string'; + public value: string; + + constructor(parent: ASTNode, offset: number, length?: number) { + super(parent, offset, length); + this.value = ''; + } +} + +export class PropertyASTNodeImpl extends ASTNodeImpl implements PropertyASTNode { + public type: 'property' = 'property'; + public keyNode: StringASTNode; + public valueNode: ASTNode; + public colonOffset: number; + + constructor(parent: ObjectASTNode, offset: number, length?: number) { + super(parent, offset, length); + this.colonOffset = -1; + } + + public get children(): ASTNode[] { + return this.valueNode ? [this.keyNode, this.valueNode] : [this.keyNode]; + } +} + +export class ObjectASTNodeImpl extends ASTNodeImpl implements ObjectASTNode { + public type: 'object' = 'object'; + public properties: PropertyASTNode[]; + + constructor(parent: ASTNode, offset: number, length?: number) { + super(parent, offset, length); + + this.properties = []; + } + + public get children(): ASTNode[] { + return this.properties; + } } -export class ArrayASTNode extends ASTNode { - - public items: ASTNode[]; - - constructor(parent: ASTNode, name: Json.Segment, start: number, end?: number) { - super(parent, 'array', name, start, end); - this.items = []; - } - - public getChildNodes(): ASTNode[] { - return this.items; - } - - public getLastChild(): ASTNode { - return this.items[this.items.length - 1]; - } - - public getValue(): any { - return this.items.map((v) => v.getValue()); - } - - public addItem(item: ASTNode): boolean { - if (item) { - this.items.push(item); - return true; - } - return false; - } - - public visit(visitor: (node: ASTNode) => boolean): boolean { - let ctn = visitor(this); - for (let i = 0; i < this.items.length && ctn; i++) { - ctn = this.items[i].visit(visitor); - } - return ctn; - } - - public validate(schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { - if (!matchingSchemas.include(this)) { - return; - } - super.validate(schema, validationResult, matchingSchemas); - - if (Array.isArray(schema.items)) { - let subSchemas = schema.items; - subSchemas.forEach((subSchema, index) => { - let itemValidationResult = new ValidationResult(); - let item = this.items[index]; - if (item) { - item.validate(subSchema, itemValidationResult, matchingSchemas); - validationResult.mergePropertyMatch(itemValidationResult); - } else if (this.items.length >= subSchemas.length) { - validationResult.propertiesValueMatches++; - } - }); - if (this.items.length > subSchemas.length) { - if (typeof schema.additionalItems === 'object') { - for (let i = subSchemas.length; i < this.items.length; i++) { - let itemValidationResult = new ValidationResult(); - this.items[i].validate(schema.additionalItems, itemValidationResult, matchingSchemas); - validationResult.mergePropertyMatch(itemValidationResult); - } - } else if (schema.additionalItems === false) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('additionalItemsWarning', 'Array has too many items according to schema. Expected {0} or fewer.', subSchemas.length) - }); - } - } - } - else if (schema.items) { - this.items.forEach((item) => { - let itemValidationResult = new ValidationResult(); - item.validate(schema.items, itemValidationResult, matchingSchemas); - validationResult.mergePropertyMatch(itemValidationResult); - }); - } - - if (schema.minItems && this.items.length < schema.minItems) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('minItemsWarning', 'Array has too few items. Expected {0} or more.', schema.minItems) - }); - } - - if (schema.maxItems && this.items.length > schema.maxItems) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('maxItemsWarning', 'Array has too many items. Expected {0} or fewer.', schema.minItems) - }); - } - - if (schema.uniqueItems === true) { - let values = this.items.map((node) => { - return node.getValue(); - }); - let duplicates = values.some((value, index) => { - return index !== values.lastIndexOf(value); - }); - if (duplicates) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('uniqueItemsWarning', 'Array has duplicate items.') - }); - } - } - } +export function asSchema(schema: JSONSchemaRef) { + if (isBoolean(schema)) { + return schema ? {} : { "not": {} }; + } + return schema; } -export class NumberASTNode extends ASTNode { - - public isInteger: boolean; - public value: number; - - constructor(parent: ASTNode, name: Json.Segment, start: number, end?: number) { - super(parent, 'number', name, start, end); - this.isInteger = true; - this.value = Number.NaN; - } - - public getValue(): any { - return this.value; - } - - public validate(schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { - if (!matchingSchemas.include(this)) { - return; - } - - // work around type validation in the base class - let typeIsInteger = false; - if (schema.type === 'integer' || (Array.isArray(schema.type) && (schema.type).indexOf('integer') !== -1)) { - typeIsInteger = true; - } - if (typeIsInteger && this.isInteger === true) { - this.type = 'integer'; - } - super.validate(schema, validationResult, matchingSchemas); - this.type = 'number'; - - let val = this.getValue(); - - if (typeof schema.multipleOf === 'number') { - if (val % schema.multipleOf !== 0) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('multipleOfWarning', 'Value is not divisible by {0}.', schema.multipleOf) - }); - } - } - - if (typeof schema.minimum === 'number') { - if (schema.exclusiveMinimum && val <= schema.minimum) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('exclusiveMinimumWarning', 'Value is below the exclusive minimum of {0}.', schema.minimum) - }); - } - if (!schema.exclusiveMinimum && val < schema.minimum) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('minimumWarning', 'Value is below the minimum of {0}.', schema.minimum) - }); - } - } - - if (typeof schema.maximum === 'number') { - if (schema.exclusiveMaximum && val >= schema.maximum) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('exclusiveMaximumWarning', 'Value is above the exclusive maximum of {0}.', schema.maximum) - }); - } - if (!schema.exclusiveMaximum && val > schema.maximum) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('maximumWarning', 'Value is above the maximum of {0}.', schema.maximum) - }); - } - } - - } -} - -export class StringASTNode extends ASTNode { - public isKey: boolean; - public value: string; - - constructor(parent: ASTNode, name: Json.Segment, isKey: boolean, start: number, end?: number) { - super(parent, 'string', name, start, end); - this.isKey = isKey; - this.value = ''; - } - - public getValue(): any { - return this.value; - } - - public validate(schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { - if (!matchingSchemas.include(this)) { - return; - } - super.validate(schema, validationResult, matchingSchemas); - - if (schema.minLength && this.value.length < schema.minLength) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('minLengthWarning', 'String is shorter than the minimum length of {0}.', schema.minLength) - }); - } - - if (schema.maxLength && this.value.length > schema.maxLength) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('maxLengthWarning', 'String is longer than the maximum length of {0}.', schema.maxLength) - }); - } - - if (schema.pattern) { - let regex = new RegExp(schema.pattern); - if (!regex.test(this.value)) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: schema.patternErrorMessage || schema.errorMessage || localize('patternWarning', 'String does not match the pattern of "{0}".', schema.pattern) - }); - } - } - - } -} - -export class PropertyASTNode extends ASTNode { - public key: StringASTNode; - public value: ASTNode; - public colonOffset: number; - - constructor(parent: ASTNode, key: StringASTNode) { - super(parent, 'property', null, key.start); - this.key = key; - key.parent = this; - key.location = key.value; - this.colonOffset = -1; - } - - public getChildNodes(): ASTNode[] { - return this.value ? [this.key, this.value] : [this.key]; - } - - public getLastChild(): ASTNode { - return this.value; - } - - public setValue(value: ASTNode): boolean { - this.value = value; - return value !== null; - } - - public visit(visitor: (node: ASTNode) => boolean): boolean { - return visitor(this) && this.key.visit(visitor) && this.value && this.value.visit(visitor); - } - - public validate(schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { - if (!matchingSchemas.include(this)) { - return; - } - if (this.value) { - this.value.validate(schema, validationResult, matchingSchemas); - } - } -} - -export class ObjectASTNode extends ASTNode { - public properties: PropertyASTNode[]; - - constructor(parent: ASTNode, name: Json.Segment, start: number, end?: number) { - super(parent, 'object', name, start, end); - - this.properties = []; - } - - public getChildNodes(): ASTNode[] { - return this.properties; - } - - public getLastChild(): ASTNode { - return this.properties[this.properties.length - 1]; - } - - public addProperty(node: PropertyASTNode): boolean { - if (!node) { - return false; - } - this.properties.push(node); - return true; - } - - public getFirstProperty(key: string): PropertyASTNode { - for (let i = 0; i < this.properties.length; i++) { - if (this.properties[i].key.value === key) { - return this.properties[i]; - } - } - return null; - } - - public getKeyList(): string[] { - return this.properties.map((p) => p.key.getValue()); - } - - public getValue(): any { - let value: any = Object.create(null); - this.properties.forEach((p) => { - let v = p.value && p.value.getValue(); - if (typeof v !== 'undefined') { - value[p.key.getValue()] = v; - } - }); - return value; - } - - public visit(visitor: (node: ASTNode) => boolean): boolean { - let ctn = visitor(this); - for (let i = 0; i < this.properties.length && ctn; i++) { - ctn = this.properties[i].visit(visitor); - } - return ctn; - } - - public validate(schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { - if (!matchingSchemas.include(this)) { - return; - } - - super.validate(schema, validationResult, matchingSchemas); - let seenKeys: { [key: string]: ASTNode } = Object.create(null); - let unprocessedProperties: string[] = []; - this.properties.forEach((node) => { - - let key = node.key.value; - - //Replace the merge key with the actual values of what the node value points to in seen keys - if(key === "<<" && node.value) { - - switch(node.value.type) { - case "object": { - node.value["properties"].forEach(propASTNode => { - let propKey = propASTNode.key.value; - seenKeys[propKey] = propASTNode.value; - unprocessedProperties.push(propKey); - }); - break; - } - case "array": { - node.value["items"].forEach(sequenceNode => { - sequenceNode["properties"].forEach(propASTNode => { - let seqKey = propASTNode.key.value; - seenKeys[seqKey] = propASTNode.value; - unprocessedProperties.push(seqKey); - }); - }); - break; - } - default: { - break; - } - } - }else{ - seenKeys[key] = node.value; - unprocessedProperties.push(key); - } - - }); - - if (Array.isArray(schema.required)) { - schema.required.forEach((propertyName: string) => { - if (!seenKeys[propertyName]) { - let key = this.parent && this.parent && (this.parent).key; - let location = key ? { start: key.start, end: key.end } : { start: this.start, end: this.start + 1 }; - validationResult.problems.push({ - location: location, - severity: ProblemSeverity.Warning, - message: localize('MissingRequiredPropWarning', 'Missing property "{0}".', propertyName) - }); - } - }); - } - - let propertyProcessed = (prop: string) => { - let index = unprocessedProperties.indexOf(prop); - while (index >= 0) { - unprocessedProperties.splice(index, 1); - index = unprocessedProperties.indexOf(prop); - } - }; - - if (schema.properties) { - Object.keys(schema.properties).forEach((propertyName: string) => { - propertyProcessed(propertyName); - let prop = schema.properties[propertyName]; - let child = seenKeys[propertyName]; - if (child) { - let propertyValidationResult = new ValidationResult(); - child.validate(prop, propertyValidationResult, matchingSchemas); - validationResult.mergePropertyMatch(propertyValidationResult); - } - - }); - } - - if (schema.patternProperties) { - Object.keys(schema.patternProperties).forEach((propertyPattern: string) => { - let regex = new RegExp(propertyPattern); - unprocessedProperties.slice(0).forEach((propertyName: string) => { - if (regex.test(propertyName)) { - propertyProcessed(propertyName); - let child = seenKeys[propertyName]; - if (child) { - let propertyValidationResult = new ValidationResult(); - child.validate(schema.patternProperties[propertyPattern], propertyValidationResult, matchingSchemas); - validationResult.mergePropertyMatch(propertyValidationResult); - } - - } - }); - }); - } - - if (typeof schema.additionalProperties === 'object') { - unprocessedProperties.forEach((propertyName: string) => { - let child = seenKeys[propertyName]; - if (child) { - let propertyValidationResult = new ValidationResult(); - child.validate(schema.additionalProperties, propertyValidationResult, matchingSchemas); - validationResult.mergePropertyMatch(propertyValidationResult); - } - }); - } else if (schema.additionalProperties === false) { - if (unprocessedProperties.length > 0) { - unprocessedProperties.forEach((propertyName: string) => { - let child = seenKeys[propertyName]; - if (child) { - let propertyNode = null; - if(child.type !== "property"){ - propertyNode = child.parent; - if(propertyNode.type === "object"){ - propertyNode = propertyNode.properties[0]; - } - }else{ - propertyNode = child; - } - validationResult.problems.push({ - location: { start: propertyNode.key.start, end: propertyNode.key.end }, - severity: ProblemSeverity.Warning, - message: schema.errorMessage || localize('DisallowedExtraPropWarning', 'Unexpected property {0}', propertyName) - }); - } - }); - } - } - - if (schema.maxProperties) { - if (this.properties.length > schema.maxProperties) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('MaxPropWarning', 'Object has more properties than limit of {0}.', schema.maxProperties) - }); - } - } - - if (schema.minProperties) { - if (this.properties.length < schema.minProperties) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('MinPropWarning', 'Object has fewer properties than the required number of {0}', schema.minProperties) - }); - } - } - - if (schema.dependencies) { - Object.keys(schema.dependencies).forEach((key: string) => { - let prop = seenKeys[key]; - if (prop) { - let propertyDep = schema.dependencies[key] - if (Array.isArray(propertyDep)) { - propertyDep.forEach((requiredProp: string) => { - if (!seenKeys[requiredProp]) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('RequiredDependentPropWarning', 'Object is missing property {0} required by property {1}.', requiredProp, key) - }); - } else { - validationResult.propertiesValueMatches++; - } - }); - } else if (propertyDep) { - let propertyvalidationResult = new ValidationResult(); - this.validate(propertyDep, propertyvalidationResult, matchingSchemas); - validationResult.mergePropertyMatch(propertyvalidationResult); - } - } - }); - } - } +export interface JSONDocumentConfig { + collectComments?: boolean; } export interface IApplicableSchema { - node: ASTNode; - inverted?: boolean; - schema: JSONSchema; + node: ASTNode; + inverted?: boolean; + schema: JSONSchema; } export enum EnumMatch { - Key, Enum + Key, Enum } export interface ISchemaCollector { - schemas: IApplicableSchema[]; - add(schema: IApplicableSchema): void; - merge(other: ISchemaCollector): void; - include(node: ASTNode): boolean; - newSub(): ISchemaCollector; + schemas: IApplicableSchema[]; + add(schema: IApplicableSchema): void; + merge(other: ISchemaCollector): void; + include(node: ASTNode): boolean; + newSub(): ISchemaCollector; } class SchemaCollector implements ISchemaCollector { - schemas: IApplicableSchema[] = []; - constructor(private focusOffset = -1, private exclude: ASTNode = null) { - } - add(schema: IApplicableSchema) { - this.schemas.push(schema); - } - merge(other: ISchemaCollector) { - this.schemas.push(...other.schemas); - } - include(node: ASTNode) { - return (this.focusOffset === -1 || node.contains(this.focusOffset)) && (node !== this.exclude); - } - newSub(): ISchemaCollector { - return new SchemaCollector(-1, this.exclude); - } + schemas: IApplicableSchema[] = []; + constructor(private focusOffset = -1, private exclude: ASTNode = null) { + } + add(schema: IApplicableSchema) { + this.schemas.push(schema); + } + merge(other: ISchemaCollector) { + this.schemas.push(...other.schemas); + } + include(node: ASTNode) { + return (this.focusOffset === -1 || contains(node, this.focusOffset)) && (node !== this.exclude); + } + newSub(): ISchemaCollector { + return new SchemaCollector(-1, this.exclude); + } } class NoOpSchemaCollector implements ISchemaCollector { - get schemas() { return []; } - add(schema: IApplicableSchema) { } - merge(other: ISchemaCollector) { } - include(node: ASTNode) { return true; } - newSub(): ISchemaCollector { return this; } + private constructor() { } + get schemas() { return []; } + add(schema: IApplicableSchema) { } + merge(other: ISchemaCollector) { } + include(node: ASTNode) { return true; } + newSub(): ISchemaCollector { return this; } + + static instance = new NoOpSchemaCollector(); } export class ValidationResult { - public problems: IProblem[]; + public problems: IProblem[]; - public propertiesMatches: number; - public propertiesValueMatches: number; - public primaryValueMatches: number; - public enumValueMatch: boolean; - public enumValues: any[]; - public warnings; - public errors; + public propertiesMatches: number; + public propertiesValueMatches: number; + public primaryValueMatches: number; + public enumValueMatch: boolean; + public enumValues: any[]; - constructor() { - this.problems = []; - this.propertiesMatches = 0; - this.propertiesValueMatches = 0; - this.primaryValueMatches = 0; - this.enumValueMatch = false; - this.enumValues = null; - this.warnings = []; - this.errors = []; - } + constructor() { + this.problems = []; + this.propertiesMatches = 0; + this.propertiesValueMatches = 0; + this.primaryValueMatches = 0; + this.enumValueMatch = false; + this.enumValues = null; + } - public hasProblems(): boolean { - return !!this.problems.length; - } + public hasProblems(): boolean { + return !!this.problems.length; + } - public mergeAll(validationResults: ValidationResult[]): void { - validationResults.forEach((validationResult) => { - this.merge(validationResult); - }); - } + public mergeAll(validationResults: ValidationResult[]): void { + for (const validationResult of validationResults) { + this.merge(validationResult); + } + } - public merge(validationResult: ValidationResult): void { - this.problems = this.problems.concat(validationResult.problems); - } + public merge(validationResult: ValidationResult): void { + this.problems = this.problems.concat(validationResult.problems); + } - public mergeEnumValues(validationResult: ValidationResult): void { - if (!this.enumValueMatch && !validationResult.enumValueMatch && this.enumValues && validationResult.enumValues) { - this.enumValues = this.enumValues.concat(validationResult.enumValues); - for (let error of this.problems) { - if (error.code === ErrorCode.EnumValueMismatch) { - error.message = localize('enumWarning', 'Value is not accepted. Valid values: {0}.', this.enumValues.map(v => JSON.stringify(v)).join(', ')); - } - } - } - } + public mergeEnumValues(validationResult: ValidationResult): void { + if (!this.enumValueMatch && !validationResult.enumValueMatch && this.enumValues && validationResult.enumValues) { + this.enumValues = this.enumValues.concat(validationResult.enumValues); + for (let error of this.problems) { + if (error.code === ErrorCode.EnumValueMismatch) { + error.message = localize('enumWarning', 'Value is not accepted. Valid values: {0}.', this.enumValues.map(v => JSON.stringify(v)).join(', ')); + } + } + } + } - public mergePropertyMatch(propertyValidationResult: ValidationResult): void { - this.merge(propertyValidationResult); - this.propertiesMatches++; - if (propertyValidationResult.enumValueMatch || !this.hasProblems() && propertyValidationResult.propertiesMatches) { - this.propertiesValueMatches++; - } - if (propertyValidationResult.enumValueMatch && propertyValidationResult.enumValues && propertyValidationResult.enumValues.length === 1) { - this.primaryValueMatches++; - } - } + public mergePropertyMatch(propertyValidationResult: ValidationResult): void { + this.merge(propertyValidationResult); + this.propertiesMatches++; + if (propertyValidationResult.enumValueMatch || !propertyValidationResult.hasProblems() && propertyValidationResult.propertiesMatches) { + this.propertiesValueMatches++; + } + if (propertyValidationResult.enumValueMatch && propertyValidationResult.enumValues && propertyValidationResult.enumValues.length === 1) { + this.primaryValueMatches++; + } + } - public compareGeneric(other: ValidationResult): number { - let hasProblems = this.hasProblems(); - if (hasProblems !== other.hasProblems()) { - return hasProblems ? -1 : 1; - } - if (this.enumValueMatch !== other.enumValueMatch) { - return other.enumValueMatch ? -1 : 1; - } - if (this.propertiesValueMatches !== other.propertiesValueMatches) { - return this.propertiesValueMatches - other.propertiesValueMatches; - } - if (this.primaryValueMatches !== other.primaryValueMatches) { - return this.primaryValueMatches - other.primaryValueMatches; - } - return this.propertiesMatches - other.propertiesMatches; - } + public compare(other: ValidationResult): number { + let hasProblems = this.hasProblems(); + if (hasProblems !== other.hasProblems()) { + return hasProblems ? -1 : 1; + } + if (this.enumValueMatch !== other.enumValueMatch) { + return other.enumValueMatch ? -1 : 1; + } + if (this.primaryValueMatches !== other.primaryValueMatches) { + return this.primaryValueMatches - other.primaryValueMatches; + } + if (this.propertiesValueMatches !== other.propertiesValueMatches) { + return this.propertiesValueMatches - other.propertiesValueMatches; + } + return this.propertiesMatches - other.propertiesMatches; + } - public compareKubernetes(other: ValidationResult): number { - let hasProblems = this.hasProblems(); - if(this.propertiesMatches !== other.propertiesMatches){ - return this.propertiesMatches - other.propertiesMatches; - } - if (this.enumValueMatch !== other.enumValueMatch) { - return other.enumValueMatch ? -1 : 1; - } - if (this.primaryValueMatches !== other.primaryValueMatches) { - return this.primaryValueMatches - other.primaryValueMatches; - } - if (this.propertiesValueMatches !== other.propertiesValueMatches) { - return this.propertiesValueMatches - other.propertiesValueMatches; - } - if (hasProblems !== other.hasProblems()) { - return hasProblems ? -1 : 1; - } - return this.propertiesMatches - other.propertiesMatches; - } +} +export function newJSONDocument(root: ASTNode, diagnostics: Diagnostic[] = []) { + return new JSONDocument(root, diagnostics, []); +} + +export function getNodeValue(node: ASTNode): any { + return Json.getNodeValue(node); +} + +export function getNodePath(node: ASTNode): JSONPath { + return Json.getNodePath(node); +} + +export function contains(node: ASTNode, offset: number, includeRightBound = false): boolean { + return offset >= node.offset && offset < (node.offset + node.length) || includeRightBound && offset === (node.offset + node.length); } export class JSONDocument { - constructor(public readonly root: ASTNode, public readonly syntaxErrors: IProblem[]) { - } + constructor(public root: ASTNode, public readonly syntaxErrors: Diagnostic[] = [], public readonly comments: Range[] = []) { + } - public getNodeFromOffset(offset: number): ASTNode { - return this.root && this.root.getNodeFromOffset(offset); - } + public getNodeFromOffset(offset: number, includeRightBound = false): ASTNode | undefined { + if (this.root) { + return Json.findNodeAtOffset(this.root, offset, includeRightBound); + } + return void 0; + } - public getNodeFromOffsetEndInclusive(offset: number): ASTNode { - return this.root && this.root.getNodeFromOffsetEndInclusive(offset); - } + public visit(visitor: (node: ASTNode) => boolean): void { + if (this.root) { + let doVisit = (node: ASTNode): boolean => { + let ctn = visitor(node); + let children = node.children; + if (Array.isArray(children)) { + for (let i = 0; i < children.length && ctn; i++) { + ctn = doVisit(children[i]); + } + } + return ctn; + }; + doVisit(this.root); + } + } - public visit(visitor: (node: ASTNode) => boolean): void { - if (this.root) { - this.root.visit(visitor); - } - } + public validate(textDocument: TextDocument, schema: JSONSchema): Diagnostic[] { + if (this.root && schema) { + let validationResult = new ValidationResult(); + validate(this.root, schema, validationResult, NoOpSchemaCollector.instance); + return validationResult.problems.map(p => { + let range = Range.create(textDocument.positionAt(p.location.offset), textDocument.positionAt(p.location.offset + p.location.length)); + return Diagnostic.create(range, p.message, p.severity, p.code); + }); + } + return null; + } - public configureSettings(parserSettings: LanguageSettings){ - if(this.root) { - this.root.setParserSettings(parserSettings); - } - } - - public validate(schema: JSONSchema): IProblem[] { - if (this.root && schema) { - let validationResult = new ValidationResult(); - this.root.validate(schema, validationResult, new NoOpSchemaCollector()); - return validationResult.problems; - } - return null; - } - - public getMatchingSchemas(schema: JSONSchema, focusOffset: number = -1, exclude: ASTNode = null): IApplicableSchema[] { - let matchingSchemas = new SchemaCollector(focusOffset, exclude); - let validationResult = new ValidationResult(); - if (this.root && schema) { - this.root.validate(schema, validationResult, matchingSchemas); - } - return matchingSchemas.schemas; - } - - public getValidationProblems(schema: JSONSchema, focusOffset: number = -1, exclude: ASTNode = null) { - let matchingSchemas = new SchemaCollector(focusOffset, exclude); - let validationResult = new ValidationResult(); - if (this.root && schema) { - this.root.validate(schema, validationResult, matchingSchemas); - } - return validationResult.problems; - } + public getMatchingSchemas(schema: JSONSchema, focusOffset: number = -1, exclude: ASTNode = null): IApplicableSchema[] { + let matchingSchemas = new SchemaCollector(focusOffset, exclude); + if (this.root && schema) { + validate(this.root, schema, new ValidationResult(), matchingSchemas); + } + return matchingSchemas.schemas; + } } -//Alternative comparison is specifically used by the kubernetes/openshift schema but may lead to better results then genericComparison depending on the schema -function alternativeComparison(subValidationResult, bestMatch, subSchema, subMatchingSchemas){ - let compareResult = subValidationResult.compareKubernetes(bestMatch.validationResult); - if (compareResult > 0) { - // our node is the best matching so far - bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas }; - } else if (compareResult === 0) { - // there's already a best matching but we are as good - bestMatch.matchingSchemas.merge(subMatchingSchemas); - bestMatch.validationResult.mergeEnumValues(subValidationResult); - } - return bestMatch; +function validate(node: ASTNode, schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector) { + + if (!node || !matchingSchemas.include(node)) { + return; + } + + switch (node.type) { + case 'object': + _validateObjectNode(node, schema, validationResult, matchingSchemas); + break; + case 'array': + _validateArrayNode(node, schema, validationResult, matchingSchemas); + break; + case 'string': + _validateStringNode(node, schema, validationResult, matchingSchemas); + break; + case 'number': + _validateNumberNode(node, schema, validationResult, matchingSchemas); + break; + case 'property': + return validate(node.valueNode, schema, validationResult, matchingSchemas); + } + _validateNode(); + + matchingSchemas.add({ node: node, schema: schema }); + + function _validateNode() { + + function matchesType(type: string) { + return node.type === type || (type === 'integer' && node.type === 'number' && node.isInteger); + } + + if (Array.isArray(schema.type)) { + if (!schema.type.some(matchesType)) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: schema.errorMessage || localize('typeArrayMismatchWarning', 'Incorrect type. Expected one of {0}.', (schema.type).join(', ')) + }); + } + } + else if (schema.type) { + if (!matchesType(schema.type)) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: schema.errorMessage || localize('typeMismatchWarning', 'Incorrect type. Expected "{0}".', schema.type) + }); + } + } + if (Array.isArray(schema.allOf)) { + for (const subSchemaRef of schema.allOf) { + validate(node, asSchema(subSchemaRef), validationResult, matchingSchemas); + } + } + let notSchema = asSchema(schema.not); + if (notSchema) { + let subValidationResult = new ValidationResult(); + let subMatchingSchemas = matchingSchemas.newSub(); + validate(node, notSchema, subValidationResult, subMatchingSchemas); + if (!subValidationResult.hasProblems()) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: localize('notSchemaWarning', "Matches a schema that is not allowed.") + }); + } + for (const ms of subMatchingSchemas.schemas) { + ms.inverted = !ms.inverted; + matchingSchemas.add(ms); + } + } + + let testAlternatives = (alternatives: JSONSchemaRef[], maxOneMatch: boolean) => { + let matches = []; + + // remember the best match that is used for error messages + let bestMatch: { schema: JSONSchema; validationResult: ValidationResult; matchingSchemas: ISchemaCollector; } = null; + for (const subSchemaRef of alternatives) { + let subSchema = asSchema(subSchemaRef); + let subValidationResult = new ValidationResult(); + let subMatchingSchemas = matchingSchemas.newSub(); + validate(node, subSchema, subValidationResult, subMatchingSchemas); + if (!subValidationResult.hasProblems()) { + matches.push(subSchema); + } + if (!bestMatch) { + bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas }; + } else { + if (!maxOneMatch && !subValidationResult.hasProblems() && !bestMatch.validationResult.hasProblems()) { + // no errors, both are equally good matches + bestMatch.matchingSchemas.merge(subMatchingSchemas); + bestMatch.validationResult.propertiesMatches += subValidationResult.propertiesMatches; + bestMatch.validationResult.propertiesValueMatches += subValidationResult.propertiesValueMatches; + } else { + let compareResult = subValidationResult.compare(bestMatch.validationResult); + if (compareResult > 0) { + // our node is the best matching so far + bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas }; + } else if (compareResult === 0) { + // there's already a best matching but we are as good + bestMatch.matchingSchemas.merge(subMatchingSchemas); + bestMatch.validationResult.mergeEnumValues(subValidationResult); + } + } + } + } + + if (matches.length > 1 && maxOneMatch) { + validationResult.problems.push({ + location: { offset: node.offset, length: 1 }, + severity: DiagnosticSeverity.Warning, + message: localize('oneOfWarning', "Matches multiple schemas when only one must validate.") + }); + } + if (bestMatch !== null) { + validationResult.merge(bestMatch.validationResult); + validationResult.propertiesMatches += bestMatch.validationResult.propertiesMatches; + validationResult.propertiesValueMatches += bestMatch.validationResult.propertiesValueMatches; + matchingSchemas.merge(bestMatch.matchingSchemas); + } + return matches.length; + }; + if (Array.isArray(schema.anyOf)) { + testAlternatives(schema.anyOf, false); + } + if (Array.isArray(schema.oneOf)) { + testAlternatives(schema.oneOf, true); + } + + let testBranch = (schema: JSONSchemaRef) => { + let subValidationResult = new ValidationResult(); + let subMatchingSchemas = matchingSchemas.newSub(); + + validate(node, asSchema(schema), subValidationResult, subMatchingSchemas); + + validationResult.merge(subValidationResult); + validationResult.propertiesMatches += subValidationResult.propertiesMatches; + validationResult.propertiesValueMatches += subValidationResult.propertiesValueMatches; + matchingSchemas.merge(subMatchingSchemas); + }; + + let testCondition = (ifSchema: JSONSchemaRef, thenSchema?: JSONSchemaRef, elseSchema?: JSONSchemaRef) => { + let subSchema = asSchema(ifSchema); + let subValidationResult = new ValidationResult(); + let subMatchingSchemas = matchingSchemas.newSub(); + + validate(node, subSchema, subValidationResult, subMatchingSchemas); + matchingSchemas.merge(subMatchingSchemas); + + if (!subValidationResult.hasProblems()) { + if (thenSchema) { + testBranch(thenSchema); + } + } else if (elseSchema) { + testBranch(elseSchema); + } + }; + + let ifSchema = asSchema(schema.if); + if (ifSchema) { + testCondition(ifSchema, asSchema(schema.then), asSchema(schema.else)); + } + + if (Array.isArray(schema.enum)) { + let val = getNodeValue(node); + let enumValueMatch = false; + for (let e of schema.enum) { + if (equals(val, e)) { + enumValueMatch = true; + break; + } + } + validationResult.enumValues = schema.enum; + validationResult.enumValueMatch = enumValueMatch; + if (!enumValueMatch) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + code: ErrorCode.EnumValueMismatch, + message: schema.errorMessage || localize('enumWarning', 'Value is not accepted. Valid values: {0}.', schema.enum.map(v => JSON.stringify(v)).join(', ')) + }); + } + } + + if (isDefined(schema.const)) { + let val = getNodeValue(node); + if (!equals(val, schema.const)) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + code: ErrorCode.EnumValueMismatch, + message: schema.errorMessage || localize('constWarning', 'Value must be {0}.', JSON.stringify(schema.const)) + }); + validationResult.enumValueMatch = false; + } else { + validationResult.enumValueMatch = true; + } + validationResult.enumValues = [schema.const]; + } + + if (schema.deprecationMessage && node.parent) { + validationResult.problems.push({ + location: { offset: node.parent.offset, length: node.parent.length }, + severity: DiagnosticSeverity.Warning, + message: schema.deprecationMessage + }); + } + } + + + + function _validateNumberNode(node: NumberASTNode, schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { + let val = node.value; + + if (isNumber(schema.multipleOf)) { + if (val % schema.multipleOf !== 0) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: localize('multipleOfWarning', 'Value is not divisible by {0}.', schema.multipleOf) + }); + } + } + function getExclusiveLimit(limit: number | undefined, exclusive: boolean | number | undefined): number | undefined { + if (isNumber(exclusive)) { + return exclusive; + } + if (isBoolean(exclusive) && exclusive) { + return limit; + } + return void 0; + } + function getLimit(limit: number | undefined, exclusive: boolean | number | undefined): number | undefined { + if (!isBoolean(exclusive) || !exclusive) { + return limit; + } + return void 0; + } + let exclusiveMinimum = getExclusiveLimit(schema.minimum, schema.exclusiveMinimum); + if (isNumber(exclusiveMinimum) && val <= exclusiveMinimum) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: localize('exclusiveMinimumWarning', 'Value is below the exclusive minimum of {0}.', exclusiveMinimum) + }); + } + let exclusiveMaximum = getExclusiveLimit(schema.maximum, schema.exclusiveMaximum); + if (isNumber(exclusiveMaximum) && val >= exclusiveMaximum) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: localize('exclusiveMaximumWarning', 'Value is above the exclusive maximum of {0}.', exclusiveMaximum) + }); + } + let minimum = getLimit(schema.minimum, schema.exclusiveMinimum); + if (isNumber(minimum) && val < minimum) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: localize('minimumWarning', 'Value is below the minimum of {0}.', minimum) + }); + } + let maximum = getLimit(schema.maximum, schema.exclusiveMaximum); + if (isNumber(maximum) && val > maximum) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: localize('maximumWarning', 'Value is above the maximum of {0}.', maximum) + }); + } + } + + function _validateStringNode(node: StringASTNode, schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { + if (isNumber(schema.minLength) && node.value.length < schema.minLength) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: localize('minLengthWarning', 'String is shorter than the minimum length of {0}.', schema.minLength) + }); + } + + if (isNumber(schema.maxLength) && node.value.length > schema.maxLength) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: localize('maxLengthWarning', 'String is longer than the maximum length of {0}.', schema.maxLength) + }); + } + + if (isString(schema.pattern)) { + let regex = new RegExp(schema.pattern); + if (!regex.test(node.value)) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: schema.patternErrorMessage || schema.errorMessage || localize('patternWarning', 'String does not match the pattern of "{0}".', schema.pattern) + }); + } + } + + if (schema.format) { + switch (schema.format) { + case 'uri': + case 'uri-reference': { + let errorMessage; + if (!node.value) { + errorMessage = localize('uriEmpty', 'URI expected.'); + } else { + try { + let uri = Uri.parse(node.value); + if (!uri.scheme && schema.format === 'uri') { + errorMessage = localize('uriSchemeMissing', 'URI with a scheme is expected.'); + } + } catch (e) { + errorMessage = e.message; + } + } + if (errorMessage) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: schema.patternErrorMessage || schema.errorMessage || localize('uriFormatWarning', 'String is not a URI: {0}', errorMessage) + }); + } + } + break; + case 'email': { + if (!node.value.match(emailPattern)) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: schema.patternErrorMessage || schema.errorMessage || localize('emailFormatWarning', 'String is not an e-mail address.') + }); + } + } + break; + case 'color-hex': { + if (!node.value.match(colorHexPattern)) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: schema.patternErrorMessage || schema.errorMessage || localize('colorHexFormatWarning', 'Invalid color format. Use #RGB, #RGBA, #RRGGBB or #RRGGBBAA.') + }); + } + } + break; + default: + } + } + + } + function _validateArrayNode(node: ArrayASTNode, schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { + if (Array.isArray(schema.items)) { + let subSchemas = schema.items; + for (let index = 0; index < subSchemas.length; index++) { + const subSchemaRef = subSchemas[index]; + let subSchema = asSchema(subSchemaRef); + let itemValidationResult = new ValidationResult(); + let item = node.items[index]; + if (item) { + validate(item, subSchema, itemValidationResult, matchingSchemas); + validationResult.mergePropertyMatch(itemValidationResult); + } else if (node.items.length >= subSchemas.length) { + validationResult.propertiesValueMatches++; + } + } + if (node.items.length > subSchemas.length) { + if (typeof schema.additionalItems === 'object') { + for (let i = subSchemas.length; i < node.items.length; i++) { + let itemValidationResult = new ValidationResult(); + validate(node.items[i], schema.additionalItems, itemValidationResult, matchingSchemas); + validationResult.mergePropertyMatch(itemValidationResult); + } + } else if (schema.additionalItems === false) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: localize('additionalItemsWarning', 'Array has too many items according to schema. Expected {0} or fewer.', subSchemas.length) + }); + } + } + } else { + let itemSchema = asSchema(schema.items); + if (itemSchema) { + for (const item of node.items) { + let itemValidationResult = new ValidationResult(); + validate(item, itemSchema, itemValidationResult, matchingSchemas); + validationResult.mergePropertyMatch(itemValidationResult); + } + } + } + + let containsSchema = asSchema(schema.contains); + if (containsSchema) { + let doesContain = node.items.some(item => { + let itemValidationResult = new ValidationResult(); + validate(item, containsSchema, itemValidationResult, NoOpSchemaCollector.instance); + return !itemValidationResult.hasProblems(); + }); + + if (!doesContain) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: schema.errorMessage || localize('requiredItemMissingWarning', 'Array does not contain required item.') + }); + } + } + + if (isNumber(schema.minItems) && node.items.length < schema.minItems) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: localize('minItemsWarning', 'Array has too few items. Expected {0} or more.', schema.minItems) + }); + } + + if (isNumber(schema.maxItems) && node.items.length > schema.maxItems) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: localize('maxItemsWarning', 'Array has too many items. Expected {0} or fewer.', schema.maxItems) + }); + } + + if (schema.uniqueItems === true) { + let values = getNodeValue(node); + let duplicates = values.some((value, index) => { + return index !== values.lastIndexOf(value); + }); + if (duplicates) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: localize('uniqueItemsWarning', 'Array has duplicate items.') + }); + } + } + + } + + function _validateObjectNode(node: ObjectASTNode, schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { + let seenKeys: { [key: string]: ASTNode } = Object.create(null); + let unprocessedProperties: string[] = []; + for (const propertyNode of node.properties) { + let key = propertyNode.keyNode.value; + + // TODO: see https://github.com/redhat-developer/vscode-yaml/issues/60 + // Replace the merge key with the actual values of what the node value points to in seen keys + if (key === "<<" && propertyNode.valueNode) { + switch (propertyNode.valueNode.type) { + case "object": { + propertyNode.value["properties"].forEach(propASTNode => { + let propKey = propASTNode.key.value; + seenKeys[propKey] = propASTNode.value; + unprocessedProperties.push(propKey); + }); + break; + } + case "array": { + propertyNode.value["items"].forEach(sequenceNode => { + sequenceNode["properties"].forEach(propASTNode => { + let seqKey = propASTNode.key.value; + seenKeys[seqKey] = propASTNode.value; + unprocessedProperties.push(seqKey); + }); + }); + break; + } + default: { + break; + } + } + } else { + seenKeys[key] = propertyNode.valueNode; + unprocessedProperties.push(key); + } + } + + if (Array.isArray(schema.required)) { + for (const propertyName of schema.required) { + if (!seenKeys[propertyName]) { + let keyNode = node.parent && node.parent.type === 'property' && node.parent.keyNode; + let location = keyNode ? { offset: keyNode.offset, length: keyNode.length } : { offset: node.offset, length: 1 }; + validationResult.problems.push({ + location: location, + severity: DiagnosticSeverity.Warning, + message: localize('MissingRequiredPropWarning', 'Missing property "{0}".', propertyName) + }); + } + } + } + + let propertyProcessed = (prop: string) => { + let index = unprocessedProperties.indexOf(prop); + while (index >= 0) { + unprocessedProperties.splice(index, 1); + index = unprocessedProperties.indexOf(prop); + } + }; + + if (schema.properties) { + for (const propertyName of Object.keys(schema.properties)) { + propertyProcessed(propertyName); + let propertySchema = schema.properties[propertyName]; + let child = seenKeys[propertyName]; + if (child) { + if (isBoolean(propertySchema)) { + if (!propertySchema) { + let propertyNode = child.parent; + validationResult.problems.push({ + location: { offset: propertyNode.keyNode.offset, length: propertyNode.keyNode.length }, + severity: DiagnosticSeverity.Warning, + message: schema.errorMessage || localize('DisallowedExtraPropWarning', 'Property {0} is not allowed.', propertyName) + }); + } else { + validationResult.propertiesMatches++; + validationResult.propertiesValueMatches++; + } + } else { + let propertyValidationResult = new ValidationResult(); + validate(child, propertySchema, propertyValidationResult, matchingSchemas); + validationResult.mergePropertyMatch(propertyValidationResult); + } + } + + } + } + + if (schema.patternProperties) { + for (const propertyPattern of Object.keys(schema.patternProperties)) { + let regex = new RegExp(propertyPattern); + for (const propertyName of unprocessedProperties.slice(0)) { + if (regex.test(propertyName)) { + propertyProcessed(propertyName); + let child = seenKeys[propertyName]; + if (child) { + let propertySchema = schema.patternProperties[propertyPattern]; + if (isBoolean(propertySchema)) { + if (!propertySchema) { + let propertyNode = child.parent; + validationResult.problems.push({ + location: { offset: propertyNode.keyNode.offset, length: propertyNode.keyNode.length }, + severity: DiagnosticSeverity.Warning, + message: schema.errorMessage || localize('DisallowedExtraPropWarning', 'Property {0} is not allowed.', propertyName) + }); + } else { + validationResult.propertiesMatches++; + validationResult.propertiesValueMatches++; + } + } else { + let propertyValidationResult = new ValidationResult(); + validate(child, propertySchema, propertyValidationResult, matchingSchemas); + validationResult.mergePropertyMatch(propertyValidationResult); + } + } + } + } + } + } + + if (typeof schema.additionalProperties === 'object') { + for (const propertyName of unprocessedProperties) { + let child = seenKeys[propertyName]; + if (child) { + let propertyValidationResult = new ValidationResult(); + validate(child, schema.additionalProperties, propertyValidationResult, matchingSchemas); + validationResult.mergePropertyMatch(propertyValidationResult); + } + } + } else if (schema.additionalProperties === false) { + if (unprocessedProperties.length > 0) { + for (const propertyName of unprocessedProperties) { + let child = seenKeys[propertyName]; + if (child) { + let propertyNode = child.parent; + + validationResult.problems.push({ + location: { offset: propertyNode.keyNode.offset, length: propertyNode.keyNode.length }, + severity: DiagnosticSeverity.Warning, + message: schema.errorMessage || localize('DisallowedExtraPropWarning', 'Property {0} is not allowed.', propertyName) + }); + } + } + } + } + + if (isNumber(schema.maxProperties)) { + if (node.properties.length > schema.maxProperties) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: localize('MaxPropWarning', 'Object has more properties than limit of {0}.', schema.maxProperties) + }); + } + } + + if (isNumber(schema.minProperties)) { + if (node.properties.length < schema.minProperties) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: localize('MinPropWarning', 'Object has fewer properties than the required number of {0}', schema.minProperties) + }); + } + } + + if (schema.dependencies) { + for (const key of Object.keys(schema.dependencies)) { + let prop = seenKeys[key]; + if (prop) { + let propertyDep = schema.dependencies[key]; + if (Array.isArray(propertyDep)) { + for (const requiredProp of propertyDep) { + if (!seenKeys[requiredProp]) { + validationResult.problems.push({ + location: { offset: node.offset, length: node.length }, + severity: DiagnosticSeverity.Warning, + message: localize('RequiredDependentPropWarning', 'Object is missing property {0} required by property {1}.', requiredProp, key) + }); + } else { + validationResult.propertiesValueMatches++; + } + } + } else { + let propertySchema = asSchema(propertyDep); + if (propertySchema) { + let propertyValidationResult = new ValidationResult(); + validate(node, propertySchema, propertyValidationResult, matchingSchemas); + validationResult.mergePropertyMatch(propertyValidationResult); + } + } + } + } + } + + let propertyNames = asSchema(schema.propertyNames); + if (propertyNames) { + for (const f of node.properties) { + let key = f.keyNode; + if (key) { + validate(key, propertyNames, validationResult, NoOpSchemaCollector.instance); + } + } + } + } } -//genericComparison tries to find the best matching schema using a generic comparison -function genericComparison(maxOneMatch, subValidationResult, bestMatch, subSchema, subMatchingSchemas){ - if (!maxOneMatch && !subValidationResult.hasProblems() && !bestMatch.validationResult.hasProblems()) { - // no errors, both are equally good matches - bestMatch.matchingSchemas.merge(subMatchingSchemas); - bestMatch.validationResult.propertiesMatches += subValidationResult.propertiesMatches; - bestMatch.validationResult.propertiesValueMatches += subValidationResult.propertiesValueMatches; - } else { - let compareResult = subValidationResult.compareGeneric(bestMatch.validationResult); - if (compareResult > 0) { - // our node is the best matching so far - bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas }; - } else if (compareResult === 0) { - // there's already a best matching but we are as good - bestMatch.matchingSchemas.merge(subMatchingSchemas); - bestMatch.validationResult.mergeEnumValues(subValidationResult); - } - } - return bestMatch; -} \ No newline at end of file + +export function parse(textDocument: TextDocument, config?: JSONDocumentConfig): JSONDocument { + + let problems: Diagnostic[] = []; + let lastProblemOffset = -1; + let text = textDocument.getText(); + let scanner = Json.createScanner(text, false); + + let commentRanges: Range[] = config && config.collectComments ? [] : void 0; + + function _scanNext(): Json.SyntaxKind { + while (true) { + let token = scanner.scan(); + _checkScanError(); + switch (token) { + case Json.SyntaxKind.LineCommentTrivia: + case Json.SyntaxKind.BlockCommentTrivia: + if (Array.isArray(commentRanges)) { + commentRanges.push(Range.create(textDocument.positionAt(scanner.getTokenOffset()), textDocument.positionAt(scanner.getTokenOffset() + scanner.getTokenLength()))); + } + break; + case Json.SyntaxKind.Trivia: + case Json.SyntaxKind.LineBreakTrivia: + break; + default: + return token; + } + } + } + + function _accept(token: Json.SyntaxKind): boolean { + if (scanner.getToken() === token) { + _scanNext(); + return true; + } + return false; + } + + function _errorAtRange(message: string, code: ErrorCode, startOffset: number, endOffset: number, severity: DiagnosticSeverity = DiagnosticSeverity.Error): void { + + if (problems.length === 0 || startOffset !== lastProblemOffset) { + let range = Range.create(textDocument.positionAt(startOffset), textDocument.positionAt(endOffset)); + problems.push(Diagnostic.create(range, message, severity, code, textDocument.languageId)); + lastProblemOffset = startOffset; + } + } + + function _error(message: string, code: ErrorCode, node: T = null, skipUntilAfter: Json.SyntaxKind[] = [], skipUntil: Json.SyntaxKind[] = []): T { + let start = scanner.getTokenOffset(); + let end = scanner.getTokenOffset() + scanner.getTokenLength(); + if (start === end && start > 0) { + start--; + while (start > 0 && /\s/.test(text.charAt(start))) { + start--; + } + end = start + 1; + } + _errorAtRange(message, code, start, end); + + if (node) { + _finalize(node, false); + } + if (skipUntilAfter.length + skipUntil.length > 0) { + let token = scanner.getToken(); + while (token !== Json.SyntaxKind.EOF) { + if (skipUntilAfter.indexOf(token) !== -1) { + _scanNext(); + break; + } else if (skipUntil.indexOf(token) !== -1) { + break; + } + token = _scanNext(); + } + } + return node; + } + + function _checkScanError(): boolean { + switch (scanner.getTokenError()) { + case Json.ScanError.InvalidUnicode: + _error(localize('InvalidUnicode', 'Invalid unicode sequence in string.'), ErrorCode.InvalidUnicode); + return true; + case Json.ScanError.InvalidEscapeCharacter: + _error(localize('InvalidEscapeCharacter', 'Invalid escape character in string.'), ErrorCode.InvalidEscapeCharacter); + return true; + case Json.ScanError.UnexpectedEndOfNumber: + _error(localize('UnexpectedEndOfNumber', 'Unexpected end of number.'), ErrorCode.UnexpectedEndOfNumber); + return true; + case Json.ScanError.UnexpectedEndOfComment: + _error(localize('UnexpectedEndOfComment', 'Unexpected end of comment.'), ErrorCode.UnexpectedEndOfComment); + return true; + case Json.ScanError.UnexpectedEndOfString: + _error(localize('UnexpectedEndOfString', 'Unexpected end of string.'), ErrorCode.UnexpectedEndOfString); + return true; + case Json.ScanError.InvalidCharacter: + _error(localize('InvalidCharacter', 'Invalid characters in string. Control characters must be escaped.'), ErrorCode.InvalidCharacter); + return true; + } + return false; + } + + function _finalize(node: T, scanNext: boolean): T { + node.length = scanner.getTokenOffset() + scanner.getTokenLength() - node.offset; + + if (scanNext) { + _scanNext(); + } + + return node; + } + + function _parseArray(parent: ASTNode): ArrayASTNode { + if (scanner.getToken() !== Json.SyntaxKind.OpenBracketToken) { + return null; + } + let node = new ArrayASTNodeImpl(parent, scanner.getTokenOffset()); + _scanNext(); // consume OpenBracketToken + + let count = 0; + let needsComma = false; + while (scanner.getToken() !== Json.SyntaxKind.CloseBracketToken && scanner.getToken() !== Json.SyntaxKind.EOF) { + if (scanner.getToken() === Json.SyntaxKind.CommaToken) { + if (!needsComma) { + _error(localize('ValueExpected', 'Value expected'), ErrorCode.ValueExpected); + } + let commaOffset = scanner.getTokenOffset(); + _scanNext(); // consume comma + if (scanner.getToken() === Json.SyntaxKind.CloseBracketToken) { + if (needsComma) { + _errorAtRange(localize('TrailingComma', 'Trailing comma'), ErrorCode.TrailingComma, commaOffset, commaOffset + 1); + } + continue; + } + } else if (needsComma) { + _error(localize('ExpectedComma', 'Expected comma'), ErrorCode.CommaExpected); + } + let item = _parseValue(node, count++); + if (!item) { + _error(localize('PropertyExpected', 'Value expected'), ErrorCode.ValueExpected, null, [], [Json.SyntaxKind.CloseBracketToken, Json.SyntaxKind.CommaToken]); + } else { + node.items.push(item); + } + needsComma = true; + } + + if (scanner.getToken() !== Json.SyntaxKind.CloseBracketToken) { + return _error(localize('ExpectedCloseBracket', 'Expected comma or closing bracket'), ErrorCode.CommaOrCloseBacketExpected, node); + } + + return _finalize(node, true); + } + + function _parseProperty(parent: ObjectASTNode, keysSeen: { [key: string]: (PropertyASTNode | boolean) }): PropertyASTNode { + + let node = new PropertyASTNodeImpl(parent, scanner.getTokenOffset()); + let key = _parseString(node); + if (!key) { + if (scanner.getToken() === Json.SyntaxKind.Unknown) { + // give a more helpful error message + _error(localize('DoubleQuotesExpected', 'Property keys must be doublequoted'), ErrorCode.Undefined); + let keyNode = new StringASTNodeImpl(node, scanner.getTokenOffset(), scanner.getTokenLength()); + keyNode.value = scanner.getTokenValue(); + key = keyNode; + _scanNext(); // consume Unknown + } else { + return null; + } + } + node.keyNode = key; + + let seen = keysSeen[key.value]; + if (seen) { + _errorAtRange(localize('DuplicateKeyWarning', "Duplicate object key"), ErrorCode.DuplicateKey, node.keyNode.offset, node.keyNode.offset + node.keyNode.length, DiagnosticSeverity.Warning); + if (typeof seen === 'object') { + _errorAtRange(localize('DuplicateKeyWarning', "Duplicate object key"), ErrorCode.DuplicateKey, seen.keyNode.offset, seen.keyNode.offset + seen.keyNode.length, DiagnosticSeverity.Warning); + } + keysSeen[key.value] = true; // if the same key is duplicate again, avoid duplicate error reporting + } else { + keysSeen[key.value] = node; + } + + if (scanner.getToken() === Json.SyntaxKind.ColonToken) { + node.colonOffset = scanner.getTokenOffset(); + _scanNext(); // consume ColonToken + } else { + _error(localize('ColonExpected', 'Colon expected'), ErrorCode.ColonExpected); + if (scanner.getToken() === Json.SyntaxKind.StringLiteral && textDocument.positionAt(key.offset + key.length).line < textDocument.positionAt(scanner.getTokenOffset()).line) { + node.length = key.length; + return node; + } + } + let value = _parseValue(node, key.value); + if (!value) { + return _error(localize('ValueExpected', 'Value expected'), ErrorCode.ValueExpected, node, [], [Json.SyntaxKind.CloseBraceToken, Json.SyntaxKind.CommaToken]); + } + node.valueNode = value; + node.length = value.offset + value.length - node.offset; + return node; + } + + function _parseObject(parent: ASTNode): ObjectASTNode { + if (scanner.getToken() !== Json.SyntaxKind.OpenBraceToken) { + return null; + } + let node = new ObjectASTNodeImpl(parent, scanner.getTokenOffset()); + let keysSeen: any = Object.create(null); + _scanNext(); // consume OpenBraceToken + let needsComma = false; + + while (scanner.getToken() !== Json.SyntaxKind.CloseBraceToken && scanner.getToken() !== Json.SyntaxKind.EOF) { + if (scanner.getToken() === Json.SyntaxKind.CommaToken) { + if (!needsComma) { + _error(localize('PropertyExpected', 'Property expected'), ErrorCode.PropertyExpected); + } + let commaOffset = scanner.getTokenOffset(); + _scanNext(); // consume comma + if (scanner.getToken() === Json.SyntaxKind.CloseBraceToken) { + if (needsComma) { + _errorAtRange(localize('TrailingComma', 'Trailing comma'), ErrorCode.TrailingComma, commaOffset, commaOffset + 1); + } + continue; + } + } else if (needsComma) { + _error(localize('ExpectedComma', 'Expected comma'), ErrorCode.CommaExpected); + } + let property = _parseProperty(node, keysSeen); + if (!property) { + _error(localize('PropertyExpected', 'Property expected'), ErrorCode.PropertyExpected, null, [], [Json.SyntaxKind.CloseBraceToken, Json.SyntaxKind.CommaToken]); + } else { + node.properties.push(property); + } + needsComma = true; + } + + if (scanner.getToken() !== Json.SyntaxKind.CloseBraceToken) { + return _error(localize('ExpectedCloseBrace', 'Expected comma or closing brace'), ErrorCode.CommaOrCloseBraceExpected, node); + } + return _finalize(node, true); + } + + function _parseString(parent: ASTNode): StringASTNode { + if (scanner.getToken() !== Json.SyntaxKind.StringLiteral) { + return null; + } + + let node = new StringASTNodeImpl(parent, scanner.getTokenOffset()); + node.value = scanner.getTokenValue(); + + return _finalize(node, true); + } + + function _parseNumber(parent: ASTNode): NumberASTNode { + if (scanner.getToken() !== Json.SyntaxKind.NumericLiteral) { + return null; + } + + let node = new NumberASTNodeImpl(parent, scanner.getTokenOffset()); + if (scanner.getTokenError() === Json.ScanError.None) { + let tokenValue = scanner.getTokenValue(); + try { + let numberValue = JSON.parse(tokenValue); + if (!isNumber(numberValue)) { + return _error(localize('InvalidNumberFormat', 'Invalid number format.'), ErrorCode.Undefined, node); + } + node.value = numberValue; + } catch (e) { + return _error(localize('InvalidNumberFormat', 'Invalid number format.'), ErrorCode.Undefined, node); + } + node.isInteger = tokenValue.indexOf('.') === -1; + } + return _finalize(node, true); + } + + function _parseLiteral(parent: ASTNode): ASTNode { + let node: ASTNodeImpl; + switch (scanner.getToken()) { + case Json.SyntaxKind.NullKeyword: + return _finalize(new NullASTNodeImpl(parent, scanner.getTokenOffset()), true); + case Json.SyntaxKind.TrueKeyword: + return _finalize(new BooleanASTNodeImpl(parent, true, scanner.getTokenOffset()), true); + case Json.SyntaxKind.FalseKeyword: + return _finalize(new BooleanASTNodeImpl(parent, false, scanner.getTokenOffset()), true); + default: + return null; + } + } + + function _parseValue(parent: ASTNode, name: Json.Segment): ASTNode { + return _parseArray(parent) || _parseObject(parent) || _parseString(parent) || _parseNumber(parent) || _parseLiteral(parent); + } + + let _root = null; + let token = _scanNext(); + if (token !== Json.SyntaxKind.EOF) { + _root = _parseValue(null, null); + if (!_root) { + _error(localize('Invalid symbol', 'Expected a JSON object, array or literal.'), ErrorCode.Undefined); + } else if (scanner.getToken() !== Json.SyntaxKind.EOF) { + _error(localize('End of file expected', 'End of file expected.'), ErrorCode.Undefined); + } + } + return new JSONDocument(_root, problems, commentRanges); +} diff --git a/src/languageservice/parser/yamlParser.ts b/src/languageservice/parser/yamlParser.ts index cc76289..1ddf5e2 100644 --- a/src/languageservice/parser/yamlParser.ts +++ b/src/languageservice/parser/yamlParser.ts @@ -5,270 +5,188 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { ASTNode, ErrorCode, BooleanASTNode, NullASTNode, ArrayASTNode, NumberASTNode, ObjectASTNode, PropertyASTNode, StringASTNode, IApplicableSchema, JSONDocument } from './jsonParser'; - import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); import * as Yaml from '../../yaml-ast-parser/index' -import { Kind } from '../../yaml-ast-parser/index' import { Schema, Type } from 'js-yaml'; -import { getLineStartPositions, getPosition } from '../utils/documentPositionCalculator' +import { getLineStartPositions } from '../utils/documentPositionCalculator' import { parseYamlBoolean } from './scalar-type'; - -export class SingleYAMLDocument extends JSONDocument { - private lines; - public root; - public errors; - public warnings; - - constructor(lines: number[]) { - super(null, []); - this.lines = lines; - this.root = null; - this.errors = []; - this.warnings = []; - } - - public getSchemas(schema, doc, node) { - let matchingSchemas = []; - doc.validate(schema, matchingSchemas, node.start); - return matchingSchemas; - } - - public getNodeFromOffset(offset: number): ASTNode { - return this.getNodeFromOffsetEndInclusive(offset); - } - - private getNodeByIndent = (lines: number[], offset: number, node: ASTNode) => { - - const { line, column: indent } = getPosition(offset, this.lines) - - const children = node.getChildNodes() - - function findNode(children) { - for (var idx = 0; idx < children.length; idx++) { - var child = children[idx]; - - const { line: childLine, column: childCol } = getPosition(child.start, lines); - - if (childCol > indent) { - return null; - } - - const newChildren = child.getChildNodes() - const foundNode = findNode(newChildren) - - if (foundNode) { - return foundNode; - } - - // We have the right indentation, need to return based on line - if (childLine == line) { - return child; - } - if (childLine > line) { - // Get previous - (idx - 1) >= 0 ? children[idx - 1] : child; - } - // Else continue loop to try next element - } - - // Special case, we found the correct - return children[children.length - 1] - } - - return findNode(children) || node - } -} - +import { ObjectASTNodeImpl, StringASTNodeImpl, PropertyASTNodeImpl, NullASTNodeImpl, ArrayASTNodeImpl, BooleanASTNodeImpl, NumberASTNodeImpl } from './jsonParser'; +import { ASTNode, PropertyASTNode, ErrorCode } from '../jsonLanguageTypes'; +import { SingleYAMLDocument, YAMLDocument } from '../yamlLanguageTypes'; function recursivelyBuildAst(parent: ASTNode, node: Yaml.YAMLNode): ASTNode { - if (!node) { - return; - } + if (!node) { + return; + } - switch (node.kind) { - case Yaml.Kind.MAP: { - const instance = node; + switch (node.kind) { + case Yaml.Kind.MAP: { + const instance = node; - const result = new ObjectASTNode(parent, null, node.startPosition, node.endPosition) - result.addProperty + const result = new ObjectASTNodeImpl(parent, node.startPosition, node.endPosition - node.startPosition) - for (const mapping of instance.mappings) { - result.addProperty(recursivelyBuildAst(result, mapping)) - } + for (const mapping of instance.mappings) { + result.properties.push(recursivelyBuildAst(result, mapping)); + } - return result; - } - case Yaml.Kind.MAPPING: { - const instance = node; - const key = instance.key; + return result; + } + case Yaml.Kind.MAPPING: { + const instance = node; + const key = instance.key; - // Technically, this is an arbitrary node in YAML - // I doubt we would get a better string representation by parsing it - const keyNode = new StringASTNode(null, null, true, key.startPosition, key.endPosition); - keyNode.value = key.value; + const result = new PropertyASTNodeImpl(parent, key.startPosition, instance.endPosition - key.startPosition); - const result = new PropertyASTNode(parent, keyNode) - result.end = instance.endPosition - const valueNode = (instance.value) ? recursivelyBuildAst(result, instance.value) : new NullASTNode(parent, key.value, instance.endPosition, instance.endPosition) - valueNode.location = key.value + // Technically, this is an arbitrary node in YAML + // I doubt we would get a better string representation by parsing it + const keyNode = new StringASTNodeImpl(result, key.startPosition, key.endPosition - key.startPosition); + keyNode.value = key.value; - result.setValue(valueNode) + const valueNode = (instance.value) ? recursivelyBuildAst(result, instance.value) : new NullASTNodeImpl(parent, instance.startPosition) - return result; - } - case Yaml.Kind.SEQ: { - const instance = node; + result.keyNode = keyNode; + result.valueNode = valueNode; - const result = new ArrayASTNode(parent, null, instance.startPosition, instance.endPosition); + return result; + } + case Yaml.Kind.SEQ: { + const instance = node; - let count = 0; - for (const item of instance.items) { - if (item === null && count === instance.items.length - 1) { - break; - } + const result = new ArrayASTNodeImpl(parent, instance.startPosition, instance.endPosition - instance.startPosition); - // Be aware of https://github.com/nodeca/js-yaml/issues/321 - // Cannot simply work around it here because we need to know if we are in Flow or Block - var itemNode = (item === null) ? new NullASTNode(parent, null, instance.endPosition, instance.endPosition) : recursivelyBuildAst(result, item); + let count = 0; + for (const item of instance.items) { + if (item === null && count === instance.items.length - 1) { + break; + } - itemNode.location = count++; - result.addItem(itemNode); - } + // Be aware of https://github.com/nodeca/js-yaml/issues/321 + // Cannot simply work around it here because we need to know if we are in Flow or Block + var itemNode = (item === null) ? new NullASTNodeImpl(parent, instance.startPosition, instance.endPosition - instance.startPosition) : recursivelyBuildAst(result, item); - return result; - } - case Yaml.Kind.SCALAR: { - const instance = node; - const type = Yaml.determineScalarType(instance) + result.items.push(itemNode); + } - // The name is set either by the sequence or the mapping case. - const name = null; - const value = instance.value; + return result; + } + case Yaml.Kind.SCALAR: { + const instance = node; + const type = Yaml.determineScalarType(instance) - //This is a patch for redirecting values with these strings to be boolean nodes because its not supported in the parser. - let possibleBooleanValues = ['y', 'Y', 'yes', 'Yes', 'YES', 'n', 'N', 'no', 'No', 'NO', 'on', 'On', 'ON', 'off', 'Off', 'OFF']; - if (instance.plainScalar && possibleBooleanValues.indexOf(value.toString()) !== -1) { - return new BooleanASTNode(parent, name, parseYamlBoolean(value), node.startPosition, node.endPosition) - } + // The name is set either by the sequence or the mapping case. + const name = null; + const value = instance.value; - switch (type) { - case Yaml.ScalarType.null: { - return new StringASTNode(parent, name, false, instance.startPosition, instance.endPosition); - } - case Yaml.ScalarType.bool: { - return new BooleanASTNode(parent, name, Yaml.parseYamlBoolean(value), node.startPosition, node.endPosition) - } - case Yaml.ScalarType.int: { - const result = new NumberASTNode(parent, name, node.startPosition, node.endPosition); - result.value = Yaml.parseYamlInteger(value); - result.isInteger = true; - return result; - } - case Yaml.ScalarType.float: { - const result = new NumberASTNode(parent, name, node.startPosition, node.endPosition); - result.value = Yaml.parseYamlFloat(value); - result.isInteger = false; - return result; - } - case Yaml.ScalarType.string: { - const result = new StringASTNode(parent, name, false, node.startPosition, node.endPosition); - result.value = node.value; - return result; - } - } + //This is a patch for redirecting values with these strings to be boolean nodes because its not supported in the parser. + let possibleBooleanValues = ['y', 'Y', 'yes', 'Yes', 'YES', 'n', 'N', 'no', 'No', 'NO', 'on', 'On', 'ON', 'off', 'Off', 'OFF']; + if (instance.plainScalar && possibleBooleanValues.indexOf(value.toString()) !== -1) { + return new BooleanASTNodeImpl(parent, parseYamlBoolean(value), node.startPosition, node.endPosition - node.startPosition) + } - break; - } - case Yaml.Kind.ANCHOR_REF: { - const instance = (node).value + switch (type) { + case Yaml.ScalarType.null: { + return new NullASTNodeImpl(parent, instance.startPosition, instance.endPosition - instance.startPosition); + } + case Yaml.ScalarType.bool: { + return new BooleanASTNodeImpl(parent, Yaml.parseYamlBoolean(value), node.startPosition, node.endPosition - node.startPosition) + } + case Yaml.ScalarType.int: { + const result = new NumberASTNodeImpl(parent, node.startPosition, node.endPosition - node.startPosition); + result.value = Yaml.parseYamlInteger(value); + result.isInteger = true; + return result; + } + case Yaml.ScalarType.float: { + const result = new NumberASTNodeImpl(parent, node.startPosition, node.endPosition - node.startPosition); + result.value = Yaml.parseYamlFloat(value); + result.isInteger = false; + return result; + } + case Yaml.ScalarType.string: { + const result = new StringASTNodeImpl(parent, node.startPosition, node.endPosition - node.startPosition); + result.value = node.value; + return result; + } + } - return recursivelyBuildAst(parent, instance) || - new NullASTNode(parent, null, node.startPosition, node.endPosition); - } - case Yaml.Kind.INCLUDE_REF: { - const result = new StringASTNode(parent, null, false, node.startPosition, node.endPosition); - result.value = node.value; - return result; - } - } + break; + } + case Yaml.Kind.ANCHOR_REF: { + const instance = (node).value + + return recursivelyBuildAst(parent, instance) || + new NullASTNodeImpl(parent, node.startPosition, node.endPosition - node.startPosition); + } + case Yaml.Kind.INCLUDE_REF: { + const result = new StringASTNodeImpl(parent, node.startPosition, node.endPosition - node.startPosition); + result.value = node.value; + return result; + } + } } function convertError(e: Yaml.Error) { - return { message: `${e.reason}`, location: { start: e.mark.position, end: e.mark.position + e.mark.column, code: ErrorCode.Undefined } } + return { message: `${e.reason}`, location: { start: e.mark.position, end: e.mark.position + e.mark.column, code: ErrorCode.Undefined } } } function createJSONDocument(yamlDoc: Yaml.YAMLNode, startPositions: number[], text: string) { - let _doc = new SingleYAMLDocument(startPositions); - _doc.root = recursivelyBuildAst(null, yamlDoc) + let _doc = new SingleYAMLDocument(startPositions); + _doc.root = recursivelyBuildAst(null, yamlDoc) - if (!_doc.root) { - // TODO: When this is true, consider not pushing the other errors. - _doc.errors.push({ message: localize('Invalid symbol', 'Expected a YAML object, array or literal'), code: ErrorCode.Undefined, location: { start: yamlDoc.startPosition, end: yamlDoc.endPosition } }); - } + if (!_doc.root) { + // TODO: When this is true, consider not pushing the other errors. + _doc.errors.push({ message: localize('Invalid symbol', 'Expected a YAML object, array or literal'), code: ErrorCode.Undefined, location: { start: yamlDoc.startPosition, end: yamlDoc.endPosition } }); + } - const duplicateKeyReason = 'duplicate key' + const duplicateKeyReason = 'duplicate key' - //Patch ontop of yaml-ast-parser to disable duplicate key message on merge key - let isDuplicateAndNotMergeKey = function (error: Yaml.Error, yamlText: string) { - let errorConverted = convertError(error); - let errorStart = errorConverted.location.start; - let errorEnd = errorConverted.location.end; - if (error.reason === duplicateKeyReason && yamlText.substring(errorStart, errorEnd).startsWith("<<")) { - return false; - } - return true; - }; - const errors = yamlDoc.errors.filter(e => e.reason !== duplicateKeyReason && !e.isWarning).map(e => convertError(e)) - const warnings = yamlDoc.errors.filter(e => (e.reason === duplicateKeyReason && isDuplicateAndNotMergeKey(e, text)) || e.isWarning).map(e => convertError(e)) + //Patch ontop of yaml-ast-parser to disable duplicate key message on merge key + let isDuplicateAndNotMergeKey = function (error: Yaml.Error, yamlText: string) { + let errorConverted = convertError(error); + let errorStart = errorConverted.location.start; + let errorEnd = errorConverted.location.end; + if (error.reason === duplicateKeyReason && yamlText.substring(errorStart, errorEnd).startsWith("<<")) { + return false; + } + return true; + }; + const errors = yamlDoc.errors.filter(e => e.reason !== duplicateKeyReason && !e.isWarning).map(e => convertError(e)) + const warnings = yamlDoc.errors.filter(e => (e.reason === duplicateKeyReason && isDuplicateAndNotMergeKey(e, text)) || e.isWarning).map(e => convertError(e)) - errors.forEach(e => _doc.errors.push(e)); - warnings.forEach(e => _doc.warnings.push(e)); - - return _doc; -} - -export class YAMLDocument { - public documents: JSONDocument[] - private errors; - private warnings; - - constructor(documents: JSONDocument[]) { - this.documents = documents; - this.errors = []; - this.warnings = []; - } + errors.forEach(e => _doc.errors.push(e)); + warnings.forEach(e => _doc.warnings.push(e)); + return _doc; } export function parse(text: string, customTags = []): YAMLDocument { - const startPositions = getLineStartPositions(text) - // This is documented to return a YAMLNode even though the - // typing only returns a YAMLDocument - const yamlDocs = [] + const startPositions = getLineStartPositions(text) + // This is documented to return a YAMLNode even though the + // typing only returns a YAMLDocument + const yamlDocs = [] - let schemaWithAdditionalTags = Schema.create(customTags.map((tag) => { - const typeInfo = tag.split(' '); - return new Type(typeInfo[0], { kind: typeInfo[1] || 'scalar' }); - })); + let schemaWithAdditionalTags = Schema.create(customTags.map((tag) => { + const typeInfo = tag.split(' '); + return new Type(typeInfo[0], { kind: typeInfo[1] || 'scalar' }); + })); - //We need compiledTypeMap to be available from schemaWithAdditionalTags before we add the new custom properties - customTags.map((tag) => { - const typeInfo = tag.split(' '); - schemaWithAdditionalTags.compiledTypeMap[typeInfo[0]] = new Type(typeInfo[0], { kind: typeInfo[1] || 'scalar' }); - }); + //We need compiledTypeMap to be available from schemaWithAdditionalTags before we add the new custom properties + customTags.map((tag) => { + const typeInfo = tag.split(' '); + schemaWithAdditionalTags.compiledTypeMap[typeInfo[0]] = new Type(typeInfo[0], { kind: typeInfo[1] || 'scalar' }); + }); - let additionalOptions: Yaml.LoadOptions = { - schema: schemaWithAdditionalTags - } + let additionalOptions: Yaml.LoadOptions = { + schema: schemaWithAdditionalTags + } - Yaml.loadAll(text, doc => yamlDocs.push(doc), additionalOptions); + Yaml.loadAll(text, doc => yamlDocs.push(doc), additionalOptions); - return new YAMLDocument(yamlDocs.map(doc => createJSONDocument(doc, startPositions, text))); -} \ No newline at end of file + return new YAMLDocument(yamlDocs.map(doc => createJSONDocument(doc, startPositions, text))); +} diff --git a/src/languageservice/services/documentSymbols.ts b/src/languageservice/services/documentSymbols.ts index 92a03e7..f22f743 100644 --- a/src/languageservice/services/documentSymbols.ts +++ b/src/languageservice/services/documentSymbols.ts @@ -5,68 +5,130 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import * as Parser from '../parser/jsonParser'; - -import { SymbolInformation, SymbolKind, TextDocument, Range, Location } from 'vscode-languageserver-types'; -import { Thenable } from "../yamlLanguageService"; -import { IJSONSchemaService } from "./jsonSchemaService"; +import { SymbolKind, TextDocument, Range, DocumentSymbol, ColorInformation, ColorPresentation, Color, TextEdit } from 'vscode-languageserver-types'; +import { ASTNode, PropertyASTNode, Thenable } from '../jsonLanguageTypes'; +import { YAMLDocument, SingleYAMLDocument } from '../yamlLanguageTypes'; +import { IJSONSchemaService } from './jsonSchemaService'; +import { colorFromHex } from '../utils/colors'; +import { getNodeValue } from '../parser/jsonParser'; export class YAMLDocumentSymbols { + constructor(private schemaService: IJSONSchemaService) { + } - public findDocumentSymbols(document: TextDocument, doc: Parser.JSONDocument): SymbolInformation[] { + public findDocumentSymbols(document: TextDocument, doc: YAMLDocument): DocumentSymbol[] { + if (!doc || doc.documents.length === 0) { + return null; + } - if(!doc || doc["documents"].length === 0){ - return null; - } + let collectOutlineEntries = (result: DocumentSymbol[], node: ASTNode): DocumentSymbol[] => { + if (node.type === 'array') { + node.items.forEach((node, index) => { + if (node) { + let range = getRange(document, node); + let selectionRange = range; + let name = String(index); + let children = collectOutlineEntries([], node); + result.push({ name, kind: this.getSymbolKind(node.type), range, selectionRange, children }); + } + }); + } else if (node.type === 'object') { + node.properties.forEach((property: PropertyASTNode) => { + let valueNode = property.valueNode; + if (valueNode) { + let range = getRange(document, property); + let selectionRange = getRange(document, property.keyNode); + let name = property.keyNode.value; + let children = collectOutlineEntries([], valueNode); + result.push({ name, kind: this.getSymbolKind(valueNode.type), range, selectionRange, children }); + } + }); + } + return result; + }; - let collectOutlineEntries = (result: SymbolInformation[], node: Parser.ASTNode, containerName: string): SymbolInformation[] => { - if (node.type === 'array') { - (node).items.forEach((node: Parser.ASTNode) => { - collectOutlineEntries(result, node, containerName); - }); - } else if (node.type === 'object') { - let objectNode = node; + let results = []; + for (let yamlDoc of doc.documents) { + if (yamlDoc.root) { + const result = collectOutlineEntries([], yamlDoc.root); + results = results.concat(result); + } + } - objectNode.properties.forEach((property: Parser.PropertyASTNode) => { - let location = Location.create(document.uri, Range.create(document.positionAt(property.start), document.positionAt(property.end))); - let valueNode = property.value; - if (valueNode) { - let childContainerName = containerName ? containerName + '.' + property.key.value : property.key.value; - result.push({ name: property.key.getValue(), kind: this.getSymbolKind(valueNode.type), location: location, containerName: containerName }); - collectOutlineEntries(result, valueNode, childContainerName); - } - }); - } - return result; - }; + return results; + } - let results = []; - for(let yamlDoc in doc["documents"]){ - let currentYAMLDoc = doc["documents"][yamlDoc]; - if(currentYAMLDoc.root){ - let result = collectOutlineEntries([], currentYAMLDoc.root, void 0); - results = results.concat(result); - } - } + private getSymbolKind(nodeType: string): SymbolKind { + switch (nodeType) { + case 'object': + return SymbolKind.Module; + case 'string': + return SymbolKind.String; + case 'number': + return SymbolKind.Number; + case 'array': + return SymbolKind.Array; + case 'boolean': + return SymbolKind.Boolean; + default: // 'null' + return SymbolKind.Variable; + } + } - return results; - } + public findDocumentColors(document: TextDocument, doc: YAMLDocument): Thenable { + if (!doc || doc.documents.length === 0) { + return Promise.resolve([]); + } - private getSymbolKind(nodeType: string): SymbolKind { - switch (nodeType) { - case 'object': - return SymbolKind.Module; - case 'string': - return SymbolKind.String; - case 'number': - return SymbolKind.Number; - case 'array': - return SymbolKind.Array; - case 'boolean': - return SymbolKind.Boolean; - default: // 'null' - return SymbolKind.Variable; - } - } + const _findDocumentColors = (currentDoc: SingleYAMLDocument) => { + return this.schemaService.getSchemaForResource(document.uri, currentDoc).then(schema => { + let result: ColorInformation[] = []; + if (schema) { + let matchingSchemas = currentDoc.getMatchingSchemas(schema.schema); + let visitedNode = {}; + for (let s of matchingSchemas) { + if (!s.inverted && s.schema && (s.schema.format === 'color' || s.schema.format === 'color-hex') && s.node && s.node.type === 'string') { + let nodeId = String(s.node.offset); + if (!visitedNode[nodeId]) { + let color = colorFromHex(getNodeValue(s.node)); + if (color) { + let range = getRange(document, s.node); + result.push({ color, range }); + } + visitedNode[nodeId] = true; + } + } + } + } + return result; + }); + } -} \ No newline at end of file + return Promise.all(doc.documents.map(currentDoc => _findDocumentColors(currentDoc))) + .then(infoArray => infoArray.reduce((acc, infos) => ([...acc, ...infos]), [])); + } + + public getColorPresentations(document: TextDocument, doc: YAMLDocument, color: Color, range: Range): ColorPresentation[] { + let result: ColorPresentation[] = []; + let red256 = Math.round(color.red * 255), green256 = Math.round(color.green * 255), blue256 = Math.round(color.blue * 255); + + function toTwoDigitHex(n: number): string { + const r = n.toString(16); + return r.length !== 2 ? '0' + r : r; + } + + let label; + if (color.alpha === 1) { + label = `#${toTwoDigitHex(red256)}${toTwoDigitHex(green256)}${toTwoDigitHex(blue256)}`; + } else { + label = `#${toTwoDigitHex(red256)}${toTwoDigitHex(green256)}${toTwoDigitHex(blue256)}${toTwoDigitHex(Math.round(color.alpha * 255))}`; + } + result.push({ label: label, textEdit: TextEdit.replace(range, JSON.stringify(label)) }); + + return result; + } +} + +function getRange(document: TextDocument, node: ASTNode) { + return Range.create(document.positionAt(node.offset), document.positionAt(node.offset + node.length)); +} diff --git a/src/languageservice/services/jsonSchemaService.ts b/src/languageservice/services/jsonSchemaService.ts index 3aa158c..77033b8 100644 --- a/src/languageservice/services/jsonSchemaService.ts +++ b/src/languageservice/services/jsonSchemaService.ts @@ -6,36 +6,17 @@ 'use strict'; import * as Json from 'jsonc-parser'; -import {JSONSchema, JSONSchemaMap} from '../jsonSchema'; +import {JSONSchema, JSONSchemaMap, JSONSchemaRef} from '../jsonSchema'; import URI from 'vscode-uri'; import * as Strings from '../utils/strings'; +import * as Parser from '../parser/jsonParser'; import {SchemaRequestService, WorkspaceContextService, Thenable} from '../yamlLanguageService'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); -/** - * getParseErrorMessage has been removed from jsonc-parser since 1.0.0 - * - * see https://github.com/Microsoft/node-jsonc-parser/blob/42ec16f9c91582d4267a0c48199cdac283c90fc9/CHANGELOG.md - * 1.0.0 - * remove nls dependency (remove getParseErrorMessage) - */ -function getParseErrorMessage(errorCode: Json.ParseErrorCode): string { - switch (errorCode) { - case Json.ParseErrorCode.InvalidSymbol: return localize('error.invalidSymbol', 'Invalid symbol'); - case Json.ParseErrorCode.InvalidNumberFormat: return localize('error.invalidNumberFormat', 'Invalid number format'); - case Json.ParseErrorCode.PropertyNameExpected: return localize('error.propertyNameExpected', 'Property name expected'); - case Json.ParseErrorCode.ValueExpected: return localize('error.valueExpected', 'Value expected'); - case Json.ParseErrorCode.ColonExpected: return localize('error.colonExpected', 'Colon expected'); - case Json.ParseErrorCode.CommaExpected: return localize('error.commaExpected', 'Comma expected'); - case Json.ParseErrorCode.CloseBraceExpected: return localize('error.closeBraceExpected', 'Closing brace expected'); - case Json.ParseErrorCode.CloseBracketExpected: return localize('error.closeBracketExpected', 'Closing bracket expected'); - case Json.ParseErrorCode.EndOfFileExpected: return localize('error.endOfFileExpected', 'End of file expected'); - default: return ''; - } -} +export declare type CustomSchemaProvider = (uri: string) => Thenable; export interface IJSONSchemaService { @@ -57,7 +38,7 @@ export interface IJSONSchemaService { /** * Looks up the appropriate schema for the given URI */ - getSchemaForResource(resource: string): Thenable; + getSchemaForResource(resource: string, document?: Parser.JSONDocument): Thenable; /** * Returns all registered schema ids @@ -74,8 +55,6 @@ export interface ISchemaContributions { schemaAssociations?: ISchemaAssociations; } -export declare type CustomSchemaProvider = (uri: string) => Thenable; - export interface ISchemaHandle { /** * The schema id @@ -94,39 +73,31 @@ export interface ISchemaHandle { } -export class FilePatternAssociation { +class FilePatternAssociation { private schemas: string[]; - private combinedSchemaId: string; private patternRegExp: RegExp; - private combinedSchema: ISchemaHandle; constructor(pattern: string) { - this.combinedSchemaId = 'schemaservice://combinedSchema/' + encodeURIComponent(pattern); try { - this.patternRegExp = Strings.convertSimple2RegExp(pattern); + this.patternRegExp = new RegExp(Strings.convertSimple2RegExpPattern(pattern) + '$'); } catch (e) { // invalid pattern this.patternRegExp = null; } this.schemas = []; - this.combinedSchema = null; } public addSchema(id: string) { this.schemas.push(id); - this.combinedSchema = null; } public matchesPattern(fileName: string): boolean { return this.patternRegExp && this.patternRegExp.test(fileName); } - public getCombinedSchema(service: JSONSchemaService): ISchemaHandle { - if (!this.combinedSchema) { - this.combinedSchema = service.createCombinedSchema(this.combinedSchemaId, this.schemas); - } - return this.combinedSchema; + public getSchemas() { + return this.schemas; } } @@ -188,40 +159,34 @@ export class ResolvedSchema { } public getSection(path: string[]): JSONSchema { - return this.getSectionRecursive(path, this.schema); + return Parser.asSchema(this.getSectionRecursive(path, this.schema)); } - private getSectionRecursive(path: string[], schema: JSONSchema): JSONSchema { - if (!schema || path.length === 0) { + private getSectionRecursive(path: string[], schema: JSONSchemaRef): JSONSchemaRef { + if (!schema || typeof schema === 'boolean' || path.length === 0) { return schema; } let next = path.shift(); - if (schema.properties && schema.properties[next]) { + if (schema.properties && typeof schema.properties[next]) { return this.getSectionRecursive(path, schema.properties[next]); } else if (schema.patternProperties) { - Object.keys(schema.patternProperties).forEach((pattern) => { + for (const pattern of Object.keys(schema.patternProperties)) { let regex = new RegExp(pattern); if (regex.test(next)) { return this.getSectionRecursive(path, schema.patternProperties[pattern]); } - }); - } else if (schema.additionalProperties) { + } + } else if (typeof schema.additionalProperties === 'object') { return this.getSectionRecursive(path, schema.additionalProperties); } else if (next.match('[0-9]+')) { - if (schema.items) { + if (Array.isArray(schema.items)) { + let index = parseInt(next, 10); + if (!isNaN(index) && schema.items[index]) { + return this.getSectionRecursive(path, schema.items[index]); + } + } else if (schema.items) { return this.getSectionRecursive(path, schema.items); - } else if (Array.isArray(schema.items)) { - try { - let index = parseInt(next, 10); - if (schema.items[index]) { - return this.getSectionRecursive(path, schema.items[index]); - } - return null; - } - catch (e) { - return null; - } } } @@ -248,7 +213,7 @@ export class JSONSchemaService implements IJSONSchemaService { this.contextService = contextService; this.requestService = requestService; this.callOnDispose = []; - this.customSchemaProvider = undefined; + this.contributionSchemas = {}; this.contributionAssociations = {}; this.schemasById = {}; @@ -257,10 +222,6 @@ export class JSONSchemaService implements IJSONSchemaService { this.registeredSchemasIds = {}; } - registerCustomSchemaProvider(customSchemaProvider: CustomSchemaProvider) { - this.customSchemaProvider = customSchemaProvider; - } - public getRegisteredSchemaIds(filter?: (scheme) => boolean): string[] { return Object.keys(this.registeredSchemasIds).filter(id => { let scheme = URI.parse(id).scheme; @@ -268,6 +229,10 @@ export class JSONSchemaService implements IJSONSchemaService { }); } + registerCustomSchemaProvider(customSchemaProvider: CustomSchemaProvider) { + this.customSchemaProvider = customSchemaProvider; + } + public dispose(): void { while (this.callOnDispose.length > 0) { this.callOnDispose.pop()(); @@ -304,10 +269,10 @@ export class JSONSchemaService implements IJSONSchemaService { this.contributionAssociations[pattern] = associations; var fpa = this.getOrAddFilePatternAssociation(pattern); - associations.forEach(schemaId => { + for (const schemaId of associations) { let id = this.normalizeId(schemaId); fpa.addSchema(id); - }); + } } } } @@ -337,9 +302,9 @@ export class JSONSchemaService implements IJSONSchemaService { this.registeredSchemasIds[id] = true; if (filePatterns) { - filePatterns.forEach(pattern => { + for (const pattern of filePatterns) { this.getOrAddFilePatternAssociation(pattern).addSchema(id); - }); + } } return unresolvedSchemaContent ? this.addSchemaHandle(id, unresolvedSchemaContent) : this.getOrAddSchemaHandle(id); } @@ -356,11 +321,10 @@ export class JSONSchemaService implements IJSONSchemaService { } for (let pattern in this.contributionAssociations) { var fpa = this.getOrAddFilePatternAssociation(pattern); - - this.contributionAssociations[pattern].forEach(schemaId => { + for (const schemaId of this.contributionAssociations[pattern]) { let id = this.normalizeId(schemaId); fpa.addSchema(id); - }); + } } } @@ -386,13 +350,18 @@ export class JSONSchemaService implements IJSONSchemaService { } let schemaContent: JSONSchema = {}; - let jsonErrors = []; + let jsonErrors: Json.ParseError[] = []; schemaContent = Json.parse(content, jsonErrors); - let errors = jsonErrors.length ? [localize('json.schema.invalidFormat', 'Unable to parse content from \'{0}\': {1}.', toDisplayString(url), getParseErrorMessage(jsonErrors[0]))] : []; + let errors = jsonErrors.length ? [localize('json.schema.invalidFormat', 'Unable to parse content from \'{0}\': Parse error at offset {1}.', toDisplayString(url), jsonErrors[0].offset)] : []; return new UnresolvedSchema(schemaContent, errors); }, (error: any) => { - let errorMessage = localize('json.schema.unabletoload', 'Unable to load schema from \'{0}\': {1}', toDisplayString(url), error.toString()); + let errorMessage = error.toString(); + let errorSplit = error.toString().split('Error: '); + if(errorSplit.length > 1) { + // more concise error message, URL and context are attached by caller anyways + errorMessage = errorSplit[1]; + } return new UnresolvedSchema({}, [errorMessage]); } ); @@ -419,21 +388,20 @@ export class JSONSchemaService implements IJSONSchemaService { return current; }; - let resolveLink = (node: any, linkedSchema: JSONSchema, linkPath: string): void => { - let section = findSection(linkedSchema, linkPath); + let merge = (target: JSONSchema, sourceRoot: JSONSchema, sourceURI: string, path: string): void => { + let section = findSection(sourceRoot, path); if (section) { for (let key in section) { - if (section.hasOwnProperty(key) && !node.hasOwnProperty(key)) { - node[key] = section[key]; + if (section.hasOwnProperty(key) && !target.hasOwnProperty(key)) { + target[key] = section[key]; } } } else { - resolveErrors.push(localize('json.schema.invalidref', '$ref \'{0}\' in {1} can not be resolved.', linkPath, linkedSchema.id)); + resolveErrors.push(localize('json.schema.invalidref', '$ref \'{0}\' in \'{1}\' can not be resolved.', path, sourceURI)); } - delete node.$ref; }; - let resolveExternalLink = (node: any, uri: string, linkPath: string, parentSchemaURL: string): Thenable => { + let resolveExternalLink = (node: JSONSchema, uri: string, linkPath: string, parentSchemaURL: string): Thenable => { if (contextService && !/^\w+:\/\/.*/.test(uri)) { uri = contextService.resolveRelativePath(uri, parentSchemaURL); } @@ -443,13 +411,13 @@ export class JSONSchemaService implements IJSONSchemaService { let loc = linkPath ? uri + '#' + linkPath : uri; resolveErrors.push(localize('json.schema.problemloadingref', 'Problems loading reference \'{0}\': {1}', loc, unresolvedSchema.errors[0])); } - resolveLink(node, unresolvedSchema.schema, linkPath); + merge(node, unresolvedSchema.schema, uri, linkPath); return resolveRefs(node, unresolvedSchema.schema, uri); }); }; let resolveRefs = (node: JSONSchema, parentSchema: JSONSchema, parentSchemaURL: string): Thenable => { - if (!node) { + if (!node || typeof node !== 'object') { return Promise.resolve(null); } @@ -458,7 +426,7 @@ export class JSONSchemaService implements IJSONSchemaService { let openPromises: Thenable[] = []; - let collectEntries = (...entries: JSONSchema[]) => { + let collectEntries = (...entries: JSONSchemaRef[]) => { for (let entry of entries) { if (typeof entry === 'object') { toWalk.push(entry); @@ -470,36 +438,48 @@ export class JSONSchemaService implements IJSONSchemaService { if (typeof map === 'object') { for (let key in map) { let entry = map[key]; - toWalk.push(entry); + if (typeof entry === 'object') { + toWalk.push(entry); + } } } } }; - let collectArrayEntries = (...arrays: JSONSchema[][]) => { + let collectArrayEntries = (...arrays: JSONSchemaRef[][]) => { for (let array of arrays) { if (Array.isArray(array)) { - toWalk.push.apply(toWalk, array); + for (let entry of array) { + if (typeof entry === 'object') { + toWalk.push(entry); + } + } } } }; + let handleRef = (next: JSONSchema) => { + while (next.$ref) { + let segments = next.$ref.split('#', 2); + delete next.$ref; + if (segments[0].length > 0) { + openPromises.push(resolveExternalLink(next, segments[0], segments[1], parentSchemaURL)); + return; + } else { + merge(next, parentSchema, parentSchemaURL, segments[1]); // can set next.$ref again + } + } + + collectEntries(next.items, next.additionalProperties, next.not, next.contains, next.propertyNames, next.if, next.then, next.else); + collectMapEntries(next.definitions, next.properties, next.patternProperties, next.dependencies); + collectArrayEntries(next.anyOf, next.allOf, next.oneOf, next.items); + }; + while (toWalk.length) { let next = toWalk.pop(); if (seen.indexOf(next) >= 0) { continue; } seen.push(next); - if (next.$ref) { - let segments = next.$ref.split('#', 2); - if (segments[0].length > 0) { - openPromises.push(resolveExternalLink(next, segments[0], segments[1], parentSchemaURL)); - continue; - } else { - resolveLink(next, parentSchema, segments[1]); - } - } - collectEntries(next.items, next.additionalProperties, next.not); - collectMapEntries(next.definitions, next.properties, next.patternProperties, next.dependencies); - collectArrayEntries(next.anyOf, next.allOf, next.oneOf, next.items, next.schemaSequence); + handleRef(next); } return Promise.all(openPromises); }; @@ -507,32 +487,47 @@ export class JSONSchemaService implements IJSONSchemaService { return resolveRefs(schema, schema, schemaURL).then(_ => new ResolvedSchema(schema, resolveErrors)); } - public getSchemaForResource(resource: string ): Thenable { - const resolveSchema = () => { - // check for matching file names, last to first - for (let i = this.filePatternAssociations.length - 1; i >= 0; i--) { - let entry = this.filePatternAssociations[i]; - if (entry.matchesPattern(resource)) { - return entry.getCombinedSchema(this).getResolvedSchema(); + public getSchemaForResource(resource: string, document?: Parser.JSONDocument): Thenable { + + // first use $schema if present + if (document && document.root && document.root.type === 'object') { + let schemaProperties = document.root.properties.filter(p => (p.keyNode.value === '$schema') && p.valueNode && p.valueNode.type === 'string'); + if (schemaProperties.length > 0) { + let schemeId = Parser.getNodeValue(schemaProperties[0].valueNode); + if (schemeId && Strings.startsWith(schemeId, '.') && this.contextService) { + schemeId = this.contextService.resolveRelativePath(schemeId, resource); + } + if (schemeId) { + let id = this.normalizeId(schemeId); + return this.getOrAddSchemaHandle(id).getResolvedSchema(); } } - return Promise.resolve(null); - }; - if (this.customSchemaProvider) { - return this.customSchemaProvider(resource).then(schemaUri => { - return this.loadSchema(schemaUri).then(unsolvedSchema => this.resolveSchemaContent(unsolvedSchema, schemaUri)); - }).then(schema => schema, err => { - return resolveSchema(); - }); - } else { - return resolveSchema(); } + + let seen: { [schemaId: string]: boolean } = Object.create(null); + let schemas: string[] = []; + for (let entry of this.filePatternAssociations) { + if (entry.matchesPattern(resource)) { + for (let schemaId of entry.getSchemas()) { + if (!seen[schemaId]) { + schemas.push(schemaId); + seen[schemaId] = true; + } + } + } + } + if (schemas.length > 0) { + return this.createCombinedSchema(resource, schemas).getResolvedSchema(); + } + + return Promise.resolve(null); } - public createCombinedSchema(combinedSchemaId: string, schemaIds: string[]): ISchemaHandle { + private createCombinedSchema(resource: string, schemaIds: string[]): ISchemaHandle { if (schemaIds.length === 1) { return this.getOrAddSchemaHandle(schemaIds[0]); } else { + let combinedSchemaId = 'schemaservice://combinedSchema/' + encodeURIComponent(resource); let combinedSchema: JSONSchema = { allOf: schemaIds.map(schemaId => ({ $ref: schemaId })) }; diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index f965a31..704c29d 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -9,696 +9,898 @@ import * as Parser from '../parser/jsonParser'; import * as Json from 'jsonc-parser'; import * as SchemaService from './jsonSchemaService'; -import { JSONSchema } from '../jsonSchema'; +import { JSONSchema, JSONSchemaRef } from '../jsonSchema'; import { JSONWorkerContribution, CompletionsCollector } from '../jsonContributions'; import { Thenable } from 'vscode-json-languageservice'; -import { CompletionItem, CompletionItemKind, CompletionList, TextDocument, Position, Range, TextEdit, InsertTextFormat } from 'vscode-languageserver-types'; +import { CompletionItem, CompletionItemKind, CompletionList, TextDocument, Position, Range, TextEdit, InsertTextFormat, MarkupContent, MarkupKind } from 'vscode-languageserver-types'; import * as nls from 'vscode-nls'; +import { PropertyASTNode, StringASTNode, ObjectASTNode, ASTNode, ArrayASTNode, ClientCapabilities } from '../jsonLanguageTypes'; +import { endsWith } from '../utils/strings'; +import { isDefined } from '../utils/objects'; +import { stringifyObject } from '../utils/json'; +import { YAMLDocument } from '../yamlLanguageTypes'; import { matchOffsetToDocument } from '../utils/arrUtils'; -import { LanguageSettings } from '../yamlLanguageService'; const localize = nls.loadMessageBundle(); +// !! FIXME: this implementation is buggy export class YAMLCompletion { - private schemaService: SchemaService.IJSONSchemaService; - private contributions: JSONWorkerContribution[]; - private customTags: Array; - private completion: boolean; + private supportsMarkdown: boolean | undefined; - constructor(schemaService: SchemaService.IJSONSchemaService, contributions: JSONWorkerContribution[] = []) { - this.schemaService = schemaService; - this.contributions = contributions; - this.customTags = []; - this.completion = true; - } + constructor(private schemaService: SchemaService.IJSONSchemaService, private contributions: JSONWorkerContribution[] = [], private clientCapabilities: ClientCapabilities = {}) { + this.schemaService = schemaService; + this.contributions = contributions; + } - public configure(languageSettings: LanguageSettings, customTags: Array){ - if (languageSettings) { - this.completion = languageSettings.completion; - } - this.customTags = customTags; - } + public doResolve(item: CompletionItem): Thenable { + for (let i = this.contributions.length - 1; i >= 0; i--) { + if (this.contributions[i].resolveCompletion) { + let resolver = this.contributions[i].resolveCompletion(item); + if (resolver) { + return resolver; + } + } + } + return Promise.resolve(item); + } - public doResolve(item: CompletionItem): Thenable { - for (let i = this.contributions.length - 1; i >= 0; i--) { - if (this.contributions[i].resolveCompletion) { - let resolver = this.contributions[i].resolveCompletion(item); - if (resolver) { - return resolver; - } - } - } - return Promise.resolve(item); - } + public doComplete(document: TextDocument, position: Position, doc: YAMLDocument): Thenable { - public doComplete(document: TextDocument, position: Position, doc): Thenable { + let result: CompletionList = { + items: [], + isIncomplete: false + }; - let result: CompletionList = { - items: [], - isIncomplete: false - }; + let offset = document.offsetAt(position); - if (!this.completion) { + let currentDoc = matchOffsetToDocument(offset, doc); + + if(currentDoc === null){ return Promise.resolve(result); } - let offset = document.offsetAt(position); - if(document.getText()[offset] === ":"){ - return Promise.resolve(result); - } + let node = currentDoc.getNodeFromOffset(offset, true); + if (this.isInComment(document, node ? node.offset : 0, offset)) { + return Promise.resolve(result); + } - let currentDoc = matchOffsetToDocument(offset, doc); - if(currentDoc === null){ - return Promise.resolve(result); - } - const currentDocIndex = doc.documents.indexOf(currentDoc); - let node = currentDoc.getNodeFromOffsetEndInclusive(offset); - if (this.isInComment(document, node ? node.start : 0, offset)) { - return Promise.resolve(result); - } + let currentWord = this.getCurrentWord(document, offset); + let overwriteRange = null; - let currentWord = this.getCurrentWord(document, offset); + if (node && (node.type === 'string' || node.type === 'number' || node.type === 'boolean' || node.type === 'null')) { + overwriteRange = Range.create(document.positionAt(node.offset), document.positionAt(node.offset + node.length)); + } else { + let overwriteStart = offset - currentWord.length; + if (overwriteStart > 0 && document.getText()[overwriteStart - 1] === '"') { + overwriteStart--; + } + overwriteRange = Range.create(document.positionAt(overwriteStart), position); + } - let overwriteRange = null; - if(node && node.type === 'null'){ - let nodeStartPos = document.positionAt(node.start); - nodeStartPos.character += 1; - let nodeEndPos = document.positionAt(node.end); - nodeEndPos.character += 1; - overwriteRange = Range.create(nodeStartPos, nodeEndPos); - }else if (node && (node.type === 'string' || node.type === 'number' || node.type === 'boolean')) { - overwriteRange = Range.create(document.positionAt(node.start), document.positionAt(node.end)); - } else { - let overwriteStart = offset - currentWord.length; - if (overwriteStart > 0 && document.getText()[overwriteStart - 1] === '"') { - overwriteStart--; - } - overwriteRange = Range.create(document.positionAt(overwriteStart), position); - } + let proposed: { [key: string]: CompletionItem } = {}; + let collector: CompletionsCollector = { + add: (suggestion: CompletionItem) => { + let existing = proposed[suggestion.label]; + if (!existing) { + proposed[suggestion.label] = suggestion; + if (overwriteRange) { + suggestion.textEdit = TextEdit.replace(overwriteRange, suggestion.insertText); + } - let proposed: { [key: string]: CompletionItem } = {}; - let collector: CompletionsCollector = { - add: (suggestion: CompletionItem) => { - let existing = proposed[suggestion.label]; - if (!existing) { - proposed[suggestion.label] = suggestion; - if (overwriteRange) { - suggestion.textEdit = TextEdit.replace(overwriteRange, suggestion.insertText); - } - result.items.push(suggestion); - } else if (!existing.documentation) { - existing.documentation = suggestion.documentation; - } - }, - setAsIncomplete: () => { - result.isIncomplete = true; - }, - error: (message: string) => { - console.error(message); - }, - log: (message: string) => { - console.log(message); - }, - getNumberOfProposals: () => { - return result.items.length; - } - }; + result.items.push(suggestion); + } else if (!existing.documentation) { + existing.documentation = suggestion.documentation; + } + }, + setAsIncomplete: () => { + result.isIncomplete = true; + }, + error: (message: string) => { + console.error(message); + }, + log: (message: string) => { + console.log(message); + }, + getNumberOfProposals: () => { + return result.items.length; + } + }; - return this.schemaService.getSchemaForResource(document.uri).then((schema) => { + return this.schemaService.getSchemaForResource(document.uri, currentDoc).then((schema) => { + let collectionPromises: Thenable[] = []; - if(!schema){ - return Promise.resolve(result); - } - let newSchema = schema; - if (schema.schema && schema.schema.schemaSequence && schema.schema.schemaSequence[currentDocIndex]) { - newSchema = new SchemaService.ResolvedSchema(schema.schema.schemaSequence[currentDocIndex]); - } + let addValue = true; + let currentKey = ''; - let collectionPromises: Thenable[] = []; + let currentProperty: PropertyASTNode = null; + if (node) { - let addValue = true; + if (node.type === 'string') { + let parent = node.parent; + if (parent && parent.type === 'property' && parent.keyNode === node) { + addValue = !parent.valueNode; + currentProperty = parent; + currentKey = document.getText().substr(node.offset + 1, node.length - 2); + if (parent) { + node = parent.parent; + } + } + } + } - let currentProperty: Parser.PropertyASTNode = null; - if (node) { + // proposals for properties + if (node && node.type === 'object') { + // don't suggest keys when the cursor is just before the opening curly brace + if (node.offset === offset) { + return result; + } + // don't suggest properties that are already present + let properties = node.properties; + properties.forEach(p => { + if (!currentProperty || currentProperty !== p) { + proposed[p.keyNode.value] = CompletionItem.create('__'); + } + }); + let separatorAfter = ''; + if (addValue) { + separatorAfter = this.evaluateSeparatorAfter(document, document.offsetAt(overwriteRange.end)); + } - if (node.type === 'string') { - let stringNode = node; - if (stringNode.isKey) { - addValue = !(node.parent && ((node.parent).value)); - currentProperty = node.parent ? node.parent : null; - if (node.parent) { - node = node.parent.parent; - } - } - } - } + if (schema) { + // property proposals with schema + this.getPropertyCompletions(schema, currentDoc, node, addValue, separatorAfter, collector); + } else { + // property proposals without schema + this.getSchemaLessPropertyCompletions(currentDoc, node, currentKey, collector); + } - // proposals for properties - if (node && node.type === 'object') { - // don't suggest properties that are already present - let properties = (node).properties; - properties.forEach(p => { - if (!currentProperty || currentProperty !== p) { - proposed[p.key.value] = CompletionItem.create('__'); - } - }); + let location = Parser.getNodePath(node); + this.contributions.forEach((contribution) => { + let collectPromise = contribution.collectPropertyCompletions(document.uri, location, currentWord, addValue, separatorAfter === '', collector); + if (collectPromise) { + collectionPromises.push(collectPromise); + } + }); + if ((!schema && currentWord.length > 0 && document.getText().charAt(offset - currentWord.length - 1) !== '"')) { + collector.add({ + kind: CompletionItemKind.Property, + label: this.getLabelForValue(currentWord), + insertText: this.getInsertTextForProperty(currentWord, null, false, separatorAfter), + insertTextFormat: InsertTextFormat.Snippet, documentation: '' + }); + } + } - let separatorAfter = ''; - if (addValue) { - separatorAfter = this.evaluateSeparatorAfter(document, document.offsetAt(overwriteRange.end)); - } + // proposals for values + let types: { [type: string]: boolean } = {}; + if (schema) { + // value proposals with schema + this.getValueCompletions(schema, currentDoc, node, offset, document, collector, types); + } else { + // value proposals without schema + this.getSchemaLessValueCompletions(currentDoc, node, offset, document, collector); + } + if (this.contributions.length > 0) { + this.getContributedValueCompletions(currentDoc, node, offset, document, collector, collectionPromises); + } - if (newSchema) { - // property proposals with schema - this.getPropertyCompletions(newSchema, currentDoc, node, addValue, collector, separatorAfter); - } + return Promise.all(collectionPromises).then(() => { + if (collector.getNumberOfProposals() === 0) { + let offsetForSeparator = offset; + if (node && (node.type === 'string' || node.type === 'number' || node.type === 'boolean' || node.type === 'null')) { + offsetForSeparator = node.offset + node.length; + } + let separatorAfter = this.evaluateSeparatorAfter(document, offsetForSeparator); + this.addFillerValueCompletions(types, separatorAfter, collector); + } + return result; + }); + }); + } - let location = node.getPath(); - this.contributions.forEach((contribution) => { - let collectPromise = contribution.collectPropertyCompletions(document.uri, location, currentWord, addValue, false, collector); - if (collectPromise) { - collectionPromises.push(collectPromise); - } - }); - if ((!schema && currentWord.length > 0 && document.getText().charAt(offset - currentWord.length - 1) !== '"')) { - collector.add({ - kind: CompletionItemKind.Property, - label: this.getLabelForValue(currentWord), - insertText: this.getInsertTextForProperty(currentWord, null, false, separatorAfter), - insertTextFormat: InsertTextFormat.Snippet, - documentation: '' - }); - } - } + private getPropertyCompletions(schema: SchemaService.ResolvedSchema, doc: Parser.JSONDocument, node: ASTNode, addValue: boolean, separatorAfter: string, collector: CompletionsCollector): void { + let matchingSchemas = doc.getMatchingSchemas(schema.schema, node.offset); + matchingSchemas.forEach((s) => { + if (s.node === node && !s.inverted) { + let schemaProperties = s.schema.properties; + if (schemaProperties) { + Object.keys(schemaProperties).forEach((key: string) => { + let propertySchema = schemaProperties[key]; + if (typeof propertySchema === 'object' && !propertySchema.deprecationMessage && !propertySchema.doNotSuggest) { + let proposal: CompletionItem = { + kind: CompletionItemKind.Property, + label: key, + insertText: this.getInsertTextForProperty(key, propertySchema, addValue, separatorAfter), + insertTextFormat: InsertTextFormat.Snippet, + filterText: this.getFilterTextForValue(key), + documentation: this.fromMarkup(propertySchema.markdownDescription) || propertySchema.description || '', + }; + if (endsWith(proposal.insertText, `$1${separatorAfter}`)) { + proposal.command = { + title: 'Suggest', + command: 'editor.action.triggerSuggest' + }; + } + collector.add(proposal); + } + }); + } - // proposals for values - if (newSchema) { - this.getValueCompletions(newSchema, currentDoc, node, offset, document, collector); - } - if (this.contributions.length > 0) { - this.getContributedValueCompletions(currentDoc, node, offset, document, collector, collectionPromises); - } - if (this.customTags.length > 0) { - this.getCustomTagValueCompletions(collector); - } + // Error fix + // If this is a array of string/boolean/number + // test: + // - item1 + // it will treated as a property key since `:` has been appended + if (node.type === 'object' && node.parent && node.parent.type === 'array' && s.schema.type !== 'object') { + this.addSchemaValueCompletions(s.schema, separatorAfter, collector, {}); + } + } + }); + } - return Promise.all(collectionPromises).then(() => { - return result; - }); - }); - } - private getPropertyCompletions(schema: SchemaService.ResolvedSchema, doc, node: Parser.ASTNode, addValue: boolean, collector: CompletionsCollector, separatorAfter: string): void { - let matchingSchemas = doc.getMatchingSchemas(schema.schema); - matchingSchemas.forEach((s) => { - if (s.node === node && !s.inverted) { - let schemaProperties = s.schema.properties; - if (schemaProperties) { - Object.keys(schemaProperties).forEach((key: string) => { - let propertySchema = schemaProperties[key]; - if (!propertySchema.deprecationMessage && !propertySchema["doNotSuggest"]) { - collector.add({ - kind: CompletionItemKind.Property, - label: key, - insertText: this.getInsertTextForProperty(key, propertySchema, addValue, separatorAfter), - insertTextFormat: InsertTextFormat.Snippet, - documentation: propertySchema.description || '' - }); - } - }); - } - // Error fix - // If this is a array of string/boolean/number - // test: - // - item1 - // it will treated as a property key since `:` has been appended - if (node.type === 'object' && node.parent && node.parent.type === 'array' && s.schema.type !== 'object') { - this.addSchemaValueCompletions(s.schema, collector, separatorAfter) - } - } - }); - } + private getSchemaLessPropertyCompletions(doc: Parser.JSONDocument, node: ASTNode, currentKey: string, collector: CompletionsCollector): void { + let collectCompletionsForSimilarObject = (obj: ObjectASTNode) => { + obj.properties.forEach((p) => { + let key = p.keyNode.value; + collector.add({ + kind: CompletionItemKind.Property, + label: key, + insertText: this.getInsertTextForValue(key, ''), + insertTextFormat: InsertTextFormat.Snippet, + filterText: this.getFilterTextForValue(key), + documentation: '' + }); + }); + }; + if (node.parent) { + if (node.parent.type === 'property') { + // if the object is a property value, check the tree for other objects that hang under a property of the same name + let parentKey = node.parent.keyNode.value; + doc.visit(n => { + if (n.type === 'property' && n !== node.parent && n.keyNode.value === parentKey && n.valueNode && n.valueNode.type === 'object') { + collectCompletionsForSimilarObject(n.valueNode); + } + return true; + }); + } else if (node.parent.type === 'array') { + // if the object is in an array, use all other array elements as similar objects + node.parent.items.forEach(n => { + if (n.type === 'object' && n !== node) { + collectCompletionsForSimilarObject(n); + } + }); + } + } else if (node.type === 'object') { + collector.add({ + kind: CompletionItemKind.Property, + label: '$schema', + insertText: this.getInsertTextForProperty('$schema', null, true, ''), + insertTextFormat: InsertTextFormat.Snippet, documentation: '', + filterText: this.getFilterTextForValue("$schema") + }); + } + } - private getValueCompletions(schema: SchemaService.ResolvedSchema, doc, node: Parser.ASTNode, offset: number, document: TextDocument, collector: CompletionsCollector): void { - let offsetForSeparator = offset; - let parentKey: string = null; - let valueNode: Parser.ASTNode = null; + private getSchemaLessValueCompletions(doc: Parser.JSONDocument, node: ASTNode, offset: number, document: TextDocument, collector: CompletionsCollector): void { + let offsetForSeparator = offset; + if (node && (node.type === 'string' || node.type === 'number' || node.type === 'boolean' || node.type === 'null')) { + offsetForSeparator = node.offset + node.length; + node = node.parent; + } - if (node && (node.type === 'string' || node.type === 'number' || node.type === 'boolean')) { - offsetForSeparator = node.end; - valueNode = node; - node = node.parent; - } + if (!node) { + collector.add({ + kind: this.getSuggestionKind('object'), + label: 'Empty object', + insertText: this.getInsertTextForValue({}, ''), + insertTextFormat: InsertTextFormat.Snippet, + documentation: '' + }); + collector.add({ + kind: this.getSuggestionKind('array'), + label: 'Empty array', + insertText: this.getInsertTextForValue([], ''), + insertTextFormat: InsertTextFormat.Snippet, + documentation: '' + }); + return; + } + let separatorAfter = this.evaluateSeparatorAfter(document, offsetForSeparator); + let collectSuggestionsForValues = (value: ASTNode) => { + if (!Parser.contains(value.parent, offset, true)) { + collector.add({ + kind: this.getSuggestionKind(value.type), + label: this.getLabelTextForMatchingNode(value, document), + insertText: this.getInsertTextForMatchingNode(value, document, separatorAfter), + insertTextFormat: InsertTextFormat.Snippet, documentation: '' + }); + } + if (value.type === 'boolean') { + this.addBooleanValueCompletion(!value.value, separatorAfter, collector); + } + }; - if(node && node.type === 'null'){ - let nodeParent = node.parent; + if (node.type === 'property') { + if (offset > node.colonOffset) { - /* - * This is going to be an object for some reason and we need to find the property - * Its an issue with the null node - */ - if(nodeParent && nodeParent.type === "object"){ - for(let prop in nodeParent["properties"]){ - let currNode = nodeParent["properties"][prop]; - if(currNode.key && currNode.key.location === node.location){ - node = currNode; - } - } - } - } + let valueNode = node.valueNode; + if (valueNode && (offset > (valueNode.offset + valueNode.length) || valueNode.type === 'object' || valueNode.type === 'array')) { + return; + } + // suggest values at the same key + let parentKey = node.keyNode.value; + doc.visit(n => { + if (n.type === 'property' && n.keyNode.value === parentKey && n.valueNode) { + collectSuggestionsForValues(n.valueNode); + } + return true; + }); + if (parentKey === '$schema' && node.parent && !node.parent.parent) { + this.addDollarSchemaCompletions(separatorAfter, collector); + } + } + } + if (node.type === 'array') { + if (node.parent && node.parent.type === 'property') { - if (!node) { - this.addSchemaValueCompletions(schema.schema, collector, ""); - return; - } + // suggest items of an array at the same key + let parentKey = node.parent.keyNode.value; + doc.visit((n) => { + if (n.type === 'property' && n.keyNode.value === parentKey && n.valueNode && n.valueNode.type === 'array') { + n.valueNode.items.forEach(collectSuggestionsForValues); + } + return true; + }); + } else { + // suggest items in the same array + node.items.forEach(collectSuggestionsForValues); + } + } + } - if ((node.type === 'property') && offset > (node).colonOffset) { - let propertyNode = node; - let valueNode = propertyNode.value; - if (valueNode && offset > valueNode.end) { - return; // we are past the value node - } - parentKey = propertyNode.key.value; - node = node.parent; - } - let separatorAfter = this.evaluateSeparatorAfter(document, offsetForSeparator); - if (node && (parentKey !== null || node.type === 'array')) { - let matchingSchemas = doc.getMatchingSchemas(schema.schema); - matchingSchemas.forEach(s => { - if (s.node === node && !s.inverted && s.schema) { - if (s.schema.items) { - if (Array.isArray(s.schema.items)) { - let index = this.findItemAtOffset(node, document, offset); - if (index < s.schema.items.length) { - this.addSchemaValueCompletions(s.schema.items[index], collector, separatorAfter, true); - } - } else if (s.schema.items.type === 'object') { - collector.add({ - kind: this.getSuggestionKind(s.schema.items.type), - label: `- (array item)`, - documentation: `Create an item of an array${s.schema.description === undefined ? '' : '(' + s.schema.description + ')'}`, - insertText: `- ${this.getInsertTextForObject(s.schema.items, separatorAfter).insertText.trimLeft()}`, - insertTextFormat: InsertTextFormat.Snippet, - }); - } - else { - this.addSchemaValueCompletions(s.schema.items, collector, separatorAfter, true); - } - } - if (s.schema.properties) { - let propertySchema = s.schema.properties[parentKey]; - if (propertySchema) { - this.addSchemaValueCompletions(propertySchema, collector, separatorAfter, false); - } - } - } - }); - } - } + private getValueCompletions(schema: SchemaService.ResolvedSchema, doc: Parser.JSONDocument, node: ASTNode, offset: number, document: TextDocument, collector: CompletionsCollector, types: { [type: string]: boolean }): void { + let offsetForSeparator = offset; + let parentKey: string = null; + let valueNode: ASTNode = null; - private getContributedValueCompletions(doc: Parser.JSONDocument, node: Parser.ASTNode, offset: number, document: TextDocument, collector: CompletionsCollector, collectionPromises: Thenable[]) { - if (!node) { - this.contributions.forEach((contribution) => { - let collectPromise = contribution.collectDefaultCompletions(document.uri, collector); - if (collectPromise) { - collectionPromises.push(collectPromise); - } - }); - } else { - if (node.type === 'string' || node.type === 'number' || node.type === 'boolean' || node.type === 'null') { - node = node.parent; - } - if ((node.type === 'property') && offset > (node).colonOffset) { - let parentKey = (node).key.value; + if (node && (node.type === 'string' || node.type === 'number' || node.type === 'boolean' || node.type === 'null')) { + offsetForSeparator = node.offset + node.length; + valueNode = node; + node = node.parent; + } - let valueNode = (node).value; - if (!valueNode || offset <= valueNode.end) { - let location = node.parent.getPath(); - this.contributions.forEach((contribution) => { - let collectPromise = contribution.collectValueCompletions(document.uri, location, parentKey, collector); - if (collectPromise) { - collectionPromises.push(collectPromise); - } - }); - } - } - } - } + if (!node) { + this.addSchemaValueCompletions(schema.schema, '', collector, types); + return; + } - private getCustomTagValueCompletions(collector: CompletionsCollector) { - this.customTags.forEach((customTagItem) => { - let tagItemSplit = customTagItem.split(" "); - if(tagItemSplit && tagItemSplit[0]){ - this.addCustomTagValueCompletion(collector, " ", tagItemSplit[0]); - } - }); - } + if ((node.type === 'property') && offset > node.colonOffset) { + let valueNode = node.valueNode; + if (valueNode && offset > (valueNode.offset + valueNode.length)) { + return; // we are past the value node + } + parentKey = node.keyNode.value; + node = node.parent; + } - private addSchemaValueCompletions(schema: JSONSchema, collector: CompletionsCollector, separatorAfter: string, forArrayItem = false): void { - let types: { [type: string]: boolean } = {}; - this.addSchemaValueCompletionsCore(schema, collector, types, separatorAfter, forArrayItem); - if (types['boolean']) { - this.addBooleanValueCompletion(true, collector, separatorAfter); - this.addBooleanValueCompletion(false, collector, separatorAfter); - } - if (types['null']) { - this.addNullValueCompletion(collector, separatorAfter); - } - } + if (node && (parentKey !== null || node.type === 'array')) { + let separatorAfter = this.evaluateSeparatorAfter(document, offsetForSeparator); - private addSchemaValueCompletionsCore(schema: JSONSchema, collector: CompletionsCollector, types: { [type: string]: boolean }, separatorAfter: string, forArrayItem = false): void { - this.addDefaultValueCompletions(schema, collector, separatorAfter, 0, forArrayItem); - this.addEnumValueCompletions(schema, collector, separatorAfter, forArrayItem); - this.collectTypes(schema, types); - if (Array.isArray(schema.allOf)) { - schema.allOf.forEach(s => this.addSchemaValueCompletionsCore(s, collector, types, separatorAfter, forArrayItem)); - } - if (Array.isArray(schema.anyOf)) { - schema.anyOf.forEach(s => this.addSchemaValueCompletionsCore(s, collector, types, separatorAfter, forArrayItem)); - } - if (Array.isArray(schema.oneOf)) { - schema.oneOf.forEach(s => this.addSchemaValueCompletionsCore(s, collector, types, separatorAfter, forArrayItem)); - } - } + let matchingSchemas = doc.getMatchingSchemas(schema.schema, node.offset, valueNode); + matchingSchemas.forEach(s => { + if (s.node === node && !s.inverted && s.schema) { + if (node.type === 'array' && s.schema.items) { + if (Array.isArray(s.schema.items)) { + let index = this.findItemAtOffset(node, document, offset); + if (index < s.schema.items.length) { + this.addSchemaValueCompletions(s.schema.items[index], separatorAfter, collector, types); + } + } else { + this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types); + } + } + if (s.schema.properties) { + let propertySchema = s.schema.properties[parentKey]; + if (propertySchema) { + this.addSchemaValueCompletions(propertySchema, separatorAfter, collector, types); + } + } + } + }); + if (parentKey === '$schema' && !node.parent) { + this.addDollarSchemaCompletions(separatorAfter, collector); + } + if (types['boolean']) { + this.addBooleanValueCompletion(true, separatorAfter, collector); + this.addBooleanValueCompletion(false, separatorAfter, collector); + } + if (types['null']) { + this.addNullValueCompletion(separatorAfter, collector); + } + } - private addDefaultValueCompletions(schema: JSONSchema, collector: CompletionsCollector, separatorAfter: string, arrayDepth = 0, forArrayItem = false): void { - let hasProposals = false; - if (schema.default) { - let type = schema.type; - let value = schema.default; - for (let i = arrayDepth; i > 0; i--) { - value = [value]; - type = 'array'; - } - collector.add({ - kind: this.getSuggestionKind(type), - label: forArrayItem ? `- ${this.getLabelForValue(value)}` : this.getLabelForValue(value), - insertText: forArrayItem ? `- ${this.getInsertTextForValue(value, separatorAfter)}` : this.getInsertTextForValue(value, separatorAfter), - insertTextFormat: InsertTextFormat.Snippet, - detail: localize('json.suggest.default', 'Default value'), - }); - hasProposals = true; - } - if (!hasProposals && schema.items && !Array.isArray(schema.items)) { - this.addDefaultValueCompletions(schema.items, collector, separatorAfter, arrayDepth + 1); - } - } + } - private addEnumValueCompletions(schema: JSONSchema, collector: CompletionsCollector, separatorAfter: string, forArrayItem = false): void { - if (Array.isArray(schema.enum)) { - for (let i = 0, length = schema.enum.length; i < length; i++) { - let enm = schema.enum[i]; - let documentation = schema.description; - if (schema.enumDescriptions && i < schema.enumDescriptions.length) { - documentation = schema.enumDescriptions[i]; - } - collector.add({ - kind: this.getSuggestionKind(schema.type), - label: forArrayItem ? `- ${this.getLabelForValue(enm)}` : this.getLabelForValue(enm), - insertText: forArrayItem ? `- ${this.getInsertTextForValue(enm, separatorAfter)}` : this.getInsertTextForValue(enm, separatorAfter), - insertTextFormat: InsertTextFormat.Snippet, - documentation - }); - } - } - } + private getContributedValueCompletions(doc: Parser.JSONDocument, node: ASTNode, offset: number, document: TextDocument, collector: CompletionsCollector, collectionPromises: Thenable[]) { + if (!node) { + this.contributions.forEach((contribution) => { + let collectPromise = contribution.collectDefaultCompletions(document.uri, collector); + if (collectPromise) { + collectionPromises.push(collectPromise); + } + }); + } else { + if (node.type === 'string' || node.type === 'number' || node.type === 'boolean' || node.type === 'null') { + node = node.parent; + } + if ((node.type === 'property') && offset > node.colonOffset) { + let parentKey = node.keyNode.value; - private collectTypes(schema: JSONSchema, types: { [type: string]: boolean }) { - let type = schema.type; - if (Array.isArray(type)) { - type.forEach(t => types[t] = true); - } else { - types[type] = true; - } - } + let valueNode = node.valueNode; + if (!valueNode || offset <= (valueNode.offset + valueNode.length)) { + let location = Parser.getNodePath(node.parent); + this.contributions.forEach((contribution) => { + let collectPromise = contribution.collectValueCompletions(document.uri, location, parentKey, collector); + if (collectPromise) { + collectionPromises.push(collectPromise); + } + }); + } + } + } + } - private addBooleanValueCompletion(value: boolean, collector: CompletionsCollector, separatorAfter: string): void { - collector.add({ - kind: this.getSuggestionKind('boolean'), - label: value ? 'true' : 'false', - insertText: this.getInsertTextForValue(value, separatorAfter), - insertTextFormat: InsertTextFormat.Snippet, - documentation: '' - }); - } + private addSchemaValueCompletions(schema: JSONSchemaRef, separatorAfter: string, collector: CompletionsCollector, types: { [type: string]: boolean }): void { + if (typeof schema === 'object') { + this.addEnumValueCompletions(schema, separatorAfter, collector); + this.addDefaultValueCompletions(schema, separatorAfter, collector); + this.collectTypes(schema, types); + if (Array.isArray(schema.allOf)) { + schema.allOf.forEach(s => this.addSchemaValueCompletions(s, separatorAfter, collector, types)); + } + if (Array.isArray(schema.anyOf)) { + schema.anyOf.forEach(s => this.addSchemaValueCompletions(s, separatorAfter, collector, types)); + } + if (Array.isArray(schema.oneOf)) { + schema.oneOf.forEach(s => this.addSchemaValueCompletions(s, separatorAfter, collector, types)); + } + } + } - private addNullValueCompletion(collector: CompletionsCollector, separatorAfter: string): void { - collector.add({ - kind: this.getSuggestionKind('null'), - label: 'null', - insertText: 'null' + separatorAfter, - insertTextFormat: InsertTextFormat.Snippet, - documentation: '' - }); - } + private addDefaultValueCompletions(schema: JSONSchema, separatorAfter: string, collector: CompletionsCollector, arrayDepth = 0): void { + let hasProposals = false; + if (isDefined(schema.default)) { + let type = schema.type; + let value = schema.default; + for (let i = arrayDepth; i > 0; i--) { + value = [value]; + type = 'array'; + } + collector.add({ + kind: this.getSuggestionKind(type), + label: this.getLabelForValue(value), + insertText: this.getInsertTextForValue(value, separatorAfter), + insertTextFormat: InsertTextFormat.Snippet, + detail: localize('json.suggest.default', 'Default value') + }); + hasProposals = true; + } + if (Array.isArray(schema.examples)) { + schema.examples.forEach(example => { + let type = schema.type; + let value = example; + for (let i = arrayDepth; i > 0; i--) { + value = [value]; + type = 'array'; + } + collector.add({ + kind: this.getSuggestionKind(type), + label: this.getLabelForValue(value), + insertText: this.getInsertTextForValue(value, separatorAfter), + insertTextFormat: InsertTextFormat.Snippet + }); + hasProposals = true; + }); + } + if (Array.isArray(schema.defaultSnippets)) { + schema.defaultSnippets.forEach(s => { + let type = schema.type; + let value = s.body; + let label = s.label; + let insertText: string; + let filterText: string; + if (isDefined(value)) { + let type = schema.type; + for (let i = arrayDepth; i > 0; i--) { + value = [value]; + type = 'array'; + } + insertText = this.getInsertTextForSnippetValue(value, separatorAfter); + filterText = this.getFilterTextForSnippetValue(value); + label = label || this.getLabelForSnippetValue(value); + } else if (typeof s.bodyText === 'string') { + let prefix = '', suffix = '', indent = ''; + for (let i = arrayDepth; i > 0; i--) { + prefix = prefix + indent + '[\n'; + suffix = suffix + '\n' + indent + ']'; + indent += '\t'; + type = 'array'; + } + insertText = prefix + indent + s.bodyText.split('\n').join('\n' + indent) + suffix + separatorAfter; + label = label || insertText; + filterText = insertText.replace(/[\n]/g, ''); // remove new lines + } + collector.add({ + kind: this.getSuggestionKind(type), + label, + documentation: this.fromMarkup(s.markdownDescription) || s.description, + insertText, + insertTextFormat: InsertTextFormat.Snippet, + filterText + }); + hasProposals = true; + }); + } + if (!hasProposals && typeof schema.items === 'object' && !Array.isArray(schema.items)) { + this.addDefaultValueCompletions(schema.items, separatorAfter, collector, arrayDepth + 1); + } + } - private addCustomTagValueCompletion(collector: CompletionsCollector, separatorAfter: string, label: string): void { - collector.add({ - kind: this.getSuggestionKind('string'), - label: label, - insertText: label + separatorAfter, - insertTextFormat: InsertTextFormat.Snippet, - documentation: '' - }); - } - private getLabelForValue(value: any): string { - let label = typeof value === "string" ? value : JSON.stringify(value); - if (label.length > 57) { - return label.substr(0, 57).trim() + '...'; - } - return label; - } + private addEnumValueCompletions(schema: JSONSchema, separatorAfter: string, collector: CompletionsCollector): void { + if (isDefined(schema.const)) { + collector.add({ + kind: this.getSuggestionKind(schema.type), + label: this.getLabelForValue(schema.const), + insertText: this.getInsertTextForValue(schema.const, separatorAfter), + insertTextFormat: InsertTextFormat.Snippet, + documentation: this.fromMarkup(schema.markdownDescription) || schema.description + }); + } - private getSuggestionKind(type: any): CompletionItemKind { - if (Array.isArray(type)) { - let array = type; - type = array.length > 0 ? array[0] : null; - } - if (!type) { - return CompletionItemKind.Value; - } - switch (type) { - case 'string': return CompletionItemKind.Value; - case 'object': return CompletionItemKind.Module; - case 'property': return CompletionItemKind.Property; - default: return CompletionItemKind.Value; - } - } + if (Array.isArray(schema.enum)) { + for (let i = 0, length = schema.enum.length; i < length; i++) { + let enm = schema.enum[i]; + let documentation: string | MarkupContent = this.fromMarkup(schema.markdownDescription) || schema.description; + if (schema.markdownEnumDescriptions && i < schema.markdownEnumDescriptions.length && this.doesSupportMarkdown()) { + documentation = this.fromMarkup(schema.markdownEnumDescriptions[i]); + } else if (schema.enumDescriptions && i < schema.enumDescriptions.length) { + documentation = schema.enumDescriptions[i]; + } + collector.add({ + kind: this.getSuggestionKind(schema.type), + label: this.getLabelForValue(enm), + insertText: this.getInsertTextForValue(enm, separatorAfter), + insertTextFormat: InsertTextFormat.Snippet, + documentation + }); + } + } + } - private getCurrentWord(document: TextDocument, offset: number) { - var i = offset - 1; - var text = document.getText(); - while (i >= 0 && ' \t\n\r\v":{[,]}'.indexOf(text.charAt(i)) === -1) { - i--; - } - return text.substring(i + 1, offset); - } + private collectTypes(schema: JSONSchema, types: { [type: string]: boolean }) { + if (Array.isArray(schema.enum) || isDefined(schema.const)) { + return; + } + let type = schema.type; + if (Array.isArray(type)) { + type.forEach(t => types[t] = true); + } else { + types[type] = true; + } + } - private findItemAtOffset(node: Parser.ASTNode, document: TextDocument, offset: number) { - let scanner = Json.createScanner(document.getText(), true); - let children = node.getChildNodes(); - for (let i = children.length - 1; i >= 0; i--) { - let child = children[i]; - if (offset > child.end) { - scanner.setPosition(child.end); - let token = scanner.scan(); - if (token === Json.SyntaxKind.CommaToken && offset >= scanner.getTokenOffset() + scanner.getTokenLength()) { - return i + 1; - } - return i; - } else if (offset >= child.start) { - return i; - } - } - return 0; - } + private addFillerValueCompletions(types: { [type: string]: boolean }, separatorAfter: string, collector: CompletionsCollector): void { + if (types['object']) { + collector.add({ + kind: this.getSuggestionKind('object'), + label: '{}', + insertText: this.getInsertTextForGuessedValue({}, separatorAfter), + insertTextFormat: InsertTextFormat.Snippet, + detail: localize('defaults.object', 'New object'), + documentation: '' + }); + } + if (types['array']) { + collector.add({ + kind: this.getSuggestionKind('array'), + label: '[]', + insertText: this.getInsertTextForGuessedValue([], separatorAfter), + insertTextFormat: InsertTextFormat.Snippet, + detail: localize('defaults.array', 'New array'), + documentation: '' + }); + } + } - private isInComment(document: TextDocument, start: number, offset: number) { - let scanner = Json.createScanner(document.getText(), false); - scanner.setPosition(start); - let token = scanner.scan(); - while (token !== Json.SyntaxKind.EOF && (scanner.getTokenOffset() + scanner.getTokenLength() < offset)) { - token = scanner.scan(); - } - return (token === Json.SyntaxKind.LineCommentTrivia || token === Json.SyntaxKind.BlockCommentTrivia) && scanner.getTokenOffset() <= offset; - } + private addBooleanValueCompletion(value: boolean, separatorAfter: string, collector: CompletionsCollector): void { + collector.add({ + kind: this.getSuggestionKind('boolean'), + label: value ? 'true' : 'false', + insertText: this.getInsertTextForValue(value, separatorAfter), + insertTextFormat: InsertTextFormat.Snippet, + documentation: '' + }); + } - private getInsertTextForPlainText(text: string): string { - return text.replace(/[\\\$\}]/g, '\\$&'); // escape $, \ and } - } + private addNullValueCompletion(separatorAfter: string, collector: CompletionsCollector): void { + collector.add({ + kind: this.getSuggestionKind('null'), + label: 'null', + insertText: 'null' + separatorAfter, + insertTextFormat: InsertTextFormat.Snippet, + documentation: '' + }); + } - private getInsertTextForValue(value: any, separatorAfter: string): string { - var text = value; - if (text === '{}') { - return '{\n\t$1\n}' + separatorAfter; - } else if (text === '[]') { - return '[\n\t$1\n]' + separatorAfter; - } - return this.getInsertTextForPlainText(text + separatorAfter); - } + private addDollarSchemaCompletions(separatorAfter: string, collector: CompletionsCollector): void { + let schemaIds = this.schemaService.getRegisteredSchemaIds(schema => schema === 'http' || schema === 'https'); + schemaIds.forEach(schemaId => collector.add({ + kind: CompletionItemKind.Module, + label: this.getLabelForValue(schemaId), + filterText: this.getFilterTextForValue(schemaId), + insertText: this.getInsertTextForValue(schemaId, separatorAfter), + insertTextFormat: InsertTextFormat.Snippet, documentation: '' + })); + } - private getInsertTextForObject(schema: JSONSchema, separatorAfter: string, indent = '\t', insertIndex = 1) { - let insertText = ""; - if (!schema.properties) { - insertText = `${indent}\$${insertIndex++}\n`; - return { insertText, insertIndex }; - } + private getLabelForValue(value: any): string { + let label = JSON.stringify(value); + if (label.length > 57) { + return label.substr(0, 57).trim() + '...'; + } + return label; + } - Object.keys(schema.properties).forEach((key: string) => { - let propertySchema = schema.properties[key]; - let type = Array.isArray(propertySchema.type) ? propertySchema.type[0] : propertySchema.type; - if (!type) { - if (propertySchema.properties) { - type = 'object'; - } - if (propertySchema.items) { - type = 'array'; - } - } - if (schema.required && schema.required.indexOf(key) > -1) { - switch (type) { - case 'boolean': - case 'string': - case 'number': - case 'integer': - insertText += `${indent}${key}: \$${insertIndex++}\n` - break; - case 'array': - let arrayInsertResult = this.getInsertTextForArray(propertySchema.items, separatorAfter, `${indent}\t`, insertIndex++); - insertIndex = arrayInsertResult.insertIndex; - insertText += `${indent}${key}:\n${indent}\t- ${arrayInsertResult.insertText}\n`; - break; - case 'object': - let objectInsertResult = this.getInsertTextForObject(propertySchema, separatorAfter, `${indent}\t`, insertIndex++); - insertIndex = objectInsertResult.insertIndex; - insertText += `${indent}${key}:\n${objectInsertResult.insertText}\n`; - break; - } - } else if (propertySchema.default !== undefined) { - switch (type) { - case 'boolean': - case 'string': - case 'number': - case 'integer': - insertText += `${indent}${key}: \${${insertIndex++}:${propertySchema.default}}\n` - break; - case 'array': - case 'object': - // TODO: support default value for array object - break; - } - } - }); - if (insertText.trim().length === 0) { - insertText = `${indent}\$${insertIndex++}\n`; - } - insertText = insertText.trimRight() + separatorAfter; - return { insertText, insertIndex }; - } + private getFilterTextForValue(value): string { + return JSON.stringify(value); + } - private getInsertTextForArray(schema: JSONSchema, separatorAfter: string, indent = '\t', insertIndex = 1) { - let insertText = ''; - if (!schema) { - insertText = `\$${insertIndex++}`; - } - let type = Array.isArray(schema.type) ? schema.type[0] : schema.type; - if (!type) { - if (schema.properties) { - type = 'object'; - } - if (schema.items) { - type = 'array'; - } - } - switch (schema.type) { - case 'boolean': - insertText = `\${${insertIndex++}:false}`; - break; - case 'number': - case 'integer': - insertText = `\${${insertIndex++}:0}`; - break; - case 'string': - insertText = `\${${insertIndex++}:null}`; - break; - case 'object': - let objectInsertResult = this.getInsertTextForObject(schema, separatorAfter, `${indent}\t`, insertIndex++); - insertText = objectInsertResult.insertText.trimLeft(); - insertIndex = objectInsertResult.insertIndex; - break; - } - return { insertText, insertIndex }; - } + private getFilterTextForSnippetValue(value): string { + return JSON.stringify(value).replace(/\$\{\d+:([^}]+)\}|\$\d+/g, '$1'); + } - private getInsertTextForProperty(key: string, propertySchema: JSONSchema, addValue: boolean, separatorAfter: string): string { + private getLabelForSnippetValue(value: any): string { + let label = JSON.stringify(value); + label = label.replace(/\$\{\d+:([^}]+)\}|\$\d+/g, '$1'); + if (label.length > 57) { + return label.substr(0, 57).trim() + '...'; + } + return label; + } - let propertyText = this.getInsertTextForValue(key, ''); - // if (!addValue) { - // return propertyText; - // } - let resultText = propertyText + ':'; + private getInsertTextForPlainText(text: string): string { + return text.replace(/[\\\$\}]/g, '\\$&'); // escape $, \ and } + } - let value; - if (propertySchema) { - if (propertySchema.default !== undefined) { - value = ` \${1:${propertySchema.default}}` - } - else if (propertySchema.properties) { - return `${resultText}\n${this.getInsertTextForObject(propertySchema, separatorAfter).insertText}`; - } - else if (propertySchema.items) { - return `${resultText}\n\t- ${this.getInsertTextForArray(propertySchema.items, separatorAfter).insertText}`; - } - else { - var type = Array.isArray(propertySchema.type) ? propertySchema.type[0] : propertySchema.type; - switch (type) { - case 'boolean': - value = ' $1'; - break; - case 'string': - value = ' $1'; - break; - case 'object': - value = '\n\t'; - break; - case 'array': - value = '\n\t- '; - break; - case 'number': - case 'integer': - value = ' ${1:0}'; - break; - case 'null': - value = ' ${1:null}'; - break; - default: - return propertyText; - } - } - } - if (!value) { - value = '$1'; - } - return resultText + value + separatorAfter; - } + private getInsertTextForValue(value: any, separatorAfter: string): string { + var text = JSON.stringify(value, null, '\t'); + if (text === '{}') { + return '{$1}' + separatorAfter; + } else if (text === '[]') { + return '[$1]' + separatorAfter; + } + return this.getInsertTextForPlainText(text + separatorAfter); + } - private evaluateSeparatorAfter(document: TextDocument, offset: number) { - let scanner = Json.createScanner(document.getText(), true); - scanner.setPosition(offset); - let token = scanner.scan(); - switch (token) { - case Json.SyntaxKind.CommaToken: - case Json.SyntaxKind.CloseBraceToken: - case Json.SyntaxKind.CloseBracketToken: - case Json.SyntaxKind.EOF: - return ''; - default: - return ''; - } - } -} \ No newline at end of file + private getInsertTextForSnippetValue(value: any, separatorAfter: string): string { + let replacer = (value) => { + if (typeof value === 'string') { + if (value[0] === '^') { + return value.substr(1); + } + } + return JSON.stringify(value); + }; + return stringifyObject(value, '', replacer) + separatorAfter; + } + + private templateVarIdCounter = 0; + + private getInsertTextForGuessedValue(value: any, separatorAfter: string): string { + switch (typeof value) { + case 'object': + if (value === null) { + return '${1:null}' + separatorAfter; + } + return this.getInsertTextForValue(value, separatorAfter); + case 'string': + let snippetValue = JSON.stringify(value); + snippetValue = snippetValue.substr(1, snippetValue.length - 2); // remove quotes + snippetValue = this.getInsertTextForPlainText(snippetValue); // escape \ and } + return '"${1:' + snippetValue + '}"' + separatorAfter; + case 'number': + case 'boolean': + return '${1:' + JSON.stringify(value) + '}' + separatorAfter; + } + return this.getInsertTextForValue(value, separatorAfter); + } + + private getSuggestionKind(type: any): CompletionItemKind { + if (Array.isArray(type)) { + let array = type; + type = array.length > 0 ? array[0] : null; + } + if (!type) { + return CompletionItemKind.Value; + } + switch (type) { + case 'string': return CompletionItemKind.Value; + case 'object': return CompletionItemKind.Module; + case 'property': return CompletionItemKind.Property; + default: return CompletionItemKind.Value; + } + } + + private getLabelTextForMatchingNode(node: ASTNode, document: TextDocument): string { + switch (node.type) { + case 'array': + return '[]'; + case 'object': + return '{}'; + default: + let content = document.getText().substr(node.offset, node.length); + return content; + } + } + + private getInsertTextForMatchingNode(node: ASTNode, document: TextDocument, separatorAfter: string): string { + switch (node.type) { + case 'array': + return this.getInsertTextForValue([], separatorAfter); + case 'object': + return this.getInsertTextForValue({}, separatorAfter); + default: + let content = document.getText().substr(node.offset, node.length) + separatorAfter; + return this.getInsertTextForPlainText(content); + } + } + + private getInsertTextForProperty(key: string, propertySchema: JSONSchema, addValue: boolean, separatorAfter: string): string { + + let propertyText = this.getInsertTextForValue(key, ''); + if (!addValue) { + return propertyText; + } + let resultText = propertyText + ': '; + + let value; + let nValueProposals = 0; + if (propertySchema) { + if (Array.isArray(propertySchema.defaultSnippets)) { + if (propertySchema.defaultSnippets.length === 1) { + let body = propertySchema.defaultSnippets[0].body; + if (isDefined(body)) { + value = this.getInsertTextForSnippetValue(body, ''); + } + } + nValueProposals += propertySchema.defaultSnippets.length; + } + if (propertySchema.enum) { + if (!value && propertySchema.enum.length === 1) { + value = this.getInsertTextForGuessedValue(propertySchema.enum[0], ''); + } + nValueProposals += propertySchema.enum.length; + } + if (isDefined(propertySchema.default)) { + if (!value) { + value = this.getInsertTextForGuessedValue(propertySchema.default, ''); + } + nValueProposals++; + } + if (nValueProposals === 0) { + var type = Array.isArray(propertySchema.type) ? propertySchema.type[0] : propertySchema.type; + if (!type) { + if (propertySchema.properties) { + type = 'object'; + } else if (propertySchema.items) { + type = 'array'; + } + } + switch (type) { + case 'boolean': + value = '$1'; + break; + case 'string': + value = '"$1"'; + break; + case 'object': + value = '{$1}'; + break; + case 'array': + value = '[$1]'; + break; + case 'number': + case 'integer': + value = '${1:0}'; + break; + case 'null': + value = '${1:null}'; + break; + default: + return propertyText; + } + } + } + if (!value || nValueProposals > 1) { + value = '$1'; + } + return resultText + value + separatorAfter; + } + + private getCurrentWord(document: TextDocument, offset: number) { + var i = offset - 1; + var text = document.getText(); + while (i >= 0 && ' \t\n\r\v":{[,]}'.indexOf(text.charAt(i)) === -1) { + i--; + } + return text.substring(i + 1, offset); + } + + private evaluateSeparatorAfter(document: TextDocument, offset: number) { + let scanner = Json.createScanner(document.getText(), true); + scanner.setPosition(offset); + let token = scanner.scan(); + switch (token) { + case Json.SyntaxKind.CommaToken: + case Json.SyntaxKind.CloseBraceToken: + case Json.SyntaxKind.CloseBracketToken: + case Json.SyntaxKind.EOF: + return ''; + default: + return ','; + } + } + + private findItemAtOffset(node: ArrayASTNode, document: TextDocument, offset: number) { + let scanner = Json.createScanner(document.getText(), true); + let children = node.items; + for (let i = children.length - 1; i >= 0; i--) { + let child = children[i]; + if (offset > child.offset + child.length) { + scanner.setPosition(child.offset + child.length); + let token = scanner.scan(); + if (token === Json.SyntaxKind.CommaToken && offset >= scanner.getTokenOffset() + scanner.getTokenLength()) { + return i + 1; + } + return i; + } else if (offset >= child.offset) { + return i; + } + } + return 0; + } + + private isInComment(document: TextDocument, start: number, offset: number) { + let scanner = Json.createScanner(document.getText(), false); + scanner.setPosition(start); + let token = scanner.scan(); + while (token !== Json.SyntaxKind.EOF && (scanner.getTokenOffset() + scanner.getTokenLength() < offset)) { + token = scanner.scan(); + } + return (token === Json.SyntaxKind.LineCommentTrivia || token === Json.SyntaxKind.BlockCommentTrivia) && scanner.getTokenOffset() <= offset; + } + + private fromMarkup(markupString: string | undefined): MarkupContent | string | undefined { + if (markupString && this.doesSupportMarkdown()) { + return { + kind: MarkupKind.Markdown, + value: markupString + }; + } + return undefined; + } + + private doesSupportMarkdown() { + if (!isDefined(this.supportsMarkdown)) { + const completion = this.clientCapabilities.textDocument && this.clientCapabilities.textDocument.completion; + this.supportsMarkdown = completion && completion.completionItem && Array.isArray(completion.completionItem.documentationFormat) && completion.completionItem.documentationFormat.indexOf(MarkupKind.Markdown) !== -1; + } + return this.supportsMarkdown; + } +} diff --git a/src/languageservice/services/yamlHover.ts b/src/languageservice/services/yamlHover.ts index 520ca71..f006071 100644 --- a/src/languageservice/services/yamlHover.ts +++ b/src/languageservice/services/yamlHover.ts @@ -8,62 +8,44 @@ import * as Parser from '../parser/jsonParser'; import * as SchemaService from './jsonSchemaService'; -import {JSONWorkerContribution} from '../jsonContributions'; -import {Thenable} from 'vscode-json-languageservice'; +import { JSONWorkerContribution } from '../jsonContributions'; +import { Thenable } from 'vscode-json-languageservice'; -import {Hover, TextDocument, Position, Range, MarkedString} from 'vscode-languageserver-types'; +import { Hover, TextDocument, Position, Range, MarkedString } from 'vscode-languageserver-types'; +import { YAMLDocument } from '../yamlLanguageTypes'; import { matchOffsetToDocument } from '../utils/arrUtils'; -import { LanguageSettings } from '../yamlLanguageService'; export class YAMLHover { - private schemaService: SchemaService.IJSONSchemaService; - private contributions: JSONWorkerContribution[]; - private shouldHover: boolean; + constructor(private schemaService: SchemaService.IJSONSchemaService, private contributions: JSONWorkerContribution[] = []) { + } - constructor(schemaService: SchemaService.IJSONSchemaService, contributions: JSONWorkerContribution[] = []) { - this.schemaService = schemaService; - this.contributions = contributions; - this.shouldHover = true; - } + public doHover(document: TextDocument, position: Position, doc: YAMLDocument): Thenable { - public configure(languageSettings: LanguageSettings){ - if(languageSettings){ - this.shouldHover = languageSettings.hover; - } - } - - public doHover(document: TextDocument, position: Position, doc): Thenable { - - if(!this.shouldHover || !document){ - return Promise.resolve(void 0); - } - - let offset = document.offsetAt(position); + let offset = document.offsetAt(position); let currentDoc = matchOffsetToDocument(offset, doc); if(currentDoc === null){ return Promise.resolve(void 0); } const currentDocIndex = doc.documents.indexOf(currentDoc); let node = currentDoc.getNodeFromOffset(offset); - if (!node || (node.type === 'object' || node.type === 'array') && offset > node.start + 1 && offset < node.end - 1) { - return Promise.resolve(void 0); + if (!node || (node.type === 'object' || node.type === 'array') && offset > node.offset + 1 && offset < node.offset + node.length - 1) { + return Promise.resolve(null); } let hoverRangeNode = node; // use the property description when hovering over an object key if (node.type === 'string') { - let stringNode = node; - if (stringNode.isKey) { - let propertyNode = node.parent; - node = propertyNode.value; + let parent = node.parent; + if (parent && parent.type === 'property' && parent.keyNode === node) { + node = parent.valueNode; if (!node) { - return Promise.resolve(void 0); + return Promise.resolve(null); } } } - let hoverRange = Range.create(document.positionAt(hoverRangeNode.start), document.positionAt(hoverRangeNode.end)); + let hoverRange = Range.create(document.positionAt(hoverRangeNode.offset), document.positionAt(hoverRangeNode.offset + hoverRangeNode.length)); var createHover = (contents: MarkedString[]) => { let result: Hover = { @@ -73,7 +55,7 @@ export class YAMLHover { return result; }; - let location = node.getPath(); + let location = Parser.getNodePath(node); for (let i = this.contributions.length - 1; i >= 0; i--) { let contribution = this.contributions[i]; let promise = contribution.getInfoContribution(document.uri, location); @@ -82,25 +64,21 @@ export class YAMLHover { } } - return this.schemaService.getSchemaForResource(document.uri).then((schema) => { + return this.schemaService.getSchemaForResource(document.uri, currentDoc).then((schema) => { if (schema) { - let newSchema = schema; - if (schema.schema && schema.schema.schemaSequence && schema.schema.schemaSequence[currentDocIndex]) { - newSchema = new SchemaService.ResolvedSchema(schema.schema.schemaSequence[currentDocIndex]); - } - let matchingSchemas = currentDoc.getMatchingSchemas(newSchema.schema, node.start); + let matchingSchemas = currentDoc.getMatchingSchemas(schema.schema, node.offset); let title: string = null; let markdownDescription: string = null; let markdownEnumValueDescription = null, enumValue = null; - matchingSchemas.every((s) => { + matchingSchemas.forEach((s) => { if (s.node === node && !s.inverted && s.schema) { title = title || s.schema.title; - markdownDescription = markdownDescription || s.schema["markdownDescription"] || toMarkdown(s.schema.description); - if (s.schema.enum) { - let idx = s.schema.enum.indexOf(node.getValue()); - if (s.schema["markdownEnumDescriptions"]) { - markdownEnumValueDescription = s.schema["markdownEnumDescriptions"][idx]; + markdownDescription = markdownDescription || s.schema.markdownDescription || toMarkdown(s.schema.description); + if (s.schema.enum) { + let idx = s.schema.enum.indexOf(Parser.getNodeValue(node)); + if (s.schema.markdownEnumDescriptions) { + markdownEnumValueDescription = s.schema.markdownEnumDescriptions[idx]; } else if (s.schema.enumDescriptions) { markdownEnumValueDescription = toMarkdown(s.schema.enumDescriptions[idx]); } @@ -132,15 +110,15 @@ export class YAMLHover { } return createHover([result]); } - return void 0; + return null; }); } } function toMarkdown(plain: string) { - if (plain) { - let res = plain.replace(/([^\n\r])(\r?\n)([^\n\r])/gm, '$1\n\n$3'); // single new lines to \n\n (Markdown paragraph) - return res.replace(/[\\`*_{}[\]()#+\-.!]/g, "\\$&"); // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash - } - return void 0; -} \ No newline at end of file + if (plain) { + let res = plain.replace(/([^\n\r])(\r?\n)([^\n\r])/gm, '$1\n\n$3'); // single new lines to \n\n (Markdown paragraph) + return res.replace(/[\\`*_{}[\]()#+\-.!]/g, "\\$&"); // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash + } + return void 0; +} diff --git a/src/languageservice/services/yamlValidation.ts b/src/languageservice/services/yamlValidation.ts index 8d2cd5a..b35f3f3 100644 --- a/src/languageservice/services/yamlValidation.ts +++ b/src/languageservice/services/yamlValidation.ts @@ -6,31 +6,27 @@ 'use strict'; import { JSONSchemaService, ResolvedSchema } from './jsonSchemaService'; -import { DiagnosticSeverity } from 'vscode-languageserver-types'; -import { LanguageSettings} from '../yamlLanguageService'; +import { DiagnosticSeverity, TextDocument } from 'vscode-languageserver-types'; +import { LanguageSettings } from '../yamlLanguageService'; +import { YAMLDocument } from '../yamlLanguageTypes'; export class YAMLValidation { - private jsonSchemaService: JSONSchemaService; - private validationEnabled: boolean; + private validationEnabled: boolean; + public constructor(private jsonSchemaService: JSONSchemaService) { + this.validationEnabled = true; + } - public constructor(jsonSchemaService) { - this.jsonSchemaService = jsonSchemaService; - this.validationEnabled = true; - } - - public configure(shouldValidate: LanguageSettings){ - if(shouldValidate){ - this.validationEnabled = shouldValidate.validate; + public configure(raw: LanguageSettings) { + if (raw) { + this.validationEnabled = raw.validate; } } - public doValidation(textDocument, yamlDocument) { - - if(!this.validationEnabled){ + public doValidation(textDocument: TextDocument, yamlDocument: YAMLDocument) { + if (!this.validationEnabled) { return Promise.resolve([]); } - return this.jsonSchemaService.getSchemaForResource(textDocument.uri).then(function (schema) { var diagnostics = []; var added = {}; @@ -42,10 +38,10 @@ export class YAMLValidation { if (schema.schema && schema.schema.schemaSequence && schema.schema.schemaSequence[documentIndex]) { newSchema = new ResolvedSchema(schema.schema.schemaSequence[documentIndex]); } - let diagnostics = currentDoc.getValidationProblems(newSchema.schema); + let diagnostics = currentDoc.validate(textDocument, newSchema.schema); for(let diag in diagnostics){ let curDiagnostic = diagnostics[diag]; - currentDoc.errors.push({ location: { start: curDiagnostic.location.start, end: curDiagnostic.location.end }, message: curDiagnostic.message }) + currentDoc.errors.push({ location: { start: curDiagnostic.range.start, end: curDiagnostic.range.end }, message: curDiagnostic.message }) } documentIndex++; } @@ -93,4 +89,4 @@ export class YAMLValidation { return diagnostics; }); } -} \ No newline at end of file +} diff --git a/src/languageservice/utils/arrUtils.ts b/src/languageservice/utils/arrUtils.ts index 4130f35..4dbf2d3 100644 --- a/src/languageservice/utils/arrUtils.ts +++ b/src/languageservice/utils/arrUtils.ts @@ -1,75 +1,71 @@ +import { SingleYAMLDocument, YAMLDocument } from '../yamlLanguageTypes'; + /*--------------------------------------------------------------------------------------------- * Copyright (c) Red Hat, Inc. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SingleYAMLDocument } from "../parser/yamlParser"; - export function removeDuplicates(arr, prop) { - var new_arr = []; - var lookup = {}; + var new_arr = []; + var lookup = {}; - for (var i in arr) { - lookup[arr[i][prop]] = arr[i]; - } + for (var i in arr) { + lookup[arr[i][prop]] = arr[i]; + } - for (i in lookup) { - new_arr.push(lookup[i]); - } + for (i in lookup) { + new_arr.push(lookup[i]); + } - return new_arr; + return new_arr; } export function getLineOffsets(textDocString: String): number[] { - - let lineOffsets: number[] = []; - let text = textDocString; - let isLineStart = true; - for (let i = 0; i < text.length; i++) { - if (isLineStart) { - lineOffsets.push(i); - isLineStart = false; - } - let ch = text.charAt(i); - isLineStart = (ch === '\r' || ch === '\n'); - if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') { - i++; - } - } - if (isLineStart && text.length > 0) { - lineOffsets.push(text.length); - } - - return lineOffsets; + + let lineOffsets: number[] = []; + let text = textDocString; + let isLineStart = true; + for (let i = 0; i < text.length; i++) { + if (isLineStart) { + lineOffsets.push(i); + isLineStart = false; + } + let ch = text.charAt(i); + isLineStart = (ch === '\r' || ch === '\n'); + if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') { + i++; + } + } + if (isLineStart && text.length > 0) { + lineOffsets.push(text.length); + } + + return lineOffsets; } -export function removeDuplicatesObj(objArray){ - - let nonDuplicateSet = new Set(); - let nonDuplicateArr = []; - for(let obj in objArray){ +export function removeDuplicatesObj(objArray) { - let currObj = objArray[obj]; - let stringifiedObj = JSON.stringify(currObj); - if(!nonDuplicateSet.has(stringifiedObj)){ - nonDuplicateArr.push(currObj); - nonDuplicateSet.add(stringifiedObj); - } + let nonDuplicateSet = new Set(); + let nonDuplicateArr = []; + for (let obj in objArray) { - } + let currObj = objArray[obj]; + let stringifiedObj = JSON.stringify(currObj); + if (!nonDuplicateSet.has(stringifiedObj)) { + nonDuplicateArr.push(currObj); + nonDuplicateSet.add(stringifiedObj); + } - return nonDuplicateArr; + } + + return nonDuplicateArr; } -export function matchOffsetToDocument(offset: number, jsonDocuments): SingleYAMLDocument { - - for(let jsonDoc in jsonDocuments.documents){ - let currJsonDoc = jsonDocuments.documents[jsonDoc]; - if(currJsonDoc.root && currJsonDoc.root.end >= offset && currJsonDoc.root.start <= offset){ - return currJsonDoc; - } - } - - return null; - -} \ No newline at end of file +export function matchOffsetToDocument(offset: number, doc: YAMLDocument): SingleYAMLDocument { + for (let currDoc of doc.documents) { + if (currDoc.root && (currDoc.root.length + currDoc.root.offset) >= offset && currDoc.root.offset <= offset) { + return currDoc; + } + } + return null; +} diff --git a/src/languageservice/utils/colors.ts b/src/languageservice/utils/colors.ts new file mode 100644 index 0000000..d0ecbd1 --- /dev/null +++ b/src/languageservice/utils/colors.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { Color } from "../jsonLanguageTypes"; + +const Digit0 = 48; +const Digit9 = 57; +const A = 65; +const a = 97; +const f = 102; + +export function hexDigit(charCode: number) { + if (charCode < Digit0) { + return 0; + } + if (charCode <= Digit9) { + return charCode - Digit0; + } + if (charCode < a) { + charCode += (a - A); + } + if (charCode >= a && charCode <= f) { + return charCode - a + 10; + } + return 0; +} + +export function colorFromHex(text: string): Color { + if (text[0] !== '#') { + return null; + } + switch (text.length) { + case 4: + return { + red: (hexDigit(text.charCodeAt(1)) * 0x11) / 255.0, + green: (hexDigit(text.charCodeAt(2)) * 0x11) / 255.0, + blue: (hexDigit(text.charCodeAt(3)) * 0x11) / 255.0, + alpha: 1 + }; + case 5: + return { + red: (hexDigit(text.charCodeAt(1)) * 0x11) / 255.0, + green: (hexDigit(text.charCodeAt(2)) * 0x11) / 255.0, + blue: (hexDigit(text.charCodeAt(3)) * 0x11) / 255.0, + alpha: (hexDigit(text.charCodeAt(4)) * 0x11) / 255.0, + }; + case 7: + return { + red: (hexDigit(text.charCodeAt(1)) * 0x10 + hexDigit(text.charCodeAt(2))) / 255.0, + green: (hexDigit(text.charCodeAt(3)) * 0x10 + hexDigit(text.charCodeAt(4))) / 255.0, + blue: (hexDigit(text.charCodeAt(5)) * 0x10 + hexDigit(text.charCodeAt(6))) / 255.0, + alpha: 1 + }; + case 9: + return { + red: (hexDigit(text.charCodeAt(1)) * 0x10 + hexDigit(text.charCodeAt(2))) / 255.0, + green: (hexDigit(text.charCodeAt(3)) * 0x10 + hexDigit(text.charCodeAt(4))) / 255.0, + blue: (hexDigit(text.charCodeAt(5)) * 0x10 + hexDigit(text.charCodeAt(6))) / 255.0, + alpha: (hexDigit(text.charCodeAt(7)) * 0x10 + hexDigit(text.charCodeAt(8))) / 255.0 + }; + } + return null; +} + +export function colorFrom256RGB(red: number, green: number, blue: number, alpha: number = 1.0) { + return { + red: red / 255.0, + green: green / 255.0, + blue: blue / 255.0, + alpha + }; +} \ No newline at end of file diff --git a/src/languageservice/utils/json.ts b/src/languageservice/utils/json.ts new file mode 100644 index 0000000..2d3aaf2 --- /dev/null +++ b/src/languageservice/utils/json.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ +'use strict'; + +export function stringifyObject(obj: any, indent: string, stringifyLiteral: (val: any) => string) : string { + if (obj !== null && typeof obj === 'object') { + let newIndent = indent + '\t'; + if (Array.isArray(obj)) { + if (obj.length === 0) { + return '[]'; + } + let result = '[\n'; + for (let i = 0; i < obj.length; i++) { + result += newIndent + stringifyObject(obj[i], newIndent, stringifyLiteral); + if (i < obj.length - 1) { + result += ','; + } + result += '\n'; + } + result += indent + ']'; + return result; + } else { + let keys = Object.keys(obj); + if (keys.length === 0) { + return '{}'; + } + let result = '{\n'; + for (let i = 0; i < keys.length; i++) { + let key = keys[i]; + + result += newIndent + JSON.stringify(key) + ': ' + stringifyObject(obj[key], newIndent, stringifyLiteral); + if (i < keys.length - 1) { + result += ','; + } + result += '\n'; + } + result += indent + '}'; + return result; + } + } + return stringifyLiteral(obj); +} \ No newline at end of file diff --git a/src/languageservice/utils/objects.ts b/src/languageservice/utils/objects.ts index 33cfde0..5eeb051 100644 --- a/src/languageservice/utils/objects.ts +++ b/src/languageservice/utils/objects.ts @@ -55,4 +55,20 @@ export function equals(one: any, other: any): boolean { } } return true; -} \ No newline at end of file +} + +export function isNumber(val: any): val is number { + return typeof val === 'number'; +} + +export function isDefined(val: any): val is object { + return typeof val !== 'undefined'; +} + +export function isBoolean(val: any): val is boolean { + return typeof val === 'boolean'; +} + +export function isString(val: any): val is string { + return typeof val === 'string'; +} diff --git a/src/languageservice/utils/strings.ts b/src/languageservice/utils/strings.ts index 2050772..25ce683 100644 --- a/src/languageservice/utils/strings.ts +++ b/src/languageservice/utils/strings.ts @@ -38,10 +38,14 @@ export function convertSimple2RegExp(pattern: string): RegExp { : convertGlobalPattern2RegExp(pattern) } +export function convertSimple2RegExpPattern(pattern: string): string { + return pattern.replace(/[\-\\\{\}\+\?\|\^\$\.\,\[\]\(\)\#\s]/g, '\\$&').replace(/[\*]/g, '.*'); +} + function convertGlobalPattern2RegExp(pattern: string): RegExp { return new RegExp(pattern.replace(/[\-\\\{\}\+\?\|\^\$\.\,\[\]\(\)\#\s]/g, '\\$&').replace(/[\*]/g, '.*') + '$'); } function convertRegexString2RegExp(pattern: string, flag: string): RegExp { return new RegExp(pattern, flag); -} \ No newline at end of file +} diff --git a/src/languageservice/yamlLanguageService.ts b/src/languageservice/yamlLanguageService.ts index f4fbc17..4cddd5b 100644 --- a/src/languageservice/yamlLanguageService.ts +++ b/src/languageservice/yamlLanguageService.ts @@ -5,15 +5,16 @@ *--------------------------------------------------------------------------------------------*/ import { JSONSchemaService, CustomSchemaProvider } from './services/jsonSchemaService' -import { TextDocument, Position, CompletionList, Diagnostic, FormattingOptions } from 'vscode-languageserver-types'; +import { TextDocument, Position, CompletionList, Diagnostic, FormattingOptions, DocumentSymbol, CompletionItem, Color, ColorInformation, ColorPresentation, Range } from 'vscode-languageserver-types'; import { JSONSchema } from './jsonSchema'; import { YAMLDocumentSymbols } from './services/documentSymbols'; import { YAMLCompletion } from './services/yamlCompletion'; import { YAMLHover } from './services/yamlHover'; import { YAMLValidation } from './services/yamlValidation'; import { format } from './services/yamlFormatter'; -import { JSONDocument, JSONWorkerContribution } from 'vscode-json-languageservice'; -import { parse as parseYAML } from "./parser/yamlParser"; +import { parse as parseYAML } from './parser/yamlParser'; +import { JSONWorkerContribution } from './jsonContributions'; +import { YAMLDocument } from './yamlLanguageTypes'; export interface LanguageSettings { validate?: boolean; //Setting for whether we want to validate the schema @@ -24,54 +25,54 @@ export interface LanguageSettings { customTags?: Array; //Array of Custom Tags } -export type YAMLDocument = { documents: JSONDocument[] }; - export interface Thenable { - /** - * Attaches callbacks for the resolution and/or rejection of the Promise. - * @param onfulfilled The callback to execute when the Promise is resolved. - * @param onrejected The callback to execute when the Promise is rejected. - * @returns A Promise for the completion of which ever callback is executed. - */ - then(onfulfilled?: (value: R) => TResult | Thenable, onrejected?: (reason: any) => TResult | Thenable): Thenable; - then(onfulfilled?: (value: R) => TResult | Thenable, onrejected?: (reason: any) => void): Thenable; + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: (value: R) => TResult | Thenable, onrejected?: (reason: any) => TResult | Thenable): Thenable; + then(onfulfilled?: (value: R) => TResult | Thenable, onrejected?: (reason: any) => void): Thenable; } export interface WorkspaceContextService { - resolveRelativePath(relativePath: string, resource: string): string; + resolveRelativePath(relativePath: string, resource: string): string; } /** * The schema request service is used to fetch schemas. The result should the schema file comment, or, * in case of an error, a displayable error string */ export interface SchemaRequestService { - (uri: string): Thenable; + (uri: string): Thenable; } export interface SchemaConfiguration { /** * The URI of the schema, which is also the identifier of the schema. */ - uri: string; + uri: string; /** * A list of file names that are associated to the schema. The '*' wildcard can be used. For example '*.schema.json', 'package.json' */ - fileMatch?: string[]; + fileMatch?: string[]; /** * The schema for the given URI. * If no schema is provided, the schema will be fetched with the schema request service (if available). */ - schema?: JSONSchema; + schema?: JSONSchema; } export interface LanguageService { configure(settings: LanguageSettings): void; registerCustomSchemaProvider(schemaProvider: CustomSchemaProvider): void; // Register a custom schema provider - doComplete(document: TextDocument, position: Position, doc): Thenable; - doValidation(document: TextDocument, yamlDocument): Thenable; - doHover(document: TextDocument, position: Position, doc); - findDocumentSymbols(document: TextDocument, doc); - doResolve(completionItem); + doComplete(document: TextDocument, position: Position, doc: YAMLDocument): Thenable; + doValidation(document: TextDocument, yamlDocument: YAMLDocument): Thenable; + doHover(document: TextDocument, position: Position, doc: YAMLDocument); + findDocumentSymbols(document: TextDocument, doc: YAMLDocument): DocumentSymbol[]; + findDocumentColors(document: TextDocument, doc: YAMLDocument): Thenable; + getColorPresentations(document: TextDocument, doc: YAMLDocument, color: Color, range: Range): ColorPresentation[]; + doResolve(completionItem: CompletionItem): Thenable; resetSchema(uri: string): boolean; doFormat(document: TextDocument, options: FormattingOptions, customTags: Array); parseYAMLDocument(document: TextDocument): YAMLDocument; @@ -82,32 +83,30 @@ export function getLanguageService(schemaRequestService: SchemaRequestService, w let completer = new YAMLCompletion(schemaService, contributions); let hover = new YAMLHover(schemaService, contributions); - let yamlDocumentSymbols = new YAMLDocumentSymbols(); + let yamlDocumentSymbols = new YAMLDocumentSymbols(schemaService); let yamlValidation = new YAMLValidation(schemaService); return { - configure: (settings) => { - schemaService.clearExternalSchemas(); - if (settings.schemas) { - settings.schemas.forEach(settings => { - schemaService.registerExternalSchema(settings.uri, settings.fileMatch, settings.schema); - }); - } - yamlValidation.configure(settings); - hover.configure(settings); - let customTagsSetting = settings && settings['customTags'] ? settings['customTags'] : []; - completer.configure(settings, customTagsSetting); - }, - registerCustomSchemaProvider: (schemaProvider: CustomSchemaProvider) => { - schemaService.registerCustomSchemaProvider(schemaProvider); - }, - doComplete: completer.doComplete.bind(completer), - doResolve: completer.doResolve.bind(completer), - doValidation: yamlValidation.doValidation.bind(yamlValidation), - doHover: hover.doHover.bind(hover), - findDocumentSymbols: yamlDocumentSymbols.findDocumentSymbols.bind(yamlDocumentSymbols), - resetSchema: (uri: string) => schemaService.onResourceChange(uri), - doFormat: format, - parseYAMLDocument: (document: TextDocument) => parseYAML(document.getText()), + configure: (settings) => { + schemaService.clearExternalSchemas(); + if (settings.schemas) { + settings.schemas.forEach(settings => { + schemaService.registerExternalSchema(settings.uri, settings.fileMatch, settings.schema); + }); + } + }, + registerCustomSchemaProvider: (schemaProvider: CustomSchemaProvider) => { + schemaService.registerCustomSchemaProvider(schemaProvider); + }, + doComplete: completer.doComplete.bind(completer), + doResolve: completer.doResolve.bind(completer), + doValidation: yamlValidation.doValidation.bind(yamlValidation), + doHover: hover.doHover.bind(hover), + findDocumentSymbols: yamlDocumentSymbols.findDocumentSymbols.bind(yamlDocumentSymbols), + findDocumentColors: yamlDocumentSymbols.findDocumentColors.bind(yamlDocumentSymbols), + getColorPresentations: yamlDocumentSymbols.getColorPresentations.bind(yamlDocumentSymbols), + resetSchema: (uri: string) => schemaService.onResourceChange(uri), + doFormat: format, + parseYAMLDocument: (document: TextDocument) => parseYAML(document.getText()), } } diff --git a/src/languageservice/yamlLanguageTypes.ts b/src/languageservice/yamlLanguageTypes.ts new file mode 100644 index 0000000..00d8366 --- /dev/null +++ b/src/languageservice/yamlLanguageTypes.ts @@ -0,0 +1,37 @@ +import { JSONDocument } from './parser/jsonParser'; +import { ASTNode } from './jsonLanguageTypes'; + +export class SingleYAMLDocument extends JSONDocument { + public lines; + public errors; + public warnings; + + constructor(lines: number[]) { + super(null, []); + this.lines = lines; + this.errors = []; + this.warnings = []; + } + + public getSchemas(schema, doc, node) { + let matchingSchemas = []; + doc.validate(schema, matchingSchemas, node.start); + return matchingSchemas; + } + + public getNodeFromOffset(offset: number, includeRightBound = false): ASTNode { + return super.getNodeFromOffset(offset, includeRightBound); + } +} + +export class YAMLDocument { + public documents: SingleYAMLDocument[] + public errors; + public warnings; + + constructor(documents: SingleYAMLDocument[]) { + this.documents = documents; + this.errors = []; + this.warnings = []; + } +} diff --git a/src/yamlMode.ts b/src/yamlMode.ts index 6001336..8537627 100644 --- a/src/yamlMode.ts +++ b/src/yamlMode.ts @@ -26,13 +26,14 @@ export function setupMode(defaults: LanguageServiceDefaultsImpl): void { let languageId = defaults.languageId; - disposables.push(monaco.languages.registerCompletionItemProvider(languageId, new languageFeatures.CompletionAdapter(worker))); + // TODO: + // disposables.push(monaco.languages.registerCompletionItemProvider(languageId, new languageFeatures.CompletionAdapter(worker))); disposables.push(monaco.languages.registerHoverProvider(languageId, new languageFeatures.HoverAdapter(worker))); - disposables.push(monaco.languages.registerDocumentSymbolProvider(languageId, new languageFeatures.DocumentSymbolAdapter(worker))); + disposables.push(monaco.languages.registerDocumentSymbolProvider(languageId, new languageFeatures.DocumentSymbolAdapter(worker))); + disposables.push(monaco.languages.registerColorProvider(languageId, new languageFeatures.DocumentColorAdapter(worker))); disposables.push(monaco.languages.registerDocumentFormattingEditProvider(languageId, new languageFeatures.DocumentFormattingEditProvider(worker))); disposables.push(monaco.languages.registerDocumentRangeFormattingEditProvider(languageId, new languageFeatures.DocumentRangeFormattingEditProvider(worker))); disposables.push(new languageFeatures.DiagnosticsAdapter(languageId, worker, defaults)); - // disposables.push(monaco.languages.setTokensProvider(languageId, createTokenizationSupport(true))); disposables.push(monaco.languages.setLanguageConfiguration(languageId, richEditConfiguration)); } diff --git a/src/yamlWorker.ts b/src/yamlWorker.ts index 3145651..e0e33e5 100644 --- a/src/yamlWorker.ts +++ b/src/yamlWorker.ts @@ -63,11 +63,23 @@ export class YAMLWorker { resetSchema(uri: string): Thenable { return Promise.as(this._languageService.resetSchema(uri)); } - findDocumentSymbols(uri: string): Thenable { + findDocumentSymbols(uri: string): Thenable { let document = this._getTextDocument(uri); let yamlDocument = this._languageService.parseYAMLDocument(document); let symbols = this._languageService.findDocumentSymbols(document, yamlDocument); return Promise.as(symbols); + } + findDocumentColors(uri: string): Thenable { + let document = this._getTextDocument(uri); + let stylesheet = this._languageService.parseYAMLDocument(document); + let colorSymbols = this._languageService.findDocumentColors(document, stylesheet); + return Promise.as(colorSymbols); + } + getColorPresentations(uri: string, color: ls.Color, range: ls.Range): Thenable { + let document = this._getTextDocument(uri); + let stylesheet = this._languageService.parseYAMLDocument(document); + let colorPresentations = this._languageService.getColorPresentations(document, stylesheet, color, range); + return Promise.as(colorPresentations); } private _getTextDocument(uri: string): ls.TextDocument { let models = this._ctx.getMirrorModels(); @@ -88,4 +100,4 @@ export interface ICreateData { export function create(ctx: IWorkerContext, createData: ICreateData): YAMLWorker { return new YAMLWorker(ctx, createData); -} \ No newline at end of file +} diff --git a/test/index.html b/test/index.html index 6ae731a..475733c 100644 --- a/test/index.html +++ b/test/index.html @@ -1,90 +1,90 @@ - - - - - - - - - - - -

Monaco Editor YAML test page

- -
- - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + +

Monaco Editor YAML test page

+ +
+ + + + + + + + + + + diff --git a/yarn.lock b/yarn.lock index 27bc91b..7954c44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14,6 +14,13 @@ version "10.9.3" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.9.3.tgz#85f288502503ade0b3bfc049fe1777b05d0327d5" +agent-base@4, agent-base@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" + integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg== + dependencies: + es6-promisify "^5.0.0" + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -39,6 +46,32 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" +debug@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@^3.1.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +es6-promise@^4.0.3: + version "4.2.5" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.5.tgz#da6d0d5692efb461e082c14817fe2427d8f5d054" + integrity sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" @@ -58,6 +91,22 @@ glob@^7.0.5: once "^1.3.0" path-is-absolute "^1.0.0" +http-proxy-agent@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" + integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg== + dependencies: + agent-base "4" + debug "3.1.0" + +https-proxy-agent@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0" + integrity sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ== + dependencies: + agent-base "^4.1.0" + debug "^3.1.0" + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -76,7 +125,12 @@ js-yaml@^3.12.0: argparse "^1.0.7" esprima "^4.0.0" -jsonc-parser@^2.0.1, jsonc-parser@^2.0.2: +jsonc-parser@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-1.0.3.tgz#1d53d7160e401a783dbceabaad82473f80e6ad7e" + integrity sha512-hk/69oAeaIzchq/v3lS50PXuzn5O2ynldopMC+SWBql7J2WtdptfB9dy8Y7+Og5rPkTCpn83zTiO8FMcqlXJ/g== + +jsonc-parser@^2.0.0-next.1, jsonc-parser@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.0.2.tgz#42fcf56d70852a043fadafde51ddb4a85649978d" @@ -102,6 +156,16 @@ monaco-plugin-helpers@^1.0.2: dependencies: typescript "^2.7.2" +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -112,6 +176,20 @@ path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" +prettier@^1.14.3: + version "1.15.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.15.2.tgz#d31abe22afa4351efa14c7f8b94b58bb7452205e" + integrity sha512-YgPLFFA0CdKL4Eg2IHtUSjzj/BWgszDHiNQAe0VAIBse34148whfdzLagRL+QiKS+YfK5ftB6X4v/MBw8yCoug== + +request-light@^0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.2.4.tgz#3cea29c126682e6bcadf7915353322eeba01a755" + integrity sha512-pM9Fq5jRnSb+82V7M97rp8FE9/YNeP2L9eckB4Szd7lyeclSIx02aIpPO/6e4m6Dy31+FBN/zkFMTd2HkNO3ow== + dependencies: + http-proxy-agent "^2.1.0" + https-proxy-agent "^2.2.1" + vscode-nls "^4.0.0" + requirejs@^2.3.5: version "2.3.6" resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.6.tgz#e5093d9601c2829251258c0b9445d4d19fa9e7c9" @@ -146,27 +224,86 @@ uglify-es@^3.3.9: commander "~2.13.0" source-map "~0.6.1" -vscode-json-languageservice@^3.1.6: - version "3.1.6" - resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-3.1.6.tgz#272e21eb9abcefe6c1ed38be141f0a76d5ddf0cd" +vscode-json-languageservice@3.0.12: + version "3.0.12" + resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-3.0.12.tgz#85258632f2f7718028fbdfbb95b4ad009107b821" + integrity sha512-XSgRVY/vsPqOa//ZwLD5DWx1wzTQGgeZfsOlVqFlLya10dpimSnd27kbuL45hzxh4B+MvmHZtZeWQKjSYnNF0A== dependencies: - jsonc-parser "^2.0.1" - vscode-languageserver-types "^3.12.0" - vscode-nls "^3.2.4" - vscode-uri "^1.0.6" + jsonc-parser "^2.0.0-next.1" + vscode-languageserver-types "^3.6.1" + vscode-nls "^3.2.1" + vscode-uri "^1.0.3" -vscode-languageserver-types@3.12.0, vscode-languageserver-types@^3.12.0: +vscode-jsonrpc@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-4.0.0.tgz#a7bf74ef3254d0a0c272fab15c82128e378b3be9" + integrity sha512-perEnXQdQOJMTDFNv+UF3h1Y0z4iSiaN9jIlb0OqIYgosPCZGYh/MCUlkFtV2668PL69lRDO32hmvL2yiidUYg== + +vscode-languageserver-protocol@^3.10.3: + version "3.13.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.13.0.tgz#710d8e42119bb3affb1416e1e104bd6b4d503595" + integrity sha512-2ZGKwI+P2ovQll2PGAp+2UfJH+FK9eait86VBUdkPd9HRlm8e58aYT9pV/NYanHOcp3pL6x2yTLVCFMcTer0mg== + dependencies: + vscode-jsonrpc "^4.0.0" + vscode-languageserver-types "3.13.0" + +vscode-languageserver-types@3.12.0: version "3.12.0" resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.12.0.tgz#f96051381b6a050b7175b37d6cb5d2f2eb64b944" + integrity sha512-UxqnpzBToPO7Mi2tr/s5JeyPOSKSJtLB8lIdxCg9ZNdvP2cU8wS7iTDtwQKz91Ne4CUmTdf85ddR5SIZKXmMjQ== -vscode-nls@^3.2.4: +vscode-languageserver-types@3.13.0, vscode-languageserver-types@^3.6.1: + version "3.13.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.13.0.tgz#b704b024cef059f7b326611c99b9c8753c0a18b4" + integrity sha512-BnJIxS+5+8UWiNKCP7W3g9FlE7fErFw0ofP5BXJe7c2tl0VeWh+nNHFbwAS2vmVC4a5kYxHBjRy0UeOtziemVA== + +vscode-languageserver@^4.0.0: + version "4.4.2" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-4.4.2.tgz#600ae9cc7a6ff1e84d93c7807840c2cb5b22821b" + integrity sha512-61y8Raevi9EigDgg9NelvT9cUAohiEbUl1LOwQQgOCAaNX62yKny/ddi0uC+FUTm4CzsjhBu+06R+vYgfCYReA== + dependencies: + vscode-languageserver-protocol "^3.10.3" + vscode-uri "^1.0.5" + +vscode-nls@^3.2.1, vscode-nls@^3.2.2: version "3.2.5" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.5.tgz#25520c1955108036dec607c85e00a522f247f1a4" -vscode-uri@^1.0.6: +vscode-nls@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002" + integrity sha512-qCfdzcH+0LgQnBpZA53bA32kzp9rpq/f66Som577ObeuDlFIrtbEJ+A/+CCxjIh4G8dpJYNCKIsxpRAHIfsbNw== + +vscode-uri@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.3.tgz#631bdbf716dccab0e65291a8dc25c23232085a52" + integrity sha1-Yxvb9xbcyrDmUpGo3CXCMjIIWlI= + +vscode-uri@^1.0.3, vscode-uri@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.6.tgz#6b8f141b0bbc44ad7b07e94f82f168ac7608ad4d" wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +yaml-ast-parser@0.0.40: + version "0.0.40" + resolved "https://registry.yarnpkg.com/yaml-ast-parser/-/yaml-ast-parser-0.0.40.tgz#08536d4e73d322b1c9ce207ab8dd70e04d20ae6e" + integrity sha1-CFNtTnPTIrHJziB6uN1w4E0grm4= + +yaml-language-server@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yaml-language-server/-/yaml-language-server-0.1.0.tgz#ab3aff5c601ae33e03dc08c0960fd7c70296e635" + integrity sha512-2sA9eMgBnvEnjcQVIxbakIkiL8PQ6xJw1yXfdiVRn8ms+dI3EuaaatoUuIPhYnwmDOeaobDwkJs1bpQvFO4nqQ== + dependencies: + js-yaml "^3.12.0" + jsonc-parser "^1.0.3" + prettier "^1.14.3" + request-light "^0.2.3" + vscode-json-languageservice "3.0.12" + vscode-languageserver "^4.0.0" + vscode-languageserver-types "^3.6.1" + vscode-nls "^3.2.2" + vscode-uri "1.0.3" + yaml-ast-parser "0.0.40"