Merge branch 'master' into ns-use-display-layers

# Conflicts:
#	package.json
#	src/display-buffer.coffee
#	src/text-editor.coffee
#	src/tokenized-buffer.coffee
This commit is contained in:
Antonio Scandurra
2016-03-10 13:53:14 +01:00
114 changed files with 2475 additions and 1047 deletions

1
.gitignore vendored
View File

@@ -15,3 +15,4 @@ debug.log
docs/output
docs/includes
spec/fixtures/evil-files/
out/

View File

@@ -365,7 +365,7 @@ Please open an issue on `atom/atom` if you have suggestions for new labels, and
| `blocked` | [search][search-atom-repo-label-blocked] | [search][search-atom-org-label-blocked] | Issues blocked on other issues. |
| `duplicate` | [search][search-atom-repo-label-duplicate] | [search][search-atom-org-label-duplicate] | Issues which are duplicates of other issues, i.e. they have been reported before. |
| `wontfix` | [search][search-atom-repo-label-wontfix] | [search][search-atom-org-label-wontfix] | The Atom core team has decided not to fix these issues for now, either because they're working as intended or for some other reason. |
| `invalid` | [search][search-atom-repo-label-invalid] | [search][search-atom-org-label-invalid] | Issues which are't valid (e.g. user errors). |
| `invalid` | [search][search-atom-repo-label-invalid] | [search][search-atom-org-label-invalid] | Issues which aren't valid (e.g. user errors). |
| `package-idea` | [search][search-atom-repo-label-package-idea] | [search][search-atom-org-label-package-idea] | Feature request which might be good candidates for new packages, instead of extending Atom or core Atom packages. |
| `wrong-repo` | [search][search-atom-repo-label-wrong-repo] | [search][search-atom-org-label-wrong-repo] | Issues reported on the wrong repository (e.g. a bug related to the [Settings View package](https://github.com/atom/settings-view) was reported on [Atom core](https://github.com/atom/atom)). |
@@ -406,10 +406,6 @@ Please open an issue on `atom/atom` if you have suggestions for new labels, and
| Label name | `atom/atom` :mag_right: | `atom`org :mag_right: | Description |
| --- | --- | --- | --- |
| `in-progress` | [search][search-atom-repo-label-in-progress] | [search][search-atom-org-label-in-progress] | Tasks which the Atom core team is working on currently. |
| `on-deck` | [search][search-atom-repo-label-on-deck] | [search][search-atom-org-label-on-deck] | Tasks which the Atom core team plans to work on next. |
| `shipping` | [search][search-atom-repo-label-shipping] | [search][search-atom-org-label-shipping] | Tasks which the Atom core team completed and will be released in one of the next releases. |
| `post-1.0-roadmap` | [search][search-atom-repo-label-post-1.0-roadmap] | [search][search-atom-org-label-post-1.0-roadmap] | The Atom core team's roadmap post version 1.0.0. |
| `atom` | [search][search-atom-repo-label-atom] | [search][search-atom-org-label-atom] | Topics discussed for prioritization at the next meeting of Atom core team members. |
#### Pull Request Labels
@@ -498,14 +494,6 @@ Please open an issue on `atom/atom` if you have suggestions for new labels, and
[search-atom-org-label-deprecation-help]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Adeprecation-help
[search-atom-repo-label-electron]: https://github.com/issues?q=is%3Aissue+repo%3Aatom%2Fatom+is%3Aopen+label%3Aelectron
[search-atom-org-label-electron]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aelectron
[search-atom-repo-label-on-deck]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aon-deck
[search-atom-org-label-on-deck]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aon-deck
[search-atom-repo-label-in-progress]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Ain-progress
[search-atom-org-label-in-progress]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Ain-progress
[search-atom-repo-label-shipping]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Ashipping
[search-atom-org-label-shipping]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Ashipping
[search-atom-repo-label-post-1.0-roadmap]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Apost-1.0-roadmap
[search-atom-org-label-post-1.0-roadmap]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Apost-1.0-roadmap
[search-atom-repo-label-atom]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aatom
[search-atom-org-label-atom]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aatom
[search-atom-repo-label-work-in-progress]: https://github.com/pulls?q=is%3Aopen+is%3Apr+repo%3Aatom%2Fatom+label%3Awork-in-progress

28
ISSUE_TEMPLATE.md Normal file
View File

@@ -0,0 +1,28 @@
### Prerequisites
* [ ] Can you reproduce the problem in [safe mode](https://atom.io/docs/latest/hacking-atom-debugging#check-if-the-problem-shows-up-in-safe-mode)?
* [ ] Are you running the [latest version of Atom](https://atom.io/docs/latest/hacking-atom-debugging#update-to-the-latest-version)?
* [ ] Did you check the [debugging guide](https://atom.io/docs/latest/hacking-atom-debugging)?
* [ ] Did you check the [FAQs on Discuss](https://discuss.atom.io/c/faq)?
* [ ] Are you reporting to the [correct repository](https://github.com/atom/atom/blob/master/CONTRIBUTING.md#atom-and-packages)?
* [ ] Did you [perform a cursory search](https://github.com/issues?q=is%3Aissue+user%3Aatom+-repo%3Aatom%2Felectron) to see if your bug or enhancement is already reported?
For more information on how to write a good [bug report](https://github.com/atom/atom/blob/master/CONTRIBUTING.md#how-do-i-submit-a-good-bug-report) or [enhancement request](https://github.com/atom/atom/blob/master/CONTRIBUTING.md#how-do-i-submit-a-good-enhancement-suggestion), see the `CONTRIBUTING` guide.
### Description
[Description of the bug or feature]
### Steps to Reproduce
1. [First Step]
2. [Second Step]
3. [and so on...]
**Expected behavior:** [What you expected to happen]
**Actual behavior:** [What actually happened]
### Versions
You can get this information from executing `atom --version` and `apm --version` at the command line.

View File

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

View File

@@ -4,8 +4,6 @@ if [ "$(uname)" == 'Darwin' ]; then
OS='Mac'
elif [ "$(expr substr $(uname -s) 1 5)" == 'Linux' ]; then
OS='Linux'
elif [ "$(expr substr $(uname -s) 1 10)" == 'MINGW32_NT' ]; then
OS='Cygwin'
else
echo "Your platform ($(uname -a)) is not supported."
exit 1

View File

@@ -34,23 +34,10 @@ module.exports = (grunt) ->
grunt.file.setBase(path.resolve('..'))
# Options
[defaultChannel, releaseBranch] = getDefaultChannelAndReleaseBranch(packageJson.version)
installDir = grunt.option('install-dir')
buildDir = grunt.option('build-dir')
buildDir ?= 'out'
buildDir = path.resolve(buildDir)
channel = grunt.option('channel')
releasableBranches = ['stable', 'beta']
if process.env.APPVEYOR and not process.env.APPVEYOR_PULL_REQUEST_NUMBER
channel ?= process.env.APPVEYOR_REPO_BRANCH if process.env.APPVEYOR_REPO_BRANCH in releasableBranches
if process.env.TRAVIS and not process.env.TRAVIS_PULL_REQUEST
channel ?= process.env.TRAVIS_BRANCH if process.env.TRAVIS_BRANCH in releasableBranches
if process.env.JANKY_BRANCH
channel ?= process.env.JANKY_BRANCH if process.env.JANKY_BRANCH in releasableBranches
channel ?= 'dev'
buildDir = path.resolve(grunt.option('build-dir') ? 'out')
channel = grunt.option('channel') ? defaultChannel
metadata = packageJson
appName = packageJson.productName
@@ -189,7 +176,7 @@ module.exports = (grunt) ->
pkg: grunt.file.readJSON('package.json')
atom: {
appName, channel, metadata,
appName, channel, metadata, releaseBranch,
appFileName, apmFileName,
appDir, buildDir, contentsDir, installDir, shellAppDir, symbolsDir,
}
@@ -310,3 +297,20 @@ module.exports = (grunt) ->
unless process.platform is 'linux' or grunt.option('no-install')
defaultTasks.push 'install'
grunt.registerTask('default', defaultTasks)
getDefaultChannelAndReleaseBranch = (version) ->
if version.match(/dev/) or isBuildingPR()
channel = 'dev'
releaseBranch = null
else
if version.match(/beta/)
channel = 'beta'
else
channel = 'stable'
minorVersion = version.match(/^\d\.\d/)[0]
releaseBranch = "#{minorVersion}-releases"
[channel, releaseBranch]
isBuildingPR = ->
process.env.APPVEYOR_PULL_REQUEST_NUMBER? or process.env.TRAVIS_PULL_REQUEST?

View File

@@ -25,7 +25,7 @@
"grunt-contrib-less": "~0.8.0",
"grunt-cson": "0.16.0",
"grunt-download-electron": "^2.1.1",
"grunt-electron-installer": "1.0.6",
"grunt-electron-installer": "1.2.2",
"grunt-lesslint": "0.17.0",
"grunt-peg": "~1.1.0",
"grunt-shell": "~0.3.1",

View File

@@ -31,14 +31,9 @@ module.exports = (gruntObject) ->
cp path.join(docsOutputDir, 'api.json'), path.join(buildDir, 'atom-api.json')
grunt.registerTask 'upload-assets', 'Upload the assets to a GitHub release', ->
channel = grunt.config.get('atom.channel')
switch channel
when 'stable'
isPrerelease = false
when 'beta'
isPrerelease = true
else
return
releaseBranch = grunt.config.get('atom.releaseBranch')
isPrerelease = grunt.config.get('atom.channel') is 'beta'
return unless releaseBranch?
doneCallback = @async()
startTime = Date.now()
@@ -55,7 +50,7 @@ module.exports = (gruntObject) ->
zipAssets buildDir, assets, (error) ->
return done(error) if error?
getAtomDraftRelease isPrerelease, channel, (error, release) ->
getAtomDraftRelease isPrerelease, releaseBranch, (error, release) ->
return done(error) if error?
assetNames = (asset.assetName for asset in assets)
deleteExistingAssets release, assetNames, (error) ->

View File

@@ -5,9 +5,7 @@ module.exports = (grunt) ->
{spawn} = require('./task-helpers')(grunt)
getVersion = (callback) ->
releasableBranches = ['stable', 'beta']
channel = grunt.config.get('atom.channel')
shouldUseCommitHash = if channel in releasableBranches then false else true
shouldUseCommitHash = grunt.config.get('atom.channel') is 'dev'
inRepository = fs.existsSync(path.resolve(__dirname, '..', '..', '.git'))
{version} = require(path.join(grunt.config.get('atom.appDir'), 'package.json'))
if shouldUseCommitHash and inRepository

View File

@@ -1,5 +1,6 @@
fs = require 'fs'
path = require 'path'
temp = require('temp').track()
_ = require 'underscore-plus'
async = require 'async'
@@ -86,7 +87,7 @@ module.exports = (grunt) ->
packageSpecQueue.concurrency = Math.max(1, concurrency - 1)
packageSpecQueue.drain = -> callback(null, failedPackages)
runCoreSpecs = (callback) ->
runCoreSpecs = (callback, logOutput = false) ->
appPath = getAppPath()
resourcePath = process.cwd()
coreSpecsPath = path.resolve('spec')
@@ -94,7 +95,7 @@ module.exports = (grunt) ->
if process.platform in ['darwin', 'linux']
options =
cmd: appPath
args: ['--test', "--resource-path=#{resourcePath}", coreSpecsPath]
args: ['--test', "--resource-path=#{resourcePath}", coreSpecsPath, "--user-data-dir=#{temp.mkdirSync('atom-user-data-dir')}"]
opts:
env: _.extend({}, process.env,
ATOM_INTEGRATION_TESTS_ENABLED: true
@@ -109,6 +110,9 @@ module.exports = (grunt) ->
ATOM_INTEGRATION_TESTS_ENABLED: true
)
if logOutput
options.opts.stdio = 'inherit'
grunt.log.ok "Launching core specs."
spawn options, (error, results, code) ->
if process.platform is 'win32'
@@ -130,11 +134,17 @@ module.exports = (grunt) ->
else
async.parallel
# If we're just running the core specs then we won't have any output to
# indicate the tests actually *are* running. This upsets Travis:
# https://github.com/atom/atom/issues/10837. So pass the test output
# through.
runCoreSpecsWithLogging = (callback) -> runCoreSpecs(callback, true)
specs =
if process.env.ATOM_SPECS_TASK is 'packages'
[runPackageSpecs]
else if process.env.ATOM_SPECS_TASK is 'core'
[runCoreSpecs]
[runCoreSpecsWithLogging]
else
[runCoreSpecs, runPackageSpecs]

View File

@@ -52,8 +52,10 @@ module.exports = (grunt) ->
stderr = []
error = null
proc = childProcess.spawn(options.cmd, options.args, options.opts)
proc.stdout.on 'data', (data) -> stdout.push(data.toString())
proc.stderr.on 'data', (data) -> stderr.push(data.toString())
if proc.stdout?
proc.stdout.on 'data', (data) -> stdout.push(data.toString())
if proc.stderr?
proc.stderr.on 'data', (data) -> stderr.push(data.toString())
proc.on 'error', (processError) -> error ?= processError
proc.on 'close', (exitCode, signal) ->
error ?= new Error(signal) if exitCode isnt 0

4
circle.yml Normal file
View File

@@ -0,0 +1,4 @@
general:
branches:
only:
- io-circle-ci

View File

@@ -64,7 +64,7 @@ If you have problems with permissions don't forget to prefix with `sudo`
script/build
```
This will create the atom application at `$TMPDIR/atom-build/Atom`.
This will create the atom application at `out/Atom`.
4. Install the `atom` and `apm` commands to `/usr/local/bin` by executing:
@@ -74,7 +74,7 @@ If you have problems with permissions don't forget to prefix with `sudo`
To use the newly installed Atom, quit and restart all running Atom instances.
5. *Optionally*, you may generate distributable packages of Atom at `$TMPDIR/atom-build`. Currently, `.deb` and `.rpm` package types are supported. To create a `.deb` package run:
5. *Optionally*, you may generate distributable packages of Atom at `out`. Currently, `.deb` and `.rpm` package types are supported. To create a `.deb` package run:
```sh
script/grunt mkdeb

View File

@@ -3,32 +3,40 @@
## Requirements
### General
* [Node.js](http://nodejs.org/en/download/) v4.x
* [Python](https://www.python.org/downloads/) v2.7.x
* [Node.js](http://nodejs.org/en/download/) v4.x
* [Python](https://www.python.org/downloads/) v2.7.x
* The python.exe must be available at `%SystemDrive%\Python27\python.exe`.
If it is installed elsewhere, you can create a symbolic link to the
directory containing the python.exe using:
`mklink /d %SystemDrive%\Python27 D:\elsewhere\Python27`
* [GitHub Desktop](http://desktop.github.com/)
### On Windows 7
* [Visual Studio 2013 Update 5](http://www.visualstudio.com/en-us/downloads/download-visual-studio-vs#DownloadFamilies_4)
### Visual Studio
### 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)
* To ensure that node-gyp knows what version of Visual Studio is installed, set the `GYP_MSVS_VERSION` environment variable to the Visual Studio version (e.g. `2013` or `2015`)
You can use either:
* [Visual Studio 2013 Update 5](http://www.visualstudio.com/en-us/downloads/download-visual-studio-vs) (Express or better) on Windows 7, 8 or 10
* [Visual Studio 2015](http://www.visualstudio.com/en-us/downloads/download-visual-studio-vs) (Community or better) with Windows 8 or 10
Whichever version you use, ensure that:
* The default installation folder is chosen so the build tools can find it
* Visual C++ support is installed
* You set the `GYP_MSVS_VERSION` environment variable to the Visual Studio version (`2013` or `2015`), e.g. , e.g. ``[Environment]::SetEnvironmentVariable("GYP_MSVS_VERSION", "2015", "User")`` in PowerShell or set it in Windows advanced system settings control panel.
* The git command is in your path
## Instructions
You can run these commands using Command Prompt, PowerShell or Git Shell via [GitHub Desktop](https://desktop.github.com/). These instructions will assume the use of Bash from Git Shell - if you are using Command Prompt use a backslash instead: i.e. `script\build`.
**VS2015 + Git Shell users** should note that the default path supplied with Git Shell includes reference to an older version of msbuild that will fail. It is recommended you use a PowerShell window that has git in the path at this time.
```bash
# Use the Git Shell program which was installed by GitHub Desktop
cd C:\
git clone https://github.com/atom/atom/
cd atom
script/build # Creates application in the `Program Files` directory
script/build
```
Note: If you use cmd or Powershell instead of Git Shell, use a backslash instead: i.e. `script\build`.
These instructions will assume the use of Git Shell.
This will create the Atom application in the `Program Files` folder.
### `script/build` Options
* `--install-dir` - Creates the final built application in this directory. Example (trailing slash is optional):
@@ -41,24 +49,25 @@ These instructions will assume the use of Git Shell.
```
* `--verbose` - Verbose mode. A lot more information output.
## Why do I have to use GitHub Desktop?
## Do I have to use GitHub Desktop?
You don't. You can use your existing Git! GitHub Desktop's Git Shell is just easier to set up.
No, you can use your existing Git! GitHub Desktop's Git Shell is just easier to set up.
If you _prefer_ using your existing Git installation, make sure git's cmd directory is in your PATH env variable (e.g. `C:\Program Files (x86)\Git\cmd`) before you open your powershell or command window.
Note that you may have to open your command window as administrator. For powershell that doesn't seem to always be the case, though.
If you _prefer_ using your existing Git installation, make sure git's cmd directory is in your PATH env variable (e.g. `C:\Program Files (x86)\Git\cmd`) before you open your PowerShell or Command Prompt.
If none of this works, do install Github Desktop and use its Git shell. Makes life easier.
It is also recommended you open your Command Prompt or PowerShell as Administrator.
If none of this works, do install Github Desktop and use its Git Shell as it makes life easier.
## Troubleshooting
### Common Errors
* `node is not recognized`
* If you just installed node, you'll need to restart your computer before node is
available on your Path.
* If you just installed Node.js, you'll need to restart your PowerShell/Command Prompt/Git Shell before the node
command is available on your Path.
* `script/build` outputs only the Node and Python versions before returning
* `script/build` outputs only the Node.js and Python versions before returning
* Try moving the repository to `C:\atom`. Most likely, the path is too long.
See [issue #2200](https://github.com/atom/atom/issues/2200).
@@ -67,7 +76,7 @@ If none of this works, do install Github Desktop and use its Git shell. Makes li
* This can occur because your home directory (`%USERPROFILE%`) has non-ASCII
characters in it. This is a bug in [gyp](https://code.google.com/p/gyp/)
which is used to build native node modules and there is no known workaround.
which is used to build native Node.js modules and there is no known workaround.
* https://github.com/TooTallNate/node-gyp/issues/297
* https://code.google.com/p/gyp/issues/detail?id=393
@@ -75,19 +84,20 @@ If none of this works, do install Github Desktop and use its Git shell. Makes li
* See the next item.
* `error MSB8020: The build tools for Visual Studio 2010 (Platform Toolset = 'v100') cannot be found.`
* `error MSB8020: The build tools for Visual Studio 201? (Platform Toolset = 'v1?0') cannot be found.`
* If you're building Atom with Visual Studio 2013 or above make sure the `GYP_MSVS_VERSION` environment variable is set, and then re-run `script/build`:
* If you're building Atom with Visual Studio 2013 or above make sure the `GYP_MSVS_VERSION` environment variable is set, and then re-run `script/build` after a clean:
```bash
$env:GYP_MSVS_VERSION='2013' # '2015' if using Visual Studio 2015, and so on
script/clean
script/build
```
* If you are using Visual Studio 2013 or above and the build fails with some other error message this environment variable might still be required.
* If you are using Visual Studio 2013 or above and the build fails with some other error message this environment variable might still be required and ensure you have Visual C++ language support installed.
* Other `node-gyp` errors on first build attempt, even though the right node and python versions are installed.
* Other `node-gyp` errors on first build attempt, even though the right Node.js and Python versions are installed.
* Do try the build command one more time, as experience shows it often works on second try in many of these cases.
### Windows build error reports in atom/atom
* If all fails, use [this search](https://github.com/atom/atom/search?q=label%3Abuild-error+label%3Awindows&type=Issues) to get a list of reports about build errors on Windows, and see if yours has already been reported.
* If it hasn't, please open a new issue with your Windows version, architecture (x86 or amd64), and a screenshot of your build output, including the Node and Python versions.
* If it hasn't, please open a new issue with your Windows version, architecture (x86 or amd64), and a screenshot of your build output, including the Node.js and Python versions.

View File

@@ -73,8 +73,10 @@
'cmd-alt-right': 'pane:show-next-item'
'ctrl-pageup': 'pane:show-previous-item'
'ctrl-pagedown': 'pane:show-next-item'
'ctrl-tab': 'pane:show-next-item'
'ctrl-shift-tab': 'pane:show-previous-item'
'ctrl-tab': 'pane:show-next-recently-used-item'
'ctrl-tab ^ctrl': 'pane:move-active-item-to-top-of-stack'
'ctrl-shift-tab': 'pane:show-previous-recently-used-item'
'ctrl-shift-tab ^ctrl': 'pane:move-active-item-to-top-of-stack'
'cmd-=': 'window:increase-font-size'
'cmd-+': 'window:increase-font-size'
'cmd--': 'window:decrease-font-size'

View File

@@ -46,8 +46,10 @@
'pagedown': 'core:page-down'
'backspace': 'core:backspace'
'shift-backspace': 'core:backspace'
'ctrl-tab': 'pane:show-next-item'
'ctrl-shift-tab': 'pane:show-previous-item'
'ctrl-tab': 'pane:show-next-recently-used-item'
'ctrl-tab ^ctrl': 'pane:move-active-item-to-top-of-stack'
'ctrl-shift-tab': 'pane:show-previous-recently-used-item'
'ctrl-shift-tab ^ctrl': 'pane:move-active-item-to-top-of-stack'
'ctrl-pageup': 'pane:show-previous-item'
'ctrl-pagedown': 'pane:show-next-item'
'ctrl-up': 'core:move-up'

View File

@@ -52,8 +52,10 @@
'pagedown': 'core:page-down'
'backspace': 'core:backspace'
'shift-backspace': 'core:backspace'
'ctrl-tab': 'pane:show-next-item'
'ctrl-shift-tab': 'pane:show-previous-item'
'ctrl-tab': 'pane:show-next-recently-used-item'
'ctrl-tab ^ctrl': 'pane:move-active-item-to-top-of-stack'
'ctrl-shift-tab': 'pane:show-previous-recently-used-item'
'ctrl-shift-tab ^ctrl': 'pane:move-active-item-to-top-of-stack'
'ctrl-pageup': 'pane:show-previous-item'
'ctrl-pagedown': 'pane:show-next-item'
'ctrl-shift-up': 'core:move-up'

View File

@@ -200,7 +200,6 @@
submenu: [
{ label: 'Terms of Use', command: 'application:open-terms-of-use' }
{ label: 'Documentation', command: 'application:open-documentation' }
{ label: 'Roadmap', command: 'application:open-roadmap' }
{ label: 'Frequently Asked Questions', command: 'application:open-faq' }
{ type: 'separator' }
{ label: 'Community Discussions', command: 'application:open-discussions' }

View File

@@ -174,7 +174,6 @@
{ label: "VERSION", enabled: false }
{ type: 'separator' }
{ label: '&Documentation', command: 'application:open-documentation' }
{ label: 'Roadmap', command: 'application:open-roadmap' }
{ label: 'Frequently Asked Questions', command: 'application:open-faq' }
{ type: 'separator' }
{ label: 'Community Discussions', command: 'application:open-discussions' }

View File

@@ -177,7 +177,6 @@
{ label: 'Downloading Update', enabled: false, visible: false}
{ type: 'separator' }
{ label: '&Documentation', command: 'application:open-documentation' }
{ label: 'Roadmap', command: 'application:open-roadmap' }
{ label: 'Frequently Asked Questions', command: 'application:open-faq' }
{ type: 'separator' }
{ label: 'Community Discussions', command: 'application:open-discussions' }

View File

@@ -12,10 +12,10 @@
"url": "https://github.com/atom/atom/issues"
},
"license": "MIT",
"electronVersion": "0.34.5",
"electronVersion": "0.36.8",
"dependencies": {
"async": "0.2.6",
"atom-keymap": "^6.2.0",
"atom-keymap": "^6.3.1",
"babel-core": "^5.8.21",
"bootstrap": "^3.3.4",
"cached-run-in-this-context": "0.4.1",
@@ -28,7 +28,7 @@
"fs-plus": "^2.8.0",
"fstream": "0.1.24",
"fuzzaldrin": "^2.1",
"git-utils": "^4.1.0",
"git-utils": "^4.1.2",
"grim": "1.5.0",
"jasmine-json": "~0.0",
"jasmine-tagged": "^1.1.4",
@@ -38,7 +38,7 @@
"line-top-index": "0.2.0",
"marked": "^0.3.4",
"marker-index": "^3.0.4",
"nodegit": "0.9.0",
"nodegit": "0.11.9",
"normalize-package-data": "^2.0.0",
"nslog": "^3",
"oniguruma": "^5",
@@ -55,7 +55,7 @@
"service-hub": "^0.7.0",
"source-map-support": "^0.3.2",
"temp": "0.8.1",
"text-buffer": "8.2.1",
"text-buffer": "8.4.1",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.6",
"yargs": "^3.23.0"
@@ -67,70 +67,70 @@
"atom-light-ui": "0.43.0",
"base16-tomorrow-dark-theme": "1.1.0",
"base16-tomorrow-light-theme": "1.1.1",
"one-dark-ui": "1.1.9",
"one-light-ui": "1.1.9",
"one-dark-ui": "1.2.0",
"one-light-ui": "1.2.0",
"one-dark-syntax": "1.2.0",
"one-light-syntax": "1.2.0",
"solarized-dark-syntax": "1.0.0",
"solarized-light-syntax": "1.0.0",
"about": "1.3.0",
"archive-view": "0.61.0",
"about": "1.4.0",
"archive-view": "0.61.1",
"autocomplete-atom-api": "0.10.0",
"autocomplete-css": "0.11.0",
"autocomplete-html": "0.7.2",
"autocomplete-plus": "2.25.0",
"autocomplete-plus": "2.29.1",
"autocomplete-snippets": "1.10.0",
"autoflow": "0.27.0",
"autosave": "0.23.0",
"autosave": "0.23.1",
"background-tips": "0.26.0",
"bookmarks": "0.38.2",
"bracket-matcher": "0.79.0",
"bracket-matcher": "0.81.0",
"command-palette": "0.38.0",
"deprecation-cop": "0.54.0",
"deprecation-cop": "0.54.1",
"dev-live-reload": "0.47.0",
"encoding-selector": "0.21.0",
"exception-reporting": "0.37.0",
"find-and-replace": "0.197.1",
"fuzzy-finder": "0.94.0",
"git-diff": "0.57.0",
"find-and-replace": "0.197.4",
"fuzzy-finder": "1.0.3",
"git-diff": "1.0.0",
"go-to-line": "0.30.0",
"grammar-selector": "0.48.0",
"image-view": "0.56.0",
"incompatible-packages": "0.25.0",
"keybinding-resolver": "0.33.0",
"line-ending-selector": "0.3.0",
"grammar-selector": "0.48.1",
"image-view": "0.57.0",
"incompatible-packages": "0.25.1",
"keybinding-resolver": "0.35.0",
"line-ending-selector": "0.3.1",
"link": "0.31.0",
"markdown-preview": "0.157.2",
"markdown-preview": "0.157.3",
"metrics": "0.53.1",
"notifications": "0.62.1",
"open-on-github": "0.41.0",
"package-generator": "0.41.0",
"settings-view": "0.232.3",
"notifications": "0.62.3",
"open-on-github": "1.0.0",
"package-generator": "0.41.1",
"settings-view": "0.232.4",
"snippets": "1.0.1",
"spell-check": "0.65.0",
"status-bar": "0.83.0",
"styleguide": "0.45.1",
"symbols-view": "0.110.1",
"tabs": "0.90.0",
"timecop": "0.33.0",
"tree-view": "0.201.0",
"spell-check": "0.67.0",
"status-bar": "1.1.0",
"styleguide": "0.45.2",
"symbols-view": "0.112.0",
"tabs": "0.91.3",
"timecop": "0.33.1",
"tree-view": "0.203.0",
"update-package-dependencies": "0.10.0",
"welcome": "0.33.0",
"whitespace": "0.32.1",
"welcome": "0.34.0",
"whitespace": "0.32.2",
"wrap-guide": "0.38.1",
"language-c": "0.51.1",
"language-clojure": "0.19.1",
"language-coffee-script": "0.46.0",
"language-coffee-script": "0.46.1",
"language-csharp": "0.11.0",
"language-css": "0.36.0",
"language-gfm": "0.84.0",
"language-gfm": "0.85.0",
"language-git": "0.12.1",
"language-go": "0.42.0",
"language-html": "0.44.0",
"language-html": "0.44.1",
"language-hyperlink": "0.16.0",
"language-java": "0.17.0",
"language-javascript": "0.110.0",
"language-json": "0.17.4",
"language-json": "0.17.5",
"language-less": "0.29.0",
"language-make": "0.21.0",
"language-mustache": "0.13.0",
@@ -139,16 +139,16 @@
"language-php": "0.37.0",
"language-property-list": "0.8.0",
"language-python": "0.43.0",
"language-ruby": "0.68.0",
"language-ruby": "0.68.1",
"language-ruby-on-rails": "0.25.0",
"language-sass": "0.45.0",
"language-shellscript": "0.21.0",
"language-source": "0.9.0",
"language-sql": "0.20.0",
"language-text": "0.7.0",
"language-text": "0.7.1",
"language-todo": "0.27.0",
"language-toml": "0.18.0",
"language-xml": "0.34.2",
"language-xml": "0.34.4",
"language-yaml": "0.25.1"
},
"private": true,
@@ -174,7 +174,8 @@
"runs",
"spyOn",
"waitsFor",
"waitsForPromise"
"waitsForPromise",
"indexedDB"
]
}
}

View File

@@ -22,31 +22,13 @@ FOR %%a IN (%*) DO (
)
)
rem Getting the process ID in cmd of the current cmd process: http://superuser.com/questions/881789/identify-and-kill-batch-script-started-before
set T=%TEMP%\atomCmdProcessId-%time::=%.tmp
wmic process where (Name="WMIC.exe" AND CommandLine LIKE "%%%TIME%%%") get ParentProcessId /value | find "ParentProcessId" >%T%
set /P A=<%T%
set PID=%A:~16%
del %T%
IF "%EXPECT_OUTPUT%"=="YES" (
SET ELECTRON_ENABLE_LOGGING=YES
IF "%WAIT%"=="YES" (
"%~dp0\..\..\atom.exe" --pid=%PID% %*
rem If the wait flag is set, don't exit this process until Atom tells it to.
goto waitLoop
)
ELSE (
powershell -noexit "%~dp0\..\..\atom.exe" --pid=$pid %* ; wait-event
) ELSE (
"%~dp0\..\..\atom.exe" %*
)
) ELSE (
"%~dp0\..\app\apm\bin\node.exe" "%~dp0\atom.js" %*
)
goto end
:waitLoop
sleep 1
goto waitLoop
:end

View File

@@ -1,49 +1,2 @@
#!/bin/sh
while getopts ":fhtvw-:" opt; do
case "$opt" in
-)
case "${OPTARG}" in
wait)
WAIT=1
;;
help|version)
REDIRECT_STDERR=1
EXPECT_OUTPUT=1
;;
foreground|test)
EXPECT_OUTPUT=1
;;
esac
;;
w)
WAIT=1
;;
h|v)
REDIRECT_STDERR=1
EXPECT_OUTPUT=1
;;
f|t)
EXPECT_OUTPUT=1
;;
esac
done
directory=$(dirname "$0")
WINPS=`ps | grep -i $$`
PID=`echo $WINPS | cut -d' ' -f 4`
if [ $EXPECT_OUTPUT ]; then
export ELECTRON_ENABLE_LOGGING=1
"$directory/../../atom.exe" --executed-from="$(pwd)" --pid=$PID "$@"
else
"$directory/../app/apm/bin/node.exe" "$directory/atom.js" "$@"
fi
# If the wait flag is set, don't exit this process until Atom tells it to.
if [ $WAIT ]; then
while true; do
sleep 1
done
fi
$(dirname "$0")/atom.cmd "$@"

View File

@@ -4,15 +4,16 @@ var fs = require('fs');
var path = require('path');
var os = require('os');
var removeCommand = process.platform === 'win32' ? 'rmdir /S /Q ' : 'rm -rf ';
var isWindows = process.platform === 'win32';
var removeCommand = isWindows ? 'rmdir /S /Q ' : 'rm -rf ';
var productName = require('../package.json').productName;
process.chdir(path.dirname(__dirname));
var home = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'];
var home = process.env[isWindows ? 'USERPROFILE' : 'HOME'];
var tmpdir = os.tmpdir();
// Windows: Use START as a way to ignore error if Atom.exe isnt running
var killatom = process.platform === 'win32' ? 'START taskkill /F /IM ' + productName + '.exe' : 'pkill -9 ' + productName + ' || true';
var killatom = isWindows ? 'START taskkill /F /IM ' + productName + '.exe' : 'pkill -9 ' + productName + ' || true';
var commands = [
killatom,
@@ -39,12 +40,32 @@ var run = function() {
if (Array.isArray(next)) {
var pathToRemove = path.resolve.apply(path.resolve, next);
if (fs.existsSync(pathToRemove))
next = removeCommand + pathToRemove;
else
if (fs.existsSync(pathToRemove)) {
if (isWindows) {
removeFolderRecursive(pathToRemove);
} else {
next = removeCommand + pathToRemove;
cp.safeExec(next, run);
}
}
else {
return run();
}
}
cp.safeExec(next, run);
else
cp.safeExec(next, run);
};
run();
// Windows has a 260-char path limit for rmdir etc. Just recursively delete in Node.
var removeFolderRecursive = function(folderPath) {
fs.readdirSync(folderPath).forEach(function(entry, index) {
var entryPath = path.join(folderPath, entry);
if (fs.lstatSync(entryPath).isDirectory()) {
removeFolderRecursive(entryPath);
} else {
fs.unlinkSync(entryPath);
}
});
fs.rmdirSync(folderPath);
};

View File

@@ -19,7 +19,9 @@ exports.afterEach = (fn) ->
waitsForPromise = (fn) ->
promise = fn()
waitsFor 'spec promise to resolve', 30000, (done) ->
# This timeout is 3 minutes. We need to bump it back down once we fix backgrounding
# of the renderer process on CI. See https://github.com/atom/electron/issues/4317
waitsFor 'spec promise to resolve', 3 * 60 * 1000, (done) ->
promise.then(
done,
(error) ->

View File

@@ -152,6 +152,8 @@ describe "AtomEnvironment", ->
atom.enablePersistence = false
it "selects the state based on the current project paths", ->
jasmine.useRealClock()
[dir1, dir2] = [temp.mkdirSync("dir1-"), temp.mkdirSync("dir2-")]
loadSettings = _.extend atom.getLoadSettings(),
@@ -159,20 +161,55 @@ describe "AtomEnvironment", ->
windowState: null
spyOn(atom, 'getLoadSettings').andCallFake -> loadSettings
spyOn(atom.getStorageFolder(), 'getPath').andReturn(temp.mkdirSync("storage-dir-"))
spyOn(atom, 'serialize').andReturn({stuff: 'cool'})
atom.state.stuff = "cool"
atom.project.setPaths([dir1, dir2])
atom.saveStateSync()
# State persistence will fail if other Atom instances are running
waitsForPromise ->
atom.stateStore.connect().then (isConnected) ->
expect(isConnected).toBe true
atom.state = {}
atom.loadStateSync()
expect(atom.state.stuff).toBeUndefined()
waitsForPromise ->
atom.saveState().then ->
atom.loadState().then (state) ->
expect(state).toBeNull()
loadSettings.initialPaths = [dir2, dir1]
atom.state = {}
atom.loadStateSync()
expect(atom.state.stuff).toBe("cool")
waitsForPromise ->
loadSettings.initialPaths = [dir2, dir1]
atom.loadState().then (state) ->
expect(state).toEqual({stuff: 'cool'})
it "saves state on keydown, mousedown, and when the editor window unloads", ->
spyOn(atom, 'saveState')
keydown = new KeyboardEvent('keydown')
atom.document.dispatchEvent(keydown)
advanceClock atom.saveStateDebounceInterval
expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false})
expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true})
atom.saveState.reset()
mousedown = new MouseEvent('mousedown')
atom.document.dispatchEvent(mousedown)
advanceClock atom.saveStateDebounceInterval
expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false})
expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true})
atom.saveState.reset()
atom.unloadEditorWindow()
mousedown = new MouseEvent('mousedown')
atom.document.dispatchEvent(mousedown)
advanceClock atom.saveStateDebounceInterval
expect(atom.saveState).toHaveBeenCalledWith({isUnloading: true})
expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: false})
it "serializes the project state with all the options supplied in saveState", ->
spyOn(atom.project, 'serialize').andReturn({foo: 42})
waitsForPromise -> atom.saveState({anyOption: 'any option'})
runs ->
expect(atom.project.serialize.calls.length).toBe(1)
expect(atom.project.serialize.mostRecentCall.args[0]).toEqual({anyOption: 'any option'})
describe "openInitialEmptyEditorIfNecessary", ->
describe "when there are no paths set", ->
@@ -230,23 +267,6 @@ describe "AtomEnvironment", ->
atomEnvironment.destroy()
it "saves the serialized state of the window so it can be deserialized after reload", ->
atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, window, document})
spyOn(atomEnvironment, 'saveStateSync')
workspaceState = atomEnvironment.workspace.serialize()
grammarsState = {grammarOverridesByPath: atomEnvironment.grammars.grammarOverridesByPath}
projectState = atomEnvironment.project.serialize()
atomEnvironment.unloadEditorWindow()
expect(atomEnvironment.state.workspace).toEqual workspaceState
expect(atomEnvironment.state.grammars).toEqual grammarsState
expect(atomEnvironment.state.project).toEqual projectState
expect(atomEnvironment.saveStateSync).toHaveBeenCalled()
atomEnvironment.destroy()
describe "::destroy()", ->
it "does not throw exceptions when unsubscribing from ipc events (regression)", ->
configDirPath = temp.mkdirSync()
@@ -258,6 +278,7 @@ describe "AtomEnvironment", ->
}
atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, window, document: fakeDocument})
spyOn(atomEnvironment.packages, 'getAvailablePackagePaths').andReturn []
spyOn(atomEnvironment, 'displayWindow').andReturn Promise.resolve()
atomEnvironment.startEditorWindow()
atomEnvironment.unloadEditorWindow()
atomEnvironment.destroy()
@@ -273,6 +294,14 @@ describe "AtomEnvironment", ->
atom.openLocations([{pathToOpen}])
expect(atom.project.getPaths()[0]).toBe __dirname
describe "then a second path is opened with forceAddToWindow", ->
it "adds the second path to the project's paths", ->
firstPathToOpen = __dirname
secondPathToOpen = path.resolve(__dirname, './fixtures')
atom.openLocations([{pathToOpen: firstPathToOpen}])
atom.openLocations([{pathToOpen: secondPathToOpen, forceAddToWindow: true}])
expect(atom.project.getPaths()).toEqual([firstPathToOpen, secondPathToOpen])
describe "when the opened path does not exist but its parent directory does", ->
it "adds the parent directory to the project paths", ->
pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt')
@@ -318,3 +347,18 @@ describe "AtomEnvironment", ->
runs ->
{releaseVersion} = updateAvailableHandler.mostRecentCall.args[0]
expect(releaseVersion).toBe 'version'
describe "::getReleaseChannel()", ->
[version] = []
beforeEach ->
spyOn(atom, 'getVersion').andCallFake -> version
it "returns the correct channel based on the version number", ->
version = '1.5.6'
expect(atom.getReleaseChannel()).toBe 'stable'
version = '1.5.0-beta10'
expect(atom.getReleaseChannel()).toBe 'beta'
version = '1.7.0-dev-5340c91'
expect(atom.getReleaseChannel()).toBe 'dev'

View File

@@ -172,7 +172,7 @@ class AtomReporter
listen document, 'click', '.stack-trace', (event) ->
event.currentTarget.classList.toggle('expanded')
@reloadButton.addEventListener('click', -> require('ipc').send('call-window-method', 'restart'))
@reloadButton.addEventListener('click', -> require('electron').ipcRenderer.send('call-window-method', 'restart'))
updateSpecCounts: ->
if @skippedCount

View File

@@ -0,0 +1,115 @@
'use babel'
import AutoUpdateManager from '../src/auto-update-manager'
import {remote} from 'electron'
const electronAutoUpdater = remote.require('electron').autoUpdater
describe('AutoUpdateManager (renderer)', () => {
let autoUpdateManager
beforeEach(() => {
autoUpdateManager = new AutoUpdateManager({
applicationDelegate: atom.applicationDelegate
})
})
afterEach(() => {
autoUpdateManager.destroy()
})
describe('::onDidBeginCheckingForUpdate', () => {
it('subscribes to "did-begin-checking-for-update" event', () => {
const spy = jasmine.createSpy('spy')
autoUpdateManager.onDidBeginCheckingForUpdate(spy)
electronAutoUpdater.emit('checking-for-update')
waitsFor(() => {
return spy.callCount === 1
})
})
})
describe('::onDidBeginDownloadingUpdate', () => {
it('subscribes to "did-begin-downloading-update" event', () => {
const spy = jasmine.createSpy('spy')
autoUpdateManager.onDidBeginDownloadingUpdate(spy)
electronAutoUpdater.emit('update-available')
waitsFor(() => {
return spy.callCount === 1
})
})
})
describe('::onDidCompleteDownloadingUpdate', () => {
it('subscribes to "did-complete-downloading-update" event', () => {
const spy = jasmine.createSpy('spy')
autoUpdateManager.onDidCompleteDownloadingUpdate(spy)
electronAutoUpdater.emit('update-downloaded', null, null, '1.2.3')
waitsFor(() => {
return spy.callCount === 1
})
runs(() => {
expect(spy.mostRecentCall.args[0].releaseVersion).toBe('1.2.3')
})
})
})
describe('::onUpdateNotAvailable', () => {
it('subscribes to "update-not-available" event', () => {
const spy = jasmine.createSpy('spy')
autoUpdateManager.onUpdateNotAvailable(spy)
electronAutoUpdater.emit('update-not-available')
waitsFor(() => {
return spy.callCount === 1
})
})
})
describe('::platformSupportsUpdates', () => {
let state, releaseChannel
it('returns true on OS X and Windows when in stable', () => {
spyOn(autoUpdateManager, 'getState').andCallFake(() => state)
spyOn(atom, 'getReleaseChannel').andCallFake(() => releaseChannel)
state = 'idle'
releaseChannel = 'stable'
expect(autoUpdateManager.platformSupportsUpdates()).toBe(true)
state = 'idle'
releaseChannel = 'dev'
expect(autoUpdateManager.platformSupportsUpdates()).toBe(false)
state = 'unsupported'
releaseChannel = 'stable'
expect(autoUpdateManager.platformSupportsUpdates()).toBe(false)
state = 'unsupported'
releaseChannel = 'dev'
expect(autoUpdateManager.platformSupportsUpdates()).toBe(false)
})
})
describe('::destroy', () => {
it('unsubscribes from all events', () => {
const spy = jasmine.createSpy('spy')
const doneIndicator = jasmine.createSpy('spy')
atom.applicationDelegate.onUpdateNotAvailable(doneIndicator)
autoUpdateManager.onDidBeginCheckingForUpdate(spy)
autoUpdateManager.onDidBeginDownloadingUpdate(spy)
autoUpdateManager.onDidCompleteDownloadingUpdate(spy)
autoUpdateManager.onUpdateNotAvailable(spy)
autoUpdateManager.destroy()
electronAutoUpdater.emit('checking-for-update')
electronAutoUpdater.emit('update-available')
electronAutoUpdater.emit('update-downloaded', null, null, '1.2.3')
electronAutoUpdater.emit('update-not-available')
waitsFor(() => {
return doneIndicator.callCount === 1
})
runs(() => {
expect(spy.callCount).toBe(0)
})
})
})
})

View File

@@ -15,7 +15,6 @@ describe "Babel transpiler support", ->
CompileCache.setCacheDirectory(temp.mkdirSync('compile-cache'))
for cacheKey in Object.keys(require.cache)
if cacheKey.startsWith(path.join(__dirname, 'fixtures', 'babel'))
console.log('deleting', cacheKey)
delete require.cache[cacheKey]
afterEach ->

View File

@@ -1621,6 +1621,16 @@ describe "Config", ->
expect(color.toHexString()).toBe '#ff0000'
expect(color.toRGBAString()).toBe 'rgba(255, 0, 0, 1)'
color.red = 11
color.green = 11
color.blue = 124
color.alpha = 1
atom.config.set('foo.bar.aColor', color)
color = atom.config.get('foo.bar.aColor')
expect(color.toHexString()).toBe '#0b0b7c'
expect(color.toRGBAString()).toBe 'rgba(11, 11, 124, 1)'
it 'coerces various types to a color object', ->
atom.config.set('foo.bar.aColor', 'red')
expect(atom.config.get('foo.bar.aColor')).toEqual {red: 255, green: 0, blue: 0, alpha: 1}

View File

@@ -1,6 +0,0 @@
module.exports = function (state) {
return {
wasDeserializedBy: 'Deserializer1',
state: state
}
}

View File

@@ -1,6 +0,0 @@
module.exports = function (state) {
return {
wasDeserializedBy: 'Deserializer2',
state: state
}
}

View File

@@ -1,3 +1,17 @@
module.exports = {
activate: function() {}
activate () {},
deserializeMethod1 (state) {
return {
wasDeserializedBy: 'deserializeMethod1',
state: state
}
},
deserializeMethod2 (state) {
return {
wasDeserializedBy: 'deserializeMethod2',
state: state
}
}
}

View File

@@ -3,7 +3,7 @@
"version": "1.0.0",
"main": "./index",
"deserializers": {
"Deserializer1": "./deserializer-1.js",
"Deserializer2": "./deserializer-2.js"
"Deserializer1": "deserializeMethod1",
"Deserializer2": "deserializeMethod2"
}
}

View File

@@ -0,0 +1,8 @@
{
"name": "package-with-a-git-prefixed-git-repo-url",
"repository": {
"type": "git",
"url": "git+https://github.com/example/repo.git"
},
"_id": "this is here to simulate the URL being already normalized by npm. we still need to stript git+ from the beginning and .git from the end."
}

View File

@@ -1,3 +0,0 @@
module.exports = function (state) {
return {state: state}
}

View File

@@ -1,3 +1,25 @@
'use strict'
module.exports = {
activate: function() {}
activate () {},
theDeserializerMethod (state) {
return {state: state}
},
viewProviderMethod1 (model) {
if (model.worksWithViewProvider1) {
let element = document.createElement('div')
element.dataset['createdBy'] = 'view-provider-1'
return element
}
},
viewProviderMethod2 (model) {
if (model.worksWithViewProvider2) {
let element = document.createElement('div')
element.dataset['createdBy'] = 'view-provider-2'
return element
}
}
}

View File

@@ -3,10 +3,10 @@
"main": "./index",
"version": "1.0.0",
"deserializers": {
"DeserializerFromPackageWithViewProviders": "./deserializer"
"DeserializerFromPackageWithViewProviders": "theDeserializerMethod"
},
"viewProviders": [
"./view-provider-1",
"./view-provider-2"
"viewProviderMethod1",
"viewProviderMethod2"
]
}

View File

@@ -1,9 +0,0 @@
'use strict'
module.exports = function (model) {
if (model.worksWithViewProvider1) {
let element = document.createElement('div')
element.dataset['createdBy'] = 'view-provider-1'
return element
}
}

View File

@@ -1,9 +0,0 @@
'use strict'
module.exports = function (model) {
if (model.worksWithViewProvider2) {
let element = document.createElement('div')
element.dataset['createdBy'] = 'view-provider-2'
return element
}
}

View File

@@ -9,12 +9,23 @@ var quicksort = function () {
// Wowza
if (items.length <= 1) return items;
var pivot = items.shift(), current, left = [], right = [];
/*
This is a multiline comment block with
an empty line inside of it.
Awesome.
*/
while(items.length > 0) {
current = items.shift();
current < pivot ? left.push(current) : right.push(current);
}
// This is a collection of
// single line comments
// ...with an empty line
// among it, geez!
return sort(left).concat(pivot).concat(sort(right));
};
// this is a single-line comment
return sort(Array.apply(this, arguments));
};
};

View File

@@ -338,10 +338,10 @@ describe('GitRepositoryAsync', () => {
})
describe('.refreshStatus()', () => {
let newPath, modifiedPath, cleanPath
let newPath, modifiedPath, cleanPath, workingDirectory
beforeEach(() => {
const workingDirectory = copyRepository()
workingDirectory = copyRepository()
repo = GitRepositoryAsync.open(workingDirectory)
modifiedPath = path.join(workingDirectory, 'file.txt')
newPath = path.join(workingDirectory, 'untracked.txt')
@@ -362,7 +362,7 @@ describe('GitRepositoryAsync', () => {
describe('in a repository with submodules', () => {
beforeEach(() => {
const workingDirectory = copySubmoduleRepository()
workingDirectory = copySubmoduleRepository()
repo = GitRepositoryAsync.open(workingDirectory)
modifiedPath = path.join(workingDirectory, 'jstips', 'README.md')
newPath = path.join(workingDirectory, 'You-Dont-Need-jQuery', 'untracked.txt')
@@ -380,6 +380,86 @@ describe('GitRepositoryAsync', () => {
expect(repo.isStatusModified(await repo.getCachedPathStatus(modifiedPath))).toBe(true)
})
})
it('caches the proper statuses when a subdir is open', async () => {
const subDir = path.join(workingDirectory, 'dir')
fs.mkdirSync(subDir)
const filePath = path.join(subDir, 'b.txt')
fs.writeFileSync(filePath, '')
atom.project.setPaths([subDir])
await atom.workspace.open('b.txt')
const repo = atom.project.getRepositories()[0].async
await repo.refreshStatus()
const status = await repo.getCachedPathStatus(filePath)
expect(repo.isStatusModified(status)).toBe(false)
expect(repo.isStatusNew(status)).toBe(false)
})
it('caches the proper statuses when multiple project are open', async () => {
const otherWorkingDirectory = copyRepository()
atom.project.setPaths([workingDirectory, otherWorkingDirectory])
await atom.workspace.open('b.txt')
const repo = atom.project.getRepositories()[0].async
await repo.refreshStatus()
const subDir = path.join(workingDirectory, 'dir')
fs.mkdirSync(subDir)
const filePath = path.join(subDir, 'b.txt')
fs.writeFileSync(filePath, 'some content!')
const status = await repo.getCachedPathStatus(filePath)
expect(repo.isStatusModified(status)).toBe(true)
expect(repo.isStatusNew(status)).toBe(false)
})
it('emits did-change-statuses if the status changes', async () => {
const someNewPath = path.join(workingDirectory, 'MyNewJSFramework.md')
fs.writeFileSync(someNewPath, '')
const statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatuses(statusHandler)
await repo.refreshStatus()
waitsFor('the onDidChangeStatuses handler to be called', () => statusHandler.callCount > 0)
})
it('emits did-change-statuses if the branch changes', async () => {
const statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatuses(statusHandler)
repo._refreshBranch = jasmine.createSpy('_refreshBranch').andCallFake(() => {
return Promise.resolve(true)
})
await repo.refreshStatus()
waitsFor('the onDidChangeStatuses handler to be called', () => statusHandler.callCount > 0)
})
it('emits did-change-statuses if the ahead/behind changes', async () => {
const statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatuses(statusHandler)
repo._refreshAheadBehindCount = jasmine.createSpy('_refreshAheadBehindCount').andCallFake(() => {
return Promise.resolve(true)
})
await repo.refreshStatus()
waitsFor('the onDidChangeStatuses handler to be called', () => statusHandler.callCount > 0)
})
})
describe('.isProjectAtRoot()', () => {
@@ -499,7 +579,7 @@ describe('GitRepositoryAsync', () => {
await atom.workspace.open('file.txt')
project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
project2.deserialize(atom.project.serialize(), atom.deserializers)
project2.deserialize(atom.project.serialize({isUnloading: true}))
const repo = project2.getRepositories()[0].async
waitsForPromise(() => repo.refreshStatus())
@@ -548,6 +628,14 @@ describe('GitRepositoryAsync', () => {
const relativizedPath = repo.relativize(`${workdir}/a/b.txt`, workdir)
expect(relativizedPath).toBe('a/b.txt')
})
it('preserves file case', () => {
repo.isCaseInsensitive = true
const workdir = '/tmp/foo/bar/baz/'
const relativizedPath = repo.relativize(`${workdir}a/README.txt`, workdir)
expect(relativizedPath).toBe('a/README.txt')
})
})
describe('.getShortHead(path)', () => {
@@ -626,7 +714,7 @@ describe('GitRepositoryAsync', () => {
repo = GitRepositoryAsync.open(workingDirectory)
})
it('returns 0, 0 for a branch with no upstream', async () => {
it('returns 1, 0 for a branch which is ahead by 1', async () => {
await repo.refreshStatus()
const {ahead, behind} = await repo.getCachedUpstreamAheadBehindCount('You-Dont-Need-jQuery')

View File

@@ -205,7 +205,7 @@ describe "GitRepository", ->
expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe true
describe ".refreshStatus()", ->
[newPath, modifiedPath, cleanPath, originalModifiedPathText] = []
[newPath, modifiedPath, cleanPath, originalModifiedPathText, workingDirectory] = []
beforeEach ->
workingDirectory = copyRepository()
@@ -231,6 +231,64 @@ describe "GitRepository", ->
expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy()
expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy()
it 'caches the proper statuses when a subdir is open', ->
subDir = path.join(workingDirectory, 'dir')
fs.mkdirSync(subDir)
filePath = path.join(subDir, 'b.txt')
fs.writeFileSync(filePath, '')
atom.project.setPaths([subDir])
waitsForPromise ->
atom.workspace.open('b.txt')
statusHandler = null
runs ->
repo = atom.project.getRepositories()[0]
statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatuses statusHandler
repo.refreshStatus()
waitsFor ->
statusHandler.callCount > 0
runs ->
status = repo.getCachedPathStatus(filePath)
expect(repo.isStatusModified(status)).toBe false
expect(repo.isStatusNew(status)).toBe false
it 'caches the proper statuses when multiple project are open', ->
otherWorkingDirectory = copyRepository()
atom.project.setPaths([workingDirectory, otherWorkingDirectory])
waitsForPromise ->
atom.workspace.open('b.txt')
statusHandler = null
runs ->
repo = atom.project.getRepositories()[0]
statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatuses statusHandler
repo.refreshStatus()
waitsFor ->
statusHandler.callCount > 0
runs ->
subDir = path.join(workingDirectory, 'dir')
fs.mkdirSync(subDir)
filePath = path.join(subDir, 'b.txt')
fs.writeFileSync(filePath, '')
status = repo.getCachedPathStatus(filePath)
expect(repo.isStatusModified(status)).toBe true
expect(repo.isStatusNew(status)).toBe false
describe "buffer events", ->
[editor] = []
@@ -289,7 +347,7 @@ describe "GitRepository", ->
runs ->
project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
project2.deserialize(atom.project.serialize(), atom.deserializers)
project2.deserialize(atom.project.serialize({isUnloading: false}))
buffer = project2.getBuffers()[0]
waitsFor ->

View File

@@ -15,6 +15,8 @@ ChromedriverPort = 9515
ChromedriverURLBase = "/wd/hub"
ChromedriverStatusURL = "http://localhost:#{ChromedriverPort}#{ChromedriverURLBase}/status"
userDataDir = temp.mkdirSync('atom-user-data-dir')
chromeDriverUp = (done) ->
checkStatus = ->
http
@@ -48,7 +50,7 @@ buildAtomClient = (args, env) ->
"atom-env=#{map(env, (value, key) -> "#{key}=#{value}").join(" ")}"
"dev"
"safe"
"user-data-dir=#{temp.mkdirSync('atom-user-data-dir')}"
"user-data-dir=#{userDataDir}"
"socket-path=#{SocketPath}"
])
@@ -124,7 +126,7 @@ buildAtomClient = (args, env) ->
.addCommand "simulateQuit", (done) ->
@execute -> atom.unloadEditorWindow()
.execute -> require("remote").require("app").emit("before-quit")
.execute -> require("electron").remote.app.emit("before-quit")
.call(done)
module.exports = (args, env, fn) ->

View File

@@ -28,13 +28,12 @@ describe "Starting Atom", ->
it "opens the parent directory and creates an empty text editor", ->
runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: atomHome}, (client) ->
client
.waitForPaneItemCount(1, 1000)
.treeViewRootDirectories()
.then ({value}) -> expect(value).toEqual([tempDirPath])
.waitForExist("atom-text-editor", 5000)
.then (exists) -> expect(exists).toBe true
.waitForPaneItemCount(1, 1000)
.click("atom-text-editor")
.keys("Hello!")
.execute -> atom.workspace.getActiveTextEditor().getText()
@@ -124,6 +123,34 @@ describe "Starting Atom", ->
.waitForPaneItemCount(0, 1000)
.treeViewRootDirectories()
.then ({value}) -> expect(value).toEqual([otherTempDirPath])
describe "when using the -a, --add option", ->
it "reuses that window and add the folder to project paths", ->
fourthTempDir = temp.mkdirSync("a-fourth-dir")
fourthTempFilePath = path.join(fourthTempDir, "a-file")
fs.writeFileSync(fourthTempFilePath, "4 - This file was already here.")
fifthTempDir = temp.mkdirSync("a-fifth-dir")
fifthTempFilePath = path.join(fifthTempDir, "a-file")
fs.writeFileSync(fifthTempFilePath, "5 - This file was already here.")
runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: atomHome}, (client) ->
client
.waitForPaneItemCount(1, 5000)
# Opening another file reuses the same window and add parent dir to
# project paths.
.startAnotherAtom(['-a', fourthTempFilePath], ATOM_HOME: atomHome)
.waitForPaneItemCount(2, 5000)
.waitForWindowCount(1, 1000)
.treeViewRootDirectories()
.then ({value}) -> expect(value).toEqual([tempDirPath, fourthTempDir])
.execute -> atom.workspace.getActiveTextEditor().getText()
.then ({value: text}) -> expect(text).toBe "4 - This file was already here."
# Opening another directory resuses the same window and add the folder to project paths.
.startAnotherAtom(['--add', fifthTempDir], ATOM_HOME: atomHome)
.treeViewRootDirectories()
.then ({value}) -> expect(value).toEqual([tempDirPath, fourthTempDir, fifthTempDir])
it "opens the new window offset from the other window", ->
runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: atomHome}, (client) ->
@@ -153,6 +180,8 @@ describe "Starting Atom", ->
.waitForPaneItemCount(0, 3000)
.execute -> atom.workspace.open()
.waitForPaneItemCount(1, 3000)
.keys("Hello!")
.waitUntil((-> Promise.resolve(false)), 1100)
runAtom [tempDirPath], {ATOM_HOME: atomHome}, (client) ->
client

View File

@@ -1,7 +1,7 @@
_ = require 'underscore-plus'
fs = require 'fs-plus'
path = require 'path'
ipc = require 'ipc'
{ipcRenderer} = require 'electron'
module.exports = ({logFile, headless, testPaths, buildAtomEnvironment}) ->
window[key] = value for key, value of require '../vendor/jasmine'
@@ -88,7 +88,7 @@ buildTerminalReporter = (logFile, resolveWithExitCode) ->
if logStream?
fs.writeSync(logStream, str)
else
ipc.send 'write-to-stderr', str
ipcRenderer.send 'write-to-stderr', str
{TerminalReporter} = require 'jasmine-tagged'
new TerminalReporter

View File

@@ -430,7 +430,7 @@ describe "LanguageMode", ->
languageMode.foldAll()
fold1 = editor.tokenizedLineForScreenRow(0).fold
expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 19]
expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 30]
fold1.destroy()
fold2 = editor.tokenizedLineForScreenRow(1).fold
@@ -441,6 +441,14 @@ describe "LanguageMode", ->
fold4 = editor.tokenizedLineForScreenRow(3).fold
expect([fold4.getStartRow(), fold4.getEndRow()]).toEqual [6, 8]
fold5 = editor.tokenizedLineForScreenRow(6).fold
expect([fold5.getStartRow(), fold5.getEndRow()]).toEqual [11, 16]
fold5.destroy()
fold6 = editor.tokenizedLineForScreenRow(13).fold
expect([fold6.getStartRow(), fold6.getEndRow()]).toEqual [21, 22]
fold6.destroy()
describe ".foldAllAtIndentLevel()", ->
it "folds every foldable range at a given indentLevel", ->
languageMode.foldAllAtIndentLevel(2)
@@ -450,14 +458,26 @@ describe "LanguageMode", ->
fold1.destroy()
fold2 = editor.tokenizedLineForScreenRow(11).fold
expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [11, 14]
expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [11, 16]
fold2.destroy()
fold3 = editor.tokenizedLineForScreenRow(17).fold
expect([fold3.getStartRow(), fold3.getEndRow()]).toEqual [17, 20]
fold3.destroy()
fold4 = editor.tokenizedLineForScreenRow(21).fold
expect([fold4.getStartRow(), fold4.getEndRow()]).toEqual [21, 22]
fold4.destroy()
fold5 = editor.tokenizedLineForScreenRow(24).fold
expect([fold5.getStartRow(), fold5.getEndRow()]).toEqual [24, 25]
fold5.destroy()
it "does not fold anything but the indentLevel", ->
languageMode.foldAllAtIndentLevel(0)
fold1 = editor.tokenizedLineForScreenRow(0).fold
expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 19]
expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 30]
fold1.destroy()
fold2 = editor.tokenizedLineForScreenRow(5).fold
@@ -467,7 +487,13 @@ describe "LanguageMode", ->
it "returns true if the line starts a multi-line comment", ->
expect(languageMode.isFoldableAtBufferRow(1)).toBe true
expect(languageMode.isFoldableAtBufferRow(6)).toBe true
expect(languageMode.isFoldableAtBufferRow(17)).toBe false
expect(languageMode.isFoldableAtBufferRow(8)).toBe false
expect(languageMode.isFoldableAtBufferRow(11)).toBe true
expect(languageMode.isFoldableAtBufferRow(15)).toBe false
expect(languageMode.isFoldableAtBufferRow(17)).toBe true
expect(languageMode.isFoldableAtBufferRow(21)).toBe true
expect(languageMode.isFoldableAtBufferRow(24)).toBe true
expect(languageMode.isFoldableAtBufferRow(28)).toBe false
it "does not return true for a line in the middle of a comment that's followed by an indented line", ->
expect(languageMode.isFoldableAtBufferRow(7)).toBe false

View File

@@ -55,12 +55,17 @@ describe "PackageManager", ->
it "normalizes short repository urls in package.json", ->
{metadata} = atom.packages.loadPackage("package-with-short-url-package-json")
expect(metadata.repository.type).toBe "git"
expect(metadata.repository.url).toBe "https://github.com/example/repo.git"
expect(metadata.repository.url).toBe "https://github.com/example/repo"
{metadata} = atom.packages.loadPackage("package-with-invalid-url-package-json")
expect(metadata.repository.type).toBe "git"
expect(metadata.repository.url).toBe "foo"
it "trims git+ from the beginning and .git from the end of repository URLs, even if npm already normalized them ", ->
{metadata} = atom.packages.loadPackage("package-with-prefixed-and-suffixed-repo-url")
expect(metadata.repository.type).toBe "git"
expect(metadata.repository.url).toBe "https://github.com/example/repo"
it "returns null if the package is not found in any package directory", ->
spyOn(console, 'warn')
expect(atom.packages.loadPackage("this-package-cannot-be-found")).toBeNull()
@@ -88,18 +93,16 @@ describe "PackageManager", ->
state1 = {deserializer: 'Deserializer1', a: 'b'}
expect(atom.deserializers.deserialize(state1)).toEqual {
wasDeserializedBy: 'Deserializer1'
wasDeserializedBy: 'deserializeMethod1'
state: state1
}
state2 = {deserializer: 'Deserializer2', c: 'd'}
expect(atom.deserializers.deserialize(state2)).toEqual {
wasDeserializedBy: 'Deserializer2'
wasDeserializedBy: 'deserializeMethod2'
state: state2
}
expect(pack.mainModule).toBeNull()
describe "when there are view providers specified in the package's package.json", ->
model1 = {worksWithViewProvider1: true}
model2 = {worksWithViewProvider2: true}
@@ -448,16 +451,15 @@ describe "PackageManager", ->
pack = null
waitsForPromise ->
atom.packages.activatePackage("package-with-serialization").then (p) -> pack = p
runs ->
expect(pack.mainModule.someNumber).not.toBe 77
pack.mainModule.someNumber = 77
atom.packages.deactivatePackage("package-with-serialization")
spyOn(pack.mainModule, 'activate').andCallThrough()
waitsForPromise ->
atom.packages.activatePackage("package-with-serialization")
runs ->
expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77})
waitsForPromise ->
atom.packages.activatePackage("package-with-serialization")
runs ->
expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77})
it "invokes ::onDidActivatePackage listeners with the activated package", ->
activatedPackage = null
@@ -821,6 +823,34 @@ describe "PackageManager", ->
expect(atom.packages.isPackageActive("package-with-missing-provided-services")).toBe true
expect(addErrorHandler.callCount).toBe 0
describe "::serialize", ->
it "does not serialize packages that threw an error during activation", ->
spyOn(console, 'warn')
badPack = null
waitsForPromise ->
atom.packages.activatePackage("package-that-throws-on-activate").then (p) -> badPack = p
runs ->
spyOn(badPack.mainModule, 'serialize').andCallThrough()
atom.packages.serialize()
expect(badPack.mainModule.serialize).not.toHaveBeenCalled()
it "absorbs exceptions that are thrown by the package module's serialize method", ->
spyOn(console, 'error')
waitsForPromise ->
atom.packages.activatePackage('package-with-serialize-error')
waitsForPromise ->
atom.packages.activatePackage('package-with-serialization')
runs ->
atom.packages.serialize()
expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined()
expect(atom.packages.packageStates['package-with-serialization']).toEqual someNumber: 1
expect(console.error).toHaveBeenCalled()
describe "::deactivatePackage(id)", ->
afterEach ->
atom.packages.unloadPackages()
@@ -852,33 +882,6 @@ describe "PackageManager", ->
expect(badPack.mainModule.deactivate).not.toHaveBeenCalled()
expect(atom.packages.isPackageActive("package-that-throws-on-activate")).toBeFalsy()
it "does not serialize packages that have not been activated called on their main module", ->
spyOn(console, 'warn')
badPack = null
waitsForPromise ->
atom.packages.activatePackage("package-that-throws-on-activate").then (p) -> badPack = p
runs ->
spyOn(badPack.mainModule, 'serialize').andCallThrough()
atom.packages.deactivatePackage("package-that-throws-on-activate")
expect(badPack.mainModule.serialize).not.toHaveBeenCalled()
it "absorbs exceptions that are thrown by the package module's serialize method", ->
spyOn(console, 'error')
waitsForPromise ->
atom.packages.activatePackage('package-with-serialize-error')
waitsForPromise ->
atom.packages.activatePackage('package-with-serialization')
runs ->
atom.packages.deactivatePackages()
expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined()
expect(atom.packages.packageStates['package-with-serialization']).toEqual someNumber: 1
expect(console.error).toHaveBeenCalled()
it "absorbs exceptions that are thrown by the package module's deactivate method", ->
spyOn(console, 'error')

View File

@@ -1,5 +1,6 @@
{extend} = require 'underscore-plus'
{Emitter} = require 'event-kit'
Grim = require 'grim'
Pane = require '../src/pane'
PaneAxis = require '../src/pane-axis'
PaneContainer = require '../src/pane-container'
@@ -18,8 +19,8 @@ describe "Pane", ->
onDidDestroy: (fn) -> @emitter.on('did-destroy', fn)
destroy: -> @destroyed = true; @emitter.emit('did-destroy')
isDestroyed: -> @destroyed
isPending: -> @pending
pending: false
onDidTerminatePendingState: (callback) -> @emitter.on 'terminate-pending-state', callback
terminatePendingState: -> @emitter.emit 'terminate-pending-state'
beforeEach ->
confirm = spyOn(atom.applicationDelegate, 'confirm')
@@ -92,7 +93,7 @@ describe "Pane", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B")]))
[item1, item2] = pane.getItems()
item3 = new Item("C")
pane.addItem(item3, 1)
pane.addItem(item3, index: 1)
expect(pane.getItems()).toEqual [item1, item3, item2]
it "adds the item after the active item if no index is provided", ->
@@ -115,7 +116,7 @@ describe "Pane", ->
pane.onDidAddItem (event) -> events.push(event)
item = new Item("C")
pane.addItem(item, 1)
pane.addItem(item, index: 1)
expect(events).toEqual [{item, index: 1, moved: false}]
it "throws an exception if the item is already present on a pane", ->
@@ -132,15 +133,56 @@ describe "Pane", ->
expect(-> pane.addItem('foo')).toThrow()
expect(-> pane.addItem(1)).toThrow()
it "destroys any existing pending item if the new item is pending", ->
it "destroys any existing pending item", ->
pane = new Pane(paneParams(items: []))
itemA = new Item("A")
itemB = new Item("B")
itemA.pending = true
itemB.pending = true
pane.addItem(itemA)
itemC = new Item("C")
pane.addItem(itemA, pending: false)
pane.addItem(itemB, pending: true)
pane.addItem(itemC, pending: false)
expect(itemB.isDestroyed()).toBe true
it "adds the new item before destroying any existing pending item", ->
eventOrder = []
pane = new Pane(paneParams(items: []))
itemA = new Item("A")
itemB = new Item("B")
pane.addItem(itemA, pending: true)
pane.onDidAddItem ({item}) ->
eventOrder.push("add") if item is itemB
pane.onDidRemoveItem ({item}) ->
eventOrder.push("remove") if item is itemA
pane.addItem(itemB)
expect(itemA.isDestroyed()).toBe true
waitsFor ->
eventOrder.length is 2
runs ->
expect(eventOrder).toEqual ["add", "remove"]
describe "when using the old API of ::addItem(item, index)", ->
beforeEach ->
spyOn Grim, "deprecate"
it "supports the older public API", ->
pane = new Pane(paneParams(items: []))
itemA = new Item("A")
itemB = new Item("B")
itemC = new Item("C")
pane.addItem(itemA, 0)
pane.addItem(itemB, 0)
pane.addItem(itemC, 0)
expect(pane.getItems()).toEqual [itemC, itemB, itemA]
it "shows a deprecation warning", ->
pane = new Pane(paneParams(items: []))
pane.addItem(new Item(), 2)
expect(Grim.deprecate).toHaveBeenCalledWith "Pane::addItem(item, 2) is deprecated in favor of Pane::addItem(item, {index: 2})"
describe "::activateItem(item)", ->
pane = null
@@ -172,21 +214,83 @@ describe "Pane", ->
beforeEach ->
itemC = new Item("C")
itemD = new Item("D")
itemC.pending = true
itemD.pending = true
it "replaces the active item if it is pending", ->
pane.activateItem(itemC)
pane.activateItem(itemC, pending: true)
expect(pane.getItems().map (item) -> item.name).toEqual ['A', 'C', 'B']
pane.activateItem(itemD)
pane.activateItem(itemD, pending: true)
expect(pane.getItems().map (item) -> item.name).toEqual ['A', 'D', 'B']
it "adds the item after the active item if it is not pending", ->
pane.activateItem(itemC)
pane.activateItem(itemC, pending: true)
pane.activateItemAtIndex(2)
pane.activateItem(itemD)
pane.activateItem(itemD, pending: true)
expect(pane.getItems().map (item) -> item.name).toEqual ['A', 'B', 'D']
describe "::setPendingItem", ->
pane = null
beforeEach ->
pane = atom.workspace.getActivePane()
it "changes the pending item", ->
expect(pane.getPendingItem()).toBeNull()
pane.setPendingItem("fake item")
expect(pane.getPendingItem()).toEqual "fake item"
describe "::onItemDidTerminatePendingState callback", ->
pane = null
callbackCalled = false
beforeEach ->
pane = atom.workspace.getActivePane()
callbackCalled = false
it "is called when the pending item changes", ->
pane.setPendingItem("fake item one")
pane.onItemDidTerminatePendingState (item) ->
callbackCalled = true
expect(item).toEqual "fake item one"
pane.setPendingItem("fake item two")
expect(callbackCalled).toBeTruthy()
it "has access to the new pending item via ::getPendingItem", ->
pane.setPendingItem("fake item one")
pane.onItemDidTerminatePendingState (item) ->
callbackCalled = true
expect(pane.getPendingItem()).toEqual "fake item two"
pane.setPendingItem("fake item two")
expect(callbackCalled).toBeTruthy()
describe "::activateNextRecentlyUsedItem() and ::activatePreviousRecentlyUsedItem()", ->
it "sets the active item to the next/previous item in the itemStack, looping around at either end", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C"), new Item("D"), new Item("E")]))
[item1, item2, item3, item4, item5] = pane.getItems()
pane.itemStack = [item3, item1, item2, item5, item4]
pane.activateItem(item4)
expect(pane.getActiveItem()).toBe item4
pane.activateNextRecentlyUsedItem()
expect(pane.getActiveItem()).toBe item5
pane.activateNextRecentlyUsedItem()
expect(pane.getActiveItem()).toBe item2
pane.activatePreviousRecentlyUsedItem()
expect(pane.getActiveItem()).toBe item5
pane.activatePreviousRecentlyUsedItem()
expect(pane.getActiveItem()).toBe item4
pane.activatePreviousRecentlyUsedItem()
expect(pane.getActiveItem()).toBe item3
pane.activatePreviousRecentlyUsedItem()
expect(pane.getActiveItem()).toBe item1
pane.activateNextRecentlyUsedItem()
expect(pane.getActiveItem()).toBe item3
pane.activateNextRecentlyUsedItem()
expect(pane.getActiveItem()).toBe item4
pane.activateNextRecentlyUsedItem()
pane.moveActiveItemToTopOfStack()
expect(pane.getActiveItem()).toBe item5
expect(pane.itemStack[4]).toBe item5
describe "::activateNextItem() and ::activatePreviousItem()", ->
it "sets the active item to the next/previous item, looping around at either end", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")]))
@@ -253,7 +357,7 @@ describe "Pane", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")]))
[item1, item2, item3] = pane.getItems()
it "removes the item from the items list and destroyes it", ->
it "removes the item from the items list and destroys it", ->
expect(pane.getActiveItem()).toBe item1
pane.destroyItem(item2)
expect(item2 in pane.getItems()).toBe false
@@ -264,6 +368,23 @@ describe "Pane", ->
expect(item1 in pane.getItems()).toBe false
expect(item1.isDestroyed()).toBe true
it "removes the item from the itemStack", ->
pane.itemStack = [item2, item3, item1]
pane.activateItem(item1)
expect(pane.getActiveItem()).toBe item1
pane.destroyItem(item3)
expect(pane.itemStack).toEqual [item2, item1]
expect(pane.getActiveItem()).toBe item1
pane.destroyItem(item1)
expect(pane.itemStack).toEqual [item2]
expect(pane.getActiveItem()).toBe item2
pane.destroyItem(item2)
expect(pane.itemStack).toEqual []
expect(pane.getActiveItem()).toBeUndefined()
it "invokes ::onWillDestroyItem() observers before destroying the item", ->
events = []
pane.onWillDestroyItem (event) ->
@@ -605,6 +726,23 @@ describe "Pane", ->
expect(pane2.isDestroyed()).toBe true
expect(item4.isDestroyed()).toBe false
describe "when the item being moved is pending", ->
it "is made permanent in the new pane", ->
item6 = new Item("F")
pane1.addItem(item6, pending: true)
expect(pane1.getPendingItem()).toEqual item6
pane1.moveItemToPane(item6, pane2, 0)
expect(pane2.getPendingItem()).not.toEqual item6
describe "when the target pane has a pending item", ->
it "does not destroy the pending item", ->
item6 = new Item("F")
pane1.addItem(item6, pending: true)
expect(pane1.getPendingItem()).toEqual item6
pane2.moveItemToPane(item5, pane1, 0)
expect(pane1.getPendingItem()).toEqual item6
describe "split methods", ->
[pane1, item1, container] = []
@@ -806,6 +944,67 @@ describe "Pane", ->
pane2.destroy()
expect(container.root).toBe pane1
describe "pending state", ->
editor1 = null
pane = null
eventCount = null
beforeEach ->
waitsForPromise ->
atom.workspace.open('sample.txt', pending: true).then (o) ->
editor1 = o
pane = atom.workspace.getActivePane()
runs ->
eventCount = 0
editor1.onDidTerminatePendingState -> eventCount++
it "does not open file in pending state by default", ->
waitsForPromise ->
atom.workspace.open('sample.js').then (o) ->
editor1 = o
pane = atom.workspace.getActivePane()
runs ->
expect(pane.getPendingItem()).toBeNull()
it "opens file in pending state if 'pending' option is true", ->
expect(pane.getPendingItem()).toEqual editor1
it "terminates pending state if ::terminatePendingState is invoked", ->
editor1.terminatePendingState()
expect(pane.getPendingItem()).toBeNull()
expect(eventCount).toBe 1
it "terminates pending state when buffer is changed", ->
editor1.insertText('I\'ll be back!')
advanceClock(editor1.getBuffer().stoppedChangingDelay)
expect(pane.getPendingItem()).toBeNull()
expect(eventCount).toBe 1
it "only calls terminate handler once when text is modified twice", ->
editor1.insertText('Some text')
advanceClock(editor1.getBuffer().stoppedChangingDelay)
editor1.save()
editor1.insertText('More text')
advanceClock(editor1.getBuffer().stoppedChangingDelay)
expect(pane.getPendingItem()).toBeNull()
expect(eventCount).toBe 1
it "only calls clearPendingItem if there is a pending item to clear", ->
spyOn(pane, "clearPendingItem").andCallThrough()
editor1.terminatePendingState()
editor1.terminatePendingState()
expect(pane.getPendingItem()).toBeNull()
expect(pane.clearPendingItem.callCount).toBe 1
describe "serialization", ->
pane = null
@@ -837,3 +1036,30 @@ describe "Pane", ->
pane.focus()
newPane = Pane.deserialize(pane.serialize(), atom)
expect(newPane.focused).toBe true
it "can serialize and deserialize the order of the items in the itemStack", ->
[item1, item2, item3] = pane.getItems()
pane.itemStack = [item3, item1, item2]
newPane = Pane.deserialize(pane.serialize(), atom)
expect(newPane.itemStack).toEqual pane.itemStack
expect(newPane.itemStack[2]).toEqual item2
it "builds the itemStack if the itemStack is not serialized", ->
[item1, item2, item3] = pane.getItems()
newPane = Pane.deserialize(pane.serialize(), atom)
expect(newPane.getItems()).toEqual newPane.itemStack
it "rebuilds the itemStack if items.length does not match itemStack.length", ->
[item1, item2, item3] = pane.getItems()
pane.itemStack = [item2, item3]
newPane = Pane.deserialize(pane.serialize(), atom)
expect(newPane.getItems()).toEqual newPane.itemStack
it "does not serialize the reference to the items in the itemStack for pane items that will not be serialized", ->
[item1, item2, item3] = pane.getItems()
pane.itemStack = [item2, item1, item3]
unserializable = {}
pane.activateItem(unserializable)
newPane = Pane.deserialize(pane.serialize(), atom)
expect(newPane.itemStack).toEqual [item2, item1, item3]

View File

@@ -21,6 +21,14 @@ describe "Project", ->
afterEach ->
deserializedProject?.destroy()
it "does not deserialize paths to non directories", ->
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
state = atom.project.serialize()
state.paths.push('/directory/that/does/not/exist')
state.paths.push(path.join(__dirname, 'fixtures', 'sample.js'))
deserializedProject.deserialize(state, atom.deserializers)
expect(deserializedProject.getPaths()).toEqual(atom.project.getPaths())
it "does not include unretained buffers in the serialized state", ->
waitsForPromise ->
atom.project.bufferForPath('a')
@@ -29,7 +37,7 @@ describe "Project", ->
expect(atom.project.getBuffers().length).toBe 1
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
deserializedProject.deserialize(atom.project.serialize(), atom.deserializers)
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
expect(deserializedProject.getBuffers().length).toBe 0
it "listens for destroyed events on deserialized buffers and removes them when they are destroyed", ->
@@ -39,7 +47,7 @@ describe "Project", ->
runs ->
expect(atom.project.getBuffers().length).toBe 1
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
deserializedProject.deserialize(atom.project.serialize(), atom.deserializers)
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
expect(deserializedProject.getBuffers().length).toBe 1
deserializedProject.getBuffers()[0].destroy()
@@ -56,7 +64,7 @@ describe "Project", ->
expect(atom.project.getBuffers().length).toBe 1
fs.mkdirSync(pathToOpen)
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
deserializedProject.deserialize(atom.project.serialize(), atom.deserializers)
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
expect(deserializedProject.getBuffers().length).toBe 0
it "does not deserialize buffers when their path is inaccessible", ->
@@ -70,9 +78,26 @@ describe "Project", ->
expect(atom.project.getBuffers().length).toBe 1
fs.chmodSync(pathToOpen, '000')
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
deserializedProject.deserialize(atom.project.serialize(), atom.deserializers)
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
expect(deserializedProject.getBuffers().length).toBe 0
it "serializes marker layers only if Atom is quitting", ->
waitsForPromise ->
atom.workspace.open('a')
runs ->
bufferA = atom.project.getBuffers()[0]
layerA = bufferA.addMarkerLayer(maintainHistory: true)
markerA = layerA.markPosition([0, 3])
notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
notQuittingProject.deserialize(atom.project.serialize({isUnloading: false}))
expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).toBeUndefined()
quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
quittingProject.deserialize(atom.project.serialize({isUnloading: true}))
expect(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).not.toBeUndefined()
describe "when an editor is saved and the project has no path", ->
it "sets the project's path to the saved file's parent directory", ->
tempFile = temp.openSync().path

View File

@@ -83,3 +83,11 @@ describe "Selection", ->
selection.setBufferRange([[2, 0], [2, 10]])
selection.destroy()
expect(selection.marker.isDestroyed()).toBeTruthy()
describe ".insertText(text, options)", ->
it "allows pasting white space only lines when autoIndent is enabled", ->
selection.setBufferRange [[0, 0], [0, 0]]
selection.insertText(" \n \n\n", autoIndent: true)
expect(buffer.lineForRow(0)).toBe " "
expect(buffer.lineForRow(1)).toBe " "
expect(buffer.lineForRow(2)).toBe ""

61
spec/state-store-spec.js Normal file
View File

@@ -0,0 +1,61 @@
/** @babel */
import {it, fit, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers'
const StateStore = require('../src/state-store.js')
describe("StateStore", () => {
let databaseName = `test-database-${Date.now()}`
let version = 1
it("can save and load states", () => {
const store = new StateStore(databaseName, version)
return store.save('key', {foo:'bar'})
.then(() => store.load('key'))
.then((state) => {
expect(state).toEqual({foo:'bar'})
})
})
it("resolves with null when a non-existent key is loaded", () => {
const store = new StateStore(databaseName, version)
return store.load('no-such-key').then((value) => {
expect(value).toBeNull()
})
})
it("can clear the state object store", () => {
const store = new StateStore(databaseName, version)
return store.save('key', {foo:'bar'})
.then(() => store.count())
.then((count) =>
expect(count).toBe(1)
)
.then(() => store.clear())
.then(() => store.count())
.then((count) => {
expect(count).toBe(0)
})
})
describe("when there is an error reading from the database", () => {
it("rejects the promise returned by load", () => {
const store = new StateStore(databaseName, version)
const fakeErrorEvent = {target: {errorCode: "Something bad happened"}}
spyOn(IDBObjectStore.prototype, 'get').andCallFake((key) => {
let request = {}
process.nextTick(() => request.onerror(fakeErrorEvent))
return request
})
return store.load('nonexistentKey')
.then(() => {
throw new Error("Promise should have been rejected")
})
.catch((event) => {
expect(event).toBe(fakeErrorEvent)
})
})
})
})

View File

@@ -1,6 +1,6 @@
/** @babel */
import {it, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers'
import {it, fit, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers'
import TextEditorElement from '../src/text-editor-element'
import _, {extend, flatten, last, toArray} from 'underscore-plus'
@@ -2151,10 +2151,11 @@ describe('TextEditorComponent', function () {
item.style.height = itemHeight + 'px'
wrapperNode.style.width = windowWidth + 'px'
wrapperNode.style.height = windowHeight + 'px'
atom.setWindowDimensions({
await atom.setWindowDimensions({
width: windowWidth,
height: windowHeight
})
component.measureDimensions()
component.measureWindowSize()
await nextViewUpdatePromise()
@@ -4834,7 +4835,7 @@ describe('TextEditorComponent', function () {
it('pastes the previously selected text at the clicked location', async function () {
let clipboardWrittenTo = false
spyOn(require('ipc'), 'send').andCallFake(function (eventName, selectedText) {
spyOn(require('electron').ipcRenderer, 'send').andCallFake(function (eventName, selectedText) {
if (eventName === 'write-text-to-selection-clipboard') {
require('../src/safe-clipboard').writeText(selectedText, 'selection')
clipboardWrittenTo = true

View File

@@ -91,11 +91,13 @@ describe "TextEditorPresenter", ->
expectNoStateUpdate = (presenter, fn) -> expectStateUpdatedToBe(false, presenter, fn)
waitsForStateToUpdate = (presenter, fn) ->
waitsFor "presenter state to update", 1000, (done) ->
fn?()
line = new Error().stack.split('\n')[2].split(':')[1]
waitsFor "presenter state to update at line #{line}", 1000, (done) ->
disposable = presenter.onDidUpdateState ->
disposable.dispose()
process.nextTick(done)
fn?()
tiledContentContract = (stateFn) ->
it "contains states for tiles that are visible on screen", ->
@@ -633,16 +635,28 @@ describe "TextEditorPresenter", ->
expectStateUpdate presenter, -> presenter.setExplicitHeight(500)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe 500
it "adds the computed clientHeight to the computed scrollHeight if editor.scrollPastEnd is true", ->
presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10)
expectStateUpdate presenter, -> presenter.setScrollTop(300)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
describe "scrollPastEnd", ->
it "adds the computed clientHeight to the computed scrollHeight if editor.scrollPastEnd is true", ->
presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10)
expectStateUpdate presenter, -> presenter.setScrollTop(300)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", true)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight + presenter.clientHeight - (presenter.lineHeight * 3)
expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", true)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight + presenter.clientHeight - (presenter.lineHeight * 3)
expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
it "doesn't add the computed clientHeight to the computed scrollHeight if editor.scrollPastEnd is true but the presenter is created with scrollPastEnd as false", ->
presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10, scrollPastEnd: false)
expectStateUpdate presenter, -> presenter.setScrollTop(300)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", true)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
describe ".scrollTop", ->
it "tracks the value of ::scrollTop", ->
@@ -1336,9 +1350,11 @@ describe "TextEditorPresenter", ->
presenter = buildPresenter()
blockDecoration2 = addBlockDecorationBeforeScreenRow(3)
blockDecoration3 = addBlockDecorationBeforeScreenRow(7)
blockDecoration4 = addBlockDecorationAfterScreenRow(7)
blockDecoration4 = null
waitsForStateToUpdate presenter, ->
blockDecoration4 = addBlockDecorationAfterScreenRow(7)
waitsForStateToUpdate presenter
runs ->
expect(lineStateForScreenRow(presenter, 0).precedingBlockDecorations).toEqual([blockDecoration1])
expect(lineStateForScreenRow(presenter, 0).followingBlockDecorations).toEqual([])
@@ -1472,9 +1488,9 @@ describe "TextEditorPresenter", ->
decoration1 = editor.decorateMarker(marker1, type: 'line', class: 'a')
presenter = buildPresenter()
marker2 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch')
decoration2 = editor.decorateMarker(marker2, type: 'line', class: 'b')
decoration2 = null
waitsForStateToUpdate presenter
waitsForStateToUpdate presenter, -> decoration2 = editor.decorateMarker(marker2, type: 'line', class: 'b')
runs ->
expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
@@ -2150,31 +2166,40 @@ describe "TextEditorPresenter", ->
}
# becoming empty
waitsForStateToUpdate presenter, -> editor.getSelections()[1].clear(autoscroll: false)
runs ->
editor.getSelections()[1].clear(autoscroll: false)
waitsForStateToUpdate presenter
runs ->
expectUndefinedStateForSelection(presenter, 1)
# becoming non-empty
waitsForStateToUpdate presenter, -> editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]], autoscroll: false)
runs ->
editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]], autoscroll: false)
waitsForStateToUpdate presenter
runs ->
expectValues stateForSelectionInTile(presenter, 1, 2), {
regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}]
}
# moving out of view
waitsForStateToUpdate presenter, -> editor.getSelections()[1].setBufferRange([[3, 4], [3, 6]], autoscroll: false)
runs ->
editor.getSelections()[1].setBufferRange([[3, 4], [3, 6]], autoscroll: false)
waitsForStateToUpdate presenter
runs ->
expectUndefinedStateForSelection(presenter, 1)
# adding
waitsForStateToUpdate presenter, -> editor.addSelectionForBufferRange([[1, 4], [1, 6]], autoscroll: false)
runs -> editor.addSelectionForBufferRange([[1, 4], [1, 6]], autoscroll: false)
waitsForStateToUpdate presenter
runs ->
expectValues stateForSelectionInTile(presenter, 2, 0), {
regions: [{top: 10, left: 4 * 10, width: 2 * 10, height: 10}]
}
# moving added selection
waitsForStateToUpdate presenter, -> editor.getSelections()[2].setBufferRange([[1, 4], [1, 8]], autoscroll: false)
runs ->
editor.getSelections()[2].setBufferRange([[1, 4], [1, 8]], autoscroll: false)
waitsForStateToUpdate presenter
destroyedSelection = null
runs ->
@@ -2208,8 +2233,9 @@ describe "TextEditorPresenter", ->
presenter = buildPresenter(explicitHeight: 30, scrollTop: 20, tileSize: 2)
marker = editor.markBufferPosition([2, 2])
highlight = editor.decorateMarker(marker, type: 'highlight', class: 'a')
highlight = null
waitsForStateToUpdate presenter, ->
highlight = editor.decorateMarker(marker, type: 'highlight', class: 'a')
marker.setBufferRange([[2, 2], [5, 2]])
highlight.flash('b', 500)
runs ->
@@ -2969,9 +2995,8 @@ describe "TextEditorPresenter", ->
presenter.setBlockDecorationDimensions(blockDecoration4, 0, 35)
presenter.setBlockDecorationDimensions(blockDecoration4, 0, 40)
presenter.setBlockDecorationDimensions(blockDecoration5, 0, 50)
presenter.setBlockDecorationDimensions(blockDecoration6, 0, 60)
waitsForStateToUpdate presenter
waitsForStateToUpdate presenter, -> presenter.setBlockDecorationDimensions(blockDecoration6, 0, 60)
runs ->
expect(lineNumberStateForScreenRow(presenter, 0).blockDecorationsHeight).toBe(10)
expect(lineNumberStateForScreenRow(presenter, 1).blockDecorationsHeight).toBe(0)
@@ -3460,9 +3485,9 @@ describe "TextEditorPresenter", ->
gutterName: 'test-gutter-2'
class: 'test-class'
marker4 = editor.markBufferRange([[0, 0], [1, 0]])
decoration4 = editor.decorateMarker(marker4, decorationParams)
decoration4 = null
waitsForStateToUpdate presenter
waitsForStateToUpdate presenter, -> decoration4 = editor.decorateMarker(marker4, decorationParams)
runs ->
expectStateUpdate presenter, -> editor.addGutter({name: 'test-gutter-2'})

View File

@@ -0,0 +1,38 @@
TextEditorRegistry = require '../src/text-editor-registry'
describe "TextEditorRegistry", ->
[registry, editor] = []
beforeEach ->
registry = new TextEditorRegistry
describe "when a TextEditor is added", ->
it "gets added to the list of registered editors", ->
editor = {}
registry.add(editor)
expect(registry.editors.size).toBe 1
expect(registry.editors.has(editor)).toBe(true)
it "returns a Disposable that can unregister the editor", ->
editor = {}
disposable = registry.add(editor)
expect(registry.editors.size).toBe 1
disposable.dispose()
expect(registry.editors.size).toBe 0
describe "when the registry is observed", ->
it "calls the callback for current and future editors until unsubscribed", ->
[editor1, editor2, editor3] = [{}, {}, {}]
registry.add(editor1)
subscription = registry.observe spy = jasmine.createSpy()
expect(spy.calls.length).toBe 1
registry.add(editor2)
expect(spy.calls.length).toBe 2
expect(spy.argsForCall[0][0]).toBe editor1
expect(spy.argsForCall[1][0]).toBe editor2
subscription.dispose()
registry.add(editor3)
expect(spy.calls.length).toBe 2

View File

@@ -55,16 +55,6 @@ describe "TextEditor", ->
expect(editor.tokenizedLineForScreenRow(0).invisibles.eol).toBe '?'
it "restores pending tabs in pending state", ->
expect(editor.isPending()).toBe false
editor2 = TextEditor.deserialize(editor.serialize(), atom)
expect(editor2.isPending()).toBe false
pendingEditor = atom.workspace.buildTextEditor(pending: true)
expect(pendingEditor.isPending()).toBe true
editor3 = TextEditor.deserialize(pendingEditor.serialize(), atom)
expect(editor3.isPending()).toBe true
describe "when the editor is constructed with the largeFileMode option set to true", ->
it "loads the editor but doesn't tokenize", ->
editor = null
@@ -139,6 +129,15 @@ describe "TextEditor", ->
expect(editor2.getSoftTabs()).toBe true
expect(editor2.getEncoding()).toBe 'macroman'
atom.config.set('editor.tabLength', -1)
expect(editor2.getTabLength()).toBe 1
atom.config.set('editor.tabLength', 2)
expect(editor2.getTabLength()).toBe 2
atom.config.set('editor.tabLength', 17)
expect(editor2.getTabLength()).toBe 17
atom.config.set('editor.tabLength', 128)
expect(editor2.getTabLength()).toBe 128
it "uses scoped `core.fileEncoding` values", ->
editor1 = null
editor2 = null
@@ -2133,20 +2132,31 @@ describe "TextEditor", ->
editor.splitSelectionsIntoLines()
expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 3]]]
describe ".consolidateSelections()", ->
it "destroys all selections but the least recent, returning true if any selections were destroyed", ->
editor.setSelectedBufferRange([[3, 16], [3, 21]])
selection1 = editor.getLastSelection()
describe "::consolidateSelections()", ->
makeMultipleSelections = ->
selection.setBufferRange [[3, 16], [3, 21]]
selection2 = editor.addSelectionForBufferRange([[3, 25], [3, 34]])
selection3 = editor.addSelectionForBufferRange([[8, 4], [8, 10]])
selection4 = editor.addSelectionForBufferRange([[1, 6], [1, 10]])
expect(editor.getSelections()).toEqual [selection, selection2, selection3, selection4]
[selection, selection2, selection3, selection4]
it "destroys all selections but the oldest selection and autoscrolls to it, returning true if any selections were destroyed", ->
[selection1] = makeMultipleSelections()
autoscrollEvents = []
editor.onDidRequestAutoscroll (event) -> autoscrollEvents.push(event)
expect(editor.getSelections()).toEqual [selection1, selection2, selection3]
expect(editor.consolidateSelections()).toBeTruthy()
expect(editor.getSelections()).toEqual [selection1]
expect(selection1.isEmpty()).toBeFalsy()
expect(editor.consolidateSelections()).toBeFalsy()
expect(editor.getSelections()).toEqual [selection1]
expect(autoscrollEvents).toEqual([
{screenRange: selection1.getScreenRange(), options: {center: true, reversed: false}}
])
describe "when the cursor is moved while there is a selection", ->
makeSelection = -> selection.setBufferRange [[1, 2], [1, 5]]
@@ -5819,52 +5829,29 @@ describe "TextEditor", ->
rangeIsReversed: false
}
describe "pending state", ->
editor1 = null
eventCount = null
describe "when the editor is constructed with the showInvisibles option set to false", ->
beforeEach ->
atom.workspace.destroyActivePane()
waitsForPromise ->
atom.workspace.open('sample.txt', pending: true).then (o) -> editor1 = o
atom.workspace.open('sample.js', showInvisibles: false).then (o) -> editor = o
runs ->
eventCount = 0
editor1.onDidTerminatePendingState -> eventCount++
it "ignores invisibles even if editor.showInvisibles is true", ->
atom.config.set('editor.showInvisibles', true)
invisibles = editor.tokenizedLineForScreenRow(0).invisibles
expect(invisibles).toBe(null)
it "does not open file in pending state by default", ->
expect(editor.isPending()).toBe false
describe "when the editor is constructed with the grammar option set", ->
beforeEach ->
atom.workspace.destroyActivePane()
waitsForPromise ->
atom.packages.activatePackage('language-coffee-script')
it "opens file in pending state if 'pending' option is true", ->
expect(editor1.isPending()).toBe true
waitsForPromise ->
atom.workspace.open('sample.js', grammar: atom.grammars.grammarForScopeName('source.coffee')).then (o) -> editor = o
it "terminates pending state if ::terminatePendingState is invoked", ->
editor1.terminatePendingState()
it "sets the grammar", ->
expect(editor.getGrammar().name).toBe 'CoffeeScript'
expect(editor1.isPending()).toBe false
expect(eventCount).toBe 1
it "terminates pending state when buffer is changed", ->
editor1.insertText('I\'ll be back!')
advanceClock(editor1.getBuffer().stoppedChangingDelay)
expect(editor1.isPending()).toBe false
expect(eventCount).toBe 1
it "only calls terminate handler once when text is modified twice", ->
editor1.insertText('Some text')
advanceClock(editor1.getBuffer().stoppedChangingDelay)
editor1.save()
editor1.insertText('More text')
advanceClock(editor1.getBuffer().stoppedChangingDelay)
expect(editor1.isPending()).toBe false
expect(eventCount).toBe 1
it "only calls terminate handler once when terminatePendingState is called twice", ->
editor1.terminatePendingState()
editor1.terminatePendingState()
expect(editor1.isPending()).toBe false
expect(eventCount).toBe 1
describe "::getElement", ->
it "returns an element", ->
expect(editor.getElement() instanceof HTMLElement).toBe(true)

View File

@@ -202,8 +202,7 @@ describe "TokenizedBuffer", ->
expect(tokenizedBuffer.firstInvalidRow()).toBe 3
advanceClock()
# we discover that row 2 starts a foldable region when line 3 gets tokenized
expect(changeHandler).toHaveBeenCalledWith(start: 2, end: 7, delta: 0)
expect(changeHandler).toHaveBeenCalledWith(start: 3, end: 7, delta: 0)
expect(tokenizedBuffer.firstInvalidRow()).toBe 8
describe "when there is a buffer change surrounding an invalid row", ->
@@ -253,7 +252,7 @@ describe "TokenizedBuffer", ->
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
delete event.bufferChange
expect(event).toEqual(start: 1, end: 2, delta: 0)
expect(event).toEqual(start: 2, end: 2, delta: 0)
changeHandler.reset()
advanceClock()
@@ -263,8 +262,7 @@ describe "TokenizedBuffer", ->
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
delete event.bufferChange
# we discover that row 2 starts a foldable region when line 3 gets tokenized
expect(event).toEqual(start: 2, end: 5, delta: 0)
expect(event).toEqual(start: 3, end: 5, delta: 0)
it "resumes highlighting with the state of the previous line", ->
buffer.insert([0, 0], '/*')
@@ -292,7 +290,7 @@ describe "TokenizedBuffer", ->
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
delete event.bufferChange
expect(event).toEqual(start: 0, end: 3, delta: -2) # starts at 0 because foldable on row 0 becomes false
expect(event).toEqual(start: 1, end: 3, delta: -2)
describe "when the change invalidates the tokenization of subsequent lines", ->
it "schedules the invalidated lines to be tokenized in the background", ->
@@ -305,7 +303,7 @@ describe "TokenizedBuffer", ->
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
delete event.bufferChange
expect(event).toEqual(start: 1, end: 3, delta: -1)
expect(event).toEqual(start: 2, end: 3, delta: -1)
changeHandler.reset()
advanceClock()
@@ -314,8 +312,7 @@ describe "TokenizedBuffer", ->
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
delete event.bufferChange
# we discover that row 2 starts a foldable region when line 3 gets tokenized
expect(event).toEqual(start: 2, end: 4, delta: 0)
expect(event).toEqual(start: 3, end: 4, delta: 0)
describe "when lines are both updated and inserted", ->
it "updates tokens to reflect the change", ->
@@ -339,7 +336,7 @@ describe "TokenizedBuffer", ->
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
delete event.bufferChange
expect(event).toEqual(start: 0, end: 2, delta: 2) # starts at 0 because .foldable becomes false on row 0
expect(event).toEqual(start: 1, end: 2, delta: 2)
describe "when the change invalidates the tokenization of subsequent lines", ->
it "schedules the invalidated lines to be tokenized in the background", ->
@@ -350,7 +347,7 @@ describe "TokenizedBuffer", ->
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
delete event.bufferChange
expect(event).toEqual(start: 1, end: 2, delta: 2)
expect(event).toEqual(start: 2, end: 2, delta: 2)
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.js']
expect(tokenizedBuffer.tokenizedLineForRow(3).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
expect(tokenizedBuffer.tokenizedLineForRow(4).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
@@ -894,7 +891,7 @@ describe "TokenizedBuffer", ->
buffer.setTextInRange([[7, 0], [8, 65]], ' ok')
delete changeHandler.argsForCall[0][0].bufferChange
expect(changeHandler).toHaveBeenCalledWith(start: 4, end: 10, delta: -1) # starts at row 4 because it became foldable
expect(changeHandler).toHaveBeenCalledWith(start: 5, end: 10, delta: -1)
expect(tokenizedBuffer.tokenizedLineForRow(5).indentLevel).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(6).indentLevel).toBe 2
@@ -903,7 +900,7 @@ describe "TokenizedBuffer", ->
expect(tokenizedBuffer.tokenizedLineForRow(9).indentLevel).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(10).indentLevel).toBe 2 # }
describe ".foldable on tokenized lines", ->
describe "::isFoldableAtRow(row)", ->
changes = null
beforeEach ->
@@ -915,74 +912,66 @@ describe "TokenizedBuffer", ->
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
fullyTokenize(tokenizedBuffer)
tokenizedBuffer.onDidChange (change) ->
delete change.bufferChange
changes.push(change)
it "sets .foldable to true on the first line of multi-line comments", ->
expect(tokenizedBuffer.tokenizedLineForRow(0).foldable).toBe true
expect(tokenizedBuffer.tokenizedLineForRow(1).foldable).toBe false
expect(tokenizedBuffer.tokenizedLineForRow(2).foldable).toBe false
expect(tokenizedBuffer.tokenizedLineForRow(3).foldable).toBe true # because of indent
expect(tokenizedBuffer.tokenizedLineForRow(13).foldable).toBe true
expect(tokenizedBuffer.tokenizedLineForRow(14).foldable).toBe false
expect(tokenizedBuffer.tokenizedLineForRow(15).foldable).toBe false
expect(tokenizedBuffer.tokenizedLineForRow(16).foldable).toBe false
it "includes the first line of multi-line comments", ->
expect(tokenizedBuffer.isFoldableAtRow(0)).toBe true
expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false
expect(tokenizedBuffer.isFoldableAtRow(2)).toBe false
expect(tokenizedBuffer.isFoldableAtRow(3)).toBe true # because of indent
expect(tokenizedBuffer.isFoldableAtRow(13)).toBe true
expect(tokenizedBuffer.isFoldableAtRow(14)).toBe false
expect(tokenizedBuffer.isFoldableAtRow(15)).toBe false
expect(tokenizedBuffer.isFoldableAtRow(16)).toBe false
buffer.insert([0, Infinity], '\n')
expect(changes).toEqual [{start: 0, end: 1, delta: 1}]
expect(tokenizedBuffer.tokenizedLineForRow(0).foldable).toBe false
expect(tokenizedBuffer.tokenizedLineForRow(1).foldable).toBe false
expect(tokenizedBuffer.tokenizedLineForRow(2).foldable).toBe true
expect(tokenizedBuffer.tokenizedLineForRow(3).foldable).toBe false
expect(tokenizedBuffer.isFoldableAtRow(0)).toBe false
expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false
expect(tokenizedBuffer.isFoldableAtRow(2)).toBe true
expect(tokenizedBuffer.isFoldableAtRow(3)).toBe false
changes = []
buffer.undo()
expect(changes).toEqual [{start: 0, end: 2, delta: -1}]
expect(tokenizedBuffer.tokenizedLineForRow(0).foldable).toBe true
expect(tokenizedBuffer.tokenizedLineForRow(1).foldable).toBe false
expect(tokenizedBuffer.tokenizedLineForRow(2).foldable).toBe false
expect(tokenizedBuffer.tokenizedLineForRow(3).foldable).toBe true # because of indent
it "sets .foldable to true on non-comment lines that precede an increase in indentation", ->
expect(tokenizedBuffer.isFoldableAtRow(0)).toBe true
expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false
expect(tokenizedBuffer.isFoldableAtRow(2)).toBe false
expect(tokenizedBuffer.isFoldableAtRow(3)).toBe true # because of indent
it "includes non-comment lines that precede an increase in indentation", ->
buffer.insert([2, 0], ' ') # commented lines preceding an indent aren't foldable
expect(tokenizedBuffer.tokenizedLineForRow(1).foldable).toBe false
expect(tokenizedBuffer.tokenizedLineForRow(2).foldable).toBe false
expect(tokenizedBuffer.tokenizedLineForRow(3).foldable).toBe true
expect(tokenizedBuffer.tokenizedLineForRow(4).foldable).toBe true
expect(tokenizedBuffer.tokenizedLineForRow(5).foldable).toBe false
expect(tokenizedBuffer.tokenizedLineForRow(6).foldable).toBe false
expect(tokenizedBuffer.tokenizedLineForRow(7).foldable).toBe true
expect(tokenizedBuffer.tokenizedLineForRow(8).foldable).toBe false
changes = []
expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false
expect(tokenizedBuffer.isFoldableAtRow(2)).toBe false
expect(tokenizedBuffer.isFoldableAtRow(3)).toBe true
expect(tokenizedBuffer.isFoldableAtRow(4)).toBe true
expect(tokenizedBuffer.isFoldableAtRow(5)).toBe false
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe false
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe true
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false
buffer.insert([7, 0], ' ')
expect(changes).toEqual [{start: 6, end: 7, delta: 0}]
expect(tokenizedBuffer.tokenizedLineForRow(6).foldable).toBe true
expect(tokenizedBuffer.tokenizedLineForRow(7).foldable).toBe false
expect(tokenizedBuffer.tokenizedLineForRow(8).foldable).toBe false
changes = []
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe true
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe false
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false
buffer.undo()
expect(changes).toEqual [{start: 6, end: 7, delta: 0}]
expect(tokenizedBuffer.tokenizedLineForRow(6).foldable).toBe false
expect(tokenizedBuffer.tokenizedLineForRow(7).foldable).toBe true
expect(tokenizedBuffer.tokenizedLineForRow(8).foldable).toBe false
changes = []
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe false
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe true
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false
buffer.insert([7, 0], " \n x\n")
expect(changes).toEqual [{start: 6, end: 7, delta: 2}]
expect(tokenizedBuffer.tokenizedLineForRow(6).foldable).toBe true
expect(tokenizedBuffer.tokenizedLineForRow(7).foldable).toBe false
expect(tokenizedBuffer.tokenizedLineForRow(8).foldable).toBe false
changes = []
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe true
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe false
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false
buffer.insert([9, 0], " ")
expect(changes).toEqual [{start: 9, end: 9, delta: 0}]
expect(tokenizedBuffer.tokenizedLineForRow(6).foldable).toBe true
expect(tokenizedBuffer.tokenizedLineForRow(7).foldable).toBe false
expect(tokenizedBuffer.tokenizedLineForRow(8).foldable).toBe false
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe true
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe false
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false
describe "when the buffer is configured with the null grammar", ->
it "uses the placeholder tokens and does not actually tokenize using the grammar", ->

View File

@@ -28,6 +28,12 @@ describe "TooltipManager", ->
hover element, ->
expect(document.body.querySelector(".tooltip")).toHaveText("Title")
it "creates a tooltip immediately if the trigger type is manual", ->
disposable = manager.add element, title: "Title", trigger: "manual"
expect(document.body.querySelector(".tooltip")).toHaveText("Title")
disposable.dispose()
expect(document.body.querySelector(".tooltip")).toBeNull()
it "allows jQuery elements to be passed as the target", ->
element2 = document.createElement('div')
jasmine.attachToDOM(element2)

View File

@@ -23,6 +23,15 @@ describe "ViewRegistry", ->
component = new TestComponent
expect(registry.getView(component)).toBe component.element
describe "when passed an object with a getElement function", ->
it "returns the return value of getElement if it's an instance of HTMLElement", ->
class TestComponent
getElement: ->
@myElement ?= document.createElement('div')
component = new TestComponent
expect(registry.getView(component)).toBe component.myElement
describe "when passed a model object", ->
describe "when a view provider is registered matching the object's constructor", ->
it "constructs a view element and assigns the model on it", ->

View File

@@ -4,7 +4,7 @@ fs = require 'fs-plus'
temp = require 'temp'
TextEditor = require '../src/text-editor'
WindowEventHandler = require '../src/window-event-handler'
ipc = require 'ipc'
{ipcRenderer} = require 'electron'
describe "WindowEventHandler", ->
[projectPath, windowEventHandler] = []
@@ -53,7 +53,7 @@ describe "WindowEventHandler", ->
describe "beforeunload event", ->
beforeEach ->
jasmine.unspy(TextEditor.prototype, "shouldPromptToSave")
spyOn(ipc, 'send')
spyOn(ipcRenderer, 'send')
describe "when pane items are modified", ->
editor = null
@@ -65,13 +65,13 @@ describe "WindowEventHandler", ->
spyOn(atom.workspace, 'confirmClose').andReturn(true)
window.dispatchEvent(new CustomEvent('beforeunload'))
expect(atom.workspace.confirmClose).toHaveBeenCalled()
expect(ipc.send).not.toHaveBeenCalledWith('did-cancel-window-unload')
expect(ipcRenderer.send).not.toHaveBeenCalledWith('did-cancel-window-unload')
it "cancels the unload if the user selects cancel", ->
spyOn(atom.workspace, 'confirmClose').andReturn(false)
window.dispatchEvent(new CustomEvent('beforeunload'))
expect(atom.workspace.confirmClose).toHaveBeenCalled()
expect(ipc.send).toHaveBeenCalledWith('did-cancel-window-unload')
expect(ipcRenderer.send).toHaveBeenCalledWith('did-cancel-window-unload')
describe "when a link is clicked", ->
it "opens the http/https links in an external application", ->

View File

@@ -1,4 +1,4 @@
ipc = require 'ipc'
{ipcRenderer} = require 'electron'
path = require 'path'
temp = require('temp').track()
@@ -127,35 +127,35 @@ describe "WorkspaceElement", ->
describe "the 'window:run-package-specs' command", ->
it "runs the package specs for the active item's project path, or the first project path", ->
workspaceElement = atom.views.getView(atom.workspace)
spyOn(ipc, 'send')
spyOn(ipcRenderer, 'send')
# No project paths. Don't try to run specs.
atom.commands.dispatch(workspaceElement, "window:run-package-specs")
expect(ipc.send).not.toHaveBeenCalledWith("run-package-specs")
expect(ipcRenderer.send).not.toHaveBeenCalledWith("run-package-specs")
projectPaths = [temp.mkdirSync("dir1-"), temp.mkdirSync("dir2-")]
atom.project.setPaths(projectPaths)
# No active item. Use first project directory.
atom.commands.dispatch(workspaceElement, "window:run-package-specs")
expect(ipc.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec"))
ipc.send.reset()
expect(ipcRenderer.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec"))
ipcRenderer.send.reset()
# Active item doesn't implement ::getPath(). Use first project directory.
item = document.createElement("div")
atom.workspace.getActivePane().activateItem(item)
atom.commands.dispatch(workspaceElement, "window:run-package-specs")
expect(ipc.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec"))
ipc.send.reset()
expect(ipcRenderer.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec"))
ipcRenderer.send.reset()
# Active item has no path. Use first project directory.
item.getPath = -> null
atom.commands.dispatch(workspaceElement, "window:run-package-specs")
expect(ipc.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec"))
ipc.send.reset()
expect(ipcRenderer.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec"))
ipcRenderer.send.reset()
# Active item has path. Use project path for item path.
item.getPath = -> path.join(projectPaths[1], "a-file.txt")
atom.commands.dispatch(workspaceElement, "window:run-package-specs")
expect(ipc.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[1], "spec"))
ipc.send.reset()
expect(ipcRenderer.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[1], "spec"))
ipcRenderer.send.reset()

View File

@@ -22,11 +22,11 @@ describe "Workspace", ->
describe "serialization", ->
simulateReload = ->
workspaceState = atom.workspace.serialize()
projectState = atom.project.serialize()
projectState = atom.project.serialize({isUnloading: true})
atom.workspace.destroy()
atom.project.destroy()
atom.project = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm.bind(atom)})
atom.project.deserialize(projectState, atom.deserializers)
atom.project.deserialize(projectState)
atom.workspace = new Workspace({
config: atom.config, project: atom.project, packageManager: atom.packages,
grammarRegistry: atom.grammars, deserializerManager: atom.deserializers,
@@ -585,6 +585,72 @@ describe "Workspace", ->
open = -> workspace.open('file1', workspace.getActivePane())
expect(open).toThrow()
describe "when the file is already open in pending state", ->
it "should terminate the pending state", ->
editor = null
pane = null
waitsForPromise ->
atom.workspace.open('sample.js', pending: true).then (o) ->
editor = o
pane = atom.workspace.getActivePane()
runs ->
expect(pane.getPendingItem()).toEqual editor
waitsForPromise ->
atom.workspace.open('sample.js')
runs ->
expect(pane.getPendingItem()).toBeNull()
describe "when opening will switch from a pending tab to a permanent tab", ->
it "keeps the pending tab open", ->
editor1 = null
editor2 = null
waitsForPromise ->
atom.workspace.open('sample.txt').then (o) ->
editor1 = o
waitsForPromise ->
atom.workspace.open('sample2.txt', pending: true).then (o) ->
editor2 = o
runs ->
pane = atom.workspace.getActivePane()
pane.activateItem(editor1)
expect(pane.getItems().length).toBe 2
expect(pane.getItems()).toEqual [editor1, editor2]
describe "when replacing a pending item which is the last item in a second pane", ->
it "does not destory the pane even if core.destroyEmptyPanes is on", ->
atom.config.set('core.destroyEmptyPanes', true)
editor1 = null
editor2 = null
leftPane = atom.workspace.getActivePane()
rightPane = null
waitsForPromise ->
atom.workspace.open('sample.js', pending: true, split: 'right').then (o) ->
editor1 = o
rightPane = atom.workspace.getActivePane()
spyOn rightPane, "destroyed"
runs ->
expect(leftPane).not.toBe rightPane
expect(atom.workspace.getActivePane()).toBe rightPane
expect(atom.workspace.getActivePane().getItems().length).toBe 1
expect(rightPane.getPendingItem()).toBe editor1
waitsForPromise ->
atom.workspace.open('sample.txt', pending: true).then (o) ->
editor2 = o
runs ->
expect(rightPane.getPendingItem()).toBe editor2
expect(rightPane.destroyed.callCount).toBe 0
describe "::reopenItem()", ->
it "opens the uri associated with the last closed pane that isn't currently open", ->
pane = workspace.getActivePane()
@@ -1532,3 +1598,15 @@ describe "Workspace", ->
atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow()
expect(atom.close).toHaveBeenCalled()
describe "when the core.allowPendingPaneItems option is falsey", ->
it "does not open item with `pending: true` option as pending", ->
pane = null
atom.config.set('core.allowPendingPaneItems', false)
waitsForPromise ->
atom.workspace.open('sample.js', pending: true).then ->
pane = atom.workspace.getActivePane()
runs ->
expect(pane.getPendingItem()).toBeFalsy()

View File

@@ -1,69 +1,73 @@
_ = require 'underscore-plus'
ipc = require 'ipc'
remote = require 'remote'
shell = require 'shell'
webFrame = require 'web-frame'
{ipcRenderer, remote, shell, webFrame} = require 'electron'
ipcHelpers = require './ipc-helpers'
{Disposable} = require 'event-kit'
{getWindowLoadSettings, setWindowLoadSettings} = require './window-load-settings-helpers'
module.exports =
class ApplicationDelegate
open: (params) ->
ipc.send('open', params)
ipcRenderer.send('open', params)
pickFolder: (callback) ->
responseChannel = "atom-pick-folder-response"
ipc.on responseChannel, (path) ->
ipc.removeAllListeners(responseChannel)
ipcRenderer.on responseChannel, (event, path) ->
ipcRenderer.removeAllListeners(responseChannel)
callback(path)
ipc.send("pick-folder", responseChannel)
ipcRenderer.send("pick-folder", responseChannel)
getCurrentWindow: ->
remote.getCurrentWindow()
closeWindow: ->
ipc.send("call-window-method", "close")
ipcRenderer.send("call-window-method", "close")
getTemporaryWindowState: ->
ipcHelpers.call('get-temporary-window-state')
setTemporaryWindowState: (state) ->
ipcHelpers.call('set-temporary-window-state', state)
getWindowSize: ->
[width, height] = remote.getCurrentWindow().getSize()
{width, height}
setWindowSize: (width, height) ->
remote.getCurrentWindow().setSize(width, height)
ipcHelpers.call('set-window-size', width, height)
getWindowPosition: ->
[x, y] = remote.getCurrentWindow().getPosition()
{x, y}
setWindowPosition: (x, y) ->
ipc.send("call-window-method", "setPosition", x, y)
ipcHelpers.call('set-window-position', x, y)
centerWindow: ->
ipc.send("call-window-method", "center")
ipcHelpers.call('center-window')
focusWindow: ->
ipc.send("call-window-method", "focus")
ipcHelpers.call('focus-window')
showWindow: ->
ipc.send("call-window-method", "show")
ipcHelpers.call('show-window')
hideWindow: ->
ipc.send("call-window-method", "hide")
ipcHelpers.call('hide-window')
restartWindow: ->
ipc.send("call-window-method", "restart")
reloadWindow: ->
ipcRenderer.send("call-window-method", "reload")
isWindowMaximized: ->
remote.getCurrentWindow().isMaximized()
maximizeWindow: ->
ipc.send("call-window-method", "maximize")
ipcRenderer.send("call-window-method", "maximize")
isWindowFullScreen: ->
remote.getCurrentWindow().isFullScreen()
setWindowFullScreen: (fullScreen=false) ->
ipc.send("call-window-method", "setFullScreen", fullScreen)
ipcRenderer.send("call-window-method", "setFullScreen", fullScreen)
openWindowDevTools: ->
new Promise (resolve) ->
@@ -75,7 +79,7 @@ class ApplicationDelegate
resolve()
else
remote.getCurrentWindow().once("devtools-opened", -> resolve())
ipc.send("call-window-method", "openDevTools")
ipcRenderer.send("call-window-method", "openDevTools")
closeWindowDevTools: ->
new Promise (resolve) ->
@@ -87,7 +91,7 @@ class ApplicationDelegate
resolve()
else
remote.getCurrentWindow().once("devtools-closed", -> resolve())
ipc.send("call-window-method", "closeDevTools")
ipcRenderer.send("call-window-method", "closeDevTools")
toggleWindowDevTools: ->
new Promise (resolve) =>
@@ -101,16 +105,16 @@ class ApplicationDelegate
@openWindowDevTools().then(resolve)
executeJavaScriptInWindowDevTools: (code) ->
ipc.send("call-window-method", "executeJavaScriptInDevTools", code)
ipcRenderer.send("execute-javascript-in-dev-tools", code)
setWindowDocumentEdited: (edited) ->
ipc.send("call-window-method", "setDocumentEdited", edited)
ipcRenderer.send("call-window-method", "setDocumentEdited", edited)
setRepresentedFilename: (filename) ->
ipc.send("call-window-method", "setRepresentedFilename", filename)
ipcRenderer.send("call-window-method", "setRepresentedFilename", filename)
addRecentDocument: (filename) ->
ipc.send("add-recent-document", filename)
ipcRenderer.send("add-recent-document", filename)
setRepresentedDirectoryPaths: (paths) ->
loadSettings = getWindowLoadSettings()
@@ -118,14 +122,13 @@ class ApplicationDelegate
setWindowLoadSettings(loadSettings)
setAutoHideWindowMenuBar: (autoHide) ->
ipc.send("call-window-method", "setAutoHideMenuBar", autoHide)
ipcRenderer.send("call-window-method", "setAutoHideMenuBar", autoHide)
setWindowMenuBarVisibility: (visible) ->
remote.getCurrentWindow().setMenuBarVisibility(visible)
getPrimaryDisplayWorkAreaSize: ->
screen = remote.require 'screen'
screen.getPrimaryDisplay().workAreaSize
remote.screen.getPrimaryDisplay().workAreaSize
confirm: ({message, detailedMessage, buttons}) ->
buttons ?= {}
@@ -134,8 +137,7 @@ class ApplicationDelegate
else
buttonLabels = Object.keys(buttons)
dialog = remote.require('dialog')
chosen = dialog.showMessageBox(remote.getCurrentWindow(), {
chosen = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info'
message: message
detail: detailedMessage
@@ -157,45 +159,88 @@ class ApplicationDelegate
params = _.clone(params)
params.title ?= 'Save File'
params.defaultPath ?= getWindowLoadSettings().initialPaths[0]
dialog = remote.require('dialog')
dialog.showSaveDialog remote.getCurrentWindow(), params
remote.dialog.showSaveDialog remote.getCurrentWindow(), params
playBeepSound: ->
shell.beep()
onDidOpenLocations: (callback) ->
outerCallback = (message, detail) ->
if message is 'open-locations'
callback(detail)
outerCallback = (event, message, detail) ->
callback(detail) if message is 'open-locations'
ipc.on('message', outerCallback)
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipc.removeListener('message', outerCallback)
ipcRenderer.removeListener('message', outerCallback)
onUpdateAvailable: (callback) ->
outerCallback = (message, detail) ->
if message is 'update-available'
callback(detail)
outerCallback = (event, message, detail) ->
# TODO: Yes, this is strange that `onUpdateAvailable` is listening for
# `did-begin-downloading-update`. We currently have no mechanism to know
# if there is an update, so begin of downloading is a good proxy.
callback(detail) if message is 'did-begin-downloading-update'
ipc.on('message', outerCallback)
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipc.removeListener('message', outerCallback)
ipcRenderer.removeListener('message', outerCallback)
onDidBeginDownloadingUpdate: (callback) ->
@onUpdateAvailable(callback)
onDidBeginCheckingForUpdate: (callback) ->
outerCallback = (event, message, detail) ->
callback(detail) if message is 'checking-for-update'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onDidCompleteDownloadingUpdate: (callback) ->
outerCallback = (event, message, detail) ->
# TODO: We could rename this event to `did-complete-downloading-update`
callback(detail) if message is 'update-available'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onUpdateNotAvailable: (callback) ->
outerCallback = (event, message, detail) ->
callback(detail) if message is 'update-not-available'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onApplicationMenuCommand: (callback) ->
ipc.on('command', callback)
outerCallback = (event, args...) ->
callback(args...)
ipcRenderer.on('command', outerCallback)
new Disposable ->
ipc.removeListener('command', callback)
ipcRenderer.removeListener('command', outerCallback)
onContextMenuCommand: (callback) ->
ipc.on('context-command', callback)
outerCallback = (event, args...) ->
callback(args...)
ipcRenderer.on('context-command', outerCallback)
new Disposable ->
ipc.removeListener('context-command', callback)
ipcRenderer.removeListener('context-command', outerCallback)
didCancelWindowUnload: ->
ipc.send('did-cancel-window-unload')
ipcRenderer.send('did-cancel-window-unload')
openExternal: (url) ->
shell.openExternal(url)
disablePinchToZoom: ->
webFrame.setZoomLevelLimits(1, 1)
checkForUpdate: ->
ipcRenderer.send('check-for-update')
restartAndInstallUpdate: ->
ipcRenderer.send('install-update')
getAutoUpdateManagerState: ->
ipcRenderer.sendSync('get-auto-update-manager-state')

View File

@@ -1,17 +1,17 @@
crypto = require 'crypto'
path = require 'path'
ipc = require 'ipc'
{ipcRenderer} = require 'electron'
_ = require 'underscore-plus'
{deprecate} = require 'grim'
{CompositeDisposable, Emitter} = require 'event-kit'
{CompositeDisposable, Disposable, Emitter} = require 'event-kit'
fs = require 'fs-plus'
{mapSourcePosition} = require 'source-map-support'
Model = require './model'
WindowEventHandler = require './window-event-handler'
StylesElement = require './styles-element'
StorageFolder = require './storage-folder'
{getWindowLoadSettings} = require './window-load-settings-helpers'
StateStore = require './state-store'
{getWindowLoadSettings, setWindowLoadSettings} = require './window-load-settings-helpers'
registerDefaultCommands = require './register-default-commands'
DeserializerManager = require './deserializer-manager'
@@ -40,6 +40,8 @@ Project = require './project'
TextEditor = require './text-editor'
TextBuffer = require 'text-buffer'
Gutter = require './gutter'
TextEditorRegistry = require './text-editor-registry'
AutoUpdateManager = require './auto-update-manager'
WorkspaceElement = require './workspace-element'
PanelContainerElement = require './panel-container-element'
@@ -111,6 +113,14 @@ class AtomEnvironment extends Model
# Public: A {Workspace} instance
workspace: null
# Public: A {TextEditorRegistry} instance
textEditors: null
# Private: An {AutoUpdateManager} instance
autoUpdater: null
saveStateDebounceInterval: 1000
###
Section: Construction and Destruction
###
@@ -119,14 +129,17 @@ class AtomEnvironment extends Model
constructor: (params={}) ->
{@blobStore, @applicationDelegate, @window, @document, configDirPath, @enablePersistence, onlyLoadBaseStyleSheets} = params
@state = {version: @constructor.version}
@unloaded = false
@loadTime = null
{devMode, safeMode, resourcePath} = @getLoadSettings()
{devMode, safeMode, resourcePath, clearWindowState} = @getLoadSettings()
@emitter = new Emitter
@disposables = new CompositeDisposable
@stateStore = new StateStore('AtomEnvironments', 1)
@stateStore.clear() if clearWindowState
@deserializers = new DeserializerManager(this)
@deserializeTimings = {}
@@ -179,6 +192,9 @@ class AtomEnvironment extends Model
})
@themes.workspace = @workspace
@textEditors = new TextEditorRegistry
@autoUpdater = new AutoUpdateManager({@applicationDelegate})
@config.load()
@themes.loadBaseStylesheets()
@@ -200,19 +216,29 @@ class AtomEnvironment extends Model
@registerDefaultViewProviders()
@installUncaughtErrorHandler()
@attachSaveStateListeners()
@installWindowEventHandler()
@observeAutoHideMenuBar()
checkPortableHomeWritable = ->
responseChannel = "check-portable-home-writable-response"
ipc.on responseChannel, (response) ->
ipc.removeAllListeners(responseChannel)
ipcRenderer.on responseChannel, (event, response) ->
ipcRenderer.removeAllListeners(responseChannel)
atom.notifications.addWarning("#{response.message.replace(/([\\\.+\\-_#!])/g, '\\$1')}") if not response.writable
ipc.send('check-portable-home-writable', responseChannel)
ipcRenderer.send('check-portable-home-writable', responseChannel)
checkPortableHomeWritable()
attachSaveStateListeners: ->
saveState = => @saveState({isUnloading: false}) unless @unloaded
debouncedSaveState = _.debounce(saveState, @saveStateDebounceInterval)
@document.addEventListener('mousedown', debouncedSaveState, true)
@document.addEventListener('keydown', debouncedSaveState, true)
@disposables.add new Disposable =>
@document.removeEventListener('mousedown', debouncedSaveState, true)
@document.removeEventListener('keydown', debouncedSaveState, true)
setConfigSchema: ->
@config.setSchema null, {type: 'object', properties: _.clone(require('./config-schema'))}
@@ -241,8 +267,6 @@ class AtomEnvironment extends Model
new PaneAxisElement().initialize(model, env)
@views.addViewProvider Pane, (model, env) ->
new PaneElement().initialize(model, env)
@views.addViewProvider TextEditor, (model, env) ->
new TextEditorElement().initialize(model, env)
@views.addViewProvider(Gutter, createGutterView)
registerDefaultOpeners: ->
@@ -302,9 +326,6 @@ class AtomEnvironment extends Model
@views.clear()
@registerDefaultViewProviders()
@state.packageStates = {}
delete @state.workspace
destroy: ->
return if not @project
@@ -317,6 +338,7 @@ class AtomEnvironment extends Model
@commands.clear()
@stylesElement.remove()
@config.unobserveUserConfig()
@autoUpdater.destroy()
@uninstallWindowEventHandler()
@@ -384,12 +406,27 @@ class AtomEnvironment extends Model
inSpecMode: ->
@specMode ?= @getLoadSettings().isSpec
# Returns a {Boolean} indicating whether this the first time the window's been
# loaded.
isFirstLoad: ->
@firstLoad ?= @getLoadSettings().firstLoad
# Public: Get the version of the Atom application.
#
# Returns the version text {String}.
getVersion: ->
@appVersion ?= @getLoadSettings().appVersion
# Returns the release channel as a {String}. Will return one of `'dev', 'beta', 'stable'`
getReleaseChannel: ->
version = @getVersion()
if version.indexOf('beta') > -1
'beta'
else if version.indexOf('dev') > -1
'dev'
else
'stable'
# Public: Returns a {Boolean} that is `true` if the current version is an official release.
isReleasedVersion: ->
not /\w{7}/.test(@getVersion()) # Check if the release is a 7-character SHA prefix
@@ -492,7 +529,7 @@ class AtomEnvironment extends Model
# Extended: Reload the current window.
reload: ->
@applicationDelegate.restartWindow()
@applicationDelegate.reloadWindow()
# Extended: Returns a {Boolean} that is `true` if the current window is maximized.
isMaximized: ->
@@ -519,16 +556,18 @@ class AtomEnvironment extends Model
# Restore the window to its previous dimensions and show it.
#
# Also restores the full screen and maximized state on the next tick to
# Restores the full screen and maximized state after the window has resized to
# prevent resize glitches.
displayWindow: ->
dimensions = @restoreWindowDimensions()
@show()
@focus()
setImmediate =>
@setFullScreen(true) if @workspace?.fullScreen
@maximize() if dimensions?.maximized and process.platform isnt 'darwin'
@restoreWindowDimensions().then =>
steps = [
@restoreWindowBackground(),
@show(),
@focus()
]
steps.push(@setFullScreen(true)) if @windowDimensions?.fullScreen
steps.push(@maximize()) if @windowDimensions?.maximized and process.platform isnt 'darwin'
Promise.all(steps)
# Get the dimensions of this window.
#
@@ -556,22 +595,24 @@ class AtomEnvironment extends Model
# * `width` The new width.
# * `height` The new height.
setWindowDimensions: ({x, y, width, height}) ->
steps = []
if width? and height?
@setSize(width, height)
steps.push(@setSize(width, height))
if x? and y?
@setPosition(x, y)
steps.push(@setPosition(x, y))
else
@center()
steps.push(@center())
Promise.all(steps)
# Returns true if the dimensions are useable, false if they should be ignored.
# Work around for https://github.com/atom/atom-shell/issues/473
isValidDimensions: ({x, y, width, height}={}) ->
width > 0 and height > 0 and x + width > 0 and y + height > 0
storeDefaultWindowDimensions: ->
dimensions = @getWindowDimensions()
if @isValidDimensions(dimensions)
localStorage.setItem("defaultWindowDimensions", JSON.stringify(dimensions))
storeWindowDimensions: ->
@windowDimensions = @getWindowDimensions()
if @isValidDimensions(@windowDimensions)
localStorage.setItem("defaultWindowDimensions", JSON.stringify(@windowDimensions))
getDefaultWindowDimensions: ->
{windowDimensions} = @getLoadSettings()
@@ -591,15 +632,16 @@ class AtomEnvironment extends Model
{x: 0, y: 0, width: Math.min(1024, width), height}
restoreWindowDimensions: ->
dimensions = @state.windowDimensions
unless @isValidDimensions(dimensions)
dimensions = @getDefaultWindowDimensions()
@setWindowDimensions(dimensions)
dimensions
unless @windowDimensions? and @isValidDimensions(@windowDimensions)
@windowDimensions = @getDefaultWindowDimensions()
@setWindowDimensions(@windowDimensions).then -> @windowDimensions
storeWindowDimensions: ->
dimensions = @getWindowDimensions()
@state.windowDimensions = dimensions if @isValidDimensions(dimensions)
restoreWindowBackground: ->
if backgroundColor = window.localStorage.getItem('atom:window-background-color')
@backgroundStylesheet = document.createElement('style')
@backgroundStylesheet.type = 'text/css'
@backgroundStylesheet.innerText = 'html, body { background: ' + backgroundColor + ' !important; }'
document.head.appendChild(@backgroundStylesheet)
storeWindowBackground: ->
return if @inSpecMode()
@@ -610,44 +652,57 @@ class AtomEnvironment extends Model
# Call this method when establishing a real application window.
startEditorWindow: ->
@commandInstaller.installAtomCommand false, (error) ->
console.warn error.message if error?
@commandInstaller.installApmCommand false, (error) ->
console.warn error.message if error?
@loadState().then (state) =>
@windowDimensions = state?.windowDimensions
@displayWindow().then =>
@commandInstaller.installAtomCommand false, (error) ->
console.warn error.message if error?
@commandInstaller.installApmCommand false, (error) ->
console.warn error.message if error?
@disposables.add(@applicationDelegate.onDidOpenLocations(@openLocations.bind(this)))
@disposables.add(@applicationDelegate.onApplicationMenuCommand(@dispatchApplicationMenuCommand.bind(this)))
@disposables.add(@applicationDelegate.onContextMenuCommand(@dispatchContextMenuCommand.bind(this)))
@listenForUpdates()
@disposables.add(@applicationDelegate.onDidOpenLocations(@openLocations.bind(this)))
@disposables.add(@applicationDelegate.onApplicationMenuCommand(@dispatchApplicationMenuCommand.bind(this)))
@disposables.add(@applicationDelegate.onContextMenuCommand(@dispatchContextMenuCommand.bind(this)))
@listenForUpdates()
@registerDefaultTargetForKeymaps()
@registerDefaultTargetForKeymaps()
@packages.loadPackages()
@loadStateSync()
@document.body.appendChild(@views.getView(@workspace))
@packages.loadPackages()
@watchProjectPath()
startTime = Date.now()
@deserialize(state) if state?
@deserializeTimings.atom = Date.now() - startTime
@packages.activate()
@keymaps.loadUserKeymap()
@requireUserInitScript() unless @getLoadSettings().safeMode
@document.body.appendChild(@views.getView(@workspace))
@backgroundStylesheet?.remove()
@menu.update()
@watchProjectPaths()
@openInitialEmptyEditorIfNecessary()
@packages.activate()
@keymaps.loadUserKeymap()
@requireUserInitScript() unless @getLoadSettings().safeMode
@menu.update()
@openInitialEmptyEditorIfNecessary()
serialize: (options) ->
version: @constructor.version
project: @project.serialize(options)
workspace: @workspace.serialize()
packageStates: @packages.serialize()
grammars: {grammarOverridesByPath: @grammars.grammarOverridesByPath}
fullScreen: @isFullScreen()
windowDimensions: @windowDimensions
unloadEditorWindow: ->
return if not @project
@saveState({isUnloading: true})
@storeWindowBackground()
@state.grammars = {grammarOverridesByPath: @grammars.grammarOverridesByPath}
@state.project = @project.serialize()
@state.workspace = @workspace.serialize()
@packages.deactivatePackages()
@state.packageStates = @packages.packageStates
@state.fullScreen = @isFullScreen()
@saveStateSync()
@saveBlobStoreSync()
@unloaded = true
openInitialEmptyEditorIfNecessary: ->
return unless @config.get('core.openEmptyEditorOnStart')
@@ -755,7 +810,7 @@ class AtomEnvironment extends Model
@themes.load()
# Notify the browser project of the window's current project path
watchProjectPath: ->
watchProjectPaths: ->
@disposables.add @project.onDidChangePaths =>
@applicationDelegate.setRepresentedDirectoryPaths(@project.getPaths())
@@ -780,45 +835,44 @@ class AtomEnvironment extends Model
@blobStore.save()
saveStateSync: ->
return unless @enablePersistence
saveState: (options) ->
return Promise.resolve() unless @enablePersistence
if storageKey = @getStateKey(@project?.getPaths())
@getStorageFolder().store(storageKey, @state)
new Promise (resolve, reject) =>
window.requestIdleCallback =>
return if not @project
state = @serialize(options)
savePromise =
if storageKey = @getStateKey(@project?.getPaths())
@stateStore.save(storageKey, state)
else
@applicationDelegate.setTemporaryWindowState(state)
savePromise.catch(reject).then(resolve)
loadState: ->
if @enablePersistence
if stateKey = @getStateKey(@getLoadSettings().initialPaths)
@stateStore.load(stateKey)
else
@applicationDelegate.getTemporaryWindowState()
else
@getCurrentWindow().loadSettings.windowState = JSON.stringify(@state)
Promise.resolve(null)
loadStateSync: ->
return unless @enablePersistence
startTime = Date.now()
if stateKey = @getStateKey(@getLoadSettings().initialPaths)
if state = @getStorageFolder().load(stateKey)
@state = state
if not @state? and windowState = @getLoadSettings().windowState
try
if state = JSON.parse(@getLoadSettings().windowState)
@state = state
catch error
console.warn "Error parsing window state: #{statePath} #{error.stack}", error
@deserializeTimings.atom = Date.now() - startTime
if grammarOverridesByPath = @state.grammars?.grammarOverridesByPath
deserialize: (state) ->
if grammarOverridesByPath = state.grammars?.grammarOverridesByPath
@grammars.grammarOverridesByPath = grammarOverridesByPath
@setFullScreen(@state.fullScreen)
@setFullScreen(state.fullScreen)
@packages.packageStates = @state.packageStates ? {}
@packages.packageStates = state.packageStates ? {}
startTime = Date.now()
@project.deserialize(@state.project, @deserializers) if @state.project?
@project.deserialize(state.project, @deserializers) if state.project?
@deserializeTimings.project = Date.now() - startTime
startTime = Date.now()
@workspace.deserialize(@state.workspace, @deserializers) if @state.workspace?
@workspace.deserialize(state.workspace, @deserializers) if state.workspace?
@deserializeTimings.workspace = Date.now() - startTime
getStateKey: (paths) ->
@@ -831,9 +885,6 @@ class AtomEnvironment extends Model
getConfigDirPath: ->
@configDirPath ?= process.env.ATOM_HOME
getStorageFolder: ->
@storageFolder ?= new StorageFolder(@getConfigDirPath())
getUserInitScriptPath: ->
initScriptPath = fs.resolve(@getConfigDirPath(), 'init', ['js', 'coffee'])
initScriptPath ? path.join(@getConfigDirPath(), 'init.coffee')
@@ -847,6 +898,7 @@ class AtomEnvironment extends Model
detail: error.message
dismissable: true
# TODO: We should deprecate the update events here, and use `atom.autoUpdater` instead
onUpdateAvailable: (callback) ->
@emitter.on 'update-available', callback
@@ -854,7 +906,8 @@ class AtomEnvironment extends Model
@emitter.emit 'update-available', details
listenForUpdates: ->
@disposables.add(@applicationDelegate.onUpdateAvailable(@updateAvailable.bind(this)))
# listen for updates available locally (that have been successfully downloaded)
@disposables.add(@autoUpdater.onDidCompleteDownloadingUpdate(@updateAvailable.bind(this)))
setBodyPlatformClass: ->
@document.body.classList.add("platform-#{process.platform}")
@@ -876,8 +929,8 @@ class AtomEnvironment extends Model
openLocations: (locations) ->
needsProjectPaths = @project?.getPaths().length is 0
for {pathToOpen, initialLine, initialColumn} in locations
if pathToOpen? and needsProjectPaths
for {pathToOpen, initialLine, initialColumn, forceAddToWindow} in locations
if pathToOpen? and (needsProjectPaths or forceAddToWindow)
if fs.existsSync(pathToOpen)
@project.addPath(pathToOpen)
else if fs.existsSync(path.dirname(pathToOpen))

View File

@@ -0,0 +1,73 @@
'use babel'
import {Emitter, CompositeDisposable} from 'event-kit'
export default class AutoUpdateManager {
constructor ({applicationDelegate}) {
this.applicationDelegate = applicationDelegate
this.subscriptions = new CompositeDisposable()
this.emitter = new Emitter()
this.subscriptions.add(
applicationDelegate.onDidBeginCheckingForUpdate(() => {
this.emitter.emit('did-begin-checking-for-update')
}),
applicationDelegate.onDidBeginDownloadingUpdate(() => {
this.emitter.emit('did-begin-downloading-update')
}),
applicationDelegate.onDidCompleteDownloadingUpdate((details) => {
this.emitter.emit('did-complete-downloading-update', details)
}),
applicationDelegate.onUpdateNotAvailable(() => {
this.emitter.emit('update-not-available')
})
)
}
destroy () {
this.subscriptions.dispose()
this.emitter.dispose()
}
checkForUpdate () {
this.applicationDelegate.checkForUpdate()
}
restartAndInstallUpdate () {
this.applicationDelegate.restartAndInstallUpdate()
}
getState () {
return this.applicationDelegate.getAutoUpdateManagerState()
}
platformSupportsUpdates () {
return atom.getReleaseChannel() !== 'dev' && this.getState() !== 'unsupported'
}
onDidBeginCheckingForUpdate (callback) {
return this.emitter.on('did-begin-checking-for-update', callback)
}
onDidBeginDownloadingUpdate (callback) {
return this.emitter.on('did-begin-downloading-update', callback)
}
onDidCompleteDownloadingUpdate (callback) {
return this.emitter.on('did-complete-downloading-update', callback)
}
// TODO: When https://github.com/atom/electron/issues/4587 is closed, we can
// add an update-available event.
// onUpdateAvailable (callback) {
// return this.emitter.on('update-available', callback)
// }
onUpdateNotAvailable (callback) {
return this.emitter.on('update-not-available', callback)
}
getPlatform () {
return process.platform
}
}

View File

@@ -1,6 +1,4 @@
app = require 'app'
ipc = require 'ipc'
Menu = require 'menu'
{app, Menu} = require 'electron'
_ = require 'underscore-plus'
# Used to manage the global application menu.

View File

@@ -2,14 +2,10 @@ AtomWindow = require './atom-window'
ApplicationMenu = require './application-menu'
AtomProtocolHandler = require './atom-protocol-handler'
AutoUpdateManager = require './auto-update-manager'
BrowserWindow = require 'browser-window'
StorageFolder = require '../storage-folder'
Menu = require 'menu'
app = require 'app'
dialog = require 'dialog'
shell = require 'shell'
ipcHelpers = require '../ipc-helpers'
{BrowserWindow, Menu, app, dialog, ipcMain, shell} = require 'electron'
fs = require 'fs-plus'
ipc = require 'ipc'
path = require 'path'
os = require 'os'
net = require 'net'
@@ -51,7 +47,7 @@ class AtomApplication
client = net.connect {path: options.socketPath}, ->
client.write JSON.stringify(options), ->
client.end()
app.terminate()
app.quit()
client.on 'error', createAtomApplication
@@ -65,7 +61,7 @@ class AtomApplication
exit: (status) -> app.exit(status)
constructor: (options) ->
{@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, timeout} = options
{@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, timeout, clearWindowState} = options
@socketPath = null if options.test
@@ -89,16 +85,16 @@ class AtomApplication
else
@loadState(options) or @openPath(options)
openWithOptions: ({pathsToOpen, executedFrom, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, profileStartup, timeout}) ->
openWithOptions: ({initialPaths, pathsToOpen, executedFrom, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, profileStartup, timeout, clearWindowState, addToLastWindow}) ->
if test
@runTests({headless: true, devMode, @resourcePath, executedFrom, pathsToOpen, logFile, timeout})
else if pathsToOpen.length > 0
@openPaths({pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup})
@openPaths({initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState, addToLastWindow})
else if urlsToOpen.length > 0
@openUrl({urlToOpen, devMode, safeMode}) for urlToOpen in urlsToOpen
else
# Always open a editor window if this is the first instance of Atom.
@openPath({pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup})
@openPath({initialPaths, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState, addToLastWindow})
# Public: Removes the {AtomWindow} from the global window list.
removeWindow: (window) ->
@@ -179,7 +175,6 @@ class AtomApplication
@on 'application:open-documentation', -> shell.openExternal('https://atom.io/docs/latest/?app')
@on 'application:open-discussions', -> shell.openExternal('https://discuss.atom.io')
@on 'application:open-roadmap', -> shell.openExternal('https://atom.io/roadmap?app')
@on 'application:open-faq', -> shell.openExternal('https://atom.io/faq')
@on 'application:open-terms-of-use', -> shell.openExternal('https://atom.io/terms')
@on 'application:report-issue', -> shell.openExternal('https://github.com/atom/atom/blob/master/CONTRIBUTING.md#submitting-issues')
@@ -212,7 +207,6 @@ class AtomApplication
@openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md'))
app.on 'before-quit', =>
@saveState(false)
@quitting = true
app.on 'will-quit', =>
@@ -232,7 +226,7 @@ class AtomApplication
@emit('application:new-window')
# A request from the associated render process to open a new render process.
ipc.on 'open', (event, options) =>
ipcMain.on 'open', (event, options) =>
window = @windowForEvent(event)
if options?
if typeof options.pathsToOpen is 'string'
@@ -245,44 +239,80 @@ class AtomApplication
else
@promptForPathToOpen('all', {window})
ipc.on 'update-application-menu', (event, template, keystrokesByCommand) =>
ipcMain.on 'update-application-menu', (event, template, keystrokesByCommand) =>
win = BrowserWindow.fromWebContents(event.sender)
@applicationMenu.update(win, template, keystrokesByCommand)
ipc.on 'run-package-specs', (event, packageSpecPath) =>
ipcMain.on 'run-package-specs', (event, packageSpecPath) =>
@runTests({resourcePath: @devResourcePath, pathsToOpen: [packageSpecPath], headless: false})
ipc.on 'command', (event, command) =>
ipcMain.on 'command', (event, command) =>
@emit(command)
ipc.on 'window-command', (event, command, args...) ->
ipcMain.on 'window-command', (event, command, args...) ->
win = BrowserWindow.fromWebContents(event.sender)
win.emit(command, args...)
ipc.on 'call-window-method', (event, method, args...) ->
ipcMain.on 'call-window-method', (event, method, args...) ->
win = BrowserWindow.fromWebContents(event.sender)
win[method](args...)
ipc.on 'pick-folder', (event, responseChannel) =>
ipcMain.on 'pick-folder', (event, responseChannel) =>
@promptForPath "folder", (selectedPaths) ->
event.sender.send(responseChannel, selectedPaths)
ipc.on 'did-cancel-window-unload', =>
ipcHelpers.respondTo 'set-window-size', (win, width, height) ->
win.setSize(width, height)
ipcHelpers.respondTo 'set-window-position', (win, x, y) ->
win.setPosition(x, y)
ipcHelpers.respondTo 'center-window', (win) ->
win.center()
ipcHelpers.respondTo 'focus-window', (win) ->
win.focus()
ipcHelpers.respondTo 'show-window', (win) ->
win.show()
ipcHelpers.respondTo 'hide-window', (win) ->
win.hide()
ipcHelpers.respondTo 'get-temporary-window-state', (win) ->
win.temporaryState
ipcHelpers.respondTo 'set-temporary-window-state', (win, state) ->
win.temporaryState = state
ipcMain.on 'did-cancel-window-unload', =>
@quitting = false
clipboard = require '../safe-clipboard'
ipc.on 'write-text-to-selection-clipboard', (event, selectedText) ->
ipcMain.on 'write-text-to-selection-clipboard', (event, selectedText) ->
clipboard.writeText(selectedText, 'selection')
ipc.on 'write-to-stdout', (event, output) ->
ipcMain.on 'write-to-stdout', (event, output) ->
process.stdout.write(output)
ipc.on 'write-to-stderr', (event, output) ->
ipcMain.on 'write-to-stderr', (event, output) ->
process.stderr.write(output)
ipc.on 'add-recent-document', (event, filename) ->
ipcMain.on 'add-recent-document', (event, filename) ->
app.addRecentDocument(filename)
ipcMain.on 'execute-javascript-in-dev-tools', (event, code) ->
event.sender.devToolsWebContents?.executeJavaScript(code)
ipcMain.on 'check-for-update', =>
@autoUpdateManager.check()
ipcMain.on 'get-auto-update-manager-state', (event) =>
event.returnValue = @autoUpdateManager.getState()
ipcMain.on 'execute-javascript-in-dev-tools', (event, code) ->
event.sender.devToolsWebContents?.executeJavaScript(code)
setupDockMenu: ->
if process.platform is 'darwin'
dockMenu = Menu.buildFromTemplate [
@@ -350,7 +380,7 @@ class AtomApplication
_.find @windows, (atomWindow) ->
atomWindow.devMode is devMode and atomWindow.containsPaths(pathsToOpen)
# Returns the {AtomWindow} for the given ipc event.
# Returns the {AtomWindow} for the given ipcMain event.
windowForEvent: ({sender}) ->
window = BrowserWindow.fromWebContents(sender)
_.find @windows, ({browserWindow}) -> window is browserWindow
@@ -387,8 +417,9 @@ class AtomApplication
# :safeMode - Boolean to control the opened window's safe mode.
# :profileStartup - Boolean to control creating a profile of the startup time.
# :window - {AtomWindow} to open file paths in.
openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window} = {}) ->
@openPaths({pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window})
# :addToLastWindow - Boolean of whether this should be opened in last focused window.
openPath: ({initialPaths, pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow} = {}) ->
@openPaths({initialPaths, pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow})
# Public: Opens multiple paths, in existing windows if possible.
#
@@ -400,10 +431,12 @@ class AtomApplication
# :safeMode - Boolean to control the opened window's safe mode.
# :windowDimensions - Object with height and width keys.
# :window - {AtomWindow} to open file paths in.
openPaths: ({pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window}={}) ->
# :addToLastWindow - Boolean of whether this should be opened in last focused window.
openPaths: ({initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window, clearWindowState, addToLastWindow}={}) ->
devMode = Boolean(devMode)
safeMode = Boolean(safeMode)
locationsToOpen = (@locationForPathToOpen(pathToOpen, executedFrom) for pathToOpen in pathsToOpen)
clearWindowState = Boolean(clearWindowState)
locationsToOpen = (@locationForPathToOpen(pathToOpen, executedFrom, addToLastWindow) for pathToOpen in pathsToOpen)
pathsToOpen = (locationToOpen.pathToOpen for locationToOpen in locationsToOpen)
unless pidToKillWhenClosed or newWindow
@@ -412,6 +445,7 @@ class AtomApplication
unless existingWindow?
if currentWindow = window ? @lastFocusedWindow
existingWindow = currentWindow if (
addToLastWindow or
currentWindow.devMode is devMode and
(
stats.every((stat) -> stat.isFile?()) or
@@ -435,7 +469,7 @@ class AtomApplication
windowInitializationScript ?= require.resolve('../initialize-application-window')
resourcePath ?= @resourcePath
windowDimensions ?= @getDimensionsForNewWindow()
openedWindow = new AtomWindow({locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup})
openedWindow = new AtomWindow({initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState})
if pidToKillWhenClosed?
@pidsToOpenWindows[pidToKillWhenClosed] = openedWindow
@@ -472,13 +506,14 @@ class AtomApplication
if loadSettings = window.getLoadSettings()
states.push(initialPaths: loadSettings.initialPaths)
if states.length > 0 or allowEmpty
@storageFolder.store('application.json', states)
@storageFolder.storeSync('application.json', states)
loadState: (options) ->
if (states = @storageFolder.load('application.json'))?.length > 0
for state in states
@openWithOptions(_.extend(options, {
pathsToOpen: state.initialPaths
initialPaths: state.initialPaths
pathsToOpen: state.initialPaths.filter (directoryPath) -> fs.isDirectorySync(directoryPath)
urlsToOpen: []
devMode: @devMode
safeMode: @safeMode
@@ -511,7 +546,7 @@ class AtomApplication
if pack.urlMain
packagePath = @packages.resolvePackagePath(packageName)
windowInitializationScript = path.resolve(packagePath, pack.urlMain)
windowDimensions = @focusedWindow()?.getDimensions()
windowDimensions = @getDimensionsForNewWindow()
new AtomWindow({windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions})
else
console.log "Package '#{pack.name}' does not have a url main: #{urlToOpen}"
@@ -580,7 +615,7 @@ class AtomApplication
catch error
require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'jasmine-test-runner'))
locationForPathToOpen: (pathToOpen, executedFrom='') ->
locationForPathToOpen: (pathToOpen, executedFrom='', forceAddToWindow) ->
return {pathToOpen} unless pathToOpen
pathToOpen = pathToOpen.replace(/[:\s]+$/, '')
@@ -596,7 +631,7 @@ class AtomApplication
unless url.parse(pathToOpen).protocol?
pathToOpen = path.resolve(executedFrom, fs.normalize(pathToOpen))
{pathToOpen, initialLine, initialColumn}
{pathToOpen, initialLine, initialColumn, forceAddToWindow}
# Opens a native dialog to prompt the user for a path.
#

View File

@@ -1,6 +1,6 @@
fs = require 'fs-plus'
path = require 'path'
ipc = require 'ipc'
{ipcMain} = require 'electron'
module.exports =
class AtomPortable
@@ -30,6 +30,6 @@ class AtomPortable
catch error
message = "Failed to use portable Atom home directory (#{@getPortableAtomHomePath()}). Using the default instead (#{defaultHome}). #{error.message}"
ipc.on 'check-portable-home-writable', (event) ->
ipcMain.on 'check-portable-home-writable', (event) ->
event.sender.send 'check-portable-home-writable-response', {writable, message}
writable

View File

@@ -1,7 +1,6 @@
app = require 'app'
{app, protocol} = require 'electron'
fs = require 'fs'
path = require 'path'
protocol = require 'protocol'
# Handles requests with 'atom' protocol.
#

View File

@@ -1,6 +1,4 @@
BrowserWindow = require 'browser-window'
app = require 'app'
dialog = require 'dialog'
{BrowserWindow, app, dialog} = require 'electron'
path = require 'path'
fs = require 'fs'
url = require 'url'
@@ -19,7 +17,7 @@ class AtomWindow
isSpec: null
constructor: (settings={}) ->
{@resourcePath, pathToOpen, locationsToOpen, @isSpec, @headless, @safeMode, @devMode} = settings
{@resourcePath, initialPaths, pathToOpen, locationsToOpen, @isSpec, @headless, @safeMode, @devMode} = settings
locationsToOpen ?= [{pathToOpen}] if pathToOpen
locationsToOpen ?= []
@@ -43,19 +41,13 @@ class AtomWindow
@handleEvents()
loadSettings = _.extend({}, settings)
loadSettings.windowState ?= '{}'
loadSettings.appVersion = app.getVersion()
loadSettings.resourcePath = @resourcePath
loadSettings.devMode ?= false
loadSettings.safeMode ?= false
loadSettings.atomHome = process.env.ATOM_HOME
# Only send to the first non-spec window created
if @constructor.includeShellLoadTime and not @isSpec
@constructor.includeShellLoadTime = false
loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime
loadSettings.initialPaths =
loadSettings.clearWindowState ?= false
loadSettings.initialPaths ?=
for {pathToOpen} in locationsToOpen when pathToOpen
if fs.statSyncNoException(pathToOpen).isFile?()
path.dirname(pathToOpen)
@@ -64,24 +56,26 @@ class AtomWindow
loadSettings.initialPaths.sort()
# Only send to the first non-spec window created
if @constructor.includeShellLoadTime and not @isSpec
@constructor.includeShellLoadTime = false
loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime
@browserWindow.loadSettings = loadSettings
@browserWindow.once 'window:loaded', =>
@emit 'window:loaded'
@loaded = true
@setLoadSettings(loadSettings)
@browserWindow.focusOnWebView() if @isSpec
@browserWindow.temporaryState = {windowDimensions} if windowDimensions?
hasPathToOpen = not (locationsToOpen.length is 1 and not locationsToOpen[0].pathToOpen?)
@openLocations(locationsToOpen) if hasPathToOpen and not @isSpecWindow()
setLoadSettings: (loadSettingsObj) ->
# Ignore the windowState when passing loadSettings via URL, since it could
# be quite large.
loadSettings = _.clone(loadSettingsObj)
delete loadSettings['windowState']
@browserWindow.loadUrl url.format
setLoadSettings: (loadSettings) ->
@browserWindow.loadURL url.format
protocol: 'file'
pathname: "#{@resourcePath}/static/index.html"
slashes: true
@@ -89,7 +83,7 @@ class AtomWindow
getLoadSettings: ->
if @browserWindow.webContents? and not @browserWindow.webContents.isLoading()
hash = url.parse(@browserWindow.webContents.getUrl()).hash.substr(1)
hash = url.parse(@browserWindow.webContents.getURL()).hash.substr(1)
JSON.parse(decodeURIComponent(hash))
hasProjectPath: -> @getLoadSettings().initialPaths?.length > 0
@@ -121,6 +115,9 @@ class AtomWindow
false
handleEvents: ->
@browserWindow.on 'close', ->
global.atomApplication.saveState(false)
@browserWindow.on 'closed', =>
global.atomApplication.removeWindow(this)
@@ -144,10 +141,10 @@ class AtomWindow
detail: 'Please report this issue to https://github.com/atom/atom'
switch chosen
when 0 then @browserWindow.destroy()
when 1 then @browserWindow.restart()
when 1 then @browserWindow.reload()
@browserWindow.webContents.on 'will-navigate', (event, url) =>
unless url is @browserWindow.webContents.getUrl()
unless url is @browserWindow.webContents.getURL()
event.preventDefault()
@setupContextMenu()
@@ -220,6 +217,6 @@ class AtomWindow
isSpecWindow: -> @isSpec
reload: -> @browserWindow.restart()
reload: -> @browserWindow.reload()
toggleDevTools: -> @browserWindow.toggleDevTools()

View File

@@ -29,26 +29,34 @@ class AutoUpdateManager
if process.platform is 'win32'
autoUpdater = require './auto-updater-win32'
else
autoUpdater = require 'auto-updater'
{autoUpdater} = require 'electron'
autoUpdater.on 'error', (event, message) =>
@setState(ErrorState)
console.error "Error Downloading Update: #{message}"
autoUpdater.setFeedUrl @feedUrl
autoUpdater.setFeedURL @feedUrl
autoUpdater.on 'checking-for-update', =>
@setState(CheckingState)
@emitWindowEvent('checking-for-update')
autoUpdater.on 'update-not-available', =>
@setState(NoUpdateAvailableState)
@emitWindowEvent('update-not-available')
autoUpdater.on 'update-available', =>
@setState(DownladingState)
# We use sendMessage to send an event called 'update-available' in 'update-downloaded'
# once the update download is complete. This mismatch between the electron
# autoUpdater events is unfortunate but in the interest of not changing the
# one existing event handled by applicationDelegate
@emitWindowEvent('did-begin-downloading-update')
@emit('did-begin-download')
autoUpdater.on 'update-downloaded', (event, releaseNotes, @releaseVersion) =>
@setState(UpdateAvailableState)
@emitUpdateAvailableEvent(@getWindows()...)
@emitUpdateAvailableEvent()
@config.onDidChange 'core.automaticallyUpdate', ({newValue}) =>
if newValue
@@ -64,10 +72,14 @@ class AutoUpdateManager
when 'linux'
@setState(UnsupportedState)
emitUpdateAvailableEvent: (windows...) ->
emitUpdateAvailableEvent: ->
return unless @releaseVersion?
for atomWindow in windows
atomWindow.sendMessage('update-available', {@releaseVersion})
@emitWindowEvent('update-available', {@releaseVersion})
return
emitWindowEvent: (eventName, payload) ->
for atomWindow in @getWindows()
atomWindow.sendMessage(eventName, payload)
return
setState: (state) ->
@@ -104,7 +116,7 @@ class AutoUpdateManager
onUpdateNotAvailable: =>
autoUpdater.removeListener 'error', @onUpdateError
dialog = require 'dialog'
{dialog} = require 'electron'
dialog.showMessageBox
type: 'info'
buttons: ['OK']
@@ -115,7 +127,7 @@ class AutoUpdateManager
onUpdateError: (event, message) =>
autoUpdater.removeListener 'update-not-available', @onUpdateNotAvailable
dialog = require 'dialog'
{dialog} = require 'electron'
dialog.showMessageBox
type: 'warning'
buttons: ['OK']

View File

@@ -5,13 +5,13 @@ SquirrelUpdate = require './squirrel-update'
class AutoUpdater
_.extend @prototype, EventEmitter.prototype
setFeedUrl: (@updateUrl) ->
setFeedURL: (@updateUrl) ->
quitAndInstall: ->
if SquirrelUpdate.existsSync()
SquirrelUpdate.restartAtom(require('app'))
SquirrelUpdate.restartAtom(require('electron').app)
else
require('auto-updater').quitAndInstall()
require('electron').autoUpdater.quitAndInstall()
downloadUpdate: (callback) ->
SquirrelUpdate.spawn ['--download', @updateUrl], (error, stdout) ->

View File

@@ -1,4 +1,4 @@
Menu = require 'menu'
{Menu} = require 'electron'
module.exports =
class ContextMenu

View File

@@ -4,10 +4,10 @@ process.on 'uncaughtException', (error={}) ->
console.log(error.message) if error.message?
console.log(error.stack) if error.stack?
crashReporter = require 'crash-reporter'
app = require 'app'
{crashReporter, app} = require 'electron'
fs = require 'fs-plus'
path = require 'path'
temp = require 'temp'
yargs = require 'yargs'
console.log = require 'nslog'
@@ -32,6 +32,11 @@ start = ->
app.on 'open-url', addUrlToOpen
app.on 'will-finish-launching', setupCrashReporter
if args.userDataDir?
app.setPath('userData', args.userDataDir)
else if args.test
app.setPath('userData', temp.mkdirSync('atom-test-data'))
app.on 'ready', ->
app.removeListener 'open-file', addPathToOpen
app.removeListener 'open-url', addUrlToOpen
@@ -54,12 +59,12 @@ handleStartupEventWithSquirrel = ->
SquirrelUpdate.handleStartupEvent(app, squirrelCommand)
setupCrashReporter = ->
crashReporter.start(productName: 'Atom', companyName: 'GitHub')
crashReporter.start(productName: 'Atom', companyName: 'GitHub', submitURL: 'http://54.249.141.255:1127/post')
setupAtomHome = ({setPortable}) ->
return if process.env.ATOM_HOME
atomHome = path.join(app.getHomeDir(), '.atom')
atomHome = path.join(app.getPath('home'), '.atom')
AtomPortable = require './atom-portable'
if setPortable and not AtomPortable.isPortableInstall(process.platform, process.env.ATOM_HOME, atomHome)
@@ -81,6 +86,15 @@ setupCompileCache = ->
compileCache = require('../compile-cache')
compileCache.setAtomHomeDirectory(process.env.ATOM_HOME)
writeFullVersion = ->
process.stdout.write """
Atom : #{app.getVersion()}
Electron: #{process.versions.electron}
Chrome : #{process.versions.chrome}
Node : #{process.versions.node}
"""
parseCommandLine = ->
version = app.getVersion()
options = yargs(process.argv[1..]).wrap(100)
@@ -116,9 +130,12 @@ parseCommandLine = ->
options.boolean('portable').describe('portable', 'Set portable mode. Copies the ~/.atom folder to be a sibling of the installed Atom location if a .atom folder is not already there.')
options.alias('t', 'test').boolean('t').describe('t', 'Run the specified specs and exit with error code on failures.')
options.string('timeout').describe('timeout', 'When in test mode, waits until the specified time (in minutes) and kills the process (exit code: 130).')
options.alias('v', 'version').boolean('v').describe('v', 'Print the version.')
options.alias('v', 'version').boolean('v').describe('v', 'Print the version information.')
options.alias('w', 'wait').boolean('w').describe('w', 'Wait for window to be closed before returning.')
options.alias('a', 'add').boolean('a').describe('add', 'Open path as a new project in last used window.')
options.string('socket-path')
options.string('user-data-dir')
options.boolean('clear-window-state').describe('clear-window-state', 'Delete all Atom environment state.')
args = options.argv
@@ -127,9 +144,10 @@ parseCommandLine = ->
process.exit(0)
if args.version
process.stdout.write("#{version}\n")
writeFullVersion()
process.exit(0)
addToLastWindow = args['add']
executedFrom = args['executed-from']?.toString() ? process.cwd()
devMode = args['dev']
safeMode = args['safe']
@@ -140,9 +158,11 @@ parseCommandLine = ->
pidToKillWhenClosed = args['pid'] if args['wait']
logFile = args['log-file']
socketPath = args['socket-path']
userDataDir = args['user-data-dir']
profileStartup = args['profile-startup']
clearWindowState = args['clear-window-state']
urlsToOpen = []
devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH ? path.join(app.getHomeDir(), 'github', 'atom')
devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH ? path.join(app.getPath('home'), 'github', 'atom')
setPortable = args.portable
if args['resource-path']
@@ -164,6 +184,7 @@ parseCommandLine = ->
{resourcePath, devResourcePath, pathsToOpen, urlsToOpen, executedFrom, test,
version, pidToKillWhenClosed, devMode, safeMode, newWindow,
logFile, socketPath, profileStartup, timeout, setPortable}
logFile, socketPath, userDataDir, profileStartup, timeout, setPortable,
clearWindowState, addToLastWindow}
start()

View File

@@ -11,15 +11,18 @@ exeName = path.basename(process.execPath)
if process.env.SystemRoot
system32Path = path.join(process.env.SystemRoot, 'System32')
regPath = path.join(system32Path, 'reg.exe')
powershellPath = path.join(system32Path, 'WindowsPowerShell', 'v1.0', 'powershell.exe')
setxPath = path.join(system32Path, 'setx.exe')
else
regPath = 'reg.exe'
powershellPath = 'powershell.exe'
setxPath = 'setx.exe'
# Registry keys used for context menu
fileKeyPath = 'HKCU\\Software\\Classes\\*\\shell\\Atom'
directoryKeyPath = 'HKCU\\Software\\Classes\\directory\\shell\\Atom'
backgroundKeyPath = 'HKCU\\Software\\Classes\\directory\\background\\shell\\Atom'
applicationsKeyPath = 'HKCU\\Software\\Classes\\Applications\\atom.exe'
environmentKeyPath = 'HKCU\\Environment'
# Spawn a command and invoke the callback when it completes with an error
@@ -43,11 +46,31 @@ spawn = (command, args, callback) ->
error?.code ?= code
error?.stdout ?= stdout
callback?(error, stdout)
# This is necessary if using Powershell 2 on Windows 7 to get the events to raise
# http://stackoverflow.com/questions/9155289/calling-powershell-from-nodejs
spawnedProcess.stdin.end()
# Spawn reg.exe and callback when it completes
spawnReg = (args, callback) ->
spawn(regPath, args, callback)
# Spawn powershell.exe and callback when it completes
spawnPowershell = (args, callback) ->
# set encoding and execute the command, capture the output, and return it via .NET's console in order to have consistent UTF-8 encoding
# http://stackoverflow.com/questions/22349139/utf-8-output-from-powershell
# to address https://github.com/atom/atom/issues/5063
args[0] = """
[Console]::OutputEncoding=[System.Text.Encoding]::UTF8
$output=#{args[0]}
[Console]::WriteLine($output)
"""
args.unshift('-command')
args.unshift('RemoteSigned')
args.unshift('-ExecutionPolicy')
args.unshift('-noprofile')
spawn(powershellPath, args, callback)
# Spawn setx.exe and callback when it completes
spawnSetx = (args, callback) ->
spawn(setxPath, args, callback)
@@ -64,6 +87,10 @@ installContextMenu = (callback) ->
args.push('/f')
spawnReg(args, callback)
installFileHandler = (callback) ->
args = ["#{applicationsKeyPath}\\shell\\open\\command", '/ve', '/d', "\"#{process.execPath}\" \"%1\""]
addToRegistry(args, callback)
installMenu = (keyPath, arg, callback) ->
args = [keyPath, '/ve', '/d', 'Open with Atom']
addToRegistry args, ->
@@ -74,48 +101,17 @@ installContextMenu = (callback) ->
installMenu fileKeyPath, '%1', ->
installMenu directoryKeyPath, '%1', ->
installMenu(backgroundKeyPath, '%V', callback)
isAscii = (text) ->
index = 0
while index < text.length
return false if text.charCodeAt(index) > 127
index++
true
installMenu backgroundKeyPath, '%V', ->
installFileHandler(callback)
# Get the user's PATH environment variable registry value.
getPath = (callback) ->
spawnReg ['query', environmentKeyPath, '/v', 'Path'], (error, stdout) ->
spawnPowershell ['[environment]::GetEnvironmentVariable(\'Path\',\'User\')'], (error, stdout) ->
if error?
if error.code is 1
# FIXME Don't overwrite path when reading value is disabled
# https://github.com/atom/atom/issues/5092
if stdout.indexOf('ERROR: Registry editing has been disabled by your administrator.') isnt -1
return callback(error)
return callback(error)
# The query failed so the Path does not exist yet in the registry
return callback(null, '')
else
return callback(error)
# Registry query output is in the form:
#
# HKEY_CURRENT_USER\Environment
# Path REG_SZ C:\a\folder\on\the\path;C\another\folder
#
lines = stdout.split(/[\r\n]+/).filter (line) -> line
segments = lines[lines.length - 1]?.split(' ')
if segments[1] is 'Path' and segments.length >= 3
pathEnv = segments?[3..].join(' ')
if isAscii(pathEnv)
callback(null, pathEnv)
else
# FIXME Don't corrupt non-ASCII PATH values
# https://github.com/atom/atom/issues/5063
callback(new Error('PATH contains non-ASCII values'))
else
callback(new Error('Registry query for PATH failed'))
pathOutput = stdout.replace(/^\s+|\s+$/g, '')
callback(null, pathOutput)
# Uninstall the Open with Atom explorer context menu items via the registry.
uninstallContextMenu = (callback) ->
@@ -124,7 +120,8 @@ uninstallContextMenu = (callback) ->
deleteFromRegistry fileKeyPath, ->
deleteFromRegistry directoryKeyPath, ->
deleteFromRegistry(backgroundKeyPath, callback)
deleteFromRegistry backgroundKeyPath, ->
deleteFromRegistry(applicationsKeyPath, callback)
# Add atom and apm to the PATH
#

View File

@@ -46,7 +46,7 @@ class BufferedNodeProcess extends BufferedProcess
options ?= {}
options.env ?= Object.create(process.env)
options.env['ATOM_SHELL_INTERNAL_RUN_AS_NODE'] = 1
options.env['ELECTRON_RUN_AS_NODE'] = 1
args = args?.slice() ? []
args.unshift(command)

View File

@@ -85,5 +85,5 @@ parseAlpha = (alpha) ->
numberToHexString = (number) ->
hex = number.toString(16)
hex = "0#{hex}" if number < 10
hex = "0#{hex}" if number < 16
hex

View File

@@ -108,6 +108,10 @@ module.exports =
description: 'Automatically update Atom when a new release is available.'
type: 'boolean'
default: true
allowPendingPaneItems:
description: 'Allow items to be previewed without adding them to a pane permanently, such as when single clicking files in the tree view.'
type: 'boolean'
default: true
editor:
type: 'object'
@@ -171,7 +175,7 @@ module.exports =
tabLength:
type: 'integer'
default: 2
enum: [1, 2, 3, 4, 6, 8]
minimum: 1
description: 'Number of spaces used to represent a tab.'
softWrap:
type: 'boolean'

View File

@@ -319,6 +319,23 @@ ScopeDescriptor = require './scope-descriptor'
# * line breaks - `line breaks<br/>`
# * ~~strikethrough~~ - `~~strikethrough~~`
#
# #### order
#
# The settings view orders your settings alphabetically. You can override this
# ordering with the order key.
#
# ```coffee
# config:
# zSetting:
# type: 'integer'
# default: 4
# order: 1
# aSetting:
# type: 'integer'
# default: 4
# order: 2
# ```
#
# ## Best practices
#
# * Don't depend on (or write to) configuration keys outside of your keypath.

View File

@@ -4,7 +4,7 @@ CSON = require 'season'
fs = require 'fs-plus'
{calculateSpecificity, validateSelector} = require 'clear-cut'
{Disposable} = require 'event-kit'
remote = require 'remote'
{remote} = require 'electron'
MenuHelpers = require './menu-helpers'
platformContextMenu = require('../package.json')?._atomMenu?['context-menu']

View File

@@ -393,7 +393,10 @@ class DisplayBuffer extends Model
@displayLayer.foldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity))).length > 0
isFoldedAtScreenRow: (screenRow) ->
@isFoldedAtBufferRow(@bufferRowForScreenRow(screenRow))?
@isFoldedAtBufferRow(@bufferRowForScreenRow(screenRow))
isFoldableAtBufferRow: (row) ->
@tokenizedBuffer.isFoldableAtRow(row)
# Removes any folds found that contain the given buffer row.
#

View File

@@ -202,32 +202,35 @@ export default class GitRepositoryAsync {
workingDirectory = workingDirectory.replace(/\/$/, '')
// Depending on where the paths come from, they may have a '/private/'
// prefix. Standardize by stripping that out.
_path = _path.replace(/^\/private\//i, '/')
workingDirectory = workingDirectory.replace(/^\/private\//i, '/')
const originalPath = _path
const originalWorkingDirectory = workingDirectory
if (this.isCaseInsensitive) {
_path = _path.toLowerCase()
workingDirectory = workingDirectory.toLowerCase()
}
// Depending on where the paths come from, they may have a '/private/'
// prefix. Standardize by stripping that out.
_path = _path.replace(/^\/private\//, '/')
workingDirectory = workingDirectory.replace(/^\/private\//, '/')
const originalPath = _path
if (_path.indexOf(workingDirectory) === 0) {
return originalPath.substring(workingDirectory.length + 1)
return originalPath.substring(originalWorkingDirectory.length + 1)
} else if (_path === workingDirectory) {
return ''
}
if (openedWorkingDirectory) {
openedWorkingDirectory = openedWorkingDirectory.replace(/\/$/, '')
openedWorkingDirectory = openedWorkingDirectory.replace(/^\/private\//i, '/')
const originalOpenedWorkingDirectory = openedWorkingDirectory
if (this.isCaseInsensitive) {
openedWorkingDirectory = openedWorkingDirectory.toLowerCase()
}
openedWorkingDirectory = openedWorkingDirectory.replace(/\/$/, '')
openedWorkingDirectory = openedWorkingDirectory.replace(/^\/private\//, '/')
if (_path.indexOf(openedWorkingDirectory) === 0) {
return originalPath.substring(openedWorkingDirectory.length + 1)
return originalPath.substring(originalOpenedWorkingDirectory.length + 1)
} else if (_path === openedWorkingDirectory) {
return ''
}
@@ -456,7 +459,10 @@ export default class GitRepositoryAsync {
// information.
getDirectoryStatus (directoryPath) {
return this.relativizeToWorkingDirectory(directoryPath)
.then(relativePath => this._getStatus([relativePath]))
.then(relativePath => {
const pathspec = relativePath + '/**'
return this._getStatus([pathspec])
})
.then(statuses => {
return Promise.all(statuses.map(s => s.statusBit())).then(bits => {
return bits
@@ -590,7 +596,15 @@ export default class GitRepositoryAsync {
.then(([repo, headCommit]) => Promise.all([repo, headCommit.getTree()]))
.then(([repo, tree]) => {
const options = new Git.DiffOptions()
options.contextLines = 0
options.flags = Git.Diff.OPTION.DISABLE_PATHSPEC_MATCH
options.pathspec = this.relativize(_path, repo.workdir())
if (process.platform === 'win32') {
// Ignore eol of line differences on windows so that files checked in
// as LF don't report every line modified when the text contains CRLF
// endings.
options.flags |= Git.Diff.OPTION.IGNORE_WHITESPACE_EOL
}
return Git.Diff.treeToWorkdir(repo, tree, options)
})
.then(diff => this._getDiffLines(diff))
@@ -769,12 +783,17 @@ export default class GitRepositoryAsync {
// Get the current branch and update this.branch.
//
// Returns a {Promise} which resolves to the {String} branch name.
// Returns a {Promise} which resolves to a {boolean} indicating whether the
// branch name changed.
_refreshBranch () {
return this.getRepo()
.then(repo => repo.getCurrentBranch())
.then(ref => ref.name())
.then(branchName => this.branch = branchName)
.then(branchName => {
const changed = branchName !== this.branch
this.branch = branchName
return changed
})
}
// Refresh the cached ahead/behind count with the given branch.
@@ -782,10 +801,15 @@ export default class GitRepositoryAsync {
// * `branchName` The {String} name of the branch whose ahead/behind should be
// used for the refresh.
//
// Returns a {Promise} which will resolve to {null}.
// Returns a {Promise} which will resolve to a {boolean} indicating whether
// the ahead/behind count changed.
_refreshAheadBehindCount (branchName) {
return this.getAheadBehindCount(branchName)
.then(counts => this.upstream = counts)
.then(counts => {
const changed = !_.isEqual(counts, this.upstream)
this.upstream = counts
return changed
})
}
// Get the status for this repository.
@@ -800,7 +824,7 @@ export default class GitRepositoryAsync {
}
return Promise.all(projectPathsPromises)
.then(paths => paths.filter(p => p.length > 0))
.then(paths => paths.map(p => p.length > 0 ? p + '/**' : '*'))
.then(projectPaths => {
return this._getStatus(projectPaths.length > 0 ? projectPaths : null)
})
@@ -891,15 +915,15 @@ export default class GitRepositoryAsync {
// Refresh the cached status.
//
// Returns a {Promise} which will resolve to {null}.
// Returns a {Promise} which will resolve to a {boolean} indicating whether
// any statuses changed.
_refreshStatus () {
return Promise.all([this._getRepositoryStatus(), this._getSubmoduleStatuses()])
.then(([repositoryStatus, submoduleStatus]) => {
const statusesByPath = _.extend({}, repositoryStatus, submoduleStatus)
if (!_.isEqual(this.pathStatusCache, statusesByPath) && this.emitter != null) {
this.emitter.emit('did-change-statuses')
}
const changed = !_.isEqual(this.pathStatusCache, statusesByPath)
this.pathStatusCache = statusesByPath
return changed
})
}
@@ -909,11 +933,17 @@ export default class GitRepositoryAsync {
refreshStatus () {
const status = this._refreshStatus()
const branch = this._refreshBranch()
const aheadBehind = branch.then(branchName => this._refreshAheadBehindCount(branchName))
const aheadBehind = branch.then(() => this._refreshAheadBehindCount(this.branch))
this._refreshingPromise = this._refreshingPromise.then(_ => {
return Promise.all([status, branch, aheadBehind])
.then(_ => null)
.then(([statusChanged, branchChanged, aheadBehindChanged]) => {
if (this.emitter && (statusChanged || branchChanged || aheadBehindChanged)) {
this.emitter.emit('did-change-statuses')
}
return null
})
// Because all these refresh steps happen asynchronously, it's entirely
// possible the repository was destroyed while we were working. In which
// case we should just swallow the error.
@@ -1032,7 +1062,7 @@ export default class GitRepositoryAsync {
return this.getRepo()
.then(repo => {
const opts = {
flags: Git.Status.OPT.INCLUDE_UNTRACKED | Git.Status.OPT.RECURSE_UNTRACKED_DIRS | Git.Status.OPT.DISABLE_PATHSPEC_MATCH
flags: Git.Status.OPT.INCLUDE_UNTRACKED | Git.Status.OPT.RECURSE_UNTRACKED_DIRS
}
if (paths) {

View File

@@ -484,7 +484,7 @@ class GitRepository
relativeProjectPaths = @project?.getPaths()
.map (path) => @relativize(path)
.filter (path) -> path.length > 0
.map (path) -> if path.length > 0 then path + '/**' else '*'
@statusTask?.terminate()
@statusTask = Task.once @handlerPath, @getPath(), relativeProjectPaths, ({statuses, upstream, branch, submodules}) =>

View File

@@ -23,11 +23,10 @@ module.exports = ({blobStore}) ->
enablePersistence: true
})
atom.displayWindow()
atom.startEditorWindow()
atom.startEditorWindow().then ->
# Workaround for focus getting cleared upon window creation
windowFocused = ->
window.removeEventListener('focus', windowFocused)
setTimeout (-> document.querySelector('atom-workspace').focus()), 0
window.addEventListener('focus', windowFocused)
# Workaround for focus getting cleared upon window creation
windowFocused = ->
window.removeEventListener('focus', windowFocused)
setTimeout (-> document.querySelector('atom-workspace').focus()), 0
window.addEventListener('focus', windowFocused)

View File

@@ -4,17 +4,17 @@ cloneObject = (object) ->
clone
module.exports = ({blobStore}) ->
{crashReporter, remote} = require 'electron'
# Start the crash reporter before anything else.
require('crash-reporter').start(productName: 'Atom', companyName: 'GitHub')
remote = require 'remote'
crashReporter.start(productName: 'Atom', companyName: 'GitHub', submitURL: 'http://54.249.141.255:1127/post')
exitWithStatusCode = (status) ->
remote.require('app').emit('will-quit')
remote.app.emit('will-quit')
remote.process.exit(status)
try
path = require 'path'
ipc = require 'ipc'
{ipcRenderer} = require 'electron'
{getWindowLoadSettings} = require './window-load-settings-helpers'
AtomEnvironment = require '../src/atom-environment'
ApplicationDelegate = require '../src/application-delegate'
@@ -29,15 +29,15 @@ module.exports = ({blobStore}) ->
handleKeydown = (event) ->
# Reload: cmd-r / ctrl-r
if (event.metaKey or event.ctrlKey) and event.keyCode is 82
ipc.send('call-window-method', 'restart')
ipcRenderer.send('call-window-method', 'reload')
# Toggle Dev Tools: cmd-alt-i / ctrl-alt-i
if (event.metaKey or event.ctrlKey) and event.altKey and event.keyCode is 73
ipc.send('call-window-method', 'toggleDevTools')
ipcRenderer.send('call-window-method', 'toggleDevTools')
# Reload: cmd-w / ctrl-w
if (event.metaKey or event.ctrlKey) and event.keyCode is 87
ipc.send('call-window-method', 'close')
ipcRenderer.send('call-window-method', 'close')
window.addEventListener('keydown', handleKeydown, true)
@@ -68,7 +68,8 @@ module.exports = ({blobStore}) ->
logFile, headless, testPaths, buildAtomEnvironment, buildDefaultApplicationDelegate, legacyTestRunner
})
promise.then(exitWithStatusCode) if getWindowLoadSettings().headless
promise.then (statusCode) ->
exitWithStatusCode(statusCode) if getWindowLoadSettings().headless
catch error
if getWindowLoadSettings().headless
console.error(error.stack ? error)

40
src/ipc-helpers.js Normal file
View File

@@ -0,0 +1,40 @@
var ipcRenderer = null
var ipcMain = null
var BrowserWindow = null
exports.call = function (methodName, ...args) {
if (!ipcRenderer) {
ipcRenderer = require('electron').ipcRenderer
}
var responseChannel = getResponseChannel(methodName)
return new Promise(function (resolve) {
ipcRenderer.on(responseChannel, function (event, result) {
ipcRenderer.removeAllListeners(responseChannel)
resolve(result)
})
ipcRenderer.send(methodName, ...args)
})
}
exports.respondTo = function (methodName, callback) {
if (!ipcMain) {
var electron = require('electron')
ipcMain = electron.ipcMain
BrowserWindow = electron.BrowserWindow
}
var responseChannel = getResponseChannel(methodName)
ipcMain.on(methodName, function (event, ...args) {
var browserWindow = BrowserWindow.fromWebContents(event.sender)
var result = callback(browserWindow, ...args)
event.sender.send(responseChannel, result)
})
}
function getResponseChannel (methodName) {
return 'ipc-helpers-' + methodName + '-response'
}

View File

@@ -10,6 +10,7 @@ class LanguageMode
# editor - The {TextEditor} to associate with
constructor: (@editor, @config) ->
{@buffer} = @editor
@regexesByPattern = {}
destroy: ->
@@ -144,13 +145,11 @@ class LanguageMode
if bufferRow > 0
for currentRow in [bufferRow-1..0] by -1
break if @buffer.isRowBlank(currentRow)
break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
startRow = currentRow
if bufferRow < @buffer.getLastRow()
for currentRow in [bufferRow+1..@buffer.getLastRow()] by 1
break if @buffer.isRowBlank(currentRow)
break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
endRow = currentRow
@@ -326,7 +325,8 @@ class LanguageMode
getRegexForProperty: (scopeDescriptor, property) ->
if pattern = @config.get(property, scope: scopeDescriptor)
new OnigRegExp(pattern)
@regexesByPattern[pattern] ?= new OnigRegExp(pattern)
@regexesByPattern[pattern]
increaseIndentRegexForScopeDescriptor: (scopeDescriptor) ->
@getRegexForProperty(scopeDescriptor, 'editor.increaseIndentPattern')

View File

@@ -1,7 +1,7 @@
path = require 'path'
_ = require 'underscore-plus'
ipc = require 'ipc'
{ipcRenderer} = require 'electron'
CSON = require 'season'
fs = require 'fs-plus'
{Disposable} = require 'event-kit'
@@ -191,7 +191,7 @@ class MenuManager
sendToBrowserProcess: (template, keystrokesByCommand) ->
keystrokesByCommand = @filterMultipleKeystroke(keystrokesByCommand)
ipc.send 'update-application-menu', template, keystrokesByCommand
ipcRenderer.send 'update-application-menu', template, keystrokesByCommand
# Get an {Array} of {String} classes for the given element.
classesForElement: (element) ->

View File

@@ -208,7 +208,7 @@ registerBuiltins = (devMode) ->
cache.builtins[builtin] = path.join(commonRoot, "#{builtin}.js")
rendererRoot = path.join(atomShellRoot, 'renderer', 'api', 'lib')
rendererBuiltins = ['ipc', 'remote']
rendererBuiltins = ['ipc-renderer', 'remote']
for builtin in rendererBuiltins
cache.builtins[builtin] = path.join(rendererRoot, "#{builtin}.js")

View File

@@ -357,7 +357,9 @@ class PackageManager
packagePaths = @getAvailablePackagePaths()
packagePaths = packagePaths.filter (packagePath) => not @isPackageDisabled(path.basename(packagePath))
packagePaths = _.uniq packagePaths, (packagePath) -> path.basename(packagePath)
@loadPackage(packagePath) for packagePath in packagePaths
@config.transact =>
@loadPackage(packagePath) for packagePath in packagePaths
return
@emitter.emit 'did-load-initial-packages'
loadPackage: (nameOrPath) ->
@@ -467,6 +469,14 @@ class PackageManager
return unless hook? and _.isString(hook) and hook.length > 0
@activationHookEmitter.on(hook, callback)
serialize: ->
for pack in @getActivePackages()
@serializePackage(pack)
@packageStates
serializePackage: (pack) ->
@setPackageState(pack.name, state) if state = pack.serialize?()
# Deactivate all packages
deactivatePackages: ->
@config.transact =>
@@ -478,8 +488,7 @@ class PackageManager
# Deactivate the package with the given name
deactivatePackage: (name) ->
pack = @getLoadedPackage(name)
if @isPackageActive(name)
@setPackageState(pack.name, state) if state = pack.serialize?()
@serializePackage(pack) if @isPackageActive(pack.name)
pack.deactivate()
delete @activePackages[pack.name]
delete @activatingPackages[pack.name]
@@ -532,11 +541,12 @@ class PackageManager
unless typeof metadata.name is 'string' and metadata.name.length > 0
metadata.name = packageName
if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string'
metadata.repository.url = metadata.repository.url.replace(/(^git\+)|(\.git$)/g, '')
metadata
normalizePackageMetadata: (metadata) ->
unless metadata?._id
normalizePackageData ?= require 'normalize-package-data'
normalizePackageData(metadata)
if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string'
metadata.repository.url = metadata.repository.url.replace(/^git\+/, '')

View File

@@ -84,7 +84,7 @@ class Package
@loadKeymaps()
@loadMenus()
@loadStylesheets()
@loadDeserializers()
@registerDeserializerMethods()
@configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata()
@settingsPromise = @loadSettings()
if @shouldRequireMainModuleOnLoad() and not @mainModule?
@@ -277,24 +277,24 @@ class Package
@stylesheets = @getStylesheetPaths().map (stylesheetPath) =>
[stylesheetPath, @themeManager.loadStylesheet(stylesheetPath, true)]
loadDeserializers: ->
registerDeserializerMethods: ->
if @metadata.deserializers?
for name, implementationPath of @metadata.deserializers
do =>
deserializePath = path.join(@path, implementationPath)
deserializeFunction = null
atom.deserializers.add
name: name,
deserialize: =>
@registerViewProviders()
deserializeFunction ?= require(deserializePath)
deserializeFunction.apply(this, arguments)
Object.keys(@metadata.deserializers).forEach (deserializerName) =>
methodName = @metadata.deserializers[deserializerName]
atom.deserializers.add
name: deserializerName,
deserialize: (state, atomEnvironment) =>
@registerViewProviders()
@requireMainModule()
@mainModule[methodName](state, atomEnvironment)
return
registerViewProviders: ->
if @metadata.viewProviders? and not @registeredViewProviders
for implementationPath in @metadata.viewProviders
@viewRegistry.addViewProvider(require(path.join(@path, implementationPath)))
@requireMainModule()
@metadata.viewProviders.forEach (methodName) =>
@viewRegistry.addViewProvider (model) =>
@mainModule[methodName](model)
@registeredViewProviders = true
getStylesheetsPath: ->

View File

@@ -1,5 +1,6 @@
Grim = require 'grim'
{find, compact, extend, last} = require 'underscore-plus'
{Emitter} = require 'event-kit'
{CompositeDisposable, Emitter} = require 'event-kit'
Model = require './model'
PaneAxis = require './pane-axis'
TextEditor = require './text-editor'
@@ -8,6 +9,11 @@ TextEditor = require './text-editor'
# Panes can contain multiple items, one of which is *active* at a given time.
# The view corresponding to the active item is displayed in the interface. In
# the default configuration, tabs are also displayed for each item.
#
# Each pane may also contain one *pending* item. When a pending item is added
# to a pane, it will replace the currently pending item, if any, instead of
# simply being added. In the default configuration, the text in the tab for
# pending items is shown in italics.
module.exports =
class Pane extends Model
container: undefined
@@ -15,7 +21,7 @@ class Pane extends Model
focused: false
@deserialize: (state, {deserializers, applicationDelegate, config, notifications}) ->
{items, activeItemURI, activeItemUri} = state
{items, itemStackIndices, activeItemURI, activeItemUri} = state
activeItemURI ?= activeItemUri
state.items = compact(items.map (itemState) -> deserializers.deserialize(itemState))
state.activeItem = find state.items, (item) ->
@@ -37,20 +43,25 @@ class Pane extends Model
} = params
@emitter = new Emitter
@itemSubscriptions = new WeakMap
@subscriptionsPerItem = new WeakMap
@items = []
@itemStack = []
@addItems(compact(params?.items ? []))
@setActiveItem(@items[0]) unless @getActiveItem()?
@addItemsToStack(params?.itemStackIndices ? [])
@setFlexScale(params?.flexScale ? 1)
serialize: ->
if typeof @activeItem?.getURI is 'function'
activeItemURI = @activeItem.getURI()
itemsToBeSerialized = compact(@items.map((item) -> item if typeof item.serialize is 'function'))
itemStackIndices = (itemsToBeSerialized.indexOf(item) for item in @itemStack when typeof item.serialize is 'function')
deserializer: 'Pane'
id: @id
items: compact(@items.map((item) -> item.serialize?()))
items: itemsToBeSerialized.map((item) -> item.serialize())
itemStackIndices: itemStackIndices
activeItemURI: activeItemURI
focused: @focused
flexScale: @flexScale
@@ -260,8 +271,8 @@ class Pane extends Model
getPanes: -> [this]
unsubscribeFromItem: (item) ->
@itemSubscriptions.get(item)?.dispose()
@itemSubscriptions.delete(item)
@subscriptionsPerItem.get(item)?.dispose()
@subscriptionsPerItem.delete(item)
###
Section: Items
@@ -278,12 +289,30 @@ class Pane extends Model
# Returns a pane item.
getActiveItem: -> @activeItem
setActiveItem: (activeItem) ->
setActiveItem: (activeItem, options) ->
{modifyStack} = options if options?
unless activeItem is @activeItem
@addItemToStack(activeItem) unless modifyStack is false
@activeItem = activeItem
@emitter.emit 'did-change-active-item', @activeItem
@activeItem
# Build the itemStack after deserializing
addItemsToStack: (itemStackIndices) ->
if @items.length > 0
if itemStackIndices.length is 0 or itemStackIndices.length isnt @items.length or itemStackIndices.indexOf(-1) >= 0
itemStackIndices = (i for i in [0..@items.length-1])
for itemIndex in itemStackIndices
@addItemToStack(@items[itemIndex])
return
# Add item (or move item) to the end of the itemStack
addItemToStack: (newItem) ->
return unless newItem?
index = @itemStack.indexOf(newItem)
@itemStack.splice(index, 1) unless index is -1
@itemStack.push(newItem)
# Return an {TextEditor} if the pane item is an {TextEditor}, or null otherwise.
getActiveEditor: ->
@activeItem if @activeItem instanceof TextEditor
@@ -296,6 +325,29 @@ class Pane extends Model
itemAtIndex: (index) ->
@items[index]
# Makes the next item in the itemStack active.
activateNextRecentlyUsedItem: ->
if @items.length > 1
@itemStackIndex = @itemStack.length - 1 unless @itemStackIndex?
@itemStackIndex = @itemStack.length if @itemStackIndex is 0
@itemStackIndex = @itemStackIndex - 1
nextRecentlyUsedItem = @itemStack[@itemStackIndex]
@setActiveItem(nextRecentlyUsedItem, modifyStack: false)
# Makes the previous item in the itemStack active.
activatePreviousRecentlyUsedItem: ->
if @items.length > 1
if @itemStackIndex + 1 is @itemStack.length or not @itemStackIndex?
@itemStackIndex = -1
@itemStackIndex = @itemStackIndex + 1
previousRecentlyUsedItem = @itemStack[@itemStackIndex]
@setActiveItem(previousRecentlyUsedItem, modifyStack: false)
# Moves the active item to the end of the itemStack once the ctrl key is lifted
moveActiveItemToTopOfStack: ->
delete @itemStackIndex
@addItemToStack(@activeItem)
# Public: Makes the next item active.
activateNextItem: ->
index = @getActiveItemIndex()
@@ -342,43 +394,81 @@ class Pane extends Model
# Public: Make the given item *active*, causing it to be displayed by
# the pane's view.
activateItem: (item) ->
#
# * `options` (optional) {Object}
# * `pending` (optional) {Boolean} indicating that the item should be added
# in a pending state if it does not yet exist in the pane. Existing pending
# items in a pane are replaced with new pending items when they are opened.
activateItem: (item, options={}) ->
if item?
if @activeItem?.isPending?()
if @getPendingItem() is @activeItem
index = @getActiveItemIndex()
else
index = @getActiveItemIndex() + 1
@addItem(item, index, false)
@addItem(item, extend({}, options, {index: index}))
@setActiveItem(item)
# Public: Add the given item to the pane.
#
# * `item` The item to add. It can be a model with an associated view or a
# view.
# * `index` (optional) {Number} indicating the index at which to add the item.
# If omitted, the item is added after the current active item.
# * `options` (optional) {Object}
# * `index` (optional) {Number} indicating the index at which to add the item.
# If omitted, the item is added after the current active item.
# * `pending` (optional) {Boolean} indicating that the item should be
# added in a pending state. Existing pending items in a pane are replaced with
# new pending items when they are opened.
#
# Returns the added item.
addItem: (item, index=@getActiveItemIndex() + 1, moved=false) ->
addItem: (item, options={}) ->
# Backward compat with old API:
# addItem(item, index=@getActiveItemIndex() + 1)
if typeof options is "number"
Grim.deprecate("Pane::addItem(item, #{options}) is deprecated in favor of Pane::addItem(item, {index: #{options}})")
options = index: options
index = options.index ? @getActiveItemIndex() + 1
moved = options.moved ? false
pending = options.pending ? false
throw new Error("Pane items must be objects. Attempted to add item #{item}.") unless item? and typeof item is 'object'
throw new Error("Adding a pane item with URI '#{item.getURI?()}' that has already been destroyed") if item.isDestroyed?()
return if item in @items
if item.isPending?()
for existingItem, i in @items
if existingItem.isPending?()
@destroyItem(existingItem)
break
if typeof item.onDidDestroy is 'function'
@itemSubscriptions.set item, item.onDidDestroy => @removeItem(item, false)
itemSubscriptions = new CompositeDisposable
itemSubscriptions.add item.onDidDestroy => @removeItem(item, false)
if typeof item.onDidTerminatePendingState is "function"
itemSubscriptions.add item.onDidTerminatePendingState =>
@clearPendingItem() if @getPendingItem() is item
itemSubscriptions.add item.onDidDestroy => @removeItem(item, false)
@subscriptionsPerItem.set item, itemSubscriptions
@items.splice(index, 0, item)
lastPendingItem = @getPendingItem()
@setPendingItem(item) if pending
@emitter.emit 'did-add-item', {item, index, moved}
@destroyItem(lastPendingItem) if lastPendingItem? and not moved
@setActiveItem(item) unless @getActiveItem()?
item
setPendingItem: (item) =>
if @pendingItem isnt item
mostRecentPendingItem = @pendingItem
@pendingItem = item
@emitter.emit 'item-did-terminate-pending-state', mostRecentPendingItem
getPendingItem: =>
@pendingItem or null
clearPendingItem: =>
@setPendingItem(null)
onItemDidTerminatePendingState: (callback) =>
@emitter.on 'item-did-terminate-pending-state', callback
# Public: Add the given items to the pane.
#
# * `items` An {Array} of items to add. Items can be views or models with
@@ -390,13 +480,14 @@ class Pane extends Model
# Returns an {Array} of added items.
addItems: (items, index=@getActiveItemIndex() + 1) ->
items = items.filter (item) => not (item in @items)
@addItem(item, index + i, false) for item, i in items
@addItem(item, {index: index + i}) for item, i in items
items
removeItem: (item, moved) ->
index = @items.indexOf(item)
return if index is -1
@pendingItem = null if @getPendingItem() is item
@removeItemFromStack(item)
@emitter.emit 'will-remove-item', {item, index, destroyed: not moved, moved}
@unsubscribeFromItem(item)
@@ -412,6 +503,14 @@ class Pane extends Model
@container?.didDestroyPaneItem({item, index, pane: this}) unless moved
@destroy() if @items.length is 0 and @config.get('core.destroyEmptyPanes')
# Remove the given item from the itemStack.
#
# * `item` The item to remove.
# * `index` {Number} indicating the index to which to remove the item from the itemStack.
removeItemFromStack: (item) ->
index = @itemStack.indexOf(item)
@itemStack.splice(index, 1) unless index is -1
# Public: Move the given item to the given index.
#
# * `item` The item to move.
@@ -430,7 +529,7 @@ class Pane extends Model
# given pane.
moveItemToPane: (item, pane, index) ->
@removeItem(item, true)
pane.addItem(item, index, true)
pane.addItem(item, {index: index, moved: true})
# Public: Destroy the active item and activate the next item.
destroyActiveItem: ->
@@ -661,7 +760,7 @@ class Pane extends Model
@parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this], @flexScale}))
@setFlexScale(1)
newPane = new Pane(extend({@applicationDelegate, @deserializerManager, @config}, params))
newPane = new Pane(extend({@applicationDelegate, @notificationManager, @deserializerManager, @config}, params))
switch side
when 'before' then @parent.insertChildBefore(this, newPane)
when 'after' then @parent.insertChildAfter(this, newPane)
@@ -713,7 +812,7 @@ class Pane extends Model
if @parent.orientation is 'vertical'
bottommostSibling = last(@parent.children)
if bottommostSibling instanceof PaneAxis
@splitRight()
@splitDown()
else
bottommostSibling
else

View File

@@ -54,8 +54,9 @@ class Project extends Model
Section: Serialization
###
deserialize: (state, deserializerManager) ->
deserialize: (state) ->
state.paths = [state.path] if state.path? # backward compatibility
state.paths = state.paths.filter (directoryPath) -> fs.isDirectorySync(directoryPath)
@buffers = _.compact state.buffers.map (bufferState) ->
# Check that buffer's file path is accessible
@@ -65,15 +66,15 @@ class Project extends Model
fs.closeSync(fs.openSync(bufferState.filePath, 'r'))
catch error
return unless error.code is 'ENOENT'
deserializerManager.deserialize(bufferState)
TextBuffer.deserialize(bufferState)
@subscribeToBuffer(buffer) for buffer in @buffers
@setPaths(state.paths)
serialize: ->
serialize: (options={}) ->
deserializer: 'Project'
paths: @getPaths()
buffers: _.compact(@buffers.map (buffer) -> buffer.serialize() if buffer.isRetained())
buffers: _.compact(@buffers.map (buffer) -> buffer.serialize({markerLayers: options.isUnloading is true}) if buffer.isRetained())
###
Section: Event Subscription

View File

@@ -1,7 +1,10 @@
ipc = require 'ipc'
{ipcRenderer} = require 'electron'
module.exports = ({commandRegistry, commandInstaller, config}) ->
commandRegistry.add 'atom-workspace',
'pane:show-next-recently-used-item': -> @getModel().getActivePane().activateNextRecentlyUsedItem()
'pane:show-previous-recently-used-item': -> @getModel().getActivePane().activatePreviousRecentlyUsedItem()
'pane:move-active-item-to-top-of-stack': -> @getModel().getActivePane().moveActiveItemToTopOfStack()
'pane:show-next-item': -> @getModel().getActivePane().activateNextItem()
'pane:show-previous-item': -> @getModel().getActivePane().activatePreviousItem()
'pane:show-item-1': -> @getModel().getActivePane().activateItemAtIndex(0)
@@ -18,30 +21,30 @@ module.exports = ({commandRegistry, commandInstaller, config}) ->
'window:increase-font-size': -> @getModel().increaseFontSize()
'window:decrease-font-size': -> @getModel().decreaseFontSize()
'window:reset-font-size': -> @getModel().resetFontSize()
'application:about': -> ipc.send('command', 'application:about')
'application:show-preferences': -> ipc.send('command', 'application:show-settings')
'application:show-settings': -> ipc.send('command', 'application:show-settings')
'application:quit': -> ipc.send('command', 'application:quit')
'application:hide': -> ipc.send('command', 'application:hide')
'application:hide-other-applications': -> ipc.send('command', 'application:hide-other-applications')
'application:install-update': -> ipc.send('command', 'application:install-update')
'application:unhide-all-applications': -> ipc.send('command', 'application:unhide-all-applications')
'application:new-window': -> ipc.send('command', 'application:new-window')
'application:new-file': -> ipc.send('command', 'application:new-file')
'application:open': -> ipc.send('command', 'application:open')
'application:open-file': -> ipc.send('command', 'application:open-file')
'application:open-folder': -> ipc.send('command', 'application:open-folder')
'application:open-dev': -> ipc.send('command', 'application:open-dev')
'application:open-safe': -> ipc.send('command', 'application:open-safe')
'application:about': -> ipcRenderer.send('command', 'application:about')
'application:show-preferences': -> ipcRenderer.send('command', 'application:show-settings')
'application:show-settings': -> ipcRenderer.send('command', 'application:show-settings')
'application:quit': -> ipcRenderer.send('command', 'application:quit')
'application:hide': -> ipcRenderer.send('command', 'application:hide')
'application:hide-other-applications': -> ipcRenderer.send('command', 'application:hide-other-applications')
'application:install-update': -> ipcRenderer.send('command', 'application:install-update')
'application:unhide-all-applications': -> ipcRenderer.send('command', 'application:unhide-all-applications')
'application:new-window': -> ipcRenderer.send('command', 'application:new-window')
'application:new-file': -> ipcRenderer.send('command', 'application:new-file')
'application:open': -> ipcRenderer.send('command', 'application:open')
'application:open-file': -> ipcRenderer.send('command', 'application:open-file')
'application:open-folder': -> ipcRenderer.send('command', 'application:open-folder')
'application:open-dev': -> ipcRenderer.send('command', 'application:open-dev')
'application:open-safe': -> ipcRenderer.send('command', 'application:open-safe')
'application:add-project-folder': -> atom.addProjectFolder()
'application:minimize': -> ipc.send('command', 'application:minimize')
'application:zoom': -> ipc.send('command', 'application:zoom')
'application:bring-all-windows-to-front': -> ipc.send('command', 'application:bring-all-windows-to-front')
'application:open-your-config': -> ipc.send('command', 'application:open-your-config')
'application:open-your-init-script': -> ipc.send('command', 'application:open-your-init-script')
'application:open-your-keymap': -> ipc.send('command', 'application:open-your-keymap')
'application:open-your-snippets': -> ipc.send('command', 'application:open-your-snippets')
'application:open-your-stylesheet': -> ipc.send('command', 'application:open-your-stylesheet')
'application:minimize': -> ipcRenderer.send('command', 'application:minimize')
'application:zoom': -> ipcRenderer.send('command', 'application:zoom')
'application:bring-all-windows-to-front': -> ipcRenderer.send('command', 'application:bring-all-windows-to-front')
'application:open-your-config': -> ipcRenderer.send('command', 'application:open-your-config')
'application:open-your-init-script': -> ipcRenderer.send('command', 'application:open-your-init-script')
'application:open-your-keymap': -> ipcRenderer.send('command', 'application:open-your-keymap')
'application:open-your-snippets': -> ipcRenderer.send('command', 'application:open-your-snippets')
'application:open-your-stylesheet': -> ipcRenderer.send('command', 'application:open-your-stylesheet')
'application:open-license': -> @getModel().openLicense()
'window:run-package-specs': -> @runPackageSpecs()
'window:focus-next-pane': -> @getModel().activateNextPane()

View File

@@ -1,6 +1,6 @@
# Using clipboard in renderer process is not safe on Linux.
module.exports =
if process.platform is 'linux' and process.type is 'renderer'
require('remote').require('clipboard')
require('electron').remote.clipboard
else
require('clipboard')
require('electron').clipboard

View File

@@ -378,7 +378,7 @@ class Selection extends Model
indentAdjustment = @editor.indentLevelForLine(precedingText) - options.indentBasis
@adjustIndent(remainingLines, indentAdjustment)
if options.autoIndent and not NonWhitespaceRegExp.test(precedingText) and remainingLines.length > 0
if options.autoIndent and NonWhitespaceRegExp.test(text) and not NonWhitespaceRegExp.test(precedingText) and remainingLines.length > 0
autoIndentFirstLine = true
firstLine = precedingText + firstInsertedLine
desiredIndentLevel = @editor.languageMode.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine)
@@ -749,7 +749,7 @@ class Selection extends Model
#
# * `otherSelection` A {Selection} to compare against
compare: (otherSelection) ->
@getBufferRange().compare(otherSelection.getBufferRange())
@marker.compare(otherSelection.marker)
###
Section: Private Utilities
@@ -804,11 +804,11 @@ class Selection extends Model
@wordwise = false
@linewise = false
autoscroll: ->
autoscroll: (options) ->
if @marker.hasTail()
@editor.scrollToScreenRange(@getScreenRange(), reversed: @isReversed())
@editor.scrollToScreenRange(@getScreenRange(), Object.assign({reversed: @isReversed()}, options))
else
@cursor.autoscroll()
@cursor.autoscroll(options)
clearAutoscroll: ->

95
src/state-store.js Normal file
View File

@@ -0,0 +1,95 @@
'use strict'
module.exports =
class StateStore {
constructor (databaseName, version) {
this.dbPromise = new Promise((resolve) => {
let dbOpenRequest = indexedDB.open(databaseName, version)
dbOpenRequest.onupgradeneeded = (event) => {
let db = event.target.result
db.createObjectStore('states')
}
dbOpenRequest.onsuccess = () => {
resolve(dbOpenRequest.result)
}
dbOpenRequest.onerror = (error) => {
console.error('Could not connect to indexedDB', error)
resolve(null)
}
})
}
connect () {
return this.dbPromise.then(db => !!db)
}
save (key, value) {
return new Promise((resolve, reject) => {
this.dbPromise.then(db => {
if (db == null) resolve()
var request = db.transaction(['states'], 'readwrite')
.objectStore('states')
.put({value: value, storedAt: new Date().toString()}, key)
request.onsuccess = resolve
request.onerror = reject
})
})
}
load (key) {
return this.dbPromise.then(db => {
if (!db) return
return new Promise((resolve, reject) => {
var request = db.transaction(['states'])
.objectStore('states')
.get(key)
request.onsuccess = (event) => {
let result = event.target.result
if (result && !result.isJSON) {
resolve(result.value)
} else {
resolve(null)
}
}
request.onerror = (event) => reject(event)
})
})
}
clear () {
return this.dbPromise.then(db => {
if (!db) return
return new Promise((resolve, reject) => {
var request = db.transaction(['states'], 'readwrite')
.objectStore('states')
.clear()
request.onsuccess = resolve
request.onerror = reject
})
})
}
count () {
return this.dbPromise.then(db => {
if (!db) return
return new Promise((resolve, reject) => {
var request = db.transaction(['states'])
.objectStore('states')
.count()
request.onsuccess = () => {
resolve(request.result)
}
request.onerror = reject
})
})
}
}

View File

@@ -6,7 +6,7 @@ class StorageFolder
constructor: (containingPath) ->
@path = path.join(containingPath, "storage") if containingPath?
store: (name, object) ->
storeSync: (name, object) ->
return unless @path?
fs.writeFileSync(@pathForKey(name), JSON.stringify(object), 'utf8')

View File

@@ -2,7 +2,7 @@ _ = require 'underscore-plus'
scrollbarStyle = require 'scrollbar-style'
{Range, Point} = require 'text-buffer'
{CompositeDisposable} = require 'event-kit'
ipc = require 'ipc'
{ipcRenderer} = require 'electron'
TextEditorPresenter = require './text-editor-presenter'
GutterContainerComponent = require './gutter-container-component'
@@ -43,7 +43,7 @@ class TextEditorComponent
@assert domNode?, "TextEditorComponent::domNode was set to null."
@domNodeValue = domNode
constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, tileSize, @views, @themes, @config, @workspace, @assert, @grammars}) ->
constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, tileSize, @views, @themes, @config, @workspace, @assert, @grammars, scrollPastEnd}) ->
@tileSize = tileSize if tileSize?
@disposables = new CompositeDisposable
@@ -61,6 +61,7 @@ class TextEditorComponent
stoppedScrollingDelay: 200
config: @config
lineTopIndex: lineTopIndex
scrollPastEnd: scrollPastEnd
@presenter.onDidUpdateState(@requestUpdate)
@@ -279,10 +280,10 @@ class TextEditorComponent
writeSelectedTextToSelectionClipboard = =>
return if @editor.isDestroyed()
if selectedText = @editor.getSelectedText()
# This uses ipc.send instead of clipboard.writeText because
# clipboard.writeText is a sync ipc call on Linux and that
# This uses ipcRenderer.send instead of clipboard.writeText because
# clipboard.writeText is a sync ipcRenderer call on Linux and that
# will slow down selections.
ipc.send('write-text-to-selection-clipboard', selectedText)
ipcRenderer.send('write-text-to-selection-clipboard', selectedText)
@disposables.add @editor.onDidChangeSelectionRange ->
clearTimeout(timeoutId)
timeoutId = setTimeout(writeSelectedTextToSelectionClipboard)

View File

@@ -17,6 +17,8 @@ class TextEditorElement extends HTMLElement
focusOnAttach: false
hasTiledRendering: true
logicalDisplayBuffer: true
scrollPastEnd: true
autoHeight: true
createdCallback: ->
# Use globals when the following instance variables aren't set.
@@ -38,6 +40,9 @@ class TextEditorElement extends HTMLElement
@setAttribute('tabindex', -1)
initializeContent: (attributes) ->
unless @autoHeight
@style.height = "100%"
if @config.get('editor.useShadowDOM')
@useShadowDOM = true
@@ -86,7 +91,7 @@ class TextEditorElement extends HTMLElement
@subscriptions.add @component.onDidChangeScrollLeft =>
@emitter.emit("did-change-scroll-left", arguments...)
initialize: (model, {@views, @config, @themes, @workspace, @assert, @styles, @grammars}) ->
initialize: (model, {@views, @config, @themes, @workspace, @assert, @styles, @grammars}, @autoHeight = true, @scrollPastEnd = true) ->
throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @views?
throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @config?
throw new Error("Must pass a themes parameter when initializing TextEditorElements") unless @themes?
@@ -143,6 +148,7 @@ class TextEditorElement extends HTMLElement
workspace: @workspace
assert: @assert
grammars: @grammars
scrollPastEnd: @scrollPastEnd
)
@rootElement.appendChild(@component.getDomNode())

Some files were not shown because too many files have changed in this diff Show More