Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show all json schemas for yaml file in codelens #424

Merged
merged 6 commits into from
Mar 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
JPinkney marked this conversation as resolved.
Show resolved Hide resolved
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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still does not confirm that it is a JSONSchema.

"I'm looking for a dog. Is a bird? No, it's not a bird. OK, it's not a bird so it must be a dog" That does not work.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have only two choices, subSchema can be boolean or JSONSchema
It would be enough to check that schema is not boolean, as rest check will do TS compiler.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, since you are not using an any I guess that could work.

I was thinking that if you wanted to confirm that matched the interface then test for expected mandatory fields. But I'll leave it up to you.

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