mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-11-10 10:17:11 +03:00
964 lines
36 KiB
CoffeeScript
964 lines
36 KiB
CoffeeScript
{includeDeprecatedAPIs, deprecate} = require 'grim'
|
|
_ = require 'underscore-plus'
|
|
path = require 'path'
|
|
{join} = path
|
|
Q = require 'q'
|
|
Serializable = require 'serializable'
|
|
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
|
|
Grim = require 'grim'
|
|
fs = require 'fs-plus'
|
|
Model = require './model'
|
|
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)
|
|
|
|
constructor: (params) ->
|
|
super
|
|
|
|
unless Grim.includeDeprecatedAPIs
|
|
@paneContainer = params?.paneContainer
|
|
@fullScreen = params?.fullScreen ? false
|
|
@destroyedItemURIs = params?.destroyedItemURIs ? []
|
|
|
|
@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'
|
|
atom.project.open(atom.styles.getUserStyleSheetPath())
|
|
when 'atom://.atom/keymap'
|
|
atom.project.open(atom.keymaps.getUserKeymapPath())
|
|
when 'atom://.atom/config'
|
|
atom.project.open(atom.config.getUserConfigPath())
|
|
when 'atom://.atom/init-script'
|
|
atom.project.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))
|
|
return
|
|
|
|
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 if includeDeprecatedAPIs
|
|
|
|
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'
|
|
projectPaths = atom.project?.getPaths() ? []
|
|
if item = @getActivePaneItem()
|
|
itemPath = item.getPath?()
|
|
itemTitle = item.getTitle?()
|
|
projectPath = _.find projectPaths, (projectPath) ->
|
|
itemPath is projectPath or itemPath?.startsWith(projectPath + path.sep)
|
|
itemTitle ?= "untitled"
|
|
projectPath ?= projectPaths[0]
|
|
|
|
if item? and projectPath?
|
|
document.title = "#{itemTitle} - #{projectPath} - #{appName}"
|
|
atom.setRepresentedFilename(itemPath ? projectPath)
|
|
else if projectPath?
|
|
document.title = "#{projectPath} - #{appName}"
|
|
atom.setRepresentedFilename(projectPath)
|
|
else
|
|
document.title = "#{itemTitle} - #{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
|
|
|
|
###
|
|
Section: Opening
|
|
###
|
|
|
|
# Essential: Opens the given URI in Atom asynchronously.
|
|
# If the URI is already open, the existing item for that URI will be
|
|
# activated. If no URI is given, or no registered opener can open
|
|
# the URI, a new empty {TextEditor} will be created.
|
|
#
|
|
# * `uri` (optional) 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 includeDeprecatedAPIs and 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 not 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 includeDeprecatedAPIs and 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 not 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" if includeDeprecatedAPIs
|
|
@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()
|
|
|
|
# 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) ->
|
|
if includeDeprecatedAPIs
|
|
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})
|
|
if item? and typeof item.on is 'function' and typeof item.onDidChangeTitle isnt 'function'
|
|
Grim.deprecate("If you would like your pane item with class `#{item.constructor.name}` to support title change behavior, please implement a `::onDidChangeTitle()` method. `::on` methods for items are no longer supported. If not, ignore this message.", {packageName})
|
|
if item? and typeof item.on is 'function' and typeof item.onDidChangeModified isnt 'function'
|
|
Grim.deprecate("If you would like your pane item with class `#{item.constructor.name}` to support modified behavior, please implement a `::onDidChangeModified()` method. If not, ignore this message. `::on` methods for items are no longer supported.", {packageName})
|
|
item
|
|
|
|
@openers.push(wrappedOpener)
|
|
new Disposable => _.remove(@openers, wrappedOpener)
|
|
else
|
|
@openers.push(opener)
|
|
new Disposable => _.remove(@openers, opener)
|
|
|
|
getOpeners: ->
|
|
@openers
|
|
|
|
###
|
|
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
|
|
|
|
# Save all pane items.
|
|
saveAll: ->
|
|
@paneContainer.saveAll()
|
|
|
|
confirmClose: (options) ->
|
|
@paneContainer.confirmClose(options)
|
|
|
|
# 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: ->
|
|
@getActivePane().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: ->
|
|
@getActivePane().saveActiveItemAs()
|
|
|
|
# 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)
|
|
|
|
# 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')
|
|
|
|
task = Task.once require.resolve('./scan-handler'), atom.project.getPaths(), 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 = not openPaths.length
|
|
outOfProcessFinished = not 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
|
|
|
|
if includeDeprecatedAPIs
|
|
Workspace.properties
|
|
paneContainer: null
|
|
fullScreen: false
|
|
destroyedItemURIs: -> []
|
|
|
|
Object.defineProperty Workspace::, 'activePaneItem',
|
|
get: ->
|
|
Grim.deprecate "Use ::getActivePaneItem() instead of the ::activePaneItem property"
|
|
@getActivePaneItem()
|
|
|
|
Object.defineProperty Workspace::, 'activePane',
|
|
get: ->
|
|
Grim.deprecate "Use ::getActivePane() instead of the ::activePane property"
|
|
@getActivePane()
|
|
|
|
StackTraceParser = require 'stacktrace-parser'
|
|
|
|
Workspace::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
|
|
|
|
Workspace::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
|
|
|
|
Workspace::eachEditor = (callback) ->
|
|
deprecate("Use Workspace::observeTextEditors instead")
|
|
|
|
callback(editor) for editor in @getEditors()
|
|
@subscribe this, 'editor-created', (editor) -> callback(editor)
|
|
|
|
Workspace::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
|
|
|
|
Workspace::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
|
|
|
|
Workspace::reopenItemSync = ->
|
|
deprecate("Use Workspace::reopenItem instead")
|
|
if uri = @destroyedItemURIs.pop()
|
|
@openSync(uri)
|
|
|
|
Workspace::registerOpener = (opener) ->
|
|
Grim.deprecate("Call Workspace::addOpener instead")
|
|
@addOpener(opener)
|
|
|
|
Workspace::unregisterOpener = (opener) ->
|
|
Grim.deprecate("Call .dispose() on the Disposable returned from ::addOpener instead")
|
|
_.remove(@openers, opener)
|
|
|
|
Workspace::getActiveEditor = ->
|
|
Grim.deprecate "Call ::getActiveTextEditor instead"
|
|
@getActivePane()?.getActiveEditor()
|
|
|
|
Workspace::paneForUri = (uri) ->
|
|
deprecate("Use ::paneForURI instead.")
|
|
@paneForURI(uri)
|