Merge branch 'master' into sm-atom-workspace-axis

This commit is contained in:
simurai
2016-05-19 11:45:41 +09:00
129 changed files with 3949 additions and 6669 deletions

View File

@@ -1,24 +1,46 @@
# Contributor Code of Conduct
# Contributor Covenant Code of Conduct
As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
## Our Pledge
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery
- Personal attacks
- Trolling or insulting/derogatory comments
- Public or private harassment
- Publishing other's private information, such as physical or electronic addresses, without explicit permission
- Other unethical or unprofessional conduct
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer at [atom@github.com](mailto:atom@github.com). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident.
## Enforcement
This Code of Conduct is adapted from the Contributor Covenant, version 1.3.0, available from http://contributor-covenant.org/version/1/3/0/
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [atom@github.com](mailto:atom@github.com). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@@ -50,7 +50,7 @@ To get a sense for the packages that are bundled with Atom, you can go to Settin
Here's a list of the big ones:
* [atom/atom](https://github.com/atom/atom) - Atom Core! The core editor component is responsible for basic text editing (e.g. cursors, selections, scrolling), text indentation, wrapping, and folding, text rendering, editor rendering, file system operations (e.g. saving), and installation and auto-updating. You should also use this repository for feedback related to the [core API](https://atom.io/docs/api/latest/Notification) and for large, overarching design proposals.
* [atom/atom](https://github.com/atom/atom) - Atom Core! The core editor component is responsible for basic text editing (e.g. cursors, selections, scrolling), text indentation, wrapping, and folding, text rendering, editor rendering, file system operations (e.g. saving), and installation and auto-updating. You should also use this repository for feedback related to the [core API](https://atom.io/docs/api/latest) and for large, overarching design proposals.
* [tree-view](https://github.com/atom/tree-view) - file and directory listing on the left of the UI.
* [fuzzy-finder](https://github.com/atom/fuzzy-finder) - the quick file opener.
* [find-and-replace](https://github.com/atom/find-and-replace) - all search and replace functionality.
@@ -62,7 +62,7 @@ Here's a list of the big ones:
* [git-diff](https://github.com/atom/git-diff) - Git change indicators shown in the editor's gutter.
* [language-javascript](https://github.com/atom/language-javascript) - all bundled languages are packages too, and each one has a separate package `language-[name]`. Use these for feedback on syntax highlighting issues that only appear for a specific language.
* [one-dark-ui](https://github.com/atom/one-dark-ui) - the default UI styling for anything but the text editor. UI theme packages (i.e. packages with a `-ui` suffix) provide only styling and it's possible that a bundled package is responsible for a UI issue. There are other other bundled UI themes, such as [one-light-ui](https://github.com/atom/one-light-ui).
* [one-dark-syntax](https://github.com/atom/one-dark-syntax) - the default syntax highlighting styles applied for all languages. There are other other bundled syntax themes, such as [solarized-dark](https://github.com/atom/solarized-dark). You should use these packages for reporting issues that appear in many languages, but disappear if you change to another syntax theme.
* [one-dark-syntax](https://github.com/atom/one-dark-syntax) - the default syntax highlighting styles applied for all languages. There are other other bundled syntax themes, such as [solarized-dark-syntax](https://github.com/atom/solarized-dark-syntax). You should use these packages for reporting issues that appear in many languages, but disappear if you change to another syntax theme.
* [apm](https://github.com/atom/apm) - the `apm` command line tool (Atom Package Manager). You should use this repository for any contributions related to the `apm` tool and to publishing packages.
* [atom.io](https://github.com/atom/atom.io) - the repository for feedback on the [Atom.io website](https://atom.io) and the [Atom.io package API](https://github.com/atom/atom/blob/master/docs/apm-rest-api.md) used by [apm](https://github.com/atom/apm).
@@ -82,7 +82,7 @@ Before creating bug reports, please check [this list](#before-submitting-a-bug-r
#### Before Submitting A Bug Report
* **Check the [debugging guide](https://atom.io/docs/latest/hacking-atom-debugging).** You might be able to find the cause of the problem and fix things yourself. Most importantly, check if you can reproduce the problem [in the latest version of Atom](https://atom.io/docs/latest/hacking-atom-debugging#update-to-the-latest-version), if the problem happens when you run Atom in [safe mode](https://atom.io/docs/latest/hacking-atom-debugging#check-if-the-problem-shows-up-in-safe-mode), and if you can get the desired behavior by changing [Atom's or packages' config settings](https://atom.io/docs/latest/hacking-atom-debugging#check-atom-and-package-settings).
* **Check the [debugging guide](http://flight-manual.atom.io/hacking-atom/sections/debugging/).** You might be able to find the cause of the problem and fix things yourself. Most importantly, check if you can reproduce the problem [in the latest version of Atom](http://flight-manual.atom.io/hacking-atom/sections/debugging/#update-to-the-latest-version), if the problem happens when you run Atom in [safe mode](http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-if-the-problem-shows-up-in-safe-mode), and if you can get the desired behavior by changing [Atom's or packages' config settings](http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-atom-and-package-settings).
* **Check the [FAQs on the forum](https://discuss.atom.io/c/faq)** for a list of common questions and problems.
* **Determine [which repository the problem should be reported in](#atom-and-packages)**.
* **Perform a [cursory search](https://github.com/issues?q=+is%3Aissue+user%3Aatom)** to see if the problem has already been reported. If it has, add a comment to the existing issue instead of opening a new one.
@@ -100,13 +100,13 @@ Explain the problem and include additional details to help maintainers reproduce
* **Explain which behavior you expected to see instead and why.**
* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. If you use the keyboard while following the steps, **record the GIF with the [Keybinding Resolver](https://github.com/atom/keybinding-resolver) shown**. You can use [this tool](http://www.cockos.com/licecap/) to record GIFs on OSX and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.
* **If you're reporting that Atom crashed**, include a crash report with a stack trace from the operating system. On OSX, the crash report will be available in `Console.app` under "Diagnostic and usage information" > "User diagnostic reports". Include the crash report in the issue in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines), a [file attachment](https://help.github.com/articles/file-attachments-on-issues-and-pull-requests/), or put it in a [gist](https://gist.github.com/) and provide link to that gist.
* **If the problem is related to performance**, include a [CPU profile capture and a screenshot](https://atom.io/docs/latest/hacking-atom-debugging#diagnose-performance-problems-with-the-dev-tools-cpu-profiler) with your report.
* **If the problem is related to performance**, include a [CPU profile capture and a screenshot](http://flight-manual.atom.io/hacking-atom/sections/debugging/#diagnose-performance-problems-with-the-dev-tools-cpu-profiler) with your report.
* **If the Chrome's developer tools pane is shown without you triggering it**, that normally means that an exception was thrown. The Console tab will include an entry for the exception. Expand the exception so that the stack trace is visible, and provide the full exception and stack trace in a [code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines) and as a screenshot.
* **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below.
Provide more context by answering these questions:
* **Can you reproduce the problem in [safe mode](https://atom.io/docs/latest/hacking-atom-debugging#check-if-the-problem-shows-up-in-safe-mode)?**
* **Can you reproduce the problem in [safe mode](http://flight-manual.atom.io/hacking-atom/sections/debugging/#diagnose-runtime-performance-problems-with-the-dev-tools-cpu-profiler)?**
* **Did the problem start happening recently** (e.g. after updating to a new version of Atom) or was this always a problem?
* If the problem started happening recently, **can you reproduce the problem in an older version of Atom?** What's the most recent version in which the problem doesn't happen? You can download older versions of Atom from [the releases page](https://github.com/atom/atom/releases).
* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens.
@@ -118,7 +118,7 @@ Include details about your configuration and environment:
* **What's the name and version of the OS you're using**?
* **Are you running Atom in a virtual machine?** If so, which VM software are you using and which operating systems and versions are used for the host and the guest?
* **Which [packages](#atom-and-packages) do you have installed?** You can get that list by running `apm list --installed`.
* **Are you using [local configuration files](https://atom.io/docs/latest/using-atom-basic-customization)** `config.cson`, `keymap.cson`, `snippets.cson`, `styles.less` and `init.coffee` to customize Atom? If so, provide the contents of those files, preferably in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines) or with a link to a [gist](https://gist.github.com/).
* **Are you using [local configuration files](http://flight-manual.atom.io/using-atom/sections/basic-customization/)** `config.cson`, `keymap.cson`, `snippets.cson`, `styles.less` and `init.coffee` to customize Atom? If so, provide the contents of those files, preferably in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines) or with a link to a [gist](https://gist.github.com/).
* **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?
@@ -166,7 +166,7 @@ Before creating enhancement suggestions, please check [this list](#before-submit
#### Before Submitting An Enhancement Suggestion
* **Check the [debugging guide](https://atom.io/docs/latest/hacking-atom-debugging)** for tips — you might discover that the enhancement is already available. Most importantly, check if you're using [the latest version of Atom](https://atom.io/docs/latest/hacking-atom-debugging#update-to-the-latest-version) and if you can get the desired behavior by changing [Atom's or packages' config settings](https://atom.io/docs/latest/hacking-atom-debugging#check-atom-and-package-settings).
* **Check the [debugging guide](http://flight-manual.atom.io/hacking-atom/sections/debugging/)** for tips — you might discover that the enhancement is already available. Most importantly, check if you're using [the latest version of Atom](http://flight-manual.atom.io/hacking-atom/sections/debugging/#update-to-the-latest-version) and if you can get the desired behavior by changing [Atom's or packages' config settings](http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-atom-and-package-settings).
* **Check if there's already [a package](https://atom.io/packages) which provides that enhancement.**
* **Determine [which repository the enhancement should be suggested in](#atom-and-packages).**
* **Perform a [cursory search](https://github.com/issues?q=+is%3Aissue+user%3Aatom)** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
@@ -376,7 +376,7 @@ Please open an issue on `atom/atom` if you have suggestions for new labels, and
| `windows` | [search][search-atom-repo-label-windows] | [search][search-atom-org-label-windows] | Related to Atom running on Windows. |
| `linux` | [search][search-atom-repo-label-linux] | [search][search-atom-org-label-linux] | Related to Atom running on Linux. |
| `mac` | [search][search-atom-repo-label-mac] | [search][search-atom-org-label-mac] | Related to Atom running on OSX. |
| `documentation` | [search][search-atom-repo-label-documentation] | [search][search-atom-org-label-documentation] | Related to any type of documentation (e.g. [API documentation](https://atom.io/docs/api/latest/Atom) and the [flight manual](https://atom.io/docs/latest/)). |
| `documentation` | [search][search-atom-repo-label-documentation] | [search][search-atom-org-label-documentation] | Related to any type of documentation (e.g. [API documentation](https://atom.io/docs/api/latest/) and the [flight manual](http://flight-manual.atom.io/)). |
| `performance` | [search][search-atom-repo-label-performance] | [search][search-atom-org-label-performance] | Related to performance. |
| `security` | [search][search-atom-repo-label-security] | [search][search-atom-org-label-security] | Related to security. |
| `ui` | [search][search-atom-repo-label-ui] | [search][search-atom-org-label-ui] | Related to visual design. |

View File

@@ -1,8 +1,8 @@
### Prerequisites
* [ ] Can you reproduce the problem in [safe mode](https://atom.io/docs/latest/hacking-atom-debugging#check-if-the-problem-shows-up-in-safe-mode)?
* [ ] Are you running the [latest version of Atom](https://atom.io/docs/latest/hacking-atom-debugging#update-to-the-latest-version)?
* [ ] Did you check the [debugging guide](https://atom.io/docs/latest/hacking-atom-debugging)?
* [ ] Can you reproduce the problem in [safe mode](http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-if-the-problem-shows-up-in-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?
@@ -25,4 +25,4 @@ For more information on how to write a good [bug report](https://github.com/atom
### Versions
You can get this information from executing `atom --version` and `apm --version` at the command line.
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.

View File

@@ -76,6 +76,22 @@ Currently only a 64-bit version is available.
The Linux version does not currently automatically update so you will need to
repeat these steps to upgrade to future releases.
### Archive extraction
An archive is available for people who don't want to install `atom` as root.
This version enables you to install multiple Atom versions in parallel. It has been built on Ubuntu 64-bit,
but should be compatible with other Linux distributions.
1. Install dependencies (on Ubuntu): `sudo apt install git gconf2 gconf-service libgtk2.0-0 libudev1 libgcrypt20
libnotify4 libxtst6 libnss3 python gvfs-bin xdg-utils libcap2`
2. Download `atom-amd64.tar.gz` from the [Atom releases page](https://github.com/atom/atom/releases/latest).
3. Run `tar xf atom-amd64.tar.gz` in the directory where you want to extract the Atom folder.
4. Launch Atom using the installed `atom` command from the newly extracted directory.
The Linux version does not currently automatically update so you will need to
repeat these steps to upgrade to future releases.
## Building
* [Linux](docs/build-instructions/linux.md)

View File

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

View File

@@ -57,7 +57,7 @@ module.exports = (grunt) ->
homeDir = process.env.USERPROFILE
contentsDir = shellAppDir
appDir = path.join(shellAppDir, 'resources', 'app')
installDir ?= path.join(process.env.ProgramFiles, appName)
installDir ?= path.join(process.env.LOCALAPPDATA, appName, 'app-dev')
killCommand = 'taskkill /F /IM atom.exe'
else if process.platform is 'darwin'
homeDir = process.env.HOME
@@ -285,6 +285,7 @@ module.exports = (grunt) ->
ciTasks.push('dump-symbols') if process.platform is 'darwin'
ciTasks.push('set-version', 'check-licenses', 'lint', 'generate-asar')
ciTasks.push('mkdeb') if process.platform is 'linux'
ciTasks.push('mktar') if process.platform is 'linux'
ciTasks.push('codesign:exe') if process.platform is 'win32' and not process.env.CI
ciTasks.push('create-windows-installer:installer') if process.platform is 'win32'
ciTasks.push('test') if process.platform is 'darwin'
@@ -297,6 +298,7 @@ module.exports = (grunt) ->
unless process.platform is 'linux' or grunt.option('no-install')
defaultTasks.push 'install'
grunt.registerTask('default', defaultTasks)
grunt.registerTask('build-and-sign', ['download-electron', 'download-electron-chromedriver', 'build', 'set-version', 'generate-asar', 'codesign:app', 'install'])
getDefaultChannelAndReleaseBranch = (version) ->
if version.match(/dev/) or isBuildingPR()

Binary file not shown.

View File

@@ -54,9 +54,9 @@ module.exports = (grunt) ->
# so that it doesn't becomes larger than it needs to be.
ignoredPaths = [
path.join('git-utils', 'deps')
path.join('nodegit', 'vendor')
path.join('nodegit', 'node_modules', 'node-pre-gyp')
path.join('nodegit', 'node_modules', '.bin')
path.join('ohnogit', 'node_modules', 'nodegit', 'vendor')
path.join('ohnogit', 'node_modules', 'nodegit', 'node_modules', 'node-pre-gyp')
path.join('ohnogit', 'node_modules', 'nodegit', 'node_modules', '.bin')
path.join('oniguruma', 'deps')
path.join('less', 'dist')
path.join('bootstrap', 'docs')
@@ -122,9 +122,9 @@ module.exports = (grunt) ->
# Ignore *.cc and *.h files from native modules
ignoredPaths.push "#{_.escapeRegExp(path.join('ctags', 'src') + path.sep)}.*\\.(cc|h)*"
ignoredPaths.push "#{_.escapeRegExp(path.join('git-utils', 'src') + path.sep)}.*\\.(cc|h)*"
ignoredPaths.push "#{_.escapeRegExp(path.join('nodegit', 'src') + path.sep)}.*\\.(cc|h)?"
ignoredPaths.push "#{_.escapeRegExp(path.join('nodegit', 'generate') + path.sep)}.*\\.(cc|h)?"
ignoredPaths.push "#{_.escapeRegExp(path.join('nodegit', 'include') + path.sep)}.*\\.(cc|h)?"
ignoredPaths.push "#{_.escapeRegExp(path.join('ohnogit', 'node_modules', 'nodegit', 'src') + path.sep)}.*\\.(cc|h)?"
ignoredPaths.push "#{_.escapeRegExp(path.join('ohnogit', 'node_modules', 'nodegit', 'generate') + path.sep)}.*\\.(cc|h)?"
ignoredPaths.push "#{_.escapeRegExp(path.join('ohnogit', 'node_modules', 'nodegit', 'include') + path.sep)}.*\\.(cc|h)?"
ignoredPaths.push "#{_.escapeRegExp(path.join('keytar', 'src') + path.sep)}.*\\.(cc|h)*"
ignoredPaths.push "#{_.escapeRegExp(path.join('nslog', 'src') + path.sep)}.*\\.(cc|h)*"
ignoredPaths.push "#{_.escapeRegExp(path.join('oniguruma', 'src') + path.sep)}.*\\.(cc|h)*"
@@ -133,7 +133,6 @@ module.exports = (grunt) ->
ignoredPaths.push "#{_.escapeRegExp(path.join('scrollbar-style', 'src') + path.sep)}.*\\.(cc|h)*"
ignoredPaths.push "#{_.escapeRegExp(path.join('spellchecker', 'src') + path.sep)}.*\\.(cc|h)*"
ignoredPaths.push "#{_.escapeRegExp(path.join('cached-run-in-this-context', 'src') + path.sep)}.*\\.(cc|h)?"
ignoredPaths.push "#{_.escapeRegExp(path.join('marker-index', 'src') + path.sep)}.*\\.(cc|h)?"
ignoredPaths.push "#{_.escapeRegExp(path.join('keyboard-layout', 'src') + path.sep)}.*\\.(cc|h|mm)*"
# Ignore build files

View File

@@ -1,39 +1,71 @@
path = require 'path'
fs = require 'fs'
request = require 'request'
module.exports = (grunt) ->
{spawn} = require('./task-helpers')(grunt)
grunt.registerTask 'codesign:exe', 'Codesign atom.exe and Update.exe', ->
signUsingWindowsSDK = (exeToSign, callback) ->
{WIN_P12KEY_PASSWORD, WIN_P12KEY_URL} = process.env
if WIN_P12KEY_URL?
grunt.log.ok("Obtaining signing key")
downloadedKeyFile = path.resolve(__dirname, 'DownloadedSignKey.p12')
downloadFile WIN_P12KEY_URL, downloadedKeyFile, (done) ->
signUsingWindowsSDKTool exeToSign, downloadedKeyFile, WIN_P12KEY_PASSWORD, (done) ->
fs.unlinkSync(downloadedKeyFile)
callback()
else
signUsingWindowsSDKTool exeToSign, path.resolve(__dirname, '..', 'certs', 'AtomDevTestSignKey.p12'), 'password', callback
signUsingWindowsSDKTool = (exeToSign, keyFilePath, password, callback) ->
grunt.log.ok("Signing #{exeToSign}")
args = ['sign', '/v', '/p', password, '/f', keyFilePath, exeToSign]
spawn {cmd: 'C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v7.1A\\bin\\signtool.exe', args: args}, callback
signUsingJanky = (exeToSign, callback) ->
spawn {cmd: process.env.JANKY_SIGNTOOL, args: [exeToSign]}, callback
signWindowsExecutable = if process.env.JANKY_SIGNTOOL then signUsingJanky else signUsingWindowsSDK
grunt.registerTask 'codesign:exe', 'CodeSign Atom.exe and Update.exe', ->
done = @async()
spawn {cmd: 'taskkill', args: ['/F', '/IM', 'atom.exe']}, ->
cmd = process.env.JANKY_SIGNTOOL ? 'signtool'
atomExePath = path.join(grunt.config.get('atom.shellAppDir'), 'atom.exe')
spawn {cmd, args: [atomExePath]}, (error) ->
signWindowsExecutable atomExePath, (error) ->
return done(error) if error?
updateExePath = path.resolve(__dirname, '..', 'node_modules', 'grunt-electron-installer', 'vendor', 'Update.exe')
spawn {cmd, args: [updateExePath]}, (error) -> done(error)
signWindowsExecutable updateExePath, (error) -> done(error)
grunt.registerTask 'codesign:installer', 'Codesign AtomSetup.exe', ->
grunt.registerTask 'codesign:installer', 'CodeSign AtomSetup.exe', ->
done = @async()
cmd = process.env.JANKY_SIGNTOOL ? 'signtool'
atomSetupExePath = path.resolve(grunt.config.get('atom.buildDir'), 'installer', 'AtomSetup.exe')
spawn {cmd, args: [atomSetupExePath]}, (error) -> done(error)
signWindowsExecutable atomSetupExePath, (error) -> done(error)
grunt.registerTask 'codesign:app', 'Codesign Atom.app', ->
grunt.registerTask 'codesign:app', 'CodeSign Atom.app', ->
done = @async()
unlockKeychain (error) ->
return done(error) if error?
cmd = 'codesign'
args = ['--deep', '--force', '--verbose', '--sign', 'Developer ID Application: GitHub', grunt.config.get('atom.shellAppDir')]
spawn {cmd, args}, (error) -> done(error)
spawn {cmd: 'codesign', args: args}, (error) -> done(error)
unlockKeychain = (callback) ->
return callback() unless process.env.XCODE_KEYCHAIN
cmd = 'security'
{XCODE_KEYCHAIN_PASSWORD, XCODE_KEYCHAIN} = process.env
args = ['unlock-keychain', '-p', XCODE_KEYCHAIN_PASSWORD, XCODE_KEYCHAIN]
spawn {cmd, args}, (error) -> callback(error)
spawn {cmd: 'security', args: args}, (error) -> callback(error)
downloadFile = (sourceUrl, targetPath, callback) ->
options = {
url: sourceUrl
headers: {
'User-Agent': 'Atom Signing Key build task',
'Accept': 'application/vnd.github.VERSION.raw'
}
}
request(options)
.pipe(fs.createWriteStream(targetPath))
.on('finish', callback)

View File

@@ -16,10 +16,22 @@ module.exports = (grunt) ->
{description} = grunt.config.get('atom.metadata')
if process.platform is 'win32'
runas ?= require 'runas'
copyFolder = path.resolve 'script', 'copy-folder.cmd'
if runas('cmd', ['/c', copyFolder, shellAppDir, installDir], admin: true) isnt 0
grunt.log.error("Failed to copy #{shellAppDir} to #{installDir}")
done = @async()
fs.access(installDir, fs.W_OK, (err) ->
adminRequired = true if err
if adminRequired
grunt.log.ok("User does not have write access to #{installDir}, elevating to admin")
runas ?= require 'runas'
copyFolder = path.resolve 'script', 'copy-folder.cmd'
if runas('cmd', ['/c', copyFolder, shellAppDir, installDir], admin: adminRequired) isnt 0
grunt.log.error("Failed to copy #{shellAppDir} to #{installDir}")
else
grunt.log.ok("Installed into #{installDir}")
done()
)
else if process.platform is 'darwin'
rm installDir
mkdir path.dirname(installDir)

View File

@@ -123,10 +123,10 @@ module.exports =
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
'tweetnacl@0.13.2':
'tweetnacl@0.14.3':
repository: 'https://github.com/dchest/tweetnacl-js'
license: 'Public Domain'
source: 'https://github.com/dchest/tweetnacl-js/blob/2f328394f74d83564634fb89ea2798caa3a4edb9/README.md says public domain.'
source: 'https://github.com/dchest/tweetnacl-js/blob/1042c9c65dc8f1dcb9e981d962d7dbbcf58f1fdc/COPYING.txt says public domain.'
'json-schema@0.2.2':
repository: 'https://github.com/kriszyp/json-schema'
license: 'BSD'

View File

@@ -0,0 +1,30 @@
path = require 'path'
module.exports = (grunt) ->
{spawn, fillTemplate} = require('./task-helpers')(grunt)
grunt.registerTask 'mktar', 'Create an archive', ->
done = @async()
appFileName = grunt.config.get('atom.appFileName')
buildDir = grunt.config.get('atom.buildDir')
shellAppDir = grunt.config.get('atom.shellAppDir')
{version, description} = grunt.config.get('atom.metadata')
if process.arch is 'ia32'
arch = 'i386'
else if process.arch is 'x64'
arch = 'amd64'
else
return done("Unsupported arch #{process.arch}")
iconPath = path.join(shellAppDir, 'resources', 'app.asar.unpacked', 'resources', 'atom.png')
cmd = path.join('script', 'mktar')
args = [appFileName, version, arch, iconPath, buildDir]
spawn {cmd, args}, (error) ->
if error?
done(error)
else
grunt.log.ok "Created " + path.join(buildDir, "#{appFileName}-#{version}-#{arch}.tar.gz")
done()

View File

@@ -74,7 +74,7 @@ getAssets = ->
]
when 'win32'
assets = [{assetName: 'atom-windows.zip', sourcePath: appName}]
for squirrelAsset in ['AtomSetup.exe', 'RELEASES', "atom-#{version}-full.nupkg", "atom-#{version}-delta.nupkg"]
for squirrelAsset in ['AtomSetup.exe', 'AtomSetup.msi', 'RELEASES', "atom-#{version}-full.nupkg", "atom-#{version}-delta.nupkg"]
cp path.join(buildDir, 'installer', squirrelAsset), path.join(buildDir, squirrelAsset)
assets.push({assetName: squirrelAsset, sourcePath: assetName})
assets
@@ -85,13 +85,13 @@ getAssets = ->
arch = 'amd64'
# Check for a Debian build
sourcePath = "#{buildDir}/#{appFileName}-#{version}-#{arch}.deb"
sourcePath = path.join(buildDir, "#{appFileName}-#{version}-#{arch}.deb")
assetName = "atom-#{arch}.deb"
# Check for a Fedora build
unless fs.isFileSync(sourcePath)
rpmName = fs.readdirSync("#{buildDir}/rpm")[0]
sourcePath = "#{buildDir}/rpm/#{rpmName}"
sourcePath = path.join(buildDir, "rpm", rpmName)
if process.arch is 'ia32'
arch = 'i386'
else
@@ -99,10 +99,17 @@ getAssets = ->
assetName = "atom.#{arch}.rpm"
cp sourcePath, path.join(buildDir, assetName)
assets = [{assetName, sourcePath}]
[
{assetName, sourcePath}
]
# Check for an archive build on a debian build machine.
# We could provide a Fedora version if some libraries are not compatible
sourcePath = path.join(buildDir, "#{appFileName}-#{version}-#{arch}.tar.gz")
if fs.isFileSync(sourcePath)
assetName = "atom-#{arch}.tar.gz"
cp sourcePath, path.join(buildDir, assetName)
assets.push({assetName, sourcePath})
assets
logError = (message, error, details) ->
grunt.log.error(message)

View File

@@ -17,20 +17,6 @@ module.exports = (grunt) ->
packageSpecQueue = null
logDeprecations = (label, {stderr}={}) ->
return unless process.env.JANKY_SHA1 or process.env.CI
stderr ?= ''
deprecatedStart = stderr.indexOf('Calls to deprecated functions')
return if deprecatedStart is -1
grunt.log.error(label)
stderr = stderr.substring(deprecatedStart)
stderr = stderr.replace(/^\s*\[[^\]]+\]\s+/gm, '')
stderr = stderr.replace(/source: .*$/gm, '')
stderr = stderr.replace(/^"/gm, '')
stderr = stderr.replace(/",\s*$/gm, '')
grunt.log.error(stderr)
getAppPath = ->
contentsDir = grunt.config.get('atom.contentsDir')
switch process.platform
@@ -57,14 +43,14 @@ module.exports = (grunt) ->
args: ['--test', "--resource-path=#{resourcePath}", path.join(packagePath, 'spec')]
opts:
cwd: packagePath
env: _.extend({}, process.env, ATOM_PATH: rootDir)
env: _.extend({}, process.env, ELECTRON_ENABLE_LOGGING: true, ATOM_PATH: rootDir)
else if process.platform is 'win32'
options =
cmd: process.env.comspec
args: ['/c', appPath, '--test', "--resource-path=#{resourcePath}", "--log-file=ci.log", path.join(packagePath, 'spec')]
opts:
cwd: packagePath
env: _.extend({}, process.env, ATOM_PATH: rootDir)
env: _.extend({}, process.env, ELECTRON_ENABLE_LOGGING: true, ATOM_PATH: rootDir)
grunt.log.ok "Launching #{path.basename(packagePath)} specs."
spawn options, (error, results, code) ->
@@ -74,7 +60,6 @@ module.exports = (grunt) ->
fs.unlinkSync(path.join(packagePath, 'ci.log'))
failedPackages.push path.basename(packagePath) if error
logDeprecations("#{path.basename(packagePath)} Specs", results)
callback()
modulesDirectory = path.resolve('node_modules')
@@ -87,7 +72,7 @@ module.exports = (grunt) ->
packageSpecQueue.concurrency = Math.max(1, concurrency - 1)
packageSpecQueue.drain = -> callback(null, failedPackages)
runCoreSpecs = (callback, logOutput = false) ->
runCoreSpecs = (callback) ->
appPath = getAppPath()
resourcePath = process.cwd()
coreSpecsPath = path.resolve('spec')
@@ -97,21 +82,16 @@ module.exports = (grunt) ->
cmd: appPath
args: ['--test', "--resource-path=#{resourcePath}", coreSpecsPath, "--user-data-dir=#{temp.mkdirSync('atom-user-data-dir')}"]
opts:
env: _.extend({}, process.env,
ATOM_INTEGRATION_TESTS_ENABLED: true
)
env: _.extend({}, process.env, {ELECTRON_ENABLE_LOGGING: true, ATOM_INTEGRATION_TESTS_ENABLED: true})
stdio: 'inherit'
else if process.platform is 'win32'
options =
cmd: process.env.comspec
args: ['/c', appPath, '--test', "--resource-path=#{resourcePath}", '--log-file=ci.log', coreSpecsPath]
opts:
env: _.extend({}, process.env,
ATOM_INTEGRATION_TESTS_ENABLED: true
)
if logOutput
options.opts.stdio = 'inherit'
env: _.extend({}, process.env, {ELECTRON_ENABLE_LOGGING: true, ATOM_INTEGRATION_TESTS_ENABLED: true})
stdio: 'inherit'
grunt.log.ok "Launching core specs."
spawn options, (error, results, code) ->
@@ -121,7 +101,6 @@ module.exports = (grunt) ->
else
# TODO: Restore concurrency on Windows
packageSpecQueue?.concurrency = concurrency
logDeprecations('Core Specs', results)
callback(null, error)
@@ -134,17 +113,11 @@ module.exports = (grunt) ->
else
async.parallel
# If we're just running the core specs then we won't have any output to
# indicate the tests actually *are* running. This upsets Travis:
# https://github.com/atom/atom/issues/10837. So pass the test output
# through.
runCoreSpecsWithLogging = (callback) -> runCoreSpecs(callback, true)
specs =
if process.env.ATOM_SPECS_TASK is 'packages'
[runPackageSpecs]
else if process.env.ATOM_SPECS_TASK is 'core'
[runCoreSpecsWithLogging]
[runCoreSpecs]
else
[runCoreSpecs, runPackageSpecs]

View File

@@ -6,8 +6,8 @@ Ubuntu LTS 12.04 64-bit is the recommended platform.
* OS with 64-bit or 32-bit architecture
* C++ toolchain
* [Git](http://git-scm.com/)
* [Node.js](http://nodejs.org/download/) (0.10.x or above)
* [Git](https://git-scm.com/)
* [Node.js](https://nodejs.org/en/download/) (0.10.x or above)
* [npm](https://www.npmjs.com/) v1.4.x or above (automatically bundled with Node.js)
* `npm -v` to check the version.
* `npm config set python /usr/bin/python2 -g` to ensure that gyp uses python2.
@@ -74,18 +74,24 @@ If you have problems with permissions don't forget to prefix with `sudo`
To use the newly installed Atom, quit and restart all running Atom instances.
5. *Optionally*, you may generate distributable packages of Atom at `out`. Currently, `.deb` and `.rpm` package types are supported. To create a `.deb` package run:
5. *Optionally*, you may generate distributable packages of Atom at `out`. Currently, `.deb` and `.rpm` package types are supported, as well as a `.tar.gz` archive. To create a `.deb` package run:
```sh
script/grunt mkdeb
```
To create an `.rpm` package run
To create a `.rpm` package run
```sh
script/grunt mkrpm
```
To create a `.tar.gz` archive run
```sh
script/grunt mktar
```
## Advanced Options
### Custom build directory

View File

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

View File

@@ -3,7 +3,7 @@
## Requirements
### General
* [Node.js](http://nodejs.org/en/download/) v4.x
* [Node.js](https://nodejs.org/en/download/) v4.x
* [Python](https://www.python.org/downloads/) v2.7.x
* The python.exe must be available at `%SystemDrive%\Python27\python.exe`.
If it is installed elsewhere, you can create a symbolic link to the
@@ -14,29 +14,27 @@
You can use either:
* [Visual Studio 2013 Update 5](http://www.visualstudio.com/en-us/downloads/download-visual-studio-vs) (Express or better) on Windows 7, 8 or 10
* [Visual Studio 2015](http://www.visualstudio.com/en-us/downloads/download-visual-studio-vs) (Community or better) with Windows 8 or 10
* [Visual Studio 2013 Update 5](https://www.visualstudio.com/en-us/downloads/download-visual-studio-vs) (Express or better) on Windows 7, 8 or 10
* [Visual Studio 2015](https://www.visualstudio.com/en-us/downloads/download-visual-studio-vs) (Community or better) with Windows 8 or 10
Whichever version you use, ensure that:
* The default installation folder is chosen so the build tools can find it
* Visual C++ support is installed
* You set the `GYP_MSVS_VERSION` environment variable to the Visual Studio version (`2013` or `2015`), e.g. , e.g. ``[Environment]::SetEnvironmentVariable("GYP_MSVS_VERSION", "2015", "User")`` in PowerShell or set it in Windows advanced system settings control panel.
* The git command is in your path
* A `git` command is in your path
* If you have both VS2013 and VS2015 installed set the `GYP_MSVS_VERSION` environment variable to the Visual Studio version (`2013` or `2015`) you wish to use, e.g. ``[Environment]::SetEnvironmentVariable("GYP_MSVS_VERSION", "2015", "User")`` in PowerShell or set it in Windows advanced system settings control panel.
## Instructions
You can run these commands using Command Prompt, PowerShell or Git Shell via [GitHub Desktop](https://desktop.github.com/). These instructions will assume the use of Bash from Git Shell - if you are using Command Prompt use a backslash instead: i.e. `script\build`.
**VS2015 + Git Shell users** should note that the default path supplied with Git Shell includes reference to an older version of msbuild that will fail. It is recommended you use a PowerShell window that has git in the path at this time.
```bash
cd C:\
git clone https://github.com/atom/atom/
cd atom
script/build
```
This will create the Atom application in the `Program Files` folder.
This will create the Atom application in the `out\Atom` folder as well as copy it to a subfolder of your user profile (e.g. `c:\Users\Bob`) called `AppData\Local\atom\app-dev`.
### `script/build` Options
* `--install-dir` - Creates the final built application in this directory. Example (trailing slash is optional):
@@ -47,6 +45,7 @@ This will create the Atom application in the `Program Files` folder.
```bash
./script/build --build-dir Z:\Some\Temporary\Directory\
```
* `--no-install` - Skips the installation task after building.
* `--verbose` - Verbose mode. A lot more information output.
## Do I have to use GitHub Desktop?
@@ -63,37 +62,34 @@ If none of this works, do install Github Desktop and use its Git Shell as it mak
### Common Errors
* `node is not recognized`
* If you just installed Node.js, you'll need to restart your PowerShell/Command Prompt/Git Shell before the node
command is available on your Path.
* `script/build` outputs only the Node.js and Python versions before returning
* `msbuild.exe failed with exit code: 1`
* Ensure you have Visual C++ support installed. Go into Add/Remove Programs, select Visual Studio and press Modify and then check the Visual C++ box.
* `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. e.g. use `c:\atom` and not `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).
* `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/)
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
* `script/build` stops at installing runas with `Failed at the runas@x.y.z install script.`
* `'node_modules\.bin\npm' is not recognized as an internal or external command, operable program or batch file.`
* This occurs if the previous build left things in a bad state. Run `script\clean` and then `script\build` again.
* `script/build` stops at installing runas with `Failed at the runas@x.y.z install script.`
* See the next item.
* `error MSB8020: The build tools for Visual Studio 201? (Platform Toolset = 'v1?0') cannot be found.`
* If you're building Atom with Visual Studio 2013 or above make sure the `GYP_MSVS_VERSION` environment variable is set, and then re-run `script/build` after a clean:
```bash
$env:GYP_MSVS_VERSION='2013' # '2015' if using Visual Studio 2015, and so on
script/clean
script/build
```
* If you are using Visual Studio 2013 or above and the build fails with some other error message this environment variable might still be required and ensure you have Visual C++ language support installed.
* If you're building Atom with Visual Studio 2013 try setting the `GYP_MSVS_VERSION` environment variable to 2013 and then `script/clean` followed by `script/build` (re-open your command prompt or Powershell window if you set it using the GUI)
* 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.

18
docs/native-profiling.md Normal file
View File

@@ -0,0 +1,18 @@
# Profiling the Atom Render Process on OS X with Instruments
![Instruments](https://cloud.githubusercontent.com/assets/1789/14193295/d503db7a-f760-11e5-88bf-fe417c0cd913.png)
* Determine the version of Electron for your version of Atom.
* Open the dev tools with `alt-cmd-i`
* Evaluate `process.versions.electron` in the console.
* Based on this version, download the appropriate Electron symbols from the [releases](https://github.com/atom/electron/releases) page.
* The file name should look like `electron-v0.X.Y-darwin-x64-dsym.zip`.
* Decompress these symbols in your `~/Downloads` directory.
* Now create a time profile in Instruments.
* Open `Instruments.app`.
* Select `Time Profiler`
* In Atom, determine the pid to attach to by evaluating `process.pid` in the dev tools console.
* Attach to this pid via the menu at the upper left corner of the Instruments profiler.
* Click record, do your thing.
* Click stop.
* The symbols should have been automatically located by Instruments (via Spotlight or something?), giving you a readable profile.

View File

@@ -18,15 +18,15 @@
# 'ctrl-p': 'core:move-down'
#
# You can find more information about keymaps in these guides:
# * https://atom.io/docs/latest/using-atom-basic-customization#customizing-key-bindings
# * https://atom.io/docs/latest/behind-atom-keymaps-in-depth
# * http://flight-manual.atom.io/using-atom/sections/basic-customization/#_customizing_keybindings
# * http://flight-manual.atom.io/behind-atom/sections/keymaps-in-depth/
#
# If you're having trouble with your keybindings not working, try the
# Keybinding Resolver: `Cmd+.` on OS X and `Ctrl+.` on other platforms. See the
# Debugging Guide for more information:
# * https://atom.io/docs/latest/hacking-atom-debugging#check-the-keybindings
# * http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-the-keybindings
#
# This file uses CoffeeScript Object Notation (CSON).
# If you are unfamiliar with CSON, you can read more about it in the
# Atom Flight Manual:
# https://atom.io/docs/latest/using-atom-basic-customization#cson
# http://flight-manual.atom.io/using-atom/sections/basic-customization/#_cson

View File

@@ -18,4 +18,4 @@
# This file uses CoffeeScript Object Notation (CSON).
# If you are unfamiliar with CSON, you can read more about it in the
# Atom Flight Manual:
# https://atom.io/docs/latest/using-atom-basic-customization#cson
# http://flight-manual.atom.io/using-atom/sections/basic-customization/#_cson

View File

@@ -130,6 +130,8 @@
# Atom Specific
'ctrl-W': 'editor:select-word'
'cmd-ctrl-left': 'editor:move-selection-left'
'cmd-ctrl-right': 'editor:move-selection-right'
# Sublime Parity
'cmd-a': 'core:select-all'

View File

@@ -14,6 +14,8 @@
'ctrl-shift-pageup': 'pane:move-item-left'
'ctrl-shift-pagedown': 'pane:move-item-right'
'f11': 'window:toggle-full-screen'
'alt-shift-left': 'editor:move-selection-left'
'alt-shift-right': 'editor:move-selection-right'
# Sublime Parity
'ctrl-,': 'application:show-settings'
@@ -25,6 +27,7 @@
'ctrl-n': 'application:new-file'
'ctrl-s': 'core:save'
'ctrl-S': 'core:save-as'
'ctrl-f4': 'core:close'
'ctrl-w': 'core:close'
'ctrl-z': 'core:undo'
'ctrl-y': 'core:redo'

View File

@@ -20,6 +20,8 @@
'ctrl-shift-left': 'pane:move-item-left'
'ctrl-shift-right': 'pane:move-item-right'
'f11': 'window:toggle-full-screen'
'alt-shift-left': 'editor:move-selection-left'
'alt-shift-right': 'editor:move-selection-right'
# Sublime Parity
'ctrl-,': 'application:show-settings'

View File

@@ -75,6 +75,13 @@
{ label: 'Join Lines', command: 'editor:join-lines' }
]
}
{
label: 'Columns',
submenu: [
{ label: 'Move Selection Left', command: 'editor:move-selection-left' }
{ label: 'Move Selection Right', command: 'editor:move-selection-right' }
]
}
{
label: 'Text',
submenu: [
@@ -221,10 +228,10 @@
{label: 'Delete', command: 'core:delete'}
{label: 'Select All', command: 'core:select-all'}
{type: 'separator'}
{label: 'Split Up', command: 'pane:split-up'}
{label: 'Split Down', command: 'pane:split-down'}
{label: 'Split Left', command: 'pane:split-left'}
{label: 'Split Right', command: 'pane:split-right'}
{label: 'Split Up', command: 'pane:split-up-and-copy-active-item'}
{label: 'Split Down', command: 'pane:split-down-and-copy-active-item'}
{label: 'Split Left', command: 'pane:split-left-and-copy-active-item'}
{label: 'Split Right', command: 'pane:split-right-and-copy-active-item'}
{label: 'Close Pane', command: 'pane:close'}
{type: 'separator'}
]

View File

@@ -48,6 +48,13 @@
{ label: '&Join Lines', command: 'editor:join-lines' }
]
}
{
label: 'Columns',
submenu: [
{ label: 'Move Selection &Left', command: 'editor:move-selection-left' }
{ label: 'Move Selection &Right', command: 'editor:move-selection-right' }
]
}
{
label: 'Text',
submenu: [
@@ -197,10 +204,10 @@
{label: 'Delete', command: 'core:delete'}
{label: 'Select All', command: 'core:select-all'}
{type: 'separator'}
{label: 'Split Up', command: 'pane:split-up'}
{label: 'Split Down', command: 'pane:split-down'}
{label: 'Split Left', command: 'pane:split-left'}
{label: 'Split Right', command: 'pane:split-right'}
{label: 'Split Up', command: 'pane:split-up-and-copy-active-item'}
{label: 'Split Down', command: 'pane:split-down-and-copy-active-item'}
{label: 'Split Left', command: 'pane:split-left-and-copy-active-item'}
{label: 'Split Right', command: 'pane:split-right-and-copy-active-item'}
{label: 'Close Pane', command: 'pane:close'}
{type: 'separator'}
]

View File

@@ -56,6 +56,13 @@
{ label: '&Join Lines', command: 'editor:join-lines' }
]
}
{
label: 'Columns',
submenu: [
{ label: 'Move Selection &Left', command: 'editor:move-selection-left' }
{ label: 'Move Selection &Right', command: 'editor:move-selection-right' }
]
}
{
label: 'Text',
submenu: [
@@ -200,10 +207,10 @@
{label: 'Delete', command: 'core:delete'}
{label: 'Select All', command: 'core:select-all'}
{type: 'separator'}
{label: 'Split Up', command: 'pane:split-up'}
{label: 'Split Down', command: 'pane:split-down'}
{label: 'Split Left', command: 'pane:split-left'}
{label: 'Split Right', command: 'pane:split-right'}
{label: 'Split Up', command: 'pane:split-up-and-copy-active-item'}
{label: 'Split Down', command: 'pane:split-down-and-copy-active-item'}
{label: 'Split Left', command: 'pane:split-left-and-copy-active-item'}
{label: 'Split Right', command: 'pane:split-right-and-copy-active-item'}
{label: 'Close Pane', command: 'pane:close'}
{type: 'separator'}
]

View File

@@ -1,7 +1,7 @@
{
"name": "atom",
"productName": "Atom",
"version": "1.8.0-dev",
"version": "1.9.0-dev",
"description": "A hackable text editor for the 21st Century.",
"main": "./src/browser/main.js",
"repository": {
@@ -12,16 +12,17 @@
"url": "https://github.com/atom/atom/issues"
},
"license": "MIT",
"electronVersion": "0.36.8",
"electronVersion": "0.36.12",
"dependencies": {
"async": "0.2.6",
"atom-keymap": "^6.3.1",
"atom-keymap": "6.3.2",
"babel-core": "^5.8.21",
"bootstrap": "^3.3.4",
"cached-run-in-this-context": "0.4.1",
"clear-cut": "^2.0.1",
"coffee-script": "1.8.0",
"color": "^0.7.3",
"devtron": "1.1.0",
"event-kit": "^1.5.0",
"find-parent-dir": "^0.3.0",
"first-mate": "^5.1.1",
@@ -36,12 +37,12 @@
"key-path-helpers": "^0.4.0",
"less-cache": "0.23",
"line-top-index": "0.2.0",
"marked": "^0.3.4",
"nodegit": "0.11.9",
"marked": "^0.3.5",
"normalize-package-data": "^2.0.0",
"nslog": "^3",
"ohnogit": "0.0.11",
"oniguruma": "^5",
"pathwatcher": "~6.2",
"pathwatcher": "~6.5",
"property-accessors": "^1.1.3",
"random-words": "0.0.1",
"resolve": "^1.1.6",
@@ -54,7 +55,7 @@
"service-hub": "^0.7.0",
"source-map-support": "^0.3.2",
"temp": "0.8.1",
"text-buffer": "8.4.2",
"text-buffer": "9.1.0",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.6",
"yargs": "^3.23.0"
@@ -66,89 +67,89 @@
"atom-light-ui": "0.43.0",
"base16-tomorrow-dark-theme": "1.1.0",
"base16-tomorrow-light-theme": "1.1.1",
"one-dark-ui": "1.3.0",
"one-light-ui": "1.3.0",
"one-dark-ui": "1.3.1",
"one-light-ui": "1.3.1",
"one-dark-syntax": "1.2.0",
"one-light-syntax": "1.2.0",
"solarized-dark-syntax": "1.0.0",
"solarized-light-syntax": "1.0.0",
"about": "1.4.2",
"solarized-dark-syntax": "1.0.2",
"solarized-light-syntax": "1.0.2",
"about": "1.5.2",
"archive-view": "0.61.1",
"autocomplete-atom-api": "0.10.0",
"autocomplete-css": "0.11.0",
"autocomplete-css": "0.11.1",
"autocomplete-html": "0.7.2",
"autocomplete-plus": "2.29.1",
"autocomplete-snippets": "1.10.0",
"autocomplete-plus": "2.31.0",
"autocomplete-snippets": "1.11.0",
"autoflow": "0.27.0",
"autosave": "0.23.1",
"background-tips": "0.26.0",
"bookmarks": "0.38.2",
"bracket-matcher": "0.81.0",
"bookmarks": "0.41.0",
"bracket-matcher": "0.82.1",
"command-palette": "0.38.0",
"deprecation-cop": "0.54.1",
"dev-live-reload": "0.47.0",
"encoding-selector": "0.21.0",
"exception-reporting": "0.37.0",
"find-and-replace": "0.197.4",
"fuzzy-finder": "1.0.3",
"encoding-selector": "0.22.0",
"exception-reporting": "0.38.1",
"fuzzy-finder": "1.2.0",
"git-diff": "1.0.1",
"find-and-replace": "0.198.0",
"go-to-line": "0.30.0",
"grammar-selector": "0.48.1",
"image-view": "0.57.0",
"incompatible-packages": "0.26.1",
"keybinding-resolver": "0.35.0",
"line-ending-selector": "0.4.1",
"line-ending-selector": "0.5.0",
"link": "0.31.1",
"markdown-preview": "0.158.0",
"metrics": "0.53.1",
"notifications": "0.63.1",
"open-on-github": "1.0.1",
"notifications": "0.64.1",
"open-on-github": "1.1.0",
"package-generator": "1.0.0",
"settings-view": "0.235.1",
"settings-view": "0.237.0",
"snippets": "1.0.2",
"spell-check": "0.67.0",
"status-bar": "1.2.0",
"spell-check": "0.67.1",
"status-bar": "1.2.6",
"styleguide": "0.45.2",
"symbols-view": "0.112.0",
"tabs": "0.92.0",
"symbols-view": "0.113.0",
"tabs": "0.95.0",
"timecop": "0.33.1",
"tree-view": "0.203.2",
"tree-view": "0.207.0",
"update-package-dependencies": "0.10.0",
"welcome": "0.34.0",
"whitespace": "0.32.2",
"wrap-guide": "0.38.1",
"language-c": "0.51.1",
"language-c": "0.52.0",
"language-clojure": "0.20.0",
"language-coffee-script": "0.46.1",
"language-csharp": "0.12.0",
"language-css": "0.36.0",
"language-gfm": "0.85.0",
"language-git": "0.12.1",
"language-coffee-script": "0.47.0",
"language-csharp": "0.12.1",
"language-css": "0.36.1",
"language-gfm": "0.86.0",
"language-git": "0.13.0",
"language-go": "0.42.0",
"language-html": "0.44.1",
"language-hyperlink": "0.16.0",
"language-java": "0.17.0",
"language-java": "0.19.0",
"language-javascript": "0.110.0",
"language-json": "0.17.6",
"language-less": "0.29.0",
"language-make": "0.21.0",
"language-json": "0.18.0",
"language-less": "0.29.3",
"language-make": "0.22.0",
"language-mustache": "0.13.0",
"language-objective-c": "0.15.1",
"language-perl": "0.32.0",
"language-perl": "0.35.0",
"language-php": "0.37.0",
"language-property-list": "0.8.0",
"language-python": "0.43.0",
"language-ruby": "0.68.3",
"language-python": "0.44.0",
"language-ruby": "0.68.5",
"language-ruby-on-rails": "0.25.0",
"language-sass": "0.46.0",
"language-shellscript": "0.21.1",
"language-sass": "0.52.0",
"language-shellscript": "0.22.2",
"language-source": "0.9.0",
"language-sql": "0.20.0",
"language-sql": "0.21.0",
"language-text": "0.7.1",
"language-todo": "0.27.0",
"language-toml": "0.18.0",
"language-xml": "0.34.4",
"language-yaml": "0.25.2"
"language-xml": "0.34.6",
"language-yaml": "0.26.0"
},
"private": true,
"scripts": {

View File

@@ -2,6 +2,7 @@
SET EXPECT_OUTPUT=
SET WAIT=
SET PSARGS=%*
FOR %%a IN (%*) DO (
IF /I "%%a"=="-f" SET EXPECT_OUTPUT=YES
@@ -25,7 +26,8 @@ FOR %%a IN (%*) DO (
IF "%EXPECT_OUTPUT%"=="YES" (
SET ELECTRON_ENABLE_LOGGING=YES
IF "%WAIT%"=="YES" (
powershell -noexit "%~dp0\..\..\atom.exe" --pid=$pid %* ; wait-event
powershell -noexit "Start-Process -FilePath \"%~dp0\..\..\atom.exe\" -ArgumentList \"--pid=$pid $env:PSARGS\" ; wait-event"
exit 0
) ELSE (
"%~dp0\..\..\atom.exe" %*
)

View File

@@ -1,2 +1,5 @@
#!/bin/sh
$(dirname "$0")/atom.cmd "$@"
pushd "$(dirname "$0")" > /dev/null
ATOMCMD=""$(pwd -W)"/atom.cmd"
popd > /dev/null
cmd.exe //c "$ATOMCMD" "$@"

View File

@@ -2,9 +2,24 @@
var cp = require('./utils/child-process-wrapper.js');
var runGrunt = require('./utils/run-grunt.js');
var path = require('path');
var fs = require('fs');
process.chdir(path.dirname(__dirname));
if (process.platform === 'win32') {
process.env['PATH'] = process.env['PATH']
.split(';')
.filter(function(p) {
if (fs.existsSync(path.resolve(p, 'msbuild.exe'))) {
console.log('Excluding "' + p + '" from PATH to avoid msbuild.exe mismatch')
return false;
} else {
return true;
}
})
.join(';');
}
cp.safeExec('node script/bootstrap', function() {
// build/node_modules/.bin/grunt "$@"
var args = process.argv.slice(2);

View File

@@ -1,11 +1,10 @@
#!/usr/bin/env node
var cp = require('./utils/child-process-wrapper.js');
var childProcess = require('./utils/child-process-wrapper.js');
var fs = require('fs');
var path = require('path');
var os = require('os');
var isWindows = process.platform === 'win32';
var removeCommand = isWindows ? 'rmdir /S /Q ' : 'rm -rf ';
var productName = require('../package.json').productName;
process.chdir(path.dirname(__dirname));
@@ -13,10 +12,10 @@ var home = process.env[isWindows ? 'USERPROFILE' : 'HOME'];
var tmpdir = os.tmpdir();
// Windows: Use START as a way to ignore error if Atom.exe isnt running
var killatom = isWindows ? 'START taskkill /F /IM ' + productName + '.exe' : 'pkill -9 ' + productName + ' || true';
var killAtomCommand = isWindows ? 'START taskkill /F /IM ' + productName + '.exe' : 'pkill -9 ' + productName + ' || true';
//childProcess.safeExec(killAtomCommand);
var commands = [
killatom,
var pathsToRemove = [
[__dirname, '..', 'node_modules'],
[__dirname, '..', 'build', 'node_modules'],
[__dirname, '..', 'apm', 'node_modules'],
@@ -32,37 +31,30 @@ var commands = [
[home, '.atom', 'electron'],
[tmpdir, 'atom-build'],
[tmpdir, 'atom-cached-atom-shells'],
];
var run = function() {
var next = commands.shift();
if (!next)
process.exit(0);
].map(function(pathSegments) {
return path.resolve.apply(null, pathSegments);
});
if (Array.isArray(next)) {
var pathToRemove = path.resolve.apply(path.resolve, next);
if (fs.existsSync(pathToRemove)) {
if (isWindows) {
removeFolderRecursive(pathToRemove);
} else {
next = removeCommand + pathToRemove;
cp.safeExec(next, run);
}
}
else {
return run();
}
pathsToRemove.forEach(function(pathToRemove) {
if (fs.existsSync(pathToRemove)) {
removePath(pathToRemove);
}
else
cp.safeExec(next, run);
};
run();
});
function removePath(pathToRemove) {
if (isWindows) {
removePathOnWindows(pathToRemove);
} else {
childProcess.safeExec('rm -rf ' + pathToRemove);
}
}
// Windows has a 260-char path limit for rmdir etc. Just recursively delete in Node.
var removeFolderRecursive = function(folderPath) {
function removePathOnWindows(folderPath) {
fs.readdirSync(folderPath).forEach(function(entry, index) {
var entryPath = path.join(folderPath, entry);
if (fs.lstatSync(entryPath).isDirectory()) {
removeFolderRecursive(entryPath);
removePathOnWindows(entryPath);
} else {
fs.unlinkSync(entryPath);
}

39
script/mktar Executable file
View File

@@ -0,0 +1,39 @@
#!/bin/bash
# mktar name version arch icon-path build-root-path
set -e
SCRIPT=`readlink -f "$0"`
ROOT=`readlink -f $(dirname $SCRIPT)/..`
cd $ROOT
NAME="$1"
VERSION="$2"
ARCH="$3"
ICON_FILE="$4"
BUILD_ROOT_PATH="$5"
FILE_MODE=755
TAR_PATH=$BUILD_ROOT_PATH
ATOM_PATH="$BUILD_ROOT_PATH/Atom"
TARGET_ROOT="`mktemp -d`"
chmod $FILE_MODE "$TARGET_ROOT"
NAME_IN_TAR="$NAME-$VERSION-$ARCH"
TARGET="$TARGET_ROOT/$NAME_IN_TAR"
# Copy executable and resources
cp -a "$ATOM_PATH" "$TARGET"
# Copy icon file
cp "$ICON_FILE" "$TARGET/$NAME.png"
# Remove executable bit from .node files
find "$TARGET" -type f -name "*.node" -exec chmod a-x {} \;
# Create the archive
pushd "$TARGET_ROOT"
tar caf "$TAR_PATH/$NAME_IN_TAR.tar.gz" "$NAME_IN_TAR"
popd
rm -rf "$TARGET_ROOT"

View File

@@ -17,7 +17,7 @@ exports.safeExec = function(command, options, callback) {
var child = childProcess.exec(command, options, function(error, stdout, stderr) {
if (error)
process.exit(error.code || 1);
else
else if (callback)
callback(null);
});
child.stderr.pipe(process.stderr);

View File

@@ -4,6 +4,7 @@ temp = require 'temp'
Package = require '../src/package'
ThemeManager = require '../src/theme-manager'
AtomEnvironment = require '../src/atom-environment'
StorageFolder = require '../src/storage-folder'
describe "AtomEnvironment", ->
describe 'window sizing methods', ->
@@ -27,8 +28,10 @@ describe "AtomEnvironment", ->
atom.setSize(originalSize.width, originalSize.height)
it 'sets the size of the window, and can retrieve the size just set', ->
atom.setSize(100, 400)
expect(atom.getSize()).toEqual width: 100, height: 400
newWidth = originalSize.width + 12
newHeight = originalSize.height + 23
atom.setSize(newWidth, newHeight)
expect(atom.getSize()).toEqual width: newWidth, height: newHeight
describe ".isReleasedVersion()", ->
it "returns false if the version is a SHA and true otherwise", ->
@@ -172,19 +175,48 @@ describe "AtomEnvironment", ->
waitsForPromise ->
atom.saveState().then ->
atom.loadState().then (state) ->
expect(state).toBeNull()
expect(state).toBeFalsy()
waitsForPromise ->
loadSettings.initialPaths = [dir2, dir1]
atom.loadState().then (state) ->
expect(state).toEqual({stuff: 'cool'})
it "saves state on keydown, mousedown, and when the editor window unloads", ->
it "loads state from the storage folder when it can't be found in atom.stateStore", ->
jasmine.useRealClock()
storageFolderState = {foo: 1, bar: 2}
serializedState = {someState: 42}
loadSettings = _.extend(atom.getLoadSettings(), {initialPaths: [temp.mkdirSync("project-directory")]})
spyOn(atom, 'getLoadSettings').andReturn(loadSettings)
spyOn(atom, 'serialize').andReturn(serializedState)
spyOn(atom, 'getStorageFolder').andReturn(new StorageFolder(temp.mkdirSync("config-directory")))
atom.project.setPaths(atom.getLoadSettings().initialPaths)
waitsForPromise ->
atom.stateStore.connect()
runs ->
atom.getStorageFolder().storeSync(atom.getStateKey(loadSettings.initialPaths), storageFolderState)
waitsForPromise ->
atom.loadState().then (state) -> expect(state).toEqual(storageFolderState)
waitsForPromise ->
atom.saveState()
waitsForPromise ->
atom.loadState().then (state) -> expect(state).toEqual(serializedState)
it "saves state when the CPU is idle after a keydown or mousedown event", ->
spyOn(atom, 'saveState')
idleCallbacks = []
spyOn(window, 'requestIdleCallback').andCallFake (callback) -> idleCallbacks.push(callback)
keydown = new KeyboardEvent('keydown')
atom.document.dispatchEvent(keydown)
advanceClock atom.saveStateDebounceInterval
idleCallbacks.shift()()
expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false})
expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true})
@@ -192,17 +224,33 @@ describe "AtomEnvironment", ->
mousedown = new MouseEvent('mousedown')
atom.document.dispatchEvent(mousedown)
advanceClock atom.saveStateDebounceInterval
idleCallbacks.shift()()
expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false})
expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true})
atom.saveState.reset()
it "saves state immediately when unloading the editor window, ignoring pending and successive mousedown/keydown events", ->
spyOn(atom, 'saveState')
idleCallbacks = []
spyOn(window, 'requestIdleCallback').andCallFake (callback) -> idleCallbacks.push(callback)
mousedown = new MouseEvent('mousedown')
atom.document.dispatchEvent(mousedown)
atom.unloadEditorWindow()
mousedown = new MouseEvent('mousedown')
atom.document.dispatchEvent(mousedown)
advanceClock atom.saveStateDebounceInterval
expect(atom.saveState).toHaveBeenCalledWith({isUnloading: true})
expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: false})
atom.saveState.reset()
advanceClock atom.saveStateDebounceInterval
idleCallbacks.shift()()
expect(atom.saveState).not.toHaveBeenCalled()
atom.saveState.reset()
mousedown = new MouseEvent('mousedown')
atom.document.dispatchEvent(mousedown)
advanceClock atom.saveStateDebounceInterval
idleCallbacks.shift()()
expect(atom.saveState).not.toHaveBeenCalled()
it "serializes the project state with all the options supplied in saveState", ->
spyOn(atom.project, 'serialize').andReturn({foo: 42})
@@ -338,7 +386,7 @@ describe "AtomEnvironment", ->
updateAvailableHandler = jasmine.createSpy("update-available-handler")
subscription = atom.onUpdateAvailable updateAvailableHandler
autoUpdater = require('remote').require('auto-updater')
autoUpdater = require('electron').remote.require('auto-updater')
autoUpdater.emit 'update-downloaded', null, "notes", "version"
waitsFor ->

View File

@@ -64,6 +64,16 @@ describe('AutoUpdateManager (renderer)', () => {
})
})
describe('::onUpdateError', () => {
it('subscribes to "update-error" event', () => {
const spy = jasmine.createSpy('spy')
autoUpdateManager.onUpdateError(spy)
electronAutoUpdater.emit('error', {}, 'an error message')
waitsFor(() => spy.callCount === 1)
runs(() => expect(autoUpdateManager.getErrorMessage()).toBe('an error message'))
})
})
describe('::platformSupportsUpdates', () => {
let state, releaseChannel
it('returns true on OS X and Windows when in stable', () => {

View File

@@ -1,5 +1,6 @@
ChildProcess = require 'child_process'
path = require 'path'
fs = require 'fs-plus'
BufferedProcess = require '../src/buffered-process'
describe "BufferedProcess", ->
@@ -15,20 +16,20 @@ describe "BufferedProcess", ->
describe "when there is an error handler specified", ->
describe "when an error event is emitted by the process", ->
it "calls the error handler and does not throw an exception", ->
process = new BufferedProcess
command: 'bad-command-nope'
bufferedProcess = new BufferedProcess
command: 'bad-command-nope1'
args: ['nothing']
options: {}
options: {shell: false}
errorSpy = jasmine.createSpy().andCallFake (error) -> error.handle()
process.onWillThrowError(errorSpy)
bufferedProcess.onWillThrowError(errorSpy)
waitsFor -> errorSpy.callCount > 0
runs ->
expect(window.onerror).not.toHaveBeenCalled()
expect(errorSpy).toHaveBeenCalled()
expect(errorSpy.mostRecentCall.args[0].error.message).toContain 'spawn bad-command-nope ENOENT'
expect(errorSpy.mostRecentCall.args[0].error.message).toContain 'spawn bad-command-nope1 ENOENT'
describe "when an error is thrown spawning the process", ->
it "calls the error handler and does not throw an exception", ->
@@ -37,13 +38,13 @@ describe "BufferedProcess", ->
error.code = 'EAGAIN'
throw error
process = new BufferedProcess
bufferedProcess = new BufferedProcess
command: 'ls'
args: []
options: {}
errorSpy = jasmine.createSpy().andCallFake (error) -> error.handle()
process.onWillThrowError(errorSpy)
bufferedProcess.onWillThrowError(errorSpy)
waitsFor -> errorSpy.callCount > 0
@@ -53,55 +54,24 @@ describe "BufferedProcess", ->
expect(errorSpy.mostRecentCall.args[0].error.message).toContain 'Something is really wrong'
describe "when there is not an error handler specified", ->
it "calls the error handler and does not throw an exception", ->
process = new BufferedProcess
command: 'bad-command-nope'
it "does throw an exception", ->
new BufferedProcess
command: 'bad-command-nope2'
args: ['nothing']
options: {}
options: {shell: false}
waitsFor -> window.onerror.callCount > 0
runs ->
expect(window.onerror).toHaveBeenCalled()
expect(window.onerror.mostRecentCall.args[0]).toContain 'Failed to spawn command `bad-command-nope`'
expect(window.onerror.mostRecentCall.args[0]).toContain 'Failed to spawn command `bad-command-nope2`'
expect(window.onerror.mostRecentCall.args[4].name).toBe 'BufferedProcessError'
describe "on Windows", ->
originalPlatform = null
beforeEach ->
# Prevent any commands from actually running and affecting the host
originalSpawn = ChildProcess.spawn
spyOn(ChildProcess, 'spawn').andCallFake ->
# Just spawn something that won't actually modify the host
if originalPlatform is 'win32'
originalSpawn('dir')
else
originalSpawn('ls')
originalPlatform = process.platform
Object.defineProperty process, 'platform', value: 'win32'
afterEach ->
Object.defineProperty process, 'platform', value: originalPlatform
describe "when the explorer command is spawned on Windows", ->
it "doesn't quote arguments of the form /root,C...", ->
new BufferedProcess({command: 'explorer.exe', args: ['/root,C:\\foo']})
expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe '"explorer.exe /root,C:\\foo"'
it "spawns the command using a cmd.exe wrapper", ->
new BufferedProcess({command: 'dir'})
expect(path.basename(ChildProcess.spawn.argsForCall[0][0])).toBe 'cmd.exe'
expect(ChildProcess.spawn.argsForCall[0][1][0]).toBe '/s'
expect(ChildProcess.spawn.argsForCall[0][1][1]).toBe '/c'
expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe '"dir"'
it "calls the specified stdout, stderr, and exit callbacks", ->
stdout = ''
stderr = ''
exitCallback = jasmine.createSpy('exit callback')
process = new BufferedProcess
new BufferedProcess
command: atom.packages.getApmPath()
args: ['-h']
options: {}
@@ -115,29 +85,51 @@ describe "BufferedProcess", ->
expect(stderr).toContain 'apm - Atom Package Manager'
expect(stdout).toEqual ''
it "calls the specified stdout callback only with whole lines", ->
it "calls the specified stdout callback with whole lines", ->
exitCallback = jasmine.createSpy('exit callback')
baseContent = "There are dozens of us! Dozens! It's as Ann as the nose on Plain's face. Can you believe that the only reason the club is going under is because it's in a terrifying neighborhood? She calls it a Mayonegg. Waiting for the Emmys. BTW did you know won 6 Emmys and was still canceled early by Fox? COME ON. I'll buy you a hundred George Michaels that you can teach to drive! Never once touched my per diem. I'd go to Craft Service, get some raw veggies, bacon, Cup-A-Soup…baby, I got a stew goin'"
content = (baseContent for _ in [1..200]).join('\n')
loremPath = require.resolve("./fixtures/lorem.txt")
content = fs.readFileSync(loremPath).toString()
baseContent = content.split('\n')
stdout = ''
endLength = 10
outputAlwaysEndsWithStew = true
process = new BufferedProcess
command: '/bin/echo'
args: [content]
allLinesEndWithNewline = true
new BufferedProcess
command: if process.platform is 'win32' then 'type' else 'cat'
args: [loremPath]
options: {}
stdout: (lines) ->
endsWithNewline = (lines.charAt lines.length - 1) is '\n'
if not endsWithNewline then allLinesEndWithNewline = false
stdout += lines
end = baseContent.substr(baseContent.length - endLength, endLength)
lineEndsWithStew = lines.substr(lines.length - endLength, endLength) is end
expect(lineEndsWithStew).toBeTrue
outputAlwaysEndsWithStew = outputAlwaysEndsWithStew and lineEndsWithStew
exit: exitCallback
waitsFor -> exitCallback.callCount is 1
runs ->
expect(outputAlwaysEndsWithStew).toBeTrue
expect(stdout).toBe content += '\n'
expect(allLinesEndWithNewline).toBeTrue
expect(stdout).toBe content
describe "on Windows", ->
originalPlatform = null
beforeEach ->
# Prevent any commands from actually running and affecting the host
originalSpawn = ChildProcess.spawn
spyOn(ChildProcess, 'spawn')
originalPlatform = process.platform
Object.defineProperty process, 'platform', value: 'win32'
afterEach ->
Object.defineProperty process, 'platform', value: originalPlatform
describe "when the explorer command is spawned on Windows", ->
it "doesn't quote arguments of the form /root,C...", ->
new BufferedProcess({command: 'explorer.exe', args: ['/root,C:\\foo']})
expect(ChildProcess.spawn.argsForCall[0][1][3]).toBe '"explorer.exe /root,C:\\foo"'
it "spawns the command using a cmd.exe wrapper when options.shell is undefined", ->
new BufferedProcess({command: 'dir'})
expect(path.basename(ChildProcess.spawn.argsForCall[0][0])).toBe 'cmd.exe'
expect(ChildProcess.spawn.argsForCall[0][1][0]).toBe '/s'
expect(ChildProcess.spawn.argsForCall[0][1][1]).toBe '/d'
expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe '/c'
expect(ChildProcess.spawn.argsForCall[0][1][3]).toBe '"dir"'

View File

@@ -74,6 +74,13 @@ describe "CommandRegistry", ->
grandchild.dispatchEvent(new CustomEvent('command', bubbles: true))
expect(calls).toEqual ['.foo.bar', '.bar', '.foo']
it "orders inline listeners by reverse registration order", ->
calls = []
registry.add child, 'command', -> calls.push('child1')
registry.add child, 'command', -> calls.push('child2')
child.dispatchEvent(new CustomEvent('command', bubbles: true))
expect(calls).toEqual ['child2', 'child1']
it "stops bubbling through ancestors when .stopPropagation() is called on the event", ->
calls = []

View File

@@ -0,0 +1,85 @@
DecorationManager = require '../src/decoration-manager'
_ = require 'underscore-plus'
describe "DecorationManager", ->
[decorationManager, buffer, defaultMarkerLayer] = []
beforeEach ->
buffer = atom.project.bufferForPathSync('sample.js')
displayLayer = buffer.addDisplayLayer()
defaultMarkerLayer = displayLayer.addMarkerLayer()
decorationManager = new DecorationManager(displayLayer, defaultMarkerLayer)
waitsForPromise ->
atom.packages.activatePackage('language-javascript')
afterEach ->
decorationManager.destroy()
buffer.release()
describe "decorations", ->
[marker, decoration, decorationProperties] = []
beforeEach ->
marker = defaultMarkerLayer.markBufferRange([[2, 13], [3, 15]])
decorationProperties = {type: 'line-number', class: 'one'}
decoration = decorationManager.decorateMarker(marker, decorationProperties)
it "can add decorations associated with markers and remove them", ->
expect(decoration).toBeDefined()
expect(decoration.getProperties()).toBe decorationProperties
expect(decorationManager.decorationForId(decoration.id)).toBe decoration
expect(decorationManager.decorationsForScreenRowRange(2, 3)[marker.id][0]).toBe decoration
decoration.destroy()
expect(decorationManager.decorationsForScreenRowRange(2, 3)[marker.id]).not.toBeDefined()
expect(decorationManager.decorationForId(decoration.id)).not.toBeDefined()
it "will not fail if the decoration is removed twice", ->
decoration.destroy()
decoration.destroy()
expect(decorationManager.decorationForId(decoration.id)).not.toBeDefined()
it "does not allow destroyed markers to be decorated", ->
marker.destroy()
expect(->
decorationManager.decorateMarker(marker, {type: 'overlay', item: document.createElement('div')})
).toThrow("Cannot decorate a destroyed marker")
expect(decorationManager.getOverlayDecorations()).toEqual []
describe "when a decoration is updated via Decoration::update()", ->
it "emits an 'updated' event containing the new and old params", ->
decoration.onDidChangeProperties updatedSpy = jasmine.createSpy()
decoration.setProperties type: 'line-number', class: 'two'
{oldProperties, newProperties} = updatedSpy.mostRecentCall.args[0]
expect(oldProperties).toEqual decorationProperties
expect(newProperties).toEqual {type: 'line-number', gutterName: 'line-number', class: 'two'}
describe "::getDecorations(properties)", ->
it "returns decorations matching the given optional properties", ->
expect(decorationManager.getDecorations()).toEqual [decoration]
expect(decorationManager.getDecorations(class: 'two').length).toEqual 0
expect(decorationManager.getDecorations(class: 'one').length).toEqual 1
describe "::decorateMarker", ->
describe "when decorating gutters", ->
[marker] = []
beforeEach ->
marker = defaultMarkerLayer.markBufferRange([[1, 0], [1, 0]])
it "creates a decoration that is both of 'line-number' and 'gutter' type when called with the 'line-number' type", ->
decorationProperties = {type: 'line-number', class: 'one'}
decoration = decorationManager.decorateMarker(marker, decorationProperties)
expect(decoration.isType('line-number')).toBe true
expect(decoration.isType('gutter')).toBe true
expect(decoration.getProperties().gutterName).toBe 'line-number'
expect(decoration.getProperties().class).toBe 'one'
it "creates a decoration that is only of 'gutter' type if called with the 'gutter' type and a 'gutterName'", ->
decorationProperties = {type: 'gutter', gutterName: 'test-gutter', class: 'one'}
decoration = decorationManager.decorateMarker(marker, decorationProperties)
expect(decoration.isType('gutter')).toBe true
expect(decoration.isType('line-number')).toBe false
expect(decoration.getProperties().gutterName).toBe 'test-gutter'
expect(decoration.getProperties().class).toBe 'one'

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,10 @@
{Point} = require 'text-buffer'
{isPairedCharacter} = require '../src/text-utils'
module.exports =
class FakeLinesYardstick
constructor: (@model, @lineTopIndex) ->
{@displayLayer} = @model
@characterWidthsByScope = {}
getScopedCharacterWidth: (scopeNames, char) ->
@@ -24,31 +26,38 @@ class FakeLinesYardstick
targetRow = screenPosition.row
targetColumn = screenPosition.column
baseCharacterWidth = @model.getDefaultCharWidth()
top = @lineTopIndex.pixelPositionAfterBlocksForRow(targetRow)
left = 0
column = 0
iterator = @model.tokenizedLineForScreenRow(targetRow).getTokenIterator()
while iterator.next()
characterWidths = @getScopedCharacterWidths(iterator.getScopes())
scopes = []
startIndex = 0
{tagCodes, lineText} = @model.screenLineForScreenRow(targetRow)
for tagCode in tagCodes
if @displayLayer.isOpenTagCode(tagCode)
scopes.push(@displayLayer.tagForCode(tagCode))
else if @displayLayer.isCloseTagCode(tagCode)
scopes.splice(scopes.lastIndexOf(@displayLayer.tagForCode(tagCode)), 1)
else
text = lineText.substr(startIndex, tagCode)
startIndex += tagCode
characterWidths = @getScopedCharacterWidths(scopes)
valueIndex = 0
text = iterator.getText()
while valueIndex < text.length
if iterator.isPairedCharacter()
char = text
charLength = 2
valueIndex += 2
else
char = text[valueIndex]
charLength = 1
valueIndex++
valueIndex = 0
while valueIndex < text.length
if isPairedCharacter(text, valueIndex)
char = text[valueIndex...valueIndex + 2]
charLength = 2
valueIndex += 2
else
char = text[valueIndex]
charLength = 1
valueIndex++
break if column is targetColumn
break if column is targetColumn
left += characterWidths[char] ? baseCharacterWidth unless char is '\0'
column += charLength
left += characterWidths[char] ? @model.getDefaultCharWidth() unless char is '\0'
column += charLength
{top, left}

View File

@@ -5,6 +5,12 @@
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[branch "master"]
remote = origin
merge = refs/heads/master
[remote "origin"]
url = git@github.com:atom/some-repo-i-guess.git
fetch = +refs/heads/*:refs/remotes/origin/*
[submodule "jstips"]
url = https://github.com/loverajoel/jstips
[submodule "You-Dont-Need-jQuery"]

View File

@@ -0,0 +1 @@
d2b0ad9cbc6f6c4372e8956e5cc5af771b2342e5

3
spec/fixtures/lorem.txt vendored Normal file
View File

@@ -0,0 +1,3 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ultricies nulla id nibh aliquam, vitae euismod ipsum scelerisque. Vestibulum vulputate facilisis nisi, eu rhoncus turpis pretium ut. Curabitur facilisis urna in diam efficitur, vel maximus tellus consectetur. Suspendisse pulvinar felis sed metus tristique, a posuere dui suscipit. Ut vehicula, tellus ac blandit consequat, libero dui hendrerit elit, non pretium metus odio sed dolor. Vivamus quis volutpat ipsum. In convallis magna nec nunc tristique malesuada. Sed sed hendrerit lacus. Etiam arcu dui, consequat vel neque vitae, iaculis egestas justo. Donec lacinia odio nulla, condimentum porta erat accumsan at. Nunc vulputate nulla vel nunc fermentum egestas.
Duis ultricies libero elit, nec facilisis mi rhoncus ornare. Aliquam aliquet libero vitae arcu porttitor mattis. Vestibulum ultricies consectetur arcu, non gravida magna eleifend vel. Phasellus varius mattis ultricies. Vestibulum placerat lacus non consectetur fringilla. Duis congue, arcu iaculis vehicula hendrerit, purus odio faucibus ipsum, et fermentum massa tellus euismod nulla. Vivamus pellentesque blandit massa, sit amet hendrerit turpis congue eu. Suspendisse diam dui, vestibulum nec semper varius, maximus eu nunc. Vivamus facilisis pulvinar viverra. Praesent luctus lectus id est porttitor volutpat. Suspendisse est augue, mattis a tincidunt id, condimentum in turpis. Curabitur at erat commodo orci interdum tincidunt. Sed sodales elit odio, a placerat ipsum luctus nec. Sed maximus, justo ut pharetra pellentesque, orci mi faucibus enim, quis viverra arcu dui sed nisl. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Praesent quis velit libero.
Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Phasellus a rutrum tortor. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Fusce bibendum odio et neque vestibulum rutrum. Vestibulum commodo, nibh non sodales lobortis, dui ex consectetur leo, a finibus libero lectus ac diam. Etiam dui nunc, bibendum a tempor vel, vestibulum lacinia neque. Mauris consectetur odio sit amet maximus pretium. Sed rutrum nunc at ante ullamcorper fermentum. Proin at quam a mauris pellentesque viverra. Nunc pretium pulvinar ipsum. Vestibulum eu nibh ut ex gravida tempus. Praesent ut elit ut ligula tristique dapibus ut sit amet leo. Proin non molestie erat.

View File

@@ -1,3 +1,3 @@
#!/usr/bin/ruby
puts "America fuck yeah!"
puts "Atom fixture test"

View File

@@ -3,7 +3,6 @@
import fs from 'fs-plus'
import path from 'path'
import temp from 'temp'
import Git from 'nodegit'
import {it, beforeEach, afterEach} from './async-spec-helpers'
@@ -47,7 +46,7 @@ describe('GitRepositoryAsync', () => {
let threw = false
try {
await repo.repoPromise
await repo.getRepo()
} catch (e) {
threw = true
}
@@ -56,6 +55,14 @@ describe('GitRepositoryAsync', () => {
})
})
describe('openedPath', () => {
it('is the path passed to .open', () => {
const workingDirPath = copyRepository()
repo = GitRepositoryAsync.open(workingDirPath)
expect(repo.openedPath).toBe(workingDirPath)
})
})
describe('.getRepo()', () => {
beforeEach(() => {
const workingDirectory = copySubmoduleRepository()
@@ -64,19 +71,19 @@ describe('GitRepositoryAsync', () => {
})
it('returns the repository when not given a path', async () => {
const nodeGitRepo1 = await repo.repoPromise
const nodeGitRepo1 = await repo.getRepo()
const nodeGitRepo2 = await repo.getRepo()
expect(nodeGitRepo1.workdir()).toBe(nodeGitRepo2.workdir())
})
it('returns the repository when given a non-submodule path', async () => {
const nodeGitRepo1 = await repo.repoPromise
const nodeGitRepo1 = await repo.getRepo()
const nodeGitRepo2 = await repo.getRepo('README')
expect(nodeGitRepo1.workdir()).toBe(nodeGitRepo2.workdir())
})
it('returns the submodule repository when given a submodule path', async () => {
const nodeGitRepo1 = await repo.repoPromise
const nodeGitRepo1 = await repo.getRepo()
const nodeGitRepo2 = await repo.getRepo('jstips')
expect(nodeGitRepo1.workdir()).not.toBe(nodeGitRepo2.workdir())
@@ -103,7 +110,7 @@ describe('GitRepositoryAsync', () => {
it('returns the repository path for a repository path', async () => {
repo = openFixture('master.git')
const repoPath = await repo.getPath()
expect(repoPath).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git'))
expect(repoPath).toEqualPath(path.join(__dirname, 'fixtures', 'git', 'master.git'))
})
})
@@ -230,9 +237,7 @@ describe('GitRepositoryAsync', () => {
})
})
// @joshaber: Disabling for now. There seems to be some race with path
// subscriptions leading to intermittent test failures, e.g.: https://travis-ci.org/atom/atom/jobs/102702554
xdescribe('.checkoutHeadForEditor(editor)', () => {
describe('.checkoutHeadForEditor(editor)', () => {
let filePath
let editor
@@ -305,7 +310,7 @@ describe('GitRepositoryAsync', () => {
await repo.getPathStatus(filePath)
expect(statusHandler.callCount).toBe(1)
const status = Git.Status.STATUS.WT_MODIFIED
const status = GitRepositoryAsync.Git.Status.STATUS.WT_MODIFIED
expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status})
fs.writeFileSync(filePath, 'abc')
@@ -880,4 +885,34 @@ describe('GitRepositoryAsync', () => {
})
})
})
describe('.getOriginURL()', () => {
beforeEach(() => {
const workingDirectory = copyRepository('repo-with-submodules')
repo = GitRepositoryAsync.open(workingDirectory)
})
it('returns the origin URL', async () => {
const url = await repo.getOriginURL()
expect(url).toBe('git@github.com:atom/some-repo-i-guess.git')
})
})
describe('.getUpstreamBranch()', () => {
it('returns null when there is no upstream branch', async () => {
const workingDirectory = copyRepository()
repo = GitRepositoryAsync.open(workingDirectory)
const upstream = await repo.getUpstreamBranch()
expect(upstream).toBe(null)
})
it('returns the upstream branch', async () => {
const workingDirectory = copyRepository('repo-with-submodules')
repo = GitRepositoryAsync.open(workingDirectory)
const upstream = await repo.getUpstreamBranch()
expect(upstream).toBe('refs/remotes/origin/master')
})
})
})

View File

@@ -33,7 +33,7 @@ describe "GitRepository", ->
waitsForPromise ->
repo.async.getPath().then(onSuccess)
runs ->
expect(onSuccess.mostRecentCall.args[0]).toBe(repoPath)
expect(onSuccess.mostRecentCall.args[0]).toEqualPath(repoPath)
describe "new GitRepository(path)", ->
it "throws an exception when no repository is found", ->
@@ -289,6 +289,16 @@ describe "GitRepository", ->
expect(repo.isStatusModified(status)).toBe true
expect(repo.isStatusNew(status)).toBe false
it 'caches statuses that were looked up synchronously', ->
originalContent = 'undefined'
fs.writeFileSync(modifiedPath, 'making this path modified')
repo.getPathStatus('file.txt')
fs.writeFileSync(modifiedPath, originalContent)
waitsForPromise -> repo.refreshStatus()
runs ->
expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeFalsy()
describe "buffer events", ->
[editor] = []

View File

@@ -268,6 +268,36 @@ describe "Starting Atom", ->
[otherTempDirPath]
].sort()
it "doesn't reopen any previously opened windows if restorePreviousWindowsOnStart is disabled", ->
runAtom [tempDirPath], {ATOM_HOME: atomHome}, (client) ->
client
.waitForExist("atom-workspace")
.waitForNewWindow(->
@startAnotherAtom([otherTempDirPath], ATOM_HOME: atomHome)
, 5000)
.waitForExist("atom-workspace")
configPath = path.join(atomHome, 'config.cson')
config = CSON.readFileSync(configPath)
config['*'].core = {restorePreviousWindowsOnStart: false}
CSON.writeFileSync(configPath, config)
runAtom [], {ATOM_HOME: atomHome}, (client) ->
windowProjectPaths = []
client
.waitForWindowCount(1, 10000)
.then ({value: windowHandles}) ->
@window(windowHandles[0])
.waitForExist("atom-workspace")
.treeViewRootDirectories()
.then ({value: directories}) -> windowProjectPaths.push(directories)
.call ->
expect(windowProjectPaths).toEqual [
[]
]
describe "opening a remote directory", ->
it "opens the parent directory and creates an empty text editor", ->
remoteDirectory = 'remote://server:3437/some/directory/path'

View File

@@ -1,3 +1,4 @@
Grim = require 'grim'
_ = require 'underscore-plus'
fs = require 'fs-plus'
path = require 'path'
@@ -96,13 +97,10 @@ buildTerminalReporter = (logFile, resolveWithExitCode) ->
log(str)
onComplete: (runner) ->
fs.closeSync(logStream) if logStream?
if process.env.JANKY_SHA1 or process.env.CI
grim = require 'grim'
if grim.getDeprecationsLength() > 0
grim.logDeprecations()
resolveWithExitCode(1)
return
if Grim.getDeprecationsLength() > 0
Grim.logDeprecations()
resolveWithExitCode(1)
return
if runner.results().failedCount > 0
resolveWithExitCode(1)

View File

@@ -334,66 +334,56 @@ describe "LanguageMode", ->
it "folds every foldable line", ->
languageMode.foldAll()
fold1 = editor.tokenizedLineForScreenRow(0).fold
expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 12]
fold1.destroy()
fold2 = editor.tokenizedLineForScreenRow(1).fold
expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [1, 9]
fold2.destroy()
fold3 = editor.tokenizedLineForScreenRow(4).fold
expect([fold3.getStartRow(), fold3.getEndRow()]).toEqual [4, 7]
[fold1, fold2, fold3] = languageMode.unfoldAll()
expect([fold1.start.row, fold1.end.row]).toEqual [0, 12]
expect([fold2.start.row, fold2.end.row]).toEqual [1, 9]
expect([fold3.start.row, fold3.end.row]).toEqual [4, 7]
describe ".foldBufferRow(bufferRow)", ->
describe "when bufferRow can be folded", ->
it "creates a fold based on the syntactic region starting at the given row", ->
languageMode.foldBufferRow(1)
fold = editor.tokenizedLineForScreenRow(1).fold
expect(fold.getStartRow()).toBe 1
expect(fold.getEndRow()).toBe 9
[fold] = languageMode.unfoldAll()
expect([fold.start.row, fold.end.row]).toEqual [1, 9]
describe "when bufferRow can't be folded", ->
it "searches upward for the first row that begins a syntatic region containing the given buffer row (and folds it)", ->
languageMode.foldBufferRow(8)
fold = editor.tokenizedLineForScreenRow(1).fold
expect(fold.getStartRow()).toBe 1
expect(fold.getEndRow()).toBe 9
[fold] = languageMode.unfoldAll()
expect([fold.start.row, fold.end.row]).toEqual [1, 9]
describe "when the bufferRow is already folded", ->
it "searches upward for the first row that begins a syntatic region containing the folded row (and folds it)", ->
languageMode.foldBufferRow(2)
expect(editor.tokenizedLineForScreenRow(1).fold).toBeDefined()
expect(editor.tokenizedLineForScreenRow(0).fold).not.toBeDefined()
expect(editor.isFoldedAtBufferRow(0)).toBe(false)
expect(editor.isFoldedAtBufferRow(1)).toBe(true)
languageMode.foldBufferRow(1)
expect(editor.tokenizedLineForScreenRow(0).fold).toBeDefined()
expect(editor.isFoldedAtBufferRow(0)).toBe(true)
describe "when the bufferRow is in a multi-line comment", ->
it "searches upward and downward for surrounding comment lines and folds them as a single fold", ->
buffer.insert([1, 0], " //this is a comment\n // and\n //more docs\n\n//second comment")
languageMode.foldBufferRow(1)
fold = editor.tokenizedLineForScreenRow(1).fold
expect(fold.getStartRow()).toBe 1
expect(fold.getEndRow()).toBe 3
[fold] = languageMode.unfoldAll()
expect([fold.start.row, fold.end.row]).toEqual [1, 3]
describe "when the bufferRow is a single-line comment", ->
it "searches upward for the first row that begins a syntatic region containing the folded row (and folds it)", ->
buffer.insert([1, 0], " //this is a single line comment\n")
languageMode.foldBufferRow(1)
fold = editor.tokenizedLineForScreenRow(0).fold
expect(fold.getStartRow()).toBe 0
expect(fold.getEndRow()).toBe 13
[fold] = languageMode.unfoldAll()
expect([fold.start.row, fold.end.row]).toEqual [0, 13]
describe ".foldAllAtIndentLevel(indentLevel)", ->
it "folds blocks of text at the given indentation level", ->
languageMode.foldAllAtIndentLevel(0)
expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {"
expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {" + editor.displayLayer.foldCharacter
expect(editor.getLastScreenRow()).toBe 0
languageMode.foldAllAtIndentLevel(1)
expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {"
expect(editor.lineTextForScreenRow(1)).toBe " var sort = function(items) {"
expect(editor.lineTextForScreenRow(1)).toBe " var sort = function(items) {" + editor.displayLayer.foldCharacter
expect(editor.getLastScreenRow()).toBe 4
languageMode.foldAllAtIndentLevel(2)
@@ -429,59 +419,35 @@ describe "LanguageMode", ->
it "folds every foldable line", ->
languageMode.foldAll()
fold1 = editor.tokenizedLineForScreenRow(0).fold
expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 30]
fold1.destroy()
fold2 = editor.tokenizedLineForScreenRow(1).fold
expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [1, 4]
fold3 = editor.tokenizedLineForScreenRow(2).fold.destroy()
fold4 = editor.tokenizedLineForScreenRow(3).fold
expect([fold4.getStartRow(), fold4.getEndRow()]).toEqual [6, 8]
fold5 = editor.tokenizedLineForScreenRow(6).fold
expect([fold5.getStartRow(), fold5.getEndRow()]).toEqual [11, 16]
fold5.destroy()
fold6 = editor.tokenizedLineForScreenRow(13).fold
expect([fold6.getStartRow(), fold6.getEndRow()]).toEqual [21, 22]
fold6.destroy()
folds = languageMode.unfoldAll()
expect(folds.length).toBe 8
expect([folds[0].start.row, folds[0].end.row]).toEqual [0, 30]
expect([folds[1].start.row, folds[1].end.row]).toEqual [1, 4]
expect([folds[2].start.row, folds[2].end.row]).toEqual [5, 27]
expect([folds[3].start.row, folds[3].end.row]).toEqual [6, 8]
expect([folds[4].start.row, folds[4].end.row]).toEqual [11, 16]
expect([folds[5].start.row, folds[5].end.row]).toEqual [17, 20]
expect([folds[6].start.row, folds[6].end.row]).toEqual [21, 22]
expect([folds[7].start.row, folds[7].end.row]).toEqual [24, 25]
describe ".foldAllAtIndentLevel()", ->
it "folds every foldable range at a given indentLevel", ->
languageMode.foldAllAtIndentLevel(2)
fold1 = editor.tokenizedLineForScreenRow(6).fold
expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [6, 8]
fold1.destroy()
fold2 = editor.tokenizedLineForScreenRow(11).fold
expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [11, 16]
fold2.destroy()
fold3 = editor.tokenizedLineForScreenRow(17).fold
expect([fold3.getStartRow(), fold3.getEndRow()]).toEqual [17, 20]
fold3.destroy()
fold4 = editor.tokenizedLineForScreenRow(21).fold
expect([fold4.getStartRow(), fold4.getEndRow()]).toEqual [21, 22]
fold4.destroy()
fold5 = editor.tokenizedLineForScreenRow(24).fold
expect([fold5.getStartRow(), fold5.getEndRow()]).toEqual [24, 25]
fold5.destroy()
folds = languageMode.unfoldAll()
expect(folds.length).toBe 5
expect([folds[0].start.row, folds[0].end.row]).toEqual [6, 8]
expect([folds[1].start.row, folds[1].end.row]).toEqual [11, 16]
expect([folds[2].start.row, folds[2].end.row]).toEqual [17, 20]
expect([folds[3].start.row, folds[3].end.row]).toEqual [21, 22]
expect([folds[4].start.row, folds[4].end.row]).toEqual [24, 25]
it "does not fold anything but the indentLevel", ->
languageMode.foldAllAtIndentLevel(0)
fold1 = editor.tokenizedLineForScreenRow(0).fold
expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 30]
fold1.destroy()
fold2 = editor.tokenizedLineForScreenRow(5).fold
expect(fold2).toBeFalsy()
folds = languageMode.unfoldAll()
expect(folds.length).toBe 1
expect([folds[0].start.row, folds[0].end.row]).toEqual [0, 30]
describe ".isFoldableAtBufferRow(bufferRow)", ->
it "returns true if the line starts a multi-line comment", ->

View File

@@ -19,36 +19,45 @@ describe "LinesYardstick", ->
screenRowsToMeasure = []
buildLineNode = (screenRow) ->
tokenizedLine = editor.tokenizedLineForScreenRow(screenRow)
iterator = tokenizedLine.getTokenIterator()
startIndex = 0
scopes = []
screenLine = editor.screenLineForScreenRow(screenRow)
lineNode = document.createElement("div")
lineNode.style.whiteSpace = "pre"
while iterator.next()
span = document.createElement("span")
span.className = iterator.getScopes().join(' ').replace(/\.+/g, ' ')
span.textContent = iterator.getText()
lineNode.appendChild(span)
for tagCode in screenLine.tagCodes when tagCode isnt 0
if editor.displayLayer.isCloseTagCode(tagCode)
scopes.pop()
else if editor.displayLayer.isOpenTagCode(tagCode)
scopes.push(editor.displayLayer.tagForCode(tagCode))
else
text = screenLine.lineText.substr(startIndex, tagCode)
startIndex += tagCode
span = document.createElement("span")
span.className = scopes.join(' ').replace(/\.+/g, ' ')
span.textContent = text
lineNode.appendChild(span)
jasmine.attachToDOM(lineNode)
createdLineNodes.push(lineNode)
lineNode
mockLineNodesProvider =
lineNodeForLineIdAndScreenRow: (lineId, screenRow) ->
buildLineNode(screenRow)
lineNodesById: {}
lineIdForScreenRow: (screenRow) ->
editor.screenLineForScreenRow(screenRow).id
textNodesForLineIdAndScreenRow: (lineId, screenRow) ->
lineNode = @lineNodeForLineIdAndScreenRow(lineId, screenRow)
lineNodeForScreenRow: (screenRow) ->
@lineNodesById[@lineIdForScreenRow(screenRow)] ?= buildLineNode(screenRow)
textNodesForScreenRow: (screenRow) ->
lineNode = @lineNodeForScreenRow(screenRow)
iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT)
textNodes = []
while textNode = iterator.nextNode()
textNodes.push(textNode)
textNodes.push(textNode) while textNode = iterator.nextNode()
textNodes
editor.setLineHeightInPixels(14)
lineTopIndex = new LineTopIndex({
defaultLineHeight: editor.getLineHeightInPixels()
})
lineTopIndex = new LineTopIndex({defaultLineHeight: editor.getLineHeightInPixels()})
linesYardstick = new LinesYardstick(editor, mockLineNodesProvider, lineTopIndex, atom.grammars)
afterEach ->
@@ -69,9 +78,9 @@ 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: 37.78125, top: 0})
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 6))).toEqual({left: 43.171875, top: 14})
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 9))).toEqual({left: 72.171875, top: 14})
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})
it "reuses already computed pixel positions unless it is invalidated", ->
@@ -82,9 +91,9 @@ describe "LinesYardstick", ->
}
"""
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 19.203125, top: 14})
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 19, top: 14})
expect(linesYardstick.pixelPositionForScreenPosition(Point(2, 6))).toEqual({left: 57.609375, top: 28})
expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 95.609375, top: 70})
expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 96, top: 70})
atom.styles.addStyleSheet """
* {
@@ -92,9 +101,9 @@ describe "LinesYardstick", ->
}
"""
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 19.203125, top: 14})
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 19, top: 14})
expect(linesYardstick.pixelPositionForScreenPosition(Point(2, 6))).toEqual({left: 57.609375, top: 28})
expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 95.609375, top: 70})
expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 96, top: 70})
linesYardstick.invalidateCache()
@@ -102,23 +111,6 @@ describe "LinesYardstick", ->
expect(linesYardstick.pixelPositionForScreenPosition(Point(2, 6))).toEqual({left: 72, top: 28})
expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 120, top: 70})
it "correctly handles RTL characters", ->
atom.styles.addStyleSheet """
* {
font-size: 14px;
font-family: monospace;
}
"""
editor.setText("السلام عليكم")
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 0)).left).toBe 0
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 1)).left).toBe 8
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 2)).left).toBe 16
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 5)).left).toBe 33
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 7)).left).toBe 50
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 9)).left).toBe 67
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 11)).left).toBe 84
it "doesn't report a width greater than 0 when the character to measure is at the beginning of a text node", ->
# This spec documents what seems to be a bug in Chromium, because we'd
# expect that Range(0, 0).getBoundingClientRect().width to always be zero.
@@ -163,9 +155,38 @@ describe "LinesYardstick", ->
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: 80, left: 99.9})).toEqual([5, 14])
expect(linesYardstick.screenPositionForPixelPosition({top: 80, left: 224.2365234375})).toEqual([5, 29])
expect(linesYardstick.screenPositionForPixelPosition({top: 80, left: 225})).toEqual([5, 30])
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-family: monospace;
}
"""
buildLineNode = (screenRow) ->
lineNode = document.createElement("div")
lineNode.style.whiteSpace = "pre"
lineNode.innerHTML = '<span>foo</span><span style="margin-left: 40px">bar</span>'
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]
@@ -178,3 +199,7 @@ describe "LinesYardstick", ->
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]

View File

@@ -8,13 +8,13 @@ describe 'ModuleCache', ->
beforeEach ->
spyOn(Module, '_findPath').andCallThrough()
it 'resolves atom shell module paths without hitting the filesystem', ->
it 'resolves Electron module paths without hitting the filesystem', ->
builtins = ModuleCache.cache.builtins
expect(Object.keys(builtins).length).toBeGreaterThan 0
for builtinName, builtinPath of builtins
expect(require.resolve(builtinName)).toBe builtinPath
expect(fs.isFileSync(require.resolve(builtinName)))
expect(fs.isFileSync(require.resolve(builtinName))).toBeTruthy()
expect(Module._findPath.callCount).toBe 0

View File

@@ -66,6 +66,9 @@ describe "PackageManager", ->
expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load the package-with-broken-package-json package")
expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-broken-package-json"
it "returns null if the package name or path starts with a dot", ->
expect(atom.packages.loadPackage("/Users/user/.atom/packages/.git")).toBeNull()
it "normalizes short repository urls in package.json", ->
{metadata} = atom.packages.loadPackage("package-with-short-url-package-json")
expect(metadata.repository.type).toBe "git"

View File

@@ -0,0 +1,34 @@
PaneAxis = require '../src/pane-axis'
PaneContainer = require '../src/pane-container'
Pane = require '../src/pane'
buildPane = ->
new Pane({
applicationDelegate: atom.applicationDelegate,
config: atom.config,
deserializerManager: atom.deserializers,
notificationManager: atom.notifications
})
describe "PaneAxisElement", ->
it "correctly subscribes and unsubscribes to the underlying model events on attach/detach", ->
container = new PaneContainer(config: atom.config, applicationDelegate: atom.applicationDelegate)
axis = new PaneAxis
axis.setContainer(container)
axisElement = atom.views.getView(axis)
panes = [buildPane(), buildPane(), buildPane()]
jasmine.attachToDOM(axisElement)
axis.addChild(panes[0])
expect(axisElement.children[0]).toBe(atom.views.getView(panes[0]))
axisElement.remove()
axis.addChild(panes[1])
expect(axisElement.children[2]).toBeUndefined()
jasmine.attachToDOM(axisElement)
expect(axisElement.children[2]).toBe(atom.views.getView(panes[1]))
axis.addChild(panes[2])
expect(axisElement.children[4]).toBe(atom.views.getView(panes[2]))

View File

@@ -262,6 +262,26 @@ describe "Pane", ->
pane.setPendingItem("fake item two")
expect(callbackCalled).toBeTruthy()
it "isn't called when a pending item is replaced with a new one", ->
pane = null
pendingSpy = jasmine.createSpy("onItemDidTerminatePendingState")
destroySpy = jasmine.createSpy("onWillDestroyItem")
waitsForPromise ->
atom.workspace.open('sample.txt', pending: true).then ->
pane = atom.workspace.getActivePane()
runs ->
pane.onItemDidTerminatePendingState pendingSpy
pane.onWillDestroyItem destroySpy
waitsForPromise ->
atom.workspace.open('sample.js', pending: true)
runs ->
expect(destroySpy).toHaveBeenCalled()
expect(pendingSpy).not.toHaveBeenCalled()
describe "::activateNextRecentlyUsedItem() and ::activatePreviousRecentlyUsedItem()", ->
it "sets the active item to the next/previous item in the itemStack, looping around at either end", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C"), new Item("D"), new Item("E")]))
@@ -897,6 +917,82 @@ describe "Pane", ->
expect(item1.save).not.toHaveBeenCalled()
expect(pane.isDestroyed()).toBe false
describe "when item fails to save", ->
[pane, item1, item2] = []
beforeEach ->
pane = new Pane({items: [new Item("A"), new Item("B")], applicationDelegate: atom.applicationDelegate, config: atom.config})
[item1, item2] = pane.getItems()
item1.shouldPromptToSave = -> true
item1.getURI = -> "/test/path"
item1.save = jasmine.createSpy("save").andCallFake ->
error = new Error("EACCES, permission denied '/test/path'")
error.path = '/test/path'
error.code = 'EACCES'
throw error
it "does not destroy the pane if save fails and user clicks cancel", ->
confirmations = 0
confirm.andCallFake ->
confirmations++
if confirmations is 1
return 0 # click save
else
return 1 # click cancel
pane.close()
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
expect(confirmations).toBe(2)
expect(item1.save).toHaveBeenCalled()
expect(pane.isDestroyed()).toBe false
it "does destroy the pane if the user saves the file under a new name", ->
item1.saveAs = jasmine.createSpy("saveAs").andReturn(true)
confirmations = 0
confirm.andCallFake ->
confirmations++
return 0 # save and then save as
showSaveDialog.andReturn("new/path")
pane.close()
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
expect(confirmations).toBe(2)
expect(atom.applicationDelegate.showSaveDialog).toHaveBeenCalled()
expect(item1.save).toHaveBeenCalled()
expect(item1.saveAs).toHaveBeenCalled()
expect(pane.isDestroyed()).toBe true
it "asks again if the saveAs also fails", ->
item1.saveAs = jasmine.createSpy("saveAs").andCallFake ->
error = new Error("EACCES, permission denied '/test/path'")
error.path = '/test/path'
error.code = 'EACCES'
throw error
confirmations = 0
confirm.andCallFake ->
confirmations++
if confirmations < 3
return 0 # save, save as, save as
return 2 # don't save
showSaveDialog.andReturn("new/path")
pane.close()
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
expect(confirmations).toBe(3)
expect(atom.applicationDelegate.showSaveDialog).toHaveBeenCalled()
expect(item1.save).toHaveBeenCalled()
expect(item1.saveAs).toHaveBeenCalled()
expect(pane.isDestroyed()).toBe true
describe "::destroy()", ->
[container, pane1, pane2] = []

View File

@@ -87,7 +87,7 @@ describe "Project", ->
runs ->
bufferA = atom.project.getBuffers()[0]
layerA = bufferA.addMarkerLayer(maintainHistory: true)
layerA = bufferA.addMarkerLayer(persistent: true)
markerA = layerA.markPosition([0, 3])
notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
@@ -526,7 +526,7 @@ describe "Project", ->
expect(atom.project.getDirectories()[1].contains(inputPath)).toBe true
expect(atom.project.relativizePath(inputPath)).toEqual [
atom.project.getPaths()[1],
'somewhere/something.txt'
path.join('somewhere', 'something.txt')
]
describe ".contains(path)", ->

View File

@@ -17,7 +17,7 @@ describe "TextEditor", ->
buffer = new TextBuffer
editor = atom.workspace.buildTextEditor({buffer})
editor.setEditorWidthInChars(80)
tokenizedBuffer = editor.displayBuffer.tokenizedBuffer
tokenizedBuffer = editor.tokenizedBuffer
steps = []
times 30, ->
@@ -33,8 +33,8 @@ describe "TextEditor", ->
logLines()
throw new Error("Invalid buffer row #{actualBufferRow} for screen row #{screenRow}", )
actualScreenLine = editor.tokenizedLineForScreenRow(screenRow)
unless actualScreenLine.text is referenceScreenLine.text
actualScreenLine = editor.lineTextForScreenRow(screenRow)
unless actualScreenLine is referenceScreenLine
logLines()
throw new Error("Invalid line text at screen row #{screenRow}")
@@ -84,7 +84,8 @@ describe "TextEditor", ->
referenceEditor.setEditorWidthInChars(80)
referenceEditor.setText(editor.getText())
referenceEditor.setSoftWrapped(editor.isSoftWrapped())
screenLines = referenceEditor.tokenizedLinesForScreenRows(0, referenceEditor.getLastScreenRow())
screenLines = [0..referenceEditor.getLastScreenRow()].map (row) => referenceEditor.lineTextForScreenRow(row)
bufferRows = referenceEditor.bufferRowsForScreenRows(0, referenceEditor.getLastScreenRow())
{screenLines, bufferRows}

View File

@@ -91,3 +91,32 @@ describe "Selection", ->
expect(buffer.lineForRow(0)).toBe " "
expect(buffer.lineForRow(1)).toBe " "
expect(buffer.lineForRow(2)).toBe ""
it "auto-indents if only a newline is inserted", ->
selection.setBufferRange [[2, 0], [3, 0]]
selection.insertText("\n", autoIndent: true)
expect(buffer.lineForRow(2)).toBe " "
it "auto-indents if only a carriage return + newline is inserted", ->
selection.setBufferRange [[2, 0], [3, 0]]
selection.insertText("\r\n", autoIndent: true)
expect(buffer.lineForRow(2)).toBe " "
describe ".fold()", ->
it "folds the buffer range spanned by the selection", ->
selection.setBufferRange([[0, 3], [1, 6]])
selection.fold()
expect(selection.getScreenRange()).toEqual([[0, 4], [0, 4]])
expect(selection.getBufferRange()).toEqual([[1, 6], [1, 6]])
expect(editor.lineTextForScreenRow(0)).toBe "var#{editor.displayLayer.foldCharacter}sort = function(items) {"
expect(editor.isFoldedAtBufferRow(0)).toBe(true)
it "doesn't create a fold when the selection is empty", ->
selection.setBufferRange([[0, 3], [0, 3]])
selection.fold()
expect(selection.getScreenRange()).toEqual([[0, 3], [0, 3]])
expect(selection.getBufferRange()).toEqual([[0, 3], [0, 3]])
expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {"
expect(editor.isFoldedAtBufferRow(0)).toBe(false)

57
spec/spawner-spec.coffee Normal file
View File

@@ -0,0 +1,57 @@
ChildProcess = require 'child_process'
Spawner = require '../src/browser/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

View File

@@ -112,14 +112,14 @@ afterEach ->
document.getElementById('jasmine-content').innerHTML = '' unless window.debugContent
ensureNoPathSubscriptions()
warnIfLeakingPathSubscriptions()
waits(0) # yield to ui thread to make screen update more frequently
ensureNoPathSubscriptions = ->
warnIfLeakingPathSubscriptions = ->
watchedPaths = pathwatcher.getWatchedPaths()
pathwatcher.closeAllWatchers()
if watchedPaths.length > 0
throw new Error("Leaking subscriptions for paths: " + watchedPaths.join(", "))
console.error("WARNING: Leaking subscriptions for paths: " + watchedPaths.join(", "))
pathwatcher.closeAllWatchers()
ensureNoDeprecatedFunctionsCalled = ->
deprecations = Grim.getDeprecations()
@@ -172,8 +172,8 @@ jasmine.useRealClock = ->
addCustomMatchers = (spec) ->
spec.addMatchers
toBeInstanceOf: (expected) ->
notText = if @isNot then " not" else ""
this.message = => "Expected #{jasmine.pp(@actual)} to#{notText} be instance of #{expected.name} class"
beOrNotBe = if @isNot then "not be" else "be"
this.message = => "Expected #{jasmine.pp(@actual)} to #{beOrNotBe} instance of #{expected.name} class"
@actual instanceof expected
toHaveLength: (expected) ->
@@ -181,32 +181,38 @@ addCustomMatchers = (spec) ->
this.message = => "Expected object #{@actual} has no length method"
false
else
notText = if @isNot then " not" else ""
this.message = => "Expected object with length #{@actual.length} to#{notText} have length #{expected}"
haveOrNotHave = if @isNot then "not have" else "have"
this.message = => "Expected object with length #{@actual.length} to #{haveOrNotHave} length #{expected}"
@actual.length is expected
toExistOnDisk: (expected) ->
notText = this.isNot and " not" or ""
@message = -> return "Expected path '" + @actual + "'" + notText + " to exist."
toOrNotTo = this.isNot and "not to" or "to"
@message = -> return "Expected path '#{@actual}' #{toOrNotTo} exist."
fs.existsSync(@actual)
toHaveFocus: ->
notText = this.isNot and " not" or ""
toOrNotTo = this.isNot and "not to" or "to"
if not document.hasFocus()
console.error "Specs will fail because the Dev Tools have focus. To fix this close the Dev Tools or click the spec runner."
@message = -> return "Expected element '" + @actual + "' or its descendants" + notText + " to have focus."
@message = -> return "Expected element '#{@actual}' or its descendants #{toOrNotTo} have focus."
element = @actual
element = element.get(0) if element.jquery
element is document.activeElement or element.contains(document.activeElement)
toShow: ->
notText = if @isNot then " not" else ""
toOrNotTo = this.isNot and "not to" or "to"
element = @actual
element = element.get(0) if element.jquery
@message = -> return "Expected element '#{element}' or its descendants#{notText} to show."
@message = -> return "Expected element '#{element}' or its descendants #{toOrNotTo} show."
element.style.display in ['block', 'inline-block', 'static', 'fixed']
toEqualPath: (expected) ->
actualPath = path.normalize(@actual)
expectedPath = path.normalize(expected)
@message = -> return "Expected path '#{actualPath}' to be equal to '#{expectedPath}'."
actualPath is expectedPath
window.waitsForPromise = (args...) ->
label = null
if args.length > 1

View File

@@ -1,39 +1,37 @@
ChildProcess = require 'child_process'
{EventEmitter} = require 'events'
fs = require 'fs-plus'
path = require 'path'
temp = require 'temp'
SquirrelUpdate = require '../src/browser/squirrel-update'
Spawner = require '../src/browser/spawner'
WinPowerShell = require '../src/browser/win-powershell'
WinRegistry = require '../src/browser/win-registry'
# Run passed callback as Spawner.spawn() would do
invokeCallback = (callback) ->
error = null
stdout = ''
callback?(error, stdout)
describe "Windows Squirrel Update", ->
tempHomeDirectory = null
originalSpawn = ChildProcess.spawn
harmlessSpawn = ->
# Just spawn something that won't actually modify the host
if process.platform is 'win32'
originalSpawn('dir')
else
originalSpawn('ls')
beforeEach ->
# Prevent the actual home directory from being manipulated
tempHomeDirectory = temp.mkdirSync('atom-temp-home-')
spyOn(fs, 'getHomeDirectory').andReturn(tempHomeDirectory)
# Prevent any commands from actually running and affecting the host
spyOn(ChildProcess, 'spawn').andCallFake (command, args) ->
harmlessSpawn()
# Prevent any spawned command from actually running and affecting the host
spyOn(Spawner, 'spawn').andCallFake (command, args, callback) ->
# do nothing on command, just run passed callback
invokeCallback callback
it "ignores errors spawning Squirrel", ->
jasmine.unspy(ChildProcess, 'spawn')
spyOn(ChildProcess, 'spawn').andCallFake -> throw new Error("EBUSY")
app = quit: jasmine.createSpy('quit')
expect(SquirrelUpdate.handleStartupEvent(app, '--squirrel-install')).toBe true
waitsFor ->
app.quit.callCount is 1
# Prevent any actual change to Windows registry
for own method of WinRegistry
# all WinRegistry APIs share the same signature
spyOn(WinRegistry, method).andCallFake (callback) ->
# do nothing on registry, just run passed callback
invokeCallback callback
it "quits the app on all squirrel events", ->
app = quit: jasmine.createSpy('quit')
@@ -69,51 +67,52 @@ describe "Windows Squirrel Update", ->
describe "Desktop shortcut", ->
desktopShortcutPath = '/non/existing/path'
beforeEach ->
desktopShortcutPath = path.join(tempHomeDirectory, 'Desktop', 'Atom.lnk')
jasmine.unspy(ChildProcess, 'spawn')
spyOn(ChildProcess, 'spawn').andCallFake (command, args) ->
jasmine.unspy(Spawner, 'spawn')
spyOn(Spawner, 'spawn').andCallFake (command, args, callback) ->
if path.basename(command) is 'Update.exe' and args?[0] is '--createShortcut'
fs.writeFileSync(path.join(tempHomeDirectory, 'Desktop', 'Atom.lnk'), '')
harmlessSpawn()
fs.writeFileSync(desktopShortcutPath, '')
else
throw new Error("API not mocked")
# simply ignore other commands
invokeCallback callback
it "does not exist before install", ->
expect(fs.existsSync(desktopShortcutPath)).toBe false
describe "on install", ->
beforeEach ->
app = quit: jasmine.createSpy('quit')
SquirrelUpdate.handleStartupEvent(app, '--squirrel-install')
waitsFor ->
app.quit.callCount is 1
it "creates desktop shortcut", ->
expect(fs.existsSync(desktopShortcutPath)).toBe true
describe "when shortcut is deleted and then app is updated", ->
beforeEach ->
fs.removeSync(desktopShortcutPath)
expect(fs.existsSync(desktopShortcutPath)).toBe false
app = quit: jasmine.createSpy('quit')
SquirrelUpdate.handleStartupEvent(app, '--squirrel-updated')
waitsFor ->
app.quit.callCount is 1
it "does not recreate shortcut", ->
expect(fs.existsSync(desktopShortcutPath)).toBe false
describe "when shortcut is kept and app is updated", ->
beforeEach ->
app = quit: jasmine.createSpy('quit')
SquirrelUpdate.handleStartupEvent(app, '--squirrel-updated')
waitsFor ->
app.quit.callCount is 1
it "still has desktop shortcut", ->
expect(fs.existsSync(desktopShortcutPath)).toBe true
@@ -125,7 +124,7 @@ describe "Windows Squirrel Update", ->
SquirrelUpdate.restartAtom(app)
expect(app.quit.callCount).toBe 1
expect(ChildProcess.spawn.callCount).toBe 0
expect(Spawner.spawn.callCount).toBe 0
app.emit('will-quit')
expect(ChildProcess.spawn.callCount).toBe 1
expect(path.basename(ChildProcess.spawn.argsForCall[0][0])).toBe 'atom.cmd'
expect(Spawner.spawn.callCount).toBe 1
expect(path.basename(Spawner.spawn.argsForCall[0][0])).toBe 'atom.cmd'

View File

@@ -69,13 +69,12 @@ describe('TextEditorComponent', function () {
describe('line rendering', async function () {
function expectTileContainsRow (tileNode, screenRow, {top}) {
let lineNode = tileNode.querySelector('[data-screen-row="' + screenRow + '"]')
let tokenizedLine = editor.tokenizedLineForScreenRow(screenRow)
let text = editor.lineTextForScreenRow(screenRow)
expect(lineNode.offsetTop).toBe(top)
if (tokenizedLine.text === '') {
expect(lineNode.innerHTML).toBe('&nbsp;')
if (text === '') {
expect(lineNode.textContent).toBe(' ')
} else {
expect(lineNode.textContent).toBe(tokenizedLine.text)
expect(lineNode.textContent).toBe(text)
}
}
@@ -294,12 +293,12 @@ describe('TextEditorComponent', function () {
await nextViewUpdatePromise()
expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.tokenizedLineForScreenRow(3).text)
expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.lineTextForScreenRow(3))
buffer.delete([[0, 0], [3, 0]])
await nextViewUpdatePromise()
expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.tokenizedLineForScreenRow(3).text)
expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.lineTextForScreenRow(3))
})
it('updates the top position of lines when the line height changes', async function () {
@@ -361,9 +360,9 @@ describe('TextEditorComponent', function () {
}
})
it('renders an nbsp on empty lines when no line-ending character is defined', function () {
it('renders an placeholder space on empty lines when no line-ending character is defined', function () {
atom.config.set('editor.showInvisibles', false)
expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP)
expect(component.lineNodeForScreenRow(10).textContent).toBe(' ')
})
it('gives the lines and tiles divs the same background color as the editor to improve GPU performance', async function () {
@@ -429,13 +428,14 @@ describe('TextEditorComponent', function () {
expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false)
})
it('keeps rebuilding lines when continuous reflow is on', function () {
it('keeps rebuilding lines when continuous reflow is on', async function () {
wrapperNode.setContinuousReflow(true)
let oldLineNode = componentNode.querySelector('.line')
let oldLineNode = componentNode.querySelectorAll('.line')[1]
waitsFor(function () {
return componentNode.querySelector('.line') !== oldLineNode
})
while (true) {
await nextViewUpdatePromise()
if (componentNode.querySelectorAll('.line')[1] !== oldLineNode) break
}
})
describe('when showInvisibles is enabled', function () {
@@ -484,7 +484,7 @@ describe('TextEditorComponent', function () {
it('displays newlines as their own token outside of the other tokens\' scopeDescriptor', async function () {
editor.setText('let\n')
await nextViewUpdatePromise()
expect(component.lineNodeForScreenRow(0).innerHTML).toBe('<span class="source js"><span class="storage type var js">let</span></span><span class="invisible-character">' + invisibles.eol + '</span>')
expect(component.lineNodeForScreenRow(0).innerHTML).toBe('<span class="source js"><span class="storage type var js">let</span><span class="invisible-character eol">' + invisibles.eol + '</span></span>')
})
it('displays trailing carriage returns using a visible, non-empty value', async function () {
@@ -497,20 +497,20 @@ describe('TextEditorComponent', function () {
expect(component.lineNodeForScreenRow(10).textContent).toBe(invisibles.eol)
})
it('renders an nbsp on empty lines when the line-ending character is an empty string', async function () {
it('renders a placeholder space on empty lines when the line-ending character is an empty string', async function () {
atom.config.set('editor.invisibles', {
eol: ''
})
await nextViewUpdatePromise()
expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP)
expect(component.lineNodeForScreenRow(10).textContent).toBe(' ')
})
it('renders an nbsp on empty lines when the line-ending character is false', async function () {
it('renders an placeholder space on empty lines when the line-ending character is false', async function () {
atom.config.set('editor.invisibles', {
eol: false
})
await nextViewUpdatePromise()
expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP)
expect(component.lineNodeForScreenRow(10).textContent).toBe(' ')
})
it('interleaves invisible line-ending characters with indent guides on empty lines', async function () {
@@ -518,24 +518,25 @@ describe('TextEditorComponent', function () {
await nextViewUpdatePromise()
editor.setTabLength(2)
editor.setTextInBufferRange([[10, 0], [11, 0]], '\r\n', {
normalizeLineEndings: false
})
await nextViewUpdatePromise()
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="source js"><span class="invisible-character eol indent-guide">CE</span></span>')
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="indent-guide"><span class="invisible-character">C</span><span class="invisible-character">E</span></span>')
editor.setTabLength(3)
await nextViewUpdatePromise()
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="source js"><span class="invisible-character eol indent-guide">CE</span></span>')
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="indent-guide"><span class="invisible-character">C</span><span class="invisible-character">E</span> </span>')
editor.setTabLength(1)
await nextViewUpdatePromise()
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="source js"><span class="invisible-character eol indent-guide">CE</span></span>')
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="indent-guide"><span class="invisible-character">C</span></span><span class="indent-guide"><span class="invisible-character">E</span></span>')
editor.setTextInBufferRange([[9, 0], [9, Infinity]], ' ')
editor.setTextInBufferRange([[11, 0], [11, Infinity]], ' ')
await nextViewUpdatePromise()
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="indent-guide"><span class="invisible-character">C</span></span><span class="invisible-character">E</span>')
expect(component.lineNodeForScreenRow(10).innerHTML).toBe('<span class="source js"><span class="invisible-character eol indent-guide">CE</span></span>')
})
describe('when soft wrapping is enabled', function () {
@@ -550,8 +551,8 @@ describe('TextEditorComponent', function () {
})
it('does not show end of line invisibles at the end of wrapped lines', function () {
expect(component.lineNodeForScreenRow(0).textContent).toBe('a line that ')
expect(component.lineNodeForScreenRow(1).textContent).toBe('wraps' + invisibles.space + invisibles.eol)
expect(component.lineNodeForScreenRow(0).textContent).toBe('a line ')
expect(component.lineNodeForScreenRow(1).textContent).toBe('that wraps' + invisibles.space + invisibles.eol)
})
})
})
@@ -986,13 +987,14 @@ describe('TextEditorComponent', function () {
expect(component.lineNumberNodeForScreenRow(3) != null).toBe(true)
})
it('keeps rebuilding line numbers when continuous reflow is on', function () {
it('keeps rebuilding line numbers when continuous reflow is on', async function () {
wrapperNode.setContinuousReflow(true)
let oldLineNode = componentNode.querySelectorAll('.line-number')[1]
waitsFor(function () {
return componentNode.querySelectorAll('.line-number')[1] !== oldLineNode
})
while (true) {
await nextViewUpdatePromise()
if (componentNode.querySelectorAll('.line-number')[1] !== oldLineNode) break
}
})
describe('fold decorations', function () {
@@ -1051,7 +1053,7 @@ describe('TextEditorComponent', function () {
beforeEach(async function () {
editor.setSoftWrapped(true)
await nextViewUpdatePromise()
componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px'
componentNode.style.width = 20 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px'
component.measureDimensions()
await nextViewUpdatePromise()
})
@@ -1060,6 +1062,14 @@ describe('TextEditorComponent', function () {
expect(lineNumberHasClass(0, 'foldable')).toBe(true)
expect(lineNumberHasClass(1, 'foldable')).toBe(false)
})
it('does not add the folded class for soft-wrapped lines that contain a fold', async function () {
editor.foldBufferRange([[3, 19], [3, 21]])
await nextViewUpdatePromise()
expect(lineNumberHasClass(11, 'folded')).toBe(true)
expect(lineNumberHasClass(12, 'folded')).toBe(false)
})
})
})
@@ -1082,7 +1092,7 @@ describe('TextEditorComponent', function () {
component.destroy()
lineNumber = component.lineNumberNodeForScreenRow(1)
target = lineNumber.querySelector('.icon-right')
return target.dispatchEvent(buildClickEvent(target))
target.dispatchEvent(buildClickEvent(target))
})
})
@@ -1106,6 +1116,37 @@ describe('TextEditorComponent', function () {
expect(lineNumberHasClass(1, 'folded')).toBe(false)
})
it('unfolds all the free-form folds intersecting the buffer row when clicked', async function () {
expect(lineNumberHasClass(3, 'foldable')).toBe(false)
editor.foldBufferRange([[3, 4], [5, 4]])
editor.foldBufferRange([[5, 5], [8, 10]])
await nextViewUpdatePromise()
expect(lineNumberHasClass(3, 'folded')).toBe(true)
expect(lineNumberHasClass(5, 'folded')).toBe(false)
let lineNumber = component.lineNumberNodeForScreenRow(3)
let target = lineNumber.querySelector('.icon-right')
target.dispatchEvent(buildClickEvent(target))
await nextViewUpdatePromise()
expect(lineNumberHasClass(3, 'folded')).toBe(false)
expect(lineNumberHasClass(5, 'folded')).toBe(true)
editor.setSoftWrapped(true)
componentNode.style.width = 20 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px'
component.measureDimensions()
await nextViewUpdatePromise()
editor.foldBufferRange([[3, 19], [3, 21]]) // fold starting on a soft-wrapped portion of the line
await nextViewUpdatePromise()
expect(lineNumberHasClass(11, 'folded')).toBe(true)
lineNumber = component.lineNumberNodeForScreenRow(11)
target = lineNumber.querySelector('.icon-right')
target.dispatchEvent(buildClickEvent(target))
await nextViewUpdatePromise()
expect(lineNumberHasClass(11, 'folded')).toBe(false)
})
it('does not fold when the line number componentNode is clicked', function () {
let lineNumber = component.lineNumberNodeForScreenRow(1)
lineNumber.dispatchEvent(buildClickEvent(lineNumber))
@@ -1200,7 +1241,7 @@ describe('TextEditorComponent', function () {
let cursor = componentNode.querySelector('.cursor')
let cursorRect = cursor.getBoundingClientRect()
let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.source.js').childNodes[2]
let range = document.createRange()
let range = document.createRange(cursorLocationTextNode)
range.setStart(cursorLocationTextNode, 0)
range.setEnd(cursorLocationTextNode, 1)
let rangeRect = range.getBoundingClientRect()
@@ -1208,6 +1249,17 @@ describe('TextEditorComponent', function () {
expect(cursorRect.width).toBeCloseTo(rangeRect.width, 0)
})
it('positions cursors after the fold-marker when a fold ends the line', async function () {
editor.foldBufferRow(0)
await nextViewUpdatePromise()
editor.setCursorScreenPosition([0, 30])
await nextViewUpdatePromise()
let cursorRect = componentNode.querySelector('.cursor').getBoundingClientRect()
let foldMarkerRect = componentNode.querySelector('.fold-marker').getBoundingClientRect()
expect(cursorRect.left).toBeCloseTo(foldMarkerRect.right, 0)
})
it('positions cursors correctly after character widths are changed via a stylesheet change', async function () {
atom.config.set('editor.fontFamily', 'sans-serif')
editor.setCursorScreenPosition([0, 16])
@@ -1475,7 +1527,7 @@ describe('TextEditorComponent', function () {
component.measureDimensions()
await nextViewUpdatePromise()
let marker2 = editor.displayBuffer.markBufferRange([[9, 0], [9, 0]])
let marker2 = editor.markBufferRange([[9, 0], [9, 0]])
editor.decorateMarker(marker2, {
type: ['line-number', 'line'],
'class': 'b'
@@ -1887,7 +1939,7 @@ describe('TextEditorComponent', function () {
component.measureDimensions()
await nextViewUpdatePromise()
marker = editor.displayBuffer.markBufferRange([[9, 2], [9, 4]], {
marker = editor.markBufferRange([[9, 2], [9, 4]], {
invalidate: 'inside'
})
editor.decorateMarker(marker, {
@@ -2082,7 +2134,7 @@ describe('TextEditorComponent', function () {
describe('when the marker is empty', function () {
it('renders an overlay decoration when added and removes the overlay when the decoration is destroyed', async function () {
let marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], {
let marker = editor.markBufferRange([[2, 13], [2, 13]], {
invalidate: 'never'
})
let decoration = editor.decorateMarker(marker, {
@@ -2104,7 +2156,7 @@ describe('TextEditorComponent', function () {
})
it('renders the overlay element with the CSS class specified by the decoration', async function () {
let marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], {
let marker = editor.markBufferRange([[2, 13], [2, 13]], {
invalidate: 'never'
})
let decoration = editor.decorateMarker(marker, {
@@ -2125,7 +2177,7 @@ describe('TextEditorComponent', function () {
describe('when the marker is not empty', function () {
it('renders at the head of the marker by default', async function () {
let marker = editor.displayBuffer.markBufferRange([[2, 5], [2, 10]], {
let marker = editor.markBufferRange([[2, 5], [2, 10]], {
invalidate: 'never'
})
let decoration = editor.decorateMarker(marker, {
@@ -2171,7 +2223,7 @@ describe('TextEditorComponent', function () {
})
it('slides horizontally left when near the right edge on #win32 and #darwin', async function () {
let marker = editor.displayBuffer.markBufferRange([[0, 26], [0, 26]], {
let marker = editor.markBufferRange([[0, 26], [0, 26]], {
invalidate: 'never'
})
let decoration = editor.decorateMarker(marker, {
@@ -2753,20 +2805,60 @@ describe('TextEditorComponent', function () {
})
})
describe('when a line is folded', function () {
beforeEach(async function () {
editor.foldBufferRow(4)
describe('when a fold marker is clicked', function () {
function clickElementAtPosition (marker, position) {
linesNode.dispatchEvent(
buildMouseEvent('mousedown', clientCoordinatesForScreenPosition(position), {target: marker})
)
}
it('unfolds only the selected fold when other folds are on the same line', async function () {
editor.foldBufferRange([[4, 6], [4, 10]])
editor.foldBufferRange([[4, 15], [4, 20]])
await nextViewUpdatePromise()
let foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
expect(foldMarkers.length).toBe(2)
expect(editor.isFoldedAtBufferRow(4)).toBe(true)
clickElementAtPosition(foldMarkers[0], [4, 6])
await nextViewUpdatePromise()
foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
expect(foldMarkers.length).toBe(1)
expect(editor.isFoldedAtBufferRow(4)).toBe(true)
clickElementAtPosition(foldMarkers[0], [4, 15])
await nextViewUpdatePromise()
foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
expect(foldMarkers.length).toBe(0)
expect(editor.isFoldedAtBufferRow(4)).toBe(false)
})
describe('when the folded line\'s fold-marker is clicked', function () {
it('unfolds the buffer row', function () {
let target = component.lineNodeForScreenRow(4).querySelector('.fold-marker')
linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]), {
target: target
}))
expect(editor.isFoldedAtBufferRow(4)).toBe(false)
})
it('unfolds only the selected fold when other folds are inside it', async function () {
editor.foldBufferRange([[4, 10], [4, 15]])
editor.foldBufferRange([[4, 4], [4, 5]])
editor.foldBufferRange([[4, 4], [4, 20]])
await nextViewUpdatePromise()
let foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
expect(foldMarkers.length).toBe(1)
expect(editor.isFoldedAtBufferRow(4)).toBe(true)
clickElementAtPosition(foldMarkers[0], [4, 4])
await nextViewUpdatePromise()
foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
expect(foldMarkers.length).toBe(1)
expect(editor.isFoldedAtBufferRow(4)).toBe(true)
clickElementAtPosition(foldMarkers[0], [4, 4])
await nextViewUpdatePromise()
foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
expect(foldMarkers.length).toBe(1)
expect(editor.isFoldedAtBufferRow(4)).toBe(true)
clickElementAtPosition(foldMarkers[0], [4, 10])
await nextViewUpdatePromise()
foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
expect(foldMarkers.length).toBe(0)
expect(editor.isFoldedAtBufferRow(4)).toBe(false)
})
})
@@ -3101,7 +3193,7 @@ describe('TextEditorComponent', function () {
gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(11), {
shiftKey: true
}))
expect(editor.getSelectedScreenRange()).toEqual([[7, 4], [16, 0]])
expect(editor.getSelectedScreenRange()).toEqual([[7, 4], [17, 0]])
})
})
})
@@ -3175,7 +3267,7 @@ describe('TextEditorComponent', function () {
gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(11), {
metaKey: true
}))
expect(editor.getSelectedScreenRanges()).toEqual([[[7, 4], [7, 6]], [[11, 4], [19, 0]]])
expect(editor.getSelectedScreenRanges()).toEqual([[[7, 4], [7, 6]], [[11, 4], [20, 0]]])
})
it('merges overlapping selections on mouseup', async function () {
@@ -3189,7 +3281,7 @@ describe('TextEditorComponent', function () {
gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(5), {
metaKey: true
}))
expect(editor.getSelectedScreenRanges()).toEqual([[[5, 0], [19, 0]]])
expect(editor.getSelectedScreenRanges()).toEqual([[[5, 0], [20, 0]]])
})
})
})
@@ -3204,7 +3296,7 @@ describe('TextEditorComponent', function () {
}))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11)))
await nextAnimationFramePromise()
expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [11, 14]])
expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [11, 5]])
})
})
@@ -3745,6 +3837,21 @@ describe('TextEditorComponent', function () {
return event
}
function buildKeydownEvent ({keyCode, target}) {
let event = new KeyboardEvent('keydown')
Object.defineProperty(event, 'keyCode', {
get: function () {
return keyCode
}
})
Object.defineProperty(event, 'target', {
get: function () {
return target
}
})
return event
}
let inputNode
beforeEach(function () {
@@ -3767,11 +3874,12 @@ describe('TextEditorComponent', function () {
expect(editor.lineTextForBufferRow(0)).toBe('xyvar quicksort = function () {')
})
it('replaces the last character if the length of the input\'s value does not increase, as occurs with the accented character menu', async function () {
componentNode.dispatchEvent(buildTextInputEvent({
data: 'u',
target: inputNode
}))
it('replaces the last character if a keypress event is bracketed by keydown events with matching keyCodes, which occurs when the accented character menu is shown', async function () {
componentNode.dispatchEvent(buildKeydownEvent({keyCode: 85, target: inputNode}))
componentNode.dispatchEvent(buildTextInputEvent({data: 'u', target: inputNode}))
componentNode.dispatchEvent(new KeyboardEvent('keypress'))
componentNode.dispatchEvent(buildKeydownEvent({keyCode: 85, target: inputNode}))
componentNode.dispatchEvent(new KeyboardEvent('keyup'))
await nextViewUpdatePromise()
expect(editor.lineTextForBufferRow(0)).toBe('uvar quicksort = function () {')
@@ -4006,15 +4114,33 @@ describe('TextEditorComponent', function () {
})
})
describe('when changing the font', async function () {
it('measures the default char, the korean char, the double width char and the half width char widths', async function () {
expect(editor.getDefaultCharWidth()).toBeCloseTo(12, 0)
describe('when decreasing the fontSize', async function () {
it('decreases the widths of the korean char, the double width char and the half width char', async function () {
originalDefaultCharWidth = editor.getDefaultCharWidth()
koreanDefaultCharWidth = editor.getKoreanCharWidth()
doubleWidthDefaultCharWidth = editor.getDoubleWidthCharWidth()
halfWidthDefaultCharWidth = editor.getHalfWidthCharWidth()
component.setFontSize(10)
await nextViewUpdatePromise()
expect(editor.getDefaultCharWidth()).toBeCloseTo(6, 0)
expect(editor.getKoreanCharWidth()).toBeCloseTo(9, 0)
expect(editor.getDoubleWidthCharWidth()).toBe(10)
expect(editor.getHalfWidthCharWidth()).toBe(5)
expect(editor.getDefaultCharWidth()).toBeLessThan(originalDefaultCharWidth)
expect(editor.getKoreanCharWidth()).toBeLessThan(koreanDefaultCharWidth)
expect(editor.getDoubleWidthCharWidth()).toBeLessThan(doubleWidthDefaultCharWidth)
expect(editor.getHalfWidthCharWidth()).toBeLessThan(halfWidthDefaultCharWidth)
})
})
describe('when increasing the fontSize', function() {
it('increases the widths of the korean char, the double width char and the half width char', async function () {
originalDefaultCharWidth = editor.getDefaultCharWidth()
koreanDefaultCharWidth = editor.getKoreanCharWidth()
doubleWidthDefaultCharWidth = editor.getDoubleWidthCharWidth()
halfWidthDefaultCharWidth = editor.getHalfWidthCharWidth()
component.setFontSize(25)
await nextViewUpdatePromise()
expect(editor.getDefaultCharWidth()).toBeGreaterThan(originalDefaultCharWidth)
expect(editor.getKoreanCharWidth()).toBeGreaterThan(koreanDefaultCharWidth)
expect(editor.getDoubleWidthCharWidth()).toBeGreaterThan(doubleWidthDefaultCharWidth)
expect(editor.getHalfWidthCharWidth()).toBeGreaterThan(halfWidthDefaultCharWidth)
})
})
@@ -4932,7 +5058,7 @@ describe('TextEditorComponent', function () {
function lineNumberForBufferRowHasClass (bufferRow, klass) {
let screenRow
screenRow = editor.displayBuffer.screenRowForBufferRow(bufferRow)
screenRow = editor.screenRowForBufferRow(bufferRow)
return component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass)
}

View File

@@ -1143,53 +1143,6 @@ describe "TextEditorPresenter", ->
expectStateUpdate presenter, -> presenter.setScrollLeft(-300)
expect(getState(presenter).content.scrollLeft).toBe 0
describe ".indentGuidesVisible", ->
it "is initialized based on the editor.showIndentGuide config setting", ->
presenter = buildPresenter()
expect(getState(presenter).content.indentGuidesVisible).toBe false
atom.config.set('editor.showIndentGuide', true)
presenter = buildPresenter()
expect(getState(presenter).content.indentGuidesVisible).toBe true
it "updates when the editor.showIndentGuide config setting changes", ->
presenter = buildPresenter()
expect(getState(presenter).content.indentGuidesVisible).toBe false
expectStateUpdate presenter, -> atom.config.set('editor.showIndentGuide', true)
expect(getState(presenter).content.indentGuidesVisible).toBe true
expectStateUpdate presenter, -> atom.config.set('editor.showIndentGuide', false)
expect(getState(presenter).content.indentGuidesVisible).toBe false
it "updates when the editor's grammar changes", ->
atom.config.set('editor.showIndentGuide', true, scopeSelector: ".source.js")
presenter = buildPresenter()
expect(getState(presenter).content.indentGuidesVisible).toBe false
stateUpdated = false
presenter.onDidUpdateState -> stateUpdated = true
waitsForPromise -> atom.packages.activatePackage('language-javascript')
runs ->
expect(stateUpdated).toBe true
expect(getState(presenter).content.indentGuidesVisible).toBe true
expectStateUpdate presenter, -> editor.setGrammar(atom.grammars.selectGrammar('.txt'))
expect(getState(presenter).content.indentGuidesVisible).toBe false
it "is always false when the editor is mini", ->
atom.config.set('editor.showIndentGuide', true)
editor.setMini(true)
presenter = buildPresenter()
expect(getState(presenter).content.indentGuidesVisible).toBe false
editor.setMini(false)
expect(getState(presenter).content.indentGuidesVisible).toBe true
editor.setMini(true)
expect(getState(presenter).content.indentGuidesVisible).toBe false
describe ".backgroundColor", ->
it "is assigned to ::backgroundColor unless the editor is mini", ->
presenter = buildPresenter()
@@ -1229,9 +1182,19 @@ describe "TextEditorPresenter", ->
describe ".tiles", ->
lineStateForScreenRow = (presenter, row) ->
lineId = presenter.model.tokenizedLineForScreenRow(row).id
tileRow = presenter.tileForRow(row)
getState(presenter).content.tiles[tileRow]?.lines[lineId]
tilesState = getState(presenter).content.tiles
lineId = presenter.linesByScreenRow.get(row)?.id
tilesState[presenter.tileForRow(row)]?.lines[lineId]
tagsForCodes = (presenter, tagCodes) ->
openTags = []
closeTags = []
for tagCode in tagCodes when tagCode < 0 # skip text codes
if presenter.isOpenTagCode(tagCode)
openTags.push(presenter.tagForCode(tagCode))
else
closeTags.push(presenter.tagForCode(tagCode))
{openTags, closeTags}
tiledContentContract (presenter) -> getState(presenter).content
@@ -1241,73 +1204,12 @@ describe "TextEditorPresenter", ->
presenter.setExplicitHeight(3)
expect(lineStateForScreenRow(presenter, 2)).toBeUndefined()
line3 = editor.tokenizedLineForScreenRow(3)
expectValues lineStateForScreenRow(presenter, 3), {
screenRow: 3
text: line3.text
tags: line3.tags
specialTokens: line3.specialTokens
firstNonWhitespaceIndex: line3.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line3.firstTrailingWhitespaceIndex
invisibles: line3.invisibles
}
line4 = editor.tokenizedLineForScreenRow(4)
expectValues lineStateForScreenRow(presenter, 4), {
screenRow: 4
text: line4.text
tags: line4.tags
specialTokens: line4.specialTokens
firstNonWhitespaceIndex: line4.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line4.firstTrailingWhitespaceIndex
invisibles: line4.invisibles
}
line5 = editor.tokenizedLineForScreenRow(5)
expectValues lineStateForScreenRow(presenter, 5), {
screenRow: 5
text: line5.text
tags: line5.tags
specialTokens: line5.specialTokens
firstNonWhitespaceIndex: line5.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line5.firstTrailingWhitespaceIndex
invisibles: line5.invisibles
}
line6 = editor.tokenizedLineForScreenRow(6)
expectValues lineStateForScreenRow(presenter, 6), {
screenRow: 6
text: line6.text
tags: line6.tags
specialTokens: line6.specialTokens
firstNonWhitespaceIndex: line6.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line6.firstTrailingWhitespaceIndex
invisibles: line6.invisibles
}
line7 = editor.tokenizedLineForScreenRow(7)
expectValues lineStateForScreenRow(presenter, 7), {
screenRow: 7
text: line7.text
tags: line7.tags
specialTokens: line7.specialTokens
firstNonWhitespaceIndex: line7.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line7.firstTrailingWhitespaceIndex
invisibles: line7.invisibles
}
line8 = editor.tokenizedLineForScreenRow(8)
expectValues lineStateForScreenRow(presenter, 8), {
screenRow: 8
text: line8.text
tags: line8.tags
specialTokens: line8.specialTokens
firstNonWhitespaceIndex: line8.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line8.firstTrailingWhitespaceIndex
invisibles: line8.invisibles
}
expectValues lineStateForScreenRow(presenter, 3), {screenRow: 3, tagCodes: editor.screenLineForScreenRow(3).tagCodes}
expectValues lineStateForScreenRow(presenter, 4), {screenRow: 4, tagCodes: editor.screenLineForScreenRow(4).tagCodes}
expectValues lineStateForScreenRow(presenter, 5), {screenRow: 5, tagCodes: editor.screenLineForScreenRow(5).tagCodes}
expectValues lineStateForScreenRow(presenter, 6), {screenRow: 6, tagCodes: editor.screenLineForScreenRow(6).tagCodes}
expectValues lineStateForScreenRow(presenter, 7), {screenRow: 7, tagCodes: editor.screenLineForScreenRow(7).tagCodes}
expectValues lineStateForScreenRow(presenter, 8), {screenRow: 8, tagCodes: editor.screenLineForScreenRow(8).tagCodes}
expect(lineStateForScreenRow(presenter, 9)).toBeUndefined()
it "updates when the editor's content changes", ->
@@ -1315,34 +1217,20 @@ describe "TextEditorPresenter", ->
expectStateUpdate presenter, -> buffer.insert([2, 0], "hello\nworld\n")
line1 = editor.tokenizedLineForScreenRow(1)
expectValues lineStateForScreenRow(presenter, 1), {
text: line1.text
tags: line1.tags
}
line2 = editor.tokenizedLineForScreenRow(2)
expectValues lineStateForScreenRow(presenter, 2), {
text: line2.text
tags: line2.tags
}
line3 = editor.tokenizedLineForScreenRow(3)
expectValues lineStateForScreenRow(presenter, 3), {
text: line3.text
tags: line3.tags
}
expectValues lineStateForScreenRow(presenter, 1), {screenRow: 1, tagCodes: editor.screenLineForScreenRow(1).tagCodes}
expectValues lineStateForScreenRow(presenter, 2), {screenRow: 2, tagCodes: editor.screenLineForScreenRow(2).tagCodes}
expectValues lineStateForScreenRow(presenter, 3), {screenRow: 3, tagCodes: editor.screenLineForScreenRow(3).tagCodes}
it "includes the .endOfLineInvisibles if the editor.showInvisibles config option is true", ->
editor.setText("hello\nworld\r\n")
presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10)
expect(lineStateForScreenRow(presenter, 0).endOfLineInvisibles).toBeNull()
expect(lineStateForScreenRow(presenter, 1).endOfLineInvisibles).toBeNull()
expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 0).tagCodes).openTags).not.toContain('invisible-character eol')
expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 1).tagCodes).openTags).not.toContain('invisible-character eol')
atom.config.set('editor.showInvisibles', true)
presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10)
expect(lineStateForScreenRow(presenter, 0).endOfLineInvisibles).toEqual [atom.config.get('editor.invisibles.eol')]
expect(lineStateForScreenRow(presenter, 1).endOfLineInvisibles).toEqual [atom.config.get('editor.invisibles.cr'), atom.config.get('editor.invisibles.eol')]
expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 0).tagCodes).openTags).toContain('invisible-character eol')
expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 1).tagCodes).openTags).toContain('invisible-character eol')
describe ".blockDecorations", ->
it "contains all block decorations that are present before/after a line, both initially and when decorations change", ->
@@ -2905,12 +2793,9 @@ describe "TextEditorPresenter", ->
describe ".content.tiles", ->
lineNumberStateForScreenRow = (presenter, screenRow) ->
editor = presenter.model
tileRow = presenter.tileForRow(screenRow)
line = editor.tokenizedLineForScreenRow(screenRow)
gutterState = getLineNumberGutterState(presenter)
gutterState.content.tiles[tileRow]?.lineNumbers[line?.id]
tilesState = getLineNumberGutterState(presenter).content.tiles
line = presenter.linesByScreenRow.get(screenRow)
tilesState[presenter.tileForRow(screenRow)]?.lineNumbers[line?.id]
tiledContentContract (presenter) -> getLineNumberGutterState(presenter).content
@@ -2919,17 +2804,21 @@ describe "TextEditorPresenter", ->
editor.foldBufferRow(4)
editor.setSoftWrapped(true)
editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(50)
presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineHeight: 10, tileSize: 2)
editor.setEditorWidthInChars(51)
presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineHeight: 10, tileSize: 3)
presenter.setScreenRowsToMeasure([9, 11])
expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined()
expectValues lineNumberStateForScreenRow(presenter, 2), {screenRow: 2, bufferRow: 2, softWrapped: false}
expect(lineNumberStateForScreenRow(presenter, 2)).toBeUndefined()
expectValues lineNumberStateForScreenRow(presenter, 3), {screenRow: 3, bufferRow: 3, softWrapped: false}
expectValues lineNumberStateForScreenRow(presenter, 4), {screenRow: 4, bufferRow: 3, softWrapped: true}
expectValues lineNumberStateForScreenRow(presenter, 5), {screenRow: 5, bufferRow: 4, softWrapped: false}
expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 7, softWrapped: false}
expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 8, softWrapped: false}
expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined()
expectValues lineNumberStateForScreenRow(presenter, 8), {screenRow: 8, bufferRow: 8, softWrapped: true}
expect(lineNumberStateForScreenRow(presenter, 9)).toBeUndefined()
expect(lineNumberStateForScreenRow(presenter, 10)).toBeUndefined()
expect(lineNumberStateForScreenRow(presenter, 11)).toBeUndefined()
expect(lineNumberStateForScreenRow(presenter, 12)).toBeUndefined()
it "updates when the editor's content changes", ->
editor.foldBufferRow(4)
@@ -3184,6 +3073,29 @@ describe "TextEditorPresenter", ->
expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a'
expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toContain 'a'
describe "when a fold spans a single soft-wrapped buffer row", ->
it "applies the 'folded' decoration only to its initial screen row", ->
editor.setSoftWrapped(true)
editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(20)
editor.foldBufferRange([[0, 20], [0, 22]])
editor.foldBufferRange([[0, 10], [0, 14]])
presenter = buildPresenter(explicitHeight: 35, scrollTop: 0, tileSize: 2)
expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain('folded')
expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull()
describe "when a fold is at the end of a soft-wrapped buffer row", ->
it "applies the 'folded' decoration only to its initial screen row", ->
editor.setSoftWrapped(true)
editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(25)
editor.foldBufferRow(1)
presenter = buildPresenter(explicitHeight: 35, scrollTop: 0, tileSize: 2)
expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toContain('folded')
expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
describe ".foldable", ->
it "marks line numbers at the start of a foldable region as foldable", ->
presenter = buildPresenter()

View File

@@ -10,6 +10,7 @@ describe "TextEditorRegistry", ->
it "gets added to the list of registered editors", ->
editor = {}
registry.add(editor)
expect(editor.registered).toBe true
expect(registry.editors.size).toBe 1
expect(registry.editors.has(editor)).toBe(true)
@@ -19,6 +20,16 @@ describe "TextEditorRegistry", ->
expect(registry.editors.size).toBe 1
disposable.dispose()
expect(registry.editors.size).toBe 0
expect(editor.registered).toBe false
it "can be removed", ->
editor = {}
registry.add(editor)
expect(registry.editors.size).toBe 1
success = registry.remove(editor)
expect(success).toBe true
expect(registry.editors.size).toBe 0
expect(editor.registered).toBe false
describe "when the registry is observed", ->
it "calls the callback for current and future editors until unsubscribed", ->

View File

@@ -39,21 +39,19 @@ describe "TextEditor", ->
it "preserves the invisibles setting", ->
atom.config.set('editor.showInvisibles', true)
previousInvisibles = editor.tokenizedLineForScreenRow(0).invisibles
previousLineText = editor.lineTextForScreenRow(0)
editor2 = TextEditor.deserialize(editor.serialize(), atom)
expect(previousInvisibles).toBeDefined()
expect(editor2.displayBuffer.tokenizedLineForScreenRow(0).invisibles).toEqual previousInvisibles
expect(editor2.lineTextForScreenRow(0)).toBe(previousLineText)
it "updates invisibles if the settings have changed between serialization and deserialization", ->
atom.config.set('editor.showInvisibles', true)
previousLineText = editor.lineTextForScreenRow(0)
state = editor.serialize()
atom.config.set('editor.invisibles', eol: '?')
editor2 = TextEditor.deserialize(state, atom)
expect(editor.tokenizedLineForScreenRow(0).invisibles.eol).toBe '?'
expect(editor2.lineTextForScreenRow(0)).not.toBe(previousLineText)
expect(editor2.lineTextForScreenRow(0).endsWith('?')).toBe(true)
describe "when the editor is constructed with the largeFileMode option set to true", ->
it "loads the editor but doesn't tokenize", ->
@@ -64,15 +62,14 @@ describe "TextEditor", ->
runs ->
buffer = editor.getBuffer()
expect(editor.tokenizedLineForScreenRow(0).text).toBe buffer.lineForRow(0)
expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBe 1
expect(editor.tokenizedLineForScreenRow(1).tokens.length).toBe 2 # soft tab
expect(editor.tokenizedLineForScreenRow(12).text).toBe buffer.lineForRow(12)
expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBe 1
expect(editor.lineTextForScreenRow(0)).toBe buffer.lineForRow(0)
expect(editor.tokensForScreenRow(0).length).toBe 1
expect(editor.tokensForScreenRow(1).length).toBe 2 # soft tab
expect(editor.lineTextForScreenRow(12)).toBe buffer.lineForRow(12)
expect(editor.getCursorScreenPosition()).toEqual [0, 0]
editor.insertText('hey"')
expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBe 1
expect(editor.tokenizedLineForScreenRow(1).tokens.length).toBe 2 # sof tab
expect(editor.tokensForScreenRow(0).length).toBe 1
expect(editor.tokensForScreenRow(1).length).toBe 2 # soft tab
describe ".copy()", ->
it "returns a different edit session with the same initial state", ->
@@ -182,17 +179,19 @@ describe "TextEditor", ->
expect(editor1.getLongTitle()).toBe "readme \u2014 sample-theme-1"
expect(editor2.getLongTitle()).toBe "readme \u2014 sample-theme-2"
it "returns '<filename> — <parent-directories>' when opened files have identical file and dir names", ->
it "returns '<filename> — <parent-directories>' when opened files have identical file names in subdirectories", ->
editor1 = null
editor2 = null
path1 = path.join('sample-theme-1', 'src', 'js')
path2 = path.join('sample-theme-2', 'src', 'js')
waitsForPromise ->
atom.workspace.open(path.join('sample-theme-1', 'src', 'js', 'main.js')).then (o) ->
atom.workspace.open(path.join(path1, 'main.js')).then (o) ->
editor1 = o
atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'main.js')).then (o) ->
atom.workspace.open(path.join(path2, 'main.js')).then (o) ->
editor2 = o
runs ->
expect(editor1.getLongTitle()).toBe "main.js \u2014 sample-theme-1/src/js"
expect(editor2.getLongTitle()).toBe "main.js \u2014 sample-theme-2/src/js"
expect(editor1.getLongTitle()).toBe "main.js \u2014 #{path1}"
expect(editor2.getLongTitle()).toBe "main.js \u2014 #{path2}"
it "returns '<filename> — <parent-directories>' when opened files have identical file and same parent dir name", ->
editor1 = null
@@ -204,7 +203,7 @@ describe "TextEditor", ->
editor2 = o
runs ->
expect(editor1.getLongTitle()).toBe "main.js \u2014 js"
expect(editor2.getLongTitle()).toBe "main.js \u2014 js/plugin"
expect(editor2.getLongTitle()).toBe "main.js \u2014 " + path.join('js', 'plugin')
it "notifies ::onDidChangeTitle observers when the underlying buffer path changes", ->
observed = []
@@ -312,7 +311,7 @@ describe "TextEditor", ->
editor.setSoftWrapped(true)
editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(50)
editor.createFold(2, 3)
editor.foldBufferRowRange(2, 3)
it "positions the cursor at the buffer position that corresponds to the given screen position", ->
editor.setCursorScreenPosition([9, 0])
@@ -493,7 +492,7 @@ describe "TextEditor", ->
it "wraps to the end of the previous line", ->
editor.setCursorScreenPosition([4, 4])
editor.moveLeft()
expect(editor.getCursorScreenPosition()).toEqual [3, 50]
expect(editor.getCursorScreenPosition()).toEqual [3, 46]
describe "when the cursor is on the first line", ->
it "remains in the same position (0,0)", ->
@@ -681,7 +680,7 @@ describe "TextEditor", ->
editor.setCursorScreenPosition([0, 2])
editor.moveToEndOfLine()
cursor = editor.getLastCursor()
expect(cursor.getScreenPosition()).toEqual [3, 4]
expect(cursor.getScreenPosition()).toEqual [4, 4]
describe ".moveToFirstCharacterOfLine()", ->
describe "when soft wrap is on", ->
@@ -1187,14 +1186,10 @@ describe "TextEditor", ->
cursor2 = editor.addCursorAtBufferPosition([1, 4])
expect(cursor2.marker).toBe cursor1.marker
describe '.logCursorScope()', ->
beforeEach ->
spyOn(atom.notifications, 'addInfo')
it 'opens a notification', ->
editor.logCursorScope()
expect(atom.notifications.addInfo).toHaveBeenCalled()
describe '.getCursorScope()', ->
it 'returns the current scope', ->
descriptor = editor.getCursorScope()
expect(descriptor.scopes).toContain('source.js')
describe "selection", ->
selection = null
@@ -1800,22 +1795,22 @@ describe "TextEditor", ->
describe "when the 'preserveFolds' option is false (the default)", ->
it "removes folds that contain the selections", ->
editor.setSelectedBufferRange([[0, 0], [0, 0]])
editor.createFold(1, 4)
editor.createFold(2, 3)
editor.createFold(6, 8)
editor.createFold(10, 11)
editor.foldBufferRowRange(1, 4)
editor.foldBufferRowRange(2, 3)
editor.foldBufferRowRange(6, 8)
editor.foldBufferRowRange(10, 11)
editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 6], [7, 7]]])
expect(editor.tokenizedLineForScreenRow(1).fold).toBeUndefined()
expect(editor.tokenizedLineForScreenRow(2).fold).toBeUndefined()
expect(editor.tokenizedLineForScreenRow(6).fold).toBeUndefined()
expect(editor.tokenizedLineForScreenRow(10).fold).toBeDefined()
expect(editor.isFoldedAtScreenRow(1)).toBeFalsy()
expect(editor.isFoldedAtScreenRow(2)).toBeFalsy()
expect(editor.isFoldedAtScreenRow(6)).toBeFalsy()
expect(editor.isFoldedAtScreenRow(10)).toBeTruthy()
describe "when the 'preserveFolds' option is true", ->
it "does not remove folds that contain the selections", ->
editor.setSelectedBufferRange([[0, 0], [0, 0]])
editor.createFold(1, 4)
editor.createFold(6, 8)
editor.foldBufferRowRange(1, 4)
editor.foldBufferRowRange(6, 8)
editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 0], [6, 1]]], preserveFolds: true)
expect(editor.isFoldedAtBufferRow(1)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
@@ -2227,7 +2222,7 @@ describe "TextEditor", ->
it "moves the line to the previous row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRange([[4, 2], [4, 9]], preserveFolds: true)
expect(editor.getSelectedBufferRange()).toEqual [[4, 2], [4, 9]]
@@ -2255,7 +2250,7 @@ describe "TextEditor", ->
expect(editor.lineTextForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));"
expect(editor.lineTextForBufferRow(9)).toBe " };"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
@@ -2293,7 +2288,7 @@ describe "TextEditor", ->
it "moves the lines to the previous row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRange([[3, 2], [4, 9]], preserveFolds: true)
expect(editor.isFoldedAtBufferRow(3)).toBeFalsy()
@@ -2321,7 +2316,7 @@ describe "TextEditor", ->
it "moves the lines to the previous row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRange([[4, 2], [8, 9]], preserveFolds: true)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
@@ -2365,7 +2360,7 @@ describe "TextEditor", ->
expect(editor.lineTextForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));"
expect(editor.lineTextForBufferRow(9)).toBe " };"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
@@ -2405,7 +2400,7 @@ describe "TextEditor", ->
it "moves the lines to the previous row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRanges([
[[2, 2], [2, 9]],
[[4, 2], [4, 9]]
@@ -2443,7 +2438,7 @@ describe "TextEditor", ->
describe "when there is a fold", ->
it "moves all lines that spanned by a selection to preceding row, preserving all folds", ->
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
@@ -2470,8 +2465,8 @@ describe "TextEditor", ->
describe 'and many selections intersects folded rows', ->
it 'moves and preserves all the folds', ->
editor.createFold(2, 4)
editor.createFold(7, 9)
editor.foldBufferRowRange(2, 4)
editor.foldBufferRowRange(7, 9)
editor.setSelectedBufferRanges([
[[1, 0], [5, 4]],
@@ -2555,7 +2550,7 @@ describe "TextEditor", ->
it "moves the line to the following row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRange([[4, 2], [4, 9]], preserveFolds: true)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
@@ -2581,7 +2576,7 @@ describe "TextEditor", ->
expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];"
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
@@ -2635,7 +2630,7 @@ describe "TextEditor", ->
it "moves the lines to the following row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRange([[3, 2], [4, 9]], preserveFolds: true)
expect(editor.isFoldedAtBufferRow(3)).toBeFalsy()
@@ -2663,7 +2658,7 @@ describe "TextEditor", ->
it "moves the lines to the following row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRange([[4, 2], [8, 9]], preserveFolds: true)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
@@ -2693,7 +2688,7 @@ describe "TextEditor", ->
expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;"
expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
@@ -2735,8 +2730,8 @@ describe "TextEditor", ->
describe 'and many selections intersects folded rows', ->
it 'moves and preserves all the folds', ->
editor.createFold(2, 4)
editor.createFold(7, 9)
editor.foldBufferRowRange(2, 4)
editor.foldBufferRowRange(7, 9)
editor.setSelectedBufferRanges([
[[2, 0], [2, 4]],
@@ -2765,7 +2760,7 @@ describe "TextEditor", ->
describe "when there is a fold below one of the selected row", ->
it "moves all lines spanned by a selection to the following row, preserving the fold", ->
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
@@ -2788,7 +2783,7 @@ describe "TextEditor", ->
describe "when there is a fold below a group of multiple selections without any lines with no selection in-between", ->
it "moves all the lines below the fold, preserving the fold", ->
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
@@ -2813,7 +2808,7 @@ describe "TextEditor", ->
it "moves the lines to the previous row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
editor.createFold(4, 7)
editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRanges([
[[2, 2], [2, 9]],
[[4, 2], [4, 9]]
@@ -2880,6 +2875,13 @@ describe "TextEditor", ->
expect(editor.lineTextForBufferRow(1)).toBe "1"
expect(editor.lineTextForBufferRow(2)).toBe "2"
describe "when the line is the last buffer row", ->
it "doesn't move it", ->
editor.setText("abc\ndef")
editor.setCursorBufferPosition([1, 0])
editor.moveLineDown()
expect(editor.getText()).toBe("abc\ndef")
describe ".insertText(text)", ->
describe "when there is a single selection", ->
beforeEach ->
@@ -2951,10 +2953,10 @@ describe "TextEditor", ->
describe "when there is a selection that ends on a folded line", ->
it "destroys the selection", ->
editor.createFold(2, 4)
editor.foldBufferRowRange(2, 4)
editor.setSelectedBufferRange([[1, 0], [2, 0]])
editor.insertText('holy cow')
expect(editor.tokenizedLineForScreenRow(2).fold).toBeUndefined()
expect(editor.isFoldedAtScreenRow(2)).toBeFalsy()
describe "when there are ::onWillInsertText and ::onDidInsertText observers", ->
beforeEach ->
@@ -3176,7 +3178,7 @@ describe "TextEditor", ->
expect(editor.indentationForBufferRow(0)).toBe 1
expect(editor.indentationForBufferRow(1)).toBe 1
it "indents the new line to the correct level when editor.autoIndent is true and using a off-side rule language", ->
it "indents the new line to the correct level when editor.autoIndent is true and using an off-side rule language", ->
waitsForPromise ->
atom.packages.activatePackage('language-coffee-script')
@@ -3248,15 +3250,14 @@ describe "TextEditor", ->
editor.setCursorScreenPosition(row: 0, column: 0)
editor.backspace()
describe "when the cursor is on the first column of a line below a fold", ->
it "deletes the folded lines", ->
editor.setCursorScreenPosition([4, 0])
editor.foldCurrentRow()
editor.setCursorScreenPosition([5, 0])
describe "when the cursor is after a fold", ->
it "deletes the folded range", ->
editor.foldBufferRange([[4, 7], [5, 8]])
editor.setCursorBufferPosition([5, 8])
editor.backspace()
expect(buffer.lineForRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));"
expect(buffer.lineForRow(4).fold).toBeUndefined()
expect(buffer.lineForRow(4)).toBe " whirrent = items.shift();"
expect(editor.isFoldedAtBufferRow(4)).toBe(false)
describe "when the cursor is in the middle of a line below a fold", ->
it "backspaces as normal", ->
@@ -3269,14 +3270,13 @@ describe "TextEditor", ->
expect(buffer.lineForRow(8)).toBe " eturn sort(left).concat(pivot).concat(sort(right));"
describe "when the cursor is on a folded screen line", ->
it "deletes all of the folded lines along with the fold", ->
it "deletes the contents of the fold before the cursor", ->
editor.setCursorBufferPosition([3, 0])
editor.foldCurrentRow()
editor.backspace()
expect(buffer.lineForRow(1)).toBe ""
expect(buffer.lineForRow(2)).toBe " return sort(Array.apply(this, arguments));"
expect(editor.getCursorScreenPosition()).toEqual [1, 0]
expect(buffer.lineForRow(1)).toBe " var sort = function(items) var pivot = items.shift(), current, left = [], right = [];"
expect(editor.getCursorScreenPosition()).toEqual [1, 29]
describe "when there are multiple cursors", ->
describe "when cursors are on the same line", ->
@@ -3343,7 +3343,7 @@ describe "TextEditor", ->
editor.backspace()
expect(buffer.lineForRow(3)).toBe " while(items.length > 0) {"
expect(editor.tokenizedLineForScreenRow(3).fold).toBeDefined()
expect(editor.isFoldedAtScreenRow(3)).toBe(true)
describe "when there are multiple selections", ->
it "removes all selected text", ->
@@ -3516,16 +3516,16 @@ describe "TextEditor", ->
editor.delete()
expect(buffer.lineForRow(12)).toBe '};'
describe "when the cursor is on the end of a line above a fold", ->
describe "when the cursor is before a fold", ->
it "only deletes the lines inside the fold", ->
editor.foldBufferRow(4)
editor.setCursorScreenPosition([3, Infinity])
editor.foldBufferRange([[3, 6], [4, 8]])
editor.setCursorScreenPosition([3, 6])
cursorPositionBefore = editor.getCursorScreenPosition()
editor.delete()
expect(buffer.lineForRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];"
expect(buffer.lineForRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));"
expect(buffer.lineForRow(3)).toBe " vae(items.length > 0) {"
expect(buffer.lineForRow(4)).toBe " current = items.shift();"
expect(editor.getCursorScreenPosition()).toEqual cursorPositionBefore
describe "when the cursor is in the middle a line above a fold", ->
@@ -3537,20 +3537,21 @@ describe "TextEditor", ->
editor.delete()
expect(buffer.lineForRow(3)).toBe " ar pivot = items.shift(), current, left = [], right = [];"
expect(editor.tokenizedLineForScreenRow(4).fold).toBeDefined()
expect(editor.isFoldedAtScreenRow(4)).toBe(true)
expect(editor.getCursorScreenPosition()).toEqual [3, 4]
describe "when the cursor is on a folded line", ->
it "removes the lines contained by the fold", ->
editor.setSelectedBufferRange([[2, 0], [2, 0]])
editor.createFold(2, 4)
editor.createFold(2, 6)
oldLine7 = buffer.lineForRow(7)
oldLine8 = buffer.lineForRow(8)
describe "when the cursor is inside a fold", ->
it "removes the folded content after the cursor", ->
editor.foldBufferRange([[2, 6], [6, 21]])
editor.setCursorBufferPosition([4, 9])
editor.delete()
expect(editor.tokenizedLineForScreenRow(2).text).toBe oldLine7
expect(editor.tokenizedLineForScreenRow(3).text).toBe oldLine8
expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;'
expect(buffer.lineForRow(3)).toBe ' var pivot = items.shift(), current, left = [], right = [];'
expect(buffer.lineForRow(4)).toBe ' while ? left.push(current) : right.push(current);'
expect(buffer.lineForRow(5)).toBe ' }'
expect(editor.getCursorBufferPosition()).toEqual [4, 9]
describe "when there are multiple cursors", ->
describe "when cursors are on the same line", ->
@@ -3807,10 +3808,10 @@ describe "TextEditor", ->
it "cuts up to the end of the line", ->
editor.setSoftWrapped(true)
editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(10)
editor.setCursorScreenPosition([2, 2])
editor.setEditorWidthInChars(25)
editor.setCursorScreenPosition([2, 6])
editor.cutToEndOfLine()
expect(editor.tokenizedLineForScreenRow(2).text).toBe '= () {'
expect(editor.lineTextForScreenRow(2)).toBe ' var function(items) {'
describe "when soft wrap is off", ->
describe "when nothing is selected", ->
@@ -4561,11 +4562,142 @@ describe "TextEditor", ->
expect(cursor1.getBufferPosition()).toEqual [0, 0]
expect(cursor3.getBufferPosition()).toEqual [1, 2]
describe ".moveSelectionLeft()", ->
it "moves one active selection on one line one column to the left", ->
editor.setSelectedBufferRange [[0, 4], [0, 13]]
expect(editor.getSelectedText()).toBe 'quicksort'
editor.moveSelectionLeft()
expect(editor.getSelectedText()).toBe 'quicksort'
expect(editor.getSelectedBufferRange()).toEqual [[0, 3], [0, 12]]
it "moves multiple active selections on one line one column to the left", ->
editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]])
selections = editor.getSelections()
expect(selections[0].getText()).toBe 'quicksort'
expect(selections[1].getText()).toBe 'function'
editor.moveSelectionLeft()
expect(selections[0].getText()).toBe 'quicksort'
expect(selections[1].getText()).toBe 'function'
expect(editor.getSelectedBufferRanges()).toEqual [[[0, 3], [0, 12]], [[0, 15], [0, 23]]]
it "moves multiple active selections on multiple lines one column to the left", ->
editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]])
selections = editor.getSelections()
expect(selections[0].getText()).toBe 'quicksort'
expect(selections[1].getText()).toBe 'sort'
editor.moveSelectionLeft()
expect(selections[0].getText()).toBe 'quicksort'
expect(selections[1].getText()).toBe 'sort'
expect(editor.getSelectedBufferRanges()).toEqual [[[0, 3], [0, 12]], [[1, 5], [1, 9]]]
describe "when a selection is at the first column of a line", ->
it "does not change the selection", ->
editor.setSelectedBufferRanges([[[0, 0], [0, 3]], [[1, 0], [1, 3]]])
selections = editor.getSelections()
expect(selections[0].getText()).toBe 'var'
expect(selections[1].getText()).toBe ' v'
editor.moveSelectionLeft()
editor.moveSelectionLeft()
expect(selections[0].getText()).toBe 'var'
expect(selections[1].getText()).toBe ' v'
expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 3]], [[1, 0], [1, 3]]]
describe "when multiple selections are active on one line", ->
it "does not change the selection", ->
editor.setSelectedBufferRanges([[[0, 0], [0, 3]], [[0, 4], [0, 13]]])
selections = editor.getSelections()
expect(selections[0].getText()).toBe 'var'
expect(selections[1].getText()).toBe 'quicksort'
editor.moveSelectionLeft()
expect(selections[0].getText()).toBe 'var'
expect(selections[1].getText()).toBe 'quicksort'
expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 3]], [[0, 4], [0, 13]]]
describe ".moveSelectionRight()", ->
it "moves one active selection on one line one column to the right", ->
editor.setSelectedBufferRange [[0, 4], [0, 13]]
expect(editor.getSelectedText()).toBe 'quicksort'
editor.moveSelectionRight()
expect(editor.getSelectedText()).toBe 'quicksort'
expect(editor.getSelectedBufferRange()).toEqual [[0, 5], [0, 14]]
it "moves multiple active selections on one line one column to the right", ->
editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]])
selections = editor.getSelections()
expect(selections[0].getText()).toBe 'quicksort'
expect(selections[1].getText()).toBe 'function'
editor.moveSelectionRight()
expect(selections[0].getText()).toBe 'quicksort'
expect(selections[1].getText()).toBe 'function'
expect(editor.getSelectedBufferRanges()).toEqual [[[0, 5], [0, 14]], [[0, 17], [0, 25]]]
it "moves multiple active selections on multiple lines one column to the right", ->
editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]])
selections = editor.getSelections()
expect(selections[0].getText()).toBe 'quicksort'
expect(selections[1].getText()).toBe 'sort'
editor.moveSelectionRight()
expect(selections[0].getText()).toBe 'quicksort'
expect(selections[1].getText()).toBe 'sort'
expect(editor.getSelectedBufferRanges()).toEqual [[[0, 5], [0, 14]], [[1, 7], [1, 11]]]
describe "when a selection is at the last column of a line", ->
it "does not change the selection", ->
editor.setSelectedBufferRanges([[[2, 34], [2, 40]], [[5, 22], [5, 30]]])
selections = editor.getSelections()
expect(selections[0].getText()).toBe 'items;'
expect(selections[1].getText()).toBe 'shift();'
editor.moveSelectionRight()
editor.moveSelectionRight()
expect(selections[0].getText()).toBe 'items;'
expect(selections[1].getText()).toBe 'shift();'
expect(editor.getSelectedBufferRanges()).toEqual [[[2, 34], [2, 40]], [[5, 22], [5, 30]]]
describe "when multiple selections are active on one line", ->
it "does not change the selection", ->
editor.setSelectedBufferRanges([[[2, 27], [2, 33]], [[2, 34], [2, 40]]])
selections = editor.getSelections()
expect(selections[0].getText()).toBe 'return'
expect(selections[1].getText()).toBe 'items;'
editor.moveSelectionRight()
expect(selections[0].getText()).toBe 'return'
expect(selections[1].getText()).toBe 'items;'
expect(editor.getSelectedBufferRanges()).toEqual [[[2, 27], [2, 33]], [[2, 34], [2, 40]]]
describe 'reading text', ->
it '.lineTextForScreenRow(row)', ->
editor.foldBufferRow(4)
expect(editor.lineTextForScreenRow(5)).toEqual ' return sort(left).concat(pivot).concat(sort(right));'
expect(editor.lineTextForScreenRow(100)).not.toBeDefined()
expect(editor.lineTextForScreenRow(9)).toEqual '};'
expect(editor.lineTextForScreenRow(10)).toBeUndefined()
describe ".deleteLine()", ->
it "deletes the first line when the cursor is there", ->
@@ -4922,11 +5054,13 @@ describe "TextEditor", ->
it 'retokenizes when the tab length is updated via .setTabLength()', ->
expect(editor.getTabLength()).toBe 2
expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 2
leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace'
expect(leadingWhitespaceTokens.length).toBe(3)
editor.setTabLength(6)
expect(editor.getTabLength()).toBe 6
expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 6
leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace'
expect(leadingWhitespaceTokens.length).toBe(1)
changeHandler = jasmine.createSpy('changeHandler')
editor.onDidChange(changeHandler)
@@ -4935,21 +5069,25 @@ describe "TextEditor", ->
it 'retokenizes when the editor.tabLength setting is updated', ->
expect(editor.getTabLength()).toBe 2
expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 2
leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace'
expect(leadingWhitespaceTokens.length).toBe(3)
atom.config.set 'editor.tabLength', 6, scopeSelector: '.source.js'
expect(editor.getTabLength()).toBe 6
expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 6
leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace'
expect(leadingWhitespaceTokens.length).toBe(1)
it 'updates the tab length when the grammar changes', ->
atom.config.set 'editor.tabLength', 6, scopeSelector: '.source.coffee'
expect(editor.getTabLength()).toBe 2
expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 2
leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace'
expect(leadingWhitespaceTokens.length).toBe(3)
editor.setGrammar(coffeeEditor.getGrammar())
expect(editor.getTabLength()).toBe 6
expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 6
leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace'
expect(leadingWhitespaceTokens.length).toBe(1)
describe ".indentLevelForLine(line)", ->
it "returns the indent level when the line has only leading whitespace", ->
@@ -4985,11 +5123,11 @@ describe "TextEditor", ->
runs ->
expect(editor.getGrammar()).toBe atom.grammars.nullGrammar
expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBe 1
expect(editor.tokensForScreenRow(0).length).toBe(1)
atom.grammars.addGrammar(jsGrammar)
expect(editor.getGrammar()).toBe jsGrammar
expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBeGreaterThan 1
expect(editor.tokensForScreenRow(0).length).toBeGreaterThan 1
describe "editor.autoIndent", ->
describe "when editor.autoIndent is false (default)", ->
@@ -5025,7 +5163,7 @@ describe "TextEditor", ->
expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1
describe "when the line preceding the newline does't add a level of indentation", ->
it "indents the new line to the same level a as the preceding line", ->
it "indents the new line to the same level as the preceding line", ->
editor.setCursorBufferPosition([5, 14])
editor.insertText('\n')
expect(editor.indentationForBufferRow(6)).toBe editor.indentationForBufferRow(5)
@@ -5130,10 +5268,34 @@ describe "TextEditor", ->
coffeeEditor.insertText("\n")
expect(coffeeEditor.lineTextForBufferRow(2)).toBe ""
describe "editor.atomicSoftTabs", ->
it "skips tab-length runs of leading whitespace when moving the cursor", ->
atom.config.set('editor.tabLength', 4)
atom.config.set('editor.atomicSoftTabs', true)
editor.setCursorScreenPosition([2, 3])
expect(editor.getCursorScreenPosition()).toEqual [2, 4]
atom.config.set('editor.atomicSoftTabs', false)
editor.setCursorScreenPosition([2, 3])
expect(editor.getCursorScreenPosition()).toEqual [2, 3]
atom.config.set('editor.atomicSoftTabs', true)
editor.setCursorScreenPosition([2, 3])
expect(editor.getCursorScreenPosition()).toEqual [2, 4]
atom.config.set('editor.atomicSoftTabs', false, scopeSelector: '.source.foo')
editor.setCursorScreenPosition([2, 3])
expect(editor.getCursorScreenPosition()).toEqual [2, 4]
atom.config.set('editor.atomicSoftTabs', false, scopeSelector: '.source.js')
editor.setCursorScreenPosition([2, 3])
expect(editor.getCursorScreenPosition()).toEqual [2, 3]
describe ".destroy()", ->
it "destroys marker layers associated with the text editor", ->
selectionsMarkerLayerId = editor.selectionsMarkerLayer.id
foldsMarkerLayerId = editor.displayBuffer.foldsMarkerLayer.id
foldsMarkerLayerId = editor.displayLayer.foldsMarkerLayer.id
editor.destroy()
expect(buffer.getMarkerLayer(selectionsMarkerLayerId)).toBeUndefined()
expect(buffer.getMarkerLayer(foldsMarkerLayerId)).toBeUndefined()
@@ -5217,10 +5379,10 @@ describe "TextEditor", ->
expect(editor.getSelectedBufferRanges()).toEqual [[[3, 5], [3, 5]], [[9, 0], [14, 0]]]
# folds are also duplicated
expect(editor.tokenizedLineForScreenRow(5).fold).toBeDefined()
expect(editor.tokenizedLineForScreenRow(7).fold).toBeDefined()
expect(editor.tokenizedLineForScreenRow(7).text).toBe " while(items.length > 0) {"
expect(editor.tokenizedLineForScreenRow(8).text).toBe " return sort(left).concat(pivot).concat(sort(right));"
expect(editor.isFoldedAtScreenRow(5)).toBe(true)
expect(editor.isFoldedAtScreenRow(7)).toBe(true)
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", ->
editor.foldBufferRow(4)
@@ -5416,17 +5578,15 @@ describe "TextEditor", ->
runs ->
editor.setText("// http://github.com")
{tokens} = editor.tokenizedLineForScreenRow(0)
expect(tokens[1].value).toBe " http://github.com"
expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"]
tokens = editor.tokensForScreenRow(0)
expect(tokens).toEqual ['source.js', 'comment.line.double-slash.js', 'punctuation.definition.comment.js']
waitsForPromise ->
atom.packages.activatePackage('language-hyperlink')
runs ->
{tokens} = editor.tokenizedLineForScreenRow(0)
expect(tokens[2].value).toBe "http://github.com"
expect(tokens[2].scopes).toEqual ["source.js", "comment.line.double-slash.js", "markup.underline.link.http.hyperlink"]
tokens = editor.tokensForScreenRow(0)
expect(tokens).toEqual ['source.js', 'comment.line.double-slash.js', 'punctuation.definition.comment.js', 'markup.underline.link.http.hyperlink']
describe "when the grammar is updated", ->
it "retokenizes existing buffers that contain tokens that match the injection selector", ->
@@ -5436,25 +5596,22 @@ describe "TextEditor", ->
runs ->
editor.setText("// SELECT * FROM OCTOCATS")
{tokens} = editor.tokenizedLineForScreenRow(0)
expect(tokens[1].value).toBe " SELECT * FROM OCTOCATS"
expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"]
tokens = editor.tokensForScreenRow(0)
expect(tokens).toEqual ['source.js', 'comment.line.double-slash.js', 'punctuation.definition.comment.js']
waitsForPromise ->
atom.packages.activatePackage('package-with-injection-selector')
runs ->
{tokens} = editor.tokenizedLineForScreenRow(0)
expect(tokens[1].value).toBe " SELECT * FROM OCTOCATS"
expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"]
tokens = editor.tokensForScreenRow(0)
expect(tokens).toEqual ['source.js', 'comment.line.double-slash.js', 'punctuation.definition.comment.js']
waitsForPromise ->
atom.packages.activatePackage('language-sql')
runs ->
{tokens} = editor.tokenizedLineForScreenRow(0)
expect(tokens[2].value).toBe "SELECT"
expect(tokens[2].scopes).toEqual ["source.js", "comment.line.double-slash.js", "keyword.other.DML.sql"]
tokens = editor.tokensForScreenRow(0)
expect(tokens).toEqual ['source.js', 'comment.line.double-slash.js', 'punctuation.definition.comment.js', 'keyword.other.DML.sql', 'keyword.operator.star.sql', 'keyword.other.DML.sql']
describe ".normalizeTabsInBufferRange()", ->
it "normalizes tabs depending on the editor's soft tab/tab length settings", ->
@@ -5582,6 +5739,19 @@ describe "TextEditor", ->
expect(editor.getFirstVisibleScreenRow()).toEqual 89
expect(editor.getVisibleRowRange()).toEqual [89, 99]
describe "::scrollToScreenPosition(position, [options])", ->
it "triggers ::onDidRequestAutoscroll with the logical coordinates along with the options", ->
scrollSpy = jasmine.createSpy("::onDidRequestAutoscroll")
editor.onDidRequestAutoscroll(scrollSpy)
editor.scrollToScreenPosition([8, 20])
editor.scrollToScreenPosition([8, 20], center: true)
editor.scrollToScreenPosition([8, 20], center: false, reversed: true)
expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {})
expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: true})
expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: false, reversed: true})
describe '.get/setPlaceholderText()', ->
it 'can be created with placeholderText', ->
newEditor = atom.workspace.buildTextEditor(
@@ -5599,28 +5769,6 @@ describe "TextEditor", ->
expect(handler).toHaveBeenCalledWith 'OK'
expect(editor.getPlaceholderText()).toBe 'OK'
describe ".checkoutHeadRevision()", ->
it "reverts to the version of its file checked into the project repository", ->
atom.config.set("editor.confirmCheckoutHeadRevision", false)
editor.setCursorBufferPosition([0, 0])
editor.insertText("---\n")
expect(editor.lineTextForBufferRow(0)).toBe "---"
waitsForPromise ->
editor.checkoutHeadRevision()
runs ->
expect(editor.lineTextForBufferRow(0)).toBe "var quicksort = function () {"
describe "when there's no repository for the editor's file", ->
it "doesn't do anything", ->
editor = atom.workspace.buildTextEditor()
editor.setText("stuff")
editor.checkoutHeadRevision()
waitsForPromise -> editor.checkoutHeadRevision()
describe 'gutters', ->
describe 'the TextEditor constructor', ->
it 'creates a line-number gutter', ->
@@ -5749,6 +5897,7 @@ describe "TextEditor", ->
expect(editor.decorationsStateForScreenRowRange(0, 5)[decoration.id]).toEqual {
properties: {type: 'highlight', class: 'foo'}
screenRange: marker.getScreenRange(),
bufferRange: marker.getBufferRange(),
rangeIsReversed: false
}
@@ -5769,26 +5918,31 @@ describe "TextEditor", ->
expect(decorationState["#{layer1Decoration1.id}-#{marker1.id}"]).toEqual {
properties: {type: 'highlight', class: 'foo'},
screenRange: marker1.getRange(),
bufferRange: marker1.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer1Decoration1.id}-#{marker2.id}"]).toEqual {
properties: {type: 'highlight', class: 'foo'},
screenRange: marker2.getRange(),
bufferRange: marker2.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
properties: {type: 'highlight', class: 'bar'},
screenRange: marker1.getRange(),
bufferRange: marker1.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer1Decoration2.id}-#{marker2.id}"]).toEqual {
properties: {type: 'highlight', class: 'bar'},
screenRange: marker2.getRange(),
bufferRange: marker2.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer2Decoration.id}-#{marker3.id}"]).toEqual {
properties: {type: 'highlight', class: 'baz'},
screenRange: marker3.getRange(),
bufferRange: marker3.getRange(),
rangeIsReversed: false
}
@@ -5800,16 +5954,19 @@ describe "TextEditor", ->
expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
properties: {type: 'highlight', class: 'bar'},
screenRange: marker1.getRange(),
bufferRange: marker1.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer1Decoration2.id}-#{marker2.id}"]).toEqual {
properties: {type: 'highlight', class: 'bar'},
screenRange: marker2.getRange(),
bufferRange: marker2.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer2Decoration.id}-#{marker3.id}"]).toEqual {
properties: {type: 'highlight', class: 'baz'},
screenRange: marker3.getRange(),
bufferRange: marker3.getRange(),
rangeIsReversed: false
}
@@ -5818,6 +5975,7 @@ describe "TextEditor", ->
expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
properties: {type: 'highlight', class: 'quux'},
screenRange: marker1.getRange(),
bufferRange: marker1.getRange(),
rangeIsReversed: false
}
@@ -5826,6 +5984,7 @@ describe "TextEditor", ->
expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
properties: {type: 'highlight', class: 'bar'},
screenRange: marker1.getRange(),
bufferRange: marker1.getRange(),
rangeIsReversed: false
}
@@ -5837,8 +5996,21 @@ describe "TextEditor", ->
it "ignores invisibles even if editor.showInvisibles is true", ->
atom.config.set('editor.showInvisibles', true)
invisibles = editor.tokenizedLineForScreenRow(0).invisibles
expect(invisibles).toBe(null)
expect(editor.lineTextForScreenRow(0).indexOf(atom.config.get('editor.invisibles.eol'))).toBe(-1)
describe "indent guides", ->
it "shows indent guides when `editor.showIndentGuide` is set to true and the editor is not mini", ->
editor.setText(" foo")
atom.config.set('editor.tabLength', 2)
atom.config.set('editor.showIndentGuide', false)
expect(editor.tokensForScreenRow(0)).toEqual ['source.js', 'leading-whitespace']
atom.config.set('editor.showIndentGuide', true)
expect(editor.tokensForScreenRow(0)).toEqual ['source.js', 'leading-whitespace indent-guide']
editor.setMini(true)
expect(editor.tokensForScreenRow(0)).toEqual ['source.js', 'leading-whitespace']
describe "when the editor is constructed with the grammar option set", ->
beforeEach ->

View File

@@ -75,22 +75,23 @@ describe 'text utilities', ->
expect(textUtils.isKoreanCharacter("O")).toBe(false)
describe ".isCJKCharacter(character)", ->
it "returns true when the character is either a korean, half-width or double-width character", ->
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
expect(textUtils.isCJKCharacter("")).toBe(true)
describe ".isWrapBoundary(previousCharacter, character)", ->
it "returns true when the character is CJK or when the previous character is a space/tab", ->
anyCharacter = 'x'
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isWrapBoundary(anyCharacter, "")).toBe(true)
expect(textUtils.isDoubleWidthCharacter("a")).toBe(false)
expect(textUtils.isDoubleWidthCharacter("O")).toBe(false)
expect(textUtils.isDoubleWidthCharacter("z")).toBe(false)
expect(textUtils.isWrapBoundary(' ', 'h')).toBe(true)
expect(textUtils.isWrapBoundary('\t', 'h')).toBe(true)
expect(textUtils.isWrapBoundary('a', 'h')).toBe(false)

View File

@@ -175,7 +175,7 @@ describe "atom.themes", ->
expect(styleElementAddedHandler).toHaveBeenCalled()
element = document.querySelector('head style[source-path*="css.css"]')
expect(element.getAttribute('source-path')).toBe atom.themes.stringToId(cssPath)
expect(element.getAttribute('source-path')).toEqualPath atom.themes.stringToId(cssPath)
expect(element.textContent).toBe fs.readFileSync(cssPath, 'utf8')
# doesn't append twice
@@ -194,7 +194,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')).toBe atom.themes.stringToId(lessPath)
expect(element.getAttribute('source-path')).toEqualPath atom.themes.stringToId(lessPath)
expect(element.textContent).toBe """
#header {
color: #4d926f;
@@ -213,9 +213,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')).toBe atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('css.css'))
expect(document.querySelector('head style[source-path*="css.css"]').getAttribute('source-path')).toEqualPath atom.themes.stringToId(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')).toBe atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('sample.less'))
expect(document.querySelector('head style[source-path*="sample.less"]').getAttribute('source-path')).toEqualPath atom.themes.stringToId(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()

View File

@@ -0,0 +1,103 @@
/** @babel */
import TokenizedBufferIterator from '../src/tokenized-buffer-iterator'
import {Point} from 'text-buffer'
describe('TokenizedBufferIterator', () => {
it('reports two boundaries at the same position when tags close, open, then close again without a non-negative integer separating them (regression)', () => {
const tokenizedBuffer = {
tokenizedLineForRow () {
return {
tags: [-1, -2, -1, -2],
text: '',
openScopes: []
}
}
}
const grammarRegistry = {
scopeForId () {
return 'foo'
}
}
const iterator = new TokenizedBufferIterator(tokenizedBuffer, grammarRegistry)
iterator.seek(Point(0, 0))
expect(iterator.getPosition()).toEqual(Point(0, 0))
expect(iterator.getCloseTags()).toEqual([])
expect(iterator.getOpenTags()).toEqual(['foo'])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(0, 0))
expect(iterator.getCloseTags()).toEqual(['foo'])
expect(iterator.getOpenTags()).toEqual(['foo'])
iterator.moveToSuccessor()
expect(iterator.getCloseTags()).toEqual(['foo'])
expect(iterator.getOpenTags()).toEqual([])
})
it("reports a boundary at line end if the next line's open scopes don't match the containing tags for the current line", () => {
const tokenizedBuffer = {
tokenizedLineForRow (row) {
if (row === 0) {
return {
tags: [-1, 3, -2, -3],
text: 'bar',
openScopes: []
}
} else if (row === 1) {
return {
tags: [3],
text: 'baz',
openScopes: [-1]
}
} else if (row === 2) {
return {
tags: [-2],
text: '',
openScopes: [-1]
}
}
}
}
const grammarRegistry = {
scopeForId (id) {
if (id === -2 || id === -1) {
return 'foo'
} else if (id === -3) {
return 'qux'
}
}
}
const iterator = new TokenizedBufferIterator(tokenizedBuffer, grammarRegistry)
iterator.seek(Point(0, 0))
expect(iterator.getPosition()).toEqual(Point(0, 0))
expect(iterator.getCloseTags()).toEqual([])
expect(iterator.getOpenTags()).toEqual(['foo'])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(0, 3))
expect(iterator.getCloseTags()).toEqual(['foo'])
expect(iterator.getOpenTags()).toEqual(['qux'])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(0, 3))
expect(iterator.getCloseTags()).toEqual(['qux'])
expect(iterator.getOpenTags()).toEqual([])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(1, 0))
expect(iterator.getCloseTags()).toEqual([])
expect(iterator.getOpenTags()).toEqual(['foo'])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(2, 0))
expect(iterator.getCloseTags()).toEqual(['foo'])
expect(iterator.getOpenTags()).toEqual([])
})
})

View File

@@ -1,5 +1,5 @@
TokenizedBuffer = require '../src/tokenized-buffer'
TextBuffer = require 'text-buffer'
{Point} = TextBuffer = require 'text-buffer'
_ = require 'underscore-plus'
describe "TokenizedBuffer", ->
@@ -134,13 +134,10 @@ describe "TokenizedBuffer", ->
describe "on construction", ->
it "initially creates un-tokenized screen lines, then tokenizes lines chunk at a time in the background", ->
line0 = tokenizedBuffer.tokenizedLineForRow(0)
expect(line0.tokens.length).toBe 1
expect(line0.tokens[0]).toEqual(value: line0.text, scopes: ['source.js'])
expect(line0.tokens).toEqual([value: line0.text, scopes: ['source.js']])
line11 = tokenizedBuffer.tokenizedLineForRow(11)
expect(line11.tokens.length).toBe 2
expect(line11.tokens[0]).toEqual(value: " ", scopes: ['source.js'], isAtomic: true)
expect(line11.tokens[1]).toEqual(value: "return sort(Array.apply(this, arguments));", scopes: ['source.js'])
expect(line11.tokens).toEqual([value: " return sort(Array.apply(this, arguments));", scopes: ['source.js']])
# background tokenization has not begun
expect(tokenizedBuffer.tokenizedLineForRow(0).ruleStack).toBeUndefined()
@@ -236,7 +233,7 @@ describe "TokenizedBuffer", ->
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1]).toEqual(value: '(', scopes: ['source.js', 'meta.function-call.js', 'meta.arguments.js', 'punctuation.definition.arguments.begin.bracket.round.js'])
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[0]).toEqual(value: '7', scopes: ['source.js', 'constant.numeric.decimal.js'])
# line 2 is unchanged
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[2]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
@@ -283,9 +280,9 @@ describe "TokenizedBuffer", ->
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[6]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
# lines below deleted regions should be shifted upward
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[2]).toEqual(value: 'while', scopes: ['source.js', 'keyword.control.js'])
expect(tokenizedBuffer.tokenizedLineForRow(3).tokens[4]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
expect(tokenizedBuffer.tokenizedLineForRow(4).tokens[4]).toEqual(value: '<', scopes: ['source.js', 'keyword.operator.comparison.js'])
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1]).toEqual(value: 'while', scopes: ['source.js', 'keyword.control.js'])
expect(tokenizedBuffer.tokenizedLineForRow(3).tokens[1]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
expect(tokenizedBuffer.tokenizedLineForRow(4).tokens[1]).toEqual(value: '<', scopes: ['source.js', 'keyword.operator.comparison.js'])
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
@@ -331,7 +328,7 @@ describe "TokenizedBuffer", ->
expect(tokenizedBuffer.tokenizedLineForRow(4).tokens[4]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
# previous line 3 is pushed down to become line 5
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[4]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[3]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
@@ -377,32 +374,6 @@ describe "TokenizedBuffer", ->
expect(tokenizedBuffer.tokenizedLineForRow(5).ruleStack?).toBeTruthy()
expect(tokenizedBuffer.tokenizedLineForRow(6).ruleStack?).toBeTruthy()
it "tokenizes leading whitespace based on the new tab length", ->
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[0].isAtomic).toBeTruthy()
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[0].value).toBe " "
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[1].isAtomic).toBeTruthy()
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[1].value).toBe " "
tokenizedBuffer.setTabLength(4)
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[0].isAtomic).toBeTruthy()
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[0].value).toBe " "
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[1].isAtomic).toBeFalsy()
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[1].value).toBe " current "
it "does not tokenize whitespaces followed by combining characters as leading whitespace", ->
buffer.setText(" \u030b")
fullyTokenize(tokenizedBuffer)
{tokens} = tokenizedBuffer.tokenizedLineForRow(0)
expect(tokens[0].value).toBe " "
expect(tokens[0].hasLeadingWhitespace()).toBe true
expect(tokens[1].value).toBe " "
expect(tokens[1].hasLeadingWhitespace()).toBe true
expect(tokens[2].value).toBe " \u030b"
expect(tokens[2].hasLeadingWhitespace()).toBe false
it "does not break out soft tabs across a scope boundary", ->
waitsForPromise ->
atom.packages.activatePackage('language-gfm')
@@ -439,133 +410,6 @@ describe "TokenizedBuffer", ->
beforeEach ->
fullyTokenize(tokenizedBuffer)
it "renders each tab as its own atomic token with a value of size tabLength", ->
tabAsSpaces = _.multiplyString(' ', tokenizedBuffer.getTabLength())
screenLine0 = tokenizedBuffer.tokenizedLineForRow(0)
expect(screenLine0.text).toBe "# Econ 101#{tabAsSpaces}"
{tokens} = screenLine0
expect(tokens.length).toBe 4
expect(tokens[0].value).toBe "#"
expect(tokens[1].value).toBe " Econ 101"
expect(tokens[2].value).toBe tabAsSpaces
expect(tokens[2].scopes).toEqual tokens[1].scopes
expect(tokens[2].isAtomic).toBeTruthy()
expect(tokens[3].value).toBe ""
expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "#{tabAsSpaces} buy()#{tabAsSpaces}while supply > demand"
it "aligns the hard tabs to the correct tab stop column", ->
buffer.setText """
1\t2 \t3\t4
12\t3 \t4\t5
123\t4 \t5\t6
"""
tokenizedBuffer.setTabLength(4)
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "1 2 3 4"
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].screenDelta).toBe 3
expect(tokenizedBuffer.tokenizedLineForRow(1).text).toBe "12 3 4 5"
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].screenDelta).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "123 4 5 6"
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].screenDelta).toBe 1
tokenizedBuffer.setTabLength(3)
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "1 2 3 4"
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].screenDelta).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(1).text).toBe "12 3 4 5"
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].screenDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "123 4 5 6"
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].screenDelta).toBe 3
tokenizedBuffer.setTabLength(2)
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "1 2 3 4"
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].screenDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(1).text).toBe "12 3 4 5"
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].screenDelta).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "123 4 5 6"
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].screenDelta).toBe 1
tokenizedBuffer.setTabLength(1)
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "1 2 3 4"
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].screenDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(1).text).toBe "12 3 4 5"
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].screenDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "123 4 5 6"
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].bufferDelta).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].screenDelta).toBe 1
describe "when the buffer contains UTF-8 surrogate pairs", ->
beforeEach ->
waitsForPromise ->
atom.packages.activatePackage('language-javascript')
runs ->
buffer = atom.project.bufferForPathSync 'sample-with-pairs.js'
buffer.setText """
'abc\uD835\uDF97def'
//\uD835\uDF97xyz
"""
tokenizedBuffer = new TokenizedBuffer({
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
fullyTokenize(tokenizedBuffer)
afterEach ->
tokenizedBuffer.destroy()
buffer.release()
it "renders each UTF-8 surrogate pair as its own atomic token", ->
screenLine0 = tokenizedBuffer.tokenizedLineForRow(0)
expect(screenLine0.text).toBe "'abc\uD835\uDF97def'"
{tokens} = screenLine0
expect(tokens.length).toBe 5
expect(tokens[0].value).toBe "'"
expect(tokens[1].value).toBe "abc"
expect(tokens[2].value).toBe "\uD835\uDF97"
expect(tokens[2].isAtomic).toBeTruthy()
expect(tokens[3].value).toBe "def"
expect(tokens[4].value).toBe "'"
screenLine1 = tokenizedBuffer.tokenizedLineForRow(1)
expect(screenLine1.text).toBe "//\uD835\uDF97xyz"
{tokens} = screenLine1
expect(tokens.length).toBe 4
expect(tokens[0].value).toBe '//'
expect(tokens[1].value).toBe '\uD835\uDF97'
expect(tokens[1].value).toBeTruthy()
expect(tokens[2].value).toBe 'xyz'
expect(tokens[3].value).toBe ''
describe "when the grammar is tokenized", ->
it "emits the `tokenized` event", ->
editor = null
@@ -575,7 +419,7 @@ describe "TokenizedBuffer", ->
atom.workspace.open('sample.js').then (o) -> editor = o
runs ->
tokenizedBuffer = editor.displayBuffer.tokenizedBuffer
tokenizedBuffer = editor.tokenizedBuffer
tokenizedBuffer.onDidTokenize tokenizedHandler
fullyTokenize(tokenizedBuffer)
expect(tokenizedHandler.callCount).toBe(1)
@@ -588,7 +432,7 @@ describe "TokenizedBuffer", ->
atom.workspace.open('sample.js').then (o) -> editor = o
runs ->
tokenizedBuffer = editor.displayBuffer.tokenizedBuffer
tokenizedBuffer = editor.tokenizedBuffer
fullyTokenize(tokenizedBuffer)
tokenizedBuffer.onDidTokenize tokenizedHandler
@@ -606,7 +450,7 @@ describe "TokenizedBuffer", ->
atom.workspace.open('coffee.coffee').then (o) -> editor = o
runs ->
tokenizedBuffer = editor.displayBuffer.tokenizedBuffer
tokenizedBuffer = editor.tokenizedBuffer
tokenizedBuffer.onDidTokenize tokenizedHandler
fullyTokenize(tokenizedBuffer)
tokenizedHandler.reset()
@@ -683,132 +527,7 @@ describe "TokenizedBuffer", ->
it "returns the range covered by all contigous tokens (within a single line)", ->
expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.function', [1, 18])).toEqual [[1, 6], [1, 28]]
describe "when the editor.tabLength config value changes", ->
it "updates the tab length of the tokenized lines", ->
buffer = atom.project.bufferForPathSync('sample.js')
buffer.setText('\ttest')
tokenizedBuffer = new TokenizedBuffer({
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' '
atom.config.set('editor.tabLength', 6)
expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' '
it "does not allow the tab length to be less than 1", ->
buffer = atom.project.bufferForPathSync('sample.js')
buffer.setText('\ttest')
tokenizedBuffer = new TokenizedBuffer({
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' '
atom.config.set('editor.tabLength', 1)
expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' '
atom.config.set('editor.tabLength', 0)
expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' '
describe "when the invisibles value changes", ->
beforeEach ->
it "updates the tokens with the appropriate invisible characters", ->
buffer = new TextBuffer(text: " \t a line with tabs\tand \tspaces \t ")
tokenizedBuffer = new TokenizedBuffer({
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
fullyTokenize(tokenizedBuffer)
atom.config.set("editor.showInvisibles", true)
atom.config.set("editor.invisibles", space: 'S', tab: 'T')
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "SST Sa line with tabsTand T spacesSTS"
# Also needs to work for copies
expect(tokenizedBuffer.tokenizedLineForRow(0).copy().text).toBe "SST Sa line with tabsTand T spacesSTS"
it "assigns endOfLineInvisibles to tokenized lines", ->
buffer = new TextBuffer(text: "a line that ends in a carriage-return-line-feed \r\na line that ends in just a line-feed\na line with no ending")
tokenizedBuffer = new TokenizedBuffer({
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
atom.config.set('editor.showInvisibles', true)
atom.config.set("editor.invisibles", cr: 'R', eol: 'N')
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLineForRow(0).endOfLineInvisibles).toEqual ['R', 'N']
expect(tokenizedBuffer.tokenizedLineForRow(1).endOfLineInvisibles).toEqual ['N']
# Lines ending in soft wraps get no invisibles
[left, right] = tokenizedBuffer.tokenizedLineForRow(0).softWrapAt(20)
expect(left.endOfLineInvisibles).toBe null
expect(right.endOfLineInvisibles).toEqual ['R', 'N']
atom.config.set("editor.invisibles", cr: 'R', eol: false)
expect(tokenizedBuffer.tokenizedLineForRow(0).endOfLineInvisibles).toEqual ['R']
expect(tokenizedBuffer.tokenizedLineForRow(1).endOfLineInvisibles).toEqual []
describe "leading and trailing whitespace", ->
beforeEach ->
buffer = atom.project.bufferForPathSync('sample.js')
tokenizedBuffer = new TokenizedBuffer({
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
fullyTokenize(tokenizedBuffer)
it "assigns ::firstNonWhitespaceIndex on tokens that have leading whitespace", ->
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[0].firstNonWhitespaceIndex).toBe null
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[0].firstNonWhitespaceIndex).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].firstNonWhitespaceIndex).toBe null
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[0].firstNonWhitespaceIndex).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].firstNonWhitespaceIndex).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[2].firstNonWhitespaceIndex).toBe null
# The 4th token *has* leading whitespace, but isn't entirely whitespace
buffer.insert([5, 0], ' ')
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[3].firstNonWhitespaceIndex).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[4].firstNonWhitespaceIndex).toBe null
# Lines that are *only* whitespace are not considered to have leading whitespace
buffer.insert([10, 0], ' ')
expect(tokenizedBuffer.tokenizedLineForRow(10).tokens[0].firstNonWhitespaceIndex).toBe null
it "assigns ::firstTrailingWhitespaceIndex on tokens that have trailing whitespace", ->
buffer.insert([0, Infinity], ' ')
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[11].firstTrailingWhitespaceIndex).toBe null
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[12].firstTrailingWhitespaceIndex).toBe 0
# The last token *has* trailing whitespace, but isn't entirely whitespace
buffer.setTextInRange([[2, 39], [2, 40]], ' ')
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[14].firstTrailingWhitespaceIndex).toBe null
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[15].firstTrailingWhitespaceIndex).toBe 6
# Lines that are *only* whitespace are considered to have trailing whitespace
buffer.insert([10, 0], ' ')
expect(tokenizedBuffer.tokenizedLineForRow(10).tokens[0].firstTrailingWhitespaceIndex).toBe 0
it "only marks trailing whitespace on the last segment of a soft-wrapped line", ->
buffer.insert([0, Infinity], ' ')
tokenizedLine = tokenizedBuffer.tokenizedLineForRow(0)
[segment1, segment2] = tokenizedLine.softWrapAt(16)
expect(segment1.tokens[5].value).toBe ' '
expect(segment1.tokens[5].firstTrailingWhitespaceIndex).toBe null
expect(segment2.tokens[6].value).toBe ' '
expect(segment2.tokens[6].firstTrailingWhitespaceIndex).toBe 0
it "sets leading and trailing whitespace correctly on a line with invisible characters that is copied", ->
buffer.setText(" \t a line with tabs\tand \tspaces \t ")
atom.config.set("editor.showInvisibles", true)
atom.config.set("editor.invisibles", space: 'S', tab: 'T')
fullyTokenize(tokenizedBuffer)
line = tokenizedBuffer.tokenizedLineForRow(0).copy()
expect(line.tokens[0].firstNonWhitespaceIndex).toBe 2
expect(line.tokens[line.tokens.length - 1].firstTrailingWhitespaceIndex).toBe 0
describe ".indentLevel on tokenized lines", ->
describe ".indentLevelForRow(row)", ->
beforeEach ->
buffer = atom.project.bufferForPathSync('sample.js')
tokenizedBuffer = new TokenizedBuffer({
@@ -818,43 +537,43 @@ describe "TokenizedBuffer", ->
describe "when the line is non-empty", ->
it "has an indent level based on the leading whitespace on the line", ->
expect(tokenizedBuffer.tokenizedLineForRow(0).indentLevel).toBe 0
expect(tokenizedBuffer.tokenizedLineForRow(1).indentLevel).toBe 1
expect(tokenizedBuffer.tokenizedLineForRow(2).indentLevel).toBe 2
expect(tokenizedBuffer.indentLevelForRow(0)).toBe 0
expect(tokenizedBuffer.indentLevelForRow(1)).toBe 1
expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2
buffer.insert([2, 0], ' ')
expect(tokenizedBuffer.tokenizedLineForRow(2).indentLevel).toBe 2.5
expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2.5
describe "when the line is empty", ->
it "assumes the indentation level of the first non-empty line below or above if one exists", ->
buffer.insert([12, 0], ' ')
buffer.insert([12, Infinity], '\n\n')
expect(tokenizedBuffer.tokenizedLineForRow(13).indentLevel).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(14).indentLevel).toBe 2
expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2
expect(tokenizedBuffer.indentLevelForRow(14)).toBe 2
buffer.insert([1, Infinity], '\n\n')
expect(tokenizedBuffer.tokenizedLineForRow(2).indentLevel).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(3).indentLevel).toBe 2
expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2
expect(tokenizedBuffer.indentLevelForRow(3)).toBe 2
buffer.setText('\n\n\n')
expect(tokenizedBuffer.tokenizedLineForRow(1).indentLevel).toBe 0
expect(tokenizedBuffer.indentLevelForRow(1)).toBe 0
describe "when the changed lines are surrounded by whitespace-only lines", ->
it "updates the indentLevel of empty lines that precede the change", ->
expect(tokenizedBuffer.tokenizedLineForRow(12).indentLevel).toBe 0
expect(tokenizedBuffer.indentLevelForRow(12)).toBe 0
buffer.insert([12, 0], '\n')
buffer.insert([13, 0], ' ')
expect(tokenizedBuffer.tokenizedLineForRow(12).indentLevel).toBe 1
expect(tokenizedBuffer.indentLevelForRow(12)).toBe 1
it "updates empty line indent guides when the empty line is the last line", ->
buffer.insert([12, 2], '\n')
# The newline and the tab need to be in two different operations to surface the bug
buffer.insert([12, 0], ' ')
expect(tokenizedBuffer.tokenizedLineForRow(13).indentLevel).toBe 1
expect(tokenizedBuffer.indentLevelForRow(13)).toBe 1
buffer.insert([12, 0], ' ')
expect(tokenizedBuffer.tokenizedLineForRow(13).indentLevel).toBe 2
expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(14)).not.toBeDefined()
it "updates the indentLevel of empty lines surrounding a change that inserts lines", ->
@@ -862,24 +581,24 @@ describe "TokenizedBuffer", ->
buffer.insert([7, 0], '\n\n')
buffer.insert([5, 0], '\n\n')
expect(tokenizedBuffer.tokenizedLineForRow(5).indentLevel).toBe 3
expect(tokenizedBuffer.tokenizedLineForRow(6).indentLevel).toBe 3
expect(tokenizedBuffer.tokenizedLineForRow(9).indentLevel).toBe 3
expect(tokenizedBuffer.tokenizedLineForRow(10).indentLevel).toBe 3
expect(tokenizedBuffer.tokenizedLineForRow(11).indentLevel).toBe 2
expect(tokenizedBuffer.indentLevelForRow(5)).toBe 3
expect(tokenizedBuffer.indentLevelForRow(6)).toBe 3
expect(tokenizedBuffer.indentLevelForRow(9)).toBe 3
expect(tokenizedBuffer.indentLevelForRow(10)).toBe 3
expect(tokenizedBuffer.indentLevelForRow(11)).toBe 2
tokenizedBuffer.onDidChange changeHandler = jasmine.createSpy('changeHandler')
buffer.setTextInRange([[7, 0], [8, 65]], ' one\n two\n three\n four')
delete changeHandler.argsForCall[0][0].bufferChange
expect(changeHandler).toHaveBeenCalledWith(start: 5, end: 10, delta: 2)
expect(changeHandler).toHaveBeenCalledWith(start: 7, end: 8, delta: 2)
expect(tokenizedBuffer.tokenizedLineForRow(5).indentLevel).toBe 4
expect(tokenizedBuffer.tokenizedLineForRow(6).indentLevel).toBe 4
expect(tokenizedBuffer.tokenizedLineForRow(11).indentLevel).toBe 4
expect(tokenizedBuffer.tokenizedLineForRow(12).indentLevel).toBe 4
expect(tokenizedBuffer.tokenizedLineForRow(13).indentLevel).toBe 2
expect(tokenizedBuffer.indentLevelForRow(5)).toBe 4
expect(tokenizedBuffer.indentLevelForRow(6)).toBe 4
expect(tokenizedBuffer.indentLevelForRow(11)).toBe 4
expect(tokenizedBuffer.indentLevelForRow(12)).toBe 4
expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2
it "updates the indentLevel of empty lines surrounding a change that removes lines", ->
# create some new lines
@@ -891,14 +610,14 @@ describe "TokenizedBuffer", ->
buffer.setTextInRange([[7, 0], [8, 65]], ' ok')
delete changeHandler.argsForCall[0][0].bufferChange
expect(changeHandler).toHaveBeenCalledWith(start: 5, end: 10, delta: -1)
expect(changeHandler).toHaveBeenCalledWith(start: 7, end: 8, delta: -1)
expect(tokenizedBuffer.tokenizedLineForRow(5).indentLevel).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(6).indentLevel).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(7).indentLevel).toBe 2 # new text
expect(tokenizedBuffer.tokenizedLineForRow(8).indentLevel).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(9).indentLevel).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(10).indentLevel).toBe 2 # }
expect(tokenizedBuffer.indentLevelForRow(5)).toBe 2
expect(tokenizedBuffer.indentLevelForRow(6)).toBe 2
expect(tokenizedBuffer.indentLevelForRow(7)).toBe 2 # new text
expect(tokenizedBuffer.indentLevelForRow(8)).toBe 2
expect(tokenizedBuffer.indentLevelForRow(9)).toBe 2
expect(tokenizedBuffer.indentLevelForRow(10)).toBe 2 # }
describe "::isFoldableAtRow(row)", ->
changes = null
@@ -1050,3 +769,107 @@ describe "TokenizedBuffer", ->
runs ->
expect(coffeeCalled).toBe true
describe "text decoration layer API", ->
describe "iterator", ->
it "iterates over the syntactic scope boundaries", ->
buffer = new TextBuffer(text: "var foo = 1 /*\nhello*/var bar = 2\n")
tokenizedBuffer = new TokenizedBuffer({
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
tokenizedBuffer.setGrammar(atom.grammars.selectGrammar(".js"))
fullyTokenize(tokenizedBuffer)
iterator = tokenizedBuffer.buildIterator()
iterator.seek(Point(0, 0))
expectedBoundaries = [
{position: Point(0, 0), closeTags: [], openTags: ["source.js", "storage.type.var.js"]}
{position: Point(0, 3), closeTags: ["storage.type.var.js"], openTags: []}
{position: Point(0, 8), closeTags: [], openTags: ["keyword.operator.assignment.js"]}
{position: Point(0, 9), closeTags: ["keyword.operator.assignment.js"], openTags: []}
{position: Point(0, 10), closeTags: [], openTags: ["constant.numeric.decimal.js"]}
{position: Point(0, 11), closeTags: ["constant.numeric.decimal.js"], openTags: []}
{position: Point(0, 12), closeTags: [], openTags: ["comment.block.js", "punctuation.definition.comment.js"]}
{position: Point(0, 14), closeTags: ["punctuation.definition.comment.js"], openTags: []}
{position: Point(1, 5), closeTags: [], openTags: ["punctuation.definition.comment.js"]}
{position: Point(1, 7), closeTags: ["punctuation.definition.comment.js", "comment.block.js"], openTags: ["storage.type.var.js"]}
{position: Point(1, 10), closeTags: ["storage.type.var.js"], openTags: []}
{position: Point(1, 15), closeTags: [], openTags: ["keyword.operator.assignment.js"]}
{position: Point(1, 16), closeTags: ["keyword.operator.assignment.js"], openTags: []}
{position: Point(1, 17), closeTags: [], openTags: ["constant.numeric.decimal.js"]}
{position: Point(1, 18), closeTags: ["constant.numeric.decimal.js"], openTags: []}
]
loop
boundary = {
position: iterator.getPosition(),
closeTags: iterator.getCloseTags(),
openTags: iterator.getOpenTags()
}
expect(boundary).toEqual(expectedBoundaries.shift())
break unless iterator.moveToSuccessor()
expect(iterator.seek(Point(0, 1))).toEqual(["source.js", "storage.type.var.js"])
expect(iterator.getPosition()).toEqual(Point(0, 3))
expect(iterator.seek(Point(0, 8))).toEqual(["source.js"])
expect(iterator.getPosition()).toEqual(Point(0, 8))
expect(iterator.seek(Point(1, 0))).toEqual(["source.js", "comment.block.js"])
expect(iterator.getPosition()).toEqual(Point(1, 5))
expect(iterator.seek(Point(1, 18))).toEqual(["source.js", "constant.numeric.decimal.js"])
expect(iterator.getPosition()).toEqual(Point(1, 18))
expect(iterator.seek(Point(2, 0))).toEqual(["source.js"])
iterator.moveToSuccessor() # ensure we don't infinitely loop (regression test)
it "does not report columns beyond the length of the line", ->
waitsForPromise ->
atom.packages.activatePackage('language-coffee-script')
runs ->
buffer = new TextBuffer(text: "# hello\n# world")
tokenizedBuffer = new TokenizedBuffer({
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
tokenizedBuffer.setGrammar(atom.grammars.selectGrammar(".coffee"))
fullyTokenize(tokenizedBuffer)
iterator = tokenizedBuffer.buildIterator()
iterator.seek(Point(0, 0))
iterator.moveToSuccessor()
iterator.moveToSuccessor()
expect(iterator.getPosition().column).toBe(7)
iterator.moveToSuccessor()
expect(iterator.getPosition().column).toBe(0)
iterator.seek(Point(0, 7))
expect(iterator.getPosition().column).toBe(7)
iterator.seek(Point(0, 8))
expect(iterator.getPosition().column).toBe(7)
it "correctly terminates scopes at the beginning of the line (regression)", ->
grammar = atom.grammars.createGrammar('test', {
'scopeName': 'text.broken'
'name': 'Broken grammar'
'patterns': [
{'begin': 'start', 'end': '(?=end)', 'name': 'blue.broken'},
{'match': '.', 'name': 'yellow.broken'}
]
})
buffer = new TextBuffer(text: 'start x\nend x\nx')
tokenizedBuffer = new TokenizedBuffer({
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
tokenizedBuffer.setGrammar(grammar)
fullyTokenize(tokenizedBuffer)
iterator = tokenizedBuffer.buildIterator()
iterator.seek(Point(1, 0))
expect(iterator.getPosition()).toEqual([1, 0])
expect(iterator.getCloseTags()).toEqual ['blue.broken']
expect(iterator.getOpenTags()).toEqual ['yellow.broken']

View File

@@ -1,19 +0,0 @@
describe "TokenizedLine", ->
editor = null
beforeEach ->
waitsForPromise -> atom.packages.activatePackage('language-coffee-script')
describe "::isOnlyWhitespace()", ->
beforeEach ->
waitsForPromise ->
atom.workspace.open('coffee.coffee').then (o) -> editor = o
it "returns true when the line is only whitespace", ->
expect(editor.tokenizedLineForScreenRow(3).isOnlyWhitespace()).toBe true
expect(editor.tokenizedLineForScreenRow(7).isOnlyWhitespace()).toBe true
expect(editor.tokenizedLineForScreenRow(23).isOnlyWhitespace()).toBe true
it "returns false when the line is not only whitespace", ->
expect(editor.tokenizedLineForScreenRow(0).isOnlyWhitespace()).toBe false
expect(editor.tokenizedLineForScreenRow(2).isOnlyWhitespace()).toBe false

View File

@@ -75,7 +75,7 @@ describe "WindowEventHandler", ->
describe "when a link is clicked", ->
it "opens the http/https links in an external application", ->
shell = require 'shell'
{shell} = require 'electron'
spyOn(shell, 'openExternal')
link = document.createElement('a')

View File

@@ -47,9 +47,14 @@ describe "WorkspaceElement", ->
it "updates the font-family based on the 'editor.fontFamily' config value", ->
initialCharWidth = editor.getDefaultCharWidth()
expect(getComputedStyle(editorElement).fontFamily).toBe atom.config.get('editor.fontFamily')
fontFamily = atom.config.get('editor.fontFamily')
fontFamily += ", 'Apple Color Emoji'" if process.platform is 'darwin'
expect(getComputedStyle(editorElement).fontFamily).toBe fontFamily
atom.config.set('editor.fontFamily', 'sans-serif')
expect(getComputedStyle(editorElement).fontFamily).toBe atom.config.get('editor.fontFamily')
fontFamily = atom.config.get('editor.fontFamily')
fontFamily += ", 'Apple Color Emoji'" if process.platform is 'darwin'
expect(getComputedStyle(editorElement).fontFamily).toBe fontFamily
expect(editor.getDefaultCharWidth()).not.toBe initialCharWidth
it "updates the line-height based on the 'editor.lineHeight' config value", ->

View File

@@ -80,7 +80,8 @@ describe "Workspace", ->
expect(untitledEditor.getText()).toBe("An untitled editor.")
expect(atom.workspace.getActiveTextEditor().getPath()).toBe editor3.getPath()
expect(document.title).toMatch ///^#{path.basename(editor3.getLongTitle())}\ \u2014\ #{atom.project.getPaths()[0]}///
pathEscaped = escapeStringRegex(atom.project.getPaths()[0])
expect(document.title).toMatch ///^#{path.basename(editor3.getLongTitle())}\ \u2014\ #{pathEscaped}///
describe "where there are no open panes or editors", ->
it "constructs the view with no open editors", ->
@@ -428,7 +429,7 @@ describe "Workspace", ->
workspace.open('sample.js').then (e) -> editor = e
runs ->
expect(editor.displayBuffer.largeFileMode).toBe true
expect(editor.largeFileMode).toBe true
describe "when the file is over 20MB", ->
it "prompts the user to make sure they want to open a file this big", ->
@@ -453,7 +454,7 @@ describe "Workspace", ->
runs ->
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
expect(editor.displayBuffer.largeFileMode).toBe true
expect(editor.largeFileMode).toBe true
describe "when passed a path that matches a custom opener", ->
it "returns the resource returned by the custom opener", ->
@@ -833,25 +834,29 @@ describe "Workspace", ->
describe "when there is an active pane item", ->
it "sets the title to the pane item's title plus the project path", ->
item = atom.workspace.getActivePaneItem()
expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{atom.project.getPaths()[0]}///
pathEscaped = escapeStringRegex(atom.project.getPaths()[0])
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'))
expect(document.title).toMatch ///^#{editor.getTitle()}\ \u2014\ #{atom.project.getPaths()[0]}///
pathEscaped = escapeStringRegex(atom.project.getPaths()[0])
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()
expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{atom.project.getPaths()[0]}///
pathEscaped = escapeStringRegex(atom.project.getPaths()[0])
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", ->
atom.workspace.getActivePane().destroy()
expect(atom.workspace.getActivePaneItem()).toBeUndefined()
expect(document.title).toMatch ///^#{atom.project.getPaths()[0]}///
pathEscaped = escapeStringRegex(atom.project.getPaths()[0])
expect(document.title).toMatch ///^#{pathEscaped}///
describe "when an inactive pane's item changes", ->
it "does not update the title", ->
@@ -875,7 +880,8 @@ describe "Workspace", ->
})
workspace2.deserialize(atom.workspace.serialize(), atom.deserializers)
item = workspace2.getActivePaneItem()
expect(document.title).toMatch ///^#{item.getLongTitle()}\ \u2014\ #{atom.project.getPaths()[0]}///
pathEscaped = escapeStringRegex(atom.project.getPaths()[0])
expect(document.title).toMatch ///^#{item.getLongTitle()}\ \u2014\ #{pathEscaped}///
workspace2.destroy()
describe "document edited status", ->
@@ -1610,3 +1616,48 @@ describe "Workspace", ->
runs ->
expect(pane.getPendingItem()).toBeFalsy()
describe "grammar activation", ->
beforeEach ->
waitsForPromise ->
atom.packages.activatePackage('language-javascript')
it "notifies the workspace of which grammar is used", ->
editor = null
grammarUsed = jasmine.createSpy()
atom.workspace.handleGrammarUsed = grammarUsed
waitsForPromise -> atom.workspace.open('sample-with-comments.js').then (o) -> editor = o
waitsFor -> grammarUsed.callCount is 1
runs ->
expect(grammarUsed.argsForCall[0][0].name).toBe 'JavaScript'
describe ".checkoutHeadRevision()", ->
editor = null
beforeEach ->
atom.config.set("editor.confirmCheckoutHeadRevision", false)
waitsForPromise -> atom.workspace.open('sample-with-comments.js').then (o) -> editor = o
it "reverts to the version of its file checked into the project repository", ->
editor.setCursorBufferPosition([0, 0])
editor.insertText("---\n")
expect(editor.lineTextForBufferRow(0)).toBe "---"
waitsForPromise ->
atom.workspace.checkoutHeadRevision(editor)
runs ->
expect(editor.lineTextForBufferRow(0)).toBe ""
describe "when there's no repository for the editor's file", ->
it "doesn't do anything", ->
editor = atom.workspace.buildTextEditor()
editor.setText("stuff")
atom.workspace.checkoutHeadRevision(editor)
waitsForPromise -> atom.workspace.checkoutHeadRevision(editor)
escapeStringRegex = (str) ->
str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')

View File

@@ -1,5 +1,5 @@
_ = require 'underscore-plus'
{ipcRenderer, remote, shell, webFrame} = require 'electron'
{screen, ipcRenderer, remote, shell, webFrame} = require 'electron'
ipcHelpers = require './ipc-helpers'
{Disposable} = require 'event-kit'
{getWindowLoadSettings, setWindowLoadSettings} = require './window-load-settings-helpers'
@@ -211,6 +211,14 @@ class ApplicationDelegate
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onUpdateError: (callback) ->
outerCallback = (event, message, detail) ->
callback(detail) if message is 'update-error'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onApplicationMenuCommand: (callback) ->
outerCallback = (event, args...) ->
callback(args...)
@@ -233,14 +241,28 @@ class ApplicationDelegate
openExternal: (url) ->
shell.openExternal(url)
disablePinchToZoom: ->
webFrame.setZoomLevelLimits(1, 1)
disableZoom: ->
outerCallback = ->
webFrame.setZoomLevelLimits(1, 1)
outerCallback()
# Set the limits every time a display is added or removed, otherwise the
# configuration gets reset to the default, which allows zooming the
# webframe.
screen.on('display-added', outerCallback)
screen.on('display-removed', outerCallback)
new Disposable ->
screen.removeListener('display-added', outerCallback)
screen.removeListener('display-removed', outerCallback)
checkForUpdate: ->
ipcRenderer.send('check-for-update')
ipcRenderer.send('command', 'application:check-for-update')
restartAndInstallUpdate: ->
ipcRenderer.send('install-update')
ipcRenderer.send('command', 'application:install-update')
getAutoUpdateManagerState: ->
ipcRenderer.sendSync('get-auto-update-manager-state')
getAutoUpdateManagerErrorMessage: ->
ipcRenderer.sendSync('get-auto-update-manager-error')

View File

@@ -11,6 +11,7 @@ Model = require './model'
WindowEventHandler = require './window-event-handler'
StylesElement = require './styles-element'
StateStore = require './state-store'
StorageFolder = require './storage-folder'
{getWindowLoadSettings, setWindowLoadSettings} = require './window-load-settings-helpers'
registerDefaultCommands = require './register-default-commands'
@@ -127,7 +128,7 @@ class AtomEnvironment extends Model
# Call .loadOrCreate instead
constructor: (params={}) ->
{@blobStore, @applicationDelegate, @window, @document, configDirPath, @enablePersistence, onlyLoadBaseStyleSheets} = params
{@blobStore, @applicationDelegate, @window, @document, @configDirPath, @enablePersistence, onlyLoadBaseStyleSheets} = params
@unloaded = false
@loadTime = null
@@ -138,7 +139,9 @@ class AtomEnvironment extends Model
@stateStore = new StateStore('AtomEnvironments', 1)
@stateStore.clear() if clearWindowState
if clearWindowState
@getStorageFolder().clear()
@stateStore.clear()
@deserializers = new DeserializerManager(this)
@deserializeTimings = {}
@@ -147,10 +150,10 @@ class AtomEnvironment extends Model
@notifications = new NotificationManager
@config = new Config({configDirPath, resourcePath, notificationManager: @notifications, @enablePersistence})
@config = new Config({@configDirPath, resourcePath, notificationManager: @notifications, @enablePersistence})
@setConfigSchema()
@keymaps = new KeymapManager({configDirPath, resourcePath, notificationManager: @notifications})
@keymaps = new KeymapManager({@configDirPath, resourcePath, notificationManager: @notifications})
@tooltips = new TooltipManager(keymapManager: @keymaps)
@@ -159,16 +162,16 @@ class AtomEnvironment extends Model
@grammars = new GrammarRegistry({@config})
@styles = new StyleManager({configDirPath})
@styles = new StyleManager({@configDirPath})
@packages = new PackageManager({
devMode, configDirPath, resourcePath, safeMode, @config, styleManager: @styles,
devMode, @configDirPath, resourcePath, safeMode, @config, styleManager: @styles,
commandRegistry: @commands, keymapManager: @keymaps, notificationManager: @notifications,
grammarRegistry: @grammars, deserializerManager: @deserializers, viewRegistry: @views
})
@themes = new ThemeManager({
packageManager: @packages, configDirPath, resourcePath, safeMode, @config,
packageManager: @packages, @configDirPath, resourcePath, safeMode, @config,
styleManager: @styles, notificationManager: @notifications, viewRegistry: @views
})
@@ -205,7 +208,7 @@ class AtomEnvironment extends Model
@stylesElement = @styles.buildStylesElement()
@document.head.appendChild(@stylesElement)
@applicationDelegate.disablePinchToZoom()
@disposables.add(@applicationDelegate.disableZoom())
@keymaps.subscribeToFileReadFailure()
@keymaps.loadBundledKeymaps()
@@ -231,13 +234,14 @@ class AtomEnvironment extends Model
checkPortableHomeWritable()
attachSaveStateListeners: ->
saveState = => @saveState({isUnloading: false}) unless @unloaded
debouncedSaveState = _.debounce(saveState, @saveStateDebounceInterval)
@document.addEventListener('mousedown', debouncedSaveState, true)
@document.addEventListener('keydown', debouncedSaveState, true)
saveState = _.debounce((=>
window.requestIdleCallback => @saveState({isUnloading: false}) unless @unloaded
), @saveStateDebounceInterval)
@document.addEventListener('mousedown', saveState, true)
@document.addEventListener('keydown', saveState, true)
@disposables.add new Disposable =>
@document.removeEventListener('mousedown', debouncedSaveState, true)
@document.removeEventListener('keydown', debouncedSaveState, true)
@document.removeEventListener('mousedown', saveState, true)
@document.removeEventListener('keydown', saveState, true)
setConfigSchema: ->
@config.setSchema null, {type: 'object', properties: _.clone(require('./config-schema'))}
@@ -252,7 +256,7 @@ class AtomEnvironment extends Model
@deserializers.add(TextBuffer)
registerDefaultCommands: ->
registerDefaultCommands({commandRegistry: @commands, @config, @commandInstaller})
registerDefaultCommands({commandRegistry: @commands, @config, @commandInstaller, notificationManager: @notifications, @project, @clipboard})
registerDefaultViewProviders: ->
@views.addViewProvider Workspace, (model, env) ->
@@ -652,6 +656,7 @@ class AtomEnvironment extends Model
# Call this method when establishing a real application window.
startEditorWindow: ->
@unloaded = false
@loadState().then (state) =>
@windowDimensions = state?.windowDimensions
@displayWindow().then =>
@@ -785,6 +790,7 @@ class AtomEnvironment extends Model
# Returns a {Promise} that resolves when the DevTools have been opened or
# closed.
toggleDevTools: ->
require("devtron").install()
@applicationDelegate.toggleWindowDevTools()
# Extended: Execute code in dev tools.
@@ -839,21 +845,25 @@ class AtomEnvironment extends Model
return Promise.resolve() unless @enablePersistence
new Promise (resolve, reject) =>
window.requestIdleCallback =>
return if not @project
return if not @project
state = @serialize(options)
savePromise =
if storageKey = @getStateKey(@project?.getPaths())
@stateStore.save(storageKey, state)
else
@applicationDelegate.setTemporaryWindowState(state)
savePromise.catch(reject).then(resolve)
state = @serialize(options)
savePromise =
if storageKey = @getStateKey(@project?.getPaths())
@stateStore.save(storageKey, state)
else
@applicationDelegate.setTemporaryWindowState(state)
savePromise.catch(reject).then(resolve)
loadState: ->
if @enablePersistence
if stateKey = @getStateKey(@getLoadSettings().initialPaths)
@stateStore.load(stateKey)
@stateStore.load(stateKey).then (state) =>
if state
state
else
# TODO: remove this when every user has migrated to the IndexedDb state store.
@getStorageFolder().load(stateKey)
else
@applicationDelegate.getTemporaryWindowState()
else
@@ -882,6 +892,9 @@ class AtomEnvironment extends Model
else
null
getStorageFolder: ->
@storageFolder ?= new StorageFolder(@getConfigDirPath())
getConfigDirPath: ->
@configDirPath ?= process.env.ATOM_HOME

View File

@@ -20,6 +20,9 @@ export default class AutoUpdateManager {
}),
applicationDelegate.onUpdateNotAvailable(() => {
this.emitter.emit('update-not-available')
}),
applicationDelegate.onUpdateError(() => {
this.emitter.emit('update-error')
})
)
}
@@ -41,6 +44,10 @@ export default class AutoUpdateManager {
return this.applicationDelegate.getAutoUpdateManagerState()
}
getErrorMessage () {
return this.applicationDelegate.getAutoUpdateManagerErrorMessage()
}
platformSupportsUpdates () {
return atom.getReleaseChannel() !== 'dev' && this.getState() !== 'unsupported'
}
@@ -67,6 +74,10 @@ export default class AutoUpdateManager {
return this.emitter.on('update-not-available', callback)
}
onUpdateError (callback) {
return this.emitter.on('update-error', callback)
}
getPlatform () {
return process.platform
}

View File

@@ -3,6 +3,7 @@ ApplicationMenu = require './application-menu'
AtomProtocolHandler = require './atom-protocol-handler'
AutoUpdateManager = require './auto-update-manager'
StorageFolder = require '../storage-folder'
Config = require '../config'
ipcHelpers = require '../ipc-helpers'
{BrowserWindow, Menu, app, dialog, ipcMain, shell} = require 'electron'
fs = require 'fs-plus'
@@ -70,7 +71,11 @@ class AtomApplication
@pidsToOpenWindows = {}
@windows = []
@autoUpdateManager = new AutoUpdateManager(@version, options.test, @resourcePath)
@config = new Config({configDirPath: process.env.ATOM_HOME, @resourcePath, enablePersistence: true})
@config.setSchema null, {type: 'object', properties: _.clone(require('../config-schema'))}
@config.load()
@autoUpdateManager = new AutoUpdateManager(@version, options.test, @resourcePath, @config)
@applicationMenu = new ApplicationMenu(@version, @autoUpdateManager)
@atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode)
@@ -133,7 +138,11 @@ class AtomApplication
return unless @socketPath?
@deleteSocketFile()
server = net.createServer (connection) =>
connection.on 'data', (data) =>
data = ''
connection.on 'data', (chunk) ->
data = data + chunk
connection.on 'end', =>
options = JSON.parse(data)
@openWithOptions(options)
@@ -165,9 +174,6 @@ class AtomApplication
@on 'application:quit', -> app.quit()
@on 'application:new-window', -> @openPath(getLoadSettings())
@on 'application:new-file', -> (@focusedWindow() ? this).openPath()
@on 'application:open', -> @promptForPathToOpen('all', getLoadSettings())
@on 'application:open-file', -> @promptForPathToOpen('file', getLoadSettings())
@on 'application:open-folder', -> @promptForPathToOpen('folder', getLoadSettings())
@on 'application:open-dev', -> @promptForPathToOpen('all', devMode: true)
@on 'application:open-safe', -> @promptForPathToOpen('all', safeMode: true)
@on 'application:inspect', ({x, y, atomWindow}) ->
@@ -250,6 +256,14 @@ class AtomApplication
ipcMain.on 'command', (event, command) =>
@emit(command)
ipcMain.on 'open-command', (event, command, args...) =>
defaultPath = args[0] if args.length > 0
switch command
when 'application:open' then @promptForPathToOpen('all', getLoadSettings(), defaultPath)
when 'application:open-file' then @promptForPathToOpen('file', getLoadSettings(), defaultPath)
when 'application:open-folder' then @promptForPathToOpen('folder', getLoadSettings(), defaultPath)
else console.log "Invalid open-command received: " + command
ipcMain.on 'window-command', (event, command, args...) ->
win = BrowserWindow.fromWebContents(event.sender)
win.emit(command, args...)
@@ -305,12 +319,12 @@ class AtomApplication
ipcMain.on 'execute-javascript-in-dev-tools', (event, code) ->
event.sender.devToolsWebContents?.executeJavaScript(code)
ipcMain.on 'check-for-update', =>
@autoUpdateManager.check()
ipcMain.on 'get-auto-update-manager-state', (event) =>
event.returnValue = @autoUpdateManager.getState()
ipcMain.on 'get-auto-update-manager-error', (event) =>
event.returnValue = @autoUpdateManager.getErrorMessage()
ipcMain.on 'execute-javascript-in-dev-tools', (event, code) ->
event.sender.devToolsWebContents?.executeJavaScript(code)
@@ -461,6 +475,7 @@ class AtomApplication
openedWindow.restore()
else
openedWindow.focus()
openedWindow.replaceEnvironment(env)
else
if devMode
try
@@ -510,7 +525,8 @@ class AtomApplication
@storageFolder.storeSync('application.json', states)
loadState: (options) ->
if (states = @storageFolder.load('application.json'))?.length > 0
restorePreviousState = @config.get('core.restorePreviousWindowsOnStart') ? true
if restorePreviousState and (states = @storageFolder.load('application.json'))?.length > 0
for state in states
@openWithOptions(_.extend(options, {
initialPaths: state.initialPaths
@@ -646,11 +662,13 @@ class AtomApplication
# :safeMode - A Boolean which controls whether any newly opened windows
# should be in safe mode or not.
# :window - An {AtomWindow} to use for opening a selected file path.
promptForPathToOpen: (type, {devMode, safeMode, window}) ->
@promptForPath type, (pathsToOpen) =>
@openPaths({pathsToOpen, devMode, safeMode, window})
# :path - An optional String which controls the default path to which the
# file dialog opens.
promptForPathToOpen: (type, {devMode, safeMode, window}, path=null) ->
@promptForPath type, ((pathsToOpen) =>
@openPaths({pathsToOpen, devMode, safeMode, window})), path
promptForPath: (type, callback) ->
promptForPath: (type, callback, path) ->
properties =
switch type
when 'file' then ['openFile']
@@ -673,8 +691,8 @@ class AtomApplication
when 'folder' then 'Open Folder'
else 'Open'
if process.platform is 'linux'
if projectPath = @lastFocusedWindow?.projectPath
openOptions.defaultPath = projectPath
# File dialog defaults to project directory of currently active editor
if path?
openOptions.defaultPath = path
dialog.showOpenDialog(parentWindow, openOptions, callback)

View File

@@ -68,6 +68,7 @@ class AtomWindow
@loaded = true
@setLoadSettings(loadSettings)
@env = loadSettings.env if loadSettings.env?
@browserWindow.focusOnWebView() if @isSpec
@browserWindow.temporaryState = {windowDimensions} if windowDimensions?
@@ -169,6 +170,9 @@ class AtomWindow
else
@browserWindow.once 'window:loaded', => @openLocations(locationsToOpen)
replaceEnvironment: (env) ->
@browserWindow.webContents.send 'environment', env
sendMessage: (message, detail) ->
@browserWindow.webContents.send 'message', message, detail

View File

@@ -1,6 +1,5 @@
autoUpdater = null
_ = require 'underscore-plus'
Config = require '../config'
{EventEmitter} = require 'events'
path = require 'path'
@@ -16,13 +15,10 @@ module.exports =
class AutoUpdateManager
_.extend @prototype, EventEmitter.prototype
constructor: (@version, @testMode, resourcePath) ->
constructor: (@version, @testMode, resourcePath, @config) ->
@state = IdleState
@iconPath = path.resolve(__dirname, '..', '..', 'resources', 'atom.png')
@feedUrl = "https://atom.io/api/updates?version=#{@version}"
@config = new Config({configDirPath: process.env.ATOM_HOME, resourcePath, enablePersistence: true})
@config.setSchema null, {type: 'object', properties: _.clone(require('../config-schema'))}
@config.load()
process.nextTick => @setupAutoUpdater()
setupAutoUpdater: ->
@@ -32,7 +28,8 @@ class AutoUpdateManager
{autoUpdater} = require 'electron'
autoUpdater.on 'error', (event, message) =>
@setState(ErrorState)
@setState(ErrorState, message)
@emitWindowEvent('update-error')
console.error "Error Downloading Update: #{message}"
autoUpdater.setFeedURL @feedUrl
@@ -82,14 +79,18 @@ class AutoUpdateManager
atomWindow.sendMessage(eventName, payload)
return
setState: (state) ->
setState: (state, errorMessage) ->
return if @state is state
@state = state
@errorMessage = errorMessage
@emit 'state-changed', @state
getState: ->
@state
getErrorMessage: ->
@errorMessage
scheduleUpdateCheck: ->
# Only schedule update check periodically if running in release version and
# and there is no existing scheduled update check.

View File

@@ -0,0 +1,36 @@
ChildProcess = require 'child_process'
# Spawn a command and invoke the callback when it completes with an error
# and the output from standard out.
#
# * `command` The underlying OS command {String} to execute.
# * `args` (optional) The {Array} with arguments to be passed to command.
# * `callback` (optional) The {Function} to call after the command has run. It will be invoked with arguments:
# * `error` (optional) An {Error} object returned by the command, `null` if no error was thrown.
# * `code` Error code returned by the command.
# * `stdout` The {String} output text generated by the command.
# * `stdout` The {String} output text generated by the command.
#
# Returns `undefined`.
exports.spawn = (command, args, callback) ->
stdout = ''
try
spawnedProcess = ChildProcess.spawn(command, args)
catch error
# Spawn can throw an error
process.nextTick -> callback?(error, stdout)
return
spawnedProcess.stdout.on 'data', (data) -> stdout += data
error = null
spawnedProcess.on 'error', (processError) -> error ?= processError
spawnedProcess.on 'close', (code, signal) ->
error ?= new Error("Command failed: #{signal ? code}") if code isnt 0
error?.code ?= code
error?.stdout ?= stdout
callback?(error, stdout)
# This is necessary if using Powershell 2 on Windows 7 to get the events to raise
# http://stackoverflow.com/questions/9155289/calling-powershell-from-nodejs
spawnedProcess.stdin.end()

View File

@@ -1,6 +1,8 @@
ChildProcess = require 'child_process'
fs = require 'fs-plus'
path = require 'path'
Spawner = require './spawner'
WinRegistry = require './win-registry'
WinPowerShell = require './win-powershell'
appFolder = path.resolve(process.execPath, '..')
rootAtomFolder = path.resolve(appFolder, '..')
@@ -10,118 +12,18 @@ exeName = path.basename(process.execPath)
if process.env.SystemRoot
system32Path = path.join(process.env.SystemRoot, 'System32')
regPath = path.join(system32Path, 'reg.exe')
powershellPath = path.join(system32Path, 'WindowsPowerShell', 'v1.0', 'powershell.exe')
setxPath = path.join(system32Path, 'setx.exe')
else
regPath = 'reg.exe'
powershellPath = 'powershell.exe'
setxPath = 'setx.exe'
# Registry keys used for context menu
fileKeyPath = 'HKCU\\Software\\Classes\\*\\shell\\Atom'
directoryKeyPath = 'HKCU\\Software\\Classes\\directory\\shell\\Atom'
backgroundKeyPath = 'HKCU\\Software\\Classes\\directory\\background\\shell\\Atom'
applicationsKeyPath = 'HKCU\\Software\\Classes\\Applications\\atom.exe'
environmentKeyPath = 'HKCU\\Environment'
# Spawn a command and invoke the callback when it completes with an error
# and the output from standard out.
spawn = (command, args, callback) ->
stdout = ''
try
spawnedProcess = ChildProcess.spawn(command, args)
catch error
# Spawn can throw an error
process.nextTick -> callback?(error, stdout)
return
spawnedProcess.stdout.on 'data', (data) -> stdout += data
error = null
spawnedProcess.on 'error', (processError) -> error ?= processError
spawnedProcess.on 'close', (code, signal) ->
error ?= new Error("Command failed: #{signal ? code}") if code isnt 0
error?.code ?= code
error?.stdout ?= stdout
callback?(error, stdout)
# This is necessary if using Powershell 2 on Windows 7 to get the events to raise
# http://stackoverflow.com/questions/9155289/calling-powershell-from-nodejs
spawnedProcess.stdin.end()
# Spawn reg.exe and callback when it completes
spawnReg = (args, callback) ->
spawn(regPath, args, callback)
# Spawn powershell.exe and callback when it completes
spawnPowershell = (args, callback) ->
# set encoding and execute the command, capture the output, and return it via .NET's console in order to have consistent UTF-8 encoding
# http://stackoverflow.com/questions/22349139/utf-8-output-from-powershell
# to address https://github.com/atom/atom/issues/5063
args[0] = """
[Console]::OutputEncoding=[System.Text.Encoding]::UTF8
$output=#{args[0]}
[Console]::WriteLine($output)
"""
args.unshift('-command')
args.unshift('RemoteSigned')
args.unshift('-ExecutionPolicy')
args.unshift('-noprofile')
spawn(powershellPath, args, callback)
# Spawn setx.exe and callback when it completes
spawnSetx = (args, callback) ->
spawn(setxPath, args, callback)
Spawner.spawn(setxPath, args, callback)
# Spawn the Update.exe with the given arguments and invoke the callback when
# the command completes.
spawnUpdate = (args, callback) ->
spawn(updateDotExe, args, callback)
# Install the Open with Atom explorer context menu items via the registry.
installContextMenu = (callback) ->
addToRegistry = (args, callback) ->
args.unshift('add')
args.push('/f')
spawnReg(args, callback)
installFileHandler = (callback) ->
args = ["#{applicationsKeyPath}\\shell\\open\\command", '/ve', '/d', "\"#{process.execPath}\" \"%1\""]
addToRegistry(args, callback)
installMenu = (keyPath, arg, callback) ->
args = [keyPath, '/ve', '/d', 'Open with Atom']
addToRegistry args, ->
args = [keyPath, '/v', 'Icon', '/d', "\"#{process.execPath}\""]
addToRegistry args, ->
args = ["#{keyPath}\\command", '/ve', '/d', "\"#{process.execPath}\" \"#{arg}\""]
addToRegistry(args, callback)
installMenu fileKeyPath, '%1', ->
installMenu directoryKeyPath, '%1', ->
installMenu backgroundKeyPath, '%V', ->
installFileHandler(callback)
# Get the user's PATH environment variable registry value.
getPath = (callback) ->
spawnPowershell ['[environment]::GetEnvironmentVariable(\'Path\',\'User\')'], (error, stdout) ->
if error?
return callback(error)
pathOutput = stdout.replace(/^\s+|\s+$/g, '')
callback(null, pathOutput)
# Uninstall the Open with Atom explorer context menu items via the registry.
uninstallContextMenu = (callback) ->
deleteFromRegistry = (keyPath, callback) ->
spawnReg(['delete', keyPath, '/f'], callback)
deleteFromRegistry fileKeyPath, ->
deleteFromRegistry directoryKeyPath, ->
deleteFromRegistry backgroundKeyPath, ->
deleteFromRegistry(applicationsKeyPath, callback)
Spawner.spawn(updateDotExe, args, callback)
# Add atom and apm to the PATH
#
@@ -160,7 +62,7 @@ addCommandsToPath = (callback) ->
installCommands (error) ->
return callback(error) if error?
getPath (error, pathEnv) ->
WinPowerShell.getPath (error, pathEnv) ->
return callback(error) if error?
pathSegments = pathEnv.split(/;+/).filter (pathSegment) -> pathSegment
@@ -171,7 +73,7 @@ addCommandsToPath = (callback) ->
# Remove atom and apm from the PATH
removeCommandsFromPath = (callback) ->
getPath (error, pathEnv) ->
WinPowerShell.getPath (error, pathEnv) ->
return callback(error) if error?
pathSegments = pathEnv.split(/;+/).filter (pathSegment) ->
@@ -220,7 +122,7 @@ exports.existsSync = ->
exports.restartAtom = (app) ->
if projectPath = global.atomApplication?.lastFocusedWindow?.projectPath
args = [projectPath]
app.once 'will-quit', -> spawn(path.join(binFolder, 'atom.cmd'), args)
app.once 'will-quit', -> Spawner.spawn(path.join(binFolder, 'atom.cmd'), args)
app.quit()
# Handle squirrel events denoted by --squirrel-* command line arguments.
@@ -228,19 +130,19 @@ exports.handleStartupEvent = (app, squirrelCommand) ->
switch squirrelCommand
when '--squirrel-install'
createShortcuts ->
installContextMenu ->
WinRegistry.installContextMenu ->
addCommandsToPath ->
app.quit()
true
when '--squirrel-updated'
updateShortcuts ->
installContextMenu ->
WinRegistry.installContextMenu ->
addCommandsToPath ->
app.quit()
true
when '--squirrel-uninstall'
removeShortcuts ->
uninstallContextMenu ->
WinRegistry.uninstallContextMenu ->
removeCommandsFromPath ->
app.quit()
true

View File

@@ -0,0 +1,39 @@
path = require 'path'
Spawner = require './spawner'
if process.env.SystemRoot
system32Path = path.join(process.env.SystemRoot, 'System32')
powershellPath = path.join(system32Path, 'WindowsPowerShell', 'v1.0', 'powershell.exe')
else
powershellPath = 'powershell.exe'
# Spawn powershell.exe and callback when it completes
spawnPowershell = (args, callback) ->
# Set encoding and execute the command, capture the output, and return it
# via .NET's console in order to have consistent UTF-8 encoding.
# See http://stackoverflow.com/questions/22349139/utf-8-output-from-powershell
# to address https://github.com/atom/atom/issues/5063
args[0] = """
[Console]::OutputEncoding=[System.Text.Encoding]::UTF8
$output=#{args[0]}
[Console]::WriteLine($output)
"""
args.unshift('-command')
args.unshift('RemoteSigned')
args.unshift('-ExecutionPolicy')
args.unshift('-noprofile')
Spawner.spawn(powershellPath, args, callback)
# Get the user's PATH environment variable registry value.
#
# * `callback` The {Function} to call after registry operation is done.
# It will be invoked with the same arguments provided by {Spawner.spawn}.
#
# Returns the user's path {String}.
exports.getPath = (callback) ->
spawnPowershell ['[environment]::GetEnvironmentVariable(\'Path\',\'User\')'], (error, stdout) ->
if error?
return callback(error)
pathOutput = stdout.replace(/^\s+|\s+$/g, '')
callback(null, pathOutput)

View File

@@ -0,0 +1,62 @@
path = require 'path'
Spawner = require './spawner'
if process.env.SystemRoot
system32Path = path.join(process.env.SystemRoot, 'System32')
regPath = path.join(system32Path, 'reg.exe')
else
regPath = 'reg.exe'
# Registry keys used for context menu
fileKeyPath = 'HKCU\\Software\\Classes\\*\\shell\\Atom'
directoryKeyPath = 'HKCU\\Software\\Classes\\directory\\shell\\Atom'
backgroundKeyPath = 'HKCU\\Software\\Classes\\directory\\background\\shell\\Atom'
applicationsKeyPath = 'HKCU\\Software\\Classes\\Applications\\atom.exe'
# Spawn reg.exe and callback when it completes
spawnReg = (args, callback) ->
Spawner.spawn(regPath, args, callback)
# Install the Open with Atom explorer context menu items via the registry.
#
# * `callback` The {Function} to call after registry operation is done.
# It will be invoked with the same arguments provided by {Spawner.spawn}.
#
# Returns `undefined`.
exports.installContextMenu = (callback) ->
addToRegistry = (args, callback) ->
args.unshift('add')
args.push('/f')
spawnReg(args, callback)
installFileHandler = (callback) ->
args = ["#{applicationsKeyPath}\\shell\\open\\command", '/ve', '/d', "\"#{process.execPath}\" \"%1\""]
addToRegistry(args, callback)
installMenu = (keyPath, arg, callback) ->
args = [keyPath, '/ve', '/d', 'Open with Atom']
addToRegistry args, ->
args = [keyPath, '/v', 'Icon', '/d', "\"#{process.execPath}\""]
addToRegistry args, ->
args = ["#{keyPath}\\command", '/ve', '/d', "\"#{process.execPath}\" \"#{arg}\""]
addToRegistry(args, callback)
installMenu fileKeyPath, '%1', ->
installMenu directoryKeyPath, '%1', ->
installMenu backgroundKeyPath, '%V', ->
installFileHandler(callback)
# Uninstall the Open with Atom explorer context menu items via the registry.
#
# * `callback` The {Function} to call after registry operation is done.
# It will be invoked with the same arguments provided by {Spawner.spawn}.
#
# Returns `undefined`.
exports.uninstallContextMenu = (callback) ->
deleteFromRegistry = (keyPath, callback) ->
spawnReg(['delete', keyPath, '/f'], callback)
deleteFromRegistry fileKeyPath, ->
deleteFromRegistry directoryKeyPath, ->
deleteFromRegistry backgroundKeyPath, ->
deleteFromRegistry(applicationsKeyPath, callback)

View File

@@ -47,6 +47,7 @@ class BufferedNodeProcess extends BufferedProcess
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)

View File

@@ -50,7 +50,7 @@ class BufferedProcess
options ?= {}
@command = command
# Related to joyent/node#2318
if process.platform is 'win32'
if process.platform is 'win32' and not options.shell?
# Quote all arguments and escapes inner quotes
if args?
cmdArgs = args.filter (arg) -> arg?
@@ -67,7 +67,7 @@ class BufferedProcess
cmdArgs.unshift("\"#{command}\"")
else
cmdArgs.unshift(command)
cmdArgs = ['/s', '/c', "\"#{cmdArgs.join(' ')}\""]
cmdArgs = ['/s', '/d', '/c', "\"#{cmdArgs.join(' ')}\""]
cmdOptions = _.clone(options)
cmdOptions.windowsVerbatimArguments = true
@spawn(@getCmdPath(), cmdArgs, cmdOptions)

View File

@@ -244,11 +244,14 @@ class CommandRegistry
(@selectorBasedListenersByCommandName[event.type] ? [])
.filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector)
.sort (a, b) -> a.compare(b)
listeners = listeners.concat(selectorBasedListeners)
listeners = selectorBasedListeners.concat(listeners)
matched = true if listeners.length > 0
for listener in listeners
# Call inline listeners first in reverse registration order,
# and selector-based listeners by specificity and reverse
# registration order.
for listener in listeners by -1
break if immediatePropagationStopped
listener.callback.call(currentTarget, dispatchedEvent)
@@ -271,8 +274,8 @@ class SelectorBasedListener
@sequenceNumber = SequenceCount++
compare: (other) ->
other.specificity - @specificity or
other.sequenceNumber - @sequenceNumber
@specificity - other.specificity or
@sequenceNumber - other.sequenceNumber
class InlineListener
constructor: (@callback) ->

View File

@@ -155,6 +155,10 @@ module.exports =
type: 'boolean'
default: true
description: 'Show line numbers in the editor\'s gutter.'
atomicSoftTabs:
type: 'boolean'
default: true
description: 'Skip over tab-length runs of leading whitespace when moving the cursor.'
autoIndent:
type: 'boolean'
default: true

View File

@@ -9,7 +9,7 @@ EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g
# where text can be inserted.
#
# Cursors belong to {TextEditor}s and have some metadata attached in the form
# of a {TextEditorMarker}.
# of a {DisplayMarker}.
module.exports =
class Cursor extends Model
screenPosition: null
@@ -129,7 +129,7 @@ class Cursor extends Model
Section: Cursor Position Details
###
# Public: Returns the underlying {TextEditorMarker} for the cursor.
# Public: Returns the underlying {DisplayMarker} for the cursor.
# Useful with overlay {Decoration}s.
getMarker: -> @marker
@@ -261,11 +261,11 @@ class Cursor extends Model
while columnCount > column and row > 0
columnCount -= column
column = @editor.lineTextForScreenRow(--row).length
column = @editor.lineLengthForScreenRow(--row)
columnCount-- # subtract 1 for the row move
column = column - columnCount
@setScreenPosition({row, column}, clip: 'backward')
@setScreenPosition({row, column}, clipDirection: 'backward')
# Public: Moves the cursor right one screen column.
#
@@ -280,7 +280,7 @@ class Cursor extends Model
else
{row, column} = @getScreenPosition()
maxLines = @editor.getScreenLineCount()
rowLength = @editor.lineTextForScreenRow(row).length
rowLength = @editor.lineLengthForScreenRow(row)
columnsRemainingInLine = rowLength - column
while columnCount > columnsRemainingInLine and row < maxLines - 1
@@ -288,11 +288,11 @@ class Cursor extends Model
columnCount-- # subtract 1 for the row move
column = 0
rowLength = @editor.lineTextForScreenRow(++row).length
rowLength = @editor.lineLengthForScreenRow(++row)
columnsRemainingInLine = rowLength
column = column + columnCount
@setScreenPosition({row, column}, clip: 'forward', wrapBeyondNewlines: true, wrapAtSoftNewlines: true)
@setScreenPosition({row, column}, clipDirection: 'forward')
# Public: Moves the cursor to the top of the buffer.
moveToTop: ->

View File

@@ -0,0 +1,181 @@
{Emitter} = require 'event-kit'
Model = require './model'
Decoration = require './decoration'
LayerDecoration = require './layer-decoration'
module.exports =
class DecorationManager extends Model
didUpdateDecorationsEventScheduled: false
updatedSynchronously: false
constructor: (@displayLayer, @defaultMarkerLayer) ->
super
@emitter = new Emitter
@decorationsById = {}
@decorationsByMarkerId = {}
@overlayDecorationsById = {}
@layerDecorationsByMarkerLayerId = {}
@decorationCountsByLayerId = {}
@layerUpdateDisposablesByLayerId = {}
observeDecorations: (callback) ->
callback(decoration) for decoration in @getDecorations()
@onDidAddDecoration(callback)
onDidAddDecoration: (callback) ->
@emitter.on 'did-add-decoration', callback
onDidRemoveDecoration: (callback) ->
@emitter.on 'did-remove-decoration', callback
onDidUpdateDecorations: (callback) ->
@emitter.on 'did-update-decorations', callback
setUpdatedSynchronously: (@updatedSynchronously) ->
decorationForId: (id) ->
@decorationsById[id]
getDecorations: (propertyFilter) ->
allDecorations = []
for markerId, decorations of @decorationsByMarkerId
allDecorations.push(decorations...) if decorations?
if propertyFilter?
allDecorations = allDecorations.filter (decoration) ->
for key, value of propertyFilter
return false unless decoration.properties[key] is value
true
allDecorations
getLineDecorations: (propertyFilter) ->
@getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line')
getLineNumberDecorations: (propertyFilter) ->
@getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line-number')
getHighlightDecorations: (propertyFilter) ->
@getDecorations(propertyFilter).filter (decoration) -> decoration.isType('highlight')
getOverlayDecorations: (propertyFilter) ->
result = []
for id, decoration of @overlayDecorationsById
result.push(decoration)
if propertyFilter?
result.filter (decoration) ->
for key, value of propertyFilter
return false unless decoration.properties[key] is value
true
else
result
decorationsForScreenRowRange: (startScreenRow, endScreenRow) ->
decorationsByMarkerId = {}
for marker in @defaultMarkerLayer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow])
if decorations = @decorationsByMarkerId[marker.id]
decorationsByMarkerId[marker.id] = decorations
decorationsByMarkerId
decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) ->
decorationsState = {}
for layerId of @decorationCountsByLayerId
layer = @displayLayer.getMarkerLayer(layerId)
for marker in layer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) when marker.isValid()
screenRange = marker.getScreenRange()
bufferRange = marker.getBufferRange()
rangeIsReversed = marker.isReversed()
if decorations = @decorationsByMarkerId[marker.id]
for decoration in decorations
decorationsState[decoration.id] = {
properties: decoration.properties
screenRange, bufferRange, rangeIsReversed
}
if layerDecorations = @layerDecorationsByMarkerLayerId[layerId]
for layerDecoration in layerDecorations
decorationsState["#{layerDecoration.id}-#{marker.id}"] = {
properties: layerDecoration.overridePropertiesByMarkerId[marker.id] ? layerDecoration.properties
screenRange, bufferRange, rangeIsReversed
}
decorationsState
decorateMarker: (marker, decorationParams) ->
throw new Error("Cannot decorate a destroyed marker") if marker.isDestroyed()
marker = @displayLayer.getMarkerLayer(marker.layer.id).getMarker(marker.id)
decoration = new Decoration(marker, this, decorationParams)
@decorationsByMarkerId[marker.id] ?= []
@decorationsByMarkerId[marker.id].push(decoration)
@overlayDecorationsById[decoration.id] = decoration if decoration.isType('overlay')
@decorationsById[decoration.id] = decoration
@observeDecoratedLayer(marker.layer)
@scheduleUpdateDecorationsEvent()
@emitter.emit 'did-add-decoration', decoration
decoration
decorateMarkerLayer: (markerLayer, decorationParams) ->
decoration = new LayerDecoration(markerLayer, this, decorationParams)
@layerDecorationsByMarkerLayerId[markerLayer.id] ?= []
@layerDecorationsByMarkerLayerId[markerLayer.id].push(decoration)
@observeDecoratedLayer(markerLayer)
@scheduleUpdateDecorationsEvent()
decoration
decorationsForMarkerId: (markerId) ->
@decorationsByMarkerId[markerId]
scheduleUpdateDecorationsEvent: ->
if @updatedSynchronously
@emitter.emit 'did-update-decorations'
return
unless @didUpdateDecorationsEventScheduled
@didUpdateDecorationsEventScheduled = true
process.nextTick =>
@didUpdateDecorationsEventScheduled = false
@emitter.emit 'did-update-decorations'
decorationDidChangeType: (decoration) ->
if decoration.isType('overlay')
@overlayDecorationsById[decoration.id] = decoration
else
delete @overlayDecorationsById[decoration.id]
didDestroyDecoration: (decoration) ->
{marker} = decoration
return unless decorations = @decorationsByMarkerId[marker.id]
index = decorations.indexOf(decoration)
if index > -1
decorations.splice(index, 1)
delete @decorationsById[decoration.id]
@emitter.emit 'did-remove-decoration', decoration
delete @decorationsByMarkerId[marker.id] if decorations.length is 0
delete @overlayDecorationsById[decoration.id]
@unobserveDecoratedLayer(marker.layer)
@scheduleUpdateDecorationsEvent()
didDestroyLayerDecoration: (decoration) ->
{markerLayer} = decoration
return unless decorations = @layerDecorationsByMarkerLayerId[markerLayer.id]
index = decorations.indexOf(decoration)
if index > -1
decorations.splice(index, 1)
delete @layerDecorationsByMarkerLayerId[markerLayer.id] if decorations.length is 0
@unobserveDecoratedLayer(markerLayer)
@scheduleUpdateDecorationsEvent()
observeDecoratedLayer: (layer) ->
@decorationCountsByLayerId[layer.id] ?= 0
if ++@decorationCountsByLayerId[layer.id] is 1
@layerUpdateDisposablesByLayerId[layer.id] = layer.onDidUpdate(@scheduleUpdateDecorationsEvent.bind(this))
unobserveDecoratedLayer: (layer) ->
if --@decorationCountsByLayerId[layer.id] is 0
@layerUpdateDisposablesByLayerId[layer.id].dispose()
delete @decorationCountsByLayerId[layer.id]
delete @layerUpdateDisposablesByLayerId[layer.id]

View File

@@ -11,7 +11,7 @@ translateDecorationParamsOldToNew = (decorationParams) ->
decorationParams.gutterName = 'line-number'
decorationParams
# Essential: Represents a decoration that follows a {TextEditorMarker}. A decoration is
# Essential: Represents a decoration that follows a {DisplayMarker}. A decoration is
# basically a visual representation of a marker. It allows you to add CSS
# classes to line numbers in the gutter, lines, and add selection-line regions
# around marked ranges of text.
@@ -25,7 +25,7 @@ translateDecorationParamsOldToNew = (decorationParams) ->
# decoration = editor.decorateMarker(marker, {type: 'line', class: 'my-line-class'})
# ```
#
# Best practice for destroying the decoration is by destroying the {TextEditorMarker}.
# Best practice for destroying the decoration is by destroying the {DisplayMarker}.
#
# ```coffee
# marker.destroy()
@@ -62,7 +62,7 @@ class Decoration
Section: Construction and Destruction
###
constructor: (@marker, @displayBuffer, properties) ->
constructor: (@marker, @decorationManager, properties) ->
@emitter = new Emitter
@id = nextId()
@setProperties properties
@@ -71,14 +71,14 @@ class Decoration
# Essential: Destroy this marker.
#
# If you own the marker, you should use {TextEditorMarker::destroy} which will destroy
# If you own the marker, you should use {DisplayMarker::destroy} which will destroy
# this decoration.
destroy: ->
return if @destroyed
@markerDestroyDisposable.dispose()
@markerDestroyDisposable = null
@destroyed = true
@displayBuffer.didDestroyDecoration(this)
@decorationManager.didDestroyDecoration(this)
@emitter.emit 'did-destroy'
@emitter.dispose()
@@ -149,8 +149,8 @@ class Decoration
oldProperties = @properties
@properties = translateDecorationParamsOldToNew(newProperties)
if newProperties.type?
@displayBuffer.decorationDidChangeType(this)
@displayBuffer.scheduleUpdateDecorationsEvent()
@decorationManager.decorationDidChangeType(this)
@decorationManager.scheduleUpdateDecorationsEvent()
@emitter.emit 'did-change-properties', {oldProperties, newProperties}
###
@@ -175,5 +175,5 @@ class Decoration
@properties.flashCount++
@properties.flashClass = klass
@properties.flashDuration = duration
@displayBuffer.scheduleUpdateDecorationsEvent()
@decorationManager.scheduleUpdateDecorationsEvent()
@emitter.emit 'did-flash'

File diff suppressed because it is too large Load Diff

View File

@@ -58,7 +58,7 @@ function getFromShell () {
function needsPatching (options = { platform: process.platform, env: process.env }) {
if (options.platform === 'darwin' && !options.env.PWD) {
let shell = getUserShell()
if (shell.endsWith('csh') || shell.endsWith('tcsh')) {
if (shell.endsWith('csh') || shell.endsWith('tcsh') || shell.endsWith('fish')) {
return false
}
return true
@@ -67,9 +67,20 @@ function needsPatching (options = { platform: process.platform, env: process.env
return false
}
// Fix for #11302 because `process.env` on Windows is a magic object that offers case-insensitive
// environment variable matching. By always cloning to `process.env` we prevent breaking the
// underlying functionality.
function clone (to, from) {
for (var key in to) {
delete to[key]
}
Object.assign(to, from)
}
function normalize (options = {}) {
if (options && options.env) {
process.env = options.env
clone(process.env, options.env)
}
if (!options.env) {
@@ -85,10 +96,18 @@ function normalize (options = {}) {
// in #4126. Retain the original in case someone needs it.
let shellEnv = getFromShell()
if (shellEnv && shellEnv.PATH) {
process._originalEnv = process.env
process.env = shellEnv
process._originalEnv = Object.assign({}, process.env)
clone(process.env, shellEnv)
}
}
}
export default { getFromShell, needsPatching, normalize }
function replace (env) {
if (!env || !env.PATH) {
return
}
clone(process.env, env)
}
export default { getFromShell, needsPatching, normalize, replace }

View File

@@ -1,83 +0,0 @@
{Point, Range} = require 'text-buffer'
# Represents a fold that collapses multiple buffer lines into a single
# line on the screen.
#
# Their creation is managed by the {DisplayBuffer}.
module.exports =
class Fold
id: null
displayBuffer: null
marker: null
constructor: (@displayBuffer, @marker) ->
@id = @marker.id
@displayBuffer.foldsByMarkerId[@marker.id] = this
@marker.onDidDestroy => @destroyed()
@marker.onDidChange ({isValid}) => @destroy() unless isValid
# Returns whether this fold is contained within another fold
isInsideLargerFold: ->
largestContainingFoldMarker = @displayBuffer.findFoldMarker(containsRange: @getBufferRange())
largestContainingFoldMarker and
not largestContainingFoldMarker.getRange().isEqual(@getBufferRange())
# Destroys this fold
destroy: ->
@marker.destroy()
# Returns the fold's {Range} in buffer coordinates
#
# includeNewline - A {Boolean} which, if `true`, includes the trailing newline
#
# Returns a {Range}.
getBufferRange: ({includeNewline}={}) ->
range = @marker.getRange()
if range.end.row > range.start.row and nextFold = @displayBuffer.largestFoldStartingAtBufferRow(range.end.row)
nextRange = nextFold.getBufferRange()
range = new Range(range.start, nextRange.end)
if includeNewline
range = range.copy()
range.end.row++
range.end.column = 0
range
getBufferRowRange: ->
{start, end} = @getBufferRange()
[start.row, end.row]
# Returns the fold's start row as a {Number}.
getStartRow: ->
@getBufferRange().start.row
# Returns the fold's end row as a {Number}.
getEndRow: ->
@getBufferRange().end.row
# Returns a {String} representation of the fold.
inspect: ->
"Fold(#{@getStartRow()}, #{@getEndRow()})"
# Retrieves the number of buffer rows spanned by the fold.
#
# Returns a {Number}.
getBufferRowCount: ->
@getEndRow() - @getStartRow() + 1
# Identifies if a fold is nested within a fold.
#
# fold - A {Fold} to check
#
# Returns a {Boolean}.
isContainedByFold: (fold) ->
@isContainedByRange(fold.getBufferRange())
updateDisplayBuffer: ->
unless @isInsideLargerFold()
@displayBuffer.updateScreenLines(@getStartRow(), @getEndRow() + 1, 0, updateMarkers: true)
destroyed: ->
delete @displayBuffer.foldsByMarkerId[@marker.id]
@updateDisplayBuffer()

View File

@@ -1,19 +1,7 @@
'use babel'
import fs from 'fs-plus'
import path from 'path'
import Git from 'nodegit'
import {Emitter, CompositeDisposable, Disposable} from 'event-kit'
const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.WT_TYPECHANGE | Git.Status.STATUS.INDEX_TYPECHANGE
const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW
const deletedStatusFlags = Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED
const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.INDEX_RENAMED | Git.Status.STATUS.INDEX_TYPECHANGE
const ignoredStatusFlags = 1 << 14 // TODO: compose this from libgit2 constants
const submoduleMode = 57344 // TODO: compose this from libgit2 constants
// Just using this for _.isEqual and _.object, we should impl our own here
import _ from 'underscore-plus'
import {Repository} from 'ohnogit'
import {CompositeDisposable, Disposable} from 'event-kit'
// For the most part, this class behaves the same as `GitRepository`, with a few
// notable differences:
@@ -28,32 +16,19 @@ export default class GitRepositoryAsync {
}
static get Git () {
return Git
return Repository.Git
}
// The name of the error thrown when an action is attempted on a destroyed
// repository.
static get DestroyedErrorName () {
return 'GitRepositoryAsync.destroyed'
return Repository.DestroyedErrorName
}
constructor (_path, options = {}) {
Git.enableThreadSafety()
this.repo = Repository.open(_path, options)
this.emitter = new Emitter()
this.subscriptions = new CompositeDisposable()
this.pathStatusCache = {}
// NB: These needs to happen before the following .openRepository call.
this.openedPath = _path
this._openExactPath = options.openExactPath || false
this.repoPromise = this.openRepository()
this.isCaseInsensitive = fs.isCaseInsensitive()
this.upstream = {}
this.submodules = {}
this._refreshingPromise = Promise.resolve()
let {refreshOnWindowFocus = true} = options
if (refreshOnWindowFocus) {
@@ -70,22 +45,26 @@ export default class GitRepositoryAsync {
}
}
// This exists to provide backwards compatibility.
get _refreshingPromise () {
return this.repo._refreshingPromise
}
get openedPath () {
return this.repo.openedPath
}
// Public: Destroy this {GitRepositoryAsync} object.
//
// This destroys any tasks and subscriptions and releases the underlying
// libgit2 repository handle. This method is idempotent.
destroy () {
if (this.emitter) {
this.emitter.emit('did-destroy')
this.emitter.dispose()
this.emitter = null
}
this.repo.destroy()
if (this.subscriptions) {
this.subscriptions.dispose()
this.subscriptions = null
}
this.repoPromise = null
}
// Event subscription
@@ -98,7 +77,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidDestroy (callback) {
return this.emitter.on('did-destroy', callback)
return this.repo.onDidDestroy(callback)
}
// Public: Invoke the given callback when a specific file's status has
@@ -113,7 +92,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeStatus (callback) {
return this.emitter.on('did-change-status', callback)
return this.repo.onDidChangeStatus(callback)
}
// Public: Invoke the given callback when a multiple files' statuses have
@@ -125,7 +104,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeStatuses (callback) {
return this.emitter.on('did-change-statuses', callback)
return this.repo.onDidChangeStatuses(callback)
}
// Repository details
@@ -142,13 +121,13 @@ export default class GitRepositoryAsync {
// Public: Returns a {Promise} which resolves to the {String} path of the
// repository.
getPath () {
return this.getRepo().then(repo => repo.path().replace(/\/$/, ''))
return this.repo.getPath()
}
// Public: Returns a {Promise} which resolves to the {String} working
// directory path of the repository.
getWorkingDirectory () {
return this.getRepo().then(repo => repo.workdir())
getWorkingDirectory (_path) {
return this.repo.getWorkingDirectory()
}
// Public: Returns a {Promise} that resolves to true if at the root, false if
@@ -157,8 +136,8 @@ export default class GitRepositoryAsync {
if (!this.project) return Promise.resolve(false)
if (!this.projectAtRoot) {
this.projectAtRoot = this.getRepo()
.then(repo => this.project.relativize(repo.workdir()) === '')
this.projectAtRoot = this.getWorkingDirectory()
.then(wd => this.project.relativize(wd) === '')
}
return this.projectAtRoot
@@ -170,8 +149,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Promise} which resolves to the relative {String} path.
relativizeToWorkingDirectory (_path) {
return this.getRepo()
.then(repo => this.relativize(_path, repo.workdir()))
return this.repo.relativizeToWorkingDirectory(_path)
}
// Public: Makes a path relative to the repository's working directory.
@@ -181,76 +159,13 @@ export default class GitRepositoryAsync {
//
// Returns the relative {String} path.
relativize (_path, workingDirectory) {
// The original implementation also handled null workingDirectory as it
// pulled it from a sync function that could return null. We require it
// to be passed here.
let openedWorkingDirectory
if (!_path || !workingDirectory) {
return _path
}
// If the opened directory and the workdir differ, this is a symlinked repo
// root, so we have to do all the checks below twice--once against the realpath
// and one against the opened path
const opened = this.openedPath.replace(/\/\.git$/, '')
if (path.relative(opened, workingDirectory) !== '') {
openedWorkingDirectory = opened
}
if (process.platform === 'win32') {
_path = _path.replace(/\\/g, '/')
} else {
if (_path[0] !== '/') {
return _path
}
}
workingDirectory = workingDirectory.replace(/\/$/, '')
// Depending on where the paths come from, they may have a '/private/'
// prefix. Standardize by stripping that out.
_path = _path.replace(/^\/private\//i, '/')
workingDirectory = workingDirectory.replace(/^\/private\//i, '/')
const originalPath = _path
const originalWorkingDirectory = workingDirectory
if (this.isCaseInsensitive) {
_path = _path.toLowerCase()
workingDirectory = workingDirectory.toLowerCase()
}
if (_path.indexOf(workingDirectory) === 0) {
return originalPath.substring(originalWorkingDirectory.length + 1)
} else if (_path === workingDirectory) {
return ''
}
if (openedWorkingDirectory) {
openedWorkingDirectory = openedWorkingDirectory.replace(/\/$/, '')
openedWorkingDirectory = openedWorkingDirectory.replace(/^\/private\//i, '/')
const originalOpenedWorkingDirectory = openedWorkingDirectory
if (this.isCaseInsensitive) {
openedWorkingDirectory = openedWorkingDirectory.toLowerCase()
}
if (_path.indexOf(openedWorkingDirectory) === 0) {
return originalPath.substring(originalOpenedWorkingDirectory.length + 1)
} else if (_path === openedWorkingDirectory) {
return ''
}
}
return _path
return this.repo.relativize(_path, workingDirectory)
}
// Public: Returns a {Promise} which resolves to whether the given branch
// exists.
hasBranch (branch) {
return this.getRepo()
.then(repo => repo.getBranch(branch))
.then(branch => branch != null)
.catch(_ => false)
return this.repo.hasBranch(branch)
}
// Public: Retrieves a shortened version of the HEAD reference value.
@@ -264,9 +179,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Promise} which resolves to a {String}.
getShortHead (_path) {
return this.getRepo(_path)
.then(repo => repo.getCurrentBranch())
.then(branch => branch.shorthand())
return this.repo.getShortHead(_path)
}
// Public: Is the given path a submodule in the repository?
@@ -276,15 +189,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} that resolves true if the given path is a submodule in
// the repository.
isSubmodule (_path) {
return this.getRepo()
.then(repo => repo.openIndex())
.then(index => Promise.all([index, this.relativizeToWorkingDirectory(_path)]))
.then(([index, relativePath]) => {
const entry = index.getByPath(relativePath)
if (!entry) return false
return entry.mode === submoduleMode
})
return this.repo.isSubmodule(_path)
}
// Public: Returns the number of commits behind the current branch is from the
@@ -298,16 +203,7 @@ export default class GitRepositoryAsync {
// * `ahead` The {Number} of commits ahead.
// * `behind` The {Number} of commits behind.
getAheadBehindCount (reference, _path) {
return this.getRepo(_path)
.then(repo => Promise.all([repo, repo.getBranch(reference)]))
.then(([repo, local]) => {
const upstream = Git.Branch.upstream(local)
return Promise.all([repo, local, upstream])
})
.then(([repo, local, upstream]) => {
return Git.Graph.aheadBehind(repo, local.target(), upstream.target())
})
.catch(_ => ({ahead: 0, behind: 0}))
return this.repo.getAheadBehindCount(reference, _path)
}
// Public: Get the cached ahead/behind commit counts for the current branch's
@@ -320,15 +216,7 @@ export default class GitRepositoryAsync {
// * `ahead` The {Number} of commits ahead.
// * `behind` The {Number} of commits behind.
getCachedUpstreamAheadBehindCount (_path) {
return this.relativizeToWorkingDirectory(_path)
.then(relativePath => this._submoduleForPath(_path))
.then(submodule => {
if (submodule) {
return submodule.getCachedUpstreamAheadBehindCount(_path)
} else {
return this.upstream
}
})
return this.repo.getCachedUpstreamAheadBehindCount(_path)
}
// Public: Returns the git configuration value specified by the key.
@@ -339,10 +227,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to the {String} git configuration value
// specified by the key.
getConfigValue (key, _path) {
return this.getRepo(_path)
.then(repo => repo.configSnapshot())
.then(config => config.getStringBuf(key))
.catch(_ => null)
return this.repo.getConfigValue(key, _path)
}
// Public: Get the URL for the 'origin' remote.
@@ -353,7 +238,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to the {String} origin url of the
// repository.
getOriginURL (_path) {
return this.getConfigValue('remote.origin.url', _path)
return this.repo.getOriginURL(_path)
}
// Public: Returns the upstream branch for the current HEAD, or null if there
@@ -365,9 +250,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to a {String} branch name such as
// `refs/remotes/origin/master`.
getUpstreamBranch (_path) {
return this.getRepo(_path)
.then(repo => repo.getCurrentBranch())
.then(branch => Git.Branch.upstream(branch))
return this.repo.getUpstreamBranch(_path)
}
// Public: Gets all the local and remote references.
@@ -380,23 +263,7 @@ export default class GitRepositoryAsync {
// * `remotes` An {Array} of remote reference names.
// * `tags` An {Array} of tag reference names.
getReferences (_path) {
return this.getRepo(_path)
.then(repo => repo.getReferences(Git.Reference.TYPE.LISTALL))
.then(refs => {
const heads = []
const remotes = []
const tags = []
for (const ref of refs) {
if (ref.isTag()) {
tags.push(ref.name())
} else if (ref.isRemote()) {
remotes.push(ref.name())
} else if (ref.isBranch()) {
heads.push(ref.name())
}
}
return {heads, remotes, tags}
})
return this.repo.getReferences(_path)
}
// Public: Get the SHA for the given reference.
@@ -408,9 +275,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to the current {String} SHA for the
// given reference.
getReferenceTarget (reference, _path) {
return this.getRepo(_path)
.then(repo => Git.Reference.nameToId(repo, reference))
.then(oid => oid.tostrS())
return this.repo.getReferenceTarget(reference, _path)
}
// Reading Status
@@ -423,9 +288,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to a {Boolean} that's true if the `path`
// is modified.
isPathModified (_path) {
return this.relativizeToWorkingDirectory(_path)
.then(relativePath => this._getStatus([relativePath]))
.then(statuses => statuses.some(status => status.isModified()))
return this.repo.isPathModified(_path)
}
// Public: Resolves true if the given path is new.
@@ -435,9 +298,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to a {Boolean} that's true if the `path`
// is new.
isPathNew (_path) {
return this.relativizeToWorkingDirectory(_path)
.then(relativePath => this._getStatus([relativePath]))
.then(statuses => statuses.some(status => status.isNew()))
return this.repo.isPathNew(_path)
}
// Public: Is the given path ignored?
@@ -447,12 +308,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to a {Boolean} that's true if the `path`
// is ignored.
isPathIgnored (_path) {
return this.getRepo()
.then(repo => {
const relativePath = this.relativize(_path, repo.workdir())
return Git.Ignore.pathIsIgnored(repo, relativePath)
})
.then(ignored => Boolean(ignored))
return this.repo.isPathIgnored(_path)
}
// Get the status of a directory in the repository's working directory.
@@ -463,18 +319,7 @@ export default class GitRepositoryAsync {
// value can be passed to {::isStatusModified} or {::isStatusNew} to get more
// information.
getDirectoryStatus (directoryPath) {
return this.relativizeToWorkingDirectory(directoryPath)
.then(relativePath => {
const pathspec = relativePath + '/**'
return this._getStatus([pathspec])
})
.then(statuses => {
return Promise.all(statuses.map(s => s.statusBit())).then(bits => {
return bits
.filter(b => b > 0)
.reduce((status, bit) => status | bit, 0)
})
})
return this.repo.getDirectoryStatus(directoryPath)
}
// Refresh the status bit for the given path.
@@ -487,27 +332,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to a {Number} which is the refreshed
// status bit for the path.
refreshStatusForPath (_path) {
let relativePath
return this.getRepo()
.then(repo => {
relativePath = this.relativize(_path, repo.workdir())
return this._getStatus([relativePath])
})
.then(statuses => {
const cachedStatus = this.pathStatusCache[relativePath] || 0
const status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT
if (status !== cachedStatus) {
if (status === Git.Status.STATUS.CURRENT) {
delete this.pathStatusCache[relativePath]
} else {
this.pathStatusCache[relativePath] = status
}
this.emitter.emit('did-change-status', {path: _path, pathStatus: status})
}
return status
})
return this.repo.refreshStatusForPath(_path)
}
// Returns a Promise that resolves to the status bit of a given path if it has
@@ -523,8 +348,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to a status {Number} or null if the
// path is not in the cache.
getCachedPathStatus (_path) {
return this.relativizeToWorkingDirectory(_path)
.then(relativePath => this.pathStatusCache[relativePath])
return this.repo.getCachedPathStatus(_path)
}
// Public: Get the cached statuses for the repository.
@@ -532,7 +356,7 @@ export default class GitRepositoryAsync {
// Returns an {Object} of {Number} statuses, keyed by {String} working
// directory-relative file names.
getCachedPathStatuses () {
return this.pathStatusCache
return this.repo.pathStatusCache
}
// Public: Returns true if the given status indicates modification.
@@ -541,7 +365,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Boolean} that's true if the `statusBit` indicates modification.
isStatusModified (statusBit) {
return (statusBit & modifiedStatusFlags) > 0
return this.repo.isStatusModified(statusBit)
}
// Public: Returns true if the given status indicates a new path.
@@ -550,7 +374,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Boolean} that's true if the `statusBit` indicates a new path.
isStatusNew (statusBit) {
return (statusBit & newStatusFlags) > 0
return this.repo.isStatusNew(statusBit)
}
// Public: Returns true if the given status indicates the path is staged.
@@ -560,7 +384,7 @@ export default class GitRepositoryAsync {
// Returns a {Boolean} that's true if the `statusBit` indicates the path is
// staged.
isStatusStaged (statusBit) {
return (statusBit & indexStatusFlags) > 0
return this.repo.isStatusStaged(statusBit)
}
// Public: Returns true if the given status indicates the path is ignored.
@@ -570,7 +394,7 @@ export default class GitRepositoryAsync {
// Returns a {Boolean} that's true if the `statusBit` indicates the path is
// ignored.
isStatusIgnored (statusBit) {
return (statusBit & ignoredStatusFlags) > 0
return this.repo.isStatusIgnored(statusBit)
}
// Public: Returns true if the given status indicates the path is deleted.
@@ -580,7 +404,7 @@ export default class GitRepositoryAsync {
// Returns a {Boolean} that's true if the `statusBit` indicates the path is
// deleted.
isStatusDeleted (statusBit) {
return (statusBit & deletedStatusFlags) > 0
return this.repo.isStatusDeleted(statusBit)
}
// Retrieving Diffs
@@ -596,35 +420,7 @@ export default class GitRepositoryAsync {
// * `added` The {Number} of added lines.
// * `deleted` The {Number} of deleted lines.
getDiffStats (_path) {
return this.getRepo()
.then(repo => Promise.all([repo, repo.getHeadCommit()]))
.then(([repo, headCommit]) => Promise.all([repo, headCommit.getTree()]))
.then(([repo, tree]) => {
const options = new Git.DiffOptions()
options.contextLines = 0
options.flags = Git.Diff.OPTION.DISABLE_PATHSPEC_MATCH
options.pathspec = this.relativize(_path, repo.workdir())
if (process.platform === 'win32') {
// Ignore eol of line differences on windows so that files checked in
// as LF don't report every line modified when the text contains CRLF
// endings.
options.flags |= Git.Diff.OPTION.IGNORE_WHITESPACE_EOL
}
return Git.Diff.treeToWorkdir(repo, tree, options)
})
.then(diff => this._getDiffLines(diff))
.then(lines => {
const stats = {added: 0, deleted: 0}
for (const line of lines) {
const origin = line.origin()
if (origin === Git.Diff.LINE.ADDITION) {
stats.added++
} else if (origin === Git.Diff.LINE.DELETION) {
stats.deleted++
}
}
return stats
})
return this.repo.getDiffStats(_path)
}
// Public: Retrieves the line diffs comparing the `HEAD` version of the given
@@ -639,25 +435,7 @@ export default class GitRepositoryAsync {
// * `oldLines` The {Number} of lines in the old hunk.
// * `newLines` The {Number} of lines in the new hunk
getLineDiffs (_path, text) {
let relativePath = null
return this.getRepo()
.then(repo => {
relativePath = this.relativize(_path, repo.workdir())
return repo.getHeadCommit()
})
.then(commit => commit.getEntry(relativePath))
.then(entry => entry.getBlob())
.then(blob => {
const options = new Git.DiffOptions()
options.contextLines = 0
if (process.platform === 'win32') {
// Ignore eol of line differences on windows so that files checked in
// as LF don't report every line modified when the text contains CRLF
// endings.
options.flags = Git.Diff.OPTION.IGNORE_WHITESPACE_EOL
}
return this._diffBlobToBuffer(blob, text, options)
})
return this.repo.getLineDiffs(_path, text)
}
// Checking Out
@@ -678,14 +456,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} that resolves or rejects depending on whether the
// method was successful.
checkoutHead (_path) {
return this.getRepo()
.then(repo => {
const checkoutOptions = new Git.CheckoutOptions()
checkoutOptions.paths = [this.relativize(_path, repo.workdir())]
checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH
return Git.Checkout.head(repo, checkoutOptions)
})
.then(() => this.refreshStatusForPath(_path))
return this.repo.checkoutHead(_path)
}
// Public: Checks out a branch in your repository.
@@ -696,17 +467,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Promise} that resolves if the method was successful.
checkoutReference (reference, create) {
return this.getRepo()
.then(repo => repo.checkoutBranch(reference))
.catch(error => {
if (create) {
return this._createBranch(reference)
.then(_ => this.checkoutReference(reference, false))
} else {
throw error
}
})
.then(_ => null)
return this.repo.checkoutReference(reference, create)
}
// Private
@@ -725,103 +486,10 @@ export default class GitRepositoryAsync {
return this.checkoutHead(filePath)
}
// Create a new branch with the given name.
// Refreshes the git status.
//
// * `name` The {String} name of the new branch.
//
// Returns a {Promise} which resolves to a {NodeGit.Ref} reference to the
// created branch.
_createBranch (name) {
return this.getRepo()
.then(repo => Promise.all([repo, repo.getHeadCommit()]))
.then(([repo, commit]) => repo.createBranch(name, commit))
}
// Get all the hunks in the diff.
//
// * `diff` The {NodeGit.Diff} whose hunks should be retrieved.
//
// Returns a {Promise} which resolves to an {Array} of {NodeGit.Hunk}.
_getDiffHunks (diff) {
return diff.patches()
.then(patches => Promise.all(patches.map(p => p.hunks()))) // patches :: Array<Patch>
.then(hunks => _.flatten(hunks)) // hunks :: Array<Array<Hunk>>
}
// Get all the lines contained in the diff.
//
// * `diff` The {NodeGit.Diff} use lines should be retrieved.
//
// Returns a {Promise} which resolves to an {Array} of {NodeGit.Line}.
_getDiffLines (diff) {
return this._getDiffHunks(diff)
.then(hunks => Promise.all(hunks.map(h => h.lines())))
.then(lines => _.flatten(lines)) // lines :: Array<Array<Line>>
}
// Diff the given blob and buffer with the provided options.
//
// * `blob` The {NodeGit.Blob}
// * `buffer` The {String} buffer.
// * `options` The {NodeGit.DiffOptions}
//
// Returns a {Promise} which resolves to an {Array} of {Object}s which have
// the following keys:
// * `oldStart` The {Number} of the old starting line.
// * `newStart` The {Number} of the new starting line.
// * `oldLines` The {Number} of old lines.
// * `newLines` The {Number} of new lines.
_diffBlobToBuffer (blob, buffer, options) {
const hunks = []
const hunkCallback = (delta, hunk, payload) => {
hunks.push({
oldStart: hunk.oldStart(),
newStart: hunk.newStart(),
oldLines: hunk.oldLines(),
newLines: hunk.newLines()
})
}
return Git.Diff.blobToBuffer(blob, null, buffer, null, options, null, null, hunkCallback, null)
.then(_ => hunks)
}
// Get the current branch and update this.branch.
//
// Returns a {Promise} which resolves to a {boolean} indicating whether the
// branch name changed.
_refreshBranch () {
return this.getRepo()
.then(repo => repo.getCurrentBranch())
.then(ref => ref.name())
.then(branchName => {
const changed = branchName !== this.branch
this.branch = branchName
return changed
})
}
// Refresh the cached ahead/behind count with the given branch.
//
// * `branchName` The {String} name of the branch whose ahead/behind should be
// used for the refresh.
//
// Returns a {Promise} which will resolve to a {boolean} indicating whether
// the ahead/behind count changed.
_refreshAheadBehindCount (branchName) {
return this.getAheadBehindCount(branchName)
.then(counts => {
const changed = !_.isEqual(counts, this.upstream)
this.upstream = counts
return changed
})
}
// Get the status for this repository.
//
// Returns a {Promise} that will resolve to an object of {String} paths to the
// {Number} status.
_getRepositoryStatus () {
// Returns a {Promise} which will resolve to {null} when refresh is complete.
refreshStatus () {
let projectPathsPromises = [Promise.resolve('')]
if (this.project) {
projectPathsPromises = this.project.getPaths()
@@ -830,162 +498,7 @@ export default class GitRepositoryAsync {
return Promise.all(projectPathsPromises)
.then(paths => paths.map(p => p.length > 0 ? p + '/**' : '*'))
.then(projectPaths => {
return this._getStatus(projectPaths.length > 0 ? projectPaths : null)
})
.then(statuses => {
const statusPairs = statuses.map(status => [status.path(), status.statusBit()])
return _.object(statusPairs)
})
}
// Get the status for the given submodule.
//
// * `submodule` The {GitRepositoryAsync} for the submodule.
//
// Returns a {Promise} which resolves to an {Object}, keyed by {String}
// repo-relative {Number} statuses.
async _getSubmoduleStatus (submodule) {
// At this point, we've called submodule._refreshSubmodules(), which would
// have refreshed the status on *its* submodules, etc. So we know that its
// cached path statuses are up-to-date.
//
// Now we just need to hoist those statuses into our repository by changing
// their paths to be relative to us.
const statuses = submodule.getCachedPathStatuses()
const repoRelativeStatuses = {}
const submoduleRepo = await submodule.getRepo()
const submoduleWorkDir = submoduleRepo.workdir()
for (const relativePath in statuses) {
const statusBit = statuses[relativePath]
const absolutePath = path.join(submoduleWorkDir, relativePath)
const repoRelativePath = await this.relativizeToWorkingDirectory(absolutePath)
repoRelativeStatuses[repoRelativePath] = statusBit
}
return repoRelativeStatuses
}
// Refresh the list of submodules in the repository.
//
// Returns a {Promise} which resolves to an {Object} keyed by {String}
// submodule names with {GitRepositoryAsync} values.
async _refreshSubmodules () {
const repo = await this.getRepo()
const submoduleNames = await repo.getSubmoduleNames()
for (const name of submoduleNames) {
const alreadyExists = Boolean(this.submodules[name])
if (alreadyExists) continue
const submodule = await Git.Submodule.lookup(repo, name)
const absolutePath = path.join(repo.workdir(), submodule.path())
const submoduleRepo = GitRepositoryAsync.open(absolutePath, {openExactPath: true, refreshOnWindowFocus: false})
this.submodules[name] = submoduleRepo
}
for (const name in this.submodules) {
const repo = this.submodules[name]
const gone = submoduleNames.indexOf(name) < 0
if (gone) {
repo.destroy()
delete this.submodules[name]
} else {
try {
await repo.refreshStatus()
} catch (e) {
// libgit2 will sometimes report submodules that aren't actually valid
// (https://github.com/libgit2/libgit2/issues/3580). So check the
// validity of the submodules by removing any that fail.
repo.destroy()
delete this.submodules[name]
}
}
}
return _.values(this.submodules)
}
// Get the status for the submodules in the repository.
//
// Returns a {Promise} that will resolve to an object of {String} paths to the
// {Number} status.
_getSubmoduleStatuses () {
return this._refreshSubmodules()
.then(repos => {
return Promise.all(repos.map(repo => this._getSubmoduleStatus(repo)))
})
.then(statuses => _.extend({}, ...statuses))
}
// Refresh the cached status.
//
// Returns a {Promise} which will resolve to a {boolean} indicating whether
// any statuses changed.
_refreshStatus () {
return Promise.all([this._getRepositoryStatus(), this._getSubmoduleStatuses()])
.then(([repositoryStatus, submoduleStatus]) => {
const statusesByPath = _.extend({}, repositoryStatus, submoduleStatus)
const changed = !_.isEqual(this.pathStatusCache, statusesByPath)
this.pathStatusCache = statusesByPath
return changed
})
}
// Refreshes the git status.
//
// Returns a {Promise} which will resolve to {null} when refresh is complete.
refreshStatus () {
const status = this._refreshStatus()
const branch = this._refreshBranch()
const aheadBehind = branch.then(() => this._refreshAheadBehindCount(this.branch))
this._refreshingPromise = this._refreshingPromise.then(_ => {
return Promise.all([status, branch, aheadBehind])
.then(([statusChanged, branchChanged, aheadBehindChanged]) => {
if (this.emitter && (statusChanged || branchChanged || aheadBehindChanged)) {
this.emitter.emit('did-change-statuses')
}
return null
})
// Because all these refresh steps happen asynchronously, it's entirely
// possible the repository was destroyed while we were working. In which
// case we should just swallow the error.
.catch(e => {
if (this._isDestroyed()) {
return null
} else {
return Promise.reject(e)
}
})
.catch(e => {
console.error('Error refreshing repository status:')
console.error(e)
return Promise.reject(e)
})
})
return this._refreshingPromise
}
// Get the submodule for the given path.
//
// Returns a {Promise} which resolves to the {GitRepositoryAsync} submodule or
// null if it isn't a submodule path.
async _submoduleForPath (_path) {
let relativePath = await this.relativizeToWorkingDirectory(_path)
for (const submodulePath in this.submodules) {
const submoduleRepo = this.submodules[submodulePath]
if (relativePath === submodulePath) {
return submoduleRepo
} else if (relativePath.indexOf(`${submodulePath}/`) === 0) {
relativePath = relativePath.substring(submodulePath.length + 1)
const innerSubmodule = await submoduleRepo._submoduleForPath(relativePath)
return innerSubmodule || submoduleRepo
}
}
return null
.then(pathspecs => this.repo.refreshStatus(pathspecs))
}
// Get the NodeGit repository for the given path.
@@ -996,16 +509,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Promise} which resolves to the {NodeGit.Repository}.
getRepo (_path) {
if (this._isDestroyed()) {
const error = new Error('Repository has been destroyed')
error.name = GitRepositoryAsync.DestroyedErrorName
return Promise.reject(error)
}
if (!_path) return this.repoPromise
return this._submoduleForPath(_path)
.then(submodule => submodule ? submodule.getRepo() : this.repoPromise)
return this.repo.getRepo(_path)
}
// Open a new instance of the underlying {NodeGit.Repository}.
@@ -1015,11 +519,7 @@ export default class GitRepositoryAsync {
//
// Returns the new {NodeGit.Repository}.
openRepository () {
if (this._openExactPath) {
return Git.Repository.open(this.openedPath)
} else {
return Git.Repository.openExt(this.openedPath, 0, '')
}
return this.repo.openRepository()
}
// Section: Private
@@ -1029,7 +529,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Boolean}.
_isDestroyed () {
return this.repoPromise == null
return this.repo._isDestroyed()
}
// Subscribe to events on the given buffer.
@@ -1055,26 +555,4 @@ export default class GitRepositoryAsync {
this.subscriptions.add(bufferSubscriptions)
}
// Get the status for the given paths.
//
// * `paths` The {String} paths whose status is wanted. If undefined, get the
// status for the whole repository.
//
// Returns a {Promise} which resolves to an {Array} of {NodeGit.StatusFile}
// statuses for the paths.
_getStatus (paths, repo) {
return this.getRepo()
.then(repo => {
const opts = {
flags: Git.Status.OPT.INCLUDE_UNTRACKED | Git.Status.OPT.RECURSE_UNTRACKED_DIRS
}
if (paths) {
opts.pathspec = paths
}
return repo.getStatusExt(opts)
})
}
}

View File

@@ -83,11 +83,12 @@ class GitRepository
asyncOptions.subscribeToBuffers = false
@async = GitRepositoryAsync.open(path, asyncOptions)
@statuses = {}
@upstream = {ahead: 0, behind: 0}
for submodulePath, submoduleRepo of @repo.submodules
submoduleRepo.upstream = {ahead: 0, behind: 0}
@statusesByPath = {}
{@project, @config, refreshOnWindowFocus} = options
refreshOnWindowFocus ?= true
@@ -317,7 +318,7 @@ class GitRepository
getDirectoryStatus: (directoryPath) ->
directoryPath = "#{@relativize(directoryPath)}/"
directoryStatus = 0
for path, status of @statuses
for path, status of _.extend({}, @async.getCachedPathStatuses(), @statusesByPath)
directoryStatus |= status if path.indexOf(directoryPath) is 0
directoryStatus
@@ -328,18 +329,26 @@ class GitRepository
# Returns a {Number} representing the status. This value can be passed to
# {::isStatusModified} or {::isStatusNew} to get more information.
getPathStatus: (path) ->
repo = @getRepo(path)
relativePath = @relativize(path)
# This is a bit particular. If a package calls `getPathStatus` like this:
# - change the file
# - getPathStatus
# - change the file
# - getPathStatus
# We need to preserve the guarantee that each call to `getPathStatus` will
# synchronously emit 'did-change-status'. So we need to keep a cache of the
# statuses found from this call.
currentPathStatus = @getCachedRelativePathStatus(relativePath) ? 0
# Trigger events emitted on the async repo as well
@async.refreshStatusForPath(path)
repo = @getRepo(path)
relativePath = @relativize(path)
currentPathStatus = @statuses[relativePath] ? 0
pathStatus = repo.getStatus(repo.relativize(path)) ? 0
pathStatus = 0 if repo.isStatusIgnored(pathStatus)
if pathStatus > 0
@statuses[relativePath] = pathStatus
else
delete @statuses[relativePath]
@statusesByPath[relativePath] = pathStatus
if currentPathStatus isnt pathStatus
@emitter.emit 'did-change-status', {path, pathStatus}
@@ -351,7 +360,11 @@ class GitRepository
#
# Returns a status {Number} or null if the path is not in the cache.
getCachedPathStatus: (path) ->
@statuses[@relativize(path)]
relativePath = @relativize(path)
@getCachedRelativePathStatus(relativePath)
getCachedRelativePathStatus: (relativePath) ->
@statusesByPath[relativePath] ? @async.getCachedPathStatuses()[relativePath]
# Public: Returns true if the given status indicates modification.
#
@@ -478,24 +491,34 @@ class GitRepository
#
# Returns a promise that resolves when the repository has been refreshed.
refreshStatus: ->
asyncRefresh = @async.refreshStatus()
statusesChanged = false
# Listen for `did-change-statuses` so we know if something changed. But we
# need to wait to propagate it until after we've set the branch and cleared
# the `statusesByPath` cache. So just set a flag, and we'll emit the event
# after refresh is done.
subscription = @async.onDidChangeStatuses ->
subscription?.dispose()
subscription = null
statusesChanged = true
asyncRefresh = @async.refreshStatus().then =>
subscription?.dispose()
subscription = null
@branch = @async?.branch
@statusesByPath = {}
if statusesChanged
@emitter.emit 'did-change-statuses'
syncRefresh = new Promise (resolve, reject) =>
@handlerPath ?= require.resolve('./repository-status-handler')
relativeProjectPaths = @project?.getPaths()
.map (path) => @relativize(path)
.map (path) -> if path.length > 0 then path + '/**' else '*'
@statusTask?.terminate()
@statusTask = Task.once @handlerPath, @getPath(), relativeProjectPaths, ({statuses, upstream, branch, submodules}) =>
statusesUnchanged = _.isEqual(statuses, @statuses) and
_.isEqual(upstream, @upstream) and
_.isEqual(branch, @branch) and
_.isEqual(submodules, @submodules)
@statuses = statuses
@statusTask = Task.once @handlerPath, @getPath(), ({upstream, submodules}) =>
@upstream = upstream
@branch = branch
@submodules = submodules
for submodulePath, submoduleRepo of @getRepo().submodules
@@ -503,7 +526,4 @@ class GitRepository
resolve()
unless statusesUnchanged
@emitter.emit 'did-change-statuses'
return Promise.all([asyncRefresh, syncRefresh])

View File

@@ -71,13 +71,13 @@ class Gutter
isVisible: ->
@visible
# Essential: Add a decoration that tracks a {TextEditorMarker}. When the marker moves,
# Essential: Add a decoration that tracks a {DisplayMarker}. When the marker moves,
# is invalidated, or is destroyed, the decoration will be updated to reflect
# the marker's state.
#
# ## Arguments
#
# * `marker` A {TextEditorMarker} you want this decoration to follow.
# * `marker` A {DisplayMarker} you want this decoration to follow.
# * `decorationParams` An {Object} representing the decoration. It is passed
# to {TextEditor::decorateMarker} as its `decorationParams` and so supports
# all options documented there.

View File

@@ -4,11 +4,12 @@ module.exports = ({blobStore}) ->
path = require 'path'
require './window'
{getWindowLoadSettings} = require './window-load-settings-helpers'
{ipcRenderer} = require 'electron'
{resourcePath, isSpec, devMode, env} = getWindowLoadSettings()
# Set baseline environment
environmentHelpers.normalize({env: env})
env = process.env
# Add application-specific exports to module search path.
exportsPath = path.join(resourcePath, 'exports')
@@ -25,7 +26,7 @@ module.exports = ({blobStore}) ->
applicationDelegate: new ApplicationDelegate,
configDirPath: process.env.ATOM_HOME
enablePersistence: true
env: env
env: process.env
})
atom.startEditorWindow().then ->
@@ -35,3 +36,6 @@ module.exports = ({blobStore}) ->
window.removeEventListener('focus', windowFocused)
setTimeout (-> document.querySelector('atom-workspace').focus()), 0
window.addEventListener('focus', windowFocused)
ipcRenderer.on('environment', (event, env) ->
environmentHelpers.replace(env)
)

View File

@@ -90,30 +90,36 @@ class LanguageMode
# Folds all the foldable lines in the buffer.
foldAll: ->
@unfoldAll()
foldedRowRanges = {}
for currentRow in [0..@buffer.getLastRow()] by 1
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
rowRange = [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow?
@editor.createFold(startRow, endRow)
continue if foldedRowRanges[rowRange]
@editor.foldBufferRowRange(startRow, endRow)
foldedRowRanges[rowRange] = true
return
# Unfolds all the foldable lines in the buffer.
unfoldAll: ->
for fold in @editor.displayBuffer.foldsIntersectingBufferRowRange(0, @buffer.getLastRow()) by -1
fold.destroy()
return
@editor.displayLayer.destroyAllFolds()
# Fold all comment and code blocks at a given indentLevel
#
# indentLevel - A {Number} indicating indentLevel; 0 based.
foldAllAtIndentLevel: (indentLevel) ->
@unfoldAll()
foldedRowRanges = {}
for currentRow in [0..@buffer.getLastRow()] by 1
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
rowRange = [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow?
continue if foldedRowRanges[rowRange]
# assumption: startRow will always be the min indent level for the entire range
if @editor.indentationForBufferRow(startRow) is indentLevel
@editor.createFold(startRow, endRow)
@editor.foldBufferRowRange(startRow, endRow)
foldedRowRanges[rowRange] = true
return
# Given a buffer row, creates a fold at it.
@@ -125,8 +131,8 @@ class LanguageMode
for currentRow in [bufferRow..0] by -1
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow? and startRow <= bufferRow <= endRow
fold = @editor.displayBuffer.largestFoldStartingAtBufferRow(startRow)
return @editor.createFold(startRow, endRow) unless fold
unless @editor.isFoldedAtBufferRow(startRow)
return @editor.foldBufferRowRange(startRow, endRow)
# Find the row range for a fold at a given bufferRow. Will handle comments
# and code.
@@ -140,19 +146,19 @@ class LanguageMode
rowRange
rowRangeForCommentAtBufferRow: (bufferRow) ->
return unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
return unless @editor.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
startRow = bufferRow
endRow = bufferRow
if bufferRow > 0
for currentRow in [bufferRow-1..0] by -1
break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
break unless @editor.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
startRow = currentRow
if bufferRow < @buffer.getLastRow()
for currentRow in [bufferRow+1..@buffer.getLastRow()] by 1
break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
break unless @editor.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
endRow = currentRow
return [startRow, endRow] if startRow isnt endRow
@@ -175,13 +181,13 @@ class LanguageMode
[bufferRow, foldEndRow]
isFoldableAtBufferRow: (bufferRow) ->
@editor.displayBuffer.tokenizedBuffer.isFoldableAtRow(bufferRow)
@editor.tokenizedBuffer.isFoldableAtRow(bufferRow)
# Returns a {Boolean} indicating whether the line at the given buffer
# row is a comment.
isLineCommentedAtBufferRow: (bufferRow) ->
return false unless 0 <= bufferRow <= @editor.getLastBufferRow()
@editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
@editor.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
# Find a row range for a 'paragraph' around specified bufferRow. A paragraph
# is a block of text bounded by and empty line or a block of text that is not
@@ -234,11 +240,11 @@ class LanguageMode
# Returns a {Number}.
suggestedIndentForBufferRow: (bufferRow, options) ->
line = @buffer.lineForRow(bufferRow)
tokenizedLine = @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow)
tokenizedLine = @editor.tokenizedBuffer.tokenizedLineForRow(bufferRow)
@suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
suggestedIndentForLineAtBufferRow: (bufferRow, line, options) ->
tokenizedLine = @editor.displayBuffer.tokenizedBuffer.buildTokenizedLineForRowWithText(bufferRow, line)
tokenizedLine = @editor.tokenizedBuffer.buildTokenizedLineForRowWithText(bufferRow, line)
@suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
suggestedIndentForTokenizedLineAtBufferRow: (bufferRow, line, tokenizedLine, options) ->

View File

@@ -7,7 +7,7 @@ nextId = -> idCounter++
# layer. Created via {TextEditor::decorateMarkerLayer}.
module.exports =
class LayerDecoration
constructor: (@markerLayer, @displayBuffer, @properties) ->
constructor: (@markerLayer, @decorationManager, @properties) ->
@id = nextId()
@destroyed = false
@markerLayerDestroyedDisposable = @markerLayer.onDidDestroy => @destroy()
@@ -19,7 +19,7 @@ class LayerDecoration
@markerLayerDestroyedDisposable.dispose()
@markerLayerDestroyedDisposable = null
@destroyed = true
@displayBuffer.didDestroyLayerDecoration(this)
@decorationManager.didDestroyLayerDecoration(this)
# Essential: Determine whether this decoration is destroyed.
#
@@ -44,11 +44,11 @@ class LayerDecoration
setProperties: (newProperties) ->
return if @destroyed
@properties = newProperties
@displayBuffer.scheduleUpdateDecorationsEvent()
@decorationManager.scheduleUpdateDecorationsEvent()
# Essential: Override the decoration properties for a specific marker.
#
# * `marker` The {TextEditorMarker} or {Marker} for which to override
# * `marker` The {DisplayMarker} or {Marker} for which to override
# properties.
# * `properties` An {Object} containing properties to apply to this marker.
# Pass `null` to clear the override.
@@ -58,4 +58,4 @@ class LayerDecoration
@overridePropertiesByMarkerId[marker.id] = properties
else
delete @overridePropertiesByMarkerId[marker.id]
@displayBuffer.scheduleUpdateDecorationsEvent()
@decorationManager.scheduleUpdateDecorationsEvent()

View File

@@ -93,9 +93,9 @@ class LineNumberGutterComponent extends TiledComponent
{target} = event
lineNumber = target.parentNode
if target.classList.contains('icon-right') and lineNumber.classList.contains('foldable')
if target.classList.contains('icon-right')
bufferRow = parseInt(lineNumber.getAttribute('data-buffer-row'))
if lineNumber.classList.contains('folded')
@editor.unfoldBufferRow(bufferRow)
else
else if lineNumber.classList.contains('foldable')
@editor.foldBufferRow(bufferRow)

View File

@@ -43,7 +43,7 @@ class LinesComponent extends TiledComponent
@domNode
shouldRecreateAllTilesOnUpdate: ->
@oldState.indentGuidesVisible isnt @newState.indentGuidesVisible or @newState.continuousReflow
@newState.continuousReflow
beforeUpdateSync: (state) ->
if @newState.maxHeight isnt @oldState.maxHeight
@@ -70,8 +70,6 @@ class LinesComponent extends TiledComponent
@cursorsComponent.updateSync(state)
@oldState.indentGuidesVisible = @newState.indentGuidesVisible
buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter, @domElementPool, @assert, @grammars})
buildEmptyState: ->
@@ -97,10 +95,14 @@ class LinesComponent extends TiledComponent
@presenter.setLineHeight(lineHeightInPixels)
@presenter.setBaseCharacterWidth(defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth)
lineNodeForLineIdAndScreenRow: (lineId, screenRow) ->
lineIdForScreenRow: (screenRow) ->
tile = @presenter.tileForRow(screenRow)
@getComponentForTile(tile)?.lineNodeForLineId(lineId)
@getComponentForTile(tile)?.lineIdForScreenRow(screenRow)
textNodesForLineIdAndScreenRow: (lineId, screenRow) ->
lineNodeForScreenRow: (screenRow) ->
tile = @presenter.tileForRow(screenRow)
@getComponentForTile(tile)?.textNodesForLineId(lineId)
@getComponentForTile(tile)?.lineNodeForScreenRow(screenRow)
textNodesForScreenRow: (screenRow) ->
tile = @presenter.tileForRow(screenRow)
@getComponentForTile(tile)?.textNodesForScreenRow(screenRow)

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