adobe/brackets

View on GitHub
src/view/Pane.js

Summary

Maintainability
F
3 days
Test Coverage
/*
 * Copyright (c) 2014 - present Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

 /**
  * Pane objects host views of files, editors, etc... Clients cannot access
  * Pane objects directly. Instead the implementation is protected by the
  * MainViewManager -- however View Factories are given a Pane object which
  * they can use to add views.  References to Pane objects should not be kept
  * as they may be destroyed and removed from the DOM.
  *
  * To get a custom view, there are two components:
  *
  *  1) A View Factory
  *  2) A View Object
  *
  * View objects are anonymous object that have a particular interface.
  *
  * Views can be added to a pane but do not have to exist in the Pane object's view list.
  * Such views are "temporary views".  Temporary views are not serialized with the Pane state
  * or reconstituted when the pane is serialized from disk.  They are destroyed at the earliest
  * opportunity.
  *
  * Temporary views are added by calling `Pane.showView()` and passing it the view object. The view
  * will be destroyed when the next view is shown, the pane is mereged with another pane or the "Close All"
  * command is exectuted on the Pane.  Temporary Editor Views do not contain any modifications and are
  * added to the workingset (and are no longer tempoary views) once the document has been modified. They
  * will remain in the working set until closed from that point on.
  *
  * Views that have a longer life span are added by calling addView to associate the view with a
  * filename in the _views object.  These views are not destroyed until they are removed from the pane
  * by calling one of the following: removeView, removeViews, or _reset
  *
  * Pane Object Events:
  *
  *  - viewListChange - Whenever there is a file change to a file in the working set.  These 2 events: `DocumentManager.pathRemove`
  *  and `DocumentManager.fileNameChange` will cause a `viewListChange` event so the WorkingSetView can update.
  *
  *  - currentViewChange - Whenever the current view changes.
  *             (e, newView:View, oldView:View)
  *
  *  - viewDestroy - Whenever a view has been destroyed
  *             (e, view:View)
  *
  * View Interface:
  *
  * The view is an anonymous object which has the following method signatures. see ImageViewer for an example or the sample
  * provided with Brackets `src/extensions/samples/BracketsConfigCentral`
  *
  *     {
  *         $el:jQuery
  *         getFile: function ():!File
  *         updateLayout: function(forceRefresh:boolean)
  *         destroy: function()
  *         getScrollPos: function():*=
  *         adjustScrollPos: function(state:Object=, heightDelta:number)=
  *         notifyContainerChange: function()=
  *         notifyVisibilityChange: function(boolean)=
  *         focus:function()=
  *     }
  *
  * When views are created they can be added to the pane by calling `pane.addView()`.
  * Views can be created and parented by attaching directly  to `pane.$el`
  *
  *     this._codeMirror = new CodeMirror(pane.$el, ...)
  *
  * Factories can create a view that's initially hidden by calling `pane.addView(view)` and passing `false` for the show parameter.
  * Hidden views can be later shown by calling `pane.showView(view)`
  *
  * `$el:jQuery!`
  *
  *  property that stores the jQuery wrapped DOM element of the view. All views must have one so pane objects can manipulate the DOM
  *  element when necessary (e.g. `showView`, `_reparent`, etc...)
  *
  * `getFile():File!`
  *
  *  Called throughout the life of a View when the current file is queried by the system.
  *
  * `updateLayout(forceRefresh:boolean)`
  *
  *  Called to notify the view that it should be resized to fit its parent container.  This may be called several times
  *  or only once.  Views can ignore the `forceRefresh` flag. It is used for editor views to force a relayout of the editor
  *  which probably isn't necessary for most views.  Views should implement their html to be dynamic and not rely on this
  *  function to be called whenever possible.
  *
  * `destroy()`
  *
  *  Views must implement a destroy method to remove their DOM element at the very least.  There is no default
  *  implementation and views are hidden before this method is called. The Pane object doesn't make assumptions
  *  about when it is safe to remove a node. In some instances other cleanup  must take place before a the DOM
  *  node is destroyed so the implementation details are left to the view.
  *
  *  Views can implement a simple destroy by calling
  *
  *      this.$el.remove()
  *
  *  These members are optional and need not be implemented by Views
  *
  *      getScrollPos()
  *      adjustScrollPos()
  *
  *  The system at various times will want to save and restore a view's scroll position.  The data returned by `getScrollPos()`
  *  is specific to the view and will be passed back to `adjustScrollPos()` when the scroll position needs to be restored.
  *
  *  When Modal Bars are invoked, the system calls `getScrollPos()` so that the current scroll psotion of all visible Views can be cached.
  *  That cached scroll position is later passed to `adjustScrollPos()` along with a height delta.  The height delta is used to
  *  scroll the view so that it doesn't appear to have "jumped" when invoking the Modal Bar.
  *
  *  Height delta will be a positive when the Modal Bar is being shown and negative number when the Modal Bar is being hidden.
  *
  *  `getViewState()` is another optional member that is used to cache a view's state when hiding or destroying a view or closing the project.
  *  The data returned by this member is stored in `ViewStateManager` and is saved with the project.
  *
  *  Views or View Factories are responsible for restoring the view state when the view of that file is created by recalling the cached state
  *
  *      var view = createIconView(file, pane);
  *      view.restoreViewState(ViewStateManager.getViewState(file.fullPath));
  *
  *  Notifications
  *  The following optional methods receive notifications from the Pane object when certain events take place which affect the view:
  *
  * `notifyContainerChange()`
  *
  *  Optional Notification callback called when the container changes. The view can perform any synchronization or state update
  *  it needs to do when its parent container changes.
  *
  * `notifyVisiblityChange()`
  *
  *  Optional Notification callback called when the view's vsibility changes.  The view can perform any synchronization or
  *  state update it needs to do when its visiblity state changes.
  */
define(function (require, exports, module) {
    "use strict";

    var _                   = require("thirdparty/lodash"),
        Mustache            = require("thirdparty/mustache/mustache"),
        EventDispatcher     = require("utils/EventDispatcher"),
        FileSystem          = require("filesystem/FileSystem"),
        InMemoryFile        = require("document/InMemoryFile"),
        ViewStateManager    = require("view/ViewStateManager"),
        MainViewManager     = require("view/MainViewManager"),
        PreferencesManager  = require("preferences/PreferencesManager"),
        DocumentManager     = require("document/DocumentManager"),
        CommandManager      = require("command/CommandManager"),
        Commands            = require("command/Commands"),
        Strings             = require("strings"),
        StringUtils         = require("utils/StringUtils"),
        ViewUtils           = require("utils/ViewUtils"),
        ProjectManager      = require("project/ProjectManager"),
        paneTemplate        = require("text!htmlContent/pane.html");

    /**
     * Internal pane id
     * @const
     * @private
     */
    var FIRST_PANE          = "first-pane";

    /**
     * Internal pane id
     * @const
     * @private
     */
    var SECOND_PANE         = "second-pane";

    // Define showPaneHeaderButtons, which controls when to show close and flip-view buttons
    // on the header.
    PreferencesManager.definePreference("pane.showPaneHeaderButtons", "string", "hover", {
        description: Strings.DESCRIPTION_SHOW_PANE_HEADER_BUTTONS,
        values: ["hover", "always", "never"]
    });

    // Define mergePanesWhenLastFileClosed, which controls if a split view pane should be
    // closed when the last file is closed, skipping the "Open a file while this pane has focus"
    // step completely.
    PreferencesManager.definePreference("pane.mergePanesWhenLastFileClosed", "boolean", false, {
        description: Strings.DESCRIPTION_MERGE_PANES_WHEN_LAST_FILE_CLOSED
    });

    /**
     * Make an index request object
     * @param {boolean} requestIndex - true to request an index, false if not
     * @param {number} index - the index to request
     * @return {indexRequested:boolean, index:number} an object that can be passed to
     * {@link Pane#addToViewList} to insert the item at a specific index
     * @see Pane#addToViewList
     */
    function _makeIndexRequestObject(requestIndex, index) {
        return {indexRequested: requestIndex, index: index};
    }

    /**
     * Ensures that the given pane is focused after other focus related events occur
     * @params {string} paneId - paneId of the pane to focus
     * @private
     */
    function _ensurePaneIsFocused(paneId) {
        var pane = MainViewManager._getPane(paneId);

        // Defer the focusing until other focus events have occurred.
        setTimeout(function () {
            // Focus has most likely changed: give it back to the given pane.
            pane.focus();
            this._lastFocusedElement = pane.$el[0];
            MainViewManager.setActivePaneId(paneId);
        }, 1);
    }

    /**
     * @typedef {!$el: jQuery, getFile:function():!File, updateLayout:function(forceRefresh:boolean), destroy:function(),  getScrollPos:function():?,  adjustScrollPos:function(state:Object=, heightDelta:number)=, getViewState:function():?*=, restoreViewState:function(viewState:!*)=, notifyContainerChange:function()=, notifyVisibilityChange:function(boolean)=} View
     */

    /**
     * Pane Objects are constructed by the MainViewManager object when a Pane view is needed
     * @see {@link MainViewManager} for more information
     *
     * @constructor
     * @param {!string} id - The id to use to identify this pane
     * @param {!JQuery} $container - The parent $container to place the pane view
     */
    function Pane(id, $container) {
        this._initialize();

        // Setup the container and the element we're inserting
        var self = this,
            showPaneHeaderButtonsPref = PreferencesManager.get("pane.showPaneHeaderButtons"),
            $el = $container.append(Mustache.render(paneTemplate, {id: id})).find("#" + id),
            $header  = $el.find(".pane-header"),
            $headerText = $header.find(".pane-header-text"),
            $headerFlipViewBtn = $header.find(".pane-header-flipview-btn"),
            $headerCloseBtn = $header.find(".pane-header-close-btn"),
            $content = $el.find(".pane-content");

        $el.on("focusin.pane", function (e) {
            self._lastFocusedElement = e.target;
        });

        // Flips the current file to the other pane when clicked
        $headerFlipViewBtn.on("click.pane", function (e) {
            var currentFile = self.getCurrentlyViewedFile();
            var otherPaneId = self.id === FIRST_PANE ? SECOND_PANE : FIRST_PANE;
            var otherPane = MainViewManager._getPane(otherPaneId);
            var sameDocInOtherView = otherPane.getViewForPath(currentFile.fullPath);
            
            // If the same doc view is present in the destination, show the file instead of flipping it
            if (sameDocInOtherView) {
                CommandManager.execute(Commands.FILE_OPEN, {fullPath: currentFile.fullPath,
                                                            paneId: otherPaneId}).always(function () {
                    _ensurePaneIsFocused(otherPaneId);
                });
                return;
            }

            // Currently active pane is not necessarily self.id as just clicking the button does not
            // give focus to the pane. This way it is possible to flip multiple panes to the active one
            // without losing focus.
            var activePaneIdBeforeFlip = MainViewManager.getActivePaneId();

            MainViewManager._moveView(self.id, otherPaneId, currentFile).always(function () {
                CommandManager.execute(Commands.FILE_OPEN, {fullPath: currentFile.fullPath,
                                                            paneId: otherPaneId}).always(function () {
                    // Trigger view list changes for both panes
                    self.trigger("viewListChange");
                    otherPane.trigger("viewListChange");
                    _ensurePaneIsFocused(activePaneIdBeforeFlip);
                });
            });
        });

        // Closes the current view on the pane when clicked. If pane has no files, merge
        // panes.
        $headerCloseBtn.on("click.pane", function () {
            //set clicked pane as active to ensure that this._currentView is updated before closing
            MainViewManager.setActivePaneId(self.id);
            var file = self.getCurrentlyViewedFile();

            if (file) {
                CommandManager.execute(Commands.FILE_CLOSE, {File: file, paneId: self.id});

                if (!self.getCurrentlyViewedFile() && PreferencesManager.get("pane.mergePanesWhenLastFileClosed")) {
                    MainViewManager.setLayoutScheme(1, 1);
                }
            } else {
                MainViewManager.setLayoutScheme(1, 1);
            }
        });

        this._lastFocusedElement = $el[0];

        // Make these properties read only
        Object.defineProperty(this,  "id", {
            get: function () {
                return id;
            },
            set: function () {
                console.error("cannot change the id of a working pane");
            }
        });

        Object.defineProperty(this,  "$el", {
            get: function () {
                return $el;
            },
            set: function () {
                console.error("cannot change the DOM node of a working pane");
            }
        });

        Object.defineProperty(this,  "$header", {
            get: function () {
                return $header;
            },
            set: function () {
                console.error("cannot change the DOM node of a working pane");
            }
        });

        Object.defineProperty(this,  "$headerText", {
            get: function () {
                return $headerText;
            },
            set: function () {
                console.error("cannot change the DOM node of a working pane");
            }
        });

        Object.defineProperty(this,  "$headerFlipViewBtn", {
            get: function () {
                return $headerFlipViewBtn;
            },
            set: function () {
                console.error("cannot change the DOM node of a working pane");
            }
        });

        Object.defineProperty(this,  "$headerCloseBtn", {
            get: function () {
                return $headerCloseBtn;
            },
            set: function () {
                console.error("cannot change the DOM node of a working pane");
            }
        });

        Object.defineProperty(this,  "$content", {
            get: function () {
                return $content;
            },
            set: function () {
                console.error("cannot change the DOM node of a working pane");
            }
        });

        Object.defineProperty(this,  "$container", {
            get: function () {
                return $container;
            },
            set: function () {
                console.error("cannot change the DOM node of a working pane");
            }
        });

        this.updateHeaderText();

        switch (showPaneHeaderButtonsPref) {
        case "always":
            this.$header.addClass("always-show-header-buttons");
            break;
        case "never":
            this.$headerFlipViewBtn.css("display", "none");
            this.$headerCloseBtn.css("display", "none");
            break;
        }

        // Listen to document events so we can update ourself
        DocumentManager.on(this._makeEventName("fileNameChange"),  _.bind(this._handleFileNameChange, this));
        DocumentManager.on(this._makeEventName("pathDeleted"), _.bind(this._handleFileDeleted, this));
        MainViewManager.on(this._makeEventName("activePaneChange"), _.bind(this._handleActivePaneChange, this));
        MainViewManager.on(this._makeEventName("workingSetAdd"), _.bind(this.updateHeaderText, this));
        MainViewManager.on(this._makeEventName("workingSetRemove"), _.bind(this.updateHeaderText, this));
        MainViewManager.on(this._makeEventName("workingSetAddList"), _.bind(this.updateHeaderText, this));
        MainViewManager.on(this._makeEventName("workingSetRemoveList"), _.bind(this.updateHeaderText, this));
        MainViewManager.on(this._makeEventName("paneLayoutChange"), _.bind(this.updateFlipViewIcon, this));
    }
    EventDispatcher.makeEventDispatcher(Pane.prototype);

    /**
     * id of the pane
     * @readonly
     * @type {!string}
     */
    Pane.prototype.id = null;

    /**
     * container where the pane lives
     * @readonly
     * @type {JQuery}
     */
    Pane.prototype.$container = null;

    /**
     * the wrapped DOM node of this pane
     * @readonly
     * @type {JQuery}
     */
    Pane.prototype.$el = null;

    /**
     * the wrapped DOM node container that contains name of current view and the switch view button, or informational string if there is no view
     * @readonly
     * @type {JQuery}
     */
    Pane.prototype.$header = null;

    /**
     * the wrapped DOM node that contains name of current view, or informational string if there is no view
     * @readonly
     * @type {JQuery}
     */
    Pane.prototype.$headerText = null;

    /**
     * the wrapped DOM node that is used to flip the view to another pane
     * @readonly
     * @type {JQuery}
     */
    Pane.prototype.$headerFlipViewBtn = null;

    /**
     * close button of the pane
     * @readonly
     * @type {JQuery}
     */
    Pane.prototype.$headerCloseBtn = null;

    /**
     * the wrapped DOM node that contains views
     * @readonly
     * @type {JQuery}
     */
    Pane.prototype.$content = null;

    /**
     * The list of files views
     * @type {Array.<File>}
     */
    Pane.prototype._viewList = [];

    /**
     * The list of files views in MRU order
     * @type {Array.<File>}
     */
    Pane.prototype._viewListMRUOrder = [];

    /**
     * The list of files views in Added order
     * @type {Array.<File>}
     */
    Pane.prototype._viewListAddedOrder = [];

    /**
     * Dictionary mapping fullpath to view
     * @type {Object.<!string, !View>}
     * @private
     */
    Pane.prototype._views = {};

    /**
     * The current view
     * @type {?View}
     * @private
     */
    Pane.prototype._currentView = null;

    /**
     * The last thing that received a focus event
     * @type {?DomElement}
     * @private
     */
    Pane.prototype._lastFocusedElement = null;

    /**
     * Initializes the Pane to its default state
     * @private
     */
    Pane.prototype._initialize = function () {
        this._viewList = [];
        this._viewListMRUOrder = [];
        this._viewListAddedOrder = [];
        this._views = {};
        this._currentView = null;
        this.showInterstitial(true);
    };

   /**
     * Creates a pane event namespaced to this pane
     * (pass an empty string to generate just the namespace key to pass to jQuery to turn off all events handled by this pane)
     * @private
     * @param {!string} name - the name of the event to namespace
     * @return {string} an event namespaced to this pane
     */
    Pane.prototype._makeEventName = function (name) {
        return name + ".pane-" + this.id;
    };

   /**
     * Reparents a view to this pane
     * @private
     * @param {!View} view - the view to reparent
     */
    Pane.prototype._reparent = function (view) {
        view.$el.appendTo(this.$content);
        this._views[view.getFile().fullPath] = view;
        if (view.notifyContainerChange) {
            view.notifyContainerChange();
        }
    };

    /**
     * Hides the current view if there is one, shows the
     *  interstitial screen and notifies that the view changed
     */
    Pane.prototype._hideCurrentView = function () {
        if (this._currentView) {
            var currentView = this._currentView;
            this._setViewVisibility(this._currentView, false);
            this.showInterstitial(true);
            this._currentView = null;
            this._notifyCurrentViewChange(null, currentView);
        }
    };

    /**
     * moves a view from one pane to another
     * @param {!File} file - the File to move
     * @param {Pane} destinationPane - the destination pane
     * @param {Number} destinationIndex - the working set index of the file in the destination pane
     * @return {jQuery.Promise} a promise object which resolves after the view has been moved and its
     * replacement document has been opened
     * @private
     */
    Pane.prototype.moveView = function (file, destinationPane, destinationIndex) {
        var self = this,
            openNextPromise = new $.Deferred(),
            result = new $.Deferred();

        // if we're moving the currently viewed file we
        //  need to open another file so wait for that operation
        //  to finish before we move the view
        if ((this.getCurrentlyViewedPath() === file.fullPath)) {
            var nextFile = this.traverseViewListByMRU(1, file.fullPath);
            if (nextFile) {
                this._execOpenFile(nextFile.fullPath)
                    .fail(function () {
                        // the FILE_OPEN failed
                        self._hideCurrentView();
                    })
                    .always(function () {
                        openNextPromise.resolve();
                    });
            } else {
                this._hideCurrentView();
                openNextPromise.resolve();
            }
        } else {
            openNextPromise.resolve();
        }

        // Once the next file has opened, we can
        //  move the item in the working set and
        //  open it in the destination pane
        openNextPromise.done(function () {
            var viewListIndex = self.findInViewList(file.fullPath);
            var shouldAddView = viewListIndex !== -1;
            var view = self._views[file.fullPath];

            // If the file isn't in working set, destroy the view and delete it from
            // source pane's view map and return as solved
            if (!shouldAddView) {
                if (view) {
                    self._doDestroyView(view);
                }
                return result.resolve();
            }

            // Remove file from all 3 view lists
            self._viewList.splice(viewListIndex, 1);
            self._viewListMRUOrder.splice(self.findInViewListMRUOrder(file.fullPath), 1);
            self._viewListAddedOrder.splice(self.findInViewListAddedOrder(file.fullPath), 1);

            // insert the view into the working set
            destinationPane._addToViewList(file,  _makeIndexRequestObject(true, destinationIndex));

            // if we had a view, it had previously been opened
            // otherwise, the file was in the working set unopened
            if (view) {
                // delete it from the source pane's view map and add it to the destination pane's view map
                delete self._views[file.fullPath];
                destinationPane.addView(view, !destinationPane.getCurrentlyViewedFile());

                // we're done
                result.resolve();
            } else if (!destinationPane.getCurrentlyViewedFile()) {
                // The view has not have been created and the pane was
                //  not showing anything so open the file moved in to the pane
                destinationPane._execOpenFile(file.fullPath).always(function () {
                    // wait until the file has been opened before
                    //  we resolve the promise so the working set
                    //  view can sync appropriately
                    result.resolve();
                });
            } else {
                // nothing to do, we're done
                result.resolve();
            }
        });
        return result.promise();
    };

    /**
     * Merges the another Pane object's contents into this Pane
     * @param {!Pane} Other - Pane from which to copy
     */
    Pane.prototype.mergeFrom = function (other) {
        // save this because we're setting it to null and we
        //  may need to destroy it if it's a temporary view
        var otherCurrentView = other._currentView;

        // Hide the current view while we
        //  merge the 2 panes together
        other._hideCurrentView();

        // Copy the File lists
        this._viewList = _.union(this._viewList, other._viewList);
        this._viewListMRUOrder = _.union(this._viewListMRUOrder, other._viewListMRUOrder);
        this._viewListAddedOrder = _.union(this._viewListAddedOrder, other._viewListAddedOrder);

        var self = this,
            viewsToDestroy = [];

        // Copy the views
        _.forEach(other._views, function (view) {
            var file = view.getFile(),
                fullPath = file && file.fullPath;
            if (fullPath && other.findInViewList(fullPath) !== -1) {
                // switch the container to this Pane
                self._reparent(view);
            } else {
                // We don't copy temporary views so destroy them
                viewsToDestroy.push(view);
            }
        });

        // 1-off views
        if (otherCurrentView && !other._isViewNeeded(otherCurrentView) && viewsToDestroy.indexOf(otherCurrentView) === -1) {
            viewsToDestroy.push(otherCurrentView);
        }

        // Destroy temporary views
        _.forEach(viewsToDestroy, function (view) {
            self.trigger("viewDestroy", view);
            view.destroy();
        });

        // this _reset all internal data structures
        //  and will set the current view to null
        other._initialize();
    };

    /**
     * Removes the DOM node for the Pane, removes all
     *  event handlers and _resets all internal data structures
     */
    Pane.prototype.destroy = function () {
        if (this._currentView ||
                Object.keys(this._views).length > 0 ||
                this._viewList.length > 0) {
            console.warn("destroying a pane that isn't empty");
        }

        this._reset();

        DocumentManager.off(this._makeEventName(""));
        MainViewManager.off(this._makeEventName(""));

        this.$el.off(".pane");
        this.$el.remove();
    };

   /**
     * Returns a copy of the view file list
     * @return {!Array.<File>}
     */
    Pane.prototype.getViewList = function () {
        return _.clone(this._viewList);
    };

    /**
     * Returns the number of entries in the view file list
     * @return {number}
     */
    Pane.prototype.getViewListSize = function () {
        return this._viewList.length;
    };

    /**
     * Returns the index of the item in the view file list
     * @param {!string} fullPath the full path of the item to look for
     * @return {number} index of the item or -1 if not found
     */
    Pane.prototype.findInViewList = function (fullPath) {
        return _.findIndex(this._viewList, function (file) {
            return file.fullPath === fullPath;
        });
    };

    /**
     * Returns the order in which the item was added
     * @param {!string} fullPath the full path of the item to look for
     * @return {number} order of the item or -1 if not found
     */
    Pane.prototype.findInViewListAddedOrder = function (fullPath) {
        return _.findIndex(this._viewListAddedOrder, function (file) {
            return file.fullPath === fullPath;
        });
    };

   /**
     * Returns the order in which the item was last used
     * @param {!string} fullPath the full path of the item to look for
     * @return {number} order of the item or -1 if not found.
     *      0 indicates most recently used, followed by 1 and so on...
     */
    Pane.prototype.findInViewListMRUOrder = function (fullPath) {
        return _.findIndex(this._viewListMRUOrder, function (file) {
            return file.fullPath === fullPath;
        });
    };

    /**
     * Return value from reorderItem when the Item was not found
     * @see {@link Pane#reorderItem}
     * @const
     */
    Pane.prototype.ITEM_NOT_FOUND = -1;

    /**
     * Return value from reorderItem when the Item was found at its natural index
     * and the workingset does not need to be resorted
     * @see {@link Pane#reorderItem}
     * @const
     */
    Pane.prototype.ITEM_FOUND_NO_SORT = 0;

    /**
     * Return value from reorderItem when the Item was found and reindexed
     * and the workingset needs to be resorted
     * @see {@link Pane#reorderItem}
     * @const
     */
    Pane.prototype.ITEM_FOUND_NEEDS_SORT = 1;

    /**
     * reorders the specified file in the view list to the desired position
     *
     * @param {File} file - the file object of the item to reorder
     * @param {number=} index - the new position of the item
     * @param {boolean=} force - true to force the item into that position, false otherwise.  (Requires an index be requested)
     * @return {number} this function returns one of the following manifest constants:
     *            ITEM_NOT_FOUND        : The request file object was not found
     *            ITEM_FOUND_NO_SORT    : The request file object was found but it was already at the requested index
     *            ITEM_FOUND_NEEDS_SORT : The request file object was found and moved to a new index and the list should be resorted
     */
    Pane.prototype.reorderItem = function (file, index, force) {
        var indexRequested = (index !== undefined && index !== null && index >= 0),
            curIndex = this.findInViewList(file.fullPath);

        if (curIndex !== -1) {
            // File is in view list, but not at the specifically requested index - only need to reorder
            if (force || (indexRequested && curIndex !== index)) {
                var entry = this._viewList.splice(curIndex, 1)[0];
                this._viewList.splice(index, 0, entry);
                return this.ITEM_FOUND_NEEDS_SORT;
            }
            return this.ITEM_FOUND_NO_SORT;
        }

        return this.ITEM_NOT_FOUND;
    };

    /**
     * Determines if a file can be added to our file list
     * @private
     * @param {!File} file - file object to test
     * @return {boolean} true if it can be added, false if not
     */
    Pane.prototype._canAddFile = function (file) {
        return ((this._views.hasOwnProperty(file.fullPath) && this.findInViewList(file.fullPath) === -1) ||
                    (MainViewManager._getPaneIdForPath(file.fullPath) !== this.id));
    };

    /**
     * Adds the given file to the end of the workingset, if it is not already in the list
     * @private
     * @param {!File} file
     * @param {Object=} inPlace record with inPlace add data (index, indexRequested). Used internally
     */
    Pane.prototype._addToViewList = function (file, inPlace) {
        if (inPlace && inPlace.indexRequested) {
            // If specified, insert into the workingset at this 0-based index
            this._viewList.splice(inPlace.index, 0, file);
        } else {
            // If no index is specified, just add the file to the end of the workingset.
            this._viewList.push(file);
        }

        // Add to MRU order: either first or last, depending on whether it's already the current doc or not
        var currentPath = this.getCurrentlyViewedPath();
        if (currentPath && currentPath === file.fullPath) {
            this._viewListMRUOrder.unshift(file);
        } else {
            this._viewListMRUOrder.push(file);
        }

        // Add first to Added order
        this._viewListAddedOrder.unshift(file);
    };


    /**
     * Adds the given file to the end of the workingset, if it is not already in the list
     * Does not change which document is currently open in the editor. Completes synchronously.
     * @param {!File} file - file to add
     * @param {number=} index - position where to add the item
     * @return {number} index of where the item was added
     */
    Pane.prototype.addToViewList = function (file, index) {
        var indexRequested = (index !== undefined && index !== null && index >= 0 && index < this._viewList.length);
        this._addToViewList(file, _makeIndexRequestObject(indexRequested, index));

        if (!indexRequested) {
            index = this._viewList.length - 1;
        }

        return index;
    };


    /**
     * Adds the given file list to the end of the workingset.
     * @param {!Array.<File>} fileList
     * @return {!Array.<File>} list of files added to the list
     */
    Pane.prototype.addListToViewList = function (fileList) {
        var self = this,
            uniqueFileList = [];

        // Process only files not already in view list
        fileList.forEach(function (file) {
            if (self._canAddFile(file)) {
                self._addToViewList(file);
                uniqueFileList.push(file);
            }
        });

        return uniqueFileList;
    };

    /**
     * Dispatches a currentViewChange event
     * @param {?View} newView - the view become the current view
     * @param {?View} oldView - the view being replaced
     */
    Pane.prototype._notifyCurrentViewChange = function (newView, oldView) {
        this.updateHeaderText();

        this.trigger("currentViewChange", newView, oldView);
    };


    /**
     * Destroys a view and removes it from the view map. If it is the current view then the view
     * is first hidden and the interstitial page is displayed
     * @private
     * @param {!View} view - view to destroy
     */
    Pane.prototype._doDestroyView = function (view) {
        if (this._currentView === view) {
            // if we're removing the current
            //  view then we need to hide the view
            this._hideCurrentView();
        }
        delete this._views[view.getFile().fullPath];
        this.trigger("viewDestroy", view);
        view.destroy();
    };

    /**
     * Removes the specifed file from all internal lists, destroys the view of the file (if there is one)
     *  and shows the interstitial page if the current view is destroyed
     * @private
     * @param {!File} file - file to remove
     * @param {boolean} preventViewChange - false to hide the current view if removing the current view, true
     *                                      to prevent the current view from changing.
     *
     * When passing true for preventViewChange, it is assumed that the caller will perform an OPEN_FILE op
     * to show the next file in line to view.  Since the file was removed from the workingset in _doRemove
     * its view is now considered to be a temporary view and the call to showView for the OPEN_FILE op
     * will destroy the view. the caller needs to handle the reject case in the event of failure
     *
     * @return {boolean} true if removed, false if the file was not found either in a list or view
     */
    Pane.prototype._doRemove = function (file, preventViewChange) {

        // If it's in the view list then we need to remove it
        var index = this.findInViewList(file.fullPath);

        if (index > -1) {
            // Remove it from all 3 view lists
            this._viewList.splice(index, 1);
            this._viewListMRUOrder.splice(this.findInViewListMRUOrder(file.fullPath), 1);
            this._viewListAddedOrder.splice(this.findInViewListAddedOrder(file.fullPath), 1);
        }

        // Destroy the view
        var view = this._views[file.fullPath];

        if (view) {
            if (!preventViewChange) {
                this._doDestroyView(view);
            }
        }

        return ((index > -1) || Boolean(view));
    };

    /**
     * Moves the specified file to the front of the MRU list
     * @param {!File} file
     */
    Pane.prototype.makeViewMostRecent = function (file) {
        var index = this.findInViewListMRUOrder(file.fullPath);
        if (index !== -1) {
            this._viewListMRUOrder.splice(index, 1);
            this._viewListMRUOrder.unshift(file);
        }
    };

    /**
     * Sorts items in the pane's view list
     * @param {function(paneId:!string, left:!string, right:!string):number} compareFn - the function used to compare items in the viewList
     */

    /**
     * invokes Array.sort method on the internal view list.
     * @param {sortFunctionCallback} compareFn - the function to call to determine if the
     */
    Pane.prototype.sortViewList = function (compareFn) {
        this._viewList.sort(_.partial(compareFn, this.id));
    };

    /**
     * moves a working set item from one index to another shifting the items
     * after in the working set up and reinserting it at the desired location
     * @param {!number} fromIndex - the index of the item to move
     * @param {!number} toIndex - the index to move to
     * @private
     */
    Pane.prototype.moveWorkingSetItem = function (fromIndex, toIndex) {
        this._viewList.splice(toIndex, 0, this._viewList.splice(fromIndex, 1)[0]);
    };

    /**
     * Swaps two items in the file view list (used while dragging items in the working set view)
     * @param {number} index1 - the index of the first item to swap
     * @param {number} index2 - the index of the second item to swap
     * @return {boolean}} true
     */
    Pane.prototype.swapViewListIndexes = function (index1, index2) {
        var temp = this._viewList[index1];
        this._viewList[index1] = this._viewList[index2];
        this._viewList[index2] = temp;
        return true;
    };

    /**
     * Traverses the list and returns the File object of the next item in the MRU order
     * @param {!number} direction - Must be 1 or -1 to traverse forward or backward
     * @param {string=} current - the fullPath of the item where traversal is to start.
     *                              If this parameter is omitted then the path of the current view is used.
     *                              If the current view is a temporary view then the first item in the MRU list is returned
     * @return {?File}  The File object of the next item in the travesal order or null if there isn't one.
     */
    Pane.prototype.traverseViewListByMRU = function (direction, current) {
        if (!current && this._currentView) {
            var file = this._currentView.getFile();
            current = file && file.fullPath;
        }

        var index = current ? this.findInViewListMRUOrder(current) : -1;
        return ViewUtils.traverseViewArray(this._viewListMRUOrder, index, direction);
    };

    /**
     * Updates flipview icon in pane header
     * @private
     */
    Pane.prototype.updateFlipViewIcon = function () {
        var paneID = this.id,
            directionIndex = 0,
            ICON_CLASSES = ["flipview-icon-none", "flipview-icon-top", "flipview-icon-right", "flipview-icon-bottom", "flipview-icon-left"],
            DIRECTION_STRINGS = ["", Strings.TOP, Strings.RIGHT, Strings.BOTTOM, Strings.LEFT],
            layoutScheme = MainViewManager.getLayoutScheme(),
            hasFile = this.getCurrentlyViewedFile();

        if (layoutScheme.columns > 1 && hasFile) {
            directionIndex = paneID === FIRST_PANE ? 2 : 4;
        } else if (layoutScheme.rows > 1 && hasFile) {
            directionIndex = paneID === FIRST_PANE ? 3 : 1;
        }

        this.$headerFlipViewBtn.removeClass(ICON_CLASSES.join(" "))
                      .addClass(ICON_CLASSES[directionIndex]);

        this.$headerFlipViewBtn.attr("title", StringUtils.format(Strings.FLIPVIEW_BTN_TOOLTIP,  DIRECTION_STRINGS[directionIndex].toLowerCase()));
    };

    /**
     * Updates text in pane header
     * @private
     */
    Pane.prototype.updateHeaderText = function () {
        var file = this.getCurrentlyViewedFile(),
            files,
            displayName;

        if (file) {
            files = MainViewManager.getAllOpenFiles().filter(function (item) {
                return (item.name === file.name);
            });
            if (files.length < 2) {
                this.$headerText.text(file.name);
            } else {
                displayName = ProjectManager.makeProjectRelativeIfPossible(file.fullPath);
                this.$headerText.text(displayName);
            }
        } else {
            this.$headerText.html(Strings.EMPTY_VIEW_HEADER);
        }

        this.updateFlipViewIcon();
    };

    /**
     * Event handler when a file changes name
     * @private
     * @param {!JQuery.Event} e - jQuery event object
     * @param {!string} oldname - path of the file that was renamed
     * @param {!string} newname - the new path to the file
     */
    Pane.prototype._handleFileNameChange = function (e, oldname, newname) {
        // Check to see if we need to dispatch a viewListChange event
        // The list contains references to file objects and, for a rename event,
        // the File object's name has changed by the time we've gotten the event.
        // So, we need to look for the file by its new name to determine if
        // if we need to dispatch the event which may look funny
        var dispatchEvent = (this.findInViewList(newname) >= 0);

        // rename the view
        if (this._views.hasOwnProperty(oldname)) {
            var view = this._views[oldname];

            this._views[newname] = view;
            delete this._views[oldname];
        }

        this.updateHeaderText();

        // dispatch the change event
        if (dispatchEvent) {
            this.trigger("viewListChange");
        }
    };

    /**
     * Event handler when a file is deleted
     * @private
     * @param {!JQuery.Event} e - jQuery event object
     * @param {!string} fullPath - path of the file that was deleted
     */
    Pane.prototype._handleFileDeleted = function (e, fullPath) {
        if (this.removeView({fullPath: fullPath})) {
            this.trigger("viewListChange");
        }
    };

    /**
     * Shows the pane's interstitial page
     * @param {boolean} show - show or hide the interstitial page
     */
    Pane.prototype.showInterstitial = function (show) {
        if (this.$content) {
            this.$content.find(".not-editor").css("display", (show) ? "" : "none");
        }
    };

    /**
     * retrieves the view object for the given path
     * @param {!string}  path - the fullPath of the view to retrieve
     * @return {boolean} show - show or hide the interstitial page
     */
    Pane.prototype.getViewForPath = function (path) {
        return this._views[path];
    };

    /**
     * Adds a view to the pane
     * @param {!View} view - the View object to add
     * @param {boolean} show - true to show the view right away, false otherwise
     */
    Pane.prototype.addView = function (view, show) {
        var file = view.getFile(),
            path = file && file.fullPath;

        if (!path) {
            console.error("cannot add a view that does not have a fullPath");
            return;
        }

        if (view.$el.parent() !== this.$content) {
            this._reparent(view);
        } else {
            this._views[path] = view;
        }

        // Ensure that we don't endup marking the custom views
        if (view.markPaneId) {
            view.markPaneId(this.id);
        }

        if (show) {
            this.showView(view);
        }
    };

    /**
     * Shows or hides a view
     * @param {!View} view - the to show or hide
     * @param {boolean} visible - true to show the view, false to hide it
     * @private
     */
    Pane.prototype._setViewVisibility = function (view, visible) {
        view.$el.css("display", (visible ? "" : "none"));
        if (view.notifyVisibilityChange) {
            view.notifyVisibilityChange(visible);
        }
    };

    /**
     * Swaps the current view with the requested view.
     * If the interstitial page is shown, it is hidden.
     * If the currentView is a temporary view, it is destroyed.
     * @param {!View} view - the to show
     */
    Pane.prototype.showView = function (view) {
        if (this._currentView && this._currentView === view) {
            this._setViewVisibility(this._currentView, true);
            this.updateLayout(true);
            return;
        }

        var file = view.getFile(),
            newPath = file && file.fullPath,
            oldView = this._currentView;

        if (this._currentView) {
            if (this._currentView.getFile()) {
                ViewStateManager.updateViewState(this._currentView);
            }
            this._setViewVisibility(this._currentView, false);
        } else {
            this.showInterstitial(false);
        }

        this._currentView = view;
        this._setViewVisibility(this._currentView, true);
        this.updateLayout();

        this._notifyCurrentViewChange(view, oldView);

        if (oldView) {
            this.destroyViewIfNotNeeded(oldView);
        }

        if (!this._views.hasOwnProperty(newPath)) {
            console.error(newPath + " found in pane working set but pane.addView() has not been called for the view created for it");
        }
    };

    /**
     * Update header and content height
     */
    Pane.prototype._updateHeaderHeight = function () {
        var paneContentHeight = this.$el.height();

        // Adjust pane content height for header
        if (MainViewManager.getPaneCount() > 1) {
            this.$header.show();
            paneContentHeight -= this.$header.outerHeight();
        } else {
            this.$header.hide();
        }

        this.$content.height(paneContentHeight);
    };

    /**
     * Sets pane content height. Updates the layout causing the current view to redraw itself
     * @param {boolean} forceRefresh - true to force a resize and refresh of the current view,
     * false if just to resize forceRefresh is only used by Editor views to force a relayout
     * of all editor DOM elements. Custom View implementations should just ignore this flag.
     */
    Pane.prototype.updateLayout = function (forceRefresh) {
        this._updateHeaderHeight();
        if (this._currentView) {
            this._currentView.updateLayout(forceRefresh);
        }
    };

    /**
     * Determines if the view can be disposed of
     * @private
     * @param {!View} view - the View object to test
     * @return {boolean}} true if the view can be disposed, false if not
     */
    Pane.prototype._isViewNeeded = function (view) {
        var path = view.getFile().fullPath,
            currentPath = this.getCurrentlyViewedPath();

        return ((this._currentView && currentPath === path) || (this.findInViewList(path) !== -1));
    };


    /**
     * Retrieves the File object of the current view
     * @return {?File} the File object of the current view or null if there isn't one
     */
    Pane.prototype.getCurrentlyViewedFile = function () {
        return this._currentView ? this._currentView.getFile() : null;
    };

    /**
     * Retrieves the path of the current view
     * @return {?string} the path of the current view or null if there isn't one
     */
    Pane.prototype.getCurrentlyViewedPath = function () {
        var file = this.getCurrentlyViewedFile();
        return file ? file.fullPath : null;
    };

    /**
     * destroys the view if it isn't needed
     * @param {View} view - the view to destroy
     */
    Pane.prototype.destroyViewIfNotNeeded = function (view) {
        if (!this._isViewNeeded(view)) {
            var file = view.getFile(),
                path = file && file.fullPath;
            delete this._views[path];
            this.trigger("viewDestroy", view);
            view.destroy();
        }
    };

    /**
     * _resets the pane to an empty state
     * @private
     */
    Pane.prototype._reset = function () {
        var self = this,
            views = [],
            view = this._currentView;

        _.forEach(this._views, function (_view) {
            views.push(_view);
        });

        // If the current view is a temporary view,
        //  add it to the destroy list to dispose of
        if (this._currentView && views.indexOf(this._currentView) === -1) {
            views.push(this._currentView);
        }

        // This will reinitialize the object back to
        //  the default state
        this._initialize();

        if (view) {
            this._notifyCurrentViewChange(null, view);
        }

        // Now destroy the views
        views.forEach(function (_view) {
            self.trigger("viewDestroy", _view);
            _view.destroy();
        });
    };

    /**
     * Executes a FILE_OPEN command to open a file
     * @param  {!string} fullPath - path of the file to open
     * @return {jQuery.promise} promise that will resolve when the file is opened
     */
    Pane.prototype._execOpenFile = function (fullPath) {
        return CommandManager.execute(Commands.CMD_ADD_TO_WORKINGSET_AND_OPEN, { fullPath: fullPath, paneId: this.id, options: {noPaneActivate: true}});
    };

    /**
     * Removes the view and opens the next view
     * @param {File} file - the file to close
     * @param {boolean} suppressOpenNextFile - suppresses opening the next file in MRU order
     * @param {boolean} preventViewChange - if suppressOpenNextFile is truthy, this flag can be used to
     *                                      prevent the current view from being destroyed.
     *                                      Ignored if suppressOpenNextFile is falsy
     * @return {boolean} true if the file was removed from the working set
     *  This function will remove a temporary view of a file but will return false in that case
     */
    Pane.prototype.removeView = function (file, suppressOpenNextFile, preventViewChange) {
        var nextFile = !suppressOpenNextFile && this.traverseViewListByMRU(1, file.fullPath);
        if (nextFile && nextFile.fullPath !== file.fullPath && this.getCurrentlyViewedPath() === file.fullPath) {
            var self = this,
                fullPath = nextFile.fullPath,
                needOpenNextFile = this.findInViewList(fullPath) !== -1;

            if (this._doRemove(file, needOpenNextFile)) {
                if (needOpenNextFile) {
                    // this will destroy the current view
                    this._execOpenFile(fullPath)
                        .fail(function () {
                            // the FILE_OPEN op failed so destroy the current view
                            self._doDestroyView(self._currentView);
                        });
                }
                return true;
            } else {
                // Nothing was removed so don't try to remove it again
                return false;
            }
        } else {
            return this._doRemove(file, preventViewChange);
        }
    };

    /**
     * Removes the specifed file from all internal lists, destroys the view of the file (if there is one)
     *  and shows the interstitial page if the current view is destroyed.
     * @param {!Array.<File>}  list - Array of files to remove
     * @return {!Array.<File>} Array of File objects removed from the working set.
     *  This function will remove temporary views but the file objects for those views will not be found
     *  in the result set.  Only the file objects removed from the working set are returned.
     */
    Pane.prototype.removeViews = function (list) {
        var self = this,
            needsDestroyCurrentView = false,
            result;

        // Check to see if we need to destroy the current view later
        needsDestroyCurrentView = _.findIndex(list, function (file) {
            return file.fullPath === self.getCurrentlyViewedPath();
        }) !== -1;

        // destroy the views in the list
        result = list.filter(function (file) {
            return (self.removeView(file, true, true));
        });

        // we may have been passed a list of files that did not include the current view
        if (needsDestroyCurrentView) {
            // _doRemove will have whittled the MRU list down to just the remaining views
            var nextFile = this.traverseViewListByMRU(1, this.getCurrentlyViewedPath()),
                fullPath = nextFile && nextFile.fullPath,
                needOpenNextFile = fullPath && (this.findInViewList(fullPath) !== -1);

            if (needOpenNextFile) {
                // A successful open will destroy the current view
                this._execOpenFile(fullPath)
                    .fail(function () {
                        // the FILE_OPEN op failed so destroy the current view
                        self._doDestroyView(self._currentView);
                    });
            } else {
                // Nothing left to show so destroy the current view
                this._doDestroyView(this._currentView);
            }
        }

        // return the result
        return result;
    };

    /**
     * Gives focus to the last thing that had focus, the current view or the pane in that order
     */
    Pane.prototype.focus = function () {
        var current = window.document.activeElement,
            self = this;

        // Helper to focus the current view if it can
        function tryFocusingCurrentView() {
            if (self._currentView) {
                if (self._currentView.focus) {
                    //  Views can implement a focus
                    //  method for focusing a complex
                    //  DOM like codemirror
                    self._currentView.focus();
                } else {
                    //  Otherwise, no focus method
                    //  just try and give the DOM
                    //  element focus
                    self._currentView.$el.focus();
                }
            } else {
                // no view so just focus the pane
                self.$el.focus();
            }
        }

        // short-circuit for performance
        if (this._lastFocusedElement === current) {
            return;
        }

        // If the focus was in a <textarea> (assumed to be CodeMirror) and currentView is
        // anything other than an Editor, blur the textarea explicitly, in case the new
        // _currentView's $el isn't focusable. E.g.:
        //  1. Open a js file in the left pane and an image in the right pane and
        //  2. Focus the js file using the working-set
        //  3. Focus the image view using the working-set.
        //  ==> Focus is still in the text area. Any keyboard input will modify the document
        if (current.tagName.toLowerCase() === "textarea" &&
                (!this._currentView || !this._currentView._codeMirror)) {
            current.blur();
        }

        var $lfe = $(this._lastFocusedElement);

        if ($lfe.length && !$lfe.is(".view-pane") && $lfe.is(":visible")) {
            // if we had a last focused element
            //  and it wasn't a pane element
            //  and it's still visible, focus it
            $lfe.focus();
        } else {
            // otherwise, just try to give focus
            //  to the currently active view
            tryFocusingCurrentView();
        }
    };

    /**
     * MainViewManager.activePaneChange handler
     * @param {jQuery.event} e - event data
     * @param {!string} activePaneId - the new active pane id
     */
    Pane.prototype._handleActivePaneChange = function (e, activePaneId) {
        this.$el.toggleClass("active-pane", Boolean(activePaneId === this.id));
    };



    /**
     * serializes the pane state from JSON
     * @param {!Object} state - the state to load
     * @return {jQuery.Promise} A promise which resolves to
     *              {fullPath:string, paneId:string}
     *              which can be passed as command data to FILE_OPEN
     */
    Pane.prototype.loadState = function (state) {
        var filesToAdd = [],
            viewStates = {},
            activeFile,
            data,
            self = this;

        var getInitialViewFilePath = function () {
            return (self._viewList.length > 0) ? self._viewList[0].fullPath : null;
        };

        _.forEach(state, function (entry) {
            filesToAdd.push(FileSystem.getFileForPath(entry.file));
            if (entry.active) {
                activeFile = entry.file;
            }
            if (entry.viewState) {
                viewStates[entry.file] = entry.viewState;
            }
        });

        this.addListToViewList(filesToAdd);

        ViewStateManager.addViewStates(viewStates);

        activeFile = activeFile || getInitialViewFilePath();

        if (activeFile) {
            data = {paneId: self.id, fullPath: activeFile};
        }

        return new $.Deferred().resolve(data);
    };

    /**
     * Returns the JSON-ified state of the object so it can be serialize
     * @return {!Object} state - the state to save
     */
    Pane.prototype.saveState = function () {
        var result = [],
            currentlyViewedPath = this.getCurrentlyViewedPath();

        // Save the current view state first
        if (this._currentView && this._currentView.getFile()) {
            // We save the view state of the current view before
            //  hiding the view and showing to a different file
            // But the current view's view state may not be
            //  up to date in the view state cache so update it
            //  before we save so we don't JSON-ify stale data.
            ViewStateManager.updateViewState(this._currentView);
        }

        // walk the list of views and save
        this._viewList.forEach(function (file) {
            // Do not persist untitled document paths
            if (!(file instanceof InMemoryFile)) {
                result.push({
                    file: file.fullPath,
                    active: (file.fullPath === currentlyViewedPath),
                    viewState:  ViewStateManager.getViewState(file)
                });
            }
        });

        return result;
    };

    /**
     * gets the current view's scroll state data
     * @return {Object=} scroll state - the current scroll state
     */
    Pane.prototype.getScrollState = function () {
        if (this._currentView && this._currentView.getScrollPos) {
            return {scrollPos: this._currentView.getScrollPos()};
        }
    };

    /**
     * tells the current view to restore its scroll state from cached data and apply a height delta
     * @param {Object=} state - the current scroll state
     * @param {number=} heightDelta - the amount to add or subtract from the state
     */
    Pane.prototype.restoreAndAdjustScrollState = function (state, heightDelta) {
        if (this._currentView && state && state.scrollPos && this._currentView.adjustScrollPos) {
            this._currentView.adjustScrollPos(state.scrollPos, heightDelta);
        }
    };

    exports.Pane = Pane;
});