/ src / pane.js
pane.js
   1  const Grim = require('grim');
   2  const { CompositeDisposable, Emitter } = require('event-kit');
   3  const PaneAxis = require('./pane-axis');
   4  const TextEditor = require('./text-editor');
   5  const PaneElement = require('./pane-element');
   6  
   7  let nextInstanceId = 1;
   8  
   9  class SaveCancelledError extends Error {}
  10  
  11  // Extended: A container for presenting content in the center of the workspace.
  12  // Panes can contain multiple items, one of which is *active* at a given time.
  13  // The view corresponding to the active item is displayed in the interface. In
  14  // the default configuration, tabs are also displayed for each item.
  15  //
  16  // Each pane may also contain one *pending* item. When a pending item is added
  17  // to a pane, it will replace the currently pending item, if any, instead of
  18  // simply being added. In the default configuration, the text in the tab for
  19  // pending items is shown in italics.
  20  module.exports = class Pane {
  21    inspect() {
  22      return `Pane ${this.id}`;
  23    }
  24  
  25    static deserialize(
  26      state,
  27      { deserializers, applicationDelegate, config, notifications, views }
  28    ) {
  29      const { activeItemIndex } = state;
  30      const activeItemURI = state.activeItemURI || state.activeItemUri;
  31  
  32      const items = [];
  33      for (const itemState of state.items) {
  34        const item = deserializers.deserialize(itemState);
  35        if (item) items.push(item);
  36      }
  37      state.items = items;
  38  
  39      state.activeItem = items[activeItemIndex];
  40      if (!state.activeItem && activeItemURI) {
  41        state.activeItem = state.items.find(
  42          item =>
  43            typeof item.getURI === 'function' && item.getURI() === activeItemURI
  44        );
  45      }
  46  
  47      return new Pane(
  48        Object.assign(
  49          {
  50            deserializerManager: deserializers,
  51            notificationManager: notifications,
  52            viewRegistry: views,
  53            config,
  54            applicationDelegate
  55          },
  56          state
  57        )
  58      );
  59    }
  60  
  61    constructor(params = {}) {
  62      this.setPendingItem = this.setPendingItem.bind(this);
  63      this.getPendingItem = this.getPendingItem.bind(this);
  64      this.clearPendingItem = this.clearPendingItem.bind(this);
  65      this.onItemDidTerminatePendingState = this.onItemDidTerminatePendingState.bind(
  66        this
  67      );
  68      this.saveItem = this.saveItem.bind(this);
  69      this.saveItemAs = this.saveItemAs.bind(this);
  70  
  71      this.id = params.id;
  72      if (this.id != null) {
  73        nextInstanceId = Math.max(nextInstanceId, this.id + 1);
  74      } else {
  75        this.id = nextInstanceId++;
  76      }
  77  
  78      this.activeItem = params.activeItem;
  79      this.focused = params.focused != null ? params.focused : false;
  80      this.applicationDelegate = params.applicationDelegate;
  81      this.notificationManager = params.notificationManager;
  82      this.config = params.config;
  83      this.deserializerManager = params.deserializerManager;
  84      this.viewRegistry = params.viewRegistry;
  85  
  86      this.emitter = new Emitter();
  87      this.alive = true;
  88      this.subscriptionsPerItem = new WeakMap();
  89      this.items = [];
  90      this.itemStack = [];
  91      this.container = null;
  92  
  93      this.addItems((params.items || []).filter(item => item));
  94      if (!this.getActiveItem()) this.setActiveItem(this.items[0]);
  95      this.addItemsToStack(params.itemStackIndices || []);
  96      this.setFlexScale(params.flexScale || 1);
  97    }
  98  
  99    getElement() {
 100      if (!this.element) {
 101        this.element = new PaneElement().initialize(this, {
 102          views: this.viewRegistry,
 103          applicationDelegate: this.applicationDelegate
 104        });
 105      }
 106      return this.element;
 107    }
 108  
 109    serialize() {
 110      const itemsToBeSerialized = this.items.filter(
 111        item => item && typeof item.serialize === 'function'
 112      );
 113  
 114      const itemStackIndices = [];
 115      for (const item of this.itemStack) {
 116        if (typeof item.serialize === 'function') {
 117          itemStackIndices.push(itemsToBeSerialized.indexOf(item));
 118        }
 119      }
 120  
 121      const activeItemIndex = itemsToBeSerialized.indexOf(this.activeItem);
 122  
 123      return {
 124        deserializer: 'Pane',
 125        id: this.id,
 126        items: itemsToBeSerialized.map(item => item.serialize()),
 127        itemStackIndices,
 128        activeItemIndex,
 129        focused: this.focused,
 130        flexScale: this.flexScale
 131      };
 132    }
 133  
 134    getParent() {
 135      return this.parent;
 136    }
 137  
 138    setParent(parent) {
 139      this.parent = parent;
 140    }
 141  
 142    getContainer() {
 143      return this.container;
 144    }
 145  
 146    setContainer(container) {
 147      if (container && container !== this.container) {
 148        this.container = container;
 149        container.didAddPane({ pane: this });
 150      }
 151    }
 152  
 153    // Private: Determine whether the given item is allowed to exist in this pane.
 154    //
 155    // * `item` the Item
 156    //
 157    // Returns a {Boolean}.
 158    isItemAllowed(item) {
 159      if (typeof item.getAllowedLocations !== 'function') {
 160        return true;
 161      } else {
 162        return item
 163          .getAllowedLocations()
 164          .includes(this.getContainer().getLocation());
 165      }
 166    }
 167  
 168    setFlexScale(flexScale) {
 169      this.flexScale = flexScale;
 170      this.emitter.emit('did-change-flex-scale', this.flexScale);
 171      return this.flexScale;
 172    }
 173  
 174    getFlexScale() {
 175      return this.flexScale;
 176    }
 177  
 178    increaseSize() {
 179      if (this.getContainer().getPanes().length > 1) {
 180        this.setFlexScale(this.getFlexScale() * 1.1);
 181      }
 182    }
 183  
 184    decreaseSize() {
 185      if (this.getContainer().getPanes().length > 1) {
 186        this.setFlexScale(this.getFlexScale() / 1.1);
 187      }
 188    }
 189  
 190    /*
 191    Section: Event Subscription
 192    */
 193  
 194    // Public: Invoke the given callback when the pane resizes
 195    //
 196    // The callback will be invoked when pane's flexScale property changes.
 197    // Use {::getFlexScale} to get the current value.
 198    //
 199    // * `callback` {Function} to be called when the pane is resized
 200    //   * `flexScale` {Number} representing the panes `flex-grow`; ability for a
 201    //     flex item to grow if necessary.
 202    //
 203    // Returns a {Disposable} on which '.dispose()' can be called to unsubscribe.
 204    onDidChangeFlexScale(callback) {
 205      return this.emitter.on('did-change-flex-scale', callback);
 206    }
 207  
 208    // Public: Invoke the given callback with the current and future values of
 209    // {::getFlexScale}.
 210    //
 211    // * `callback` {Function} to be called with the current and future values of
 212    //   the {::getFlexScale} property.
 213    //   * `flexScale` {Number} representing the panes `flex-grow`; ability for a
 214    //     flex item to grow if necessary.
 215    //
 216    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 217    observeFlexScale(callback) {
 218      callback(this.flexScale);
 219      return this.onDidChangeFlexScale(callback);
 220    }
 221  
 222    // Public: Invoke the given callback when the pane is activated.
 223    //
 224    // The given callback will be invoked whenever {::activate} is called on the
 225    // pane, even if it is already active at the time.
 226    //
 227    // * `callback` {Function} to be called when the pane is activated.
 228    //
 229    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 230    onDidActivate(callback) {
 231      return this.emitter.on('did-activate', callback);
 232    }
 233  
 234    // Public: Invoke the given callback before the pane is destroyed.
 235    //
 236    // * `callback` {Function} to be called before the pane is destroyed.
 237    //
 238    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 239    onWillDestroy(callback) {
 240      return this.emitter.on('will-destroy', callback);
 241    }
 242  
 243    // Public: Invoke the given callback when the pane is destroyed.
 244    //
 245    // * `callback` {Function} to be called when the pane is destroyed.
 246    //
 247    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 248    onDidDestroy(callback) {
 249      return this.emitter.once('did-destroy', callback);
 250    }
 251  
 252    // Public: Invoke the given callback when the value of the {::isActive}
 253    // property changes.
 254    //
 255    // * `callback` {Function} to be called when the value of the {::isActive}
 256    //   property changes.
 257    //   * `active` {Boolean} indicating whether the pane is active.
 258    //
 259    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 260    onDidChangeActive(callback) {
 261      return this.container.onDidChangeActivePane(activePane => {
 262        const isActive = this === activePane;
 263        callback(isActive);
 264      });
 265    }
 266  
 267    // Public: Invoke the given callback with the current and future values of the
 268    // {::isActive} property.
 269    //
 270    // * `callback` {Function} to be called with the current and future values of
 271    //   the {::isActive} property.
 272    //   * `active` {Boolean} indicating whether the pane is active.
 273    //
 274    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 275    observeActive(callback) {
 276      callback(this.isActive());
 277      return this.onDidChangeActive(callback);
 278    }
 279  
 280    // Public: Invoke the given callback when an item is added to the pane.
 281    //
 282    // * `callback` {Function} to be called with when items are added.
 283    //   * `event` {Object} with the following keys:
 284    //     * `item` The added pane item.
 285    //     * `index` {Number} indicating where the item is located.
 286    //
 287    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 288    onDidAddItem(callback) {
 289      return this.emitter.on('did-add-item', callback);
 290    }
 291  
 292    // Public: Invoke the given callback when an item is removed from the pane.
 293    //
 294    // * `callback` {Function} to be called with when items are removed.
 295    //   * `event` {Object} with the following keys:
 296    //     * `item` The removed pane item.
 297    //     * `index` {Number} indicating where the item was located.
 298    //
 299    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 300    onDidRemoveItem(callback) {
 301      return this.emitter.on('did-remove-item', callback);
 302    }
 303  
 304    // Public: Invoke the given callback before an item is removed from the pane.
 305    //
 306    // * `callback` {Function} to be called with when items are removed.
 307    //   * `event` {Object} with the following keys:
 308    //     * `item` The pane item to be removed.
 309    //     * `index` {Number} indicating where the item is located.
 310    onWillRemoveItem(callback) {
 311      return this.emitter.on('will-remove-item', callback);
 312    }
 313  
 314    // Public: Invoke the given callback when an item is moved within the pane.
 315    //
 316    // * `callback` {Function} to be called with when items are moved.
 317    //   * `event` {Object} with the following keys:
 318    //     * `item` The removed pane item.
 319    //     * `oldIndex` {Number} indicating where the item was located.
 320    //     * `newIndex` {Number} indicating where the item is now located.
 321    //
 322    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 323    onDidMoveItem(callback) {
 324      return this.emitter.on('did-move-item', callback);
 325    }
 326  
 327    // Public: Invoke the given callback with all current and future items.
 328    //
 329    // * `callback` {Function} to be called with current and future items.
 330    //   * `item` An item that is present in {::getItems} at the time of
 331    //     subscription or that is added at some later time.
 332    //
 333    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 334    observeItems(callback) {
 335      for (let item of this.getItems()) {
 336        callback(item);
 337      }
 338      return this.onDidAddItem(({ item }) => callback(item));
 339    }
 340  
 341    // Public: Invoke the given callback when the value of {::getActiveItem}
 342    // changes.
 343    //
 344    // * `callback` {Function} to be called with when the active item changes.
 345    //   * `activeItem` The current active item.
 346    //
 347    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 348    onDidChangeActiveItem(callback) {
 349      return this.emitter.on('did-change-active-item', callback);
 350    }
 351  
 352    // Public: Invoke the given callback when {::activateNextRecentlyUsedItem}
 353    // has been called, either initiating or continuing a forward MRU traversal of
 354    // pane items.
 355    //
 356    // * `callback` {Function} to be called with when the active item changes.
 357    //   * `nextRecentlyUsedItem` The next MRU item, now being set active
 358    //
 359    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 360    onChooseNextMRUItem(callback) {
 361      return this.emitter.on('choose-next-mru-item', callback);
 362    }
 363  
 364    // Public: Invoke the given callback when {::activatePreviousRecentlyUsedItem}
 365    // has been called, either initiating or continuing a reverse MRU traversal of
 366    // pane items.
 367    //
 368    // * `callback` {Function} to be called with when the active item changes.
 369    //   * `previousRecentlyUsedItem` The previous MRU item, now being set active
 370    //
 371    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 372    onChooseLastMRUItem(callback) {
 373      return this.emitter.on('choose-last-mru-item', callback);
 374    }
 375  
 376    // Public: Invoke the given callback when {::moveActiveItemToTopOfStack}
 377    // has been called, terminating an MRU traversal of pane items and moving the
 378    // current active item to the top of the stack. Typically bound to a modifier
 379    // (e.g. CTRL) key up event.
 380    //
 381    // * `callback` {Function} to be called with when the MRU traversal is done.
 382    //
 383    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 384    onDoneChoosingMRUItem(callback) {
 385      return this.emitter.on('done-choosing-mru-item', callback);
 386    }
 387  
 388    // Public: Invoke the given callback with the current and future values of
 389    // {::getActiveItem}.
 390    //
 391    // * `callback` {Function} to be called with the current and future active
 392    //   items.
 393    //   * `activeItem` The current active item.
 394    //
 395    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 396    observeActiveItem(callback) {
 397      callback(this.getActiveItem());
 398      return this.onDidChangeActiveItem(callback);
 399    }
 400  
 401    // Public: Invoke the given callback before items are destroyed.
 402    //
 403    // * `callback` {Function} to be called before items are destroyed.
 404    //   * `event` {Object} with the following keys:
 405    //     * `item` The item that will be destroyed.
 406    //     * `index` The location of the item.
 407    //
 408    // Returns a {Disposable} on which `.dispose()` can be called to
 409    // unsubscribe.
 410    onWillDestroyItem(callback) {
 411      return this.emitter.on('will-destroy-item', callback);
 412    }
 413  
 414    // Called by the view layer to indicate that the pane has gained focus.
 415    focus() {
 416      return this.activate();
 417    }
 418  
 419    // Called by the view layer to indicate that the pane has lost focus.
 420    blur() {
 421      this.focused = false;
 422      return true; // if this is called from an event handler, don't cancel it
 423    }
 424  
 425    isFocused() {
 426      return this.focused;
 427    }
 428  
 429    getPanes() {
 430      return [this];
 431    }
 432  
 433    unsubscribeFromItem(item) {
 434      const subscription = this.subscriptionsPerItem.get(item);
 435      if (subscription) {
 436        subscription.dispose();
 437        this.subscriptionsPerItem.delete(item);
 438      }
 439    }
 440  
 441    /*
 442    Section: Items
 443    */
 444  
 445    // Public: Get the items in this pane.
 446    //
 447    // Returns an {Array} of items.
 448    getItems() {
 449      return this.items.slice();
 450    }
 451  
 452    // Public: Get the active pane item in this pane.
 453    //
 454    // Returns a pane item.
 455    getActiveItem() {
 456      return this.activeItem;
 457    }
 458  
 459    setActiveItem(activeItem, options) {
 460      const modifyStack = options && options.modifyStack;
 461      if (activeItem !== this.activeItem) {
 462        if (modifyStack !== false) this.addItemToStack(activeItem);
 463        this.activeItem = activeItem;
 464        this.emitter.emit('did-change-active-item', this.activeItem);
 465        if (this.container)
 466          this.container.didChangeActiveItemOnPane(this, this.activeItem);
 467      }
 468      return this.activeItem;
 469    }
 470  
 471    // Build the itemStack after deserializing
 472    addItemsToStack(itemStackIndices) {
 473      if (this.items.length > 0) {
 474        if (
 475          itemStackIndices.length !== this.items.length ||
 476          itemStackIndices.includes(-1)
 477        ) {
 478          itemStackIndices = this.items.map((item, i) => i);
 479        }
 480  
 481        for (let itemIndex of itemStackIndices) {
 482          this.addItemToStack(this.items[itemIndex]);
 483        }
 484      }
 485    }
 486  
 487    // Add item (or move item) to the end of the itemStack
 488    addItemToStack(newItem) {
 489      if (newItem == null) {
 490        return;
 491      }
 492      const index = this.itemStack.indexOf(newItem);
 493      if (index !== -1) this.itemStack.splice(index, 1);
 494      return this.itemStack.push(newItem);
 495    }
 496  
 497    // Return an {TextEditor} if the pane item is an {TextEditor}, or null otherwise.
 498    getActiveEditor() {
 499      if (this.activeItem instanceof TextEditor) return this.activeItem;
 500    }
 501  
 502    // Public: Return the item at the given index.
 503    //
 504    // * `index` {Number}
 505    //
 506    // Returns an item or `null` if no item exists at the given index.
 507    itemAtIndex(index) {
 508      return this.items[index];
 509    }
 510  
 511    // Makes the next item in the itemStack active.
 512    activateNextRecentlyUsedItem() {
 513      if (this.items.length > 1) {
 514        if (this.itemStackIndex == null)
 515          this.itemStackIndex = this.itemStack.length - 1;
 516        if (this.itemStackIndex === 0)
 517          this.itemStackIndex = this.itemStack.length;
 518        this.itemStackIndex--;
 519        const nextRecentlyUsedItem = this.itemStack[this.itemStackIndex];
 520        this.emitter.emit('choose-next-mru-item', nextRecentlyUsedItem);
 521        this.setActiveItem(nextRecentlyUsedItem, { modifyStack: false });
 522      }
 523    }
 524  
 525    // Makes the previous item in the itemStack active.
 526    activatePreviousRecentlyUsedItem() {
 527      if (this.items.length > 1) {
 528        if (
 529          this.itemStackIndex + 1 === this.itemStack.length ||
 530          this.itemStackIndex == null
 531        ) {
 532          this.itemStackIndex = -1;
 533        }
 534        this.itemStackIndex++;
 535        const previousRecentlyUsedItem = this.itemStack[this.itemStackIndex];
 536        this.emitter.emit('choose-last-mru-item', previousRecentlyUsedItem);
 537        this.setActiveItem(previousRecentlyUsedItem, { modifyStack: false });
 538      }
 539    }
 540  
 541    // Moves the active item to the end of the itemStack once the ctrl key is lifted
 542    moveActiveItemToTopOfStack() {
 543      delete this.itemStackIndex;
 544      this.addItemToStack(this.activeItem);
 545      this.emitter.emit('done-choosing-mru-item');
 546    }
 547  
 548    // Public: Makes the next item active.
 549    activateNextItem() {
 550      const index = this.getActiveItemIndex();
 551      if (index < this.items.length - 1) {
 552        this.activateItemAtIndex(index + 1);
 553      } else {
 554        this.activateItemAtIndex(0);
 555      }
 556    }
 557  
 558    // Public: Makes the previous item active.
 559    activatePreviousItem() {
 560      const index = this.getActiveItemIndex();
 561      if (index > 0) {
 562        this.activateItemAtIndex(index - 1);
 563      } else {
 564        this.activateItemAtIndex(this.items.length - 1);
 565      }
 566    }
 567  
 568    activateLastItem() {
 569      this.activateItemAtIndex(this.items.length - 1);
 570    }
 571  
 572    // Public: Move the active tab to the right.
 573    moveItemRight() {
 574      const index = this.getActiveItemIndex();
 575      const rightItemIndex = index + 1;
 576      if (rightItemIndex <= this.items.length - 1)
 577        this.moveItem(this.getActiveItem(), rightItemIndex);
 578    }
 579  
 580    // Public: Move the active tab to the left
 581    moveItemLeft() {
 582      const index = this.getActiveItemIndex();
 583      const leftItemIndex = index - 1;
 584      if (leftItemIndex >= 0)
 585        return this.moveItem(this.getActiveItem(), leftItemIndex);
 586    }
 587  
 588    // Public: Get the index of the active item.
 589    //
 590    // Returns a {Number}.
 591    getActiveItemIndex() {
 592      return this.items.indexOf(this.activeItem);
 593    }
 594  
 595    // Public: Activate the item at the given index.
 596    //
 597    // * `index` {Number}
 598    activateItemAtIndex(index) {
 599      const item = this.itemAtIndex(index) || this.getActiveItem();
 600      return this.setActiveItem(item);
 601    }
 602  
 603    // Public: Make the given item *active*, causing it to be displayed by
 604    // the pane's view.
 605    //
 606    // * `item` The item to activate
 607    // * `options` (optional) {Object}
 608    //   * `pending` (optional) {Boolean} indicating that the item should be added
 609    //     in a pending state if it does not yet exist in the pane. Existing pending
 610    //     items in a pane are replaced with new pending items when they are opened.
 611    activateItem(item, options = {}) {
 612      if (item) {
 613        const index =
 614          this.getPendingItem() === this.activeItem
 615            ? this.getActiveItemIndex()
 616            : this.getActiveItemIndex() + 1;
 617        this.addItem(item, Object.assign({}, options, { index }));
 618        this.setActiveItem(item);
 619      }
 620    }
 621  
 622    // Public: Add the given item to the pane.
 623    //
 624    // * `item` The item to add. It can be a model with an associated view or a
 625    //   view.
 626    // * `options` (optional) {Object}
 627    //   * `index` (optional) {Number} indicating the index at which to add the item.
 628    //     If omitted, the item is added after the current active item.
 629    //   * `pending` (optional) {Boolean} indicating that the item should be
 630    //     added in a pending state. Existing pending items in a pane are replaced with
 631    //     new pending items when they are opened.
 632    //
 633    // Returns the added item.
 634    addItem(item, options = {}) {
 635      // Backward compat with old API:
 636      //   addItem(item, index=@getActiveItemIndex() + 1)
 637      if (typeof options === 'number') {
 638        Grim.deprecate(
 639          `Pane::addItem(item, ${options}) is deprecated in favor of Pane::addItem(item, {index: ${options}})`
 640        );
 641        options = { index: options };
 642      }
 643  
 644      const index =
 645        options.index != null ? options.index : this.getActiveItemIndex() + 1;
 646      const moved = options.moved != null ? options.moved : false;
 647      const pending = options.pending != null ? options.pending : false;
 648  
 649      if (!item || typeof item !== 'object') {
 650        throw new Error(
 651          `Pane items must be objects. Attempted to add item ${item}.`
 652        );
 653      }
 654  
 655      if (typeof item.isDestroyed === 'function' && item.isDestroyed()) {
 656        throw new Error(
 657          `Adding a pane item with URI '${typeof item.getURI === 'function' &&
 658            item.getURI()}' that has already been destroyed`
 659        );
 660      }
 661  
 662      if (this.items.includes(item)) return;
 663  
 664      const itemSubscriptions = new CompositeDisposable();
 665      this.subscriptionsPerItem.set(item, itemSubscriptions);
 666      if (typeof item.onDidDestroy === 'function') {
 667        itemSubscriptions.add(
 668          item.onDidDestroy(() => this.removeItem(item, false))
 669        );
 670      }
 671      if (typeof item.onDidTerminatePendingState === 'function') {
 672        itemSubscriptions.add(
 673          item.onDidTerminatePendingState(() => {
 674            if (this.getPendingItem() === item) this.clearPendingItem();
 675          })
 676        );
 677      }
 678  
 679      this.items.splice(index, 0, item);
 680      const lastPendingItem = this.getPendingItem();
 681      const replacingPendingItem = lastPendingItem != null && !moved;
 682      if (replacingPendingItem) this.pendingItem = null;
 683      if (pending) this.setPendingItem(item);
 684  
 685      this.emitter.emit('did-add-item', { item, index, moved });
 686      if (!moved) {
 687        if (this.container) this.container.didAddPaneItem(item, this, index);
 688      }
 689  
 690      if (replacingPendingItem) this.destroyItem(lastPendingItem);
 691      if (!this.getActiveItem()) this.setActiveItem(item);
 692      return item;
 693    }
 694  
 695    setPendingItem(item) {
 696      if (this.pendingItem !== item) {
 697        const mostRecentPendingItem = this.pendingItem;
 698        this.pendingItem = item;
 699        if (mostRecentPendingItem) {
 700          this.emitter.emit(
 701            'item-did-terminate-pending-state',
 702            mostRecentPendingItem
 703          );
 704        }
 705      }
 706    }
 707  
 708    getPendingItem() {
 709      return this.pendingItem || null;
 710    }
 711  
 712    clearPendingItem() {
 713      this.setPendingItem(null);
 714    }
 715  
 716    onItemDidTerminatePendingState(callback) {
 717      return this.emitter.on('item-did-terminate-pending-state', callback);
 718    }
 719  
 720    // Public: Add the given items to the pane.
 721    //
 722    // * `items` An {Array} of items to add. Items can be views or models with
 723    //   associated views. Any objects that are already present in the pane's
 724    //   current items will not be added again.
 725    // * `index` (optional) {Number} index at which to add the items. If omitted,
 726    //   the item is #   added after the current active item.
 727    //
 728    // Returns an {Array} of added items.
 729    addItems(items, index = this.getActiveItemIndex() + 1) {
 730      items = items.filter(item => !this.items.includes(item));
 731      for (let i = 0; i < items.length; i++) {
 732        const item = items[i];
 733        this.addItem(item, { index: index + i });
 734      }
 735      return items;
 736    }
 737  
 738    removeItem(item, moved) {
 739      const index = this.items.indexOf(item);
 740      if (index === -1) return;
 741      if (this.getPendingItem() === item) this.pendingItem = null;
 742      this.removeItemFromStack(item);
 743      this.emitter.emit('will-remove-item', {
 744        item,
 745        index,
 746        destroyed: !moved,
 747        moved
 748      });
 749      this.unsubscribeFromItem(item);
 750  
 751      if (item === this.activeItem) {
 752        if (this.items.length === 1) {
 753          this.setActiveItem(undefined);
 754        } else if (index === 0) {
 755          this.activateNextItem();
 756        } else {
 757          this.activatePreviousItem();
 758        }
 759      }
 760      this.items.splice(index, 1);
 761      this.emitter.emit('did-remove-item', {
 762        item,
 763        index,
 764        destroyed: !moved,
 765        moved
 766      });
 767      if (!moved && this.container)
 768        this.container.didDestroyPaneItem({ item, index, pane: this });
 769      if (this.items.length === 0 && this.config.get('core.destroyEmptyPanes'))
 770        this.destroy();
 771    }
 772  
 773    // Remove the given item from the itemStack.
 774    //
 775    // * `item` The item to remove.
 776    // * `index` {Number} indicating the index to which to remove the item from the itemStack.
 777    removeItemFromStack(item) {
 778      const index = this.itemStack.indexOf(item);
 779      if (index !== -1) this.itemStack.splice(index, 1);
 780    }
 781  
 782    // Public: Move the given item to the given index.
 783    //
 784    // * `item` The item to move.
 785    // * `index` {Number} indicating the index to which to move the item.
 786    moveItem(item, newIndex) {
 787      const oldIndex = this.items.indexOf(item);
 788      this.items.splice(oldIndex, 1);
 789      this.items.splice(newIndex, 0, item);
 790      this.emitter.emit('did-move-item', { item, oldIndex, newIndex });
 791    }
 792  
 793    // Public: Move the given item to the given index on another pane.
 794    //
 795    // * `item` The item to move.
 796    // * `pane` {Pane} to which to move the item.
 797    // * `index` {Number} indicating the index to which to move the item in the
 798    //   given pane.
 799    moveItemToPane(item, pane, index) {
 800      this.removeItem(item, true);
 801      return pane.addItem(item, { index, moved: true });
 802    }
 803  
 804    // Public: Destroy the active item and activate the next item.
 805    //
 806    // Returns a {Promise} that resolves when the item is destroyed.
 807    destroyActiveItem() {
 808      return this.destroyItem(this.activeItem);
 809    }
 810  
 811    // Public: Destroy the given item.
 812    //
 813    // If the item is active, the next item will be activated. If the item is the
 814    // last item, the pane will be destroyed if the `core.destroyEmptyPanes` config
 815    // setting is `true`.
 816    //
 817    // * `item` Item to destroy
 818    // * `force` (optional) {Boolean} Destroy the item without prompting to save
 819    //    it, even if the item's `isPermanentDockItem` method returns true.
 820    //
 821    // Returns a {Promise} that resolves with a {Boolean} indicating whether or not
 822    // the item was destroyed.
 823    async destroyItem(item, force) {
 824      const index = this.items.indexOf(item);
 825      if (index === -1) return false;
 826  
 827      if (
 828        !force &&
 829        typeof item.isPermanentDockItem === 'function' &&
 830        item.isPermanentDockItem() &&
 831        (!this.container || this.container.getLocation() !== 'center')
 832      ) {
 833        return false;
 834      }
 835  
 836      // In the case where there are no `onWillDestroyPaneItem` listeners, preserve the old behavior
 837      // where `Pane.destroyItem` and callers such as `Pane.close` take effect synchronously.
 838      if (this.emitter.listenerCountForEventName('will-destroy-item') > 0) {
 839        await this.emitter.emitAsync('will-destroy-item', { item, index });
 840      }
 841      if (
 842        this.container &&
 843        this.container.emitter.listenerCountForEventName(
 844          'will-destroy-pane-item'
 845        ) > 0
 846      ) {
 847        await this.container.willDestroyPaneItem({ item, index, pane: this });
 848      }
 849  
 850      if (
 851        !force &&
 852        typeof item.shouldPromptToSave === 'function' &&
 853        item.shouldPromptToSave()
 854      ) {
 855        if (!(await this.promptToSaveItem(item))) return false;
 856      }
 857      this.removeItem(item, false);
 858      if (typeof item.destroy === 'function') item.destroy();
 859      return true;
 860    }
 861  
 862    // Public: Destroy all items.
 863    destroyItems() {
 864      return Promise.all(this.getItems().map(item => this.destroyItem(item)));
 865    }
 866  
 867    // Public: Destroy all items except for the active item.
 868    destroyInactiveItems() {
 869      return Promise.all(
 870        this.getItems()
 871          .filter(item => item !== this.activeItem)
 872          .map(item => this.destroyItem(item))
 873      );
 874    }
 875  
 876    promptToSaveItem(item, options = {}) {
 877      return new Promise((resolve, reject) => {
 878        if (
 879          typeof item.shouldPromptToSave !== 'function' ||
 880          !item.shouldPromptToSave(options)
 881        ) {
 882          return resolve(true);
 883        }
 884  
 885        let uri;
 886        if (typeof item.getURI === 'function') {
 887          uri = item.getURI();
 888        } else if (typeof item.getUri === 'function') {
 889          uri = item.getUri();
 890        } else {
 891          return resolve(true);
 892        }
 893  
 894        const title =
 895          (typeof item.getTitle === 'function' && item.getTitle()) || uri;
 896  
 897        const saveDialog = (saveButtonText, saveFn, message) => {
 898          this.applicationDelegate.confirm(
 899            {
 900              message,
 901              detail:
 902                'Your changes will be lost if you close this item without saving.',
 903              buttons: [saveButtonText, 'Cancel', "&Don't Save"]
 904            },
 905            response => {
 906              switch (response) {
 907                case 0:
 908                  return saveFn(item, error => {
 909                    if (error instanceof SaveCancelledError) {
 910                      resolve(false);
 911                    } else if (error) {
 912                      saveDialog(
 913                        'Save as',
 914                        this.saveItemAs,
 915                        `'${title}' could not be saved.\nError: ${this.getMessageForErrorCode(
 916                          error.code
 917                        )}`
 918                      );
 919                    } else {
 920                      resolve(true);
 921                    }
 922                  });
 923                case 1:
 924                  return resolve(false);
 925                case 2:
 926                  return resolve(true);
 927              }
 928            }
 929          );
 930        };
 931  
 932        saveDialog(
 933          'Save',
 934          this.saveItem,
 935          `'${title}' has changes, do you want to save them?`
 936        );
 937      });
 938    }
 939  
 940    // Public: Save the active item.
 941    saveActiveItem(nextAction) {
 942      return this.saveItem(this.getActiveItem(), nextAction);
 943    }
 944  
 945    // Public: Prompt the user for a location and save the active item with the
 946    // path they select.
 947    //
 948    // * `nextAction` (optional) {Function} which will be called after the item is
 949    //   successfully saved.
 950    //
 951    // Returns a {Promise} that resolves when the save is complete
 952    saveActiveItemAs(nextAction) {
 953      return this.saveItemAs(this.getActiveItem(), nextAction);
 954    }
 955  
 956    // Public: Save the given item.
 957    //
 958    // * `item` The item to save.
 959    // * `nextAction` (optional) {Function} which will be called with no argument
 960    //   after the item is successfully saved, or with the error if it failed.
 961    //   The return value will be that of `nextAction` or `undefined` if it was not
 962    //   provided
 963    //
 964    // Returns a {Promise} that resolves when the save is complete
 965    saveItem(item, nextAction) {
 966      if (!item) return Promise.resolve();
 967  
 968      let itemURI;
 969      if (typeof item.getURI === 'function') {
 970        itemURI = item.getURI();
 971      } else if (typeof item.getUri === 'function') {
 972        itemURI = item.getUri();
 973      }
 974  
 975      if (itemURI != null) {
 976        if (typeof item.save === 'function') {
 977          return promisify(() => item.save())
 978            .then(() => {
 979              if (nextAction) nextAction();
 980            })
 981            .catch(error => {
 982              if (nextAction) {
 983                nextAction(error);
 984              } else {
 985                this.handleSaveError(error, item);
 986              }
 987            });
 988        } else if (nextAction) {
 989          nextAction();
 990          return Promise.resolve();
 991        }
 992      } else {
 993        return this.saveItemAs(item, nextAction);
 994      }
 995    }
 996  
 997    // Public: Prompt the user for a location and save the active item with the
 998    // path they select.
 999    //
1000    // * `item` The item to save.
1001    // * `nextAction` (optional) {Function} which will be called with no argument
1002    //   after the item is successfully saved, or with the error if it failed.
1003    //   The return value will be that of `nextAction` or `undefined` if it was not
1004    //   provided
1005    async saveItemAs(item, nextAction) {
1006      if (!item) return;
1007      if (typeof item.saveAs !== 'function') return;
1008  
1009      const saveOptions =
1010        typeof item.getSaveDialogOptions === 'function'
1011          ? item.getSaveDialogOptions()
1012          : {};
1013  
1014      const itemPath = item.getPath();
1015      if (itemPath && !saveOptions.defaultPath)
1016        saveOptions.defaultPath = itemPath;
1017  
1018      let resolveSaveDialogPromise = null;
1019      const saveDialogPromise = new Promise(resolve => {
1020        resolveSaveDialogPromise = resolve;
1021      });
1022      this.applicationDelegate.showSaveDialog(saveOptions, newItemPath => {
1023        if (newItemPath) {
1024          promisify(() => item.saveAs(newItemPath))
1025            .then(() => {
1026              if (nextAction) {
1027                resolveSaveDialogPromise(nextAction());
1028              } else {
1029                resolveSaveDialogPromise();
1030              }
1031            })
1032            .catch(error => {
1033              if (nextAction) {
1034                resolveSaveDialogPromise(nextAction(error));
1035              } else {
1036                this.handleSaveError(error, item);
1037                resolveSaveDialogPromise();
1038              }
1039            });
1040        } else if (nextAction) {
1041          resolveSaveDialogPromise(
1042            nextAction(new SaveCancelledError('Save Cancelled'))
1043          );
1044        } else {
1045          resolveSaveDialogPromise();
1046        }
1047      });
1048  
1049      return saveDialogPromise;
1050    }
1051  
1052    // Public: Save all items.
1053    saveItems() {
1054      for (let item of this.getItems()) {
1055        if (typeof item.isModified === 'function' && item.isModified()) {
1056          this.saveItem(item);
1057        }
1058      }
1059    }
1060  
1061    // Public: Return the first item that matches the given URI or undefined if
1062    // none exists.
1063    //
1064    // * `uri` {String} containing a URI.
1065    itemForURI(uri) {
1066      return this.items.find(item => {
1067        if (typeof item.getURI === 'function') {
1068          return item.getURI() === uri;
1069        } else if (typeof item.getUri === 'function') {
1070          return item.getUri() === uri;
1071        }
1072      });
1073    }
1074  
1075    // Public: Activate the first item that matches the given URI.
1076    //
1077    // * `uri` {String} containing a URI.
1078    //
1079    // Returns a {Boolean} indicating whether an item matching the URI was found.
1080    activateItemForURI(uri) {
1081      const item = this.itemForURI(uri);
1082      if (item) {
1083        this.activateItem(item);
1084        return true;
1085      } else {
1086        return false;
1087      }
1088    }
1089  
1090    copyActiveItem() {
1091      if (this.activeItem && typeof this.activeItem.copy === 'function') {
1092        return this.activeItem.copy();
1093      }
1094    }
1095  
1096    /*
1097    Section: Lifecycle
1098    */
1099  
1100    // Public: Determine whether the pane is active.
1101    //
1102    // Returns a {Boolean}.
1103    isActive() {
1104      return this.container && this.container.getActivePane() === this;
1105    }
1106  
1107    // Public: Makes this pane the *active* pane, causing it to gain focus.
1108    activate() {
1109      if (this.isDestroyed()) throw new Error('Pane has been destroyed');
1110      this.focused = true;
1111  
1112      if (this.container) this.container.didActivatePane(this);
1113      this.emitter.emit('did-activate');
1114    }
1115  
1116    // Public: Close the pane and destroy all its items.
1117    //
1118    // If this is the last pane, all the items will be destroyed but the pane
1119    // itself will not be destroyed.
1120    destroy() {
1121      if (
1122        this.container &&
1123        this.container.isAlive() &&
1124        this.container.getPanes().length === 1
1125      ) {
1126        return this.destroyItems();
1127      }
1128  
1129      this.emitter.emit('will-destroy');
1130      this.alive = false;
1131      if (this.container) {
1132        this.container.willDestroyPane({ pane: this });
1133        if (this.isActive()) this.container.activateNextPane();
1134      }
1135      this.emitter.emit('did-destroy');
1136      this.emitter.dispose();
1137      for (let item of this.items.slice()) {
1138        if (typeof item.destroy === 'function') item.destroy();
1139      }
1140      if (this.container) this.container.didDestroyPane({ pane: this });
1141    }
1142  
1143    isAlive() {
1144      return this.alive;
1145    }
1146  
1147    // Public: Determine whether this pane has been destroyed.
1148    //
1149    // Returns a {Boolean}.
1150    isDestroyed() {
1151      return !this.isAlive();
1152    }
1153  
1154    /*
1155    Section: Splitting
1156    */
1157  
1158    // Public: Create a new pane to the left of this pane.
1159    //
1160    // * `params` (optional) {Object} with the following keys:
1161    //   * `items` (optional) {Array} of items to add to the new pane.
1162    //   * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane
1163    //
1164    // Returns the new {Pane}.
1165    splitLeft(params) {
1166      return this.split('horizontal', 'before', params);
1167    }
1168  
1169    // Public: Create a new pane to the right of this pane.
1170    //
1171    // * `params` (optional) {Object} with the following keys:
1172    //   * `items` (optional) {Array} of items to add to the new pane.
1173    //   * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane
1174    //
1175    // Returns the new {Pane}.
1176    splitRight(params) {
1177      return this.split('horizontal', 'after', params);
1178    }
1179  
1180    // Public: Creates a new pane above the receiver.
1181    //
1182    // * `params` (optional) {Object} with the following keys:
1183    //   * `items` (optional) {Array} of items to add to the new pane.
1184    //   * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane
1185    //
1186    // Returns the new {Pane}.
1187    splitUp(params) {
1188      return this.split('vertical', 'before', params);
1189    }
1190  
1191    // Public: Creates a new pane below the receiver.
1192    //
1193    // * `params` (optional) {Object} with the following keys:
1194    //   * `items` (optional) {Array} of items to add to the new pane.
1195    //   * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane
1196    //
1197    // Returns the new {Pane}.
1198    splitDown(params) {
1199      return this.split('vertical', 'after', params);
1200    }
1201  
1202    split(orientation, side, params) {
1203      if (params && params.copyActiveItem) {
1204        if (!params.items) params.items = [];
1205        params.items.push(this.copyActiveItem());
1206      }
1207  
1208      if (this.parent.orientation !== orientation) {
1209        this.parent.replaceChild(
1210          this,
1211          new PaneAxis(
1212            {
1213              container: this.container,
1214              orientation,
1215              children: [this],
1216              flexScale: this.flexScale
1217            },
1218            this.viewRegistry
1219          )
1220        );
1221        this.setFlexScale(1);
1222      }
1223  
1224      const newPane = new Pane(
1225        Object.assign(
1226          {
1227            applicationDelegate: this.applicationDelegate,
1228            notificationManager: this.notificationManager,
1229            deserializerManager: this.deserializerManager,
1230            config: this.config,
1231            viewRegistry: this.viewRegistry
1232          },
1233          params
1234        )
1235      );
1236  
1237      switch (side) {
1238        case 'before':
1239          this.parent.insertChildBefore(this, newPane);
1240          break;
1241        case 'after':
1242          this.parent.insertChildAfter(this, newPane);
1243          break;
1244      }
1245  
1246      if (params && params.moveActiveItem && this.activeItem)
1247        this.moveItemToPane(this.activeItem, newPane);
1248  
1249      newPane.activate();
1250      return newPane;
1251    }
1252  
1253    // If the parent is a horizontal axis, returns its first child if it is a pane;
1254    // otherwise returns this pane.
1255    findLeftmostSibling() {
1256      if (this.parent.orientation === 'horizontal') {
1257        const [leftmostSibling] = this.parent.children;
1258        if (leftmostSibling instanceof PaneAxis) {
1259          return this;
1260        } else {
1261          return leftmostSibling;
1262        }
1263      } else {
1264        return this;
1265      }
1266    }
1267  
1268    findRightmostSibling() {
1269      if (this.parent.orientation === 'horizontal') {
1270        const rightmostSibling = this.parent.children[
1271          this.parent.children.length - 1
1272        ];
1273        if (rightmostSibling instanceof PaneAxis) {
1274          return this;
1275        } else {
1276          return rightmostSibling;
1277        }
1278      } else {
1279        return this;
1280      }
1281    }
1282  
1283    // If the parent is a horizontal axis, returns its last child if it is a pane;
1284    // otherwise returns a new pane created by splitting this pane rightward.
1285    findOrCreateRightmostSibling() {
1286      const rightmostSibling = this.findRightmostSibling();
1287      if (rightmostSibling === this) {
1288        return this.splitRight();
1289      } else {
1290        return rightmostSibling;
1291      }
1292    }
1293  
1294    // If the parent is a vertical axis, returns its first child if it is a pane;
1295    // otherwise returns this pane.
1296    findTopmostSibling() {
1297      if (this.parent.orientation === 'vertical') {
1298        const [topmostSibling] = this.parent.children;
1299        if (topmostSibling instanceof PaneAxis) {
1300          return this;
1301        } else {
1302          return topmostSibling;
1303        }
1304      } else {
1305        return this;
1306      }
1307    }
1308  
1309    findBottommostSibling() {
1310      if (this.parent.orientation === 'vertical') {
1311        const bottommostSibling = this.parent.children[
1312          this.parent.children.length - 1
1313        ];
1314        if (bottommostSibling instanceof PaneAxis) {
1315          return this;
1316        } else {
1317          return bottommostSibling;
1318        }
1319      } else {
1320        return this;
1321      }
1322    }
1323  
1324    // If the parent is a vertical axis, returns its last child if it is a pane;
1325    // otherwise returns a new pane created by splitting this pane bottomward.
1326    findOrCreateBottommostSibling() {
1327      const bottommostSibling = this.findBottommostSibling();
1328      if (bottommostSibling === this) {
1329        return this.splitDown();
1330      } else {
1331        return bottommostSibling;
1332      }
1333    }
1334  
1335    // Private: Close the pane unless the user cancels the action via a dialog.
1336    //
1337    // Returns a {Promise} that resolves once the pane is either closed, or the
1338    // closing has been cancelled.
1339    close() {
1340      return Promise.all(
1341        this.getItems().map(item => this.promptToSaveItem(item))
1342      ).then(results => {
1343        if (!results.includes(false)) return this.destroy();
1344      });
1345    }
1346  
1347    handleSaveError(error, item) {
1348      const itemPath =
1349        error.path || (typeof item.getPath === 'function' && item.getPath());
1350      const addWarningWithPath = (message, options) => {
1351        if (itemPath) message = `${message} '${itemPath}'`;
1352        this.notificationManager.addWarning(message, options);
1353      };
1354  
1355      const customMessage = this.getMessageForErrorCode(error.code);
1356      if (customMessage != null) {
1357        addWarningWithPath(`Unable to save file: ${customMessage}`);
1358      } else if (
1359        error.code === 'EISDIR' ||
1360        (error.message && error.message.endsWith('is a directory'))
1361      ) {
1362        return this.notificationManager.addWarning(
1363          `Unable to save file: ${error.message}`
1364        );
1365      } else if (
1366        ['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST', 'ELOOP', 'EAGAIN'].includes(
1367          error.code
1368        )
1369      ) {
1370        addWarningWithPath('Unable to save file', { detail: error.message });
1371      } else {
1372        const errorMatch = /ENOTDIR, not a directory '([^']+)'/.exec(
1373          error.message
1374        );
1375        if (errorMatch) {
1376          const fileName = errorMatch[1];
1377          this.notificationManager.addWarning(
1378            `Unable to save file: A directory in the path '${fileName}' could not be written to`
1379          );
1380        } else {
1381          throw error;
1382        }
1383      }
1384    }
1385  
1386    getMessageForErrorCode(errorCode) {
1387      switch (errorCode) {
1388        case 'EACCES':
1389          return 'Permission denied';
1390        case 'ECONNRESET':
1391          return 'Connection reset';
1392        case 'EINTR':
1393          return 'Interrupted system call';
1394        case 'EIO':
1395          return 'I/O error writing file';
1396        case 'ENOSPC':
1397          return 'No space left on device';
1398        case 'ENOTSUP':
1399          return 'Operation not supported on socket';
1400        case 'ENXIO':
1401          return 'No such device or address';
1402        case 'EROFS':
1403          return 'Read-only file system';
1404        case 'ESPIPE':
1405          return 'Invalid seek';
1406        case 'ETIMEDOUT':
1407          return 'Connection timed out';
1408      }
1409    }
1410  };
1411  
1412  function promisify(callback) {
1413    try {
1414      return Promise.resolve(callback());
1415    } catch (error) {
1416      return Promise.reject(error);
1417    }
1418  }