diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 53c175220..5c223ea12 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -6,7 +6,7 @@ { "label": "watch typescript", "type": "shell", - "command": "yarn run watch", + "command": "npm run watch", "presentation": { "reveal": "never" }, diff --git a/l10n/bundle.l10n.de.json b/l10n/bundle.l10n.de.json index 4244abdd1..86e8504a2 100644 --- a/l10n/bundle.l10n.de.json +++ b/l10n/bundle.l10n.de.json @@ -52,5 +52,7 @@ "flowStyleMapForbidden": "Flow-Stil-Mapping ist verboten", "flowStyleSeqForbidden": "Flow-Stil-Sequenz ist verboten", "unUsedAnchor": "Nicht verwendeter Anker \"{0}\"", - "unUsedAlias": "Nicht aufgelöstes Alias \"{0}\"" + "unUsedAlias": "Nicht aufgelöstes Alias \"{0}\"", + "convertToFoldedBlockString": "Konvertieren Sie die Zeichenfolge in eine gefaltete Blockzeichenfolge", + "convertToLiteralBlockString": "Konvertieren Sie die Zeichenfolge in eine wörtliche Blockzeichenfolge" } diff --git a/l10n/bundle.l10n.fr.json b/l10n/bundle.l10n.fr.json index b34781920..8f05d89ae 100644 --- a/l10n/bundle.l10n.fr.json +++ b/l10n/bundle.l10n.fr.json @@ -52,5 +52,7 @@ "flowStyleMapForbidden": "Le mappage de style de flux est interdit", "flowStyleSeqForbidden": "La séquence de style Flow est interdite", "unUsedAnchor": "Ancre inutilisée '{0}'", - "unUsedAlias": "Alias ​​non résolu '{0}'" + "unUsedAlias": "Alias non résolu '{0}'", + "convertToFoldedBlockString": "Convertir la chaîne en style de bloc pliée", + "convertToLiteralBlockString": "Convertir la chaîne en style de bloc littérale" } diff --git a/l10n/bundle.l10n.ja.json b/l10n/bundle.l10n.ja.json index 69ef7bc79..c15942e01 100644 --- a/l10n/bundle.l10n.ja.json +++ b/l10n/bundle.l10n.ja.json @@ -52,5 +52,7 @@ "flowStyleMapForbidden": "フロースタイルのマッピングは禁止されています", "flowStyleSeqForbidden": "フロースタイルのシーケンスは禁止されています", "unUsedAnchor": "未使用のアンカー \"{0}\"", - "unUsedAlias": "未解決のエイリアス \"{0}\"" + "unUsedAlias": "未解決のエイリアス \"{0}\"", + "convertToFoldedBlockString": "文字列を折り畳みブロック文字列に変換する", + "convertToLiteralBlockString": "文字列をリテラルブロック文字列に変換する" } diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index c33936f61..fe954f00c 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -52,5 +52,7 @@ "flowStyleMapForbidden": "Flow style mapping is forbidden", "flowStyleSeqForbidden": "Flow style sequence is forbidden", "unUsedAnchor": "Unused anchor \"{0}\"", - "unUsedAlias": "Unresolved alias \"{0}\"" + "unUsedAlias": "Unresolved alias \"{0}\"", + "convertToFoldedBlockString": "Convert string to folded block string", + "convertToLiteralBlockString": "Convert string to literal block string" } diff --git a/l10n/bundle.l10n.ko.json b/l10n/bundle.l10n.ko.json index cfaf4197a..430b0dd15 100644 --- a/l10n/bundle.l10n.ko.json +++ b/l10n/bundle.l10n.ko.json @@ -52,5 +52,7 @@ "flowStyleMapForbidden": "Flow 스타일 맵 사용이 금지됨", "flowStyleSeqForbidden": "Flow 스타일 시퀀스 사용이 금지됨", "unUsedAnchor": "사용되지 않은 앵커 \"{0}\"", - "unUsedAlias": "해결되지 않은 별칭 \"{0}\"" + "unUsedAlias": "해결되지 않은 별칭 \"{0}\"", + "convertToFoldedBlockString": "문자열을 접힌 블록 문자열로 변환합니다.", + "convertToLiteralBlockString": "문자열을 리터럴 블록 문자열로 변환합니다." } diff --git a/l10n/bundle.l10n.zh-cn.json b/l10n/bundle.l10n.zh-cn.json index fcd7a5172..05e6964e6 100644 --- a/l10n/bundle.l10n.zh-cn.json +++ b/l10n/bundle.l10n.zh-cn.json @@ -52,5 +52,7 @@ "flowStyleMapForbidden": "禁止使用 flow 样式的映射", "flowStyleSeqForbidden": "禁止使用 flow 样式的序列", "unUsedAnchor": "未使用的锚点 \"{0}\"", - "unUsedAlias": "未解析的别名 \"{0}\"" + "unUsedAlias": "未解析的别名 \"{0}\"", + "convertToFoldedBlockString": "将字符串转换为折叠块字符串", + "convertToLiteralBlockString": "将字符串转换为文字块字符串" } diff --git a/l10n/bundle.l10n.zh-tw.json b/l10n/bundle.l10n.zh-tw.json index 6aea89590..311ab5d9e 100644 --- a/l10n/bundle.l10n.zh-tw.json +++ b/l10n/bundle.l10n.zh-tw.json @@ -52,5 +52,7 @@ "flowStyleMapForbidden": "禁止使用 Flow 風格的對應", "flowStyleSeqForbidden": "禁止使用 Flow 風格的序列", "unUsedAnchor": "未使用的錨點 \"{0}\"", - "unUsedAlias": "未解析的別名 \"{0}\"" + "unUsedAlias": "未解析的別名 \"{0}\"", + "convertToFoldedBlockString": "將字串轉換為折疊塊字串", + "convertToLiteralBlockString": "將字串轉換為文字區塊字串" } diff --git a/src/languageservice/services/yamlCodeActions.ts b/src/languageservice/services/yamlCodeActions.ts index 2b24c87bb..c08c70d73 100644 --- a/src/languageservice/services/yamlCodeActions.ts +++ b/src/languageservice/services/yamlCodeActions.ts @@ -3,6 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as l10n from '@vscode/l10n'; +import * as _ from 'lodash'; +import * as path from 'path'; +import { ErrorCode } from 'vscode-json-languageservice'; +import { ClientCapabilities, CodeActionParams } from 'vscode-languageserver-protocol'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { CodeAction, @@ -14,22 +19,18 @@ import { TextEdit, WorkspaceEdit, } from 'vscode-languageserver-types'; -import { ClientCapabilities, CodeActionParams } from 'vscode-languageserver-protocol'; +import { CST, isMap, isScalar, isSeq, Scalar, visit, YAMLMap } from 'yaml'; +import { SourceToken } from 'yaml/dist/parse/cst'; import { YamlCommands } from '../../commands'; -import * as path from 'path'; -import { TextBuffer } from '../utils/textBuffer'; -import { LanguageSettings } from '../yamlLanguageService'; +import { ASTNode } from '../jsonASTTypes'; import { YAML_SOURCE } from '../parser/jsonParser07'; -import { getFirstNonWhitespaceCharacterAfterOffset } from '../utils/strings'; -import { matchOffsetToDocument } from '../utils/arrUtils'; -import { CST, isMap, isSeq, YAMLMap } from 'yaml'; import { yamlDocumentsCache } from '../parser/yaml-documents'; +import { matchOffsetToDocument } from '../utils/arrUtils'; +import { BlockStringRewriter } from '../utils/block-string-rewriter'; 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'; -import * as l10n from '@vscode/l10n'; +import { getFirstNonWhitespaceCharacterAfterOffset } from '../utils/strings'; +import { TextBuffer } from '../utils/textBuffer'; +import { LanguageSettings } from '../yamlLanguageService'; interface YamlDiagnosticData { schemaUri: string[]; @@ -38,11 +39,13 @@ interface YamlDiagnosticData { } export class YamlCodeActions { private indentation = ' '; + private lineWidth = 80; constructor(private readonly clientCapabilities: ClientCapabilities) {} - configure(settings: LanguageSettings): void { + configure(settings: LanguageSettings, printWidth: number): void { this.indentation = settings.indentation; + this.lineWidth = printWidth; } getCodeAction(document: TextDocument, params: CodeActionParams): CodeAction[] | undefined { @@ -57,6 +60,7 @@ export class YamlCodeActions { result.push(...this.getTabToSpaceConverting(params.context.diagnostics, document)); result.push(...this.getUnusedAnchorsDelete(params.context.diagnostics, document)); result.push(...this.getConvertToBlockStyleActions(params.context.diagnostics, document)); + result.push(...this.getConvertStringToBlockStyleActions(params.context.diagnostics, document)); result.push(...this.getKeyOrderActions(params.context.diagnostics, document)); result.push(...this.getQuickFixForPropertyOrValueMismatch(params.context.diagnostics, document)); @@ -243,6 +247,49 @@ export class YamlCodeActions { return results; } + private getConvertStringToBlockStyleActions(diagnostics: Diagnostic[], document: TextDocument): CodeAction[] { + const yamlDocument = yamlDocumentsCache.getYamlDocument(document); + + const results: CodeAction[] = []; + for (const singleYamlDocument of yamlDocument.documents) { + const matchingNodes: Scalar[] = []; + visit(singleYamlDocument.internalDocument, (key, node) => { + if (isScalar(node)) { + if (node.type === 'QUOTE_DOUBLE' || node.type === 'QUOTE_SINGLE') { + if (typeof node.value === 'string' && (node.value.indexOf('\n') >= 0 || node.value.length > this.lineWidth)) { + matchingNodes.push(>node); + } + } + } + }); + for (const node of matchingNodes) { + const range = Range.create(document.positionAt(node.range[0]), document.positionAt(node.range[2])); + const rewriter = new BlockStringRewriter(this.indentation, this.lineWidth); + const foldedBlockScalar = rewriter.writeFoldedBlockScalar(node); + if (foldedBlockScalar !== null) { + results.push( + CodeAction.create( + l10n.t('convertToFoldedBlockString'), + createWorkspaceEdit(document.uri, [TextEdit.replace(range, foldedBlockScalar)]), + CodeActionKind.Refactor + ) + ); + } + const literalBlockScalar = rewriter.writeLiteralBlockScalar(node); + if (literalBlockScalar !== null) { + results.push( + CodeAction.create( + l10n.t('convertToLiteralBlockString'), + createWorkspaceEdit(document.uri, [TextEdit.replace(range, literalBlockScalar)]), + CodeActionKind.Refactor + ) + ); + } + } + } + return results; + } + private getKeyOrderActions(diagnostics: Diagnostic[], document: TextDocument): CodeAction[] { const results: CodeAction[] = []; for (const diagnostic of diagnostics) { diff --git a/src/languageservice/utils/block-string-rewriter.ts b/src/languageservice/utils/block-string-rewriter.ts new file mode 100644 index 000000000..17e4a9755 --- /dev/null +++ b/src/languageservice/utils/block-string-rewriter.ts @@ -0,0 +1,233 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) IBM Corp. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CST, Scalar } from 'yaml'; +import { FlowScalar } from 'yaml/dist/parse/cst'; + +export class BlockStringRewriter { + constructor( + private readonly indentation: string, + private readonly maxLineLength: number + ) {} + + public writeFoldedBlockScalar(node: Scalar): string | null { + if (node.type !== 'QUOTE_DOUBLE' && node.type !== 'QUOTE_SINGLE') { + return null; + } + + const stringContent = node.value; + const currentIndentNum = (node.srcToken as FlowScalar).indent; + let indentText = this.indentation; + for (let i = 0; i < currentIndentNum / this.indentation.length; i++) { + indentText += this.indentation; + } + + const lines: string[] = []; + const splitLines = stringContent.split('\n'); + for (const line of splitLines) { + let remainder = line; + slicing: while (remainder.length > this.maxLineLength) { + let location = this.maxLineLength; + // for folded strings, space characters are placed in place of each line break + // so we need to split the line on a space and remove the space + while (!/ /.test(remainder.charAt(location))) { + location++; + if (location >= remainder.length) { + break slicing; + } + } + // however any leading space characters will be taken literally and also a newline gets inserted + // so instead we need them to be trailing + // which could be problematic as "trim trailing whitespace" is a common setting to have enabled but oh well + while (/ /.test(remainder.charAt(location))) { + location++; + if (location >= remainder.length) { + break slicing; + } + } + const head = remainder.substring( + 0, + location - 1 /* -1 to remove one space character, which is automatically added between lines */ + ); + lines.push(head); + remainder = remainder.substring(location); + } + lines.push(remainder); + lines.push('\n'); + } + // no trailng newline + lines.pop(); + + for (let i = 1; i < lines.length; ) { + if (/^[ \t]+$/.test(lines[i])) { + if (lines[i - 1] === '\n' || lines[i - 1] === '') { + lines.splice(i - 1, 1); + // i now points to the next entry, + // i - 1 points to the current entry + // so do not increment i + continue; + } else { + // It's unconvertable, give up + // Explanation: + // If the line of text is only whitespace and it's more whitespace than the expected indentation, + // then it's joined with the previous line with a real newline instead of a space. + // This means an extra newline gets inserted if we change nothing. + // We can avoid this if the preceeding text is a newline, + // because we can just remove the preceeding newline to compensate, + // but if it's not we are SOL + return null; + } + } + i++; + } + + let blockScalarHeaderSource = '>'; + if (lines[lines.length - 1] !== '\n' && lines[lines.length - 1] !== '') { + blockScalarHeaderSource += '-'; + } else if ( + (lines[lines.length - 2] === '\n' || lines[lines.length - 2] === '') && + lines[lines.length - 3] !== '\n' && + lines[lines.length - 3] !== '' + ) { + lines.splice(lines.length - 2, 2); + } else { + blockScalarHeaderSource += '+'; + } + if (/ /.test(stringContent.charAt(0))) { + blockScalarHeaderSource += `${indentText.length}`; + } + + const newProps: CST.Token[] = lines.flatMap((line) => { + if (line === '\n' || line === '') { + // newlines can be represented as two newlines in folded blocks + return [ + { + type: 'newline', + indent: 0, + offset: node.srcToken.offset, + source: '\n', + }, + ]; + } + return [ + { + type: 'newline', + indent: 0, + offset: node.srcToken.offset, + source: '\n', + }, + { + type: 'space', + indent: 0, + offset: node.srcToken.offset, + source: indentText, + }, + { + type: 'scalar', + indent: 0, + offset: node.srcToken.offset, + source: line, + }, + ]; + }); + + newProps.unshift({ + type: 'block-scalar-header', + source: blockScalarHeaderSource, + offset: node.srcToken.offset, + indent: 0, + }); + + const blockString: CST.BlockScalar = { + type: 'block-scalar', + offset: node.srcToken.offset, + indent: 0, + source: '', + props: newProps, + }; + + return CST.stringify(blockString as CST.Token); + } + + public writeLiteralBlockScalar(node: Scalar): string | null { + if (node.type !== 'QUOTE_DOUBLE' && node.type !== 'QUOTE_SINGLE') { + return null; + } + + const stringContent = node.value; + // I don't think it's worth it + if (stringContent.indexOf('\n') < 0) { + return null; + } + const currentIndentNum = (node.srcToken as FlowScalar).indent; + let indentText = this.indentation; + for (let i = 0; i < currentIndentNum / this.indentation.length; i++) { + indentText += this.indentation; + } + + const lines: string[] = stringContent.split('\n'); + + let blockScalarHeaderSource = '|'; + if (lines[lines.length - 1] !== '\n' && lines[lines.length - 1] !== '') { + blockScalarHeaderSource += '-'; + } else if (lines[lines.length - 2] !== '\n' && lines[lines.length - 2] !== '') { + lines.splice(lines.length - 1, 1); + } else { + blockScalarHeaderSource += '+'; + } + if (/ /.test(stringContent.charAt(0))) { + blockScalarHeaderSource += `${indentText.length}`; + } + + const newProps: CST.Token[] = lines.flatMap((line) => { + if (line === '') { + return [ + { + type: 'newline', + indent: 0, + offset: node.srcToken.offset, + source: '\n', + }, + ]; + } + return [ + { + type: 'newline', + indent: 0, + offset: node.srcToken.offset, + source: '\n', + }, + { + type: 'space', + indent: 0, + offset: node.srcToken.offset, + source: indentText, + }, + { + type: 'scalar', + indent: 0, + offset: node.srcToken.offset, + source: line, + }, + ]; + }); + + newProps.unshift({ + type: 'block-scalar-header', + source: blockScalarHeaderSource, + offset: node.srcToken.offset, + indent: 0, + }); + + const blockString: CST.BlockScalar = { + type: 'block-scalar', + offset: node.srcToken.offset, + indent: 0, + source: '', + props: newProps, + }; + + return CST.stringify(blockString as CST.Token); + } +} diff --git a/src/languageservice/yamlLanguageService.ts b/src/languageservice/yamlLanguageService.ts index fd414a319..afa96c573 100644 --- a/src/languageservice/yamlLanguageService.ts +++ b/src/languageservice/yamlLanguageService.ts @@ -225,7 +225,7 @@ export function getLanguageService(params: { hover.configure(settings); completer.configure(settings, params.yamlSettings); formatter.configure(settings); - yamlCodeActions.configure(settings); + yamlCodeActions.configure(settings, params?.yamlSettings?.yamlFormatterSettings?.printWidth || 80); }, registerCustomSchemaProvider: (schemaProvider: CustomSchemaProvider) => { schemaService.registerCustomSchemaProvider(schemaProvider); diff --git a/test/yamlCodeActions.test.ts b/test/yamlCodeActions.test.ts index 34ea6ff1e..9b9f430ec 100644 --- a/test/yamlCodeActions.test.ts +++ b/test/yamlCodeActions.test.ts @@ -12,6 +12,7 @@ import { CodeActionContext, Command, DiagnosticSeverity, + Position, Range, TextDocumentIdentifier, TextEdit, @@ -130,7 +131,7 @@ describe('CodeActions Tests', () => { textDocument: TextDocumentIdentifier.create(TEST_URI), }; const actions = new YamlCodeActions(clientCapabilities); - actions.configure({ indentation: ' ' } as LanguageSettings); + actions.configure({ indentation: ' ' } as LanguageSettings, 80); const result = actions.getCodeAction(doc, params); expect(result[0].title).to.be.equal('Convert Tab to Spaces'); @@ -457,4 +458,313 @@ animals: [dog , cat , mouse] `; expect(result[0].edit.changes[TEST_URI]).deep.equal([TextEdit.replace(Range.create(0, 5, 0, 11), '5')]); }); }); + + describe('Change string to block string', function () { + it('should split up double quoted text with newlines', function () { + const doc = setupTextDocument('foo: "line 1\\nline 2\\nline 3"'); + const params: CodeActionParams = { + context: CodeActionContext.create([]), + range: undefined, + textDocument: TextDocumentIdentifier.create(TEST_URI), + }; + const actions = new YamlCodeActions(clientCapabilities); + const result = actions.getCodeAction(doc, params); + expect(result).to.have.length(2); + expect(result[0].title).to.equal('Convert string to folded block string'); + const edit0: WorkspaceEdit = { + changes: {}, + }; + edit0.changes[TEST_URI] = [ + TextEdit.replace(Range.create(Position.create(0, 5), Position.create(0, 29)), '>-\n line 1\n\n line 2\n\n line 3'), + ]; + expect(result[0].edit).to.deep.equal(edit0); + + expect(result[1].title).to.equal('Convert string to literal block string'); + const edit1: WorkspaceEdit = { + changes: {}, + }; + edit1.changes[TEST_URI] = [ + TextEdit.replace(Range.create(Position.create(0, 5), Position.create(0, 29)), '|-\n line 1\n line 2\n line 3'), + ]; + expect(result[1].edit).to.deep.equal(edit1); + }); + it('should split up double quoted text with newlines and trailing newline', function () { + const doc = setupTextDocument('foo: "line 1\\nline 2\\nline 3\\n"'); + const params: CodeActionParams = { + context: CodeActionContext.create([]), + range: undefined, + textDocument: TextDocumentIdentifier.create(TEST_URI), + }; + const actions = new YamlCodeActions(clientCapabilities); + const result = actions.getCodeAction(doc, params); + expect(result).to.have.length(2); + expect(result[0].title).to.equal('Convert string to folded block string'); + const edit0: WorkspaceEdit = { + changes: {}, + }; + edit0.changes[TEST_URI] = [ + TextEdit.replace(Range.create(Position.create(0, 5), Position.create(0, 31)), '>\n line 1\n\n line 2\n\n line 3'), + ]; + expect(result[0].edit).to.deep.equal(edit0); + + expect(result[1].title).to.equal('Convert string to literal block string'); + const edit1: WorkspaceEdit = { + changes: {}, + }; + edit1.changes[TEST_URI] = [ + TextEdit.replace(Range.create(Position.create(0, 5), Position.create(0, 31)), '|\n line 1\n line 2\n line 3'), + ]; + expect(result[1].edit).to.deep.equal(edit1); + }); + it('should split up double quoted text with newlines and double trailing newline', function () { + const doc = setupTextDocument('foo: "line 1\\nline 2\\nline 3\\n\\n"'); + const params: CodeActionParams = { + context: CodeActionContext.create([]), + range: undefined, + textDocument: TextDocumentIdentifier.create(TEST_URI), + }; + const actions = new YamlCodeActions(clientCapabilities); + const result = actions.getCodeAction(doc, params); + expect(result).to.have.length(2); + expect(result[0].title).to.equal('Convert string to folded block string'); + const edit0: WorkspaceEdit = { + changes: {}, + }; + edit0.changes[TEST_URI] = [ + TextEdit.replace( + Range.create(Position.create(0, 5), Position.create(0, 33)), + '>+\n line 1\n\n line 2\n\n line 3\n\n\n\n' + ), + ]; + expect(result[0].edit).to.deep.equal(edit0); + + expect(result[1].title).to.equal('Convert string to literal block string'); + const edit1: WorkspaceEdit = { + changes: {}, + }; + edit1.changes[TEST_URI] = [ + TextEdit.replace(Range.create(Position.create(0, 5), Position.create(0, 33)), '|+\n line 1\n line 2\n line 3\n\n'), + ]; + expect(result[1].edit).to.deep.equal(edit1); + }); + it('should split up long lines of double quoted text', function () { + let docContent = 'foo: "'; + for (let i = 0; i < 80 / 4 + 1; i++) { + docContent += 'cat '; + } + docContent += 'cat"'; + const doc = setupTextDocument(docContent); + const params: CodeActionParams = { + context: CodeActionContext.create([]), + range: undefined, + textDocument: TextDocumentIdentifier.create(TEST_URI), + }; + const actions = new YamlCodeActions(clientCapabilities); + const result = actions.getCodeAction(doc, params); + expect(result).to.have.length(1); + expect(result[0].title).to.equal('Convert string to folded block string'); + const edit0: WorkspaceEdit = { + changes: {}, + }; + let resultText = '>-\n '; + for (let i = 0; i < 80 / 4; i++) { + resultText += ' cat'; + } + resultText += ' cat\n cat'; + edit0.changes[TEST_URI] = [ + TextEdit.replace(Range.create(Position.create(0, 5), Position.create(0, 5 + 80 + 8 + 1)), resultText), + ]; + expect(result[0].edit).to.deep.equal(edit0); + }); + it('should split up long lines of double quoted text using configured width', function () { + let docContent = 'foo: "'; + for (let i = 0; i < 40 / 4 + 1; i++) { + docContent += 'cat '; + } + docContent += 'cat"'; + const doc = setupTextDocument(docContent); + const params: CodeActionParams = { + context: CodeActionContext.create([]), + range: undefined, + textDocument: TextDocumentIdentifier.create(TEST_URI), + }; + const actions = new YamlCodeActions(clientCapabilities); + actions.configure({ indentation: ' ' }, 40); + const result = actions.getCodeAction(doc, params); + expect(result).to.have.length(1); + expect(result[0].title).to.equal('Convert string to folded block string'); + const edit0: WorkspaceEdit = { + changes: {}, + }; + let resultText = '>-\n '; + for (let i = 0; i < 40 / 4; i++) { + resultText += ' cat'; + } + resultText += ' cat\n cat'; + edit0.changes[TEST_URI] = [ + TextEdit.replace(Range.create(Position.create(0, 5), Position.create(0, 5 + 40 + 8 + 1)), resultText), + ]; + expect(result[0].edit).to.deep.equal(edit0); + }); + it('should convert single quote text with newline', function () { + const docContent = `root: 'aaa + aaa + + bbb + bbb'`; + const doc = setupTextDocument(docContent); + const params: CodeActionParams = { + context: CodeActionContext.create([]), + range: undefined, + textDocument: TextDocumentIdentifier.create(TEST_URI), + }; + const actions = new YamlCodeActions(clientCapabilities); + const result = actions.getCodeAction(doc, params); + expect(result).to.have.length(2); + expect(result[0].title).to.equal('Convert string to folded block string'); + const edit0: WorkspaceEdit = { + changes: {}, + }; + edit0.changes[TEST_URI] = [ + TextEdit.replace(Range.create(Position.create(0, 6), Position.create(4, 6)), '>-\n aaa aaa\n\n bbb bbb'), + ]; + expect(result[0].edit).to.deep.equal(edit0); + + expect(result[1].title).to.equal('Convert string to literal block string'); + const edit1: WorkspaceEdit = { + changes: {}, + }; + edit1.changes[TEST_URI] = [ + TextEdit.replace(Range.create(Position.create(0, 6), Position.create(4, 6)), '|-\n aaa aaa\n bbb bbb'), + ]; + expect(result[1].edit).to.deep.equal(edit1); + }); + it('should convert single quote text with leading whitespace', function () { + const docContent = `root: ' aaa + aaa + + bbb + bbb'`; + const doc = setupTextDocument(docContent); + const params: CodeActionParams = { + context: CodeActionContext.create([]), + range: undefined, + textDocument: TextDocumentIdentifier.create(TEST_URI), + }; + const actions = new YamlCodeActions(clientCapabilities); + const result = actions.getCodeAction(doc, params); + expect(result).to.have.length(2); + expect(result[0].title).to.equal('Convert string to folded block string'); + const edit0: WorkspaceEdit = { + changes: {}, + }; + edit0.changes[TEST_URI] = [ + TextEdit.replace(Range.create(Position.create(0, 6), Position.create(4, 6)), '>-2\n aaa aaa\n\n bbb bbb'), + ]; + expect(result[0].edit).to.deep.equal(edit0); + + expect(result[1].title).to.equal('Convert string to literal block string'); + const edit1: WorkspaceEdit = { + changes: {}, + }; + edit1.changes[TEST_URI] = [ + TextEdit.replace(Range.create(Position.create(0, 6), Position.create(4, 6)), '|-2\n aaa aaa\n bbb bbb'), + ]; + expect(result[1].edit).to.deep.equal(edit1); + }); + it('should leave the whitespace at the end of the line when folding a double quoted string', function () { + const docContent = + 'root: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaa"'; + const doc = setupTextDocument(docContent); + const params: CodeActionParams = { + context: CodeActionContext.create([]), + range: undefined, + textDocument: TextDocumentIdentifier.create(TEST_URI), + }; + const actions = new YamlCodeActions(clientCapabilities); + const result = actions.getCodeAction(doc, params); + expect(result).to.have.length(1); + expect(result[0].title).to.equal('Convert string to folded block string'); + const edit0: WorkspaceEdit = { + changes: {}, + }; + edit0.changes[TEST_URI] = [ + TextEdit.replace( + Range.create(Position.create(0, 6), Position.create(0, 138)), + '>-\n aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \n aaaa' + ), + ]; + expect(result[0].edit).to.deep.equal(edit0); + }); + it("should use the '+' block chomping indicator when there are trailing newlines", function () { + const docContent = 'root: "aaaa\\n\\n\\n"'; + const doc = setupTextDocument(docContent); + const params: CodeActionParams = { + context: CodeActionContext.create([]), + range: undefined, + textDocument: TextDocumentIdentifier.create(TEST_URI), + }; + const actions = new YamlCodeActions(clientCapabilities); + const result = actions.getCodeAction(doc, params); + expect(result).to.have.length(2); + expect(result[0].title).to.equal('Convert string to folded block string'); + const edit0: WorkspaceEdit = { + changes: {}, + }; + edit0.changes[TEST_URI] = [ + TextEdit.replace(Range.create(Position.create(0, 6), Position.create(0, 18)), '>+\n aaaa\n\n\n\n\n\n'), + ]; + expect(result[0].edit).to.deep.equal(edit0); + expect(result[1].title).to.equal('Convert string to literal block string'); + const edit1: WorkspaceEdit = { + changes: {}, + }; + edit1.changes[TEST_URI] = [ + TextEdit.replace(Range.create(Position.create(0, 6), Position.create(0, 18)), '|+\n aaaa\n\n\n'), + ]; + expect(result[1].edit).to.deep.equal(edit1); + }); + it('should handle nested indentation', function () { + const docContent = 'root:\n toot:\n boot: "aaaa\\naaaa"'; + const doc = setupTextDocument(docContent); + const params: CodeActionParams = { + context: CodeActionContext.create([]), + range: undefined, + textDocument: TextDocumentIdentifier.create(TEST_URI), + }; + const actions = new YamlCodeActions(clientCapabilities); + const result = actions.getCodeAction(doc, params); + expect(result).to.have.length(2); + expect(result[0].title).to.equal('Convert string to folded block string'); + const edit0: WorkspaceEdit = { + changes: {}, + }; + edit0.changes[TEST_URI] = [ + TextEdit.replace(Range.create(Position.create(2, 10), Position.create(2, 22)), '>-\n aaaa\n\n aaaa'), + ]; + expect(result[0].edit).to.deep.equal(edit0); + expect(result[1].title).to.equal('Convert string to literal block string'); + const edit1: WorkspaceEdit = { + changes: {}, + }; + edit1.changes[TEST_URI] = [ + TextEdit.replace(Range.create(Position.create(2, 10), Position.create(2, 22)), '|-\n aaaa\n aaaa'), + ]; + expect(result[1].edit).to.deep.equal(edit1); + }); + it('should give up on folded block string if there is trailing whitespace', function () { + const docContent = 'root: " "'; + const doc = setupTextDocument(docContent); + const params: CodeActionParams = { + context: CodeActionContext.create([]), + range: undefined, + textDocument: TextDocumentIdentifier.create(TEST_URI), + }; + const actions = new YamlCodeActions(clientCapabilities); + const result = actions.getCodeAction(doc, params); + // cannot be represented as folded stinrg + // a block string makes no sense since it's one line long + expect(result).to.have.length(0); + }); + }); }); diff --git a/test/yamlOnTypeFormatting.test.ts b/test/yamlOnTypeFormatting.test.ts index 6984a481b..e2141b62f 100644 --- a/test/yamlOnTypeFormatting.test.ts +++ b/test/yamlOnTypeFormatting.test.ts @@ -17,7 +17,7 @@ function createParams(position: Position): DocumentOnTypeFormattingParams { }; } describe('YAML On Type Formatter', () => { - it('should react on "\n" only', () => { + it('should react on "\\n" only', () => { const doc = setupTextDocument('foo:'); const params = createParams(Position.create(1, 0)); params.ch = '\t';