mirror of
https://github.com/atom/atom.git
synced 2026-02-14 00:25:08 -05:00
Merge branch 'master' into add-subword-cursors-4
Conflicts: spec/text-editor-spec.coffee
This commit is contained in:
269
src/atom.coffee
269
src/atom.coffee
@@ -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")
|
||||
|
||||
@@ -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().
|
||||
|
||||
@@ -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("+")
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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, ->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)?
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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] ?= {})
|
||||
|
||||
@@ -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
|
||||
|
||||
110
src/custom-gutter-component.coffee
Normal file
110
src/custom-gutter-component.coffee
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
#
|
||||
|
||||
95
src/default-directory-searcher.coffee
Normal file
95
src/default-directory-searcher.coffee
Normal 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()
|
||||
}
|
||||
43
src/deprecated-packages.coffee
Normal file
43
src/deprecated-packages.coffee
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
28
src/gutter-component-helpers.coffee
Normal file
28
src/gutter-component-helpers.coffee
Normal 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
|
||||
111
src/gutter-container-component.coffee
Normal file
111
src/gutter-container-component.coffee
Normal 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()
|
||||
96
src/gutter-container.coffee
Normal file
96
src/gutter-container.coffee
Normal 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
69
src/gutter.coffee
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}
|
||||
|
||||
97
src/line-number-gutter-component.coffee
Normal file
97
src/line-number-gutter-component.coffee
Normal 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)
|
||||
@@ -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)
|
||||
@@ -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 ' '
|
||||
|
||||
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()
|
||||
|
||||
368
src/lines-tile-component.coffee
Normal file
368
src/lines-tile-component.coffee
Normal 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 ' '
|
||||
|
||||
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 '&'
|
||||
when '"' then '"'
|
||||
when "'" then '''
|
||||
when '<' then '<'
|
||||
when '>' then '>'
|
||||
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()
|
||||
12
src/marker-observation-window.coffee
Normal file
12
src/marker-observation-window.coffee
Normal 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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
34
src/model.coffee
Normal 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()
|
||||
@@ -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']
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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: ->
|
||||
|
||||
68
src/pane-resize-handle-element.coffee
Normal file
68
src/pane-resize-handle-element.coffee
Normal 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
|
||||
@@ -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?
|
||||
|
||||
|
||||
190
src/pane.coffee
190
src/pane.coffee
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: ->
|
||||
|
||||
6
src/safe-clipboard.coffee
Normal file
6
src/safe-clipboard.coffee
Normal 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')
|
||||
@@ -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, ->
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -12,6 +12,9 @@ class ScrollbarComponent
|
||||
|
||||
@domNode.addEventListener 'scroll', @onScrollCallback
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
updateSync: (state) ->
|
||||
@oldState ?= {}
|
||||
switch @orientation
|
||||
|
||||
@@ -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 ?= {}
|
||||
|
||||
@@ -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()
|
||||
|
||||
6
src/special-token-symbols.coffee
Normal file
6
src/special-token-symbols.coffee
Normal 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
27
src/storage-folder.coffee
Normal 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
|
||||
@@ -152,6 +152,8 @@ class StyleManager
|
||||
for styleElement in styleElementsToRestore
|
||||
@addStyleElement(styleElement) unless styleElement in existingStyleElements
|
||||
|
||||
return
|
||||
|
||||
###
|
||||
Section: Paths
|
||||
###
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
51
src/tiled-component.coffee
Normal file
51
src/tiled-component.coffee
Normal 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
83
src/token-iterator.coffee
Normal 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()
|
||||
202
src/token.coffee
202
src/token.coffee
@@ -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 '&'
|
||||
when '"' then '"'
|
||||
when "'" then '''
|
||||
when '<' then '<'
|
||||
when '>' then '>'
|
||||
else match
|
||||
|
||||
hasLeadingWhitespace: ->
|
||||
@firstNonWhitespaceIndex? and @firstNonWhitespaceIndex > 0
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
106
src/typescript.coffee
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
###
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user