/ src / workspace.js
workspace.js
   1  const _ = require('underscore-plus');
   2  const url = require('url');
   3  const path = require('path');
   4  const { Emitter, Disposable, CompositeDisposable } = require('event-kit');
   5  const fs = require('fs-plus');
   6  const { Directory } = require('pathwatcher');
   7  const Grim = require('grim');
   8  const DefaultDirectorySearcher = require('./default-directory-searcher');
   9  const RipgrepDirectorySearcher = require('./ripgrep-directory-searcher');
  10  const Dock = require('./dock');
  11  const Model = require('./model');
  12  const StateStore = require('./state-store');
  13  const TextEditor = require('./text-editor');
  14  const Panel = require('./panel');
  15  const PanelContainer = require('./panel-container');
  16  const Task = require('./task');
  17  const WorkspaceCenter = require('./workspace-center');
  18  const WorkspaceElement = require('./workspace-element');
  19  
  20  const STOPPED_CHANGING_ACTIVE_PANE_ITEM_DELAY = 100;
  21  const ALL_LOCATIONS = ['center', 'left', 'right', 'bottom'];
  22  
  23  // Essential: Represents the state of the user interface for the entire window.
  24  // An instance of this class is available via the `atom.workspace` global.
  25  //
  26  // Interact with this object to open files, be notified of current and future
  27  // editors, and manipulate panes. To add panels, use {Workspace::addTopPanel}
  28  // and friends.
  29  //
  30  // ## Workspace Items
  31  //
  32  // The term "item" refers to anything that can be displayed
  33  // in a pane within the workspace, either in the {WorkspaceCenter} or in one
  34  // of the three {Dock}s. The workspace expects items to conform to the
  35  // following interface:
  36  //
  37  // ### Required Methods
  38  //
  39  // #### `getTitle()`
  40  //
  41  // Returns a {String} containing the title of the item to display on its
  42  // associated tab.
  43  //
  44  // ### Optional Methods
  45  //
  46  // #### `getElement()`
  47  //
  48  // If your item already *is* a DOM element, you do not need to implement this
  49  // method. Otherwise it should return the element you want to display to
  50  // represent this item.
  51  //
  52  // #### `destroy()`
  53  //
  54  // Destroys the item. This will be called when the item is removed from its
  55  // parent pane.
  56  //
  57  // #### `onDidDestroy(callback)`
  58  //
  59  // Called by the workspace so it can be notified when the item is destroyed.
  60  // Must return a {Disposable}.
  61  //
  62  // #### `serialize()`
  63  //
  64  // Serialize the state of the item. Must return an object that can be passed to
  65  // `JSON.stringify`. The state should include a field called `deserializer`,
  66  // which names a deserializer declared in your `package.json`. This method is
  67  // invoked on items when serializing the workspace so they can be restored to
  68  // the same location later.
  69  //
  70  // #### `getURI()`
  71  //
  72  // Returns the URI associated with the item.
  73  //
  74  // #### `getLongTitle()`
  75  //
  76  // Returns a {String} containing a longer version of the title to display in
  77  // places like the window title or on tabs their short titles are ambiguous.
  78  //
  79  // #### `onDidChangeTitle(callback)`
  80  //
  81  // Called by the workspace so it can be notified when the item's title changes.
  82  // Must return a {Disposable}.
  83  //
  84  // #### `getIconName()`
  85  //
  86  // Return a {String} with the name of an icon. If this method is defined and
  87  // returns a string, the item's tab element will be rendered with the `icon` and
  88  // `icon-${iconName}` CSS classes.
  89  //
  90  // ### `onDidChangeIcon(callback)`
  91  //
  92  // Called by the workspace so it can be notified when the item's icon changes.
  93  // Must return a {Disposable}.
  94  //
  95  // #### `getDefaultLocation()`
  96  //
  97  // Tells the workspace where your item should be opened in absence of a user
  98  // override. Items can appear in the center or in a dock on the left, right, or
  99  // bottom of the workspace.
 100  //
 101  // Returns a {String} with one of the following values: `'center'`, `'left'`,
 102  // `'right'`, `'bottom'`. If this method is not defined, `'center'` is the
 103  // default.
 104  //
 105  // #### `getAllowedLocations()`
 106  //
 107  // Tells the workspace where this item can be moved. Returns an {Array} of one
 108  // or more of the following values: `'center'`, `'left'`, `'right'`, or
 109  // `'bottom'`.
 110  //
 111  // #### `isPermanentDockItem()`
 112  //
 113  // Tells the workspace whether or not this item can be closed by the user by
 114  // clicking an `x` on its tab. Use of this feature is discouraged unless there's
 115  // a very good reason not to allow users to close your item. Items can be made
 116  // permanent *only* when they are contained in docks. Center pane items can
 117  // always be removed. Note that it is currently still possible to close dock
 118  // items via the `Close Pane` option in the context menu and via Atom APIs, so
 119  // you should still be prepared to handle your dock items being destroyed by the
 120  // user even if you implement this method.
 121  //
 122  // #### `save()`
 123  //
 124  // Saves the item.
 125  //
 126  // #### `saveAs(path)`
 127  //
 128  // Saves the item to the specified path.
 129  //
 130  // #### `getPath()`
 131  //
 132  // Returns the local path associated with this item. This is only used to set
 133  // the initial location of the "save as" dialog.
 134  //
 135  // #### `isModified()`
 136  //
 137  // Returns whether or not the item is modified to reflect modification in the
 138  // UI.
 139  //
 140  // #### `onDidChangeModified()`
 141  //
 142  // Called by the workspace so it can be notified when item's modified status
 143  // changes. Must return a {Disposable}.
 144  //
 145  // #### `copy()`
 146  //
 147  // Create a copy of the item. If defined, the workspace will call this method to
 148  // duplicate the item when splitting panes via certain split commands.
 149  //
 150  // #### `getPreferredHeight()`
 151  //
 152  // If this item is displayed in the bottom {Dock}, called by the workspace when
 153  // initially displaying the dock to set its height. Once the dock has been
 154  // resized by the user, their height will override this value.
 155  //
 156  // Returns a {Number}.
 157  //
 158  // #### `getPreferredWidth()`
 159  //
 160  // If this item is displayed in the left or right {Dock}, called by the
 161  // workspace when initially displaying the dock to set its width. Once the dock
 162  // has been resized by the user, their width will override this value.
 163  //
 164  // Returns a {Number}.
 165  //
 166  // #### `onDidTerminatePendingState(callback)`
 167  //
 168  // If the workspace is configured to use *pending pane items*, the workspace
 169  // will subscribe to this method to terminate the pending state of the item.
 170  // Must return a {Disposable}.
 171  //
 172  // #### `shouldPromptToSave()`
 173  //
 174  // This method indicates whether Atom should prompt the user to save this item
 175  // when the user closes or reloads the window. Returns a boolean.
 176  module.exports = class Workspace extends Model {
 177    constructor(params) {
 178      super(...arguments);
 179  
 180      this.updateWindowTitle = this.updateWindowTitle.bind(this);
 181      this.updateDocumentEdited = this.updateDocumentEdited.bind(this);
 182      this.didDestroyPaneItem = this.didDestroyPaneItem.bind(this);
 183      this.didChangeActivePaneOnPaneContainer = this.didChangeActivePaneOnPaneContainer.bind(
 184        this
 185      );
 186      this.didChangeActivePaneItemOnPaneContainer = this.didChangeActivePaneItemOnPaneContainer.bind(
 187        this
 188      );
 189      this.didActivatePaneContainer = this.didActivatePaneContainer.bind(this);
 190  
 191      this.enablePersistence = params.enablePersistence;
 192      this.packageManager = params.packageManager;
 193      this.config = params.config;
 194      this.project = params.project;
 195      this.notificationManager = params.notificationManager;
 196      this.viewRegistry = params.viewRegistry;
 197      this.grammarRegistry = params.grammarRegistry;
 198      this.applicationDelegate = params.applicationDelegate;
 199      this.assert = params.assert;
 200      this.deserializerManager = params.deserializerManager;
 201      this.textEditorRegistry = params.textEditorRegistry;
 202      this.styleManager = params.styleManager;
 203      this.draggingItem = false;
 204      this.itemLocationStore = new StateStore('AtomPreviousItemLocations', 1);
 205  
 206      this.emitter = new Emitter();
 207      this.openers = [];
 208      this.destroyedItemURIs = [];
 209      this.stoppedChangingActivePaneItemTimeout = null;
 210  
 211      this.scandalDirectorySearcher = new DefaultDirectorySearcher();
 212      this.ripgrepDirectorySearcher = new RipgrepDirectorySearcher();
 213      this.consumeServices(this.packageManager);
 214  
 215      this.paneContainers = {
 216        center: this.createCenter(),
 217        left: this.createDock('left'),
 218        right: this.createDock('right'),
 219        bottom: this.createDock('bottom')
 220      };
 221      this.activePaneContainer = this.paneContainers.center;
 222      this.hasActiveTextEditor = false;
 223  
 224      this.panelContainers = {
 225        top: new PanelContainer({
 226          viewRegistry: this.viewRegistry,
 227          location: 'top'
 228        }),
 229        left: new PanelContainer({
 230          viewRegistry: this.viewRegistry,
 231          location: 'left',
 232          dock: this.paneContainers.left
 233        }),
 234        right: new PanelContainer({
 235          viewRegistry: this.viewRegistry,
 236          location: 'right',
 237          dock: this.paneContainers.right
 238        }),
 239        bottom: new PanelContainer({
 240          viewRegistry: this.viewRegistry,
 241          location: 'bottom',
 242          dock: this.paneContainers.bottom
 243        }),
 244        header: new PanelContainer({
 245          viewRegistry: this.viewRegistry,
 246          location: 'header'
 247        }),
 248        footer: new PanelContainer({
 249          viewRegistry: this.viewRegistry,
 250          location: 'footer'
 251        }),
 252        modal: new PanelContainer({
 253          viewRegistry: this.viewRegistry,
 254          location: 'modal'
 255        })
 256      };
 257  
 258      this.incoming = new Map();
 259    }
 260  
 261    get paneContainer() {
 262      Grim.deprecate(
 263        '`atom.workspace.paneContainer` has always been private, but it is now gone. Please use `atom.workspace.getCenter()` instead and consult the workspace API docs for public methods.'
 264      );
 265      return this.paneContainers.center.paneContainer;
 266    }
 267  
 268    getElement() {
 269      if (!this.element) {
 270        this.element = new WorkspaceElement().initialize(this, {
 271          config: this.config,
 272          project: this.project,
 273          viewRegistry: this.viewRegistry,
 274          styleManager: this.styleManager
 275        });
 276      }
 277      return this.element;
 278    }
 279  
 280    createCenter() {
 281      return new WorkspaceCenter({
 282        config: this.config,
 283        applicationDelegate: this.applicationDelegate,
 284        notificationManager: this.notificationManager,
 285        deserializerManager: this.deserializerManager,
 286        viewRegistry: this.viewRegistry,
 287        didActivate: this.didActivatePaneContainer,
 288        didChangeActivePane: this.didChangeActivePaneOnPaneContainer,
 289        didChangeActivePaneItem: this.didChangeActivePaneItemOnPaneContainer,
 290        didDestroyPaneItem: this.didDestroyPaneItem
 291      });
 292    }
 293  
 294    createDock(location) {
 295      return new Dock({
 296        location,
 297        config: this.config,
 298        applicationDelegate: this.applicationDelegate,
 299        deserializerManager: this.deserializerManager,
 300        notificationManager: this.notificationManager,
 301        viewRegistry: this.viewRegistry,
 302        didActivate: this.didActivatePaneContainer,
 303        didChangeActivePane: this.didChangeActivePaneOnPaneContainer,
 304        didChangeActivePaneItem: this.didChangeActivePaneItemOnPaneContainer,
 305        didDestroyPaneItem: this.didDestroyPaneItem
 306      });
 307    }
 308  
 309    reset(packageManager) {
 310      this.packageManager = packageManager;
 311      this.emitter.dispose();
 312      this.emitter = new Emitter();
 313  
 314      this.paneContainers.center.destroy();
 315      this.paneContainers.left.destroy();
 316      this.paneContainers.right.destroy();
 317      this.paneContainers.bottom.destroy();
 318  
 319      _.values(this.panelContainers).forEach(panelContainer => {
 320        panelContainer.destroy();
 321      });
 322  
 323      this.paneContainers = {
 324        center: this.createCenter(),
 325        left: this.createDock('left'),
 326        right: this.createDock('right'),
 327        bottom: this.createDock('bottom')
 328      };
 329      this.activePaneContainer = this.paneContainers.center;
 330      this.hasActiveTextEditor = false;
 331  
 332      this.panelContainers = {
 333        top: new PanelContainer({
 334          viewRegistry: this.viewRegistry,
 335          location: 'top'
 336        }),
 337        left: new PanelContainer({
 338          viewRegistry: this.viewRegistry,
 339          location: 'left',
 340          dock: this.paneContainers.left
 341        }),
 342        right: new PanelContainer({
 343          viewRegistry: this.viewRegistry,
 344          location: 'right',
 345          dock: this.paneContainers.right
 346        }),
 347        bottom: new PanelContainer({
 348          viewRegistry: this.viewRegistry,
 349          location: 'bottom',
 350          dock: this.paneContainers.bottom
 351        }),
 352        header: new PanelContainer({
 353          viewRegistry: this.viewRegistry,
 354          location: 'header'
 355        }),
 356        footer: new PanelContainer({
 357          viewRegistry: this.viewRegistry,
 358          location: 'footer'
 359        }),
 360        modal: new PanelContainer({
 361          viewRegistry: this.viewRegistry,
 362          location: 'modal'
 363        })
 364      };
 365  
 366      this.originalFontSize = null;
 367      this.openers = [];
 368      this.destroyedItemURIs = [];
 369      if (this.element) {
 370        this.element.destroy();
 371        this.element = null;
 372      }
 373      this.consumeServices(this.packageManager);
 374    }
 375  
 376    initialize() {
 377      this.originalFontSize = this.config.get('editor.fontSize');
 378      this.project.onDidChangePaths(this.updateWindowTitle);
 379      this.subscribeToAddedItems();
 380      this.subscribeToMovedItems();
 381      this.subscribeToDockToggling();
 382    }
 383  
 384    consumeServices({ serviceHub }) {
 385      this.directorySearchers = [];
 386      serviceHub.consume('atom.directory-searcher', '^0.1.0', provider =>
 387        this.directorySearchers.unshift(provider)
 388      );
 389    }
 390  
 391    // Called by the Serializable mixin during serialization.
 392    serialize() {
 393      return {
 394        deserializer: 'Workspace',
 395        packagesWithActiveGrammars: this.getPackageNamesWithActiveGrammars(),
 396        destroyedItemURIs: this.destroyedItemURIs.slice(),
 397        // Ensure deserializing 1.17 state with pre 1.17 Atom does not error
 398        // TODO: Remove after 1.17 has been on stable for a while
 399        paneContainer: { version: 2 },
 400        paneContainers: {
 401          center: this.paneContainers.center.serialize(),
 402          left: this.paneContainers.left.serialize(),
 403          right: this.paneContainers.right.serialize(),
 404          bottom: this.paneContainers.bottom.serialize()
 405        }
 406      };
 407    }
 408  
 409    deserialize(state, deserializerManager) {
 410      const packagesWithActiveGrammars =
 411        state.packagesWithActiveGrammars != null
 412          ? state.packagesWithActiveGrammars
 413          : [];
 414      for (let packageName of packagesWithActiveGrammars) {
 415        const pkg = this.packageManager.getLoadedPackage(packageName);
 416        if (pkg != null) {
 417          pkg.loadGrammarsSync();
 418        }
 419      }
 420      if (state.destroyedItemURIs != null) {
 421        this.destroyedItemURIs = state.destroyedItemURIs;
 422      }
 423  
 424      if (state.paneContainers) {
 425        this.paneContainers.center.deserialize(
 426          state.paneContainers.center,
 427          deserializerManager
 428        );
 429        this.paneContainers.left.deserialize(
 430          state.paneContainers.left,
 431          deserializerManager
 432        );
 433        this.paneContainers.right.deserialize(
 434          state.paneContainers.right,
 435          deserializerManager
 436        );
 437        this.paneContainers.bottom.deserialize(
 438          state.paneContainers.bottom,
 439          deserializerManager
 440        );
 441      } else if (state.paneContainer) {
 442        // TODO: Remove this fallback once a lot of time has passed since 1.17 was released
 443        this.paneContainers.center.deserialize(
 444          state.paneContainer,
 445          deserializerManager
 446        );
 447      }
 448  
 449      this.hasActiveTextEditor = this.getActiveTextEditor() != null;
 450  
 451      this.updateWindowTitle();
 452    }
 453  
 454    getPackageNamesWithActiveGrammars() {
 455      const packageNames = [];
 456      const addGrammar = ({ includedGrammarScopes, packageName } = {}) => {
 457        if (!packageName) {
 458          return;
 459        }
 460        // Prevent cycles
 461        if (packageNames.indexOf(packageName) !== -1) {
 462          return;
 463        }
 464  
 465        packageNames.push(packageName);
 466        for (let scopeName of includedGrammarScopes != null
 467          ? includedGrammarScopes
 468          : []) {
 469          addGrammar(this.grammarRegistry.grammarForScopeName(scopeName));
 470        }
 471      };
 472  
 473      const editors = this.getTextEditors();
 474      for (let editor of editors) {
 475        addGrammar(editor.getGrammar());
 476      }
 477  
 478      if (editors.length > 0) {
 479        for (let grammar of this.grammarRegistry.getGrammars()) {
 480          if (grammar.injectionSelector) {
 481            addGrammar(grammar);
 482          }
 483        }
 484      }
 485  
 486      return _.uniq(packageNames);
 487    }
 488  
 489    didActivatePaneContainer(paneContainer) {
 490      if (paneContainer !== this.getActivePaneContainer()) {
 491        this.activePaneContainer = paneContainer;
 492        this.didChangeActivePaneItem(
 493          this.activePaneContainer.getActivePaneItem()
 494        );
 495        this.emitter.emit(
 496          'did-change-active-pane-container',
 497          this.activePaneContainer
 498        );
 499        this.emitter.emit(
 500          'did-change-active-pane',
 501          this.activePaneContainer.getActivePane()
 502        );
 503        this.emitter.emit(
 504          'did-change-active-pane-item',
 505          this.activePaneContainer.getActivePaneItem()
 506        );
 507      }
 508    }
 509  
 510    didChangeActivePaneOnPaneContainer(paneContainer, pane) {
 511      if (paneContainer === this.getActivePaneContainer()) {
 512        this.emitter.emit('did-change-active-pane', pane);
 513      }
 514    }
 515  
 516    didChangeActivePaneItemOnPaneContainer(paneContainer, item) {
 517      if (paneContainer === this.getActivePaneContainer()) {
 518        this.didChangeActivePaneItem(item);
 519        this.emitter.emit('did-change-active-pane-item', item);
 520      }
 521  
 522      if (paneContainer === this.getCenter()) {
 523        const hadActiveTextEditor = this.hasActiveTextEditor;
 524        this.hasActiveTextEditor = item instanceof TextEditor;
 525  
 526        if (this.hasActiveTextEditor || hadActiveTextEditor) {
 527          const itemValue = this.hasActiveTextEditor ? item : undefined;
 528          this.emitter.emit('did-change-active-text-editor', itemValue);
 529        }
 530      }
 531    }
 532  
 533    didChangeActivePaneItem(item) {
 534      this.updateWindowTitle();
 535      this.updateDocumentEdited();
 536      if (this.activeItemSubscriptions) this.activeItemSubscriptions.dispose();
 537      this.activeItemSubscriptions = new CompositeDisposable();
 538  
 539      let modifiedSubscription, titleSubscription;
 540  
 541      if (item != null && typeof item.onDidChangeTitle === 'function') {
 542        titleSubscription = item.onDidChangeTitle(this.updateWindowTitle);
 543      } else if (item != null && typeof item.on === 'function') {
 544        titleSubscription = item.on('title-changed', this.updateWindowTitle);
 545        if (
 546          titleSubscription == null ||
 547          typeof titleSubscription.dispose !== 'function'
 548        ) {
 549          titleSubscription = new Disposable(() => {
 550            item.off('title-changed', this.updateWindowTitle);
 551          });
 552        }
 553      }
 554  
 555      if (item != null && typeof item.onDidChangeModified === 'function') {
 556        modifiedSubscription = item.onDidChangeModified(
 557          this.updateDocumentEdited
 558        );
 559      } else if (item != null && typeof item.on === 'function') {
 560        modifiedSubscription = item.on(
 561          'modified-status-changed',
 562          this.updateDocumentEdited
 563        );
 564        if (
 565          modifiedSubscription == null ||
 566          typeof modifiedSubscription.dispose !== 'function'
 567        ) {
 568          modifiedSubscription = new Disposable(() => {
 569            item.off('modified-status-changed', this.updateDocumentEdited);
 570          });
 571        }
 572      }
 573  
 574      if (titleSubscription != null) {
 575        this.activeItemSubscriptions.add(titleSubscription);
 576      }
 577      if (modifiedSubscription != null) {
 578        this.activeItemSubscriptions.add(modifiedSubscription);
 579      }
 580  
 581      this.cancelStoppedChangingActivePaneItemTimeout();
 582      this.stoppedChangingActivePaneItemTimeout = setTimeout(() => {
 583        this.stoppedChangingActivePaneItemTimeout = null;
 584        this.emitter.emit('did-stop-changing-active-pane-item', item);
 585      }, STOPPED_CHANGING_ACTIVE_PANE_ITEM_DELAY);
 586    }
 587  
 588    cancelStoppedChangingActivePaneItemTimeout() {
 589      if (this.stoppedChangingActivePaneItemTimeout != null) {
 590        clearTimeout(this.stoppedChangingActivePaneItemTimeout);
 591      }
 592    }
 593  
 594    setDraggingItem(draggingItem) {
 595      _.values(this.paneContainers).forEach(dock => {
 596        dock.setDraggingItem(draggingItem);
 597      });
 598    }
 599  
 600    subscribeToAddedItems() {
 601      this.onDidAddPaneItem(({ item, pane, index }) => {
 602        if (item instanceof TextEditor) {
 603          const subscriptions = new CompositeDisposable(
 604            this.textEditorRegistry.add(item),
 605            this.textEditorRegistry.maintainConfig(item)
 606          );
 607          if (!this.project.findBufferForId(item.buffer.id)) {
 608            this.project.addBuffer(item.buffer);
 609          }
 610          item.onDidDestroy(() => {
 611            subscriptions.dispose();
 612          });
 613          this.emitter.emit('did-add-text-editor', {
 614            textEditor: item,
 615            pane,
 616            index
 617          });
 618          // It's important to call handleGrammarUsed after emitting the did-add event:
 619          // if we activate a package between adding the editor to the registry and emitting
 620          // the package may receive the editor twice from `observeTextEditors`.
 621          // (Note that the item can be destroyed by an `observeTextEditors` handler.)
 622          if (!item.isDestroyed()) {
 623            subscriptions.add(
 624              item.observeGrammar(this.handleGrammarUsed.bind(this))
 625            );
 626          }
 627        }
 628      });
 629    }
 630  
 631    subscribeToDockToggling() {
 632      const docks = [
 633        this.getLeftDock(),
 634        this.getRightDock(),
 635        this.getBottomDock()
 636      ];
 637      docks.forEach(dock => {
 638        dock.onDidChangeVisible(visible => {
 639          if (visible) return;
 640          const { activeElement } = document;
 641          const dockElement = dock.getElement();
 642          if (
 643            dockElement === activeElement ||
 644            dockElement.contains(activeElement)
 645          ) {
 646            this.getCenter().activate();
 647          }
 648        });
 649      });
 650    }
 651  
 652    subscribeToMovedItems() {
 653      for (const paneContainer of this.getPaneContainers()) {
 654        paneContainer.observePanes(pane => {
 655          pane.onDidAddItem(({ item }) => {
 656            if (typeof item.getURI === 'function' && this.enablePersistence) {
 657              const uri = item.getURI();
 658              if (uri) {
 659                const location = paneContainer.getLocation();
 660                let defaultLocation;
 661                if (typeof item.getDefaultLocation === 'function') {
 662                  defaultLocation = item.getDefaultLocation();
 663                }
 664                defaultLocation = defaultLocation || 'center';
 665                if (location === defaultLocation) {
 666                  this.itemLocationStore.delete(item.getURI());
 667                } else {
 668                  this.itemLocationStore.save(item.getURI(), location);
 669                }
 670              }
 671            }
 672          });
 673        });
 674      }
 675    }
 676  
 677    // Updates the application's title and proxy icon based on whichever file is
 678    // open.
 679    updateWindowTitle() {
 680      let itemPath, itemTitle, projectPath, representedPath;
 681      const appName = atom.getAppName();
 682      const left = this.project.getPaths();
 683      const projectPaths = left != null ? left : [];
 684      const item = this.getActivePaneItem();
 685      if (item) {
 686        itemPath =
 687          typeof item.getPath === 'function' ? item.getPath() : undefined;
 688        const longTitle =
 689          typeof item.getLongTitle === 'function'
 690            ? item.getLongTitle()
 691            : undefined;
 692        itemTitle =
 693          longTitle == null
 694            ? typeof item.getTitle === 'function'
 695              ? item.getTitle()
 696              : undefined
 697            : longTitle;
 698        projectPath = _.find(
 699          projectPaths,
 700          projectPath =>
 701            itemPath === projectPath ||
 702            (itemPath != null
 703              ? itemPath.startsWith(projectPath + path.sep)
 704              : undefined)
 705        );
 706      }
 707      if (itemTitle == null) {
 708        itemTitle = 'untitled';
 709      }
 710      if (projectPath == null) {
 711        projectPath = itemPath ? path.dirname(itemPath) : projectPaths[0];
 712      }
 713      if (projectPath != null) {
 714        projectPath = fs.tildify(projectPath);
 715      }
 716  
 717      const titleParts = [];
 718      if (item != null && projectPath != null) {
 719        titleParts.push(itemTitle, projectPath);
 720        representedPath = itemPath != null ? itemPath : projectPath;
 721      } else if (projectPath != null) {
 722        titleParts.push(projectPath);
 723        representedPath = projectPath;
 724      } else {
 725        titleParts.push(itemTitle);
 726        representedPath = '';
 727      }
 728  
 729      if (process.platform !== 'darwin') {
 730        titleParts.push(appName);
 731      }
 732  
 733      document.title = titleParts.join(' \u2014 ');
 734      this.applicationDelegate.setRepresentedFilename(representedPath);
 735      this.emitter.emit('did-change-window-title');
 736    }
 737  
 738    // On macOS, fades the application window's proxy icon when the current file
 739    // has been modified.
 740    updateDocumentEdited() {
 741      const activePaneItem = this.getActivePaneItem();
 742      const modified =
 743        activePaneItem != null && typeof activePaneItem.isModified === 'function'
 744          ? activePaneItem.isModified() || false
 745          : false;
 746      this.applicationDelegate.setWindowDocumentEdited(modified);
 747    }
 748  
 749    /*
 750    Section: Event Subscription
 751    */
 752  
 753    onDidChangeActivePaneContainer(callback) {
 754      return this.emitter.on('did-change-active-pane-container', callback);
 755    }
 756  
 757    // Essential: Invoke the given callback with all current and future text
 758    // editors in the workspace.
 759    //
 760    // * `callback` {Function} to be called with current and future text editors.
 761    //   * `editor` A {TextEditor} that is present in {::getTextEditors} at the time
 762    //     of subscription or that is added at some later time.
 763    //
 764    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 765    observeTextEditors(callback) {
 766      for (let textEditor of this.getTextEditors()) {
 767        callback(textEditor);
 768      }
 769      return this.onDidAddTextEditor(({ textEditor }) => callback(textEditor));
 770    }
 771  
 772    // Essential: Invoke the given callback with all current and future panes items
 773    // in the workspace.
 774    //
 775    // * `callback` {Function} to be called with current and future pane items.
 776    //   * `item` An item that is present in {::getPaneItems} at the time of
 777    //      subscription or that is added at some later time.
 778    //
 779    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 780    observePaneItems(callback) {
 781      return new CompositeDisposable(
 782        ...this.getPaneContainers().map(container =>
 783          container.observePaneItems(callback)
 784        )
 785      );
 786    }
 787  
 788    // Essential: Invoke the given callback when the active pane item changes.
 789    //
 790    // Because observers are invoked synchronously, it's important not to perform
 791    // any expensive operations via this method. Consider
 792    // {::onDidStopChangingActivePaneItem} to delay operations until after changes
 793    // stop occurring.
 794    //
 795    // * `callback` {Function} to be called when the active pane item changes.
 796    //   * `item` The active pane item.
 797    //
 798    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 799    onDidChangeActivePaneItem(callback) {
 800      return this.emitter.on('did-change-active-pane-item', callback);
 801    }
 802  
 803    // Essential: Invoke the given callback when the active pane item stops
 804    // changing.
 805    //
 806    // Observers are called asynchronously 100ms after the last active pane item
 807    // change. Handling changes here rather than in the synchronous
 808    // {::onDidChangeActivePaneItem} prevents unneeded work if the user is quickly
 809    // changing or closing tabs and ensures critical UI feedback, like changing the
 810    // highlighted tab, gets priority over work that can be done asynchronously.
 811    //
 812    // * `callback` {Function} to be called when the active pane item stops
 813    //   changing.
 814    //   * `item` The active pane item.
 815    //
 816    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 817    onDidStopChangingActivePaneItem(callback) {
 818      return this.emitter.on('did-stop-changing-active-pane-item', callback);
 819    }
 820  
 821    // Essential: Invoke the given callback when a text editor becomes the active
 822    // text editor and when there is no longer an active text editor.
 823    //
 824    // * `callback` {Function} to be called when the active text editor changes.
 825    //   * `editor` The active {TextEditor} or undefined if there is no longer an
 826    //      active text editor.
 827    //
 828    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 829    onDidChangeActiveTextEditor(callback) {
 830      return this.emitter.on('did-change-active-text-editor', callback);
 831    }
 832  
 833    // Essential: Invoke the given callback with the current active pane item and
 834    // with all future active pane items in the workspace.
 835    //
 836    // * `callback` {Function} to be called when the active pane item changes.
 837    //   * `item` The current active pane item.
 838    //
 839    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 840    observeActivePaneItem(callback) {
 841      callback(this.getActivePaneItem());
 842      return this.onDidChangeActivePaneItem(callback);
 843    }
 844  
 845    // Essential: Invoke the given callback with the current active text editor
 846    // (if any), with all future active text editors, and when there is no longer
 847    // an active text editor.
 848    //
 849    // * `callback` {Function} to be called when the active text editor changes.
 850    //   * `editor` The active {TextEditor} or undefined if there is not an
 851    //      active text editor.
 852    //
 853    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 854    observeActiveTextEditor(callback) {
 855      callback(this.getActiveTextEditor());
 856  
 857      return this.onDidChangeActiveTextEditor(callback);
 858    }
 859  
 860    // Essential: Invoke the given callback whenever an item is opened. Unlike
 861    // {::onDidAddPaneItem}, observers will be notified for items that are already
 862    // present in the workspace when they are reopened.
 863    //
 864    // * `callback` {Function} to be called whenever an item is opened.
 865    //   * `event` {Object} with the following keys:
 866    //     * `uri` {String} representing the opened URI. Could be `undefined`.
 867    //     * `item` The opened item.
 868    //     * `pane` The pane in which the item was opened.
 869    //     * `index` The index of the opened item on its pane.
 870    //
 871    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 872    onDidOpen(callback) {
 873      return this.emitter.on('did-open', callback);
 874    }
 875  
 876    // Extended: Invoke the given callback when a pane is added to the workspace.
 877    //
 878    // * `callback` {Function} to be called panes are added.
 879    //   * `event` {Object} with the following keys:
 880    //     * `pane` The added pane.
 881    //
 882    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 883    onDidAddPane(callback) {
 884      return new CompositeDisposable(
 885        ...this.getPaneContainers().map(container =>
 886          container.onDidAddPane(callback)
 887        )
 888      );
 889    }
 890  
 891    // Extended: Invoke the given callback before a pane is destroyed in the
 892    // workspace.
 893    //
 894    // * `callback` {Function} to be called before panes are destroyed.
 895    //   * `event` {Object} with the following keys:
 896    //     * `pane` The pane to be destroyed.
 897    //
 898    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 899    onWillDestroyPane(callback) {
 900      return new CompositeDisposable(
 901        ...this.getPaneContainers().map(container =>
 902          container.onWillDestroyPane(callback)
 903        )
 904      );
 905    }
 906  
 907    // Extended: Invoke the given callback when a pane is destroyed in the
 908    // workspace.
 909    //
 910    // * `callback` {Function} to be called panes are destroyed.
 911    //   * `event` {Object} with the following keys:
 912    //     * `pane` The destroyed pane.
 913    //
 914    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 915    onDidDestroyPane(callback) {
 916      return new CompositeDisposable(
 917        ...this.getPaneContainers().map(container =>
 918          container.onDidDestroyPane(callback)
 919        )
 920      );
 921    }
 922  
 923    // Extended: Invoke the given callback with all current and future panes in the
 924    // workspace.
 925    //
 926    // * `callback` {Function} to be called with current and future panes.
 927    //   * `pane` A {Pane} that is present in {::getPanes} at the time of
 928    //      subscription or that is added at some later time.
 929    //
 930    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 931    observePanes(callback) {
 932      return new CompositeDisposable(
 933        ...this.getPaneContainers().map(container =>
 934          container.observePanes(callback)
 935        )
 936      );
 937    }
 938  
 939    // Extended: Invoke the given callback when the active pane changes.
 940    //
 941    // * `callback` {Function} to be called when the active pane changes.
 942    //   * `pane` A {Pane} that is the current return value of {::getActivePane}.
 943    //
 944    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 945    onDidChangeActivePane(callback) {
 946      return this.emitter.on('did-change-active-pane', callback);
 947    }
 948  
 949    // Extended: Invoke the given callback with the current active pane and when
 950    // the active pane changes.
 951    //
 952    // * `callback` {Function} to be called with the current and future active#
 953    //   panes.
 954    //   * `pane` A {Pane} that is the current return value of {::getActivePane}.
 955    //
 956    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 957    observeActivePane(callback) {
 958      callback(this.getActivePane());
 959      return this.onDidChangeActivePane(callback);
 960    }
 961  
 962    // Extended: Invoke the given callback when a pane item is added to the
 963    // workspace.
 964    //
 965    // * `callback` {Function} to be called when pane items are added.
 966    //   * `event` {Object} with the following keys:
 967    //     * `item` The added pane item.
 968    //     * `pane` {Pane} containing the added item.
 969    //     * `index` {Number} indicating the index of the added item in its pane.
 970    //
 971    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
 972    onDidAddPaneItem(callback) {
 973      return new CompositeDisposable(
 974        ...this.getPaneContainers().map(container =>
 975          container.onDidAddPaneItem(callback)
 976        )
 977      );
 978    }
 979  
 980    // Extended: Invoke the given callback when a pane item is about to be
 981    // destroyed, before the user is prompted to save it.
 982    //
 983    // * `callback` {Function} to be called before pane items are destroyed. If this function returns
 984    //   a {Promise}, then the item will not be destroyed until the promise resolves.
 985    //   * `event` {Object} with the following keys:
 986    //     * `item` The item to be destroyed.
 987    //     * `pane` {Pane} containing the item to be destroyed.
 988    //     * `index` {Number} indicating the index of the item to be destroyed in
 989    //       its pane.
 990    //
 991    // Returns a {Disposable} on which `.dispose` can be called to unsubscribe.
 992    onWillDestroyPaneItem(callback) {
 993      return new CompositeDisposable(
 994        ...this.getPaneContainers().map(container =>
 995          container.onWillDestroyPaneItem(callback)
 996        )
 997      );
 998    }
 999  
1000    // Extended: Invoke the given callback when a pane item is destroyed.
1001    //
1002    // * `callback` {Function} to be called when pane items are destroyed.
1003    //   * `event` {Object} with the following keys:
1004    //     * `item` The destroyed item.
1005    //     * `pane` {Pane} containing the destroyed item.
1006    //     * `index` {Number} indicating the index of the destroyed item in its
1007    //       pane.
1008    //
1009    // Returns a {Disposable} on which `.dispose` can be called to unsubscribe.
1010    onDidDestroyPaneItem(callback) {
1011      return new CompositeDisposable(
1012        ...this.getPaneContainers().map(container =>
1013          container.onDidDestroyPaneItem(callback)
1014        )
1015      );
1016    }
1017  
1018    // Extended: Invoke the given callback when a text editor is added to the
1019    // workspace.
1020    //
1021    // * `callback` {Function} to be called panes are added.
1022    //   * `event` {Object} with the following keys:
1023    //     * `textEditor` {TextEditor} that was added.
1024    //     * `pane` {Pane} containing the added text editor.
1025    //     * `index` {Number} indicating the index of the added text editor in its
1026    //        pane.
1027    //
1028    // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
1029    onDidAddTextEditor(callback) {
1030      return this.emitter.on('did-add-text-editor', callback);
1031    }
1032  
1033    onDidChangeWindowTitle(callback) {
1034      return this.emitter.on('did-change-window-title', callback);
1035    }
1036  
1037    /*
1038    Section: Opening
1039    */
1040  
1041    // Essential: Opens the given URI in Atom asynchronously.
1042    // If the URI is already open, the existing item for that URI will be
1043    // activated. If no URI is given, or no registered opener can open
1044    // the URI, a new empty {TextEditor} will be created.
1045    //
1046    // * `uri` (optional) A {String} containing a URI.
1047    // * `options` (optional) {Object}
1048    //   * `initialLine` A {Number} indicating which row to move the cursor to
1049    //     initially. Defaults to `0`.
1050    //   * `initialColumn` A {Number} indicating which column to move the cursor to
1051    //     initially. Defaults to `0`.
1052    //   * `split` Either 'left', 'right', 'up' or 'down'.
1053    //     If 'left', the item will be opened in leftmost pane of the current active pane's row.
1054    //     If 'right', the item will be opened in the rightmost pane of the current active pane's row. If only one pane exists in the row, a new pane will be created.
1055    //     If 'up', the item will be opened in topmost pane of the current active pane's column.
1056    //     If 'down', the item will be opened in the bottommost pane of the current active pane's column. If only one pane exists in the column, a new pane will be created.
1057    //   * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on
1058    //     containing pane. Defaults to `true`.
1059    //   * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem}
1060    //     on containing pane. Defaults to `true`.
1061    //   * `pending` A {Boolean} indicating whether or not the item should be opened
1062    //     in a pending state. Existing pending items in a pane are replaced with
1063    //     new pending items when they are opened.
1064    //   * `searchAllPanes` A {Boolean}. If `true`, the workspace will attempt to
1065    //     activate an existing item for the given URI on any pane.
1066    //     If `false`, only the active pane will be searched for
1067    //     an existing item for the same URI. Defaults to `false`.
1068    //   * `location` (optional) A {String} containing the name of the location
1069    //     in which this item should be opened (one of "left", "right", "bottom",
1070    //     or "center"). If omitted, Atom will fall back to the last location in
1071    //     which a user has placed an item with the same URI or, if this is a new
1072    //     URI, the default location specified by the item. NOTE: This option
1073    //     should almost always be omitted to honor user preference.
1074    //
1075    // Returns a {Promise} that resolves to the {TextEditor} for the file URI.
1076    async open(itemOrURI, options = {}) {
1077      let uri, item;
1078      if (typeof itemOrURI === 'string') {
1079        uri = this.project.resolvePath(itemOrURI);
1080      } else if (itemOrURI) {
1081        item = itemOrURI;
1082        if (typeof item.getURI === 'function') uri = item.getURI();
1083      }
1084  
1085      let resolveItem = () => {};
1086      if (uri) {
1087        const incomingItem = this.incoming.get(uri);
1088        if (!incomingItem) {
1089          this.incoming.set(
1090            uri,
1091            new Promise(resolve => {
1092              resolveItem = resolve;
1093            })
1094          );
1095        } else {
1096          await incomingItem;
1097        }
1098      }
1099  
1100      try {
1101        if (!atom.config.get('core.allowPendingPaneItems')) {
1102          options.pending = false;
1103        }
1104  
1105        // Avoid adding URLs as recent documents to work-around this Spotlight crash:
1106        // https://github.com/atom/atom/issues/10071
1107        if (uri && (!url.parse(uri).protocol || process.platform === 'win32')) {
1108          this.applicationDelegate.addRecentDocument(uri);
1109        }
1110  
1111        let pane, itemExistsInWorkspace;
1112  
1113        // Try to find an existing item in the workspace.
1114        if (item || uri) {
1115          if (options.pane) {
1116            pane = options.pane;
1117          } else if (options.searchAllPanes) {
1118            pane = item ? this.paneForItem(item) : this.paneForURI(uri);
1119          } else {
1120            // If an item with the given URI is already in the workspace, assume
1121            // that item's pane container is the preferred location for that URI.
1122            let container;
1123            if (uri) container = this.paneContainerForURI(uri);
1124            if (!container) container = this.getActivePaneContainer();
1125  
1126            // The `split` option affects where we search for the item.
1127            pane = container.getActivePane();
1128            switch (options.split) {
1129              case 'left':
1130                pane = pane.findLeftmostSibling();
1131                break;
1132              case 'right':
1133                pane = pane.findRightmostSibling();
1134                break;
1135              case 'up':
1136                pane = pane.findTopmostSibling();
1137                break;
1138              case 'down':
1139                pane = pane.findBottommostSibling();
1140                break;
1141            }
1142          }
1143  
1144          if (pane) {
1145            if (item) {
1146              itemExistsInWorkspace = pane.getItems().includes(item);
1147            } else {
1148              item = pane.itemForURI(uri);
1149              itemExistsInWorkspace = item != null;
1150            }
1151          }
1152        }
1153  
1154        // If we already have an item at this stage, we won't need to do an async
1155        // lookup of the URI, so we yield the event loop to ensure this method
1156        // is consistently asynchronous.
1157        if (item) await Promise.resolve();
1158  
1159        if (!itemExistsInWorkspace) {
1160          item = item || (await this.createItemForURI(uri, options));
1161          if (!item) return;
1162  
1163          if (options.pane) {
1164            pane = options.pane;
1165          } else {
1166            let location = options.location;
1167            if (!location && !options.split && uri && this.enablePersistence) {
1168              location = await this.itemLocationStore.load(uri);
1169            }
1170            if (!location && typeof item.getDefaultLocation === 'function') {
1171              location = item.getDefaultLocation();
1172            }
1173  
1174            const allowedLocations =
1175              typeof item.getAllowedLocations === 'function'
1176                ? item.getAllowedLocations()
1177                : ALL_LOCATIONS;
1178            location = allowedLocations.includes(location)
1179              ? location
1180              : allowedLocations[0];
1181  
1182            const container = this.paneContainers[location] || this.getCenter();
1183            pane = container.getActivePane();
1184            switch (options.split) {
1185              case 'left':
1186                pane = pane.findLeftmostSibling();
1187                break;
1188              case 'right':
1189                pane = pane.findOrCreateRightmostSibling();
1190                break;
1191              case 'up':
1192                pane = pane.findTopmostSibling();
1193                break;
1194              case 'down':
1195                pane = pane.findOrCreateBottommostSibling();
1196                break;
1197            }
1198          }
1199        }
1200  
1201        if (!options.pending && pane.getPendingItem() === item) {
1202          pane.clearPendingItem();
1203        }
1204  
1205        this.itemOpened(item);
1206  
1207        if (options.activateItem === false) {
1208          pane.addItem(item, { pending: options.pending });
1209        } else {
1210          pane.activateItem(item, { pending: options.pending });
1211        }
1212  
1213        if (options.activatePane !== false) {
1214          pane.activate();
1215        }
1216  
1217        let initialColumn = 0;
1218        let initialLine = 0;
1219        if (!Number.isNaN(options.initialLine)) {
1220          initialLine = options.initialLine;
1221        }
1222        if (!Number.isNaN(options.initialColumn)) {
1223          initialColumn = options.initialColumn;
1224        }
1225        if (initialLine >= 0 || initialColumn >= 0) {
1226          if (typeof item.setCursorBufferPosition === 'function') {
1227            item.setCursorBufferPosition([initialLine, initialColumn]);
1228          }
1229          if (typeof item.unfoldBufferRow === 'function') {
1230            item.unfoldBufferRow(initialLine);
1231          }
1232          if (typeof item.scrollToBufferPosition === 'function') {
1233            item.scrollToBufferPosition([initialLine, initialColumn], {
1234              center: true
1235            });
1236          }
1237        }
1238  
1239        const index = pane.getActiveItemIndex();
1240        this.emitter.emit('did-open', { uri, pane, item, index });
1241        if (uri) {
1242          this.incoming.delete(uri);
1243        }
1244      } finally {
1245        resolveItem();
1246      }
1247      return item;
1248    }
1249  
1250    // Essential: Search the workspace for items matching the given URI and hide them.
1251    //
1252    // * `itemOrURI` The item to hide or a {String} containing the URI
1253    //   of the item to hide.
1254    //
1255    // Returns a {Boolean} indicating whether any items were found (and hidden).
1256    hide(itemOrURI) {
1257      let foundItems = false;
1258  
1259      // If any visible item has the given URI, hide it
1260      for (const container of this.getPaneContainers()) {
1261        const isCenter = container === this.getCenter();
1262        if (isCenter || container.isVisible()) {
1263          for (const pane of container.getPanes()) {
1264            const activeItem = pane.getActiveItem();
1265            const foundItem =
1266              activeItem != null &&
1267              (activeItem === itemOrURI ||
1268                (typeof activeItem.getURI === 'function' &&
1269                  activeItem.getURI() === itemOrURI));
1270            if (foundItem) {
1271              foundItems = true;
1272              // We can't really hide the center so we just destroy the item.
1273              if (isCenter) {
1274                pane.destroyItem(activeItem);
1275              } else {
1276                container.hide();
1277              }
1278            }
1279          }
1280        }
1281      }
1282  
1283      return foundItems;
1284    }
1285  
1286    // Essential: Search the workspace for items matching the given URI. If any are found, hide them.
1287    // Otherwise, open the URL.
1288    //
1289    // * `itemOrURI` (optional) The item to toggle or a {String} containing the URI
1290    //   of the item to toggle.
1291    //
1292    // Returns a Promise that resolves when the item is shown or hidden.
1293    toggle(itemOrURI) {
1294      if (this.hide(itemOrURI)) {
1295        return Promise.resolve();
1296      } else {
1297        return this.open(itemOrURI, { searchAllPanes: true });
1298      }
1299    }
1300  
1301    // Open Atom's license in the active pane.
1302    openLicense() {
1303      return this.open(path.join(process.resourcesPath, 'LICENSE.md'));
1304    }
1305  
1306    // Synchronously open the given URI in the active pane. **Only use this method
1307    // in specs. Calling this in production code will block the UI thread and
1308    // everyone will be mad at you.**
1309    //
1310    // * `uri` A {String} containing a URI.
1311    // * `options` An optional options {Object}
1312    //   * `initialLine` A {Number} indicating which row to move the cursor to
1313    //     initially. Defaults to `0`.
1314    //   * `initialColumn` A {Number} indicating which column to move the cursor to
1315    //     initially. Defaults to `0`.
1316    //   * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on
1317    //     the containing pane. Defaults to `true`.
1318    //   * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem}
1319    //     on containing pane. Defaults to `true`.
1320    openSync(uri_ = '', options = {}) {
1321      const { initialLine, initialColumn } = options;
1322      const activatePane =
1323        options.activatePane != null ? options.activatePane : true;
1324      const activateItem =
1325        options.activateItem != null ? options.activateItem : true;
1326  
1327      const uri = this.project.resolvePath(uri_);
1328      let item = this.getActivePane().itemForURI(uri);
1329      if (uri && item == null) {
1330        for (const opener of this.getOpeners()) {
1331          item = opener(uri, options);
1332          if (item) break;
1333        }
1334      }
1335      if (item == null) {
1336        item = this.project.openSync(uri, { initialLine, initialColumn });
1337      }
1338  
1339      if (activateItem) {
1340        this.getActivePane().activateItem(item);
1341      }
1342      this.itemOpened(item);
1343      if (activatePane) {
1344        this.getActivePane().activate();
1345      }
1346      return item;
1347    }
1348  
1349    openURIInPane(uri, pane) {
1350      return this.open(uri, { pane });
1351    }
1352  
1353    // Public: Creates a new item that corresponds to the provided URI.
1354    //
1355    // If no URI is given, or no registered opener can open the URI, a new empty
1356    // {TextEditor} will be created.
1357    //
1358    // * `uri` A {String} containing a URI.
1359    //
1360    // Returns a {Promise} that resolves to the {TextEditor} (or other item) for the given URI.
1361    async createItemForURI(uri, options) {
1362      if (uri != null) {
1363        for (const opener of this.getOpeners()) {
1364          const item = opener(uri, options);
1365          if (item != null) return item;
1366        }
1367      }
1368  
1369      try {
1370        const item = await this.openTextFile(uri, options);
1371        return item;
1372      } catch (error) {
1373        switch (error.code) {
1374          case 'CANCELLED':
1375            return Promise.resolve();
1376          case 'EACCES':
1377            this.notificationManager.addWarning(
1378              `Permission denied '${error.path}'`
1379            );
1380            return Promise.resolve();
1381          case 'EPERM':
1382          case 'EBUSY':
1383          case 'ENXIO':
1384          case 'EIO':
1385          case 'ENOTCONN':
1386          case 'UNKNOWN':
1387          case 'ECONNRESET':
1388          case 'EINVAL':
1389          case 'EMFILE':
1390          case 'ENOTDIR':
1391          case 'EAGAIN':
1392            this.notificationManager.addWarning(
1393              `Unable to open '${error.path != null ? error.path : uri}'`,
1394              { detail: error.message }
1395            );
1396            return Promise.resolve();
1397          default:
1398            throw error;
1399        }
1400      }
1401    }
1402  
1403    async openTextFile(uri, options) {
1404      const filePath = this.project.resolvePath(uri);
1405  
1406      if (filePath != null) {
1407        try {
1408          fs.closeSync(fs.openSync(filePath, 'r'));
1409        } catch (error) {
1410          // allow ENOENT errors to create an editor for paths that dont exist
1411          if (error.code !== 'ENOENT') {
1412            throw error;
1413          }
1414        }
1415      }
1416  
1417      const fileSize = fs.getSizeSync(filePath);
1418  
1419      if (fileSize >= this.config.get('core.warnOnLargeFileLimit') * 1048576) {
1420        // 40MB by default
1421        await new Promise((resolve, reject) => {
1422          this.applicationDelegate.confirm(
1423            {
1424              message:
1425                'Atom will be unresponsive during the loading of very large files.',
1426              detail: 'Do you still want to load this file?',
1427              buttons: ['Proceed', 'Cancel']
1428            },
1429            response => {
1430              if (response === 1) {
1431                const error = new Error();
1432                error.code = 'CANCELLED';
1433                reject(error);
1434              } else {
1435                resolve();
1436              }
1437            }
1438          );
1439        });
1440      }
1441  
1442      const buffer = await this.project.bufferForPath(filePath, options);
1443      return this.textEditorRegistry.build(
1444        Object.assign({ buffer, autoHeight: false }, options)
1445      );
1446    }
1447  
1448    handleGrammarUsed(grammar) {
1449      if (grammar == null) {
1450        return;
1451      }
1452      this.packageManager.triggerActivationHook(
1453        `${grammar.scopeName}:root-scope-used`
1454      );
1455      this.packageManager.triggerActivationHook(
1456        `${grammar.packageName}:grammar-used`
1457      );
1458    }
1459  
1460    // Public: Returns a {Boolean} that is `true` if `object` is a `TextEditor`.
1461    //
1462    // * `object` An {Object} you want to perform the check against.
1463    isTextEditor(object) {
1464      return object instanceof TextEditor;
1465    }
1466  
1467    // Extended: Create a new text editor.
1468    //
1469    // Returns a {TextEditor}.
1470    buildTextEditor(params) {
1471      const editor = this.textEditorRegistry.build(params);
1472      const subscription = this.textEditorRegistry.maintainConfig(editor);
1473      editor.onDidDestroy(() => subscription.dispose());
1474      return editor;
1475    }
1476  
1477    // Public: Asynchronously reopens the last-closed item's URI if it hasn't already been
1478    // reopened.
1479    //
1480    // Returns a {Promise} that is resolved when the item is opened
1481    reopenItem() {
1482      const uri = this.destroyedItemURIs.pop();
1483      if (uri) {
1484        return this.open(uri);
1485      } else {
1486        return Promise.resolve();
1487      }
1488    }
1489  
1490    // Public: Register an opener for a uri.
1491    //
1492    // When a URI is opened via {Workspace::open}, Atom loops through its registered
1493    // opener functions until one returns a value for the given uri.
1494    // Openers are expected to return an object that inherits from HTMLElement or
1495    // a model which has an associated view in the {ViewRegistry}.
1496    // A {TextEditor} will be used if no opener returns a value.
1497    //
1498    // ## Examples
1499    //
1500    // ```coffee
1501    // atom.workspace.addOpener (uri) ->
1502    //   if path.extname(uri) is '.toml'
1503    //     return new TomlEditor(uri)
1504    // ```
1505    //
1506    // * `opener` A {Function} to be called when a path is being opened.
1507    //
1508    // Returns a {Disposable} on which `.dispose()` can be called to remove the
1509    // opener.
1510    //
1511    // Note that the opener will be called if and only if the URI is not already open
1512    // in the current pane. The searchAllPanes flag expands the search from the
1513    // current pane to all panes. If you wish to open a view of a different type for
1514    // a file that is already open, consider changing the protocol of the URI. For
1515    // example, perhaps you wish to preview a rendered version of the file `/foo/bar/baz.quux`
1516    // that is already open in a text editor view. You could signal this by calling
1517    // {Workspace::open} on the URI `quux-preview://foo/bar/baz.quux`. Then your opener
1518    // can check the protocol for quux-preview and only handle those URIs that match.
1519    //
1520    // To defer your package's activation until a specific URL is opened, add a
1521    // `workspaceOpeners` field to your `package.json` containing an array of URL
1522    // strings.
1523    addOpener(opener) {
1524      this.openers.push(opener);
1525      return new Disposable(() => {
1526        _.remove(this.openers, opener);
1527      });
1528    }
1529  
1530    getOpeners() {
1531      return this.openers;
1532    }
1533  
1534    /*
1535    Section: Pane Items
1536    */
1537  
1538    // Essential: Get all pane items in the workspace.
1539    //
1540    // Returns an {Array} of items.
1541    getPaneItems() {
1542      return _.flatten(
1543        this.getPaneContainers().map(container => container.getPaneItems())
1544      );
1545    }
1546  
1547    // Essential: Get the active {Pane}'s active item.
1548    //
1549    // Returns an pane item {Object}.
1550    getActivePaneItem() {
1551      return this.getActivePaneContainer().getActivePaneItem();
1552    }
1553  
1554    // Essential: Get all text editors in the workspace.
1555    //
1556    // Returns an {Array} of {TextEditor}s.
1557    getTextEditors() {
1558      return this.getPaneItems().filter(item => item instanceof TextEditor);
1559    }
1560  
1561    // Essential: Get the workspace center's active item if it is a {TextEditor}.
1562    //
1563    // Returns a {TextEditor} or `undefined` if the workspace center's current
1564    // active item is not a {TextEditor}.
1565    getActiveTextEditor() {
1566      const activeItem = this.getCenter().getActivePaneItem();
1567      if (activeItem instanceof TextEditor) {
1568        return activeItem;
1569      }
1570    }
1571  
1572    // Save all pane items.
1573    saveAll() {
1574      this.getPaneContainers().forEach(container => {
1575        container.saveAll();
1576      });
1577    }
1578  
1579    confirmClose(options) {
1580      return Promise.all(
1581        this.getPaneContainers().map(container => container.confirmClose(options))
1582      ).then(results => !results.includes(false));
1583    }
1584  
1585    // Save the active pane item.
1586    //
1587    // If the active pane item currently has a URI according to the item's
1588    // `.getURI` method, calls `.save` on the item. Otherwise
1589    // {::saveActivePaneItemAs} # will be called instead. This method does nothing
1590    // if the active item does not implement a `.save` method.
1591    saveActivePaneItem() {
1592      return this.getCenter()
1593        .getActivePane()
1594        .saveActiveItem();
1595    }
1596  
1597    // Prompt the user for a path and save the active pane item to it.
1598    //
1599    // Opens a native dialog where the user selects a path on disk, then calls
1600    // `.saveAs` on the item with the selected path. This method does nothing if
1601    // the active item does not implement a `.saveAs` method.
1602    saveActivePaneItemAs() {
1603      this.getCenter()
1604        .getActivePane()
1605        .saveActiveItemAs();
1606    }
1607  
1608    // Destroy (close) the active pane item.
1609    //
1610    // Removes the active pane item and calls the `.destroy` method on it if one is
1611    // defined.
1612    destroyActivePaneItem() {
1613      return this.getActivePane().destroyActiveItem();
1614    }
1615  
1616    /*
1617    Section: Panes
1618    */
1619  
1620    // Extended: Get the most recently focused pane container.
1621    //
1622    // Returns a {Dock} or the {WorkspaceCenter}.
1623    getActivePaneContainer() {
1624      return this.activePaneContainer;
1625    }
1626  
1627    // Extended: Get all panes in the workspace.
1628    //
1629    // Returns an {Array} of {Pane}s.
1630    getPanes() {
1631      return _.flatten(
1632        this.getPaneContainers().map(container => container.getPanes())
1633      );
1634    }
1635  
1636    getVisiblePanes() {
1637      return _.flatten(
1638        this.getVisiblePaneContainers().map(container => container.getPanes())
1639      );
1640    }
1641  
1642    // Extended: Get the active {Pane}.
1643    //
1644    // Returns a {Pane}.
1645    getActivePane() {
1646      return this.getActivePaneContainer().getActivePane();
1647    }
1648  
1649    // Extended: Make the next pane active.
1650    activateNextPane() {
1651      return this.getActivePaneContainer().activateNextPane();
1652    }
1653  
1654    // Extended: Make the previous pane active.
1655    activatePreviousPane() {
1656      return this.getActivePaneContainer().activatePreviousPane();
1657    }
1658  
1659    // Extended: Get the first pane container that contains an item with the given
1660    // URI.
1661    //
1662    // * `uri` {String} uri
1663    //
1664    // Returns a {Dock}, the {WorkspaceCenter}, or `undefined` if no item exists
1665    // with the given URI.
1666    paneContainerForURI(uri) {
1667      return this.getPaneContainers().find(container =>
1668        container.paneForURI(uri)
1669      );
1670    }
1671  
1672    // Extended: Get the first pane container that contains the given item.
1673    //
1674    // * `item` the Item that the returned pane container must contain.
1675    //
1676    // Returns a {Dock}, the {WorkspaceCenter}, or `undefined` if no item exists
1677    // with the given URI.
1678    paneContainerForItem(uri) {
1679      return this.getPaneContainers().find(container =>
1680        container.paneForItem(uri)
1681      );
1682    }
1683  
1684    // Extended: Get the first {Pane} that contains an item with the given URI.
1685    //
1686    // * `uri` {String} uri
1687    //
1688    // Returns a {Pane} or `undefined` if no item exists with the given URI.
1689    paneForURI(uri) {
1690      for (let location of this.getPaneContainers()) {
1691        const pane = location.paneForURI(uri);
1692        if (pane != null) {
1693          return pane;
1694        }
1695      }
1696    }
1697  
1698    // Extended: Get the {Pane} containing the given item.
1699    //
1700    // * `item` the Item that the returned pane must contain.
1701    //
1702    // Returns a {Pane} or `undefined` if no pane exists for the given item.
1703    paneForItem(item) {
1704      for (let location of this.getPaneContainers()) {
1705        const pane = location.paneForItem(item);
1706        if (pane != null) {
1707          return pane;
1708        }
1709      }
1710    }
1711  
1712    // Destroy (close) the active pane.
1713    destroyActivePane() {
1714      const activePane = this.getActivePane();
1715      if (activePane != null) {
1716        activePane.destroy();
1717      }
1718    }
1719  
1720    // Close the active center pane item, or the active center pane if it is
1721    // empty, or the current window if there is only the empty root pane.
1722    closeActivePaneItemOrEmptyPaneOrWindow() {
1723      if (this.getCenter().getActivePaneItem() != null) {
1724        this.getCenter()
1725          .getActivePane()
1726          .destroyActiveItem();
1727      } else if (this.getCenter().getPanes().length > 1) {
1728        this.getCenter().destroyActivePane();
1729      } else if (this.config.get('core.closeEmptyWindows')) {
1730        atom.close();
1731      }
1732    }
1733  
1734    // Increase the editor font size by 1px.
1735    increaseFontSize() {
1736      this.config.set('editor.fontSize', this.config.get('editor.fontSize') + 1);
1737    }
1738  
1739    // Decrease the editor font size by 1px.
1740    decreaseFontSize() {
1741      const fontSize = this.config.get('editor.fontSize');
1742      if (fontSize > 1) {
1743        this.config.set('editor.fontSize', fontSize - 1);
1744      }
1745    }
1746  
1747    // Restore to the window's original editor font size.
1748    resetFontSize() {
1749      if (this.originalFontSize) {
1750        this.config.set('editor.fontSize', this.originalFontSize);
1751      }
1752    }
1753  
1754    // Removes the item's uri from the list of potential items to reopen.
1755    itemOpened(item) {
1756      let uri;
1757      if (typeof item.getURI === 'function') {
1758        uri = item.getURI();
1759      } else if (typeof item.getUri === 'function') {
1760        uri = item.getUri();
1761      }
1762  
1763      if (uri != null) {
1764        _.remove(this.destroyedItemURIs, uri);
1765      }
1766    }
1767  
1768    // Adds the destroyed item's uri to the list of items to reopen.
1769    didDestroyPaneItem({ item }) {
1770      let uri;
1771      if (typeof item.getURI === 'function') {
1772        uri = item.getURI();
1773      } else if (typeof item.getUri === 'function') {
1774        uri = item.getUri();
1775      }
1776  
1777      if (uri != null) {
1778        this.destroyedItemURIs.push(uri);
1779      }
1780    }
1781  
1782    // Called by Model superclass when destroyed
1783    destroyed() {
1784      this.paneContainers.center.destroy();
1785      this.paneContainers.left.destroy();
1786      this.paneContainers.right.destroy();
1787      this.paneContainers.bottom.destroy();
1788      this.cancelStoppedChangingActivePaneItemTimeout();
1789      if (this.activeItemSubscriptions != null) {
1790        this.activeItemSubscriptions.dispose();
1791      }
1792      if (this.element) this.element.destroy();
1793    }
1794  
1795    /*
1796    Section: Pane Locations
1797    */
1798  
1799    // Essential: Get the {WorkspaceCenter} at the center of the editor window.
1800    getCenter() {
1801      return this.paneContainers.center;
1802    }
1803  
1804    // Essential: Get the {Dock} to the left of the editor window.
1805    getLeftDock() {
1806      return this.paneContainers.left;
1807    }
1808  
1809    // Essential: Get the {Dock} to the right of the editor window.
1810    getRightDock() {
1811      return this.paneContainers.right;
1812    }
1813  
1814    // Essential: Get the {Dock} below the editor window.
1815    getBottomDock() {
1816      return this.paneContainers.bottom;
1817    }
1818  
1819    getPaneContainers() {
1820      return [
1821        this.paneContainers.center,
1822        this.paneContainers.left,
1823        this.paneContainers.right,
1824        this.paneContainers.bottom
1825      ];
1826    }
1827  
1828    getVisiblePaneContainers() {
1829      const center = this.getCenter();
1830      return atom.workspace
1831        .getPaneContainers()
1832        .filter(container => container === center || container.isVisible());
1833    }
1834  
1835    /*
1836    Section: Panels
1837  
1838    Panels are used to display UI related to an editor window. They are placed at one of the four
1839    edges of the window: left, right, top or bottom. If there are multiple panels on the same window
1840    edge they are stacked in order of priority: higher priority is closer to the center, lower
1841    priority towards the edge.
1842  
1843    *Note:* If your panel changes its size throughout its lifetime, consider giving it a higher
1844    priority, allowing fixed size panels to be closer to the edge. This allows control targets to
1845    remain more static for easier targeting by users that employ mice or trackpads. (See
1846    [atom/atom#4834](https://github.com/atom/atom/issues/4834) for discussion.)
1847    */
1848  
1849    // Essential: Get an {Array} of all the panel items at the bottom of the editor window.
1850    getBottomPanels() {
1851      return this.getPanels('bottom');
1852    }
1853  
1854    // Essential: Adds a panel item to the bottom of the editor window.
1855    //
1856    // * `options` {Object}
1857    //   * `item` Your panel content. It can be DOM element, a jQuery element, or
1858    //     a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the
1859    //     latter. See {ViewRegistry::addViewProvider} for more information.
1860    //   * `visible` (optional) {Boolean} false if you want the panel to initially be hidden
1861    //     (default: true)
1862    //   * `priority` (optional) {Number} Determines stacking order. Lower priority items are
1863    //     forced closer to the edges of the window. (default: 100)
1864    //
1865    // Returns a {Panel}
1866    addBottomPanel(options) {
1867      return this.addPanel('bottom', options);
1868    }
1869  
1870    // Essential: Get an {Array} of all the panel items to the left of the editor window.
1871    getLeftPanels() {
1872      return this.getPanels('left');
1873    }
1874  
1875    // Essential: Adds a panel item to the left of the editor window.
1876    //
1877    // * `options` {Object}
1878    //   * `item` Your panel content. It can be DOM element, a jQuery element, or
1879    //     a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the
1880    //     latter. See {ViewRegistry::addViewProvider} for more information.
1881    //   * `visible` (optional) {Boolean} false if you want the panel to initially be hidden
1882    //     (default: true)
1883    //   * `priority` (optional) {Number} Determines stacking order. Lower priority items are
1884    //     forced closer to the edges of the window. (default: 100)
1885    //
1886    // Returns a {Panel}
1887    addLeftPanel(options) {
1888      return this.addPanel('left', options);
1889    }
1890  
1891    // Essential: Get an {Array} of all the panel items to the right of the editor window.
1892    getRightPanels() {
1893      return this.getPanels('right');
1894    }
1895  
1896    // Essential: Adds a panel item to the right of the editor window.
1897    //
1898    // * `options` {Object}
1899    //   * `item` Your panel content. It can be DOM element, a jQuery element, or
1900    //     a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the
1901    //     latter. See {ViewRegistry::addViewProvider} for more information.
1902    //   * `visible` (optional) {Boolean} false if you want the panel to initially be hidden
1903    //     (default: true)
1904    //   * `priority` (optional) {Number} Determines stacking order. Lower priority items are
1905    //     forced closer to the edges of the window. (default: 100)
1906    //
1907    // Returns a {Panel}
1908    addRightPanel(options) {
1909      return this.addPanel('right', options);
1910    }
1911  
1912    // Essential: Get an {Array} of all the panel items at the top of the editor window.
1913    getTopPanels() {
1914      return this.getPanels('top');
1915    }
1916  
1917    // Essential: Adds a panel item to the top of the editor window above the tabs.
1918    //
1919    // * `options` {Object}
1920    //   * `item` Your panel content. It can be DOM element, a jQuery element, or
1921    //     a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the
1922    //     latter. See {ViewRegistry::addViewProvider} for more information.
1923    //   * `visible` (optional) {Boolean} false if you want the panel to initially be hidden
1924    //     (default: true)
1925    //   * `priority` (optional) {Number} Determines stacking order. Lower priority items are
1926    //     forced closer to the edges of the window. (default: 100)
1927    //
1928    // Returns a {Panel}
1929    addTopPanel(options) {
1930      return this.addPanel('top', options);
1931    }
1932  
1933    // Essential: Get an {Array} of all the panel items in the header.
1934    getHeaderPanels() {
1935      return this.getPanels('header');
1936    }
1937  
1938    // Essential: Adds a panel item to the header.
1939    //
1940    // * `options` {Object}
1941    //   * `item` Your panel content. It can be DOM element, a jQuery element, or
1942    //     a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the
1943    //     latter. See {ViewRegistry::addViewProvider} for more information.
1944    //   * `visible` (optional) {Boolean} false if you want the panel to initially be hidden
1945    //     (default: true)
1946    //   * `priority` (optional) {Number} Determines stacking order. Lower priority items are
1947    //     forced closer to the edges of the window. (default: 100)
1948    //
1949    // Returns a {Panel}
1950    addHeaderPanel(options) {
1951      return this.addPanel('header', options);
1952    }
1953  
1954    // Essential: Get an {Array} of all the panel items in the footer.
1955    getFooterPanels() {
1956      return this.getPanels('footer');
1957    }
1958  
1959    // Essential: Adds a panel item to the footer.
1960    //
1961    // * `options` {Object}
1962    //   * `item` Your panel content. It can be DOM element, a jQuery element, or
1963    //     a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the
1964    //     latter. See {ViewRegistry::addViewProvider} for more information.
1965    //   * `visible` (optional) {Boolean} false if you want the panel to initially be hidden
1966    //     (default: true)
1967    //   * `priority` (optional) {Number} Determines stacking order. Lower priority items are
1968    //     forced closer to the edges of the window. (default: 100)
1969    //
1970    // Returns a {Panel}
1971    addFooterPanel(options) {
1972      return this.addPanel('footer', options);
1973    }
1974  
1975    // Essential: Get an {Array} of all the modal panel items
1976    getModalPanels() {
1977      return this.getPanels('modal');
1978    }
1979  
1980    // Essential: Adds a panel item as a modal dialog.
1981    //
1982    // * `options` {Object}
1983    //   * `item` Your panel content. It can be a DOM element, a jQuery element, or
1984    //     a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the
1985    //     model option. See {ViewRegistry::addViewProvider} for more information.
1986    //   * `visible` (optional) {Boolean} false if you want the panel to initially be hidden
1987    //     (default: true)
1988    //   * `priority` (optional) {Number} Determines stacking order. Lower priority items are
1989    //     forced closer to the edges of the window. (default: 100)
1990    //   * `autoFocus` (optional) {Boolean|Element} true if you want modal focus managed for you by Atom.
1991    //     Atom will automatically focus on this element or your modal panel's first tabbable element when the modal
1992    //     opens and will restore the previously selected element when the modal closes. Atom will
1993    //     also automatically restrict user tab focus within your modal while it is open.
1994    //     (default: false)
1995    //
1996    // Returns a {Panel}
1997    addModalPanel(options = {}) {
1998      return this.addPanel('modal', options);
1999    }
2000  
2001    // Essential: Returns the {Panel} associated with the given item. Returns
2002    // `null` when the item has no panel.
2003    //
2004    // * `item` Item the panel contains
2005    panelForItem(item) {
2006      for (let location in this.panelContainers) {
2007        const container = this.panelContainers[location];
2008        const panel = container.panelForItem(item);
2009        if (panel != null) {
2010          return panel;
2011        }
2012      }
2013      return null;
2014    }
2015  
2016    getPanels(location) {
2017      return this.panelContainers[location].getPanels();
2018    }
2019  
2020    addPanel(location, options) {
2021      if (options == null) {
2022        options = {};
2023      }
2024      return this.panelContainers[location].addPanel(
2025        new Panel(options, this.viewRegistry)
2026      );
2027    }
2028  
2029    /*
2030    Section: Searching and Replacing
2031    */
2032  
2033    // Public: Performs a search across all files in the workspace.
2034    //
2035    // * `regex` {RegExp} to search with.
2036    // * `options` (optional) {Object}
2037    //   * `paths` An {Array} of glob patterns to search within.
2038    //   * `onPathsSearched` (optional) {Function} to be periodically called
2039    //     with number of paths searched.
2040    //   * `leadingContextLineCount` {Number} default `0`; The number of lines
2041    //      before the matched line to include in the results object.
2042    //   * `trailingContextLineCount` {Number} default `0`; The number of lines
2043    //      after the matched line to include in the results object.
2044    // * `iterator` {Function} callback on each file found.
2045    //
2046    // Returns a {Promise} with a `cancel()` method that will cancel all
2047    // of the underlying searches that were started as part of this scan.
2048    scan(regex, options = {}, iterator) {
2049      if (_.isFunction(options)) {
2050        iterator = options;
2051        options = {};
2052      }
2053  
2054      // Find a searcher for every Directory in the project. Each searcher that is matched
2055      // will be associated with an Array of Directory objects in the Map.
2056      const directoriesForSearcher = new Map();
2057      for (const directory of this.project.getDirectories()) {
2058        let searcher = options.ripgrep
2059          ? this.ripgrepDirectorySearcher
2060          : this.scandalDirectorySearcher;
2061        for (const directorySearcher of this.directorySearchers) {
2062          if (directorySearcher.canSearchDirectory(directory)) {
2063            searcher = directorySearcher;
2064            break;
2065          }
2066        }
2067        let directories = directoriesForSearcher.get(searcher);
2068        if (!directories) {
2069          directories = [];
2070          directoriesForSearcher.set(searcher, directories);
2071        }
2072        directories.push(directory);
2073      }
2074  
2075      // Define the onPathsSearched callback.
2076      let onPathsSearched;
2077      if (_.isFunction(options.onPathsSearched)) {
2078        // Maintain a map of directories to the number of search results. When notified of a new count,
2079        // replace the entry in the map and update the total.
2080        const onPathsSearchedOption = options.onPathsSearched;
2081        let totalNumberOfPathsSearched = 0;
2082        const numberOfPathsSearchedForSearcher = new Map();
2083        onPathsSearched = function(searcher, numberOfPathsSearched) {
2084          const oldValue = numberOfPathsSearchedForSearcher.get(searcher);
2085          if (oldValue) {
2086            totalNumberOfPathsSearched -= oldValue;
2087          }
2088          numberOfPathsSearchedForSearcher.set(searcher, numberOfPathsSearched);
2089          totalNumberOfPathsSearched += numberOfPathsSearched;
2090          return onPathsSearchedOption(totalNumberOfPathsSearched);
2091        };
2092      } else {
2093        onPathsSearched = function() {};
2094      }
2095  
2096      // Kick off all of the searches and unify them into one Promise.
2097      const allSearches = [];
2098      directoriesForSearcher.forEach((directories, searcher) => {
2099        const searchOptions = {
2100          inclusions: options.paths || [],
2101          includeHidden: true,
2102          excludeVcsIgnores: this.config.get('core.excludeVcsIgnoredPaths'),
2103          exclusions: this.config.get('core.ignoredNames'),
2104          follow: this.config.get('core.followSymlinks'),
2105          leadingContextLineCount: options.leadingContextLineCount || 0,
2106          trailingContextLineCount: options.trailingContextLineCount || 0,
2107          PCRE2: options.PCRE2,
2108          didMatch: result => {
2109            if (!this.project.isPathModified(result.filePath)) {
2110              return iterator(result);
2111            }
2112          },
2113          didError(error) {
2114            return iterator(null, error);
2115          },
2116          didSearchPaths(count) {
2117            return onPathsSearched(searcher, count);
2118          }
2119        };
2120        const directorySearcher = searcher.search(
2121          directories,
2122          regex,
2123          searchOptions
2124        );
2125        allSearches.push(directorySearcher);
2126      });
2127      const searchPromise = Promise.all(allSearches);
2128  
2129      for (let buffer of this.project.getBuffers()) {
2130        if (buffer.isModified()) {
2131          const filePath = buffer.getPath();
2132          if (!this.project.contains(filePath)) {
2133            continue;
2134          }
2135          var matches = [];
2136          buffer.scan(regex, match => matches.push(match));
2137          if (matches.length > 0) {
2138            iterator({ filePath, matches });
2139          }
2140        }
2141      }
2142  
2143      // Make sure the Promise that is returned to the client is cancelable. To be consistent
2144      // with the existing behavior, instead of cancel() rejecting the promise, it should
2145      // resolve it with the special value 'cancelled'. At least the built-in find-and-replace
2146      // package relies on this behavior.
2147      let isCancelled = false;
2148      const cancellablePromise = new Promise((resolve, reject) => {
2149        const onSuccess = function() {
2150          if (isCancelled) {
2151            resolve('cancelled');
2152          } else {
2153            resolve(null);
2154          }
2155        };
2156  
2157        const onFailure = function(error) {
2158          for (let promise of allSearches) {
2159            promise.cancel();
2160          }
2161          reject(error);
2162        };
2163  
2164        searchPromise.then(onSuccess, onFailure);
2165      });
2166      cancellablePromise.cancel = () => {
2167        isCancelled = true;
2168        // Note that cancelling all of the members of allSearches will cause all of the searches
2169        // to resolve, which causes searchPromise to resolve, which is ultimately what causes
2170        // cancellablePromise to resolve.
2171        allSearches.map(promise => promise.cancel());
2172      };
2173  
2174      return cancellablePromise;
2175    }
2176  
2177    // Public: Performs a replace across all the specified files in the project.
2178    //
2179    // * `regex` A {RegExp} to search with.
2180    // * `replacementText` {String} to replace all matches of regex with.
2181    // * `filePaths` An {Array} of file path strings to run the replace on.
2182    // * `iterator` A {Function} callback on each file with replacements:
2183    //   * `options` {Object} with keys `filePath` and `replacements`.
2184    //
2185    // Returns a {Promise}.
2186    replace(regex, replacementText, filePaths, iterator) {
2187      return new Promise((resolve, reject) => {
2188        let buffer;
2189        const openPaths = this.project
2190          .getBuffers()
2191          .map(buffer => buffer.getPath());
2192        const outOfProcessPaths = _.difference(filePaths, openPaths);
2193  
2194        let inProcessFinished = !openPaths.length;
2195        let outOfProcessFinished = !outOfProcessPaths.length;
2196        const checkFinished = () => {
2197          if (outOfProcessFinished && inProcessFinished) {
2198            resolve();
2199          }
2200        };
2201  
2202        if (!outOfProcessFinished.length) {
2203          let flags = 'g';
2204          if (regex.multiline) {
2205            flags += 'm';
2206          }
2207          if (regex.ignoreCase) {
2208            flags += 'i';
2209          }
2210  
2211          const task = Task.once(
2212            require.resolve('./replace-handler'),
2213            outOfProcessPaths,
2214            regex.source,
2215            flags,
2216            replacementText,
2217            () => {
2218              outOfProcessFinished = true;
2219              checkFinished();
2220            }
2221          );
2222  
2223          task.on('replace:path-replaced', iterator);
2224          task.on('replace:file-error', error => {
2225            iterator(null, error);
2226          });
2227        }
2228  
2229        for (buffer of this.project.getBuffers()) {
2230          if (!filePaths.includes(buffer.getPath())) {
2231            continue;
2232          }
2233          const replacements = buffer.replace(regex, replacementText, iterator);
2234          if (replacements) {
2235            iterator({ filePath: buffer.getPath(), replacements });
2236          }
2237        }
2238  
2239        inProcessFinished = true;
2240        checkFinished();
2241      });
2242    }
2243  
2244    checkoutHeadRevision(editor) {
2245      if (editor.getPath()) {
2246        const checkoutHead = async () => {
2247          const repository = await this.project.repositoryForDirectory(
2248            new Directory(editor.getDirectoryPath())
2249          );
2250          if (repository) repository.checkoutHeadForEditor(editor);
2251        };
2252  
2253        if (this.config.get('editor.confirmCheckoutHeadRevision')) {
2254          this.applicationDelegate.confirm(
2255            {
2256              message: 'Confirm Checkout HEAD Revision',
2257              detail: `Are you sure you want to discard all changes to "${editor.getFileName()}" since the last Git commit?`,
2258              buttons: ['OK', 'Cancel']
2259            },
2260            response => {
2261              if (response === 0) checkoutHead();
2262            }
2263          );
2264        } else {
2265          checkoutHead();
2266        }
2267      }
2268    }
2269  };