[xtext.util] improved the algorithm of the formatting migrator

This commit is contained in:
Moritz Eysholdt 2011-10-26 05:35:32 +02:00
parent 7a2cde83d2
commit 99a1285cce
2 changed files with 141 additions and 77 deletions

View file

@ -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<Mapping> mappings, List<Region> 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<Mapping> mappings = Lists.newArrayList();
List<Region> remainingRegions = Lists.newArrayList();
findLinearMatches(formatted, toBeFormatted, mappings, remainingRegions);
for (Mapping m : mappings)
toBeFormatted.migrateFrom(formatted, m);
return toBeFormatted.toString();
}
}

View file

@ -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);
}
}