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

204 support schema associations in file #280

Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#### 0.10.0

- Allows to declare a schema inside the yaml file through modeline `# yaml-language-server: $schema=<urlOfTheSchema>`

#### 0.9.0
- Improve Diagnostic positions [#260](https://github.com/redhat-developer/yaml-language-server/issues/260)
- Support `maxProperties` when providing completion [#269](https://github.com/redhat-developer/yaml-language-server/issues/269)
Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ and that will associate the composer schema with myYamlFile.yaml.

## More examples of schema association:

### Using yaml.schemas settings

#### Single root schema association:

When associating a schema it should follow the format below
```
yaml.schemas: {
Expand Down Expand Up @@ -118,7 +122,7 @@ yaml.schemas: {
}
```

## Multi root schema association:
#### Multi root schema association:
You can also use relative paths when working with multi root workspaces.

Suppose you have a multi root workspace that is laid out like:
Expand All @@ -141,6 +145,14 @@ yaml.schemas: {

`yaml.schemas` allows you to specify json schemas that you want to validate against the yaml that you write. Kubernetes is an optional field. It does not require a url as the language server will provide that. You just need the keyword kubernetes and a glob pattern.

### Using inlined schema
JPinkney marked this conversation as resolved.
Show resolved Hide resolved

It is possible to specify a yaml schema using a modeline.

```
# yaml-language-server: $schema=<urlToTheSchema>
```

## Clients
This repository only contains the server implementation. Here are some known clients consuming this server:

Expand Down
24 changes: 24 additions & 0 deletions src/languageservice/parser/yamlParser07.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import { ASTNode } from '../jsonASTTypes';
import { ErrorCode } from 'vscode-json-languageservice';
import { emit } from 'process';

const YAML_DIRECTIVE_PREFIX = '%';
const YAML_COMMENT_PREFIX = '#';
const YAML_DATA_INSTANCE_SEPARATOR = '---';

/**
* These documents are collected into a final YAMLDocument
* and passed to the `parseYAML` caller.
Expand All @@ -29,13 +33,15 @@ export class SingleYAMLDocument extends JSONDocument {
public warnings: YAMLDocDiagnostic[];
public isKubernetes: boolean;
public currentDocIndex: number;
public lineComments: string[];

constructor(lines: number[]) {
super(null, []);
this.lines = lines;
this.root = null;
this.errors = [];
this.warnings = [];
this.lineComments = [];
}

public getSchemas(schema, doc, node) {
Expand Down Expand Up @@ -101,7 +107,25 @@ export function parse(text: string, customTags = []): YAMLDocument {
// Generate the SingleYAMLDocs from the AST nodes
const startPositions = getLineStartPositions(text);
const yamlDocs: SingleYAMLDocument[] = yamlNodes.map(node => nodeToSingleDoc(node, startPositions, text));

parseLineComments(text, yamlDocs);

// Consolidate the SingleYAMLDocs
return new YAMLDocument(yamlDocs);
}

function parseLineComments(text: string, yamlDocs: SingleYAMLDocument[]) {
const lines = text.split(/[\r\n]+/g);
let yamlDocCount = 0;
lines.forEach(line => {
if (line.startsWith(YAML_DIRECTIVE_PREFIX) && yamlDocCount === 0) {
yamlDocCount--;
}
if (line === YAML_DATA_INSTANCE_SEPARATOR) {
yamlDocCount++;
}
if (line.startsWith(YAML_COMMENT_PREFIX)) {
yamlDocs[yamlDocCount].lineComments.push(line);
}
});
}
43 changes: 39 additions & 4 deletions src/languageservice/services/yamlSchemaService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import { URI } from 'vscode-uri';

import * as nls from 'vscode-nls';
import { convertSimple2RegExpPattern } from '../utils/strings';
import { TextDocument } from 'vscode-languageserver';
import { SingleYAMLDocument } from '../parser/yamlParser07';
import { stringifyObject } from '../utils/json';
import { getNodeValue, JSONDocument } from '../parser/jsonParser07';
import { Parser } from 'prettier';
const localize = nls.loadMessageBundle();

export declare type CustomSchemaProvider = (uri: string) => Thenable<string | string[]>;
Expand Down Expand Up @@ -214,11 +219,17 @@ export class YAMLSchemaService extends JSONSchemaService {
}
//tslint:enable

public getSchemaForResource (resource: string, doc = undefined): Thenable<ResolvedSchema> {
public getSchemaForResource (resource: string, doc : JSONDocument): Thenable<ResolvedSchema> {
const resolveSchema = () => {

const seen: { [schemaId: string]: boolean } = Object.create(null);
const schemas: string[] = [];

const schemaFromModeline = this.getSchemaFromModeline(doc);
if(schemaFromModeline !== undefined) {
schemas.push(schemaFromModeline);
seen[schemaFromModeline] = true;
}

for (const entry of this.filePatternAssociations) {
if (entry.matchesPattern(resource)) {
for (const schemaId of entry.getURIs()) {
Expand All @@ -243,8 +254,8 @@ export class YAMLSchemaService extends JSONSchemaService {

if (schemas.length > 0) {
return super.createCombinedSchema(resource, schemas).getResolvedSchema().then(schema => {
if (schema.schema && schema.schema.schemaSequence && schema.schema.schemaSequence[doc.currentDocIndex]) {
return new ResolvedSchema(schema.schema.schemaSequence[doc.currentDocIndex]);
if (schema.schema && schema.schema.schemaSequence && schema.schema.schemaSequence[(<SingleYAMLDocument>doc).currentDocIndex]) {
return new ResolvedSchema(schema.schema.schemaSequence[(<SingleYAMLDocument>doc).currentDocIndex]);
}
return schema;
});
Expand Down Expand Up @@ -282,6 +293,30 @@ export class YAMLSchemaService extends JSONSchemaService {
return resolveSchema();
}
}

/**
* Retrieve schema if declared as modeline.
* Public for testing purpose, not part of the API.
* @param doc
*/
public getSchemaFromModeline(doc: any) : string{
if (doc instanceof SingleYAMLDocument) {
const yamlLanguageServerModeline = doc.lineComments.find(lineComment => {
const matchModeline = lineComment.match(/^#\s+yaml-language-server\s*:/g);
return matchModeline !== null && matchModeline.length === 1;
});
if (yamlLanguageServerModeline != undefined) {
const schemaMatchs = yamlLanguageServerModeline.match(/\$schema=\S+/g);
if(schemaMatchs !== null && schemaMatchs.length >= 1) {
if (schemaMatchs.length >= 2) {
console.log('Several $schema attributes has been found on the yaml-language-server modeline. The first one will be picked.');
Copy link
Member Author

@apupier apupier Jul 23, 2020

Choose a reason for hiding this comment

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

How can I send a notification to the client?
(can be done in a different PR)

Copy link
Collaborator

Choose a reason for hiding this comment

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

This can be fine for now. server.ts binds console.log and console.error to the connection.

Copy link
Contributor

Choose a reason for hiding this comment

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

...have been found...

Copy link
Member Author

Choose a reason for hiding this comment

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

}
return schemaMatchs[0].substring('$schema='.length);
}
}
}
return undefined;
}

private async resolveCustomSchema (schemaUri, doc) {
const unresolvedSchema = await this.loadSchema(schemaUri);
Expand Down
37 changes: 36 additions & 1 deletion test/autoCompletion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) Red Hat. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/;
import { SCHEMA_ID, setupSchemaIDTextDocument, configureLanguageService } from './utils/testHelper';
import { SCHEMA_ID, setupSchemaIDTextDocument, configureLanguageService, toFsPath } from './utils/testHelper';
import assert = require('assert');
import path = require('path');
import { createExpectedCompletion } from './utils/verifyError';
Expand Down Expand Up @@ -924,5 +924,40 @@ suite('Auto Completion Tests', () => {
}).then(done, done);
});
});

describe('Yaml schema defined in file', function () {

const uri = toFsPath(path.join(__dirname, './fixtures/testArrayMaxProperties.json'));

it('Provide completion from schema declared in file', done => {
const content = `# yaml-language-server: $schema=${uri}\n- `;
const completion = parseSetup(content, content.length);
completion.then(function (result) {
assert.equal(result.items.length, 3);
}).then(done, done);
});

it('Provide completion from schema declared in file with several attributes', done => {
const content = `# yaml-language-server: $schema=${uri} anothermodeline=value\n- `;
const completion = parseSetup(content, content.length);
completion.then(function (result) {
assert.equal(result.items.length, 3);
}).then(done, done);
});

it('Provide completion from schema declared in file with several documents', done => {
const documentContent1 = `# yaml-language-server: $schema=${uri} anothermodeline=value\n- `;
const content = `${documentContent1}\n---\n- `;
const completionDoc1 = parseSetup(content, documentContent1.length);
completionDoc1.then(function (result) {
assert.equal(result.items.length, 3, `Expecting 3 items in completion but found ${result.items.length}`);
const completionDoc2 = parseSetup(content, content.length);
completionDoc2.then(function (resultDoc2) {
assert.equal(resultDoc2.items.length, 0, `Expecting no items in completion but found ${resultDoc2.items.length}`);
}).then(done, done);
}, done);

});
});
});
});
86 changes: 86 additions & 0 deletions test/parser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Red Hat. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert = require('assert');
import {parse} from './../src/languageservice/parser/yamlParser07';

suite('Test parser', () => {

describe('test parser', function () {

it('parse emtpy text', () => {
const parsedDocument = parse('');
assert(parsedDocument.documents.length === 0, 'A document has been created for an empty text');
});

it('parse only comment', () => {
const parsedDocument = parse('# a comment');
assert(parsedDocument.documents.length === 1, 'No document has been created when there is a comment');
});

it('parse single document', () => {
const parsedDocument = parse('test');
assert(parsedDocument.documents.length === 1, `A single document should be available but there are ${parsedDocument.documents.length}`);
assert(parsedDocument.documents[0].root.children.length === 0, `There should no children available but there are ${parsedDocument.documents[0].root.children.length}`);
});

it('parse single document with directives', () => {
const parsedDocument = parse('%TAG demo\n---\ntest');
assert(parsedDocument.documents.length === 1, `A single document should be available but there are ${parsedDocument.documents.length}`);
assert(parsedDocument.documents[0].root.children.length === 0, `There should no children available but there are ${parsedDocument.documents[0].root.children.length}`);
});

it('parse 2 documents', () => {
const parsedDocument = parse('test\n---\ntest2');
assert(parsedDocument.documents.length === 2, `2 documents should be available but there are ${parsedDocument.documents.length}`);
assert(parsedDocument.documents[0].root.children.length === 0, `There should no children available but there are ${parsedDocument.documents[0].root.children.length}`);
assert(parsedDocument.documents[0].root.value === 'test');
assert(parsedDocument.documents[1].root.children.length === 0, `There should no children available but there are ${parsedDocument.documents[1].root.children.length}`);
assert(parsedDocument.documents[1].root.value === 'test2');
});

it('parse 3 documents', () => {
const parsedDocument = parse('test\n---\ntest2\n---\ntest3');
assert(parsedDocument.documents.length === 3, `3 documents should be available but there are ${parsedDocument.documents.length}`);
assert(parsedDocument.documents[0].root.children.length === 0, `There should no children available but there are ${parsedDocument.documents[0].root.children.length}`);
assert(parsedDocument.documents[0].root.value === 'test');
assert(parsedDocument.documents[1].root.children.length === 0, `There should no children available but there are ${parsedDocument.documents[1].root.children.length}`);
assert(parsedDocument.documents[1].root.value === 'test2');
assert(parsedDocument.documents[2].root.children.length === 0, `There should no children available but there are ${parsedDocument.documents[2].root.children.length}`);
assert(parsedDocument.documents[2].root.value === 'test3');
});

it('parse single document with comment', () => {
const parsedDocument = parse('# a comment\ntest');
assert(parsedDocument.documents.length === 1, `A single document should be available but there are ${parsedDocument.documents.length}`);
assert(parsedDocument.documents[0].root.children.length === 0, `There should no children available but there are ${parsedDocument.documents[0].root.children.length}`);
assert(parsedDocument.documents[0].lineComments.length === 1);
assert(parsedDocument.documents[0].lineComments[0] === '# a comment');
});

it('parse 2 documents with comment', () => {
const parsedDocument = parse('# a comment\ntest\n---\n# a second comment\ntest2');
assert(parsedDocument.documents.length === 2, `2 documents should be available but there are ${parsedDocument.documents.length}`);
assert(parsedDocument.documents[0].root.children.length === 0, `There should no children available but there are ${parsedDocument.documents[0].root.children.length}`);
assert(parsedDocument.documents[0].lineComments.length === 1);
assert(parsedDocument.documents[0].lineComments[0] === '# a comment');

assert(parsedDocument.documents[1].root.children.length === 0, `There should no children available but there are ${parsedDocument.documents[0].root.children.length}`);
assert(parsedDocument.documents[1].lineComments.length === 1);
assert(parsedDocument.documents[1].lineComments[0] === '# a second comment');
});

it('parse 2 documents with comment and a directive', () => {
const parsedDocument = parse('%TAG demo\n---\n# a comment\ntest\n---\n# a second comment\ntest2');
assert(parsedDocument.documents.length === 2, `2 documents should be available but there are ${parsedDocument.documents.length}`);
assert(parsedDocument.documents[0].root.children.length === 0, `There should no children available but there are ${parsedDocument.documents[0].root.children.length}`);
assert(parsedDocument.documents[0].lineComments.length === 1);
assert(parsedDocument.documents[0].lineComments[0] === '# a comment');

assert(parsedDocument.documents[1].root.children.length === 0, `There should no children available but there are ${parsedDocument.documents[0].root.children.length}`);
assert(parsedDocument.documents[1].lineComments.length === 1);
assert(parsedDocument.documents[1].lineComments[0] === '# a second comment');
});
});
});
Loading