Merge branch 'master' into atom-shell

Conflicts:
	package.json
	script/bootstrap
	spec/app/window-spec.coffee
	src/app/window.coffee
This commit is contained in:
Cheng Zhao 2013-05-22 20:52:42 +08:00
commit 61f8878e05
80 changed files with 1834 additions and 1097 deletions

3
.gitmodules vendored
View File

@ -85,3 +85,6 @@
[submodule "vendor/packages/hyperlink-helper.tmbundle"]
path = vendor/packages/hyperlink-helper.tmbundle
url = https://github.com/textmate/hyperlink-helper.tmbundle
[submodule "vendor/apm"]
path = vendor/apm
url = https://github.com/github/apm.git

View File

@ -1,3 +1,7 @@
* Fixed: Not being able to disable packages from configuration UI.
* Fixed: Fuzzy finder showing poor results for entered text
* Improved: App icon
* Fixed: Fuzzy finder being empty sometimes
* Improved: App icon

View File

@ -50,8 +50,7 @@ task :install => [:build] do
raise "Missing directory for `atom` binary"
end
FileUtils.cp("#{ATOM_SRC_PATH}/atom.sh", cli_path)
FileUtils.chmod(0755, cli_path)
FileUtils.ln_s "#{ATOM_SRC_PATH}/atom.sh", cli_path, :force => true
Rake::Task["clone-default-bundles"].invoke()

0
atom.sh Normal file → Executable file
View File

View File

@ -106,7 +106,7 @@ describe "TokenizedBuffer.", ->
[languageMode, buffer] = []
beforeEach ->
editSession = benchmarkFixturesProject.buildEditSession('medium.coffee')
editSession = benchmarkFixturesProject.open('medium.coffee')
{ languageMode, buffer } = editSession
benchmark "construction", 20, ->

View File

@ -1,133 +0,0 @@
# Authoring Packages
A package can contain a variety of different resource types to change Atom's
behavior. The basic package layout is as follows (not every package will
have all of these directories):
```text
my-package/
lib/
config/
stylesheets/
keymaps/
snippets/
grammars/
package.json
index.coffee
```
**NOTE:** NPM behavior is partially implemented until we get a working Node.js
API built into Atom. The goal is to make Atom packages be a superset of NPM
packages
## package.json
Similar to npm packages, Atom packages can contain a `package.json` file in their
top-level directory. This file contains metadata about the package, such as the
path to its "main" module, library dependencies, and manifests specifying the
order in which its resources should be loaded.
## Source Code
If you want to extend Atom's behavior, your package should contain a single
top-level module, which you export from `index.coffee` or another file as
indicated by the `main` key in your `package.json` file. The remainder of your
code should be placed in the `lib` directory, and required from your top-level
file.
Your package's top-level module is a singleton object that manages the lifecycle
of your extensions to Atom. Even if your package creates ten different views and
appends them to different parts of the DOM, it's all managed from your top-level
object. Your package's top-level module should implement the following methods:
- `activate(rootView, state)` **Required**: This method is called when your
package is loaded. It is always passed the window's global `rootView`, and is
sometimes passed state data if the window has been reloaded and your module
implements the `serialize` method.
- `serialize()` **Optional**: This method is called when the window is shutting
down, allowing you to return JSON to represent the state of your component. When
the window is later restored, the data you returned will be passed to your
module's `activate` method so you can restore your view to where the user left
off.
- `deactivate()` **Optional**: This method is called when the window is shutting
down. If your package is watching any files or holding external resources in any
other way, release them here. If you're just subscribing to things on window
you don't need to worry because that's getting torn down anyway.
## A Simple Package Layout:
```text
my-package/
package.json # optional
index.coffee
lib/
my-package.coffee
```
`index.coffee`:
```coffeescript
module.exports = require "./lib/my-package"
```
`my-package/my-package.coffee`:
```coffeescript
module.exports =
activate: (rootView, state) -> # ...
deactivate: -> # ...
serialize: -> # ...
```
Beyond this simple contract, your package has full access to Atom's internal
API. Anything we call internally, you can call as well. Be aware that since we
are early in development, APIs are subject to change and we have not yet
established clear boundaries between what is public and what is private. Also,
Please collaborate with us if you need an API that doesn't exist. Our goal is
to build out Atom's API organically based on the needs of package authors like
you. See [Atom's built-in packages](https://github.com/github/atom/tree/master/src/packages)
for examples of Atom's API in action.
## Stylesheets
Stylesheets for your package should be placed in the `stylesheets` directory.
Any stylesheets in this directory will be loaded and attached to the DOM when
your package is activated. An optional `stylesheets` key in your `package.json`
can list the stylesheets by name in order to specify a load order; otherwise
stylesheets are loaded alphabetically.
## Keymaps
Keymaps are placed in the `keymaps` subdirectory. By default, all keymaps will be
loaded in alphabetical order unless there is a `keymaps` array in `package.json`
specifying which keymaps to load and in what order. It's a good idea to provide
default keymaps for your extension. They can be customized by users later. See
the (main keymaps documentation)[#keymaps] for more information on how keymaps
work.
## Snippets
An extension can supply snippets in a `snippets` directory as `.cson` or `.json`
files:
```coffeescript
".source.coffee .specs":
"Expect":
prefix: "ex"
body: "expect($1).to$2"
"Describe":
prefix: "de"
body: """
describe "${1:description}", ->
${2:body}
"""
```
A snippets file contains scope selectors at its top level. Each scope selector
contains a hash of snippets keyed by their name. Each snippet specifies a
`prefix` and a `body` key.
All files in the directory will be automatically loaded, unless the
`package.json` supplies a `snippets` key as a manifest. As with all scoped
items, snippets loaded later take precedence over earlier snippets when two
snippets match a scope with the same specificity.

View File

@ -1,22 +1,26 @@
## Command Panel
A partial implementation of the [Sam command language](http://man.cat-v.org/plan_9/1/sam)
The command panel contains a partial implementation of the [Sam command language](http://man.cat-v.org/plan_9/1/sam).
In addition, packages are free to design and define any scoped command.
*Examples*
Pop open the command line by hitting .
You can get a list of commands available to Atom (including any keybindings) by hitting `meta-p`.
`,` selects entire file
## Examples
`1,4` selects lines 1-4
`,` selects the entire file
`1,4` selects lines 1-4 in the current file
`/pattern` selects the first match after the cursor/selection
`s/pattern/replacement` replace first text matching pattern in current selection
`s/pattern/replacement` replaces the first text matching pattern in current selection
`s/pattern/replacement/g` replace all text matching pattern in current selection
`s/pattern/replacement/g` replaces all text matching pattern in current selection
`,s/pattern/replacement/g` replace all text matching pattern in file
`,s/pattern/replacement/g` replaces all text matching pattern in file
`1,4s/pattern/replacement` replace all text matching pattern in lines 1-4
`1,4s/pattern/replacement` replaces all text matching pattern in lines 1-4
`x/pattern` selects all matches in the current selections

View File

@ -1,17 +1,16 @@
## Wrap Guide
The `wrap-guide` extension places a vertical line in each editor at a certain
column to guide your formatting so lines do not exceed a certain width.
column to guide your formatting, so lines do not exceed a certain width.
By default the wrap-guide is placed at the 80th column.
By default, the wrap-guide is placed at the 80th column.
### Configuration
You can customize where the column is placed using the `wrapGuide.columns`
config option.
config option:
config.cson:
```coffee-cript
```coffeescript
"wrap-guide":
columns: [
{ pattern: "\.mm$", column: 200 },

138
docs/configuring-atom.md Normal file
View File

@ -0,0 +1,138 @@
# Configuration Settings
## Your .atom Directory
When you install Atom, an _.atom_ directory is created in your home directory.
If you press `meta-,`, that directory is opened in a new window. For the
time being, this serves as the primary interface for adjusting configuration
settings, adding and changing key bindings, tweaking styles, etc.
Atom loads configuration settings from the `config.cson` file in your _~/.atom_
directory, which contains CoffeeScript-style JSON:
```coffeescript
core:
hideGitIgnoredFiles: true
editor:
fontSize: 18
```
Configuration is broken into namespaces, which are defined by the config hash's
top-level keys. In addition to Atom's core components, each package may define
its own namespace.
## Glossary of Config Keys
- `core`
- `disablePackages`: An array of package names to disable
- `hideGitIgnoredFiles`: Whether files in the _.gitignore_ should be hidden
- `ignoredNames`: File names to ignore across all of Atom (not fully implemented)
- `themes`: An array of theme names to load, in cascading order
- `autosave`: Save a buffer when its view loses focus
- `editor`
- `autoIndent`: Enable/disable basic auto-indent (defaults to `true`)
- `autoIndentOnPaste`: Enable/disable auto-indented pasted text (defaults to `false`)
- `nonWordCharacters`: A string of non-word characters to define word boundaries
- `fontSize`: The editor font size
- `fontFamily`: The editor font family
- `invisibles`: Specify characters that Atom renders for invisibles in this hash
- `tab`: Hard tab characters
- `cr`: Carriage return (for Microsoft-style line endings)
- `eol`: `\n` characters
- `space`: Leading and trailing space characters
- `preferredLineLength`: Identifies the length of a line (defaults to `80`)
- `showInvisibles`: Whether to render placeholders for invisible characters (defaults to `false`)
- `fuzzyFinder`
- `ignoredNames`: Files to ignore *only* in the fuzzy-finder
- `whitespace`
- `ensureSingleTrailingNewline`: Whether to reduce multiple newlines to one at the end of files
- `wrapGuide`
- `columns`: Array of hashes with a `pattern` and `column` key to match the
the path of the current editor to a column position.
## Customizing Key Bindings
Atom keymaps work similarly to stylesheets. Just as stylesheets use selectors
to apply styles to elements, Atom keymaps use selectors to associate keystrokes
with events in specific contexts. Here's a small example, excerpted from Atom's
built-in keymaps:
```coffee-script
'.editor':
'enter': 'editor:newline'
".select-list .editor.mini":
'enter': 'core:confirm',
```
This keymap defines the meaning of `enter` in two different contexts. In a
normal editor, pressing `enter` emits the `editor:newline` event, which causes
the editor to insert a newline. But if the same keystroke occurs inside of a
select list's mini-editor, it instead emits the `core:confirm` event based on
the binding in the more-specific selector.
By default, any keymap files in your `~/.atom/keymaps` directory are loaded
in alphabetical order when Atom is started. They will always be loaded last,
giving you the chance to override bindings that are defined by Atom's core
keymaps or third-party packages.
## Changing The Theme
Atom comes bundled with two themes `atom-dark-*` and `atom-light-*`.
Because Atom themes are based on CSS, it's possible to have multiple themes
active at the same time. For example, you'll usually select a theme for the UI
and another theme for syntax highlighting. You can select themes by specifying
them in the `core.themes` array in your `config.cson`:
```coffee-script
core:
themes: ["atom-light-ui", "atom-light-syntax"]
# or, if the sun is going down:
# themes: ["atom-dark-ui", "atom-dark-syntax"]
```
You install new themes by placing them in the _~/.atom/themes_ directory. A
theme can be a CSS file, a directory containing multiple CSS files, or a
TextMate theme (either _.tmTheme_ or _.plist_).
## Installing Packages (Partially Implemented)
To install a package, clone it into the _~/.atom/packages_ directory. Atom will
also load grammars and snippets from TextMate bundles. If you want to disable a
package without removing it from the packages directory, insert its name into
_config.core.disabledPackages_:
```coffeescript
core:
disabledPackages: [
"fuzzy-finder",
"tree-view"
]
```
## Quick Personal Hacks
### user.coffee
When Atom finishes loading, it will evaluate _user.coffee_ in your _~/.atom_
directory, giving you a chance to run arbitrary personal CoffeeScript code to
make customizations. You have full access to Atom's API from code in this file.
Please refer to the [Atom Internals Guide](./internals/intro,md) for more information. If your
customizations become extensive, consider [creating a package](./packages/creating_packages.md).
### user.css
If you want to apply quick-and-dirty personal styling changes without creating
an entire theme that you intend to distribute, you can add styles to
_user.css_ in your _~/.atom_ directory.
For example, to change the color of the highlighted line number for the line that
contains the cursor, you could add the following style to _user.css_:
```css
.editor .line-number.cursor-line {
color: pink;
}
```

View File

@ -1,32 +1,33 @@
# Getting Started
Welcome to Atom. This documentation is intended to offer a basic introduction
of how to get productive with this editor. Then we'll delve into more details
about configuring, theming, and extending Atom.
Welcome to Atom. This documentation provides a basic introduction to being
productive with this editor. We'll then delve into more details about configuring,
theming, and extending Atom.
## The Command Palette
If there's one key-command you learn in Atom, it should be `meta-p` (`meta` is
If there's one key-command you must remember in Atom, it should be `meta-p` (`meta` is
synonymous with the ⌘ key). You can always hit `meta-p` to bring up a list of
commands that are relevant to the currently focused UI element. If there is a
key binding for a given command, it is also displayed. This is a great way to
explore the system and get to know the key commands interactively. If you'd like
to add or change a binding for a command, refer to the [key
bindings](#customizing-key-bindings) section to learn how.
to learn about adding or changing a binding for a command, refer to the [key
bindings](#customizing-key-bindings) section.
![Command Palette](http://f.cl.ly/items/32041o3w471F3C0F0V2O/Screen%20Shot%202013-02-13%20at%207.27.41%20PM.png)
## Basic Key Bindings
Remember you can always use `meta-p` to explore available commands and their
You can always use `meta-p` to explore available commands and their
bindings, but here's a list of a few useful commands.
- `meta-o` : open file/directory
- `meta-n` : new window
- `meta-o` : open a file or directory
- `meta-n` : open new window
- `meta-r` : reload the current window
- `meta-alt-ctrl-s` : run specs
- `meta-t` : open fuzzy file finder
- `meta-alt-ctrl-s` : run test specs
- `meta-t` : open file finder to navigate files in your project
- `meta-;` : open command prompt
- `meta-f` : open command prompt with `/`
- `meta-f` : open command prompt with `/` for a local file search
- `meta-g` : repeat the last local search
- `meta-shift-f` : open command prompt with `Xx/` for a project-wide search
- `meta-\` : focus/open tree view, or close it when it is focused
@ -52,8 +53,8 @@ issue so you can keep working.
The fastest way to find a file in your project is to use the fuzzy finder. Just
hit `meta-t` and start typing the name of the file you're looking for. If you
already have the file open and want to jump to it, hit `meta-b` to bring up a
searchable list of open buffers.
already have the file open as a tab and want to jump to it, hit `meta-b` to bring
up a searchable list of open buffers.
You can also use the tree view to navigate to a file. To open or move focus to
the tree view, hit `meta-\`. You can then navigate to a file and select it with
@ -73,28 +74,32 @@ To delete a file, select it in the tree view and hit `delete`.
#### Using the Command Line
Atom has a command line similar to editors Emacs and Vim, which is currently the
only interface for performing searches. Hitting `meta-f` will open the command
line prepopulated with the `/` command, which finds forward in the current
buffer from the location of the cursor. Pressing `meta-g` will repeat the
search. Hitting `meta-shift-f` will open the command line prepopulated with
`Xx/`, which is a composite command that performs a global search. The results
of the search will appear in the operation preview list, which you can focus
Atom has a command line similar to old-school editors such as emacs and vim. Nearly
every command has a key binding which you can discover with `meta-p`.
The command line is also (currently) the only place you can perform a search. Hitting
`meta-f` opens the command line and prepopulates it with the `/` command. This finds
text in the current buffer, starting at the location of the cursor. Pressing `meta-g`
repeats the search. Hitting `meta-shift-f` opens the command line and prepopulates
it with `Xx/`, which is a composite command that performs a global search. The results
of the search appear in the operation preview list, which you can focus
with `meta-:`.
Atom's command language is still under construction and is loosely based on
Atom's command language is still under construction, and is loosely based on
the [Sam editor](http://doc.cat-v.org/bell_labs/sam_lang_tutorial/) from the
Plan 9 operating system. It's similar to Ex mode in Vim, but is selection-based
Plan 9 operating system. It's similar to Ex mode in vim, but is selection-based
rather than line-based. It allows you to compose commands together in
interesting ways.
#### Navigating By Symbols
If you want to jump to a method, you can use the ctags-based symbols package.
The `meta-j` binding will open a list of all symbols in the current file. The
`meta-shift-j` binding will open a list of all symbols for the current project
based on a tags file. And `meta-.` will jump to the tag for the word currently
under the cursor. Make sure you have a tags file generated for the project for
The `meta-j` binding opens a list of all symbols in the current file. The
`meta-shift-j` binding opens a list of all symbols for the current project
based on a tags file. `meta-.` jumps to the tag for the word currently
under the cursor.
Make sure you have a tags file generated for the project for
the latter of these two bindings to work. Also, if you're editing CoffeeScript,
it's a good idea to update your `~/.ctags` file to understand the language. Here
is [a good example](https://github.com/kevinsawicki/dotfiles/blob/master/.ctags).
@ -106,8 +111,8 @@ command, as follows: `s/foo/bar/g`. Note that if you have a selection, the
replacement will only occur inside the selected text. An empty selection will
cause the replacement to occur across the whole buffer. If you want to run the
command on the whole buffer even if you have a selection, precede your
substitution with the `,` address, which specifies that the command following it
operate on the whole buffer.
substitution with the `,` address; this indicates that the following command should
run on the whole buffer.
### Split Panes
@ -125,12 +130,12 @@ planning to improve it soon.
### Soft-Wrap
If you want to toggle soft wrap, trigger the command from the command palette.
Press `meta-p` to open the palette, then type "wrap" to find the correct
Hit `meta-p` to open the palette, then type "wrap" to find the correct
command.
## Your .atom Directory
When you install Atom, a `.atom` directory is created in your home directory.
When you install Atom, an `.atom` directory is created in your home directory.
If you press `meta-,`, that directory will be opened in a new window. For the
time being, this will serve as the primary interface for adjusting configuration
settings, adding and changing key bindings, tweaking styles, etc.

View File

@ -49,10 +49,7 @@ the following way:
# basic key update
config.set("core.autosave", true)
# if you mutate a config key, you'll need to call `config.update` to inform
# observers of the change
config.get("fuzzyFinder.ignoredPaths").push "vendor"
config.update()
config.pushAtKeyPath("core.disabledPackages", "wrap-guide")
```
You can also use `setDefaults`, which will assign default values for keys that

View File

@ -1,10 +1,14 @@
getting-started.md
configuring-atom.md
built-in-packages/intro.md
built-in-packages/command-panel.md
built-in-packages/markdown-preview.md
built-in-packages/wrap-guide.md
authoring-themes.md
authoring-packages..md
packages/authoring-packages.md
packages/creating_a_package.md
packages/included_libraries.md
packages/package_json.md
themes/authoring-themes.md
internals/intro.md
internals/configuration.md
internals/keymaps.md

View File

@ -0,0 +1,211 @@
# Authoring Packages
Packages are at the core of Atom. Nearly everything outside of the main editor manipulation
is handled by a package. That includes "core" pieces like the command panel, status bar,
file tree, and more.
A package can contain a variety of different resource types to change Atom's
behavior. The basic package layout is as follows (though not every package will
have all of these directories):
```text
my-package/
lib/
stylesheets/
keymaps/
snippets/
grammars/
spec/
package.json
index.coffee
```
**NOTE:** NPM behavior is partially implemented until we get a working Node.js
API built into Atom. The goal is to make Atom packages be a superset of NPM
packages.
Below, we'll break down each directory. There's also [a tutorial](./creating_a_package.md)
on creating your first package.
## package.json
Similar to [npm packages](http://en.wikipedia.org/wiki/Npm_(software)), Atom packages
can contain a _package.json_ file in their top-level directory. This file contains metadata
about the package, such as the path to its "main" module, library dependencies,
and manifests specifying the order in which its resources should be loaded.
In addition to the regular [npm package.json keys](https://npmjs.org/doc/json.html)
available, Atom package.json files [have their own additions](./package_json.md).
## Source Code
If you want to extend Atom's behavior, your package should contain a single
top-level module, which you export from _index.coffee_ (or whichever file is
indicated by the `main` key in your _package.json_ file). The remainder of your
code should be placed in the `lib` directory, and required from your top-level
file.
Your package's top-level module is a singleton object that manages the lifecycle
of your extensions to Atom. Even if your package creates ten different views and
appends them to different parts of the DOM, it's all managed from your top-level
object.
Your package's top-level module should implement the following methods:
- `activate(rootView, state)`: This **required** method is called when your
package is loaded. It is always passed the window's global `rootView`, and is
sometimes passed state data if the window has been reloaded and your module
implements the `serialize` method. Use this to do initialization work when your
package is started (like setting up DOM elements or binding events).
- `serialize()`: This **optional** method is called when the window is shutting
down, allowing you to return JSON to represent the state of your component. When
the window is later restored, the data you returned is passed to your
module's `activate` method so you can restore your view to where the user left
off.
- `deactivate()`: This **optional** method is called when the window is shutting
down. If your package is watching any files or holding external resources in any
other way, release them here. If you're just subscribing to things on window,
you don't need to worry because that's getting torn down anyway.
### Simple Package Code
```text
my-package/
package.json # optional
index.coffee
lib/
my-package.coffee
```
`index.coffee`:
```coffeescript
module.exports = require "./lib/my-package"
```
`my-package/my-package.coffee`:
```coffeescript
module.exports =
activate: (rootView, state) -> # ...
deactivate: -> # ...
serialize: -> # ...
```
Beyond this simple contract, your package has full access to Atom's internal
API. Anything we call internally, you can call as well. Be aware that since we
are early in development, APIs are subject to change and we have not yet
established clear boundaries between what is public and what is private. Also,
please collaborate with us if you need an API that doesn't exist. Our goal is
to build out Atom's API organically based on the needs of package authors like
you.
See [Atom's built-in packages](https://github.com/github/atom/tree/master/src/packages)
for examples of Atom's API in action.
## Stylesheets
Stylesheets for your package should be placed in the _stylesheets_ directory.
Any stylesheets in this directory will be loaded and attached to the DOM when
your package is activated. Stylesheets can be written as CSS or LESS.
An optional `stylesheets` array in your _package.json_ can list the stylesheets by
name to specify a loading order; otherwise, stylesheets are loaded alphabetically.
## Keymaps
Keymaps are placed in the _keymaps_ subdirectory. It's a good idea to provide
default keymaps for your extension, especially if you're also adding a new command.
By default, all keymaps are loaded in alphabetical order. An optional `keymaps`
array in your _package.json_ can specify which keymaps to load and in what order.
See the [main keymaps documentation](../internals/keymaps.md) for more information on
how keymaps work.
## Snippets
An extension can supply language snippets in the _snippets_ directory. These can
be `.cson` or `.json` files. Here's an example:
```coffeescript
".source.coffee .specs":
"Expect":
prefix: "ex"
body: "expect($1).to$2"
"Describe":
prefix: "de"
body: """
describe "${1:description}", ->
${2:body}
"""
```
A snippets file contains scope selectors at its top level (`.source.coffee .spec`).
Each scope selector contains a hash of snippets keyed by their name (`Expect`, `Describe`).
Each snippet also specifies a `prefix` and a `body` key. The `prefix` represents
the first few letters to type before hitting the `tab` key to autocomplete. The
`body` defines the autofilled text. You can use placeholders like `$1`, `$2`, to indicate
regions in the body the user can navigate to every time they hit `tab`.
All files in the directory are automatically loaded, unless the
_package.json_ supplies a `snippets` key. As with all scoped
items, snippets loaded later take precedence over earlier snippets when two
snippets match a scope with the same specificity.
## Language Grammars
If you're developing a new language grammar, you'll want to place your file in
the _grammars_ directory. Each grammar is a pairing of two keys, `match` and
`captures`. `match` is a regular expression identifying the pattern to highlight,
while `captures` is a JSON representing what to do with each matching group.
For example:
```json
{
'match': '(?:^|\\s)(__[^_]+__)'
'captures':
'1': 'name': 'markup.bold.gfm'
}
```
This indicates that the first matching capture (`(__[^_]+__)`) should have the
`markup.bold.gfm` token applied to it.
To capture a single group, simply use the `name` key instead:
```json
{
'match': '^#{1,6}\\s+.+$'
'name': 'markup.heading.gfm'
}
```
This indicates that Markdown header lines (`#`, `##`, `###`) should be applied with
the `markup.heading.gfm` token.
More information about the significance of these tokens can be found in
[section 12.4 of the TextMate Manual](http://manual.macromates.com/en/language_grammars.html).
Your grammar should also include a `filetypes` array, which is a list of file extensions
your grammar supports:
```
'fileTypes': [
'markdown'
'md'
'mkd'
'mkdown'
'ron'
]
```
## Writing Tests
Your package **should** have tests, and if they're placed in the _spec_ directory,
they can be run by Atom.
Under the hood, [Jasmine](https://github.com/pivotal/jasmine) is being used to run
to execute the tests, so you can assume that any DSL available there is available
to your package as well.

View File

@ -0,0 +1,254 @@
# Creating Packages
Let's take a look at creating our first package.
Atom has a command you can enter that'll create a package for you:
`package-generator:generate`. Otherwise, you can hit `meta-p`, and start typing
"Package Generator." Once you activate this package, it'll ask you for a name for
your new package. Let's call ours _changer_.
Now, _changer_ is going to have a default set of folders and files created for us.
Hit `meta-R` to reload Atom, then hit `meta-p` and start typing "Changer." You'll
see a new `Changer:Toggle` command which, if selected, pops up a new message. So
far, so good!
In order to demonstrate the capabilities of Atom and its API, our Changer plugin
is going to do two things:
1. It'll show only modified files in the file tree
2. It'll append a new pane to the editor with some information about the modified
files
Let's get started!
## Changing Keybindings and Commands
Since Changer is primarily concerned with the file tree, let's write a keybinding
that works only when the tree is focused. Instead of using the default `toggle`,
our keybinding executes a new command called `magic`.
_keymaps/changer.cson_ can easily become this:
```cson
'.tree-view-scroller':
'ctrl-V': 'changer:magic'
```
Notice that the keybinding is called `ctrl-V`--that's actually `ctrl-shift-v`.
You can use capital letters to denote using `shift` for your binding.
`.tree-view-scroller` represents the parent container for the tree view. Keybindings
only work within the context of where they're entered. For example, hitting `ctrl-V`
anywhere other than tree won't do anything. You can map to `body` if you want
to scope to anywhere in Atom, or just `.editor` for the editor portion.
To bind keybindings to a command, we'll use the `rootView.command` method. This
takes a command name and executes a function in the code. For example:
```coffeescript
rootView.command "changer:magic", => @magic()
```
It's common practice to namespace your commands with your package name, and separate
it with a colon (`:`). Rename the existing `toggle` method to `magic` to get the
binding to work.
Reload the editor, click on the tree, hit your keybinding, and...nothing happens! What the heck?!
Open up the _package.json_ file, and notice the key that says `activationEvents`.
Basically, this tells Atom to not load a package until it hears a certain event.
Let's change the event to `changer:magic` and reload the editor.
Hitting the key binding on the tree now works!
## Working with styles
The next step is to hide elements in the tree that aren't modified. To do that,
we'll first try and get a list of files that have not changed.
All packages are able to use jQuery in their code. In fact, we have [a list of
some of the bundled libraries Atom provides by default](./included_libraries.md).
Let's bring in jQuery:
```coffeescript
$ = require 'jquery'
```
Now, we can query the tree to get us a list of every file that _wasn't_ modified:
```coffeescript
magic: ->
$('ol.entries li').each (i, el) ->
if !$(el).hasClass("modified")
console.log el
```
You can access the dev console by hitting `alt-meta-i`. When we execute the
`changer:magic` command, the browser console lists the items that are not being
modified. Let's add a class to each of these elements called `hide-me`:
```coffeescript
magic: ->
$('ol.entries li').each (i, el) ->
if !$(el).hasClass("modified")
$(el).addClass("hide-me")
```
With our newly added class, we can manipulate the visibility of the elements
with a simple stylesheet. Open up _changer.css_ in the _stylesheets_ directory,
and add a single entry:
```css
ol.entries .hide-me {
display: none;
}
```
Refresh atom, and run the `changer` command. You'll see all the non-changed files
disappear from the tree. There are a number of ways you can get the list back;
let's just naively iterate over the same elements and remove the class:
```coffeescript
magic: ->
$('ol.entries li').each (i, el) ->
if !$(el).hasClass("modified")
if !$(el).hasClass("hide-me")
$(el).addClass("hide-me")
else
$(el).removeClass("hide-me")
```
## Creating a New Pane
The next goal of this package is to append a pane to the Atom editor that lists
some information about the modified files.
To do that, we're going to first create a new class method called `content`. Every
package that extends from the `View` class can provide an optional class method
called `content`. The `content` method constructs the DOM that your package uses
as its UI. The principals of `content` are built entirely on [SpacePen](https://github.com/nathansobo/space-pen),
which we'll touch upon only briefly here.
Our display will simply be an unordered list of the file names, and their
modified times. Let's start by carving out a `div` to hold the filenames:
```coffeescript
@content: ->
@div class: 'modified-files-container', =>
@ul class: 'modified-files-list', outlet: 'modifiedFilesList', =>
@li 'Test'
@li 'Test2'
```
You can add any HTML5 attribute you like. `outlet` names the variable
your package can uses to manipulate the element directly. The fat pipe (`=>`) indicates
that the next set are nested children.
We'll add one more line to `magic` to make this pane appear:
```coffeescript
rootView.vertical.append(this)
```
If you hit the key command, you'll see a box appear right underneath the editor.
Success!
Before we populate this, let's apply some logic to toggle the pane off and on, just
like we did with the tree view:
```coffeescript
# toggles the pane
if @hasParent()
rootView.vertical.children().last().remove()
else
rootView.vertical.append(this)
```
There are about a hundred different ways to toggle a pane on and off, and this
might not be the most efficient one. If you know your package needs to be toggled
on and off more freely, it might be better to draw the UI during the initialization,
then immediately call `hide()` on the element to remove it from the view. You can
then swap between `show()` and `hide()`, and instead of forcing Atom to add and remove
the element as we're doing here, it'll just set a CSS property to control your package's
visibility.
You might have noticed that our two `li` elements aren't showing up. Let's set
a color on them so that they pop. Open up `changer.css` and add this CSS:
```css
ul.modified-files-list {
color: white;
}
```
Refresh Atom, hit the key combo, and see your brilliantly white test list.
## Calling Node.js Code
Since Atom is built on top of Node.js, you can call any of its libraries, including
other modules that your package requires.
We'll iterate through our resulting tree, and construct the path to our modified
file based on its depth in the tree:
```coffeescript
path = require 'path'
# ...
modifiedFiles = []
# for each single entry...
$('ol.entries li.file.modified span.name').each (i, el) ->
filePath = []
# ...grab its name...
filePath.unshift($(el).text())
# ... then find its parent directories, and grab their names
parents = $(el).parents('.directory.modified')
parents.each (i, el) ->
filePath.unshift($(el).find('div.header span.name').eq(0).text())
modifiedFilePath = path.join(project.rootDirectory.path, filePath.join(path.sep))
modifiedFiles.push modifiedFilePath
```
`modifiedFiles` is an array containing a list of our modified files. We're also using
the node.js [`path` library](http://nodejs.org/docs/latest/api/path.html) to get
the proper directory separator for our system.
Let's remove the two `@li` elements we added in `@content`, so that we can populate
our `modifiedFilesList` with real information. We'll do that by iterating over
`modifiedFiles`, accessing a file's last modified time, and appending it to
`modifiedFilesList`:
```coffeescript
# toggles the pane
if @hasParent()
rootView.vertical.children().last().remove()
else
for file in modifiedFiles
stat = fs.lstatSync(file)
mtime = stat.mtime
@modifiedFilesList.append("<li>#{file} - Modified at #{mtime}")
rootView.vertical.append(this)
```
When you toggle the modified files list, your pane is now populated with the filenames
and modified times of files in your project. You might notice that subsequent calls
to this command reduplicate information. We could provide an elegant way of rechecking
files already in the list, but for this demonstration, we'll just clear the `modifiedFilesList`
each time it's closed:
```coffeescript
# toggles the pane
if @hasParent()
@modifiedFilesList.empty()
rootView.vertical.children().last().remove()
else
for file in modifiedFiles
stat = fs.lstatSync(file)
mtime = stat.mtime
@modifiedFilesList.append("<li>#{file} - Modified at #{mtime}")
rootView.vertical.append(this)
```

View File

@ -0,0 +1,10 @@
# Included Libraries
In addition to core node.js modules, all packages can `require` the following popular
libraries into their packages:
* [SpacePen](https://github.com/nathansobo/space-pen) (as `require 'space-pen'`)
* [jQuery](http://jquery.com/) (as `require 'jquery'`)
* [Underscore](http://underscorejs.org/) (as `require 'underscore'`)
Additional libraries can be found by browsing Atom's _node_modules_ folder.

View File

@ -0,0 +1,18 @@
# package.json format
The following keys are available to your package's _package.json_ manifest file:
- `main` (**Required**): the path to the CoffeeScript file that's the entry point
to your package
- `stylesheets` (**Optional**): an Array of Strings identifying the order of the
stylesheets your package needs to load. If not specified, stylesheets in the _stylesheets_
directory are added alphabetically.
- `keymaps`(**Optional**): an Array of Strings identifying the order of the
key mappings your package needs to load. If not specified, mappings in the _keymaps_
directory are added alphabetically.
- `snippets` (**Optional**): an Array of Strings identifying the order of the
snippets your package needs to load. If not specified, snippets in the _snippets_
directory are added alphabetically.
- `activationEvents` (**Optional**): an Array of Strings identifying events that
trigger your package's activation. You can delay the loading of your package until
one of these events is trigged.

View File

@ -1,4 +1,4 @@
# Authoring A Theme
# Authoring Themes
If you understand CSS, you can write an Atom theme easily. Your theme can style
Atom's user interface, specify the appearance of syntax-highlighted code, or
@ -9,9 +9,9 @@ translate scope names to CSS classes. To theme Atom's user interface, take a
look at the existing light and dark themes for an example. Pressing `alt-meta-i`
and inspecting the Atom's markup directly can also be helpful.
The most basic theme is just a `.css` file. More complex themes occupy their own
The most basic theme is just a _.css_ file. More complex themes occupy their own
folder, which can contain multiple stylesheets along with an optional
`package.cson` file containing a manifest to control their load-order:
_package.cson_ file containing a manifest to control their load-order:
```text
~/.atom/themes/

View File

@ -140,7 +140,7 @@
- (id)initConfig {
_isConfig = true;
return [self initWithBootstrapScript:@"config-bootstrap" background:NO useBundleResourcePath:![self isDevMode]];
return [self initWithBootstrapScript:@"config-bootstrap" background:NO useBundleResourcePath:![self isDevFlagSpecified]];
}
- (void)addBrowserToView:(NSView *)view url:(const char *)url cefHandler:(CefRefPtr<AtomCefClient>)cefClient {

View File

@ -1,14 +1,17 @@
{
"name" : "atom",
"version" : "0.0.0",
"main" : "src/main.js",
"name": "atom",
"version": "0.0.0",
"main": "src/main.js",
"repository": {
"type": "git",
"url": "https://github.com/github/atom.git"
},
"dependencies": {
"coffee-script": "1.6.2",
"ctags": "0.3.0",
"oniguruma": "0.11.0",
"mkdirp": "0.3.5",
"git-utils": "0.15.0",
"git-utils": "0.17.0",
"underscore": "1.4.4",
"d3": "3.0.8",
"coffee-cache": "0.1.0",
@ -17,20 +20,20 @@
"nak": "0.2.12",
"spellchecker": "0.3.0",
"pathwatcher": "0.3.0",
"keytar": "0.4.0",
"plist": "git://github.com/nathansobo/node-plist.git",
"space-pen": "git://github.com/nathansobo/space-pen.git",
"space-pen": "1.0.0",
"less": "git://github.com/nathansobo/less.js.git",
"roaster": "0.0.3",
"roaster": "0.0.5",
"jqueryui-browser": "1.10.2-1",
"optimist": "0.4.0"
"optimist": "0.4.0",
"season": "0.7.0",
"humanize-plus": "1.1.0"
},
"devDependencies" : {
"biscotto" : "0.0.11"
"devDependencies": {
"biscotto": "0.0.11"
},
"private": true,
"scripts": {
"preinstall": "true"
}

View File

@ -21,13 +21,10 @@ exit_unless_npm_exists() {
exit_unless_xcode_exists
exit_unless_npm_exists
npm install npm --silent
export npm_config_disturl="https://gh-contractor-zcbenz.s3.amazonaws.com/atom-shell/dist"
export npm_config_target="0.10.5"
export npm_config_arch="ia32"
NODE_DIR="$HOME/.atom-shell-gyp"
HOME=$NODE_DIR ./node_modules/.bin/npm install --silent
git submodule --quiet sync
git submodule --quiet update --recursive --init
(cd vendor/apm && npm install .)
npm install --silent vendor/apm
./node_modules/.bin/apm install --silent

View File

@ -18,4 +18,4 @@ node --version > /dev/null 2>&1 || {
INPUT_FILE="${1}"
OUTPUT_FILE="${2}"
node_modules/.bin/coffee script/compile-cson.coffee -- "${INPUT_FILE}" "${OUTPUT_FILE}"
node_modules/.bin/csonc --root "${INPUT_FILE}" "${OUTPUT_FILE}"

View File

@ -1,23 +0,0 @@
fs = require 'fs'
{exec} = require 'child_process'
inputFile = process.argv[2]
unless inputFile?.length > 0
console.error("Input file must be first argument")
process.exit(1)
outputFile = process.argv[3]
unless outputFile?.length > 0
console.error("Output file must be second argument")
process.exit(1)
contents = fs.readFileSync(inputFile)?.toString() ? ''
exec "node_modules/.bin/coffee -bcp #{inputFile}", (error, stdout, stderr) ->
if error
console.error(error)
process.exit(1)
json = eval(stdout.toString()) ? {}
if json isnt Object(json)
console.error("CSON file does not contain valid JSON")
process.exit(1)
fs.writeFileSync(outputFile, JSON.stringify(json, null, 2))

View File

@ -1,3 +1,4 @@
fs = require 'fs'
fsUtils = require 'fs-utils'
describe "Config", ->
@ -7,6 +8,13 @@ describe "Config", ->
expect(config.get("foo.bar.baz")).toBe 42
expect(config.get("bogus.key.path")).toBeUndefined()
it "returns a deep clone of the key path's value", ->
config.set('value', array: [1, b: 2, 3])
retrievedValue = config.get('value')
retrievedValue.array[0] = 4
retrievedValue.array[1].b = 2.1
expect(config.get('value')).toEqual(array: [1, b: 2, 3])
describe ".set(keyPath, value)", ->
it "allows a key path's value to be written", ->
expect(config.set("foo.bar.baz", 42)).toBe 42
@ -33,6 +41,28 @@ describe "Config", ->
config.set('foo.changes', 1)
expect(config.settings.foo).toEqual {}
describe ".pushAtKeyPath(keyPath, value)", ->
it "pushes the given value to the array at the key path and updates observers", ->
config.set("foo.bar.baz", ["a"])
observeHandler = jasmine.createSpy "observeHandler"
config.observe "foo.bar.baz", observeHandler
observeHandler.reset()
expect(config.pushAtKeyPath("foo.bar.baz", "b")).toBe 2
expect(config.get("foo.bar.baz")).toEqual ["a", "b"]
expect(observeHandler).toHaveBeenCalledWith config.get("foo.bar.baz")
describe ".removeAtKeyPath(keyPath, value)", ->
it "removes the given value from the array at the key path and updates observers", ->
config.set("foo.bar.baz", ["a", "b", "c"])
observeHandler = jasmine.createSpy "observeHandler"
config.observe "foo.bar.baz", observeHandler
observeHandler.reset()
expect(config.removeAtKeyPath("foo.bar.baz", "b")).toEqual ["a", "c"]
expect(config.get("foo.bar.baz")).toEqual ["a", "c"]
expect(observeHandler).toHaveBeenCalledWith config.get("foo.bar.baz")
describe ".getPositiveInt(keyPath, defaultValue)", ->
it "returns the proper current or default value", ->
config.set('editor.preferredLineLength', 0)
@ -46,7 +76,7 @@ describe "Config", ->
describe ".save()", ->
beforeEach ->
spyOn(fsUtils, 'write')
spyOn(fs, 'writeFileSync')
jasmine.unspy config, 'save'
describe "when ~/.atom/config.json exists", ->
@ -57,11 +87,11 @@ describe "Config", ->
config.set("x.y.z", 3)
config.setDefaults("a.b", e: 4, f: 5)
fsUtils.write.reset()
fs.writeFileSync.reset()
config.save()
expect(fsUtils.write.argsForCall[0][0]).toBe(fsUtils.join(config.configDirPath, "config.json"))
writtenConfig = JSON.parse(fsUtils.write.argsForCall[0][1])
expect(fs.writeFileSync.argsForCall[0][0]).toBe(fsUtils.join(config.configDirPath, "config.json"))
writtenConfig = JSON.parse(fs.writeFileSync.argsForCall[0][1])
expect(writtenConfig).toEqual config.settings
describe "when ~/.atom/config.json doesn't exist", ->
@ -72,12 +102,12 @@ describe "Config", ->
config.set("x.y.z", 3)
config.setDefaults("a.b", e: 4, f: 5)
fsUtils.write.reset()
fs.writeFileSync.reset()
config.save()
expect(fsUtils.write.argsForCall[0][0]).toBe(fsUtils.join(config.configDirPath, "config.cson"))
expect(fs.writeFileSync.argsForCall[0][0]).toBe(fsUtils.join(config.configDirPath, "config.cson"))
CoffeeScript = require 'coffee-script'
writtenConfig = CoffeeScript.eval(fsUtils.write.argsForCall[0][1], bare: true)
writtenConfig = CoffeeScript.eval(fs.writeFileSync.argsForCall[0][1], bare: true)
expect(writtenConfig).toEqual config.settings
describe ".setDefaults(keyPath, defaults)", ->
@ -92,21 +122,6 @@ describe "Config", ->
expect(config.get("foo.quux.x")).toBe 0
expect(config.get("foo.quux.y")).toBe 1
describe ".update()", ->
it "updates observers if a value is mutated without the use of .set", ->
config.set("foo.bar.baz", ["a"])
observeHandler = jasmine.createSpy "observeHandler"
config.observe "foo.bar.baz", observeHandler
observeHandler.reset()
config.get("foo.bar.baz").push("b")
config.update()
expect(observeHandler).toHaveBeenCalledWith config.get("foo.bar.baz")
observeHandler.reset()
config.update()
expect(observeHandler).not.toHaveBeenCalled()
describe ".observe(keyPath)", ->
observeHandler = null

View File

@ -10,7 +10,7 @@ describe "EditSession", ->
beforeEach ->
atom.activatePackage('javascript.tmbundle', sync: true)
editSession = project.buildEditSession('sample.js', autoIndent: false)
editSession = project.open('sample.js', autoIndent: false)
buffer = editSession.buffer
lineLengths = buffer.getLines().map (line) -> line.length
@ -1011,194 +1011,6 @@ describe "EditSession", ->
editSession.insertText('holy cow')
expect(editSession.lineForScreenRow(2).fold).toBeUndefined()
describe "when auto-indent is enabled", ->
describe "when a single newline is inserted", ->
describe "when the newline is inserted on a line that starts a new level of indentation", ->
it "auto-indents the new line to one additional level of indentation beyond the preceding line", ->
editSession.setCursorBufferPosition([1, Infinity])
editSession.insertText('\n', autoIndent: true)
expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) + 1
describe "when the newline is inserted on a normal line", ->
it "auto-indents the new line to the same level of indentation as the preceding line", ->
editSession.setCursorBufferPosition([5, 13])
editSession.insertText('\n', autoIndent: true)
expect(editSession.indentationForBufferRow(6)).toBe editSession.indentationForBufferRow(5)
describe "when text without newlines is inserted", ->
describe "when the current line matches an auto-outdent pattern", ->
describe "when the preceding line matches an auto-indent pattern", ->
it "auto-decreases the indentation of the line to match that of the preceding line", ->
editSession.setCursorBufferPosition([2, 4])
editSession.insertText('\n', autoIndent: true)
editSession.setCursorBufferPosition([2, 4])
expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) + 1
editSession.insertText(' }', autoIndent: true)
expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1)
describe "when the preceding does not match an auto-indent pattern", ->
describe "when the inserted text is whitespace", ->
it "does not auto-decreases the indentation", ->
editSession.setCursorBufferPosition([12, 0])
editSession.insertText(' ', autoIndent: true)
expect(editSession.lineForBufferRow(12)).toBe ' };'
editSession.insertText('\t\t', autoIndent: true)
expect(editSession.lineForBufferRow(12)).toBe ' \t\t};'
describe "when the inserted text is non-whitespace", ->
it "auto-decreases the indentation of the line to be one level below that of the preceding line", ->
editSession.setCursorBufferPosition([3, Infinity])
editSession.insertText('\n', autoIndent: true)
expect(editSession.indentationForBufferRow(4)).toBe editSession.indentationForBufferRow(3)
editSession.insertText(' }', autoIndent: true)
expect(editSession.indentationForBufferRow(4)).toBe editSession.indentationForBufferRow(3) - 1
describe "when the current line does not match an auto-outdent pattern", ->
it "leaves the line unchanged", ->
editSession.setCursorBufferPosition([2, 4])
expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) + 1
editSession.insertText('foo', autoIndent: true)
expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) + 1
describe "when the `normalizeIndent` option is true", ->
describe "when the inserted text contains no newlines", ->
it "does not adjust the indentation level of the text", ->
editSession.setCursorBufferPosition([5, 2])
editSession.insertText("foo", normalizeIndent: true)
expect(editSession.lineForBufferRow(5)).toBe " foo current = items.shift();"
describe "when the inserted text contains newlines", ->
text = null
beforeEach ->
editSession.setCursorBufferPosition([2, Infinity])
text = [
" while (true) {"
" foo();"
" }"
" bar();"
].join('\n')
removeLeadingWhitespace = (text) -> text.replace(/^\s*/, '')
describe "when the cursor is preceded only by whitespace", ->
describe "when auto-indent is enabled", ->
describe "when the cursor's current column is less than the suggested indent level", ->
describe "when the indentBasis is inferred from the first line", ->
it "indents all lines relative to the suggested indent", ->
editSession.insertText('\n xx', autoIndent: true)
editSession.setCursorBufferPosition([3, 1])
editSession.insertText(text, normalizeIndent: true, autoIndent: true)
expect(editSession.lineForBufferRow(3)).toBe " while (true) {"
expect(editSession.lineForBufferRow(4)).toBe " foo();"
expect(editSession.lineForBufferRow(5)).toBe " }"
expect(editSession.lineForBufferRow(6)).toBe " bar();xx"
describe "when an indentBasis is provided", ->
it "indents all lines relative to the suggested indent", ->
editSession.insertText('\n xx')
editSession.setCursorBufferPosition([3, 1])
editSession.insertText(removeLeadingWhitespace(text), normalizeIndent: true, indentBasis: 2, autoIndent: true)
expect(editSession.lineForBufferRow(3)).toBe " while (true) {"
expect(editSession.lineForBufferRow(4)).toBe " foo();"
expect(editSession.lineForBufferRow(5)).toBe " }"
expect(editSession.lineForBufferRow(6)).toBe " bar();xx"
describe "when inserting on a line that has mixed tabs and whitespace in hard tabs mode (regression)", ->
it "correctly indents the inserted text", ->
editSession.softTabs = false
buffer.setText """
not indented
\tmixed indented
"""
editSession.setCursorBufferPosition([1, 0])
editSession.insertText(text, normalizeIndent: true, autoIndent: true)
expect(editSession.lineForBufferRow(1)).toBe "\t\t\twhile (true) {"
expect(editSession.lineForBufferRow(2)).toBe "\t\t\t\tfoo();"
expect(editSession.lineForBufferRow(3)).toBe "\t\t\t}"
expect(editSession.lineForBufferRow(4)).toBe "\t\tbar(); \tmixed indented"
describe "when inserting on a fractionally-indented line in hard tabs mode (regression)", ->
it "correctly indents the inserted text", ->
editSession.softTabs = false
buffer.setText """
not indented
fractional indentation
"""
editSession.setCursorBufferPosition([1, 0])
editSession.insertText(text, normalizeIndent: true, autoIndent: true)
expect(editSession.lineForBufferRow(1)).toBe "\t\twhile (true) {"
expect(editSession.lineForBufferRow(2)).toBe "\t\t\tfoo();"
expect(editSession.lineForBufferRow(3)).toBe "\t\t}"
expect(editSession.lineForBufferRow(4)).toBe "\tbar(); fractional indentation"
describe "when the cursor's current column is greater than the suggested indent level", ->
describe "when the indentBasis is inferred from the first line", ->
it "preserves the current indent level, indenting all lines relative to it", ->
editSession.insertText('\n ')
editSession.insertText(text, normalizeIndent: true)
expect(editSession.lineForBufferRow(3)).toBe " while (true) {"
expect(editSession.lineForBufferRow(4)).toBe " foo();"
expect(editSession.lineForBufferRow(5)).toBe " }"
expect(editSession.lineForBufferRow(6)).toBe " bar();"
describe "when an indentBasis is provided", ->
it "preserves the current indent level, indenting all lines relative to it", ->
editSession.insertText('\n ')
editSession.insertText(removeLeadingWhitespace(text), normalizeIndent: true, indentBasis: 2)
expect(editSession.lineForBufferRow(3)).toBe " while (true) {"
expect(editSession.lineForBufferRow(4)).toBe " foo();"
expect(editSession.lineForBufferRow(5)).toBe " }"
expect(editSession.lineForBufferRow(6)).toBe " bar();"
describe "if auto-indent is disabled", ->
describe "when the indentBasis is inferred from the first line", ->
it "always normalizes indented lines to the cursor's current indentation level", ->
editSession.insertText('\n ')
editSession.insertText(text, normalizeIndent: true)
expect(editSession.lineForBufferRow(3)).toBe " while (true) {"
expect(editSession.lineForBufferRow(4)).toBe " foo();"
expect(editSession.lineForBufferRow(5)).toBe " }"
expect(editSession.lineForBufferRow(6)).toBe "bar();"
describe "when an indentBasis is provided", ->
it "always normalizes indented lines to the cursor's current indentation level", ->
editSession.insertText('\n ')
editSession.insertText(removeLeadingWhitespace(text), normalizeIndent: true, indentBasis: 2)
expect(editSession.lineForBufferRow(3)).toBe " while (true) {"
expect(editSession.lineForBufferRow(4)).toBe " foo();"
expect(editSession.lineForBufferRow(5)).toBe " }"
describe "when the cursor is preceded by non-whitespace characters", ->
describe "when the indentBasis is inferred from the first line", ->
it "normalizes the indentation level of all lines based on the level of the existing first line", ->
editSession.buffer.delete([[2, 0], [2, 2]])
editSession.insertText(text, normalizeIndent:true)
expect(editSession.lineForBufferRow(2)).toBe " if (items.length <= 1) return items;while (true) {"
expect(editSession.lineForBufferRow(3)).toBe " foo();"
expect(editSession.lineForBufferRow(4)).toBe " }"
expect(editSession.lineForBufferRow(5)).toBe "bar();"
describe "when an indentBasis is provided", ->
it "normalizes the indentation level of all lines based on the level of the existing first line", ->
editSession.buffer.delete([[2, 0], [2, 2]])
editSession.insertText(removeLeadingWhitespace(text), normalizeIndent:true, indentBasis: 2)
expect(editSession.lineForBufferRow(2)).toBe " if (items.length <= 1) return items;while (true) {"
expect(editSession.lineForBufferRow(3)).toBe " foo();"
expect(editSession.lineForBufferRow(4)).toBe " }"
expect(editSession.lineForBufferRow(5)).toBe "bar();"
describe ".insertNewline()", ->
describe "when there is a single cursor", ->
describe "when the cursor is at the beginning of a line", ->
@ -1779,17 +1591,6 @@ describe "EditSession", ->
expect(editSession.buffer.lineForRow(0)).toBe "var first = function () {"
expect(buffer.lineForRow(1)).toBe " var first = function(items) {"
it "preserves the indent level when copying and pasting multiple lines", ->
editSession.setSelectedBufferRange([[4, 4], [7, 5]])
editSession.copySelectedText()
editSession.setCursorBufferPosition([10, 0])
editSession.pasteText(autoIndent: true)
expect(editSession.lineForBufferRow(10)).toBe " while(items.length > 0) {"
expect(editSession.lineForBufferRow(11)).toBe " current = items.shift();"
expect(editSession.lineForBufferRow(12)).toBe " current < pivot ? left.push(current) : right.push(current);"
expect(editSession.lineForBufferRow(13)).toBe " }"
describe ".indentSelectedRows()", ->
describe "when nothing is selected", ->
describe "when softTabs is enabled", ->
@ -1961,7 +1762,7 @@ describe "EditSession", ->
it "does not explode if the current language mode has no comment regex", ->
editSession.destroy()
editSession = project.buildEditSession(null, autoIndent: false)
editSession = project.open(null, autoIndent: false)
editSession.setSelectedBufferRange([[4, 5], [4, 5]])
editSession.toggleLineCommentsInSelection()
expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {"
@ -2348,13 +2149,13 @@ describe "EditSession", ->
describe "soft-tabs detection", ->
it "assign soft / hard tabs based on the contents of the buffer, or uses the default if unknown", ->
editSession = project.buildEditSession('sample.js', softTabs: false)
editSession = project.open('sample.js', softTabs: false)
expect(editSession.softTabs).toBeTruthy()
editSession = project.buildEditSession('sample-with-tabs.coffee', softTabs: true)
editSession = project.open('sample-with-tabs.coffee', softTabs: true)
expect(editSession.softTabs).toBeFalsy()
editSession = project.buildEditSession(null, softTabs: false)
editSession = project.open(null, softTabs: false)
expect(editSession.softTabs).toBeFalsy()
describe ".indentLevelForLine(line)", ->
@ -2383,7 +2184,7 @@ describe "EditSession", ->
jsGrammar = syntax.selectGrammar('a.js')
syntax.removeGrammar(jsGrammar)
editSession = project.buildEditSession('sample.js', autoIndent: false)
editSession = project.open('sample.js', autoIndent: false)
expect(editSession.getGrammar()).toBe syntax.nullGrammar
expect(editSession.lineForScreenRow(0).tokens.length).toBe 1
@ -2392,82 +2193,219 @@ describe "EditSession", ->
expect(editSession.lineForScreenRow(0).tokens.length).toBeGreaterThan 1
describe "auto-indent", ->
copyText = (text, {startColumn}={}) ->
startColumn ?= 0
editSession.setCursorBufferPosition([0, 0])
editSession.insertText(text)
numberOfNewlines = text.match(/\n/g)?.length
endColumn = text.match(/[^\n]*$/)[0]?.length
editSession.getSelection().setBufferRange([[0,startColumn], [numberOfNewlines,endColumn]])
editSession.cutSelectedText()
describe "editor.autoIndent", ->
it "auto-indents newlines if editor.autoIndent is true", ->
config.set("editor.autoIndent", undefined)
editSession.setCursorBufferPosition([1, 30])
editSession.insertText("\n")
expect(editSession.lineForBufferRow(2)).toBe " "
describe "when editor.autoIndent is false (default)", ->
describe "when `indent` is triggered", ->
it "does not auto-indent the line", ->
editSession.setCursorBufferPosition([1, 30])
editSession.insertText("\n ")
expect(editSession.lineForBufferRow(2)).toBe " "
it "does not auto-indent newlines if editor.autoIndent is false", ->
config.set("editor.autoIndent", false)
editSession.setCursorBufferPosition([1, 30])
editSession.insertText("\n")
expect(editSession.lineForBufferRow(2)).toBe ""
config.set("editor.autoIndent", false)
editSession.indent()
expect(editSession.lineForBufferRow(2)).toBe " "
it "auto-indents calls to `indent` if editor.autoIndent is true", ->
config.set("editor.autoIndent", true)
editSession.setCursorBufferPosition([1, 30])
editSession.insertText("\n ")
expect(editSession.lineForBufferRow(2)).toBe " "
editSession.indent()
expect(editSession.lineForBufferRow(2)).toBe " "
describe "when editor.autoIndent is true", ->
beforeEach ->
config.set("editor.autoIndent", true)
it "does not auto-indents calls to `indent` if editor.autoIndent is false", ->
config.set("editor.autoIndent", false)
editSession.setCursorBufferPosition([1, 30])
editSession.insertText("\n ")
expect(editSession.lineForBufferRow(2)).toBe " "
editSession.indent()
expect(editSession.lineForBufferRow(2)).toBe " "
describe "when `indent` is triggered", ->
it "auto-indents the line", ->
editSession.setCursorBufferPosition([1, 30])
editSession.insertText("\n ")
expect(editSession.lineForBufferRow(2)).toBe " "
it "auto-indents selection when autoIndent is called", ->
editSession.setCursorBufferPosition([2, 0])
editSession.insertText(" 0\n 2\n4\n")
config.set("editor.autoIndent", true)
editSession.indent()
expect(editSession.lineForBufferRow(2)).toBe " "
editSession.setSelectedBufferRange([[2, 0], [4, 0]])
editSession.autoIndentSelectedRows()
describe "when a newline is added", ->
describe "when the line preceding the newline adds a new level of indentation", ->
it "indents the newline to one additional level of indentation beyond the preceding line", ->
editSession.setCursorBufferPosition([1, Infinity])
editSession.insertText('\n')
expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) + 1
expect(editSession.lineForBufferRow(2)).toBe " 0"
expect(editSession.lineForBufferRow(3)).toBe " 2"
expect(editSession.lineForBufferRow(4)).toBe "4"
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", ->
editSession.setCursorBufferPosition([5, 13])
editSession.insertText('\n')
expect(editSession.indentationForBufferRow(6)).toBe editSession.indentationForBufferRow(5)
describe "when the line preceding the newline is a comment", ->
it "maintains the indent of the commented line", ->
editSession.setCursorBufferPosition([0, 0])
editSession.insertText(' //')
editSession.setCursorBufferPosition([0, Infinity])
editSession.insertText('\n')
expect(editSession.indentationForBufferRow(1)).toBe 2
it "does not indent the line preceding the newline", ->
editSession.setCursorBufferPosition([2, 0])
editSession.insertText(' var this-line-should-be-indented-more\n')
expect(editSession.indentationForBufferRow(1)).toBe 1
config.set("editor.autoIndent", true)
editSession.setCursorBufferPosition([2, Infinity])
editSession.insertText('\n')
expect(editSession.indentationForBufferRow(1)).toBe 1
expect(editSession.indentationForBufferRow(2)).toBe 1
describe "when inserted text matches a decrease indent pattern", ->
describe "when the preceding line matches an increase indent pattern", ->
it "decreases the indentation to match that of the preceding line", ->
editSession.setCursorBufferPosition([1, Infinity])
editSession.insertText('\n')
expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) + 1
editSession.insertText('}')
expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1)
describe "when the preceding line doesn't match an increase indent pattern", ->
it "decreases the indentation to be one level below that of the preceding line", ->
editSession.setCursorBufferPosition([3, Infinity])
editSession.insertText('\n ')
expect(editSession.indentationForBufferRow(4)).toBe editSession.indentationForBufferRow(3)
editSession.insertText('}')
expect(editSession.indentationForBufferRow(4)).toBe editSession.indentationForBufferRow(3) - 1
it "doesn't break when decreasing the indentation on a row that has no indentation", ->
editSession.setCursorBufferPosition([12, Infinity])
editSession.insertText("\n}; # too many closing brackets!")
expect(editSession.lineForBufferRow(13)).toBe "}; # too many closing brackets!"
describe "when inserted text does not match a decrease indent pattern", ->
it "does not the indentation", ->
editSession.setCursorBufferPosition([12, 0])
editSession.insertText(' ')
expect(editSession.lineForBufferRow(12)).toBe ' };'
editSession.insertText('\t\t')
expect(editSession.lineForBufferRow(12)).toBe ' \t\t};'
describe "when the current line does not match a decrease indent pattern", ->
it "leaves the line unchanged", ->
editSession.setCursorBufferPosition([2, 4])
expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) + 1
editSession.insertText('foo')
expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) + 1
describe "editor.autoIndentOnPaste", ->
it "does not auto-indent pasted text by default", ->
editSession.setCursorBufferPosition([2, 0])
editSession.insertText("0\n 2\n 4\n")
editSession.getSelection().setBufferRange([[2,0], [5,0]])
editSession.cutSelectedText()
describe "when the text contains multiple lines", ->
beforeEach ->
copyText("function() {\ninside=true\n}\n i=1\n")
editSession.setCursorBufferPosition([2, 0])
it "does not auto-indent pasted text by default", ->
editSession.pasteText()
expect(editSession.lineForBufferRow(2)).toBe "function() {"
expect(editSession.lineForBufferRow(3)).toBe "inside=true"
expect(editSession.lineForBufferRow(4)).toBe "}"
expect(editSession.lineForBufferRow(5)).toBe " i=1"
it "auto-indents pasted text when editor.autoIndentOnPaste is true", ->
config.set("editor.autoIndentOnPaste", true)
editSession.pasteText()
expect(editSession.lineForBufferRow(2)).toBe " function() {"
expect(editSession.lineForBufferRow(3)).toBe " inside=true"
expect(editSession.lineForBufferRow(4)).toBe " }"
expect(editSession.lineForBufferRow(5)).toBe " i=1"
describe "when the text contains no newlines", ->
it "increaseses indent of pasted text when editor.autoIndentOnPaste is true", ->
copyText("var number")
editSession.setCursorBufferPosition([10, 0])
config.set("editor.autoIndentOnPaste", true)
editSession.pasteText()
expect(editSession.lineForBufferRow(10)).toBe " var number"
it "decreaseses indent of pasted text when editor.autoIndentOnPaste is true", ->
copyText(" var number")
editSession.setCursorBufferPosition([10, 0])
config.set("editor.autoIndentOnPaste", true)
editSession.pasteText()
expect(editSession.lineForBufferRow(10)).toBe " var number"
describe "editor.normalizeIndentOnPaste", ->
beforeEach ->
config.set('editor.normalizeIndentOnPaste', true)
it "does not normalize the indentation level of the text when editor.autoIndentOnPaste is true", ->
copyText(" function() {\nvar cool = 1;\n }\n")
config.set('editor.autoIndentOnPaste', true)
editSession.setCursorBufferPosition([5, ])
editSession.pasteText()
expect(editSession.lineForBufferRow(2)).toBe "0"
expect(editSession.lineForBufferRow(3)).toBe " 2"
expect(editSession.lineForBufferRow(4)).toBe " 4"
it "auto-indents pasted text when editor.autoIndentOnPaste is true", ->
config.set("editor.autoIndentOnPaste", true)
editSession.setCursorBufferPosition([2, 0])
editSession.insertText("0\n 2\n 4\n")
editSession.getSelection().setBufferRange([[2,0], [5,0]])
editSession.cutSelectedText()
expect(editSession.lineForBufferRow(5)).toBe " function() {"
expect(editSession.lineForBufferRow(6)).toBe " var cool = 1;"
expect(editSession.lineForBufferRow(7)).toBe " }"
it "does not normalize the indentation level of the text when editor.normalizeIndentOnPaste is false", ->
copyText(" function() {\nvar cool = 1;\n }\n")
config.set('editor.normalizeIndentOnPaste', false)
editSession.setCursorBufferPosition([5, 2])
editSession.pasteText()
expect(editSession.lineForBufferRow(2)).toBe " 0"
expect(editSession.lineForBufferRow(3)).toBe " 2"
expect(editSession.lineForBufferRow(4)).toBe " 4"
expect(editSession.lineForBufferRow(5)).toBe " function() {"
expect(editSession.lineForBufferRow(6)).toBe "var cool = 1;"
expect(editSession.lineForBufferRow(7)).toBe " }"
describe ".autoDecreaseIndentForRow()", ->
it "doesn't outdent the first and only row", ->
editSession.selectAll()
editSession.insertText("}")
editSession.autoDecreaseIndentForRow(0)
expect(editSession.lineForBufferRow(0)).toBe "}"
describe "when the inserted text contains no newlines", ->
it "does not adjust the indentation level of the text", ->
editSession.setCursorBufferPosition([5, 2])
editSession.insertText("foo", indentBasis: 5)
expect(editSession.lineForBufferRow(5)).toBe " foo current = items.shift();"
it "doesn't outdent a row that is already fully outdented", ->
editSession.selectAll()
editSession.insertText("var i;\n}")
editSession.autoDecreaseIndentForRow(1)
expect(editSession.lineForBufferRow(1)).toBe "}"
describe "when the inserted text contains newlines", ->
describe "when copied text includes whitespace on first line", ->
describe "when cursor is preceded by whitespace and followed non-whitespace", ->
it "normalizes indented lines to the cursor's current indentation level", ->
copyText(" while (true) {\n foo();\n }\n", {startColumn: 4})
editSession.setCursorBufferPosition([3, 4])
editSession.pasteText()
expect(editSession.lineForBufferRow(3)).toBe " while (true) {"
expect(editSession.lineForBufferRow(4)).toBe " foo();"
expect(editSession.lineForBufferRow(5)).toBe " }"
expect(editSession.lineForBufferRow(6)).toBe "var pivot = items.shift(), current, left = [], right = [];"
describe "when cursor is preceded by whitespace and followed by whitespace", ->
it "normalizes indented lines to the cursor's current indentation level", ->
copyText(" while (true) {\n foo();\n }\n", {startColumn: 0})
editSession.setCursorBufferPosition([3, 4])
editSession.pasteText()
expect(editSession.lineForBufferRow(3)).toBe " while (true) {"
expect(editSession.lineForBufferRow(4)).toBe " foo();"
expect(editSession.lineForBufferRow(5)).toBe " }"
expect(editSession.lineForBufferRow(6)).toBe "var pivot = items.shift(), current, left = [], right = [];"
describe "when the cursor is preceded by non-whitespace characters", ->
it "normalizes the indentation level of all lines based on the level of the existing first line", ->
copyText(" while (true) {\n foo();\n }\n", {startColumn: 0})
editSession.setCursorBufferPosition([1, Infinity])
editSession.pasteText()
expect(editSession.lineForBufferRow(1)).toBe " var sort = function(items) { while (true) {"
expect(editSession.lineForBufferRow(2)).toBe " foo();"
expect(editSession.lineForBufferRow(3)).toBe " }"
expect(editSession.lineForBufferRow(4)).toBe ""
it "autoIndentSelectedRows auto-indents the selection", ->
editSession.setCursorBufferPosition([2, 0])
editSession.insertText("function() {\ninside=true\n}\n i=1\n")
editSession.getSelection().setBufferRange([[2,0], [6,0]])
editSession.autoIndentSelectedRows()
expect(editSession.lineForBufferRow(2)).toBe " function() {"
expect(editSession.lineForBufferRow(3)).toBe " inside=true"
expect(editSession.lineForBufferRow(4)).toBe " }"
expect(editSession.lineForBufferRow(5)).toBe " i=1"
describe ".destroy()", ->
it "destroys all markers associated with the edit session", ->
@ -2517,7 +2455,7 @@ describe "EditSession", ->
expect(editSession.shouldPromptToSave()).toBeFalsy()
buffer.setText('changed')
expect(editSession.shouldPromptToSave()).toBeTruthy()
editSession2 = project.buildEditSession('sample.js', autoIndent: false)
editSession2 = project.open('sample.js', autoIndent: false)
expect(editSession.shouldPromptToSave()).toBeFalsy()
editSession2.destroy()
expect(editSession.shouldPromptToSave()).toBeTruthy()

View File

@ -15,7 +15,7 @@ describe "Editor", ->
beforeEach ->
atom.activatePackage('text.tmbundle', sync: true)
atom.activatePackage('javascript.tmbundle', sync: true)
editSession = project.buildEditSession('sample.js')
editSession = project.open('sample.js')
buffer = editSession.buffer
editor = new Editor(editSession)
editor.lineOverdraw = 2
@ -38,7 +38,7 @@ describe "Editor", ->
cachedCharWidth
calcDimensions = ->
editorForMeasurement = new Editor(editSession: project.buildEditSession('sample.js'))
editorForMeasurement = new Editor(editSession: project.open('sample.js'))
editorForMeasurement.attachToDom()
cachedLineHeight = editorForMeasurement.lineHeight
cachedCharWidth = editorForMeasurement.charWidth
@ -85,7 +85,7 @@ describe "Editor", ->
it "triggers an alert", ->
path = "/tmp/atom-changed-file.txt"
fsUtils.write(path, "")
editSession = project.buildEditSession(path)
editSession = project.open(path)
editor.edit(editSession)
editor.insertText("now the buffer is modified")
@ -111,7 +111,7 @@ describe "Editor", ->
[newEditSession, newBuffer] = []
beforeEach ->
newEditSession = project.buildEditSession('two-hundred.txt')
newEditSession = project.open('two-hundred.txt')
newBuffer = newEditSession.buffer
it "updates the rendered lines, cursors, selections, scroll position, and event subscriptions to match the given edit session", ->
@ -149,7 +149,7 @@ describe "Editor", ->
it "triggers alert if edit session's buffer goes into conflict with changes on disk", ->
path = "/tmp/atom-changed-file.txt"
fsUtils.write(path, "")
tempEditSession = project.buildEditSession(path)
tempEditSession = project.open(path)
editor.edit(tempEditSession)
tempEditSession.insertText("a buffer change")
@ -243,7 +243,7 @@ describe "Editor", ->
it "emits event when editor receives a new buffer", ->
eventHandler = jasmine.createSpy('eventHandler')
editor.on 'editor:path-changed', eventHandler
editor.edit(project.buildEditSession(path))
editor.edit(project.open(path))
expect(eventHandler).toHaveBeenCalled()
it "stops listening to events on previously set buffers", ->
@ -251,7 +251,7 @@ describe "Editor", ->
oldBuffer = editor.getBuffer()
editor.on 'editor:path-changed', eventHandler
editor.edit(project.buildEditSession(path))
editor.edit(project.open(path))
expect(eventHandler).toHaveBeenCalled()
eventHandler.reset()
@ -1399,7 +1399,7 @@ describe "Editor", ->
describe "when autoscrolling at the end of the document", ->
it "renders lines properly", ->
editor.edit(project.buildEditSession('two-hundred.txt'))
editor.edit(project.open('two-hundred.txt'))
editor.attachToDom(heightInLines: 5.5)
expect(editor.renderedLines.find('.line').length).toBe 8
@ -1624,7 +1624,7 @@ describe "Editor", ->
expect(editor.bufferPositionForScreenPosition(editor.getCursorScreenPosition())).toEqual [3, 60]
it "does not wrap the lines of any newly assigned buffers", ->
otherEditSession = project.buildEditSession()
otherEditSession = project.open()
otherEditSession.buffer.setText([1..100].join(''))
editor.edit(otherEditSession)
expect(editor.renderedLines.find('.line').length).toBe(1)
@ -1660,7 +1660,7 @@ describe "Editor", ->
expect(editor.getCursorScreenPosition()).toEqual [11, 0]
it "calls .setSoftWrapColumn() when the editor is attached because now its dimensions are available to calculate it", ->
otherEditor = new Editor(editSession: project.buildEditSession('sample.js'))
otherEditor = new Editor(editSession: project.open('sample.js'))
spyOn(otherEditor, 'setSoftWrapColumn')
otherEditor.setSoftWrap(true)
@ -1764,7 +1764,7 @@ describe "Editor", ->
describe "when the switching from an edit session for a long buffer to an edit session for a short buffer", ->
it "updates the line numbers to reflect the shorter buffer", ->
emptyEditSession = project.buildEditSession(null)
emptyEditSession = project.open(null)
editor.edit(emptyEditSession)
expect(editor.gutter.lineNumbers.find('.line-number').length).toBe 1
@ -1926,7 +1926,7 @@ describe "Editor", ->
describe "folding", ->
beforeEach ->
editSession = project.buildEditSession('two-hundred.txt')
editSession = project.open('two-hundred.txt')
buffer = editSession.buffer
editor.edit(editSession)
editor.attachToDom()
@ -2064,7 +2064,7 @@ describe "Editor", ->
beforeEach ->
path = project.resolve('git/working-dir/file.txt')
originalPathText = fsUtils.read(path)
editor.edit(project.buildEditSession(path))
editor.edit(project.open(path))
afterEach ->
fsUtils.write(path, originalPathText)
@ -2084,7 +2084,6 @@ describe "Editor", ->
runs ->
expect(editor.getText()).toBe(originalPathText)
describe ".pixelPositionForBufferPosition(position)", ->
describe "when the editor is detached", ->
it "returns top and left values of 0", ->
@ -2186,7 +2185,7 @@ describe "Editor", ->
fsUtils.remove(path) if fsUtils.exists(path)
it "updates all the rendered lines when the grammar changes", ->
editor.edit(project.buildEditSession(path))
editor.edit(project.open(path))
expect(editor.getGrammar().name).toBe 'Plain Text'
syntax.setGrammarOverrideForPath(path, 'source.js')
editor.reloadGrammar()
@ -2206,7 +2205,7 @@ describe "Editor", ->
expect(editor.getGrammar().name).toBe 'JavaScript'
it "emits an editor:grammar-changed event when updated", ->
editor.edit(project.buildEditSession(path))
editor.edit(project.open(path))
eventHandler = jasmine.createSpy('eventHandler')
editor.on('editor:grammar-changed', eventHandler)
@ -2620,7 +2619,6 @@ describe "Editor", ->
editor.getPane().destroyActiveItem()
expect(willBeRemovedHandler).toHaveBeenCalled()
describe "when setInvisibles is toggled (regression)", ->
it "renders inserted newlines properly", ->
editor.setShowInvisibles(true)

View File

@ -214,7 +214,7 @@ describe "Git", ->
expect(repo.isStatusNew(statuses[newPath])).toBeTruthy()
expect(repo.isStatusModified(statuses[modifiedPath])).toBeTruthy()
it "only starts a single web worker at a time and schedules a restart if one is already running", =>
it "only starts a single task at a time and schedules a restart if one is already running", =>
fsUtils.write(modifiedPath, 'making this path modified')
statusHandler = jasmine.createSpy('statusHandler')
repo.on 'statuses-changed', statusHandler

View File

@ -11,7 +11,7 @@ describe "LanguageMode", ->
describe "javascript", ->
beforeEach ->
atom.activatePackage('javascript.tmbundle', sync: true)
editSession = project.buildEditSession('sample.js', autoIndent: false)
editSession = project.open('sample.js', autoIndent: false)
{ buffer, languageMode } = editSession
describe ".toggleLineCommentsForBufferRows(start, end)", ->
@ -53,7 +53,7 @@ describe "LanguageMode", ->
describe "coffeescript", ->
beforeEach ->
atom.activatePackage('coffee-script-tmbundle', sync: true)
editSession = project.buildEditSession('coffee.coffee', autoIndent: false)
editSession = project.open('coffee.coffee', autoIndent: false)
{ buffer, languageMode } = editSession
describe ".toggleLineCommentsForBufferRows(start, end)", ->
@ -89,7 +89,7 @@ describe "LanguageMode", ->
describe "css", ->
beforeEach ->
atom.activatePackage('css.tmbundle', sync: true)
editSession = project.buildEditSession('css.css', autoIndent: false)
editSession = project.open('css.css', autoIndent: false)
{ buffer, languageMode } = editSession
describe ".toggleLineCommentsForBufferRows(start, end)", ->

View File

@ -10,8 +10,8 @@ describe "Pane", ->
container = new PaneContainer
view1 = $$ -> @div id: 'view-1', tabindex: -1, 'View 1'
view2 = $$ -> @div id: 'view-2', tabindex: -1, 'View 2'
editSession1 = project.buildEditSession('sample.js')
editSession2 = project.buildEditSession('sample.txt')
editSession1 = project.open('sample.js')
editSession2 = project.open('sample.txt')
pane = new Pane(view1, editSession1, view2, editSession2)
container.append(pane)

View File

@ -9,8 +9,8 @@ describe "Project", ->
describe "when an edit session is destroyed", ->
it "removes edit session and calls destroy on buffer (if buffer is not referenced by other edit sessions)", ->
editSession = project.buildEditSession("a")
anotherEditSession = project.buildEditSession("a")
editSession = project.open("a")
anotherEditSession = project.open("a")
expect(project.editSessions.length).toBe 2
expect(editSession.buffer).toBe anotherEditSession.buffer
@ -26,13 +26,13 @@ describe "Project", ->
path = project.resolve('a')
project.setPath(undefined)
expect(project.getPath()).toBeUndefined()
editSession = project.buildEditSession()
editSession = project.open()
editSession.saveAs('/tmp/atom-test-save-sets-project-path')
expect(project.getPath()).toBe '/tmp'
fsUtils.remove('/tmp/atom-test-save-sets-project-path')
describe ".buildEditSession(path)", ->
[absolutePath, newBufferHandler, newEditSessionHandler] = []
describe ".open(path)", ->
[fooOpener, barOpener, absolutePath, newBufferHandler, newEditSessionHandler] = []
beforeEach ->
absolutePath = fsUtils.resolveOnLoadPath('fixtures/dir/a')
newBufferHandler = jasmine.createSpy('newBufferHandler')
@ -40,35 +40,50 @@ describe "Project", ->
newEditSessionHandler = jasmine.createSpy('newEditSessionHandler')
project.on 'edit-session-created', newEditSessionHandler
describe "when given an absolute path that hasn't been opened previously", ->
it "returns a new edit session for the given path and emits 'buffer-created' and 'edit-session-created' events", ->
editSession = project.buildEditSession(absolutePath)
expect(editSession.buffer.getPath()).toBe absolutePath
expect(newBufferHandler).toHaveBeenCalledWith editSession.buffer
expect(newEditSessionHandler).toHaveBeenCalledWith editSession
fooOpener = (path, options) -> { foo: path, options } if path?.match(/\.foo/)
barOpener = (path) -> { bar: path } if path?.match(/^bar:\/\//)
Project.registerOpener(fooOpener)
Project.registerOpener(barOpener)
describe "when given a relative path that hasn't been opened previously", ->
it "returns a new edit session for the given path (relative to the project root) and emits 'buffer-created' and 'edit-session-created' events", ->
editSession = project.buildEditSession('a')
expect(editSession.buffer.getPath()).toBe absolutePath
expect(newBufferHandler).toHaveBeenCalledWith editSession.buffer
expect(newEditSessionHandler).toHaveBeenCalledWith editSession
afterEach ->
Project.unregisterOpener(fooOpener)
Project.unregisterOpener(barOpener)
describe "when passed the path to a buffer that has already been opened", ->
it "returns a new edit session containing previously opened buffer and emits a 'edit-session-created' event", ->
editSession = project.buildEditSession(absolutePath)
newBufferHandler.reset()
expect(project.buildEditSession(absolutePath).buffer).toBe editSession.buffer
expect(project.buildEditSession('a').buffer).toBe editSession.buffer
expect(newBufferHandler).not.toHaveBeenCalled()
expect(newEditSessionHandler).toHaveBeenCalledWith editSession
describe "when passed a path that doesn't match a custom opener", ->
describe "when given an absolute path that hasn't been opened previously", ->
it "returns a new edit session for the given path and emits 'buffer-created' and 'edit-session-created' events", ->
editSession = project.open(absolutePath)
expect(editSession.buffer.getPath()).toBe absolutePath
expect(newBufferHandler).toHaveBeenCalledWith editSession.buffer
expect(newEditSessionHandler).toHaveBeenCalledWith editSession
describe "when not passed a path", ->
it "returns a new edit session and emits 'buffer-created' and 'edit-session-created' events", ->
editSession = project.buildEditSession()
expect(editSession.buffer.getPath()).toBeUndefined()
expect(newBufferHandler).toHaveBeenCalledWith(editSession.buffer)
expect(newEditSessionHandler).toHaveBeenCalledWith editSession
describe "when given a relative path that hasn't been opened previously", ->
it "returns a new edit session for the given path (relative to the project root) and emits 'buffer-created' and 'edit-session-created' events", ->
editSession = project.open('a')
expect(editSession.buffer.getPath()).toBe absolutePath
expect(newBufferHandler).toHaveBeenCalledWith editSession.buffer
expect(newEditSessionHandler).toHaveBeenCalledWith editSession
describe "when passed the path to a buffer that has already been opened", ->
it "returns a new edit session containing previously opened buffer and emits a 'edit-session-created' event", ->
editSession = project.open(absolutePath)
newBufferHandler.reset()
expect(project.open(absolutePath).buffer).toBe editSession.buffer
expect(project.open('a').buffer).toBe editSession.buffer
expect(newBufferHandler).not.toHaveBeenCalled()
expect(newEditSessionHandler).toHaveBeenCalledWith editSession
describe "when not passed a path", ->
it "returns a new edit session and emits 'buffer-created' and 'edit-session-created' events", ->
editSession = project.open()
expect(editSession.buffer.getPath()).toBeUndefined()
expect(newBufferHandler).toHaveBeenCalledWith(editSession.buffer)
expect(newEditSessionHandler).toHaveBeenCalledWith editSession
describe "when passed a path that matches a custom opener", ->
it "returns the resource returned by the custom opener", ->
expect(project.open("a.foo", hey: "there")).toEqual { foo: "a.foo", options: {hey: "there"} }
expect(project.open("bar://baz")).toEqual { bar: "bar://baz" }
describe ".bufferForPath(path)", ->
describe "when opening a previously opened path", ->
@ -84,12 +99,17 @@ describe "Project", ->
buffer = project.bufferForPath("a").retain().release()
expect(project.bufferForPath("a").retain().release()).not.toBe buffer
describe ".resolve(path)", ->
it "returns an absolute path based on the project's root", ->
absolutePath = fsUtils.resolveOnLoadPath('fixtures/dir/a')
expect(project.resolve('a')).toBe absolutePath
expect(project.resolve(absolutePath + '/../a')).toBe absolutePath
expect(project.resolve('a/../a')).toBe absolutePath
describe ".resolve(uri)", ->
describe "when passed an absolute or relative path", ->
it "returns an absolute path based on the project's root", ->
absolutePath = fsUtils.resolveOnLoadPath('fixtures/dir/a')
expect(project.resolve('a')).toBe absolutePath
expect(project.resolve(absolutePath + '/../a')).toBe absolutePath
expect(project.resolve('a/../a')).toBe absolutePath
describe "when passed a uri with a scheme", ->
it "does not modify uris that begin with a scheme", ->
expect(project.resolve('http://zombo.com')).toBe 'http://zombo.com'
describe ".relativize(path)", ->
it "returns an relative path based on the project's root", ->
@ -160,8 +180,7 @@ describe "Project", ->
it "ignores ignored.txt file", ->
paths = null
config.get("core.ignoredNames").push("ignored.txt")
config.update()
config.pushAtKeyPath("core.ignoredNames", "ignored.txt")
waitsForPromise ->
project.getFilePaths().done (foundPaths) -> paths = foundPaths
@ -301,6 +320,22 @@ describe "Project", ->
expect(paths[0]).toBe filePath
expect(matches.length).toBe 1
it "excludes values in core.ignoredNames", ->
projectPath = '/tmp/atom-tests/folder-with-dot-git/.git'
filePath = fsUtils.join(projectPath, 'test.txt')
fsUtils.write(filePath, 'match this')
project.setPath(projectPath)
paths = []
matches = []
waitsForPromise ->
project.scan /match/, ({path, match, range}) ->
paths.push(path)
matches.push(match)
runs ->
expect(paths.length).toBe 0
expect(matches.length).toBe 0
describe "serialization", ->
it "restores the project path", ->
newProject = Project.deserialize(project.serialize())

View File

@ -46,10 +46,10 @@ describe "RootView", ->
pane2 = pane1.splitRight()
pane3 = pane2.splitRight()
pane4 = pane2.splitDown()
pane2.showItem(project.buildEditSession('b'))
pane3.showItem(project.buildEditSession('../sample.js'))
pane2.showItem(project.open('b'))
pane3.showItem(project.open('../sample.js'))
pane3.activeItem.setCursorScreenPosition([2, 4])
pane4.showItem(project.buildEditSession('../sample.txt'))
pane4.showItem(project.open('../sample.txt'))
pane4.activeItem.setCursorScreenPosition([0, 2])
pane2.focus()

View File

@ -198,11 +198,11 @@ describe "TextMateGrammar", ->
{tokens} = grammar.tokenizeLine('%Q+matz had some #{%Q-crazy ideas-} for ruby syntax+ # damn.')
expect(tokens[0]).toEqual value: '%Q+', scopes: ["source.ruby","string.quoted.other.literal.upper.ruby","punctuation.definition.string.begin.ruby"]
expect(tokens[1]).toEqual value: 'matz had some ', scopes: ["source.ruby","string.quoted.other.literal.upper.ruby"]
expect(tokens[2]).toEqual value: '#{', scopes: ["source.ruby","string.quoted.other.literal.upper.ruby","source.ruby.embedded.source","punctuation.section.embedded.ruby"]
expect(tokens[3]).toEqual value: '%Q-', scopes: ["source.ruby","string.quoted.other.literal.upper.ruby","source.ruby.embedded.source","string.quoted.other.literal.upper.ruby","punctuation.definition.string.begin.ruby"]
expect(tokens[4]).toEqual value: 'crazy ideas', scopes: ["source.ruby","string.quoted.other.literal.upper.ruby","source.ruby.embedded.source","string.quoted.other.literal.upper.ruby"]
expect(tokens[5]).toEqual value: '-', scopes: ["source.ruby","string.quoted.other.literal.upper.ruby","source.ruby.embedded.source","string.quoted.other.literal.upper.ruby","punctuation.definition.string.end.ruby"]
expect(tokens[6]).toEqual value: '}', scopes: ["source.ruby","string.quoted.other.literal.upper.ruby","source.ruby.embedded.source","punctuation.section.embedded.ruby"]
expect(tokens[2]).toEqual value: '#{', scopes: ["source.ruby","string.quoted.other.literal.upper.ruby","meta.embedded.line.ruby","punctuation.section.embedded.begin.ruby"]
expect(tokens[3]).toEqual value: '%Q-', scopes: ["source.ruby","string.quoted.other.literal.upper.ruby","meta.embedded.line.ruby","string.quoted.other.literal.upper.ruby","punctuation.definition.string.begin.ruby"]
expect(tokens[4]).toEqual value: 'crazy ideas', scopes: ["source.ruby","string.quoted.other.literal.upper.ruby","meta.embedded.line.ruby","string.quoted.other.literal.upper.ruby"]
expect(tokens[5]).toEqual value: '-', scopes: ["source.ruby","string.quoted.other.literal.upper.ruby","meta.embedded.line.ruby","string.quoted.other.literal.upper.ruby","punctuation.definition.string.end.ruby"]
expect(tokens[6]).toEqual value: '}', scopes: ["source.ruby","string.quoted.other.literal.upper.ruby","meta.embedded.line.ruby","punctuation.section.embedded.end.ruby", "source.ruby"]
expect(tokens[7]).toEqual value: ' for ruby syntax', scopes: ["source.ruby","string.quoted.other.literal.upper.ruby"]
expect(tokens[8]).toEqual value: '+', scopes: ["source.ruby","string.quoted.other.literal.upper.ruby","punctuation.definition.string.end.ruby"]
expect(tokens[9]).toEqual value: ' ', scopes: ["source.ruby"]
@ -448,7 +448,7 @@ describe "TextMateGrammar", ->
describe "when the grammar is added", ->
it "retokenizes existing buffers that contain tokens that match the injection selector", ->
editSession = project.buildEditSession('sample.js')
editSession = project.open('sample.js')
editSession.setText("// http://github.com")
{tokens} = editSession.lineForScreenRow(0)
@ -463,7 +463,7 @@ describe "TextMateGrammar", ->
describe "when the grammar is updated", ->
it "retokenizes existing buffers that contain tokens that match the injection selector", ->
editSession = project.buildEditSession('sample.js')
editSession = project.open('sample.js')
editSession.setText("// SELECT * FROM OCTOCATS")
{tokens} = editSession.lineForScreenRow(0)

View File

@ -343,3 +343,17 @@ describe "TokenizedBuffer", ->
fullyTokenize(tokenizedBuffer)
{tokens} = tokenizedBuffer.lineForScreenRow(0)
expect(tokens[0]).toEqual value: '<', scopes: ["text.html.ruby","meta.tag.block.any.html","punctuation.definition.tag.begin.html"]
describe ".tokenForPosition(position)", ->
afterEach ->
tokenizedBuffer.destroy()
buffer.release()
it "returns the correct token (regression)", ->
buffer = project.bufferForPath('sample.js')
tokenizedBuffer = new TokenizedBuffer(buffer)
tokenizedBuffer.setVisible(true)
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenForPosition([1,0]).scopes).toEqual ["source.js"]
expect(tokenizedBuffer.tokenForPosition([1,1]).scopes).toEqual ["source.js"]
expect(tokenizedBuffer.tokenForPosition([1,2]).scopes).toEqual ["source.js", "storage.modifier.js"]

View File

@ -1,18 +1,20 @@
$ = require 'jquery'
{$$} = require 'space-pen'
fsUtils = require 'fs-utils'
{less} = require 'less'
WindowEventHandler = require 'window-event-handler'
describe "Window", ->
projectPath = null
[projectPath, windowEventHandler] = []
beforeEach ->
spyOn(atom, 'getPathToOpen').andReturn(project.getPath())
window.handleEvents()
windowEventHandler = new WindowEventHandler()
window.deserializeEditorWindow()
projectPath = project.getPath()
afterEach ->
window.unloadEditorWindow()
windowEventHandler.unsubscribe()
$(window).off 'beforeunload'
describe "when the window is loaded", ->
@ -154,23 +156,6 @@ describe "Window", ->
window.unloadEditorWindow()
expect(atom.saveWindowState.callCount).toBe 1
describe ".installAtomCommand(commandPath)", ->
commandPath = '/tmp/installed-atom-command/atom'
afterEach ->
fsUtils.remove(commandPath) if fsUtils.exists(commandPath)
describe "when the command path doesn't exist", ->
it "copies atom.sh to the specified path", ->
expect(fsUtils.exists(commandPath)).toBeFalsy()
window.installAtomCommand(commandPath)
waitsFor ->
fsUtils.exists(commandPath)
runs ->
expect(fsUtils.read(commandPath).length).toBeGreaterThan 1
describe ".deserialize(state)", ->
class Foo
@deserialize: ({name}) -> new Foo(name)
@ -251,3 +236,82 @@ describe "Window", ->
shell.openExternal.reset()
$("<a href='#scroll-me'>link</a>").appendTo(document.body).click().remove()
expect(shell.openExternal).not.toHaveBeenCalled()
describe "core:focus-next and core:focus-previous", ->
describe "when there is no currently focused element", ->
it "focuses the element with the lowest/highest tabindex", ->
elements = $$ ->
@div =>
@button tabindex: 2
@input tabindex: 1
elements.attachToDom()
elements.trigger "core:focus-next"
expect(elements.find("[tabindex=1]:focus")).toExist()
$(":focus").blur()
elements.trigger "core:focus-previous"
expect(elements.find("[tabindex=2]:focus")).toExist()
describe "when a tabindex is set on the currently focused element", ->
it "focuses the element with the next highest tabindex", ->
elements = $$ ->
@div =>
@input tabindex: 1
@button tabindex: 2
@button tabindex: 5
@input tabindex: -1
@input tabindex: 3
@button tabindex: 7
elements.attachToDom()
elements.find("[tabindex=1]").focus()
elements.trigger "core:focus-next"
expect(elements.find("[tabindex=2]:focus")).toExist()
elements.trigger "core:focus-next"
expect(elements.find("[tabindex=3]:focus")).toExist()
elements.focus().trigger "core:focus-next"
expect(elements.find("[tabindex=5]:focus")).toExist()
elements.focus().trigger "core:focus-next"
expect(elements.find("[tabindex=7]:focus")).toExist()
elements.focus().trigger "core:focus-next"
expect(elements.find("[tabindex=1]:focus")).toExist()
elements.trigger "core:focus-previous"
expect(elements.find("[tabindex=7]:focus")).toExist()
elements.trigger "core:focus-previous"
expect(elements.find("[tabindex=5]:focus")).toExist()
elements.focus().trigger "core:focus-previous"
expect(elements.find("[tabindex=3]:focus")).toExist()
elements.focus().trigger "core:focus-previous"
expect(elements.find("[tabindex=2]:focus")).toExist()
elements.focus().trigger "core:focus-previous"
expect(elements.find("[tabindex=1]:focus")).toExist()
it "skips disabled elements", ->
elements = $$ ->
@div =>
@input tabindex: 1
@button tabindex: 2, disabled: 'disabled'
@input tabindex: 3
elements.attachToDom()
elements.find("[tabindex=1]").focus()
elements.trigger "core:focus-next"
expect(elements.find("[tabindex=3]:focus")).toExist()
elements.trigger "core:focus-previous"
expect(elements.find("[tabindex=1]:focus")).toExist()
>>>>>>> master

View File

@ -0,0 +1,34 @@
fs = require 'fs'
fsUtils = require 'fs-utils'
installer = require 'command-installer'
describe "install(commandPath, callback)", ->
directory = '/tmp/install-atom-command/atom'
commandPath = "#{directory}/source"
destinationPath = "#{directory}/bin/source"
beforeEach ->
spyOn(installer, 'findInstallDirectory').andCallFake (callback) ->
callback(directory)
fsUtils.remove(directory) if fsUtils.exists(directory)
it "symlinks the command and makes it executable", ->
fsUtils.write(commandPath, 'test')
expect(fsUtils.isFile(commandPath)).toBeTruthy()
expect(fsUtils.isExecutable(commandPath)).toBeFalsy()
expect(fsUtils.isFile(destinationPath)).toBeFalsy()
installDone = false
installError = null
installer.install commandPath, (error) ->
installDone = true
installError = error
waitsFor -> installDone
runs ->
expect(installError).toBeNull()
expect(fsUtils.isFile(destinationPath)).toBeTruthy()
expect(fs.realpathSync(destinationPath)).toBe fs.realpathSync(commandPath)
expect(fsUtils.isExecutable(destinationPath)).toBeTruthy()

View File

@ -1,92 +0,0 @@
CSON = require 'cson'
describe "CSON", ->
describe "@stringify(object)", ->
describe "when the object is undefined", ->
it "throws an exception", ->
expect(-> CSON.stringify()).toThrow()
describe "when the object is a function", ->
it "throws an exception", ->
expect(-> CSON.stringify(-> 'function')).toThrow()
describe "when the object contains a function", ->
it "throws an exception", ->
expect(-> CSON.stringify(a: -> 'function')).toThrow()
describe "when formatting an undefined key", ->
it "does not include the key in the formatted CSON", ->
expect(CSON.stringify(b: 1, c: undefined)).toBe "'b': 1"
describe "when formatting a string", ->
it "returns formatted CSON", ->
expect(CSON.stringify(a: 'b')).toBe "'a': 'b'"
it "escapes single quotes", ->
expect(CSON.stringify(a: "'b'")).toBe "'a': '\\\'b\\\''"
it "doesn't escape double quotes", ->
expect(CSON.stringify(a: '"b"')).toBe "'a': '\"b\"'"
it "escapes newlines", ->
expect(CSON.stringify("a\nb")).toBe "'a\\nb'"
describe "when formatting a boolean", ->
it "returns formatted CSON", ->
expect(CSON.stringify(true)).toBe 'true'
expect(CSON.stringify(false)).toBe 'false'
expect(CSON.stringify(a: true)).toBe "'a': true"
expect(CSON.stringify(a: false)).toBe "'a': false"
describe "when formatting a number", ->
it "returns formatted CSON", ->
expect(CSON.stringify(54321.012345)).toBe '54321.012345'
expect(CSON.stringify(a: 14)).toBe "'a': 14"
expect(CSON.stringify(a: 1.23)).toBe "'a': 1.23"
describe "when formatting null", ->
it "returns formatted CSON", ->
expect(CSON.stringify(null)).toBe 'null'
expect(CSON.stringify(a: null)).toBe "'a': null"
describe "when formatting an array", ->
describe "when the array is empty", ->
it "puts the array on a single line", ->
expect(CSON.stringify([])).toBe "[]"
it "returns formatted CSON", ->
expect(CSON.stringify(a: ['b'])).toBe "'a': [\n 'b'\n]"
expect(CSON.stringify(a: ['b', 4])).toBe "'a': [\n 'b'\n 4\n]"
describe "when the array has an undefined value", ->
it "formats the undefined value as null", ->
expect(CSON.stringify(['a', undefined, 'b'])).toBe "[\n 'a'\n null\n 'b'\n]"
describe "when the array contains an object", ->
it "wraps the object in {}", ->
expect(CSON.stringify([{a:'b', a1: 'b1'}, {c: 'd'}])).toBe "[\n {\n 'a': 'b'\n 'a1': 'b1'\n }\n {\n 'c': 'd'\n }\n]"
describe "when formatting an object", ->
describe "when the object is empty", ->
it "returns {}", ->
expect(CSON.stringify({})).toBe "{}"
it "returns formatted CSON", ->
expect(CSON.stringify(a: {b: 'c'})).toBe "'a':\n 'b': 'c'"
expect(CSON.stringify(a:{})).toBe "'a': {}"
expect(CSON.stringify(a:[])).toBe "'a': []"
describe "when converting back to an object", ->
it "produces the original object", ->
object =
showInvisibles: true
fontSize: 20
core:
themes: ['a', 'b']
whitespace:
ensureSingleTrailingNewline: true
cson = CSON.stringify(object)
CoffeeScript = require 'coffee-script'
evaledObject = CoffeeScript.eval(cson, bare: true)
expect(evaledObject).toEqual object

View File

@ -1,9 +1,10 @@
Subscriber = require 'subscriber'
EventEmitter = require 'event-emitter'
_ = require 'underscore'
{$$} = require 'space-pen'
describe "Subscriber", ->
[emitter1, emitter2, event1Handler, event2Handler, subscriber] = []
[emitter1, emitter2, emitter3, event1Handler, event2Handler, event3Handler, subscriber] = []
class TestEventEmitter
_.extend TestEventEmitter.prototype, EventEmitter
@ -14,11 +15,17 @@ describe "Subscriber", ->
beforeEach ->
emitter1 = new TestEventEmitter
emitter2 = new TestEventEmitter
emitter3 = $$ ->
@div =>
@a()
@span()
subscriber = new TestSubscriber
event1Handler = jasmine.createSpy("event1Handler")
event2Handler = jasmine.createSpy("event2Handler")
event3Handler = jasmine.createSpy("event3Handler")
subscriber.subscribe emitter1, 'event1', event1Handler
subscriber.subscribe emitter2, 'event2', event2Handler
subscriber.subscribe emitter3, 'event3', 'a', event3Handler
it "subscribes to events on the specified object", ->
emitter1.trigger 'event1', 'foo'
@ -27,6 +34,12 @@ describe "Subscriber", ->
emitter2.trigger 'event2', 'bar'
expect(event2Handler).toHaveBeenCalledWith('bar')
emitter3.find('span').trigger 'event3'
expect(event3Handler).not.toHaveBeenCalledWith()
emitter3.find('a').trigger 'event3'
expect(event3Handler).toHaveBeenCalled()
it "allows an object to unsubscribe en-masse", ->
subscriber.unsubscribe()
emitter1.trigger 'event1', 'foo'

View File

@ -3,8 +3,7 @@ Package = require 'package'
fsUtils = require 'fs-utils'
_ = require 'underscore'
$ = require 'jquery'
CSON = require 'cson'
CSON = require 'season'
### Internal: Loads and resolves packages. ###
@ -63,11 +62,11 @@ class AtomPackage extends Package
loadMetadata: ->
if metadataPath = fsUtils.resolveExtension(fsUtils.join(@path, 'package'), ['json', 'cson'])
@metadata = CSON.readObject(metadataPath)
@metadata = CSON.readFileSync(metadataPath)
@metadata ?= {}
loadKeymaps: ->
@keymaps = @getKeymapPaths().map (path) -> [path, CSON.readObject(path)]
@keymaps = @getKeymapPaths().map (path) -> [path, CSON.readFileSync(path)]
getKeymapPaths: ->
keymapsDirPath = fsUtils.join(@path, 'keymaps')

View File

@ -73,7 +73,7 @@ window.atom =
loadPackage: (id, options) ->
if @isPackageDisabled(id)
return console.warn("Tried to load disabled packaged '#{id}'")
return console.warn("Tried to load disabled package '#{id}'")
if path = @resolvePackagePath(id)
return pack if pack = @getLoadedPackage(id)

View File

@ -1,7 +1,7 @@
fsUtils = require 'fs-utils'
_ = require 'underscore'
EventEmitter = require 'event-emitter'
CSON = require 'cson'
CSON = require 'season'
fs = require 'fs'
async = require 'async'
pathWatcher = require 'pathwatcher'
@ -72,7 +72,7 @@ class Config
loadUserConfig: ->
if fsUtils.exists(@configFilePath)
try
userConfig = CSON.readObject(@configFilePath)
userConfig = CSON.readFileSync(@configFilePath)
_.extend(@settings, userConfig)
@configFileHasErrors = false
@trigger 'updated'
@ -108,8 +108,8 @@ class Config
# Returns the value from Atom's default settings, the user's configuration file,
# or `null` if the key doesn't exist in either.
get: (keyPath) ->
_.valueForKeyPath(@settings, keyPath) ?
_.valueForKeyPath(@defaultSettings, keyPath)
value = _.valueForKeyPath(@settings, keyPath) ? _.valueForKeyPath(@defaultSettings, keyPath)
_.deepClone(value)
# Retrieves the setting for the given key as an integer.
#
@ -146,6 +146,30 @@ class Config
@update()
value
# Push the value to the array at the key path.
#
# keyPath - The {String} key path.
# value - The value to push to the array.
#
# Returns the new array length of the setting.
pushAtKeyPath: (keyPath, value) ->
arrayValue = @get(keyPath) ? []
result = arrayValue.push(value)
@set(keyPath, arrayValue)
result
# Remove the value from the array at the key path.
#
# keyPath - The {String} key path.
# value - The value to remove from the array.
#
# Returns the new array value of the setting.
removeAtKeyPath: (keyPath, value) ->
arrayValue = @get(keyPath) ? []
result = _.remove(arrayValue, value)
@set(keyPath, arrayValue)
result
# Establishes an event listener for a given key.
#
# `callback` is fired immediately and whenever the value of the key is changed
@ -175,6 +199,6 @@ class Config
@trigger 'updated'
save: ->
CSON.writeObject(@configFilePath, @settings)
CSON.writeFileSync(@configFilePath, @settings)
_.extend Config.prototype, EventEmitter

View File

@ -9,6 +9,7 @@ Subscriber = require 'subscriber'
Range = require 'range'
_ = require 'underscore'
fsUtils = require 'fs-utils'
TextMateScopeSelector = require 'text-mate-scope-selector'
# An `EditSession` manages the states between {Editor}s, {Buffer}s, and the project as a whole.
module.exports =
@ -23,7 +24,7 @@ class EditSession
session = project.buildEditSessionForBuffer(Buffer.deserialize(state.buffer))
if !session?
console.warn "Could not build edit session for path '#{state.buffer}' because that file no longer exists" if state.buffer
session = project.buildEditSession(null)
session = project.open(null)
session.setScrollTop(state.scrollTop)
session.setScrollLeft(state.scrollLeft)
session.setCursorScreenPosition(state.cursorScreenPosition)
@ -284,6 +285,14 @@ class EditSession
# {Delegates to: Buffer.isRowBlank}
isBufferRowBlank: (bufferRow) -> @buffer.isRowBlank(bufferRow)
# Test if an entire row is a comment
#
# Returns a {Boole}.
isBufferRowCommented: (bufferRow) ->
if match = @lineForBufferRow(bufferRow).match(/\S/)
scopes = @tokenForBufferPosition([bufferRow, match.index]).scopes
new TextMateScopeSelector('comment.*').matches(scopes)
# {Delegates to: Buffer.nextNonBlankRow}
nextNonBlankBufferRow: (bufferRow) -> @buffer.nextNonBlankRow(bufferRow)
@ -361,24 +370,13 @@ class EditSession
# Returns an {Array} of {String}s.
getCursorScopes: -> @getCursor().getScopes()
# Determines whether the {Editor} will auto indent rows.
#
# Returns a {Boolean}.
shouldAutoIndent: ->
config.get("editor.autoIndent")
# Determines whether the {Editor} will auto indent pasted text.
#
# Returns a {Boolean}.
shouldAutoIndentPastedText: ->
config.get("editor.autoIndentOnPaste")
# Inserts text at the current cursor positions
#
# text - A {String} representing the text to insert.
# options - A set of options equivalent to {Selection.insertText}
insertText: (text, options={}) ->
options.autoIndent ?= @shouldAutoIndent()
options.autoIndentNewline ?= @shouldAutoIndent()
options.autoDecreaseIndent ?= @shouldAutoIndent()
@mutateSelectedText (selection) -> selection.insertText(text, options)
# Inserts a new line at the current cursor positions.
@ -486,11 +484,11 @@ class EditSession
#
# options - A set of options equivalent to {Selection.insertText}.
pasteText: (options={}) ->
options.normalizeIndent ?= true
options.autoIndent ?= @shouldAutoIndentPastedText()
[text, metadata] = pasteboard.read()
_.extend(options, metadata) if metadata
options.autoIndent ?= @shouldAutoIndentPastedText()
if config.get('editor.normalizeIndentOnPaste') and metadata
options.indentBasis ?= metadata.indentBasis
@insertText(text, options)
@ -609,16 +607,10 @@ class EditSession
autoIndentBufferRow: (bufferRow) ->
@languageMode.autoIndentBufferRow(bufferRow)
# Given a buffer row, this increases the indentation.
#
# bufferRow - The row {Number}
autoIncreaseIndentForBufferRow: (bufferRow) ->
@languageMode.autoIncreaseIndentForBufferRow(bufferRow)
# Given a buffer row, this decreases the indentation.
#
# bufferRow - The row {Number}
autoDecreaseIndentForRow: (bufferRow) ->
autoDecreaseIndentForBufferRow: (bufferRow) ->
@languageMode.autoDecreaseIndentForBufferRow(bufferRow)
# Wraps the lines between two rows in comments.
@ -1247,10 +1239,6 @@ class EditSession
@mergeIntersectingSelections(options)
return
# Internal:
inspect: ->
JSON.stringify @serialize()
preserveCursorPositionOnBufferReload: ->
cursorPosition = null
@subscribe @buffer, "will-reload", =>
@ -1273,12 +1261,21 @@ class EditSession
### Internal ###
shouldAutoIndent: ->
config.get("editor.autoIndent")
shouldAutoIndentPastedText: ->
config.get("editor.autoIndentOnPaste")
transact: (fn) -> @buffer.transact(fn)
commit: -> @buffer.commit()
abort: -> @buffer.abort()
inspect: ->
JSON.stringify @serialize()
logScreenLines: (start, end) -> @displayBuffer.logLines(start, end)
handleGrammarChange: ->

View File

@ -22,6 +22,7 @@ class Editor extends View
showLineNumbers: true
autoIndent: true
autoIndentOnPaste: false
normalizeIndentOnPaste: false
nonWordCharacters: "./\\()\"':,.;<>~!@#$%^&*|+=[]{}`~?-"
preferredLineLength: 80

View File

@ -8,11 +8,19 @@ module.exports=
class ImageEditSession
registerDeserializer(this)
# Files with these extensions will be opened as images
@imageExtensions: ['.gif', '.jpeg', '.jpg', '.png']
### Internal ###
Project = require 'project'
Project.registerOpener (path) =>
new ImageEditSession(path) if _.include(@imageExtensions, fsUtils.extension(path))
@deserialize: (state) ->
if fsUtils.exists(state.path)
project.buildEditSession(state.path)
project.open(state.path)
else
console.warn "Could not build edit session for path '#{state.path}' because that file no longer exists"
@ -27,19 +35,6 @@ class ImageEditSession
### Public ###
# Identifies if a path can be opened by the image viewer.
#
# path - The {String} name of the path to check
#
# Returns a {Boolean}.
@canOpen: (path) ->
_.indexOf([
'.gif'
'.jpeg'
'.jpg'
'.png'
], fsUtils.extension(path), true) >= 0
# Retrieves the filename of the open file.
#
# This is `'untitled'` if the file is new and not saved to the disk.

View File

@ -1,8 +1,7 @@
$ = require 'jquery'
_ = require 'underscore'
fsUtils = require 'fs-utils'
CSON = require 'cson'
CSON = require 'season'
BindingSet = require 'binding-set'
# Internal: Associates keymaps with actions.
@ -46,7 +45,7 @@ class Keymap
@load(filePath) for filePath in fsUtils.list(directoryPath, ['.cson', '.json'])
load: (path) ->
@add(path, CSON.readObject(path))
@add(path, CSON.readFileSync(path))
add: (args...) ->
name = args.shift() if args.length > 1

View File

@ -67,10 +67,10 @@
# allow standard input fields to work correctly
'input:not(.hidden-input)':
'tab': 'core:focus-next'
'shift-tab': 'core:focus-previous'
'left': 'native!'
'right': 'native!'
'tab': 'native!'
'shift-tab': 'native!'
'shift-left': 'native!'
'shift-right': 'native!'
'backspace': 'native!'
@ -81,3 +81,7 @@
'meta-x': 'native!'
'meta-c': 'native!'
'meta-v': 'native!'
'button':
'tab': 'core:focus-next'
'shift-tab': 'core:focus-previous'

View File

@ -162,14 +162,13 @@ class LanguageMode
return currentIndentLevel unless precedingRow?
precedingLine = @buffer.lineForRow(precedingRow)
desiredIndentLevel = @editSession.indentationForBufferRow(precedingRow)
desiredIndentLevel += 1 if increaseIndentRegex.test(precedingLine)
desiredIndentLevel += 1 if increaseIndentRegex.test(precedingLine) and not @editSession.isBufferRowCommented(precedingRow)
return desiredIndentLevel unless decreaseIndentRegex = @decreaseIndentRegexForScopes(scopes)
desiredIndentLevel -= 1 if decreaseIndentRegex.test(currentLine)
Math.max(desiredIndentLevel, currentIndentLevel)
desiredIndentLevel
# Indents all the rows between two buffer row numbers.
#
@ -182,26 +181,8 @@ class LanguageMode
#
# bufferRow - The row {Number}
autoIndentBufferRow: (bufferRow) ->
@autoIncreaseIndentForBufferRow(bufferRow)
@autoDecreaseIndentForBufferRow(bufferRow)
# Given a buffer row, this increases the indentation.
#
# bufferRow - The row {Number}
autoIncreaseIndentForBufferRow: (bufferRow) ->
precedingRow = @buffer.previousNonBlankRow(bufferRow)
return unless precedingRow?
precedingLine = @editSession.lineForBufferRow(precedingRow)
scopes = @editSession.scopesForBufferPosition([precedingRow, Infinity])
increaseIndentRegex = @increaseIndentRegexForScopes(scopes)
return unless increaseIndentRegex
currentIndentLevel = @editSession.indentationForBufferRow(bufferRow)
desiredIndentLevel = @editSession.indentationForBufferRow(precedingRow)
desiredIndentLevel += 1 if increaseIndentRegex.test(precedingLine)
if desiredIndentLevel > currentIndentLevel
@editSession.setIndentationForBufferRow(bufferRow, desiredIndentLevel)
indentLevel = @suggestedIndentForBufferRow(bufferRow)
@editSession.setIndentationForBufferRow(bufferRow, indentLevel)
# Given a buffer row, this decreases the indentation.
#

View File

@ -30,10 +30,9 @@ class PackageConfigPanel extends ConfigPanel
checkbox = $(e.target)
name = checkbox.closest('tr').attr('name')
if checkbox.attr('checked')
_.remove(config.get('core.disabledPackages'), name)
config.removeAtKeyPath('core.disabledPackages', name)
else
config.get('core.disabledPackages').push(name)
config.update()
config.pushAtKeyPath('core.disabledPackages', name)
@observeConfig 'core.disabledPackages', (disabledPackages) =>
@packageTableBody.find("input[type='checkbox']").attr('checked', true)

View File

@ -4,7 +4,6 @@ $ = require 'jquery'
Range = require 'range'
Buffer = require 'text-buffer'
EditSession = require 'edit-session'
ImageEditSession = require 'image-edit-session'
EventEmitter = require 'event-emitter'
Directory = require 'directory'
BufferedProcess = require 'buffered-process'
@ -17,6 +16,14 @@ module.exports =
class Project
registerDeserializer(this)
@openers: []
@registerOpener: (opener) ->
@openers.push(opener)
@unregisterOpener: (opener) ->
_.remove(@openers, opener)
tabLength: 2
softTabs: true
softWrap: false
@ -104,14 +111,18 @@ class Project
ignoreRepositoryPath: (path) ->
config.get("core.hideGitIgnoredFiles") and git?.isPathIgnored(fsUtils.join(@getPath(), path))
# Given a path, this resolves it relative to the project directory.
# Given a uri, this resolves it relative to the project directory. If the path
# is already absolute or if it is prefixed with a scheme, it is returned unchanged.
#
# filePath - The {String} name of the path to convert
# uri - The {String} name of the path to convert
#
# Returns a {String}.
resolve: (filePath) ->
filePath = fsUtils.join(@getPath(), filePath) unless filePath[0] == '/'
fsUtils.absolute filePath
resolve: (uri) ->
if uri?.match(/[A-Za-z0-9+-.]+:\/\//) # leave path alone if it has a scheme
uri
else
uri = fsUtils.join(@getPath(), uri) unless uri[0] == '/'
fsUtils.absolute uri
# Given a path, this makes it relative to the project directory.
#
@ -148,11 +159,11 @@ class Project
# editSessionOptions - Options that you can pass to the `EditSession` constructor
#
# Returns either an {EditSession} (for text) or {ImageEditSession} (for images).
buildEditSession: (filePath, editSessionOptions={}) ->
if ImageEditSession.canOpen(filePath)
new ImageEditSession(filePath)
else
@buildEditSessionForBuffer(@bufferForPath(filePath), editSessionOptions)
open: (filePath, options={}) ->
for opener in @constructor.openers
return resource if resource = opener(filePath, options)
@buildEditSessionForBuffer(@bufferForPath(filePath), options)
# Retrieves all the {EditSession}s in the project; that is, the `EditSession`s for all open files.
#
@ -265,7 +276,9 @@ class Project
command = require.resolve('nak')
args = ['--hidden', '--ackmate', regex.source, @getPath()]
args.unshift("--addVCSIgnores") if config.get('core.excludeVcsIgnoredPaths')
ignoredNames = config.get('core.ignoredNames') ? []
args.unshift('--ignore', ignoredNames.join(',')) if ignoredNames.length > 0
args.unshift('--addVCSIgnores') if config.get('core.excludeVcsIgnoredPaths')
new BufferedProcess({command, args, stdout, exit})
deferred
@ -300,3 +313,5 @@ class Project
@on 'buffer-created', (buffer) -> callback(buffer)
_.extend Project.prototype, EventEmitter
require 'image-edit-session'

View File

@ -204,6 +204,12 @@ class Range
getRowCount: ->
@end.row - @start.row + 1
# Returns an array of all rows in a `Range`
#
# Returns an {Array}
getRows: ->
[@start.row..@end.row]
### Internal ###
inspect: ->

View File

@ -109,10 +109,10 @@ class RootView extends View
changeFocus = options.changeFocus ? true
path = project.resolve(path) if path?
if activePane = @getActivePane()
editSession = activePane.itemForUri(path) ? project.buildEditSession(path)
editSession = activePane.itemForUri(path) ? project.open(path)
activePane.showItem(editSession)
else
editSession = project.buildEditSession(path)
editSession = project.open(path)
activePane = new Pane(editSession)
@panes.append(activePane)

View File

@ -254,17 +254,21 @@ class Selection
#
# text - A {String} representing the text to add
# options - A hash containing the following options:
# normalizeIndent: TODO
# select: if `true`, selects the newly added text
# autoIndent: if `true`, indents the newly added text appropriately
# autoIndent: if `true`, indents all inserted text appropriately
# autoIndentNewline: if `true`, indent newline appropriately
# autoDecreaseIndent: if `true`, decreases indent level appropriately (for example, when a closing bracket is inserted)
insertText: (text, options={}) ->
oldBufferRange = @getBufferRange()
@editSession.destroyFoldsContainingBufferRow(oldBufferRange.end.row)
wasReversed = @isReversed()
text = @normalizeIndent(text, options) if options.normalizeIndent
@clear()
@cursor.needsAutoscroll = @cursor.isLastCursor()
if options.indentBasis? and not options.autoIndent
text = @normalizeIndents(text, options.indentBasis)
newBufferRange = @editSession.buffer.change(oldBufferRange, text)
if options.select
@setBufferRange(newBufferRange, reverse: wasReversed)
@ -272,13 +276,39 @@ class Selection
@cursor.setBufferPosition(newBufferRange.end, skipAtomicTokens: true) if wasReversed
if options.autoIndent
if text == '\n'
@editSession.autoIndentBufferRow(newBufferRange.end.row)
else if /\S/.test(text)
@editSession.autoDecreaseIndentForRow(newBufferRange.start.row)
@editSession.autoIndentBufferRow(row) for row in newBufferRange.getRows()
else if options.autoIndentNewline and text == '\n'
@editSession.autoIndentBufferRow(newBufferRange.end.row)
else if options.autoDecreaseIndent and /\S/.test text
@editSession.autoDecreaseIndentForBufferRow(newBufferRange.start.row)
newBufferRange
normalizeIndents: (text, indentBasis) ->
textPrecedingCursor = @cursor.getCurrentBufferLine()[0...@cursor.getBufferColumn()]
isCursorInsideExistingLine = /\S/.test(textPrecedingCursor)
lines = text.split('\n')
firstLineIndentLevel = @editSession.indentLevelForLine(lines[0])
if isCursorInsideExistingLine
minimumIndentLevel = @editSession.indentationForBufferRow(@cursor.getBufferRow())
else
minimumIndentLevel = @cursor.getIndentLevel() + firstLineIndentLevel
normalizedLines = []
for line, i in lines
if i == 0
indentLevel = firstLineIndentLevel
else if /$^/.test line # remove all indentation from empty lines
indentLevel = 0
else
lineIndentLevel = @editSession.indentLevelForLine(lines[i])
indentLevel = minimumIndentLevel + (lineIndentLevel - indentBasis)
normalizedLines.push(@setIndentationForLine(line, indentLevel))
normalizedLines.join('\n')
# Indents the selection.
#
# options - A hash with one key, `autoIndent`. If `true`, the indentation is
@ -304,43 +334,8 @@ class Selection
for row in [start..end]
@editSession.buffer.insert([row, 0], @editSession.getTabText()) unless @editSession.buffer.lineLengthForRow(row) == 0
normalizeIndent: (text, options) ->
return text unless /\n/.test(text)
currentBufferRow = @cursor.getBufferRow()
currentBufferColumn = @cursor.getBufferColumn()
lines = text.split('\n')
currentBasis = options.indentBasis ? @editSession.indentLevelForLine(lines[0])
lines[0] = lines[0].replace(/^\s*/, '') # strip leading space from first line
normalizedLines = []
textPrecedingCursor = @editSession.buffer.getTextInRange([[currentBufferRow, 0], [currentBufferRow, currentBufferColumn]])
insideExistingLine = textPrecedingCursor.match(/\S/)
if insideExistingLine
desiredBasis = @editSession.indentationForBufferRow(currentBufferRow)
else if options.autoIndent
desiredBasis = @editSession.suggestedIndentForBufferRow(currentBufferRow)
else
desiredBasis = @cursor.getIndentLevel()
for line, i in lines
if i == 0
if insideExistingLine
delta = 0
else
delta = desiredBasis - @cursor.getIndentLevel()
else
delta = desiredBasis - currentBasis
normalizedLines.push(@adjustIndentationForLine(line, delta))
normalizedLines.join('\n')
adjustIndentationForLine: (line, delta) ->
currentIndentLevel = @editSession.indentLevelForLine(line)
desiredIndentLevel = Math.max(0, currentIndentLevel + delta)
setIndentationForLine: (line, indentLevel) ->
desiredIndentLevel = Math.max(0, indentLevel)
desiredIndentString = @editSession.buildIndentString(desiredIndentLevel)
line.replace(/^[\t ]*/, desiredIndentString)
@ -481,12 +476,6 @@ class Selection
@editSession.createFold(range.start.row, range.end.row)
@cursor.setBufferPosition([range.end.row + 1, 0])
autoIndentText: (text) ->
@editSession.autoIndentTextAfterBufferPosition(text, @cursor.getBufferPosition())
autoOutdent: ->
@editSession.autoOutdentBufferRow(@cursor.getBufferRow())
modifySelection: (fn) ->
@retainSelection = true
@placeTail()

View File

@ -93,7 +93,7 @@ class TokenizedLine
delta = 0
for token in @tokens
delta += token.bufferDelta
return token if delta >= bufferColumn
return token if delta > bufferColumn
token
breakOutAtomicTokens: (inputTokens, tabLength) ->

View File

@ -0,0 +1,88 @@
$ = require 'jquery'
_ = require 'underscore'
ipc = require 'ipc'
Subscriber = require 'subscriber'
module.exports =
class WindowEventHandler
constructor: ->
@subscribe ipc, 'command', (command) -> $(window).trigger command
@subscribe $(window), 'focus', -> $("body").removeClass('is-blurred')
@subscribe $(window), 'blur', -> $("body").addClass('is-blurred')
@subscribeToCommand $(window), 'window:toggle-full-screen', => atom.toggleFullScreen()
@subscribeToCommand $(window), 'window:close', =>
if rootView?
rootView.confirmClose().done -> closeWithoutConfirm()
else
closeWithoutConfirm()
@subscribeToCommand $(window), 'window:reload', => atom.reload()
@subscribeToCommand $(document), 'core:focus-next', @focusNext
@subscribeToCommand $(document), 'core:focus-previous', @focusPrevious
@subscribe $(document), 'keydown', keymap.handleKeyEvent
@subscribe $(document), 'drop', onDrop
@subscribe $(document), 'dragover', (e) ->
e.preventDefault()
e.stopPropagation()
@subscribe $(document), 'click', 'a', @openLink
openLink: (event) =>
location = $(event.target).attr('href')
return unless location
return if location[0] is '#'
if location.indexOf('https://') is 0 or location.indexOf('http://') is 0
require('child_process').spawn('open', [location])
false
eachTabIndexedElement: (callback) ->
for element in $('[tabindex]')
element = $(element)
continue if element.isDisabled()
tabIndex = parseInt(element.attr('tabindex'))
continue unless tabIndex >= 0
callback(element, tabIndex)
focusNext: =>
focusedTabIndex = parseInt($(':focus').attr('tabindex')) or -Infinity
nextElement = null
nextTabIndex = Infinity
lowestElement = null
lowestTabIndex = Infinity
@eachTabIndexedElement (element, tabIndex) ->
if tabIndex < lowestTabIndex
lowestTabIndex = tabIndex
lowestElement = element
if focusedTabIndex < tabIndex < nextTabIndex
nextTabIndex = tabIndex
nextElement = element
(nextElement ? lowestElement).focus()
focusPrevious: =>
focusedTabIndex = parseInt($(':focus').attr('tabindex')) or Infinity
previousElement = null
previousTabIndex = -Infinity
highestElement = null
highestTabIndex = -Infinity
@eachTabIndexedElement (element, tabIndex) ->
if tabIndex > highestTabIndex
highestTabIndex = tabIndex
highestElement = element
if focusedTabIndex > tabIndex > previousTabIndex
previousTabIndex = tabIndex
previousElement = element
(previousElement ? highestElement).focus()
_.extend WindowEventHandler.prototype, Subscriber

View File

@ -1,10 +1,9 @@
fs = require 'fs'
fsUtils = require 'fs-utils'
$ = require 'jquery'
_ = require 'underscore'
less = require 'less'
ipc = require 'ipc'
remote = require 'remote'
WindowEventHandler = require 'window-event-handler'
require 'jquery-extensions'
require 'underscore-extensions'
require 'space-pen-extensions'
@ -15,6 +14,8 @@ defaultWindowDimensions = {x: 0, y: 0, width: 800, height: 600}
### Internal ###
windowEventHandler = null
# This method is called in any window needing a general environment, including specs
window.setUpEnvironment = ->
Config = require 'config'
@ -27,7 +28,6 @@ window.setUpEnvironment = ->
window.syntax = deserialize(atom.getWindowState('syntax')) ? new Syntax
window.pasteboard = new Pasteboard
window.keymap = new Keymap()
$(document).on 'keydown', keymap.handleKeyEvent
keymap.bindDefaultKeys()
@ -43,15 +43,11 @@ window.setUpEnvironment = ->
# This method is only called when opening a real application window
window.startEditorWindow = ->
directory = _.find ['/opt/boxen', '/opt/github', '/usr/local'], (dir) -> fsUtils.isDirectory(dir)
if directory
installAtomCommand(fsUtils.join(directory, 'bin/atom'))
else
console.warn "Failed to install `atom` binary"
installAtomCommand()
installApmCommand()
atom.windowMode = 'editor'
handleEvents()
handleDragDrop()
windowEventHandler = new WindowEventHandler
config.load()
keymap.loadBundledKeymaps()
atom.loadThemes()
@ -65,7 +61,7 @@ window.startEditorWindow = ->
window.startConfigWindow = ->
atom.windowMode = 'config'
handleEvents()
windowEventHandler = new WindowEventHandler
config.load()
keymap.loadBundledKeymaps()
atom.loadThemes()
@ -88,57 +84,25 @@ window.unloadEditorWindow = ->
rootView.remove()
project.destroy()
git?.destroy()
$(window).off('focus blur before')
windowEventHandler?.unsubscribe()
window.rootView = null
window.project = null
window.git = null
window.installAtomCommand = (commandPath, done) ->
fs.exists commandPath, (exists) ->
return if exists
window.installAtomCommand = (callback) ->
commandPath = fsUtils.join(window.resourcePath, 'atom.sh')
require('command-installer').install(commandPath, callback)
bundledCommandPath = fsUtils.resolve(window.resourcePath, 'atom.sh')
if bundledCommandPath?
fs.readFile bundledCommandPath, (error, data) ->
if error?
console.warn "Failed to install `atom` binary", error
else
fsUtils.writeAsync commandPath, data, (error) ->
if error?
console.warn "Failed to install `atom` binary", error
else
fs.chmod(commandPath, 0o755, commandPath)
window.installApmCommand = (callback) ->
commandPath = fsUtils.join(window.resourcePath, 'node_modules', '.bin', 'apm')
require('command-installer').install(commandPath, callback)
window.unloadConfigWindow = ->
return if not configView
atom.setWindowState('configView', configView.serialize())
configView.remove()
windowEventHandler?.unsubscribe()
window.configView = null
$(window).off('focus blur before')
window.handleEvents = ->
ipc.on 'command', (command) -> console.log command; $(window).trigger command
$(window).command 'window:toggle-full-screen', => atom.toggleFullScreen()
$(window).on 'focus', -> $("body").removeClass('is-blurred')
$(window).on 'blur', -> $("body").addClass('is-blurred')
$(window).command 'window:close', => confirmClose()
$(window).command 'window:reload', => atom.reload()
$(document).on 'click', 'a', (e) ->
location = $(e.target).attr('href')
return unless location
return if location[0] is '#'
if location.indexOf('https://') is 0 or location.indexOf('http://') is 0
require('shell').openExternal(location)
false
window.handleDragDrop = ->
$(document).on 'dragover', (e) ->
e.preventDefault()
e.stopPropagation()
$(document).on 'drop', onDrop
window.onDrop = (e) ->
e.preventDefault()
@ -281,10 +245,3 @@ window.profile = (description, fn) ->
value = fn()
console.profileEnd(description)
value
# Public: Shows a dialog asking if the window was _really_ meant to be closed.
confirmClose = ->
if rootView?
rootView.confirmClose().done -> closeWithoutConfirm()
else
closeWithoutConfirm()

View File

@ -40,7 +40,7 @@ describe "AutocompleteView", ->
beforeEach ->
window.rootView = new RootView
editor = new Editor(editSession: project.buildEditSession('sample.js'))
editor = new Editor(editSession: project.open('sample.js'))
atom.activatePackage('autocomplete')
autocomplete = new AutocompleteView(editor)
miniEditor = autocomplete.miniEditor

View File

@ -10,7 +10,7 @@ describe "CommandInterpreter", ->
beforeEach ->
interpreter = new CommandInterpreter(project)
editSession = project.buildEditSession('sample.js')
editSession = project.open('sample.js')
buffer = editSession.buffer
afterEach ->
@ -445,7 +445,7 @@ describe "CommandInterpreter", ->
runs ->
expect(operationsToPreview.length).toBeGreaterThan 3
for operation in operationsToPreview
editSession = project.buildEditSession(operation.getPath())
editSession = project.open(operation.getPath())
editSession.setSelectedBufferRange(operation.execute(editSession))
expect(editSession.getSelectedText()).toMatch /a+/
editSession.destroy()

View File

@ -2,6 +2,7 @@
SelectList = require 'select-list'
_ = require 'underscore'
$ = require 'jquery'
humanize = require 'humanize-plus'
fsUtils = require 'fs-utils'
LoadPathsTask = require './load-paths-task'
@ -16,6 +17,7 @@ class FuzzyFinderView extends SelectList
maxItems: 10
projectPaths: null
reloadProjectPaths: true
filterKey: 'projectRelativePath'
initialize: ->
super
@ -35,7 +37,7 @@ class FuzzyFinderView extends SelectList
@miniEditor.command 'pane:split-up', =>
@splitOpenPath (pane, session) -> pane.splitUp(session)
itemForElement: (path) ->
itemForElement: ({path, projectRelativePath}) ->
$$ ->
@li class: 'two-lines', =>
if git?
@ -60,20 +62,20 @@ class FuzzyFinderView extends SelectList
typeClass = 'text-name'
@div fsUtils.base(path), class: "primary-line file #{typeClass}"
@div project.relativize(path), class: 'secondary-line path'
@div projectRelativePath, class: 'secondary-line path'
openPath: (path) ->
rootView.open(path, {@allowActiveEditorChange}) if path
splitOpenPath: (fn) ->
path = @getSelectedElement()
{path} = @getSelectedElement()
return unless path
if pane = rootView.getActivePane()
fn(pane, project.buildEditSession(path))
fn(pane, project.open(path))
else
@openPath(path)
confirmed : (path) ->
confirmed : ({path}) ->
return unless path.length
if fsUtils.isFile(path)
@cancel()
@ -131,6 +133,13 @@ class FuzzyFinderView extends SelectList
@attach()
@miniEditor.setText(currentWord)
setArray: (paths) ->
projectRelativePaths = paths.map (path) ->
projectRelativePath = project.relativize(path)
{path, projectRelativePath}
super(projectRelativePaths)
populateGitStatusPaths: ->
paths = []
paths.push(path) for path, status of git.statuses when fsUtils.isFile(path)
@ -159,7 +168,7 @@ class FuzzyFinderView extends SelectList
@populateProjectPaths(options)
@loadPathsTask = new LoadPathsTask(callback)
@loadPathsTask.on 'paths-loaded', (paths) =>
@loadingBadge.text(paths.length)
@loadingBadge.text(humanize.intcomma(paths.length))
@loadPathsTask.start()
populateOpenBufferPaths: ->

View File

@ -35,9 +35,15 @@ class PathLoader
loadPath: (path) ->
@asyncCallStarting()
fs.stat path, (error, stats) =>
fs.lstat path, (error, stats) =>
unless error?
if stats.isDirectory()
if stats.isSymbolicLink()
@asyncCallStarting()
fs.stat path, (error, stats) =>
unless error?
@pathLoaded(path) if stats.isFile()
@asyncCallDone()
else if stats.isDirectory()
@loadFolder(path) unless @isIgnored(path)
else if stats.isFile()
@pathLoaded(path)

View File

@ -71,6 +71,17 @@ describe 'FuzzyFinder', ->
runs ->
expect(finderView.list.find("li:contains(symlink-to-file)")).toExist()
it "excludes symlinked folder paths", ->
rootView.attachToDom()
finderView.maxItems = Infinity
rootView.trigger 'fuzzy-finder:toggle-file-finder'
waitsFor "all project paths to load", 5000, ->
not finderView.reloadProjectPaths
runs ->
expect(finderView.list.find("li:contains(symlink-to-dir)")).not.toExist()
describe "when root view's project has no path", ->
beforeEach ->
project.setPath(null)
@ -89,7 +100,7 @@ describe 'FuzzyFinder', ->
rootView.trigger 'fuzzy-finder:toggle-file-finder'
expectedPath = project.resolve('dir/a')
finderView.confirmed(expectedPath)
finderView.confirmed({path: expectedPath})
expect(finderView.hasParent()).toBeFalsy()
expect(editor1.getPath()).not.toBe expectedPath
@ -101,7 +112,7 @@ describe 'FuzzyFinder', ->
rootView.attachToDom()
path = rootView.getActiveView().getPath()
rootView.trigger 'fuzzy-finder:toggle-file-finder'
finderView.confirmed('dir/this/is/not/a/file.txt')
finderView.confirmed({path: 'dir/this/is/not/a/file.txt'})
expect(finderView.hasParent()).toBeTruthy()
expect(rootView.getActiveView().getPath()).toBe path
expect(finderView.find('.error').text().length).toBeGreaterThan 0
@ -199,7 +210,7 @@ describe 'FuzzyFinder', ->
describe "when the active pane has an item for the selected path", ->
it "switches to the item for the selected path", ->
expectedPath = project.resolve('sample.txt')
finderView.confirmed(expectedPath)
finderView.confirmed({path: expectedPath})
expect(finderView.hasParent()).toBeFalsy()
expect(editor1.getPath()).not.toBe expectedPath
@ -215,7 +226,7 @@ describe 'FuzzyFinder', ->
expect(rootView.getActiveView()).toBe editor1
expectedPath = project.resolve('sample.txt')
finderView.confirmed(expectedPath)
finderView.confirmed({path: expectedPath})
expect(finderView.hasParent()).toBeFalsy()
expect(editor1.getPath()).toBe expectedPath
@ -438,7 +449,7 @@ describe 'FuzzyFinder', ->
spyOn(pane, "splitLeft").andCallThrough()
rootView.trigger 'fuzzy-finder:toggle-buffer-finder'
path = finderView.getSelectedElement()
{path} = finderView.getSelectedElement()
finderView.miniEditor.trigger 'pane:split-left'
expect(rootView.getPanes().length).toBe 2
@ -451,7 +462,7 @@ describe 'FuzzyFinder', ->
spyOn(pane, "splitRight").andCallThrough()
rootView.trigger 'fuzzy-finder:toggle-buffer-finder'
path = finderView.getSelectedElement()
{path} = finderView.getSelectedElement()
finderView.miniEditor.trigger 'pane:split-right'
expect(rootView.getPanes().length).toBe 2
@ -464,7 +475,7 @@ describe 'FuzzyFinder', ->
spyOn(pane, "splitUp").andCallThrough()
rootView.trigger 'fuzzy-finder:toggle-buffer-finder'
path = finderView.getSelectedElement()
{path} = finderView.getSelectedElement()
finderView.miniEditor.trigger 'pane:split-up'
expect(rootView.getPanes().length).toBe 2
@ -477,7 +488,7 @@ describe 'FuzzyFinder', ->
spyOn(pane, "splitDown").andCallThrough()
rootView.trigger 'fuzzy-finder:toggle-buffer-finder'
path = finderView.getSelectedElement()
{path} = finderView.getSelectedElement()
finderView.miniEditor.trigger 'pane:split-down'
expect(rootView.getPanes().length).toBe 2

View File

@ -54,80 +54,80 @@
'name': 'comment.hr.gfm'
}
{
'begin': '^```coffee(script)?$'
'begin': '^`{3,}coffee(script)?$'
'beginCaptures':
'0': 'name': 'support.gfm'
'end': '^```$'
'end': '^`{3,}$'
'endCaptures':
'0': 'name': 'support.gfm'
'name': 'markup.code.coffee.gfm'
'patterns': ['include': 'source.coffee']
}
{
'begin': '^```(javascript|js)$'
'begin': '^`{3,}(javascript|js)$'
'beginCaptures':
'0': 'name': 'support.gfm'
'end': '^```$'
'end': '^`{3,}$'
'endCaptures':
'0': 'name': 'support.gfm'
'name': 'markup.code.js.gfm'
'patterns': ['include': 'source.js']
}
{
'begin': '^```css$'
'begin': '^`{3,}css$'
'beginCaptures':
'0': 'name': 'support.gfm'
'end': '^```$'
'end': '^`{3,}$'
'endCaptures':
'0': 'name': 'support.gfm'
'name': 'markup.code.css.gfm'
'patterns': ['include': 'source.css']
}
{
'begin': '^```xml$'
'begin': '^`{3,}xml$'
'beginCaptures':
'0': 'name': 'support.gfm'
'end': '^```$'
'end': '^`{3,}$'
'endCaptures':
'0': 'name': 'support.gfm'
'name': 'markup.code.xml.gfm'
'patterns': ['include': 'text.xml']
}
{
'begin': '^```(ruby|rb)$'
'begin': '^`{3,}(ruby|rb)$'
'beginCaptures':
'0': 'name': 'support.gfm'
'end': '^```$'
'end': '^`{3,}$'
'endCaptures':
'0': 'name': 'support.gfm'
'name': 'markup.code.ruby.gfm'
'patterns': ['include': 'source.ruby']
}
{
'begin': '^```java$'
'begin': '^`{3,}java$'
'beginCaptures':
'0': 'name': 'support.gfm'
'end': '^```$'
'end': '^`{3,}$'
'endCaptures':
'0': 'name': 'support.gfm'
'name': 'markup.code.java.gfm'
'patterns': ['include': 'source.java']
}
{
'begin': '^```(sh|bash)$'
'begin': '^`{3,}(sh|bash)$'
'beginCaptures':
'0': 'name': 'support.gfm'
'end': '^```$'
'end': '^`{3,}$'
'endCaptures':
'0': 'name': 'support.gfm'
'name': 'markup.code.shell.gfm'
'patterns': ['include': 'source.shell']
}
{
'begin': '^```.*$'
'begin': '^`{3,}.*$'
'beginCaptures':
'0': 'name': 'support.gfm'
'end': '^```$'
'end': '^`{3,}$'
'endCaptures':
'0': 'name': 'support.gfm'
'name': 'markup.raw.gfm'

View File

@ -1,6 +1,5 @@
$ = require 'jquery'
{$$} = require 'space-pen'
module.exports =
class Gists
@activate: -> new Gists
@ -22,6 +21,9 @@ class Gists
dataType: 'json'
contentType: 'application/json; charset=UTF-8'
data: JSON.stringify(gist)
beforeSend: (xhr) ->
if token = require('keytar').getPassword('GitHub.com', 'github')
xhr.setRequestHeader('Authorization', "bearer #{token}")
success: (response) =>
pasteboard.write(response.html_url)
notification = $$ ->

View File

@ -0,0 +1,7 @@
'.sign-in-view':
'esc': 'core:cancel'
'.sign-in-view input':
'meta-enter': 'core:confirm'
'.sign-in-view button':
'enter': 'core:confirm'
'meta-enter': 'core:confirm'

View File

@ -0,0 +1,121 @@
$ = require 'jquery'
_ = require 'underscore'
ScrollView = require 'scroll-view'
keytar = require 'keytar'
class SignInView extends ScrollView
@content: ->
@div class: 'sign-in-view overlay from-top', =>
@h4 'Sign in to GitHub'
@p 'Your password will only be used to generate a token that will be stored in your keychain.'
@div class: 'form-inline', =>
@input outlet: 'username', type: 'text', placeholder: 'Username or Email', tabindex: 1
@input outlet: 'password', type: 'password', placeholder: 'Password', tabindex: 2
@button outlet: 'signIn', class: 'btn', disabled: 'disabled', tabindex: 3, 'Sign in'
@button outlet: 'cancel', class: 'btn', tabindex: 4, 'Cancel'
@div outlet: 'alert', class: 'alert alert-error'
initialize: ({@signedInUser}={})->
rootView.command 'github:sign-in', => @attach()
@username.on 'core:confirm', => @generateOAuth2Token()
@username.on 'input', => @validate()
@password.on 'core:confirm', => @generateOAuth2Token()
@password.on 'input', => @validate()
@signIn.on 'core:confirm', => @generateOAuth2Token()
@signIn.on 'click', => @generateOAuth2Token()
@cancel.on 'core:confirm', => @generateOAuth2Token()
@cancel.on 'click', => @detach()
@on 'core:cancel', => @detach()
@subscribe $(document.body), 'click focusin', (e) =>
@detach() unless $.contains(this[0], e.target)
serialize: -> {@signedInUser}
validate: ->
if $.trim(@username.val()).length > 0 and @password.val().length > 0
@signIn.enable()
else
@signIn.disable()
generateOAuth2Token: ->
return if @signIn.isDisabled()
@alert.hide()
@username.disable()
@password.disable()
@signIn.disable()
username = $.trim(@username.val())
credentials = btoa("#{username}:#{@password.val()}")
request =
scopes: ['user', 'repo', 'gist']
note: 'GitHub Atom'
note_url: 'https://github.com/github/atom'
$.ajax
url: 'https://api.github.com/authorizations'
type: 'POST'
dataType: 'json'
contentType: 'application/json; charset=UTF-8'
data: JSON.stringify(request)
beforeSend: (xhr) ->
xhr.setRequestHeader('Authorization', "Basic #{credentials}")
success: ({token}={}) =>
if token?.length > 0
@signedInUser = username
unless keytar.replacePassword('github.com', 'github', token)
console.warn 'Unable to save GitHub token to keychain'
@detach()
error: (response={}) =>
if _.isString(response.responseText)
try
message = JSON.parse(response.responseText)?.message
else
message = response.responseText?.message
message ?= ''
@alert.text(message).show()
@username.enable()
@password.enable()
@signIn.enable()
@password.focus()
attach: ->
if @signedInUser?
@username.val(@signedInUser)
else
@username.val('')
@password.val('')
@username.enable()
@password.enable()
@alert.hide()
rootView.append(this)
if @signedInUser?
@password.focus()
else
@username.focus()
module.exports =
signInView: null
activate: (state) ->
@signInView = new SignInView(state)
deactivate: ->
@signInView?.remove()
@signInView = null
serialize: ->
if @signInView?
@signInView.serialize()
else
@state

View File

@ -0,0 +1,4 @@
'main': 'lib/sign-in-view'
'activationEvents': [
'github:sign-in'
]

View File

@ -0,0 +1,14 @@
.sign-in-view {
input, button, .alert {
margin-bottom: 10px;
}
button {
margin-right: 10px;
}
.alert:before {
content: 'Sign in failed. ';
font-weight: bold;
}
}

View File

@ -16,6 +16,8 @@ fenceNameToExtension =
"java": "java"
"sh": "sh"
"bash": "sh"
"js": "js"
"javascript": "js"
module.exports =
class MarkdownPreviewView extends ScrollView
@ -93,7 +95,7 @@ class MarkdownPreviewView extends ScrollView
renderMarkdown: ->
@setLoading()
roaster @buffer.getText(), {}, (err, html) =>
roaster @buffer.getText(), (err, html) =>
if err
@setErrorHtml(err)
else

View File

@ -5,7 +5,7 @@ _ = require 'underscore'
SnippetExpansion = require './snippet-expansion'
Snippet = require './snippet'
TextMatePackage = require 'text-mate-package'
CSON = require 'cson'
CSON = require 'season'
async = require 'async'
module.exports =
@ -41,7 +41,7 @@ module.exports =
loadSnippetFile = (filename, done) =>
return done() if filename.indexOf('.') is 0
filepath = fsUtils.join(snippetsDirPath, filename)
CSON.readObjectAsync filepath, (err, object) =>
CSON.readFile filepath, (err, object) =>
if err
console.warn "Error reading snippets file '#{filepath}': #{err.stack}"
else
@ -66,7 +66,7 @@ module.exports =
try
readObject =
if CSON.isObjectPath(filepath)
CSON.readObjectAsync.bind(CSON)
CSON.readFile.bind(CSON)
else
fsUtils.readPlistAsync.bind(fsUtils)

View File

@ -6,9 +6,12 @@
-webkit-user-select: none;
cursor: default;
overflow: hidden;
white-space: nowrap;
min-width: -webkit-min-content;
}
.status-bar .git-branch {
margin-left: 10px;
float: right;
}

View File

@ -36,7 +36,7 @@ describe "TabBarView", ->
registerDeserializer(TestView)
item1 = new TestView('Item 1')
item2 = new TestView('Item 2')
editSession1 = project.buildEditSession('sample.js')
editSession1 = project.open('sample.js')
paneContainer = new PaneContainer
pane = new Pane(item1, editSession1, item2)
pane.showItem(item2)
@ -77,7 +77,7 @@ describe "TabBarView", ->
expect(tabBar.tabAtIndex(1).find('.title')).toHaveText 'Item 3'
it "adds the 'modified' class to the new tab if the item is initially modified", ->
editSession2 = project.buildEditSession('sample.txt')
editSession2 = project.open('sample.txt')
editSession2.insertText('x')
pane.showItem(editSession2)
expect(tabBar.tabForItem(editSession2)).toHaveClass 'modified'

View File

@ -2,6 +2,7 @@
FileView = require './file-view'
Directory = require 'directory'
$ = require 'jquery'
fs = require 'fs'
module.exports =
class DirectoryView extends View
@ -34,7 +35,7 @@ class DirectoryView extends View
@updateStatus()
@updateStatus()
else
iconClass = 'repository-icon' if path is git.getWorkingDirectory()
iconClass = 'repository-icon' if @isRepositoryRoot()
@directoryName.addClass(iconClass)
@ -53,6 +54,12 @@ class DirectoryView extends View
getPath: ->
@directory.path
isRepositoryRoot: ->
try
git? and git.getWorkingDirectory() is fs.realpathSync(@getPath())
catch e
false
isPathIgnored: (path) ->
config.get("core.hideGitIgnoredFiles") and git?.isPathIgnored(path)

View File

@ -22,7 +22,6 @@ describe "Whitespace", ->
spyOn(fsUtils, 'write')
config.set("whitespace.ensureSingleTrailingNewline", false)
config.update()
# works for buffers that are already open when extension is initialized
editor.insertText("foo \nbar\t \n\nbaz")
@ -45,7 +44,6 @@ describe "Whitespace", ->
afterEach ->
config.set("whitespace.ensureSingleTrailingNewline", originalConfigValue)
config.update()
it "adds a trailing newline when there is no trailing newline", ->
editor.insertText "foo"
@ -74,7 +72,6 @@ describe "Whitespace", ->
it "does not add trailing newline if ensureSingleTrailingNewline is false", ->
config.set("whitespace.ensureSingleTrailingNewline", false)
config.update()
editor.insertText "no trailing newline"
editor.getBuffer().save()

View File

@ -0,0 +1,50 @@
path = require 'path'
fs = require 'fs'
_ = require 'underscore'
async = require 'async'
mkdirp = require 'mkdirp'
fsUtils = require 'fs-utils'
symlinkCommand = (sourcePath, destinationPath, callback) ->
mkdirp fsUtils.directory(destinationPath), (error) ->
if error?
callback(error)
else
fs.symlink sourcePath, destinationPath, (error) ->
if error?
callback(error)
else
fs.chmod(destinationPath, 0o755, callback)
unlinkCommand = (destinationPath, callback) ->
fs.exists destinationPath, (exists) ->
if exists
fs.unlink(destinationPath, callback)
else
callback()
module.exports =
findInstallDirectory: (callback) ->
directories = ['/opt/boxen','/opt/github','/usr/local']
async.detect(directories, fsUtils.isDirectoryAsync, callback)
install: (commandPath, commandName, callback) ->
if not commandName? or _.isFunction(commandName)
callback = commandName
commandName = path.basename(commandPath, path.extname(commandPath))
installCallback = (error) ->
if error?
console.warn "Failed to install `#{commandName}` binary", error
callback?(error)
@findInstallDirectory (directory) ->
if directory?
destinationPath = path.join(directory, 'bin', commandName)
unlinkCommand destinationPath, (error) ->
if error?
installCallback(error)
else
symlinkCommand(commandPath, destinationPath, installCallback)
else
installCallback(new Error("No destination directory exists to install"))

View File

@ -1,117 +0,0 @@
require 'underscore-extensions'
_ = require 'underscore'
fs = require 'fs'
fsUtils = require 'fs-utils'
module.exports =
isObjectPath: (path) ->
extension = fsUtils.extension(path)
extension is '.cson' or extension is '.json'
readObject: (path) ->
@parseObject(path, fsUtils.read(path))
readObjectAsync: (path, done) ->
fs.readFile path, 'utf8', (err, contents) =>
return done(err) if err?
try
done(null, @parseObject(path, contents))
catch err
done(err)
parseObject: (path, contents) ->
if fsUtils.extension(path) is '.cson'
CoffeeScript = require 'coffee-script'
CoffeeScript.eval(contents, bare: true)
else
JSON.parse(contents)
writeObject: (path, object) ->
if fsUtils.extension(path) is '.cson'
content = @stringify(object)
else
content = JSON.stringify(object, undefined, 2)
fsUtils.write(path, "#{content}\n")
stringifyIndent: (level=0) -> _.multiplyString(' ', Math.max(level, 0))
stringifyString: (string) ->
string = JSON.stringify(string)
string = string[1...-1] # Remove surrounding double quotes
string = string.replace(/\\"/g, '"') # Unescape escaped double quotes
string = string.replace(/'/g, '\\\'') # Escape single quotes
"'#{string}'" # Wrap in single quotes
stringifyBoolean: (boolean) -> "#{boolean}"
stringifyNumber: (number) -> "#{number}"
stringifyNull: -> 'null'
stringifyArray: (array, indentLevel=0) ->
return '[]' if array.length is 0
cson = '[\n'
for value in array
indent = @stringifyIndent(indentLevel + 2)
cson += indent
if _.isString(value)
cson += @stringifyString(value)
else if _.isBoolean(value)
cson += @stringifyBoolean(value)
else if _.isNumber(value)
cson += @stringifyNumber(value)
else if _.isNull(value) or value is undefined
cson += @stringifyNull(value)
else if _.isArray(value)
cson += @stringifyArray(value, indentLevel + 2)
else if _.isObject(value)
cson += "{\n#{@stringifyObject(value, indentLevel + 4)}\n#{indent}}"
else
throw new Error("Unrecognized type for array value: #{value}")
cson += '\n'
"#{cson}#{@stringifyIndent(indentLevel)}]"
stringifyObject: (object, indentLevel=0) ->
return '{}' if _.isEmpty(object)
cson = ''
prefix = ''
for key, value of object
continue if value is undefined
if _.isFunction(value)
throw new Error("Function specified as value to key: #{key}")
cson += "#{prefix}#{@stringifyIndent(indentLevel)}'#{key}':"
if _.isString(value)
cson += " #{@stringifyString(value)}"
else if _.isBoolean(value)
cson += " #{@stringifyBoolean(value)}"
else if _.isNumber(value)
cson += " #{@stringifyNumber(value)}"
else if _.isNull(value)
cson += " #{@stringifyNull(value)}"
else if _.isArray(value)
cson += " #{@stringifyArray(value, indentLevel)}"
else if _.isObject(value)
if _.isEmpty(value)
cson += ' {}'
else
cson += "\n#{@stringifyObject(value, indentLevel + 2)}"
else
throw new Error("Unrecognized value type for key: #{key} with value: #{value}")
prefix = '\n'
cson
stringify: (object) ->
throw new Error("Cannot stringify undefined object") if object is undefined
throw new Error("Cannot stringify function: #{object}") if _.isFunction(object)
return @stringifyString(object) if _.isString(object)
return @stringifyBoolean(object) if _.isBoolean(object)
return @stringifyNumber(object) if _.isNumber(object)
return @stringifyNull(object) if _.isNull(object)
return @stringifyArray(object) if _.isArray(object)
return @stringifyObject(object) if _.isObject(object)
throw new Error("Unrecognized type to stringify: #{object}")

View File

@ -73,8 +73,11 @@ module.exports =
return done(false) unless path?.length > 0
fs.exists path, (exists) ->
if exists
fs.stat path, (err, stat) ->
done(stat?.isDirectory() ? false)
fs.stat path, (error, stat) ->
if error?
done(false)
else
done(stat.isDirectory())
else
done(false)
@ -87,6 +90,13 @@ module.exports =
catch e
false
# Returns true if the specified path is exectuable.
isExecutable: (path) ->
try
(fs.statSync(path).mode & 0o777 & 1) isnt 0
catch e
false
# Returns an array with all the names of files contained
# in the directory path.
list: (rootPath, extensions) ->
@ -334,15 +344,15 @@ module.exports =
done(err)
readObject: (path) ->
cson = require 'cson'
if cson.isObjectPath(path)
cson.readObject(path)
CSON = require 'season'
if CSON.isObjectPath(path)
CSON.readFileSync(path)
else
@readPlist(path)
readObjectAsync: (path, done) ->
cson = require 'cson'
if cson.isObjectPath(path)
cson.readObjectAsync(path, done)
CSON = require 'season'
if CSON.isObjectPath(path)
CSON.readFile(path, done)
else
@readPlistAsync(path, done)

View File

@ -37,6 +37,15 @@ $.fn.isOnDom = ->
$.fn.isVisible = ->
@is(':visible')
$.fn.isDisabled = ->
!!@attr('disabled')
$.fn.enable = ->
@removeAttr('disabled')
$.fn.disable = ->
@attr('disabled', 'disabled')
$.fn.containsElement = (element) ->
(element[0].compareDocumentPosition(this[0]) & 8) == 8

View File

@ -1,20 +1,27 @@
_ = require 'underscore'
module.exports =
subscribe: (eventEmitter, eventName, callback) ->
eventEmitter.on eventName, callback
subscribeWith: (eventEmitter, methodName, args) ->
eventEmitter[methodName](args...)
@subscriptions ?= []
@subscriptionsByObject ?= new WeakMap
@subscriptionsByObject.set(eventEmitter, []) unless @subscriptionsByObject.has(eventEmitter)
subscription = cancel: -> eventEmitter.off eventName, callback
eventName = _.first(args)
callback = _.last(args)
subscription = cancel: ->
# node's EventEmitter doesn't have 'off' method.
removeListener = eventEmitter.off ? eventEmitter.removeListener
removeListener.call eventEmitter, eventName, callback
@subscriptions.push(subscription)
@subscriptionsByObject.get(eventEmitter).push(subscription)
subscribeToCommand: (view, eventName, callback) ->
view.command eventName, callback
@subscriptions ?= []
@subscriptions.push(cancel: -> view.off eventName, callback)
subscribe: (eventEmitter, args...) ->
@subscribeWith(eventEmitter, 'on', args)
subscribeToCommand: (eventEmitter, args...) ->
@subscribeWith(eventEmitter, 'command', args)
unsubscribe: (object) ->
if object?

View File

@ -34,7 +34,8 @@ global.document = window.document
global.callTaskMethod = (method, args...) ->
process.send(method: method, args: args)
# The worker's initial handler replaces itglobal when `start` is invoked
# The worker's initial handler replaces the global hadndler when `start` is
# invoked.
global.handler =
start: ({globals, handlerPath}) ->
for key, value of globals

View File

@ -4,6 +4,7 @@ _.mixin
remove: (array, element) ->
index = array.indexOf(element)
array.splice(index, 1) if index >= 0
array
spliceWithArray: (originalArray, start, length, insertedArray, chunkSize=100000) ->
if insertedArray.length < chunkSize
@ -126,6 +127,35 @@ _.mixin
endsWith: (string, suffix) ->
string.indexOf(suffix, string.length - suffix.length) isnt -1
# Transform the given object into another object.
#
# `object` - The object to transform.
# `iterator` -
# A function that takes `(key, value)` arguments and returns a
# `[key, value]` tuple.
#
# Returns a new object based with the key/values returned by the iterator.
mapObject: (object, iterator) ->
newObject = {}
for key, value of object
[key, value] = iterator(key, value)
newObject[key] = value
newObject
# Deep clones the given JSON object.
#
# `object` - The JSON object to clone.
#
# Returns a deep clone of the JSON object.
deepClone: (object) ->
if _.isArray(object)
object.map (value) -> _.deepClone(value)
else if _.isObject(object)
@mapObject object, (key, value) => [key, @deepClone(value)]
else
object
valueForKeyPath: (object, keyPath) ->
keys = keyPath.split('.')
for key in keys

View File

@ -62,7 +62,7 @@ html, body {
.pane .item-views > * {
-webkit-flex: 1;
min-height: 0;
min-width: 0;
}
}

1
vendor/apm vendored Submodule

@ -0,0 +1 @@
Subproject commit 01ded1c72cb71a0ac85fe267e3dafc370797211f

@ -1 +1 @@
Subproject commit daad8ef03de9630e74578a046240fd9acc63b8b5
Subproject commit 4b74231cb27d8d6e8e917947c566d21ec9d6e76a