Merge pull request #126 from github/dev

Merge dev into master
This commit is contained in:
Kevin Sawicki
2013-01-11 11:39:40 -08:00
75 changed files with 1308 additions and 627 deletions

View File

@@ -0,0 +1,44 @@
".source.coffee":
"Describe block":
prefix: "de"
body: """
describe "${1:description}", ->
${2:body}
"""
"It block":
prefix: "i"
body: """
it "$1", ->
$2
"""
"Before each":
prefix: "be"
body: """
beforeEach ->
$1
"""
"After each":
prefix: "af"
body: """
afterEach ->
$1
"""
"Expectation":
prefix: "ex"
body: "expect($1).to$2"
"Console log":
prefix: "log"
body: "console.log $1"
"Range array":
prefix: "ra"
body: "[[$1, $2], [$3, $4]]"
"Point array":
prefix: "pt"
body: "[$1, $2]"
"Key-value pair":
prefix: ":"
body: '${1:"${2:key}"}: ${3:value}'
"Create Jasmine spy":
prefix: "spy"
body: 'jasmine.createSpy("${1:description}")$2'

View File

@@ -1,34 +0,0 @@
snippet de "Describe block"
describe "${1:description}", ->
${2:body}
endsnippet
snippet i "It block"
it "$1", ->
$2
endsnippet
snippet be "Before each"
beforeEach ->
$1
endsnippet
snippet ex "Expectation"
expect($1).to$2
endsnippet
snippet log "Console log"
console.log $1
endsnippet
snippet ra "Range array"
[[$1, $2], [$3, $4]]
endsnippet
snippet pt "Point array"
[$1, $2]
endsnippet
snippet spy "Jasmine spy"
jasmine.createSpy("${1:description}")$2
endsnippet

View File

@@ -1,3 +1,3 @@
[docs]
title = The Guide to Atom
manifest = intro.md, configuring-and-extending.md, styling.md, extensions/intro.md, extensions/markdown-preview.md, extensions/wrap-guide.md
manifest = intro.md, features.md, configuring-and-extending.md, styling.md, packages/intro.md, packages/installing.md, packages/markdown-preview.md, packages/wrap-guide.md

View File

@@ -157,153 +157,6 @@ directory, it will automatically be translated from TextMate's format to CSS
so it works with Atom. There are a few slight differences between TextMate's
semantics and those of stylesheets, but they should be negligible in practice.
# Packages
## Installing Packages (Partially Implemented)
To install a package, clone it into the `~/.atom/packages` directory.
If you want to disable a package without removing it from the packages
directory, insert its name into `config.core.disabledPackages`:
config.cson:
```coffeescript
core:
disabledPackages: [
"fuzzy-finder",
"tree-view"
]
```
## Anatomy of a Package
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.
### Config Settings
### Stylesheets
### Keymaps (Not Implemented)
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** (todo) for more information.
### Snippets (Not Implemented)
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.
### Grammars
## TextMate Compatibility

View File

@@ -1 +0,0 @@
## Extensions

1
docs/features.md Normal file
View File

@@ -0,0 +1 @@
# Features

View File

@@ -1,3 +1,3 @@
## The Definitive Guide to Atom
# The Atom Guide
Welcome!

View File

@@ -0,0 +1,14 @@
## Installing Packages (Partially Implemented)
To install a package, clone it into the `~/.atom/packages` directory.
If you want to disable a package without removing it from the packages
directory, insert its name into `config.core.disabledPackages`:
config.cson:
```coffeescript
core:
disabledPackages: [
"fuzzy-finder",
"tree-view"
]
```

150
docs/packages/intro.md Normal file
View File

@@ -0,0 +1,150 @@
# Packages
### Package Layout
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.
#### Config Settings
#### Stylesheets
#### Keymaps (Not Implemented)
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** (todo) for more information.
#### Snippets (Not Implemented)
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.
### Included Packages
Atom comes with several built-in packages that add features to the default
editor.
The current built-in packages are:
* Autocomplete
* Command Logger
* Command Palette
* Fuzzy finder
* [Markdown Preview](#markdown-preview)
* Outline View
* Snippets
* Status Bar
* Strip Trailing Whitespace
* Tabs
* Tree View
* [Wrap Guide](#wrap-guide)

View File

@@ -10,14 +10,16 @@ var $git = {};
native function getDiffStats(path);
native function isSubmodule(path);
native function refreshIndex();
native function destroy();
function GitRepository(path) {
var repo = getRepository(path);
if (repo) {
repo.constructor = GitRepository;
repo.__proto__ = GitRepository.prototype;
return repo;
}
if (!repo)
throw new Error("No Git repository found searching path: " + path);
repo.constructor = GitRepository;
repo.__proto__ = GitRepository.prototype;
return repo;
}
GitRepository.prototype.getHead = getHead;
@@ -28,5 +30,6 @@ var $git = {};
GitRepository.prototype.getDiffStats = getDiffStats;
GitRepository.prototype.isSubmodule = isSubmodule;
GitRepository.prototype.refreshIndex = refreshIndex;
GitRepository.prototype.destroy = destroy;
this.GitRepository = GitRepository;
})();

View File

@@ -16,10 +16,17 @@ public:
}
~GitRepository() {
git_repository_free(repo);
Destroy();
}
BOOL exists() {
void Destroy() {
if (Exists()) {
git_repository_free(repo);
repo = NULL;
}
}
BOOL Exists() {
return repo != NULL;
}
@@ -190,7 +197,7 @@ bool Git::Execute(const CefString& name,
CefString& exception) {
if (name == "getRepository") {
GitRepository *repository = new GitRepository(arguments[0]->GetStringValue().ToString().c_str());
if (repository->exists()) {
if (repository->Exists()) {
CefRefPtr<CefBase> userData = repository;
retval = CefV8Value::CreateObject(NULL);
retval->SetUserData(userData);
@@ -248,6 +255,12 @@ bool Git::Execute(const CefString& name,
return true;
}
if (name == "destroy") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
userData->Destroy();
return true;
}
return false;
}

View File

@@ -7,15 +7,18 @@ namespace v8_extensions {
class Native : public CefV8Handler {
public:
Native();
virtual bool Execute(const CefString& name,
CefRefPtr<CefV8Value> object,
const CefV8ValueList& arguments,
CefRefPtr<CefV8Value>& retval,
CefString& exception) OVERRIDE;
// Provide the reference counting implementation for this class.
IMPLEMENT_REFCOUNTING(Native);
private:
std::string windowState;
};
}

View File

@@ -76,4 +76,10 @@ var $native = {};
native function getPlatform();
$native.getPlatform = getPlatform;
native function setWindowState(state);
$native.setWindowState = setWindowState;
native function getWindowState();
$native.getWindowState = getWindowState;
})();

View File

@@ -31,6 +31,7 @@ void throwException(const CefRefPtr<CefV8Value>& global, CefRefPtr<CefV8Exceptio
Native::Native() : CefV8Handler() {
NSString *filePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"v8_extensions/native.js"];
NSString *extensionCode = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
windowState = "{}";
CefRegisterExtension("v8/native", [extensionCode UTF8String], this);
}
@@ -486,6 +487,16 @@ bool Native::Execute(const CefString& name,
return true;
}
else if (name == "setWindowState") {
windowState = arguments[0]->GetStringValue().ToString();
return true;
}
else if (name == "getWindowState") {
retval = CefV8Value::CreateString(windowState);
return true;
}
return false;
};

View File

@@ -18,6 +18,11 @@ describe "the `atom` global", ->
atom.loadPackage("package-with-module")
expect(rootView.activatePackage).toHaveBeenCalledWith('package-with-module', extension)
it "logs warning instead of throwing an exception if a package fails to load", ->
spyOn(console, "warn")
expect(-> atom.loadPackage("package-that-throws-an-exception")).not.toThrow()
expect(console.warn).toHaveBeenCalled()
describe "keymap loading", ->
describe "when package.json does not contain a 'keymaps' manifest", ->
it "loads all keymaps in the directory", ->

View File

@@ -307,6 +307,36 @@ describe "EditSession", ->
editSession.moveCursorToEndOfWord()
expect(editSession.getCursorBufferPosition()).toEqual endPosition
describe ".getCurrentParagraphBufferRange()", ->
it "returns the buffer range of the current paragraph, delimited by blank lines or the beginning / end of the file", ->
buffer.setText """
I am the first paragraph,
bordered by the beginning of
the file
#{' '}
I am the second paragraph
with blank lines above and below
me.
I am the last paragraph,
bordered by the end of the file.
"""
# in a paragraph
editSession.setCursorBufferPosition([1, 7])
expect(editSession.getCurrentParagraphBufferRange()).toEqual [[0, 0], [2, 8]]
editSession.setCursorBufferPosition([7, 1])
expect(editSession.getCurrentParagraphBufferRange()).toEqual [[5, 0], [7, 3]]
editSession.setCursorBufferPosition([9, 10])
expect(editSession.getCurrentParagraphBufferRange()).toEqual [[9, 0], [10, 32]]
# between paragraphs
editSession.setCursorBufferPosition([3, 1])
expect(editSession.getCurrentParagraphBufferRange()).toBeUndefined()
describe "selection", ->
selection = null
@@ -675,10 +705,7 @@ describe "EditSession", ->
editSession.insertText('holy cow')
expect(editSession.lineForScreenRow(2).fold).toBeUndefined()
describe "when auto-indent is enabled and the `autoIndent` option is true", ->
beforeEach ->
editSession.setAutoIndent(true)
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", ->
@@ -739,16 +766,13 @@ describe "EditSession", ->
removeLeadingWhitespace = (text) -> text.replace(/^\s*/, '')
describe "when the cursor is preceded only by whitespace", ->
describe "when auto-indent is enabled", ->
beforeEach ->
editSession.setAutoIndent(true)
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')
editSession.insertText('\n xx', autoIndent: true)
editSession.setCursorBufferPosition([3, 1])
editSession.insertText(text, normalizeIndent: true)
editSession.insertText(text, normalizeIndent: true, autoIndent: true)
expect(editSession.lineForBufferRow(3)).toBe " while (true) {"
expect(editSession.lineForBufferRow(4)).toBe " foo();"
@@ -759,7 +783,7 @@ describe "EditSession", ->
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)
editSession.insertText(removeLeadingWhitespace(text), normalizeIndent: true, indentBasis: 2, autoIndent: true)
expect(editSession.lineForBufferRow(3)).toBe " while (true) {"
expect(editSession.lineForBufferRow(4)).toBe " foo();"
@@ -775,7 +799,7 @@ describe "EditSession", ->
"""
editSession.setCursorBufferPosition([1, 0])
editSession.insertText(text, normalizeIndent: true)
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();"
@@ -791,7 +815,7 @@ describe "EditSession", ->
"""
editSession.setCursorBufferPosition([1, 0])
editSession.insertText(text, normalizeIndent: true)
editSession.insertText(text, normalizeIndent: true, autoIndent: true)
expect(editSession.lineForBufferRow(1)).toBe "\t\twhile (true) {"
expect(editSession.lineForBufferRow(2)).toBe "\t\t\tfoo();"
@@ -820,9 +844,6 @@ describe "EditSession", ->
expect(editSession.lineForBufferRow(6)).toBe " bar();"
describe "if auto-indent is disabled", ->
beforeEach ->
expect(editSession.autoIndent).toBeFalsy()
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 ')
@@ -845,7 +866,6 @@ describe "EditSession", ->
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.setAutoIndent(true)
editSession.buffer.delete([[2, 0], [2, 2]])
editSession.insertText(text, normalizeIndent:true)
@@ -856,7 +876,6 @@ describe "EditSession", ->
describe "when an indentBasis is provided", ->
it "normalizes the indentation level of all lines based on the level of the existing first line", ->
editSession.setAutoIndent(true)
editSession.buffer.delete([[2, 0], [2, 2]])
editSession.insertText(removeLeadingWhitespace(text), normalizeIndent:true, indentBasis: 2)
@@ -1311,8 +1330,7 @@ describe "EditSession", ->
it "moves the cursor to the end of the leading whitespace and inserts enough whitespace to bring the line to the suggested level of indentaion", ->
buffer.insert([5, 0], " \n")
editSession.setCursorBufferPosition [5, 0]
editSession.setAutoIndent(true)
editSession.indent()
editSession.indent(autoIndent: true)
expect(buffer.lineForRow(5)).toMatch /^\s+$/
expect(buffer.lineForRow(5).length).toBe 6
expect(editSession.getCursorBufferPosition()).toEqual [5, 6]
@@ -1323,8 +1341,7 @@ describe "EditSession", ->
editSession.softTabs = false
buffer.insert([5, 0], "\t\n")
editSession.setCursorBufferPosition [5, 0]
editSession.setAutoIndent(true)
editSession.indent()
editSession.indent(autoIndent: true)
expect(buffer.lineForRow(5)).toMatch /^\t\t\t$/
expect(editSession.getCursorBufferPosition()).toEqual [5, 3]
@@ -1333,8 +1350,7 @@ describe "EditSession", ->
it "moves the cursor to the end of the leading whitespace and inserts 'tabLength' spaces into the buffer", ->
buffer.insert([7, 0], " \n")
editSession.setCursorBufferPosition [7, 2]
editSession.setAutoIndent(true)
editSession.indent()
editSession.indent(autoIndent: true)
expect(buffer.lineForRow(7)).toMatch /^\s+$/
expect(buffer.lineForRow(7).length).toBe 8
expect(editSession.getCursorBufferPosition()).toEqual [7, 8]
@@ -1345,8 +1361,7 @@ describe "EditSession", ->
editSession.softTabs = false
buffer.insert([7, 0], "\t\t\t\n")
editSession.setCursorBufferPosition [7, 1]
editSession.setAutoIndent(true)
editSession.indent()
editSession.indent(autoIndent: true)
expect(buffer.lineForRow(7)).toMatch /^\t\t\t\t$/
expect(editSession.getCursorBufferPosition()).toEqual [7, 4]
@@ -1373,11 +1388,7 @@ describe "EditSession", ->
expect(editSession.getCursorScreenPosition()).toEqual [0, editSession.getTabLength() * 2]
describe "pasteboard operations", ->
pasteboard = null
beforeEach ->
pasteboard = 'first'
spyOn($native, 'writeToPasteboard').andCallFake (text) -> pasteboard = text
spyOn($native, 'readFromPasteboard').andCallFake -> pasteboard
editSession.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]])
describe ".cutSelectedText()", ->
@@ -1396,7 +1407,7 @@ describe "EditSession", ->
editSession.cutToEndOfLine()
expect(buffer.lineForRow(2)).toBe ' if (items.length'
expect(buffer.lineForRow(3)).toBe ' var pivot = item'
expect(pasteboard).toBe ' <= 1) return items;\ns.shift(), current, left = [], right = [];'
expect(pasteboard.read()[0]).toBe ' <= 1) return items;\ns.shift(), current, left = [], right = [];'
describe "when text is selected", ->
it "only cuts the selected text, not to the end of the line", ->
@@ -1406,7 +1417,7 @@ describe "EditSession", ->
expect(buffer.lineForRow(2)).toBe ' if (items.lengthurn items;'
expect(buffer.lineForRow(3)).toBe ' var pivot = item'
expect(pasteboard).toBe ' <= 1) ret\ns.shift(), current, left = [], right = [];'
expect(pasteboard.read()[0]).toBe ' <= 1) ret\ns.shift(), current, left = [], right = [];'
describe ".copySelectedText()", ->
it "copies selected text onto the clipboard", ->
@@ -1417,21 +1428,21 @@ describe "EditSession", ->
describe ".pasteText()", ->
it "pastes text into the buffer", ->
pasteboard.write('first')
editSession.pasteText()
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.setAutoIndent(true)
editSession.setSelectedBufferRange([[4, 4], [7, 5]])
editSession.copySelectedText()
editSession.setCursorBufferPosition([10, 0])
editSession.pasteText()
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 " }"
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", ->
@@ -1931,3 +1942,57 @@ describe "EditSession", ->
editSession.setCursorScreenPosition([0, 1])
editSession.buffer.reload()
expect(editSession.getCursorScreenPosition()).toEqual [0,1]
describe "auto-indent", ->
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 " "
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 ""
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 " "
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 "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()
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()
editSession.pasteText()
expect(editSession.lineForBufferRow(2)).toBe " 0"
expect(editSession.lineForBufferRow(3)).toBe " 2"
expect(editSession.lineForBufferRow(4)).toBe " 4"

View File

@@ -2136,3 +2136,70 @@ describe "Editor", ->
expect(editor.reloadGrammar()).toBeFalsy()
expect(editor.updateDisplay).not.toHaveBeenCalled()
expect(editor.getGrammar().name).toBe 'JavaScript'
it "emits an editor:grammar-changed event when updated", ->
rootView.open(path)
editor = rootView.getActiveEditor()
eventHandler = jasmine.createSpy('eventHandler')
editor.on('editor:grammar-changed', eventHandler)
editor.reloadGrammar()
expect(eventHandler).not.toHaveBeenCalled()
jsGrammar = syntax.grammarForFilePath('/tmp/js.js')
rootView.project.addGrammarOverrideForPath(path, jsGrammar)
editor.reloadGrammar()
expect(eventHandler).toHaveBeenCalled()
describe ".replaceSelectedText()", ->
it "doesn't call the replace function when the selection is empty", ->
replaced = false
edited = false
replacer = (text) ->
replaced = true
'new'
editor.moveCursorToTop()
edited = editor.replaceSelectedText(replacer)
expect(replaced).toBe false
expect(edited).toBe false
it "returns true when transformed text is non-empty", ->
replaced = false
edited = false
replacer = (text) ->
replaced = true
'new'
editor.moveCursorToTop()
editor.selectToEndOfLine()
edited = editor.replaceSelectedText(replacer)
expect(replaced).toBe true
expect(edited).toBe true
it "returns false when transformed text is null", ->
replaced = false
edited = false
replacer = (text) ->
replaced = true
null
editor.moveCursorToTop()
editor.selectToEndOfLine()
edited = editor.replaceSelectedText(replacer)
expect(replaced).toBe true
expect(edited).toBe false
it "returns false when transformed text is undefined", ->
replaced = false
edited = false
replacer = (text) ->
replaced = true
undefined
editor.moveCursorToTop()
editor.selectToEndOfLine()
edited = editor.replaceSelectedText(replacer)
expect(replaced).toBe true
expect(edited).toBe false

View File

@@ -8,7 +8,7 @@ describe "Git", ->
describe "@open(path)", ->
it "returns null when no repository is found", ->
expect(Git.open('/tmp/nogit.txt')).toBeNull(0)
expect(Git.open('/tmp/nogit.txt')).toBeNull()
describe "new Git(path)", ->
it "throws an exception when no repository is found", ->
@@ -121,3 +121,9 @@ describe "Git", ->
expect(repo.checkoutHead(path1)).toBeTruthy()
expect(fs.read(path2)).toBe('path 2 is edited')
expect(repo.isPathModified(path2)).toBeTruthy()
describe ".destroy()", ->
it "throws an exception when any method is called after it is called", ->
repo = new Git(require.resolve('fixtures/git/master.git/HEAD'))
repo.destroy()
expect(-> repo.getHead()).toThrow()

View File

@@ -1,17 +1,10 @@
describe "Pasteboard", ->
nativePasteboard = null
beforeEach ->
nativePasteboard = 'first'
spyOn($native, 'writeToPasteboard').andCallFake (text) -> nativePasteboard = text
spyOn($native, 'readFromPasteboard').andCallFake -> nativePasteboard
describe "write(text, metadata) and read()", ->
it "writes and reads text to/from the native pasteboard", ->
expect(pasteboard.read()).toEqual ['first']
expect(pasteboard.read()).toEqual ['initial pasteboard content']
pasteboard.write('next')
expect(nativePasteboard).toBe 'next'
expect(pasteboard.read()[0]).toBe 'next'
it "returns metadata if the item on the native pasteboard matches the last written item", ->
pasteboard.write('next', {meta: 'data'})
expect(nativePasteboard).toBe 'next'
expect(pasteboard.read()).toEqual ['next', {meta: 'data'}]

View File

@@ -69,52 +69,67 @@ describe "RootView", ->
path = require.resolve 'fixtures'
rootView.remove()
rootView = new RootView(path)
rootView.open('dir/a')
editor1 = rootView.getActiveEditor()
editor2 = editor1.splitRight()
editor3 = editor2.splitRight()
editor4 = editor2.splitDown()
editor2.edit(rootView.project.buildEditSessionForPath('dir/b'))
editor3.edit(rootView.project.buildEditSessionForPath('sample.js'))
editor3.setCursorScreenPosition([2, 4])
editor4.edit(rootView.project.buildEditSessionForPath('sample.txt'))
editor4.setCursorScreenPosition([0, 2])
rootView.attachToDom()
editor2.focus()
viewState = rootView.serialize()
rootView.remove()
describe "when there are open editors", ->
beforeEach ->
rootView.open('dir/a')
editor1 = rootView.getActiveEditor()
editor2 = editor1.splitRight()
editor3 = editor2.splitRight()
editor4 = editor2.splitDown()
editor2.edit(rootView.project.buildEditSessionForPath('dir/b'))
editor3.edit(rootView.project.buildEditSessionForPath('sample.js'))
editor3.setCursorScreenPosition([2, 4])
editor4.edit(rootView.project.buildEditSessionForPath('sample.txt'))
editor4.setCursorScreenPosition([0, 2])
rootView.attachToDom()
editor2.focus()
viewState = rootView.serialize()
rootView.remove()
it "constructs the view with the same project and panes", ->
rootView = RootView.deserialize(viewState)
rootView.attachToDom()
it "constructs the view with the same project and panes", ->
rootView = RootView.deserialize(viewState)
rootView.attachToDom()
expect(rootView.getEditors().length).toBe 4
editor1 = rootView.panes.find('.row > .pane .editor:eq(0)').view()
editor3 = rootView.panes.find('.row > .pane .editor:eq(1)').view()
editor2 = rootView.panes.find('.row > .column > .pane .editor:eq(0)').view()
editor4 = rootView.panes.find('.row > .column > .pane .editor:eq(1)').view()
expect(rootView.getEditors().length).toBe 4
editor1 = rootView.panes.find('.row > .pane .editor:eq(0)').view()
editor3 = rootView.panes.find('.row > .pane .editor:eq(1)').view()
editor2 = rootView.panes.find('.row > .column > .pane .editor:eq(0)').view()
editor4 = rootView.panes.find('.row > .column > .pane .editor:eq(1)').view()
expect(editor1.getPath()).toBe require.resolve('fixtures/dir/a')
expect(editor2.getPath()).toBe require.resolve('fixtures/dir/b')
expect(editor3.getPath()).toBe require.resolve('fixtures/sample.js')
expect(editor3.getCursorScreenPosition()).toEqual [2, 4]
expect(editor4.getPath()).toBe require.resolve('fixtures/sample.txt')
expect(editor4.getCursorScreenPosition()).toEqual [0, 2]
expect(editor1.getPath()).toBe require.resolve('fixtures/dir/a')
expect(editor2.getPath()).toBe require.resolve('fixtures/dir/b')
expect(editor3.getPath()).toBe require.resolve('fixtures/sample.js')
expect(editor3.getCursorScreenPosition()).toEqual [2, 4]
expect(editor4.getPath()).toBe require.resolve('fixtures/sample.txt')
expect(editor4.getCursorScreenPosition()).toEqual [0, 2]
# ensure adjust pane dimensions is called
expect(editor1.width()).toBeGreaterThan 0
expect(editor2.width()).toBeGreaterThan 0
expect(editor3.width()).toBeGreaterThan 0
expect(editor4.width()).toBeGreaterThan 0
# ensure adjust pane dimensions is called
expect(editor1.width()).toBeGreaterThan 0
expect(editor2.width()).toBeGreaterThan 0
expect(editor3.width()).toBeGreaterThan 0
expect(editor4.width()).toBeGreaterThan 0
# ensure correct editor is focused again
expect(editor2.isFocused).toBeTruthy()
expect(editor1.isFocused).toBeFalsy()
expect(editor3.isFocused).toBeFalsy()
expect(editor4.isFocused).toBeFalsy()
# ensure correct editor is focused again
expect(editor2.isFocused).toBeTruthy()
expect(editor1.isFocused).toBeFalsy()
expect(editor3.isFocused).toBeFalsy()
expect(editor4.isFocused).toBeFalsy()
expect(rootView.getTitle()).toBe "#{fs.base(editor2.getPath())} #{rootView.project.getPath()}"
describe "where there are no open editors", ->
beforeEach ->
rootView.attachToDom()
viewState = rootView.serialize()
rootView.remove()
it "constructs the view with no open editors", ->
rootView = RootView.deserialize(viewState)
rootView.attachToDom()
expect(rootView.getEditors().length).toBe 0
expect(rootView.getTitle()).toBe "#{fs.base(editor2.getPath())} #{rootView.project.getPath()}"
describe "when called with no pathToOpen", ->
it "opens an empty buffer", ->
@@ -710,3 +725,59 @@ describe "RootView", ->
lowerRightEditor = rightEditor.splitDown()
expect(lowerRightEditor.find(".line:first").text()).toBe " "
describe ".eachEditor(callback)", ->
beforeEach ->
rootView.attachToDom()
it "invokes the callback for existing editor", ->
count = 0
callbackEditor = null
callback = (editor) ->
callbackEditor = editor
count++
rootView.eachEditor(callback)
expect(count).toBe 1
expect(callbackEditor).toBe rootView.getActiveEditor()
it "invokes the callback for new editor", ->
count = 0
callbackEditor = null
callback = (editor) ->
callbackEditor = editor
count++
rootView.eachEditor(callback)
count = 0
callbackEditor = null
rootView.getActiveEditor().splitRight()
expect(count).toBe 1
expect(callbackEditor).toBe rootView.getActiveEditor()
describe ".eachBuffer(callback)", ->
beforeEach ->
rootView.attachToDom()
it "invokes the callback for existing buffer", ->
count = 0
callbackBuffer = null
callback = (buffer) ->
callbackBuffer = buffer
count++
rootView.eachBuffer(callback)
expect(count).toBe 1
expect(callbackBuffer).toBe rootView.getActiveEditor().getBuffer()
it "invokes the callback for new buffer", ->
count = 0
callbackBuffer = null
callback = (buffer) ->
callbackBuffer = buffer
count++
rootView.eachBuffer(callback)
count = 0
callbackBuffer = null
rootView.open(require.resolve('fixtures/sample.txt'))
expect(count).toBe 1
expect(callbackBuffer).toBe rootView.getActiveEditor().getBuffer()

View File

@@ -0,0 +1 @@
throw new Error("This package throws an exception")

View File

@@ -0,0 +1,4 @@
".test":
"Test Snippet":
prefix: "test"
body: "testing 123"

View File

@@ -13,11 +13,12 @@ TokenizedBuffer = require 'tokenized-buffer'
fs = require 'fs'
require 'window'
requireStylesheet "jasmine.css"
require.paths.unshift(require.resolve('fixtures/packages'))
fixturePackagesPath = require.resolve('fixtures/packages')
require.paths.unshift(fixturePackagesPath)
[bindingSetsToRestore, bindingSetsByFirstKeystrokeToRestore] = []
# Load TextMate bundles, which specs rely on (but not other packages)
atom.loadPackages(atom.getAvailableTextMateBundles())
atom.loadTextMatePackages()
beforeEach ->
window.fixturesProject = new Project(require.resolve('fixtures'))
@@ -29,9 +30,11 @@ beforeEach ->
# reset config before each spec; don't load or save from/to `config.json`
window.config = new Config()
config.packageDirPaths.unshift(fixturePackagesPath)
spyOn(config, 'load')
spyOn(config, 'save')
config.set "editor.fontSize", 16
config.set "editor.autoIndent", false
# make editor display updates synchronous
spyOn(Editor.prototype, 'requestDisplayUpdate').andCallFake -> @updateDisplay()
@@ -44,6 +47,10 @@ beforeEach ->
TokenizedBuffer.prototype.chunkSize = Infinity
spyOn(TokenizedBuffer.prototype, "tokenizeInBackground").andCallFake -> @tokenizeNextChunk()
pasteboardContent = 'initial pasteboard content'
spyOn($native, 'writeToPasteboard').andCallFake (text) -> pasteboardContent = text
spyOn($native, 'readFromPasteboard').andCallFake -> pasteboardContent
afterEach ->
keymap.bindingSets = bindingSetsToRestore
keymap.bindingSetsByFirstKeystrokeToRestore = bindingSetsByFirstKeystrokeToRestore
@@ -193,8 +200,5 @@ $.fn.textInput = (data) ->
event = jQuery.event.fix(event)
$(this).trigger(event)
$.fn.simulateDomAttachment = ->
$('<html>').append(this)
unless fs.md5ForPath(require.resolve('fixtures/sample.js')) == "dd38087d0d7e3e4802a6d3f9b9745f2b"
throw "Sample.js is modified"

View File

@@ -1,11 +1,13 @@
fs = require 'fs'
require 'spec-helper'
# Run core specs
for path in fs.listTree(require.resolve("spec")) when /-spec\.coffee$/.test path
require path
# Run extension specs
for packagePath in fs.listTree(require.resolve("src/packages"))
for path in fs.listTree(fs.join(packagePath, "spec")) when /-spec\.coffee$/.test path
require path
for packageDirPath in config.packageDirPaths
for packagePath in fs.listTree(packageDirPath)
for path in fs.listTree(fs.join(packagePath, "spec")) when /-spec\.coffee$/.test path
require path

View File

@@ -1,5 +1,5 @@
$ = require 'jquery'
{$$} = require 'space-pen'
{View, $$} = require 'space-pen'
describe 'jQuery extensions', ->
describe '$.fn.preempt(eventName, handler)', ->
@@ -75,3 +75,33 @@ describe 'jQuery extensions', ->
'b2': "B2: Looks evil. Kinda is."
'a1': "A1: Waste perfectly-good steak"
'a2': null
describe "Event.prototype", ->
class GrandchildView extends View
@content: -> @div class: 'grandchild'
class ChildView extends View
@content: ->
@div class: 'child', =>
@subview 'grandchild', new GrandchildView
class ParentView extends View
@content: ->
@div class: 'parent', =>
@subview 'child', new ChildView
[parentView, event] = []
beforeEach ->
parentView = new ParentView
eventHandler = jasmine.createSpy('eventHandler')
parentView.on 'foo', '.child', eventHandler
parentView.child.grandchild.trigger 'foo'
event = eventHandler.argsForCall[0][0]
describe ".currentTargetView()", ->
it "returns the current target's space pen view", ->
expect(event.currentTargetView()).toBe parentView.child
describe ".targetView()", ->
it "returns the target's space pen view", ->
expect(event.targetView()).toBe parentView.child.grandchild

49
spec/time-reporter.coffee Normal file
View File

@@ -0,0 +1,49 @@
_ = require 'underscore'
module.exports =
class TimeReporter extends jasmine.Reporter
timedSpecs: []
timedSuites: {}
constructor: ->
window.logLongestSpec = -> window.logLongestSpecs(1)
window.logLongestSpecs = (number=10) =>
console.log "#{number} longest running specs:"
for spec in _.sortBy(@timedSpecs, (spec) -> -spec.time)[0...number]
console.log "#{spec.time}ms"
console.log spec.description
window.logLongestSuite = -> window.logLongestSuites(1)
window.logLongestSuites = (number=10) =>
console.log "#{number} longest running suites:"
suites = _.map(@timedSuites, (key, value) -> [value, key])
for suite in _.sortBy(suites, (suite) => -suite[1])[0...number]
console.log suite[0], suite[1]
reportSpecStarting: (spec) ->
stack = [spec.description]
suite = spec.suite
while suite
stack.unshift suite.description
@suite = suite.description
suite = suite.parentSuite
@time = new Date().getTime()
reducer = (memo, description, index) ->
"#{memo}#{_.multiplyString(' ', index)}#{description}\n"
@description = _.reduce(stack, reducer, "")
reportSpecResults: ->
return unless @time? and @description?
duration = new Date().getTime() - @time
@timedSpecs.push
description: @description
time: duration
if @timedSuites[@suite]
@timedSuites[@suite] += duration
else
@timedSuites[@suite] = duration
@time = null
@description = null

View File

@@ -1,4 +1,7 @@
Range = require 'range'
EventEmitter = require 'event-emitter'
Subscriber = require 'subscriber'
_ = require 'underscore'
module.exports =
class AnchorRange
@@ -6,11 +9,14 @@ class AnchorRange
end: null
buffer: null
editSession: null # optional
destroyed: false
constructor: (bufferRange, @buffer, @editSession) ->
bufferRange = Range.fromObject(bufferRange)
@startAnchor = @buffer.addAnchorAtPosition(bufferRange.start, ignoreChangesStartingOnAnchor: true)
@endAnchor = @buffer.addAnchorAtPosition(bufferRange.end)
@subscribe @startAnchor, 'destroyed', => @destroy()
@subscribe @endAnchor, 'destroyed', => @destroy()
getBufferRange: ->
new Range(@startAnchor.getBufferPosition(), @endAnchor.getBufferPosition())
@@ -22,7 +28,14 @@ class AnchorRange
@getBufferRange().containsPoint(bufferPosition)
destroy: ->
return if @destroyed
@unsubscribe()
@startAnchor.destroy()
@endAnchor.destroy()
@buffer.removeAnchorRange(this)
@editSession?.removeAnchorRange(this)
@destroyed = true
@trigger 'destroyed'
_.extend(AnchorRange.prototype, EventEmitter)
_.extend(AnchorRange.prototype, Subscriber)

View File

@@ -10,6 +10,7 @@ class Anchor
screenPosition: null
ignoreChangesStartingOnAnchor: false
strong: false
destroyed: false
constructor: (@buffer, options = {}) ->
{ @editSession, @ignoreChangesStartingOnAnchor, @strong } = options
@@ -81,8 +82,10 @@ class Anchor
@setScreenPosition(screenPosition, bufferChange: options.bufferChange, clip: false, assignBufferPosition: false, autoscroll: options.autoscroll)
destroy: ->
return if @destroyed
@buffer.removeAnchor(this)
@editSession?.removeAnchor(this)
@destroyed = true
@trigger 'destroyed'
_.extend(Anchor.prototype, EventEmitter)

View File

@@ -9,18 +9,18 @@ class AtomPackage extends Package
constructor: (@name) ->
super
@keymapsDirPath = fs.join(@path, 'keymaps')
if @requireModule
@module = require(@path)
@module.name = @name
load: ->
try
if @requireModule
@module = require(@path)
@module.name = @name
@loadMetadata()
@loadKeymaps()
@loadStylesheets()
rootView.activatePackage(@name, @module) if @module
catch e
console.error "Failed to load package named '#{@name}'", e.stack
console.warn "Failed to load package named '#{@name}'", e.stack
loadMetadata: ->
if metadataPath = fs.resolveExtension(fs.join(@path, "package"), ['cson', 'json'])

View File

@@ -12,7 +12,23 @@ _.extend atom,
pendingBrowserProcessCallbacks: {}
getAvailablePackages: ->
loadPackages: ->
pack.load() for pack in @getPackages()
getPackages: ->
@getPackageNames().map (name) -> Package.build(name)
loadTextMatePackages: ->
pack.load() for pack in @getTextMatePackages()
getTextMatePackages: ->
@getPackages().filter (pack) -> pack instanceof TextMatePackage
loadPackage: (name) ->
Package.build(name).load()
getPackageNames: ->
disabledPackages = config.get("core.disabledPackages") ? []
allPackageNames = []
for packageDirPath in config.packageDirPaths
packageNames = fs.list(packageDirPath)
@@ -20,17 +36,7 @@ _.extend atom,
.map((packagePath) -> fs.base(packagePath))
allPackageNames.push(packageNames...)
_.unique(allPackageNames)
getAvailableTextMateBundles: ->
@getAvailablePackages().filter (packageName) => TextMatePackage.testName(packageName)
loadPackages: (packageNames=@getAvailablePackages()) ->
disabledPackages = config.get("core.disabledPackages") ? []
for packageName in packageNames
@loadPackage(packageName) unless _.contains(disabledPackages, packageName)
loadPackage: (name) ->
Package.load(name)
.filter (name) -> not _.contains(disabledPackages, name)
loadThemes: ->
themeNames = config.get("core.themes") ? ['IR_Black']
@@ -103,3 +109,16 @@ _.extend atom,
if name is 'reply'
[messageId, callbackIndex] = data.shift()
@pendingBrowserProcessCallbacks[messageId]?[callbackIndex]?(data...)
setWindowState: (keyPath, value) ->
windowState = @getWindowState()
_.setValueForKeyPath(windowState, keyPath, value)
$native.setWindowState(JSON.stringify(windowState))
windowState
getWindowState: (keyPath) ->
windowState = JSON.parse($native.getWindowState())
if keyPath
_.valueForKeyPath(windowState, keyPath)
else
windowState

View File

@@ -47,14 +47,7 @@ class Config
_.valueForKeyPath(@defaultSettings, keyPath)
set: (keyPath, value) ->
keys = keyPath.split('.')
hash = @settings
while keys.length > 1
key = keys.shift()
hash[key] ?= {}
hash = hash[key]
hash[keys.shift()] = value
_.setValueForKeyPath(@settings, keyPath, value)
@update()
value

View File

@@ -175,6 +175,23 @@ class Cursor
getCurrentLineBufferRange: (options) ->
@editSession.bufferRangeForBufferRow(@getBufferRow(), options)
getCurrentParagraphBufferRange: ->
row = @getBufferRow()
return unless /\w/.test(@editSession.lineForBufferRow(row))
startRow = row
while startRow > 0
break unless /\w/.test(@editSession.lineForBufferRow(startRow - 1))
startRow--
endRow = row
lastRow = @editSession.getLastBufferRow()
while endRow < lastRow
break unless /\w/.test(@editSession.lineForBufferRow(endRow + 1))
endRow++
new Range([startRow, 0], [endRow, @editSession.lineLengthForBufferRow(endRow)])
getCurrentWordPrefix: ->
@editSession.getTextInBufferRange([@getBeginningOfCurrentWordBufferPosition(), @getBufferPosition()])

View File

@@ -33,11 +33,10 @@ class EditSession
anchorRanges: null
cursors: null
selections: null
autoIndent: false # TODO: re-enabled auto-indent after fixing the rest of tokenization
softTabs: true
softWrap: false
constructor: ({@project, @buffer, tabLength, @autoIndent, softTabs, @softWrap }) ->
constructor: ({@project, @buffer, tabLength, softTabs, @softWrap }) ->
@softTabs = @buffer.usesSoftTabs() ? softTabs ? true
@languageMode = new LanguageMode(this, @buffer.getExtension())
@displayBuffer = new DisplayBuffer(@buffer, { @languageMode, tabLength })
@@ -93,7 +92,6 @@ class EditSession
getScrollLeft: -> @scrollLeft
setSoftWrapColumn: (@softWrapColumn) -> @displayBuffer.setSoftWrapColumn(@softWrapColumn)
setAutoIndent: (@autoIndent) ->
setSoftTabs: (@softTabs) ->
getSoftWrap: -> @softWrap
@@ -141,6 +139,7 @@ class EditSession
getLastBufferRow: -> @buffer.getLastRow()
bufferRangeForBufferRow: (row, options) -> @buffer.rangeForRow(row, options)
lineForBufferRow: (row) -> @buffer.lineForRow(row)
lineLengthForBufferRow: (row) -> @buffer.lineLengthForRow(row)
scanInRange: (args...) -> @buffer.scanInRange(args...)
backwardsScanInRange: (args...) -> @buffer.backwardsScanInRange(args...)
@@ -159,18 +158,26 @@ class EditSession
getCursorScopes: -> @getCursor().getScopes()
logScreenLines: (start, end) -> @displayBuffer.logLines(start, end)
insertText: (text, options) ->
shouldAutoIndent: ->
config.get("editor.autoIndent")
shouldAutoIndentPastedText: ->
config.get("editor.autoIndentOnPaste")
insertText: (text, options={}) ->
options.autoIndent ?= @shouldAutoIndent()
@mutateSelectedText (selection) -> selection.insertText(text, options)
insertNewline: ->
@insertText('\n', autoIndent: true)
@insertText('\n')
insertNewlineBelow: ->
@moveCursorToEndOfLine()
@insertNewline()
indent: ->
@mutateSelectedText (selection) -> selection.indent()
indent: (options={})->
options.autoIndent ?= @shouldAutoIndent()
@mutateSelectedText (selection) -> selection.indent(options)
backspace: ->
@mutateSelectedText (selection) -> selection.backspace()
@@ -217,9 +224,14 @@ class EditSession
selection.copy(maintainPasteboard)
maintainPasteboard = true
pasteText: ->
pasteText: (options={}) ->
options.normalizeIndent ?= true
options.autoIndent ?= @shouldAutoIndentPastedText()
[text, metadata] = pasteboard.read()
@insertText(text, _.extend(metadata ? {}, normalizeIndent: true))
_.extend(options, metadata) if metadata
@insertText(text, options)
undo: ->
@buffer.undo(this)
@@ -478,6 +490,9 @@ class EditSession
getTextInBufferRange: (range) ->
@buffer.getTextInRange(range)
getCurrentParagraphBufferRange: ->
@getCursor().getCurrentParagraphBufferRange()
moveCursorUp: (lineCount) ->
@moveCursors (cursor) -> cursor.moveUp(lineCount)

View File

@@ -17,6 +17,8 @@ class Editor extends View
fontSize: 20
showInvisibles: false
autosave: false
autoIndent: true
autoIndentOnPaste: false
@content: (params) ->
@div class: @classes(params), tabindex: -1, =>
@@ -80,7 +82,6 @@ class Editor extends View
buffer: new Buffer()
softWrap: false
tabLength: 2
autoIndent: false
softTabs: true
@editSessions.push editSession
@@ -176,8 +177,8 @@ class Editor extends View
'editor:toggle-line-comments': @toggleLineCommentsInSelection
'editor:log-cursor-scope': @logCursorScope
'editor:checkout-head-revision': @checkoutHead
'editor:close-other-editors': @destroyInactiveEditSessions
'editor:close-all-editors': @destroyAllEditSessions
'editor:close-other-edit-sessions': @destroyInactiveEditSessions
'editor:close-all-edit-sessions': @destroyAllEditSessions
'editor:select-grammar': @selectGrammar
documentation = {}
@@ -205,6 +206,7 @@ class Editor extends View
getCursorScreenRow: -> @activeEditSession.getCursorScreenRow()
setCursorBufferPosition: (position, options) -> @activeEditSession.setCursorBufferPosition(position, options)
getCursorBufferPosition: -> @activeEditSession.getCursorBufferPosition()
getCurrentParagraphBufferRange: -> @activeEditSession.getCurrentParagraphBufferRange()
getSelection: (index) -> @activeEditSession.getSelection(index)
getSelections: -> @activeEditSession.getSelections()
@@ -244,7 +246,7 @@ class Editor extends View
insertText: (text, options) -> @activeEditSession.insertText(text, options)
insertNewline: -> @activeEditSession.insertNewline()
insertNewlineBelow: -> @activeEditSession.insertNewlineBelow()
indent: -> @activeEditSession.indent()
indent: (options) -> @activeEditSession.indent(options)
indentSelectedRows: -> @activeEditSession.indentSelectedRows()
outdentSelectedRows: -> @activeEditSession.outdentSelectedRows()
cutSelection: -> @activeEditSession.cutSelectedText()
@@ -381,7 +383,7 @@ class Editor extends View
@selectOnMousemoveUntilMouseup()
@on "textInput", (e) =>
@insertText(e.originalEvent.data, autoIndent: true)
@insertText(e.originalEvent.data)
false
@scrollView.on 'mousewheel', (e) =>
@@ -1126,4 +1128,22 @@ class Editor extends View
if grammarChanged
@clearRenderedLines()
@updateDisplay()
@trigger 'editor:grammar-changed'
grammarChanged
bindToKeyedEvent: (key, event, callback) ->
binding = {}
binding[key] = event
window.keymap.bindKeys '.editor', binding
@on event, =>
callback(this, event)
replaceSelectedText: (replaceFn) ->
selection = @getSelection()
return false if selection.isEmpty()
text = replaceFn(@getTextInRange(selection.getBufferRange()))
return false if text is null or text is undefined
@insertText(text, select: true)
true

View File

@@ -1,4 +1,6 @@
$ = require 'jquery'
_ = require 'underscore'
Subscriber = require 'subscriber'
module.exports =
class Git
@@ -23,26 +25,34 @@ class Git
constructor: (path) ->
@repo = new GitRepository(path)
@subscribe $(window), 'focus', => @refreshIndex()
getRepo: ->
unless @repo?
throw new Error("No Git repository found searching path: #{path}")
$(window).on 'focus', => @refreshIndex()
throw new Error("Repository has been destroyed")
@repo
refreshIndex: -> @repo.refreshIndex()
refreshIndex: -> @getRepo().refreshIndex()
getPath: -> @repo.getPath()
getPath: -> @getRepo().getPath()
destroy: ->
@getRepo().destroy()
@repo = null
@unsubscribe()
getWorkingDirectory: ->
repoPath = @getPath()
repoPath?.substring(0, repoPath.length - 6)
getHead: ->
@repo.getHead() or ''
@getRepo().getHead() or ''
getPathStatus: (path) ->
pathStatus = @repo.getStatus(@relativize(path))
pathStatus = @getRepo().getStatus(@relativize(path))
isPathIgnored: (path) ->
@repo.isIgnored(@relativize(path))
@getRepo().isIgnored(@relativize(path))
isStatusModified: (status) ->
modifiedFlags = @statusFlags.working_dir_modified |
@@ -80,10 +90,12 @@ class Git
return head
checkoutHead: (path) ->
@repo.checkoutHead(@relativize(path))
@getRepo().checkoutHead(@relativize(path))
getDiffStats: (path) ->
@repo.getDiffStats(@relativize(path)) or added: 0, deleted: 0
@getRepo().getDiffStats(@relativize(path)) or added: 0, deleted: 0
isSubmodule: (path) ->
@repo.isSubmodule(@relativize(path))
@getRepo().isSubmodule(@relativize(path))
_.extend Git.prototype, Subscriber

View File

@@ -33,3 +33,4 @@
'.tool-panel':
'meta-escape': 'tool-panel:unfocus'
'escape': 'core:close'
'meta-w': 'noop'

View File

@@ -32,6 +32,6 @@
'meta-alt-p': 'editor:log-cursor-scope'
'meta-u': 'editor:upper-case'
'meta-U': 'editor:lower-case'
'alt-meta-w': 'editor:close-other-editors'
'meta-P': 'editor:close-all-editors'
'alt-meta-w': 'editor:close-other-edit-sessions'
'meta-P': 'editor:close-all-edit-sessions'
'meta-l': 'editor:select-grammar'

View File

@@ -2,15 +2,13 @@ fs = require 'fs'
module.exports =
class Package
@load: (name) ->
@build: (name) ->
AtomPackage = require 'atom-package'
TextMatePackage = require 'text-mate-package'
if TextMatePackage.testName(name)
new TextMatePackage(name).load()
new TextMatePackage(name)
else
new AtomPackage(name).load()
new AtomPackage(name)
name: null
path: null
@@ -26,10 +24,3 @@ class Package
else
@requireModule = true
@path = fs.directory(@path)
load: ->
for grammar in @getGrammars()
syntax.addGrammar(grammar)
for { selector, properties } in @getScopedProperties()
syntax.addProperties(selector, properties)

View File

@@ -16,7 +16,6 @@ class Project
new Project(state.path, state.grammarOverridesByPath)
tabLength: 2
autoIndent: true
softTabs: true
softWrap: false
rootDirectory: null
@@ -34,6 +33,8 @@ class Project
grammarOverridesByPath: @grammarOverridesByPath
destroy: ->
@repo?.destroy()
@repo = null
editSession.destroy() for editSession in @getEditSessions()
addGrammarOverrideForPath: (path, grammar) ->
@@ -91,9 +92,6 @@ class Project
relativize: (fullPath) ->
fullPath.replace(@getPath(), "").replace(/^\//, '')
getAutoIndent: -> @autoIndent
setAutoIndent: (@autoIndent) ->
getSoftTabs: -> @softTabs
setSoftTabs: (@softTabs) ->
@@ -114,7 +112,6 @@ class Project
defaultEditSessionOptions: ->
tabLength: @tabLength
autoIndent: @getAutoIndent()
softTabs: @getSoftTabs()
softWrap: @getSoftWrap()

View File

@@ -23,33 +23,39 @@ class RootView extends View
@div id: 'vertical', outlet: 'vertical', =>
@div id: 'panes', outlet: 'panes'
@deserialize: ({ projectState, panesViewState, packageStates }) ->
project = Project.deserialize(projectState) if projectState
rootView = new RootView(project, packageStates: packageStates, suppressOpen: true)
@deserialize: ({ projectState, panesViewState, packageStates, projectPath }) ->
if projectState
projectOrPathToOpen = Project.deserialize(projectState)
else
projectOrPathToOpen = projectPath # This will migrate people over to the new project serialization scheme. It should be removed eventually.
rootView = new RootView(projectOrPathToOpen , packageStates: packageStates, suppressOpen: true)
rootView.setRootPane(rootView.deserializeView(panesViewState)) if panesViewState
rootView
packageModules: null
packageStates: null
title: null
pathToOpenIsFile: false
initialize: (projectOrPathToOpen, { @packageStates, suppressOpen } = {}) ->
window.rootView = this
@packageStates ?= {}
@packageModules = {}
@handleEvents()
if not projectOrPathToOpen or _.isString(projectOrPathToOpen)
pathToOpen = projectOrPathToOpen
@project = new Project(projectOrPathToOpen)
else
@project = projectOrPathToOpen
pathToOpen = @project?.getPath()
@pathToOpenIsFile = pathToOpen and fs.isFile(pathToOpen)
config.load()
@handleEvents()
if pathToOpen
@open(pathToOpen) if fs.isFile(pathToOpen) and not suppressOpen
@open(pathToOpen) if @pathToOpenIsFile and not suppressOpen
else
@open()
@@ -74,12 +80,12 @@ class RootView extends View
handleEvents: ->
@command 'toggle-dev-tools', => atom.toggleDevTools()
@on 'focus', (e) => @handleFocus(e)
$(window).on 'focus', (e) =>
@subscribe $(window), 'focus', (e) =>
@handleFocus(e) if document.activeElement is document.body
@on 'root-view:active-path-changed', (e, path) =>
@project.setPath(path) unless @project.getRootDirectory()
if path
@project.setPath(path) unless @project.getRootDirectory()
@setTitle(fs.base(path))
else
@setTitle("untitled")
@@ -98,6 +104,10 @@ class RootView extends View
config.set("editor.showInvisibles", !config.get("editor.showInvisibles"))
@command 'window:toggle-ignored-files', =>
config.set("core.hideGitIgnoredFiles", not config.core.hideGitIgnoredFiles)
@command 'window:toggle-auto-indent', =>
config.set("editor.autoIndent", !config.get("editor.autoIndent"))
@command 'window:toggle-auto-indent-on-paste', =>
config.set("editor.autoIndentOnPaste", !config.get("editor.autoIndentOnPaste"))
afterAttach: (onDom) ->
@focus() if onDom
@@ -254,3 +264,17 @@ class RootView extends View
saveAll: ->
editor.save() for editor in @getEditors()
eachEditor: (callback) ->
for editor in @getEditors()
callback(editor)
@on 'editor:attached', (e, editor) ->
callback(editor)
eachBuffer: (callback) ->
for buffer in @project.getBuffers()
callback(buffer)
@project.on 'buffer-created', (buffer) ->
callback(buffer)

View File

@@ -174,15 +174,18 @@ class Selection
text = @normalizeIndent(text, options) if options.normalizeIndent
@clear()
newBufferRange = @editSession.buffer.change(oldBufferRange, text)
@cursor.setBufferPosition(newBufferRange.end, skipAtomicTokens: true) if wasReversed
if options.select
@setBufferRange(newBufferRange, reverse: wasReversed)
else
@cursor.setBufferPosition(newBufferRange.end, skipAtomicTokens: true) if wasReversed
if @editSession.autoIndent and options.autoIndent
if options.autoIndent
if text == '\n'
@editSession.autoIndentBufferRow(newBufferRange.end.row)
else
@editSession.autoDecreaseIndentForRow(newBufferRange.start.row)
indent: ->
indent: ({ autoIndent }={})->
{ row, column } = @cursor.getBufferPosition()
if @isEmpty()
@@ -190,7 +193,7 @@ class Selection
desiredIndent = @editSession.suggestedIndentForBufferRow(row)
delta = desiredIndent - @cursor.getIndentLevel()
if @editSession.autoIndent and delta > 0
if autoIndent and delta > 0
@insertText(@editSession.buildIndentString(delta))
else
@insertText(@editSession.getTabText())
@@ -218,7 +221,7 @@ class Selection
if insideExistingLine
desiredBasis = @editSession.indentationForBufferRow(currentBufferRow)
else if @editSession.autoIndent
else if options.autoIndent
desiredBasis = @editSession.suggestedIndentForBufferRow(currentBufferRow)
else
desiredBasis = @cursor.getIndentLevel()

View File

@@ -23,6 +23,16 @@ class TextMatePackage extends Package
@preferencesPath = fs.join(@path, "Preferences")
@syntaxesPath = fs.join(@path, "Syntaxes")
load: ->
try
for grammar in @getGrammars()
syntax.addGrammar(grammar)
for { selector, properties } in @getScopedProperties()
syntax.addProperties(selector, properties)
catch e
console.warn "Failed to load package named '#{@name}'", e.stack
getGrammars: ->
return @grammars if @grammars
@grammars = []

View File

@@ -47,8 +47,10 @@ windowAdditions =
false
shutdown: ->
@rootView?.deactivate()
@rootView = null
if @rootView
atom.setWindowState('pathToOpen', @rootView.project.getPath())
@rootView.deactivate()
@rootView = null
$(window).unbind('focus')
$(window).unbind('blur')
$(window).off('before')

View File

@@ -0,0 +1 @@
module.exports = require './lib/autoflow'

View File

@@ -0,0 +1,33 @@
module.exports =
activate: (rootView) ->
rootView.command 'autoflow:reflow-paragraph', '.editor', (e) =>
@reflowParagraph(e.currentTargetView())
reflowParagraph: (editor) ->
if range = editor.getCurrentParagraphBufferRange()
editor.getBuffer().change(range, @reflow(editor.getTextInRange(range)))
reflow: (text) ->
wrapColumn = config.get('editor.preferredLineLength') ? 80
lines = []
currentLine = []
currentLineLength = 0
for segment in @segmentText(text.replace(/\n/g, ' '))
if /\w/.test(segment) and
(currentLineLength + segment.length > wrapColumn) and
(currentLineLength > 0 or segment.length < wrapColumn)
lines.push(currentLine.join(''))
currentLine = []
currentLineLength = 0
currentLine.push(segment)
currentLineLength += segment.length
lines.push(currentLine.join(''))
lines.join('\n').replace(/\s+\n/g, '\n')
segmentText: (text) ->
segments = []
re = /[\s]+|[^\s]+/g
segments.push(match[0]) while match = re.exec(text)
segments

View File

@@ -0,0 +1,53 @@
RootView = require 'root-view'
describe "Autoflow package", ->
editor = null
beforeEach ->
rootView = new RootView
atom.loadPackage 'autoflow'
editor = rootView.getActiveEditor()
config.set('editor.preferredLineLength', 30)
describe "autoflow:reflow-paragraph", ->
it "rearranges line breaks in the current paragraph to ensure lines are shorter than config.editor.preferredLineLength", ->
editor.setText """
This is a preceding paragraph, which shouldn't be modified by a reflow of the following paragraph.
The quick brown fox jumps over the lazy
dog. The preceding sentence contains every letter
in the entire English alphabet, which has absolutely no relevance
to this test.
This is a following paragraph, which shouldn't be modified by a reflow of the preciding paragraph.
"""
editor.setCursorBufferPosition([3, 5])
editor.trigger 'autoflow:reflow-paragraph'
expect(editor.getText()).toBe """
This is a preceding paragraph, which shouldn't be modified by a reflow of the following paragraph.
The quick brown fox jumps over
the lazy dog. The preceding
sentence contains every letter
in the entire English
alphabet, which has absolutely
no relevance to this test.
This is a following paragraph, which shouldn't be modified by a reflow of the preciding paragraph.
"""
it "allows for single words that exceed the preferred wrap column length", ->
editor.setText("this-is-a-super-long-word-that-shouldn't-break-autoflow and these are some smaller words")
editor.setCursorBufferPosition([0, 4])
editor.trigger 'autoflow:reflow-paragraph'
expect(editor.getText()).toBe """
this-is-a-super-long-word-that-shouldn't-break-autoflow
and these are some smaller
words
"""

View File

@@ -9,7 +9,6 @@ describe "CommandLogger", ->
atom.loadPackage 'command-logger'
editor = rootView.getActiveEditor()
commandLogger = CommandLogger.instance
rootView.attachToDom()
afterEach ->
rootView.deactivate()

View File

@@ -24,6 +24,10 @@ class CommandLogger extends ScrollView
'core:cancel'
'core:confirm'
'core:delete'
'core:move-down'
'core:move-left'
'core:move-right'
'core:move-up'
'editor:newline'
'tree-view:directory-modified'
]
@@ -82,6 +86,28 @@ class CommandLogger extends ScrollView
@div style: "height:#{node.dy - 1}px;width:#{node.dx - 1}px", =>
@span node.name
updateCategoryHeader: (node) ->
@categoryHeader.text("#{node.name} Commands")
reduceRunCount = (previous, current) ->
if current.size?
previous + current.size
else if current.children?.length > 0
current.children.reduce(reduceRunCount, previous)
else
previous
runCount = node.children.reduce(reduceRunCount, 0)
reduceCommandCount = (previous, current) ->
if current.children?.length > 0
current.children.reduce(reduceCommandCount, previous)
else
previous + 1
commandCount = node.children.reduce(reduceCommandCount, 0)
@categorySummary.text("#{_.pluralize(commandCount, 'command')}, #{_.pluralize(runCount, 'invocation')}")
updateTreeMapSize: ->
@treeMap.width(@width() - 20)
@treeMap.height(@height() - @categoryHeader.outerHeight() - @categorySummary.outerHeight() - 20)
addTreeMap: ->
root =
name: 'All'
@@ -90,33 +116,17 @@ class CommandLogger extends ScrollView
@treeMap.empty()
@updateCategoryHeader(root)
@updateTreeMapSize()
w = @treeMap.width()
h = @treeMap.height()
x = d3.scale.linear().range([0, w])
y = d3.scale.linear().range([0, h])
color = d3.scale.category20()
updateCategoryHeader = (node) =>
@categoryHeader.text("#{node.name} Commands")
reduceRunCount = (previous, current) ->
if current.size?
previous + current.size
else if current.children?.length > 0
current.children.reduce(reduceRunCount, previous)
else
previous
runCount = node.children.reduce(reduceRunCount, 0)
reduceCommandCount = (previous, current) ->
if current.children?.length > 0
current.children.reduce(reduceCommandCount, previous)
else
previous + 1
commandCount = node.children.reduce(reduceCommandCount, 0)
@categorySummary.text("#{_.pluralize(commandCount, 'command')}, #{_.pluralize(runCount, 'invocation')}")
updateCategoryHeader(root)
zoom = (d) ->
updateCategoryHeader(d)
zoom = (d) =>
@updateCategoryHeader(d)
kx = w / d.dx
ky = h / d.dy
x.domain([d.x, d.x + d.dx])

View File

@@ -8,7 +8,8 @@
color: #eee;
overflow: auto;
z-index: 99;
padding: 20px;
padding-top: 10px;
padding-bottom: 10px;
}
.command-logger .category-header {
@@ -25,9 +26,6 @@
.command-logger .tree-map {
margin: auto;
position: relative;
width: 960px;
height: 700px;
background-color: #efefef;
border: 1px solid #999;
}

View File

@@ -127,7 +127,7 @@ describe "CommandPanel", ->
describe "when the preview list is/was previously visible", ->
beforeEach ->
rootView.trigger 'command-panel:toggle'
waitsForPromise -> commandPanel.execute('X x/a+/')
waitsForPromise -> commandPanel.execute('X x/quicksort/')
describe "when the command panel is visible", ->
beforeEach ->
@@ -290,22 +290,22 @@ describe "CommandPanel", ->
rootView.attachToDom()
editor.remove()
rootView.trigger 'command-panel:toggle'
waitsForPromise -> commandPanel.execute('X x/a+/')
waitsForPromise -> commandPanel.execute('X x/quicksort/')
it "displays and focuses the operation preview list", ->
expect(commandPanel).toBeVisible()
expect(commandPanel.previewList).toBeVisible()
expect(commandPanel.previewList).toMatchSelector ':focus'
previewItem = commandPanel.previewList.find("li:contains(dir/a):first")
expect(previewItem.text()).toBe "dir/a"
expect(previewItem.next().find('.preview').text()).toBe "aaa bbb"
expect(previewItem.next().find('.preview > .match').text()).toBe "aaa"
previewItem = commandPanel.previewList.find("li:contains(sample.js):first")
expect(previewItem.text()).toBe "sample.js"
expect(previewItem.next().find('.preview').text()).toBe "var quicksort = function () {"
expect(previewItem.next().find('.preview > .match').text()).toBe "quicksort"
rootView.trigger 'command-panel:toggle-preview' # ensure we can close panel without problems
expect(commandPanel).toBeHidden()
it "destroys previously previewed operations if there are any", ->
waitsForPromise -> commandPanel.execute('X x/b+/')
waitsForPromise -> commandPanel.execute('X x/pivot/')
# there shouldn't be any dangling operations after this
describe "if the command is malformed", ->
@@ -375,7 +375,7 @@ describe "CommandPanel", ->
beforeEach ->
previewList = commandPanel.previewList
rootView.trigger 'command-panel:toggle'
waitsForPromise -> commandPanel.execute('X x/a/')
waitsForPromise -> commandPanel.execute('X x/sort/')
describe "when move-down and move-up are triggered on the preview list", ->
it "selects the next/previous operation (if there is one), and scrolls the list if needed", ->

View File

@@ -19,7 +19,7 @@ describe 'FuzzyFinder', ->
describe "file-finder behavior", ->
describe "toggling", ->
describe "when the root view's project has a path", ->
it "shows the FuzzyFinder or hides it nad returns focus to the active editor if it already showing", ->
it "shows the FuzzyFinder or hides it and returns focus to the active editor if it already showing", ->
rootView.attachToDom()
expect(rootView.find('.fuzzy-finder')).not.toExist()
rootView.find('.editor').trigger 'editor:split-right'
@@ -86,6 +86,18 @@ describe 'FuzzyFinder', ->
expect(editor2.getPath()).toBe expectedPath
expect(editor2.isFocused).toBeTruthy()
describe "when the selected path isn't a file that exists", ->
it "leaves the the tree view open, doesn't open the path in the editor, and displays an error", ->
rootView.attachToDom()
path = rootView.getActiveEditor().getPath()
rootView.trigger 'fuzzy-finder:toggle-file-finder'
finder.confirmed('dir/this/is/not/a/file.txt')
expect(finder.hasParent()).toBeTruthy()
expect(rootView.getActiveEditor().getPath()).toBe path
expect(finder.find('.error').text().length).toBeGreaterThan 0
advanceClock(2000)
expect(finder.find('.error').text().length).toBe 0
describe "buffer-finder behavior", ->
describe "toggling", ->
describe "when the active editor contains edit sessions for buffers with paths", ->
@@ -243,36 +255,20 @@ describe 'FuzzyFinder', ->
$(window).trigger 'focus'
rootView.trigger 'fuzzy-finder:toggle-file-finder'
rootView.trigger 'fuzzy-finder:toggle-file-finder'
waitsFor ->
finder.list.children('li').length > 0
runs ->
expect(rootView.project.getFilePaths).toHaveBeenCalled()
describe "path ignoring", ->
it "ignores paths that match entries in config.fuzzy-finder.ignoredNames", ->
it "ignores paths that match entries in config.fuzzyFinder.ignoredNames", ->
spyOn(rootView.project, "getFilePaths").andCallThrough()
config.set("fuzzy-finder.ignoredNames", ["tree-view"])
config.set("fuzzyFinder.ignoredNames", ["tree-view.js"])
rootView.trigger 'fuzzy-finder:toggle-file-finder'
finder.maxItems = Infinity
finder.miniEditor.setText("file1")
waitsFor ->
finder.list.children('li').length > 0
runs ->
expect(rootView.project.getFilePaths).toHaveBeenCalled()
rootView.project.getFilePaths.reset()
$(window).trigger 'focus'
rootView.trigger 'fuzzy-finder:toggle-file-finder'
rootView.trigger 'fuzzy-finder:toggle-file-finder'
waitsFor ->
finder.list.children('li').length > 0
runs ->
expect(rootView.project.getFilePaths).toHaveBeenCalled()
expect(finder.list.find("li:contains(tree-view.js)")).not.toExist()
describe "opening a path into a split", ->
beforeEach ->

View File

@@ -17,12 +17,13 @@ class FuzzyFinder extends SelectList
allowActiveEditorChange: null
maxItems: 10
projectPaths: null
reloadProjectPaths: true
initialize: (@rootView) ->
super
@subscribe $(window), 'focus', => @projectPaths = null
@observeConfig 'fuzzy-finder.ignoredNames', (ignoredNames) =>
@projectPaths = null
@subscribe $(window), 'focus', => @reloadProjectPaths = true
@observeConfig 'fuzzy-finder.ignoredNames', => @reloadProjectPaths = true
@miniEditor.command 'editor:split-left', =>
@splitOpenPath (editor, session) -> editor.splitLeft(session)
@@ -64,8 +65,12 @@ class FuzzyFinder extends SelectList
confirmed : (path) ->
return unless path.length
@cancel()
@openPath(path)
if fs.isFile(rootView.project.resolve(path))
@cancel()
@openPath(path)
else
@setError('Selected path does not exist')
setTimeout((=> @setError()), 2000)
cancelled: ->
@miniEditor.setText('')
@@ -93,6 +98,8 @@ class FuzzyFinder extends SelectList
@setArray(@projectPaths)
else
@setLoading("Indexing...")
if @reloadProjectPaths
@rootView.project.getFilePaths().done (paths) =>
ignoredNames = config.get("fuzzyFinder.ignoredNames") or []
ignoredNames = ignoredNames.concat(config.get("core.ignoredNames") or [])
@@ -103,6 +110,7 @@ class FuzzyFinder extends SelectList
return false if _.contains(ignoredNames, segment)
return true
@reloadProjectPaths = false
@setArray(@projectPaths)
populateOpenBufferPaths: ->

View File

@@ -9,7 +9,6 @@ describe "MarkdownPreview", ->
rootView = new RootView(require.resolve('fixtures/markdown'))
atom.loadPackage("markdown-preview")
markdownPreview = MarkdownPreview.instance
rootView.attachToDom()
afterEach ->
rootView.deactivate()

View File

@@ -1,39 +1,15 @@
{
var Snippet = require('snippets/src/snippet');
var Point = require('point');
}
bodyContent = content:(tabStop / bodyContentText)* { return content; }
bodyContentText = text:bodyContentChar+ { return text.join(''); }
bodyContentChar = !tabStop char:. { return char; }
snippets = snippets:snippet+ ws? {
var snippetsByPrefix = {};
snippets.forEach(function(snippet) {
snippetsByPrefix[snippet.prefix] = snippet
});
return snippetsByPrefix;
}
placeholderContent = content:(tabStop / placeholderContentText)* { return content; }
placeholderContentText = text:placeholderContentChar+ { return text.join(''); }
placeholderContentChar = !tabStop char:[^}] { return char; }
snippet = ws? start ws prefix:prefix ws description:string bodyPosition:beforeBody body:body end {
return new Snippet({ bodyPosition: bodyPosition, prefix: prefix, description: description, body: body });
}
start = 'snippet'
prefix = prefix:[A-Za-z0-9_]+ { return prefix.join(''); }
string = ['] body:[^']* ['] { return body.join(''); }
/ ["] body:[^"]* ["] { return body.join(''); }
beforeBody = [ ]* '\n' { return new Point(line, 0); } // return start position of body: body begins on next line, so don't subtract 1 from line
body = bodyLine+
bodyLine = content:(tabStop / bodyText)* '\n' { return content; }
bodyText = text:bodyChar+ { return text.join(''); }
bodyChar = !(end / tabStop) char:[^\n] { return char; }
tabStop = simpleTabStop / tabStopWithPlaceholder
simpleTabStop = '$' index:[0-9]+ {
return { index: parseInt(index), placeholderText: '' };
return { index: parseInt(index), content: [] };
}
tabStopWithPlaceholder = '${' index:[0-9]+ ':' placeholderText:[^}]* '}' {
return { index: parseInt(index), placeholderText: placeholderText.join('') };
tabStopWithPlaceholder = '${' index:[0-9]+ ':' content:placeholderContent '}' {
return { index: parseInt(index), content: content };
}
end = 'endsnippet'
ws = ([ \n] / comment)+
comment = '#' [^\n]*

View File

@@ -1,14 +1,19 @@
Snippets = require 'snippets'
Snippet = require 'snippets/src/snippet'
RootView = require 'root-view'
Buffer = require 'buffer'
Editor = require 'editor'
_ = require 'underscore'
fs = require 'fs'
AtomPackage = require 'atom-package'
TextMatePackage = require 'text-mate-package'
describe "Snippets extension", ->
[buffer, editor] = []
beforeEach ->
rootView = new RootView(require.resolve('fixtures/sample.js'))
spyOn(AtomPackage.prototype, 'loadSnippets')
spyOn(TextMatePackage.prototype, 'loadSnippets')
atom.loadPackage("snippets")
editor = rootView.getActiveEditor()
buffer = editor.getBuffer()
@@ -17,36 +22,50 @@ describe "Snippets extension", ->
afterEach ->
rootView.remove()
delete window.snippets
describe "when 'tab' is triggered on the editor", ->
beforeEach ->
Snippets.evalSnippets 'js', """
snippet t1 "Snippet without tab stops"
this is a test
endsnippet
snippets.add
".source.js":
"without tab stops":
prefix: "t1"
body: "this is a test"
snippet t2 "With tab stops"
go here next:($2) and finally go here:($3)
go here first:($1)
"tab stops":
prefix: "t2"
body: """
go here next:($2) and finally go here:($0)
go here first:($1)
endsnippet
"""
snippet t3 "With indented second line"
line 1
line 2$1
"indented second line":
prefix: "t3"
body: """
line 1
line 2$1
endsnippet
"""
snippet t4 "With tab stop placeholders"
go here ${1:first} and then here ${2:second}
"tab stop placeholders":
prefix: "t4"
body: """
go here ${1:first
think a while}, and then here ${2:second}
endsnippet
"""
snippet t5 "Caused problems with undo"
first line$1
${2:placeholder ending second line}
endsnippet
"""
"nested tab stops":
prefix: "t5"
body: '${1:"${2:key}"}: ${3:value}'
"caused problems with undo":
prefix: "t6"
body: """
first line$1
${2:placeholder ending second line}
"""
describe "when the letters preceding the cursor trigger a snippet", ->
describe "when the snippet contains no tab stops", ->
@@ -98,8 +117,22 @@ describe "Snippets extension", ->
it "auto-fills the placeholder text and highlights it when navigating to that tab stop", ->
editor.insertText 't4'
editor.trigger 'snippets:expand'
expect(buffer.lineForRow(0)).toBe 'go here first and then here second'
expect(editor.getSelectedBufferRange()).toEqual [[0, 8], [0, 13]]
expect(buffer.lineForRow(0)).toBe 'go here first'
expect(buffer.lineForRow(1)).toBe 'think a while, and then here second'
expect(editor.getSelectedBufferRange()).toEqual [[0, 8], [1, 13]]
editor.trigger keydownEvent('tab', target: editor[0])
expect(editor.getSelectedBufferRange()).toEqual [[1, 29], [1, 35]]
describe "when tab stops are nested", ->
it "destroys the inner tab stop if the outer tab stop is modified", ->
buffer.setText('')
editor.insertText 't5'
editor.trigger 'snippets:expand'
expect(buffer.lineForRow(0)).toBe '"key": value'
expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 5]]
editor.insertText("foo")
editor.trigger keydownEvent('tab', target: editor[0])
expect(editor.getSelectedBufferRange()).toEqual [[0, 5], [0, 10]]
describe "when the cursor is moved beyond the bounds of a tab stop", ->
it "terminates the snippet", ->
@@ -152,92 +185,68 @@ describe "Snippets extension", ->
describe "when a previous snippet expansion has just been undone", ->
it "expands the snippet based on the current prefix rather than jumping to the old snippet's tab stop", ->
editor.insertText 't5\n'
editor.insertText 't6\n'
editor.setCursorBufferPosition [0, 2]
editor.trigger keydownEvent('tab', target: editor[0])
expect(buffer.lineForRow(0)).toBe "first line"
editor.undo()
expect(buffer.lineForRow(0)).toBe "t5"
expect(buffer.lineForRow(0)).toBe "t6"
editor.trigger keydownEvent('tab', target: editor[0])
expect(buffer.lineForRow(0)).toBe "first line"
describe "when a snippet expansion is undone and redone", ->
it "recreates the snippet's tab stops", ->
editor.insertText ' t5\n'
editor.insertText ' t6\n'
editor.setCursorBufferPosition [0, 6]
editor.trigger keydownEvent('tab', target: editor[0])
expect(buffer.lineForRow(0)).toBe " first line"
editor.undo()
editor.redo()
expect(editor.getCursorBufferPosition()).toEqual [0, 14]
editor.trigger keydownEvent('tab', target: editor[0])
expect(editor.getSelectedBufferRange()).toEqual [[1, 6], [1, 36]]
it "restores tabs stops in active edit session even when the initial expansion was in a different edit session", ->
anotherEditor = editor.splitRight()
describe "snippet loading", ->
it "loads snippets from all atom packages with a snippets directory", ->
jasmine.unspy(AtomPackage.prototype, 'loadSnippets')
snippets.loadAll()
editor.insertText ' t5\n'
editor.setCursorBufferPosition [0, 6]
editor.trigger keydownEvent('tab', target: editor[0])
expect(buffer.lineForRow(0)).toBe " first line"
editor.undo()
expect(syntax.getProperty(['.test'], 'snippets.test')?.constructor).toBe Snippet
anotherEditor.redo()
expect(anotherEditor.getCursorBufferPosition()).toEqual [0, 14]
anotherEditor.trigger keydownEvent('tab', target: anotherEditor[0])
expect(anotherEditor.getSelectedBufferRange()).toEqual [[1, 6], [1, 36]]
it "loads snippets from all TextMate packages with snippets", ->
jasmine.unspy(TextMatePackage.prototype, 'loadSnippets')
snippets.loadAll()
describe ".loadSnippetsFile(path)", ->
it "loads the snippets in the given file", ->
spyOn(fs, 'read').andReturn """
snippet t1 "Test snippet 1"
this is a test 1
endsnippet
snippet = syntax.getProperty(['.source.js'], 'snippets.fun')
expect(snippet.constructor).toBe Snippet
expect(snippet.prefix).toBe 'fun'
expect(snippet.name).toBe 'Function'
expect(snippet.body).toBe """
function function_name (argument) {
\t// body...
}
"""
Snippets.loadSnippetsFile('/tmp/foo/js.snippets')
expect(fs.read).toHaveBeenCalledWith('/tmp/foo/js.snippets')
editor.insertText("t1")
editor.trigger 'snippets:expand'
expect(buffer.lineForRow(0)).toBe "this is a test 1var quicksort = function () {"
describe "Snippets parser", ->
it "can parse multiple snippets", ->
snippets = Snippets.snippetsParser.parse """
snippet t1 "Test snippet 1"
this is a test 1
endsnippet
snippet t2 "Test snippet 2"
this is a test 2
endsnippet
"""
expect(_.keys(snippets).length).toBe 2
snippet = snippets['t1']
expect(snippet.prefix).toBe 't1'
expect(snippet.description).toBe "Test snippet 1"
expect(snippet.body).toBe "this is a test 1"
snippet = snippets['t2']
expect(snippet.prefix).toBe 't2'
expect(snippet.description).toBe "Test snippet 2"
expect(snippet.body).toBe "this is a test 2"
it "can parse snippets with tabstops", ->
snippets = Snippets.snippetsParser.parse """
# this line intentially left blank.
snippet t1 "Snippet with tab stops"
go here next:($2) and finally go here:($3)
go here first:($1)
endsnippet
it "breaks a snippet body into lines, with each line containing tab stops at the appropriate position", ->
bodyTree = Snippets.parser.parse """
the quick brown $1fox ${2:jumped ${3:over}
}the ${4:lazy} dog
"""
snippet = snippets['t1']
expect(snippet.body).toBe """
go here next:() and finally go here:()
go here first:()
"""
expect(snippet.tabStops).toEqual [[[1, 15], [1, 15]], [[0, 14], [0, 14]], [[0, 37], [0, 37]]]
expect(bodyTree).toEqual [
"the quick brown ",
{ index: 1, content: [] },
"fox ",
{
index: 2,
content: [
"jumped ",
{ index: 3, content: ["over"]},
"\n"
],
}
"the "
{ index: 4, content: ["lazy"] },
" dog"
]

View File

@@ -0,0 +1,27 @@
AtomPackage = require 'atom-package'
TextMatePackage = require 'text-mate-package'
fs = require 'fs'
AtomPackage.prototype.loadSnippets = ->
snippetsDirPath = fs.join(@path, 'snippets')
if fs.exists(snippetsDirPath)
for snippetsPath in fs.list(snippetsDirPath)
snippets.load(snippetsPath)
TextMatePackage.prototype.loadSnippets = ->
snippetsDirPath = fs.join(@path, 'Snippets')
if fs.exists(snippetsDirPath)
tmSnippets = fs.list(snippetsDirPath).map (snippetPath) -> fs.readPlist(snippetPath)
snippets.add(@translateSnippets(tmSnippets))
TextMatePackage.prototype.translateSnippets = (tmSnippets) ->
atomSnippets = {}
for { scope, name, content, tabTrigger } in tmSnippets
if scope
scope = TextMatePackage.cssSelectorFromScopeSelector(scope)
else
scope = '*'
snippetsForScope = (atomSnippets[scope] ?= {})
snippetsForScope[name] = { prefix: tabTrigger, body: content }
atomSnippets

View File

@@ -1,33 +1,44 @@
Subscriber = require 'subscriber'
_ = require 'underscore'
module.exports =
class SnippetExpansion
snippet: null
tabStopAnchorRanges: null
settingTabStop: false
constructor: (snippet, @editSession) ->
constructor: (@snippet, @editSession) ->
@editSession.selectToBeginningOfWord()
startPosition = @editSession.getCursorBufferPosition()
@editSession.transact =>
@editSession.insertText(snippet.body, autoIndent: false)
if snippet.tabStops.length
@placeTabStopAnchorRanges(startPosition, snippet.tabStops)
if snippet.lineCount > 1
@indentSubsequentLines(startPosition.row, snippet)
editSession.pushOperation
do: =>
@subscribe @editSession, 'cursor-moved.snippet-expansion', (e) => @cursorMoved(e)
@placeTabStopAnchorRanges(startPosition, snippet.tabStops)
@editSession.snippetExpansion = this
undo: => @destroy()
@indentSubsequentLines(startPosition.row, snippet) if snippet.lineCount > 1
@editSession.on 'cursor-moved.snippet-expansion', ({oldBufferPosition, newBufferPosition}) =>
return if @settingTabStop
cursorMoved: ({oldBufferPosition, newBufferPosition}) ->
return if @settingTabStop
oldTabStops = @tabStopsForBufferPosition(oldBufferPosition)
newTabStops = @tabStopsForBufferPosition(newBufferPosition)
oldTabStops = @tabStopsForBufferPosition(oldBufferPosition)
newTabStops = @tabStopsForBufferPosition(newBufferPosition)
@destroy() unless _.intersect(oldTabStops, newTabStops).length
@destroy() unless _.intersect(oldTabStops, newTabStops).length
placeTabStopAnchorRanges: (startPosition, tabStopRanges) ->
return unless @snippet.tabStops.length > 0
@tabStopAnchorRanges = tabStopRanges.map ({start, end}) =>
@editSession.addAnchorRange([startPosition.add(start), startPosition.add(end)])
anchorRange = @editSession.addAnchorRange([startPosition.add(start), startPosition.add(end)])
@subscribe anchorRange, 'destroyed', =>
_.remove(@tabStopAnchorRanges, anchorRange)
anchorRange
@setTabStopIndex(0)
indentSubsequentLines: (startRow, snippet) ->
initialIndent = @editSession.lineForBufferRow(startRow).match(/^\s*/)[0]
for row in [startRow + 1...startRow + snippet.lineCount]
@@ -68,11 +79,13 @@ class SnippetExpansion
_.intersection(@tabStopAnchorRanges, @editSession.anchorRangesForBufferPosition(bufferPosition))
destroy: ->
@unsubscribe()
anchorRange.destroy() for anchorRange in @tabStopAnchorRanges
@editSession.off '.snippet-expansion'
@editSession.snippetExpansion = null
restore: (@editSession) ->
@editSession.snippetExpansion = this
@tabStopAnchorRanges = @tabStopAnchorRanges.map (anchorRange) =>
@editSession.addAnchorRange(anchorRange.getBufferRange())
_.extend(SnippetExpansion.prototype, Subscriber)

View File

@@ -3,34 +3,41 @@ Range = require 'range'
module.exports =
class Snippet
name: null
prefix: null
body: null
lineCount: null
tabStops: null
constructor: ({@bodyPosition, @prefix, @description, body}) ->
@body = @extractTabStops(body)
constructor: ({@name, @prefix, bodyTree}) ->
@body = @extractTabStops(bodyTree)
extractTabStops: (bodyLines) ->
extractTabStops: (bodyTree) ->
tabStopsByIndex = {}
bodyText = []
[row, column] = [0, 0]
for bodyLine, i in bodyLines
lineText = []
for segment in bodyLine
if segment.index
{ index, placeholderText } = segment
tabStopsByIndex[index] = new Range([row, column], [row, column + placeholderText.length])
lineText.push(placeholderText)
else
lineText.push(segment)
column += segment.length
bodyText.push(lineText.join(''))
row++; column = 0
@lineCount = row
# recursive helper function; mutates vars above
extractTabStops = (bodyTree) ->
for segment in bodyTree
if segment.index?
{ index, content } = segment
index = Infinity if index == 0
start = [row, column]
extractTabStops(content)
tabStopsByIndex[index] = new Range(start, [row, column])
else if _.isString(segment)
bodyText.push(segment)
segmentLines = segment.split('\n')
column += segmentLines.shift().length
while nextLine = segmentLines.shift()
row += 1
column = nextLine.length
extractTabStops(bodyTree)
@lineCount = row + 1
@tabStops = []
for index in _.keys(tabStopsByIndex).sort()
@tabStops.push tabStopsByIndex[index]
bodyText.join('\n')
bodyText.join('')

View File

@@ -2,38 +2,46 @@ fs = require 'fs'
PEG = require 'pegjs'
_ = require 'underscore'
SnippetExpansion = require 'snippets/src/snippet-expansion'
Snippet = require './snippet'
require './package-extensions'
module.exports =
name: 'Snippets'
snippetsByExtension: {}
snippetsParser: PEG.buildParser(fs.read(require.resolve 'snippets/snippets.pegjs'), trackLineAndColumn: true)
parser: PEG.buildParser(fs.read(require.resolve 'snippets/snippets.pegjs'), trackLineAndColumn: true)
userSnippetsDir: fs.join(config.configDirPath, 'snippets')
activate: (@rootView) ->
@loadSnippets()
window.snippets = this
@loadAll()
@rootView.on 'editor:attached', (e, editor) => @enableSnippetsInEditor(editor)
loadSnippets: ->
snippetsDir = fs.join(config.configDirPath, 'snippets')
if fs.exists(snippetsDir)
@loadSnippetsFile(path) for path in fs.list(snippetsDir) when fs.extension(path) == '.snippets'
loadAll: ->
for pack in atom.getPackages()
pack.loadSnippets()
loadSnippetsFile: (path) ->
@evalSnippets(fs.base(path, '.snippets'), fs.read(path))
for snippetsPath in fs.list(@userSnippetsDir)
@load(snippetsPath)
evalSnippets: (extension, text) ->
@snippetsByExtension[extension] = @snippetsParser.parse(text)
load: (snippetsPath) ->
@add(fs.readObject(snippetsPath))
add: (snippetsBySelector) ->
for selector, snippetsByName of snippetsBySelector
snippetsByPrefix = {}
for name, attributes of snippetsByName
{ prefix, body } = attributes
bodyTree = @parser.parse(body)
snippet = new Snippet({name, prefix, bodyTree})
snippetsByPrefix[snippet.prefix] = snippet
syntax.addProperties(selector, snippets: snippetsByPrefix)
enableSnippetsInEditor: (editor) ->
editor.command 'snippets:expand', (e) =>
editSession = editor.activeEditSession
prefix = editSession.getCursor().getCurrentWordPrefix()
if snippet = @snippetsByExtension[editSession.getFileExtension()]?[prefix]
if snippet = syntax.getProperty(editSession.getCursorScopes(), "snippets.#{prefix}")
editSession.transact ->
snippetExpansion = new SnippetExpansion(snippet, editSession)
editSession.snippetExpansion = snippetExpansion
editSession.pushOperation
undo: -> snippetExpansion.destroy()
redo: (editSession) -> snippetExpansion.restore(editSession)
new SnippetExpansion(snippet, editSession)
else
e.abortKeyBinding()

View File

@@ -176,3 +176,21 @@ describe "StatusBar", ->
it "displays the diff stat for new files", ->
rootView.open(newPath)
expect(statusBar.gitStatusIcon).toHaveText('+1')
describe "grammar label", ->
it "displays the name of the current grammar", ->
expect(statusBar.find('.grammar-name').text()).toBe 'JavaScript'
describe "when the editor's grammar changes", ->
it "displays the new grammar of the editor", ->
textGrammar = _.find syntax.grammars, (grammar) -> grammar.name is 'Plain Text'
rootView.project.addGrammarOverrideForPath(editor.getPath(), textGrammar)
editor.reloadGrammar()
expect(statusBar.find('.grammar-name').text()).toBe textGrammar.name
describe "when clicked", ->
it "toggles the editor:select-grammar event", ->
eventHandler = jasmine.createSpy('eventHandler')
editor.on 'editor:select-grammar', eventHandler
statusBar.find('.grammar-name').click()
expect(eventHandler).toHaveBeenCalled()

View File

@@ -25,7 +25,7 @@ class StatusBar extends View
@span class: 'current-path', outlet: 'currentPath'
@span class: 'buffer-modified', outlet: 'bufferModified'
@span class: 'cursor-position', outlet: 'cursorPosition'
@span class: 'grammar-name', outlet: 'grammarName'
initialize: (@rootView, @editor) ->
@updatePathText()
@@ -36,6 +36,8 @@ class StatusBar extends View
@updateCursorPositionText()
@subscribe @editor, 'cursor:moved', => @updateCursorPositionText()
@subscribe $(window), 'focus', => @updateStatusBar()
@subscribe @grammarName, 'click', => @editor.trigger 'editor:select-grammar'
@subscribe @editor, 'editor:grammar-changed', => @updateGrammarText()
@subscribeToBuffer()
@@ -48,10 +50,14 @@ class StatusBar extends View
@updateStatusBar()
updateStatusBar: ->
@updateGrammarText()
@updateBranchText()
@updateBufferHasModifiedText(@buffer.isModified())
@updateStatusText()
updateGrammarText: ->
@grammarName.text(@editor.getGrammar().name)
updateBufferHasModifiedText: (differsFromDisk)->
if differsFromDisk
@bufferModified.text('*') unless @isModified

View File

@@ -6,12 +6,19 @@
line-height: 14px;
color: #969696;
position: relative;
-webkit-user-select: none;
cursor: default;
}
.status-bar .cursor-position {
.status-bar .cursor-position,
.status-bar .grammar-name {
padding-left: 10px;
}
.status-bar .grammar-name {
cursor: pointer;
}
.status-bar .git-branch {
float: right;
}

View File

@@ -1,12 +1,6 @@
module.exports =
name: "strip trailing whitespace"
activate: (rootView) ->
for buffer in rootView.project.getBuffers()
@stripTrailingWhitespaceBeforeSave(buffer)
rootView.project.on 'buffer-created', (buffer) =>
@stripTrailingWhitespaceBeforeSave(buffer)
rootView.eachBuffer (buffer) => @stripTrailingWhitespaceBeforeSave(buffer)
stripTrailingWhitespaceBeforeSave: (buffer) ->
buffer.on 'will-be-saved', ->

View File

@@ -14,7 +14,7 @@ describe "TreeView", ->
project = rootView.project
atom.loadPackage("tree-view")
treeView = rootView.find(".tree-view").view()
treeView = TreeView.instance
treeView.root = treeView.find('> li:first').view()
sampleJs = treeView.find('.file:contains(tree-view.js)')
sampleTxt = treeView.find('.file:contains(tree-view.txt)')
@@ -52,24 +52,38 @@ describe "TreeView", ->
rootView = new RootView
atom.loadPackage 'tree-view'
treeView = rootView.find(".tree-view").view()
treeView = TreeView.instance
it "does not create a root node", ->
it "does not attach to the root view or create a root node when initialized", ->
expect(treeView.hasParent()).toBeFalsy()
expect(treeView.root).not.toExist()
it "does not attach to the root view or create a root node when attach() is called", ->
treeView.attach()
expect(treeView.hasParent()).toBeFalsy()
expect(treeView.root).not.toExist()
it "serializes without throwing an exception", ->
expect(-> treeView.serialize()).not.toThrow()
it "creates a root view when the project path is created", ->
rootView.open(require.resolve('fixtures/sample.js'))
expect(treeView.root.getPath()).toBe require.resolve('fixtures')
expect(treeView.root.parent()).toMatchSelector(".tree-view")
describe "when the project is assigned a path because a new buffer is saved", ->
it "creates a root directory view but does not attach to the root view", ->
rootView.getActiveEditSession().saveAs("/tmp/test.txt")
expect(treeView.hasParent()).toBeFalsy()
expect(treeView.root.getPath()).toBe require.resolve('/tmp')
expect(treeView.root.parent()).toMatchSelector(".tree-view")
oldRoot = treeView.root
describe "when the root view is opened to a file path", ->
beforeEach ->
rootView.deactivate()
rootView.project.setPath('/tmp')
expect(treeView.root).not.toEqual oldRoot
expect(oldRoot.hasParent()).toBeFalsy()
rootView = new RootView(require.resolve('fixtures/tree-view/tree-view.js'))
atom.loadPackage 'tree-view'
treeView = TreeView.instance
it "does not attach to the root view but does create a root node when initialized", ->
expect(treeView.hasParent()).toBeFalsy()
expect(treeView.root).toExist()
describe "serialization", ->
[newRootView, newTreeView] = []

View File

@@ -16,7 +16,9 @@ class TreeView extends ScrollView
@instance = TreeView.deserialize(state, rootView)
else
@instance = new TreeView(rootView)
@instance.attach()
if rootView.project.getPath() and not rootView.pathToOpenIsFile
@instance.attach()
@deactivate: ->
@instance.deactivate()
@@ -91,6 +93,7 @@ class TreeView extends ScrollView
@attach()
attach: ->
return unless rootView.project.getPath()
@rootView.horizontal.prepend(this)
@focus()
@@ -118,8 +121,8 @@ class TreeView extends ScrollView
updateRoot: ->
@root?.remove()
if @rootView.project.getRootDirectory()
@root = new DirectoryView(directory: @rootView.project.getRootDirectory(), isExpanded: true, project: @rootView.project)
if rootDirectory = @rootView.project.getRootDirectory()
@root = new DirectoryView(directory: rootDirectory, isExpanded: true, project: @rootView.project)
@append(@root)
else
@root = null
@@ -130,7 +133,6 @@ class TreeView extends ScrollView
revealActiveFile: ->
@attach()
@focus()
return unless activeFilePath = @rootView.getActiveEditor()?.getPath()

View File

@@ -184,3 +184,11 @@ module.exports =
CoffeeScript.eval(contents, bare: true)
else
JSON.parse(contents)
readPlist: (path) ->
plist = require 'plist'
object = null
plist.parseString @read(path), (e, data) ->
throw new Error(e) if e
object = data[0]
object

View File

@@ -74,3 +74,5 @@ $.fn.command = (args...) ->
@on(args...)
$.Event.prototype.abortKeyBinding = ->
$.Event.prototype.currentTargetView = -> $(this.currentTarget).view()
$.Event.prototype.targetView = -> $(this.target).view()

View File

@@ -99,6 +99,14 @@ _.mixin
return unless object?
object
setValueForKeyPath: (object, keyPath, value) ->
keys = keyPath.split('.')
while keys.length > 1
key = keys.shift()
object[key] ?= {}
object = object[key]
object[keys.shift()] = value
compactObject: (object) ->
newObject = {}
for key, value of object

View File

@@ -1,4 +1,6 @@
# Like sands through the hourglass, so are the days of our lives.
require 'atom'
require 'window'
window.attachRootView(window.location.params.pathToOpen)
pathToOpen = atom.getWindowState('pathToOpen') ? window.location.params.pathToOpen
window.attachRootView(pathToOpen)

View File

@@ -87,7 +87,7 @@ html, body {
@font-face {
font-family: 'Octicons Regular';
src: url(octicons-regular-webfont.ttf) format(truetype);
src: url("octicons-regular-webfont.woff") format("woff");
font-weight: normal;
font-style: normal;
}

Binary file not shown.

Binary file not shown.

View File

@@ -6,6 +6,7 @@ module.exports.runSpecSuite = (specSuite, logErrors=true) ->
nakedLoad 'jasmine-focused'
$ = require 'jquery'
TimeReporter = require 'time-reporter'
$('body').append $$ ->
@div id: 'jasmine-content'
@@ -18,5 +19,7 @@ module.exports.runSpecSuite = (specSuite, logErrors=true) ->
require specSuite
jasmineEnv = jasmine.getEnv()
jasmineEnv.addReporter(reporter)
jasmineEnv.addReporter(new TimeReporter())
jasmineEnv.specFilter = (spec) -> reporter.specFilter(spec)
jasmineEnv.execute()