// Copyright 2012 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.collide.shared.ot; import com.google.collide.dto.DocOp; import com.google.collide.dto.shared.DocOpFactory; import com.google.collide.json.shared.JsonArray; import com.google.collide.shared.document.Document; import com.google.collide.shared.document.LineNumberAndColumn; import com.google.collide.shared.document.TextChange; import com.google.collide.shared.document.Document.TextListener; import com.google.collide.shared.util.JsonCollections; import com.google.collide.shared.util.ListenerRegistrar; /** * A mechanism that translates file positions to/from file position at * earlier point in time when this position migrator was created or reset. * The scope of PositionMigrator object is currently opened document. * On "start" position migrator starts tracking text changes. * Calling "reset" clears tracked docops and moves tracking start to current * moment. * */ public class PositionMigrator { private final TextListener textListener = new TextListener() { @Override public void onTextChange(Document document, JsonArray<TextChange> textChanges) { PositionMigrator.this.onTextChange(textChanges); } }; private final DocOpFactory docOpFactory; private final JsonArray<DocOp> appliedDocOps = JsonCollections.createArray(); private ListenerRegistrar.Remover textListenerRemover; public PositionMigrator(DocOpFactory docOpFactory) { this.docOpFactory = docOpFactory; } /** * Converts given position as it was at the time when tracking started, * to current position. * * @param lineNumber old position line number * @param column old position column * @return current position line number and column */ public LineNumberAndColumn migrateToNow(int lineNumber, int column) { // TODO: Cache the result. DocOp docOp = composeCurrentDocOps(); if (docOp == null) { return LineNumberAndColumn.from(lineNumber, column); } PositionTransformer positionTransformer = new PositionTransformer(lineNumber, column); positionTransformer.transform(docOp); return LineNumberAndColumn.from(positionTransformer.getLineNumber(), positionTransformer.getColumn()); } public boolean haveChanges() { return composeCurrentDocOps() != null; } /** * Converts given position at current time to position as it was at the time * when tracking started. * * @param lineNumber current position line number * @param column current position column * @return old position line number and column */ public LineNumberAndColumn migrateFromNow(int lineNumber, int column) { // TODO: Cache the result. DocOp docOp = composeCurrentDocOps(); if (docOp == null) { return LineNumberAndColumn.from(lineNumber, column); } PositionTransformer positionTransformer = new PositionTransformer(lineNumber, column); positionTransformer.transform(Inverter.invert(docOpFactory, docOp)); return LineNumberAndColumn.from(positionTransformer.getLineNumber(), positionTransformer.getColumn()); } /** * Forgets about all currently recorded text changes and continues tracking * if started. */ public void reset() { appliedDocOps.clear(); } /** * Starts tracking text changes. Text changes collected so far are discarded. * * @param textListenerRegistrar listener registrar to use for listening text * changes */ public void start(ListenerRegistrar<TextListener> textListenerRegistrar) { reset(); stop(); this.textListenerRemover = textListenerRegistrar.add(textListener); } /** * Stops tracking text changes, keeping currently recorded text changes. */ public void stop() { if (textListenerRemover != null) { textListenerRemover.remove(); textListenerRemover = null; } } private void onTextChange(JsonArray<TextChange> textChanges) { try { appliedDocOps.add(DocOpUtils.createFromTextChanges(docOpFactory, textChanges)); } catch (Composer.ComposeException e) { throw new RuntimeException(e); } } /** * Composes currently collected doc ops into a single doc op and returns it. * * @return composed doc op or {@code null} if no doc ops were collected so far */ private DocOp composeCurrentDocOps() { if (appliedDocOps.size() < 2) { return appliedDocOps.size() > 0 ? appliedDocOps.get(0) : null; } try { DocOp docOp = Composer.compose(docOpFactory, appliedDocOps.asIterable()); appliedDocOps.clear(); appliedDocOps.add(docOp); return docOp; } catch (Composer.ComposeException e) { throw new RuntimeException(e); } } }