Skip to content

Commit 7a8a078

Browse files
committed
Provide completion for schema ids in modeline #560
potential improvements: - support range when there is already a value provided - provide completion for local yaml schema files Extracted a specific file for modeline related things. fixes #560 Signed-off-by: Aurélien Pupier <apupier@redhat.com>
1 parent de4e8fd commit 7a8a078

File tree

5 files changed

+76
-35
lines changed

5 files changed

+76
-35
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Red Hat, Inc. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
'use strict';
6+
import { SingleYAMLDocument } from '../parser/yamlParser07';
7+
import { JSONDocument } from '../parser/jsonParser07';
8+
9+
/**
10+
* Retrieve schema if declared as modeline.
11+
* Public for testing purpose, not part of the API.
12+
* @param doc
13+
*/
14+
export function getSchemaFromModeline(doc: SingleYAMLDocument | JSONDocument): string {
15+
if (doc instanceof SingleYAMLDocument) {
16+
const yamlLanguageServerModeline = doc.lineComments.find((lineComment) => {
17+
return isModeline(lineComment);
18+
});
19+
if (yamlLanguageServerModeline != undefined) {
20+
const schemaMatchs = yamlLanguageServerModeline.match(/\$schema=\S+/g);
21+
if (schemaMatchs !== null && schemaMatchs.length >= 1) {
22+
if (schemaMatchs.length >= 2) {
23+
console.log(
24+
'Several $schema attributes have been found on the yaml-language-server modeline. The first one will be picked.'
25+
);
26+
}
27+
return schemaMatchs[0].substring('$schema='.length);
28+
}
29+
}
30+
}
31+
return undefined;
32+
}
33+
34+
export function isModeline(lineText: string): boolean {
35+
const matchModeline = lineText.match(/^#\s+yaml-language-server\s*:/g);
36+
return matchModeline !== null && matchModeline.length === 1;
37+
}

src/languageservice/services/yamlCompletion.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import * as nls from 'vscode-nls';
3333
import { setKubernetesParserOption } from '../parser/isKubernetes';
3434
import { isMapContainsEmptyPair } from '../utils/astUtils';
3535
import { indexOf } from '../utils/astUtils';
36+
import { isModeline } from './modelineUtil';
3637

3738
const localize = nls.loadMessageBundle();
3839

@@ -172,10 +173,15 @@ export class YamlCompletion {
172173
this.getCustomTagValueCompletions(collector);
173174
}
174175

176+
let lineContent = textBuffer.getLineContent(position.line);
177+
if (lineContent.endsWith('\n')) {
178+
lineContent = lineContent.substr(0, lineContent.length - 1);
179+
}
180+
175181
try {
176182
const schema = await this.schemaService.getSchemaForResource(document.uri, currentDoc);
177183
if (!schema || schema.errors.length) {
178-
if (position.line === 0 && position.character === 0 && !textBuffer.getLineContent(0).includes('# yaml-language-server')) {
184+
if (position.line === 0 && position.character === 0 && !isModeline(lineContent)) {
179185
const inlineSchemaCompletion = {
180186
kind: CompletionItemKind.Text,
181187
label: 'Inline schema',
@@ -184,6 +190,20 @@ export class YamlCompletion {
184190
};
185191
result.items.push(inlineSchemaCompletion);
186192
}
193+
if (isModeline(lineContent)) {
194+
const schemaIndex = lineContent.indexOf('$schema=');
195+
if (schemaIndex !== -1 && schemaIndex + '$schema='.length === position.character) {
196+
this.schemaService.getRegisteredSchemaIds().forEach((schemaId) => {
197+
const schemaIdCompletion = {
198+
kind: CompletionItemKind.Text,
199+
label: schemaId,
200+
insertText: schemaId,
201+
insertTextFormat: InsertTextFormat.PlainText,
202+
};
203+
result.items.push(schemaIdCompletion);
204+
});
205+
}
206+
}
187207
return result;
188208
}
189209

@@ -201,10 +221,6 @@ export class YamlCompletion {
201221
}
202222
}
203223

204-
let lineContent = textBuffer.getLineContent(position.line);
205-
if (lineContent.endsWith('\n')) {
206-
lineContent = lineContent.substr(0, lineContent.length - 1);
207-
}
208224
if (node) {
209225
if (lineContent.length === 0) {
210226
node = currentDoc.internalDocument.contents as Node;

src/languageservice/services/yamlSchemaService.ts

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { SingleYAMLDocument } from '../parser/yamlParser07';
2424
import { JSONDocument } from '../parser/jsonParser07';
2525
import { parse } from 'yaml';
2626
import * as path from 'path';
27+
import { getSchemaFromModeline } from './modelineUtil';
2728

2829
const localize = nls.loadMessageBundle();
2930

@@ -294,7 +295,7 @@ export class YAMLSchemaService extends JSONSchemaService {
294295
const seen: { [schemaId: string]: boolean } = Object.create(null);
295296
const schemas: string[] = [];
296297

297-
let schemaFromModeline = this.getSchemaFromModeline(doc);
298+
let schemaFromModeline = getSchemaFromModeline(doc);
298299
if (schemaFromModeline !== undefined) {
299300
if (!schemaFromModeline.startsWith('file:') && !schemaFromModeline.startsWith('http')) {
300301
if (!path.isAbsolute(schemaFromModeline)) {
@@ -438,32 +439,6 @@ export class YAMLSchemaService extends JSONSchemaService {
438439
return priorityMapping.get(highestPrio) || [];
439440
}
440441

441-
/**
442-
* Retrieve schema if declared as modeline.
443-
* Public for testing purpose, not part of the API.
444-
* @param doc
445-
*/
446-
public getSchemaFromModeline(doc: SingleYAMLDocument | JSONDocument): string {
447-
if (doc instanceof SingleYAMLDocument) {
448-
const yamlLanguageServerModeline = doc.lineComments.find((lineComment) => {
449-
const matchModeline = lineComment.match(/^#\s+yaml-language-server\s*:/g);
450-
return matchModeline !== null && matchModeline.length === 1;
451-
});
452-
if (yamlLanguageServerModeline != undefined) {
453-
const schemaMatchs = yamlLanguageServerModeline.match(/\$schema=\S+/g);
454-
if (schemaMatchs !== null && schemaMatchs.length >= 1) {
455-
if (schemaMatchs.length >= 2) {
456-
console.log(
457-
'Several $schema attributes have been found on the yaml-language-server modeline. The first one will be picked.'
458-
);
459-
}
460-
return schemaMatchs[0].substring('$schema='.length);
461-
}
462-
}
463-
}
464-
return undefined;
465-
}
466-
467442
private async resolveCustomSchema(schemaUri, doc): ResolvedSchema {
468443
const unresolvedSchema = await this.loadSchema(schemaUri);
469444
const schema = await this.resolveSchemaContent(unresolvedSchema, schemaUri, []);

test/autoCompletion.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1625,7 +1625,7 @@ describe('Auto Completion Tests', () => {
16251625
});
16261626

16271627
it('should not provide modeline completion on first character when modeline already present', async () => {
1628-
const testTextDocument = setupSchemaIDTextDocument('# yaml-language-server', path.join(__dirname, 'test.yaml'));
1628+
const testTextDocument = setupSchemaIDTextDocument('# yaml-language-server:', path.join(__dirname, 'test.yaml'));
16291629
yamlSettings.documents = new TextDocumentTestManager();
16301630
(yamlSettings.documents as TextDocumentTestManager).set(testTextDocument);
16311631
const result = await languageHandler.completionHandler({
@@ -1634,6 +1634,19 @@ describe('Auto Completion Tests', () => {
16341634
});
16351635
assert.strictEqual(result.items.length, 0, `Expecting 0 item in completion but found ${result.items.length}`);
16361636
});
1637+
1638+
it('should provide schema id completion in modeline', async () => {
1639+
const modeline = '# yaml-language-server: $schema=';
1640+
const testTextDocument = setupSchemaIDTextDocument(modeline, path.join(__dirname, 'test.yaml'));
1641+
yamlSettings.documents = new TextDocumentTestManager();
1642+
(yamlSettings.documents as TextDocumentTestManager).set(testTextDocument);
1643+
const result = await languageHandler.completionHandler({
1644+
position: testTextDocument.positionAt(modeline.length),
1645+
textDocument: testTextDocument,
1646+
});
1647+
assert.strictEqual(result.items.length, 1, `Expecting 1 item in completion but found ${result.items.length}`);
1648+
assert.strictEqual(result.items[0].label, 'http://google.com/');
1649+
});
16371650
});
16381651

16391652
describe('Configuration based indentation', () => {

test/schema.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { setupLanguageService, setupTextDocument, TEST_URI } from './utils/testH
1515
import { LanguageService, SchemaPriority } from '../src';
1616
import { Position } from 'vscode-languageserver';
1717
import { LineCounter } from 'yaml';
18+
import { getSchemaFromModeline } from '../src/languageservice/services/modelineUtil';
1819

1920
const requestServiceMock = function (uri: string): Promise<string> {
2021
return Promise.reject<string>(`Resource ${uri} not found.`);
@@ -737,10 +738,9 @@ describe('JSON Schema', () => {
737738
});
738739

739740
function checkReturnSchemaUrl(modeline: string, expectedResult: string): void {
740-
const service = new SchemaService.YAMLSchemaService(schemaRequestServiceForURL, workspaceContext);
741741
const yamlDoc = new parser.SingleYAMLDocument(new LineCounter());
742742
yamlDoc.lineComments = [modeline];
743-
assert.equal(service.getSchemaFromModeline(yamlDoc), expectedResult);
743+
assert.strictEqual(getSchemaFromModeline(yamlDoc), expectedResult);
744744
}
745745
});
746746
});

0 commit comments

Comments
 (0)