Skip to content

Commit

Permalink
Show all json schemas for yaml file in codelens (#424)
Browse files Browse the repository at this point in the history
* #412 Implement CodeLens, show all json schemas in codelens

Signed-off-by: Yevhen Vydolob <[email protected]>

* Fix review comments

Signed-off-by: Yevhen Vydolob <[email protected]>

* delete commented code

Signed-off-by: Yevhen Vydolob <[email protected]>

* Use 'isBooelan' as type gard

Signed-off-by: Yevhen Vydolob <[email protected]>

* fix tests

Signed-off-by: Yevhen Vydolob <[email protected]>
  • Loading branch information
evidolob authored Mar 24, 2021
1 parent 520d7af commit c8a1179
Show file tree
Hide file tree
Showing 17 changed files with 445 additions and 70 deletions.
17 changes: 16 additions & 1 deletion src/languageserver/handlers/languageHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<DocumentLink[]> {
Expand Down Expand Up @@ -177,4 +180,16 @@ export class LanguageHandlers {

return this.languageService.getCodeAction(textDocument, params);
}

codeLensHandler(params: CodeLensParams): Thenable<CodeLens[] | undefined> | 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> | CodeLens {
return this.languageService.resolveCodeLens(param);
}
}
55 changes: 55 additions & 0 deletions src/languageservice/parser/yaml-documents.ts
Original file line number Diff line number Diff line change
@@ -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<string, YamlCachedDocument>();

/**
* 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();
10 changes: 5 additions & 5 deletions src/languageservice/services/documentSymbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand Down
20 changes: 2 additions & 18 deletions src/languageservice/services/yamlCodeActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
CodeActionKind,
CodeActionParams,
Command,
Connection,
Diagnostic,
Position,
Range,
Expand All @@ -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';

Expand All @@ -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;
Expand Down Expand Up @@ -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, []);
}
Expand Down
103 changes: 103 additions & 0 deletions src/languageservice/services/yamlCodeLens.ts
Original file line number Diff line number Diff line change
@@ -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<CodeLens[]> {
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> | 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<string, JSONSchema> {
const result = new Map<string, JSONSchema>();
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<string, JSONSchema>): 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<string, JSONSchema>): void {
for (const subSchema of schemas) {
if (!isBoolean(subSchema)) {
if (subSchema.url && !result.has(subSchema.url)) {
result.set(subSchema.url, subSchema);
}
}
}
}
32 changes: 32 additions & 0 deletions src/languageservice/services/yamlCommands.ts
Original file line number Diff line number Diff line change
@@ -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}`);
}
});
}
2 changes: 1 addition & 1 deletion src/languageservice/services/yamlCompletion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
7 changes: 4 additions & 3 deletions src/languageservice/services/yamlFolding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
4 changes: 2 additions & 2 deletions src/languageservice/services/yamlHover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions src/languageservice/services/yamlLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DocumentLink[]> {
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) {
Expand Down
Loading

0 comments on commit c8a1179

Please sign in to comment.