diff --git a/.atom/snippets/coffee.cson b/.atom/snippets/coffee.cson new file mode 100644 index 000000000..5421f12f4 --- /dev/null +++ b/.atom/snippets/coffee.cson @@ -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' diff --git a/.atom/snippets/coffee.snippets b/.atom/snippets/coffee.snippets deleted file mode 100644 index 57b3e3980..000000000 --- a/.atom/snippets/coffee.snippets +++ /dev/null @@ -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 diff --git a/.github b/.github index cca25cd49..079fcb433 100644 --- a/.github +++ b/.github @@ -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 diff --git a/docs/configuring-and-extending.md b/docs/configuring-and-extending.md index b452efe04..f978f117d 100644 --- a/docs/configuring-and-extending.md +++ b/docs/configuring-and-extending.md @@ -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 diff --git a/docs/extensions/intro.md b/docs/extensions/intro.md deleted file mode 100644 index 01c1c040e..000000000 --- a/docs/extensions/intro.md +++ /dev/null @@ -1 +0,0 @@ -## Extensions diff --git a/docs/features.md b/docs/features.md new file mode 100644 index 000000000..ead022319 --- /dev/null +++ b/docs/features.md @@ -0,0 +1 @@ +# Features diff --git a/docs/intro.md b/docs/intro.md index 402cf5a27..32a838246 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -1,3 +1,3 @@ -## The Definitive Guide to Atom +# The Atom Guide Welcome! diff --git a/docs/packages/installing.md b/docs/packages/installing.md new file mode 100644 index 000000000..38dc43160 --- /dev/null +++ b/docs/packages/installing.md @@ -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" + ] +``` diff --git a/docs/packages/intro.md b/docs/packages/intro.md new file mode 100644 index 000000000..4f37c9446 --- /dev/null +++ b/docs/packages/intro.md @@ -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) diff --git a/docs/extensions/markdown-preview.md b/docs/packages/markdown-preview.md similarity index 100% rename from docs/extensions/markdown-preview.md rename to docs/packages/markdown-preview.md diff --git a/docs/extensions/wrap-guide.md b/docs/packages/wrap-guide.md similarity index 100% rename from docs/extensions/wrap-guide.md rename to docs/packages/wrap-guide.md diff --git a/native/v8_extensions/git.js b/native/v8_extensions/git.js index 4354c9f61..fa4b1b49c 100644 --- a/native/v8_extensions/git.js +++ b/native/v8_extensions/git.js @@ -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; })(); diff --git a/native/v8_extensions/git.mm b/native/v8_extensions/git.mm index 39ef3dc0d..b0d8cebe9 100644 --- a/native/v8_extensions/git.mm +++ b/native/v8_extensions/git.mm @@ -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 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; } diff --git a/native/v8_extensions/native.h b/native/v8_extensions/native.h index 1031be7ec..e7a200493 100644 --- a/native/v8_extensions/native.h +++ b/native/v8_extensions/native.h @@ -7,15 +7,18 @@ namespace v8_extensions { class Native : public CefV8Handler { public: Native(); - + virtual bool Execute(const CefString& name, CefRefPtr object, const CefV8ValueList& arguments, CefRefPtr& retval, CefString& exception) OVERRIDE; - + // Provide the reference counting implementation for this class. IMPLEMENT_REFCOUNTING(Native); + +private: + std::string windowState; }; } diff --git a/native/v8_extensions/native.js b/native/v8_extensions/native.js index 85cdf5be8..9124d89fc 100644 --- a/native/v8_extensions/native.js +++ b/native/v8_extensions/native.js @@ -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; + })(); diff --git a/native/v8_extensions/native.mm b/native/v8_extensions/native.mm index c947aec01..0a66591ca 100644 --- a/native/v8_extensions/native.mm +++ b/native/v8_extensions/native.mm @@ -31,6 +31,7 @@ void throwException(const CefRefPtr& global, CefRefPtrGetStringValue().ToString(); + return true; + } + + else if (name == "getWindowState") { + retval = CefV8Value::CreateString(windowState); + return true; + } + return false; }; diff --git a/spec/app/atom-spec.coffee b/spec/app/atom-spec.coffee index 58fa6f810..a117d48b8 100644 --- a/spec/app/atom-spec.coffee +++ b/spec/app/atom-spec.coffee @@ -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", -> diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 4b70927d9..2adf7feb4 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -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" diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index ec4337d76..ae616f814 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -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 diff --git a/spec/app/git-spec.coffee b/spec/app/git-spec.coffee index ace54d1c4..20edab6bb 100644 --- a/spec/app/git-spec.coffee +++ b/spec/app/git-spec.coffee @@ -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() diff --git a/spec/app/pasteboard-spec.coffee b/spec/app/pasteboard-spec.coffee index 8dc6624ff..3cf2b991d 100644 --- a/spec/app/pasteboard-spec.coffee +++ b/spec/app/pasteboard-spec.coffee @@ -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'}] diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index e7660f854..e5abf32a0 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -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() diff --git a/spec/fixtures/packages/package-that-throws-an-exception/index.coffee b/spec/fixtures/packages/package-that-throws-an-exception/index.coffee new file mode 100644 index 000000000..9e2c06779 --- /dev/null +++ b/spec/fixtures/packages/package-that-throws-an-exception/index.coffee @@ -0,0 +1 @@ +throw new Error("This package throws an exception") \ No newline at end of file diff --git a/spec/fixtures/packages/package-with-snippets/snippets/test.cson b/spec/fixtures/packages/package-with-snippets/snippets/test.cson new file mode 100644 index 000000000..b936fea16 --- /dev/null +++ b/spec/fixtures/packages/package-with-snippets/snippets/test.cson @@ -0,0 +1,4 @@ +".test": + "Test Snippet": + prefix: "test" + body: "testing 123" diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index c7dc053ca..01736d4af 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -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 = -> - $('').append(this) - unless fs.md5ForPath(require.resolve('fixtures/sample.js')) == "dd38087d0d7e3e4802a6d3f9b9745f2b" throw "Sample.js is modified" diff --git a/spec/spec-suite.coffee b/spec/spec-suite.coffee index 4e42fa2d7..f888d8a0d 100644 --- a/spec/spec-suite.coffee +++ b/spec/spec-suite.coffee @@ -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 diff --git a/spec/stdlib/jquery-extensions-spec.coffee b/spec/stdlib/jquery-extensions-spec.coffee index 21e8862a7..6b781370b 100644 --- a/spec/stdlib/jquery-extensions-spec.coffee +++ b/spec/stdlib/jquery-extensions-spec.coffee @@ -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 diff --git a/spec/time-reporter.coffee b/spec/time-reporter.coffee new file mode 100644 index 000000000..6546a68bd --- /dev/null +++ b/spec/time-reporter.coffee @@ -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 diff --git a/src/app/anchor-range.coffee b/src/app/anchor-range.coffee index 7adfa1cf2..1f1ad5151 100644 --- a/src/app/anchor-range.coffee +++ b/src/app/anchor-range.coffee @@ -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) diff --git a/src/app/anchor.coffee b/src/app/anchor.coffee index 42932c97c..f0b26b10b 100644 --- a/src/app/anchor.coffee +++ b/src/app/anchor.coffee @@ -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) diff --git a/src/app/atom-package.coffee b/src/app/atom-package.coffee index bb91b60af..7d66ab6a7 100644 --- a/src/app/atom-package.coffee +++ b/src/app/atom-package.coffee @@ -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']) diff --git a/src/app/atom.coffee b/src/app/atom.coffee index a90b675b3..8f02a3db0 100644 --- a/src/app/atom.coffee +++ b/src/app/atom.coffee @@ -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 diff --git a/src/app/config.coffee b/src/app/config.coffee index d006e7540..448a7fc2d 100644 --- a/src/app/config.coffee +++ b/src/app/config.coffee @@ -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 diff --git a/src/app/cursor.coffee b/src/app/cursor.coffee index da44e890e..498b2ef83 100644 --- a/src/app/cursor.coffee +++ b/src/app/cursor.coffee @@ -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()]) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 04aa8d523..24bcb6bf1 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -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) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 90d473597..720afce55 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -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 diff --git a/src/app/git.coffee b/src/app/git.coffee index cdca748b0..a08595ffb 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -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 diff --git a/src/app/keymaps/atom.cson b/src/app/keymaps/atom.cson index 76cc697c6..0780ad30e 100644 --- a/src/app/keymaps/atom.cson +++ b/src/app/keymaps/atom.cson @@ -33,3 +33,4 @@ '.tool-panel': 'meta-escape': 'tool-panel:unfocus' 'escape': 'core:close' + 'meta-w': 'noop' \ No newline at end of file diff --git a/src/app/keymaps/editor.cson b/src/app/keymaps/editor.cson index 27ea35c55..376f822a2 100644 --- a/src/app/keymaps/editor.cson +++ b/src/app/keymaps/editor.cson @@ -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' diff --git a/src/app/package.coffee b/src/app/package.coffee index 49d6445ac..8bcbecfa1 100644 --- a/src/app/package.coffee +++ b/src/app/package.coffee @@ -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) diff --git a/src/app/project.coffee b/src/app/project.coffee index d49a1dae9..3a2b47cf4 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -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() diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 5adee2902..f15e458c1 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -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) diff --git a/src/app/selection.coffee b/src/app/selection.coffee index b3aaf9bed..dce6f1b3e 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -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() diff --git a/src/app/text-mate-package.coffee b/src/app/text-mate-package.coffee index 4e2ce8598..a7cc7bcbc 100644 --- a/src/app/text-mate-package.coffee +++ b/src/app/text-mate-package.coffee @@ -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 = [] diff --git a/src/app/window.coffee b/src/app/window.coffee index 85077e84a..4d0b6e6a7 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -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') diff --git a/src/packages/autoflow/index.coffee b/src/packages/autoflow/index.coffee new file mode 100644 index 000000000..950ab2727 --- /dev/null +++ b/src/packages/autoflow/index.coffee @@ -0,0 +1 @@ +module.exports = require './lib/autoflow' diff --git a/src/packages/autoflow/lib/autoflow.coffee b/src/packages/autoflow/lib/autoflow.coffee new file mode 100644 index 000000000..a1dc9f73b --- /dev/null +++ b/src/packages/autoflow/lib/autoflow.coffee @@ -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 diff --git a/src/packages/autoflow/spec/autoflow-spec.coffee b/src/packages/autoflow/spec/autoflow-spec.coffee new file mode 100644 index 000000000..206172843 --- /dev/null +++ b/src/packages/autoflow/spec/autoflow-spec.coffee @@ -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 + """ diff --git a/src/packages/command-logger/spec/command-logger-spec.coffee b/src/packages/command-logger/spec/command-logger-spec.coffee index c7164454a..1eaee9318 100644 --- a/src/packages/command-logger/spec/command-logger-spec.coffee +++ b/src/packages/command-logger/spec/command-logger-spec.coffee @@ -9,7 +9,6 @@ describe "CommandLogger", -> atom.loadPackage 'command-logger' editor = rootView.getActiveEditor() commandLogger = CommandLogger.instance - rootView.attachToDom() afterEach -> rootView.deactivate() diff --git a/src/packages/command-logger/src/command-logger.coffee b/src/packages/command-logger/src/command-logger.coffee index 29097e8df..75b66cf29 100644 --- a/src/packages/command-logger/src/command-logger.coffee +++ b/src/packages/command-logger/src/command-logger.coffee @@ -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]) diff --git a/src/packages/command-logger/stylesheets/command-logger.css b/src/packages/command-logger/stylesheets/command-logger.css index b0be41559..5e9f34ffa 100644 --- a/src/packages/command-logger/stylesheets/command-logger.css +++ b/src/packages/command-logger/stylesheets/command-logger.css @@ -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; } diff --git a/src/packages/command-panel/spec/command-panel-spec.coffee b/src/packages/command-panel/spec/command-panel-spec.coffee index 884d9abe5..500b401e9 100644 --- a/src/packages/command-panel/spec/command-panel-spec.coffee +++ b/src/packages/command-panel/spec/command-panel-spec.coffee @@ -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", -> diff --git a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee index aba066a63..4453b2602 100644 --- a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee +++ b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee @@ -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 -> diff --git a/src/packages/fuzzy-finder/src/fuzzy-finder.coffee b/src/packages/fuzzy-finder/src/fuzzy-finder.coffee index 1e2566c15..44a9aa17a 100644 --- a/src/packages/fuzzy-finder/src/fuzzy-finder.coffee +++ b/src/packages/fuzzy-finder/src/fuzzy-finder.coffee @@ -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: -> diff --git a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee index 55a364261..aff38972b 100644 --- a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee +++ b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee @@ -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() diff --git a/src/packages/snippets/snippets.pegjs b/src/packages/snippets/snippets.pegjs index 8d3ea766f..72ec9be48 100644 --- a/src/packages/snippets/snippets.pegjs +++ b/src/packages/snippets/snippets.pegjs @@ -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]* diff --git a/src/packages/snippets/spec/snippets-spec.coffee b/src/packages/snippets/spec/snippets-spec.coffee index 6a1688d4d..fd8cea73d 100644 --- a/src/packages/snippets/spec/snippets-spec.coffee +++ b/src/packages/snippets/spec/snippets-spec.coffee @@ -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" + ] diff --git a/src/packages/snippets/src/package-extensions.coffee b/src/packages/snippets/src/package-extensions.coffee new file mode 100644 index 000000000..8ce001487 --- /dev/null +++ b/src/packages/snippets/src/package-extensions.coffee @@ -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 diff --git a/src/packages/snippets/src/snippet-expansion.coffee b/src/packages/snippets/src/snippet-expansion.coffee index b0e80f83d..74dcd5d6f 100644 --- a/src/packages/snippets/src/snippet-expansion.coffee +++ b/src/packages/snippets/src/snippet-expansion.coffee @@ -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) diff --git a/src/packages/snippets/src/snippet.coffee b/src/packages/snippets/src/snippet.coffee index bcdbd3de5..ef7c3d0aa 100644 --- a/src/packages/snippets/src/snippet.coffee +++ b/src/packages/snippets/src/snippet.coffee @@ -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('') diff --git a/src/packages/snippets/src/snippets.coffee b/src/packages/snippets/src/snippets.coffee index 2d06d3998..657153a8d 100644 --- a/src/packages/snippets/src/snippets.coffee +++ b/src/packages/snippets/src/snippets.coffee @@ -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() diff --git a/src/packages/status-bar/spec/status-bar-spec.coffee b/src/packages/status-bar/spec/status-bar-spec.coffee index d990b6a7a..d3c9a2b7f 100644 --- a/src/packages/status-bar/spec/status-bar-spec.coffee +++ b/src/packages/status-bar/spec/status-bar-spec.coffee @@ -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() diff --git a/src/packages/status-bar/src/status-bar.coffee b/src/packages/status-bar/src/status-bar.coffee index feee92e4f..0836898d3 100644 --- a/src/packages/status-bar/src/status-bar.coffee +++ b/src/packages/status-bar/src/status-bar.coffee @@ -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 diff --git a/src/packages/status-bar/stylesheets/status-bar.css b/src/packages/status-bar/stylesheets/status-bar.css index 14ba139c7..e15105d5e 100644 --- a/src/packages/status-bar/stylesheets/status-bar.css +++ b/src/packages/status-bar/stylesheets/status-bar.css @@ -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; } diff --git a/src/packages/strip-trailing-whitespace/src/strip-trailing-whitespace.coffee b/src/packages/strip-trailing-whitespace/src/strip-trailing-whitespace.coffee index f0f784c1b..bf01c6bbe 100644 --- a/src/packages/strip-trailing-whitespace/src/strip-trailing-whitespace.coffee +++ b/src/packages/strip-trailing-whitespace/src/strip-trailing-whitespace.coffee @@ -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', -> diff --git a/src/packages/tree-view/spec/tree-view-spec.coffee b/src/packages/tree-view/spec/tree-view-spec.coffee index 664022285..0ead23590 100644 --- a/src/packages/tree-view/spec/tree-view-spec.coffee +++ b/src/packages/tree-view/spec/tree-view-spec.coffee @@ -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] = [] diff --git a/src/packages/tree-view/src/tree-view.coffee b/src/packages/tree-view/src/tree-view.coffee index 7f8470b7c..a07a4bb7a 100644 --- a/src/packages/tree-view/src/tree-view.coffee +++ b/src/packages/tree-view/src/tree-view.coffee @@ -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() diff --git a/src/stdlib/fs.coffee b/src/stdlib/fs.coffee index cb22ab5b1..8f1540426 100644 --- a/src/stdlib/fs.coffee +++ b/src/stdlib/fs.coffee @@ -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 diff --git a/src/stdlib/jquery-extensions.coffee b/src/stdlib/jquery-extensions.coffee index 53a851e28..c90fa6763 100644 --- a/src/stdlib/jquery-extensions.coffee +++ b/src/stdlib/jquery-extensions.coffee @@ -74,3 +74,5 @@ $.fn.command = (args...) -> @on(args...) $.Event.prototype.abortKeyBinding = -> +$.Event.prototype.currentTargetView = -> $(this.currentTarget).view() +$.Event.prototype.targetView = -> $(this.target).view() diff --git a/src/stdlib/underscore-extensions.coffee b/src/stdlib/underscore-extensions.coffee index 41fa22764..be1245349 100644 --- a/src/stdlib/underscore-extensions.coffee +++ b/src/stdlib/underscore-extensions.coffee @@ -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 diff --git a/src/window-bootstrap.coffee b/src/window-bootstrap.coffee index 86404bb98..5720d9af9 100644 --- a/src/window-bootstrap.coffee +++ b/src/window-bootstrap.coffee @@ -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) diff --git a/static/atom.css b/static/atom.css index dd94ac496..13a207fee 100644 --- a/static/atom.css +++ b/static/atom.css @@ -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; } diff --git a/static/octicons-regular-webfont.ttf b/static/octicons-regular-webfont.ttf deleted file mode 100644 index 0c956f793..000000000 Binary files a/static/octicons-regular-webfont.ttf and /dev/null differ diff --git a/static/octicons-regular-webfont.woff b/static/octicons-regular-webfont.woff new file mode 100644 index 000000000..0e9c3f1ee Binary files /dev/null and b/static/octicons-regular-webfont.woff differ diff --git a/vendor/jasmine-helper.coffee b/vendor/jasmine-helper.coffee index c99b44051..0defd1990 100644 --- a/vendor/jasmine-helper.coffee +++ b/vendor/jasmine-helper.coffee @@ -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()