diff --git a/java/src/processing/mode/java/pdex/ASTGenerator.java b/java/src/processing/mode/java/pdex/ASTGenerator.java index 9ddf86951..e1add78a0 100644 --- a/java/src/processing/mode/java/pdex/ASTGenerator.java +++ b/java/src/processing/mode/java/pdex/ASTGenerator.java @@ -2110,12 +2110,12 @@ public class ASTGenerator { // If the parsed code contains pde enhancements, take 'em out. // TODO: test this - SourceMapping mapping = new SourceMapping(); - mapping.addAll(SourceUtils.replaceTypeConstructors(pdePhrase)); - mapping.addAll(SourceUtils.replaceHexLiterals(pdePhrase)); - mapping.addAll(SourceUtils.replaceColorRegex(pdePhrase)); - mapping.addAll(SourceUtils.fixFloatsRegex(pdePhrase)); - String phrase = mapping.apply(pdePhrase); + TextTransform transform = new TextTransform(pdePhrase); + transform.addAll(SourceUtils.replaceTypeConstructors(pdePhrase)); + transform.addAll(SourceUtils.replaceHexLiterals(pdePhrase)); + transform.addAll(SourceUtils.replaceColorRegex(pdePhrase)); + transform.addAll(SourceUtils.fixFloatsRegex(pdePhrase)); + String phrase = transform.apply(); //After typing 'arg.' all members of arg type are to be listed. This one is a flag for it boolean noCompare = phrase.endsWith("."); diff --git a/java/src/processing/mode/java/pdex/ErrorCheckerService.java b/java/src/processing/mode/java/pdex/ErrorCheckerService.java index 4fdc077bf..833a4195e 100644 --- a/java/src/processing/mode/java/pdex/ErrorCheckerService.java +++ b/java/src/processing/mode/java/pdex/ErrorCheckerService.java @@ -86,6 +86,7 @@ import processing.data.IntList; import processing.data.StringList; import processing.mode.java.JavaMode; import processing.mode.java.JavaEditor; +import processing.mode.java.pdex.TextTransform.OffsetMapper; import processing.mode.java.preproc.PdePreprocessor; import processing.mode.java.preproc.PdePreprocessor.Mode; @@ -356,23 +357,18 @@ public class ErrorCheckerService { {{ // SYNTAX CHECK - try { - SourceUtils.scrubCommentsAndStrings(workBuffer); - } catch (RuntimeException e) { - // Continue normally, comments were scrubbed - // Unterminated comment will get caught during syntax check - } + SourceUtils.scrubCommentsAndStrings(workBuffer); Mode mode = PdePreprocessor.parseMode(workBuffer); // Prepare transforms - SourceMapping syntaxMapping = new SourceMapping(); - syntaxMapping.addAll(SourceUtils.insertImports(coreAndDefaultImports)); - syntaxMapping.addAll(SourceUtils.insertImports(codeFolderImports)); - syntaxMapping.addAll(SourceUtils.parseProgramImports(workBuffer, programImports)); - syntaxMapping.addAll(SourceUtils.replaceTypeConstructors(workBuffer)); - syntaxMapping.addAll(SourceUtils.replaceHexLiterals(workBuffer)); - syntaxMapping.addAll(SourceUtils.wrapSketch(mode, className, workBuffer.length())); + TextTransform toParsable = new TextTransform(pdeStage); + toParsable.addAll(SourceUtils.insertImports(coreAndDefaultImports)); + toParsable.addAll(SourceUtils.insertImports(codeFolderImports)); + toParsable.addAll(SourceUtils.parseProgramImports(workBuffer, programImports)); + toParsable.addAll(SourceUtils.replaceTypeConstructors(workBuffer)); + toParsable.addAll(SourceUtils.replaceHexLiterals(workBuffer)); + toParsable.addAll(SourceUtils.wrapSketch(mode, className, workBuffer.length())); boolean importsChanged = prevResult == null || prevResult.classPath == null || prevResult.classLoader == null || @@ -402,34 +398,35 @@ public class ErrorCheckerService { } // Transform code - String syntaxStage = syntaxMapping.apply(pdeStage); + String parsableStage = toParsable.apply(); + OffsetMapper parsableMapper = toParsable.getMapper(); // Create AST - CompilationUnit syntaxCU = - makeAST(parser, syntaxStage.toCharArray(), COMPILER_OPTIONS); + CompilationUnit parsableCU = + makeAST(parser, parsableStage.toCharArray(), COMPILER_OPTIONS); // Prepare transforms - SourceMapping compilationMapping = new SourceMapping(); - compilationMapping.addAll(SourceUtils.addPublicToTopLevelMethods(syntaxCU)); - compilationMapping.addAll(SourceUtils.replaceColorAndFixFloats(syntaxCU)); + TextTransform toCompilable = new TextTransform(parsableStage); + toCompilable.addAll(SourceUtils.addPublicToTopLevelMethods(parsableCU)); + toCompilable.addAll(SourceUtils.replaceColorAndFixFloats(parsableCU)); // Transform code - String javaStage = compilationMapping.apply(syntaxStage); - javaStageChars = javaStage.toCharArray(); + String compilableStage = toCompilable.apply(); + OffsetMapper compilableMapper = toCompilable.getMapper(); + javaStageChars = compilableStage.toCharArray(); // Create AST - CompilationUnit compilationCU = + CompilationUnit compilableCU = makeASTWithBindings(parser, javaStageChars, COMPILER_OPTIONS, className, result.classPathArray); // Update result - result.syntaxMapping = syntaxMapping; - result.compilationMapping = compilationMapping; - result.javaCode = javaStage; - result.compilationUnit = compilationCU; + result.offsetMapper = parsableMapper.thenMapping(compilableMapper); + result.javaCode = compilableStage; + result.compilationUnit = compilableCU; // Get syntax problems - List syntaxProblems = Arrays.asList(compilationCU.getProblems()); + List syntaxProblems = Arrays.asList(compilableCU.getProblems()); result.hasSyntaxErrors = syntaxProblems.stream().anyMatch(IProblem::isError); }} @@ -444,7 +441,7 @@ public class ErrorCheckerService { // TODO: handle error stuff *after* building PreprocessedSketch List mappedCompilationProblems = mapProblems(compilationProblems, result.tabStarts, result.pdeCode, - result.compilationMapping, result.syntaxMapping); + result.offsetMapper); if (Preferences.getBoolean(JavaMode.SUGGEST_IMPORTS_PREF)) { Map> undefinedTypeProblems = mappedCompilationProblems.stream() @@ -561,7 +558,7 @@ public class ErrorCheckerService { protected static List mapProblems(List problems, int[] tabStarts, String pdeCode, - SourceMapping... mappings) { + OffsetMapper mapper) { return problems.stream() // Filter Warnings if they are not enabled .filter(iproblem -> !(iproblem.isWarning() && !JavaMode.warningsEnabled)) @@ -577,11 +574,9 @@ public class ErrorCheckerService { int start = iproblem.getSourceStart(); int stop = iproblem.getSourceEnd(); // inclusive - // Apply mappings - for (SourceMapping mapping : mappings) { - start = mapping.getInputOffset(start); - stop = mapping.getInputOffset(stop); - } + // Apply mapping + start = mapper.getInputOffset(start); + stop = mapper.getInputOffset(stop); if (stop < start) { // Should not happen, just to be sure diff --git a/java/src/processing/mode/java/pdex/PreprocessedSketch.java b/java/src/processing/mode/java/pdex/PreprocessedSketch.java index 7c8ab194d..55d811058 100644 --- a/java/src/processing/mode/java/pdex/PreprocessedSketch.java +++ b/java/src/processing/mode/java/pdex/PreprocessedSketch.java @@ -11,6 +11,7 @@ import java.util.Collections; import java.util.List; import processing.app.Sketch; +import processing.mode.java.pdex.TextTransform.OffsetMapper; public class PreprocessedSketch { @@ -27,8 +28,7 @@ public class PreprocessedSketch { public final String pdeCode; public final String javaCode; - public final SourceMapping syntaxMapping; - public final SourceMapping compilationMapping; + public final OffsetMapper offsetMapper; public final boolean hasSyntaxErrors; public final boolean hasCompilationErrors; @@ -81,10 +81,7 @@ public class PreprocessedSketch { int tabStartLine = tabIndexToTabStartLine(tabIndex); int pdeLine = tabStartLine + tabLine; int pdeLineOffset = lineToOffset(pdeCode, pdeLine); - int javaLineOffset = syntaxMapping.getOutputOffset(pdeLineOffset); - if (compilationMapping != null) { - javaLineOffset = compilationMapping.getOutputOffset(javaLineOffset); - } + int javaLineOffset = offsetMapper.getOutputOffset(pdeLineOffset); return offsetToLine(javaCode, javaLineOffset); } @@ -93,23 +90,12 @@ public class PreprocessedSketch { int tabStartLine = tabIndexToTabStartLine(tabIndex); int tabStartOffset = lineToOffset(pdeCode, tabStartLine); int pdeOffset = tabStartOffset + tabOffset; - int javaOffset = syntaxMapping.getOutputOffset(pdeOffset); - if (compilationMapping != null) { - javaOffset = compilationMapping.getOutputOffset(javaOffset); - } - return javaOffset; + return offsetMapper.getOutputOffset(pdeOffset); } public int javaOffsetToPdeOffset(int javaOffset) { - int pdeOffset = javaOffset; - if (compilationMapping != null) { - pdeOffset = compilationMapping.getInputOffset(pdeOffset); - } - if (syntaxMapping != null) { - pdeOffset = syntaxMapping.getInputOffset(pdeOffset); - } - return pdeOffset; + return offsetMapper.getInputOffset(javaOffset); } @@ -157,8 +143,7 @@ public class PreprocessedSketch { public String pdeCode; public String javaCode; - public SourceMapping syntaxMapping; - public SourceMapping compilationMapping; + public OffsetMapper offsetMapper; public boolean hasSyntaxErrors; public boolean hasCompilationErrors; @@ -192,8 +177,7 @@ public class PreprocessedSketch { pdeCode = b.pdeCode; javaCode = b.javaCode; - syntaxMapping = b.syntaxMapping; - compilationMapping = b.compilationMapping; + offsetMapper = b.offsetMapper != null ? b.offsetMapper : OffsetMapper.EMPTY_MAPPER; hasSyntaxErrors = b.hasSyntaxErrors; hasCompilationErrors = b.hasCompilationErrors; diff --git a/java/src/processing/mode/java/pdex/SourceMapping.java b/java/src/processing/mode/java/pdex/SourceMapping.java deleted file mode 100644 index efac198f9..000000000 --- a/java/src/processing/mode/java/pdex/SourceMapping.java +++ /dev/null @@ -1,236 +0,0 @@ -package processing.mode.java.pdex; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.ListIterator; -import java.util.stream.Collectors; - -import processing.app.Base; -import processing.core.PApplet; - -import static java.awt.SystemColor.text; - - -public class SourceMapping { - - private static final Comparator INPUT_OFFSET_COMP = - (o1, o2) -> Integer.compare(o1.fromOffset, o2.fromOffset); - - private static final Comparator OUTPUT_OFFSET_COMP = - (o1, o2) -> Integer.compare(o1.toOffset, o2.toOffset); - - private boolean applied; - - private List edits = new ArrayList<>(); - - private List inMap = new ArrayList<>(); - private List outMap = new ArrayList<>(); - - - public void add(Edit edit) { - edits.add(edit); - } - - - public void addAll(Collection edits) { - this.edits.addAll(edits); - } - - - public String apply(CharSequence input) { - - final int inLength = input.length(); - final StringBuilder output = new StringBuilder(inLength); - - // Make copies of Edits to preserve original edits - List inEdits = edits.stream().map(Edit::new).collect(Collectors.toList()); - List outEdits = new ArrayList<>(inEdits); - - // Edits sorted by input offsets - Collections.sort(inEdits, INPUT_OFFSET_COMP); - - // Edits sorted by output offsets - Collections.sort(outEdits, OUTPUT_OFFSET_COMP); - - // TODO: add some validation - - // Input - ListIterator inIt = inEdits.listIterator(); - Edit inEdit = inIt.hasNext() ? inIt.next() : null; - int inEditOff = inEdit == null ? input.length() : inEdit.fromOffset; - - // Output - ListIterator outIt = outEdits.listIterator(); - Edit outEdit = outIt.hasNext() ? outIt.next() : null; - int outEditOff = outEdit == null ? input.length() : outEdit.toOffset; - - int offset = 0; - - inMap.clear(); - outMap.clear(); - - // Walk through the input, apply changes, create mapping - while (offset < inLength || inEdit != null || outEdit != null) { - - { // Copy the unmodified portion of the input, create mapping for it - int nextEditOffset = Math.min(inEditOff, outEditOff); - - { // Insert move block to have mapping for unmodified portions too - int length = nextEditOffset - offset; - if (length > 0) { - Edit ch = Edit.move(offset, length, output.length()); - inMap.add(ch); - outMap.add(ch); - } - } - - // Copy the block without changes from the input - output.append(input, offset, nextEditOffset); - - // Move offset accordingly - offset = nextEditOffset; - } - - // Process encountered input edits - while (inEdit != null && offset >= inEditOff) { - offset += inEdit.fromLength; - inMap.add(inEdit); - inEdit = inIt.hasNext() ? inIt.next() : null; - inEditOff = inEdit != null ? inEdit.fromOffset : inLength; - } - - // Process encountered output edits - while (outEdit != null && offset >= outEditOff) { - outEdit.toOffset = output.length(); - outMap.add(outEdit); - if (outEdit.toLength > 0) { - if (outEdit.outputText != null) { - output.append(outEdit.outputText); - } else { - output.append(input, outEdit.fromOffset, outEdit.fromOffset + outEdit.fromLength); - } - } - outEdit = outIt.hasNext() ? outIt.next() : null; - outEditOff = outEdit != null ? outEdit.toOffset : inLength; - } - } - - applied = true; - - return output.toString(); - } - - - public int getInputOffset(int outputOffset) { - if (Base.DEBUG) checkApplied(); - Edit searchKey = new Edit(0, 0, outputOffset, Integer.MAX_VALUE, null); - int i = Collections.binarySearch(outMap, searchKey, OUTPUT_OFFSET_COMP); - if (i < 0) { - i = -(i + 1); - i -= 1; - } - i = PApplet.constrain(i, 0, outMap.size()-1); - Edit edit = outMap.get(i); - int diff = outputOffset - edit.toOffset; - return edit.fromOffset + Math.min(diff, Math.max(0, edit.fromLength - 1)); - } - - - public int getOutputOffset(int inputOffset) { - if (Base.DEBUG) checkApplied(); - Edit searchKey = new Edit(inputOffset, Integer.MAX_VALUE, 0, 0, null); - int i = Collections.binarySearch(inMap, searchKey, INPUT_OFFSET_COMP); - if (i < 0) { - i = -(i + 1); - i -= 1; - } - i = PApplet.constrain(i, 0, inMap.size()-1); - Edit edit = inMap.get(i); - int diff = inputOffset - edit.fromOffset; - return edit.toOffset + Math.min(diff, Math.max(0, edit.toLength - 1)); - } - - - public void clear() { - applied = false; - edits.clear(); - inMap.clear(); - outMap.clear(); - } - - - private void checkNotApplied() { - if (applied) throw new RuntimeException("this mapping was already applied"); - } - - - private void checkApplied() { - if (!applied) throw new RuntimeException("this mapping was not applied yet"); - } - - - @Override - public String toString() { - return "SourceMapping{" + - "edits=" + edits + - ", applied=" + applied + - '}'; - } - - - protected static class Edit { - - static Edit insert(int offset, String text) { - return new Edit(offset, 0, offset, text.length(), text); - } - - static Edit replace(int offset, int length, String text) { - return new Edit(offset, length, offset, text.length(), text); - } - - static Edit move(int fromOffset, int length, int toOffset) { - Edit result = new Edit(fromOffset, length, toOffset, length, null); - result.toOffset = toOffset; - return result; - } - - static Edit delete(int position, int length) { - return new Edit(position, length, position, 0, null); - } - - Edit(Edit edit) { - this.fromOffset = edit.fromOffset; - this.fromLength = edit.fromLength; - this.toOffset = edit.toOffset; - this.toLength = edit.toLength; - this.outputText = edit.outputText; - } - - Edit(int fromOffset, int fromLength, int toOffset, int toLength, String text) { - this.fromOffset = fromOffset; - this.fromLength = fromLength; - this.toOffset = toOffset; - this.toLength = toLength; - this.outputText = text; - } - - final int fromOffset; - final int fromLength; - int toOffset; - final int toLength; - final String outputText; - - @Override - public String toString() { - return "Edit{" + - "from=" + fromOffset + ":" + fromLength + - ", to=" + toOffset + ":" + toLength + - ((text != null) ? (", text='" + outputText + '\'') : "") + - '}'; - } - } - -} diff --git a/java/src/processing/mode/java/pdex/SourceUtils.java b/java/src/processing/mode/java/pdex/SourceUtils.java index bc9cd7e97..c7e042b78 100644 --- a/java/src/processing/mode/java/pdex/SourceUtils.java +++ b/java/src/processing/mode/java/pdex/SourceUtils.java @@ -12,7 +12,7 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -import processing.mode.java.pdex.SourceMapping.Edit; +import processing.mode.java.pdex.TextTransform.Edit; import processing.mode.java.preproc.PdePreprocessor; public class SourceUtils { @@ -299,7 +299,4 @@ public class SourceUtils { } - public static void main(String[] args) { - System.out.println(); - } } \ No newline at end of file diff --git a/java/src/processing/mode/java/pdex/TextTransform.java b/java/src/processing/mode/java/pdex/TextTransform.java new file mode 100644 index 000000000..7f20a08ab --- /dev/null +++ b/java/src/processing/mode/java/pdex/TextTransform.java @@ -0,0 +1,310 @@ +package processing.mode.java.pdex; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.ListIterator; +import java.util.stream.Collectors; + +import processing.core.PApplet; + +import static java.awt.SystemColor.text; + + +public class TextTransform { + + private static final Comparator INPUT_OFFSET_COMP = + (o1, o2) -> Integer.compare(o1.fromOffset, o2.fromOffset); + + private static final Comparator OUTPUT_OFFSET_COMP = + (o1, o2) -> Integer.compare(o1.toOffset, o2.toOffset); + + + private CharSequence input; + + private List edits = new ArrayList<>(); + + private List inMap = new ArrayList<>(); + private List outMap = new ArrayList<>(); + + private boolean built; + private int builtForLength; + + + TextTransform(CharSequence input) { + this.input = input; + } + + + public void add(Edit edit) { + edits.add(edit); + built = false; + } + + + public void addAll(Collection edits) { + this.edits.addAll(edits); + built = false; + } + + + public String apply() { + final int inLength = input.length(); + final StringBuilder output = new StringBuilder(inLength); + + buildIfNeeded(inLength); + + outMap.stream() + // Filter out Delete edits + .filter(outEdit -> outEdit.toLength > 0) + .forEach(outEdit -> { + if (outEdit.outputText != null) { + // Insert or Replace edit + output.append(outEdit.outputText); + } else { + // Move edit + output.append(input, outEdit.fromOffset, outEdit.fromOffset + outEdit.fromLength); + } + }); + + return output.toString(); + } + + + public OffsetMapper getMapper() { + int inLength = input.length(); + buildIfNeeded(inLength); + return new SimpleOffsetMapper(inMap, outMap); + } + + + private void buildIfNeeded(int inLength) { + if (built && inLength == builtForLength) return; + + // Make copies of Edits to preserve original edits + List inEdits = edits.stream().map(Edit::new).collect(Collectors.toList()); + List outEdits = new ArrayList<>(inEdits); + + // Edits sorted by input offsets + Collections.sort(inEdits, INPUT_OFFSET_COMP); + + // Edits sorted by output offsets + Collections.sort(outEdits, OUTPUT_OFFSET_COMP); + + // TODO: add some validation + + // Input + ListIterator inIt = inEdits.listIterator(); + Edit inEdit = inIt.hasNext() ? inIt.next() : null; + int inEditOff = inEdit == null ? inLength : inEdit.fromOffset; + + // Output + ListIterator outIt = outEdits.listIterator(); + Edit outEdit = outIt.hasNext() ? outIt.next() : null; + int outEditOff = outEdit == null ? inLength : outEdit.toOffset; + + int inOffset = 0; + int outOffset = 0; + + inMap.clear(); + outMap.clear(); + + // Walk through the input, apply changes, create mapping + while (inOffset < inLength || inEdit != null || outEdit != null) { + + { // Create mapping for unmodified portion of the input + int nextEditOffset = Math.min(inEditOff, outEditOff); + + { // Insert move block to have mapping for unmodified portions too + int length = nextEditOffset - inOffset; + if (length > 0) { + Edit ch = Edit.move(inOffset, length, outOffset); + inMap.add(ch); + outMap.add(ch); + } + } + + // Move offsets accordingly + outOffset += nextEditOffset - inOffset; + inOffset = nextEditOffset; + } + + // Process encountered input edits + while (inEdit != null && inOffset >= inEditOff) { + inOffset += inEdit.fromLength; + inMap.add(inEdit); + inEdit = inIt.hasNext() ? inIt.next() : null; + inEditOff = inEdit != null ? inEdit.fromOffset : inLength; + } + + // Process encountered output edits + while (outEdit != null && inOffset >= outEditOff) { + outEdit.toOffset = outOffset; + outMap.add(outEdit); + outOffset += outEdit.toLength; + outEdit = outIt.hasNext() ? outIt.next() : null; + outEditOff = outEdit != null ? outEdit.toOffset : inLength; + } + } + + built = true; + builtForLength = inLength; + } + + + @Override + public String toString() { + return "SourceMapping{" + + "edits=" + edits + + '}'; + } + + + protected static class Edit { + + static Edit insert(int offset, String text) { + return new Edit(offset, 0, offset, text.length(), text); + } + + static Edit replace(int offset, int length, String text) { + return new Edit(offset, length, offset, text.length(), text); + } + + static Edit move(int fromOffset, int length, int toOffset) { + Edit result = new Edit(fromOffset, length, toOffset, length, null); + result.toOffset = toOffset; + return result; + } + + static Edit delete(int position, int length) { + return new Edit(position, length, position, 0, null); + } + + Edit(Edit edit) { + this.fromOffset = edit.fromOffset; + this.fromLength = edit.fromLength; + this.toOffset = edit.toOffset; + this.toLength = edit.toLength; + this.outputText = edit.outputText; + } + + Edit(int fromOffset, int fromLength, int toOffset, int toLength, String text) { + this.fromOffset = fromOffset; + this.fromLength = fromLength; + this.toOffset = toOffset; + this.toLength = toLength; + this.outputText = text; + } + + private final int fromOffset; + private final int fromLength; + private int toOffset; + private final int toLength; + private final String outputText; + + @Override + public String toString() { + return "Edit{" + + "from=" + fromOffset + ":" + fromLength + + ", to=" + toOffset + ":" + toLength + + ((text != null) ? (", text='" + outputText + '\'') : "") + + '}'; + } + } + + + protected interface OffsetMapper { + int getInputOffset(int outputOffset); + int getOutputOffset(int inputOffset); + OffsetMapper thenMapping(OffsetMapper mapper); + OffsetMapper EMPTY_MAPPER = CompositeOffsetMapper.of(); + } + + + private static class SimpleOffsetMapper implements OffsetMapper { + private List inMap = new ArrayList<>(); + private List outMap = new ArrayList<>(); + + private SimpleOffsetMapper(List inMap, List outMap) { + this.inMap.addAll(inMap); + this.outMap.addAll(outMap); + } + + @Override + public int getInputOffset(int outputOffset) { + Edit searchKey = new Edit(0, 0, outputOffset, Integer.MAX_VALUE, null); + int i = Collections.binarySearch(outMap, searchKey, OUTPUT_OFFSET_COMP); + if (i < 0) { + i = -(i + 1); + i -= 1; + } + i = PApplet.constrain(i, 0, outMap.size()-1); + Edit edit = outMap.get(i); + int diff = outputOffset - edit.toOffset; + return edit.fromOffset + Math.min(diff, Math.max(0, edit.fromLength - 1)); + } + + @Override + public int getOutputOffset(int inputOffset) { + Edit searchKey = new Edit(inputOffset, Integer.MAX_VALUE, 0, 0, null); + int i = Collections.binarySearch(inMap, searchKey, INPUT_OFFSET_COMP); + if (i < 0) { + i = -(i + 1); + i -= 1; + } + i = PApplet.constrain(i, 0, inMap.size()-1); + Edit edit = inMap.get(i); + int diff = inputOffset - edit.fromOffset; + return edit.toOffset + Math.min(diff, Math.max(0, edit.toLength - 1)); + } + + @Override + public OffsetMapper thenMapping(OffsetMapper mapper) { + return CompositeOffsetMapper.of(this, mapper); + } + } + + + private static class CompositeOffsetMapper implements OffsetMapper { + private List mappers = new ArrayList<>(); + + public static CompositeOffsetMapper of(OffsetMapper... inMappers) { + CompositeOffsetMapper composite = new CompositeOffsetMapper(); + + // Add mappers one by one, unwrap if Composite + for (OffsetMapper mapper : inMappers) { + if (mapper instanceof CompositeOffsetMapper) { + composite.mappers.addAll(((CompositeOffsetMapper) mapper).mappers); + } else { + composite.mappers.add(mapper); + } + } + + return composite; + } + + @Override + public int getInputOffset(int outputOffset) { + for (int i = mappers.size() - 1; i >= 0; i--) { + outputOffset = mappers.get(i).getInputOffset(outputOffset); + } + return outputOffset; + } + + @Override + public int getOutputOffset(int inputOffset) { + for (OffsetMapper mapper : mappers) { + inputOffset = mapper.getOutputOffset(inputOffset); + } + return inputOffset; + } + + @Override + public OffsetMapper thenMapping(OffsetMapper mapper) { + return CompositeOffsetMapper.of(this, mapper); + } + } + +}