Merge branch 'master' into brumm-master

This commit is contained in:
Nathan Sobo
2016-08-02 15:39:57 -06:00
60 changed files with 1249 additions and 2666 deletions

View File

@@ -230,11 +230,12 @@ class AtomEnvironment extends Model
@observeAutoHideMenuBar()
checkPortableHomeWritable = ->
checkPortableHomeWritable = =>
responseChannel = "check-portable-home-writable-response"
ipcRenderer.on responseChannel, (event, response) ->
ipcRenderer.removeAllListeners(responseChannel)
atom.notifications.addWarning("#{response.message.replace(/([\\\.+\\-_#!])/g, '\\$1')}") if not response.writable
@notifications.addWarning("#{response.message.replace(/([\\\.+\\-_#!])/g, '\\$1')}") if not response.writable
@disposables.add new Disposable -> ipcRenderer.removeAllListeners(responseChannel)
ipcRenderer.send('check-portable-home-writable', responseChannel)
checkPortableHomeWritable()

View File

@@ -1,61 +0,0 @@
crypto = require 'crypto'
clipboard = require './safe-clipboard'
# Extended: Represents the clipboard used for copying and pasting in Atom.
#
# An instance of this class is always available as the `atom.clipboard` global.
#
# ## Examples
#
# ```coffee
# atom.clipboard.write('hello')
#
# console.log(atom.clipboard.read()) # 'hello'
# ```
module.exports =
class Clipboard
constructor: ->
@reset()
reset: ->
@metadata = null
@signatureForMetadata = null
# Creates an `md5` hash of some text.
#
# * `text` A {String} to hash.
#
# Returns a hashed {String}.
md5: (text) ->
crypto.createHash('md5').update(text, 'utf8').digest('hex')
# Public: Write the given text to the clipboard.
#
# The metadata associated with the text is available by calling
# {::readWithMetadata}.
#
# * `text` The {String} to store.
# * `metadata` (optional) The additional info to associate with the text.
write: (text, metadata) ->
@signatureForMetadata = @md5(text)
@metadata = metadata
clipboard.writeText(text)
# Public: Read the text from the clipboard.
#
# Returns a {String}.
read: ->
clipboard.readText()
# Public: Read the text from the clipboard and return both the text and the
# associated metadata.
#
# Returns an {Object} with the following keys:
# * `text` The {String} clipboard text.
# * `metadata` The metadata stored by an earlier call to {::write}.
readWithMetadata: ->
text = @read()
if @signatureForMetadata is @md5(text)
{text, @metadata}
else
{text}

70
src/clipboard.js Normal file
View File

@@ -0,0 +1,70 @@
/** @babel */
import crypto from 'crypto'
import clipboard from './safe-clipboard'
// Extended: Represents the clipboard used for copying and pasting in Atom.
//
// An instance of this class is always available as the `atom.clipboard` global.
//
// ## Examples
//
// ```coffee
// atom.clipboard.write('hello')
//
// console.log(atom.clipboard.read()) # 'hello'
// ```
export default class Clipboard {
constructor () {
this.reset()
}
reset () {
this.metadata = null
this.signatureForMetadata = null
}
// Creates an `md5` hash of some text.
//
// * `text` A {String} to hash.
//
// Returns a hashed {String}.
md5 (text) {
return crypto.createHash('md5').update(text, 'utf8').digest('hex')
}
// Public: Write the given text to the clipboard.
//
// The metadata associated with the text is available by calling
// {::readWithMetadata}.
//
// * `text` The {String} to store.
// * `metadata` (optional) The additional info to associate with the text.
write (text, metadata) {
this.signatureForMetadata = this.md5(text)
this.metadata = metadata
clipboard.writeText(text)
}
// Public: Read the text from the clipboard.
//
// Returns a {String}.
read () {
return clipboard.readText()
}
// Public: Read the text from the clipboard and return both the text and the
// associated metadata.
//
// Returns an {Object} with the following keys:
// * `text` The {String} clipboard text.
// * `metadata` The metadata stored by an earlier call to {::write}.
readWithMetadata () {
let text = this.read()
if (this.signatureForMetadata === this.md5(text)) {
return {text, metadata: this.metadata}
} else {
return {text}
}
}
}

View File

@@ -1,89 +0,0 @@
_ = require 'underscore-plus'
ParsedColor = null
# Essential: A simple color class returned from {Config::get} when the value
# at the key path is of type 'color'.
module.exports =
class Color
# Essential: Parse a {String} or {Object} into a {Color}.
#
# * `value` A {String} such as `'white'`, `#ff00ff`, or
# `'rgba(255, 15, 60, .75)'` or an {Object} with `red`, `green`, `blue`,
# and `alpha` properties.
#
# Returns a {Color} or `null` if it cannot be parsed.
@parse: (value) ->
return null if _.isArray(value) or _.isFunction(value)
return null unless _.isObject(value) or _.isString(value)
ParsedColor ?= require 'color'
try
parsedColor = new ParsedColor(value)
catch error
return null
new Color(parsedColor.red(), parsedColor.green(), parsedColor.blue(), parsedColor.alpha())
constructor: (red, green, blue, alpha) ->
Object.defineProperties this,
red:
set: (newRed) -> red = parseColor(newRed)
get: -> red
enumerable: true
configurable: false
green:
set: (newGreen) -> green = parseColor(newGreen)
get: -> green
enumerable: true
configurable: false
blue:
set: (newBlue) -> blue = parseColor(newBlue)
get: -> blue
enumerable: true
configurable: false
alpha:
set: (newAlpha) -> alpha = parseAlpha(newAlpha)
get: -> alpha
enumerable: true
configurable: false
@red = red
@green = green
@blue = blue
@alpha = alpha
# Essential: Returns a {String} in the form `'#abcdef'`.
toHexString: ->
"##{numberToHexString(@red)}#{numberToHexString(@green)}#{numberToHexString(@blue)}"
# Essential: Returns a {String} in the form `'rgba(25, 50, 75, .9)'`.
toRGBAString: ->
"rgba(#{@red}, #{@green}, #{@blue}, #{@alpha})"
isEqual: (color) ->
return true if this is color
color = Color.parse(color) unless color instanceof Color
return false unless color?
color.red is @red and color.blue is @blue and color.green is @green and color.alpha is @alpha
clone: -> new Color(@red, @green, @blue, @alpha)
parseColor = (color) ->
color = parseInt(color)
color = 0 if isNaN(color)
color = Math.max(color, 0)
color = Math.min(color, 255)
color
parseAlpha = (alpha) ->
alpha = parseFloat(alpha)
alpha = 1 if isNaN(alpha)
alpha = Math.max(alpha, 0)
alpha = Math.min(alpha, 1)
alpha
numberToHexString = (number) ->
hex = number.toString(16)
hex = "0#{hex}" if number < 16
hex

134
src/color.js Normal file
View File

@@ -0,0 +1,134 @@
/** @babel */
let ParsedColor = null
// Essential: A simple color class returned from {Config::get} when the value
// at the key path is of type 'color'.
export default class Color {
// Essential: Parse a {String} or {Object} into a {Color}.
//
// * `value` A {String} such as `'white'`, `#ff00ff`, or
// `'rgba(255, 15, 60, .75)'` or an {Object} with `red`, `green`, `blue`,
// and `alpha` properties.
//
// Returns a {Color} or `null` if it cannot be parsed.
static parse (value) {
switch (typeof value) {
case 'string':
break
case 'object':
if (Array.isArray(value)) { return null }
break
default:
return null
}
if (!ParsedColor) {
ParsedColor = require('color')
}
try {
var parsedColor = new ParsedColor(value)
} catch (error) {
return null
}
return new Color(parsedColor.red(), parsedColor.green(), parsedColor.blue(), parsedColor.alpha())
}
constructor (red, green, blue, alpha) {
this.red = red
this.green = green
this.blue = blue
this.alpha = alpha
}
set red (red) {
this._red = parseColor(red)
}
set green (green) {
this._green = parseColor(green)
}
set blue (blue) {
this._blue = parseColor(blue)
}
set alpha (alpha) {
this._alpha = parseAlpha(alpha)
}
get red () {
return this._red
}
get green () {
return this._green
}
get blue () {
return this._blue
}
get alpha () {
return this._alpha
}
// Essential: Returns a {String} in the form `'#abcdef'`.
toHexString () {
return `#${numberToHexString(this.red)}${numberToHexString(this.green)}${numberToHexString(this.blue)}`
}
// Essential: Returns a {String} in the form `'rgba(25, 50, 75, .9)'`.
toRGBAString () {
return `rgba(${this.red}, ${this.green}, ${this.blue}, ${this.alpha})`
}
isEqual (color) {
if (this === color) {
return true
}
if (!(color instanceof Color)) {
color = Color.parse(color)
}
if (color == null) {
return false
}
return color.red === this.red && color.blue === this.blue && color.green === this.green && color.alpha === this.alpha
}
clone () {
return new Color(this.red, this.green, this.blue, this.alpha)
}
}
function parseColor (colorString) {
const color = parseInt(colorString, 10)
if (isNaN(color)) {
return 0
} else {
return Math.min(Math.max(color, 0), 255)
}
}
function parseAlpha (alphaString) {
const alpha = parseFloat(alphaString)
if (isNaN(alpha)) {
return 1
} else {
return Math.min(Math.max(alpha, 0), 1)
}
}
function numberToHexString (number) {
const hex = number.toString(16)
if (number < 16) {
return `0${hex}`
} else {
return hex
}
}

View File

@@ -786,9 +786,10 @@ class Config
rootSchema = properties[key]
Object.assign rootSchema, schema
@setDefaults(keyPath, @extractDefaultsFromSchema(schema))
@setScopedDefaultsFromSchema(keyPath, schema)
@resetSettingsForSchemaChange()
@transact =>
@setDefaults(keyPath, @extractDefaultsFromSchema(schema))
@setScopedDefaultsFromSchema(keyPath, schema)
@resetSettingsForSchemaChange()
load: ->
@initializeConfigDirectory()
@@ -958,9 +959,10 @@ class Config
setDefaults: (keyPath, defaults) ->
if defaults? and isPlainObject(defaults)
keys = splitKeyPath(keyPath)
for key, childValue of defaults
continue unless defaults.hasOwnProperty(key)
@setDefaults(keys.concat([key]).join('.'), childValue)
@transact =>
for key, childValue of defaults
continue unless defaults.hasOwnProperty(key)
@setDefaults(keys.concat([key]).join('.'), childValue)
else
try
defaults = @makeValueConformToSchema(keyPath, defaults)

View File

@@ -1,13 +0,0 @@
module.exports = (extra) ->
# Breakpad on Mac OS X must be running on UI and non-UI processes
# Crashpad on Windows and Linux should only be running on non-UI process
return if process.type is 'renderer' and process.platform isnt 'darwin'
{crashReporter} = require 'electron'
crashReporter.start({
productName: 'Atom',
companyName: 'GitHub',
submitURL: 'http://54.249.141.255:1127/post'
extra: extra
})

View File

@@ -0,0 +1,10 @@
module.exports = function (extra) {
const {crashReporter} = require('electron')
crashReporter.start({
productName: 'Atom',
companyName: 'GitHub',
submitURL: 'https://crashreporter.atom.io',
autoSubmit: false,
extra: extra
})
}

View File

@@ -1,68 +0,0 @@
{Disposable} = require 'event-kit'
# Extended: Manages the deserializers used for serialized state
#
# An instance of this class is always available as the `atom.deserializers`
# global.
#
# ## Examples
#
# ```coffee
# class MyPackageView extends View
# atom.deserializers.add(this)
#
# @deserialize: (state) ->
# new MyPackageView(state)
#
# constructor: (@state) ->
#
# serialize: ->
# @state
# ```
module.exports =
class DeserializerManager
constructor: (@atomEnvironment) ->
@deserializers = {}
# Public: Register the given class(es) as deserializers.
#
# * `deserializers` One or more deserializers to register. A deserializer can
# be any object with a `.name` property and a `.deserialize()` method. A
# common approach is to register a *constructor* as the deserializer for its
# instances by adding a `.deserialize()` class method. When your method is
# called, it will be passed serialized state as the first argument and the
# {Atom} environment object as the second argument, which is useful if you
# wish to avoid referencing the `atom` global.
add: (deserializers...) ->
@deserializers[deserializer.name] = deserializer for deserializer in deserializers
new Disposable =>
delete @deserializers[deserializer.name] for deserializer in deserializers
return
getDeserializerCount: ->
Object.keys(@deserializers).length
# Public: Deserialize the state and params.
#
# * `state` The state {Object} to deserialize.
deserialize: (state) ->
return unless state?
if deserializer = @get(state)
stateVersion = state.get?('version') ? state.version
return if deserializer.version? and deserializer.version isnt stateVersion
deserializer.deserialize(state, @atomEnvironment)
else
console.warn "No deserializer found for", state
# Get the deserializer for the state.
#
# * `state` The state {Object} being deserialized.
get: (state) ->
return unless state?
name = state.get?('deserializer') ? state.deserializer
@deserializers[name]
clear: ->
@deserializers = {}

100
src/deserializer-manager.js Normal file
View File

@@ -0,0 +1,100 @@
/** @babel */
import {Disposable} from 'event-kit'
// Extended: Manages the deserializers used for serialized state
//
// An instance of this class is always available as the `atom.deserializers`
// global.
//
// ## Examples
//
// ```coffee
// class MyPackageView extends View
// atom.deserializers.add(this)
//
// @deserialize: (state) ->
// new MyPackageView(state)
//
// constructor: (@state) ->
//
// serialize: ->
// @state
// ```
export default class DeserializerManager {
constructor (atomEnvironment) {
this.atomEnvironment = atomEnvironment
this.deserializers = {}
}
// Public: Register the given class(es) as deserializers.
//
// * `deserializers` One or more deserializers to register. A deserializer can
// be any object with a `.name` property and a `.deserialize()` method. A
// common approach is to register a *constructor* as the deserializer for its
// instances by adding a `.deserialize()` class method. When your method is
// called, it will be passed serialized state as the first argument and the
// {Atom} environment object as the second argument, which is useful if you
// wish to avoid referencing the `atom` global.
add (...deserializers) {
for (let i = 0; i < deserializers.length; i++) {
let deserializer = deserializers[i]
this.deserializers[deserializer.name] = deserializer
}
return new Disposable(() => {
for (let j = 0; j < deserializers.length; j++) {
let deserializer = deserializers[j]
delete this.deserializers[deserializer.name]
}
})
}
getDeserializerCount () {
return Object.keys(this.deserializers).length
}
// Public: Deserialize the state and params.
//
// * `state` The state {Object} to deserialize.
deserialize (state) {
if (state == null) {
return
}
const deserializer = this.get(state)
if (deserializer) {
let stateVersion = (
(typeof state.get === 'function') && state.get('version') ||
state.version
)
if ((deserializer.version != null) && deserializer.version !== stateVersion) {
return
}
return deserializer.deserialize(state, this.atomEnvironment)
} else {
return console.warn('No deserializer found for', state)
}
}
// Get the deserializer for the state.
//
// * `state` The state {Object} being deserialized.
get (state) {
if (state == null) {
return
}
let stateDeserializer = (
(typeof state.get === 'function') && state.get('deserializer') ||
state.deserializer
)
return this.deserializers[stateDeserializer]
}
clear () {
this.deserializers = {}
}
}

View File

@@ -1,558 +0,0 @@
'use babel'
import {Repository} from 'ohnogit'
import {CompositeDisposable, Disposable} from 'event-kit'
// For the most part, this class behaves the same as `GitRepository`, with a few
// notable differences:
// * Errors are generally propagated out to the caller instead of being
// swallowed within `GitRepositoryAsync`.
// * Methods accepting a path shouldn't be given a null path, unless it is
// specifically allowed as noted in the method's documentation.
export default class GitRepositoryAsync {
static open (path, options = {}) {
// QUESTION: Should this wrap Git.Repository and reject with a nicer message?
return new GitRepositoryAsync(path, options)
}
static get Git () {
return Repository.Git
}
// The name of the error thrown when an action is attempted on a destroyed
// repository.
static get DestroyedErrorName () {
return Repository.DestroyedErrorName
}
constructor (_path, options = {}) {
this.repo = Repository.open(_path, options)
this.subscriptions = new CompositeDisposable()
let {refreshOnWindowFocus = true} = options
if (refreshOnWindowFocus) {
const onWindowFocus = () => this.refreshStatus()
window.addEventListener('focus', onWindowFocus)
this.subscriptions.add(new Disposable(() => window.removeEventListener('focus', onWindowFocus)))
}
const {project, subscribeToBuffers} = options
this.project = project
if (this.project && subscribeToBuffers) {
this.project.getBuffers().forEach(buffer => this.subscribeToBuffer(buffer))
this.subscriptions.add(this.project.onDidAddBuffer(buffer => this.subscribeToBuffer(buffer)))
}
}
// This exists to provide backwards compatibility.
get _refreshingPromise () {
return this.repo._refreshingPromise
}
get openedPath () {
return this.repo.openedPath
}
// Public: Destroy this {GitRepositoryAsync} object.
//
// This destroys any tasks and subscriptions and releases the underlying
// libgit2 repository handle. This method is idempotent.
destroy () {
this.repo.destroy()
if (this.subscriptions) {
this.subscriptions.dispose()
this.subscriptions = null
}
}
// Event subscription
// ==================
// Public: Invoke the given callback when this GitRepositoryAsync's destroy()
// method is invoked.
//
// * `callback` {Function}
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidDestroy (callback) {
return this.repo.onDidDestroy(callback)
}
// Public: Invoke the given callback when a specific file's status has
// changed. When a file is updated, reloaded, etc, and the status changes, this
// will be fired.
//
// * `callback` {Function}
// * `event` {Object}
// * `path` {String} the old parameters the decoration used to have
// * `pathStatus` {Number} representing the status. This value can be passed to
// {::isStatusModified} or {::isStatusNew} to get more information.
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeStatus (callback) {
return this.repo.onDidChangeStatus(callback)
}
// Public: Invoke the given callback when a multiple files' statuses have
// changed. For example, on window focus, the status of all the paths in the
// repo is checked. If any of them have changed, this will be fired. Call
// {::getPathStatus(path)} to get the status for your path of choice.
//
// * `callback` {Function}
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeStatuses (callback) {
return this.repo.onDidChangeStatuses(callback)
}
// Repository details
// ==================
// Public: A {String} indicating the type of version control system used by
// this repository.
//
// Returns `"git"`.
getType () {
return 'git'
}
// Public: Returns a {Promise} which resolves to the {String} path of the
// repository.
getPath () {
return this.repo.getPath()
}
// Public: Returns a {Promise} which resolves to the {String} working
// directory path of the repository.
getWorkingDirectory (_path) {
return this.repo.getWorkingDirectory()
}
// Public: Returns a {Promise} that resolves to true if at the root, false if
// in a subfolder of the repository.
isProjectAtRoot () {
if (!this.project) return Promise.resolve(false)
if (!this.projectAtRoot) {
this.projectAtRoot = this.getWorkingDirectory()
.then(wd => this.project.relativize(wd) === '')
}
return this.projectAtRoot
}
// Public: Makes a path relative to the repository's working directory.
//
// * `path` The {String} path to relativize.
//
// Returns a {Promise} which resolves to the relative {String} path.
relativizeToWorkingDirectory (_path) {
return this.repo.relativizeToWorkingDirectory(_path)
}
// Public: Makes a path relative to the repository's working directory.
//
// * `path` The {String} path to relativize.
// * `workingDirectory` The {String} working directory path.
//
// Returns the relative {String} path.
relativize (_path, workingDirectory) {
return this.repo.relativize(_path, workingDirectory)
}
// Public: Returns a {Promise} which resolves to whether the given branch
// exists.
hasBranch (branch) {
return this.repo.hasBranch(branch)
}
// Public: Retrieves a shortened version of the HEAD reference value.
//
// This removes the leading segments of `refs/heads`, `refs/tags`, or
// `refs/remotes`. It also shortens the SHA-1 of a detached `HEAD` to 7
// characters.
//
// * `path` An optional {String} path in the repository to get this information
// for, only needed if the repository contains submodules.
//
// Returns a {Promise} which resolves to a {String}.
getShortHead (_path) {
return this.repo.getShortHead(_path)
}
// Public: Is the given path a submodule in the repository?
//
// * `path` The {String} path to check.
//
// Returns a {Promise} that resolves true if the given path is a submodule in
// the repository.
isSubmodule (_path) {
return this.repo.isSubmodule(_path)
}
// Public: Returns the number of commits behind the current branch is from the
// its upstream remote branch.
//
// * `reference` The {String} branch reference name.
// * `path` The {String} path in the repository to get this information
// for, only needed if the repository contains submodules.
//
// Returns a {Promise} which resolves to an {Object} with the following keys:
// * `ahead` The {Number} of commits ahead.
// * `behind` The {Number} of commits behind.
getAheadBehindCount (reference, _path) {
return this.repo.getAheadBehindCount(reference, _path)
}
// Public: Get the cached ahead/behind commit counts for the current branch's
// upstream branch.
//
// * `path` An optional {String} path in the repository to get this information
// for, only needed if the repository has submodules.
//
// Returns a {Promise} which resolves to an {Object} with the following keys:
// * `ahead` The {Number} of commits ahead.
// * `behind` The {Number} of commits behind.
getCachedUpstreamAheadBehindCount (_path) {
return this.repo.getCachedUpstreamAheadBehindCount(_path)
}
// Public: Returns the git configuration value specified by the key.
//
// * `path` An optional {String} path in the repository to get this information
// for, only needed if the repository has submodules.
//
// Returns a {Promise} which resolves to the {String} git configuration value
// specified by the key.
getConfigValue (key, _path) {
return this.repo.getConfigValue(key, _path)
}
// Public: Get the URL for the 'origin' remote.
//
// * `path` (optional) {String} path in the repository to get this information
// for, only needed if the repository has submodules.
//
// Returns a {Promise} which resolves to the {String} origin url of the
// repository.
getOriginURL (_path) {
return this.repo.getOriginURL(_path)
}
// Public: Returns the upstream branch for the current HEAD, or null if there
// is no upstream branch for the current HEAD.
//
// * `path` An optional {String} path in the repo to get this information for,
// only needed if the repository contains submodules.
//
// Returns a {Promise} which resolves to a {String} branch name such as
// `refs/remotes/origin/master`.
getUpstreamBranch (_path) {
return this.repo.getUpstreamBranch(_path)
}
// Public: Gets all the local and remote references.
//
// * `path` An optional {String} path in the repository to get this information
// for, only needed if the repository has submodules.
//
// Returns a {Promise} which resolves to an {Object} with the following keys:
// * `heads` An {Array} of head reference names.
// * `remotes` An {Array} of remote reference names.
// * `tags` An {Array} of tag reference names.
getReferences (_path) {
return this.repo.getReferences(_path)
}
// Public: Get the SHA for the given reference.
//
// * `reference` The {String} reference to get the target of.
// * `path` An optional {String} path in the repo to get the reference target
// for. Only needed if the repository contains submodules.
//
// Returns a {Promise} which resolves to the current {String} SHA for the
// given reference.
getReferenceTarget (reference, _path) {
return this.repo.getReferenceTarget(reference, _path)
}
// Reading Status
// ==============
// Public: Resolves true if the given path is modified.
//
// * `path` The {String} path to check.
//
// Returns a {Promise} which resolves to a {Boolean} that's true if the `path`
// is modified.
isPathModified (_path) {
return this.repo.isPathModified(_path)
}
// Public: Resolves true if the given path is new.
//
// * `path` The {String} path to check.
//
// Returns a {Promise} which resolves to a {Boolean} that's true if the `path`
// is new.
isPathNew (_path) {
return this.repo.isPathNew(_path)
}
// Public: Is the given path ignored?
//
// * `path` The {String} path to check.
//
// Returns a {Promise} which resolves to a {Boolean} that's true if the `path`
// is ignored.
isPathIgnored (_path) {
return this.repo.isPathIgnored(_path)
}
// Get the status of a directory in the repository's working directory.
//
// * `directoryPath` The {String} path to check.
//
// Returns a {Promise} resolving to a {Number} representing the status. This
// value can be passed to {::isStatusModified} or {::isStatusNew} to get more
// information.
getDirectoryStatus (directoryPath) {
return this.repo.getDirectoryStatus(directoryPath)
}
// Refresh the status bit for the given path.
//
// Note that if the status of the path has changed, this will emit a
// 'did-change-status' event.
//
// * `path` The {String} path whose status should be refreshed.
//
// Returns a {Promise} which resolves to a {Number} which is the refreshed
// status bit for the path.
refreshStatusForPath (_path) {
return this.repo.refreshStatusForPath(_path)
}
// Returns a Promise that resolves to the status bit of a given path if it has
// one, otherwise 'current'.
getPathStatus (_path) {
return this.refreshStatusForPath(_path)
}
// Public: Get the cached status for the given path.
//
// * `path` A {String} path in the repository, relative or absolute.
//
// Returns a {Promise} which resolves to a status {Number} or null if the
// path is not in the cache.
getCachedPathStatus (_path) {
return this.repo.getCachedPathStatus(_path)
}
// Public: Get the cached statuses for the repository.
//
// Returns an {Object} of {Number} statuses, keyed by {String} working
// directory-relative file names.
getCachedPathStatuses () {
return this.repo.pathStatusCache
}
// Public: Returns true if the given status indicates modification.
//
// * `statusBit` A {Number} representing the status.
//
// Returns a {Boolean} that's true if the `statusBit` indicates modification.
isStatusModified (statusBit) {
return this.repo.isStatusModified(statusBit)
}
// Public: Returns true if the given status indicates a new path.
//
// * `statusBit` A {Number} representing the status.
//
// Returns a {Boolean} that's true if the `statusBit` indicates a new path.
isStatusNew (statusBit) {
return this.repo.isStatusNew(statusBit)
}
// Public: Returns true if the given status indicates the path is staged.
//
// * `statusBit` A {Number} representing the status.
//
// Returns a {Boolean} that's true if the `statusBit` indicates the path is
// staged.
isStatusStaged (statusBit) {
return this.repo.isStatusStaged(statusBit)
}
// Public: Returns true if the given status indicates the path is ignored.
//
// * `statusBit` A {Number} representing the status.
//
// Returns a {Boolean} that's true if the `statusBit` indicates the path is
// ignored.
isStatusIgnored (statusBit) {
return this.repo.isStatusIgnored(statusBit)
}
// Public: Returns true if the given status indicates the path is deleted.
//
// * `statusBit` A {Number} representing the status.
//
// Returns a {Boolean} that's true if the `statusBit` indicates the path is
// deleted.
isStatusDeleted (statusBit) {
return this.repo.isStatusDeleted(statusBit)
}
// Retrieving Diffs
// ================
// Public: Retrieves the number of lines added and removed to a path.
//
// This compares the working directory contents of the path to the `HEAD`
// version.
//
// * `path` The {String} path to check.
//
// Returns a {Promise} which resolves to an {Object} with the following keys:
// * `added` The {Number} of added lines.
// * `deleted` The {Number} of deleted lines.
getDiffStats (_path) {
return this.repo.getDiffStats(_path)
}
// Public: Retrieves the line diffs comparing the `HEAD` version of the given
// path and the given text.
//
// * `path` The {String} path relative to the repository.
// * `text` The {String} to compare against the `HEAD` contents
//
// Returns an {Array} of hunk {Object}s with the following keys:
// * `oldStart` The line {Number} of the old hunk.
// * `newStart` The line {Number} of the new hunk.
// * `oldLines` The {Number} of lines in the old hunk.
// * `newLines` The {Number} of lines in the new hunk
getLineDiffs (_path, text) {
return this.repo.getLineDiffs(_path, text)
}
// Checking Out
// ============
// Public: Restore the contents of a path in the working directory and index
// to the version at `HEAD`.
//
// This is essentially the same as running:
//
// ```sh
// git reset HEAD -- <path>
// git checkout HEAD -- <path>
// ```
//
// * `path` The {String} path to checkout.
//
// Returns a {Promise} that resolves or rejects depending on whether the
// method was successful.
checkoutHead (_path) {
return this.repo.checkoutHead(_path)
}
// Public: Checks out a branch in your repository.
//
// * `reference` The {String} reference to checkout.
// * `create` A {Boolean} value which, if true creates the new reference if
// it doesn't exist.
//
// Returns a {Promise} that resolves if the method was successful.
checkoutReference (reference, create) {
return this.repo.checkoutReference(reference, create)
}
// Private
// =======
checkoutHeadForEditor (editor) {
const filePath = editor.getPath()
if (!filePath) {
return Promise.reject()
}
if (editor.buffer.isModified()) {
editor.buffer.reload()
}
return this.checkoutHead(filePath)
}
// Refreshes the git status.
//
// Returns a {Promise} which will resolve to {null} when refresh is complete.
refreshStatus () {
let projectPathsPromises = [Promise.resolve('')]
if (this.project) {
projectPathsPromises = this.project.getPaths()
.map(p => this.relativizeToWorkingDirectory(p))
}
return Promise.all(projectPathsPromises)
.then(paths => paths.map(p => p.length > 0 ? p + '/**' : '*'))
.then(pathspecs => this.repo.refreshStatus(pathspecs))
}
// Get the NodeGit repository for the given path.
//
// * `path` The optional {String} path within the repository. This is only
// needed if you want to get the repository for that path if it is a
// submodule.
//
// Returns a {Promise} which resolves to the {NodeGit.Repository}.
getRepo (_path) {
return this.repo.getRepo(_path)
}
// Open a new instance of the underlying {NodeGit.Repository}.
//
// By opening multiple connections to the same underlying repository, users
// can safely access the same repository concurrently.
//
// Returns the new {NodeGit.Repository}.
openRepository () {
return this.repo.openRepository()
}
// Section: Private
// ================
// Has the repository been destroyed?
//
// Returns a {Boolean}.
_isDestroyed () {
return this.repo._isDestroyed()
}
// Subscribe to events on the given buffer.
subscribeToBuffer (buffer) {
const bufferSubscriptions = new CompositeDisposable()
const refreshStatusForBuffer = () => {
const _path = buffer.getPath()
if (_path) {
this.refreshStatusForPath(_path)
}
}
bufferSubscriptions.add(
buffer.onDidSave(refreshStatusForBuffer),
buffer.onDidReload(refreshStatusForBuffer),
buffer.onDidChangePath(refreshStatusForBuffer),
buffer.onDidDestroy(() => {
bufferSubscriptions.dispose()
this.subscriptions.remove(bufferSubscriptions)
})
)
this.subscriptions.add(bufferSubscriptions)
}
}

View File

@@ -77,7 +77,7 @@ class GitRepositoryProvider
unless repo
repo = GitRepository.open(gitDirPath, {@project, @config})
return null unless repo
repo.async.onDidDestroy(=> delete @pathToRepository[gitDirPath])
repo.onDidDestroy(=> delete @pathToRepository[gitDirPath])
@pathToRepository[gitDirPath] = repo
repo.refreshIndex()
repo.refreshStatus()

View File

@@ -3,7 +3,6 @@
_ = require 'underscore-plus'
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
fs = require 'fs-plus'
GitRepositoryAsync = require './git-repository-async'
GitUtils = require 'git-utils'
Task = require './task'
@@ -76,19 +75,11 @@ class GitRepository
unless @repo?
throw new Error("No Git repository found searching path: #{path}")
asyncOptions = _.clone(options)
# GitRepository itself will handle these cases by manually calling through
# to the async repo.
asyncOptions.refreshOnWindowFocus = false
asyncOptions.subscribeToBuffers = false
@async = GitRepositoryAsync.open(path, asyncOptions)
@statuses = {}
@upstream = {ahead: 0, behind: 0}
for submodulePath, submoduleRepo of @repo.submodules
submoduleRepo.upstream = {ahead: 0, behind: 0}
@statusesByPath = {}
{@project, @config, refreshOnWindowFocus} = options
refreshOnWindowFocus ?= true
@@ -126,10 +117,6 @@ class GitRepository
@subscriptions.dispose()
@subscriptions = null
if @async?
@async.destroy()
@async = null
# Public: Returns a {Boolean} indicating if this repository has been destroyed.
isDestroyed: ->
not @repo?
@@ -322,7 +309,7 @@ class GitRepository
getDirectoryStatus: (directoryPath) ->
directoryPath = "#{@relativize(directoryPath)}/"
directoryStatus = 0
for path, status of Object.assign({}, @async.getCachedPathStatuses(), @statusesByPath)
for path, status of @statuses
directoryStatus |= status if path.indexOf(directoryPath) is 0
directoryStatus
@@ -335,24 +322,13 @@ class GitRepository
getPathStatus: (path) ->
repo = @getRepo(path)
relativePath = @relativize(path)
# This is a bit particular. If a package calls `getPathStatus` like this:
# - change the file
# - getPathStatus
# - change the file
# - getPathStatus
# We need to preserve the guarantee that each call to `getPathStatus` will
# synchronously emit 'did-change-status'. So we need to keep a cache of the
# statuses found from this call.
currentPathStatus = @getCachedRelativePathStatus(relativePath) ? 0
# Trigger events emitted on the async repo as well
@async.refreshStatusForPath(path)
currentPathStatus = @statuses[relativePath] ? 0
pathStatus = repo.getStatus(repo.relativize(path)) ? 0
pathStatus = 0 if repo.isStatusIgnored(pathStatus)
@statusesByPath[relativePath] = pathStatus
if pathStatus > 0
@statuses[relativePath] = pathStatus
else
delete @statuses[relativePath]
if currentPathStatus isnt pathStatus
@emitter.emit 'did-change-status', {path, pathStatus}
@@ -364,11 +340,7 @@ class GitRepository
#
# Returns a status {Number} or null if the path is not in the cache.
getCachedPathStatus: (path) ->
relativePath = @relativize(path)
@getCachedRelativePathStatus(relativePath)
getCachedRelativePathStatus: (relativePath) ->
@statusesByPath[relativePath] ? @async.getCachedPathStatuses()[relativePath]
@statuses[@relativize(path)]
# Public: Returns true if the given status indicates modification.
#
@@ -492,42 +464,29 @@ class GitRepository
# Refreshes the current git status in an outside process and asynchronously
# updates the relevant properties.
#
# Returns a promise that resolves when the repository has been refreshed.
refreshStatus: ->
statusesChanged = false
@handlerPath ?= require.resolve('./repository-status-handler')
# Listen for `did-change-statuses` so we know if something changed. But we
# need to wait to propagate it until after we've set the branch and cleared
# the `statusesByPath` cache. So just set a flag, and we'll emit the event
# after refresh is done.
subscription = @async.onDidChangeStatuses ->
subscription?.dispose()
subscription = null
relativeProjectPaths = @project?.getPaths()
.map (path) => @relativize(path)
.filter (path) -> path.length > 0
statusesChanged = true
@statusTask?.terminate()
new Promise (resolve) =>
@statusTask = Task.once @handlerPath, @getPath(), relativeProjectPaths, ({statuses, upstream, branch, submodules}) =>
statusesUnchanged = _.isEqual(statuses, @statuses) and
_.isEqual(upstream, @upstream) and
_.isEqual(branch, @branch) and
_.isEqual(submodules, @submodules)
asyncRefresh = @async.refreshStatus().then =>
subscription?.dispose()
subscription = null
@branch = @async?.branch
@statusesByPath = {}
if statusesChanged
@emitter.emit 'did-change-statuses'
syncRefresh = new Promise (resolve, reject) =>
@handlerPath ?= require.resolve('./repository-status-handler')
@statusTask?.terminate()
@statusTask = Task.once @handlerPath, @getPath(), ({upstream, submodules}) =>
@statuses = statuses
@upstream = upstream
@branch = branch
@submodules = submodules
for submodulePath, submoduleRepo of @getRepo().submodules
submoduleRepo.upstream = submodules[submodulePath]?.upstream ? {ahead: 0, behind: 0}
unless statusesUnchanged
@emitter.emit 'did-change-statuses'
resolve()
return Promise.all([asyncRefresh, syncRefresh])

View File

@@ -1,35 +0,0 @@
fs = require 'fs-plus'
path = require 'path'
{ipcMain} = require 'electron'
module.exports =
class AtomPortable
@getPortableAtomHomePath: ->
execDirectoryPath = path.dirname(process.execPath)
path.join(execDirectoryPath, '..', '.atom')
@setPortable: (existingAtomHome) ->
fs.copySync(existingAtomHome, @getPortableAtomHomePath())
@isPortableInstall: (platform, environmentAtomHome, defaultHome) ->
return false unless platform in ['linux', 'win32']
return false if environmentAtomHome
return false if not fs.existsSync(@getPortableAtomHomePath())
# currently checking only that the directory exists and is writable,
# probably want to do some integrity checks on contents in future
@isPortableAtomHomePathWritable(defaultHome)
@isPortableAtomHomePathWritable: (defaultHome) ->
writable = false
message = ""
try
writePermissionTestFile = path.join(@getPortableAtomHomePath(), "write.test")
fs.writeFileSync(writePermissionTestFile, "test") if not fs.existsSync(writePermissionTestFile)
fs.removeSync(writePermissionTestFile)
writable = true
catch error
message = "Failed to use portable Atom home directory (#{@getPortableAtomHomePath()}). Using the default instead (#{defaultHome}). #{error.message}"
ipcMain.on 'check-portable-home-writable', (event) ->
event.sender.send 'check-portable-home-writable-response', {writable, message}
writable

View File

@@ -0,0 +1,58 @@
const fs = require('fs-plus')
const path = require('path')
const {ipcMain} = require('electron')
module.exports = class AtomPortable {
static getPortableAtomHomePath () {
const execDirectoryPath = path.dirname(process.execPath)
return path.join(execDirectoryPath, '..', '.atom')
}
static setPortable (existingAtomHome) {
fs.copySync(existingAtomHome, this.getPortableAtomHomePath())
}
static isPortableInstall (platform, environmentAtomHome, defaultHome) {
if (!['linux', 'win32'].includes(platform)) {
return false
}
if (environmentAtomHome) {
return false
}
if (!fs.existsSync(this.getPortableAtomHomePath())) {
return false
}
// Currently checking only that the directory exists and is writable,
// probably want to do some integrity checks on contents in future.
return this.isPortableAtomHomePathWritable(defaultHome)
}
static isPortableAtomHomePathWritable (defaultHome) {
let writable = false
let message = ''
try {
const writePermissionTestFile = path.join(this.getPortableAtomHomePath(), 'write.test')
if (!fs.existsSync(writePermissionTestFile)) {
fs.writeFileSync(writePermissionTestFile, 'test')
}
fs.removeSync(writePermissionTestFile)
writable = true
} catch (error) {
message = `Failed to use portable Atom home directory (${this.getPortableAtomHomePath()}). Using the default instead (${defaultHome}). ${error.message}.`
}
ipcMain.on('check-portable-home-writable', function (event) {
event.sender.send('check-portable-home-writable-response', {
writable: writable,
message: message
})
})
return writable
}
}

View File

@@ -1,198 +0,0 @@
global.shellStartTime = Date.now()
process.on 'uncaughtException', (error={}) ->
console.log(error.message) if error.message?
console.log(error.stack) if error.stack?
{app} = require 'electron'
fs = require 'fs-plus'
path = require 'path'
temp = require 'temp'
yargs = require 'yargs'
previousConsoleLog = console.log
startCrashReporter = require('../crash-reporter-start')
console.log = require 'nslog'
start = ->
args = parseCommandLine()
args.env = process.env
setupAtomHome(args)
setupCompileCache()
if handleStartupEventWithSquirrel()
return
else if args.test and args.mainProcess
console.log = previousConsoleLog
testRunner = require(path.join(args.resourcePath, 'spec/main-process/mocha-test-runner'))
app.on 'ready', -> testRunner(args.pathsToOpen)
return
# NB: This prevents Win10 from showing dupe items in the taskbar
app.setAppUserModelId('com.squirrel.atom.atom')
addPathToOpen = (event, pathToOpen) ->
event.preventDefault()
args.pathsToOpen.push(pathToOpen)
addUrlToOpen = (event, urlToOpen) ->
event.preventDefault()
args.urlsToOpen.push(urlToOpen)
app.on 'open-file', addPathToOpen
app.on 'open-url', addUrlToOpen
app.on 'will-finish-launching', startCrashReporter
if args.userDataDir?
app.setPath('userData', args.userDataDir)
else if args.test
app.setPath('userData', temp.mkdirSync('atom-test-data'))
app.on 'ready', ->
app.removeListener 'open-file', addPathToOpen
app.removeListener 'open-url', addUrlToOpen
AtomApplication = require path.join(args.resourcePath, 'src', 'main-process', 'atom-application')
AtomApplication.open(args)
console.log("App load time: #{Date.now() - global.shellStartTime}ms") unless args.test
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)
setupAtomHome = ({setPortable}) ->
return if process.env.ATOM_HOME
atomHome = path.join(app.getPath('home'), '.atom')
AtomPortable = require './atom-portable'
if setPortable and not AtomPortable.isPortableInstall(process.platform, process.env.ATOM_HOME, atomHome)
try
AtomPortable.setPortable(atomHome)
catch error
console.log("Failed copying portable directory '#{atomHome}' to '#{AtomPortable.getPortableAtomHomePath()}'")
console.log("#{error.message} #{error.stack}")
if AtomPortable.isPortableInstall(process.platform, process.env.ATOM_HOME, atomHome)
atomHome = AtomPortable.getPortableAtomHomePath()
try
atomHome = fs.realpathSync(atomHome)
process.env.ATOM_HOME = atomHome
setupCompileCache = ->
compileCache = require('../compile-cache')
compileCache.setAtomHomeDirectory(process.env.ATOM_HOME)
writeFullVersion = ->
process.stdout.write """
Atom : #{app.getVersion()}
Electron: #{process.versions.electron}
Chrome : #{process.versions.chrome}
Node : #{process.versions.node}
"""
parseCommandLine = ->
version = app.getVersion()
options = yargs(process.argv[1..]).wrap(100)
options.usage """
Atom Editor v#{version}
Usage: atom [options] [path ...]
One or more paths to files or folders may be specified. If there is an
existing Atom window that contains all of the given folders, the paths
will be opened in that window. Otherwise, they will be opened in a new
window.
Environment Variables:
ATOM_DEV_RESOURCE_PATH The path from which Atom loads source code in dev mode.
Defaults to `~/github/atom`.
ATOM_HOME The root path for all configuration files and folders.
Defaults to `~/.atom`.
"""
# Deprecated 1.0 API preview flag
options.alias('1', 'one').boolean('1').describe('1', 'This option is no longer supported.')
options.boolean('include-deprecated-apis').describe('include-deprecated-apis', 'This option is not currently supported.')
options.alias('d', 'dev').boolean('d').describe('d', 'Run in development mode.')
options.alias('f', 'foreground').boolean('f').describe('f', 'Keep the main process in the foreground.')
options.alias('h', 'help').boolean('h').describe('h', 'Print this usage message.')
options.alias('l', 'log-file').string('l').describe('l', 'Log all output to file.')
options.alias('n', 'new-window').boolean('n').describe('n', 'Open a new window.')
options.boolean('profile-startup').describe('profile-startup', 'Create a profile of the startup execution time.')
options.alias('r', 'resource-path').string('r').describe('r', 'Set the path to the Atom source directory and enable dev-mode.')
options.boolean('safe').describe('safe', 'Do not load packages from ~/.atom/packages or ~/.atom/dev/packages.')
options.boolean('portable').describe('portable', 'Set portable mode. Copies the ~/.atom folder to be a sibling of the installed Atom location if a .atom folder is not already there.')
options.alias('t', 'test').boolean('t').describe('t', 'Run the specified specs and exit with error code on failures.')
options.alias('m', 'main-process').boolean('m').describe('m', 'Run the specified specs in the main process.')
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.')
args = options.argv
if args.help
process.stdout.write(options.help())
process.exit(0)
if args.version
writeFullVersion()
process.exit(0)
addToLastWindow = args['add']
executedFrom = args['executed-from']?.toString() ? process.cwd()
devMode = args['dev']
safeMode = args['safe']
pathsToOpen = args._
test = args['test']
mainProcess = args['main-process']
timeout = args['timeout']
newWindow = args['new-window']
pidToKillWhenClosed = args['pid'] if args['wait']
logFile = args['log-file']
socketPath = args['socket-path']
userDataDir = args['user-data-dir']
profileStartup = args['profile-startup']
clearWindowState = args['clear-window-state']
urlsToOpen = []
devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH ? path.join(app.getPath('home'), 'github', 'atom')
setPortable = args.portable
if args['resource-path']
devMode = true
resourcePath = args['resource-path']
devMode = true if test
resourcePath ?= devResourcePath if devMode
unless fs.statSyncNoException(resourcePath)
resourcePath = path.dirname(path.dirname(__dirname))
# On Yosemite the $PATH is not inherited by the "open" command, so we have to
# explicitly pass it by command line, see http://git.io/YC8_Ew.
process.env.PATH = args['path-environment'] if args['path-environment']
resourcePath = normalizeDriveLetterName(resourcePath)
devResourcePath = normalizeDriveLetterName(devResourcePath)
{resourcePath, devResourcePath, pathsToOpen, urlsToOpen, executedFrom, test,
version, pidToKillWhenClosed, devMode, safeMode, newWindow,
logFile, socketPath, userDataDir, profileStartup, timeout, setPortable,
clearWindowState, addToLastWindow, mainProcess}
start()

263
src/main-process/main.js Normal file
View File

@@ -0,0 +1,263 @@
global.shellStartTime = Date.now()
process.on('uncaughtException', function (error = {}) {
if (error.message != null) {
console.log(error.message)
}
if (error.stack != null) {
console.log(error.stack)
}
})
const {app} = require('electron')
const fs = require('fs-plus')
const path = require('path')
const temp = require('temp')
const yargs = require('yargs')
const dedent = require('dedent')
const startCrashReporter = require('../crash-reporter-start')
const previousConsoleLog = console.log
console.log = require('nslog')
function start () {
const args = parseCommandLine()
args.env = process.env
setupAtomHome(args)
setupCompileCache()
if (handleStartupEventWithSquirrel()) {
return
} else if (args.test && args.mainProcess) {
console.log = previousConsoleLog
app.on('ready', function () {
const testRunner = require(path.join(args.resourcePath, 'spec/main-process/mocha-test-runner'))
testRunner(args.pathsToOpen)
})
return
}
// NB: This prevents Win10 from showing dupe items in the taskbar
app.setAppUserModelId('com.squirrel.atom.atom')
function addPathToOpen (event, pathToOpen) {
event.preventDefault()
args.pathsToOpen.push(pathToOpen)
}
function addUrlToOpen (event, urlToOpen) {
event.preventDefault()
args.urlsToOpen.push(urlToOpen)
}
app.on('open-file', addPathToOpen)
app.on('open-url', addUrlToOpen)
app.on('will-finish-launching', startCrashReporter)
if (args.userDataDir != null) {
app.setPath('userData', args.userDataDir)
} else if (args.test) {
app.setPath('userData', temp.mkdirSync('atom-test-data'))
}
app.on('ready', function () {
app.removeListener('open-file', addPathToOpen)
app.removeListener('open-url', addUrlToOpen)
const AtomApplication = require(path.join(args.resourcePath, 'src', 'main-process', 'atom-application'))
AtomApplication.open(args)
if (!args.test) {
console.log(`App load time: ${Date.now() - global.shellStartTime}ms`)
}
})
}
function normalizeDriveLetterName (filePath) {
if (process.platform === 'win32') {
return filePath.replace(/^([a-z]):/, ([driveLetter]) => driveLetter.toUpperCase() + ':')
} else {
return filePath
}
}
function handleStartupEventWithSquirrel () {
if (process.platform !== 'win32') {
return false
}
const SquirrelUpdate = require('./squirrel-update')
const squirrelCommand = process.argv[1]
return SquirrelUpdate.handleStartupEvent(app, squirrelCommand)
}
function setupAtomHome ({setPortable}) {
if (process.env.ATOM_HOME) {
return
}
let atomHome = path.join(app.getPath('home'), '.atom')
const AtomPortable = require('./atom-portable')
if (setPortable && !AtomPortable.isPortableInstall(process.platform, process.env.ATOM_HOME, atomHome)) {
try {
AtomPortable.setPortable(atomHome)
} catch (error) {
console.log(`Failed copying portable directory '${atomHome}' to '${AtomPortable.getPortableAtomHomePath()}'`)
console.log(`${error.message} ${error.stack}`)
}
}
if (AtomPortable.isPortableInstall(process.platform, process.env.ATOM_HOME, atomHome)) {
atomHome = AtomPortable.getPortableAtomHomePath()
}
try {
atomHome = fs.realpathSync(atomHome)
} finally {
process.env.ATOM_HOME = atomHome
}
}
function setupCompileCache () {
const CompileCache = require('../compile-cache')
CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME)
}
function writeFullVersion () {
process.stdout.write(
`Atom : ${app.getVersion()}\n` +
`Electron: ${process.versions.electron}\n` +
`Chrome : ${process.versions.chrome}\n` +
`Node : ${process.versions.node}\n`
)
}
function parseCommandLine () {
const options = yargs(process.argv.slice(1)).wrap(100)
const version = app.getVersion()
options.usage(
dedent`Atom Editor v${version}
Usage: atom [options] [path ...]
One or more paths to files or folders may be specified. If there is an
existing Atom window that contains all of the given folders, the paths
will be opened in that window. Otherwise, they will be opened in a new
window.
Environment Variables:
ATOM_DEV_RESOURCE_PATH The path from which Atom loads source code in dev mode.
Defaults to \`~/github/atom\`.
ATOM_HOME The root path for all configuration files and folders.
Defaults to \`~/.atom\`.`
)
// Deprecated 1.0 API preview flag
options.alias('1', 'one').boolean('1').describe('1', 'This option is no longer supported.')
options.boolean('include-deprecated-apis').describe('include-deprecated-apis', 'This option is not currently supported.')
options.alias('d', 'dev').boolean('d').describe('d', 'Run in development mode.')
options.alias('f', 'foreground').boolean('f').describe('f', 'Keep the main process in the foreground.')
options.alias('h', 'help').boolean('h').describe('h', 'Print this usage message.')
options.alias('l', 'log-file').string('l').describe('l', 'Log all output to file.')
options.alias('n', 'new-window').boolean('n').describe('n', 'Open a new window.')
options.boolean('profile-startup').describe('profile-startup', 'Create a profile of the startup execution time.')
options.alias('r', 'resource-path').string('r').describe('r', 'Set the path to the Atom source directory and enable dev-mode.')
options.boolean('safe').describe(
'safe',
'Do not load packages from ~/.atom/packages or ~/.atom/dev/packages.'
)
options.boolean('portable').describe(
'portable',
'Set portable mode. Copies the ~/.atom folder to be a sibling of the installed Atom location if a .atom folder is not already there.'
)
options.alias('t', 'test').boolean('t').describe('t', 'Run the specified specs and exit with error code on failures.')
options.alias('m', 'main-process').boolean('m').describe('m', 'Run the specified specs in the main process.')
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.')
const args = options.argv
if (args.help) {
process.stdout.write(options.help())
process.exit(0)
}
if (args.version) {
writeFullVersion()
process.exit(0)
}
const addToLastWindow = args['add']
const safeMode = args['safe']
const pathsToOpen = args._
const test = args['test']
const mainProcess = args['main-process']
const timeout = args['timeout']
const newWindow = args['new-window']
let executedFrom = null
if (args['executed-from'] && args['executed-from'].toString()) {
executedFrom = args['executed-from'].toString()
} else {
executedFrom = process.cwd()
}
let pidToKillWhenClosed = null
if (args['wait']) {
pidToKillWhenClosed = args['pid']
}
const logFile = args['log-file']
const socketPath = args['socket-path']
const userDataDir = args['user-data-dir']
const profileStartup = args['profile-startup']
const clearWindowState = args['clear-window-state']
const urlsToOpen = []
const setPortable = args.portable
let devMode = args['dev']
let devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH || path.join(app.getPath('home'), 'github', 'atom')
let resourcePath = null
if (args['resource-path']) {
devMode = true
resourcePath = args['resource-path']
}
if (test) {
devMode = true
}
if (devMode && !resourcePath) {
resourcePath = devResourcePath
}
if (!fs.statSyncNoException(resourcePath)) {
resourcePath = path.dirname(path.dirname(__dirname))
}
if (args['path-environment']) {
// On Yosemite the $PATH is not inherited by the "open" command, so we have to
// explicitly pass it by command line, see http://git.io/YC8_Ew.
process.env.PATH = args['path-environment']
}
resourcePath = normalizeDriveLetterName(resourcePath)
devResourcePath = normalizeDriveLetterName(devResourcePath)
return {
resourcePath, devResourcePath, pathsToOpen, urlsToOpen, executedFrom, test,
version, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, socketPath,
userDataDir, profileStartup, timeout, setPortable, clearWindowState,
addToLastWindow, mainProcess
}
}
start()

View File

@@ -1,7 +1,7 @@
fs = require 'fs-plus'
path = require 'path'
Spawner = require './spawner'
WinRegistry = require './win-registry'
WinShell = require './win-shell'
WinPowerShell = require './win-powershell'
appFolder = path.resolve(process.execPath, '..')
@@ -125,26 +125,36 @@ exports.restartAtom = (app) ->
app.once 'will-quit', -> Spawner.spawn(path.join(binFolder, 'atom.cmd'), args)
app.quit()
updateContextMenus = (callback) ->
WinShell.fileContextMenu.update ->
WinShell.folderContextMenu.update ->
WinShell.folderBackgroundContextMenu.update ->
callback()
# Handle squirrel events denoted by --squirrel-* command line arguments.
exports.handleStartupEvent = (app, squirrelCommand) ->
switch squirrelCommand
when '--squirrel-install'
createShortcuts ->
WinRegistry.installContextMenu ->
addCommandsToPath ->
app.quit()
addCommandsToPath ->
WinShell.fileHandler.register ->
updateContextMenus ->
app.quit()
true
when '--squirrel-updated'
updateShortcuts ->
WinRegistry.installContextMenu ->
addCommandsToPath ->
addCommandsToPath ->
updateContextMenus ->
app.quit()
true
when '--squirrel-uninstall'
removeShortcuts ->
WinRegistry.uninstallContextMenu ->
removeCommandsFromPath ->
app.quit()
removeCommandsFromPath ->
WinShell.fileHandler.deregister ->
WinShell.fileContextMenu.deregister ->
WinShell.folderContextMenu.deregister ->
WinShell.folderBackgroundContextMenu.deregister ->
app.quit()
true
when '--squirrel-obsolete'
app.quit()

View File

@@ -1,62 +0,0 @@
path = require 'path'
Spawner = require './spawner'
if process.env.SystemRoot
system32Path = path.join(process.env.SystemRoot, 'System32')
regPath = path.join(system32Path, 'reg.exe')
else
regPath = 'reg.exe'
# Registry keys used for context menu
fileKeyPath = 'HKCU\\Software\\Classes\\*\\shell\\Atom'
directoryKeyPath = 'HKCU\\Software\\Classes\\directory\\shell\\Atom'
backgroundKeyPath = 'HKCU\\Software\\Classes\\directory\\background\\shell\\Atom'
applicationsKeyPath = 'HKCU\\Software\\Classes\\Applications\\atom.exe'
# Spawn reg.exe and callback when it completes
spawnReg = (args, callback) ->
Spawner.spawn(regPath, args, callback)
# Install the Open with Atom explorer context menu items via the registry.
#
# * `callback` The {Function} to call after registry operation is done.
# It will be invoked with the same arguments provided by {Spawner.spawn}.
#
# Returns `undefined`.
exports.installContextMenu = (callback) ->
addToRegistry = (args, callback) ->
args.unshift('add')
args.push('/f')
spawnReg(args, callback)
installFileHandler = (callback) ->
args = ["#{applicationsKeyPath}\\shell\\open\\command", '/ve', '/d', "\"#{process.execPath}\" \"%1\""]
addToRegistry(args, callback)
installMenu = (keyPath, arg, callback) ->
args = [keyPath, '/ve', '/d', 'Open with Atom']
addToRegistry args, ->
args = [keyPath, '/v', 'Icon', '/d', "\"#{process.execPath}\""]
addToRegistry args, ->
args = ["#{keyPath}\\command", '/ve', '/d', "\"#{process.execPath}\" \"#{arg}\""]
addToRegistry(args, callback)
installMenu fileKeyPath, '%1', ->
installMenu directoryKeyPath, '%1', ->
installMenu backgroundKeyPath, '%V', ->
installFileHandler(callback)
# Uninstall the Open with Atom explorer context menu items via the registry.
#
# * `callback` The {Function} to call after registry operation is done.
# It will be invoked with the same arguments provided by {Spawner.spawn}.
#
# Returns `undefined`.
exports.uninstallContextMenu = (callback) ->
deleteFromRegistry = (keyPath, callback) ->
spawnReg(['delete', keyPath, '/f'], callback)
deleteFromRegistry fileKeyPath, ->
deleteFromRegistry directoryKeyPath, ->
deleteFromRegistry backgroundKeyPath, ->
deleteFromRegistry(applicationsKeyPath, callback)

View File

@@ -0,0 +1,58 @@
Registry = require 'winreg'
Path = require 'path'
exeName = Path.basename(process.execPath)
appPath = "\"#{process.execPath}\""
isBeta = appPath.includes(' Beta')
appName = exeName.replace('atom', (if isBeta then 'Atom Beta' else 'Atom' )).replace('.exe', '')
class ShellOption
constructor: (key, parts) ->
@key = key
@parts = parts
isRegistered: (callback) =>
new Registry({hive: 'HKCU', key: "#{@key}\\#{@parts[0].key}"})
.get @parts[0].name, (err, val) =>
callback(not err? and val? and val.value is @parts[0].value)
register: (callback) =>
doneCount = @parts.length
@parts.forEach (part) =>
reg = new Registry({hive: 'HKCU', key: if part.key? then "#{@key}\\#{part.key}" else @key})
reg.create( -> reg.set part.name, Registry.REG_SZ, part.value, -> callback() if --doneCount is 0)
deregister: (callback) =>
@isRegistered (isRegistered) =>
if isRegistered
new Registry({hive: 'HKCU', key: @key}).destroy -> callback null, true
else
callback null, false
update: (callback) =>
new Registry({hive: 'HKCU', key: "#{@key}\\#{@parts[0].key}"})
.get @parts[0].name, (err, val) =>
if err? or not val? or val.value.includes '\\' + exeName
callback(err)
else
@register callback
exports.appName = appName
exports.fileHandler = new ShellOption("\\Software\\Classes\\Applications\\#{exeName}",
[{key: 'shell\\open\\command', name: '', value: "#{appPath} \"%1\""}]
)
contextParts = [
{key: 'command', name: '', value: "#{appPath} \"%1\""},
{name: '', value: "Open with #{appName}"},
{name: 'Icon', value: "#{appPath}"}
]
exports.fileContextMenu = new ShellOption("\\Software\\Classes\\*\\shell\\#{appName}", contextParts)
exports.folderContextMenu = new ShellOption("\\Software\\Classes\\Directory\\shell\\#{appName}", contextParts)
exports.folderBackgroundContextMenu = new ShellOption("\\Software\\Classes\\Directory\\background\\shell\\#{appName}",
JSON.parse(JSON.stringify(contextParts).replace('%1', '%V'))
)

View File

@@ -33,8 +33,20 @@ class NotificationManager
#
# * `message` A {String} message
# * `options` (optional) An options {Object} with the following keys:
# * `detail` (optional) A {String} with additional details about the
# notification.
# * `buttons` (optional) An {Array} of {Object} where each {Object} has the
# following options:
# * `className` (optional) {String} a class name to add to the button's
# default class name (`btn btn-success`).
# * `onDidClick` (optional) {Function} callback to call when the button
# has been clicked. The context will be set to the
# {NotificationElement} instance.
# * `text` {String} inner text for the button
# * `description` (optional) A Markdown {String} containing a longer
# description about the notification. By default, this **will not**
# preserve newlines and whitespace when it is rendered.
# * `detail` (optional) A plain-text {String} containing additional details
# about the notification. By default, this **will** preserve newlines
# and whitespace when it is rendered.
# * `dismissable` (optional) A {Boolean} indicating whether this
# notification can be dismissed by the user. Defaults to `false`.
# * `icon` (optional) A {String} name of an icon from Octicons to display
@@ -46,8 +58,20 @@ class NotificationManager
#
# * `message` A {String} message
# * `options` (optional) An options {Object} with the following keys:
# * `detail` (optional) A {String} with additional details about the
# notification.
# * `buttons` (optional) An {Array} of {Object} where each {Object} has the
# following options:
# * `className` (optional) {String} a class name to add to the button's
# default class name (`btn btn-info`).
# * `onDidClick` (optional) {Function} callback to call when the button
# has been clicked. The context will be set to the
# {NotificationElement} instance.
# * `text` {String} inner text for the button
# * `description` (optional) A Markdown {String} containing a longer
# description about the notification. By default, this **will not**
# preserve newlines and whitespace when it is rendered.
# * `detail` (optional) A plain-text {String} containing additional details
# about the notification. By default, this **will** preserve newlines
# and whitespace when it is rendered.
# * `dismissable` (optional) A {Boolean} indicating whether this
# notification can be dismissed by the user. Defaults to `false`.
# * `icon` (optional) A {String} name of an icon from Octicons to display
@@ -59,8 +83,20 @@ class NotificationManager
#
# * `message` A {String} message
# * `options` (optional) An options {Object} with the following keys:
# * `detail` (optional) A {String} with additional details about the
# notification.
# * `buttons` (optional) An {Array} of {Object} where each {Object} has the
# following options:
# * `className` (optional) {String} a class name to add to the button's
# default class name (`btn btn-warning`).
# * `onDidClick` (optional) {Function} callback to call when the button
# has been clicked. The context will be set to the
# {NotificationElement} instance.
# * `text` {String} inner text for the button
# * `description` (optional) A Markdown {String} containing a longer
# description about the notification. By default, this **will not**
# preserve newlines and whitespace when it is rendered.
# * `detail` (optional) A plain-text {String} containing additional details
# about the notification. By default, this **will** preserve newlines
# and whitespace when it is rendered.
# * `dismissable` (optional) A {Boolean} indicating whether this
# notification can be dismissed by the user. Defaults to `false`.
# * `icon` (optional) A {String} name of an icon from Octicons to display
@@ -72,12 +108,26 @@ class NotificationManager
#
# * `message` A {String} message
# * `options` (optional) An options {Object} with the following keys:
# * `detail` (optional) A {String} with additional details about the
# notification.
# * `buttons` (optional) An {Array} of {Object} where each {Object} has the
# following options:
# * `className` (optional) {String} a class name to add to the button's
# default class name (`btn btn-error`).
# * `onDidClick` (optional) {Function} callback to call when the button
# has been clicked. The context will be set to the
# {NotificationElement} instance.
# * `text` {String} inner text for the button
# * `description` (optional) A Markdown {String} containing a longer
# description about the notification. By default, this **will not**
# preserve newlines and whitespace when it is rendered.
# * `detail` (optional) A plain-text {String} containing additional details
# about the notification. By default, this **will** preserve newlines
# and whitespace when it is rendered.
# * `dismissable` (optional) A {Boolean} indicating whether this
# notification can be dismissed by the user. Defaults to `false`.
# * `icon` (optional) A {String} name of an icon from Octicons to display
# in the notification header. Defaults to `'flame'`.
# * `stack` (optional) A preformatted {String} with stack trace information
# describing the location of the error.
addError: (message, options) ->
@addNotification(new Notification('error', message, options))
@@ -85,12 +135,26 @@ class NotificationManager
#
# * `message` A {String} message
# * `options` (optional) An options {Object} with the following keys:
# * `detail` (optional) A {String} with additional details about the
# notification.
# * `buttons` (optional) An {Array} of {Object} where each {Object} has the
# following options:
# * `className` (optional) {String} a class name to add to the button's
# default class name (`btn btn-error`).
# * `onDidClick` (optional) {Function} callback to call when the button
# has been clicked. The context will be set to the
# {NotificationElement} instance.
# * `text` {String} inner text for the button
# * `description` (optional) A Markdown {String} containing a longer
# description about the notification. By default, this **will not**
# preserve newlines and whitespace when it is rendered.
# * `detail` (optional) A plain-text {String} containing additional details
# about the notification. By default, this **will** preserve newlines
# and whitespace when it is rendered.
# * `dismissable` (optional) A {Boolean} indicating whether this
# notification can be dismissed by the user. Defaults to `false`.
# * `icon` (optional) A {String} name of an icon from Octicons to display
# in the notification header. Defaults to `'bug'`.
# * `stack` (optional) A preformatted {String} with stack trace information
# describing the location of the error.
addFatalError: (message, options) ->
@addNotification(new Notification('fatal', message, options))

View File

@@ -159,6 +159,7 @@ class Package
# TODO: Remove. Settings view calls this method currently.
activateConfig: ->
return if @configSchemaRegisteredOnLoad
@requireMainModule()
@registerConfigSchemaFromMainModule()
@@ -426,8 +427,8 @@ class Package
return @mainModule if @mainModuleRequired
unless @isCompatible()
console.warn """
Failed to require the main module of '#{@name}' because it requires an incompatible native module.
Run `apm rebuild` in the package directory to resolve.
Failed to require the main module of '#{@name}' because it requires one or more incompatible native modules (#{_.pluck(@incompatibleModules, 'name').join(', ')}).
Run `apm rebuild` in the package directory and restart Atom to resolve.
"""
return
mainModulePath = @getMainModulePath()

View File

@@ -5,15 +5,32 @@ module.exports = (repoPath, paths = []) ->
repo = Git.open(repoPath)
upstream = {}
statuses = {}
submodules = {}
branch = null
if repo?
# Statuses in main repo
workingDirectoryPath = repo.getWorkingDirectory()
repoStatus = (if paths.length > 0 then repo.getStatusForPaths(paths) else repo.getStatus())
for filePath, status of repoStatus
statuses[filePath] = status
# Statuses in submodules
for submodulePath, submoduleRepo of repo.submodules
submodules[submodulePath] =
branch: submoduleRepo.getHead()
upstream: submoduleRepo.getAheadBehindCount()
workingDirectoryPath = submoduleRepo.getWorkingDirectory()
for filePath, status of submoduleRepo.getStatus()
absolutePath = path.join(workingDirectoryPath, filePath)
# Make path relative to parent repository
relativePath = repo.relativize(absolutePath)
statuses[relativePath] = status
upstream = repo.getAheadBehindCount()
branch = repo.getHead()
repo.release()
{upstream, submodules}
{statuses, upstream, branch, submodules}

View File

@@ -1,120 +0,0 @@
{spliceWithArray} = require 'underscore-plus'
# Used by the display buffer to map screen rows to buffer rows and vice-versa.
# This mapping may not be 1:1 due to folds and soft-wraps. This object maintains
# an array of regions, which contain `bufferRows` and `screenRows` fields.
#
# Rectangular Regions:
# If a region has the same number of buffer rows and screen rows, it is referred
# to as "rectangular", and represents one or more non-soft-wrapped, non-folded
# lines.
#
# Trapezoidal Regions:
# If a region has one buffer row and more than one screen row, it represents a
# soft-wrapped line. If a region has one screen row and more than one buffer
# row, it represents folded lines
module.exports =
class RowMap
constructor: ->
@regions = []
# Public: Returns a copy of all the regions in the map
getRegions: ->
@regions.slice()
# Public: Returns an end-row-exclusive range of screen rows corresponding to
# the given buffer row. If the buffer row is soft-wrapped, the range may span
# multiple screen rows. Otherwise it will span a single screen row.
screenRowRangeForBufferRow: (targetBufferRow) ->
{region, bufferRows, screenRows} = @traverseToBufferRow(targetBufferRow)
if region? and region.bufferRows isnt region.screenRows
[screenRows, screenRows + region.screenRows]
else
screenRows += targetBufferRow - bufferRows
[screenRows, screenRows + 1]
# Public: Returns an end-row-exclusive range of buffer rows corresponding to
# the given screen row. If the screen row is the first line of a folded range
# of buffer rows, the range may span multiple buffer rows. Otherwise it will
# span a single buffer row.
bufferRowRangeForScreenRow: (targetScreenRow) ->
{region, screenRows, bufferRows} = @traverseToScreenRow(targetScreenRow)
if region? and region.bufferRows isnt region.screenRows
[bufferRows, bufferRows + region.bufferRows]
else
bufferRows += targetScreenRow - screenRows
[bufferRows, bufferRows + 1]
# Public: If the given buffer row is part of a folded row range, returns that
# row range. Otherwise returns a range spanning only the given buffer row.
bufferRowRangeForBufferRow: (targetBufferRow) ->
{region, bufferRows} = @traverseToBufferRow(targetBufferRow)
if region? and region.bufferRows isnt region.screenRows
[bufferRows, bufferRows + region.bufferRows]
else
[targetBufferRow, targetBufferRow + 1]
# Public: Given a starting buffer row, the number of buffer rows to replace,
# and an array of regions of shape {bufferRows: n, screenRows: m}, splices
# the regions at the appropriate location in the map. This method is used by
# display buffer to keep the map updated when the underlying buffer changes.
spliceRegions: (startBufferRow, bufferRowCount, regions) ->
endBufferRow = startBufferRow + bufferRowCount
{index, bufferRows} = @traverseToBufferRow(startBufferRow)
precedingRows = startBufferRow - bufferRows
count = 0
while region = @regions[index + count]
count++
bufferRows += region.bufferRows
if bufferRows >= endBufferRow
followingRows = bufferRows - endBufferRow
break
if precedingRows > 0
regions.unshift({bufferRows: precedingRows, screenRows: precedingRows})
if followingRows > 0
regions.push({bufferRows: followingRows, screenRows: followingRows})
spliceWithArray(@regions, index, count, regions)
@mergeAdjacentRectangularRegions(index - 1, index + regions.length)
traverseToBufferRow: (targetBufferRow) ->
bufferRows = 0
screenRows = 0
for region, index in @regions
if (bufferRows + region.bufferRows) > targetBufferRow
return {region, index, screenRows, bufferRows}
bufferRows += region.bufferRows
screenRows += region.screenRows
{index, screenRows, bufferRows}
traverseToScreenRow: (targetScreenRow) ->
bufferRows = 0
screenRows = 0
for region, index in @regions
if (screenRows + region.screenRows) > targetScreenRow
return {region, index, screenRows, bufferRows}
bufferRows += region.bufferRows
screenRows += region.screenRows
{index, screenRows, bufferRows}
mergeAdjacentRectangularRegions: (startIndex, endIndex) ->
for index in [endIndex..startIndex]
if 0 < index < @regions.length
leftRegion = @regions[index - 1]
rightRegion = @regions[index]
leftIsRectangular = leftRegion.bufferRows is leftRegion.screenRows
rightIsRectangular = rightRegion.bufferRows is rightRegion.screenRows
if leftIsRectangular and rightIsRectangular
@regions.splice index - 1, 2,
bufferRows: leftRegion.bufferRows + rightRegion.bufferRows
screenRows: leftRegion.screenRows + rightRegion.screenRows
return
# Public: Returns an array of strings describing the map's regions.
inspect: ->
for {bufferRows, screenRows} in @regions
"#{bufferRows}:#{screenRows}"

View File

@@ -365,7 +365,12 @@ class TextEditorComponent
onTextInput: (event) =>
event.stopPropagation()
event.preventDefault()
# WARNING: If we call preventDefault on the input of a space character,
# then the browser interprets the spacebar keypress as a page-down command,
# causing spaces to scroll elements containing editors. This is impossible
# to test.
event.preventDefault() if event.data isnt ' '
return unless @isInputEnabled()

View File

@@ -18,24 +18,31 @@ class TokenizedBufferIterator
@currentLineLength = currentLine.text.length
@containingTags = @currentLineOpenTags.map (id) => @grammarRegistry.scopeForId(id)
currentColumn = 0
for tag, index in @currentTags
if tag >= 0
if currentColumn >= position.column and @isAtTagBoundary()
if currentColumn is position.column
@tagIndex = index
break
else
currentColumn += tag
@containingTags.pop() while @closeTags.shift()
@containingTags.push(tag) while tag = @openTags.shift()
else
scopeName = @grammarRegistry.scopeForId(tag)
if tag % 2 is 0
if @openTags.length > 0
@containingTags.push(openTag) while openTag = @openTags.shift()
if currentColumn > position.column
@tagIndex = index
break
else
@closeTags.push(scopeName)
else
else
scopeName = @grammarRegistry.scopeForId(tag)
if tag % 2 is 0 # close tag
if @openTags.length > 0
if currentColumn is position.column
@tagIndex = index
break
else
@containingTags.pop() while @closeTags.shift()
@containingTags.push(openTag) while openTag = @openTags.shift()
@closeTags.push(scopeName)
else # open tag
@openTags.push(scopeName)
@tagIndex ?= @currentTags.length

View File

@@ -1,27 +0,0 @@
# Public: Measure how long a function takes to run.
#
# description - A {String} description that will be logged to the console when
# the function completes.
# fn - A {Function} to measure the duration of.
#
# Returns the value returned by the given function.
window.measure = (description, fn) ->
start = Date.now()
value = fn()
result = Date.now() - start
console.log description, result
value
# Public: Create a dev tools profile for a function.
#
# description - A {String} description that will be available in the Profiles
# tab of the dev tools.
# fn - A {Function} to profile.
#
# Returns the value returned by the given function.
window.profile = (description, fn) ->
measure description, ->
console.profile(description)
value = fn()
console.profileEnd(description)
value

30
src/window.js Normal file
View File

@@ -0,0 +1,30 @@
// Public: Measure how long a function takes to run.
//
// description - A {String} description that will be logged to the console when
// the function completes.
// fn - A {Function} to measure the duration of.
//
// Returns the value returned by the given function.
window.measure = function (description, fn) {
let start = Date.now()
let value = fn()
let result = Date.now() - start
console.log(description, result)
return value
}
// Public: Create a dev tools profile for a function.
//
// description - A {String} description that will be available in the Profiles
// tab of the dev tools.
// fn - A {Function} to profile.
//
// Returns the value returned by the given function.
window.profile = function (description, fn) {
window.measure(description, function () {
console.profile(description)
let value = fn()
console.profileEnd(description)
return value
})
}

View File

@@ -1097,7 +1097,7 @@ class Workspace extends Model
checkoutHead = =>
@project.repositoryForDirectory(new Directory(editor.getDirectoryPath()))
.then (repository) ->
repository?.async.checkoutHeadForEditor(editor)
repository?.checkoutHeadForEditor(editor)
if @config.get('editor.confirmCheckoutHeadRevision')
@applicationDelegate.confirm