{deprecate} = require 'grim'
_ = require 'underscore-plus'
path = require 'path'
{join} = path
{Model} = require 'theorist'
Q = require 'q'
Serializable = require 'serializable'
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
Grim = require 'grim'
fs = require 'fs-plus'
StackTraceParser = require 'stacktrace-parser'
TextEditor = require './text-editor'
PaneContainer = require './pane-container'
Pane = require './pane'
Panel = require './panel'
PanelElement = require './panel-element'
PanelContainer = require './panel-container'
PanelContainerElement = require './panel-container-element'
WorkspaceElement = require './workspace-element'
Task = require './task'

# Essential: Represents the state of the user interface for the entire window.
# An instance of this class is available via the `atom.workspace` global.
#
# Interact with this object to open files, be notified of current and future
# editors, and manipulate panes. To add panels, you'll need to use the
# {WorkspaceView} class for now until we establish APIs at the model layer.
#
# * `editor` {TextEditor} the new editor
#
module.exports =
class Workspace extends Model
  atom.deserializers.add(this)
  Serializable.includeInto(this)

  Object.defineProperty @::, 'activePaneItem',
    get: ->
      Grim.deprecate "Use ::getActivePaneItem() instead of the ::activePaneItem property"
      @getActivePaneItem()

  Object.defineProperty @::, 'activePane',
    get: ->
      Grim.deprecate "Use ::getActivePane() instead of the ::activePane property"
      @getActivePane()

  @properties
    paneContainer: null
    fullScreen: false
    destroyedItemURIs: -> []

  constructor: (params) ->
    super

    @emitter = new Emitter
    @openers = []

    @paneContainer ?= new PaneContainer()
    @paneContainer.onDidDestroyPaneItem(@didDestroyPaneItem)

    @panelContainers =
      top: new PanelContainer({location: 'top'})
      left: new PanelContainer({location: 'left'})
      right: new PanelContainer({location: 'right'})
      bottom: new PanelContainer({location: 'bottom'})
      modal: new PanelContainer({location: 'modal'})

    @subscribeToActiveItem()

    @addOpener (filePath) =>
      switch filePath
        when 'atom://.atom/stylesheet'
          @open(atom.styles.getUserStyleSheetPath())
        when 'atom://.atom/keymap'
          @open(atom.keymaps.getUserKeymapPath())
        when 'atom://.atom/config'
          @open(atom.config.getUserConfigPath())
        when 'atom://.atom/init-script'
          @open(atom.getUserInitScriptPath())

    atom.views.addViewProvider Workspace, (model) ->
      new WorkspaceElement().initialize(model)

    atom.views.addViewProvider PanelContainer, (model) ->
      new PanelContainerElement().initialize(model)

    atom.views.addViewProvider Panel, (model) ->
      new PanelElement().initialize(model)

  # Called by the Serializable mixin during deserialization
  deserializeParams: (params) ->
    for packageName in params.packagesWithActiveGrammars ? []
      atom.packages.getLoadedPackage(packageName)?.loadGrammarsSync()

    params.paneContainer = PaneContainer.deserialize(params.paneContainer)
    params

  # Called by the Serializable mixin during serialization.
  serializeParams: ->
    paneContainer: @paneContainer.serialize()
    fullScreen: atom.isFullScreen()
    packagesWithActiveGrammars: @getPackageNamesWithActiveGrammars()

  getPackageNamesWithActiveGrammars: ->
    packageNames = []
    addGrammar = ({includedGrammarScopes, packageName}={}) ->
      return unless packageName
      # Prevent cycles
      return if packageNames.indexOf(packageName) isnt -1

      packageNames.push(packageName)
      for scopeName in includedGrammarScopes ? []
        addGrammar(atom.grammars.grammarForScopeName(scopeName))

    editors = @getTextEditors()
    addGrammar(editor.getGrammar()) for editor in editors

    if editors.length > 0
      for grammar in atom.grammars.getGrammars() when grammar.injectionSelector
        addGrammar(grammar)

    _.uniq(packageNames)

  editorAdded: (editor) ->
    @emit 'editor-created', editor

  installShellCommands: ->
    require('./command-installer').installShellCommandsInteractively()

  subscribeToActiveItem: ->
    @updateWindowTitle()
    @updateDocumentEdited()
    atom.project.onDidChangePaths @updateWindowTitle

    @observeActivePaneItem (item) =>
      @updateWindowTitle()
      @updateDocumentEdited()

      @activeItemSubscriptions?.dispose()
      @activeItemSubscriptions = new CompositeDisposable

      if typeof item?.onDidChangeTitle is 'function'
        titleSubscription = item.onDidChangeTitle(@updateWindowTitle)
      else if typeof item?.on is 'function'
        titleSubscription = item.on('title-changed', @updateWindowTitle)
        unless typeof titleSubscription?.dispose is 'function'
          titleSubscription = new Disposable => item.off('title-changed', @updateWindowTitle)

      if typeof item?.onDidChangeModified is 'function'
        modifiedSubscription = item.onDidChangeModified(@updateDocumentEdited)
      else if typeof item?.on? is 'function'
        modifiedSubscription = item.on('modified-status-changed', @updateDocumentEdited)
        unless typeof modifiedSubscription?.dispose is 'function'
          modifiedSubscription = new Disposable => item.off('modified-status-changed', @updateDocumentEdited)

      @activeItemSubscriptions.add(titleSubscription) if titleSubscription?
      @activeItemSubscriptions.add(modifiedSubscription) if modifiedSubscription?

  # Updates the application's title and proxy icon based on whichever file is
  # open.
  updateWindowTitle: =>
    appName = 'Atom'
    if projectPath = atom.project?.getPaths()[0]
      if item = @getActivePaneItem()
        document.title = "#{item.getTitle?() ? 'untitled'} - #{projectPath} - #{appName}"
        atom.setRepresentedFilename(item.getPath?() ? projectPath)
      else
        document.title = "#{projectPath} - #{appName}"
        atom.setRepresentedFilename(projectPath)
    else
      document.title = "untitled - #{appName}"
      atom.setRepresentedFilename('')

  # On OS X, fades the application window's proxy icon when the current file
  # has been modified.
  updateDocumentEdited: =>
    modified = @getActivePaneItem()?.isModified?() ? false
    atom.setDocumentEdited(modified)

  ###
  Section: Event Subscription
  ###

  # Essential: Invoke the given callback with all current and future text
  # editors in the workspace.
  #
  # * `callback` {Function} to be called with current and future text editors.
  #   * `editor` An {TextEditor} that is present in {::getTextEditors} at the time
  #     of subscription or that is added at some later time.
  #
  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  observeTextEditors: (callback) ->
    callback(textEditor) for textEditor in @getTextEditors()
    @onDidAddTextEditor ({textEditor}) -> callback(textEditor)

  # Essential: Invoke the given callback with all current and future panes items
  # in the workspace.
  #
  # * `callback` {Function} to be called with current and future pane items.
  #   * `item` An item that is present in {::getPaneItems} at the time of
  #      subscription or that is added at some later time.
  #
  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  observePaneItems: (callback) -> @paneContainer.observePaneItems(callback)

  # Essential: Invoke the given callback when the active pane item changes.
  #
  # * `callback` {Function} to be called when the active pane item changes.
  #   * `item` The active pane item.
  #
  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidChangeActivePaneItem: (callback) -> @paneContainer.onDidChangeActivePaneItem(callback)

  # Essential: Invoke the given callback with the current active pane item and
  # with all future active pane items in the workspace.
  #
  # * `callback` {Function} to be called when the active pane item changes.
  #   * `item` The current active pane item.
  #
  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  observeActivePaneItem: (callback) -> @paneContainer.observeActivePaneItem(callback)

  # Essential: Invoke the given callback whenever an item is opened. Unlike
  # {::onDidAddPaneItem}, observers will be notified for items that are already
  # present in the workspace when they are reopened.
  #
  # * `callback` {Function} to be called whenever an item is opened.
  #   * `event` {Object} with the following keys:
  #     * `uri` {String} representing the opened URI. Could be `undefined`.
  #     * `item` The opened item.
  #     * `pane` The pane in which the item was opened.
  #     * `index` The index of the opened item on its pane.
  #
  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidOpen: (callback) ->
    @emitter.on 'did-open', callback

  # Extended: Invoke the given callback when a pane is added to the workspace.
  #
  # * `callback` {Function} to be called panes are added.
  #   * `event` {Object} with the following keys:
  #     * `pane` The added pane.
  #
  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidAddPane: (callback) -> @paneContainer.onDidAddPane(callback)

  # Extended: Invoke the given callback when a pane is destroyed in the
  # workspace.
  #
  # * `callback` {Function} to be called panes are destroyed.
  #   * `event` {Object} with the following keys:
  #     * `pane` The destroyed pane.
  #
  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidDestroyPane: (callback) -> @paneContainer.onDidDestroyPane(callback)

  # Extended: Invoke the given callback with all current and future panes in the
  # workspace.
  #
  # * `callback` {Function} to be called with current and future panes.
  #   * `pane` A {Pane} that is present in {::getPanes} at the time of
  #      subscription or that is added at some later time.
  #
  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  observePanes: (callback) -> @paneContainer.observePanes(callback)

  # Extended: Invoke the given callback when the active pane changes.
  #
  # * `callback` {Function} to be called when the active pane changes.
  #   * `pane` A {Pane} that is the current return value of {::getActivePane}.
  #
  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidChangeActivePane: (callback) -> @paneContainer.onDidChangeActivePane(callback)

  # Extended: Invoke the given callback with the current active pane and when
  # the active pane changes.
  #
  # * `callback` {Function} to be called with the current and future active#
  #   panes.
  #   * `pane` A {Pane} that is the current return value of {::getActivePane}.
  #
  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  observeActivePane: (callback) -> @paneContainer.observeActivePane(callback)

  # Extended: Invoke the given callback when a pane item is added to the
  # workspace.
  #
  # * `callback` {Function} to be called when pane items are added.
  #   * `event` {Object} with the following keys:
  #     * `item` The added pane item.
  #     * `pane` {Pane} containing the added item.
  #     * `index` {Number} indicating the index of the added item in its pane.
  #
  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidAddPaneItem: (callback) -> @paneContainer.onDidAddPaneItem(callback)

  # Extended: Invoke the given callback when a pane item is about to be
  # destroyed, before the user is prompted to save it.
  #
  # * `callback` {Function} to be called before pane items are destroyed.
  #   * `event` {Object} with the following keys:
  #     * `item` The item to be destroyed.
  #     * `pane` {Pane} containing the item to be destroyed.
  #     * `index` {Number} indicating the index of the item to be destroyed in
  #       its pane.
  #
  # Returns a {Disposable} on which `.dispose` can be called to unsubscribe.
  onWillDestroyPaneItem: (callback) -> @paneContainer.onWillDestroyPaneItem(callback)

  # Extended: Invoke the given callback when a pane item is destroyed.
  #
  # * `callback` {Function} to be called when pane items are destroyed.
  #   * `event` {Object} with the following keys:
  #     * `item` The destroyed item.
  #     * `pane` {Pane} containing the destroyed item.
  #     * `index` {Number} indicating the index of the destroyed item in its
  #       pane.
  #
  # Returns a {Disposable} on which `.dispose` can be called to unsubscribe.
  onDidDestroyPaneItem: (callback) -> @paneContainer.onDidDestroyPaneItem(callback)

  # Extended: Invoke the given callback when a text editor is added to the
  # workspace.
  #
  # * `callback` {Function} to be called panes are added.
  #   * `event` {Object} with the following keys:
  #     * `textEditor` {TextEditor} that was added.
  #     * `pane` {Pane} containing the added text editor.
  #     * `index` {Number} indicating the index of the added text editor in its
  #        pane.
  #
  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidAddTextEditor: (callback) ->
    @onDidAddPaneItem ({item, pane, index}) ->
      callback({textEditor: item, pane, index}) if item instanceof TextEditor

  eachEditor: (callback) ->
    deprecate("Use Workspace::observeTextEditors instead")

    callback(editor) for editor in @getEditors()
    @subscribe this, 'editor-created', (editor) -> callback(editor)

  getEditors: ->
    deprecate("Use Workspace::getTextEditors instead")

    editors = []
    for pane in @paneContainer.getPanes()
      editors.push(item) for item in pane.getItems() when item instanceof TextEditor

    editors

  on: (eventName) ->
    switch eventName
      when 'editor-created'
        deprecate("Use Workspace::onDidAddTextEditor or Workspace::observeTextEditors instead.")
      when 'uri-opened'
        deprecate("Use Workspace::onDidOpen or Workspace::onDidAddPaneItem instead. https://atom.io/docs/api/latest/Workspace#instance-onDidOpen")
      else
        deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead.")

    super

  ###
  Section: Opening
  ###

  # Essential: Open a given a URI in Atom asynchronously.
  #
  # * `uri` A {String} containing a URI.
  # * `options` (optional) {Object}
  #   * `initialLine` A {Number} indicating which row to move the cursor to
  #     initially. Defaults to `0`.
  #   * `initialColumn` A {Number} indicating which column to move the cursor to
  #     initially. Defaults to `0`.
  #   * `split` Either 'left' or 'right'. If 'left', the item will be opened in
  #     leftmost pane of the current active pane's row. If 'right', the
  #     item will be opened in the rightmost pane of the current active pane's row.
  #   * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on
  #     containing pane. Defaults to `true`.
  #   * `searchAllPanes` A {Boolean}. If `true`, the workspace will attempt to
  #     activate an existing item for the given URI on any pane.
  #     If `false`, only the active pane will be searched for
  #     an existing item for the same URI. Defaults to `false`.
  #
  # Returns a promise that resolves to the {TextEditor} for the file URI.
  open: (uri, options={}) ->
    searchAllPanes = options.searchAllPanes
    split = options.split
    uri = atom.project.resolvePath(uri)

    pane = @paneContainer.paneForURI(uri) if searchAllPanes
    pane ?= switch split
      when 'left'
        @getActivePane().findLeftmostSibling()
      when 'right'
        @getActivePane().findOrCreateRightmostSibling()
      else
        @getActivePane()

    @openURIInPane(uri, pane, options)

  # Open Atom's license in the active pane.
  openLicense: ->
    @open(join(atom.getLoadSettings().resourcePath, 'LICENSE.md'))

  # Synchronously open the given URI in the active pane. **Only use this method
  # in specs. Calling this in production code will block the UI thread and
  # everyone will be mad at you.**
  #
  # * `uri` A {String} containing a URI.
  # * `options` An optional options {Object}
  #   * `initialLine` A {Number} indicating which row to move the cursor to
  #     initially. Defaults to `0`.
  #   * `initialColumn` A {Number} indicating which column to move the cursor to
  #     initially. Defaults to `0`.
  #   * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on
  #     the containing pane. Defaults to `true`.
  openSync: (uri='', options={}) ->
    # TODO: Remove deprecated changeFocus option
    if options.changeFocus?
      deprecate("The `changeFocus` option has been renamed to `activatePane`")
      options.activatePane = options.changeFocus
      delete options.changeFocus

    {initialLine, initialColumn} = options
    activatePane = options.activatePane ? true

    uri = atom.project.resolvePath(uri)
    item = @getActivePane().itemForURI(uri)
    if uri
      item ?= opener(uri, options) for opener in @getOpeners() when !item
    item ?= atom.project.openSync(uri, {initialLine, initialColumn})

    @getActivePane().activateItem(item)
    @itemOpened(item)
    @getActivePane().activate() if activatePane
    item

  openURIInPane: (uri, pane, options={}) ->
    # TODO: Remove deprecated changeFocus option
    if options.changeFocus?
      deprecate("The `changeFocus` option has been renamed to `activatePane`")
      options.activatePane = options.changeFocus
      delete options.changeFocus

    activatePane = options.activatePane ? true

    if uri?
      item = pane.itemForURI(uri)
      item ?= opener(uri, options) for opener in @getOpeners() when !item

    try
      item ?= atom.project.open(uri, options)
    catch error
      switch error.code
        when 'EFILETOOLARGE'
          atom.notifications.addWarning("#{error.message} Large file support is being tracked at [atom/atom#307](https://github.com/atom/atom/issues/307).")
        when 'EACCES'
          atom.notifications.addWarning("Permission denied '#{error.path}'")
        when 'EPERM', 'EBUSY'
          atom.notifications.addWarning("Unable to open '#{error.path}'", detail: error.message)
        else
          throw error
      return Q()

    Q(item)
      .then (item) =>
        if not pane
          pane = new Pane(items: [item])
          @paneContainer.root = pane
        @itemOpened(item)
        pane.activateItem(item)
        pane.activate() if activatePane
        if options.initialLine? or options.initialColumn?
          item.setCursorBufferPosition?([options.initialLine, options.initialColumn])
        index = pane.getActiveItemIndex()
        @emit "uri-opened"
        @emitter.emit 'did-open', {uri, pane, item, index}
        item

  # Public: Asynchronously reopens the last-closed item's URI if it hasn't already been
  # reopened.
  #
  # Returns a promise that is resolved when the item is opened
  reopenItem: ->
    if uri = @destroyedItemURIs.pop()
      @open(uri)
    else
      Q()

  # Deprecated
  reopenItemSync: ->
    deprecate("Use Workspace::reopenItem instead")
    if uri = @destroyedItemURIs.pop()
      @openSync(uri)

  # Public: Register an opener for a uri.
  #
  # An {TextEditor} will be used if no openers return a value.
  #
  # ## Examples
  #
  # ```coffee
  # atom.workspace.addOpener (uri) ->
  #   if path.extname(uri) is '.toml'
  #     return new TomlEditor(uri)
  # ```
  #
  # * `opener` A {Function} to be called when a path is being opened.
  #
  # Returns a {Disposable} on which `.dispose()` can be called to remove the
  # opener.
  addOpener: (opener) ->
    packageName = @getCallingPackageName()

    wrappedOpener = (uri, options) ->
      item = opener(uri, options)
      if item? and typeof item.getUri is 'function' and typeof item.getURI isnt 'function'
        Grim.deprecate("Pane item with class `#{item.constructor.name}` should implement `::getURI` instead of `::getUri`.", {packageName})
      item

    @openers.push(wrappedOpener)
    new Disposable => _.remove(@openers, wrappedOpener)

  registerOpener: (opener) ->
    Grim.deprecate("Call Workspace::addOpener instead")
    @addOpener(opener)

  unregisterOpener: (opener) ->
    Grim.deprecate("Call .dispose() on the Disposable returned from ::addOpener instead")
    _.remove(@openers, opener)

  getOpeners: ->
    @openers

  getCallingPackageName: ->
    error = new Error
    Error.captureStackTrace(error)
    stack = StackTraceParser.parse(error.stack)

    packagePaths = @getPackagePathsByPackageName()

    for i in [0...stack.length]
      stackFramePath = stack[i].file

      # Empty when it was run from the dev console
      return unless stackFramePath

      for packageName, packagePath of packagePaths
        continue if stackFramePath is 'node.js'
        relativePath = path.relative(packagePath, stackFramePath)
        return packageName unless /^\.\./.test(relativePath)
    return

  getPackagePathsByPackageName: ->
    packagePathsByPackageName = {}
    for pack in atom.packages.getLoadedPackages()
      packagePath = pack.path
      if packagePath.indexOf('.atom/dev/packages') > -1 or packagePath.indexOf('.atom/packages') > -1
        packagePath = fs.realpathSync(packagePath)
      packagePathsByPackageName[pack.name] = packagePath
    packagePathsByPackageName

  ###
  Section: Pane Items
  ###

  # Essential: Get all pane items in the workspace.
  #
  # Returns an {Array} of items.
  getPaneItems: ->
    @paneContainer.getPaneItems()

  # Essential: Get the active {Pane}'s active item.
  #
  # Returns an pane item {Object}.
  getActivePaneItem: ->
    @paneContainer.getActivePaneItem()

  # Essential: Get all text editors in the workspace.
  #
  # Returns an {Array} of {TextEditor}s.
  getTextEditors: ->
    @getPaneItems().filter (item) -> item instanceof TextEditor

  # Essential: Get the active item if it is an {TextEditor}.
  #
  # Returns an {TextEditor} or `undefined` if the current active item is not an
  # {TextEditor}.
  getActiveTextEditor: ->
    activeItem = @getActivePaneItem()
    activeItem if activeItem instanceof TextEditor

  # Deprecated
  getActiveEditor: ->
    Grim.deprecate "Call ::getActiveTextEditor instead"
    @getActivePane()?.getActiveEditor()

  # Save all pane items.
  saveAll: ->
    @paneContainer.saveAll()

  confirmClose: ->
    @paneContainer.confirmClose()

  # Save the active pane item.
  #
  # If the active pane item currently has a URI according to the item's
  # `.getURI` method, calls `.save` on the item. Otherwise
  # {::saveActivePaneItemAs} # will be called instead. This method does nothing
  # if the active item does not implement a `.save` method.
  saveActivePaneItem: ->
    @saveActivePaneItemAndReportErrors('saveActiveItem')

  # Prompt the user for a path and save the active pane item to it.
  #
  # Opens a native dialog where the user selects a path on disk, then calls
  # `.saveAs` on the item with the selected path. This method does nothing if
  # the active item does not implement a `.saveAs` method.
  saveActivePaneItemAs: ->
    @saveActivePaneItemAndReportErrors('saveActiveItemAs')

  saveActivePaneItemAndReportErrors: (method) ->
    try
      @getActivePane()[method]()
    catch error
      if error.message.endsWith('is a directory')
        atom.notifications.addWarning("Unable to save file: #{error.message}")
      else if error.code is 'EACCES' and error.path?
        atom.notifications.addWarning("Unable to save file: Permission denied '#{error.path}'")
      else if error.code is 'EPERM' and error.path?
        atom.notifications.addWarning("Unable to save file '#{error.path}'", detail: error.message)
      else if error.code is 'EBUSY' and error.path?
        atom.notifications.addWarning("Unable to save file '#{error.path}'", detail: error.message)
      else if error.code is 'EROFS' and error.path?
        atom.notifications.addWarning("Unable to save file: Read-only file system '#{error.path}'")
      else if errorMatch = /ENOTDIR, not a directory '([^']+)'/.exec(error.message)
        fileName = errorMatch[1]
        atom.notifications.addWarning("Unable to save file: A directory in the path '#{fileName}' could not be written to")
      else
        throw error

  # Destroy (close) the active pane item.
  #
  # Removes the active pane item and calls the `.destroy` method on it if one is
  # defined.
  destroyActivePaneItem: ->
    @getActivePane().destroyActiveItem()

  ###
  Section: Panes
  ###

  # Extended: Get all panes in the workspace.
  #
  # Returns an {Array} of {Pane}s.
  getPanes: ->
    @paneContainer.getPanes()

  # Extended: Get the active {Pane}.
  #
  # Returns a {Pane}.
  getActivePane: ->
    @paneContainer.getActivePane()

  # Extended: Make the next pane active.
  activateNextPane: ->
    @paneContainer.activateNextPane()

  # Extended: Make the previous pane active.
  activatePreviousPane: ->
    @paneContainer.activatePreviousPane()

  # Extended: Get the first {Pane} with an item for the given URI.
  #
  # * `uri` {String} uri
  #
  # Returns a {Pane} or `undefined` if no pane exists for the given URI.
  paneForURI: (uri) ->
    @paneContainer.paneForURI(uri)

  paneForUri: (uri) ->
    deprecate("Use ::paneForURI instead.")
    @paneForURI(uri)

  # Extended: Get the {Pane} containing the given item.
  #
  # * `item` Item the returned pane contains.
  #
  # Returns a {Pane} or `undefined` if no pane exists for the given item.
  paneForItem: (item) ->
    @paneContainer.paneForItem(item)

  # Destroy (close) the active pane.
  destroyActivePane: ->
    @getActivePane()?.destroy()

  # Destroy the active pane item or the active pane if it is empty.
  destroyActivePaneItemOrEmptyPane: ->
    if @getActivePaneItem()? then @destroyActivePaneItem() else @destroyActivePane()

  # Increase the editor font size by 1px.
  increaseFontSize: ->
    atom.config.set("editor.fontSize", atom.config.get("editor.fontSize") + 1)

  # Decrease the editor font size by 1px.
  decreaseFontSize: ->
    fontSize = atom.config.get("editor.fontSize")
    atom.config.set("editor.fontSize", fontSize - 1) if fontSize > 1

  # Restore to a default editor font size.
  resetFontSize: ->
    atom.config.unset("editor.fontSize")

  # Removes the item's uri from the list of potential items to reopen.
  itemOpened: (item) ->
    if typeof item.getURI is 'function'
      uri = item.getURI()
    else if typeof item.getUri is 'function'
      uri = item.getUri()

    if uri?
      _.remove(@destroyedItemURIs, uri)

  # Adds the destroyed item's uri to the list of items to reopen.
  didDestroyPaneItem: ({item}) =>
    if typeof item.getURI is 'function'
      uri = item.getURI()
    else if typeof item.getUri is 'function'
      uri = item.getUri()

    if uri?
      @destroyedItemURIs.push(uri)

  # Called by Model superclass when destroyed
  destroyed: ->
    @paneContainer.destroy()
    @activeItemSubscriptions?.dispose()


  ###
  Section: Panels

  Panels are used to display UI related to an editor window. They are placed at one of the four
  edges of the window: left, right, top or bottom. If there are multiple panels on the same window
  edge they are stacked in order of priority: higher priority is closer to the center, lower
  priority towards the edge.

  *Note:* If your panel changes its size throughout its lifetime, consider giving it a higher
  priority, allowing fixed size panels to be closer to the edge. This allows control targets to
  remain more static for easier targeting by users that employ mice or trackpads. (See
  [atom/atom#4834](https://github.com/atom/atom/issues/4834) for discussion.)
  ###

  # Essential: Get an {Array} of all the panel items at the bottom of the editor window.
  getBottomPanels: ->
    @getPanels('bottom')

  # Essential: Adds a panel item to the bottom of the editor window.
  #
  # * `options` {Object}
  #   * `item` Your panel content. It can be DOM element, a jQuery element, or
  #     a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the
  #     latter. See {ViewRegistry::addViewProvider} for more information.
  #   * `visible` (optional) {Boolean} false if you want the panel to initially be hidden
  #     (default: true)
  #   * `priority` (optional) {Number} Determines stacking order. Lower priority items are
  #     forced closer to the edges of the window. (default: 100)
  #
  # Returns a {Panel}
  addBottomPanel: (options) ->
    @addPanel('bottom', options)

  # Essential: Get an {Array} of all the panel items to the left of the editor window.
  getLeftPanels: ->
    @getPanels('left')

  # Essential: Adds a panel item to the left of the editor window.
  #
  # * `options` {Object}
  #   * `item` Your panel content. It can be DOM element, a jQuery element, or
  #     a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the
  #     latter. See {ViewRegistry::addViewProvider} for more information.
  #   * `visible` (optional) {Boolean} false if you want the panel to initially be hidden
  #     (default: true)
  #   * `priority` (optional) {Number} Determines stacking order. Lower priority items are
  #     forced closer to the edges of the window. (default: 100)
  #
  # Returns a {Panel}
  addLeftPanel: (options) ->
    @addPanel('left', options)

  # Essential: Get an {Array} of all the panel items to the right of the editor window.
  getRightPanels: ->
    @getPanels('right')

  # Essential: Adds a panel item to the right of the editor window.
  #
  # * `options` {Object}
  #   * `item` Your panel content. It can be DOM element, a jQuery element, or
  #     a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the
  #     latter. See {ViewRegistry::addViewProvider} for more information.
  #   * `visible` (optional) {Boolean} false if you want the panel to initially be hidden
  #     (default: true)
  #   * `priority` (optional) {Number} Determines stacking order. Lower priority items are
  #     forced closer to the edges of the window. (default: 100)
  #
  # Returns a {Panel}
  addRightPanel: (options) ->
    @addPanel('right', options)

  # Essential: Get an {Array} of all the panel items at the top of the editor window.
  getTopPanels: ->
    @getPanels('top')

  # Essential: Adds a panel item to the top of the editor window above the tabs.
  #
  # * `options` {Object}
  #   * `item` Your panel content. It can be DOM element, a jQuery element, or
  #     a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the
  #     latter. See {ViewRegistry::addViewProvider} for more information.
  #   * `visible` (optional) {Boolean} false if you want the panel to initially be hidden
  #     (default: true)
  #   * `priority` (optional) {Number} Determines stacking order. Lower priority items are
  #     forced closer to the edges of the window. (default: 100)
  #
  # Returns a {Panel}
  addTopPanel: (options) ->
    @addPanel('top', options)

  # Essential: Get an {Array} of all the modal panel items
  getModalPanels: ->
    @getPanels('modal')

  # Essential: Adds a panel item as a modal dialog.
  #
  # * `options` {Object}
  #   * `item` Your panel content. It can be DOM element, a jQuery element, or
  #     a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the
  #     latter. See {ViewRegistry::addViewProvider} for more information.
  #   * `visible` (optional) {Boolean} false if you want the panel to initially be hidden
  #     (default: true)
  #   * `priority` (optional) {Number} Determines stacking order. Lower priority items are
  #     forced closer to the edges of the window. (default: 100)
  #
  # Returns a {Panel}
  addModalPanel: (options={}) ->
    @addPanel('modal', options)

  # Essential: Returns the {Panel} associated with the given item. Returns
  # `null` when the item has no panel.
  #
  # * `item` Item the panel contains
  panelForItem: (item) ->
    for location, container of @panelContainers
      panel = container.panelForItem(item)
      return panel if panel?
    null

  getPanels: (location) ->
    @panelContainers[location].getPanels()

  addPanel: (location, options) ->
    options ?= {}
    @panelContainers[location].addPanel(new Panel(options))

  ###
  Section: Searching and Replacing
  ###

  # Public: Performs a search across all the files in the workspace.
  #
  # * `regex` {RegExp} to search with.
  # * `options` (optional) {Object} (default: {})
  #   * `paths` An {Array} of glob patterns to search within
  # * `iterator` {Function} callback on each file found
  #
  # Returns a `Promise`.
  scan: (regex, options={}, iterator) ->
    if _.isFunction(options)
      iterator = options
      options = {}

    deferred = Q.defer()

    searchOptions =
      ignoreCase: regex.ignoreCase
      inclusions: options.paths
      includeHidden: true
      excludeVcsIgnores: atom.config.get('core.excludeVcsIgnoredPaths')
      exclusions: atom.config.get('core.ignoredNames')
      follow: atom.config.get('core.followSymlinks')

    # TODO: need to support all paths in @getPaths()
    task = Task.once require.resolve('./scan-handler'), atom.project.getPaths()[0], regex.source, searchOptions, ->
      deferred.resolve()

    task.on 'scan:result-found', (result) ->
      iterator(result) unless atom.project.isPathModified(result.filePath)

    task.on 'scan:file-error', (error) ->
      iterator(null, error)

    if _.isFunction(options.onPathsSearched)
      task.on 'scan:paths-searched', (numberOfPathsSearched) ->
        options.onPathsSearched(numberOfPathsSearched)

    for buffer in atom.project.getBuffers() when buffer.isModified()
      filePath = buffer.getPath()
      continue unless atom.project.contains(filePath)
      matches = []
      buffer.scan regex, (match) -> matches.push match
      iterator {filePath, matches} if matches.length > 0

    promise = deferred.promise
    promise.cancel = ->
      task.terminate()
      deferred.resolve('cancelled')
    promise

  # Public: Performs a replace across all the specified files in the project.
  #
  # * `regex` A {RegExp} to search with.
  # * `replacementText` Text to replace all matches of regex with
  # * `filePaths` List of file path strings to run the replace on.
  # * `iterator` A {Function} callback on each file with replacements:
  #   * `options` {Object} with keys `filePath` and `replacements`
  #
  # Returns a `Promise`.
  replace: (regex, replacementText, filePaths, iterator) ->
    deferred = Q.defer()

    openPaths = (buffer.getPath() for buffer in atom.project.getBuffers())
    outOfProcessPaths = _.difference(filePaths, openPaths)

    inProcessFinished = !openPaths.length
    outOfProcessFinished = !outOfProcessPaths.length
    checkFinished = ->
      deferred.resolve() if outOfProcessFinished and inProcessFinished

    unless outOfProcessFinished.length
      flags = 'g'
      flags += 'i' if regex.ignoreCase

      task = Task.once require.resolve('./replace-handler'), outOfProcessPaths, regex.source, flags, replacementText, ->
        outOfProcessFinished = true
        checkFinished()

      task.on 'replace:path-replaced', iterator
      task.on 'replace:file-error', (error) -> iterator(null, error)

    for buffer in atom.project.getBuffers()
      continue unless buffer.getPath() in filePaths
      replacements = buffer.replace(regex, replacementText, iterator)
      iterator({filePath: buffer.getPath(), replacements}) if replacements

    inProcessFinished = true
    checkFinished()

    deferred.promise
