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

This commit is contained in:
Luke Pommersheim
2015-09-02 09:30:42 +02:00
64 changed files with 2060 additions and 1240 deletions

View File

@@ -9,7 +9,7 @@ _ = require 'underscore-plus'
{deprecate, includeDeprecatedAPIs} = require 'grim'
{CompositeDisposable, Emitter} = require 'event-kit'
fs = require 'fs-plus'
{convertStackTrace, convertLine} = require 'coffeestack'
{mapSourcePosition} = require 'source-map-support'
Model = require './model'
{$} = require './space-pen-extensions'
WindowEventHandler = require './window-event-handler'
@@ -196,15 +196,11 @@ class Atom extends Model
#
# Call after this instance has been assigned to the `atom` global.
initialize: ->
sourceMapCache = {}
window.onerror = =>
@lastUncaughtError = Array::slice.call(arguments)
[message, url, line, column, originalError] = @lastUncaughtError
convertedLine = convertLine(url, line, column, sourceMapCache)
{line, column} = convertedLine if convertedLine?
originalError.stack = convertStackTrace(originalError.stack, sourceMapCache) if originalError
{line, column} = mapSourcePosition({source: url, line, column})
eventObject = {message, url, line, column, originalError}
@@ -673,6 +669,7 @@ class Atom extends Model
@windowEventHandler?.unsubscribe()
openInitialEmptyEditorIfNecessary: ->
return unless @config.get('core.openEmptyEditorOnStart')
if @getLoadSettings().initialPaths?.length is 0 and @workspace.getPaneItems().length is 0
@workspace.open(null)

View File

@@ -1,200 +0,0 @@
###
Cache for source code transpiled by Babel.
Inspired by https://github.com/atom/atom/blob/6b963a562f8d495fbebe6abdbafbc7caf705f2c3/src/coffee-cache.coffee.
###
crypto = require 'crypto'
fs = require 'fs-plus'
path = require 'path'
babel = null # Defer until used
Grim = null # Defer until used
stats =
hits: 0
misses: 0
defaultOptions =
# Currently, the cache key is a function of:
# * The version of Babel used to transpile the .js file.
# * The contents of this defaultOptions object.
# * The contents of the .js file.
# That means that we cannot allow information from an unknown source
# to affect the cache key for the output of transpilation, which means
# we cannot allow users to override these default options via a .babelrc
# file, because the contents of that .babelrc file will not make it into
# the cache key. It would be great to support .babelrc files once we
# have a way to do so that is safe with respect to caching.
breakConfig: true
# The Chrome dev tools will show the original version of the file
# when the source map is inlined.
sourceMap: 'inline'
# Blacklisted features do not get transpiled. Features that are
# natively supported in the target environment should be listed
# here. Because Atom uses a bleeding edge version of Node/io.js,
# I think this can include es6.arrowFunctions, es6.classes, and
# possibly others, but I want to be conservative.
blacklist: [
'es6.forOf'
'useStrict'
]
optional: [
# Target a version of the regenerator runtime that
# supports yield so the transpiled code is cleaner/smaller.
'asyncToGenerator'
]
# Includes support for es7 features listed at:
# http://babeljs.io/docs/usage/experimental/.
stage: 0
###
shasum - Hash with an update() method.
value - Must be a value that could be returned by JSON.parse().
###
updateDigestForJsonValue = (shasum, value) ->
# Implmentation is similar to that of pretty-printing a JSON object, except:
# * Strings are not escaped.
# * No effort is made to avoid trailing commas.
# These shortcuts should not affect the correctness of this function.
type = typeof value
if type is 'string'
shasum.update('"', 'utf8')
shasum.update(value, 'utf8')
shasum.update('"', 'utf8')
else if type in ['boolean', 'number']
shasum.update(value.toString(), 'utf8')
else if value is null
shasum.update('null', 'utf8')
else if Array.isArray value
shasum.update('[', 'utf8')
for item in value
updateDigestForJsonValue(shasum, item)
shasum.update(',', 'utf8')
shasum.update(']', 'utf8')
else
# value must be an object: be sure to sort the keys.
keys = Object.keys value
keys.sort()
shasum.update('{', 'utf8')
for key in keys
updateDigestForJsonValue(shasum, key)
shasum.update(': ', 'utf8')
updateDigestForJsonValue(shasum, value[key])
shasum.update(',', 'utf8')
shasum.update('}', 'utf8')
createBabelVersionAndOptionsDigest = (version, options) ->
shasum = crypto.createHash('sha1')
# Include the version of babel in the hash.
shasum.update('babel-core', 'utf8')
shasum.update('\0', 'utf8')
shasum.update(version, 'utf8')
shasum.update('\0', 'utf8')
updateDigestForJsonValue(shasum, options)
shasum.digest('hex')
cacheDir = null
jsCacheDir = null
getCachePath = (sourceCode) ->
digest = crypto.createHash('sha1').update(sourceCode, 'utf8').digest('hex')
unless jsCacheDir?
to5Version = require('babel-core/package.json').version
jsCacheDir = path.join(cacheDir, createBabelVersionAndOptionsDigest(to5Version, defaultOptions))
path.join(jsCacheDir, "#{digest}.js")
getCachedJavaScript = (cachePath) ->
if fs.isFileSync(cachePath)
try
cachedJavaScript = fs.readFileSync(cachePath, 'utf8')
stats.hits++
return cachedJavaScript
null
# Returns the babel options that should be used to transpile filePath.
createOptions = (filePath) ->
options = filename: filePath
for key, value of defaultOptions
options[key] = value
options
transpile = (sourceCode, filePath, cachePath) ->
options = createOptions(filePath)
babel ?= require 'babel-core'
js = babel.transform(sourceCode, options).code
stats.misses++
try
fs.writeFileSync(cachePath, js)
js
# Function that obeys the contract of an entry in the require.extensions map.
# Returns the transpiled version of the JavaScript code at filePath, which is
# either generated on the fly or pulled from cache.
loadFile = (module, filePath) ->
sourceCode = fs.readFileSync(filePath, 'utf8')
if sourceCode.startsWith('"use babel"') or sourceCode.startsWith("'use babel'")
# Continue.
else if sourceCode.startsWith('"use 6to5"') or sourceCode.startsWith("'use 6to5'")
# Create a manual deprecation since the stack is too deep to use Grim
# which limits the depth to 3
Grim ?= require 'grim'
stack = [
{
fileName: __filename
functionName: 'loadFile'
location: "#{__filename}:161:5"
}
{
fileName: filePath
functionName: '<unknown>'
location: "#{filePath}:1:1"
}
]
deprecation =
message: "Use the 'use babel' pragma instead of 'use 6to5'"
stacks: [stack]
Grim.addSerializedDeprecation(deprecation)
else
return module._compile(sourceCode, filePath)
cachePath = getCachePath(sourceCode)
js = getCachedJavaScript(cachePath) ? transpile(sourceCode, filePath, cachePath)
module._compile(js, filePath)
register = ->
Object.defineProperty(require.extensions, '.js', {
enumerable: true
writable: false
value: loadFile
})
setCacheDirectory = (newCacheDir) ->
if cacheDir isnt newCacheDir
cacheDir = newCacheDir
jsCacheDir = null
module.exports =
register: register
setCacheDirectory: setCacheDirectory
getCacheMisses: -> stats.misses
getCacheHits: -> stats.hits
# Visible for testing.
createBabelVersionAndOptionsDigest: createBabelVersionAndOptionsDigest
addPathToCache: (filePath) ->
return if path.extname(filePath) isnt '.js'
sourceCode = fs.readFileSync(filePath, 'utf8')
cachePath = getCachePath(sourceCode)
transpile(sourceCode, filePath, cachePath)

63
src/babel.js Normal file
View File

@@ -0,0 +1,63 @@
'use strict'
var crypto = require('crypto')
var path = require('path')
var defaultOptions = require('../static/babelrc.json')
var babel = null
var babelVersionDirectory = null
var PREFIXES = [
'/** @babel */',
'"use babel"',
'\'use babel\''
]
var PREFIX_LENGTH = Math.max.apply(Math, PREFIXES.map(function (prefix) {
return prefix.length
}))
exports.shouldCompile = function (sourceCode) {
var start = sourceCode.substr(0, PREFIX_LENGTH)
return PREFIXES.some(function (prefix) {
return start.indexOf(prefix) === 0
})
}
exports.getCachePath = function (sourceCode) {
if (babelVersionDirectory == null) {
var babelVersion = require('babel-core/package.json').version
babelVersionDirectory = path.join('js', 'babel', createVersionAndOptionsDigest(babelVersion, defaultOptions))
}
return path.join(
babelVersionDirectory,
crypto
.createHash('sha1')
.update(sourceCode, 'utf8')
.digest('hex') + '.js'
)
}
exports.compile = function (sourceCode, filePath) {
if (!babel) {
babel = require('babel-core')
}
var options = {filename: filePath}
for (var key in defaultOptions) {
options[key] = defaultOptions[key]
}
return babel.transform(sourceCode, options).code
}
function createVersionAndOptionsDigest (version, options) {
return crypto
.createHash('sha1')
.update('babel-core', 'utf8')
.update('\0', 'utf8')
.update(version, 'utf8')
.update('\0', 'utf8')
.update(JSON.stringify(options), 'utf8')
.digest('hex')
}

View File

@@ -65,9 +65,6 @@ class AtomApplication
constructor: (options) ->
{@resourcePath, @version, @devMode, @safeMode, @socketPath} = options
# Normalize to make sure drive letter case is consistent on Windows
@resourcePath = path.normalize(@resourcePath) if @resourcePath
global.atomApplication = this
@pidsToOpenWindows = {}

View File

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

View File

@@ -16,7 +16,7 @@ process.on 'uncaughtException', (error={}) ->
start = ->
setupAtomHome()
setupCoffeeCache()
setupCompileCache()
if process.platform is 'win32'
SquirrelUpdate = require './squirrel-update'
@@ -54,17 +54,20 @@ start = ->
else
path.resolve(pathToOpen)
if args.devMode
AtomApplication = require path.join(args.resourcePath, 'src', 'browser', 'atom-application')
else
AtomApplication = require './atom-application'
AtomApplication = require path.join(args.resourcePath, 'src', 'browser', 'atom-application')
AtomApplication.open(args)
console.log("App load time: #{Date.now() - global.shellStartTime}ms") unless args.test
global.devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH ? path.join(app.getHomeDir(), 'github', 'atom')
# Normalize to make sure drive letter case is consistent on Windows
global.devResourcePath = path.normalize(global.devResourcePath) if global.devResourcePath
normalizeDriveLetterName = (filePath) ->
if process.platform is 'win32'
filePath.replace /^([a-z]):/, ([driveLetter]) -> driveLetter.toUpperCase() + ":"
else
filePath
global.devResourcePath = normalizeDriveLetterName(
process.env.ATOM_DEV_RESOURCE_PATH ? path.join(app.getHomeDir(), 'github', 'atom')
)
setupCrashReporter = ->
crashReporter.start(productName: 'Atom', companyName: 'GitHub')
@@ -77,14 +80,9 @@ setupAtomHome = ->
atomHome = fs.realpathSync(atomHome)
process.env.ATOM_HOME = atomHome
setupCoffeeCache = ->
CoffeeCache = require 'coffee-cash'
cacheDir = path.join(process.env.ATOM_HOME, 'compile-cache')
# Use separate compile cache when sudo'ing as root to avoid permission issues
if process.env.USER is 'root' and process.env.SUDO_USER and process.env.SUDO_USER isnt process.env.USER
cacheDir = path.join(cacheDir, 'root')
CoffeeCache.setCacheDirectory(path.join(cacheDir, 'coffee'))
CoffeeCache.register()
setupCompileCache = ->
compileCache = require('../compile-cache')
compileCache.setAtomHomeDirectory(process.env.ATOM_HOME)
parseCommandLine = ->
version = app.getVersion()
@@ -169,6 +167,8 @@ parseCommandLine = ->
# 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)
{resourcePath, pathsToOpen, executedFrom, test, version, pidToKillWhenClosed,
devMode, safeMode, newWindow, specDirectory, logFile, socketPath, profileStartup}

44
src/coffee-script.js Normal file
View File

@@ -0,0 +1,44 @@
'use strict'
var crypto = require('crypto')
var path = require('path')
var CoffeeScript = null
exports.shouldCompile = function () {
return true
}
exports.getCachePath = function (sourceCode) {
return path.join(
'coffee',
crypto
.createHash('sha1')
.update(sourceCode, 'utf8')
.digest('hex') + '.js'
)
}
exports.compile = function (sourceCode, filePath) {
if (!CoffeeScript) {
var previousPrepareStackTrace = Error.prepareStackTrace
CoffeeScript = require('coffee-script')
// When it loads, coffee-script reassigns Error.prepareStackTrace. We have
// already reassigned it via the 'source-map-support' module, so we need
// to set it back.
Error.prepareStackTrace = previousPrepareStackTrace
}
var output = CoffeeScript.compile(sourceCode, {
filename: filePath,
sourceFiles: [filePath],
sourceMap: true
})
var js = output.js
js += '\n'
js += '//# sourceMappingURL=data:application/json;base64,'
js += new Buffer(output.v3SourceMap).toString('base64')
js += '\n'
return js
}

View File

@@ -182,9 +182,20 @@ class CommandRegistry
stopImmediatePropagation: value: ->
@handleCommandEvent(eventWithTarget)
# Public: Invoke the given callback before dispatching a command event.
#
# * `callback` {Function} to be called before dispatching each command
# * `event` The Event that will be dispatched
onWillDispatch: (callback) ->
@emitter.on 'will-dispatch', callback
# Public: Invoke the given callback after dispatching a command event.
#
# * `callback` {Function} to be called after dispatching each command
# * `event` The Event that was dispatched
onDidDispatch: (callback) ->
@emitter.on 'did-dispatch', callback
getSnapshot: ->
snapshot = {}
for commandName, listeners of @selectorBasedListenersByCommandName
@@ -239,6 +250,8 @@ class CommandRegistry
break if propagationStopped
currentTarget = currentTarget.parentNode ? window
@emitter.emit 'did-dispatch', syntheticEvent
matched
commandRegistered: (commandName) ->

View File

@@ -1,30 +0,0 @@
path = require 'path'
CSON = require 'season'
CoffeeCache = require 'coffee-cash'
babel = require './babel'
typescript = require './typescript'
# This file is required directly by apm so that files can be cached during
# package install so that the first package load in Atom doesn't have to
# compile anything.
exports.addPathToCache = (filePath, atomHome) ->
atomHome ?= process.env.ATOM_HOME
cacheDir = path.join(atomHome, 'compile-cache')
# Use separate compile cache when sudo'ing as root to avoid permission issues
if process.env.USER is 'root' and process.env.SUDO_USER and process.env.SUDO_USER isnt process.env.USER
cacheDir = path.join(cacheDir, 'root')
CoffeeCache.setCacheDirectory(path.join(cacheDir, 'coffee'))
CSON.setCacheDir(path.join(cacheDir, 'cson'))
babel.setCacheDirectory(path.join(cacheDir, 'js', 'babel'))
typescript.setCacheDirectory(path.join(cacheDir, 'ts'))
switch path.extname(filePath)
when '.coffee'
CoffeeCache.addPathToCache(filePath)
when '.cson'
CSON.readFileSync(filePath)
when '.js'
babel.addPathToCache(filePath)
when '.ts'
typescript.addPathToCache(filePath)

174
src/compile-cache.js Normal file
View File

@@ -0,0 +1,174 @@
'use strict'
var path = require('path')
var fs = require('fs-plus')
var CSON = null
var COMPILERS = {
'.js': require('./babel'),
'.ts': require('./typescript'),
'.coffee': require('./coffee-script')
}
var cacheStats = {}
var cacheDirectory = null
exports.setAtomHomeDirectory = function (atomHome) {
var cacheDir = path.join(atomHome, 'compile-cache')
if (process.env.USER === 'root' && process.env.SUDO_USER && process.env.SUDO_USER !== process.env.USER) {
cacheDir = path.join(cacheDir, 'root')
}
this.setCacheDirectory(cacheDir)
}
exports.setCacheDirectory = function (directory) {
cacheDirectory = directory
}
exports.getCacheDirectory = function () {
return cacheDirectory
}
exports.addPathToCache = function (filePath, atomHome) {
this.setAtomHomeDirectory(atomHome)
var extension = path.extname(filePath)
if (extension === '.cson') {
if (!CSON) {
CSON = require('season')
CSON.setCacheDir(this.getCacheDirectory())
}
CSON.readFileSync(filePath)
} else {
var compiler = COMPILERS[extension]
if (compiler) {
compileFileAtPath(compiler, filePath, extension)
}
}
}
exports.getCacheStats = function () {
return cacheStats
}
exports.resetCacheStats = function () {
Object.keys(COMPILERS).forEach(function (extension) {
cacheStats[extension] = {
hits: 0,
misses: 0
}
})
}
function compileFileAtPath (compiler, filePath, extension) {
var sourceCode = fs.readFileSync(filePath, 'utf8')
if (compiler.shouldCompile(sourceCode, filePath)) {
var cachePath = compiler.getCachePath(sourceCode, filePath)
var compiledCode = readCachedJavascript(cachePath)
if (compiledCode != null) {
cacheStats[extension].hits++
} else {
cacheStats[extension].misses++
compiledCode = addSourceURL(compiler.compile(sourceCode, filePath), filePath)
writeCachedJavascript(cachePath, compiledCode)
}
return compiledCode
}
return sourceCode
}
function readCachedJavascript (relativeCachePath) {
var cachePath = path.join(cacheDirectory, relativeCachePath)
if (fs.isFileSync(cachePath)) {
try {
return fs.readFileSync(cachePath, 'utf8')
} catch (error) {}
}
return null
}
function writeCachedJavascript (relativeCachePath, code) {
var cachePath = path.join(cacheDirectory, relativeCachePath)
fs.writeFileSync(cachePath, code, 'utf8')
}
function addSourceURL (jsCode, filePath) {
if (process.platform === 'win32') {
filePath = '/' + path.resolve(filePath).replace(/\\/g, '/')
}
return jsCode + '\n' + '//# sourceURL=' + encodeURI(filePath) + '\n'
}
var INLINE_SOURCE_MAP_REGEXP = /\/\/[#@]\s*sourceMappingURL=([^'"\n]+)\s*$/mg
require('source-map-support').install({
handleUncaughtExceptions: false,
// Most of this logic is the same as the default implementation in the
// source-map-support module, but we've overridden it to read the javascript
// code from our cache directory.
retrieveSourceMap: function (filePath) {
if (!cacheDirectory || !fs.isFileSync(filePath)) {
return null
}
try {
var sourceCode = fs.readFileSync(filePath, 'utf8')
} catch (error) {
console.warn('Error reading source file', error.stack)
return null
}
var compiler = COMPILERS[path.extname(filePath)]
try {
var fileData = readCachedJavascript(compiler.getCachePath(sourceCode, filePath))
} catch (error) {
console.warn('Error reading compiled file', error.stack)
return null
}
if (fileData == null) {
return null
}
var match, lastMatch
INLINE_SOURCE_MAP_REGEXP.lastIndex = 0
while ((match = INLINE_SOURCE_MAP_REGEXP.exec(fileData))) {
lastMatch = match
}
if (lastMatch == null) {
return null
}
var sourceMappingURL = lastMatch[1]
var rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(',') + 1)
try {
var sourceMap = JSON.parse(new Buffer(rawData, 'base64'))
} catch (error) {
console.warn('Error parsing source map', error.stack)
return null
}
return {
map: sourceMap,
url: null
}
}
})
Object.keys(COMPILERS).forEach(function (extension) {
var compiler = COMPILERS[extension]
Object.defineProperty(require.extensions, extension, {
enumerable: true,
writable: false,
value: function (module, filePath) {
var code = compileFileAtPath(compiler, filePath, extension)
return module._compile(code, filePath)
}
})
})
exports.resetCacheStats()

View File

@@ -26,6 +26,14 @@ module.exports =
default: []
items:
type: 'string'
customFileTypes:
type: 'object'
default: {}
description: 'Associates scope names (e.g. "source.js") with arrays of file extensions and file names (e.g. ["Somefile", ".js2"])'
additionalProperties:
type: 'array'
items:
type: 'string'
themes:
type: 'array'
default: ['one-dark-ui', 'one-dark-syntax']
@@ -81,6 +89,10 @@ module.exports =
'windows1258',
'windows866'
]
openEmptyEditorOnStart:
description: 'Automatically opens an empty editor when atom starts.'
type: 'boolean'
default: true
editor:
type: 'object'
@@ -143,6 +155,11 @@ module.exports =
softTabs:
type: 'boolean'
default: true
tabType:
type: 'string'
default: 'auto'
enum: ['auto', 'soft', 'hard']
description: 'Determine character inserted during Tab keypress.'
softWrapAtPreferredLineLength:
type: 'boolean'
default: false

View File

@@ -283,6 +283,17 @@ ScopeDescriptor = require './scope-descriptor'
# __Note__: You should strive to be so clear in your naming of the setting that
# you do not need to specify a title or description!
#
# Descriptions allow a subset of
# [Markdown formatting](https://help.github.com/articles/github-flavored-markdown/).
# Specifically, you may use the following in configuration setting descriptions:
#
# * **bold** - `**bold**`
# * *italics* - `*italics*`
# * [links](https://atom.io) - `[links](https://atom.io)`
# * `code spans` - `\`code spans\``
# * line breaks - `line breaks<br/>`
# * ~~strikethrough~~ - `~~strikethrough~~`
#
# ## Best practices
#
# * Don't depend on (or write to) configuration keys outside of your keypath.
@@ -664,13 +675,24 @@ class Config
# * `keyPath` The {String} name of the key.
#
# Returns an {Object} eg. `{type: 'integer', default: 23, minimum: 1}`.
# Returns `null` when the keyPath has no schema specified.
# Returns `null` when the keyPath has no schema specified, but is accessible
# from the root schema.
getSchema: (keyPath) ->
keys = splitKeyPath(keyPath)
schema = @schema
for key in keys
break unless schema?
schema = schema.properties?[key]
if schema.type is 'object'
childSchema = schema.properties?[key]
unless childSchema?
if isPlainObject(schema.additionalProperties)
childSchema = schema.additionalProperties
else if schema.additionalProperties is false
return null
else
return {type: 'any'}
else
return null
schema = childSchema
schema
# Extended: Get the {String} path to the config file being used.
@@ -843,7 +865,7 @@ class Config
if value?
value = @deepClone(value)
_.defaults(value, defaultValue) if isPlainObject(value) and isPlainObject(defaultValue)
@deepDefaults(value, defaultValue) if isPlainObject(value) and isPlainObject(defaultValue)
else
value = @deepClone(defaultValue)
@@ -906,6 +928,19 @@ class Config
else
object
deepDefaults: (target) ->
result = target
i = 0
while ++i < arguments.length
object = arguments[i]
if isPlainObject(result) and isPlainObject(object)
for key in Object.keys(object)
result[key] = @deepDefaults(result[key], object[key])
else
if not result?
result = @deepClone(object)
result
# `schema` will look something like this
#
# ```coffee
@@ -948,8 +983,9 @@ class Config
catch e
undefined
else
value = @constructor.executeSchemaEnforcers(keyPath, value, schema) if schema = @getSchema(keyPath)
value
unless (schema = @getSchema(keyPath))?
throw new Error("Illegal key path #{keyPath}") if schema is false
@constructor.executeSchemaEnforcers(keyPath, value, schema)
# When the schema is changed / added, there may be values set in the config
# that do not conform to the schema. This will reset make them conform.
@@ -1027,6 +1063,10 @@ class Config
# order of specification. Then the `*` enforcers will be run, in order of
# specification.
Config.addSchemaEnforcers
'any':
coerce: (keyPath, value, schema) ->
value
'integer':
coerce: (keyPath, value, schema) ->
value = parseInt(value)
@@ -1077,17 +1117,26 @@ Config.addSchemaEnforcers
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be an object") unless isPlainObject(value)
return value unless schema.properties?
defaultChildSchema = null
allowsAdditionalProperties = true
if isPlainObject(schema.additionalProperties)
defaultChildSchema = schema.additionalProperties
if schema.additionalProperties is false
allowsAdditionalProperties = false
newValue = {}
for prop, propValue of value
childSchema = schema.properties[prop]
childSchema = schema.properties[prop] ? defaultChildSchema
if childSchema?
try
newValue[prop] = @executeSchemaEnforcers("#{keyPath}.#{prop}", propValue, childSchema)
catch error
console.warn "Error setting item in object: #{error.message}"
else
else if allowsAdditionalProperties
# Just pass through un-schema'd values
newValue[prop] = propValue
else
console.warn "Illegal object key: #{keyPath}.#{prop}"
newValue

View File

@@ -86,9 +86,14 @@ class ContextMenuManager
# * `label` (Optional) A {String} containing the menu item's label.
# * `command` (Optional) A {String} containing the command to invoke on the
# target of the right click that invoked the context menu.
# * `enabled` (Optional) A {Boolean} indicating whether the menu item
# should be clickable. Disabled menu items typically appear grayed out.
# Defaults to `true`.
# * `submenu` (Optional) An {Array} of additional items.
# * `type` (Optional) If you want to create a separator, provide an item
# with `type: 'separator'` and no other keys.
# * `visible` (Optional) A {Boolean} indicating whether the menu item
# should appear in the menu. Defaults to `true`.
# * `created` (Optional) A {Function} that is called on the item each time a
# context menu is created via a right click. You can assign properties to
# `this` to dynamically compute the command, label, etc. This method is

View File

@@ -177,21 +177,18 @@ class DisplayBuffer extends Model
# visible - A {Boolean} indicating of the tokenized buffer is shown
setVisible: (visible) -> @tokenizedBuffer.setVisible(visible)
getVerticalScrollMargin: -> Math.min(@verticalScrollMargin, (@getHeight() - @getLineHeightInPixels()) / 2)
getVerticalScrollMargin: ->
maxScrollMargin = Math.floor(((@getHeight() / @getLineHeightInPixels()) - 1) / 2)
Math.min(@verticalScrollMargin, maxScrollMargin)
setVerticalScrollMargin: (@verticalScrollMargin) -> @verticalScrollMargin
getVerticalScrollMarginInPixels: ->
scrollMarginInPixels = @getVerticalScrollMargin() * @getLineHeightInPixels()
maxScrollMarginInPixels = (@getHeight() - @getLineHeightInPixels()) / 2
Math.min(scrollMarginInPixels, maxScrollMarginInPixels)
getVerticalScrollMarginInPixels: -> @getVerticalScrollMargin() * @getLineHeightInPixels()
getHorizontalScrollMargin: -> Math.min(@horizontalScrollMargin, (@getWidth() - @getDefaultCharWidth()) / 2)
getHorizontalScrollMargin: -> Math.min(@horizontalScrollMargin, Math.floor(((@getWidth() / @getDefaultCharWidth()) - 1) / 2))
setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin
getHorizontalScrollMarginInPixels: ->
scrollMarginInPixels = @getHorizontalScrollMargin() * @getDefaultCharWidth()
maxScrollMarginInPixels = (@getWidth() - @getDefaultCharWidth()) / 2
Math.min(scrollMarginInPixels, maxScrollMarginInPixels)
getHorizontalScrollMarginInPixels: -> scrollMarginInPixels = @getHorizontalScrollMargin() * @getDefaultCharWidth()
getHorizontalScrollbarHeight: -> @horizontalScrollbarHeight
setHorizontalScrollbarHeight: (@horizontalScrollbarHeight) -> @horizontalScrollbarHeight

View File

@@ -1,7 +1,11 @@
_ = require 'underscore-plus'
{Emitter} = require 'event-kit'
{includeDeprecatedAPIs, deprecate} = require 'grim'
FirstMate = require 'first-mate'
Token = require './token'
fs = require 'fs-plus'
PathSplitRegex = new RegExp("[/.]")
# Extended: Syntax class holding the grammars used for tokenizing.
#
@@ -39,7 +43,7 @@ class GrammarRegistry extends FirstMate.GrammarRegistry
bestMatch = null
highestScore = -Infinity
for grammar in @grammars
score = grammar.getScore(filePath, fileContents)
score = @getGrammarScore(grammar, filePath, fileContents)
if score > highestScore or not bestMatch?
bestMatch = grammar
highestScore = score
@@ -47,6 +51,90 @@ class GrammarRegistry extends FirstMate.GrammarRegistry
bestMatch = grammar unless grammar.bundledPackage
bestMatch
# Extended: Returns a {Number} representing how well the grammar matches the
# `filePath` and `contents`.
getGrammarScore: (grammar, filePath, contents) ->
contents = fs.readFileSync(filePath, 'utf8') if not contents? and fs.isFileSync(filePath)
if @grammarOverrideForPath(filePath) is grammar.scopeName
2 + (filePath?.length ? 0)
else if @grammarMatchesContents(grammar, contents)
1 + (filePath?.length ? 0)
else
@getGrammarPathScore(grammar, filePath)
getGrammarPathScore: (grammar, filePath) ->
return -1 unless filePath
filePath = filePath.replace(/\\/g, '/') if process.platform is 'win32'
pathComponents = filePath.toLowerCase().split(PathSplitRegex)
pathScore = -1
fileTypes = grammar.fileTypes
if customFileTypes = atom.config.get('core.customFileTypes')?[grammar.scopeName]
fileTypes = fileTypes.concat(customFileTypes)
for fileType, i in fileTypes
fileTypeComponents = fileType.toLowerCase().split(PathSplitRegex)
pathSuffix = pathComponents[-fileTypeComponents.length..-1]
if _.isEqual(pathSuffix, fileTypeComponents)
pathScore = Math.max(pathScore, fileType.length)
if i >= grammar.fileTypes.length
pathScore += 0.5
pathScore
grammarMatchesContents: (grammar, contents) ->
return false unless contents? and grammar.firstLineRegex?
escaped = false
numberOfNewlinesInRegex = 0
for character in grammar.firstLineRegex.source
switch character
when '\\'
escaped = not escaped
when 'n'
numberOfNewlinesInRegex++ if escaped
escaped = false
else
escaped = false
lines = contents.split('\n')
grammar.firstLineRegex.testSync(lines[0..numberOfNewlinesInRegex].join('\n'))
# Public: Get the grammar override for the given file path.
#
# * `filePath` A {String} file path.
#
# Returns a {Grammar} or undefined.
grammarOverrideForPath: (filePath) ->
@grammarOverridesByPath[filePath]
# Public: Set the grammar override for the given file path.
#
# * `filePath` A non-empty {String} file path.
# * `scopeName` A {String} such as `"source.js"`.
#
# Returns a {Grammar} or undefined.
setGrammarOverrideForPath: (filePath, scopeName) ->
if filePath
@grammarOverridesByPath[filePath] = scopeName
# Public: Remove the grammar override for the given file path.
#
# * `filePath` A {String} file path.
#
# Returns undefined.
clearGrammarOverrideForPath: (filePath) ->
delete @grammarOverridesByPath[filePath]
undefined
# Public: Remove all grammar overrides.
#
# Returns undefined.
clearGrammarOverrides: ->
@grammarOverridesByPath = {}
undefined
clearObservers: ->
@off() if includeDeprecatedAPIs
@emitter = new Emitter

View File

@@ -1,29 +1,22 @@
{Emitter} = require 'event-kit'
Gutter = require './gutter'
# This class encapsulates the logic for adding and modifying a set of gutters.
module.exports =
class GutterContainer
# * `textEditor` The {TextEditor} to which this {GutterContainer} belongs.
constructor: (textEditor) ->
@gutters = []
@textEditor = textEditor
@emitter = new Emitter
destroy: ->
@gutters = null
# Create a copy, because `Gutter::destroy` removes the gutter from
# GutterContainer's @gutters.
guttersToDestroy = @gutters.slice(0)
for gutter in guttersToDestroy
gutter.destroy() if gutter.name isnt 'line-number'
@gutters = []
@emitter.dispose()
# Creates and returns a {Gutter}.
# * `options` An {Object} with the following fields:
# * `name` (required) A unique {String} to identify this gutter.
# * `priority` (optional) A {Number} that determines stacking order between
# gutters. Lower priority items are forced closer to the edges of the
# window. (default: -100)
# * `visible` (optional) {Boolean} specifying whether the gutter is visible
# initially after being created. (default: true)
addGutter: (options) ->
options = options ? {}
gutterName = options.name
@@ -54,20 +47,13 @@ class GutterContainer
if gutter.name is name then return gutter
null
###
Section: Event Subscription
###
# See {TextEditor::observeGutters} for details.
observeGutters: (callback) ->
callback(gutter) for gutter in @getGutters()
@onDidAddGutter callback
# See {TextEditor::onDidAddGutter} for details.
onDidAddGutter: (callback) ->
@emitter.on 'did-add-gutter', callback
# See {TextEditor::onDidRemoveGutter} for details.
onDidRemoveGutter: (callback) ->
@emitter.on 'did-remove-gutter', callback

View File

@@ -1,19 +1,12 @@
{Emitter} = require 'event-kit'
# Public: This class represents a gutter within a TextEditor.
DefaultPriority = -100
# Extended: Represents a gutter within a {TextEditor}.
#
# See {TextEditor::addGutter} for information on creating a gutter.
module.exports =
class Gutter
# * `gutterContainer` The {GutterContainer} object to which this gutter belongs.
# * `options` An {Object} with the following fields:
# * `name` (required) A unique {String} to identify this gutter.
# * `priority` (optional) A {Number} that determines stacking order between
# gutters. Lower priority items are forced closer to the edges of the
# window. (default: -100)
# * `visible` (optional) {Boolean} specifying whether the gutter is visible
# initially after being created. (default: true)
constructor: (gutterContainer, options) ->
@gutterContainer = gutterContainer
@name = options?.name
@@ -22,6 +15,11 @@ class Gutter
@emitter = new Emitter
###
Section: Gutter Destruction
###
# Essential: Destroys the gutter.
destroy: ->
if @name is 'line-number'
throw new Error('The line-number gutter cannot be destroyed.')
@@ -30,42 +28,65 @@ class Gutter
@emitter.emit 'did-destroy'
@emitter.dispose()
hide: ->
if @visible
@visible = false
@emitter.emit 'did-change-visible', this
###
Section: Event Subscription
###
show: ->
if not @visible
@visible = true
@emitter.emit 'did-change-visible', this
isVisible: ->
@visible
# * `marker` (required) A Marker object.
# * `options` (optional) An object with the following fields:
# * `class` (optional)
# * `item` (optional) A model {Object} with a corresponding view registered,
# or an {HTMLElement}.
#
# Returns a {Decoration} object.
decorateMarker: (marker, options) ->
@gutterContainer.addGutterDecoration(this, marker, options)
# Calls your `callback` when the {Gutter}'s' visibility changes.
# Essential: Calls your `callback` when the gutter's visibility changes.
#
# * `callback` {Function}
# * `gutter` The {Gutter} whose visibility changed.
# * `gutter` The gutter whose visibility changed.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeVisible: (callback) ->
@emitter.on 'did-change-visible', callback
# Calls your `callback` when the {Gutter} is destroyed
# Essential: Calls your `callback` when the gutter is destroyed.
#
# * `callback` {Function}
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidDestroy: (callback) ->
@emitter.on 'did-destroy', callback
###
Section: Visibility
###
# Essential: Hide the gutter.
hide: ->
if @visible
@visible = false
@emitter.emit 'did-change-visible', this
# Essential: Show the gutter.
show: ->
if not @visible
@visible = true
@emitter.emit 'did-change-visible', this
# Essential: Determine whether the gutter is visible.
#
# Returns a {Boolean}.
isVisible: ->
@visible
# Essential: Add a decoration that tracks a {Marker}. When the marker moves,
# is invalidated, or is destroyed, the decoration will be updated to reflect
# the marker's state.
#
# ## Arguments
#
# * `marker` A {Marker} you want this decoration to follow.
# * `decorationParams` An {Object} representing the decoration
# * `class` This CSS class will be applied to the decorated line number.
# * `onlyHead` (optional) If `true`, the decoration will only be applied to
# the head of the marker.
# * `onlyEmpty` (optional) If `true`, the decoration will only be applied if
# the associated marker is empty.
# * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied
# if the associated marker is non-empty.
#
# Returns a {Decoration} object
decorateMarker: (marker, options) ->
@gutterContainer.addGutterDecoration(this, marker, options)

View File

@@ -47,9 +47,9 @@ class LineNumberGutterComponent extends TiledComponent
beforeUpdateSync: (state) ->
@appendDummyLineNumber() unless @dummyLineNumberNode?
if @newState.styles.scrollHeight isnt @oldState.styles.scrollHeight
@lineNumbersNode.style.height = @newState.styles.scrollHeight + 'px'
@oldState.scrollHeight = @newState.scrollHeight
if @newState.styles.maxHeight isnt @oldState.styles.maxHeight
@lineNumbersNode.style.height = @newState.styles.maxHeight + 'px'
@oldState.maxHeight = @newState.maxHeight
if @newState.styles.backgroundColor isnt @oldState.styles.backgroundColor
@lineNumbersNode.style.backgroundColor = @newState.styles.backgroundColor

View File

@@ -42,6 +42,10 @@ class LineNumbersTileComponent
@domNode.style['-webkit-transform'] = "translate3d(0, #{@newTileState.top}px, 0px)"
@oldTileState.top = @newTileState.top
if @newTileState.zIndex isnt @oldTileState.zIndex
@domNode.style.zIndex = @newTileState.zIndex
@oldTileState.zIndex = @newTileState.zIndex
if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits
node.remove() for id, node of @lineNumberNodesById
@oldState.tiles[@id] = {lineNumbers: {}}
@@ -84,9 +88,9 @@ class LineNumbersTileComponent
return
buildLineNumberHTML: (lineNumberState) ->
{screenRow, bufferRow, softWrapped, top, decorationClasses} = lineNumberState
{screenRow, bufferRow, softWrapped, top, decorationClasses, zIndex} = lineNumberState
if screenRow?
style = "position: absolute; top: #{top}px;"
style = "position: absolute; top: #{top}px; z-index: #{zIndex};"
else
style = "visibility: hidden;"
className = @buildLineNumberClassName(lineNumberState)
@@ -121,6 +125,10 @@ class LineNumbersTileComponent
oldLineNumberState.top = newLineNumberState.top
oldLineNumberState.screenRow = newLineNumberState.screenRow
unless oldLineNumberState.zIndex is newLineNumberState.zIndex
node.style.zIndex = newLineNumberState.zIndex
oldLineNumberState.zIndex = newLineNumberState.zIndex
buildLineNumberClassName: ({bufferRow, foldable, decorationClasses, softWrapped}) ->
className = "line-number line-number-#{bufferRow}"
className += " " + decorationClasses.join(' ') if decorationClasses?

View File

@@ -35,9 +35,9 @@ class LinesComponent extends TiledComponent
@oldState.indentGuidesVisible isnt @newState.indentGuidesVisible
beforeUpdateSync: (state) ->
if @newState.scrollHeight isnt @oldState.scrollHeight
@domNode.style.height = @newState.scrollHeight + 'px'
@oldState.scrollHeight = @newState.scrollHeight
if @newState.maxHeight isnt @oldState.maxHeight
@domNode.style.height = @newState.maxHeight + 'px'
@oldState.maxHeight = @newState.maxHeight
if @newState.backgroundColor isnt @oldState.backgroundColor
@domNode.style.backgroundColor = @newState.backgroundColor

View File

@@ -92,10 +92,14 @@ class Marker
#
# * `callback` {Function} to be called when the marker changes.
# * `event` {Object} with the following keys:
# * `oldHeadPosition` {Point} representing the former head position
# * `newHeadPosition` {Point} representing the new head position
# * `oldTailPosition` {Point} representing the former tail position
# * `newTailPosition` {Point} representing the new tail position
# * `oldHeadBufferPosition` {Point} representing the former head buffer position
# * `newHeadBufferPosition` {Point} representing the new head buffer position
# * `oldTailBufferPosition` {Point} representing the former tail buffer position
# * `newTailBufferPosition` {Point} representing the new tail buffer position
# * `oldHeadScreenPosition` {Point} representing the former head screen position
# * `newHeadScreenPosition` {Point} representing the new head screen position
# * `oldTailScreenPosition` {Point} representing the former tail screen position
# * `newTailScreenPosition` {Point} representing the new tail screen position
# * `wasValid` {Boolean} indicating whether the marker was valid before the change
# * `isValid` {Boolean} indicating whether the marker is now valid
# * `hadTail` {Boolean} indicating whether the marker had a tail before the change

View File

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

View File

@@ -82,21 +82,26 @@ class Pane extends Model
Section: Event Subscription
###
# Public: Invoke the given callback when the pane resize
# Public: Invoke the given callback when the pane resizes
#
# the callback will be invoked when pane's flexScale property changes
# The callback will be invoked when pane's flexScale property changes.
# Use {::getFlexScale} to get the current value.
#
# * `callback` {Function} to be called when the pane is resized
# * `flexScale` {Number} representing the panes `flex-grow`; ability for a
# flex item to grow if necessary.
#
# Returns a {Disposable} on which '.dispose()' can be called to unsubscribe.
onDidChangeFlexScale: (callback) ->
@emitter.on 'did-change-flex-scale', callback
# Public: Invoke the given callback with all current and future items.
# Public: Invoke the given callback with the current and future values of
# {::getFlexScale}.
#
# * `callback` {Function} to be called with current and future items.
# * `item` An item that is present in {::getItems} at the time of
# subscription or that is added at some later time.
# * `callback` {Function} to be called with the current and future values of
# the {::getFlexScale} property.
# * `flexScale` {Number} representing the panes `flex-grow`; ability for a
# flex item to grow if necessary.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
observeFlexScale: (callback) ->

View File

@@ -34,12 +34,11 @@ class Project extends Model
@rootDirectories = []
@repositories = []
@directoryProviders = [new DefaultDirectoryProvider()]
@directoryProviders = []
@defaultDirectoryProvider = new DefaultDirectoryProvider()
atom.packages.serviceHub.consume(
'atom.directory-provider',
'^0.1.0',
# New providers are added to the front of @directoryProviders because
# DefaultDirectoryProvider is a catch-all that will always provide a Directory.
(provider) => @directoryProviders.unshift(provider))
# Mapping from the real path of a {Directory} to a {Promise} that resolves
@@ -48,8 +47,6 @@ class Project extends Model
# the same real path, so it is not a good key.
@repositoryPromisesByPath = new Map()
# Note that the GitRepositoryProvider is registered synchronously so that
# it is available immediately on startup.
@repositoryProviders = [new GitRepositoryProvider(this)]
atom.packages.serviceHub.consume(
'atom.repository-provider',
@@ -186,18 +183,16 @@ class Project extends Model
#
# * `projectPath` {String} The path to the directory to add.
addPath: (projectPath, options) ->
for directory in @getDirectories()
# Apparently a Directory does not believe it can contain itself, so we
# must also check whether the paths match.
return if directory.contains(projectPath) or directory.getPath() is projectPath
directory = null
for provider in @directoryProviders
break if directory = provider.directoryForURISync?(projectPath)
if directory is null
# This should never happen because DefaultDirectoryProvider should always
# return a Directory.
throw new Error(projectPath + ' could not be resolved to a directory')
directory ?= @defaultDirectoryProvider.directoryForURISync(projectPath)
directoryExists = directory.existsSync()
for rootDirectory in @getDirectories()
return if rootDirectory.getPath() is directory.getPath()
return if not directoryExists and rootDirectory.contains(directory.getPath())
@rootDirectories.push(directory)
repo = null
@@ -267,10 +262,13 @@ class Project extends Model
# * `relativePath` {String} The relative path from the project directory to
# the given path.
relativizePath: (fullPath) ->
for rootDirectory in @rootDirectories
relativePath = rootDirectory.relativize(fullPath)
return [rootDirectory.getPath(), relativePath] unless relativePath is fullPath
[null, fullPath]
result = [null, fullPath]
if fullPath?
for rootDirectory in @rootDirectories
relativePath = rootDirectory.relativize(fullPath)
if relativePath?.length < result[1].length
result = [rootDirectory.getPath(), relativePath]
result
# Public: Determines whether the given path (real or symbolic) is inside the
# project's directory.

View File

@@ -190,7 +190,7 @@ class Selection extends Model
# position.
#
# * `position` An instance of {Point}, with a given `row` and `column`.
selectToScreenPosition: (position) ->
selectToScreenPosition: (position, options) ->
position = Point.fromObject(position)
@modifySelection =>
@@ -200,12 +200,12 @@ class Selection extends Model
else
@marker.setScreenRange([@initialScreenRange.start, position], reversed: false)
else
@cursor.setScreenPosition(position)
@cursor.setScreenPosition(position, options)
if @linewise
@expandOverLine()
@expandOverLine(options)
else if @wordwise
@expandOverWord()
@expandOverWord(options)
# Public: Selects the text from the current cursor position to a given buffer
# position.
@@ -311,28 +311,28 @@ class Selection extends Model
# Public: Modifies the selection to encompass the current word.
#
# Returns a {Range}.
selectWord: ->
options = {}
selectWord: (options={}) ->
options.wordRegex = /[\t ]*/ if @cursor.isSurroundedByWhitespace()
if @cursor.isBetweenWordAndNonWord()
options.includeNonWordCharacters = false
@setBufferRange(@cursor.getCurrentWordBufferRange(options))
@setBufferRange(@cursor.getCurrentWordBufferRange(options), options)
@wordwise = true
@initialScreenRange = @getScreenRange()
# Public: Expands the newest selection to include the entire word on which
# the cursors rests.
expandOverWord: ->
expandOverWord: (options) ->
@setBufferRange(@getBufferRange().union(@cursor.getCurrentWordBufferRange()), autoscroll: false)
@cursor.autoscroll()
@cursor.autoscroll() if options?.autoscroll ? true
# Public: Selects an entire line in the buffer.
#
# * `row` The line {Number} to select (default: the row of the cursor).
selectLine: (row=@cursor.getBufferPosition().row) ->
selectLine: (row, options) ->
row ?= @cursor.getBufferPosition().row
range = @editor.bufferRangeForBufferRow(row, includeNewline: true)
@setBufferRange(@getBufferRange().union(range), autoscroll: true)
@setBufferRange(@getBufferRange().union(range), options)
@linewise = true
@wordwise = false
@initialScreenRange = @getScreenRange()
@@ -341,10 +341,10 @@ class Selection extends Model
# the cursor currently rests.
#
# It also includes the newline character.
expandOverLine: ->
expandOverLine: (options) ->
range = @getBufferRange().union(@cursor.getCurrentLineBufferRange(includeNewline: true))
@setBufferRange(range, autoscroll: false)
@cursor.autoscroll()
@cursor.autoscroll() if options?.autoscroll ? true
###
Section: Modifying the selected text

View File

@@ -66,15 +66,11 @@ class Task
# * `taskPath` The {String} path to the CoffeeScript/JavaScript file that
# exports a single {Function} to execute.
constructor: (taskPath) ->
coffeeCacheRequire = "require('#{require.resolve('coffee-cash')}')"
coffeeCachePath = require('coffee-cash').getCacheDirectory()
coffeeStackRequire = "require('#{require.resolve('coffeestack')}')"
stackCachePath = require('coffeestack').getCacheDirectory()
compileCacheRequire = "require('#{require.resolve('./compile-cache')}')"
compileCachePath = require('./compile-cache').getCacheDirectory()
taskBootstrapRequire = "require('#{require.resolve('./task-bootstrap')}');"
bootstrap = """
#{coffeeCacheRequire}.setCacheDirectory('#{coffeeCachePath}');
#{coffeeCacheRequire}.register();
#{coffeeStackRequire}.setCacheDirectory('#{stackCachePath}');
#{compileCacheRequire}.setCacheDirectory('#{compileCachePath}');
#{taskBootstrapRequire}
"""
bootstrap = bootstrap.replace(/\\/g, "\\\\")

View File

@@ -241,13 +241,13 @@ class TextEditorComponent
# 4. compositionend fired
# 5. textInput fired; event.data == the completion string
selectedText = null
checkpoint = null
@domNode.addEventListener 'compositionstart', =>
selectedText = @editor.getSelectedText()
checkpoint = @editor.createCheckpoint()
@domNode.addEventListener 'compositionupdate', (event) =>
@editor.insertText(event.data, select: true, undo: 'skip')
@editor.insertText(event.data, select: true)
@domNode.addEventListener 'compositionend', (event) =>
@editor.insertText(selectedText, select: true, undo: 'skip')
@editor.revertToCheckpoint(checkpoint)
event.target.value = ''
# Listen for selection changes and store the currently selected text
@@ -395,16 +395,16 @@ class TextEditorComponent
if cursorAtScreenPosition and @editor.hasMultipleCursors()
cursorAtScreenPosition.destroy()
else
@editor.addCursorAtScreenPosition(screenPosition)
@editor.addCursorAtScreenPosition(screenPosition, autoscroll: false)
else
@editor.setCursorScreenPosition(screenPosition)
@editor.setCursorScreenPosition(screenPosition, autoscroll: false)
when 2
@editor.getLastSelection().selectWord()
@editor.getLastSelection().selectWord(autoscroll: false)
when 3
@editor.getLastSelection().selectLine()
@editor.getLastSelection().selectLine(null, autoscroll: false)
@handleDragUntilMouseUp event, (screenPosition) =>
@editor.selectToScreenPosition(screenPosition, true)
@handleDragUntilMouseUp (screenPosition) =>
@editor.selectToScreenPosition(screenPosition, suppressSelectionMerge: true, autoscroll: false)
onLineNumberGutterMouseDown: (event) =>
return unless event.button is 0 # only handle the left mouse button
@@ -419,61 +419,43 @@ class TextEditorComponent
@onGutterClick(event)
onGutterClick: (event) =>
clickedRow = @screenPositionForMouseEvent(event).row
clickedBufferRow = @editor.bufferRowForScreenRow(clickedRow)
@editor.setSelectedBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]], preserveFolds: true)
@handleDragUntilMouseUp event, (screenPosition) =>
dragRow = screenPosition.row
dragBufferRow = @editor.bufferRowForScreenRow(dragRow)
if dragBufferRow < clickedBufferRow # dragging up
@editor.setSelectedBufferRange([[dragBufferRow, 0], [clickedBufferRow + 1, 0]], reversed: true, preserveFolds: true, autoscroll: false)
else
@editor.setSelectedBufferRange([[clickedBufferRow, 0], [dragBufferRow + 1, 0]], reversed: false, preserveFolds: true, autoscroll: false)
@editor.getLastCursor().autoscroll()
clickedScreenRow = @screenPositionForMouseEvent(event).row
clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
@editor.setSelectedScreenRange(initialScreenRange, preserveFolds: true, autoscroll: false)
@handleGutterDrag(initialScreenRange)
onGutterMetaClick: (event) =>
clickedRow = @screenPositionForMouseEvent(event).row
clickedBufferRow = @editor.bufferRowForScreenRow(clickedRow)
bufferRange = new Range([clickedBufferRow, 0], [clickedBufferRow + 1, 0])
rowSelection = @editor.addSelectionForBufferRange(bufferRange, preserveFolds: true)
@handleDragUntilMouseUp event, (screenPosition) =>
dragRow = screenPosition.row
dragBufferRow = @editor.bufferRowForScreenRow(dragRow)
if dragBufferRow < clickedBufferRow # dragging up
rowSelection.setBufferRange([[dragBufferRow, 0], [clickedBufferRow + 1, 0]], preserveFolds: true)
else
rowSelection.setBufferRange([[clickedBufferRow, 0], [dragBufferRow + 1, 0]], preserveFolds: true)
# The merge process will possibly destroy the current selection because
# it will be merged into another one. Therefore, we need to obtain a
# reference to the new selection that contains the originally selected row
rowSelection = _.find @editor.getSelections(), (selection) ->
selection.intersectsBufferRange(bufferRange)
clickedScreenRow = @screenPositionForMouseEvent(event).row
clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
@editor.addSelectionForScreenRange(initialScreenRange, preserveFolds: true, autoscroll: false)
@handleGutterDrag(initialScreenRange)
onGutterShiftClick: (event) =>
clickedRow = @screenPositionForMouseEvent(event).row
clickedBufferRow = @editor.bufferRowForScreenRow(clickedRow)
tailPosition = @editor.getLastSelection().getTailScreenPosition()
tailBufferPosition = @editor.bufferPositionForScreenPosition(tailPosition)
tailScreenPosition = @editor.getLastSelection().getTailScreenPosition()
clickedScreenRow = @screenPositionForMouseEvent(event).row
clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
clickedLineScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
if clickedRow < tailPosition.row
@editor.selectToBufferPosition([clickedBufferRow, 0])
if clickedScreenRow < tailScreenPosition.row
@editor.selectToScreenPosition(clickedLineScreenRange.start, suppressSelectionMerge: true, autoscroll: false)
else
@editor.selectToBufferPosition([clickedBufferRow + 1, 0])
@editor.selectToScreenPosition(clickedLineScreenRange.end, suppressSelectionMerge: true, autoscroll: false)
@handleDragUntilMouseUp event, (screenPosition) =>
@handleGutterDrag(new Range(tailScreenPosition, tailScreenPosition))
handleGutterDrag: (initialRange) ->
@handleDragUntilMouseUp (screenPosition) =>
dragRow = screenPosition.row
dragBufferRow = @editor.bufferRowForScreenRow(dragRow)
if dragRow < tailPosition.row # dragging up
@editor.setSelectedBufferRange([[dragBufferRow, 0], tailBufferPosition], preserveFolds: true)
if dragRow < initialRange.start.row
startPosition = @editor.clipScreenPosition([dragRow, 0], skipSoftWrapIndentation: true)
screenRange = new Range(startPosition, startPosition).union(initialRange)
@editor.getLastSelection().setScreenRange(screenRange, reversed: true, autoscroll: false, preserveFolds: true)
else
@editor.setSelectedBufferRange([tailBufferPosition, [dragBufferRow + 1, 0]], preserveFolds: true)
endPosition = [dragRow + 1, 0]
screenRange = new Range(endPosition, endPosition).union(initialRange)
@editor.getLastSelection().setScreenRange(screenRange, reversed: false, autoscroll: false, preserveFolds: true)
onStylesheetsChanged: (styleElement) =>
return unless @performedInitialMeasurement
@@ -523,13 +505,15 @@ class TextEditorComponent
onCursorMoved: =>
@cursorMoved = true
handleDragUntilMouseUp: (event, dragHandler) =>
handleDragUntilMouseUp: (dragHandler) =>
dragging = false
lastMousePosition = {}
animationLoop = =>
@requestAnimationFrame =>
if dragging and @mounted
screenPosition = @screenPositionForMouseEvent(lastMousePosition)
linesClientRect = @linesComponent.getDomNode().getBoundingClientRect()
autoscroll(lastMousePosition, linesClientRect)
screenPosition = @screenPositionForMouseEvent(lastMousePosition, linesClientRect)
dragHandler(screenPosition)
animationLoop()
else if not @mounted
@@ -548,15 +532,47 @@ class TextEditorComponent
onMouseUp() if event.which is 0
onMouseUp = (event) =>
stopDragging()
@editor.finalizeSelections()
@editor.mergeIntersectingSelections()
if dragging
stopDragging()
@editor.finalizeSelections()
@editor.mergeIntersectingSelections()
pasteSelectionClipboard(event)
stopDragging = ->
dragging = false
window.removeEventListener('mousemove', onMouseMove)
window.removeEventListener('mouseup', onMouseUp)
disposables.dispose()
autoscroll = (mouseClientPosition) =>
{top, bottom, left, right} = @scrollViewNode.getBoundingClientRect()
top += 30
bottom -= 30
left += 30
right -= 30
if mouseClientPosition.clientY < top
mouseYDelta = top - mouseClientPosition.clientY
yDirection = -1
else if mouseClientPosition.clientY > bottom
mouseYDelta = mouseClientPosition.clientY - bottom
yDirection = 1
if mouseClientPosition.clientX < left
mouseXDelta = left - mouseClientPosition.clientX
xDirection = -1
else if mouseClientPosition.clientX > right
mouseXDelta = mouseClientPosition.clientX - right
xDirection = 1
if mouseYDelta?
@presenter.setScrollTop(@presenter.getScrollTop() + yDirection * scaleScrollDelta(mouseYDelta))
if mouseXDelta?
@presenter.setScrollLeft(@presenter.getScrollLeft() + xDirection * scaleScrollDelta(mouseXDelta))
scaleScrollDelta = (scrollDelta) ->
Math.pow(scrollDelta / 2, 3) / 280
pasteSelectionClipboard = (event) =>
if event?.which is 2 and process.platform is 'linux'
@@ -565,6 +581,9 @@ class TextEditorComponent
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp)
disposables = new CompositeDisposable
disposables.add(@editor.getBuffer().onWillChange(onMouseUp))
disposables.add(@editor.onDidDestroy(stopDragging))
isVisible: ->
@domNode.offsetHeight > 0 or @domNode.offsetWidth > 0
@@ -762,17 +781,20 @@ class TextEditorComponent
if scrollSensitivity = parseInt(scrollSensitivity)
@scrollSensitivity = Math.abs(scrollSensitivity) / 100
screenPositionForMouseEvent: (event) ->
pixelPosition = @pixelPositionForMouseEvent(event)
screenPositionForMouseEvent: (event, linesClientRect) ->
pixelPosition = @pixelPositionForMouseEvent(event, linesClientRect)
@editor.screenPositionForPixelPosition(pixelPosition)
pixelPositionForMouseEvent: (event) ->
pixelPositionForMouseEvent: (event, linesClientRect) ->
{clientX, clientY} = event
linesClientRect = @linesComponent.getDomNode().getBoundingClientRect()
linesClientRect ?= @linesComponent.getDomNode().getBoundingClientRect()
top = clientY - linesClientRect.top + @presenter.scrollTop
left = clientX - linesClientRect.left + @presenter.scrollLeft
{top, left}
bottom = linesClientRect.top + @presenter.scrollTop + linesClientRect.height - clientY
right = linesClientRect.left + @presenter.scrollLeft + linesClientRect.width - clientX
{top, left, bottom, right}
getModel: ->
@editor

View File

@@ -302,6 +302,7 @@ atom.commands.add 'atom-text-editor', stopEventPropagationAndGroupUndo(
'editor:transpose': -> @transpose()
'editor:upper-case': -> @upperCase()
'editor:lower-case': -> @lowerCase()
'editor:copy-selection': -> @copyOnlySelectedText()
)
atom.commands.add 'atom-text-editor:not([mini])', stopEventPropagation(

View File

@@ -304,6 +304,10 @@ class TextEditorPresenter
@state.hiddenInput.width = Math.max(width, 2)
updateContentState: ->
if @boundingClientRect?
@sharedGutterStyles.maxHeight = @boundingClientRect.height
@state.content.maxHeight = @boundingClientRect.height
@state.content.width = Math.max(@contentWidth + @verticalScrollbarWidth, @contentFrameWidth)
@state.content.scrollWidth = @scrollWidth
@state.content.scrollLeft = @scrollLeft
@@ -340,18 +344,20 @@ class TextEditorPresenter
tile.left = -@scrollLeft
tile.height = @tileSize * @lineHeight
tile.display = "block"
tile.zIndex = zIndex--
tile.zIndex = zIndex
tile.highlights ?= {}
gutterTile = @lineNumberGutter.tiles[startRow] ?= {}
gutterTile.top = startRow * @lineHeight - @scrollTop
gutterTile.height = @tileSize * @lineHeight
gutterTile.display = "block"
gutterTile.zIndex = zIndex
@updateLinesState(tile, startRow, endRow) if @shouldUpdateLinesState
@updateLineNumbersState(gutterTile, startRow, endRow) if @shouldUpdateLineNumbersState
visibleTiles[startRow] = true
zIndex--
if @mouseWheelScreenRow? and @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)?
mouseWheelTile = @tileForRow(@mouseWheelScreenRow)
@@ -410,19 +416,15 @@ class TextEditorPresenter
@updateCursorState(cursor) for cursor in @model.cursors # using property directly to avoid allocation
return
updateCursorState: (cursor, destroyOnly = false) ->
delete @state.content.cursors[cursor.id]
return if destroyOnly
updateCursorState: (cursor) ->
return unless @startRow? and @endRow? and @hasPixelRectRequirements() and @baseCharacterWidth?
return unless cursor.isVisible() and @startRow <= cursor.getScreenRow() < @endRow
screenRange = cursor.getScreenRange()
return unless cursor.isVisible() and @startRow <= screenRange.start.row < @endRow
pixelRect = @pixelRectForScreenRange(cursor.getScreenRange())
pixelRect = @pixelRectForScreenRange(screenRange)
pixelRect.width = @baseCharacterWidth if pixelRect.width is 0
@state.content.cursors[cursor.id] = pixelRect
@emitDidUpdateState()
updateOverlaysState: ->
return unless @hasOverlayPositionRequirements()
@@ -546,7 +548,7 @@ class TextEditorPresenter
@clearDecorationsForCustomGutterName(gutterName)
else
@customGutterDecorations[gutterName] = {}
return if not @gutterIsVisible(gutter)
continue if not @gutterIsVisible(gutter)
relevantDecorations = @customGutterDecorationsInRange(gutterName, @startRow, @endRow - 1)
relevantDecorations.forEach (decoration) =>
@@ -588,7 +590,9 @@ class TextEditorPresenter
wrapCount = 0
if endRow > startRow
for bufferRow, i in @model.bufferRowsForScreenRows(startRow, endRow - 1)
bufferRows = @model.bufferRowsForScreenRows(startRow, endRow - 1)
zIndex = bufferRows.length - 1
for bufferRow, i in bufferRows
if bufferRow is lastBufferRow
wrapCount++
id = bufferRow + '-' + wrapCount
@@ -604,8 +608,9 @@ class TextEditorPresenter
decorationClasses = @lineNumberDecorationClassesForRow(screenRow)
foldable = @model.isFoldableAtScreenRow(screenRow)
tileState.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable}
tileState.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable, zIndex}
visibleLineNumberIds[id] = true
zIndex--
for id of tileState.lineNumbers
delete tileState.lineNumbers[id] unless visibleLineNumberIds[id]
@@ -923,6 +928,7 @@ class TextEditorPresenter
unless @clientRectsEqual(@boundingClientRect, boundingClientRect)
@boundingClientRect = boundingClientRect
@shouldUpdateOverlaysState = true
@shouldUpdateContentState = true
@emitDidUpdateState()
@@ -1166,7 +1172,7 @@ class TextEditorPresenter
if decoration.isType('line') or decoration.isType('gutter')
@addToLineDecorationCaches(decoration, range)
else if decoration.isType('highlight')
@updateHighlightState(decoration)
@updateHighlightState(decoration, range)
for tileId, tileState of @state.content.tiles
for id, highlight of tileState.highlights
@@ -1237,12 +1243,11 @@ class TextEditorPresenter
intersectingRange
updateHighlightState: (decoration) ->
updateHighlightState: (decoration, range) ->
return unless @startRow? and @endRow? and @lineHeight? and @hasPixelPositionRequirements()
properties = decoration.getProperties()
marker = decoration.getMarker()
range = marker.getScreenRange()
if decoration.isDestroyed() or not marker.isValid() or range.isEmpty() or not range.intersectsRowRange(@startRow, @endRow - 1)
return
@@ -1365,20 +1370,22 @@ class TextEditorPresenter
observeCursor: (cursor) ->
didChangePositionDisposable = cursor.onDidChangePosition =>
@shouldUpdateHiddenInputState = true if cursor.isLastCursor()
@shouldUpdateCursorsState = true
@pauseCursorBlinking()
@updateCursorState(cursor)
@emitDidUpdateState()
didChangeVisibilityDisposable = cursor.onDidChangeVisibility =>
@updateCursorState(cursor)
@shouldUpdateCursorsState = true
@emitDidUpdateState()
didDestroyDisposable = cursor.onDidDestroy =>
@disposables.remove(didChangePositionDisposable)
@disposables.remove(didChangeVisibilityDisposable)
@disposables.remove(didDestroyDisposable)
@shouldUpdateHiddenInputState = true
@updateCursorState(cursor, true)
@shouldUpdateCursorsState = true
@emitDidUpdateState()
@@ -1389,8 +1396,9 @@ class TextEditorPresenter
didAddCursor: (cursor) ->
@observeCursor(cursor)
@shouldUpdateHiddenInputState = true
@shouldUpdateCursorsState = true
@pauseCursorBlinking()
@updateCursorState(cursor)
@emitDidUpdateState()
startBlinkingCursors: ->

View File

@@ -14,7 +14,7 @@ TextMateScopeSelector = require('first-mate').ScopeSelector
{Directory} = require "pathwatcher"
GutterContainer = require './gutter-container'
# Public: This class represents all essential editing state for a single
# Essential: This class represents all essential editing state for a single
# {TextBuffer}, including cursor and selection positions, folds, and soft wraps.
# If you're manipulating the state of an editor, use this class. If you're
# interested in the visual appearance of editors, use {TextEditorView} instead.
@@ -86,16 +86,16 @@ class TextEditor extends Model
buffer ?= new TextBuffer
@displayBuffer ?= new DisplayBuffer({buffer, tabLength, softWrapped, ignoreInvisibles: @mini, largeFileMode})
@buffer = @displayBuffer.buffer
@softTabs = @usesSoftTabs() ? @softTabs ? atom.config.get('editor.softTabs') ? true
for marker in @findMarkers(@getSelectionMarkerAttributes())
marker.setProperties(preserveFolds: true)
@addSelection(marker)
@subscribeToTabTypeConfig()
@subscribeToBuffer()
@subscribeToDisplayBuffer()
if @getCursors().length is 0 and not suppressCursorCreation
if @cursors.length is 0 and not suppressCursorCreation
initialLine = Math.max(parseInt(initialLine) or 0, 0)
initialColumn = Math.max(parseInt(initialColumn) or 0, 0)
@addCursorAtBufferPosition([initialLine, initialColumn])
@@ -176,10 +176,16 @@ class TextEditor extends Model
@subscribe @displayBuffer.onDidAddDecoration (decoration) => @emit 'decoration-added', decoration
@subscribe @displayBuffer.onDidRemoveDecoration (decoration) => @emit 'decoration-removed', decoration
subscribeToTabTypeConfig: ->
@tabTypeSubscription?.dispose()
@tabTypeSubscription = atom.config.observe 'editor.tabType', scope: @getRootScopeDescriptor(), =>
@softTabs = @shouldUseSoftTabs(defaultValue: @softTabs)
destroyed: ->
@unsubscribe() if includeDeprecatedAPIs
@disposables.dispose()
selection.destroy() for selection in @getSelections()
@tabTypeSubscription.dispose()
selection.destroy() for selection in @selections.slice()
@buffer.release()
@displayBuffer.destroy()
@languageMode.destroy()
@@ -335,7 +341,7 @@ class TextEditor extends Model
onDidInsertText: (callback) ->
@emitter.on 'did-insert-text', callback
# Public: Invoke the given callback after the buffer is saved to disk.
# Essential: Invoke the given callback after the buffer is saved to disk.
#
# * `callback` {Function} to be called after the buffer is saved.
# * `event` {Object} with the following keys:
@@ -345,7 +351,7 @@ class TextEditor extends Model
onDidSave: (callback) ->
@getBuffer().onDidSave(callback)
# Public: Invoke the given callback when the editor is destroyed.
# Essential: Invoke the given callback when the editor is destroyed.
#
# * `callback` {Function} to be called when the editor is destroyed.
#
@@ -464,7 +470,7 @@ class TextEditor extends Model
onDidUpdateMarkers: (callback) ->
@displayBuffer.onDidUpdateMarkers(callback)
# Public: Retrieves the current {TextBuffer}.
# Essential: Retrieves the current {TextBuffer}.
getBuffer: -> @buffer
# Retrieves the current buffer's URI.
@@ -508,20 +514,7 @@ class TextEditor extends Model
onDidChangeLineNumberGutterVisible: (callback) ->
@emitter.on 'did-change-line-number-gutter-visible', callback
# Public: Creates and returns a {Gutter}.
# See {GutterContainer::addGutter} for more details.
addGutter: (options) ->
@gutterContainer.addGutter(options)
# Public: Returns the {Array} of all gutters on this editor.
getGutters: ->
@gutterContainer.getGutters()
# Public: Returns the {Gutter} with the given name, or null if it doesn't exist.
gutterWithName: (name) ->
@gutterContainer.gutterWithName(name)
# Calls your `callback` when a {Gutter} is added to the editor.
# Essential: Calls your `callback` when a {Gutter} is added to the editor.
# Immediately calls your callback for each existing gutter.
#
# * `callback` {Function}
@@ -531,7 +524,7 @@ class TextEditor extends Model
observeGutters: (callback) ->
@gutterContainer.observeGutters callback
# Calls your `callback` when a {Gutter} is added to the editor.
# Essential: Calls your `callback` when a {Gutter} is added to the editor.
#
# * `callback` {Function}
# * `gutter` {Gutter} that was added.
@@ -540,7 +533,7 @@ class TextEditor extends Model
onDidAddGutter: (callback) ->
@gutterContainer.onDidAddGutter callback
# Calls your `callback` when a {Gutter} is removed from the editor.
# Essential: Calls your `callback` when a {Gutter} is removed from the editor.
#
# * `callback` {Function}
# * `name` The name of the {Gutter} that was removed.
@@ -623,7 +616,7 @@ class TextEditor extends Model
# See {TextBuffer::save} for more details.
save: -> @buffer.save(backup: atom.config.get('editor.backUpBeforeSaving'))
# Public: Saves the editor's text buffer as the given path.
# Essential: Saves the editor's text buffer as the given path.
#
# See {TextBuffer::saveAs} for more details.
#
@@ -736,7 +729,7 @@ class TextEditor extends Model
# {Delegates to: TextBuffer.getEndPosition}
getEofBufferPosition: -> @buffer.getEndPosition()
# Public: Get the {Range} of the paragraph surrounding the most recently added
# Essential: Get the {Range} of the paragraph surrounding the most recently added
# cursor.
#
# Returns a {Range}.
@@ -1130,12 +1123,12 @@ class TextEditor extends Model
# Essential: Undo the last change.
undo: ->
@buffer.undo()
@avoidMergingSelections => @buffer.undo()
@getLastSelection().autoscroll()
# Essential: Redo the last change.
redo: ->
@buffer.redo(this)
@avoidMergingSelections => @buffer.redo()
@getLastSelection().autoscroll()
# Extended: Batch multiple operations as a single undo/redo step.
@@ -1303,7 +1296,7 @@ class TextEditor extends Model
#
# * __line__: Adds your CSS `class` to the line nodes within the range
# marked by the marker
# * __gutter__: Adds your CSS `class` to the line number nodes within the
# * __line-number__: Adds your CSS `class` to the line number nodes within the
# range marked by the marker
# * __highlight__: Adds a new highlight div to the editor surrounding the
# range marked by the marker. When the user selects text, the selection is
@@ -1321,9 +1314,9 @@ class TextEditor extends Model
# * `marker` A {Marker} you want this decoration to follow.
# * `decorationParams` An {Object} representing the decoration e.g.
# `{type: 'line-number', class: 'linter-error'}`
# * `type` There are a few supported decoration types: `gutter`, `line`,
# * `type` There are a few supported decoration types: `line-number`, `line`,
# `highlight`, and `overlay`. The behavior of the types are as follows:
# * `gutter` Adds the given `class` to the line numbers overlapping the
# * `line-number` Adds the given `class` to the line numbers overlapping the
# rows spanned by the marker.
# * `line` Adds the given `class` to the lines overlapping the rows
# spanned by the marker.
@@ -1335,19 +1328,17 @@ class TextEditor extends Model
# * `class` This CSS class will be applied to the decorated line number,
# line, highlight, or overlay.
# * `onlyHead` (optional) If `true`, the decoration will only be applied to
# the head of the marker. Only applicable to the `line` and `gutter`
# the head of the marker. Only applicable to the `line` and `line-number`
# types.
# * `onlyEmpty` (optional) If `true`, the decoration will only be applied if
# the associated marker is empty. Only applicable to the `line` and
# `gutter` types.
# `line-number` types.
# * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied
# if the associated marker is non-empty. Only applicable to the `line`
# and gutter types.
# and `line-number` types.
# * `position` (optional) Only applicable to decorations of type `overlay`,
# controls where the overlay view is positioned relative to the marker.
# Values can be `'head'` (the default), or `'tail'`.
# * `gutterName` (optional) Only applicable to the `gutter` type. If provided,
# the decoration will be applied to the gutter with the specified name.
#
# Returns a {Decoration} object
decorateMarker: (marker, decorationParams) ->
@@ -1356,7 +1347,7 @@ class TextEditor extends Model
decorationParams.type = 'line-number'
@displayBuffer.decorateMarker(marker, decorationParams)
# Public: Get all the decorations within a screen row range.
# Essential: Get all the decorations within a screen row range.
#
# * `startScreenRow` the {Number} beginning screen row
# * `endScreenRow` the {Number} end screen row (inclusive)
@@ -1755,6 +1746,7 @@ class TextEditor extends Model
# Extended: Returns the most recently added {Cursor}
getLastCursor: ->
@createLastSelectionIfNeeded()
_.last(@cursors)
# Extended: Returns the word surrounding the most recently added cursor.
@@ -1765,6 +1757,7 @@ class TextEditor extends Model
# Extended: Get an Array of all {Cursor}s.
getCursors: ->
@createLastSelectionIfNeeded()
@cursors.slice()
# Extended: Get all {Cursors}s, ordered by their position in the buffer
@@ -1850,6 +1843,8 @@ class TextEditor extends Model
# * `options` (optional) An options {Object}:
# * `reversed` A {Boolean} indicating whether to create the selection in a
# reversed orientation.
# * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the
# selection is set.
setSelectedBufferRange: (bufferRange, options) ->
@setSelectedBufferRanges([bufferRange], options)
@@ -1860,6 +1855,8 @@ class TextEditor extends Model
# * `options` (optional) An options {Object}:
# * `reversed` A {Boolean} indicating whether to create the selection in a
# reversed orientation.
# * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the
# selection is set.
setSelectedBufferRanges: (bufferRanges, options={}) ->
throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length
@@ -1965,10 +1962,10 @@ class TextEditor extends Model
# This method may merge selections that end up intesecting.
#
# * `position` An instance of {Point}, with a given `row` and `column`.
selectToScreenPosition: (position, suppressMerge) ->
selectToScreenPosition: (position, options) ->
lastSelection = @getLastSelection()
lastSelection.selectToScreenPosition(position)
unless suppressMerge
lastSelection.selectToScreenPosition(position, options)
unless options?.suppressSelectionMerge
@mergeIntersectingSelections(reversed: lastSelection.isReversed())
# Essential: Move the cursor of each selection one character upward while
@@ -2140,12 +2137,14 @@ class TextEditor extends Model
#
# Returns a {Selection}.
getLastSelection: ->
@createLastSelectionIfNeeded()
_.last(@selections)
# Extended: Get current {Selection}s.
#
# Returns: An {Array} of {Selection}s.
getSelections: ->
@createLastSelectionIfNeeded()
@selections.slice()
# Extended: Get all {Selection}s, ordered by their position in the buffer
@@ -2224,6 +2223,9 @@ class TextEditor extends Model
previousSelection.intersectsScreenRowRange(screenRange.start.row, screenRange.end.row)
avoidMergingSelections: (args...) ->
@mergeSelections args..., -> false
mergeSelections: (args...) ->
mergePredicate = args.pop()
fn = args.pop() if _.isFunction(_.last(args))
@@ -2299,6 +2301,10 @@ class TextEditor extends Model
@emit 'selection-screen-range-changed', event if includeDeprecatedAPIs
@emitter.emit 'did-change-selection-range', event
createLastSelectionIfNeeded: ->
if @selections.length is 0
@addSelectionForBufferRange([[0, 0], [0, 0]], autoscroll: false, preserveFolds: true)
###
Section: Searching and Replacing
###
@@ -2321,7 +2327,7 @@ class TextEditor extends Model
# * `replace` Call this {Function} with a {String} to replace the match.
scan: (regex, iterator) -> @buffer.scan(regex, iterator)
# Public: Scan regular expression matches in a given range, calling the given
# Essential: Scan regular expression matches in a given range, calling the given
# iterator function on each match.
#
# * `regex` A {RegExp} to search for.
@@ -2335,7 +2341,7 @@ class TextEditor extends Model
# * `replace` Call this {Function} with a {String} to replace the match.
scanInBufferRange: (regex, range, iterator) -> @buffer.scanInRange(regex, range, iterator)
# Public: Scan regular expression matches in a given range in reverse order,
# Essential: Scan regular expression matches in a given range in reverse order,
# calling the given iterator function on each match.
#
# * `regex` A {RegExp} to search for.
@@ -2387,7 +2393,7 @@ class TextEditor extends Model
usesSoftTabs: ->
# FIXME Remove once this can be specified as a scoped setting in the
# language-make package
return false if @getGrammar().scopeName is 'source.makefile'
return false if @getGrammar()?.scopeName is 'source.makefile'
for bufferRow in [0..@buffer.getLastRow()]
continue if @displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
@@ -2412,6 +2418,20 @@ class TextEditor extends Model
return unless @getSoftTabs()
@scanInBufferRange /\t/g, bufferRange, ({replace}) => replace(@getTabText())
# Private: Computes whether or not this editor should use softTabs based on
# the `editor.tabType` setting.
#
# Returns a {Boolean}
shouldUseSoftTabs: ({defaultValue}) ->
tabType = atom.config.get('editor.tabType', scope: @getRootScopeDescriptor())
switch tabType
when 'auto'
@usesSoftTabs() ? defaultValue ? atom.config.get('editor.softTabs') ? true
when 'hard'
false
when 'soft'
true
###
Section: Soft Wrap Behavior
###
@@ -2433,7 +2453,7 @@ class TextEditor extends Model
# Returns a {Boolean}.
toggleSoftWrapped: -> @setSoftWrapped(not @isSoftWrapped())
# Public: Gets the column at which column will soft wrap
# Essential: Gets the column at which column will soft wrap
getSoftWrapColumn: -> @displayBuffer.getSoftWrapColumn()
###
@@ -2605,6 +2625,15 @@ class TextEditor extends Model
maintainClipboard = true
return
# Private: For each selection, only copy highlighted text.
copyOnlySelectedText: ->
maintainClipboard = false
for selection in @getSelectionsOrderedByBufferPosition()
if not selection.isEmpty()
selection.copy(maintainClipboard, true)
maintainClipboard = true
return
# Essential: For each selection, cut the selected text.
cutSelectedText: ->
maintainClipboard = false
@@ -2659,7 +2688,7 @@ class TextEditor extends Model
@emit('did-insert-text', didInsertEvent) if includeDeprecatedAPIs
@emitter.emit 'did-insert-text', didInsertEvent
# Public: For each selection, if the selection is empty, cut all characters
# Essential: For each selection, if the selection is empty, cut all characters
# of the containing line following the cursor. Otherwise cut the selected
# text.
cutToEndOfLine: ->
@@ -2807,6 +2836,36 @@ class TextEditor extends Model
outermostFoldsInBufferRowRange: (startRow, endRow) ->
@displayBuffer.outermostFoldsInBufferRowRange(startRow, endRow)
###
Section: Gutters
###
# Essential: Add a custom {Gutter}.
#
# * `options` An {Object} with the following fields:
# * `name` (required) A unique {String} to identify this gutter.
# * `priority` (optional) A {Number} that determines stacking order between
# gutters. Lower priority items are forced closer to the edges of the
# window. (default: -100)
# * `visible` (optional) {Boolean} specifying whether the gutter is visible
# initially after being created. (default: true)
#
# Returns the newly-created {Gutter}.
addGutter: (options) ->
@gutterContainer.addGutter(options)
# Essential: Get this editor's gutters.
#
# Returns an {Array} of {Gutter}s.
getGutters: ->
@gutterContainer.getGutters()
# Essential: Get the gutter with the given name.
#
# Returns a {Gutter}, or `null` if no gutter exists for the given name.
gutterWithName: (name) ->
@gutterContainer.gutterWithName(name)
###
Section: Scrolling the TextEditor
###
@@ -2858,14 +2917,10 @@ class TextEditor extends Model
setVerticalScrollbarWidth: (width) -> @displayBuffer.setVerticalScrollbarWidth(width)
pageUp: ->
newScrollTop = @getScrollTop() - @getHeight()
@moveUp(@getRowsPerPage())
@setScrollTop(newScrollTop)
pageDown: ->
newScrollTop = @getScrollTop() + @getHeight()
@moveDown(@getRowsPerPage())
@setScrollTop(newScrollTop)
selectPageUp: ->
@selectUp(@getRowsPerPage())
@@ -2875,7 +2930,7 @@ class TextEditor extends Model
# Returns the number of rows per page
getRowsPerPage: ->
Math.max(1, Math.ceil(@getHeight() / @getLineHeightInPixels()))
Math.max(1, Math.floor(@getHeight() / @getLineHeightInPixels()))
###
Section: Config
@@ -2892,10 +2947,11 @@ class TextEditor extends Model
###
handleTokenization: ->
@softTabs = @usesSoftTabs() ? @softTabs
@softTabs = @shouldUseSoftTabs(defaultValue: @softTabs)
handleGrammarChange: ->
@unfoldAll()
@subscribeToTabTypeConfig()
@emitter.emit 'did-change-grammar', @getGrammar()
handleMarkerCreated: (marker) =>
@@ -2906,13 +2962,13 @@ class TextEditor extends Model
Section: TextEditor Rendering
###
# Public: Retrieves the greyed out placeholder of a mini editor.
# Essential: Retrieves the greyed out placeholder of a mini editor.
#
# Returns a {String}.
getPlaceholderText: ->
@placeholderText
# Public: Set the greyed out placeholder of a mini editor. Placeholder text
# Essential: Set the greyed out placeholder of a mini editor. Placeholder text
# will be displayed when the editor has no content.
#
# * `placeholderText` {String} text that is displayed when the editor has no content.

View File

@@ -68,7 +68,7 @@ class TokenizedBuffer extends Model
if grammar.injectionSelector?
@retokenizeLines() if @hasTokenForSelector(grammar.injectionSelector)
else
newScore = grammar.getScore(@buffer.getPath(), @getGrammarSelectionContent())
newScore = atom.grammars.getGrammarScore(grammar, @buffer.getPath(), @getGrammarSelectionContent())
@setGrammar(grammar, newScore) if newScore > @currentGrammarScore
setGrammar: (grammar, score) ->
@@ -76,7 +76,7 @@ class TokenizedBuffer extends Model
@grammar = grammar
@rootScopeDescriptor = new ScopeDescriptor(scopes: [@grammar.scopeName])
@currentGrammarScore = score ? grammar.getScore(@buffer.getPath(), @getGrammarSelectionContent())
@currentGrammarScore = score ? atom.grammars.getGrammarScore(grammar, @buffer.getPath(), @getGrammarSelectionContent())
@grammarUpdateDisposable?.dispose()
@grammarUpdateDisposable = @grammar.onDidUpdate => @retokenizeLines()

View File

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

53
src/typescript.js Normal file
View File

@@ -0,0 +1,53 @@
'use strict'
var _ = require('underscore-plus')
var crypto = require('crypto')
var path = require('path')
var defaultOptions = {
target: 1,
module: 'commonjs',
sourceMap: true
}
var TypeScriptSimple = null
var typescriptVersionDir = null
exports.shouldCompile = function () {
return true
}
exports.getCachePath = function (sourceCode) {
if (typescriptVersionDir == null) {
var version = require('typescript-simple/package.json').version
typescriptVersionDir = path.join('ts', createVersionAndOptionsDigest(version, defaultOptions))
}
return path.join(
typescriptVersionDir,
crypto
.createHash('sha1')
.update(sourceCode, 'utf8')
.digest('hex') + '.js'
)
}
exports.compile = function (sourceCode, filePath) {
if (!TypeScriptSimple) {
TypeScriptSimple = require('typescript-simple').TypeScriptSimple
}
var options = _.defaults({filename: filePath}, defaultOptions)
return new TypeScriptSimple(options, false).compile(sourceCode, filePath)
}
function createVersionAndOptionsDigest (version, options) {
return crypto
.createHash('sha1')
.update('typescript', 'utf8')
.update('\0', 'utf8')
.update(version, 'utf8')
.update('\0', 'utf8')
.update(JSON.stringify(options), 'utf8')
.digest('hex')
}

View File

@@ -124,6 +124,22 @@ class ViewRegistry
# * `object` The object for which you want to retrieve a view. This can be a
# pane item, a pane, or the workspace itself.
#
# ## View Resolution Algorithm
#
# The view associated with the object is resolved using the following
# sequence
#
# 1. Is the object an instance of `HTMLElement`? If true, return the object.
# 2. Does the object have a property named `element` with a value which is
# an instance of `HTMLElement`? If true, return the property value.
# 3. Is the object a jQuery object, indicated by the presence of a `jquery`
# property? If true, return the root DOM element (i.e. `object[0]`).
# 4. Has a view provider been registered for the object? If true, use the
# provider to create a view associated with the object, and return the
# view.
#
# If no associated view is returned by the sequence an error is thrown.
#
# Returns a DOM element.
getView: (object) ->
return unless object?
@@ -138,6 +154,8 @@ class ViewRegistry
createView: (object) ->
if object instanceof HTMLElement
object
else if object?.element instanceof HTMLElement
object.element
else if object?.jquery
object[0]
else if provider = @findProvider(object)

View File

@@ -89,6 +89,10 @@ class WindowEventHandler
@subscribeToCommand $(window), 'window:toggle-menu-bar', ->
atom.config.set('core.autoHideMenuBar', not atom.config.get('core.autoHideMenuBar'))
if atom.config.get('core.autoHideMenuBar')
detail = "To toggle, press the Alt key or execute the window:toggle-menu-bar command"
atom.notifications.addInfo('Menu bar hidden', {detail})
@subscribeToCommand $(document), 'core:focus-next', @focusNext
@subscribeToCommand $(document), 'core:focus-previous', @focusPrevious

View File

@@ -779,9 +779,9 @@ class Workspace extends Model
# Essential: Adds a panel item as a modal dialog.
#
# * `options` {Object}
# * `item` Your panel content. It can be DOM element, a jQuery element, or
# * `item` Your panel content. It can be a DOM element, a jQuery element, or
# a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the
# latter. See {ViewRegistry::addViewProvider} for more information.
# model option. See {ViewRegistry::addViewProvider} for more information.
# * `visible` (optional) {Boolean} false if you want the panel to initially be hidden
# (default: true)
# * `priority` (optional) {Number} Determines stacking order. Lower priority items are