diff --git a/plugins/org.eclipse.xtext.util/src/org/eclipse/xtext/util/internal/FormattingMigrator.java b/plugins/org.eclipse.xtext.util/src/org/eclipse/xtext/util/internal/FormattingMigrator.java index c9c449320..ba3b1f2be 100644 --- a/plugins/org.eclipse.xtext.util/src/org/eclipse/xtext/util/internal/FormattingMigrator.java +++ b/plugins/org.eclipse.xtext.util/src/org/eclipse/xtext/util/internal/FormattingMigrator.java @@ -7,9 +7,14 @@ *******************************************************************************/ package org.eclipse.xtext.util.internal; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.eclipse.xtext.util.Strings; + +import com.google.inject.internal.Lists; + /** * @author Moritz Eysholdt - Initial contribution and API * @since 2.1 @@ -35,10 +40,32 @@ public class FormattingMigrator { return semantic.charAt(index); } + public int indexOf(String str, int fromIndex) { + return semantic.indexOf(str, fromIndex); + } + public int length() { return semantic.length(); } + public void migrateFrom(FormattedString source, Mapping mapping) { + int src = mapping.srcOffset; + int dst = mapping.dstOffset; + int len = mapping.length; + if (src > 0 && dst > 0) { + src++; + dst++; + len--; + } + if (src + len + 1 < source.formatting.length && dst + len == semantic.length()) + len++; + System.arraycopy(source.formatting, src, formatting, dst, len); + } + + public String substring(int index, int lenght) { + return semantic.substring(index, lenght); + } + @Override public String toString() { StringBuilder result = new StringBuilder(); @@ -53,50 +80,59 @@ public class FormattingMigrator { } } - protected class FormattingMatch { - protected int overlapCharsAfter; - protected int overlapCharsBefore; - protected int overlapTokensAfter; - protected int overlapTokensBefore; - protected int pos; + protected class Mapping { + protected FormattedString dst; + protected int dstOffset; + protected int length; + protected FormattedString src; + protected int srcOffset; - public FormattingMatch(int pos) { + public Mapping(FormattedString src, FormattedString dst, int srcOffset, int dstOffset, int length) { super(); - this.pos = pos; + this.src = src; + this.dst = dst; + this.srcOffset = srcOffset; + this.dstOffset = dstOffset; + this.length = length; } - protected void calcOverlap(FormattedString s1, int i1, FormattedString s2, int i2) { - overlapCharsAfter = 0; - overlapCharsBefore = 0; - overlapTokensAfter = 0; - overlapTokensBefore = 0; - int downTo = Math.min(i1, i2); - int i = 1; - while (downTo - i >= 0 && s1.charAt(i1 - i) == s2.charAt(i2 - i)) { - overlapCharsBefore++; - if (s1.formatting[i1 - i] != null || s2.formatting[i2 - i] != null) - overlapTokensBefore++; - i++; - } - int upTo = Math.min(s1.length() - i1, s2.length() - i2); - int j = 1; - while (j < upTo && s1.charAt(i1 + j) == s2.charAt(i2 + j)) { - overlapCharsAfter++; - if (s1.formatting[i1 + j + 1] != null || s2.formatting[i2 + j + 1] != null) - overlapTokensAfter++; - j++; - } + @Override + public String toString() { + String s1 = src.substring(srcOffset, srcOffset + length); + String s2 = dst.substring(dstOffset, dstOffset + length); + if (s1.equals(s2)) + return s1; + return "'" + s1 + "' != '" + s2 + "'"; } - public int getOverlapChars() { - return overlapCharsBefore + overlapCharsAfter; + } + + protected class Region { + protected int length; + protected int offset; + + public Region(int offset, int length) { + super(); + this.offset = offset; + this.length = length; } - public int getOverlapTokens() { - return overlapTokensBefore + overlapTokensAfter; + @Override + public String toString() { + return offset + ">" + length; } } + protected static final Pattern WS = Pattern.compile("\\s+", Pattern.MULTILINE); + + protected int countOverlappingChars(FormattedString s1, FormattedString s2, int s1Offset, int s2Offset) { + int i = 0; + while (i + s1Offset < s1.length() && i + s2Offset < s2.length() + && s1.charAt(i + s1Offset) == s2.charAt(i + s2Offset)) + i++; + return i; + } + protected FormattedString createFormattedString(String string, Pattern format) { Matcher matcher = format.matcher(string); StringBuffer semantic = new StringBuffer(); @@ -111,57 +147,65 @@ public class FormattingMigrator { return new FormattedString(semantic.toString(), formatting); } - protected FormattingMatch match(FormattedString searchFor, int searchAt, FormattedString searchIn) { - char c1 = searchAt > 0 ? searchFor.charAt(searchAt - 1) : 0; - char c2 = searchAt < searchFor.length() ? searchFor.charAt(searchAt) : 0; - FormattingMatch lastMatch = null; - for (int i = 0; i < searchIn.length(); i++) { - FormattingMatch match = null; - if (searchIn.charAt(i) == c1) { - match = new FormattingMatch(i + 1); - match.calcOverlap(searchFor, searchAt - 1, searchIn, i); - } else if (searchIn.charAt(i) == c2) { - match = new FormattingMatch(i); - match.calcOverlap(searchFor, searchAt, searchIn, i); + protected void findLinearMatches(FormattedString formattedString, FormattedString toBeFormattedString, + List mappings, List remainingRegions) { + int i1 = 0; + int i2 = 0; + while (i1 < formattedString.length() && i2 < toBeFormattedString.length()) { + int match = countOverlappingChars(formattedString, toBeFormattedString, i1, i2); + if (match > 0) { + mappings.add(new Mapping(formattedString, toBeFormattedString, i1, i2, match)); + i1 += match; + i2 += match; } - if (match != null && qualifies(match) && (lastMatch == null || precedes(match, lastMatch))) - lastMatch = match; + if (i1 >= formattedString.length() || i2 >= toBeFormattedString.length()) + return; + int[] next = findNextOverlappingChar(formattedString, toBeFormattedString, i1, i2); + if (next == null) { + remainingRegions.add(new Region(i2, toBeFormattedString.length() - i2)); + return; + } + remainingRegions.add(new Region(i2, next[1])); + i1 += next[0]; + i2 += next[1]; } - return lastMatch; + } + + protected int[] findNextOverlappingChar(FormattedString s1, FormattedString s2, int s1Offset, int s2Offset) { + final int lenght = 2; + if (lenght + s2Offset >= s2.length()) + return null; + int[] best = null; + int i1 = 0; + while (i1 + lenght + s1Offset < s1.length()) { + String cand = s1.substring(s1Offset + i1, s1Offset + i1 + lenght); + int i2 = s2.indexOf(cand, s2Offset) - s2Offset; + if (i2 >= 0 && (best == null || best[0] + best[1] > i1 + i2)) + best = new int[] { i1, i2 }; + if (best != null && best[0] + best[1] > i1) + return best; + i1++; + } + return best; + } + + public String migrate(String formattedString, String toBeFormattedString) { + return migrate(formattedString, toBeFormattedString, WS); } public String migrate(String formattedString, String toBeFormattedString, Pattern format) { + if (Strings.isEmpty(toBeFormattedString) || Strings.isEmpty(formattedString)) + return toBeFormattedString; FormattedString formatted = createFormattedString(formattedString, format); FormattedString toBeFormatted = createFormattedString(toBeFormattedString, format); if (formatted.semantic.equals(toBeFormatted.semantic)) return formattedString; - FormattedString result = new FormattedString(toBeFormatted.semantic); - for (int i = 0; i <= formatted.semantic.length(); i++) - if (formatted.formatting[i] != null) { - FormattingMatch match = match(formatted, i, result); - if (match != null) { - // result.formatting[match.pos] = "[" + formatted.formatting[i] + i + "]"; - result.formatting[match.pos] = formatted.formatting[i]; - if (toBeFormatted.formatting[match.pos] != null) - toBeFormatted.formatting[match.pos] = null; - } - } - for (int i = 0; i <= toBeFormatted.semantic.length(); i++) - if (toBeFormatted.formatting[i] != null) - result.formatting[i] = toBeFormatted.formatting[i]; - return result.toString(); - } - - protected boolean precedes(FormattingMatch candidate, FormattingMatch competitor) { - if (candidate.getOverlapTokens() > competitor.getOverlapTokens()) - return true; - if (candidate.getOverlapChars() > competitor.getOverlapChars()) - return true; - return false; - } - - protected boolean qualifies(FormattingMatch match) { - return match.getOverlapTokens() >= 1; + List mappings = Lists.newArrayList(); + List remainingRegions = Lists.newArrayList(); + findLinearMatches(formatted, toBeFormatted, mappings, remainingRegions); + for (Mapping m : mappings) + toBeFormatted.migrateFrom(formatted, m); + return toBeFormatted.toString(); } } diff --git a/tests/org.eclipse.xtext.tests/src/org/eclipse/xtext/util/FormattingMigratorTest.java b/tests/org.eclipse.xtext.tests/src/org/eclipse/xtext/util/FormattingMigratorTest.java index 1329b92a9..ce05760f8 100644 --- a/tests/org.eclipse.xtext.tests/src/org/eclipse/xtext/util/FormattingMigratorTest.java +++ b/tests/org.eclipse.xtext.tests/src/org/eclipse/xtext/util/FormattingMigratorTest.java @@ -22,6 +22,26 @@ public class FormattingMigratorTest extends TestCase { private Pattern ws = Pattern.compile("\\s+"); + public void testRobust1() { + String act = mig.migrate(" foo bar ", null, ws); + assertNull(act); + } + + public void testRobust2() { + String act = mig.migrate(" foo bar ", "", ws); + assertEquals("", act); + } + + public void testRobust3() { + String act = mig.migrate(null, " foo bar ", ws); + assertEquals(" foo bar ", act); + } + + public void testRobust4() { + String act = mig.migrate("", " foo bar ", ws); + assertEquals(" foo bar ", act); + } + public void testKeepFormatting() { String act = mig.migrate(" foo bar ", " foo bar ", ws); assertEquals(" foo bar ", act); @@ -58,8 +78,8 @@ public class FormattingMigratorTest extends TestCase { } public void testPartialMatch() { - String act = mig.migrate(" foo baz ", "foo bar baz", ws); - assertEquals(" foo bar baz ", act); + String act = mig.migrate(" xxx zzz ", "xxx yyy zzz", ws); + assertEquals(" xxx yyy zzz ", act); } }