mirror of
https://github.com/atom/atom.git
synced 2026-02-17 18:11:29 -05:00
Merge remote-tracking branch 'upstream/master' into move-lines-up-and-down-with-multiple-selections
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
200
src/babel.coffee
200
src/babel.coffee
@@ -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
63
src/babel.js
Normal 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')
|
||||
}
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
44
src/coffee-script.js
Normal 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
|
||||
}
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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
174
src/compile-cache.js
Normal 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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
###
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, "\\\\")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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: ->
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
53
src/typescript.js
Normal 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')
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user