Skip to content

Commit

Permalink
fix: auto-indent not working properly with multi-line CRLF text
Browse files Browse the repository at this point in the history
  • Loading branch information
sebthom committed Mar 9, 2024
1 parent 501ede8 commit c66b225
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ && isFollowedBy(doc, command.offset, charPair.close))) {
command.length += offsetInLine;
}
command.text = TextUtils.replaceIndent(command.text, cursorCfg.indentSize,
cursorCfg.normalizeIndentation(newIndent)).toString();
cursorCfg.normalizeIndentation(newIndent), false).toString();
command.shiftsCaret = true;
}
} catch (final BadLocationException ex) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ public static boolean isEmptyLine(final IDocument doc, final int lineIndex) {
}
}

public static CharSequence replaceIndent(final CharSequence multiLineString, final int tabSize, final String newIndent) {
public static CharSequence replaceIndent(final CharSequence multiLineString, final int tabSize, final String newIndent,
final boolean indentEmptyLines) {
final int effectiveTabSize = Math.max(1, tabSize);

abstract class CharConsumer implements IntConsumer {
Expand All @@ -164,8 +165,8 @@ public void accept(final int value) {
}

abstract void onChar(char ch);

}

/*
* determine common indentation of all lines
*/
Expand All @@ -178,25 +179,30 @@ final class IndentDetector extends CharConsumer implements IntPredicate {

@Override
void onChar(final char ch) {
if (ch == '\r' && prevChar != '\n' || ch == '\n' && prevChar != '\r') {
// handle new line chars
if (ch == '\n' || ch == '\r') {
if (ch == '\n' && prevChar == '\r'
|| ch == '\r' && prevChar == '\n') {
return;
}
lineCount++;
skipToLineEnd = false;
if (!isEmptyLine && indentOfLine < existingIndent)
existingIndent = indentOfLine;
indentOfLine = 0;
isEmptyLine = true;
if (existingIndent == 0)
return;
} else {
isEmptyLine = false;
if (!skipToLineEnd) {
if (ch == '\t') {
indentOfLine += effectiveTabSize;
} else if (Character.isWhitespace(ch)) {
indentOfLine++;
} else {
skipToLineEnd = true;
}
return;
}

// handle other chars
isEmptyLine = false;
if (!skipToLineEnd) {
if (ch == '\t') {
indentOfLine += effectiveTabSize;
} else if (Character.isWhitespace(ch)) {
indentOfLine++;
} else {
skipToLineEnd = true;
}
}
}
Expand All @@ -220,29 +226,39 @@ public boolean test(final int value) {
* replace common indentation of all lines
*/
final var sb = new StringBuilder(Math.max(0, multiLineString.length() - (indentDetector.lineCount * existingIndent)));
sb.append(newIndent);
final class IdentReplacer extends CharConsumer {
int indentOfLineSkipped = 0;
int skippedIndentOfLine = 0;
boolean isEmptyLine = true;

@Override
public void onChar(final char ch) {
if (ch == '\r')
return;

if (ch == '\n') {
if (isEmptyLine && indentEmptyLines) {
sb.append(newIndent);
}
if (prevChar == '\r') {
sb.append('\r');
}
sb.append(ch);
sb.append(newIndent);
indentOfLineSkipped = 0;
skippedIndentOfLine = 0;
isEmptyLine = true;
return;
}
if (prevChar == '\r') {
sb.append(newIndent);
indentOfLineSkipped = 0;
}
if (indentOfLineSkipped >= existingIndent) {

if (skippedIndentOfLine >= existingIndent) {
if (isEmptyLine) {
sb.append(newIndent);
isEmptyLine = false;
}
sb.append(ch);
} else {
if (ch == '\t') {
indentOfLineSkipped += effectiveTabSize;
skippedIndentOfLine += effectiveTabSize;
} else {
indentOfLineSkipped++;
skippedIndentOfLine++;
}
}
}
Expand All @@ -251,9 +267,10 @@ public void onChar(final char ch) {
final var indentReplacer = new IdentReplacer();
multiLineString.chars().forEach(indentReplacer);

// don't indent trailing new line
if (indentReplacer.prevChar == '\n' || indentReplacer.prevChar == '\r')
sb.setLength(sb.length() - newIndent.length());
// special case
if (indentEmptyLines && sb.isEmpty() && !multiLineString.isEmpty()) {
sb.append(newIndent);
}
return sb;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,29 +97,72 @@ void testIsEmptyLine() {
}

@Test
void testReplaceIndent() {
assertEquals(" ", replaceIndent("\t\t", 2, " ").toString());

assertEquals("", replaceIndent("\t\t", 2, "").toString().toString());
assertEquals("foo ", replaceIndent("foo ", 2, "").toString());
assertEquals("foo", replaceIndent(" \t foo", 2, "").toString());
assertEquals("foo\nbar", replaceIndent(" foo\n bar", 2, "").toString());
assertEquals("foo\nbar", replaceIndent(" foo\n\tbar", 2, "").toString());
assertEquals("foo\nbar", replaceIndent(" foo\n\tbar", 2, "").toString());
assertEquals("foo\n\tbar", replaceIndent("\tfoo\n\t\tbar", 2, "").toString());
assertEquals("foo\n\tbar", replaceIndent("\tfoo\n \tbar", 2, "").toString());

assertEquals(" ", replaceIndent("\t\t", 2, " ").toString());
assertEquals(" foo ", replaceIndent("foo ", 2, " ").toString());
assertEquals(" foo", replaceIndent(" \t foo", 2, " ").toString());
assertEquals(" foo\n bar", replaceIndent(" foo\n bar", 2, " ").toString());
assertEquals(" foo\n bar", replaceIndent(" foo\n\tbar", 2, " ").toString());
assertEquals(" foo\n bar", replaceIndent(" foo\n\tbar", 2, " ").toString());
assertEquals(" foo\n \tbar", replaceIndent("\tfoo\n\t\tbar", 2, " ").toString());
assertEquals(" foo\n \tbar", replaceIndent("\tfoo\n \tbar", 2, " ").toString());

assertEquals(" \n \n", replaceIndent("\n\n", 2, " ").toString());
assertEquals(" foo\n bar\n", replaceIndent("\tfoo\n\tbar\n", 2, " ").toString());
void testReplaceIndent_IndentEmptyLines() {
assertEquals("", replaceIndent("\t\t", 2, "", true).toString());
assertEquals("foo ", replaceIndent("foo ", 2, "", true).toString());
assertEquals("foo", replaceIndent(" \t foo", 2, "", true).toString());
assertEquals("foo\nbar", replaceIndent(" foo\n bar", 2, "", true).toString());
assertEquals("foo\nbar", replaceIndent(" foo\n\tbar", 2, "", true).toString());
assertEquals("foo\nbar", replaceIndent(" foo\n\tbar", 2, "", true).toString());
assertEquals("foo\n\tbar", replaceIndent("\tfoo\n\t\tbar", 2, "", true).toString());
assertEquals("foo\n\tbar", replaceIndent("\tfoo\n \tbar", 2, "", true).toString());

assertEquals("foo\r\nbar", replaceIndent(" foo\r\n bar", 2, "", true).toString());
assertEquals("foo\r\nbar", replaceIndent(" foo\r\n\tbar", 2, "", true).toString());
assertEquals("foo\r\nbar", replaceIndent(" foo\r\n\tbar", 2, "", true).toString());
assertEquals("foo\r\n\tbar", replaceIndent("\tfoo\r\n\t\tbar", 2, "", true).toString());
assertEquals("foo\r\n\tbar", replaceIndent("\tfoo\r\n \tbar", 2, "", true).toString());

assertEquals("..", replaceIndent("\t\t", 2, "..", true).toString());
assertEquals("..foo ", replaceIndent("foo ", 2, "..", true).toString());
assertEquals("..foo", replaceIndent(" \t foo", 2, "..", true).toString());
assertEquals("..foo\n..bar", replaceIndent(" foo\n bar", 2, "..", true).toString());
assertEquals("..foo\n..bar", replaceIndent(" foo\n\tbar", 2, "..", true).toString());
assertEquals("..foo\n..bar", replaceIndent(" foo\n\tbar", 2, "..", true).toString());
assertEquals("..foo\n..\tbar", replaceIndent("\tfoo\n\t\tbar", 2, "..", true).toString());
assertEquals("..foo\n..\tbar", replaceIndent("\tfoo\n \tbar", 2, "..", true).toString());

assertEquals("..\n", replaceIndent("\n", 2, "..", true).toString());
assertEquals("..\n..\n", replaceIndent("\n\n", 2, "..", true).toString());
assertEquals("..foo\n..bar\n", replaceIndent("\tfoo\n\tbar\n", 2, "..", true).toString());

assertEquals("..\r\n", replaceIndent("\r\n", 2, "..", true).toString());
assertEquals("..\r\n..\r\n", replaceIndent("\r\n\r\n", 2, "..", true).toString());
assertEquals("..foo\r\n..bar\r\n", replaceIndent("\tfoo\r\n\tbar\r\n", 2, "..", true).toString());
}

@Test
void testReplaceIndent_DoNotIndentEmptyLines() {
assertEquals("", replaceIndent("\t\t", 2, "", false).toString());
assertEquals("foo ", replaceIndent("foo ", 2, "", false).toString());
assertEquals("foo", replaceIndent(" \t foo", 2, "", false).toString());
assertEquals("foo\nbar", replaceIndent(" foo\n bar", 2, "", false).toString());
assertEquals("foo\nbar", replaceIndent(" foo\n\tbar", 2, "", false).toString());
assertEquals("foo\nbar", replaceIndent(" foo\n\tbar", 2, "", false).toString());
assertEquals("foo\n\tbar", replaceIndent("\tfoo\n\t\tbar", 2, "", false).toString());
assertEquals("foo\n\tbar", replaceIndent("\tfoo\n \tbar", 2, "", false).toString());

assertEquals("foo\r\nbar", replaceIndent(" foo\r\n bar", 2, "", false).toString());
assertEquals("foo\r\nbar", replaceIndent(" foo\r\n\tbar", 2, "", false).toString());
assertEquals("foo\r\nbar", replaceIndent(" foo\r\n\tbar", 2, "", false).toString());
assertEquals("foo\r\n\tbar", replaceIndent("\tfoo\r\n\t\tbar", 2, "", false).toString());
assertEquals("foo\r\n\tbar", replaceIndent("\tfoo\r\n \tbar", 2, "", false).toString());

assertEquals("", replaceIndent("\t\t", 2, "..", false).toString());
assertEquals("..foo ", replaceIndent("foo ", 2, "..", false).toString());
assertEquals("..foo", replaceIndent(" \t foo", 2, "..", false).toString());
assertEquals("..foo\n..bar", replaceIndent(" foo\n bar", 2, "..", false).toString());
assertEquals("..foo\n..bar", replaceIndent(" foo\n\tbar", 2, "..", false).toString());
assertEquals("..foo\n..bar", replaceIndent(" foo\n\tbar", 2, "..", false).toString());
assertEquals("..foo\n..\tbar", replaceIndent("\tfoo\n\t\tbar", 2, "..", false).toString());
assertEquals("..foo\n..\tbar", replaceIndent("\tfoo\n \tbar", 2, "..", false).toString());

assertEquals("\n", replaceIndent("\n", 2, "..", false).toString());
assertEquals("\n\n", replaceIndent("\n\n", 2, "..", false).toString());
assertEquals("..foo\n..bar\n", replaceIndent("\tfoo\n\tbar\n", 2, "..", false).toString());

assertEquals("\r\n", replaceIndent("\r\n", 2, "..", false).toString());
assertEquals("\r\n\r\n", replaceIndent("\r\n\r\n", 2, "..", false).toString());
assertEquals("..foo\r\n..bar\r\n", replaceIndent("\tfoo\r\n\tbar\r\n", 2, "..", false).toString());
}
}

0 comments on commit c66b225

Please sign in to comment.