diff --git a/CHANGELOG.md b/CHANGELOG.md index c1c381fd..06b89ee4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +### 1.15.0 +- Fix: some small type issues [#918](https://github.com/redhat-developer/yaml-language-server/pull/918) +- Add: volar-service-yaml to clients [#920](https://github.com/redhat-developer/yaml-language-server/pull/920) +- Fix: Remove ide-yaml from known clients [#921](https://github.com/redhat-developer/yaml-language-server/pull/921) +- Fix: schema loading performance [#923](https://github.com/redhat-developer/yaml-language-server/pull/923) +- Fix: undefined error in mergingResult.problemArgs [#924](https://github.com/redhat-developer/yaml-language-server/pull/924) +- Add: unify string insert text for array and property [#934](https://github.com/redhat-developer/yaml-language-server/pull/934) +- Add: Improve some special cases for selection ranges [#939](https://github.com/redhat-developer/yaml-language-server/pull/939) +- Fix: show all enums on hover [#942](https://github.com/redhat-developer/yaml-language-server/pull/942) +- Fix: update README syntax highlighting [#945](https://github.com/redhat-developer/yaml-language-server/pull/945) +- Fix: render examples as yaml on hover [#947](https://github.com/redhat-developer/yaml-language-server/pull/947) +- Fix: snippets in additionalProperties [#951](https://github.com/redhat-developer/yaml-language-server/pull/951) +- Fix: crash when url is undefined [#954](https://github.com/redhat-developer/yaml-language-server/pull/954) +- Fix: Add null check for customTags [#955](https://github.com/redhat-developer/yaml-language-server/pull/955) + +Thanks to [Remco Haszing](https://github.com/remcohaszing), [Petr Spacek](https://github.com/p-spacek), [Tony](https://github.com/Legend-Master), [Gustav Eikaas](https://github.com/GustavEikaas), [Skip Baney](https://github.com/twelvelabs) and [Pierre Prinetti](https://github.com/pierreprinetti) for your contributions. + ### 1.14.0 - Fix: Request textDocument/hover failed with message: Invalid regular expression: /(?s).*/: Invalid group [#874](https://github.com/redhat-developer/yaml-language-server/issues/874) - Fix: nested anyof const [#888](https://github.com/redhat-developer/yaml-language-server/pull/888) diff --git a/src/languageserver/handlers/settingsHandlers.ts b/src/languageserver/handlers/settingsHandlers.ts index 95ddba77..e2351ea7 100644 --- a/src/languageserver/handlers/settingsHandlers.ts +++ b/src/languageserver/handlers/settingsHandlers.ts @@ -81,7 +81,7 @@ export class SettingsHandler { if (settings.yaml.schemaStore) { this.yamlSettings.schemaStoreEnabled = settings.yaml.schemaStore.enable; - if (settings.yaml.schemaStore.url.length !== 0) { + if (settings.yaml.schemaStore.url?.length !== 0) { this.yamlSettings.schemaStoreUrl = settings.yaml.schemaStore.url; } } @@ -180,7 +180,7 @@ export class SettingsHandler { private async setSchemaStoreSettingsIfNotSet(): Promise { const schemaStoreIsSet = this.yamlSettings.schemaStoreSettings.length !== 0; let schemaStoreUrl = ''; - if (this.yamlSettings.schemaStoreUrl.length !== 0) { + if (this.yamlSettings.schemaStoreUrl?.length !== 0) { schemaStoreUrl = this.yamlSettings.schemaStoreUrl; } else { schemaStoreUrl = JSON_SCHEMASTORE_URL; diff --git a/src/languageservice/parser/jsonParser07.ts b/src/languageservice/parser/jsonParser07.ts index b4fdfe4b..c17f4352 100644 --- a/src/languageservice/parser/jsonParser07.ts +++ b/src/languageservice/parser/jsonParser07.ts @@ -888,6 +888,7 @@ function validate( ), source: getSchemaSource(schema, originalSchema), schemaUri: getSchemaUri(schema, originalSchema), + data: { values: schema.enum }, }); } } @@ -907,6 +908,7 @@ function validate( source: getSchemaSource(schema, originalSchema), schemaUri: getSchemaUri(schema, originalSchema), problemArgs: [JSON.stringify(schema.const)], + data: { values: [schema.const] }, }); validationResult.enumValueMatch = false; } else { @@ -1385,6 +1387,7 @@ function validate( length: propertyNode.keyNode.length, }, severity: DiagnosticSeverity.Warning, + code: ErrorCode.PropertyExpected, message: schema.errorMessage || localize('DisallowedExtraPropWarning', MSG_PROPERTY_NOT_ALLOWED, propertyName), source: getSchemaSource(schema, originalSchema), schemaUri: getSchemaUri(schema, originalSchema), diff --git a/src/languageservice/services/yamlCodeActions.ts b/src/languageservice/services/yamlCodeActions.ts index c0ef20b5..da0c7eea 100644 --- a/src/languageservice/services/yamlCodeActions.ts +++ b/src/languageservice/services/yamlCodeActions.ts @@ -28,9 +28,12 @@ import { FlowStyleRewriter } from '../utils/flow-style-rewriter'; import { ASTNode } from '../jsonASTTypes'; import * as _ from 'lodash'; import { SourceToken } from 'yaml/dist/parse/cst'; +import { ErrorCode } from 'vscode-json-languageservice'; interface YamlDiagnosticData { schemaUri: string[]; + values?: string[]; + properties?: string[]; } export class YamlCodeActions { private indentation = ' '; @@ -54,6 +57,7 @@ export class YamlCodeActions { result.push(...this.getUnusedAnchorsDelete(params.context.diagnostics, document)); result.push(...this.getConvertToBlockStyleActions(params.context.diagnostics, document)); result.push(...this.getKeyOrderActions(params.context.diagnostics, document)); + result.push(...this.getQuickFixForPropertyOrValueMismatch(params.context.diagnostics, document)); return result; } @@ -221,7 +225,7 @@ export class YamlCodeActions { const results: CodeAction[] = []; for (const diagnostic of diagnostics) { if (diagnostic.code === 'flowMap' || diagnostic.code === 'flowSeq') { - const node = getNodeforDiagnostic(document, diagnostic); + const node = getNodeForDiagnostic(document, diagnostic); if (isMap(node.internalNode) || isSeq(node.internalNode)) { const blockTypeDescription = isMap(node.internalNode) ? 'map' : 'sequence'; const rewriter = new FlowStyleRewriter(this.indentation); @@ -242,7 +246,7 @@ export class YamlCodeActions { const results: CodeAction[] = []; for (const diagnostic of diagnostics) { if (diagnostic?.code === 'mapKeyOrder') { - let node = getNodeforDiagnostic(document, diagnostic); + let node = getNodeForDiagnostic(document, diagnostic); while (node && node.type !== 'object') { node = node.parent; } @@ -292,8 +296,8 @@ export class YamlCodeActions { item.value.end.splice(newLineIndex, 1); } } else if (item.value?.type === 'block-scalar') { - const nwline = item.value.props.find((p) => p.type === 'newline'); - if (!nwline) { + const newline = item.value.props.find((p) => p.type === 'newline'); + if (!newline) { item.value.props.push({ type: 'newline', indent: 0, offset: item.value.offset, source: '\n' } as SourceToken); } } @@ -312,9 +316,52 @@ export class YamlCodeActions { } return results; } + + /** + * Check if diagnostic contains info for quick fix + * Supports Enum/Const/Property mismatch + */ + private getPossibleQuickFixValues(diagnostic: Diagnostic): string[] | undefined { + if (typeof diagnostic.data !== 'object') { + return; + } + if ( + diagnostic.code === ErrorCode.EnumValueMismatch && + 'values' in diagnostic.data && + Array.isArray((diagnostic.data as YamlDiagnosticData).values) + ) { + return (diagnostic.data as YamlDiagnosticData).values; + } else if ( + diagnostic.code === ErrorCode.PropertyExpected && + 'properties' in diagnostic.data && + Array.isArray((diagnostic.data as YamlDiagnosticData).properties) + ) { + return (diagnostic.data as YamlDiagnosticData).properties; + } + } + + private getQuickFixForPropertyOrValueMismatch(diagnostics: Diagnostic[], document: TextDocument): CodeAction[] { + const results: CodeAction[] = []; + for (const diagnostic of diagnostics) { + const values = this.getPossibleQuickFixValues(diagnostic); + if (!values?.length) { + continue; + } + for (const value of values) { + results.push( + CodeAction.create( + value, + createWorkspaceEdit(document.uri, [TextEdit.replace(diagnostic.range, value)]), + CodeActionKind.QuickFix + ) + ); + } + } + return results; + } } -function getNodeforDiagnostic(document: TextDocument, diagnostic: Diagnostic): ASTNode { +function getNodeForDiagnostic(document: TextDocument, diagnostic: Diagnostic): ASTNode { const yamlDocuments = yamlDocumentsCache.getYamlDocument(document); const startOffset = document.offsetAt(diagnostic.range.start); const yamlDoc = matchOffsetToDocument(startOffset, yamlDocuments); diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index 99e061f0..513a23d6 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -293,7 +293,7 @@ export class YamlCompletion { proposed, }; - if (this.customTags.length > 0) { + if (this.customTags && this.customTags.length > 0) { this.getCustomTagValueCompletions(collector); } @@ -925,7 +925,8 @@ export class YamlCompletion { if (propertySchema) { this.addSchemaValueCompletions(propertySchema, separatorAfter, collector, types, 'value'); } - } else if (s.schema.additionalProperties) { + } + if (s.schema.additionalProperties) { this.addSchemaValueCompletions(s.schema.additionalProperties, separatorAfter, collector, types, 'value'); } } @@ -1205,7 +1206,7 @@ export class YamlCompletion { insertText = `\${${insertIndex++}:0}`; break; case 'string': - insertText = `\${${insertIndex++}:""}`; + insertText = `\${${insertIndex++}}`; break; case 'object': { diff --git a/src/languageservice/services/yamlSelectionRanges.ts b/src/languageservice/services/yamlSelectionRanges.ts index b361ba8d..f60dcc91 100644 --- a/src/languageservice/services/yamlSelectionRanges.ts +++ b/src/languageservice/services/yamlSelectionRanges.ts @@ -3,81 +3,72 @@ import { yamlDocumentsCache } from '../parser/yaml-documents'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { ASTNode } from 'vscode-json-languageservice'; -export function getSelectionRanges(document: TextDocument, positions: Position[]): SelectionRange[] | undefined { - if (!document) { - return; - } +export function getSelectionRanges(document: TextDocument, positions: Position[]): SelectionRange[] { const doc = yamlDocumentsCache.getYamlDocument(document); return positions.map((position) => { const ranges = getRanges(position); - let current: SelectionRange; + let current: SelectionRange | undefined; for (const range of ranges) { current = SelectionRange.create(range, current); } - if (!current) { - current = SelectionRange.create({ - start: position, - end: position, - }); - } - return current; + return current ?? SelectionRange.create({ start: position, end: position }); }); function getRanges(position: Position): Range[] { const offset = document.offsetAt(position); const result: Range[] = []; for (const ymlDoc of doc.documents) { - let currentNode: ASTNode; - let firstNodeOffset: number; - let isFirstNode = true; + let currentNode: ASTNode | undefined; + let overrideStartOffset: number | undefined; ymlDoc.visit((node) => { const endOffset = node.offset + node.length; // Skip if end offset doesn't even reach cursor position if (endOffset < offset) { return true; } - let startOffset = node.offset; - // Recheck start offset with the trimmed one in case of this - // key: - // - value - // ↑ - if (startOffset > offset) { - const nodePosition = document.positionAt(startOffset); - if (nodePosition.line !== position.line) { - return true; - } - const lineBeginning = { line: nodePosition.line, character: 0 }; - const text = document.getText({ - start: lineBeginning, - end: nodePosition, - }); - if (text.trim().length !== 0) { + // Skip if we're ending at new line + // times: + // - second: 1 + // millisecond: 10 + // | - second: 2 + // ↑ millisecond: 0 + // (| is actually part of { second: 1, millisecond: 10 }) + // \r\n doesn't matter here + if (getTextFromOffsets(endOffset - 1, endOffset) === '\n') { + if (endOffset - 1 < offset) { return true; } - startOffset = document.offsetAt(lineBeginning); - if (startOffset > offset) { + } + + let startOffset = node.offset; + if (startOffset > offset) { + // Recheck start offset for some special cases + const newOffset = getStartOffsetForSpecialCases(node, position); + if (!newOffset || newOffset > offset) { return true; } + startOffset = newOffset; } + // Allow equal for children to override if (!currentNode || startOffset >= currentNode.offset) { currentNode = node; - firstNodeOffset = startOffset; + overrideStartOffset = startOffset; } return true; }); while (currentNode) { - const startOffset = isFirstNode ? firstNodeOffset : currentNode.offset; + const startOffset = overrideStartOffset ?? currentNode.offset; const endOffset = currentNode.offset + currentNode.length; const range = { start: document.positionAt(startOffset), end: document.positionAt(endOffset), }; const text = document.getText(range); - const trimmedText = text.trimEnd(); - const trimmedLength = text.length - trimmedText.length; - if (trimmedLength > 0) { - range.end = document.positionAt(endOffset - trimmedLength); + const trimmedText = trimEndNewLine(text); + const trimmedEndOffset = startOffset + trimmedText.length; + if (trimmedEndOffset >= offset) { + range.end = document.positionAt(trimmedEndOffset); } // Add a jump between '' "" {} [] const isSurroundedBy = (startCharacter: string, endCharacter?: string): boolean => { @@ -95,7 +86,7 @@ export function getSelectionRanges(document: TextDocument, positions: Position[] } result.push(range); currentNode = currentNode.parent; - isFirstNode = false; + overrideStartOffset = undefined; } // A position can't be in multiple documents if (result.length > 0) { @@ -104,4 +95,48 @@ export function getSelectionRanges(document: TextDocument, positions: Position[] } return result.reverse(); } + + function getStartOffsetForSpecialCases(node: ASTNode, position: Position): number | undefined { + const nodeStartPosition = document.positionAt(node.offset); + if (nodeStartPosition.line !== position.line) { + return; + } + + if (node.parent?.type === 'array') { + // array: + // - value + // ↑ + if (getTextFromOffsets(node.offset - 2, node.offset) === '- ') { + return node.offset - 2; + } + } + + if (node.type === 'array' || node.type === 'object') { + // array: + // - value + // ↑ + const lineBeginning = { line: nodeStartPosition.line, character: 0 }; + const text = document.getText({ start: lineBeginning, end: nodeStartPosition }); + if (text.trim().length === 0) { + return document.offsetAt(lineBeginning); + } + } + } + + function getTextFromOffsets(startOffset: number, endOffset: number): string { + return document.getText({ + start: document.positionAt(startOffset), + end: document.positionAt(endOffset), + }); + } +} + +function trimEndNewLine(str: string): string { + if (str.endsWith('\r\n')) { + return str.substring(0, str.length - 2); + } + if (str.endsWith('\n')) { + return str.substring(0, str.length - 1); + } + return str; } diff --git a/src/languageservice/yamlLanguageService.ts b/src/languageservice/yamlLanguageService.ts index f688fd40..697c9d70 100644 --- a/src/languageservice/yamlLanguageService.ts +++ b/src/languageservice/yamlLanguageService.ts @@ -181,7 +181,7 @@ export interface LanguageService { deleteSchemaContent: (schemaDeletions: SchemaDeletions) => void; deleteSchemasWhole: (schemaDeletions: SchemaDeletionsAll) => void; getFoldingRanges: (document: TextDocument, context: FoldingRangesContext) => FoldingRange[] | null; - getSelectionRanges: (document: TextDocument, positions: Position[]) => SelectionRange[] | undefined; + getSelectionRanges: (document: TextDocument, positions: Position[]) => SelectionRange[]; getCodeAction: (document: TextDocument, params: CodeActionParams) => CodeAction[] | undefined; getCodeLens: (document: TextDocument) => PromiseLike | CodeLens[] | undefined; resolveCodeLens: (param: CodeLens) => PromiseLike | CodeLens; diff --git a/test/autoCompletion.test.ts b/test/autoCompletion.test.ts index 78b14996..eba8fa6d 100644 --- a/test/autoCompletion.test.ts +++ b/test/autoCompletion.test.ts @@ -1047,7 +1047,7 @@ describe('Auto Completion Tests', () => { const completion = parseSetup(content, content.lastIndexOf('Ba') + 2); // pos: 3+2 completion .then(function (result) { - assert.strictEqual('fooBar:\n - ${1:""}', result.items[0].insertText); + assert.strictEqual('fooBar:\n - ${1}', result.items[0].insertText); }) .then(done, done); }); @@ -3126,5 +3126,10 @@ describe('Auto Completion Tests', () => { expect(result.items.map((i) => i.label)).to.have.members(['fruit', 'vegetable']); }); }); + it('Should function when settings are undefined', async () => { + languageService.configure({ completion: true }); + const content = ''; + await parseSetup(content, 0); + }); }); }); diff --git a/test/autoCompletionFix.test.ts b/test/autoCompletionFix.test.ts index 81053315..c720ce76 100644 --- a/test/autoCompletionFix.test.ts +++ b/test/autoCompletionFix.test.ts @@ -482,7 +482,7 @@ objB: expect(completion.items.length).equal(1); expect(completion.items[0]).to.be.deep.equal( - createExpectedCompletion('objectWithArray', 'objectWithArray:\n - ${1:""}', 1, 4, 1, 4, 10, 2, { + createExpectedCompletion('objectWithArray', 'objectWithArray:\n - ${1}', 1, 4, 1, 4, 10, 2, { documentation: '', }) ); @@ -1183,6 +1183,30 @@ objB: expect(completion.items[0].insertText).to.be.equal('test1'); }); + it('should suggest defaultSnippets from additionalProperties', async () => { + const schema: JSONSchema = { + type: 'object', + properties: { + id: { + type: 'string', + }, + }, + additionalProperties: { + anyOf: [ + { + type: 'string', + defaultSnippets: [{ label: 'snippet', body: 'snippetBody' }], + }, + ], + }, + }; + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = 'value: |\n|'; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['snippetBody']); + }); + describe('should suggest prop of the object (based on not completed prop name)', () => { const schema: JSONSchema = { definitions: { diff --git a/test/schemaValidation.test.ts b/test/schemaValidation.test.ts index 71268db7..829310c1 100644 --- a/test/schemaValidation.test.ts +++ b/test/schemaValidation.test.ts @@ -26,6 +26,7 @@ import { KUBERNETES_SCHEMA_URL } from '../src/languageservice/utils/schemaUrls'; import { IProblem } from '../src/languageservice/parser/jsonParser07'; import { JSONSchema } from '../src/languageservice/jsonSchema'; import { TestTelemetry } from './utils/testsTypes'; +import { ErrorCode } from 'vscode-json-languageservice'; describe('Validation Tests', () => { let languageSettingsSetup: ServiceSetup; @@ -396,7 +397,8 @@ describe('Validation Tests', () => { 4, DiagnosticSeverity.Error, `yaml-schema: file:///${SCHEMA_ID}`, - `file:///${SCHEMA_ID}` + `file:///${SCHEMA_ID}`, + ErrorCode.PropertyExpected ) ); }) @@ -1289,7 +1291,7 @@ obj: 4, 18, DiagnosticSeverity.Error, - 'yaml-schema: Package', + 'yaml-schema: Composer Package', 'https://raw.githubusercontent.com/composer/composer/master/res/composer-schema.json' ) ); @@ -1312,6 +1314,7 @@ obj: DiagnosticSeverity.Error, 'yaml-schema: Drone CI configuration file', 'https://json.schemastore.org/drone', + ErrorCode.PropertyExpected, { properties: [ 'type', diff --git a/test/utils/verifyError.ts b/test/utils/verifyError.ts index ea7f17c5..d2769c74 100644 --- a/test/utils/verifyError.ts +++ b/test/utils/verifyError.ts @@ -39,9 +39,19 @@ export function createDiagnosticWithData( severity: DiagnosticSeverity = 1, source = 'YAML', schemaUri: string | string[], + code: string | number = ErrorCode.Undefined, data: Record = {} ): Diagnostic { - const diagnostic: Diagnostic = createExpectedError(message, startLine, startCharacter, endLine, endCharacter, severity, source); + const diagnostic: Diagnostic = createExpectedError( + message, + startLine, + startCharacter, + endLine, + endCharacter, + severity, + source, + code + ); diagnostic.data = { schemaUri: typeof schemaUri === 'string' ? [schemaUri] : schemaUri, ...data }; return diagnostic; } diff --git a/test/yamlCodeActions.test.ts b/test/yamlCodeActions.test.ts index aa2b72ce..447cfdcb 100644 --- a/test/yamlCodeActions.test.ts +++ b/test/yamlCodeActions.test.ts @@ -22,6 +22,7 @@ import { setupTextDocument, TEST_URI } from './utils/testHelper'; import { createDiagnosticWithData, createExpectedError, createUnusedAnchorDiagnostic } from './utils/verifyError'; import { YamlCommands } from '../src/commands'; import { LanguageSettings } from '../src'; +import { ErrorCode } from 'vscode-json-languageservice'; const expect = chai.expect; chai.use(sinonChai); @@ -377,4 +378,58 @@ animals: [dog , cat , mouse] `; ]); }); }); + + describe('Enum value or property mismatch quick fix', () => { + it('should generate proper action for enum mismatch', () => { + const doc = setupTextDocument('foo: value1'); + const diagnostic = createDiagnosticWithData( + 'message', + 0, + 5, + 0, + 11, + DiagnosticSeverity.Hint, + 'YAML', + 'schemaUri', + ErrorCode.EnumValueMismatch, + { values: ['valueX', 'valueY'] } + ); + const params: CodeActionParams = { + context: CodeActionContext.create([diagnostic]), + range: undefined, + textDocument: TextDocumentIdentifier.create(TEST_URI), + }; + const actions = new YamlCodeActions(clientCapabilities); + const result = actions.getCodeAction(doc, params); + expect(result.map((r) => r.title)).deep.equal(['valueX', 'valueY']); + expect(result[0].edit.changes[TEST_URI]).deep.equal([TextEdit.replace(Range.create(0, 5, 0, 11), 'valueX')]); + }); + + it('should generate proper action for wrong property', () => { + const doc = setupTextDocument('foo: value1'); + const diagnostic = createDiagnosticWithData( + 'message', + 0, + 0, + 0, + 3, + DiagnosticSeverity.Hint, + 'YAML', + 'schemaUri', + ErrorCode.PropertyExpected, + { + properties: ['fooX', 'fooY'], + } + ); + const params: CodeActionParams = { + context: CodeActionContext.create([diagnostic]), + range: undefined, + textDocument: TextDocumentIdentifier.create(TEST_URI), + }; + const actions = new YamlCodeActions(clientCapabilities); + const result = actions.getCodeAction(doc, params); + expect(result.map((r) => r.title)).deep.equal(['fooX', 'fooY']); + expect(result[0].edit.changes[TEST_URI]).deep.equal([TextEdit.replace(Range.create(0, 0, 0, 3), 'fooX')]); + }); + }); }); diff --git a/test/yamlSelectionRanges.test.ts b/test/yamlSelectionRanges.test.ts index 3ac78ab4..93fd67e7 100644 --- a/test/yamlSelectionRanges.test.ts +++ b/test/yamlSelectionRanges.test.ts @@ -12,14 +12,16 @@ function isRangesEqual(range1: Range, range2: Range): boolean { ); } -function expectSelections(selectionRange: SelectionRange, ranges: Range[]): void { +function expectSelections(selectionRange: SelectionRange | undefined, ranges: Range[]): void { for (const range of ranges) { - expect(selectionRange.range).eql(range); + expect(selectionRange?.range).eql(range); + // Deduplicate ranges - while (selectionRange.parent && isRangesEqual(selectionRange.range, selectionRange.parent.range)) { + while (selectionRange?.parent && isRangesEqual(selectionRange.range, selectionRange.parent.range)) { selectionRange = selectionRange.parent; } - selectionRange = selectionRange.parent; + + selectionRange = selectionRange?.parent; } } @@ -62,6 +64,20 @@ key: { start: { line: 1, character: 0 }, end: { line: 3, character: 8 } }, ]); + positions = [ + { + line: 3, + character: 3, + }, + ]; + ranges = getSelectionRanges(document, positions); + expect(ranges.length).equal(positions.length); + expectSelections(ranges[0], [ + { start: { line: 3, character: 2 }, end: { line: 3, character: 8 } }, + { start: { line: 2, character: 2 }, end: { line: 3, character: 8 } }, + { start: { line: 1, character: 0 }, end: { line: 3, character: 8 } }, + ]); + positions = [ { line: 2, @@ -76,6 +92,64 @@ key: ]); }); + it('selection ranges for array of objects', () => { + const yaml = ` +times: + - second: 1 + millisecond: 10 + - second: 2 + millisecond: 0 + `; + let positions: Position[] = [ + { + line: 4, + character: 0, + }, + ]; + const document = setupTextDocument(yaml); + let ranges = getSelectionRanges(document, positions); + expect(ranges.length).equal(positions.length); + expectSelections(ranges[0], [ + { start: { line: 2, character: 2 }, end: { line: 5, character: 18 } }, + { start: { line: 1, character: 0 }, end: { line: 5, character: 18 } }, + ]); + + positions = [ + { + line: 5, + character: 2, + }, + ]; + ranges = getSelectionRanges(document, positions); + expect(ranges.length).equal(positions.length); + expectSelections(ranges[0], [ + { start: { line: 4, character: 4 }, end: { line: 5, character: 18 } }, + { start: { line: 2, character: 2 }, end: { line: 5, character: 18 } }, + { start: { line: 1, character: 0 }, end: { line: 5, character: 18 } }, + ]); + }); + + it('selection ranges for trailing spaces', () => { + const yaml = ` +key: + - 1 + - 2 \t + `; + const positions: Position[] = [ + { + line: 2, + character: 9, + }, + ]; + const document = setupTextDocument(yaml); + const ranges = getSelectionRanges(document, positions); + expect(ranges.length).equal(positions.length); + expectSelections(ranges[0], [ + { start: { line: 2, character: 2 }, end: { line: 3, character: 9 } }, + { start: { line: 1, character: 0 }, end: { line: 3, character: 9 } }, + ]); + }); + it('selection ranges jump for "" \'\'', () => { const yaml = ` - "word" diff --git a/tsconfig.json b/tsconfig.json index 5294b266..fa23d249 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "outDir": "./out/server", "sourceMap": true, "target": "es2020", - "allowSyntheticDefaultImports": true, + "allowSyntheticDefaultImports": true, "skipLibCheck": true }, "include": [ "src", "test" ],