Files
atom/src/pane.coffee
t9md d9828cc85e should not use bind for map's callback.
map pass item index as 2nd arg, so bind is not appropriate for the
function which take 2nd arg.
2017-07-25 03:49:18 +09:00

993 lines
34 KiB
CoffeeScript

Grim = require 'grim'
{find, compact, extend, last} = require 'underscore-plus'
{CompositeDisposable, Emitter} = require 'event-kit'
PaneAxis = require './pane-axis'
TextEditor = require './text-editor'
PaneElement = require './pane-element'
nextInstanceId = 1
class SaveCancelledError extends Error
constructor: -> super
# Extended: A container for presenting content in the center of the workspace.
# Panes can contain multiple items, one of which is *active* at a given time.
# The view corresponding to the active item is displayed in the interface. In
# the default configuration, tabs are also displayed for each item.
#
# Each pane may also contain one *pending* item. When a pending item is added
# to a pane, it will replace the currently pending item, if any, instead of
# simply being added. In the default configuration, the text in the tab for
# pending items is shown in italics.
module.exports =
class Pane
inspect: -> "Pane #{@id}"
@deserialize: (state, {deserializers, applicationDelegate, config, notifications, views}) ->
{items, activeItemIndex, activeItemURI, activeItemUri} = state
activeItemURI ?= activeItemUri
items = items.map (itemState) -> deserializers.deserialize(itemState)
state.activeItem = items[activeItemIndex]
state.items = compact(items)
if activeItemURI?
state.activeItem ?= find state.items, (item) ->
if typeof item.getURI is 'function'
itemURI = item.getURI()
itemURI is activeItemURI
new Pane(extend(state, {
deserializerManager: deserializers,
notificationManager: notifications,
viewRegistry: views,
config, applicationDelegate
}))
constructor: (params) ->
{
@id, @activeItem, @focused, @applicationDelegate, @notificationManager, @config,
@deserializerManager, @viewRegistry
} = params
if @id?
nextInstanceId = Math.max(nextInstanceId, @id + 1)
else
@id = nextInstanceId++
@emitter = new Emitter
@alive = true
@subscriptionsPerItem = new WeakMap
@items = []
@itemStack = []
@container = null
@activeItem ?= undefined
@focused ?= false
@addItems(compact(params?.items ? []))
@setActiveItem(@items[0]) unless @getActiveItem()?
@addItemsToStack(params?.itemStackIndices ? [])
@setFlexScale(params?.flexScale ? 1)
getElement: ->
@element ?= new PaneElement().initialize(this, {views: @viewRegistry, @applicationDelegate})
serialize: ->
itemsToBeSerialized = compact(@items.map((item) -> item if typeof item.serialize is 'function'))
itemStackIndices = (itemsToBeSerialized.indexOf(item) for item in @itemStack when typeof item.serialize is 'function')
activeItemIndex = itemsToBeSerialized.indexOf(@activeItem)
{
deserializer: 'Pane',
id: @id,
items: itemsToBeSerialized.map((item) -> item.serialize())
itemStackIndices: itemStackIndices
activeItemIndex: activeItemIndex
focused: @focused
flexScale: @flexScale
}
getParent: -> @parent
setParent: (@parent) -> @parent
getContainer: -> @container
setContainer: (container) ->
if container and container isnt @container
@container = container
container.didAddPane({pane: this})
# Private: Determine whether the given item is allowed to exist in this pane.
#
# * `item` the Item
#
# Returns a {Boolean}.
isItemAllowed: (item) ->
if (typeof item.getAllowedLocations isnt 'function')
true
else
item.getAllowedLocations().includes(@getContainer().getLocation())
setFlexScale: (@flexScale) ->
@emitter.emit 'did-change-flex-scale', @flexScale
@flexScale
getFlexScale: -> @flexScale
increaseSize: -> @setFlexScale(@getFlexScale() * 1.1)
decreaseSize: -> @setFlexScale(@getFlexScale() / 1.1)
###
Section: Event Subscription
###
# Public: Invoke the given callback when the pane resizes
#
# The callback will be invoked when pane's flexScale property changes.
# Use {::getFlexScale} to get the current value.
#
# * `callback` {Function} to be called when the pane is resized
# * `flexScale` {Number} representing the panes `flex-grow`; ability for a
# flex item to grow if necessary.
#
# Returns a {Disposable} on which '.dispose()' can be called to unsubscribe.
onDidChangeFlexScale: (callback) ->
@emitter.on 'did-change-flex-scale', callback
# Public: Invoke the given callback with the current and future values of
# {::getFlexScale}.
#
# * `callback` {Function} to be called with the current and future values of
# the {::getFlexScale} property.
# * `flexScale` {Number} representing the panes `flex-grow`; ability for a
# flex item to grow if necessary.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
observeFlexScale: (callback) ->
callback(@flexScale)
@onDidChangeFlexScale(callback)
# Public: Invoke the given callback when the pane is activated.
#
# The given callback will be invoked whenever {::activate} is called on the
# pane, even if it is already active at the time.
#
# * `callback` {Function} to be called when the pane is activated.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidActivate: (callback) ->
@emitter.on 'did-activate', callback
# Public: Invoke the given callback before the pane is destroyed.
#
# * `callback` {Function} to be called before the pane is destroyed.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onWillDestroy: (callback) ->
@emitter.on 'will-destroy', callback
# Public: Invoke the given callback when the pane is destroyed.
#
# * `callback` {Function} to be called when the pane is destroyed.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidDestroy: (callback) ->
@emitter.once 'did-destroy', callback
# Public: Invoke the given callback when the value of the {::isActive}
# property changes.
#
# * `callback` {Function} to be called when the value of the {::isActive}
# property changes.
# * `active` {Boolean} indicating whether the pane is active.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeActive: (callback) ->
@container.onDidChangeActivePane (activePane) =>
callback(this is activePane)
# Public: Invoke the given callback with the current and future values of the
# {::isActive} property.
#
# * `callback` {Function} to be called with the current and future values of
# the {::isActive} property.
# * `active` {Boolean} indicating whether the pane is active.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
observeActive: (callback) ->
callback(@isActive())
@onDidChangeActive(callback)
# Public: Invoke the given callback when an item is added to the pane.
#
# * `callback` {Function} to be called with when items are added.
# * `event` {Object} with the following keys:
# * `item` The added pane item.
# * `index` {Number} indicating where the item is located.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidAddItem: (callback) ->
@emitter.on 'did-add-item', callback
# Public: Invoke the given callback when an item is removed from the pane.
#
# * `callback` {Function} to be called with when items are removed.
# * `event` {Object} with the following keys:
# * `item` The removed pane item.
# * `index` {Number} indicating where the item was located.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidRemoveItem: (callback) ->
@emitter.on 'did-remove-item', callback
# Public: Invoke the given callback before an item is removed from the pane.
#
# * `callback` {Function} to be called with when items are removed.
# * `event` {Object} with the following keys:
# * `item` The pane item to be removed.
# * `index` {Number} indicating where the item is located.
onWillRemoveItem: (callback) ->
@emitter.on 'will-remove-item', callback
# Public: Invoke the given callback when an item is moved within the pane.
#
# * `callback` {Function} to be called with when items are moved.
# * `event` {Object} with the following keys:
# * `item` The removed pane item.
# * `oldIndex` {Number} indicating where the item was located.
# * `newIndex` {Number} indicating where the item is now located.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidMoveItem: (callback) ->
@emitter.on 'did-move-item', callback
# Public: Invoke the given callback with all current and future items.
#
# * `callback` {Function} to be called with current and future items.
# * `item` An item that is present in {::getItems} at the time of
# subscription or that is added at some later time.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
observeItems: (callback) ->
callback(item) for item in @getItems()
@onDidAddItem ({item}) -> callback(item)
# Public: Invoke the given callback when the value of {::getActiveItem}
# changes.
#
# * `callback` {Function} to be called with when the active item changes.
# * `activeItem` The current active item.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeActiveItem: (callback) ->
@emitter.on 'did-change-active-item', callback
# Public: Invoke the given callback when {::activateNextRecentlyUsedItem}
# has been called, either initiating or continuing a forward MRU traversal of
# pane items.
#
# * `callback` {Function} to be called with when the active item changes.
# * `nextRecentlyUsedItem` The next MRU item, now being set active
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onChooseNextMRUItem: (callback) ->
@emitter.on 'choose-next-mru-item', callback
# Public: Invoke the given callback when {::activatePreviousRecentlyUsedItem}
# has been called, either initiating or continuing a reverse MRU traversal of
# pane items.
#
# * `callback` {Function} to be called with when the active item changes.
# * `previousRecentlyUsedItem` The previous MRU item, now being set active
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onChooseLastMRUItem: (callback) ->
@emitter.on 'choose-last-mru-item', callback
# Public: Invoke the given callback when {::moveActiveItemToTopOfStack}
# has been called, terminating an MRU traversal of pane items and moving the
# current active item to the top of the stack. Typically bound to a modifier
# (e.g. CTRL) key up event.
#
# * `callback` {Function} to be called with when the MRU traversal is done.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDoneChoosingMRUItem: (callback) ->
@emitter.on 'done-choosing-mru-item', callback
# Public: Invoke the given callback with the current and future values of
# {::getActiveItem}.
#
# * `callback` {Function} to be called with the current and future active
# items.
# * `activeItem` The current active item.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
observeActiveItem: (callback) ->
callback(@getActiveItem())
@onDidChangeActiveItem(callback)
# Public: Invoke the given callback before items are destroyed.
#
# * `callback` {Function} to be called before items are destroyed.
# * `event` {Object} with the following keys:
# * `item` The item that will be destroyed.
# * `index` The location of the item.
#
# Returns a {Disposable} on which `.dispose()` can be called to
# unsubscribe.
onWillDestroyItem: (callback) ->
@emitter.on 'will-destroy-item', callback
# Called by the view layer to indicate that the pane has gained focus.
focus: ->
@focused = true
@activate()
# Called by the view layer to indicate that the pane has lost focus.
blur: ->
@focused = false
true # if this is called from an event handler, don't cancel it
isFocused: -> @focused
getPanes: -> [this]
unsubscribeFromItem: (item) ->
@subscriptionsPerItem.get(item)?.dispose()
@subscriptionsPerItem.delete(item)
###
Section: Items
###
# Public: Get the items in this pane.
#
# Returns an {Array} of items.
getItems: ->
@items.slice()
# Public: Get the active pane item in this pane.
#
# Returns a pane item.
getActiveItem: -> @activeItem
setActiveItem: (activeItem, options) ->
{modifyStack} = options if options?
unless activeItem is @activeItem
@addItemToStack(activeItem) unless modifyStack is false
@activeItem = activeItem
@emitter.emit 'did-change-active-item', @activeItem
@container?.didChangeActiveItemOnPane(this, @activeItem)
@activeItem
# Build the itemStack after deserializing
addItemsToStack: (itemStackIndices) ->
if @items.length > 0
if itemStackIndices.length is 0 or itemStackIndices.length isnt @items.length or itemStackIndices.indexOf(-1) >= 0
itemStackIndices = (i for i in [0..@items.length-1])
for itemIndex in itemStackIndices
@addItemToStack(@items[itemIndex])
return
# Add item (or move item) to the end of the itemStack
addItemToStack: (newItem) ->
return unless newItem?
index = @itemStack.indexOf(newItem)
@itemStack.splice(index, 1) unless index is -1
@itemStack.push(newItem)
# Return an {TextEditor} if the pane item is an {TextEditor}, or null otherwise.
getActiveEditor: ->
@activeItem if @activeItem instanceof TextEditor
# Public: Return the item at the given index.
#
# * `index` {Number}
#
# Returns an item or `null` if no item exists at the given index.
itemAtIndex: (index) ->
@items[index]
# Makes the next item in the itemStack active.
activateNextRecentlyUsedItem: ->
if @items.length > 1
@itemStackIndex = @itemStack.length - 1 unless @itemStackIndex?
@itemStackIndex = @itemStack.length if @itemStackIndex is 0
@itemStackIndex = @itemStackIndex - 1
nextRecentlyUsedItem = @itemStack[@itemStackIndex]
@emitter.emit 'choose-next-mru-item', nextRecentlyUsedItem
@setActiveItem(nextRecentlyUsedItem, modifyStack: false)
# Makes the previous item in the itemStack active.
activatePreviousRecentlyUsedItem: ->
if @items.length > 1
if @itemStackIndex + 1 is @itemStack.length or not @itemStackIndex?
@itemStackIndex = -1
@itemStackIndex = @itemStackIndex + 1
previousRecentlyUsedItem = @itemStack[@itemStackIndex]
@emitter.emit 'choose-last-mru-item', previousRecentlyUsedItem
@setActiveItem(previousRecentlyUsedItem, modifyStack: false)
# Moves the active item to the end of the itemStack once the ctrl key is lifted
moveActiveItemToTopOfStack: ->
delete @itemStackIndex
@addItemToStack(@activeItem)
@emitter.emit 'done-choosing-mru-item'
# Public: Makes the next item active.
activateNextItem: ->
index = @getActiveItemIndex()
if index < @items.length - 1
@activateItemAtIndex(index + 1)
else
@activateItemAtIndex(0)
# Public: Makes the previous item active.
activatePreviousItem: ->
index = @getActiveItemIndex()
if index > 0
@activateItemAtIndex(index - 1)
else
@activateItemAtIndex(@items.length - 1)
activateLastItem: ->
@activateItemAtIndex(@items.length - 1)
# Public: Move the active tab to the right.
moveItemRight: ->
index = @getActiveItemIndex()
rightItemIndex = index + 1
@moveItem(@getActiveItem(), rightItemIndex) unless rightItemIndex > @items.length - 1
# Public: Move the active tab to the left
moveItemLeft: ->
index = @getActiveItemIndex()
leftItemIndex = index - 1
@moveItem(@getActiveItem(), leftItemIndex) unless leftItemIndex < 0
# Public: Get the index of the active item.
#
# Returns a {Number}.
getActiveItemIndex: ->
@items.indexOf(@activeItem)
# Public: Activate the item at the given index.
#
# * `index` {Number}
activateItemAtIndex: (index) ->
item = @itemAtIndex(index) or @getActiveItem()
@setActiveItem(item)
# Public: Make the given item *active*, causing it to be displayed by
# the pane's view.
#
# * `item` The item to activate
# * `options` (optional) {Object}
# * `pending` (optional) {Boolean} indicating that the item should be added
# in a pending state if it does not yet exist in the pane. Existing pending
# items in a pane are replaced with new pending items when they are opened.
activateItem: (item, options={}) ->
if item?
if @getPendingItem() is @activeItem
index = @getActiveItemIndex()
else
index = @getActiveItemIndex() + 1
@addItem(item, extend({}, options, {index: index}))
@setActiveItem(item)
# Public: Add the given item to the pane.
#
# * `item` The item to add. It can be a model with an associated view or a
# view.
# * `options` (optional) {Object}
# * `index` (optional) {Number} indicating the index at which to add the item.
# If omitted, the item is added after the current active item.
# * `pending` (optional) {Boolean} indicating that the item should be
# added in a pending state. Existing pending items in a pane are replaced with
# new pending items when they are opened.
#
# Returns the added item.
addItem: (item, options={}) ->
# Backward compat with old API:
# addItem(item, index=@getActiveItemIndex() + 1)
if typeof options is "number"
Grim.deprecate("Pane::addItem(item, #{options}) is deprecated in favor of Pane::addItem(item, {index: #{options}})")
options = index: options
index = options.index ? @getActiveItemIndex() + 1
moved = options.moved ? false
pending = options.pending ? false
throw new Error("Pane items must be objects. Attempted to add item #{item}.") unless item? and typeof item is 'object'
throw new Error("Adding a pane item with URI '#{item.getURI?()}' that has already been destroyed") if item.isDestroyed?()
return if item in @items
if typeof item.onDidDestroy is 'function'
itemSubscriptions = new CompositeDisposable
itemSubscriptions.add item.onDidDestroy => @removeItem(item, false)
if typeof item.onDidTerminatePendingState is "function"
itemSubscriptions.add item.onDidTerminatePendingState =>
@clearPendingItem() if @getPendingItem() is item
@subscriptionsPerItem.set item, itemSubscriptions
@items.splice(index, 0, item)
lastPendingItem = @getPendingItem()
replacingPendingItem = lastPendingItem? and not moved
@pendingItem = null if replacingPendingItem
@setPendingItem(item) if pending
@emitter.emit 'did-add-item', {item, index, moved}
@container?.didAddPaneItem(item, this, index) unless moved
@destroyItem(lastPendingItem) if replacingPendingItem
@setActiveItem(item) unless @getActiveItem()?
item
setPendingItem: (item) =>
if @pendingItem isnt item
mostRecentPendingItem = @pendingItem
@pendingItem = item
if mostRecentPendingItem?
@emitter.emit 'item-did-terminate-pending-state', mostRecentPendingItem
getPendingItem: =>
@pendingItem or null
clearPendingItem: =>
@setPendingItem(null)
onItemDidTerminatePendingState: (callback) =>
@emitter.on 'item-did-terminate-pending-state', callback
# Public: Add the given items to the pane.
#
# * `items` An {Array} of items to add. Items can be views or models with
# associated views. Any objects that are already present in the pane's
# current items will not be added again.
# * `index` (optional) {Number} index at which to add the items. If omitted,
# the item is # added after the current active item.
#
# Returns an {Array} of added items.
addItems: (items, index=@getActiveItemIndex() + 1) ->
items = items.filter (item) => not (item in @items)
@addItem(item, {index: index + i}) for item, i in items
items
removeItem: (item, moved) ->
index = @items.indexOf(item)
return if index is -1
@pendingItem = null if @getPendingItem() is item
@removeItemFromStack(item)
@emitter.emit 'will-remove-item', {item, index, destroyed: not moved, moved}
@unsubscribeFromItem(item)
if item is @activeItem
if @items.length is 1
@setActiveItem(undefined)
else if index is 0
@activateNextItem()
else
@activatePreviousItem()
@items.splice(index, 1)
@emitter.emit 'did-remove-item', {item, index, destroyed: not moved, moved}
@container?.didDestroyPaneItem({item, index, pane: this}) unless moved
@destroy() if @items.length is 0 and @config.get('core.destroyEmptyPanes')
# Remove the given item from the itemStack.
#
# * `item` The item to remove.
# * `index` {Number} indicating the index to which to remove the item from the itemStack.
removeItemFromStack: (item) ->
index = @itemStack.indexOf(item)
@itemStack.splice(index, 1) unless index is -1
# Public: Move the given item to the given index.
#
# * `item` The item to move.
# * `index` {Number} indicating the index to which to move the item.
moveItem: (item, newIndex) ->
oldIndex = @items.indexOf(item)
@items.splice(oldIndex, 1)
@items.splice(newIndex, 0, item)
@emitter.emit 'did-move-item', {item, oldIndex, newIndex}
# Public: Move the given item to the given index on another pane.
#
# * `item` The item to move.
# * `pane` {Pane} to which to move the item.
# * `index` {Number} indicating the index to which to move the item in the
# given pane.
moveItemToPane: (item, pane, index) ->
@removeItem(item, true)
pane.addItem(item, {index: index, moved: true})
# Public: Destroy the active item and activate the next item.
destroyActiveItem: ->
@destroyItem(@activeItem)
false
# Public: Destroy the given item.
#
# If the item is active, the next item will be activated. If the item is the
# last item, the pane will be destroyed if the `core.destroyEmptyPanes` config
# setting is `true`.
#
# * `item` Item to destroy
# * `force` (optional) {Boolean} Destroy the item without prompting to save
# it, even if the item's `isPermanentDockItem` method returns true.
#
# Returns a {Promise} that resolves with a {Boolean} indicating whether or not
# the item was destroyed.
destroyItem: (item, force) ->
index = @items.indexOf(item)
if index isnt -1
return false if not force and @getContainer()?.getLocation() isnt 'center' and item.isPermanentDockItem?()
@emitter.emit 'will-destroy-item', {item, index}
@container?.willDestroyPaneItem({item, index, pane: this})
if force or not item?.shouldPromptToSave?()
@removeItem(item, false)
item.destroy?()
else
@promptToSaveItem(item).then (result) =>
if result
@removeItem(item, false)
item.destroy?()
result
# Public: Destroy all items.
destroyItems: ->
Promise.all(
@getItems().map((item) => @destroyItem(item))
)
# Public: Destroy all items except for the active item.
destroyInactiveItems: ->
Promise.all(
@getItems()
.filter((item) => item isnt @activeItem)
.map((item) => @destroyItem(item))
)
promptToSaveItem: (item, options={}) ->
return Promise.resolve(true) unless item.shouldPromptToSave?(options)
if typeof item.getURI is 'function'
uri = item.getURI()
else if typeof item.getUri is 'function'
uri = item.getUri()
else
return Promise.resolve(true)
saveDialog = (saveButtonText, saveFn, message) =>
chosen = @applicationDelegate.confirm
message: message
detailedMessage: "Your changes will be lost if you close this item without saving."
buttons: [saveButtonText, "Cancel", "&Don't Save"]
switch chosen
when 0
new Promise (resolve) ->
saveFn item, (error) ->
if error instanceof SaveCancelledError
resolve(false)
else
saveError(error).then(resolve)
when 1
Promise.resolve(false)
when 2
Promise.resolve(true)
saveError = (error) =>
if error
saveDialog("Save as", @saveItemAs, "'#{item.getTitle?() ? uri}' could not be saved.\nError: #{@getMessageForErrorCode(error.code)}")
else
Promise.resolve(true)
saveDialog("Save", @saveItem, "'#{item.getTitle?() ? uri}' has changes, do you want to save them?")
# Public: Save the active item.
saveActiveItem: (nextAction) ->
@saveItem(@getActiveItem(), nextAction)
# Public: Prompt the user for a location and save the active item with the
# path they select.
#
# * `nextAction` (optional) {Function} which will be called after the item is
# successfully saved.
#
# Returns a {Promise} that resolves when the save is complete
saveActiveItemAs: (nextAction) ->
@saveItemAs(@getActiveItem(), nextAction)
# Public: Save the given item.
#
# * `item` The item to save.
# * `nextAction` (optional) {Function} which will be called with no argument
# after the item is successfully saved, or with the error if it failed.
# The return value will be that of `nextAction` or `undefined` if it was not
# provided
#
# Returns a {Promise} that resolves when the save is complete
saveItem: (item, nextAction) =>
if typeof item?.getURI is 'function'
itemURI = item.getURI()
else if typeof item?.getUri is 'function'
itemURI = item.getUri()
if itemURI?
if item.save?
promisify -> item.save()
.then -> nextAction?()
.catch (error) =>
if nextAction
nextAction(error)
else
@handleSaveError(error, item)
else
nextAction?()
else
@saveItemAs(item, nextAction)
# Public: Prompt the user for a location and save the active item with the
# path they select.
#
# * `item` The item to save.
# * `nextAction` (optional) {Function} which will be called with no argument
# after the item is successfully saved, or with the error if it failed.
# The return value will be that of `nextAction` or `undefined` if it was not
# provided
saveItemAs: (item, nextAction) =>
return unless item?.saveAs?
saveOptions = item.getSaveDialogOptions?() ? {}
saveOptions.defaultPath ?= item.getPath()
newItemPath = @applicationDelegate.showSaveDialog(saveOptions)
if newItemPath
promisify -> item.saveAs(newItemPath)
.then -> nextAction?()
.catch (error) =>
if nextAction?
nextAction(error)
else
@handleSaveError(error, item)
else if nextAction?
nextAction(new SaveCancelledError('Save Cancelled'))
# Public: Save all items.
saveItems: ->
for item in @getItems()
@saveItem(item) if item.isModified?()
return
# Public: Return the first item that matches the given URI or undefined if
# none exists.
#
# * `uri` {String} containing a URI.
itemForURI: (uri) ->
find @items, (item) ->
if typeof item.getURI is 'function'
itemUri = item.getURI()
else if typeof item.getUri is 'function'
itemUri = item.getUri()
itemUri is uri
# Public: Activate the first item that matches the given URI.
#
# * `uri` {String} containing a URI.
#
# Returns a {Boolean} indicating whether an item matching the URI was found.
activateItemForURI: (uri) ->
if item = @itemForURI(uri)
@activateItem(item)
true
else
false
copyActiveItem: ->
@activeItem?.copy?()
###
Section: Lifecycle
###
# Public: Determine whether the pane is active.
#
# Returns a {Boolean}.
isActive: ->
@container?.getActivePane() is this
# Public: Makes this pane the *active* pane, causing it to gain focus.
activate: ->
throw new Error("Pane has been destroyed") if @isDestroyed()
@container?.didActivatePane(this)
@emitter.emit 'did-activate'
# Public: Close the pane and destroy all its items.
#
# If this is the last pane, all the items will be destroyed but the pane
# itself will not be destroyed.
destroy: ->
if @container?.isAlive() and @container.getPanes().length is 1
@destroyItems()
else
@emitter.emit 'will-destroy'
@alive = false
@container?.willDestroyPane(pane: this)
@container.activateNextPane() if @isActive()
@emitter.emit 'did-destroy'
@emitter.dispose()
item.destroy?() for item in @items.slice()
@container?.didDestroyPane(pane: this)
isAlive: -> @alive
# Public: Determine whether this pane has been destroyed.
#
# Returns a {Boolean}.
isDestroyed: -> not @isAlive()
###
Section: Splitting
###
# Public: Create a new pane to the left of this pane.
#
# * `params` (optional) {Object} with the following keys:
# * `items` (optional) {Array} of items to add to the new pane.
# * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane
#
# Returns the new {Pane}.
splitLeft: (params) ->
@split('horizontal', 'before', params)
# Public: Create a new pane to the right of this pane.
#
# * `params` (optional) {Object} with the following keys:
# * `items` (optional) {Array} of items to add to the new pane.
# * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane
#
# Returns the new {Pane}.
splitRight: (params) ->
@split('horizontal', 'after', params)
# Public: Creates a new pane above the receiver.
#
# * `params` (optional) {Object} with the following keys:
# * `items` (optional) {Array} of items to add to the new pane.
# * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane
#
# Returns the new {Pane}.
splitUp: (params) ->
@split('vertical', 'before', params)
# Public: Creates a new pane below the receiver.
#
# * `params` (optional) {Object} with the following keys:
# * `items` (optional) {Array} of items to add to the new pane.
# * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane
#
# Returns the new {Pane}.
splitDown: (params) ->
@split('vertical', 'after', params)
split: (orientation, side, params) ->
if params?.copyActiveItem
params.items ?= []
params.items.push(@copyActiveItem())
if @parent.orientation isnt orientation
@parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this], @flexScale}, @viewRegistry))
@setFlexScale(1)
newPane = new Pane(extend({@applicationDelegate, @notificationManager, @deserializerManager, @config, @viewRegistry}, params))
switch side
when 'before' then @parent.insertChildBefore(this, newPane)
when 'after' then @parent.insertChildAfter(this, newPane)
@moveItemToPane(@activeItem, newPane) if params?.moveActiveItem
newPane.activate()
newPane
# If the parent is a horizontal axis, returns its first child if it is a pane;
# otherwise returns this pane.
findLeftmostSibling: ->
if @parent.orientation is 'horizontal'
[leftmostSibling] = @parent.children
if leftmostSibling instanceof PaneAxis
this
else
leftmostSibling
else
this
findRightmostSibling: ->
if @parent.orientation is 'horizontal'
rightmostSibling = last(@parent.children)
if rightmostSibling instanceof PaneAxis
this
else
rightmostSibling
else
this
# If the parent is a horizontal axis, returns its last child if it is a pane;
# otherwise returns a new pane created by splitting this pane rightward.
findOrCreateRightmostSibling: ->
rightmostSibling = @findRightmostSibling()
if rightmostSibling is this then @splitRight() else rightmostSibling
# If the parent is a vertical axis, returns its first child if it is a pane;
# otherwise returns this pane.
findTopmostSibling: ->
if @parent.orientation is 'vertical'
[topmostSibling] = @parent.children
if topmostSibling instanceof PaneAxis
this
else
topmostSibling
else
this
findBottommostSibling: ->
if @parent.orientation is 'vertical'
bottommostSibling = last(@parent.children)
if bottommostSibling instanceof PaneAxis
this
else
bottommostSibling
else
this
# If the parent is a vertical axis, returns its last child if it is a pane;
# otherwise returns a new pane created by splitting this pane bottomward.
findOrCreateBottommostSibling: ->
bottommostSibling = @findBottommostSibling()
if bottommostSibling is this then @splitDown() else bottommostSibling
# Private: Close the pane unless the user cancels the action via a dialog.
#
# Returns a {Promise} that resolves once the pane is either closed, or the
# closing has been cancelled.
close: ->
Promise.all(@getItems().map((item) => @promptToSaveItem(item))).then (results) =>
@destroy() unless results.includes(false)
handleSaveError: (error, item) ->
itemPath = error.path ? item?.getPath?()
addWarningWithPath = (message, options) =>
message = "#{message} '#{itemPath}'" if itemPath
@notificationManager.addWarning(message, options)
customMessage = @getMessageForErrorCode(error.code)
if customMessage?
addWarningWithPath("Unable to save file: #{customMessage}")
else if error.code is 'EISDIR' or error.message?.endsWith?('is a directory')
@notificationManager.addWarning("Unable to save file: #{error.message}")
else if error.code in ['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST', 'ELOOP', 'EAGAIN']
addWarningWithPath('Unable to save file', detail: error.message)
else if errorMatch = /ENOTDIR, not a directory '([^']+)'/.exec(error.message)
fileName = errorMatch[1]
@notificationManager.addWarning("Unable to save file: A directory in the path '#{fileName}' could not be written to")
else
throw error
getMessageForErrorCode: (errorCode) ->
switch errorCode
when 'EACCES' then 'Permission denied'
when 'ECONNRESET' then 'Connection reset'
when 'EINTR' then 'Interrupted system call'
when 'EIO' then 'I/O error writing file'
when 'ENOSPC' then 'No space left on device'
when 'ENOTSUP' then 'Operation not supported on socket'
when 'ENXIO' then 'No such device or address'
when 'EROFS' then 'Read-only file system'
when 'ESPIPE' then 'Invalid seek'
when 'ETIMEDOUT' then 'Connection timed out'
promisify = (callback) ->
try
Promise.resolve(callback())
catch error
Promise.reject(error)