mirror of
https://github.com/atom/atom.git
synced 2026-02-11 23:25:03 -05:00
Merge remote-tracking branch 'upstream/master' into move-lines-up-and-down-with-multiple-selections
This commit is contained in:
@@ -6,6 +6,6 @@
|
||||
"url": "https://github.com/atom/atom.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"atom-package-manager": "1.0.1"
|
||||
"atom-package-manager": "1.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']
|
||||
|
||||
18
build/lib/uses-babel.coffee
Normal file
18
build/lib/uses-babel.coffee
Normal 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)
|
||||
@@ -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",
|
||||
|
||||
@@ -10,7 +10,7 @@ module.exports = (grunt) ->
|
||||
|
||||
unpack = [
|
||||
'*.node'
|
||||
'.ctags'
|
||||
'ctags-config'
|
||||
'ctags-darwin'
|
||||
'ctags-linux'
|
||||
'ctags-win32.exe'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/)
|
||||
|
||||
|
||||
52
package.json
52
package.json
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = '<>'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
3
spec/fixtures/babel/6to5-single-quotes.js
vendored
3
spec/fixtures/babel/6to5-single-quotes.js
vendored
@@ -1,3 +0,0 @@
|
||||
'use 6to5';
|
||||
|
||||
module.exports = v => v + 1
|
||||
@@ -1,3 +1,3 @@
|
||||
"use 6to5";
|
||||
/** @babel */
|
||||
|
||||
module.exports = v => v + 1
|
||||
@@ -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')
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()', ->
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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", ->
|
||||
|
||||
@@ -9,7 +9,7 @@ _ = require 'underscore-plus'
|
||||
{deprecate, includeDeprecatedAPIs} = require 'grim'
|
||||
{CompositeDisposable, Emitter} = require 'event-kit'
|
||||
fs = require 'fs-plus'
|
||||
{convertStackTrace, convertLine} = require 'coffeestack'
|
||||
{mapSourcePosition} = require 'source-map-support'
|
||||
Model = require './model'
|
||||
{$} = require './space-pen-extensions'
|
||||
WindowEventHandler = require './window-event-handler'
|
||||
@@ -196,15 +196,11 @@ class Atom extends Model
|
||||
#
|
||||
# Call after this instance has been assigned to the `atom` global.
|
||||
initialize: ->
|
||||
sourceMapCache = {}
|
||||
|
||||
window.onerror = =>
|
||||
@lastUncaughtError = Array::slice.call(arguments)
|
||||
[message, url, line, column, originalError] = @lastUncaughtError
|
||||
|
||||
convertedLine = convertLine(url, line, column, sourceMapCache)
|
||||
{line, column} = convertedLine if convertedLine?
|
||||
originalError.stack = convertStackTrace(originalError.stack, sourceMapCache) if originalError
|
||||
{line, column} = mapSourcePosition({source: url, line, column})
|
||||
|
||||
eventObject = {message, url, line, column, originalError}
|
||||
|
||||
@@ -673,6 +669,7 @@ class Atom extends Model
|
||||
@windowEventHandler?.unsubscribe()
|
||||
|
||||
openInitialEmptyEditorIfNecessary: ->
|
||||
return unless @config.get('core.openEmptyEditorOnStart')
|
||||
if @getLoadSettings().initialPaths?.length is 0 and @workspace.getPaneItems().length is 0
|
||||
@workspace.open(null)
|
||||
|
||||
|
||||
200
src/babel.coffee
200
src/babel.coffee
@@ -1,200 +0,0 @@
|
||||
###
|
||||
Cache for source code transpiled by Babel.
|
||||
|
||||
Inspired by https://github.com/atom/atom/blob/6b963a562f8d495fbebe6abdbafbc7caf705f2c3/src/coffee-cache.coffee.
|
||||
###
|
||||
|
||||
crypto = require 'crypto'
|
||||
fs = require 'fs-plus'
|
||||
path = require 'path'
|
||||
babel = null # Defer until used
|
||||
Grim = null # Defer until used
|
||||
|
||||
stats =
|
||||
hits: 0
|
||||
misses: 0
|
||||
|
||||
defaultOptions =
|
||||
# Currently, the cache key is a function of:
|
||||
# * The version of Babel used to transpile the .js file.
|
||||
# * The contents of this defaultOptions object.
|
||||
# * The contents of the .js file.
|
||||
# That means that we cannot allow information from an unknown source
|
||||
# to affect the cache key for the output of transpilation, which means
|
||||
# we cannot allow users to override these default options via a .babelrc
|
||||
# file, because the contents of that .babelrc file will not make it into
|
||||
# the cache key. It would be great to support .babelrc files once we
|
||||
# have a way to do so that is safe with respect to caching.
|
||||
breakConfig: true
|
||||
|
||||
# The Chrome dev tools will show the original version of the file
|
||||
# when the source map is inlined.
|
||||
sourceMap: 'inline'
|
||||
|
||||
# Blacklisted features do not get transpiled. Features that are
|
||||
# natively supported in the target environment should be listed
|
||||
# here. Because Atom uses a bleeding edge version of Node/io.js,
|
||||
# I think this can include es6.arrowFunctions, es6.classes, and
|
||||
# possibly others, but I want to be conservative.
|
||||
blacklist: [
|
||||
'es6.forOf'
|
||||
'useStrict'
|
||||
]
|
||||
|
||||
optional: [
|
||||
# Target a version of the regenerator runtime that
|
||||
# supports yield so the transpiled code is cleaner/smaller.
|
||||
'asyncToGenerator'
|
||||
]
|
||||
|
||||
# Includes support for es7 features listed at:
|
||||
# http://babeljs.io/docs/usage/experimental/.
|
||||
stage: 0
|
||||
|
||||
|
||||
###
|
||||
shasum - Hash with an update() method.
|
||||
value - Must be a value that could be returned by JSON.parse().
|
||||
###
|
||||
updateDigestForJsonValue = (shasum, value) ->
|
||||
# Implmentation is similar to that of pretty-printing a JSON object, except:
|
||||
# * Strings are not escaped.
|
||||
# * No effort is made to avoid trailing commas.
|
||||
# These shortcuts should not affect the correctness of this function.
|
||||
type = typeof value
|
||||
if type is 'string'
|
||||
shasum.update('"', 'utf8')
|
||||
shasum.update(value, 'utf8')
|
||||
shasum.update('"', 'utf8')
|
||||
else if type in ['boolean', 'number']
|
||||
shasum.update(value.toString(), 'utf8')
|
||||
else if value is null
|
||||
shasum.update('null', 'utf8')
|
||||
else if Array.isArray value
|
||||
shasum.update('[', 'utf8')
|
||||
for item in value
|
||||
updateDigestForJsonValue(shasum, item)
|
||||
shasum.update(',', 'utf8')
|
||||
shasum.update(']', 'utf8')
|
||||
else
|
||||
# value must be an object: be sure to sort the keys.
|
||||
keys = Object.keys value
|
||||
keys.sort()
|
||||
|
||||
shasum.update('{', 'utf8')
|
||||
for key in keys
|
||||
updateDigestForJsonValue(shasum, key)
|
||||
shasum.update(': ', 'utf8')
|
||||
updateDigestForJsonValue(shasum, value[key])
|
||||
shasum.update(',', 'utf8')
|
||||
shasum.update('}', 'utf8')
|
||||
|
||||
createBabelVersionAndOptionsDigest = (version, options) ->
|
||||
shasum = crypto.createHash('sha1')
|
||||
# Include the version of babel in the hash.
|
||||
shasum.update('babel-core', 'utf8')
|
||||
shasum.update('\0', 'utf8')
|
||||
shasum.update(version, 'utf8')
|
||||
shasum.update('\0', 'utf8')
|
||||
updateDigestForJsonValue(shasum, options)
|
||||
shasum.digest('hex')
|
||||
|
||||
cacheDir = null
|
||||
jsCacheDir = null
|
||||
|
||||
getCachePath = (sourceCode) ->
|
||||
digest = crypto.createHash('sha1').update(sourceCode, 'utf8').digest('hex')
|
||||
|
||||
unless jsCacheDir?
|
||||
to5Version = require('babel-core/package.json').version
|
||||
jsCacheDir = path.join(cacheDir, createBabelVersionAndOptionsDigest(to5Version, defaultOptions))
|
||||
|
||||
path.join(jsCacheDir, "#{digest}.js")
|
||||
|
||||
getCachedJavaScript = (cachePath) ->
|
||||
if fs.isFileSync(cachePath)
|
||||
try
|
||||
cachedJavaScript = fs.readFileSync(cachePath, 'utf8')
|
||||
stats.hits++
|
||||
return cachedJavaScript
|
||||
null
|
||||
|
||||
# Returns the babel options that should be used to transpile filePath.
|
||||
createOptions = (filePath) ->
|
||||
options = filename: filePath
|
||||
for key, value of defaultOptions
|
||||
options[key] = value
|
||||
options
|
||||
|
||||
transpile = (sourceCode, filePath, cachePath) ->
|
||||
options = createOptions(filePath)
|
||||
babel ?= require 'babel-core'
|
||||
js = babel.transform(sourceCode, options).code
|
||||
stats.misses++
|
||||
|
||||
try
|
||||
fs.writeFileSync(cachePath, js)
|
||||
|
||||
js
|
||||
|
||||
# Function that obeys the contract of an entry in the require.extensions map.
|
||||
# Returns the transpiled version of the JavaScript code at filePath, which is
|
||||
# either generated on the fly or pulled from cache.
|
||||
loadFile = (module, filePath) ->
|
||||
sourceCode = fs.readFileSync(filePath, 'utf8')
|
||||
if sourceCode.startsWith('"use babel"') or sourceCode.startsWith("'use babel'")
|
||||
# Continue.
|
||||
else if sourceCode.startsWith('"use 6to5"') or sourceCode.startsWith("'use 6to5'")
|
||||
# Create a manual deprecation since the stack is too deep to use Grim
|
||||
# which limits the depth to 3
|
||||
Grim ?= require 'grim'
|
||||
stack = [
|
||||
{
|
||||
fileName: __filename
|
||||
functionName: 'loadFile'
|
||||
location: "#{__filename}:161:5"
|
||||
}
|
||||
{
|
||||
fileName: filePath
|
||||
functionName: '<unknown>'
|
||||
location: "#{filePath}:1:1"
|
||||
}
|
||||
]
|
||||
deprecation =
|
||||
message: "Use the 'use babel' pragma instead of 'use 6to5'"
|
||||
stacks: [stack]
|
||||
Grim.addSerializedDeprecation(deprecation)
|
||||
else
|
||||
return module._compile(sourceCode, filePath)
|
||||
|
||||
cachePath = getCachePath(sourceCode)
|
||||
js = getCachedJavaScript(cachePath) ? transpile(sourceCode, filePath, cachePath)
|
||||
module._compile(js, filePath)
|
||||
|
||||
register = ->
|
||||
Object.defineProperty(require.extensions, '.js', {
|
||||
enumerable: true
|
||||
writable: false
|
||||
value: loadFile
|
||||
})
|
||||
|
||||
setCacheDirectory = (newCacheDir) ->
|
||||
if cacheDir isnt newCacheDir
|
||||
cacheDir = newCacheDir
|
||||
jsCacheDir = null
|
||||
|
||||
module.exports =
|
||||
register: register
|
||||
setCacheDirectory: setCacheDirectory
|
||||
getCacheMisses: -> stats.misses
|
||||
getCacheHits: -> stats.hits
|
||||
|
||||
# Visible for testing.
|
||||
createBabelVersionAndOptionsDigest: createBabelVersionAndOptionsDigest
|
||||
|
||||
addPathToCache: (filePath) ->
|
||||
return if path.extname(filePath) isnt '.js'
|
||||
|
||||
sourceCode = fs.readFileSync(filePath, 'utf8')
|
||||
cachePath = getCachePath(sourceCode)
|
||||
transpile(sourceCode, filePath, cachePath)
|
||||
63
src/babel.js
Normal file
63
src/babel.js
Normal file
@@ -0,0 +1,63 @@
|
||||
'use strict'
|
||||
|
||||
var crypto = require('crypto')
|
||||
var path = require('path')
|
||||
var defaultOptions = require('../static/babelrc.json')
|
||||
|
||||
var babel = null
|
||||
var babelVersionDirectory = null
|
||||
|
||||
var PREFIXES = [
|
||||
'/** @babel */',
|
||||
'"use babel"',
|
||||
'\'use babel\''
|
||||
]
|
||||
|
||||
var PREFIX_LENGTH = Math.max.apply(Math, PREFIXES.map(function (prefix) {
|
||||
return prefix.length
|
||||
}))
|
||||
|
||||
exports.shouldCompile = function (sourceCode) {
|
||||
var start = sourceCode.substr(0, PREFIX_LENGTH)
|
||||
return PREFIXES.some(function (prefix) {
|
||||
return start.indexOf(prefix) === 0
|
||||
})
|
||||
}
|
||||
|
||||
exports.getCachePath = function (sourceCode) {
|
||||
if (babelVersionDirectory == null) {
|
||||
var babelVersion = require('babel-core/package.json').version
|
||||
babelVersionDirectory = path.join('js', 'babel', createVersionAndOptionsDigest(babelVersion, defaultOptions))
|
||||
}
|
||||
|
||||
return path.join(
|
||||
babelVersionDirectory,
|
||||
crypto
|
||||
.createHash('sha1')
|
||||
.update(sourceCode, 'utf8')
|
||||
.digest('hex') + '.js'
|
||||
)
|
||||
}
|
||||
|
||||
exports.compile = function (sourceCode, filePath) {
|
||||
if (!babel) {
|
||||
babel = require('babel-core')
|
||||
}
|
||||
|
||||
var options = {filename: filePath}
|
||||
for (var key in defaultOptions) {
|
||||
options[key] = defaultOptions[key]
|
||||
}
|
||||
return babel.transform(sourceCode, options).code
|
||||
}
|
||||
|
||||
function createVersionAndOptionsDigest (version, options) {
|
||||
return crypto
|
||||
.createHash('sha1')
|
||||
.update('babel-core', 'utf8')
|
||||
.update('\0', 'utf8')
|
||||
.update(version, 'utf8')
|
||||
.update('\0', 'utf8')
|
||||
.update(JSON.stringify(options), 'utf8')
|
||||
.digest('hex')
|
||||
}
|
||||
@@ -65,9 +65,6 @@ class AtomApplication
|
||||
constructor: (options) ->
|
||||
{@resourcePath, @version, @devMode, @safeMode, @socketPath} = options
|
||||
|
||||
# Normalize to make sure drive letter case is consistent on Windows
|
||||
@resourcePath = path.normalize(@resourcePath) if @resourcePath
|
||||
|
||||
global.atomApplication = this
|
||||
|
||||
@pidsToOpenWindows = {}
|
||||
|
||||
@@ -23,9 +23,6 @@ class AtomWindow
|
||||
locationsToOpen ?= [{pathToOpen}] if pathToOpen
|
||||
locationsToOpen ?= []
|
||||
|
||||
# Normalize to make sure drive letter case is consistent on Windows
|
||||
@resourcePath = path.normalize(@resourcePath) if @resourcePath
|
||||
|
||||
options =
|
||||
show: false
|
||||
title: 'Atom'
|
||||
|
||||
@@ -16,7 +16,7 @@ process.on 'uncaughtException', (error={}) ->
|
||||
|
||||
start = ->
|
||||
setupAtomHome()
|
||||
setupCoffeeCache()
|
||||
setupCompileCache()
|
||||
|
||||
if process.platform is 'win32'
|
||||
SquirrelUpdate = require './squirrel-update'
|
||||
@@ -54,17 +54,20 @@ start = ->
|
||||
else
|
||||
path.resolve(pathToOpen)
|
||||
|
||||
if args.devMode
|
||||
AtomApplication = require path.join(args.resourcePath, 'src', 'browser', 'atom-application')
|
||||
else
|
||||
AtomApplication = require './atom-application'
|
||||
AtomApplication = require path.join(args.resourcePath, 'src', 'browser', 'atom-application')
|
||||
|
||||
AtomApplication.open(args)
|
||||
console.log("App load time: #{Date.now() - global.shellStartTime}ms") unless args.test
|
||||
|
||||
global.devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH ? path.join(app.getHomeDir(), 'github', 'atom')
|
||||
# Normalize to make sure drive letter case is consistent on Windows
|
||||
global.devResourcePath = path.normalize(global.devResourcePath) if global.devResourcePath
|
||||
normalizeDriveLetterName = (filePath) ->
|
||||
if process.platform is 'win32'
|
||||
filePath.replace /^([a-z]):/, ([driveLetter]) -> driveLetter.toUpperCase() + ":"
|
||||
else
|
||||
filePath
|
||||
|
||||
global.devResourcePath = normalizeDriveLetterName(
|
||||
process.env.ATOM_DEV_RESOURCE_PATH ? path.join(app.getHomeDir(), 'github', 'atom')
|
||||
)
|
||||
|
||||
setupCrashReporter = ->
|
||||
crashReporter.start(productName: 'Atom', companyName: 'GitHub')
|
||||
@@ -77,14 +80,9 @@ setupAtomHome = ->
|
||||
atomHome = fs.realpathSync(atomHome)
|
||||
process.env.ATOM_HOME = atomHome
|
||||
|
||||
setupCoffeeCache = ->
|
||||
CoffeeCache = require 'coffee-cash'
|
||||
cacheDir = path.join(process.env.ATOM_HOME, 'compile-cache')
|
||||
# Use separate compile cache when sudo'ing as root to avoid permission issues
|
||||
if process.env.USER is 'root' and process.env.SUDO_USER and process.env.SUDO_USER isnt process.env.USER
|
||||
cacheDir = path.join(cacheDir, 'root')
|
||||
CoffeeCache.setCacheDirectory(path.join(cacheDir, 'coffee'))
|
||||
CoffeeCache.register()
|
||||
setupCompileCache = ->
|
||||
compileCache = require('../compile-cache')
|
||||
compileCache.setAtomHomeDirectory(process.env.ATOM_HOME)
|
||||
|
||||
parseCommandLine = ->
|
||||
version = app.getVersion()
|
||||
@@ -169,6 +167,8 @@ parseCommandLine = ->
|
||||
# explicitly pass it by command line, see http://git.io/YC8_Ew.
|
||||
process.env.PATH = args['path-environment'] if args['path-environment']
|
||||
|
||||
resourcePath = normalizeDriveLetterName(resourcePath)
|
||||
|
||||
{resourcePath, pathsToOpen, executedFrom, test, version, pidToKillWhenClosed,
|
||||
devMode, safeMode, newWindow, specDirectory, logFile, socketPath, profileStartup}
|
||||
|
||||
|
||||
44
src/coffee-script.js
Normal file
44
src/coffee-script.js
Normal file
@@ -0,0 +1,44 @@
|
||||
'use strict'
|
||||
|
||||
var crypto = require('crypto')
|
||||
var path = require('path')
|
||||
var CoffeeScript = null
|
||||
|
||||
exports.shouldCompile = function () {
|
||||
return true
|
||||
}
|
||||
|
||||
exports.getCachePath = function (sourceCode) {
|
||||
return path.join(
|
||||
'coffee',
|
||||
crypto
|
||||
.createHash('sha1')
|
||||
.update(sourceCode, 'utf8')
|
||||
.digest('hex') + '.js'
|
||||
)
|
||||
}
|
||||
|
||||
exports.compile = function (sourceCode, filePath) {
|
||||
if (!CoffeeScript) {
|
||||
var previousPrepareStackTrace = Error.prepareStackTrace
|
||||
CoffeeScript = require('coffee-script')
|
||||
|
||||
// When it loads, coffee-script reassigns Error.prepareStackTrace. We have
|
||||
// already reassigned it via the 'source-map-support' module, so we need
|
||||
// to set it back.
|
||||
Error.prepareStackTrace = previousPrepareStackTrace
|
||||
}
|
||||
|
||||
var output = CoffeeScript.compile(sourceCode, {
|
||||
filename: filePath,
|
||||
sourceFiles: [filePath],
|
||||
sourceMap: true
|
||||
})
|
||||
|
||||
var js = output.js
|
||||
js += '\n'
|
||||
js += '//# sourceMappingURL=data:application/json;base64,'
|
||||
js += new Buffer(output.v3SourceMap).toString('base64')
|
||||
js += '\n'
|
||||
return js
|
||||
}
|
||||
@@ -182,9 +182,20 @@ class CommandRegistry
|
||||
stopImmediatePropagation: value: ->
|
||||
@handleCommandEvent(eventWithTarget)
|
||||
|
||||
# Public: Invoke the given callback before dispatching a command event.
|
||||
#
|
||||
# * `callback` {Function} to be called before dispatching each command
|
||||
# * `event` The Event that will be dispatched
|
||||
onWillDispatch: (callback) ->
|
||||
@emitter.on 'will-dispatch', callback
|
||||
|
||||
# Public: Invoke the given callback after dispatching a command event.
|
||||
#
|
||||
# * `callback` {Function} to be called after dispatching each command
|
||||
# * `event` The Event that was dispatched
|
||||
onDidDispatch: (callback) ->
|
||||
@emitter.on 'did-dispatch', callback
|
||||
|
||||
getSnapshot: ->
|
||||
snapshot = {}
|
||||
for commandName, listeners of @selectorBasedListenersByCommandName
|
||||
@@ -239,6 +250,8 @@ class CommandRegistry
|
||||
break if propagationStopped
|
||||
currentTarget = currentTarget.parentNode ? window
|
||||
|
||||
@emitter.emit 'did-dispatch', syntheticEvent
|
||||
|
||||
matched
|
||||
|
||||
commandRegistered: (commandName) ->
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
path = require 'path'
|
||||
CSON = require 'season'
|
||||
CoffeeCache = require 'coffee-cash'
|
||||
babel = require './babel'
|
||||
typescript = require './typescript'
|
||||
|
||||
# This file is required directly by apm so that files can be cached during
|
||||
# package install so that the first package load in Atom doesn't have to
|
||||
# compile anything.
|
||||
exports.addPathToCache = (filePath, atomHome) ->
|
||||
atomHome ?= process.env.ATOM_HOME
|
||||
cacheDir = path.join(atomHome, 'compile-cache')
|
||||
# Use separate compile cache when sudo'ing as root to avoid permission issues
|
||||
if process.env.USER is 'root' and process.env.SUDO_USER and process.env.SUDO_USER isnt process.env.USER
|
||||
cacheDir = path.join(cacheDir, 'root')
|
||||
|
||||
CoffeeCache.setCacheDirectory(path.join(cacheDir, 'coffee'))
|
||||
CSON.setCacheDir(path.join(cacheDir, 'cson'))
|
||||
babel.setCacheDirectory(path.join(cacheDir, 'js', 'babel'))
|
||||
typescript.setCacheDirectory(path.join(cacheDir, 'ts'))
|
||||
|
||||
switch path.extname(filePath)
|
||||
when '.coffee'
|
||||
CoffeeCache.addPathToCache(filePath)
|
||||
when '.cson'
|
||||
CSON.readFileSync(filePath)
|
||||
when '.js'
|
||||
babel.addPathToCache(filePath)
|
||||
when '.ts'
|
||||
typescript.addPathToCache(filePath)
|
||||
174
src/compile-cache.js
Normal file
174
src/compile-cache.js
Normal file
@@ -0,0 +1,174 @@
|
||||
'use strict'
|
||||
|
||||
var path = require('path')
|
||||
var fs = require('fs-plus')
|
||||
var CSON = null
|
||||
|
||||
var COMPILERS = {
|
||||
'.js': require('./babel'),
|
||||
'.ts': require('./typescript'),
|
||||
'.coffee': require('./coffee-script')
|
||||
}
|
||||
|
||||
var cacheStats = {}
|
||||
var cacheDirectory = null
|
||||
|
||||
exports.setAtomHomeDirectory = function (atomHome) {
|
||||
var cacheDir = path.join(atomHome, 'compile-cache')
|
||||
if (process.env.USER === 'root' && process.env.SUDO_USER && process.env.SUDO_USER !== process.env.USER) {
|
||||
cacheDir = path.join(cacheDir, 'root')
|
||||
}
|
||||
this.setCacheDirectory(cacheDir)
|
||||
}
|
||||
|
||||
exports.setCacheDirectory = function (directory) {
|
||||
cacheDirectory = directory
|
||||
}
|
||||
|
||||
exports.getCacheDirectory = function () {
|
||||
return cacheDirectory
|
||||
}
|
||||
|
||||
exports.addPathToCache = function (filePath, atomHome) {
|
||||
this.setAtomHomeDirectory(atomHome)
|
||||
var extension = path.extname(filePath)
|
||||
|
||||
if (extension === '.cson') {
|
||||
if (!CSON) {
|
||||
CSON = require('season')
|
||||
CSON.setCacheDir(this.getCacheDirectory())
|
||||
}
|
||||
CSON.readFileSync(filePath)
|
||||
} else {
|
||||
var compiler = COMPILERS[extension]
|
||||
if (compiler) {
|
||||
compileFileAtPath(compiler, filePath, extension)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.getCacheStats = function () {
|
||||
return cacheStats
|
||||
}
|
||||
|
||||
exports.resetCacheStats = function () {
|
||||
Object.keys(COMPILERS).forEach(function (extension) {
|
||||
cacheStats[extension] = {
|
||||
hits: 0,
|
||||
misses: 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function compileFileAtPath (compiler, filePath, extension) {
|
||||
var sourceCode = fs.readFileSync(filePath, 'utf8')
|
||||
if (compiler.shouldCompile(sourceCode, filePath)) {
|
||||
var cachePath = compiler.getCachePath(sourceCode, filePath)
|
||||
var compiledCode = readCachedJavascript(cachePath)
|
||||
if (compiledCode != null) {
|
||||
cacheStats[extension].hits++
|
||||
} else {
|
||||
cacheStats[extension].misses++
|
||||
compiledCode = addSourceURL(compiler.compile(sourceCode, filePath), filePath)
|
||||
writeCachedJavascript(cachePath, compiledCode)
|
||||
}
|
||||
return compiledCode
|
||||
}
|
||||
return sourceCode
|
||||
}
|
||||
|
||||
function readCachedJavascript (relativeCachePath) {
|
||||
var cachePath = path.join(cacheDirectory, relativeCachePath)
|
||||
if (fs.isFileSync(cachePath)) {
|
||||
try {
|
||||
return fs.readFileSync(cachePath, 'utf8')
|
||||
} catch (error) {}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function writeCachedJavascript (relativeCachePath, code) {
|
||||
var cachePath = path.join(cacheDirectory, relativeCachePath)
|
||||
fs.writeFileSync(cachePath, code, 'utf8')
|
||||
}
|
||||
|
||||
function addSourceURL (jsCode, filePath) {
|
||||
if (process.platform === 'win32') {
|
||||
filePath = '/' + path.resolve(filePath).replace(/\\/g, '/')
|
||||
}
|
||||
return jsCode + '\n' + '//# sourceURL=' + encodeURI(filePath) + '\n'
|
||||
}
|
||||
|
||||
var INLINE_SOURCE_MAP_REGEXP = /\/\/[#@]\s*sourceMappingURL=([^'"\n]+)\s*$/mg
|
||||
|
||||
require('source-map-support').install({
|
||||
handleUncaughtExceptions: false,
|
||||
|
||||
// Most of this logic is the same as the default implementation in the
|
||||
// source-map-support module, but we've overridden it to read the javascript
|
||||
// code from our cache directory.
|
||||
retrieveSourceMap: function (filePath) {
|
||||
if (!cacheDirectory || !fs.isFileSync(filePath)) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
var sourceCode = fs.readFileSync(filePath, 'utf8')
|
||||
} catch (error) {
|
||||
console.warn('Error reading source file', error.stack)
|
||||
return null
|
||||
}
|
||||
|
||||
var compiler = COMPILERS[path.extname(filePath)]
|
||||
|
||||
try {
|
||||
var fileData = readCachedJavascript(compiler.getCachePath(sourceCode, filePath))
|
||||
} catch (error) {
|
||||
console.warn('Error reading compiled file', error.stack)
|
||||
return null
|
||||
}
|
||||
|
||||
if (fileData == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
var match, lastMatch
|
||||
INLINE_SOURCE_MAP_REGEXP.lastIndex = 0
|
||||
while ((match = INLINE_SOURCE_MAP_REGEXP.exec(fileData))) {
|
||||
lastMatch = match
|
||||
}
|
||||
if (lastMatch == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
var sourceMappingURL = lastMatch[1]
|
||||
var rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(',') + 1)
|
||||
|
||||
try {
|
||||
var sourceMap = JSON.parse(new Buffer(rawData, 'base64'))
|
||||
} catch (error) {
|
||||
console.warn('Error parsing source map', error.stack)
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
map: sourceMap,
|
||||
url: null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Object.keys(COMPILERS).forEach(function (extension) {
|
||||
var compiler = COMPILERS[extension]
|
||||
|
||||
Object.defineProperty(require.extensions, extension, {
|
||||
enumerable: true,
|
||||
writable: false,
|
||||
value: function (module, filePath) {
|
||||
var code = compileFileAtPath(compiler, filePath, extension)
|
||||
return module._compile(code, filePath)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
exports.resetCacheStats()
|
||||
@@ -26,6 +26,14 @@ module.exports =
|
||||
default: []
|
||||
items:
|
||||
type: 'string'
|
||||
customFileTypes:
|
||||
type: 'object'
|
||||
default: {}
|
||||
description: 'Associates scope names (e.g. "source.js") with arrays of file extensions and file names (e.g. ["Somefile", ".js2"])'
|
||||
additionalProperties:
|
||||
type: 'array'
|
||||
items:
|
||||
type: 'string'
|
||||
themes:
|
||||
type: 'array'
|
||||
default: ['one-dark-ui', 'one-dark-syntax']
|
||||
@@ -81,6 +89,10 @@ module.exports =
|
||||
'windows1258',
|
||||
'windows866'
|
||||
]
|
||||
openEmptyEditorOnStart:
|
||||
description: 'Automatically opens an empty editor when atom starts.'
|
||||
type: 'boolean'
|
||||
default: true
|
||||
|
||||
editor:
|
||||
type: 'object'
|
||||
@@ -143,6 +155,11 @@ module.exports =
|
||||
softTabs:
|
||||
type: 'boolean'
|
||||
default: true
|
||||
tabType:
|
||||
type: 'string'
|
||||
default: 'auto'
|
||||
enum: ['auto', 'soft', 'hard']
|
||||
description: 'Determine character inserted during Tab keypress.'
|
||||
softWrapAtPreferredLineLength:
|
||||
type: 'boolean'
|
||||
default: false
|
||||
|
||||
@@ -283,6 +283,17 @@ ScopeDescriptor = require './scope-descriptor'
|
||||
# __Note__: You should strive to be so clear in your naming of the setting that
|
||||
# you do not need to specify a title or description!
|
||||
#
|
||||
# Descriptions allow a subset of
|
||||
# [Markdown formatting](https://help.github.com/articles/github-flavored-markdown/).
|
||||
# Specifically, you may use the following in configuration setting descriptions:
|
||||
#
|
||||
# * **bold** - `**bold**`
|
||||
# * *italics* - `*italics*`
|
||||
# * [links](https://atom.io) - `[links](https://atom.io)`
|
||||
# * `code spans` - `\`code spans\``
|
||||
# * line breaks - `line breaks<br/>`
|
||||
# * ~~strikethrough~~ - `~~strikethrough~~`
|
||||
#
|
||||
# ## Best practices
|
||||
#
|
||||
# * Don't depend on (or write to) configuration keys outside of your keypath.
|
||||
@@ -664,13 +675,24 @@ class Config
|
||||
# * `keyPath` The {String} name of the key.
|
||||
#
|
||||
# Returns an {Object} eg. `{type: 'integer', default: 23, minimum: 1}`.
|
||||
# Returns `null` when the keyPath has no schema specified.
|
||||
# Returns `null` when the keyPath has no schema specified, but is accessible
|
||||
# from the root schema.
|
||||
getSchema: (keyPath) ->
|
||||
keys = splitKeyPath(keyPath)
|
||||
schema = @schema
|
||||
for key in keys
|
||||
break unless schema?
|
||||
schema = schema.properties?[key]
|
||||
if schema.type is 'object'
|
||||
childSchema = schema.properties?[key]
|
||||
unless childSchema?
|
||||
if isPlainObject(schema.additionalProperties)
|
||||
childSchema = schema.additionalProperties
|
||||
else if schema.additionalProperties is false
|
||||
return null
|
||||
else
|
||||
return {type: 'any'}
|
||||
else
|
||||
return null
|
||||
schema = childSchema
|
||||
schema
|
||||
|
||||
# Extended: Get the {String} path to the config file being used.
|
||||
@@ -843,7 +865,7 @@ class Config
|
||||
|
||||
if value?
|
||||
value = @deepClone(value)
|
||||
_.defaults(value, defaultValue) if isPlainObject(value) and isPlainObject(defaultValue)
|
||||
@deepDefaults(value, defaultValue) if isPlainObject(value) and isPlainObject(defaultValue)
|
||||
else
|
||||
value = @deepClone(defaultValue)
|
||||
|
||||
@@ -906,6 +928,19 @@ class Config
|
||||
else
|
||||
object
|
||||
|
||||
deepDefaults: (target) ->
|
||||
result = target
|
||||
i = 0
|
||||
while ++i < arguments.length
|
||||
object = arguments[i]
|
||||
if isPlainObject(result) and isPlainObject(object)
|
||||
for key in Object.keys(object)
|
||||
result[key] = @deepDefaults(result[key], object[key])
|
||||
else
|
||||
if not result?
|
||||
result = @deepClone(object)
|
||||
result
|
||||
|
||||
# `schema` will look something like this
|
||||
#
|
||||
# ```coffee
|
||||
@@ -948,8 +983,9 @@ class Config
|
||||
catch e
|
||||
undefined
|
||||
else
|
||||
value = @constructor.executeSchemaEnforcers(keyPath, value, schema) if schema = @getSchema(keyPath)
|
||||
value
|
||||
unless (schema = @getSchema(keyPath))?
|
||||
throw new Error("Illegal key path #{keyPath}") if schema is false
|
||||
@constructor.executeSchemaEnforcers(keyPath, value, schema)
|
||||
|
||||
# When the schema is changed / added, there may be values set in the config
|
||||
# that do not conform to the schema. This will reset make them conform.
|
||||
@@ -1027,6 +1063,10 @@ class Config
|
||||
# order of specification. Then the `*` enforcers will be run, in order of
|
||||
# specification.
|
||||
Config.addSchemaEnforcers
|
||||
'any':
|
||||
coerce: (keyPath, value, schema) ->
|
||||
value
|
||||
|
||||
'integer':
|
||||
coerce: (keyPath, value, schema) ->
|
||||
value = parseInt(value)
|
||||
@@ -1077,17 +1117,26 @@ Config.addSchemaEnforcers
|
||||
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be an object") unless isPlainObject(value)
|
||||
return value unless schema.properties?
|
||||
|
||||
defaultChildSchema = null
|
||||
allowsAdditionalProperties = true
|
||||
if isPlainObject(schema.additionalProperties)
|
||||
defaultChildSchema = schema.additionalProperties
|
||||
if schema.additionalProperties is false
|
||||
allowsAdditionalProperties = false
|
||||
|
||||
newValue = {}
|
||||
for prop, propValue of value
|
||||
childSchema = schema.properties[prop]
|
||||
childSchema = schema.properties[prop] ? defaultChildSchema
|
||||
if childSchema?
|
||||
try
|
||||
newValue[prop] = @executeSchemaEnforcers("#{keyPath}.#{prop}", propValue, childSchema)
|
||||
catch error
|
||||
console.warn "Error setting item in object: #{error.message}"
|
||||
else
|
||||
else if allowsAdditionalProperties
|
||||
# Just pass through un-schema'd values
|
||||
newValue[prop] = propValue
|
||||
else
|
||||
console.warn "Illegal object key: #{keyPath}.#{prop}"
|
||||
|
||||
newValue
|
||||
|
||||
|
||||
@@ -86,9 +86,14 @@ class ContextMenuManager
|
||||
# * `label` (Optional) A {String} containing the menu item's label.
|
||||
# * `command` (Optional) A {String} containing the command to invoke on the
|
||||
# target of the right click that invoked the context menu.
|
||||
# * `enabled` (Optional) A {Boolean} indicating whether the menu item
|
||||
# should be clickable. Disabled menu items typically appear grayed out.
|
||||
# Defaults to `true`.
|
||||
# * `submenu` (Optional) An {Array} of additional items.
|
||||
# * `type` (Optional) If you want to create a separator, provide an item
|
||||
# with `type: 'separator'` and no other keys.
|
||||
# * `visible` (Optional) A {Boolean} indicating whether the menu item
|
||||
# should appear in the menu. Defaults to `true`.
|
||||
# * `created` (Optional) A {Function} that is called on the item each time a
|
||||
# context menu is created via a right click. You can assign properties to
|
||||
# `this` to dynamically compute the command, label, etc. This method is
|
||||
|
||||
@@ -177,21 +177,18 @@ class DisplayBuffer extends Model
|
||||
# visible - A {Boolean} indicating of the tokenized buffer is shown
|
||||
setVisible: (visible) -> @tokenizedBuffer.setVisible(visible)
|
||||
|
||||
getVerticalScrollMargin: -> Math.min(@verticalScrollMargin, (@getHeight() - @getLineHeightInPixels()) / 2)
|
||||
getVerticalScrollMargin: ->
|
||||
maxScrollMargin = Math.floor(((@getHeight() / @getLineHeightInPixels()) - 1) / 2)
|
||||
Math.min(@verticalScrollMargin, maxScrollMargin)
|
||||
|
||||
setVerticalScrollMargin: (@verticalScrollMargin) -> @verticalScrollMargin
|
||||
|
||||
getVerticalScrollMarginInPixels: ->
|
||||
scrollMarginInPixels = @getVerticalScrollMargin() * @getLineHeightInPixels()
|
||||
maxScrollMarginInPixels = (@getHeight() - @getLineHeightInPixels()) / 2
|
||||
Math.min(scrollMarginInPixels, maxScrollMarginInPixels)
|
||||
getVerticalScrollMarginInPixels: -> @getVerticalScrollMargin() * @getLineHeightInPixels()
|
||||
|
||||
getHorizontalScrollMargin: -> Math.min(@horizontalScrollMargin, (@getWidth() - @getDefaultCharWidth()) / 2)
|
||||
getHorizontalScrollMargin: -> Math.min(@horizontalScrollMargin, Math.floor(((@getWidth() / @getDefaultCharWidth()) - 1) / 2))
|
||||
setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin
|
||||
|
||||
getHorizontalScrollMarginInPixels: ->
|
||||
scrollMarginInPixels = @getHorizontalScrollMargin() * @getDefaultCharWidth()
|
||||
maxScrollMarginInPixels = (@getWidth() - @getDefaultCharWidth()) / 2
|
||||
Math.min(scrollMarginInPixels, maxScrollMarginInPixels)
|
||||
getHorizontalScrollMarginInPixels: -> scrollMarginInPixels = @getHorizontalScrollMargin() * @getDefaultCharWidth()
|
||||
|
||||
getHorizontalScrollbarHeight: -> @horizontalScrollbarHeight
|
||||
setHorizontalScrollbarHeight: (@horizontalScrollbarHeight) -> @horizontalScrollbarHeight
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
_ = require 'underscore-plus'
|
||||
{Emitter} = require 'event-kit'
|
||||
{includeDeprecatedAPIs, deprecate} = require 'grim'
|
||||
FirstMate = require 'first-mate'
|
||||
Token = require './token'
|
||||
fs = require 'fs-plus'
|
||||
|
||||
PathSplitRegex = new RegExp("[/.]")
|
||||
|
||||
# Extended: Syntax class holding the grammars used for tokenizing.
|
||||
#
|
||||
@@ -39,7 +43,7 @@ class GrammarRegistry extends FirstMate.GrammarRegistry
|
||||
bestMatch = null
|
||||
highestScore = -Infinity
|
||||
for grammar in @grammars
|
||||
score = grammar.getScore(filePath, fileContents)
|
||||
score = @getGrammarScore(grammar, filePath, fileContents)
|
||||
if score > highestScore or not bestMatch?
|
||||
bestMatch = grammar
|
||||
highestScore = score
|
||||
@@ -47,6 +51,90 @@ class GrammarRegistry extends FirstMate.GrammarRegistry
|
||||
bestMatch = grammar unless grammar.bundledPackage
|
||||
bestMatch
|
||||
|
||||
# Extended: Returns a {Number} representing how well the grammar matches the
|
||||
# `filePath` and `contents`.
|
||||
getGrammarScore: (grammar, filePath, contents) ->
|
||||
contents = fs.readFileSync(filePath, 'utf8') if not contents? and fs.isFileSync(filePath)
|
||||
|
||||
if @grammarOverrideForPath(filePath) is grammar.scopeName
|
||||
2 + (filePath?.length ? 0)
|
||||
else if @grammarMatchesContents(grammar, contents)
|
||||
1 + (filePath?.length ? 0)
|
||||
else
|
||||
@getGrammarPathScore(grammar, filePath)
|
||||
|
||||
getGrammarPathScore: (grammar, filePath) ->
|
||||
return -1 unless filePath
|
||||
filePath = filePath.replace(/\\/g, '/') if process.platform is 'win32'
|
||||
|
||||
pathComponents = filePath.toLowerCase().split(PathSplitRegex)
|
||||
pathScore = -1
|
||||
|
||||
fileTypes = grammar.fileTypes
|
||||
if customFileTypes = atom.config.get('core.customFileTypes')?[grammar.scopeName]
|
||||
fileTypes = fileTypes.concat(customFileTypes)
|
||||
|
||||
for fileType, i in fileTypes
|
||||
fileTypeComponents = fileType.toLowerCase().split(PathSplitRegex)
|
||||
pathSuffix = pathComponents[-fileTypeComponents.length..-1]
|
||||
if _.isEqual(pathSuffix, fileTypeComponents)
|
||||
pathScore = Math.max(pathScore, fileType.length)
|
||||
if i >= grammar.fileTypes.length
|
||||
pathScore += 0.5
|
||||
|
||||
pathScore
|
||||
|
||||
grammarMatchesContents: (grammar, contents) ->
|
||||
return false unless contents? and grammar.firstLineRegex?
|
||||
|
||||
escaped = false
|
||||
numberOfNewlinesInRegex = 0
|
||||
for character in grammar.firstLineRegex.source
|
||||
switch character
|
||||
when '\\'
|
||||
escaped = not escaped
|
||||
when 'n'
|
||||
numberOfNewlinesInRegex++ if escaped
|
||||
escaped = false
|
||||
else
|
||||
escaped = false
|
||||
lines = contents.split('\n')
|
||||
grammar.firstLineRegex.testSync(lines[0..numberOfNewlinesInRegex].join('\n'))
|
||||
|
||||
# Public: Get the grammar override for the given file path.
|
||||
#
|
||||
# * `filePath` A {String} file path.
|
||||
#
|
||||
# Returns a {Grammar} or undefined.
|
||||
grammarOverrideForPath: (filePath) ->
|
||||
@grammarOverridesByPath[filePath]
|
||||
|
||||
# Public: Set the grammar override for the given file path.
|
||||
#
|
||||
# * `filePath` A non-empty {String} file path.
|
||||
# * `scopeName` A {String} such as `"source.js"`.
|
||||
#
|
||||
# Returns a {Grammar} or undefined.
|
||||
setGrammarOverrideForPath: (filePath, scopeName) ->
|
||||
if filePath
|
||||
@grammarOverridesByPath[filePath] = scopeName
|
||||
|
||||
# Public: Remove the grammar override for the given file path.
|
||||
#
|
||||
# * `filePath` A {String} file path.
|
||||
#
|
||||
# Returns undefined.
|
||||
clearGrammarOverrideForPath: (filePath) ->
|
||||
delete @grammarOverridesByPath[filePath]
|
||||
undefined
|
||||
|
||||
# Public: Remove all grammar overrides.
|
||||
#
|
||||
# Returns undefined.
|
||||
clearGrammarOverrides: ->
|
||||
@grammarOverridesByPath = {}
|
||||
undefined
|
||||
|
||||
clearObservers: ->
|
||||
@off() if includeDeprecatedAPIs
|
||||
@emitter = new Emitter
|
||||
|
||||
@@ -1,29 +1,22 @@
|
||||
{Emitter} = require 'event-kit'
|
||||
Gutter = require './gutter'
|
||||
|
||||
# This class encapsulates the logic for adding and modifying a set of gutters.
|
||||
|
||||
module.exports =
|
||||
class GutterContainer
|
||||
|
||||
# * `textEditor` The {TextEditor} to which this {GutterContainer} belongs.
|
||||
constructor: (textEditor) ->
|
||||
@gutters = []
|
||||
@textEditor = textEditor
|
||||
@emitter = new Emitter
|
||||
|
||||
destroy: ->
|
||||
@gutters = null
|
||||
# Create a copy, because `Gutter::destroy` removes the gutter from
|
||||
# GutterContainer's @gutters.
|
||||
guttersToDestroy = @gutters.slice(0)
|
||||
for gutter in guttersToDestroy
|
||||
gutter.destroy() if gutter.name isnt 'line-number'
|
||||
@gutters = []
|
||||
@emitter.dispose()
|
||||
|
||||
# Creates and returns a {Gutter}.
|
||||
# * `options` An {Object} with the following fields:
|
||||
# * `name` (required) A unique {String} to identify this gutter.
|
||||
# * `priority` (optional) A {Number} that determines stacking order between
|
||||
# gutters. Lower priority items are forced closer to the edges of the
|
||||
# window. (default: -100)
|
||||
# * `visible` (optional) {Boolean} specifying whether the gutter is visible
|
||||
# initially after being created. (default: true)
|
||||
addGutter: (options) ->
|
||||
options = options ? {}
|
||||
gutterName = options.name
|
||||
@@ -54,20 +47,13 @@ class GutterContainer
|
||||
if gutter.name is name then return gutter
|
||||
null
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
|
||||
# See {TextEditor::observeGutters} for details.
|
||||
observeGutters: (callback) ->
|
||||
callback(gutter) for gutter in @getGutters()
|
||||
@onDidAddGutter callback
|
||||
|
||||
# See {TextEditor::onDidAddGutter} for details.
|
||||
onDidAddGutter: (callback) ->
|
||||
@emitter.on 'did-add-gutter', callback
|
||||
|
||||
# See {TextEditor::onDidRemoveGutter} for details.
|
||||
onDidRemoveGutter: (callback) ->
|
||||
@emitter.on 'did-remove-gutter', callback
|
||||
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
{Emitter} = require 'event-kit'
|
||||
|
||||
# Public: This class represents a gutter within a TextEditor.
|
||||
|
||||
DefaultPriority = -100
|
||||
|
||||
# Extended: Represents a gutter within a {TextEditor}.
|
||||
#
|
||||
# See {TextEditor::addGutter} for information on creating a gutter.
|
||||
module.exports =
|
||||
class Gutter
|
||||
# * `gutterContainer` The {GutterContainer} object to which this gutter belongs.
|
||||
# * `options` An {Object} with the following fields:
|
||||
# * `name` (required) A unique {String} to identify this gutter.
|
||||
# * `priority` (optional) A {Number} that determines stacking order between
|
||||
# gutters. Lower priority items are forced closer to the edges of the
|
||||
# window. (default: -100)
|
||||
# * `visible` (optional) {Boolean} specifying whether the gutter is visible
|
||||
# initially after being created. (default: true)
|
||||
constructor: (gutterContainer, options) ->
|
||||
@gutterContainer = gutterContainer
|
||||
@name = options?.name
|
||||
@@ -22,6 +15,11 @@ class Gutter
|
||||
|
||||
@emitter = new Emitter
|
||||
|
||||
###
|
||||
Section: Gutter Destruction
|
||||
###
|
||||
|
||||
# Essential: Destroys the gutter.
|
||||
destroy: ->
|
||||
if @name is 'line-number'
|
||||
throw new Error('The line-number gutter cannot be destroyed.')
|
||||
@@ -30,42 +28,65 @@ class Gutter
|
||||
@emitter.emit 'did-destroy'
|
||||
@emitter.dispose()
|
||||
|
||||
hide: ->
|
||||
if @visible
|
||||
@visible = false
|
||||
@emitter.emit 'did-change-visible', this
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
|
||||
show: ->
|
||||
if not @visible
|
||||
@visible = true
|
||||
@emitter.emit 'did-change-visible', this
|
||||
|
||||
isVisible: ->
|
||||
@visible
|
||||
|
||||
# * `marker` (required) A Marker object.
|
||||
# * `options` (optional) An object with the following fields:
|
||||
# * `class` (optional)
|
||||
# * `item` (optional) A model {Object} with a corresponding view registered,
|
||||
# or an {HTMLElement}.
|
||||
#
|
||||
# Returns a {Decoration} object.
|
||||
decorateMarker: (marker, options) ->
|
||||
@gutterContainer.addGutterDecoration(this, marker, options)
|
||||
|
||||
# Calls your `callback` when the {Gutter}'s' visibility changes.
|
||||
# Essential: Calls your `callback` when the gutter's visibility changes.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
# * `gutter` The {Gutter} whose visibility changed.
|
||||
# * `gutter` The gutter whose visibility changed.
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeVisible: (callback) ->
|
||||
@emitter.on 'did-change-visible', callback
|
||||
|
||||
# Calls your `callback` when the {Gutter} is destroyed
|
||||
# Essential: Calls your `callback` when the gutter is destroyed.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.on 'did-destroy', callback
|
||||
|
||||
###
|
||||
Section: Visibility
|
||||
###
|
||||
|
||||
# Essential: Hide the gutter.
|
||||
hide: ->
|
||||
if @visible
|
||||
@visible = false
|
||||
@emitter.emit 'did-change-visible', this
|
||||
|
||||
# Essential: Show the gutter.
|
||||
show: ->
|
||||
if not @visible
|
||||
@visible = true
|
||||
@emitter.emit 'did-change-visible', this
|
||||
|
||||
# Essential: Determine whether the gutter is visible.
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
isVisible: ->
|
||||
@visible
|
||||
|
||||
# Essential: Add a decoration that tracks a {Marker}. When the marker moves,
|
||||
# is invalidated, or is destroyed, the decoration will be updated to reflect
|
||||
# the marker's state.
|
||||
#
|
||||
# ## Arguments
|
||||
#
|
||||
# * `marker` A {Marker} you want this decoration to follow.
|
||||
# * `decorationParams` An {Object} representing the decoration
|
||||
# * `class` This CSS class will be applied to the decorated line number.
|
||||
# * `onlyHead` (optional) If `true`, the decoration will only be applied to
|
||||
# the head of the marker.
|
||||
# * `onlyEmpty` (optional) If `true`, the decoration will only be applied if
|
||||
# the associated marker is empty.
|
||||
# * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied
|
||||
# if the associated marker is non-empty.
|
||||
#
|
||||
# Returns a {Decoration} object
|
||||
decorateMarker: (marker, options) ->
|
||||
@gutterContainer.addGutterDecoration(this, marker, options)
|
||||
|
||||
@@ -47,9 +47,9 @@ class LineNumberGutterComponent extends TiledComponent
|
||||
beforeUpdateSync: (state) ->
|
||||
@appendDummyLineNumber() unless @dummyLineNumberNode?
|
||||
|
||||
if @newState.styles.scrollHeight isnt @oldState.styles.scrollHeight
|
||||
@lineNumbersNode.style.height = @newState.styles.scrollHeight + 'px'
|
||||
@oldState.scrollHeight = @newState.scrollHeight
|
||||
if @newState.styles.maxHeight isnt @oldState.styles.maxHeight
|
||||
@lineNumbersNode.style.height = @newState.styles.maxHeight + 'px'
|
||||
@oldState.maxHeight = @newState.maxHeight
|
||||
|
||||
if @newState.styles.backgroundColor isnt @oldState.styles.backgroundColor
|
||||
@lineNumbersNode.style.backgroundColor = @newState.styles.backgroundColor
|
||||
|
||||
@@ -42,6 +42,10 @@ class LineNumbersTileComponent
|
||||
@domNode.style['-webkit-transform'] = "translate3d(0, #{@newTileState.top}px, 0px)"
|
||||
@oldTileState.top = @newTileState.top
|
||||
|
||||
if @newTileState.zIndex isnt @oldTileState.zIndex
|
||||
@domNode.style.zIndex = @newTileState.zIndex
|
||||
@oldTileState.zIndex = @newTileState.zIndex
|
||||
|
||||
if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits
|
||||
node.remove() for id, node of @lineNumberNodesById
|
||||
@oldState.tiles[@id] = {lineNumbers: {}}
|
||||
@@ -84,9 +88,9 @@ class LineNumbersTileComponent
|
||||
return
|
||||
|
||||
buildLineNumberHTML: (lineNumberState) ->
|
||||
{screenRow, bufferRow, softWrapped, top, decorationClasses} = lineNumberState
|
||||
{screenRow, bufferRow, softWrapped, top, decorationClasses, zIndex} = lineNumberState
|
||||
if screenRow?
|
||||
style = "position: absolute; top: #{top}px;"
|
||||
style = "position: absolute; top: #{top}px; z-index: #{zIndex};"
|
||||
else
|
||||
style = "visibility: hidden;"
|
||||
className = @buildLineNumberClassName(lineNumberState)
|
||||
@@ -121,6 +125,10 @@ class LineNumbersTileComponent
|
||||
oldLineNumberState.top = newLineNumberState.top
|
||||
oldLineNumberState.screenRow = newLineNumberState.screenRow
|
||||
|
||||
unless oldLineNumberState.zIndex is newLineNumberState.zIndex
|
||||
node.style.zIndex = newLineNumberState.zIndex
|
||||
oldLineNumberState.zIndex = newLineNumberState.zIndex
|
||||
|
||||
buildLineNumberClassName: ({bufferRow, foldable, decorationClasses, softWrapped}) ->
|
||||
className = "line-number line-number-#{bufferRow}"
|
||||
className += " " + decorationClasses.join(' ') if decorationClasses?
|
||||
|
||||
@@ -35,9 +35,9 @@ class LinesComponent extends TiledComponent
|
||||
@oldState.indentGuidesVisible isnt @newState.indentGuidesVisible
|
||||
|
||||
beforeUpdateSync: (state) ->
|
||||
if @newState.scrollHeight isnt @oldState.scrollHeight
|
||||
@domNode.style.height = @newState.scrollHeight + 'px'
|
||||
@oldState.scrollHeight = @newState.scrollHeight
|
||||
if @newState.maxHeight isnt @oldState.maxHeight
|
||||
@domNode.style.height = @newState.maxHeight + 'px'
|
||||
@oldState.maxHeight = @newState.maxHeight
|
||||
|
||||
if @newState.backgroundColor isnt @oldState.backgroundColor
|
||||
@domNode.style.backgroundColor = @newState.backgroundColor
|
||||
|
||||
@@ -92,10 +92,14 @@ class Marker
|
||||
#
|
||||
# * `callback` {Function} to be called when the marker changes.
|
||||
# * `event` {Object} with the following keys:
|
||||
# * `oldHeadPosition` {Point} representing the former head position
|
||||
# * `newHeadPosition` {Point} representing the new head position
|
||||
# * `oldTailPosition` {Point} representing the former tail position
|
||||
# * `newTailPosition` {Point} representing the new tail position
|
||||
# * `oldHeadBufferPosition` {Point} representing the former head buffer position
|
||||
# * `newHeadBufferPosition` {Point} representing the new head buffer position
|
||||
# * `oldTailBufferPosition` {Point} representing the former tail buffer position
|
||||
# * `newTailBufferPosition` {Point} representing the new tail buffer position
|
||||
# * `oldHeadScreenPosition` {Point} representing the former head screen position
|
||||
# * `newHeadScreenPosition` {Point} representing the new head screen position
|
||||
# * `oldTailScreenPosition` {Point} representing the former tail screen position
|
||||
# * `newTailScreenPosition` {Point} representing the new tail screen position
|
||||
# * `wasValid` {Boolean} indicating whether the marker was valid before the change
|
||||
# * `isValid` {Boolean} indicating whether the marker is now valid
|
||||
# * `hadTail` {Boolean} indicating whether the marker had a tail before the change
|
||||
|
||||
@@ -80,7 +80,7 @@ class NotificationManager
|
||||
|
||||
# Public: Get all the notifications.
|
||||
#
|
||||
# Returns an {Array} of {Notifications}s.
|
||||
# Returns an {Array} of {Notification}s.
|
||||
getNotifications: -> @notifications.slice()
|
||||
|
||||
###
|
||||
|
||||
@@ -82,21 +82,26 @@ class Pane extends Model
|
||||
Section: Event Subscription
|
||||
###
|
||||
|
||||
# Public: Invoke the given callback when the pane resize
|
||||
# Public: Invoke the given callback when the pane resizes
|
||||
#
|
||||
# the callback will be invoked when pane's flexScale property changes
|
||||
# The callback will be invoked when pane's flexScale property changes.
|
||||
# Use {::getFlexScale} to get the current value.
|
||||
#
|
||||
# * `callback` {Function} to be called when the pane is resized
|
||||
# * `flexScale` {Number} representing the panes `flex-grow`; ability for a
|
||||
# flex item to grow if necessary.
|
||||
#
|
||||
# Returns a {Disposable} on which '.dispose()' can be called to unsubscribe.
|
||||
onDidChangeFlexScale: (callback) ->
|
||||
@emitter.on 'did-change-flex-scale', callback
|
||||
|
||||
# Public: Invoke the given callback with all current and future items.
|
||||
# Public: Invoke the given callback with the current and future values of
|
||||
# {::getFlexScale}.
|
||||
#
|
||||
# * `callback` {Function} to be called with current and future items.
|
||||
# * `item` An item that is present in {::getItems} at the time of
|
||||
# subscription or that is added at some later time.
|
||||
# * `callback` {Function} to be called with the current and future values of
|
||||
# the {::getFlexScale} property.
|
||||
# * `flexScale` {Number} representing the panes `flex-grow`; ability for a
|
||||
# flex item to grow if necessary.
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
observeFlexScale: (callback) ->
|
||||
|
||||
@@ -34,12 +34,11 @@ class Project extends Model
|
||||
@rootDirectories = []
|
||||
@repositories = []
|
||||
|
||||
@directoryProviders = [new DefaultDirectoryProvider()]
|
||||
@directoryProviders = []
|
||||
@defaultDirectoryProvider = new DefaultDirectoryProvider()
|
||||
atom.packages.serviceHub.consume(
|
||||
'atom.directory-provider',
|
||||
'^0.1.0',
|
||||
# New providers are added to the front of @directoryProviders because
|
||||
# DefaultDirectoryProvider is a catch-all that will always provide a Directory.
|
||||
(provider) => @directoryProviders.unshift(provider))
|
||||
|
||||
# Mapping from the real path of a {Directory} to a {Promise} that resolves
|
||||
@@ -48,8 +47,6 @@ class Project extends Model
|
||||
# the same real path, so it is not a good key.
|
||||
@repositoryPromisesByPath = new Map()
|
||||
|
||||
# Note that the GitRepositoryProvider is registered synchronously so that
|
||||
# it is available immediately on startup.
|
||||
@repositoryProviders = [new GitRepositoryProvider(this)]
|
||||
atom.packages.serviceHub.consume(
|
||||
'atom.repository-provider',
|
||||
@@ -186,18 +183,16 @@ class Project extends Model
|
||||
#
|
||||
# * `projectPath` {String} The path to the directory to add.
|
||||
addPath: (projectPath, options) ->
|
||||
for directory in @getDirectories()
|
||||
# Apparently a Directory does not believe it can contain itself, so we
|
||||
# must also check whether the paths match.
|
||||
return if directory.contains(projectPath) or directory.getPath() is projectPath
|
||||
|
||||
directory = null
|
||||
for provider in @directoryProviders
|
||||
break if directory = provider.directoryForURISync?(projectPath)
|
||||
if directory is null
|
||||
# This should never happen because DefaultDirectoryProvider should always
|
||||
# return a Directory.
|
||||
throw new Error(projectPath + ' could not be resolved to a directory')
|
||||
directory ?= @defaultDirectoryProvider.directoryForURISync(projectPath)
|
||||
|
||||
directoryExists = directory.existsSync()
|
||||
for rootDirectory in @getDirectories()
|
||||
return if rootDirectory.getPath() is directory.getPath()
|
||||
return if not directoryExists and rootDirectory.contains(directory.getPath())
|
||||
|
||||
@rootDirectories.push(directory)
|
||||
|
||||
repo = null
|
||||
@@ -267,10 +262,13 @@ class Project extends Model
|
||||
# * `relativePath` {String} The relative path from the project directory to
|
||||
# the given path.
|
||||
relativizePath: (fullPath) ->
|
||||
for rootDirectory in @rootDirectories
|
||||
relativePath = rootDirectory.relativize(fullPath)
|
||||
return [rootDirectory.getPath(), relativePath] unless relativePath is fullPath
|
||||
[null, fullPath]
|
||||
result = [null, fullPath]
|
||||
if fullPath?
|
||||
for rootDirectory in @rootDirectories
|
||||
relativePath = rootDirectory.relativize(fullPath)
|
||||
if relativePath?.length < result[1].length
|
||||
result = [rootDirectory.getPath(), relativePath]
|
||||
result
|
||||
|
||||
# Public: Determines whether the given path (real or symbolic) is inside the
|
||||
# project's directory.
|
||||
|
||||
@@ -190,7 +190,7 @@ class Selection extends Model
|
||||
# position.
|
||||
#
|
||||
# * `position` An instance of {Point}, with a given `row` and `column`.
|
||||
selectToScreenPosition: (position) ->
|
||||
selectToScreenPosition: (position, options) ->
|
||||
position = Point.fromObject(position)
|
||||
|
||||
@modifySelection =>
|
||||
@@ -200,12 +200,12 @@ class Selection extends Model
|
||||
else
|
||||
@marker.setScreenRange([@initialScreenRange.start, position], reversed: false)
|
||||
else
|
||||
@cursor.setScreenPosition(position)
|
||||
@cursor.setScreenPosition(position, options)
|
||||
|
||||
if @linewise
|
||||
@expandOverLine()
|
||||
@expandOverLine(options)
|
||||
else if @wordwise
|
||||
@expandOverWord()
|
||||
@expandOverWord(options)
|
||||
|
||||
# Public: Selects the text from the current cursor position to a given buffer
|
||||
# position.
|
||||
@@ -311,28 +311,28 @@ class Selection extends Model
|
||||
# Public: Modifies the selection to encompass the current word.
|
||||
#
|
||||
# Returns a {Range}.
|
||||
selectWord: ->
|
||||
options = {}
|
||||
selectWord: (options={}) ->
|
||||
options.wordRegex = /[\t ]*/ if @cursor.isSurroundedByWhitespace()
|
||||
if @cursor.isBetweenWordAndNonWord()
|
||||
options.includeNonWordCharacters = false
|
||||
|
||||
@setBufferRange(@cursor.getCurrentWordBufferRange(options))
|
||||
@setBufferRange(@cursor.getCurrentWordBufferRange(options), options)
|
||||
@wordwise = true
|
||||
@initialScreenRange = @getScreenRange()
|
||||
|
||||
# Public: Expands the newest selection to include the entire word on which
|
||||
# the cursors rests.
|
||||
expandOverWord: ->
|
||||
expandOverWord: (options) ->
|
||||
@setBufferRange(@getBufferRange().union(@cursor.getCurrentWordBufferRange()), autoscroll: false)
|
||||
@cursor.autoscroll()
|
||||
@cursor.autoscroll() if options?.autoscroll ? true
|
||||
|
||||
# Public: Selects an entire line in the buffer.
|
||||
#
|
||||
# * `row` The line {Number} to select (default: the row of the cursor).
|
||||
selectLine: (row=@cursor.getBufferPosition().row) ->
|
||||
selectLine: (row, options) ->
|
||||
row ?= @cursor.getBufferPosition().row
|
||||
range = @editor.bufferRangeForBufferRow(row, includeNewline: true)
|
||||
@setBufferRange(@getBufferRange().union(range), autoscroll: true)
|
||||
@setBufferRange(@getBufferRange().union(range), options)
|
||||
@linewise = true
|
||||
@wordwise = false
|
||||
@initialScreenRange = @getScreenRange()
|
||||
@@ -341,10 +341,10 @@ class Selection extends Model
|
||||
# the cursor currently rests.
|
||||
#
|
||||
# It also includes the newline character.
|
||||
expandOverLine: ->
|
||||
expandOverLine: (options) ->
|
||||
range = @getBufferRange().union(@cursor.getCurrentLineBufferRange(includeNewline: true))
|
||||
@setBufferRange(range, autoscroll: false)
|
||||
@cursor.autoscroll()
|
||||
@cursor.autoscroll() if options?.autoscroll ? true
|
||||
|
||||
###
|
||||
Section: Modifying the selected text
|
||||
|
||||
@@ -66,15 +66,11 @@ class Task
|
||||
# * `taskPath` The {String} path to the CoffeeScript/JavaScript file that
|
||||
# exports a single {Function} to execute.
|
||||
constructor: (taskPath) ->
|
||||
coffeeCacheRequire = "require('#{require.resolve('coffee-cash')}')"
|
||||
coffeeCachePath = require('coffee-cash').getCacheDirectory()
|
||||
coffeeStackRequire = "require('#{require.resolve('coffeestack')}')"
|
||||
stackCachePath = require('coffeestack').getCacheDirectory()
|
||||
compileCacheRequire = "require('#{require.resolve('./compile-cache')}')"
|
||||
compileCachePath = require('./compile-cache').getCacheDirectory()
|
||||
taskBootstrapRequire = "require('#{require.resolve('./task-bootstrap')}');"
|
||||
bootstrap = """
|
||||
#{coffeeCacheRequire}.setCacheDirectory('#{coffeeCachePath}');
|
||||
#{coffeeCacheRequire}.register();
|
||||
#{coffeeStackRequire}.setCacheDirectory('#{stackCachePath}');
|
||||
#{compileCacheRequire}.setCacheDirectory('#{compileCachePath}');
|
||||
#{taskBootstrapRequire}
|
||||
"""
|
||||
bootstrap = bootstrap.replace(/\\/g, "\\\\")
|
||||
|
||||
@@ -241,13 +241,13 @@ class TextEditorComponent
|
||||
# 4. compositionend fired
|
||||
# 5. textInput fired; event.data == the completion string
|
||||
|
||||
selectedText = null
|
||||
checkpoint = null
|
||||
@domNode.addEventListener 'compositionstart', =>
|
||||
selectedText = @editor.getSelectedText()
|
||||
checkpoint = @editor.createCheckpoint()
|
||||
@domNode.addEventListener 'compositionupdate', (event) =>
|
||||
@editor.insertText(event.data, select: true, undo: 'skip')
|
||||
@editor.insertText(event.data, select: true)
|
||||
@domNode.addEventListener 'compositionend', (event) =>
|
||||
@editor.insertText(selectedText, select: true, undo: 'skip')
|
||||
@editor.revertToCheckpoint(checkpoint)
|
||||
event.target.value = ''
|
||||
|
||||
# Listen for selection changes and store the currently selected text
|
||||
@@ -395,16 +395,16 @@ class TextEditorComponent
|
||||
if cursorAtScreenPosition and @editor.hasMultipleCursors()
|
||||
cursorAtScreenPosition.destroy()
|
||||
else
|
||||
@editor.addCursorAtScreenPosition(screenPosition)
|
||||
@editor.addCursorAtScreenPosition(screenPosition, autoscroll: false)
|
||||
else
|
||||
@editor.setCursorScreenPosition(screenPosition)
|
||||
@editor.setCursorScreenPosition(screenPosition, autoscroll: false)
|
||||
when 2
|
||||
@editor.getLastSelection().selectWord()
|
||||
@editor.getLastSelection().selectWord(autoscroll: false)
|
||||
when 3
|
||||
@editor.getLastSelection().selectLine()
|
||||
@editor.getLastSelection().selectLine(null, autoscroll: false)
|
||||
|
||||
@handleDragUntilMouseUp event, (screenPosition) =>
|
||||
@editor.selectToScreenPosition(screenPosition, true)
|
||||
@handleDragUntilMouseUp (screenPosition) =>
|
||||
@editor.selectToScreenPosition(screenPosition, suppressSelectionMerge: true, autoscroll: false)
|
||||
|
||||
onLineNumberGutterMouseDown: (event) =>
|
||||
return unless event.button is 0 # only handle the left mouse button
|
||||
@@ -419,61 +419,43 @@ class TextEditorComponent
|
||||
@onGutterClick(event)
|
||||
|
||||
onGutterClick: (event) =>
|
||||
clickedRow = @screenPositionForMouseEvent(event).row
|
||||
clickedBufferRow = @editor.bufferRowForScreenRow(clickedRow)
|
||||
|
||||
@editor.setSelectedBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]], preserveFolds: true)
|
||||
|
||||
@handleDragUntilMouseUp event, (screenPosition) =>
|
||||
dragRow = screenPosition.row
|
||||
dragBufferRow = @editor.bufferRowForScreenRow(dragRow)
|
||||
if dragBufferRow < clickedBufferRow # dragging up
|
||||
@editor.setSelectedBufferRange([[dragBufferRow, 0], [clickedBufferRow + 1, 0]], reversed: true, preserveFolds: true, autoscroll: false)
|
||||
else
|
||||
@editor.setSelectedBufferRange([[clickedBufferRow, 0], [dragBufferRow + 1, 0]], reversed: false, preserveFolds: true, autoscroll: false)
|
||||
@editor.getLastCursor().autoscroll()
|
||||
clickedScreenRow = @screenPositionForMouseEvent(event).row
|
||||
clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
|
||||
initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
|
||||
@editor.setSelectedScreenRange(initialScreenRange, preserveFolds: true, autoscroll: false)
|
||||
@handleGutterDrag(initialScreenRange)
|
||||
|
||||
onGutterMetaClick: (event) =>
|
||||
clickedRow = @screenPositionForMouseEvent(event).row
|
||||
clickedBufferRow = @editor.bufferRowForScreenRow(clickedRow)
|
||||
|
||||
bufferRange = new Range([clickedBufferRow, 0], [clickedBufferRow + 1, 0])
|
||||
rowSelection = @editor.addSelectionForBufferRange(bufferRange, preserveFolds: true)
|
||||
|
||||
@handleDragUntilMouseUp event, (screenPosition) =>
|
||||
dragRow = screenPosition.row
|
||||
dragBufferRow = @editor.bufferRowForScreenRow(dragRow)
|
||||
|
||||
if dragBufferRow < clickedBufferRow # dragging up
|
||||
rowSelection.setBufferRange([[dragBufferRow, 0], [clickedBufferRow + 1, 0]], preserveFolds: true)
|
||||
else
|
||||
rowSelection.setBufferRange([[clickedBufferRow, 0], [dragBufferRow + 1, 0]], preserveFolds: true)
|
||||
|
||||
# The merge process will possibly destroy the current selection because
|
||||
# it will be merged into another one. Therefore, we need to obtain a
|
||||
# reference to the new selection that contains the originally selected row
|
||||
rowSelection = _.find @editor.getSelections(), (selection) ->
|
||||
selection.intersectsBufferRange(bufferRange)
|
||||
clickedScreenRow = @screenPositionForMouseEvent(event).row
|
||||
clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
|
||||
initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
|
||||
@editor.addSelectionForScreenRange(initialScreenRange, preserveFolds: true, autoscroll: false)
|
||||
@handleGutterDrag(initialScreenRange)
|
||||
|
||||
onGutterShiftClick: (event) =>
|
||||
clickedRow = @screenPositionForMouseEvent(event).row
|
||||
clickedBufferRow = @editor.bufferRowForScreenRow(clickedRow)
|
||||
tailPosition = @editor.getLastSelection().getTailScreenPosition()
|
||||
tailBufferPosition = @editor.bufferPositionForScreenPosition(tailPosition)
|
||||
tailScreenPosition = @editor.getLastSelection().getTailScreenPosition()
|
||||
clickedScreenRow = @screenPositionForMouseEvent(event).row
|
||||
clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
|
||||
clickedLineScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
|
||||
|
||||
if clickedRow < tailPosition.row
|
||||
@editor.selectToBufferPosition([clickedBufferRow, 0])
|
||||
if clickedScreenRow < tailScreenPosition.row
|
||||
@editor.selectToScreenPosition(clickedLineScreenRange.start, suppressSelectionMerge: true, autoscroll: false)
|
||||
else
|
||||
@editor.selectToBufferPosition([clickedBufferRow + 1, 0])
|
||||
@editor.selectToScreenPosition(clickedLineScreenRange.end, suppressSelectionMerge: true, autoscroll: false)
|
||||
|
||||
@handleDragUntilMouseUp event, (screenPosition) =>
|
||||
@handleGutterDrag(new Range(tailScreenPosition, tailScreenPosition))
|
||||
|
||||
handleGutterDrag: (initialRange) ->
|
||||
@handleDragUntilMouseUp (screenPosition) =>
|
||||
dragRow = screenPosition.row
|
||||
dragBufferRow = @editor.bufferRowForScreenRow(dragRow)
|
||||
if dragRow < tailPosition.row # dragging up
|
||||
@editor.setSelectedBufferRange([[dragBufferRow, 0], tailBufferPosition], preserveFolds: true)
|
||||
if dragRow < initialRange.start.row
|
||||
startPosition = @editor.clipScreenPosition([dragRow, 0], skipSoftWrapIndentation: true)
|
||||
screenRange = new Range(startPosition, startPosition).union(initialRange)
|
||||
@editor.getLastSelection().setScreenRange(screenRange, reversed: true, autoscroll: false, preserveFolds: true)
|
||||
else
|
||||
@editor.setSelectedBufferRange([tailBufferPosition, [dragBufferRow + 1, 0]], preserveFolds: true)
|
||||
|
||||
endPosition = [dragRow + 1, 0]
|
||||
screenRange = new Range(endPosition, endPosition).union(initialRange)
|
||||
@editor.getLastSelection().setScreenRange(screenRange, reversed: false, autoscroll: false, preserveFolds: true)
|
||||
|
||||
onStylesheetsChanged: (styleElement) =>
|
||||
return unless @performedInitialMeasurement
|
||||
@@ -523,13 +505,15 @@ class TextEditorComponent
|
||||
onCursorMoved: =>
|
||||
@cursorMoved = true
|
||||
|
||||
handleDragUntilMouseUp: (event, dragHandler) =>
|
||||
handleDragUntilMouseUp: (dragHandler) =>
|
||||
dragging = false
|
||||
lastMousePosition = {}
|
||||
animationLoop = =>
|
||||
@requestAnimationFrame =>
|
||||
if dragging and @mounted
|
||||
screenPosition = @screenPositionForMouseEvent(lastMousePosition)
|
||||
linesClientRect = @linesComponent.getDomNode().getBoundingClientRect()
|
||||
autoscroll(lastMousePosition, linesClientRect)
|
||||
screenPosition = @screenPositionForMouseEvent(lastMousePosition, linesClientRect)
|
||||
dragHandler(screenPosition)
|
||||
animationLoop()
|
||||
else if not @mounted
|
||||
@@ -548,15 +532,47 @@ class TextEditorComponent
|
||||
onMouseUp() if event.which is 0
|
||||
|
||||
onMouseUp = (event) =>
|
||||
stopDragging()
|
||||
@editor.finalizeSelections()
|
||||
@editor.mergeIntersectingSelections()
|
||||
if dragging
|
||||
stopDragging()
|
||||
@editor.finalizeSelections()
|
||||
@editor.mergeIntersectingSelections()
|
||||
pasteSelectionClipboard(event)
|
||||
|
||||
stopDragging = ->
|
||||
dragging = false
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
window.removeEventListener('mouseup', onMouseUp)
|
||||
disposables.dispose()
|
||||
|
||||
autoscroll = (mouseClientPosition) =>
|
||||
{top, bottom, left, right} = @scrollViewNode.getBoundingClientRect()
|
||||
top += 30
|
||||
bottom -= 30
|
||||
left += 30
|
||||
right -= 30
|
||||
|
||||
if mouseClientPosition.clientY < top
|
||||
mouseYDelta = top - mouseClientPosition.clientY
|
||||
yDirection = -1
|
||||
else if mouseClientPosition.clientY > bottom
|
||||
mouseYDelta = mouseClientPosition.clientY - bottom
|
||||
yDirection = 1
|
||||
|
||||
if mouseClientPosition.clientX < left
|
||||
mouseXDelta = left - mouseClientPosition.clientX
|
||||
xDirection = -1
|
||||
else if mouseClientPosition.clientX > right
|
||||
mouseXDelta = mouseClientPosition.clientX - right
|
||||
xDirection = 1
|
||||
|
||||
if mouseYDelta?
|
||||
@presenter.setScrollTop(@presenter.getScrollTop() + yDirection * scaleScrollDelta(mouseYDelta))
|
||||
|
||||
if mouseXDelta?
|
||||
@presenter.setScrollLeft(@presenter.getScrollLeft() + xDirection * scaleScrollDelta(mouseXDelta))
|
||||
|
||||
scaleScrollDelta = (scrollDelta) ->
|
||||
Math.pow(scrollDelta / 2, 3) / 280
|
||||
|
||||
pasteSelectionClipboard = (event) =>
|
||||
if event?.which is 2 and process.platform is 'linux'
|
||||
@@ -565,6 +581,9 @@ class TextEditorComponent
|
||||
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
window.addEventListener('mouseup', onMouseUp)
|
||||
disposables = new CompositeDisposable
|
||||
disposables.add(@editor.getBuffer().onWillChange(onMouseUp))
|
||||
disposables.add(@editor.onDidDestroy(stopDragging))
|
||||
|
||||
isVisible: ->
|
||||
@domNode.offsetHeight > 0 or @domNode.offsetWidth > 0
|
||||
@@ -762,17 +781,20 @@ class TextEditorComponent
|
||||
if scrollSensitivity = parseInt(scrollSensitivity)
|
||||
@scrollSensitivity = Math.abs(scrollSensitivity) / 100
|
||||
|
||||
screenPositionForMouseEvent: (event) ->
|
||||
pixelPosition = @pixelPositionForMouseEvent(event)
|
||||
screenPositionForMouseEvent: (event, linesClientRect) ->
|
||||
pixelPosition = @pixelPositionForMouseEvent(event, linesClientRect)
|
||||
@editor.screenPositionForPixelPosition(pixelPosition)
|
||||
|
||||
pixelPositionForMouseEvent: (event) ->
|
||||
pixelPositionForMouseEvent: (event, linesClientRect) ->
|
||||
{clientX, clientY} = event
|
||||
|
||||
linesClientRect = @linesComponent.getDomNode().getBoundingClientRect()
|
||||
linesClientRect ?= @linesComponent.getDomNode().getBoundingClientRect()
|
||||
top = clientY - linesClientRect.top + @presenter.scrollTop
|
||||
left = clientX - linesClientRect.left + @presenter.scrollLeft
|
||||
{top, left}
|
||||
bottom = linesClientRect.top + @presenter.scrollTop + linesClientRect.height - clientY
|
||||
right = linesClientRect.left + @presenter.scrollLeft + linesClientRect.width - clientX
|
||||
|
||||
{top, left, bottom, right}
|
||||
|
||||
getModel: ->
|
||||
@editor
|
||||
|
||||
@@ -302,6 +302,7 @@ atom.commands.add 'atom-text-editor', stopEventPropagationAndGroupUndo(
|
||||
'editor:transpose': -> @transpose()
|
||||
'editor:upper-case': -> @upperCase()
|
||||
'editor:lower-case': -> @lowerCase()
|
||||
'editor:copy-selection': -> @copyOnlySelectedText()
|
||||
)
|
||||
|
||||
atom.commands.add 'atom-text-editor:not([mini])', stopEventPropagation(
|
||||
|
||||
@@ -304,6 +304,10 @@ class TextEditorPresenter
|
||||
@state.hiddenInput.width = Math.max(width, 2)
|
||||
|
||||
updateContentState: ->
|
||||
if @boundingClientRect?
|
||||
@sharedGutterStyles.maxHeight = @boundingClientRect.height
|
||||
@state.content.maxHeight = @boundingClientRect.height
|
||||
|
||||
@state.content.width = Math.max(@contentWidth + @verticalScrollbarWidth, @contentFrameWidth)
|
||||
@state.content.scrollWidth = @scrollWidth
|
||||
@state.content.scrollLeft = @scrollLeft
|
||||
@@ -340,18 +344,20 @@ class TextEditorPresenter
|
||||
tile.left = -@scrollLeft
|
||||
tile.height = @tileSize * @lineHeight
|
||||
tile.display = "block"
|
||||
tile.zIndex = zIndex--
|
||||
tile.zIndex = zIndex
|
||||
tile.highlights ?= {}
|
||||
|
||||
gutterTile = @lineNumberGutter.tiles[startRow] ?= {}
|
||||
gutterTile.top = startRow * @lineHeight - @scrollTop
|
||||
gutterTile.height = @tileSize * @lineHeight
|
||||
gutterTile.display = "block"
|
||||
gutterTile.zIndex = zIndex
|
||||
|
||||
@updateLinesState(tile, startRow, endRow) if @shouldUpdateLinesState
|
||||
@updateLineNumbersState(gutterTile, startRow, endRow) if @shouldUpdateLineNumbersState
|
||||
|
||||
visibleTiles[startRow] = true
|
||||
zIndex--
|
||||
|
||||
if @mouseWheelScreenRow? and @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)?
|
||||
mouseWheelTile = @tileForRow(@mouseWheelScreenRow)
|
||||
@@ -410,19 +416,15 @@ class TextEditorPresenter
|
||||
@updateCursorState(cursor) for cursor in @model.cursors # using property directly to avoid allocation
|
||||
return
|
||||
|
||||
updateCursorState: (cursor, destroyOnly = false) ->
|
||||
delete @state.content.cursors[cursor.id]
|
||||
|
||||
return if destroyOnly
|
||||
updateCursorState: (cursor) ->
|
||||
return unless @startRow? and @endRow? and @hasPixelRectRequirements() and @baseCharacterWidth?
|
||||
return unless cursor.isVisible() and @startRow <= cursor.getScreenRow() < @endRow
|
||||
screenRange = cursor.getScreenRange()
|
||||
return unless cursor.isVisible() and @startRow <= screenRange.start.row < @endRow
|
||||
|
||||
pixelRect = @pixelRectForScreenRange(cursor.getScreenRange())
|
||||
pixelRect = @pixelRectForScreenRange(screenRange)
|
||||
pixelRect.width = @baseCharacterWidth if pixelRect.width is 0
|
||||
@state.content.cursors[cursor.id] = pixelRect
|
||||
|
||||
@emitDidUpdateState()
|
||||
|
||||
updateOverlaysState: ->
|
||||
return unless @hasOverlayPositionRequirements()
|
||||
|
||||
@@ -546,7 +548,7 @@ class TextEditorPresenter
|
||||
@clearDecorationsForCustomGutterName(gutterName)
|
||||
else
|
||||
@customGutterDecorations[gutterName] = {}
|
||||
return if not @gutterIsVisible(gutter)
|
||||
continue if not @gutterIsVisible(gutter)
|
||||
|
||||
relevantDecorations = @customGutterDecorationsInRange(gutterName, @startRow, @endRow - 1)
|
||||
relevantDecorations.forEach (decoration) =>
|
||||
@@ -588,7 +590,9 @@ class TextEditorPresenter
|
||||
wrapCount = 0
|
||||
|
||||
if endRow > startRow
|
||||
for bufferRow, i in @model.bufferRowsForScreenRows(startRow, endRow - 1)
|
||||
bufferRows = @model.bufferRowsForScreenRows(startRow, endRow - 1)
|
||||
zIndex = bufferRows.length - 1
|
||||
for bufferRow, i in bufferRows
|
||||
if bufferRow is lastBufferRow
|
||||
wrapCount++
|
||||
id = bufferRow + '-' + wrapCount
|
||||
@@ -604,8 +608,9 @@ class TextEditorPresenter
|
||||
decorationClasses = @lineNumberDecorationClassesForRow(screenRow)
|
||||
foldable = @model.isFoldableAtScreenRow(screenRow)
|
||||
|
||||
tileState.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable}
|
||||
tileState.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable, zIndex}
|
||||
visibleLineNumberIds[id] = true
|
||||
zIndex--
|
||||
|
||||
for id of tileState.lineNumbers
|
||||
delete tileState.lineNumbers[id] unless visibleLineNumberIds[id]
|
||||
@@ -923,6 +928,7 @@ class TextEditorPresenter
|
||||
unless @clientRectsEqual(@boundingClientRect, boundingClientRect)
|
||||
@boundingClientRect = boundingClientRect
|
||||
@shouldUpdateOverlaysState = true
|
||||
@shouldUpdateContentState = true
|
||||
|
||||
@emitDidUpdateState()
|
||||
|
||||
@@ -1166,7 +1172,7 @@ class TextEditorPresenter
|
||||
if decoration.isType('line') or decoration.isType('gutter')
|
||||
@addToLineDecorationCaches(decoration, range)
|
||||
else if decoration.isType('highlight')
|
||||
@updateHighlightState(decoration)
|
||||
@updateHighlightState(decoration, range)
|
||||
|
||||
for tileId, tileState of @state.content.tiles
|
||||
for id, highlight of tileState.highlights
|
||||
@@ -1237,12 +1243,11 @@ class TextEditorPresenter
|
||||
|
||||
intersectingRange
|
||||
|
||||
updateHighlightState: (decoration) ->
|
||||
updateHighlightState: (decoration, range) ->
|
||||
return unless @startRow? and @endRow? and @lineHeight? and @hasPixelPositionRequirements()
|
||||
|
||||
properties = decoration.getProperties()
|
||||
marker = decoration.getMarker()
|
||||
range = marker.getScreenRange()
|
||||
|
||||
if decoration.isDestroyed() or not marker.isValid() or range.isEmpty() or not range.intersectsRowRange(@startRow, @endRow - 1)
|
||||
return
|
||||
@@ -1365,20 +1370,22 @@ class TextEditorPresenter
|
||||
observeCursor: (cursor) ->
|
||||
didChangePositionDisposable = cursor.onDidChangePosition =>
|
||||
@shouldUpdateHiddenInputState = true if cursor.isLastCursor()
|
||||
@shouldUpdateCursorsState = true
|
||||
@pauseCursorBlinking()
|
||||
@updateCursorState(cursor)
|
||||
|
||||
@emitDidUpdateState()
|
||||
|
||||
didChangeVisibilityDisposable = cursor.onDidChangeVisibility =>
|
||||
@updateCursorState(cursor)
|
||||
@shouldUpdateCursorsState = true
|
||||
|
||||
@emitDidUpdateState()
|
||||
|
||||
didDestroyDisposable = cursor.onDidDestroy =>
|
||||
@disposables.remove(didChangePositionDisposable)
|
||||
@disposables.remove(didChangeVisibilityDisposable)
|
||||
@disposables.remove(didDestroyDisposable)
|
||||
@shouldUpdateHiddenInputState = true
|
||||
@updateCursorState(cursor, true)
|
||||
@shouldUpdateCursorsState = true
|
||||
|
||||
@emitDidUpdateState()
|
||||
|
||||
@@ -1389,8 +1396,9 @@ class TextEditorPresenter
|
||||
didAddCursor: (cursor) ->
|
||||
@observeCursor(cursor)
|
||||
@shouldUpdateHiddenInputState = true
|
||||
@shouldUpdateCursorsState = true
|
||||
@pauseCursorBlinking()
|
||||
@updateCursorState(cursor)
|
||||
|
||||
@emitDidUpdateState()
|
||||
|
||||
startBlinkingCursors: ->
|
||||
|
||||
@@ -14,7 +14,7 @@ TextMateScopeSelector = require('first-mate').ScopeSelector
|
||||
{Directory} = require "pathwatcher"
|
||||
GutterContainer = require './gutter-container'
|
||||
|
||||
# Public: This class represents all essential editing state for a single
|
||||
# Essential: This class represents all essential editing state for a single
|
||||
# {TextBuffer}, including cursor and selection positions, folds, and soft wraps.
|
||||
# If you're manipulating the state of an editor, use this class. If you're
|
||||
# interested in the visual appearance of editors, use {TextEditorView} instead.
|
||||
@@ -86,16 +86,16 @@ class TextEditor extends Model
|
||||
buffer ?= new TextBuffer
|
||||
@displayBuffer ?= new DisplayBuffer({buffer, tabLength, softWrapped, ignoreInvisibles: @mini, largeFileMode})
|
||||
@buffer = @displayBuffer.buffer
|
||||
@softTabs = @usesSoftTabs() ? @softTabs ? atom.config.get('editor.softTabs') ? true
|
||||
|
||||
for marker in @findMarkers(@getSelectionMarkerAttributes())
|
||||
marker.setProperties(preserveFolds: true)
|
||||
@addSelection(marker)
|
||||
|
||||
@subscribeToTabTypeConfig()
|
||||
@subscribeToBuffer()
|
||||
@subscribeToDisplayBuffer()
|
||||
|
||||
if @getCursors().length is 0 and not suppressCursorCreation
|
||||
if @cursors.length is 0 and not suppressCursorCreation
|
||||
initialLine = Math.max(parseInt(initialLine) or 0, 0)
|
||||
initialColumn = Math.max(parseInt(initialColumn) or 0, 0)
|
||||
@addCursorAtBufferPosition([initialLine, initialColumn])
|
||||
@@ -176,10 +176,16 @@ class TextEditor extends Model
|
||||
@subscribe @displayBuffer.onDidAddDecoration (decoration) => @emit 'decoration-added', decoration
|
||||
@subscribe @displayBuffer.onDidRemoveDecoration (decoration) => @emit 'decoration-removed', decoration
|
||||
|
||||
subscribeToTabTypeConfig: ->
|
||||
@tabTypeSubscription?.dispose()
|
||||
@tabTypeSubscription = atom.config.observe 'editor.tabType', scope: @getRootScopeDescriptor(), =>
|
||||
@softTabs = @shouldUseSoftTabs(defaultValue: @softTabs)
|
||||
|
||||
destroyed: ->
|
||||
@unsubscribe() if includeDeprecatedAPIs
|
||||
@disposables.dispose()
|
||||
selection.destroy() for selection in @getSelections()
|
||||
@tabTypeSubscription.dispose()
|
||||
selection.destroy() for selection in @selections.slice()
|
||||
@buffer.release()
|
||||
@displayBuffer.destroy()
|
||||
@languageMode.destroy()
|
||||
@@ -335,7 +341,7 @@ class TextEditor extends Model
|
||||
onDidInsertText: (callback) ->
|
||||
@emitter.on 'did-insert-text', callback
|
||||
|
||||
# Public: Invoke the given callback after the buffer is saved to disk.
|
||||
# Essential: Invoke the given callback after the buffer is saved to disk.
|
||||
#
|
||||
# * `callback` {Function} to be called after the buffer is saved.
|
||||
# * `event` {Object} with the following keys:
|
||||
@@ -345,7 +351,7 @@ class TextEditor extends Model
|
||||
onDidSave: (callback) ->
|
||||
@getBuffer().onDidSave(callback)
|
||||
|
||||
# Public: Invoke the given callback when the editor is destroyed.
|
||||
# Essential: Invoke the given callback when the editor is destroyed.
|
||||
#
|
||||
# * `callback` {Function} to be called when the editor is destroyed.
|
||||
#
|
||||
@@ -464,7 +470,7 @@ class TextEditor extends Model
|
||||
onDidUpdateMarkers: (callback) ->
|
||||
@displayBuffer.onDidUpdateMarkers(callback)
|
||||
|
||||
# Public: Retrieves the current {TextBuffer}.
|
||||
# Essential: Retrieves the current {TextBuffer}.
|
||||
getBuffer: -> @buffer
|
||||
|
||||
# Retrieves the current buffer's URI.
|
||||
@@ -508,20 +514,7 @@ class TextEditor extends Model
|
||||
onDidChangeLineNumberGutterVisible: (callback) ->
|
||||
@emitter.on 'did-change-line-number-gutter-visible', callback
|
||||
|
||||
# Public: Creates and returns a {Gutter}.
|
||||
# See {GutterContainer::addGutter} for more details.
|
||||
addGutter: (options) ->
|
||||
@gutterContainer.addGutter(options)
|
||||
|
||||
# Public: Returns the {Array} of all gutters on this editor.
|
||||
getGutters: ->
|
||||
@gutterContainer.getGutters()
|
||||
|
||||
# Public: Returns the {Gutter} with the given name, or null if it doesn't exist.
|
||||
gutterWithName: (name) ->
|
||||
@gutterContainer.gutterWithName(name)
|
||||
|
||||
# Calls your `callback` when a {Gutter} is added to the editor.
|
||||
# Essential: Calls your `callback` when a {Gutter} is added to the editor.
|
||||
# Immediately calls your callback for each existing gutter.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
@@ -531,7 +524,7 @@ class TextEditor extends Model
|
||||
observeGutters: (callback) ->
|
||||
@gutterContainer.observeGutters callback
|
||||
|
||||
# Calls your `callback` when a {Gutter} is added to the editor.
|
||||
# Essential: Calls your `callback` when a {Gutter} is added to the editor.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
# * `gutter` {Gutter} that was added.
|
||||
@@ -540,7 +533,7 @@ class TextEditor extends Model
|
||||
onDidAddGutter: (callback) ->
|
||||
@gutterContainer.onDidAddGutter callback
|
||||
|
||||
# Calls your `callback` when a {Gutter} is removed from the editor.
|
||||
# Essential: Calls your `callback` when a {Gutter} is removed from the editor.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
# * `name` The name of the {Gutter} that was removed.
|
||||
@@ -623,7 +616,7 @@ class TextEditor extends Model
|
||||
# See {TextBuffer::save} for more details.
|
||||
save: -> @buffer.save(backup: atom.config.get('editor.backUpBeforeSaving'))
|
||||
|
||||
# Public: Saves the editor's text buffer as the given path.
|
||||
# Essential: Saves the editor's text buffer as the given path.
|
||||
#
|
||||
# See {TextBuffer::saveAs} for more details.
|
||||
#
|
||||
@@ -736,7 +729,7 @@ class TextEditor extends Model
|
||||
# {Delegates to: TextBuffer.getEndPosition}
|
||||
getEofBufferPosition: -> @buffer.getEndPosition()
|
||||
|
||||
# Public: Get the {Range} of the paragraph surrounding the most recently added
|
||||
# Essential: Get the {Range} of the paragraph surrounding the most recently added
|
||||
# cursor.
|
||||
#
|
||||
# Returns a {Range}.
|
||||
@@ -1130,12 +1123,12 @@ class TextEditor extends Model
|
||||
|
||||
# Essential: Undo the last change.
|
||||
undo: ->
|
||||
@buffer.undo()
|
||||
@avoidMergingSelections => @buffer.undo()
|
||||
@getLastSelection().autoscroll()
|
||||
|
||||
# Essential: Redo the last change.
|
||||
redo: ->
|
||||
@buffer.redo(this)
|
||||
@avoidMergingSelections => @buffer.redo()
|
||||
@getLastSelection().autoscroll()
|
||||
|
||||
# Extended: Batch multiple operations as a single undo/redo step.
|
||||
@@ -1303,7 +1296,7 @@ class TextEditor extends Model
|
||||
#
|
||||
# * __line__: Adds your CSS `class` to the line nodes within the range
|
||||
# marked by the marker
|
||||
# * __gutter__: Adds your CSS `class` to the line number nodes within the
|
||||
# * __line-number__: Adds your CSS `class` to the line number nodes within the
|
||||
# range marked by the marker
|
||||
# * __highlight__: Adds a new highlight div to the editor surrounding the
|
||||
# range marked by the marker. When the user selects text, the selection is
|
||||
@@ -1321,9 +1314,9 @@ class TextEditor extends Model
|
||||
# * `marker` A {Marker} you want this decoration to follow.
|
||||
# * `decorationParams` An {Object} representing the decoration e.g.
|
||||
# `{type: 'line-number', class: 'linter-error'}`
|
||||
# * `type` There are a few supported decoration types: `gutter`, `line`,
|
||||
# * `type` There are a few supported decoration types: `line-number`, `line`,
|
||||
# `highlight`, and `overlay`. The behavior of the types are as follows:
|
||||
# * `gutter` Adds the given `class` to the line numbers overlapping the
|
||||
# * `line-number` Adds the given `class` to the line numbers overlapping the
|
||||
# rows spanned by the marker.
|
||||
# * `line` Adds the given `class` to the lines overlapping the rows
|
||||
# spanned by the marker.
|
||||
@@ -1335,19 +1328,17 @@ class TextEditor extends Model
|
||||
# * `class` This CSS class will be applied to the decorated line number,
|
||||
# line, highlight, or overlay.
|
||||
# * `onlyHead` (optional) If `true`, the decoration will only be applied to
|
||||
# the head of the marker. Only applicable to the `line` and `gutter`
|
||||
# the head of the marker. Only applicable to the `line` and `line-number`
|
||||
# types.
|
||||
# * `onlyEmpty` (optional) If `true`, the decoration will only be applied if
|
||||
# the associated marker is empty. Only applicable to the `line` and
|
||||
# `gutter` types.
|
||||
# `line-number` types.
|
||||
# * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied
|
||||
# if the associated marker is non-empty. Only applicable to the `line`
|
||||
# and gutter types.
|
||||
# and `line-number` types.
|
||||
# * `position` (optional) Only applicable to decorations of type `overlay`,
|
||||
# controls where the overlay view is positioned relative to the marker.
|
||||
# Values can be `'head'` (the default), or `'tail'`.
|
||||
# * `gutterName` (optional) Only applicable to the `gutter` type. If provided,
|
||||
# the decoration will be applied to the gutter with the specified name.
|
||||
#
|
||||
# Returns a {Decoration} object
|
||||
decorateMarker: (marker, decorationParams) ->
|
||||
@@ -1356,7 +1347,7 @@ class TextEditor extends Model
|
||||
decorationParams.type = 'line-number'
|
||||
@displayBuffer.decorateMarker(marker, decorationParams)
|
||||
|
||||
# Public: Get all the decorations within a screen row range.
|
||||
# Essential: Get all the decorations within a screen row range.
|
||||
#
|
||||
# * `startScreenRow` the {Number} beginning screen row
|
||||
# * `endScreenRow` the {Number} end screen row (inclusive)
|
||||
@@ -1755,6 +1746,7 @@ class TextEditor extends Model
|
||||
|
||||
# Extended: Returns the most recently added {Cursor}
|
||||
getLastCursor: ->
|
||||
@createLastSelectionIfNeeded()
|
||||
_.last(@cursors)
|
||||
|
||||
# Extended: Returns the word surrounding the most recently added cursor.
|
||||
@@ -1765,6 +1757,7 @@ class TextEditor extends Model
|
||||
|
||||
# Extended: Get an Array of all {Cursor}s.
|
||||
getCursors: ->
|
||||
@createLastSelectionIfNeeded()
|
||||
@cursors.slice()
|
||||
|
||||
# Extended: Get all {Cursors}s, ordered by their position in the buffer
|
||||
@@ -1850,6 +1843,8 @@ class TextEditor extends Model
|
||||
# * `options` (optional) An options {Object}:
|
||||
# * `reversed` A {Boolean} indicating whether to create the selection in a
|
||||
# reversed orientation.
|
||||
# * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the
|
||||
# selection is set.
|
||||
setSelectedBufferRange: (bufferRange, options) ->
|
||||
@setSelectedBufferRanges([bufferRange], options)
|
||||
|
||||
@@ -1860,6 +1855,8 @@ class TextEditor extends Model
|
||||
# * `options` (optional) An options {Object}:
|
||||
# * `reversed` A {Boolean} indicating whether to create the selection in a
|
||||
# reversed orientation.
|
||||
# * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the
|
||||
# selection is set.
|
||||
setSelectedBufferRanges: (bufferRanges, options={}) ->
|
||||
throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length
|
||||
|
||||
@@ -1965,10 +1962,10 @@ class TextEditor extends Model
|
||||
# This method may merge selections that end up intesecting.
|
||||
#
|
||||
# * `position` An instance of {Point}, with a given `row` and `column`.
|
||||
selectToScreenPosition: (position, suppressMerge) ->
|
||||
selectToScreenPosition: (position, options) ->
|
||||
lastSelection = @getLastSelection()
|
||||
lastSelection.selectToScreenPosition(position)
|
||||
unless suppressMerge
|
||||
lastSelection.selectToScreenPosition(position, options)
|
||||
unless options?.suppressSelectionMerge
|
||||
@mergeIntersectingSelections(reversed: lastSelection.isReversed())
|
||||
|
||||
# Essential: Move the cursor of each selection one character upward while
|
||||
@@ -2140,12 +2137,14 @@ class TextEditor extends Model
|
||||
#
|
||||
# Returns a {Selection}.
|
||||
getLastSelection: ->
|
||||
@createLastSelectionIfNeeded()
|
||||
_.last(@selections)
|
||||
|
||||
# Extended: Get current {Selection}s.
|
||||
#
|
||||
# Returns: An {Array} of {Selection}s.
|
||||
getSelections: ->
|
||||
@createLastSelectionIfNeeded()
|
||||
@selections.slice()
|
||||
|
||||
# Extended: Get all {Selection}s, ordered by their position in the buffer
|
||||
@@ -2224,6 +2223,9 @@ class TextEditor extends Model
|
||||
|
||||
previousSelection.intersectsScreenRowRange(screenRange.start.row, screenRange.end.row)
|
||||
|
||||
avoidMergingSelections: (args...) ->
|
||||
@mergeSelections args..., -> false
|
||||
|
||||
mergeSelections: (args...) ->
|
||||
mergePredicate = args.pop()
|
||||
fn = args.pop() if _.isFunction(_.last(args))
|
||||
@@ -2299,6 +2301,10 @@ class TextEditor extends Model
|
||||
@emit 'selection-screen-range-changed', event if includeDeprecatedAPIs
|
||||
@emitter.emit 'did-change-selection-range', event
|
||||
|
||||
createLastSelectionIfNeeded: ->
|
||||
if @selections.length is 0
|
||||
@addSelectionForBufferRange([[0, 0], [0, 0]], autoscroll: false, preserveFolds: true)
|
||||
|
||||
###
|
||||
Section: Searching and Replacing
|
||||
###
|
||||
@@ -2321,7 +2327,7 @@ class TextEditor extends Model
|
||||
# * `replace` Call this {Function} with a {String} to replace the match.
|
||||
scan: (regex, iterator) -> @buffer.scan(regex, iterator)
|
||||
|
||||
# Public: Scan regular expression matches in a given range, calling the given
|
||||
# Essential: Scan regular expression matches in a given range, calling the given
|
||||
# iterator function on each match.
|
||||
#
|
||||
# * `regex` A {RegExp} to search for.
|
||||
@@ -2335,7 +2341,7 @@ class TextEditor extends Model
|
||||
# * `replace` Call this {Function} with a {String} to replace the match.
|
||||
scanInBufferRange: (regex, range, iterator) -> @buffer.scanInRange(regex, range, iterator)
|
||||
|
||||
# Public: Scan regular expression matches in a given range in reverse order,
|
||||
# Essential: Scan regular expression matches in a given range in reverse order,
|
||||
# calling the given iterator function on each match.
|
||||
#
|
||||
# * `regex` A {RegExp} to search for.
|
||||
@@ -2387,7 +2393,7 @@ class TextEditor extends Model
|
||||
usesSoftTabs: ->
|
||||
# FIXME Remove once this can be specified as a scoped setting in the
|
||||
# language-make package
|
||||
return false if @getGrammar().scopeName is 'source.makefile'
|
||||
return false if @getGrammar()?.scopeName is 'source.makefile'
|
||||
|
||||
for bufferRow in [0..@buffer.getLastRow()]
|
||||
continue if @displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
|
||||
@@ -2412,6 +2418,20 @@ class TextEditor extends Model
|
||||
return unless @getSoftTabs()
|
||||
@scanInBufferRange /\t/g, bufferRange, ({replace}) => replace(@getTabText())
|
||||
|
||||
# Private: Computes whether or not this editor should use softTabs based on
|
||||
# the `editor.tabType` setting.
|
||||
#
|
||||
# Returns a {Boolean}
|
||||
shouldUseSoftTabs: ({defaultValue}) ->
|
||||
tabType = atom.config.get('editor.tabType', scope: @getRootScopeDescriptor())
|
||||
switch tabType
|
||||
when 'auto'
|
||||
@usesSoftTabs() ? defaultValue ? atom.config.get('editor.softTabs') ? true
|
||||
when 'hard'
|
||||
false
|
||||
when 'soft'
|
||||
true
|
||||
|
||||
###
|
||||
Section: Soft Wrap Behavior
|
||||
###
|
||||
@@ -2433,7 +2453,7 @@ class TextEditor extends Model
|
||||
# Returns a {Boolean}.
|
||||
toggleSoftWrapped: -> @setSoftWrapped(not @isSoftWrapped())
|
||||
|
||||
# Public: Gets the column at which column will soft wrap
|
||||
# Essential: Gets the column at which column will soft wrap
|
||||
getSoftWrapColumn: -> @displayBuffer.getSoftWrapColumn()
|
||||
|
||||
###
|
||||
@@ -2605,6 +2625,15 @@ class TextEditor extends Model
|
||||
maintainClipboard = true
|
||||
return
|
||||
|
||||
# Private: For each selection, only copy highlighted text.
|
||||
copyOnlySelectedText: ->
|
||||
maintainClipboard = false
|
||||
for selection in @getSelectionsOrderedByBufferPosition()
|
||||
if not selection.isEmpty()
|
||||
selection.copy(maintainClipboard, true)
|
||||
maintainClipboard = true
|
||||
return
|
||||
|
||||
# Essential: For each selection, cut the selected text.
|
||||
cutSelectedText: ->
|
||||
maintainClipboard = false
|
||||
@@ -2659,7 +2688,7 @@ class TextEditor extends Model
|
||||
@emit('did-insert-text', didInsertEvent) if includeDeprecatedAPIs
|
||||
@emitter.emit 'did-insert-text', didInsertEvent
|
||||
|
||||
# Public: For each selection, if the selection is empty, cut all characters
|
||||
# Essential: For each selection, if the selection is empty, cut all characters
|
||||
# of the containing line following the cursor. Otherwise cut the selected
|
||||
# text.
|
||||
cutToEndOfLine: ->
|
||||
@@ -2807,6 +2836,36 @@ class TextEditor extends Model
|
||||
outermostFoldsInBufferRowRange: (startRow, endRow) ->
|
||||
@displayBuffer.outermostFoldsInBufferRowRange(startRow, endRow)
|
||||
|
||||
###
|
||||
Section: Gutters
|
||||
###
|
||||
|
||||
# Essential: Add a custom {Gutter}.
|
||||
#
|
||||
# * `options` An {Object} with the following fields:
|
||||
# * `name` (required) A unique {String} to identify this gutter.
|
||||
# * `priority` (optional) A {Number} that determines stacking order between
|
||||
# gutters. Lower priority items are forced closer to the edges of the
|
||||
# window. (default: -100)
|
||||
# * `visible` (optional) {Boolean} specifying whether the gutter is visible
|
||||
# initially after being created. (default: true)
|
||||
#
|
||||
# Returns the newly-created {Gutter}.
|
||||
addGutter: (options) ->
|
||||
@gutterContainer.addGutter(options)
|
||||
|
||||
# Essential: Get this editor's gutters.
|
||||
#
|
||||
# Returns an {Array} of {Gutter}s.
|
||||
getGutters: ->
|
||||
@gutterContainer.getGutters()
|
||||
|
||||
# Essential: Get the gutter with the given name.
|
||||
#
|
||||
# Returns a {Gutter}, or `null` if no gutter exists for the given name.
|
||||
gutterWithName: (name) ->
|
||||
@gutterContainer.gutterWithName(name)
|
||||
|
||||
###
|
||||
Section: Scrolling the TextEditor
|
||||
###
|
||||
@@ -2858,14 +2917,10 @@ class TextEditor extends Model
|
||||
setVerticalScrollbarWidth: (width) -> @displayBuffer.setVerticalScrollbarWidth(width)
|
||||
|
||||
pageUp: ->
|
||||
newScrollTop = @getScrollTop() - @getHeight()
|
||||
@moveUp(@getRowsPerPage())
|
||||
@setScrollTop(newScrollTop)
|
||||
|
||||
pageDown: ->
|
||||
newScrollTop = @getScrollTop() + @getHeight()
|
||||
@moveDown(@getRowsPerPage())
|
||||
@setScrollTop(newScrollTop)
|
||||
|
||||
selectPageUp: ->
|
||||
@selectUp(@getRowsPerPage())
|
||||
@@ -2875,7 +2930,7 @@ class TextEditor extends Model
|
||||
|
||||
# Returns the number of rows per page
|
||||
getRowsPerPage: ->
|
||||
Math.max(1, Math.ceil(@getHeight() / @getLineHeightInPixels()))
|
||||
Math.max(1, Math.floor(@getHeight() / @getLineHeightInPixels()))
|
||||
|
||||
###
|
||||
Section: Config
|
||||
@@ -2892,10 +2947,11 @@ class TextEditor extends Model
|
||||
###
|
||||
|
||||
handleTokenization: ->
|
||||
@softTabs = @usesSoftTabs() ? @softTabs
|
||||
@softTabs = @shouldUseSoftTabs(defaultValue: @softTabs)
|
||||
|
||||
handleGrammarChange: ->
|
||||
@unfoldAll()
|
||||
@subscribeToTabTypeConfig()
|
||||
@emitter.emit 'did-change-grammar', @getGrammar()
|
||||
|
||||
handleMarkerCreated: (marker) =>
|
||||
@@ -2906,13 +2962,13 @@ class TextEditor extends Model
|
||||
Section: TextEditor Rendering
|
||||
###
|
||||
|
||||
# Public: Retrieves the greyed out placeholder of a mini editor.
|
||||
# Essential: Retrieves the greyed out placeholder of a mini editor.
|
||||
#
|
||||
# Returns a {String}.
|
||||
getPlaceholderText: ->
|
||||
@placeholderText
|
||||
|
||||
# Public: Set the greyed out placeholder of a mini editor. Placeholder text
|
||||
# Essential: Set the greyed out placeholder of a mini editor. Placeholder text
|
||||
# will be displayed when the editor has no content.
|
||||
#
|
||||
# * `placeholderText` {String} text that is displayed when the editor has no content.
|
||||
|
||||
@@ -68,7 +68,7 @@ class TokenizedBuffer extends Model
|
||||
if grammar.injectionSelector?
|
||||
@retokenizeLines() if @hasTokenForSelector(grammar.injectionSelector)
|
||||
else
|
||||
newScore = grammar.getScore(@buffer.getPath(), @getGrammarSelectionContent())
|
||||
newScore = atom.grammars.getGrammarScore(grammar, @buffer.getPath(), @getGrammarSelectionContent())
|
||||
@setGrammar(grammar, newScore) if newScore > @currentGrammarScore
|
||||
|
||||
setGrammar: (grammar, score) ->
|
||||
@@ -76,7 +76,7 @@ class TokenizedBuffer extends Model
|
||||
|
||||
@grammar = grammar
|
||||
@rootScopeDescriptor = new ScopeDescriptor(scopes: [@grammar.scopeName])
|
||||
@currentGrammarScore = score ? grammar.getScore(@buffer.getPath(), @getGrammarSelectionContent())
|
||||
@currentGrammarScore = score ? atom.grammars.getGrammarScore(grammar, @buffer.getPath(), @getGrammarSelectionContent())
|
||||
|
||||
@grammarUpdateDisposable?.dispose()
|
||||
@grammarUpdateDisposable = @grammar.onDidUpdate => @retokenizeLines()
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
###
|
||||
Cache for source code transpiled by TypeScript.
|
||||
|
||||
Inspired by https://github.com/atom/atom/blob/7a719d585db96ff7d2977db9067e1d9d4d0adf1a/src/babel.coffee
|
||||
###
|
||||
|
||||
crypto = require 'crypto'
|
||||
fs = require 'fs-plus'
|
||||
path = require 'path'
|
||||
tss = null # Defer until used
|
||||
|
||||
stats =
|
||||
hits: 0
|
||||
misses: 0
|
||||
|
||||
defaultOptions =
|
||||
target: 1 # ES5
|
||||
module: 'commonjs'
|
||||
sourceMap: true
|
||||
|
||||
createTypeScriptVersionAndOptionsDigest = (version, options) ->
|
||||
shasum = crypto.createHash('sha1')
|
||||
# Include the version of typescript in the hash.
|
||||
shasum.update('typescript', 'utf8')
|
||||
shasum.update('\0', 'utf8')
|
||||
shasum.update(version, 'utf8')
|
||||
shasum.update('\0', 'utf8')
|
||||
shasum.update(JSON.stringify(options))
|
||||
shasum.digest('hex')
|
||||
|
||||
cacheDir = null
|
||||
jsCacheDir = null
|
||||
|
||||
getCachePath = (sourceCode) ->
|
||||
digest = crypto.createHash('sha1').update(sourceCode, 'utf8').digest('hex')
|
||||
|
||||
unless jsCacheDir?
|
||||
tssVersion = require('typescript-simple/package.json').version
|
||||
jsCacheDir = path.join(cacheDir, createTypeScriptVersionAndOptionsDigest(tssVersion, defaultOptions))
|
||||
|
||||
path.join(jsCacheDir, "#{digest}.js")
|
||||
|
||||
getCachedJavaScript = (cachePath) ->
|
||||
if fs.isFileSync(cachePath)
|
||||
try
|
||||
cachedJavaScript = fs.readFileSync(cachePath, 'utf8')
|
||||
stats.hits++
|
||||
return cachedJavaScript
|
||||
null
|
||||
|
||||
# Returns the TypeScript options that should be used to transpile filePath.
|
||||
createOptions = (filePath) ->
|
||||
options = filename: filePath
|
||||
for key, value of defaultOptions
|
||||
options[key] = value
|
||||
options
|
||||
|
||||
transpile = (sourceCode, filePath, cachePath) ->
|
||||
options = createOptions(filePath)
|
||||
unless tss?
|
||||
{TypeScriptSimple} = require 'typescript-simple'
|
||||
tss = new TypeScriptSimple(options, false)
|
||||
js = tss.compile(sourceCode, filePath)
|
||||
stats.misses++
|
||||
|
||||
try
|
||||
fs.writeFileSync(cachePath, js)
|
||||
|
||||
js
|
||||
|
||||
# Function that obeys the contract of an entry in the require.extensions map.
|
||||
# Returns the transpiled version of the JavaScript code at filePath, which is
|
||||
# either generated on the fly or pulled from cache.
|
||||
loadFile = (module, filePath) ->
|
||||
sourceCode = fs.readFileSync(filePath, 'utf8')
|
||||
cachePath = getCachePath(sourceCode)
|
||||
js = getCachedJavaScript(cachePath) ? transpile(sourceCode, filePath, cachePath)
|
||||
module._compile(js, filePath)
|
||||
|
||||
register = ->
|
||||
Object.defineProperty(require.extensions, '.ts', {
|
||||
enumerable: true
|
||||
writable: false
|
||||
value: loadFile
|
||||
})
|
||||
|
||||
setCacheDirectory = (newCacheDir) ->
|
||||
if cacheDir isnt newCacheDir
|
||||
cacheDir = newCacheDir
|
||||
jsCacheDir = null
|
||||
|
||||
module.exports =
|
||||
register: register
|
||||
setCacheDirectory: setCacheDirectory
|
||||
getCacheMisses: -> stats.misses
|
||||
getCacheHits: -> stats.hits
|
||||
|
||||
# Visible for testing.
|
||||
createTypeScriptVersionAndOptionsDigest: createTypeScriptVersionAndOptionsDigest
|
||||
|
||||
addPathToCache: (filePath) ->
|
||||
return if path.extname(filePath) isnt '.ts'
|
||||
|
||||
sourceCode = fs.readFileSync(filePath, 'utf8')
|
||||
cachePath = getCachePath(sourceCode)
|
||||
transpile(sourceCode, filePath, cachePath)
|
||||
53
src/typescript.js
Normal file
53
src/typescript.js
Normal file
@@ -0,0 +1,53 @@
|
||||
'use strict'
|
||||
|
||||
var _ = require('underscore-plus')
|
||||
var crypto = require('crypto')
|
||||
var path = require('path')
|
||||
|
||||
var defaultOptions = {
|
||||
target: 1,
|
||||
module: 'commonjs',
|
||||
sourceMap: true
|
||||
}
|
||||
|
||||
var TypeScriptSimple = null
|
||||
var typescriptVersionDir = null
|
||||
|
||||
exports.shouldCompile = function () {
|
||||
return true
|
||||
}
|
||||
|
||||
exports.getCachePath = function (sourceCode) {
|
||||
if (typescriptVersionDir == null) {
|
||||
var version = require('typescript-simple/package.json').version
|
||||
typescriptVersionDir = path.join('ts', createVersionAndOptionsDigest(version, defaultOptions))
|
||||
}
|
||||
|
||||
return path.join(
|
||||
typescriptVersionDir,
|
||||
crypto
|
||||
.createHash('sha1')
|
||||
.update(sourceCode, 'utf8')
|
||||
.digest('hex') + '.js'
|
||||
)
|
||||
}
|
||||
|
||||
exports.compile = function (sourceCode, filePath) {
|
||||
if (!TypeScriptSimple) {
|
||||
TypeScriptSimple = require('typescript-simple').TypeScriptSimple
|
||||
}
|
||||
|
||||
var options = _.defaults({filename: filePath}, defaultOptions)
|
||||
return new TypeScriptSimple(options, false).compile(sourceCode, filePath)
|
||||
}
|
||||
|
||||
function createVersionAndOptionsDigest (version, options) {
|
||||
return crypto
|
||||
.createHash('sha1')
|
||||
.update('typescript', 'utf8')
|
||||
.update('\0', 'utf8')
|
||||
.update(version, 'utf8')
|
||||
.update('\0', 'utf8')
|
||||
.update(JSON.stringify(options), 'utf8')
|
||||
.digest('hex')
|
||||
}
|
||||
@@ -124,6 +124,22 @@ class ViewRegistry
|
||||
# * `object` The object for which you want to retrieve a view. This can be a
|
||||
# pane item, a pane, or the workspace itself.
|
||||
#
|
||||
# ## View Resolution Algorithm
|
||||
#
|
||||
# The view associated with the object is resolved using the following
|
||||
# sequence
|
||||
#
|
||||
# 1. Is the object an instance of `HTMLElement`? If true, return the object.
|
||||
# 2. Does the object have a property named `element` with a value which is
|
||||
# an instance of `HTMLElement`? If true, return the property value.
|
||||
# 3. Is the object a jQuery object, indicated by the presence of a `jquery`
|
||||
# property? If true, return the root DOM element (i.e. `object[0]`).
|
||||
# 4. Has a view provider been registered for the object? If true, use the
|
||||
# provider to create a view associated with the object, and return the
|
||||
# view.
|
||||
#
|
||||
# If no associated view is returned by the sequence an error is thrown.
|
||||
#
|
||||
# Returns a DOM element.
|
||||
getView: (object) ->
|
||||
return unless object?
|
||||
@@ -138,6 +154,8 @@ class ViewRegistry
|
||||
createView: (object) ->
|
||||
if object instanceof HTMLElement
|
||||
object
|
||||
else if object?.element instanceof HTMLElement
|
||||
object.element
|
||||
else if object?.jquery
|
||||
object[0]
|
||||
else if provider = @findProvider(object)
|
||||
|
||||
@@ -89,6 +89,10 @@ class WindowEventHandler
|
||||
@subscribeToCommand $(window), 'window:toggle-menu-bar', ->
|
||||
atom.config.set('core.autoHideMenuBar', not atom.config.get('core.autoHideMenuBar'))
|
||||
|
||||
if atom.config.get('core.autoHideMenuBar')
|
||||
detail = "To toggle, press the Alt key or execute the window:toggle-menu-bar command"
|
||||
atom.notifications.addInfo('Menu bar hidden', {detail})
|
||||
|
||||
@subscribeToCommand $(document), 'core:focus-next', @focusNext
|
||||
|
||||
@subscribeToCommand $(document), 'core:focus-previous', @focusPrevious
|
||||
|
||||
@@ -779,9 +779,9 @@ class Workspace extends Model
|
||||
# Essential: Adds a panel item as a modal dialog.
|
||||
#
|
||||
# * `options` {Object}
|
||||
# * `item` Your panel content. It can be DOM element, a jQuery element, or
|
||||
# * `item` Your panel content. It can be a DOM element, a jQuery element, or
|
||||
# a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the
|
||||
# latter. See {ViewRegistry::addViewProvider} for more information.
|
||||
# model option. See {ViewRegistry::addViewProvider} for more information.
|
||||
# * `visible` (optional) {Boolean} false if you want the panel to initially be hidden
|
||||
# (default: true)
|
||||
# * `priority` (optional) {Number} Determines stacking order. Lower priority items are
|
||||
|
||||
7
static/babelrc.json
Normal file
7
static/babelrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"breakConfig": true,
|
||||
"sourceMap": "inline",
|
||||
"blacklist": ["es6.forOf", "useStrict"],
|
||||
"optional": ["asyncToGenerator"],
|
||||
"stage": 0
|
||||
}
|
||||
403
static/index.js
403
static/index.js
@@ -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()
|
||||
})()
|
||||
|
||||
Reference in New Issue
Block a user