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

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

View File

@@ -6,6 +6,6 @@
"url": "https://github.com/atom/atom.git"
},
"dependencies": {
"atom-package-manager": "1.0.1"
"atom-package-manager": "1.0.4"
}
}

View File

@@ -1,6 +1,9 @@
fs = require 'fs'
path = require 'path'
os = require 'os'
glob = require 'glob'
usesBabel = require './lib/uses-babel'
babelOptions = require '../static/babelrc'
# Add support for obselete APIs of vm module so we can make some third-party
# modules work under node v0.11.x.
@@ -10,13 +13,11 @@ _ = require 'underscore-plus'
packageJson = require '../package.json'
# Shim harmony collections in case grunt was invoked without harmony
# collections enabled
_.extend(global, require('harmony-collections')) unless global.WeakMap?
module.exports = (grunt) ->
grunt.loadNpmTasks('grunt-babel')
grunt.loadNpmTasks('grunt-coffeelint')
grunt.loadNpmTasks('grunt-lesslint')
grunt.loadNpmTasks('grunt-standard')
grunt.loadNpmTasks('grunt-cson')
grunt.loadNpmTasks('grunt-contrib-csslint')
grunt.loadNpmTasks('grunt-contrib-coffee')
@@ -77,6 +78,11 @@ module.exports = (grunt) ->
dest: appDir
ext: '.js'
babelConfig =
options: babelOptions
dist:
files: []
lessConfig =
options:
paths: [
@@ -141,6 +147,13 @@ module.exports = (grunt) ->
pegConfig.glob_to_multiple.src.push("#{directory}/lib/*.pegjs")
for jsFile in glob.sync("#{directory}/lib/**/*.js")
if usesBabel(jsFile)
babelConfig.dist.files.push({
src: [jsFile]
dest: path.join(appDir, jsFile)
})
grunt.initConfig
pkg: grunt.file.readJSON('package.json')
@@ -148,6 +161,8 @@ module.exports = (grunt) ->
docsOutputDir: 'docs/output'
babel: babelConfig
coffee: coffeeConfig
less: lessConfig
@@ -174,6 +189,12 @@ module.exports = (grunt) ->
'spec/*.coffee'
]
standard:
src: [
'src/**/*.js'
'static/*.js'
]
csslint:
options:
'adjoining-classes': false
@@ -229,8 +250,8 @@ module.exports = (grunt) ->
stderr: false
failOnError: false
grunt.registerTask('compile', ['coffee', 'prebuild-less', 'cson', 'peg'])
grunt.registerTask('lint', ['coffeelint', 'csslint', 'lesslint'])
grunt.registerTask('compile', ['babel', 'coffee', 'prebuild-less', 'cson', 'peg'])
grunt.registerTask('lint', ['standard', 'coffeelint', 'csslint', 'lesslint'])
grunt.registerTask('test', ['shell:kill-atom', 'run-specs'])
ciTasks = ['output-disk-space', 'download-atom-shell', 'download-atom-shell-chromedriver', 'build']

View File

@@ -0,0 +1,18 @@
fs = require 'fs'
BABEL_PREFIXES = [
"'use babel'"
'"use babel"'
'/** @babel */'
]
PREFIX_LENGTH = Math.max(BABEL_PREFIXES.map((prefix) -> prefix.length)...)
buffer = Buffer(PREFIX_LENGTH)
module.exports = (filename) ->
file = fs.openSync(filename, 'r')
fs.readSync(file, buffer, 0, PREFIX_LENGTH)
fs.closeSync(file)
BABEL_PREFIXES.some (prefix) ->
prefix is buffer.toString('utf8', 0, prefix.length)

View File

@@ -12,19 +12,21 @@
"formidable": "~1.0.14",
"fs-plus": "2.x",
"github-releases": "~0.2.0",
"glob": "^5.0.14",
"grunt": "~0.4.1",
"grunt-electron-installer": "^0.37.0",
"grunt-babel": "^5.0.1",
"grunt-cli": "~0.1.9",
"grunt-coffeelint": "git+https://github.com/atom/grunt-coffeelint.git#cfb99aa99811d52687969532bd5a98011ed95bfe",
"grunt-contrib-coffee": "~0.12.0",
"grunt-contrib-csslint": "~0.2.0",
"grunt-contrib-less": "~0.8.0",
"grunt-cson": "0.14.0",
"grunt-download-atom-shell": "~0.14.0",
"grunt-download-atom-shell": "~0.15.1",
"grunt-electron-installer": "1.0.0",
"grunt-lesslint": "0.17.0",
"grunt-peg": "~1.1.0",
"grunt-shell": "~0.3.1",
"harmony-collections": "~0.3.8",
"grunt-standard": "^1.0.2",
"legal-eagle": "~0.10.0",
"minidump": "~0.9",
"npm": "2.13.3",

View File

@@ -10,7 +10,7 @@ module.exports = (grunt) ->
unpack = [
'*.node'
'.ctags'
'ctags-config'
'ctags-darwin'
'ctags-linux'
'ctags-win32.exe'

View File

@@ -7,7 +7,7 @@ Ubuntu LTS 12.04 64-bit is the recommended platform.
* OS with 64-bit or 32-bit architecture
* C++ toolchain
* [Git](http://git-scm.com/)
* [node.js](http://nodejs.org/download/) (0.10.x or 0.12.x) or [io.js](https://iojs.org) (1.x)
* [node.js](http://nodejs.org/download/) (0.10.x or 0.12.x) or [io.js](https://iojs.org) (1.x or 2.x)
* [npm](https://www.npmjs.com/) v1.4.x (bundled with Node.js)
* `npm -v` to check the version.
* `npm config set python /usr/bin/python2 -g` to ensure that gyp uses python2.
@@ -24,7 +24,7 @@ Ubuntu LTS 12.04 64-bit is the recommended platform.
### Fedora / CentOS / RHEL
* `sudo yum --assumeyes install make gcc gcc-c++ glibc-devel git-core libgnome-keyring-devel rpmdevtools`
* `sudo dnf --assumeyes install make gcc gcc-c++ glibc-devel git-core libgnome-keyring-devel rpmdevtools`
* Instructions for [Node.js](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager#fedora).
### Arch

View File

@@ -3,7 +3,7 @@
## Requirements
* OS X 10.8 or later
* [node.js](http://nodejs.org/download/) (0.10.x or 0.12.x) or [io.js](https://iojs.org) (1.x)
* [node.js](http://nodejs.org/download/) (0.10.x or 0.12.x) or [io.js](https://iojs.org) (1.x or 2.x)
* Command Line Tools for [Xcode](https://developer.apple.com/xcode/downloads/) (run `xcode-select --install` to install)
## Instructions

View File

@@ -5,7 +5,7 @@
### On Windows 7
* [Visual C++ 2010 Express](http://www.visualstudio.com/en-us/downloads/download-visual-studio-vs#DownloadFamilies_4)
* [Visual Studio 2010 Service Pack 1](http://www.microsoft.com/en-us/download/details.aspx?id=23691)
* [node.js](http://nodejs.org/download/) (0.10.x or 0.12.x) or [io.js](https://iojs.org) (1.x)
* [node.js](http://nodejs.org/download/) (0.10.x or 0.12.x) or [io.js](https://iojs.org) (1.x or 2.x)
* For 64-bit builds of node and native modules you **must** have the
[Windows 7 64-bit SDK](http://www.microsoft.com/en-us/download/details.aspx?id=8279).
You may also need the [compiler update for the Windows SDK 7.1](http://www.microsoft.com/en-us/download/details.aspx?id=4422)
@@ -16,9 +16,9 @@
`mklink /d %SystemDrive%\Python27 D:\elsewhere\Python27`
* [GitHub for Windows](http://windows.github.com/)
### On Windows 8
* [Visual Studio Express 2013 for Windows Desktop](http://www.visualstudio.com/en-us/downloads/download-visual-studio-vs#DownloadFamilies_2)
* [node.js](http://nodejs.org/download/) (0.10.x or 0.12.x) or [io.js](https://iojs.org) (1.x)
### On Windows 8 or 10
* [Visual Studio Express 2013 or 2015 for Windows Desktop](http://www.visualstudio.com/en-us/downloads/download-visual-studio-vs#DownloadFamilies_2)
* [node.js](http://nodejs.org/download/) (0.10.x or 0.12.x) or [io.js](https://iojs.org) (1.x or 2.x)
* [Python](https://www.python.org/downloads/) v2.7.x (required by [node-gyp](https://github.com/TooTallNate/node-gyp))
* [GitHub for Windows](http://windows.github.com/)

View File

@@ -1,7 +1,7 @@
{
"name": "atom",
"productName": "Atom",
"version": "1.0.8",
"version": "1.0.11",
"description": "A hackable text editor for the 21st Century.",
"main": "./src/browser/main.js",
"repository": {
@@ -20,19 +20,17 @@
"babel-core": "^5.8.21",
"bootstrap": "^3.3.4",
"clear-cut": "^2.0.1",
"coffee-cash": "0.8.0",
"coffee-script": "1.8.0",
"coffeestack": "^1.1.2",
"color": "^0.7.3",
"delegato": "^1",
"emissary": "^1.3.3",
"event-kit": "^1.2.0",
"first-mate": "^4.2",
"event-kit": "^1.3.0",
"first-mate": "^5.0.0",
"fs-plus": "^2.8.0",
"fstream": "0.1.24",
"fuzzaldrin": "^2.1",
"git-utils": "^3.0.0",
"grim": "1.4.1",
"grim": "1.4.2",
"jasmine-json": "~0.0",
"jasmine-tagged": "^1.1.4",
"jquery": "^2.1.1",
@@ -54,21 +52,22 @@
"semver": "^4.3.3",
"serializable": "^1",
"service-hub": "^0.6.2",
"source-map-support": "^0.3.2",
"space-pen": "3.8.2",
"stacktrace-parser": "0.1.1",
"temp": "0.8.1",
"text-buffer": "6.5.2",
"text-buffer": "6.7.0",
"theorist": "^1.0.2",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.6",
"yargs": "^3.9"
"yargs": "^3.23.0"
},
"packageDependencies": {
"atom-dark-syntax": "0.27.0",
"atom-dark-ui": "0.50.0",
"atom-dark-ui": "0.51.0",
"atom-light-syntax": "0.28.0",
"atom-light-ui": "0.43.0",
"base16-tomorrow-dark-theme": "0.26.0",
"base16-tomorrow-dark-theme": "0.27.0",
"base16-tomorrow-light-theme": "0.9.0",
"one-dark-ui": "1.0.3",
"one-dark-syntax": "1.1.0",
@@ -77,7 +76,7 @@
"solarized-dark-syntax": "0.38.1",
"solarized-light-syntax": "0.22.1",
"about": "1.1.0",
"archive-view": "0.58.0",
"archive-view": "0.60.0",
"autocomplete-atom-api": "0.9.2",
"autocomplete-css": "0.10.1",
"autocomplete-html": "0.7.2",
@@ -94,54 +93,55 @@
"encoding-selector": "0.21.0",
"exception-reporting": "0.36.0",
"find-and-replace": "0.180.0",
"fuzzy-finder": "0.87.0",
"fuzzy-finder": "0.88.0",
"git-diff": "0.55.0",
"go-to-line": "0.30.0",
"grammar-selector": "0.47.0",
"image-view": "0.54.0",
"incompatible-packages": "0.24.1",
"keybinding-resolver": "0.33.0",
"line-ending-selector": "0.0.5",
"link": "0.30.0",
"markdown-preview": "0.150.0",
"metrics": "0.51.0",
"notifications": "0.57.0",
"notifications": "0.59.0",
"open-on-github": "0.38.0",
"package-generator": "0.40.0",
"release-notes": "0.53.0",
"settings-view": "0.213.0",
"settings-view": "0.216.0",
"snippets": "0.95.0",
"spell-check": "0.59.0",
"status-bar": "0.77.0",
"status-bar": "0.79.0",
"styleguide": "0.44.0",
"symbols-view": "0.100.0",
"tabs": "0.82.0",
"symbols-view": "0.104.0",
"tabs": "0.84.0",
"timecop": "0.31.0",
"tree-view": "0.183.0",
"tree-view": "0.186.0",
"update-package-dependencies": "0.10.0",
"welcome": "0.30.0",
"whitespace": "0.30.0",
"whitespace": "0.31.0",
"wrap-guide": "0.35.0",
"language-c": "0.47.0",
"language-c": "0.47.1",
"language-clojure": "0.16.0",
"language-coffee-script": "0.41.0",
"language-csharp": "0.7.0",
"language-css": "0.33.0",
"language-gfm": "0.80.0",
"language-gfm": "0.81.0",
"language-git": "0.10.0",
"language-go": "0.37.0",
"language-html": "0.40.1",
"language-html": "0.41.2",
"language-hyperlink": "0.14.0",
"language-java": "0.16.0",
"language-javascript": "0.87.1",
"language-javascript": "0.92.0",
"language-json": "0.16.0",
"language-less": "0.28.2",
"language-make": "0.16.0",
"language-make": "0.17.0",
"language-mustache": "0.12.0",
"language-objective-c": "0.15.0",
"language-perl": "0.28.0",
"language-php": "0.29.0",
"language-property-list": "0.8.0",
"language-python": "0.38.0",
"language-python": "0.39.0",
"language-ruby": "0.57.0",
"language-ruby-on-rails": "0.22.0",
"language-sass": "0.40.1",
@@ -149,7 +149,7 @@
"language-source": "0.9.0",
"language-sql": "0.17.0",
"language-text": "0.7.0",
"language-todo": "0.26.0",
"language-todo": "0.27.0",
"language-toml": "0.16.0",
"language-xml": "0.32.0",
"language-yaml": "0.24.0"

View File

@@ -1,24 +1,19 @@
path = require 'path'
_ = require 'underscore-plus'
{convertStackTrace} = require 'coffeestack'
{View, $, $$} = require '../src/space-pen-extensions'
grim = require 'grim'
marked = require 'marked'
sourceMaps = {}
formatStackTrace = (spec, message='', stackTrace) ->
return stackTrace unless stackTrace
jasminePattern = /^\s*at\s+.*\(?.*[/\\]jasmine(-[^/\\]*)?\.js:\d+:\d+\)?\s*$/
firstJasmineLinePattern = /^\s*at [/\\].*[/\\]jasmine(-[^/\\]*)?\.js:\d+:\d+\)?\s*$/
convertedLines = []
lines = []
for line in stackTrace.split('\n')
convertedLines.push(line) unless jasminePattern.test(line)
lines.push(line) unless jasminePattern.test(line)
break if firstJasmineLinePattern.test(line)
stackTrace = convertStackTrace(convertedLines.join('\n'), sourceMaps)
lines = stackTrace.split('\n')
# Remove first line of stack when it is the same as the error message
errorMatch = lines[0]?.match(/^Error: (.*)/)
lines.shift() if message.trim() is errorMatch?[1]?.trim()

View File

@@ -1,64 +1,19 @@
babel = require '../src/babel'
crypto = require 'crypto'
grim = require 'grim'
describe "Babel transpiler support", ->
beforeEach ->
jasmine.snapshotDeprecations()
afterEach ->
jasmine.restoreDeprecationsSnapshot()
describe "::createBabelVersionAndOptionsDigest", ->
it "returns a digest for the library version and specified options", ->
defaultOptions =
blacklist: [
'useStrict'
]
experimental: true
optional: [
'asyncToGenerator'
]
reactCompat: true
sourceMap: 'inline'
version = '3.0.14'
shasum = crypto.createHash('sha1')
shasum.update('babel-core', 'utf8')
shasum.update('\0', 'utf8')
shasum.update(version, 'utf8')
shasum.update('\0', 'utf8')
shasum.update('{"blacklist": ["useStrict",],"experimental": true,"optional": ["asyncToGenerator",],"reactCompat": true,"sourceMap": "inline",}')
expectedDigest = shasum.digest('hex')
observedDigest = babel.createBabelVersionAndOptionsDigest(version, defaultOptions)
expect(observedDigest).toEqual expectedDigest
describe 'when a .js file starts with /** @babel */;', ->
it "transpiles it using babel", ->
transpiled = require('./fixtures/babel/babel-comment.js')
expect(transpiled(3)).toBe 4
describe "when a .js file starts with 'use babel';", ->
it "transpiles it using babel", ->
transpiled = require('./fixtures/babel/babel-single-quotes.js')
expect(transpiled(3)).toBe 4
expect(grim.getDeprecationsLength()).toBe 0
describe "when a .js file starts with 'use 6to5';", ->
it "transpiles it using babel and adds a pragma deprecation", ->
expect(grim.getDeprecationsLength()).toBe 0
transpiled = require('./fixtures/babel/6to5-single-quotes.js')
expect(transpiled(3)).toBe 4
expect(grim.getDeprecationsLength()).toBe 1
describe 'when a .js file starts with "use babel";', ->
it "transpiles it using babel", ->
transpiled = require('./fixtures/babel/babel-double-quotes.js')
expect(transpiled(3)).toBe 4
expect(grim.getDeprecationsLength()).toBe 0
describe 'when a .js file starts with "use 6to5";', ->
it "transpiles it using babel and adds a pragma deprecation", ->
expect(grim.getDeprecationsLength()).toBe 0
transpiled = require('./fixtures/babel/6to5-double-quotes.js')
expect(transpiled(3)).toBe 4
expect(grim.getDeprecationsLength()).toBe 1
describe "when a .js file does not start with 'use 6to6';", ->
describe "when a .js file does not start with 'use babel';", ->
it "does not transpile it using babel", ->
expect(-> require('./fixtures/babel/invalid.js')).toThrow()

View File

@@ -148,6 +148,28 @@ describe "CommandRegistry", ->
grandchild.dispatchEvent(new CustomEvent('command-2', bubbles: true))
expect(calls).toEqual []
it "invokes callbacks registered with ::onWillDispatch and ::onDidDispatch", ->
sequence = []
registry.onDidDispatch (event) ->
sequence.push ['onDidDispatch', event]
registry.add '.grandchild', 'command', (event) ->
sequence.push ['listener', event]
registry.onWillDispatch (event) ->
sequence.push ['onWillDispatch', event]
grandchild.dispatchEvent(new CustomEvent('command', bubbles: true))
expect(sequence[0][0]).toBe 'onWillDispatch'
expect(sequence[1][0]).toBe 'listener'
expect(sequence[2][0]).toBe 'onDidDispatch'
expect(sequence[0][1] is sequence[1][1] is sequence[2][1]).toBe true
expect(sequence[0][1].constructor).toBe CustomEvent
expect(sequence[0][1].target).toBe grandchild
describe "::add(selector, commandName, callback)", ->
it "throws an error when called with an invalid selector", ->
badSelector = '<>'

View File

@@ -1,39 +1,71 @@
path = require 'path'
temp = require('temp').track()
Babel = require 'babel-core'
CoffeeScript = require 'coffee-script'
{TypeScriptSimple} = require 'typescript-simple'
CSON = require 'season'
CoffeeCache = require 'coffee-cash'
babel = require '../src/babel'
typescript = require '../src/typescript'
CSONParser = require 'season/node_modules/cson-parser'
CompileCache = require '../src/compile-cache'
describe "Compile Cache", ->
describe ".addPathToCache(filePath)", ->
it "adds the path to the correct CSON, CoffeeScript, babel or typescript cache", ->
spyOn(CSON, 'readFileSync').andCallThrough()
spyOn(CoffeeCache, 'addPathToCache').andCallThrough()
spyOn(babel, 'addPathToCache').andCallThrough()
spyOn(typescript, 'addPathToCache').andCallThrough()
describe 'CompileCache', ->
[atomHome, fixtures] = []
CompileCache.addPathToCache(path.join(__dirname, 'fixtures', 'cson.cson'))
expect(CSON.readFileSync.callCount).toBe 1
expect(CoffeeCache.addPathToCache.callCount).toBe 0
expect(babel.addPathToCache.callCount).toBe 0
expect(typescript.addPathToCache.callCount).toBe 0
beforeEach ->
fixtures = atom.project.getPaths()[0]
atomHome = temp.mkdirSync('fake-atom-home')
CompileCache.addPathToCache(path.join(__dirname, 'fixtures', 'coffee.coffee'))
expect(CSON.readFileSync.callCount).toBe 1
expect(CoffeeCache.addPathToCache.callCount).toBe 1
expect(babel.addPathToCache.callCount).toBe 0
expect(typescript.addPathToCache.callCount).toBe 0
CSON.setCacheDir(null)
CompileCache.resetCacheStats()
CompileCache.addPathToCache(path.join(__dirname, 'fixtures', 'babel', 'babel-double-quotes.js'))
expect(CSON.readFileSync.callCount).toBe 1
expect(CoffeeCache.addPathToCache.callCount).toBe 1
expect(babel.addPathToCache.callCount).toBe 1
expect(typescript.addPathToCache.callCount).toBe 0
spyOn(Babel, 'transform').andReturn {code: 'the-babel-code'}
spyOn(CoffeeScript, 'compile').andReturn {js: 'the-coffee-code', v3SourceMap: {}}
spyOn(TypeScriptSimple::, 'compile').andReturn 'the-typescript-code'
spyOn(CSONParser, 'parse').andReturn {the: 'cson-data'}
CompileCache.addPathToCache(path.join(__dirname, 'fixtures', 'typescript', 'valid.ts'))
expect(CSON.readFileSync.callCount).toBe 1
expect(CoffeeCache.addPathToCache.callCount).toBe 1
expect(babel.addPathToCache.callCount).toBe 1
expect(typescript.addPathToCache.callCount).toBe 1
afterEach ->
CSON.setCacheDir(CompileCache.getCacheDirectory())
CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME)
describe 'addPathToCache(filePath, atomHome)', ->
describe 'when the given file is plain javascript', ->
it 'does not compile or cache the file', ->
CompileCache.addPathToCache(path.join(fixtures, 'sample.js'), atomHome)
expect(CompileCache.getCacheStats()['.js']).toEqual {hits: 0, misses: 0}
describe 'when the given file uses babel', ->
it 'compiles the file with babel and caches it', ->
CompileCache.addPathToCache(path.join(fixtures, 'babel', 'babel-comment.js'), atomHome)
expect(CompileCache.getCacheStats()['.js']).toEqual {hits: 0, misses: 1}
expect(Babel.transform.callCount).toBe 1
CompileCache.addPathToCache(path.join(fixtures, 'babel', 'babel-comment.js'), atomHome)
expect(CompileCache.getCacheStats()['.js']).toEqual {hits: 1, misses: 1}
expect(Babel.transform.callCount).toBe 1
describe 'when the given file is coffee-script', ->
it 'compiles the file with coffee-script and caches it', ->
CompileCache.addPathToCache(path.join(fixtures, 'coffee.coffee'), atomHome)
expect(CompileCache.getCacheStats()['.coffee']).toEqual {hits: 0, misses: 1}
expect(CoffeeScript.compile.callCount).toBe 1
CompileCache.addPathToCache(path.join(fixtures, 'coffee.coffee'), atomHome)
expect(CompileCache.getCacheStats()['.coffee']).toEqual {hits: 1, misses: 1}
expect(CoffeeScript.compile.callCount).toBe 1
describe 'when the given file is typescript', ->
it 'compiles the file with typescript and caches it', ->
CompileCache.addPathToCache(path.join(fixtures, 'typescript', 'valid.ts'), atomHome)
expect(CompileCache.getCacheStats()['.ts']).toEqual {hits: 0, misses: 1}
expect(TypeScriptSimple::compile.callCount).toBe 1
CompileCache.addPathToCache(path.join(fixtures, 'typescript', 'valid.ts'), atomHome)
expect(CompileCache.getCacheStats()['.ts']).toEqual {hits: 1, misses: 1}
expect(TypeScriptSimple::compile.callCount).toBe 1
describe 'when the given file is CSON', ->
it 'compiles the file to JSON and caches it', ->
CompileCache.addPathToCache(path.join(fixtures, 'cson.cson'), atomHome)
expect(CSONParser.parse.callCount).toBe 1
CompileCache.addPathToCache(path.join(fixtures, 'cson.cson'), atomHome)
expect(CSONParser.parse.callCount).toBe 1

View File

@@ -1110,6 +1110,24 @@ describe "Config", ->
nestedObject:
superNestedInt: 36
expect(atom.config.get("foo")).toEqual {
bar:
anInt: 12
anObject:
nestedInt: 24
nestedObject:
superNestedInt: 36
}
atom.config.set("foo.bar.anObject.nestedObject.superNestedInt", 37)
expect(atom.config.get("foo")).toEqual {
bar:
anInt: 12
anObject:
nestedInt: 24
nestedObject:
superNestedInt: 37
}
it 'can set a non-object schema', ->
schema =
type: 'integer'
@@ -1142,8 +1160,8 @@ describe "Config", ->
type: 'integer'
default: 12
expect(atom.config.getSchema('foo.baz')).toBeUndefined()
expect(atom.config.getSchema('foo.bar.anInt.baz')).toBeUndefined()
expect(atom.config.getSchema('foo.baz')).toEqual {type: 'any'}
expect(atom.config.getSchema('foo.bar.anInt.baz')).toBe(null)
it "respects the schema for scoped settings", ->
schema =
@@ -1380,6 +1398,10 @@ describe "Config", ->
expect(atom.config.set('foo.bar.aString', nope: 'nope')).toBe false
expect(atom.config.get('foo.bar.aString')).toBe 'ok'
it 'does not allow setting children of that key-path', ->
expect(atom.config.set('foo.bar.aString.something', 123)).toBe false
expect(atom.config.get('foo.bar.aString')).toBe 'ok'
describe 'when the schema has a "maximumLength" key', ->
it "trims the string to be no longer than the specified maximum", ->
schema =
@@ -1425,6 +1447,47 @@ describe "Config", ->
expect(atom.config.get('foo.bar.anInt')).toEqual 12
expect(atom.config.get('foo.bar.nestedObject.nestedBool')).toEqual true
describe "when the value has additionalProperties set to false", ->
it 'does not allow other properties to be set on the object', ->
atom.config.setSchema('foo.bar',
type: 'object'
properties:
anInt:
type: 'integer'
default: 12
additionalProperties: false
)
expect(atom.config.set('foo.bar', {anInt: 5, somethingElse: 'ok'})).toBe true
expect(atom.config.get('foo.bar.anInt')).toBe 5
expect(atom.config.get('foo.bar.somethingElse')).toBeUndefined()
expect(atom.config.set('foo.bar.somethingElse', {anInt: 5})).toBe false
expect(atom.config.get('foo.bar.somethingElse')).toBeUndefined()
describe 'when the value has an additionalProperties schema', ->
it 'validates properties of the object against that schema', ->
atom.config.setSchema('foo.bar',
type: 'object'
properties:
anInt:
type: 'integer'
default: 12
additionalProperties:
type: 'string'
)
expect(atom.config.set('foo.bar', {anInt: 5, somethingElse: 'ok'})).toBe true
expect(atom.config.get('foo.bar.anInt')).toBe 5
expect(atom.config.get('foo.bar.somethingElse')).toBe 'ok'
expect(atom.config.set('foo.bar.somethingElse', 7)).toBe false
expect(atom.config.get('foo.bar.somethingElse')).toBe 'ok'
expect(atom.config.set('foo.bar', {anInt: 6, somethingElse: 7})).toBe true
expect(atom.config.get('foo.bar.anInt')).toBe 6
expect(atom.config.get('foo.bar.somethingElse')).toBe undefined
describe 'when the value has an "array" type', ->
beforeEach ->
schema =
@@ -1438,6 +1501,11 @@ describe "Config", ->
atom.config.set 'foo.bar', ['2', '3', '4']
expect(atom.config.get('foo.bar')).toEqual [2, 3, 4]
it 'does not allow setting children of that key-path', ->
expect(atom.config.set('foo.bar.child', 123)).toBe false
expect(atom.config.set('foo.bar.child.grandchild', 123)).toBe false
expect(atom.config.get('foo.bar')).toEqual [1, 2, 3]
describe 'when the value has a "color" type', ->
beforeEach ->
schema =

View File

@@ -1025,7 +1025,7 @@ describe "DisplayBuffer", ->
markerChangedHandler.reset()
marker2ChangedHandler.reset()
marker3 = displayBuffer.markBufferRange([[8, 1], [8, 2]], maintainHistory: true)
marker3 = displayBuffer.markBufferRange([[8, 1], [8, 2]])
marker3.onDidChange marker3ChangedHandler = jasmine.createSpy("marker3ChangedHandler")
onDisplayBufferChange = ->
@@ -1039,10 +1039,6 @@ describe "DisplayBuffer", ->
expect(marker.getHeadScreenPosition()).toEqual [5, 10]
expect(marker.getTailScreenPosition()).toEqual [5, 4]
# but marker snapshots are not restored until the end of the undo.
expect(marker2.isValid()).toBeFalsy()
expect(marker3.isValid()).toBeFalsy()
buffer.undo()
expect(changeHandler).toHaveBeenCalled()
expect(markerChangedHandler).toHaveBeenCalled()
@@ -1078,8 +1074,6 @@ describe "DisplayBuffer", ->
expect(markerChangedHandler).toHaveBeenCalled()
expect(marker2ChangedHandler).toHaveBeenCalled()
expect(marker3ChangedHandler).toHaveBeenCalled()
expect(marker2.isValid()).toBeFalsy()
expect(marker3.isValid()).toBeTruthy()
it "updates the position of markers before emitting change events that aren't caused by a buffer change", ->
displayBuffer.onDidChange changeHandler = jasmine.createSpy("changeHandler").andCallFake ->

View File

@@ -1,3 +0,0 @@
'use 6to5';
module.exports = v => v + 1

View File

@@ -1,3 +1,3 @@
"use 6to5";
/** @babel */
module.exports = v => v + 1

View File

@@ -1,6 +1,7 @@
path = require 'path'
fs = require 'fs-plus'
temp = require 'temp'
GrammarRegistry = require '../src/grammar-registry'
describe "the `grammars` global", ->
beforeEach ->
@@ -16,6 +17,9 @@ describe "the `grammars` global", ->
waitsForPromise ->
atom.packages.activatePackage('language-ruby')
waitsForPromise ->
atom.packages.activatePackage('language-git')
afterEach ->
atom.packages.deactivatePackages()
atom.packages.unloadPackages()
@@ -30,6 +34,30 @@ describe "the `grammars` global", ->
expect(grammars2.selectGrammar(filePath).name).toBe 'Ruby'
describe ".selectGrammar(filePath)", ->
it "always returns a grammar", ->
registry = new GrammarRegistry()
expect(registry.selectGrammar().scopeName).toBe 'text.plain.null-grammar'
it "selects the text.plain grammar over the null grammar", ->
expect(atom.grammars.selectGrammar('test.txt').scopeName).toBe 'text.plain'
it "selects a grammar based on the file path case insensitively", ->
expect(atom.grammars.selectGrammar('/tmp/source.coffee').scopeName).toBe 'source.coffee'
expect(atom.grammars.selectGrammar('/tmp/source.COFFEE').scopeName).toBe 'source.coffee'
describe "on Windows", ->
originalPlatform = null
beforeEach ->
originalPlatform = process.platform
Object.defineProperty process, 'platform', value: 'win32'
afterEach ->
Object.defineProperty process, 'platform', value: originalPlatform
it "normalizes back slashes to forward slashes when matching the fileTypes", ->
expect(atom.grammars.selectGrammar('something\\.git\\config').scopeName).toBe 'source.git-config'
it "can use the filePath to load the correct grammar based on the grammar's filetype", ->
waitsForPromise ->
atom.packages.activatePackage('language-git')
@@ -110,6 +138,23 @@ describe "the `grammars` global", ->
expect(-> atom.grammars.selectGrammar(null, '')).not.toThrow()
expect(-> atom.grammars.selectGrammar(null, null)).not.toThrow()
describe "when the user has custom grammar file types", ->
it "considers the custom file types as well as those defined in the grammar", ->
atom.config.set('core.customFileTypes', 'source.ruby': ['Cheffile'])
expect(atom.grammars.selectGrammar('build/Cheffile', 'cookbook "postgres"').scopeName).toBe 'source.ruby'
it "favors user-defined file types over built-in ones of equal length", ->
atom.config.set('core.customFileTypes',
'source.coffee': ['Rakefile'],
'source.ruby': ['Cakefile']
)
expect(atom.grammars.selectGrammar('Rakefile', '').scopeName).toBe 'source.coffee'
expect(atom.grammars.selectGrammar('Cakefile', '').scopeName).toBe 'source.ruby'
it "favors grammars with matching first-line-regexps even if custom file types match the file", ->
atom.config.set('core.customFileTypes', 'source.ruby': ['bootstrap'])
expect(atom.grammars.selectGrammar('bootstrap', '#!/usr/bin/env node').scopeName).toBe 'source.js'
describe ".removeGrammar(grammar)", ->
it "removes the grammar, so it won't be returned by selectGrammar", ->
grammar = atom.grammars.selectGrammar('foo.js')

View File

@@ -50,3 +50,13 @@ describe 'GutterContainer', ->
otherGutterContainer = new GutterContainer fakeOtherTextEditor
gutter = new Gutter 'gutter-name', otherGutterContainer
expect(gutterContainer.removeGutter.bind(null, gutter)).toThrow()
describe '::destroy', ->
it 'clears its array of gutters and destroys custom gutters', ->
newGutter = gutterContainer.addGutter {'test-gutter', priority: 1}
newGutterSpy = jasmine.createSpy()
newGutter.onDidDestroy(newGutterSpy)
gutterContainer.destroy()
expect(newGutterSpy).toHaveBeenCalled()
expect(gutterContainer.getGutters()).toEqual []

View File

@@ -10,6 +10,7 @@ fs = require "fs"
path = require "path"
temp = require("temp").track()
runAtom = require "./helpers/start-atom"
CSON = require "season"
describe "Starting Atom", ->
[tempDirPath, otherTempDirPath, atomHome] = []
@@ -197,6 +198,17 @@ describe "Starting Atom", ->
.waitForExist("atom-workspace")
.waitForPaneItemCount(1, 5000)
it "doesn't open a new window if openEmptyEditorOnStart is disabled", ->
configPath = path.join(atomHome, 'config.cson')
config = CSON.readFileSync(configPath)
config['*'].core = {openEmptyEditorOnStart: false}
CSON.writeFileSync(configPath, config)
runAtom [], {ATOM_HOME: atomHome}, (client) ->
client
.waitForExist("atom-workspace")
.waitForPaneItemCount(0, 5000)
it "reopens any previously opened windows", ->
runAtom [tempDirPath], {ATOM_HOME: atomHome}, (client) ->
client

View File

@@ -16,103 +16,29 @@ describe "Project", ->
# Wait for project's service consumers to be asynchronously added
waits(1)
describe "constructor", ->
it "enables a custom DirectoryProvider to supersede the DefaultDirectoryProvider", ->
remotePath = "ssh://foreign-directory:8080/"
class DummyDirectory
constructor: (@path) ->
getPath: -> @path
getFile: -> existsSync: -> false
getSubdirectory: -> existsSync: -> false
isRoot: -> true
off: ->
contains: (filePath) -> filePath.startsWith(remotePath)
directoryProvider =
directoryForURISync: (uri) ->
if uri.startsWith("ssh://")
new DummyDirectory(uri)
else
null
directoryForURI: (uri) -> throw new Error("This should not be called.")
atom.packages.serviceHub.provide(
"atom.directory-provider", "0.1.0", directoryProvider)
tmp = temp.mkdirSync()
atom.project.setPaths([tmp, remotePath])
directories = atom.project.getDirectories()
expect(directories.length).toBe 2
localDirectory = directories[0]
expect(localDirectory.getPath()).toBe tmp
expect(localDirectory instanceof Directory).toBe true
dummyDirectory = directories[1]
expect(dummyDirectory.getPath()).toBe remotePath
expect(dummyDirectory instanceof DummyDirectory).toBe true
expect(atom.project.getPaths()).toEqual([tmp, remotePath])
# Make sure that DummyDirectory.contains() is honored.
remotePathSubdirectory = remotePath + "a/subdirectory"
atom.project.addPath(remotePathSubdirectory)
expect(atom.project.getDirectories().length).toBe 2
# Make sure that a new DummyDirectory that is not contained by the first
# DummyDirectory can be added.
otherRemotePath = "ssh://other-foreign-directory:8080/"
atom.project.addPath(otherRemotePath)
newDirectories = atom.project.getDirectories()
expect(newDirectories.length).toBe 3
otherDummyDirectory = newDirectories[2]
expect(otherDummyDirectory.getPath()).toBe otherRemotePath
expect(otherDummyDirectory instanceof DummyDirectory).toBe true
it "uses the default directory provider if no custom provider can handle the URI", ->
directoryProvider =
directoryForURISync: (uri) -> null
directoryForURI: (uri) -> throw new Error("This should not be called.")
atom.packages.serviceHub.provide(
"atom.directory-provider", "0.1.0", directoryProvider)
tmp = temp.mkdirSync()
atom.project.setPaths([tmp])
directories = atom.project.getDirectories()
expect(directories.length).toBe 1
expect(directories[0].getPath()).toBe tmp
it "gets the parent directory from the default directory provider if it's a local directory", ->
tmp = temp.mkdirSync()
atom.project.setPaths([path.join(tmp, "not-existing")])
directories = atom.project.getDirectories()
expect(directories.length).toBe 1
expect(directories[0].getPath()).toBe tmp
it "only normalizes the directory path if it isn't on the local filesystem", ->
nonLocalFsDirectory = "custom_proto://abc/def"
atom.project.setPaths([nonLocalFsDirectory])
directories = atom.project.getDirectories()
expect(directories.length).toBe 1
expect(directories[0].getPath()).toBe path.normalize(nonLocalFsDirectory)
it "tries to update repositories when a new RepositoryProvider is registered", ->
tmp = temp.mkdirSync('atom-project')
atom.project.setPaths([tmp])
describe "when a new repository-provider is added", ->
it "uses it to create repositories for any directories that need one", ->
projectPath = temp.mkdirSync('atom-project')
atom.project.setPaths([projectPath])
expect(atom.project.getRepositories()).toEqual [null]
expect(atom.project.repositoryProviders.length).toEqual 1
# Register a new RepositoryProvider.
dummyRepository = destroy: ->
repositoryProvider =
dummyRepository = {destroy: -> null}
atom.packages.serviceHub.provide("atom.repository-provider", "0.1.0", {
repositoryForDirectory: (directory) -> Promise.resolve(dummyRepository)
repositoryForDirectorySync: (directory) -> dummyRepository
atom.packages.serviceHub.provide(
"atom.repository-provider", "0.1.0", repositoryProvider)
})
waitsFor -> atom.project.repositoryProviders.length is 2
runs -> expect(atom.project.getRepositories()).toEqual [dummyRepository]
repository = null
it "does not update @repositories if every path has a Repository", ->
waitsFor "repository to be updated", ->
repository = atom.project.getRepositories()[0]
runs ->
expect(repository).toBe dummyRepository
it "does not create any new repositories if every directory has a repository", ->
repositories = atom.project.getRepositories()
expect(repositories.length).toEqual 1
[repository] = repositories
@@ -336,12 +262,13 @@ describe "Project", ->
# Verify that the result is cached.
expect(atom.project.repositoryForDirectory(directory)).toBe(promise)
describe ".setPaths(path)", ->
describe ".setPaths(paths)", ->
describe "when path is a file", ->
it "sets its path to the files parent directory and updates the root directory", ->
atom.project.setPaths([require.resolve('./fixtures/dir/a')])
expect(atom.project.getPaths()[0]).toEqual path.dirname(require.resolve('./fixtures/dir/a'))
expect(atom.project.getDirectories()[0].path).toEqual path.dirname(require.resolve('./fixtures/dir/a'))
filePath = require.resolve('./fixtures/dir/a')
atom.project.setPaths([filePath])
expect(atom.project.getPaths()[0]).toEqual path.dirname(filePath)
expect(atom.project.getDirectories()[0].path).toEqual path.dirname(filePath)
describe "when path is a directory", ->
it "assigns the directories and repositories", ->
@@ -372,17 +299,86 @@ describe "Project", ->
expect(onDidChangePathsSpy.callCount).toBe 1
expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths)
describe "when path is null", ->
it "sets its path and root directory to null", ->
describe "when no paths are given", ->
it "clears its path", ->
atom.project.setPaths([])
expect(atom.project.getPaths()[0]?).toBeFalsy()
expect(atom.project.getDirectories()[0]?).toBeFalsy()
expect(atom.project.getPaths()).toEqual []
expect(atom.project.getDirectories()).toEqual []
it "normalizes the path to remove consecutive slashes, ., and .. segments", ->
atom.project.setPaths(["#{require.resolve('./fixtures/dir/a')}#{path.sep}b#{path.sep}#{path.sep}.."])
expect(atom.project.getPaths()[0]).toEqual path.dirname(require.resolve('./fixtures/dir/a'))
expect(atom.project.getDirectories()[0].path).toEqual path.dirname(require.resolve('./fixtures/dir/a'))
it "only normalizes the directory path if it isn't on the local filesystem", ->
nonLocalFsDirectory = "custom_proto://abc/def"
atom.project.setPaths([nonLocalFsDirectory])
directories = atom.project.getDirectories()
expect(directories.length).toBe 1
expect(directories[0].getPath()).toBe path.normalize(nonLocalFsDirectory)
describe "when a custom directory provider has been added", ->
describe "when custom provider handles the given path", ->
it "creates a directory using that provider", ->
class DummyDirectory
constructor: (@path) ->
getPath: -> @path
getFile: -> {existsSync: -> false}
getSubdirectory: -> {existsSync: -> false}
isRoot: -> true
existsSync: -> /does-exist/.test(@path)
off: ->
contains: (filePath) -> filePath.startsWith(@path)
atom.packages.serviceHub.provide("atom.directory-provider", "0.1.0", {
directoryForURISync: (uri) ->
if uri.startsWith("ssh://")
new DummyDirectory(uri)
else
null
})
localPath = temp.mkdirSync('local-path')
remotePath = "ssh://foreign-directory:8080/exists"
atom.project.setPaths([localPath, remotePath])
directories = atom.project.getDirectories()
expect(directories[0].getPath()).toBe localPath
expect(directories[0] instanceof Directory).toBe true
expect(directories[1].getPath()).toBe remotePath
expect(directories[1] instanceof DummyDirectory).toBe true
# Make sure that DummyDirectory.contains() is honored.
remotePathSubdirectory = remotePath + "a/subdirectory"
atom.project.addPath(remotePathSubdirectory)
expect(atom.project.getDirectories().length).toBe 2
# Make sure that a new DummyDirectory that is not contained by the first
# DummyDirectory can be added.
otherRemotePath = "ssh://other-foreign-directory:8080/"
atom.project.addPath(otherRemotePath)
newDirectories = atom.project.getDirectories()
expect(newDirectories.length).toBe 3
otherDummyDirectory = newDirectories[2]
expect(otherDummyDirectory.getPath()).toBe otherRemotePath
expect(otherDummyDirectory instanceof DummyDirectory).toBe true
describe "when a custom provider does not handle the path", ->
it "creates a local directory for the path", ->
directoryProvider =
directoryForURISync: (uri) -> null
directoryForURI: (uri) -> throw new Error("This should not be called.")
atom.packages.serviceHub.provide(
"atom.directory-provider", "0.1.0", directoryProvider)
tmp = temp.mkdirSync()
atom.project.setPaths([tmp])
directories = atom.project.getDirectories()
expect(directories.length).toBe 1
expect(directories[0].getPath()).toBe tmp
describe ".addPath(path)", ->
it "calls callbacks registered with ::onDidChangePaths", ->
onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
@@ -396,20 +392,26 @@ describe "Project", ->
expect(onDidChangePathsSpy.callCount).toBe 1
expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath])
describe "when the project already has the path or one of its descendants", ->
it "doesn't add it again", ->
onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
atom.project.onDidChangePaths(onDidChangePathsSpy)
it "doesn't add redundant paths", ->
onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
atom.project.onDidChangePaths(onDidChangePathsSpy)
[oldPath] = atom.project.getPaths()
[oldPath] = atom.project.getPaths()
# Doesn't re-add an existing root directory
atom.project.addPath(oldPath)
expect(atom.project.getPaths()).toEqual([oldPath])
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
atom.project.addPath(oldPath)
atom.project.addPath(path.join(oldPath, "some-file.txt"))
atom.project.addPath(path.join(oldPath, "a-dir"))
atom.project.addPath(path.join(oldPath, "a-dir", "oh-git"))
# Doesn't add an entry for a file-path within an existing root directory
atom.project.addPath(path.join(oldPath, 'some-file.txt'))
expect(atom.project.getPaths()).toEqual([oldPath])
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
expect(atom.project.getPaths()).toEqual([oldPath])
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
# Does add an entry for a directory within an existing directory
newPath = path.join(oldPath, "a-dir")
atom.project.addPath(newPath)
expect(atom.project.getPaths()).toEqual([oldPath, newPath])
expect(onDidChangePathsSpy).toHaveBeenCalled()
describe ".removePath(path)", ->
onDidChangePathsSpy = null
@@ -440,19 +442,22 @@ describe "Project", ->
expect(atom.project.getRepositories()[0].isSubmodule("src")).toBe false
it "removes a path that is represented as a URI", ->
ftpURI = "ftp://example.com/some/folder"
directoryProvider =
atom.packages.serviceHub.provide("atom.directory-provider", "0.1.0", {
directoryForURISync: (uri) ->
# Dummy implementation of Directory for which GitRepositoryProvider
# will not try to create a GitRepository.
getPath: -> ftpURI
getSubdirectory: -> {}
isRoot: -> true
off: ->
atom.packages.serviceHub.provide(
"atom.directory-provider", "0.1.0", directoryProvider)
{
getPath: -> uri
getSubdirectory: -> {}
isRoot: -> true
existsSync: -> true
off: ->
}
})
ftpURI = "ftp://example.com/some/folder"
atom.project.setPaths([ftpURI])
expect(atom.project.getPaths()).toEqual [ftpURI]
atom.project.removePath(ftpURI)
expect(atom.project.getPaths()).toEqual []
@@ -494,6 +499,19 @@ describe "Project", ->
url = "http://the-path"
expect(atom.project.relativizePath(url)).toEqual [null, url]
describe "when the given path is inside more than one root folder", ->
it "uses the root folder that is closest to the given path", ->
atom.project.addPath(path.join(atom.project.getPaths()[0], 'a-dir'))
inputPath = path.join(atom.project.getPaths()[1], 'somewhere/something.txt')
expect(atom.project.getDirectories()[0].contains(inputPath)).toBe true
expect(atom.project.getDirectories()[1].contains(inputPath)).toBe true
expect(atom.project.relativizePath(inputPath)).toEqual [
atom.project.getPaths()[1],
'somewhere/something.txt'
]
describe ".contains(path)", ->
it "returns whether or not the given path is in one of the root directories", ->
rootPath = atom.project.getPaths()[0]

View File

@@ -88,7 +88,22 @@ describe "TextEditorComponent", ->
else
expect(lineNode.textContent).toBe(tokenizedLine.text)
it "renders tiles upper in the stack in front of the ones below", ->
it "gives the lines container the same height as the wrapper node", ->
linesNode = componentNode.querySelector(".lines")
wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px'
component.measureDimensions()
nextAnimationFrame()
expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels)
wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px'
component.measureDimensions()
nextAnimationFrame()
expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels)
it "renders higher tiles in front of lower ones", ->
wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px'
component.measureDimensions()
nextAnimationFrame()
@@ -586,6 +601,63 @@ describe "TextEditorComponent", ->
expect(lineNode.offsetTop).toBe(top)
expect(lineNode.textContent).toBe(text)
it "renders higher tiles in front of lower ones", ->
wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px'
component.measureDimensions()
nextAnimationFrame()
tilesNodes = componentNode.querySelector(".line-numbers").querySelectorAll(".tile")
expect(tilesNodes[0].style.zIndex).toBe("2")
expect(tilesNodes[1].style.zIndex).toBe("1")
expect(tilesNodes[2].style.zIndex).toBe("0")
verticalScrollbarNode.scrollTop = 1 * lineHeightInPixels
verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
nextAnimationFrame()
tilesNodes = componentNode.querySelector(".line-numbers").querySelectorAll(".tile")
expect(tilesNodes[0].style.zIndex).toBe("3")
expect(tilesNodes[1].style.zIndex).toBe("2")
expect(tilesNodes[2].style.zIndex).toBe("1")
expect(tilesNodes[3].style.zIndex).toBe("0")
it "renders higher line numbers in front of lower ones", ->
wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px'
component.measureDimensions()
nextAnimationFrame()
# Tile 0
expect(component.lineNumberNodeForScreenRow(0).style.zIndex).toBe("2")
expect(component.lineNumberNodeForScreenRow(1).style.zIndex).toBe("1")
expect(component.lineNumberNodeForScreenRow(2).style.zIndex).toBe("0")
# Tile 1
expect(component.lineNumberNodeForScreenRow(3).style.zIndex).toBe("2")
expect(component.lineNumberNodeForScreenRow(4).style.zIndex).toBe("1")
expect(component.lineNumberNodeForScreenRow(5).style.zIndex).toBe("0")
# Tile 2
expect(component.lineNumberNodeForScreenRow(6).style.zIndex).toBe("2")
expect(component.lineNumberNodeForScreenRow(7).style.zIndex).toBe("1")
expect(component.lineNumberNodeForScreenRow(8).style.zIndex).toBe("0")
it "gives the line numbers container the same height as the wrapper node", ->
linesNode = componentNode.querySelector(".line-numbers")
wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px'
component.measureDimensions()
nextAnimationFrame()
expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels)
wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px'
component.measureDimensions()
nextAnimationFrame()
expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels)
it "renders the currently-visible line numbers in a tiled fashion", ->
wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px'
component.measureDimensions()
@@ -1778,6 +1850,40 @@ describe "TextEditorComponent", ->
nextAnimationFrame()
expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [10, 0]]
it "autoscrolls when the cursor approaches the boundaries of the editor", ->
wrapperNode.style.height = '100px'
wrapperNode.style.width = '100px'
component.measureDimensions()
nextAnimationFrame()
expect(editor.getScrollTop()).toBe(0)
expect(editor.getScrollLeft()).toBe(0)
linesNode.dispatchEvent(buildMouseEvent('mousedown', {clientX: 0, clientY: 0}, which: 1))
linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 100, clientY: 50}, which: 1))
nextAnimationFrame()
expect(editor.getScrollTop()).toBe(0)
expect(editor.getScrollLeft()).toBeGreaterThan(0)
linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 100, clientY: 100}, which: 1))
nextAnimationFrame()
expect(editor.getScrollTop()).toBeGreaterThan(0)
previousScrollTop = editor.getScrollTop()
previousScrollLeft = editor.getScrollLeft()
linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 10, clientY: 50}, which: 1))
nextAnimationFrame()
expect(editor.getScrollTop()).toBe(previousScrollTop)
expect(editor.getScrollLeft()).toBeLessThan(previousScrollLeft)
linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 10, clientY: 10}, which: 1))
nextAnimationFrame()
expect(editor.getScrollTop()).toBeLessThan(previousScrollTop)
it "stops selecting if the mouse is dragged into the dev tools", ->
linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1))
linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1))
@@ -1792,6 +1898,35 @@ describe "TextEditorComponent", ->
expect(nextAnimationFrame).toBe noAnimationFrame
expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]]
it "stops selecting before the buffer is modified during the drag", ->
linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1))
linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1))
nextAnimationFrame()
expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]]
editor.insertText('x')
nextAnimationFrame()
expect(editor.getSelectedScreenRange()).toEqual [[2, 5], [2, 5]]
linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), which: 1))
expect(nextAnimationFrame).toBe noAnimationFrame
expect(editor.getSelectedScreenRange()).toEqual [[2, 5], [2, 5]]
linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1))
linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([5, 4]), which: 1))
nextAnimationFrame()
expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [5, 4]]
editor.delete()
nextAnimationFrame()
expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [2, 4]]
linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), which: 1))
expect(nextAnimationFrame).toBe noAnimationFrame
expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [2, 4]]
describe "when the command key is held down", ->
it "adds a new selection and selects to the nearest screen position, then merges intersecting selections when the mouse button is released", ->
editor.setSelectedScreenRange([[4, 4], [4, 9]])
@@ -1838,7 +1973,7 @@ describe "TextEditorComponent", ->
linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([11, 11]), which: 1))
nextAnimationFrame()
expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [11, 13]]
expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [12, 2]]
maximalScrollTop = editor.getScrollTop()
@@ -1865,13 +2000,13 @@ describe "TextEditorComponent", ->
linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([11, 11]), which: 1))
nextAnimationFrame()
expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [12, 0]]
expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [12, 2]]
maximalScrollTop = editor.getScrollTop()
linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 4]), which: 1))
nextAnimationFrame()
expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [9, 0]]
expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [8, 0]]
expect(editor.getScrollTop()).toBe maximalScrollTop # does not autoscroll upward (regression)
linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([9, 3]), which: 1))
@@ -1962,23 +2097,47 @@ describe "TextEditorComponent", ->
nextAnimationFrame()
expect(editor.getLastSelection().isReversed()).toBe false
it "autoscrolls to the cursor position, but not the entire selected range", ->
it "autoscrolls when the cursor approaches the top or bottom of the editor", ->
wrapperNode.style.height = 6 * lineHeightInPixels + 'px'
component.measureDimensions()
nextAnimationFrame()
expect(editor.getScrollTop()).toBe 0
gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2)))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6)))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8)))
nextAnimationFrame()
expect(editor.getScrollTop()).toBeGreaterThan 0
maxScrollTop = editor.getScrollTop()
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(5)))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(10)))
nextAnimationFrame()
expect(editor.getScrollTop()).toBe maxScrollTop
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7)))
nextAnimationFrame()
expect(editor.getScrollTop()).toBeLessThan maxScrollTop
it "stops selecting if a textInput event occurs during the drag", ->
gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2)))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6)))
nextAnimationFrame()
expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [7, 0]]
inputEvent = new Event('textInput')
inputEvent.data = 'x'
Object.defineProperty(inputEvent, 'target', get: -> componentNode.querySelector('.hidden-input'))
componentNode.dispatchEvent(inputEvent)
nextAnimationFrame()
expect(editor.getSelectedScreenRange()).toEqual [[2, 1], [2, 1]]
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(12)))
expect(nextAnimationFrame).toBe noAnimationFrame
expect(editor.getSelectedScreenRange()).toEqual [[2, 1], [2, 1]]
describe "when the gutter is meta-clicked and dragged", ->
beforeEach ->
editor.setSelectedScreenRange([[3, 0], [3, 2]])
@@ -2097,99 +2256,99 @@ describe "TextEditorComponent", ->
expect(editor.getSelectedScreenRange()).toEqual [[0, 0], [7, 4]]
describe "when the clicked row is after the current selection's tail", ->
it "selects to the beginning of the buffer row following the clicked buffer row", ->
it "selects to the beginning of the screen row following the clicked buffer row", ->
gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(11), shiftKey: true))
expect(editor.getSelectedScreenRange()).toEqual [[7, 4], [16, 0]]
describe "when the gutter is clicked and dragged", ->
describe "when dragging downward", ->
it "selects the buffer rows between the start and end of the drag", ->
it "selects the buffer row containing the click, then screen rows until the end of the drag", ->
gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1)))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6)))
nextAnimationFrame()
gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6)))
expect(editor.getSelectedScreenRange()).toEqual [[0, 0], [10, 0]]
expect(editor.getSelectedScreenRange()).toEqual [[0, 0], [6, 14]]
describe "when dragging upward", ->
it "selects the buffer rows between the start and end of the drag", ->
it "selects the buffer row containing the click, then screen rows until the end of the drag", ->
gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6)))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1)))
nextAnimationFrame()
gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(1)))
expect(editor.getSelectedScreenRange()).toEqual [[0, 0], [10, 0]]
expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [10, 0]]
describe "when the gutter is meta-clicked and dragged", ->
beforeEach ->
editor.setSelectedScreenRange([[7, 4], [7, 6]])
describe "when dragging downward", ->
it "selects the buffer rows between the start and end of the drag", ->
it "adds a selection from the buffer row containing the click to the screen row containing the end of the drag", ->
gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), metaKey: true))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(3), metaKey: true))
nextAnimationFrame()
gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(3), metaKey: true))
expect(editor.getSelectedScreenRanges()).toEqual [[[7, 4], [7, 6]], [[0, 0], [5, 0]]]
expect(editor.getSelectedScreenRanges()).toEqual [[[7, 4], [7, 6]], [[0, 0], [3, 14]]]
it "merges overlapping selections", ->
it "merges overlapping selections on mouseup", ->
gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), metaKey: true))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7), metaKey: true))
nextAnimationFrame()
gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(7), metaKey: true))
expect(editor.getSelectedScreenRanges()).toEqual [[[0, 0], [10, 0]]]
expect(editor.getSelectedScreenRanges()).toEqual [[[0, 0], [7, 12]]]
describe "when dragging upward", ->
it "selects the buffer rows between the start and end of the drag", ->
it "adds a selection from the buffer row containing the click to the screen row containing the end of the drag", ->
gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(17), metaKey: true))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11), metaKey: true))
nextAnimationFrame()
gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(11), metaKey: true))
expect(editor.getSelectedScreenRanges()).toEqual [[[7, 4], [7, 6]], [[10, 0], [19, 0]]]
expect(editor.getSelectedScreenRanges()).toEqual [[[7, 4], [7, 6]], [[11, 4], [19, 0]]]
it "merges overlapping selections", ->
it "merges overlapping selections on mouseup", ->
gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(17), metaKey: true))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(9), metaKey: true))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(5), metaKey: true))
nextAnimationFrame()
gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(9), metaKey: true))
gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(5), metaKey: true))
expect(editor.getSelectedScreenRanges()).toEqual [[[5, 0], [19, 0]]]
describe "when the gutter is shift-clicked and dragged", ->
describe "when the shift-click is below the existing selection's tail", ->
describe "when dragging downward", ->
it "selects the buffer rows between the existing selection's tail and the end of the drag", ->
it "selects the screen rows between the existing selection's tail and the end of the drag", ->
editor.setSelectedScreenRange([[1, 4], [1, 7]])
gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), shiftKey: true))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11)))
nextAnimationFrame()
expect(editor.getSelectedScreenRange()).toEqual [[1, 4], [16, 0]]
expect(editor.getSelectedScreenRange()).toEqual [[1, 4], [11, 14]]
describe "when dragging upward", ->
it "selects the buffer rows between the end of the drag and the tail of the existing selection", ->
it "selects the screen rows between the end of the drag and the tail of the existing selection", ->
editor.setSelectedScreenRange([[1, 4], [1, 7]])
gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(11), shiftKey: true))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7)))
nextAnimationFrame()
expect(editor.getSelectedScreenRange()).toEqual [[1, 4], [10, 0]]
expect(editor.getSelectedScreenRange()).toEqual [[1, 4], [7, 12]]
describe "when the shift-click is above the existing selection's tail", ->
describe "when dragging upward", ->
it "selects the buffer rows between the end of the drag and the tail of the existing selection", ->
it "selects the screen rows between the end of the drag and the tail of the existing selection", ->
editor.setSelectedScreenRange([[7, 4], [7, 6]])
gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(3), shiftKey: true))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1)))
nextAnimationFrame()
expect(editor.getSelectedScreenRange()).toEqual [[0, 0], [7, 4]]
expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [7, 4]]
describe "when dragging downward", ->
it "selects the buffer rows between the existing selection's tail and the end of the drag", ->
it "selects the screen rows between the existing selection's tail and the end of the drag", ->
editor.setSelectedScreenRange([[7, 4], [7, 6]])
gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), shiftKey: true))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(3)))
nextAnimationFrame()
expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [7, 4]]
expect(editor.getSelectedScreenRange()).toEqual [[3, 2], [7, 4]]
describe "focus handling", ->
inputNode = null
@@ -2680,27 +2839,27 @@ describe "TextEditorComponent", ->
describe "when a string is selected", ->
beforeEach ->
editor.setSelectedBufferRange [[0, 4], [0, 9]] # select 'quick'
editor.setSelectedBufferRanges [[[0, 4], [0, 9]], [[0, 16], [0, 19]]] # select 'quick' and 'fun'
it "inserts the chosen completion", ->
componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', target: inputNode))
componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 's', target: inputNode))
expect(editor.lineTextForBufferRow(0)).toBe 'var ssort = function () {'
expect(editor.lineTextForBufferRow(0)).toBe 'var ssort = sction () {'
componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 'sd', target: inputNode))
expect(editor.lineTextForBufferRow(0)).toBe 'var sdsort = function () {'
expect(editor.lineTextForBufferRow(0)).toBe 'var sdsort = sdction () {'
componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', target: inputNode))
componentNode.dispatchEvent(buildTextInputEvent(data: '速度', target: inputNode))
expect(editor.lineTextForBufferRow(0)).toBe 'var 速度sort = function () {'
expect(editor.lineTextForBufferRow(0)).toBe 'var 速度sort = 速度ction () {'
it "reverts back to the original text when the completion helper is dismissed", ->
componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', target: inputNode))
componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 's', target: inputNode))
expect(editor.lineTextForBufferRow(0)).toBe 'var ssort = function () {'
expect(editor.lineTextForBufferRow(0)).toBe 'var ssort = sction () {'
componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 'sd', target: inputNode))
expect(editor.lineTextForBufferRow(0)).toBe 'var sdsort = function () {'
expect(editor.lineTextForBufferRow(0)).toBe 'var sdsort = sdction () {'
componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', target: inputNode))
expect(editor.lineTextForBufferRow(0)).toBe 'var quicksort = function () {'
@@ -3189,7 +3348,7 @@ describe "TextEditorComponent", ->
{clientX, clientY}
clientCoordinatesForScreenRowInGutter = (screenRow) ->
positionOffset = wrapperNode.pixelPositionForScreenPosition([screenRow, 1])
positionOffset = wrapperNode.pixelPositionForScreenPosition([screenRow, Infinity])
gutterClientRect = componentNode.querySelector('.gutter').getBoundingClientRect()
clientX = gutterClientRect.left + positionOffset.left - editor.getScrollLeft()
clientY = gutterClientRect.top + positionOffset.top - editor.getScrollTop()

View File

@@ -559,6 +559,18 @@ describe "TextEditorPresenter", ->
expectStateUpdate presenter, -> advanceClock(100)
expect(presenter.getState().content.scrollingVertically).toBe false
describe ".maxHeight", ->
it "changes based on boundingClientRect", ->
presenter = buildPresenter(scrollTop: 0, lineHeight: 10)
expectStateUpdate presenter, ->
presenter.setBoundingClientRect(left: 0, top: 0, height: 20, width: 0)
expect(presenter.getState().content.maxHeight).toBe(20)
expectStateUpdate presenter, ->
presenter.setBoundingClientRect(left: 0, top: 0, height: 50, width: 0)
expect(presenter.getState().content.maxHeight).toBe(50)
describe ".scrollHeight", ->
it "is initialized based on the lineHeight, the number of lines, and the height", ->
presenter = buildPresenter(scrollTop: 0, lineHeight: 10)
@@ -1202,7 +1214,7 @@ describe "TextEditorPresenter", ->
# showing
expectStateUpdate presenter, -> editor.getSelections()[1].clear()
expect(stateForCursor(presenter, 1)).toEqual {top: 5, left: 5 * 10, width: 10, height: 10}
expect(stateForCursor(presenter, 1)).toEqual {top: 0, left: 5 * 10, width: 10, height: 10}
# hiding
expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[3, 4], [3, 5]])
@@ -1214,11 +1226,11 @@ describe "TextEditorPresenter", ->
# adding
expectStateUpdate presenter, -> editor.addCursorAtBufferPosition([4, 4])
expect(stateForCursor(presenter, 2)).toEqual {top: 5, left: 4 * 10, width: 10, height: 10}
expect(stateForCursor(presenter, 2)).toEqual {top: 0, left: 4 * 10, width: 10, height: 10}
# moving added cursor
expectStateUpdate presenter, -> editor.getCursors()[2].setBufferPosition([4, 6])
expect(stateForCursor(presenter, 2)).toEqual {top: 5, left: 6 * 10, width: 10, height: 10}
expect(stateForCursor(presenter, 2)).toEqual {top: 0, left: 6 * 10, width: 10, height: 10}
# destroying
destroyedCursor = editor.getCursors()[2]
@@ -2300,6 +2312,17 @@ describe "TextEditorPresenter", ->
expect(decorationState[decoration3.id]).toBeUndefined()
it "updates all the gutters, even when a gutter with higher priority is hidden", ->
hiddenGutter = {name: 'test-gutter-1', priority: -150, visible: false}
editor.addGutter(hiddenGutter)
# This update will scroll decoration1 out of view, and decoration3 into view.
expectStateUpdate presenter, -> presenter.setScrollTop(scrollTop + lineHeight * 5)
decorationState = getContentForGutterWithName(presenter, 'test-gutter')
expect(decorationState[decoration1.id]).toBeUndefined()
expect(decorationState[decoration3.id].top).toBeDefined()
it "updates when ::scrollTop changes", ->
# This update will scroll decoration1 out of view, and decoration3 into view.
expectStateUpdate presenter, -> presenter.setScrollTop(scrollTop + lineHeight * 5)

View File

@@ -207,6 +207,15 @@ describe "TextEditor", ->
lastCursor = editor.addCursorAtScreenPosition([2, 0])
expect(editor.getLastCursor()).toBe lastCursor
it "creates a new cursor at (0, 0) if the last cursor has been destroyed", ->
editor.getLastCursor().destroy()
expect(editor.getLastCursor().getBufferPosition()).toEqual([0, 0])
describe ".getCursors()", ->
it "creates a new cursor at (0, 0) if the last cursor has been destroyed", ->
editor.getLastCursor().destroy()
expect(editor.getCursors()[0].getBufferPosition()).toEqual([0, 0])
describe "when the cursor moves", ->
it "clears a goal column established by vertical movement", ->
editor.setText('b')
@@ -1027,6 +1036,16 @@ describe "TextEditor", ->
beforeEach ->
selection = editor.getLastSelection()
describe ".getLastSelection()", ->
it "creates a new selection at (0, 0) if the last selection has been destroyed", ->
editor.getLastSelection().destroy()
expect(editor.getLastSelection().getBufferRange()).toEqual([[0, 0], [0, 0]])
describe ".getSelections()", ->
it "creates a new selection at (0, 0) if the last selection has been destroyed", ->
editor.getLastSelection().destroy()
expect(editor.getSelections()[0].getBufferRange()).toEqual([[0, 0], [0, 0]])
describe "when the selection range changes", ->
it "emits an event with the old range, new range, and the selection that moved", ->
editor.setSelectedBufferRange([[3, 0], [4, 5]])
@@ -2544,7 +2563,6 @@ describe "TextEditor", ->
expect(editor.indentationForBufferRow(1)).toBe 1
expect(editor.indentationForBufferRow(2)).toBe 0
describe ".backspace()", ->
describe "when there is a single cursor", ->
changeScreenRangeHandler = null
@@ -3219,6 +3237,28 @@ describe "TextEditor", ->
items
"""
describe ".copyOnlySelectedText()", ->
describe "when thee are multiple selections", ->
it "copies selected text onto the clipboard", ->
editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]], [[2, 8], [2, 13]]])
editor.copyOnlySelectedText()
expect(buffer.lineForRow(0)).toBe "var quicksort = function () {"
expect(buffer.lineForRow(1)).toBe " var sort = function(items) {"
expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;"
expect(clipboard.readText()).toBe 'quicksort\nsort\nitems'
expect(atom.clipboard.read()).toEqual """
quicksort
sort
items
"""
describe "when no text is selected", ->
it "does not copy anything", ->
editor.setCursorBufferPosition([1, 5])
editor.copyOnlySelectedText()
expect(atom.clipboard.read()).toEqual "initial clipboard content"
describe ".pasteText()", ->
copyText = (text, {startColumn, textEditor}={}) ->
startColumn ?= 0
@@ -3694,6 +3734,63 @@ describe "TextEditor", ->
expect(buffer.lineForRow(0)).not.toContain "foo"
expect(buffer.lineForRow(0)).toContain "fovar"
it "restores cursors and selections to their states before and after undone and redone changes", ->
editor.setSelectedBufferRanges([
[[0, 0], [0, 0]],
[[1, 0], [1, 3]],
])
editor.insertText("abc")
expect(editor.getSelectedBufferRanges()).toEqual [
[[0, 3], [0, 3]],
[[1, 3], [1, 3]]
]
editor.setCursorBufferPosition([0, 0])
editor.setSelectedBufferRanges([
[[2, 0], [2, 0]],
[[3, 0], [3, 0]],
[[4, 0], [4, 3]],
])
editor.insertText("def")
expect(editor.getSelectedBufferRanges()).toEqual [
[[2, 3], [2, 3]],
[[3, 3], [3, 3]]
[[4, 3], [4, 3]]
]
editor.setCursorBufferPosition([0, 0])
editor.undo()
expect(editor.getSelectedBufferRanges()).toEqual [
[[2, 0], [2, 0]],
[[3, 0], [3, 0]],
[[4, 0], [4, 3]],
]
editor.undo()
expect(editor.getSelectedBufferRanges()).toEqual [
[[0, 0], [0, 0]],
[[1, 0], [1, 3]]
]
editor.redo()
expect(editor.getSelectedBufferRanges()).toEqual [
[[0, 3], [0, 3]],
[[1, 3], [1, 3]]
]
editor.redo()
expect(editor.getSelectedBufferRanges()).toEqual [
[[2, 3], [2, 3]],
[[3, 3], [3, 3]]
[[4, 3], [4, 3]]
]
it "restores the selected ranges after undo and redo", ->
editor.setSelectedBufferRanges([[[1, 6], [1, 10]], [[1, 22], [1, 27]]])
editor.delete()
@@ -3974,23 +4071,177 @@ describe "TextEditor", ->
expect(editor.lineTextForBufferRow(0)).toBe 'abC'
expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 2]]
describe "soft-tabs detection", ->
it "assigns soft / hard tabs based on the contents of the buffer, or uses the default if unknown", ->
waitsForPromise ->
atom.workspace.open('sample.js', softTabs: false).then (editor) ->
expect(editor.getSoftTabs()).toBeTruthy()
describe "soft and hard tabs", ->
afterEach ->
atom.packages.deactivatePackages()
atom.packages.unloadPackages()
waitsForPromise ->
atom.workspace.open('sample-with-tabs.coffee', softTabs: true).then (editor) ->
expect(editor.getSoftTabs()).toBeFalsy()
describe "when editor.tabType is 'auto'", ->
beforeEach ->
atom.config.set('editor.tabType', 'auto')
waitsForPromise ->
atom.workspace.open('sample-with-tabs-and-initial-comment.js', softTabs: true).then (editor) ->
expect(editor.getSoftTabs()).toBeFalsy()
it "auto-detects soft / hard tabs based on the contents of the buffer, or uses the default if unknown, and setSoftTabs() overrides", ->
waitsForPromise ->
atom.workspace.open('sample.js', softTabs: false).then (editor) ->
expect(editor.getSoftTabs()).toBe true
editor.setSoftTabs(false)
expect(editor.getSoftTabs()).toBe false
waitsForPromise ->
atom.workspace.open(null, softTabs: false).then (editor) ->
expect(editor.getSoftTabs()).toBeFalsy()
waitsForPromise ->
atom.workspace.open('sample-with-tabs.coffee', softTabs: true).then (editor) ->
expect(editor.getSoftTabs()).toBe false
editor.setSoftTabs(true)
expect(editor.getSoftTabs()).toBe true
waitsForPromise ->
atom.workspace.open('sample-with-tabs-and-initial-comment.js', softTabs: true).then (editor) ->
expect(editor.getSoftTabs()).toBe false
editor.setSoftTabs(true)
expect(editor.getSoftTabs()).toBe true
waitsForPromise ->
atom.workspace.open(null, softTabs: false).then (editor) ->
expect(editor.getSoftTabs()).toBe false
editor.setSoftTabs(true)
expect(editor.getSoftTabs()).toBe true
it "resets the tab style when tokenization is complete", ->
editor.destroy()
waitsForPromise ->
atom.project.open('sample-with-tabs-and-leading-comment.coffee').then (o) -> editor = o
runs ->
expect(editor.softTabs).toBe true
waitsForPromise ->
atom.packages.activatePackage('language-coffee-script')
runs ->
expect(editor.softTabs).toBe false
it "uses hard tabs in Makefile files", ->
# FIXME remove once this is handled by a scoped setting in the
# language-make package
waitsForPromise ->
atom.packages.activatePackage('language-make')
waitsForPromise ->
atom.project.open('Makefile').then (o) -> editor = o
runs ->
expect(editor.softTabs).toBe false
describe "when editor.tabType is 'hard'", ->
beforeEach ->
atom.config.set('editor.tabType', 'hard')
it "always chooses hard tabs and setSoftTabs() overrides the setting", ->
waitsForPromise ->
atom.workspace.open('sample.js').then (editor) ->
expect(editor.getSoftTabs()).toBe false
editor.setSoftTabs(true)
expect(editor.getSoftTabs()).toBe true
waitsForPromise ->
atom.workspace.open('sample-with-tabs.coffee').then (editor) ->
expect(editor.getSoftTabs()).toBe false
editor.setSoftTabs(true)
expect(editor.getSoftTabs()).toBe true
waitsForPromise ->
atom.workspace.open('sample-with-tabs-and-initial-comment.js').then (editor) ->
expect(editor.getSoftTabs()).toBe false
editor.setSoftTabs(true)
expect(editor.getSoftTabs()).toBe true
waitsForPromise ->
atom.workspace.open(null).then (editor) ->
expect(editor.getSoftTabs()).toBe false
editor.setSoftTabs(true)
expect(editor.getSoftTabs()).toBe true
describe "when editor.tabType is 'soft'", ->
beforeEach ->
atom.config.set('editor.tabType', 'soft')
it "always chooses soft tabs and setSoftTabs() overrides the setting", ->
waitsForPromise ->
atom.workspace.open('sample.js').then (editor) ->
expect(editor.getSoftTabs()).toBe true
editor.setSoftTabs(false)
expect(editor.getSoftTabs()).toBe false
waitsForPromise ->
atom.workspace.open('sample-with-tabs.coffee').then (editor) ->
expect(editor.getSoftTabs()).toBe true
editor.setSoftTabs(false)
expect(editor.getSoftTabs()).toBe false
waitsForPromise ->
atom.workspace.open('sample-with-tabs-and-initial-comment.js').then (editor) ->
expect(editor.getSoftTabs()).toBe true
editor.setSoftTabs(false)
expect(editor.getSoftTabs()).toBe false
waitsForPromise ->
atom.workspace.open(null).then (editor) ->
expect(editor.getSoftTabs()).toBe true
editor.setSoftTabs(false)
expect(editor.getSoftTabs()).toBe false
it "keeps the tabType when tokenization is complete", ->
editor.destroy()
waitsForPromise ->
atom.project.open('sample-with-tabs-and-leading-comment.coffee').then (o) -> editor = o
runs ->
expect(editor.softTabs).toBe true
waitsForPromise ->
atom.packages.activatePackage('language-coffee-script')
runs ->
expect(editor.softTabs).toBe true
describe "when editor.tabType changes", ->
beforeEach ->
atom.config.set('editor.tabType', 'auto')
it "updates based on the value chosen", ->
waitsForPromise ->
atom.workspace.open('sample.js').then (editor) ->
expect(editor.getSoftTabs()).toBe true
atom.config.set('editor.tabType', 'hard')
expect(editor.getSoftTabs()).toBe false
atom.config.set('editor.tabType', 'auto')
expect(editor.getSoftTabs()).toBe true
atom.config.set('editor.tabType', 'hard', scopeSelector: '.source.js')
expect(editor.getSoftTabs()).toBe false
waitsForPromise ->
atom.workspace.open('sample-with-tabs.coffee').then (editor) ->
expect(editor.getSoftTabs()).toBe false
atom.config.set('editor.tabType', 'soft')
expect(editor.getSoftTabs()).toBe true
atom.config.set('editor.tabType', 'auto')
expect(editor.getSoftTabs()).toBe false
describe "when the grammar changes", ->
coffeeEditor = null
beforeEach ->
atom.config.set('editor.tabType', 'hard', scopeSelector: '.source.js')
atom.config.set('editor.tabType', 'soft', scopeSelector: '.source.coffee')
waitsForPromise ->
atom.packages.activatePackage('language-coffee-script')
it "updates based on the value chosen", ->
expect(editor.getSoftTabs()).toBe false
editor.setGrammar(atom.grammars.grammarForScopeName('source.coffee'))
expect(editor.getSoftTabs()).toBe true
describe '.getTabLength()', ->
describe 'when scoped settings are used', ->
@@ -4221,39 +4472,6 @@ describe "TextEditor", ->
coffeeEditor.insertText("\n")
expect(coffeeEditor.lineTextForBufferRow(2)).toBe ""
describe "soft and hard tabs", ->
afterEach ->
atom.packages.deactivatePackages()
atom.packages.unloadPackages()
it "resets the tab style when tokenization is complete", ->
editor.destroy()
waitsForPromise ->
atom.project.open('sample-with-tabs-and-leading-comment.coffee').then (o) -> editor = o
runs ->
expect(editor.softTabs).toBe true
waitsForPromise ->
atom.packages.activatePackage('language-coffee-script')
runs ->
expect(editor.softTabs).toBe false
it "uses hard tabs in Makefile files", ->
# FIXME remove once this is handled by a scoped setting in the
# language-make package
waitsForPromise ->
atom.packages.activatePackage('language-make')
waitsForPromise ->
atom.project.open('Makefile').then (o) -> editor = o
runs ->
expect(editor.softTabs).toBe false
describe ".destroy()", ->
it "destroys all markers associated with the edit session", ->
editor.foldAll()
@@ -4615,26 +4833,21 @@ describe "TextEditor", ->
expect(editor.getScrollBottom()).toBe (9 + editor.getVerticalScrollMargin()) * 10
describe ".pageUp/Down()", ->
it "scrolls one screen height up or down and moves the cursor one page length", ->
it "moves the cursor down one page length", ->
editor.setLineHeightInPixels(10)
editor.setHeight(50)
expect(editor.getScrollHeight()).toBe 130
expect(editor.getCursorBufferPosition().row).toBe 0
editor.pageDown()
expect(editor.getScrollTop()).toBe 50
expect(editor.getCursorBufferPosition().row).toBe 5
editor.pageDown()
expect(editor.getScrollTop()).toBe 80
expect(editor.getCursorBufferPosition().row).toBe 10
editor.pageUp()
expect(editor.getScrollTop()).toBe 30
expect(editor.getCursorBufferPosition().row).toBe 5
editor.pageUp()
expect(editor.getScrollTop()).toBe 0
expect(editor.getCursorBufferPosition().row).toBe 0
describe ".selectPageUp/Down()", ->
@@ -4645,28 +4858,22 @@ describe "TextEditor", ->
expect(editor.getCursorBufferPosition().row).toBe 0
editor.selectPageDown()
expect(editor.getScrollTop()).toBe 30
expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [5, 0]]]
editor.selectPageDown()
expect(editor.getScrollTop()).toBe 80
expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [10, 0]]]
editor.selectPageDown()
expect(editor.getScrollTop()).toBe 80
expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [12, 2]]]
editor.moveToBottom()
editor.selectPageUp()
expect(editor.getScrollTop()).toBe 50
expect(editor.getSelectedBufferRanges()).toEqual [[[7, 0], [12, 2]]]
editor.selectPageUp()
expect(editor.getScrollTop()).toBe 0
expect(editor.getSelectedBufferRanges()).toEqual [[[2, 0], [12, 2]]]
editor.selectPageUp()
expect(editor.getScrollTop()).toBe 0
expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [12, 2]]]
describe '.get/setPlaceholderText()', ->

View File

@@ -1,25 +1,4 @@
typescript = require '../src/typescript'
crypto = require 'crypto'
describe "TypeScript transpiler support", ->
describe "::createTypeScriptVersionAndOptionsDigest", ->
it "returns a digest for the library version and specified options", ->
defaultOptions =
target: 1 # ES5
module: 'commonjs'
sourceMap: true
version = '1.4.1'
shasum = crypto.createHash('sha1')
shasum.update('typescript', 'utf8')
shasum.update('\0', 'utf8')
shasum.update(version, 'utf8')
shasum.update('\0', 'utf8')
shasum.update(JSON.stringify(defaultOptions))
expectedDigest = shasum.digest('hex')
observedDigest = typescript.createTypeScriptVersionAndOptionsDigest(version, defaultOptions)
expect(observedDigest).toEqual expectedDigest
describe "when there is a .ts file", ->
it "transpiles it using typescript", ->
transpiled = require('./fixtures/typescript/valid.ts')

View File

@@ -26,6 +26,14 @@ describe "ViewRegistry", ->
expect(node.textContent).toBe "Hello"
expect(node.spacePenView).toBe view
describe "when passed an object with an element property", ->
it "returns the element property if it's an instance of HTMLElement", ->
class TestComponent
constructor: -> @element = document.createElement('div')
component = new TestComponent
expect(registry.getView(component)).toBe component.element
describe "when passed a model object", ->
describe "when a view provider is registered matching the object's constructor", ->
it "constructs a view element and assigns the model on it", ->

View File

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

View File

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

63
src/babel.js Normal file
View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

53
src/typescript.js Normal file
View File

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

View File

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

View File

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

View File

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

7
static/babelrc.json Normal file
View File

@@ -0,0 +1,7 @@
{
"breakConfig": true,
"sourceMap": "inline",
"blacklist": ["es6.forOf", "useStrict"],
"optional": ["asyncToGenerator"],
"stage": 0
}

View File

@@ -1,234 +1,197 @@
(function() {
(function () {
var fs = require('fs')
var path = require('path')
var fs = require('fs');
var path = require('path');
var loadSettings = null
var loadSettingsError = null
var loadSettings = null;
var loadSettingsError = null;
window.onload = function() {
try {
var startTime = Date.now();
process.on('unhandledRejection', function(error, promise) {
console.error('Unhandled promise rejection %o with error: %o', promise, error);
});
// Ensure ATOM_HOME is always set before anything else is required
setupAtomHome();
// Normalize to make sure drive letter case is consistent on Windows
process.resourcesPath = path.normalize(process.resourcesPath);
if (loadSettingsError) {
throw loadSettingsError;
}
var devMode = loadSettings.devMode || !loadSettings.resourcePath.startsWith(process.resourcesPath + path.sep);
if (devMode) {
setupDeprecatedPackages();
}
if (loadSettings.profileStartup) {
profileStartup(loadSettings, Date.now() - startTime);
} else {
setupWindow(loadSettings);
setLoadTime(Date.now() - startTime);
}
} catch (error) {
handleSetupError(error);
}
}
var getCacheDirectory = function() {
var 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 === 'root' && process.env.SUDO_USER && process.env.SUDO_USER !== process.env.USER) {
cacheDir = path.join(cacheDir, 'root');
}
return cacheDir;
}
var setLoadTime = function(loadTime) {
if (global.atom) {
global.atom.loadTime = loadTime;
console.log('Window load time: ' + global.atom.getWindowLoadTime() + 'ms');
}
}
var handleSetupError = function(error) {
var currentWindow = require('remote').getCurrentWindow();
currentWindow.setSize(800, 600);
currentWindow.center();
currentWindow.show();
currentWindow.openDevTools();
console.error(error.stack || error);
}
var setupWindow = function(loadSettings) {
var cacheDir = getCacheDirectory();
setupCoffeeCache(cacheDir);
ModuleCache = require('../src/module-cache');
ModuleCache.register(loadSettings);
ModuleCache.add(loadSettings.resourcePath);
// Only include deprecated APIs when running core spec
require('grim').includeDeprecatedAPIs = isRunningCoreSpecs(loadSettings);
// Start the crash reporter before anything else.
require('crash-reporter').start({
productName: 'Atom',
companyName: 'GitHub',
// By explicitly passing the app version here, we could save the call
// of "require('remote').require('app').getVersion()".
extra: {_version: loadSettings.appVersion}
});
setupVmCompatibility();
setupCsonCache(cacheDir);
setupSourceMapCache(cacheDir);
setupBabel(cacheDir);
setupTypeScript(cacheDir);
require(loadSettings.bootstrapScript);
require('ipc').sendChannel('window-command', 'window:loaded');
}
var setupCoffeeCache = function(cacheDir) {
var CoffeeCache = require('coffee-cash');
CoffeeCache.setCacheDirectory(path.join(cacheDir, 'coffee'));
CoffeeCache.register();
}
var setupAtomHome = function() {
if (!process.env.ATOM_HOME) {
var home;
if (process.platform === 'win32') {
home = process.env.USERPROFILE;
} else {
home = process.env.HOME;
}
var atomHome = path.join(home, '.atom');
try {
atomHome = fs.realpathSync(atomHome);
} catch (error) {
// Ignore since the path might just not exist yet.
}
process.env.ATOM_HOME = atomHome;
}
}
var setupBabel = function(cacheDir) {
var babel = require('../src/babel');
babel.setCacheDirectory(path.join(cacheDir, 'js', 'babel'));
babel.register();
}
var setupTypeScript = function(cacheDir) {
var typescript = require('../src/typescript');
typescript.setCacheDirectory(path.join(cacheDir, 'typescript'));
typescript.register();
}
var setupCsonCache = function(cacheDir) {
require('season').setCacheDir(path.join(cacheDir, 'cson'));
}
var setupSourceMapCache = function(cacheDir) {
require('coffeestack').setCacheDirectory(path.join(cacheDir, 'coffee', 'source-maps'));
}
var setupVmCompatibility = function() {
var vm = require('vm');
if (!vm.Script.createContext) {
vm.Script.createContext = vm.createContext;
}
}
var setupDeprecatedPackages = function() {
var metadata = require('../package.json');
if (!metadata._deprecatedPackages) {
try {
metadata._deprecatedPackages = require('../build/deprecated-packages.json');
} catch(requireError) {
console.error('Failed to setup deprecated packages list', requireError.stack);
}
}
}
var profileStartup = function(loadSettings, initialTime) {
var profile = function() {
console.profile('startup');
window.onload = function () {
try {
var startTime = Date.now()
setupWindow(loadSettings);
setLoadTime(Date.now() - startTime + initialTime);
process.on('unhandledRejection', function (error, promise) {
console.error('Unhandled promise rejection %o with error: %o', promise, error)
})
// Ensure ATOM_HOME is always set before anything else is required
setupAtomHome()
// Normalize to make sure drive letter case is consistent on Windows
process.resourcesPath = path.normalize(process.resourcesPath)
if (loadSettingsError) {
throw loadSettingsError
}
var devMode = loadSettings.devMode || !loadSettings.resourcePath.startsWith(process.resourcesPath + path.sep)
if (devMode) {
setupDeprecatedPackages()
}
if (loadSettings.profileStartup) {
profileStartup(loadSettings, Date.now() - startTime)
} else {
setupWindow(loadSettings)
setLoadTime(Date.now() - startTime)
}
} catch (error) {
handleSetupError(error);
} finally {
console.profileEnd('startup');
console.log("Switch to the Profiles tab to view the created startup profile")
handleSetupError(error)
}
};
var currentWindow = require('remote').getCurrentWindow();
if (currentWindow.devToolsWebContents) {
profile();
} else {
currentWindow.openDevTools();
currentWindow.once('devtools-opened', function() {
setTimeout(profile, 100);
});
}
}
var parseLoadSettings = function() {
var rawLoadSettings = decodeURIComponent(location.hash.substr(1));
try {
loadSettings = JSON.parse(rawLoadSettings);
} catch (error) {
console.error("Failed to parse load settings: " + rawLoadSettings);
loadSettingsError = error;
}
}
var setupWindowBackground = function() {
if (loadSettings && loadSettings.isSpec) {
return;
}
var backgroundColor = window.localStorage.getItem('atom:window-background-color');
if (!backgroundColor) {
return;
function setLoadTime (loadTime) {
if (global.atom) {
global.atom.loadTime = loadTime
console.log('Window load time: ' + global.atom.getWindowLoadTime() + 'ms')
}
}
var backgroundStylesheet = document.createElement('style');
backgroundStylesheet.type = 'text/css';
backgroundStylesheet.innerText = 'html, body { background: ' + backgroundColor + '; }';
document.head.appendChild(backgroundStylesheet);
function handleSetupError (error) {
var currentWindow = require('remote').getCurrentWindow()
currentWindow.setSize(800, 600)
currentWindow.center()
currentWindow.show()
currentWindow.openDevTools()
console.error(error.stack || error)
}
// Remove once the page loads
window.addEventListener("load", function loadWindow() {
window.removeEventListener("load", loadWindow, false);
setTimeout(function() {
backgroundStylesheet.remove();
backgroundStylesheet = null;
}, 1000);
}, false);
}
function setupWindow (loadSettings) {
var CompileCache = require('../src/compile-cache')
CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME)
var isRunningCoreSpecs = function(loadSettings) {
return !!(loadSettings &&
loadSettings.isSpec &&
loadSettings.specDirectory &&
loadSettings.resourcePath &&
path.dirname(loadSettings.specDirectory) === loadSettings.resourcePath);
}
var ModuleCache = require('../src/module-cache')
ModuleCache.register(loadSettings)
ModuleCache.add(loadSettings.resourcePath)
parseLoadSettings();
setupWindowBackground();
// Only include deprecated APIs when running core spec
require('grim').includeDeprecatedAPIs = isRunningCoreSpecs(loadSettings)
})();
// Start the crash reporter before anything else.
require('crash-reporter').start({
productName: 'Atom',
companyName: 'GitHub',
// By explicitly passing the app version here, we could save the call
// of "require('remote').require('app').getVersion()".
extra: {_version: loadSettings.appVersion}
})
setupVmCompatibility()
setupCsonCache(CompileCache.getCacheDirectory())
require(loadSettings.bootstrapScript)
require('ipc').sendChannel('window-command', 'window:loaded')
}
function setupAtomHome () {
if (!process.env.ATOM_HOME) {
var home
if (process.platform === 'win32') {
home = process.env.USERPROFILE
} else {
home = process.env.HOME
}
var atomHome = path.join(home, '.atom')
try {
atomHome = fs.realpathSync(atomHome)
} catch (error) {
// Ignore since the path might just not exist yet.
}
process.env.ATOM_HOME = atomHome
}
}
function setupCsonCache (cacheDir) {
require('season').setCacheDir(path.join(cacheDir, 'cson'))
}
function setupVmCompatibility () {
var vm = require('vm')
if (!vm.Script.createContext) {
vm.Script.createContext = vm.createContext
}
}
function setupDeprecatedPackages () {
var metadata = require('../package.json')
if (!metadata._deprecatedPackages) {
try {
metadata._deprecatedPackages = require('../build/deprecated-packages.json')
} catch(requireError) {
console.error('Failed to setup deprecated packages list', requireError.stack)
}
}
}
function profileStartup (loadSettings, initialTime) {
function profile () {
console.profile('startup')
try {
var startTime = Date.now()
setupWindow(loadSettings)
setLoadTime(Date.now() - startTime + initialTime)
} catch (error) {
handleSetupError(error)
} finally {
console.profileEnd('startup')
console.log('Switch to the Profiles tab to view the created startup profile')
}
}
var currentWindow = require('remote').getCurrentWindow()
if (currentWindow.devToolsWebContents) {
profile()
} else {
currentWindow.openDevTools()
currentWindow.once('devtools-opened', function () {
setTimeout(profile, 100)
})
}
}
function parseLoadSettings () {
var rawLoadSettings = decodeURIComponent(window.location.hash.substr(1))
try {
loadSettings = JSON.parse(rawLoadSettings)
} catch (error) {
console.error('Failed to parse load settings: ' + rawLoadSettings)
loadSettingsError = error
}
}
function setupWindowBackground () {
if (loadSettings && loadSettings.isSpec) {
return
}
var backgroundColor = window.localStorage.getItem('atom:window-background-color')
if (!backgroundColor) {
return
}
var backgroundStylesheet = document.createElement('style')
backgroundStylesheet.type = 'text/css'
backgroundStylesheet.innerText = 'html, body { background: ' + backgroundColor + '; }'
document.head.appendChild(backgroundStylesheet)
// Remove once the page loads
window.addEventListener('load', function loadWindow () {
window.removeEventListener('load', loadWindow, false)
setTimeout(function () {
backgroundStylesheet.remove()
backgroundStylesheet = null
}, 1000)
}, false)
}
function isRunningCoreSpecs (loadSettings) {
return !!(loadSettings &&
loadSettings.isSpec &&
loadSettings.specDirectory &&
loadSettings.resourcePath &&
path.dirname(loadSettings.specDirectory) === loadSettings.resourcePath)
}
parseLoadSettings()
setupWindowBackground()
})()