Merge branch 'master' into add-subword-cursors-4

Conflicts:
	spec/text-editor-spec.coffee
This commit is contained in:
Nathan Sobo
2015-06-30 15:43:33 -05:00
253 changed files with 13174 additions and 8112 deletions

View File

@@ -6,15 +6,15 @@ remote = require 'remote'
shell = require 'shell'
_ = require 'underscore-plus'
{deprecate} = require 'grim'
{Emitter} = require 'event-kit'
{Model} = require 'theorist'
{deprecate, includeDeprecatedAPIs} = require 'grim'
{CompositeDisposable, Emitter} = require 'event-kit'
fs = require 'fs-plus'
{convertStackTrace, convertLine} = require 'coffeestack'
Model = require './model'
{$} = require './space-pen-extensions'
WindowEventHandler = require './window-event-handler'
StylesElement = require './styles-element'
StorageFolder = require './storage-folder'
# Essential: Atom global for dealing with packages, themes, menus, and the window.
#
@@ -34,35 +34,36 @@ class Atom extends Model
atom = @deserialize(@loadState(mode)) ? new this({mode, @version})
atom.deserializeTimings.atom = Date.now() - startTime
workspaceViewDeprecationMessage = """
atom.workspaceView is no longer available.
In most cases you will not need the view. See the Workspace docs for
alternatives: https://atom.io/docs/api/latest/Workspace.
If you do need the view, please use `atom.views.getView(atom.workspace)`,
which returns an HTMLElement.
"""
if includeDeprecatedAPIs
workspaceViewDeprecationMessage = """
atom.workspaceView is no longer available.
In most cases you will not need the view. See the Workspace docs for
alternatives: https://atom.io/docs/api/latest/Workspace.
If you do need the view, please use `atom.views.getView(atom.workspace)`,
which returns an HTMLElement.
"""
serviceHubDeprecationMessage = """
atom.services is no longer available. To register service providers and
consumers, use the `providedServices` and `consumedServices` fields in
your package's package.json.
"""
serviceHubDeprecationMessage = """
atom.services is no longer available. To register service providers and
consumers, use the `providedServices` and `consumedServices` fields in
your package's package.json.
"""
Object.defineProperty atom, 'workspaceView',
get: ->
deprecate(workspaceViewDeprecationMessage)
atom.__workspaceView
set: (newValue) ->
deprecate(workspaceViewDeprecationMessage)
atom.__workspaceView = newValue
Object.defineProperty atom, 'workspaceView',
get: ->
deprecate(workspaceViewDeprecationMessage)
atom.__workspaceView
set: (newValue) ->
deprecate(workspaceViewDeprecationMessage)
atom.__workspaceView = newValue
Object.defineProperty atom, 'services',
get: ->
deprecate(serviceHubDeprecationMessage)
atom.packages.serviceHub
set: (newValue) ->
deprecate(serviceHubDeprecationMessage)
atom.packages.serviceHub = newValue
Object.defineProperty atom, 'services',
get: ->
deprecate(serviceHubDeprecationMessage)
atom.packages.serviceHub
set: (newValue) ->
deprecate(serviceHubDeprecationMessage)
atom.packages.serviceHub = newValue
atom
@@ -73,34 +74,24 @@ class Atom extends Model
# Loads and returns the serialized state corresponding to this window
# if it exists; otherwise returns undefined.
@loadState: (mode) ->
statePath = @getStatePath(@getLoadSettings().initialPaths, mode)
if stateKey = @getStateKey(@getLoadSettings().initialPaths, mode)
if state = @getStorageFolder().load(stateKey)
return state
if fs.existsSync(statePath)
if windowState = @getLoadSettings().windowState
try
stateString = fs.readFileSync(statePath, 'utf8')
JSON.parse(@getLoadSettings().windowState)
catch error
console.warn "Error reading window state: #{statePath}", error.stack, error
else
stateString = @getLoadSettings().windowState
try
JSON.parse(stateString) if stateString?
catch error
console.warn "Error parsing window state: #{statePath} #{error.stack}", error
console.warn "Error parsing window state: #{statePath} #{error.stack}", error
# Returns the path where the state for the current window will be
# located if it exists.
@getStatePath: (paths, mode) ->
switch mode
when 'spec'
filename = 'spec'
when 'editor'
if paths?.length > 0
sha1 = crypto.createHash('sha1').update(paths.slice().sort().join("\n")).digest('hex')
filename = "editor-#{sha1}"
if filename
path.join(@getStorageDirPath(), filename)
@getStateKey: (paths, mode) ->
if mode is 'spec'
'spec'
else if mode is 'editor' and paths?.length > 0
sha1 = crypto.createHash('sha1').update(paths.slice().sort().join("\n")).digest('hex')
"editor-#{sha1}"
else
null
@@ -110,15 +101,12 @@ class Atom extends Model
@getConfigDirPath: ->
@configDirPath ?= process.env.ATOM_HOME
# Get the path to Atom's storage directory.
#
# Returns the absolute path to ~/.atom/storage
@getStorageDirPath: ->
@storageDirPath ?= path.join(@getConfigDirPath(), 'storage')
@getStorageFolder: ->
@storageFolder ?= new StorageFolder(@getConfigDirPath())
# Returns the load settings hash associated with the current window.
@getLoadSettings: ->
@loadSettings ?= JSON.parse(decodeURIComponent(location.search.substr(14)))
@loadSettings ?= JSON.parse(decodeURIComponent(location.hash.substr(1)))
cloned = _.deepClone(@loadSettings)
# The loadSettings.windowState could be large, request it only when needed.
cloned.__defineGetter__ 'windowState', =>
@@ -127,6 +115,11 @@ class Atom extends Model
@getCurrentWindow().loadSettings.windowState = value
cloned
@updateLoadSetting: (key, value) ->
@getLoadSettings()
@loadSettings[key] = value
location.hash = encodeURIComponent(JSON.stringify(@loadSettings))
@getCurrentWindow: ->
remote.getCurrentWindow()
@@ -158,7 +151,7 @@ class Atom extends Model
# Public: A {TooltipManager} instance
tooltips: null
# Experimental: A {NotificationManager} instance
# Public: A {NotificationManager} instance
notifications: null
# Public: A {Project} instance
@@ -192,6 +185,7 @@ class Atom extends Model
# Call .loadOrCreate instead
constructor: (@state) ->
@emitter = new Emitter
@disposables = new CompositeDisposable
{@mode} = @state
DeserializerManager = require './deserializer-manager'
@deserializers = new DeserializerManager()
@@ -202,12 +196,6 @@ class Atom extends Model
#
# Call after this instance has been assigned to the `atom` global.
initialize: ->
# Disable deprecations unless in dev mode or spec mode so that regular
# editor performance isn't impacted by generating stack traces for
# deprecated calls.
unless @inDevMode() or @inSpecMode()
require('grim').deprecate = ->
sourceMapCache = {}
window.onerror = =>
@@ -227,12 +215,16 @@ class Atom extends Model
if openDevTools
@openDevTools()
@executeJavaScriptInDevTools('InspectorFrontendAPI.showConsole()')
@executeJavaScriptInDevTools('DevToolsAPI.showConsole()')
@emit 'uncaught-error', arguments...
@emit 'uncaught-error', arguments... if includeDeprecatedAPIs
@emitter.emit 'did-throw-error', {message, url, line, column, originalError}
@unsubscribe()
@disposables?.dispose()
@disposables = new CompositeDisposable
@displayWindow() unless @inSpecMode()
@setBodyPlatformClass()
@loadTime = null
@@ -264,7 +256,10 @@ class Atom extends Model
@config = new Config({configDirPath, resourcePath})
@keymaps = new KeymapManager({configDirPath, resourcePath})
@keymap = @keymaps # Deprecated
if includeDeprecatedAPIs
@keymap = @keymaps # Deprecated
@keymaps.subscribeToFileReadFailure()
@tooltips = new TooltipManager
@notifications = new NotificationManager
@@ -280,11 +275,12 @@ class Atom extends Model
@grammars = @deserializers.deserialize(@state.grammars ? @state.syntax) ? new GrammarRegistry()
Object.defineProperty this, 'syntax', get: ->
deprecate "The atom.syntax global is deprecated. Use atom.grammars instead."
@grammars
if includeDeprecatedAPIs
Object.defineProperty this, 'syntax', get: ->
deprecate "The atom.syntax global is deprecated. Use atom.grammars instead."
@grammars
@subscribe @packages.onDidActivateInitialPackages => @watchThemes()
@disposables.add @packages.onDidActivateInitialPackages => @watchThemes()
Project = require './project'
TextBuffer = require 'text-buffer'
@@ -343,15 +339,15 @@ class Atom extends Model
# Public: Is the current window in development mode?
inDevMode: ->
@getLoadSettings().devMode
@devMode ?= @getLoadSettings().devMode
# Public: Is the current window in safe mode?
inSafeMode: ->
@getLoadSettings().safeMode
@safeMode ?= @getLoadSettings().safeMode
# Public: Is the current window running specs?
inSpecMode: ->
@getLoadSettings().isSpec
@specMode ?= @getLoadSettings().isSpec
# Public: Get the version of the Atom application.
#
@@ -406,10 +402,11 @@ class Atom extends Model
open: (options) ->
ipc.send('open', options)
# Extended: Show the native dialog to prompt the user to select a folder.
# Extended: Prompt the user to select one or more folders.
#
# * `callback` A {Function} to call once the user has selected a folder.
# * `path` {String} the path to the folder the user selected.
# * `callback` A {Function} to call once the user has confirmed the selection.
# * `paths` An {Array} of {String} paths that the user selected, or `null`
# if the user dismissed the dialog.
pickFolder: (callback) ->
responseChannel = "atom-pick-folder-response"
ipc.on responseChannel, (path) ->
@@ -475,9 +472,13 @@ class Atom extends Model
ipc.send('call-window-method', 'restart')
# Extended: Returns a {Boolean} true when the current window is maximized.
isMaximixed: ->
isMaximized: ->
@getCurrentWindow().isMaximized()
isMaximixed: ->
deprecate "Use atom.isMaximized() instead"
@isMaximized()
maximize: ->
ipc.send('call-window-method', 'maximize')
@@ -488,22 +489,27 @@ class Atom extends Model
# Extended: Set the full screen state of the current window.
setFullScreen: (fullScreen=false) ->
ipc.send('call-window-method', 'setFullScreen', fullScreen)
if fullScreen then document.body.classList.add("fullscreen") else document.body.classList.remove("fullscreen")
if fullScreen
document.body.classList.add("fullscreen")
else
document.body.classList.remove("fullscreen")
# Extended: Toggle the full screen state of the current window.
toggleFullScreen: ->
@setFullScreen(!@isFullScreen())
@setFullScreen(not @isFullScreen())
# Schedule the window to be shown and focused on the next tick.
# Restore the window to its previous dimensions and show it.
#
# This is done in a next tick to prevent a white flicker from occurring
# if called synchronously.
displayWindow: ({maximize}={}) ->
# Also restores the full screen and maximized state on the next tick to
# prevent resize glitches.
displayWindow: ->
dimensions = @restoreWindowDimensions()
@show()
@focus()
setImmediate =>
@show()
@focus()
@setFullScreen(true) if @workspace.fullScreen
@maximize() if maximize
@setFullScreen(true) if @workspace?.fullScreen
@maximize() if dimensions?.maximized and process.platform isnt 'darwin'
# Get the dimensions of this window.
#
@@ -577,17 +583,23 @@ class Atom extends Model
dimensions = @getWindowDimensions()
@state.windowDimensions = dimensions if @isValidDimensions(dimensions)
storeWindowBackground: ->
return if @inSpecMode()
workspaceElement = @views.getView(@workspace)
backgroundColor = window.getComputedStyle(workspaceElement)['background-color']
window.localStorage.setItem('atom:window-background-color', backgroundColor)
# Call this method when establishing a real application window.
startEditorWindow: ->
{resourcePath, safeMode} = @getLoadSettings()
{safeMode} = @getLoadSettings()
CommandInstaller = require './command-installer'
CommandInstaller.installAtomCommand resourcePath, false, (error) ->
CommandInstaller.installAtomCommand false, (error) ->
console.warn error.message if error?
CommandInstaller.installApmCommand resourcePath, false, (error) ->
CommandInstaller.installApmCommand false, (error) ->
console.warn error.message if error?
dimensions = @restoreWindowDimensions()
@loadConfig()
@keymaps.loadBundledKeymaps()
@themes.loadBaseStylesheets()
@@ -601,16 +613,16 @@ class Atom extends Model
@requireUserInitScript() unless safeMode
@menu.update()
@subscribe @config.onDidChange 'core.autoHideMenuBar', ({newValue}) =>
@disposables.add @config.onDidChange 'core.autoHideMenuBar', ({newValue}) =>
@setAutoHideMenuBar(newValue)
@setAutoHideMenuBar(true) if @config.get('core.autoHideMenuBar')
maximize = dimensions?.maximized and process.platform isnt 'darwin'
@displayWindow({maximize})
@openInitialEmptyEditorIfNecessary()
unloadEditorWindow: ->
return if not @project
@storeWindowBackground()
@state.grammars = @grammars.serialize()
@state.project = @project.serialize()
@state.workspace = @workspace.serialize()
@@ -629,6 +641,10 @@ class Atom extends Model
@windowEventHandler?.unsubscribe()
openInitialEmptyEditorIfNecessary: ->
if @getLoadSettings().initialPaths?.length is 0 and @workspace.getPaneItems().length is 0
@workspace.open(null)
###
Section: Messaging the User
###
@@ -708,13 +724,18 @@ class Atom extends Model
deserializeWorkspaceView: ->
Workspace = require './workspace'
WorkspaceView = require './workspace-view'
if includeDeprecatedAPIs
WorkspaceView = require './workspace-view'
startTime = Date.now()
@workspace = Workspace.deserialize(@state.workspace) ? new Workspace
workspaceElement = @views.getView(@workspace)
@__workspaceView = workspaceElement.__spacePenView
if includeDeprecatedAPIs
@__workspaceView = workspaceElement.__spacePenView
@deserializeTimings.workspace = Date.now() - startTime
@keymaps.defaultTarget = workspaceElement
@@ -741,14 +762,12 @@ class Atom extends Model
# Only reload stylesheets from non-theme packages
for pack in @packages.getActivePackages() when pack.getType() isnt 'theme'
pack.reloadStylesheets?()
null
return
# Notify the browser project of the window's current project path
watchProjectPath: ->
onProjectPathChanged = =>
ipc.send('window-command', 'project-path-changed', @project.getPaths())
@subscribe @project.onDidChangePaths(onProjectPathChanged)
onProjectPathChanged()
@disposables.add @project.onDidChangePaths =>
@constructor.updateLoadSetting('initialPaths', @project.getPaths())
exit: (status) ->
app = remote.require('app')
@@ -761,21 +780,29 @@ class Atom extends Model
setRepresentedFilename: (filename) ->
ipc.send('call-window-method', 'setRepresentedFilename', filename)
addProjectFolder: ->
@pickFolder (selectedPaths = []) =>
@project.addPath(selectedPath) for selectedPath in selectedPaths
showSaveDialog: (callback) ->
callback(showSaveDialogSync())
showSaveDialogSync: (defaultPath) ->
defaultPath ?= @project?.getPath()
showSaveDialogSync: (options={}) ->
if _.isString(options)
options = defaultPath: options
else
options = _.clone(options)
currentWindow = @getCurrentWindow()
dialog = remote.require('dialog')
dialog.showSaveDialog currentWindow, {title: 'Save File', defaultPath}
options.title ?= 'Save File'
options.defaultPath ?= @project?.getPaths()[0]
dialog.showSaveDialog currentWindow, options
saveSync: ->
stateString = JSON.stringify(@state)
if statePath = @constructor.getStatePath(@project?.getPaths(), @mode)
fs.writeFileSync(statePath, stateString, 'utf8')
if storageKey = @constructor.getStateKey(@project?.getPaths(), @mode)
@constructor.getStorageFolder().store(storageKey, @state)
else
@getCurrentWindow().loadSettings.windowState = stateString
@getCurrentWindow().loadSettings.windowState = JSON.stringify(@state)
crashMainProcess: ->
remote.process.crash()
@@ -816,6 +843,7 @@ class Atom extends Model
delete window[key]
else
window[key] = value
return
onUpdateAvailable: (callback) ->
@emitter.on 'update-available', callback
@@ -823,17 +851,18 @@ class Atom extends Model
updateAvailable: (details) ->
@emitter.emit 'update-available', details
# Deprecated: Callers should be converted to use atom.deserializers
registerRepresentationClass: ->
deprecate("Callers should be converted to use atom.deserializers")
# Deprecated: Callers should be converted to use atom.deserializers
registerRepresentationClasses: ->
deprecate("Callers should be converted to use atom.deserializers")
setBodyPlatformClass: ->
document.body.classList.add("platform-#{process.platform}")
setAutoHideMenuBar: (autoHide) ->
ipc.send('call-window-method', 'setAutoHideMenuBar', autoHide)
ipc.send('call-window-method', 'setMenuBarVisibility', !autoHide)
ipc.send('call-window-method', 'setMenuBarVisibility', not autoHide)
if includeDeprecatedAPIs
# Deprecated: Callers should be converted to use atom.deserializers
Atom::registerRepresentationClass = ->
deprecate("Callers should be converted to use atom.deserializers")
# Deprecated: Callers should be converted to use atom.deserializers
Atom::registerRepresentationClasses = ->
deprecate("Callers should be converted to use atom.deserializers")

View File

@@ -28,21 +28,17 @@ defaultOptions =
'useStrict'
]
# Includes support for es7 features listed at:
# http://babeljs.io/docs/usage/transformers/#es7-experimental-.
experimental: true
optional: [
# Target a version of the regenerator runtime that
# supports yield so the transpiled code is cleaner/smaller.
'asyncToGenerator'
# Because Atom is currently packaged with a fork of React v0.11,
# it makes sense to use the reactCompat transform so the React
# JSX transformer produces pre-v0.12 code.
'reactCompat'
]
# Includes support for es7 features listed at:
# http://babeljs.io/docs/usage/experimental/.
stage: 0
###
shasum - Hash with an update() method.
value - Must be a value that could be returned by JSON.parse().

View File

@@ -9,11 +9,10 @@ _ = require 'underscore-plus'
# and maintain the state of all menu items.
module.exports =
class ApplicationMenu
constructor: (@version) ->
constructor: (@version, @autoUpdateManager) ->
@windowTemplates = new WeakMap()
@setActiveTemplate(@getDefaultTemplate())
global.atomApplication.autoUpdateManager.on 'state-changed', (state) =>
@showUpdateMenuItem(state)
@autoUpdateManager.on 'state-changed', (state) => @showUpdateMenuItem(state)
# Public: Updates the entire menu with the given keybindings.
#
@@ -33,7 +32,7 @@ class ApplicationMenu
@menu = Menu.buildFromTemplate(_.deepClone(template))
Menu.setApplicationMenu(@menu)
@showUpdateMenuItem(global.atomApplication.autoUpdateManager.getState())
@showUpdateMenuItem(@autoUpdateManager.getState())
# Register a BrowserWindow with this application menu.
addWindow: (window) ->
@@ -82,19 +81,20 @@ class ApplicationMenu
# window specific items.
enableWindowSpecificItems: (enable) ->
for item in @flattenMenuItems(@menu)
item.enabled = enable if item.metadata?['windowSpecific']
item.enabled = enable if item.metadata?.windowSpecific
return
# Replaces VERSION with the current version.
substituteVersion: (template) ->
if (item = _.find(@flattenMenuTemplate(template), ({label}) -> label == 'VERSION'))
if (item = _.find(@flattenMenuTemplate(template), ({label}) -> label is 'VERSION'))
item.label = "Version #{@version}"
# Sets the proper visible state the update menu items
showUpdateMenuItem: (state) ->
checkForUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label == 'Check for Update')
checkingForUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label == 'Checking for Update')
downloadingUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label == 'Downloading Update')
installUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label == 'Restart and Install Update')
checkForUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Check for Update')
checkingForUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Checking for Update')
downloadingUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Downloading Update')
installUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Restart and Install Update')
return unless checkForUpdateItem? and checkingForUpdateItem? and downloadingUpdateItem? and installUpdateItem?
@@ -120,11 +120,11 @@ class ApplicationMenu
[
label: "Atom"
submenu: [
{ label: "Check for Update", metadata: {autoUpdate: true}}
{ label: 'Reload', accelerator: 'Command+R', click: => @focusedWindow()?.reload() }
{ label: 'Close Window', accelerator: 'Command+Shift+W', click: => @focusedWindow()?.close() }
{ label: 'Toggle Dev Tools', accelerator: 'Command+Alt+I', click: => @focusedWindow()?.toggleDevTools() }
{ label: 'Quit', accelerator: 'Command+Q', click: -> app.quit() }
{label: "Check for Update", metadata: {autoUpdate: true}}
{label: 'Reload', accelerator: 'Command+R', click: => @focusedWindow()?.reload()}
{label: 'Close Window', accelerator: 'Command+Shift+W', click: => @focusedWindow()?.close()}
{label: 'Toggle Dev Tools', accelerator: 'Command+Alt+I', click: => @focusedWindow()?.toggleDevTools()}
{label: 'Quit', accelerator: 'Command+Q', click: -> app.quit()}
]
]
@@ -145,7 +145,7 @@ class ApplicationMenu
if item.command
item.accelerator = @acceleratorForCommand(item.command, keystrokesByCommand)
item.click = -> global.atomApplication.sendCommand(item.command)
item.metadata['windowSpecific'] = true unless /^application:/.test(item.command)
item.metadata.windowSpecific = true unless /^application:/.test(item.command)
@translateTemplate(item.submenu, keystrokesByCommand) if item.submenu
template
@@ -161,8 +161,8 @@ class ApplicationMenu
firstKeystroke = keystrokesByCommand[command]?[0]
return null unless firstKeystroke
modifiers = firstKeystroke.split('-')
key = modifiers.pop()
modifiers = firstKeystroke.split(/-(?=.)/)
key = modifiers.pop().toUpperCase().replace('+', 'Plus')
modifiers = modifiers.map (modifier) ->
modifier.replace(/shift/ig, "Shift")
@@ -170,5 +170,5 @@ class ApplicationMenu
.replace(/ctrl/ig, "Ctrl")
.replace(/alt/ig, "Alt")
keys = modifiers.concat([key.toUpperCase()])
keys = modifiers.concat([key])
keys.join("+")

View File

@@ -3,6 +3,7 @@ ApplicationMenu = require './application-menu'
AtomProtocolHandler = require './atom-protocol-handler'
AutoUpdateManager = require './auto-update-manager'
BrowserWindow = require 'browser-window'
StorageFolder = require '../storage-folder'
Menu = require 'menu'
app = require 'app'
fs = require 'fs-plus'
@@ -18,7 +19,7 @@ DefaultSocketPath =
if process.platform is 'win32'
'\\\\.\\pipe\\atom-sock'
else
path.join(os.tmpdir(), 'atom.sock')
path.join(os.tmpdir(), "atom-#{process.env.USER}.sock")
# The application's singleton class.
#
@@ -43,7 +44,6 @@ class AtomApplication
createAtomApplication()
return
client = net.connect {path: options.socketPath}, ->
client.write JSON.stringify(options), ->
client.end()
@@ -56,11 +56,12 @@ class AtomApplication
atomProtocolHandler: null
resourcePath: null
version: null
quitting: false
exit: (status) -> app.exit(status)
constructor: (options) ->
{@resourcePath, @version, @devMode, @safeMode, @socketPath, @enableMultiFolderProject} = options
{@resourcePath, @version, @devMode, @safeMode, @socketPath} = options
# Normalize to make sure drive letter case is consistent on Windows
@resourcePath = path.normalize(@resourcePath) if @resourcePath
@@ -71,31 +72,40 @@ class AtomApplication
@pathsToOpen ?= []
@windows = []
@autoUpdateManager = new AutoUpdateManager(@version)
@applicationMenu = new ApplicationMenu(@version)
@autoUpdateManager = new AutoUpdateManager(@version, options.test)
@applicationMenu = new ApplicationMenu(@version, @autoUpdateManager)
@atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode)
@listenForArgumentsFromNewProcess()
@setupJavaScriptArguments()
@handleEvents()
@storageFolder = new StorageFolder(process.env.ATOM_HOME)
@openWithOptions(options)
if options.pathsToOpen?.length > 0 or options.urlsToOpen?.length > 0 or options.test
@openWithOptions(options)
else
@loadState() or @openPath(options)
# Opens a new window based on the options provided.
openWithOptions: ({pathsToOpen, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, specDirectory, logFile}) ->
openWithOptions: ({pathsToOpen, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, specDirectory, logFile, profileStartup}) ->
if test
@runSpecs({exitWhenDone: true, @resourcePath, specDirectory, logFile})
else if pathsToOpen.length > 0
@openPaths({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode})
@openPaths({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup})
else if urlsToOpen.length > 0
@openUrl({urlToOpen, devMode, safeMode}) for urlToOpen in urlsToOpen
else
@openPath({pidToKillWhenClosed, newWindow, devMode, safeMode}) # Always open a editor window if this is the first instance of Atom.
# Always open a editor window if this is the first instance of Atom.
@openPath({pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup})
# Public: Removes the {AtomWindow} from the global window list.
removeWindow: (window) ->
@windows.splice @windows.indexOf(window), 1
@applicationMenu?.enableWindowSpecificItems(false) if @windows.length == 0
if @windows.length is 1
@applicationMenu?.enableWindowSpecificItems(false)
if process.platform in ['win32', 'linux']
app.quit()
return
@windows.splice(@windows.indexOf(window), 1)
@saveState(true) unless window.isSpec
# Public: Adds the {AtomWindow} to the global window list.
addWindow: (window) ->
@@ -106,10 +116,14 @@ class AtomApplication
unless window.isSpec
focusHandler = => @lastFocusedWindow = window
blurHandler = => @saveState(false)
window.browserWindow.on 'focus', focusHandler
window.browserWindow.on 'blur', blurHandler
window.browserWindow.once 'closed', =>
@lastFocusedWindow = null if window is @lastFocusedWindow
window.browserWindow.removeListener 'focus', focusHandler
window.browserWindow.removeListener 'blur', blurHandler
window.browserWindow.webContents.once 'did-finish-load', => @saveState(false)
# Creates server to listen for additional atom application launches.
#
@@ -157,7 +171,7 @@ class AtomApplication
@on 'application:open-folder', -> @promptForPathToOpen('folder', getLoadSettings())
@on 'application:open-dev', -> @promptForPathToOpen('all', devMode: true)
@on 'application:open-safe', -> @promptForPathToOpen('all', safeMode: true)
@on 'application:inspect', ({x,y, atomWindow}) ->
@on 'application:inspect', ({x, y, atomWindow}) ->
atomWindow ?= @focusedWindow()
atomWindow?.browserWindow.inspectElement(x, y)
@@ -166,10 +180,13 @@ class AtomApplication
@on 'application:open-roadmap', -> require('shell').openExternal('https://atom.io/roadmap?app')
@on 'application:open-faq', -> require('shell').openExternal('https://atom.io/faq')
@on 'application:open-terms-of-use', -> require('shell').openExternal('https://atom.io/terms')
@on 'application:report-issue', -> require('shell').openExternal('https://github.com/atom/atom/issues/new')
@on 'application:report-issue', -> require('shell').openExternal('https://github.com/atom/atom/blob/master/CONTRIBUTING.md#submitting-issues')
@on 'application:search-issues', -> require('shell').openExternal('https://github.com/issues?q=+is%3Aissue+user%3Aatom')
@on 'application:install-update', -> @autoUpdateManager.install()
@on 'application:install-update', =>
@quitting = true
@autoUpdateManager.install()
@on 'application:check-for-update', => @autoUpdateManager.check()
if process.platform is 'darwin'
@@ -190,16 +207,18 @@ class AtomApplication
@openPathOnEvent('application:open-your-keymap', 'atom://.atom/keymap')
@openPathOnEvent('application:open-your-snippets', 'atom://.atom/snippets')
@openPathOnEvent('application:open-your-stylesheet', 'atom://.atom/stylesheet')
@openPathOnEvent('application:open-license', path.join(@resourcePath, 'LICENSE.md'))
@openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md'))
app.on 'window-all-closed', ->
app.quit() if process.platform in ['win32', 'linux']
app.on 'before-quit', =>
@saveState(false)
@quitting = true
app.on 'will-quit', =>
@killAllProcesses()
@deleteSocketFile()
app.on 'will-exit', =>
@saveState(false)
@killAllProcesses()
@deleteSocketFile()
@@ -251,9 +270,11 @@ class AtomApplication
@promptForPath "folder", (selectedPaths) ->
event.sender.send(responseChannel, selectedPaths)
clipboard = null
ipc.on 'cancel-window-close', =>
@quitting = false
clipboard = require '../safe-clipboard'
ipc.on 'write-text-to-selection-clipboard', (event, selectedText) ->
clipboard ?= require 'clipboard'
clipboard.writeText(selectedText, 'selection')
# Public: Executes the given command.
@@ -325,7 +346,7 @@ class AtomApplication
focusedWindow: ->
_.find @windows, (atomWindow) -> atomWindow.isFocused()
# Public: Opens multiple paths, in existing windows if possible.
# Public: Opens a single path, in an existing window if possible.
#
# options -
# :pathToOpen - The file path to open
@@ -333,11 +354,12 @@ class AtomApplication
# :newWindow - Boolean of whether this should be opened in a new window.
# :devMode - Boolean to control the opened window's dev mode.
# :safeMode - Boolean to control the opened window's safe mode.
# :profileStartup - Boolean to control creating a profile of the startup time.
# :window - {AtomWindow} to open file paths in.
openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, window}) ->
@openPaths({pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, window})
openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window}) ->
@openPaths({pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window})
# Public: Opens a single path, in an existing window if possible.
# Public: Opens multiple paths, in existing windows if possible.
#
# options -
# :pathsToOpen - The array of file paths to open
@@ -347,13 +369,13 @@ class AtomApplication
# :safeMode - Boolean to control the opened window's safe mode.
# :windowDimensions - Object with height and width keys.
# :window - {AtomWindow} to open file paths in.
openPaths: ({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, window}={}) ->
if pathsToOpen?.length > 1 and not @enableMultiFolderProject
for pathToOpen in pathsToOpen
@openPath({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, window})
return
openPaths: ({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window}={}) ->
pathsToOpen = pathsToOpen.map (pathToOpen) ->
if fs.existsSync(pathToOpen)
fs.normalize(pathToOpen)
else
pathToOpen
pathsToOpen = (fs.normalize(pathToOpen) for pathToOpen in pathsToOpen)
locationsToOpen = (@locationForPathToOpen(pathToOpen) for pathToOpen in pathsToOpen)
unless pidToKillWhenClosed or newWindow
@@ -382,7 +404,7 @@ class AtomApplication
bootstrapScript ?= require.resolve('../window-bootstrap')
resourcePath ?= @resourcePath
openedWindow = new AtomWindow({locationsToOpen, bootstrapScript, resourcePath, devMode, safeMode, windowDimensions})
openedWindow = new AtomWindow({locationsToOpen, bootstrapScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup})
if pidToKillWhenClosed?
@pidsToOpenWindows[pidToKillWhenClosed] = openedWindow
@@ -393,11 +415,13 @@ class AtomApplication
# Kill all processes associated with opened windows.
killAllProcesses: ->
@killProcess(pid) for pid of @pidsToOpenWindows
return
# Kill process associated with the given opened window.
killProcessForWindow: (openedWindow) ->
for pid, trackedWindow of @pidsToOpenWindows
@killProcess(pid) if trackedWindow is openedWindow
return
# Kill the process with the given pid.
killProcess: (pid) ->
@@ -409,6 +433,29 @@ class AtomApplication
console.log("Killing process #{pid} failed: #{error.code ? error.message}")
delete @pidsToOpenWindows[pid]
saveState: (allowEmpty=false) ->
return if @quitting
states = []
for window in @windows
unless window.isSpec
if loadSettings = window.getLoadSettings()
states.push(initialPaths: loadSettings.initialPaths)
if states.length > 0 or allowEmpty
@storageFolder.store('application.json', states)
loadState: ->
if (states = @storageFolder.load('application.json'))?.length > 0
for state in states
@openWithOptions({
pathsToOpen: state.initialPaths
urlsToOpen: []
devMode: @devMode
safeMode: @safeMode
})
true
else
false
# Open an atom:// url.
#
# The host of the URL being opened is assumed to be the package name
@@ -477,15 +524,18 @@ class AtomApplication
locationForPathToOpen: (pathToOpen) ->
return {pathToOpen} unless pathToOpen
return {pathToOpen} if url.parse(pathToOpen).protocol?
return {pathToOpen} if fs.existsSync(pathToOpen)
pathToOpen = pathToOpen.replace(/[:\s]+$/, '')
[fileToOpen, initialLine, initialColumn] = path.basename(pathToOpen).split(':')
return {pathToOpen} unless initialLine
return {pathToOpen} unless parseInt(initialLine) > 0
return {pathToOpen} unless parseInt(initialLine) >= 0
# Convert line numbers to a base of 0
initialLine -= 1 if initialLine
initialColumn -= 1 if initialColumn
initialLine = Math.max(0, initialLine - 1) if initialLine
initialColumn = Math.max(0, initialColumn - 1) if initialColumn
pathToOpen = path.join(path.dirname(pathToOpen), fileToOpen)
{pathToOpen, initialLine, initialColumn}

View File

@@ -61,39 +61,41 @@ class AtomWindow
pathToOpen
loadSettings.initialPaths.sort()
@projectPaths = loadSettings.initialPaths
@browserWindow.loadSettings = loadSettings
@browserWindow.once 'window:loaded', =>
@emit 'window:loaded'
@loaded = true
@browserWindow.on 'project-path-changed', (@projectPaths) =>
@browserWindow.loadUrl @getUrl(loadSettings)
@setLoadSettings(loadSettings)
@browserWindow.focusOnWebView() if @isSpec
@openLocations(locationsToOpen) unless @isSpecWindow()
hasPathToOpen = not (locationsToOpen.length is 1 and not locationsToOpen[0].pathToOpen?)
@openLocations(locationsToOpen) if hasPathToOpen and not @isSpecWindow()
getUrl: (loadSettingsObj) ->
setLoadSettings: (loadSettingsObj) ->
# Ignore the windowState when passing loadSettings via URL, since it could
# be quite large.
loadSettings = _.clone(loadSettingsObj)
delete loadSettings['windowState']
url.format
@browserWindow.loadUrl url.format
protocol: 'file'
pathname: "#{@resourcePath}/static/index.html"
slashes: true
query: {loadSettings: JSON.stringify(loadSettings)}
hash: encodeURIComponent(JSON.stringify(loadSettings))
hasProjectPath: -> @projectPaths?.length > 0
getLoadSettings: ->
if @browserWindow.webContents?.loaded
hash = url.parse(@browserWindow.webContents.getUrl()).hash.substr(1)
JSON.parse(decodeURIComponent(hash))
hasProjectPath: -> @getLoadSettings().initialPaths?.length > 0
setupContextMenu: ->
ContextMenu = null
ContextMenu = require './context-menu'
@browserWindow.on 'context-menu', (menuTemplate) =>
ContextMenu ?= require './context-menu'
new ContextMenu(menuTemplate, this)
containsPaths: (paths) ->
@@ -102,7 +104,7 @@ class AtomWindow
true
containsPath: (pathToCheck) ->
@projectPaths.some (projectPath) ->
@getLoadSettings()?.initialPaths?.some (projectPath) ->
if not projectPath
false
else if not pathToCheck
@@ -162,7 +164,6 @@ class AtomWindow
openLocations: (locationsToOpen) ->
if @loaded
@focus()
@sendMessage 'open-locations', locationsToOpen
else
@browserWindow.once 'window:loaded', => @openLocations(locationsToOpen)

View File

@@ -15,7 +15,7 @@ module.exports =
class AutoUpdateManager
_.extend @prototype, EventEmitter.prototype
constructor: (@version) ->
constructor: (@version, @testMode) ->
@state = IdleState
if process.platform is 'win32'
# Squirrel for Windows can't handle query params
@@ -33,6 +33,10 @@ class AutoUpdateManager
else
autoUpdater = require 'auto-updater'
autoUpdater.on 'error', (event, message) =>
@setState(ErrorState)
console.error "Error Downloading Update: #{message}"
autoUpdater.setFeedUrl @feedUrl
autoUpdater.on 'checking-for-update', =>
@@ -44,16 +48,12 @@ class AutoUpdateManager
autoUpdater.on 'update-available', =>
@setState(DownladingState)
autoUpdater.on 'error', (event, message) =>
@setState(ErrorState)
console.error "Error Downloading Update: #{message}"
autoUpdater.on 'update-downloaded', (event, releaseNotes, @releaseVersion) =>
@setState(UpdateAvailableState)
@emitUpdateAvailableEvent(@getWindows()...)
# Only released versions should check for updates.
@check(hidePopups: true) unless /\w{7}/.test(@version)
@scheduleUpdateCheck() unless /\w{7}/.test(@version)
switch process.platform
when 'win32'
@@ -65,6 +65,7 @@ class AutoUpdateManager
return unless @releaseVersion?
for atomWindow in windows
atomWindow.sendMessage('update-available', {@releaseVersion})
return
setState: (state) ->
return if @state is state
@@ -74,15 +75,21 @@ class AutoUpdateManager
getState: ->
@state
scheduleUpdateCheck: ->
checkForUpdates = => @check(hidePopups: true)
fourHours = 1000 * 60 * 60 * 4
setInterval(checkForUpdates, fourHours)
checkForUpdates()
check: ({hidePopups}={}) ->
unless hidePopups
autoUpdater.once 'update-not-available', @onUpdateNotAvailable
autoUpdater.once 'error', @onUpdateError
autoUpdater.checkForUpdates()
autoUpdater.checkForUpdates() unless @testMode
install: ->
autoUpdater.quitAndInstall()
autoUpdater.quitAndInstall() unless @testMode
onUpdateNotAvailable: =>
autoUpdater.removeListener 'error', @onUpdateError

View File

@@ -51,12 +51,13 @@ class AutoUpdater
@emit 'update-not-available'
return
@emit 'update-available'
@installUpdate (error) =>
if error?
@emit 'update-not-available'
return
@emit 'update-available'
@emit 'update-downloaded', {}, update.releaseNotes, update.version, new Date(), 'https://atom.io', => @quitAndInstall()
module.exports = new AutoUpdater()

View File

@@ -4,7 +4,8 @@ crashReporter = require 'crash-reporter'
app = require 'app'
fs = require 'fs-plus'
path = require 'path'
optimist = require 'optimist'
yargs = require 'yargs'
url = require 'url'
nslog = require 'nslog'
console.log = nslog
@@ -45,9 +46,11 @@ start = ->
cwd = args.executedFrom?.toString() or process.cwd()
args.pathsToOpen = args.pathsToOpen.map (pathToOpen) ->
pathToOpen = fs.normalize(pathToOpen)
if cwd
path.resolve(cwd, pathToOpen)
normalizedPath = fs.normalize(pathToOpen)
if url.parse(pathToOpen).protocol?
pathToOpen
else if cwd
path.resolve(cwd, normalizedPath)
else
path.resolve(pathToOpen)
@@ -85,18 +88,16 @@ setupCoffeeCache = ->
parseCommandLine = ->
version = app.getVersion()
options = optimist(process.argv[1..])
options = yargs(process.argv[1..]).wrap(100)
options.usage """
Atom Editor v#{version}
Usage: atom [options] [path ...]
One or more paths to files or folders to open may be specified.
File paths will open in the current window.
Folder paths will open in an existing window if that folder has already been
opened or a new window if it hasn't.
One or more paths to files or folders may be specified. If there is an
existing Atom window that contains all of the given folders, the paths
will be opened in that window. Otherwise, they will be opened in a new
window.
Environment Variables:
@@ -106,11 +107,15 @@ parseCommandLine = ->
ATOM_HOME The root path for all configuration files and folders.
Defaults to `~/.atom`.
"""
# Deprecated 1.0 API preview flag
options.alias('1', 'one').boolean('1').describe('1', 'This option is no longer supported. Atom now defaults to launching with the 1.0 API. Use --include-deprecated-apis to run Atom with deprecated APIs.')
options.boolean('include-deprecated-apis').describe('include-deprecated-apis', 'Include deprecated APIs.')
options.alias('d', 'dev').boolean('d').describe('d', 'Run in development mode.')
options.alias('f', 'foreground').boolean('f').describe('f', 'Keep the browser process in the foreground.')
options.alias('h', 'help').boolean('h').describe('h', 'Print this usage message.')
options.alias('l', 'log-file').string('l').describe('l', 'Log all output to file.')
options.alias('n', 'new-window').boolean('n').describe('n', 'Open a new window.')
options.boolean('profile-startup').describe('profile-startup', 'Create a profile of the startup execution time.')
options.alias('r', 'resource-path').string('r').describe('r', 'Set the path to the Atom source directory and enable dev-mode.')
options.alias('s', 'spec-directory').string('s').describe('s', 'Set the directory from which to run package specs (default: Atom\'s spec directory).')
options.boolean('safe').describe('safe', 'Do not load packages from ~/.atom/packages or ~/.atom/dev/packages.')
@@ -118,7 +123,7 @@ parseCommandLine = ->
options.alias('v', 'version').boolean('v').describe('v', 'Print the version.')
options.alias('w', 'wait').boolean('w').describe('w', 'Wait for window to be closed before returning.')
options.string('socket-path')
options.boolean('multi-folder')
args = options.argv
if args.help
@@ -133,14 +138,13 @@ parseCommandLine = ->
devMode = args['dev']
safeMode = args['safe']
pathsToOpen = args._
pathsToOpen = [executedFrom] if executedFrom and pathsToOpen.length is 0
test = args['test']
specDirectory = args['spec-directory']
newWindow = args['new-window']
pidToKillWhenClosed = args['pid'] if args['wait']
logFile = args['log-file']
socketPath = args['socket-path']
enableMultiFolderProject = args['multi-folder']
profileStartup = args['profile-startup']
if args['resource-path']
devMode = true
@@ -166,6 +170,6 @@ parseCommandLine = ->
process.env.PATH = args['path-environment'] if args['path-environment']
{resourcePath, pathsToOpen, executedFrom, test, version, pidToKillWhenClosed,
devMode, safeMode, newWindow, specDirectory, logFile, socketPath, enableMultiFolderProject}
devMode, safeMode, newWindow, specDirectory, logFile, socketPath, profileStartup}
start()

View File

@@ -76,11 +76,23 @@ installContextMenu = (callback) ->
installMenu directoryKeyPath, '%1', ->
installMenu(backgroundKeyPath, '%V', callback)
isAscii = (text) ->
index = 0
while index < text.length
return false if text.charCodeAt(index) > 127
index++
true
# Get the user's PATH environment variable registry value.
getPath = (callback) ->
spawnReg ['query', environmentKeyPath, '/v', 'Path'], (error, stdout) ->
if error?
if error.code is 1
# FIXME Don't overwrite path when reading value is disabled
# https://github.com/atom/atom/issues/5092
if stdout.indexOf('ERROR: Registry editing has been disabled by your administrator.') isnt -1
return callback(error)
# The query failed so the Path does not exist yet in the registry
return callback(null, '')
else
@@ -96,7 +108,12 @@ getPath = (callback) ->
segments = lines[lines.length - 1]?.split(' ')
if segments[1] is 'Path' and segments.length >= 3
pathEnv = segments?[3..].join(' ')
callback(null, pathEnv)
if isAscii(pathEnv)
callback(null, pathEnv)
else
# FIXME Don't corrupt non-ASCII PATH values
# https://github.com/atom/atom/issues/5063
callback(new Error('PATH contains non-ASCII values'))
else
callback(new Error('Registry query for PATH failed'))
@@ -122,7 +139,7 @@ addCommandsToPath = (callback) ->
atomShCommandPath = path.join(binFolder, 'atom')
relativeAtomShPath = path.relative(binFolder, path.join(appFolder, 'resources', 'cli', 'atom.sh'))
atomShCommand = "#!/bin/sh\r\n\"$0/../#{relativeAtomShPath.replace(/\\/g, '/')}\" \"$@\""
atomShCommand = "#!/bin/sh\r\n\"$(dirname \"$0\")/#{relativeAtomShPath.replace(/\\/g, '/')}\" \"$@\""
apmCommandPath = path.join(binFolder, 'apm.cmd')
relativeApmPath = path.relative(binFolder, path.join(process.resourcesPath, 'app', 'apm', 'bin', 'apm.cmd'))
@@ -130,7 +147,7 @@ addCommandsToPath = (callback) ->
apmShCommandPath = path.join(binFolder, 'apm')
relativeApmShPath = path.relative(binFolder, path.join(appFolder, 'resources', 'cli', 'apm.sh'))
apmShCommand = "#!/bin/sh\r\n\"$0/../#{relativeApmShPath.replace(/\\/g, '/')}\" \"$@\""
apmShCommand = "#!/bin/sh\r\n\"$(dirname \"$0\")/#{relativeApmShPath.replace(/\\/g, '/')}\" \"$@\""
fs.writeFile atomCommandPath, atomCommand, ->
fs.writeFile atomShCommandPath, atomShCommand, ->

View File

@@ -48,6 +48,7 @@ class BufferedProcess
constructor: ({command, args, options, stdout, stderr, exit}={}) ->
@emitter = new Emitter
options ?= {}
@command = command
# Related to joyent/node#2318
if process.platform is 'win32'
# Quote all arguments and escapes inner quotes
@@ -69,50 +70,12 @@ class BufferedProcess
cmdArgs = ['/s', '/c', "\"#{cmdArgs.join(' ')}\""]
cmdOptions = _.clone(options)
cmdOptions.windowsVerbatimArguments = true
@process = ChildProcess.spawn(@getCmdPath(), cmdArgs, cmdOptions)
@spawn(@getCmdPath(), cmdArgs, cmdOptions)
else
@process = ChildProcess.spawn(command, args, options)
@spawn(command, args, options)
@killed = false
stdoutClosed = true
stderrClosed = true
processExited = true
exitCode = 0
triggerExitCallback = ->
return if @killed
if stdoutClosed and stderrClosed and processExited
exit?(exitCode)
if stdout
stdoutClosed = false
@bufferStream @process.stdout, stdout, ->
stdoutClosed = true
triggerExitCallback()
if stderr
stderrClosed = false
@bufferStream @process.stderr, stderr, ->
stderrClosed = true
triggerExitCallback()
if exit
processExited = false
@process.on 'exit', (code) ->
exitCode = code
processExited = true
triggerExitCallback()
@process.on 'error', (error) =>
handled = false
handle = -> handled = true
@emitter.emit 'will-throw-error', {error, handle}
if error.code is 'ENOENT' and error.syscall.indexOf('spawn') is 0
error = new Error("Failed to spawn command `#{command}`. Make sure `#{command}` is installed and on your PATH", error.path)
error.name = 'BufferedProcessError'
throw error unless handled
@handleEvents(stdout, stderr, exit)
###
Section: Event Subscription
@@ -164,6 +127,8 @@ class BufferedProcess
# This is required since killing the cmd.exe does not terminate child
# processes.
killOnWindows: ->
return unless @process?
parentPid = @process.pid
cmd = 'wmic'
args = [
@@ -174,7 +139,12 @@ class BufferedProcess
'processid'
]
wmicProcess = ChildProcess.spawn(cmd, args)
try
wmicProcess = ChildProcess.spawn(cmd, args)
catch spawnError
@killProcess()
return
wmicProcess.on 'error', -> # ignore errors
output = ''
wmicProcess.stdout.on 'data', (data) -> output += data
@@ -220,3 +190,55 @@ class BufferedProcess
@killProcess()
undefined
spawn: (command, args, options) ->
try
@process = ChildProcess.spawn(command, args, options)
catch spawnError
process.nextTick => @handleError(spawnError)
handleEvents: (stdout, stderr, exit) ->
return unless @process?
stdoutClosed = true
stderrClosed = true
processExited = true
exitCode = 0
triggerExitCallback = ->
return if @killed
if stdoutClosed and stderrClosed and processExited
exit?(exitCode)
if stdout
stdoutClosed = false
@bufferStream @process.stdout, stdout, ->
stdoutClosed = true
triggerExitCallback()
if stderr
stderrClosed = false
@bufferStream @process.stderr, stderr, ->
stderrClosed = true
triggerExitCallback()
if exit
processExited = false
@process.on 'exit', (code) ->
exitCode = code
processExited = true
triggerExitCallback()
@process.on 'error', (error) => @handleError(error)
return
handleError: (error) ->
handled = false
handle = -> handled = true
@emitter.emit 'will-throw-error', {error, handle}
if error.code is 'ENOENT' and error.syscall.indexOf('spawn') is 0
error = new Error("Failed to spawn command `#{@command}`. Make sure `#{@command}` is installed and on your PATH", error.path)
error.name = 'BufferedProcessError'
throw error unless handled

View File

@@ -1,5 +1,5 @@
clipboard = require 'clipboard'
crypto = require 'crypto'
clipboard = require './safe-clipboard'
# Extended: Represents the clipboard used for copying and pasting in Atom.
#
@@ -31,7 +31,7 @@ class Clipboard
# {::readWithMetadata}.
#
# * `text` The {String} to store.
# * `metadata` The additional info to associate with the text.
# * `metadata` (optional) The additional info to associate with the text.
write: (text, metadata) ->
@signatureForMetadata = @md5(text)
@metadata = metadata

View File

@@ -1,12 +1,10 @@
path = require 'path'
_ = require 'underscore-plus'
async = require 'async'
fs = require 'fs-plus'
runas = null # defer until used
symlinkCommand = (sourcePath, destinationPath, callback) ->
fs.unlink destinationPath, (error) ->
if error? and error?.code != 'ENOENT'
if error? and error?.code isnt 'ENOENT'
callback(error)
else
fs.makeTree path.dirname(destinationPath), (error) ->
@@ -17,13 +15,13 @@ symlinkCommand = (sourcePath, destinationPath, callback) ->
symlinkCommandWithPrivilegeSync = (sourcePath, destinationPath) ->
runas ?= require 'runas'
if runas('/bin/rm', ['-f', destinationPath], admin: true) != 0
if runas('/bin/rm', ['-f', destinationPath], admin: true) isnt 0
throw new Error("Failed to remove '#{destinationPath}'")
if runas('/bin/mkdir', ['-p', path.dirname(destinationPath)], admin: true) != 0
if runas('/bin/mkdir', ['-p', path.dirname(destinationPath)], admin: true) isnt 0
throw new Error("Failed to create directory '#{destinationPath}'")
if runas('/bin/ln', ['-s', sourcePath, destinationPath], admin: true) != 0
if runas('/bin/ln', ['-s', sourcePath, destinationPath], admin: true) isnt 0
throw new Error("Failed to symlink '#{sourcePath}' to '#{destinationPath}'")
module.exports =
@@ -36,12 +34,11 @@ module.exports =
message: "Failed to install shell commands"
detailedMessage: error.message
resourcePath = atom.getLoadSettings().resourcePath
@installAtomCommand resourcePath, true, (error) =>
@installAtomCommand true, (error) =>
if error?
showErrorDialog(error)
else
@installApmCommand resourcePath, true, (error) ->
@installApmCommand true, (error) ->
if error?
showErrorDialog(error)
else
@@ -49,12 +46,12 @@ module.exports =
message: "Commands installed."
detailedMessage: "The shell commands `atom` and `apm` are installed."
installAtomCommand: (resourcePath, askForPrivilege, callback) ->
commandPath = path.join(resourcePath, 'atom.sh')
installAtomCommand: (askForPrivilege, callback) ->
commandPath = path.join(process.resourcesPath, 'app', 'atom.sh')
@createSymlink commandPath, askForPrivilege, callback
installApmCommand: (resourcePath, askForPrivilege, callback) ->
commandPath = path.join(resourcePath, 'apm', 'node_modules', '.bin', 'apm')
installApmCommand: (askForPrivilege, callback) ->
commandPath = path.join(process.resourcesPath, 'app', 'apm', 'node_modules', '.bin', 'apm')
@createSymlink commandPath, askForPrivilege, callback
createSymlink: (commandPath, askForPrivilege, callback) ->

View File

@@ -1,10 +1,9 @@
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
{specificity} = require 'clear-cut'
{calculateSpecificity, validateSelector} = require 'clear-cut'
_ = require 'underscore-plus'
{$} = require './space-pen-extensions'
SequenceCount = 0
SpecificityCache = {}
# Public: Associates listener functions with commands in a
# context-sensitive way using CSS selectors. You can access a global instance of
@@ -19,6 +18,12 @@ SpecificityCache = {}
# command event listeners globally on `atom.commands` and constrain them to
# specific kinds of elements with CSS selectors.
#
# Command names must follow the `namespace:action` pattern, where `namespace`
# will typically be the name of your package, and `action` describes the
# behavior of your command. If either part consists of multiple words, these
# must be separated by hyphens. E.g. `awesome-package:turn-it-up-to-eleven`.
# All words should be lowercased.
#
# As the event bubbles upward through the DOM, all registered event listeners
# with matching selectors are invoked in order of specificity. In the event of a
# specificity tie, the most recently registered listener is invoked first. This
@@ -49,6 +54,7 @@ class CommandRegistry
destroy: ->
for commandName of @registeredCommands
window.removeEventListener(commandName, @handleCommandEvent, true)
return
# Public: Add one or more command listeners associated with a selector.
#
@@ -86,7 +92,11 @@ class CommandRegistry
disposable.add @add(target, commandName, callback)
return disposable
if typeof callback isnt 'function'
throw new Error("Can't register a command with non-function callback.")
if typeof target is 'string'
validateSelector(target)
@addSelectorBasedListener(target, commandName, callback)
else
@addInlineListener(target, commandName, callback)
@@ -185,6 +195,7 @@ class CommandRegistry
@selectorBasedListenersByCommandName = {}
for commandName, listeners of snapshot
@selectorBasedListenersByCommandName[commandName] = listeners.slice()
return
handleCommandEvent: (originalEvent) =>
propagationStopped = false
@@ -237,7 +248,7 @@ class CommandRegistry
class SelectorBasedListener
constructor: (@selector, @callback) ->
@specificity = (SpecificityCache[@selector] ?= specificity(@selector))
@specificity = calculateSpecificity(@selector)
@sequenceNumber = SequenceCount++
compare: (other) ->

View File

@@ -2,6 +2,7 @@ path = require 'path'
CSON = require 'season'
CoffeeCache = require 'coffee-cash'
babel = require './babel'
typescript = require './typescript'
# This file is required directly by apm so that files can be cached during
# package install so that the first package load in Atom doesn't have to
@@ -16,6 +17,7 @@ exports.addPathToCache = (filePath, atomHome) ->
CoffeeCache.setCacheDirectory(path.join(cacheDir, 'coffee'))
CSON.setCacheDir(path.join(cacheDir, 'cson'))
babel.setCacheDirectory(path.join(cacheDir, 'js', 'babel'))
typescript.setCacheDirectory(path.join(cacheDir, 'ts'))
switch path.extname(filePath)
when '.coffee'
@@ -24,3 +26,5 @@ exports.addPathToCache = (filePath, atomHome) ->
CSON.readFileSync(filePath)
when '.js'
babel.addPathToCache(filePath)
when '.ts'
typescript.addPathToCache(filePath)

View File

@@ -9,7 +9,7 @@ module.exports =
properties:
ignoredNames:
type: 'array'
default: [".git", ".hg", ".svn", ".DS_Store", "Thumbs.db"]
default: [".git", ".hg", ".svn", ".DS_Store", "._*", "Thumbs.db"]
items:
type: 'string'
excludeVcsIgnoredPaths:
@@ -18,7 +18,7 @@ module.exports =
title: 'Exclude VCS Ignored Paths'
followSymlinks:
type: 'boolean'
default: false
default: true
title: 'Follow symlinks'
description: 'Used when searching and when opening files with the fuzzy finder.'
disabledPackages:
@@ -28,7 +28,7 @@ module.exports =
type: 'string'
themes:
type: 'array'
default: ['atom-dark-ui', 'atom-dark-syntax']
default: ['one-dark-ui', 'one-dark-syntax']
items:
type: 'string'
projectHome:
@@ -98,21 +98,17 @@ module.exports =
type: ['string', 'null']
# These can be used as globals or scoped, thus defaults.
completions:
type: "array"
items:
type: "string"
default: []
fontFamily:
type: 'string'
default: ''
fontSize:
type: 'integer'
default: 16
default: 14
minimum: 1
maximum: 100
lineHeight:
type: ['string', 'number']
default: 1.3
default: 1.5
showInvisibles:
type: 'boolean'
default: false
@@ -143,12 +139,18 @@ module.exports =
softWrap:
type: 'boolean'
default: false
description: 'Wraps lines that exceed the width of the window. When `Soft Wrap At Preferred Line Length` is set, it will wrap to the number of characters defined by the `Preferred Line Length` setting.'
softTabs:
type: 'boolean'
default: true
softWrapAtPreferredLineLength:
type: 'boolean'
default: false
description: 'Will wrap to the number of characters defined by the `Preferred Line Length` setting. This will only take effect when soft wrap is enabled globally or for the current language.'
softWrapHangingIndent:
type: 'integer'
default: 0
minimum: 0
scrollSensitivity:
type: 'integer'
default: 40
@@ -177,15 +179,19 @@ module.exports =
eol:
type: ['boolean', 'string']
default: '\u00ac'
maximumLength: 1
space:
type: ['boolean', 'string']
default: '\u00b7'
maximumLength: 1
tab:
type: ['boolean', 'string']
default: '\u00bb'
maximumLength: 1
cr:
type: ['boolean', 'string']
default: '\u00a4'
maximumLength: 1
zoomFontWhenCtrlScrolling:
type: 'boolean'
default: process.platform isnt 'darwin'

View File

@@ -1,6 +1,5 @@
_ = require 'underscore-plus'
fs = require 'fs-plus'
EmitterMixin = require('emissary').Emitter
{CompositeDisposable, Disposable, Emitter} = require 'event-kit'
CSON = require 'season'
path = require 'path'
@@ -78,7 +77,7 @@ ScopeDescriptor = require './scope-descriptor'
# # ...
# ```
#
# See [Creating a Package](https://atom.io/docs/latest/creating-a-package) for
# See [package docs](https://atom.io/docs/latest/hacking-atom-package-word-count) for
# more info.
#
# ## Config Schemas
@@ -290,7 +289,6 @@ ScopeDescriptor = require './scope-descriptor'
#
module.exports =
class Config
EmitterMixin.includeInto(this)
@schemaEnforcers = {}
@addSchemaEnforcer: (typeName, enforcerFunction) ->
@@ -301,6 +299,7 @@ class Config
for typeName, functions of filters
for name, enforcerFunction of functions
@addSchemaEnforcer(typeName, enforcerFunction)
return
@executeSchemaEnforcers: (keyPath, value, schema) ->
error = null
@@ -333,9 +332,16 @@ class Config
@configFilePath = fs.resolve(@configDirPath, 'config', ['json', 'cson'])
@configFilePath ?= path.join(@configDirPath, 'config.cson')
@transactDepth = 0
@savePending = false
@debouncedSave = _.debounce(@save, 100)
@debouncedLoad = _.debounce(@loadUserConfig, 100)
@requestLoad = _.debounce(@loadUserConfig, 100)
@requestSave = =>
@savePending = true
debouncedSave.call(this)
save = =>
@savePending = false
@save()
debouncedSave = _.debounce(save, 100)
###
Section: Config Subscription
@@ -360,7 +366,7 @@ class Config
# * `scopeDescriptor` (optional) {ScopeDescriptor} describing a path from
# the root of the syntax tree to a token. Get one by calling
# {editor.getLastCursor().getScopeDescriptor()}. See {::get} for examples.
# See [the scopes docs](https://atom.io/docs/latest/advanced/scopes-and-scope-descriptors)
# See [the scopes docs](https://atom.io/docs/latest/behind-atom-scoped-settings-scopes-and-scope-descriptors)
# for more information.
# * `callback` {Function} to call when the value of the key changes.
# * `value` the new value of the key
@@ -370,7 +376,7 @@ class Config
observe: ->
if arguments.length is 2
[keyPath, callback] = arguments
else if arguments.length is 3 and (_.isArray(arguments[0]) or arguments[0] instanceof ScopeDescriptor)
else if Grim.includeDeprecatedAPIs and arguments.length is 3 and (_.isArray(arguments[0]) or arguments[0] instanceof ScopeDescriptor)
Grim.deprecate """
Passing a scope descriptor as the first argument to Config::observe is deprecated.
Pass a `scope` in an options hash as the third argument instead.
@@ -379,7 +385,7 @@ class Config
else if arguments.length is 3 and (_.isString(arguments[0]) and _.isObject(arguments[1]))
[keyPath, options, callback] = arguments
scopeDescriptor = options.scope
if options.callNow?
if Grim.includeDeprecatedAPIs and options.callNow?
Grim.deprecate """
Config::observe no longer takes a `callNow` option. Use ::onDidChange instead.
Note that ::onDidChange passes its callback different arguments.
@@ -403,7 +409,7 @@ class Config
# * `scopeDescriptor` (optional) {ScopeDescriptor} describing a path from
# the root of the syntax tree to a token. Get one by calling
# {editor.getLastCursor().getScopeDescriptor()}. See {::get} for examples.
# See [the scopes docs](https://atom.io/docs/latest/advanced/scopes-and-scope-descriptors)
# See [the scopes docs](https://atom.io/docs/latest/behind-atom-scoped-settings-scopes-and-scope-descriptors)
# for more information.
# * `callback` {Function} to call when the value of the key changes.
# * `event` {Object}
@@ -418,7 +424,7 @@ class Config
[callback] = arguments
else if arguments.length is 2
[keyPath, callback] = arguments
else if _.isArray(arguments[0]) or arguments[0] instanceof ScopeDescriptor
else if Grim.includeDeprecatedAPIs and _.isArray(arguments[0]) or arguments[0] instanceof ScopeDescriptor
Grim.deprecate """
Passing a scope descriptor as the first argument to Config::onDidChange is deprecated.
Pass a `scope` in an options hash as the third argument instead.
@@ -487,7 +493,7 @@ class Config
# * `scope` (optional) {ScopeDescriptor} describing a path from
# the root of the syntax tree to a token. Get one by calling
# {editor.getLastCursor().getScopeDescriptor()}
# See [the scopes docs](https://atom.io/docs/latest/advanced/scopes-and-scope-descriptors)
# See [the scopes docs](https://atom.io/docs/latest/behind-atom-scoped-settings-scopes-and-scope-descriptors)
# for more information.
#
# Returns the value from Atom's default settings, the user's configuration
@@ -497,7 +503,7 @@ class Config
if typeof arguments[0] is 'string' or not arguments[0]?
[keyPath, options] = arguments
{scope} = options
else
else if Grim.includeDeprecatedAPIs
Grim.deprecate """
Passing a scope descriptor as the first argument to Config::get is deprecated.
Pass a `scope` in an options hash as the final argument instead.
@@ -568,7 +574,7 @@ class Config
# setting to the default value.
# * `options` (optional) {Object}
# * `scopeSelector` (optional) {String}. eg. '.source.ruby'
# See [the scopes docs](https://atom.io/docs/latest/advanced/scopes-and-scope-descriptors)
# See [the scopes docs](https://atom.io/docs/latest/behind-atom-scoped-settings-scopes-and-scope-descriptors)
# for more information.
# * `source` (optional) {String} The name of a file with which the setting
# is associated. Defaults to the user's config file.
@@ -577,7 +583,7 @@ class Config
# * `true` if the value was set.
# * `false` if the value was not able to be coerced to the type specified in the setting's schema.
set: ->
if arguments[0]?[0] is '.'
if Grim.includeDeprecatedAPIs and arguments[0]?[0] is '.'
Grim.deprecate """
Passing a scope selector as the first argument to Config::set is deprecated.
Pass a `scopeSelector` in an options hash as the final argument instead.
@@ -606,7 +612,7 @@ class Config
else
@setRawValue(keyPath, value)
@debouncedSave() if source is @getUserConfigPath() and shouldSave and not @configFileHasErrors
@requestSave() if source is @getUserConfigPath() and shouldSave and not @configFileHasErrors
true
# Essential: Restore the setting at `keyPath` to its default value.
@@ -616,7 +622,7 @@ class Config
# * `scopeSelector` (optional) {String}. See {::set}
# * `source` (optional) {String}. See {::set}
unset: (keyPath, options) ->
if typeof options is 'string'
if Grim.includeDeprecatedAPIs and typeof options is 'string'
Grim.deprecate """
Passing a scope selector as the first argument to Config::unset is deprecated.
Pass a `scopeSelector` in an options hash as the second argument instead.
@@ -636,7 +642,7 @@ class Config
_.setValueForKeyPath(settings, keyPath, undefined)
settings = withoutEmptyObjects(settings)
@set(null, settings, {scopeSelector, source, priority: @priorityForSource(source)}) if settings?
@debouncedSave()
@requestSave()
else
@scopedSettingsStore.removePropertiesForSourceAndSelector(source, scopeSelector)
@emitChangeEvent()
@@ -651,47 +657,6 @@ class Config
getSources: ->
_.uniq(_.pluck(@scopedSettingsStore.propertySets, 'source')).sort()
# Deprecated: Restore the global setting at `keyPath` to its default value.
#
# Returns the new value.
restoreDefault: (scopeSelector, keyPath) ->
Grim.deprecate("Use ::unset instead.")
@unset(scopeSelector, keyPath)
@get(keyPath)
# Deprecated: Get the global default value of the key path. _Please note_ that in most
# cases calling this is not necessary! {::get} returns the default value when
# a custom value is not specified.
#
# * `scopeSelector` (optional) {String}. eg. '.source.ruby'
# * `keyPath` The {String} name of the key.
#
# Returns the default value.
getDefault: ->
Grim.deprecate("Use `::get(keyPath, {scope, excludeSources: [atom.config.getUserConfigPath()]})` instead")
if arguments.length is 1
[keyPath] = arguments
else
[scopeSelector, keyPath] = arguments
scope = [scopeSelector]
@get(keyPath, {scope, excludeSources: [@getUserConfigPath()]})
# Deprecated: Is the value at `keyPath` its default value?
#
# * `scopeSelector` (optional) {String}. eg. '.source.ruby'
# * `keyPath` The {String} name of the key.
#
# Returns a {Boolean}, `true` if the current value is the default, `false`
# otherwise.
isDefault: ->
Grim.deprecate("Use `not ::get(keyPath, {scope, sources: [atom.config.getUserConfigPath()]})?` instead")
if arguments.length is 1
[keyPath] = arguments
else
[scopeSelector, keyPath] = arguments
scope = [scopeSelector]
not @get(keyPath, {scope, sources: [@getUserConfigPath()]})?
# Extended: Retrieve the schema for a specific key path. The schema will tell
# you what type the keyPath expects, and other metadata about the config
# option.
@@ -708,12 +673,6 @@ class Config
schema = schema.properties?[key]
schema
# Deprecated: Returns a new {Object} containing all of the global settings and
# defaults. Returns the scoped settings when a `scopeSelector` is specified.
getSettings: ->
Grim.deprecate "Use ::get(keyPath) instead"
_.deepExtend({}, @settings, @defaultSettings)
# Extended: Get the {String} path to the config file being used.
getUserConfigPath: ->
@configFilePath
@@ -731,31 +690,6 @@ class Config
@transactDepth--
@emitChangeEvent()
###
Section: Deprecated
###
getInt: (keyPath) ->
Grim.deprecate '''Config::getInt is no longer necessary. Use ::get instead.
Make sure the config option you are accessing has specified an `integer`
schema. See the schema section of
https://atom.io/docs/api/latest/Config for more info.'''
parseInt(@get(keyPath))
getPositiveInt: (keyPath, defaultValue=0) ->
Grim.deprecate '''Config::getPositiveInt is no longer necessary. Use ::get instead.
Make sure the config option you are accessing has specified an `integer`
schema with `minimum: 1`. See the schema section of
https://atom.io/docs/api/latest/Config for more info.'''
Math.max(@getInt(keyPath), 0) or defaultValue
toggle: (keyPath) ->
Grim.deprecate 'Config::toggle is no longer supported. Please remove from your code.'
@set(keyPath, !@get(keyPath))
unobserve: (keyPath) ->
Grim.deprecate 'Config::unobserve no longer does anything. Call `.dispose()` on the object returned by Config::observe instead.'
###
Section: Internal methods used by core
###
@@ -830,9 +764,10 @@ class Config
CSON.writeFileSync(@configFilePath, {})
try
userConfig = CSON.readFileSync(@configFilePath)
@resetUserSettings(userConfig)
@configFileHasErrors = false
unless @savePending
userConfig = CSON.readFileSync(@configFilePath)
@resetUserSettings(userConfig)
@configFileHasErrors = false
catch error
@configFileHasErrors = true
message = "Failed to load `#{path.basename(@configFilePath)}`"
@@ -849,7 +784,7 @@ class Config
observeUserConfig: ->
try
@watchSubscription ?= pathWatcher.watch @configFilePath, (eventType) =>
@debouncedLoad() if eventType is 'change' and @watchSubscription?
@requestLoad() if eventType is 'change' and @watchSubscription?
catch error
@notifyFailure """
Unable to watch path: `#{path.basename(@configFilePath)}`. Make sure you have permissions to
@@ -898,6 +833,7 @@ class Config
@transact =>
@settings = {}
@set(key, value, save: false) for key, value of newSettings
return
getRawValue: (keyPath, options) ->
unless options?.excludeSources?.indexOf(@getUserConfigPath()) >= 0
@@ -958,6 +894,7 @@ class Config
@setRawDefault(keyPath, defaults)
catch e
console.warn("'#{keyPath}' could not set the default. Attempted default: #{JSON.stringify(defaults)}; Schema: #{JSON.stringify(@getSchema(keyPath))}")
return
deepClone: (object) ->
if object instanceof Color
@@ -1053,16 +990,6 @@ class Config
@emitChangeEvent()
addScopedSettings: (source, selector, value, options) ->
Grim.deprecate("Use ::set instead")
settingsBySelector = {}
settingsBySelector[selector] = value
disposable = @scopedSettingsStore.addProperties(source, settingsBySelector, options)
@emitChangeEvent()
new Disposable =>
disposable.dispose()
@emitChangeEvent()
setRawScopedValue: (keyPath, value, source, selector, options) ->
if keyPath?
newValue = {}
@@ -1091,11 +1018,6 @@ class Config
oldValue = newValue
callback(event)
settingsForScopeDescriptor: (scopeDescriptor, keyPath) ->
Grim.deprecate("Use Config::getAll instead")
entries = @getAll(null, scope: scopeDescriptor)
value for {value} in entries when _.valueForKeyPath(value, keyPath)?
# Base schema enforcers. These will coerce raw input into the specified type,
# and will throw an error when the value cannot be coerced. Throwing the error
# will indicate that the value should not be set.
@@ -1138,6 +1060,12 @@ Config.addSchemaEnforcers
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be a string")
value
validateMaximumLength: (keyPath, value, schema) ->
if typeof schema.maximumLength is 'number' and value.length > schema.maximumLength
value.slice(0, schema.maximumLength)
else
value
'null':
# null sort of isnt supported. It will just unset in this case
coerce: (keyPath, value, schema) ->
@@ -1212,7 +1140,7 @@ splitKeyPath = (keyPath) ->
startIndex = 0
keyPathArray = []
for char, i in keyPath
if char is '.' and (i is 0 or keyPath[i-1] != '\\')
if char is '.' and (i is 0 or keyPath[i-1] isnt '\\')
keyPathArray.push keyPath.substring(startIndex, i)
startIndex = i + 1
keyPathArray.push keyPath.substr(startIndex, keyPath.length)
@@ -1229,3 +1157,71 @@ withoutEmptyObjects = (object) ->
else
resultObject = object
resultObject
# TODO remove in 1.0 API
Config::unobserve = (keyPath) ->
Grim.deprecate 'Config::unobserve no longer does anything. Call `.dispose()` on the object returned by Config::observe instead.'
if Grim.includeDeprecatedAPIs
EmitterMixin = require('emissary').Emitter
EmitterMixin.includeInto(Config)
Config::restoreDefault = (scopeSelector, keyPath) ->
Grim.deprecate("Use ::unset instead.")
@unset(scopeSelector, keyPath)
@get(keyPath)
Config::getDefault = ->
Grim.deprecate("Use `::get(keyPath, {scope, excludeSources: [atom.config.getUserConfigPath()]})` instead")
if arguments.length is 1
[keyPath] = arguments
else
[scopeSelector, keyPath] = arguments
scope = [scopeSelector]
@get(keyPath, {scope, excludeSources: [@getUserConfigPath()]})
Config::isDefault = ->
Grim.deprecate("Use `not ::get(keyPath, {scope, sources: [atom.config.getUserConfigPath()]})?` instead")
if arguments.length is 1
[keyPath] = arguments
else
[scopeSelector, keyPath] = arguments
scope = [scopeSelector]
not @get(keyPath, {scope, sources: [@getUserConfigPath()]})?
Config::getSettings = ->
Grim.deprecate "Use ::get(keyPath) instead"
_.deepExtend({}, @settings, @defaultSettings)
Config::getInt = (keyPath) ->
Grim.deprecate '''Config::getInt is no longer necessary. Use ::get instead.
Make sure the config option you are accessing has specified an `integer`
schema. See the schema section of
https://atom.io/docs/api/latest/Config for more info.'''
parseInt(@get(keyPath))
Config::getPositiveInt = (keyPath, defaultValue=0) ->
Grim.deprecate '''Config::getPositiveInt is no longer necessary. Use ::get instead.
Make sure the config option you are accessing has specified an `integer`
schema with `minimum: 1`. See the schema section of
https://atom.io/docs/api/latest/Config for more info.'''
Math.max(@getInt(keyPath), 0) or defaultValue
Config::toggle = (keyPath) ->
Grim.deprecate 'Config::toggle is no longer supported. Please remove from your code.'
@set(keyPath, not @get(keyPath))
Config::addScopedSettings = (source, selector, value, options) ->
Grim.deprecate("Use ::set instead")
settingsBySelector = {}
settingsBySelector[selector] = value
disposable = @scopedSettingsStore.addProperties(source, settingsBySelector, options)
@emitChangeEvent()
new Disposable =>
disposable.dispose()
@emitChangeEvent()
Config::settingsForScopeDescriptor = (scopeDescriptor, keyPath) ->
Grim.deprecate("Use Config::getAll instead")
entries = @getAll(null, scope: scopeDescriptor)
value for {value} in entries when _.valueForKeyPath(value, keyPath)?

View File

@@ -1,15 +1,13 @@
{$} = require './space-pen-extensions'
_ = require 'underscore-plus'
remote = require 'remote'
path = require 'path'
CSON = require 'season'
fs = require 'fs-plus'
{specificity} = require 'clear-cut'
{calculateSpecificity, validateSelector} = require 'clear-cut'
{Disposable} = require 'event-kit'
Grim = require 'grim'
MenuHelpers = require './menu-helpers'
SpecificityCache = {}
platformContextMenu = require('../package.json')?._atomMenu?['context-menu']
# Extended: Provides a registry for commands that you'd like to appear in the
# context menu.
@@ -50,10 +48,13 @@ class ContextMenuManager
atom.keymaps.onDidLoadBundledKeymaps => @loadPlatformItems()
loadPlatformItems: ->
menusDirPath = path.join(@resourcePath, 'menus')
platformMenuPath = fs.resolve(menusDirPath, process.platform, ['cson', 'json'])
map = CSON.readFileSync(platformMenuPath)
atom.contextMenu.add(map['context-menu'])
if platformContextMenu?
@add(platformContextMenu)
else
menusDirPath = path.join(@resourcePath, 'menus')
platformMenuPath = fs.resolve(menusDirPath, process.platform, ['cson', 'json'])
map = CSON.readFileSync(platformMenuPath)
@add(map['context-menu'])
# Public: Add context menu items scoped by CSS selectors.
#
@@ -99,30 +100,35 @@ class ContextMenuManager
# whether to display this item on a given context menu deployment. Called
# with the following argument:
# * `event` The click event that deployed the context menu.
#
# Returns a {Disposable} on which `.dispose()` can be called to remove the
# added menu items.
add: (itemsBySelector) ->
# Detect deprecated file path as first argument
if itemsBySelector? and typeof itemsBySelector isnt 'object'
Grim.deprecate """
ContextMenuManager::add has changed to take a single object as its
argument. Please see
https://atom.io/docs/api/latest/ContextMenuManager for more info.
"""
itemsBySelector = arguments[1]
devMode = arguments[2]?.devMode
# Detect deprecated format for items object
for key, value of itemsBySelector
unless _.isArray(value)
if Grim.includeDeprecatedAPIs
# Detect deprecated file path as first argument
if itemsBySelector? and typeof itemsBySelector isnt 'object'
Grim.deprecate """
ContextMenuManager::add has changed to take a single object as its
`ContextMenuManager::add` has changed to take a single object as its
argument. Please see
https://atom.io/docs/api/latest/ContextMenuManager for more info.
https://atom.io/docs/api/latest/ContextMenuManager#context-menu-cson-format for more info.
"""
itemsBySelector = @convertLegacyItemsBySelector(itemsBySelector, devMode)
itemsBySelector = arguments[1]
devMode = arguments[2]?.devMode
# Detect deprecated format for items object
for key, value of itemsBySelector
unless _.isArray(value)
Grim.deprecate """
`ContextMenuManager::add` has changed to take a single object as its
argument. Please see
https://atom.io/docs/api/latest/ContextMenuManager#context-menu-cson-format for more info.
"""
itemsBySelector = @convertLegacyItemsBySelector(itemsBySelector, devMode)
addedItemSets = []
for selector, items of itemsBySelector
validateSelector(selector)
itemSet = new ContextMenuItemSet(selector, items)
addedItemSets.push(itemSet)
@itemSets.push(itemSet)
@@ -130,6 +136,7 @@ class ContextMenuManager
new Disposable =>
for itemSet in addedItemSets
@itemSets.splice(@itemSets.indexOf(itemSet), 1)
return
templateForElement: (target) ->
@templateForEvent({target})
@@ -185,7 +192,7 @@ class ContextMenuManager
menuTemplate = @templateForEvent(event)
return unless menuTemplate?.length > 0
remote.getCurrentWindow().emit('context-menu', menuTemplate)
atom.getCurrentWindow().emit('context-menu', menuTemplate)
return
clear: ->
@@ -202,4 +209,4 @@ class ContextMenuManager
class ContextMenuItemSet
constructor: (@selector, @items) ->
@specificity = (SpecificityCache[@selector] ?= specificity(@selector))
@specificity = calculateSpecificity(@selector)

View File

@@ -1,8 +1,8 @@
{Point, Range} = require 'text-buffer'
{Model} = require 'theorist'
{Emitter} = require 'event-kit'
_ = require 'underscore-plus'
Grim = require 'grim'
Model = require './model'
# Extended: The `Cursor` class represents the little blinking line identifying
# where text can be inserted.
@@ -15,7 +15,6 @@ class Cursor extends Model
bufferPosition: null
goalColumn: null
visible: true
needsAutoscroll: null
# Instantiated by a {TextEditor}
constructor: ({@editor, @marker, id}) ->
@@ -30,10 +29,6 @@ class Cursor extends Model
{textChanged} = e
return if oldHeadScreenPosition.isEqual(newHeadScreenPosition)
# Supports old editor view
@needsAutoscroll ?= @isLastCursor() and !textChanged
@autoscroll() if @editor.manageScrollPosition and @isLastCursor() and textChanged
@goalColumn = null
movedEvent =
@@ -44,16 +39,15 @@ class Cursor extends Model
textChanged: textChanged
cursor: this
@emit 'moved', movedEvent
@emit 'moved', movedEvent if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change-position', movedEvent
@editor.cursorMoved(movedEvent)
@marker.onDidDestroy =>
@destroyed = true
@editor.removeCursor(this)
@emit 'destroyed'
@emit 'destroyed' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-destroy'
@emitter.dispose()
@needsAutoscroll = true
destroy: ->
@marker.destroy()
@@ -95,13 +89,13 @@ class Cursor extends Model
@emitter.on 'did-change-visibility', callback
on: (eventName) ->
return unless Grim.includeDeprecatedAPIs
switch eventName
when 'moved'
Grim.deprecate("Use Cursor::onDidChangePosition instead")
when 'destroyed'
Grim.deprecate("Use Cursor::onDidDestroy instead")
when 'destroyed'
Grim.deprecate("Use Cursor::onDidDestroy instead")
else
Grim.deprecate("::on is no longer supported. Use the event subscription methods instead")
super
@@ -128,8 +122,9 @@ class Cursor extends Model
#
# * `bufferPosition` {Array} of two numbers: the buffer row, and the buffer column.
# * `options` (optional) {Object} with the following keys:
# * `autoscroll` A Boolean which, if `true`, scrolls the {TextEditor} to wherever
# the cursor moves to.
# * `autoscroll` {Boolean} indicating whether to autoscroll to the new
# position. Defaults to `true` if this is the most recently added cursor,
# `false` otherwise.
setBufferPosition: (bufferPosition, options={}) ->
@changePosition options, =>
@marker.setHeadBufferPosition(bufferPosition, options)
@@ -161,7 +156,7 @@ class Cursor extends Model
# Public: Returns whether the cursor is at the start of a line.
isAtBeginningOfLine: ->
@getBufferPosition().column == 0
@getBufferPosition().column is 0
# Public: Returns whether the cursor is on the line return character.
isAtEndOfLine: ->
@@ -215,7 +210,7 @@ class Cursor extends Model
isInsideWord: (options) ->
{row, column} = @getBufferPosition()
range = [[row, column], [row, Infinity]]
@editor.getTextInBufferRange(range).search(options?.wordRegex ? @wordRegExp()) == 0
@editor.getTextInBufferRange(range).search(options?.wordRegex ? @wordRegExp()) is 0
# Public: Returns the indentation level of the current line.
getIndentLevel: ->
@@ -229,9 +224,6 @@ class Cursor extends Model
# Returns a {ScopeDescriptor}
getScopeDescriptor: ->
@editor.scopeDescriptorForBufferPosition(@getBufferPosition())
getScopes: ->
Grim.deprecate 'Use Cursor::getScopeDescriptor() instead'
@getScopeDescriptor().getScopesArray()
# Public: Returns true if this cursor has no non-whitespace characters before
# its current position.
@@ -251,7 +243,7 @@ class Cursor extends Model
#
# Returns a {Boolean}.
isLastCursor: ->
this == @editor.getLastCursor()
this is @editor.getLastCursor()
###
Section: Moving the Cursor
@@ -266,9 +258,9 @@ class Cursor extends Model
moveUp: (rowCount=1, {moveToEndOfSelection}={}) ->
range = @marker.getScreenRange()
if moveToEndOfSelection and not range.isEmpty()
{ row, column } = range.start
{row, column} = range.start
else
{ row, column } = @getScreenPosition()
{row, column} = @getScreenPosition()
column = @goalColumn if @goalColumn?
@setScreenPosition({row: row - rowCount, column: column}, skipSoftWrapIndentation: true)
@@ -283,9 +275,9 @@ class Cursor extends Model
moveDown: (rowCount=1, {moveToEndOfSelection}={}) ->
range = @marker.getScreenRange()
if moveToEndOfSelection and not range.isEmpty()
{ row, column } = range.end
{row, column} = range.end
else
{ row, column } = @getScreenPosition()
{row, column} = @getScreenPosition()
column = @goalColumn if @goalColumn?
@setScreenPosition({row: row + rowCount, column: column}, skipSoftWrapIndentation: true)
@@ -310,7 +302,7 @@ class Cursor extends Model
columnCount-- # subtract 1 for the row move
column = column - columnCount
@setScreenPosition({row, column})
@setScreenPosition({row, column}, clip: 'backward')
# Public: Moves the cursor right one screen column.
#
@@ -323,7 +315,7 @@ class Cursor extends Model
if moveToEndOfSelection and not range.isEmpty()
@setScreenPosition(range.end)
else
{ row, column } = @getScreenPosition()
{row, column} = @getScreenPosition()
maxLines = @editor.getScreenLineCount()
rowLength = @editor.lineTextForScreenRow(row).length
columnsRemainingInLine = rowLength - column
@@ -337,11 +329,11 @@ class Cursor extends Model
columnsRemainingInLine = rowLength
column = column + columnCount
@setScreenPosition({row, column}, skipAtomicTokens: true, wrapBeyondNewlines: true, wrapAtSoftNewlines: true)
@setScreenPosition({row, column}, clip: 'forward', wrapBeyondNewlines: true, wrapAtSoftNewlines: true)
# Public: Moves the cursor to the top of the buffer.
moveToTop: ->
@setBufferPosition([0,0])
@setBufferPosition([0, 0])
# Public: Moves the cursor to the bottom of the buffer.
moveToBottom: ->
@@ -498,10 +490,6 @@ class Cursor extends Model
endOfWordPosition or currentBufferPosition
getMoveNextWordBoundaryBufferPosition: (options) ->
Grim.deprecate 'Use `::getNextWordBoundaryBufferPosition(options)` instead'
@getNextWordBoundaryBufferPosition(options)
# Public: Retrieves the buffer position of where the current word starts.
#
# * `options` (optional) An {Object} with the following keys:
@@ -613,10 +601,9 @@ class Cursor extends Model
# Public: Sets whether the cursor is visible.
setVisible: (visible) ->
if @visible != visible
if @visible isnt visible
@visible = visible
@needsAutoscroll ?= true if @visible and @isLastCursor()
@emit 'visibility-changed', @visible
@emit 'visibility-changed', @visible if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change-visibility', @visible
# Public: Returns the visibility of the cursor.
@@ -643,11 +630,10 @@ class Cursor extends Model
# Public: Prevents this cursor from causing scrolling.
clearAutoscroll: ->
@needsAutoscroll = null
# Public: Deselects the current selection.
clearSelection: ->
@selection?.clear()
clearSelection: (options) ->
@selection?.clear(options)
# Public: Get the RegExp used by the cursor to determine what a "word" is.
#
@@ -689,12 +675,9 @@ class Cursor extends Model
###
changePosition: (options, fn) ->
@clearSelection()
@needsAutoscroll = options.autoscroll ? @isLastCursor()
@clearSelection(autoscroll: false)
fn()
if @needsAutoscroll
@emit 'autoscrolled' # Support legacy editor
@autoscroll() if @needsAutoscroll and @editor.manageScrollPosition # Support react editor view
@autoscroll() if options.autoscroll ? @isLastCursor()
getPixelRect: ->
@editor.pixelRectForScreenRange(@getScreenRange())
@@ -715,20 +698,29 @@ class Cursor extends Model
position = new Point(row, column - 1)
@editor.scanInBufferRange /^\n*$/g, scanRange, ({range, stop}) ->
if !range.start.isEqual(start)
unless range.start.isEqual(start)
position = range.start
stop()
@editor.screenPositionForBufferPosition(position)
position
getBeginningOfPreviousParagraphBufferPosition: ->
start = @getBufferPosition()
{row, column} = start
scanRange = [[row-1, column], [0,0]]
scanRange = [[row-1, column], [0, 0]]
position = new Point(0, 0)
zero = new Point(0,0)
zero = new Point(0, 0)
@editor.backwardsScanInBufferRange /^\n*$/g, scanRange, ({range, stop}) ->
if !range.start.isEqual(zero)
unless range.start.isEqual(zero)
position = range.start
stop()
@editor.screenPositionForBufferPosition(position)
position
if Grim.includeDeprecatedAPIs
Cursor::getScopes = ->
Grim.deprecate 'Use Cursor::getScopeDescriptor() instead'
@getScopeDescriptor().getScopesArray()
Cursor::getMoveNextWordBoundaryBufferPosition = (options) ->
Grim.deprecate 'Use `::getNextWordBoundaryBufferPosition(options)` instead'
@getNextWordBoundaryBufferPosition(options)

View File

@@ -7,6 +7,9 @@ class CursorsComponent
@domNode = document.createElement('div')
@domNode.classList.add('cursors')
getDomNode: ->
@domNode
updateSync: (state) ->
newState = state.content
@oldState ?= {cursors: {}}
@@ -35,6 +38,8 @@ class CursorsComponent
@domNode.appendChild(cursorNode)
@updateCursorNode(id, cursorState)
return
updateCursorNode: (id, newCursorState) ->
cursorNode = @cursorNodesById[id]
oldCursorState = (@oldState.cursors[id] ?= {})

View File

@@ -7,9 +7,11 @@ CustomEventMixin =
for name, listeners in @customEventListeners
for listener in listeners
@getDOMNode().removeEventListener(name, listener)
return
addCustomEventListeners: (customEventListeners) ->
for name, listener of customEventListeners
@customEventListeners[name] ?= []
@customEventListeners[name].push(listener)
@getDOMNode().addEventListener(name, listener)
return

View File

@@ -0,0 +1,110 @@
{setDimensionsAndBackground} = require './gutter-component-helpers'
# This class represents a gutter other than the 'line-numbers' gutter.
# The contents of this gutter may be specified by Decorations.
module.exports =
class CustomGutterComponent
constructor: ({@gutter}) ->
@decorationNodesById = {}
@decorationItemsById = {}
@visible = true
@domNode = atom.views.getView(@gutter)
@decorationsNode = @domNode.firstChild
# Clear the contents in case the domNode is being reused.
@decorationsNode.innerHTML = ''
getDomNode: ->
@domNode
hideNode: ->
if @visible
@domNode.style.display = 'none'
@visible = false
showNode: ->
if not @visible
@domNode.style.removeProperty('display')
@visible = true
# `state` is a subset of the TextEditorPresenter state that is specific
# to this line number gutter.
updateSync: (state) ->
@oldDimensionsAndBackgroundState ?= {}
setDimensionsAndBackground(@oldDimensionsAndBackgroundState, state.styles, @decorationsNode)
@oldDecorationPositionState ?= {}
decorationState = state.content
updatedDecorationIds = new Set
for decorationId, decorationInfo of decorationState
updatedDecorationIds.add(decorationId)
existingDecoration = @decorationNodesById[decorationId]
if existingDecoration
@updateDecorationNode(existingDecoration, decorationId, decorationInfo)
else
newNode = @buildDecorationNode(decorationId, decorationInfo)
@decorationNodesById[decorationId] = newNode
@decorationsNode.appendChild(newNode)
for decorationId, decorationNode of @decorationNodesById
if not updatedDecorationIds.has(decorationId)
decorationNode.remove()
delete @decorationNodesById[decorationId]
delete @decorationItemsById[decorationId]
delete @oldDecorationPositionState[decorationId]
###
Section: Private Methods
###
# Builds and returns an HTMLElement to represent the specified decoration.
buildDecorationNode: (decorationId, decorationInfo) ->
@oldDecorationPositionState[decorationId] = {}
newNode = document.createElement('div')
newNode.style.position = 'absolute'
@updateDecorationNode(newNode, decorationId, decorationInfo)
newNode
# Updates the existing HTMLNode with the new decoration info. Attempts to
# minimize changes to the DOM.
updateDecorationNode: (node, decorationId, newDecorationInfo) ->
oldPositionState = @oldDecorationPositionState[decorationId]
if oldPositionState.top isnt newDecorationInfo.top + 'px'
node.style.top = newDecorationInfo.top + 'px'
oldPositionState.top = newDecorationInfo.top + 'px'
if oldPositionState.height isnt newDecorationInfo.height + 'px'
node.style.height = newDecorationInfo.height + 'px'
oldPositionState.height = newDecorationInfo.height + 'px'
if newDecorationInfo.class and not node.classList.contains(newDecorationInfo.class)
node.className = 'decoration'
node.classList.add(newDecorationInfo.class)
else if not newDecorationInfo.class
node.className = 'decoration'
@setDecorationItem(newDecorationInfo.item, newDecorationInfo.height, decorationId, node)
# Sets the decorationItem on the decorationNode.
# If `decorationItem` is undefined, the decorationNode's child item will be cleared.
setDecorationItem: (newItem, decorationHeight, decorationId, decorationNode) ->
if newItem isnt @decorationItemsById[decorationId]
while decorationNode.firstChild
decorationNode.removeChild(decorationNode.firstChild)
delete @decorationItemsById[decorationId]
if newItem
# `item` should be either an HTMLElement or a space-pen View.
newItemNode = null
if newItem instanceof HTMLElement
newItemNode = newItem
else
newItemNode = newItem.element
newItemNode.style.height = decorationHeight + 'px'
decorationNode.appendChild(newItemNode)
@decorationItemsById[decorationId] = newItem

View File

@@ -1,11 +1,17 @@
_ = require 'underscore-plus'
EmitterMixin = require('emissary').Emitter
{Emitter} = require 'event-kit'
Grim = require 'grim'
idCounter = 0
nextId = -> idCounter++
# Applies changes to a decorationsParam {Object} to make it possible to
# differentiate decorations on custom gutters versus the line-number gutter.
translateDecorationParamsOldToNew = (decorationParams) ->
if decorationParams.type is 'line-number'
decorationParams.gutterName = 'line-number'
decorationParams
# Essential: Represents a decoration that follows a {Marker}. A decoration is
# basically a visual representation of a marker. It allows you to add CSS
# classes to line numbers in the gutter, lines, and add selection-line regions
@@ -20,7 +26,7 @@ nextId = -> idCounter++
# decoration = editor.decorateMarker(marker, {type: 'line', class: 'my-line-class'})
# ```
#
# Best practice for destorying the decoration is by destroying the {Marker}.
# Best practice for destroying the decoration is by destroying the {Marker}.
#
# ```coffee
# marker.destroy()
@@ -30,7 +36,6 @@ nextId = -> idCounter++
# the marker.
module.exports =
class Decoration
EmitterMixin.includeInto(this)
# Private: Check if the `decorationProperties.type` matches `type`
#
@@ -40,23 +45,32 @@ class Decoration
# type matches any in the array.
#
# Returns {Boolean}
# Note: 'line-number' is a special subtype of the 'gutter' type. I.e., a
# 'line-number' is a 'gutter', but a 'gutter' is not a 'line-number'.
@isType: (decorationProperties, type) ->
# 'line-number' is a special case of 'gutter'.
if _.isArray(decorationProperties.type)
type in decorationProperties.type
return true if type in decorationProperties.type
if type is 'gutter'
return true if 'line-number' in decorationProperties.type
return false
else
type is decorationProperties.type
if type is 'gutter'
return true if decorationProperties.type in ['gutter', 'line-number']
else
type is decorationProperties.type
###
Section: Construction and Destruction
###
constructor: (@marker, @displayBuffer, @properties) ->
constructor: (@marker, @displayBuffer, properties) ->
@emitter = new Emitter
@id = nextId()
@setProperties properties
@properties.id = @id
@flashQueue = null
@destroyed = false
@markerDestroyDisposable = @marker.onDidDestroy => @destroy()
# Essential: Destroy this marker.
@@ -68,7 +82,7 @@ class Decoration
@markerDestroyDisposable.dispose()
@markerDestroyDisposable = null
@destroyed = true
@emit 'destroyed'
@emit 'destroyed' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-destroy'
@emitter.dispose()
@@ -124,9 +138,6 @@ class Decoration
# Essential: Returns the {Decoration}'s properties.
getProperties: ->
@properties
getParams: ->
Grim.deprecate 'Use Decoration::getProperties instead'
@getProperties()
# Essential: Update the marker with new Properties. Allows you to change the decoration's class.
#
@@ -140,13 +151,12 @@ class Decoration
setProperties: (newProperties) ->
return if @destroyed
oldProperties = @properties
@properties = newProperties
@properties = translateDecorationParamsOldToNew(newProperties)
@properties.id = @id
@emit 'updated', {oldParams: oldProperties, newParams: newProperties}
if newProperties.type?
@displayBuffer.decorationDidChangeType(this)
@emit 'updated', {oldParams: oldProperties, newParams: newProperties} if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change-properties', {oldProperties, newProperties}
update: (newProperties) ->
Grim.deprecate 'Use Decoration::setProperties instead'
@setProperties(newProperties)
###
Section: Private methods
@@ -155,7 +165,7 @@ class Decoration
matchesPattern: (decorationPattern) ->
return false unless decorationPattern?
for key, value of decorationPattern
return false if @properties[key] != value
return false if @properties[key] isnt value
true
onDidFlash: (callback) ->
@@ -165,14 +175,18 @@ class Decoration
flashObject = {class: klass, duration}
@flashQueue ?= []
@flashQueue.push(flashObject)
@emit 'flash'
@emit 'flash' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-flash'
consumeNextFlash: ->
return @flashQueue.shift() if @flashQueue?.length > 0
null
on: (eventName) ->
if Grim.includeDeprecatedAPIs
EmitterMixin = require('emissary').Emitter
EmitterMixin.includeInto(Decoration)
Decoration::on = (eventName) ->
switch eventName
when 'updated'
Grim.deprecate 'Use Decoration::onDidChangeProperties instead'
@@ -184,3 +198,11 @@ class Decoration
Grim.deprecate 'Decoration::on is deprecated. Use event subscription methods instead.'
EmitterMixin::on.apply(this, arguments)
Decoration::getParams = ->
Grim.deprecate 'Use Decoration::getProperties instead'
@getProperties()
Decoration::update = (newProperties) ->
Grim.deprecate 'Use Decoration::setProperties instead'
@setProperties(newProperties)

View File

@@ -1,6 +1,7 @@
{Directory} = require 'pathwatcher'
fs = require 'fs-plus'
path = require 'path'
url = require 'url'
module.exports =
class DefaultDirectoryProvider
@@ -14,14 +15,22 @@ class DefaultDirectoryProvider
# * {Directory} if the given URI is compatible with this provider.
# * `null` if the given URI is not compatibile with this provider.
directoryForURISync: (uri) ->
projectPath = path.normalize(uri)
directoryPath = if fs.isDirectorySync(projectPath)
projectPath
normalizedPath = path.normalize(uri)
{protocol} = url.parse(uri)
directoryPath = if protocol?
uri
else if not fs.isDirectorySync(normalizedPath) and fs.isDirectorySync(path.dirname(normalizedPath))
path.dirname(normalizedPath)
else
path.dirname(projectPath)
normalizedPath
new Directory(directoryPath)
# TODO: Stop normalizing the path in pathwatcher's Directory.
directory = new Directory(directoryPath)
if protocol?
directory.path = directoryPath
if fs.isCaseInsensitive()
directory.lowerCasePath = directoryPath.toLowerCase()
directory
# Public: Create a Directory that corresponds to the specified URI.
#

View File

@@ -0,0 +1,95 @@
Task = require './task'
# Public: Searches local files for lines matching a specified regex.
#
# Implements thenable so it can be used with `Promise.all()`.
class DirectorySearch
constructor: (rootPaths, regex, options) ->
scanHandlerOptions =
ignoreCase: regex.ignoreCase
inclusions: options.inclusions
includeHidden: options.includeHidden
excludeVcsIgnores: options.excludeVcsIgnores
exclusions: options.exclusions
follow: options.follow
@task = new Task(require.resolve('./scan-handler'))
@task.on 'scan:result-found', options.didMatch
@task.on 'scan:file-error', options.didError
@task.on 'scan:paths-searched', options.didSearchPaths
@promise = new Promise (resolve, reject) =>
@task.on('task:cancelled', reject)
@task.start(rootPaths, regex.source, scanHandlerOptions, resolve)
# Public: Implementation of `then()` to satisfy the *thenable* contract.
# This makes it possible to use a `DirectorySearch` with `Promise.all()`.
#
# Returns `Promise`.
then: (args...) ->
@promise.then.apply(@promise, args)
# Public: Cancels the search.
cancel: ->
# This will cause @promise to reject.
@task.cancel()
null
# Default provider for the `atom.directory-searcher` service.
module.exports =
class DefaultDirectorySearcher
# Public: Determines whether this object supports search for a `Directory`.
#
# * `directory` {Directory} whose search needs might be supported by this object.
#
# Returns a `boolean` indicating whether this object can search this `Directory`.
canSearchDirectory: (directory) -> true
# Public: Performs a text search for files in the specified `Directory`, subject to the
# specified parameters.
#
# Results are streamed back to the caller by invoking methods on the specified `options`,
# such as `didMatch` and `didError`.
#
# * `directories` {Array} of {Directory} objects to search, all of which have been accepted by
# this searcher's `canSearchDirectory()` predicate.
# * `regex` {RegExp} to search with.
# * `options` {Object} with the following properties:
# * `didMatch` {Function} call with a search result structured as follows:
# * `searchResult` {Object} with the following keys:
# * `filePath` {String} absolute path to the matching file.
# * `matches` {Array} with object elements with the following keys:
# * `lineText` {String} The full text of the matching line (without a line terminator character).
# * `lineTextOffset` {Number} (This always seems to be 0?)
# * `matchText` {String} The text that matched the `regex` used for the search.
# * `range` {Range} Identifies the matching region in the file. (Likely as an array of numeric arrays.)
# * `didError` {Function} call with an Error if there is a problem during the search.
# * `didSearchPaths` {Function} periodically call with the number of paths searched thus far.
# * `inclusions` {Array} of glob patterns (as strings) to search within. Note that this
# array may be empty, indicating that all files should be searched.
#
# Each item in the array is a file/directory pattern, e.g., `src` to search in the "src"
# directory or `*.js` to search all JavaScript files. In practice, this often comes from the
# comma-delimited list of patterns in the bottom text input of the ProjectFindView dialog.
# * `ignoreHidden` {boolean} whether to ignore hidden files.
# * `excludeVcsIgnores` {boolean} whether to exclude VCS ignored paths.
# * `exclusions` {Array} similar to inclusions
# * `follow` {boolean} whether symlinks should be followed.
#
# Returns a *thenable* `DirectorySearch` that includes a `cancel()` method. If `cancel()` is
# invoked before the `DirectorySearch` is determined, it will resolve the `DirectorySearch`.
search: (directories, regex, options) ->
rootPaths = directories.map (directory) -> directory.getPath()
isCancelled = false
directorySearch = new DirectorySearch(rootPaths, regex, options)
promise = new Promise (resolve, reject) ->
directorySearch.then resolve, ->
if isCancelled
resolve()
else
reject()
return {
then: promise.then.bind(promise)
cancel: ->
isCancelled = true
directorySearch.cancel()
}

View File

@@ -0,0 +1,43 @@
semver = require 'semver'
deprecatedPackages = require('../package.json')?._deprecatedPackages ? {}
ranges = {}
exports.getDeprecatedPackageMetadata = (name) ->
metadata = null
if deprecatedPackages.hasOwnProperty(name)
metadata = deprecatedPackages[name]
Object.freeze(metadata) if metadata
metadata
exports.isDeprecatedPackage = (name, version) ->
return false unless deprecatedPackages.hasOwnProperty(name)
deprecatedVersionRange = deprecatedPackages[name].version
return true unless deprecatedVersionRange
semver.valid(version) and satisfies(version, deprecatedVersionRange)
satisfies = (version, rawRange) ->
unless parsedRange = ranges[rawRange]
parsedRange = new Range(rawRange)
ranges[rawRange] = parsedRange
parsedRange.test(version)
# Extend semver.Range to memoize matched versions for speed
class Range extends semver.Range
constructor: ->
super
@matchedVersions = new Set()
@unmatchedVersions = new Set()
test: (version) ->
return true if @matchedVersions.has(version)
return false if @unmatchedVersions.has(version)
matches = super
if matches
@matchedVersions.add(version)
else
@unmatchedVersions.add(version)
matches

View File

@@ -35,10 +35,7 @@ class DeserializerManager
@deserializers[deserializer.name] = deserializer for deserializer in deserializers
new Disposable =>
delete @deserializers[deserializer.name] for deserializer in deserializers
remove: (classes...) ->
Grim.deprecate("Call .dispose() on the Disposable return from ::add instead")
delete @deserializers[name] for {name} in classes
return
# Public: Deserialize the state and params.
#
@@ -63,3 +60,9 @@ class DeserializerManager
name = state.get?('deserializer') ? state.deserializer
@deserializers[name]
if Grim.includeDeprecatedAPIs
DeserializerManager::remove = (classes...) ->
Grim.deprecate("Call .dispose() on the Disposable return from ::add instead")
delete @deserializers[name] for {name} in classes
return

View File

@@ -1,16 +1,15 @@
_ = require 'underscore-plus'
EmitterMixin = require('emissary').Emitter
Serializable = require 'serializable'
{Model} = require 'theorist'
{CompositeDisposable, Emitter} = require 'event-kit'
{Point, Range} = require 'text-buffer'
Grim = require 'grim'
TokenizedBuffer = require './tokenized-buffer'
RowMap = require './row-map'
Fold = require './fold'
Model = require './model'
Token = require './token'
Decoration = require './decoration'
Marker = require './marker'
Grim = require 'grim'
class BufferToScreenConversionError extends Error
constructor: (@message, @metadata) ->
@@ -21,42 +20,32 @@ module.exports =
class DisplayBuffer extends Model
Serializable.includeInto(this)
@properties
manageScrollPosition: false
softWrapped: null
editorWidthInChars: null
lineHeightInPixels: null
defaultCharWidth: null
height: null
width: null
scrollTop: 0
scrollLeft: 0
scrollWidth: 0
verticalScrollbarWidth: 15
horizontalScrollbarHeight: 15
verticalScrollMargin: 2
horizontalScrollMargin: 6
scopedCharacterWidthsChangeCount: 0
constructor: ({tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, @invisibles}={}) ->
constructor: ({tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, ignoreInvisibles, @largeFileMode}={}) ->
super
@emitter = new Emitter
@disposables = new CompositeDisposable
@tokenizedBuffer ?= new TokenizedBuffer({tabLength, buffer, @invisibles})
@tokenizedBuffer ?= new TokenizedBuffer({tabLength, buffer, ignoreInvisibles, @largeFileMode})
@buffer = @tokenizedBuffer.buffer
@charWidthsByScope = {}
@markers = {}
@foldsByMarkerId = {}
@decorationsById = {}
@decorationsByMarkerId = {}
@subscribe @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings
@subscribe @tokenizedBuffer.onDidChange @handleTokenizedBufferChange
@subscribe @buffer.onDidUpdateMarkers @handleBufferMarkersUpdated
@subscribe @buffer.onDidCreateMarker @handleBufferMarkerCreated
@overlayDecorationsById = {}
@disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings
@disposables.add @tokenizedBuffer.onDidChange @handleTokenizedBufferChange
@disposables.add @buffer.onDidCreateMarker @handleBufferMarkerCreated
@disposables.add @buffer.onDidUpdateMarkers => @emitter.emit 'did-update-markers'
@foldMarkerAttributes = Object.freeze({class: 'fold', displayBufferId: @id})
folds = (new Fold(this, marker) for marker in @buffer.findMarkers(@getFoldMarkerAttributes()))
@updateAllScreenLines()
@createFoldForMarker(marker) for marker in @buffer.findMarkers(@getFoldMarkerAttributes())
@decorateFold(fold) for fold in folds
subscribeToScopedConfigSettings: =>
@scopedConfigSubscriptions?.dispose()
@@ -69,12 +58,17 @@ class DisplayBuffer extends Model
scrollPastEnd: atom.config.get('editor.scrollPastEnd', scope: scopeDescriptor)
softWrap: atom.config.get('editor.softWrap', scope: scopeDescriptor)
softWrapAtPreferredLineLength: atom.config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor)
softWrapHangingIndent: atom.config.get('editor.softWrapHangingIndent', scope: scopeDescriptor)
preferredLineLength: atom.config.get('editor.preferredLineLength', scope: scopeDescriptor)
subscriptions.add atom.config.onDidChange 'editor.softWrap', scope: scopeDescriptor, ({newValue}) =>
@configSettings.softWrap = newValue
@updateWrappedScreenLines()
subscriptions.add atom.config.onDidChange 'editor.softWrapHangingIndent', scope: scopeDescriptor, ({newValue}) =>
@configSettings.softWrapHangingIndent = newValue
@updateWrappedScreenLines()
subscriptions.add atom.config.onDidChange 'editor.softWrapAtPreferredLineLength', scope: scopeDescriptor, ({newValue}) =>
@configSettings.softWrapAtPreferredLineLength = newValue
@updateWrappedScreenLines() if @isSoftWrapped()
@@ -95,14 +89,14 @@ class DisplayBuffer extends Model
scrollTop: @scrollTop
scrollLeft: @scrollLeft
tokenizedBuffer: @tokenizedBuffer.serialize()
invisibles: _.clone(@invisibles)
largeFileMode: @largeFileMode
deserializeParams: (params) ->
params.tokenizedBuffer = TokenizedBuffer.deserialize(params.tokenizedBuffer)
params
copy: ->
newDisplayBuffer = new DisplayBuffer({@buffer, tabLength: @getTabLength(), @invisibles})
newDisplayBuffer = new DisplayBuffer({@buffer, tabLength: @getTabLength(), @largeFileMode})
newDisplayBuffer.setScrollTop(@getScrollTop())
newDisplayBuffer.setScrollLeft(@getScrollLeft())
@@ -131,6 +125,20 @@ class DisplayBuffer extends Model
onDidChangeCharacterWidths: (callback) ->
@emitter.on 'did-change-character-widths', callback
onDidChangeScrollTop: (callback) ->
@emitter.on 'did-change-scroll-top', callback
onDidChangeScrollLeft: (callback) ->
@emitter.on 'did-change-scroll-left', callback
observeScrollTop: (callback) ->
callback(@scrollTop)
@onDidChangeScrollTop(callback)
observeScrollLeft: (callback) ->
callback(@scrollLeft)
@onDidChangeScrollLeft(callback)
observeDecorations: (callback) ->
callback(decoration) for decoration in @getDecorations()
@onDidAddDecoration(callback)
@@ -147,40 +155,13 @@ class DisplayBuffer extends Model
onDidUpdateMarkers: (callback) ->
@emitter.on 'did-update-markers', callback
on: (eventName) ->
switch eventName
when 'changed'
Grim.deprecate("Use DisplayBuffer::onDidChange instead")
when 'grammar-changed'
Grim.deprecate("Use DisplayBuffer::onDidChangeGrammar instead")
when 'soft-wrap-changed'
Grim.deprecate("Use DisplayBuffer::onDidChangeSoftWrap instead")
when 'character-widths-changed'
Grim.deprecate("Use DisplayBuffer::onDidChangeCharacterWidths instead")
when 'decoration-added'
Grim.deprecate("Use DisplayBuffer::onDidAddDecoration instead")
when 'decoration-removed'
Grim.deprecate("Use DisplayBuffer::onDidRemoveDecoration instead")
when 'decoration-changed'
Grim.deprecate("Use decoration.getMarker().onDidChange() instead")
when 'decoration-updated'
Grim.deprecate("Use Decoration::onDidChangeProperties instead")
when 'marker-created'
Grim.deprecate("Use Decoration::onDidCreateMarker instead")
when 'markers-updated'
Grim.deprecate("Use Decoration::onDidUpdateMarkers instead")
else
Grim.deprecate("DisplayBuffer::on is deprecated. Use event subscription methods instead.")
EmitterMixin::on.apply(this, arguments)
emitDidChange: (eventProperties, refreshMarkers=true) ->
if refreshMarkers
@pauseMarkerChangeEvents()
@refreshMarkerScreenPositions()
@emit 'changed', eventProperties
@emit 'changed', eventProperties if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change', eventProperties
@resumeMarkerChangeEvents()
if refreshMarkers
@refreshMarkerScreenPositions()
@emit 'markers-updated' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-update-markers'
updateWrappedScreenLines: ->
start = 0
@@ -188,19 +169,29 @@ class DisplayBuffer extends Model
@updateAllScreenLines()
screenDelta = @getLastRow() - end
bufferDelta = 0
@emitDidChange({ start, end, screenDelta, bufferDelta })
@emitDidChange({start, end, screenDelta, bufferDelta})
# Sets the visibility of the tokenized buffer.
#
# visible - A {Boolean} indicating of the tokenized buffer is shown
setVisible: (visible) -> @tokenizedBuffer.setVisible(visible)
getVerticalScrollMargin: -> @verticalScrollMargin
getVerticalScrollMargin: -> Math.min(@verticalScrollMargin, (@getHeight() - @getLineHeightInPixels()) / 2)
setVerticalScrollMargin: (@verticalScrollMargin) -> @verticalScrollMargin
getHorizontalScrollMargin: -> @horizontalScrollMargin
getVerticalScrollMarginInPixels: ->
scrollMarginInPixels = @getVerticalScrollMargin() * @getLineHeightInPixels()
maxScrollMarginInPixels = (@getHeight() - @getLineHeightInPixels()) / 2
Math.min(scrollMarginInPixels, maxScrollMarginInPixels)
getHorizontalScrollMargin: -> Math.min(@horizontalScrollMargin, (@getWidth() - @getDefaultCharWidth()) / 2)
setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin
getHorizontalScrollMarginInPixels: ->
scrollMarginInPixels = @getHorizontalScrollMargin() * @getDefaultCharWidth()
maxScrollMarginInPixels = (@getWidth() - @getDefaultCharWidth()) / 2
Math.min(scrollMarginInPixels, maxScrollMarginInPixels)
getHorizontalScrollbarHeight: -> @horizontalScrollbarHeight
setHorizontalScrollbarHeight: (@horizontalScrollbarHeight) -> @horizontalScrollbarHeight
@@ -263,26 +254,27 @@ class DisplayBuffer extends Model
getScrollTop: -> @scrollTop
setScrollTop: (scrollTop) ->
if @manageScrollPosition
@scrollTop = Math.round(Math.max(0, Math.min(@getMaxScrollTop(), scrollTop)))
else
@scrollTop = Math.round(scrollTop)
scrollTop = Math.round(Math.max(0, Math.min(@getMaxScrollTop(), scrollTop)))
unless scrollTop is @scrollTop
@scrollTop = scrollTop
@emitter.emit 'did-change-scroll-top', @scrollTop
@scrollTop
getMaxScrollTop: ->
@getScrollHeight() - @getClientHeight()
getScrollBottom: -> @scrollTop + @height
getScrollBottom: -> @scrollTop + @getClientHeight()
setScrollBottom: (scrollBottom) ->
@setScrollTop(scrollBottom - @getClientHeight())
@getScrollBottom()
getScrollLeft: -> @scrollLeft
setScrollLeft: (scrollLeft) ->
if @manageScrollPosition
@scrollLeft = Math.round(Math.max(0, Math.min(@getScrollWidth() - @getClientWidth(), scrollLeft)))
@scrollLeft
else
@scrollLeft = Math.round(scrollLeft)
scrollLeft = Math.round(Math.max(0, Math.min(@getScrollWidth() - @getClientWidth(), scrollLeft)))
unless scrollLeft is @scrollLeft
@scrollLeft = scrollLeft
@emitter.emit 'did-change-scroll-left', @scrollLeft
@scrollLeft
getMaxScrollLeft: ->
@getScrollWidth() - @getClientWidth()
@@ -329,7 +321,7 @@ class DisplayBuffer extends Model
characterWidthsChanged: ->
@computeScrollWidth()
@emit 'character-widths-changed', @scopedCharacterWidthsChangeCount
@emit 'character-widths-changed', @scopedCharacterWidthsChangeCount if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change-character-widths', @scopedCharacterWidthsChangeCount
clearScopedCharWidths: ->
@@ -348,12 +340,13 @@ class DisplayBuffer extends Model
getScrollWidth: ->
@scrollWidth
# Returns an {Array} of two numbers representing the first and the last visible rows.
getVisibleRowRange: ->
return [0, 0] unless @getLineHeightInPixels() > 0
heightInLines = Math.ceil(@getHeight() / @getLineHeightInPixels()) + 1
startRow = Math.floor(@getScrollTop() / @getLineHeightInPixels())
endRow = Math.min(@getLineCount(), startRow + heightInLines)
endRow = Math.ceil((@getScrollTop() + @getHeight()) / @getLineHeightInPixels()) - 1
endRow = Math.min(@getLineCount(), endRow)
[startRow, endRow]
@@ -366,15 +359,16 @@ class DisplayBuffer extends Model
@intersectsVisibleRowRange(start.row, end.row + 1)
scrollToScreenRange: (screenRange, options) ->
verticalScrollMarginInPixels = @getVerticalScrollMargin() * @getLineHeightInPixels()
horizontalScrollMarginInPixels = @getHorizontalScrollMargin() * @getDefaultCharWidth()
verticalScrollMarginInPixels = @getVerticalScrollMarginInPixels()
horizontalScrollMarginInPixels = @getHorizontalScrollMarginInPixels()
{top, left, height, width} = @pixelRectForScreenRange(screenRange)
bottom = top + height
right = left + width
{top, left} = @pixelRectForScreenRange(new Range(screenRange.start, screenRange.start))
{top: endTop, left: endLeft, height: endHeight} = @pixelRectForScreenRange(new Range(screenRange.end, screenRange.end))
bottom = endTop + endHeight
right = endLeft
if options?.center
desiredScrollCenter = top + height / 2
desiredScrollCenter = (top + bottom) / 2
unless @getScrollTop() < desiredScrollCenter < @getScrollBottom()
desiredScrollTop = desiredScrollCenter - @getHeight() / 2
desiredScrollBottom = desiredScrollCenter + @getHeight() / 2
@@ -385,15 +379,26 @@ class DisplayBuffer extends Model
desiredScrollLeft = left - horizontalScrollMarginInPixels
desiredScrollRight = right + horizontalScrollMarginInPixels
if desiredScrollTop < @getScrollTop()
@setScrollTop(desiredScrollTop)
else if desiredScrollBottom > @getScrollBottom()
@setScrollBottom(desiredScrollBottom)
if options?.reversed ? true
if desiredScrollBottom > @getScrollBottom()
@setScrollBottom(desiredScrollBottom)
if desiredScrollTop < @getScrollTop()
@setScrollTop(desiredScrollTop)
if desiredScrollLeft < @getScrollLeft()
@setScrollLeft(desiredScrollLeft)
else if desiredScrollRight > @getScrollRight()
@setScrollRight(desiredScrollRight)
if desiredScrollRight > @getScrollRight()
@setScrollRight(desiredScrollRight)
if desiredScrollLeft < @getScrollLeft()
@setScrollLeft(desiredScrollLeft)
else
if desiredScrollTop < @getScrollTop()
@setScrollTop(desiredScrollTop)
if desiredScrollBottom > @getScrollBottom()
@setScrollBottom(desiredScrollBottom)
if desiredScrollLeft < @getScrollLeft()
@setScrollLeft(desiredScrollLeft)
if desiredScrollRight > @getScrollRight()
@setScrollRight(desiredScrollRight)
scrollToScreenPosition: (screenPosition, options) ->
@scrollToScreenRange(new Range(screenPosition, screenPosition), options)
@@ -426,22 +431,25 @@ class DisplayBuffer extends Model
setTabLength: (tabLength) ->
@tokenizedBuffer.setTabLength(tabLength)
setInvisibles: (@invisibles) ->
@tokenizedBuffer.setInvisibles(@invisibles)
setIgnoreInvisibles: (ignoreInvisibles) ->
@tokenizedBuffer.setIgnoreInvisibles(ignoreInvisibles)
setSoftWrapped: (softWrapped) ->
if softWrapped isnt @softWrapped
@softWrapped = softWrapped
@updateWrappedScreenLines()
softWrapped = @isSoftWrapped()
@emit 'soft-wrap-changed', softWrapped
@emit 'soft-wrap-changed', softWrapped if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change-soft-wrapped', softWrapped
softWrapped
else
@isSoftWrapped()
isSoftWrapped: ->
@softWrapped ? @configSettings.softWrap ? false
if @largeFileMode
false
else
@softWrapped ? @configSettings.softWrap ? false
# Set the number of characters that fit horizontally in the editor.
#
@@ -474,7 +482,14 @@ class DisplayBuffer extends Model
#
# Returns {TokenizedLine}
tokenizedLineForScreenRow: (screenRow) ->
@screenLines[screenRow]
if @largeFileMode
if line = @tokenizedBuffer.tokenizedLineForRow(screenRow)
if line.text.length > @maxLineLength
@maxLineLength = line.text.length
@longestScreenRow = screenRow
line
else
@screenLines[screenRow]
# Gets the screen lines for the given screen row range.
#
@@ -483,13 +498,19 @@ class DisplayBuffer extends Model
#
# Returns an {Array} of {TokenizedLine}s.
tokenizedLinesForScreenRows: (startRow, endRow) ->
@screenLines[startRow..endRow]
if @largeFileMode
@tokenizedBuffer.tokenizedLinesForRows(startRow, endRow)
else
@screenLines[startRow..endRow]
# Gets all the screen lines.
#
# Returns an {Array} of {TokenizedLine}s.
getTokenizedLines: ->
new Array(@screenLines...)
if @largeFileMode
@tokenizedBuffer.tokenizedLinesForRows(0, @getLastRow())
else
new Array(@screenLines...)
indentLevelForLine: (line) ->
@tokenizedBuffer.indentLevelForLine(line)
@@ -502,8 +523,11 @@ class DisplayBuffer extends Model
#
# Returns an {Array} of buffer rows as {Numbers}s.
bufferRowsForScreenRows: (startScreenRow, endScreenRow) ->
for screenRow in [startScreenRow..endScreenRow]
@rowMap.bufferRowRangeForScreenRow(screenRow)[0]
if @largeFileMode
[startScreenRow..endScreenRow]
else
for screenRow in [startScreenRow..endScreenRow]
@rowMap.bufferRowRangeForScreenRow(screenRow)[0]
# Creates a new fold between two row numbers.
#
@@ -512,10 +536,11 @@ class DisplayBuffer extends Model
#
# Returns the new {Fold}.
createFold: (startRow, endRow) ->
foldMarker =
@findFoldMarker({startRow, endRow}) ?
@buffer.markRange([[startRow, 0], [endRow, Infinity]], @getFoldMarkerAttributes())
@foldForMarker(foldMarker)
unless @largeFileMode
foldMarker =
@findFoldMarker({startRow, endRow}) ?
@buffer.markRange([[startRow, 0], [endRow, Infinity]], @getFoldMarkerAttributes())
@foldForMarker(foldMarker)
isFoldedAtBufferRow: (bufferRow) ->
@largestFoldContainingBufferRow(bufferRow)?
@@ -532,6 +557,7 @@ class DisplayBuffer extends Model
# bufferRow - The buffer row {Number} to check against
unfoldBufferRow: (bufferRow) ->
fold.destroy() for fold in @foldsContainingBufferRow(bufferRow)
return
# Given a buffer row, this returns the largest fold that starts there.
#
@@ -578,9 +604,17 @@ class DisplayBuffer extends Model
# Returns the folds in the given row range (exclusive of end row) that are
# not contained by any other folds.
outermostFoldsInBufferRowRange: (startRow, endRow) ->
@findFoldMarkers(containedInRange: [[startRow, 0], [endRow, 0]])
.map (marker) => @foldForMarker(marker)
.filter (fold) -> not fold.isInsideLargerFold()
folds = []
lastFoldEndRow = -1
for marker in @findFoldMarkers(intersectsRowRange: [startRow, endRow])
range = marker.getRange()
if range.start.row > lastFoldEndRow
lastFoldEndRow = range.end.row
if startRow <= range.start.row <= range.end.row < endRow
folds.push(@foldForMarker(marker))
folds
# Public: Given a buffer row, this returns folds that include it.
#
@@ -598,10 +632,16 @@ class DisplayBuffer extends Model
#
# Returns a {Number}.
screenRowForBufferRow: (bufferRow) ->
@rowMap.screenRowRangeForBufferRow(bufferRow)[0]
if @largeFileMode
bufferRow
else
@rowMap.screenRowRangeForBufferRow(bufferRow)[0]
lastScreenRowForBufferRow: (bufferRow) ->
@rowMap.screenRowRangeForBufferRow(bufferRow)[1] - 1
if @largeFileMode
bufferRow
else
@rowMap.screenRowRangeForBufferRow(bufferRow)[1] - 1
# Given a screen row, this converts it into a buffer row.
#
@@ -609,7 +649,10 @@ class DisplayBuffer extends Model
#
# Returns a {Number}.
bufferRowForScreenRow: (screenRow) ->
@rowMap.bufferRowRangeForScreenRow(screenRow)[0]
if @largeFileMode
screenRow
else
@rowMap.bufferRowRangeForScreenRow(screenRow)[0]
# Given a buffer range, this converts it into a screen position.
#
@@ -648,16 +691,19 @@ class DisplayBuffer extends Model
top = targetRow * @lineHeightInPixels
left = 0
column = 0
for token in @tokenizedLineForScreenRow(targetRow).tokens
charWidths = @getScopedCharWidths(token.scopes)
iterator = @tokenizedLineForScreenRow(targetRow).getTokenIterator()
while iterator.next()
charWidths = @getScopedCharWidths(iterator.getScopes())
valueIndex = 0
while valueIndex < token.value.length
if token.hasPairedCharacter
char = token.value.substr(valueIndex, 2)
value = iterator.getText()
while valueIndex < value.length
if iterator.isPairedCharacter()
char = value
charLength = 2
valueIndex += 2
else
char = token.value[valueIndex]
char = value[valueIndex]
charLength = 1
valueIndex++
@@ -671,22 +717,26 @@ class DisplayBuffer extends Model
targetLeft = pixelPosition.left
defaultCharWidth = @defaultCharWidth
row = Math.floor(targetTop / @getLineHeightInPixels())
targetLeft = 0 if row < 0
targetLeft = Infinity if row > @getLastRow()
row = Math.min(row, @getLastRow())
row = Math.max(0, row)
left = 0
column = 0
for token in @tokenizedLineForScreenRow(row).tokens
charWidths = @getScopedCharWidths(token.scopes)
iterator = @tokenizedLineForScreenRow(row).getTokenIterator()
while iterator.next()
charWidths = @getScopedCharWidths(iterator.getScopes())
value = iterator.getText()
valueIndex = 0
while valueIndex < token.value.length
if token.hasPairedCharacter
char = token.value.substr(valueIndex, 2)
while valueIndex < value.length
if iterator.isPairedCharacter()
char = value
charLength = 2
valueIndex += 2
else
char = token.value[valueIndex]
char = value[valueIndex]
charLength = 1
valueIndex++
@@ -704,7 +754,10 @@ class DisplayBuffer extends Model
#
# Returns a {Number}.
getLineCount: ->
@screenLines.length
if @largeFileMode
@tokenizedBuffer.getLineCount()
else
@screenLines.length
# Gets the number of the last screen line.
#
@@ -736,10 +789,10 @@ class DisplayBuffer extends Model
screenPositionForBufferPosition: (bufferPosition, options) ->
throw new Error("This TextEditor has been destroyed") if @isDestroyed()
{ row, column } = @buffer.clipPosition(bufferPosition)
{row, column} = @buffer.clipPosition(bufferPosition)
[startScreenRow, endScreenRow] = @rowMap.screenRowRangeForBufferRow(row)
for screenRow in [startScreenRow...endScreenRow]
screenLine = @screenLines[screenRow]
screenLine = @tokenizedLineForScreenRow(screenRow)
unless screenLine?
throw new BufferToScreenConversionError "No screen line exists when converting buffer row to screen row",
@@ -770,9 +823,9 @@ class DisplayBuffer extends Model
#
# Returns a {Point}.
bufferPositionForScreenPosition: (screenPosition, options) ->
{ row, column } = @clipScreenPosition(Point.fromObject(screenPosition), options)
{row, column} = @clipScreenPosition(Point.fromObject(screenPosition), options)
[bufferRow] = @rowMap.bufferRowRangeForScreenRow(row)
new Point(bufferRow, @screenLines[row].bufferColumnForScreenColumn(column))
new Point(bufferRow, @tokenizedLineForScreenRow(row).bufferColumnForScreenColumn(column))
# Retrieves the grammar's token scopeDescriptor for a buffer position.
#
@@ -824,8 +877,8 @@ class DisplayBuffer extends Model
#
# Returns the new, clipped {Point}. Note that this could be the same as `position` if no clipping was performed.
clipScreenPosition: (screenPosition, options={}) ->
{ wrapBeyondNewlines, wrapAtSoftNewlines, skipSoftWrapIndentation } = options
{ row, column } = Point.fromObject(screenPosition)
{wrapBeyondNewlines, wrapAtSoftNewlines, skipSoftWrapIndentation} = options
{row, column} = Point.fromObject(screenPosition)
if row < 0
row = 0
@@ -836,13 +889,13 @@ class DisplayBuffer extends Model
else if column < 0
column = 0
screenLine = @screenLines[row]
screenLine = @tokenizedLineForScreenRow(row)
maxScreenColumn = screenLine.getMaxScreenColumn()
if screenLine.isSoftWrapped() and column >= maxScreenColumn
if wrapAtSoftNewlines
row++
column = @screenLines[row].clipScreenColumn(0)
column = @tokenizedLineForScreenRow(row).clipScreenColumn(0)
else
column = screenLine.clipScreenColumn(maxScreenColumn - 1)
else if screenLine.isColumnInsideSoftWrapIndentation(column)
@@ -850,7 +903,7 @@ class DisplayBuffer extends Model
column = screenLine.clipScreenColumn(0)
else
row--
column = @screenLines[row].getMaxScreenColumn() - 1
column = @tokenizedLineForScreenRow(row).getMaxScreenColumn() - 1
else if wrapBeyondNewlines and column > maxScreenColumn and row < @getLastRow()
row++
column = 0
@@ -858,6 +911,18 @@ class DisplayBuffer extends Model
column = screenLine.clipScreenColumn(column, options)
new Point(row, column)
# Clip the start and end of the given range to valid positions on screen.
# See {::clipScreenPosition} for more information.
#
# * `range` The {Range} to clip.
# * `options` (optional) See {::clipScreenPosition} `options`.
# Returns a {Range}.
clipScreenRange: (range, options) ->
start = @clipScreenPosition(range.start, options)
end = @clipScreenPosition(range.end, options)
new Range(start, end)
# Calculates a {Range} representing the start of the {TextBuffer} until the end.
#
# Returns a {Range}.
@@ -870,7 +935,7 @@ class DisplayBuffer extends Model
getDecorations: (propertyFilter) ->
allDecorations = []
for markerId, decorations of @decorationsByMarkerId
allDecorations = allDecorations.concat(decorations) if decorations?
allDecorations.push(decorations...) if decorations?
if propertyFilter?
allDecorations = allDecorations.filter (decoration) ->
for key, value of propertyFilter
@@ -888,7 +953,16 @@ class DisplayBuffer extends Model
@getDecorations(propertyFilter).filter (decoration) -> decoration.isType('highlight')
getOverlayDecorations: (propertyFilter) ->
@getDecorations(propertyFilter).filter (decoration) -> decoration.isType('overlay')
result = []
for id, decoration of @overlayDecorationsById
result.push(decoration)
if propertyFilter?
result.filter (decoration) ->
for key, value of propertyFilter
return false unless decoration.properties[key] is value
true
else
result
decorationsForScreenRowRange: (startScreenRow, endScreenRow) ->
decorationsByMarkerId = {}
@@ -900,11 +974,15 @@ class DisplayBuffer extends Model
decorateMarker: (marker, decorationParams) ->
marker = @getMarker(marker.id)
decoration = new Decoration(marker, this, decorationParams)
@subscribe decoration.onDidDestroy => @removeDecoration(decoration)
decorationDestroyedDisposable = decoration.onDidDestroy =>
@removeDecoration(decoration)
@disposables.remove(decorationDestroyedDisposable)
@disposables.add(decorationDestroyedDisposable)
@decorationsByMarkerId[marker.id] ?= []
@decorationsByMarkerId[marker.id].push(decoration)
@overlayDecorationsById[decoration.id] = decoration if decoration.isType('overlay')
@decorationsById[decoration.id] = decoration
@emit 'decoration-added', decoration
@emit 'decoration-added', decoration if Grim.includeDeprecatedAPIs
@emitter.emit 'did-add-decoration', decoration
decoration
@@ -916,9 +994,13 @@ class DisplayBuffer extends Model
if index > -1
decorations.splice(index, 1)
delete @decorationsById[decoration.id]
@emit 'decoration-removed', decoration
@emit 'decoration-removed', decoration if Grim.includeDeprecatedAPIs
@emitter.emit 'did-remove-decoration', decoration
delete @decorationsByMarkerId[marker.id] if decorations.length is 0
delete @overlayDecorationsById[decoration.id]
decorationsForMarkerId: (markerId) ->
@decorationsByMarkerId[markerId]
# Retrieves a {Marker} based on its id.
#
@@ -1059,41 +1141,40 @@ class DisplayBuffer extends Model
findFoldMarkers: (attributes) ->
@buffer.findMarkers(@getFoldMarkerAttributes(attributes))
getFoldMarkerAttributes: (attributes={}) ->
_.extend(attributes, class: 'fold', displayBufferId: @id)
pauseMarkerChangeEvents: ->
marker.pauseChangeEvents() for marker in @getMarkers()
resumeMarkerChangeEvents: ->
marker.resumeChangeEvents() for marker in @getMarkers()
@emit 'markers-updated'
@emitter.emit 'did-update-markers'
getFoldMarkerAttributes: (attributes) ->
if attributes
_.extend(attributes, @foldMarkerAttributes)
else
@foldMarkerAttributes
refreshMarkerScreenPositions: ->
for marker in @getMarkers()
marker.notifyObservers(textChanged: false)
return
destroyed: ->
marker.unsubscribe() for id, marker of @markers
marker.disposables.dispose() for id, marker of @markers
@scopedConfigSubscriptions.dispose()
@unsubscribe()
@disposables.dispose()
@tokenizedBuffer.destroy()
logLines: (start=0, end=@getLastRow()) ->
for row in [start..end]
line = @tokenizedLineForScreenRow(row).text
console.log row, @bufferRowForScreenRow(row), line, line.length
return
getRootScopeDescriptor: ->
@tokenizedBuffer.rootScopeDescriptor
handleTokenizedBufferChange: (tokenizedBufferChange) =>
{start, end, delta, bufferChange} = tokenizedBufferChange
@updateScreenLines(start, end + 1, delta, delayChangeEvent: bufferChange?)
@setScrollTop(Math.min(@getScrollTop(), @getMaxScrollTop())) if @manageScrollPosition and delta < 0
@updateScreenLines(start, end + 1, delta, refreshMarkers: false)
@setScrollTop(Math.min(@getScrollTop(), @getMaxScrollTop())) if delta < 0
updateScreenLines: (startBufferRow, endBufferRow, bufferDelta=0, options={}) ->
return if @largeFileMode
startBufferRow = @rowMap.bufferRowRangeForBufferRow(startBufferRow)[0]
endBufferRow = @rowMap.bufferRowRangeForBufferRow(endBufferRow - 1)[1]
startScreenRow = @rowMap.screenRowRangeForBufferRow(startBufferRow)[0]
@@ -1101,7 +1182,7 @@ class DisplayBuffer extends Model
{screenLines, regions} = @buildScreenLines(startBufferRow, endBufferRow + bufferDelta)
screenDelta = screenLines.length - (endScreenRow - startScreenRow)
@screenLines[startScreenRow...endScreenRow] = screenLines
_.spliceWithArray(@screenLines, startScreenRow, endScreenRow - startScreenRow, screenLines, 10000)
@rowMap.spliceRegions(startBufferRow, endBufferRow - startBufferRow, regions)
@findMaxLineLength(startScreenRow, endScreenRow, screenLines, screenDelta)
@@ -1113,22 +1194,22 @@ class DisplayBuffer extends Model
screenDelta: screenDelta
bufferDelta: bufferDelta
if options.delayChangeEvent
@pauseMarkerChangeEvents()
@pendingChangeEvent = changeEvent
else
@emitDidChange(changeEvent, options.refreshMarkers)
@emitDidChange(changeEvent, options.refreshMarkers)
buildScreenLines: (startBufferRow, endBufferRow) ->
screenLines = []
regions = []
rectangularRegion = null
foldsByStartRow = {}
for fold in @outermostFoldsInBufferRowRange(startBufferRow, endBufferRow)
foldsByStartRow[fold.getStartRow()] = fold
bufferRow = startBufferRow
while bufferRow < endBufferRow
tokenizedLine = @tokenizedBuffer.tokenizedLineForRow(bufferRow)
if fold = @largestFoldStartingAtBufferRow(bufferRow)
if fold = foldsByStartRow[bufferRow]
foldLine = tokenizedLine.copy()
foldLine.fold = fold
screenLines.push(foldLine)
@@ -1144,7 +1225,10 @@ class DisplayBuffer extends Model
softWraps = 0
if @isSoftWrapped()
while wrapScreenColumn = tokenizedLine.findWrapColumn(@getSoftWrapColumn())
[wrappedLine, tokenizedLine] = tokenizedLine.softWrapAt(wrapScreenColumn)
[wrappedLine, tokenizedLine] = tokenizedLine.softWrapAt(
wrapScreenColumn,
@configSettings.softWrapHangingIndent
)
break if wrappedLine.hasOnlySoftWrapIndentation()
screenLines.push(wrappedLine)
softWraps++
@@ -1194,22 +1278,81 @@ class DisplayBuffer extends Model
@scrollWidth += 1 unless @isSoftWrapped()
@setScrollLeft(Math.min(@getScrollLeft(), @getMaxScrollLeft()))
handleBufferMarkersUpdated: =>
if event = @pendingChangeEvent
@pendingChangeEvent = null
@emitDidChange(event, false)
handleBufferMarkerCreated: (textBufferMarker) =>
@createFoldForMarker(textBufferMarker) if textBufferMarker.matchesParams(@getFoldMarkerAttributes())
if textBufferMarker.matchesParams(@getFoldMarkerAttributes())
fold = new Fold(this, textBufferMarker)
fold.updateDisplayBuffer()
@decorateFold(fold)
if marker = @getMarker(textBufferMarker.id)
# The marker might have been removed in some other handler called before
# this one. Only emit when the marker still exists.
@emit 'marker-created', marker
@emit 'marker-created', marker if Grim.includeDeprecatedAPIs
@emitter.emit 'did-create-marker', marker
createFoldForMarker: (marker) ->
@decorateMarker(marker, type: 'line-number', class: 'folded')
new Fold(this, marker)
decorateFold: (fold) ->
@decorateMarker(fold.marker, type: 'line-number', class: 'folded')
foldForMarker: (marker) ->
@foldsByMarkerId[marker.id]
decorationDidChangeType: (decoration) ->
if decoration.isType('overlay')
@overlayDecorationsById[decoration.id] = decoration
else
delete @overlayDecorationsById[decoration.id]
if Grim.includeDeprecatedAPIs
DisplayBuffer.properties
softWrapped: null
editorWidthInChars: null
lineHeightInPixels: null
defaultCharWidth: null
height: null
width: null
scrollTop: 0
scrollLeft: 0
scrollWidth: 0
verticalScrollbarWidth: 15
horizontalScrollbarHeight: 15
EmitterMixin = require('emissary').Emitter
DisplayBuffer::on = (eventName) ->
switch eventName
when 'changed'
Grim.deprecate("Use DisplayBuffer::onDidChange instead")
when 'grammar-changed'
Grim.deprecate("Use DisplayBuffer::onDidChangeGrammar instead")
when 'soft-wrap-changed'
Grim.deprecate("Use DisplayBuffer::onDidChangeSoftWrap instead")
when 'character-widths-changed'
Grim.deprecate("Use DisplayBuffer::onDidChangeCharacterWidths instead")
when 'decoration-added'
Grim.deprecate("Use DisplayBuffer::onDidAddDecoration instead")
when 'decoration-removed'
Grim.deprecate("Use DisplayBuffer::onDidRemoveDecoration instead")
when 'decoration-changed'
Grim.deprecate("Use decoration.getMarker().onDidChange() instead")
when 'decoration-updated'
Grim.deprecate("Use Decoration::onDidChangeProperties instead")
when 'marker-created'
Grim.deprecate("Use Decoration::onDidCreateMarker instead")
when 'markers-updated'
Grim.deprecate("Use Decoration::onDidUpdateMarkers instead")
else
Grim.deprecate("DisplayBuffer::on is deprecated. Use event subscription methods instead.")
EmitterMixin::on.apply(this, arguments)
else
DisplayBuffer::softWrapped = null
DisplayBuffer::editorWidthInChars = null
DisplayBuffer::lineHeightInPixels = null
DisplayBuffer::defaultCharWidth = null
DisplayBuffer::height = null
DisplayBuffer::width = null
DisplayBuffer::scrollTop = 0
DisplayBuffer::scrollLeft = 0
DisplayBuffer::scrollWidth = 0
DisplayBuffer::verticalScrollbarWidth = 15
DisplayBuffer::horizontalScrollbarHeight = 15

View File

@@ -13,7 +13,6 @@ class Fold
constructor: (@displayBuffer, @marker) ->
@id = @marker.id
@displayBuffer.foldsByMarkerId[@marker.id] = this
@updateDisplayBuffer()
@marker.onDidDestroy => @destroyed()
@marker.onDidChange ({isValid}) => @destroy() unless isValid

View File

@@ -1,11 +1,10 @@
{basename, join} = require 'path'
_ = require 'underscore-plus'
EmitterMixin = require('emissary').Emitter
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
fs = require 'fs-plus'
GitUtils = require 'git-utils'
{deprecate} = require 'grim'
{includeDeprecatedAPIs, deprecate} = require 'grim'
Task = require './task'
@@ -43,8 +42,6 @@ Task = require './task'
# ```
module.exports =
class GitRepository
EmitterMixin.includeInto(this)
@exists: (path) ->
if git = @open(path)
git.destroy()
@@ -96,7 +93,8 @@ class GitRepository
@subscriptions.add new Disposable(-> window.removeEventListener 'focus', onWindowFocus)
if @project?
@subscriptions.add @project.eachBuffer (buffer) => @subscribeToBuffer(buffer)
@project.getBuffers().forEach (buffer) => @subscribeToBuffer(buffer)
@subscriptions.add @project.onDidAddBuffer (buffer) => @subscribeToBuffer(buffer)
# Public: Destroy this {GitRepository} object.
#
@@ -122,6 +120,10 @@ class GitRepository
# Public: Invoke the given callback when this GitRepository's destroy() method
# is invoked.
#
# * `callback` {Function}
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidDestroy: (callback) ->
@emitter.on 'did-destroy', callback
@@ -154,20 +156,16 @@ class GitRepository
onDidChangeStatuses: (callback) ->
@emitter.on 'did-change-statuses', callback
on: (eventName) ->
switch eventName
when 'status-changed'
deprecate 'Use GitRepository::onDidChangeStatus instead'
when 'statuses-changed'
deprecate 'Use GitRepository::onDidChangeStatuses instead'
else
deprecate 'GitRepository::on is deprecated. Use event subscription methods instead.'
EmitterMixin::on.apply(this, arguments)
###
Section: Repository Details
###
# Public: A {String} indicating the type of version control system used by
# this repository.
#
# Returns `"git"`.
getType: -> 'git'
# Public: Returns the {String} path of the repository.
getPath: ->
@path ?= fs.absolute(@getRepo().getPath())
@@ -245,9 +243,6 @@ class GitRepository
# * `path` (optional) {String} path in the repository to get this information
# for, only needed if the repository has submodules.
getOriginURL: (path) -> @getConfigValue('remote.origin.url', path)
getOriginUrl: (path) ->
deprecate 'Use ::getOriginURL instead.'
@getOriginURL(path)
# Public: Returns the upstream branch for the current HEAD, or null if there
# is no upstream branch for the current HEAD.
@@ -282,14 +277,24 @@ class GitRepository
###
# Public: Returns true if the given path is modified.
#
# * `path` The {String} path to check.
#
# Returns a {Boolean} that's true if the `path` is modified.
isPathModified: (path) -> @isStatusModified(@getPathStatus(path))
# Public: Returns true if the given path is new.
#
# * `path` The {String} path to check.
#
# Returns a {Boolean} that's true if the `path` is new.
isPathNew: (path) -> @isStatusNew(@getPathStatus(path))
# Public: Is the given path ignored?
#
# Returns a {Boolean}.
# * `path` The {String} path to check.
#
# Returns a {Boolean} that's true if the `path` is ignored.
isPathIgnored: (path) -> @getRepo().isIgnored(@relativize(path))
# Public: Get the status of a directory in the repository's working directory.
@@ -322,7 +327,7 @@ class GitRepository
else
delete @statuses[relativePath]
if currentPathStatus isnt pathStatus
@emit 'status-changed', path, pathStatus
@emit 'status-changed', path, pathStatus if includeDeprecatedAPIs
@emitter.emit 'did-change-status', {path, pathStatus}
pathStatus
@@ -336,9 +341,17 @@ class GitRepository
@statuses[@relativize(path)]
# Public: Returns true if the given status indicates modification.
#
# * `status` A {Number} representing the status.
#
# Returns a {Boolean} that's true if the `status` indicates modification.
isStatusModified: (status) -> @getRepo().isStatusModified(status)
# Public: Returns true if the given status indicates a new path.
#
# * `status` A {Number} representing the status.
#
# Returns a {Boolean} that's true if the `status` indicates a new path.
isStatusNew: (status) -> @getRepo().isStatusNew(status)
###
@@ -483,5 +496,23 @@ class GitRepository
submoduleRepo.upstream = submodules[submodulePath]?.upstream ? {ahead: 0, behind: 0}
unless statusesUnchanged
@emit 'statuses-changed'
@emit 'statuses-changed' if includeDeprecatedAPIs
@emitter.emit 'did-change-statuses'
if includeDeprecatedAPIs
EmitterMixin = require('emissary').Emitter
EmitterMixin.includeInto(GitRepository)
GitRepository::on = (eventName) ->
switch eventName
when 'status-changed'
deprecate 'Use GitRepository::onDidChangeStatus instead'
when 'statuses-changed'
deprecate 'Use GitRepository::onDidChangeStatuses instead'
else
deprecate 'GitRepository::on is deprecated. Use event subscription methods instead.'
EmitterMixin::on.apply(this, arguments)
GitRepository::getOriginUrl = (path) ->
deprecate 'Use ::getOriginURL instead.'
@getOriginURL(path)

View File

@@ -1,13 +1,6 @@
_ = require 'underscore-plus'
{deprecate} = require 'grim'
{specificity} = require 'clear-cut'
{Subscriber} = require 'emissary'
{Emitter} = require 'event-kit'
{includeDeprecatedAPIs, deprecate} = require 'grim'
FirstMate = require 'first-mate'
{ScopeSelector} = FirstMate
ScopedPropertyStore = require 'scoped-property-store'
PropertyAccessors = require 'property-accessors'
{$, $$} = require './space-pen-extensions'
Token = require './token'
# Extended: Syntax class holding the grammars used for tokenizing.
@@ -18,16 +11,12 @@ Token = require './token'
# language-specific comment regexes. See {::getProperty} for more details.
module.exports =
class GrammarRegistry extends FirstMate.GrammarRegistry
PropertyAccessors.includeInto(this)
Subscriber.includeInto(this)
@deserialize: ({grammarOverridesByPath}) ->
grammarRegistry = new GrammarRegistry()
grammarRegistry.grammarOverridesByPath = grammarOverridesByPath
grammarRegistry
atom.deserializers.add(this)
atom.deserializers.add(name: 'Syntax', deserialize: @deserialize) # Support old serialization
constructor: ->
super(maxTokensPerLine: 100)
@@ -46,26 +35,50 @@ class GrammarRegistry extends FirstMate.GrammarRegistry
# * `fileContents` A {String} of text for the file path.
#
# Returns a {Grammar}, never null.
selectGrammar: (filePath, fileContents) -> super
selectGrammar: (filePath, fileContents) ->
bestMatch = null
highestScore = -Infinity
for grammar in @grammars
score = grammar.getScore(filePath, fileContents)
if score > highestScore or not bestMatch?
bestMatch = grammar
highestScore = score
else if score is highestScore and bestMatch?.bundledPackage
bestMatch = grammar unless grammar.bundledPackage
bestMatch
clearObservers: ->
@off() if includeDeprecatedAPIs
@emitter = new Emitter
if includeDeprecatedAPIs
PropertyAccessors = require 'property-accessors'
PropertyAccessors.includeInto(GrammarRegistry)
{Subscriber} = require 'emissary'
Subscriber.includeInto(GrammarRegistry)
# Support old serialization
atom.deserializers.add(name: 'Syntax', deserialize: GrammarRegistry.deserialize)
# Deprecated: Used by settings-view to display snippets for packages
@::accessor 'propertyStore', ->
GrammarRegistry::accessor 'propertyStore', ->
deprecate("Do not use this. Use a public method on Config")
atom.config.scopedSettingsStore
addProperties: (args...) ->
args.unshift(null) if args.length == 2
GrammarRegistry::addProperties = (args...) ->
args.unshift(null) if args.length is 2
deprecate 'Consider using atom.config.set() instead. A direct (but private) replacement is available at atom.config.addScopedSettings().'
atom.config.addScopedSettings(args...)
removeProperties: (name) ->
GrammarRegistry::removeProperties = (name) ->
deprecate 'atom.config.addScopedSettings() now returns a disposable you can call .dispose() on'
atom.config.scopedSettingsStore.removeProperties(name)
getProperty: (scope, keyPath) ->
GrammarRegistry::getProperty = (scope, keyPath) ->
deprecate 'A direct (but private) replacement is available at atom.config.getRawScopedValue().'
atom.config.getRawScopedValue(scope, keyPath)
propertiesForScope: (scope, keyPath) ->
GrammarRegistry::propertiesForScope = (scope, keyPath) ->
deprecate 'Use atom.config.getAll instead.'
atom.config.settingsForScopeDescriptor(scope, keyPath)

View File

@@ -0,0 +1,28 @@
# Helper methods shared among GutterComponent classes.
module.exports =
createGutterView: (gutterModel) ->
domNode = document.createElement('div')
domNode.classList.add('gutter')
domNode.setAttribute('gutter-name', gutterModel.name)
childNode = document.createElement('div')
if gutterModel.name is 'line-number'
childNode.classList.add('line-numbers')
else
childNode.classList.add('custom-decorations')
domNode.appendChild(childNode)
domNode
# Sets scrollHeight, scrollTop, and backgroundColor on the given domNode.
setDimensionsAndBackground: (oldState, newState, domNode) ->
if newState.scrollHeight isnt oldState.scrollHeight
domNode.style.height = newState.scrollHeight + 'px'
oldState.scrollHeight = newState.scrollHeight
if newState.scrollTop isnt oldState.scrollTop
domNode.style['-webkit-transform'] = "translate3d(0px, #{-newState.scrollTop}px, 0px)"
oldState.scrollTop = newState.scrollTop
if newState.backgroundColor isnt oldState.backgroundColor
domNode.style.backgroundColor = newState.backgroundColor
oldState.backgroundColor = newState.backgroundColor

View File

@@ -0,0 +1,111 @@
_ = require 'underscore-plus'
CustomGutterComponent = require './custom-gutter-component'
LineNumberGutterComponent = require './line-number-gutter-component'
# The GutterContainerComponent manages the GutterComponents of a particular
# TextEditorComponent.
module.exports =
class GutterContainerComponent
constructor: ({@onLineNumberGutterMouseDown, @editor}) ->
# An array of objects of the form: {name: {String}, component: {Object}}
@gutterComponents = []
@gutterComponentsByGutterName = {}
@lineNumberGutterComponent = null
@domNode = document.createElement('div')
@domNode.classList.add('gutter-container')
@domNode.style.display = 'flex'
destroy: ->
for {name, component} in @gutterComponents
component.destroy?()
return
getDomNode: ->
@domNode
getLineNumberGutterComponent: ->
@lineNumberGutterComponent
updateSync: (state) ->
# The GutterContainerComponent expects the gutters to be sorted in the order
# they should appear.
newState = state.gutters
newGutterComponents = []
newGutterComponentsByGutterName = {}
for {gutter, visible, styles, content} in newState
gutterComponent = @gutterComponentsByGutterName[gutter.name]
if not gutterComponent
if gutter.name is 'line-number'
gutterComponent = new LineNumberGutterComponent({onMouseDown: @onLineNumberGutterMouseDown, @editor, gutter})
@lineNumberGutterComponent = gutterComponent
else
gutterComponent = new CustomGutterComponent({gutter})
if visible then gutterComponent.showNode() else gutterComponent.hideNode()
# Pass the gutter only the state that it needs.
if gutter.name is 'line-number'
# For ease of use in the line number gutter component, set the shared
# 'styles' as a field under the 'content'.
gutterSubstate = _.clone(content)
gutterSubstate.styles = styles
else
# Custom gutter 'content' is keyed on gutter name, so we cannot set
# 'styles' as a subfield directly under it.
gutterSubstate = {content, styles}
gutterComponent.updateSync(gutterSubstate)
newGutterComponents.push({
name: gutter.name,
component: gutterComponent,
})
newGutterComponentsByGutterName[gutter.name] = gutterComponent
@reorderGutters(newGutterComponents, newGutterComponentsByGutterName)
@gutterComponents = newGutterComponents
@gutterComponentsByGutterName = newGutterComponentsByGutterName
###
Section: Private Methods
###
reorderGutters: (newGutterComponents, newGutterComponentsByGutterName) ->
# First, insert new gutters into the DOM.
indexInOldGutters = 0
oldGuttersLength = @gutterComponents.length
for gutterComponentDescription in newGutterComponents
gutterComponent = gutterComponentDescription.component
gutterName = gutterComponentDescription.name
if @gutterComponentsByGutterName[gutterName]
# If the gutter existed previously, we first try to move the cursor to
# the point at which it occurs in the previous gutters.
matchingGutterFound = false
while indexInOldGutters < oldGuttersLength
existingGutterComponentDescription = @gutterComponents[indexInOldGutters]
existingGutterComponent = existingGutterComponentDescription.component
indexInOldGutters++
if existingGutterComponent is gutterComponent
matchingGutterFound = true
break
if not matchingGutterFound
# If we've reached this point, the gutter previously existed, but its
# position has moved. Remove it from the DOM and re-insert it.
gutterComponent.getDomNode().remove()
@domNode.appendChild(gutterComponent.getDomNode())
else
if indexInOldGutters is oldGuttersLength
@domNode.appendChild(gutterComponent.getDomNode())
else
@domNode.insertBefore(gutterComponent.getDomNode(), @domNode.children[indexInOldGutters])
# Remove any gutters that were not present in the new gutters state.
for gutterComponentDescription in @gutterComponents
if not newGutterComponentsByGutterName[gutterComponentDescription.name]
gutterComponent = gutterComponentDescription.component
gutterComponent.getDomNode().remove()

View File

@@ -0,0 +1,96 @@
{Emitter} = require 'event-kit'
Gutter = require './gutter'
# This class encapsulates the logic for adding and modifying a set of gutters.
module.exports =
class GutterContainer
# * `textEditor` The {TextEditor} to which this {GutterContainer} belongs.
constructor: (textEditor) ->
@gutters = []
@textEditor = textEditor
@emitter = new Emitter
destroy: ->
@gutters = null
@emitter.dispose()
# Creates and returns a {Gutter}.
# * `options` An {Object} with the following fields:
# * `name` (required) A unique {String} to identify this gutter.
# * `priority` (optional) A {Number} that determines stacking order between
# gutters. Lower priority items are forced closer to the edges of the
# window. (default: -100)
# * `visible` (optional) {Boolean} specifying whether the gutter is visible
# initially after being created. (default: true)
addGutter: (options) ->
options = options ? {}
gutterName = options.name
if gutterName is null
throw new Error('A name is required to create a gutter.')
if @gutterWithName(gutterName)
throw new Error('Tried to create a gutter with a name that is already in use.')
newGutter = new Gutter(this, options)
inserted = false
# Insert the gutter into the gutters array, sorted in ascending order by 'priority'.
# This could be optimized, but there are unlikely to be many gutters.
for i in [0...@gutters.length]
if @gutters[i].priority >= newGutter.priority
@gutters.splice(i, 0, newGutter)
inserted = true
break
if not inserted
@gutters.push newGutter
@emitter.emit 'did-add-gutter', newGutter
return newGutter
getGutters: ->
@gutters.slice()
gutterWithName: (name) ->
for gutter in @gutters
if gutter.name is name then return gutter
null
###
Section: Event Subscription
###
# See {TextEditor::observeGutters} for details.
observeGutters: (callback) ->
callback(gutter) for gutter in @getGutters()
@onDidAddGutter callback
# See {TextEditor::onDidAddGutter} for details.
onDidAddGutter: (callback) ->
@emitter.on 'did-add-gutter', callback
# See {TextEditor::onDidRemoveGutter} for details.
onDidRemoveGutter: (callback) ->
@emitter.on 'did-remove-gutter', callback
###
Section: Private Methods
###
# Processes the destruction of the gutter. Throws an error if this gutter is
# not within this gutterContainer.
removeGutter: (gutter) ->
index = @gutters.indexOf(gutter)
if index > -1
@gutters.splice(index, 1)
@emitter.emit 'did-remove-gutter', gutter.name
else
throw new Error 'The given gutter cannot be removed because it is not ' +
'within this GutterContainer.'
# The public interface is Gutter::decorateMarker or TextEditor::decorateMarker.
addGutterDecoration: (gutter, marker, options) ->
if gutter.name is 'line-number'
options.type = 'line-number'
else
options.type = 'gutter'
options.gutterName = gutter.name
@textEditor.decorateMarker(marker, options)

69
src/gutter.coffee Normal file
View File

@@ -0,0 +1,69 @@
{Emitter} = require 'event-kit'
# Public: This class represents a gutter within a TextEditor.
DefaultPriority = -100
module.exports =
class Gutter
# * `gutterContainer` The {GutterContainer} object to which this gutter belongs.
# * `options` An {Object} with the following fields:
# * `name` (required) A unique {String} to identify this gutter.
# * `priority` (optional) A {Number} that determines stacking order between
# gutters. Lower priority items are forced closer to the edges of the
# window. (default: -100)
# * `visible` (optional) {Boolean} specifying whether the gutter is visible
# initially after being created. (default: true)
constructor: (gutterContainer, options) ->
@gutterContainer = gutterContainer
@name = options?.name
@priority = options?.priority ? DefaultPriority
@visible = options?.visible ? true
@emitter = new Emitter
destroy: ->
if @name is 'line-number'
throw new Error('The line-number gutter cannot be destroyed.')
else
@gutterContainer.removeGutter(this)
@emitter.emit 'did-destroy'
@emitter.dispose()
hide: ->
if @visible
@visible = false
@emitter.emit 'did-change-visible', this
show: ->
if not @visible
@visible = true
@emitter.emit 'did-change-visible', this
isVisible: ->
@visible
# * `marker` (required) A Marker object.
# * `options` (optional) An object with the following fields:
# * `class` (optional)
# * `item` (optional) A model {Object} with a corresponding view registered,
# or an {HTMLElement}.
decorateMarker: (marker, options) ->
@gutterContainer.addGutterDecoration(this, marker, options)
# Calls your `callback` when the {Gutter}'s' visibility changes.
#
# * `callback` {Function}
# * `gutter` The {Gutter} whose visibility changed.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeVisible: (callback) ->
@emitter.on 'did-change-visible', callback
# Calls your `callback` when the {Gutter} is destroyed
#
# * `callback` {Function}
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidDestroy: (callback) ->
@emitter.on 'did-destroy', callback

View File

@@ -12,13 +12,11 @@ class HighlightsComponent
@domNode = document.createElement('div')
@domNode.classList.add('highlights')
if atom.config.get('editor.useShadowDOM')
insertionPoint = document.createElement('content')
insertionPoint.setAttribute('select', '.underlayer')
@domNode.appendChild(insertionPoint)
getDomNode: ->
@domNode
updateSync: (state) ->
newState = state.content.highlights
newState = state.highlights
@oldState ?= {}
# remove highlights
@@ -39,6 +37,8 @@ class HighlightsComponent
@domNode.appendChild(highlightNode)
@updateHighlightNode(id, highlightState)
return
updateHighlightNode: (id, newHighlightState) ->
highlightNode = @highlightNodesById[id]
oldHighlightState = (@oldState[id] ?= {regions: [], flashCount: 0})
@@ -92,6 +92,8 @@ class HighlightsComponent
else
regionNode.style[property] = ''
return
flashHighlightNodeIfRequested: (id, newHighlightState) ->
oldHighlightState = @oldState[id]
return unless newHighlightState.flashCount > oldHighlightState.flashCount

View File

@@ -7,6 +7,9 @@ class InputComponent
@domNode.style['-webkit-transform'] = 'translateZ(0)'
@domNode.addEventListener 'paste', (event) -> event.preventDefault()
getDomNode: ->
@domNode
updateSync: (state) ->
@oldState ?= {}
newState = state.hiddenInput

View File

@@ -3,13 +3,23 @@ path = require 'path'
KeymapManager = require 'atom-keymap'
CSON = require 'season'
{jQuery} = require 'space-pen'
Grim = require 'grim'
bundledKeymaps = require('../package.json')?._atomKeymaps
KeymapManager::onDidLoadBundledKeymaps = (callback) ->
@emitter.on 'did-load-bundled-keymaps', callback
KeymapManager::loadBundledKeymaps = ->
@loadKeymap(path.join(@resourcePath, 'keymaps'))
@emit 'bundled-keymaps-loaded'
keymapsPath = path.join(@resourcePath, 'keymaps')
if bundledKeymaps?
for keymapName, keymap of bundledKeymaps
keymapPath = path.join(keymapsPath, keymapName)
@add(keymapPath, keymap)
else
@loadKeymap(keymapsPath)
@emit 'bundled-keymaps-loaded' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-load-bundled-keymaps'
KeymapManager::getUserKeymapPath = ->
@@ -50,7 +60,7 @@ KeymapManager::subscribeToFileReadFailure = ->
else
error.message
atom.notifications.addError(message, {detail: detail, dismissable: true})
atom.notifications.addError(message, {detail, dismissable: true})
# This enables command handlers registered via jQuery to call
# `.abortKeyBinding()` on the `jQuery.Event` object passed to the handler.

View File

@@ -1,14 +1,10 @@
{Range} = require 'text-buffer'
_ = require 'underscore-plus'
{OnigRegExp} = require 'oniguruma'
{Emitter, Subscriber} = require 'emissary'
ScopeDescriptor = require './scope-descriptor'
module.exports =
class LanguageMode
Emitter.includeInto(this)
Subscriber.includeInto(this)
# Sets up a `LanguageMode` for the given {TextEditor}.
#
# editor - The {TextEditor} to associate with
@@ -16,7 +12,6 @@ class LanguageMode
{@buffer} = @editor
destroy: ->
@unsubscribe()
toggleLineCommentForBufferRow: (row) ->
@toggleLineCommentsForBufferRows(row, row)
@@ -29,14 +24,8 @@ class LanguageMode
# endRow - The row {Number} to end at
toggleLineCommentsForBufferRows: (start, end) ->
scope = @editor.scopeDescriptorForBufferPosition([start, 0])
commentStartEntry = atom.config.getAll('editor.commentStart', {scope})[0]
return unless commentStartEntry?
commentEndEntry = _.find atom.config.getAll('editor.commentEnd', {scope}), (entry) ->
entry.scopeSelector is commentStartEntry.scopeSelector
commentStartString = commentStartEntry?.value
commentEndString = commentEndEntry?.value
{commentStartString, commentEndString} = @commentStartAndEndStringsForScope(scope)
return unless commentStartString?
buffer = @editor.buffer
commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?')
@@ -67,7 +56,7 @@ class LanguageMode
allBlank = true
allBlankOrCommented = true
for row in [start..end]
for row in [start..end] by 1
line = buffer.lineForRow(row)
blank = line?.match(/^\s*$/)
@@ -77,7 +66,7 @@ class LanguageMode
shouldUncomment = allBlankOrCommented and not allBlank
if shouldUncomment
for row in [start..end]
for row in [start..end] by 1
if match = commentStartRegex.searchSync(buffer.lineForRow(row))
columnStart = match[1].length
columnEnd = columnStart + match[2].length
@@ -90,7 +79,7 @@ class LanguageMode
indentString = @editor.buildIndentString(indent)
tabLength = @editor.getTabLength()
indentRegex = new RegExp("(\t|[ ]{#{tabLength}}){#{Math.floor(indent)}}")
for row in [start..end]
for row in [start..end] by 1
line = buffer.lineForRow(row)
if indentLength = line.match(indentRegex)?[0].length
buffer.insert([row, indentLength], commentStartString)
@@ -100,28 +89,31 @@ class LanguageMode
# Folds all the foldable lines in the buffer.
foldAll: ->
for currentRow in [0..@buffer.getLastRow()]
for currentRow in [0..@buffer.getLastRow()] by 1
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow?
@editor.createFold(startRow, endRow)
return
# Unfolds all the foldable lines in the buffer.
unfoldAll: ->
for row in [@buffer.getLastRow()..0]
for row in [@buffer.getLastRow()..0] by -1
fold.destroy() for fold in @editor.displayBuffer.foldsStartingAtBufferRow(row)
return
# Fold all comment and code blocks at a given indentLevel
#
# indentLevel - A {Number} indicating indentLevel; 0 based.
foldAllAtIndentLevel: (indentLevel) ->
@unfoldAll()
for currentRow in [0..@buffer.getLastRow()]
for currentRow in [0..@buffer.getLastRow()] by 1
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow?
# assumption: startRow will always be the min indent level for the entire range
if @editor.indentationForBufferRow(startRow) == indentLevel
if @editor.indentationForBufferRow(startRow) is indentLevel
@editor.createFold(startRow, endRow)
return
# Given a buffer row, creates a fold at it.
#
@@ -129,7 +121,7 @@ class LanguageMode
#
# Returns the new {Fold}.
foldBufferRow: (bufferRow) ->
for currentRow in [bufferRow..0]
for currentRow in [bufferRow..0] by -1
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow? and startRow <= bufferRow <= endRow
fold = @editor.displayBuffer.largestFoldStartingAtBufferRow(startRow)
@@ -153,13 +145,13 @@ class LanguageMode
endRow = bufferRow
if bufferRow > 0
for currentRow in [bufferRow-1..0]
for currentRow in [bufferRow-1..0] by -1
break if @buffer.isRowBlank(currentRow)
break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
startRow = currentRow
if bufferRow < @buffer.getLastRow()
for currentRow in [bufferRow+1..@buffer.getLastRow()]
for currentRow in [bufferRow+1..@buffer.getLastRow()] by 1
break if @buffer.isRowBlank(currentRow)
break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
endRow = currentRow
@@ -171,11 +163,11 @@ class LanguageMode
startIndentLevel = @editor.indentationForBufferRow(bufferRow)
scopeDescriptor = @editor.scopeDescriptorForBufferPosition([bufferRow, 0])
for row in [(bufferRow + 1)..@editor.getLastBufferRow()]
for row in [(bufferRow + 1)..@editor.getLastBufferRow()] by 1
continue if @editor.isBufferRowBlank(row)
indentation = @editor.indentationForBufferRow(row)
if indentation <= startIndentLevel
includeRowInFold = indentation == startIndentLevel and @foldEndRegexForScopeDescriptor(scopeDescriptor)?.searchSync(@editor.lineTextForBufferRow(row))
includeRowInFold = indentation is startIndentLevel and @foldEndRegexForScopeDescriptor(scopeDescriptor)?.searchSync(@editor.lineTextForBufferRow(row))
foldEndRow = row if includeRowInFold
break
@@ -192,11 +184,24 @@ class LanguageMode
return false unless 0 <= bufferRow <= @editor.getLastBufferRow()
@editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
# Find a row range for a 'paragraph' around specified bufferRow.
# Right now, a paragraph is a block of text bounded by and empty line or a
# block of text that is not the same type (comments next to source code).
# Find a row range for a 'paragraph' around specified bufferRow. A paragraph
# is a block of text bounded by and empty line or a block of text that is not
# the same type (comments next to source code).
rowRangeForParagraphAtBufferRow: (bufferRow) ->
return unless /\w/.test(@editor.lineTextForBufferRow(bufferRow))
scope = @editor.scopeDescriptorForBufferPosition([bufferRow, 0])
{commentStartString, commentEndString} = @commentStartAndEndStringsForScope(scope)
commentStartRegex = null
if commentStartString? and not commentEndString?
commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?')
commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})")
filterCommentStart = (line) ->
if commentStartRegex?
matches = commentStartRegex.searchSync(line)
line = line.substring(matches[0].end) if matches?.length
line
return unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(bufferRow)))
if @isLineCommentedAtBufferRow(bufferRow)
isOriginalRowComment = true
@@ -208,15 +213,15 @@ class LanguageMode
startRow = bufferRow
while startRow > firstRow
break if @isLineCommentedAtBufferRow(startRow - 1) != isOriginalRowComment
break unless /\w/.test(@editor.lineTextForBufferRow(startRow - 1))
break if @isLineCommentedAtBufferRow(startRow - 1) isnt isOriginalRowComment
break unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(startRow - 1)))
startRow--
endRow = bufferRow
lastRow = @editor.getLastBufferRow()
while endRow < lastRow
break if @isLineCommentedAtBufferRow(endRow + 1) != isOriginalRowComment
break unless /\w/.test(@editor.lineTextForBufferRow(endRow + 1))
break if @isLineCommentedAtBufferRow(endRow + 1) isnt isOriginalRowComment
break unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(endRow + 1)))
endRow++
new Range([startRow, 0], [endRow, @editor.lineTextForBufferRow(endRow).length])
@@ -237,8 +242,9 @@ class LanguageMode
@suggestedIndentForTokenizedLineAtBufferRow(bufferRow, tokenizedLine, options)
suggestedIndentForTokenizedLineAtBufferRow: (bufferRow, tokenizedLine, options) ->
scopes = tokenizedLine.tokens[0].scopes
scopeDescriptor = new ScopeDescriptor({scopes})
iterator = tokenizedLine.getTokenIterator()
iterator.next()
scopeDescriptor = new ScopeDescriptor(scopes: iterator.getScopes())
currentIndentLevel = @editor.indentationForBufferRow(bufferRow)
return currentIndentLevel unless increaseIndentRegex = @increaseIndentRegexForScopeDescriptor(scopeDescriptor)
@@ -255,7 +261,8 @@ class LanguageMode
desiredIndentLevel += 1 if increaseIndentRegex.testSync(precedingLine) and not @editor.isBufferRowCommented(precedingRow)
return desiredIndentLevel unless decreaseIndentRegex = @decreaseIndentRegexForScopeDescriptor(scopeDescriptor)
desiredIndentLevel -= 1 if decreaseIndentRegex.testSync(tokenizedLine.text)
line = @buffer.lineForRow(bufferRow)
desiredIndentLevel -= 1 if decreaseIndentRegex.testSync(line)
Math.max(desiredIndentLevel, 0)
@@ -266,7 +273,7 @@ class LanguageMode
#
# Returns a {Number} of the indent level of the block of lines.
minIndentLevelForRowRange: (startRow, endRow) ->
indents = (@editor.indentationForBufferRow(row) for row in [startRow..endRow] when not @editor.isBufferRowBlank(row))
indents = (@editor.indentationForBufferRow(row) for row in [startRow..endRow] by 1 when not @editor.isBufferRowBlank(row))
indents = [0] unless indents.length
Math.min(indents...)
@@ -275,7 +282,8 @@ class LanguageMode
# startRow - The row {Number} to start at
# endRow - The row {Number} to end at
autoIndentBufferRows: (startRow, endRow) ->
@autoIndentBufferRow(row) for row in [startRow..endRow]
@autoIndentBufferRow(row) for row in [startRow..endRow] by 1
return
# Given a buffer row, this indents it.
#
@@ -320,3 +328,11 @@ class LanguageMode
foldEndRegexForScopeDescriptor: (scopeDescriptor) ->
@getRegexForProperty(scopeDescriptor, 'editor.foldEndPattern')
commentStartAndEndStringsForScope: (scope) ->
commentStartEntry = atom.config.getAll('editor.commentStart', {scope})[0]
commentEndEntry = _.find atom.config.getAll('editor.commentEnd', {scope}), (entry) ->
entry.scopeSelector is commentStartEntry.scopeSelector
commentStartString = commentStartEntry?.value
commentEndString = commentEndEntry?.value
{commentStartString, commentEndString}

View File

@@ -0,0 +1,97 @@
TiledComponent = require './tiled-component'
LineNumbersTileComponent = require './line-numbers-tile-component'
WrapperDiv = document.createElement('div')
DummyLineNumberComponent = LineNumbersTileComponent.createDummy()
module.exports =
class LineNumberGutterComponent extends TiledComponent
dummyLineNumberNode: null
constructor: ({@onMouseDown, @editor, @gutter}) ->
@visible = true
@domNode = atom.views.getView(@gutter)
@lineNumbersNode = @domNode.firstChild
@lineNumbersNode.innerHTML = ''
@domNode.addEventListener 'click', @onClick
@domNode.addEventListener 'mousedown', @onMouseDown
destroy: ->
@domNode.removeEventListener 'click', @onClick
@domNode.removeEventListener 'mousedown', @onMouseDown
getDomNode: ->
@domNode
hideNode: ->
if @visible
@domNode.style.display = 'none'
@visible = false
showNode: ->
if not @visible
@domNode.style.removeProperty('display')
@visible = true
buildEmptyState: ->
{
tiles: {}
styles: {}
}
getNewState: (state) -> state
getTilesNode: -> @lineNumbersNode
beforeUpdateSync: (state) ->
@appendDummyLineNumber() unless @dummyLineNumberNode?
if @newState.styles.scrollHeight isnt @oldState.styles.scrollHeight
@lineNumbersNode.style.height = @newState.styles.scrollHeight + 'px'
@oldState.scrollHeight = @newState.scrollHeight
if @newState.styles.backgroundColor isnt @oldState.styles.backgroundColor
@lineNumbersNode.style.backgroundColor = @newState.styles.backgroundColor
@oldState.styles.backgroundColor = @newState.styles.backgroundColor
if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits
@updateDummyLineNumber()
@oldState.styles = {}
@oldState.maxLineNumberDigits = @newState.maxLineNumberDigits
buildComponentForTile: (id) -> new LineNumbersTileComponent({id})
###
Section: Private Methods
###
# This dummy line number element holds the gutter to the appropriate width,
# since the real line numbers are absolutely positioned for performance reasons.
appendDummyLineNumber: ->
DummyLineNumberComponent.newState = @newState
WrapperDiv.innerHTML = DummyLineNumberComponent.buildLineNumberHTML({bufferRow: -1})
@dummyLineNumberNode = WrapperDiv.children[0]
@lineNumbersNode.appendChild(@dummyLineNumberNode)
updateDummyLineNumber: ->
DummyLineNumberComponent.newState = @newState
@dummyLineNumberNode.innerHTML = DummyLineNumberComponent.buildLineNumberInnerHTML(0, false)
onMouseDown: (event) =>
{target} = event
lineNumber = target.parentNode
unless target.classList.contains('icon-right') and lineNumber.classList.contains('foldable')
@onMouseDown(event)
onClick: (event) =>
{target} = event
lineNumber = target.parentNode
if target.classList.contains('icon-right') and lineNumber.classList.contains('foldable')
bufferRow = parseInt(lineNumber.getAttribute('data-buffer-row'))
if lineNumber.classList.contains('folded')
@editor.unfoldBufferRow(bufferRow)
else
@editor.foldBufferRow(bufferRow)

View File

@@ -1,88 +1,87 @@
_ = require 'underscore-plus'
WrapperDiv = document.createElement('div')
module.exports =
class GutterComponent
dummyLineNumberNode: null
class LineNumbersTileComponent
@createDummy: ->
new LineNumbersTileComponent({id: -1})
constructor: ({@presenter, @onMouseDown, @editor}) ->
constructor: ({@id}) ->
@lineNumberNodesById = {}
@domNode = document.createElement("div")
@domNode.classList.add("tile")
@domNode.style.position = "absolute"
@domNode.style.display = "block"
@domNode.style.top = 0 # Cover the space occupied by a dummy lineNumber
@domNode = document.createElement('div')
@domNode.classList.add('gutter')
@lineNumbersNode = document.createElement('div')
@lineNumbersNode.classList.add('line-numbers')
@domNode.appendChild(@lineNumbersNode)
@domNode.addEventListener 'click', @onClick
@domNode.addEventListener 'mousedown', @onMouseDown
getDomNode: ->
@domNode
updateSync: (state) ->
@newState = state.gutter
@oldState ?= {lineNumbers: {}}
@newState = state
unless @oldState
@oldState = {tiles: {}, styles: {}}
@oldState.tiles[@id] = {lineNumbers: {}}
@appendDummyLineNumber() unless @dummyLineNumberNode?
@newTileState = @newState.tiles[@id]
@oldTileState = @oldState.tiles[@id]
if @newState.scrollHeight isnt @oldState.scrollHeight
@lineNumbersNode.style.height = @newState.scrollHeight + 'px'
@oldState.scrollHeight = @newState.scrollHeight
if @newTileState.display isnt @oldTileState.display
@domNode.style.display = @newTileState.display
@oldTileState.display = @newTileState.display
if @newState.scrollTop isnt @oldState.scrollTop
@lineNumbersNode.style['-webkit-transform'] = "translate3d(0px, #{-@newState.scrollTop}px, 0px)"
@oldState.scrollTop = @newState.scrollTop
if @newState.styles.backgroundColor isnt @oldState.styles.backgroundColor
@domNode.style.backgroundColor = @newState.styles.backgroundColor
@oldState.styles.backgroundColor = @newState.styles.backgroundColor
if @newState.backgroundColor isnt @oldState.backgroundColor
@lineNumbersNode.style.backgroundColor = @newState.backgroundColor
@oldState.backgroundColor = @newState.backgroundColor
if @newTileState.height isnt @oldTileState.height
@domNode.style.height = @newTileState.height + 'px'
@oldTileState.height = @newTileState.height
if @newTileState.top isnt @oldTileState.top
@domNode.style['-webkit-transform'] = "translate3d(0, #{@newTileState.top}px, 0px)"
@oldTileState.top = @newTileState.top
if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits
@updateDummyLineNumber()
node.remove() for id, node of @lineNumberNodesById
@oldState = {maxLineNumberDigits: @newState.maxLineNumberDigits, lineNumbers: {}}
@oldState.tiles[@id] = {lineNumbers: {}}
@oldTileState = @oldState.tiles[@id]
@lineNumberNodesById = {}
@oldState.maxLineNumberDigits = @newState.maxLineNumberDigits
@updateLineNumbers()
# This dummy line number element holds the gutter to the appropriate width,
# since the real line numbers are absolutely positioned for performance reasons.
appendDummyLineNumber: ->
WrapperDiv.innerHTML = @buildLineNumberHTML({bufferRow: -1})
@dummyLineNumberNode = WrapperDiv.children[0]
@lineNumbersNode.appendChild(@dummyLineNumberNode)
updateDummyLineNumber: ->
@dummyLineNumberNode.innerHTML = @buildLineNumberInnerHTML(0, false)
updateLineNumbers: ->
newLineNumberIds = null
newLineNumbersHTML = null
for id, lineNumberState of @newState.lineNumbers
if @oldState.lineNumbers.hasOwnProperty(id)
for id, lineNumberState of @oldTileState.lineNumbers
unless @newTileState.lineNumbers.hasOwnProperty(id)
@lineNumberNodesById[id].remove()
delete @lineNumberNodesById[id]
delete @oldTileState.lineNumbers[id]
for id, lineNumberState of @newTileState.lineNumbers
if @oldTileState.lineNumbers.hasOwnProperty(id)
@updateLineNumberNode(id, lineNumberState)
else
newLineNumberIds ?= []
newLineNumbersHTML ?= ""
newLineNumberIds.push(id)
newLineNumbersHTML += @buildLineNumberHTML(lineNumberState)
@oldState.lineNumbers[id] = _.clone(lineNumberState)
@oldTileState.lineNumbers[id] = _.clone(lineNumberState)
if newLineNumberIds?
WrapperDiv.innerHTML = newLineNumbersHTML
newLineNumberNodes = _.toArray(WrapperDiv.children)
node = @lineNumbersNode
node = @domNode
for id, i in newLineNumberIds
lineNumberNode = newLineNumberNodes[i]
@lineNumberNodesById[id] = lineNumberNode
node.appendChild(lineNumberNode)
for id, lineNumberState of @oldState.lineNumbers
unless @newState.lineNumbers.hasOwnProperty(id)
@lineNumberNodesById[id].remove()
delete @lineNumberNodesById[id]
delete @oldState.lineNumbers[id]
return
buildLineNumberHTML: (lineNumberState) ->
{screenRow, bufferRow, softWrapped, top, decorationClasses} = lineNumberState
@@ -108,7 +107,7 @@ class GutterComponent
padding + lineNumber + iconHTML
updateLineNumberNode: (lineNumberId, newLineNumberState) ->
oldLineNumberState = @oldState.lineNumbers[lineNumberId]
oldLineNumberState = @oldTileState.lineNumbers[lineNumberId]
node = @lineNumberNodesById[lineNumberId]
unless oldLineNumberState.foldable is newLineNumberState.foldable and _.isEqual(oldLineNumberState.decorationClasses, newLineNumberState.decorationClasses)
@@ -129,25 +128,7 @@ class GutterComponent
className
lineNumberNodeForScreenRow: (screenRow) ->
for id, lineNumberState of @oldState.lineNumbers
for id, lineNumberState of @oldTileState.lineNumbers
if lineNumberState.screenRow is screenRow
return @lineNumberNodesById[id]
null
onMouseDown: (event) =>
{target} = event
lineNumber = target.parentNode
unless target.classList.contains('icon-right') and lineNumber.classList.contains('foldable')
@onMouseDown(event)
onClick: (event) =>
{target} = event
lineNumber = target.parentNode
if target.classList.contains('icon-right') and lineNumber.classList.contains('foldable')
bufferRow = parseInt(lineNumber.getAttribute('data-buffer-row'))
if lineNumber.classList.contains('folded')
@editor.unfoldBufferRow(bufferRow)
else
@editor.foldBufferRow(bufferRow)

View File

@@ -1,69 +1,43 @@
_ = require 'underscore-plus'
{toArray} = require 'underscore-plus'
{$$} = require 'space-pen'
CursorsComponent = require './cursors-component'
HighlightsComponent = require './highlights-component'
OverlayManager = require './overlay-manager'
LinesTileComponent = require './lines-tile-component'
TiledComponent = require './tiled-component'
DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0]
AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
WrapperDiv = document.createElement('div')
cloneObject = (object) ->
clone = {}
clone[key] = value for key, value of object
clone
module.exports =
class LinesComponent
class LinesComponent extends TiledComponent
placeholderTextDiv: null
constructor: ({@presenter, @hostElement, @useShadowDOM, visible}) ->
@measuredLines = new Set
@lineNodesByLineId = {}
@screenRowsByLineId = {}
@lineIdsByScreenRow = {}
@renderedDecorationsByLineId = {}
@domNode = document.createElement('div')
@domNode.classList.add('lines')
@cursorsComponent = new CursorsComponent(@presenter)
@domNode.appendChild(@cursorsComponent.domNode)
@highlightsComponent = new HighlightsComponent(@presenter)
@domNode.appendChild(@highlightsComponent.domNode)
@cursorsComponent = new CursorsComponent
@domNode.appendChild(@cursorsComponent.getDomNode())
if @useShadowDOM
insertionPoint = document.createElement('content')
insertionPoint.setAttribute('select', '.overlayer')
@domNode.appendChild(insertionPoint)
insertionPoint = document.createElement('content')
insertionPoint.setAttribute('select', 'atom-overlay')
@overlayManager = new OverlayManager(@presenter, @hostElement)
@domNode.appendChild(insertionPoint)
else
@overlayManager = new OverlayManager(@presenter, @domNode)
getDomNode: ->
@domNode
updateSync: (state) ->
@newState = state.content
@oldState ?= {lines: {}}
shouldRecreateAllTilesOnUpdate: ->
@oldState.indentGuidesVisible isnt @newState.indentGuidesVisible
beforeUpdateSync: (state) ->
if @newState.scrollHeight isnt @oldState.scrollHeight
@domNode.style.height = @newState.scrollHeight + 'px'
@oldState.scrollHeight = @newState.scrollHeight
if @newState.scrollTop isnt @oldState.scrollTop or @newState.scrollLeft isnt @oldState.scrollLeft
@domNode.style['-webkit-transform'] = "translate3d(#{-@newState.scrollLeft}px, #{-@newState.scrollTop}px, 0px)"
@oldState.scrollTop = @newState.scrollTop
@oldState.scrollLeft = @newState.scrollLeft
if @newState.backgroundColor isnt @oldState.backgroundColor
@domNode.style.backgroundColor = @newState.backgroundColor
@oldState.backgroundColor = @newState.backgroundColor
afterUpdateSync: (state) ->
if @newState.placeholderText isnt @oldState.placeholderText
@placeholderTextDiv?.remove()
if @newState.placeholderText?
@@ -72,188 +46,23 @@ class LinesComponent
@placeholderTextDiv.textContent = @newState.placeholderText
@domNode.appendChild(@placeholderTextDiv)
@removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible
@updateLineNodes()
if @newState.scrollWidth isnt @oldState.scrollWidth
@domNode.style.width = @newState.scrollWidth + 'px'
@oldState.scrollWidth = @newState.scrollWidth
if @newState.width isnt @oldState.width
@domNode.style.width = @newState.width + 'px'
@oldState.width = @newState.width
@cursorsComponent.updateSync(state)
@highlightsComponent.updateSync(state)
@overlayManager?.render(state)
@oldState.indentGuidesVisible = @newState.indentGuidesVisible
@oldState.scrollWidth = @newState.scrollWidth
removeLineNodes: ->
@removeLineNode(id) for id of @oldState.lines
buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter})
removeLineNode: (id) ->
@lineNodesByLineId[id].remove()
delete @lineNodesByLineId[id]
delete @lineIdsByScreenRow[@screenRowsByLineId[id]]
delete @screenRowsByLineId[id]
delete @oldState.lines[id]
buildEmptyState: ->
{tiles: {}}
updateLineNodes: ->
for id of @oldState.lines
unless @newState.lines.hasOwnProperty(id)
@removeLineNode(id)
getNewState: (state) ->
state.content
newLineIds = null
newLinesHTML = null
for id, lineState of @newState.lines
if @oldState.lines.hasOwnProperty(id)
@updateLineNode(id)
else
newLineIds ?= []
newLinesHTML ?= ""
newLineIds.push(id)
newLinesHTML += @buildLineHTML(id)
@screenRowsByLineId[id] = lineState.screenRow
@lineIdsByScreenRow[lineState.screenRow] = id
@oldState.lines[id] = cloneObject(lineState)
return unless newLineIds?
WrapperDiv.innerHTML = newLinesHTML
newLineNodes = _.toArray(WrapperDiv.children)
for id, i in newLineIds
lineNode = newLineNodes[i]
@lineNodesByLineId[id] = lineNode
@domNode.appendChild(lineNode)
buildLineHTML: (id) ->
{scrollWidth} = @newState
{screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newState.lines[id]
classes = ''
if decorationClasses?
for decorationClass in decorationClasses
classes += decorationClass + ' '
classes += 'line'
lineHTML = "<div class=\"#{classes}\" style=\"position: absolute; top: #{top}px; width: #{scrollWidth}px;\" data-screen-row=\"#{screenRow}\">"
if text is ""
lineHTML += @buildEmptyLineInnerHTML(id)
else
lineHTML += @buildLineInnerHTML(id)
lineHTML += '<span class="fold-marker"></span>' if fold
lineHTML += "</div>"
lineHTML
buildEmptyLineInnerHTML: (id) ->
{indentGuidesVisible} = @newState
{indentLevel, tabLength, endOfLineInvisibles} = @newState.lines[id]
if indentGuidesVisible and indentLevel > 0
invisibleIndex = 0
lineHTML = ''
for i in [0...indentLevel]
lineHTML += "<span class='indent-guide'>"
for j in [0...tabLength]
if invisible = endOfLineInvisibles?[invisibleIndex++]
lineHTML += "<span class='invisible-character'>#{invisible}</span>"
else
lineHTML += ' '
lineHTML += "</span>"
while invisibleIndex < endOfLineInvisibles?.length
lineHTML += "<span class='invisible-character'>#{endOfLineInvisibles[invisibleIndex++]}</span>"
lineHTML
else
@buildEndOfLineHTML(id) or '&nbsp;'
buildLineInnerHTML: (id) ->
{indentGuidesVisible} = @newState
{tokens, text, isOnlyWhitespace} = @newState.lines[id]
innerHTML = ""
scopeStack = []
for token in tokens
innerHTML += @updateScopeStack(scopeStack, token.scopes)
hasIndentGuide = indentGuidesVisible and (token.hasLeadingWhitespace() or (token.hasTrailingWhitespace() and isOnlyWhitespace))
innerHTML += token.getValueAsHtml({hasIndentGuide})
innerHTML += @popScope(scopeStack) while scopeStack.length > 0
innerHTML += @buildEndOfLineHTML(id)
innerHTML
buildEndOfLineHTML: (id) ->
{endOfLineInvisibles} = @newState.lines[id]
html = ''
if endOfLineInvisibles?
for invisible in endOfLineInvisibles
html += "<span class='invisible-character'>#{invisible}</span>"
html
updateScopeStack: (scopeStack, desiredScopeDescriptor) ->
html = ""
# Find a common prefix
for scope, i in desiredScopeDescriptor
break unless scopeStack[i] is desiredScopeDescriptor[i]
# Pop scopeDescriptor until we're at the common prefx
until scopeStack.length is i
html += @popScope(scopeStack)
# Push onto common prefix until scopeStack equals desiredScopeDescriptor
for j in [i...desiredScopeDescriptor.length]
html += @pushScope(scopeStack, desiredScopeDescriptor[j])
html
popScope: (scopeStack) ->
scopeStack.pop()
"</span>"
pushScope: (scopeStack, scope) ->
scopeStack.push(scope)
"<span class=\"#{scope.replace(/\.+/g, ' ')}\">"
updateLineNode: (id) ->
oldLineState = @oldState.lines[id]
newLineState = @newState.lines[id]
lineNode = @lineNodesByLineId[id]
if @newState.scrollWidth isnt @oldState.scrollWidth
lineNode.style.width = @newState.scrollWidth + 'px'
newDecorationClasses = newLineState.decorationClasses
oldDecorationClasses = oldLineState.decorationClasses
if oldDecorationClasses?
for decorationClass in oldDecorationClasses
unless newDecorationClasses? and decorationClass in newDecorationClasses
lineNode.classList.remove(decorationClass)
if newDecorationClasses?
for decorationClass in newDecorationClasses
unless oldDecorationClasses? and decorationClass in oldDecorationClasses
lineNode.classList.add(decorationClass)
oldLineState.decorationClasses = newLineState.decorationClasses
if newLineState.top isnt oldLineState.top
lineNode.style.top = newLineState.top + 'px'
oldLineState.top = newLineState.cop
if newLineState.screenRow isnt oldLineState.screenRow
lineNode.dataset.screenRow = newLineState.screenRow
oldLineState.screenRow = newLineState.screenRow
@lineIdsByScreenRow[newLineState.screenRow] = id
lineNodeForScreenRow: (screenRow) ->
@lineNodesByLineId[@lineIdsByScreenRow[screenRow]]
getTilesNode: -> @domNode
measureLineHeightAndDefaultCharWidth: ->
@domNode.appendChild(DummyLineNode)
@@ -272,56 +81,13 @@ class LinesComponent
measureCharactersInNewLines: ->
@presenter.batchCharacterMeasurement =>
for id, lineState of @oldState.lines
unless @measuredLines.has(id)
lineNode = @lineNodesByLineId[id]
@measureCharactersInLine(id, lineState, lineNode)
for id, component of @componentsByTileId
component.measureCharactersInNewLines()
return
measureCharactersInLine: (lineId, tokenizedLine, lineNode) ->
rangeForMeasurement = null
iterator = null
charIndex = 0
for {value, scopes, hasPairedCharacter} in tokenizedLine.tokens
charWidths = @presenter.getScopedCharacterWidths(scopes)
valueIndex = 0
while valueIndex < value.length
if hasPairedCharacter
char = value.substr(valueIndex, 2)
charLength = 2
valueIndex += 2
else
char = value[valueIndex]
charLength = 1
valueIndex++
continue if char is '\0'
unless charWidths[char]?
unless textNode?
rangeForMeasurement ?= document.createRange()
iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter)
textNode = iterator.nextNode()
textNodeIndex = 0
nextTextNodeIndex = textNode.textContent.length
while nextTextNodeIndex <= charIndex
textNode = iterator.nextNode()
textNodeIndex = nextTextNodeIndex
nextTextNodeIndex = textNodeIndex + textNode.textContent.length
i = charIndex - textNodeIndex
rangeForMeasurement.setStart(textNode, i)
rangeForMeasurement.setEnd(textNode, i + charLength)
charWidth = rangeForMeasurement.getBoundingClientRect().width
@presenter.setScopedCharacterWidth(scopes, char, charWidth)
charIndex += charLength
@measuredLines.add(lineId)
clearScopedCharWidths: ->
@measuredLines.clear()
for id, component of @componentsByTileId
component.clearMeasurements()
@presenter.clearScopedCharacterWidths()

View File

@@ -0,0 +1,368 @@
_ = require 'underscore-plus'
HighlightsComponent = require './highlights-component'
TokenIterator = require './token-iterator'
AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
WrapperDiv = document.createElement('div')
TokenTextEscapeRegex = /[&"'<>]/g
MaxTokenLength = 20000
cloneObject = (object) ->
clone = {}
clone[key] = value for key, value of object
clone
module.exports =
class LinesTileComponent
constructor: ({@presenter, @id}) ->
@tokenIterator = new TokenIterator
@measuredLines = new Set
@lineNodesByLineId = {}
@screenRowsByLineId = {}
@lineIdsByScreenRow = {}
@domNode = document.createElement("div")
@domNode.classList.add("tile")
@domNode.style.position = "absolute"
@domNode.style.display = "block"
@highlightsComponent = new HighlightsComponent
@domNode.appendChild(@highlightsComponent.getDomNode())
getDomNode: ->
@domNode
updateSync: (state) ->
@newState = state
unless @oldState
@oldState = {tiles: {}}
@oldState.tiles[@id] = {lines: {}}
@newTileState = @newState.tiles[@id]
@oldTileState = @oldState.tiles[@id]
if @newState.backgroundColor isnt @oldState.backgroundColor
@domNode.style.backgroundColor = @newState.backgroundColor
@oldState.backgroundColor = @newState.backgroundColor
if @newTileState.display isnt @oldTileState.display
@domNode.style.display = @newTileState.display
@oldTileState.display = @newTileState.display
if @newTileState.height isnt @oldTileState.height
@domNode.style.height = @newTileState.height + 'px'
@oldTileState.height = @newTileState.height
if @newState.width isnt @oldState.width
@domNode.style.width = @newState.width + 'px'
@oldTileState.width = @newTileState.width
if @newTileState.top isnt @oldTileState.top or @newTileState.left isnt @oldTileState.left
@domNode.style['-webkit-transform'] = "translate3d(#{@newTileState.left}px, #{@newTileState.top}px, 0px)"
@oldTileState.top = @newTileState.top
@oldTileState.left = @newTileState.left
@removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible
@updateLineNodes()
@highlightsComponent.updateSync(@newTileState)
@oldState.indentGuidesVisible = @newState.indentGuidesVisible
removeLineNodes: ->
@removeLineNode(id) for id of @oldTileState.lines
return
removeLineNode: (id) ->
@lineNodesByLineId[id].remove()
delete @lineNodesByLineId[id]
delete @lineIdsByScreenRow[@screenRowsByLineId[id]]
delete @screenRowsByLineId[id]
delete @oldTileState.lines[id]
updateLineNodes: ->
for id of @oldTileState.lines
unless @newTileState.lines.hasOwnProperty(id)
@removeLineNode(id)
newLineIds = null
newLinesHTML = null
for id, lineState of @newTileState.lines
if @oldTileState.lines.hasOwnProperty(id)
@updateLineNode(id)
else
newLineIds ?= []
newLinesHTML ?= ""
newLineIds.push(id)
newLinesHTML += @buildLineHTML(id)
@screenRowsByLineId[id] = lineState.screenRow
@lineIdsByScreenRow[lineState.screenRow] = id
@oldTileState.lines[id] = cloneObject(lineState)
return unless newLineIds?
WrapperDiv.innerHTML = newLinesHTML
newLineNodes = _.toArray(WrapperDiv.children)
for id, i in newLineIds
lineNode = newLineNodes[i]
@lineNodesByLineId[id] = lineNode
@domNode.appendChild(lineNode)
return
buildLineHTML: (id) ->
{width} = @newState
{screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newTileState.lines[id]
classes = ''
if decorationClasses?
for decorationClass in decorationClasses
classes += decorationClass + ' '
classes += 'line'
lineHTML = "<div class=\"#{classes}\" style=\"position: absolute; top: #{top}px; width: #{width}px;\" data-screen-row=\"#{screenRow}\">"
if text is ""
lineHTML += @buildEmptyLineInnerHTML(id)
else
lineHTML += @buildLineInnerHTML(id)
lineHTML += '<span class="fold-marker"></span>' if fold
lineHTML += "</div>"
lineHTML
buildEmptyLineInnerHTML: (id) ->
{indentGuidesVisible} = @newState
{indentLevel, tabLength, endOfLineInvisibles} = @newTileState.lines[id]
if indentGuidesVisible and indentLevel > 0
invisibleIndex = 0
lineHTML = ''
for i in [0...indentLevel]
lineHTML += "<span class='indent-guide'>"
for j in [0...tabLength]
if invisible = endOfLineInvisibles?[invisibleIndex++]
lineHTML += "<span class='invisible-character'>#{invisible}</span>"
else
lineHTML += ' '
lineHTML += "</span>"
while invisibleIndex < endOfLineInvisibles?.length
lineHTML += "<span class='invisible-character'>#{endOfLineInvisibles[invisibleIndex++]}</span>"
lineHTML
else
@buildEndOfLineHTML(id) or '&nbsp;'
buildLineInnerHTML: (id) ->
lineState = @newTileState.lines[id]
{firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, invisibles} = lineState
lineIsWhitespaceOnly = firstTrailingWhitespaceIndex is 0
innerHTML = ""
@tokenIterator.reset(lineState)
while @tokenIterator.next()
for scope in @tokenIterator.getScopeEnds()
innerHTML += "</span>"
for scope in @tokenIterator.getScopeStarts()
innerHTML += "<span class=\"#{scope.replace(/\.+/g, ' ')}\">"
tokenStart = @tokenIterator.getScreenStart()
tokenEnd = @tokenIterator.getScreenEnd()
tokenText = @tokenIterator.getText()
isHardTab = @tokenIterator.isHardTab()
if hasLeadingWhitespace = tokenStart < firstNonWhitespaceIndex
tokenFirstNonWhitespaceIndex = firstNonWhitespaceIndex - tokenStart
else
tokenFirstNonWhitespaceIndex = null
if hasTrailingWhitespace = tokenEnd > firstTrailingWhitespaceIndex
tokenFirstTrailingWhitespaceIndex = Math.max(0, firstTrailingWhitespaceIndex - tokenStart)
else
tokenFirstTrailingWhitespaceIndex = null
hasIndentGuide =
@newState.indentGuidesVisible and
(hasLeadingWhitespace or lineIsWhitespaceOnly)
hasInvisibleCharacters =
(invisibles?.tab and isHardTab) or
(invisibles?.space and (hasLeadingWhitespace or hasTrailingWhitespace))
innerHTML += @buildTokenHTML(tokenText, isHardTab, tokenFirstNonWhitespaceIndex, tokenFirstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters)
for scope in @tokenIterator.getScopeEnds()
innerHTML += "</span>"
for scope in @tokenIterator.getScopes()
innerHTML += "</span>"
innerHTML += @buildEndOfLineHTML(id)
innerHTML
buildTokenHTML: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters) ->
if isHardTab
classes = 'hard-tab'
classes += ' leading-whitespace' if firstNonWhitespaceIndex?
classes += ' trailing-whitespace' if firstTrailingWhitespaceIndex?
classes += ' indent-guide' if hasIndentGuide
classes += ' invisible-character' if hasInvisibleCharacters
return "<span class='#{classes}'>#{@escapeTokenText(tokenText)}</span>"
else
startIndex = 0
endIndex = tokenText.length
leadingHtml = ''
trailingHtml = ''
if firstNonWhitespaceIndex?
leadingWhitespace = tokenText.substring(0, firstNonWhitespaceIndex)
classes = 'leading-whitespace'
classes += ' indent-guide' if hasIndentGuide
classes += ' invisible-character' if hasInvisibleCharacters
leadingHtml = "<span class='#{classes}'>#{leadingWhitespace}</span>"
startIndex = firstNonWhitespaceIndex
if firstTrailingWhitespaceIndex?
tokenIsOnlyWhitespace = firstTrailingWhitespaceIndex is 0
trailingWhitespace = tokenText.substring(firstTrailingWhitespaceIndex)
classes = 'trailing-whitespace'
classes += ' indent-guide' if hasIndentGuide and not firstNonWhitespaceIndex? and tokenIsOnlyWhitespace
classes += ' invisible-character' if hasInvisibleCharacters
trailingHtml = "<span class='#{classes}'>#{trailingWhitespace}</span>"
endIndex = firstTrailingWhitespaceIndex
html = leadingHtml
if tokenText.length > MaxTokenLength
while startIndex < endIndex
html += "<span>" + @escapeTokenText(tokenText, startIndex, startIndex + MaxTokenLength) + "</span>"
startIndex += MaxTokenLength
else
html += @escapeTokenText(tokenText, startIndex, endIndex)
html += trailingHtml
html
escapeTokenText: (tokenText, startIndex, endIndex) ->
if startIndex? and endIndex? and startIndex > 0 or endIndex < tokenText.length
tokenText = tokenText.slice(startIndex, endIndex)
tokenText.replace(TokenTextEscapeRegex, @escapeTokenTextReplace)
escapeTokenTextReplace: (match) ->
switch match
when '&' then '&amp;'
when '"' then '&quot;'
when "'" then '&#39;'
when '<' then '&lt;'
when '>' then '&gt;'
else match
buildEndOfLineHTML: (id) ->
{endOfLineInvisibles} = @newTileState.lines[id]
html = ''
if endOfLineInvisibles?
for invisible in endOfLineInvisibles
html += "<span class='invisible-character'>#{invisible}</span>"
html
updateLineNode: (id) ->
oldLineState = @oldTileState.lines[id]
newLineState = @newTileState.lines[id]
lineNode = @lineNodesByLineId[id]
if @newState.width isnt @oldState.width
lineNode.style.width = @newState.width + 'px'
newDecorationClasses = newLineState.decorationClasses
oldDecorationClasses = oldLineState.decorationClasses
if oldDecorationClasses?
for decorationClass in oldDecorationClasses
unless newDecorationClasses? and decorationClass in newDecorationClasses
lineNode.classList.remove(decorationClass)
if newDecorationClasses?
for decorationClass in newDecorationClasses
unless oldDecorationClasses? and decorationClass in oldDecorationClasses
lineNode.classList.add(decorationClass)
oldLineState.decorationClasses = newLineState.decorationClasses
if newLineState.top isnt oldLineState.top
lineNode.style.top = newLineState.top + 'px'
oldLineState.top = newLineState.top
if newLineState.screenRow isnt oldLineState.screenRow
lineNode.dataset.screenRow = newLineState.screenRow
oldLineState.screenRow = newLineState.screenRow
@lineIdsByScreenRow[newLineState.screenRow] = id
lineNodeForScreenRow: (screenRow) ->
@lineNodesByLineId[@lineIdsByScreenRow[screenRow]]
measureCharactersInNewLines: ->
for id, lineState of @oldTileState.lines
unless @measuredLines.has(id)
lineNode = @lineNodesByLineId[id]
@measureCharactersInLine(id, lineState, lineNode)
return
measureCharactersInLine: (lineId, tokenizedLine, lineNode) ->
rangeForMeasurement = null
iterator = null
charIndex = 0
@tokenIterator.reset(tokenizedLine)
while @tokenIterator.next()
scopes = @tokenIterator.getScopes()
text = @tokenIterator.getText()
charWidths = @presenter.getScopedCharacterWidths(scopes)
textIndex = 0
while textIndex < text.length
if @tokenIterator.isPairedCharacter()
char = text
charLength = 2
textIndex += 2
else
char = text[textIndex]
charLength = 1
textIndex++
continue if char is '\0'
unless charWidths[char]?
unless textNode?
rangeForMeasurement ?= document.createRange()
iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter)
textNode = iterator.nextNode()
textNodeIndex = 0
nextTextNodeIndex = textNode.textContent.length
while nextTextNodeIndex <= charIndex
textNode = iterator.nextNode()
textNodeIndex = nextTextNodeIndex
nextTextNodeIndex = textNodeIndex + textNode.textContent.length
i = charIndex - textNodeIndex
rangeForMeasurement.setStart(textNode, i)
rangeForMeasurement.setEnd(textNode, i + charLength)
charWidth = rangeForMeasurement.getBoundingClientRect().width
@presenter.setScopedCharacterWidth(scopes, char, charWidth)
charIndex += charLength
@measuredLines.add(lineId)
clearMeasurements: ->
@measuredLines.clear()

View File

@@ -0,0 +1,12 @@
module.exports =
class MarkerObservationWindow
constructor: (@displayBuffer, @bufferWindow) ->
setScreenRange: (range) ->
@bufferWindow.setRange(@displayBuffer.bufferRangeForScreenRange(range))
setBufferRange: (range) ->
@bufferWindow.setRange(range)
destroy: ->
@bufferWindow.destroy()

View File

@@ -1,8 +1,5 @@
{Range} = require 'text-buffer'
_ = require 'underscore-plus'
{Subscriber} = require 'emissary'
EmitterMixin = require('emissary').Emitter
{Emitter} = require 'event-kit'
{CompositeDisposable, Emitter} = require 'event-kit'
Grim = require 'grim'
# Essential: Represents a buffer annotation that remains logically stationary
@@ -45,16 +42,13 @@ Grim = require 'grim'
# See {TextEditor::markBufferRange} for usage.
module.exports =
class Marker
EmitterMixin.includeInto(this)
Subscriber.includeInto(this)
bufferMarkerSubscription: null
oldHeadBufferPosition: null
oldHeadScreenPosition: null
oldTailBufferPosition: null
oldTailScreenPosition: null
wasValid: true
deferredChangeEvents: null
hasChangeObservers: false
###
Section: Construction and Destruction
@@ -62,21 +56,16 @@ class Marker
constructor: ({@bufferMarker, @displayBuffer}) ->
@emitter = new Emitter
@disposables = new CompositeDisposable
@id = @bufferMarker.id
@oldHeadBufferPosition = @getHeadBufferPosition()
@oldHeadScreenPosition = @getHeadScreenPosition()
@oldTailBufferPosition = @getTailBufferPosition()
@oldTailScreenPosition = @getTailScreenPosition()
@wasValid = @isValid()
@subscribe @bufferMarker.onDidDestroy => @destroyed()
@subscribe @bufferMarker.onDidChange (event) => @notifyObservers(event)
@disposables.add @bufferMarker.onDidDestroy => @destroyed()
# Essential: Destroys the marker, causing it to emit the 'destroyed' event. Once
# destroyed, a marker cannot be restored by undo/redo operations.
destroy: ->
@bufferMarker.destroy()
@unsubscribe()
@disposables.dispose()
# Essential: Creates and returns a new {Marker} with the same properties as this
# marker.
@@ -108,6 +97,14 @@ class Marker
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChange: (callback) ->
unless @hasChangeObservers
@oldHeadBufferPosition = @getHeadBufferPosition()
@oldHeadScreenPosition = @getHeadScreenPosition()
@oldTailBufferPosition = @getTailBufferPosition()
@oldTailScreenPosition = @getTailScreenPosition()
@wasValid = @isValid()
@disposables.add @bufferMarker.onDidChange (event) => @notifyObservers(event)
@hasChangeObservers = true
@emitter.on 'did-change', callback
# Essential: Invoke the given callback when the marker is destroyed.
@@ -118,15 +115,6 @@ class Marker
onDidDestroy: (callback) ->
@emitter.on 'did-destroy', callback
on: (eventName) ->
switch eventName
when 'changed'
Grim.deprecate("Use Marker::onDidChange instead")
when 'destroyed'
Grim.deprecate("Use Marker::onDidDestroy instead")
EmitterMixin::on.apply(this, arguments)
###
Section: Marker Details
###
@@ -159,9 +147,6 @@ class Marker
# the marker.
getProperties: ->
@bufferMarker.getProperties()
getAttributes: ->
Grim.deprecate 'Use Marker::getProperties instead'
@getProperties()
# Essential: Merges an {Object} containing new properties into the marker's
# existing properties.
@@ -169,16 +154,10 @@ class Marker
# * `properties` {Object}
setProperties: (properties) ->
@bufferMarker.setProperties(properties)
setAttributes: (properties) ->
Grim.deprecate 'Use Marker::getProperties instead'
@setProperties(properties)
matchesProperties: (attributes) ->
attributes = @displayBuffer.translateToBufferMarkerParams(attributes)
@bufferMarker.matchesParams(attributes)
matchesAttributes: (attributes) ->
Grim.deprecate 'Use Marker::matchesProperties instead'
@matchesProperties(attributes)
###
Section: Comparing to other markers
@@ -284,7 +263,6 @@ class Marker
# * `screenPosition` The new {Point} to use
# * `properties` (optional) {Object} properties to associate with the marker.
setHeadScreenPosition: (screenPosition, properties) ->
screenPosition = @displayBuffer.clipScreenPosition(screenPosition, properties)
@setHeadBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, properties))
# Extended: Retrieves the buffer position of the marker's tail.
@@ -311,7 +289,6 @@ class Marker
# * `screenPosition` The new {Point} to use
# * `properties` (optional) {Object} properties to associate with the marker.
setTailScreenPosition: (screenPosition, options) ->
screenPosition = @displayBuffer.clipScreenPosition(screenPosition, options)
@setTailBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, options))
# Extended: Returns a {Boolean} indicating whether the marker has a tail.
@@ -344,7 +321,7 @@ class Marker
destroyed: ->
delete @displayBuffer.markers[@id]
@emit 'destroyed'
@emit 'destroyed' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-destroy'
@emitter.dispose()
@@ -357,11 +334,11 @@ class Marker
newTailScreenPosition = @getTailScreenPosition()
isValid = @isValid()
return if _.isEqual(isValid, @wasValid) and
_.isEqual(newHeadBufferPosition, @oldHeadBufferPosition) and
_.isEqual(newHeadScreenPosition, @oldHeadScreenPosition) and
_.isEqual(newTailBufferPosition, @oldTailBufferPosition) and
_.isEqual(newTailScreenPosition, @oldTailScreenPosition)
return if isValid is @wasValid and
newHeadBufferPosition.isEqual(@oldHeadBufferPosition) and
newHeadScreenPosition.isEqual(@oldHeadScreenPosition) and
newTailBufferPosition.isEqual(@oldTailBufferPosition) and
newTailScreenPosition.isEqual(@oldTailScreenPosition)
changeEvent = {
@oldHeadScreenPosition, newHeadScreenPosition,
@@ -372,28 +349,41 @@ class Marker
isValid
}
if @deferredChangeEvents?
@deferredChangeEvents.push(changeEvent)
else
@emit 'changed', changeEvent
@emitter.emit 'did-change', changeEvent
@oldHeadBufferPosition = newHeadBufferPosition
@oldHeadScreenPosition = newHeadScreenPosition
@oldTailBufferPosition = newTailBufferPosition
@oldTailScreenPosition = newTailScreenPosition
@wasValid = isValid
pauseChangeEvents: ->
@deferredChangeEvents = []
resumeChangeEvents: ->
if deferredChangeEvents = @deferredChangeEvents
@deferredChangeEvents = null
for event in deferredChangeEvents
@emit 'changed', event
@emitter.emit 'did-change', event
@emit 'changed', changeEvent if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change', changeEvent
getPixelRange: ->
@displayBuffer.pixelRangeForScreenRange(@getScreenRange(), false)
if Grim.includeDeprecatedAPIs
EmitterMixin = require('emissary').Emitter
EmitterMixin.includeInto(Marker)
Marker::on = (eventName) ->
switch eventName
when 'changed'
Grim.deprecate("Use Marker::onDidChange instead")
when 'destroyed'
Grim.deprecate("Use Marker::onDidDestroy instead")
else
Grim.deprecate("Marker::on is deprecated. Use documented event subscription methods instead.")
EmitterMixin::on.apply(this, arguments)
Marker::getAttributes = ->
Grim.deprecate 'Use Marker::getProperties instead'
@getProperties()
Marker::setAttributes = (properties) ->
Grim.deprecate 'Use Marker::setProperties instead'
@setProperties(properties)
Marker::matchesAttributes = (attributes) ->
Grim.deprecate 'Use Marker::matchesProperties instead'
@matchesProperties(attributes)

View File

@@ -17,6 +17,8 @@ merge = (menu, item, itemSpecificity=Infinity) ->
else unless item.type is 'separator' and _.last(menu)?.type is 'separator'
menu.push(item)
return
unmerge = (menu, item) ->
matchingItemIndex = findMatchingItemIndex(menu, item)
matchingItem = menu[matchingItemIndex] unless matchingItemIndex is - 1

View File

@@ -8,6 +8,8 @@ fs = require 'fs-plus'
MenuHelpers = require './menu-helpers'
platformMenu = require('../package.json')?._atomMenu?.menu
# Extended: Provides a registry for menu items that you'd like to appear in the
# application menu.
#
@@ -61,6 +63,7 @@ class MenuManager
@pendingUpdateOperation = null
@template = []
atom.keymaps.onDidLoadBundledKeymaps => @loadPlatformItems()
atom.keymaps.onDidReloadKeymap => @update()
atom.packages.onDidActivateInitialPackages => @sortPackagesMenu()
# Public: Adds the given items to the application menu.
@@ -137,17 +140,29 @@ class MenuManager
update: ->
clearImmediate(@pendingUpdateOperation) if @pendingUpdateOperation?
@pendingUpdateOperation = setImmediate =>
keystrokesByCommand = {}
includedBindings = []
unsetKeystrokes = new Set
for binding in atom.keymaps.getKeyBindings() when @includeSelector(binding.selector)
includedBindings.push(binding)
if binding.command is 'unset!'
unsetKeystrokes.add(binding.keystrokes)
keystrokesByCommand = {}
for binding in includedBindings when not unsetKeystrokes.has(binding.keystrokes)
keystrokesByCommand[binding.command] ?= []
keystrokesByCommand[binding.command].unshift binding.keystrokes
@sendToBrowserProcess(@template, keystrokesByCommand)
loadPlatformItems: ->
menusDirPath = path.join(@resourcePath, 'menus')
platformMenuPath = fs.resolve(menusDirPath, process.platform, ['cson', 'json'])
{menu} = CSON.readFileSync(platformMenuPath)
@add(menu)
if platformMenu?
@add(platformMenu)
else
menusDirPath = path.join(@resourcePath, 'menus')
platformMenuPath = fs.resolve(menusDirPath, process.platform, ['cson', 'json'])
{menu} = CSON.readFileSync(platformMenuPath)
@add(menu)
# Merges an item in a submenu aware way such that new items are always
# appended to the bottom of existing menus where possible.
@@ -164,7 +179,7 @@ class MenuManager
filtered = {}
for key, bindings of keystrokesByCommand
for binding in bindings
continue if binding.indexOf(' ') != -1
continue if binding.indexOf(' ') isnt -1
filtered[key] ?= []
filtered[key].push(binding)

34
src/model.coffee Normal file
View File

@@ -0,0 +1,34 @@
Grim = require 'grim'
if Grim.includeDeprecatedAPIs
module.exports = require('theorist').Model
return
PropertyAccessors = require 'property-accessors'
nextInstanceId = 1
module.exports =
class Model
PropertyAccessors.includeInto(this)
@resetNextInstanceId: -> nextInstanceId = 1
alive: true
constructor: (params) ->
@assignId(params?.id)
assignId: (id) ->
@id ?= id ? nextInstanceId++
@::advisedAccessor 'id',
set: (id) -> nextInstanceId = id + 1 if id >= nextInstanceId
destroy: ->
return unless @isAlive()
@alive = false
@destroyed?()
isAlive: -> @alive
isDestroyed: -> not @isAlive()

View File

@@ -200,7 +200,7 @@ registerBuiltins = (devMode) ->
cache.builtins.atom = atomCoffeePath if fs.isFileSync(atomCoffeePath)
cache.builtins.atom ?= path.join(cache.resourcePath, 'exports', 'atom.js')
atomShellRoot = path.join(process.resourcesPath, 'atom')
atomShellRoot = path.join(process.resourcesPath, 'atom.asar')
commonRoot = path.join(atomShellRoot, 'common', 'api', 'lib')
commonBuiltins = ['callbacks-registry', 'clipboard', 'crash-reporter', 'screen', 'shell']

View File

@@ -1,8 +1,8 @@
{Emitter, Disposable} = require 'event-kit'
Notification = require '../src/notification'
# Experimental: Allows messaging the user. This will likely change, dont use
# quite yet!
# Public: A notification manager used to create {Notification}s to be shown
# to the user.
module.exports =
class NotificationManager
constructor: ->
@@ -13,6 +13,12 @@ class NotificationManager
Section: Events
###
# Public: Invoke the given callback after a notification has been added.
#
# * `callback` {Function} to be called after the notification is added.
# * `notification` The {Notification} that was added.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidAddNotification: (callback) ->
@emitter.on 'did-add-notification', callback
@@ -20,18 +26,43 @@ class NotificationManager
Section: Adding Notifications
###
# Public: Add a success notification.
#
# * `message` A {String} message
# * `options` An options {Object} with optional keys such as:
# * `detail` A {String} with additional details about the notification
addSuccess: (message, options) ->
@addNotification(new Notification('success', message, options))
# Public: Add an informational notification.
#
# * `message` A {String} message
# * `options` An options {Object} with optional keys such as:
# * `detail` A {String} with additional details about the notification
addInfo: (message, options) ->
@addNotification(new Notification('info', message, options))
# Public: Add a warning notification.
#
# * `message` A {String} message
# * `options` An options {Object} with optional keys such as:
# * `detail` A {String} with additional details about the notification
addWarning: (message, options) ->
@addNotification(new Notification('warning', message, options))
# Public: Add an error notification.
#
# * `message` A {String} message
# * `options` An options {Object} with optional keys such as:
# * `detail` A {String} with additional details about the notification
addError: (message, options) ->
@addNotification(new Notification('error', message, options))
# Public: Add a fatal error notification.
#
# * `message` A {String} message
# * `options` An options {Object} with optional keys such as:
# * `detail` A {String} with additional details about the notification
addFatalError: (message, options) ->
@addNotification(new Notification('fatal', message, options))
@@ -47,7 +78,10 @@ class NotificationManager
Section: Getting Notifications
###
getNotifications: -> @notifications
# Public: Get all the notifications.
#
# Returns an {Array} of {Notifications}s.
getNotifications: -> @notifications.slice()
###
Section: Managing Notifications

View File

@@ -1,6 +1,6 @@
{Emitter} = require 'event-kit'
# Experimental: This will likely change, do not use.
# Public: A notification to the user containing a message and type.
module.exports =
class Notification
constructor: (@type, @message, @options={}) ->
@@ -18,8 +18,10 @@ class Notification
getOptions: -> @options
# Public: Retrieves the {String} type.
getType: -> @type
# Public: Retrieves the {String} message.
getMessage: -> @message
getTimestamp: -> @timestamp
@@ -27,9 +29,9 @@ class Notification
getDetail: -> @options.detail
isEqual: (other) ->
@getMessage() == other.getMessage() \
and @getType() == other.getType() \
and @getDetail() == other.getDetail()
@getMessage() is other.getMessage() \
and @getType() is other.getType() \
and @getDetail() is other.getDetail()
dismiss: ->
return unless @isDismissable() and not @isDismissed()

View File

@@ -1,39 +1,44 @@
module.exports =
class OverlayManager
constructor: (@presenter, @container) ->
@overlayNodesById = {}
@overlaysById = {}
render: (state) ->
for decorationId, {pixelPosition, item} of state.content.overlays
@renderOverlay(state, decorationId, item, pixelPosition)
for decorationId, overlay of state.content.overlays
if @shouldUpdateOverlay(decorationId, overlay)
@renderOverlay(state, decorationId, overlay)
for id, overlayNode of @overlayNodesById
for id, {overlayNode} of @overlaysById
unless state.content.overlays.hasOwnProperty(id)
delete @overlayNodesById[id]
delete @overlaysById[id]
overlayNode.remove()
return
shouldUpdateOverlay: (decorationId, overlay) ->
cachedOverlay = @overlaysById[decorationId]
return true unless cachedOverlay?
cachedOverlay.pixelPosition?.top isnt overlay.pixelPosition?.top or
cachedOverlay.pixelPosition?.left isnt overlay.pixelPosition?.left
renderOverlay: (state, decorationId, item, pixelPosition) ->
item = atom.views.getView(item)
unless overlayNode = @overlayNodesById[decorationId]
overlayNode = @overlayNodesById[decorationId] = document.createElement('atom-overlay')
overlayNode.appendChild(item)
measureOverlays: ->
for decorationId, {itemView} of @overlaysById
@measureOverlay(decorationId, itemView)
measureOverlay: (decorationId, itemView) ->
contentMargin = parseInt(getComputedStyle(itemView)['margin-left']) ? 0
@presenter.setOverlayDimensions(decorationId, itemView.offsetWidth, itemView.offsetHeight, contentMargin)
renderOverlay: (state, decorationId, {item, pixelPosition}) ->
itemView = atom.views.getView(item)
cachedOverlay = @overlaysById[decorationId]
unless overlayNode = cachedOverlay?.overlayNode
overlayNode = document.createElement('atom-overlay')
@container.appendChild(overlayNode)
@overlaysById[decorationId] = cachedOverlay = {overlayNode, itemView}
itemWidth = item.offsetWidth
itemHeight = item.offsetHeight
# The same node may be used in more than one overlay. This steals the node
# back if it has been displayed in another overlay.
overlayNode.appendChild(itemView) if overlayNode.childNodes.length is 0
{scrollTop, scrollLeft} = state.content
left = pixelPosition.left
if left + itemWidth - scrollLeft > @presenter.contentFrameWidth and left - itemWidth >= scrollLeft
left -= itemWidth
top = pixelPosition.top + @presenter.lineHeight
if top + itemHeight - scrollTop > @presenter.height and top - itemHeight - @presenter.lineHeight >= scrollTop
top -= itemHeight + @presenter.lineHeight
overlayNode.style.top = top + 'px'
overlayNode.style.left = left + 'px'
cachedOverlay.pixelPosition = pixelPosition
overlayNode.style.top = pixelPosition.top + 'px'
overlayNode.style.left = pixelPosition.left + 'px'

View File

@@ -1,7 +1,6 @@
path = require 'path'
_ = require 'underscore-plus'
EmitterMixin = require('emissary').Emitter
{Emitter} = require 'event-kit'
fs = require 'fs-plus'
Q = require 'q'
@@ -10,6 +9,7 @@ Grim = require 'grim'
ServiceHub = require 'service-hub'
Package = require './package'
ThemePackage = require './theme-package'
{isDeprecatedPackage, getDeprecatedPackageMetadata} = require './deprecated-packages'
# Extended: Package manager for coordinating the lifecycle of Atom packages.
#
@@ -28,8 +28,6 @@ ThemePackage = require './theme-package'
# settings and also by calling `enablePackage()/disablePackage()`.
module.exports =
class PackageManager
EmitterMixin.includeInto(this)
constructor: ({configDirPath, @devMode, safeMode, @resourcePath}) ->
@emitter = new Emitter
@packageDirPaths = []
@@ -57,11 +55,6 @@ class PackageManager
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidLoadInitialPackages: (callback) ->
@emitter.on 'did-load-initial-packages', callback
@emitter.on 'did-load-all', callback # TODO: Remove once deprecated pre-1.0 APIs are gone
onDidLoadAll: (callback) ->
Grim.deprecate("Use `::onDidLoadInitialPackages` instead.")
@onDidLoadInitialPackages(callback)
# Public: Invoke the given callback when all packages have been activated.
#
@@ -70,11 +63,6 @@ class PackageManager
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidActivateInitialPackages: (callback) ->
@emitter.on 'did-activate-initial-packages', callback
@emitter.on 'did-activate-all', callback # TODO: Remove once deprecated pre-1.0 APIs are gone
onDidActivateAll: (callback) ->
Grim.deprecate("Use `::onDidActivateInitialPackages` instead.")
@onDidActivateInitialPackages(callback)
# Public: Invoke the given callback when a package is activated.
#
@@ -112,16 +100,6 @@ class PackageManager
onDidUnloadPackage: (callback) ->
@emitter.on 'did-unload-package', callback
on: (eventName) ->
switch eventName
when 'loaded'
Grim.deprecate 'Use PackageManager::onDidLoadInitialPackages instead'
when 'activated'
Grim.deprecate 'Use PackageManager::onDidActivateInitialPackages instead'
else
Grim.deprecate 'PackageManager::on is deprecated. Use event subscription methods instead.'
EmitterMixin::on.apply(this, arguments)
###
Section: Package system data
###
@@ -134,7 +112,7 @@ class PackageManager
commandName = 'apm'
commandName += '.cmd' if process.platform is 'win32'
apmRoot = path.resolve(__dirname, '..', 'apm')
apmRoot = path.join(process.resourcesPath, 'app', 'apm')
@apmPath = path.join(apmRoot, 'bin', commandName)
unless fs.isFileSync(@apmPath)
@apmPath = path.join(apmRoot, 'node_modules', 'atom-package-manager', 'bin', commandName)
@@ -172,6 +150,12 @@ class PackageManager
isBundledPackage: (name) ->
@getPackageDependencies().hasOwnProperty(name)
isDeprecatedPackage: (name, version) ->
isDeprecatedPackage(name, version)
getDeprecatedPackageMetadata: (name) ->
getDeprecatedPackageMetadata(name)
###
Section: Enabling and disabling packages
###
@@ -299,8 +283,7 @@ class PackageManager
getPackageDependencies: ->
unless @packageDependencies?
try
metadataPath = path.join(@resourcePath, 'package.json')
{@packageDependencies} = JSON.parse(fs.readFileSync(metadataPath)) ? {}
@packageDependencies = require('../package.json')?.packageDependencies
@packageDependencies ?= {}
@packageDependencies
@@ -327,11 +310,18 @@ class PackageManager
# of the first package isn't skewed by being the first to require atom
require '../exports/atom'
# TODO: remove after a few atom versions.
@uninstallAutocompletePlus()
packagePaths = @getAvailablePackagePaths()
# TODO: remove after a few atom versions.
@migrateSublimeTabsSettings(packagePaths)
packagePaths = packagePaths.filter (packagePath) => not @isPackageDisabled(path.basename(packagePath))
packagePaths = _.uniq packagePaths, (packagePath) -> path.basename(packagePath)
@loadPackage(packagePath) for packagePath in packagePaths
@emit 'loaded'
@emit 'loaded' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-load-initial-packages'
loadPackage: (nameOrPath) ->
@@ -343,16 +333,23 @@ class PackageManager
try
metadata = Package.loadMetadata(packagePath) ? {}
if metadata.theme
pack = new ThemePackage(packagePath, metadata)
else
pack = new Package(packagePath, metadata)
pack.load()
@loadedPackages[pack.name] = pack
@emitter.emit 'did-load-package', pack
return pack
catch error
console.warn "Failed to load package.json '#{path.basename(packagePath)}'", error.stack ? error
@handleMetadataError(error, packagePath)
return null
unless @isBundledPackage(metadata.name) or Grim.includeDeprecatedAPIs
if @isDeprecatedPackage(metadata.name, metadata.version)
console.warn "Could not load #{metadata.name}@#{metadata.version} because it uses deprecated APIs that have been removed."
return null
if metadata.theme
pack = new ThemePackage(packagePath, metadata)
else
pack = new Package(packagePath, metadata)
pack.load()
@loadedPackages[pack.name] = pack
@emitter.emit 'did-load-package', pack
return pack
else
console.warn "Could not resolve '#{nameOrPath}' to a package path"
null
@@ -378,7 +375,7 @@ class PackageManager
packages = @getLoadedPackagesForTypes(types)
promises = promises.concat(activator.activatePackages(packages))
Q.all(promises).then =>
@emit 'activated'
@emit 'activated' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-activate-initial-packages'
# another type of package manager can handle other package types.
@@ -392,6 +389,7 @@ class PackageManager
for pack in packages
promise = @activatePackage(pack.name)
promises.push(promise) unless pack.hasActivationCommands()
return
@observeDisabledPackages()
promises
@@ -411,6 +409,7 @@ class PackageManager
deactivatePackages: ->
atom.config.transact =>
@deactivatePackage(pack.name) for pack in @getLoadedPackages()
return
@unobserveDisabledPackages()
# Deactivate the package with the given name
@@ -421,3 +420,73 @@ class PackageManager
pack.deactivate()
delete @activePackages[pack.name]
@emitter.emit 'did-deactivate-package', pack
handleMetadataError: (error, packagePath) ->
metadataPath = path.join(packagePath, 'package.json')
detail = "#{error.message} in #{metadataPath}"
stack = "#{error.stack}\n at #{metadataPath}:1:1"
message = "Failed to load the #{path.basename(packagePath)} package"
atom.notifications.addError(message, {stack, detail, dismissable: true})
# TODO: remove these autocomplete-plus specific helpers after a few versions.
uninstallAutocompletePlus: ->
packageDir = null
devDir = path.join("dev", "packages")
for packageDirPath in @packageDirPaths
if not packageDirPath.endsWith(devDir)
packageDir = packageDirPath
break
if packageDir?
dirsToRemove = [
path.join(packageDir, 'autocomplete-plus')
path.join(packageDir, 'autocomplete-atom-api')
path.join(packageDir, 'autocomplete-css')
path.join(packageDir, 'autocomplete-html')
path.join(packageDir, 'autocomplete-snippets')
]
for dirToRemove in dirsToRemove
@uninstallDirectory(dirToRemove)
return
# TODO: remove this after a few versions
migrateSublimeTabsSettings: (packagePaths) ->
return if Grim.includeDeprecatedAPIs
for packagePath in packagePaths when path.basename(packagePath) is 'sublime-tabs'
atom.config.removeAtKeyPath('core.disabledPackages', 'tree-view')
atom.config.removeAtKeyPath('core.disabledPackages', 'tabs')
return
uninstallDirectory: (directory) ->
symlinkPromise = new Promise (resolve) ->
fs.isSymbolicLink directory, (isSymLink) -> resolve(isSymLink)
dirPromise = new Promise (resolve) ->
fs.isDirectory directory, (isDir) -> resolve(isDir)
Promise.all([symlinkPromise, dirPromise]).then (values) ->
[isSymLink, isDir] = values
if not isSymLink and isDir
fs.remove directory, ->
if Grim.includeDeprecatedAPIs
EmitterMixin = require('emissary').Emitter
EmitterMixin.includeInto(PackageManager)
PackageManager::on = (eventName) ->
switch eventName
when 'loaded'
Grim.deprecate 'Use PackageManager::onDidLoadInitialPackages instead'
when 'activated'
Grim.deprecate 'Use PackageManager::onDidActivateInitialPackages instead'
else
Grim.deprecate 'PackageManager::on is deprecated. Use event subscription methods instead.'
EmitterMixin::on.apply(this, arguments)
PackageManager::onDidLoadAll = (callback) ->
Grim.deprecate("Use `::onDidLoadInitialPackages` instead.")
@onDidLoadInitialPackages(callback)
PackageManager::onDidActivateAll = (callback) ->
Grim.deprecate("Use `::onDidActivateInitialPackages` instead.")
@onDidActivateInitialPackages(callback)

View File

@@ -1,28 +1,23 @@
path = require 'path'
normalizePackageData = null
_ = require 'underscore-plus'
async = require 'async'
CSON = require 'season'
fs = require 'fs-plus'
EmitterMixin = require('emissary').Emitter
{Emitter, CompositeDisposable} = require 'event-kit'
Q = require 'q'
{deprecate} = require 'grim'
{includeDeprecatedAPIs, deprecate} = require 'grim'
ModuleCache = require './module-cache'
ScopedProperties = require './scoped-properties'
try
packagesCache = require('../package.json')?._atomPackages ? {}
catch error
packagesCache = {}
packagesCache = require('../package.json')?._atomPackages ? {}
# Loads and activates a package's main module and resources such as
# stylesheets, keymaps, grammar, editor properties, and menus.
module.exports =
class Package
EmitterMixin.includeInto(this)
@isBundledPackagePath: (packagePath) ->
if atom.packages.devMode
return false unless atom.packages.resourcePath.startsWith("#{process.resourcesPath}#{path.sep}")
@@ -30,6 +25,13 @@ class Package
@resourcePathWithTrailingSlash ?= "#{atom.packages.resourcePath}#{path.sep}"
packagePath?.startsWith(@resourcePathWithTrailingSlash)
@normalizeMetadata: (metadata) ->
unless metadata?._id
normalizePackageData ?= require 'normalize-package-data'
normalizePackageData(metadata)
if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string'
metadata.repository.url = metadata.repository.url.replace(/^git\+/, '')
@loadMetadata: (packagePath, ignoreErrors=false) ->
packageName = path.basename(packagePath)
if @isBundledPackagePath(packagePath)
@@ -38,16 +40,19 @@ class Package
if metadataPath = CSON.resolve(path.join(packagePath, 'package'))
try
metadata = CSON.readFileSync(metadataPath)
@normalizeMetadata(metadata)
catch error
throw error unless ignoreErrors
metadata ?= {}
metadata.name = packageName
if metadata.stylesheetMain?
metadata ?= {}
unless typeof metadata.name is 'string' and metadata.name.length > 0
metadata.name = packageName
if includeDeprecatedAPIs and metadata.stylesheetMain?
deprecate("Use the `mainStyleSheet` key instead of `stylesheetMain` in the `package.json` of `#{packageName}`", {packageName})
metadata.mainStyleSheet = metadata.stylesheetMain
if metadata.stylesheets?
if includeDeprecatedAPIs and metadata.stylesheets?
deprecate("Use the `styleSheets` key instead of `stylesheets` in the `package.json` of `#{packageName}`", {packageName})
metadata.styleSheets = metadata.stylesheets
@@ -87,14 +92,6 @@ class Package
onDidDeactivate: (callback) ->
@emitter.on 'did-deactivate', callback
on: (eventName) ->
switch eventName
when 'deactivated'
deprecate 'Use Package::onDidDeactivate instead'
else
deprecate 'Package::on is deprecated. Use event subscription methods instead.'
EmitterMixin::on.apply(this, arguments)
###
Section: Instance Methods
###
@@ -126,9 +123,8 @@ class Package
@loadStylesheets()
@settingsPromise = @loadSettings()
@requireMainModule() unless @hasActivationCommands()
catch error
console.warn "Failed to load package named '#{@name}'", error.stack ? error
@handleError("Failed to load the #{@name} package", error)
this
reset: ->
@@ -144,11 +140,14 @@ class Package
unless @activationDeferred?
@activationDeferred = Q.defer()
@measure 'activateTime', =>
@activateResources()
if @hasActivationCommands()
@subscribeToActivationCommands()
else
@activateNow()
try
@activateResources()
if @hasActivationCommands()
@subscribeToActivationCommands()
else
@activateNow()
catch error
@handleError("Failed to activate the #{@name} package", error)
Q.all([@grammarsPromise, @settingsPromise, @activationDeferred.promise])
@@ -160,8 +159,8 @@ class Package
@mainModule.activate?(atom.packages.getPackageState(@name) ? {})
@mainActivated = true
@activateServices()
catch e
console.warn "Failed to activate package named '#{@name}'", e.stack
catch error
@handleError("Failed to activate the #{@name} package", error)
@activationDeferred?.resolve()
@@ -173,9 +172,9 @@ class Package
if @mainModule.config? and typeof @mainModule.config is 'object'
atom.config.setSchema @name, {type: 'object', properties: @mainModule.config}
else if @mainModule.configDefaults? and typeof @mainModule.configDefaults is 'object'
deprecate """Use a config schema instead. See the configuration section
of https://atom.io/docs/latest/creating-a-package and
https://atom.io/docs/api/latest/Config for more details"""
deprecate("""Use a config schema instead. See the configuration section
of https://atom.io/docs/latest/hacking-atom-package-word-count and
https://atom.io/docs/api/latest/Config for more details""", {packageName: @name})
atom.config.setDefaults(@name, @mainModule.configDefaults)
@mainModule.activateConfig?()
@configActivated = true
@@ -200,7 +199,28 @@ class Package
activateResources: ->
@activationDisposables = new CompositeDisposable
@activationDisposables.add(atom.keymaps.add(keymapPath, map)) for [keymapPath, map] in @keymaps
@activationDisposables.add(atom.contextMenu.add(map['context-menu'])) for [menuPath, map] in @menus when map['context-menu']?
for [menuPath, map] in @menus when map['context-menu']?
try
itemsBySelector = map['context-menu']
# Detect deprecated format for items object
for key, value of itemsBySelector
unless _.isArray(value)
deprecate("""
The context menu CSON format has changed. Please see
https://atom.io/docs/api/latest/ContextMenuManager#context-menu-cson-format
for more info.
""", {packageName: @name})
itemsBySelector = atom.contextMenu.convertLegacyItemsBySelector(itemsBySelector)
@activationDisposables.add(atom.contextMenu.add(itemsBySelector))
catch error
if error.code is 'EBADSELECTOR'
error.message += " in #{menuPath}"
error.stack += "\n at #{menuPath}:1:1"
throw error
@activationDisposables.add(atom.menu.add(map['menu'])) for [menuPath, map] in @menus when map['menu']?
unless @grammarsActivated
@@ -212,24 +232,31 @@ class Package
activateServices: ->
for name, {versions} of @metadata.providedServices
servicesByVersion = {}
for version, methodName of versions
@activationDisposables.add atom.packages.serviceHub.provide(name, version, @mainModule[methodName]())
if typeof @mainModule[methodName] is 'function'
servicesByVersion[version] = @mainModule[methodName]()
@activationDisposables.add atom.packages.serviceHub.provide(name, servicesByVersion)
for name, {versions} of @metadata.consumedServices
for version, methodName of versions
@activationDisposables.add atom.packages.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule))
if typeof @mainModule[methodName] is 'function'
@activationDisposables.add atom.packages.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule))
return
loadKeymaps: ->
if @bundledPackage and packagesCache[@name]?
@keymaps = (["#{atom.packages.resourcePath}#{path.sep}#{keymapPath}", keymapObject] for keymapPath, keymapObject of packagesCache[@name].keymaps)
else
@keymaps = @getKeymapPaths().map (keymapPath) -> [keymapPath, CSON.readFileSync(keymapPath) ? {}]
return
loadMenus: ->
if @bundledPackage and packagesCache[@name]?
@menus = (["#{atom.packages.resourcePath}#{path.sep}#{menuPath}", menuObject] for menuPath, menuObject of packagesCache[@name].menus)
else
@menus = @getMenuPaths().map (menuPath) -> [menuPath, CSON.readFileSync(menuPath) ? {}]
return
getKeymapPaths: ->
keymapsDirPath = path.join(@path, 'keymaps')
@@ -276,6 +303,7 @@ class Package
try
grammar = atom.grammars.readGrammarSync(grammarPath)
grammar.packageName = @name
grammar.bundledPackage = @bundledPackage
@grammars.push(grammar)
grammar.activate()
catch error
@@ -290,17 +318,23 @@ class Package
loadGrammar = (grammarPath, callback) =>
atom.grammars.readGrammar grammarPath, (error, grammar) =>
if error?
console.warn("Failed to load grammar: #{grammarPath}", error.stack ? error)
detail = "#{error.message} in #{grammarPath}"
stack = "#{error.stack}\n at #{grammarPath}:1:1"
atom.notifications.addFatalError("Failed to load a #{@name} package grammar", {stack, detail, dismissable: true})
else
grammar.packageName = @name
grammar.bundledPackage = @bundledPackage
@grammars.push(grammar)
grammar.activate() if @grammarsActivated
callback()
deferred = Q.defer()
grammarsDirPath = path.join(@path, 'grammars')
fs.list grammarsDirPath, ['json', 'cson'], (error, grammarPaths=[]) ->
async.each grammarPaths, loadGrammar, -> deferred.resolve()
fs.exists grammarsDirPath, (grammarsDirExists) ->
return deferred.resolve() unless grammarsDirExists
fs.list grammarsDirPath, ['json', 'cson'], (error, grammarPaths=[]) ->
async.each grammarPaths, loadGrammar, -> deferred.resolve()
deferred.promise
loadSettings: ->
@@ -309,7 +343,9 @@ class Package
loadSettingsFile = (settingsPath, callback) =>
ScopedProperties.load settingsPath, (error, settings) =>
if error?
console.warn("Failed to load package settings: #{settingsPath}", error.stack ? error)
detail = "#{error.message} in #{settingsPath}"
stack = "#{error.stack}\n at #{settingsPath}:1:1"
atom.notifications.addFatalError("Failed to load the #{@name} package settings", {stack, detail, dismissable: true})
else
@settings.push(settings)
settings.activate() if @settingsActivated
@@ -323,8 +359,11 @@ class Package
else
settingsDirPath = path.join(@path, 'settings')
fs.list settingsDirPath, ['json', 'cson'], (error, settingsPaths=[]) ->
async.each settingsPaths, loadSettingsFile, -> deferred.resolve()
fs.exists settingsDirPath, (settingsDirExists) ->
return deferred.resolve() unless settingsDirExists
fs.list settingsDirPath, ['json', 'cson'], (error, settingsPaths=[]) ->
async.each settingsPaths, loadSettingsFile, -> deferred.resolve()
deferred.promise
serialize: ->
@@ -345,7 +384,7 @@ class Package
@mainModule?.deactivate?()
catch e
console.error "Error deactivating package '#{@name}'", e.stack
@emit 'deactivated'
@emit 'deactivated' if includeDeprecatedAPIs
@emitter.emit 'did-deactivate'
deactivateConfig: ->
@@ -363,14 +402,19 @@ class Package
reloadStylesheets: ->
oldSheets = _.clone(@stylesheets)
@loadStylesheets()
try
@loadStylesheets()
catch error
@handleError("Failed to reload the #{@name} package stylesheets", error)
@stylesheetDisposables?.dispose()
@stylesheetDisposables = new CompositeDisposable
@stylesheetsActivated = false
@activateStylesheets()
requireMainModule: ->
return @mainModule if @mainModule?
return @mainModule if @mainModuleRequired
unless @isCompatible()
console.warn """
Failed to require the main module of '#{@name}' because it requires an incompatible native module.
@@ -378,7 +422,9 @@ class Package
"""
return
mainModulePath = @getMainModulePath()
@mainModule = require(mainModulePath) if fs.isFileSync(mainModulePath)
if fs.isFileSync(mainModulePath)
@mainModuleRequired = true
@mainModule = require(mainModulePath)
getMainModulePath: ->
return @mainModulePath if @resolvedMainModulePath
@@ -409,7 +455,15 @@ class Package
do (selector, command) =>
# Add dummy command so it appears in menu.
# The real command will be registered on package activation
@activationCommandSubscriptions.add atom.commands.add selector, command, ->
try
@activationCommandSubscriptions.add atom.commands.add selector, command, ->
catch error
if error.code is 'EBADSELECTOR'
metadataPath = path.join(@path, 'package.json')
error.message += " in #{metadataPath}"
error.stack += "\n at #{metadataPath}:1:1"
throw error
@activationCommandSubscriptions.add atom.commands.onWillDispatch (event) =>
return unless event.type is command
currentTarget = event.target
@@ -419,6 +473,8 @@ class Package
@activateNow()
break
currentTarget = currentTarget.parentElement
return
return
getActivationCommands: ->
return @activationCommands if @activationCommands?
@@ -434,7 +490,7 @@ class Package
@activationCommands[selector].push(commands...)
if @metadata.activationEvents?
deprecate """
deprecate("""
Use `activationCommands` instead of `activationEvents` in your package.json
Commands should be grouped by selector as follows:
```json
@@ -443,7 +499,7 @@ class Package
"atom-text-editor": ["foo:quux"]
}
```
"""
""", {packageName: @name})
if _.isArray(@metadata.activationEvents)
for eventName in @metadata.activationEvents
@activationCommands['atom-workspace'] ?= []
@@ -477,6 +533,7 @@ class Package
for modulePath in fs.listSync(nodeModulesPath)
nativeModulePaths.push(modulePath) if @isNativeModule(modulePath)
traversePath(path.join(modulePath, 'node_modules'))
return
traversePath(path.join(@path, 'node_modules'))
nativeModulePaths
@@ -528,3 +585,37 @@ class Package
@compatible = @incompatibleModules.length is 0
else
@compatible = true
handleError: (message, error) ->
if error.filename and error.location and (error instanceof SyntaxError)
location = "#{error.filename}:#{error.location.first_line + 1}:#{error.location.first_column + 1}"
detail = "#{error.message} in #{location}"
stack = """
SyntaxError: #{error.message}
at #{location}
"""
else if error.less and error.filename and error.column? and error.line?
# Less errors
location = "#{error.filename}:#{error.line}:#{error.column}"
detail = "#{error.message} in #{location}"
stack = """
LessError: #{error.message}
at #{location}
"""
else
detail = error.message
stack = error.stack ? error
atom.notifications.addFatalError(message, {stack, detail, dismissable: true})
if includeDeprecatedAPIs
EmitterMixin = require('emissary').Emitter
EmitterMixin.includeInto(Package)
Package::on = (eventName) ->
switch eventName
when 'deactivated'
deprecate 'Use Package::onDidDeactivate instead'
else
deprecate 'Package::on is deprecated. Use event subscription methods instead.'
EmitterMixin::on.apply(this, arguments)

View File

@@ -1,5 +1,6 @@
{CompositeDisposable} = require 'event-kit'
{callAttachHooks} = require './space-pen-extensions'
PaneResizeHandleElement = require './pane-resize-handle-element'
class PaneAxisElement extends HTMLElement
createdCallback: ->
@@ -12,6 +13,7 @@ class PaneAxisElement extends HTMLElement
@subscriptions.add @model.onDidAddChild(@childAdded.bind(this))
@subscriptions.add @model.onDidRemoveChild(@childRemoved.bind(this))
@subscriptions.add @model.onDidReplaceChild(@childReplaced.bind(this))
@subscriptions.add @model.observeFlexScale(@flexScaleChanged.bind(this))
@childAdded({child, index}) for child, index in @model.getChildren()
@@ -22,21 +24,43 @@ class PaneAxisElement extends HTMLElement
@classList.add('vertical', 'pane-column')
this
isPaneResizeHandleElement: (element) ->
element?.nodeName.toLowerCase() is 'atom-pane-resize-handle'
childAdded: ({child, index}) ->
view = atom.views.getView(child)
@insertBefore(view, @children[index])
@insertBefore(view, @children[index * 2])
prevElement = view.previousSibling
# if previous element is not pane resize element, then insert new resize element
if prevElement? and not @isPaneResizeHandleElement(prevElement)
resizeHandle = document.createElement('atom-pane-resize-handle')
@insertBefore(resizeHandle, view)
nextElement = view.nextSibling
# if next element isnot resize element, then insert new resize element
if nextElement? and not @isPaneResizeHandleElement(nextElement)
resizeHandle = document.createElement('atom-pane-resize-handle')
@insertBefore(resizeHandle, nextElement)
callAttachHooks(view) # for backward compatibility with SpacePen views
childRemoved: ({child}) ->
view = atom.views.getView(child)
siblingView = view.previousSibling
# make sure next sibling view is pane resize view
if siblingView? and @isPaneResizeHandleElement(siblingView)
siblingView.remove()
view.remove()
childReplaced: ({index, oldChild, newChild}) ->
childReplaced: ({index, oldChild, newChild}) ->
focusedElement = document.activeElement if @hasFocus()
@childRemoved({child: oldChild, index})
@childAdded({child: newChild, index})
focusedElement?.focus() if document.activeElement is document.body
flexScaleChanged: (flexScale) -> @style.flexGrow = flexScale
hasFocus: ->
this is document.activeElement or @contains(document.activeElement)

View File

@@ -1,7 +1,7 @@
{Model} = require 'theorist'
{Emitter, CompositeDisposable} = require 'event-kit'
{flatten} = require 'underscore-plus'
Serializable = require 'serializable'
Model = require './model'
module.exports =
class PaneAxis extends Model
@@ -12,13 +12,14 @@ class PaneAxis extends Model
container: null
orientation: null
constructor: ({@container, @orientation, children}) ->
constructor: ({@container, @orientation, children, flexScale}={}) ->
@emitter = new Emitter
@subscriptionsByChild = new WeakMap
@subscriptions = new CompositeDisposable
@children = []
if children?
@addChild(child) for child in children
@flexScale = flexScale ? 1
deserializeParams: (params) ->
{container} = params
@@ -28,6 +29,13 @@ class PaneAxis extends Model
serializeParams: ->
children: @children.map (child) -> child.serialize()
orientation: @orientation
flexScale: @flexScale
getFlexScale: -> @flexScale
setFlexScale: (@flexScale) ->
@emitter.emit 'did-change-flex-scale', @flexScale
@flexScale
getParent: -> @parent
@@ -59,6 +67,13 @@ class PaneAxis extends Model
onDidDestroy: (fn) ->
@emitter.on 'did-destroy', fn
onDidChangeFlexScale: (fn) ->
@emitter.on 'did-change-flex-scale', fn
observeFlexScale: (fn) ->
fn(@flexScale)
@onDidChangeFlexScale(fn)
addChild: (child, index=@children.length) ->
child.setParent(this)
child.setContainer(@container)
@@ -68,6 +83,16 @@ class PaneAxis extends Model
@children.splice(index, 0, child)
@emitter.emit 'did-add-child', {child, index}
adjustFlexScale: ->
# get current total flex scale of children
total = 0
total += child.getFlexScale() for child in @children
needTotal = @children.length
# set every child's flex scale by the ratio
for child in @children
child.setFlexScale(needTotal * child.getFlexScale() / total)
removeChild: (child, replacing=false) ->
index = @children.indexOf(child)
throw new Error("Removing non-existent child") if index is -1
@@ -75,6 +100,7 @@ class PaneAxis extends Model
@unsubscribeFromChild(child)
@children.splice(index, 1)
@adjustFlexScale()
@emitter.emit 'did-remove-child', {child, index}
@reparentLastChild() if not replacing and @children.length < 2
@@ -98,7 +124,9 @@ class PaneAxis extends Model
@addChild(newChild, index + 1)
reparentLastChild: ->
@parent.replaceChild(this, @children[0])
lastChild = @children[0]
lastChild.setFlexScale(@flexScale)
@parent.replaceChild(this, lastChild)
@destroy()
subscribeToChild: (child) ->

View File

@@ -1,4 +1,5 @@
{CompositeDisposable} = require 'event-kit'
Grim = require 'grim'
{callAttachHooks} = require './space-pen-extensions'
PaneContainerView = null
_ = require 'underscore-plus'
@@ -8,12 +9,14 @@ class PaneContainerElement extends HTMLElement
createdCallback: ->
@subscriptions = new CompositeDisposable
@classList.add 'panes'
PaneContainerView ?= require './pane-container-view'
@__spacePenView = new PaneContainerView(this)
if Grim.includeDeprecatedAPIs
PaneContainerView ?= require './pane-container-view'
@__spacePenView = new PaneContainerView(this)
initialize: (@model) ->
@subscriptions.add @model.observeRoot(@rootChanged.bind(this))
@__spacePenView.setModel(@model)
@__spacePenView.setModel(@model) if Grim.includeDeprecatedAPIs
this
rootChanged: (root) ->

View File

@@ -1,7 +1,10 @@
{find, flatten} = require 'underscore-plus'
{Model} = require 'theorist'
Grim = require 'grim'
{Emitter, CompositeDisposable} = require 'event-kit'
Serializable = require 'serializable'
{createGutterView} = require './gutter-component-helpers'
Gutter = require './gutter'
Model = require './model'
Pane = require './pane'
PaneElement = require './pane-element'
PaneContainerElement = require './pane-container-element'
@@ -18,19 +21,14 @@ class PaneContainer extends Model
@version: 1
@properties
activePane: null
root: null
@behavior 'activePaneItem', ->
@$activePane
.switch((activePane) -> activePane?.$activeItem)
.distinctUntilChanged()
constructor: (params) ->
super
unless Grim.includeDeprecatedAPIs
@activePane = params?.activePane
@emitter = new Emitter
@subscriptions = new CompositeDisposable
@@ -64,6 +62,7 @@ class PaneContainer extends Model
new PaneElement().initialize(model)
atom.views.addViewProvider TextEditor, (model) ->
new TextEditorElement().initialize(model)
atom.views.addViewProvider(Gutter, createGutterView)
onDidChangeRoot: (fn) ->
@emitter.on 'did-change-root', fn
@@ -151,6 +150,7 @@ class PaneContainer extends Model
saveAll: ->
pane.saveItems() for pane in @getPanes()
return
confirmClose: (options) ->
allSaved = true
@@ -186,6 +186,7 @@ class PaneContainer extends Model
destroyEmptyPanes: ->
pane.destroy() for pane in @getPanes() when pane.items.length is 0
return
willDestroyPaneItem: (event) ->
@emitter.emit 'will-destroy-pane-item', event
@@ -234,3 +235,12 @@ class PaneContainer extends Model
removedPaneItem: (item) ->
@itemRegistry.removeItem(item)
if Grim.includeDeprecatedAPIs
PaneContainer.properties
activePane: null
PaneContainer.behavior 'activePaneItem', ->
@$activePane
.switch((activePane) -> activePane?.$activeItem)
.distinctUntilChanged()

View File

@@ -1,6 +1,8 @@
path = require 'path'
{CompositeDisposable} = require 'event-kit'
Grim = require 'grim'
{$, callAttachHooks, callRemoveHooks} = require './space-pen-extensions'
PaneView = require './pane-view'
PaneView = null
class PaneElement extends HTMLElement
attached: false
@@ -12,7 +14,7 @@ class PaneElement extends HTMLElement
@initializeContent()
@subscribeToDOMEvents()
@createSpacePenShim()
@createSpacePenShim() if Grim.includeDeprecatedAPIs
attachedCallback: ->
@attached = true
@@ -37,10 +39,24 @@ class PaneElement extends HTMLElement
handleBlur = (event) =>
@model.blur() unless @contains(event.relatedTarget)
handleDragOver = (event) ->
event.preventDefault()
event.stopPropagation()
handleDrop = (event) =>
event.preventDefault()
event.stopPropagation()
@getModel().activate()
pathsToOpen = Array::map.call event.dataTransfer.files, (file) -> file.path
atom.open({pathsToOpen}) if pathsToOpen.length > 0
@addEventListener 'focus', handleFocus, true
@addEventListener 'blur', handleBlur, true
@addEventListener 'dragover', handleDragOver
@addEventListener 'drop', handleDrop
createSpacePenShim: ->
PaneView ?= require './pane-view'
@__spacePenView = new PaneView(this)
initialize: (@model) ->
@@ -49,7 +65,9 @@ class PaneElement extends HTMLElement
@subscriptions.add @model.observeActiveItem(@activeItemChanged.bind(this))
@subscriptions.add @model.onDidRemoveItem(@itemRemoved.bind(this))
@subscriptions.add @model.onDidDestroy(@paneDestroyed.bind(this))
@__spacePenView.setModel(@model)
@subscriptions.add @model.observeFlexScale(@flexScaleChanged.bind(this))
@__spacePenView.setModel(@model) if Grim.includeDeprecatedAPIs
this
getModel: -> @model
@@ -64,11 +82,18 @@ class PaneElement extends HTMLElement
@classList.remove('active')
activeItemChanged: (item) ->
delete @dataset.activeItemName
delete @dataset.activeItemPath
return unless item?
hasFocus = @hasFocus()
itemView = atom.views.getView(item)
if itemPath = item.getPath?()
@dataset.activeItemName = path.basename(itemPath)
@dataset.activeItemPath = itemPath
unless @itemViews.contains(itemView)
@itemViews.appendChild(itemView)
callAttachHooks(itemView)
@@ -102,6 +127,9 @@ class PaneElement extends HTMLElement
paneDestroyed: ->
@subscriptions.dispose()
flexScaleChanged: (flexScale) ->
@style.flexGrow = flexScale
getActiveView: -> atom.views.getView(@model.getActiveItem())
hasFocus: ->

View File

@@ -0,0 +1,68 @@
class PaneResizeHandleElement extends HTMLElement
createdCallback: ->
@resizePane = @resizePane.bind(this)
@resizeStopped = @resizeStopped.bind(this)
@subscribeToDOMEvents()
subscribeToDOMEvents: ->
@addEventListener 'dblclick', @resizeToFitContent.bind(this)
@addEventListener 'mousedown', @resizeStarted.bind(this)
attachedCallback: ->
@isHorizontal = @parentElement.classList.contains("horizontal")
@classList.add if @isHorizontal then 'horizontal' else 'vertical'
detachedCallback: ->
@resizeStopped()
resizeToFitContent: ->
# clear flex-grow css style of both pane
@previousSibling?.model.setFlexScale(1)
@nextSibling?.model.setFlexScale(1)
resizeStarted: (e) ->
e.stopPropagation()
document.addEventListener 'mousemove', @resizePane
document.addEventListener 'mouseup', @resizeStopped
resizeStopped: ->
document.removeEventListener 'mousemove', @resizePane
document.removeEventListener 'mouseup', @resizeStopped
calcRatio: (ratio1, ratio2, total) ->
allRatio = ratio1 + ratio2
[total * ratio1 / allRatio, total * ratio2 / allRatio]
setFlexGrow: (prevSize, nextSize) ->
@prevModel = @previousSibling.model
@nextModel = @nextSibling.model
totalScale = @prevModel.getFlexScale() + @nextModel.getFlexScale()
flexGrows = @calcRatio(prevSize, nextSize, totalScale)
@prevModel.setFlexScale flexGrows[0]
@nextModel.setFlexScale flexGrows[1]
fixInRange: (val, minValue, maxValue) ->
Math.min(Math.max(val, minValue), maxValue)
resizePane: ({clientX, clientY, which}) ->
return @resizeStopped() unless which is 1
return @resizeStopped() unless @previousSibling? and @nextSibling?
if @isHorizontal
totalWidth = @previousSibling.clientWidth + @nextSibling.clientWidth
#get the left and right width after move the resize view
leftWidth = clientX - @previousSibling.getBoundingClientRect().left
leftWidth = @fixInRange(leftWidth, 0, totalWidth)
rightWidth = totalWidth - leftWidth
# set the flex grow by the ratio of left width and right width
# to change pane width
@setFlexGrow(leftWidth, rightWidth)
else
totalHeight = @previousSibling.clientHeight + @nextSibling.clientHeight
topHeight = clientY - @previousSibling.getBoundingClientRect().top
topHeight = @fixInRange(topHeight, 0, totalHeight)
bottomHeight = totalHeight - topHeight
@setFlexGrow(topHeight, bottomHeight)
module.exports = PaneResizeHandleElement =
document.registerElement 'atom-pane-resize-handle', prototype: PaneResizeHandleElement.prototype

View File

@@ -116,19 +116,15 @@ class PaneView extends View
if item.onDidChangeTitle?
disposable = item.onDidChangeTitle(@activeItemTitleChanged)
deprecate 'Please return a Disposable object from your ::onDidChangeTitle method!' unless disposable?.dispose?
@activeItemDisposables.add(disposable) if disposable?.dispose?
else if item.on?
deprecate 'If you would like your pane item to support title change behavior, please implement a ::onDidChangeTitle() method. ::on methods for items are no longer supported. If not, ignore this message.'
disposable = item.on('title-changed', @activeItemTitleChanged)
@activeItemDisposables.add(disposable) if disposable?.dispose?
if item.onDidChangeModified?
disposable = item.onDidChangeModified(@activeItemModifiedChanged)
deprecate 'Please return a Disposable object from your ::onDidChangeModified method!' unless disposable?.dispose?
@activeItemDisposables.add(disposable) if disposable?.dispose?
else if item.on?
deprecate 'If you would like your pane item to support modified behavior, please implement a ::onDidChangeModified() method. If not, ignore this message. ::on methods for items are no longer supported.'
item.on('modified-status-changed', @activeItemModifiedChanged)
@activeItemDisposables.add(disposable) if disposable?.dispose?

View File

@@ -1,11 +1,10 @@
{find, compact, extend, last} = require 'underscore-plus'
{Model} = require 'theorist'
{Emitter} = require 'event-kit'
Serializable = require 'serializable'
Grim = require 'grim'
Model = require './model'
PaneAxis = require './pane-axis'
TextEditor = require './text-editor'
PaneView = null
# 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.
@@ -16,40 +15,33 @@ class Pane extends Model
atom.deserializers.add(this)
Serializable.includeInto(this)
@properties
container: undefined
activeItem: undefined
focused: false
# Public: Only one pane is considered *active* at a time. A pane is activated
# when it is focused, and when focus returns to the pane container after
# moving to another element such as a panel, it returns to the active pane.
@behavior 'active', ->
@$container
.switch((container) -> container?.$activePane)
.map((activePane) => activePane is this)
.distinctUntilChanged()
constructor: (params) ->
super
unless Grim.includeDeprecatedAPIs
@container = params?.container
@activeItem = params?.activeItem
@emitter = new Emitter
@itemSubscriptions = new WeakMap
@items = []
@addItems(compact(params?.items ? []))
@setActiveItem(@items[0]) unless @getActiveItem()?
@setFlexScale(params?.flexScale ? 1)
# Called by the Serializable mixin during serialization.
serializeParams: ->
if typeof @activeItem?.getURI is 'function'
activeItemURI = @activeItem.getURI()
else if typeof @activeItem?.getUri is 'function'
else if Grim.includeDeprecatedAPIs and typeof @activeItem?.getUri is 'function'
activeItemURI = @activeItem.getUri()
id: @id
items: compact(@items.map((item) -> item.serialize?()))
activeItemURI: activeItemURI
focused: @focused
flexScale: @flexScale
# Called by the Serializable mixin during deserialization.
deserializeParams: (params) ->
@@ -59,7 +51,7 @@ class Pane extends Model
params.activeItem = find params.items, (item) ->
if typeof item.getURI is 'function'
itemURI = item.getURI()
else if typeof item.getUri is 'function'
else if Grim.includeDeprecatedAPIs and typeof item.getUri is 'function'
itemURI = item.getUri()
itemURI is activeItemURI
@@ -76,10 +68,36 @@ class Pane extends Model
@container = container
container.didAddPane({pane: this})
setFlexScale: (@flexScale) ->
@emitter.emit 'did-change-flex-scale', @flexScale
@flexScale
getFlexScale: -> @flexScale
###
Section: Event Subscription
###
# Public: Invoke the given callback when the pane resize
#
# the callback will be invoked when pane's flexScale property changes
#
# * `callback` {Function} to be called when the pane is resized
#
# 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 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.
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
@@ -202,39 +220,6 @@ class Pane extends Model
onWillDestroyItem: (callback) ->
@emitter.on 'will-destroy-item', callback
on: (eventName) ->
switch eventName
when 'activated'
Grim.deprecate("Use Pane::onDidActivate instead")
when 'destroyed'
Grim.deprecate("Use Pane::onDidDestroy instead")
when 'item-added'
Grim.deprecate("Use Pane::onDidAddItem instead")
when 'item-removed'
Grim.deprecate("Use Pane::onDidRemoveItem instead")
when 'item-moved'
Grim.deprecate("Use Pane::onDidMoveItem instead")
when 'before-item-destroyed'
Grim.deprecate("Use Pane::onWillDestroyItem instead")
else
Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead.")
super
behavior: (behaviorName) ->
switch behaviorName
when 'active'
Grim.deprecate("The $active behavior property is deprecated. Use ::observeActive or ::onDidChangeActive instead.")
when 'container'
Grim.deprecate("The $container behavior property is deprecated.")
when 'activeItem'
Grim.deprecate("The $activeItem behavior property is deprecated. Use ::observeActiveItem or ::onDidChangeActiveItem instead.")
when 'focused'
Grim.deprecate("The $focused behavior property is deprecated.")
else
Grim.deprecate("Pane::behavior is deprecated. Use event subscription methods instead.")
super
# Called by the view layer to indicate that the pane has gained focus.
focus: ->
@focused = true
@@ -249,6 +234,10 @@ class Pane extends Model
getPanes: -> [this]
unsubscribeFromItem: (item) ->
@itemSubscriptions.get(item)?.dispose()
@itemSubscriptions.delete(item)
###
Section: Items
###
@@ -340,11 +329,13 @@ class Pane extends Model
addItem: (item, index=@getActiveItemIndex() + 1) ->
return if item in @items
if typeof item.on is 'function'
if typeof item.onDidDestroy is 'function'
@itemSubscriptions.set item, item.onDidDestroy => @removeItem(item, true)
else if Grim.includeDeprecatedAPIs and typeof item.on is 'function'
@subscribe item, 'destroyed', => @removeItem(item, true)
@items.splice(index, 0, item)
@emit 'item-added', item, index
@emit 'item-added', item, index if Grim.includeDeprecatedAPIs
@emitter.emit 'did-add-item', {item, index}
@setActiveItem(item) unless @getActiveItem()?
item
@@ -367,8 +358,9 @@ class Pane extends Model
index = @items.indexOf(item)
return if index is -1
if typeof item.on is 'function'
if Grim.includeDeprecatedAPIs and typeof item.on is 'function'
@unsubscribe item
@unsubscribeFromItem(item)
if item is @activeItem
if @items.length is 1
@@ -378,7 +370,7 @@ class Pane extends Model
else
@activatePreviousItem()
@items.splice(index, 1)
@emit 'item-removed', item, index, destroyed
@emit 'item-removed', item, index, destroyed if Grim.includeDeprecatedAPIs
@emitter.emit 'did-remove-item', {item, index, destroyed}
@container?.didDestroyPaneItem({item, index, pane: this}) if destroyed
@destroy() if @items.length is 0 and atom.config.get('core.destroyEmptyPanes')
@@ -391,7 +383,7 @@ class Pane extends Model
oldIndex = @items.indexOf(item)
@items.splice(oldIndex, 1)
@items.splice(newIndex, 0, item)
@emit 'item-moved', item, newIndex
@emit 'item-moved', item, newIndex if Grim.includeDeprecatedAPIs
@emitter.emit 'did-move-item', {item, oldIndex, newIndex}
# Public: Move the given item to the given index on another pane.
@@ -419,7 +411,7 @@ class Pane extends Model
destroyItem: (item) ->
index = @items.indexOf(item)
if index isnt -1
@emit 'before-item-destroyed', item
@emit 'before-item-destroyed', item if Grim.includeDeprecatedAPIs
@emitter.emit 'will-destroy-item', {item, index}
@container?.willDestroyPaneItem({item, index, pane: this})
if @promptToSaveItem(item)
@@ -432,10 +424,12 @@ class Pane extends Model
# Public: Destroy all items.
destroyItems: ->
@destroyItem(item) for item in @getItems()
return
# Public: Destroy all items except for the active item.
destroyInactiveItems: ->
@destroyItem(item) for item in @getItems() when item isnt @activeItem
return
promptToSaveItem: (item, options={}) ->
return true unless item.shouldPromptToSave?(options)
@@ -498,8 +492,9 @@ class Pane extends Model
saveItemAs: (item, nextAction) ->
return unless item?.saveAs?
itemPath = item.getPath?()
newItemPath = atom.showSaveDialogSync(itemPath)
saveOptions = item.getSaveDialogOptions?() ? {}
saveOptions.defaultPath ?= item.getPath()
newItemPath = atom.showSaveDialogSync(saveOptions)
if newItemPath
try
item.saveAs(newItemPath)
@@ -510,6 +505,7 @@ class Pane extends Model
# Public: Save all items.
saveItems: ->
@saveItem(item) for item in @getItems()
return
# Public: Return the first item that matches the given URI or undefined if
# none exists.
@@ -524,10 +520,6 @@ class Pane extends Model
itemUri is uri
itemForUri: (uri) ->
Grim.deprecate("Use `::itemForURI` instead.")
@itemForURI(uri)
# Public: Activate the first item that matches the given URI.
#
# Returns a {Boolean} indicating whether an item matching the URI was found.
@@ -538,10 +530,6 @@ class Pane extends Model
else
false
activateItemForUri: (uri) ->
Grim.deprecate("Use `::activateItemForURI` instead.")
@activateItemForURI(uri)
copyActiveItem: ->
if @activeItem?
@activeItem.copy?() ? atom.deserializers.deserialize(@activeItem.serialize())
@@ -561,7 +549,7 @@ class Pane extends Model
throw new Error("Pane has been destroyed") if @isDestroyed()
@container?.setActivePane(this)
@emit 'activated'
@emit 'activated' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-activate'
# Public: Close the pane and destroy all its items.
@@ -632,7 +620,8 @@ class Pane extends Model
params.items.push(@copyActiveItem())
if @parent.orientation isnt orientation
@parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this]}))
@parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this], @flexScale}))
@setFlexScale(1)
newPane = new @constructor(params)
switch side
@@ -675,7 +664,7 @@ class Pane extends Model
true
handleSaveError: (error) ->
if error.message.endsWith('is a directory')
if error.code is 'EISDIR' or 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}'")
@@ -683,8 +672,69 @@ class Pane extends Model
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 error.code is 'ENOSPC' and error.path?
atom.notifications.addWarning("Unable to save file: No space left on device '#{error.path}'")
else if error.code is 'ENXIO' and error.path?
atom.notifications.addWarning("Unable to save file: No such device or address '#{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
if Grim.includeDeprecatedAPIs
Pane.properties
container: undefined
activeItem: undefined
focused: false
Pane.behavior 'active', ->
@$container
.switch((container) -> container?.$activePane)
.map((activePane) => activePane is this)
.distinctUntilChanged()
Pane::on = (eventName) ->
switch eventName
when 'activated'
Grim.deprecate("Use Pane::onDidActivate instead")
when 'destroyed'
Grim.deprecate("Use Pane::onDidDestroy instead")
when 'item-added'
Grim.deprecate("Use Pane::onDidAddItem instead")
when 'item-removed'
Grim.deprecate("Use Pane::onDidRemoveItem instead")
when 'item-moved'
Grim.deprecate("Use Pane::onDidMoveItem instead")
when 'before-item-destroyed'
Grim.deprecate("Use Pane::onWillDestroyItem instead")
else
Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead.")
super
Pane::behavior = (behaviorName) ->
switch behaviorName
when 'active'
Grim.deprecate("The $active behavior property is deprecated. Use ::observeActive or ::onDidChangeActive instead.")
when 'container'
Grim.deprecate("The $container behavior property is deprecated.")
when 'activeItem'
Grim.deprecate("The $activeItem behavior property is deprecated. Use ::observeActiveItem or ::onDidChangeActiveItem instead.")
when 'focused'
Grim.deprecate("The $focused behavior property is deprecated.")
else
Grim.deprecate("Pane::behavior is deprecated. Use event subscription methods instead.")
super
Pane::itemForUri = (uri) ->
Grim.deprecate("Use `::itemForURI` instead.")
@itemForURI(uri)
Pane::activateItemForUri = (uri) ->
Grim.deprecate("Use `::activateItemForURI` instead.")
@activateItemForURI(uri)
else
Pane::container = undefined
Pane::activeItem = undefined
Pane::focused = undefined

View File

@@ -4,15 +4,14 @@ url = require 'url'
_ = require 'underscore-plus'
fs = require 'fs-plus'
Q = require 'q'
{deprecate} = require 'grim'
{Model} = require 'theorist'
{Subscriber} = require 'emissary'
{includeDeprecatedAPIs, deprecate} = require 'grim'
{Emitter} = require 'event-kit'
DefaultDirectoryProvider = require './default-directory-provider'
Serializable = require 'serializable'
TextBuffer = require 'text-buffer'
Grim = require 'grim'
DefaultDirectoryProvider = require './default-directory-provider'
Model = require './model'
TextEditor = require './text-editor'
Task = require './task'
GitRepositoryProvider = require './git-repository-provider'
@@ -25,12 +24,6 @@ class Project extends Model
atom.deserializers.add(this)
Serializable.includeInto(this)
@pathForRepositoryUrl: (repoUrl) ->
deprecate '::pathForRepositoryUrl will be removed. Please remove from your code.'
[repoName] = url.parse(repoUrl).path.split('/')[-1..]
repoName = repoName.replace(/\.git$/, '')
path.join(atom.config.get('core.projectHome'), repoName)
###
Section: Construction and Destruction
###
@@ -53,7 +46,7 @@ class Project extends Model
# to either a {Repository} or null. Ideally, the {Directory} would be used
# as the key; however, there can be multiple {Directory} objects created for
# the same real path, so it is not a good key.
@repositoryPromisesByPath = new Map();
@repositoryPromisesByPath = new Map()
# Note that the GitRepositoryProvider is registered synchronously so that
# it is available immediately on startup.
@@ -73,7 +66,9 @@ class Project extends Model
@subscribeToBuffer(buffer) for buffer in @buffers
Grim.deprecate("Pass 'paths' array instead of 'path' to project constructor") if path?
if Grim.includeDeprecatedAPIs and path?
Grim.deprecate("Pass 'paths' array instead of 'path' to project constructor")
paths ?= _.compact([path])
@setPaths(paths)
@@ -83,6 +78,7 @@ class Project extends Model
destroyUnretainedBuffers: ->
buffer.destroy() for buffer in @getBuffers() when not buffer.isRetained()
return
###
Section: Serialization
@@ -109,13 +105,17 @@ class Project extends Model
Section: Event Subscription
###
# Public: Invoke the given callback when the project paths change.
#
# * `callback` {Function} to be called after the project paths change.
# * `projectPaths` An {Array} of {String} project paths.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangePaths: (callback) ->
@emitter.on 'did-change-paths', callback
on: (eventName) ->
if eventName is 'path-changed'
Grim.deprecate("Use Project::onDidChangePaths instead")
super
onDidAddBuffer: (callback) ->
@emitter.on 'did-add-buffer', callback
###
Section: Accessing the git repository
@@ -128,13 +128,10 @@ class Project extends Model
# Prefer the following, which evaluates to a {Promise} that resolves to an
# {Array} of {Repository} objects:
# ```
# Promise.all(project.getDirectories().map(
# project.repositoryForDirectory.bind(project)))
# Promise.all(atom.project.getDirectories().map(
# atom.project.repositoryForDirectory.bind(atom.project)))
# ```
getRepositories: -> @repositories
getRepo: ->
Grim.deprecate("Use ::getRepositories instead")
@getRepositories()[0]
# Public: Get the repository for a given directory asynchronously.
#
@@ -168,28 +165,23 @@ class Project extends Model
# Public: Get an {Array} of {String}s containing the paths of the project's
# directories.
getPaths: -> rootDirectory.getPath() for rootDirectory in @rootDirectories
getPath: ->
Grim.deprecate("Use ::getPaths instead")
@getPaths()[0]
# Public: Set the paths of the project's directories.
#
# * `projectPaths` {Array} of {String} paths.
setPaths: (projectPaths) ->
rootDirectory.off() for rootDirectory in @rootDirectories
if includeDeprecatedAPIs
rootDirectory.off() for rootDirectory in @rootDirectories
repository?.destroy() for repository in @repositories
@rootDirectories = []
@repositories = []
@addPath(projectPath, emitEvent: false) for projectPath in projectPaths
@emit "path-changed"
@emit "path-changed" if includeDeprecatedAPIs
@emitter.emit 'did-change-paths', projectPaths
setPath: (path) ->
Grim.deprecate("Use ::setPaths instead")
@setPaths([path])
# Public: Add a path to the project's list of root paths
#
# * `projectPath` {String} The path to the directory to add.
@@ -214,14 +206,16 @@ class Project extends Model
@repositories.push(repo ? null)
unless options?.emitEvent is false
@emit "path-changed"
@emit "path-changed" if includeDeprecatedAPIs
@emitter.emit 'did-change-paths', @getPaths()
# Public: remove a path from the project's list of root paths.
#
# * `projectPath` {String} The path to remove.
removePath: (projectPath) ->
projectPath = path.normalize(projectPath)
# The projectPath may be a URI, in which case it should not be normalized.
unless projectPath in @getPaths()
projectPath = path.normalize(projectPath)
indexToRemove = null
for directory, i in @rootDirectories
@@ -232,9 +226,9 @@ class Project extends Model
if indexToRemove?
[removedDirectory] = @rootDirectories.splice(indexToRemove, 1)
[removedRepository] = @repositories.splice(indexToRemove, 1)
removedDirectory.off()
removedDirectory.off() if includeDeprecatedAPIs
removedRepository?.destroy() unless removedRepository in @repositories
@emit "path-changed"
@emit "path-changed" if includeDeprecatedAPIs
@emitter.emit "did-change-paths", @getPaths()
true
else
@@ -243,13 +237,6 @@ class Project extends Model
# Public: Get an {Array} of {Directory}s associated with this project.
getDirectories: ->
@rootDirectories
getRootDirectory: ->
Grim.deprecate("Use ::getDirectories instead")
@getDirectories()[0]
resolve: (uri) ->
Grim.deprecate("Use `Project::getDirectories()[0]?.resolve()` instead")
@resolvePath(uri)
resolvePath: (uri) ->
return unless uri
@@ -280,7 +267,6 @@ class Project extends Model
# * `relativePath` {String} The relative path from the project directory to
# the given path.
relativizePath: (fullPath) ->
return fullPath if fullPath?.match(/[A-Za-z0-9+-.]+:\/\//) # leave path alone if it has a scheme
for rootDirectory in @rootDirectories
relativePath = rootDirectory.relativize(fullPath)
return [rootDirectory.getPath(), relativePath] unless relativePath is fullPath
@@ -316,18 +302,6 @@ class Project extends Model
contains: (pathToCheck) ->
@rootDirectories.some (dir) -> dir.contains(pathToCheck)
###
Section: Searching and Replacing
###
scan: (regex, options={}, iterator) ->
Grim.deprecate("Use atom.workspace.scan instead of atom.project.scan")
atom.workspace.scan(regex, options, iterator)
replace: (regex, replacementText, filePaths, iterator) ->
Grim.deprecate("Use atom.workspace.replace instead of atom.project.replace")
atom.workspace.replace(regex, replacementText, filePaths, iterator)
###
Section: Private
###
@@ -349,14 +323,22 @@ class Project extends Model
# allow ENOENT errors to create an editor for paths that dont exist
throw error unless error.code is 'ENOENT'
@bufferForPath(filePath).then (buffer) =>
@buildEditorForBuffer(buffer, options)
absoluteFilePath = @resolvePath(filePath)
# Deprecated
openSync: (filePath, options={}) ->
deprecate("Use Project::open instead")
filePath = @resolvePath(filePath)
@buildEditorForBuffer(@bufferForPathSync(filePath), options)
fileSize = fs.getSizeSync(absoluteFilePath)
if fileSize >= 20 * 1048576 # 20MB
choice = atom.confirm
message: 'Atom will be unresponsive during the loading of very large files.'
detailedMessage: "Do you still want to load this file?"
buttons: ["Proceed", "Cancel"]
if choice is 1
error = new Error
error.code = 'CANCELLED'
throw error
@bufferForPath(absoluteFilePath).then (buffer) =>
@buildEditorForBuffer(buffer, _.extend({fileSize}, options))
# Retrieves all the {TextBuffer}s in the project; that is, the
# buffers for all open files.
@@ -370,7 +352,7 @@ class Project extends Model
@findBufferForPath(@resolvePath(filePath))?.isModified()
findBufferForPath: (filePath) ->
_.find @buffers, (buffer) -> buffer.getPath() == filePath
_.find @buffers, (buffer) -> buffer.getPath() is filePath
# Only to be used in specs
bufferForPathSync: (filePath) ->
@@ -386,8 +368,7 @@ class Project extends Model
# * `filePath` A {String} representing a path. If `null`, an "Untitled" buffer is created.
#
# Returns a promise that resolves to the {TextBuffer}.
bufferForPath: (filePath) ->
absoluteFilePath = @resolvePath(filePath)
bufferForPath: (absoluteFilePath) ->
existingBuffer = @findBufferForPath(absoluteFilePath) if absoluteFilePath
Q(existingBuffer ? @buildBuffer(absoluteFilePath))
@@ -408,11 +389,6 @@ class Project extends Model
#
# Returns a promise that resolves to the {TextBuffer}.
buildBuffer: (absoluteFilePath) ->
if fs.getSizeSync(absoluteFilePath) >= 2 * 1048576 # 2MB
error = new Error("Atom can only handle files < 2MB for now.")
error.code = 'EFILETOOLARGE'
throw error
buffer = new TextBuffer({filePath: absoluteFilePath})
@addBuffer(buffer)
buffer.load()
@@ -426,7 +402,8 @@ class Project extends Model
addBufferAtIndex: (buffer, index, options={}) ->
@buffers.splice(index, 0, buffer)
@subscribeToBuffer(buffer)
@emit 'buffer-created', buffer
@emit 'buffer-created', buffer if includeDeprecatedAPIs
@emitter.emit 'did-add-buffer', buffer
buffer
# Removes a {TextBuffer} association from the project.
@@ -441,7 +418,8 @@ class Project extends Model
buffer?.destroy()
buildEditorForBuffer: (buffer, editorOptions) ->
editor = new TextEditor(_.extend({buffer, registerEditor: true}, editorOptions))
largeFileMode = editorOptions.fileSize >= 2 * 1048576 # 2MB
editor = new TextEditor(_.extend({buffer, largeFileMode, registerEditor: true}, editorOptions))
editor
eachBuffer: (args...) ->
@@ -465,22 +443,65 @@ class Project extends Model
detail: error.message
dismissable: true
# Deprecated: delegate
registerOpener: (opener) ->
deprecate("Use Workspace::addOpener instead")
atom.workspace.registerOpener(opener)
if includeDeprecatedAPIs
Project.pathForRepositoryUrl = (repoUrl) ->
deprecate '::pathForRepositoryUrl will be removed. Please remove from your code.'
[repoName] = url.parse(repoUrl).path.split('/')[-1..]
repoName = repoName.replace(/\.git$/, '')
path.join(atom.config.get('core.projectHome'), repoName)
# Deprecated: delegate
unregisterOpener: (opener) ->
Project::registerOpener = (opener) ->
deprecate("Use Workspace::addOpener instead")
atom.workspace.addOpener(opener)
Project::unregisterOpener = (opener) ->
deprecate("Call .dispose() on the Disposable returned from ::addOpener instead")
atom.workspace.unregisterOpener(opener)
# Deprecated: delegate
eachEditor: (callback) ->
deprecate("Use Workspace::eachEditor instead")
atom.workspace.eachEditor(callback)
Project::eachEditor = (callback) ->
deprecate("Use Workspace::observeTextEditors instead")
atom.workspace.observeTextEditors(callback)
# Deprecated: delegate
getEditors: ->
deprecate("Use Workspace::getEditors instead")
atom.workspace.getEditors()
Project::getEditors = ->
deprecate("Use Workspace::getTextEditors instead")
atom.workspace.getTextEditors()
Project::on = (eventName) ->
if eventName is 'path-changed'
Grim.deprecate("Use Project::onDidChangePaths instead")
else
Grim.deprecate("Project::on is deprecated. Use documented event subscription methods instead.")
super
Project::getRepo = ->
Grim.deprecate("Use ::getRepositories instead")
@getRepositories()[0]
Project::getPath = ->
Grim.deprecate("Use ::getPaths instead")
@getPaths()[0]
Project::setPath = (path) ->
Grim.deprecate("Use ::setPaths instead")
@setPaths([path])
Project::getRootDirectory = ->
Grim.deprecate("Use ::getDirectories instead")
@getDirectories()[0]
Project::resolve = (uri) ->
Grim.deprecate("Use `Project::getDirectories()[0]?.resolve()` instead")
@resolvePath(uri)
Project::scan = (regex, options={}, iterator) ->
Grim.deprecate("Use atom.workspace.scan instead of atom.project.scan")
atom.workspace.scan(regex, options, iterator)
Project::replace = (regex, replacementText, filePaths, iterator) ->
Grim.deprecate("Use atom.workspace.replace instead of atom.project.replace")
atom.workspace.replace(regex, replacementText, filePaths, iterator)
Project::openSync = (filePath, options={}) ->
deprecate("Use Project::open instead")
filePath = @resolvePath(filePath)
@buildEditorForBuffer(@bufferForPathSync(filePath), options)

View File

@@ -112,6 +112,7 @@ class RowMap
@regions.splice index - 1, 2,
bufferRows: leftRegion.bufferRows + rightRegion.bufferRows
screenRows: leftRegion.screenRows + rightRegion.screenRows
return
# Public: Returns an array of strings describing the map's regions.
inspect: ->

View File

@@ -0,0 +1,6 @@
# Using clipboard in renderer process is not safe on Linux.
module.exports =
if process.platform is 'linux' and process.type is 'renderer'
require('remote').require('clipboard')
else
require('clipboard')

View File

@@ -34,7 +34,7 @@ module.exports = (rootPaths, regexSource, options) ->
scanner.on 'path-found', ->
pathsSearched++
if pathsSearched % PATHS_COUNTER_SEARCHED_CHUNK == 0
if pathsSearched % PATHS_COUNTER_SEARCHED_CHUNK is 0
emit('scan:paths-searched', pathsSearched)
search regex, scanner, searcher, ->

View File

@@ -15,7 +15,7 @@
# specific position in the buffer.
# * {Cursor::getScopeDescriptor} to get a cursor's descriptor based on position.
#
# See the [scopes and scope descriptor guide](https://atom.io/docs/latest/advanced/scopes-and-scope-descriptors)
# See the [scopes and scope descriptor guide](https://atom.io/docs/latest/behind-atom-scoped-settings-scopes-and-scope-descriptors)
# for more information.
module.exports =
class ScopeDescriptor
@@ -44,3 +44,6 @@ class ScopeDescriptor
scope = ".#{scope}" unless scope[0] is '.'
scope
.join(' ')
toString: ->
@getScopeChain()

View File

@@ -12,6 +12,9 @@ class ScrollbarComponent
@domNode.addEventListener 'scroll', @onScrollCallback
getDomNode: ->
@domNode
updateSync: (state) ->
@oldState ?= {}
switch @orientation

View File

@@ -1,12 +1,15 @@
module.exports =
class ScrollbarCornerComponent
constructor: () ->
constructor: ->
@domNode = document.createElement('div')
@domNode.classList.add('scrollbar-corner')
@contentNode = document.createElement('div')
@domNode.appendChild(@contentNode)
getDomNode: ->
@domNode
updateSync: (state) ->
@oldState ?= {}
@newState ?= {}

View File

@@ -1,8 +1,8 @@
{Point, Range} = require 'text-buffer'
{Model} = require 'theorist'
{pick} = require 'underscore-plus'
{pick} = _ = require 'underscore-plus'
{Emitter} = require 'event-kit'
Grim = require 'grim'
Model = require './model'
NonWhitespaceRegExp = /\S/
@@ -14,7 +14,6 @@ class Selection extends Model
editor: null
initialScreenRange: null
wordwise: false
needsAutoscroll: null
constructor: ({@cursor, @marker, @editor, id}) ->
@emitter = new Emitter
@@ -28,13 +27,16 @@ class Selection extends Model
unless @editor.isDestroyed()
@destroyed = true
@editor.removeSelection(this)
@emit 'destroyed'
@emit 'destroyed' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-destroy'
@emitter.dispose()
destroy: ->
@marker.destroy()
isLastSelection: ->
this is @editor.getLastSelection()
###
Section: Event Subscription
###
@@ -61,16 +63,6 @@ class Selection extends Model
onDidDestroy: (callback) ->
@emitter.on 'did-destroy', callback
on: (eventName) ->
switch eventName
when 'screen-range-changed'
Grim.deprecate("Use Selection::onDidChangeRange instead. Call ::getScreenRange() yourself in your callback if you need the range.")
when 'destroyed'
Grim.deprecate("Use Selection::onDidDestroy instead.")
super
###
Section: Managing the selection range
###
@@ -92,21 +84,22 @@ class Selection extends Model
# Public: Modifies the buffer {Range} for the selection.
#
# * `screenRange` The new {Range} to select.
# * `bufferRange` The new {Range} to select.
# * `options` (optional) {Object} with the keys:
# * `preserveFolds` if `true`, the fold settings are preserved after the selection moves.
# * `autoscroll` if `true`, the {TextEditor} scrolls to the new selection.
# * `preserveFolds` if `true`, the fold settings are preserved after the
# selection moves.
# * `autoscroll` {Boolean} indicating whether to autoscroll to the new
# range. Defaults to `true` if this is the most recently added selection,
# `false` otherwise.
setBufferRange: (bufferRange, options={}) ->
bufferRange = Range.fromObject(bufferRange)
@needsAutoscroll = options.autoscroll
options.reversed ?= @isReversed()
@editor.destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds
@editor.destroyFoldsContainingBufferRange(bufferRange) unless options.preserveFolds
@modifySelection =>
needsFlash = options.flash
delete options.flash if options.flash?
@cursor.needsAutoscroll = false if @needsAutoscroll?
@marker.setBufferRange(bufferRange, options)
@autoscroll() if @needsAutoscroll and @editor.manageScrollPosition
@autoscroll() if options?.autoscroll ? @isLastSelection()
@decoration.flash('flash', @editor.selectionFlashDuration) if needsFlash
# Public: Returns the starting and ending buffer rows the selection is
@@ -117,7 +110,7 @@ class Selection extends Model
range = @getBufferRange()
start = range.start.row
end = range.end.row
end = Math.max(start, end - 1) if range.end.column == 0
end = Math.max(start, end - 1) if range.end.column is 0
[start, end]
getTailScreenPosition: ->
@@ -182,9 +175,15 @@ class Selection extends Model
###
# Public: Clears the selection, moving the marker to the head.
clear: ->
@marker.setProperties(goalBufferRange: null)
#
# * `options` (optional) {Object} with the following keys:
# * `autoscroll` {Boolean} indicating whether to autoscroll to the new
# range. Defaults to `true` if this is the most recently added selection,
# `false` otherwise.
clear: (options) ->
@marker.setProperties(goalScreenRange: null)
@marker.clearTail() unless @retainSelection
@autoscroll() if options?.autoscroll ? @isLastSelection()
@finalize()
# Public: Selects the text from the current cursor position to a given screen
@@ -365,7 +364,6 @@ class Selection extends Model
@editor.unfoldBufferRow(oldBufferRange.end.row)
wasReversed = @isReversed()
@clear()
@cursor.needsAutoscroll = @cursor.isLastCursor()
autoIndentFirstLine = false
precedingText = @editor.getTextInRange([[oldBufferRange.start.row, 0], oldBufferRange.start])
@@ -376,7 +374,7 @@ class Selection extends Model
indentAdjustment = @editor.indentLevelForLine(precedingText) - options.indentBasis
@adjustIndent(remainingLines, indentAdjustment)
if options.autoIndent and not NonWhitespaceRegExp.test(precedingText)
if options.autoIndent and not NonWhitespaceRegExp.test(precedingText) and remainingLines.length > 0
autoIndentFirstLine = true
firstLine = precedingText + firstInsertedLine
desiredIndentLevel = @editor.languageMode.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine)
@@ -391,12 +389,12 @@ class Selection extends Model
if options.select
@setBufferRange(newBufferRange, reversed: wasReversed)
else
@cursor.setBufferPosition(newBufferRange.end, skipAtomicTokens: true) if wasReversed
@cursor.setBufferPosition(newBufferRange.end, clip: 'forward') if wasReversed
if autoIndentFirstLine
@editor.setIndentationForBufferRow(oldBufferRange.start.row, desiredIndentLevel)
if options.autoIndentNewline and text == '\n'
if options.autoIndentNewline and text is '\n'
currentIndentation = @editor.indentationForBufferRow(newBufferRange.start.row)
@editor.autoIndentBufferRow(newBufferRange.end.row, preserveLeadingWhitespace: true, skipBlankLines: false)
if @editor.indentationForBufferRow(newBufferRange.end.row) < currentIndentation
@@ -404,6 +402,8 @@ class Selection extends Model
else if options.autoDecreaseIndent and NonWhitespaceRegExp.test(text)
@editor.autoDecreaseIndentForBufferRow(newBufferRange.start.row)
@autoscroll() if @isLastSelection()
newBufferRange
# Public: Removes the first character before the selection if the selection
@@ -412,15 +412,19 @@ class Selection extends Model
@selectLeft() if @isEmpty() and not @editor.isFoldedAtScreenRow(@cursor.getScreenRow())
@deleteSelectedText()
# Deprecated: Use {::deleteToBeginningOfWord} instead.
backspaceToBeginningOfWord: ->
deprecate("Use Selection::deleteToBeginningOfWord() instead")
@deleteToBeginningOfWord()
# Public: Removes the selection or, if nothing is selected, then all
# characters from the start of the selection back to the previous word
# boundary.
deleteToPreviousWordBoundary: ->
@selectToPreviousWordBoundary() if @isEmpty()
@deleteSelectedText()
# Deprecated: Use {::deleteToBeginningOfLine} instead.
backspaceToBeginningOfLine: ->
deprecate("Use Selection::deleteToBeginningOfLine() instead")
@deleteToBeginningOfLine()
# Public: Removes the selection or, if nothing is selected, then all
# characters from the start of the selection up to the next word
# boundary.
deleteToNextWordBoundary: ->
@selectToNextWordBoundary() if @isEmpty()
@deleteSelectedText()
# Public: Removes from the start of the selection to the beginning of the
# current word if the selection is empty otherwise it deletes the selection.
@@ -556,6 +560,7 @@ class Selection extends Model
for row in [start..end]
if matchLength = buffer.lineForRow(row).match(leadingTabRegex)?[0].length
buffer.delete [[row, 0], [row, matchLength]]
return
# Public: Sets the indentation level of all selected rows to values suggested
# by the relevant grammars.
@@ -630,14 +635,15 @@ class Selection extends Model
# of levels. Leaves the first line unchanged.
adjustIndent: (lines, indentAdjustment) ->
for line, i in lines
if indentAdjustment == 0 or line is ''
if indentAdjustment is 0 or line is ''
continue
else if indentAdjustment > 0
lines[i] = @editor.buildIndentString(indentAdjustment) + line
else
currentIndentLevel = @editor.indentLevelForLine(lines[i])
indentLevel = Math.max(0, currentIndentLevel + indentAdjustment)
lines[i] = line.replace(/^[\t ]+/, @editor.buildIndentString(indentLevel))
lines[i] = line.replace(/^(\t+| +)/, @editor.buildIndentString(indentLevel))
return
# Indent the current line(s).
#
@@ -648,8 +654,8 @@ class Selection extends Model
# * `options` (optional) {Object} with the keys:
# * `autoIndent` If `true`, the line is indented to an automatically-inferred
# level. Otherwise, {TextEditor::getTabText} is inserted.
indent: ({ autoIndent }={}) ->
{ row, column } = @cursor.getBufferPosition()
indent: ({autoIndent}={}) ->
{row, column} = @cursor.getBufferPosition()
if @isEmpty()
@cursor.skipLeadingWhitespace()
@@ -668,7 +674,8 @@ class Selection extends Model
indentSelectedRows: ->
[start, end] = @getBufferRowRange()
for row in [start..end]
@editor.buffer.insert([row, 0], @editor.getTabText()) unless @editor.buffer.lineLengthForRow(row) == 0
@editor.buffer.insert([row, 0], @editor.getTabText()) unless @editor.buffer.lineLengthForRow(row) is 0
return
###
Section: Managing multiple selections
@@ -676,53 +683,59 @@ class Selection extends Model
# Public: Moves the selection down one row.
addSelectionBelow: ->
range = (@getGoalBufferRange() ? @getBufferRange()).copy()
range = (@getGoalScreenRange() ? @getScreenRange()).copy()
nextRow = range.end.row + 1
for row in [nextRow..@editor.getLastBufferRow()]
for row in [nextRow..@editor.getLastScreenRow()]
range.start.row = row
range.end.row = row
clippedRange = @editor.clipBufferRange(range)
clippedRange = @editor.clipScreenRange(range, skipSoftWrapIndentation: true)
if range.isEmpty()
continue if range.end.column > 0 and clippedRange.end.column is 0
else
continue if clippedRange.isEmpty()
@editor.addSelectionForBufferRange(range, goalBufferRange: range)
@editor.addSelectionForScreenRange(clippedRange, goalScreenRange: range)
break
return
# Public: Moves the selection up one row.
addSelectionAbove: ->
range = (@getGoalBufferRange() ? @getBufferRange()).copy()
range = (@getGoalScreenRange() ? @getScreenRange()).copy()
previousRow = range.end.row - 1
for row in [previousRow..0]
range.start.row = row
range.end.row = row
clippedRange = @editor.clipBufferRange(range)
clippedRange = @editor.clipScreenRange(range, skipSoftWrapIndentation: true)
if range.isEmpty()
continue if range.end.column > 0 and clippedRange.end.column is 0
else
continue if clippedRange.isEmpty()
@editor.addSelectionForBufferRange(range, goalBufferRange: range)
@editor.addSelectionForScreenRange(clippedRange, goalScreenRange: range)
break
return
# Public: Combines the given selection into this selection and then destroys
# the given selection.
#
# * `otherSelection` A {Selection} to merge with.
# * `options` (optional) {Object} options matching those found in {::setBufferRange}.
merge: (otherSelection, options) ->
myGoalBufferRange = @getGoalBufferRange()
otherGoalBufferRange = otherSelection.getGoalBufferRange()
if myGoalBufferRange? and otherGoalBufferRange?
options.goalBufferRange = myGoalBufferRange.union(otherGoalBufferRange)
myGoalScreenRange = @getGoalScreenRange()
otherGoalScreenRange = otherSelection.getGoalScreenRange()
if myGoalScreenRange? and otherGoalScreenRange?
options.goalScreenRange = myGoalScreenRange.union(otherGoalScreenRange)
else
options.goalBufferRange = myGoalBufferRange ? otherGoalBufferRange
@setBufferRange(@getBufferRange().union(otherSelection.getBufferRange()), options)
options.goalScreenRange = myGoalScreenRange ? otherGoalScreenRange
@setBufferRange(@getBufferRange().union(otherSelection.getBufferRange()), _.extend(autoscroll: false, options))
otherSelection.destroy()
###
@@ -753,7 +766,7 @@ class Selection extends Model
newScreenRange: @getScreenRange()
selection: this
@emit 'screen-range-changed', @getScreenRange() # old event
@emit 'screen-range-changed', @getScreenRange() if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change-range'
@editor.selectionRangeChanged(eventObject)
@@ -764,10 +777,12 @@ class Selection extends Model
@linewise = false
autoscroll: ->
@editor.scrollToScreenRange(@getScreenRange())
if @marker.hasTail()
@editor.scrollToScreenRange(@getScreenRange(), reversed: @isReversed())
else
@cursor.autoscroll()
clearAutoscroll: ->
@needsAutoscroll = null
modifySelection: (fn) ->
@retainSelection = true
@@ -783,6 +798,28 @@ class Selection extends Model
plantTail: ->
@marker.plantTail()
getGoalBufferRange: ->
if goalBufferRange = @marker.getProperties().goalBufferRange
Range.fromObject(goalBufferRange)
getGoalScreenRange: ->
if goalScreenRange = @marker.getProperties().goalScreenRange
Range.fromObject(goalScreenRange)
if Grim.includeDeprecatedAPIs
Selection::on = (eventName) ->
switch eventName
when 'screen-range-changed'
Grim.deprecate("Use Selection::onDidChangeRange instead. Call ::getScreenRange() yourself in your callback if you need the range.")
when 'destroyed'
Grim.deprecate("Use Selection::onDidDestroy instead.")
else
Grim.deprecate("Selection::on is deprecated. Use documented event subscription methods instead.")
super
# Deprecated: Use {::deleteToBeginningOfWord} instead.
Selection::backspaceToBeginningOfWord = ->
deprecate("Use Selection::deleteToBeginningOfWord() instead")
@deleteToBeginningOfWord()
# Deprecated: Use {::deleteToBeginningOfLine} instead.
Selection::backspaceToBeginningOfLine = ->
deprecate("Use Selection::deleteToBeginningOfLine() instead")
@deleteToBeginningOfLine()

View File

@@ -0,0 +1,6 @@
module.exports = {
SoftTab: Symbol('SoftTab')
HardTab: Symbol('HardTab')
PairedCharacter: Symbol('PairedCharacter')
SoftWrapIndent: Symbol('SoftWrapIndent')
}

27
src/storage-folder.coffee Normal file
View File

@@ -0,0 +1,27 @@
path = require "path"
fs = require "fs-plus"
module.exports =
class StorageFolder
constructor: (containingPath) ->
@path = path.join(containingPath, "storage")
store: (name, object) ->
fs.writeFileSync(@pathForKey(name), JSON.stringify(object), 'utf8')
load: (name) ->
statePath = @pathForKey(name)
try
stateString = fs.readFileSync(statePath, 'utf8')
catch error
unless error.code is 'ENOENT'
console.warn "Error reading state file: #{statePath}", error.stack, error
return undefined
try
JSON.parse(stateString)
catch error
console.warn "Error parsing state file: #{statePath}", error.stack, error
pathForKey: (name) -> path.join(@getPath(), name)
getPath: -> @path

View File

@@ -152,6 +152,8 @@ class StyleManager
for styleElement in styleElementsToRestore
@addStyleElement(styleElement) unless styleElement in existingStyleElements
return
###
Section: Paths
###

View File

@@ -46,6 +46,7 @@ class StylesElement extends HTMLElement
@styleElementRemoved(child) for child in Array::slice.call(@children)
@context = @getAttribute('context')
@styleElementAdded(styleElement) for styleElement in atom.styles.getStyleElements()
return
styleElementAdded: (styleElement) ->
return unless @styleElementMatchesContext(styleElement)

View File

@@ -83,7 +83,7 @@ class Task
taskPath = taskPath.replace(/\\/g, "\\\\")
env = _.extend({}, process.env, {taskPath, userAgent: navigator.userAgent})
@childProcess = fork '--eval', [bootstrap], {env, cwd: __dirname}
@childProcess = fork '--eval', [bootstrap], {env, silent: true}
@on "task:log", -> console.log(arguments...)
@on "task:warn", -> console.warn(arguments...)
@@ -101,6 +101,12 @@ class Task
@childProcess.on 'message', ({event, args}) =>
@emit(event, args...) if @childProcess?
# Catch the errors that happened before task-bootstrap.
@childProcess.stdout.removeAllListeners()
@childProcess.stdout.on 'data', (data) -> console.log data.toString()
@childProcess.stderr.removeAllListeners()
@childProcess.stderr.on 'data', (data) -> console.error data.toString()
# Public: Starts the task.
#
# Throws an error if this task has already been terminated or if sending a
@@ -144,10 +150,18 @@ class Task
#
# No more events are emitted once this method is called.
terminate: ->
return unless @childProcess?
return false unless @childProcess?
@childProcess.removeAllListeners()
@childProcess.stdout.removeAllListeners()
@childProcess.stderr.removeAllListeners()
@childProcess.kill()
@childProcess = null
undefined
true
cancel: ->
didForcefullyTerminate = @terminate()
if didForcefullyTerminate
@emit('task:cancelled')
didForcefullyTerminate

View File

@@ -6,18 +6,19 @@ grim = require 'grim'
ipc = require 'ipc'
TextEditorPresenter = require './text-editor-presenter'
GutterComponent = require './gutter-component'
GutterContainerComponent = require './gutter-container-component'
InputComponent = require './input-component'
LinesComponent = require './lines-component'
ScrollbarComponent = require './scrollbar-component'
ScrollbarCornerComponent = require './scrollbar-corner-component'
OverlayManager = require './overlay-manager'
module.exports =
class TextEditorComponent
scrollSensitivity: 0.4
cursorBlinkPeriod: 800
cursorBlinkResumeDelay: 100
lineOverdrawMargin: 15
tileSize: 12
pendingScrollTop: null
pendingScrollLeft: null
@@ -35,11 +36,10 @@ class TextEditorComponent
gutterComponent: null
mounted: true
constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, lineOverdrawMargin}) ->
@lineOverdrawMargin = lineOverdrawMargin if lineOverdrawMargin?
constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, tileSize}) ->
@tileSize = tileSize if tileSize?
@disposables = new CompositeDisposable
@editor.manageScrollPosition = true
@observeConfig()
@setScrollSensitivity(atom.config.get('editor.scrollSensitivity'))
@@ -47,7 +47,7 @@ class TextEditorComponent
model: @editor
scrollTop: @editor.getScrollTop()
scrollLeft: @editor.getScrollLeft()
lineOverdrawMargin: lineOverdrawMargin
tileSize: tileSize
cursorBlinkPeriod: @cursorBlinkPeriod
cursorBlinkResumeDelay: @cursorBlinkResumeDelay
stoppedScrollingDelay: 200
@@ -57,29 +57,35 @@ class TextEditorComponent
@domNode = document.createElement('div')
if @useShadowDOM
@domNode.classList.add('editor-contents--private')
insertionPoint = document.createElement('content')
insertionPoint.setAttribute('select', 'atom-overlay')
@domNode.appendChild(insertionPoint)
@overlayManager = new OverlayManager(@presenter, @hostElement)
else
@domNode.classList.add('editor-contents')
@overlayManager = new OverlayManager(@presenter, @domNode)
@scrollViewNode = document.createElement('div')
@scrollViewNode.classList.add('scroll-view')
@domNode.appendChild(@scrollViewNode)
@mountGutterComponent() if @presenter.getState().gutter.visible
@mountGutterContainerComponent() if @presenter.getState().gutters.length
@hiddenInputComponent = new InputComponent
@scrollViewNode.appendChild(@hiddenInputComponent.domNode)
@scrollViewNode.appendChild(@hiddenInputComponent.getDomNode())
@linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM})
@scrollViewNode.appendChild(@linesComponent.domNode)
@scrollViewNode.appendChild(@linesComponent.getDomNode())
@horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll})
@scrollViewNode.appendChild(@horizontalScrollbarComponent.domNode)
@scrollViewNode.appendChild(@horizontalScrollbarComponent.getDomNode())
@verticalScrollbarComponent = new ScrollbarComponent({orientation: 'vertical', onScroll: @onVerticalScroll})
@domNode.appendChild(@verticalScrollbarComponent.domNode)
@domNode.appendChild(@verticalScrollbarComponent.getDomNode())
@scrollbarCornerComponent = new ScrollbarCornerComponent
@domNode.appendChild(@scrollbarCornerComponent.domNode)
@domNode.appendChild(@scrollbarCornerComponent.getDomNode())
@observeEditor()
@listenForDOMEvents()
@@ -89,7 +95,7 @@ class TextEditorComponent
@disposables.add @stylesElement.onDidRemoveStyleElement @onStylesheetsChanged
unless atom.themes.isInitialLoadComplete()
@disposables.add atom.themes.onDidChangeActiveThemes @onAllThemesLoaded
@disposables.add scrollbarStyle.changes.onValue @refreshScrollbars
@disposables.add scrollbarStyle.onDidChangePreferredScrollbarStyle @refreshScrollbars
@disposables.add atom.views.pollDocument(@pollDOM)
@@ -100,8 +106,12 @@ class TextEditorComponent
@mounted = false
@disposables.dispose()
@presenter.destroy()
@gutterContainerComponent?.destroy()
window.removeEventListener 'resize', @requestHeightAndWidthMeasurement
getDomNode: ->
@domNode
updateSync: ->
@oldState ?= {}
@newState = @presenter.getState()
@@ -111,7 +121,7 @@ class TextEditorComponent
@cursorMoved = false
@selectionChanged = false
if @editor.getLastSelection()? and !@editor.getLastSelection().isEmpty()
if @editor.getLastSelection()? and not @editor.getLastSelection().isEmpty()
@domNode.classList.add('has-selection')
else
@domNode.classList.remove('has-selection')
@@ -128,12 +138,12 @@ class TextEditorComponent
else
@domNode.style.height = ''
if @newState.gutter.visible
@mountGutterComponent() unless @gutterComponent?
@gutterComponent.updateSync(@newState)
if @newState.gutters.length
@mountGutterContainerComponent() unless @gutterContainerComponent?
@gutterContainerComponent.updateSync(@newState)
else
@gutterComponent?.domNode?.remove()
@gutterComponent = null
@gutterContainerComponent?.getDomNode()?.remove()
@gutterContainerComponent = null
@hiddenInputComponent.updateSync(@newState)
@linesComponent.updateSync(@newState)
@@ -141,26 +151,31 @@ class TextEditorComponent
@verticalScrollbarComponent.updateSync(@newState)
@scrollbarCornerComponent.updateSync(@newState)
@overlayManager?.render(@newState)
if @editor.isAlive()
@updateParentViewFocusedClassIfNeeded()
@updateParentViewMiniClass()
@hostElement.__spacePenView.trigger 'cursor:moved' if cursorMoved
@hostElement.__spacePenView.trigger 'selection:changed' if selectionChanged
@hostElement.__spacePenView.trigger 'editor:display-updated'
if grim.includeDeprecatedAPIs
@hostElement.__spacePenView.trigger 'cursor:moved' if cursorMoved
@hostElement.__spacePenView.trigger 'selection:changed' if selectionChanged
@hostElement.__spacePenView.trigger 'editor:display-updated'
readAfterUpdateSync: =>
@linesComponent.measureCharactersInNewLines() if @isVisible() and not @newState.content.scrollingVertically
@overlayManager?.measureOverlays()
mountGutterComponent: ->
@gutterComponent = new GutterComponent({@editor, onMouseDown: @onGutterMouseDown})
@domNode.insertBefore(@gutterComponent.domNode, @domNode.firstChild)
mountGutterContainerComponent: ->
@gutterContainerComponent = new GutterContainerComponent({@editor, @onLineNumberGutterMouseDown})
@domNode.insertBefore(@gutterContainerComponent.getDomNode(), @domNode.firstChild)
becameVisible: ->
@updatesPaused = true
@measureScrollbars() if @measureScrollbarsWhenShown
@sampleFontStyling()
@sampleBackgroundColors()
@measureHeightAndWidth()
@measureWindowSize()
@measureDimensions()
@measureLineHeightAndDefaultCharWidth() if @measureLineHeightAndDefaultCharWidthWhenShown
@remeasureCharacterWidths() if @remeasureCharacterWidthsWhenShown
@editor.setVisible(true)
@@ -181,7 +196,7 @@ class TextEditorComponent
@updateRequested = true
atom.views.updateDocument =>
@updateRequested = false
@updateSync() if @editor.isAlive()
@updateSync() if @canUpdate()
atom.views.readDocument(@readAfterUpdateSync)
canUpdate: ->
@@ -271,7 +286,7 @@ class TextEditorComponent
focused: ->
if @mounted
@presenter.setFocused(true)
@hiddenInputComponent.domNode.focus()
@hiddenInputComponent.getDomNode().focus()
blurred: ->
if @mounted
@@ -295,8 +310,7 @@ class TextEditorComponent
selectedLength = inputNode.selectionEnd - inputNode.selectionStart
@editor.selectLeft() if selectedLength is 1
insertedRange = @editor.transact atom.config.get('editor.undoGroupingInterval'), =>
@editor.insertText(event.data)
insertedRange = @editor.insertText(event.data, groupUndo: true)
inputNode.value = event.data if insertedRange
onVerticalScroll: (scrollTop) =>
@@ -335,15 +349,15 @@ class TextEditorComponent
if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY)
# Scrolling horizontally
previousScrollLeft = @editor.getScrollLeft()
previousScrollLeft = @presenter.getScrollLeft()
@presenter.setScrollLeft(previousScrollLeft - Math.round(wheelDeltaX * @scrollSensitivity))
event.preventDefault() unless previousScrollLeft is @editor.getScrollLeft()
event.preventDefault() unless previousScrollLeft is @presenter.getScrollLeft()
else
# Scrolling vertically
@presenter.setMouseWheelScreenRow(@screenRowForNode(event.target))
previousScrollTop = @presenter.scrollTop
previousScrollTop = @presenter.getScrollTop()
@presenter.setScrollTop(previousScrollTop - Math.round(wheelDeltaY * @scrollSensitivity))
event.preventDefault() unless previousScrollTop is @editor.getScrollTop()
event.preventDefault() unless previousScrollTop is @presenter.getScrollTop()
onScrollViewScroll: =>
if @mounted
@@ -379,7 +393,11 @@ class TextEditorComponent
if shiftKey
@editor.selectToScreenPosition(screenPosition)
else if metaKey or (ctrlKey and process.platform isnt 'darwin')
@editor.addCursorAtScreenPosition(screenPosition)
cursorAtScreenPosition = @editor.getCursorAtScreenPosition(screenPosition)
if cursorAtScreenPosition and @editor.hasMultipleCursors()
cursorAtScreenPosition.destroy()
else
@editor.addCursorAtScreenPosition(screenPosition)
else
@editor.setCursorScreenPosition(screenPosition)
when 2
@@ -390,7 +408,7 @@ class TextEditorComponent
@handleDragUntilMouseUp event, (screenPosition) =>
@editor.selectToScreenPosition(screenPosition)
onGutterMouseDown: (event) =>
onLineNumberGutterMouseDown: (event) =>
return unless event.button is 0 # only handle the left mouse button
{shiftKey, metaKey, ctrlKey} = event
@@ -404,29 +422,33 @@ class TextEditorComponent
onGutterClick: (event) =>
clickedRow = @screenPositionForMouseEvent(event).row
clickedBufferRow = @editor.bufferRowForScreenRow(clickedRow)
@editor.setSelectedScreenRange([[clickedRow, 0], [clickedRow + 1, 0]], preserveFolds: true)
@editor.setSelectedBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]], preserveFolds: true)
@handleDragUntilMouseUp event, (screenPosition) =>
dragRow = screenPosition.row
if dragRow < clickedRow # dragging up
@editor.setSelectedScreenRange([[dragRow, 0], [clickedRow + 1, 0]], preserveFolds: true)
dragBufferRow = @editor.bufferRowForScreenRow(dragRow)
if dragBufferRow < clickedBufferRow # dragging up
@editor.setSelectedBufferRange([[dragBufferRow, 0], [clickedBufferRow + 1, 0]], preserveFolds: true)
else
@editor.setSelectedScreenRange([[clickedRow, 0], [dragRow + 1, 0]], preserveFolds: true)
@editor.setSelectedBufferRange([[clickedBufferRow, 0], [dragBufferRow + 1, 0]], preserveFolds: true)
onGutterMetaClick: (event) =>
clickedRow = @screenPositionForMouseEvent(event).row
clickedBufferRow = @editor.bufferRowForScreenRow(clickedRow)
bufferRange = @editor.bufferRangeForScreenRange([[clickedRow, 0], [clickedRow + 1, 0]])
bufferRange = new Range([clickedBufferRow, 0], [clickedBufferRow + 1, 0])
rowSelection = @editor.addSelectionForBufferRange(bufferRange, preserveFolds: true)
@handleDragUntilMouseUp event, (screenPosition) =>
dragRow = screenPosition.row
dragBufferRow = @editor.bufferRowForScreenRow(dragRow)
if dragRow < clickedRow # dragging up
rowSelection.setScreenRange([[dragRow, 0], [clickedRow + 1, 0]], preserveFolds: true)
if dragBufferRow < clickedBufferRow # dragging up
rowSelection.setBufferRange([[dragBufferRow, 0], [clickedBufferRow + 1, 0]], preserveFolds: true)
else
rowSelection.setScreenRange([[clickedRow, 0], [dragRow + 1, 0]], preserveFolds: true)
rowSelection.setBufferRange([[clickedBufferRow, 0], [dragBufferRow + 1, 0]], preserveFolds: true)
# After updating the selected screen range, merge overlapping selections
@editor.mergeIntersectingSelections(preserveFolds: true)
@@ -439,19 +461,23 @@ class TextEditorComponent
onGutterShiftClick: (event) =>
clickedRow = @screenPositionForMouseEvent(event).row
clickedBufferRow = @editor.bufferRowForScreenRow(clickedRow)
tailPosition = @editor.getLastSelection().getTailScreenPosition()
tailBufferPosition = @editor.bufferPositionForScreenPosition(tailPosition)
if clickedRow < tailPosition.row
@editor.selectToScreenPosition([clickedRow, 0])
@editor.selectToBufferPosition([clickedBufferRow, 0])
else
@editor.selectToScreenPosition([clickedRow + 1, 0])
@editor.selectToBufferPosition([clickedBufferRow + 1, 0])
@handleDragUntilMouseUp event, (screenPosition) =>
dragRow = screenPosition.row
dragBufferRow = @editor.bufferRowForScreenRow(dragRow)
if dragRow < tailPosition.row # dragging up
@editor.setSelectedScreenRange([[dragRow, 0], tailPosition], preserveFolds: true)
@editor.setSelectedBufferRange([[dragBufferRow, 0], tailBufferPosition], preserveFolds: true)
else
@editor.setSelectedScreenRange([tailPosition, [dragRow + 1, 0]], preserveFolds: true)
@editor.setSelectedBufferRange([tailBufferPosition, [dragBufferRow + 1, 0]], preserveFolds: true)
onStylesheetsChanged: (styleElement) =>
return unless @performedInitialMeasurement
@@ -537,7 +563,7 @@ class TextEditorComponent
pasteSelectionClipboard = (event) =>
if event?.which is 2 and process.platform is 'linux'
if selection = require('clipboard').readText('selection')
if selection = require('./safe-clipboard').readText('selection')
@editor.insertText(selection)
window.addEventListener('mousemove', onMouseMove)
@@ -549,8 +575,9 @@ class TextEditorComponent
pollDOM: =>
unless @checkForVisibilityChange()
@sampleBackgroundColors()
@measureHeightAndWidth()
@measureDimensions()
@sampleFontStyling()
@overlayManager?.measureOverlays()
checkForVisibilityChange: ->
if @isVisible()
@@ -568,13 +595,14 @@ class TextEditorComponent
@heightAndWidthMeasurementRequested = true
requestAnimationFrame =>
@heightAndWidthMeasurementRequested = false
@measureHeightAndWidth()
@measureDimensions()
@measureWindowSize()
# Measure explicitly-styled height and width and relay them to the model. If
# these values aren't explicitly styled, we assume the editor is unconstrained
# and use the scrollHeight / scrollWidth as its height and width in
# calculations.
measureHeightAndWidth: ->
measureDimensions: ->
return unless @mounted
{position} = getComputedStyle(@hostElement)
@@ -595,6 +623,17 @@ class TextEditorComponent
if clientWidth > 0
@presenter.setContentFrameWidth(clientWidth)
@presenter.setGutterWidth(@gutterContainerComponent?.getDomNode().offsetWidth ? 0)
@presenter.setBoundingClientRect(@hostElement.getBoundingClientRect())
measureWindowSize: ->
return unless @mounted
# FIXME: on Ubuntu (via xvfb) `window.innerWidth` reports an incorrect value
# when window gets resized through `atom.setWindowDimensions({width:
# windowWidth, height: windowHeight})`.
@presenter.setWindowSize(window.innerWidth, window.innerHeight)
sampleFontStyling: =>
oldFontSize = @fontSize
oldFontFamily = @fontFamily
@@ -613,8 +652,9 @@ class TextEditorComponent
@presenter.setBackgroundColor(backgroundColor)
if @gutterComponent?
gutterBackgroundColor = getComputedStyle(@gutterComponent.domNode).backgroundColor
lineNumberGutter = @gutterContainerComponent?.getLineNumberGutterComponent()
if lineNumberGutter
gutterBackgroundColor = getComputedStyle(lineNumberGutter.getDomNode()).backgroundColor
@presenter.setGutterBackgroundColor(gutterBackgroundColor)
measureLineHeightAndDefaultCharWidth: ->
@@ -634,7 +674,7 @@ class TextEditorComponent
measureScrollbars: ->
@measureScrollbarsWhenShown = false
cornerNode = @scrollbarCornerComponent.domNode
cornerNode = @scrollbarCornerComponent.getDomNode()
originalDisplayValue = cornerNode.style.display
cornerNode.style.display = 'block'
@@ -660,9 +700,9 @@ class TextEditorComponent
@measureScrollbarsWhenShown = true
return
verticalNode = @verticalScrollbarComponent.domNode
horizontalNode = @horizontalScrollbarComponent.domNode
cornerNode = @scrollbarCornerComponent.domNode
verticalNode = @verticalScrollbarComponent.getDomNode()
horizontalNode = @horizontalScrollbarComponent.getDomNode()
cornerNode = @scrollbarCornerComponent.getDomNode()
originalVerticalDisplayValue = verticalNode.style.display
originalHorizontalDisplayValue = horizontalNode.style.display
@@ -689,9 +729,18 @@ class TextEditorComponent
consolidateSelections: (e) ->
e.abortKeyBinding() unless @editor.consolidateSelections()
lineNodeForScreenRow: (screenRow) -> @linesComponent.lineNodeForScreenRow(screenRow)
lineNodeForScreenRow: (screenRow) ->
tileRow = @presenter.tileForRow(screenRow)
tileComponent = @linesComponent.getComponentForTile(tileRow)
lineNumberNodeForScreenRow: (screenRow) -> @gutterComponent.lineNumberNodeForScreenRow(screenRow)
tileComponent?.lineNodeForScreenRow(screenRow)
lineNumberNodeForScreenRow: (screenRow) ->
tileRow = @presenter.tileForRow(screenRow)
gutterComponent = @gutterContainerComponent.getLineNumberGutterComponent()
tileComponent = gutterComponent.getComponentForTile(tileRow)
tileComponent?.lineNumberNodeForScreenRow(screenRow)
screenRowForNode: (node) ->
while node?
@@ -721,15 +770,6 @@ class TextEditorComponent
setShowIndentGuide: (showIndentGuide) ->
atom.config.set("editor.showIndentGuide", showIndentGuide)
# Deprecated
setInvisibles: (invisibles={}) ->
grim.deprecate "Use config.set('editor.invisibles', invisibles) instead"
atom.config.set('editor.invisibles', invisibles)
# Deprecated
setShowInvisibles: (showInvisibles) ->
atom.config.set('editor.showInvisibles', showInvisibles)
setScrollSensitivity: (scrollSensitivity) =>
if scrollSensitivity = parseInt(scrollSensitivity)
@scrollSensitivity = Math.abs(scrollSensitivity) / 100
@@ -741,9 +781,9 @@ class TextEditorComponent
pixelPositionForMouseEvent: (event) ->
{clientX, clientY} = event
linesClientRect = @linesComponent.domNode.getBoundingClientRect()
top = clientY - linesClientRect.top
left = clientX - linesClientRect.left
linesClientRect = @linesComponent.getDomNode().getBoundingClientRect()
top = clientY - linesClientRect.top + @presenter.scrollTop
left = clientX - linesClientRect.left + @presenter.scrollLeft
{top, left}
getModel: ->
@@ -762,3 +802,12 @@ class TextEditorComponent
updateParentViewMiniClass: ->
@hostElement.classList.toggle('mini', @editor.isMini())
@rootElement.classList.toggle('mini', @editor.isMini())
if grim.includeDeprecatedAPIs
TextEditorComponent::setInvisibles = (invisibles={}) ->
grim.deprecate "Use config.set('editor.invisibles', invisibles) instead"
atom.config.set('editor.invisibles', invisibles)
TextEditorComponent::setShowInvisibles = (showInvisibles) ->
grim.deprecate "Use config.set('editor.showInvisibles', showInvisibles) instead"
atom.config.set('editor.showInvisibles', showInvisibles)

View File

@@ -3,6 +3,7 @@
Path = require 'path'
{defaults} = require 'underscore-plus'
TextBuffer = require 'text-buffer'
Grim = require 'grim'
TextEditor = require './text-editor'
TextEditorComponent = require './text-editor-component'
TextEditorView = null
@@ -14,13 +15,14 @@ class TextEditorElement extends HTMLElement
componentDescriptor: null
component: null
attached: false
lineOverdrawMargin: null
tileSize: null
focusOnAttach: false
hasTiledRendering: true
createdCallback: ->
@emitter = new Emitter
@initializeContent()
@createSpacePenShim()
@createSpacePenShim() if Grim.includeDeprecatedAPIs
@addEventListener 'focus', @focused.bind(this)
@addEventListener 'blur', @blurred.bind(this)
@@ -86,7 +88,7 @@ class TextEditorElement extends HTMLElement
@model.onDidChangeGrammar => @addGrammarScopeAttribute()
@model.onDidChangeEncoding => @addEncodingAttribute()
@model.onDidDestroy => @unmountComponent()
@__spacePenView.setModel(@model)
@__spacePenView.setModel(@model) if Grim.includeDeprecatedAPIs
@model
getModel: ->
@@ -99,7 +101,7 @@ class TextEditorElement extends HTMLElement
tabLength: 2
softTabs: true
mini: @hasAttribute('mini')
gutterVisible: not @hasAttribute('gutter-hidden')
lineNumberGutterVisible: not @hasAttribute('gutter-hidden')
placeholderText: @getAttribute('placeholder-text')
))
@@ -109,15 +111,15 @@ class TextEditorElement extends HTMLElement
rootElement: @rootElement
stylesElement: @stylesElement
editor: @model
lineOverdrawMargin: @lineOverdrawMargin
tileSize: @tileSize
useShadowDOM: @useShadowDOM
)
@rootElement.appendChild(@component.domNode)
@rootElement.appendChild(@component.getDomNode())
if @useShadowDOM
@shadowRoot.addEventListener('blur', @shadowRootBlurred.bind(this), true)
else
inputNode = @component.hiddenInputComponent.domNode
inputNode = @component.hiddenInputComponent.getDomNode()
inputNode.addEventListener 'focus', @focused.bind(this)
inputNode.addEventListener 'blur', => @dispatchEvent(new FocusEvent('blur', bubbles: false))
@@ -125,7 +127,7 @@ class TextEditorElement extends HTMLElement
callRemoveHooks(this)
if @component?
@component.destroy()
@component.domNode.remove()
@component.getDomNode().remove()
@component = null
focused: ->
@@ -133,7 +135,7 @@ class TextEditorElement extends HTMLElement
blurred: (event) ->
unless @useShadowDOM
if event.relatedTarget is @component.hiddenInputComponent.domNode
if event.relatedTarget is @component.hiddenInputComponent.getDomNode()
event.stopImmediatePropagation()
return
@@ -243,8 +245,9 @@ atom.commands.add 'atom-text-editor', stopEventPropagation(
'core:move-right': -> @moveRight()
'core:select-left': -> @selectLeft()
'core:select-right': -> @selectRight()
'core:select-up': -> @selectUp()
'core:select-down': -> @selectDown()
'core:select-all': -> @selectAll()
'editor:move-to-previous-word': -> @moveToPreviousWord()
'editor:select-word': -> @selectWordsContainingCursors()
'editor:consolidate-selections': (event) -> event.abortKeyBinding() unless @consolidateSelections()
'editor:move-to-beginning-of-next-paragraph': -> @moveToBeginningOfNextParagraph()
@@ -282,6 +285,8 @@ atom.commands.add 'atom-text-editor', stopEventPropagationAndGroupUndo(
'core:cut': -> @cutSelectedText()
'core:copy': -> @copySelectedText()
'core:paste': -> @pasteText()
'editor:delete-to-previous-word-boundary': -> @deleteToPreviousWordBoundary()
'editor:delete-to-next-word-boundary': -> @deleteToNextWordBoundary()
'editor:delete-to-beginning-of-word': -> @deleteToBeginningOfWord()
'editor:delete-to-beginning-of-line': -> @deleteToBeginningOfLine()
'editor:delete-to-end-of-line': -> @deleteToEndOfLine()
@@ -302,8 +307,6 @@ atom.commands.add 'atom-text-editor:not([mini])', stopEventPropagation(
'core:move-to-bottom': -> @moveToBottom()
'core:page-up': -> @pageUp()
'core:page-down': -> @pageDown()
'core:select-up': -> @selectUp()
'core:select-down': -> @selectDown()
'core:select-to-top': -> @selectToTop()
'core:select-to-bottom': -> @selectToBottom()
'core:select-page-up': -> @selectPageUp()

File diff suppressed because it is too large Load Diff

View File

@@ -60,7 +60,7 @@ class TextEditorView extends View
placeholderText: placeholderText
element = new TextEditorElement
element.lineOverdrawMargin = props?.lineOverdrawMargin
element.tileSize = props?.tileSize
element.setAttribute(name, value) for name, value of attributes if attributes?
element.setModel(model)
return element.__spacePenView
@@ -120,14 +120,14 @@ class TextEditorView extends View
getEditor: -> @model
Object.defineProperty @::, 'lineHeight', get: -> @model.getLineHeightInPixels()
Object.defineProperty @::, 'charWidth', get: -> @model.getDefaultCharWidth()
Object.defineProperty @::, 'firstRenderedScreenRow', get: -> @component.getRenderedRowRange()[0]
Object.defineProperty @::, 'lastRenderedScreenRow', get: -> @component.getRenderedRowRange()[1]
Object.defineProperty @::, 'active', get: -> @is(@getPaneView()?.activeView)
Object.defineProperty @::, 'isFocused', get: -> document.activeElement is @element or document.activeElement is @element.component?.hiddenInputComponent?.domNode
Object.defineProperty @::, 'mini', get: -> @model?.isMini()
Object.defineProperty @::, 'component', get: -> @element?.component
Object.defineProperty @prototype, 'lineHeight', get: -> @model.getLineHeightInPixels()
Object.defineProperty @prototype, 'charWidth', get: -> @model.getDefaultCharWidth()
Object.defineProperty @prototype, 'firstRenderedScreenRow', get: -> @component.getRenderedRowRange()[0]
Object.defineProperty @prototype, 'lastRenderedScreenRow', get: -> @component.getRenderedRowRange()[1]
Object.defineProperty @prototype, 'active', get: -> @is(@getPaneView()?.activeView)
Object.defineProperty @prototype, 'isFocused', get: -> document.activeElement is @element or document.activeElement is @element.component?.hiddenInputComponent?.getDomNode()
Object.defineProperty @prototype, 'mini', get: -> @model?.isMini()
Object.defineProperty @prototype, 'component', get: -> @element?.component
afterAttach: (onDom) ->
return unless onDom

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,16 @@
path = require 'path'
_ = require 'underscore-plus'
EmitterMixin = require('emissary').Emitter
{Emitter, Disposable} = require 'event-kit'
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
{File} = require 'pathwatcher'
fs = require 'fs-plus'
Q = require 'q'
Grim = require 'grim'
Package = require './package'
# Extended: Handles loading and activating available themes.
#
# An instance of this class is always available as the `atom.themes` global.
module.exports =
class ThemeManager
EmitterMixin.includeInto(this)
constructor: ({@packageManager, @resourcePath, @configDirPath, @safeMode}) ->
@emitter = new Emitter
@styleSheetDisposablesBySourcePath = {}
@@ -33,24 +27,24 @@ class ThemeManager
styleElementAdded: (styleElement) ->
{sheet} = styleElement
@sheetsByStyleElement.set(styleElement, sheet)
@emit 'stylesheet-added', sheet
@emit 'stylesheet-added', sheet if Grim.includeDeprecatedAPIs
@emitter.emit 'did-add-stylesheet', sheet
@emit 'stylesheets-changed'
@emit 'stylesheets-changed' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change-stylesheets'
styleElementRemoved: (styleElement) ->
sheet = @sheetsByStyleElement.get(styleElement)
@emit 'stylesheet-removed', sheet
@emit 'stylesheet-removed', sheet if Grim.includeDeprecatedAPIs
@emitter.emit 'did-remove-stylesheet', sheet
@emit 'stylesheets-changed'
@emit 'stylesheets-changed' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change-stylesheets'
styleElementUpdated: ({sheet}) ->
@emit 'stylesheet-removed', sheet
@emit 'stylesheet-removed', sheet if Grim.includeDeprecatedAPIs
@emitter.emit 'did-remove-stylesheet', sheet
@emit 'stylesheet-added', sheet
@emit 'stylesheet-added', sheet if Grim.includeDeprecatedAPIs
@emitter.emit 'did-add-stylesheet', sheet
@emit 'stylesheets-changed'
@emit 'stylesheets-changed' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change-stylesheets'
###
@@ -65,65 +59,6 @@ class ThemeManager
@emitter.on 'did-change-active-themes', callback
@emitter.on 'did-reload-all', callback # TODO: Remove once deprecated pre-1.0 APIs are gone
onDidReloadAll: (callback) ->
Grim.deprecate("Use `::onDidChangeActiveThemes` instead.")
@onDidChangeActiveThemes(callback)
# Deprecated: Invoke `callback` when a stylesheet has been added to the dom.
#
# * `callback` {Function}
# * `stylesheet` {StyleSheet} the style node
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidAddStylesheet: (callback) ->
Grim.deprecate("Use atom.styles.onDidAddStyleElement instead")
@emitter.on 'did-add-stylesheet', callback
# Deprecated: Invoke `callback` when a stylesheet has been removed from the dom.
#
# * `callback` {Function}
# * `stylesheet` {StyleSheet} the style node
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidRemoveStylesheet: (callback) ->
Grim.deprecate("Use atom.styles.onDidRemoveStyleElement instead")
@emitter.on 'did-remove-stylesheet', callback
# Deprecated: Invoke `callback` when a stylesheet has been updated.
#
# * `callback` {Function}
# * `stylesheet` {StyleSheet} the style node
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidUpdateStylesheet: (callback) ->
Grim.deprecate("Use atom.styles.onDidUpdateStyleElement instead")
@emitter.on 'did-update-stylesheet', callback
# Deprecated: Invoke `callback` when any stylesheet has been updated, added, or removed.
#
# * `callback` {Function}
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeStylesheets: (callback) ->
Grim.deprecate("Use atom.styles.onDidAdd/RemoveStyleElement instead")
@emitter.on 'did-change-stylesheets', callback
on: (eventName) ->
switch eventName
when 'reloaded'
Grim.deprecate 'Use ThemeManager::onDidChangeActiveThemes instead'
when 'stylesheet-added'
Grim.deprecate 'Use ThemeManager::onDidAddStylesheet instead'
when 'stylesheet-removed'
Grim.deprecate 'Use ThemeManager::onDidRemoveStylesheet instead'
when 'stylesheet-updated'
Grim.deprecate 'Use ThemeManager::onDidUpdateStylesheet instead'
when 'stylesheets-changed'
Grim.deprecate 'Use ThemeManager::onDidChangeStylesheets instead'
else
Grim.deprecate 'ThemeManager::on is deprecated. Use event subscription methods instead.'
EmitterMixin::on.apply(this, arguments)
###
Section: Accessing Available Themes
###
@@ -140,10 +75,6 @@ class ThemeManager
getLoadedThemeNames: ->
theme.name for theme in @getLoadedThemes()
getLoadedNames: ->
Grim.deprecate("Use `::getLoadedThemeNames` instead.")
@getLoadedThemeNames()
# Public: Get an array of all the loaded themes.
getLoadedThemes: ->
pack for pack in @packageManager.getLoadedPackages() when pack.isTheme()
@@ -156,10 +87,6 @@ class ThemeManager
getActiveThemeNames: ->
theme.name for theme in @getActiveThemes()
getActiveNames: ->
Grim.deprecate("Use `::getActiveThemeNames` instead.")
@getActiveThemeNames()
# Public: Get an array of all the active themes.
getActiveThemes: ->
pack for pack in @packageManager.getActivePackages() when pack.isTheme()
@@ -208,22 +135,10 @@ class ThemeManager
# the first/top theme to override later themes in the stack.
themeNames.reverse()
# Set the list of enabled themes.
#
# * `enabledThemeNames` An {Array} of {String} theme names.
setEnabledThemes: (enabledThemeNames) ->
Grim.deprecate("Use `atom.config.set('core.themes', arrayOfThemeNames)` instead")
atom.config.set('core.themes', enabledThemeNames)
###
Section: Private
###
# Returns the {String} path to the user's stylesheet under ~/.atom
getUserStylesheetPath: ->
Grim.deprecate("Call atom.styles.getUserStyleSheetPath() instead")
atom.styles.getUserStyleSheetPath()
# Resolve and apply the stylesheet specified by the path.
#
# This supports both CSS and Less stylsheets.
@@ -241,7 +156,8 @@ class ThemeManager
throw new Error("Could not find a file at path '#{stylesheetPath}'")
unwatchUserStylesheet: ->
@userStylesheetFile?.off()
@userStylsheetSubscriptions?.dispose()
@userStylsheetSubscriptions = null
@userStylesheetFile = null
@userStyleSheetDisposable?.dispose()
@userStyleSheetDisposable = null
@@ -254,7 +170,11 @@ class ThemeManager
try
@userStylesheetFile = new File(userStylesheetPath)
@userStylesheetFile.on 'contents-changed moved removed', => @loadUserStylesheet()
@userStylsheetSubscriptions = new CompositeDisposable()
reloadStylesheet = => @loadUserStylesheet()
@userStylsheetSubscriptions.add(@userStylesheetFile.onDidChange(reloadStylesheet))
@userStylsheetSubscriptions.add(@userStylesheetFile.onDidRename(reloadStylesheet))
@userStylsheetSubscriptions.add(@userStylesheetFile.onDidDelete(reloadStylesheet))
catch error
message = """
Unable to watch path: `#{path.basename(userStylesheetPath)}`. Make sure
@@ -313,7 +233,11 @@ class ThemeManager
else
@lessCache.read(lessStylesheetPath)
catch error
error.less = true
if error.line?
# Adjust line numbers for import fallbacks
error.line -= 2 if importFallbackVariables
message = "Error compiling Less stylesheet: `#{lessStylesheetPath}`"
detail = """
Line number: #{error.line}
@@ -357,7 +281,7 @@ class ThemeManager
@loadUserStylesheet()
@reloadBaseStylesheets()
@initialLoadComplete = true
@emit 'reloaded'
@emit 'reloaded' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change-active-themes'
deferred.resolve()
@@ -401,3 +325,59 @@ class ThemeManager
themePaths.push(path.join(themePath, 'styles'))
themePaths.filter (themePath) -> fs.isDirectorySync(themePath)
if Grim.includeDeprecatedAPIs
EmitterMixin = require('emissary').Emitter
EmitterMixin.includeInto(ThemeManager)
ThemeManager::on = (eventName) ->
switch eventName
when 'reloaded'
Grim.deprecate 'Use ThemeManager::onDidChangeActiveThemes instead'
when 'stylesheet-added'
Grim.deprecate 'Use ThemeManager::onDidAddStylesheet instead'
when 'stylesheet-removed'
Grim.deprecate 'Use ThemeManager::onDidRemoveStylesheet instead'
when 'stylesheet-updated'
Grim.deprecate 'Use ThemeManager::onDidUpdateStylesheet instead'
when 'stylesheets-changed'
Grim.deprecate 'Use ThemeManager::onDidChangeStylesheets instead'
else
Grim.deprecate 'ThemeManager::on is deprecated. Use event subscription methods instead.'
EmitterMixin::on.apply(this, arguments)
ThemeManager::onDidReloadAll = (callback) ->
Grim.deprecate("Use `::onDidChangeActiveThemes` instead.")
@onDidChangeActiveThemes(callback)
ThemeManager::onDidAddStylesheet = (callback) ->
Grim.deprecate("Use atom.styles.onDidAddStyleElement instead")
@emitter.on 'did-add-stylesheet', callback
ThemeManager::onDidRemoveStylesheet = (callback) ->
Grim.deprecate("Use atom.styles.onDidRemoveStyleElement instead")
@emitter.on 'did-remove-stylesheet', callback
ThemeManager::onDidUpdateStylesheet = (callback) ->
Grim.deprecate("Use atom.styles.onDidUpdateStyleElement instead")
@emitter.on 'did-update-stylesheet', callback
ThemeManager::onDidChangeStylesheets = (callback) ->
Grim.deprecate("Use atom.styles.onDidAdd/RemoveStyleElement instead")
@emitter.on 'did-change-stylesheets', callback
ThemeManager::getUserStylesheetPath = ->
Grim.deprecate("Call atom.styles.getUserStyleSheetPath() instead")
atom.styles.getUserStyleSheetPath()
ThemeManager::getLoadedNames = ->
Grim.deprecate("Use `::getLoadedThemeNames` instead.")
@getLoadedThemeNames()
ThemeManager::getActiveNames = ->
Grim.deprecate("Use `::getActiveThemeNames` instead.")
@getActiveThemeNames()
ThemeManager::setEnabledThemes = (enabledThemeNames) ->
Grim.deprecate("Use `atom.config.set('core.themes', arrayOfThemeNames)` instead")
atom.config.set('core.themes', enabledThemeNames)

View File

@@ -14,11 +14,7 @@ class ThemePackage extends Package
atom.config.removeAtKeyPath('core.themes', @name)
load: ->
@measure 'loadTime', =>
try
@metadata ?= Package.loadMetadata(@path)
catch error
console.warn "Failed to load theme named '#{@name}'", error.stack ? error
@loadTime = 0
this
activate: ->
@@ -26,7 +22,10 @@ class ThemePackage extends Package
@activationDeferred = Q.defer()
@measure 'activateTime', =>
@loadStylesheets()
@activateNow()
try
@loadStylesheets()
@activateNow()
catch error
@handleError("Failed to activate the #{@name} theme", error)
@activationDeferred.promise

View File

@@ -0,0 +1,51 @@
cloneObject = (object) ->
clone = {}
clone[key] = value for key, value of object
clone
module.exports =
class TiledComponent
updateSync: (state) ->
@newState = @getNewState(state)
@oldState ?= @buildEmptyState()
@beforeUpdateSync?(state)
@removeTileNodes() if @shouldRecreateAllTilesOnUpdate?()
@updateTileNodes()
@afterUpdateSync?(state)
removeTileNodes: ->
@removeTileNode(tileRow) for tileRow of @oldState.tiles
return
removeTileNode: (tileRow) ->
node = @componentsByTileId[tileRow].getDomNode()
node.remove()
delete @componentsByTileId[tileRow]
delete @oldState.tiles[tileRow]
updateTileNodes: ->
@componentsByTileId ?= {}
for tileRow of @oldState.tiles
unless @newState.tiles.hasOwnProperty(tileRow)
@removeTileNode(tileRow)
for tileRow, tileState of @newState.tiles
if @oldState.tiles.hasOwnProperty(tileRow)
component = @componentsByTileId[tileRow]
else
component = @componentsByTileId[tileRow] = @buildComponentForTile(tileRow)
@getTilesNode().appendChild(component.getDomNode())
@oldState.tiles[tileRow] = cloneObject(tileState)
component.updateSync(@newState)
return
getComponentForTile: (tileRow) ->
@componentsByTileId[tileRow]

83
src/token-iterator.coffee Normal file
View File

@@ -0,0 +1,83 @@
{SoftTab, HardTab, PairedCharacter, SoftWrapIndent} = require './special-token-symbols'
module.exports =
class TokenIterator
constructor: (line) ->
@reset(line) if line?
reset: (@line) ->
@index = null
@bufferStart = @line.startBufferColumn
@bufferEnd = @bufferStart
@screenStart = 0
@screenEnd = 0
@scopes = @line.openScopes.map (id) -> atom.grammars.scopeForId(id)
@scopeStarts = @scopes.slice()
@scopeEnds = []
this
next: ->
{tags} = @line
if @index?
@index++
@scopeEnds.length = 0
@scopeStarts.length = 0
@bufferStart = @bufferEnd
@screenStart = @screenEnd
else
@index = 0
while @index < tags.length
tag = tags[@index]
if tag < 0
if tag % 2 is 0
@scopeEnds.push(atom.grammars.scopeForId(tag + 1))
@scopes.pop()
else
scope = atom.grammars.scopeForId(tag)
@scopeStarts.push(scope)
@scopes.push(scope)
@index++
else
if @isHardTab()
@screenEnd = @screenStart + tag
@bufferEnd = @bufferStart + 1
else if @isSoftWrapIndentation()
@screenEnd = @screenStart + tag
@bufferEnd = @bufferStart + 0
else
@screenEnd = @screenStart + tag
@bufferEnd = @bufferStart + tag
return true
false
getBufferStart: -> @bufferStart
getBufferEnd: -> @bufferEnd
getScreenStart: -> @screenStart
getScreenEnd: -> @screenEnd
getScopeStarts: -> @scopeStarts
getScopeEnds: -> @scopeEnds
getScopes: -> @scopes
getText: ->
@line.text.substring(@screenStart, @screenEnd)
isSoftTab: ->
@line.specialTokens[@index] is SoftTab
isHardTab: ->
@line.specialTokens[@index] is HardTab
isSoftWrapIndentation: ->
@line.specialTokens[@index] is SoftWrapIndent
isPairedCharacter: ->
@line.specialTokens[@index] is PairedCharacter
isAtomic: ->
@isSoftTab() or @isHardTab() or @isSoftWrapIndentation() or @isPairedCharacter()

View File

@@ -1,14 +1,8 @@
_ = require 'underscore-plus'
{deprecate} = require 'grim'
textUtils = require './text-utils'
WhitespaceRegexesByTabLength = {}
EscapeRegex = /[&"'<>]/g
StartDotRegex = /^\.?/
WhitespaceRegex = /\S/
MaxTokenLength = 20000
# Represents a single unit of text as selected by a grammar.
module.exports =
class Token
@@ -21,138 +15,22 @@ class Token
firstTrailingWhitespaceIndex: null
hasInvisibleCharacters: false
constructor: ({@value, @scopes, @isAtomic, @bufferDelta, @isHardTab, @hasPairedCharacter, @isSoftWrapIndentation}) ->
constructor: (properties) ->
{@value, @scopes, @isAtomic, @isHardTab, @bufferDelta} = properties
{@hasInvisibleCharacters, @hasPairedCharacter, @isSoftWrapIndentation} = properties
@firstNonWhitespaceIndex = properties.firstNonWhitespaceIndex ? null
@firstTrailingWhitespaceIndex = properties.firstTrailingWhitespaceIndex ? null
@screenDelta = @value.length
@bufferDelta ?= @screenDelta
@hasPairedCharacter ?= textUtils.hasPairedCharacter(@value)
isEqual: (other) ->
# TODO: scopes is deprecated. This is here for the sake of lang package tests
@value == other.value and _.isEqual(@scopes, other.scopes) and !!@isAtomic == !!other.isAtomic
@value is other.value and _.isEqual(@scopes, other.scopes) and !!@isAtomic is !!other.isAtomic
isBracket: ->
/^meta\.brace\b/.test(_.last(@scopes))
splitAt: (splitIndex) ->
leftToken = new Token(value: @value.substring(0, splitIndex), scopes: @scopes)
rightToken = new Token(value: @value.substring(splitIndex), scopes: @scopes)
if @firstNonWhitespaceIndex?
leftToken.firstNonWhitespaceIndex = Math.min(splitIndex, @firstNonWhitespaceIndex)
leftToken.hasInvisibleCharacters = @hasInvisibleCharacters
if @firstNonWhitespaceIndex > splitIndex
rightToken.firstNonWhitespaceIndex = @firstNonWhitespaceIndex - splitIndex
rightToken.hasInvisibleCharacters = @hasInvisibleCharacters
if @firstTrailingWhitespaceIndex?
rightToken.firstTrailingWhitespaceIndex = Math.max(0, @firstTrailingWhitespaceIndex - splitIndex)
rightToken.hasInvisibleCharacters = @hasInvisibleCharacters
if @firstTrailingWhitespaceIndex < splitIndex
leftToken.firstTrailingWhitespaceIndex = @firstTrailingWhitespaceIndex
leftToken.hasInvisibleCharacters = @hasInvisibleCharacters
[leftToken, rightToken]
whitespaceRegexForTabLength: (tabLength) ->
WhitespaceRegexesByTabLength[tabLength] ?= new RegExp("([ ]{#{tabLength}})|(\t)|([^\t]+)", "g")
breakOutAtomicTokens: (tabLength, breakOutLeadingSoftTabs, startColumn) ->
if @hasPairedCharacter
outputTokens = []
column = startColumn
for token in @breakOutPairedCharacters()
if token.isAtomic
outputTokens.push(token)
else
outputTokens.push(token.breakOutAtomicTokens(tabLength, breakOutLeadingSoftTabs, column)...)
breakOutLeadingSoftTabs = token.isOnlyWhitespace() if breakOutLeadingSoftTabs
column += token.value.length
outputTokens
else
return [this] if @isAtomic
if breakOutLeadingSoftTabs
return [this] unless /^[ ]|\t/.test(@value)
else
return [this] unless /\t/.test(@value)
outputTokens = []
regex = @whitespaceRegexForTabLength(tabLength)
column = startColumn
while match = regex.exec(@value)
[fullMatch, softTab, hardTab] = match
token = null
if softTab and breakOutLeadingSoftTabs
token = @buildSoftTabToken(tabLength)
else if hardTab
breakOutLeadingSoftTabs = false
token = @buildHardTabToken(tabLength, column)
else
breakOutLeadingSoftTabs = false
value = match[0]
token = new Token({value, @scopes})
column += token.value.length
outputTokens.push(token)
outputTokens
breakOutPairedCharacters: ->
outputTokens = []
index = 0
nonPairStart = 0
while index < @value.length
if textUtils.isPairedCharacter(@value, index)
if nonPairStart isnt index
outputTokens.push(new Token({value: @value[nonPairStart...index], @scopes}))
outputTokens.push(@buildPairedCharacterToken(@value, index))
index += 2
nonPairStart = index
else
index++
if nonPairStart isnt index
outputTokens.push(new Token({value: @value[nonPairStart...index], @scopes}))
outputTokens
buildPairedCharacterToken: (value, index) ->
new Token(
value: value[index..index + 1]
scopes: @scopes
isAtomic: true
hasPairedCharacter: true
)
buildHardTabToken: (tabLength, column) ->
@buildTabToken(tabLength, true, column)
buildSoftTabToken: (tabLength) ->
@buildTabToken(tabLength, false, 0)
buildTabToken: (tabLength, isHardTab, column=0) ->
tabStop = tabLength - (column % tabLength)
new Token(
value: _.multiplyString(" ", tabStop)
scopes: @scopes
bufferDelta: if isHardTab then 1 else tabStop
isAtomic: true
isHardTab: isHardTab
)
buildSoftWrapIndentationToken: (length) ->
new Token(
value: _.multiplyString(" ", length),
scopes: @scopes,
bufferDelta: 0,
isAtomic: true,
isSoftWrapIndentation: true
)
isOnlyWhitespace: ->
not WhitespaceRegex.test(@value)
@@ -162,72 +40,6 @@ class Token
scopeClasses = scope.split('.')
_.isSubset(targetClasses, scopeClasses)
getValueAsHtml: ({hasIndentGuide}) ->
if @isHardTab
classes = 'hard-tab'
classes += ' leading-whitespace' if @hasLeadingWhitespace()
classes += ' trailing-whitespace' if @hasTrailingWhitespace()
classes += ' indent-guide' if hasIndentGuide
classes += ' invisible-character' if @hasInvisibleCharacters
html = "<span class='#{classes}'>#{@escapeString(@value)}</span>"
else
startIndex = 0
endIndex = @value.length
leadingHtml = ''
trailingHtml = ''
if @hasLeadingWhitespace()
leadingWhitespace = @value.substring(0, @firstNonWhitespaceIndex)
classes = 'leading-whitespace'
classes += ' indent-guide' if hasIndentGuide
classes += ' invisible-character' if @hasInvisibleCharacters
leadingHtml = "<span class='#{classes}'>#{leadingWhitespace}</span>"
startIndex = @firstNonWhitespaceIndex
if @hasTrailingWhitespace()
tokenIsOnlyWhitespace = @firstTrailingWhitespaceIndex is 0
trailingWhitespace = @value.substring(@firstTrailingWhitespaceIndex)
classes = 'trailing-whitespace'
classes += ' indent-guide' if hasIndentGuide and not @hasLeadingWhitespace() and tokenIsOnlyWhitespace
classes += ' invisible-character' if @hasInvisibleCharacters
trailingHtml = "<span class='#{classes}'>#{trailingWhitespace}</span>"
endIndex = @firstTrailingWhitespaceIndex
html = leadingHtml
if @value.length > MaxTokenLength
while startIndex < endIndex
html += "<span>" + @escapeString(@value, startIndex, startIndex + MaxTokenLength) + "</span>"
startIndex += MaxTokenLength
else
html += @escapeString(@value, startIndex, endIndex)
html += trailingHtml
html
escapeString: (str, startIndex, endIndex) ->
strLength = str.length
startIndex ?= 0
endIndex ?= strLength
str = str.slice(startIndex, endIndex) if startIndex > 0 or endIndex < strLength
str.replace(EscapeRegex, @escapeStringReplace)
escapeStringReplace: (match) ->
switch match
when '&' then '&amp;'
when '"' then '&quot;'
when "'" then '&#39;'
when '<' then '&lt;'
when '>' then '&gt;'
else match
hasLeadingWhitespace: ->
@firstNonWhitespaceIndex? and @firstNonWhitespaceIndex > 0

View File

@@ -1,10 +1,11 @@
_ = require 'underscore-plus'
{Model} = require 'theorist'
EmitterMixin = require('emissary').Emitter
{Emitter} = require 'event-kit'
{CompositeDisposable, Emitter} = require 'event-kit'
{Point, Range} = require 'text-buffer'
{ScopeSelector} = require 'first-mate'
Serializable = require 'serializable'
Model = require './model'
TokenizedLine = require './tokenized-line'
TokenIterator = require './token-iterator'
Token = require './token'
ScopeDescriptor = require './scope-descriptor'
Grim = require 'grim'
@@ -13,31 +14,37 @@ module.exports =
class TokenizedBuffer extends Model
Serializable.includeInto(this)
@property 'tabLength'
grammar: null
currentGrammarScore: null
buffer: null
tabLength: null
tokenizedLines: null
chunkSize: 50
invalidRows: null
visible: false
configSettings: null
constructor: ({@buffer, @tabLength, @invisibles}) ->
constructor: ({@buffer, @tabLength, @ignoreInvisibles, @largeFileMode}) ->
@emitter = new Emitter
@disposables = new CompositeDisposable
@tokenIterator = new TokenIterator
@subscribe atom.grammars.onDidAddGrammar(@grammarAddedOrUpdated)
@subscribe atom.grammars.onDidUpdateGrammar(@grammarAddedOrUpdated)
@disposables.add atom.grammars.onDidAddGrammar(@grammarAddedOrUpdated)
@disposables.add atom.grammars.onDidUpdateGrammar(@grammarAddedOrUpdated)
@subscribe @buffer.preemptDidChange (e) => @handleBufferChange(e)
@subscribe @buffer.onDidChangePath (@bufferPath) => @reloadGrammar()
@disposables.add @buffer.preemptDidChange (e) => @handleBufferChange(e)
@disposables.add @buffer.onDidChangePath (@bufferPath) => @reloadGrammar()
@reloadGrammar()
destroyed: ->
@disposables.dispose()
serializeParams: ->
bufferPath: @buffer.getPath()
tabLength: @tabLength
invisibles: _.clone(@invisibles)
ignoreInvisibles: @ignoreInvisibles
largeFileMode: @largeFileMode
deserializeParams: (params) ->
params.buffer = atom.project.bufferForPathSync(params.bufferPath)
@@ -56,67 +63,72 @@ class TokenizedBuffer extends Model
onDidTokenize: (callback) ->
@emitter.on 'did-tokenize', callback
on: (eventName) ->
switch eventName
when 'changed'
Grim.deprecate("Use TokenizedBuffer::onDidChange instead")
when 'grammar-changed'
Grim.deprecate("Use TokenizedBuffer::onDidChangeGrammar instead")
when 'tokenized'
Grim.deprecate("Use TokenizedBuffer::onDidTokenize instead")
else
Grim.deprecate("TokenizedBuffer::on is deprecated. Use event subscription methods instead.")
EmitterMixin::on.apply(this, arguments)
grammarAddedOrUpdated: (grammar) =>
if grammar.injectionSelector?
@retokenizeLines() if @hasTokenForSelector(grammar.injectionSelector)
else
newScore = grammar.getScore(@buffer.getPath(), @buffer.getText())
newScore = grammar.getScore(@buffer.getPath(), @getGrammarSelectionContent())
@setGrammar(grammar, newScore) if newScore > @currentGrammarScore
setGrammar: (grammar, score) ->
return if grammar is @grammar
@unsubscribe(@grammar) if @grammar
@grammar = grammar
@rootScopeDescriptor = new ScopeDescriptor(scopes: [@grammar.scopeName])
@currentGrammarScore = score ? grammar.getScore(@buffer.getPath(), @buffer.getText())
@subscribe @grammar.onDidUpdate => @retokenizeLines()
@currentGrammarScore = score ? grammar.getScore(@buffer.getPath(), @getGrammarSelectionContent())
@configSettings = tabLength: atom.config.get('editor.tabLength', scope: @rootScopeDescriptor)
@grammarUpdateDisposable?.dispose()
@grammarUpdateDisposable = @grammar.onDidUpdate => @retokenizeLines()
@disposables.add(@grammarUpdateDisposable)
@grammarTabLengthSubscription?.dispose()
@grammarTabLengthSubscription = atom.config.onDidChange 'editor.tabLength', scope: @rootScopeDescriptor, ({newValue}) =>
scopeOptions = {scope: @rootScopeDescriptor}
@configSettings =
tabLength: atom.config.get('editor.tabLength', scopeOptions)
invisibles: atom.config.get('editor.invisibles', scopeOptions)
showInvisibles: atom.config.get('editor.showInvisibles', scopeOptions)
if @configSubscriptions?
@configSubscriptions.dispose()
@disposables.remove(@configSubscriptions)
@configSubscriptions = new CompositeDisposable
@configSubscriptions.add atom.config.onDidChange 'editor.tabLength', scopeOptions, ({newValue}) =>
@configSettings.tabLength = newValue
@retokenizeLines()
@subscribe @grammarTabLengthSubscription
['invisibles', 'showInvisibles'].forEach (key) =>
@configSubscriptions.add atom.config.onDidChange "editor.#{key}", scopeOptions, ({newValue}) =>
oldInvisibles = @getInvisiblesToShow()
@configSettings[key] = newValue
@retokenizeLines() unless _.isEqual(@getInvisiblesToShow(), oldInvisibles)
@disposables.add(@configSubscriptions)
@retokenizeLines()
@emit 'grammar-changed', grammar
@emit 'grammar-changed', grammar if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change-grammar', grammar
getGrammarSelectionContent: ->
@buffer.getTextInRange([[0, 0], [10, 0]])
reloadGrammar: ->
if grammar = atom.grammars.selectGrammar(@buffer.getPath(), @buffer.getText())
if grammar = atom.grammars.selectGrammar(@buffer.getPath(), @getGrammarSelectionContent())
@setGrammar(grammar)
else
throw new Error("No grammar found for path: #{path}")
hasTokenForSelector: (selector) ->
for {tokens} in @tokenizedLines
for token in tokens
for tokenizedLine in @tokenizedLines when tokenizedLine?
for token in tokenizedLine.tokens
return true if selector.matches(token.scopes)
false
retokenizeLines: ->
lastRow = @buffer.getLastRow()
@tokenizedLines = @buildPlaceholderTokenizedLinesForRows(0, lastRow)
@tokenizedLines = new Array(lastRow + 1)
@invalidRows = []
@invalidateRow(0)
@fullyTokenized = false
event = {start: 0, end: lastRow, delta: 0}
@emit 'changed', event
@emit 'changed', event if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change', event
setVisible: (@visible) ->
@@ -131,19 +143,27 @@ class TokenizedBuffer extends Model
@tabLength = tabLength
@retokenizeLines()
setInvisibles: (invisibles) ->
unless _.isEqual(invisibles, @invisibles)
@invisibles = invisibles
@retokenizeLines()
setIgnoreInvisibles: (ignoreInvisibles) ->
if ignoreInvisibles isnt @ignoreInvisibles
@ignoreInvisibles = ignoreInvisibles
if @configSettings.showInvisibles and @configSettings.invisibles?
@retokenizeLines()
tokenizeInBackground: ->
return if not @visible or @pendingChunk or not @isAlive()
@pendingChunk = true
_.defer =>
@pendingChunk = false
@tokenizeNextChunk() if @isAlive() and @buffer.isAlive()
tokenizeNextChunk: ->
# Short circuit null grammar which can just use the placeholder tokens
if @grammar is atom.grammars.nullGrammar and @firstInvalidRow()?
@invalidRows = []
@markTokenizationComplete()
return
rowsRemaining = @chunkSize
while @firstInvalidRow()? and rowsRemaining > 0
@@ -154,12 +174,12 @@ class TokenizedBuffer extends Model
row = startRow
loop
previousStack = @stackForRow(row)
@tokenizedLines[row] = @buildTokenizedLineForRow(row, @stackForRow(row - 1))
if --rowsRemaining == 0
@tokenizedLines[row] = @buildTokenizedLineForRow(row, @stackForRow(row - 1), @openScopesForRow(row))
if --rowsRemaining is 0
filledRegion = false
endRow = row
break
if row == lastRow or _.isEqual(@stackForRow(row), previousStack)
if row is lastRow or _.isEqual(@stackForRow(row), previousStack)
filledRegion = true
endRow = row
break
@@ -171,24 +191,30 @@ class TokenizedBuffer extends Model
[startRow, endRow] = @updateFoldableStatus(startRow, endRow)
event = {start: startRow, end: endRow, delta: 0}
@emit 'changed', event
@emit 'changed', event if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change', event
if @firstInvalidRow()?
@tokenizeInBackground()
else
unless @fullyTokenized
@emit 'tokenized'
@emitter.emit 'did-tokenize'
@fullyTokenized = true
@markTokenizationComplete()
markTokenizationComplete: ->
unless @fullyTokenized
@emit 'tokenized' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-tokenize'
@fullyTokenized = true
firstInvalidRow: ->
@invalidRows[0]
validateRow: (row) ->
@invalidRows.shift() while @invalidRows[0] <= row
return
invalidateRow: (row) ->
return if @largeFileMode
@invalidRows.push(row)
@invalidRows.sort (a, b) -> a - b
@tokenizeInBackground()
@@ -210,7 +236,10 @@ class TokenizedBuffer extends Model
@updateInvalidRows(start, end, delta)
previousEndStack = @stackForRow(end) # used in spill detection below
newTokenizedLines = @buildTokenizedLinesForRows(start, end + delta, @stackForRow(start - 1))
if @largeFileMode
newTokenizedLines = @buildPlaceholderTokenizedLinesForRows(start, end + delta)
else
newTokenizedLines = @buildTokenizedLinesForRows(start, end + delta, @stackForRow(start - 1), @openScopesForRow(start))
_.spliceWithArray(@tokenizedLines, start, end - start + 1, newTokenizedLines)
start = @retokenizeWhitespaceRowsIfIndentLevelChanged(start - 1, -1)
@@ -223,21 +252,23 @@ class TokenizedBuffer extends Model
[start, end] = @updateFoldableStatus(start, end + delta)
end -= delta
event = { start, end, delta, bufferChange: e }
@emit 'changed', event
event = {start, end, delta, bufferChange: e}
@emit 'changed', event if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change', event
retokenizeWhitespaceRowsIfIndentLevelChanged: (row, increment) ->
line = @tokenizedLines[row]
line = @tokenizedLineForRow(row)
if line?.isOnlyWhitespace() and @indentLevelForRow(row) isnt line.indentLevel
while line?.isOnlyWhitespace()
@tokenizedLines[row] = @buildTokenizedLineForRow(row, @stackForRow(row - 1))
@tokenizedLines[row] = @buildTokenizedLineForRow(row, @stackForRow(row - 1), @openScopesForRow(row))
row += increment
line = @tokenizedLines[row]
line = @tokenizedLineForRow(row)
row - increment
updateFoldableStatus: (startRow, endRow) ->
return [startRow, endRow] if @largeFileMode
scanStartRow = @buffer.previousNonBlankRow(startRow) ? startRow
scanStartRow-- while scanStartRow > 0 and @tokenizedLineForRow(scanStartRow).isComment()
scanEndRow = @buffer.nextNonBlankRow(endRow) ? endRow
@@ -253,7 +284,10 @@ class TokenizedBuffer extends Model
[startRow, endRow]
isFoldableAtRow: (row) ->
@isFoldableCodeAtRow(row) or @isFoldableCommentAtRow(row)
if @largeFileMode
false
else
@isFoldableCodeAtRow(row) or @isFoldableCommentAtRow(row)
# Returns a {Boolean} indicating whether the given buffer row starts
# a a foldable row range due to the code's indentation patterns.
@@ -273,16 +307,18 @@ class TokenizedBuffer extends Model
@tokenizedLineForRow(row).isComment() and
@tokenizedLineForRow(nextRow).isComment()
buildTokenizedLinesForRows: (startRow, endRow, startingStack) ->
buildTokenizedLinesForRows: (startRow, endRow, startingStack, startingopenScopes) ->
ruleStack = startingStack
openScopes = startingopenScopes
stopTokenizingAt = startRow + @chunkSize
tokenizedLines = for row in [startRow..endRow]
if (ruleStack or row == 0) and row < stopTokenizingAt
screenLine = @buildTokenizedLineForRow(row, ruleStack)
ruleStack = screenLine.ruleStack
if (ruleStack or row is 0) and row < stopTokenizingAt
tokenizedLine = @buildTokenizedLineForRow(row, ruleStack, openScopes)
ruleStack = tokenizedLine.ruleStack
openScopes = @scopesFromTags(openScopes, tokenizedLine.tags)
else
screenLine = @buildPlaceholderTokenizedLineForRow(row)
screenLine
tokenizedLine = @buildPlaceholderTokenizedLineForRow(row, openScopes)
tokenizedLine
if endRow >= stopTokenizingAt
@invalidateRow(stopTokenizingAt)
@@ -291,32 +327,63 @@ class TokenizedBuffer extends Model
tokenizedLines
buildPlaceholderTokenizedLinesForRows: (startRow, endRow) ->
@buildPlaceholderTokenizedLineForRow(row) for row in [startRow..endRow]
@buildPlaceholderTokenizedLineForRow(row) for row in [startRow..endRow] by 1
buildPlaceholderTokenizedLineForRow: (row) ->
line = @buffer.lineForRow(row)
tokens = [new Token(value: line, scopes: [@grammar.scopeName])]
openScopes = [@grammar.startIdForScope(@grammar.scopeName)]
text = @buffer.lineForRow(row)
tags = [text.length]
tabLength = @getTabLength()
indentLevel = @indentLevelForRow(row)
lineEnding = @buffer.lineEndingForRow(row)
new TokenizedLine({tokens, tabLength, indentLevel, @invisibles, lineEnding})
new TokenizedLine({openScopes, text, tags, tabLength, indentLevel, invisibles: @getInvisiblesToShow(), lineEnding, @tokenIterator})
buildTokenizedLineForRow: (row, ruleStack) ->
@buildTokenizedLineForRowWithText(row, @buffer.lineForRow(row), ruleStack)
buildTokenizedLineForRow: (row, ruleStack, openScopes) ->
@buildTokenizedLineForRowWithText(row, @buffer.lineForRow(row), ruleStack, openScopes)
buildTokenizedLineForRowWithText: (row, line, ruleStack = @stackForRow(row - 1)) ->
buildTokenizedLineForRowWithText: (row, text, ruleStack = @stackForRow(row - 1), openScopes = @openScopesForRow(row)) ->
lineEnding = @buffer.lineEndingForRow(row)
tabLength = @getTabLength()
indentLevel = @indentLevelForRow(row)
{tokens, ruleStack} = @grammar.tokenizeLine(line, ruleStack, row is 0)
new TokenizedLine({tokens, ruleStack, tabLength, lineEnding, indentLevel, @invisibles})
{tags, ruleStack} = @grammar.tokenizeLine(text, ruleStack, row is 0, false)
new TokenizedLine({openScopes, text, tags, ruleStack, tabLength, lineEnding, indentLevel, invisibles: @getInvisiblesToShow(), @tokenIterator})
getInvisiblesToShow: ->
if @configSettings.showInvisibles and not @ignoreInvisibles
@configSettings.invisibles
else
null
tokenizedLineForRow: (bufferRow) ->
@tokenizedLines[bufferRow]
if 0 <= bufferRow < @tokenizedLines.length
@tokenizedLines[bufferRow] ?= @buildPlaceholderTokenizedLineForRow(bufferRow)
tokenizedLinesForRows: (startRow, endRow) ->
for row in [startRow..endRow] by 1
@tokenizedLineForRow(row)
stackForRow: (bufferRow) ->
@tokenizedLines[bufferRow]?.ruleStack
openScopesForRow: (bufferRow) ->
if bufferRow > 0
precedingLine = @tokenizedLines[bufferRow - 1]
@scopesFromTags(precedingLine.openScopes, precedingLine.tags)
else
[]
scopesFromTags: (startingScopes, tags) ->
scopes = startingScopes.slice()
for tag in tags when tag < 0
if (tag % 2) is -1
scopes.push(tag)
else
expectedScope = tag + 1
poppedScope = scopes.pop()
unless poppedScope is expectedScope
throw new Error("Encountered an invalid scope end id. Popped #{poppedScope}, expected to pop #{expectedScope}.")
scopes
indentLevelForRow: (bufferRow) ->
line = @buffer.lineForRow(bufferRow)
indentLevel = 0
@@ -344,106 +411,86 @@ class TokenizedBuffer extends Model
@indentLevelForLine(line)
indentLevelForLine: (line) ->
if match = line.match(/^[\t ]+/)
leadingWhitespace = match[0]
tabCount = leadingWhitespace.match(/\t/g)?.length ? 0
spaceCount = leadingWhitespace.match(/[ ]/g)?.length ? 0
tabCount + (spaceCount / @getTabLength())
if match = line.match(/^\t+/)
match[0].length
else if match = line.match(/^ +/)
match[0].length / @getTabLength()
else
0
scopeDescriptorForPosition: (position) ->
new ScopeDescriptor(scopes: @tokenForPosition(position).scopes)
{row, column} = Point.fromObject(position)
iterator = @tokenizedLines[row].getTokenIterator()
while iterator.next()
if iterator.getBufferEnd() > column
scopes = iterator.getScopes()
break
# rebuild scope of last token if we iterated off the end
unless scopes?
scopes = iterator.getScopes()
scopes.push(iterator.getScopeEnds().reverse()...)
new ScopeDescriptor({scopes})
tokenForPosition: (position) ->
{row, column} = Point.fromObject(position)
@tokenizedLines[row].tokenAtBufferColumn(column)
@tokenizedLineForRow(row).tokenAtBufferColumn(column)
tokenStartPositionForPosition: (position) ->
{row, column} = Point.fromObject(position)
column = @tokenizedLines[row].tokenStartColumnForBufferColumn(column)
column = @tokenizedLineForRow(row).tokenStartColumnForBufferColumn(column)
new Point(row, column)
bufferRangeForScopeAtPosition: (selector, position) ->
position = Point.fromObject(position)
tokenizedLine = @tokenizedLines[position.row]
startIndex = tokenizedLine.tokenIndexAtBufferColumn(position.column)
for index in [startIndex..0]
token = tokenizedLine.tokenAtIndex(index)
break unless token.matchesScopeSelector(selector)
firstToken = token
{openScopes, tags} = @tokenizedLineForRow(position.row)
scopes = openScopes.map (tag) -> atom.grammars.scopeForId(tag)
for index in [startIndex...tokenizedLine.getTokenCount()]
token = tokenizedLine.tokenAtIndex(index)
break unless token.matchesScopeSelector(selector)
lastToken = token
startColumn = 0
for tag, tokenIndex in tags
if tag < 0
if tag % 2 is -1
scopes.push(atom.grammars.scopeForId(tag))
else
scopes.pop()
else
endColumn = startColumn + tag
if endColumn > position.column
break
else
startColumn = endColumn
return unless firstToken? and lastToken?
startColumn = tokenizedLine.bufferColumnForToken(firstToken)
endColumn = tokenizedLine.bufferColumnForToken(lastToken) + lastToken.bufferDelta
new Range([position.row, startColumn], [position.row, endColumn])
return unless selectorMatchesAnyScope(selector, scopes)
iterateTokensInBufferRange: (bufferRange, iterator) ->
bufferRange = Range.fromObject(bufferRange)
{ start, end } = bufferRange
startScopes = scopes.slice()
for startTokenIndex in [(tokenIndex - 1)..0] by -1
tag = tags[startTokenIndex]
if tag < 0
if tag % 2 is -1
startScopes.pop()
else
startScopes.push(atom.grammars.scopeForId(tag))
else
break unless selectorMatchesAnyScope(selector, startScopes)
startColumn -= tag
keepLooping = true
stop = -> keepLooping = false
endScopes = scopes.slice()
for endTokenIndex in [(tokenIndex + 1)...tags.length] by 1
tag = tags[endTokenIndex]
if tag < 0
if tag % 2 is -1
endScopes.push(atom.grammars.scopeForId(tag))
else
endScopes.pop()
else
break unless selectorMatchesAnyScope(selector, endScopes)
endColumn += tag
for bufferRow in [start.row..end.row]
bufferColumn = 0
for token in @tokenizedLines[bufferRow].tokens
startOfToken = new Point(bufferRow, bufferColumn)
iterator(token, startOfToken, { stop }) if bufferRange.containsPoint(startOfToken)
return unless keepLooping
bufferColumn += token.bufferDelta
backwardsIterateTokensInBufferRange: (bufferRange, iterator) ->
bufferRange = Range.fromObject(bufferRange)
{ start, end } = bufferRange
keepLooping = true
stop = -> keepLooping = false
for bufferRow in [end.row..start.row]
bufferColumn = @buffer.lineLengthForRow(bufferRow)
for token in new Array(@tokenizedLines[bufferRow].tokens...).reverse()
bufferColumn -= token.bufferDelta
startOfToken = new Point(bufferRow, bufferColumn)
iterator(token, startOfToken, { stop }) if bufferRange.containsPoint(startOfToken)
return unless keepLooping
findOpeningBracket: (startBufferPosition) ->
range = [[0,0], startBufferPosition]
position = null
depth = 0
@backwardsIterateTokensInBufferRange range, (token, startPosition, { stop }) ->
if token.isBracket()
if token.value == '}'
depth++
else if token.value == '{'
depth--
if depth == 0
position = startPosition
stop()
position
findClosingBracket: (startBufferPosition) ->
range = [startBufferPosition, @buffer.getEndPosition()]
position = null
depth = 0
@iterateTokensInBufferRange range, (token, startPosition, { stop }) ->
if token.isBracket()
if token.value == '{'
depth++
else if token.value == '}'
depth--
if depth == 0
position = startPosition
stop()
position
new Range(new Point(position.row, startColumn), new Point(position.row, endColumn))
# Gets the row number of the last line.
#
@@ -458,3 +505,26 @@ class TokenizedBuffer extends Model
for row in [start..end]
line = @tokenizedLineForRow(row).text
console.log row, line, line.length
return
if Grim.includeDeprecatedAPIs
EmitterMixin = require('emissary').Emitter
TokenizedBuffer::on = (eventName) ->
switch eventName
when 'changed'
Grim.deprecate("Use TokenizedBuffer::onDidChange instead")
when 'grammar-changed'
Grim.deprecate("Use TokenizedBuffer::onDidChangeGrammar instead")
when 'tokenized'
Grim.deprecate("Use TokenizedBuffer::onDidTokenize instead")
else
Grim.deprecate("TokenizedBuffer::on is deprecated. Use event subscription methods instead.")
EmitterMixin::on.apply(this, arguments)
selectorMatchesAnyScope = (selector, scopes) ->
targetClasses = selector.replace(/^\./, '').split('.')
_.any scopes, (scope) ->
scopeClasses = scope.split('.')
_.isSubset(targetClasses, scopeClasses)

View File

@@ -1,84 +1,306 @@
_ = require 'underscore-plus'
{isPairedCharacter} = require './text-utils'
Token = require './token'
{SoftTab, HardTab, PairedCharacter, SoftWrapIndent} = require './special-token-symbols'
NonWhitespaceRegex = /\S/
LeadingWhitespaceRegex = /^\s*/
TrailingWhitespaceRegex = /\s*$/
RepeatedSpaceRegex = /[ ]/g
CommentScopeRegex = /(\b|\.)comment/
TabCharCode = 9
SpaceCharCode = 32
SpaceString = ' '
TabStringsByLength = {
1: ' '
2: ' '
3: ' '
4: ' '
}
idCounter = 1
getTabString = (length) ->
TabStringsByLength[length] ?= buildTabString(length)
buildTabString = (length) ->
string = SpaceString
string += SpaceString for i in [1...length] by 1
string
module.exports =
class TokenizedLine
endOfLineInvisibles: null
lineIsWhitespaceOnly: false
firstNonWhitespaceIndex: 0
foldable: false
constructor: ({tokens, @lineEnding, @ruleStack, @startBufferColumn, @fold, @tabLength, @indentLevel, @invisibles}) ->
@startBufferColumn ?= 0
@tokens = @breakOutAtomicTokens(tokens)
@text = @buildText()
@bufferDelta = @buildBufferDelta()
@softWrapIndentationTokens = @getSoftWrapIndentationTokens()
@softWrapIndentationDelta = @buildSoftWrapIndentationDelta()
constructor: (properties) ->
@id = idCounter++
@markLeadingAndTrailingWhitespaceTokens()
if @invisibles
@substituteInvisibleCharacters()
@buildEndOfLineInvisibles() if @lineEnding?
buildText: ->
text = ""
text += token.value for token in @tokens
text
return unless properties?
buildBufferDelta: ->
delta = 0
delta += token.bufferDelta for token in @tokens
delta
@specialTokens = {}
{@openScopes, @text, @tags, @lineEnding, @ruleStack, @tokenIterator} = properties
{@startBufferColumn, @fold, @tabLength, @indentLevel, @invisibles} = properties
@startBufferColumn ?= 0
@bufferDelta = @text.length
@transformContent()
@buildEndOfLineInvisibles() if @invisibles? and @lineEnding?
transformContent: ->
text = ''
bufferColumn = 0
screenColumn = 0
tokenIndex = 0
tokenOffset = 0
firstNonWhitespaceColumn = null
lastNonWhitespaceColumn = null
substringStart = 0
substringEnd = 0
while bufferColumn < @text.length
# advance to next token if we've iterated over its length
if tokenOffset is @tags[tokenIndex]
tokenIndex++
tokenOffset = 0
# advance to next token tag
tokenIndex++ while @tags[tokenIndex] < 0
charCode = @text.charCodeAt(bufferColumn)
# split out unicode surrogate pairs
if isPairedCharacter(@text, bufferColumn)
prefix = tokenOffset
suffix = @tags[tokenIndex] - tokenOffset - 2
i = tokenIndex
@tags.splice(i, 1)
@tags.splice(i++, 0, prefix) if prefix > 0
@tags.splice(i++, 0, 2)
@tags.splice(i, 0, suffix) if suffix > 0
firstNonWhitespaceColumn ?= screenColumn
lastNonWhitespaceColumn = screenColumn + 1
substringEnd += 2
screenColumn += 2
bufferColumn += 2
tokenIndex++ if prefix > 0
@specialTokens[tokenIndex] = PairedCharacter
tokenIndex++
tokenOffset = 0
# split out leading soft tabs
else if charCode is SpaceCharCode
if firstNonWhitespaceColumn?
substringEnd += 1
else
if (screenColumn + 1) % @tabLength is 0
suffix = @tags[tokenIndex] - @tabLength
if suffix >= 0
@specialTokens[tokenIndex] = SoftTab
@tags.splice(tokenIndex, 1, @tabLength)
@tags.splice(tokenIndex + 1, 0, suffix) if suffix > 0
if @invisibles?.space
if substringEnd > substringStart
text += @text.substring(substringStart, substringEnd)
substringStart = substringEnd
text += @invisibles.space
substringStart += 1
substringEnd += 1
screenColumn++
bufferColumn++
tokenOffset++
# expand hard tabs to the next tab stop
else if charCode is TabCharCode
if substringEnd > substringStart
text += @text.substring(substringStart, substringEnd)
substringStart = substringEnd
tabLength = @tabLength - (screenColumn % @tabLength)
if @invisibles?.tab
text += @invisibles.tab
text += getTabString(tabLength - 1) if tabLength > 1
else
text += getTabString(tabLength)
substringStart += 1
substringEnd += 1
prefix = tokenOffset
suffix = @tags[tokenIndex] - tokenOffset - 1
i = tokenIndex
@tags.splice(i, 1)
@tags.splice(i++, 0, prefix) if prefix > 0
@tags.splice(i++, 0, tabLength)
@tags.splice(i, 0, suffix) if suffix > 0
screenColumn += tabLength
bufferColumn++
tokenIndex++ if prefix > 0
@specialTokens[tokenIndex] = HardTab
tokenIndex++
tokenOffset = 0
# continue past any other character
else
firstNonWhitespaceColumn ?= screenColumn
lastNonWhitespaceColumn = screenColumn
substringEnd += 1
screenColumn++
bufferColumn++
tokenOffset++
if substringEnd > substringStart
unless substringStart is 0 and substringEnd is @text.length
text += @text.substring(substringStart, substringEnd)
@text = text
else
@text = text
@firstNonWhitespaceIndex = firstNonWhitespaceColumn
if lastNonWhitespaceColumn?
if lastNonWhitespaceColumn + 1 < @text.length
@firstTrailingWhitespaceIndex = lastNonWhitespaceColumn + 1
if @invisibles?.space
@text =
@text.substring(0, @firstTrailingWhitespaceIndex) +
@text.substring(@firstTrailingWhitespaceIndex)
.replace(RepeatedSpaceRegex, @invisibles.space)
else
@lineIsWhitespaceOnly = true
@firstTrailingWhitespaceIndex = 0
getTokenIterator: -> @tokenIterator.reset(this)
Object.defineProperty @prototype, 'tokens', get: ->
iterator = @getTokenIterator()
tokens = []
while iterator.next()
properties = {
value: iterator.getText()
scopes: iterator.getScopes().slice()
isAtomic: iterator.isAtomic()
isHardTab: iterator.isHardTab()
hasPairedCharacter: iterator.isPairedCharacter()
isSoftWrapIndentation: iterator.isSoftWrapIndentation()
}
if iterator.isHardTab()
properties.bufferDelta = 1
properties.hasInvisibleCharacters = true if @invisibles?.tab
if iterator.getScreenStart() < @firstNonWhitespaceIndex
properties.firstNonWhitespaceIndex =
Math.min(@firstNonWhitespaceIndex, iterator.getScreenEnd()) - iterator.getScreenStart()
properties.hasInvisibleCharacters = true if @invisibles?.space
if @lineEnding? and iterator.getScreenEnd() > @firstTrailingWhitespaceIndex
properties.firstTrailingWhitespaceIndex =
Math.max(0, @firstTrailingWhitespaceIndex - iterator.getScreenStart())
properties.hasInvisibleCharacters = true if @invisibles?.space
tokens.push(new Token(properties))
tokens
copy: ->
new TokenizedLine({@tokens, @lineEnding, @ruleStack, @startBufferColumn, @fold})
copy = new TokenizedLine
copy.tokenIterator = @tokenIterator
copy.openScopes = @openScopes
copy.text = @text
copy.tags = @tags
copy.specialTokens = @specialTokens
copy.startBufferColumn = @startBufferColumn
copy.bufferDelta = @bufferDelta
copy.ruleStack = @ruleStack
copy.lineEnding = @lineEnding
copy.invisibles = @invisibles
copy.endOfLineInvisibles = @endOfLineInvisibles
copy.indentLevel = @indentLevel
copy.tabLength = @tabLength
copy.firstNonWhitespaceIndex = @firstNonWhitespaceIndex
copy.firstTrailingWhitespaceIndex = @firstTrailingWhitespaceIndex
copy.fold = @fold
copy
# This clips a given screen column to a valid column that's within the line
# and not in the middle of any atomic tokens.
#
# column - A {Number} representing the column to clip
# options - A hash with the key clip. Valid values for this key:
# 'closest' (default): clip to the closest edge of an atomic token.
# 'forward': clip to the forward edge.
# 'backward': clip to the backward edge.
#
# Returns a {Number} representing the clipped column.
clipScreenColumn: (column, options={}) ->
return 0 if @tokens.length == 0
return 0 if @tags.length is 0
{ skipAtomicTokens } = options
{clip} = options
column = Math.min(column, @getMaxScreenColumn())
tokenStartColumn = 0
for token in @tokens
break if tokenStartColumn + token.screenDelta > column
tokenStartColumn += token.screenDelta
if @isColumnInsideSoftWrapIndentation(tokenStartColumn)
@softWrapIndentationDelta
else if token.isAtomic and tokenStartColumn < column
if skipAtomicTokens
tokenStartColumn + token.screenDelta
else
tokenStartColumn
iterator = @getTokenIterator()
while iterator.next()
break if iterator.getScreenEnd() > column
if iterator.isSoftWrapIndentation()
iterator.next() while iterator.isSoftWrapIndentation()
iterator.getScreenStart()
else if iterator.isAtomic() and iterator.getScreenStart() < column
if clip is 'forward'
iterator.getScreenEnd()
else if clip is 'backward'
iterator.getScreenStart()
else #'closest'
if column > ((iterator.getScreenStart() + iterator.getScreenEnd()) / 2)
iterator.getScreenEnd()
else
iterator.getScreenStart()
else
column
screenColumnForBufferColumn: (bufferColumn, options) ->
bufferColumn = bufferColumn - @startBufferColumn
screenColumn = 0
currentBufferColumn = 0
for token in @tokens
break if currentBufferColumn > bufferColumn
screenColumn += token.screenDelta
currentBufferColumn += token.bufferDelta
@clipScreenColumn(screenColumn + (bufferColumn - currentBufferColumn))
screenColumnForBufferColumn: (targetBufferColumn, options) ->
iterator = @getTokenIterator()
while iterator.next()
tokenBufferStart = iterator.getBufferStart()
tokenBufferEnd = iterator.getBufferEnd()
if tokenBufferStart <= targetBufferColumn < tokenBufferEnd
overshoot = targetBufferColumn - tokenBufferStart
return Math.min(
iterator.getScreenStart() + overshoot,
iterator.getScreenEnd()
)
iterator.getScreenEnd()
bufferColumnForScreenColumn: (screenColumn, options) ->
bufferColumn = @startBufferColumn
currentScreenColumn = 0
for token in @tokens
break if currentScreenColumn + token.screenDelta > screenColumn
bufferColumn += token.bufferDelta
currentScreenColumn += token.screenDelta
bufferColumn + (screenColumn - currentScreenColumn)
bufferColumnForScreenColumn: (targetScreenColumn) ->
iterator = @getTokenIterator()
while iterator.next()
tokenScreenStart = iterator.getScreenStart()
tokenScreenEnd = iterator.getScreenEnd()
if tokenScreenStart <= targetScreenColumn < tokenScreenEnd
overshoot = targetScreenColumn - tokenScreenStart
return Math.min(
iterator.getBufferStart() + overshoot,
iterator.getBufferEnd()
)
iterator.getBufferEnd()
getMaxScreenColumn: ->
if @fold
@@ -96,6 +318,7 @@ class TokenizedLine
# Returns a {Number} representing the `line` position where the wrap would take place.
# Returns `null` if a wrap wouldn't occur.
findWrapColumn: (maxColumn) ->
return unless maxColumn?
return unless @text.length > maxColumn
if /\s/.test(@text[maxColumn])
@@ -106,85 +329,133 @@ class TokenizedLine
return @text.length
else
# search backward for the start of the word on the boundary
for column in [maxColumn..0] when @isColumnOutsideSoftWrapIndentation(column)
for column in [maxColumn..@firstNonWhitespaceIndex]
return column + 1 if /\s/.test(@text[column])
return maxColumn
# Calculates how many trailing spaces in this line's indentation cannot fit in a single tab.
#
# Returns a {Number} representing the odd indentation spaces in this line.
getOddIndentationSpaces: ->
oddIndentLevel = @indentLevel - Math.floor(@indentLevel)
Math.round(@tabLength * oddIndentLevel)
softWrapAt: (column, hangingIndent) ->
return [null, this] if column is 0
buildSoftWrapIndentationTokens: (token) ->
indentTokens = [0...Math.floor(@indentLevel)].map =>
token.buildSoftWrapIndentationToken(@tabLength)
leftText = @text.substring(0, column)
rightText = @text.substring(column)
if @getOddIndentationSpaces()
indentTokens.concat(
token.buildSoftWrapIndentationToken @getOddIndentationSpaces()
)
else
indentTokens
leftTags = []
rightTags = []
softWrapAt: (column) ->
return [new TokenizedLine([], '', [0, 0], [0, 0]), this] if column == 0
leftSpecialTokens = {}
rightSpecialTokens = {}
rightTokens = new Array(@tokens...)
leftTokens = []
leftTextLength = 0
while leftTextLength < column
if leftTextLength + rightTokens[0].value.length > column
rightTokens[0..0] = rightTokens[0].splitAt(column - leftTextLength)
nextToken = rightTokens.shift()
leftTextLength += nextToken.value.length
leftTokens.push nextToken
rightOpenScopes = @openScopes.slice()
indentationTokens = @buildSoftWrapIndentationTokens(leftTokens[0])
screenColumn = 0
for tag, index in @tags
# tag represents a token
if tag >= 0
# token ends before the soft wrap column
if screenColumn + tag <= column
if specialToken = @specialTokens[index]
leftSpecialTokens[index] = specialToken
leftTags.push(tag)
screenColumn += tag
# token starts before and ends after the split column
else if screenColumn <= column
leftSuffix = column - screenColumn
rightPrefix = screenColumn + tag - column
leftTags.push(leftSuffix) if leftSuffix > 0
softWrapIndent = @indentLevel * @tabLength + (hangingIndent ? 0)
for i in [0...softWrapIndent] by 1
rightText = ' ' + rightText
remainingSoftWrapIndent = softWrapIndent
while remainingSoftWrapIndent > 0
indentToken = Math.min(remainingSoftWrapIndent, @tabLength)
rightSpecialTokens[rightTags.length] = SoftWrapIndent
rightTags.push(indentToken)
remainingSoftWrapIndent -= indentToken
rightTags.push(rightPrefix) if rightPrefix > 0
screenColumn += tag
# token is after split column
else
if specialToken = @specialTokens[index]
rightSpecialTokens[rightTags.length] = specialToken
rightTags.push(tag)
# tag represents the start or end of a scop
else if (tag % 2) is -1
if screenColumn < column
leftTags.push(tag)
rightOpenScopes.push(tag)
else
rightTags.push(tag)
else
if screenColumn < column
leftTags.push(tag)
rightOpenScopes.pop()
else
rightTags.push(tag)
splitBufferColumn = @bufferColumnForScreenColumn(column)
leftFragment = new TokenizedLine
leftFragment.tokenIterator = @tokenIterator
leftFragment.openScopes = @openScopes
leftFragment.text = leftText
leftFragment.tags = leftTags
leftFragment.specialTokens = leftSpecialTokens
leftFragment.startBufferColumn = @startBufferColumn
leftFragment.bufferDelta = splitBufferColumn - @startBufferColumn
leftFragment.ruleStack = @ruleStack
leftFragment.invisibles = @invisibles
leftFragment.lineEnding = null
leftFragment.indentLevel = @indentLevel
leftFragment.tabLength = @tabLength
leftFragment.firstNonWhitespaceIndex = Math.min(column, @firstNonWhitespaceIndex)
leftFragment.firstTrailingWhitespaceIndex = Math.min(column, @firstTrailingWhitespaceIndex)
rightFragment = new TokenizedLine
rightFragment.tokenIterator = @tokenIterator
rightFragment.openScopes = rightOpenScopes
rightFragment.text = rightText
rightFragment.tags = rightTags
rightFragment.specialTokens = rightSpecialTokens
rightFragment.startBufferColumn = splitBufferColumn
rightFragment.bufferDelta = @startBufferColumn + @bufferDelta - splitBufferColumn
rightFragment.ruleStack = @ruleStack
rightFragment.invisibles = @invisibles
rightFragment.lineEnding = @lineEnding
rightFragment.indentLevel = @indentLevel
rightFragment.tabLength = @tabLength
rightFragment.endOfLineInvisibles = @endOfLineInvisibles
rightFragment.firstNonWhitespaceIndex = Math.max(softWrapIndent, @firstNonWhitespaceIndex - column + softWrapIndent)
rightFragment.firstTrailingWhitespaceIndex = Math.max(softWrapIndent, @firstTrailingWhitespaceIndex - column + softWrapIndent)
leftFragment = new TokenizedLine(
tokens: leftTokens
startBufferColumn: @startBufferColumn
ruleStack: @ruleStack
invisibles: @invisibles
lineEnding: null,
indentLevel: @indentLevel,
tabLength: @tabLength
)
rightFragment = new TokenizedLine(
tokens: indentationTokens.concat(rightTokens)
startBufferColumn: @bufferColumnForScreenColumn(column)
ruleStack: @ruleStack
invisibles: @invisibles
lineEnding: @lineEnding,
indentLevel: @indentLevel,
tabLength: @tabLength
)
[leftFragment, rightFragment]
isSoftWrapped: ->
@lineEnding is null
isColumnOutsideSoftWrapIndentation: (column) ->
return true if @softWrapIndentationTokens.length == 0
isColumnInsideSoftWrapIndentation: (targetColumn) ->
targetColumn < @getSoftWrapIndentationDelta()
column > @softWrapIndentationDelta
isColumnInsideSoftWrapIndentation: (column) ->
return false if @softWrapIndentationTokens.length == 0
column < @softWrapIndentationDelta
getSoftWrapIndentationTokens: ->
_.select(@tokens, (token) -> token.isSoftWrapIndentation)
buildSoftWrapIndentationDelta: ->
_.reduce @softWrapIndentationTokens, ((acc, token) -> acc + token.screenDelta), 0
getSoftWrapIndentationDelta: ->
delta = 0
for tag, index in @tags
if tag >= 0
if @specialTokens[index] is SoftWrapIndent
delta += tag
else
break
delta
hasOnlySoftWrapIndentation: ->
@tokens.length == @softWrapIndentationTokens.length
@getSoftWrapIndentationDelta() is @text.length
tokenAtBufferColumn: (bufferColumn) ->
@tokens[@tokenIndexAtBufferColumn(bufferColumn)]
@@ -204,57 +475,6 @@ class TokenizedLine
delta = nextDelta
delta
breakOutAtomicTokens: (inputTokens) ->
outputTokens = []
breakOutLeadingSoftTabs = true
column = @startBufferColumn
for token in inputTokens
newTokens = token.breakOutAtomicTokens(@tabLength, breakOutLeadingSoftTabs, column)
column += newToken.value.length for newToken in newTokens
outputTokens.push(newTokens...)
breakOutLeadingSoftTabs = token.isOnlyWhitespace() if breakOutLeadingSoftTabs
outputTokens
markLeadingAndTrailingWhitespaceTokens: ->
firstNonWhitespaceIndex = @text.search(NonWhitespaceRegex)
if firstNonWhitespaceIndex > 0 and isPairedCharacter(@text, firstNonWhitespaceIndex - 1)
firstNonWhitespaceIndex--
firstTrailingWhitespaceIndex = @text.search(TrailingWhitespaceRegex)
@lineIsWhitespaceOnly = firstTrailingWhitespaceIndex is 0
index = 0
for token in @tokens
if index < firstNonWhitespaceIndex
token.firstNonWhitespaceIndex = Math.min(index + token.value.length, firstNonWhitespaceIndex - index)
# Only the *last* segment of a soft-wrapped line can have trailing whitespace
if @lineEnding? and (index + token.value.length > firstTrailingWhitespaceIndex)
token.firstTrailingWhitespaceIndex = Math.max(0, firstTrailingWhitespaceIndex - index)
index += token.value.length
substituteInvisibleCharacters: ->
invisibles = @invisibles
changedText = false
for token, i in @tokens
if token.isHardTab
if invisibles.tab
token.value = invisibles.tab + token.value.substring(invisibles.tab.length)
token.hasInvisibleCharacters = true
changedText = true
else
if invisibles.space
if token.hasLeadingWhitespace() and not token.isSoftWrapIndentation
token.value = token.value.replace LeadingWhitespaceRegex, (leadingWhitespace) ->
leadingWhitespace.replace RepeatedSpaceRegex, invisibles.space
token.hasInvisibleCharacters = true
changedText = true
if token.hasTrailingWhitespace()
token.value = token.value.replace TrailingWhitespaceRegex, (leadingWhitespace) ->
leadingWhitespace.replace RepeatedSpaceRegex, invisibles.space
token.hasInvisibleCharacters = true
changedText = true
@text = @buildText() if changedText
buildEndOfLineInvisibles: ->
@endOfLineInvisibles = []
{cr, eol} = @invisibles
@@ -267,11 +487,13 @@ class TokenizedLine
@endOfLineInvisibles.push(eol) if eol
isComment: ->
for token in @tokens
continue if token.scopes.length is 1
continue if token.isOnlyWhitespace()
for scope in token.scopes
return true if _.contains(scope.split('.'), 'comment')
iterator = @getTokenIterator()
while iterator.next()
scopes = iterator.getScopes()
continue if scopes.length is 1
continue unless NonWhitespaceRegex.test(iterator.getText())
for scope in scopes
return true if CommentScopeRegex.test(scope)
break
false
@@ -282,40 +504,6 @@ class TokenizedLine
@tokens[index]
getTokenCount: ->
@tokens.length
bufferColumnForToken: (targetToken) ->
column = 0
for token in @tokens
return column if token is targetToken
column += token.bufferDelta
getScopeTree: ->
return @scopeTree if @scopeTree?
scopeStack = []
for token in @tokens
@updateScopeStack(scopeStack, token.scopes)
_.last(scopeStack).children.push(token)
@scopeTree = scopeStack[0]
@updateScopeStack(scopeStack, [])
@scopeTree
updateScopeStack: (scopeStack, desiredScopeDescriptor) ->
# Find a common prefix
for scope, i in desiredScopeDescriptor
break unless scopeStack[i]?.scope is desiredScopeDescriptor[i]
# Pop scopeDescriptor until we're at the common prefx
until scopeStack.length is i
poppedScope = scopeStack.pop()
_.last(scopeStack)?.children.push(poppedScope)
# Push onto common prefix until scopeStack equals desiredScopeDescriptor
for j in [i...desiredScopeDescriptor.length]
scopeStack.push(new Scope(desiredScopeDescriptor[j]))
class Scope
constructor: (@scope) ->
@children = []
count = 0
count++ for tag in @tags when tag >= 0
count

View File

@@ -57,9 +57,10 @@ class TooltipManager
# Essential: Add a tooltip to the given element.
#
# * `target` An `HTMLElement`
# * `options` See http://getbootstrap.com/javascript/#tooltips for a full list
# of options. You can also supply the following additional options:
# * `title` {String} Text in the tip.
# * `options` See http://getbootstrap.com/javascript/#tooltips-options for a
# full list of options. You can also supply the following additional options:
# * `title` A {String} or {Function} to use for the text in the tip. If
# given a function, `this` will be set to the `target` element.
# * `keyBindingCommand` A {String} containing a command name. If you specify
# this option and a key binding exists that matches the command, it will
# be appended to the title or rendered alone if no title is specified.
@@ -87,8 +88,9 @@ class TooltipManager
new Disposable ->
tooltip = $target.data('bs.tooltip')
tooltip.leave(currentTarget: target)
tooltip.hide()
if tooltip?
tooltip.leave(currentTarget: target)
tooltip.hide()
$target.tooltip('destroy')
humanizeKeystrokes = (keystroke) ->

106
src/typescript.coffee Normal file
View File

@@ -0,0 +1,106 @@
###
Cache for source code transpiled by TypeScript.
Inspired by https://github.com/atom/atom/blob/7a719d585db96ff7d2977db9067e1d9d4d0adf1a/src/babel.coffee
###
crypto = require 'crypto'
fs = require 'fs-plus'
path = require 'path'
tss = null # Defer until used
stats =
hits: 0
misses: 0
defaultOptions =
target: 1 # ES5
module: 'commonjs'
sourceMap: true
createTypeScriptVersionAndOptionsDigest = (version, options) ->
shasum = crypto.createHash('sha1')
# Include the version of typescript in the hash.
shasum.update('typescript', 'utf8')
shasum.update('\0', 'utf8')
shasum.update(version, 'utf8')
shasum.update('\0', 'utf8')
shasum.update(JSON.stringify(options))
shasum.digest('hex')
cacheDir = null
jsCacheDir = null
getCachePath = (sourceCode) ->
digest = crypto.createHash('sha1').update(sourceCode, 'utf8').digest('hex')
unless jsCacheDir?
tssVersion = require('typescript-simple/package.json').version
jsCacheDir = path.join(cacheDir, createTypeScriptVersionAndOptionsDigest(tssVersion, defaultOptions))
path.join(jsCacheDir, "#{digest}.js")
getCachedJavaScript = (cachePath) ->
if fs.isFileSync(cachePath)
try
cachedJavaScript = fs.readFileSync(cachePath, 'utf8')
stats.hits++
return cachedJavaScript
null
# Returns the TypeScript options that should be used to transpile filePath.
createOptions = (filePath) ->
options = filename: filePath
for key, value of defaultOptions
options[key] = value
options
transpile = (sourceCode, filePath, cachePath) ->
options = createOptions(filePath)
unless tss?
{TypeScriptSimple} = require 'typescript-simple'
tss = new TypeScriptSimple(options, false)
js = tss.compile(sourceCode, filePath)
stats.misses++
try
fs.writeFileSync(cachePath, js)
js
# Function that obeys the contract of an entry in the require.extensions map.
# Returns the transpiled version of the JavaScript code at filePath, which is
# either generated on the fly or pulled from cache.
loadFile = (module, filePath) ->
sourceCode = fs.readFileSync(filePath, 'utf8')
cachePath = getCachePath(sourceCode)
js = getCachedJavaScript(cachePath) ? transpile(sourceCode, filePath, cachePath)
module._compile(js, filePath)
register = ->
Object.defineProperty(require.extensions, '.ts', {
enumerable: true
writable: false
value: loadFile
})
setCacheDirectory = (newCacheDir) ->
if cacheDir isnt newCacheDir
cacheDir = newCacheDir
jsCacheDir = null
module.exports =
register: register
setCacheDirectory: setCacheDirectory
getCacheMisses: -> stats.misses
getCacheHits: -> stats.hits
# Visible for testing.
createTypeScriptVersionAndOptionsDigest: createTypeScriptVersionAndOptionsDigest
addPathToCache: (filePath) ->
return if path.extname(filePath) isnt '.ts'
sourceCode = fs.readFileSync(filePath, 'utf8')
cachePath = getCachePath(sourceCode)
transpile(sourceCode, filePath, cachePath)

View File

@@ -44,6 +44,7 @@ module.exports =
class ViewRegistry
documentPollingInterval: 200
documentUpdateRequested: false
documentReadInProgress: false
performDocumentPollAfterUpdate: false
pollIntervalHandle: null
@@ -161,7 +162,7 @@ class ViewRegistry
updateDocument: (fn) ->
@documentWriters.push(fn)
@requestDocumentUpdate()
@requestDocumentUpdate() unless @documentReadInProgress
new Disposable =>
@documentWriters = @documentWriters.filter (writer) -> writer isnt fn
@@ -178,11 +179,15 @@ class ViewRegistry
@documentPollers = @documentPollers.filter (poller) -> poller isnt fn
@stopPollingDocument() if @documentPollers.length is 0
pollAfterNextUpdate: ->
@performDocumentPollAfterUpdate = true
clearDocumentRequests: ->
@documentReaders = []
@documentWriters = []
@documentPollers = []
@documentUpdateRequested = false
@stopPollingDocument()
requestDocumentUpdate: ->
unless @documentUpdateRequested
@@ -192,8 +197,15 @@ class ViewRegistry
performDocumentUpdate: =>
@documentUpdateRequested = false
writer() while writer = @documentWriters.shift()
@documentReadInProgress = true
reader() while reader = @documentReaders.shift()
@performDocumentPoll() if @performDocumentPollAfterUpdate
@performDocumentPollAfterUpdate = false
@documentReadInProgress = false
# process updates requested as a result of reads
writer() while writer = @documentWriters.shift()
startPollingDocument: ->
@pollIntervalHandle = window.setInterval(@performDocumentPoll, @documentPollingInterval)
@@ -205,6 +217,5 @@ class ViewRegistry
if @documentUpdateRequested
@performDocumentPollAfterUpdate = true
else
@performDocumentPollAfterUpdate = false
poller() for poller in @documentPollers
return

View File

@@ -1,6 +1,5 @@
path = require 'path'
{$} = require './space-pen-extensions'
_ = require 'underscore-plus'
{Disposable} = require 'event-kit'
ipc = require 'ipc'
shell = require 'shell'
@@ -24,14 +23,16 @@ class WindowEventHandler
if pathToOpen? and needsProjectPaths
if fs.existsSync(pathToOpen)
atom.project.addPath(pathToOpen)
else if fs.existsSync(path.dirname(pathToOpen))
atom.project.addPath(path.dirname(pathToOpen))
else
dirToOpen = path.dirname(pathToOpen)
if fs.existsSync(dirToOpen)
atom.project.addPath(dirToOpen)
atom.project.addPath(pathToOpen)
unless fs.isDirectorySync(pathToOpen)
atom.workspace?.open(pathToOpen, {initialLine, initialColumn})
return
when 'update-available'
atom.updateAvailable(detail)
@@ -63,7 +64,10 @@ class WindowEventHandler
atom.storeDefaultWindowDimensions()
atom.storeWindowDimensions()
atom.unloadEditorWindow() if confirmed
if confirmed
atom.unloadEditorWindow()
else
ipc.send('cancel-window-close')
confirmed
@@ -83,7 +87,7 @@ class WindowEventHandler
if process.platform in ['win32', 'linux']
@subscribeToCommand $(window), 'window:toggle-menu-bar', ->
atom.config.set('core.autoHideMenuBar', !atom.config.get('core.autoHideMenuBar'))
atom.config.set('core.autoHideMenuBar', not atom.config.get('core.autoHideMenuBar'))
@subscribeToCommand $(document), 'core:focus-next', @focusNext
@@ -126,6 +130,7 @@ class WindowEventHandler
bindCommandToAction('core:undo', 'undo:')
bindCommandToAction('core:redo', 'redo:')
bindCommandToAction('core:select-all', 'selectAll:')
bindCommandToAction('core:cut', 'cut:')
onKeydown: (event) ->
atom.keymaps.handleKeyboardEvent(event)
@@ -134,12 +139,11 @@ class WindowEventHandler
onDrop: (event) ->
event.preventDefault()
event.stopPropagation()
pathsToOpen = _.pluck(event.dataTransfer.files, 'path')
atom.open({pathsToOpen}) if pathsToOpen.length > 0
onDragOver: (event) ->
event.preventDefault()
event.stopPropagation()
event.dataTransfer.dropEffect = 'none'
openLink: ({target, currentTarget}) ->
location = target?.getAttribute('href') or currentTarget?.getAttribute('href')
@@ -156,6 +160,7 @@ class WindowEventHandler
continue unless tabIndex >= 0
callback(element, tabIndex)
return
focusNext: =>
focusedTabIndex = parseInt($(':focus').attr('tabindex')) or -Infinity

View File

@@ -16,10 +16,10 @@ class WorkspaceElement extends HTMLElement
@initializeContent()
@observeScrollbarStyle()
@observeTextEditorFontConfig()
@createSpacePenShim()
@createSpacePenShim() if Grim.includeDeprecatedAPIs
attachedCallback: ->
callAttachHooks(this)
callAttachHooks(this) if Grim.includeDeprecatedAPIs
@focus()
detachedCallback: ->
@@ -44,7 +44,7 @@ class WorkspaceElement extends HTMLElement
@appendChild(@horizontalAxis)
observeScrollbarStyle: ->
@subscriptions.add scrollbarStyle.onValue (style) =>
@subscriptions.add scrollbarStyle.observePreferredScrollbarStyle (style) =>
switch style
when 'legacy'
@classList.remove('scrollbars-visible-when-scrolling')
@@ -82,7 +82,7 @@ class WorkspaceElement extends HTMLElement
@appendChild(@panelContainers.modal)
@__spacePenView.setModel(@model)
@__spacePenView.setModel(@model) if Grim.includeDeprecatedAPIs
this
getModel: -> @model
@@ -113,7 +113,10 @@ class WorkspaceElement extends HTMLElement
focusPaneViewOnRight: -> @paneContainer.focusPaneViewOnRight()
runPackageSpecs: ->
[projectPath] = atom.project.getPaths()
if activePath = atom.workspace.getActivePaneItem()?.getPath?()
[projectPath] = atom.project.relativizePath(activePath)
else
[projectPath] = atom.project.getPaths()
ipc.send('run-package-specs', path.join(projectPath, 'spec')) if projectPath
atom.commands.add 'atom-workspace',
@@ -123,6 +126,7 @@ atom.commands.add 'atom-workspace',
'application:about': -> ipc.send('command', 'application:about')
'application:run-all-specs': -> ipc.send('command', 'application:run-all-specs')
'application:run-benchmarks': -> ipc.send('command', 'application:run-benchmarks')
'application:show-preferences': -> ipc.send('command', 'application:show-settings')
'application:show-settings': -> ipc.send('command', 'application:show-settings')
'application:quit': -> ipc.send('command', 'application:quit')
'application:hide': -> ipc.send('command', 'application:hide')
@@ -136,6 +140,7 @@ atom.commands.add 'atom-workspace',
'application:open-folder': -> ipc.send('command', 'application:open-folder')
'application:open-dev': -> ipc.send('command', 'application:open-dev')
'application:open-safe': -> ipc.send('command', 'application:open-safe')
'application:add-project-folder': -> atom.addProjectFolder()
'application:minimize': -> ipc.send('command', 'application:minimize')
'application:zoom': -> ipc.send('command', 'application:zoom')
'application:bring-all-windows-to-front': -> ipc.send('command', 'application:bring-all-windows-to-front')
@@ -153,9 +158,9 @@ atom.commands.add 'atom-workspace',
'window:focus-pane-on-left': -> @focusPaneViewOnLeft()
'window:focus-pane-on-right': -> @focusPaneViewOnRight()
'window:save-all': -> @getModel().saveAll()
'window:toggle-invisibles': -> atom.config.toggle("editor.showInvisibles")
'window:toggle-invisibles': -> atom.config.set("editor.showInvisibles", not atom.config.get("editor.showInvisibles"))
'window:log-deprecation-warnings': -> Grim.logDeprecations()
'window:toggle-auto-indent': -> atom.config.toggle("editor.autoIndent")
'window:toggle-auto-indent': -> atom.config.set("editor.autoIndent", not atom.config.get("editor.autoIndent"))
'pane:reopen-closed-item': -> @getModel().reopenItem()
'core:close': -> @getModel().destroyActivePaneItemOrEmptyPane()
'core:save': -> @getModel().saveActivePaneItem()

View File

@@ -4,7 +4,6 @@ Q = require 'q'
_ = require 'underscore-plus'
Delegator = require 'delegato'
{deprecate, logDeprecationWarnings} = require 'grim'
scrollbarStyle = require 'scrollbar-style'
{$, $$, View} = require './space-pen-extensions'
fs = require 'fs-plus'
Workspace = require './workspace'
@@ -222,7 +221,6 @@ class WorkspaceView extends View
for editorElement in @panes.element.querySelectorAll('atom-pane > .item-views > atom-text-editor')
$(editorElement).view()
###
Section: Deprecated
###

View File

@@ -1,14 +1,14 @@
{deprecate} = require 'grim'
{includeDeprecatedAPIs, 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'
DefaultDirectorySearcher = require './default-directory-searcher'
Model = require './model'
TextEditor = require './text-editor'
PaneContainer = require './pane-container'
Pane = require './pane'
@@ -23,8 +23,8 @@ Task = require './task'
# 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.
# editors, and manipulate panes. To add panels, use {Workspace::addTopPanel}
# and friends.
#
# * `editor` {TextEditor} the new editor
#
@@ -33,30 +33,27 @@ 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
unless Grim.includeDeprecatedAPIs
@paneContainer = params?.paneContainer
@fullScreen = params?.fullScreen ? false
@destroyedItemURIs = params?.destroyedItemURIs ? []
@emitter = new Emitter
@openers = []
@paneContainer ?= new PaneContainer()
@paneContainer.onDidDestroyPaneItem(@didDestroyPaneItem)
@directorySearchers = []
@defaultDirectorySearcher = new DefaultDirectorySearcher()
atom.packages.serviceHub.consume(
'atom.directory-searcher',
'^0.1.0',
(provider) => @directorySearchers.unshift(provider))
@panelContainers =
top: new PanelContainer({location: 'top'})
left: new PanelContainer({location: 'left'})
@@ -86,6 +83,8 @@ class Workspace extends Model
atom.views.addViewProvider Panel, (model) ->
new PanelElement().initialize(model)
@subscribeToFontSize()
# Called by the Serializable mixin during deserialization
deserializeParams: (params) ->
for packageName in params.packagesWithActiveGrammars ? []
@@ -110,6 +109,7 @@ class Workspace extends Model
packageNames.push(packageName)
for scopeName in includedGrammarScopes ? []
addGrammar(atom.grammars.grammarForScopeName(scopeName))
return
editors = @getTextEditors()
addGrammar(editor.getGrammar()) for editor in editors
@@ -121,7 +121,7 @@ class Workspace extends Model
_.uniq(packageNames)
editorAdded: (editor) ->
@emit 'editor-created', editor
@emit 'editor-created', editor if includeDeprecatedAPIs
installShellCommands: ->
require('./command-installer').installShellCommandsInteractively()
@@ -341,39 +341,16 @@ class Workspace extends Model
@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.
# 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` A {String} containing a URI.
# * `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`.
@@ -408,7 +385,7 @@ class Workspace extends Model
# Open Atom's license in the active pane.
openLicense: ->
@open(join(atom.getLoadSettings().resourcePath, 'LICENSE.md'))
@open(path.join(process.resourcesPath, '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
@@ -424,7 +401,7 @@ class Workspace extends Model
# the containing pane. Defaults to `true`.
openSync: (uri='', options={}) ->
# TODO: Remove deprecated changeFocus option
if options.changeFocus?
if includeDeprecatedAPIs and options.changeFocus?
deprecate("The `changeFocus` option has been renamed to `activatePane`")
options.activatePane = options.changeFocus
delete options.changeFocus
@@ -435,7 +412,7 @@ class Workspace extends Model
uri = atom.project.resolvePath(uri)
item = @getActivePane().itemForURI(uri)
if uri
item ?= opener(uri, options) for opener in @getOpeners() when !item
item ?= opener(uri, options) for opener in @getOpeners() when not item
item ?= atom.project.openSync(uri, {initialLine, initialColumn})
@getActivePane().activateItem(item)
@@ -445,7 +422,7 @@ class Workspace extends Model
openURIInPane: (uri, pane, options={}) ->
# TODO: Remove deprecated changeFocus option
if options.changeFocus?
if includeDeprecatedAPIs and options.changeFocus?
deprecate("The `changeFocus` option has been renamed to `activatePane`")
options.activatePane = options.changeFocus
delete options.changeFocus
@@ -454,21 +431,22 @@ class Workspace extends Model
if uri?
item = pane.itemForURI(uri)
item ?= opener(uri, options) for opener in @getOpeners() when !item
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 'CANCELLED'
return Q()
when 'EACCES'
atom.notifications.addWarning("Permission denied '#{error.path}'")
return Q()
when 'EPERM', 'EBUSY'
atom.notifications.addWarning("Unable to open '#{error.path}'", detail: error.message)
return Q()
else
throw error
return Q()
Q(item)
.then (item) =>
@@ -481,7 +459,7 @@ class Workspace extends Model
if options.initialLine? or options.initialColumn?
item.setCursorBufferPosition?([options.initialLine, options.initialColumn])
index = pane.getActiveItemIndex()
@emit "uri-opened"
@emit "uri-opened" if includeDeprecatedAPIs
@emitter.emit 'did-open', {uri, pane, item, index}
item
@@ -495,12 +473,6 @@ class Workspace extends Model
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.
@@ -518,56 +490,28 @@ class Workspace extends Model
# Returns a {Disposable} on which `.dispose()` can be called to remove the
# opener.
addOpener: (opener) ->
packageName = @getCallingPackageName()
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})
item
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)
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)
@openers.push(wrappedOpener)
new Disposable => _.remove(@openers, wrappedOpener)
else
@openers.push(opener)
new Disposable => _.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
###
@@ -598,11 +542,6 @@ class Workspace extends Model
activeItem = @getActivePaneItem()
activeItem if activeItem instanceof TextEditor
# Deprecated
getActiveEditor: ->
Grim.deprecate "Call ::getActiveTextEditor instead"
@getActivePane()?.getActiveEditor()
# Save all pane items.
saveAll: ->
@paneContainer.saveAll()
@@ -666,10 +605,6 @@ class Workspace extends Model
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.
@@ -695,9 +630,14 @@ class Workspace extends Model
fontSize = atom.config.get("editor.fontSize")
atom.config.set("editor.fontSize", fontSize - 1) if fontSize > 1
# Restore to a default editor font size.
# Restore to the window's original editor font size.
resetFontSize: ->
atom.config.unset("editor.fontSize")
if @originalFontSize
atom.config.set("editor.fontSize", @originalFontSize)
subscribeToFontSize: ->
atom.config.onDidChange 'editor.fontSize', ({oldValue}) =>
@originalFontSize ?= oldValue
# Removes the item's uri from the list of potential items to reopen.
itemOpened: (item) ->
@@ -860,36 +800,65 @@ class Workspace extends Model
# * `regex` {RegExp} to search with.
# * `options` (optional) {Object} (default: {})
# * `paths` An {Array} of glob patterns to search within
# * `onPathsSearched` (optional) {Function}
# * `iterator` {Function} callback on each file found
#
# Returns a `Promise`.
# Returns a `Promise` with a `cancel()` method that will cancel all
# of the underlying searches that were started as part of this scan.
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)
# Find a searcher for every Directory in the project. Each searcher that is matched
# will be associated with an Array of Directory objects in the Map.
directoriesForSearcher = new Map()
for directory in atom.project.getDirectories()
searcher = @defaultDirectorySearcher
for directorySearcher in @directorySearchers
if directorySearcher.canSearchDirectory(directory)
searcher = directorySearcher
break
directories = directoriesForSearcher.get(searcher)
unless directories
directories = []
directoriesForSearcher.set(searcher, directories)
directories.push(directory)
# Define the onPathsSearched callback.
if _.isFunction(options.onPathsSearched)
task.on 'scan:paths-searched', (numberOfPathsSearched) ->
options.onPathsSearched(numberOfPathsSearched)
# Maintain a map of directories to the number of search results. When notified of a new count,
# replace the entry in the map and update the total.
onPathsSearchedOption = options.onPathsSearched
totalNumberOfPathsSearched = 0
numberOfPathsSearchedForSearcher = new Map()
onPathsSearched = (searcher, numberOfPathsSearched) ->
oldValue = numberOfPathsSearchedForSearcher.get(searcher)
if oldValue
totalNumberOfPathsSearched -= oldValue
numberOfPathsSearchedForSearcher.set(searcher, numberOfPathsSearched)
totalNumberOfPathsSearched += numberOfPathsSearched
onPathsSearchedOption(totalNumberOfPathsSearched)
else
onPathsSearched = ->
# Kick off all of the searches and unify them into one Promise.
allSearches = []
directoriesForSearcher.forEach (directories, searcher) ->
searchOptions =
inclusions: options.paths or []
includeHidden: true
excludeVcsIgnores: atom.config.get('core.excludeVcsIgnoredPaths')
exclusions: atom.config.get('core.ignoredNames')
follow: atom.config.get('core.followSymlinks')
didMatch: (result) ->
iterator(result) unless atom.project.isPathModified(result.filePath)
didError: (error) ->
iterator(null, error)
didSearchPaths: (count) -> onPathsSearched(searcher, count)
directorySearcher = searcher.search(directories, regex, searchOptions)
allSearches.push(directorySearcher)
searchPromise = Promise.all(allSearches)
for buffer in atom.project.getBuffers() when buffer.isModified()
filePath = buffer.getPath()
@@ -898,11 +867,31 @@ class Workspace extends Model
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
# Make sure the Promise that is returned to the client is cancelable. To be consistent
# with the existing behavior, instead of cancel() rejecting the promise, it should
# resolve it with the special value 'cancelled'. At least the built-in find-and-replace
# package relies on this behavior.
isCancelled = false
cancellablePromise = new Promise (resolve, reject) ->
onSuccess = ->
if isCancelled
resolve('cancelled')
else
resolve(null)
searchPromise.then(onSuccess, reject)
cancellablePromise.cancel = ->
isCancelled = true
# Note that cancelling all of the members of allSearches will cause all of the searches
# to resolve, which causes searchPromise to resolve, which is ultimately what causes
# cancellablePromise to resolve.
promise.cancel() for promise in allSearches
# Although this method claims to return a `Promise`, the `ResultsPaneView.onSearch()`
# method in the find-and-replace package expects the object returned by this method to have a
# `done()` method. Include a done() method until find-and-replace can be updated.
cancellablePromise.done = (onSuccessOrFailure) ->
cancellablePromise.then(onSuccessOrFailure, onSuccessOrFailure)
cancellablePromise
# Public: Performs a replace across all the specified files in the project.
#
@@ -919,8 +908,8 @@ class Workspace extends Model
openPaths = (buffer.getPath() for buffer in atom.project.getBuffers())
outOfProcessPaths = _.difference(filePaths, openPaths)
inProcessFinished = !openPaths.length
outOfProcessFinished = !outOfProcessPaths.length
inProcessFinished = not openPaths.length
outOfProcessFinished = not outOfProcessPaths.length
checkFinished = ->
deferred.resolve() if outOfProcessFinished and inProcessFinished
@@ -944,3 +933,96 @@ class Workspace extends Model
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)