From c66b2255abfdbea39342bcaea0c5a1041dd310e4 Mon Sep 17 00:00:00 2001 From: sebthom Date: Sat, 9 Mar 2024 12:24:20 +0100 Subject: [PATCH] fix: auto-indent not working properly with multi-line CRLF text --- ...LanguageConfigurationAutoEditStrategy.java | 2 +- .../internal/utils/TextUtils.java | 75 ++++++++++------ .../internal/utils/TextUtilsTest.java | 89 ++++++++++++++----- 3 files changed, 113 insertions(+), 53 deletions(-) diff --git a/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/LanguageConfigurationAutoEditStrategy.java b/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/LanguageConfigurationAutoEditStrategy.java index 3e92f8ad4..9444ff3dc 100644 --- a/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/LanguageConfigurationAutoEditStrategy.java +++ b/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/LanguageConfigurationAutoEditStrategy.java @@ -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) { diff --git a/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/utils/TextUtils.java b/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/utils/TextUtils.java index 6a4347e6c..14ad0fbb5 100644 --- a/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/utils/TextUtils.java +++ b/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/utils/TextUtils.java @@ -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 { @@ -164,8 +165,8 @@ public void accept(final int value) { } abstract void onChar(char ch); - } + /* * determine common indentation of all lines */ @@ -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; } } } @@ -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++; } } } @@ -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; } diff --git a/org.eclipse.tm4e.languageconfiguration/src/test/java/org/eclipse/tm4e/languageconfiguration/internal/utils/TextUtilsTest.java b/org.eclipse.tm4e.languageconfiguration/src/test/java/org/eclipse/tm4e/languageconfiguration/internal/utils/TextUtilsTest.java index bb2b071e6..3bf8a981a 100644 --- a/org.eclipse.tm4e.languageconfiguration/src/test/java/org/eclipse/tm4e/languageconfiguration/internal/utils/TextUtilsTest.java +++ b/org.eclipse.tm4e.languageconfiguration/src/test/java/org/eclipse/tm4e/languageconfiguration/internal/utils/TextUtilsTest.java @@ -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()); } }