Strilanc/Quirk

View on GitHub
src/base/Revision.js

Summary

Maintainability
A
0 mins
Test Coverage
/**
 * Copyright 2017 Google Inc.
 *
 * 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.
 */

import {describe} from "./Describe.js"
import {equate} from "./Equate.js"
import {DetailedError} from "./DetailedError.js"
import {ObservableSource, ObservableValue} from "./Obs.js"

/**
 * A simple linear revision history tracker, for supporting undo and redo functionality.
 */
class Revision {
    /**
     * @param {!Array.<*>} history
     * @param {!int} index
     * @param {!boolean} isWorkingOnCommit
     */
    constructor(history, index, isWorkingOnCommit) {
        if (index < 0 || index >= history.length) {
            throw new DetailedError("Bad index", {history, index, isWorkingOnCommit});
        }
        if (!Array.isArray(history)) {
            throw new DetailedError("Bad history", {history, index, isWorkingOnCommit});
        }

        /** @type {!Array.<*>} */
        this.history = history;
        /** @type {!int} */
        this.index = index;
        /** @type {!boolean} */
        this.isWorkingOnCommit = isWorkingOnCommit;
        /** @type {!ObservableSource} */
        this._changes = new ObservableSource();
        /** @type {!ObservableSource} */
        this._latestActiveCommit = new ObservableValue(this.history[this.index]);
    }

    /**
     * @returns {!Observable.<*>}
     */
    changes() {
        return this._changes.observable();
    }

    /**
     * @returns {!Observable.<*>}
     */
    latestActiveCommit() {
        return this._latestActiveCommit.observable();
    }

    /**
     * Returns a snapshot of the current commit.
     * @returns {*}
     */
    peekActiveCommit() {
        return this._latestActiveCommit.get();
    }

    /**
     * Returns a cleared revision history, starting at the given state.
     * @param {*} state
     */
    static startingAt(state) {
        return new Revision([state], 0, false);
    }

    /**
     * @returns {!boolean}
     */
    isAtBeginningOfHistory() {
        return this.index === 0 && !this.isWorkingOnCommit;
    }

    /**
     * @returns {!boolean}
     */
    isAtEndOfHistory() {
        return this.index === this.history.length - 1;
    }

    /**
     * Throws away all revisions and resets the given state.
     * @param {*} state
     * @returns {void}
     */
    clear(state) {
        this.history = [state];
        this.index = 0;
        this.isWorkingOnCommit = false;
        this._changes.send(state);
        this._latestActiveCommit.set(state);
    }

    /**
     * Indicates that there are pending changes, so that a following 'undo' will return to the current state instead of
     * the previous state.
     * @returns {void}
     */
    startedWorkingOnCommit() {
        this.isWorkingOnCommit = true;
        this._changes.send(undefined);
    }

    /**
     * Indicates that pending changes were discarded, so that a following 'undo' should return to the previous state
     * instead of the current state.
     * @returns {*} The new current state.
     */
    cancelCommitBeingWorkedOn() {
        this.isWorkingOnCommit = false;
        let result = this.history[this.index];
        this._changes.send(result);
        this._latestActiveCommit.set(result);
        return result;
    }

    /**
     * Throws away future states, appends the given state, and marks it as the current state
     * @param {*} newCheckpoint
     * @returns {void}
     */
    commit(newCheckpoint) {
        if (newCheckpoint === this.history[this.index]) {
            this.cancelCommitBeingWorkedOn();
            return;
        }
        this.isWorkingOnCommit = false;
        this.index += 1;
        this.history.splice(this.index, this.history.length - this.index);
        this.history.push(newCheckpoint);
        this._changes.send(newCheckpoint);
        this._latestActiveCommit.set(newCheckpoint);
    }

    /**
     * Marks the previous state as the current state and returns it (or resets to the current state if
     * 'working on a commit' was indicated).
     * @returns {undefined|*} The new current state, or undefined if there's nothing to undo.
     */
    undo() {
        if (!this.isWorkingOnCommit) {
            if (this.index === 0) {
                return undefined;
            }
            this.index -= 1;
        }
        this.isWorkingOnCommit = false;
        let result = this.history[this.index];
        this._changes.send(result);
        this._latestActiveCommit.set(result);
        return result;
    }

    /**
     * Marks the next state as the current state and returns it (or does nothing if there is no next state).
     * @returns {undefined|*} The new current state, or undefined if there's nothing to redo.
     */
    redo() {
        if (this.index + 1 === this.history.length) {
            return undefined;
        }
        this.index += 1;
        this.isWorkingOnCommit = false;
        let result = this.history[this.index];
        this._changes.send(result);
        this._latestActiveCommit.set(result);
        return result;
    }

    /**
     * @returns {!string} A description of the revision.
     */
    toString() {
        return 'Revision(' + describe({
            index: this.index,
            count: this.history.length,
            workingOnCommit: this.isWorkingOnCommit,
            head: this.history[this.index]
        }) + ')';
    }

    /**
     * Determines if two revisions currently have the same state.
     * @param {*|!Revision} other
     * @returns {!boolean}
     */
    isEqualTo(other) {
        return other instanceof Revision &&
            this.index === other.index &&
            this.isWorkingOnCommit === other.isWorkingOnCommit &&
            equate(this.history, other.history);
    }
}

export {Revision}