Skip to content

Commit fb0b5a3

Browse files
authored
Fix: Indent multiline fixes (fixes #120) (#124)
* Chore: Add failing test case for indent of multiline fixes If the replacement text of an autofix contains newlines, all lines but the first in the replacement text will unexpectedly have 0 indentation if a code block is indented. I guess not only the _range_ of fixes but also the _text_ of fixes need to be adjusted. If the code block is placed within a blockquote, a `>` character also needs to be inserted. This issue was found while investigating lydell/eslint-plugin-simple-import-sort#22. That plugin several lines of imports in one go. This means that most of the imports will have too little indentation after autofix if the code block is indented (such as when placed in a list). * Fix: Indent multiline fixes (fixes #120) * Add test for underindented multiline autofix * Add test for nested blockquote multiline autofix * Add comment explaining underintented line shift
1 parent 07c9017 commit fb0b5a3

File tree

2 files changed

+150
-5
lines changed

2 files changed

+150
-5
lines changed

lib/processor.js

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,33 @@ function getComment(html) {
6565
return comment;
6666
}
6767

68-
const leadingWhitespaceRegex = /^\s*/;
68+
// Before a code block, blockquote characters (`>`) are also considered
69+
// "whitespace".
70+
const leadingWhitespaceRegex = /^[>\s]*/;
71+
72+
/**
73+
* Gets the offset for the first column of the node's first line in the
74+
* original source text.
75+
* @param {ASTNode} node A Markdown code block AST node.
76+
* @returns {number} The offset for the first column of the node's first line.
77+
*/
78+
function getBeginningOfLineOffset(node) {
79+
return node.position.start.offset - node.position.start.column + 1;
80+
}
81+
82+
/**
83+
* Gets the leading text, typically whitespace with possible blockquote chars,
84+
* used to indent a code block.
85+
* @param {string} text The text of the file.
86+
* @param {ASTNode} node A Markdown code block AST node.
87+
* @returns {string} The text from the start of the first line to the opening
88+
* fence of the code block.
89+
*/
90+
function getIndentText(text, node) {
91+
return leadingWhitespaceRegex.exec(
92+
text.slice(getBeginningOfLineOffset(node))
93+
)[0];
94+
}
6995

7096
/**
7197
* When applying fixes, the postprocess step needs to know how to map fix ranges
@@ -110,7 +136,7 @@ function getBlockRangeMap(text, node, comments) {
110136
* additional indenting, the opening fence's first backtick may be up to
111137
* three whitespace characters after the start offset.
112138
*/
113-
const startOffset = node.position.start.offset;
139+
const startOffset = getBeginningOfLineOffset(node);
114140

115141
/*
116142
* Extract the Markdown source to determine the leading whitespace for each
@@ -125,8 +151,7 @@ function getBlockRangeMap(text, node, comments) {
125151
* backtick's column is the AST node's starting column plus any additional
126152
* indentation.
127153
*/
128-
const baseIndent = node.position.start.column - 1 +
129-
leadingWhitespaceRegex.exec(lines[0])[0].length;
154+
const baseIndent = getIndentText(text, node).length;
130155

131156
/*
132157
* Track the length of any inserted configuration comments at the beginning
@@ -182,6 +207,7 @@ function getBlockRangeMap(text, node, comments) {
182207
mdOffset += line.length + 1;
183208
jsOffset += line.length - trimLength + 1;
184209
}
210+
185211
return rangeMap;
186212
}
187213

@@ -219,6 +245,7 @@ function preprocess(text) {
219245
}
220246

221247
blocks.push(assign({}, node, {
248+
baseIndentText: getIndentText(text, node),
222249
comments,
223250
rangeMap: getBlockRangeMap(text, node, comments)
224251
}));
@@ -281,7 +308,7 @@ function adjustBlock(block) {
281308
// Apply the mapping delta for this range.
282309
return range + block.rangeMap[i - 1].md;
283310
}),
284-
text: message.fix.text
311+
text: message.fix.text.replace("\n", `\n${block.baseIndentText}`)
285312
};
286313
}
287314

tests/lib/plugin.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,124 @@ describe("plugin", () => {
575575
assert.strictEqual(actual, expected);
576576
});
577577

578+
it("multiline autofix", () => {
579+
const input = [
580+
"This is Markdown.",
581+
"",
582+
" ```js",
583+
" console.log('Hello, \\",
584+
" world!')",
585+
" console.log('Hello, \\",
586+
" world!')",
587+
" ```"
588+
].join("\n");
589+
const expected = [
590+
"This is Markdown.",
591+
"",
592+
" ```js",
593+
" console.log(\"Hello, \\",
594+
" world!\")",
595+
" console.log(\"Hello, \\",
596+
" world!\")",
597+
" ```"
598+
].join("\n");
599+
const report = cli.executeOnText(input, "test.md");
600+
const actual = report.results[0].output;
601+
602+
assert.strictEqual(actual, expected);
603+
});
604+
605+
it("underindented multiline autofix", () => {
606+
const input = [
607+
" ```js",
608+
" console.log('Hello, world!')",
609+
" console.log('Hello, \\",
610+
" world!')",
611+
" console.log('Hello, world!')",
612+
" ```"
613+
].join("\n");
614+
615+
// The Markdown parser doesn't have any concept of a "negative"
616+
// indent left of the opening code fence, so autofixes move
617+
// lines that were previously underindented to the same level
618+
// as the opening code fence.
619+
const expected = [
620+
" ```js",
621+
" console.log(\"Hello, world!\")",
622+
" console.log(\"Hello, \\",
623+
" world!\")",
624+
" console.log(\"Hello, world!\")",
625+
" ```"
626+
].join("\n");
627+
const report = cli.executeOnText(input, "test.md");
628+
const actual = report.results[0].output;
629+
630+
assert.strictEqual(actual, expected);
631+
});
632+
633+
it("multiline autofix in blockquote", () => {
634+
const input = [
635+
"This is Markdown.",
636+
"",
637+
"> ```js",
638+
"> console.log('Hello, \\",
639+
"> world!')",
640+
"> console.log('Hello, \\",
641+
"> world!')",
642+
"> ```"
643+
].join("\n");
644+
const expected = [
645+
"This is Markdown.",
646+
"",
647+
"> ```js",
648+
"> console.log(\"Hello, \\",
649+
"> world!\")",
650+
"> console.log(\"Hello, \\",
651+
"> world!\")",
652+
"> ```"
653+
].join("\n");
654+
const report = cli.executeOnText(input, "test.md");
655+
const actual = report.results[0].output;
656+
657+
assert.strictEqual(actual, expected);
658+
});
659+
660+
it("multiline autofix in nested blockquote", () => {
661+
const input = [
662+
"This is Markdown.",
663+
"",
664+
"> This is a nested blockquote.",
665+
">",
666+
"> > ```js",
667+
"> > console.log('Hello, \\",
668+
"> > world!')",
669+
"> > console.log('Hello, \\",
670+
"> > world!')",
671+
"> > ```"
672+
].join("\n");
673+
674+
// The Markdown parser doesn't have any concept of a "negative"
675+
// indent left of the opening code fence, so autofixes move
676+
// lines that were previously underindented to the same level
677+
// as the opening code fence.
678+
const expected = [
679+
"This is Markdown.",
680+
"",
681+
"> This is a nested blockquote.",
682+
">",
683+
"> > ```js",
684+
"> > console.log(\"Hello, \\",
685+
"> > world!\")",
686+
"> > console.log(\"Hello, \\",
687+
"> > world!\")",
688+
"> > ```"
689+
].join("\n");
690+
const report = cli.executeOnText(input, "test.md");
691+
const actual = report.results[0].output;
692+
693+
assert.strictEqual(actual, expected);
694+
});
695+
578696
it("by one space with comments", () => {
579697
const input = [
580698
"This is Markdown.",

0 commit comments

Comments
 (0)