diff --git a/src/languageserver/handlers/languageHandlers.ts b/src/languageserver/handlers/languageHandlers.ts index 0f215fe3..49046f2f 100644 --- a/src/languageserver/handlers/languageHandlers.ts +++ b/src/languageserver/handlers/languageHandlers.ts @@ -16,8 +16,9 @@ import { FoldingRangeParams, Connection, TextDocumentPositionParams, + CodeLensParams, } from 'vscode-languageserver'; -import { DocumentSymbol, Hover, SymbolInformation, TextEdit } from 'vscode-languageserver-types'; +import { CodeLens, DocumentSymbol, Hover, SymbolInformation, TextEdit } from 'vscode-languageserver-types'; import { isKubernetesAssociatedDocument } from '../../languageservice/parser/isKubernetes'; import { LanguageService } from '../../languageservice/yamlLanguageService'; import { SettingsState } from '../../yamlSettings'; @@ -49,6 +50,8 @@ export class LanguageHandlers { this.connection.onFoldingRanges((params) => this.foldingRangeHandler(params)); this.connection.onCodeAction((params) => this.codeActionHandler(params)); this.connection.onDocumentOnTypeFormatting((params) => this.formatOnTypeHandler(params)); + this.connection.onCodeLens((params) => this.codeLensHandler(params)); + this.connection.onCodeLensResolve((params) => this.codeLensResolveHandler(params)); } documentLinkHandler(params: DocumentLinkParams): Promise { @@ -177,4 +180,16 @@ export class LanguageHandlers { return this.languageService.getCodeAction(textDocument, params); } + + codeLensHandler(params: CodeLensParams): Thenable | CodeLens[] | undefined { + const textDocument = this.yamlSettings.documents.get(params.textDocument.uri); + if (!textDocument) { + return; + } + return this.languageService.getCodeLens(textDocument, params); + } + + codeLensResolveHandler(param: CodeLens): Thenable | CodeLens { + return this.languageService.resolveCodeLens(param); + } } diff --git a/src/languageservice/parser/yaml-documents.ts b/src/languageservice/parser/yaml-documents.ts new file mode 100644 index 00000000..d319802a --- /dev/null +++ b/src/languageservice/parser/yaml-documents.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TextDocument } from 'vscode-languageserver-textdocument'; +import { YAMLDocument, parse as parseYAML } from './yamlParser07'; + +interface YamlCachedDocument { + version: number; + document: YAMLDocument; +} +export class YamlDocuments { + // a mapping of URIs to cached documents + private cache = new Map(); + + /** + * Get cached YAMLDocument + * @param document TextDocument to parse + * @param customTags YAML custom tags + * @param addRootObject if true and document is empty add empty object {} to force schema usage + * @returns the YAMLDocument + */ + getYamlDocument(document: TextDocument, customTags: string[] = [], addRootObject = false): YAMLDocument { + this.ensureCache(document, customTags, addRootObject); + return this.cache.get(document.uri).document; + } + + /** + * For test purpose only! + */ + clear(): void { + this.cache.clear(); + } + + private ensureCache(document: TextDocument, customTags: string[], addRootObject: boolean): void { + const key = document.uri; + if (!this.cache.has(key)) { + this.cache.set(key, { version: -1, document: new YAMLDocument([]) }); + } + + if (this.cache.get(key).version !== document.version) { + let text = document.getText(); + // if text is contains only whitespace wrap all text in object to force schema selection + if (addRootObject && !/\S/.test(text)) { + text = `{${text}}`; + } + const doc = parseYAML(text, customTags); + this.cache.get(key).document = doc; + this.cache.get(key).version = document.version; + } + } +} + +export const yamlDocumentsCache = new YamlDocuments(); diff --git a/src/languageservice/services/documentSymbols.ts b/src/languageservice/services/documentSymbols.ts index 504f662a..52d451ab 100644 --- a/src/languageservice/services/documentSymbols.ts +++ b/src/languageservice/services/documentSymbols.ts @@ -5,11 +5,11 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { parse as parseYAML } from '../parser/yamlParser07'; - -import { SymbolInformation, TextDocument, DocumentSymbol } from 'vscode-languageserver-types'; +import { SymbolInformation, DocumentSymbol } from 'vscode-languageserver-types'; import { YAMLSchemaService } from './yamlSchemaService'; import { JSONDocumentSymbols } from 'vscode-json-languageservice/lib/umd/services/jsonDocumentSymbols'; +import { TextDocument } from 'vscode-languageserver-textdocument'; +import { yamlDocumentsCache } from '../parser/yaml-documents'; export class YAMLDocumentSymbols { private jsonDocumentSymbols; @@ -30,7 +30,7 @@ export class YAMLDocumentSymbols { } public findDocumentSymbols(document: TextDocument): SymbolInformation[] { - const doc = parseYAML(document.getText()); + const doc = yamlDocumentsCache.getYamlDocument(document); if (!doc || doc['documents'].length === 0) { return null; } @@ -46,7 +46,7 @@ export class YAMLDocumentSymbols { } public findHierarchicalDocumentSymbols(document: TextDocument): DocumentSymbol[] { - const doc = parseYAML(document.getText()); + const doc = yamlDocumentsCache.getYamlDocument(document); if (!doc || doc['documents'].length === 0) { return null; } diff --git a/src/languageservice/services/yamlCodeActions.ts b/src/languageservice/services/yamlCodeActions.ts index abb2e022..a25a76e8 100644 --- a/src/languageservice/services/yamlCodeActions.ts +++ b/src/languageservice/services/yamlCodeActions.ts @@ -10,7 +10,6 @@ import { CodeActionKind, CodeActionParams, Command, - Connection, Diagnostic, Position, Range, @@ -19,7 +18,6 @@ import { } from 'vscode-languageserver'; import { YamlCommands } from '../../commands'; import * as path from 'path'; -import { CommandExecutor } from '../../languageserver/commandExecutor'; import { TextBuffer } from '../utils/textBuffer'; import { LanguageSettings } from '../yamlLanguageService'; @@ -29,21 +27,7 @@ interface YamlDiagnosticData { export class YamlCodeActions { private indentation = ' '; - constructor(commandExecutor: CommandExecutor, connection: Connection, private readonly clientCapabilities: ClientCapabilities) { - commandExecutor.registerCommand(YamlCommands.JUMP_TO_SCHEMA, async (uri: string) => { - if (!uri) { - return; - } - if (!uri.startsWith('file')) { - uri = 'json-schema' + uri.substring(uri.indexOf('://'), uri.length); - } - - const result = await connection.window.showDocument({ uri: uri, external: false, takeFocus: true }); - if (!result) { - connection.window.showErrorMessage(`Cannot open ${uri}`); - } - }); - } + constructor(private readonly clientCapabilities: ClientCapabilities) {} configure(settings: LanguageSettings): void { this.indentation = settings.indentation; @@ -71,7 +55,7 @@ export class YamlCodeActions { for (const diagnostic of diagnostics) { const schemaUri = (diagnostic.data as YamlDiagnosticData)?.schemaUri || []; for (const schemaUriStr of schemaUri) { - if (schemaUriStr && (schemaUriStr.startsWith('file') || schemaUriStr.startsWith('https'))) { + if (schemaUriStr) { if (!schemaUriToDiagnostic.has(schemaUriStr)) { schemaUriToDiagnostic.set(schemaUriStr, []); } diff --git a/src/languageservice/services/yamlCodeLens.ts b/src/languageservice/services/yamlCodeLens.ts new file mode 100644 index 00000000..61e117d5 --- /dev/null +++ b/src/languageservice/services/yamlCodeLens.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TextDocument } from 'vscode-languageserver-textdocument'; +import { CodeLens, Range } from 'vscode-languageserver-types'; +import { YamlCommands } from '../../commands'; +import { yamlDocumentsCache } from '../parser/yaml-documents'; +import { YAMLSchemaService } from './yamlSchemaService'; +import { URI } from 'vscode-uri'; +import * as path from 'path'; +import { JSONSchema, JSONSchemaRef } from '../jsonSchema'; +import { CodeLensParams } from 'vscode-languageserver-protocol'; +import { isBoolean } from '../utils/objects'; + +export class YamlCodeLens { + constructor(private schemaService: YAMLSchemaService) {} + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getCodeLens(document: TextDocument, params: CodeLensParams): Promise { + const yamlDocument = yamlDocumentsCache.getYamlDocument(document); + const result = []; + for (const currentYAMLDoc of yamlDocument.documents) { + const schema = await this.schemaService.getSchemaForResource(document.uri, currentYAMLDoc); + if (schema?.schema) { + const schemaUrls = getSchemaUrl(schema?.schema); + if (schemaUrls.size === 0) { + continue; + } + for (const urlToSchema of schemaUrls) { + const lens = CodeLens.create(Range.create(0, 0, 0, 0)); + lens.command = { + title: getCommandTitle(urlToSchema[0], urlToSchema[1]), + command: YamlCommands.JUMP_TO_SCHEMA, + arguments: [urlToSchema[0]], + }; + result.push(lens); + } + } + } + + return result; + } + resolveCodeLens(param: CodeLens): Thenable | CodeLens { + return param; + } +} + +function getCommandTitle(url: string, schema: JSONSchema): string { + const uri = URI.parse(url); + let baseName = path.basename(uri.fsPath); + if (!path.extname(uri.fsPath)) { + baseName += '.json'; + } + if (Object.getOwnPropertyDescriptor(schema, 'name')) { + return Object.getOwnPropertyDescriptor(schema, 'name').value + ` (${baseName})`; + } else if (schema.title) { + return schema.title + ` (${baseName})`; + } + + return baseName; +} + +function getSchemaUrl(schema: JSONSchema): Map { + const result = new Map(); + if (!schema) { + return result; + } + const url = schema.url; + if (url) { + if (url.startsWith('schemaservice://combinedSchema/')) { + addSchemasForOf(schema, result); + } else { + result.set(schema.url, schema); + } + } else { + addSchemasForOf(schema, result); + } + return result; +} + +function addSchemasForOf(schema: JSONSchema, result: Map): void { + if (schema.allOf) { + addInnerSchemaUrls(schema.allOf, result); + } + if (schema.anyOf) { + addInnerSchemaUrls(schema.anyOf, result); + } + if (schema.oneOf) { + addInnerSchemaUrls(schema.oneOf, result); + } +} + +function addInnerSchemaUrls(schemas: JSONSchemaRef[], result: Map): void { + for (const subSchema of schemas) { + if (!isBoolean(subSchema)) { + if (subSchema.url && !result.has(subSchema.url)) { + result.set(subSchema.url, subSchema); + } + } + } +} diff --git a/src/languageservice/services/yamlCommands.ts b/src/languageservice/services/yamlCommands.ts new file mode 100644 index 00000000..01556ca9 --- /dev/null +++ b/src/languageservice/services/yamlCommands.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Connection } from 'vscode-languageserver/node'; +import { YamlCommands } from '../../commands'; +import { CommandExecutor } from '../../languageserver/commandExecutor'; +import { URI } from 'vscode-uri'; + +export function registerCommands(commandExecutor: CommandExecutor, connection: Connection): void { + commandExecutor.registerCommand(YamlCommands.JUMP_TO_SCHEMA, async (uri: string) => { + if (!uri) { + return; + } + if (!uri.startsWith('file')) { + const origUri = URI.parse(uri); + const customUri = URI.from({ + scheme: 'json-schema', + authority: origUri.authority, + path: origUri.path.endsWith('.json') ? origUri.path : origUri.path + '.json', + fragment: uri, + }); + uri = customUri.toString(); + } + + const result = await connection.window.showDocument({ uri: uri, external: false, takeFocus: true }); + if (!result) { + connection.window.showErrorMessage(`Cannot open ${uri}`); + } + }); +} diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index ff5b4ca4..faf6724f 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -15,12 +15,12 @@ import { CompletionItem, CompletionItemKind, CompletionList, - TextDocument, Position, Range, TextEdit, InsertTextFormat, } from 'vscode-languageserver-types'; +import { TextDocument } from 'vscode-languageserver-textdocument'; import * as nls from 'vscode-nls'; import { getLineOffsets, filterInvalidCustomTags, matchOffsetToDocument } from '../utils/arrUtils'; import { LanguageSettings } from '../yamlLanguageService'; diff --git a/src/languageservice/services/yamlFolding.ts b/src/languageservice/services/yamlFolding.ts index 61b98199..af361c68 100644 --- a/src/languageservice/services/yamlFolding.ts +++ b/src/languageservice/services/yamlFolding.ts @@ -2,17 +2,18 @@ * Copyright (c) Red Hat, Inc. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TextDocument, FoldingRange, Range } from 'vscode-languageserver'; +import { FoldingRange, Range } from 'vscode-languageserver'; import { FoldingRangesContext } from '../yamlTypes'; -import { parse as parseYAML } from '../parser/yamlParser07'; import { ASTNode } from '../jsonASTTypes'; +import { yamlDocumentsCache } from '../parser/yaml-documents'; +import { TextDocument } from 'vscode-languageserver-textdocument'; export function getFoldingRanges(document: TextDocument, context: FoldingRangesContext): FoldingRange[] | undefined { if (!document) { return; } const result: FoldingRange[] = []; - const doc = parseYAML(document.getText()); + const doc = yamlDocumentsCache.getYamlDocument(document); for (const ymlDoc of doc.documents) { ymlDoc.visit((node) => { if ( diff --git a/src/languageservice/services/yamlHover.ts b/src/languageservice/services/yamlHover.ts index 9e0da448..20173502 100644 --- a/src/languageservice/services/yamlHover.ts +++ b/src/languageservice/services/yamlHover.ts @@ -8,11 +8,11 @@ import { Hover, Position } from 'vscode-languageserver-types'; import { matchOffsetToDocument } from '../utils/arrUtils'; import { LanguageSettings } from '../yamlLanguageService'; -import { parse as parseYAML } from '../parser/yamlParser07'; import { YAMLSchemaService } from './yamlSchemaService'; import { JSONHover } from 'vscode-json-languageservice/lib/umd/services/jsonHover'; import { setKubernetesParserOption } from '../parser/isKubernetes'; import { TextDocument } from 'vscode-languageserver-textdocument'; +import { yamlDocumentsCache } from '../parser/yaml-documents'; export class YAMLHover { private shouldHover: boolean; @@ -33,7 +33,7 @@ export class YAMLHover { if (!this.shouldHover || !document) { return Promise.resolve(undefined); } - const doc = parseYAML(document.getText()); + const doc = yamlDocumentsCache.getYamlDocument(document); const offset = document.offsetAt(position); const currentDoc = matchOffsetToDocument(offset, doc); if (currentDoc === null) { diff --git a/src/languageservice/services/yamlLinks.ts b/src/languageservice/services/yamlLinks.ts index 608bbdd3..fd8ecc7a 100644 --- a/src/languageservice/services/yamlLinks.ts +++ b/src/languageservice/services/yamlLinks.ts @@ -2,13 +2,13 @@ * Copyright (c) Red Hat, Inc. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { parse as parseYAML } from '../parser/yamlParser07'; import { findLinks as JSONFindLinks } from 'vscode-json-languageservice/lib/umd/services/jsonLinks'; import { DocumentLink } from 'vscode-languageserver'; import { TextDocument } from 'vscode-languageserver-textdocument'; +import { yamlDocumentsCache } from '../parser/yaml-documents'; export function findLinks(document: TextDocument): Promise { - const doc = parseYAML(document.getText()); + const doc = yamlDocumentsCache.getYamlDocument(document); // Find links across all YAML Documents then report them back once finished const linkPromises = []; for (const yamlDoc of doc.documents) { diff --git a/src/languageservice/services/yamlValidation.ts b/src/languageservice/services/yamlValidation.ts index 0c1c3557..3224c629 100644 --- a/src/languageservice/services/yamlValidation.ts +++ b/src/languageservice/services/yamlValidation.ts @@ -7,7 +7,7 @@ import { Diagnostic, Position } from 'vscode-languageserver'; import { LanguageSettings } from '../yamlLanguageService'; -import { parse as parseYAML, YAMLDocument } from '../parser/yamlParser07'; +import { YAMLDocument } from '../parser/yamlParser07'; import { SingleYAMLDocument } from '../parser/yamlParser07'; import { YAMLSchemaService } from './yamlSchemaService'; import { YAMLDocDiagnostic } from '../utils/parseUtils'; @@ -15,6 +15,7 @@ import { TextDocument } from 'vscode-languageserver-textdocument'; import { JSONValidation } from 'vscode-json-languageservice/lib/umd/services/jsonValidation'; import { YAML_SOURCE } from '../parser/jsonParser07'; import { TextBuffer } from '../utils/textBuffer'; +import { yamlDocumentsCache } from '../parser/yaml-documents'; /** * Convert a YAMLDocDiagnostic to a language server Diagnostic @@ -57,12 +58,7 @@ export class YAMLValidation { return Promise.resolve([]); } - let text = textDocument.getText(); - // if text is contains only whitespace wrap all text in object to force schema selection - if (!/\S/.test(text)) { - text = `{${text}}`; - } - const yamlDocument: YAMLDocument = parseYAML(text, this.customTags); + const yamlDocument: YAMLDocument = yamlDocumentsCache.getYamlDocument(textDocument, this.customTags, true); const validationResult = []; let index = 0; diff --git a/src/languageservice/yamlLanguageService.ts b/src/languageservice/yamlLanguageService.ts index ee377879..315fe979 100644 --- a/src/languageservice/yamlLanguageService.ts +++ b/src/languageservice/yamlLanguageService.ts @@ -20,6 +20,7 @@ import { DocumentSymbol, TextEdit, DocumentLink, + CodeLens, } from 'vscode-languageserver-types'; import { JSONSchema } from './jsonSchema'; import { YAMLDocumentSymbols } from './services/documentSymbols'; @@ -36,12 +37,15 @@ import { CodeAction, Connection, DocumentOnTypeFormattingParams, + CodeLensParams, } from 'vscode-languageserver/node'; import { getFoldingRanges } from './services/yamlFolding'; import { FoldingRangesContext } from './yamlTypes'; import { YamlCodeActions } from './services/yamlCodeActions'; import { commandExecutor } from '../languageserver/commandExecutor'; import { doDocumentOnTypeFormatting } from './services/yamlOnTypeFormatting'; +import { YamlCodeLens } from './services/yamlCodeLens'; +import { registerCommands } from './services/yamlCommands'; export enum SchemaPriority { SchemaStore = 1, @@ -127,6 +131,8 @@ export interface LanguageService { deleteSchemasWhole(schemaDeletions: SchemaDeletionsAll): void; getFoldingRanges(document: TextDocument, context: FoldingRangesContext): FoldingRange[] | null; getCodeAction(document: TextDocument, params: CodeActionParams): CodeAction[] | undefined; + getCodeLens(document: TextDocument, params: CodeLensParams): Thenable | CodeLens[] | undefined; + resolveCodeLens(param: CodeLens): Thenable | CodeLens; } export function getLanguageService( @@ -141,8 +147,10 @@ export function getLanguageService( const yamlDocumentSymbols = new YAMLDocumentSymbols(schemaService); const yamlValidation = new YAMLValidation(schemaService); const formatter = new YAMLFormatter(); - const yamlCodeActions = new YamlCodeActions(commandExecutor, connection, clientCapabilities); - + const yamlCodeActions = new YamlCodeActions(clientCapabilities); + const yamlCodeLens = new YamlCodeLens(schemaService); + // register all commands + registerCommands(commandExecutor, connection); return { configure: (settings) => { schemaService.clearExternalSchemas(); @@ -195,5 +203,9 @@ export function getLanguageService( getCodeAction: (document, params) => { return yamlCodeActions.getCodeAction(document, params); }, + getCodeLens: (document, params) => { + return yamlCodeLens.getCodeLens(document, params); + }, + resolveCodeLens: (param) => yamlCodeLens.resolveCodeLens(param), }; } diff --git a/src/yamlServerInit.ts b/src/yamlServerInit.ts index 255f316a..6a40dce6 100644 --- a/src/yamlServerInit.ts +++ b/src/yamlServerInit.ts @@ -94,6 +94,9 @@ export class YAMLServerInit { // disabled until we not get parser which parse comments as separate nodes foldingRangeProvider: false, codeActionProvider: true, + codeLensProvider: { + resolveProvider: false, + }, executeCommandProvider: { commands: Object.keys(YamlCommands).map((k) => YamlCommands[k]), }, diff --git a/test/utils/testHelper.ts b/test/utils/testHelper.ts index 533e617c..353ee1cd 100644 --- a/test/utils/testHelper.ts +++ b/test/utils/testHelper.ts @@ -12,6 +12,7 @@ import { ValidationHandler } from '../../src/languageserver/handlers/validationH import { LanguageHandlers } from '../../src/languageserver/handlers/languageHandlers'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { ClientCapabilities } from 'vscode-json-languageservice'; +import { yamlDocumentsCache } from '../../src/languageservice/parser/yaml-documents'; export function toFsPath(str: unknown): string { if (typeof str !== 'string') { @@ -32,10 +33,12 @@ export const TEST_URI = 'file://~/Desktop/vscode-k8s/test.yaml'; export const SCHEMA_ID = 'default_schema_id.yaml'; export function setupTextDocument(content: string): TextDocument { + yamlDocumentsCache.clear(); // clear cache return TextDocument.create(TEST_URI, 'yaml', 0, content); } export function setupSchemaIDTextDocument(content: string, customSchemaID?: string): TextDocument { + yamlDocumentsCache.clear(); // clear cache if (customSchemaID) { return TextDocument.create(customSchemaID, 'yaml', 0, content); } else { diff --git a/test/yamlCodeActions.test.ts b/test/yamlCodeActions.test.ts index 5b6029ea..8ca6b15e 100644 --- a/test/yamlCodeActions.test.ts +++ b/test/yamlCodeActions.test.ts @@ -6,7 +6,6 @@ import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; import * as chai from 'chai'; -import { commandExecutor } from '../src/languageserver/commandExecutor'; import { YamlCodeActions } from '../src/languageservice/services/yamlCodeActions'; import { ClientCapabilities, @@ -14,7 +13,6 @@ import { CodeActionContext, CodeActionParams, Command, - Connection, Range, TextDocumentIdentifier, TextEdit, @@ -34,10 +32,8 @@ const JSON_SCHEMA2_LOCAL = 'file://some/path/schema2.json'; describe('CodeActions Tests', () => { const sandbox = sinon.createSandbox(); - let commandExecutorStub: sinon.SinonStub; let clientCapabilities: ClientCapabilities; beforeEach(() => { - commandExecutorStub = sandbox.stub(commandExecutor, 'registerCommand'); clientCapabilities = {}; }); @@ -46,25 +42,6 @@ describe('CodeActions Tests', () => { }); describe('JumpToSchema tests', () => { - it('should register handler for "JumpToSchema" command', () => { - new YamlCodeActions(commandExecutor, ({} as unknown) as Connection, clientCapabilities); - expect(commandExecutorStub).to.have.been.calledWithMatch(sinon.match('jumpToSchema'), sinon.match.func); - }); - - it('JumpToSchema handler should call "showDocument"', async () => { - const showDocumentStub = sandbox.stub(); - const connection = ({ - window: { - showDocument: showDocumentStub, - }, - } as unknown) as Connection; - showDocumentStub.resolves(true); - new YamlCodeActions(commandExecutor, connection, clientCapabilities); - const arg = commandExecutorStub.args[0]; - await arg[1](JSON_SCHEMA_LOCAL); - expect(showDocumentStub).to.have.been.calledWith({ uri: JSON_SCHEMA_LOCAL, external: false, takeFocus: true }); - }); - it('should not provide any actions if there are no diagnostics', () => { const doc = setupTextDocument(''); const params: CodeActionParams = { @@ -72,7 +49,7 @@ describe('CodeActions Tests', () => { range: undefined, textDocument: TextDocumentIdentifier.create(TEST_URI), }; - const actions = new YamlCodeActions(commandExecutor, ({} as unknown) as Connection, clientCapabilities); + const actions = new YamlCodeActions(clientCapabilities); const result = actions.getCodeAction(doc, params); expect(result).to.be.undefined; }); @@ -86,7 +63,7 @@ describe('CodeActions Tests', () => { textDocument: TextDocumentIdentifier.create(TEST_URI), }; clientCapabilities.window = { showDocument: { support: true } }; - const actions = new YamlCodeActions(commandExecutor, ({} as unknown) as Connection, clientCapabilities); + const actions = new YamlCodeActions(clientCapabilities); const result = actions.getCodeAction(doc, params); const codeAction = CodeAction.create( @@ -108,7 +85,7 @@ describe('CodeActions Tests', () => { textDocument: TextDocumentIdentifier.create(TEST_URI), }; clientCapabilities.window = { showDocument: { support: true } }; - const actions = new YamlCodeActions(commandExecutor, ({} as unknown) as Connection, clientCapabilities); + const actions = new YamlCodeActions(clientCapabilities); const result = actions.getCodeAction(doc, params); const codeAction = CodeAction.create( @@ -135,7 +112,7 @@ describe('CodeActions Tests', () => { range: undefined, textDocument: TextDocumentIdentifier.create(TEST_URI), }; - const actions = new YamlCodeActions(commandExecutor, ({} as unknown) as Connection, clientCapabilities); + const actions = new YamlCodeActions(clientCapabilities); const result = actions.getCodeAction(doc, params); expect(result).to.has.length(2); expect(result[0].title).to.be.equal('Convert Tab to Spaces'); @@ -151,7 +128,7 @@ describe('CodeActions Tests', () => { range: undefined, textDocument: TextDocumentIdentifier.create(TEST_URI), }; - const actions = new YamlCodeActions(commandExecutor, ({} as unknown) as Connection, clientCapabilities); + const actions = new YamlCodeActions(clientCapabilities); actions.configure({ indentation: ' ' } as LanguageSettings); const result = actions.getCodeAction(doc, params); @@ -167,7 +144,7 @@ describe('CodeActions Tests', () => { range: undefined, textDocument: TextDocumentIdentifier.create(TEST_URI), }; - const actions = new YamlCodeActions(commandExecutor, ({} as unknown) as Connection, clientCapabilities); + const actions = new YamlCodeActions(clientCapabilities); const result = actions.getCodeAction(doc, params); expect(result[1].title).to.be.equal('Convert all Tabs to Spaces'); diff --git a/test/yamlCodeLens.test.ts b/test/yamlCodeLens.test.ts new file mode 100644 index 00000000..7ae09f98 --- /dev/null +++ b/test/yamlCodeLens.test.ts @@ -0,0 +1,146 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import * as chai from 'chai'; +import { YamlCodeLens } from '../src/languageservice/services/yamlCodeLens'; +import { YAMLSchemaService } from '../src/languageservice/services/yamlSchemaService'; +import { setupTextDocument } from './utils/testHelper'; +import { JSONSchema } from '../src/languageservice/jsonSchema'; +import { CodeLens, Command, Range } from 'vscode-languageserver-protocol'; +import { YamlCommands } from '../src/commands'; + +const expect = chai.expect; +chai.use(sinonChai); + +describe('YAML CodeLens', () => { + const sandbox = sinon.createSandbox(); + let yamlSchemaService: sinon.SinonStubbedInstance; + + beforeEach(() => { + yamlSchemaService = sandbox.createStubInstance(YAMLSchemaService); + }); + + afterEach(() => { + sandbox.restore(); + }); + + function createCommand(title: string, arg: string): Command { + return { + title, + command: YamlCommands.JUMP_TO_SCHEMA, + arguments: [arg], + }; + } + + function createCodeLens(title: string, arg: string): CodeLens { + const lens = CodeLens.create(Range.create(0, 0, 0, 0)); + lens.command = createCommand(title, arg); + return lens; + } + + it('should provides CodeLens with jumpToSchema command', async () => { + const doc = setupTextDocument('foo: bar'); + const schema: JSONSchema = { + url: 'some://url/to/schema.json', + }; + yamlSchemaService.getSchemaForResource.resolves({ schema }); + const codeLens = new YamlCodeLens((yamlSchemaService as unknown) as YAMLSchemaService); + const result = await codeLens.getCodeLens(doc, { textDocument: { uri: doc.uri } }); + expect(result).is.not.empty; + expect(result[0].command).is.not.undefined; + expect(result[0].command).is.deep.equal(createCommand('schema.json', 'some://url/to/schema.json')); + }); + + it('should place CodeLens at beginning of the file and it has command', async () => { + const doc = setupTextDocument('foo: bar'); + const schema: JSONSchema = { + url: 'some://url/to/schema.json', + }; + yamlSchemaService.getSchemaForResource.resolves({ schema }); + const codeLens = new YamlCodeLens((yamlSchemaService as unknown) as YAMLSchemaService); + const result = await codeLens.getCodeLens(doc, { textDocument: { uri: doc.uri } }); + expect(result[0].range).is.deep.equal(Range.create(0, 0, 0, 0)); + expect(result[0].command).is.deep.equal(createCommand('schema.json', 'some://url/to/schema.json')); + }); + + it('command name should contains schema title', async () => { + const doc = setupTextDocument('foo: bar'); + const schema = { + url: 'some://url/to/schema.json', + title: 'fooBar', + } as JSONSchema; + yamlSchemaService.getSchemaForResource.resolves({ schema }); + const codeLens = new YamlCodeLens((yamlSchemaService as unknown) as YAMLSchemaService); + const result = await codeLens.getCodeLens(doc, { textDocument: { uri: doc.uri } }); + expect(result[0].command).is.deep.equal(createCommand('fooBar (schema.json)', 'some://url/to/schema.json')); + }); + + it('should provide lens for oneOf schemas', async () => { + const doc = setupTextDocument('foo: bar'); + const schema = { + oneOf: [ + { + url: 'some://url/schema1.json', + }, + { + url: 'some://url/schema2.json', + }, + ], + } as JSONSchema; + yamlSchemaService.getSchemaForResource.resolves({ schema }); + const codeLens = new YamlCodeLens((yamlSchemaService as unknown) as YAMLSchemaService); + const result = await codeLens.getCodeLens(doc, { textDocument: { uri: doc.uri } }); + expect(result).has.length(2); + expect(result).is.deep.equal([ + createCodeLens('schema1.json', 'some://url/schema1.json'), + createCodeLens('schema2.json', 'some://url/schema2.json'), + ]); + }); + + it('should provide lens for allOf schemas', async () => { + const doc = setupTextDocument('foo: bar'); + const schema = { + allOf: [ + { + url: 'some://url/schema1.json', + }, + { + url: 'some://url/schema2.json', + }, + ], + } as JSONSchema; + yamlSchemaService.getSchemaForResource.resolves({ schema }); + const codeLens = new YamlCodeLens((yamlSchemaService as unknown) as YAMLSchemaService); + const result = await codeLens.getCodeLens(doc, { textDocument: { uri: doc.uri } }); + expect(result).has.length(2); + expect(result).is.deep.equal([ + createCodeLens('schema1.json', 'some://url/schema1.json'), + createCodeLens('schema2.json', 'some://url/schema2.json'), + ]); + }); + + it('should provide lens for anyOf schemas', async () => { + const doc = setupTextDocument('foo: bar'); + const schema = { + anyOf: [ + { + url: 'some://url/schema1.json', + }, + { + url: 'some://url/schema2.json', + }, + ], + } as JSONSchema; + yamlSchemaService.getSchemaForResource.resolves({ schema }); + const codeLens = new YamlCodeLens((yamlSchemaService as unknown) as YAMLSchemaService); + const result = await codeLens.getCodeLens(doc, { textDocument: { uri: doc.uri } }); + expect(result).has.length(2); + expect(result).is.deep.equal([ + createCodeLens('schema1.json', 'some://url/schema1.json'), + createCodeLens('schema2.json', 'some://url/schema2.json'), + ]); + }); +}); diff --git a/test/yamlCommands.test.ts b/test/yamlCommands.test.ts new file mode 100644 index 00000000..7b4db254 --- /dev/null +++ b/test/yamlCommands.test.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import * as chai from 'chai'; +import { registerCommands } from '../src/languageservice/services/yamlCommands'; +import { commandExecutor } from '../src/languageserver/commandExecutor'; +import { Connection } from 'vscode-languageserver/node'; + +const expect = chai.expect; +chai.use(sinonChai); + +describe('Yaml Commands', () => { + const JSON_SCHEMA_LOCAL = 'file://some/path/schema.json'; + const sandbox = sinon.createSandbox(); + + let commandExecutorStub: sinon.SinonStub; + + beforeEach(() => { + commandExecutorStub = sandbox.stub(commandExecutor, 'registerCommand'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should register handler for "JumpToSchema" command', () => { + registerCommands(commandExecutor, {} as Connection); + expect(commandExecutorStub).to.have.been.calledWithMatch(sinon.match('jumpToSchema'), sinon.match.func); + }); + + it('JumpToSchema handler should call "showDocument"', async () => { + const showDocumentStub = sandbox.stub(); + const connection = ({ + window: { + showDocument: showDocumentStub, + }, + } as unknown) as Connection; + showDocumentStub.resolves(true); + registerCommands(commandExecutor, connection); + const arg = commandExecutorStub.args[0]; + await arg[1](JSON_SCHEMA_LOCAL); + expect(showDocumentStub).to.have.been.calledWith({ uri: JSON_SCHEMA_LOCAL, external: false, takeFocus: true }); + }); +});