diff --git a/.gitignore b/.gitignore index 6eec21c2a..bce6c56d3 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ debug.log docs/output docs/includes spec/fixtures/evil-files/ +out/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ada420a40..b4519bcf1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..73ff2f50d --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -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. diff --git a/apm/package.json b/apm/package.json index 2e6b0b8ea..4b599bc39 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "1.6.0" + "atom-package-manager": "1.7.1" } } diff --git a/atom.sh b/atom.sh index ef8dbcdc4..b68716bf4 100755 --- a/atom.sh +++ b/atom.sh @@ -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 diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index a34e1fbd4..d9375d05c 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -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? diff --git a/build/package.json b/build/package.json index 6c41d4355..2f8d88d8a 100644 --- a/build/package.json +++ b/build/package.json @@ -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", diff --git a/build/tasks/publish-build-task.coffee b/build/tasks/publish-build-task.coffee index 4f8df6336..de46eb4fe 100644 --- a/build/tasks/publish-build-task.coffee +++ b/build/tasks/publish-build-task.coffee @@ -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) -> diff --git a/build/tasks/set-version-task.coffee b/build/tasks/set-version-task.coffee index fc2382476..c7a29b584 100644 --- a/build/tasks/set-version-task.coffee +++ b/build/tasks/set-version-task.coffee @@ -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 diff --git a/build/tasks/spec-task.coffee b/build/tasks/spec-task.coffee index 892c92696..40b7ad5ce 100644 --- a/build/tasks/spec-task.coffee +++ b/build/tasks/spec-task.coffee @@ -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] diff --git a/build/tasks/task-helpers.coffee b/build/tasks/task-helpers.coffee index d24cdec77..b42b4dd15 100644 --- a/build/tasks/task-helpers.coffee +++ b/build/tasks/task-helpers.coffee @@ -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 diff --git a/circle.yml b/circle.yml new file mode 100644 index 000000000..a55900cca --- /dev/null +++ b/circle.yml @@ -0,0 +1,4 @@ +general: + branches: + only: + - io-circle-ci diff --git a/docs/build-instructions/linux.md b/docs/build-instructions/linux.md index fc2f73ee7..1d746bcd6 100644 --- a/docs/build-instructions/linux.md +++ b/docs/build-instructions/linux.md @@ -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 diff --git a/docs/build-instructions/windows.md b/docs/build-instructions/windows.md index 7eb2ed03d..fdd55fdde 100644 --- a/docs/build-instructions/windows.md +++ b/docs/build-instructions/windows.md @@ -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. diff --git a/keymaps/darwin.cson b/keymaps/darwin.cson index 4859f9b67..819e0079e 100644 --- a/keymaps/darwin.cson +++ b/keymaps/darwin.cson @@ -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' diff --git a/keymaps/linux.cson b/keymaps/linux.cson index 9ddb760e2..e676ed5ab 100644 --- a/keymaps/linux.cson +++ b/keymaps/linux.cson @@ -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' diff --git a/keymaps/win32.cson b/keymaps/win32.cson index e4703bac8..5869a3ed8 100644 --- a/keymaps/win32.cson +++ b/keymaps/win32.cson @@ -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' diff --git a/menus/darwin.cson b/menus/darwin.cson index a2636887d..7fa2aaf6d 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -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' } diff --git a/menus/linux.cson b/menus/linux.cson index 1276748d8..7cfd72885 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -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' } diff --git a/menus/win32.cson b/menus/win32.cson index a7d41b28f..349e3e064 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -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' } diff --git a/package.json b/package.json index 300009216..4093ee7d0 100644 --- a/package.json +++ b/package.json @@ -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" ] } } diff --git a/resources/win/atom.cmd b/resources/win/atom.cmd index c9bfdd5ba..a1af5cd53 100644 --- a/resources/win/atom.cmd +++ b/resources/win/atom.cmd @@ -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 diff --git a/resources/win/atom.sh b/resources/win/atom.sh index 0eaf193c0..7380bf122 100644 --- a/resources/win/atom.sh +++ b/resources/win/atom.sh @@ -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 "$@" diff --git a/script/clean b/script/clean index fd0aa5bfa..0c947baf2 100755 --- a/script/clean +++ b/script/clean @@ -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); +}; diff --git a/spec/async-spec-helpers.coffee b/spec/async-spec-helpers.coffee index 5f8e03ca3..6ed8a5a2b 100644 --- a/spec/async-spec-helpers.coffee +++ b/spec/async-spec-helpers.coffee @@ -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) -> diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index b5b975112..3283b63d6 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -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' diff --git a/spec/atom-reporter.coffee b/spec/atom-reporter.coffee index 150eb9a4a..dcda4633b 100644 --- a/spec/atom-reporter.coffee +++ b/spec/atom-reporter.coffee @@ -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 diff --git a/spec/auto-update-manager-spec.js b/spec/auto-update-manager-spec.js new file mode 100644 index 000000000..6f7dbbb1a --- /dev/null +++ b/spec/auto-update-manager-spec.js @@ -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) + }) + }) + }) +}) diff --git a/spec/babel-spec.coffee b/spec/babel-spec.coffee index 02f0583ee..e95b000cb 100644 --- a/spec/babel-spec.coffee +++ b/spec/babel-spec.coffee @@ -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 -> diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index c7485ea65..acd9b112b 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -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} diff --git a/spec/fixtures/packages/package-with-deserializers/deserializer-1.js b/spec/fixtures/packages/package-with-deserializers/deserializer-1.js deleted file mode 100644 index f4d7a1488..000000000 --- a/spec/fixtures/packages/package-with-deserializers/deserializer-1.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = function (state) { - return { - wasDeserializedBy: 'Deserializer1', - state: state - } -} diff --git a/spec/fixtures/packages/package-with-deserializers/deserializer-2.js b/spec/fixtures/packages/package-with-deserializers/deserializer-2.js deleted file mode 100644 index 3099d2b15..000000000 --- a/spec/fixtures/packages/package-with-deserializers/deserializer-2.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = function (state) { - return { - wasDeserializedBy: 'Deserializer2', - state: state - } -} diff --git a/spec/fixtures/packages/package-with-deserializers/index.js b/spec/fixtures/packages/package-with-deserializers/index.js index 19bba5ecb..b9be23854 100644 --- a/spec/fixtures/packages/package-with-deserializers/index.js +++ b/spec/fixtures/packages/package-with-deserializers/index.js @@ -1,3 +1,17 @@ module.exports = { - activate: function() {} + activate () {}, + + deserializeMethod1 (state) { + return { + wasDeserializedBy: 'deserializeMethod1', + state: state + } + }, + + deserializeMethod2 (state) { + return { + wasDeserializedBy: 'deserializeMethod2', + state: state + } + } } diff --git a/spec/fixtures/packages/package-with-deserializers/package.json b/spec/fixtures/packages/package-with-deserializers/package.json index daa5776bf..bae0776a6 100644 --- a/spec/fixtures/packages/package-with-deserializers/package.json +++ b/spec/fixtures/packages/package-with-deserializers/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "main": "./index", "deserializers": { - "Deserializer1": "./deserializer-1.js", - "Deserializer2": "./deserializer-2.js" + "Deserializer1": "deserializeMethod1", + "Deserializer2": "deserializeMethod2" } } diff --git a/spec/fixtures/packages/package-with-prefixed-and-suffixed-repo-url/package.json b/spec/fixtures/packages/package-with-prefixed-and-suffixed-repo-url/package.json new file mode 100644 index 000000000..ce57f7501 --- /dev/null +++ b/spec/fixtures/packages/package-with-prefixed-and-suffixed-repo-url/package.json @@ -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." +} diff --git a/spec/fixtures/packages/package-with-view-providers/deserializer.js b/spec/fixtures/packages/package-with-view-providers/deserializer.js deleted file mode 100644 index 334e7b2ab..000000000 --- a/spec/fixtures/packages/package-with-view-providers/deserializer.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function (state) { - return {state: state} -} diff --git a/spec/fixtures/packages/package-with-view-providers/index.js b/spec/fixtures/packages/package-with-view-providers/index.js index 19bba5ecb..66e62171d 100644 --- a/spec/fixtures/packages/package-with-view-providers/index.js +++ b/spec/fixtures/packages/package-with-view-providers/index.js @@ -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 + } + } } diff --git a/spec/fixtures/packages/package-with-view-providers/package.json b/spec/fixtures/packages/package-with-view-providers/package.json index f67477280..eb5c80025 100644 --- a/spec/fixtures/packages/package-with-view-providers/package.json +++ b/spec/fixtures/packages/package-with-view-providers/package.json @@ -3,10 +3,10 @@ "main": "./index", "version": "1.0.0", "deserializers": { - "DeserializerFromPackageWithViewProviders": "./deserializer" + "DeserializerFromPackageWithViewProviders": "theDeserializerMethod" }, "viewProviders": [ - "./view-provider-1", - "./view-provider-2" + "viewProviderMethod1", + "viewProviderMethod2" ] } diff --git a/spec/fixtures/packages/package-with-view-providers/view-provider-1.js b/spec/fixtures/packages/package-with-view-providers/view-provider-1.js deleted file mode 100644 index e4f0dcc0b..000000000 --- a/spec/fixtures/packages/package-with-view-providers/view-provider-1.js +++ /dev/null @@ -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 - } -} diff --git a/spec/fixtures/packages/package-with-view-providers/view-provider-2.js b/spec/fixtures/packages/package-with-view-providers/view-provider-2.js deleted file mode 100644 index a3b58a3aa..000000000 --- a/spec/fixtures/packages/package-with-view-providers/view-provider-2.js +++ /dev/null @@ -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 - } -} diff --git a/spec/fixtures/sample-with-comments.js b/spec/fixtures/sample-with-comments.js index c10d42232..b40ddc890 100644 --- a/spec/fixtures/sample-with-comments.js +++ b/spec/fixtures/sample-with-comments.js @@ -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)); -}; \ No newline at end of file +}; diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index dfc3d5803..900d81bfb 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -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') diff --git a/spec/git-spec.coffee b/spec/git-spec.coffee index cda2afaa8..3afd4da75 100644 --- a/spec/git-spec.coffee +++ b/spec/git-spec.coffee @@ -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 -> diff --git a/spec/integration/helpers/start-atom.coffee b/spec/integration/helpers/start-atom.coffee index 3c1016ad2..2939dd6ab 100644 --- a/spec/integration/helpers/start-atom.coffee +++ b/spec/integration/helpers/start-atom.coffee @@ -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) -> diff --git a/spec/integration/startup-spec.coffee b/spec/integration/startup-spec.coffee index 6e8a7f55a..f6b0e1cf3 100644 --- a/spec/integration/startup-spec.coffee +++ b/spec/integration/startup-spec.coffee @@ -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 diff --git a/spec/jasmine-test-runner.coffee b/spec/jasmine-test-runner.coffee index 62b42d0a9..5b5d4e225 100644 --- a/spec/jasmine-test-runner.coffee +++ b/spec/jasmine-test-runner.coffee @@ -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 diff --git a/spec/language-mode-spec.coffee b/spec/language-mode-spec.coffee index 7ea4a1ae9..26bb19b0e 100644 --- a/spec/language-mode-spec.coffee +++ b/spec/language-mode-spec.coffee @@ -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 diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 46d1d11ee..3b54691b2 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -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') diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee index 8c228e2a8..d0b191f38 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -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] diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 9d42f9a7e..499efd017 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -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 diff --git a/spec/selection-spec.coffee b/spec/selection-spec.coffee index ec40e32cc..319e2d438 100644 --- a/spec/selection-spec.coffee +++ b/spec/selection-spec.coffee @@ -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 "" diff --git a/spec/state-store-spec.js b/spec/state-store-spec.js new file mode 100644 index 000000000..95fdcb71b --- /dev/null +++ b/spec/state-store-spec.js @@ -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) + }) + }) + }) +}) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 9663c6222..37a9751e1 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -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 diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 9253ff103..f8117af09 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -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'}) diff --git a/spec/text-editor-registry-spec.coffee b/spec/text-editor-registry-spec.coffee new file mode 100644 index 000000000..04665bef2 --- /dev/null +++ b/spec/text-editor-registry-spec.coffee @@ -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 diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 1f998f4a5..852e7b50a 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -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) diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index 134121594..4dec5bdf5 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -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", -> diff --git a/spec/tooltip-manager-spec.coffee b/spec/tooltip-manager-spec.coffee index 87082504a..d4bfc1bd6 100644 --- a/spec/tooltip-manager-spec.coffee +++ b/spec/tooltip-manager-spec.coffee @@ -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) diff --git a/spec/view-registry-spec.coffee b/spec/view-registry-spec.coffee index 16672b25d..68a482b48 100644 --- a/spec/view-registry-spec.coffee +++ b/spec/view-registry-spec.coffee @@ -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", -> diff --git a/spec/window-event-handler-spec.coffee b/spec/window-event-handler-spec.coffee index a988ae7de..bb7e1665b 100644 --- a/spec/window-event-handler-spec.coffee +++ b/spec/window-event-handler-spec.coffee @@ -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", -> diff --git a/spec/workspace-element-spec.coffee b/spec/workspace-element-spec.coffee index 883bad2fc..cf773b4ef 100644 --- a/spec/workspace-element-spec.coffee +++ b/spec/workspace-element-spec.coffee @@ -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() diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index e89e4c6bd..38d4839b0 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -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() diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index 59259d223..3aff9e457 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -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') diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index e8d656a43..8c79d66f6 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -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)) diff --git a/src/auto-update-manager.js b/src/auto-update-manager.js new file mode 100644 index 000000000..62cc03f85 --- /dev/null +++ b/src/auto-update-manager.js @@ -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 + } +} diff --git a/src/browser/application-menu.coffee b/src/browser/application-menu.coffee index 74da80e43..b0a6e3267 100644 --- a/src/browser/application-menu.coffee +++ b/src/browser/application-menu.coffee @@ -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. diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index 44848eb72..230e1bb9f 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -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. # diff --git a/src/browser/atom-portable.coffee b/src/browser/atom-portable.coffee index 5f8f10cf6..ae4bb67ec 100644 --- a/src/browser/atom-portable.coffee +++ b/src/browser/atom-portable.coffee @@ -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 diff --git a/src/browser/atom-protocol-handler.coffee b/src/browser/atom-protocol-handler.coffee index 0fda8095b..3967c0525 100644 --- a/src/browser/atom-protocol-handler.coffee +++ b/src/browser/atom-protocol-handler.coffee @@ -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. # diff --git a/src/browser/atom-window.coffee b/src/browser/atom-window.coffee index 6e2d39266..33c64da7d 100644 --- a/src/browser/atom-window.coffee +++ b/src/browser/atom-window.coffee @@ -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() diff --git a/src/browser/auto-update-manager.coffee b/src/browser/auto-update-manager.coffee index 6a008d44d..c8c57cb01 100644 --- a/src/browser/auto-update-manager.coffee +++ b/src/browser/auto-update-manager.coffee @@ -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'] diff --git a/src/browser/auto-updater-win32.coffee b/src/browser/auto-updater-win32.coffee index 4d043ac4e..e31578d49 100644 --- a/src/browser/auto-updater-win32.coffee +++ b/src/browser/auto-updater-win32.coffee @@ -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) -> diff --git a/src/browser/context-menu.coffee b/src/browser/context-menu.coffee index 44b57cdc9..1bc9c29ba 100644 --- a/src/browser/context-menu.coffee +++ b/src/browser/context-menu.coffee @@ -1,4 +1,4 @@ -Menu = require 'menu' +{Menu} = require 'electron' module.exports = class ContextMenu diff --git a/src/browser/main.coffee b/src/browser/main.coffee index ca9d7e3ae..b4df62bd6 100644 --- a/src/browser/main.coffee +++ b/src/browser/main.coffee @@ -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() diff --git a/src/browser/squirrel-update.coffee b/src/browser/squirrel-update.coffee index 3660158fc..9d6f772db 100644 --- a/src/browser/squirrel-update.coffee +++ b/src/browser/squirrel-update.coffee @@ -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 # diff --git a/src/buffered-node-process.coffee b/src/buffered-node-process.coffee index bb1a1c655..3b4916b24 100644 --- a/src/buffered-node-process.coffee +++ b/src/buffered-node-process.coffee @@ -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) diff --git a/src/color.coffee b/src/color.coffee index fc751ce42..b413b9e2c 100644 --- a/src/color.coffee +++ b/src/color.coffee @@ -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 diff --git a/src/config-schema.coffee b/src/config-schema.coffee index 88e00c71d..346551ff5 100644 --- a/src/config-schema.coffee +++ b/src/config-schema.coffee @@ -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' diff --git a/src/config.coffee b/src/config.coffee index 348b7b94f..66f07516e 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -319,6 +319,23 @@ ScopeDescriptor = require './scope-descriptor' # * line breaks - `line breaks
` # * ~~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. diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index 1d80dec09..936a9c6b6 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -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'] diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 980148229..cba8a6551 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -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. # diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 38792e02e..f80f46a13 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -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) { diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 2fdcd790a..0513c2293 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -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}) => diff --git a/src/initialize-application-window.coffee b/src/initialize-application-window.coffee index 57aa33ce0..cea4e1c3c 100644 --- a/src/initialize-application-window.coffee +++ b/src/initialize-application-window.coffee @@ -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) diff --git a/src/initialize-test-window.coffee b/src/initialize-test-window.coffee index f3507b479..690180fc8 100644 --- a/src/initialize-test-window.coffee +++ b/src/initialize-test-window.coffee @@ -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) diff --git a/src/ipc-helpers.js b/src/ipc-helpers.js new file mode 100644 index 000000000..c0b38c50e --- /dev/null +++ b/src/ipc-helpers.js @@ -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' +} diff --git a/src/language-mode.coffee b/src/language-mode.coffee index b58744558..e60c661b2 100644 --- a/src/language-mode.coffee +++ b/src/language-mode.coffee @@ -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') diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee index fa78d3cd6..67076dbfa 100644 --- a/src/menu-manager.coffee +++ b/src/menu-manager.coffee @@ -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) -> diff --git a/src/module-cache.coffee b/src/module-cache.coffee index e9245cf40..a2840a864 100644 --- a/src/module-cache.coffee +++ b/src/module-cache.coffee @@ -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") diff --git a/src/package-manager.coffee b/src/package-manager.coffee index 1ecdc5448..94b55a793 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -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\+/, '') diff --git a/src/package.coffee b/src/package.coffee index 8230ce4e4..94e759947 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -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: -> diff --git a/src/pane.coffee b/src/pane.coffee index 0a5cca4c3..3ff62993c 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -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 diff --git a/src/project.coffee b/src/project.coffee index 008d81e3e..93a3ed496 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -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 diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index 32c74569b..dacb1d228 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -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() diff --git a/src/safe-clipboard.coffee b/src/safe-clipboard.coffee index 8301f9d54..1f91803e2 100644 --- a/src/safe-clipboard.coffee +++ b/src/safe-clipboard.coffee @@ -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 diff --git a/src/selection.coffee b/src/selection.coffee index 7afa10767..faee09742 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -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: -> diff --git a/src/state-store.js b/src/state-store.js new file mode 100644 index 000000000..a2d3b476b --- /dev/null +++ b/src/state-store.js @@ -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 + }) + }) + } +} diff --git a/src/storage-folder.coffee b/src/storage-folder.coffee index da8af3f2e..06beae56a 100644 --- a/src/storage-folder.coffee +++ b/src/storage-folder.coffee @@ -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') diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 4a51badbd..9b091100d 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -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) diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 380417163..2a9b5e262 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -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()) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 2a661bd67..ab471684e 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -14,7 +14,7 @@ class TextEditorPresenter minimumReflowInterval: 200 constructor: (params) -> - {@model, @config, @lineTopIndex} = params + {@model, @config, @lineTopIndex, scrollPastEnd} = params {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @tileSize} = params {@contentFrameWidth} = params @tokenIterator = @model.displayLayer.buildTokenIterator() @@ -47,6 +47,8 @@ class TextEditorPresenter @startReflowing() if @continuousReflow @updating = false + @scrollPastEndOverride = scrollPastEnd ? true + setLinesYardstick: (@linesYardstick) -> getLinesYardstick: -> @linesYardstick @@ -432,18 +434,14 @@ class TextEditorPresenter return updateCursorsState: -> - @state.content.cursors = {} - @updateCursorState(cursor) for cursor in @model.cursors # using property directly to avoid allocation - return - - updateCursorState: (cursor) -> return unless @startRow? and @endRow? and @hasPixelRectRequirements() and @baseCharacterWidth? - screenRange = cursor.getScreenRange() - return unless cursor.isVisible() and @startRow <= screenRange.start.row < @endRow - pixelRect = @pixelRectForScreenRange(screenRange) - pixelRect.width = Math.round(@baseCharacterWidth) if pixelRect.width is 0 - @state.content.cursors[cursor.id] = pixelRect + @state.content.cursors = {} + for cursor in @model.cursorsForScreenRowRange(@startRow, @endRow - 1) when cursor.isVisible() + pixelRect = @pixelRectForScreenRange(cursor.getScreenRange()) + pixelRect.width = Math.round(@baseCharacterWidth) if pixelRect.width is 0 + @state.content.cursors[cursor.id] = pixelRect + return updateOverlaysState: -> return unless @hasOverlayPositionRequirements() @@ -603,7 +601,14 @@ class TextEditorPresenter if endRow > startRow bufferRows = @model.bufferRowsForScreenRows(startRow, endRow - 1) + previousBufferRow = -1 + foldable = false for bufferRow, i in bufferRows + # don't compute foldability more than once per buffer row + if previousBufferRow isnt bufferRow + foldable = @model.isFoldableAtBufferRow(bufferRow) + previousBufferRow = bufferRow + if bufferRow is lastBufferRow softWrapped = true else @@ -613,7 +618,6 @@ class TextEditorPresenter screenRow = startRow + i lineId = @lineIdForScreenRow(screenRow) decorationClasses = @lineNumberDecorationClassesForRow(screenRow) - foldable = @model.isFoldableAtScreenRow(screenRow) blockDecorationsBeforeCurrentScreenRowHeight = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRow) - @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRow) blockDecorationsHeight = blockDecorationsBeforeCurrentScreenRowHeight if screenRow % @tileSize isnt 0 @@ -659,7 +663,7 @@ class TextEditorPresenter return unless @contentHeight? and @clientHeight? contentHeight = @contentHeight - if @scrollPastEnd + if @scrollPastEnd and @scrollPastEndOverride extraScrollHeight = @clientHeight - (@lineHeight * 3) contentHeight += extraScrollHeight if extraScrollHeight > 0 scrollHeight = Math.max(contentHeight, @height) diff --git a/src/text-editor-registry.coffee b/src/text-editor-registry.coffee new file mode 100644 index 000000000..8a17335d4 --- /dev/null +++ b/src/text-editor-registry.coffee @@ -0,0 +1,40 @@ +{Emitter, Disposable} = require 'event-kit' + +# Experimental: This global registry tracks registered `TextEditors`. +# +# If you want to add functionality to a wider set of text editors than just +# those appearing within workspace panes, use `atom.textEditors.observe` to +# invoke a callback for all current and future registered text editors. +# +# If you want packages to be able to add functionality to your non-pane text +# editors (such as a search field in a custom user interface element), register +# them for observation via `atom.textEditors.add`. **Important:** When you're +# done using your editor, be sure to call `dispose` on the returned disposable +# to avoid leaking editors. +module.exports = +class TextEditorRegistry + constructor: -> + @editors = new Set + @emitter = new Emitter + + # Register a `TextEditor`. + # + # * `editor` The editor to register. + # + # Returns a {Disposable} on which `.dispose()` can be called to remove the + # added editor. To avoid any memory leaks this should be called when the + # editor is destroyed. + add: (editor) -> + @editors.add(editor) + @emitter.emit 'did-add-editor', editor + new Disposable => @editors.delete(editor) + + # Invoke the given callback with all the current and future registered + # `TextEditors`. + # + # * `callback` {Function} to be called with current and future text editors. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observe: (callback) -> + @editors.forEach(callback) + @emitter.on 'did-add-editor', callback diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 44a7cc9f7..b2ea7985e 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -11,6 +11,7 @@ Selection = require './selection' TextMateScopeSelector = require('first-mate').ScopeSelector {Directory} = require "pathwatcher" GutterContainer = require './gutter-container' +TextEditorElement = require './text-editor-element' # Essential: This class represents all essential editing state for a single # {TextBuffer}, including cursor and selection positions, folds, and soft wraps. @@ -61,6 +62,10 @@ class TextEditor extends Model suppressSelectionMerging: false selectionFlashDuration: 500 gutterContainer: null + editorElement: null + + Object.defineProperty @prototype, "element", + get: -> @getElement() @deserialize: (state, atomEnvironment) -> try @@ -82,7 +87,10 @@ class TextEditor extends Model state.project = atomEnvironment.project state.assert = atomEnvironment.assert.bind(atomEnvironment) state.applicationDelegate = atomEnvironment.applicationDelegate - new this(state) + editor = new this(state) + disposable = atomEnvironment.textEditors.add(editor) + editor.onDidDestroy -> disposable.dispose() + editor constructor: (params={}) -> super @@ -92,7 +100,7 @@ class TextEditor extends Model softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation, @mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config, @notificationManager, @packageManager, @clipboard, @viewRegistry, @grammarRegistry, - @project, @assert, @applicationDelegate, @pending + @project, @assert, @applicationDelegate, grammar, showInvisibles, @autoHeight, @scrollPastEnd } = params throw new Error("Must pass a config parameter when constructing TextEditors") unless @config? @@ -109,11 +117,17 @@ class TextEditor extends Model @emitter = new Emitter @disposables = new CompositeDisposable @cursors = [] + @cursorsByMarkerId = new Map @selections = [] + @autoHeight ?= true + @scrollPastEnd ?= true + @hasTerminatedPendingState = false + + showInvisibles ?= true buffer ?= new TextBuffer @displayBuffer ?= new DisplayBuffer({ - buffer, tabLength, softWrapped, ignoreInvisibles: @mini, largeFileMode, + buffer, tabLength, softWrapped, ignoreInvisibles: @mini or not showInvisibles, largeFileMode, @config, @assert, @grammarRegistry, @packageManager }) {@buffer, @displayLayer} = @displayBuffer @@ -142,6 +156,9 @@ class TextEditor extends Model priority: 0 visible: lineNumberGutterVisible + if grammar? + @setGrammar(grammar) + serialize: -> deserializer: 'TextEditor' id: @id @@ -150,7 +167,6 @@ class TextEditor extends Model firstVisibleScreenColumn: @getFirstVisibleScreenColumn() displayBuffer: @displayBuffer.serialize() selectionsMarkerLayerId: @selectionsMarkerLayer.id - pending: @isPending() subscribeToBuffer: -> @buffer.retain() @@ -162,12 +178,18 @@ class TextEditor extends Model @disposables.add @buffer.onDidChangeEncoding => @emitter.emit 'did-change-encoding', @getEncoding() @disposables.add @buffer.onDidDestroy => @destroy() - if @pending - @disposables.add @buffer.onDidChangeModified => - @terminatePendingState() if @buffer.isModified() + @disposables.add @buffer.onDidChangeModified => + @terminatePendingState() if not @hasTerminatedPendingState and @buffer.isModified() @preserveCursorPositionOnBufferReload() + terminatePendingState: -> + @emitter.emit 'did-terminate-pending-state' if not @hasTerminatedPendingState + @hasTerminatedPendingState = true + + onDidTerminatePendingState: (callback) -> + @emitter.on 'did-terminate-pending-state', callback + subscribeToDisplayBuffer: -> @disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this) @disposables.add @displayBuffer.onDidChangeGrammar @handleGrammarChange.bind(this) @@ -574,13 +596,6 @@ class TextEditor extends Model getEditorWidthInChars: -> @displayBuffer.getEditorWidthInChars() - onDidTerminatePendingState: (callback) -> - @emitter.on 'did-terminate-pending-state', callback - - terminatePendingState: -> - return if not @pending - @pending = false - @emitter.emit 'did-terminate-pending-state' ### Section: File Details @@ -665,9 +680,6 @@ class TextEditor extends Model # Essential: Returns {Boolean} `true` if this editor has no content. isEmpty: -> @buffer.isEmpty() - # Returns {Boolean} `true` if this editor is pending and `false` if it is permanent. - isPending: -> Boolean(@pending) - # Copies the current file path to the native clipboard. copyPathToClipboard: (relative = false) -> if filePath = @getPath() @@ -1954,10 +1966,18 @@ class TextEditor extends Model getCursorsOrderedByBufferPosition: -> @getCursors().sort (a, b) -> a.compare(b) + cursorsForScreenRowRange: (startScreenRow, endScreenRow) -> + cursors = [] + for marker in @selectionsMarkerLayer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) + if cursor = @cursorsByMarkerId.get(marker.id) + cursors.push(cursor) + cursors + # Add a cursor based on the given {DisplayMarker}. addCursor: (marker) -> cursor = new Cursor(editor: this, marker: marker, config: @config) @cursors.push(cursor) + @cursorsByMarkerId.set(marker.id, cursor) @decorateMarker(marker, type: 'line-number', class: 'cursor-line') @decorateMarker(marker, type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true) @decorateMarker(marker, type: 'line', class: 'cursor-line', onlyEmpty: true) @@ -2456,6 +2476,7 @@ class TextEditor extends Model removeSelection: (selection) -> _.remove(@cursors, selection.cursor) _.remove(@selections, selection) + @cursorsByMarkerId.delete(selection.cursor.marker.id) @emitter.emit 'did-remove-cursor', selection.cursor @emitter.emit 'did-remove-selection', selection @@ -2470,6 +2491,7 @@ class TextEditor extends Model selections = @getSelections() if selections.length > 1 selection.destroy() for selection in selections[1...(selections.length)] + selections[0].autoscroll(center: true) true else false @@ -2924,6 +2946,7 @@ class TextEditor extends Model # Extended: Unfold all existing folds. unfoldAll: -> @languageMode.unfoldAll() + @scrollToCursorPosition() # Extended: Fold all foldable lines at the given indent level. # @@ -2939,8 +2962,7 @@ class TextEditor extends Model # # Returns a {Boolean}. isFoldableAtBufferRow: (bufferRow) -> - # @languageMode.isFoldableAtBufferRow(bufferRow) - @displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow)?.foldable ? false + @displayBuffer.isFoldableAtBufferRow(bufferRow) # Extended: Determine whether the given row in screen coordinates is foldable. # @@ -3119,6 +3141,10 @@ class TextEditor extends Model Section: TextEditor Rendering ### + # Get the Element for the editor. + getElement: -> + @editorElement ?= new TextEditorElement().initialize(this, atom, @autoHeight, @scrollPastEnd) + # Essential: Retrieves the greyed out placeholder of a mini editor. # # Returns a {String}. @@ -3192,8 +3218,8 @@ class TextEditor extends Model # top of the visible area. setFirstVisibleScreenRow: (screenRow, fromView) -> unless fromView - maxScreenRow = @getLineCount() - 1 - unless @config.get('editor.scrollPastEnd') + maxScreenRow = @getScreenLineCount() - 1 + unless @config.get('editor.scrollPastEnd') and @scrollPastEnd height = @displayBuffer.getHeight() lineHeightInPixels = @displayBuffer.getLineHeightInPixels() if height? and lineHeightInPixels? @@ -3210,7 +3236,7 @@ class TextEditor extends Model height = @displayBuffer.getHeight() lineHeightInPixels = @displayBuffer.getLineHeightInPixels() if height? and lineHeightInPixels? - Math.min(@firstVisibleScreenRow + Math.floor(height / lineHeightInPixels), @getLineCount() - 1) + Math.min(@firstVisibleScreenRow + Math.floor(height / lineHeightInPixels), @getScreenLineCount() - 1) else null diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 96d1f4649..eb2230e37 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -220,8 +220,6 @@ class TokenizedBuffer extends Model @validateRow(endRow) @invalidateRow(endRow + 1) unless filledRegion - [startRow, endRow] = @updateFoldableStatus(startRow, endRow) - event = {start: startRow, end: endRow, delta: 0} @emitter.emit 'did-change', event @emitter.emit 'did-invalidate-range', Range(Point(startRow, 0), Point(endRow + 1, 0)) @@ -282,10 +280,8 @@ class TokenizedBuffer extends Model if newEndStack and not _.isEqual(newEndStack, previousEndStack) @invalidateRow(end + delta + 1) - [start, end] = @updateFoldableStatus(start, end + delta) - end -= delta - @invalidatedRange = Range(start, end) + event = {start, end, delta, bufferChange: e} @emitter.emit 'did-change', event @@ -299,23 +295,6 @@ class TokenizedBuffer extends Model row - increment - updateFoldableStatus: (startRow, endRow) -> - return [startRow, endRow] if @largeFileMode - - scanStartRow = @buffer.previousNonBlankRow(startRow) ? startRow - scanStartRow-- while scanStartRow > 0 and @tokenizedLineForRow(scanStartRow).isComment() - scanEndRow = @buffer.nextNonBlankRow(endRow) ? endRow - - for row in [scanStartRow..scanEndRow] by 1 - foldable = @isFoldableAtRow(row) - line = @tokenizedLineForRow(row) - unless line.foldable is foldable - line.foldable = foldable - startRow = Math.min(startRow, row) - endRow = Math.max(endRow, row) - - [startRow, endRow] - isFoldableAtRow: (row) -> if @largeFileMode false diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee index e063387eb..5d092ab82 100644 --- a/src/tokenized-line.coffee +++ b/src/tokenized-line.coffee @@ -33,7 +33,6 @@ class TokenizedLine endOfLineInvisibles: null lineIsWhitespaceOnly: false firstNonWhitespaceIndex: 0 - foldable: false constructor: (properties) -> @id = idCounter++ @@ -355,15 +354,19 @@ class TokenizedLine @endOfLineInvisibles.push(eol) if eol isComment: -> + return @isCommentLine if @isCommentLine? + + @isCommentLine = false iterator = @getTokenIterator() while iterator.next() scopes = iterator.getScopes() continue if scopes.length is 1 - continue unless NonWhitespaceRegex.test(iterator.getText()) for scope in scopes - return true if CommentScopeRegex.test(scope) + if CommentScopeRegex.test(scope) + @isCommentLine = true + break break - false + @isCommentLine isOnlyWhitespace: -> @lineIsWhitespaceOnly diff --git a/src/tooltip-manager.coffee b/src/tooltip-manager.coffee index 247437535..90f0ab8e6 100644 --- a/src/tooltip-manager.coffee +++ b/src/tooltip-manager.coffee @@ -63,6 +63,8 @@ class TooltipManager # full list of options. You can also supply the following additional options: # * `title` A {String} or {Function} to use for the text in the tip. If # given a function, `this` will be set to the `target` element. + # * `trigger` A {String} that's the same as Bootstrap 'click | hover | focus + # | manual', except 'manual' will show the tooltip immediately. # * `keyBindingCommand` A {String} containing a command name. If you specify # this option and a key binding exists that matches the command, it will # be appended to the title or rendered alone if no title is specified. diff --git a/src/tooltip.js b/src/tooltip.js index 4ea952a64..ad5ce0cdd 100644 --- a/src/tooltip.js +++ b/src/tooltip.js @@ -64,7 +64,9 @@ Tooltip.prototype.init = function (element, options) { if (trigger === 'click') { this.disposables.add(listen(this.element, 'click', this.options.selector, this.toggle.bind(this))) - } else if (trigger !== 'manual') { + } else if (trigger === 'manual') { + this.show() + } else { var eventIn, eventOut if (trigger === 'hover') { diff --git a/src/view-registry.coffee b/src/view-registry.coffee index ef7151353..5fbfba729 100644 --- a/src/view-registry.coffee +++ b/src/view-registry.coffee @@ -171,6 +171,11 @@ class ViewRegistry if object instanceof HTMLElement return object + if typeof object?.getElement is 'function' + element = object.getElement() + if element instanceof HTMLElement + return element + if object?.element instanceof HTMLElement return object.element diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index d3a231f77..6c338320d 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -15,7 +15,8 @@ class WindowEventHandler @addEventListener(@window, 'focus', @handleWindowFocus) @addEventListener(@window, 'blur', @handleWindowBlur) - @addEventListener(@document, 'keydown', @handleDocumentKeydown) + @addEventListener(@document, 'keyup', @handleDocumentKeyEvent) + @addEventListener(@document, 'keydown', @handleDocumentKeyEvent) @addEventListener(@document, 'drop', @handleDocumentDrop) @addEventListener(@document, 'dragover', @handleDocumentDragover) @addEventListener(@document, 'contextmenu', @handleDocumentContextmenu) @@ -66,7 +67,7 @@ class WindowEventHandler target.addEventListener(eventName, handler) @subscriptions.add(new Disposable(-> target.removeEventListener(eventName, handler))) - handleDocumentKeydown: (event) => + handleDocumentKeyEvent: (event) => @atomEnvironment.keymaps.handleKeyboardEvent(event) event.stopImmediatePropagation() @@ -133,7 +134,7 @@ class WindowEventHandler handleWindowBlur: => @document.body.classList.add('is-blurred') - @atomEnvironment.storeDefaultWindowDimensions() + @atomEnvironment.storeWindowDimensions() handleWindowBeforeunload: => confirmed = @atomEnvironment.workspace?.confirmClose(windowCloseRequested: true) @@ -141,7 +142,6 @@ class WindowEventHandler @atomEnvironment.hide() @reloadRequested = false - @atomEnvironment.storeDefaultWindowDimensions() @atomEnvironment.storeWindowDimensions() if confirmed @atomEnvironment.unloadEditorWindow() diff --git a/src/window-load-settings-helpers.coffee b/src/window-load-settings-helpers.coffee index 59ee2f382..73fd31a3d 100644 --- a/src/window-load-settings-helpers.coffee +++ b/src/window-load-settings-helpers.coffee @@ -1,19 +1,10 @@ -remote = require 'remote' +{remote} = require 'electron' _ = require 'underscore-plus' windowLoadSettings = null exports.getWindowLoadSettings = -> windowLoadSettings ?= JSON.parse(window.decodeURIComponent(window.location.hash.substr(1))) - clone = _.deepClone(windowLoadSettings) - - # The windowLoadSettings.windowState could be large, request it only when needed. - clone.__defineGetter__ 'windowState', -> - remote.getCurrentWindow().loadSettings.windowState - clone.__defineSetter__ 'windowState', (value) -> - remote.getCurrentWindow().loadSettings.windowState = value - - clone exports.setWindowLoadSettings = (settings) -> windowLoadSettings = settings diff --git a/src/workspace-element.coffee b/src/workspace-element.coffee index f7805ed57..8f6dca48a 100644 --- a/src/workspace-element.coffee +++ b/src/workspace-element.coffee @@ -1,4 +1,4 @@ -ipc = require 'ipc' +{ipcRenderer} = require 'electron' path = require 'path' {Disposable, CompositeDisposable} = require 'event-kit' Grim = require 'grim' @@ -117,6 +117,6 @@ class WorkspaceElement extends HTMLElement [projectPath] = @project.relativizePath(activePath) else [projectPath] = @project.getPaths() - ipc.send('run-package-specs', path.join(projectPath, 'spec')) if projectPath + ipcRenderer.send('run-package-specs', path.join(projectPath, 'spec')) if projectPath module.exports = WorkspaceElement = document.registerElement 'atom-workspace', prototype: WorkspaceElement.prototype diff --git a/src/workspace.coffee b/src/workspace.coffee index ebaf1b337..9b1bf14fc 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -43,6 +43,12 @@ class Workspace extends Model @defaultDirectorySearcher = new DefaultDirectorySearcher() @consumeServices(@packageManager) + # One cannot simply .bind here since it could be used as a component with + # Etch, in which case it'd be `new`d. And when it's `new`d, `this` is always + # the newly created object. + realThis = this + @buildTextEditor = -> Workspace.prototype.buildTextEditor.apply(realThis, arguments) + @panelContainers = top: new PanelContainer({location: 'top'}) left: new PanelContainer({location: 'left'}) @@ -394,15 +400,18 @@ class Workspace extends Model # initially. Defaults to `0`. # * `initialColumn` A {Number} indicating which column to move the cursor to # initially. Defaults to `0`. - # * `split` Either 'left', 'right', 'top' or 'bottom'. + # * `split` Either 'left', 'right', 'up' or 'down'. # If 'left', the item will be opened in leftmost pane of the current active pane's row. - # If 'right', the item will be opened in the rightmost pane of the current active pane's row. - # If 'up', the item will be opened in topmost pane of the current active pane's row. - # If 'down', the item will be opened in the bottommost pane of the current active pane's row. + # If 'right', the item will be opened in the rightmost pane of the current active pane's row. If only one pane exists in the row, a new pane will be created. + # If 'up', the item will be opened in topmost pane of the current active pane's column. + # If 'down', the item will be opened in the bottommost pane of the current active pane's column. If only one pane exists in the column, a new pane will be created. # * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on # containing pane. Defaults to `true`. # * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem} # on containing pane. Defaults to `true`. + # * `pending` A {Boolean} indicating whether or not the item should be opened + # in a pending state. Existing pending items in a pane are replaced with + # new pending items when they are opened. # * `searchAllPanes` A {Boolean}. If `true`, the workspace will attempt to # activate an existing item for the given URI on any pane. # If `false`, only the active pane will be searched for @@ -414,6 +423,9 @@ class Workspace extends Model split = options.split uri = @project.resolvePath(uri) + if not atom.config.get('core.allowPendingPaneItems') + options.pending = false + # Avoid adding URLs as recent documents to work-around this Spotlight crash: # https://github.com/atom/atom/issues/10071 if uri? and not url.parse(uri).protocol? @@ -473,7 +485,8 @@ class Workspace extends Model activateItem = options.activateItem ? true if uri? - item = pane.itemForURI(uri) + if item = pane.itemForURI(uri) + pane.clearPendingItem() if not options.pending and pane.getPendingItem() is item item ?= opener(uri, options) for opener in @getOpeners() when not item try @@ -496,7 +509,7 @@ class Workspace extends Model return item if pane.isDestroyed() @itemOpened(item) - pane.activateItem(item) if activateItem + pane.activateItem(item, {pending: options.pending}) if activateItem pane.activate() if activatePane initialLine = initialColumn = 0 @@ -551,7 +564,10 @@ class Workspace extends Model @config, @notificationManager, @packageManager, @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate }, params) - new TextEditor(params) + editor = new TextEditor(params) + disposable = atom.textEditors.add(editor) + editor.onDidDestroy -> disposable.dispose() + editor # Public: Asynchronously reopens the last-closed item's URI if it hasn't already been # reopened. diff --git a/static/index.html b/static/index.html index 5fcb30ad2..0bd4a7954 100644 --- a/static/index.html +++ b/static/index.html @@ -1,7 +1,7 @@ - + diff --git a/static/index.js b/static/index.js index 6d65d3c52..c33eda67a 100644 --- a/static/index.js +++ b/static/index.js @@ -54,7 +54,7 @@ } function handleSetupError (error) { - var currentWindow = require('remote').getCurrentWindow() + var currentWindow = require('electron').remote.getCurrentWindow() currentWindow.setSize(800, 600) currentWindow.center() currentWindow.show() @@ -71,9 +71,10 @@ ModuleCache.add(loadSettings.resourcePath) // Start the crash reporter before anything else. - require('crash-reporter').start({ + require('electron').crashReporter.start({ productName: 'Atom', companyName: 'GitHub', + submitURL: 'http://54.249.141.255:1127/post', // By explicitly passing the app version here, we could save the call // of "require('remote').require('app').getVersion()". extra: {_version: loadSettings.appVersion} @@ -83,8 +84,9 @@ setupCsonCache(CompileCache.getCacheDirectory()) var initialize = require(loadSettings.windowInitializationScript) - initialize({blobStore: blobStore}) - require('ipc').sendChannel('window-command', 'window:loaded') + return initialize({blobStore: blobStore}).then(function () { + require('electron').ipcRenderer.send('window-command', 'window:loaded') + }) } function setupCsonCache (cacheDir) { @@ -112,19 +114,15 @@ function profileStartup (loadSettings, initialTime) { function profile () { console.profile('startup') - try { - var startTime = Date.now() - setupWindow(loadSettings) + var startTime = Date.now() + setupWindow(loadSettings).then(function () { setLoadTime(Date.now() - startTime + initialTime) - } catch (error) { - handleSetupError(error) - } finally { console.profileEnd('startup') console.log('Switch to the Profiles tab to view the created startup profile') - } + }) } - var currentWindow = require('remote').getCurrentWindow() + var currentWindow = require('electron').remote.getCurrentWindow() if (currentWindow.devToolsWebContents) { profile() } else { @@ -145,31 +143,6 @@ } } - function setupWindowBackground () { - if (loadSettings && loadSettings.isSpec) { - return - } - - var backgroundColor = window.localStorage.getItem('atom:window-background-color') - if (!backgroundColor) { - return - } - - var backgroundStylesheet = document.createElement('style') - backgroundStylesheet.type = 'text/css' - backgroundStylesheet.innerText = 'html, body { background: ' + backgroundColor + ' !important; }' - document.head.appendChild(backgroundStylesheet) - - // Remove once the page loads - window.addEventListener('load', function loadWindow () { - window.removeEventListener('load', loadWindow, false) - setTimeout(function () { - backgroundStylesheet.remove() - backgroundStylesheet = null - }, 1000) - }, false) - } - var setupAtomHome = function () { if (process.env.ATOM_HOME) { return @@ -185,5 +158,4 @@ parseLoadSettings() setupAtomHome() - setupWindowBackground() })()