Merge branch 'master' into pr/10930

This commit is contained in:
joshaber
2016-03-14 13:34:03 -04:00
62 changed files with 1397 additions and 399 deletions

View File

@@ -166,8 +166,7 @@ class ApplicationDelegate
onDidOpenLocations: (callback) ->
outerCallback = (event, message, detail) ->
if message is 'open-locations'
callback(detail)
callback(detail) if message is 'open-locations'
ipcRenderer.on('message', outerCallback)
new Disposable ->
@@ -175,8 +174,38 @@ class ApplicationDelegate
onUpdateAvailable: (callback) ->
outerCallback = (event, message, detail) ->
if message is 'update-available'
callback(detail)
# TODO: Yes, this is strange that `onUpdateAvailable` is listening for
# `did-begin-downloading-update`. We currently have no mechanism to know
# if there is an update, so begin of downloading is a good proxy.
callback(detail) if message is 'did-begin-downloading-update'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onDidBeginDownloadingUpdate: (callback) ->
@onUpdateAvailable(callback)
onDidBeginCheckingForUpdate: (callback) ->
outerCallback = (event, message, detail) ->
callback(detail) if message is 'checking-for-update'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onDidCompleteDownloadingUpdate: (callback) ->
outerCallback = (event, message, detail) ->
# TODO: We could rename this event to `did-complete-downloading-update`
callback(detail) if message is 'update-available'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onUpdateNotAvailable: (callback) ->
outerCallback = (event, message, detail) ->
callback(detail) if message is 'update-not-available'
ipcRenderer.on('message', outerCallback)
new Disposable ->
@@ -206,3 +235,12 @@ class ApplicationDelegate
disablePinchToZoom: ->
webFrame.setZoomLevelLimits(1, 1)
checkForUpdate: ->
ipcRenderer.send('check-for-update')
restartAndInstallUpdate: ->
ipcRenderer.send('install-update')
getAutoUpdateManagerState: ->
ipcRenderer.sendSync('get-auto-update-manager-state')

View File

@@ -40,6 +40,8 @@ Project = require './project'
TextEditor = require './text-editor'
TextBuffer = require 'text-buffer'
Gutter = require './gutter'
TextEditorRegistry = require './text-editor-registry'
AutoUpdateManager = require './auto-update-manager'
WorkspaceElement = require './workspace-element'
PanelContainerElement = require './panel-container-element'
@@ -111,6 +113,12 @@ class AtomEnvironment extends Model
# Public: A {Workspace} instance
workspace: null
# Public: A {TextEditorRegistry} instance
textEditors: null
# Private: An {AutoUpdateManager} instance
autoUpdater: null
saveStateDebounceInterval: 1000
###
@@ -121,6 +129,7 @@ class AtomEnvironment extends Model
constructor: (params={}) ->
{@blobStore, @applicationDelegate, @window, @document, configDirPath, @enablePersistence, onlyLoadBaseStyleSheets} = params
@unloaded = false
@loadTime = null
{devMode, safeMode, resourcePath, clearWindowState} = @getLoadSettings()
@@ -183,6 +192,9 @@ class AtomEnvironment extends Model
})
@themes.workspace = @workspace
@textEditors = new TextEditorRegistry
@autoUpdater = new AutoUpdateManager({@applicationDelegate})
@config.load()
@themes.loadBaseStylesheets()
@@ -219,7 +231,8 @@ class AtomEnvironment extends Model
checkPortableHomeWritable()
attachSaveStateListeners: ->
debouncedSaveState = _.debounce((=> @saveState()), @saveStateDebounceInterval)
saveState = => @saveState({isUnloading: false}) unless @unloaded
debouncedSaveState = _.debounce(saveState, @saveStateDebounceInterval)
@document.addEventListener('mousedown', debouncedSaveState, true)
@document.addEventListener('keydown', debouncedSaveState, true)
@disposables.add new Disposable =>
@@ -254,8 +267,6 @@ class AtomEnvironment extends Model
new PaneAxisElement().initialize(model, env)
@views.addViewProvider Pane, (model, env) ->
new PaneElement().initialize(model, env)
@views.addViewProvider TextEditor, (model, env) ->
new TextEditorElement().initialize(model, env)
@views.addViewProvider(Gutter, createGutterView)
registerDefaultOpeners: ->
@@ -327,6 +338,7 @@ class AtomEnvironment extends Model
@commands.clear()
@stylesElement.remove()
@config.unobserveUserConfig()
@autoUpdater.destroy()
@uninstallWindowEventHandler()
@@ -405,6 +417,16 @@ class AtomEnvironment extends Model
getVersion: ->
@appVersion ?= @getLoadSettings().appVersion
# Returns the release channel as a {String}. Will return one of `'dev', 'beta', 'stable'`
getReleaseChannel: ->
version = @getVersion()
if version.indexOf('beta') > -1
'beta'
else if version.indexOf('dev') > -1
'dev'
else
'stable'
# Public: Returns a {Boolean} that is `true` if the current version is an official release.
isReleasedVersion: ->
not /\w{7}/.test(@getVersion()) # Check if the release is a 7-character SHA prefix
@@ -654,7 +676,7 @@ class AtomEnvironment extends Model
@document.body.appendChild(@views.getView(@workspace))
@backgroundStylesheet?.remove()
@watchProjectPath()
@watchProjectPaths()
@packages.activate()
@keymaps.loadUserKeymap()
@@ -664,9 +686,9 @@ class AtomEnvironment extends Model
@openInitialEmptyEditorIfNecessary()
serialize: ->
serialize: (options) ->
version: @constructor.version
project: @project.serialize()
project: @project.serialize(options)
workspace: @workspace.serialize()
packageStates: @packages.serialize()
grammars: {grammarOverridesByPath: @grammars.grammarOverridesByPath}
@@ -676,9 +698,11 @@ class AtomEnvironment extends Model
unloadEditorWindow: ->
return if not @project
@saveState({isUnloading: true})
@storeWindowBackground()
@packages.deactivatePackages()
@saveBlobStoreSync()
@unloaded = true
openInitialEmptyEditorIfNecessary: ->
return unless @config.get('core.openEmptyEditorOnStart')
@@ -786,7 +810,7 @@ class AtomEnvironment extends Model
@themes.load()
# Notify the browser project of the window's current project path
watchProjectPath: ->
watchProjectPaths: ->
@disposables.add @project.onDidChangePaths =>
@applicationDelegate.setRepresentedDirectoryPaths(@project.getPaths())
@@ -811,14 +835,20 @@ class AtomEnvironment extends Model
@blobStore.save()
saveState: ->
saveState: (options) ->
return Promise.resolve() unless @enablePersistence
state = @serialize()
if storageKey = @getStateKey(@project?.getPaths())
@stateStore.save(storageKey, state)
else
@applicationDelegate.setTemporaryWindowState(state)
new Promise (resolve, reject) =>
window.requestIdleCallback =>
return if not @project
state = @serialize(options)
savePromise =
if storageKey = @getStateKey(@project?.getPaths())
@stateStore.save(storageKey, state)
else
@applicationDelegate.setTemporaryWindowState(state)
savePromise.catch(reject).then(resolve)
loadState: ->
if @enablePersistence
@@ -868,6 +898,7 @@ class AtomEnvironment extends Model
detail: error.message
dismissable: true
# TODO: We should deprecate the update events here, and use `atom.autoUpdater` instead
onUpdateAvailable: (callback) ->
@emitter.on 'update-available', callback
@@ -875,7 +906,8 @@ class AtomEnvironment extends Model
@emitter.emit 'update-available', details
listenForUpdates: ->
@disposables.add(@applicationDelegate.onUpdateAvailable(@updateAvailable.bind(this)))
# listen for updates available locally (that have been successfully downloaded)
@disposables.add(@autoUpdater.onDidCompleteDownloadingUpdate(@updateAvailable.bind(this)))
setBodyPlatformClass: ->
@document.body.classList.add("platform-#{process.platform}")
@@ -897,8 +929,8 @@ class AtomEnvironment extends Model
openLocations: (locations) ->
needsProjectPaths = @project?.getPaths().length is 0
for {pathToOpen, initialLine, initialColumn} in locations
if pathToOpen? and needsProjectPaths
for {pathToOpen, initialLine, initialColumn, forceAddToWindow} in locations
if pathToOpen? and (needsProjectPaths or forceAddToWindow)
if fs.existsSync(pathToOpen)
@project.addPath(pathToOpen)
else if fs.existsSync(path.dirname(pathToOpen))

View File

@@ -0,0 +1,73 @@
'use babel'
import {Emitter, CompositeDisposable} from 'event-kit'
export default class AutoUpdateManager {
constructor ({applicationDelegate}) {
this.applicationDelegate = applicationDelegate
this.subscriptions = new CompositeDisposable()
this.emitter = new Emitter()
this.subscriptions.add(
applicationDelegate.onDidBeginCheckingForUpdate(() => {
this.emitter.emit('did-begin-checking-for-update')
}),
applicationDelegate.onDidBeginDownloadingUpdate(() => {
this.emitter.emit('did-begin-downloading-update')
}),
applicationDelegate.onDidCompleteDownloadingUpdate((details) => {
this.emitter.emit('did-complete-downloading-update', details)
}),
applicationDelegate.onUpdateNotAvailable(() => {
this.emitter.emit('update-not-available')
})
)
}
destroy () {
this.subscriptions.dispose()
this.emitter.dispose()
}
checkForUpdate () {
this.applicationDelegate.checkForUpdate()
}
restartAndInstallUpdate () {
this.applicationDelegate.restartAndInstallUpdate()
}
getState () {
return this.applicationDelegate.getAutoUpdateManagerState()
}
platformSupportsUpdates () {
return atom.getReleaseChannel() !== 'dev' && this.getState() !== 'unsupported'
}
onDidBeginCheckingForUpdate (callback) {
return this.emitter.on('did-begin-checking-for-update', callback)
}
onDidBeginDownloadingUpdate (callback) {
return this.emitter.on('did-begin-downloading-update', callback)
}
onDidCompleteDownloadingUpdate (callback) {
return this.emitter.on('did-complete-downloading-update', callback)
}
// TODO: When https://github.com/atom/electron/issues/4587 is closed, we can
// add an update-available event.
// onUpdateAvailable (callback) {
// return this.emitter.on('update-available', callback)
// }
onUpdateNotAvailable (callback) {
return this.emitter.on('update-not-available', callback)
}
getPlatform () {
return process.platform
}
}

View File

@@ -85,16 +85,16 @@ class AtomApplication
else
@loadState(options) or @openPath(options)
openWithOptions: ({pathsToOpen, executedFrom, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, profileStartup, timeout, clearWindowState}) ->
openWithOptions: ({initialPaths, pathsToOpen, executedFrom, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, profileStartup, timeout, clearWindowState, addToLastWindow}) ->
if test
@runTests({headless: true, devMode, @resourcePath, executedFrom, pathsToOpen, logFile, timeout})
else if pathsToOpen.length > 0
@openPaths({pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState})
@openPaths({initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState, addToLastWindow})
else if urlsToOpen.length > 0
@openUrl({urlToOpen, devMode, safeMode}) for urlToOpen in urlsToOpen
else
# Always open a editor window if this is the first instance of Atom.
@openPath({pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState})
@openPath({initialPaths, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState, addToLastWindow})
# Public: Removes the {AtomWindow} from the global window list.
removeWindow: (window) ->
@@ -304,6 +304,15 @@ class AtomApplication
ipcMain.on 'execute-javascript-in-dev-tools', (event, code) ->
event.sender.devToolsWebContents?.executeJavaScript(code)
ipcMain.on 'check-for-update', =>
@autoUpdateManager.check()
ipcMain.on 'get-auto-update-manager-state', (event) =>
event.returnValue = @autoUpdateManager.getState()
ipcMain.on 'execute-javascript-in-dev-tools', (event, code) ->
event.sender.devToolsWebContents?.executeJavaScript(code)
setupDockMenu: ->
if process.platform is 'darwin'
dockMenu = Menu.buildFromTemplate [
@@ -408,8 +417,9 @@ class AtomApplication
# :safeMode - Boolean to control the opened window's safe mode.
# :profileStartup - Boolean to control creating a profile of the startup time.
# :window - {AtomWindow} to open file paths in.
openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState} = {}) ->
@openPaths({pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState})
# :addToLastWindow - Boolean of whether this should be opened in last focused window.
openPath: ({initialPaths, pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow} = {}) ->
@openPaths({initialPaths, pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow})
# Public: Opens multiple paths, in existing windows if possible.
#
@@ -421,11 +431,12 @@ 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, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window, clearWindowState}={}) ->
# :addToLastWindow - Boolean of whether this should be opened in last focused window.
openPaths: ({initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window, clearWindowState, addToLastWindow}={}) ->
devMode = Boolean(devMode)
safeMode = Boolean(safeMode)
clearWindowState = Boolean(clearWindowState)
locationsToOpen = (@locationForPathToOpen(pathToOpen, executedFrom) for pathToOpen in pathsToOpen)
locationsToOpen = (@locationForPathToOpen(pathToOpen, executedFrom, addToLastWindow) for pathToOpen in pathsToOpen)
pathsToOpen = (locationToOpen.pathToOpen for locationToOpen in locationsToOpen)
unless pidToKillWhenClosed or newWindow
@@ -434,6 +445,7 @@ class AtomApplication
unless existingWindow?
if currentWindow = window ? @lastFocusedWindow
existingWindow = currentWindow if (
addToLastWindow or
currentWindow.devMode is devMode and
(
stats.every((stat) -> stat.isFile?()) or
@@ -457,7 +469,7 @@ class AtomApplication
windowInitializationScript ?= require.resolve('../initialize-application-window')
resourcePath ?= @resourcePath
windowDimensions ?= @getDimensionsForNewWindow()
openedWindow = new AtomWindow({locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState})
openedWindow = new AtomWindow({initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState})
if pidToKillWhenClosed?
@pidsToOpenWindows[pidToKillWhenClosed] = openedWindow
@@ -500,7 +512,8 @@ class AtomApplication
if (states = @storageFolder.load('application.json'))?.length > 0
for state in states
@openWithOptions(_.extend(options, {
pathsToOpen: state.initialPaths
initialPaths: state.initialPaths
pathsToOpen: state.initialPaths.filter (directoryPath) -> fs.isDirectorySync(directoryPath)
urlsToOpen: []
devMode: @devMode
safeMode: @safeMode
@@ -602,7 +615,7 @@ class AtomApplication
catch error
require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'jasmine-test-runner'))
locationForPathToOpen: (pathToOpen, executedFrom='') ->
locationForPathToOpen: (pathToOpen, executedFrom='', forceAddToWindow) ->
return {pathToOpen} unless pathToOpen
pathToOpen = pathToOpen.replace(/[:\s]+$/, '')
@@ -618,7 +631,7 @@ class AtomApplication
unless url.parse(pathToOpen).protocol?
pathToOpen = path.resolve(executedFrom, fs.normalize(pathToOpen))
{pathToOpen, initialLine, initialColumn}
{pathToOpen, initialLine, initialColumn, forceAddToWindow}
# Opens a native dialog to prompt the user for a path.
#

View File

@@ -17,7 +17,7 @@ class AtomWindow
isSpec: null
constructor: (settings={}) ->
{@resourcePath, pathToOpen, locationsToOpen, @isSpec, @headless, @safeMode, @devMode} = settings
{@resourcePath, initialPaths, pathToOpen, locationsToOpen, @isSpec, @headless, @safeMode, @devMode} = settings
locationsToOpen ?= [{pathToOpen}] if pathToOpen
locationsToOpen ?= []
@@ -47,13 +47,7 @@ class AtomWindow
loadSettings.safeMode ?= false
loadSettings.atomHome = process.env.ATOM_HOME
loadSettings.clearWindowState ?= false
# Only send to the first non-spec window created
if @constructor.includeShellLoadTime and not @isSpec
@constructor.includeShellLoadTime = false
loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime
loadSettings.initialPaths =
loadSettings.initialPaths ?=
for {pathToOpen} in locationsToOpen when pathToOpen
if fs.statSyncNoException(pathToOpen).isFile?()
path.dirname(pathToOpen)
@@ -62,6 +56,13 @@ class AtomWindow
loadSettings.initialPaths.sort()
# Only send to the first non-spec window created
if @constructor.includeShellLoadTime and not @isSpec
@constructor.includeShellLoadTime = false
loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime
@browserWindow.loadSettings = loadSettings
@browserWindow.once 'window:loaded', =>
@emit 'window:loaded'
@loaded = true

View File

@@ -39,16 +39,24 @@ class AutoUpdateManager
autoUpdater.on 'checking-for-update', =>
@setState(CheckingState)
@emitWindowEvent('checking-for-update')
autoUpdater.on 'update-not-available', =>
@setState(NoUpdateAvailableState)
@emitWindowEvent('update-not-available')
autoUpdater.on 'update-available', =>
@setState(DownladingState)
# We use sendMessage to send an event called 'update-available' in 'update-downloaded'
# once the update download is complete. This mismatch between the electron
# autoUpdater events is unfortunate but in the interest of not changing the
# one existing event handled by applicationDelegate
@emitWindowEvent('did-begin-downloading-update')
@emit('did-begin-download')
autoUpdater.on 'update-downloaded', (event, releaseNotes, @releaseVersion) =>
@setState(UpdateAvailableState)
@emitUpdateAvailableEvent(@getWindows()...)
@emitUpdateAvailableEvent()
@config.onDidChange 'core.automaticallyUpdate', ({newValue}) =>
if newValue
@@ -64,10 +72,14 @@ class AutoUpdateManager
when 'linux'
@setState(UnsupportedState)
emitUpdateAvailableEvent: (windows...) ->
emitUpdateAvailableEvent: ->
return unless @releaseVersion?
for atomWindow in windows
atomWindow.sendMessage('update-available', {@releaseVersion})
@emitWindowEvent('update-available', {@releaseVersion})
return
emitWindowEvent: (eventName, payload) ->
for atomWindow in @getWindows()
atomWindow.sendMessage(eventName, payload)
return
setState: (state) ->

View File

@@ -132,6 +132,7 @@ parseCommandLine = ->
options.string('timeout').describe('timeout', 'When in test mode, waits until the specified time (in minutes) and kills the process (exit code: 130).')
options.alias('v', 'version').boolean('v').describe('v', 'Print the version information.')
options.alias('w', 'wait').boolean('w').describe('w', 'Wait for window to be closed before returning.')
options.alias('a', 'add').boolean('a').describe('add', 'Open path as a new project in last used window.')
options.string('socket-path')
options.string('user-data-dir')
options.boolean('clear-window-state').describe('clear-window-state', 'Delete all Atom environment state.')
@@ -146,6 +147,7 @@ parseCommandLine = ->
writeFullVersion()
process.exit(0)
addToLastWindow = args['add']
executedFrom = args['executed-from']?.toString() ? process.cwd()
devMode = args['dev']
safeMode = args['safe']
@@ -183,6 +185,6 @@ parseCommandLine = ->
{resourcePath, devResourcePath, pathsToOpen, urlsToOpen, executedFrom, test,
version, pidToKillWhenClosed, devMode, safeMode, newWindow,
logFile, socketPath, userDataDir, profileStartup, timeout, setPortable,
clearWindowState}
clearWindowState, addToLastWindow}
start()

View File

@@ -11,9 +11,11 @@ exeName = path.basename(process.execPath)
if process.env.SystemRoot
system32Path = path.join(process.env.SystemRoot, 'System32')
regPath = path.join(system32Path, 'reg.exe')
powershellPath = path.join(system32Path, 'WindowsPowerShell', 'v1.0', 'powershell.exe')
setxPath = path.join(system32Path, 'setx.exe')
else
regPath = 'reg.exe'
powershellPath = 'powershell.exe'
setxPath = 'setx.exe'
# Registry keys used for context menu
@@ -44,11 +46,31 @@ spawn = (command, args, callback) ->
error?.code ?= code
error?.stdout ?= stdout
callback?(error, stdout)
# This is necessary if using Powershell 2 on Windows 7 to get the events to raise
# http://stackoverflow.com/questions/9155289/calling-powershell-from-nodejs
spawnedProcess.stdin.end()
# Spawn reg.exe and callback when it completes
spawnReg = (args, callback) ->
spawn(regPath, args, callback)
# Spawn powershell.exe and callback when it completes
spawnPowershell = (args, callback) ->
# set encoding and execute the command, capture the output, and return it via .NET's console in order to have consistent UTF-8 encoding
# http://stackoverflow.com/questions/22349139/utf-8-output-from-powershell
# to address https://github.com/atom/atom/issues/5063
args[0] = """
[Console]::OutputEncoding=[System.Text.Encoding]::UTF8
$output=#{args[0]}
[Console]::WriteLine($output)
"""
args.unshift('-command')
args.unshift('RemoteSigned')
args.unshift('-ExecutionPolicy')
args.unshift('-noprofile')
spawn(powershellPath, args, callback)
# Spawn setx.exe and callback when it completes
spawnSetx = (args, callback) ->
spawn(setxPath, args, callback)
@@ -82,46 +104,14 @@ installContextMenu = (callback) ->
installMenu backgroundKeyPath, '%V', ->
installFileHandler(callback)
isAscii = (text) ->
index = 0
while index < text.length
return false if text.charCodeAt(index) > 127
index++
true
# Get the user's PATH environment variable registry value.
getPath = (callback) ->
spawnReg ['query', environmentKeyPath, '/v', 'Path'], (error, stdout) ->
spawnPowershell ['[environment]::GetEnvironmentVariable(\'Path\',\'User\')'], (error, stdout) ->
if error?
if error.code is 1
# FIXME Don't overwrite path when reading value is disabled
# https://github.com/atom/atom/issues/5092
if stdout.indexOf('ERROR: Registry editing has been disabled by your administrator.') isnt -1
return callback(error)
return callback(error)
# The query failed so the Path does not exist yet in the registry
return callback(null, '')
else
return callback(error)
# Registry query output is in the form:
#
# HKEY_CURRENT_USER\Environment
# Path REG_SZ C:\a\folder\on\the\path;C\another\folder
#
lines = stdout.split(/[\r\n]+/).filter (line) -> line
segments = lines[lines.length - 1]?.split(' ')
if segments[1] is 'Path' and segments.length >= 3
pathEnv = segments?[3..].join(' ')
if isAscii(pathEnv)
callback(null, pathEnv)
else
# FIXME Don't corrupt non-ASCII PATH values
# https://github.com/atom/atom/issues/5063
callback(new Error('PATH contains non-ASCII values'))
else
callback(new Error('Registry query for PATH failed'))
pathOutput = stdout.replace(/^\s+|\s+$/g, '')
callback(null, pathOutput)
# Uninstall the Open with Atom explorer context menu items via the registry.
uninstallContextMenu = (callback) ->

View File

@@ -46,7 +46,7 @@ class BufferedNodeProcess extends BufferedProcess
options ?= {}
options.env ?= Object.create(process.env)
options.env['ATOM_SHELL_INTERNAL_RUN_AS_NODE'] = 1
options.env['ELECTRON_RUN_AS_NODE'] = 1
args = args?.slice() ? []
args.unshift(command)

View File

@@ -85,5 +85,5 @@ parseAlpha = (alpha) ->
numberToHexString = (number) ->
hex = number.toString(16)
hex = "0#{hex}" if number < 10
hex = "0#{hex}" if number < 16
hex

View File

@@ -319,6 +319,23 @@ ScopeDescriptor = require './scope-descriptor'
# * line breaks - `line breaks<br/>`
# * ~~strikethrough~~ - `~~strikethrough~~`
#
# #### order
#
# The settings view orders your settings alphabetically. You can override this
# ordering with the order key.
#
# ```coffee
# config:
# zSetting:
# type: 'integer'
# default: 4
# order: 1
# aSetting:
# type: 'integer'
# default: 4
# order: 2
# ```
#
# ## Best practices
#
# * Don't depend on (or write to) configuration keys outside of your keypath.

View File

@@ -596,7 +596,15 @@ export default class GitRepositoryAsync {
.then(([repo, headCommit]) => Promise.all([repo, headCommit.getTree()]))
.then(([repo, tree]) => {
const options = new Git.DiffOptions()
options.contextLines = 0
options.flags = Git.Diff.OPTION.DISABLE_PATHSPEC_MATCH
options.pathspec = this.relativize(_path, repo.workdir())
if (process.platform === 'win32') {
// Ignore eol of line differences on windows so that files checked in
// as LF don't report every line modified when the text contains CRLF
// endings.
options.flags |= Git.Diff.OPTION.IGNORE_WHITESPACE_EOL
}
return Git.Diff.treeToWorkdir(repo, tree, options)
})
.then(diff => this._getDiffLines(diff))
@@ -775,12 +783,17 @@ export default class GitRepositoryAsync {
// Get the current branch and update this.branch.
//
// Returns a {Promise} which resolves to the {String} branch name.
// Returns a {Promise} which resolves to a {boolean} indicating whether the
// branch name changed.
_refreshBranch () {
return this.getRepo()
.then(repo => repo.getCurrentBranch())
.then(ref => ref.name())
.then(branchName => this.branch = branchName)
.then(branchName => {
const changed = branchName !== this.branch
this.branch = branchName
return changed
})
}
// Refresh the cached ahead/behind count with the given branch.
@@ -788,10 +801,15 @@ export default class GitRepositoryAsync {
// * `branchName` The {String} name of the branch whose ahead/behind should be
// used for the refresh.
//
// Returns a {Promise} which will resolve to {null}.
// Returns a {Promise} which will resolve to a {boolean} indicating whether
// the ahead/behind count changed.
_refreshAheadBehindCount (branchName) {
return this.getAheadBehindCount(branchName)
.then(counts => this.upstream = counts)
.then(counts => {
const changed = !_.isEqual(counts, this.upstream)
this.upstream = counts
return changed
})
}
// Get the status for this repository.
@@ -897,15 +915,15 @@ export default class GitRepositoryAsync {
// Refresh the cached status.
//
// Returns a {Promise} which will resolve to {null}.
// Returns a {Promise} which will resolve to a {boolean} indicating whether
// any statuses changed.
_refreshStatus () {
return Promise.all([this._getRepositoryStatus(), this._getSubmoduleStatuses()])
.then(([repositoryStatus, submoduleStatus]) => {
const statusesByPath = _.extend({}, repositoryStatus, submoduleStatus)
if (!_.isEqual(this.pathStatusCache, statusesByPath) && this.emitter != null) {
this.emitter.emit('did-change-statuses')
}
const changed = !_.isEqual(this.pathStatusCache, statusesByPath)
this.pathStatusCache = statusesByPath
return changed
})
}
@@ -915,11 +933,17 @@ export default class GitRepositoryAsync {
refreshStatus () {
const status = this._refreshStatus()
const branch = this._refreshBranch()
const aheadBehind = branch.then(branchName => this._refreshAheadBehindCount(branchName))
const aheadBehind = branch.then(() => this._refreshAheadBehindCount(this.branch))
this._refreshingPromise = this._refreshingPromise.then(_ => {
return Promise.all([status, branch, aheadBehind])
.then(_ => null)
.then(([statusChanged, branchChanged, aheadBehindChanged]) => {
if (this.emitter && (statusChanged || branchChanged || aheadBehindChanged)) {
this.emitter.emit('did-change-statuses')
}
return null
})
// Because all these refresh steps happen asynchronously, it's entirely
// possible the repository was destroyed while we were working. In which
// case we should just swallow the error.

View File

@@ -147,13 +147,11 @@ class LanguageMode
if bufferRow > 0
for currentRow in [bufferRow-1..0] by -1
break if @buffer.isRowBlank(currentRow)
break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
startRow = currentRow
if bufferRow < @buffer.getLastRow()
for currentRow in [bufferRow+1..@buffer.getLastRow()] by 1
break if @buffer.isRowBlank(currentRow)
break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
endRow = currentRow

View File

@@ -128,8 +128,12 @@ class PackageManager
# Public: Get the path to the apm command.
#
# Uses the value of the `core.apmPath` config setting if it exists.
#
# Return a {String} file path to apm.
getApmPath: ->
configPath = atom.config.get('core.apmPath')
return configPath if configPath
return @apmPath if @apmPath?
commandName = 'apm'
@@ -541,11 +545,12 @@ class PackageManager
unless typeof metadata.name is 'string' and metadata.name.length > 0
metadata.name = packageName
if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string'
metadata.repository.url = metadata.repository.url.replace(/(^git\+)|(\.git$)/g, '')
metadata
normalizePackageMetadata: (metadata) ->
unless metadata?._id
normalizePackageData ?= require 'normalize-package-data'
normalizePackageData(metadata)
if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string'
metadata.repository.url = metadata.repository.url.replace(/^git\+/, '')

View File

@@ -1,5 +1,6 @@
Grim = require 'grim'
{find, compact, extend, last} = require 'underscore-plus'
{Emitter} = require 'event-kit'
{CompositeDisposable, Emitter} = require 'event-kit'
Model = require './model'
PaneAxis = require './pane-axis'
TextEditor = require './text-editor'
@@ -8,6 +9,11 @@ TextEditor = require './text-editor'
# Panes can contain multiple items, one of which is *active* at a given time.
# The view corresponding to the active item is displayed in the interface. In
# the default configuration, tabs are also displayed for each item.
#
# Each pane may also contain one *pending* item. When a pending item is added
# to a pane, it will replace the currently pending item, if any, instead of
# simply being added. In the default configuration, the text in the tab for
# pending items is shown in italics.
module.exports =
class Pane extends Model
container: undefined
@@ -15,7 +21,7 @@ class Pane extends Model
focused: false
@deserialize: (state, {deserializers, applicationDelegate, config, notifications}) ->
{items, activeItemURI, activeItemUri} = state
{items, itemStackIndices, activeItemURI, activeItemUri} = state
activeItemURI ?= activeItemUri
state.items = compact(items.map (itemState) -> deserializers.deserialize(itemState))
state.activeItem = find state.items, (item) ->
@@ -37,20 +43,25 @@ class Pane extends Model
} = params
@emitter = new Emitter
@itemSubscriptions = new WeakMap
@subscriptionsPerItem = new WeakMap
@items = []
@itemStack = []
@addItems(compact(params?.items ? []))
@setActiveItem(@items[0]) unless @getActiveItem()?
@addItemsToStack(params?.itemStackIndices ? [])
@setFlexScale(params?.flexScale ? 1)
serialize: ->
if typeof @activeItem?.getURI is 'function'
activeItemURI = @activeItem.getURI()
itemsToBeSerialized = compact(@items.map((item) -> item if typeof item.serialize is 'function'))
itemStackIndices = (itemsToBeSerialized.indexOf(item) for item in @itemStack when typeof item.serialize is 'function')
deserializer: 'Pane'
id: @id
items: compact(@items.map((item) -> item.serialize?()))
items: itemsToBeSerialized.map((item) -> item.serialize())
itemStackIndices: itemStackIndices
activeItemURI: activeItemURI
focused: @focused
flexScale: @flexScale
@@ -260,8 +271,8 @@ class Pane extends Model
getPanes: -> [this]
unsubscribeFromItem: (item) ->
@itemSubscriptions.get(item)?.dispose()
@itemSubscriptions.delete(item)
@subscriptionsPerItem.get(item)?.dispose()
@subscriptionsPerItem.delete(item)
###
Section: Items
@@ -278,12 +289,30 @@ class Pane extends Model
# Returns a pane item.
getActiveItem: -> @activeItem
setActiveItem: (activeItem) ->
setActiveItem: (activeItem, options) ->
{modifyStack} = options if options?
unless activeItem is @activeItem
@addItemToStack(activeItem) unless modifyStack is false
@activeItem = activeItem
@emitter.emit 'did-change-active-item', @activeItem
@activeItem
# Build the itemStack after deserializing
addItemsToStack: (itemStackIndices) ->
if @items.length > 0
if itemStackIndices.length is 0 or itemStackIndices.length isnt @items.length or itemStackIndices.indexOf(-1) >= 0
itemStackIndices = (i for i in [0..@items.length-1])
for itemIndex in itemStackIndices
@addItemToStack(@items[itemIndex])
return
# Add item (or move item) to the end of the itemStack
addItemToStack: (newItem) ->
return unless newItem?
index = @itemStack.indexOf(newItem)
@itemStack.splice(index, 1) unless index is -1
@itemStack.push(newItem)
# Return an {TextEditor} if the pane item is an {TextEditor}, or null otherwise.
getActiveEditor: ->
@activeItem if @activeItem instanceof TextEditor
@@ -296,6 +325,29 @@ class Pane extends Model
itemAtIndex: (index) ->
@items[index]
# Makes the next item in the itemStack active.
activateNextRecentlyUsedItem: ->
if @items.length > 1
@itemStackIndex = @itemStack.length - 1 unless @itemStackIndex?
@itemStackIndex = @itemStack.length if @itemStackIndex is 0
@itemStackIndex = @itemStackIndex - 1
nextRecentlyUsedItem = @itemStack[@itemStackIndex]
@setActiveItem(nextRecentlyUsedItem, modifyStack: false)
# Makes the previous item in the itemStack active.
activatePreviousRecentlyUsedItem: ->
if @items.length > 1
if @itemStackIndex + 1 is @itemStack.length or not @itemStackIndex?
@itemStackIndex = -1
@itemStackIndex = @itemStackIndex + 1
previousRecentlyUsedItem = @itemStack[@itemStackIndex]
@setActiveItem(previousRecentlyUsedItem, modifyStack: false)
# Moves the active item to the end of the itemStack once the ctrl key is lifted
moveActiveItemToTopOfStack: ->
delete @itemStackIndex
@addItemToStack(@activeItem)
# Public: Makes the next item active.
activateNextItem: ->
index = @getActiveItemIndex()
@@ -342,43 +394,81 @@ class Pane extends Model
# Public: Make the given item *active*, causing it to be displayed by
# the pane's view.
activateItem: (item) ->
#
# * `options` (optional) {Object}
# * `pending` (optional) {Boolean} indicating that the item should be added
# in a pending state if it does not yet exist in the pane. Existing pending
# items in a pane are replaced with new pending items when they are opened.
activateItem: (item, options={}) ->
if item?
if @activeItem?.isPending?()
if @getPendingItem() is @activeItem
index = @getActiveItemIndex()
else
index = @getActiveItemIndex() + 1
@addItem(item, index, false)
@addItem(item, extend({}, options, {index: index}))
@setActiveItem(item)
# Public: Add the given item to the pane.
#
# * `item` The item to add. It can be a model with an associated view or a
# view.
# * `index` (optional) {Number} indicating the index at which to add the item.
# If omitted, the item is added after the current active item.
# * `options` (optional) {Object}
# * `index` (optional) {Number} indicating the index at which to add the item.
# If omitted, the item is added after the current active item.
# * `pending` (optional) {Boolean} indicating that the item should be
# added in a pending state. Existing pending items in a pane are replaced with
# new pending items when they are opened.
#
# Returns the added item.
addItem: (item, index=@getActiveItemIndex() + 1, moved=false) ->
addItem: (item, options={}) ->
# Backward compat with old API:
# addItem(item, index=@getActiveItemIndex() + 1)
if typeof options is "number"
Grim.deprecate("Pane::addItem(item, #{options}) is deprecated in favor of Pane::addItem(item, {index: #{options}})")
options = index: options
index = options.index ? @getActiveItemIndex() + 1
moved = options.moved ? false
pending = options.pending ? false
throw new Error("Pane items must be objects. Attempted to add item #{item}.") unless item? and typeof item is 'object'
throw new Error("Adding a pane item with URI '#{item.getURI?()}' that has already been destroyed") if item.isDestroyed?()
return if item in @items
if item.isPending?()
for existingItem, i in @items
if existingItem.isPending?()
@destroyItem(existingItem)
break
if typeof item.onDidDestroy is 'function'
@itemSubscriptions.set item, item.onDidDestroy => @removeItem(item, false)
itemSubscriptions = new CompositeDisposable
itemSubscriptions.add item.onDidDestroy => @removeItem(item, false)
if typeof item.onDidTerminatePendingState is "function"
itemSubscriptions.add item.onDidTerminatePendingState =>
@clearPendingItem() if @getPendingItem() is item
itemSubscriptions.add item.onDidDestroy => @removeItem(item, false)
@subscriptionsPerItem.set item, itemSubscriptions
@items.splice(index, 0, item)
lastPendingItem = @getPendingItem()
@setPendingItem(item) if pending
@emitter.emit 'did-add-item', {item, index, moved}
@destroyItem(lastPendingItem) if lastPendingItem? and not moved
@setActiveItem(item) unless @getActiveItem()?
item
setPendingItem: (item) =>
if @pendingItem isnt item
mostRecentPendingItem = @pendingItem
@pendingItem = item
@emitter.emit 'item-did-terminate-pending-state', mostRecentPendingItem
getPendingItem: =>
@pendingItem or null
clearPendingItem: =>
@setPendingItem(null)
onItemDidTerminatePendingState: (callback) =>
@emitter.on 'item-did-terminate-pending-state', callback
# Public: Add the given items to the pane.
#
# * `items` An {Array} of items to add. Items can be views or models with
@@ -390,13 +480,14 @@ class Pane extends Model
# Returns an {Array} of added items.
addItems: (items, index=@getActiveItemIndex() + 1) ->
items = items.filter (item) => not (item in @items)
@addItem(item, index + i, false) for item, i in items
@addItem(item, {index: index + i}) for item, i in items
items
removeItem: (item, moved) ->
index = @items.indexOf(item)
return if index is -1
@pendingItem = null if @getPendingItem() is item
@removeItemFromStack(item)
@emitter.emit 'will-remove-item', {item, index, destroyed: not moved, moved}
@unsubscribeFromItem(item)
@@ -412,6 +503,14 @@ class Pane extends Model
@container?.didDestroyPaneItem({item, index, pane: this}) unless moved
@destroy() if @items.length is 0 and @config.get('core.destroyEmptyPanes')
# Remove the given item from the itemStack.
#
# * `item` The item to remove.
# * `index` {Number} indicating the index to which to remove the item from the itemStack.
removeItemFromStack: (item) ->
index = @itemStack.indexOf(item)
@itemStack.splice(index, 1) unless index is -1
# Public: Move the given item to the given index.
#
# * `item` The item to move.
@@ -430,7 +529,7 @@ class Pane extends Model
# given pane.
moveItemToPane: (item, pane, index) ->
@removeItem(item, true)
pane.addItem(item, index, true)
pane.addItem(item, {index: index, moved: true})
# Public: Destroy the active item and activate the next item.
destroyActiveItem: ->

View File

@@ -54,8 +54,9 @@ class Project extends Model
Section: Serialization
###
deserialize: (state, deserializerManager) ->
deserialize: (state) ->
state.paths = [state.path] if state.path? # backward compatibility
state.paths = state.paths.filter (directoryPath) -> fs.isDirectorySync(directoryPath)
@buffers = _.compact state.buffers.map (bufferState) ->
# Check that buffer's file path is accessible
@@ -65,15 +66,15 @@ class Project extends Model
fs.closeSync(fs.openSync(bufferState.filePath, 'r'))
catch error
return unless error.code is 'ENOENT'
deserializerManager.deserialize(bufferState)
TextBuffer.deserialize(bufferState)
@subscribeToBuffer(buffer) for buffer in @buffers
@setPaths(state.paths)
serialize: ->
serialize: (options={}) ->
deserializer: 'Project'
paths: @getPaths()
buffers: _.compact(@buffers.map (buffer) -> buffer.serialize() if buffer.isRetained())
buffers: _.compact(@buffers.map (buffer) -> buffer.serialize({markerLayers: options.isUnloading is true}) if buffer.isRetained())
###
Section: Event Subscription

View File

@@ -2,6 +2,9 @@
module.exports = ({commandRegistry, commandInstaller, config}) ->
commandRegistry.add 'atom-workspace',
'pane:show-next-recently-used-item': -> @getModel().getActivePane().activateNextRecentlyUsedItem()
'pane:show-previous-recently-used-item': -> @getModel().getActivePane().activatePreviousRecentlyUsedItem()
'pane:move-active-item-to-top-of-stack': -> @getModel().getActivePane().moveActiveItemToTopOfStack()
'pane:show-next-item': -> @getModel().getActivePane().activateNextItem()
'pane:show-previous-item': -> @getModel().getActivePane().activatePreviousItem()
'pane:show-item-1': -> @getModel().getActivePane().activateItemAtIndex(0)

View File

@@ -810,11 +810,11 @@ class Selection extends Model
@wordwise = false
@linewise = false
autoscroll: ->
autoscroll: (options) ->
if @marker.hasTail()
@editor.scrollToScreenRange(@getScreenRange(), reversed: @isReversed())
@editor.scrollToScreenRange(@getScreenRange(), Object.assign({reversed: @isReversed()}, options))
else
@cursor.autoscroll()
@cursor.autoscroll(options)
clearAutoscroll: ->

View File

@@ -24,10 +24,10 @@ class StateStore {
}
save (key, value) {
return this.dbPromise.then(db => {
if (!db) return
return new Promise((resolve, reject) => {
this.dbPromise.then(db => {
if (db == null) resolve()
return new Promise((resolve, reject) => {
var request = db.transaction(['states'], 'readwrite')
.objectStore('states')
.put({value: value, storedAt: new Date().toString()}, key)
@@ -49,7 +49,11 @@ class StateStore {
request.onsuccess = (event) => {
let result = event.target.result
resolve(result ? result.value : null)
if (result && !result.isJSON) {
resolve(result.value)
} else {
resolve(null)
}
}
request.onerror = (event) => reject(event)

View File

@@ -43,7 +43,7 @@ class TextEditorComponent
@assert domNode?, "TextEditorComponent::domNode was set to null."
@domNodeValue = domNode
constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, tileSize, @views, @themes, @config, @workspace, @assert, @grammars}) ->
constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, tileSize, @views, @themes, @config, @workspace, @assert, @grammars, scrollPastEnd}) ->
@tileSize = tileSize if tileSize?
@disposables = new CompositeDisposable
@@ -61,6 +61,7 @@ class TextEditorComponent
stoppedScrollingDelay: 200
config: @config
lineTopIndex: lineTopIndex
scrollPastEnd: scrollPastEnd
@presenter.onDidUpdateState(@requestUpdate)

View File

@@ -17,6 +17,8 @@ class TextEditorElement extends HTMLElement
focusOnAttach: false
hasTiledRendering: true
logicalDisplayBuffer: true
scrollPastEnd: true
autoHeight: true
createdCallback: ->
# Use globals when the following instance variables aren't set.
@@ -38,6 +40,9 @@ class TextEditorElement extends HTMLElement
@setAttribute('tabindex', -1)
initializeContent: (attributes) ->
unless @autoHeight
@style.height = "100%"
if @config.get('editor.useShadowDOM')
@useShadowDOM = true
@@ -86,7 +91,7 @@ class TextEditorElement extends HTMLElement
@subscriptions.add @component.onDidChangeScrollLeft =>
@emitter.emit("did-change-scroll-left", arguments...)
initialize: (model, {@views, @config, @themes, @workspace, @assert, @styles, @grammars}) ->
initialize: (model, {@views, @config, @themes, @workspace, @assert, @styles, @grammars}, @autoHeight = true, @scrollPastEnd = true) ->
throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @views?
throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @config?
throw new Error("Must pass a themes parameter when initializing TextEditorElements") unless @themes?
@@ -143,6 +148,7 @@ class TextEditorElement extends HTMLElement
workspace: @workspace
assert: @assert
grammars: @grammars
scrollPastEnd: @scrollPastEnd
)
@rootElement.appendChild(@component.getDomNode())

View File

@@ -13,7 +13,7 @@ class TextEditorPresenter
minimumReflowInterval: 200
constructor: (params) ->
{@model, @config, @lineTopIndex} = params
{@model, @config, @lineTopIndex, scrollPastEnd} = params
{@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @tileSize} = params
{@contentFrameWidth} = params
@@ -42,6 +42,8 @@ class TextEditorPresenter
@startReflowing() if @continuousReflow
@updating = false
@scrollPastEndOverride = scrollPastEnd ? true
setLinesYardstick: (@linesYardstick) ->
getLinesYardstick: -> @linesYardstick
@@ -661,7 +663,7 @@ class TextEditorPresenter
return unless @contentHeight? and @clientHeight?
contentHeight = @contentHeight
if @scrollPastEnd
if @scrollPastEnd and @scrollPastEndOverride
extraScrollHeight = @clientHeight - (@lineHeight * 3)
contentHeight += extraScrollHeight if extraScrollHeight > 0
scrollHeight = Math.max(contentHeight, @height)

View File

@@ -0,0 +1,40 @@
{Emitter, Disposable} = require 'event-kit'
# Experimental: This global registry tracks registered `TextEditors`.
#
# If you want to add functionality to a wider set of text editors than just
# those appearing within workspace panes, use `atom.textEditors.observe` to
# invoke a callback for all current and future registered text editors.
#
# If you want packages to be able to add functionality to your non-pane text
# editors (such as a search field in a custom user interface element), register
# them for observation via `atom.textEditors.add`. **Important:** When you're
# done using your editor, be sure to call `dispose` on the returned disposable
# to avoid leaking editors.
module.exports =
class TextEditorRegistry
constructor: ->
@editors = new Set
@emitter = new Emitter
# Register a `TextEditor`.
#
# * `editor` The editor to register.
#
# Returns a {Disposable} on which `.dispose()` can be called to remove the
# added editor. To avoid any memory leaks this should be called when the
# editor is destroyed.
add: (editor) ->
@editors.add(editor)
@emitter.emit 'did-add-editor', editor
new Disposable => @editors.delete(editor)
# Invoke the given callback with all the current and future registered
# `TextEditors`.
#
# * `callback` {Function} to be called with current and future text editors.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
observe: (callback) ->
@editors.forEach(callback)
@emitter.on 'did-add-editor', callback

View File

@@ -11,6 +11,7 @@ Selection = require './selection'
TextMateScopeSelector = require('first-mate').ScopeSelector
{Directory} = require "pathwatcher"
GutterContainer = require './gutter-container'
TextEditorElement = require './text-editor-element'
# Essential: This class represents all essential editing state for a single
# {TextBuffer}, including cursor and selection positions, folds, and soft wraps.
@@ -61,6 +62,10 @@ class TextEditor extends Model
suppressSelectionMerging: false
selectionFlashDuration: 500
gutterContainer: null
editorElement: null
Object.defineProperty @prototype, "element",
get: -> @getElement()
@deserialize: (state, atomEnvironment) ->
try
@@ -82,7 +87,10 @@ class TextEditor extends Model
state.project = atomEnvironment.project
state.assert = atomEnvironment.assert.bind(atomEnvironment)
state.applicationDelegate = atomEnvironment.applicationDelegate
new this(state)
editor = new this(state)
disposable = atomEnvironment.textEditors.add(editor)
editor.onDidDestroy -> disposable.dispose()
editor
constructor: (params={}) ->
super
@@ -92,7 +100,7 @@ class TextEditor extends Model
softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation,
@mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config,
@notificationManager, @packageManager, @clipboard, @viewRegistry, @grammarRegistry,
@project, @assert, @applicationDelegate, @pending
@project, @assert, @applicationDelegate, grammar, showInvisibles, @autoHeight, @scrollPastEnd
} = params
throw new Error("Must pass a config parameter when constructing TextEditors") unless @config?
@@ -111,10 +119,15 @@ class TextEditor extends Model
@cursors = []
@cursorsByMarkerId = new Map
@selections = []
@autoHeight ?= true
@scrollPastEnd ?= true
@hasTerminatedPendingState = false
showInvisibles ?= true
buffer ?= new TextBuffer
@displayBuffer ?= new DisplayBuffer({
buffer, tabLength, softWrapped, ignoreInvisibles: @mini, largeFileMode,
buffer, tabLength, softWrapped, ignoreInvisibles: @mini or not showInvisibles, largeFileMode,
@config, @assert, @grammarRegistry, @packageManager
})
@buffer = @displayBuffer.buffer
@@ -143,6 +156,9 @@ class TextEditor extends Model
priority: 0
visible: lineNumberGutterVisible
if grammar?
@setGrammar(grammar)
serialize: ->
deserializer: 'TextEditor'
id: @id
@@ -151,7 +167,6 @@ class TextEditor extends Model
firstVisibleScreenColumn: @getFirstVisibleScreenColumn()
displayBuffer: @displayBuffer.serialize()
selectionsMarkerLayerId: @selectionsMarkerLayer.id
pending: @isPending()
subscribeToBuffer: ->
@buffer.retain()
@@ -163,12 +178,18 @@ class TextEditor extends Model
@disposables.add @buffer.onDidChangeEncoding =>
@emitter.emit 'did-change-encoding', @getEncoding()
@disposables.add @buffer.onDidDestroy => @destroy()
if @pending
@disposables.add @buffer.onDidChangeModified =>
@terminatePendingState() if @buffer.isModified()
@disposables.add @buffer.onDidChangeModified =>
@terminatePendingState() if not @hasTerminatedPendingState and @buffer.isModified()
@preserveCursorPositionOnBufferReload()
terminatePendingState: ->
@emitter.emit 'did-terminate-pending-state' if not @hasTerminatedPendingState
@hasTerminatedPendingState = true
onDidTerminatePendingState: (callback) ->
@emitter.on 'did-terminate-pending-state', callback
subscribeToDisplayBuffer: ->
@disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this)
@disposables.add @displayBuffer.onDidChangeGrammar @handleGrammarChange.bind(this)
@@ -575,13 +596,6 @@ class TextEditor extends Model
getEditorWidthInChars: ->
@displayBuffer.getEditorWidthInChars()
onDidTerminatePendingState: (callback) ->
@emitter.on 'did-terminate-pending-state', callback
terminatePendingState: ->
return if not @pending
@pending = false
@emitter.emit 'did-terminate-pending-state'
###
Section: File Details
@@ -666,9 +680,6 @@ class TextEditor extends Model
# Essential: Returns {Boolean} `true` if this editor has no content.
isEmpty: -> @buffer.isEmpty()
# Returns {Boolean} `true` if this editor is pending and `false` if it is permanent.
isPending: -> Boolean(@pending)
# Copies the current file path to the native clipboard.
copyPathToClipboard: (relative = false) ->
if filePath = @getPath()
@@ -2470,6 +2481,7 @@ class TextEditor extends Model
selections = @getSelections()
if selections.length > 1
selection.destroy() for selection in selections[1...(selections.length)]
selections[0].autoscroll(center: true)
true
else
false
@@ -2924,6 +2936,7 @@ class TextEditor extends Model
# Extended: Unfold all existing folds.
unfoldAll: ->
@languageMode.unfoldAll()
@scrollToCursorPosition()
# Extended: Fold all foldable lines at the given indent level.
#
@@ -3142,6 +3155,10 @@ class TextEditor extends Model
Section: TextEditor Rendering
###
# Get the Element for the editor.
getElement: ->
@editorElement ?= new TextEditorElement().initialize(this, atom, @autoHeight, @scrollPastEnd)
# Essential: Retrieves the greyed out placeholder of a mini editor.
#
# Returns a {String}.
@@ -3215,8 +3232,8 @@ class TextEditor extends Model
# top of the visible area.
setFirstVisibleScreenRow: (screenRow, fromView) ->
unless fromView
maxScreenRow = @getLineCount() - 1
unless @config.get('editor.scrollPastEnd')
maxScreenRow = @getScreenLineCount() - 1
unless @config.get('editor.scrollPastEnd') and @scrollPastEnd
height = @displayBuffer.getHeight()
lineHeightInPixels = @displayBuffer.getLineHeightInPixels()
if height? and lineHeightInPixels?
@@ -3233,7 +3250,7 @@ class TextEditor extends Model
height = @displayBuffer.getHeight()
lineHeightInPixels = @displayBuffer.getLineHeightInPixels()
if height? and lineHeightInPixels?
Math.min(@firstVisibleScreenRow + Math.floor(height / lineHeightInPixels), @getLineCount() - 1)
Math.min(@firstVisibleScreenRow + Math.floor(height / lineHeightInPixels), @getScreenLineCount() - 1)
else
null

View File

@@ -498,7 +498,6 @@ class TokenizedLine
while iterator.next()
scopes = iterator.getScopes()
continue if scopes.length is 1
continue unless NonWhitespaceRegex.test(iterator.getText())
for scope in scopes
if CommentScopeRegex.test(scope)
@isCommentLine = true

View File

@@ -63,6 +63,8 @@ class TooltipManager
# full list of options. You can also supply the following additional options:
# * `title` A {String} or {Function} to use for the text in the tip. If
# given a function, `this` will be set to the `target` element.
# * `trigger` A {String} that's the same as Bootstrap 'click | hover | focus
# | manual', except 'manual' will show the tooltip immediately.
# * `keyBindingCommand` A {String} containing a command name. If you specify
# this option and a key binding exists that matches the command, it will
# be appended to the title or rendered alone if no title is specified.

View File

@@ -64,7 +64,9 @@ Tooltip.prototype.init = function (element, options) {
if (trigger === 'click') {
this.disposables.add(listen(this.element, 'click', this.options.selector, this.toggle.bind(this)))
} else if (trigger !== 'manual') {
} else if (trigger === 'manual') {
this.show()
} else {
var eventIn, eventOut
if (trigger === 'hover') {

View File

@@ -171,6 +171,11 @@ class ViewRegistry
if object instanceof HTMLElement
return object
if typeof object?.getElement is 'function'
element = object.getElement()
if element instanceof HTMLElement
return element
if object?.element instanceof HTMLElement
return object.element

View File

@@ -15,7 +15,8 @@ class WindowEventHandler
@addEventListener(@window, 'focus', @handleWindowFocus)
@addEventListener(@window, 'blur', @handleWindowBlur)
@addEventListener(@document, 'keydown', @handleDocumentKeydown)
@addEventListener(@document, 'keyup', @handleDocumentKeyEvent)
@addEventListener(@document, 'keydown', @handleDocumentKeyEvent)
@addEventListener(@document, 'drop', @handleDocumentDrop)
@addEventListener(@document, 'dragover', @handleDocumentDragover)
@addEventListener(@document, 'contextmenu', @handleDocumentContextmenu)
@@ -66,7 +67,7 @@ class WindowEventHandler
target.addEventListener(eventName, handler)
@subscriptions.add(new Disposable(-> target.removeEventListener(eventName, handler)))
handleDocumentKeydown: (event) =>
handleDocumentKeyEvent: (event) =>
@atomEnvironment.keymaps.handleKeyboardEvent(event)
event.stopImmediatePropagation()
@@ -142,7 +143,6 @@ class WindowEventHandler
@reloadRequested = false
@atomEnvironment.storeWindowDimensions()
@atomEnvironment.saveState()
if confirmed
@atomEnvironment.unloadEditorWindow()
else

View File

@@ -43,6 +43,12 @@ class Workspace extends Model
@defaultDirectorySearcher = new DefaultDirectorySearcher()
@consumeServices(@packageManager)
# One cannot simply .bind here since it could be used as a component with
# Etch, in which case it'd be `new`d. And when it's `new`d, `this` is always
# the newly created object.
realThis = this
@buildTextEditor = -> Workspace.prototype.buildTextEditor.apply(realThis, arguments)
@panelContainers =
top: new PanelContainer({location: 'top'})
left: new PanelContainer({location: 'left'})
@@ -403,6 +409,9 @@ class Workspace extends Model
# containing pane. Defaults to `true`.
# * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem}
# on containing pane. Defaults to `true`.
# * `pending` A {Boolean} indicating whether or not the item should be opened
# in a pending state. Existing pending items in a pane are replaced with
# new pending items when they are opened.
# * `searchAllPanes` A {Boolean}. If `true`, the workspace will attempt to
# activate an existing item for the given URI on any pane.
# If `false`, only the active pane will be searched for
@@ -477,7 +486,7 @@ class Workspace extends Model
if uri?
if item = pane.itemForURI(uri)
item.terminatePendingState?() if item.isPending?() and not options.pending
pane.clearPendingItem() if not options.pending and pane.getPendingItem() is item
item ?= opener(uri, options) for opener in @getOpeners() when not item
try
@@ -500,7 +509,7 @@ class Workspace extends Model
return item if pane.isDestroyed()
@itemOpened(item)
pane.activateItem(item) if activateItem
pane.activateItem(item, {pending: options.pending}) if activateItem
pane.activate() if activatePane
initialLine = initialColumn = 0
@@ -555,7 +564,10 @@ class Workspace extends Model
@config, @notificationManager, @packageManager, @clipboard, @viewRegistry,
@grammarRegistry, @project, @assert, @applicationDelegate
}, params)
new TextEditor(params)
editor = new TextEditor(params)
disposable = atom.textEditors.add(editor)
editor.onDidDestroy -> disposable.dispose()
editor
# Public: Asynchronously reopens the last-closed item's URI if it hasn't already been
# reopened.