diff --git a/src/languageserver/handlers/schemaSelectionHandlers.ts b/src/languageserver/handlers/schemaSelectionHandlers.ts index da958631..65b7504e 100644 --- a/src/languageserver/handlers/schemaSelectionHandlers.ts +++ b/src/languageserver/handlers/schemaSelectionHandlers.ts @@ -14,13 +14,13 @@ import { JSONSchemaDescription, JSONSchemaDescriptionExt, SchemaSelectionRequest export class JSONSchemaSelection { constructor( private readonly schemaService: YAMLSchemaService, - private readonly yamlSettings: SettingsState, - private readonly connection: Connection + private readonly yamlSettings?: SettingsState, + private readonly connection?: Connection ) { - this.connection.onRequest(SchemaSelectionRequests.getSchema, (fileUri) => { + this.connection?.onRequest(SchemaSelectionRequests.getSchema, (fileUri) => { return this.getSchemas(fileUri); }); - this.connection.onRequest(SchemaSelectionRequests.getAllSchemas, (fileUri) => { + this.connection?.onRequest(SchemaSelectionRequests.getAllSchemas, (fileUri) => { return this.getAllSchemas(fileUri); }); } @@ -38,7 +38,7 @@ export class JSONSchemaSelection { } private async getSchemasForFile(docUri: string): Promise> { - const document = this.yamlSettings.documents.get(docUri); + const document = this.yamlSettings?.documents.get(docUri); const schemas = new Map(); if (!document) { return schemas; diff --git a/src/languageservice/services/documentSymbols.ts b/src/languageservice/services/documentSymbols.ts index 2b14edfe..6aa631d6 100644 --- a/src/languageservice/services/documentSymbols.ts +++ b/src/languageservice/services/documentSymbols.ts @@ -18,7 +18,7 @@ import { convertErrorToTelemetryMsg } from '../utils/objects'; export class YAMLDocumentSymbols { private jsonDocumentSymbols; - constructor(schemaService: YAMLSchemaService, private readonly telemetry: Telemetry) { + constructor(schemaService: YAMLSchemaService, private readonly telemetry?: Telemetry) { this.jsonDocumentSymbols = new JSONDocumentSymbols(schemaService); // override 'getKeyLabel' to handle complex mapping @@ -54,7 +54,7 @@ export class YAMLDocumentSymbols { } } } catch (err) { - this.telemetry.sendError('yaml.documentSymbols.error', { error: convertErrorToTelemetryMsg(err) }); + this.telemetry?.sendError('yaml.documentSymbols.error', { error: convertErrorToTelemetryMsg(err) }); } return results; } @@ -76,7 +76,7 @@ export class YAMLDocumentSymbols { } } } catch (err) { - this.telemetry.sendError('yaml.hierarchicalDocumentSymbols.error', { error: convertErrorToTelemetryMsg(err) }); + this.telemetry?.sendError('yaml.hierarchicalDocumentSymbols.error', { error: convertErrorToTelemetryMsg(err) }); } return results; diff --git a/src/languageservice/services/yamlCodeLens.ts b/src/languageservice/services/yamlCodeLens.ts index 353e83fc..7157ccf8 100644 --- a/src/languageservice/services/yamlCodeLens.ts +++ b/src/languageservice/services/yamlCodeLens.ts @@ -15,7 +15,7 @@ import { convertErrorToTelemetryMsg } from '../utils/objects'; import { getSchemaTitle } from '../utils/schemaUtils'; export class YamlCodeLens { - constructor(private schemaService: YAMLSchemaService, private readonly telemetry: Telemetry) {} + constructor(private schemaService: YAMLSchemaService, private readonly telemetry?: Telemetry) {} async getCodeLens(document: TextDocument): Promise { const result = []; @@ -39,7 +39,7 @@ export class YamlCodeLens { result.push(lens); } } catch (err) { - this.telemetry.sendError('yaml.codeLens.error', { error: convertErrorToTelemetryMsg(err) }); + this.telemetry?.sendError('yaml.codeLens.error', { error: convertErrorToTelemetryMsg(err) }); } return result; diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index 42306e0c..cc245652 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -81,7 +81,7 @@ export class YamlCompletion { private schemaService: YAMLSchemaService, private clientCapabilities: ClientCapabilities = {}, private yamlDocument: YamlDocuments, - private readonly telemetry: Telemetry + private readonly telemetry?: Telemetry ) {} configure(languageSettings: LanguageSettings): void { @@ -279,7 +279,7 @@ export class YamlCompletion { } }, error: (message: string) => { - this.telemetry.sendError('yaml.completion.error', { error: convertErrorToTelemetryMsg(message) }); + this.telemetry?.sendError('yaml.completion.error', { error: convertErrorToTelemetryMsg(message) }); }, log: (message: string) => { console.log(message); @@ -530,7 +530,7 @@ export class YamlCompletion { const types: { [type: string]: boolean } = {}; this.getValueCompletions(schema, currentDoc, node, offset, document, collector, types, doComplete); } catch (err) { - this.telemetry.sendError('yaml.completion.error', { error: convertErrorToTelemetryMsg(err) }); + this.telemetry?.sendError('yaml.completion.error', { error: convertErrorToTelemetryMsg(err) }); } this.finalizeParentCompletion(result); diff --git a/src/languageservice/services/yamlDefinition.ts b/src/languageservice/services/yamlDefinition.ts index 079383fd..b2f1b975 100644 --- a/src/languageservice/services/yamlDefinition.ts +++ b/src/languageservice/services/yamlDefinition.ts @@ -14,7 +14,7 @@ import { convertErrorToTelemetryMsg } from '../utils/objects'; import { TextBuffer } from '../utils/textBuffer'; export class YamlDefinition { - constructor(private readonly telemetry: Telemetry) {} + constructor(private readonly telemetry?: Telemetry) {} getDefinition(document: TextDocument, params: DefinitionParams): DefinitionLink[] | undefined { try { @@ -33,7 +33,7 @@ export class YamlDefinition { } } } catch (err) { - this.telemetry.sendError('yaml.definition.error', { error: convertErrorToTelemetryMsg(err) }); + this.telemetry?.sendError('yaml.definition.error', { error: convertErrorToTelemetryMsg(err) }); } return undefined; diff --git a/src/languageservice/services/yamlHover.ts b/src/languageservice/services/yamlHover.ts index 9ad870cd..0a5fb2a0 100644 --- a/src/languageservice/services/yamlHover.ts +++ b/src/languageservice/services/yamlHover.ts @@ -26,7 +26,7 @@ export class YAMLHover { private indentation: string; private schemaService: YAMLSchemaService; - constructor(schemaService: YAMLSchemaService, private readonly telemetry: Telemetry) { + constructor(schemaService: YAMLSchemaService, private readonly telemetry?: Telemetry) { this.shouldHover = true; this.schemaService = schemaService; } @@ -55,7 +55,7 @@ export class YAMLHover { currentDoc.currentDocIndex = currentDocIndex; return this.getHover(document, position, currentDoc); } catch (error) { - this.telemetry.sendError('yaml.hover.error', { error: convertErrorToTelemetryMsg(error) }); + this.telemetry?.sendError('yaml.hover.error', { error: convertErrorToTelemetryMsg(error) }); } } @@ -88,10 +88,14 @@ export class YAMLHover { ); const createHover = (contents: string): Hover => { - const regex = new RegExp(this.indentation, 'g'); + if (this.indentation !== undefined) { + const indentationMatchRegex = new RegExp(this.indentation, 'g'); + contents = contents.replace(indentationMatchRegex, ' '); + } + const markupContent: MarkupContent = { kind: MarkupKind.Markdown, - value: contents.replace(regex, ' '), + value: contents, }; const result: Hover = { contents: markupContent, diff --git a/src/languageservice/services/yamlLinks.ts b/src/languageservice/services/yamlLinks.ts index 57d0459e..fe22e455 100644 --- a/src/languageservice/services/yamlLinks.ts +++ b/src/languageservice/services/yamlLinks.ts @@ -10,7 +10,7 @@ import { yamlDocumentsCache } from '../parser/yaml-documents'; import { convertErrorToTelemetryMsg } from '../utils/objects'; export class YamlLinks { - constructor(private readonly telemetry: Telemetry) {} + constructor(private readonly telemetry?: Telemetry) {} findLinks(document: TextDocument): Promise { try { @@ -23,7 +23,7 @@ export class YamlLinks { // Wait for all the promises to return and then flatten them into one DocumentLink array return Promise.all(linkPromises).then((yamlLinkArray) => [].concat(...yamlLinkArray)); } catch (err) { - this.telemetry.sendError('yaml.documentLink.error', { error: convertErrorToTelemetryMsg(err) }); + this.telemetry?.sendError('yaml.documentLink.error', { error: convertErrorToTelemetryMsg(err) }); } } } diff --git a/src/languageservice/services/yamlValidation.ts b/src/languageservice/services/yamlValidation.ts index 3185e35b..5c7fd355 100644 --- a/src/languageservice/services/yamlValidation.ts +++ b/src/languageservice/services/yamlValidation.ts @@ -49,7 +49,7 @@ export class YAMLValidation { private MATCHES_MULTIPLE = 'Matches multiple schemas when only one must validate.'; - constructor(schemaService: YAMLSchemaService, private readonly telemetry: Telemetry) { + constructor(schemaService: YAMLSchemaService, private readonly telemetry?: Telemetry) { this.validationEnabled = true; this.jsonValidation = new JSONValidation(schemaService, Promise); } @@ -108,7 +108,7 @@ export class YAMLValidation { index++; } } catch (err) { - this.telemetry.sendError('yaml.validation.error', { error: convertErrorToTelemetryMsg(err) }); + this.telemetry?.sendError('yaml.validation.error', { error: convertErrorToTelemetryMsg(err) }); } let previousErr: Diagnostic; diff --git a/src/languageservice/yamlLanguageService.ts b/src/languageservice/yamlLanguageService.ts index b3ad43b9..196845fc 100644 --- a/src/languageservice/yamlLanguageService.ts +++ b/src/languageservice/yamlLanguageService.ts @@ -43,10 +43,8 @@ import { TextDocument } from 'vscode-languageserver-textdocument'; import { getFoldingRanges } from './services/yamlFolding'; import { FoldingRangesContext, SchemaVersions } 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'; import { Telemetry } from './telemetry'; import { YamlVersion } from './parser/yamlParser07'; import { YamlCompletion } from './services/yamlCompletion'; @@ -180,29 +178,27 @@ export interface LanguageService { resolveCodeLens(param: CodeLens): Thenable | CodeLens; } -export function getLanguageService( - schemaRequestService: SchemaRequestService, - workspaceContext: WorkspaceContextService, - connection: Connection, - telemetry: Telemetry, - yamlSettings: SettingsState, - clientCapabilities?: ClientCapabilities -): LanguageService { - const schemaService = new YAMLSchemaService(schemaRequestService, workspaceContext); - const completer = new YamlCompletion(schemaService, clientCapabilities, yamlDocumentsCache, telemetry); - const hover = new YAMLHover(schemaService, telemetry); - const yamlDocumentSymbols = new YAMLDocumentSymbols(schemaService, telemetry); - const yamlValidation = new YAMLValidation(schemaService, telemetry); +export function getLanguageService(params: { + schemaRequestService: SchemaRequestService; + workspaceContext: WorkspaceContextService; + connection?: Connection; + telemetry?: Telemetry; + yamlSettings?: SettingsState; + clientCapabilities?: ClientCapabilities; +}): LanguageService { + const schemaService = new YAMLSchemaService(params.schemaRequestService, params.workspaceContext); + const completer = new YamlCompletion(schemaService, params.clientCapabilities, yamlDocumentsCache, params.telemetry); + const hover = new YAMLHover(schemaService, params.telemetry); + const yamlDocumentSymbols = new YAMLDocumentSymbols(schemaService, params.telemetry); + const yamlValidation = new YAMLValidation(schemaService, params.telemetry); const formatter = new YAMLFormatter(); - const yamlCodeActions = new YamlCodeActions(clientCapabilities); - const yamlCodeLens = new YamlCodeLens(schemaService, telemetry); - const yamlLinks = new YamlLinks(telemetry); - const yamlDefinition = new YamlDefinition(telemetry); + const yamlCodeActions = new YamlCodeActions(params.clientCapabilities); + const yamlCodeLens = new YamlCodeLens(schemaService, params.telemetry); + const yamlLinks = new YamlLinks(params.telemetry); + const yamlDefinition = new YamlDefinition(params.telemetry); - new JSONSchemaSelection(schemaService, yamlSettings, connection); + new JSONSchemaSelection(schemaService, params.yamlSettings, params.connection); - // register all commands - registerCommands(commandExecutor, connection); return { configure: (settings) => { schemaService.clearExternalSchemas(); diff --git a/src/yamlServerInit.ts b/src/yamlServerInit.ts index 0139c48b..65496128 100644 --- a/src/yamlServerInit.ts +++ b/src/yamlServerInit.ts @@ -17,6 +17,7 @@ import { YamlCommands } from './commands'; import { WorkspaceHandlers } from './languageserver/handlers/workspaceHandlers'; import { commandExecutor } from './languageserver/commandExecutor'; import { Telemetry } from './languageservice/telemetry'; +import { registerCommands } from './languageservice/services/yamlCommands'; export class YAMLServerInit { languageService: LanguageService; @@ -57,14 +58,14 @@ export class YAMLServerInit { // public for test setup connectionInitialized(params: InitializeParams): InitializeResult { this.yamlSettings.capabilities = params.capabilities; - this.languageService = getCustomLanguageService( - this.schemaRequestService, - this.workspaceContext, - this.connection, - this.telemetry, - this.yamlSettings, - params.capabilities - ); + this.languageService = getCustomLanguageService({ + schemaRequestService: this.schemaRequestService, + workspaceContext: this.workspaceContext, + connection: this.connection, + yamlSettings: this.yamlSettings, + telemetry: this.telemetry, + clientCapabilities: params.capabilities, + }); // Only try to parse the workspace root if its not null. Otherwise initialize will fail if (params.rootUri) { @@ -95,6 +96,7 @@ export class YAMLServerInit { this.yamlSettings.capabilities.workspace.didChangeWatchedFiles.dynamicRegistration ); this.registerHandlers(); + registerCommands(commandExecutor, this.connection); return { capabilities: { diff --git a/test/yamlLanguageService.test.ts b/test/yamlLanguageService.test.ts new file mode 100644 index 00000000..97c08bf5 --- /dev/null +++ b/test/yamlLanguageService.test.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { assert } from 'chai'; +import { Position, TextDocument } from 'vscode-languageserver-textdocument'; +import { getLanguageService, LanguageService, SchemaRequestService, WorkspaceContextService } from '../src'; +import { workspaceContext } from '../src/languageservice/services/schemaRequestHandler'; +import { caretPosition, setupSchemaIDTextDocument } from './utils/testHelper'; + +/** + * Builds a simple schema request service + * @param contentMap Mapping of a schema uri to the schema content + */ +function schemaRequestServiceBuilder(contentMap: { [uri: string]: string }): SchemaRequestService { + return async (uri: string) => { + return contentMap[uri]; + }; +} + +describe('getLanguageService()', () => { + it('successfully creates an instance without optional arguments', () => { + getLanguageService({ + schemaRequestService: {} as SchemaRequestService, + workspaceContext: {} as WorkspaceContextService, + }); + }); + + describe('minimal language service hover happy path', () => { + const schemaUri = 'my.schema.uri'; + const schemaContentMap: { [uri: string]: string } = {}; + + let schemaRequestService: SchemaRequestService; + let textDocument: TextDocument; + let hoverPosition: Position; // Position the 'mouse' is hovering on the content + let minimalYamlService: LanguageService; + + before(async () => { + // Setup object that resolves schema content + schemaContentMap[schemaUri] = ` + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "The person's first name." + } + } + } + `; + schemaRequestService = schemaRequestServiceBuilder(schemaContentMap); + + // Setup the document and where the hover is on it + const contentWithHoverPosition = 'fi|r|stName: "Nikolas"'; + const { content, position: offset } = caretPosition(contentWithHoverPosition); + textDocument = setupSchemaIDTextDocument(content); + hoverPosition = textDocument.positionAt(offset); + + // Setup minimal language service + indicate to provide hover functionality + minimalYamlService = getLanguageService({ + schemaRequestService: schemaRequestService, + workspaceContext: workspaceContext, + }); + minimalYamlService.configure({ + hover: true, + schemas: [ + { + fileMatch: [textDocument.uri], + uri: schemaUri, + }, + ], + }); + }); + + it('successfully creates an instance without optional arguments', async () => { + const result = await minimalYamlService.doHover(textDocument, hoverPosition); + + assert.deepEqual(result, { + contents: { + kind: 'markdown', + value: "The person's first name\\.\n\nSource: [my.schema.uri](my.schema.uri)", + }, + range: { + start: { + line: 0, + character: 0, + }, + end: { + line: 0, + character: 9, + }, + }, + }); + }); + }); +});