Skip to content

Commit

Permalink
Add methods which allow client get schemas info (#556)
Browse files Browse the repository at this point in the history
* Add methods which allow client get schemas info

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

* fix review comment

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

* Delete commented line

Signed-off-by: Yevhen Vydolob <[email protected]>
  • Loading branch information
evidolob authored Nov 30, 2021
1 parent fcfe397 commit 520a328
Show file tree
Hide file tree
Showing 12 changed files with 375 additions and 58 deletions.
84 changes: 84 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,90 @@ docker run -it quay.io/redhat-developer/yaml-language-server:latest

`yaml-language-server` use `[email protected]` which implements [LSP 3.16](https://github.com/Microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-16.md)

## Language Server Protocol extensions

### SchemaSelectionRequests

#### SupportSchemaSelection Notification

The support schema selection notification is sent from a client to the server to inform server that client supports JSON Schema selection.

_Notification:_
- method: `'yaml/supportSchemaSelection'`
- params: `void`

#### SchemaStoreInitialized Notification

The schema store initialized notification is sent from the server to a client to inform client that server has finished initializing/loading schemas from schema store, and client now can ask for schemas.

_Notification:_
- method: `'yaml/schema/store/initialized'`
- params: `void`

#### GetAllSchemas Request

The get all schemas request sent from a client to server to get all known schemas.

_Request:_
- method: `'yaml/get/all/jsonSchemas'`;
- params: the document uri, server will mark used schema for document

_Response:_

- result: `JSONSchemaDescriptionExt[]`
```typescript
interface JSONSchemaDescriptionExt {
/**
* Schema URI
*/
uri: string;
/**
* Schema name, from schema store
*/
name?: string;
/**
* Schema description, from schema store
*/
description?: string;
/**
* Is schema used for current document
*/
usedForCurrentFile: boolean;
/**
* Is schema from schema store
*/
fromStore: boolean;
}
```

#### GetSchemas Request

The request sent from a client to server to get schemas used for current document. Client can use this method to indicate in UI which schemas used for current YAML document.

_Request:_
- method: `'yaml/get/jsonSchema'`;
- params: the document uri to get used schemas

_Response:_
- result: `JSONSchemaDescription[]`

```typescript
interface JSONSchemaDescriptionExt {
/**
* Schema URI
*/
uri: string;
/**
* Schema name, from schema store
*/
name?: string;
/**
* Schema description, from schema store
*/
description?: string;
}
```

## Clients

This repository only contains the server implementation. Here are some known clients consuming this server:
Expand Down
6 changes: 6 additions & 0 deletions src/languageserver/handlers/notificationHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
CustomSchemaRequest,
DynamicCustomSchemaRequestRegistration,
SchemaAssociationNotification,
SchemaSelectionRequests,
VSCodeContentRequestRegistration,
} from '../../requestTypes';
import { SettingsState } from '../../yamlSettings';
Expand Down Expand Up @@ -36,6 +37,7 @@ export class NotificationHandlers {
);
this.connection.onNotification(DynamicCustomSchemaRequestRegistration.type, () => this.dynamicSchemaRequestHandler());
this.connection.onNotification(VSCodeContentRequestRegistration.type, () => this.vscodeContentRequestHandler());
this.connection.onNotification(SchemaSelectionRequests.type, () => this.schemaSelectionRequestHandler());
}

/**
Expand Down Expand Up @@ -68,4 +70,8 @@ export class NotificationHandlers {
private vscodeContentRequestHandler(): void {
this.yamlSettings.useVSCodeContentRequest = true;
}

private schemaSelectionRequestHandler(): void {
this.yamlSettings.useSchemaSelectionRequests = true;
}
}
84 changes: 84 additions & 0 deletions src/languageserver/handlers/schemaSelectionHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*---------------------------------------------------------------------------------------------
* 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 { JSONSchema } from '../../languageservice/jsonSchema';
import { yamlDocumentsCache } from '../../languageservice/parser/yaml-documents';
import { YAMLSchemaService } from '../../languageservice/services/yamlSchemaService';
import { getSchemaUrls } from '../../languageservice/utils/schemaUrls';
import { SettingsState } from '../../yamlSettings';
import { JSONSchemaDescription, JSONSchemaDescriptionExt, SchemaSelectionRequests } from '../../requestTypes';

export class JSONSchemaSelection {
constructor(
private readonly schemaService: YAMLSchemaService,
private readonly yamlSettings: SettingsState,
private readonly connection: Connection
) {
this.connection.onRequest(SchemaSelectionRequests.getSchema, (fileUri) => {
return this.getSchemas(fileUri);
});
this.connection.onRequest(SchemaSelectionRequests.getAllSchemas, (fileUri) => {
return this.getAllSchemas(fileUri);
});
}

async getSchemas(docUri: string): Promise<JSONSchemaDescription[]> {
const schemas = await this.getSchemasForFile(docUri);
const result = Array.from(schemas).map((val) => {
return {
name: val[1].title,
uri: val[0],
description: val[1].description,
};
});

return result;
}

private async getSchemasForFile(docUri: string): Promise<Map<string, JSONSchema>> {
const document = this.yamlSettings.documents.get(docUri);
const schemas = new Map<string, JSONSchema>();
if (!document) {
return schemas;
}

const yamlDoc = yamlDocumentsCache.getYamlDocument(document);

for (const currentYAMLDoc of yamlDoc.documents) {
const schema = await this.schemaService.getSchemaForResource(document.uri, currentYAMLDoc);
if (schema?.schema) {
const schemaUrls = getSchemaUrls(schema?.schema);
if (schemaUrls.size === 0) {
continue;
}
for (const urlToSchema of schemaUrls) {
schemas.set(urlToSchema[0], urlToSchema[1]);
}
}
}
return schemas;
}

async getAllSchemas(docUri: string): Promise<JSONSchemaDescriptionExt[]> {
const fileSchemas = await this.getSchemasForFile(docUri);
const fileSchemasHandle: JSONSchemaDescriptionExt[] = Array.from(fileSchemas.entries()).map((val) => {
return {
uri: val[0],
fromStore: false,
usedForCurrentFile: true,
name: val[1].title,
description: val[1].description,
};
});
const result = [];
let allSchemas = this.schemaService.getAllSchemas();
allSchemas = allSchemas.filter((val) => !fileSchemas.has(val.uri));
result.push(...fileSchemasHandle);
result.push(...allSchemas);

return result;
}
}
8 changes: 6 additions & 2 deletions src/languageserver/handlers/settingsHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { DocumentFormattingRequest, Connection, DidChangeConfigurationNotificati
import { isRelativePath, relativeToAbsolutePath } from '../../languageservice/utils/paths';
import { checkSchemaURI, JSON_SCHEMASTORE_URL, KUBERNETES_SCHEMA_URL } from '../../languageservice/utils/schemaUrls';
import { LanguageService, LanguageSettings, SchemaPriority } from '../../languageservice/yamlLanguageService';
import { SchemaSelectionRequests } from '../../requestTypes';
import { Settings, SettingsState } from '../../yamlSettings';
import { Telemetry } from '../telemetry';
import { ValidationHandler } from './validationHandlers';
Expand Down Expand Up @@ -53,7 +54,7 @@ export class SettingsHandler {
this.setConfiguration(settings);
}

setConfiguration(settings: Settings): void {
async setConfiguration(settings: Settings): Promise<void> {
configureHttpRequests(settings.http && settings.http.proxy, settings.http && settings.http.proxyStrictSSL);

this.yamlSettings.specificValidatorPaths = [];
Expand Down Expand Up @@ -120,8 +121,11 @@ export class SettingsHandler {
this.yamlSettings.schemaConfigurationSettings.push(schemaObj);
}

this.setSchemaStoreSettingsIfNotSet();
await this.setSchemaStoreSettingsIfNotSet();
this.updateConfiguration();
if (this.yamlSettings.useSchemaSelectionRequests) {
this.connection.sendNotification(SchemaSelectionRequests.schemaStoreInitialized, {});
}

// dynamically enable & disable the formatter
if (this.yamlSettings.clientDynamicRegisterSupport) {
Expand Down
46 changes: 3 additions & 43 deletions src/languageservice/services/yamlCodeLens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ 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 { JSONSchema } from '../jsonSchema';
import { CodeLensParams } from 'vscode-languageserver-protocol';
import { isBoolean } from '../utils/objects';
import { Telemetry } from '../../languageserver/telemetry';
import { getSchemaUrls } from '../utils/schemaUrls';

export class YamlCodeLens {
constructor(private schemaService: YAMLSchemaService, private readonly telemetry: Telemetry) {}
Expand All @@ -26,7 +26,7 @@ export class YamlCodeLens {
for (const currentYAMLDoc of yamlDocument.documents) {
const schema = await this.schemaService.getSchemaForResource(document.uri, currentYAMLDoc);
if (schema?.schema) {
const schemaUrls = getSchemaUrl(schema?.schema);
const schemaUrls = getSchemaUrls(schema?.schema);
if (schemaUrls.size === 0) {
continue;
}
Expand Down Expand Up @@ -66,43 +66,3 @@ function getCommandTitle(url: string, schema: JSONSchema): string {

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);
}
}
}
}
47 changes: 47 additions & 0 deletions src/languageservice/utils/schemaUrls.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { WorkspaceFolder } from 'vscode-languageserver';
import { URI } from 'vscode-uri';
import { Telemetry } from '../../languageserver/telemetry';
import { JSONSchema, JSONSchemaRef } from '../jsonSchema';
import { isBoolean } from './objects';
import { isRelativePath, relativeToAbsolutePath } from './paths';

export const KUBERNETES_SCHEMA_URL =
Expand All @@ -22,3 +24,48 @@ export function checkSchemaURI(
return uri;
}
}

/**
* Collect all urls of sub schemas
* @param schema the root schema
* @returns map url to schema
*/
export function getSchemaUrls(schema: JSONSchema): Map<string, JSONSchema> {
const result = new Map<string, JSONSchema>();
if (!schema) {
return result;
}

if (schema.url) {
if (schema.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);
}
}
}
}
6 changes: 6 additions & 0 deletions src/languageservice/yamlLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ import { Telemetry } from '../languageserver/telemetry';
import { YamlVersion } from './parser/yamlParser07';
import { YamlCompletion } from './services/yamlCompletion';
import { yamlDocumentsCache } from './parser/yaml-documents';
import { SettingsState } from '../yamlSettings';
import { JSONSchemaSelection } from '../languageserver/handlers/schemaSelectionHandlers';
import { getDefinition } from './services/yamlDefinition';

export enum SchemaPriority {
Expand Down Expand Up @@ -159,6 +161,7 @@ export function getLanguageService(
workspaceContext: WorkspaceContextService,
connection: Connection,
telemetry: Telemetry,
yamlSettings: SettingsState,
clientCapabilities?: ClientCapabilities
): LanguageService {
const schemaService = new YAMLSchemaService(schemaRequestService, workspaceContext);
Expand All @@ -169,6 +172,9 @@ export function getLanguageService(
const formatter = new YAMLFormatter();
const yamlCodeActions = new YamlCodeActions(clientCapabilities);
const yamlCodeLens = new YamlCodeLens(schemaService, telemetry);

new JSONSchemaSelection(schemaService, yamlSettings, connection);

// register all commands
registerCommands(commandExecutor, connection);
return {
Expand Down
7 changes: 7 additions & 0 deletions src/requestTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,10 @@ export namespace ColorSymbolRequest {
export namespace SchemaModificationNotification {
export const type: RequestType<SchemaAdditions | SchemaDeletions, void, {}> = new RequestType('json/schema/modify');
}

export namespace SchemaSelectionRequests {
export const type: NotificationType<void> = new NotificationType('yaml/supportSchemaSelection');
export const getSchema: RequestType<string, JSONSchemaDescription[], {}> = new RequestType('yaml/get/jsonSchema');
export const getAllSchemas: RequestType<string, JSONSchemaDescriptionExt[], {}> = new RequestType('yaml/get/all/jsonSchemas');
export const schemaStoreInitialized: NotificationType<{}> = new NotificationType('yaml/schema/store/initialized');
}
Loading

0 comments on commit 520a328

Please sign in to comment.