Merge remote-tracking branch 'upstream/master' into move-lines-up-and-down-with-multiple-selections

This commit is contained in:
Luke Pommersheim
2015-09-23 08:38:37 +02:00
163 changed files with 3401 additions and 14348 deletions

View File

@@ -11,7 +11,6 @@ _ = require 'underscore-plus'
fs = require 'fs-plus'
{mapSourcePosition} = require 'source-map-support'
Model = require './model'
{$} = require './space-pen-extensions'
WindowEventHandler = require './window-event-handler'
StylesElement = require './styles-element'
StorageFolder = require './storage-folder'
@@ -35,28 +34,12 @@ class Atom extends Model
atom.deserializeTimings.atom = Date.now() - startTime
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.
"""
Object.defineProperty atom, 'workspaceView',
get: ->
deprecate(workspaceViewDeprecationMessage)
atom.__workspaceView
set: (newValue) ->
deprecate(workspaceViewDeprecationMessage)
atom.__workspaceView = newValue
Object.defineProperty atom, 'services',
get: ->
deprecate(serviceHubDeprecationMessage)
@@ -123,7 +106,7 @@ class Atom extends Model
@getCurrentWindow: ->
remote.getCurrentWindow()
workspaceViewParentSelector: 'body'
workspaceParentSelectorctor: 'body'
lastUncaughtError: null
###
@@ -484,7 +467,7 @@ class Atom extends Model
# Extended: Focus the current window.
focus: ->
ipc.send('call-window-method', 'focus')
$(window).focus()
window.focus()
# Extended: Show the current window.
show: ->
@@ -680,7 +663,6 @@ class Atom extends Model
# Essential: Visually and audibly trigger a beep.
beep: ->
shell.beep() if @config.get('core.audioBeep')
@__workspaceView?.trigger 'beep'
@emitter.emit 'did-beep'
# Essential: A flexible way to open a dialog akin to an alert dialog.
@@ -761,24 +743,18 @@ class Atom extends Model
@project ?= @deserializers.deserialize(@state.project) ? new Project()
@deserializeTimings.project = Date.now() - startTime
deserializeWorkspaceView: ->
deserializeWorkspace: ->
Workspace = require './workspace'
if includeDeprecatedAPIs
WorkspaceView = require './workspace-view'
startTime = Date.now()
@workspace = Workspace.deserialize(@state.workspace) ? new Workspace
workspaceElement = @views.getView(@workspace)
if includeDeprecatedAPIs
@__workspaceView = workspaceElement.__spacePenView
@deserializeTimings.workspace = Date.now() - startTime
@keymaps.defaultTarget = workspaceElement
document.querySelector(@workspaceViewParentSelector).appendChild(workspaceElement)
document.querySelector(@workspaceParentSelectorctor).appendChild(workspaceElement)
deserializePackageStates: ->
@packages.packageStates = @state.packageStates ? {}
@@ -787,7 +763,7 @@ class Atom extends Model
deserializeEditorWindow: ->
@deserializePackageStates()
@deserializeProject()
@deserializeWorkspaceView()
@deserializeWorkspace()
loadConfig: ->
@config.setSchema null, {type: 'object', properties: _.clone(require('./config-schema'))}
@@ -897,6 +873,11 @@ class Atom extends Model
ipc.send('call-window-method', 'setAutoHideMenuBar', autoHide)
ipc.send('call-window-method', 'setMenuBarVisibility', not autoHide)
# Preserve this deprecation until 2.0. Sorry. Should have removed Q sooner.
Promise.prototype.done = (callback) ->
deprecate("Atom now uses ES6 Promises instead of Q. Call promise.then instead of promise.done")
@then(callback)
if includeDeprecatedAPIs
# Deprecated: Callers should be converted to use atom.deserializers
Atom::registerRepresentationClass = ->

View File

@@ -17,6 +17,8 @@ url = require 'url'
{EventEmitter} = require 'events'
_ = require 'underscore-plus'
LocationSuffixRegExp = /(:\d+)(:\d+)?$/
DefaultSocketPath =
if process.platform is 'win32'
'\\\\.\\pipe\\atom-sock'
@@ -63,15 +65,11 @@ class AtomApplication
exit: (status) -> app.exit(status)
constructor: (options) ->
{@resourcePath, @version, @devMode, @safeMode, @socketPath} = options
# Normalize to make sure drive letter case is consistent on Windows
@resourcePath = path.normalize(@resourcePath) if @resourcePath
{@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath} = options
global.atomApplication = this
@pidsToOpenWindows = {}
@pathsToOpen ?= []
@windows = []
@autoUpdateManager = new AutoUpdateManager(@version, options.test)
@@ -88,11 +86,11 @@ class AtomApplication
else
@loadState() or @openPath(options)
openWithOptions: ({pathsToOpen, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, specDirectory, logFile, profileStartup}) ->
openWithOptions: ({pathsToOpen, executedFrom, 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, profileStartup})
@openPaths({pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup})
else if urlsToOpen.length > 0
@openUrl({urlToOpen, devMode, safeMode}) for urlToOpen in urlsToOpen
else
@@ -163,8 +161,7 @@ class AtomApplication
devMode: @focusedWindow()?.devMode
safeMode: @focusedWindow()?.safeMode
@on 'application:run-all-specs', -> @runSpecs(exitWhenDone: false, resourcePath: global.devResourcePath, safeMode: @focusedWindow()?.safeMode)
@on 'application:run-benchmarks', -> @runBenchmarks()
@on 'application:run-all-specs', -> @runSpecs(exitWhenDone: false, resourcePath: @devResourcePath, safeMode: @focusedWindow()?.safeMode)
@on 'application:quit', -> app.quit()
@on 'application:new-window', -> @openPath(_.extend(windowDimensions: @focusedWindow()?.getDimensions(), getLoadSettings()))
@on 'application:new-file', -> (@focusedWindow() ? this).openPath()
@@ -255,7 +252,7 @@ class AtomApplication
@applicationMenu.update(win, template, keystrokesByCommand)
ipc.on 'run-package-specs', (event, specDirectory) =>
@runSpecs({resourcePath: global.devResourcePath, specDirectory: specDirectory, exitWhenDone: false})
@runSpecs({resourcePath: @devResourcePath, specDirectory: specDirectory, exitWhenDone: false})
ipc.on 'command', (event, command) =>
@emit(command)
@@ -371,14 +368,9 @@ 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, profileStartup, window}={}) ->
pathsToOpen = pathsToOpen.map (pathToOpen) ->
if fs.existsSync(pathToOpen)
fs.normalize(pathToOpen)
else
pathToOpen
locationsToOpen = (@locationForPathToOpen(pathToOpen) for pathToOpen in pathsToOpen)
openPaths: ({pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window}={}) ->
locationsToOpen = (@locationForPathToOpen(pathToOpen, executedFrom) for pathToOpen in pathsToOpen)
pathsToOpen = (locationToOpen.pathToOpen for locationToOpen in locationsToOpen)
unless pidToKillWhenClosed or newWindow
existingWindow = @windowForPaths(pathsToOpen, devMode)
@@ -401,8 +393,8 @@ class AtomApplication
else
if devMode
try
bootstrapScript = require.resolve(path.join(global.devResourcePath, 'src', 'window-bootstrap'))
resourcePath = global.devResourcePath
bootstrapScript = require.resolve(path.join(@devResourcePath, 'src', 'window-bootstrap'))
resourcePath = @devResourcePath
bootstrapScript ?= require.resolve('../window-bootstrap')
resourcePath ?= @resourcePath
@@ -503,7 +495,7 @@ class AtomApplication
resourcePath = @resourcePath
try
bootstrapScript = require.resolve(path.resolve(global.devResourcePath, 'spec', 'spec-bootstrap'))
bootstrapScript = require.resolve(path.resolve(@devResourcePath, 'spec', 'spec-bootstrap'))
catch error
bootstrapScript = require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'spec-bootstrap'))
@@ -512,33 +504,22 @@ class AtomApplication
safeMode ?= false
new AtomWindow({bootstrapScript, resourcePath, exitWhenDone, isSpec, devMode, specDirectory, logFile, safeMode})
runBenchmarks: ({exitWhenDone, specDirectory}={}) ->
try
bootstrapScript = require.resolve(path.resolve(global.devResourcePath, 'benchmark', 'benchmark-bootstrap'))
catch error
bootstrapScript = require.resolve(path.resolve(__dirname, '..', '..', 'benchmark', 'benchmark-bootstrap'))
specDirectory ?= path.dirname(bootstrapScript)
isSpec = true
devMode = true
new AtomWindow({bootstrapScript, @resourcePath, exitWhenDone, isSpec, specDirectory, devMode})
locationForPathToOpen: (pathToOpen) ->
locationForPathToOpen: (pathToOpen, executedFrom='') ->
return {pathToOpen} unless pathToOpen
return {pathToOpen} if url.parse(pathToOpen).protocol?
return {pathToOpen} if fs.existsSync(pathToOpen)
pathToOpen = pathToOpen.replace(/[:\s]+$/, '')
match = pathToOpen.match(LocationSuffixRegExp)
[fileToOpen, initialLine, initialColumn] = path.basename(pathToOpen).split(':')
return {pathToOpen} unless initialLine
return {pathToOpen} unless parseInt(initialLine) >= 0
if match?
pathToOpen = pathToOpen.slice(0, -match[0].length)
initialLine = Math.max(0, parseInt(match[1].slice(1)) - 1) if match[1]
initialColumn = Math.max(0, parseInt(match[2].slice(1)) - 1) if match[2]
else
initialLine = initialColumn = null
unless url.parse(pathToOpen).protocol?
pathToOpen = path.resolve(executedFrom, fs.normalize(pathToOpen))
# Convert line numbers to a base of 0
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}
# Opens a native dialog to prompt the user for a path.

View File

@@ -23,9 +23,6 @@ class AtomWindow
locationsToOpen ?= [{pathToOpen}] if pathToOpen
locationsToOpen ?= []
# Normalize to make sure drive letter case is consistent on Windows
@resourcePath = path.normalize(@resourcePath) if @resourcePath
options =
show: false
title: 'Atom'

View File

@@ -1,27 +1,23 @@
global.shellStartTime = Date.now()
process.on 'uncaughtException', (error={}) ->
console.log(error.message) if error.message?
console.log(error.stack) if error.stack?
crashReporter = require 'crash-reporter'
app = require 'app'
fs = require 'fs-plus'
path = require 'path'
yargs = require 'yargs'
url = require 'url'
nslog = require 'nslog'
console.log = nslog
process.on 'uncaughtException', (error={}) ->
nslog(error.message) if error.message?
nslog(error.stack) if error.stack?
console.log = require 'nslog'
start = ->
setupAtomHome()
setupCompileCache()
return if handleStartupEventWithSquirrel()
if process.platform is 'win32'
SquirrelUpdate = require './squirrel-update'
squirrelCommand = process.argv[1]
return if SquirrelUpdate.handleStartupEvent(app, squirrelCommand)
# NB: This prevents Win10 from showing dupe items in the taskbar
app.setAppUserModelId('com.squirrel.atom.atom')
args = parseCommandLine()
@@ -29,49 +25,40 @@ start = ->
event.preventDefault()
args.pathsToOpen.push(pathToOpen)
args.urlsToOpen = []
addUrlToOpen = (event, urlToOpen) ->
event.preventDefault()
args.urlsToOpen.push(urlToOpen)
app.on 'open-file', addPathToOpen
app.on 'open-url', addUrlToOpen
app.on 'will-finish-launching', ->
setupCrashReporter()
app.on 'will-finish-launching', setupCrashReporter
app.on 'ready', ->
app.removeListener 'open-file', addPathToOpen
app.removeListener 'open-url', addUrlToOpen
cwd = args.executedFrom?.toString() or process.cwd()
args.pathsToOpen = args.pathsToOpen.map (pathToOpen) ->
normalizedPath = fs.normalize(pathToOpen)
if url.parse(pathToOpen).protocol?
pathToOpen
else if cwd
path.resolve(cwd, normalizedPath)
else
path.resolve(pathToOpen)
if args.devMode
AtomApplication = require path.join(args.resourcePath, 'src', 'browser', 'atom-application')
else
AtomApplication = require './atom-application'
AtomApplication = require path.join(args.resourcePath, 'src', 'browser', 'atom-application')
AtomApplication.open(args)
console.log("App load time: #{Date.now() - global.shellStartTime}ms") unless args.test
global.devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH ? path.join(app.getHomeDir(), 'github', 'atom')
# Normalize to make sure drive letter case is consistent on Windows
global.devResourcePath = path.normalize(global.devResourcePath) if global.devResourcePath
normalizeDriveLetterName = (filePath) ->
if process.platform is 'win32'
filePath.replace /^([a-z]):/, ([driveLetter]) -> driveLetter.toUpperCase() + ":"
else
filePath
handleStartupEventWithSquirrel = ->
return false unless process.platform is 'win32'
SquirrelUpdate = require './squirrel-update'
squirrelCommand = process.argv[1]
SquirrelUpdate.handleStartupEvent(app, squirrelCommand)
setupCrashReporter = ->
crashReporter.start(productName: 'Atom', companyName: 'GitHub')
setupAtomHome = ->
return if process.env.ATOM_HOME
atomHome = path.join(app.getHomeDir(), '.atom')
try
atomHome = fs.realpathSync(atomHome)
@@ -129,7 +116,7 @@ parseCommandLine = ->
process.stdout.write("#{version}\n")
process.exit(0)
executedFrom = args['executed-from']
executedFrom = args['executed-from']?.toString() ? process.cwd()
devMode = args['dev']
safeMode = args['safe']
pathsToOpen = args._
@@ -140,6 +127,8 @@ parseCommandLine = ->
logFile = args['log-file']
socketPath = args['socket-path']
profileStartup = args['profile-startup']
urlsToOpen = []
devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH ? path.join(app.getHomeDir(), 'github', 'atom')
if args['resource-path']
devMode = true
@@ -155,7 +144,7 @@ parseCommandLine = ->
resourcePath = packageDirectoryPath if packageManifest.name is 'atom'
if devMode
resourcePath ?= global.devResourcePath
resourcePath ?= devResourcePath
unless fs.statSyncNoException(resourcePath)
resourcePath = path.dirname(path.dirname(__dirname))
@@ -164,7 +153,11 @@ parseCommandLine = ->
# explicitly pass it by command line, see http://git.io/YC8_Ew.
process.env.PATH = args['path-environment'] if args['path-environment']
{resourcePath, pathsToOpen, executedFrom, test, version, pidToKillWhenClosed,
devMode, safeMode, newWindow, specDirectory, logFile, socketPath, profileStartup}
resourcePath = normalizeDriveLetterName(resourcePath)
devResourcePath = normalizeDriveLetterName(devResourcePath)
{resourcePath, devResourcePath, pathsToOpen, urlsToOpen, executedFrom, test,
version, pidToKillWhenClosed, devMode, safeMode, newWindow, specDirectory,
logFile, socketPath, profileStartup}
start()

View File

@@ -70,6 +70,7 @@ module.exports =
try
error = null
symlinkCommandWithPrivilegeSync(commandPath, destinationPath)
catch error
catch err
error = err
callback?(error)

View File

@@ -1,7 +1,6 @@
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
{calculateSpecificity, validateSelector} = require 'clear-cut'
_ = require 'underscore-plus'
{$} = require './space-pen-extensions'
SequenceCount = 0
@@ -138,8 +137,6 @@ class CommandRegistry
# * `name` The name of the command. For example, `user:insert-date`.
# * `displayName` The display name of the command. For example,
# `User: Insert Date`.
# * `jQuery` Present if the command was registered with the legacy
# `$::command` method.
findCommands: ({target}) ->
commandNames = new Set
commands = []
@@ -175,12 +172,8 @@ class CommandRegistry
# * `commandName` {String} indicating the name of the command to dispatch.
dispatch: (target, commandName, detail) ->
event = new CustomEvent(commandName, {bubbles: true, detail})
eventWithTarget = Object.create event,
target: value: target
preventDefault: value: ->
stopPropagation: value: ->
stopImmediatePropagation: value: ->
@handleCommandEvent(eventWithTarget)
Object.defineProperty(event, 'target', value: target)
@handleCommandEvent(event)
# Public: Invoke the given callback before dispatching a command event.
#
@@ -208,34 +201,39 @@ class CommandRegistry
@selectorBasedListenersByCommandName[commandName] = listeners.slice()
return
handleCommandEvent: (originalEvent) =>
handleCommandEvent: (event) =>
propagationStopped = false
immediatePropagationStopped = false
matched = false
currentTarget = originalEvent.target
currentTarget = event.target
{preventDefault, stopPropagation, stopImmediatePropagation, abortKeyBinding} = event
syntheticEvent = Object.create originalEvent,
eventPhase: value: Event.BUBBLING_PHASE
currentTarget: get: -> currentTarget
preventDefault: value: ->
originalEvent.preventDefault()
stopPropagation: value: ->
originalEvent.stopPropagation()
propagationStopped = true
stopImmediatePropagation: value: ->
originalEvent.stopImmediatePropagation()
propagationStopped = true
immediatePropagationStopped = true
abortKeyBinding: value: ->
originalEvent.abortKeyBinding?()
dispatchedEvent = new CustomEvent(event.type, {bubbles: true, detail: event.detail})
Object.defineProperty dispatchedEvent, 'eventPhase', value: Event.BUBBLING_PHASE
Object.defineProperty dispatchedEvent, 'currentTarget', get: -> currentTarget
Object.defineProperty dispatchedEvent, 'target', value: currentTarget
Object.defineProperty dispatchedEvent, 'preventDefault', value: ->
event.preventDefault()
Object.defineProperty dispatchedEvent, 'stopPropagation', value: ->
event.stopPropagation()
propagationStopped = true
Object.defineProperty dispatchedEvent, 'stopImmediatePropagation', value: ->
event.stopImmediatePropagation()
propagationStopped = true
immediatePropagationStopped = true
Object.defineProperty dispatchedEvent, 'abortKeyBinding', value: ->
event.abortKeyBinding?()
@emitter.emit 'will-dispatch', syntheticEvent
for key in Object.keys(event)
dispatchedEvent[key] = event[key]
@emitter.emit 'will-dispatch', dispatchedEvent
loop
listeners = @inlineListenersByCommandName[originalEvent.type]?.get(currentTarget) ? []
listeners = @inlineListenersByCommandName[event.type]?.get(currentTarget) ? []
if currentTarget.webkitMatchesSelector?
selectorBasedListeners =
(@selectorBasedListenersByCommandName[originalEvent.type] ? [])
(@selectorBasedListenersByCommandName[event.type] ? [])
.filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector)
.sort (a, b) -> a.compare(b)
listeners = listeners.concat(selectorBasedListeners)
@@ -244,13 +242,13 @@ class CommandRegistry
for listener in listeners
break if immediatePropagationStopped
listener.callback.call(currentTarget, syntheticEvent)
listener.callback.call(currentTarget, dispatchedEvent)
break if currentTarget is window
break if propagationStopped
currentTarget = currentTarget.parentNode ? window
@emitter.emit 'did-dispatch', syntheticEvent
@emitter.emit 'did-dispatch', dispatchedEvent
matched

View File

@@ -16,7 +16,7 @@ var cacheDirectory = null
exports.setAtomHomeDirectory = function (atomHome) {
var cacheDir = path.join(atomHome, 'compile-cache')
if (process.env.USER === 'root' && process.env.SUDO_USER && process.env.SUDO_USER !== process.env.USER) {
cacheDir = path.join(cacheDirectory, 'root')
cacheDir = path.join(cacheDir, 'root')
}
this.setCacheDirectory(cacheDir)
}
@@ -108,7 +108,7 @@ require('source-map-support').install({
// source-map-support module, but we've overridden it to read the javascript
// code from our cache directory.
retrieveSourceMap: function (filePath) {
if (!fs.isFileSync(filePath)) {
if (!cacheDirectory || !fs.isFileSync(filePath)) {
return null
}

View File

@@ -22,32 +22,6 @@ class Cursor extends Model
@assignId(id)
@updateVisibility()
@marker.onDidChange (e) =>
@updateVisibility()
{oldHeadScreenPosition, newHeadScreenPosition} = e
{oldHeadBufferPosition, newHeadBufferPosition} = e
{textChanged} = e
return if oldHeadScreenPosition.isEqual(newHeadScreenPosition)
@goalColumn = null
movedEvent =
oldBufferPosition: oldHeadBufferPosition
oldScreenPosition: oldHeadScreenPosition
newBufferPosition: newHeadBufferPosition
newScreenPosition: newHeadScreenPosition
textChanged: textChanged
cursor: this
@emit 'moved', movedEvent if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change-position', movedEvent
@editor.cursorMoved(movedEvent)
@marker.onDidDestroy =>
@destroyed = true
@editor.removeCursor(this)
@emit 'destroyed' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-destroy'
@emitter.dispose()
destroy: ->
@marker.destroy()

View File

@@ -98,7 +98,6 @@ class CustomGutterComponent
delete @decorationItemsById[decorationId]
if newItem
# `item` should be either an HTMLElement or a space-pen View.
newItemNode = null
if newItem instanceof HTMLElement
newItemNode = newItem

41
src/delegated-listener.js Normal file
View File

@@ -0,0 +1,41 @@
const EventKit = require('event-kit')
module.exports =
function listen (element, eventName, selector, handler) {
var innerHandler = function (event) {
if (selector) {
var currentTarget = event.target
while (true) {
if (currentTarget.matches && currentTarget.matches(selector)) {
handler({
type: event.type,
currentTarget: currentTarget,
target: event.target,
preventDefault: function () {
event.preventDefault()
},
originalEvent: event
})
}
if (currentTarget === element) break
currentTarget = currentTarget.parentNode
}
} else {
handler({
type: event.type,
currentTarget: event.currentTarget,
target: event.target,
preventDefault: function () {
event.preventDefault()
},
originalEvent: event
})
}
}
element.addEventListener(eventName, innerHandler)
return new EventKit.Disposable(function () {
element.removeEventListener(eventName, innerHandler)
})
}

View File

@@ -0,0 +1,42 @@
module.exports =
class DOMElementPool
constructor: ->
@freeElementsByTagName = {}
@freedElements = new Set
clear: ->
@freedElements.clear()
for tagName, freeElements of @freeElementsByTagName
freeElements.length = 0
return
build: (tagName, className, textContent = "") ->
element = @freeElementsByTagName[tagName]?.pop()
element ?= document.createElement(tagName)
delete element.dataset[dataId] for dataId of element.dataset
element.removeAttribute("class")
element.removeAttribute("style")
element.className = className if className?
element.textContent = textContent
@freedElements.delete(element)
element
freeElementAndDescendants: (element) ->
@free(element)
for index in [element.children.length - 1..0] by -1
child = element.children[index]
@freeElementAndDescendants(child)
return
free: (element) ->
throw new Error("The element cannot be null or undefined.") unless element?
throw new Error("The element has already been freed!") if @freedElements.has(element)
tagName = element.tagName.toLowerCase()
@freeElementsByTagName[tagName] ?= []
@freeElementsByTagName[tagName].push(element)
@freedElements.add(element)
element.remove()

View File

@@ -7,7 +7,7 @@ LineNumberGutterComponent = require './line-number-gutter-component'
module.exports =
class GutterContainerComponent
constructor: ({@onLineNumberGutterMouseDown, @editor}) ->
constructor: ({@onLineNumberGutterMouseDown, @editor, @domElementPool}) ->
# An array of objects of the form: {name: {String}, component: {Object}}
@gutterComponents = []
@gutterComponentsByGutterName = {}
@@ -39,7 +39,7 @@ class GutterContainerComponent
gutterComponent = @gutterComponentsByGutterName[gutter.name]
if not gutterComponent
if gutter.name is 'line-number'
gutterComponent = new LineNumberGutterComponent({onMouseDown: @onLineNumberGutterMouseDown, @editor, gutter})
gutterComponent = new LineNumberGutterComponent({onMouseDown: @onLineNumberGutterMouseDown, @editor, gutter, @domElementPool})
@lineNumberGutterComponent = gutterComponent
else
gutterComponent = new CustomGutterComponent({gutter})

View File

@@ -5,12 +5,11 @@ module.exports =
class HighlightsComponent
oldState: null
constructor: ->
constructor: (@domElementPool) ->
@highlightNodesById = {}
@regionNodesByHighlightId = {}
@domNode = document.createElement('div')
@domNode.classList.add('highlights')
@domNode = @domElementPool.build("div", "highlights")
getDomNode: ->
@domNode
@@ -30,8 +29,7 @@ class HighlightsComponent
# add or update highlights
for id, highlightState of newState
unless @oldState[id]?
highlightNode = document.createElement('div')
highlightNode.classList.add('highlight')
highlightNode = @domElementPool.build("div", "highlight")
@highlightNodesById[id] = highlightNode
@regionNodesByHighlightId[id] = {}
@domNode.appendChild(highlightNode)
@@ -75,12 +73,11 @@ class HighlightsComponent
for newRegionState, i in newHighlightState.regions
unless oldHighlightState.regions[i]?
oldHighlightState.regions[i] = {}
regionNode = document.createElement('div')
regionNode = @domElementPool.build("div", "region")
# This prevents highlights at the tiles boundaries to be hidden by the
# subsequent tile. When this happens, subpixel anti-aliasing gets
# disabled.
regionNode.style.boxSizing = "border-box"
regionNode.classList.add('region')
regionNode.classList.add(newHighlightState.deprecatedRegionClass) if newHighlightState.deprecatedRegionClass?
@regionNodesByHighlightId[id][i] = regionNode
highlightNode.appendChild(regionNode)

View File

@@ -3,6 +3,7 @@ class InputComponent
constructor: ->
@domNode = document.createElement('input')
@domNode.classList.add('hidden-input')
@domNode.setAttribute('tabindex', -1)
@domNode.setAttribute('data-react-skip-selection-restoration', true)
@domNode.style['-webkit-transform'] = 'translateZ(0)'
@domNode.addEventListener 'paste', (event) -> event.preventDefault()

View File

@@ -2,7 +2,6 @@ fs = require 'fs-plus'
path = require 'path'
KeymapManager = require 'atom-keymap'
CSON = require 'season'
{jQuery} = require 'space-pen'
Grim = require 'grim'
bundledKeymaps = require('../package.json')?._atomKeymaps
@@ -61,9 +60,4 @@ KeymapManager::subscribeToFileReadFailure = ->
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.
jQuery.Event::abortKeyBinding = ->
@originalEvent?.abortKeyBinding?()
module.exports = KeymapManager

View File

@@ -1,15 +1,17 @@
TiledComponent = require './tiled-component'
LineNumbersTileComponent = require './line-numbers-tile-component'
WrapperDiv = document.createElement('div')
DummyLineNumberComponent = LineNumbersTileComponent.createDummy()
DOMElementPool = require './dom-element-pool'
module.exports =
class LineNumberGutterComponent extends TiledComponent
dummyLineNumberNode: null
constructor: ({@onMouseDown, @editor, @gutter}) ->
constructor: ({@onMouseDown, @editor, @gutter, @domElementPool}) ->
@visible = true
@dummyLineNumberComponent = LineNumbersTileComponent.createDummy(@domElementPool)
@domNode = atom.views.getView(@gutter)
@lineNumbersNode = @domNode.firstChild
@lineNumbersNode.innerHTML = ''
@@ -60,7 +62,10 @@ class LineNumberGutterComponent extends TiledComponent
@oldState.styles = {}
@oldState.maxLineNumberDigits = @newState.maxLineNumberDigits
buildComponentForTile: (id) -> new LineNumbersTileComponent({id})
buildComponentForTile: (id) -> new LineNumbersTileComponent({id, @domElementPool})
shouldRecreateAllTilesOnUpdate: ->
@newState.continuousReflow
###
Section: Private Methods
@@ -69,14 +74,13 @@ class LineNumberGutterComponent extends TiledComponent
# 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]
@dummyLineNumberComponent.newState = @newState
@dummyLineNumberNode = @dummyLineNumberComponent.buildLineNumberNode({bufferRow: -1})
@lineNumbersNode.appendChild(@dummyLineNumberNode)
updateDummyLineNumber: ->
DummyLineNumberComponent.newState = @newState
@dummyLineNumberNode.innerHTML = DummyLineNumberComponent.buildLineNumberInnerHTML(0, false)
@dummyLineNumberComponent.newState = @newState
@dummyLineNumberComponent.setLineNumberInnerNodes(0, false, @dummyLineNumberNode)
onMouseDown: (event) =>
{target} = event

View File

@@ -1,19 +1,20 @@
_ = require 'underscore-plus'
WrapperDiv = document.createElement('div')
module.exports =
class LineNumbersTileComponent
@createDummy: ->
new LineNumbersTileComponent({id: -1})
@createDummy: (domElementPool) ->
new LineNumbersTileComponent({id: -1, domElementPool})
constructor: ({@id}) ->
constructor: ({@id, @domElementPool}) ->
@lineNumberNodesById = {}
@domNode = document.createElement("div")
@domNode.classList.add("tile")
@domNode = @domElementPool.build("div")
@domNode.style.position = "absolute"
@domNode.style.display = "block"
@domNode.style.top = 0 # Cover the space occupied by a dummy lineNumber
destroy: ->
@domElementPool.freeElementAndDescendants(@domNode)
getDomNode: ->
@domNode
@@ -47,7 +48,9 @@ class LineNumbersTileComponent
@oldTileState.zIndex = @newTileState.zIndex
if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits
node.remove() for id, node of @lineNumberNodesById
for id, node of @lineNumberNodesById
@domElementPool.freeElementAndDescendants(node)
@oldState.tiles[@id] = {lineNumbers: {}}
@oldTileState = @oldState.tiles[@id]
@lineNumberNodesById = {}
@@ -57,11 +60,11 @@ class LineNumbersTileComponent
updateLineNumbers: ->
newLineNumberIds = null
newLineNumbersHTML = null
newLineNumberNodes = null
for id, lineNumberState of @oldTileState.lineNumbers
unless @newTileState.lineNumbers.hasOwnProperty(id)
@lineNumberNodesById[id].remove()
@domElementPool.freeElementAndDescendants(@lineNumberNodesById[id])
delete @lineNumberNodesById[id]
delete @oldTileState.lineNumbers[id]
@@ -70,35 +73,40 @@ class LineNumbersTileComponent
@updateLineNumberNode(id, lineNumberState)
else
newLineNumberIds ?= []
newLineNumbersHTML ?= ""
newLineNumberNodes ?= []
newLineNumberIds.push(id)
newLineNumbersHTML += @buildLineNumberHTML(lineNumberState)
newLineNumberNodes.push(@buildLineNumberNode(lineNumberState))
@oldTileState.lineNumbers[id] = _.clone(lineNumberState)
if newLineNumberIds?
WrapperDiv.innerHTML = newLineNumbersHTML
newLineNumberNodes = _.toArray(WrapperDiv.children)
return unless newLineNumberIds?
node = @domNode
for id, i in newLineNumberIds
lineNumberNode = newLineNumberNodes[i]
@lineNumberNodesById[id] = lineNumberNode
node.appendChild(lineNumberNode)
for id, i in newLineNumberIds
lineNumberNode = newLineNumberNodes[i]
@lineNumberNodesById[id] = lineNumberNode
if nextNode = @findNodeNextTo(lineNumberNode)
@domNode.insertBefore(lineNumberNode, nextNode)
else
@domNode.appendChild(lineNumberNode)
findNodeNextTo: (node) ->
for nextNode in @domNode.children
return nextNode if @screenRowForNode(node) < @screenRowForNode(nextNode)
return
buildLineNumberHTML: (lineNumberState) ->
screenRowForNode: (node) -> parseInt(node.dataset.screenRow)
buildLineNumberNode: (lineNumberState) ->
{screenRow, bufferRow, softWrapped, top, decorationClasses, zIndex} = lineNumberState
if screenRow?
style = "position: absolute; top: #{top}px; z-index: #{zIndex};"
else
style = "visibility: hidden;"
className = @buildLineNumberClassName(lineNumberState)
innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped)
lineNumberNode = @domElementPool.build("div", className)
lineNumberNode.dataset.screenRow = screenRow
lineNumberNode.dataset.bufferRow = bufferRow
"<div class=\"#{className}\" style=\"#{style}\" data-buffer-row=\"#{bufferRow}\" data-screen-row=\"#{screenRow}\">#{innerHTML}</div>"
@setLineNumberInnerNodes(bufferRow, softWrapped, lineNumberNode)
lineNumberNode
buildLineNumberInnerHTML: (bufferRow, softWrapped) ->
setLineNumberInnerNodes: (bufferRow, softWrapped, lineNumberNode) ->
{maxLineNumberDigits} = @newState
if softWrapped
@@ -106,9 +114,11 @@ class LineNumbersTileComponent
else
lineNumber = (bufferRow + 1).toString()
padding = _.multiplyString('&nbsp;', maxLineNumberDigits - lineNumber.length)
iconHTML = '<div class="icon-right"></div>'
padding + lineNumber + iconHTML
padding = _.multiplyString("\u00a0", maxLineNumberDigits - lineNumber.length)
iconRight = @domElementPool.build("div", "icon-right")
lineNumberNode.textContent = padding + lineNumber
lineNumberNode.appendChild(iconRight)
updateLineNumberNode: (lineNumberId, newLineNumberState) ->
oldLineNumberState = @oldTileState.lineNumbers[lineNumberId]
@@ -119,18 +129,15 @@ class LineNumbersTileComponent
oldLineNumberState.foldable = newLineNumberState.foldable
oldLineNumberState.decorationClasses = _.clone(newLineNumberState.decorationClasses)
unless oldLineNumberState.top is newLineNumberState.top
node.style.top = newLineNumberState.top + 'px'
unless oldLineNumberState.screenRow is newLineNumberState.screenRow and oldLineNumberState.bufferRow is newLineNumberState.bufferRow
@setLineNumberInnerNodes(newLineNumberState.bufferRow, newLineNumberState.softWrapped, node)
node.dataset.screenRow = newLineNumberState.screenRow
oldLineNumberState.top = newLineNumberState.top
node.dataset.bufferRow = newLineNumberState.bufferRow
oldLineNumberState.screenRow = newLineNumberState.screenRow
unless oldLineNumberState.zIndex is newLineNumberState.zIndex
node.style.zIndex = newLineNumberState.zIndex
oldLineNumberState.zIndex = newLineNumberState.zIndex
oldLineNumberState.bufferRow = newLineNumberState.bufferRow
buildLineNumberClassName: ({bufferRow, foldable, decorationClasses, softWrapped}) ->
className = "line-number line-number-#{bufferRow}"
className = "line-number"
className += " " + decorationClasses.join(' ') if decorationClasses?
className += " foldable" if foldable and not softWrapped
className

View File

@@ -1,16 +1,19 @@
{$$} = require 'space-pen'
CursorsComponent = require './cursors-component'
LinesTileComponent = require './lines-tile-component'
TiledComponent = require './tiled-component'
DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0]
DummyLineNode = document.createElement('div')
DummyLineNode.className = 'line'
DummyLineNode.style.position = 'absolute'
DummyLineNode.style.visibility = 'hidden'
DummyLineNode.appendChild(document.createElement('span'))
DummyLineNode.firstChild.textContent = 'x'
module.exports =
class LinesComponent extends TiledComponent
placeholderTextDiv: null
constructor: ({@presenter, @hostElement, @useShadowDOM, visible}) ->
constructor: ({@presenter, @hostElement, @useShadowDOM, visible, @domElementPool}) ->
@domNode = document.createElement('div')
@domNode.classList.add('lines')
@tilesNode = document.createElement("div")
@@ -32,7 +35,7 @@ class LinesComponent extends TiledComponent
@domNode
shouldRecreateAllTilesOnUpdate: ->
@oldState.indentGuidesVisible isnt @newState.indentGuidesVisible
@oldState.indentGuidesVisible isnt @newState.indentGuidesVisible or @newState.continuousReflow
beforeUpdateSync: (state) ->
if @newState.maxHeight isnt @oldState.maxHeight
@@ -60,7 +63,7 @@ class LinesComponent extends TiledComponent
@oldState.indentGuidesVisible = @newState.indentGuidesVisible
buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter})
buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter, @domElementPool})
buildEmptyState: ->
{tiles: {}}

View File

@@ -3,7 +3,6 @@ _ = require 'underscore-plus'
HighlightsComponent = require './highlights-component'
TokenIterator = require './token-iterator'
AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
WrapperDiv = document.createElement('div')
TokenTextEscapeRegex = /[&"'<>]/g
MaxTokenLength = 20000
@@ -14,20 +13,22 @@ cloneObject = (object) ->
module.exports =
class LinesTileComponent
constructor: ({@presenter, @id}) ->
constructor: ({@presenter, @id, @domElementPool}) ->
@tokenIterator = new TokenIterator
@measuredLines = new Set
@lineNodesByLineId = {}
@screenRowsByLineId = {}
@lineIdsByScreenRow = {}
@domNode = document.createElement("div")
@domNode.classList.add("tile")
@domNode = @domElementPool.build("div")
@domNode.style.position = "absolute"
@domNode.style.display = "block"
@highlightsComponent = new HighlightsComponent
@highlightsComponent = new HighlightsComponent(@domElementPool)
@domNode.appendChild(@highlightsComponent.getDomNode())
destroy: ->
@domElementPool.freeElementAndDescendants(@domNode)
getDomNode: ->
@domNode
@@ -77,7 +78,7 @@ class LinesTileComponent
return
removeLineNode: (id) ->
@lineNodesByLineId[id].remove()
@domElementPool.freeElementAndDescendants(@lineNodesByLineId[id])
delete @lineNodesByLineId[id]
delete @lineIdsByScreenRow[@screenRowsByLineId[id]]
delete @screenRowsByLineId[id]
@@ -89,89 +90,99 @@ class LinesTileComponent
@removeLineNode(id)
newLineIds = null
newLinesHTML = null
newLineNodes = null
for id, lineState of @newTileState.lines
if @oldTileState.lines.hasOwnProperty(id)
@updateLineNode(id)
else
newLineIds ?= []
newLinesHTML ?= ""
newLineNodes ?= []
newLineIds.push(id)
newLinesHTML += @buildLineHTML(id)
newLineNodes.push(@buildLineNode(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)
if nextNode = @findNodeNextTo(lineNode)
@domNode.insertBefore(lineNode, nextNode)
else
@domNode.appendChild(lineNode)
findNodeNextTo: (node) ->
for nextNode, index in @domNode.children
continue if index is 0 # skips highlights node
return nextNode if @screenRowForNode(node) < @screenRowForNode(nextNode)
return
buildLineHTML: (id) ->
screenRowForNode: (node) -> parseInt(node.dataset.screenRow)
buildLineNode: (id) ->
{width} = @newState
{screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newTileState.lines[id]
classes = ''
lineNode = @domElementPool.build("div", "line")
lineNode.dataset.screenRow = screenRow
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}\">"
lineNode.classList.add(decorationClass)
if text is ""
lineHTML += @buildEmptyLineInnerHTML(id)
@setEmptyLineInnerNodes(id, lineNode)
else
lineHTML += @buildLineInnerHTML(id)
@setLineInnerNodes(id, lineNode)
lineHTML += '<span class="fold-marker"></span>' if fold
lineHTML += "</div>"
lineHTML
lineNode.appendChild(@domElementPool.build("span", "fold-marker")) if fold
lineNode
buildEmptyLineInnerHTML: (id) ->
setEmptyLineInnerNodes: (id, lineNode) ->
{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'>"
indentGuide = @domElementPool.build("span", "indent-guide")
for j in [0...tabLength]
if invisible = endOfLineInvisibles?[invisibleIndex++]
lineHTML += "<span class='invisible-character'>#{invisible}</span>"
indentGuide.appendChild(
@domElementPool.build("span", "invisible-character", invisible)
)
else
lineHTML += ' '
lineHTML += "</span>"
indentGuide.insertAdjacentText("beforeend", " ")
lineNode.appendChild(indentGuide)
while invisibleIndex < endOfLineInvisibles?.length
lineHTML += "<span class='invisible-character'>#{endOfLineInvisibles[invisibleIndex++]}</span>"
lineHTML
invisible = endOfLineInvisibles[invisibleIndex++]
lineNode.appendChild(
@domElementPool.build("span", "invisible-character", invisible)
)
else
@buildEndOfLineHTML(id) or '&nbsp;'
unless @appendEndOfLineNodes(id, lineNode)
lineNode.textContent = "\u00a0"
buildLineInnerHTML: (id) ->
setLineInnerNodes: (id, lineNode) ->
lineState = @newTileState.lines[id]
{firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, invisibles} = lineState
lineIsWhitespaceOnly = firstTrailingWhitespaceIndex is 0
innerHTML = ""
@tokenIterator.reset(lineState)
openScopeNode = lineNode
while @tokenIterator.next()
for scope in @tokenIterator.getScopeEnds()
innerHTML += "</span>"
openScopeNode = openScopeNode.parentElement
for scope in @tokenIterator.getScopeStarts()
innerHTML += "<span class=\"#{scope.replace(/\.+/g, ' ')}\">"
newScopeNode = @domElementPool.build("span", scope.replace(/\.+/g, ' '))
openScopeNode.appendChild(newScopeNode)
openScopeNode = newScopeNode
tokenStart = @tokenIterator.getScreenStart()
tokenEnd = @tokenIterator.getScreenEnd()
@@ -196,87 +207,79 @@ class LinesTileComponent
(invisibles?.tab and isHardTab) or
(invisibles?.space and (hasLeadingWhitespace or hasTrailingWhitespace))
innerHTML += @buildTokenHTML(tokenText, isHardTab, tokenFirstNonWhitespaceIndex, tokenFirstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters)
@appendTokenNodes(tokenText, isHardTab, tokenFirstNonWhitespaceIndex, tokenFirstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, openScopeNode)
for scope in @tokenIterator.getScopeEnds()
innerHTML += "</span>"
@appendEndOfLineNodes(id, lineNode)
for scope in @tokenIterator.getScopes()
innerHTML += "</span>"
innerHTML += @buildEndOfLineHTML(id)
innerHTML
buildTokenHTML: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters) ->
appendTokenNodes: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, scopeNode) ->
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>"
hardTabNode = @domElementPool.build("span", "hard-tab", tokenText)
hardTabNode.classList.add("leading-whitespace") if firstNonWhitespaceIndex?
hardTabNode.classList.add("trailing-whitespace") if firstTrailingWhitespaceIndex?
hardTabNode.classList.add("indent-guide") if hasIndentGuide
hardTabNode.classList.add("invisible-character") if hasInvisibleCharacters
scopeNode.appendChild(hardTabNode)
else
startIndex = 0
endIndex = tokenText.length
leadingHtml = ''
trailingHtml = ''
leadingWhitespaceNode = null
trailingWhitespaceNode = null
if firstNonWhitespaceIndex?
leadingWhitespace = tokenText.substring(0, firstNonWhitespaceIndex)
leadingWhitespaceNode = @domElementPool.build(
"span",
"leading-whitespace",
tokenText.substring(0, firstNonWhitespaceIndex)
)
leadingWhitespaceNode.classList.add("indent-guide") if hasIndentGuide
leadingWhitespaceNode.classList.add("invisible-character") if hasInvisibleCharacters
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>"
trailingWhitespaceNode = @domElementPool.build(
"span",
"trailing-whitespace",
tokenText.substring(firstTrailingWhitespaceIndex)
)
trailingWhitespaceNode.classList.add("indent-guide") if hasIndentGuide and not firstNonWhitespaceIndex? and tokenIsOnlyWhitespace
trailingWhitespaceNode.classList.add("invisible-character") if hasInvisibleCharacters
endIndex = firstTrailingWhitespaceIndex
html = leadingHtml
scopeNode.appendChild(leadingWhitespaceNode) if leadingWhitespaceNode?
if tokenText.length > MaxTokenLength
while startIndex < endIndex
html += "<span>" + @escapeTokenText(tokenText, startIndex, startIndex + MaxTokenLength) + "</span>"
text = @sliceText(tokenText, startIndex, startIndex + MaxTokenLength)
scopeNode.appendChild(@domElementPool.build("span", null, text))
startIndex += MaxTokenLength
else
html += @escapeTokenText(tokenText, startIndex, endIndex)
scopeNode.insertAdjacentText("beforeend", @sliceText(tokenText, startIndex, endIndex))
html += trailingHtml
html
scopeNode.appendChild(trailingWhitespaceNode) if trailingWhitespaceNode?
escapeTokenText: (tokenText, startIndex, endIndex) ->
sliceText: (tokenText, startIndex, endIndex) ->
if startIndex? and endIndex? and startIndex > 0 or endIndex < tokenText.length
tokenText = tokenText.slice(startIndex, endIndex)
tokenText.replace(TokenTextEscapeRegex, @escapeTokenTextReplace)
tokenText
escapeTokenTextReplace: (match) ->
switch match
when '&' then '&amp;'
when '"' then '&quot;'
when "'" then '&#39;'
when '<' then '&lt;'
when '>' then '&gt;'
else match
buildEndOfLineHTML: (id) ->
appendEndOfLineNodes: (id, lineNode) ->
{endOfLineInvisibles} = @newTileState.lines[id]
html = ''
hasInvisibles = false
if endOfLineInvisibles?
for invisible in endOfLineInvisibles
html += "<span class='invisible-character'>#{invisible}</span>"
html
hasInvisibles = true
lineNode.appendChild(
@domElementPool.build("span", "invisible-character", invisible)
)
hasInvisibles
updateLineNode: (id) ->
oldLineState = @oldTileState.lines[id]
@@ -284,9 +287,6 @@ class LinesTileComponent
lineNode = @lineNodesByLineId[id]
if @newState.width isnt @oldState.width
lineNode.style.width = @newState.width + 'px'
newDecorationClasses = newLineState.decorationClasses
oldDecorationClasses = oldLineState.decorationClasses
@@ -302,10 +302,6 @@ class LinesTileComponent
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
@@ -343,8 +339,6 @@ class LinesTileComponent
charLength = 1
textIndex++
continue if char is '\0'
unless charWidths[char]?
unless textNode?
rangeForMeasurement ?= document.createRange()

View File

@@ -80,7 +80,7 @@ class NotificationManager
# Public: Get all the notifications.
#
# Returns an {Array} of {Notifications}s.
# Returns an {Array} of {Notification}s.
getNotifications: -> @notifications.slice()
###

View File

@@ -3,7 +3,6 @@ path = require 'path'
_ = require 'underscore-plus'
{Emitter} = require 'event-kit'
fs = require 'fs-plus'
Q = require 'q'
Grim = require 'grim'
ServiceHub = require 'service-hub'
@@ -392,7 +391,7 @@ class PackageManager
for [activator, types] in @packageActivators
packages = @getLoadedPackagesForTypes(types)
promises = promises.concat(activator.activatePackages(packages))
Q.all(promises).then =>
Promise.all(promises).then =>
@emit 'activated' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-activate-initial-packages'
@@ -415,14 +414,14 @@ class PackageManager
# Activate a single package by name
activatePackage: (name) ->
if pack = @getActivePackage(name)
Q(pack)
Promise.resolve(pack)
else if pack = @loadPackage(name)
pack.activate().then =>
@activePackages[pack.name] = pack
@emitter.emit 'did-activate-package', pack
pack
else
Q.reject(new Error("Failed to load package '#{name}'"))
Promise.reject(new Error("Failed to load package '#{name}'"))
triggerActivationHook: (hook) ->
return new Error("Cannot trigger an empty activation hook") unless hook? and _.isString(hook) and hook.length > 0

View File

@@ -6,15 +6,15 @@ async = require 'async'
CSON = require 'season'
fs = require 'fs-plus'
{Emitter, CompositeDisposable} = require 'event-kit'
Q = require 'q'
{includeDeprecatedAPIs, deprecate} = require 'grim'
ModuleCache = require './module-cache'
ScopedProperties = require './scoped-properties'
BufferedProcess = require './buffered-process'
packagesCache = require('../package.json')?._atomPackages ? {}
# Loads and activates a package's main module and resources such as
# Extended: Loads and activates a package's main module and resources such as
# stylesheets, keymaps, grammar, editor properties, and menus.
module.exports =
class Package
@@ -138,20 +138,21 @@ class Package
activate: ->
@grammarsPromise ?= @loadGrammars()
@activationPromise ?=
new Promise (resolve, reject) =>
@resolveActivationPromise = resolve
@rejectActivationPromise = reject
@measure 'activateTime', =>
try
@activateResources()
if @activationShouldBeDeferred()
@subscribeToDeferredActivation()
else
@activateNow()
catch error
@handleError("Failed to activate the #{@name} package", error)
unless @activationDeferred?
@activationDeferred = Q.defer()
@measure 'activateTime', =>
try
@activateResources()
if @activationShouldBeDeferred()
@subscribeToDeferredActivation()
else
@activateNow()
catch error
@handleError("Failed to activate the #{@name} package", error)
Q.all([@grammarsPromise, @settingsPromise, @activationDeferred.promise])
Promise.all([@grammarsPromise, @settingsPromise, @activationPromise])
activateNow: ->
try
@@ -164,7 +165,7 @@ class Package
catch error
@handleError("Failed to activate the #{@name} package", error)
@activationDeferred?.resolve()
@resolveActivationPromise?()
activateConfig: ->
return if @configActivated
@@ -344,7 +345,7 @@ class Package
@grammarsActivated = true
loadGrammars: ->
return Q() if @grammarsLoaded
return Promise.resolve() if @grammarsLoaded
loadGrammar = (grammarPath, callback) =>
atom.grammars.readGrammar grammarPath, (error, grammar) =>
@@ -359,14 +360,13 @@ class Package
grammar.activate() if @grammarsActivated
callback()
deferred = Q.defer()
grammarsDirPath = path.join(@path, 'grammars')
fs.exists grammarsDirPath, (grammarsDirExists) ->
return deferred.resolve() unless grammarsDirExists
new Promise (resolve) =>
grammarsDirPath = path.join(@path, 'grammars')
fs.exists grammarsDirPath, (grammarsDirExists) ->
return resolve() unless grammarsDirExists
fs.list grammarsDirPath, ['json', 'cson'], (error, grammarPaths=[]) ->
async.each grammarPaths, loadGrammar, -> deferred.resolve()
deferred.promise
fs.list grammarsDirPath, ['json', 'cson'], (error, grammarPaths=[]) ->
async.each grammarPaths, loadGrammar, -> resolve()
loadSettings: ->
@settings = []
@@ -382,20 +382,18 @@ class Package
settings.activate() if @settingsActivated
callback()
deferred = Q.defer()
new Promise (resolve) =>
if fs.isDirectorySync(path.join(@path, 'scoped-properties'))
settingsDirPath = path.join(@path, 'scoped-properties')
deprecate("Store package settings files in the `settings/` directory instead of `scoped-properties/`", packageName: @name)
else
settingsDirPath = path.join(@path, 'settings')
if fs.isDirectorySync(path.join(@path, 'scoped-properties'))
settingsDirPath = path.join(@path, 'scoped-properties')
deprecate("Store package settings files in the `settings/` directory instead of `scoped-properties/`", packageName: @name)
else
settingsDirPath = path.join(@path, 'settings')
fs.exists settingsDirPath, (settingsDirExists) ->
return resolve() unless settingsDirExists
fs.exists settingsDirPath, (settingsDirExists) ->
return deferred.resolve() unless settingsDirExists
fs.list settingsDirPath, ['json', 'cson'], (error, settingsPaths=[]) ->
async.each settingsPaths, loadSettingsFile, -> deferred.resolve()
deferred.promise
fs.list settingsDirPath, ['json', 'cson'], (error, settingsPaths=[]) ->
async.each settingsPaths, loadSettingsFile, -> resolve()
serialize: ->
if @mainActivated
@@ -405,8 +403,10 @@ class Package
console.error "Error serializing package '#{@name}'", e.stack
deactivate: ->
@activationDeferred?.reject()
@activationDeferred = null
@rejectActivationPromise?()
@activationPromise = null
@resolveActivationPromise = null
@rejectActivationPromise = null
@activationCommandSubscriptions?.dispose()
@deactivateResources()
@deactivateConfig()
@@ -589,10 +589,20 @@ class Package
false
# Get an array of all the native modules that this package depends on.
# This will recurse through all dependencies.
#
# First try to get this information from
# @metadata._atomModuleCache.extensions. If @metadata._atomModuleCache doesn't
# exist, recurse through all dependencies.
getNativeModuleDependencyPaths: ->
nativeModulePaths = []
if @metadata._atomModuleCache?
relativeNativeModuleBindingPaths = @metadata._atomModuleCache.extensions?['.node'] ? []
for relativeNativeModuleBindingPath in relativeNativeModuleBindingPaths
nativeModulePath = path.join(@path, relativeNativeModuleBindingPath, '..', '..', '..')
nativeModulePaths.push(nativeModulePath)
return nativeModulePaths
traversePath = (nodeModulesPath) =>
try
for modulePath in fs.listSync(nodeModulesPath)
@@ -603,6 +613,70 @@ class Package
traversePath(path.join(@path, 'node_modules'))
nativeModulePaths
###
Section: Native Module Compatibility
###
# Extended: Are all native modules depended on by this package correctly
# compiled against the current version of Atom?
#
# Incompatible packages cannot be activated.
#
# Returns a {Boolean}, true if compatible, false if incompatible.
isCompatible: ->
return @compatible if @compatible?
if @path.indexOf(path.join(atom.packages.resourcePath, 'node_modules') + path.sep) is 0
# Bundled packages are always considered compatible
@compatible = true
else if @getMainModulePath()
@incompatibleModules = @getIncompatibleNativeModules()
@compatible = @incompatibleModules.length is 0 and not @getBuildFailureOutput()?
else
@compatible = true
# Extended: Rebuild native modules in this package's dependencies for the
# current version of Atom.
#
# Returns a {Promise} that resolves with an object containing `code`,
# `stdout`, and `stderr` properties based on the results of running
# `apm rebuild` on the package.
rebuild: ->
new Promise (resolve) =>
@runRebuildProcess (result) =>
if result.code is 0
global.localStorage.removeItem(@getBuildFailureOutputStorageKey())
else
@compatible = false
global.localStorage.setItem(@getBuildFailureOutputStorageKey(), result.stderr)
global.localStorage.setItem(@getIncompatibleNativeModulesStorageKey(), '[]')
resolve(result)
# Extended: If a previous rebuild failed, get the contents of stderr.
#
# Returns a {String} or null if no previous build failure occurred.
getBuildFailureOutput: ->
global.localStorage.getItem(@getBuildFailureOutputStorageKey())
runRebuildProcess: (callback) ->
stderr = ''
stdout = ''
new BufferedProcess({
command: atom.packages.getApmPath()
args: ['rebuild', '--no-color']
options: {cwd: @path}
stderr: (output) -> stderr += output
stdout: (output) -> stdout += output
exit: (code) -> callback({code, stdout, stderr})
})
getBuildFailureOutputStorageKey: ->
"installed-packages:#{@name}:#{@metadata.version}:build-error"
getIncompatibleNativeModulesStorageKey: ->
electronVersion = process.versions['electron'] ? process.versions['atom-shell']
"installed-packages:#{@name}:#{@metadata.version}:electron-#{electronVersion}:incompatible-native-modules"
# Get the incompatible native modules that this package depends on.
# This recurses through all dependencies and requires all modules that
# contain a `.node` file.
@@ -610,11 +684,10 @@ class Package
# This information is cached in local storage on a per package/version basis
# to minimize the impact on startup time.
getIncompatibleNativeModules: ->
localStorageKey = "installed-packages:#{@name}:#{@metadata.version}"
unless atom.inDevMode()
try
{incompatibleNativeModules} = JSON.parse(global.localStorage.getItem(localStorageKey)) ? {}
return incompatibleNativeModules if incompatibleNativeModules?
if arrayAsString = global.localStorage.getItem(@getIncompatibleNativeModulesStorageKey())
return JSON.parse(arrayAsString)
incompatibleNativeModules = []
for nativeModulePath in @getNativeModuleDependencyPaths()
@@ -629,28 +702,9 @@ class Package
version: version
error: error.message
global.localStorage.setItem(localStorageKey, JSON.stringify({incompatibleNativeModules}))
global.localStorage.setItem(@getIncompatibleNativeModulesStorageKey(), JSON.stringify(incompatibleNativeModules))
incompatibleNativeModules
# Public: Is this package compatible with this version of Atom?
#
# Incompatible packages cannot be activated. This will include packages
# installed to ~/.atom/packages that were built against node 0.11.10 but
# now need to be upgrade to node 0.11.13.
#
# Returns a {Boolean}, true if compatible, false if incompatible.
isCompatible: ->
return @compatible if @compatible?
if @path.indexOf(path.join(atom.packages.resourcePath, 'node_modules') + path.sep) is 0
# Bundled packages are always considered compatible
@compatible = true
else if @getMainModulePath()
@incompatibleModules = @getIncompatibleNativeModules()
@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}"

View File

@@ -1,5 +1,4 @@
{CompositeDisposable} = require 'event-kit'
{callAttachHooks} = require './space-pen-extensions'
PaneResizeHandleElement = require './pane-resize-handle-element'
class PaneAxisElement extends HTMLElement
@@ -43,8 +42,6 @@ class PaneAxisElement extends HTMLElement
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

View File

@@ -1,7 +1,5 @@
{CompositeDisposable} = require 'event-kit'
Grim = require 'grim'
{callAttachHooks} = require './space-pen-extensions'
PaneContainerView = null
_ = require 'underscore-plus'
module.exports =
@@ -10,13 +8,8 @@ class PaneContainerElement extends HTMLElement
@subscriptions = new CompositeDisposable
@classList.add 'panes'
if Grim.includeDeprecatedAPIs
PaneContainerView ?= require './pane-container-view'
@__spacePenView = new PaneContainerView(this)
initialize: (@model) ->
@subscriptions.add @model.observeRoot(@rootChanged.bind(this))
@__spacePenView.setModel(@model) if Grim.includeDeprecatedAPIs
this
rootChanged: (root) ->
@@ -25,7 +18,6 @@ class PaneContainerElement extends HTMLElement
if root?
view = atom.views.getView(root)
@appendChild(view)
callAttachHooks(view)
focusedElement?.focus()
hasFocus: ->

View File

@@ -1,89 +0,0 @@
{deprecate} = require 'grim'
Delegator = require 'delegato'
{CompositeDisposable} = require 'event-kit'
{$, View, callAttachHooks} = require './space-pen-extensions'
PaneView = require './pane-view'
PaneContainer = require './pane-container'
# Manages the list of panes within a {WorkspaceView}
module.exports =
class PaneContainerView extends View
Delegator.includeInto(this)
@delegatesMethod 'saveAll', toProperty: 'model'
@content: ->
@div class: 'panes'
constructor: (@element) ->
super
@subscriptions = new CompositeDisposable
setModel: (@model) ->
@subscriptions.add @model.onDidChangeActivePaneItem(@onActivePaneItemChanged)
getRoot: ->
view = atom.views.getView(@model.getRoot())
view.__spacePenView ? view
onActivePaneItemChanged: (activeItem) =>
@trigger 'pane-container:active-pane-item-changed', [activeItem]
confirmClose: ->
@model.confirmClose()
getPaneViews: ->
@find('atom-pane').views()
indexOfPane: (paneView) ->
@getPaneViews().indexOf(paneView.view())
paneAtIndex: (index) ->
@getPaneViews()[index]
eachPaneView: (callback) ->
callback(paneView) for paneView in @getPaneViews()
paneViewAttached = (e) -> callback($(e.target).view())
@on 'pane:attached', paneViewAttached
off: => @off 'pane:attached', paneViewAttached
getFocusedPane: ->
@find('atom-pane:has(:focus)').view()
getActivePane: ->
deprecate("Use PaneContainerView::getActivePaneView instead.")
@getActivePaneView()
getActivePaneView: ->
atom.views.getView(@model.getActivePane()).__spacePenView
getActivePaneItem: ->
@model.getActivePaneItem()
getActiveView: ->
@getActivePaneView()?.activeView
paneForUri: (uri) ->
atom.views.getView(@model.paneForURI(uri)).__spacePenView
focusNextPaneView: ->
@model.activateNextPane()
focusPreviousPaneView: ->
@model.activatePreviousPane()
focusPaneViewAbove: ->
@element.focusPaneViewAbove()
focusPaneViewBelow: ->
@element.focusPaneViewBelow()
focusPaneViewOnLeft: ->
@element.focusPaneViewOnLeft()
focusPaneViewOnRight: ->
@element.focusPaneViewOnRight()
getPanes: ->
deprecate("Use PaneContainerView::getPaneViews() instead")
@getPaneViews()

View File

@@ -1,8 +1,6 @@
path = require 'path'
{CompositeDisposable} = require 'event-kit'
Grim = require 'grim'
{$, callAttachHooks, callRemoveHooks} = require './space-pen-extensions'
PaneView = null
class PaneElement extends HTMLElement
attached: false
@@ -14,7 +12,6 @@ class PaneElement extends HTMLElement
@initializeContent()
@subscribeToDOMEvents()
@createSpacePenShim() if Grim.includeDeprecatedAPIs
attachedCallback: ->
@attached = true
@@ -55,10 +52,6 @@ class PaneElement extends HTMLElement
@addEventListener 'dragover', handleDragOver
@addEventListener 'drop', handleDrop
createSpacePenShim: ->
PaneView ?= require './pane-view'
@__spacePenView = new PaneView(this)
initialize: (@model) ->
@subscriptions.add @model.onDidActivate(@activated.bind(this))
@subscriptions.add @model.observeActive(@activeStatusChanged.bind(this))
@@ -66,8 +59,6 @@ class PaneElement extends HTMLElement
@subscriptions.add @model.onDidRemoveItem(@itemRemoved.bind(this))
@subscriptions.add @model.onDidDestroy(@paneDestroyed.bind(this))
@subscriptions.add @model.observeFlexScale(@flexScaleChanged.bind(this))
@__spacePenView.setModel(@model) if Grim.includeDeprecatedAPIs
this
getModel: -> @model
@@ -96,7 +87,6 @@ class PaneElement extends HTMLElement
unless @itemViews.contains(itemView)
@itemViews.appendChild(itemView)
callAttachHooks(itemView)
for child in @itemViews.children
if child is itemView
@@ -121,7 +111,6 @@ class PaneElement extends HTMLElement
itemRemoved: ({item, index, destroyed}) ->
if viewToRemove = atom.views.getView(item)
callRemoveHooks(viewToRemove) if destroyed
viewToRemove.remove()
paneDestroyed: ->

View File

@@ -1,167 +0,0 @@
{$, View} = require './space-pen-extensions'
Delegator = require 'delegato'
{deprecate} = require 'grim'
{CompositeDisposable} = require 'event-kit'
PropertyAccessors = require 'property-accessors'
Pane = require './pane'
# A container which can contains multiple items to be switched between.
#
# Items can be almost anything however most commonly they're {TextEditorView}s.
#
# Most packages won't need to use this class, unless you're interested in
# building a package that deals with switching between panes or items.
module.exports =
class PaneView extends View
Delegator.includeInto(this)
PropertyAccessors.includeInto(this)
@delegatesProperties 'items', 'activeItem', toProperty: 'model'
@delegatesMethods 'getItems', 'activateNextItem', 'activatePreviousItem', 'getActiveItemIndex',
'activateItemAtIndex', 'activateItem', 'addItem', 'itemAtIndex', 'moveItem', 'moveItemToPane',
'destroyItem', 'destroyItems', 'destroyActiveItem', 'destroyInactiveItems',
'saveActiveItem', 'saveActiveItemAs', 'saveItem', 'saveItemAs', 'saveItems',
'itemForUri', 'activateItemForUri', 'promptToSaveItem', 'copyActiveItem', 'isActive',
'activate', 'getActiveItem', toProperty: 'model'
previousActiveItem: null
attached: false
constructor: (@element) ->
@itemViews = $(element.itemViews)
super
setModel: (@model) ->
@subscriptions = new CompositeDisposable
@subscriptions.add @model.observeActiveItem(@onActiveItemChanged)
@subscriptions.add @model.onDidAddItem(@onItemAdded)
@subscriptions.add @model.onDidRemoveItem(@onItemRemoved)
@subscriptions.add @model.onDidMoveItem(@onItemMoved)
@subscriptions.add @model.onWillDestroyItem(@onBeforeItemDestroyed)
@subscriptions.add @model.observeActive(@onActiveStatusChanged)
@subscriptions.add @model.onDidDestroy(@onPaneDestroyed)
afterAttach: ->
@container ?= @closest('atom-pane-container').view()
@trigger('pane:attached', [this]) unless @attached
@attached = true
onPaneDestroyed: =>
@container?.trigger 'pane:removed', [this]
@subscriptions.dispose()
remove: ->
@model.destroy() unless @model.isDestroyed()
# Essential: Returns the {Pane} model underlying this pane view
getModel: -> @model
# Deprecated: Use ::destroyItem
removeItem: (item) ->
deprecate("Use PaneView::destroyItem instead")
@destroyItem(item)
# Deprecated: Use ::activateItem
showItem: (item) ->
deprecate("Use PaneView::activateItem instead")
@activateItem(item)
# Deprecated: Use ::activateItemForUri
showItemForUri: (item) ->
deprecate("Use PaneView::activateItemForUri instead")
@activateItemForUri(item)
# Deprecated: Use ::activateItemAtIndex
showItemAtIndex: (index) ->
deprecate("Use PaneView::activateItemAtIndex instead")
@activateItemAtIndex(index)
# Deprecated: Use ::activateNextItem
showNextItem: ->
deprecate("Use PaneView::activateNextItem instead")
@activateNextItem()
# Deprecated: Use ::activatePreviousItem
showPreviousItem: ->
deprecate("Use PaneView::activatePreviousItem instead")
@activatePreviousItem()
onActiveStatusChanged: (active) =>
if active
@trigger 'pane:became-active'
else
@trigger 'pane:became-inactive'
# Public: Returns the next pane, ordered by creation.
getNextPane: ->
panes = @container?.getPaneViews()
return unless panes.length > 1
nextIndex = (panes.indexOf(this) + 1) % panes.length
panes[nextIndex]
getActivePaneItem: ->
@activeItem
onActiveItemChanged: (item) =>
@activeItemDisposables.dispose() if @activeItemDisposables?
@activeItemDisposables = new CompositeDisposable()
if @previousActiveItem?.off?
@previousActiveItem.off 'title-changed', @activeItemTitleChanged
@previousActiveItem.off 'modified-status-changed', @activeItemModifiedChanged
@previousActiveItem = item
return unless item?
if item.onDidChangeTitle?
disposable = item.onDidChangeTitle(@activeItemTitleChanged)
@activeItemDisposables.add(disposable) if disposable?.dispose?
else if item.on?
disposable = item.on('title-changed', @activeItemTitleChanged)
@activeItemDisposables.add(disposable) if disposable?.dispose?
if item.onDidChangeModified?
disposable = item.onDidChangeModified(@activeItemModifiedChanged)
@activeItemDisposables.add(disposable) if disposable?.dispose?
else if item.on?
item.on('modified-status-changed', @activeItemModifiedChanged)
@activeItemDisposables.add(disposable) if disposable?.dispose?
@trigger 'pane:active-item-changed', [item]
onItemAdded: ({item, index}) =>
@trigger 'pane:item-added', [item, index]
onItemRemoved: ({item, index, destroyed}) =>
@trigger 'pane:item-removed', [item, index]
onItemMoved: ({item, newIndex}) =>
@trigger 'pane:item-moved', [item, newIndex]
onBeforeItemDestroyed: ({item}) =>
@unsubscribe(item) if typeof item.off is 'function'
@trigger 'pane:before-item-destroyed', [item]
activeItemTitleChanged: =>
@trigger 'pane:active-item-title-changed'
activeItemModifiedChanged: =>
@trigger 'pane:active-item-modified-status-changed'
@::accessor 'activeView', ->
element = atom.views.getView(@activeItem)
$(element).view() ? element
splitLeft: (items...) -> atom.views.getView(@model.splitLeft({items})).__spacePenView
splitRight: (items...) -> atom.views.getView(@model.splitRight({items})).__spacePenView
splitUp: (items...) -> atom.views.getView(@model.splitUp({items})).__spacePenView
splitDown: (items...) -> atom.views.getView(@model.splitDown({items})).__spacePenView
getContainer: -> @closest('atom-pane-container').view()
focus: ->
@element.focus()

View File

@@ -355,6 +355,7 @@ class Pane extends Model
# Returns the added item.
addItem: (item, index=@getActiveItemIndex() + 1) ->
throw new Error("Pane items must be objects. Attempted to add item #{item}.") unless item? and typeof item is 'object'
throw new Error("Adding a pane item with URI '#{item.getURI?()}' that has already been destroyed") if item.isDestroyed?()
return if item in @items

View File

@@ -1,5 +1,4 @@
{CompositeDisposable} = require 'event-kit'
{callAttachHooks} = require './space-pen-extensions'
Panel = require './panel'
class PanelElement extends HTMLElement
@@ -21,7 +20,6 @@ class PanelElement extends HTMLElement
atom.views.getView(@getModel().getItem())
attachedCallback: ->
callAttachHooks(@getItemView()) # for backward compatibility with SpacePen views
@visibleChanged(@getModel().isVisible())
visibleChanged: (visible) ->

View File

@@ -3,7 +3,6 @@ url = require 'url'
_ = require 'underscore-plus'
fs = require 'fs-plus'
Q = require 'q'
{includeDeprecatedAPIs, deprecate} = require 'grim'
{Emitter} = require 'event-kit'
Serializable = require 'serializable'
@@ -367,8 +366,11 @@ class Project extends Model
#
# Returns a promise that resolves to the {TextBuffer}.
bufferForPath: (absoluteFilePath) ->
existingBuffer = @findBufferForPath(absoluteFilePath) if absoluteFilePath
Q(existingBuffer ? @buildBuffer(absoluteFilePath))
existingBuffer = @findBufferForPath(absoluteFilePath) if absoluteFilePath?
if existingBuffer
Promise.resolve(existingBuffer)
else
@buildBuffer(absoluteFilePath)
bufferForId: (id) ->
_.find @buffers, (buffer) -> buffer.id is id

View File

@@ -1,38 +0,0 @@
{View} = require './space-pen-extensions'
# Deprecated: Represents a view that scrolls.
#
# Handles several core events to update scroll position:
#
# * `core:move-up` Scrolls the view up
# * `core:move-down` Scrolls the view down
# * `core:page-up` Scrolls the view up by the height of the page
# * `core:page-down` Scrolls the view down by the height of the page
# * `core:move-to-top` Scrolls the editor to the top
# * `core:move-to-bottom` Scroll the editor to the bottom
#
# Subclasses must call `super` if overriding the `initialize` method.
#
# ## Examples
#
# ```coffee
# {ScrollView} = require 'atom'
#
# class MyView extends ScrollView
# @content: ->
# @div()
#
# initialize: ->
# super
# @text('super long content that will scroll')
# ```
#
module.exports =
class ScrollView extends View
initialize: ->
@on 'core:move-up', => @scrollUp()
@on 'core:move-down', => @scrollDown()
@on 'core:page-up', => @pageUp()
@on 'core:page-down', => @pageDown()
@on 'core:move-to-top', => @scrollToTop()
@on 'core:move-to-bottom', => @scrollToBottom()

View File

@@ -1,312 +0,0 @@
{$, View} = require './space-pen-extensions'
TextEditorView = require './text-editor-view'
fuzzyFilter = require('fuzzaldrin').filter
# Deprecated: Provides a view that renders a list of items with an editor that
# filters the items. Used by many packages such as the fuzzy-finder,
# command-palette, symbols-view and autocomplete.
#
# Subclasses must implement the following methods:
#
# * {::viewForItem}
# * {::confirmed}
#
# ## Requiring in packages
#
# ```coffee
# {SelectListView} = require 'atom'
#
# class MySelectListView extends SelectListView
# initialize: ->
# super
# @addClass('overlay from-top')
# @setItems(['Hello', 'World'])
# atom.workspaceView.append(this)
# @focusFilterEditor()
#
# viewForItem: (item) ->
# "<li>#{item}</li>"
#
# confirmed: (item) ->
# console.log("#{item} was selected")
# ```
module.exports =
class SelectListView extends View
@content: ->
@div class: 'select-list', =>
@subview 'filterEditorView', new TextEditorView(mini: true)
@div class: 'error-message', outlet: 'error'
@div class: 'loading', outlet: 'loadingArea', =>
@span class: 'loading-message', outlet: 'loading'
@span class: 'badge', outlet: 'loadingBadge'
@ol class: 'list-group', outlet: 'list'
maxItems: Infinity
scheduleTimeout: null
inputThrottle: 50
cancelling: false
###
Section: Construction
###
# Essential: Initialize the select list view.
#
# This method can be overridden by subclasses but `super` should always
# be called.
initialize: ->
@filterEditorView.getEditor().getBuffer().onDidChange =>
@schedulePopulateList()
@filterEditorView.on 'blur', =>
@cancel() unless @cancelling
# This prevents the focusout event from firing on the filter editor view
# when the list is scrolled by clicking the scrollbar and dragging.
@list.on 'mousedown', ({target}) =>
false if target is @list[0]
@on 'core:move-up', =>
@selectPreviousItemView()
@on 'core:move-down', =>
@selectNextItemView()
@on 'core:move-to-top', =>
@selectItemView(@list.find('li:first'))
@list.scrollToTop()
false
@on 'core:move-to-bottom', =>
@selectItemView(@list.find('li:last'))
@list.scrollToBottom()
false
@on 'core:confirm', => @confirmSelection()
@on 'core:cancel', => @cancel()
@list.on 'mousedown', 'li', (e) =>
@selectItemView($(e.target).closest('li'))
e.preventDefault()
@list.on 'mouseup', 'li', (e) =>
@confirmSelection() if $(e.target).closest('li').hasClass('selected')
e.preventDefault()
###
Section: Methods that must be overridden
###
# Essential: Create a view for the given model item.
#
# This method must be overridden by subclasses.
#
# This is called when the item is about to appended to the list view.
#
# * `item` The model item being rendered. This will always be one of the items
# previously passed to {::setItems}.
#
# Returns a String of HTML, DOM element, jQuery object, or View.
viewForItem: (item) ->
throw new Error("Subclass must implement a viewForItem(item) method")
# Essential: Callback function for when an item is selected.
#
# This method must be overridden by subclasses.
#
# * `item` The selected model item. This will always be one of the items
# previously passed to {::setItems}.
#
# Returns a DOM element, jQuery object, or {View}.
confirmed: (item) ->
throw new Error("Subclass must implement a confirmed(item) method")
###
Section: Managing the list of items
###
# Essential: Set the array of items to display in the list.
#
# This should be model items not actual views. {::viewForItem} will be
# called to render the item when it is being appended to the list view.
#
# * `items` The {Array} of model items to display in the list (default: []).
setItems: (@items=[]) ->
@populateList()
@setLoading()
# Essential: Get the model item that is currently selected in the list view.
#
# Returns a model item.
getSelectedItem: ->
@getSelectedItemView().data('select-list-item')
# Extended: Get the property name to use when filtering items.
#
# This method may be overridden by classes to allow fuzzy filtering based
# on a specific property of the item objects.
#
# For example if the objects you pass to {::setItems} are of the type
# `{"id": 3, "name": "Atom"}` then you would return `"name"` from this method
# to fuzzy filter by that property when text is entered into this view's
# editor.
#
# Returns the property name to fuzzy filter by.
getFilterKey: ->
# Extended: Get the filter query to use when fuzzy filtering the visible
# elements.
#
# By default this method returns the text in the mini editor but it can be
# overridden by subclasses if needed.
#
# Returns a {String} to use when fuzzy filtering the elements to display.
getFilterQuery: ->
@filterEditorView.getEditor().getText()
# Extended: Set the maximum numbers of items to display in the list.
#
# * `maxItems` The maximum {Number} of items to display.
setMaxItems: (@maxItems) ->
# Extended: Populate the list view with the model items previously set by
# calling {::setItems}.
#
# Subclasses may override this method but should always call `super`.
populateList: ->
return unless @items?
filterQuery = @getFilterQuery()
if filterQuery.length
filteredItems = fuzzyFilter(@items, filterQuery, key: @getFilterKey())
else
filteredItems = @items
@list.empty()
if filteredItems.length
@setError(null)
for i in [0...Math.min(filteredItems.length, @maxItems)]
item = filteredItems[i]
itemView = $(@viewForItem(item))
itemView.data('select-list-item', item)
@list.append(itemView)
@selectItemView(@list.find('li:first'))
else
@setError(@getEmptyMessage(@items.length, filteredItems.length))
###
Section: Messages to the user
###
# Essential: Set the error message to display.
#
# * `message` The {String} error message (default: '').
setError: (message='') ->
if message.length is 0
@error.text('').hide()
else
@setLoading()
@error.text(message).show()
# Essential: Set the loading message to display.
#
# * `message` The {String} loading message (default: '').
setLoading: (message='') ->
if message.length is 0
@loading.text("")
@loadingBadge.text("")
@loadingArea.hide()
else
@setError()
@loading.text(message)
@loadingArea.show()
# Extended: Get the message to display when there are no items.
#
# Subclasses may override this method to customize the message.
#
# * `itemCount` The {Number} of items in the array specified to {::setItems}
# * `filteredItemCount` The {Number} of items that pass the fuzzy filter test.
#
# Returns a {String} message (default: 'No matches found').
getEmptyMessage: (itemCount, filteredItemCount) -> 'No matches found'
###
Section: View Actions
###
# Essential: Cancel and close this select list view.
#
# This restores focus to the previously focused element if
# {::storeFocusedElement} was called prior to this view being attached.
cancel: ->
@list.empty()
@cancelling = true
filterEditorViewFocused = @filterEditorView.isFocused
@cancelled()
@detach()
@restoreFocus() if filterEditorViewFocused
@cancelling = false
clearTimeout(@scheduleTimeout)
# Extended: Focus the fuzzy filter editor view.
focusFilterEditor: ->
@filterEditorView.focus()
# Extended: Store the currently focused element. This element will be given
# back focus when {::cancel} is called.
storeFocusedElement: ->
@previouslyFocusedElement = $(document.activeElement)
###
Section: Private
###
selectPreviousItemView: ->
view = @getSelectedItemView().prev()
view = @list.find('li:last') unless view.length
@selectItemView(view)
selectNextItemView: ->
view = @getSelectedItemView().next()
view = @list.find('li:first') unless view.length
@selectItemView(view)
selectItemView: (view) ->
return unless view.length
@list.find('.selected').removeClass('selected')
view.addClass('selected')
@scrollToItemView(view)
scrollToItemView: (view) ->
scrollTop = @list.scrollTop()
desiredTop = view.position().top + scrollTop
desiredBottom = desiredTop + view.outerHeight()
if desiredTop < scrollTop
@list.scrollTop(desiredTop)
else if desiredBottom > @list.scrollBottom()
@list.scrollBottom(desiredBottom)
restoreFocus: ->
if @previouslyFocusedElement?.isOnDom()
@previouslyFocusedElement.focus()
else
atom.workspaceView.focus()
cancelled: ->
@filterEditorView.getEditor().setText('')
getSelectedItemView: ->
@list.find('li.selected')
confirmSelection: ->
item = @getSelectedItem()
if item?
@confirmed(item)
else
@cancel()
schedulePopulateList: ->
clearTimeout(@scheduleTimeout)
populateCallback = =>
@populateList() if @isOnDom()
@scheduleTimeout = setTimeout(populateCallback, @inputThrottle)

View File

@@ -22,14 +22,8 @@ class Selection extends Model
@cursor.selection = this
@decoration = @editor.decorateMarker(@marker, type: 'highlight', class: 'selection')
@marker.onDidChange (e) => @screenRangeChanged(e)
@marker.onDidDestroy =>
unless @editor.isDestroyed()
@destroyed = true
@editor.removeSelection(this)
@emit 'destroyed' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-destroy'
@emitter.dispose()
@marker.onDidChange (e) => @markerDidChange(e)
@marker.onDidDestroy => @markerDidDestroy()
destroy: ->
@marker.destroy()
@@ -263,10 +257,15 @@ class Selection extends Model
@modifySelection => @cursor.moveToFirstCharacterOfLine()
# Public: Selects all the text from the current cursor position to the end of
# the line.
# the screen line.
selectToEndOfLine: ->
@modifySelection => @cursor.moveToEndOfScreenLine()
# Public: Selects all the text from the current cursor position to the end of
# the buffer line.
selectToEndOfBufferLine: ->
@modifySelection => @cursor.moveToEndOfLine()
# Public: Selects all the text from the current cursor position to the
# beginning of the word.
selectToBeginningOfWord: ->
@@ -330,9 +329,13 @@ class Selection extends Model
#
# * `row` The line {Number} to select (default: the row of the cursor).
selectLine: (row, options) ->
row ?= @cursor.getBufferPosition().row
range = @editor.bufferRangeForBufferRow(row, includeNewline: true)
@setBufferRange(@getBufferRange().union(range), options)
if row?
@setBufferRange(@editor.bufferRangeForBufferRow(row, includeNewline: true), options)
else
startRange = @editor.bufferRangeForBufferRow(@marker.getStartBufferPosition().row)
endRange = @editor.bufferRangeForBufferRow(@marker.getEndBufferPosition().row, includeNewline: true)
@setBufferRange(startRange.union(endRange), options)
@linewise = true
@wordwise = false
@initialScreenRange = @getScreenRange()
@@ -574,11 +577,16 @@ class Selection extends Model
toggleLineComments: ->
@editor.toggleLineCommentsForBufferRows(@getBufferRowRange()...)
# Public: Cuts the selection until the end of the line.
# Public: Cuts the selection until the end of the screen line.
cutToEndOfLine: (maintainClipboard) ->
@selectToEndOfLine() if @isEmpty()
@cut(maintainClipboard)
# Public: Cuts the selection until the end of the buffer line.
cutToEndOfBufferLine: (maintainClipboard) ->
@selectToEndOfBufferLine() if @isEmpty()
@cut(maintainClipboard)
# Public: Copies the selection to the clipboard and then deletes it.
#
# * `maintainClipboard` {Boolean} (default: false) See {::copy}
@@ -754,20 +762,48 @@ class Selection extends Model
Section: Private Utilities
###
screenRangeChanged: (e) ->
{oldHeadBufferPosition, oldTailBufferPosition} = e
{oldHeadScreenPosition, oldTailScreenPosition} = e
markerDidChange: (e) ->
{oldHeadBufferPosition, oldTailBufferPosition, newHeadBufferPosition} = e
{oldHeadScreenPosition, oldTailScreenPosition, newHeadScreenPosition} = e
{textChanged} = e
eventObject =
@cursor.updateVisibility()
unless oldHeadScreenPosition.isEqual(newHeadScreenPosition)
@cursor.goalColumn = null
cursorMovedEvent = {
oldBufferPosition: oldHeadBufferPosition
oldScreenPosition: oldHeadScreenPosition
newBufferPosition: newHeadBufferPosition
newScreenPosition: newHeadScreenPosition
textChanged: textChanged
cursor: @cursor
}
@cursor.emitter.emit('did-change-position', cursorMovedEvent)
@editor.cursorMoved(cursorMovedEvent)
@emitter.emit 'did-change-range'
@editor.selectionRangeChanged(
oldBufferRange: new Range(oldHeadBufferPosition, oldTailBufferPosition)
oldScreenRange: new Range(oldHeadScreenPosition, oldTailScreenPosition)
newBufferRange: @getBufferRange()
newScreenRange: @getScreenRange()
selection: this
)
@emit 'screen-range-changed', @getScreenRange() if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change-range'
@editor.selectionRangeChanged(eventObject)
markerDidDestroy: ->
return if @editor.isDestroyed()
@destroyed = true
@cursor.destroyed = true
@editor.removeSelection(this)
@cursor.emitter.emit 'did-destroy'
@emitter.emit 'did-destroy'
@cursor.emitter.dispose()
@emitter.dispose()
finalize: ->
@initialScreenRange = null unless @initialScreenRange?.isEqual(@getScreenRange())

View File

@@ -1,157 +0,0 @@
_ = require 'underscore-plus'
SpacePen = require 'space-pen'
{Subscriber} = require 'emissary'
Subscriber.includeInto(SpacePen.View)
jQuery = SpacePen.jQuery
JQueryCleanData = jQuery.cleanData
jQuery.cleanData = (elements) ->
jQuery(element).view()?.unsubscribe?() for element in elements
JQueryCleanData(elements)
SpacePenCallRemoveHooks = SpacePen.callRemoveHooks
SpacePen.callRemoveHooks = (element) ->
view.unsubscribe?() for view in SpacePen.viewsForElement(element)
SpacePenCallRemoveHooks(element)
NativeEventNames = new Set
NativeEventNames.add(nativeEvent) for nativeEvent in ["blur", "focus", "focusin",
"focusout", "load", "resize", "scroll", "unload", "click", "dblclick", "mousedown",
"mouseup", "mousemove", "mouseover", "mouseout", "mouseenter", "mouseleave", "change",
"select", "submit", "keydown", "keypress", "keyup", "error", "contextmenu", "textInput",
"textinput", "beforeunload"]
JQueryTrigger = jQuery.fn.trigger
jQuery.fn.trigger = (eventName, data) ->
if NativeEventNames.has(eventName) or typeof eventName is 'object'
JQueryTrigger.call(this, eventName, data)
else
data ?= {}
data.jQueryTrigger = true
for element in this
atom.commands.dispatch(element, eventName, data)
this
HandlersByOriginalHandler = new WeakMap
CommandDisposablesByElement = new WeakMap
AddEventListener = (element, type, listener) ->
if NativeEventNames.has(type)
element.addEventListener(type, listener)
else
disposable = atom.commands.add(element, type, listener)
unless disposablesByType = CommandDisposablesByElement.get(element)
disposablesByType = {}
CommandDisposablesByElement.set(element, disposablesByType)
unless disposablesByListener = disposablesByType[type]
disposablesByListener = new WeakMap
disposablesByType[type] = disposablesByListener
disposablesByListener.set(listener, disposable)
RemoveEventListener = (element, type, listener) ->
if NativeEventNames.has(type)
element.removeEventListener(type, listener)
else
CommandDisposablesByElement.get(element)?[type]?.get(listener)?.dispose()
JQueryEventAdd = jQuery.event.add
jQuery.event.add = (elem, types, originalHandler, data, selector) ->
handler = (event) ->
if arguments.length is 1 and event.originalEvent?.detail?
{detail} = event.originalEvent
if Array.isArray(detail)
originalHandler.apply(this, [event].concat(detail))
else
originalHandler.call(this, event, detail)
else
originalHandler.apply(this, arguments)
HandlersByOriginalHandler.set(originalHandler, handler)
JQueryEventAdd.call(this, elem, types, handler, data, selector, AddEventListener if atom?.commands?)
JQueryEventRemove = jQuery.event.remove
jQuery.event.remove = (elem, types, originalHandler, selector, mappedTypes) ->
if originalHandler?
handler = HandlersByOriginalHandler.get(originalHandler) ? originalHandler
JQueryEventRemove(elem, types, handler, selector, mappedTypes, RemoveEventListener if atom?.commands?)
JQueryContains = jQuery.contains
jQuery.contains = (a, b) ->
shadowRoot = null
currentNode = b
while currentNode
if currentNode instanceof ShadowRoot and a.contains(currentNode.host)
return true
currentNode = currentNode.parentNode
JQueryContains.call(this, a, b)
tooltipDefaults =
delay:
show: 1000
hide: 100
container: 'body'
html: true
placement: 'auto top'
viewportPadding: 2
humanizeKeystrokes = (keystroke) ->
keystrokes = keystroke.split(' ')
keystrokes = (_.humanizeKeystroke(stroke) for stroke in keystrokes)
keystrokes.join(' ')
getKeystroke = (bindings) ->
if bindings?.length
"<span class=\"keystroke\">#{humanizeKeystrokes(bindings[0].keystrokes)}</span>"
else
''
requireBootstrapTooltip = _.once ->
atom.requireWithGlobals('bootstrap/js/tooltip', {jQuery})
# options from http://getbootstrap.com/javascript/#tooltips
jQuery.fn.setTooltip = (tooltipOptions, {command, commandElement}={}) ->
requireBootstrapTooltip()
tooltipOptions = {title: tooltipOptions} if _.isString(tooltipOptions)
if commandElement
bindings = atom.keymaps.findKeyBindings(command: command, target: commandElement[0])
else if command
bindings = atom.keymaps.findKeyBindings(command: command)
tooltipOptions.title = "#{tooltipOptions.title} #{getKeystroke(bindings)}"
@tooltip(jQuery.extend({}, tooltipDefaults, tooltipOptions))
jQuery.fn.hideTooltip = ->
tip = @data('bs.tooltip')
if tip
tip.leave(currentTarget: this)
tip.hide()
jQuery.fn.destroyTooltip = ->
@hideTooltip()
requireBootstrapTooltip()
@tooltip('destroy')
# Hide tooltips when window is resized
jQuery(document.body).on 'show.bs.tooltip', ({target}) ->
windowHandler = -> jQuery(target).hideTooltip()
jQuery(window).one('resize', windowHandler)
jQuery(target).one 'hide.bs.tooltip', ->
jQuery(window).off('resize', windowHandler)
jQuery.fn.setTooltip.getKeystroke = getKeystroke
jQuery.fn.setTooltip.humanizeKeystrokes = humanizeKeystrokes
Object.defineProperty jQuery.fn, 'element', get: -> @[0]
module.exports = SpacePen

View File

@@ -12,6 +12,7 @@ LinesComponent = require './lines-component'
ScrollbarComponent = require './scrollbar-component'
ScrollbarCornerComponent = require './scrollbar-corner-component'
OverlayManager = require './overlay-manager'
DOMElementPool = require './dom-element-pool'
module.exports =
class TextEditorComponent
@@ -26,8 +27,6 @@ class TextEditorComponent
updatesPaused: false
updateRequestedWhilePaused: false
heightAndWidthMeasurementRequested: false
cursorMoved: false
selectionChanged: false
inputEnabled: true
measureScrollbarsWhenShown: true
measureLineHeightAndDefaultCharWidthWhenShown: true
@@ -54,6 +53,8 @@ class TextEditorComponent
@presenter.onDidUpdateState(@requestUpdate)
@domElementPool = new DOMElementPool
@domNode = document.createElement('div')
if @useShadowDOM
@domNode.classList.add('editor-contents--private')
@@ -75,7 +76,7 @@ class TextEditorComponent
@hiddenInputComponent = new InputComponent
@scrollViewNode.appendChild(@hiddenInputComponent.getDomNode())
@linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM})
@linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM, @domElementPool})
@scrollViewNode.appendChild(@linesComponent.getDomNode())
@horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll})
@@ -107,6 +108,7 @@ class TextEditorComponent
@disposables.dispose()
@presenter.destroy()
@gutterContainerComponent?.destroy()
@domElementPool.clear()
getDomNode: ->
@domNode
@@ -115,11 +117,6 @@ class TextEditorComponent
@oldState ?= {}
@newState = @presenter.getState()
cursorMoved = @cursorMoved
selectionChanged = @selectionChanged
@cursorMoved = false
@selectionChanged = false
if @editor.getLastSelection()? and not @editor.getLastSelection().isEmpty()
@domNode.classList.add('has-selection')
else
@@ -152,20 +149,20 @@ class TextEditorComponent
@overlayManager?.render(@newState)
if @clearPoolAfterUpdate
@domElementPool.clear()
@clearPoolAfterUpdate = false
if @editor.isAlive()
@updateParentViewFocusedClassIfNeeded()
@updateParentViewMiniClass()
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()
mountGutterContainerComponent: ->
@gutterContainerComponent = new GutterContainerComponent({@editor, @onLineNumberGutterMouseDown})
@gutterContainerComponent = new GutterContainerComponent({@editor, @onLineNumberGutterMouseDown, @domElementPool})
@domNode.insertBefore(@gutterContainerComponent.getDomNode(), @domNode.firstChild)
becameVisible: ->
@@ -215,8 +212,6 @@ class TextEditorComponent
observeEditor: ->
@disposables.add @editor.observeGrammar(@onGrammarChanged)
@disposables.add @editor.observeCursors(@onCursorAdded)
@disposables.add @editor.observeSelections(@onSelectionAdded)
listenForDOMEvents: ->
@domNode.addEventListener 'mousewheel', @onMouseWheel
@@ -482,29 +477,6 @@ class TextEditorComponent
@sampleBackgroundColors()
@remeasureCharacterWidths()
onSelectionAdded: (selection) =>
selectionDisposables = new CompositeDisposable
selectionDisposables.add selection.onDidChangeRange => @onSelectionChanged(selection)
selectionDisposables.add selection.onDidDestroy =>
@onSelectionChanged(selection)
selectionDisposables.dispose()
@disposables.remove(selectionDisposables)
@disposables.add(selectionDisposables)
if @editor.selectionIntersectsVisibleRowRange(selection)
@selectionChanged = true
onSelectionChanged: (selection) =>
if @editor.selectionIntersectsVisibleRowRange(selection)
@selectionChanged = true
onCursorAdded: (cursor) =>
@disposables.add cursor.onDidChangePosition @onCursorMoved
onCursorMoved: =>
@cursorMoved = true
handleDragUntilMouseUp: (dragHandler) =>
dragging = false
lastMousePosition = {}
@@ -545,20 +517,24 @@ class TextEditorComponent
disposables.dispose()
autoscroll = (mouseClientPosition) =>
editorClientRect = @domNode.getBoundingClientRect()
{top, bottom, left, right} = @scrollViewNode.getBoundingClientRect()
top += 30
bottom -= 30
left += 30
right -= 30
if mouseClientPosition.clientY < editorClientRect.top
mouseYDelta = editorClientRect.top - mouseClientPosition.clientY
if mouseClientPosition.clientY < top
mouseYDelta = top - mouseClientPosition.clientY
yDirection = -1
else if mouseClientPosition.clientY > editorClientRect.bottom
mouseYDelta = mouseClientPosition.clientY - editorClientRect.bottom
else if mouseClientPosition.clientY > bottom
mouseYDelta = mouseClientPosition.clientY - bottom
yDirection = 1
if mouseClientPosition.clientX < editorClientRect.left
mouseXDelta = editorClientRect.left - mouseClientPosition.clientX
if mouseClientPosition.clientX < left
mouseXDelta = left - mouseClientPosition.clientX
xDirection = -1
else if mouseClientPosition.clientX > editorClientRect.right
mouseXDelta = mouseClientPosition.clientX - editorClientRect.right
else if mouseClientPosition.clientX > right
mouseXDelta = mouseClientPosition.clientX - right
xDirection = 1
if mouseYDelta?
@@ -578,7 +554,7 @@ class TextEditorComponent
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp)
disposables = new CompositeDisposable
disposables.add(@editor.onWillInsertText(onMouseUp))
disposables.add(@editor.getBuffer().onWillChange(onMouseUp))
disposables.add(@editor.onDidDestroy(stopDragging))
isVisible: ->
@@ -645,6 +621,7 @@ class TextEditorComponent
{@fontSize, @fontFamily, @lineHeight} = getComputedStyle(@getTopmostDOMNode())
if @fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily or @lineHeight isnt oldLineHeight
@clearPoolAfterUpdate = true
@measureLineHeightAndDefaultCharWidth()
if (@fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily) and @performedInitialMeasurement
@@ -745,6 +722,13 @@ class TextEditorComponent
tileComponent?.lineNumberNodeForScreenRow(screenRow)
tileNodesForLines: ->
@linesComponent.getTiles()
tileNodesForLineNumbers: ->
gutterComponent = @gutterContainerComponent.getLineNumberGutterComponent()
gutterComponent.getTiles()
screenRowForNode: (node) ->
while node?
if screenRow = node.dataset.screenRow
@@ -799,6 +783,9 @@ class TextEditorComponent
setInputEnabled: (@inputEnabled) -> @inputEnabled
setContinuousReflow: (continuousReflow) ->
@presenter.setContinuousReflow(continuousReflow)
updateParentViewFocusedClassIfNeeded: ->
if @oldState.focused isnt @newState.focused
@hostElement.classList.toggle('is-focused', @newState.focused)

View File

@@ -1,12 +1,10 @@
{Emitter} = require 'event-kit'
{View, $, callRemoveHooks} = require 'space-pen'
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
ShadowStyleSheet = null
@@ -22,7 +20,6 @@ class TextEditorElement extends HTMLElement
createdCallback: ->
@emitter = new Emitter
@initializeContent()
@createSpacePenShim() if Grim.includeDeprecatedAPIs
@addEventListener 'focus', @focused.bind(this)
@addEventListener 'blur', @blurred.bind(this)
@@ -56,10 +53,6 @@ class TextEditorElement extends HTMLElement
@stylesElement = document.head.querySelector('atom-styles')
@rootElement = this
createSpacePenShim: ->
TextEditorView ?= require './text-editor-view'
@__spacePenView = new TextEditorView(this)
attachedCallback: ->
@buildModel() unless @getModel()?
atom.assert(@model.isAlive(), "Attaching a view for a destroyed editor")
@@ -90,7 +83,6 @@ class TextEditorElement extends HTMLElement
@model.onDidChangeEncoding => @addEncodingAttribute()
@model.onDidDestroy => @unmountComponent()
@model.onDidChangeMini (mini) => if mini then @addMiniAttribute() else @removeMiniAttribute()
@__spacePenView.setModel(@model) if Grim.includeDeprecatedAPIs
@model
getModel: ->
@@ -126,7 +118,6 @@ class TextEditorElement extends HTMLElement
inputNode.addEventListener 'blur', => @dispatchEvent(new FocusEvent('blur', bubbles: false))
unmountComponent: ->
callRemoveHooks(this)
if @component?
@component.destroy()
@component.getDomNode().remove()
@@ -172,6 +163,12 @@ class TextEditorElement extends HTMLElement
isUpdatedSynchronously: -> @updatedSynchronously
# Extended: Continuously reflows lines and line numbers. (Has performance overhead)
#
# `continuousReflow` A {Boolean} indicating whether to keep reflowing or not.
setContinuousReflow: (continuousReflow) ->
@component?.setContinuousReflow(continuousReflow)
# Extended: get the width of a character of text displayed in this element.
#
# Returns a {Number} of pixels.
@@ -299,6 +296,7 @@ atom.commands.add 'atom-text-editor', stopEventPropagationAndGroupUndo(
'editor:delete-to-end-of-subword': -> @deleteToEndOfSubword()
'editor:delete-line': -> @deleteLine()
'editor:cut-to-end-of-line': -> @cutToEndOfLine()
'editor:cut-to-end-of-buffer-line': -> @cutToEndOfBufferLine()
'editor:transpose': -> @transpose()
'editor:upper-case': -> @upperCase()
'editor:lower-case': -> @lowerCase()

View File

@@ -11,6 +11,7 @@ class TextEditorPresenter
mouseWheelScreenRow: null
scopedCharacterWidthsChangeCount: 0
overlayDimensions: {}
minimumReflowInterval: 200
constructor: (params) ->
{@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft, @boundingClientRect, @windowWidth, @windowHeight, @gutterWidth} = params
@@ -20,7 +21,7 @@ class TextEditorPresenter
@measuredHorizontalScrollbarHeight = horizontalScrollbarHeight
@measuredVerticalScrollbarWidth = verticalScrollbarWidth
@gutterWidth ?= 0
@tileSize ?= 12
@tileSize ?= 6
@disposables = new CompositeDisposable
@emitter = new Emitter
@@ -35,6 +36,7 @@ class TextEditorPresenter
@observeConfig()
@buildState()
@startBlinkingCursors() if @focused
@startReflowing() if @continuousReflow
@updating = false
destroy: ->
@@ -72,6 +74,7 @@ class TextEditorPresenter
@updateStartRow()
@updateEndRow()
@updateCommonGutterState()
@updateReflowState()
@updateFocusedState() if @shouldUpdateFocusedState
@updateHeightState() if @shouldUpdateHeightState
@@ -254,6 +257,23 @@ class TextEditorPresenter
@resetTrackedUpdates()
setContinuousReflow: (@continuousReflow) ->
if @continuousReflow
@startReflowing()
else
@stopReflowing()
updateReflowState: ->
@state.content.continuousReflow = @continuousReflow
@lineNumberGutter.continuousReflow = @continuousReflow
startReflowing: ->
@reflowingInterval = setInterval(@emitDidUpdateState.bind(this), @minimumReflowInterval)
stopReflowing: ->
clearInterval(@reflowingInterval)
@reflowingInterval = null
updateFocusedState: ->
@state.focused = @focused
@@ -527,7 +547,7 @@ class TextEditorPresenter
# decoration.id : {
# top: # of pixels from top
# height: # of pixels height of this decoration
# item (optional): HTMLElement or space-pen View
# item (optional): HTMLElement
# class (optional): {String} class
# }
# }
@@ -584,22 +604,15 @@ class TextEditorPresenter
if startRow > 0
rowBeforeStartRow = startRow - 1
lastBufferRow = @model.bufferRowForScreenRow(rowBeforeStartRow)
wrapCount = rowBeforeStartRow - @model.screenRowForBufferRow(lastBufferRow)
else
lastBufferRow = null
wrapCount = 0
if endRow > startRow
bufferRows = @model.bufferRowsForScreenRows(startRow, endRow - 1)
zIndex = bufferRows.length - 1
for bufferRow, i in bufferRows
if bufferRow is lastBufferRow
wrapCount++
id = bufferRow + '-' + wrapCount
softWrapped = true
else
id = bufferRow
wrapCount = 0
lastBufferRow = bufferRow
softWrapped = false
@@ -607,10 +620,10 @@ class TextEditorPresenter
top = (screenRow - startRow) * @lineHeight
decorationClasses = @lineNumberDecorationClassesForRow(screenRow)
foldable = @model.isFoldableAtScreenRow(screenRow)
id = @model.tokenizedLineForScreenRow(screenRow).id
tileState.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable, zIndex}
tileState.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable}
visibleLineNumberIds[id] = true
zIndex--
for id of tileState.lineNumbers
delete tileState.lineNumbers[id] unless visibleLineNumberIds[id]

View File

@@ -1,324 +0,0 @@
{View, $} = require 'space-pen'
{defaults} = require 'underscore-plus'
TextBuffer = require 'text-buffer'
TextEditor = require './text-editor'
TextEditorElement = require './text-editor-element'
TextEditorComponent = require './text-editor-component'
{deprecate} = require 'grim'
# Deprecated: Represents the entire visual pane in Atom.
#
# The TextEditorView manages the {TextEditor}, which manages the file buffers.
# `TextEditorView` is intentionally sparse. Most of the things you'll want
# to do are on {TextEditor}.
#
# ## Examples
#
# Requiring in packages
#
# ```coffee
# {TextEditorView} = require 'atom'
#
# miniEditorView = new TextEditorView(mini: true)
# ```
#
# Iterating over the open editor views
#
# ```coffee
# for editorView in atom.workspaceView.getEditorViews()
# console.log(editorView.getModel().getPath())
# ```
#
# Subscribing to every current and future editor
#
# ```coffee
# atom.workspace.eachEditorView (editorView) ->
# console.log(editorView.getModel().getPath())
# ```
module.exports =
class TextEditorView extends View
# The constructor for setting up an `TextEditorView` instance.
#
# * `modelOrParams` Either an {TextEditor}, or an object with one property, `mini`.
# If `mini` is `true`, a "miniature" `TextEditor` is constructed.
# Typically, this is ideal for scenarios where you need an Atom editor,
# but without all the chrome, like scrollbars, gutter, _e.t.c._.
#
constructor: (modelOrParams, props) ->
# Handle direct construction with an editor or params
unless modelOrParams instanceof HTMLElement
if modelOrParams instanceof TextEditor
model = modelOrParams
else
{editor, mini, placeholderText, attributes} = modelOrParams
model = editor ? new TextEditor
buffer: new TextBuffer
softWrapped: false
tabLength: 2
softTabs: true
mini: mini
placeholderText: placeholderText
element = new TextEditorElement
element.tileSize = props?.tileSize
element.setAttribute(name, value) for name, value of attributes if attributes?
element.setModel(model)
return element.__spacePenView
# Handle construction with an element
@element = modelOrParams
super
setModel: (@model) ->
@editor = @model
@root = $(@element.rootElement)
@scrollView = @root.find('.scroll-view')
if atom.config.get('editor.useShadowDOM')
@underlayer = $("<div class='underlayer'></div>").appendTo(this)
@overlayer = $("<div class='overlayer'></div>").appendTo(this)
else
@underlayer = @find('.highlights').addClass('underlayer')
@overlayer = @find('.lines').addClass('overlayer')
@hiddenInput = @root.find('.hidden-input')
@hiddenInput.on = (args...) =>
args[0] = 'blur' if args[0] is 'focusout'
$::on.apply(this, args)
@subscribe atom.config.observe 'editor.showLineNumbers', =>
@gutter = @root.find('.gutter')
@gutter.removeClassFromAllLines = (klass) =>
deprecate('Use decorations instead: http://blog.atom.io/2014/07/24/decorations.html')
@gutter.find('.line-number').removeClass(klass)
@gutter.getLineNumberElement = (bufferRow) =>
deprecate('Use decorations instead: http://blog.atom.io/2014/07/24/decorations.html')
@gutter.find("[data-buffer-row='#{bufferRow}']")
@gutter.addClassToLine = (bufferRow, klass) =>
deprecate('Use decorations instead: http://blog.atom.io/2014/07/24/decorations.html')
lines = @gutter.find("[data-buffer-row='#{bufferRow}']")
lines.addClass(klass)
lines.length > 0
find: ->
shadowResult = @root.find.apply(@root, arguments)
if shadowResult.length > 0
shadowResult
else
super
# Public: Get the underlying editor model for this view.
#
# Returns an {TextEditor}
getModel: -> @model
getEditor: -> @model
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
return if @attached
@attached = true
@trigger 'editor:attached', [this]
beforeRemove: ->
@trigger 'editor:detached', [this]
@trigger 'editor:will-be-removed', [this]
@attached = false
remove: (selector, keepData) ->
@model.destroy() unless keepData
super
scrollTop: (scrollTop) ->
if scrollTop?
@model.setScrollTop(scrollTop)
else
@model.getScrollTop()
scrollLeft: (scrollLeft) ->
if scrollLeft?
@model.setScrollLeft(scrollLeft)
else
@model.getScrollLeft()
scrollToBottom: ->
deprecate 'Use TextEditor::scrollToBottom instead. You can get the editor via editorView.getModel()'
@model.setScrollBottom(Infinity)
scrollToScreenPosition: (screenPosition, options) ->
deprecate 'Use TextEditor::scrollToScreenPosition instead. You can get the editor via editorView.getModel()'
@model.scrollToScreenPosition(screenPosition, options)
scrollToBufferPosition: (bufferPosition, options) ->
deprecate 'Use TextEditor::scrollToBufferPosition instead. You can get the editor via editorView.getModel()'
@model.scrollToBufferPosition(bufferPosition, options)
scrollToCursorPosition: ->
deprecate 'Use TextEditor::scrollToCursorPosition instead. You can get the editor via editorView.getModel()'
@model.scrollToCursorPosition()
pixelPositionForBufferPosition: (bufferPosition) ->
deprecate 'Use TextEditorElement::pixelPositionForBufferPosition instead. You can get the editor via editorView.getModel()'
@model.pixelPositionForBufferPosition(bufferPosition, true)
pixelPositionForScreenPosition: (screenPosition) ->
deprecate 'Use TextEditorElement::pixelPositionForScreenPosition instead. You can get the editor via editorView.getModel()'
@model.pixelPositionForScreenPosition(screenPosition, true)
appendToLinesView: (view) ->
view.css('position', 'absolute')
view.css('z-index', 1)
@overlayer.append(view)
splitLeft: ->
deprecate """
Use Pane::splitLeft instead.
To duplicate this editor into the split use:
editorView.getPaneView().getModel().splitLeft(copyActiveItem: true)
"""
pane = @getPaneView()
pane?.splitLeft(pane?.copyActiveItem()).activeView
splitRight: ->
deprecate """
Use Pane::splitRight instead.
To duplicate this editor into the split use:
editorView.getPaneView().getModel().splitRight(copyActiveItem: true)
"""
pane = @getPaneView()
pane?.splitRight(pane?.copyActiveItem()).activeView
splitUp: ->
deprecate """
Use Pane::splitUp instead.
To duplicate this editor into the split use:
editorView.getPaneView().getModel().splitUp(copyActiveItem: true)
"""
pane = @getPaneView()
pane?.splitUp(pane?.copyActiveItem()).activeView
splitDown: ->
deprecate """
Use Pane::splitDown instead.
To duplicate this editor into the split use:
editorView.getPaneView().getModel().splitDown(copyActiveItem: true)
"""
pane = @getPaneView()
pane?.splitDown(pane?.copyActiveItem()).activeView
# Public: Get this {TextEditorView}'s {PaneView}.
#
# Returns a {PaneView}
getPaneView: ->
@parent('.item-views').parents('atom-pane').view()
getPane: ->
deprecate 'Use TextEditorView::getPaneView() instead'
@getPaneView()
show: ->
super
@component?.checkForVisibilityChange()
hide: ->
super
@component?.checkForVisibilityChange()
pageDown: ->
deprecate('Use editorView.getModel().pageDown()')
@model.pageDown()
pageUp: ->
deprecate('Use editorView.getModel().pageUp()')
@model.pageUp()
getFirstVisibleScreenRow: ->
deprecate 'Use TextEditorElement::getFirstVisibleScreenRow instead.'
@model.getFirstVisibleScreenRow(true)
getLastVisibleScreenRow: ->
deprecate 'Use TextEditor::getLastVisibleScreenRow instead. You can get the editor via editorView.getModel()'
@model.getLastVisibleScreenRow()
getFontFamily: ->
deprecate 'This is going away. Use atom.config.get("editor.fontFamily") instead'
@component?.getFontFamily()
setFontFamily: (fontFamily) ->
deprecate 'This is going away. Use atom.config.set("editor.fontFamily", "my-font") instead'
@component?.setFontFamily(fontFamily)
getFontSize: ->
deprecate 'This is going away. Use atom.config.get("editor.fontSize") instead'
@component?.getFontSize()
setFontSize: (fontSize) ->
deprecate 'This is going away. Use atom.config.set("editor.fontSize", 12) instead'
@component?.setFontSize(fontSize)
setLineHeight: (lineHeight) ->
deprecate 'This is going away. Use atom.config.set("editor.lineHeight", 1.5) instead'
@component.setLineHeight(lineHeight)
setWidthInChars: (widthInChars) ->
@component.getDOMNode().style.width = (@model.getDefaultCharWidth() * widthInChars) + 'px'
setShowIndentGuide: (showIndentGuide) ->
deprecate 'This is going away. Use atom.config.set("editor.showIndentGuide", true|false) instead'
atom.config.set("editor.showIndentGuide", showIndentGuide)
setSoftWrap: (softWrapped) ->
deprecate 'Use TextEditor::setSoftWrapped instead. You can get the editor via editorView.getModel()'
@model.setSoftWrapped(softWrapped)
setShowInvisibles: (showInvisibles) ->
deprecate 'This is going away. Use atom.config.set("editor.showInvisibles", true|false) instead'
@component.setShowInvisibles(showInvisibles)
getText: ->
@model.getText()
setText: (text) ->
@model.setText(text)
insertText: (text) ->
@model.insertText(text)
isInputEnabled: ->
@component.isInputEnabled()
setInputEnabled: (inputEnabled) ->
@component.setInputEnabled(inputEnabled)
requestDisplayUpdate: ->
deprecate('Please remove from your code. ::requestDisplayUpdate no longer does anything')
updateDisplay: ->
deprecate('Please remove from your code. ::updateDisplay no longer does anything')
resetDisplay: ->
deprecate('Please remove from your code. ::resetDisplay no longer does anything')
redraw: ->
deprecate('Please remove from your code. ::redraw no longer does anything')
setPlaceholderText: (placeholderText) ->
deprecate('Use TextEditor::setPlaceholderText instead. eg. editorView.getModel().setPlaceholderText(text)')
@model.setPlaceholderText(placeholderText)
lineElementForScreenRow: (screenRow) ->
$(@component.lineNodeForScreenRow(screenRow))

View File

@@ -17,7 +17,8 @@ GutterContainer = require './gutter-container'
# Essential: This class represents all essential editing state for a single
# {TextBuffer}, including cursor and selection positions, folds, and soft wraps.
# If you're manipulating the state of an editor, use this class. If you're
# interested in the visual appearance of editors, use {TextEditorView} instead.
# interested in the visual appearance of editors, use {TextEditorElement}
# instead.
#
# A single {TextBuffer} can belong to multiple editors. For example, if the
# same file is open in two different panes, Atom creates a separate editor for
@@ -545,8 +546,8 @@ class TextEditor extends Model
# Set the number of characters that can be displayed horizontally in the
# editor.
#
# * `editorWidthInChars` A {Number} representing the width of the {TextEditorView}
# in characters.
# * `editorWidthInChars` A {Number} representing the width of the
# {TextEditorElement} in characters.
setEditorWidthInChars: (editorWidthInChars) ->
@displayBuffer.setEditorWidthInChars(editorWidthInChars)
@@ -1761,18 +1762,11 @@ class TextEditor extends Model
@emitter.emit 'did-add-cursor', cursor
cursor
# Remove the given cursor from this editor.
removeCursor: (cursor) ->
_.remove(@cursors, cursor)
@emit 'cursor-removed', cursor if includeDeprecatedAPIs
@emitter.emit 'did-remove-cursor', cursor
moveCursors: (fn) ->
fn(cursor) for cursor in @getCursors()
@mergeCursors()
cursorMoved: (event) ->
@emit 'cursor-moved', event if includeDeprecatedAPIs
@emitter.emit 'did-change-cursor-position', event
# Merge cursors that have the same screen position
@@ -2259,9 +2253,9 @@ class TextEditor extends Model
# Remove the given selection.
removeSelection: (selection) ->
_.remove(@cursors, selection.cursor)
_.remove(@selections, selection)
atom.assert @selections.length > 0, "Removed last selection"
@emit 'selection-removed', selection if includeDeprecatedAPIs
@emitter.emit 'did-remove-cursor', selection.cursor
@emitter.emit 'did-remove-selection', selection
# Reduce one or more selections to a single empty selection based on the most
@@ -2281,7 +2275,6 @@ class TextEditor extends Model
# Called by the selection
selectionRangeChanged: (event) ->
@emit 'selection-screen-range-changed', event if includeDeprecatedAPIs
@emitter.emit 'did-change-selection-range', event
createLastSelectionIfNeeded: ->
@@ -2672,7 +2665,7 @@ class TextEditor extends Model
@emitter.emit 'did-insert-text', didInsertEvent
# Essential: For each selection, if the selection is empty, cut all characters
# of the containing line following the cursor. Otherwise cut the selected
# of the containing screen line following the cursor. Otherwise cut the selected
# text.
cutToEndOfLine: ->
maintainClipboard = false
@@ -2680,6 +2673,15 @@ class TextEditor extends Model
selection.cutToEndOfLine(maintainClipboard)
maintainClipboard = true
# Essential: For each selection, if the selection is empty, cut all characters
# of the containing buffer line following the cursor. Otherwise cut the
# selected text.
cutToEndOfBufferLine: ->
maintainClipboard = false
@mutateSelectedText (selection) ->
selection.cutToEndOfBufferLine(maintainClipboard)
maintainClipboard = true
###
Section: Folds
###
@@ -3059,9 +3061,6 @@ if includeDeprecatedAPIs
'$verticalScrollbarWidth', '$horizontalScrollbarHeight', '$scrollTop', '$scrollLeft',
toProperty: 'displayBuffer'
TextEditor::getViewClass = ->
require './text-editor-view'
TextEditor::joinLine = ->
deprecate("Use TextEditor::joinLines() instead")
@joinLines()

View File

@@ -3,7 +3,6 @@ _ = require 'underscore-plus'
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
{File} = require 'pathwatcher'
fs = require 'fs-plus'
Q = require 'q'
Grim = require 'grim'
# Extended: Handles loading and activating available themes.
@@ -260,32 +259,29 @@ class ThemeManager
string.replace(/\\/g, '/')
activateThemes: ->
deferred = Q.defer()
new Promise (resolve) =>
# atom.config.observe runs the callback once, then on subsequent changes.
atom.config.observe 'core.themes', =>
@deactivateThemes()
# atom.config.observe runs the callback once, then on subsequent changes.
atom.config.observe 'core.themes', =>
@deactivateThemes()
@refreshLessCache() # Update cache for packages in core.themes config
@refreshLessCache() # Update cache for packages in core.themes config
promises = []
for themeName in @getEnabledThemeNames()
if @packageManager.resolvePackagePath(themeName)
promises.push(@packageManager.activatePackage(themeName))
else
console.warn("Failed to activate theme '#{themeName}' because it isn't installed.")
promises = []
for themeName in @getEnabledThemeNames()
if @packageManager.resolvePackagePath(themeName)
promises.push(@packageManager.activatePackage(themeName))
else
console.warn("Failed to activate theme '#{themeName}' because it isn't installed.")
Q.all(promises).then =>
@addActiveThemeClasses()
@refreshLessCache() # Update cache again now that @getActiveThemes() is populated
@loadUserStylesheet()
@reloadBaseStylesheets()
@initialLoadComplete = true
@emit 'reloaded' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change-active-themes'
deferred.resolve()
deferred.promise
Promise.all(promises).then =>
@addActiveThemeClasses()
@refreshLessCache() # Update cache again now that @getActiveThemes() is populated
@loadUserStylesheet()
@reloadBaseStylesheets()
@initialLoadComplete = true
@emit 'reloaded' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-change-active-themes'
resolve()
deactivateThemes: ->
@removeActiveThemeClasses()

View File

@@ -1,4 +1,3 @@
Q = require 'q'
Package = require './package'
module.exports =
@@ -18,14 +17,12 @@ class ThemePackage extends Package
this
activate: ->
return @activationDeferred.promise if @activationDeferred?
@activationDeferred = Q.defer()
@measure 'activateTime', =>
try
@loadStylesheets()
@activateNow()
catch error
@handleError("Failed to activate the #{@name} theme", error)
@activationDeferred.promise
@activationPromise ?= new Promise (resolve, reject) =>
@resolveActivationPromise = resolve
@rejectActivationPromise = reject
@measure 'activateTime', =>
try
@loadStylesheets()
@activateNow()
catch error
@handleError("Failed to activate the #{@name} theme", error)

View File

@@ -1,3 +1,5 @@
{values} = require 'underscore-plus'
cloneObject = (object) ->
clone = {}
clone[key] = value for key, value of object
@@ -21,9 +23,7 @@ class TiledComponent
return
removeTileNode: (tileRow) ->
node = @componentsByTileId[tileRow].getDomNode()
node.remove()
@componentsByTileId[tileRow].destroy()
delete @componentsByTileId[tileRow]
delete @oldState.tiles[tileRow]
@@ -49,3 +49,6 @@ class TiledComponent
getComponentForTile: (tileRow) ->
@componentsByTileId[tileRow]
getTiles: ->
values(@componentsByTileId).map (component) -> component.getDomNode()

View File

@@ -31,11 +31,14 @@ class TokenIterator
while @index < tags.length
tag = tags[@index]
if tag < 0
scope = atom.grammars.scopeForId(tag)
if tag % 2 is 0
@scopeEnds.push(atom.grammars.scopeForId(tag + 1))
if @scopeStarts[@scopeStarts.length - 1] is scope
@scopeStarts.pop()
else
@scopeEnds.push(scope)
@scopes.pop()
else
scope = atom.grammars.scopeForId(tag)
@scopeStarts.push(scope)
@scopes.push(scope)
@index++

View File

@@ -1,6 +1,6 @@
_ = require 'underscore-plus'
{Disposable} = require 'event-kit'
{$} = require './space-pen-extensions'
{Disposable, CompositeDisposable} = require 'event-kit'
Tooltip = null
# Essential: Associates tooltips with HTML elements or selectors.
#
@@ -71,7 +71,12 @@ class TooltipManager
# Returns a {Disposable} on which `.dispose()` can be called to remove the
# tooltip.
add: (target, options) ->
requireBootstrapTooltip()
if target.jquery
disposable = new CompositeDisposable
disposable.add @add(element, options) for element in target
return disposable
Tooltip ?= require './tooltip'
{keyBindingCommand, keyBindingTarget} = options
@@ -83,15 +88,20 @@ class TooltipManager
else if keystroke?
options.title = getKeystroke(bindings)
$target = $(target)
$target.tooltip(_.defaults(options, @defaults))
tooltip = new Tooltip(target, _.defaults(options, @defaults))
new Disposable ->
tooltip = $target.data('bs.tooltip')
if tooltip?
tooltip.leave(currentTarget: target)
tooltip.hide()
$target.tooltip('destroy')
hideTooltip = ->
tooltip.leave(currentTarget: target)
tooltip.hide()
window.addEventListener('resize', hideTooltip)
disposable = new Disposable ->
window.removeEventListener('resize', hideTooltip)
hideTooltip()
tooltip.destroy()
disposable
humanizeKeystrokes = (keystroke) ->
keystrokes = keystroke.split(' ')
@@ -101,7 +111,3 @@ humanizeKeystrokes = (keystroke) ->
getKeystroke = (bindings) ->
if bindings?.length
"<span class=\"keystroke\">#{humanizeKeystrokes(bindings[0].keystrokes)}</span>"
else
requireBootstrapTooltip = _.once ->
atom.requireWithGlobals('bootstrap/js/tooltip', {jQuery: $})

456
src/tooltip.js Normal file
View File

@@ -0,0 +1,456 @@
'use strict'
const EventKit = require('event-kit')
const tooltipComponentsByElement = new WeakMap()
const listen = require('./delegated-listener')
// This tooltip class is derived from Bootstrap 3, but modified to not require
// jQuery, which is an expensive dependency we want to eliminate.
var Tooltip = function (element, options) {
this.options = null
this.enabled = null
this.timeout = null
this.hoverState = null
this.element = null
this.inState = null
this.init(element, options)
}
Tooltip.VERSION = '3.3.5'
Tooltip.TRANSITION_DURATION = 150
Tooltip.DEFAULTS = {
animation: true,
placement: 'top',
selector: false,
template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
trigger: 'hover focus',
title: '',
delay: 0,
html: false,
container: false,
viewport: {
selector: 'body',
padding: 0
}
}
Tooltip.prototype.init = function (element, options) {
this.enabled = true
this.element = element
this.options = this.getOptions(options)
this.disposables = new EventKit.CompositeDisposable()
if (this.options.viewport) {
if (typeof this.options.viewport === 'function') {
this.viewport = this.options.viewport.call(this, this.element)
} else {
this.viewport = document.querySelector(this.options.viewport.selector || this.options.viewport)
}
}
this.inState = {click: false, hover: false, focus: false}
if (this.element instanceof document.constructor && !this.options.selector) {
throw new Error('`selector` option must be specified when initializing tooltip on the window.document object!')
}
var triggers = this.options.trigger.split(' ')
for (var i = triggers.length; i--;) {
var trigger = triggers[i]
if (trigger === 'click') {
this.disposables.add(listen(this.element, 'click', this.options.selector, this.toggle.bind(this)))
} else if (trigger !== 'manual') {
var eventIn, eventOut
if (trigger === 'hover') {
if (this.options.selector) {
eventIn = 'mouseover'
eventOut = 'mouseout'
} else {
eventIn = 'mouseenter'
eventOut = 'mouseleave'
}
} else {
eventIn = 'focusin'
eventOut = 'focusout'
}
this.disposables.add(listen(this.element, eventIn, this.options.selector, this.enter.bind(this)))
this.disposables.add(listen(this.element, eventOut, this.options.selector, this.leave.bind(this)))
}
}
this.options.selector ?
(this._options = extend({}, this.options, { trigger: 'manual', selector: '' })) :
this.fixTitle()
}
Tooltip.prototype.getDefaults = function () {
return Tooltip.DEFAULTS
}
Tooltip.prototype.getOptions = function (options) {
options = extend({}, this.getDefaults(), options)
if (options.delay && typeof options.delay === 'number') {
options.delay = {
show: options.delay,
hide: options.delay
}
}
return options
}
Tooltip.prototype.getDelegateOptions = function () {
var options = {}
var defaults = this.getDefaults()
if (this._options) {
for (var key of Object.getOwnPropertyNames(this._options)) {
var value = this._options[key]
if (defaults[key] !== value) options[key] = value
}
}
return options
}
Tooltip.prototype.enter = function (event) {
if (event) {
if (event.currentTarget !== this.element) {
this.getDelegateComponent(event.currentTarget).enter(event)
return
}
this.inState[event.type === 'focusin' ? 'focus' : 'hover'] = true
}
if (this.getTooltipElement().classList.contains('in') || this.hoverState === 'in') {
this.hoverState = 'in'
return
}
clearTimeout(this.timeout)
this.hoverState = 'in'
if (!this.options.delay || !this.options.delay.show) return this.show()
this.timeout = setTimeout(function () {
if (this.hoverState === 'in') this.show()
}.bind(this), this.options.delay.show)
}
Tooltip.prototype.isInStateTrue = function () {
for (var key in this.inState) {
if (this.inState[key]) return true
}
return false
}
Tooltip.prototype.leave = function (event) {
if (event) {
if (event.currentTarget !== this.element) {
this.getDelegateComponent(event.currentTarget).leave(event)
return
}
this.inState[event.type === 'focusout' ? 'focus' : 'hover'] = false
}
if (this.isInStateTrue()) return
clearTimeout(this.timeout)
this.hoverState = 'out'
if (!this.options.delay || !this.options.delay.hide) return this.hide()
this.timeout = setTimeout(function () {
if (this.hoverState === 'out') this.hide()
}.bind(this), this.options.delay.hide)
}
Tooltip.prototype.show = function () {
if (this.hasContent() && this.enabled) {
var tip = this.getTooltipElement()
var tipId = this.getUID('tooltip')
this.setContent()
tip.setAttribute('id', tipId)
this.element.setAttribute('aria-describedby', tipId)
if (this.options.animation) tip.classList.add('fade')
var placement = typeof this.options.placement === 'function' ?
this.options.placement.call(this, tip, this.element) :
this.options.placement
var autoToken = /\s?auto?\s?/i
var autoPlace = autoToken.test(placement)
if (autoPlace) placement = placement.replace(autoToken, '') || 'top'
tip.remove()
tip.style.top = '0px'
tip.style.left = '0px'
tip.style.display = 'block'
tip.classList.add(placement)
document.body.appendChild(tip)
var pos = this.element.getBoundingClientRect()
var actualWidth = tip.offsetWidth
var actualHeight = tip.offsetHeight
if (autoPlace) {
var orgPlacement = placement
var viewportDim = this.viewport.getBoundingClientRect()
placement = placement === 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' :
placement === 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' :
placement === 'right' && pos.right + actualWidth > viewportDim.width ? 'left' :
placement === 'left' && pos.left - actualWidth < viewportDim.left ? 'right' :
placement
tip.classList.remove(orgPlacement)
tip.classList.add(placement)
}
var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)
this.applyPlacement(calculatedOffset, placement)
var prevHoverState = this.hoverState
this.hoverState = null
if (prevHoverState === 'out') this.leave()
}
}
Tooltip.prototype.applyPlacement = function (offset, placement) {
var tip = this.getTooltipElement()
var width = tip.offsetWidth
var height = tip.offsetHeight
// manually read margins because getBoundingClientRect includes difference
var computedStyle = window.getComputedStyle(tip)
var marginTop = parseInt(computedStyle.marginTop, 10)
var marginLeft = parseInt(computedStyle.marginLeft, 10)
offset.top += marginTop
offset.left += marginLeft
tip.style.top = offset.top + 'px'
tip.style.left = offset.left + 'px'
tip.classList.add('in')
// check to see if placing tip in new offset caused the tip to resize itself
var actualWidth = tip.offsetWidth
var actualHeight = tip.offsetHeight
if (placement === 'top' && actualHeight !== height) {
offset.top = offset.top + height - actualHeight
}
var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight)
if (delta.left) offset.left += delta.left
else offset.top += delta.top
var isVertical = /top|bottom/.test(placement)
var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight
var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight'
tip.style.top = offset.top + 'px'
tip.style.left = offset.left + 'px'
this.replaceArrow(arrowDelta, tip[arrowOffsetPosition], isVertical)
}
Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) {
var arrow = this.getArrowElement()
var amount = 50 * (1 - delta / dimension) + '%'
if (isVertical) {
arrow.style.left = amount
arrow.style.top = ''
} else {
arrow.style.top = amount
arrow.style.left = ''
}
}
Tooltip.prototype.setContent = function () {
var tip = this.getTooltipElement()
var title = this.getTitle()
var inner = tip.querySelector('.tooltip-inner')
if (this.options.html) {
inner.innerHTML = title
} else {
inner.textContent = title
}
tip.classList.remove('fade', 'in', 'top', 'bottom', 'left', 'right')
}
Tooltip.prototype.hide = function (callback) {
this.tip && this.tip.classList.remove('in')
if (this.hoverState !== 'in') this.tip && this.tip.remove()
this.element.removeAttribute('aria-describedby')
callback && callback()
this.hoverState = null
return this
}
Tooltip.prototype.fixTitle = function () {
if (this.element.getAttribute('title') || typeof this.element.getAttribute('data-original-title') !== 'string') {
this.element.setAttribute('data-original-title', this.element.getAttribute('title') || '')
this.element.setAttribute('title', '')
}
}
Tooltip.prototype.hasContent = function () {
return this.getTitle()
}
Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
return placement === 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } :
placement === 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } :
placement === 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
/* placement === 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }
}
Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) {
var delta = { top: 0, left: 0 }
if (!this.viewport) return delta
var viewportPadding = this.options.viewport && this.options.viewport.padding || 0
var viewportDimensions = this.viewport.getBoundingClientRect()
if (/right|left/.test(placement)) {
var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll
var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight
if (topEdgeOffset < viewportDimensions.top) { // top overflow
delta.top = viewportDimensions.top - topEdgeOffset
} else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow
delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset
}
} else {
var leftEdgeOffset = pos.left - viewportPadding
var rightEdgeOffset = pos.left + viewportPadding + actualWidth
if (leftEdgeOffset < viewportDimensions.left) { // left overflow
delta.left = viewportDimensions.left - leftEdgeOffset
} else if (rightEdgeOffset > viewportDimensions.right) { // right overflow
delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset
}
}
return delta
}
Tooltip.prototype.getTitle = function () {
var title = this.element.getAttribute('data-original-title')
if (title) {
return title
} else {
return (typeof this.options.title === 'function')
? this.options.title.call(this.element)
: this.options.title
}
}
Tooltip.prototype.getUID = function (prefix) {
do prefix += ~~(Math.random() * 1000000)
while (document.getElementById(prefix))
return prefix
}
Tooltip.prototype.getTooltipElement = function () {
if (!this.tip) {
let div = document.createElement('div')
div.innerHTML = this.options.template
if (div.children.length !== 1) {
throw new Error('Tooltip `template` option must consist of exactly 1 top-level element!')
}
this.tip = div.firstChild
}
return this.tip
}
Tooltip.prototype.getArrowElement = function () {
this.arrow = this.arrow || this.getTooltipElement().querySelector('.tooltip-arrow')
return this.arrow
}
Tooltip.prototype.enable = function () {
this.enabled = true
}
Tooltip.prototype.disable = function () {
this.enabled = false
}
Tooltip.prototype.toggleEnabled = function () {
this.enabled = !this.enabled
}
Tooltip.prototype.toggle = function (event) {
if (event) {
if (event.currentTarget !== this.element) {
this.getDelegateComponent(event.currentTarget).toggle(event)
return
}
this.inState.click = !this.inState.click
if (this.isInStateTrue()) this.enter()
else this.leave()
} else {
this.getTooltipElement().classList.contains('in') ? this.leave() : this.enter()
}
}
Tooltip.prototype.destroy = function () {
clearTimeout(this.timeout)
this.tip && this.tip.remove()
this.disposables.dispose()
}
Tooltip.prototype.getDelegateComponent = function (element) {
var component = tooltipComponentsByElement.get(element)
if (!component) {
component = new Tooltip(element, this.getDelegateOptions())
tooltipComponentsByElement.set(element, component)
}
return component
}
function extend () {
var args = Array.prototype.slice.apply(arguments)
var target = args.shift()
var source = args.shift()
while (source) {
for (var key of Object.getOwnPropertyNames(source)) {
target[key] = source[key]
}
source = args.shift()
}
return target
}
module.exports = Tooltip

View File

@@ -1,132 +1,56 @@
path = require 'path'
{$} = require './space-pen-extensions'
{Disposable} = require 'event-kit'
{Disposable, CompositeDisposable} = require 'event-kit'
ipc = require 'ipc'
shell = require 'shell'
{Subscriber} = require 'emissary'
fs = require 'fs-plus'
listen = require './delegated-listener'
# Handles low-level events related to the window.
module.exports =
class WindowEventHandler
Subscriber.includeInto(this)
constructor: ->
@reloadRequested = false
@subscriptions = new CompositeDisposable
@subscribe ipc, 'message', (message, detail) ->
switch message
when 'open-locations'
needsProjectPaths = atom.project?.getPaths().length is 0
@on(ipc, 'message', @handleIPCMessage)
@on(ipc, 'command', @handleIPCCommand)
@on(ipc, 'context-command', @handleIPCContextCommand)
for {pathToOpen, initialLine, initialColumn} in detail
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
atom.project.addPath(pathToOpen)
@addEventListener(window, 'focus', @handleWindowFocus)
@addEventListener(window, 'blur', @handleWindowBlur)
@addEventListener(window, 'beforeunload', @handleWindowBeforeunload)
@addEventListener(window, 'unload', @handleWindowUnload)
unless fs.isDirectorySync(pathToOpen)
atom.workspace?.open(pathToOpen, {initialLine, initialColumn})
@addEventListener(document, 'keydown', @handleDocumentKeydown)
@addEventListener(document, 'drop', @handleDocumentDrop)
@addEventListener(document, 'dragover', @handleDocumentDragover)
@addEventListener(document, 'contextmenu', @handleDocumentContextmenu)
@subscriptions.add listen(document, 'click', 'a', @handleLinkClick)
@subscriptions.add listen(document, 'submit', 'form', @handleFormSubmit)
return
when 'update-available'
atom.updateAvailable(detail)
# FIXME: Remove this when deprecations are removed
{releaseVersion} = detail
detail = [releaseVersion]
if workspaceElement = atom.views.getView(atom.workspace)
atom.commands.dispatch workspaceElement, "window:update-available", detail
@subscribe ipc, 'command', (command, args...) ->
activeElement = document.activeElement
# Use the workspace element view if body has focus
if activeElement is document.body and workspaceElement = atom.views.getView(atom.workspace)
activeElement = workspaceElement
atom.commands.dispatch(activeElement, command, args[0])
@subscribe ipc, 'context-command', (command, args...) ->
$(atom.contextMenu.activeElement).trigger(command, args...)
@subscribe $(window), 'focus', -> document.body.classList.remove('is-blurred')
@subscribe $(window), 'blur', -> document.body.classList.add('is-blurred')
@subscribe $(window), 'beforeunload', =>
confirmed = atom.workspace?.confirmClose(windowCloseRequested: true)
atom.hide() if confirmed and not @reloadRequested and atom.getCurrentWindow().isWebViewFocused()
@reloadRequested = false
atom.storeDefaultWindowDimensions()
atom.storeWindowDimensions()
if confirmed
atom.unloadEditorWindow()
else
ipc.send('cancel-window-close')
confirmed
@subscribe $(window), 'blur', -> atom.storeDefaultWindowDimensions()
@subscribe $(window), 'unload', -> atom.removeEditorWindow()
@subscribeToCommand $(window), 'window:toggle-full-screen', -> atom.toggleFullScreen()
@subscribeToCommand $(window), 'window:close', -> atom.close()
@subscribeToCommand $(window), 'window:reload', =>
@reloadRequested = true
atom.reload()
@subscribeToCommand $(window), 'window:toggle-dev-tools', -> atom.toggleDevTools()
@subscriptions.add atom.commands.add window,
'window:toggle-full-screen': @handleWindowToggleFullScreen
'window:close': @handleWindowClose
'window:reload': @handleWindowReload
'window:toggle-dev-tools': @handleWindowToggleDevTools
if process.platform in ['win32', 'linux']
@subscribeToCommand $(window), 'window:toggle-menu-bar', ->
atom.config.set('core.autoHideMenuBar', not atom.config.get('core.autoHideMenuBar'))
@subscriptions.add atom.commands.add window,
'window:toggle-menu-bar': @handleWindowToggleMenuBar
if atom.config.get('core.autoHideMenuBar')
detail = "To toggle, press the Alt key or execute the window:toggle-menu-bar command"
atom.notifications.addInfo('Menu bar hidden', {detail})
@subscribeToCommand $(document), 'core:focus-next', @focusNext
@subscribeToCommand $(document), 'core:focus-previous', @focusPrevious
document.addEventListener 'keydown', @onKeydown
document.addEventListener 'drop', @onDrop
@subscribe new Disposable =>
document.removeEventListener('drop', @onDrop)
document.addEventListener 'dragover', @onDragOver
@subscribe new Disposable =>
document.removeEventListener('dragover', @onDragOver)
@subscribe $(document), 'click', 'a', @openLink
# Prevent form submits from changing the current window's URL
@subscribe $(document), 'submit', 'form', (e) -> e.preventDefault()
@subscribe $(document), 'contextmenu', (e) ->
e.preventDefault()
atom.contextMenu.showForEvent(e)
@subscriptions.add atom.commands.add document,
'core:focus-next': @handleFocusNext
'core:focus-previous': @handleFocusPrevious
@handleNativeKeybindings()
# Wire commands that should be handled by Chromium for elements with the
# `.native-key-bindings` class.
handleNativeKeybindings: ->
menu = null
bindCommandToAction = (command, action) =>
@subscribe $(document), command, (event) ->
@addEventListener document, command, (event) ->
if event.target.webkitMatchesSelector('.native-key-bindings')
atom.getCurrentWindow().webContents[action]()
true
bindCommandToAction('core:copy', 'copy')
bindCommandToAction('core:paste', 'paste')
@@ -135,38 +59,41 @@ class WindowEventHandler
bindCommandToAction('core:select-all', 'selectAll')
bindCommandToAction('core:cut', 'cut')
onKeydown: (event) ->
unsubscribe: ->
@subscriptions.dispose()
on: (target, eventName, handler) ->
target.on(eventName, handler)
@subscriptions.add(new Disposable ->
target.removeListener(eventName, handler)
)
addEventListener: (target, eventName, handler) ->
target.addEventListener(eventName, handler)
@subscriptions.add(new Disposable(-> target.removeEventListener(eventName, handler)))
handleDocumentKeydown: (event) ->
atom.keymaps.handleKeyboardEvent(event)
event.stopImmediatePropagation()
onDrop: (event) ->
handleDrop: (evenDocumentt) ->
event.preventDefault()
event.stopPropagation()
onDragOver: (event) ->
handleDragover: (Documentevent) ->
event.preventDefault()
event.stopPropagation()
event.dataTransfer.dropEffect = 'none'
openLink: ({target, currentTarget}) ->
location = target?.getAttribute('href') or currentTarget?.getAttribute('href')
if location and location[0] isnt '#' and /^https?:\/\//.test(location)
shell.openExternal(location)
false
eachTabIndexedElement: (callback) ->
for element in $('[tabindex]')
element = $(element)
continue if element.isDisabled()
tabIndex = parseInt(element.attr('tabindex'))
continue unless tabIndex >= 0
callback(element, tabIndex)
for element in document.querySelectorAll('[tabindex]')
continue if element.disabled
continue unless element.tabIndex >= 0
callback(element, element.tabIndex)
return
focusNext: =>
focusedTabIndex = parseInt($(':focus').attr('tabindex')) or -Infinity
handleFocusNext: =>
focusedTabIndex = document.activeElement.tabIndex ? -Infinity
nextElement = null
nextTabIndex = Infinity
@@ -186,8 +113,8 @@ class WindowEventHandler
else if lowestElement?
lowestElement.focus()
focusPrevious: =>
focusedTabIndex = parseInt($(':focus').attr('tabindex')) or Infinity
handleFocusPrevious: =>
focusedTabIndex = document.activeElement.tabIndex ? Infinity
previousElement = null
previousTabIndex = -Infinity
@@ -206,3 +133,92 @@ class WindowEventHandler
previousElement.focus()
else if highestElement?
highestElement.focus()
handleIPCMessage: (message, detail) ->
switch message
when 'open-locations'
needsProjectPaths = atom.project?.getPaths().length is 0
for {pathToOpen, initialLine, initialColumn} in detail
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
atom.project.addPath(pathToOpen)
unless fs.isDirectorySync(pathToOpen)
atom.workspace?.open(pathToOpen, {initialLine, initialColumn})
return
when 'update-available'
atom.updateAvailable(detail)
handleIPCCommand: (command, args...) ->
activeElement = document.activeElement
# Use the workspace element view if body has focus
if activeElement is document.body and workspaceElement = atom.views.getView(atom.workspace)
activeElement = workspaceElement
atom.commands.dispatch(activeElement, command, args[0])
handleIPCContextCommand: (command, args...) ->
atom.commands.dispatch(atom.contextMenu.activeElement, command, args)
handleWindowFocus: ->
document.body.classList.remove('is-blurred')
handleWindowBlur: ->
document.body.classList.add('is-blurred')
atom.storeDefaultWindowDimensions()
handleWindowBeforeunload: =>
confirmed = atom.workspace?.confirmClose(windowCloseRequested: true)
atom.hide() if confirmed and not @reloadRequested and atom.getCurrentWindow().isWebViewFocused()
@reloadRequested = false
atom.storeDefaultWindowDimensions()
atom.storeWindowDimensions()
if confirmed
atom.unloadEditorWindow()
else
ipc.send('cancel-window-close')
confirmed
handleWindowUnload: ->
atom.removeEditorWindow()
handleWindowToggleFullScreen: ->
atom.toggleFullScreen()
handleWindowClose: ->
atom.close()
handleWindowReload: ->
@reloadRequested = true
atom.reload()
handleWindowToggleDevTools: ->
atom.toggleDevTools()
handleWindowToggleMenuBar: ->
atom.config.set('core.autoHideMenuBar', not atom.config.get('core.autoHideMenuBar'))
if atom.config.get('core.autoHideMenuBar')
detail = "To toggle, press the Alt key or execute the window:toggle-menu-bar command"
atom.notifications.addInfo('Menu bar hidden', {detail})
handleLinkClick: (event) ->
event.preventDefault()
location = event.currentTarget?.getAttribute('href')
if location and location[0] isnt '#' and /^https?:\/\//.test(location)
shell.openExternal(location)
handleFormSubmit: (event) ->
# Prevent form submits from changing the current window's URL
event.preventDefault()
handleDocumentContextmenu: (event) ->
event.preventDefault()
atom.contextMenu.showForEvent(event)

View File

@@ -3,8 +3,6 @@ path = require 'path'
{Disposable, CompositeDisposable} = require 'event-kit'
Grim = require 'grim'
scrollbarStyle = require 'scrollbar-style'
{callAttachHooks} = require 'space-pen'
WorkspaceView = null
module.exports =
class WorkspaceElement extends HTMLElement
@@ -15,10 +13,8 @@ class WorkspaceElement extends HTMLElement
@initializeContent()
@observeScrollbarStyle()
@observeTextEditorFontConfig()
@createSpacePenShim() if Grim.includeDeprecatedAPIs
attachedCallback: ->
callAttachHooks(this) if Grim.includeDeprecatedAPIs
@focus()
detachedCallback: ->
@@ -64,10 +60,6 @@ class WorkspaceElement extends HTMLElement
"""
atom.styles.addStyleSheet(styleSheetSource, sourcePath: 'global-text-editor-styles')
createSpacePenShim: ->
WorkspaceView ?= require './workspace-view'
@__spacePenView = new WorkspaceView(this)
initialize: (@model) ->
@paneContainer = atom.views.getView(@model.paneContainer)
@verticalAxis.appendChild(@paneContainer)
@@ -88,7 +80,6 @@ class WorkspaceElement extends HTMLElement
@appendChild(@panelContainers.modal)
@__spacePenView.setModel(@model) if Grim.includeDeprecatedAPIs
this
getModel: -> @model
@@ -117,7 +108,6 @@ atom.commands.add 'atom-workspace',
'window:reset-font-size': -> @getModel().resetFontSize()
'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')

View File

@@ -1,340 +0,0 @@
ipc = require 'ipc'
path = require 'path'
Q = require 'q'
_ = require 'underscore-plus'
Delegator = require 'delegato'
{deprecate, logDeprecationWarnings} = require 'grim'
{$, $$, View} = require './space-pen-extensions'
fs = require 'fs-plus'
Workspace = require './workspace'
PaneView = require './pane-view'
PaneContainerView = require './pane-container-view'
TextEditor = require './text-editor'
# Deprecated: The top-level view for the entire window. An instance of this class is
# available via the `atom.workspaceView` global.
#
# It is backed by a model object, an instance of {Workspace}, which is available
# via the `atom.workspace` global or {::getModel}. You should prefer to interact
# with the model object when possible, but it won't always be possible with the
# current API.
#
# ## Adding Perimeter Panels
#
# Use the following methods if possible to attach panels to the perimeter of the
# workspace rather than manipulating the DOM directly to better insulate you to
# changes in the workspace markup:
#
# * {::prependToTop}
# * {::appendToTop}
# * {::prependToBottom}
# * {::appendToBottom}
# * {::prependToLeft}
# * {::appendToLeft}
# * {::prependToRight}
# * {::appendToRight}
#
# ## Requiring in package specs
#
# If you need a `WorkspaceView` instance to test your package, require it via
# the built-in `atom` module.
#
# ```coffee
# {WorkspaceView} = require 'atom'
# ```
#
# You can assign it to the `atom.workspaceView` global in the spec or just use
# it as a local, depending on what you're trying to accomplish. Building the
# `WorkspaceView` is currently expensive, so you should try build a {Workspace}
# instead if possible.
module.exports =
class WorkspaceView extends View
Delegator.includeInto(this)
@delegatesProperty 'fullScreen', 'destroyedItemURIs', toProperty: 'model'
@delegatesMethods 'open', 'openSync',
'saveActivePaneItem', 'saveActivePaneItemAs', 'saveAll', 'destroyActivePaneItem',
'destroyActivePane', 'increaseFontSize', 'decreaseFontSize', toProperty: 'model'
constructor: (@element) ->
unless @element?
return atom.views.getView(atom.workspace).__spacePenView
super
@deprecateViewEvents()
@attachedEditorViews = new WeakSet
setModel: (@model) ->
@horizontal = @find('atom-workspace-axis.horizontal')
@vertical = @find('atom-workspace-axis.vertical')
@panes = @find('atom-pane-container').view()
@subscribe @model.onDidOpen => @trigger 'uri-opened'
beforeRemove: ->
@model?.destroy()
###
Section: Accessing the Workspace Model
###
# Essential: Get the underlying model object.
#
# Returns a {Workspace}.
getModel: -> @model
###
Section: Accessing Views
###
# Essential: Register a function to be called for every current and future
# editor view in the workspace (only includes {TextEditorView}s that are pane
# items).
#
# * `callback` A {Function} with an {TextEditorView} as its only argument.
# * `editorView` {TextEditorView}
#
# Returns a subscription object with an `.off` method that you can call to
# unregister the callback.
eachEditorView: (callback) ->
for editorView in @getEditorViews()
@attachedEditorViews.add(editorView)
callback(editorView)
attachedCallback = (e, editorView) =>
unless @attachedEditorViews.has(editorView)
@attachedEditorViews.add(editorView)
callback(editorView) unless editorView.mini
@on('editor:attached', attachedCallback)
off: => @off('editor:attached', attachedCallback)
# Essential: Register a function to be called for every current and future
# pane view in the workspace.
#
# * `callback` A {Function} with a {PaneView} as its only argument.
# * `paneView` {PaneView}
#
# Returns a subscription object with an `.off` method that you can call to
# unregister the callback.
eachPaneView: (callback) ->
@panes.eachPaneView(callback)
# Essential: Get all existing pane views.
#
# Prefer {Workspace::getPanes} if you don't need access to the view objects.
# Also consider using {::eachPaneView} if you want to register a callback for
# all current and *future* pane views.
#
# Returns an Array of all open {PaneView}s.
getPaneViews: ->
@panes.getPaneViews()
# Essential: Get the active pane view.
#
# Prefer {Workspace::getActivePane} if you don't actually need access to the
# view.
#
# Returns a {PaneView}.
getActivePaneView: ->
@panes.getActivePaneView()
# Essential: Get the view associated with the active pane item.
#
# Returns a view.
getActiveView: ->
@panes.getActiveView()
###
Section: Adding elements to the workspace
###
prependToTop: (element) ->
deprecate 'Please use Workspace::addTopPanel() instead'
@vertical.prepend(element)
appendToTop: (element) ->
deprecate 'Please use Workspace::addTopPanel() instead'
@panes.before(element)
prependToBottom: (element) ->
deprecate 'Please use Workspace::addBottomPanel() instead'
@panes.after(element)
appendToBottom: (element) ->
deprecate 'Please use Workspace::addBottomPanel() instead'
@vertical.append(element)
prependToLeft: (element) ->
deprecate 'Please use Workspace::addLeftPanel() instead'
@horizontal.prepend(element)
appendToLeft: (element) ->
deprecate 'Please use Workspace::addLeftPanel() instead'
@vertical.before(element)
prependToRight: (element) ->
deprecate 'Please use Workspace::addRightPanel() instead'
@vertical.after(element)
appendToRight: (element) ->
deprecate 'Please use Workspace::addRightPanel() instead'
@horizontal.append(element)
###
Section: Focusing pane views
###
# Focus the previous pane by id.
focusPreviousPaneView: -> @model.activatePreviousPane()
# Focus the next pane by id.
focusNextPaneView: -> @model.activateNextPane()
# Focus the pane directly above the active pane.
focusPaneViewAbove: -> @panes.focusPaneViewAbove()
# Focus the pane directly below the active pane.
focusPaneViewBelow: -> @panes.focusPaneViewBelow()
# Focus the pane directly to the left of the active pane.
focusPaneViewOnLeft: -> @panes.focusPaneViewOnLeft()
# Focus the pane directly to the right of the active pane.
focusPaneViewOnRight: -> @panes.focusPaneViewOnRight()
###
Section: Private
###
# Prompts to save all unsaved items
confirmClose: ->
@model.confirmClose()
# Get all editor views.
#
# You should prefer {Workspace::getEditors} unless you absolutely need access
# to the view objects. Also consider using {::eachEditorView}, which will call
# a callback for all current and *future* editor views.
#
# Returns an {Array} of {TextEditorView}s.
getEditorViews: ->
for editorElement in @panes.element.querySelectorAll('atom-pane > .item-views > atom-text-editor')
$(editorElement).view()
###
Section: Deprecated
###
deprecateViewEvents: ->
originalWorkspaceViewOn = @on
@on = (eventName) =>
switch eventName
when 'beep'
deprecate('Use Atom::onDidBeep instead')
when 'cursor:moved'
deprecate('Use TextEditor::onDidChangeCursorPosition instead')
when 'editor:attached'
deprecate('Use Workspace::onDidAddTextEditor instead')
when 'editor:detached'
deprecate('Use TextEditor::onDidDestroy instead')
when 'editor:will-be-removed'
deprecate('Use TextEditor::onDidDestroy instead')
when 'pane:active-item-changed'
deprecate('Use Pane::onDidChangeActiveItem instead')
when 'pane:active-item-modified-status-changed'
deprecate('Use Pane::onDidChangeActiveItem and call onDidChangeModified on the active item instead')
when 'pane:active-item-title-changed'
deprecate('Use Pane::onDidChangeActiveItem and call onDidChangeTitle on the active item instead')
when 'pane:attached'
deprecate('Use Workspace::onDidAddPane instead')
when 'pane:became-active'
deprecate('Use Pane::onDidActivate instead')
when 'pane:became-inactive'
deprecate('Use Pane::onDidChangeActive instead')
when 'pane:item-added'
deprecate('Use Pane::onDidAddItem instead')
when 'pane:item-moved'
deprecate('Use Pane::onDidMoveItem instead')
when 'pane:item-removed'
deprecate('Use Pane::onDidRemoveItem instead')
when 'pane:removed'
deprecate('Use Pane::onDidDestroy instead')
when 'pane-container:active-pane-item-changed'
deprecate('Use Workspace::onDidChangeActivePaneItem instead')
when 'selection:changed'
deprecate('Use TextEditor::onDidChangeSelectionRange instead')
when 'uri-opened'
deprecate('Use Workspace::onDidOpen instead')
originalWorkspaceViewOn.apply(this, arguments)
TextEditorView = require './text-editor-view'
originalEditorViewOn = TextEditorView::on
TextEditorView::on = (eventName) ->
switch eventName
when 'cursor:moved'
deprecate('Use TextEditor::onDidChangeCursorPosition instead')
when 'editor:attached'
deprecate('Use TextEditor::onDidAddTextEditor instead')
when 'editor:detached'
deprecate('Use TextEditor::onDidDestroy instead')
when 'editor:will-be-removed'
deprecate('Use TextEditor::onDidDestroy instead')
when 'selection:changed'
deprecate('Use TextEditor::onDidChangeSelectionRange instead')
originalEditorViewOn.apply(this, arguments)
originalPaneViewOn = PaneView::on
PaneView::on = (eventName) ->
switch eventName
when 'cursor:moved'
deprecate('Use TextEditor::onDidChangeCursorPosition instead')
when 'editor:attached'
deprecate('Use TextEditor::onDidAddTextEditor instead')
when 'editor:detached'
deprecate('Use TextEditor::onDidDestroy instead')
when 'editor:will-be-removed'
deprecate('Use TextEditor::onDidDestroy instead')
when 'pane:active-item-changed'
deprecate('Use Pane::onDidChangeActiveItem instead')
when 'pane:active-item-modified-status-changed'
deprecate('Use Pane::onDidChangeActiveItem and call onDidChangeModified on the active item instead')
when 'pane:active-item-title-changed'
deprecate('Use Pane::onDidChangeActiveItem and call onDidChangeTitle on the active item instead')
when 'pane:attached'
deprecate('Use Workspace::onDidAddPane instead')
when 'pane:became-active'
deprecate('Use Pane::onDidActivate instead')
when 'pane:became-inactive'
deprecate('Use Pane::onDidChangeActive instead')
when 'pane:item-added'
deprecate('Use Pane::onDidAddItem instead')
when 'pane:item-moved'
deprecate('Use Pane::onDidMoveItem instead')
when 'pane:item-removed'
deprecate('Use Pane::onDidRemoveItem instead')
when 'pane:removed'
deprecate('Use Pane::onDidDestroy instead')
when 'selection:changed'
deprecate('Use TextEditor::onDidChangeSelectionRange instead')
originalPaneViewOn.apply(this, arguments)
# Deprecated
eachPane: (callback) ->
deprecate("Use WorkspaceView::eachPaneView instead")
@eachPaneView(callback)
# Deprecated
getPanes: ->
deprecate("Use WorkspaceView::getPaneViews instead")
@getPaneViews()
# Deprecated
getActivePane: ->
deprecate("Use WorkspaceView::getActivePaneView instead")
@getActivePaneView()
# Deprecated: Call {Workspace::getActivePaneItem} instead.
getActivePaneItem: ->
deprecate("Use Workspace::getActivePaneItem instead")
@model.getActivePaneItem()

View File

@@ -2,7 +2,6 @@
_ = require 'underscore-plus'
path = require 'path'
{join} = path
Q = require 'q'
Serializable = require 'serializable'
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
Grim = require 'grim'
@@ -448,17 +447,17 @@ class Workspace extends Model
catch error
switch error.code
when 'CANCELLED'
return Q()
return Promise.resolve()
when 'EACCES'
atom.notifications.addWarning("Permission denied '#{error.path}'")
return Q()
return Promise.resolve()
when 'EPERM', 'EBUSY', 'ENXIO', 'EIO', 'ENOTCONN', 'UNKNOWN', 'ECONNRESET', 'EINVAL'
atom.notifications.addWarning("Unable to open '#{error.path ? uri}'", detail: error.message)
return Q()
return Promise.resolve()
else
throw error
Q(item)
Promise.resolve(item)
.then (item) =>
if not pane
pane = new Pane(items: [item])
@@ -488,7 +487,7 @@ class Workspace extends Model
if uri = @destroyedItemURIs.pop()
@open(uri)
else
Q()
Promise.resolve()
# Public: Register an opener for a uri.
#
@@ -506,6 +505,15 @@ class Workspace extends Model
#
# Returns a {Disposable} on which `.dispose()` can be called to remove the
# opener.
#
# Note that the opener will be called if and only if the URI is not already open
# in the current pane. The searchAllPanes flag expands the search from the
# current pane to all panes. If you wish to open a view of a different type for
# a file that is already open, consider changing the protocol of the URI. For
# example, perhaps you wish to preview a rendered version of the file `/foo/bar/baz.quux`
# that is already open in a text editor view. You could signal this by calling
# {Workspace::open} on the URI `quux-preview://foo/bar/baz.quux`. Then your opener
# can check the protocol for quux-preview and only handle those URIs that match.
addOpener: (opener) ->
if includeDeprecatedAPIs
packageName = @getCallingPackageName()
@@ -895,7 +903,12 @@ class Workspace extends Model
resolve('cancelled')
else
resolve(null)
searchPromise.then(onSuccess, reject)
onFailure = ->
promise.cancel() for promise in allSearches
reject()
searchPromise.then(onSuccess, onFailure)
cancellablePromise.cancel = ->
isCancelled = true
# Note that cancelling all of the members of allSearches will cause all of the searches
@@ -920,36 +933,33 @@ class Workspace extends Model
#
# Returns a `Promise`.
replace: (regex, replacementText, filePaths, iterator) ->
deferred = Q.defer()
new Promise (resolve, reject) ->
openPaths = (buffer.getPath() for buffer in atom.project.getBuffers())
outOfProcessPaths = _.difference(filePaths, openPaths)
openPaths = (buffer.getPath() for buffer in atom.project.getBuffers())
outOfProcessPaths = _.difference(filePaths, openPaths)
inProcessFinished = not openPaths.length
outOfProcessFinished = not outOfProcessPaths.length
checkFinished = ->
resolve() if outOfProcessFinished and inProcessFinished
inProcessFinished = not openPaths.length
outOfProcessFinished = not outOfProcessPaths.length
checkFinished = ->
deferred.resolve() if outOfProcessFinished and inProcessFinished
unless outOfProcessFinished.length
flags = 'g'
flags += 'i' if regex.ignoreCase
unless outOfProcessFinished.length
flags = 'g'
flags += 'i' if regex.ignoreCase
task = Task.once require.resolve('./replace-handler'), outOfProcessPaths, regex.source, flags, replacementText, ->
outOfProcessFinished = true
checkFinished()
task = Task.once require.resolve('./replace-handler'), outOfProcessPaths, regex.source, flags, replacementText, ->
outOfProcessFinished = true
checkFinished()
task.on 'replace:path-replaced', iterator
task.on 'replace:file-error', (error) -> iterator(null, error)
task.on 'replace:path-replaced', iterator
task.on 'replace:file-error', (error) -> iterator(null, error)
for buffer in atom.project.getBuffers()
continue unless buffer.getPath() in filePaths
replacements = buffer.replace(regex, replacementText, iterator)
iterator({filePath: buffer.getPath(), replacements}) if replacements
for buffer in atom.project.getBuffers()
continue unless buffer.getPath() in filePaths
replacements = buffer.replace(regex, replacementText, iterator)
iterator({filePath: buffer.getPath(), replacements}) if replacements
inProcessFinished = true
checkFinished()
deferred.promise
inProcessFinished = true
checkFinished()
if includeDeprecatedAPIs
Workspace.properties