Skip to content

Commit 6269b2d

Browse files
committed
[WIP] Refactoring to change strings into block scalars
TODO: - [ ] Translations (I think the French one is probably wrong) - [ ] Handle leading spaces correctly (leading spaces are ignored, so we need another way to preserve them) - [ ] Tests, especially for edge cases Fixes #1119 Signed-off-by: David Thompson <[email protected]>
1 parent 86a61da commit 6269b2d

File tree

6 files changed

+272
-4
lines changed

6 files changed

+272
-4
lines changed

.vscode/tasks.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
{
77
"label": "watch typescript",
88
"type": "shell",
9-
"command": "yarn run watch",
9+
"command": "npm run watch",
1010
"presentation": {
1111
"reveal": "never"
1212
},

l10n/bundle.l10n.fr.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,7 @@
5252
"flowStyleMapForbidden": "Le mappage de style de flux est interdit",
5353
"flowStyleSeqForbidden": "La séquence de style Flow est interdite",
5454
"unUsedAnchor": "Ancre inutilisée '{0}'",
55-
"unUsedAlias": "Alias ​​non résolu '{0}'"
55+
"unUsedAlias": "Alias non résolu '{0}'",
56+
"convertToFoldedBlockString": "Convertir la chaîne en style de bloc plie",
57+
"convertToLiteralBlockString": "Convertir la chaîne en style de bloc literale"
5658
}

l10n/bundle.l10n.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,7 @@
5252
"flowStyleMapForbidden": "Flow style mapping is forbidden",
5353
"flowStyleSeqForbidden": "Flow style sequence is forbidden",
5454
"unUsedAnchor": "Unused anchor \"{0}\"",
55-
"unUsedAlias": "Unresolved alias \"{0}\""
55+
"unUsedAlias": "Unresolved alias \"{0}\"",
56+
"convertToFoldedBlockString": "Convert string to folded block string",
57+
"convertToLiteralBlockString": "Convert string to literal block string"
5658
}

src/languageservice/services/yamlCodeActions.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@ import { LanguageSettings } from '../yamlLanguageService';
2222
import { YAML_SOURCE } from '../parser/jsonParser07';
2323
import { getFirstNonWhitespaceCharacterAfterOffset } from '../utils/strings';
2424
import { matchOffsetToDocument } from '../utils/arrUtils';
25-
import { CST, isMap, isSeq, YAMLMap } from 'yaml';
25+
import { CST, isMap, isScalar, isSeq, Scalar, visit, YAMLMap } from 'yaml';
2626
import { yamlDocumentsCache } from '../parser/yaml-documents';
2727
import { FlowStyleRewriter } from '../utils/flow-style-rewriter';
2828
import { ASTNode } from '../jsonASTTypes';
2929
import * as _ from 'lodash';
3030
import { SourceToken } from 'yaml/dist/parse/cst';
3131
import { ErrorCode } from 'vscode-json-languageservice';
3232
import * as l10n from '@vscode/l10n';
33+
import { BlockStringRewriter } from '../utils/block-string-rewriter';
3334

3435
interface YamlDiagnosticData {
3536
schemaUri: string[];
@@ -57,6 +58,7 @@ export class YamlCodeActions {
5758
result.push(...this.getTabToSpaceConverting(params.context.diagnostics, document));
5859
result.push(...this.getUnusedAnchorsDelete(params.context.diagnostics, document));
5960
result.push(...this.getConvertToBlockStyleActions(params.context.diagnostics, document));
61+
result.push(...this.getConvertStringToBlockStyleActions(params.context.diagnostics, document));
6062
result.push(...this.getKeyOrderActions(params.context.diagnostics, document));
6163
result.push(...this.getQuickFixForPropertyOrValueMismatch(params.context.diagnostics, document));
6264

@@ -243,6 +245,50 @@ export class YamlCodeActions {
243245
return results;
244246
}
245247

248+
private getConvertStringToBlockStyleActions(diagnostics: Diagnostic[], document: TextDocument): CodeAction[] {
249+
const yamlDocument = yamlDocumentsCache.getYamlDocument(document);
250+
251+
const results: CodeAction[] = [];
252+
for (const singleYamlDocument of yamlDocument.documents) {
253+
const matchingNodes: Scalar<string>[] = [];
254+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
255+
visit(singleYamlDocument.internalDocument, (key, node, path) => {
256+
if (isScalar(node)) {
257+
if (node.type === 'QUOTE_DOUBLE' || node.type === 'QUOTE_SINGLE') {
258+
if (typeof node.value === 'string' && (node.value.indexOf('\n') >= 0 || node.value.length > 120) /* TODO: */) {
259+
matchingNodes.push(<Scalar<string>>node);
260+
}
261+
}
262+
}
263+
});
264+
for (const node of matchingNodes) {
265+
const range = Range.create(document.positionAt(node.range[0]), document.positionAt(node.range[2]));
266+
const rewriter = new BlockStringRewriter(this.indentation, 120 /* TODO: */);
267+
const foldedBlockScalar = rewriter.writeFoldedBlockScalar(node);
268+
if (foldedBlockScalar !== null) {
269+
results.push(
270+
CodeAction.create(
271+
l10n.t('convertToFoldedBlockString'),
272+
createWorkspaceEdit(document.uri, [TextEdit.replace(range, foldedBlockScalar)]),
273+
CodeActionKind.Refactor
274+
)
275+
);
276+
}
277+
const literalBlockScalar = rewriter.writeLiteralBlockScalar(node);
278+
if (literalBlockScalar !== null) {
279+
results.push(
280+
CodeAction.create(
281+
l10n.t('convertToLiteralBlockString'),
282+
createWorkspaceEdit(document.uri, [TextEdit.replace(range, literalBlockScalar)]),
283+
CodeActionKind.Refactor
284+
)
285+
);
286+
}
287+
}
288+
}
289+
return results;
290+
}
291+
246292
private getKeyOrderActions(diagnostics: Diagnostic[], document: TextDocument): CodeAction[] {
247293
const results: CodeAction[] = [];
248294
for (const diagnostic of diagnostics) {
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { CST, Scalar } from 'yaml';
2+
3+
export class BlockStringRewriter {
4+
constructor(
5+
private readonly indentation: string,
6+
private readonly maxLineLength: number
7+
) {}
8+
9+
public writeFoldedBlockScalar(node: Scalar<string>): string | null {
10+
if (node.type !== 'QUOTE_DOUBLE' && node.type !== 'QUOTE_SINGLE') {
11+
return null;
12+
}
13+
14+
const stringContent = node.value;
15+
16+
const lines: string[] = [];
17+
const splitLines = stringContent.split('\n');
18+
for (const line of splitLines) {
19+
let remainder = line;
20+
slicing: while (remainder.length > this.maxLineLength) {
21+
let location = this.maxLineLength;
22+
// for folded strings, space characters are placed in place of each line break
23+
// so we need to split the line on a space and remove the space
24+
while (!/ /.test(remainder.charAt(location))) {
25+
location++;
26+
if (location >= remainder.length) {
27+
break slicing;
28+
}
29+
}
30+
// however any leading space characters will be take literally and also a newline gets inserted
31+
// so instead we need them to be trailing
32+
// which could be problematic as "trim trailing whitespace" is a common setting to have enabled but oh well
33+
while (/ /.test(remainder.charAt(location))) {
34+
location++;
35+
if (location >= remainder.length) {
36+
break slicing;
37+
}
38+
}
39+
const head = remainder.substring(
40+
0,
41+
location - 1 /* -1 to remove one space character, which is automatically added between lines */
42+
);
43+
lines.push(head);
44+
remainder = remainder.substring(location);
45+
}
46+
lines.push(remainder);
47+
lines.push('\n');
48+
}
49+
// no trailng newline
50+
lines.pop();
51+
52+
const newProps: CST.Token[] = lines.flatMap((line) => {
53+
if (line === '\n') {
54+
// newlines can be represented as two newlines in folded blocks
55+
return [
56+
{
57+
type: 'newline',
58+
indent: 0,
59+
offset: node.srcToken.offset,
60+
source: '\n',
61+
},
62+
];
63+
}
64+
return [
65+
{
66+
type: 'newline',
67+
indent: 0,
68+
offset: node.srcToken.offset,
69+
source: '\n',
70+
},
71+
{
72+
type: 'space',
73+
indent: 0,
74+
offset: node.srcToken.offset,
75+
source: this.indentation,
76+
},
77+
{
78+
type: 'scalar',
79+
indent: 0,
80+
offset: node.srcToken.offset,
81+
source: line,
82+
},
83+
];
84+
});
85+
86+
newProps.unshift({
87+
type: 'block-scalar-header',
88+
source: '>',
89+
offset: node.srcToken.offset,
90+
indent: 0,
91+
});
92+
93+
const blockString: CST.BlockScalar = {
94+
type: 'block-scalar',
95+
offset: node.srcToken.offset,
96+
indent: 0,
97+
source: '',
98+
props: newProps,
99+
};
100+
101+
return CST.stringify(blockString as CST.Token);
102+
}
103+
104+
public writeLiteralBlockScalar(node: Scalar<string>): string | null {
105+
if (node.type !== 'QUOTE_DOUBLE' && node.type !== 'QUOTE_SINGLE') {
106+
return null;
107+
}
108+
109+
const stringContent = node.value;
110+
// I don't think it's worth it
111+
if (stringContent.indexOf('\n') < 0) {
112+
return null;
113+
}
114+
115+
const lines: string[] = stringContent.split('\n');
116+
117+
const newProps: CST.Token[] = lines.flatMap((line) => {
118+
return [
119+
{
120+
type: 'newline',
121+
indent: 0,
122+
offset: node.srcToken.offset,
123+
source: '\n',
124+
},
125+
{
126+
type: 'space',
127+
indent: 0,
128+
offset: node.srcToken.offset,
129+
source: this.indentation,
130+
},
131+
{
132+
type: 'scalar',
133+
indent: 0,
134+
offset: node.srcToken.offset,
135+
source: line,
136+
},
137+
];
138+
});
139+
140+
newProps.unshift({
141+
type: 'block-scalar-header',
142+
source: '|',
143+
offset: node.srcToken.offset,
144+
indent: 0,
145+
});
146+
147+
const blockString: CST.BlockScalar = {
148+
type: 'block-scalar',
149+
offset: node.srcToken.offset,
150+
indent: 0,
151+
source: '',
152+
props: newProps,
153+
};
154+
155+
return CST.stringify(blockString as CST.Token);
156+
}
157+
}

test/yamlCodeActions.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
CodeActionContext,
1313
Command,
1414
DiagnosticSeverity,
15+
Position,
1516
Range,
1617
TextDocumentIdentifier,
1718
TextEdit,
@@ -457,4 +458,64 @@ animals: [dog , cat , mouse] `;
457458
expect(result[0].edit.changes[TEST_URI]).deep.equal([TextEdit.replace(Range.create(0, 5, 0, 11), '5')]);
458459
});
459460
});
461+
462+
describe('Change string to block string', function () {
463+
it('should split up double quoted text with newlines', function () {
464+
const doc = setupTextDocument('foo: "line 1\\nline 2\\nline 3"');
465+
const params: CodeActionParams = {
466+
context: CodeActionContext.create([]),
467+
range: undefined,
468+
textDocument: TextDocumentIdentifier.create(TEST_URI),
469+
};
470+
const actions = new YamlCodeActions(clientCapabilities);
471+
const result = actions.getCodeAction(doc, params);
472+
expect(result).to.have.length(2);
473+
expect(result[0].title).to.equal('Convert string to folded block string');
474+
const edit0: WorkspaceEdit = {
475+
changes: {},
476+
};
477+
edit0.changes[TEST_URI] = [
478+
TextEdit.replace(Range.create(Position.create(0, 5), Position.create(0, 29)), '>\n line 1\n\n line 2\n\n line 3'),
479+
];
480+
expect(result[0].edit).to.deep.equal(edit0);
481+
482+
expect(result[1].title).to.equal('Convert string to literal block string');
483+
const edit1: WorkspaceEdit = {
484+
changes: {},
485+
};
486+
edit1.changes[TEST_URI] = [
487+
TextEdit.replace(Range.create(Position.create(0, 5), Position.create(0, 29)), '|\n line 1\n line 2\n line 3'),
488+
];
489+
expect(result[1].edit).to.deep.equal(edit1);
490+
});
491+
it('should split up long lines of double quoted text', function () {
492+
let docContent = 'foo: "';
493+
for (let i = 0; i < 120 / 4 + 1; i++) {
494+
docContent += 'cat ';
495+
}
496+
docContent += 'cat"';
497+
const doc = setupTextDocument(docContent);
498+
const params: CodeActionParams = {
499+
context: CodeActionContext.create([]),
500+
range: undefined,
501+
textDocument: TextDocumentIdentifier.create(TEST_URI),
502+
};
503+
const actions = new YamlCodeActions(clientCapabilities);
504+
const result = actions.getCodeAction(doc, params);
505+
expect(result).to.have.length(1);
506+
expect(result[0].title).to.equal('Convert string to folded block string');
507+
const edit0: WorkspaceEdit = {
508+
changes: {},
509+
};
510+
let resultText = '>\n ';
511+
for (let i = 0; i < 120 / 4; i++) {
512+
resultText += ' cat';
513+
}
514+
resultText += ' cat\n cat';
515+
edit0.changes[TEST_URI] = [
516+
TextEdit.replace(Range.create(Position.create(0, 5), Position.create(0, 5 + 120 + 8 + 1)), resultText),
517+
];
518+
expect(result[0].edit).to.deep.equal(edit0);
519+
});
520+
});
460521
});

0 commit comments

Comments
 (0)