diff --git a/.travis.yml b/.travis.yml index 505d56b40..d5918dc8d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,3 +49,5 @@ addons: - git - libgnome-keyring-dev - rpm + - libx11-dev + - libxkbfile-dev diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ceb4186d9..49606b50c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -84,7 +84,7 @@ When we make a significant decision in how we maintain the project and what we c This section guides you through submitting a bug report for Atom. Following these guidelines helps maintainers and the community understand your report :pencil:, reproduce the behavior :computer: :computer:, and find related reports :mag_right:. -Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). If you'd like, you can use [this template](#template-for-submitting-bug-reports) to structure the information. +Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). Fill out [the required template](ISSUE_TEMPLATE.md), the information it asks for helps us resolve issues faster. #### Before Submitting A Bug Report @@ -95,7 +95,7 @@ Before creating bug reports, please check [this list](#before-submitting-a-bug-r #### How Do I Submit A (Good) Bug Report? -Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined [which repository](#atom-and-packages) your bug is related to, create an issue on that repository and provide the following information. +Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined [which repository](#atom-and-packages) your bug is related to, create an issue on that repository and provide the following information by filling in [the template](ISSUE_TEMPLATE.md). Explain the problem and include additional details to help maintainers reproduce the problem: @@ -128,47 +128,11 @@ Include details about your configuration and environment: * **Are you using Atom with multiple monitors?** If so, can you reproduce the problem when you use a single monitor? * **Which keyboard layout are you using?** Are you using a US layout or some other layout? -#### Template For Submitting Bug Reports - - [Short description of problem here] - - **Reproduction Steps:** - - 1. [First Step] - 2. [Second Step] - 3. [Other Steps...] - - **Expected behavior:** - - [Describe expected behavior here] - - **Observed behavior:** - - [Describe observed behavior here] - - **Screenshots and GIFs** - - ![Screenshots and GIFs which follow reproduction steps to demonstrate the problem](url) - - **Atom version:** [Enter Atom version here] - **OS and version:** [Enter OS name and version here] - - **Installed packages:** - - [List of installed packages here] - - **Additional information:** - - * Problem can be reproduced in safe mode: [Yes/No] - * Problem started happening recently, didn't happen in an older version of Atom: [Yes/No] - * Problem can be reliably reproduced, doesn't happen randomly: [Yes/No] - * Problem happens with all files and projects, not only some files or projects: [Yes/No] - ### Suggesting Enhancements This section guides you through submitting an enhancement suggestion for Atom, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion :pencil: and find related suggestions :mag_right:. -Before creating enhancement suggestions, please check [this list](#before-submitting-an-enhancement-suggestion) as you might find out that you don't need to create one. When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). If you'd like, you can use [this template](#template-for-submitting-enhancement-suggestions) to structure the information. +Before creating enhancement suggestions, please check [this list](#before-submitting-an-enhancement-suggestion) as you might find out that you don't need to create one. When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). Fill in [the template](ISSUE_TEMPLATE.md), including the steps that you imagine you would take if the feature you're requesting existed. #### Before Submitting An Enhancement Suggestion @@ -191,33 +155,6 @@ Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com * **Specify which version of Atom you're using.** You can get the exact version by running `atom -v` in your terminal, or by starting Atom and running the `Application: About` command from the [Command Palette](https://github.com/atom/command-palette). * **Specify the name and version of the OS you're using.** -#### Template For Submitting Enhancement Suggestions - - [Short description of suggestion] - - **Steps which explain the enhancement** - - 1. [First Step] - 2. [Second Step] - 3. [Other Steps...] - - **Current and suggested behavior** - - [Describe current and suggested behavior here] - - **Why would the enhancement be useful to most users** - - [Explain why the enhancement would be useful to most users] - - [List some other text editors or applications where this enhancement exists] - - **Screenshots and GIFs** - - ![Screenshots and GIFs which demonstrate the steps or part of Atom the enhancement suggestion is related to](url) - - **Atom Version:** [Enter Atom version here] - **OS and Version:** [Enter OS name and version here] - ### Your First Code Contribution Unsure where to begin contributing to Atom? You can start by looking through these `beginner` and `help-wanted` issues: @@ -231,6 +168,7 @@ If you want to read about using Atom or developing packages in Atom, the [Atom F ### Pull Requests +* Fill in [the required template](PULL_REQUEST_TEMPLATE.md) * Include screenshots and animated GIFs in your pull request whenever possible. * Follow the [JavaScript](#javascript-styleguide) and [CoffeeScript](#coffeescript-styleguide) styleguides. * Include thoughtfully-worded, well-structured @@ -240,16 +178,12 @@ If you want to read about using Atom or developing packages in Atom, the [Atom F * End files with a newline. * Place requires in the following order: * Built in Node Modules (such as `path`) - * Built in Atom and Atom Shell Modules (such as `atom`, `shell`) + * Built in Atom and Electron Modules (such as `atom`, `remote`) * Local Modules (using relative paths) * Place class properties in the following order: * Class methods and properties (methods starting with a `@`) * Instance methods and properties -* Avoid platform-dependent code: - * Use `require('fs-plus').getHomeDirectory()` to get the home directory. - * Use `path.join()` to concatenate filenames. - * Use `os.tmpdir()` rather than `/tmp` when you need to reference the - temporary directory. +* [Avoid platform-dependent code](http://flight-manual.atom.io/hacking-atom/sections/cross-platform-compatibility/) * Using a plain `return` when returning explicitly at the end of a function. * Not `return null`, `return undefined`, `null`, or `undefined` @@ -427,12 +361,6 @@ Please open an issue on `atom/atom` if you have suggestions for new labels, and | `deprecation-help` | [search][search-atom-repo-label-deprecation-help] | [search][search-atom-org-label-deprecation-help] | Issues for helping package authors remove usage of deprecated APIs in packages. | | `electron` | [search][search-atom-repo-label-electron] | [search][search-atom-org-label-electron] | Issues that require changes to [Electron](https://electron.atom.io) to fix or implement. | -#### Core Team Project Management - -| Label name | `atom/atom` :mag_right: | `atom`‑org :mag_right: | Description | -| --- | --- | --- | --- | -| `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 | Label name | `atom/atom` :mag_right: | `atom`‑org :mag_right: | Description @@ -519,8 +447,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-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 [search-atom-org-label-work-in-progress]: https://github.com/pulls?q=is%3Aopen+is%3Apr+user%3Aatom+label%3Awork-in-progress [search-atom-repo-label-needs-review]: https://github.com/pulls?q=is%3Aopen+is%3Apr+repo%3Aatom%2Fatom+label%3Aneeds-review @@ -533,4 +459,4 @@ Please open an issue on `atom/atom` if you have suggestions for new labels, and [search-atom-org-label-needs-testing]: https://github.com/pulls?q=is%3Aopen+is%3Apr+user%3Aatom+label%3Aneeds-testing [beginner]:https://github.com/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3Abeginner+label%3Ahelp-wanted+user%3Aatom+sort%3Acomments-desc -[help-wanted]:https://github.com/issues?q=is%3Aopen+is%3Aissue+label%3Ahelp-wanted+user%3Aatom+sort%3Acomments-desc +[help-wanted]:https://github.com/issues?q=is%3Aopen+is%3Aissue+label%3Ahelp-wanted+user%3Aatom+sort%3Acomments-desc+-label%3Abeginner diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index d2ac45f05..b60bb86c9 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,17 +1,23 @@ + + ### Prerequisites -* [ ] Can you reproduce the problem in [safe mode](http://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode)? -* [ ] Are you running the [latest version of Atom](http://flight-manual.atom.io/hacking-atom/sections/debugging/#update-to-the-latest-version)? -* [ ] Did you check the [debugging guide](http://flight-manual.atom.io/hacking-atom/sections/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. +* [ ] Put an X between the brackets on this line if you have done all of the following: + * Reproduced the problem in Safe Mode: http://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode + * Followed all applicable steps in the debugging guide: http://flight-manual.atom.io/hacking-atom/sections/debugging/ + * Checked the FAQs on the message board for common solutions: https://discuss.atom.io/c/faq + * Checked that your issue isn't already filed: https://github.com/issues?utf8=✓&q=is%3Aissue+user%3Aatom + * Checked that there is not already an Atom package that provides the described functionality: https://atom.io/packages ### Description -[Description of the bug or feature] +[Description of the issue] ### Steps to Reproduce @@ -19,10 +25,16 @@ For more information on how to write a good [bug report](https://github.com/atom 2. [Second Step] 3. [and so on...] -**Expected behavior:** [What you expected to happen] +**Expected behavior:** [What you expect to happen] -**Actual behavior:** [What actually happened] +**Actual behavior:** [What actually happens] + +**Reproduces how often:** [What percentage of the time does it reproduce?] ### Versions -You can get this information from executing `atom --version` and `apm --version` at the command line. Also, please include the OS and what version of the OS you're running. +You can get this information from copy and pasting the output of `atom --version` and `apm --version` from the command line. Also, please include the OS and what version of the OS you're running. + +### Additional Information + +Any additional information, configuration or data that might be necessary to reproduce the issue. diff --git a/LICENSE.md b/LICENSE.md index 4d231b456..5bdf03cde 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2014 GitHub Inc. +Copyright (c) 2011-2017 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..a578c38ce --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,32 @@ +### Requirements + +* Filling out the template is required. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion. +* All new code requires tests to ensure against regressions + +### Description of the Change + + + +### Alternate Designs + + + +### Why Should This Be In Core? + + + +### Benefits + + + +### Possible Drawbacks + + + +### Applicable Issues + + diff --git a/README.md b/README.md index bb6f6998e..dc22ae866 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![Atom](https://cloud.githubusercontent.com/assets/72919/2874231/3af1db48-d3dd-11e3-98dc-6066f8bc766f.png) -[![macOS Build Status](https://circleci.com/gh/atom/atom.svg?style=svg)](https://circleci.com/gh/atom/atom) [![Linux Build Status](https://travis-ci.org/atom/atom.svg?branch=master)](https://travis-ci.org/atom/atom) [![Windows Build Status](https://ci.appveyor.com/api/projects/status/1tkktwh654w07eim?svg=true)](https://ci.appveyor.com/project/Atom/atom) +[![macOS Build Status](https://circleci.com/gh/atom/atom/tree/master.svg?style=shield)](https://circleci.com/gh/atom/atom) [![Linux Build Status](https://travis-ci.org/atom/atom.svg?branch=master)](https://travis-ci.org/atom/atom) [![Windows Build Status](https://ci.appveyor.com/api/projects/status/1tkktwh654w07eim?svg=true)](https://ci.appveyor.com/project/Atom/atom) [![Dependency Status](https://david-dm.org/atom/atom.svg)](https://david-dm.org/atom/atom) [![Join the Atom Community on Slack](http://atom-slack.herokuapp.com/badge.svg)](http://atom-slack.herokuapp.com/) @@ -33,15 +33,14 @@ Atom will automatically update when a new release is available. ### Windows -Download the latest [AtomSetup.exe installer](https://github.com/atom/atom/releases/latest). +Download the latest [Atom installer](https://github.com/atom/atom/releases/latest). AtomSetup.exe is 32-bit, AtomSetup-x64.exe for 64-bit systems. Atom will automatically update when a new release is available. -You can also download an `atom-windows.zip` file from the [releases page](https://github.com/atom/atom/releases/latest). +You can also download `atom-windows.zip` (32-bit) or `atom-x64-windows.zip` (64-bit) from the [releases page](https://github.com/atom/atom/releases/latest). The `.zip` version will not automatically update. -Using [chocolatey](https://chocolatey.org/)? Run `cinst Atom` to install -the latest version of Atom. +Using [chocolatey](https://chocolatey.org/)? Run `cinst Atom` to install the latest version of Atom. ### Debian Linux (Ubuntu) @@ -97,3 +96,9 @@ repeat these steps to upgrade to future releases. * [macOS](./docs/build-instructions/macos.md) * [FreeBSD](./docs/build-instructions/freebsd.md) * [Windows](./docs/build-instructions/windows.md) + +## License + +[MIT](https://github.com/atom/atom/blob/master/LICENSE.md) + +When using the Atom or other GitHub logos, be sure to follow the [GitHub logo guidelines](https://github.com/logos). diff --git a/apm/package.json b/apm/package.json index 73f860586..732ab208a 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.13.0" + "atom-package-manager": "1.15.1" } } diff --git a/appveyor.yml b/appveyor.yml index 82b02123e..a9a0d7920 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,3 +1,5 @@ +image: Visual Studio 2015 + version: "{build}" skip_tags: true @@ -6,13 +8,14 @@ clone_depth: 10 platform: - x86 + - x64 environment: global: ATOM_DEV_RESOURCE_PATH: c:\projects\atom matrix: - - NODE_VERSION: 4.4.5 + - NODE_VERSION: 6.8.0 install: - SET PATH=C:\Program Files\Atom\resources\cli;%PATH% @@ -47,6 +50,4 @@ cache: - '%APPVEYOR_BUILD_FOLDER%\node_modules' - '%APPVEYOR_BUILD_FOLDER%\electron' - '%USERPROFILE%\.atom\.apm' - - '%USERPROFILE%\.atom\.node-gyp\.atom' - - '%USERPROFILE%\.atom\.npm' - '%USERPROFILE%\.atom\compile-cache' diff --git a/atom.sh b/atom.sh index a8c30fa19..6b0e94430 100755 --- a/atom.sh +++ b/atom.sh @@ -55,27 +55,38 @@ if [ $EXPECT_OUTPUT ]; then fi if [ $OS == 'Mac' ]; then + if [ -L "$0" ]; then + SCRIPT="$(readlink "$0")" + else + SCRIPT="$0" + fi + ATOM_APP="$(dirname "$(dirname "$(dirname "$(dirname "$SCRIPT")")")")" + if [ "$ATOM_APP" == . ]; then + unset ATOM_APP + else + ATOM_PATH="$(dirname "$ATOM_APP")" + ATOM_APP_NAME="$(basename "$ATOM_APP")" + fi + if [ -n "$BETA_VERSION" ]; then - ATOM_APP_NAME="Atom Beta.app" ATOM_EXECUTABLE_NAME="Atom Beta" else - ATOM_APP_NAME="Atom.app" ATOM_EXECUTABLE_NAME="Atom" fi if [ -z "${ATOM_PATH}" ]; then - # If ATOM_PATH isnt set, check /Applications and then ~/Applications for Atom.app + # If ATOM_PATH isn't set, check /Applications and then ~/Applications for Atom.app if [ -x "/Applications/$ATOM_APP_NAME" ]; then ATOM_PATH="/Applications" elif [ -x "$HOME/Applications/$ATOM_APP_NAME" ]; then ATOM_PATH="$HOME/Applications" else - # We havent found an Atom.app, use spotlight to search for Atom + # We haven't found an Atom.app, use spotlight to search for Atom ATOM_PATH="$(mdfind "kMDItemCFBundleIdentifier == 'com.github.atom'" | grep -v ShipIt | head -1 | xargs -0 dirname)" # Exit if Atom can't be found if [ ! -x "$ATOM_PATH/$ATOM_APP_NAME" ]; then - echo "Cannot locate Atom.app, it is usually located in /Applications. Set the ATOM_PATH environment variable to the directory containing Atom.app." + echo "Cannot locate ${ATOM_APP_NAME}, it is usually located in /Applications. Set the ATOM_PATH environment variable to the directory containing ${ATOM_APP_NAME}." exit 1 fi fi diff --git a/docs/build-instructions/build-status.md b/docs/build-instructions/build-status.md index a17923224..9b381527f 100644 --- a/docs/build-instructions/build-status.md +++ b/docs/build-instructions/build-status.md @@ -1,112 +1,114 @@ # Atom build status -| System | macOS | Windows | Dependencies | -|--------|------|---------|--------------| -| Atom | [![macOS Build Status](https://travis-ci.org/atom/atom.svg?branch=master)](https://travis-ci.org/atom/atom) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/1tkktwh654w07eim?svg=true)](https://ci.appveyor.com/project/Atom/atom) | [![Dependency Status](https://david-dm.org/atom/atom.svg)](https://david-dm.org/atom/atom) | -| APM | [![macOS Build Status](https://travis-ci.org/atom/apm.svg?branch=master)](https://travis-ci.org/atom/apm) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/j6ixw374a397ugkb/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/apm/branch/master) | [![Dependency Status](https://david-dm.org/atom/apm.svg)](https://david-dm.org/atom/apm) | -| Electron | [![macOS Build Status](https://travis-ci.org/electron/electron.svg?branch=master)](https://travis-ci.org/electron/electron) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/kvxe4byi7jcxbe26/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/electron) | [![Dependency Status](https://david-dm.org/electron/electron/dev-status.svg)](https://david-dm.org/electron/electron) +| System | Travis | AppVeyor/Win | Circle/Mac | Dependencies | +|--------|--------|--------------|------------|--------------| +| [Atom](https://github.com/atom/atom) | [![Travis Build Status](https://travis-ci.org/atom/atom.svg?branch=master)](https://travis-ci.org/atom/atom) | [![AppVeyor/Wi Build Status](https://ci.appveyor.com/api/projects/status/1tkktwh654w07eim?svg=true)](https://ci.appveyor.com/project/Atom/atom) | [![Circle/Mac Build Status](https://circleci.com/gh/atom/atom.svg?style=shield)](https://circleci.com/gh/atom/atom) | [![Dependency Status](https://david-dm.org/atom/atom.svg)](https://david-dm.org/atom/atom) | +| [APM](https://github.com/atom/apm) | [![Travis Build Status](https://travis-ci.org/atom/apm.svg?branch=master)](https://travis-ci.org/atom/apm) | [![AppVeyor/Wi Build Status](https://ci.appveyor.com/api/projects/status/j6ixw374a397ugkb/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/apm/branch/master) | | [![Dependency Status](https://david-dm.org/atom/apm.svg)](https://david-dm.org/atom/apm) | +| [Electron](https://github.com/electron/electron) | [![Travis Build Status](https://travis-ci.org/electron/electron.svg?branch=master)](https://travis-ci.org/electron/electron) | [![AppVeyor/Wi Build Status](https://ci.appveyor.com/api/projects/status/kvxe4byi7jcxbe26/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/electron) | | [![Dependency Status](https://david-dm.org/electron/electron/dev-status.svg)](https://david-dm.org/electron/electron) ## Packages -| Package | macOS | Windows | Dependencies | -|---------|------|---------|--------------| -| About | [![macOS Build Status](https://travis-ci.org/atom/about.svg?branch=master)](https://travis-ci.org/atom/about) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/msprea3vq47l8oce/branch/master?svg=true)](https://ci.appveyor.com/project/atom/about/branch/master) | [![Dependency Status](https://david-dm.org/atom/about.svg)](https://david-dm.org/atom/about) | -| Archive View | [![macOS Build Status](https://travis-ci.org/atom/archive-view.svg?branch=master)](https://travis-ci.org/atom/archive-view) | [![Windows Build status](https://ci.appveyor.com/api/projects/status/u3qfgaod4lhriqlj/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/archive-view/branch/master) | [![Dependency Status](https://david-dm.org/atom/archive-view.svg)](https://david-dm.org/atom/archive-view) | -| AutoComplete Atom API | [![macOS Build Status](https://travis-ci.org/atom/autocomplete-atom-api.svg?branch=master)](https://travis-ci.org/atom/autocomplete-atom-api) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/1x3uqd9ddchpe555/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/autocomplete-atom-api/branch/master) | [![Dependency Status](https://david-dm.org/atom/autocomplete-atom-api.svg)](https://david-dm.org/atom/autocomplete-atom-api) | -| Atom Space Pen Views | [![macOS Build Status](https://travis-ci.org/atom/atom-space-pen-views.svg?branch=master)](https://travis-ci.org/atom/atom-space-pen-views) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/5lgv47has6n8uhuv/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/atom-space-pen-views/branch/master) | [![Dependency Status](https://david-dm.org/atom/atom-space-pen-views.svg)](https://david-dm.org/atom/atom-space-pen-views) | -| AutoComplete CSS | [![macOS Build Status](https://travis-ci.org/atom/autocomplete-css.svg?branch=master)](https://travis-ci.org/atom/autocomplete-css) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/k3e5uvpmpc5bkja9/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/autocomplete-css/branch/master) | [![Dependency Status](https://david-dm.org/atom/autocomplete-css.svg)](https://david-dm.org/atom/autocomplete-css) | -| AutoComplete HTML | [![macOS Build Status](https://travis-ci.org/atom/autocomplete-html.svg?branch=master)](https://travis-ci.org/atom/autocomplete-html) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/bsaqbg1fljpd9q1b/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/autocomplete-html/branch/master) | [![Dependency Status](https://david-dm.org/atom/autocomplete-html.svg)](https://david-dm.org/atom/autocomplete-html) | -| AutoComplete+ | [![macOS Build Status](https://travis-ci.org/atom/autocomplete-plus.svg?branch=master)](https://travis-ci.org/atom/autocomplete-plus) | [![Windows Build status](https://ci.appveyor.com/api/projects/status/9bpokrud2apgqsq0/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/autocomplete-plus/branch/master) | [![Dependency Status](https://david-dm.org/atom/autocomplete-plus.svg)](https://david-dm.org/atom/autocomplete-plus) | -| AutoComplete Snippets | [![macOS Build Status](https://travis-ci.org/atom/autocomplete-snippets.svg)](https://travis-ci.org/atom/autocomplete-snippets) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/72kfi83l6cw90joy/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/autocomplete-snippets/branch/master) | [![Dependency Status](https://david-dm.org/atom/autocomplete-snippets.svg)](https://david-dm.org/atom/autocomplete-snippets) | -| AutoFlow | [![macOS Build Status](https://travis-ci.org/atom/autoflow.svg?branch=master)](https://travis-ci.org/atom/autoflow) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/kpmsnkbooa29x907/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/autoflow/branch/master) | [![Dependency Status](https://david-dm.org/atom/autoflow.svg)](https://david-dm.org/atom/autoflow) | -| AutoSave | [![macOS Build Status](https://travis-ci.org/atom/autosave.svg?branch=master)](https://travis-ci.org/atom/autosave) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/3aktr9updp722fqx/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/autosave/branch/master) | [![Dependency Status](https://david-dm.org/atom/autosave.svg)](https://david-dm.org/atom/autosave) | -| Background Tips | [![macOS Build Status](https://travis-ci.org/atom/background-tips.svg?branch=master)](https://travis-ci.org/atom/background-tips) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/2utcugietl5vjc7w/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/background-tips/branch/master) | [![Dependency Status](https://david-dm.org/atom/background-tips.svg)](https://david-dm.org/atom/background-tips) | -| Bookmarks | [![macOS Build Status](https://travis-ci.org/atom/bookmarks.svg?branch=master)](https://travis-ci.org/atom/bookmarks) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/vjsf78pj4rw6ibcw/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/bookmarks/branch/master) | [![Dependency Status](https://david-dm.org/atom/bookmarks.svg)](https://david-dm.org/atom/bookmarks) | -| Bracket Matcher | [![macOS Build Status](https://travis-ci.org/atom/bracket-matcher.svg?branch=master)](https://travis-ci.org/atom/bracket-matcher) | [![Windows Build status](https://ci.appveyor.com/api/projects/status/rrsl2h7e0od26k54/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/bracket-matcher/branch/master) | [![Dependency Status](https://david-dm.org/atom/bracket-matcher.svg)](https://david-dm.org/atom/bracket-matcher) | -| Command Palette | [![macOS Build Status](https://travis-ci.org/atom/command-palette.svg?branch=master)](https://travis-ci.org/atom/command-palette) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/jqgwetayr0enorun/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/command-palette/branch/master) | [![Dependency Status](https://david-dm.org/atom/command-palette.svg)](https://david-dm.org/atom/command-palette) | -| Deprecation Cop | [![macOS Build Status](https://travis-ci.org/atom/deprecation-cop.svg?branch=master)](https://travis-ci.org/atom/deprecation-cop) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/0s870q5fj3vwihjx/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/deprecation-cop/branch/master) | [![Dependency Status](https://david-dm.org/atom/deprecation-cop.svg)](https://david-dm.org/atom/deprecation-cop) | -| Dev Live Reload | [![macOS Build Status](https://travis-ci.org/atom/dev-live-reload.svg?branch=master)](https://travis-ci.org/atom/dev-live-reload) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/g3sd27ylba1fun1v/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/dev-live-reload/branch/master) | [![Dependency Status](https://david-dm.org/atom/dev-live-reload.svg)](https://david-dm.org/atom/dev-live-reload) | -| Encoding Selector | [![macOS Build Status](https://travis-ci.org/atom/encoding-selector.svg?branch=master)](https://travis-ci.org/atom/encoding-selector) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/e08x6k2b68wpwxxc/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/encoding-selector/branch/master) | [![Dependency Status](https://david-dm.org/atom/encoding-selector.svg)](https://david-dm.org/atom/encoding-selector) | -| Exception Reporting | [![macOS Build Status](https://travis-ci.org/atom/exception-reporting.svg?branch=master)](https://travis-ci.org/atom/exception-reporting) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/i0pla7qbpv7celg2/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/exception-reporting/branch/master) | [![Dependency Status](https://david-dm.org/atom/exception-reporting.svg)](https://david-dm.org/atom/exception-reporting) | -| Find and Replace | [![macOS Build Status](https://travis-ci.org/atom/find-and-replace.svg?branch=master)](https://travis-ci.org/atom/find-and-replace) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/6w4baiiq5mw4nxky/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/find-and-replace/branch/master) | [![Dependency Status](https://david-dm.org/atom/find-and-replace.svg)](https://david-dm.org/atom/find-and-replace) | -| Fuzzy Finder | [![macOS Build Status](https://travis-ci.org/atom/fuzzy-finder.svg?branch=master)](https://travis-ci.org/atom/fuzzy-finder) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/b4b2dg5n9r1wdqad/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/fuzzy-finder/branch/master) | [![Dependency Status](https://david-dm.org/atom/fuzzy-finder.svg)](https://david-dm.org/atom/fuzzy-finder) | -| Git Diff | [![macOS Build Status](https://travis-ci.org/atom/git-diff.svg?branch=master)](https://travis-ci.org/atom/git-diff) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/9auj52cs0vso66nv/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/git-diff/branch/master) | [![Dependency Status](https://david-dm.org/atom/git-diff.svg)](https://david-dm.org/atom/git-diff) | -| Go to Line | [![macOS Build Status](https://travis-ci.org/atom/go-to-line.svg?branch=master)](https://travis-ci.org/atom/go-to-line) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/qf0isc8ulw4wxi0b/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/go-to-line/branch/master) | [![Dependency Status](https://david-dm.org/atom/go-to-line.svg)](https://david-dm.org/atom/go-to-line) | -| Grammar Selector | [![macOS Build Status](https://travis-ci.org/atom/grammar-selector.svg?branch=master)](https://travis-ci.org/atom/grammar-selector) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/pg8qss03bfh4ngqm/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/grammar-selector/branch/master) | [![Dependency Status](https://david-dm.org/atom/grammar-selector.svg)](https://david-dm.org/atom/grammar-selector) | -| Image View | [![macOS Build Status](https://travis-ci.org/atom/image-view.svg?branch=master)](https://travis-ci.org/atom/image-view) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/notavaawrswk0g10/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/image-view/branch/master) | [![Dependency Status](https://david-dm.org/atom/image-view.svg)](https://david-dm.org/atom/image-view) | -| Incompatible Packages | [![macOS Build Status](https://travis-ci.org/atom/incompatible-packages.svg?branch=master)](https://travis-ci.org/atom/incompatible-packages) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/neet595s038x7w70/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/incompatible-packages/branch/master) | [![Dependency Status](https://david-dm.org/atom/incompatible-packages.svg)](https://david-dm.org/atom/incompatible-packages) | -| Keybinding Resolver | [![macOS Build Status](https://travis-ci.org/atom/keybinding-resolver.svg?branch=master)](https://travis-ci.org/atom/keybinding-resolver) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/9jf31itx01hnn4nh/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/keybinding-resolver/branch/master) | [![Dependency Status](https://david-dm.org/atom/keybinding-resolver.svg)](https://david-dm.org/atom/keybinding-resolver) | -| Line Ending Selector | [![macOS Build Status](https://travis-ci.org/atom/line-ending-selector.svg?branch=master)](https://travis-ci.org/atom/line-ending-selector) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/b3743n9ojomlpn1g/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/line-ending-selector/branch/master) | [![Dependency Status](https://david-dm.org/atom/line-ending-selector.svg)](https://david-dm.org/atom/line-ending-selector) | -| Link | [![macOS Build Status](https://travis-ci.org/atom/link.svg?branch=master)](https://travis-ci.org/atom/link) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/1d3cb8ktd48k9vnl/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/link/branch/master) | [![Dependency Status](https://david-dm.org/atom/link.svg)](https://david-dm.org/atom/link) | -| Markdown Preview | [![macOS Build Status](https://travis-ci.org/atom/markdown-preview.svg?branch=master)](https://travis-ci.org/atom/markdown-preview) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/bvh0evhh4v6w9b29/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/markdown-preview/branch/master) | [![Dependency Status](https://david-dm.org/atom/markdown-preview.svg)](https://david-dm.org/atom/markdown-preview) | -| Metrics | [![macOS Build Status](https://travis-ci.org/atom/metrics.svg?branch=master)](https://travis-ci.org/atom/metrics) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/b5doi205xl3iex04/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/metrics/branch/master) | [![Dependency Status](https://david-dm.org/atom/metrics.svg)](https://david-dm.org/atom/metrics) | -| Notifications | [![macOS Build Status](https://travis-ci.org/atom/notifications.svg?branch=master)](https://travis-ci.org/atom/notifications) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/ps3p8tj2okw57x0e/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/notifications/branch/master) | [![Dependency Status](https://david-dm.org/atom/notifications.svg)](https://david-dm.org/atom/notifications) | -| Open on Github | [![macOS Build Status](https://travis-ci.org/atom/open-on-github.svg?branch=master)](https://travis-ci.org/atom/open-on-github) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/ccl6na4qsna5wncr/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/open-on-github/branch/master) | [![Dependency Status](https://david-dm.org/atom/open-on-github.svg)](https://david-dm.org/atom/open-on-github) | -| Package Generator | [![macOS Build Status](https://travis-ci.org/atom/package-generator.svg?branch=master)](https://travis-ci.org/atom/package-generator)| [![Windows Build Status](https://ci.appveyor.com/api/projects/status/7t1i4hdmljhigp9u/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/package-generator/branch/master) | [![Dependency Status](https://david-dm.org/atom/package-generator.svg)](https://david-dm.org/atom/package-generator) | -| Settings View | [![macOS Build Status](https://travis-ci.org/atom/settings-view.svg?branch=master)](https://travis-ci.org/atom/settings-view) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/hatgxg6k2g3grafq/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/settings-view/branch/master) | [![Dependency Status](https://david-dm.org/atom/settings-view.svg)](https://david-dm.org/atom/settings-view) | -| Snippets | [![macOS Build Status](https://travis-ci.org/atom/snippets.svg?branch=master)](https://travis-ci.org/atom/snippets) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/8hlc0onofkgbxw53/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/snippets/branch/master) | [![Dependency Status](https://david-dm.org/atom/snippets.svg)](https://david-dm.org/atom/snippets) | -| Spell Check | [![macOS Build Status](https://travis-ci.org/atom/spell-check.svg?branch=master)](https://travis-ci.org/atom/spell-check) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/1620a5reqw6kdolv/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/spell-check/branch/master) | [![Dependency Status](https://david-dm.org/atom/spell-check.svg)](https://david-dm.org/atom/spell-check) | -| Status Bar | [![macOS Build Status](https://travis-ci.org/atom/status-bar.svg?branch=master)](https://travis-ci.org/atom/status-bar) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/gu8tv4h6cnpeesg2/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/status-bar/branch/master) | [![Dependency Status](https://david-dm.org/atom/status-bar.svg)](https://david-dm.org/atom/status-bar) | -| Styleguide | [![macOS Build Status](https://travis-ci.org/atom/styleguide.svg?branch=master)](https://travis-ci.org/atom/styleguide) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/88dt9jxexkpindhw/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/styleguide/branch/master) | [![Dependency Status](https://david-dm.org/atom/styleguide.svg)](https://david-dm.org/atom/styleguide) | -| Symbols View | [![macOS Build Status](https://travis-ci.org/atom/symbols-view.svg?branch=master)](https://travis-ci.org/atom/symbols-view) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/al68vtv83x49eu5d/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/symbols-view/branch/master) | [![Dependency Status](https://david-dm.org/atom/symbols-view.svg)](https://david-dm.org/atom/symbols-view) | -| Tabs | [![macOS Build Status](https://travis-ci.org/atom/tabs.svg?branch=master)](https://travis-ci.org/atom/tabs) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/nf4hdmuk4i9xkfmb/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/tabs/branch/master) | [![Dependency Status](https://david-dm.org/atom/tabs.svg)](https://david-dm.org/atom/tabs) | -| Timecop | [![macOS Build Status](https://travis-ci.org/atom/timecop.svg?branch=master)](https://travis-ci.org/atom/timecop) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/37fhichmvx90sd97/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/timecop/branch/master) | [![Dependency Status](https://david-dm.org/atom/timecop.svg)](https://david-dm.org/atom/timecop) | -| Tree View | [![macOS Build Status](https://travis-ci.org/atom/tree-view.svg?branch=master)](https://travis-ci.org/atom/tree-view) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/com793ehi0hajrkd/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/tree-view/branch/master) | [![Dependency Status](https://david-dm.org/atom/tree-view.svg)](https://david-dm.org/atom/tree-view) | -| Update Package Dependencies | [![macOS Build Status](https://travis-ci.org/atom/update-package-dependencies.svg?branch=master)](https://travis-ci.org/atom/update-package-dependencies) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/5xqtoc3xk1e7lt2y/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/update-package-dependencies/branch/master) | [![Dependency Status](https://david-dm.org/atom/update-package-dependencies.svg)](https://david-dm.org/atom/update-package-dependencies) | -| Welcome | [![macOS Build Status](https://travis-ci.org/atom/welcome.svg?branch=master)](https://travis-ci.org/atom/welcome) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/c3ssyte35ivvnt62/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/welcome/branch/master) | [![Dependency Status](https://david-dm.org/atom/welcome.svg)](https://david-dm.org/atom/welcome) | -| Whitespace | [![macOS Build Status](https://travis-ci.org/atom/whitespace.svg?branch=master)](https://travis-ci.org/atom/whitespace) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/sf8pdb3ausdk1vtb/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/whitespace/branch/master) | [![Dependency Status](https://david-dm.org/atom/whitespace.svg)](https://david-dm.org/atom/whitespace) | -| Wrap Guide | [![macOS Build Status](https://travis-ci.org/atom/wrap-guide.svg?branch=master)](https://travis-ci.org/atom/wrap-guide) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/5qk1io3uar5j8hol/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/wrap-guide/branch/master) | [![Dependency Status](https://david-dm.org/atom/wrap-guide.svg)](https://david-dm.org/atom/wrap-guide) | - +| Package | Travis | AppVeyor/Win | Dependencies | +|---------|--------|--------------|--------------| +| [About](https://github.com/atom/about) | [![macOS Build Status](https://travis-ci.org/atom/about.svg?branch=master)](https://travis-ci.org/atom/about) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/msprea3vq47l8oce/branch/master?svg=true)](https://ci.appveyor.com/project/atom/about/branch/master) | [![Dependency Status](https://david-dm.org/atom/about.svg)](https://david-dm.org/atom/about) | +| [Archive View](https://github.com/atom/archive-view) | [![macOS Build Status](https://travis-ci.org/atom/archive-view.svg?branch=master)](https://travis-ci.org/atom/archive-view) | [![Windows Build status](https://ci.appveyor.com/api/projects/status/u3qfgaod4lhriqlj/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/archive-view/branch/master) | [![Dependency Status](https://david-dm.org/atom/archive-view.svg)](https://david-dm.org/atom/archive-view) | +| [AutoComplete Atom API](https://github.com/atom/autocomplete-atom-api) | [![macOS Build Status](https://travis-ci.org/atom/autocomplete-atom-api.svg?branch=master)](https://travis-ci.org/atom/autocomplete-atom-api) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/1x3uqd9ddchpe555/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/autocomplete-atom-api/branch/master) | [![Dependency Status](https://david-dm.org/atom/autocomplete-atom-api.svg)](https://david-dm.org/atom/autocomplete-atom-api) | +| [AutoComplete CSS](https://github.com/atom/autocomplete-css) | [![macOS Build Status](https://travis-ci.org/atom/autocomplete-css.svg?branch=master)](https://travis-ci.org/atom/autocomplete-css) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/k3e5uvpmpc5bkja9/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/autocomplete-css/branch/master) | [![Dependency Status](https://david-dm.org/atom/autocomplete-css.svg)](https://david-dm.org/atom/autocomplete-css) | +| [AutoComplete HTML](https://github.com/atom/autocomplete-html) | [![macOS Build Status](https://travis-ci.org/atom/autocomplete-html.svg?branch=master)](https://travis-ci.org/atom/autocomplete-html) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/bsaqbg1fljpd9q1b/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/autocomplete-html/branch/master) | [![Dependency Status](https://david-dm.org/atom/autocomplete-html.svg)](https://david-dm.org/atom/autocomplete-html) | +| [AutoComplete+](https://github.com/atom/autocomplete-plus) | [![macOS Build Status](https://travis-ci.org/atom/autocomplete-plus.svg?branch=master)](https://travis-ci.org/atom/autocomplete-plus) | [![Windows Build status](https://ci.appveyor.com/api/projects/status/9bpokrud2apgqsq0/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/autocomplete-plus/branch/master) | [![Dependency Status](https://david-dm.org/atom/autocomplete-plus.svg)](https://david-dm.org/atom/autocomplete-plus) | +| [AutoComplete Snippets](https://github.com/atom/autocomplete-snippets) | [![macOS Build Status](https://travis-ci.org/atom/autocomplete-snippets.svg)](https://travis-ci.org/atom/autocomplete-snippets) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/72kfi83l6cw90joy/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/autocomplete-snippets/branch/master) | [![Dependency Status](https://david-dm.org/atom/autocomplete-snippets.svg)](https://david-dm.org/atom/autocomplete-snippets) | +| [AutoFlow](https://github.com/atom/autoflow) | [![macOS Build Status](https://travis-ci.org/atom/autoflow.svg?branch=master)](https://travis-ci.org/atom/autoflow) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/kpmsnkbooa29x907/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/autoflow/branch/master) | [![Dependency Status](https://david-dm.org/atom/autoflow.svg)](https://david-dm.org/atom/autoflow) | +| [AutoSave](https://github.com/atom/autosave) | [![macOS Build Status](https://travis-ci.org/atom/autosave.svg?branch=master)](https://travis-ci.org/atom/autosave) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/3aktr9updp722fqx/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/autosave/branch/master) | [![Dependency Status](https://david-dm.org/atom/autosave.svg)](https://david-dm.org/atom/autosave) | +| [Background Tips](https://github.com/atom/background-tips) | [![macOS Build Status](https://travis-ci.org/atom/background-tips.svg?branch=master)](https://travis-ci.org/atom/background-tips) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/2utcugietl5vjc7w/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/background-tips/branch/master) | [![Dependency Status](https://david-dm.org/atom/background-tips.svg)](https://david-dm.org/atom/background-tips) | +| [Bookmarks](https://github.com/atom/bookmarks) | [![macOS Build Status](https://travis-ci.org/atom/bookmarks.svg?branch=master)](https://travis-ci.org/atom/bookmarks) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/vjsf78pj4rw6ibcw/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/bookmarks/branch/master) | [![Dependency Status](https://david-dm.org/atom/bookmarks.svg)](https://david-dm.org/atom/bookmarks) | +| [Bracket Matcher](https://github.com/atom/bracket-matcher) | [![macOS Build Status](https://travis-ci.org/atom/bracket-matcher.svg?branch=master)](https://travis-ci.org/atom/bracket-matcher) | [![Windows Build status](https://ci.appveyor.com/api/projects/status/rrsl2h7e0od26k54/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/bracket-matcher/branch/master) | [![Dependency Status](https://david-dm.org/atom/bracket-matcher.svg)](https://david-dm.org/atom/bracket-matcher) | +| [Command Palette](https://github.com/atom/command-palette) | [![macOS Build Status](https://travis-ci.org/atom/command-palette.svg?branch=master)](https://travis-ci.org/atom/command-palette) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/jqgwetayr0enorun/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/command-palette/branch/master) | [![Dependency Status](https://david-dm.org/atom/command-palette.svg)](https://david-dm.org/atom/command-palette) | +| [Deprecation Cop](https://github.com/atom/deprecation-cop) | [![macOS Build Status](https://travis-ci.org/atom/deprecation-cop.svg?branch=master)](https://travis-ci.org/atom/deprecation-cop) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/0s870q5fj3vwihjx/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/deprecation-cop/branch/master) | [![Dependency Status](https://david-dm.org/atom/deprecation-cop.svg)](https://david-dm.org/atom/deprecation-cop) | +| [Dev Live Reload](https://github.com/atom/dev-live-reload) | [![macOS Build Status](https://travis-ci.org/atom/dev-live-reload.svg?branch=master)](https://travis-ci.org/atom/dev-live-reload) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/g3sd27ylba1fun1v/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/dev-live-reload/branch/master) | [![Dependency Status](https://david-dm.org/atom/dev-live-reload.svg)](https://david-dm.org/atom/dev-live-reload) | +| [Encoding Selector](https://github.com/atom/encoding-selector) | [![macOS Build Status](https://travis-ci.org/atom/encoding-selector.svg?branch=master)](https://travis-ci.org/atom/encoding-selector) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/e08x6k2b68wpwxxc/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/encoding-selector/branch/master) | [![Dependency Status](https://david-dm.org/atom/encoding-selector.svg)](https://david-dm.org/atom/encoding-selector) | +| [Exception Reporting](https://github.com/atom/exception-reporting) | [![macOS Build Status](https://travis-ci.org/atom/exception-reporting.svg?branch=master)](https://travis-ci.org/atom/exception-reporting) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/i0pla7qbpv7celg2/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/exception-reporting/branch/master) | [![Dependency Status](https://david-dm.org/atom/exception-reporting.svg)](https://david-dm.org/atom/exception-reporting) | +| [Find and Replace](https://github.com/atom/find-and-replace) | [![macOS Build Status](https://travis-ci.org/atom/find-and-replace.svg?branch=master)](https://travis-ci.org/atom/find-and-replace) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/6w4baiiq5mw4nxky/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/find-and-replace/branch/master) | [![Dependency Status](https://david-dm.org/atom/find-and-replace.svg)](https://david-dm.org/atom/find-and-replace) | +| [Fuzzy Finder](https://github.com/atom/fuzzy-finder) | [![macOS Build Status](https://travis-ci.org/atom/fuzzy-finder.svg?branch=master)](https://travis-ci.org/atom/fuzzy-finder) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/b4b2dg5n9r1wdqad/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/fuzzy-finder/branch/master) | [![Dependency Status](https://david-dm.org/atom/fuzzy-finder.svg)](https://david-dm.org/atom/fuzzy-finder) | +| [Git Diff](https://github.com/atom/git-diff) | [![macOS Build Status](https://travis-ci.org/atom/git-diff.svg?branch=master)](https://travis-ci.org/atom/git-diff) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/9auj52cs0vso66nv/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/git-diff/branch/master) | [![Dependency Status](https://david-dm.org/atom/git-diff.svg)](https://david-dm.org/atom/git-diff) | +| [Go to Line](https://github.com/atom/go-to-line) | [![macOS Build Status](https://travis-ci.org/atom/go-to-line.svg?branch=master)](https://travis-ci.org/atom/go-to-line) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/qf0isc8ulw4wxi0b/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/go-to-line/branch/master) | [![Dependency Status](https://david-dm.org/atom/go-to-line.svg)](https://david-dm.org/atom/go-to-line) | +| [Grammar Selector](https://github.com/atom/grammar-selector) | [![macOS Build Status](https://travis-ci.org/atom/grammar-selector.svg?branch=master)](https://travis-ci.org/atom/grammar-selector) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/pg8qss03bfh4ngqm/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/grammar-selector/branch/master) | [![Dependency Status](https://david-dm.org/atom/grammar-selector.svg)](https://david-dm.org/atom/grammar-selector) | +| [Image View](https://github.com/atom/image-view) | [![macOS Build Status](https://travis-ci.org/atom/image-view.svg?branch=master)](https://travis-ci.org/atom/image-view) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/notavaawrswk0g10/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/image-view/branch/master) | [![Dependency Status](https://david-dm.org/atom/image-view.svg)](https://david-dm.org/atom/image-view) | +| [Incompatible Packages](https://github.com/atom/incompatible-packages) | [![macOS Build Status](https://travis-ci.org/atom/incompatible-packages.svg?branch=master)](https://travis-ci.org/atom/incompatible-packages) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/neet595s038x7w70/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/incompatible-packages/branch/master) | [![Dependency Status](https://david-dm.org/atom/incompatible-packages.svg)](https://david-dm.org/atom/incompatible-packages) | +| [Keybinding Resolver](https://github.com/atom/keybinding-resolver) | [![macOS Build Status](https://travis-ci.org/atom/keybinding-resolver.svg?branch=master)](https://travis-ci.org/atom/keybinding-resolver) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/9jf31itx01hnn4nh/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/keybinding-resolver/branch/master) | [![Dependency Status](https://david-dm.org/atom/keybinding-resolver.svg)](https://david-dm.org/atom/keybinding-resolver) | +| [Line Ending Selector](https://github.com/atom/line-ending-selector) | [![macOS Build Status](https://travis-ci.org/atom/line-ending-selector.svg?branch=master)](https://travis-ci.org/atom/line-ending-selector) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/b3743n9ojomlpn1g/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/line-ending-selector/branch/master) | [![Dependency Status](https://david-dm.org/atom/line-ending-selector.svg)](https://david-dm.org/atom/line-ending-selector) | +| [Link](https://github.com/atom/link) | [![macOS Build Status](https://travis-ci.org/atom/link.svg?branch=master)](https://travis-ci.org/atom/link) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/1d3cb8ktd48k9vnl/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/link/branch/master) | [![Dependency Status](https://david-dm.org/atom/link.svg)](https://david-dm.org/atom/link) | +| [Markdown Preview](https://github.com/atom/markdown-preview) | [![macOS Build Status](https://travis-ci.org/atom/markdown-preview.svg?branch=master)](https://travis-ci.org/atom/markdown-preview) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/bvh0evhh4v6w9b29/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/markdown-preview/branch/master) | [![Dependency Status](https://david-dm.org/atom/markdown-preview.svg)](https://david-dm.org/atom/markdown-preview) | +| [Metrics](https://github.com/atom/metrics) | [![macOS Build Status](https://travis-ci.org/atom/metrics.svg?branch=master)](https://travis-ci.org/atom/metrics) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/b5doi205xl3iex04/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/metrics/branch/master) | [![Dependency Status](https://david-dm.org/atom/metrics.svg)](https://david-dm.org/atom/metrics) | +| [Notifications](https://github.com/atom/notifications) | [![macOS Build Status](https://travis-ci.org/atom/notifications.svg?branch=master)](https://travis-ci.org/atom/notifications) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/ps3p8tj2okw57x0e/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/notifications/branch/master) | [![Dependency Status](https://david-dm.org/atom/notifications.svg)](https://david-dm.org/atom/notifications) | +| [Open on Github](https://github.com/atom/open-on-github) | [![macOS Build Status](https://travis-ci.org/atom/open-on-github.svg?branch=master)](https://travis-ci.org/atom/open-on-github) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/ccl6na4qsna5wncr/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/open-on-github/branch/master) | [![Dependency Status](https://david-dm.org/atom/open-on-github.svg)](https://david-dm.org/atom/open-on-github) | +| [Package Generator](https://github.com/atom/package-generator) | [![macOS Build Status](https://travis-ci.org/atom/package-generator.svg?branch=master)](https://travis-ci.org/atom/package-generator)| [![Windows Build Status](https://ci.appveyor.com/api/projects/status/7t1i4hdmljhigp9u/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/package-generator/branch/master) | [![Dependency Status](https://david-dm.org/atom/package-generator.svg)](https://david-dm.org/atom/package-generator) | +| [Settings View](https://github.com/atom/settings-view) | [![macOS Build Status](https://travis-ci.org/atom/settings-view.svg?branch=master)](https://travis-ci.org/atom/settings-view) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/hatgxg6k2g3grafq/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/settings-view/branch/master) | [![Dependency Status](https://david-dm.org/atom/settings-view.svg)](https://david-dm.org/atom/settings-view) | +| [Snippets](https://github.com/atom/snippets) | [![macOS Build Status](https://travis-ci.org/atom/snippets.svg?branch=master)](https://travis-ci.org/atom/snippets) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/8hlc0onofkgbxw53/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/snippets/branch/master) | [![Dependency Status](https://david-dm.org/atom/snippets.svg)](https://david-dm.org/atom/snippets) | +| [Spell Check](https://github.com/atom/spell-check) | [![macOS Build Status](https://travis-ci.org/atom/spell-check.svg?branch=master)](https://travis-ci.org/atom/spell-check) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/1620a5reqw6kdolv/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/spell-check/branch/master) | [![Dependency Status](https://david-dm.org/atom/spell-check.svg)](https://david-dm.org/atom/spell-check) | +| [Status Bar](https://github.com/atom/status-bar) | [![macOS Build Status](https://travis-ci.org/atom/status-bar.svg?branch=master)](https://travis-ci.org/atom/status-bar) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/gu8tv4h6cnpeesg2/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/status-bar/branch/master) | [![Dependency Status](https://david-dm.org/atom/status-bar.svg)](https://david-dm.org/atom/status-bar) | +| [Styleguide](https://github.com/atom/styleguide) | [![macOS Build Status](https://travis-ci.org/atom/styleguide.svg?branch=master)](https://travis-ci.org/atom/styleguide) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/88dt9jxexkpindhw/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/styleguide/branch/master) | [![Dependency Status](https://david-dm.org/atom/styleguide.svg)](https://david-dm.org/atom/styleguide) | +| [Symbols View](https://github.com/atom/symbols-view) | [![macOS Build Status](https://travis-ci.org/atom/symbols-view.svg?branch=master)](https://travis-ci.org/atom/symbols-view) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/al68vtv83x49eu5d/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/symbols-view/branch/master) | [![Dependency Status](https://david-dm.org/atom/symbols-view.svg)](https://david-dm.org/atom/symbols-view) | +| [Tabs](https://github.com/atom/tabs) | [![macOS Build Status](https://travis-ci.org/atom/tabs.svg?branch=master)](https://travis-ci.org/atom/tabs) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/nf4hdmuk4i9xkfmb/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/tabs/branch/master) | [![Dependency Status](https://david-dm.org/atom/tabs.svg)](https://david-dm.org/atom/tabs) | +| [Timecop](https://github.com/atom/timecop) | [![macOS Build Status](https://travis-ci.org/atom/timecop.svg?branch=master)](https://travis-ci.org/atom/timecop) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/37fhichmvx90sd97/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/timecop/branch/master) | [![Dependency Status](https://david-dm.org/atom/timecop.svg)](https://david-dm.org/atom/timecop) | +| [Tree View](https://github.com/atom/tree-view) | [![macOS Build Status](https://travis-ci.org/atom/tree-view.svg?branch=master)](https://travis-ci.org/atom/tree-view) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/com793ehi0hajrkd/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/tree-view/branch/master) | [![Dependency Status](https://david-dm.org/atom/tree-view.svg)](https://david-dm.org/atom/tree-view) | +| [Update Package Dependencies](https://github.com/atom/update-package-dependencies) | [![macOS Build Status](https://travis-ci.org/atom/update-package-dependencies.svg?branch=master)](https://travis-ci.org/atom/update-package-dependencies) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/5xqtoc3xk1e7lt2y/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/update-package-dependencies/branch/master) | [![Dependency Status](https://david-dm.org/atom/update-package-dependencies.svg)](https://david-dm.org/atom/update-package-dependencies) | +| [Welcome](https://github.com/atom/welcome) | [![macOS Build Status](https://travis-ci.org/atom/welcome.svg?branch=master)](https://travis-ci.org/atom/welcome) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/c3ssyte35ivvnt62/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/welcome/branch/master) | [![Dependency Status](https://david-dm.org/atom/welcome.svg)](https://david-dm.org/atom/welcome) | +| [Whitespace](https://github.com/atom/whitespace) | [![macOS Build Status](https://travis-ci.org/atom/whitespace.svg?branch=master)](https://travis-ci.org/atom/whitespace) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/sf8pdb3ausdk1vtb/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/whitespace/branch/master) | [![Dependency Status](https://david-dm.org/atom/whitespace.svg)](https://david-dm.org/atom/whitespace) | +| [Wrap Guide](https://github.com/atom/wrap-guide) | [![macOS Build Status](https://travis-ci.org/atom/wrap-guide.svg?branch=master)](https://travis-ci.org/atom/wrap-guide) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/5qk1io3uar5j8hol/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/wrap-guide/branch/master) | [![Dependency Status](https://david-dm.org/atom/wrap-guide.svg)](https://david-dm.org/atom/wrap-guide) | ## Libraries -| Library | macOS | Windows | Dependencies | -|---------|------|---------|--------------| -| Clear Cut | [![macOS Build Status](https://travis-ci.org/atom/clear-cut.svg?branch=master)](https://travis-ci.org/atom/clear-cut) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/civ54x89l06286m9/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/clear-cut/branch/master) | [![Dependency Status](https://david-dm.org/atom/clear-cut.svg)](https://david-dm.org/atom/clear-cut) | -| Event Kit | [![macOS Build Status](https://travis-ci.org/atom/event-kit.svg?branch=master)](https://travis-ci.org/atom/event-kit) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/lb32q70204lpmlxo/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/event-kit/branch/master) | [![Dependency Status](https://david-dm.org/atom/event-kit.svg)](https://david-dm.org/atom/event-kit) | -| Fs Plus | [![macOS Build Status](https://travis-ci.org/atom/fs-plus.svg?branch=master)](https://travis-ci.org/atom/fs-plus) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/gf2tleqp0hdek3o3/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/fs-plus/branch/master) | [![Dependency Status](https://david-dm.org/atom/fs-plus.svg)](https://david-dm.org/atom/fs-plus) | -| Grim | [![macOS Build Status](https://travis-ci.org/atom/grim.svg)](https://travis-ci.org/atom/grim) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/i4m37pol77vygrvb/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/grim/branch/master) | [![Dependency Status](https://david-dm.org/atom/grim.svg)](https://david-dm.org/atom/grim) | -| Jasmine Focused | [![macOS Build Status](https://travis-ci.org/atom/grim.svg)](https://travis-ci.org/atom/grim) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/af0ipfqqxn7aygoe/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/jasmine-focused/branch/master) | [![Dependency Status](https://david-dm.org/atom/jasmine-focused.svg)](https://david-dm.org/atom/jasmine-focused) | -| Property Accessors | [![macOS Build Status](https://travis-ci.org/atom/property-accessors.svg?branch=master)](https://travis-ci.org/atom/property-accessors) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/ww4d10hi4v5h7kbp/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/property-accessors/branch/master) | [![Dependency Status](https://david-dm.org/atom/property-accessors.svg)](https://david-dm.org/atom/property-accessors) | -| TextBuffer | [![macOS Build Status](https://travis-ci.org/atom/text-buffer.svg?branch=master)](https://travis-ci.org/atom/text-buffer) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/48xl8do1sm2thf5p/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/text-buffer/branch/master) | [![Dependency Status](https://david-dm.org/atom/text-buffer.svg)](https://david-dm.org/atom/text-buffer) | -| Underscore-Plus | [![macOS Build Status](https://travis-ci.org/atom/underscore-plus.svg?branch=master)](https://travis-ci.org/atom/underscore-plus) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/c7l8009vgpaojxcd/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/underscore-plus/branch/master) | [![Dependency Status](https://david-dm.org/atom/underscore-plus.svg)](https://david-dm.org/atom/underscore-plus) | - +| Library | Travis | AppVeyor/Win | Dependencies | +|---------|--------|--------------|--------------| +| [Clear Cut](https://github.com/atom/clear-cut) | [![macOS Build Status](https://travis-ci.org/atom/clear-cut.svg?branch=master)](https://travis-ci.org/atom/clear-cut) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/civ54x89l06286m9/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/clear-cut/branch/master) | [![Dependency Status](https://david-dm.org/atom/clear-cut.svg)](https://david-dm.org/atom/clear-cut) | +| [Event Kit](https://github.com/atom/event-kit) | [![macOS Build Status](https://travis-ci.org/atom/event-kit.svg?branch=master)](https://travis-ci.org/atom/event-kit) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/lb32q70204lpmlxo/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/event-kit/branch/master) | [![Dependency Status](https://david-dm.org/atom/event-kit.svg)](https://david-dm.org/atom/event-kit) | +| [First Mate](https://github.com/atom/first-mate) | [![macOS Build Status](https://travis-ci.org/atom/first-mate.svg?branch=master)](https://travis-ci.org/atom/first-mate) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/p5im21uq22cwgb6d/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/first-mate) | [![Dependency Status](https://david-dm.org/atom/first-mate/status.svg)](https://david-dm.org/atom/first-mate) | +| [Fs Plus](https://github.com/atom/fs-plus) | [![macOS Build Status](https://travis-ci.org/atom/fs-plus.svg?branch=master)](https://travis-ci.org/atom/fs-plus) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/gf2tleqp0hdek3o3/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/fs-plus/branch/master) | [![Dependency Status](https://david-dm.org/atom/fs-plus.svg)](https://david-dm.org/atom/fs-plus) | +| [Grim](https://github.com/atom/grim) | [![macOS Build Status](https://travis-ci.org/atom/grim.svg)](https://travis-ci.org/atom/grim) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/i4m37pol77vygrvb/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/grim/branch/master) | [![Dependency Status](https://david-dm.org/atom/grim.svg)](https://david-dm.org/atom/grim) | +| [Jasmine Focused](https://github.com/atom/jasmine-focused) | [![macOS Build Status](https://travis-ci.org/atom/grim.svg)](https://travis-ci.org/atom/grim) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/af0ipfqqxn7aygoe/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/jasmine-focused/branch/master) | [![Dependency Status](https://david-dm.org/atom/jasmine-focused.svg)](https://david-dm.org/atom/jasmine-focused) | +| [Keyboard Layout](https://github.com/atom/keyboard-layout) | [![macOS Build Status](https://travis-ci.org/atom/keyboard-layout.svg?branch=master)](https://travis-ci.org/atom/keyboard-layout) | [![Windows Build status](https://ci.appveyor.com/api/projects/status/rk8wooeyh689apgd/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/keyboard-layout) | [![Dependency Status](https://david-dm.org/atom/keyboard-layout/status.svg)](https://david-dm.org/atom/keyboard-layout) | +| [Oniguruma](https://github.com/atom/node-oniguruma) | [![macOS Build Status](https://travis-ci.org/atom/node-oniguruma.svg?branch=master)](https://travis-ci.org/atom/node-oniguruma) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/s9twhi451ef2butr/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/node-oniguruma/branch/master) | [![Dependency Status](https://david-dm.org/atom/node-oniguruma.svg)](https://david-dm.org/atom/node-oniguruma) | +| [PathWatcher](https://github.com/atom/node-pathwatcher) | [![macOS Build Status](https://travis-ci.org/atom/node-pathwatcher.svg?branch=master)](https://travis-ci.org/atom/node-pathwatcher) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/li8dkoucdrc2ryts/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/node-pathwatcher) | [![Dependency Status](https://david-dm.org/atom/node-pathwatcher/status.svg)](https://david-dm.org/atom/node-pathwatcher) | +| [Property Accessors](https://github.com/atom/property-accessors) | [![macOS Build Status](https://travis-ci.org/atom/property-accessors.svg?branch=master)](https://travis-ci.org/atom/property-accessors) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/ww4d10hi4v5h7kbp/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/property-accessors/branch/master) | [![Dependency Status](https://david-dm.org/atom/property-accessors.svg)](https://david-dm.org/atom/property-accessors) | +| [Season](https://github.com/atom/season) | [![macOS Build Status](https://travis-ci.org/atom/season.svg?branch=master)](https://travis-ci.org/atom/season) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/v3bth3ooq5q8k8lx/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/season) | [![Dependency Status](https://david-dm.org/atom/season.svg)](https://david-dm.org/atom/season) | +| [TextBuffer](https://github.com/atom/text-buffer) | [![macOS Build Status](https://travis-ci.org/atom/text-buffer.svg?branch=master)](https://travis-ci.org/atom/text-buffer) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/48xl8do1sm2thf5p/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/text-buffer/branch/master) | [![Dependency Status](https://david-dm.org/atom/text-buffer.svg)](https://david-dm.org/atom/text-buffer) | +| [Underscore-Plus](https://github.com/atom/underscore-plus) | [![macOS Build Status](https://travis-ci.org/atom/underscore-plus.svg?branch=master)](https://travis-ci.org/atom/underscore-plus) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/c7l8009vgpaojxcd/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/underscore-plus/branch/master) | [![Dependency Status](https://david-dm.org/atom/underscore-plus.svg)](https://david-dm.org/atom/underscore-plus) | ## Tools -| Language | macOS | Windows | Dependencies | -|----------|------|---------|--------------| -| AtomDoc | [![macOS Build Status](https://travis-ci.org/atom/atomdoc.svg?branch=master)](https://travis-ci.org/atom/atomdoc) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/chi2bmaafr3puyq2/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/atomdoc/branch/master) | [![Dependency Status](https://david-dm.org/atom/atomdoc.svg)](https://david-dm.org/atom/atomdoc) +| Language | Travis | AppVeyor/Win | Dependencies | +|----------|--------|--------------|--------------| +| [AtomDoc](https://github.com/atom/atomdoc) | [![macOS Build Status](https://travis-ci.org/atom/atomdoc.svg?branch=master)](https://travis-ci.org/atom/atomdoc) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/chi2bmaafr3puyq2/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/atomdoc/branch/master) | [![Dependency Status](https://david-dm.org/atom/atomdoc.svg)](https://david-dm.org/atom/atomdoc) ## Languages -| Language | macOS | Windows | -|----------|------|---------| -| C/C++ | [![macOS Build Status](https://travis-ci.org/atom/language-c.svg?branch=master)](https://travis-ci.org/atom/language-c) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/8oy1hmp4yrij7c32/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-c/branch/master) | -| C# | [![macOS Build Status](https://travis-ci.org/atom/language-csharp.svg?branch=master)](https://travis-ci.org/atom/language-csharp) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/j1as3753y5t90obn/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-csharp/branch/master) | -| Clojure | [![macOS Build Status](https://travis-ci.org/atom/language-clojure.svg?branch=master)](https://travis-ci.org/atom/language-clojure) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/6kd5fs48y5hixde6/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-clojure/branch/master) | -| CoffeeScript | [![macOS Build Status](https://travis-ci.org/atom/language-coffee-script.svg?branch=master)](https://travis-ci.org/atom/language-coffee-script) | [![Windows Build status](https://ci.appveyor.com/api/projects/status/4j9aak7iwn2f2x7a/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-coffee-script/branch/master) | -| CSS | [![macOS Build Status](https://travis-ci.org/atom/language-css.svg?branch=master)](https://travis-ci.org/atom/language-css) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/v8rvm88dxp73ko2y/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-css/branch/master) | -| Git | [![macOS Build Status](https://travis-ci.org/atom/language-git.svg?branch=master)](https://travis-ci.org/atom/language-git) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/481319gyrr1feo8b/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-git/branch/master) | -| GitHub Flavored Markdown | [![macOS Build Status](https://travis-ci.org/atom/language-gfm.svg?branch=master)](https://travis-ci.org/atom/language-gfm) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/rpub8qjyd8lt7wai/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-gfm/branch/master) | -| Go | [![macOS Build Status](https://travis-ci.org/atom/language-go.svg?branch=master)](https://travis-ci.org/atom/language-go) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/3fxxvv05p4hv92pn/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-go/branch/master) | -| HTML | [![macOS Build Status](https://travis-ci.org/atom/language-html.svg?branch=master)](https://travis-ci.org/atom/language-html) | [![Windows Build status](https://ci.appveyor.com/api/projects/status/t6pk6mmdgcelfg85/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-html/branch/master) | -| Hyperlink | [![macOS Build Status](https://travis-ci.org/atom/language-hyperlink.svg?branch=master)](https://travis-ci.org/atom/language-hyperlink) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/5tgvhph394r684l8/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-hyperlink/branch/master) | -| Java | [![macOS Build Status](https://travis-ci.org/atom/language-java.svg?branch=master)](https://travis-ci.org/atom/language-java) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/utoftje56n9u5x4h/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-java/branch/master) | -| JavaScript | [![macOS Build Status](https://travis-ci.org/atom/language-javascript.svg?branch=master)](https://travis-ci.org/atom/language-javascript) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/ktooccwna96ssiyr/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-javascript-dijf8/branch/master) | -| JSON | [![macOS Build Status](https://travis-ci.org/atom/language-json.svg?branch=master)](https://travis-ci.org/atom/language-json) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/5rx05vhdikk6c4cl/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-json/branch/master) | -| Less | [![macOS Build Status](https://travis-ci.org/atom/language-less.svg?branch=master)](https://travis-ci.org/atom/language-less) | [![Windows Build Sstatus](https://ci.appveyor.com/api/projects/status/aeina4fr4b0i7yay/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-less/branch/master) | -| Make | [![macOS Build Status](https://travis-ci.org/atom/language-make.svg?branch=master)](https://travis-ci.org/atom/language-make) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/vq1aascey21wxjh7/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-make/branch/master) | -| Mustache | [![macOS Build Status](https://travis-ci.org/atom/language-mustache.svg?branch=master)](https://travis-ci.org/atom/language-mustache) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/mbxnxaojqp0g7ldv/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-mustache/branch/master) | -| Objective-C | [![macOS Build Status](https://travis-ci.org/atom/language-objective-c.svg?branch=master)](https://travis-ci.org/atom/language-objective-c) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/27j8vfv5u95fjhkw/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-objective-c/branch/master) | -| Perl | [![macOS Build Status](https://travis-ci.org/atom/language-perl.svg?branch=master)](https://travis-ci.org/atom/language-perl) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/dfs9inkkg40hchf8/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-perl/branch/master) | -| PHP | [![macOS Build Status](https://travis-ci.org/atom/language-php.svg?branch=master)](https://travis-ci.org/atom/language-php) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/y9h45ag4b72726jy/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-php/branch/master) | -| Python | [![macOS Build Status](https://travis-ci.org/atom/language-python.svg?branch=master)](https://travis-ci.org/atom/language-python) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/hmxrb9jttjh41es9/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-python/branch/master) | -| Ruby | [![macOS Build Status](https://travis-ci.org/atom/language-ruby.svg?branch=master)](https://travis-ci.org/atom/language-ruby) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/71as182rm1adf2br/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-ruby/branch/master) | -| Ruby on Rails | [![macOS Build Status](https://travis-ci.org/atom/language-ruby-on-rails.svg?branch=master)](https://travis-ci.org/atom/language-ruby-on-rails) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/5t4pa451fu5e0ghg/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-ruby-on-rails/branch/master) | -| Sass | [![macOS Build Status](https://travis-ci.org/atom/language-sass.svg?branch=master)](https://travis-ci.org/atom/language-sass) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/g7p16vainm4iuoot/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-sass/branch/master) | -| ShellScript | [![macOS Build Status](https://travis-ci.org/atom/language-shellscript.svg?branch=master)](https://travis-ci.org/atom/language-shellscript) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/p4um3lowgrg8y0ty/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-shellscript/branch/master) | -| SQL | [![macOS Build Status](https://travis-ci.org/atom/language-sql.svg?branch=master)](https://travis-ci.org/atom/language-sql) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/ji31ouk5ehs4jdu1/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-sql/branch/master) | -| TODO | [![macOS Build Status](https://travis-ci.org/atom/language-todo.svg?branch=master)](https://travis-ci.org/atom/language-todo) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/gcgb9m7h146lv6qp/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-todo/branch/master) | -| TOML | [![macOS Build Status](https://travis-ci.org/atom/language-toml.svg?branch=master)](https://travis-ci.org/atom/language-toml) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/kohao3fjyk6xv0sc/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-toml/branch/master) | -| XML | [![macOS Build Status](https://travis-ci.org/atom/language-xml.svg?branch=master)](https://travis-ci.org/atom/language-xml) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/m5f6rn74a6h3q5uq/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-xml/branch/master) | -| YAML | [![macOS Build Status](https://travis-ci.org/atom/language-yaml.svg?branch=master)](https://travis-ci.org/atom/language-yaml) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/eaa4ql7kipgphc2n/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-yaml/branch/master) | +| Language | Travis | AppVeyor/Win | +|----------|--------|--------------| +| [C/C++](https://github.com/atom/language-c) | [![macOS Build Status](https://travis-ci.org/atom/language-c.svg?branch=master)](https://travis-ci.org/atom/language-c) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/8oy1hmp4yrij7c32/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-c/branch/master) | +| [C#](https://github.com/atom/language-csharp) | [![macOS Build Status](https://travis-ci.org/atom/language-csharp.svg?branch=master)](https://travis-ci.org/atom/language-csharp) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/j1as3753y5t90obn/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-csharp/branch/master) | +| [Clojure](https://github.com/atom/language-clojure) | [![macOS Build Status](https://travis-ci.org/atom/language-clojure.svg?branch=master)](https://travis-ci.org/atom/language-clojure) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/6kd5fs48y5hixde6/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-clojure/branch/master) | +| [CoffeeScript](https://github.com/atom/language-coffee-script) | [![macOS Build Status](https://travis-ci.org/atom/language-coffee-script.svg?branch=master)](https://travis-ci.org/atom/language-coffee-script) | [![Windows Build status](https://ci.appveyor.com/api/projects/status/4j9aak7iwn2f2x7a/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-coffee-script/branch/master) | +| [CSS](https://github.com/atom/language-css) | [![macOS Build Status](https://travis-ci.org/atom/language-css.svg?branch=master)](https://travis-ci.org/atom/language-css) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/v8rvm88dxp73ko2y/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-css/branch/master) | +| [Git](https://github.com/atom/language-git) | [![macOS Build Status](https://travis-ci.org/atom/language-git.svg?branch=master)](https://travis-ci.org/atom/language-git) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/481319gyrr1feo8b/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-git/branch/master) | +| [GitHub Flavored Markdown](https://github.com/atom/language-gfm) | [![macOS Build Status](https://travis-ci.org/atom/language-gfm.svg?branch=master)](https://travis-ci.org/atom/language-gfm) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/rpub8qjyd8lt7wai/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-gfm/branch/master) | +| [Go](https://github.com/atom/language-go) | [![macOS Build Status](https://travis-ci.org/atom/language-go.svg?branch=master)](https://travis-ci.org/atom/language-go) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/3fxxvv05p4hv92pn/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-go/branch/master) | +| [HTML](https://github.com/atom/language-html) | [![macOS Build Status](https://travis-ci.org/atom/language-html.svg?branch=master)](https://travis-ci.org/atom/language-html) | [![Windows Build status](https://ci.appveyor.com/api/projects/status/t6pk6mmdgcelfg85/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-html/branch/master) | +| [Hyperlink](https://github.com/atom/language-hyperlink) | [![macOS Build Status](https://travis-ci.org/atom/language-hyperlink.svg?branch=master)](https://travis-ci.org/atom/language-hyperlink) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/5tgvhph394r684l8/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-hyperlink/branch/master) | +| [Java](https://github.com/atom/language-java) | [![macOS Build Status](https://travis-ci.org/atom/language-java.svg?branch=master)](https://travis-ci.org/atom/language-java) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/utoftje56n9u5x4h/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-java/branch/master) | +| [JavaScript](https://github.com/atom/language-javascript) | [![macOS Build Status](https://travis-ci.org/atom/language-javascript.svg?branch=master)](https://travis-ci.org/atom/language-javascript) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/ktooccwna96ssiyr/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-javascript-dijf8/branch/master) | +| [JSON](https://github.com/atom/language-json) | [![macOS Build Status](https://travis-ci.org/atom/language-json.svg?branch=master)](https://travis-ci.org/atom/language-json) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/5rx05vhdikk6c4cl/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-json/branch/master) | +| [Less](https://github.com/atom/language-less) | [![macOS Build Status](https://travis-ci.org/atom/language-less.svg?branch=master)](https://travis-ci.org/atom/language-less) | [![Windows Build Sstatus](https://ci.appveyor.com/api/projects/status/aeina4fr4b0i7yay/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-less/branch/master) | +| [Make](https://github.com/atom/language-make) | [![macOS Build Status](https://travis-ci.org/atom/language-make.svg?branch=master)](https://travis-ci.org/atom/language-make) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/vq1aascey21wxjh7/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-make/branch/master) | +| [Mustache](https://github.com/atom/language-mustache) | [![macOS Build Status](https://travis-ci.org/atom/language-mustache.svg?branch=master)](https://travis-ci.org/atom/language-mustache) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/mbxnxaojqp0g7ldv/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-mustache/branch/master) | +| [Objective-C](https://github.com/atom/language-objective-c) | [![macOS Build Status](https://travis-ci.org/atom/language-objective-c.svg?branch=master)](https://travis-ci.org/atom/language-objective-c) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/27j8vfv5u95fjhkw/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-objective-c/branch/master) | +| [Perl](https://github.com/atom/language-perl) | [![macOS Build Status](https://travis-ci.org/atom/language-perl.svg?branch=master)](https://travis-ci.org/atom/language-perl) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/dfs9inkkg40hchf8/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-perl/branch/master) | +| [PHP](https://github.com/atom/language-php) | [![macOS Build Status](https://travis-ci.org/atom/language-php.svg?branch=master)](https://travis-ci.org/atom/language-php) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/y9h45ag4b72726jy/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-php/branch/master) | +| [Python](https://github.com/atom/language-python) | [![macOS Build Status](https://travis-ci.org/atom/language-python.svg?branch=master)](https://travis-ci.org/atom/language-python) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/hmxrb9jttjh41es9/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-python/branch/master) | +| [Ruby](https://github.com/atom/language-ruby) | [![macOS Build Status](https://travis-ci.org/atom/language-ruby.svg?branch=master)](https://travis-ci.org/atom/language-ruby) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/71as182rm1adf2br/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-ruby/branch/master) | +| [Ruby on Rails](https://github.com/atom/language-ruby-on-rails) | [![macOS Build Status](https://travis-ci.org/atom/language-ruby-on-rails.svg?branch=master)](https://travis-ci.org/atom/language-ruby-on-rails) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/5t4pa451fu5e0ghg/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-ruby-on-rails/branch/master) | +| [Sass](https://github.com/atom/language-sass) | [![macOS Build Status](https://travis-ci.org/atom/language-sass.svg?branch=master)](https://travis-ci.org/atom/language-sass) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/g7p16vainm4iuoot/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-sass/branch/master) | +| [ShellScript](https://github.com/atom/language-shellscript) | [![macOS Build Status](https://travis-ci.org/atom/language-shellscript.svg?branch=master)](https://travis-ci.org/atom/language-shellscript) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/p4um3lowgrg8y0ty/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-shellscript/branch/master) | +| [SQL](https://github.com/atom/language-sql) | [![macOS Build Status](https://travis-ci.org/atom/language-sql.svg?branch=master)](https://travis-ci.org/atom/language-sql) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/ji31ouk5ehs4jdu1/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-sql/branch/master) | +| [TODO](https://github.com/atom/language-todo) | [![macOS Build Status](https://travis-ci.org/atom/language-todo.svg?branch=master)](https://travis-ci.org/atom/language-todo) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/gcgb9m7h146lv6qp/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-todo/branch/master) | +| [TOML](https://github.com/atom/language-toml) | [![macOS Build Status](https://travis-ci.org/atom/language-toml.svg?branch=master)](https://travis-ci.org/atom/language-toml) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/kohao3fjyk6xv0sc/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-toml/branch/master) | +| [XML](https://github.com/atom/language-xml) | [![macOS Build Status](https://travis-ci.org/atom/language-xml.svg?branch=master)](https://travis-ci.org/atom/language-xml) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/m5f6rn74a6h3q5uq/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-xml/branch/master) | +| [YAML](https://github/atom/language-yaml) | [![macOS Build Status](https://travis-ci.org/atom/language-yaml.svg?branch=master)](https://travis-ci.org/atom/language-yaml) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/eaa4ql7kipgphc2n/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-yaml/branch/master) | diff --git a/docs/build-instructions/linux.md b/docs/build-instructions/linux.md index fb7e96bef..5f0211c0d 100644 --- a/docs/build-instructions/linux.md +++ b/docs/build-instructions/linux.md @@ -36,7 +36,7 @@ To also install the newly built application, use `--create-debian-package` or `- * Install GNOME headers and other basic prerequisites: ```sh - sudo apt-get install build-essential git libgnome-keyring-dev fakeroot rpm + sudo apt-get install build-essential git libgnome-keyring-dev fakeroot rpm libx11-dev libxkbfile-dev ``` * If `script/build` exits with an error, you may need to install a newer C++ compiler with C++11: @@ -49,13 +49,17 @@ To also install the newly built application, use `--create-debian-package` or `- sudo update-alternatives --config gcc # choose gcc-5 from the list ``` -### Fedora / CentOS / RHEL +### Fedora 22+ -* `sudo dnf --assumeyes install make gcc gcc-c++ glibc-devel git-core libgnome-keyring-devel rpmdevtools` +* `sudo dnf --assumeyes install make gcc gcc-c++ glibc-devel git-core libgnome-keyring-devel rpmdevtools libX11-devel libxkbfile-devel` + +### Fedora 21 / CentOS / RHEL + +* `sudo yum install -y make gcc gcc-c++ glibc-devel git-core libgnome-keyring-devel rpmdevtools` ### Arch -* `sudo pacman -S --needed gconf base-devel git nodejs npm libgnome-keyring python2` +* `sudo pacman -S --needed gconf base-devel git nodejs npm libgnome-keyring python2 libX11-devel libxkbfile-devel` * `export PYTHON=/usr/bin/python2` before building Atom. ### Slackware @@ -64,7 +68,7 @@ To also install the newly built application, use `--create-debian-package` or `- ### openSUSE -* `sudo zypper install nodejs nodejs-devel make gcc gcc-c++ glibc-devel git-core libgnome-keyring-devel rpmdevtools` +* `sudo zypper install nodejs nodejs-devel make gcc gcc-c++ glibc-devel git-core libgnome-keyring-devel rpmdevtools libX11-devel libxkbfile-devel` ## Troubleshooting diff --git a/docs/build-instructions/macos.md b/docs/build-instructions/macos.md index f03d0e385..18169435f 100644 --- a/docs/build-instructions/macos.md +++ b/docs/build-instructions/macos.md @@ -26,4 +26,4 @@ To also install the newly built application, use `script/build --install`. ## Troubleshooting ### macOS build error reports in atom/atom -* Use [this search](https://github.com/atom/atom/search?q=label%3Abuild-error+label%3Aos-x&type=Issues) to get a list of reports about build errors on macOS. +* Use [this search](https://github.com/atom/atom/search?q=label%3Abuild-error+label%3Amac&type=Issues) to get a list of reports about build errors on macOS. diff --git a/docs/build-instructions/windows.md b/docs/build-instructions/windows.md index 09be1b953..5c8c189ef 100644 --- a/docs/build-instructions/windows.md +++ b/docs/build-instructions/windows.md @@ -2,20 +2,21 @@ ## Requirements -* Node.js 4.4.x or later +* Node.js 4.4.x or later (the architecture of node available to the build system will determine whether you build 32-bit or 64-bit Atom) * Python 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` + * The python.exe must be available at `%SystemDrive%\Python27\python.exe`. If it is installed elsewhere create a symbolic link to the directory containing the python.exe using: `mklink /d %SystemDrive%\Python27 D:\elsewhere\Python27` +* 7zip (7z.exe available from the command line) - for creating distribution zip files * Visual Studio, either: * [Visual C++ Build Tools 2015](http://landinghub.visualstudio.com/visual-cpp-build-tools) * [Visual Studio 2013 Update 5](https://www.visualstudio.com/en-us/downloads/download-visual-studio-vs) (Express Edition or better) * [Visual Studio 2015](https://www.visualstudio.com/en-us/downloads/download-visual-studio-vs) (Community Edition or better) - Whichever version you use, ensure that: + Also ensure that: * The default installation folder is chosen so the build tools can find it * If using Visual Studio make sure Visual C++ support is selected/installed * If using Visual C++ Build Tools make sure Windows 8 SDK is selected/installed * A `git` command is in your path - * Set the `GYP_MSVS_VERSION` environment variable to the Visual Studio/Build Tools version (`2013` or `2015`) e.g. ``[Environment]::SetEnvironmentVariable("GYP_MSVS_VERSION", "2015", "User")`` in PowerShell or set it in Windows advanced system settings control panel. + * Set the `GYP_MSVS_VERSION` environment variable to the Visual Studio/Build Tools version (`2013` or `2015`) e.g. ``[Environment]::SetEnvironmentVariable("GYP_MSVS_VERSION", "2015", "User")`` in PowerShell (or set it in Windows advanced system settings). ## Instructions @@ -32,30 +33,39 @@ To also install the newly built application, use `script\build --create-windows- ### `script\build` Options * `--code-sign`: signs the application with the GitHub certificate specified in `$WIN_P12KEY_URL`. -* `--compress-artifacts`: zips the generated application as `out/atom-windows.zip` (requires 7-zip). -* `--create-windows-installer`: creates an `.msi`, an `.exe` and a `.nupkg` installer in the `out/` directory. +* `--compress-artifacts`: zips the generated application as `out\atom-windows.zip` (requires [7-Zip](http://www.7-zip.org)). +* `--create-windows-installer`: creates an `.msi`, an `.exe` and two `.nupkg` packages in the `out` directory. * `--install`: installs the application in `%LOCALAPPDATA%\Atom\app-dev\`. +### Running tests + +In order to run tests from command line you need `apm`, available after you install Atom or after you build from source. If you installed it, run the following commands (assuming `C:\atom` is the root of your Atom repository): + +```bash +cd C:\atom +apm test +``` + +When building Atom from source, the `apm` command is not added to the system path by default. In this case, you can either add it yourself or explicitly list the complete path in previous commands. The default install location is `%LOCALAPPDATA%\Atom\app-dev\resources\cli\`. + +**NOTE**: Please keep in mind that there are still some tests that don't pass on Windows. + ## Troubleshooting ### Common Errors * `node is not recognized` - * If you just installed Node.js, you'll need to restart Command Prompt before the `node` command is available on your Path. + * If you just installed Node.js, you'll need to restart Command Prompt before the `node` command is available on your path. * `msbuild.exe failed with exit code: 1` - * If you installed Visual Studio, ensure you have Visual C++ support installed. Go into Add/Remove Programs, select Visual Studio, press Modify, and then check the Visual C++ box. - * If you installed Visual C++ Build Tools, ensure you have Windows 8 SDK support installed. Go into Add/Remove Programs, select Visual Studio, press Modify and then check the Windows 8 SDK box. + * If using **Visual Studio**, ensure you have the **Visual C++** component installed. Go into Add/Remove Programs, select Visual Studio, press Modify, and then check the Visual C++ box. + * If using **Visual C++ Build Tools**, ensure you have the **Windows 8 SDK** component installed. Go into Add/Remove Programs, select Visual C++ Build Tools, press Modify and then check the Windows 8 SDK box. -* `script\build` stop with no error or warning shortly after displaying the versions of node, npm and Python +* `script\build` stops with no error or warning shortly after displaying the versions of node, npm and Python * Make sure that the path where you have checked out Atom does not include a space. For example, use `C:\atom` instead of `C:\my stuff\atom`. - -* `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). + * Try moving the repository to `C:\atom`. Most likely, the path is too long. See [issue #2200](https://github.com/atom/atom/issues/2200). * `error MSB4025: The project file could not be loaded. Invalid character in the given encoding.` - * 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/) + * 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.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 @@ -67,14 +77,14 @@ To also install the newly built application, use `script\build --create-windows- * See the next item. * `error MSB8020: The build tools for Visual Studio 201? (Platform Toolset = 'v1?0') cannot be found.` - * Try setting the `GYP_MSVS_VERSION` environment variable to 2013 or 2015 depending on what version of Visual Studio/Build Tools is installed and then `script\clean` followed by `script\build` (re-open the Command Prompt if you set the variable using the GUI). + * Try setting the `GYP_MSVS_VERSION` environment variable to **2013** or **2015** depending on what version of Visual Studio/Build Tools is installed and then `script\clean` followed by `script\build` (re-open the Command Prompt if you set the variable using the GUI). * `'node-gyp' is not recognized as an internal or external command, operable program or batch file.` * Try running `npm install -g node-gyp`, and run `script\build` again. * 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. + * Do try the build command one more time as experience shows it often works on second try in many 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.js and Python versions. +* If it hasn't, please open a new issue with your Windows version, architecture (x86 or x64), and a screenshot of your build output, including the Node.js and Python versions. diff --git a/keymaps/base.cson b/keymaps/base.cson index d9941643c..b421392ab 100644 --- a/keymaps/base.cson +++ b/keymaps/base.cson @@ -7,13 +7,13 @@ 'atom-text-editor:not([mini])': # Atom Specific - 'ctrl-C': 'editor:copy-path' + 'ctrl-shift-c': 'editor:copy-path' # Sublime Parity 'tab': 'editor:indent' 'enter': 'editor:newline' 'shift-tab': 'editor:outdent-selected-rows' - 'ctrl-K': 'editor:delete-line' + 'ctrl-shift-k': 'editor:delete-line' '.select-list atom-text-editor[mini]': 'enter': 'core:confirm' @@ -24,7 +24,7 @@ 'atom-text-editor !important, atom-text-editor[mini] !important': 'escape': 'editor:consolidate-selections' -# allow standard input fields to work correctly +# Allow standard input fields to work correctly 'body .native-key-bindings': 'tab': 'core:focus-next' 'shift-tab': 'core:focus-previous' @@ -66,7 +66,7 @@ 'ctrl-shift-right': 'native!' 'ctrl-b': 'native!' 'ctrl-f': 'native!' - 'ctrl-F': 'native!' - 'ctrl-B': 'native!' + 'ctrl-shift-f': 'native!' + 'ctrl-shift-b': 'native!' 'ctrl-h': 'native!' 'ctrl-d': 'native!' diff --git a/keymaps/darwin.cson b/keymaps/darwin.cson index 4ae6d88db..fa942d97c 100644 --- a/keymaps/darwin.cson +++ b/keymaps/darwin.cson @@ -10,10 +10,10 @@ 'ctrl-n': 'core:move-down' 'ctrl-b': 'core:move-left' 'ctrl-f': 'core:move-right' - 'ctrl-P': 'core:select-up' - 'ctrl-N': 'core:select-down' - 'ctrl-F': 'core:select-right' - 'ctrl-B': 'core:select-left' + 'ctrl-shift-p': 'core:select-up' + 'ctrl-shift-n': 'core:select-down' + 'ctrl-shift-f': 'core:select-right' + 'ctrl-shift-b': 'core:select-left' 'ctrl-h': 'core:backspace' 'ctrl-d': 'core:delete' @@ -34,19 +34,19 @@ # Sublime Parity 'cmd-,': 'application:show-settings' - 'cmd-N': 'application:new-window' - 'cmd-W': 'window:close' + 'cmd-shift-n': 'application:new-window' + 'cmd-shift-w': 'window:close' 'cmd-o': 'application:open' - 'cmd-O': 'application:add-project-folder' - 'cmd-T': 'pane:reopen-closed-item' + 'cmd-shift-o': 'application:add-project-folder' + 'cmd-shift-t': 'pane:reopen-closed-item' 'cmd-n': 'application:new-file' 'cmd-s': 'core:save' - 'cmd-S': 'core:save-as' + 'cmd-shift-s': 'core:save-as' 'cmd-alt-s': 'window:save-all' 'cmd-w': 'core:close' 'cmd-ctrl-f': 'window:toggle-full-screen' 'cmd-z': 'core:undo' - 'cmd-Z': 'core:redo' + 'cmd-shift-z': 'core:redo' 'cmd-y': 'core:redo' 'cmd-x': 'core:cut' 'cmd-c': 'core:copy' @@ -116,8 +116,8 @@ 'cmd-backspace': 'editor:delete-to-beginning-of-line' 'cmd-shift-backspace': 'editor:delete-to-beginning-of-line' 'cmd-delete': 'editor:delete-to-end-of-line' - 'ctrl-A': 'editor:select-to-first-character-of-line' - 'ctrl-E': 'editor:select-to-end-of-line' + 'ctrl-shift-a': 'editor:select-to-first-character-of-line' + 'ctrl-shift-e': 'editor:select-to-end-of-line' 'cmd-left': 'editor:move-to-first-character-of-line' 'cmd-right': 'editor:move-to-end-of-screen-line' 'cmd-shift-left': 'editor:select-to-first-character-of-line' @@ -129,19 +129,19 @@ 'ctrl-k': 'editor:cut-to-end-of-line' # Atom Specific - 'ctrl-W': 'editor:select-word' + 'ctrl-shift-w': 'editor:select-word' 'cmd-ctrl-left': 'editor:move-selection-left' 'cmd-ctrl-right': 'editor:move-selection-right' # Emacs 'alt-f': 'editor:move-to-end-of-word' 'alt-ctrl-f': 'editor:move-to-next-subword-boundary' - 'alt-F': 'editor:select-to-end-of-word' - 'alt-ctrl-F': 'editor:select-to-next-subword-boundary' + 'alt-shift-f': 'editor:select-to-end-of-word' + 'alt-ctrl-shift-f': 'editor:select-to-next-subword-boundary' 'alt-b': 'editor:move-to-beginning-of-word' 'alt-ctrl-b': 'editor:move-to-previous-subword-boundary' - 'alt-B': 'editor:select-to-beginning-of-word' - 'alt-ctrl-B': 'editor:select-to-previous-subword-boundary' + 'alt-shift-b': 'editor:select-to-beginning-of-word' + 'alt-ctrl-shift-b': 'editor:select-to-previous-subword-boundary' 'alt-h': 'editor:delete-to-beginning-of-word' 'alt-ctrl-h': 'editor:delete-to-beginning-of-subword' 'alt-d': 'editor:delete-to-end-of-word' @@ -178,8 +178,8 @@ 'ctrl-cmd-down': 'editor:move-line-down' 'cmd-/': 'editor:toggle-line-comments' 'cmd-j': 'editor:join-lines' - 'cmd-D': 'editor:duplicate-lines' - 'cmd-L': 'editor:split-selections-into-lines' + 'cmd-shift-d': 'editor:duplicate-lines' + 'cmd-shift-l': 'editor:split-selections-into-lines' 'ctrl-shift-up': 'editor:add-selection-above' 'ctrl-shift-down': 'editor:add-selection-below' @@ -202,10 +202,10 @@ 'cmd-alt-=': 'pane:increase-size' 'cmd-alt--': 'pane:decrease-size' -# allow standard input fields to work correctly +# Allow standard input fields to work correctly 'body .native-key-bindings': 'cmd-z': 'native!' - 'cmd-Z': 'native!' + 'cmd-shift-z': 'native!' 'cmd-x': 'native!' 'cmd-c': 'native!' 'cmd-v': 'native!' diff --git a/keymaps/linux.cson b/keymaps/linux.cson index 1f78739a9..d6ded1f90 100644 --- a/keymaps/linux.cson +++ b/keymaps/linux.cson @@ -6,11 +6,11 @@ 'down': 'core:move-down' 'left': 'core:move-left' 'right': 'core:move-right' - 'ctrl-alt-r': 'window:reload' + 'ctrl-shift-f5': 'window:reload' 'ctrl-shift-i': 'window:toggle-dev-tools' - 'ctrl-alt-p': 'window:run-package-specs' + 'ctrl-shift-y': 'window:run-package-specs' 'ctrl-shift-o': 'application:open-folder' - 'ctrl-alt-o': 'application:add-project-folder' + 'ctrl-shift-a': 'application:add-project-folder' 'ctrl-shift-pageup': 'pane:move-item-left' 'ctrl-shift-pagedown': 'pane:move-item-right' 'f11': 'window:toggle-full-screen' @@ -19,14 +19,14 @@ # Sublime Parity 'ctrl-,': 'application:show-settings' - 'ctrl-N': 'application:new-window' - 'ctrl-W': 'window:close' + 'ctrl-shift-n': 'application:new-window' + 'ctrl-shift-w': 'window:close' 'ctrl-o': 'application:open-file' 'ctrl-q': 'application:quit' - 'ctrl-T': 'pane:reopen-closed-item' + 'ctrl-shift-t': 'pane:reopen-closed-item' 'ctrl-n': 'application:new-file' 'ctrl-s': 'core:save' - 'ctrl-S': 'core:save-as' + 'ctrl-shift-s': 'core:save-as' 'ctrl-f4': 'core:close' 'ctrl-w': 'core:close' 'ctrl-z': 'core:undo' @@ -70,12 +70,12 @@ 'ctrl-k left': 'pane:split-left-and-copy-active-item' # Atom Specific 'ctrl-k right': 'pane:split-right-and-copy-active-item' # Atom Specific 'ctrl-k ctrl-w': 'pane:close' # Atom Specific - 'ctrl-k alt-ctrl-w': 'pane:close-other-items' # Atom Specific + 'ctrl-k ctrl-alt-w': 'pane:close-other-items' # Atom Specific 'ctrl-k ctrl-p': 'window:focus-previous-pane' 'ctrl-k ctrl-n': 'window:focus-next-pane' - 'ctrl-k ctrl-up': 'window:focus-pane-above' - 'ctrl-k ctrl-down': 'window:focus-pane-below' - 'ctrl-k ctrl-left': 'window:focus-pane-on-left' + 'ctrl-k ctrl-up': 'window:focus-pane-above' + 'ctrl-k ctrl-down': 'window:focus-pane-below' + 'ctrl-k ctrl-left': 'window:focus-pane-on-left' 'ctrl-k ctrl-right': 'window:focus-pane-on-right' 'alt-1': 'pane:show-item-1' 'alt-2': 'pane:show-item-2' @@ -108,16 +108,14 @@ # Sublime Parity 'ctrl-a': 'core:select-all' - 'ctrl-alt-shift-p': 'editor:log-cursor-scope' 'ctrl-k ctrl-u': 'editor:upper-case' 'ctrl-k ctrl-l': 'editor:lower-case' 'ctrl-l': 'editor:select-line' 'atom-workspace atom-text-editor:not([mini])': # Atom specific - 'alt-ctrl-z': 'editor:checkout-head-revision' 'ctrl-<': 'editor:scroll-to-cursor' - 'alt-ctrl-f': 'editor:fold-selection' + 'ctrl-alt-shift-[': 'editor:fold-selection' # Sublime Parity 'ctrl-enter': 'editor:newline-below' @@ -128,7 +126,7 @@ 'ctrl-down': 'editor:move-line-down' 'ctrl-/': 'editor:toggle-line-comments' 'ctrl-j': 'editor:join-lines' - 'ctrl-D': 'editor:duplicate-lines' + 'ctrl-shift-d': 'editor:duplicate-lines' 'alt-shift-up': 'editor:add-selection-above' 'alt-shift-down': 'editor:add-selection-below' @@ -151,10 +149,10 @@ 'ctrl-alt-=': 'pane:increase-size' 'ctrl-alt--': 'pane:decrease-size' -# allow standard input fields to work correctly +# Allow standard input fields to work correctly 'body .native-key-bindings': 'ctrl-z': 'native!' - 'ctrl-Z': 'native!' + 'ctrl-shift-z': 'native!' 'ctrl-x': 'native!' 'ctrl-c': 'native!' 'ctrl-v': 'native!' diff --git a/keymaps/win32.cson b/keymaps/win32.cson index d43c124d4..14f5a4283 100644 --- a/keymaps/win32.cson +++ b/keymaps/win32.cson @@ -12,11 +12,11 @@ 'ctrl-down': 'core:move-down' 'left': 'core:move-left' 'right': 'core:move-right' - 'ctrl-alt-r': 'window:reload' + 'ctrl-shift-f5': 'window:reload' 'ctrl-shift-i': 'window:toggle-dev-tools' - 'ctrl-alt-p': 'window:run-package-specs' + 'ctrl-shift-y': 'window:run-package-specs' 'ctrl-shift-o': 'application:open-folder' - 'ctrl-alt-o': 'application:add-project-folder' + 'ctrl-shift-a': 'application:add-project-folder' 'ctrl-shift-left': 'pane:move-item-left' 'ctrl-shift-right': 'pane:move-item-right' 'f11': 'window:toggle-full-screen' @@ -25,13 +25,13 @@ # Sublime Parity 'ctrl-,': 'application:show-settings' - 'ctrl-N': 'application:new-window' - 'ctrl-W': 'window:close' + 'ctrl-shift-n': 'application:new-window' + 'ctrl-shift-w': 'window:close' 'ctrl-o': 'application:open-file' - 'ctrl-T': 'pane:reopen-closed-item' + 'ctrl-shift-t': 'pane:reopen-closed-item' 'ctrl-n': 'application:new-file' 'ctrl-s': 'core:save' - 'ctrl-S': 'core:save-as' + 'ctrl-shift-s': 'core:save-as' 'ctrl-f4': 'core:close' 'ctrl-w': 'core:close' 'ctrl-z': 'core:undo' @@ -75,12 +75,12 @@ 'ctrl-k left': 'pane:split-left-and-copy-active-item' # Atom Specific 'ctrl-k right': 'pane:split-right-and-copy-active-item' # Atom Specific 'ctrl-k ctrl-w': 'pane:close' # Atom Specific - 'ctrl-k alt-ctrl-w': 'pane:close-other-items' # Atom Specific + 'ctrl-k ctrl-alt-w': 'pane:close-other-items' # Atom Specific 'ctrl-k ctrl-p': 'window:focus-previous-pane' 'ctrl-k ctrl-n': 'window:focus-next-pane' - 'ctrl-k ctrl-up': 'window:focus-pane-above' - 'ctrl-k ctrl-down': 'window:focus-pane-below' - 'ctrl-k ctrl-left': 'window:focus-pane-on-left' + 'ctrl-k ctrl-up': 'window:focus-pane-above' + 'ctrl-k ctrl-down': 'window:focus-pane-below' + 'ctrl-k ctrl-left': 'window:focus-pane-on-left' 'ctrl-k ctrl-right': 'window:focus-pane-on-right' 'alt-1': 'pane:show-item-1' 'alt-2': 'pane:show-item-2' @@ -113,16 +113,14 @@ # Sublime Parity 'ctrl-a': 'core:select-all' - 'ctrl-alt-shift-p': 'editor:log-cursor-scope' 'ctrl-k ctrl-u': 'editor:upper-case' 'ctrl-k ctrl-l': 'editor:lower-case' 'ctrl-l': 'editor:select-line' 'atom-workspace atom-text-editor:not([mini])': # Atom specific - 'alt-ctrl-z': 'editor:checkout-head-revision' 'ctrl-<': 'editor:scroll-to-cursor' - 'alt-ctrl-f': 'editor:fold-selection' + 'ctrl-alt-shift-[': 'editor:fold-selection' # Sublime Parity 'ctrl-enter': 'editor:newline-below' @@ -133,7 +131,7 @@ 'ctrl-down': 'editor:move-line-down' 'ctrl-/': 'editor:toggle-line-comments' 'ctrl-j': 'editor:join-lines' - 'ctrl-D': 'editor:duplicate-lines' + 'ctrl-shift-d': 'editor:duplicate-lines' 'ctrl-alt-[': 'editor:fold-current-row' 'ctrl-alt-]': 'editor:unfold-current-row' @@ -154,10 +152,10 @@ 'ctrl-alt-=': 'pane:increase-size' 'ctrl-alt--': 'pane:decrease-size' -# allow standard input fields to work correctly +# Allow standard input fields to work correctly 'body .native-key-bindings': 'ctrl-z': 'native!' - 'ctrl-Z': 'native!' + 'ctrl-shift-z': 'native!' 'ctrl-x': 'native!' 'ctrl-c': 'native!' 'ctrl-v': 'native!' diff --git a/menus/darwin.cson b/menus/darwin.cson index b967220c0..055cd2405 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -36,6 +36,13 @@ { label: 'New File', command: 'application:new-file' } { label: 'Open…', command: 'application:open' } { label: 'Add Project Folder…', command: 'application:add-project-folder' } + { + label: 'Reopen Project', + submenu: [ + { label: 'Clear Project History', command: 'application:clear-project-history' } + { type: 'separator' } + ] + } { label: 'Reopen Last Item', command: 'pane:reopen-closed-item' } { type: 'separator' } { label: 'Save', command: 'core:save' } @@ -101,9 +108,9 @@ submenu: [ { label: 'Fold', command: 'editor:fold-current-row' } { label: 'Unfold', command: 'editor:unfold-current-row' } + { label: 'Fold All', command: 'editor:fold-all' } { label: 'Unfold All', command: 'editor:unfold-all' } { type: 'separator' } - { label: 'Fold All', command: 'editor:fold-all' } { label: 'Fold Level 1', command: 'editor:fold-at-indent-level-1' } { label: 'Fold Level 2', command: 'editor:fold-at-indent-level-2' } { label: 'Fold Level 3', command: 'editor:fold-at-indent-level-3' } diff --git a/menus/linux.cson b/menus/linux.cson index 3ec2780e1..94fb90a30 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -7,6 +7,13 @@ { label: '&Open File…', command: 'application:open-file' } { label: 'Open Folder…', command: 'application:open-folder' } { label: 'Add Project Folder…', command: 'application:add-project-folder' } + { + label: 'Reopen Project', + submenu: [ + { label: 'Clear Project History', command: 'application:clear-project-history' } + { type: 'separator' } + ] + } { label: 'Reopen Last &Item', command: 'pane:reopen-closed-item' } { type: 'separator' } { label: '&Save', command: 'core:save' } @@ -74,9 +81,9 @@ submenu: [ { label: '&Fold', command: 'editor:fold-current-row' } { label: '&Unfold', command: 'editor:unfold-current-row' } + { label: 'Fol&d All', command: 'editor:fold-all' } { label: 'Unfold &All', command: 'editor:unfold-all' } { type: 'separator' } - { label: 'Fol&d All', command: 'editor:fold-all' } { label: 'Fold Level 1', command: 'editor:fold-at-indent-level-1' } { label: 'Fold Level 2', command: 'editor:fold-at-indent-level-2' } { label: 'Fold Level 3', command: 'editor:fold-at-indent-level-3' } diff --git a/menus/win32.cson b/menus/win32.cson index d6b707009..70bb1487d 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -7,6 +7,13 @@ { label: '&Open File…', command: 'application:open-file' } { label: 'Open Folder…', command: 'application:open-folder' } { label: 'Add Project Folder…', command: 'application:add-project-folder' } + { + label: 'Reopen Project', + submenu: [ + { label: 'Clear Project History', command: 'application:clear-project-history' } + { type: 'separator' } + ] + } { label: 'Reopen Last &Item', command: 'pane:reopen-closed-item' } { type: 'separator' } { label: 'Se&ttings', command: 'application:show-settings' } @@ -82,9 +89,9 @@ submenu: [ { label: '&Fold', command: 'editor:fold-current-row' } { label: '&Unfold', command: 'editor:unfold-current-row' } + { label: 'Fol&d All', command: 'editor:fold-all' } { label: 'Unfold &All', command: 'editor:unfold-all' } { type: 'separator' } - { label: 'Fol&d All', command: 'editor:fold-all' } { label: 'Fold Level 1', command: 'editor:fold-at-indent-level-1' } { label: 'Fold Level 2', command: 'editor:fold-at-indent-level-2' } { label: 'Fold Level 3', command: 'editor:fold-at-indent-level-3' } diff --git a/package.json b/package.json index 06daba2ec..e7ff9d1c7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "1.13.0-dev", + "version": "1.15.0-dev", "description": "A hackable text editor for the 21st Century.", "main": "./src/main-process/main.js", "repository": { @@ -12,10 +12,11 @@ "url": "https://github.com/atom/atom/issues" }, "license": "MIT", - "electronVersion": "1.3.6", + "electronVersion": "1.3.13", "dependencies": { "async": "0.2.6", - "atom-keymap": "7.0.6", + "atom-keymap": "7.1.18", + "atom-select-list": "0.0.6", "atom-ui": "0.4.1", "babel-core": "5.8.38", "cached-run-in-this-context": "0.4.1", @@ -32,7 +33,7 @@ "fs-plus": "2.9.2", "fstream": "0.1.24", "fuzzaldrin": "^2.1", - "git-utils": "^4.1.2", + "git-utils": "4.1.2", "glob": "^7.1.1", "grim": "1.5.0", "jasmine-json": "~0.0", @@ -42,18 +43,20 @@ "less-cache": "0.23", "line-top-index": "0.2.0", "marked": "^0.3.6", + "minimatch": "^3.0.3", "mocha": "2.5.1", + "mock-spawn": "^0.2.6", "normalize-package-data": "^2.0.0", "nslog": "^3", "oniguruma": "6.1.0", - "pathwatcher": "~6.5", + "pathwatcher": "6.8.0", "postcss": "5.2.4", "postcss-selector-parser": "2.2.1", "property-accessors": "^1.1.3", "random-words": "0.0.1", "resolve": "^1.1.6", "runas": "^3.1", - "scandal": "^2.2.1", + "scandal": "2.2.2", "scoped-property-store": "^0.17.0", "scrollbar-style": "^3.2", "season": "^5.4.1", @@ -62,7 +65,7 @@ "sinon": "1.17.4", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "9.4.0", + "text-buffer": "10.2.3", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", @@ -73,91 +76,91 @@ "atom-dark-ui": "0.53.0", "atom-light-syntax": "0.29.0", "atom-light-ui": "0.46.0", - "base16-tomorrow-dark-theme": "1.4.0", - "base16-tomorrow-light-theme": "1.4.0", - "one-dark-ui": "1.7.0", - "one-light-ui": "1.7.0", - "one-dark-syntax": "1.6.0", - "one-light-syntax": "1.6.0", + "base16-tomorrow-dark-theme": "1.5.0", + "base16-tomorrow-light-theme": "1.5.0", + "one-dark-ui": "1.9.1", + "one-light-ui": "1.9.1", + "one-dark-syntax": "1.7.1", + "one-light-syntax": "1.7.1", "solarized-dark-syntax": "1.1.1", "solarized-light-syntax": "1.1.1", - "about": "1.7.0", - "archive-view": "0.62.0", + "about": "1.7.2", + "archive-view": "0.62.2", "autocomplete-atom-api": "0.10.0", - "autocomplete-css": "0.14.1", + "autocomplete-css": "0.14.2", "autocomplete-html": "0.7.2", - "autocomplete-plus": "2.33.1", + "autocomplete-plus": "2.34.2", "autocomplete-snippets": "1.11.0", - "autoflow": "0.27.0", - "autosave": "0.23.2", + "autoflow": "0.29.0", + "autosave": "0.24.0", "background-tips": "0.26.1", - "bookmarks": "0.43.1", - "bracket-matcher": "0.82.2", - "command-palette": "0.39.1", + "bookmarks": "0.43.4", + "bracket-matcher": "0.85.2", + "command-palette": "0.39.2", "deprecation-cop": "0.55.1", "dev-live-reload": "0.47.0", "encoding-selector": "0.22.0", - "exception-reporting": "0.40.0", - "find-and-replace": "0.203.0", - "fuzzy-finder": "1.4.0", + "exception-reporting": "0.40.1", + "find-and-replace": "0.206.0", + "fuzzy-finder": "1.4.1", "git-diff": "1.2.0", - "go-to-line": "0.31.0", + "go-to-line": "0.31.2", "grammar-selector": "0.48.2", "image-view": "0.60.0", "incompatible-packages": "0.26.1", "keybinding-resolver": "0.35.0", - "line-ending-selector": "0.5.0", + "line-ending-selector": "0.5.1", "link": "0.31.2", - "markdown-preview": "0.159.1", - "metrics": "1.0.0", - "notifications": "0.65.1", + "markdown-preview": "0.159.2", + "metrics": "1.1.2", + "notifications": "0.66.1", "open-on-github": "1.2.1", "package-generator": "1.0.2", - "settings-view": "0.244.0", - "snippets": "1.0.3", - "spell-check": "0.68.5", - "status-bar": "1.6.0", - "styleguide": "0.47.3", - "symbols-view": "0.113.1", - "tabs": "0.103.0", - "timecop": "0.33.2", - "tree-view": "0.210.0", + "settings-view": "0.246.0", + "snippets": "1.0.5", + "spell-check": "0.70.2", + "status-bar": "1.7.0", + "styleguide": "0.48.0", + "symbols-view": "0.114.0", + "tabs": "0.104.1", + "timecop": "0.34.0", + "tree-view": "0.213.1", "update-package-dependencies": "0.10.0", - "welcome": "0.35.1", - "whitespace": "0.35.0", + "welcome": "0.36.0", + "whitespace": "0.36.1", "wrap-guide": "0.39.0", - "language-c": "0.54.0", + "language-c": "0.54.1", "language-clojure": "0.22.1", - "language-coffee-script": "0.48.0", - "language-csharp": "0.12.1", - "language-css": "0.40.1", + "language-coffee-script": "0.48.2", + "language-csharp": "0.14.0", + "language-css": "0.42.0", "language-gfm": "0.88.0", - "language-git": "0.15.0", - "language-go": "0.43.0", - "language-html": "0.46.1", + "language-git": "0.19.0", + "language-go": "0.43.1", + "language-html": "0.47.1", "language-hyperlink": "0.16.1", - "language-java": "0.24.0", - "language-javascript": "0.122.0", + "language-java": "0.25.0", + "language-javascript": "0.125.1", "language-json": "0.18.3", - "language-less": "0.29.6", - "language-make": "0.22.2", - "language-mustache": "0.13.0", + "language-less": "0.30.1", + "language-make": "0.22.3", + "language-mustache": "0.13.1", "language-objective-c": "0.15.1", "language-perl": "0.37.0", "language-php": "0.37.3", - "language-property-list": "0.8.0", + "language-property-list": "0.9.0", "language-python": "0.45.1", - "language-ruby": "0.70.2", + "language-ruby": "0.70.4", "language-ruby-on-rails": "0.25.1", - "language-sass": "0.57.0", - "language-shellscript": "0.23.0", + "language-sass": "0.57.1", + "language-shellscript": "0.25.0", "language-source": "0.9.0", - "language-sql": "0.25.0", + "language-sql": "0.25.2", "language-text": "0.7.1", "language-todo": "0.29.1", "language-toml": "0.18.1", - "language-xml": "0.34.12", - "language-yaml": "0.27.1" + "language-xml": "0.34.15", + "language-yaml": "0.27.2" }, "private": true, "scripts": { diff --git a/resources/linux/redhat/atom.spec.in b/resources/linux/redhat/atom.spec.in index 0ee120b35..bc2397126 100644 --- a/resources/linux/redhat/atom.spec.in +++ b/resources/linux/redhat/atom.spec.in @@ -7,7 +7,7 @@ URL: https://atom.io/ AutoReqProv: no # Avoid libchromiumcontent.so missing dependency Prefix: <%= installDir %> -Requires: lsb-core-noarch +Requires: lsb-core-noarch, libXss.so.1 %description <%= description %> diff --git a/resources/win/apm.cmd b/resources/win/apm.cmd index 510168983..371172c43 100644 --- a/resources/win/apm.cmd +++ b/resources/win/apm.cmd @@ -1,3 +1,3 @@ @echo off -"%~dp0\..\app\apm\bin\node.exe" "%~dp0\..\app\apm\lib\cli.js" %* +"%~dp0\..\app\apm\bin\apm.cmd" %* diff --git a/resources/win/apm.sh b/resources/win/apm.sh index b50a70a82..99ccfec69 100644 --- a/resources/win/apm.sh +++ b/resources/win/apm.sh @@ -1,4 +1,3 @@ #!/bin/sh -directory=$(dirname "$0") -"$directory/../app/apm/bin/node.exe" "$directory/../app/apm/lib/cli.js" "$@" +"$(dirname "$0")/../app/apm/apm.sh" "$@" diff --git a/resources/win/folder.ico b/resources/win/folder.ico new file mode 100644 index 000000000..ad1d9fc95 Binary files /dev/null and b/resources/win/folder.ico differ diff --git a/script/build b/script/build index 680666b05..bee0c9514 100755 --- a/script/build +++ b/script/build @@ -23,6 +23,7 @@ const argv = yargs .wrap(yargs.terminalWidth()) .argv +const checkChromedriverVersion = require('./lib/check-chromedriver-version') const cleanOutputDirectory = require('./lib/clean-output-directory') const codeSignOnMac = require('./lib/code-sign-on-mac') const compressArtifacts = require('./lib/compress-artifacts') @@ -30,7 +31,6 @@ const copyAssets = require('./lib/copy-assets') const createDebianPackage = require('./lib/create-debian-package') const createRpmPackage = require('./lib/create-rpm-package') const createWindowsInstaller = require('./lib/create-windows-installer') -const downloadChromedriver = require('./lib/download-chromedriver') const dumpSymbols = require('./lib/dump-symbols') const generateAPIDocs = require('./lib/generate-api-docs') const generateMetadata = require('./lib/generate-metadata') @@ -48,6 +48,7 @@ process.on('unhandledRejection', function (e) { process.exit(1) }) +checkChromedriverVersion() cleanOutputDirectory() copyAssets() transpileBabelPaths() @@ -58,7 +59,6 @@ generateModuleCache() prebuildLessCache() generateMetadata() generateAPIDocs() -downloadChromedriver() dumpSymbols() .then(packageApplication) .then(packagedAppPath => { diff --git a/script/deprecated-packages.json b/script/deprecated-packages.json index 08f4d1186..12638967e 100644 --- a/script/deprecated-packages.json +++ b/script/deprecated-packages.json @@ -866,6 +866,10 @@ "hasDeprecations": true, "latestHasDeprecations": true }, + "language-nlf": { + "hasAlternative": true, + "alternative": "language-nsis" + }, "language-rspec": { "version": "<=0.2.1", "hasDeprecations": true, diff --git a/script/lib/check-chromedriver-version.js b/script/lib/check-chromedriver-version.js new file mode 100644 index 000000000..90bc220e5 --- /dev/null +++ b/script/lib/check-chromedriver-version.js @@ -0,0 +1,22 @@ +'use strict' + +const buildMetadata = require('../package.json') +const CONFIG = require('../config') +const semver = require('semver') + +module.exports = function () { + // Chromedriver should be specified as ~x.y where x and y match Electron major/minor + const chromedriverVer = buildMetadata.dependencies['electron-chromedriver'] + + // Always use tilde on electron-chromedriver so that it can pick up the best patch vesion + if (!chromedriverVer.startsWith('~')) { + throw new Error(`electron-chromedriver version in script/package.json should start with a tilde to match latest patch version.`) + } + + const electronVer = CONFIG.appMetadata.electronVersion + if (!semver.satisfies(electronVer, chromedriverVer)) { + throw new Error(`electron-chromedriver ${chromedriverVer} incompatible with electron ${electronVer}.\n` + + 'Did you upgrade electron in package.json and forget to upgrade electron-chromedriver in ' + + `script/package.json to '~${semver.major(electronVer)}.${semver.minor(electronVer)}' ?`) + } +} diff --git a/script/lib/compress-artifacts.js b/script/lib/compress-artifacts.js index 5287b64a7..54a637162 100644 --- a/script/lib/compress-artifacts.js +++ b/script/lib/compress-artifacts.js @@ -7,23 +7,7 @@ const spawnSync = require('./spawn-sync') const CONFIG = require('../config') module.exports = function (packagedAppPath) { - let appArchiveName - if (process.platform === 'darwin') { - appArchiveName = 'atom-mac.zip' - } else if (process.platform === 'win32') { - appArchiveName = 'atom-windows.zip' - } else { - let arch - if (process.arch === 'ia32') { - arch = 'i386' - } else if (process.arch === 'x64') { - arch = 'amd64' - } else { - arch = process.arch - } - appArchiveName = `atom-${arch}.tar.gz` - } - const appArchivePath = path.join(CONFIG.buildOutputPath, appArchiveName) + const appArchivePath = path.join(CONFIG.buildOutputPath, getArchiveName()) compress(packagedAppPath, appArchivePath) if (process.platform === 'darwin') { @@ -32,6 +16,22 @@ module.exports = function (packagedAppPath) { } } +function getArchiveName () { + switch (process.platform) { + case 'darwin': return 'atom-mac.zip' + case 'win32': return `atom-windows.zip` + default: return `atom-${getLinuxArchiveArch()}.tar.gz` + } +} + +function getLinuxArchiveArch () { + switch (process.arch) { + case 'ia32': return 'i386' + case 'x64' : return 'amd64' + default: return process.arch + } +} + function compress (inputDirPath, outputArchivePath) { if (fs.existsSync(outputArchivePath)) { console.log(`Deleting "${outputArchivePath}"`) diff --git a/script/lib/create-windows-installer.js b/script/lib/create-windows-installer.js index fb22dd085..4b9e0f3a3 100644 --- a/script/lib/create-windows-installer.js +++ b/script/lib/create-windows-installer.js @@ -11,24 +11,31 @@ const spawnSync = require('./spawn-sync') const CONFIG = require('../config') module.exports = function (packagedAppPath, codeSign) { + const archSuffix = process.arch === 'ia32' ? '' : '-' + process.arch const options = { appDirectory: packagedAppPath, authors: 'GitHub Inc.', iconUrl: `https://raw.githubusercontent.com/atom/atom/master/resources/app-icons/${CONFIG.channel}/atom.ico`, loadingGif: path.join(CONFIG.repositoryRootPath, 'resources', 'win', 'loading.gif'), outputDirectory: CONFIG.buildOutputPath, - remoteReleases: `https://atom.io/api/updates?version=${CONFIG.appMetadata.version}`, + remoteReleases: `https://atom.io/api/updates${archSuffix}`, setupIcon: path.join(CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'atom.ico') } const certPath = path.join(os.tmpdir(), 'win.p12') - const signing = codeSign && process.env.WIN_P12KEY_URL + const signing = codeSign && process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL + if (signing) { - downloadFileFromGithub(process.env.WIN_P12KEY_URL, certPath) - options.certificateFile = certPath - options.certificatePassword = process.env.WIN_P12KEY_PASSWORD + downloadFileFromGithub(process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL, certPath) + var signParams = [] + signParams.push(`/f ${certPath}`) // Signing cert file + signParams.push(`/p ${process.env.ATOM_WIN_CODE_SIGNING_CERT_PASSWORD}`) // Signing cert password + signParams.push('/fd sha256') // File digest algorithm + signParams.push('/tr http://timestamp.digicert.com') // Time stamp server + signParams.push('/td sha256') // Times stamp algorithm + options.signWithParams = signParams.join(' ') } else { - console.log('Skipping code-signing. Specify the --code-sign option and provide a WIN_P12KEY_URL environment variable to perform code-signing'.gray) + console.log('Skipping code-signing. Specify the --code-sign option and provide a ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL environment variable to perform code-signing'.gray) } const cleanUp = function () { @@ -50,10 +57,10 @@ module.exports = function (packagedAppPath, codeSign) { if (signing) { for (let nupkgPath of glob.sync(`${CONFIG.buildOutputPath}/*-full.nupkg`)) { if (nupkgPath.includes(CONFIG.appMetadata.version)) { + nupkgPath = path.resolve(nupkgPath) // Switch from forward-slash notation console.log(`Extracting signed executables from ${nupkgPath} for use in portable zip`) - var atomOutPath = path.join(path.dirname(packagedAppPath), 'Atom') - spawnSync('7z.exe', ['e', nupkgPath, 'lib\\net45\\*.exe', '-aoa'], {cwd: atomOutPath}) - spawnSync(process.env.COMSPEC, ['/c', `move /y ${path.join(atomOutPath, 'squirrel.exe')} ${path.join(atomOutPath, 'update.exe')}`]) + spawnSync('7z.exe', ['e', nupkgPath, 'lib\\net45\\*.exe', '-aoa', `-o${packagedAppPath}`]) + spawnSync(process.env.COMSPEC, ['/c', 'move', '/y', path.join(packagedAppPath, 'squirrel.exe'), path.join(packagedAppPath, 'update.exe')]) return } } diff --git a/script/lib/dependencies-fingerprint.js b/script/lib/dependencies-fingerprint.js index 650efd99b..52b5d170a 100644 --- a/script/lib/dependencies-fingerprint.js +++ b/script/lib/dependencies-fingerprint.js @@ -22,7 +22,7 @@ module.exports = { // Include the electron minor version in the fingerprint since that changing requires a re-install const electronVersion = CONFIG.appMetadata.electronVersion.replace(/\.\d+$/, '') const apmVersion = CONFIG.apmMetadata.dependencies['atom-package-manager'] - const body = electronVersion + apmVersion + process.platform + process.version + const body = electronVersion + apmVersion + process.platform + process.version + process.arch return crypto.createHash('sha1').update(body).digest('hex') } } diff --git a/script/lib/download-chromedriver.js b/script/lib/download-chromedriver.js deleted file mode 100644 index ec52823f9..000000000 --- a/script/lib/download-chromedriver.js +++ /dev/null @@ -1,56 +0,0 @@ -'use strict' - -const assert = require('assert') -const downloadFileFromGithub = require('./download-file-from-github') -const fs = require('fs-extra') -const path = require('path') -const semver = require('semver') -const spawnSync = require('./spawn-sync') -const syncRequest = require('sync-request') - -const CONFIG = require('../config') - -module.exports = function () { - if (process.platform === 'darwin') { - // Chromedriver is only distributed with the first patch release for any given - // major and minor version of electron. - const electronVersion = semver.parse(CONFIG.appMetadata.electronVersion) - const electronVersionWithChromedriver = `${electronVersion.major}.${electronVersion.minor}.0` - const electronAssets = getElectronAssetsForVersion(electronVersionWithChromedriver) - const chromedriverAssets = electronAssets.filter(e => /chromedriver.*darwin-x64/.test(e.name)) - assert(chromedriverAssets.length === 1, 'Found more than one chrome driver asset to download!') - const chromedriverAsset = chromedriverAssets[0] - - const chromedriverZipPath = path.join(CONFIG.electronDownloadPath, `electron-${electronVersionWithChromedriver}-${chromedriverAsset.name}`) - if (!fs.existsSync(chromedriverZipPath)) { - downloadFileFromGithub(chromedriverAsset.url, chromedriverZipPath) - } - - const chromedriverDirPath = path.join(CONFIG.electronDownloadPath, 'chromedriver') - unzipPath(chromedriverZipPath, chromedriverDirPath) - } else { - console.log('Skipping Chromedriver download because it is used only on macOS'.gray) - } -} - -function getElectronAssetsForVersion (version) { - const releaseURL = `https://api.github.com/repos/electron/electron/releases/tags/v${version}` - const response = syncRequest('GET', releaseURL, {'headers': {'User-Agent': 'Atom Build'}}) - - if (response.statusCode === 200) { - const release = JSON.parse(response.body) - return release.assets.map(a => { return {name: a.name, url: a.browser_download_url} }) - } else { - throw new Error(`Error getting assets for ${releaseURL}. HTTP Status ${response.statusCode}.`) - } -} - -function unzipPath (inputPath, outputPath) { - if (fs.existsSync(outputPath)) { - console.log(`Removing "${outputPath}"`) - fs.removeSync(outputPath) - } - - console.log(`Unzipping "${inputPath}" to "${outputPath}"`) - spawnSync('unzip', [inputPath, '-d', outputPath]) -} diff --git a/script/lib/install-atom-dependencies.js b/script/lib/install-atom-dependencies.js index 5a66132f7..5c395a29b 100644 --- a/script/lib/install-atom-dependencies.js +++ b/script/lib/install-atom-dependencies.js @@ -12,10 +12,6 @@ module.exports = function () { // Set our target (Electron) version so that node-pre-gyp can download the // proper binaries. installEnv.npm_config_target = CONFIG.appMetadata.electronVersion; - // Force 32-bit modules on Windows. (Ref.: https://github.com/atom/atom/issues/10450) - if (process.platform === 'win32') { - installEnv.npm_config_target_arch = 'ia32' - } childProcess.execFileSync( CONFIG.getApmBinPath(), ['--loglevel=error', 'install'], diff --git a/script/lib/package-application.js b/script/lib/package-application.js index f48a5af2b..1e63b8dc0 100644 --- a/script/lib/package-application.js +++ b/script/lib/package-application.js @@ -18,7 +18,7 @@ module.exports = function () { 'app-bundle-id': 'com.github.atom', 'app-copyright': `Copyright © 2014-${(new Date()).getFullYear()} GitHub, Inc. All rights reserved.`, 'app-version': CONFIG.appMetadata.version, - 'arch': process.platform === 'win32' ? 'ia32' : 'x64', + 'arch': process.platform === 'darwin' ? 'x64' : process.arch, // OS X is 64-bit only 'asar': {unpack: buildAsarUnpackGlobExpression()}, 'build-version': CONFIG.appMetadata.version, 'download': {cache: CONFIG.electronDownloadPath}, @@ -73,7 +73,7 @@ function copyNonASARResources (packagedAppPath, bundledResourcesPath) { } else if (process.platform === 'linux') { fs.copySync(path.join(CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'png', '1024.png'), path.join(packagedAppPath, 'atom.png')) } else if (process.platform === 'win32') { - [ 'atom.cmd', 'atom.sh', 'atom.js', 'apm.cmd', 'apm.sh', 'file.ico' ] + [ 'atom.cmd', 'atom.sh', 'atom.js', 'apm.cmd', 'apm.sh', 'file.ico', 'folder.ico' ] .forEach(file => fs.copySync(path.join('resources', 'win', file), path.join(bundledResourcesPath, 'cli', file))) } @@ -169,6 +169,9 @@ function renamePackagedAppDir (packageOutputDirPath) { } else { const appName = CONFIG.channel === 'beta' ? 'Atom Beta' : 'Atom' packagedAppPath = path.join(CONFIG.buildOutputPath, appName) + if (process.platform === 'win32' && process.arch !== 'ia32') { + packagedAppPath += ` ${process.arch}` + } if (fs.existsSync(packagedAppPath)) fs.removeSync(packagedAppPath) fs.renameSync(packageOutputDirPath, packagedAppPath) } diff --git a/script/lib/verify-machine-requirements.js b/script/lib/verify-machine-requirements.js index 6ba0044ef..5cf594e6e 100644 --- a/script/lib/verify-machine-requirements.js +++ b/script/lib/verify-machine-requirements.js @@ -17,8 +17,10 @@ module.exports = function () { function verifyNode () { const fullVersion = process.versions.node const majorVersion = fullVersion.split('.')[0] - if (majorVersion >= 4) { + if (majorVersion >= 4 && majorVersion < 7) { console.log(`Node:\tv${fullVersion}`) + } else if (majorVersion >= 7) { + throw new Error(`Atom does not build properly on node v7+. node v${fullVersion} is installed.`) } else { throw new Error(`node v4+ is required to build Atom. node v${fullVersion} is installed.`) } diff --git a/script/package.json b/script/package.json index 1f4cf782f..1284fd83c 100644 --- a/script/package.json +++ b/script/package.json @@ -8,11 +8,12 @@ "colors": "1.1.2", "csslint": "1.0.2", "donna": "1.0.13", + "electron-chromedriver": "~1.3", "electron-packager": "7.3.0", - "electron-winstaller": "2.3.4", + "electron-winstaller": "2.5.1", "fs-extra": "0.30.0", "glob": "7.0.3", - "joanna": "0.0.6", + "joanna": "0.0.8", "legal-eagle": "0.13.0", "lodash.template": "4.4.0", "minidump": "0.9.0", diff --git a/script/test b/script/test index 38568207f..2e159d09b 100755 --- a/script/test +++ b/script/test @@ -93,11 +93,15 @@ function runBenchmarkTests (callback) { cp.on('close', exitCode => { callback(null, exitCode) }) } -let testSuitesToRun -if (process.platform === 'darwin') { - testSuitesToRun = [runCoreMainProcessTests, runCoreRenderProcessTests, runBenchmarkTests].concat(packageTestSuites) -} else { - testSuitesToRun = [runCoreMainProcessTests] +let testSuitesToRun = testSuitesForPlatform(process.platform) + +function testSuitesForPlatform(platform) { + switch(platform) { + case 'darwin': return [runCoreMainProcessTests, runCoreRenderProcessTests, runBenchmarkTests].concat(packageTestSuites) + case 'win32': return (process.arch === 'x64') ? [runCoreMainProcessTests, runCoreRenderProcessTests] : [runCoreMainProcessTests] + case 'linux': return [runCoreMainProcessTests] + default: return [] + } } async.series(testSuitesToRun, function (err, exitCodes) { diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 6715d04e2..9b9715a07 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -1,10 +1,13 @@ _ = require 'underscore-plus' path = require 'path' -temp = require 'temp' +temp = require('temp').track() AtomEnvironment = require '../src/atom-environment' StorageFolder = require '../src/storage-folder' describe "AtomEnvironment", -> + afterEach -> + temp.cleanupSync() + describe 'window sizing methods', -> describe '::getPosition and ::setPosition', -> originalPosition = null @@ -324,7 +327,7 @@ describe "AtomEnvironment", -> describe "::unloadEditorWindow()", -> it "saves the BlobStore so it can be loaded after reload", -> - configDirPath = temp.mkdirSync() + configDirPath = temp.mkdirSync('atom-spec-environment') fakeBlobStore = jasmine.createSpyObj("blob store", ["save"]) atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, enablePersistence: true, configDirPath, blobStore: fakeBlobStore, window, document}) @@ -336,7 +339,7 @@ describe "AtomEnvironment", -> describe "::destroy()", -> it "does not throw exceptions when unsubscribing from ipc events (regression)", -> - configDirPath = temp.mkdirSync() + configDirPath = temp.mkdirSync('atom-spec-environment') fakeDocument = { addEventListener: -> removeEventListener: -> @@ -401,6 +404,8 @@ describe "AtomEnvironment", -> subscription?.dispose() it "invokes onUpdateAvailable listeners", -> + return unless process.platform is 'darwin' # Test tied to electron autoUpdater, we use something else on Linux and Win32 + atom.listenForUpdates() updateAvailableHandler = jasmine.createSpy("update-available-handler") diff --git a/spec/atom-paths-spec.js b/spec/atom-paths-spec.js new file mode 100644 index 000000000..4b1fc7902 --- /dev/null +++ b/spec/atom-paths-spec.js @@ -0,0 +1,119 @@ +/** @babel */ + +import {it, fit, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers' +import {app} from 'remote' +import atomPaths from '../src/atom-paths' +import fs from 'fs-plus' +import path from 'path' +const temp = require('temp').track() + +describe("AtomPaths", () => { + const portableAtomHomePath = path.join(atomPaths.getAppDirectory(), '.atom') + console.log(portableAtomHomePath) + + afterEach(() => { + atomPaths.setAtomHome(app.getPath('home')) + }) + + describe('SetAtomHomePath', () => { + describe('when a portable .atom folder exists', () => { + beforeEach(() => { + delete process.env.ATOM_HOME + if (!fs.existsSync(portableAtomHomePath)) + fs.mkdirSync(portableAtomHomePath) + }) + + afterEach(() => { + delete process.env.ATOM_HOME + fs.removeSync(portableAtomHomePath) + }) + + it('sets ATOM_HOME to the portable .atom folder if it has permission', () => { + atomPaths.setAtomHome(app.getPath('home')) + expect(process.env.ATOM_HOME).toEqual(portableAtomHomePath) + }) + + it('uses ATOM_HOME if no write access to portable .atom folder', () => { + if (process.platform === 'win32') return + + const readOnlyPath = temp.mkdirSync('atom-path-spec-no-write-access') + process.env.ATOM_HOME = readOnlyPath + fs.chmodSync(portableAtomHomePath, 444) + atomPaths.setAtomHome(app.getPath('home')) + expect(process.env.ATOM_HOME).toEqual(readOnlyPath) + }) + }) + + describe('when a portable folder does not exist', () => { + beforeEach(() => { + delete process.env.ATOM_HOME + fs.removeSync(portableAtomHomePath) + }) + + afterEach(() => { + delete process.env.ATOM_HOME + }) + + it('leaves ATOM_HOME unmodified if it was already set', () => { + const temporaryHome = temp.mkdirSync('atom-spec-setatomhomepath') + process.env.ATOM_HOME = temporaryHome + atomPaths.setAtomHome(app.getPath('home')) + expect(process.env.ATOM_HOME).toEqual(temporaryHome) + }) + + it('sets ATOM_HOME to a default location if not yet set', () => { + const expectedPath = path.join(app.getPath('home'), '.atom') + atomPaths.setAtomHome(app.getPath('home')) + expect(process.env.ATOM_HOME).toEqual(expectedPath) + }) + }) + }) + + describe('setUserData', () => { + let tempAtomHomePath = null + let electronUserDataPath = null + let defaultElectronUserDataPath = null + + beforeEach(() => { + defaultElectronUserDataPath = app.getPath('userData') + delete process.env.ATOM_HOME + tempAtomHomePath = temp.mkdirSync('atom-paths-specs-userdata-home') + tempAtomConfigPath = path.join(tempAtomHomePath, '.atom') + fs.mkdirSync(tempAtomConfigPath) + electronUserDataPath = path.join(tempAtomConfigPath, 'electronUserData') + atomPaths.setAtomHome(tempAtomHomePath) + }) + + afterEach(() => { + delete process.env.ATOM_HOME + fs.removeSync(electronUserDataPath) + temp.cleanupSync() + app.setPath('userData', defaultElectronUserDataPath) + }) + + describe('when an electronUserData folder exists', () => { + it('sets userData path to the folder if it has permission', () => { + fs.mkdirSync(electronUserDataPath) + atomPaths.setUserData(app) + expect(app.getPath('userData')).toEqual(electronUserDataPath) + }) + + it('leaves userData unchanged if no write access to electronUserData folder', () => { + if (process.platform === 'win32') return + + fs.mkdirSync(electronUserDataPath) + fs.chmodSync(electronUserDataPath, 444) + atomPaths.setUserData(app) + fs.chmodSync(electronUserDataPath, 666) + expect(app.getPath('userData')).toEqual(defaultElectronUserDataPath) + }) + }) + + describe('when an electronUserDataPath folder does not exist', () => { + it('leaves userData app path unchanged', () => { + atomPaths.setUserData(app) + expect(app.getPath('userData')).toEqual(defaultElectronUserDataPath) + }) + }) + }) +}) diff --git a/spec/atom-portable-spec.coffee b/spec/atom-portable-spec.coffee deleted file mode 100644 index aeb71b7c1..000000000 --- a/spec/atom-portable-spec.coffee +++ /dev/null @@ -1,67 +0,0 @@ -path = require 'path' -fs = require 'fs-plus' -AtomPortable = require '../src/main-process/atom-portable' - -portableModeCommonPlatformBehavior = (platform) -> - describe "with ATOM_HOME environment variable", -> - it "returns false", -> - expect(AtomPortable.isPortableInstall(platform, "C:\\some\\path")).toBe false - - describe "without ATOM_HOME environment variable", -> - environmentAtomHome = undefined - portableAtomHomePath = path.join(path.dirname(process.execPath), "..", ".atom") - portableAtomHomeNaturallyExists = fs.existsSync(portableAtomHomePath) - portableAtomHomeBackupPath = "#{portableAtomHomePath}.temp" - - beforeEach -> - fs.renameSync(portableAtomHomePath, portableAtomHomeBackupPath) if fs.existsSync(portableAtomHomePath) - - afterEach -> - if portableAtomHomeNaturallyExists - fs.renameSync(portableAtomHomeBackupPath, portableAtomHomePath) if not fs.existsSync(portableAtomHomePath) - else - fs.removeSync(portableAtomHomePath) if fs.existsSync(portableAtomHomePath) - fs.removeSync(portableAtomHomeBackupPath) if fs.existsSync(portableAtomHomeBackupPath) - - describe "with .atom directory sibling to exec", -> - beforeEach -> - fs.mkdirSync(portableAtomHomePath) if not fs.existsSync(portableAtomHomePath) - - describe "without .atom directory sibling to exec", -> - beforeEach -> - fs.removeSync(portableAtomHomePath) if fs.existsSync(portableAtomHomePath) - - it "returns false", -> - expect(AtomPortable.isPortableInstall(platform, environmentAtomHome)).toBe false - -describe "Set Portable Mode on #win32", -> - portableAtomHomePath = path.join(path.dirname(process.execPath), "..", ".atom") - portableAtomHomeNaturallyExists = fs.existsSync(portableAtomHomePath) - portableAtomHomeBackupPath = "#{portableAtomHomePath}.temp" - - beforeEach -> - fs.renameSync(portableAtomHomePath, portableAtomHomeBackupPath) if fs.existsSync(portableAtomHomePath) - - afterEach -> - if portableAtomHomeNaturallyExists - fs.renameSync(portableAtomHomeBackupPath, portableAtomHomePath) if not fs.existsSync(portableAtomHomePath) - else - fs.removeSync(portableAtomHomePath) if fs.existsSync(portableAtomHomePath) - fs.removeSync(portableAtomHomeBackupPath) if fs.existsSync(portableAtomHomeBackupPath) - - it "creates a portable home directory", -> - expect(fs.existsSync(portableAtomHomePath)).toBe false - - AtomPortable.setPortable(process.env.ATOM_HOME) - expect(fs.existsSync(portableAtomHomePath)).toBe true - -describe "Check for Portable Mode", -> - describe "Windows", -> - portableModeCommonPlatformBehavior "win32" - - describe "Mac", -> - it "returns false", -> - expect(AtomPortable.isPortableInstall("darwin", "darwin")).toBe false - - describe "Linux", -> - portableModeCommonPlatformBehavior "linux" diff --git a/spec/auto-update-manager-spec.js b/spec/auto-update-manager-spec.js index be3a67c84..b38e7827c 100644 --- a/spec/auto-update-manager-spec.js +++ b/spec/auto-update-manager-spec.js @@ -5,6 +5,9 @@ import {remote} from 'electron' const electronAutoUpdater = remote.require('electron').autoUpdater describe('AutoUpdateManager (renderer)', () => { + + if (process.platform !== 'darwin') return // Tests are tied to electron autoUpdater, we use something else on Linux and Win32 + let autoUpdateManager beforeEach(() => { diff --git a/spec/babel-spec.coffee b/spec/babel-spec.coffee index e95b000cb..4e7b2b395 100644 --- a/spec/babel-spec.coffee +++ b/spec/babel-spec.coffee @@ -19,6 +19,7 @@ describe "Babel transpiler support", -> afterEach -> CompileCache.setCacheDirectory(originalCacheDir) + temp.cleanupSync() describe 'when a .js file starts with /** @babel */;', -> it "transpiles it using babel", -> diff --git a/spec/command-installer-spec.coffee b/spec/command-installer-spec.coffee index 84fd77a34..a1cf194a8 100644 --- a/spec/command-installer-spec.coffee +++ b/spec/command-installer-spec.coffee @@ -1,6 +1,6 @@ path = require 'path' fs = require 'fs-plus' -temp = require 'temp' +temp = require('temp').track() CommandInstaller = require '../src/command-installer' describe "CommandInstaller on #darwin", -> @@ -20,6 +20,9 @@ describe "CommandInstaller on #darwin", -> spyOn(CommandInstaller::, 'getResourcesDirectory').andReturn(resourcesPath) spyOn(CommandInstaller::, 'getInstallDirectory').andReturn(installationPath) + afterEach -> + temp.cleanupSync() + it "shows an error dialog when installing commands interactively fails", -> appDelegate = jasmine.createSpyObj("appDelegate", ["confirm"]) installer = new CommandInstaller("2.0.2", appDelegate) diff --git a/spec/compile-cache-spec.coffee b/spec/compile-cache-spec.coffee index bec689c7d..13db6a055 100644 --- a/spec/compile-cache-spec.coffee +++ b/spec/compile-cache-spec.coffee @@ -21,8 +21,9 @@ describe 'CompileCache', -> spyOn(TypeScriptSimple::, 'compile').andReturn 'the-typescript-code' afterEach -> - CSON.setCacheDir(CompileCache.getCacheDirectory()) CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME) + CSON.setCacheDir(CompileCache.getCacheDirectory()) + temp.cleanupSync() describe 'addPathToCache(filePath, atomHome)', -> describe 'when the given file is plain javascript', -> @@ -77,6 +78,8 @@ describe 'CompileCache', -> describe 'overriding Error.prepareStackTrace', -> it 'removes the override on the next tick, and always assigns the raw stack', -> + return if process.platform is 'win32' # Flakey Error.stack contents on Win32 + Error.prepareStackTrace = -> 'a-stack-trace' error = new Error("Oops") diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index acd9b112b..3134a428f 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -1,5 +1,5 @@ path = require 'path' -temp = require 'temp' +temp = require('temp').track() CSON = require 'season' fs = require 'fs-plus' @@ -9,13 +9,14 @@ describe "Config", -> beforeEach -> spyOn(atom.config, "load") spyOn(atom.config, "save") - dotAtomPath = temp.path('dot-atom-dir') + dotAtomPath = temp.path('atom-spec-config') atom.config.configDirPath = dotAtomPath atom.config.enablePersistence = true atom.config.configFilePath = path.join(atom.config.configDirPath, "atom.config.cson") afterEach -> atom.config.enablePersistence = false + fs.removeSync(dotAtomPath) describe ".get(keyPath, {scope, sources, excludeSources})", -> it "allows a key path's value to be read", -> @@ -486,8 +487,8 @@ describe "Config", -> observeHandler.reset() # clear the initial call atom.config.set('foo.bar.baz', "value 2") expect(observeHandler).toHaveBeenCalledWith("value 2") - observeHandler.reset() + observeHandler.reset() atom.config.set('foo.bar.baz', "value 1") expect(observeHandler).toHaveBeenCalledWith("value 1") advanceClock(100) # complete pending save that was requested in ::set @@ -1079,6 +1080,7 @@ describe "Config", -> describe "when the configDirPath doesn't exist", -> it "copies the contents of dot-atom to ~/.atom", -> + return if process.platform is 'win32' # Flakey test on Win32 initializationDone = false jasmine.unspy(window, "setTimeout") atom.config.initializeConfigDirectory -> diff --git a/spec/default-directory-provider-spec.coffee b/spec/default-directory-provider-spec.coffee index df4f589b5..821c278ee 100644 --- a/spec/default-directory-provider-spec.coffee +++ b/spec/default-directory-provider-spec.coffee @@ -1,20 +1,26 @@ DefaultDirectoryProvider = require '../src/default-directory-provider' path = require 'path' fs = require 'fs-plus' -temp = require 'temp' +temp = require('temp').track() describe "DefaultDirectoryProvider", -> + tmp = null + + beforeEach -> + tmp = temp.mkdirSync('atom-spec-default-dir-provider') + + afterEach -> + temp.cleanupSync() + describe ".directoryForURISync(uri)", -> it "returns a Directory with a path that matches the uri", -> provider = new DefaultDirectoryProvider() - tmp = temp.mkdirSync() directory = provider.directoryForURISync(tmp) expect(directory.getPath()).toEqual tmp it "normalizes its input before creating a Directory for it", -> provider = new DefaultDirectoryProvider() - tmp = temp.mkdirSync() nonNormalizedPath = tmp + path.sep + ".." + path.sep + path.basename(tmp) expect(tmp.includes("..")).toBe false expect(nonNormalizedPath.includes("..")).toBe true @@ -24,7 +30,6 @@ describe "DefaultDirectoryProvider", -> it "creates a Directory for its parent dir when passed a file", -> provider = new DefaultDirectoryProvider() - tmp = temp.mkdirSync() file = path.join(tmp, "example.txt") fs.writeFileSync(file, "data") @@ -40,7 +45,6 @@ describe "DefaultDirectoryProvider", -> describe ".directoryForURI(uri)", -> it "returns a Promise that resolves to a Directory with a path that matches the uri", -> provider = new DefaultDirectoryProvider() - tmp = temp.mkdirSync() waitsForPromise -> provider.directoryForURI(tmp).then (directory) -> diff --git a/spec/file-system-blob-store-spec.coffee b/spec/file-system-blob-store-spec.coffee index 5147e59ee..a2ed39014 100644 --- a/spec/file-system-blob-store-spec.coffee +++ b/spec/file-system-blob-store-spec.coffee @@ -1,4 +1,4 @@ -temp = require 'temp' +temp = require('temp').track() path = require 'path' fs = require 'fs-plus' FileSystemBlobStore = require '../src/file-system-blob-store' @@ -7,9 +7,12 @@ describe "FileSystemBlobStore", -> [storageDirectory, blobStore] = [] beforeEach -> - storageDirectory = temp.path() + storageDirectory = temp.path('atom-spec-filesystemblobstore') blobStore = FileSystemBlobStore.load(storageDirectory) + afterEach -> + fs.removeSync(storageDirectory) + it "is empty when the file doesn't exist", -> expect(blobStore.get("foo", "invalidation-key-1")).toBeUndefined() expect(blobStore.get("bar", "invalidation-key-2")).toBeUndefined() diff --git a/spec/fixtures/packages/package-with-deserializers/index.js b/spec/fixtures/packages/package-with-deserializers/index.js index b9be23854..e653c0e67 100644 --- a/spec/fixtures/packages/package-with-deserializers/index.js +++ b/spec/fixtures/packages/package-with-deserializers/index.js @@ -1,4 +1,5 @@ module.exports = { + initialize() {}, activate () {}, deserializeMethod1 (state) { diff --git a/spec/fixtures/sample.txt b/spec/fixtures/sample.txt index 3e715502b..27d91067e 100644 --- a/spec/fixtures/sample.txt +++ b/spec/fixtures/sample.txt @@ -1 +1 @@ -Some text. +Some textSome text. diff --git a/spec/git-repository-provider-spec.coffee b/spec/git-repository-provider-spec.coffee index bbbfb4b03..6c6a7b4b9 100644 --- a/spec/git-repository-provider-spec.coffee +++ b/spec/git-repository-provider-spec.coffee @@ -1,6 +1,6 @@ path = require 'path' fs = require 'fs-plus' -temp = require 'temp' +temp = require('temp').track() {Directory} = require 'pathwatcher' GitRepository = require '../src/git-repository' GitRepositoryProvider = require '../src/git-repository-provider' @@ -11,6 +11,9 @@ describe "GitRepositoryProvider", -> beforeEach -> provider = new GitRepositoryProvider(atom.project, atom.config, atom.confirm) + afterEach -> + temp.cleanupSync() + describe ".repositoryForDirectory(directory)", -> describe "when specified a Directory with a Git repository", -> it "returns a Promise that resolves to a GitRepository", -> diff --git a/spec/git-repository-spec.coffee b/spec/git-repository-spec.coffee index c9a3badb5..59e8c4c68 100644 --- a/spec/git-repository-spec.coffee +++ b/spec/git-repository-spec.coffee @@ -1,11 +1,11 @@ -temp = require 'temp' +temp = require('temp').track() GitRepository = require '../src/git-repository' fs = require 'fs-plus' path = require 'path' Project = require '../src/project' copyRepository = -> - workingDirPath = temp.mkdirSync('atom-working-dir') + workingDirPath = temp.mkdirSync('atom-spec-git') fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath) fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) workingDirPath @@ -19,6 +19,8 @@ describe "GitRepository", -> afterEach -> repo.destroy() if repo?.repo? + try + temp.cleanupSync() # These tests sometimes lag at shutting down resources describe "@open(path)", -> it "returns null when no repository is found", -> @@ -29,10 +31,15 @@ describe "GitRepository", -> expect(-> new GitRepository(path.join(temp.dir, 'nogit.txt'))).toThrow() describe ".getPath()", -> - it "returns the repository path for a .git directory path", -> + it "returns the repository path for a .git directory path with a file", -> + return if process.platform is 'win32' #Win32TestFailures - libgit2 does not detect files in .git folders repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git', 'HEAD')) expect(repo.getPath()).toBe path.join(__dirname, 'fixtures', 'git', 'master.git') + it "returns the repository path for a .git directory path with a directory", -> + repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects')) + expect(repo.getPath()).toBe path.join(__dirname, 'fixtures', 'git', 'master.git') + it "returns the repository path for a repository path", -> repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git')) expect(repo.getPath()).toBe path.join(__dirname, 'fixtures', 'git', 'master.git') @@ -137,6 +144,8 @@ describe "GitRepository", -> editor = atom.workspace.getActiveTextEditor() it "displays a confirmation dialog by default", -> + return if process.platform is 'win32' # Permissions issues with this test on Windows + atom.confirm.andCallFake ({buttons}) -> buttons.OK() atom.config.set('editor.confirmCheckoutHeadRevision', true) @@ -145,6 +154,7 @@ describe "GitRepository", -> expect(fs.readFileSync(filePath, 'utf8')).toBe '' it "does not display a dialog when confirmation is disabled", -> + return if process.platform is 'win32' # Flakey EPERM opening a.txt on Win32 atom.config.set('editor.confirmCheckoutHeadRevision', false) repo.checkoutHeadForEditor(editor) @@ -154,7 +164,7 @@ describe "GitRepository", -> describe ".destroy()", -> it "throws an exception when any method is called after it is called", -> - repo = new GitRepository(require.resolve('./fixtures/git/master.git/HEAD')) + repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git')) repo.destroy() expect(-> repo.getShortHead()).toThrow() diff --git a/spec/grammars-spec.coffee b/spec/grammars-spec.coffee index 7dcff8bcd..47198a124 100644 --- a/spec/grammars-spec.coffee +++ b/spec/grammars-spec.coffee @@ -1,6 +1,6 @@ path = require 'path' fs = require 'fs-plus' -temp = require 'temp' +temp = require('temp').track() GrammarRegistry = require '../src/grammar-registry' Grim = require 'grim' @@ -24,6 +24,7 @@ describe "the `grammars` global", -> afterEach -> atom.packages.deactivatePackages() atom.packages.unloadPackages() + temp.cleanupSync() describe ".selectGrammar(filePath)", -> it "always returns a grammar", -> @@ -96,6 +97,7 @@ describe "the `grammars` global", -> ) grammar1 = atom.grammars.loadGrammarSync(grammarPath1) expect(atom.grammars.selectGrammar('more.test', '')).toBe grammar1 + fs.removeSync(grammarPath1) grammarPath2 = temp.path(suffix: '.json') fs.writeFileSync grammarPath2, JSON.stringify( @@ -105,6 +107,7 @@ describe "the `grammars` global", -> ) grammar2 = atom.grammars.loadGrammarSync(grammarPath2) expect(atom.grammars.selectGrammar('more.test', '')).toBe grammar2 + fs.removeSync(grammarPath2) it "favors non-bundled packages when breaking scoring ties", -> waitsForPromise -> diff --git a/spec/gutter-container-component-spec.coffee b/spec/gutter-container-component-spec.coffee index 73a9d0f6c..c5efbaa8e 100644 --- a/spec/gutter-container-component-spec.coffee +++ b/spec/gutter-container-component-spec.coffee @@ -139,3 +139,22 @@ describe "GutterContainerComponent", -> expect(expectedCustomGutterNode1).toBe atom.views.getView(customGutter1) expectedCustomGutterNode3 = gutterContainerComponent.getDomNode().children.item(2) expect(expectedCustomGutterNode3).toBe atom.views.getView(customGutter3) + + it "reorders correctly when prepending multiple gutters at once", -> + lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'}) + testState = buildTestState([lineNumberGutter]) + gutterContainerComponent.updateSync(testState) + expect(gutterContainerComponent.getDomNode().children.length).toBe 1 + expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0) + expect(expectedCustomGutterNode).toBe atom.views.getView(lineNumberGutter) + + # Prepend two gutters at once + customGutter1 = new Gutter(mockGutterContainer, {name: 'first', priority: -200}) + customGutter2 = new Gutter(mockGutterContainer, {name: 'second', priority: -100}) + testState = buildTestState([customGutter1, customGutter2, lineNumberGutter]) + gutterContainerComponent.updateSync(testState) + expect(gutterContainerComponent.getDomNode().children.length).toBe 3 + expectedCustomGutterNode1 = gutterContainerComponent.getDomNode().children.item(0) + expect(expectedCustomGutterNode1).toBe atom.views.getView(customGutter1) + expectedCustomGutterNode2 = gutterContainerComponent.getDomNode().children.item(1) + expect(expectedCustomGutterNode2).toBe atom.views.getView(customGutter2) diff --git a/spec/history-manager-spec.js b/spec/history-manager-spec.js new file mode 100644 index 000000000..425f1efe0 --- /dev/null +++ b/spec/history-manager-spec.js @@ -0,0 +1,199 @@ +/** @babel */ + +import {it, fit, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers' +import {Emitter, Disposable, CompositeDisposable} from 'event-kit' + +import {HistoryManager, HistoryProject} from '../src/history-manager' + +describe("HistoryManager", () => { + let historyManager, commandRegistry, project, localStorage, stateStore + let commandDisposable, projectDisposable + + beforeEach(() => { + commandDisposable = jasmine.createSpyObj('Disposable', ['dispose']) + commandRegistry = jasmine.createSpyObj('CommandRegistry', ['add']) + commandRegistry.add.andReturn(commandDisposable) + + localStorage = jasmine.createSpyObj('LocalStorage', ['getItem', 'setItem']) + localStorage.items = { + history: JSON.stringify({ + projects: [ + { paths: ['/1', 'c:\\2'], lastOpened: new Date(2016, 9, 17, 17, 16, 23) }, + { paths: ['/test'], lastOpened: new Date(2016, 9, 17, 11, 12, 13) } + ] + }) + } + localStorage.getItem.andCallFake((key) => localStorage.items[key]) + localStorage.setItem.andCallFake((key, value) => localStorage.items[key] = value) + + projectDisposable = jasmine.createSpyObj('Disposable', ['dispose']) + project = jasmine.createSpyObj('Project', ['onDidChangePaths']) + project.onDidChangePaths.andCallFake((f) => { + project.didChangePathsListener = f + return projectDisposable + }) + + historyManager = new HistoryManager({project, commands:commandRegistry, localStorage}) + }) + + describe("constructor", () => { + it("registers the 'clear-project-history' command function", () => { + expect(commandRegistry.add).toHaveBeenCalled() + const cmdCall = commandRegistry.add.calls[0] + expect(cmdCall.args.length).toBe(2) + expect(cmdCall.args[0]).toBe('atom-workspace') + expect(typeof cmdCall.args[1]['application:clear-project-history']).toBe('function') + }) + + describe("getProjects", () => { + it("returns an array of HistoryProjects", () => { + expect(historyManager.getProjects()).toEqual([ + new HistoryProject(['/1', 'c:\\2'], new Date(2016, 9, 17, 17, 16, 23)), + new HistoryProject(['/test'], new Date(2016, 9, 17, 11, 12, 13)) + ]) + }) + + it("returns an array of HistoryProjects that is not mutable state", () => { + const firstProjects = historyManager.getProjects() + firstProjects.pop() + firstProjects[0].path = 'modified' + + const secondProjects = historyManager.getProjects() + expect(secondProjects.length).toBe(2) + expect(secondProjects[0].path).not.toBe('modified') + }) + }) + + describe("clearProjects", () => { + it("clears the list of projects", () => { + expect(historyManager.getProjects().length).not.toBe(0) + historyManager.clearProjects() + expect(historyManager.getProjects().length).toBe(0) + }) + + it("saves the state", () => { + expect(localStorage.setItem).not.toHaveBeenCalled() + historyManager.clearProjects() + expect(localStorage.setItem).toHaveBeenCalled() + expect(localStorage.setItem.calls[0].args[0]).toBe('history') + expect(historyManager.getProjects().length).toBe(0) + }) + + it("fires the onDidChangeProjects event", () => { + expect(localStorage.setItem).not.toHaveBeenCalled() + historyManager.clearProjects() + expect(localStorage.setItem).toHaveBeenCalled() + expect(localStorage.setItem.calls[0].args[0]).toBe('history') + expect(historyManager.getProjects().length).toBe(0) + }) + }) + + it("loads state", () => { + expect(localStorage.getItem).toHaveBeenCalledWith('history') + }) + + it("listens to project.onDidChangePaths adding a new project", () => { + const start = new Date() + project.didChangePathsListener(['/a/new', '/path/or/two']) + const projects = historyManager.getProjects() + expect(projects.length).toBe(3) + expect(projects[0].paths).toEqual(['/a/new', '/path/or/two']) + expect(projects[0].lastOpened).not.toBeLessThan(start) + }) + + it("listens to project.onDidChangePaths updating an existing project", () => { + const start = new Date() + project.didChangePathsListener(['/test']) + const projects = historyManager.getProjects() + expect(projects.length).toBe(2) + expect(projects[0].paths).toEqual(['/test']) + expect(projects[0].lastOpened).not.toBeLessThan(start) + }) + }) + + describe("loadState", () => { + it("defaults to an empty array if no state", () => { + localStorage.items.history = null + historyManager.loadState() + expect(historyManager.getProjects()).toEqual([]) + }) + + it("defaults to an empty array if no projects", () => { + localStorage.items.history = JSON.stringify('') + historyManager.loadState() + expect(historyManager.getProjects()).toEqual([]) + }) + }) + + describe("addProject", () => { + it("adds a new project to the end", () => { + const date = new Date(2010, 10, 9, 8, 7, 6) + historyManager.addProject(['/a/b'], date) + const projects = historyManager.getProjects() + expect(projects.length).toBe(3) + expect(projects[2].paths).toEqual(['/a/b']) + expect(projects[2].lastOpened).toBe(date) + }) + + it("adds a new project to the start", () => { + const date = new Date() + historyManager.addProject(['/so/new'], date) + const projects = historyManager.getProjects() + expect(projects.length).toBe(3) + expect(projects[0].paths).toEqual(['/so/new']) + expect(projects[0].lastOpened).toBe(date) + }) + + it("updates an existing project and moves it to the start", () => { + const date = new Date() + historyManager.addProject(['/test'], date) + const projects = historyManager.getProjects() + expect(projects.length).toBe(2) + expect(projects[0].paths).toEqual(['/test']) + expect(projects[0].lastOpened).toBe(date) + }) + + it("fires the onDidChangeProjects event when adding a project", () => { + const didChangeSpy = jasmine.createSpy() + const beforeCount = historyManager.getProjects().length + historyManager.onDidChangeProjects(didChangeSpy) + historyManager.addProject(['/test-new'], new Date()) + expect(didChangeSpy).toHaveBeenCalled() + expect(historyManager.getProjects().length).toBe(beforeCount + 1) + }) + + it("fires the onDidChangeProjects event when updating a project", () => { + const didChangeSpy = jasmine.createSpy() + const beforeCount = historyManager.getProjects().length + historyManager.onDidChangeProjects(didChangeSpy) + historyManager.addProject(['/test'], new Date()) + expect(didChangeSpy).toHaveBeenCalled() + expect(historyManager.getProjects().length).toBe(beforeCount) + }) + }) + + describe("getProject", () => { + it("returns a project that matches the paths", () => { + const project = historyManager.getProject(['/1', 'c:\\2']) + expect(project).not.toBeNull() + expect(project.paths).toEqual(['/1', 'c:\\2']) + }) + + it("returns null when it can't find the project", () => { + const project = historyManager.getProject(['/1']) + expect(project).toBeNull() + }) + }) + + describe("saveState" ,() => { + it("saves the state", () => { + historyManager.addProject(["/save/state"]) + historyManager.saveState() + expect(localStorage.setItem).toHaveBeenCalled() + expect(localStorage.setItem.calls[0].args[0]).toBe('history') + expect(localStorage.items['history']).toContain('/save/state') + historyManager.loadState() + expect(historyManager.getProjects()[0].paths).toEqual(['/save/state']) + }) + }) +}) diff --git a/spec/integration/helpers/start-atom.coffee b/spec/integration/helpers/start-atom.coffee index 1eb610a2f..856885f60 100644 --- a/spec/integration/helpers/start-atom.coffee +++ b/spec/integration/helpers/start-atom.coffee @@ -10,13 +10,13 @@ webdriverio = require '../../../script/node_modules/webdriverio' AtomPath = remote.process.argv[0] AtomLauncherPath = path.join(__dirname, "..", "helpers", "atom-launcher.sh") -ChromedriverPath = path.resolve(__dirname, '..', '..', '..', 'electron', 'chromedriver', 'chromedriver') +ChromedriverPath = path.resolve(__dirname, '..', '..', '..', 'script', 'node_modules', 'electron-chromedriver', 'bin', 'chromedriver') SocketPath = path.join(os.tmpdir(), "atom-integration-test-#{Date.now()}.sock") ChromedriverPort = 9515 ChromedriverURLBase = "/wd/hub" ChromedriverStatusURL = "http://localhost:#{ChromedriverPort}#{ChromedriverURLBase}/status" -userDataDir = temp.mkdirSync('atom-user-data-dir') +userDataDir = null chromeDriverUp = (done) -> checkStatus = -> @@ -38,6 +38,7 @@ chromeDriverDown = (done) -> setTimeout(checkStatus, 100) buildAtomClient = (args, env) -> + userDataDir = temp.mkdirSync('atom-user-data-dir') client = webdriverio.remote( host: 'localhost' port: ChromedriverPort diff --git a/spec/integration/smoke-spec.coffee b/spec/integration/smoke-spec.coffee index 3f921c4fe..527ed1f8f 100644 --- a/spec/integration/smoke-spec.coffee +++ b/spec/integration/smoke-spec.coffee @@ -5,6 +5,8 @@ temp = require('temp').track() runAtom = require './helpers/start-atom' describe "Smoke Test", -> + return unless process.platform is 'darwin' # Fails on win32 + atomHome = temp.mkdirSync('atom-home') beforeEach -> diff --git a/spec/keymap-extensions-spec.coffee b/spec/keymap-extensions-spec.coffee new file mode 100644 index 000000000..784764036 --- /dev/null +++ b/spec/keymap-extensions-spec.coffee @@ -0,0 +1,23 @@ +path = require 'path' +temp = require('temp').track() +CSON = require 'season' +fs = require 'fs-plus' + +describe "keymap-extensions", -> + + beforeEach -> + atom.keymaps.configDirPath = temp.path('atom-spec-keymap-ext') + fs.writeFileSync(atom.keymaps.getUserKeymapPath(), '#') + @userKeymapLoaded = -> + atom.keymaps.onDidLoadUserKeymap => @userKeymapLoaded() + + afterEach -> + fs.removeSync(atom.keymaps.configDirPath) + atom.keymaps.destroy() + + describe "did-load-user-keymap", -> + + it "fires when user keymap is loaded", -> + spyOn(this, 'userKeymapLoaded') + atom.keymaps.loadUserKeymap() + expect(@userKeymapLoaded).toHaveBeenCalled() diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index 2172267db..c1dab6c6b 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -78,9 +78,10 @@ describe "LinesYardstick", -> expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 0))).toEqual({left: 0, top: 0}) expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 1))).toEqual({left: 7, top: 0}) expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 5))).toEqual({left: 38, top: 0}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 6))).toEqual({left: 43, top: 14}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 9))).toEqual({left: 72, top: 14}) - expect(linesYardstick.pixelPositionForScreenPosition(Point(2, Infinity))).toEqual({left: 287.859375, top: 28}) + if process.platform is 'darwin' # One pixel off on left on Win32 + expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 6))).toEqual({left: 43, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 9))).toEqual({left: 72, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(2, Infinity))).toEqual({left: 287.875, top: 28}) it "reuses already computed pixel positions unless it is invalidated", -> atom.styles.addStyleSheet """ @@ -133,72 +134,92 @@ describe "LinesYardstick", -> editor.setText(text) + return unless process.platform is 'darwin' # These numbers are 15 higher on win32 and always integer expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 35)).left).toBe 230.90625 expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 36)).left).toBe 237.5 expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 37)).left).toBe 244.09375 - describe "::screenPositionForPixelPosition(pixelPosition)", -> - it "converts pixel positions to screen positions", -> + if process.platform is 'darwin' # Expectations fail on win32 + it "handles lines containing a mix of left-to-right and right-to-left characters", -> + editor.setText('Persian, locally known as Parsi or Farsi (زبان فارسی), the predominant modern descendant of Old Persian.\n') + atom.styles.addStyleSheet """ * { - font-size: 12px; - font-family: monospace; - } - .syntax--function { - font-size: 16px - } - """ - - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 12.5})).toEqual([0, 2]) - expect(linesYardstick.screenPositionForPixelPosition({top: 14, left: 18.8})).toEqual([1, 3]) - expect(linesYardstick.screenPositionForPixelPosition({top: 28, left: 100})).toEqual([2, 14]) - expect(linesYardstick.screenPositionForPixelPosition({top: 32, left: 24.3})).toEqual([2, 3]) - expect(linesYardstick.screenPositionForPixelPosition({top: 46, left: 66.5})).toEqual([3, 9]) - expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 99.9})).toEqual([5, 14]) - expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 224.2365234375})).toEqual([5, 29]) - expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 225})).toEqual([5, 30]) - expect(linesYardstick.screenPositionForPixelPosition({top: 84, left: 247.1})).toEqual([6, 33]) - - it "overshoots to the nearest character when text nodes are not spatially contiguous", -> - atom.styles.addStyleSheet """ - * { - font-size: 12px; + font-size: 14px; font-family: monospace; } """ - buildLineNode = (screenRow) -> - lineNode = document.createElement("div") - lineNode.style.whiteSpace = "pre" - lineNode.innerHTML = 'foobar' - jasmine.attachToDOM(lineNode) - createdLineNodes.push(lineNode) - lineNode - editor.setText("foobar") + lineTopIndex = new LineTopIndex({defaultLineHeight: editor.getLineHeightInPixels()}) + linesYardstick = new LinesYardstick(editor, mockLineNodesProvider, lineTopIndex, atom.grammars) + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 15))).toEqual({left: 126, top: 0}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 62))).toEqual({left: 521, top: 0}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 58))).toEqual({left: 487, top: 0}) + expect(linesYardstick.pixelPositionForScreenPosition(Point(0, Infinity))).toEqual({left: 873.625, top: 0}) - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 7})).toEqual([0, 1]) - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 14})).toEqual([0, 2]) - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 21})).toEqual([0, 3]) - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 30})).toEqual([0, 3]) - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 50})).toEqual([0, 3]) - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 62})).toEqual([0, 3]) - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 69})).toEqual([0, 4]) - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 76})).toEqual([0, 5]) - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 100})).toEqual([0, 6]) - expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 200})).toEqual([0, 6]) + describe "::screenPositionForPixelPosition(pixelPosition)", -> + it "converts pixel positions to screen positions", -> + atom.styles.addStyleSheet """ + * { + font-size: 12px; + font-family: monospace; + } + .syntax--function { + font-size: 16px + } + """ - it "clips pixel positions above buffer start", -> - expect(linesYardstick.screenPositionForPixelPosition(top: -Infinity, left: -Infinity)).toEqual [0, 0] - expect(linesYardstick.screenPositionForPixelPosition(top: -Infinity, left: Infinity)).toEqual [0, 0] - expect(linesYardstick.screenPositionForPixelPosition(top: -1, left: Infinity)).toEqual [0, 0] - expect(linesYardstick.screenPositionForPixelPosition(top: 0, left: Infinity)).toEqual [0, 29] + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 12.5})).toEqual([0, 2]) + expect(linesYardstick.screenPositionForPixelPosition({top: 14, left: 18.8})).toEqual([1, 3]) + expect(linesYardstick.screenPositionForPixelPosition({top: 28, left: 100})).toEqual([2, 14]) + expect(linesYardstick.screenPositionForPixelPosition({top: 32, left: 24.3})).toEqual([2, 3]) + expect(linesYardstick.screenPositionForPixelPosition({top: 46, left: 66.5})).toEqual([3, 9]) + expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 99.9})).toEqual([5, 14]) + expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 225})).toEqual([5, 30]) + return unless process.platform is 'darwin' # Following tests are 1 pixel off on Win32 + expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 224.2365234375})).toEqual([5, 29]) + expect(linesYardstick.screenPositionForPixelPosition({top: 84, left: 247.1})).toEqual([6, 33]) - it "clips pixel positions below buffer end", -> - expect(linesYardstick.screenPositionForPixelPosition(top: Infinity, left: -Infinity)).toEqual [12, 2] - expect(linesYardstick.screenPositionForPixelPosition(top: Infinity, left: Infinity)).toEqual [12, 2] - expect(linesYardstick.screenPositionForPixelPosition(top: (editor.getLastScreenRow() + 1) * 14, left: 0)).toEqual [12, 2] - expect(linesYardstick.screenPositionForPixelPosition(top: editor.getLastScreenRow() * 14, left: 0)).toEqual [12, 0] + it "overshoots to the nearest character when text nodes are not spatially contiguous", -> + atom.styles.addStyleSheet """ + * { + font-size: 12px; + font-family: monospace; + } + """ - it "clips negative horizontal pixel positions", -> - expect(linesYardstick.screenPositionForPixelPosition(top: 0, left: -10)).toEqual [0, 0] - expect(linesYardstick.screenPositionForPixelPosition(top: 1 * 14, left: -10)).toEqual [1, 0] + buildLineNode = (screenRow) -> + lineNode = document.createElement("div") + lineNode.style.whiteSpace = "pre" + lineNode.innerHTML = 'foobar' + jasmine.attachToDOM(lineNode) + createdLineNodes.push(lineNode) + lineNode + editor.setText("foobar") + + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 7})).toEqual([0, 1]) + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 14})).toEqual([0, 2]) + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 21})).toEqual([0, 3]) + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 30})).toEqual([0, 3]) + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 50})).toEqual([0, 3]) + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 62})).toEqual([0, 3]) + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 69})).toEqual([0, 4]) + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 76})).toEqual([0, 5]) + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 100})).toEqual([0, 6]) + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 200})).toEqual([0, 6]) + + it "clips pixel positions above buffer start", -> + expect(linesYardstick.screenPositionForPixelPosition(top: -Infinity, left: -Infinity)).toEqual [0, 0] + expect(linesYardstick.screenPositionForPixelPosition(top: -Infinity, left: Infinity)).toEqual [0, 0] + expect(linesYardstick.screenPositionForPixelPosition(top: -1, left: Infinity)).toEqual [0, 0] + expect(linesYardstick.screenPositionForPixelPosition(top: 0, left: Infinity)).toEqual [0, 29] + + it "clips pixel positions below buffer end", -> + expect(linesYardstick.screenPositionForPixelPosition(top: Infinity, left: -Infinity)).toEqual [12, 2] + expect(linesYardstick.screenPositionForPixelPosition(top: Infinity, left: Infinity)).toEqual [12, 2] + expect(linesYardstick.screenPositionForPixelPosition(top: (editor.getLastScreenRow() + 1) * 14, left: 0)).toEqual [12, 2] + expect(linesYardstick.screenPositionForPixelPosition(top: editor.getLastScreenRow() * 14, left: 0)).toEqual [12, 0] + + it "clips negative horizontal pixel positions", -> + expect(linesYardstick.screenPositionForPixelPosition(top: 0, left: -10)).toEqual [0, 0] + expect(linesYardstick.screenPositionForPixelPosition(top: 1 * 14, left: -10)).toEqual [1, 0] diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 22902d3d8..77d7987a7 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -22,7 +22,7 @@ describe('AtomApplication', function () { originalAtomHome = process.env.ATOM_HOME process.env.ATOM_HOME = makeTempDir('atom-home') // Symlinking the compile cache into the temporary home dir makes the windows load much faster - fs.symlinkSync(path.join(originalAtomHome, 'compile-cache'), path.join(process.env.ATOM_HOME, 'compile-cache')) + fs.symlinkSync(path.join(originalAtomHome, 'compile-cache'), path.join(process.env.ATOM_HOME, 'compile-cache'), 'junction') season.writeFileSync(path.join(process.env.ATOM_HOME, 'config.cson'), { '*': { welcome: {showOnStartup: false}, @@ -309,7 +309,7 @@ describe('AtomApplication', function () { const packagePath = path.join(__dirname, '..', 'fixtures', 'packages', 'package-with-directory-provider') const packagesDirPath = path.join(process.env.ATOM_HOME, 'packages') fs.mkdirSync(packagesDirPath) - fs.symlinkSync(packagePath, path.join(packagesDirPath, 'package-with-directory-provider')) + fs.symlinkSync(packagePath, path.join(packagesDirPath, 'package-with-directory-provider'), 'junction') const atomApplication = buildAtomApplication() atomApplication.config.set('core.disabledPackages', ['fuzzy-finder']) @@ -396,6 +396,30 @@ describe('AtomApplication', function () { }) } }) + + describe('when adding or removing project folders', function () { + it('stores the window state immediately', async function () { + const dirA = makeTempDir() + const dirB = makeTempDir() + + const atomApplication = buildAtomApplication() + const window = atomApplication.launch(parseCommandLine([dirA, dirB])) + await focusWindow(window) + assert.deepEqual(await getTreeViewRootDirectories(window), [dirA, dirB]) + + await evalInWebContents(window.browserWindow.webContents, (sendBackToMainProcess) => { + atom.project.removePath(atom.project.getPaths()[0]) + sendBackToMainProcess(null) + }) + assert.deepEqual(await getTreeViewRootDirectories(window), [dirB]) + + // Window state should be saved when the project folder is removed + const atomApplication2 = buildAtomApplication() + const [window2] = atomApplication2.launch(parseCommandLine([])) + await focusWindow(window2) + assert.deepEqual(await getTreeViewRootDirectories(window2), [dirB]) + }) + }) }) describe('before quitting', function () { diff --git a/spec/main-process/file-recovery-service.test.js b/spec/main-process/file-recovery-service.test.js index 19c964be7..862b7f428 100644 --- a/spec/main-process/file-recovery-service.test.js +++ b/spec/main-process/file-recovery-service.test.js @@ -2,19 +2,23 @@ import {dialog} from 'electron' import FileRecoveryService from '../../src/main-process/file-recovery-service' -import temp from 'temp' import fs from 'fs-plus' import sinon from 'sinon' import {escapeRegExp} from 'underscore-plus' +const temp = require('temp').track() describe("FileRecoveryService", () => { let recoveryService, recoveryDirectory beforeEach(() => { - recoveryDirectory = temp.mkdirSync() + recoveryDirectory = temp.mkdirSync('atom-spec-file-recovery') recoveryService = new FileRecoveryService(recoveryDirectory) }) + afterEach(() => { + temp.cleanupSync() + }) + describe("when no crash happens during a save", () => { it("creates a recovery file and deletes it after saving", () => { const mockWindow = {} @@ -28,6 +32,8 @@ describe("FileRecoveryService", () => { recoveryService.didSavePath(mockWindow, filePath) assert.equal(fs.listTreeSync(recoveryDirectory).length, 0) assert.equal(fs.readFileSync(filePath, 'utf8'), "changed") + + fs.removeSync(filePath) }) it("creates only one recovery file when many windows attempt to save the same file, deleting it when the last one finishes saving it", () => { @@ -48,6 +54,8 @@ describe("FileRecoveryService", () => { recoveryService.didSavePath(anotherMockWindow, filePath) assert.equal(fs.listTreeSync(recoveryDirectory).length, 0) assert.equal(fs.readFileSync(filePath, 'utf8'), "changed") + + fs.removeSync(filePath) }) }) @@ -64,6 +72,8 @@ describe("FileRecoveryService", () => { recoveryService.didCrashWindow(mockWindow) assert.equal(fs.listTreeSync(recoveryDirectory).length, 0) assert.equal(fs.readFileSync(filePath, 'utf8'), "some content") + + fs.removeSync(filePath) }) it("restores the created recovery file when many windows attempt to save the same file and one of them crashes", () => { @@ -94,6 +104,8 @@ describe("FileRecoveryService", () => { recoveryService.didCrashWindow(anotherMockWindow) assert.equal(fs.readFileSync(filePath, 'utf8'), "D") assert.equal(fs.listTreeSync(recoveryDirectory).length, 0) + + fs.removeSync(filePath) }) it("emits a warning when a file can't be recovered", sinon.test(function () { @@ -113,6 +125,8 @@ describe("FileRecoveryService", () => { assert.equal(logs.length, 1) assert.match(logs[0], new RegExp(escapeRegExp(filePath))) assert.match(logs[0], new RegExp(escapeRegExp(recoveryFiles[0]))) + + fs.removeSync(filePath) })) }) diff --git a/spec/menu-manager-spec.coffee b/spec/menu-manager-spec.coffee index 5de5ecf92..2db6f35a0 100644 --- a/spec/menu-manager-spec.coffee +++ b/spec/menu-manager-spec.coffee @@ -79,6 +79,7 @@ describe "MenuManager", -> runs -> expect(menu.sendToBrowserProcess.argsForCall[0][1]['b']).toBeUndefined() it "omits key bindings that could conflict with AltGraph characters on macOS", -> + Object.defineProperty process, 'platform', value: 'darwin' spyOn(menu, 'sendToBrowserProcess') menu.add [{label: "A", submenu: [ {label: "B", command: "b"}, diff --git a/spec/module-cache-spec.coffee b/spec/module-cache-spec.coffee index 4c0a549aa..1627ec776 100644 --- a/spec/module-cache-spec.coffee +++ b/spec/module-cache-spec.coffee @@ -1,13 +1,16 @@ path = require 'path' Module = require 'module' fs = require 'fs-plus' -temp = require 'temp' +temp = require('temp').track() ModuleCache = require '../src/module-cache' describe 'ModuleCache', -> beforeEach -> spyOn(Module, '_findPath').andCallThrough() + afterEach -> + temp.cleanupSync() + it 'resolves Electron module paths without hitting the filesystem', -> builtins = ModuleCache.cache.builtins expect(Object.keys(builtins).length).toBeGreaterThan 0 diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 62e96f81c..c2e9e11be 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -1,6 +1,6 @@ path = require 'path' Package = require '../src/package' -temp = require 'temp' +temp = require('temp').track() fs = require 'fs-plus' {Disposable} = require 'atom' {buildKeydownEvent} = require '../src/keymap-extensions' @@ -17,6 +17,9 @@ describe "PackageManager", -> beforeEach -> workspaceElement = atom.views.getView(atom.workspace) + afterEach -> + temp.cleanupSync() + describe "::getApmPath()", -> it "returns the path to the apm command", -> apmPath = path.join(process.resourcesPath, "app", "apm", "bin", "apm") @@ -440,11 +443,9 @@ describe "PackageManager", -> spyOn(mainModule, 'activate').andCallThrough() spyOn(Package.prototype, 'requireMainModule').andCallThrough() - promise = atom.packages.activatePackage('package-with-activation-hooks') - it "defers requiring/activating the main module until an triggering of an activation hook occurs", -> + promise = atom.packages.activatePackage('package-with-activation-hooks') expect(Package.prototype.requireMainModule.callCount).toBe 0 - atom.packages.triggerActivationHook('language-fictitious:grammar-used') atom.packages.triggerDeferredActivationHooks() @@ -455,6 +456,7 @@ describe "PackageManager", -> expect(Package.prototype.requireMainModule.callCount).toBe 1 it "does not double register activation hooks when deactivating and reactivating", -> + promise = atom.packages.activatePackage('package-with-activation-hooks') expect(mainModule.activate.callCount).toBe 0 atom.packages.triggerActivationHook('language-fictitious:grammar-used') atom.packages.triggerDeferredActivationHooks() @@ -489,6 +491,17 @@ describe "PackageManager", -> expect(mainModule.activate.callCount).toBe 1 expect(Package.prototype.requireMainModule.callCount).toBe 1 + it "activates the package immediately if the activation hook had already been triggered", -> + atom.packages.triggerActivationHook('language-fictitious:grammar-used') + atom.packages.triggerDeferredActivationHooks() + expect(Package.prototype.requireMainModule.callCount).toBe 0 + + waitsForPromise -> + atom.packages.activatePackage('package-with-activation-hooks') + + runs -> + expect(Package.prototype.requireMainModule.callCount).toBe 1 + describe "when the package has no main module", -> it "does not throw an exception", -> spyOn(console, "error") @@ -643,7 +656,7 @@ describe "PackageManager", -> [element, events, userKeymapPath] = [] beforeEach -> - userKeymapPath = path.join(temp.path(), "user-keymaps.cson") + userKeymapPath = path.join(temp.mkdirSync(), "user-keymaps.cson") spyOn(atom.keymaps, "getUserKeymapPath").andReturn(userKeymapPath) element = createTestElement('test-1') @@ -660,6 +673,8 @@ describe "PackageManager", -> atom.keymaps.watchSubscriptions[userKeymapPath].dispose() delete atom.keymaps.watchSubscriptions[userKeymapPath] + temp.cleanupSync() + it "doesn't override user-defined keymaps", -> fs.writeFileSync userKeymapPath, """ ".test-1": @@ -740,10 +755,6 @@ describe "PackageManager", -> two = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/2.less") three = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/3.css") - one = atom.themes.stringToId(one) - two = atom.themes.stringToId(two) - three = atom.themes.stringToId(three) - expect(atom.themes.stylesheetElementForId(one)).toBeNull() expect(atom.themes.stylesheetElementForId(two)).toBeNull() expect(atom.themes.stylesheetElementForId(three)).toBeNull() @@ -765,11 +776,6 @@ describe "PackageManager", -> three = require.resolve("./fixtures/packages/package-with-styles/styles/3.test-context.css") four = require.resolve("./fixtures/packages/package-with-styles/styles/4.css") - one = atom.themes.stringToId(one) - two = atom.themes.stringToId(two) - three = atom.themes.stringToId(three) - four = atom.themes.stringToId(four) - expect(atom.themes.stylesheetElementForId(one)).toBeNull() expect(atom.themes.stylesheetElementForId(two)).toBeNull() expect(atom.themes.stylesheetElementForId(three)).toBeNull() diff --git a/spec/package-spec.coffee b/spec/package-spec.coffee index a0e7ffa4d..8119136be 100644 --- a/spec/package-spec.coffee +++ b/spec/package-spec.coffee @@ -205,3 +205,26 @@ describe "Package", -> it "uses the package name defined in package.json", -> expect(metadata.name).toBe 'package-with-a-totally-different-name' + + describe "the initialize() hook", -> + it "gets called when the package is activated", -> + packagePath = atom.project.getDirectories()[0].resolve('packages/package-with-deserializers') + pack = buildPackage(packagePath) + pack.requireMainModule() + mainModule = pack.mainModule + spyOn(mainModule, 'initialize') + expect(mainModule.initialize).not.toHaveBeenCalled() + pack.activate() + expect(mainModule.initialize).toHaveBeenCalled() + expect(mainModule.initialize.callCount).toBe(1) + + it "gets called when a deserializer is used", -> + packagePath = atom.project.getDirectories()[0].resolve('packages/package-with-deserializers') + pack = buildPackage(packagePath) + pack.requireMainModule() + mainModule = pack.mainModule + spyOn(mainModule, 'initialize') + pack.load() + expect(mainModule.initialize).not.toHaveBeenCalled() + atom.deserializers.deserialize({deserializer: 'Deserializer1', a: 'b'}) + expect(mainModule.initialize).toHaveBeenCalled() diff --git a/spec/package-transpilation-registry-spec.js b/spec/package-transpilation-registry-spec.js new file mode 100644 index 000000000..bf8f12475 --- /dev/null +++ b/spec/package-transpilation-registry-spec.js @@ -0,0 +1,147 @@ +/** @babel */ +import fs from 'fs' +import path from 'path' + +import {it, fit, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers' + +import PackageTranspilationRegistry from '../src/package-transpilation-registry' + +const originalCompiler = { + getCachePath: (sourceCode, filePath) => { + return "orig-cache-path" + }, + + compile: (sourceCode, filePath) => { + return sourceCode + "-original-compiler" + }, + + shouldCompile: (sourceCode, filePath) => { + return path.extname(filePath) === '.js' + } +} + +describe("PackageTranspilationRegistry", () => { + let registry + let wrappedCompiler + + beforeEach(() => { + registry = new PackageTranspilationRegistry() + wrappedCompiler = registry.wrapTranspiler(originalCompiler) + }) + + it('falls through to the original compiler by default', () => { + spyOn(originalCompiler, 'getCachePath') + spyOn(originalCompiler, 'compile') + spyOn(originalCompiler, 'shouldCompile') + + wrappedCompiler.getCachePath('source', '/path/to/file.js') + wrappedCompiler.compile('source', '/path/to/filejs') + wrappedCompiler.shouldCompile('source', '/path/to/file.js') + + expect(originalCompiler.getCachePath).toHaveBeenCalled() + expect(originalCompiler.compile).toHaveBeenCalled() + expect(originalCompiler.shouldCompile).toHaveBeenCalled() + }) + + describe('when a file is contained in a path that has custom transpilation', () => { + const hitPath = path.join('/path/to/lib/file.js') + const hitPathCoffee = path.join('/path/to/file2.coffee') + const missPath = path.join('/path/other/file3.js') + const hitPathMissSubdir =path.join('/path/to/file4.js') + const hitPathMissExt = path.join('/path/to/file5.ts') + const nodeModulesFolder = path.join('/path/to/lib/node_modules/file6.js') + const hitNonStandardExt = path.join('/path/to/file7.omgwhatisthis') + + const jsSpec = { glob: "lib/**/*.js", transpiler: './transpiler-js', options: { type: 'js' } } + const coffeeSpec = { glob: "*.coffee", transpiler: './transpiler-coffee', options: { type: 'coffee' } } + const omgSpec = { glob: "*.omgwhatisthis", transpiler: './transpiler-omg', options: { type: 'omg' } } + + const expectedMeta = { name: 'my-package', path: path.join('/path/to'), meta: { some: 'metadata' } } + + const jsTranspiler = { + transpile: (sourceCode, filePath, options) => { + return {code: sourceCode + "-transpiler-js"} + }, + + getCacheKeyData: (sourceCode, filePath, options) => { + return 'js-transpiler-cache-data' + } + } + + const coffeeTranspiler = { + transpile: (sourceCode, filePath, options) => { + return {code: sourceCode + "-transpiler-coffee"} + }, + + getCacheKeyData: (sourceCode, filePath, options) => { + return 'coffee-transpiler-cache-data' + } + } + + const omgTranspiler = { + transpile: (sourceCode, filePath, options) => { + return {code: sourceCode + "-transpiler-omg"} + }, + + getCacheKeyData: (sourceCode, filePath, options) => { + return 'omg-transpiler-cache-data' + } + } + + beforeEach(() => { + jsSpec._transpilerSource = "js-transpiler-source" + coffeeSpec._transpilerSource = "coffee-transpiler-source" + omgTranspiler._transpilerSource = "omg-transpiler-source" + + spyOn(registry, "getTranspiler").andCallFake(spec => { + if (spec.transpiler === './transpiler-js') return jsTranspiler + if (spec.transpiler === './transpiler-coffee') return coffeeTranspiler + if (spec.transpiler === './transpiler-omg') return omgTranspiler + throw new Error('bad transpiler path ' + spec.transpiler) + }) + + registry.addTranspilerConfigForPath(path.join('/path/to'), 'my-package', { some: 'metadata' }, [ + jsSpec, coffeeSpec, omgSpec + ]) + }) + + it('always returns true from shouldCompile for a file in that dir that match a glob', () => { + spyOn(originalCompiler, 'shouldCompile').andReturn(false) + expect(wrappedCompiler.shouldCompile('source', hitPath)).toBe(true) + expect(wrappedCompiler.shouldCompile('source', hitPathCoffee)).toBe(true) + expect(wrappedCompiler.shouldCompile('source', hitNonStandardExt)).toBe(true) + expect(wrappedCompiler.shouldCompile('source', hitPathMissExt)).toBe(false) + expect(wrappedCompiler.shouldCompile('source', hitPathMissSubdir)).toBe(false) + expect(wrappedCompiler.shouldCompile('source', missPath)).toBe(false) + expect(wrappedCompiler.shouldCompile('source', nodeModulesFolder)).toBe(false) + }) + + it('calls getCacheKeyData on the transpiler to get additional cache key data', () => { + spyOn(registry, "getTranspilerPath").andReturn("./transpiler-js") + spyOn(jsTranspiler, 'getCacheKeyData').andCallThrough() + + wrappedCompiler.getCachePath('source', missPath, jsSpec) + expect(jsTranspiler.getCacheKeyData).not.toHaveBeenCalledWith('source', missPath, jsSpec.options, expectedMeta) + wrappedCompiler.getCachePath('source', hitPath, jsSpec) + expect(jsTranspiler.getCacheKeyData).toHaveBeenCalledWith('source', hitPath, jsSpec.options, expectedMeta) + }) + + it('compiles files matching a glob with the associated transpiler, and the old one otherwise', () => { + spyOn(jsTranspiler, "transpile").andCallThrough() + spyOn(coffeeTranspiler, "transpile").andCallThrough() + spyOn(omgTranspiler, "transpile").andCallThrough() + + expect(wrappedCompiler.compile('source', hitPath)).toEqual('source-transpiler-js') + expect(jsTranspiler.transpile).toHaveBeenCalledWith('source', hitPath, jsSpec.options, expectedMeta) + expect(wrappedCompiler.compile('source', hitPathCoffee)).toEqual('source-transpiler-coffee') + expect(coffeeTranspiler.transpile).toHaveBeenCalledWith('source', hitPathCoffee, coffeeSpec.options, expectedMeta) + expect(wrappedCompiler.compile('source', hitNonStandardExt)).toEqual('source-transpiler-omg') + expect(omgTranspiler.transpile).toHaveBeenCalledWith('source', hitNonStandardExt, omgSpec.options, expectedMeta) + + expect(wrappedCompiler.compile('source', missPath)).toEqual('source-original-compiler') + expect(wrappedCompiler.compile('source', hitPathMissExt)).toEqual('source-original-compiler') + expect(wrappedCompiler.compile('source', hitPathMissSubdir)).toEqual('source-original-compiler') + expect(wrappedCompiler.compile('source', nodeModulesFolder)).toEqual('source-original-compiler') + }) + }) +}) diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee index d8f74db53..596b1ecea 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -1080,6 +1080,7 @@ describe "Pane", -> expect(eventCount).toBe 1 it "only calls terminate handler once when text is modified twice", -> + originalText = editor1.getText() editor1.insertText('Some text') advanceClock(editor1.getBuffer().stoppedChangingDelay) @@ -1091,6 +1092,10 @@ describe "Pane", -> expect(pane.getPendingItem()).toBeNull() expect(eventCount).toBe 1 + # Reset fixture back to original state + editor1.setText(originalText) + editor1.save() + it "only calls clearPendingItem if there is a pending item to clear", -> spyOn(pane, "clearPendingItem").andCallThrough() diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 30415a059..d548255e5 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -1,4 +1,4 @@ -temp = require 'temp' +temp = require('temp').track() Project = require '../src/project' fs = require 'fs-plus' path = require 'path' @@ -12,6 +12,9 @@ describe "Project", -> # Wait for project's service consumers to be asynchronously added waits(1) + afterEach -> + temp.cleanupSync() + describe "serialization", -> deserializedProject = null @@ -51,7 +54,7 @@ describe "Project", -> it "does not deserialize buffers when their path is a directory that exists", -> - pathToOpen = path.join(temp.mkdirSync(), 'file.txt') + pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') waitsForPromise -> atom.workspace.open(pathToOpen) @@ -64,7 +67,8 @@ describe "Project", -> expect(deserializedProject.getBuffers().length).toBe 0 it "does not deserialize buffers when their path is inaccessible", -> - pathToOpen = path.join(temp.mkdirSync(), 'file.txt') + return if process.platform is 'win32' # chmod not supported on win32 + pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') fs.writeFileSync(pathToOpen, '') waitsForPromise -> @@ -151,7 +155,7 @@ describe "Project", -> expect(notification.getType()).toBe 'warning' expect(notification.getDetail()).toBe 'SomeError' expect(notification.getMessage()).toContain '`resurrect`' - expect(notification.getMessage()).toContain 'fixtures/dir/a' + expect(notification.getMessage()).toContain path.join('fixtures', 'dir', 'a') describe "when a custom repository-provider service is provided", -> [fakeRepositoryProvider, fakeRepository] = [] diff --git a/spec/reopen-project-menu-manager-spec.js b/spec/reopen-project-menu-manager-spec.js new file mode 100644 index 000000000..e508b68ba --- /dev/null +++ b/spec/reopen-project-menu-manager-spec.js @@ -0,0 +1,267 @@ +/** @babel */ + +import {it, fit, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers' +import {Emitter, Disposable, CompositeDisposable} from 'event-kit' + +const ReopenProjectMenuManager = require('../src/reopen-project-menu-manager') + +numberRange = (low, high) => { + const size = high - low + const result = new Array(size) + for (var i = 0; i < size; i++) + result[i] = low + i + return result +} + +describe("ReopenProjectMenuManager", () => { + let menuManager, commandRegistry, config, historyManager, reopenProjects + let commandDisposable, configDisposable, historyDisposable + + beforeEach(() => { + menuManager = jasmine.createSpyObj('MenuManager', ['add']) + menuManager.add.andReturn(new Disposable()) + + commandRegistry = jasmine.createSpyObj('CommandRegistry', ['add']) + commandDisposable = jasmine.createSpyObj('Disposable', ['dispose']) + commandRegistry.add.andReturn(commandDisposable) + + config = jasmine.createSpyObj('Config', ['onDidChange', 'get']) + config.get.andReturn(10) + configDisposable = jasmine.createSpyObj('Disposable', ['dispose']) + config.didChangeListener = { } + config.onDidChange.andCallFake((key, fn) => { + config.didChangeListener[key] = fn + return configDisposable + }) + + historyManager = jasmine.createSpyObj('historyManager', ['getProjects','onDidChangeProjects']) + historyManager.getProjects.andReturn([]) + historyDisposable = jasmine.createSpyObj('Disposable', ['dispose']) + historyManager.onDidChangeProjects.andCallFake((fn) => { + historyManager.changeProjectsListener = fn + return historyDisposable + }) + + openFunction = jasmine.createSpy() + reopenProjects = new ReopenProjectMenuManager({menu:menuManager, commands: commandRegistry, history: historyManager, config, open:openFunction}) + }) + + describe("constructor", () => { + it("registers the 'reopen-project' command function", () => { + expect(commandRegistry.add).toHaveBeenCalled() + const cmdCall = commandRegistry.add.calls[0] + expect(cmdCall.args.length).toBe(2) + expect(cmdCall.args[0]).toBe('atom-workspace') + expect(typeof cmdCall.args[1]['application:reopen-project']).toBe('function') + }) + }) + + describe("dispose", () => { + it("disposes of the history, command and config disposables", () => { + reopenProjects.dispose() + expect(historyDisposable.dispose).toHaveBeenCalled() + expect(configDisposable.dispose).toHaveBeenCalled() + expect(commandDisposable.dispose).toHaveBeenCalled() + }) + + it("disposes of the menu disposable once used", () => { + const menuDisposable = jasmine.createSpyObj('Disposable', ['dispose']) + menuManager.add.andReturn(menuDisposable) + reopenProjects.update() + expect(menuDisposable.dispose).not.toHaveBeenCalled() + reopenProjects.dispose() + expect(menuDisposable.dispose).toHaveBeenCalled() + }) + }) + + describe("the command", () => { + it("calls open with the paths of the project specified by the detail index", () => { + historyManager.getProjects.andReturn([ { paths: ['/a'] }, { paths: ['/b', 'c:\\'] }]) + reopenProjects.update() + + reopenProjectCommand = commandRegistry.add.calls[0].args[1]['application:reopen-project'] + reopenProjectCommand({ detail: { index: 1 } }) + + expect(openFunction).toHaveBeenCalled() + expect(openFunction.calls[0].args[0]).toEqual(['/b', 'c:\\']) + }) + + it("does not call open when no command detail is supplied", () => { + reopenProjectCommand = commandRegistry.add.calls[0].args[1]['application:reopen-project'] + reopenProjectCommand({}) + + expect(openFunction).not.toHaveBeenCalled() + }) + + it("does not call open when no command detail index is supplied", () => { + reopenProjectCommand = commandRegistry.add.calls[0].args[1]['application:reopen-project'] + reopenProjectCommand({ detail: { anything: 'here' } }) + + expect(openFunction).not.toHaveBeenCalled() + }) + }) + + describe("update", () => { + it("adds menu items to MenuManager based on projects from HistoryManager", () => { + historyManager.getProjects.andReturn([ { paths: ['/a'] }, { paths: ['/b', 'c:\\'] }]) + reopenProjects.update() + expect(historyManager.getProjects).toHaveBeenCalled() + expect(menuManager.add).toHaveBeenCalled() + const menuArg = menuManager.add.calls[0].args[0] + expect(menuArg.length).toBe(1) + expect(menuArg[0].label).toBe('File') + expect(menuArg[0].submenu.length).toBe(1) + const projectsMenu = menuArg[0].submenu[0] + expect(projectsMenu.label).toBe('Reopen Project') + expect(projectsMenu.submenu.length).toBe(2) + + const first = projectsMenu.submenu[0] + expect(first.label).toBe('/a') + expect(first.command).toBe('application:reopen-project') + expect(first.commandDetail).toEqual({ index: 0 }) + + const second = projectsMenu.submenu[1] + expect(second.label).toBe('b, c:\\') + expect(second.command).toBe('application:reopen-project') + expect(second.commandDetail).toEqual({ index: 1 }) + }) + + it("adds only the number of menu items specified in the 'core.reopenProjectMenuCount' config", () => { + historyManager.getProjects.andReturn(numberRange(1, 100).map(i => ({ paths: [ '/test/' + i ] }))) + reopenProjects.update() + expect(menuManager.add).toHaveBeenCalled() + const menu = menuManager.add.calls[0].args[0][0] + expect(menu.label).toBe('File') + expect(menu.submenu.length).toBe(1) + expect(menu.submenu[0].label).toBe('Reopen Project') + expect(menu.submenu[0].submenu.length).toBe(10) + }) + + it("disposes the previously menu built", () => { + const menuDisposable = jasmine.createSpyObj('Disposable', ['dispose']) + menuManager.add.andReturn(menuDisposable) + reopenProjects.update() + expect(menuDisposable.dispose).not.toHaveBeenCalled() + reopenProjects.update() + expect(menuDisposable.dispose).toHaveBeenCalled() + }) + + it("is called when the Config changes for 'core.reopenProjectMenuCount'", () => { + historyManager.getProjects.andReturn(numberRange(1, 100).map(i => ({ paths: [ '/test/' + i ] }))) + reopenProjects.update() + config.get.andReturn(25) + config.didChangeListener['core.reopenProjectMenuCount']({oldValue:10, newValue: 25}) + + const finalArgs = menuManager.add.calls[1].args[0] + const projectsMenu = finalArgs[0].submenu[0].submenu + + expect(projectsMenu.length).toBe(25) + }) + + it("is called when the HistoryManager's projects change", () => { + reopenProjects.update() + historyManager.getProjects.andReturn([ { paths: ['/a'] }, { paths: ['/b', 'c:\\'] } ]) + historyManager.changeProjectsListener() + expect(menuManager.add.calls.length).toBe(2) + + const finalArgs = menuManager.add.calls[1].args[0] + const projectsMenu = finalArgs[0].submenu[0] + + const first = projectsMenu.submenu[0] + expect(first.label).toBe('/a') + expect(first.command).toBe('application:reopen-project') + expect(first.commandDetail).toEqual({ index: 0 }) + + const second = projectsMenu.submenu[1] + expect(second.label).toBe('b, c:\\') + expect(second.command).toBe('application:reopen-project') + expect(second.commandDetail).toEqual({ index: 1 }) + }) + }) + + describe("updateProjects", () => { + it("creates correct menu items commands for recent projects", () => { + const projects = [ + { paths: [ '/users/neila' ] }, + { paths: [ '/users/buzza', 'users/michaelc' ] } + ] + + const menu = ReopenProjectMenuManager.createProjectsMenu(projects) + expect(menu.label).toBe('File') + expect(menu.submenu.length).toBe(1) + + const recentMenu = menu.submenu[0] + expect(recentMenu.label).toBe('Reopen Project') + expect(recentMenu.submenu.length).toBe(2) + + const first = recentMenu.submenu[0] + expect(first.label).toBe('/users/neila') + expect(first.command).toBe('application:reopen-project') + expect(first.commandDetail).toEqual({index: 0}) + + const second = recentMenu.submenu[1] + expect(second.label).toBe('buzza, michaelc') + expect(second.command).toBe('application:reopen-project') + expect(second.commandDetail).toEqual({index: 1}) + }) + }) + + describe("createLabel", () => { + it("returns the Unix path unchanged if there is only one", () => { + const label = ReopenProjectMenuManager.createLabel({ paths: ['/a/b/c/d/e/f'] }) + expect(label).toBe('/a/b/c/d/e/f') + }) + + it("returns the Windows path unchanged if there is only one", () => { + const label = ReopenProjectMenuManager.createLabel({ paths: ['c:\\missions\\apollo11'] }) + expect(label).toBe('c:\\missions\\apollo11') + }) + + it("returns the URL unchanged if there is only one", () => { + const label = ReopenProjectMenuManager.createLabel({ paths: ['https://launch.pad/apollo/11'] }) + expect(label).toBe('https://launch.pad/apollo/11') + }) + + it("returns a comma-seperated list of base names if there are multiple", () => { + const project = { paths: [ '/var/one', '/usr/bin/two', '/etc/mission/control/three' ] } + const label = ReopenProjectMenuManager.createLabel(project) + expect(label).toBe('one, two, three') + }) + + describe("betterBaseName", () => { + it("returns the standard base name for an absolute Unix path", () => { + const name = ReopenProjectMenuManager.betterBaseName('/one/to/three') + expect(name).toBe('three') + }) + + it("returns the standard base name for a relative Windows path", () => { + if (process.platform === 'win32') { + const name = ReopenProjectMenuManager.betterBaseName('.\\one\\two') + expect(name).toBe('two') + } + }) + + it("returns the standard base name for an absolute Windows path", () => { + if (process.platform === 'win32') { + const name = ReopenProjectMenuManager.betterBaseName('c:\\missions\\apollo\\11') + expect(name).toBe('11') + } + }) + + it("returns the drive root for a Windows drive name", () => { + const name = ReopenProjectMenuManager.betterBaseName('d:') + expect(name).toBe('d:\\') + }) + + it("returns the drive root for a Windows drive root", () => { + const name = ReopenProjectMenuManager.betterBaseName('e:\\') + expect(name).toBe('e:\\') + }) + + it("returns the final path for a URI", () => { + const name = ReopenProjectMenuManager.betterBaseName('https://something/else') + expect(name).toBe('else') + }) + }) + }) +}) diff --git a/spec/selection-spec.coffee b/spec/selection-spec.coffee index 1b21d7411..cb070310a 100644 --- a/spec/selection-spec.coffee +++ b/spec/selection-spec.coffee @@ -81,8 +81,9 @@ describe "Selection", -> describe "when the selection is destroyed", -> it "destroys its marker", -> selection.setBufferRange([[2, 0], [2, 10]]) + marker = selection.marker selection.destroy() - expect(selection.marker.isDestroyed()).toBeTruthy() + expect(marker.isDestroyed()).toBeTruthy() describe ".insertText(text, options)", -> it "allows pasting white space only lines when autoIndent is enabled", -> diff --git a/spec/spawner-spec.coffee b/spec/spawner-spec.coffee deleted file mode 100644 index 27b5be7ea..000000000 --- a/spec/spawner-spec.coffee +++ /dev/null @@ -1,57 +0,0 @@ -ChildProcess = require 'child_process' -Spawner = require '../src/main-process/spawner' - -describe "Spawner", -> - beforeEach -> - # Prevent any commands from actually running and affecting the host - originalSpawn = ChildProcess.spawn - - harmlessSpawn = - # Just spawn something that won't actually modify the host - if process.platform is 'win32' - originalSpawn('dir') - else - originalSpawn('ls') - - spyOn(ChildProcess, 'spawn').andCallFake (command, args, callback) -> - harmlessSpawn - - it "invokes passed callback", -> - someCallback = jasmine.createSpy('someCallback') - - Spawner.spawn('some-command', 'some-args', someCallback) - - waitsFor -> - someCallback.callCount is 1 - - it "spawns passed command with arguments", -> - actualCommand = null - actualArgs = null - - # Redefine fake invocation, so to remember passed arguments - jasmine.unspy(ChildProcess, 'spawn') - spyOn(ChildProcess, 'spawn').andCallFake (command, args) -> - actualCommand = command - actualArgs = args - harmlessSpawn - - expectedCommand = 'some-command' - expectedArgs = 'some-args' - someCallback = jasmine.createSpy('someCallback') - - Spawner.spawn(expectedCommand, expectedArgs, someCallback) - - expect(actualCommand).toBe expectedCommand - expect(actualArgs).toBe expectedArgs - - it "ignores errors by spawned process", -> - # Redefine fake invocation, so to cause an error - jasmine.unspy(ChildProcess, 'spawn') - spyOn(ChildProcess, 'spawn').andCallFake -> throw new Error("EBUSY") - - someCallback = jasmine.createSpy('someCallback') - - expect(Spawner.spawn('some-command', 'some-args', someCallback)).toBe undefined - - waitsFor -> - someCallback.callCount is 1 diff --git a/spec/squirrel-update-spec.coffee b/spec/squirrel-update-spec.coffee index 083b1f78d..4c7e796ac 100644 --- a/spec/squirrel-update-spec.coffee +++ b/spec/squirrel-update-spec.coffee @@ -1,7 +1,7 @@ {EventEmitter} = require 'events' fs = require 'fs-plus' path = require 'path' -temp = require 'temp' +temp = require('temp').track() SquirrelUpdate = require '../src/main-process/squirrel-update' Spawner = require '../src/main-process/spawner' WinShell = require '../src/main-process/win-shell' @@ -36,6 +36,9 @@ describe "Windows Squirrel Update", -> WinShell.folderContextMenu = new FakeShellOption() WinShell.folderBackgroundContextMenu = new FakeShellOption() + afterEach -> + temp.cleanupSync() + it "quits the app on all squirrel events", -> app = quit: jasmine.createSpy('quit') diff --git a/spec/style-manager-spec.js b/spec/style-manager-spec.js index 898e85587..88baac160 100644 --- a/spec/style-manager-spec.js +++ b/spec/style-manager-spec.js @@ -1,10 +1,11 @@ +const temp = require('temp').track() const StyleManager = require('../src/style-manager') describe('StyleManager', () => { let [styleManager, addEvents, removeEvents, updateEvents] = [] beforeEach(() => { - styleManager = new StyleManager({configDirPath: atom.getConfigDirPath()}) + styleManager = new StyleManager({configDirPath: temp.mkdirSync('atom-config')}) addEvents = [] removeEvents = [] updateEvents = [] @@ -13,6 +14,10 @@ describe('StyleManager', () => { styleManager.onDidUpdateStyleElement((event) => { updateEvents.push(event) }) }) + afterEach(() => { + temp.cleanupSync() + }) + describe('::addStyleSheet(source, params)', () => { it('adds a style sheet based on the given source and returns a disposable allowing it to be removed', () => { const disposable = styleManager.addStyleSheet('a {color: red}') @@ -43,12 +48,12 @@ describe('StyleManager', () => { atom-text-editor[mini].is-focused::shadow .class-7 { color: green; } `) expect(Array.from(styleManager.getStyleElements()[0].sheet.cssRules).map((r) => r.selectorText)).toEqual([ - 'atom-text-editor .class-1, atom-text-editor .class-2', - 'atom-text-editor > .class-3', + 'atom-text-editor.editor .class-1, atom-text-editor.editor .class-2', + 'atom-text-editor.editor > .class-3', 'atom-text-editor .class-4', 'another-element::shadow .class-5', - 'atom-text-editor[data-grammar*=\"js\"] .class-6', - 'atom-text-editor[mini].is-focused .class-7' + 'atom-text-editor[data-grammar*=\"js\"].editor .class-6', + 'atom-text-editor[mini].is-focused.editor .class-7' ]) }) @@ -75,8 +80,8 @@ describe('StyleManager', () => { `) expect(Array.from(styleManager.getStyleElements()[1].sheet.cssRules).map((r) => r.selectorText)).toEqual([ '.source > .js, .source.coffee', - 'atom-text-editor .syntax--source > .syntax--js', - 'atom-text-editor[mini].is-focused .syntax--source > .syntax--js', + 'atom-text-editor.editor .syntax--source > .syntax--js', + 'atom-text-editor[mini].is-focused.editor .syntax--source > .syntax--js', 'atom-text-editor .source > .js' ]) }) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 4478df532..82ef7dd2a 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -566,7 +566,7 @@ describe('TextEditorComponent', function () { editor.setSoftWrapped(true) runAnimationFrames() - componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' + componentNode.style.width = 17 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' component.measureDimensions() runAnimationFrames() }) @@ -700,13 +700,9 @@ describe('TextEditorComponent', function () { runAnimationFrames() let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) - expect(line2LeafNodes.length).toBe(3) - expect(line2LeafNodes[0].textContent).toBe(' ') + expect(line2LeafNodes.length).toBe(1) + expect(line2LeafNodes[0].textContent).toBe(' ') expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(false) - expect(line2LeafNodes[1].textContent).toBe(' ') - expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe(false) - expect(line2LeafNodes[2].textContent).toBe(' ') - expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe(false) }) }) @@ -939,13 +935,17 @@ describe('TextEditorComponent', function () { }) it('pads line numbers to be right-justified based on the maximum number of line number digits', function () { - editor.getBuffer().setText([1, 2, 3, 4, 5, 6, 7, 8, 9, 10].join('\n')) + const input = []; + for (let i = 1; i <= 100; ++i) { + input.push(i); + } + editor.getBuffer().setText(input.join('\n')) runAnimationFrames() for (let screenRow = 0; screenRow <= 8; ++screenRow) { - expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + NBSP + (screenRow + 1)) + expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + NBSP + NBSP + (screenRow + 1)) } - expect(component.lineNumberNodeForScreenRow(9).textContent).toBe('10') + expect(component.lineNumberNodeForScreenRow(99).textContent).toBe('100') let gutterNode = componentNode.querySelector('.gutter') let initialGutterWidth = gutterNode.offsetWidth editor.getBuffer().delete([[1, 0], [2, 0]]) @@ -953,7 +953,7 @@ describe('TextEditorComponent', function () { runAnimationFrames() for (let screenRow = 0; screenRow <= 8; ++screenRow) { - expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + (screenRow + 1)) + expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + NBSP + (screenRow + 1)) } expect(gutterNode.offsetWidth).toBeLessThan(initialGutterWidth) editor.getBuffer().insert([0, 0], '\n\n') @@ -961,9 +961,9 @@ describe('TextEditorComponent', function () { runAnimationFrames() for (let screenRow = 0; screenRow <= 8; ++screenRow) { - expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + NBSP + (screenRow + 1)) + expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + NBSP + NBSP + (screenRow + 1)) } - expect(component.lineNumberNodeForScreenRow(9).textContent).toBe('10') + expect(component.lineNumberNodeForScreenRow(99).textContent).toBe('100') expect(gutterNode.offsetWidth).toBe(initialGutterWidth) }) @@ -1269,10 +1269,10 @@ describe('TextEditorComponent', function () { let cursor = componentNode.querySelector('.cursor') let cursorRect = cursor.getBoundingClientRect() - let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.syntax--source.syntax--js').childNodes[2] + let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.syntax--source.syntax--js').childNodes[0] let range = document.createRange(cursorLocationTextNode) - range.setStart(cursorLocationTextNode, 0) - range.setEnd(cursorLocationTextNode, 1) + range.setStart(cursorLocationTextNode, 3) + range.setEnd(cursorLocationTextNode, 4) let rangeRect = range.getBoundingClientRect() expect(cursorRect.left).toBeCloseTo(rangeRect.left, 0) expect(cursorRect.width).toBeCloseTo(rangeRect.width, 0) @@ -2291,7 +2291,9 @@ describe('TextEditorComponent', function () { let position = wrapperNode.pixelPositionForBufferPosition([0, 26]) let overlay = component.getTopmostDOMNode().querySelector('atom-overlay') - expect(overlay.style.left).toBe(Math.round(position.left + gutterWidth) + 'px') + if (process.platform == 'darwin') { // Result is 359px on win32, expects 375px + expect(overlay.style.left).toBe(Math.round(position.left + gutterWidth) + 'px') + } expect(overlay.style.top).toBe(position.top + editor.getLineHeightInPixels() + 'px') editor.insertText('a') @@ -3846,6 +3848,40 @@ describe('TextEditorComponent', function () { }) }) + describe('when the mousewheel event\'s target is an SVG element inside a block decoration', function () { + it('keeps the block decoration on the DOM if it is scrolled off-screen', function () { + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 20 * charWidth + 'px' + editor.update({autoHeight: false}) + component.measureDimensions() + runAnimationFrames() + + const item = document.createElement('div') + const svgElement = document.createElementNS("http://www.w3.org/2000/svg", "svg") + item.appendChild(svgElement) + editor.decorateMarker( + editor.markScreenPosition([0, 0], {invalidate: "never"}), + {type: "block", item: item} + ) + + runAnimationFrames() + + let wheelEvent = new WheelEvent('mousewheel', { + wheelDeltaX: 0, + wheelDeltaY: -500 + }) + Object.defineProperty(wheelEvent, 'target', { + get: function () { + return svgElement + } + }) + componentNode.dispatchEvent(wheelEvent) + runAnimationFrames() + + expect(component.getTopmostDOMNode().contains(item)).toBe(true) + }) + }) + it('only prevents the default action of the mousewheel event if it actually lead to scrolling', function () { spyOn(WheelEvent.prototype, 'preventDefault').andCallThrough() wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' diff --git a/spec/text-editor-element-spec.coffee b/spec/text-editor-element-spec.coffee index 7ed4a106f..468adaf04 100644 --- a/spec/text-editor-element-spec.coffee +++ b/spec/text-editor-element-spec.coffee @@ -78,6 +78,19 @@ describe "TextEditorElement", -> jasmine.attachToDOM(element) expect(element.querySelectorAll('.decoration').length).toBe initialDecorationCount + it "can be re-focused using the previous `document.activeElement`", -> + editorElement = document.createElement('atom-text-editor') + jasmine.attachToDOM(editorElement) + editorElement.focus() + + activeElement = document.activeElement + + editorElement.remove() + jasmine.attachToDOM(editorElement) + activeElement.focus() + + expect(editorElement.hasFocus()).toBe(true) + describe "focus and blur handling", -> it "proxies focus/blur events to/from the hidden input", -> element = new TextEditorElement diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 8af2f9abd..63a616cfc 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -165,23 +165,6 @@ describe "TextEditorPresenter", -> expect(stateFn(presenter).tiles[10]).toBeUndefined() expect(stateFn(presenter).tiles[12]).toBeUndefined() - it "excludes invalid tiles for screen rows to measure", -> - presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) - presenter.setScreenRowsToMeasure([20, 30]) # unexisting rows - - expect(stateFn(presenter).tiles[0]).toBeDefined() - expect(stateFn(presenter).tiles[2]).toBeDefined() - expect(stateFn(presenter).tiles[4]).toBeDefined() - expect(stateFn(presenter).tiles[6]).toBeDefined() - expect(stateFn(presenter).tiles[8]).toBeUndefined() - expect(stateFn(presenter).tiles[10]).toBeUndefined() - expect(stateFn(presenter).tiles[12]).toBeUndefined() - - presenter.setScreenRowsToMeasure([12]) - buffer.deleteRows(12, 13) - - expect(stateFn(presenter).tiles[12]).toBeUndefined() - describe "when there are block decorations", -> it "computes each tile's height and scrollTop based on block decorations' height", -> presenter = buildPresenter(explicitHeight: 120, scrollTop: 0, lineHeight: 10, tileSize: 2) @@ -2045,6 +2028,27 @@ describe "TextEditorPresenter", -> expect(stateForHighlightInTile(presenter, highlight, 0)).toBeUndefined() + it "handles highlights that extend to the left of the visible area (regression)", -> + editor.setSelectedBufferRanges([ + [[0, 2], [1, 4]], + ]) + + presenter = buildPresenter(explicitHeight: 20, scrollLeft: 0, tileSize: 2) + expectValues stateForSelectionInTile(presenter, 0, 0), { + regions: [ + {top: 0 * 10, height: 10, left: 2 * 10, right: 0 * 10}, + {top: 1 * 10, height: 10, left: 0 * 10, width: 4 * 10} + ] + } + + presenter = buildPresenter(explicitHeight: 20, scrollLeft: 20, tileSize: 2) + expectValues stateForSelectionInTile(presenter, 0, 0), { + regions: [ + {top: 0 * 10, height: 10, left: 2 * 10, right: 0 * 10}, + {top: 1 * 10, height: 10, left: 0 * 10, width: 4 * 10} + ] + } + it "updates when ::scrollTop changes", -> editor.setSelectedBufferRanges([ [[6, 2], [6, 4]], @@ -2177,7 +2181,7 @@ describe "TextEditorPresenter", -> editor.getSelections()[2].setBufferRange([[1, 4], [1, 8]], autoscroll: false) waitsForStateToUpdate presenter - destroyedSelection = null + [destroyedSelection, destroyedDecoration] = [] runs -> expectValues stateForSelectionInTile(presenter, 2, 0), { regions: [{top: 10, left: 4 * 10, width: 4 * 10, height: 10}] @@ -2185,10 +2189,11 @@ describe "TextEditorPresenter", -> # destroying destroyedSelection = editor.getSelections()[2] + destroyedDecoration = destroyedSelection.decoration waitsForStateToUpdate presenter, -> destroyedSelection.destroy() runs -> - expectUndefinedStateForHighlight(presenter, destroyedSelection.decoration) + expectUndefinedStateForHighlight(presenter, destroyedDecoration) it "updates when highlight decorations' properties are updated", -> marker = editor.markBufferPosition([2, 2]) @@ -2518,13 +2523,13 @@ describe "TextEditorPresenter", -> pixelPosition: {top: 1 * 10, left: 26 * 10 + gutterWidth - scrollLeft} } - expectStateUpdate presenter, -> editor.insertText('a') + expectStateUpdate presenter, -> editor.insertText('abc', autoscroll: false) expectValues stateForOverlay(presenter, decoration), { item: item pixelPosition: {top: 1 * 10, left: windowWidth - itemWidth} } - expectStateUpdate presenter, -> editor.insertText('b') + expectStateUpdate presenter, -> editor.insertText('d', autoscroll: false) expectValues stateForOverlay(presenter, decoration), { item: item pixelPosition: {top: 1 * 10, left: windowWidth - itemWidth} @@ -2545,14 +2550,55 @@ describe "TextEditorPresenter", -> } expectStateUpdate presenter, -> - editor.insertNewline() - presenter.setScrollTop(scrollTop) # I'm fighting the editor + editor.insertNewline(autoscroll: false) expectValues stateForOverlay(presenter, decoration), { item: item pixelPosition: {top: 6 * 10 - scrollTop - itemHeight, left: gutterWidth} } + it "when avoidOverflow is false, does not move horizontally when overflowing the editor's scrollView horizontally", -> + scrollLeft = 20 + marker = editor.markBufferPosition([0, 26], invalidate: 'never') + decoration = editor.decorateMarker(marker, {type: 'overlay', item, avoidOverflow: false}) + + presenter = buildPresenter({scrollLeft, windowWidth, windowHeight, contentFrameWidth, boundingClientRect, gutterWidth}) + expectStateUpdate presenter, -> + presenter.setOverlayDimensions(decoration.id, itemWidth, itemHeight, contentMargin) + + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 1 * 10, left: 26 * 10 + gutterWidth - scrollLeft} + } + + expectStateUpdate presenter, -> editor.insertText('a', autoscroll: false) + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 1 * 10, left: 27 * 10 + gutterWidth - scrollLeft} + } + + it "when avoidOverflow is false, does not flip vertically when overflowing the editor's scrollView vertically", -> + scrollTop = 10 + marker = editor.markBufferPosition([5, 0], invalidate: 'never') + decoration = editor.decorateMarker(marker, {type: 'overlay', item, avoidOverflow: false}) + + presenter = buildPresenter({scrollTop, windowWidth, windowHeight, contentFrameWidth, boundingClientRect, gutterWidth}) + expectStateUpdate presenter, -> + presenter.setOverlayDimensions(decoration.id, itemWidth, itemHeight, contentMargin) + + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 6 * 10 - scrollTop, left: gutterWidth} + } + + expectStateUpdate presenter, -> + editor.insertNewline(autoscroll: false) + + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 7 * 10 - scrollTop, left: gutterWidth} + } + describe "when the overlay item has a margin", -> beforeEach -> itemWidth = 12 * 10 @@ -2773,7 +2819,7 @@ describe "TextEditorPresenter", -> expect(getLineNumberGutterState(presenter).content.maxLineNumberDigits).toBe 2 editor.setText("1\n2\n3") - expect(getLineNumberGutterState(presenter).content.maxLineNumberDigits).toBe 1 + expect(getLineNumberGutterState(presenter).content.maxLineNumberDigits).toBe 2 describe ".content.tiles", -> lineNumberStateForScreenRow = (presenter, screenRow) -> diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 1890b0ef7..6e6e1b7b1 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -299,7 +299,7 @@ describe "TextEditor", -> it "positions the cursor at the buffer position that corresponds to the given screen position", -> editor.setCursorScreenPosition([9, 0]) - expect(editor.getCursorBufferPosition()).toEqual [8, 10] + expect(editor.getCursorBufferPosition()).toEqual [8, 11] describe ".moveUp()", -> it "moves the cursor up", -> @@ -4325,15 +4325,17 @@ describe "TextEditor", -> expect(editor.getLastSelection().isEmpty()).toBeTruthy() it "does not explode if the current language mode has no comment regex", -> - editor.destroy() - - waitsForPromise -> - atom.workspace.open(null, autoIndent: false).then (o) -> editor = o + editor = new TextEditor(buffer: new TextBuffer(text: 'hello')) + editor.setSelectedBufferRange([[0, 0], [0, 5]]) + editor.toggleLineCommentsInSelection() + expect(editor.lineTextForBufferRow(0)).toBe "hello" + it "does nothing for empty lines and null grammar", -> runs -> - editor.setSelectedBufferRange([[4, 5], [4, 5]]) + editor.setGrammar(atom.grammars.grammarForScopeName('text.plain.null-grammar')) + editor.setCursorBufferPosition([10, 0]) editor.toggleLineCommentsInSelection() - expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {" + expect(editor.buffer.lineForRow(10)).toBe "" it "uncomments when the line lacks the trailing whitespace in the comment regex", -> editor.setCursorBufferPosition([10, 0]) @@ -4860,15 +4862,13 @@ describe "TextEditor", -> expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 2]] describe '.setTabLength(tabLength)', -> - it 'retokenizes the editor with the given tab length', -> + it 'clips atomic soft tabs to the given tab length', -> expect(editor.getTabLength()).toBe 2 - leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> 'leading-whitespace' in token.scopes - expect(leadingWhitespaceTokens.length).toBe(3) + expect(editor.clipScreenPosition([5, 1], clipDirection: 'forward')).toEqual([5, 2]) editor.setTabLength(6) expect(editor.getTabLength()).toBe 6 - leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> 'leading-whitespace' in token.scopes - expect(leadingWhitespaceTokens.length).toBe(1) + expect(editor.clipScreenPosition([5, 1], clipDirection: 'forward')).toEqual([5, 6]) changeHandler = jasmine.createSpy('changeHandler') editor.onDidChange(changeHandler) @@ -5051,11 +5051,13 @@ describe "TextEditor", -> describe ".destroy()", -> it "destroys marker layers associated with the text editor", -> + buffer.retain() selectionsMarkerLayerId = editor.selectionsMarkerLayer.id foldsMarkerLayerId = editor.displayLayer.foldsMarkerLayer.id editor.destroy() expect(buffer.getMarkerLayer(selectionsMarkerLayerId)).toBeUndefined() expect(buffer.getMarkerLayer(foldsMarkerLayerId)).toBeUndefined() + buffer.release() it "notifies ::onDidDestroy observers when the editor is destroyed", -> destroyObserverCalled = false @@ -5064,6 +5066,23 @@ describe "TextEditor", -> editor.destroy() expect(destroyObserverCalled).toBe true + it "does not blow up when query methods are called afterward", -> + editor.destroy() + editor.getGrammar() + editor.getLastCursor() + editor.lineTextForBufferRow(0) + + it "emits the destroy event after destroying the editor's buffer", -> + events = [] + editor.getBuffer().onDidDestroy -> + expect(editor.isDestroyed()).toBe(true) + events.push('buffer-destroyed') + editor.onDidDestroy -> + expect(buffer.isDestroyed()).toBe(true) + events.push('editor-destroyed') + editor.destroy() + expect(events).toEqual(['buffer-destroyed', 'editor-destroyed']) + describe ".joinLines()", -> describe "when no text is selected", -> describe "when the line below isn't empty", -> @@ -5141,7 +5160,7 @@ describe "TextEditor", -> expect(editor.lineTextForScreenRow(7)).toBe " while(items.length > 0) {" + editor.displayLayer.foldCharacter expect(editor.lineTextForScreenRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));" - it "duplicates all folded lines for empty selections on folded lines", -> + it "duplicates all folded lines for empty selections on lines containing folds", -> editor.foldBufferRow(4) editor.setCursorBufferPosition([4, 0]) @@ -5172,13 +5191,49 @@ describe "TextEditor", -> """ expect(editor.getSelectedBufferRange()).toEqual [[13, 0], [14, 2]] + it "only duplicates lines containing multiple selections once", -> + editor.setText(""" + aaaaaa + bbbbbb + cccccc + dddddd + """) + editor.setSelectedBufferRanges([ + [[0, 1], [0, 2]], + [[0, 3], [0, 4]], + [[2, 1], [2, 2]], + [[2, 3], [3, 1]], + [[3, 3], [3, 4]], + ]) + editor.duplicateLines() + expect(editor.getText()).toBe(""" + aaaaaa + aaaaaa + bbbbbb + cccccc + dddddd + cccccc + dddddd + """) + expect(editor.getSelectedBufferRanges()).toEqual([ + [[1, 1], [1, 2]], + [[1, 3], [1, 4]], + [[5, 1], [5, 2]], + [[5, 3], [6, 1]], + [[6, 3], [6, 4]], + ]) + describe ".shouldPromptToSave()", -> - it "returns false when an edit session's buffer is in use by more than one session", -> + it "returns true when buffer changed", -> jasmine.unspy(editor, 'shouldPromptToSave') expect(editor.shouldPromptToSave()).toBeFalsy() buffer.setText('changed') expect(editor.shouldPromptToSave()).toBeTruthy() + it "returns false when an edit session's buffer is in use by more than one session", -> + jasmine.unspy(editor, 'shouldPromptToSave') + buffer.setText('changed') + editor2 = null waitsForPromise -> atom.workspace.getActivePane().splitRight() @@ -5189,6 +5244,16 @@ describe "TextEditor", -> editor2.destroy() expect(editor.shouldPromptToSave()).toBeTruthy() + it "returns false when close of a window requested and edit session opened inside project", -> + jasmine.unspy(editor, 'shouldPromptToSave') + buffer.setText('changed') + expect(editor.shouldPromptToSave(windowCloseRequested: true, projectHasPaths: true)).toBeFalsy() + + it "returns true when close of a window requested and edit session opened without project", -> + jasmine.unspy(editor, 'shouldPromptToSave') + buffer.setText('changed') + expect(editor.shouldPromptToSave(windowCloseRequested: true, projectHasPaths: false)).toBeTruthy() + describe "when the editor contains surrogate pair characters", -> it "correctly backspaces over them", -> editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97') diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee index 68693dddc..40a3160da 100644 --- a/spec/theme-manager-spec.coffee +++ b/spec/theme-manager-spec.coffee @@ -1,6 +1,6 @@ path = require 'path' fs = require 'fs-plus' -temp = require 'temp' +temp = require('temp').track() describe "atom.themes", -> beforeEach -> @@ -8,6 +8,7 @@ describe "atom.themes", -> afterEach -> atom.themes.deactivateThemes() + temp.cleanupSync() describe "theme getters and setters", -> beforeEach -> @@ -170,7 +171,7 @@ describe "atom.themes", -> expect(styleElementAddedHandler).toHaveBeenCalled() element = document.querySelector('head style[source-path*="css.css"]') - expect(element.getAttribute('source-path')).toEqualPath atom.themes.stringToId(cssPath) + expect(element.getAttribute('source-path')).toEqualPath cssPath expect(element.textContent).toBe fs.readFileSync(cssPath, 'utf8') # doesn't append twice @@ -189,7 +190,7 @@ describe "atom.themes", -> expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1 element = document.querySelector('head style[source-path*="sample.less"]') - expect(element.getAttribute('source-path')).toEqualPath atom.themes.stringToId(lessPath) + expect(element.getAttribute('source-path')).toEqualPath lessPath expect(element.textContent).toBe """ #header { color: #4d926f; @@ -208,9 +209,9 @@ describe "atom.themes", -> it "supports requiring css and less stylesheets without an explicit extension", -> atom.themes.requireStylesheet path.join(__dirname, 'fixtures', 'css') - expect(document.querySelector('head style[source-path*="css.css"]').getAttribute('source-path')).toEqualPath atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('css.css')) + expect(document.querySelector('head style[source-path*="css.css"]').getAttribute('source-path')).toEqualPath atom.project.getDirectories()[0]?.resolve('css.css') atom.themes.requireStylesheet path.join(__dirname, 'fixtures', 'sample') - expect(document.querySelector('head style[source-path*="sample.less"]').getAttribute('source-path')).toEqualPath atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('sample.less')) + expect(document.querySelector('head style[source-path*="sample.less"]').getAttribute('source-path')).toEqualPath atom.project.getDirectories()[0]?.resolve('sample.less') document.querySelector('head style[source-path*="css.css"]').remove() document.querySelector('head style[source-path*="sample.less"]').remove() diff --git a/spec/tooltip-manager-spec.coffee b/spec/tooltip-manager-spec.coffee index 6bebd6e76..35e563dae 100644 --- a/spec/tooltip-manager-spec.coffee +++ b/spec/tooltip-manager-spec.coffee @@ -1,4 +1,6 @@ +{CompositeDisposable} = require 'atom' TooltipManager = require '../src/tooltip-manager' +Tooltip = require '../src/tooltip' _ = require 'underscore-plus' describe "TooltipManager", -> @@ -9,17 +11,27 @@ describe "TooltipManager", -> beforeEach -> manager = new TooltipManager(keymapManager: atom.keymaps, viewRegistry: atom.views) - element = document.createElement('div') - element.classList.add('foo') - jasmine.attachToDOM(element) + element = createElement 'foo' - hover = (element, fn) -> + createElement = (className) -> + el = document.createElement('div') + el.classList.add(className) + jasmine.attachToDOM(el) + el + + mouseEnter = (element) -> element.dispatchEvent(new CustomEvent('mouseenter', bubbles: false)) element.dispatchEvent(new CustomEvent('mouseover', bubbles: true)) - advanceClock(manager.hoverDefaults.delay.show) - fn() + + mouseLeave = (element) -> element.dispatchEvent(new CustomEvent('mouseleave', bubbles: false)) element.dispatchEvent(new CustomEvent('mouseout', bubbles: true)) + + hover = (element, fn) -> + mouseEnter(element) + advanceClock(manager.hoverDefaults.delay.show) + fn() + mouseLeave(element) advanceClock(manager.hoverDefaults.delay.hide) describe "::add(target, options)", -> @@ -29,6 +41,32 @@ describe "TooltipManager", -> hover element, -> expect(document.body.querySelector(".tooltip")).toHaveText("Title") + it "displays tooltips immediately when hovering over new elements once a tooltip has been displayed once", -> + disposables = new CompositeDisposable + element1 = createElement('foo') + disposables.add(manager.add element1, title: 'Title') + element2 = createElement('bar') + disposables.add(manager.add element2, title: 'Title') + element3 = createElement('baz') + disposables.add(manager.add element3, title: 'Title') + + hover element1, -> + expect(document.body.querySelector(".tooltip")).toBeNull() + + mouseEnter(element2) + expect(document.body.querySelector(".tooltip")).not.toBeNull() + mouseLeave(element2) + advanceClock(manager.hoverDefaults.delay.hide) + expect(document.body.querySelector(".tooltip")).toBeNull() + + advanceClock(Tooltip.FOLLOW_THROUGH_DURATION) + mouseEnter(element3) + expect(document.body.querySelector(".tooltip")).toBeNull() + advanceClock(manager.hoverDefaults.delay.show) + expect(document.body.querySelector(".tooltip")).not.toBeNull() + + disposables.dispose() + describe "when the trigger is 'manual'", -> it "creates a tooltip immediately and only hides it on dispose", -> disposable = manager.add element, title: "Title", trigger: "manual" @@ -147,8 +185,29 @@ describe "TooltipManager", -> describe "when the window is resized", -> it "hides the tooltips", -> - manager.add element, title: "Title" + disposable = manager.add element, title: "Title" hover element, -> - expect(document.body.querySelector(".tooltip")).toBeDefined() + expect(document.body.querySelector(".tooltip")).not.toBeNull() window.dispatchEvent(new CustomEvent('resize')) expect(document.body.querySelector(".tooltip")).toBeNull() + disposable.dispose() + + describe "findTooltips", -> + it "adds and remove tooltips correctly", -> + expect(manager.findTooltips(element).length).toBe(0) + disposable1 = manager.add element, title: "elem1" + expect(manager.findTooltips(element).length).toBe(1) + disposable2 = manager.add element, title: "elem2" + expect(manager.findTooltips(element).length).toBe(2) + disposable1.dispose() + expect(manager.findTooltips(element).length).toBe(1) + disposable2.dispose() + expect(manager.findTooltips(element).length).toBe(0) + + it "lets us hide tooltips programatically", -> + disposable = manager.add element, title: "Title" + hover element, -> + expect(document.body.querySelector(".tooltip")).not.toBeNull() + manager.findTooltips(element)[0].hide() + expect(document.body.querySelector(".tooltip")).toBeNull() + disposable.dispose() diff --git a/spec/update-process-env-spec.js b/spec/update-process-env-spec.js index 8c9db2b16..f730ae632 100644 --- a/spec/update-process-env-spec.js +++ b/spec/update-process-env-spec.js @@ -1,28 +1,38 @@ /** @babel */ /* eslint-env jasmine */ +import {it, fit, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers' import path from 'path' -import temp from 'temp' -import child_process from 'child_process' +import childProcess from 'child_process' import {updateProcessEnv, shouldGetEnvFromShell} from '../src/update-process-env' import dedent from 'dedent' +import {EventEmitter} from 'events' +import mockSpawn from 'mock-spawn' +const temp = require('temp').track() describe('updateProcessEnv(launchEnv)', function () { - let originalProcessEnv, originalProcessPlatform + let originalProcessEnv, originalProcessPlatform, originalSpawn, spawn beforeEach(function () { + originalSpawn = childProcess.spawn + spawn = mockSpawn() + childProcess.spawn = spawn originalProcessEnv = process.env originalProcessPlatform = process.platform process.env = {} }) afterEach(function () { + if (originalSpawn) { + childProcess.spawn = originalSpawn + } process.env = originalProcessEnv process.platform = originalProcessPlatform + temp.cleanupSync() }) describe('when the launch environment appears to come from a shell', function () { - it('updates process.env to match the launch environment', function () { + it('updates process.env to match the launch environment', async function () { process.env = { WILL_BE_DELETED: 'hi', NODE_ENV: 'the-node-env', @@ -32,7 +42,7 @@ describe('updateProcessEnv(launchEnv)', function () { const initialProcessEnv = process.env - updateProcessEnv({ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', TERM: 'xterm-something', KEY1: 'value1', KEY2: 'value2'}) + await updateProcessEnv({ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', TERM: 'xterm-something', KEY1: 'value1', KEY2: 'value2'}) expect(process.env).toEqual({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', @@ -50,7 +60,7 @@ describe('updateProcessEnv(launchEnv)', function () { expect(process.env).toBe(initialProcessEnv) }) - it('allows ATOM_HOME to be overwritten only if the new value is a valid path', function () { + it('allows ATOM_HOME to be overwritten only if the new value is a valid path', async function () { let newAtomHomePath = temp.mkdirSync('atom-home') process.env = { @@ -60,7 +70,7 @@ describe('updateProcessEnv(launchEnv)', function () { ATOM_HOME: '/the/atom/home' } - updateProcessEnv({ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir'}) + await updateProcessEnv({ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir'}) expect(process.env).toEqual({ PWD: '/the/dir', ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', @@ -69,7 +79,7 @@ describe('updateProcessEnv(launchEnv)', function () { ATOM_HOME: '/the/atom/home' }) - updateProcessEnv({ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', ATOM_HOME: path.join(newAtomHomePath, 'non-existent')}) + await updateProcessEnv({ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', ATOM_HOME: path.join(newAtomHomePath, 'non-existent')}) expect(process.env).toEqual({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', @@ -78,7 +88,7 @@ describe('updateProcessEnv(launchEnv)', function () { ATOM_HOME: '/the/atom/home' }) - updateProcessEnv({ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', ATOM_HOME: newAtomHomePath}) + await updateProcessEnv({ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', ATOM_HOME: newAtomHomePath}) expect(process.env).toEqual({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', @@ -88,7 +98,7 @@ describe('updateProcessEnv(launchEnv)', function () { }) }) - it('allows ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT to be preserved if set', function () { + it('allows ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT to be preserved if set', async function () { process.env = { WILL_BE_DELETED: 'hi', NODE_ENV: 'the-node-env', @@ -96,7 +106,7 @@ describe('updateProcessEnv(launchEnv)', function () { ATOM_HOME: '/the/atom/home' } - updateProcessEnv({ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: '/the/atom/home'}) + await updateProcessEnv({ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: '/the/atom/home'}) expect(process.env).toEqual({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', @@ -105,7 +115,7 @@ describe('updateProcessEnv(launchEnv)', function () { ATOM_HOME: '/the/atom/home' }) - updateProcessEnv({PWD: '/the/dir', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: '/the/atom/home'}) + await updateProcessEnv({PWD: '/the/dir', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: '/the/atom/home'}) expect(process.env).toEqual({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', @@ -115,7 +125,7 @@ describe('updateProcessEnv(launchEnv)', function () { }) }) - it('allows an existing env variable to be updated', function () { + it('allows an existing env variable to be updated', async function () { process.env = { WILL_BE_UPDATED: 'old-value', NODE_ENV: 'the-node-env', @@ -123,7 +133,7 @@ describe('updateProcessEnv(launchEnv)', function () { ATOM_HOME: '/the/atom/home' } - updateProcessEnv(process.env) + await updateProcessEnv(process.env) expect(process.env).toEqual(process.env) let updatedEnv = { @@ -135,27 +145,27 @@ describe('updateProcessEnv(launchEnv)', function () { PWD: '/the/dir' } - updateProcessEnv(updatedEnv) + await updateProcessEnv(updatedEnv) expect(process.env).toEqual(updatedEnv) }) }) describe('when the launch environment does not come from a shell', function () { - describe('on osx', function () { - it('updates process.env to match the environment in the user\'s login shell', function () { + describe('on macOS', function () { + it('updates process.env to match the environment in the user\'s login shell', async function () { + if (process.platform === 'win32') return // TestsThatFailOnWin32 + process.platform = 'darwin' process.env.SHELL = '/my/custom/bash' - - spyOn(child_process, 'spawnSync').andReturn({ - stdout: dedent` - FOO=BAR=BAZ=QUUX - TERM=xterm-something - PATH=/usr/bin:/bin:/usr/sbin:/sbin:/crazy/path - ` - }) - - updateProcessEnv(process.env) - expect(child_process.spawnSync.mostRecentCall.args[0]).toBe('/my/custom/bash') + spawn.setDefault(spawn.simple(0, dedent` + FOO=BAR=BAZ=QUUX + TERM=xterm-something + PATH=/usr/bin:/bin:/usr/sbin:/sbin:/crazy/path + `)) + await updateProcessEnv(process.env) + expect(spawn.calls.length).toBe(1) + expect(spawn.calls[0].command).toBe('/my/custom/bash') + expect(spawn.calls[0].args).toEqual(['-ilc', 'command env']) expect(process.env).toEqual({ FOO: 'BAR=BAZ=QUUX', TERM: 'xterm-something', @@ -163,25 +173,25 @@ describe('updateProcessEnv(launchEnv)', function () { }) // Doesn't error - updateProcessEnv(null) + await updateProcessEnv(null) }) }) describe('on linux', function () { - it('updates process.env to match the environment in the user\'s login shell', function () { + it('updates process.env to match the environment in the user\'s login shell', async function () { + if (process.platform === 'win32') return // TestsThatFailOnWin32 + process.platform = 'linux' process.env.SHELL = '/my/custom/bash' - - spyOn(child_process, 'spawnSync').andReturn({ - stdout: dedent` - FOO=BAR=BAZ=QUUX - TERM=xterm-something - PATH=/usr/bin:/bin:/usr/sbin:/sbin:/crazy/path - ` - }) - - updateProcessEnv(process.env) - expect(child_process.spawnSync.mostRecentCall.args[0]).toBe('/my/custom/bash') + spawn.setDefault(spawn.simple(0, dedent` + FOO=BAR=BAZ=QUUX + TERM=xterm-something + PATH=/usr/bin:/bin:/usr/sbin:/sbin:/crazy/path + `)) + await updateProcessEnv(process.env) + expect(spawn.calls.length).toBe(1) + expect(spawn.calls[0].command).toBe('/my/custom/bash') + expect(spawn.calls[0].args).toEqual(['-ilc', 'command env']) expect(process.env).toEqual({ FOO: 'BAR=BAZ=QUUX', TERM: 'xterm-something', @@ -189,24 +199,26 @@ describe('updateProcessEnv(launchEnv)', function () { }) // Doesn't error - updateProcessEnv(null) + await updateProcessEnv(null) }) }) describe('on windows', function () { - it('does not update process.env', function () { + it('does not update process.env', async function () { process.platform = 'win32' - spyOn(child_process, 'spawnSync') + spyOn(childProcess, 'spawn') process.env = {FOO: 'bar'} - updateProcessEnv(process.env) - expect(child_process.spawnSync).not.toHaveBeenCalled() + await updateProcessEnv(process.env) + expect(childProcess.spawn).not.toHaveBeenCalled() expect(process.env).toEqual({FOO: 'bar'}) }) }) describe('shouldGetEnvFromShell()', function () { it('indicates when the environment should be fetched from the shell', function () { + if (process.platform === 'win32') return // TestsThatFailOnWin32 + process.platform = 'darwin' expect(shouldGetEnvFromShell({SHELL: '/bin/sh'})).toBe(true) expect(shouldGetEnvFromShell({SHELL: '/usr/local/bin/sh'})).toBe(true) diff --git a/spec/window-event-handler-spec.coffee b/spec/window-event-handler-spec.coffee index 8e08fec35..e9a7894c3 100644 --- a/spec/window-event-handler-spec.coffee +++ b/spec/window-event-handler-spec.coffee @@ -23,6 +23,7 @@ describe "WindowEventHandler", -> describe "when the window is loaded", -> it "doesn't have .is-blurred on the body tag", -> + return if process.platform is 'win32' #Win32TestFailures - can not steal focus expect(document.body.className).not.toMatch("is-blurred") describe "when the window is blurred", -> diff --git a/spec/workspace-element-spec.coffee b/spec/workspace-element-spec.coffee index 9ffa3621a..6bcb24eed 100644 --- a/spec/workspace-element-spec.coffee +++ b/spec/workspace-element-spec.coffee @@ -4,6 +4,9 @@ temp = require('temp').track() {Disposable} = require 'event-kit' describe "WorkspaceElement", -> + afterEach -> + temp.cleanupSync() + describe "when the workspace element is focused", -> it "transfers focus to the active pane", -> workspaceElement = atom.views.getView(atom.workspace) diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index 61f1e8266..08afa6239 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -1,5 +1,5 @@ path = require 'path' -temp = require 'temp' +temp = require('temp').track() TextEditor = require '../src/text-editor' Workspace = require '../src/workspace' Project = require '../src/project' @@ -19,6 +19,9 @@ describe "Workspace", -> atom.project.setPaths([atom.project.getDirectories()[0]?.resolve('dir')]) waits(1) + afterEach -> + temp.cleanupSync() + describe "serialization", -> simulateReload = -> workspaceState = atom.workspace.serialize() @@ -489,6 +492,7 @@ describe "Workspace", -> expect(item).toEqual {bar: "bar://baz"} it "adds the file to the application's recent documents list", -> + return unless process.platform is 'darwin' # Feature only supported on macOS spyOn(atom.applicationDelegate, 'addRecentDocument') waitsForPromise -> @@ -881,12 +885,44 @@ describe "Workspace", -> expect(coffeePackage.loadGrammarsSync.callCount).toBe 1 describe "document.title", -> - describe "when the project has no path", -> + describe "when there is no item open", -> it "sets the title to 'untitled'", -> - atom.project.setPaths([]) expect(document.title).toMatch ///^untitled/// - describe "when the project has a path", -> + describe "when the active pane item's path is not inside a project path", -> + beforeEach -> + waitsForPromise -> + atom.workspace.open('b').then -> + atom.project.setPaths([]) + + it "sets the title to the pane item's title plus the item's path", -> + item = atom.workspace.getActivePaneItem() + pathEscaped = fs.tildify(escapeStringRegex(path.dirname(item.getPath()))) + expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}/// + + describe "when the title of the active pane item changes", -> + it "updates the window title based on the item's new title", -> + editor = atom.workspace.getActivePaneItem() + editor.buffer.setPath(path.join(temp.dir, 'hi')) + pathEscaped = fs.tildify(escapeStringRegex(path.dirname(editor.getPath()))) + expect(document.title).toMatch ///^#{editor.getTitle()}\ \u2014\ #{pathEscaped}/// + + describe "when the active pane's item changes", -> + it "updates the title to the new item's title plus the project path", -> + atom.workspace.getActivePane().activateNextItem() + item = atom.workspace.getActivePaneItem() + pathEscaped = fs.tildify(escapeStringRegex(path.dirname(item.getPath()))) + expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}/// + + describe "when an inactive pane's item changes", -> + it "does not update the title", -> + pane = atom.workspace.getActivePane() + pane.splitRight() + initialTitle = document.title + pane.activateNextItem() + expect(document.title).toBe initialTitle + + describe "when the active pane item is inside a project path", -> beforeEach -> waitsForPromise -> atom.workspace.open('b') @@ -900,7 +936,7 @@ describe "Workspace", -> describe "when the title of the active pane item changes", -> it "updates the window title based on the item's new title", -> editor = atom.workspace.getActivePaneItem() - editor.buffer.setPath(path.join(temp.dir, 'hi')) + editor.buffer.setPath(path.join(atom.project.getPaths()[0], 'hi')) pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) expect(document.title).toMatch ///^#{editor.getTitle()}\ \u2014\ #{pathEscaped}/// @@ -912,11 +948,10 @@ describe "Workspace", -> expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}/// describe "when the last pane item is removed", -> - it "updates the title to contain the project's path", -> + it "updates the title to be untitled", -> atom.workspace.getActivePane().destroy() expect(atom.workspace.getActivePaneItem()).toBeUndefined() - pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) - expect(document.title).toMatch ///^#{pathEscaped}/// + expect(document.title).toMatch ///^untitled/// describe "when an inactive pane's item changes", -> it "does not update the title", -> @@ -1139,6 +1174,7 @@ describe "Workspace", -> range: [[2, 6], [2, 11]] it "works on evil filenames", -> + atom.config.set('core.excludeVcsIgnoredPaths', false) platform.generateEvilFiles() atom.project.setPaths([path.join(__dirname, 'fixtures', 'evil-files')]) paths = [] @@ -1224,7 +1260,7 @@ describe "Workspace", -> expect(matches.length).toBe 1 it "includes files and folders that begin with a '.'", -> - projectPath = temp.mkdirSync() + projectPath = temp.mkdirSync('atom-spec-workspace') filePath = path.join(projectPath, '.text') fs.writeFileSync(filePath, 'match this') atom.project.setPaths([projectPath]) diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index 5d908a2c9..185db5059 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -112,6 +112,7 @@ class ApplicationDelegate loadSettings = getWindowLoadSettings() loadSettings['initialPaths'] = paths setWindowLoadSettings(loadSettings) + ipcRenderer.send("did-change-paths") setAutoHideWindowMenuBar: (autoHide) -> ipcHelpers.call('window-method', 'setAutoHideMenuBar', autoHide) @@ -244,6 +245,17 @@ class ApplicationDelegate didCancelWindowUnload: -> ipcRenderer.send('did-cancel-window-unload') + onDidChangeHistoryManager: (callback) -> + outerCallback = (event, message) -> + callback(event) + + ipcRenderer.on('did-change-history-manager', outerCallback) + new Disposable -> + ipcRenderer.removeListener('did-change-history-manager', outerCallback) + + didChangeHistoryManager: -> + ipcRenderer.send('did-change-history-manager') + openExternal: (url) -> shell.openExternal(url) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 26e9b5029..62ab5937d 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -13,6 +13,7 @@ StateStore = require './state-store' StorageFolder = require './storage-folder' {getWindowLoadSettings} = require './window-load-settings-helpers' registerDefaultCommands = require './register-default-commands' +{updateProcessEnv} = require './update-process-env' DeserializerManager = require './deserializer-manager' ViewRegistry = require './view-registry' @@ -22,6 +23,8 @@ KeymapManager = require './keymap-extensions' TooltipManager = require './tooltip-manager' CommandRegistry = require './command-registry' GrammarRegistry = require './grammar-registry' +{HistoryManager, HistoryProject} = require './history-manager' +ReopenProjectMenuManager = require './reopen-project-menu-manager' StyleManager = require './style-manager' PackageManager = require './package-manager' ThemeManager = require './theme-manager' @@ -94,6 +97,9 @@ class AtomEnvironment extends Model # Public: A {GrammarRegistry} instance grammars: null + # Public: A {HistoryManager} instance + history: null + # Public: A {PackageManager} instance packages: null @@ -226,15 +232,13 @@ class AtomEnvironment extends Model @observeAutoHideMenuBar() - checkPortableHomeWritable = => - responseChannel = "check-portable-home-writable-response" - ipcRenderer.on responseChannel, (event, response) -> - ipcRenderer.removeAllListeners(responseChannel) - @notifications.addWarning("#{response.message.replace(/([\\\.+\\-_#!])/g, '\\$1')}") if not response.writable - @disposables.add new Disposable -> ipcRenderer.removeAllListeners(responseChannel) - ipcRenderer.send('check-portable-home-writable', responseChannel) + @history = new HistoryManager({@project, @commands, localStorage}) + # Keep instances of HistoryManager in sync + @history.onDidChangeProjects (e) => + @applicationDelegate.didChangeHistoryManager() unless e.reloaded + @disposables.add @applicationDelegate.onDidChangeHistoryManager(=> @history.loadState()) - checkPortableHomeWritable() + new ReopenProjectMenuManager({@menu, @commands, @history, @config, open: (paths) => @open(pathsToOpen: paths)}) attachSaveStateListeners: -> saveState = _.debounce((=> @@ -280,13 +284,13 @@ class AtomEnvironment extends Model @workspace.addOpener (uri) => switch uri when 'atom://.atom/stylesheet' - @workspace.open(@styles.getUserStyleSheetPath()) + @workspace.openTextFile(@styles.getUserStyleSheetPath()) when 'atom://.atom/keymap' - @workspace.open(@keymaps.getUserKeymapPath()) + @workspace.openTextFile(@keymaps.getUserKeymapPath()) when 'atom://.atom/config' - @workspace.open(@config.getUserConfigPath()) + @workspace.openTextFile(@config.getUserConfigPath()) when 'atom://.atom/init-script' - @workspace.open(@getUserInitScriptPath()) + @workspace.openTextFile(@getUserInitScriptPath()) registerDefaultTargetForKeymaps: -> @keymaps.defaultTarget = @views.getView(@workspace) @@ -643,7 +647,7 @@ class AtomEnvironment extends Model restoreWindowDimensions: -> unless @windowDimensions? and @isValidDimensions(@windowDimensions) @windowDimensions = @getDefaultWindowDimensions() - @setWindowDimensions(@windowDimensions).then -> @windowDimensions + @setWindowDimensions(@windowDimensions).then => @windowDimensions restoreWindowBackground: -> if backgroundColor = window.localStorage.getItem('atom:window-background-color') @@ -662,7 +666,11 @@ class AtomEnvironment extends Model # Call this method when establishing a real application window. startEditorWindow: -> @unloaded = false - @loadState().then (state) => + updateProcessEnvPromise = updateProcessEnv(@getLoadSettings().env) + updateProcessEnvPromise.then => + @packages.triggerActivationHook('core:loaded-shell-environment') + + loadStatePromise = @loadState().then (state) => @windowDimensions = state?.windowDimensions @displayWindow().then => @commandInstaller.installAtomCommand false, (error) -> @@ -706,6 +714,8 @@ class AtomEnvironment extends Model @openInitialEmptyEditorIfNecessary() + Promise.all([loadStatePromise, updateProcessEnvPromise]) + serialize: (options) -> version: @constructor.version project: @project.serialize(options) diff --git a/src/atom-paths.js b/src/atom-paths.js new file mode 100644 index 000000000..6a5c107b3 --- /dev/null +++ b/src/atom-paths.js @@ -0,0 +1,62 @@ +/** @babel */ + +const fs = require('fs-plus') +const path = require('path') + +const hasWriteAccess = (dir) => { + const testFilePath = path.join(dir, 'write.test') + try { + fs.writeFileSync(testFilePath, new Date().toISOString(), { flag: 'w+' }) + fs.unlinkSync(testFilePath) + return true + } catch (err) { + return false + } +} + +const getAppDirectory = () => { + switch (process.platform) { + case 'darwin': + return path.join(process.execPath.substring(0, process.execPath.indexOf('.app')), '..') + case 'linux': + case 'win32': + return path.join(process.execPath, '..') + } +} + +module.exports = { + setAtomHome: (homePath) => { + // When a read-writeable .atom folder exists above app use that + const portableHomePath = path.join(getAppDirectory(), '.atom') + if (fs.existsSync(portableHomePath)) { + if (hasWriteAccess(portableHomePath)) { + process.env.ATOM_HOME = portableHomePath + } else { + // A path exists so it was intended to be used but we didn't have rights, so warn. + console.log(`Insufficient permission to portable Atom home "${portableHomePath}".`) + } + } + + // Check ATOM_HOME environment variable next + if (process.env.ATOM_HOME !== undefined) { + return + } + + // Fall back to default .atom folder in users home folder + process.env.ATOM_HOME = path.join(homePath, '.atom') + }, + + setUserData: (app) => { + const electronUserDataPath = path.join(process.env.ATOM_HOME, 'electronUserData') + if (fs.existsSync(electronUserDataPath)) { + if (hasWriteAccess(electronUserDataPath)) { + app.setPath('userData', electronUserDataPath) + } else { + // A path exists so it was intended to be used but we didn't have rights, so warn. + console.log(`Insufficient permission to Electron user data "${electronUserDataPath}".`) + } + } + }, + + getAppDirectory: getAppDirectory +} diff --git a/src/buffered-node-process.coffee b/src/buffered-node-process.coffee deleted file mode 100644 index 4987f84a4..000000000 --- a/src/buffered-node-process.coffee +++ /dev/null @@ -1,48 +0,0 @@ -BufferedProcess = require './buffered-process' -path = require 'path' - -# Extended: Like {BufferedProcess}, but accepts a Node script as the command -# to run. -# -# This is necessary on Windows since it doesn't support shebang `#!` lines. -# -# ## Examples -# -# ```coffee -# {BufferedNodeProcess} = require 'atom' -# ``` -module.exports = -class BufferedNodeProcess extends BufferedProcess - - # Public: Runs the given Node script by spawning a new child process. - # - # * `options` An {Object} with the following keys: - # * `command` The {String} path to the JavaScript script to execute. - # * `args` The {Array} of arguments to pass to the script (optional). - # * `options` The options {Object} to pass to Node's `ChildProcess.spawn` - # method (optional). - # * `stdout` The callback {Function} that receives a single argument which - # contains the standard output from the command. The callback is - # called as data is received but it's buffered to ensure only - # complete lines are passed until the source stream closes. After - # the source stream has closed all remaining data is sent in a - # final call (optional). - # * `stderr` The callback {Function} that receives a single argument which - # contains the standard error output from the command. The - # callback is called as data is received but it's buffered to - # ensure only complete lines are passed until the source stream - # closes. After the source stream has closed all remaining data - # is sent in a final call (optional). - # * `exit` The callback {Function} which receives a single argument - # containing the exit status (optional). - constructor: ({command, args, options, stdout, stderr, exit}) -> - options ?= {} - options.env ?= Object.create(process.env) - options.env['ELECTRON_RUN_AS_NODE'] = 1 - options.env['ELECTRON_NO_ATTACH_CONSOLE'] = 1 - - args = args?.slice() ? [] - args.unshift(command) - args.unshift('--no-deprecation') - - super({command: process.execPath, args, options, stdout, stderr, exit}) diff --git a/src/buffered-node-process.js b/src/buffered-node-process.js new file mode 100644 index 000000000..86b0c5747 --- /dev/null +++ b/src/buffered-node-process.js @@ -0,0 +1,56 @@ +/** @babel */ + +import BufferedProcess from './buffered-process' + +// Extended: Like {BufferedProcess}, but accepts a Node script as the command +// to run. +// +// This is necessary on Windows since it doesn't support shebang `#!` lines. +// +// ## Examples +// +// ```js +// const {BufferedNodeProcess} = require('atom') +// ``` +export default class BufferedNodeProcess extends BufferedProcess { + + // Public: Runs the given Node script by spawning a new child process. + // + // * `options` An {Object} with the following keys: + // * `command` The {String} path to the JavaScript script to execute. + // * `args` The {Array} of arguments to pass to the script (optional). + // * `options` The options {Object} to pass to Node's `ChildProcess.spawn` + // method (optional). + // * `stdout` The callback {Function} that receives a single argument which + // contains the standard output from the command. The callback is + // called as data is received but it's buffered to ensure only + // complete lines are passed until the source stream closes. After + // the source stream has closed all remaining data is sent in a + // final call (optional). + // * `stderr` The callback {Function} that receives a single argument which + // contains the standard error output from the command. The + // callback is called as data is received but it's buffered to + // ensure only complete lines are passed until the source stream + // closes. After the source stream has closed all remaining data + // is sent in a final call (optional). + // * `exit` The callback {Function} which receives a single argument + // containing the exit status (optional). + constructor ({command, args, options = {}, stdout, stderr, exit}) { + options.env = options.env || Object.create(process.env) + options.env.ELECTRON_RUN_AS_NODE = 1 + options.env.ELECTRON_NO_ATTACH_CONSOLE = 1 + + args = args ? args.slice() : [] + args.unshift(command) + args.unshift('--no-deprecation') + + super({ + command: process.execPath, + args, + options, + stdout, + stderr, + exit + }) + } +} diff --git a/src/buffered-process.coffee b/src/buffered-process.coffee deleted file mode 100644 index 07fcfb664..000000000 --- a/src/buffered-process.coffee +++ /dev/null @@ -1,246 +0,0 @@ -_ = require 'underscore-plus' -ChildProcess = require 'child_process' -{Emitter} = require 'event-kit' -path = require 'path' - -# Extended: A wrapper which provides standard error/output line buffering for -# Node's ChildProcess. -# -# ## Examples -# -# ```coffee -# {BufferedProcess} = require 'atom' -# -# command = 'ps' -# args = ['-ef'] -# stdout = (output) -> console.log(output) -# exit = (code) -> console.log("ps -ef exited with #{code}") -# process = new BufferedProcess({command, args, stdout, exit}) -# ``` -module.exports = -class BufferedProcess - ### - Section: Construction - ### - - # Public: Runs the given command by spawning a new child process. - # - # * `options` An {Object} with the following keys: - # * `command` The {String} command to execute. - # * `args` The {Array} of arguments to pass to the command (optional). - # * `options` {Object} (optional) The options {Object} to pass to Node's - # `ChildProcess.spawn` method. - # * `stdout` {Function} (optional) The callback that receives a single - # argument which contains the standard output from the command. The - # callback is called as data is received but it's buffered to ensure only - # complete lines are passed until the source stream closes. After the - # source stream has closed all remaining data is sent in a final call. - # * `data` {String} - # * `stderr` {Function} (optional) The callback that receives a single - # argument which contains the standard error output from the command. The - # callback is called as data is received but it's buffered to ensure only - # complete lines are passed until the source stream closes. After the - # source stream has closed all remaining data is sent in a final call. - # * `data` {String} - # * `exit` {Function} (optional) The callback which receives a single - # argument containing the exit status. - # * `code` {Number} - constructor: ({command, args, options, stdout, stderr, exit}={}) -> - @emitter = new Emitter - options ?= {} - @command = command - # Related to joyent/node#2318 - if process.platform is 'win32' and not options.shell? - # Quote all arguments and escapes inner quotes - if args? - cmdArgs = args.filter (arg) -> arg? - cmdArgs = cmdArgs.map (arg) => - if @isExplorerCommand(command) and /^\/[a-zA-Z]+,.*$/.test(arg) - # Don't wrap /root,C:\folder style arguments to explorer calls in - # quotes since they will not be interpreted correctly if they are - arg - else - "\"#{arg.toString().replace(/"/g, '\\"')}\"" - else - cmdArgs = [] - if /\s/.test(command) - cmdArgs.unshift("\"#{command}\"") - else - cmdArgs.unshift(command) - cmdArgs = ['/s', '/d', '/c', "\"#{cmdArgs.join(' ')}\""] - cmdOptions = _.clone(options) - cmdOptions.windowsVerbatimArguments = true - @spawn(@getCmdPath(), cmdArgs, cmdOptions) - else - @spawn(command, args, options) - - @killed = false - @handleEvents(stdout, stderr, exit) - - ### - Section: Event Subscription - ### - - # Public: Will call your callback when an error will be raised by the process. - # Usually this is due to the command not being available or not on the PATH. - # You can call `handle()` on the object passed to your callback to indicate - # that you have handled this error. - # - # * `callback` {Function} callback - # * `errorObject` {Object} - # * `error` {Object} the error object - # * `handle` {Function} call this to indicate you have handled the error. - # The error will not be thrown if this function is called. - # - # Returns a {Disposable} - onWillThrowError: (callback) -> - @emitter.on 'will-throw-error', callback - - ### - Section: Helper Methods - ### - - # Helper method to pass data line by line. - # - # * `stream` The Stream to read from. - # * `onLines` The callback to call with each line of data. - # * `onDone` The callback to call when the stream has closed. - bufferStream: (stream, onLines, onDone) -> - stream.setEncoding('utf8') - buffered = '' - - stream.on 'data', (data) => - return if @killed - bufferedLength = buffered.length - buffered += data - lastNewlineIndex = data.lastIndexOf('\n') - if lastNewlineIndex isnt -1 - lineLength = lastNewlineIndex + bufferedLength + 1 - onLines(buffered.substring(0, lineLength)) - buffered = buffered.substring(lineLength) - - stream.on 'close', => - return if @killed - onLines(buffered) if buffered.length > 0 - onDone() - - # Kill all child processes of the spawned cmd.exe process on Windows. - # - # This is required since killing the cmd.exe does not terminate child - # processes. - killOnWindows: -> - return unless @process? - - parentPid = @process.pid - cmd = 'wmic' - args = [ - 'process' - 'where' - "(ParentProcessId=#{parentPid})" - 'get' - 'processid' - ] - - try - wmicProcess = ChildProcess.spawn(cmd, args) - catch spawnError - @killProcess() - return - - wmicProcess.on 'error', -> # ignore errors - output = '' - wmicProcess.stdout.on 'data', (data) -> output += data - wmicProcess.stdout.on 'close', => - pidsToKill = output.split(/\s+/) - .filter (pid) -> /^\d+$/.test(pid) - .map (pid) -> parseInt(pid) - .filter (pid) -> pid isnt parentPid and 0 < pid < Infinity - - for pid in pidsToKill - try - process.kill(pid) - @killProcess() - - killProcess: -> - @process?.kill() - @process = null - - isExplorerCommand: (command) -> - if command is 'explorer.exe' or command is 'explorer' - true - else if process.env.SystemRoot - command is path.join(process.env.SystemRoot, 'explorer.exe') or command is path.join(process.env.SystemRoot, 'explorer') - else - false - - getCmdPath: -> - if process.env.comspec - process.env.comspec - else if process.env.SystemRoot - path.join(process.env.SystemRoot, 'System32', 'cmd.exe') - else - 'cmd.exe' - - # Public: Terminate the process. - kill: -> - return if @killed - - @killed = true - if process.platform is 'win32' - @killOnWindows() - else - @killProcess() - - undefined - - spawn: (command, args, options) -> - try - @process = ChildProcess.spawn(command, args, options) - catch spawnError - process.nextTick => @handleError(spawnError) - - handleEvents: (stdout, stderr, exit) -> - return unless @process? - - stdoutClosed = true - stderrClosed = true - processExited = true - exitCode = 0 - triggerExitCallback = -> - return if @killed - if stdoutClosed and stderrClosed and processExited - exit?(exitCode) - - if stdout - stdoutClosed = false - @bufferStream @process.stdout, stdout, -> - stdoutClosed = true - triggerExitCallback() - - if stderr - stderrClosed = false - @bufferStream @process.stderr, stderr, -> - stderrClosed = true - triggerExitCallback() - - if exit - processExited = false - @process.on 'exit', (code) -> - exitCode = code - processExited = true - triggerExitCallback() - - @process.on 'error', (error) => @handleError(error) - return - - handleError: (error) -> - handled = false - handle = -> handled = true - - @emitter.emit 'will-throw-error', {error, handle} - - if error.code is 'ENOENT' and error.syscall.indexOf('spawn') is 0 - error = new Error("Failed to spawn command `#{@command}`. Make sure `#{@command}` is installed and on your PATH", error.path) - error.name = 'BufferedProcessError' - - throw error unless handled diff --git a/src/buffered-process.js b/src/buffered-process.js new file mode 100644 index 000000000..44e501d4d --- /dev/null +++ b/src/buffered-process.js @@ -0,0 +1,298 @@ +/** @babel */ + +import _ from 'underscore-plus' +import ChildProcess from 'child_process' +import {Emitter} from 'event-kit' +import path from 'path' + +// Extended: A wrapper which provides standard error/output line buffering for +// Node's ChildProcess. +// +// ## Examples +// +// ```js +// {BufferedProcess} = require('atom') +// +// const command = 'ps' +// const args = ['-ef'] +// const stdout = (output) => console.log(output) +// const exit = (code) => console.log("ps -ef exited with #{code}") +// const process = new BufferedProcess({command, args, stdout, exit}) +// ``` +export default class BufferedProcess { + /* + Section: Construction + */ + + // Public: Runs the given command by spawning a new child process. + // + // * `options` An {Object} with the following keys: + // * `command` The {String} command to execute. + // * `args` The {Array} of arguments to pass to the command (optional). + // * `options` {Object} (optional) The options {Object} to pass to Node's + // `ChildProcess.spawn` method. + // * `stdout` {Function} (optional) The callback that receives a single + // argument which contains the standard output from the command. The + // callback is called as data is received but it's buffered to ensure only + // complete lines are passed until the source stream closes. After the + // source stream has closed all remaining data is sent in a final call. + // * `data` {String} + // * `stderr` {Function} (optional) The callback that receives a single + // argument which contains the standard error output from the command. The + // callback is called as data is received but it's buffered to ensure only + // complete lines are passed until the source stream closes. After the + // source stream has closed all remaining data is sent in a final call. + // * `data` {String} + // * `exit` {Function} (optional) The callback which receives a single + // argument containing the exit status. + // * `code` {Number} + constructor ({command, args, options = {}, stdout, stderr, exit} = {}) { + this.emitter = new Emitter() + this.command = command + // Related to joyent/node#2318 + if (process.platform === 'win32' && options.shell === undefined) { + this.spawnWithEscapedWindowsArgs(command, args, options) + } else { + this.spawn(command, args, options) + } + + this.killed = false + this.handleEvents(stdout, stderr, exit) + } + + // Windows has a bunch of special rules that node still doesn't take care of for you + spawnWithEscapedWindowsArgs (command, args, options) { + let cmdArgs = [] + // Quote all arguments and escapes inner quotes + if (args) { + cmdArgs = args.filter((arg) => arg != null) + .map((arg) => { + if (this.isExplorerCommand(command) && /^\/[a-zA-Z]+,.*$/.test(arg)) { + // Don't wrap /root,C:\folder style arguments to explorer calls in + // quotes since they will not be interpreted correctly if they are + return arg + } else { + // Escape double quotes by putting a backslash in front of them + return `\"${arg.toString().replace(/"/g, '\\"')}\"` + } + }) + } + + // The command itself is quoted if it contains spaces, &, ^, | or # chars + cmdArgs.unshift(/\s|&|\^|\(|\)|\||#/.test(command) ? `\"${command}\"` : command) + + const cmdOptions = _.clone(options) + cmdOptions.windowsVerbatimArguments = true + + this.spawn(this.getCmdPath(), ['/s', '/d', '/c', `\"${cmdArgs.join(' ')}\"`], cmdOptions) + } + + /* + Section: Event Subscription + */ + + // Public: Will call your callback when an error will be raised by the process. + // Usually this is due to the command not being available or not on the PATH. + // You can call `handle()` on the object passed to your callback to indicate + // that you have handled this error. + // + // * `callback` {Function} callback + // * `errorObject` {Object} + // * `error` {Object} the error object + // * `handle` {Function} call this to indicate you have handled the error. + // The error will not be thrown if this function is called. + // + // Returns a {Disposable} + onWillThrowError (callback) { + return this.emitter.on('will-throw-error', callback) + } + + /* + Section: Helper Methods + */ + + // Helper method to pass data line by line. + // + // * `stream` The Stream to read from. + // * `onLines` The callback to call with each line of data. + // * `onDone` The callback to call when the stream has closed. + bufferStream (stream, onLines, onDone) { + stream.setEncoding('utf8') + let buffered = '' + + stream.on('data', (data) => { + if (this.killed) return + + let bufferedLength = buffered.length + buffered += data + let lastNewlineIndex = data.lastIndexOf('\n') + + if (lastNewlineIndex !== -1) { + let lineLength = lastNewlineIndex + bufferedLength + 1 + onLines(buffered.substring(0, lineLength)) + buffered = buffered.substring(lineLength) + } + }) + + stream.on('close', () => { + if (this.killed) return + if (buffered.length > 0) onLines(buffered) + onDone() + }) + } + + // Kill all child processes of the spawned cmd.exe process on Windows. + // + // This is required since killing the cmd.exe does not terminate child + // processes. + killOnWindows () { + if (!this.process) return + + const parentPid = this.process.pid + const cmd = 'wmic' + const args = [ + 'process', + 'where', + `(ParentProcessId=${parentPid})`, + 'get', + 'processid' + ] + + let wmicProcess + + try { + wmicProcess = ChildProcess.spawn(cmd, args) + } catch (spawnError) { + this.killProcess() + return + } + + wmicProcess.on('error', () => {}) // ignore errors + + let output = '' + wmicProcess.stdout.on('data', (data) => { + output += data + }) + wmicProcess.stdout.on('close', () => { + const pidsToKill = output.split(/\s+/) + .filter((pid) => /^\d+$/.test(pid)) + .map((pid) => parseInt(pid)) + .filter((pid) => pid !== parentPid && pid > 0 && pid < Infinity) + + for (let pid of pidsToKill) { + try { + process.kill(pid) + } catch (error) {} + } + + this.killProcess() + }) + } + + killProcess () { + if (this.process) this.process.kill() + this.process = null + } + + isExplorerCommand (command) { + if (command === 'explorer.exe' || command === 'explorer') { + return true + } else if (process.env.SystemRoot) { + return command === path.join(process.env.SystemRoot, 'explorer.exe') || command === path.join(process.env.SystemRoot, 'explorer') + } else { + return false + } + } + + getCmdPath () { + if (process.env.comspec) { + return process.env.comspec + } else if (process.env.SystemRoot) { + return path.join(process.env.SystemRoot, 'System32', 'cmd.exe') + } else { + return 'cmd.exe' + } + } + + // Public: Terminate the process. + kill () { + if (this.killed) return + + this.killed = true + if (process.platform === 'win32') { + this.killOnWindows() + } else { + this.killProcess() + } + } + + spawn (command, args, options) { + try { + this.process = ChildProcess.spawn(command, args, options) + } catch (spawnError) { + process.nextTick(() => this.handleError(spawnError)) + } + } + + handleEvents (stdout, stderr, exit) { + if (!this.process) return + + const triggerExitCallback = () => { + if (this.killed) return + if (stdoutClosed && stderrClosed && processExited && typeof exit === 'function') { + exit(exitCode) + } + } + + let stdoutClosed = true + let stderrClosed = true + let processExited = true + let exitCode = 0 + + if (stdout) { + stdoutClosed = false + this.bufferStream(this.process.stdout, stdout, () => { + stdoutClosed = true + triggerExitCallback() + }) + } + + if (stderr) { + stderrClosed = false + this.bufferStream(this.process.stderr, stderr, () => { + stderrClosed = true + triggerExitCallback() + }) + } + + if (exit) { + processExited = false + this.process.on('exit', (code) => { + exitCode = code + processExited = true + triggerExitCallback() + }) + } + + this.process.on('error', (error) => { + this.handleError(error) + }) + } + + handleError (error) { + let handled = false + + const handle = () => { + handled = true + } + + this.emitter.emit('will-throw-error', {error, handle}) + + if (error.code === 'ENOENT' && error.syscall.indexOf('spawn') === 0) { + error = new Error(`Failed to spawn command \`${this.command}\`. Make sure \`${this.command}\` is installed and on your PATH`, error.path) + error.name = 'BufferedProcessError' + } + + if (!handled) throw error + } +} diff --git a/src/compile-cache.js b/src/compile-cache.js index ad1bd0a85..8a4451d90 100644 --- a/src/compile-cache.js +++ b/src/compile-cache.js @@ -7,12 +7,26 @@ var path = require('path') var fs = require('fs-plus') + +var PackageTranspilationRegistry = require('./package-transpilation-registry') var CSON = null +var packageTranspilationRegistry = new PackageTranspilationRegistry() + var COMPILERS = { - '.js': require('./babel'), - '.ts': require('./typescript'), - '.coffee': require('./coffee-script') + '.js': packageTranspilationRegistry.wrapTranspiler(require('./babel')), + '.ts': packageTranspilationRegistry.wrapTranspiler(require('./typescript')), + '.coffee': packageTranspilationRegistry.wrapTranspiler(require('./coffee-script')) +} + +exports.addTranspilerConfigForPath = function (packagePath, packageName, packageMeta, config) { + packagePath = fs.realpathSync(packagePath) + packageTranspilationRegistry.addTranspilerConfigForPath(packagePath, packageName, packageMeta, config) +} + +exports.removeTranspilerConfigForPath = function (packagePath) { + packagePath = fs.realpathSync(packagePath) + packageTranspilationRegistry.removeTranspilerConfigForPath(packagePath) } var cacheStats = {} @@ -118,6 +132,7 @@ require('source-map-support').install({ } var compiler = COMPILERS[path.extname(filePath)] + if (!compiler) compiler = COMPILERS['.js'] try { var fileData = readCachedJavascript(compiler.getCachePath(sourceCode, filePath)) diff --git a/src/config-schema.js b/src/config-schema.js index add21b317..b43cb9e10 100644 --- a/src/config-schema.js +++ b/src/config-schema.js @@ -12,7 +12,7 @@ const configSchema = { properties: { ignoredNames: { type: 'array', - default: ['.git', '.hg', '.svn', '.DS_Store', '._*', 'Thumbs.db'], + default: ['.git', '.hg', '.svn', '.DS_Store', '._*', 'Thumbs.db', 'desktop.ini'], items: { type: 'string' }, @@ -85,6 +85,8 @@ const configSchema = { default: 'utf8', enum: [ 'cp437', + 'cp850', + 'cp866', 'eucjp', 'euckr', 'gbk', @@ -117,15 +119,24 @@ const configSchema = { 'windows1255', 'windows1256', 'windows1257', - 'windows1258', - 'windows866' + 'windows1258' ] }, openEmptyEditorOnStart: { - description: 'Automatically open an empty editor on startup.', + description: 'When checked opens an untitled editor when loading a blank environment (such as with _File > New Window_ or when "Restore Previous Windows On Start" is unchecked); otherwise no editor is opened when loading a blank environment. This setting has no effect when restoring a previous state.', type: 'boolean', default: true }, + restorePreviousWindowsOnStart: { + description: 'When checked restores the last state of all Atom windows when started from the icon or `atom` by itself from the command line; otherwise a blank environment is loaded.', + type: 'boolean', + default: true + }, + reopenProjectMenuCount: { + description: 'How many recent projects to show in the Reopen Project menu.', + type: 'integer', + default: 15 + }, automaticallyUpdate: { description: 'Automatically update Atom when a new release is available.', type: 'boolean', @@ -159,7 +170,7 @@ const configSchema = { warnOnLargeFileLimit: { description: 'Warn before opening files larger than this number of megabytes.', type: 'number', - default: 20 + default: 40 } } }, diff --git a/src/cursor.coffee b/src/cursor.coffee index 85573f10e..df91d95c5 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -653,9 +653,6 @@ class Cursor extends Model fn() @autoscroll() if options.autoscroll ? @isLastCursor() - getPixelRect: -> - @editor.pixelRectForScreenRange(@getScreenRange()) - getScreenRange: -> {row, column} = @getScreenPosition() new Range(new Point(row, column), new Point(row, column + 1)) diff --git a/src/git-repository.coffee b/src/git-repository.coffee index d47b2e37c..423a5ce2f 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -238,6 +238,7 @@ class GitRepository # Public: Returns the git configuration value specified by the key. # + # * `key` The {String} key for the configuration to lookup. # * `path` An optional {String} path in the repository to get this information # for, only needed if the repository has submodules. getConfigValue: (key, path) -> @getRepo(path).getConfigValue(key) diff --git a/src/gutter-container-component.coffee b/src/gutter-container-component.coffee index 56b0fea84..ebb2d8597 100644 --- a/src/gutter-container-component.coffee +++ b/src/gutter-container-component.coffee @@ -103,6 +103,7 @@ class GutterContainerComponent @domNode.appendChild(gutterComponent.getDomNode()) else @domNode.insertBefore(gutterComponent.getDomNode(), @domNode.children[indexInOldGutters]) + indexInOldGutters += 1 # Remove any gutters that were not present in the new gutters state. for gutterComponentDescription in @gutterComponents diff --git a/src/history-manager.js b/src/history-manager.js new file mode 100644 index 000000000..c5117d00f --- /dev/null +++ b/src/history-manager.js @@ -0,0 +1,150 @@ +/** @babel */ + +import {Emitter} from 'event-kit' + +// Extended: History manager for remembering which projects have been opened. +// +// An instance of this class is always available as the `atom.history` global. +// +// The project history is used to enable the 'Reopen Project' menu. +export class HistoryManager { + constructor ({project, commands, localStorage}) { + this.localStorage = localStorage + commands.add('atom-workspace', {'application:clear-project-history': this.clearProjects.bind(this)}) + this.emitter = new Emitter() + this.loadState() + project.onDidChangePaths((projectPaths) => this.addProject(projectPaths)) + } + + // Public: Obtain a list of previously opened projects. + // + // Returns an {Array} of {HistoryProject} objects, most recent first. + getProjects () { + return this.projects.map(p => new HistoryProject(p.paths, p.lastOpened)) + } + + // Public: Clear all projects from the history. + // + // Note: This is not a privacy function - other traces will still exist, + // e.g. window state. + clearProjects () { + this.projects = [] + this.saveState() + this.didChangeProjects() + } + + // Public: Invoke the given callback when the list of projects changes. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeProjects (callback) { + return this.emitter.on('did-change-projects', callback) + } + + didChangeProjects (args) { + this.emitter.emit('did-change-projects', args || { reloaded: false }) + } + + addProject (paths, lastOpened) { + if (paths.length === 0) return + + let project = this.getProject(paths) + if (!project) { + project = new HistoryProject(paths) + this.projects.push(project) + } + project.lastOpened = lastOpened || new Date() + this.projects.sort((a, b) => b.lastOpened - a.lastOpened) + + this.saveState() + this.didChangeProjects() + } + + getProject (paths) { + for (var i = 0; i < this.projects.length; i++) { + if (arrayEquivalent(paths, this.projects[i].paths)) { + return this.projects[i] + } + } + + return null + } + + loadState () { + const state = JSON.parse(this.localStorage.getItem('history')) + if (state && state.projects) { + this.projects = state.projects.filter(p => Array.isArray(p.paths) && p.paths.length > 0).map(p => new HistoryProject(p.paths, new Date(p.lastOpened))) + this.didChangeProjects({ reloaded: true }) + } else { + this.projects = [] + } + } + + saveState () { + const state = JSON.stringify({ + projects: this.projects.map(p => ({ + paths: p.paths, lastOpened: p.lastOpened + })) + }) + this.localStorage.setItem('history', state) + } + + async importProjectHistory () { + for (let project of await HistoryImporter.getAllProjects()) { + this.addProject(project.paths, project.lastOpened) + } + this.saveState() + this.didChangeProjects() + } +} + +function arrayEquivalent (a, b) { + if (a.length !== b.length) return false + for (var i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false + } + return true +} + +export class HistoryProject { + constructor (paths, lastOpened) { + this.paths = paths + this.lastOpened = lastOpened || new Date() + } + + set paths (paths) { this._paths = paths } + get paths () { return this._paths } + + set lastOpened (lastOpened) { this._lastOpened = lastOpened } + get lastOpened () { return this._lastOpened } +} + +class HistoryImporter { + static async getStateStoreCursor () { + const db = await atom.stateStore.dbPromise + const store = db.transaction(['states']).objectStore('states') + return store.openCursor() + } + + static async getAllProjects (stateStore) { + const request = await HistoryImporter.getStateStoreCursor() + return new Promise((resolve, reject) => { + const rows = [] + request.onerror = reject + request.onsuccess = event => { + const cursor = event.target.result + if (cursor) { + let project = cursor.value.value.project + let storedAt = cursor.value.storedAt + if (project && project.paths && storedAt) { + rows.push(new HistoryProject(project.paths, new Date(Date.parse(storedAt)))) + } + cursor.continue() + } else { + resolve(rows) + } + } + }) + } +} diff --git a/src/initialize-application-window.coffee b/src/initialize-application-window.coffee index 585ec93f7..7d3a23db7 100644 --- a/src/initialize-application-window.coffee +++ b/src/initialize-application-window.coffee @@ -6,9 +6,7 @@ module.exports = ({blobStore}) -> {getWindowLoadSettings} = require './window-load-settings-helpers' {ipcRenderer} = require 'electron' {resourcePath, devMode, env} = getWindowLoadSettings() - require '../src/electron-shims' - - updateProcessEnv(env) + require './electron-shims' # Add application-specific exports to module search path. exportsPath = path.join(resourcePath, 'exports') diff --git a/src/initialize-benchmark-window.js b/src/initialize-benchmark-window.js index e4be4420b..29a210904 100644 --- a/src/initialize-benchmark-window.js +++ b/src/initialize-benchmark-window.js @@ -13,6 +13,7 @@ export default async function () { const ApplicationDelegate = require('../src/application-delegate') const AtomEnvironment = require('../src/atom-environment') const TextEditor = require('../src/text-editor') + require('./electron-shims') const exportsPath = path.join(resourcePath, 'exports') require('module').globalPaths.push(exportsPath) // Add 'exports' to module search path. diff --git a/src/initialize-test-window.coffee b/src/initialize-test-window.coffee index e4706fe1d..39a408fea 100644 --- a/src/initialize-test-window.coffee +++ b/src/initialize-test-window.coffee @@ -19,11 +19,12 @@ module.exports = ({blobStore}) -> path = require 'path' {ipcRenderer} = require 'electron' {getWindowLoadSettings} = require './window-load-settings-helpers' + CompileCache = require './compile-cache' AtomEnvironment = require '../src/atom-environment' ApplicationDelegate = require '../src/application-delegate' Clipboard = require '../src/clipboard' TextEditor = require '../src/text-editor' - require '../src/electron-shims' + require './electron-shims' {testRunnerPath, legacyTestRunnerPath, headless, logFile, testPaths} = getWindowLoadSettings() @@ -58,6 +59,13 @@ module.exports = ({blobStore}) -> require('module').globalPaths.push(exportsPath) process.env.NODE_PATH = exportsPath # Set NODE_PATH env variable since tasks may need it. + # Set up optional transpilation for packages under test if any + FindParentDir = require 'find-parent-dir' + if packageRoot = FindParentDir.sync(testPaths[0], 'package.json') + packageMetadata = require(path.join(packageRoot, 'package.json')) + if packageMetadata.atomTranspilers + CompileCache.addTranspilerConfigForPath(packageRoot, packageMetadata.name, packageMetadata, packageMetadata.atomTranspilers) + document.title = "Spec Suite" clipboard = new Clipboard diff --git a/src/input-component.coffee b/src/input-component.coffee index b8081b0d6..27543a2fd 100644 --- a/src/input-component.coffee +++ b/src/input-component.coffee @@ -1,15 +1,6 @@ module.exports = class InputComponent - constructor: -> - @domNode = document.createElement('input') - @domNode.classList.add('hidden-input') - @domNode.setAttribute('tabindex', -1) - @domNode.setAttribute('data-react-skip-selection-restoration', true) - @domNode.style['-webkit-transform'] = 'translateZ(0)' - @domNode.addEventListener 'paste', (event) -> event.preventDefault() - - getDomNode: -> - @domNode + constructor: (@domNode) -> updateSync: (state) -> @oldState ?= {} diff --git a/src/keymap-extensions.coffee b/src/keymap-extensions.coffee index b5c3964f9..bf8302f4c 100644 --- a/src/keymap-extensions.coffee +++ b/src/keymap-extensions.coffee @@ -8,6 +8,9 @@ bundledKeymaps = require('../package.json')?._atomKeymaps KeymapManager::onDidLoadBundledKeymaps = (callback) -> @emitter.on 'did-load-bundled-keymaps', callback +KeymapManager::onDidLoadUserKeymap = (callback) -> + @emitter.on 'did-load-user-keymap', callback + KeymapManager::loadBundledKeymaps = -> keymapsPath = path.join(@resourcePath, 'keymaps') if bundledKeymaps? @@ -49,6 +52,9 @@ KeymapManager::loadUserKeymap = -> stack = error.stack @notificationManager.addFatalError(error.message, {detail, stack, dismissable: true}) + @emitter.emit 'did-load-user-keymap' + + KeymapManager::subscribeToFileReadFailure = -> @onDidFailToReadFile (error) => userKeymapPath = @getUserKeymapPath() diff --git a/src/language-mode.coffee b/src/language-mode.coffee index bb9f339c4..06990bad5 100644 --- a/src/language-mode.coffee +++ b/src/language-mode.coffee @@ -27,7 +27,7 @@ class LanguageMode toggleLineCommentsForBufferRows: (start, end) -> scope = @editor.scopeDescriptorForBufferPosition([start, 0]) commentStrings = @editor.getCommentStrings(scope) - return unless commentStrings? + return unless commentStrings?.commentStartString {commentStartString, commentEndString} = commentStrings buffer = @editor.buffer diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index d4979865c..308cc5af0 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -126,4 +126,8 @@ class LinesYardstick clientRectForRange: (textNode, startIndex, endIndex) -> @rangeForMeasurement.setStart(textNode, startIndex) @rangeForMeasurement.setEnd(textNode, endIndex) - @rangeForMeasurement.getClientRects()[0] ? @rangeForMeasurement.getBoundingClientRect() + clientRects = @rangeForMeasurement.getClientRects() + if clientRects.length is 1 + clientRects[0] + else + @rangeForMeasurement.getBoundingClientRect() diff --git a/src/main-process/application-menu.coffee b/src/main-process/application-menu.coffee index b0a6e3267..f06e4933f 100644 --- a/src/main-process/application-menu.coffee +++ b/src/main-process/application-menu.coffee @@ -142,8 +142,8 @@ class ApplicationMenu item.metadata ?= {} if item.command item.accelerator = @acceleratorForCommand(item.command, keystrokesByCommand) - item.click = -> global.atomApplication.sendCommand(item.command) - item.metadata.windowSpecific = true unless /^application:/.test(item.command) + item.click = -> global.atomApplication.sendCommand(item.command, item.commandDetail) + item.metadata.windowSpecific = true unless /^application:/.test(item.command, item.commandDetail) @translateTemplate(item.submenu, keystrokesByCommand) if item.submenu template diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index afd267106..4f580be8c 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -34,7 +34,7 @@ class AtomApplication unless options.socketPath? if process.platform is 'win32' userNameSafe = new Buffer(process.env.USERNAME).toString('base64') - options.socketPath = "\\\\.\\pipe\\atom-#{options.version}-#{userNameSafe}-sock" + options.socketPath = "\\\\.\\pipe\\atom-#{options.version}-#{userNameSafe}-#{process.arch}-sock" else options.socketPath = path.join(os.tmpdir(), "atom-#{options.version}-#{process.env.USER}.sock") @@ -63,7 +63,7 @@ class AtomApplication exit: (status) -> app.exit(status) constructor: (options) -> - {@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, @logFile, @setPortable, @userDataDir} = options + {@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, @logFile, @userDataDir} = options @socketPath = null if options.test or options.benchmark or options.benchmarkTest @pidsToOpenWindows = {} @windows = [] @@ -99,7 +99,6 @@ class AtomApplication @atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode) @listenForArgumentsFromNewProcess() - @setupJavaScriptArguments() @setupDockMenu() @launch(options) @@ -207,10 +206,6 @@ class AtomApplication # which is why this check is here. throw error unless error.code is 'ENOENT' - # Configures required javascript environment flags. - setupJavaScriptArguments: -> - app.commandLine.appendSwitch 'js-flags', '--harmony' - # Registers basic application commands, non-idempotent. handleEvents: -> getLoadSettings = => @@ -285,6 +280,12 @@ class AtomApplication @disposable.add ipcHelpers.on ipcMain, 'restart-application', => @restart() + @disposable.add ipcHelpers.on ipcMain, 'did-change-history-manager', (event) => + for atomWindow in @windows + webContents = atomWindow.browserWindow.webContents + if webContents isnt event.sender + webContents.send('did-change-history-manager') + # A request from the associated render process to open a new render process. @disposable.add ipcHelpers.on ipcMain, 'open', (event, options) => window = @atomWindowForEvent(event) @@ -390,6 +391,9 @@ class AtomApplication @fileRecoveryService.didSavePath(@atomWindowForEvent(event), path) event.returnValue = true + @disposable.add ipcHelpers.on ipcMain, 'did-change-paths', => + @saveState(false) + setupDockMenu: -> if process.platform is 'darwin' dockMenu = Menu.buildFromTemplate [ @@ -514,7 +518,7 @@ class AtomApplication openPaths: ({initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window, clearWindowState, addToLastWindow, env}={}) -> if not pathsToOpen? or pathsToOpen.length is 0 return - + env = process.env unless env? devMode = Boolean(devMode) safeMode = Boolean(safeMode) clearWindowState = Boolean(clearWindowState) @@ -801,7 +805,6 @@ class AtomApplication restart: -> args = [] args.push("--safe") if @safeMode - args.push("--portable") if @setPortable args.push("--log-file=#{@logFile}") if @logFile? args.push("--socket-path=#{@socketPath}") if @socketPath? args.push("--user-data-dir=#{@userDataDir}") if @userDataDir? diff --git a/src/main-process/atom-portable.js b/src/main-process/atom-portable.js deleted file mode 100644 index 7d395c0e7..000000000 --- a/src/main-process/atom-portable.js +++ /dev/null @@ -1,58 +0,0 @@ -const fs = require('fs-plus') -const path = require('path') -const {ipcMain} = require('electron') - -module.exports = class AtomPortable { - static getPortableAtomHomePath () { - const execDirectoryPath = path.dirname(process.execPath) - return path.join(execDirectoryPath, '..', '.atom') - } - - static setPortable (existingAtomHome) { - fs.copySync(existingAtomHome, this.getPortableAtomHomePath()) - } - - static isPortableInstall (platform, environmentAtomHome, defaultHome) { - if (!['linux', 'win32'].includes(platform)) { - return false - } - - if (environmentAtomHome) { - return false - } - - if (!fs.existsSync(this.getPortableAtomHomePath())) { - return false - } - - // Currently checking only that the directory exists and is writable, - // probably want to do some integrity checks on contents in future. - return this.isPortableAtomHomePathWritable(defaultHome) - } - - static isPortableAtomHomePathWritable (defaultHome) { - let writable = false - let message = '' - try { - const writePermissionTestFile = path.join(this.getPortableAtomHomePath(), 'write.test') - - if (!fs.existsSync(writePermissionTestFile)) { - fs.writeFileSync(writePermissionTestFile, 'test') - } - - fs.removeSync(writePermissionTestFile) - writable = true - } catch (error) { - message = `Failed to use portable Atom home directory (${this.getPortableAtomHomePath()}). Using the default instead (${defaultHome}). ${error.message}.` - } - - ipcMain.on('check-portable-home-writable', function (event) { - event.sender.send('check-portable-home-writable-response', { - writable: writable, - message: message - }) - }) - - return writable - } -} diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee index 6736089e7..7fd119aa7 100644 --- a/src/main-process/atom-window.coffee +++ b/src/main-process/atom-window.coffee @@ -153,7 +153,10 @@ class AtomWindow @browserWindow.destroy() if chosen is 0 @browserWindow.webContents.on 'crashed', => - @atomApplication.exit(100) if @headless + if @headless + console.log "Renderer process crashed, exiting" + @atomApplication.exit(100) + return @fileRecoveryService.didCrashWindow(this) chosen = dialog.showMessageBox @browserWindow, diff --git a/src/main-process/auto-update-manager.coffee b/src/main-process/auto-update-manager.coffee index a4a45ce73..8fdba844d 100644 --- a/src/main-process/auto-update-manager.coffee +++ b/src/main-process/auto-update-manager.coffee @@ -17,13 +17,15 @@ class AutoUpdateManager constructor: (@version, @testMode, resourcePath, @config) -> @state = IdleState @iconPath = path.resolve(__dirname, '..', '..', 'resources', 'atom.png') - @feedUrl = "https://atom.io/api/updates?version=#{@version}" process.nextTick => @setupAutoUpdater() setupAutoUpdater: -> if process.platform is 'win32' + archSuffix = if process.arch is 'ia32' then '' else '-' + process.arch + @feedUrl = "https://atom.io/api/updates#{archSuffix}" autoUpdater = require './auto-updater-win32' else + @feedUrl = "https://atom.io/api/updates?version=#{@version}" {autoUpdater} = require 'electron' autoUpdater.on 'error', (event, message) => diff --git a/src/main-process/parse-command-line.js b/src/main-process/parse-command-line.js index 68a18fa30..4227b63ba 100644 --- a/src/main-process/parse-command-line.js +++ b/src/main-process/parse-command-line.js @@ -41,10 +41,6 @@ module.exports = function parseCommandLine (processArgs) { 'safe', 'Do not load packages from ~/.atom/packages or ~/.atom/dev/packages.' ) - 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.boolean('benchmark').describe('benchmark', 'Open a new window that runs the specified benchmarks.') options.boolean('benchmark-test').describe('benchmark--test', 'Run a faster version of the benchmarks in headless mode.') options.alias('t', 'test').boolean('t').describe('t', 'Run the specified specs and exit with error code on failures.') @@ -104,7 +100,6 @@ module.exports = function parseCommandLine (processArgs) { const profileStartup = args['profile-startup'] const clearWindowState = args['clear-window-state'] const urlsToOpen = [] - const setPortable = args.portable let devMode = args['dev'] let devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH || path.join(app.getPath('home'), 'github', 'atom') let resourcePath = null @@ -152,7 +147,6 @@ module.exports = function parseCommandLine (processArgs) { userDataDir, profileStartup, timeout, - setPortable, clearWindowState, addToLastWindow, mainProcess, diff --git a/src/main-process/start.js b/src/main-process/start.js index 84ae9b8c2..f54d263e0 100644 --- a/src/main-process/start.js +++ b/src/main-process/start.js @@ -1,10 +1,10 @@ const {app} = require('electron') -const fs = require('fs-plus') const nslog = require('nslog') const path = require('path') const temp = require('temp') const parseCommandLine = require('./parse-command-line') const startCrashReporter = require('../crash-reporter-start') +const atomPaths = require('../atom-paths') module.exports = function start (resourcePath, startTime) { global.shellStartTime = startTime @@ -23,7 +23,8 @@ module.exports = function start (resourcePath, startTime) { console.log = nslog const args = parseCommandLine(process.argv.slice(1)) - setupAtomHome(args) + atomPaths.setAtomHome(app.getPath('home')) + atomPaths.setUserData() setupCompileCache() if (handleStartupEventWithSquirrel()) { @@ -39,7 +40,7 @@ module.exports = function start (resourcePath, startTime) { } // NB: This prevents Win10 from showing dupe items in the taskbar - app.setAppUserModelId('com.squirrel.atom.atom') + app.setAppUserModelId('com.squirrel.atom.' + process.arch) function addPathToOpen (event, pathToOpen) { event.preventDefault() @@ -79,36 +80,6 @@ function handleStartupEventWithSquirrel () { return SquirrelUpdate.handleStartupEvent(app, squirrelCommand) } -function setupAtomHome ({setPortable}) { - if (process.env.ATOM_HOME) { - return - } - - let atomHome = path.join(app.getPath('home'), '.atom') - const AtomPortable = require('./atom-portable') - - if (setPortable && !AtomPortable.isPortableInstall(process.platform, process.env.ATOM_HOME, atomHome)) { - try { - AtomPortable.setPortable(atomHome) - } catch (error) { - console.log(`Failed copying portable directory '${atomHome}' to '${AtomPortable.getPortableAtomHomePath()}'`) - console.log(`${error.message} ${error.stack}`) - } - } - - if (AtomPortable.isPortableInstall(process.platform, process.env.ATOM_HOME, atomHome)) { - atomHome = AtomPortable.getPortableAtomHomePath() - } - - try { - atomHome = fs.realpathSync(atomHome) - } catch (e) { - // Don't throw an error if atomHome doesn't exist. - } - - process.env.ATOM_HOME = atomHome -} - function setupCompileCache () { const CompileCache = require('../compile-cache') CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME) diff --git a/src/main-process/win-shell.coffee b/src/main-process/win-shell.coffee deleted file mode 100644 index baf02a9fc..000000000 --- a/src/main-process/win-shell.coffee +++ /dev/null @@ -1,63 +0,0 @@ -Registry = require 'winreg' -Path = require 'path' - -exeName = Path.basename(process.execPath) -appPath = "\"#{process.execPath}\"" -fileIconPath = "\"#{Path.join(process.execPath, '..', 'resources', 'cli', 'file.ico')}\"" -isBeta = appPath.includes(' Beta') -appName = exeName.replace('atom', (if isBeta then 'Atom Beta' else 'Atom' )).replace('.exe', '') - -class ShellOption - constructor: (key, parts) -> - @key = key - @parts = parts - - isRegistered: (callback) => - new Registry({hive: 'HKCU', key: "#{@key}\\#{@parts[0].key}"}) - .get @parts[0].name, (err, val) => - callback(not err? and val? and val.value is @parts[0].value) - - register: (callback) => - doneCount = @parts.length - @parts.forEach (part) => - reg = new Registry({hive: 'HKCU', key: if part.key? then "#{@key}\\#{part.key}" else @key}) - reg.create( -> reg.set part.name, Registry.REG_SZ, part.value, -> callback() if --doneCount is 0) - - deregister: (callback) => - @isRegistered (isRegistered) => - if isRegistered - new Registry({hive: 'HKCU', key: @key}).destroy -> callback null, true - else - callback null, false - - update: (callback) => - new Registry({hive: 'HKCU', key: "#{@key}\\#{@parts[0].key}"}) - .get @parts[0].name, (err, val) => - if err? or not val? - callback(err) - else - @register callback - -exports.appName = appName - -exports.fileHandler = new ShellOption("\\Software\\Classes\\Applications\\#{exeName}", - [ - {key: 'shell\\open\\command', name: '', value: "#{appPath} \"%1\""}, - {key: 'shell\\open', name: 'FriendlyAppName', value: "#{appName}"}, - {key: 'DefaultIcon', name: '', value: "#{fileIconPath}"} - ] -) - -contextParts = [ - {key: 'command', name: '', value: "#{appPath} \"%1\""}, - {name: '', value: "Open with #{appName}"}, - {name: 'Icon', value: "#{appPath}"} -] - -exports.fileContextMenu = new ShellOption("\\Software\\Classes\\*\\shell\\#{appName}", contextParts) - -exports.folderContextMenu = new ShellOption("\\Software\\Classes\\Directory\\shell\\#{appName}", contextParts) - -exports.folderBackgroundContextMenu = new ShellOption("\\Software\\Classes\\Directory\\background\\shell\\#{appName}", - JSON.parse(JSON.stringify(contextParts).replace('%1', '%V')) -) diff --git a/src/main-process/win-shell.js b/src/main-process/win-shell.js new file mode 100644 index 000000000..9670936c7 --- /dev/null +++ b/src/main-process/win-shell.js @@ -0,0 +1,77 @@ +'use babel' + +import Registry from 'winreg' +import Path from 'path' + +let exeName = Path.basename(process.execPath) +let appPath = `\"${process.execPath}\"` +let fileIconPath = `\"${Path.join(process.execPath, '..', 'resources', 'cli', 'file.ico')}\"` +let isBeta = appPath.includes(' Beta') +let appName = exeName.replace('atom', isBeta ? 'Atom Beta' : 'Atom').replace('.exe', '') + +class ShellOption { + constructor (key, parts) { + this.isRegistered = this.isRegistered.bind(this) + this.register = this.register.bind(this) + this.deregister = this.deregister.bind(this) + this.update = this.update.bind(this) + this.key = key + this.parts = parts + } + + isRegistered (callback) { + new Registry({hive: 'HKCU', key: `${this.key}\\${this.parts[0].key}`}) + .get(this.parts[0].name, (err, val) => callback((err == null) && (val != null) && val.value === this.parts[0].value)) + } + + register (callback) { + let doneCount = this.parts.length + this.parts.forEach(part => { + let reg = new Registry({hive: 'HKCU', key: (part.key != null) ? `${this.key}\\${part.key}` : this.key}) + return reg.create(() => reg.set(part.name, Registry.REG_SZ, part.value, () => { if (--doneCount === 0) return callback() })) + }) + } + + deregister (callback) { + this.isRegistered(isRegistered => { + if (isRegistered) { + new Registry({hive: 'HKCU', key: this.key}).destroy(() => callback(null, true)) + } else { + callback(null, false) + } + }) + } + + update (callback) { + new Registry({hive: 'HKCU', key: `${this.key}\\${this.parts[0].key}`}) + .get(this.parts[0].name, (err, val) => { + if ((err != null) || (val == null)) { + callback(err) + } else { + this.register(callback) + } + }) + } +} + +exports.appName = appName + +exports.fileHandler = new ShellOption(`\\Software\\Classes\\Applications\\${exeName}`, + [ + {key: 'shell\\open\\command', name: '', value: `${appPath} \"%1\"`}, + {key: 'shell\\open', name: 'FriendlyAppName', value: `${appName}`}, + {key: 'DefaultIcon', name: '', value: `${fileIconPath}`} + ] +) + +let contextParts = [ + {key: 'command', name: '', value: `${appPath} \"%1\"`}, + {name: '', value: `Open with ${appName}`}, + {name: 'Icon', value: `${appPath}`} +] + +exports.fileContextMenu = new ShellOption(`\\Software\\Classes\\*\\shell\\${appName}`, contextParts) +exports.folderContextMenu = new ShellOption(`\\Software\\Classes\\Directory\\shell\\${appName}`, contextParts) +exports.folderBackgroundContextMenu = new ShellOption(`\\Software\\Classes\\Directory\\background\\shell\\${appName}`, + JSON.parse(JSON.stringify(contextParts).replace('%1', '%V')) +) diff --git a/src/native-compile-cache.js b/src/native-compile-cache.js index a9857fc0c..e4e7fc146 100644 --- a/src/native-compile-cache.js +++ b/src/native-compile-cache.js @@ -74,7 +74,13 @@ class NativeCompileCache { self.cacheStore.delete(cacheKey) } } else { - let compilationResult = cachedVm.runInThisContext(wrapper, filename) + let compilationResult + try { + compilationResult = cachedVm.runInThisContext(wrapper, filename) + } catch (err) { + console.error(`Error running script ${filename}`) + throw err + } if (compilationResult.cacheBuffer) { self.cacheStore.set(cacheKey, invalidationKey, compilationResult.cacheBuffer) } diff --git a/src/package-manager.coffee b/src/package-manager.coffee index 84a36dd78..fb4f7a658 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -39,6 +39,7 @@ class PackageManager @activationHookEmitter = new Emitter @packageDirPaths = [] @deferredActivationHooks = [] + @triggeredActivationHooks = new Set() if configDirPath? and not safeMode if @devMode @packageDirPaths.push(path.join(configDirPath, "dev", "packages")) @@ -67,6 +68,7 @@ class PackageManager @deactivatePackages() @loadedPackages = {} @packageStates = {} + @triggeredActivationHooks.clear() ### Section: Event Subscription @@ -460,12 +462,17 @@ class PackageManager Promise.resolve(pack) else if pack = @loadPackage(name) @activatingPackages[pack.name] = pack - pack.activate().then => + activationPromise = pack.activate().then => if @activatingPackages[pack.name]? delete @activatingPackages[pack.name] @activePackages[pack.name] = pack @emitter.emit 'did-activate-package', pack pack + + unless @deferredActivationHooks? + @triggeredActivationHooks.forEach((hook) => @activationHookEmitter.emit(hook)) + + activationPromise else Promise.reject(new Error("Failed to load package '#{name}'")) @@ -476,6 +483,7 @@ class PackageManager triggerActivationHook: (hook) -> return new Error("Cannot trigger an empty activation hook") unless hook? and _.isString(hook) and hook.length > 0 + @triggeredActivationHooks.add(hook) if @deferredActivationHooks? @deferredActivationHooks.push hook else diff --git a/src/package-transpilation-registry.js b/src/package-transpilation-registry.js new file mode 100644 index 000000000..b8e81ccad --- /dev/null +++ b/src/package-transpilation-registry.js @@ -0,0 +1,172 @@ +'use strict' +// This file is required by compile-cache, which is required directly from +// apm, so it can only use the subset of newer JavaScript features that apm's +// version of Node supports. Strict mode is required for block scoped declarations. + +const crypto = require('crypto') +const fs = require('fs') +const path = require('path') + +const minimatch = require('minimatch') + +let Resolve = null + +class PackageTranspilationRegistry { + constructor () { + this.configByPackagePath = {} + this.specByFilePath = {} + this.transpilerPaths = {} + } + + addTranspilerConfigForPath (packagePath, packageName, packageMeta, config) { + this.configByPackagePath[packagePath] = { + name: packageName, + meta: packageMeta, + path: packagePath, + specs: config.map(spec => Object.assign({}, spec)) + } + } + + removeTranspilerConfigForPath (packagePath) { + delete this.configByPackagePath[packagePath] + } + + // Wraps the transpiler in an object with the same interface + // that falls back to the original transpiler implementation if and + // only if a package hasn't registered its desire to transpile its own source. + wrapTranspiler (transpiler) { + return { + getCachePath: (sourceCode, filePath) => { + const spec = this.getPackageTranspilerSpecForFilePath(filePath) + if (spec) { + return this.getCachePath(sourceCode, filePath, spec) + } + + return transpiler.getCachePath(sourceCode, filePath) + }, + + compile: (sourceCode, filePath) => { + const spec = this.getPackageTranspilerSpecForFilePath(filePath) + if (spec) { + return this.transpileWithPackageTranspiler(sourceCode, filePath, spec) + } + + return transpiler.compile(sourceCode, filePath) + }, + + shouldCompile: (sourceCode, filePath) => { + if (this.transpilerPaths[filePath]) { + return false + } + const spec = this.getPackageTranspilerSpecForFilePath(filePath) + if (spec) { + return true + } + + return transpiler.shouldCompile(sourceCode, filePath) + } + } + } + + getPackageTranspilerSpecForFilePath (filePath) { + if (this.specByFilePath[filePath] !== undefined) return this.specByFilePath[filePath] + + // ignore node_modules + if (filePath.indexOf(path.sep + 'node_modules' + path.sep) > -1) { + return false + } + + let thisPath = filePath + let lastPath = null + // Iterate parents from the file path to the root, checking at each level + // to see if a package manages transpilation for that directory. + // This means searching for a config for `/path/to/file/here.js` only + // only iterates four times, even if there are hundreds of configs registered. + while (thisPath !== lastPath) { // until we reach the root + let config = this.configByPackagePath[thisPath] + if (config) { + for (let i = 0; i < config.specs.length; i++) { + const spec = config.specs[i] + if (minimatch(filePath, path.join(config.path, spec.glob))) { + spec._config = config + this.specByFilePath[filePath] = spec + return spec + } + } + } + + lastPath = thisPath + thisPath = path.join(thisPath, '..') + } + + this.specByFilePath[filePath] = null + return null + } + + getCachePath (sourceCode, filePath, spec) { + const transpilerPath = this.getTranspilerPath(spec) + const transpilerSource = spec._transpilerSource || fs.readFileSync(transpilerPath, 'utf8') + spec._transpilerSource = transpilerSource + const transpiler = this.getTranspiler(spec) + + let hash = crypto + .createHash('sha1') + .update(JSON.stringify(spec.options || {})) + .update(transpilerSource, 'utf8') + .update(sourceCode, 'utf8') + + if (transpiler && transpiler.getCacheKeyData) { + const meta = this.getMetadata(spec) + const additionalCacheData = transpiler.getCacheKeyData(sourceCode, filePath, spec.options || {}, meta) + hash.update(additionalCacheData, 'utf8') + } + + return path.join('package-transpile', spec._config.name, hash.digest('hex')) + } + + transpileWithPackageTranspiler (sourceCode, filePath, spec) { + const transpiler = this.getTranspiler(spec) + + if (transpiler) { + const meta = this.getMetadata(spec) + const result = transpiler.transpile(sourceCode, filePath, spec.options || {}, meta) + if (result === undefined || (result && result.code === undefined)) { + return sourceCode + } else if (result.code) { + return result.code.toString() + } else { + throw new Error('Could not find a property `.code` on the transpilation results of ' + filePath) + } + } else { + const err = new Error("Could not resolve transpiler '" + spec.transpiler + "' from '" + spec._config.path + "'") + throw err + } + } + + getMetadata (spec) { + return { + name: spec._config.name, + path: spec._config.path, + meta: spec._config.meta + } + } + + getTranspilerPath (spec) { + Resolve = Resolve || require('resolve') + return Resolve.sync(spec.transpiler, { + basedir: spec._config.path, + extensions: Object.keys(require.extensions) + }) + } + + getTranspiler (spec) { + const transpilerPath = this.getTranspilerPath(spec) + if (transpilerPath) { + const transpiler = require(transpilerPath) + this.transpilerPaths[transpilerPath] = true + return transpiler + } + } +} + +module.exports = PackageTranspilationRegistry diff --git a/src/package.coffee b/src/package.coffee index 323dfa8d2..9fa2dbe63 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -6,6 +6,7 @@ CSON = require 'season' fs = require 'fs-plus' {Emitter, CompositeDisposable} = require 'event-kit' +CompileCache = require './compile-cache' ModuleCache = require './module-cache' ScopedProperties = require './scoped-properties' BufferedProcess = require './buffered-process' @@ -23,6 +24,7 @@ class Package mainModulePath: null resolvedMainModulePath: false mainModule: null + mainInitialized: false mainActivated: false ### @@ -86,6 +88,7 @@ class Package @loadStylesheets() @registerDeserializerMethods() @activateCoreStartupServices() + @registerTranspilerConfig() @configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata() @settingsPromise = @loadSettings() if @shouldRequireMainModuleOnLoad() and not @mainModule? @@ -94,6 +97,9 @@ class Package @handleError("Failed to load the #{@name} package", error) this + unload: -> + @unregisterTranspilerConfig() + shouldRequireMainModuleOnLoad: -> not ( @metadata.deserializers? or @@ -109,8 +115,24 @@ class Package @menus = [] @grammars = [] @settings = [] + @mainInitialized = false @mainActivated = false + initializeIfNeeded: -> + return if @mainInitialized + @measure 'initializeTime', => + try + # The main module's `initialize()` method is guaranteed to be called + # before its `activate()`. This gives you a chance to handle the + # serialized package state before the package's derserializers and view + # providers are used. + @requireMainModule() unless @mainModule? + @mainModule.initialize?(@packageManager.getPackageState(@name) ? {}) + @mainInitialized = true + catch error + @handleError("Failed to initialize the #{@name} package", error) + return + activate: -> @grammarsPromise ?= @loadGrammars() @activationPromise ?= @@ -135,10 +157,13 @@ class Package @registerViewProviders() @activateStylesheets() if @mainModule? and not @mainActivated + @initializeIfNeeded() @mainModule.activateConfig?() @mainModule.activate?(@packageManager.getPackageState(@name) ? {}) @mainActivated = true @activateServices() + @activationCommandSubscriptions?.dispose() + @activationHookSubscriptions?.dispose() catch error @handleError("Failed to activate the #{@name} package", error) @@ -247,6 +272,14 @@ class Package @activationDisposables.add @packageManager.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule)) return + registerTranspilerConfig: -> + if @metadata.atomTranspilers + CompileCache.addTranspilerConfigForPath(@path, @name, @metadata, @metadata.atomTranspilers) + + unregisterTranspilerConfig: -> + if @metadata.atomTranspilers + CompileCache.removeTranspilerConfigForPath(@path) + loadKeymaps: -> if @bundledPackage and @packageManager.packagesCache[@name]? @keymaps = (["#{@packageManager.resourcePath}#{path.sep}#{keymapPath}", keymapObject] for keymapPath, keymapObject of @packageManager.packagesCache[@name].keymaps) @@ -288,6 +321,7 @@ class Package deserialize: (state, atomEnvironment) => @registerViewProviders() @requireMainModule() + @initializeIfNeeded() @mainModule[methodName](state, atomEnvironment) return @@ -305,6 +339,7 @@ class Package @requireMainModule() @metadata.viewProviders.forEach (methodName) => @viewRegistry.addViewProvider (model) => + @initializeIfNeeded() @mainModule[methodName](model) @registeredViewProviders = true @@ -407,6 +442,7 @@ class Package @mainModule?.deactivate?() @mainModule?.deactivateConfig?() @mainActivated = false + @mainInitialized = false catch e console.error "Error deactivating package '#{@name}'", e.stack @emitter.emit 'did-deactivate' diff --git a/src/pane.coffee b/src/pane.coffee index e4003ad35..c55c9f043 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -234,6 +234,39 @@ class Pane extends Model onDidChangeActiveItem: (callback) -> @emitter.on 'did-change-active-item', callback + # Public: Invoke the given callback when {::activateNextRecentlyUsedItem} + # has been called, either initiating or continuing a forward MRU traversal of + # pane items. + # + # * `callback` {Function} to be called with when the active item changes. + # * `nextRecentlyUsedItem` The next MRU item, now being set active + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onChooseNextMRUItem: (callback) -> + @emitter.on 'choose-next-mru-item', callback + + # Public: Invoke the given callback when {::activatePreviousRecentlyUsedItem} + # has been called, either initiating or continuing a reverse MRU traversal of + # pane items. + # + # * `callback` {Function} to be called with when the active item changes. + # * `previousRecentlyUsedItem` The previous MRU item, now being set active + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onChooseLastMRUItem: (callback) -> + @emitter.on 'choose-last-mru-item', callback + + # Public: Invoke the given callback when {::moveActiveItemToTopOfStack} + # has been called, terminating an MRU traversal of pane items and moving the + # current active item to the top of the stack. Typically bound to a modifier + # (e.g. CTRL) key up event. + # + # * `callback` {Function} to be called with when the MRU traversal is done. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDoneChoosingMRUItem: (callback) -> + @emitter.on 'done-choosing-mru-item', callback + # Public: Invoke the given callback with the current and future values of # {::getActiveItem}. # @@ -334,6 +367,7 @@ class Pane extends Model @itemStackIndex = @itemStack.length if @itemStackIndex is 0 @itemStackIndex = @itemStackIndex - 1 nextRecentlyUsedItem = @itemStack[@itemStackIndex] + @emitter.emit 'choose-next-mru-item', nextRecentlyUsedItem @setActiveItem(nextRecentlyUsedItem, modifyStack: false) # Makes the previous item in the itemStack active. @@ -343,12 +377,15 @@ class Pane extends Model @itemStackIndex = -1 @itemStackIndex = @itemStackIndex + 1 previousRecentlyUsedItem = @itemStack[@itemStackIndex] + @emitter.emit 'choose-last-mru-item', previousRecentlyUsedItem @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) + @emitter.emit 'done-choosing-mru-item' + # Public: Makes the next item active. activateNextItem: -> @@ -583,7 +620,7 @@ class Pane extends Model chosen = @applicationDelegate.confirm message: message detailedMessage: "Your changes will be lost if you close this item without saving." - buttons: [saveButtonText, "Cancel", "Don't save"] + buttons: [saveButtonText, "Cancel", "Don't Save"] switch chosen when 0 then saveFn(item, saveError) when 1 then false diff --git a/src/project.coffee b/src/project.coffee index 52a3223e0..522fbfbc7 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -70,7 +70,15 @@ class Project extends Model serialize: (options={}) -> deserializer: 'Project' paths: @getPaths() - buffers: _.compact(@buffers.map (buffer) -> buffer.serialize({markerLayers: options.isUnloading is true}) if buffer.isRetained()) + buffers: _.compact(@buffers.map (buffer) -> + if buffer.isRetained() + state = buffer.serialize({markerLayers: options.isUnloading is true}) + # Skip saving large buffer text unless unloading to avoid blocking main thread + if not options.isUnloading and state.text.length > 2 * 1024 * 1024 + delete state.text + delete state.digestWhenLastPersisted + state + ) ### Section: Event Subscription @@ -225,11 +233,11 @@ class Project extends Model uri else if fs.isAbsolute(uri) - path.normalize(fs.absolute(uri)) + path.normalize(fs.resolveHome(uri)) # TODO: what should we do here when there are multiple directories? else if projectPath = @getPaths()[0] - path.normalize(fs.absolute(path.join(projectPath, uri))) + path.normalize(fs.resolveHome(path.join(projectPath, uri))) else undefined diff --git a/src/reopen-project-list-view.js b/src/reopen-project-list-view.js new file mode 100644 index 000000000..f08ee725a --- /dev/null +++ b/src/reopen-project-list-view.js @@ -0,0 +1,76 @@ +/** @babel */ + +import SelectListView from 'atom-select-list' + +export default class ReopenProjectListView { + constructor (callback) { + this.callback = callback + this.selectListView = new SelectListView({ + emptyMessage: 'No projects in history.', + itemsClassList: ['mark-active'], + items: [], + filterKeyForItem: (project) => project.name, + elementForItem: (project) => { + let element = document.createElement('li') + if (project.name === this.currentProjectName) { + element.classList.add('active') + } + element.textContent = project.name + return element + }, + didConfirmSelection: (project) => { + this.cancel() + this.callback(project.value) + }, + didCancelSelection: () => { + this.cancel() + } + }) + this.selectListView.element.classList.add('reopen-project') + } + + get element () { + return this.selectListView.element + } + + dispose () { + this.cancel() + return this.selectListView.destroy() + } + + cancel () { + if (this.panel != null) { + this.panel.destroy() + } + this.panel = null + this.currentProjectName = null + if (this.previouslyFocusedElement) { + this.previouslyFocusedElement.focus() + this.previouslyFocusedElement = null + } + } + + attach () { + this.previouslyFocusedElement = document.activeElement + if (this.panel == null) { + this.panel = atom.workspace.addModalPanel({item: this}) + } + this.selectListView.focus() + this.selectListView.reset() + } + + async toggle () { + if (this.panel != null) { + this.cancel() + } else { + this.currentProjectName = atom.project != null ? this.makeName(atom.project.getPaths()) : null + const projects = atom.history.getProjects().map(p => ({ name: this.makeName(p.paths), value: p.paths })) + await this.selectListView.update({items: projects}) + this.attach() + } + } + + makeName (paths) { + return paths.join(', ') + } +} diff --git a/src/reopen-project-menu-manager.js b/src/reopen-project-menu-manager.js new file mode 100644 index 000000000..8b2a11838 --- /dev/null +++ b/src/reopen-project-menu-manager.js @@ -0,0 +1,124 @@ +/** @babel */ + +import {CompositeDisposable} from 'event-kit' +import path from 'path' + +export default class ReopenProjectMenuManager { + constructor ({menu, commands, history, config, open}) { + this.menuManager = menu + this.historyManager = history + this.config = config + this.open = open + this.projects = [] + + this.subscriptions = new CompositeDisposable() + this.subscriptions.add( + history.onDidChangeProjects(this.update.bind(this)), + config.onDidChange('core.reopenProjectMenuCount', ({oldValue, newValue}) => { + this.update() + }), + commands.add('atom-workspace', { 'application:reopen-project': this.reopenProjectCommand.bind(this) }) + ) + } + + reopenProjectCommand (e) { + if (e.detail != null && e.detail.index != null) { + this.open(this.projects[e.detail.index].paths) + } else { + this.createReopenProjectListView() + } + } + + createReopenProjectListView () { + if (this.reopenProjectListView == null) { + const ReopenProjectListView = require('./reopen-project-list-view') + this.reopenProjectListView = new ReopenProjectListView(paths => { + if (paths != null) { + this.open(paths) + } + }) + } + this.reopenProjectListView.toggle() + } + + update () { + this.disposeProjectMenu() + this.projects = this.historyManager.getProjects().slice(0, this.config.get('core.reopenProjectMenuCount')) + const newMenu = ReopenProjectMenuManager.createProjectsMenu(this.projects) + this.lastProjectMenu = this.menuManager.add([newMenu]) + this.updateWindowsJumpList() + } + + updateWindowsJumpList () { + if (process.platform !== 'win32') return + + if (this.app === undefined) { + this.app = require('remote').app + } + + this.app.setJumpList([ + { + type: 'custom', + name: 'Recent Projects', + items: this.projects.map(project => + ({ + type: 'task', + title: project.paths.map(ReopenProjectMenuManager.betterBaseName).join(', '), + description: project.paths.map(path => `${ReopenProjectMenuManager.betterBaseName(path)} (${path})`).join(' '), + program: process.execPath, + args: project.paths.map(path => `"${path}"`).join(' '), + iconPath: path.join(path.dirname(process.execPath), 'resources', 'cli', 'folder.ico'), + iconIndex: 0 + }) + ) + }, + { type: 'recent' }, + { items: [ + {type: 'task', title: 'New Window', program: process.execPath, args: '--new-window', description: 'Opens a new Atom window'} + ]} + ]) + } + + dispose () { + this.subscriptions.dispose() + this.disposeProjectMenu() + if (this.reopenProjectListView != null) { + this.reopenProjectListView.dispose() + } + } + + disposeProjectMenu () { + if (this.lastProjectMenu) { + this.lastProjectMenu.dispose() + this.lastProjectMenu = null + } + } + + static createProjectsMenu (projects) { + return { + label: 'File', + submenu: [ + { + label: 'Reopen Project', + submenu: projects.map((project, index) => ({ + label: this.createLabel(project), + command: 'application:reopen-project', + commandDetail: {index: index} + })) + } + ] + } + } + + static createLabel (project) { + return project.paths.length === 1 + ? project.paths[0] + : project.paths.map(this.betterBaseName).join(', ') + } + + static betterBaseName (directory) { + // Handles Windows roots better than path.basename which returns '' for 'd:' and 'd:\' + const match = directory.match(/^([a-z]:)[\\]?$/i) + return match ? match[1] + '\\' : path.basename(directory) + } +} diff --git a/src/selection.coffee b/src/selection.coffee index 5eaa9c8dd..8aa86157e 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -366,7 +366,7 @@ class Selection extends Model insertText: (text, options={}) -> oldBufferRange = @getBufferRange() wasReversed = @isReversed() - @clear() + @clear(options) autoIndentFirstLine = false precedingText = @editor.getTextInRange([[oldBufferRange.start.row, 0], oldBufferRange.start]) @@ -403,7 +403,7 @@ class Selection extends Model else if options.autoDecreaseIndent and NonWhitespaceRegExp.test(text) @editor.autoDecreaseIndentForBufferRow(newBufferRange.start.row) - @autoscroll() if @isLastSelection() + @autoscroll() if options.autoscroll ? @isLastSelection() newBufferRange diff --git a/src/style-manager.js b/src/style-manager.js index b273f449b..7ee11fd6d 100644 --- a/src/style-manager.js +++ b/src/style-manager.js @@ -270,7 +270,8 @@ function transformDeprecatedShadowDOMSelectors (css, context) { } } else { if (previousNodeIsAtomTextEditor && node.type === 'pseudo' && node.value === '::shadow') { - selector.removeChild(node) + node.type = 'className' + node.value = '.editor' targetsAtomTextEditorShadow = true } } diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index cccd2f4c8..5dfbced62 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -42,7 +42,7 @@ class TextEditorComponent @assert domNode?, "TextEditorComponent::domNode was set to null." @domNodeValue = domNode - constructor: ({@editor, @hostElement, tileSize, @views, @themes, @styles, @assert}) -> + constructor: ({@editor, @hostElement, tileSize, @views, @themes, @styles, @assert, hiddenInputElement}) -> @tileSize = tileSize if tileSize? @disposables = new CompositeDisposable @@ -70,8 +70,12 @@ class TextEditorComponent @scrollViewNode.classList.add('scroll-view') @domNode.appendChild(@scrollViewNode) - @hiddenInputComponent = new InputComponent - @scrollViewNode.appendChild(@hiddenInputComponent.getDomNode()) + @hiddenInputComponent = new InputComponent(hiddenInputElement) + @scrollViewNode.appendChild(hiddenInputElement) + # Add a getModel method to the hidden input component to make it easy to + # access the editor in response to DOM events or when using + # document.activeElement. + hiddenInputElement.getModel = => @editor @linesComponent = new LinesComponent({@presenter, @domElementPool, @assert, @grammars, @views}) @scrollViewNode.appendChild(@linesComponent.getDomNode()) @@ -342,7 +346,6 @@ class TextEditorComponent focused: -> if @mounted @presenter.setFocused(true) - @hiddenInputComponent.getDomNode().focus() blurred: -> if @mounted @@ -416,7 +419,6 @@ class TextEditorComponent onScrollViewScroll: => if @mounted - console.warn "TextEditorScrollView scrolled when it shouldn't have." @scrollViewNode.scrollTop = 0 @scrollViewNode.scrollLeft = 0 @@ -612,7 +614,7 @@ class TextEditorComponent screenRange = new Range(startPosition, startPosition).union(initialRange) @editor.getLastSelection().setScreenRange(screenRange, reversed: true, autoscroll: false, preserveFolds: true) else - endPosition = [dragRow + 1, 0] + endPosition = @editor.clipScreenPosition([dragRow + 1, 0], clipDirection: 'backward') screenRange = new Range(endPosition, endPosition).union(initialRange) @editor.getLastSelection().setScreenRange(screenRange, reversed: false, autoscroll: false, preserveFolds: true) @@ -905,7 +907,7 @@ class TextEditorComponent screenRowForNode: (node) -> while node? - if screenRow = node.dataset.screenRow + if screenRow = node.dataset?.screenRow return parseInt(screenRow) node = node.parentElement null diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 4a7d1598d..8c9792916 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -25,8 +25,17 @@ class TextEditorElement extends HTMLElement @emitter = new Emitter @subscriptions = new CompositeDisposable + @hiddenInputElement = document.createElement('input') + @hiddenInputElement.classList.add('hidden-input') + @hiddenInputElement.setAttribute('tabindex', -1) + @hiddenInputElement.setAttribute('data-react-skip-selection-restoration', true) + @hiddenInputElement.style['-webkit-transform'] = 'translateZ(0)' + @hiddenInputElement.addEventListener 'paste', (event) -> event.preventDefault() + @addEventListener 'focus', @focused.bind(this) @addEventListener 'blur', @blurred.bind(this) + @hiddenInputElement.addEventListener 'focus', @focused.bind(this) + @hiddenInputElement.addEventListener 'blur', @inputNodeBlurred.bind(this) @classList.add('editor') @setAttribute('tabindex', -1) @@ -117,12 +126,10 @@ class TextEditorElement extends HTMLElement themes: @themes styles: @styles workspace: @workspace - assert: @assert + assert: @assert, + hiddenInputElement: @hiddenInputElement ) @rootElement.appendChild(@component.getDomNode()) - inputNode = @component.hiddenInputComponent.getDomNode() - inputNode.addEventListener 'focus', @focused.bind(this) - inputNode.addEventListener 'blur', @inputNodeBlurred.bind(this) unmountComponent: -> if @component? @@ -132,16 +139,17 @@ class TextEditorElement extends HTMLElement focused: (event) -> @component?.focused() + @hiddenInputElement.focus() blurred: (event) -> - if event.relatedTarget is @component?.hiddenInputComponent.getDomNode() + if event.relatedTarget is @hiddenInputElement event.stopImmediatePropagation() return @component?.blurred() inputNodeBlurred: (event) -> if event.relatedTarget isnt this - @dispatchEvent(new FocusEvent('blur', bubbles: false)) + @dispatchEvent(new FocusEvent('blur', relatedTarget: event.relatedTarget, bubbles: false)) addGrammarScopeAttribute: -> @dataset.grammar = @model.getGrammar()?.scopeName?.replace(/\./g, ' ') diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 22268af18..3c4739ca5 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -306,9 +306,6 @@ class TextEditorPresenter getEndTileRow: -> @tileForRow(@endRow ? 0) - isValidScreenRow: (screenRow) -> - screenRow >= 0 and screenRow < @model.getApproximateScreenLineCount() - getScreenRowsToRender: -> startRow = @getStartTileRow() endRow = @getEndTileRow() + @tileSize @@ -320,7 +317,7 @@ class TextEditorPresenter if @screenRowsToMeasure? screenRows.push(@screenRowsToMeasure...) - screenRows = screenRows.filter @isValidScreenRow.bind(this) + screenRows = screenRows.filter (row) -> row >= 0 screenRows.sort (a, b) -> a - b _.uniq(screenRows, true) @@ -395,19 +392,17 @@ class TextEditorPresenter visibleTiles[tileStartRow] = true zIndex++ - if @mouseWheelScreenRow? and 0 <= @mouseWheelScreenRow < @model.getApproximateScreenLineCount() - mouseWheelTile = @tileForRow(@mouseWheelScreenRow) - - unless visibleTiles[mouseWheelTile]? - @lineNumberGutter.tiles[mouseWheelTile].display = "none" - @state.content.tiles[mouseWheelTile].display = "none" - visibleTiles[mouseWheelTile] = true + mouseWheelTileId = @tileForRow(@mouseWheelScreenRow) if @mouseWheelScreenRow? for id, tile of @state.content.tiles continue if visibleTiles.hasOwnProperty(id) - delete @state.content.tiles[id] - delete @lineNumberGutter.tiles[id] + if Number(id) is mouseWheelTileId + @state.content.tiles[id].display = "none" + @lineNumberGutter.tiles[id].display = "none" + else + delete @state.content.tiles[id] + delete @lineNumberGutter.tiles[id] updateLinesState: (tileState, screenRows) -> tileState.lines ?= {} @@ -456,7 +451,7 @@ class TextEditorPresenter for decoration in @model.getOverlayDecorations() continue unless decoration.getMarker().isValid() - {item, position, class: klass} = decoration.getProperties() + {item, position, class: klass, avoidOverflow} = decoration.getProperties() if position is 'tail' screenPosition = decoration.getMarker().getTailScreenPosition() else @@ -471,15 +466,16 @@ class TextEditorPresenter if overlayDimensions = @overlayDimensions[decoration.id] {itemWidth, itemHeight, contentMargin} = overlayDimensions - rightDiff = left + itemWidth + contentMargin - @windowWidth - left -= rightDiff if rightDiff > 0 + if avoidOverflow isnt false + rightDiff = left + itemWidth + contentMargin - @windowWidth + left -= rightDiff if rightDiff > 0 - leftDiff = left + contentMargin - left -= leftDiff if leftDiff < 0 + leftDiff = left + contentMargin + left -= leftDiff if leftDiff < 0 - if top + itemHeight > @windowHeight and - top - (itemHeight + @lineHeight) >= 0 - top -= itemHeight + @lineHeight + if top + itemHeight > @windowHeight and + top - (itemHeight + @lineHeight) >= 0 + top -= itemHeight + @lineHeight pixelPosition.top = top pixelPosition.left = left @@ -498,7 +494,10 @@ class TextEditorPresenter return updateLineNumberGutterState: -> - @lineNumberGutter.maxLineNumberDigits = @model.getLineCount().toString().length + @lineNumberGutter.maxLineNumberDigits = Math.max( + 2, + @model.getLineCount().toString().length + ) updateCommonGutterState: -> @sharedGutterStyles.backgroundColor = if @gutterBackgroundColor isnt "rgba(0, 0, 0, 0)" @@ -601,7 +600,8 @@ class TextEditorPresenter line = @linesByScreenRow.get(screenRow) continue unless line? lineId = line.id - {bufferRow, softWrappedAtStart: softWrapped} = @displayLayer.softWrapDescriptorForScreenRow(screenRow) + {row: bufferRow, column: bufferColumn} = @displayLayer.translateScreenPosition(Point(screenRow, 0)) + softWrapped = bufferColumn isnt 0 foldable = not softWrapped and @model.isFoldableAtBufferRow(bufferRow) decorationClasses = @lineNumberDecorationClassesForRow(screenRow) blockDecorationsBeforeCurrentScreenRowHeight = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRow) - @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRow) @@ -1006,8 +1006,7 @@ class TextEditorPresenter @lineHeight? and @baseCharacterWidth? pixelPositionForScreenPosition: (screenPosition) -> - position = - @linesYardstick.pixelPositionForScreenPosition(screenPosition) + position = @linesYardstick.pixelPositionForScreenPosition(screenPosition) position.top -= @getScrollTop() position.left -= @getScrollLeft() @@ -1145,7 +1144,9 @@ class TextEditorPresenter @lineNumberDecorationsByScreenRow[screenRow] ?= {} @lineNumberDecorationsByScreenRow[screenRow][decorationId] = properties else - for row in [screenRange.start.row..screenRange.end.row] by 1 + startRow = Math.max(screenRange.start.row, @getStartTileRow()) + endRow = Math.min(screenRange.end.row, @getEndTileRow() + @tileSize) + for row in [startRow..endRow] by 1 continue if properties.onlyHead and row isnt headScreenPosition.row continue if omitLastRow and row is screenRange.end.row @@ -1230,13 +1231,14 @@ class TextEditorPresenter screenRange.end.column = 0 repositionRegionWithinTile: (region, tileStartRow) -> - region.top += @scrollTop - @lineTopIndex.pixelPositionBeforeBlocksForRow(tileStartRow) - region.left += @scrollLeft + region.top += @scrollTop - @lineTopIndex.pixelPositionBeforeBlocksForRow(tileStartRow) buildHighlightRegions: (screenRange) -> lineHeightInPixels = @lineHeight startPixelPosition = @pixelPositionForScreenPosition(screenRange.start) endPixelPosition = @pixelPositionForScreenPosition(screenRange.end) + startPixelPosition.left += @scrollLeft + endPixelPosition.left += @scrollLeft spannedRows = screenRange.end.row - screenRange.start.row + 1 regions = [] @@ -1413,11 +1415,10 @@ class TextEditorPresenter @emitDidUpdateState() pauseCursorBlinking: -> - if @isCursorBlinking() - @stopBlinkingCursors(true) - @startBlinkingCursorsAfterDelay ?= _.debounce(@startBlinkingCursors, @getCursorBlinkResumeDelay()) - @startBlinkingCursorsAfterDelay() - @emitDidUpdateState() + @stopBlinkingCursors(true) + @startBlinkingCursorsAfterDelay ?= _.debounce(@startBlinkingCursors, @getCursorBlinkResumeDelay()) + @startBlinkingCursorsAfterDelay() + @emitDidUpdateState() requestAutoscroll: (position) -> @pendingScrollLogicalPosition = position diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 6907db8fe..821322e22 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -114,9 +114,6 @@ class TextEditor extends Model throw error state.buffer = state.tokenizedBuffer.buffer - if state.displayLayer = state.buffer.getDisplayLayer(state.displayLayerId) - state.selectionsMarkerLayer = state.displayLayer.getMarkerLayer(state.selectionsMarkerLayerId) - state.assert = atomEnvironment.assert.bind(atomEnvironment) editor = new this(state) if state.registered @@ -167,22 +164,24 @@ class TextEditor extends Model grammar, tabLength, @buffer, @largeFileMode, @assert }) - displayLayerParams = { - invisibles: @getInvisibles(), - softWrapColumn: @getSoftWrapColumn(), - showIndentGuides: not @isMini() and @doesShowIndentGuide(), - atomicSoftTabs: params.atomicSoftTabs ? true, - tabLength: tabLength, - ratioForCharacter: @ratioForCharacter.bind(this), - isWrapBoundary: isWrapBoundary, - foldCharacter: ZERO_WIDTH_NBSP, - softWrapHangingIndent: params.softWrapHangingIndentLength ? 0 - } + unless @displayLayer? + displayLayerParams = { + invisibles: @getInvisibles(), + softWrapColumn: @getSoftWrapColumn(), + showIndentGuides: not @isMini() and @doesShowIndentGuide(), + atomicSoftTabs: params.atomicSoftTabs ? true, + tabLength: tabLength, + ratioForCharacter: @ratioForCharacter.bind(this), + isWrapBoundary: isWrapBoundary, + foldCharacter: ZERO_WIDTH_NBSP, + softWrapHangingIndent: params.softWrapHangingIndentLength ? 0 + } - if @displayLayer? - @displayLayer.reset(displayLayerParams) - else - @displayLayer = @buffer.addDisplayLayer(displayLayerParams) + if @displayLayer = @buffer.getDisplayLayer(params.displayLayerId) + @displayLayer.reset(displayLayerParams) + @selectionsMarkerLayer = @displayLayer.getMarkerLayer(params.selectionsMarkerLayerId) + else + @displayLayer = @buffer.addDisplayLayer(displayLayerParams) @backgroundWorkHandle = requestIdleCallback(@doBackgroundWork) @disposables.add new Disposable => @@ -272,12 +271,12 @@ class TextEditor extends Model when 'softWrapAtPreferredLineLength' if value isnt @softWrapAtPreferredLineLength @softWrapAtPreferredLineLength = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() if @isSoftWrapped() + displayLayerParams.softWrapColumn = @getSoftWrapColumn() when 'preferredLineLength' if value isnt @preferredLineLength @preferredLineLength = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() if @isSoftWrapped() + displayLayerParams.softWrapColumn = @getSoftWrapColumn() when 'mini' if value isnt @mini @@ -322,12 +321,12 @@ class TextEditor extends Model when 'editorWidthInChars' if value > 0 and value isnt @editorWidthInChars @editorWidthInChars = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() if @isSoftWrapped() + displayLayerParams.softWrapColumn = @getSoftWrapColumn() when 'width' if value isnt @width @width = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() if @isSoftWrapped() + displayLayerParams.softWrapColumn = @getSoftWrapColumn() when 'scrollPastEnd' if value isnt @scrollPastEnd @@ -346,8 +345,7 @@ class TextEditor extends Model else throw new TypeError("Invalid TextEditor parameter: '#{param}'") - if Object.keys(displayLayerParams).length > 0 - @displayLayer.reset(displayLayerParams) + @displayLayer.reset(displayLayerParams) if @editorElement? @editorElement.views.getNextUpdatePromise() @@ -412,14 +410,15 @@ class TextEditor extends Model destroyed: -> @disposables.dispose() @displayLayer.destroy() - @disposables.dispose() @tokenizedBuffer.destroy() selection.destroy() for selection in @selections.slice() - @selectionsMarkerLayer.destroy() @buffer.release() @languageMode.destroy() @gutterContainer.destroy() @emitter.emit 'did-destroy' + @emitter.clear() + @editorElement = null + @presenter = null ### Section: Event Subscription @@ -892,8 +891,8 @@ class TextEditor extends Model # Determine whether the user should be prompted to save before closing # this editor. - shouldPromptToSave: ({windowCloseRequested}={}) -> - if windowCloseRequested + shouldPromptToSave: ({windowCloseRequested, projectHasPaths}={}) -> + if windowCloseRequested and projectHasPaths false else @isModified() and not @buffer.hasMultipleEditors() @@ -982,10 +981,7 @@ class TextEditor extends Model @bufferRowForScreenRow(screenRow) screenRowForBufferRow: (row) -> - if @largeFileMode - row - else - @displayLayer.translateBufferPosition(Point(row, 0)).row + @displayLayer.translateBufferPosition(Point(row, 0)).row getRightmostScreenPosition: -> @displayLayer.getRightmostScreenPosition() @@ -1076,8 +1072,8 @@ class TextEditor extends Model ) # Essential: For each selection, replace the selected text with a newline. - insertNewline: -> - @insertText('\n') + insertNewline: (options) -> + @insertText('\n', options) # Essential: For each selection, if the selection is empty, delete the character # following the cursor. Otherwise delete the selected text. @@ -1134,13 +1130,13 @@ class TextEditor extends Model # Don't move the last line of a multi-line selection if the selection ends at column 0 endRow-- - {bufferRow: startRow} = @displayLayer.lineStartBoundaryForBufferRow(startRow) - {bufferRow: endRow} = @displayLayer.lineEndBoundaryForBufferRow(endRow) + startRow = @displayLayer.findBoundaryPrecedingBufferRow(startRow) + endRow = @displayLayer.findBoundaryFollowingBufferRow(endRow + 1) linesRange = new Range(Point(startRow, 0), Point(endRow, 0)) # If selected line range is preceded by a fold, one line above on screen # could be multiple lines in the buffer. - {bufferRow: precedingRow} = @displayLayer.lineStartBoundaryForBufferRow(startRow - 1) + precedingRow = @displayLayer.findBoundaryPrecedingBufferRow(startRow - 1) insertDelta = linesRange.start.row - precedingRow # Any folds in the text that is moved will need to be re-created. @@ -1196,15 +1192,15 @@ class TextEditor extends Model # Don't move the last line of a multi-line selection if the selection ends at column 0 endRow-- - {bufferRow: startRow} = @displayLayer.lineStartBoundaryForBufferRow(startRow) - {bufferRow: endRow} = @displayLayer.lineEndBoundaryForBufferRow(endRow) + startRow = @displayLayer.findBoundaryPrecedingBufferRow(startRow) + endRow = @displayLayer.findBoundaryFollowingBufferRow(endRow + 1) linesRange = new Range(Point(startRow, 0), Point(endRow, 0)) # If selected line range is followed by a fold, one line below on screen # could be multiple lines in the buffer. But at the same time, if the # next buffer row is wrapped, one line in the buffer can represent many # screen rows. - {bufferRow: followingRow} = @displayLayer.lineEndBoundaryForBufferRow(endRow) + followingRow = Math.min(@buffer.getLineCount(), @displayLayer.findBoundaryFollowingBufferRow(endRow + 1)) insertDelta = followingRow - linesRange.end.row # Any folds in the text that is moved will need to be re-created. @@ -1276,30 +1272,44 @@ class TextEditor extends Model @setSelectedBufferRanges(translatedRanges) - # Duplicate the most recent cursor's current line. duplicateLines: -> @transact => - for selection in @getSelectionsOrderedByBufferPosition().reverse() - selectedBufferRange = selection.getBufferRange() - if selection.isEmpty() - {start} = selection.getScreenRange() - selection.setScreenRange([[start.row, 0], [start.row + 1, 0]], preserveFolds: true) + selections = @getSelectionsOrderedByBufferPosition() + previousSelectionRanges = [] - [startRow, endRow] = selection.getBufferRowRange() + i = selections.length - 1 + while i >= 0 + j = i + previousSelectionRanges[i] = selections[i].getBufferRange() + if selections[i].isEmpty() + {start} = selections[i].getScreenRange() + selections[i].setScreenRange([[start.row, 0], [start.row + 1, 0]], preserveFolds: true) + [startRow, endRow] = selections[i].getBufferRowRange() endRow++ + while i > 0 + [previousSelectionStartRow, previousSelectionEndRow] = selections[i - 1].getBufferRowRange() + if previousSelectionEndRow is startRow + startRow = previousSelectionStartRow + previousSelectionRanges[i - 1] = selections[i - 1].getBufferRange() + i-- + else + break intersectingFolds = @displayLayer.foldsIntersectingBufferRange([[startRow, 0], [endRow, 0]]) - rangeToDuplicate = [[startRow, 0], [endRow, 0]] - textToDuplicate = @getTextInBufferRange(rangeToDuplicate) + textToDuplicate = @getTextInBufferRange([[startRow, 0], [endRow, 0]]) textToDuplicate = '\n' + textToDuplicate if endRow > @getLastBufferRow() @buffer.insert([endRow, 0], textToDuplicate) - delta = endRow - startRow - selection.setBufferRange(selectedBufferRange.translate([delta, 0])) + insertedRowCount = endRow - startRow + + for k in [i..j] by 1 + selections[k].setBufferRange(previousSelectionRanges[k].translate([insertedRowCount, 0])) + for fold in intersectingFolds foldRange = @displayLayer.bufferRangeForFold(fold) - @displayLayer.foldBufferRange(foldRange.translate([delta, 0])) - return + @displayLayer.foldBufferRange(foldRange.translate([insertedRowCount, 0])) + + i-- replaceSelectedText: (options={}, fn) -> {selectWordIfEmpty} = options @@ -1740,10 +1750,14 @@ class TextEditor extends Model # * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied # if the associated `DisplayMarker` is non-empty. Only applicable to the # `gutter`, `line`, and `line-number` types. - # * `position` (optional) Only applicable to decorations of type `overlay` and `block`, - # controls where the view is positioned relative to the `TextEditorMarker`. + # * `position` (optional) Only applicable to decorations of type `overlay` and `block`. + # Controls where the view is positioned relative to the `TextEditorMarker`. # Values can be `'head'` (the default) or `'tail'` for overlay decorations, and # `'before'` (the default) or `'after'` for block decorations. + # * `avoidOverflow` (optional) Only applicable to decorations of type + # `overlay`. Determines whether the decoration adjusts its horizontal or + # vertical position to remain fully visible when it would otherwise + # overflow the editor. Defaults to `true`. # # Returns a {Decoration} object decorateMarker: (marker, decorationParams) -> @@ -2916,11 +2930,7 @@ class TextEditor extends Model # Essential: Determine whether lines in this editor are soft-wrapped. # # Returns a {Boolean}. - isSoftWrapped: -> - if @largeFileMode - false - else - @softWrapped + isSoftWrapped: -> @softWrapped # Essential: Enable or disable soft wrapping for this editor. # diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index 32fabf724..58297b2db 100644 --- a/src/theme-manager.coffee +++ b/src/theme-manager.coffee @@ -178,7 +178,8 @@ class ThemeManager @requireStylesheet(nativeStylesheetPath) stylesheetElementForId: (id) -> - document.head.querySelector("atom-styles style[source-path=\"#{id}\"]") + escapedId = id.replace(/\\/g, '\\\\') + document.head.querySelector("atom-styles style[source-path=\"#{escapedId}\"]") resolveStylesheet: (stylesheetPath) -> if path.extname(stylesheetPath).length > 0 @@ -231,9 +232,6 @@ class ThemeManager applyStylesheet: (path, text) -> @styleSheetDisposablesBySourcePath[path] = @styleManager.addStyleSheet(text, sourcePath: path) - stringToId: (string) -> - string.replace(/\\/g, '/') - activateThemes: -> new Promise (resolve) => # @config.observe runs the callback once, then on subsequent changes. diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index ce56e0388..234f82be9 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -41,6 +41,7 @@ class TokenizedBuffer extends Model destroyed: -> @disposables.dispose() + @tokenizedLines.length = 0 buildIterator: -> new TokenizedBufferIterator(this) @@ -94,6 +95,7 @@ class TokenizedBuffer extends Model false retokenizeLines: -> + return unless @alive @fullyTokenized = false @tokenizedLines = new Array(@buffer.getLineCount()) @invalidRows = [] @@ -198,10 +200,7 @@ class TokenizedBuffer extends Model @invalidateRow(end + delta + 1) isFoldableAtRow: (row) -> - if @largeFileMode - false - else - @isFoldableCodeAtRow(row) or @isFoldableCommentAtRow(row) + @isFoldableCodeAtRow(row) or @isFoldableCommentAtRow(row) # Returns a {Boolean} indicating whether the given buffer row starts # a a foldable row range due to the code's indentation patterns. diff --git a/src/tooltip-manager.coffee b/src/tooltip-manager.coffee index 4419ec740..03630c87f 100644 --- a/src/tooltip-manager.coffee +++ b/src/tooltip-manager.coffee @@ -56,6 +56,7 @@ class TooltipManager {delay: {show: 1000, hide: 100}} constructor: ({@keymapManager, @viewRegistry}) -> + @tooltips = new Map() # Essential: Add a tooltip to the given element. # @@ -129,19 +130,42 @@ class TooltipManager tooltip = new Tooltip(target, options, @viewRegistry) + if not @tooltips.has(target) + @tooltips.set(target, []) + @tooltips.get(target).push(tooltip) + hideTooltip = -> tooltip.leave(currentTarget: target) tooltip.hide() window.addEventListener('resize', hideTooltip) - disposable = new Disposable -> + disposable = new Disposable => window.removeEventListener('resize', hideTooltip) hideTooltip() tooltip.destroy() + if @tooltips.has(target) + tooltipsForTarget = @tooltips.get(target) + index = tooltipsForTarget.indexOf(tooltip) + if index isnt -1 + tooltipsForTarget.splice(index, 1) + if tooltipsForTarget.length is 0 + @tooltips.delete(target) + disposable + # Extended: Find the tooltips that have been applied to the given element. + # + # * `target` The `HTMLElement` to find tooltips on. + # + # Returns an {Array} of `Tooltip` objects that match the `target`. + findTooltips: (target) -> + if @tooltips.has(target) + @tooltips.get(target).slice() + else + [] + humanizeKeystrokes = (keystroke) -> keystrokes = keystroke.split(' ') keystrokes = (_.humanizeKeystroke(stroke) for stroke in keystrokes) diff --git a/src/tooltip.js b/src/tooltip.js index f0f9d1a3f..2573b1fb9 100644 --- a/src/tooltip.js +++ b/src/tooltip.js @@ -7,6 +7,8 @@ const listen = require('./delegated-listener') // This tooltip class is derived from Bootstrap 3, but modified to not require // jQuery, which is an expensive dependency we want to eliminate. +var followThroughTimer = null + var Tooltip = function (element, options, viewRegistry) { this.options = null this.enabled = null @@ -21,7 +23,7 @@ var Tooltip = function (element, options, viewRegistry) { Tooltip.VERSION = '3.3.5' -Tooltip.TRANSITION_DURATION = 150 +Tooltip.FOLLOW_THROUGH_DURATION = 300 Tooltip.DEFAULTS = { animation: true, @@ -151,7 +153,11 @@ Tooltip.prototype.enter = function (event) { this.hoverState = 'in' - if (!this.options.delay || !this.options.delay.show) return this.show() + if (!this.options.delay || + !this.options.delay.show || + followThroughTimer) { + return this.show() + } this.timeout = setTimeout(function () { if (this.hoverState === 'in') this.show() @@ -343,6 +349,14 @@ Tooltip.prototype.hide = function (callback) { this.hoverState = null + clearTimeout(followThroughTimer) + followThroughTimer = setTimeout( + function () { + followThroughTimer = null + }, + Tooltip.FOLLOW_THROUGH_DURATION + ) + return this } diff --git a/src/update-process-env.js b/src/update-process-env.js index 6544a6612..00bb13927 100644 --- a/src/update-process-env.js +++ b/src/update-process-env.js @@ -1,7 +1,7 @@ /** @babel */ import fs from 'fs' -import {spawnSync} from 'child_process' +import childProcess from 'child_process' const ENVIRONMENT_VARIABLES_TO_PRESERVE = new Set([ 'NODE_ENV', @@ -15,12 +15,14 @@ const PLATFORMS_KNOWN_TO_WORK = new Set([ 'linux' ]) -function updateProcessEnv (launchEnv) { +async function updateProcessEnv (launchEnv) { let envToAssign - if (launchEnv && shouldGetEnvFromShell(launchEnv)) { - envToAssign = getEnvFromShell(launchEnv) - } else if (launchEnv && launchEnv.PWD) { - envToAssign = launchEnv + if (launchEnv) { + if (shouldGetEnvFromShell(launchEnv)) { + envToAssign = await getEnvFromShell(launchEnv) + } else if (launchEnv.PWD) { + envToAssign = launchEnv + } } if (envToAssign) { @@ -58,24 +60,64 @@ function shouldGetEnvFromShell (env) { return true } -function getEnvFromShell (env) { - if (!shouldGetEnvFromShell(env)) { - return - } - - let {stdout} = spawnSync(env.SHELL, ['-ilc', 'command env'], {encoding: 'utf8'}) - if (stdout) { - let result = {} - for (let line of stdout.split('\n')) { - if (line.includes('=')) { - let components = line.split('=') - let key = components.shift() - let value = components.join('=') - result[key] = value +async function getEnvFromShell (env) { + let {stdout, error} = await new Promise((resolve) => { + let child + let error + let stdout = '' + let done = false + const cleanup = () => { + if (!done && child) { + child.kill() + done = true } } - return result + process.once('exit', cleanup) + setTimeout(() => { + cleanup() + }, 5000) + child = childProcess.spawn(env.SHELL, ['-ilc', 'command env'], {encoding: 'utf8', detached: true, stdio: ['ignore', 'pipe', process.stderr]}) + const buffers = [] + child.on('error', (e) => { + done = true + error = e + }) + child.stdout.on('data', (data) => { + buffers.push(data) + }) + child.on('close', (code, signal) => { + done = true + process.removeListener('exit', cleanup) + if (buffers.length) { + stdout = Buffer.concat(buffers).toString('utf8') + } + + resolve({stdout, error}) + }) + }) + + if (error) { + if (error.handle) { + error.handle() + } + console.log('warning: ' + env.SHELL + ' -ilc "command env" failed with signal (' + error.signal + ')') + console.log(error) } + + if (!stdout || stdout.trim() === '') { + return null + } + + let result = {} + for (let line of stdout.split('\n')) { + if (line.includes('=')) { + let components = line.split('=') + let key = components.shift() + let value = components.join('=') + result[key] = value + } + } + return result } export default { updateProcessEnv, shouldGetEnvFromShell } diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index 559c8ac29..62ce4527a 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -148,7 +148,8 @@ class WindowEventHandler @document.body.classList.remove("fullscreen") handleWindowBeforeunload: (event) => - confirmed = @atomEnvironment.workspace?.confirmClose(windowCloseRequested: true) + projectHasPaths = @atomEnvironment.project.getPaths().length > 0 + confirmed = @atomEnvironment.workspace?.confirmClose(windowCloseRequested: true, projectHasPaths: projectHasPaths) if confirmed and not @reloadRequested and not @atomEnvironment.inSpecMode() and @atomEnvironment.getCurrentWindow().isWebViewFocused() @atomEnvironment.hide() @reloadRequested = false diff --git a/src/workspace.coffee b/src/workspace.coffee index 89c53b678..9871db224 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -182,7 +182,7 @@ class Workspace extends Model projectPath = _.find projectPaths, (projectPath) -> itemPath is projectPath or itemPath?.startsWith(projectPath + path.sep) itemTitle ?= "untitled" - projectPath ?= projectPaths[0] + projectPath ?= if itemPath then path.dirname(itemPath) else null if projectPath? projectPath = fs.tildify(projectPath) @@ -441,7 +441,7 @@ class Workspace extends Model # 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? + if uri? and (not url.parse(uri).protocol? or process.platform is 'win32') @applicationDelegate.addRecentDocument(uri) pane = @paneContainer.paneForURI(uri) if searchAllPanes diff --git a/static/index.js b/static/index.js index b76477e03..2966eafdf 100644 --- a/static/index.js +++ b/static/index.js @@ -116,14 +116,12 @@ }) } - var currentWindow = require('electron').remote.getCurrentWindow() - if (currentWindow.devToolsWebContents) { + const webContents = require('electron').remote.getCurrentWindow().webContents + if (webContents.devToolsWebContents) { profile() } else { - currentWindow.openDevTools() - currentWindow.once('devtools-opened', function () { - setTimeout(profile, 1000) - }) + webContents.once('devtools-opened', () => { setTimeout(profile, 1000) }) + webContents.openDevTools() } } diff --git a/static/jasmine.less b/static/jasmine.less index 7dd1fcdb2..ab2695179 100644 --- a/static/jasmine.less +++ b/static/jasmine.less @@ -187,7 +187,7 @@ body { margin: 5px 0 0 0; border-radius: 2px; line-height: 18px; - color: #666; + color: #ccc; border: 1px solid #ddd; overflow: auto; } diff --git a/static/octicons-2.1.2.woff b/static/octicons-2.1.2.woff new file mode 100755 index 000000000..2b770e429 Binary files /dev/null and b/static/octicons-2.1.2.woff differ diff --git a/static/octicons.less b/static/octicons.less index 03e02122c..87e8dab9a 100644 --- a/static/octicons.less +++ b/static/octicons.less @@ -23,7 +23,10 @@ .make-icon(arrow-small-right); .make-icon(arrow-small-up); .make-icon(arrow-up); +.make-icon(beaker); .make-icon(beer); +.make-icon(bell); +.make-icon(bold); .make-icon(book); .make-icon(bookmark); .make-icon(briefcase); @@ -52,6 +55,7 @@ .make-icon(dash); .make-icon(dashboard); .make-icon(database); +.make-icon(desktop-download); .make-icon(device-camera); .make-icon(device-camera-video); .make-icon(device-desktop); @@ -62,10 +66,12 @@ .make-icon(diff-modified); .make-icon(diff-removed); .make-icon(diff-renamed); +.make-icon(ellipses); .make-icon(ellipsis); .make-icon(eye); .make-icon(eye-unwatch); .make-icon(eye-watch); +.make-icon(file); .make-icon(file-add); .make-icon(file-binary); .make-icon(file-code); @@ -97,6 +103,7 @@ .make-icon(git-pull-request); .make-icon(git-pull-request-abandoned); .make-icon(globe); +.make-icon(grabber); .make-icon(graph); .make-icon(heart); .make-icon(history); @@ -109,6 +116,7 @@ .make-icon(issue-closed); .make-icon(issue-opened); .make-icon(issue-reopened); +.make-icon(italic); .make-icon(jersey); .make-icon(jump-down); .make-icon(jump-left); @@ -126,6 +134,7 @@ .make-icon(lock); .make-icon(log-in); .make-icon(log-out); +.make-icon(logo-gist); .make-icon(logo-github); .make-icon(mail); .make-icon(mail-read); @@ -161,6 +170,7 @@ .make-icon(playback-rewind); .make-icon(plug); .make-icon(plus); +.make-icon(plus-small); .make-icon(podium); .make-icon(primitive-dot); .make-icon(primitive-square); @@ -170,6 +180,7 @@ .make-icon(quote); .make-icon(radio-tower); .make-icon(remove-close); +.make-icon(reply); .make-icon(repo); .make-icon(repo-clone); .make-icon(repo-create); @@ -188,8 +199,10 @@ .make-icon(search-save); .make-icon(server); .make-icon(settings); +.make-icon(shield); .make-icon(sign-in); .make-icon(sign-out); +.make-icon(smiley); .make-icon(split); .make-icon(squirrel); .make-icon(star); @@ -201,9 +214,13 @@ .make-icon(tag); .make-icon(tag-add); .make-icon(tag-remove); +.make-icon(tasklist); .make-icon(telescope); .make-icon(terminal); +.make-icon(text-size); .make-icon(three-bars); +.make-icon(thumbsdown); +.make-icon(thumbsup); .make-icon(tools); .make-icon(trashcan); .make-icon(triangle-down); @@ -212,6 +229,9 @@ .make-icon(triangle-up); .make-icon(unfold); .make-icon(unmute); +.make-icon(unverified); +.make-icon(verified); .make-icon(versions); +.make-icon(watch); .make-icon(x); .make-icon(zap); diff --git a/static/octicons.woff b/static/octicons.woff old mode 100755 new mode 100644 index 2b770e429..38337a82a Binary files a/static/octicons.woff and b/static/octicons.woff differ diff --git a/static/scaffolding.less b/static/scaffolding.less index 9b4e75302..e2b337d00 100644 --- a/static/scaffolding.less +++ b/static/scaffolding.less @@ -5,6 +5,7 @@ // Octicon font // -------------------------------------------------- +@font-face { .octicon-font-legacy(); } // keep for backwards compatibility @font-face { .octicon-font(); } diff --git a/static/variables/octicon-mixins.less b/static/variables/octicon-mixins.less index 0cb614814..c58ce3a52 100644 --- a/static/variables/octicon-mixins.less +++ b/static/variables/octicon-mixins.less @@ -32,10 +32,17 @@ } } +// keep for backwards compatibility +.octicon-font-legacy() { + font-family: 'Octicons Regular'; + src: url("octicons-2.1.2.woff") format("woff"); + font-weight: normal; + font-style: normal; +} + .octicon-font() { font-family: 'Octicons Regular'; src: url("octicons.woff") format("woff"); font-weight: normal; font-style: normal; } - diff --git a/static/variables/octicon-utf-codes.less b/static/variables/octicon-utf-codes.less index e3ea6d7f6..18c859737 100644 --- a/static/variables/octicon-utf-codes.less +++ b/static/variables/octicon-utf-codes.less @@ -10,7 +10,10 @@ @arrow-small-right: "\f071"; @arrow-small-up: "\f09f"; @arrow-up: "\f03d"; +@beaker: "\f0dd"; @beer: "\f069"; +@bell: "\f0de"; +@bold: "\f0e2"; @book: "\f007"; @bookmark: "\f07b"; @briefcase: "\f0d3"; @@ -39,6 +42,7 @@ @dash: "\f0ca"; @dashboard: "\f07d"; @database: "\f096"; +@desktop-download: "\f0dc"; @device-camera: "\f056"; @device-camera-video: "\f057"; @device-desktop: "\f27c"; @@ -49,10 +53,12 @@ @diff-modified: "\f06d"; @diff-removed: "\f06c"; @diff-renamed: "\f06e"; +@ellipses: "\f101"; @ellipsis: "\f09a"; @eye: "\f04e"; @eye-unwatch: "\f04e"; @eye-watch: "\f04e"; +@file: "\f102"; @file-add: "\f05d"; @file-binary: "\f094"; @file-code: "\f010"; @@ -84,6 +90,7 @@ @git-pull-request: "\f009"; @git-pull-request-abandoned: "\f009"; @globe: "\f0b6"; +@grabber: "\f103"; @graph: "\f043"; @heart: "\2665"; @history: "\f07e"; @@ -96,6 +103,7 @@ @issue-closed: "\f028"; @issue-opened: "\f026"; @issue-reopened: "\f027"; +@italic: "\f0e4"; @jersey: "\f019"; @jump-down: "\f072"; @jump-left: "\f0a5"; @@ -113,6 +121,7 @@ @lock: "\f06a"; @log-in: "\f036"; @log-out: "\f032"; +@logo-gist: "\f0ad"; @logo-github: "\f092"; @mail: "\f03b"; @mail-read: "\f03c"; @@ -148,6 +157,7 @@ @playback-rewind: "\f0bc"; @plug: "\f0d4"; @plus: "\f05d"; +@plus-small: "\f104"; @podium: "\f0af"; @primitive-dot: "\f052"; @primitive-square: "\f053"; @@ -157,6 +167,7 @@ @quote: "\f063"; @radio-tower: "\f030"; @remove-close: "\f081"; +@reply: "\f105"; @repo: "\f001"; @repo-clone: "\f04c"; @repo-create: "\f05d"; @@ -175,8 +186,10 @@ @search-save: "\f02e"; @server: "\f097"; @settings: "\f07c"; +@shield: "\f0e1"; @sign-in: "\f036"; @sign-out: "\f032"; +@smiley: "\f0e7"; @split: "\f0c6"; @squirrel: "\f0b2"; @star: "\f02a"; @@ -188,9 +201,13 @@ @tag: "\f015"; @tag-add: "\f015"; @tag-remove: "\f015"; +@tasklist: "\f0e5"; @telescope: "\f088"; @terminal: "\f0c8"; +@text-size: "\f0e3"; @three-bars: "\f05e"; +@thumbsdown: "\f0db"; +@thumbsup: "\f0da"; @tools: "\f031"; @trashcan: "\f0d0"; @triangle-down: "\f05b"; @@ -199,6 +216,9 @@ @triangle-up: "\f0aa"; @unfold: "\f039"; @unmute: "\f0ba"; +@unverified: "\f0e8"; +@verified: "\f0e6"; @versions: "\f064"; +@watch: "\f0e0"; @x: "\f081"; @zap: "\26A1";