diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab19877b4..2afb8ac19 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,7 +30,7 @@ in the proper package's repository. * Follow the [CoffeeScript](#coffeescript-styleguide), [JavaScript](https://github.com/styleguide/javascript), and [CSS](https://github.com/styleguide/css) styleguides - * Include thoughtfully worded [Jasmine](http://pivotal.github.com/jasmine/) + * Include thoughtfully worded [Jasmine](http://pivotal.github.com/jasmine) specs * Avoid placing files in `vendor`. 3rd-party packages should be added as a `package.json` dependency. @@ -61,3 +61,32 @@ in the proper package's repository. * Set parameter defaults without spaces around the equal sign * `clear = (count=1) ->` instead of `clear = (count = 1) ->` + +## Documentation Styleguide + +* Use [TomDoc](http://tomdoc.org). +* Use [Markdown](https://daringfireball.net/projects/markdown). +* Reference classes with `{ClassName}` style notation. +* Reference methods with `{ClassName.methodName}` style notation. +* Delegate to comments elsewhere with `{Delegates to: ClassName.methodName}` + style notation. + +### Example + +```coffee +# Public: Disable the package with the given name. +# +# This method emits multiple events: +# +# * `package-will-be-disabled` - before the package is disabled. +# * `package-disabled` - after the package is disabled. +# +# name - The {String} name of the package to disable. +# options - The {Object} with disable options (default: {}): +# :trackTime - `true` to track the amount of time disabling took. +# :ignoreErrors - `true` to catch and ignore errors thrown. +# callback - The {Function} to call after the package has been disabled. +# +# Returns `undefined`. +disablePackage: (name, options, callback) -> +``` diff --git a/build/package.json b/build/package.json index 8fe89a79e..3e967318a 100644 --- a/build/package.json +++ b/build/package.json @@ -7,7 +7,7 @@ }, "dependencies": { "async": "~0.2.9", - "biscotto": "git://github.com/atom/biscotto.git#12188bfbe5f7303fa9f1aa3c4f8662d40ce3c3be", + "biscotto": "0.6.0", "first-mate": "1.x", "formidable": "~1.0.14", "fs-plus": "1.x", @@ -25,15 +25,13 @@ "grunt-peg": "~1.1.0", "grunt-shell": "~0.3.1", "harmony-collections": "~0.3.8", - "js-yaml": "~2.1.0", "json-front-matter": "~0.1.3", "rcedit": "~0.1.2", "request": "~2.27.0", "rimraf": "~2.2.2", - "runas": "~0.3.0", + "runas": "0.5.x", "underscore-plus": "1.x", "unzip": "~0.1.9", - "vm-compatibility-layer": "~0.1.0", - "walkdir": "0.0.7" + "vm-compatibility-layer": "~0.1.0" } } diff --git a/build/tasks/build-task.coffee b/build/tasks/build-task.coffee index 0e1d47e61..bcbcd47aa 100644 --- a/build/tasks/build-task.coffee +++ b/build/tasks/build-task.coffee @@ -21,7 +21,6 @@ module.exports = (grunt) -> cp 'atom.sh', path.join(appDir, 'atom.sh') cp 'package.json', path.join(appDir, 'package.json') - cp 'apm', path.join(appDir, 'apm') packageDirectories = [] nonPackageDirectories = [ @@ -47,8 +46,12 @@ module.exports = (grunt) -> path.join('less', 'dist') path.join('less', 'test') path.join('bootstrap', 'docs') + path.join('bootstrap', 'examples') path.join('spellchecker', 'vendor') path.join('xmldom', 'test') + path.join('jasmine-reporters', 'ext') + path.join('build', 'Release', 'obj.target') + path.join('build', 'Release', '.deps') path.join('vendor', 'apm') path.join('resources', 'mac') path.join('resources', 'win') @@ -64,6 +67,7 @@ module.exports = (grunt) -> cp 'spec', path.join(appDir, 'spec') cp 'src', path.join(appDir, 'src'), filter: /.+\.(cson|coffee)$/ cp 'static', path.join(appDir, 'static') + cp 'apm', path.join(appDir, 'apm'), filter: nodeModulesFilter if process.platform is 'darwin' grunt.file.recurse path.join('resources', 'mac'), (sourcePath, rootDirectory, subDirectory='', filename) -> diff --git a/build/tasks/convert-theme.coffee b/build/tasks/convert-theme.coffee deleted file mode 100644 index e94370e16..000000000 --- a/build/tasks/convert-theme.coffee +++ /dev/null @@ -1,102 +0,0 @@ -path = require 'path' - -_ = require 'underscore-plus' -fs = require 'fs-plus' -{ScopeSelector} = require 'first-mate' - -module.exports = (grunt) -> - grunt.registerTask 'convert-theme', 'Convert a TextMate theme to an Atom theme', -> - if textMateThemePath = grunt.option('path') - textMateThemePath = path.resolve(textMateThemePath) - if grunt.file.isFile(textMateThemePath) - textMateTheme = new TextMateTheme(textMateThemePath) - themeName = path.basename(textMateThemePath, path.extname(textMateThemePath)) - atomThemePath = path.join(path.dirname(textMateThemePath), "#{themeName.toLowerCase()}-syntax.css") - grunt.file.write(atomThemePath, textMateTheme.getStylesheet()) - grunt.log.ok("Atom theme written to: #{atomThemePath}") - else - grunt.log.error("No theme file found at: #{textMateThemePath}") - false - else - grunt.log.error('Must specify --path=') - false - -class TextMateTheme - constructor: (@path) -> - @rulesets = [] - @buildRulesets() - - buildRulesets: -> - {settings} = fs.readPlistSync(@path) - @buildGlobalSettingsRulesets(settings[0]) - @buildScopeSelectorRulesets(settings[1..]) - - getStylesheet: -> - lines = [] - for {selector, properties} in @getRulesets() - lines.push("#{selector} {") - lines.push " #{name}: #{value};" for name, value of properties - lines.push("}\n") - lines.join('\n') - - getRulesets: -> @rulesets - - buildGlobalSettingsRulesets: ({settings}) -> - { background, foreground, caret, selection, lineHighlight } = settings - - @rulesets.push - selector: '.editor, .editor .gutter' - properties: - 'background-color': @translateColor(background) - 'color': @translateColor(foreground) - - @rulesets.push - selector: '.editor.is-focused .cursor' - properties: - 'border-color': @translateColor(caret) - - @rulesets.push - selector: '.editor.is-focused .selection .region' - properties: - 'background-color': @translateColor(selection) - - @rulesets.push - selector: '.editor.is-focused .line-number.cursor-line-no-selection, .editor.is-focused .line.cursor-line' - properties: - 'background-color': @translateColor(lineHighlight) - - buildScopeSelectorRulesets: (scopeSelectorSettings) -> - for { name, scope, settings } in scopeSelectorSettings - continue unless scope - @rulesets.push - comment: name - selector: @translateScopeSelector(scope) - properties: @translateScopeSelectorSettings(settings) - - translateScopeSelector: (textmateScopeSelector) -> - new ScopeSelector(textmateScopeSelector).toCssSelector() - - translateScopeSelectorSettings: ({ foreground, background, fontStyle }) -> - properties = {} - - if fontStyle - fontStyles = fontStyle.split(/\s+/) - properties['font-weight'] = 'bold' if _.contains(fontStyles, 'bold') - properties['font-style'] = 'italic' if _.contains(fontStyles, 'italic') - properties['text-decoration'] = 'underline' if _.contains(fontStyles, 'underline') - - properties['color'] = @translateColor(foreground) if foreground - properties['background-color'] = @translateColor(background) if background - properties - - translateColor: (textmateColor) -> - if textmateColor.length <= 7 - textmateColor - else - r = parseInt(textmateColor[1..2], 16) - g = parseInt(textmateColor[3..4], 16) - b = parseInt(textmateColor[5..6], 16) - a = parseInt(textmateColor[7..8], 16) - a = Math.round((a / 255.0) * 100) / 100 - - "rgba(#{r}, #{g}, #{b}, #{a})" diff --git a/build/tasks/docs-task.coffee b/build/tasks/docs-task.coffee index 6f9b3c8b4..53dab4ca7 100644 --- a/build/tasks/docs-task.coffee +++ b/build/tasks/docs-task.coffee @@ -15,24 +15,7 @@ module.exports = (grunt) -> grunt.registerTask 'build-docs', 'Builds the API docs in src', -> done = @async() - downloadFileFromRepo = ({repo, file}, callback) -> - uri = "https://raw2.github.com/atom/#{repo}/master/#{file}" - request uri, (error, response, contents) -> - return callback(error) if error? - downloadPath = path.join('docs', 'includes', repo, file) - fs.writeFile downloadPath, contents, (error) -> - callback(error, downloadPath) - - includes = [ - {repo: 'first-mate', file: 'src/grammar-registry.coffee'} - {repo: 'space-pen', file: 'src/space-pen.coffee'} - {repo: 'text-buffer', file: 'src/marker.coffee'} - {repo: 'text-buffer', file: 'src/point.coffee'} - {repo: 'text-buffer', file: 'src/range.coffee'} - {repo: 'theorist', file: 'src/model.coffee'} - ] - - async.map includes, downloadFileFromRepo, (error, includePaths) -> + downloadIncludes (error, includePaths) -> if error? done(error) else @@ -49,13 +32,32 @@ module.exports = (grunt) -> grunt.registerTask 'lint-docs', 'Generate stats about the doc coverage', -> done = @async() - args = [commonArgs..., '--noOutput', 'src/'] - grunt.util.spawn({cmd, args, opts}, done) + downloadIncludes (error, includePaths) -> + if error? + done(error) + else + args = [ + commonArgs... + '--noOutput' + 'src/' + includePaths... + ] + grunt.util.spawn({cmd, args, opts}, done) grunt.registerTask 'missing-docs', 'Generate stats about the doc coverage', -> done = @async() - args = [commonArgs..., '--noOutput', '--missing', 'src/'] - grunt.util.spawn({cmd, args, opts}, done) + downloadIncludes (error, includePaths) -> + if error? + done(error) + else + args = [ + commonArgs... + '--noOutput' + '--missing' + 'src/' + includePaths... + ] + grunt.util.spawn({cmd, args, opts}, done) grunt.registerTask 'copy-docs', 'Copies over latest API docs to atom-docs', -> done = @async() @@ -129,3 +131,24 @@ module.exports = (grunt) -> grunt.util.spawn({cmd, args, opts}, callback) grunt.util.async.waterfall [fetchTag, stageDocs, fetchSha, commitChanges, pushOrigin, pushHeroku], done + +downloadFileFromRepo = ({repo, file}, callback) -> + uri = "https://raw.github.com/atom/#{repo}/master/#{file}" + request uri, (error, response, contents) -> + return callback(error) if error? + downloadPath = path.join('docs', 'includes', repo, file) + fs.writeFile downloadPath, contents, (error) -> + callback(error, downloadPath) + +downloadIncludes = (callback) -> + includes = [ + {repo: 'first-mate', file: 'src/grammar.coffee'} + {repo: 'first-mate', file: 'src/grammar-registry.coffee'} + {repo: 'space-pen', file: 'src/space-pen.coffee'} + {repo: 'text-buffer', file: 'src/marker.coffee'} + {repo: 'text-buffer', file: 'src/point.coffee'} + {repo: 'text-buffer', file: 'src/range.coffee'} + {repo: 'theorist', file: 'src/model.coffee'} + ] + + async.map(includes, downloadFileFromRepo, callback) diff --git a/build/tasks/install-task.coffee b/build/tasks/install-task.coffee index a2b591734..273e1b5ef 100644 --- a/build/tasks/install-task.coffee +++ b/build/tasks/install-task.coffee @@ -1,24 +1,20 @@ path = require 'path' +runas = null module.exports = (grunt) -> - {cp, mkdir, rm, spawn} = require('./task-helpers')(grunt) + {cp, mkdir, rm} = require('./task-helpers')(grunt) grunt.registerTask 'install', 'Install the built application', -> installDir = grunt.config.get('atom.installDir') shellAppDir = grunt.config.get('atom.shellAppDir') if process.platform is 'win32' - done = @async() - - runas = require 'runas' + runas ?= require 'runas' copyFolder = path.resolve 'script', 'copy-folder.cmd' - # cmd /c ""script" "source" "destination"" - arg = "/c \"\"#{copyFolder}\" \"#{shellAppDir}\" \"#{installDir}\"\"" - if runas('cmd', [arg], hide: true) isnt 0 - done("Failed to copy #{shellAppDir} to #{installDir}") + if runas('cmd', ['/c', copyFolder, shellAppDir, installDir], admin: true) isnt 0 + grunt.log.error("Failed to copy #{shellAppDir} to #{installDir}") createShortcut = path.resolve 'script', 'create-shortcut.cmd' - args = ['/c', createShortcut, path.join(installDir, 'atom.exe'), 'Atom'] - spawn {cmd: 'cmd', args}, done + runas('cmd', ['/c', createShortcut, path.join(installDir, 'atom.exe'), 'Atom']) else rm installDir mkdir path.dirname(installDir) diff --git a/build/tasks/task-helpers.coffee b/build/tasks/task-helpers.coffee index f83430884..4b388ce63 100644 --- a/build/tasks/task-helpers.coffee +++ b/build/tasks/task-helpers.coffee @@ -1,27 +1,41 @@ -fs = require 'fs' +fs = require 'fs-plus' path = require 'path' -walkdir = require 'walkdir' module.exports = (grunt) -> cp: (source, destination, {filter}={}) -> unless grunt.file.exists(source) grunt.fatal("Cannot copy non-existent #{source.cyan} to #{destination.cyan}") - try - walkdir.sync source, (sourcePath, stats) -> - return if filter?.test(sourcePath) + copyFile = (sourcePath, destinationPath) -> + return if filter?.test(sourcePath) - destinationPath = path.join(destination, path.relative(source, sourcePath)) - if stats.isSymbolicLink() - grunt.file.mkdir(path.dirname(destinationPath)) - fs.symlinkSync(fs.readlinkSync(sourcePath), destinationPath) - else if stats.isFile() - grunt.file.copy(sourcePath, destinationPath) + stats = fs.lstatSync(sourcePath) + if stats.isSymbolicLink() + grunt.file.mkdir(path.dirname(destinationPath)) + fs.symlinkSync(fs.readlinkSync(sourcePath), destinationPath) + else if stats.isFile() + grunt.file.copy(sourcePath, destinationPath) - if grunt.file.exists(destinationPath) - fs.chmodSync(destinationPath, fs.statSync(sourcePath).mode) - catch error - grunt.fatal(error) + if grunt.file.exists(destinationPath) + fs.chmodSync(destinationPath, fs.statSync(sourcePath).mode) + + if grunt.file.isFile(source) + copyFile(source, destination) + else + try + onFile = (sourcePath) -> + destinationPath = path.join(destination, path.relative(source, sourcePath)) + copyFile(sourcePath, destinationPath) + onDirectory = (sourcePath) -> + if fs.isSymbolicLinkSync(sourcePath) + destinationPath = path.join(destination, path.relative(source, sourcePath)) + copyFile(sourcePath, destinationPath) + false + else + true + fs.traverseTreeSync source, onFile, onDirectory + catch error + grunt.fatal(error) grunt.verbose.writeln("Copied #{source.cyan} to #{destination.cyan}.") diff --git a/build/tasks/update-octicons-task.coffee b/build/tasks/update-octicons-task.coffee new file mode 100644 index 000000000..bdc56e8f5 --- /dev/null +++ b/build/tasks/update-octicons-task.coffee @@ -0,0 +1,22 @@ +path = require 'path' + +module.exports = (grunt) -> + grunt.registerTask 'update-octicons', 'Update octicon font and LESS variables', -> + pathToOcticons = path.resolve('..', 'octicons') + if grunt.file.isDir(pathToOcticons) + # Copy font-file + fontSrc = path.join(pathToOcticons, 'octicons', 'octicons.woff') + fontDest = path.resolve('static', 'octicons.woff') + grunt.file.copy(fontSrc, fontDest) + + # Update Octicon UTF codes + glyphsSrc = path.join(pathToOcticons, 'data', 'glyphs.yml') + output = [] + for {css, code} in grunt.file.readYAML(glyphsSrc) + output.push "@#{css}: \"\\#{code}\";" + + octiconUtfDest = path.resolve('static', 'variables', 'octicon-utf-codes.less') + grunt.file.write(octiconUtfDest, "#{output.join('\n')}\n") + else + grunt.log.error("octicons repo must be cloned to #{pathToOcticons}") + false diff --git a/docs/advanced/serialization.md b/docs/advanced/serialization.md index ce1d7a78e..282d9e358 100644 --- a/docs/advanced/serialization.md +++ b/docs/advanced/serialization.md @@ -71,27 +71,3 @@ will only attempt to call deserialize if the two versions match, and otherwise return undefined. We plan on implementing a migration system in the future, but this at least protects you from improperly deserializing old state. If you find yourself in dire need of the migration system, let us know. - -### Deferred Package Deserializers - -If your package defers loading on startup with an `activationEvents` property in -its `package.cson`, your deserializers won't be loaded until your package is -activated. If you want to deserialize an object from your package on startup, -this could be a problem. - -The solution is to also supply a `deferredDeserializers` array in your -`package.cson` with the names of all your deserializers. When Atom attempts to -deserialize some state whose `deserializer` matches one of these names, it will -load your package first so it can register any necessary deserializers before -proceeding. - -For example, the markdown preview package doesn't fully load until a preview is -triggered. But if you refresh a window with a preview pane, it loads the -markdown package early so Atom can deserialize the view correctly. - -```coffee-script -# markdown-preview/package.cson -'activationEvents': 'markdown-preview:toggle': '.editor' -'deferredDeserializers': ['MarkdownPreviewView'] -... -``` diff --git a/docs/converting-a-text-mate-bundle.md b/docs/converting-a-text-mate-bundle.md new file mode 100644 index 000000000..f3a5da4b8 --- /dev/null +++ b/docs/converting-a-text-mate-bundle.md @@ -0,0 +1,52 @@ +## Converting a TextMate Bundle + +This guide will show you how to convert a [TextMate][TextMate] bundle to an +Atom package. + +Converting a TextMate bundle will allow you to use its editor preferences, +snippets, and colorization inside Atom. + +### Install apm + +The `apm` command line utility that ships with Atom supports converting +a TextMate bundle to an Atom package. + +Check that you have `apm` installed by running the following command in your +terminal: + +```sh +apm help init +``` + +You should see a message print out with details about the `apm init` command. + +If you do not, launch Atom and run the _Atom > Install Shell Commmands_ menu +to install the `apm` and `atom` commands. + +### Convert the Package + +Let's convert the TextMate bundle for the [R][R] programming language. You can find other existing TextMate bundles [here][TextMateOrg]. + +You can convert the R bundle with the following command: + +```sh +apm init --package ~/.atom/packages/language-r --convert https://github.com/textmate/r.tmbundle +``` + +You can now browse to `~/.atom/packages/language-r` to see the converted bundle. + +:tada: Your new package is now ready to use, launch Atom and open a `.r` file in +the editor to see it in action! + +### Further Reading + +* Check out [Publishing a Package](publish-a-package.html) for more information + on publishing the package you just created to [atom.io][atomio]. + +[atomio]: https://atom.io +[CSS]: http://en.wikipedia.org/wiki/Cascading_Style_Sheets +[LESS]: http://lesscss.org +[plist]: http://en.wikipedia.org/wiki/Property_list +[R]: http://en.wikipedia.org/wiki/R_(programming_language) +[TextMate]: http://macromates.com +[TextMateOrg]: https://github.com/textmate/r.tmbundle diff --git a/docs/converting-a-text-mate-theme.md b/docs/converting-a-text-mate-theme.md new file mode 100644 index 000000000..3fd454d27 --- /dev/null +++ b/docs/converting-a-text-mate-theme.md @@ -0,0 +1,68 @@ +## Converting a TextMate Theme + +This guide will show you how to convert a [TextMate][TextMate] theme to an Atom +theme. + +### Differences + +TextMate themes use [plist][plist] files while Atom themes use [CSS][CSS] or +[LESS][LESS] to style the UI and syntax in the editor. + +The utility that converts the theme first parses the theme's plist file and +then creates comparable CSS rules and properties that will style Atom similarly. + +### Install apm + +The `apm` command line utility that ships with Atom supports converting +a TextMate theme to an Atom theme. + +Check that you have `apm` installed by running the following command in your +terminal: + +```sh +apm help init +``` + +You should see a message print out with details about the `apm init` command. + +If you do not, launch Atom and run the _Atom > Install Shell Commmands_ menu +to install the `apm` and `atom` commands. + +You can now run `apm help init` to see all the options for initializing new +packages and themes. + +### Convert the Theme + +Download the theme you wish to convert, you can browse existing TextMate themes +[here][TextMateThemes]. + +Now, let's say you've downloaded the theme to `~/Downloads/MyTheme.tmTheme`, +you can convert the theme with the following command: + +```sh +apm init --theme ~/.atom/packages/my-theme --convert ~/Downloads/MyTheme.tmTheme +``` + +You can browse to `~/.atom/packages/my-theme` to see the converted theme. + +### Activate the Theme + +Now that your theme is installed to `~/.atom/packages` you can enable it +by launching Atom and selecting the _Atom > Preferences..._ menu. + +Select the _Themes_ link on the left side and choose _My Theme_ from the +__Syntax Theme__ dropdown menu to enable your new theme. + +:tada: Your theme is now enabled, open an editor to see it in action! + +### Further Reading + +* Check out [Publishing a Package](publish-a-package.html) for more information + on publishing the theme you just created to [atom.io][atomio]. + +[atomio]: https://atom.io +[CSS]: http://en.wikipedia.org/wiki/Cascading_Style_Sheets +[LESS]: http://lesscss.org +[plist]: http://en.wikipedia.org/wiki/Property_list +[TextMate]: http://macromates.com +[TextMateThemes]: http://wiki.macromates.com/Themes/UserSubmittedThemes diff --git a/docs/customizing-atom.md b/docs/customizing-atom.md index e6ad89305..49d196e50 100644 --- a/docs/customizing-atom.md +++ b/docs/customizing-atom.md @@ -112,14 +112,16 @@ namespaces: `core` and `editor`. ### Quick Personal Hacks -### user.coffee +### init.coffee -When Atom finishes loading, it will evaluate _user.coffee_ in your _~/.atom_ +When Atom finishes loading, it will evaluate _init.coffee_ in your _~/.atom_ directory, giving you a chance to run arbitrary personal CoffeeScript code to make customizations. You have full access to Atom's API from code in this file. If customizations become extensive, consider [creating a package][create-a-package]. +This file can also be named _init.js_ and contain JavaScript code. + ### styles.css If you want to apply quick-and-dirty personal styling changes without creating diff --git a/docs/proposals/atom-docs.md b/docs/proposals/atom-docs.md deleted file mode 100644 index f3b6a0ec9..000000000 --- a/docs/proposals/atom-docs.md +++ /dev/null @@ -1,63 +0,0 @@ -## Atom Documentation Format - -This document describes our documentation format, which is markdown with -a few rules. - -### Philosophy - -1. Method and argument names **should** clearly communicate its use. -1. Use documentation to enhance and not correct method/argument names. - -#### Basic - -In some cases all that's required is a single line. **Do not** feel -obligated to write more because we have a format. - -```markdown -# Private: Returns the number of pixels from the top of the screen. -``` - -* **Each method should declare whether it's public or private by using `Public:` -or `Private:`** prefix. -* Following the colon, there should be a short description (that isn't redundant with the -method name). -* Documentation should be hard wrapped to 80 columns. - -### Public vs Private - -If a method is public it can be used by other classes (and possibly by -the public API). The appropriate steps should be taken to minimize the impact -when changing public methods. In some cases that might mean adding an -appropriate release note. In other cases it might mean doing the legwork to -ensure all affected packages are updated. - -#### Complex - -For complex methods it's necessary to explain exactly what arguments -are required and how different inputs effect the operation of the -function. - -The idea is to communicate things that the API user might not know about, -so repeating information that can be gleaned from the method or argument names -is not useful. - -```markdown -# Private: Determine the accelerator for a given command. -# -# * command: -# The name of the command. -# * keystrokesByCommand: -# An {Object} whose keys are commands and the values are Arrays containing -# the keystrokes. -# * options: -# + accelerators: -# Boolean to determine whether accelerators should be shown. -# -# Returns a String containing the keystroke in a format that can be interpreted -# by atom shell to provide nice icons where available. -# -# Raises an Exception if no window is available. -``` - -* Use curly brackets `{}` to provide links to other classes. -* Use `+` for the options list. diff --git a/docs/publishing-a-package.md b/docs/publishing-a-package.md new file mode 100644 index 000000000..62e2a9591 --- /dev/null +++ b/docs/publishing-a-package.md @@ -0,0 +1,97 @@ +## Publishing a Package + +This guide will show you how to publish a package or theme to the +[atom.io][atomio] package registry. + +Publishing a package allows other people to install it and use it in Atom. It +is a great way to share what you've made and get feedback and contributions from +others. + +This guide assumes your package's name is `my-package` and but you should pick a +better name. + +### Install apm + +The `apm` command line utility that ships with Atom supports publishing packages +to the atom.io registry. + +Check that you have `apm` installed by running the following command in your +terminal: + +```sh +apm help publish +``` + +You should see a message print out with details about the `apm publish` command. + +If you do not, launch Atom and run the _Atom > Install Shell Commmands_ menu +to install the `apm` and `atom` commands. + +### Prepare Your Package + +If you've followed the steps in the [your first package][your-first-package] +doc then you should be ready to publish and you can skip to the next step. + +If not, there are a few things you should check before publishing: + + * Your *package.json* file has `name`, `description`, and `repository` fields. + * Your *package.json* file has a `version` field with a value of `"0.0.0"`. + * Your *package.json* file has an `engines` field that contains an entry + for Atom such as: `"engines": {"atom": ">=0.50.0"}`. + * Your package has a `README.md` file at the root. + * Your package is in a Git repository that has been pushed to + [GitHub][github]. Follow [this guide][repo-guide] if your package isn't + already on GitHub. + +### Publish Your Package + +Before you publish a package it is a good idea to check ahead of time if +a package with the same name has already been published to atom.io. You can do +that by visiting `http://atom.io/packages/my-package` to see if the package +already exists. If it does, update your package's name to something that is +available before proceeding. + +Now let's review what the `apm publish` command does: + + 1. Registers the package name on atom.io if it is being published for the + first time. + 2. Updates the `version` field in the *package.json* file and commits it. + 3. Creates a new [Git tag][git-tag] for the version being published. + 4. Pushes the tag and current branch up to GitHub. + 5. Updates atom.io with the new version being published. + +Now run the following commands to publish your package: + +```sh +cd ~/github/my-package +apm publish minor +``` + +If this is the first package you are publishing, the `apm publish` command may +prompt you for your GitHub username and password. This is required to publish +and you only need to enter this information the first time you publish. The +credentials are stored securely in your [keychain][keychain] once you login. + +:tada: Your package is now published and available on atom.io. Head on over to +`http://atom.io/packages/my-package` to see your package's page. + +The `minor` option to the publish command tells apm to increment the second +digit of the version before publishing so the published version will be `0.1.0` +and the Git tag created will be `v0.1.0`. + +In the future you can run `apm publish major` to publish the `1.0.0` version but +since this was the first version being published it is a good idead to start +with a minor release. + +### Further Reading + +* Check out [semantic versioning][semver] to learn more about versioning your + package releases. + +[atomio]: https://atom.io +[github]: https://github.com +[git-tag]: http://git-scm.com/book/en/Git-Basics-Tagging +[keychain]: http://en.wikipedia.org/wiki/Keychain_(Apple) +[repo-guide]: http://guides.github.com/overviews/desktop +[semver]: http://semver.org +[your-first-package]: your-first-package.html diff --git a/docs/your-first-package.md b/docs/your-first-package.md index c1e0b991d..aea24456b 100644 --- a/docs/your-first-package.md +++ b/docs/your-first-package.md @@ -5,6 +5,7 @@ selected text with [ascii art](http://en.wikipedia.org/wiki/ASCII_art). When you run our new command with the word "cool" selected, it will be replaced with: ``` + ___ /\_ \ ___ ___ ___\//\ \ /'___\ / __`\ / __`\\ \ \ @@ -25,17 +26,17 @@ Atom will open a new window with the contents of our new _ascii-art_ package displayed in the Tree View. Because this window is opened **after** the package is created, the ASCII Art package will be loaded and available in our new window. To verify this, toggle the Command Palette (`cmd-shift-P`) and type -"ASCII Art" you'll see a new `ASCII Art: Toggle` command. When triggered, this +"ASCII Art". You'll see a new `ASCII Art: Toggle` command. When triggered, this command displays a default message. -Now let's edit the package files to make our ascii art package do something +Now let's edit the package files to make our ASCII Art package do something interesting. Since this package doesn't need any UI, we can remove all view-related code. Start by opening up _lib/ascii-art.coffee_. Remove all view -code, so the file looks like this: +code, so the `module.exports` section looks like this: ```coffeescript - module.exports = - activate: -> +module.exports = + activate: -> ``` ## Create a Command @@ -69,23 +70,24 @@ command palette or by pressing `ctrl-alt-cmd-l`. ## Trigger the Command Now open the command panel and search for the `ascii-art:convert` command. But -its not there! To fix this open _package.json_ and find the property called -`activationEvents`. Activation Events speed up load time by allowing an Atom to -delay a package's activation until it's needed. So add the `ascii-art:convert` -to the activationEvents array: +it's not there! To fix this, open _package.json_ and find the property called +`activationEvents`. Activation Events speed up load time by allowing Atom to +delay a package's activation until it's needed. So remove the existing command +and add `ascii-art:convert` to the `activationEvents` array: ```json "activationEvents": ["ascii-art:convert"], ``` -First, run reload the window by running the command `window:reload`. Now when -you run the `ascii-art:convert` command it will output 'Hello, World!' +First, reload the window by running the command `window:reload`. Now when you +run the `ascii-art:convert` command it will output 'Hello, World!' -## Add A Key Binding +## Add a Key Binding Now let's add a key binding to trigger the `ascii-art:convert` command. Open _keymaps/ascii-art.cson_ and add a key binding linking `ctrl-alt-a` to the -`ascii-art:convert` command. When finished, the file will look like this: +`ascii-art:convert` command. You can delete the pre-existing key binding since +you don't need it anymore. When finished, the file will look like this: ```coffeescript '.editor': @@ -105,25 +107,25 @@ that it **doesn't** work when the Tree View is focused. ## Add the ASCII Art -Now we need to convert the selected text to ascii art. To do this we will use +Now we need to convert the selected text to ASCII art. To do this we will use the [figlet](https://npmjs.org/package/figlet) [node](http://nodejs.org/) module from [npm](https://npmjs.org/). Open _package.json_ and add the latest version of figlet to the dependencies: ```json - "dependencies": { - "figlet": "1.0.8" - } +"dependencies": { + "figlet": "1.0.8" +} ``` -After saving the file run the command 'update-package-dependencies:update' from -the Command Palette. This will install the packages node module dependencies, +After saving the file, run the command 'update-package-dependencies:update' from +the Command Palette. This will install the package's node module dependencies, only figlet in this case. You will need to run 'update-package-dependencies:update' whenever you update the dependencies field in your _package.json_ file. Now require the figlet node module in _lib/ascii-art.coffee_ and instead of -inserting 'Hello, World!' convert the selected text to ascii art! +inserting 'Hello, World!' convert the selected text to ASCII art. ```coffeescript convert: -> @@ -139,7 +141,15 @@ convert: -> selection.insertText("\n#{asciiArt}\n") ``` +Select some text in an editor window and hit `cmd-alt-a`. :tada: You're now an +ASCII art professional! + ## Further reading -For more information on the mechanics of packages, check out [Creating a -Package](creating-a-package.html) +* [Getting your project on GitHub guide](http://guides.github.com/overviews/desktop) + +* [Creating a package guide](creating-a-package.html) for more information + on the mechanics of packages + +* [Publishing a package guide](publish-a-package.html) for more information + on publishing your package to [atom.io](https://atom.io) diff --git a/dot-atom/init.coffee b/dot-atom/init.coffee new file mode 100644 index 000000000..4d10e775b --- /dev/null +++ b/dot-atom/init.coffee @@ -0,0 +1,14 @@ +# Your init script +# +# Atom will evaluate this file each time a new window is opened. It is run +# after packages are loaded/activated and after the previous editor state +# has been restored. +# +# An example hack to make opened Markdown files always be soft wrapped: +# +# path = require 'path' +# +# atom.workspaceView.eachEditorView (editorView) -> +# editor = editorView.getEditor() +# if path.extname(editor.getPath()) is '.md' +# editor.setSoftWrap(true) diff --git a/dot-atom/keymap.cson b/dot-atom/keymap.cson index 872395168..bce80b757 100644 --- a/dot-atom/keymap.cson +++ b/dot-atom/keymap.cson @@ -1,9 +1,13 @@ -# User keymap +# Your keymap # # Atom keymaps work similarly to stylesheets. Just as stylesheets use selectors # to apply styles to elements, Atom keymaps use selectors to associate -# keystrokes with events in specific contexts. Here's a small example, excerpted -# from Atom's built-in keymaps: +# keystrokes with events in specific contexts. +# +# You can create a new keybinding in this file by typing "key" and then hitting +# tab. +# +# Here's an example taken from Atom's built-in keymap: # # '.editor': # 'enter': 'editor:newline' @@ -11,3 +15,4 @@ # 'body': # 'ctrl-P': 'core:move-up' # 'ctrl-p': 'core:move-down' +# diff --git a/dot-atom/snippets.cson b/dot-atom/snippets.cson index e9d644de1..957899361 100644 --- a/dot-atom/snippets.cson +++ b/dot-atom/snippets.cson @@ -3,7 +3,7 @@ # Atom snippets allow you to enter a simple prefix in the editor and hit tab to # expand the prefix into a larger code block with templated values. # -# You can create a new snippet in this file by typing `snip` and then hitting +# You can create a new snippet in this file by typing "snip" and then hitting # tab. # # An example CoffeeScript snippet to expand log to console.log: diff --git a/dot-atom/user.coffee b/dot-atom/user.coffee deleted file mode 100644 index 60880641d..000000000 --- a/dot-atom/user.coffee +++ /dev/null @@ -1 +0,0 @@ -# For more on how to configure atom open `~/github/atom/docs/configuring-and-extending.md` diff --git a/exports/atom.coffee b/exports/atom.coffee index f4fd1053a..ca48423f8 100644 --- a/exports/atom.coffee +++ b/exports/atom.coffee @@ -4,7 +4,6 @@ module.exports = _: require 'underscore-plus' BufferedNodeProcess: require '../src/buffered-node-process' BufferedProcess: require '../src/buffered-process' - ConfigObserver: require '../src/config-observer' Directory: require '../src/directory' File: require '../src/file' fs: require 'fs-plus' diff --git a/keymaps/darwin.cson b/keymaps/darwin.cson index a112bfbf7..750a1468f 100644 --- a/keymaps/darwin.cson +++ b/keymaps/darwin.cson @@ -68,6 +68,8 @@ 'cmd-=': 'window:increase-font-size' 'cmd-+': 'window:increase-font-size' 'cmd--': 'window:decrease-font-size' + 'cmd-_': 'window:decrease-font-size' + 'cmd-0': 'window:reset-font-size' 'cmd-k up': 'pane:split-up' # Atom Specific 'cmd-k down': 'pane:split-down' # Atom Specific @@ -75,8 +77,12 @@ 'cmd-k right': 'pane:split-right' # Atom Specific 'cmd-k cmd-w': 'pane:close' # Atom Specific 'cmd-k alt-cmd-w': 'pane:close-other-items' # Atom Specific - 'cmd-k cmd-left': 'window:focus-previous-pane' - 'cmd-k cmd-right': 'window:focus-next-pane' + 'cmd-k cmd-p': 'window:focus-previous-pane' + 'cmd-k cmd-n': 'window:focus-next-pane' + 'cmd-k cmd-up': 'window:focus-pane-above' + 'cmd-k cmd-down': 'window:focus-pane-below' + 'cmd-k cmd-left': 'window:focus-pane-on-left' + 'cmd-k cmd-right': 'window:focus-pane-on-right' 'cmd-1': 'pane:show-item-1' 'cmd-2': 'pane:show-item-2' 'cmd-3': 'pane:show-item-3' @@ -118,7 +124,6 @@ 'alt-cmd-z': 'editor:checkout-head-revision' 'cmd-<': 'editor:scroll-to-cursor' 'alt-cmd-ctrl-f': 'editor:fold-selection' - 'cmd-=': 'editor:auto-indent' # Sublime Parity 'cmd-enter': 'editor:newline-below' diff --git a/keymaps/win32.cson b/keymaps/win32.cson index 3b6a230a5..ca306414e 100644 --- a/keymaps/win32.cson +++ b/keymaps/win32.cson @@ -40,6 +40,8 @@ 'ctrl-=': 'window:increase-font-size' 'ctrl-+': 'window:increase-font-size' 'ctrl--': 'window:decrease-font-size' + 'ctrl-_': 'window:decrease-font-size' + 'ctrl-0': 'window:reset-font-size' 'ctrl-k up': 'pane:split-up' # Atom Specific 'ctrl-k down': 'pane:split-down' # Atom Specific @@ -47,8 +49,12 @@ 'ctrl-k right': 'pane:split-right' # Atom Specific 'ctrl-k ctrl-w': 'pane:close' # Atom Specific 'ctrl-k alt-ctrl-w': 'pane:close-other-items' # Atom Specific - 'ctrl-k ctrl-left': 'window:focus-previous-pane' - 'ctrl-k ctrl-right': 'window:focus-next-pane' + 'ctrl-k ctrl-p': 'window:focus-previous-pane' + 'ctrl-k ctrl-n': 'window:focus-next-pane' + 'ctrl-k ctrl-up': 'window:focus-pane-above' + 'ctrl-k ctrl-down': 'window:focus-pane-below' + 'ctrl-k ctrl-left': 'window:focus-pane-on-left' + 'ctrl-k ctrl-right': 'window:focus-pane-on-right' '.workspace .editor': # Windows specific @@ -65,7 +71,6 @@ 'alt-ctrl-z': 'editor:checkout-head-revision' 'ctrl-<': 'editor:scroll-to-cursor' 'alt-ctrl-f': 'editor:fold-selection' - 'ctrl-=': 'editor:auto-indent' # Sublime Parity 'ctrl-enter': 'editor:newline-below' diff --git a/menus/darwin.cson b/menus/darwin.cson index 44e88dead..27f4b1a03 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -4,10 +4,12 @@ submenu: [ { label: 'About Atom', command: 'application:about' } { label: "VERSION", enabled: false } - { label: "Install update", command: 'application:install-update', visible: false } + { label: "Restart and Install Update", command: 'application:install-update', visible: false} + { label: "Check for Update", command: 'application:check-for-update', visible: false} { type: 'separator' } { label: 'Preferences...', command: 'application:show-settings' } { label: 'Open Your Config', command: 'application:open-your-config' } + { label: 'Open Your Init Script', command: 'application:open-your-init-script' } { label: 'Open Your Keymap', command: 'application:open-your-keymap' } { label: 'Open Your Snippets', command: 'application:open-your-snippets' } { label: 'Open Your Stylesheet', command: 'application:open-your-stylesheet' } diff --git a/package.json b/package.json index 178f4a20e..5060d8d00 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "0.49.0", + "version": "0.51.0", "main": "./src/browser/main.js", "repository": { "type": "git", @@ -16,7 +16,7 @@ "url": "http://github.com/atom/atom/raw/master/LICENSE.md" } ], - "atomShellVersion": "0.8.7", + "atomShellVersion": "0.9.2", "dependencies": { "async": "0.2.6", "bootstrap": "git://github.com/atom/bootstrap.git#6af81906189f1747fd6c93479e3d998ebe041372", @@ -25,11 +25,11 @@ "coffeestack": "0.7.0", "delegato": "1.x", "emissary": "1.x", - "first-mate": ">=1.1 <2.0", + "first-mate": ">=1.1.4 <2.0", "fs-plus": "1.x", "fstream": "0.1.24", "fuzzaldrin": "1.x", - "git-utils": "0.34.0", + "git-utils": "1.x", "guid": "0.0.10", "jasmine-tagged": "1.x", "mkdirp": "0.3.5", @@ -42,102 +42,103 @@ "pathwatcher": "0.14.2", "pegjs": "0.8.0", "property-accessors": "1.x", - "q": "0.9.7", - "scandal": "0.13.0", + "q": "1.0.x", + "runas": "0.5.x", + "scandal": "0.14.0", "season": "1.x", "semver": "1.1.4", "serializable": "1.x", "space-pen": "3.1.1", "temp": "0.5.0", - "text-buffer": "0.16.0", + "text-buffer": "1.x", "theorist": "1.x", "underscore-plus": "1.x", "vm-compatibility-layer": "0.1.0" }, "packageDependencies": { - "atom-dark-syntax": "0.12.0", - "atom-dark-ui": "0.21.0", - "atom-light-syntax": "0.12.0", - "atom-light-ui": "0.20.0", - "base16-tomorrow-dark-theme": "0.10.0", - "solarized-dark-syntax": "0.8.0", - "solarized-light-syntax": "0.4.0", - "archive-view": "0.21.0", - "autocomplete": "0.21.0", - "autoflow": "0.12.0", - "autosave": "0.10.0", - "background-tips": "0.5.0", - "bookmarks": "0.18.0", - "bracket-matcher": "0.19.0", - "command-logger": "0.10.0", - "command-palette": "0.15.0", - "dev-live-reload": "0.23.0", - "editor-stats": "0.12.0", + "atom-dark-syntax": "0.13.0", + "atom-dark-ui": "0.22.0", + "atom-light-syntax": "0.13.0", + "atom-light-ui": "0.21.0", + "base16-tomorrow-dark-theme": "0.11.0", + "solarized-dark-syntax": "0.9.0", + "solarized-light-syntax": "0.5.0", + "archive-view": "0.22.0", + "autocomplete": "0.22.0", + "autoflow": "0.14.0", + "autosave": "0.11.0", + "background-tips": "0.7.0", + "bookmarks": "0.19.0", + "bracket-matcher": "0.20.0", + "command-logger": "0.11.0", + "command-palette": "0.16.0", + "dev-live-reload": "0.24.0", + "editor-stats": "0.13.0", "exception-reporting": "0.13.0", - "feedback": "0.22.0", - "find-and-replace": "0.81.0", - "fuzzy-finder": "0.32.0", - "gists": "0.15.0", - "git-diff": "0.23.0", - "github-sign-in": "0.18.0", + "feedback": "0.23.0", + "find-and-replace": "0.83.0", + "fuzzy-finder": "0.34.0", + "gists": "0.17.0", + "git-diff": "0.24.0", + "github-sign-in": "0.19.0", "go-to-line": "0.16.0", - "grammar-selector": "0.18.0", - "image-view": "0.17.0", - "keybinding-resolver": "0.9.0", - "link": "0.15.0", - "markdown-preview": "0.25.1", - "metrics": "0.24.0", - "package-generator": "0.25.0", - "release-notes": "0.17.0", - "settings-view": "0.63.0", - "snippets": "0.24.0", - "spell-check": "0.21.0", + "grammar-selector": "0.19.0", + "image-view": "0.23.0", + "keybinding-resolver": "0.10.0", + "link": "0.17.0", + "markdown-preview": "0.29.0", + "metrics": "0.26.0", + "package-generator": "0.26.0", + "release-notes": "0.20.0", + "settings-view": "0.72.0", + "snippets": "0.27.0", + "spell-check": "0.24.0", "status-bar": "0.32.0", - "styleguide": "0.22.0", - "symbols-view": "0.30.0", - "tabs": "0.18.0", + "styleguide": "0.23.0", + "symbols-view": "0.33.0", + "tabs": "0.19.0", "terminal": "0.27.0", "timecop": "0.13.0", - "to-the-hubs": "0.18.0", - "tree-view": "0.65.0", - "update-package-dependencies": "0.2.0", - "visual-bell": "0.6.0", + "to-the-hubs": "0.19.0", + "tree-view": "0.69.0", + "update-package-dependencies": "0.3.0", + "visual-bell": "0.7.0", "welcome": "0.4.0", - "whitespace": "0.10.0", - "wrap-guide": "0.12.0", - "language-c": "0.2.0", + "whitespace": "0.12.0", + "wrap-guide": "0.14.0", + "language-c": "0.4.0", "language-clojure": "0.1.0", - "language-coffee-script": "0.6.0", - "language-css": "0.2.0", - "language-gfm": "0.12.0", - "language-git": "0.3.0", - "language-go": "0.2.0", - "language-html": "0.2.0", + "language-coffee-script": "0.7.0", + "language-css": "0.3.0", + "language-gfm": "0.16.0", + "language-git": "0.4.0", + "language-go": "0.3.0", + "language-html": "0.3.0", "language-hyperlink": "0.3.0", - "language-java": "0.2.0", - "language-javascript": "0.5.0", - "language-json": "0.2.0", - "language-less": "0.1.0", - "language-make": "0.1.0", - "language-mustache": "0.1.0", - "language-objective-c": "0.2.0", - "language-pegjs": "0.1.0", - "language-perl": "0.2.0", - "language-php": "0.3.0", - "language-property-list": "0.2.0", - "language-puppet": "0.2.0", - "language-python": "0.2.0", - "language-ruby": "0.8.0", - "language-ruby-on-rails": "0.4.0", - "language-sass": "0.3.0", - "language-shellscript": "0.2.0", - "language-source": "0.2.0", - "language-sql": "0.2.0", - "language-text": "0.2.0", - "language-todo": "0.2.0", - "language-toml": "0.7.0", - "language-xml": "0.2.0", - "language-yaml": "0.1.0" + "language-java": "0.3.0", + "language-javascript": "0.6.0", + "language-json": "0.3.0", + "language-less": "0.2.0", + "language-make": "0.2.0", + "language-mustache": "0.2.0", + "language-objective-c": "0.3.0", + "language-pegjs": "0.2.0", + "language-perl": "0.3.0", + "language-php": "0.4.0", + "language-property-list": "0.3.0", + "language-puppet": "0.3.0", + "language-python": "0.3.0", + "language-ruby": "0.9.0", + "language-ruby-on-rails": "0.5.0", + "language-sass": "0.4.0", + "language-shellscript": "0.3.0", + "language-source": "0.3.0", + "language-sql": "0.3.0", + "language-text": "0.3.0", + "language-todo": "0.3.0", + "language-toml": "0.8.0", + "language-xml": "0.3.0", + "language-yaml": "0.2.0" }, "private": true, "scripts": { diff --git a/resources/win/atom.ico b/resources/win/atom.ico index 2cf161e68..446140277 100644 Binary files a/resources/win/atom.ico and b/resources/win/atom.ico differ diff --git a/script/utils/update-octicons b/script/utils/update-octicons deleted file mode 100755 index b66402d88..000000000 --- a/script/utils/update-octicons +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env coffee - -usage = """ -Usage: - update-octicons PATH-TO-OCTICONS -""" - -path = require 'path' -fs = require 'fs' -YAML = require 'js-yaml' - -scriptPath = process.argv[1] -pathToOcticons = process.argv[2] ? path.join(process.env.HOME, 'github', 'octicons') -atomDir = path.resolve(scriptPath, "../../..") - -unless fs.existsSync(pathToOcticons) - console.error(usage) - process.exit(1) - -# Copy font-file -fontSrc = path.join(pathToOcticons, 'octicons', 'octicons.woff') -fontDest = path.join(atomDir, 'static', 'octicons.woff') -fs.createReadStream(fontSrc).pipe(fs.createWriteStream(fontDest)) - -# Update Octicon UTF codes -glyphsSrc = path.join(pathToOcticons, 'data', 'glyphs.yml') -octiconUtfDest = path.join atomDir, 'static', 'variables', 'octicon-utf-codes.less' -output = [] -for {css, code} in YAML.load(fs.readFileSync(glyphsSrc).toString()) - output.push "@#{css}: \"\\#{code}\";" - -fs.writeFileSync octiconUtfDest, "#{output.join('\n')}\n" diff --git a/spec/atom-reporter.coffee b/spec/atom-reporter.coffee index c9b5e1bd2..aca1da3ff 100644 --- a/spec/atom-reporter.coffee +++ b/spec/atom-reporter.coffee @@ -1,35 +1,49 @@ -{View, $, $$} = require '../src/space-pen-extensions' +path = require 'path' _ = require 'underscore-plus' {convertStackTrace} = require 'coffeestack' +{View, $, $$} = require '../src/space-pen-extensions' sourceMaps = {} -formatStackTrace = (stackTrace) -> +formatStackTrace = (message='', stackTrace) -> return stackTrace unless stackTrace - jasminePath = require.resolve('../vendor/jasmine') - jasminePattern = new RegExp("\\(#{_.escapeRegExp(jasminePath)}:\\d+:\\d+\\)\\s*$") + jasminePattern = /^\s*at\s+.*\(?.*\/jasmine(-[^\/]*)?\.js:\d+:\d+\)?\s*$/ + firstJasmineLinePattern = /^\s*at \/.*\/jasmine(-[^\/]*)?\.js:\d+:\d+\)?\s*$/ convertedLines = [] for line in stackTrace.split('\n') convertedLines.push(line) unless jasminePattern.test(line) + break if firstJasmineLinePattern.test(line) - convertStackTrace(convertedLines.join('\n'), sourceMaps) + stackTrace = convertStackTrace(convertedLines.join('\n'), sourceMaps) + lines = stackTrace.split('\n') + + # Remove first line of stack when it is the same as the error message + errorMatch = lines[0]?.match(/^Error: (.*)/) + lines.shift() if message.trim() is errorMatch?[1]?.trim() + + # Remove prefix of lines matching: at [object Object]. (path:1:2) + for line, index in lines + prefixMatch = line.match(/at \[object Object\]\. \(([^\)]+)\)/) + lines[index] = "at #{prefixMatch[1]}" if prefixMatch + + lines = lines.map (line) -> line.trim() + lines.join('\n') module.exports = class AtomReporter extends View @content: -> - @div id: 'HTMLReporter', class: 'jasmine_reporter', => - @div outlet: 'specPopup', class: "spec-popup" + @div class: 'spec-reporter', => @div outlet: "suites" - @div outlet: 'coreArea', => - @div outlet: 'coreHeader', class: 'symbolHeader' - @ul outlet: 'coreSummary', class: 'symbolSummary list-unstyled' - @div outlet: 'bundledArea', => - @div outlet: 'bundledHeader', class: 'symbolHeader' - @ul outlet: 'bundledSummary', class: 'symbolSummary list-unstyled' - @div outlet: 'userArea', => - @div outlet: 'userHeader', class: 'symbolHeader' - @ul outlet: 'userSummary', class: 'symbolSummary list-unstyled' - @div outlet: "status", class: 'status', => + @div outlet: 'coreArea', class: 'symbol-area', => + @div outlet: 'coreHeader', class: 'symbol-header' + @ul outlet: 'coreSummary', class: 'symbol-summary list-unstyled' + @div outlet: 'bundledArea', class: 'symbol-area', => + @div outlet: 'bundledHeader', class: 'symbol-header' + @ul outlet: 'bundledSummary', class: 'symbol-summary list-unstyled' + @div outlet: 'userArea', class: 'symbol-area', => + @div outlet: 'userHeader', class: 'symbol-header' + @ul outlet: 'userSummary', class: 'symbol-summary list-unstyled' + @div outlet: "status", class: 'status alert alert-info', => @div outlet: "time", class: 'time' @div outlet: "specCount", class: 'spec-count' @div outlet: "message", class: 'message' @@ -46,7 +60,7 @@ class AtomReporter extends View reportRunnerStarting: (runner) -> @handleEvents() - @startedAt = new Date() + @startedAt = Date.now() specs = runner.specs() @totalSpecCount = specs.length @addSpecs(specs) @@ -54,57 +68,29 @@ class AtomReporter extends View reportRunnerResults: (runner) -> @updateSpecCounts() - if @failedCount == 0 - @message.text "Success!" + @status.addClass('alert-success').removeClass('alert-info') if @failedCount is 0 + if @failedCount is 1 + @message.text "#{@failedCount} failure" else - @message.text "Game Over" + @message.text "#{@failedCount} failures" reportSuiteResults: (suite) -> reportSpecResults: (spec) -> @completeSpecCount++ - spec.endedAt = new Date().getTime() + spec.endedAt = Date.now() @specComplete(spec) @updateStatusView(spec) reportSpecStarting: (spec) -> @specStarted(spec) - specFilter: (spec) -> - globalFocusPriority = jasmine.getEnv().focusPriority - parent = spec.parentSuite ? spec.suite - - if !globalFocusPriority - true - else if spec.focusPriority >= globalFocusPriority - true - else if not parent - false - else - @specFilter(parent) - handleEvents: -> - $(document).on "mouseover", ".spec-summary", ({currentTarget}) => - element = $(currentTarget) - description = element.data("description") - return unless description - - clearTimeout @timeoutId if @timeoutId? - @specPopup.show() - spec = _.find(window.timedSpecs, ({fullName}) -> description is fullName) - description = "#{description} #{spec.time}ms" if spec - @specPopup.text description - {left, top} = element.offset() - left += 20 - top += 20 - @specPopup.offset({left, top}) - @timeoutId = setTimeout((=> @specPopup.hide()), 3000) - $(document).on "click", ".spec-toggle", ({currentTarget}) => element = $(currentTarget) specFailures = element.parent().find('.spec-failures') specFailures.toggle() - if specFailures.is(":visible") then element.text "\uf03d" else element.html "\uf03f" + element.toggleClass('folded') false updateSpecCounts: -> @@ -116,7 +102,7 @@ class AtomReporter extends View updateStatusView: (spec) -> if @failedCount > 0 - @status.addClass('failed') unless @status.hasClass('failed') + @status.addClass('alert-danger').removeClass('alert-info') @updateSpecCounts() @@ -124,7 +110,7 @@ class AtomReporter extends View rootSuite = rootSuite.parentSuite while rootSuite.parentSuite @message.text rootSuite.description - time = "#{Math.round((spec.endedAt - @startedAt.getTime()) / 10)}" + time = "#{Math.round((spec.endedAt - @startedAt) / 10)}" time = "0#{time}" if time.length < 3 @time[0].textContent = "#{time[0...-2]}.#{time[-2..]}s" @@ -146,15 +132,22 @@ class AtomReporter extends View @userSummary.append symbol if coreSpecs > 0 - @coreHeader.text("Core Specs (#{coreSpecs}):") + @coreHeader.text("Core Specs (#{coreSpecs})") else @coreArea.hide() if bundledPackageSpecs > 0 - @bundledHeader.text("Bundled Package Specs (#{bundledPackageSpecs}):") + @bundledHeader.text("Bundled Package Specs (#{bundledPackageSpecs})") else @bundledArea.hide() if userPackageSpecs > 0 - @userHeader.text("User Package Specs (#{userPackageSpecs}):") + if coreSpecs is 0 and bundledPackageSpecs is 0 + # Package specs being run, show a more descriptive label + {specDirectory} = specs[0] + packageFolderName = path.basename(path.dirname(specDirectory)) + packageName = _.undasherize(_.uncamelcase(packageFolderName)) + @userHeader.text("#{packageName} Specs") + else + @userHeader.text("User Package Specs (#{userPackageSpecs})") else @userArea.hide() @@ -164,7 +157,7 @@ class AtomReporter extends View specComplete: (spec) -> specSummaryElement = $("#spec-summary-#{spec.id}") specSummaryElement.removeClass('pending') - specSummaryElement.data("description", spec.getFullName()) + specSummaryElement.setTooltip(title: spec.getFullName(), container: '.spec-reporter') results = spec.results() if results.skipped @@ -185,11 +178,9 @@ class SuiteResultView extends View @div class: 'suite', => @div outlet: 'description', class: 'description' - suite: null - initialize: (@suite) -> @attr('id', "suite-view-#{@suite.id}") - @description.html @suite.description + @description.text(@suite.description) attach: -> (@parentSuiteView() or $('.results')).append this @@ -206,20 +197,22 @@ class SuiteResultView extends View class SpecResultView extends View @content: -> @div class: 'spec', => - @div "\uf03d", class: 'spec-toggle' + @div class: 'spec-toggle' @div outlet: 'description', class: 'description' @div outlet: 'specFailures', class: 'spec-failures' - spec: null initialize: (@spec) -> @addClass("spec-view-#{@spec.id}") - @description.html @spec.description + + description = @spec.description + description = "it #{description}" if description.indexOf('it ') isnt 0 + @description.text(description) for result in @spec.results().getItems() when not result.passed() - stackTrace = formatStackTrace(result.trace.stack) + stackTrace = formatStackTrace(result.message, result.trace.stack) @specFailures.append $$ -> - @div result.message, class: 'resultMessage fail' - @div stackTrace, class: 'stackTrace' if stackTrace + @div result.message, class: 'result-message fail' + @pre stackTrace, class: 'stack-trace padded' if stackTrace attach: -> @parentSuiteView().append this diff --git a/spec/atom-spec.coffee b/spec/atom-spec.coffee index 24b646b95..038318b07 100644 --- a/spec/atom-spec.coffee +++ b/spec/atom-spec.coffee @@ -1,6 +1,7 @@ {$, $$, fs, WorkspaceView} = require 'atom' Exec = require('child_process').exec path = require 'path' +AtomPackage = require '../src/atom-package' ThemeManager = require '../src/theme-manager' describe "the `atom` global", -> @@ -9,35 +10,28 @@ describe "the `atom` global", -> describe "package lifecycle methods", -> describe ".loadPackage(name)", -> - describe "when the package has deferred deserializers", -> - it "requires the package's main module if one of its deferred deserializers is referenced", -> - pack = atom.packages.loadPackage('package-with-activation-events') - spyOn(pack, 'activateStylesheets').andCallThrough() - expect(pack.mainModule).toBeNull() - object = atom.deserializers.deserialize({deserializer: 'Foo', data: 5}) - expect(pack.mainModule).toBeDefined() - expect(object.constructor.name).toBe 'Foo' - expect(object.data).toBe 5 - expect(pack.activateStylesheets).toHaveBeenCalled() + it "continues if the package has an invalid package.json", -> + spyOn(console, 'warn') + atom.config.set("core.disabledPackages", []) + expect(-> atom.packages.loadPackage("package-with-broken-package-json")).not.toThrow() - it "continues if the package has an invalid package.json", -> - spyOn(console, 'warn') - atom.config.set("core.disabledPackages", []) - expect(-> atom.packages.loadPackage("package-with-broken-package-json")).not.toThrow() - - it "continues if the package has an invalid keymap", -> - atom.config.set("core.disabledPackages", []) - expect(-> atom.packages.loadPackage("package-with-broken-keymap")).not.toThrow() + it "continues if the package has an invalid keymap", -> + atom.config.set("core.disabledPackages", []) + expect(-> atom.packages.loadPackage("package-with-broken-keymap")).not.toThrow() describe ".unloadPackage(name)", -> describe "when the package is active", -> it "throws an error", -> - pack = atom.packages.activatePackage('package-with-main') - expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() - expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() - expect( -> atom.packages.unloadPackage(pack.name)).toThrow() - expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() - expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() + pack = null + waitsForPromise -> + atom.packages.activatePackage('package-with-main').then (p) -> pack = p + + runs -> + expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() + expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() + expect( -> atom.packages.unloadPackage(pack.name)).toThrow() + expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() + expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() describe "when the package is not loaded", -> it "throws an error", -> @@ -54,22 +48,42 @@ describe "the `atom` global", -> describe ".activatePackage(id)", -> describe "atom packages", -> + describe "when called multiple times", -> + it "it only calls activate on the package once", -> + spyOn(AtomPackage.prototype, 'activateNow').andCallThrough() + atom.packages.activatePackage('package-with-index') + atom.packages.activatePackage('package-with-index') + + waitsForPromise -> + atom.packages.activatePackage('package-with-index') + + runs -> + expect(AtomPackage.prototype.activateNow.callCount).toBe 1 + describe "when the package has a main module", -> describe "when the metadata specifies a main module path˜", -> it "requires the module at the specified path", -> mainModule = require('./fixtures/packages/package-with-main/main-module') spyOn(mainModule, 'activate') - pack = atom.packages.activatePackage('package-with-main') - expect(mainModule.activate).toHaveBeenCalled() - expect(pack.mainModule).toBe mainModule + pack = null + waitsForPromise -> + atom.packages.activatePackage('package-with-main').then (p) -> pack = p + + runs -> + expect(mainModule.activate).toHaveBeenCalled() + expect(pack.mainModule).toBe mainModule describe "when the metadata does not specify a main module", -> it "requires index.coffee", -> indexModule = require('./fixtures/packages/package-with-index/index') spyOn(indexModule, 'activate') - pack = atom.packages.activatePackage('package-with-index') - expect(indexModule.activate).toHaveBeenCalled() - expect(pack.mainModule).toBe indexModule + pack = null + waitsForPromise -> + atom.packages.activatePackage('package-with-index').then (p) -> pack = p + + runs -> + expect(indexModule.activate).toHaveBeenCalled() + expect(pack.mainModule).toBe indexModule it "assigns config defaults from the module", -> expect(atom.config.get('package-with-config-defaults.numbers.one')).toBeUndefined() @@ -78,20 +92,22 @@ describe "the `atom` global", -> expect(atom.config.get('package-with-config-defaults.numbers.two')).toBe 2 describe "when the package metadata includes activation events", -> - [mainModule, pack] = [] + [mainModule, promise] = [] beforeEach -> mainModule = require './fixtures/packages/package-with-activation-events/index' spyOn(mainModule, 'activate').andCallThrough() AtomPackage = require '../src/atom-package' spyOn(AtomPackage.prototype, 'requireMainModule').andCallThrough() - pack = atom.packages.activatePackage('package-with-activation-events') + + promise = atom.packages.activatePackage('package-with-activation-events') it "defers requiring/activating the main module until an activation event bubbles to the root view", -> - expect(pack.requireMainModule).not.toHaveBeenCalled() - expect(mainModule.activate).not.toHaveBeenCalled() + expect(promise.isFulfilled()).not.toBeTruthy() atom.workspaceView.trigger 'activation-event' - expect(mainModule.activate).toHaveBeenCalled() + + waitsForPromise -> + promise it "triggers the activation event on all handlers registered during activation", -> atom.workspaceView.openSync() @@ -116,13 +132,17 @@ describe "the `atom` global", -> expect(console.warn).not.toHaveBeenCalled() it "passes the activate method the package's previously serialized state if it exists", -> - pack = atom.packages.activatePackage("package-with-serialization") - expect(pack.mainModule.someNumber).not.toBe 77 - pack.mainModule.someNumber = 77 - atom.packages.deactivatePackage("package-with-serialization") - spyOn(pack.mainModule, 'activate').andCallThrough() - atom.packages.activatePackage("package-with-serialization") - expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77}) + pack = null + waitsForPromise -> + atom.packages.activatePackage("package-with-serialization").then (p) -> pack = p + + runs -> + expect(pack.mainModule.someNumber).not.toBe 77 + pack.mainModule.someNumber = 77 + atom.packages.deactivatePackage("package-with-serialization") + spyOn(pack.mainModule, 'activate').andCallThrough() + atom.packages.activatePackage("package-with-serialization") + expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77}) it "logs warning instead of throwing an exception if the package fails to load", -> atom.config.set("core.disabledPackages", []) @@ -245,29 +265,38 @@ describe "the `atom` global", -> describe "scoped-property loading", -> it "loads the scoped properties", -> - atom.packages.activatePackage("package-with-scoped-properties") - expect(atom.syntax.getProperty ['.source.omg'], 'editor.increaseIndentPattern').toBe '^a' + waitsForPromise -> + atom.packages.activatePackage("package-with-scoped-properties") + + runs -> + expect(atom.syntax.getProperty ['.source.omg'], 'editor.increaseIndentPattern').toBe '^a' describe "textmate packages", -> it "loads the package's grammars", -> expect(atom.syntax.selectGrammar("file.rb").name).toBe "Null Grammar" - atom.packages.activatePackage('language-ruby', sync: true) - expect(atom.syntax.selectGrammar("file.rb").name).toBe "Ruby" + + waitsForPromise -> + atom.packages.activatePackage('language-ruby') + + runs -> + expect(atom.syntax.selectGrammar("file.rb").name).toBe "Ruby" it "translates the package's scoped properties to Atom terms", -> expect(atom.syntax.getProperty(['.source.ruby'], 'editor.commentStart')).toBeUndefined() - atom.packages.activatePackage('language-ruby', sync: true) - expect(atom.syntax.getProperty(['.source.ruby'], 'editor.commentStart')).toBe '# ' + + waitsForPromise -> + atom.packages.activatePackage('language-ruby') + + runs -> + expect(atom.syntax.getProperty(['.source.ruby'], 'editor.commentStart')).toBe '# ' describe "when the package has no grammars but does have preferences", -> it "loads the package's preferences as scoped properties", -> jasmine.unspy(window, 'setTimeout') spyOn(atom.syntax, 'addProperties').andCallThrough() - atom.packages.activatePackage('package-with-preferences-tmbundle') - - waitsFor -> - atom.syntax.addProperties.callCount > 0 + waitsForPromise -> + atom.packages.activatePackage('package-with-preferences-tmbundle') runs -> expect(atom.syntax.getProperty(['.source.pref'], 'editor.increaseIndentPattern')).toBe '^abc$' @@ -275,80 +304,118 @@ describe "the `atom` global", -> describe ".deactivatePackage(id)", -> describe "atom packages", -> it "calls `deactivate` on the package's main module if activate was successful", -> - pack = atom.packages.activatePackage("package-with-deactivate") - expect(atom.packages.isPackageActive("package-with-deactivate")).toBeTruthy() - spyOn(pack.mainModule, 'deactivate').andCallThrough() + pack = null + waitsForPromise -> + atom.packages.activatePackage("package-with-deactivate").then (p) -> pack = p - atom.packages.deactivatePackage("package-with-deactivate") - expect(pack.mainModule.deactivate).toHaveBeenCalled() - expect(atom.packages.isPackageActive("package-with-module")).toBeFalsy() + runs -> + expect(atom.packages.isPackageActive("package-with-deactivate")).toBeTruthy() + spyOn(pack.mainModule, 'deactivate').andCallThrough() - spyOn(console, 'warn') - badPack = atom.packages.activatePackage("package-that-throws-on-activate") - expect(atom.packages.isPackageActive("package-that-throws-on-activate")).toBeTruthy() - spyOn(badPack.mainModule, 'deactivate').andCallThrough() + atom.packages.deactivatePackage("package-with-deactivate") + expect(pack.mainModule.deactivate).toHaveBeenCalled() + expect(atom.packages.isPackageActive("package-with-module")).toBeFalsy() - atom.packages.deactivatePackage("package-that-throws-on-activate") - expect(badPack.mainModule.deactivate).not.toHaveBeenCalled() - expect(atom.packages.isPackageActive("package-that-throws-on-activate")).toBeFalsy() + spyOn(console, 'warn') + + badPack = null + waitsForPromise -> + atom.packages.activatePackage("package-that-throws-on-activate").then (p) -> badPack = p + + runs -> + expect(atom.packages.isPackageActive("package-that-throws-on-activate")).toBeTruthy() + spyOn(badPack.mainModule, 'deactivate').andCallThrough() + + atom.packages.deactivatePackage("package-that-throws-on-activate") + expect(badPack.mainModule.deactivate).not.toHaveBeenCalled() + expect(atom.packages.isPackageActive("package-that-throws-on-activate")).toBeFalsy() it "does not serialize packages that have not been activated called on their main module", -> spyOn(console, 'warn') - badPack = atom.packages.activatePackage("package-that-throws-on-activate") - spyOn(badPack.mainModule, 'serialize').andCallThrough() + badPack = null + waitsForPromise -> + atom.packages.activatePackage("package-that-throws-on-activate").then (p) -> badPack = p - atom.packages.deactivatePackage("package-that-throws-on-activate") - expect(badPack.mainModule.serialize).not.toHaveBeenCalled() + runs -> + spyOn(badPack.mainModule, 'serialize').andCallThrough() + + atom.packages.deactivatePackage("package-that-throws-on-activate") + expect(badPack.mainModule.serialize).not.toHaveBeenCalled() it "absorbs exceptions that are thrown by the package module's serialize methods", -> spyOn(console, 'error') - atom.packages.activatePackage('package-with-serialize-error', immediate: true) - atom.packages.activatePackage('package-with-serialization', immediate: true) - atom.packages.deactivatePackages() - expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined() - expect(atom.packages.packageStates['package-with-serialization']).toEqual someNumber: 1 - expect(console.error).toHaveBeenCalled() + + waitsForPromise -> + atom.packages.activatePackage('package-with-serialize-error') + + waitsForPromise -> + atom.packages.activatePackage('package-with-serialization') + + runs -> + atom.packages.deactivatePackages() + expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined() + expect(atom.packages.packageStates['package-with-serialization']).toEqual someNumber: 1 + expect(console.error).toHaveBeenCalled() it "removes the package's grammars", -> - atom.packages.activatePackage('package-with-grammars') - atom.packages.deactivatePackage('package-with-grammars') - expect(atom.syntax.selectGrammar('a.alot').name).toBe 'Null Grammar' - expect(atom.syntax.selectGrammar('a.alittle').name).toBe 'Null Grammar' + waitsForPromise -> + atom.packages.activatePackage('package-with-grammars') + + runs -> + atom.packages.deactivatePackage('package-with-grammars') + expect(atom.syntax.selectGrammar('a.alot').name).toBe 'Null Grammar' + expect(atom.syntax.selectGrammar('a.alittle').name).toBe 'Null Grammar' it "removes the package's keymaps", -> - atom.packages.activatePackage('package-with-keymaps') - atom.packages.deactivatePackage('package-with-keymaps') - expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', $$ -> @div class: 'test-1')).toHaveLength 0 - expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', $$ -> @div class: 'test-2')).toHaveLength 0 + waitsForPromise -> + atom.packages.activatePackage('package-with-keymaps') + + runs -> + atom.packages.deactivatePackage('package-with-keymaps') + expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', $$ -> @div class: 'test-1')).toHaveLength 0 + expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', $$ -> @div class: 'test-2')).toHaveLength 0 it "removes the package's stylesheets", -> - atom.packages.activatePackage('package-with-stylesheets') - atom.packages.deactivatePackage('package-with-stylesheets') - one = require.resolve("./fixtures/packages/package-with-stylesheets-manifest/stylesheets/1.css") - two = require.resolve("./fixtures/packages/package-with-stylesheets-manifest/stylesheets/2.less") - three = require.resolve("./fixtures/packages/package-with-stylesheets-manifest/stylesheets/3.css") - expect(atom.themes.stylesheetElementForId(one)).not.toExist() - expect(atom.themes.stylesheetElementForId(two)).not.toExist() - expect(atom.themes.stylesheetElementForId(three)).not.toExist() + waitsForPromise -> + atom.packages.activatePackage('package-with-stylesheets') + + runs -> + atom.packages.deactivatePackage('package-with-stylesheets') + one = require.resolve("./fixtures/packages/package-with-stylesheets-manifest/stylesheets/1.css") + two = require.resolve("./fixtures/packages/package-with-stylesheets-manifest/stylesheets/2.less") + three = require.resolve("./fixtures/packages/package-with-stylesheets-manifest/stylesheets/3.css") + expect(atom.themes.stylesheetElementForId(one)).not.toExist() + expect(atom.themes.stylesheetElementForId(two)).not.toExist() + expect(atom.themes.stylesheetElementForId(three)).not.toExist() it "removes the package's scoped-properties", -> - atom.packages.activatePackage("package-with-scoped-properties") - expect(atom.syntax.getProperty ['.source.omg'], 'editor.increaseIndentPattern').toBe '^a' - atom.packages.deactivatePackage("package-with-scoped-properties") - expect(atom.syntax.getProperty ['.source.omg'], 'editor.increaseIndentPattern').toBeUndefined() + waitsForPromise -> + atom.packages.activatePackage("package-with-scoped-properties") + + runs -> + expect(atom.syntax.getProperty ['.source.omg'], 'editor.increaseIndentPattern').toBe '^a' + atom.packages.deactivatePackage("package-with-scoped-properties") + expect(atom.syntax.getProperty ['.source.omg'], 'editor.increaseIndentPattern').toBeUndefined() describe "textmate packages", -> it "removes the package's grammars", -> expect(atom.syntax.selectGrammar("file.rb").name).toBe "Null Grammar" - atom.packages.activatePackage('language-ruby', sync: true) - expect(atom.syntax.selectGrammar("file.rb").name).toBe "Ruby" - atom.packages.deactivatePackage('language-ruby') - expect(atom.syntax.selectGrammar("file.rb").name).toBe "Null Grammar" + + waitsForPromise -> + atom.packages.activatePackage('language-ruby') + + runs -> + expect(atom.syntax.selectGrammar("file.rb").name).toBe "Ruby" + atom.packages.deactivatePackage('language-ruby') + expect(atom.syntax.selectGrammar("file.rb").name).toBe "Null Grammar" it "removes the package's scoped properties", -> - atom.packages.activatePackage('language-ruby', sync: true) - atom.packages.deactivatePackage('language-ruby') - expect(atom.syntax.getProperty(['.source.ruby'], 'editor.commentStart')).toBeUndefined() + waitsForPromise -> + atom.packages.activatePackage('language-ruby') + + runs -> + atom.packages.deactivatePackage('language-ruby') + expect(atom.syntax.getProperty(['.source.ruby'], 'editor.commentStart')).toBeUndefined() describe ".activate()", -> packageActivator = null @@ -382,7 +449,7 @@ describe "the `atom` global", -> themes = themeActivator.mostRecentCall.args[0] expect(['theme']).toContain(theme.getType()) for theme in themes - describe ".en/disablePackage()", -> + describe ".enablePackage() and disablePackage()", -> describe "with packages", -> it ".enablePackage() enables a disabled package", -> packageName = 'package-with-main' @@ -391,28 +458,36 @@ describe "the `atom` global", -> expect(atom.config.get('core.disabledPackages')).toContain packageName pack = atom.packages.enablePackage(packageName) - loadedPackages = atom.packages.getLoadedPackages() - activatedPackages = atom.packages.getActivePackages() - expect(loadedPackages).toContain(pack) - expect(activatedPackages).toContain(pack) - expect(atom.config.get('core.disabledPackages')).not.toContain packageName + activatedPackages = null + waitsFor -> + activatedPackages = atom.packages.getActivePackages() + activatedPackages.length > 0 + + runs -> + expect(loadedPackages).toContain(pack) + expect(activatedPackages).toContain(pack) + expect(atom.config.get('core.disabledPackages')).not.toContain packageName it ".disablePackage() disables an enabled package", -> packageName = 'package-with-main' - atom.packages.activatePackage(packageName) - atom.packages.observeDisabledPackages() - expect(atom.config.get('core.disabledPackages')).not.toContain packageName + waitsForPromise -> + atom.packages.activatePackage(packageName) - pack = atom.packages.disablePackage(packageName) + runs -> + atom.packages.observeDisabledPackages() + expect(atom.config.get('core.disabledPackages')).not.toContain packageName - activatedPackages = atom.packages.getActivePackages() - expect(activatedPackages).not.toContain(pack) - expect(atom.config.get('core.disabledPackages')).toContain packageName + pack = atom.packages.disablePackage(packageName) + + activatedPackages = atom.packages.getActivePackages() + expect(activatedPackages).not.toContain(pack) + expect(atom.config.get('core.disabledPackages')).toContain packageName describe "with themes", -> beforeEach -> - atom.themes.activateThemes() + waitsForPromise -> + atom.themes.activateThemes() afterEach -> atom.themes.deactivateThemes() @@ -426,18 +501,24 @@ describe "the `atom` global", -> # enabling of theme pack = atom.packages.enablePackage(packageName) - activatedPackages = atom.packages.getActivePackages() - expect(activatedPackages).toContain(pack) - expect(atom.config.get('core.themes')).toContain packageName - expect(atom.config.get('core.disabledPackages')).not.toContain packageName - # disabling of theme - pack = atom.packages.disablePackage(packageName) - activatedPackages = atom.packages.getActivePackages() - expect(activatedPackages).not.toContain(pack) - expect(atom.config.get('core.themes')).not.toContain packageName - expect(atom.config.get('core.themes')).not.toContain packageName - expect(atom.config.get('core.disabledPackages')).not.toContain packageName + activatedPackages = null + waitsFor -> + activatedPackages = atom.packages.getActivePackages() + activatedPackages.length > 0 + + runs -> + expect(activatedPackages).toContain(pack) + expect(atom.config.get('core.themes')).toContain packageName + expect(atom.config.get('core.disabledPackages')).not.toContain packageName + + # disabling of theme + pack = atom.packages.disablePackage(packageName) + activatedPackages = atom.packages.getActivePackages() + expect(activatedPackages).not.toContain(pack) + expect(atom.config.get('core.themes')).not.toContain packageName + expect(atom.config.get('core.themes')).not.toContain packageName + expect(atom.config.get('core.disabledPackages')).not.toContain packageName describe ".isReleasedVersion()", -> it "returns false if the version is a SHA and true otherwise", -> diff --git a/spec/clipboard-spec.coffee b/spec/clipboard-spec.coffee new file mode 100644 index 000000000..0553f0eae --- /dev/null +++ b/spec/clipboard-spec.coffee @@ -0,0 +1,12 @@ +describe "Clipboard", -> + describe "write(text, metadata) and read()", -> + it "writes and reads text to/from the native clipboard", -> + expect(atom.clipboard.read()).toBe 'initial clipboard content' + atom.clipboard.write('next') + expect(atom.clipboard.read()).toBe 'next' + + it "returns metadata if the item on the native clipboard matches the last written item", -> + atom.clipboard.write('next', {meta: 'data'}) + expect(atom.clipboard.read()).toBe 'next' + expect(atom.clipboard.readWithMetadata().text).toBe 'next' + expect(atom.clipboard.readWithMetadata().metadata).toEqual {meta: 'data'} diff --git a/spec/command-installer-spec.coffee b/spec/command-installer-spec.coffee index 6bd66b4d3..a597f919d 100644 --- a/spec/command-installer-spec.coffee +++ b/spec/command-installer-spec.coffee @@ -20,7 +20,7 @@ describe "install(commandPath, callback)", -> installDone = false installError = null - installer.install commandFilePath, (error) -> + installer.install commandFilePath, false, (error) -> installDone = true installError = error diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 77455c5c2..02ef6d6d8 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -63,6 +63,19 @@ describe "Config", -> atom.config.toggle('foo.a') expect(atom.config.get('foo.a')).toBe false + describe ".restoreDefault(keyPath)", -> + it "sets the value of the key path to its default", -> + atom.config.setDefaults('a', b: 3) + atom.config.set('a.b', 4) + expect(atom.config.get('a.b')).toBe 4 + atom.config.restoreDefault('a.b') + expect(atom.config.get('a.b')).toBe 3 + + atom.config.set('a.c', 5) + expect(atom.config.get('a.c')).toBe 5 + atom.config.restoreDefault('a.c') + expect(atom.config.get('a.c')).toBeUndefined() + describe ".pushAtKeyPath(keyPath, value)", -> it "pushes the given value to the array at the key path and updates observers", -> atom.config.set("foo.bar.baz", ["a"]) @@ -220,6 +233,8 @@ describe "Config", -> expect(fs.existsSync(path.join(atom.config.configDirPath, 'packages'))).toBeTruthy() expect(fs.isFileSync(path.join(atom.config.configDirPath, 'snippets.cson'))).toBeTruthy() expect(fs.isFileSync(path.join(atom.config.configDirPath, 'config.cson'))).toBeTruthy() + expect(fs.isFileSync(path.join(atom.config.configDirPath, 'init.coffee'))).toBeTruthy() + expect(fs.isFileSync(path.join(atom.config.configDirPath, 'styles.css'))).toBeTruthy() describe ".loadUserConfig()", -> beforeEach -> diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee index e85a05049..6e13ee8f4 100644 --- a/spec/display-buffer-spec.coffee +++ b/spec/display-buffer-spec.coffee @@ -5,12 +5,15 @@ describe "DisplayBuffer", -> [displayBuffer, buffer, changeHandler, tabLength] = [] beforeEach -> tabLength = 2 - atom.packages.activatePackage('language-javascript', sync: true) + buffer = atom.project.bufferForPathSync('sample.js') displayBuffer = new DisplayBuffer({buffer, tabLength}) changeHandler = jasmine.createSpy 'changeHandler' displayBuffer.on 'changed', changeHandler + waitsForPromise -> + atom.packages.activatePackage('language-javascript') + afterEach -> displayBuffer.destroy() buffer.release() diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index ecd6e6d7a..821299f8c 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -16,11 +16,13 @@ describe "Editor", -> describe "with default options", -> beforeEach -> - atom.packages.activatePackage('language-javascript', sync: true) editor = atom.project.openSync('sample.js', autoIndent: false) buffer = editor.buffer lineLengths = buffer.getLines().map (line) -> line.length + waitsForPromise -> + atom.packages.activatePackage('language-javascript') + describe "when the editor is deserialized", -> it "restores selections and folds based on markers in the buffer", -> editor.setSelectedBufferRange([[1, 2], [3, 4]]) @@ -1856,12 +1858,12 @@ describe "Editor", -> expect(editor.getCursorBufferPosition()).toEqual [0, 2] expect(editor.getCursorScreenPosition()).toEqual [0, editor.getTabLength() * 2] - describe "pasteboard operations", -> + describe "clipboard operations", -> beforeEach -> editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) describe ".cutSelectedText()", -> - it "removes the selected text from the buffer and places it on the pasteboard", -> + it "removes the selected text from the buffer and places it on the clipboard", -> editor.cutSelectedText() expect(buffer.lineForRow(0)).toBe "var = function () {" expect(buffer.lineForRow(1)).toBe " var = function(items) {" @@ -1885,7 +1887,7 @@ describe "Editor", -> editor.cutToEndOfLine() expect(buffer.lineForRow(2)).toBe ' if (items.length' expect(buffer.lineForRow(3)).toBe ' var pivot = item' - expect(atom.pasteboard.read()[0]).toBe ' <= 1) return items;\ns.shift(), current, left = [], right = [];' + expect(atom.clipboard.read()).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", -> @@ -1895,7 +1897,7 @@ describe "Editor", -> expect(buffer.lineForRow(2)).toBe ' if (items.lengthurn items;' expect(buffer.lineForRow(3)).toBe ' var pivot = item' - expect(atom.pasteboard.read()[0]).toBe ' <= 1) ret\ns.shift(), current, left = [], right = [];' + expect(atom.clipboard.read()).toBe ' <= 1) ret\ns.shift(), current, left = [], right = [];' describe ".copySelectedText()", -> it "copies selected text onto the clipboard", -> @@ -1906,7 +1908,7 @@ describe "Editor", -> describe ".pasteText()", -> it "pastes text into the buffer", -> - atom.pasteboard.write('first') + atom.clipboard.write('first') editor.pasteText() expect(editor.buffer.lineForRow(0)).toBe "var first = function () {" expect(buffer.lineForRow(1)).toBe " var first = function(items) {" @@ -2733,19 +2735,26 @@ describe "Editor", -> describe "when the editor's grammar has an injection selector", -> beforeEach -> - atom.packages.activatePackage('language-text', sync: true) - atom.packages.activatePackage('language-javascript', sync: true) + + waitsForPromise -> + atom.packages.activatePackage('language-text') + + waitsForPromise -> + atom.packages.activatePackage('language-javascript') it "includes the grammar's patterns when the selector matches the current scope in other grammars", -> - atom.packages.activatePackage('language-hyperlink', sync: true) - grammar = atom.syntax.selectGrammar("text.js") - {tokens} = grammar.tokenizeLine("var i; // http://github.com") + waitsForPromise -> + atom.packages.activatePackage('language-hyperlink') - expect(tokens[0].value).toBe "var" - expect(tokens[0].scopes).toEqual ["source.js", "storage.modifier.js"] + runs -> + grammar = atom.syntax.selectGrammar("text.js") + {tokens} = grammar.tokenizeLine("var i; // http://github.com") - expect(tokens[6].value).toBe "http://github.com" - expect(tokens[6].scopes).toEqual ["source.js", "comment.line.double-slash.js", "markup.underline.link.http.hyperlink"] + expect(tokens[0].value).toBe "var" + expect(tokens[0].scopes).toEqual ["source.js", "storage.modifier.js"] + + expect(tokens[6].value).toBe "http://github.com" + expect(tokens[6].scopes).toEqual ["source.js", "comment.line.double-slash.js", "markup.underline.link.http.hyperlink"] describe "when the grammar is added", -> it "retokenizes existing buffers that contain tokens that match the injection selector", -> @@ -2756,11 +2765,13 @@ describe "Editor", -> expect(tokens[1].value).toBe " http://github.com" expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"] - atom.packages.activatePackage('language-hyperlink', sync: true) + waitsForPromise -> + atom.packages.activatePackage('language-hyperlink') - {tokens} = editor.lineForScreenRow(0) - expect(tokens[2].value).toBe "http://github.com" - expect(tokens[2].scopes).toEqual ["source.js", "comment.line.double-slash.js", "markup.underline.link.http.hyperlink"] + runs -> + {tokens} = editor.lineForScreenRow(0) + expect(tokens[2].value).toBe "http://github.com" + expect(tokens[2].scopes).toEqual ["source.js", "comment.line.double-slash.js", "markup.underline.link.http.hyperlink"] describe "when the grammar is updated", -> it "retokenizes existing buffers that contain tokens that match the injection selector", -> @@ -2771,14 +2782,17 @@ describe "Editor", -> expect(tokens[1].value).toBe " SELECT * FROM OCTOCATS" expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"] - atom.packages.activatePackage('package-with-injection-selector', sync: true) + + atom.packages.activatePackage('package-with-injection-selector') {tokens} = editor.lineForScreenRow(0) expect(tokens[1].value).toBe " SELECT * FROM OCTOCATS" expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"] - atom.packages.activatePackage('language-sql', sync: true) + waitsForPromise -> + atom.packages.activatePackage('language-sql') - {tokens} = editor.lineForScreenRow(0) - expect(tokens[2].value).toBe "SELECT" - expect(tokens[2].scopes).toEqual ["source.js", "comment.line.double-slash.js", "keyword.other.DML.sql"] + runs -> + {tokens} = editor.lineForScreenRow(0) + expect(tokens[2].value).toBe "SELECT" + expect(tokens[2].scopes).toEqual ["source.js", "comment.line.double-slash.js", "keyword.other.DML.sql"] diff --git a/spec/editor-view-spec.coffee b/spec/editor-view-spec.coffee index 23804de70..33db23b42 100644 --- a/spec/editor-view-spec.coffee +++ b/spec/editor-view-spec.coffee @@ -10,8 +10,6 @@ describe "EditorView", -> [buffer, editorView, editor, cachedLineHeight, cachedCharWidth] = [] beforeEach -> - atom.packages.activatePackage('language-text', sync: true) - atom.packages.activatePackage('language-javascript', sync: true) editor = atom.project.openSync('sample.js') buffer = editor.buffer editorView = new EditorView(editor) @@ -26,6 +24,12 @@ describe "EditorView", -> @width(getCharWidth() * widthInChars) if widthInChars $('#jasmine-content').append(this) + waitsForPromise -> + atom.packages.activatePackage('language-text', sync: true) + + waitsForPromise -> + atom.packages.activatePackage('language-javascript', sync: true) + getLineHeight = -> return cachedLineHeight if cachedLineHeight? calcDimensions() @@ -2515,9 +2519,9 @@ describe "EditorView", -> expect(edited).toBe false describe "when editor:copy-path is triggered", -> - it "copies the absolute path to the editor view's file to the pasteboard", -> + it "copies the absolute path to the editor view's file to the clipboard", -> editorView.trigger 'editor:copy-path' - expect(atom.pasteboard.read()[0]).toBe editor.getPath() + expect(atom.clipboard.read()).toBe editor.getPath() describe "when editor:move-line-up is triggered", -> describe "when there is no selection", -> @@ -2532,6 +2536,52 @@ describe "EditorView", -> editorView.trigger 'editor:move-line-up' expect(editor.getCursorBufferPosition()).toEqual [0,2] + describe "when the line above is folded", -> + it "moves the line around the fold", -> + editor.foldBufferRow(1) + editor.setCursorBufferPosition([10, 0]) + editorView.trigger 'editor:move-line-up' + + expect(editor.getCursorBufferPosition()).toEqual [1, 0] + expect(buffer.lineForRow(1)).toBe '' + expect(buffer.lineForRow(2)).toBe ' var sort = function(items) {' + expect(editor.isFoldedAtBufferRow(1)).toBe false + expect(editor.isFoldedAtBufferRow(2)).toBe true + + describe "when the line being moved is folded", -> + it "moves the fold around the fold above it", -> + editor.setCursorBufferPosition([0, 0]) + editor.insertText """ + var a = function() { + b = 3; + }; + + """ + editor.foldBufferRow(0) + editor.foldBufferRow(3) + editor.setCursorBufferPosition([3, 0]) + editorView.trigger 'editor:move-line-up' + + expect(editor.getCursorBufferPosition()).toEqual [0, 0] + expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {' + expect(buffer.lineForRow(13)).toBe 'var a = function() {' + editor.logScreenLines() + expect(editor.isFoldedAtBufferRow(0)).toBe true + expect(editor.isFoldedAtBufferRow(13)).toBe true + + describe "when the line above is empty and the line above that is folded", -> + it "moves the line to the empty line", -> + editor.foldBufferRow(2) + editor.setCursorBufferPosition([11, 0]) + editorView.trigger 'editor:move-line-up' + + expect(editor.getCursorBufferPosition()).toEqual [10, 0] + expect(buffer.lineForRow(9)).toBe ' };' + expect(buffer.lineForRow(10)).toBe ' return sort(Array.apply(this, arguments));' + expect(buffer.lineForRow(11)).toBe '' + expect(editor.isFoldedAtBufferRow(2)).toBe true + expect(editor.isFoldedAtBufferRow(10)).toBe false + describe "where there is a selection", -> describe "when the selection falls inside the line", -> it "maintains the selection", -> @@ -2631,6 +2681,54 @@ describe "EditorView", -> editorView.trigger 'editor:move-line-down' expect(editor.getCursorBufferPosition()).toEqual [1, 2] + describe "when the line below is folded", -> + it "moves the line around the fold", -> + editor.setCursorBufferPosition([0, 0]) + editor.foldBufferRow(1) + editorView.trigger 'editor:move-line-down' + + expect(editor.getCursorBufferPosition()).toEqual [9, 0] + expect(buffer.lineForRow(0)).toBe ' var sort = function(items) {' + expect(buffer.lineForRow(9)).toBe 'var quicksort = function () {' + expect(editor.isFoldedAtBufferRow(0)).toBe true + expect(editor.isFoldedAtBufferRow(9)).toBe false + + describe "when the line being moved is folded", -> + it "moves the fold around the fold below it", -> + editor.setCursorBufferPosition([0, 0]) + editor.insertText """ + var a = function() { + b = 3; + }; + + """ + editor.foldBufferRow(0) + editor.foldBufferRow(3) + editor.setCursorBufferPosition([0, 0]) + editorView.trigger 'editor:move-line-down' + + expect(editor.getCursorBufferPosition()).toEqual [13, 0] + expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {' + expect(buffer.lineForRow(13)).toBe 'var a = function() {' + expect(editor.isFoldedAtBufferRow(0)).toBe true + expect(editor.isFoldedAtBufferRow(13)).toBe true + + describe "when the line below is empty and the line below that is folded", -> + it "moves the line to the empty line", -> + editor.setCursorBufferPosition([0, Infinity]) + editor.insertText('\n') + editor.setCursorBufferPosition([0, 0]) + editor.foldBufferRow(2) + editorView.trigger 'editor:move-line-down' + + expect(editor.getCursorBufferPosition()).toEqual [1, 0] + expect(buffer.lineForRow(0)).toBe '' + expect(buffer.lineForRow(1)).toBe 'var quicksort = function () {' + expect(buffer.lineForRow(2)).toBe ' var sort = function(items) {' + expect(editor.isFoldedAtBufferRow(0)).toBe false + expect(editor.isFoldedAtBufferRow(1)).toBe false + expect(editor.isFoldedAtBufferRow(2)).toBe true + describe "when the cursor is on the last line", -> it "does not move the line", -> editor.moveCursorToBottom() diff --git a/spec/fixtures/packages/package-with-activation-events/package.cson b/spec/fixtures/packages/package-with-activation-events/package.cson index 45d8dfea1..dfa55c54d 100644 --- a/spec/fixtures/packages/package-with-activation-events/package.cson +++ b/spec/fixtures/packages/package-with-activation-events/package.cson @@ -1,2 +1 @@ 'activationEvents': ['activation-event'] -'deferredDeserializers': ['Foo'] diff --git a/spec/fixtures/packages/package-with-serialize-error/package.cson b/spec/fixtures/packages/package-with-serialize-error/package.cson index d49a175ed..300ce2fc0 100644 --- a/spec/fixtures/packages/package-with-serialize-error/package.cson +++ b/spec/fixtures/packages/package-with-serialize-error/package.cson @@ -1,5 +1 @@ -# This package loads async, otherwise it would log errors when it -# is automatically serialized when workspaceView is deactivatated - 'main': 'index.coffee' -'activationEvents': ['activation-event'] diff --git a/spec/fixtures/packages/theme-with-index-css/package.json b/spec/fixtures/packages/theme-with-index-css/package.json index a4dc0188d..16e770b05 100644 --- a/spec/fixtures/packages/theme-with-index-css/package.json +++ b/spec/fixtures/packages/theme-with-index-css/package.json @@ -1,3 +1,3 @@ { - "theme": true + "theme": "ui" } diff --git a/spec/fixtures/packages/theme-with-index-less/package.json b/spec/fixtures/packages/theme-with-index-less/package.json index a4dc0188d..16e770b05 100644 --- a/spec/fixtures/packages/theme-with-index-less/package.json +++ b/spec/fixtures/packages/theme-with-index-less/package.json @@ -1,3 +1,3 @@ { - "theme": true + "theme": "ui" } diff --git a/spec/fixtures/packages/theme-with-multiple-imported-files/package.json b/spec/fixtures/packages/theme-with-multiple-imported-files/package.json index a4dc0188d..16e770b05 100644 --- a/spec/fixtures/packages/theme-with-multiple-imported-files/package.json +++ b/spec/fixtures/packages/theme-with-multiple-imported-files/package.json @@ -1,3 +1,3 @@ { - "theme": true + "theme": "ui" } diff --git a/spec/fixtures/packages/theme-with-package-file/package.json b/spec/fixtures/packages/theme-with-package-file/package.json index b429a1151..a5e06495a 100644 --- a/spec/fixtures/packages/theme-with-package-file/package.json +++ b/spec/fixtures/packages/theme-with-package-file/package.json @@ -1,4 +1,4 @@ { - "theme": true, + "theme": "ui", "stylesheets": ["first.css", "second.less", "last.css"] } diff --git a/spec/fixtures/packages/theme-with-ui-variables/package.json b/spec/fixtures/packages/theme-with-ui-variables/package.json index fa1446fd6..047ee57c9 100644 --- a/spec/fixtures/packages/theme-with-ui-variables/package.json +++ b/spec/fixtures/packages/theme-with-ui-variables/package.json @@ -1,4 +1,4 @@ { - "theme": true, + "theme": "ui", "stylesheets": ["editor.less"] } diff --git a/spec/jasmine-helper.coffee b/spec/jasmine-helper.coffee index 5ff4ee868..87765634c 100644 --- a/spec/jasmine-helper.coffee +++ b/spec/jasmine-helper.coffee @@ -21,10 +21,6 @@ module.exports.runSpecSuite = (specSuite, logFile, logErrors=true) -> print: (str) -> log(str) onComplete: (runner) -> - log('\n') - timeReporter.logLongestSuites 10, (line) -> log("#{line}\n") - log('\n') - timeReporter.logLongestSpecs 10, (line) -> log("#{line}\n") fs.closeSync(logStream) if logStream? atom.exit(runner.results().failedCount > 0 ? 1 : 0) else diff --git a/spec/language-mode-spec.coffee b/spec/language-mode-spec.coffee index 2f7b1cc91..a72470704 100644 --- a/spec/language-mode-spec.coffee +++ b/spec/language-mode-spec.coffee @@ -6,10 +6,12 @@ describe "LanguageMode", -> describe "javascript", -> beforeEach -> - atom.packages.activatePackage('language-javascript', sync: true) editor = atom.project.openSync('sample.js', autoIndent: false) {buffer, languageMode} = editor + waitsForPromise -> + atom.packages.activatePackage('language-javascript') + describe ".minIndentLevelForRowRange(startRow, endRow)", -> it "returns the minimum indent level for the given row range", -> expect(languageMode.minIndentLevelForRowRange(4, 7)).toBe 2 @@ -100,10 +102,12 @@ describe "LanguageMode", -> describe "coffeescript", -> beforeEach -> - atom.packages.activatePackage('language-coffee-script', sync: true) editor = atom.project.openSync('coffee.coffee', autoIndent: false) {buffer, languageMode} = editor + waitsForPromise -> + atom.packages.activatePackage('language-coffee-script') + describe ".toggleLineCommentsForBufferRows(start, end)", -> it "comments/uncomments lines in the given range", -> languageMode.toggleLineCommentsForBufferRows(4, 6) @@ -147,10 +151,12 @@ describe "LanguageMode", -> describe "css", -> beforeEach -> - atom.packages.activatePackage('language-css', sync: true) editor = atom.project.openSync('css.css', autoIndent: false) {buffer, languageMode} = editor + waitsForPromise -> + atom.packages.activatePackage('language-css') + describe ".toggleLineCommentsForBufferRows(start, end)", -> it "comments/uncomments lines in the given range", -> languageMode.toggleLineCommentsForBufferRows(0, 1) @@ -188,11 +194,15 @@ describe "LanguageMode", -> describe "less", -> beforeEach -> - atom.packages.activatePackage('language-less', sync: true) - atom.packages.activatePackage('language-css', sync: true) editor = atom.project.openSync('sample.less', autoIndent: false) {buffer, languageMode} = editor + waitsForPromise -> + atom.packages.activatePackage('language-less') + + waitsForPromise -> + atom.packages.activatePackage('language-css') + describe "when commenting lines", -> it "only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart`", -> languageMode.toggleLineCommentsForBufferRows(0, 0) @@ -200,10 +210,12 @@ describe "LanguageMode", -> describe "folding", -> beforeEach -> - atom.packages.activatePackage('language-javascript', sync: true) editor = atom.project.openSync('sample.js', autoIndent: false) {buffer, languageMode} = editor + waitsForPromise -> + atom.packages.activatePackage('language-javascript') + it "maintains cursor buffer position when a folding/unfolding", -> editor.setCursorBufferPosition([5,5]) languageMode.foldAll() @@ -298,10 +310,12 @@ describe "LanguageMode", -> describe "folding with comments", -> beforeEach -> - atom.packages.activatePackage('language-javascript', sync: true) editor = atom.project.openSync('sample-with-comments.js', autoIndent: false) {buffer, languageMode} = editor + waitsForPromise -> + atom.packages.activatePackage('language-javascript') + describe ".unfoldAll()", -> it "unfolds every folded line", -> initialScreenLineCount = editor.getScreenLineCount() @@ -362,10 +376,12 @@ describe "LanguageMode", -> describe "css", -> beforeEach -> - atom.packages.activatePackage('language-source', sync: true) - atom.packages.activatePackage('language-css', sync: true) editor = atom.project.openSync('css.css', autoIndent: true) + waitsForPromise -> + atom.packages.activatePackage('language-source') + atom.packages.activatePackage('language-css') + describe "suggestedIndentForBufferRow", -> it "does not return negative values (regression)", -> editor.setText('.test {\npadding: 0;\n}') diff --git a/spec/pane-container-view-spec.coffee b/spec/pane-container-view-spec.coffee index 667d3bd6a..28fbd56a6 100644 --- a/spec/pane-container-view-spec.coffee +++ b/spec/pane-container-view-spec.coffee @@ -27,32 +27,6 @@ describe "PaneContainerView", -> afterEach -> atom.deserializers.remove(TestView) - describe ".focusNextPane()", -> - it "focuses the pane following the focused pane or the first pane if no pane has focus", -> - container.attachToDom() - container.focusNextPane() - expect(pane1.activeItem).toMatchSelector ':focus' - container.focusNextPane() - expect(pane2.activeItem).toMatchSelector ':focus' - container.focusNextPane() - expect(pane3.activeItem).toMatchSelector ':focus' - container.focusNextPane() - expect(pane1.activeItem).toMatchSelector ':focus' - - describe ".focusPreviousPane()", -> - it "focuses the pane preceding the focused pane or the last pane if no pane has focus", -> - container.attachToDom() - container.getPanes()[0].focus() # activate first pane - - container.focusPreviousPane() - expect(pane3.activeItem).toMatchSelector ':focus' - container.focusPreviousPane() - expect(pane2.activeItem).toMatchSelector ':focus' - container.focusPreviousPane() - expect(pane1.activeItem).toMatchSelector ':focus' - container.focusPreviousPane() - expect(pane3.activeItem).toMatchSelector ':focus' - describe ".getActivePane()", -> it "returns the most-recently focused pane", -> focusStealer = $$ -> @div tabindex: -1, "focus stealer" @@ -265,3 +239,115 @@ describe "PaneContainerView", -> pane1.remove() pane2.remove() expect(activeItemChangedHandler).not.toHaveBeenCalled() + + describe ".focusNextPane()", -> + it "focuses the pane following the focused pane or the first pane if no pane has focus", -> + container.attachToDom() + container.focusNextPane() + expect(pane1.activeItem).toMatchSelector ':focus' + container.focusNextPane() + expect(pane2.activeItem).toMatchSelector ':focus' + container.focusNextPane() + expect(pane3.activeItem).toMatchSelector ':focus' + container.focusNextPane() + expect(pane1.activeItem).toMatchSelector ':focus' + + describe ".focusPreviousPane()", -> + it "focuses the pane preceding the focused pane or the last pane if no pane has focus", -> + container.attachToDom() + container.getPanes()[0].focus() # activate first pane + + container.focusPreviousPane() + expect(pane3.activeItem).toMatchSelector ':focus' + container.focusPreviousPane() + expect(pane2.activeItem).toMatchSelector ':focus' + container.focusPreviousPane() + expect(pane1.activeItem).toMatchSelector ':focus' + container.focusPreviousPane() + expect(pane3.activeItem).toMatchSelector ':focus' + + describe "changing focus directionally between panes", -> + [pane1, pane2, pane3, pane4, pane5, pane6, pane7, pane8, pane9] = [] + + beforeEach -> + # Set up a grid of 9 panes, in the following arrangement, where the + # numbers correspond to the variable names below. + # + # ------- + # |1|2|3| + # ------- + # |4|5|6| + # ------- + # |7|8|9| + # ------- + + container = new PaneContainerView + pane1 = container.getRoot() + pane1.activateItem(new TestView('1')) + pane4 = pane1.splitDown(new TestView('4')) + pane7 = pane4.splitDown(new TestView('7')) + + pane2 = pane1.splitRight(new TestView('2')) + pane3 = pane2.splitRight(new TestView('3')) + + pane5 = pane4.splitRight(new TestView('5')) + pane6 = pane5.splitRight(new TestView('6')) + + pane8 = pane7.splitRight(new TestView('8')) + pane9 = pane8.splitRight(new TestView('9')) + + container.height(400) + container.width(400) + container.attachToDom() + + describe ".focusPaneAbove()", -> + describe "when there are multiple rows above the focused pane", -> + it "focuses up to the adjacent row", -> + pane8.focus() + container.focusPaneAbove() + expect(pane5.activeItem).toMatchSelector ':focus' + + describe "when there are no rows above the focused pane", -> + it "keeps the current pane focused", -> + pane2.focus() + container.focusPaneAbove() + expect(pane2.activeItem).toMatchSelector ':focus' + + describe ".focusPaneBelow()", -> + describe "when there are multiple rows below the focused pane", -> + it "focuses down to the adjacent row", -> + pane2.focus() + container.focusPaneBelow() + expect(pane5.activeItem).toMatchSelector ':focus' + + describe "when there are no rows below the focused pane", -> + it "keeps the current pane focused", -> + pane8.focus() + container.focusPaneBelow() + expect(pane8.activeItem).toMatchSelector ':focus' + + describe ".focusPaneOnLeft()", -> + describe "when there are multiple columns to the left of the focused pane", -> + it "focuses left to the adjacent column", -> + pane6.focus() + container.focusPaneOnLeft() + expect(pane5.activeItem).toMatchSelector ':focus' + + describe "when there are no columns to the left of the focused pane", -> + it "keeps the current pane focused", -> + pane4.focus() + container.focusPaneOnLeft() + expect(pane4.activeItem).toMatchSelector ':focus' + + describe ".focusPaneOnRight()", -> + describe "when there are multiple columns to the right of the focused pane", -> + it "focuses right to the adjacent column", -> + pane4.focus() + container.focusPaneOnRight() + expect(pane5.activeItem).toMatchSelector ':focus' + + describe "when there are no columns to the right of the focused pane", -> + it "keeps the current pane focused", -> + pane6.focus() + container.focusPaneOnRight() + expect(pane6.activeItem).toMatchSelector ':focus' diff --git a/spec/pasteboard-spec.coffee b/spec/pasteboard-spec.coffee deleted file mode 100644 index 467418204..000000000 --- a/spec/pasteboard-spec.coffee +++ /dev/null @@ -1,10 +0,0 @@ -describe "Pasteboard", -> - describe "write(text, metadata) and read()", -> - it "writes and reads text to/from the native pasteboard", -> - expect(atom.pasteboard.read()).toEqual ['initial pasteboard content'] - atom.pasteboard.write('next') - expect(atom.pasteboard.read()[0]).toBe 'next' - - it "returns metadata if the item on the native pasteboard matches the last written item", -> - atom.pasteboard.write('next', {meta: 'data'}) - expect(atom.pasteboard.read()).toEqual ['next', {meta: 'data'}] diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 1ac76e204..e33413f97 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -72,7 +72,7 @@ describe "Project", -> expect(atom.project.getEditors()[1]).toBe editor2 describe ".openSync(path)", -> - [fooOpener, barOpener, absolutePath, newBufferHandler, newEditorHandler] = [] + [absolutePath, newBufferHandler, newEditorHandler] = [] beforeEach -> absolutePath = require.resolve('./fixtures/dir/a') newBufferHandler = jasmine.createSpy('newBufferHandler') @@ -80,129 +80,92 @@ describe "Project", -> newEditorHandler = jasmine.createSpy('newEditorHandler') atom.project.on 'editor-created', newEditorHandler - fooOpener = (pathToOpen, options) -> { foo: pathToOpen, options } if pathToOpen?.match(/\.foo/) - barOpener = (pathToOpen) -> { bar: pathToOpen } if pathToOpen?.match(/^bar:\/\//) - atom.project.registerOpener(fooOpener) - atom.project.registerOpener(barOpener) + describe "when given an absolute path that hasn't been opened previously", -> + it "returns a new edit session for the given path and emits 'buffer-created' and 'editor-created' events", -> + editor = atom.project.openSync(absolutePath) + expect(editor.buffer.getPath()).toBe absolutePath + expect(newBufferHandler).toHaveBeenCalledWith editor.buffer + expect(newEditorHandler).toHaveBeenCalledWith editor - afterEach -> - atom.project.unregisterOpener(fooOpener) - atom.project.unregisterOpener(barOpener) + describe "when given a relative path that hasn't been opened previously", -> + it "returns a new edit session for the given path (relative to the project root) and emits 'buffer-created' and 'editor-created' events", -> + editor = atom.project.openSync('a') + expect(editor.buffer.getPath()).toBe absolutePath + expect(newBufferHandler).toHaveBeenCalledWith editor.buffer + expect(newEditorHandler).toHaveBeenCalledWith editor - describe "when passed a path that doesn't match a custom opener", -> - describe "when given an absolute path that hasn't been opened previously", -> - it "returns a new edit session for the given path and emits 'buffer-created' and 'editor-created' events", -> - editor = atom.project.openSync(absolutePath) + describe "when passed the path to a buffer that has already been opened", -> + it "returns a new edit session containing previously opened buffer and emits a 'editor-created' event", -> + editor = atom.project.openSync(absolutePath) + newBufferHandler.reset() + expect(atom.project.openSync(absolutePath).buffer).toBe editor.buffer + expect(atom.project.openSync('a').buffer).toBe editor.buffer + expect(newBufferHandler).not.toHaveBeenCalled() + expect(newEditorHandler).toHaveBeenCalledWith editor + + describe "when not passed a path", -> + it "returns a new edit session and emits 'buffer-created' and 'editor-created' events", -> + editor = atom.project.openSync() + expect(editor.buffer.getPath()).toBeUndefined() + expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) + expect(newEditorHandler).toHaveBeenCalledWith editor + + describe ".open(path)", -> + [absolutePath, newBufferHandler, newEditorHandler] = [] + + beforeEach -> + absolutePath = require.resolve('./fixtures/dir/a') + newBufferHandler = jasmine.createSpy('newBufferHandler') + atom.project.on 'buffer-created', newBufferHandler + newEditorHandler = jasmine.createSpy('newEditorHandler') + atom.project.on 'editor-created', newEditorHandler + + describe "when given an absolute path that isn't currently open", -> + it "returns a new edit session for the given path and emits 'buffer-created' and 'editor-created' events", -> + editor = null + waitsForPromise -> + atom.project.open(absolutePath).then (o) -> editor = o + + runs -> expect(editor.buffer.getPath()).toBe absolutePath expect(newBufferHandler).toHaveBeenCalledWith editor.buffer expect(newEditorHandler).toHaveBeenCalledWith editor - describe "when given a relative path that hasn't been opened previously", -> - it "returns a new edit session for the given path (relative to the project root) and emits 'buffer-created' and 'editor-created' events", -> - editor = atom.project.openSync('a') + describe "when given a relative path that isn't currently opened", -> + it "returns a new edit session for the given path (relative to the project root) and emits 'buffer-created' and 'editor-created' events", -> + editor = null + waitsForPromise -> + atom.project.open(absolutePath).then (o) -> editor = o + + runs -> expect(editor.buffer.getPath()).toBe absolutePath expect(newBufferHandler).toHaveBeenCalledWith editor.buffer expect(newEditorHandler).toHaveBeenCalledWith editor - describe "when passed the path to a buffer that has already been opened", -> - it "returns a new edit session containing previously opened buffer and emits a 'editor-created' event", -> - editor = atom.project.openSync(absolutePath) + describe "when passed the path to a buffer that is currently opened", -> + it "returns a new edit session containing currently opened buffer and emits a 'editor-created' event", -> + editor = null + waitsForPromise -> + atom.project.open(absolutePath).then (o) -> editor = o + + runs -> newBufferHandler.reset() expect(atom.project.openSync(absolutePath).buffer).toBe editor.buffer expect(atom.project.openSync('a').buffer).toBe editor.buffer expect(newBufferHandler).not.toHaveBeenCalled() expect(newEditorHandler).toHaveBeenCalledWith editor - describe "when not passed a path", -> - it "returns a new edit session and emits 'buffer-created' and 'editor-created' events", -> - editor = atom.project.openSync() + describe "when not passed a path", -> + it "returns a new edit session and emits 'buffer-created' and 'editor-created' events", -> + editor = null + waitsForPromise -> + atom.project.open().then (o) -> editor = o + + runs -> expect(editor.buffer.getPath()).toBeUndefined() expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) expect(newEditorHandler).toHaveBeenCalledWith editor - describe "when passed a path that matches a custom opener", -> - it "returns the resource returned by the custom opener", -> - pathToOpen = atom.project.resolve('a.foo') - expect(atom.project.openSync(pathToOpen, hey: "there")).toEqual { foo: pathToOpen, options: {hey: "there"} } - expect(atom.project.openSync("bar://baz")).toEqual { bar: "bar://baz" } - - describe ".open(path)", -> - [fooOpener, barOpener, absolutePath, newBufferHandler, newEditorHandler] = [] - - beforeEach -> - absolutePath = require.resolve('./fixtures/dir/a') - newBufferHandler = jasmine.createSpy('newBufferHandler') - atom.project.on 'buffer-created', newBufferHandler - newEditorHandler = jasmine.createSpy('newEditorHandler') - atom.project.on 'editor-created', newEditorHandler - - fooOpener = (pathToOpen, options) -> { foo: pathToOpen, options } if pathToOpen?.match(/\.foo/) - barOpener = (pathToOpen) -> { bar: pathToOpen } if pathToOpen?.match(/^bar:\/\//) - atom.project.registerOpener(fooOpener) - atom.project.registerOpener(barOpener) - - afterEach -> - atom.project.unregisterOpener(fooOpener) - atom.project.unregisterOpener(barOpener) - - describe "when passed a path that doesn't match a custom opener", -> - describe "when given an absolute path that isn't currently open", -> - it "returns a new edit session for the given path and emits 'buffer-created' and 'editor-created' events", -> - editor = null - waitsForPromise -> - atom.project.open(absolutePath).then (o) -> editor = o - - runs -> - expect(editor.buffer.getPath()).toBe absolutePath - expect(newBufferHandler).toHaveBeenCalledWith editor.buffer - expect(newEditorHandler).toHaveBeenCalledWith editor - - describe "when given a relative path that isn't currently opened", -> - it "returns a new edit session for the given path (relative to the project root) and emits 'buffer-created' and 'editor-created' events", -> - editor = null - waitsForPromise -> - atom.project.open(absolutePath).then (o) -> editor = o - - runs -> - expect(editor.buffer.getPath()).toBe absolutePath - expect(newBufferHandler).toHaveBeenCalledWith editor.buffer - expect(newEditorHandler).toHaveBeenCalledWith editor - - describe "when passed the path to a buffer that is currently opened", -> - it "returns a new edit session containing currently opened buffer and emits a 'editor-created' event", -> - editor = null - waitsForPromise -> - atom.project.open(absolutePath).then (o) -> editor = o - - runs -> - newBufferHandler.reset() - expect(atom.project.openSync(absolutePath).buffer).toBe editor.buffer - expect(atom.project.openSync('a').buffer).toBe editor.buffer - expect(newBufferHandler).not.toHaveBeenCalled() - expect(newEditorHandler).toHaveBeenCalledWith editor - - describe "when not passed a path", -> - it "returns a new edit session and emits 'buffer-created' and 'editor-created' events", -> - editor = null - waitsForPromise -> - atom.project.open().then (o) -> editor = o - - runs -> - expect(editor.buffer.getPath()).toBeUndefined() - expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) - expect(newEditorHandler).toHaveBeenCalledWith editor - - describe "when passed a path that matches a custom opener", -> - it "returns the resource returned by the custom opener", -> - waitsForPromise -> - pathToOpen = atom.project.resolve('a.foo') - atom.project.open(pathToOpen, hey: "there").then (item) -> - expect(item).toEqual { foo: pathToOpen, options: {hey: "there"} } - - waitsForPromise -> - atom.project.open("bar://baz").then (item) -> - expect(item).toEqual { bar: "bar://baz" } - it "returns number of read bytes as progress indicator", -> filePath = atom.project.resolve 'a' totalBytes = 0 diff --git a/spec/space-pen-extensions-spec.coffee b/spec/space-pen-extensions-spec.coffee index 28849c72c..92b95c600 100644 --- a/spec/space-pen-extensions-spec.coffee +++ b/spec/space-pen-extensions-spec.coffee @@ -11,35 +11,6 @@ describe "SpacePen extensions", -> parent = $$ -> @div() parent.append(view) - describe "View.observeConfig(keyPath, callback)", -> - observeHandler = null - - beforeEach -> - observeHandler = jasmine.createSpy("observeHandler") - view.observeConfig "foo.bar", observeHandler - expect(view.hasParent()).toBeTruthy() - - it "observes the keyPath and cancels the subscription when `.unobserveConfig()` is called", -> - expect(observeHandler).toHaveBeenCalledWith(undefined) - observeHandler.reset() - - atom.config.set("foo.bar", "hello") - - expect(observeHandler).toHaveBeenCalledWith("hello", previous: undefined) - observeHandler.reset() - - view.unobserveConfig() - - atom.config.set("foo.bar", "goodbye") - - expect(observeHandler).not.toHaveBeenCalled() - - it "unobserves when the view is removed", -> - observeHandler.reset() - parent.remove() - atom.config.set("foo.bar", "hello") - expect(observeHandler).not.toHaveBeenCalled() - describe "View.subscribe(eventEmitter, eventName, callback)", -> [emitter, eventHandler] = [] diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 8e939d03d..8e9fa939d 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -95,9 +95,9 @@ beforeEach -> TokenizedBuffer.prototype.chunkSize = Infinity spyOn(TokenizedBuffer.prototype, "tokenizeInBackground").andCallFake -> @tokenizeNextChunk() - pasteboardContent = 'initial pasteboard content' - spyOn(clipboard, 'writeText').andCallFake (text) -> pasteboardContent = text - spyOn(clipboard, 'readText').andCallFake -> pasteboardContent + clipboardContent = 'initial clipboard content' + spyOn(clipboard, 'writeText').andCallFake (text) -> clipboardContent = text + spyOn(clipboard, 'readText').andCallFake -> clipboardContent addCustomMatchers(this) @@ -159,6 +159,16 @@ addCustomMatchers = (spec) -> @message = -> return "Expected path '" + @actual + "'" + notText + " to exist." fs.existsSync(@actual) + toHaveFocus: -> + notText = this.isNot and " not" or "" + if not document.hasFocus() + console.error "Specs will fail because the Dev Tools have focus. To fix this close the Dev Tools or click the spec runner." + + @message = -> return "Expected element '" + @actual + "' or its descendants" + notText + " to have focus." + element = @actual + element = element.get(0) if element.jquery + element.webkitMatchesSelector(":focus") or element.querySelector(":focus") + window.keyIdentifierForKey = (key) -> if key.length > 1 # named key key diff --git a/spec/syntax-spec.coffee b/spec/syntax-spec.coffee index 92373c53d..c39d12152 100644 --- a/spec/syntax-spec.coffee +++ b/spec/syntax-spec.coffee @@ -4,10 +4,18 @@ temp = require 'temp' describe "the `syntax` global", -> beforeEach -> - atom.packages.activatePackage('language-text', sync: true) - atom.packages.activatePackage('language-javascript', sync: true) - atom.packages.activatePackage('language-coffee-script', sync: true) - atom.packages.activatePackage('language-ruby', sync: true) + + waitsForPromise -> + atom.packages.activatePackage('language-text') + + waitsForPromise -> + atom.packages.activatePackage('language-javascript') + + waitsForPromise -> + atom.packages.activatePackage('language-coffee-script') + + waitsForPromise -> + atom.packages.activatePackage('language-ruby') describe "serialization", -> it "remembers grammar overrides by path", -> @@ -20,29 +28,33 @@ describe "the `syntax` global", -> describe ".selectGrammar(filePath)", -> it "can use the filePath to load the correct grammar based on the grammar's filetype", -> - atom.packages.activatePackage('language-git', sync: true) + waitsForPromise -> + atom.packages.activatePackage('language-git') - expect(atom.syntax.selectGrammar("file.js").name).toBe "JavaScript" # based on extension (.js) - expect(atom.syntax.selectGrammar(path.join(temp.dir, '.git', 'config')).name).toBe "Git Config" # based on end of the path (.git/config) - expect(atom.syntax.selectGrammar("Rakefile").name).toBe "Ruby" # based on the file's basename (Rakefile) - expect(atom.syntax.selectGrammar("curb").name).toBe "Null Grammar" - expect(atom.syntax.selectGrammar("/hu.git/config").name).toBe "Null Grammar" + runs -> + expect(atom.syntax.selectGrammar("file.js").name).toBe "JavaScript" # based on extension (.js) + expect(atom.syntax.selectGrammar(path.join(temp.dir, '.git', 'config')).name).toBe "Git Config" # based on end of the path (.git/config) + expect(atom.syntax.selectGrammar("Rakefile").name).toBe "Ruby" # based on the file's basename (Rakefile) + expect(atom.syntax.selectGrammar("curb").name).toBe "Null Grammar" + expect(atom.syntax.selectGrammar("/hu.git/config").name).toBe "Null Grammar" it "uses the filePath's shebang line if the grammar cannot be determined by the extension or basename", -> filePath = require.resolve("./fixtures/shebang") expect(atom.syntax.selectGrammar(filePath).name).toBe "Ruby" it "uses the number of newlines in the first line regex to determine the number of lines to test against", -> - atom.packages.activatePackage('language-property-list', sync: true) + waitsForPromise -> + atom.packages.activatePackage('language-property-list') - fileContent = "first-line\n" - expect(atom.syntax.selectGrammar("dummy.coffee", fileContent).name).toBe "CoffeeScript" + runs -> + fileContent = "first-line\n" + expect(atom.syntax.selectGrammar("dummy.coffee", fileContent).name).toBe "CoffeeScript" - fileContent = '' - expect(atom.syntax.selectGrammar("grammar.tmLanguage", fileContent).name).toBe "Null Grammar" + fileContent = '' + expect(atom.syntax.selectGrammar("grammar.tmLanguage", fileContent).name).toBe "Null Grammar" - fileContent += '\n' - expect(atom.syntax.selectGrammar("grammar.tmLanguage", fileContent).name).toBe "Property List (XML)" + fileContent += '\n' + expect(atom.syntax.selectGrammar("grammar.tmLanguage", fileContent).name).toBe "Property List (XML)" it "doesn't read the file when the file contents are specified", -> filePath = require.resolve("./fixtures/shebang") diff --git a/spec/text-buffer-spec.coffee b/spec/text-buffer-spec.coffee index 7f26c663b..70e1a773f 100644 --- a/spec/text-buffer-spec.coffee +++ b/spec/text-buffer-spec.coffee @@ -571,6 +571,23 @@ describe 'TextBuffer', -> saveBuffer.reload() expect(events).toEqual ['will-reload', 'reloaded'] + it "no longer reports being in conflict", -> + saveBuffer.setText('a') + saveBuffer.save() + saveBuffer.setText('ab') + + fs.writeFileSync(saveBuffer.getPath(), 'c') + conflictHandler = jasmine.createSpy('conflictHandler') + saveBuffer.on 'contents-conflicted', conflictHandler + + waitsFor -> + conflictHandler.callCount > 0 + + runs -> + expect(saveBuffer.isInConflict()).toBe true + saveBuffer.save() + expect(saveBuffer.isInConflict()).toBe false + describe "when the buffer has no path", -> it "throws an exception", -> saveBuffer = atom.project.bufferForPathSync(null) diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee index c8d257b54..384844e93 100644 --- a/spec/theme-manager-spec.coffee +++ b/spec/theme-manager-spec.coffee @@ -26,11 +26,14 @@ describe "ThemeManager", -> expect(themes.length).toBeGreaterThan(2) it 'getActiveThemes get all the active themes', -> - themeManager.activateThemes() - names = atom.config.get('core.themes') - expect(names.length).toBeGreaterThan(0) - themes = themeManager.getActiveThemes() - expect(themes).toHaveLength(names.length) + waitsForPromise -> + themeManager.activateThemes() + + runs -> + names = atom.config.get('core.themes') + expect(names.length).toBeGreaterThan(0) + themes = themeManager.getActiveThemes() + expect(themes).toHaveLength(names.length) describe "getImportPaths()", -> it "returns the theme directories before the themes are loaded", -> @@ -51,29 +54,58 @@ describe "ThemeManager", -> it "add/removes stylesheets to reflect the new config value", -> themeManager.on 'reloaded', reloadHandler = jasmine.createSpy() spyOn(themeManager, 'getUserStylesheetPath').andCallFake -> null - themeManager.activateThemes() - atom.config.set('core.themes', []) - expect($('style.theme').length).toBe 0 - expect(reloadHandler).toHaveBeenCalled() + waitsForPromise -> + themeManager.activateThemes() - atom.config.set('core.themes', ['atom-dark-syntax']) - expect($('style.theme').length).toBe 1 - expect($('style.theme:eq(0)').attr('id')).toMatch /atom-dark-syntax/ + runs -> + reloadHandler.reset() + atom.config.set('core.themes', []) - atom.config.set('core.themes', ['atom-light-syntax', 'atom-dark-syntax']) - expect($('style.theme').length).toBe 2 - expect($('style.theme:eq(0)').attr('id')).toMatch /atom-dark-syntax/ - expect($('style.theme:eq(1)').attr('id')).toMatch /atom-light-syntax/ + waitsFor -> + reloadHandler.callCount == 1 - atom.config.set('core.themes', []) - expect($('style.theme').length).toBe 0 + runs -> + reloadHandler.reset() + expect($('style.theme')).toHaveLength 0 + atom.config.set('core.themes', ['atom-dark-syntax']) - # atom-dark-ui has an directory path, the syntax one doesn't - atom.config.set('core.themes', ['theme-with-index-less', 'atom-dark-ui']) - importPaths = themeManager.getImportPaths() - expect(importPaths.length).toBe 1 - expect(importPaths[0]).toContain 'atom-dark-ui' + waitsFor -> + reloadHandler.callCount == 1 + + runs -> + reloadHandler.reset() + expect($('style.theme')).toHaveLength 1 + expect($('style.theme:eq(0)').attr('id')).toMatch /atom-dark-syntax/ + atom.config.set('core.themes', ['atom-light-syntax', 'atom-dark-syntax']) + + waitsFor -> + reloadHandler.callCount == 1 + + runs -> + reloadHandler.reset() + expect($('style.theme')).toHaveLength 2 + expect($('style.theme:eq(0)').attr('id')).toMatch /atom-dark-syntax/ + expect($('style.theme:eq(1)').attr('id')).toMatch /atom-light-syntax/ + atom.config.set('core.themes', []) + + waitsFor -> + reloadHandler.callCount == 1 + + runs -> + reloadHandler.reset() + expect($('style.theme')).toHaveLength 0 + # atom-dark-ui has an directory path, the syntax one doesn't + atom.config.set('core.themes', ['theme-with-index-less', 'atom-dark-ui']) + + waitsFor -> + reloadHandler.callCount == 1 + + runs -> + expect($('style.theme')).toHaveLength 2 + importPaths = themeManager.getImportPaths() + expect(importPaths.length).toBe 1 + expect(importPaths[0]).toContain 'atom-dark-ui' describe "when a theme fails to load", -> it "logs a warning", -> @@ -145,18 +177,25 @@ describe "ThemeManager", -> atom.workspaceView = new WorkspaceView atom.workspaceView.append $$ -> @div class: 'editor' atom.workspaceView.attachToDom() - themeManager.activateThemes() + + waitsForPromise -> + themeManager.activateThemes() it "loads the correct values from the theme's ui-variables file", -> + themeManager.on 'reloaded', reloadHandler = jasmine.createSpy() atom.config.set('core.themes', ['theme-with-ui-variables']) - # an override loaded in the base css - expect(atom.workspaceView.css("background-color")).toBe "rgb(0, 0, 255)" + waitsFor -> + reloadHandler.callCount > 0 - # from within the theme itself - expect($(".editor").css("padding-top")).toBe "150px" - expect($(".editor").css("padding-right")).toBe "150px" - expect($(".editor").css("padding-bottom")).toBe "150px" + runs -> + # an override loaded in the base css + expect(atom.workspaceView.css("background-color")).toBe "rgb(0, 0, 255)" + + # from within the theme itself + expect($(".editor").css("padding-top")).toBe "150px" + expect($(".editor").css("padding-right")).toBe "150px" + expect($(".editor").css("padding-bottom")).toBe "150px" describe "when the user stylesheet changes", -> it "reloads it", -> @@ -164,12 +203,14 @@ describe "ThemeManager", -> fs.writeFileSync(userStylesheetPath, 'body {border-style: dotted !important;}') spyOn(themeManager, 'getUserStylesheetPath').andReturn userStylesheetPath - themeManager.activateThemes() - expect($(document.body).css('border-style')).toBe 'dotted' - spyOn(themeManager, 'loadUserStylesheet').andCallThrough() + waitsForPromise -> + themeManager.activateThemes() - fs.writeFileSync(userStylesheetPath, 'body {border-style: dashed}') + runs -> + expect($(document.body).css('border-style')).toBe 'dotted' + spyOn(themeManager, 'loadUserStylesheet').andCallThrough() + fs.writeFileSync(userStylesheetPath, 'body {border-style: dashed}') waitsFor -> themeManager.loadUserStylesheet.callCount is 1 diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index a9df78a56..2fad95256 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -5,11 +5,13 @@ describe "TokenizedBuffer", -> [tokenizedBuffer, buffer, changeHandler] = [] beforeEach -> - atom.packages.activatePackage('language-javascript', sync: true) # enable async tokenization TokenizedBuffer.prototype.chunkSize = 5 jasmine.unspy(TokenizedBuffer.prototype, 'tokenizeInBackground') + waitsForPromise -> + atom.packages.activatePackage('language-javascript') + startTokenizing = (tokenizedBuffer) -> tokenizedBuffer.setVisible(true) @@ -311,10 +313,13 @@ describe "TokenizedBuffer", -> describe "when the buffer contains hard-tabs", -> beforeEach -> - atom.packages.activatePackage('language-coffee-script', sync: true) - buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee') - tokenizedBuffer = new TokenizedBuffer({buffer}) - startTokenizing(tokenizedBuffer) + waitsForPromise -> + atom.packages.activatePackage('language-coffee-script') + + runs -> + buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee') + tokenizedBuffer = new TokenizedBuffer({buffer}) + startTokenizing(tokenizedBuffer) afterEach -> tokenizedBuffer.destroy() @@ -341,14 +346,17 @@ describe "TokenizedBuffer", -> describe "when the buffer contains surrogate pairs", -> beforeEach -> - atom.packages.activatePackage('language-javascript', sync: true) - buffer = atom.project.bufferForPathSync 'sample-with-pairs.js' - buffer.setText """ - 'abc\uD835\uDF97def' - //\uD835\uDF97xyz - """ - tokenizedBuffer = new TokenizedBuffer({buffer}) - fullyTokenize(tokenizedBuffer) + waitsForPromise -> + atom.packages.activatePackage('language-javascript') + + runs -> + buffer = atom.project.bufferForPathSync 'sample-with-pairs.js' + buffer.setText """ + 'abc\uD835\uDF97def' + //\uD835\uDF97xyz + """ + tokenizedBuffer = new TokenizedBuffer({buffer}) + fullyTokenize(tokenizedBuffer) afterEach -> tokenizedBuffer.destroy() @@ -379,22 +387,30 @@ describe "TokenizedBuffer", -> describe "when the grammar is updated because a grammar it includes is activated", -> it "retokenizes the buffer", -> - atom.packages.activatePackage('language-ruby-on-rails', sync: true) - atom.packages.activatePackage('language-ruby', sync: true) - buffer = atom.project.bufferForPathSync() - buffer.setText "
<%= User.find(2).full_name %>
" - tokenizedBuffer = new TokenizedBuffer({buffer}) - tokenizedBuffer.setGrammar(atom.syntax.selectGrammar('test.erb')) - fullyTokenize(tokenizedBuffer) + waitsForPromise -> + atom.packages.activatePackage('language-ruby-on-rails') - {tokens} = tokenizedBuffer.lineForScreenRow(0) - expect(tokens[0]).toEqual value: "
", scopes: ["text.html.ruby"] + waitsForPromise -> + atom.packages.activatePackage('language-ruby') - atom.packages.activatePackage('language-html', sync: true) - fullyTokenize(tokenizedBuffer) - {tokens} = tokenizedBuffer.lineForScreenRow(0) - expect(tokens[0]).toEqual value: '<', scopes: ["text.html.ruby","meta.tag.block.any.html","punctuation.definition.tag.begin.html"] + runs -> + buffer = atom.project.bufferForPathSync() + buffer.setText "
<%= User.find(2).full_name %>
" + tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer.setGrammar(atom.syntax.selectGrammar('test.erb')) + fullyTokenize(tokenizedBuffer) + + {tokens} = tokenizedBuffer.lineForScreenRow(0) + expect(tokens[0]).toEqual value: "
", scopes: ["text.html.ruby"] + + waitsForPromise -> + atom.packages.activatePackage('language-html') + + runs -> + fullyTokenize(tokenizedBuffer) + {tokens} = tokenizedBuffer.lineForScreenRow(0) + expect(tokens[0]).toEqual value: '<', scopes: ["text.html.ruby","meta.tag.block.any.html","punctuation.definition.tag.begin.html"] describe ".tokenForPosition(position)", -> afterEach -> diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index 937069482..312e3ea4e 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -7,49 +7,141 @@ describe "Workspace", -> atom.project.setPath(atom.project.resolve('dir')) workspace = new Workspace - describe "::open(uri)", -> + describe "::open(uri, options)", -> beforeEach -> - spyOn(workspace.activePane, 'activate') - - describe "when called without a uri", -> - it "adds and activates an empty editor on the active pane", -> - editor = null - waitsForPromise -> - workspace.open().then (o) -> editor = o - - runs -> - expect(editor.getPath()).toBeUndefined() - expect(workspace.activePane.items).toEqual [editor] - expect(workspace.activePaneItem).toBe editor - expect(workspace.activePane.activate).toHaveBeenCalled() - - describe "when called with a uri", -> - describe "when the active pane already has an editor for the given uri", -> - it "activates the existing editor on the active pane", -> - editor1 = workspace.openSync('a') - editor2 = workspace.openSync('b') + spyOn(workspace.activePane, 'activate').andCallThrough() + describe "when the 'searchAllPanes' option is false (default)", -> + describe "when called without a uri", -> + it "adds and activates an empty editor on the active pane", -> editor = null waitsForPromise -> - workspace.open('a').then (o) -> editor = o + workspace.open().then (o) -> editor = o runs -> - expect(editor).toBe editor1 - expect(workspace.activePaneItem).toBe editor - expect(workspace.activePane.activate).toHaveBeenCalled() - - describe "when the active pane does not have an editor for the given uri", -> - it "adds and activates a new editor for the given path on the active pane", -> - editor = null - waitsForPromise -> - workspace.open('a').then (o) -> editor = o - - runs -> - expect(editor.getUri()).toBe 'a' - expect(workspace.activePaneItem).toBe editor + expect(editor.getPath()).toBeUndefined() expect(workspace.activePane.items).toEqual [editor] + expect(workspace.activePaneItem).toBe editor expect(workspace.activePane.activate).toHaveBeenCalled() + describe "when called with a uri", -> + describe "when the active pane already has an editor for the given uri", -> + it "activates the existing editor on the active pane", -> + editor1 = workspace.openSync('a') + editor2 = workspace.openSync('b') + + editor = null + waitsForPromise -> + workspace.open('a').then (o) -> editor = o + + runs -> + expect(editor).toBe editor1 + expect(workspace.activePaneItem).toBe editor + expect(workspace.activePane.activate).toHaveBeenCalled() + + describe "when the active pane does not have an editor for the given uri", -> + it "adds and activates a new editor for the given path on the active pane", -> + editor = null + waitsForPromise -> + workspace.open('a').then (o) -> editor = o + + runs -> + expect(editor.getUri()).toBe 'a' + expect(workspace.activePaneItem).toBe editor + expect(workspace.activePane.items).toEqual [editor] + expect(workspace.activePane.activate).toHaveBeenCalled() + + describe "when the 'searchAllPanes' option is true", -> + describe "when an editor for the given uri is already open on an inactive pane", -> + it "activates the existing editor on the inactive pane, then activates that pane", -> + editor1 = workspace.openSync('a') + pane1 = workspace.activePane + pane2 = workspace.activePane.splitRight() + editor2 = workspace.openSync('b') + expect(workspace.activePaneItem).toBe editor2 + + waitsForPromise -> + workspace.open('a', searchAllPanes: true) + + runs -> + expect(workspace.activePane).toBe pane1 + expect(workspace.activePaneItem).toBe editor1 + + describe "when no editor for the given uri is open in any pane", -> + it "opens an editor for the given uri in the active pane", -> + editor = null + waitsForPromise -> + workspace.open('a', searchAllPanes: true).then (o) -> editor = o + + runs -> + expect(workspace.activePaneItem).toBe editor + + describe "when the 'split' option is set", -> + describe "when the 'split' option is 'left'", -> + it "opens the editor in the leftmost pane of the current pane axis", -> + pane1 = workspace.activePane + pane2 = pane1.splitRight() + expect(workspace.activePane).toBe pane2 + + editor = null + waitsForPromise -> + workspace.open('a', split: 'left').then (o) -> editor = o + + runs -> + expect(workspace.activePane).toBe pane1 + expect(pane1.items).toEqual [editor] + expect(pane2.items).toEqual [] + + # Focus right pane and reopen the file on the left + waitsForPromise -> + pane2.focus() + workspace.open('a', split: 'left').then (o) -> editor = o + + runs -> + expect(workspace.activePane).toBe pane1 + expect(pane1.items).toEqual [editor] + expect(pane2.items).toEqual [] + + describe "when the 'split' option is 'right'", -> + it "opens the editor in the rightmost pane of the current pane axis", -> + editor = null + pane1 = workspace.activePane + pane2 = null + waitsForPromise -> + workspace.open('a', split: 'right').then (o) -> editor = o + + runs -> + pane2 = workspace.getPanes().filter((p) -> p != pane1)[0] + expect(workspace.activePane).toBe pane2 + expect(pane1.items).toEqual [] + expect(pane2.items).toEqual [editor] + + # Focus right pane and reopen the file on the right + waitsForPromise -> + pane1.focus() + workspace.open('a', split: 'right').then (o) -> editor = o + + runs -> + expect(workspace.activePane).toBe pane2 + expect(pane1.items).toEqual [] + expect(pane2.items).toEqual [editor] + + describe "when passed a path that matches a custom opener", -> + it "returns the resource returned by the custom opener", -> + fooOpener = (pathToOpen, options) -> { foo: pathToOpen, options } if pathToOpen?.match(/\.foo/) + barOpener = (pathToOpen) -> { bar: pathToOpen } if pathToOpen?.match(/^bar:\/\//) + workspace.registerOpener(fooOpener) + workspace.registerOpener(barOpener) + + waitsForPromise -> + pathToOpen = atom.project.resolve('a.foo') + workspace.open(pathToOpen, hey: "there").then (item) -> + expect(item).toEqual { foo: pathToOpen, options: {hey: "there"} } + + waitsForPromise -> + workspace.open("bar://baz").then (item) -> + expect(item).toEqual { bar: "bar://baz" } + describe "::openSync(uri, options)", -> [activePane, initialItemCount] = [] @@ -92,61 +184,6 @@ describe "Workspace", -> workspace.openSync('b', activatePane: false) expect(activePane.activate).not.toHaveBeenCalled() - describe "::openSingletonSync(uri, options)", -> - describe "when an editor for the given uri is already open on the active pane", -> - it "activates the existing editor", -> - editor1 = workspace.openSync('a') - editor2 = workspace.openSync('b') - expect(workspace.activePaneItem).toBe editor2 - workspace.openSingletonSync('a') - expect(workspace.activePaneItem).toBe editor1 - - describe "when an editor for the given uri is already open on an inactive pane", -> - it "activates the existing editor on the inactive pane, then activates that pane", -> - editor1 = workspace.openSync('a') - pane1 = workspace.activePane - pane2 = workspace.activePane.splitRight() - editor2 = workspace.openSync('b') - expect(workspace.activePaneItem).toBe editor2 - workspace.openSingletonSync('a') - expect(workspace.activePane).toBe pane1 - expect(workspace.activePaneItem).toBe editor1 - - describe "when no editor for the given uri is open in any pane", -> - it "opens an editor for the given uri in the active pane", -> - editor1 = workspace.openSingletonSync('a') - expect(workspace.activePaneItem).toBe editor1 - - describe "when the 'split' option is 'left'", -> - it "opens the editor in the leftmost pane of the current pane axis", -> - pane1 = workspace.activePane - pane2 = pane1.splitRight() - expect(workspace.activePane).toBe pane2 - editor1 = workspace.openSingletonSync('a', split: 'left') - expect(workspace.activePane).toBe pane1 - expect(pane1.items).toEqual [editor1] - expect(pane2.items).toEqual [] - - describe "when the 'split' option is 'right'", -> - describe "when the active pane is in a horizontal pane axis", -> - it "activates the editor on the rightmost pane of the current pane axis", -> - pane1 = workspace.activePane - pane2 = pane1.splitRight() - pane1.activate() - editor1 = workspace.openSingletonSync('a', split: 'right') - expect(workspace.activePane).toBe pane2 - expect(pane2.items).toEqual [editor1] - expect(pane1.items).toEqual [] - - describe "when the active pane is not in a horizontal pane axis", -> - it "splits the current pane to the right, then activates the editor on the right pane", -> - pane1 = workspace.activePane - editor1 = workspace.openSingletonSync('a', split: 'right') - pane2 = workspace.activePane - expect(workspace.paneContainer.root.children).toEqual [pane1, pane2] - expect(pane2.items).toEqual [editor1] - expect(pane1.items).toEqual [] - describe "::reopenItemSync()", -> it "opens the uri associated with the last closed pane that isn't currently open", -> pane = workspace.activePane diff --git a/src/atom-package.coffee b/src/atom-package.coffee index 6546945bb..ea9358670 100644 --- a/src/atom-package.coffee +++ b/src/atom-package.coffee @@ -2,12 +2,13 @@ Package = require './package' fs = require 'fs-plus' path = require 'path' _ = require 'underscore-plus' +Q = require 'q' {$} = require './space-pen-extensions' CSON = require 'season' {Emitter} = require 'emissary' -### Internal: Loads and resolves packages. ### - +# Loads and activates a package's main module and resources such as +# stylesheets, keymaps, grammar, editor properties, and menus. module.exports = class AtomPackage extends Package Emitter.includeInto(this) @@ -42,11 +43,7 @@ class AtomPackage extends Package @loadStylesheets() @loadGrammars() @loadScopedProperties() - - if @metadata.activationEvents? - @registerDeferredDeserializers() - else - @requireMainModule() + @requireMainModule() unless @metadata.activationEvents? catch e console.warn "Failed to load package named '#{@name}'", e.stack ? e @@ -59,14 +56,19 @@ class AtomPackage extends Package @grammars = [] @scopedProperties = [] - activate: ({immediate}={}) -> + activate: -> + return @activationDeferred.promise if @activationDeferred? + + @activationDeferred = Q.defer() @measure 'activateTime', => @activateResources() - if @metadata.activationEvents? and not immediate + if @metadata.activationEvents? @subscribeToActivationEvents() else @activateNow() + @activationDeferred.promise + activateNow: -> try @activateConfig() @@ -77,6 +79,8 @@ class AtomPackage extends Package catch e console.warn "Failed to activate package named '#{@name}'", e.stack + @activationDeferred.resolve() + activateConfig: -> return if @configActivated @@ -162,6 +166,8 @@ class AtomPackage extends Package console.error "Error serializing package '#{@name}'", e.stack deactivate: -> + @activationDeferred?.reject() + @activationDeferred = null @unsubscribeFromActivationEvents() @deactivateResources() @deactivateConfig() @@ -203,12 +209,6 @@ class AtomPackage extends Package path.join(@path, 'index') @mainModulePath = fs.resolveExtension(mainModulePath, ["", _.keys(require.extensions)...]) - registerDeferredDeserializers: -> - for deserializerName in @metadata.deferredDeserializers ? [] - atom.deserializers.addDeferred deserializerName, => - @activateStylesheets() - @requireMainModule() - subscribeToActivationEvents: -> return unless @metadata.activationEvents? if _.isArray(@metadata.activationEvents) @@ -226,6 +226,8 @@ class AtomPackage extends Package @unsubscribeFromActivationEvents() unsubscribeFromActivationEvents: -> + return unless atom.workspaceView? + if _.isArray(@metadata.activationEvents) atom.workspaceView.off(event, @handleActivationEvent) for event in @metadata.activationEvents else if _.isString(@metadata.activationEvents) diff --git a/src/atom.coffee b/src/atom.coffee index 3cc30c31e..0f951aebc 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -6,8 +6,6 @@ path = require 'path' remote = require 'remote' screen = require 'screen' shell = require 'shell' -dialog = remote.require 'dialog' -app = remote.require 'app' _ = require 'underscore-plus' {Model} = require 'theorist' @@ -22,16 +20,18 @@ WindowEventHandler = require './window-event-handler' # # ## Useful properties available: # -# * `atom.config` - A {Config} instance -# * `atom.contextMenu` - A {ContextMenuManager} instance -# * `atom.keymap` - A {Keymap} instance -# * `atom.menu` - A {MenuManager} instance -# * `atom.workspaceView` - A {WorkspaceView} instance -# * `atom.packages` - A {PackageManager} instance -# * `atom.pasteboard` - A {Pasteboard} instance -# * `atom.project` - A {Project} instance -# * `atom.syntax` - A {Syntax} instance -# * `atom.themes` - A {ThemeManager} instance +# * `atom.clipboard` - A {Clipboard} instance +# * `atom.config` - A {Config} instance +# * `atom.contextMenu` - A {ContextMenuManager} instance +# * `atom.deserializers` - A {DeserializerManager} instance +# * `atom.keymap` - A {Keymap} instance +# * `atom.menu` - A {MenuManager} instance +# * `atom.packages` - A {PackageManager} instance +# * `atom.project` - A {Project} instance +# * `atom.syntax` - A {Syntax} instance +# * `atom.themes` - A {ThemeManager} instance +# * `atom.workspace` - A {Workspace} instance +# * `atom.workspaceView` - A {WorkspaceView} instance module.exports = class Atom extends Model # Public: Load or create the Atom environment in the given mode. @@ -43,11 +43,11 @@ class Atom extends Model @loadOrCreate: (mode) -> @deserialize(@loadState(mode)) ? new this({mode, version: @getVersion()}) - # Private: Deserializes the Atom environment from a state object + # Deserializes the Atom environment from a state object @deserialize: (state) -> new this(state) if state?.version is @getVersion() - # Private: Loads and returns the serialized state corresponding to this window + # Loads and returns the serialized state corresponding to this window # if it exists; otherwise returns undefined. @loadState: (mode) -> statePath = @getStatePath(mode) @@ -65,7 +65,7 @@ class Atom extends Model catch error console.warn "Error parsing window state: #{statePath} #{error.stack}", error - # Private: Returns the path where the state for the current window will be + # Returns the path where the state for the current window will be # located if it exists. @getStatePath: (mode) -> switch mode @@ -82,41 +82,47 @@ class Atom extends Model else null - # Private: Get the directory path to Atom's configuration area. + # Get the directory path to Atom's configuration area. # # Returns the absolute path to ~/.atom @getConfigDirPath: -> @configDirPath ?= fs.absolute('~/.atom') - # Private: Get the path to Atom's storage directory. + # Get the path to Atom's storage directory. # # Returns the absolute path to ~/.atom/storage @getStorageDirPath: -> @storageDirPath ?= path.join(@getConfigDirPath(), 'storage') - # Private: Returns the load settings hash associated with the current window. + # Returns the load settings hash associated with the current window. @getLoadSettings: -> - _.deepClone(@loadSettings ?= _.deepClone(@getCurrentWindow().loadSettings)) + @loadSettings ?= JSON.parse(decodeURIComponent(location.search.substr(14))) + cloned = _.deepClone(@loadSettings) + # The loadSettings.windowState could be large, request it only when needed. + cloned.__defineGetter__ 'windowState', => + @getCurrentWindow().loadSettings.windowState + cloned.__defineSetter__ 'windowState', (value) => + @getCurrentWindow().loadSettings.windowState = value + cloned - # Private: @getCurrentWindow: -> remote.getCurrentWindow() - # Private: Get the version of the Atom application. + # Get the version of the Atom application. @getVersion: -> - @version ?= app.getVersion() + @version ?= @getLoadSettings().appVersion - # Private: Determine whether the current version is an official release. + # Determine whether the current version is an official release. @isReleasedVersion: -> not /\w{7}/.test(@getVersion()) # Check if the release is a 7-character SHA prefix workspaceViewParentSelector: 'body' - # Private: Call .loadOrCreate instead + # Call .loadOrCreate instead constructor: (@state) -> {@mode} = @state DeserializerManager = require './deserializer-manager' - @deserializers = new DeserializerManager(this) + @deserializers = new DeserializerManager() # Public: Sets up the basic services that should be available in all modes # (both spec and application). Call after this instance has been assigned to @@ -134,7 +140,7 @@ class Atom extends Model Config = require './config' Keymap = require './keymap' PackageManager = require './package-manager' - Pasteboard = require './pasteboard' + Clipboard = require './clipboard' Syntax = require './syntax' ThemeManager = require './theme-manager' ContextMenuManager = require './context-menu-manager' @@ -148,7 +154,8 @@ class Atom extends Model @themes = new ThemeManager({packageManager: @packages, configDirPath, resourcePath}) @contextMenu = new ContextMenuManager(devMode) @menu = new MenuManager({resourcePath}) - @pasteboard = new Pasteboard() + @clipboard = new Clipboard() + @syntax = @deserializers.deserialize(@state.syntax) ? new Syntax() @subscribe @packages, 'activated', => @watchThemes() @@ -167,7 +174,6 @@ class Atom extends Model # Deprecated: Callers should be converted to use atom.deserializers registerRepresentationClasses: -> - # Private: setBodyPlatformClass: -> document.body.classList.add("platform-#{process.platform}") @@ -196,15 +202,13 @@ class Atom extends Model # + width: The new width. # + height: The new height. setWindowDimensions: ({x, y, width, height}) -> - browserWindow = @getCurrentWindow() if width? and height? - browserWindow.setSize(width, height) + @setSize(width, height) if x? and y? - browserWindow.setPosition(x, y) + @setPosition(x, y) else - browserWindow.center() + @center() - # Private: restoreWindowDimensions: -> workAreaSize = screen.getPrimaryDisplay().workAreaSize windowDimensions = @state.windowDimensions ? {} @@ -213,7 +217,6 @@ class Atom extends Model windowDimensions.width ?= initialSize?.width ? Math.min(workAreaSize.width, 1024) @setWindowDimensions(windowDimensions) - # Private: storeWindowDimensions: -> @state.windowDimensions = @getWindowDimensions() @@ -223,12 +226,10 @@ class Atom extends Model getLoadSettings: -> @constructor.getLoadSettings() - # Private: deserializeProject: -> Project = require './project' @project ?= @deserializers.deserialize(@project) ? new Project(path: @getLoadSettings().initialPath) - # Private: deserializeWorkspaceView: -> Workspace = require './workspace' WorkspaceView = require './workspace-view' @@ -236,24 +237,22 @@ class Atom extends Model @workspaceView = new WorkspaceView(@workspace) $(@workspaceViewParentSelector).append(@workspaceView) - # Private: deserializePackageStates: -> @packages.packageStates = @state.packageStates ? {} delete @state.packageStates - # Private: deserializeEditorWindow: -> @deserializePackageStates() @deserializeProject() @deserializeWorkspaceView() - # Private: Call this method when establishing a real application window. + # Call this method when establishing a real application window. startEditorWindow: -> CommandInstaller = require './command-installer' resourcePath = atom.getLoadSettings().resourcePath - CommandInstaller.installAtomCommand resourcePath, (error) -> + CommandInstaller.installAtomCommand resourcePath, false, (error) -> console.warn error.message if error? - CommandInstaller.installApmCommand resourcePath, (error) -> + CommandInstaller.installApmCommand resourcePath, false, (error) -> console.warn error.message if error? @restoreWindowDimensions() @@ -276,7 +275,6 @@ class Atom extends Model @displayWindow() - # Private: unloadEditorWindow: -> return if not @project and not @workspaceView @@ -292,11 +290,9 @@ class Atom extends Model @keymap.destroy() @windowState = null - # Private: loadThemes: -> @themes.load() - # Private: watchThemes: -> @themes.on 'reloaded', => # Only reload stylesheets from non-theme packages @@ -309,31 +305,32 @@ class Atom extends Model # Calling this method without an options parameter will open a prompt to pick # a file/folder to open in the new window. # - # * options - # * pathsToOpen: A string array of paths to open + # options - An {Object} with the following keys: + # :pathsToOpen - An {Array} of {String} paths to open. open: (options) -> ipc.sendChannel('open', options) # Public: Open a confirm dialog. # - # ## Example: - # ```coffeescript + # ## Example + # + # ```coffee # atom.confirm - # message: 'How you feeling?' - # detailedMessage: 'Be honest.' - # buttons: - # Good: -> window.alert('good to hear') - # Bad: -> window.alert('bummer') + # message: 'How you feeling?' + # detailedMessage: 'Be honest.' + # buttons: + # Good: -> window.alert('good to hear') + # Bad: -> window.alert('bummer') # ``` # - # * options: - # + message: The string message to display. - # + detailedMessage: The string detailed message to display. - # + buttons: Either an array of strings or an object where the values - # are callbacks to invoke when clicked. + # options - An {Object} with the following keys: + # :message - The {String} message to display. + # :detailedMessage - The {String} detailed message to display. + # :buttons - Either an array of strings or an object where keys are + # button names and the values are callbacks to invoke when + # clicked. # - # Returns the chosen index if buttons was an array or the return of the - # callback if buttons was an object. + # Returns the chosen button index {Number} if the buttons option was an array. confirm: ({message, detailedMessage, buttons}={}) -> buttons ?= {} if _.isArray(buttons) @@ -341,6 +338,7 @@ class Atom extends Model else buttonLabels = Object.keys(buttons) + dialog = remote.require('dialog') chosen = dialog.showMessageBox @getCurrentWindow(), type: 'info' message: message @@ -353,42 +351,59 @@ class Atom extends Model callback = buttons[buttonLabels[chosen]] callback?() - # Private: showSaveDialog: (callback) -> callback(showSaveDialogSync()) - # Private: showSaveDialogSync: (defaultPath) -> defaultPath ?= @project?.getPath() currentWindow = @getCurrentWindow() + dialog = remote.require('dialog') dialog.showSaveDialog currentWindow, {title: 'Save File', defaultPath} # Public: Open the dev tools for the current window. openDevTools: -> - @getCurrentWindow().openDevTools() + ipc.sendChannel('call-window-method', 'openDevTools') # Public: Toggle the visibility of the dev tools for the current window. toggleDevTools: -> - @getCurrentWindow().toggleDevTools() + ipc.sendChannel('call-window-method', 'toggleDevTools') # Public: Reload the current window. reload: -> - @getCurrentWindow().restart() + ipc.sendChannel('call-window-method', 'restart') # Public: Focus the current window. focus: -> - @getCurrentWindow().focus() + ipc.sendChannel('call-window-method', 'focus') $(window).focus() # Public: Show the current window. show: -> - @getCurrentWindow().show() + ipc.sendChannel('call-window-method', 'show') # Public: Hide the current window. hide: -> - @getCurrentWindow().hide() + ipc.sendChannel('call-window-method', 'hide') - # Private: Schedule the window to be shown and focused on the next tick. + # Public: Set the size of current window. + # + # width - The {Number} of pixels. + # height - The {Number} of pixels. + setSize: (width, height) -> + ipc.sendChannel('call-window-method', 'setSize', width, height) + + # Public: Set the position of current window. + # + # x - The {Number} of pixels. + # y - The {Number} of pixels. + setPosition: (x, y) -> + ipc.sendChannel('call-window-method', 'setPosition', x, y) + + # Public: Move current window to the center of the screen. + center: -> + ipc.sendChannel('call-window-method', 'center') + + # Schedule the window to be shown and focused on the next tick. # # This is done in a next tick to prevent a white flicker from occurring # if called synchronously. @@ -402,8 +417,9 @@ class Atom extends Model close: -> @getCurrentWindow().close() - # Private: - exit: (status) -> app.exit(status) + exit: (status) -> + app = remote.require('app') + app.exit(status) # Public: Is the current window in development mode? inDevMode: -> @@ -419,7 +435,7 @@ class Atom extends Model # Public: Set the full screen state of the current window. setFullScreen: (fullScreen=false) -> - @getCurrentWindow().setFullScreen(fullScreen) + ipc.sendChannel('call-window-method', 'setFullScreen', fullScreen) # Public: Is the current window in full screen mode? isFullScreen: -> @@ -450,7 +466,6 @@ class Atom extends Model getConfigDirPath: -> @constructor.getConfigDirPath() - # Private: saveSync: -> stateString = JSON.stringify(@state) if statePath = @constructor.getStatePath(@mode) @@ -468,11 +483,9 @@ class Atom extends Model getWindowLoadTime: -> @loadTime - # Private: crashMainProcess: -> remote.process.crash() - # Private: crashRenderProcess: -> process.crash() @@ -481,9 +494,12 @@ class Atom extends Model shell.beep() if @config.get('core.audioBeep') @workspaceView.trigger 'beep' - # Private: + getUserInitScriptPath: -> + initScriptPath = fs.resolve(@getConfigDirPath(), 'init', ['js', 'coffee']) + initScriptPath ? path.join(@getConfigDirPath(), 'init.coffee') + requireUserInitScript: -> - if userInitScriptPath = fs.resolve(@getConfigDirPath(), 'user', ['js', 'coffee']) + if userInitScriptPath = @getUserInitScriptPath() try require userInitScriptPath catch error @@ -493,6 +509,9 @@ class Atom extends Model # # The globals will be set on the `window` object and removed after the # require completes. + # + # id - The {String} module name or path. + # globals - An {Object} to set as globals during require (default: {}) requireWithGlobals: (id, globals={}) -> existingGlobals = {} for key, value of globals diff --git a/src/browser/application-menu.coffee b/src/browser/application-menu.coffee index 7de362838..905655ac8 100644 --- a/src/browser/application-menu.coffee +++ b/src/browser/application-menu.coffee @@ -3,7 +3,7 @@ ipc = require 'ipc' Menu = require 'menu' _ = require 'underscore-plus' -# Private: Used to manage the global application menu. +# Used to manage the global application menu. # # It's created by {AtomApplication} upon instantiation and used to add, remove # and maintain the state of all menu items. @@ -29,7 +29,7 @@ class ApplicationMenu @menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(@menu) - # Private: Flattens the given menu and submenu items into an single Array. + # Flattens the given menu and submenu items into an single Array. # # * menu: # A complete menu configuration object for atom-shell's menu API. @@ -42,7 +42,7 @@ class ApplicationMenu items = items.concat(@flattenMenuItems(item.submenu)) if item.submenu items - # Private: Flattens the given menu template into an single Array. + # Flattens the given menu template into an single Array. # # * template: # An object describing the menu item. @@ -64,26 +64,22 @@ class ApplicationMenu for item in @flattenMenuItems(@menu) item.enabled = enable if item.metadata?['windowSpecific'] - # Private: Replaces VERSION with the current version. + # Replaces VERSION with the current version. substituteVersion: (template) -> if (item = _.find(@flattenMenuTemplate(template), (i) -> i.label == 'VERSION')) item.label = "Version #{@version}" - # Public: Makes the download menu item visible if available. - # - # Note: The update menu item's must match 'Install update' exactly otherwise - # this function will fail to work. - # - # * newVersion: - # FIXME: Unused. - # * quitAndUpdateCallback: - # Function to call when the install menu item has been clicked. - showDownloadUpdateItem: (newVersion, quitAndUpdateCallback) -> - if (item = _.find(@flattenMenuItems(@menu), (i) -> i.label == 'Install update')) - item.visible = true - item.click = quitAndUpdateCallback + # Toggles Install Update Item + showInstallUpdateItem: (visible=true) -> + if (item = _.find(@flattenMenuItems(@menu), (i) -> i.label == 'Restart and Install Update')) + item.visible = visible - # Private: Default list of menu items. + # Toggles Check For Update Item + showCheckForUpdateItem: (visible=true) -> + if (item = _.find(@flattenMenuItems(@menu), (i) -> i.label == 'Check for Update')) + item.visible = visible + + # Default list of menu items. # # Returns an Array of menu item Objects. getDefaultTemplate: -> @@ -97,7 +93,7 @@ class ApplicationMenu ] ] - # Private: Combines a menu template with the appropriate keystroke. + # Combines a menu template with the appropriate keystroke. # # * template: # An Object conforming to atom-shell's menu api but lacking accelerator and @@ -117,7 +113,7 @@ class ApplicationMenu @translateTemplate(item.submenu, keystrokesByCommand) if item.submenu template - # Private: Determine the accelerator for a given command. + # Determine the accelerator for a given command. # # * command: # The name of the command. diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index affabc0ae..68e864862 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -1,6 +1,7 @@ AtomWindow = require './atom-window' ApplicationMenu = require './application-menu' AtomProtocolHandler = require './atom-protocol-handler' +BrowserWindow = require 'browser-window' Menu = require 'menu' autoUpdater = require 'auto-updater' app = require 'app' @@ -21,7 +22,7 @@ socketPath = else path.join(os.tmpdir(), 'atom.sock') -# Private: The application's singleton class. +# The application's singleton class. # # It's the entry point into the Atom application and maintains the global state # of the application. @@ -72,11 +73,11 @@ class AtomApplication @listenForArgumentsFromNewProcess() @setupJavaScriptArguments() @handleEvents() - @checkForUpdates() + @setupAutoUpdater() @openWithOptions(options) - # Private: Opens a new window based on the options provided. + # Opens a new window based on the options provided. openWithOptions: ({pathsToOpen, urlsToOpen, test, pidToKillWhenClosed, devMode, newWindow, specDirectory, logFile}) -> if test @runSpecs({exitWhenDone: true, @resourcePath, specDirectory, logFile}) @@ -97,7 +98,7 @@ class AtomApplication @windows.push window @applicationMenu?.enableWindowSpecificItems(true) - # Private: Creates server to listen for additional atom application launches. + # Creates server to listen for additional atom application launches. # # You can run the atom command multiple times, but after the first launch # the other launches will just pass their information to this server and then @@ -112,23 +113,60 @@ class AtomApplication server.listen socketPath server.on 'error', (error) -> console.error 'Application server failed', error - # Private: Configures required javascript environment flags. + # Configures required javascript environment flags. setupJavaScriptArguments: -> app.commandLine.appendSwitch 'js-flags', '--harmony_collections --harmony-proxies' - # Private: Enable updates unless running from a local build of Atom. - checkForUpdates: -> - versionIsSha = /\w{7}/.test @version + # Enable updates unless running from a local build of Atom. + setupAutoUpdater: -> + autoUpdater.setFeedUrl "https://atom.io/api/updates?version=#{@version}" - if versionIsSha - autoUpdater.setAutomaticallyDownloadsUpdates false - autoUpdater.setAutomaticallyChecksForUpdates false - else - autoUpdater.setAutomaticallyDownloadsUpdates true - autoUpdater.setAutomaticallyChecksForUpdates true - autoUpdater.checkForUpdatesInBackground() + autoUpdater.on 'checking-for-update', => + @applicationMenu.showInstallUpdateItem(false) + @applicationMenu.showCheckForUpdateItem(false) - # Private: Registers basic application commands, non-idempotent. + autoUpdater.on 'update-not-available', => + @applicationMenu.showInstallUpdateItem(false) + @applicationMenu.showCheckForUpdateItem(true) + + autoUpdater.on 'update-downloaded', (event, releaseNotes, releaseName, releaseDate, releaseURL) => + atomWindow.sendCommand('window:update-available', releaseName) for atomWindow in @windows + @applicationMenu.showInstallUpdateItem(true) + @applicationMenu.showCheckForUpdateItem(false) + @updateVersion = releaseName + + autoUpdater.on 'error', (event, message) => + @applicationMenu.showInstallUpdateItem(false) + @applicationMenu.showCheckForUpdateItem(true) + + # Check for update after Atom has fully started and the menus are created + setTimeout((-> autoUpdater.checkForUpdates()), 5000) + + checkForUpdate: -> + autoUpdater.once 'update-available', -> + dialog.showMessageBox + type: 'info' + buttons: ['OK'] + message: 'Update available.' + detail: 'A new update is being downloading.' + + autoUpdater.once 'update-not-available', => + dialog.showMessageBox + type: 'info' + buttons: ['OK'] + message: 'No update available.' + detail: "Version #{@version} is the latest version." + + autoUpdater.once 'error', (event, message)-> + dialog.showMessageBox + type: 'warning' + buttons: ['OK'] + message: 'There was an error checking for updates.' + detail: message + + autoUpdater.checkForUpdates() + + # Registers basic application commands, non-idempotent. handleEvents: -> @on 'application:about', -> Menu.sendActionToFirstResponder('orderFrontStandardAboutPanel:') @on 'application:run-all-specs', -> @runSpecs(exitWhenDone: false, resourcePath: global.devResourcePath) @@ -147,9 +185,12 @@ class AtomApplication @on 'application:inspect', ({x,y}) -> @focusedWindow().browserWindow.inspectElement(x, y) @on 'application:open-documentation', -> shell.openExternal('https://www.atom.io/docs/latest/?app') @on 'application:report-issue', -> shell.openExternal('https://github.com/atom/atom/issues/new') + @on 'application:install-update', -> autoUpdater.quitAndInstall() + @on 'application:check-for-update', => @checkForUpdate() @openPathOnEvent('application:show-settings', 'atom://config') @openPathOnEvent('application:open-your-config', 'atom://.atom/config') + @openPathOnEvent('application:open-your-init-script', 'atom://.atom/init-script') @openPathOnEvent('application:open-your-keymap', 'atom://.atom/keymap') @openPathOnEvent('application:open-your-snippets', 'atom://.atom/snippets') @openPathOnEvent('application:open-your-stylesheet', 'atom://.atom/stylesheet') @@ -170,12 +211,6 @@ class AtomApplication event.preventDefault() @openUrl({urlToOpen, @devMode}) - autoUpdater.on 'ready-for-update-on-quit', (event, version, quitAndUpdateCallback) => - event.preventDefault() - @updateVersion = version - @applicationMenu.showDownloadUpdateItem(version, quitAndUpdateCallback) - atomWindow.sendCommand('window:update-available', version) for atomWindow in @windows - # A request from the associated render process to open a new render process. ipc.on 'open', (processId, routingId, options) => if options? @@ -195,6 +230,14 @@ class AtomApplication ipc.on 'command', (processId, routingId, command) => @emit(command) + ipc.on 'window-command', (processId, routingId, command, args...) -> + win = BrowserWindow.fromProcessIdAndRoutingId(processId, routingId) + win.emit(command, args...) + + ipc.on 'call-window-method', (processId, routingId, method, args...) -> + win = BrowserWindow.fromProcessIdAndRoutingId(processId, routingId) + win[method](args...) + # Public: Executes the given command. # # If it isn't handled globally, delegate to the currently focused window. @@ -221,7 +264,7 @@ class AtomApplication else @openPath({pathToOpen}) - # Private: Returns the {AtomWindow} for the given path. + # Returns the {AtomWindow} for the given path. windowForPath: (pathToOpen) -> for atomWindow in @windows return atomWindow if atomWindow.containsPath(pathToOpen) @@ -309,7 +352,7 @@ class AtomApplication console.log("Killing process #{pid} failed: #{error.code}") delete @pidsToOpenWindows[pid] - # Private: Open an atom:// url. + # Open an atom:// url. # # The host of the URL being opened is assumed to be the package name # responsible for opening the URL. A new window will be created with @@ -341,7 +384,7 @@ class AtomApplication else console.log "Opening unknown url: #{urlToOpen}" - # Private: Opens up a new {AtomWindow} to run specs within. + # Opens up a new {AtomWindow} to run specs within. # # * options # + exitWhenDone: @@ -372,7 +415,7 @@ class AtomApplication isSpec = true new AtomWindow({bootstrapScript, @resourcePath, isSpec}) - # Private: Opens a native dialog to prompt the user for a path. + # Opens a native dialog to prompt the user for a path. # # Once paths are selected, they're opened in a new or existing {AtomWindow}s. # diff --git a/src/browser/atom-protocol-handler.coffee b/src/browser/atom-protocol-handler.coffee index 247a67dcc..786d50243 100644 --- a/src/browser/atom-protocol-handler.coffee +++ b/src/browser/atom-protocol-handler.coffee @@ -3,7 +3,7 @@ fs = require 'fs-plus' path = require 'path' protocol = require 'protocol' -# Private: Handles requests with 'atom' protocol. +# Handles requests with 'atom' protocol. # # It's created by {AtomApplication} upon instantiation, and is used to create a # custom resource loader by adding the 'atom' custom protocol. @@ -18,7 +18,7 @@ class AtomProtocolHandler @registerAtomProtocol() - # Private: Creates the 'atom' custom protocol handler. + # Creates the 'atom' custom protocol handler. registerAtomProtocol: -> protocol.registerProtocol 'atom', (request) => relativePath = path.normalize(request.url.substr(7)) diff --git a/src/browser/atom-window.coffee b/src/browser/atom-window.coffee index 4ca0ab3ad..156db8f6e 100644 --- a/src/browser/atom-window.coffee +++ b/src/browser/atom-window.coffee @@ -1,13 +1,14 @@ BrowserWindow = require 'browser-window' Menu = require 'menu' ContextMenu = require './context-menu' +app = require 'app' dialog = require 'dialog' ipc = require 'ipc' path = require 'path' fs = require 'fs' +url = require 'url' _ = require 'underscore-plus' -# Private: module.exports = class AtomWindow @iconPath: path.resolve(__dirname, '..', '..', 'resources', 'atom.png') @@ -31,6 +32,7 @@ class AtomWindow loadSettings = _.extend({}, settings) loadSettings.windowState ?= '{}' + loadSettings.appVersion = app.getVersion() # Only send to the first non-spec window created if @constructor.includeShellLoadTime and not @isSpec @@ -43,7 +45,7 @@ class AtomWindow @browserWindow.loadSettings = loadSettings @browserWindow.once 'window:loaded', => @loaded = true - @browserWindow.loadUrl "file://#{@resourcePath}/static/index.html" + @browserWindow.loadUrl @getUrl(loadSettings) @browserWindow.focusOnWebView() if @isSpec @openPath(pathToOpen, initialLine) @@ -51,6 +53,18 @@ class AtomWindow setupNodePath: (resourcePath) -> process.env['NODE_PATH'] = path.resolve(resourcePath, 'exports') + getUrl: (loadSettingsObj) -> + # Ignore the windowState when passing loadSettings via URL, since it could + # be quite large. + loadSettings = _.clone(loadSettingsObj) + delete loadSettings['windowState'] + + url.format + protocol: 'file' + pathname: "#{@resourcePath}/static/index.html" + slashes: true + query: {loadSettings: JSON.stringify(loadSettings)} + getInitialPath: -> @browserWindow.loadSettings.initialPath diff --git a/src/browser/context-menu.coffee b/src/browser/context-menu.coffee index 85d3b0426..e3044b30d 100644 --- a/src/browser/context-menu.coffee +++ b/src/browser/context-menu.coffee @@ -1,6 +1,5 @@ Menu = require 'menu' -# Private: module.exports = class ContextMenu constructor: (template, browserWindow) -> @@ -8,7 +7,7 @@ class ContextMenu menu = Menu.buildFromTemplate(template) menu.popup(browserWindow) - # Private: It's necessary to build the event handlers in this process, otherwise + # It's necessary to build the event handlers in this process, otherwise # closures are drug across processes and failed to be garbage collected # appropriately. createClickHandlers: (template) -> diff --git a/src/browser/main.coffee b/src/browser/main.coffee index 5d4a66224..c72081cd7 100644 --- a/src/browser/main.coffee +++ b/src/browser/main.coffee @@ -1,6 +1,5 @@ global.shellStartTime = Date.now() -autoUpdater = require 'auto-updater' crashReporter = require 'crash-reporter' app = require 'app' fs = require 'fs' @@ -42,7 +41,6 @@ start = -> app.on 'will-finish-launching', -> setupCrashReporter() - setupAutoUpdater() app.on 'finish-launching', -> app.removeListener 'open-file', addPathToOpen @@ -66,9 +64,6 @@ global.devResourcePath = path.join(app.getHomeDir(), 'github', 'atom') setupCrashReporter = -> crashReporter.start(productName: 'Atom', companyName: 'GitHub') -setupAutoUpdater = -> - autoUpdater.setFeedUrl 'https://speakeasy.githubapp.com/apps/27/appcast.xml' - parseCommandLine = -> version = app.getVersion() options = optimist(process.argv[1..]) diff --git a/src/buffered-process.coffee b/src/buffered-process.coffee index b27c315be..ed0907f3f 100644 --- a/src/buffered-process.coffee +++ b/src/buffered-process.coffee @@ -12,30 +12,27 @@ class BufferedProcess process: null killed: false - # Executes the given executable. + # Public: Executes the given executable. # - # * options - # + command: - # The path to the executable to execute. - # + args: - # The array of arguments to pass to the script (optional). - # + options: - # The options Object to pass to Node's `ChildProcess.spawn` (optional). - # + stdout: - # The callback that receives a single argument which contains the - # standard output of the script. The callback is called as data is - # received but it's buffered to ensure only complete lines are passed - # until the source stream closes. After the source stream has closed - # all remaining data is sent in a final call (optional). - # + stderr: - # The callback that receives a single argument which contains the - # standard error of the script. The callback is called as data is - # received but it's buffered to ensure only complete lines are passed - # until the source stream closes. After the source stream has closed - # all remaining data is sent in a final call (optional). - # + exit: - # The callback which receives a single argument containing the exit - # status (optional). + # options - An {Object} with the following keys: + # :command - The {String} command to execute. + # :args - The {String}} of arguments to pass to the script (optional). + # :options - The options {Object} to pass to Node's `ChildProcess.spawn` + # (optional). + # :stdout - The callback that receives a single argument which contains the + # standard output of the script. The callback is called as data is + # received but it's buffered to ensure only complete lines are + # passed until the source stream closes. After the source stream + # has closed all remaining data is sent in a final call + # (optional). + # :stderr - The callback that receives a single argument which contains the + # standard error of the script. The callback is called as data is + # received but it's buffered to ensure only complete lines are + # passed until the source stream closes. After the source stream + # has closed all remaining data is sent in a final call + # (optional). + # :exit - The callback which receives a single argument containing the exit + # status (optional). constructor: ({command, args, options, stdout, stderr, exit}={}) -> options ?= {} @process = ChildProcess.spawn(command, args, options) @@ -68,7 +65,7 @@ class BufferedProcess processExited = true triggerExitCallback() - # Private: Helper method to pass data line by line. + # Helper method to pass data line by line. # # * stream: # The Stream to read from. @@ -93,7 +90,7 @@ class BufferedProcess onLines(buffered) if buffered.length > 0 onDone() - # Public: Terminates the process. + # Public: Terminate the process. kill: -> @killed = true @process.kill() diff --git a/src/clipboard.coffee b/src/clipboard.coffee new file mode 100644 index 000000000..82d75d2d4 --- /dev/null +++ b/src/clipboard.coffee @@ -0,0 +1,49 @@ +clipboard = require 'clipboard' +crypto = require 'crypto' + +# Public: Represents the clipboard used for copying and pasting in Atom. +# +# An instance of this class is always available as the `atom.clipboard` global. +module.exports = +class Clipboard + metadata: null + signatureForMetadata: null + + # Creates an `md5` hash of some text. + # + # text - A {String} to hash. + # + # Returns a hashed {String}. + md5: (text) -> + crypto.createHash('md5').update(text, 'utf8').digest('hex') + + # Public: Write the given text to the clipboard. + # + # The metadata associated with the text is available by calling + # {.readWithMetadata}. + # + # text - The {String} to store. + # metadata - The additional info to associate with the text. + write: (text, metadata) -> + @signatureForMetadata = @md5(text) + @metadata = metadata + clipboard.writeText(text) + + # Public: Read the text from the clipboard. + # + # Returns a {String}. + read: -> + clipboard.readText() + + # Public: Read the text from the clipboard and return both the text and the + # associated metadata. + # + # Returns an {Object} with the following keys: + # :text - The {String} clipboard text. + # :metadata - The metadata stored by an earlier call to {.write}. + readWithMetadata: -> + text = @read() + if @signatureForMetadata is @md5(text) + {text, @metadata} + else + {text} diff --git a/src/command-installer.coffee b/src/command-installer.coffee index a0374bf99..e59dbd05c 100644 --- a/src/command-installer.coffee +++ b/src/command-installer.coffee @@ -3,52 +3,55 @@ _ = require 'underscore-plus' async = require 'async' fs = require 'fs-plus' mkdirp = require 'mkdirp' +runas = require 'runas' symlinkCommand = (sourcePath, destinationPath, callback) -> - mkdirp path.dirname(destinationPath), (error) -> - if error? + fs.unlink destinationPath, (error) -> + if error? and error?.code != 'ENOENT' callback(error) else - fs.symlink sourcePath, destinationPath, (error) -> + mkdirp path.dirname(destinationPath), (error) -> if error? callback(error) else - fs.chmod(destinationPath, 0o755, callback) + fs.symlink sourcePath, destinationPath, (error) -> + if error? + callback(error) + else + fs.chmod(destinationPath, '755', callback) -unlinkCommand = (destinationPath, callback) -> - fs.unlink destinationPath, (error) -> - if error? and error.code isnt 'ENOENT' - callback(error) - else - callback() +symlinkCommandWithPrivilegeSync = (sourcePath, destinationPath) -> + if runas('/bin/rm', ['-f', destinationPath], admin: true) != 0 + throw new Error("Failed to remove '#{destinationPath}'") + + if runas('/bin/mkdir', ['-p', path.dirname(destinationPath)], admin: true) != 0 + throw new Error("Failed to create directory '#{destinationPath}'") + + if runas('/bin/ln', ['-s', sourcePath, destinationPath], admin: true) != 0 + throw new Error("Failed to symlink '#{sourcePath}' to '#{destinationPath}'") module.exports = getInstallDirectory: -> "/usr/local/bin" - install: (commandPath, callback) -> + install: (commandPath, askForPrivilege, callback) -> return unless process.platform is 'darwin' commandName = path.basename(commandPath, path.extname(commandPath)) - directory = @getInstallDirectory() - if fs.existsSync(directory) - destinationPath = path.join(directory, commandName) - unlinkCommand destinationPath, (error) => - if error? - error = new Error "Could not remove file at #{destinationPath}." if error - callback?(error) - else - symlinkCommand commandPath, destinationPath, (error) => - error = new Error "Failed to symlink #{commandPath} to #{destinationPath}." if error - callback?(error) - else - error = new Error "Directory '#{directory} doesn't exist." + destinationPath = path.join(@getInstallDirectory(), commandName) + symlinkCommand commandPath, destinationPath, (error) => + if askForPrivilege and error?.code is 'EACCES' + try + error = null + symlinkCommandWithPrivilegeSync(commandPath, destinationPath) + catch error + callback?(error) - installAtomCommand: (resourcePath, callback) -> + installAtomCommand: (resourcePath, askForPrivilege, callback) -> commandPath = path.join(resourcePath, 'atom.sh') - @install commandPath, callback + @install commandPath, askForPrivilege, callback - installApmCommand: (resourcePath, callback) -> + installApmCommand: (resourcePath, askForPrivilege, callback) -> commandPath = path.join(resourcePath, 'apm', 'node_modules', '.bin', 'apm') - @install commandPath, callback + @install commandPath, askForPrivilege, callback diff --git a/src/config-observer.coffee b/src/config-observer.coffee deleted file mode 100644 index 46d3c44dd..000000000 --- a/src/config-observer.coffee +++ /dev/null @@ -1,12 +0,0 @@ -Mixin = require 'mixto' - -module.exports = -class ConfigObserver extends Mixin - observeConfig: (keyPath, args...) -> - @configSubscriptions ?= {} - @configSubscriptions[keyPath] = atom.config.observe(keyPath, args...) - - unobserveConfig: -> - if @configSubscriptions? - subscription.off() for keyPath, subscription of @configSubscriptions - @configSubscriptions = null diff --git a/src/config.coffee b/src/config.coffee index 68a7b464f..775d051ee 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -8,15 +8,14 @@ pathWatcher = require 'pathwatcher' # Public: Used to access all of Atom's configuration details. # -# A global instance of this class is available to all plugins which can be -# referenced using `atom.config` +# An instance of this class is always available as the `atom.config` global. # -# ### Best practices +# ## Best practices # # * Create your own root keypath using your package's name. # * Don't depend on (or write to) configuration keys outside of your keypath. # -# ### Example +# ## Example # # ```coffeescript # atom.config.set('myplugin.key', 'value') @@ -27,18 +26,14 @@ module.exports = class Config Emitter.includeInto(this) - defaultSettings: null - settings: null - configFileHasErrors: null - - # Private: Created during initialization, available as `global.config` + # Created during initialization, available as `atom.config` constructor: ({@configDirPath, @resourcePath}={}) -> @defaultSettings = {} @settings = {} + @configFileHasErrors = false @configFilePath = fs.resolve(@configDirPath, 'config', ['json', 'cson']) @configFilePath ?= path.join(@configDirPath, 'config.cson') - # Private: initializeConfigDirectory: (done) -> return if fs.existsSync(@configDirPath) @@ -55,13 +50,11 @@ class Config queue.push({sourcePath, destinationPath}) fs.traverseTree(templateConfigDirPath, onConfigDirFile, (path) -> true) - # Private: load: -> @initializeConfigDirectory() @loadUserConfig() @observeUserConfig() - # Private: loadUserConfig: -> unless fs.existsSync(@configFilePath) fs.makeTreeSync(path.dirname(@configFilePath)) @@ -77,17 +70,14 @@ class Config console.error "Failed to load user config '#{@configFilePath}'", e.message console.error e.stack - # Private: observeUserConfig: -> @watchSubscription ?= pathWatcher.watch @configFilePath, (eventType) => @loadUserConfig() if eventType is 'change' and @watchSubscription? - # Private: unobserveUserConfig: -> @watchSubscription?.close() @watchSubscription = null - # Private: setDefaults: (keyPath, defaults) -> keys = keyPath.split('.') hash = @defaultSettings @@ -160,6 +150,14 @@ class Config toggle: (keyPath) -> @set(keyPath, !@get(keyPath)) + # Public: Restore the key path to its default value. + # + # keyPath - The {String} name of the key. + # + # Returns the new value. + restoreDefault: (keyPath) -> + @set(keyPath, _.valueForKeyPath(@defaultSettings, keyPath)) + # Public: Push the value to the array at the key path. # # keyPath - The {String} key path. @@ -205,6 +203,9 @@ class Config # options - An optional {Object} containing the `callNow` key. # callback - The {Function} that fires when the. It is given a single argument, `value`, # which is the new value of `keyPath`. + # + # Returns an {Object} with the following keys: + # :off - A {Function} that unobserves the `keyPath` with called. observe: (keyPath, options={}, callback) -> if _.isFunction(options) callback = options @@ -230,12 +231,10 @@ class Config unobserve: (keyPath) -> @off("updated.#{keyPath.replace(/\./, '-')}") - # Private: update: -> return if @configFileHasErrors @save() @emit 'updated' - # Private: save: -> CSON.writeFileSync(@configFilePath, @settings) diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index 2cbfe0364..40228156b 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -5,10 +5,10 @@ remote = require 'remote' # Public: Provides a registry for commands that you'd like to appear in the # context menu. # -# Should be accessed via `atom.contextMenu`. +# An instance of this class is always available as the `atom.contextMenu` +# global. module.exports = class ContextMenuManager - # Private: constructor: (@devMode=false) -> @definitions = {} @devModeDefinitions = {} @@ -24,11 +24,11 @@ class ContextMenuManager # Public: Creates menu definitions from the object specified by the menu # cson API. # - # * name: The path of the file that contains the menu definitions. - # * object: The 'context-menu' object specified in the menu cson API. - # * options: - # + devMode - Determines whether the entries should only be shown when - # the window is in dev mode. + # name - The path of the file that contains the menu definitions. + # object - The 'context-menu' object specified in the menu cson API. + # options - An {Object} with the following keys: + # :devMode - Determines whether the entries should only be shown when + # the window is in dev mode. # # Returns nothing. add: (name, object, {devMode}={}) -> @@ -36,20 +36,20 @@ class ContextMenuManager for label, command of items @addBySelector(selector, {label, command}, {devMode}) - # Private: Registers a command to be displayed when the relevant item is right + # Registers a command to be displayed when the relevant item is right # clicked. # - # * selector: The css selector for the active element which should include - # the given command in its context menu. - # * definition: The object containing keys which match the menu template API. - # * options: - # + devMode: Indicates whether this command should only appear while the - # editor is in dev mode. + # selector - The css selector for the active element which should include + # the given command in its context menu. + # definition - The object containing keys which match the menu template API. + # options - An {Object} with the following keys: + # :devMode - Indicates whether this command should only appear while the + # editor is in dev mode. addBySelector: (selector, definition, {devMode}={}) -> definitions = if devMode then @devModeDefinitions else @definitions (definitions[selector] ?= []).push(definition) - # Private: Returns definitions which match the element and devMode. + # Returns definitions which match the element and devMode. definitionsForElement: (element, {devMode}={}) -> definitions = if devMode then @devModeDefinitions else @definitions matchedDefinitions = [] @@ -58,14 +58,14 @@ class ContextMenuManager matchedDefinitions - # Private: Used to generate the context menu for a specific element and it's + # Used to generate the context menu for a specific element and it's # parents. # # The menu items are sorted such that menu items that match closest to the # active element are listed first. The further down the list you go, the higher # up the ancestor hierarchy they match. # - # * element: The DOM element to generate the menu template for. + # element - The DOM element to generate the menu template for. menuTemplateForMostSpecificElement: (element, {devMode}={}) -> menuTemplate = @definitionsForElement(element, {devMode}) if element.parentElement @@ -73,7 +73,7 @@ class ContextMenuManager else menuTemplate - # Private: Returns a menu template for both normal entries as well as + # Returns a menu template for both normal entries as well as # development mode entries. combinedMenuTemplateForElement: (element) -> normalItems = @menuTemplateForMostSpecificElement(element) @@ -83,7 +83,7 @@ class ContextMenuManager menuTemplate.push({ type: 'separator' }) if normalItems.length > 0 and devItems.length > 0 menuTemplate.concat(devItems) - # Private: Executes `executeAtBuild` if defined for each menu item with + # Executes `executeAtBuild` if defined for each menu item with # the provided event and then removes the `executeAtBuild` property from # the menu item. # diff --git a/src/cursor-view.coffee b/src/cursor-view.coffee index f76de9436..857e5e137 100644 --- a/src/cursor-view.coffee +++ b/src/cursor-view.coffee @@ -1,38 +1,49 @@ {View} = require './space-pen-extensions' -{Point, Range} = require 'text-buffer' _ = require 'underscore-plus' -### Internal ### module.exports = class CursorView extends View @content: -> @div class: 'cursor idle', => @raw ' ' - blinkPeriod: 800 - editorView: null - visible: true + @blinkPeriod: 800 + @blinkCursors: -> + element.classList.toggle('blink-off') for [element] in @cursorViews + + @startBlinking: (cursorView) -> + @cursorViews ?= [] + @cursorViews.push(cursorView) + if @cursorViews.length is 1 + @blinkInterval = setInterval(@blinkCursors.bind(this), @blinkPeriod / 2) + + @stopBlinking: (cursorView) -> + cursorView[0].classList.remove('blink-off') + _.remove(@cursorViews, cursorView) + clearInterval(@blinkInterval) if @cursorViews.length is 0 + + blinking: false + visible: true needsUpdate: true needsRemoval: false shouldPauseBlinking: false initialize: (@cursor, @editorView) -> - @cursor.on 'moved.cursor-view', => + @subscribe @cursor, 'moved', => @needsUpdate = true @shouldPauseBlinking = true - @cursor.on 'visibility-changed.cursor-view', (visible) => + @subscribe @cursor, 'visibility-changed', => @needsUpdate = true - @cursor.on 'autoscrolled.cursor-view', => + @subscribe @cursor, 'autoscrolled', => @editorView.requestDisplayUpdate() - @cursor.on 'destroyed.cursor-view', => + @subscribe @cursor, 'destroyed', => @needsRemoval = true beforeRemove: -> @editorView.removeCursorView(this) - @cursor.off('.cursor-view') @stopBlinking() updateDisplay: -> @@ -53,11 +64,7 @@ class CursorView extends View # Override for speed. The base function checks the computedStyle isHidden: -> - style = this[0].style - if style.display == 'none' or not @isOnDom() - true - else - false + this[0].style.display is 'none' or not @isOnDom() needsAutoscroll: -> @cursor.needsAutoscroll @@ -69,19 +76,17 @@ class CursorView extends View @editorView.pixelPositionForScreenPosition(@getScreenPosition()) setVisible: (visible) -> - unless @visible == visible + unless @visible is visible @visible = visible @toggle(@visible) stopBlinking: -> - clearInterval(@blinkInterval) if @blinkInterval - @blinkInterval = null - this[0].classList.remove('blink-off') + @constructor.stopBlinking(this) if @blinking + @blinking = false startBlinking: -> - return if @blinkInterval? - blink = => @toggleClass('blink-off') - @blinkInterval = setInterval(blink, @blinkPeriod / 2) + @constructor.startBlinking(this) unless @blinking + @blinking = true resetBlinking: -> @stopBlinking() diff --git a/src/cursor.coffee b/src/cursor.coffee index daf91e4dc..6b3747479 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -17,7 +17,7 @@ class Cursor visible: true needsAutoscroll: null - # Private: Instantiated by an {Editor} + # Instantiated by an {Editor} constructor: ({@editor, @marker}) -> @updateVisibility() @marker.on 'changed', (e) => @@ -45,11 +45,9 @@ class Cursor @emit 'destroyed' @needsAutoscroll = true - # Private: destroy: -> @marker.destroy() - # Private: changePosition: (options, fn) -> @clearSelection() @needsAutoscroll = options.autoscroll ? @isLastCursor() @@ -58,12 +56,11 @@ class Cursor # Public: Moves a cursor to a given screen position. # - # * screenPosition: - # An {Array} of two numbers: the screen row, and the screen column. - # * options: - # + autoscroll: - # A Boolean which, if `true`, scrolls the {Editor} to wherever the - # cursor moves to. + # screenPosition - An {Array} of two numbers: the screen row, and the screen + # column. + # options - An {Object} with the following keys: + # :autoscroll - A Boolean which, if `true`, scrolls the {Editor} to wherever + # the cursor moves to. setScreenPosition: (screenPosition, options={}) -> @changePosition options, => @marker.setHeadScreenPosition(screenPosition, options) @@ -74,12 +71,11 @@ class Cursor # Public: Moves a cursor to a given buffer position. # - # * bufferPosition: - # An {Array} of two numbers: the buffer row, and the buffer column. - # * options: - # + autoscroll: - # A Boolean which, if `true`, scrolls the {Editor} to wherever the - # cursor moves to. + # bufferPosition - An {Array} of two numbers: the buffer row, and the buffer + # column. + # options - An {Object} with the following keys: + # :autoscroll - A Boolean which, if `true`, scrolls the {Editor} to wherever + # the cursor moves to. setBufferPosition: (bufferPosition, options={}) -> @changePosition options, => @marker.setHeadBufferPosition(bufferPosition, options) @@ -104,11 +100,11 @@ class Cursor # Public: Get the RegExp used by the cursor to determine what a "word" is. # - # * options: - # + includeNonWordCharacters: - # A Boolean indicating whether to include non-word characters in the regex. + # options: An {Object} with the following keys: + # :includeNonWordCharacters - A {Boolean} indicating whether to include + # non-word characters in the regex. # - # Returns a RegExp. + # Returns a {RegExp}. wordRegExp: ({includeNonWordCharacters}={})-> includeNonWordCharacters ?= true nonWordCharacters = atom.config.get('editor.nonWordCharacters') @@ -122,7 +118,7 @@ class Cursor # # "Last" is defined as the most recently added cursor. # - # Returns a Boolean. + # Returns a {Boolean}. isLastCursor: -> this == @editor.getCursor() @@ -131,7 +127,7 @@ class Cursor # "Surrounded" here means that all characters before and after the cursor is # whitespace. # - # Returns a Boolean. + # Returns a {Boolean}. isSurroundedByWhitespace: -> {row, column} = @getBufferPosition() range = [[row, Math.min(0, column - 1)], [row, Math.max(0, column + 1)]] @@ -217,9 +213,9 @@ class Cursor # Public: Moves the cursor left one screen column. # - # * options: - # + moveToEndOfSelection: - # if true, move to the left of the selection if a selection exists. + # options - An {Object} with the following keys: + # :moveToEndOfSelection - if true, move to the left of the selection if a + # selection exists. moveLeft: ({moveToEndOfSelection}={}) -> range = @marker.getScreenRange() if moveToEndOfSelection and not range.isEmpty() @@ -231,9 +227,9 @@ class Cursor # Public: Moves the cursor right one screen column. # - # * options: - # + moveToEndOfSelection: - # if true, move to the right of the selection if a selection exists. + # options - An {Object} with the following keys: + # :moveToEndOfSelection - if true, move to the right of the selection if a + # selection exists. moveRight: ({moveToEndOfSelection}={}) -> range = @marker.getScreenRange() if moveToEndOfSelection and not range.isEmpty() @@ -313,12 +309,12 @@ class Cursor # Public: Retrieves the buffer position of where the current word starts. # - # * options: - # + wordRegex: - # A RegExp indicating what constitutes a "word" (default: {.wordRegExp}) - # + includeNonWordCharacters: - # A Boolean indicating whether to include non-word characters in the - # default word regex. Has no effect if wordRegex is set. + # options - An {Object} with the following keys: + # :wordRegex - A {RegExp} indicating what constitutes a "word" + # (default: {.wordRegExp}). + # :includeNonWordCharacters - A {Boolean} indicating whether to include + # non-word characters in the default word regex. + # Has no effect if wordRegex is set. # # Returns a {Range}. getBeginningOfCurrentWordBufferPosition: (options = {}) -> @@ -381,12 +377,12 @@ class Cursor # Public: Retrieves the buffer position of where the current word ends. # - # * options: - # + wordRegex: - # A RegExp indicating what constitutes a "word" (default: {.wordRegExp}) - # + includeNonWordCharacters: - # A Boolean indicating whether to include non-word characters in the - # default word regex. Has no effect if wordRegex is set. + # options - An {Object} with the following keys: + # :wordRegex - A {RegExp} indicating what constitutes a "word" + # (default: {.wordRegExp}) + # :includeNonWordCharacters - A Boolean indicating whether to include + # non-word characters in the default word regex. + # Has no effect if wordRegex is set. # # Returns a {Range}. getEndOfCurrentWordBufferPosition: (options = {}) -> @@ -405,9 +401,9 @@ class Cursor # Public: Retrieves the buffer position of where the next word starts. # - # * options: - # + wordRegex: - # A RegExp indicating what constitutes a "word" (default: {.wordRegExp}) + # options - + # :wordRegex - A {RegExp} indicating what constitutes a "word" + # (default: {.wordRegExp}). # # Returns a {Range}. getBeginningOfNextWordBufferPosition: (options = {}) -> @@ -424,9 +420,9 @@ class Cursor # Public: Returns the buffer Range occupied by the word located under the cursor. # - # * options: - # + wordRegex: - # A RegExp indicating what constitutes a "word" (default: {.wordRegExp}) + # options - + # :wordRegex - A {RegExp} indicating what constitutes a "word" + # (default: {.wordRegExp}). getCurrentWordBufferRange: (options={}) -> startOptions = _.extend(_.clone(options), allowPrevious: false) endOptions = _.extend(_.clone(options), allowNext: false) @@ -434,9 +430,9 @@ class Cursor # Public: Returns the buffer Range for the current line. # - # * options: - # + includeNewline: - # A boolean which controls whether the Range should include the newline. + # options - + # :includeNewline: - A {Boolean} which controls whether the Range should + # include the newline. getCurrentLineBufferRange: (options) -> @editor.bufferRangeForBufferRow(@getBufferRow(), options) diff --git a/src/deserializer-manager.coffee b/src/deserializer-manager.coffee index 60df24b0a..3c7cc2f5e 100644 --- a/src/deserializer-manager.coffee +++ b/src/deserializer-manager.coffee @@ -1,25 +1,39 @@ # Public: Manages the deserializers used for serialized state # -# Should be accessed via `atom.deserializers` +# An instance of this class is always available as the `atom.deserializers` +# global. +# +# ### Registering a deserializer +# +# ```coffee +# class MyPackageView extends View +# atom.deserializers.add(this) +# +# @deserialize: (state) -> +# new MyPackageView(state) +# ``` module.exports = class DeserializerManager - constructor: (@environment) -> + constructor: -> @deserializers = {} - @deferredDeserializers = {} # Public: Register the given class(es) as deserializers. - add: (klasses...) -> - @deserializers[klass.name] = klass for klass in klasses - - # Public: Add a deferred deserializer for the given class name. - addDeferred: (name, fn) -> - @deferredDeserializers[name] = fn + # + # classes - One or more classes to register. + add: (classes...) -> + @deserializers[klass.name] = klass for klass in classes # Public: Remove the given class(es) as deserializers. - remove: (klasses...) -> - delete @deserializers[klass.name] for klass in klasses + # + # classes - One or more classes to remove. + remove: (classes...) -> + delete @deserializers[name] for {name} in classes # Public: Deserialize the state and params. + # + # state - The state {Object} to deserialize. + # params - The params {Object} to pass as the second arguments to the + # deserialize method of the deserializer. deserialize: (state, params) -> return unless state? @@ -30,13 +44,11 @@ class DeserializerManager else console.warn "No deserializer found for", state - # Private: Get the deserializer for the state. + # Get the deserializer for the state. + # + # state - The state {Object} being deserialized. get: (state) -> return unless state? name = state.get?('deserializer') ? state.deserializer - if @deferredDeserializers[name] - @deferredDeserializers[name]() - delete @deferredDeserializers[name] - @deserializers[name] diff --git a/src/directory.coffee b/src/directory.coffee index 6404dc5ba..790de62f6 100644 --- a/src/directory.coffee +++ b/src/directory.coffee @@ -7,7 +7,7 @@ pathWatcher = require 'pathwatcher' File = require './file' -# Public: Represents a directory using {File}s. +# Public: Represents a directory on disk. # # ## Requiring in packages # @@ -18,15 +18,12 @@ module.exports = class Directory Emitter.includeInto(this) - path: null realPath: null # Public: Configures a new Directory instance, no files are accessed. # - # * path: - # A String containing the absolute path to the directory. - # + symlink: - # A Boolean indicating if the path is a symlink (defaults to false). + # path - A {String} containing the absolute path to the directory. + # symlink - A {Boolean} indicating if the path is a symlink (default: false). constructor: (@path, @symlink=false) -> @on 'first-contents-changed-subscription-will-be-added', => # Triggered by emissary, when a new contents-changed listener attaches @@ -36,7 +33,7 @@ class Directory # Triggered by emissary, when the last contents-changed listener detaches @unsubscribeFromNativeChangeEvents() - # Public: Returns the basename of the directory. + # Public: Returns the {String} basename of the directory. getBaseName: -> path.basename(@path) @@ -108,8 +105,8 @@ class Directory # Public: Reads file entries in this directory from disk asynchronously. # - # * callback: A function to call with an Error as the first argument and - # an {Array} of {File} and {Directory} objects as the second argument. + # callback - A {Function} to call with an {Error} as the 1st argument and + # an {Array} of {File} and {Directory} objects as the 2nd argument. getEntries: (callback) -> fs.list @path, (error, entries) -> return callback(error) if error? @@ -134,18 +131,16 @@ class Directory async.eachLimit entries, 1, statEntry, -> callback(null, directories.concat(files)) - # Private: subscribeToNativeChangeEvents: -> unless @watchSubscription? @watchSubscription = pathWatcher.watch @path, (eventType) => @emit "contents-changed" if eventType is "change" - # Private: unsubscribeFromNativeChangeEvents: -> if @watchSubscription? @watchSubscription.close() @watchSubscription = null - # Private: Does given full path start with the given prefix? + # Does given full path start with the given prefix? isPathPrefixOf: (prefix, fullPath) -> fullPath.indexOf(prefix) is 0 and fullPath[prefix.length] is path.sep diff --git a/src/display-buffer-marker.coffee b/src/display-buffer-marker.coffee index 8c08b3c9e..184162622 100644 --- a/src/display-buffer-marker.coffee +++ b/src/display-buffer-marker.coffee @@ -2,7 +2,6 @@ _ = require 'underscore-plus' {Emitter, Subscriber} = require 'emissary' -# Private: module.exports = class DisplayBufferMarker Emitter.includeInto(this) @@ -15,8 +14,6 @@ class DisplayBufferMarker oldTailScreenPosition: null wasValid: true - ### Internal ### - constructor: ({@bufferMarker, @displayBuffer}) -> @id = @bufferMarker.id @oldHeadBufferPosition = @getHeadBufferPosition() @@ -28,8 +25,6 @@ class DisplayBufferMarker @subscribe @bufferMarker, 'destroyed', => @destroyed() @subscribe @bufferMarker, 'changed', (event) => @notifyObservers(event) - ### Public ### - copy: (attributes) -> @displayBuffer.getMarker(@bufferMarker.copy(attributes).id) @@ -170,8 +165,6 @@ class DisplayBufferMarker inspect: -> "DisplayBufferMarker(id: #{@id}, bufferRange: #{@getBufferRange()})" - ### Internal ### - destroyed: -> delete @displayBuffer.markers[@id] @emit 'destroyed' diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 2891cba99..b5063a280 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -1,5 +1,5 @@ _ = require 'underscore-plus' -{Emitter, Subscriber} = require 'emissary' +{Emitter} = require 'emissary' guid = require 'guid' Serializable = require 'serializable' {Model} = require 'theorist' @@ -9,13 +9,10 @@ RowMap = require './row-map' Fold = require './fold' Token = require './token' DisplayBufferMarker = require './display-buffer-marker' -ConfigObserver = require './config-observer' -# Private: module.exports = class DisplayBuffer extends Model Serializable.includeInto(this) - ConfigObserver.includeInto(this) @properties softWrap: null @@ -39,10 +36,10 @@ class DisplayBuffer extends Model @emit 'soft-wrap-changed', softWrap @updateWrappedScreenLines() - @observeConfig 'editor.preferredLineLength', callNow: false, => + @subscribe atom.config.observe 'editor.preferredLineLength', callNow: false, => @updateWrappedScreenLines() if @softWrap and atom.config.get('editor.softWrapAtPreferredLineLength') - @observeConfig 'editor.softWrapAtPreferredLineLength', callNow: false, => + @subscribe atom.config.observe 'editor.softWrapAtPreferredLineLength', callNow: false, => @updateWrappedScreenLines() if @softWrap serializeParams: -> @@ -82,8 +79,6 @@ class DisplayBuffer extends Model bufferDelta = 0 @emitChanged({ start, end, screenDelta, bufferDelta }) - ### Public ### - # Sets the visibility of the tokenized buffer. # # visible - A {Boolean} indicating of the tokenized buffer is shown @@ -419,8 +414,6 @@ class DisplayBuffer extends Model column = screenLine.clipScreenColumn(column, options) new Point(row, column) - ### Public ### - # Given a line, finds the point where it would wrap. # # line - The {String} to check @@ -470,7 +463,7 @@ class DisplayBuffer extends Model getMarkerCount: -> @buffer.getMarkerCount() - # Constructs a new marker at the given screen range. + # Public: Constructs a new marker at the given screen range. # # range - The marker {Range} (representing the distance between the head and tail) # options - Options to pass to the {Marker} constructor @@ -480,7 +473,7 @@ class DisplayBuffer extends Model bufferRange = @bufferRangeForScreenRange(args.shift()) @markBufferRange(bufferRange, args...) - # Constructs a new marker at the given buffer range. + # Public: Constructs a new marker at the given buffer range. # # range - The marker {Range} (representing the distance between the head and tail) # options - Options to pass to the {Marker} constructor @@ -489,7 +482,7 @@ class DisplayBuffer extends Model markBufferRange: (args...) -> @getMarker(@buffer.markRange(args...).id) - # Constructs a new marker at the given screen position. + # Public: Constructs a new marker at the given screen position. # # range - The marker {Range} (representing the distance between the head and tail) # options - Options to pass to the {Marker} constructor @@ -498,7 +491,7 @@ class DisplayBuffer extends Model markScreenPosition: (screenPosition, options) -> @markBufferPosition(@bufferPositionForScreenPosition(screenPosition), options) - # Constructs a new marker at the given buffer position. + # Public: Constructs a new marker at the given buffer position. # # range - The marker {Range} (representing the distance between the head and tail) # options - Options to pass to the {Marker} constructor @@ -507,7 +500,7 @@ class DisplayBuffer extends Model markBufferPosition: (bufferPosition, options) -> @getMarker(@buffer.markPosition(bufferPosition, options).id) - # Removes the marker with the given id. + # Public: Removes the marker with the given id. # # id - The {Number} of the ID to remove destroyMarker: (id) -> @@ -573,15 +566,12 @@ class DisplayBuffer extends Model marker.unsubscribe() for marker in @getMarkers() @tokenizedBuffer.destroy() @unsubscribe() - @unobserveConfig() logLines: (start=0, end=@getLastRow())-> for row in [start..end] line = @lineForRow(row).text console.log row, @bufferRowForScreenRow(row), line, line.length - ### Internal ### - handleTokenizedBufferChange: (tokenizedBufferChange) => {start, end, delta, bufferChange} = tokenizedBufferChange @updateScreenLines(start, end + 1, delta, delayChangeEvent: bufferChange?) diff --git a/src/editor-view.coffee b/src/editor-view.coffee index c3754070b..b64f41379 100644 --- a/src/editor-view.coffee +++ b/src/editor-view.coffee @@ -26,6 +26,7 @@ module.exports = class EditorView extends View @characterWidthCache: {} @configDefaults: + fontFamily: '' fontSize: 20 showInvisibles: false showIndentGuide: false @@ -41,8 +42,6 @@ class EditorView extends View @nextEditorId: 1 - ### Internal ### - @content: (params) -> attributes = { class: @classes(params), tabindex: -1 } _.extend(attributes, params.attributes) if params.attributes @@ -79,14 +78,12 @@ class EditorView extends View redrawOnReattach: false bottomPaddingInLines: 10 - ### Public ### - # The constructor for setting up an `EditorView` instance. # # editorOrOptions - Either an {Editor}, or an object with one property, `mini`. - # If `mini` is `true`, a "miniature" `Editor` is constructed. - # Typically, this is ideal for scenarios where you need an Atom editor, - # but without all the chrome, like scrollbars, gutter, _e.t.c._. + # If `mini` is `true`, a "miniature" `Editor` is constructed. + # Typically, this is ideal for scenarios where you need an Atom editor, + # but without all the chrome, like scrollbars, gutter, _e.t.c._. # initialize: (editorOrOptions) -> if editorOrOptions instanceof Editor @@ -120,7 +117,7 @@ class EditorView extends View else throw new Error("Must supply an Editor or mini: true") - # Internal: Sets up the core Atom commands. + # Sets up the core Atom commands. # # Some commands are excluded from mini-editors. bindKeys: -> @@ -209,7 +206,7 @@ class EditorView extends View 'editor:toggle-line-comments': => @toggleLineCommentsInSelection() 'editor:log-cursor-scope': => @logCursorScope() 'editor:checkout-head-revision': => @checkoutHead() - 'editor:copy-path': => @copyPathToPasteboard() + 'editor:copy-path': => @copyPathToClipboard() 'editor:move-line-up': => @editor.moveLineUp() 'editor:move-line-down': => @editor.moveLineDown() 'editor:duplicate-line': => @editor.duplicateLine() @@ -223,6 +220,9 @@ class EditorView extends View do (name, method) => @command name, (e) -> method(e); false + # Public: Get the underlying editor model for this view. + # + # Returns an {Editor}. getEditor: -> @editor @@ -238,7 +238,6 @@ class EditorView extends View insertText: (text, options) -> @editor.insertText(text, options) - # Private: setHeightInLines: (heightInLines)-> heightInLines ?= @calculateHeightInLines() @heightInLines = heightInLines if heightInLines @@ -248,39 +247,41 @@ class EditorView extends View widthInChars ?= @calculateWidthInChars() @editor.setEditorWidthInChars(widthInChars) if widthInChars - # Public: Emulates the "page down" key, where the last row of a buffer scrolls to become the first. + # Public: Emulates the "page down" key, where the last row of a buffer scrolls + # to become the first. pageDown: -> newScrollTop = @scrollTop() + @scrollView[0].clientHeight @editor.moveCursorDown(@getPageRows()) @scrollTop(newScrollTop, adjustVerticalScrollbar: true) - # Public: Emulates the "page up" key, where the frst row of a buffer scrolls to become the last. + # Public: Emulates the "page up" key, where the frst row of a buffer scrolls + # to become the last. pageUp: -> newScrollTop = @scrollTop() - @scrollView[0].clientHeight @editor.moveCursorUp(@getPageRows()) @scrollTop(newScrollTop, adjustVerticalScrollbar: true) - # Gets the number of actual page rows existing in an editor. + # Public: Gets the number of actual page rows existing in an editor. # # Returns a {Number}. getPageRows: -> Math.max(1, Math.ceil(@scrollView[0].clientHeight / @lineHeight)) - # Set whether invisible characters are shown. + # Public: Set whether invisible characters are shown. # - # showInvisibles - A {Boolean} which, if `true`, show invisible characters + # showInvisibles - A {Boolean} which, if `true`, show invisible characters. setShowInvisibles: (showInvisibles) -> return if showInvisibles == @showInvisibles @showInvisibles = showInvisibles @resetDisplay() - # Defines which characters are invisible. + # Public: Defines which characters are invisible. # - # invisibles - A hash defining the invisible characters: The defaults are: - # eol: `\u00ac` - # space: `\u00b7` - # tab: `\u00bb` - # cr: `\u00a4` + # invisibles - An {Object} defining the invisible characters: + # :eol - The end of line invisible {String} (default: `\u00ac`). + # :space - The space invisible {String} (default: `\u00b7`). + # :tab - The tab invisible {String} (default: `\u00bb`). + # :cr - The carriage return invisible {String} (default: `\u00a4`). setInvisibles: (@invisibles={}) -> _.defaults @invisibles, eol: '\u00ac' @@ -289,14 +290,20 @@ class EditorView extends View cr: '\u00a4' @resetDisplay() - # Sets whether you want to show the indentation guides. + # Public: Sets whether you want to show the indentation guides. # - # showIndentGuide - A {Boolean} you can set to `true` if you want to see the indentation guides. + # showIndentGuide - A {Boolean} you can set to `true` if you want to see the + # indentation guides. setShowIndentGuide: (showIndentGuide) -> return if showIndentGuide == @showIndentGuide @showIndentGuide = showIndentGuide @resetDisplay() + # Public: Set the text to appear in the editor when it is empty. + # + # This only affects mini editors. + # + # placeholderText - A {String} of text to display when empty. setPlaceholderText: (placeholderText) -> return unless @mini @placeholderText = placeholderText @@ -310,15 +317,13 @@ class EditorView extends View if path = @editor.getPath() atom.project.getRepo()?.checkoutHead(path) - ### Internal ### - configure: -> - @observeConfig 'editor.showLineNumbers', (showLineNumbers) => @gutter.setShowLineNumbers(showLineNumbers) - @observeConfig 'editor.showInvisibles', (showInvisibles) => @setShowInvisibles(showInvisibles) - @observeConfig 'editor.showIndentGuide', (showIndentGuide) => @setShowIndentGuide(showIndentGuide) - @observeConfig 'editor.invisibles', (invisibles) => @setInvisibles(invisibles) - @observeConfig 'editor.fontSize', (fontSize) => @setFontSize(fontSize) - @observeConfig 'editor.fontFamily', (fontFamily) => @setFontFamily(fontFamily) + @subscribe atom.config.observe 'editor.showLineNumbers', (showLineNumbers) => @gutter.setShowLineNumbers(showLineNumbers) + @subscribe atom.config.observe 'editor.showInvisibles', (showInvisibles) => @setShowInvisibles(showInvisibles) + @subscribe atom.config.observe 'editor.showIndentGuide', (showIndentGuide) => @setShowIndentGuide(showIndentGuide) + @subscribe atom.config.observe 'editor.invisibles', (invisibles) => @setInvisibles(invisibles) + @subscribe atom.config.observe 'editor.fontSize', (fontSize) => @setFontSize(fontSize) + @subscribe atom.config.observe 'editor.fontFamily', (fontFamily) => @setFontFamily(fontFamily) handleEvents: -> @on 'focus', => @@ -488,12 +493,11 @@ class EditorView extends View @trigger 'editor:attached', [this] - # TODO: This should be private and only called from the constructor edit: (editor) -> return if editor is @editor if @editor - @saveScrollPositionForeditor() + @saveScrollPositionForEditor() @editor.off(".editor") @editor = editor @@ -590,19 +594,18 @@ class EditorView extends View else @scrollView.scrollRight() - ### Public ### - - # Scrolls the editor to the bottom. + # Public: Scrolls the editor to the bottom. scrollToBottom: -> @scrollBottom(@editor.getScreenLineCount() * @lineHeight) - # Scrolls the editor to the position of the most recently added cursor. + # Public: Scrolls the editor to the position of the most recently added + # cursor. # # The editor is also centered. scrollToCursorPosition: -> @scrollToBufferPosition(@editor.getCursorBufferPosition(), center: true) - # Scrolls the editor to the given buffer position. + # Public: Scrolls the editor to the given buffer position. # # bufferPosition - An object that represents a buffer position. It can be either # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} @@ -610,7 +613,7 @@ class EditorView extends View scrollToBufferPosition: (bufferPosition, options) -> @scrollToPixelPosition(@pixelPositionForBufferPosition(bufferPosition), options) - # Scrolls the editor to the given screen position. + # Public: Scrolls the editor to the given screen position. # # screenPosition - An object that represents a buffer position. It can be either # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} @@ -618,18 +621,20 @@ class EditorView extends View scrollToScreenPosition: (screenPosition, options) -> @scrollToPixelPosition(@pixelPositionForScreenPosition(screenPosition), options) - # Scrolls the editor to the given pixel position. + # Public: Scrolls the editor to the given pixel position. # # pixelPosition - An object that represents a pixel position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or + # {Point}. # options - A hash with the following keys: - # center: if `true`, the position is scrolled such that it's in the center of the editor + # :center - if `true`, the position is scrolled such that it's in + # the center of the editor scrollToPixelPosition: (pixelPosition, options) -> return unless @attached @scrollVertically(pixelPosition, options) @scrollHorizontally(pixelPosition) - # Highlight all the folds within the given buffer range. + # Public: Highlight all the folds within the given buffer range. # # "Highlighting" essentially just adds the `fold-selected` class to the line's # DOM element. @@ -647,29 +652,27 @@ class EditorView extends View else element.removeClass('fold-selected') - saveScrollPositionForeditor: -> + saveScrollPositionForEditor: -> if @attached @editor.setScrollTop(@scrollTop()) @editor.setScrollLeft(@scrollLeft()) - # Toggle soft tabs on the edit session. + # Public: Toggle soft tabs on the edit session. toggleSoftTabs: -> @editor.setSoftTabs(not @editor.getSoftTabs()) - # Toggle soft wrap on the edit session. + # Public: Toggle soft wrap on the edit session. toggleSoftWrap: -> @setWidthInChars() @editor.setSoftWrap(not @editor.getSoftWrap()) - # Private: calculateWidthInChars: -> Math.floor(@scrollView.width() / @charWidth) - # Private: calculateHeightInLines: -> Math.ceil($(window).height() / @lineHeight) - # Enables/disables soft wrap on the editor. + # Public: Enables/disables soft wrap on the editor. # # softWrap - A {Boolean} which, if `true`, enables soft wrap setSoftWrap: (softWrap) -> @@ -679,7 +682,7 @@ class EditorView extends View else @removeClass 'soft-wrap' - # Sets the font size for the editor. + # Public: Sets the font size for the editor. # # fontSize - A {Number} indicating the font size in pixels. setFontSize: (fontSize) -> @@ -692,15 +695,15 @@ class EditorView extends View else @redrawOnReattach = @attached - # Retrieves the font size for the editor. + # Public: Retrieves the font size for the editor. # # Returns a {Number} indicating the font size in pixels. getFontSize: -> parseInt(@css("font-size")) - # Sets the font family for the editor. + # Public: Sets the font family for the editor. # - # fontFamily - A {String} identifying the CSS `font-family`, + # fontFamily - A {String} identifying the CSS `font-family`. setFontFamily: (fontFamily='') -> @css('font-family', fontFamily) @@ -708,12 +711,12 @@ class EditorView extends View @redraw() - # Gets the font family for the editor. + # Public: Gets the font family for the editor. # - # Returns a {String} identifying the CSS `font-family`, + # Returns a {String} identifying the CSS `font-family`. getFontFamily: -> @css("font-family") - # Redraw the editor + # Public: Redraw the editor redraw: -> return unless @hasParent() return unless @attached @@ -723,23 +726,27 @@ class EditorView extends View @updateLayerDimensions() @requestDisplayUpdate() + # Public: Split the editor view left. splitLeft: -> pane = @getPane() pane?.splitLeft(pane?.copyActiveItem()).activeView + # Public: Split the editor view right. splitRight: -> pane = @getPane() pane?.splitRight(pane?.copyActiveItem()).activeView + # Public: Split the editor view up. splitUp: -> pane = @getPane() pane?.splitUp(pane?.copyActiveItem()).activeView + # Public: Split the editor view down. splitDown: -> pane = @getPane() pane?.splitDown(pane?.copyActiveItem()).activeView - # Retrieve's the `EditorView`'s pane. + # Public: Get this view's pane. # # Returns a {Pane}. getPane: -> @@ -750,7 +757,6 @@ class EditorView extends View super atom.workspaceView?.focus() - # Private: beforeRemove: -> @trigger 'editor:will-be-removed' @removed = true @@ -797,8 +803,6 @@ class EditorView extends View appendToLinesView: (view) -> @overlayer.append(view) - ### Internal ### - # Scrolls the editor vertically to a given position. scrollVertically: (pixelPosition, {center}={}) -> scrollViewHeight = @scrollView.height() @@ -835,7 +839,7 @@ class EditorView extends View @scrollRight(desiredRight) else if desiredLeft < @scrollLeft() @scrollLeft(desiredLeft) - @saveScrollPositionForeditor() + @saveScrollPositionForEditor() calculateDimensions: -> fragment = $('') @@ -1135,9 +1139,8 @@ class EditorView extends View @renderedLines.css('padding-bottom', paddingBottom) @gutter.lineNumbers.css('padding-bottom', paddingBottom) - ### Public ### - - # Retrieves the number of the row that is visible and currently at the top of the editor. + # Public: Retrieves the number of the row that is visible and currently at the + # top of the editor. # # Returns a {Number}. getFirstVisibleScreenRow: -> @@ -1145,7 +1148,8 @@ class EditorView extends View screenRow = 0 if isNaN(screenRow) screenRow - # Retrieves the number of the row that is visible and currently at the bottom of the editor. + # Public: Retrieves the number of the row that is visible and currently at the + # bottom of the editor. # # Returns a {Number}. getLastVisibleScreenRow: -> @@ -1154,7 +1158,7 @@ class EditorView extends View screenRow = 0 if isNaN(screenRow) screenRow - # Given a row number, identifies if it is currently visible. + # Public: Given a row number, identifies if it is currently visible. # # row - A row {Number} to check # @@ -1162,8 +1166,6 @@ class EditorView extends View isScreenRowVisible: (row) -> @getFirstVisibleScreenRow() <= row <= @getLastVisibleScreenRow() - ### Internal ### - handleScreenLinesChange: (change) -> @pendingChanges.push(change) @requestDisplayUpdate() @@ -1246,21 +1248,19 @@ class EditorView extends View toggleLineCommentsInSelection: -> @editor.toggleLineCommentsInSelection() - ### Public ### - - # Converts a buffer position to a pixel position. + # Public: Converts a buffer position to a pixel position. # # position - An object that represents a buffer position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} # # Returns an object with two values: `top` and `left`, representing the pixel positions. pixelPositionForBufferPosition: (position) -> @pixelPositionForScreenPosition(@editor.screenPositionForBufferPosition(position)) - # Converts a screen position to a pixel position. + # Public: Converts a screen position to a pixel position. # # position - An object that represents a screen position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} # # Returns an object with two values: `top` and `left`, representing the pixel positions. pixelPositionForScreenPosition: (position) -> @@ -1297,7 +1297,6 @@ class EditorView extends View index++ left - # Private: measureToColumn: (lineElement, tokenizedLine, screenColumn) -> left = oldLeft = index = 0 iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, TextNodeFilter) @@ -1343,7 +1342,6 @@ class EditorView extends View returnLeft ? left - # Private: getCharacterWidthCache: (scopes, char) -> scopes ?= NoScope obj = @constructor.characterWidthCache @@ -1352,7 +1350,6 @@ class EditorView extends View return null unless obj? obj[char] - # Private: setCharacterWidthCache: (scopes, char, val) -> scopes ?= NoScope obj = @constructor.characterWidthCache @@ -1361,7 +1358,6 @@ class EditorView extends View obj = obj[scope] obj[char] = val - # Private: clearCharacterWidthCache: -> @constructor.characterWidthCache = {} @@ -1411,11 +1407,9 @@ class EditorView extends View @highlightedLine = null # Copies the current file path to the native clipboard. - copyPathToPasteboard: -> + copyPathToClipboard: -> path = @editor.getPath() - atom.pasteboard.write(path) if path? - - ### Internal ### + atom.clipboard.write(path) if path? @buildLineHtml: ({tokens, text, lineEnding, fold, isSoftWrapped, invisibles, eolInvisibles, htmlEolInvisibles, attributes, showIndentGuide, indentation, editor, mini}) -> scopeStack = [] @@ -1426,7 +1420,7 @@ class EditorView extends View line.push("
") if text == '' - html = EditorView.buildEmptyLineHtml(showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, editor, mini) + html = @buildEmptyLineHtml(showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, editor, mini) line.push(html) if html else firstNonWhitespacePosition = text.search(/\S/) diff --git a/src/editor.coffee b/src/editor.coffee index 74d1b1505..633604e54 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -78,7 +78,7 @@ class Editor extends Model position = [0, 0] @addCursorAtBufferPosition(position) - @languageMode = new LanguageMode(this, @buffer.getExtension()) + @languageMode = new LanguageMode(this) @subscribe @$scrollTop, (scrollTop) => @emit 'scroll-top-changed', scrollTop @subscribe @$scrollLeft, (scrollLeft) => @emit 'scroll-left-changed', scrollLeft @@ -97,7 +97,6 @@ class Editor extends Model params.registerEditor = true params - # Private: subscribeToBuffer: -> @buffer.retain() @subscribe @buffer, "path-changed", => @@ -111,7 +110,6 @@ class Editor extends Model @subscribe @buffer, "destroyed", => @destroy() @preserveCursorPositionOnBufferReload() - # Private: subscribeToDisplayBuffer: -> @subscribe @displayBuffer, 'marker-created', @handleMarkerCreated @subscribe @displayBuffer, "changed", (e) => @emit 'screen-lines-changed', e @@ -119,11 +117,9 @@ class Editor extends Model @subscribe @displayBuffer, 'grammar-changed', => @handleGrammarChange() @subscribe @displayBuffer, 'soft-wrap-changed', (args...) => @emit 'soft-wrap-changed', args... - # Private: getViewClass: -> require './editor-view' - # Private: destroyed: -> @unsubscribe() selection.destroy() for selection in @getSelections() @@ -132,7 +128,7 @@ class Editor extends Model @languageMode.destroy() atom.project?.removeEditor(this) - # Private: Creates an {Editor} with the same initial state + # Creates an {Editor} with the same initial state copy: -> tabLength = @getTabLength() displayBuffer = @displayBuffer.copy() @@ -238,15 +234,14 @@ class Editor extends Model # Public: Given a position, this clips it to a real position. # - # For example, if `position`'s row exceeds the row count of the buffer, + # For example, if `bufferPosition`'s row exceeds the row count of the buffer, # or if its column goes beyond a line's length, this "sanitizes" the value # to a real position. # - # * position: - # The {Point} to clip + # bufferPosition - The {Point} to clip. # # Returns the new, clipped {Point}. Note that this could be the same as - # `position` if no clipping was performed. + # `bufferPosition` if no clipping was performed. clipBufferPosition: (bufferPosition) -> @buffer.clipPosition(bufferPosition) # Public: Given a range, this clips it to a real range. @@ -255,8 +250,7 @@ class Editor extends Model # or if its column goes beyond a line's length, this "sanitizes" the value # to a real range. # - # * range: - # The {Range} to clip + # range - The {Range} to clip. # # Returns the new, clipped {Range}. Note that this could be the same as # `range` if no clipping was performed. @@ -264,17 +258,14 @@ class Editor extends Model # Public: Returns the indentation level of the given a buffer row # - # * bufferRow: - # A Number indicating the buffer row. + # bufferRow - A {Number} indicating the buffer row. indentationForBufferRow: (bufferRow) -> @indentLevelForLine(@lineForBufferRow(bufferRow)) # Public: Sets the indentation level for the given buffer row. # - # * bufferRow: - # A {Number} indicating the buffer row. - # * newLevel: - # A {Number} indicating the new indentation level. + # bufferRow - A {Number} indicating the buffer row. + # newLevel - A {Number} indicating the new indentation level. setIndentationForBufferRow: (bufferRow, newLevel) -> currentIndentLength = @lineForBufferRow(bufferRow).match(/^\s*/)[0].length newIndentString = @buildIndentString(newLevel) @@ -282,8 +273,7 @@ class Editor extends Model # Public: Returns the indentation level of the given line of text. # - # * line: - # A {String} in the current buffer. + # line - A {String} in the current buffer. # # Returns a {Number} or 0 if the text isn't found within the buffer. indentLevelForLine: (line) -> @@ -295,7 +285,7 @@ class Editor extends Model else 0 - # Private: Constructs the string used for tabs. + # Constructs the string used for tabs. buildIndentString: (number) -> if @getSoftTabs() _.multiplyString(" ", number * @getTabLength()) @@ -308,9 +298,6 @@ class Editor extends Model # {Delegates to: TextBuffer.saveAs} saveAs: (path) -> @buffer.saveAs(path) - # {Delegates to: TextBuffer.getExtension} - getFileExtension: -> @buffer.getExtension() - # {Delegates to: TextBuffer.getPath} getPath: -> @buffer.getPath() @@ -326,7 +313,7 @@ class Editor extends Model # Public: Returns a {Number} representing the number of lines in the editor. getLineCount: -> @buffer.getLineCount() - # Private: Retrieves the current {TextBuffer}. + # Retrieves the current {TextBuffer}. getBuffer: -> @buffer # Public: Retrieves the current buffer's URI. @@ -353,8 +340,8 @@ class Editor extends Model # Public: Returns the range for the given buffer row. # - # * row: A row {Number}. - # * options: An options hash with an `includeNewline` key. + # row - A row {Number}. + # options - An options hash with an `includeNewline` key. # # Returns a {Range}. bufferRangeForBufferRow: (row, options) -> @buffer.rangeForRow(row, options) @@ -362,7 +349,7 @@ class Editor extends Model # Public: Returns a {String} representing the contents of the line at the # given buffer row. # - # * row - A {Number} representing a zero-indexed buffer row. + # row - A {Number} representing a zero-indexed buffer row. lineForBufferRow: (row) -> @buffer.lineForRow(row) # Public: Returns a {Number} representing the line length for the given @@ -439,10 +426,8 @@ class Editor extends Model # Public: Inserts text at the current cursor positions # - # * text: - # A String representing the text to insert. - # * options: - # + A set of options equivalent to {Selection.insertText} + # text - A {String} representing the text to insert. + # options - A set of options equivalent to {Selection.insertText}. insertText: (text, options={}) -> options.autoIndentNewline ?= @shouldAutoIndent() options.autoDecreaseIndent ?= @shouldAutoIndent() @@ -469,8 +454,7 @@ class Editor extends Model # Public: Indents the current line. # - # * options - # + A set of options equivalent to {Selection.indent}. + # options - A set of options equivalent to {Selection.indent}. indent: (options={})-> options.autoIndent ?= @shouldAutoIndent() @mutateSelectedText (selection) -> selection.indent(options) @@ -521,7 +505,7 @@ class Editor extends Model # # If the language doesn't have comments, nothing happens. # - # Returns an {Array} of the commented {Ranges}. + # Returns an {Array} of the commented {Range}s. toggleLineCommentsInSelection: -> @mutateSelectedText (selection) -> selection.toggleLineComments() @@ -537,31 +521,30 @@ class Editor extends Model # Public: Copies and removes all characters from cursor to the end of the # line. cutToEndOfLine: -> - maintainPasteboard = false + maintainClipboard = false @mutateSelectedText (selection) -> - selection.cutToEndOfLine(maintainPasteboard) - maintainPasteboard = true + selection.cutToEndOfLine(maintainClipboard) + maintainClipboard = true # Public: Cuts the selected text. cutSelectedText: -> - maintainPasteboard = false + maintainClipboard = false @mutateSelectedText (selection) -> - selection.cut(maintainPasteboard) - maintainPasteboard = true + selection.cut(maintainClipboard) + maintainClipboard = true # Public: Copies the selected text. copySelectedText: -> - maintainPasteboard = false + maintainClipboard = false for selection in @getSelections() - selection.copy(maintainPasteboard) - maintainPasteboard = true + selection.copy(maintainClipboard) + maintainClipboard = true # Public: Pastes the text in the clipboard. # - # * options: - # + A set of options equivalent to {Selection.insertText}. + # options - A set of options equivalent to {Selection.insertText}. pasteText: (options={}) -> - [text, metadata] = atom.pasteboard.read() + {text, metadata} = atom.clipboard.readWithMetadata() containsNewlines = text.indexOf('\n') isnt -1 @@ -640,7 +623,7 @@ class Editor extends Model largestFoldStartingAtScreenRow: (screenRow) -> @displayBuffer.largestFoldStartingAtScreenRow(screenRow) - # Public: Moves the selected line up one row. + # Public: Moves the selected lines up one screen row. moveLineUp: -> selection = @getSelectedBufferRange() return if selection.start.row is 0 @@ -652,29 +635,47 @@ class Editor extends Model rows = [selection.start.row..selection.end.row] if selection.start.row isnt selection.end.row and selection.end.column is 0 rows.pop() unless @isFoldedAtBufferRow(selection.end.row) + + # Move line around the fold that is directly above the selection + precedingScreenRow = @screenPositionForBufferPosition([selection.start.row]).translate([-1]) + precedingBufferRow = @bufferPositionForScreenPosition(precedingScreenRow).row + if fold = @largestFoldContainingBufferRow(precedingBufferRow) + insertDelta = fold.getBufferRange().getRowCount() + else + insertDelta = 1 + for row in rows - screenRow = @screenPositionForBufferPosition([row]).row - if @isFoldedAtScreenRow(screenRow) - bufferRange = @bufferRangeForScreenRange([[screenRow], [screenRow + 1]]) + if fold = @displayBuffer.largestFoldStartingAtBufferRow(row) + bufferRange = fold.getBufferRange() startRow = bufferRange.start.row - endRow = bufferRange.end.row - 1 - foldedRows.push(endRow - 1) + endRow = bufferRange.end.row + foldedRows.push(startRow - insertDelta) else startRow = row endRow = row + insertPosition = Point.fromObject([startRow - insertDelta]) endPosition = Point.min([endRow + 1], @buffer.getEofPosition()) lines = @buffer.getTextInRange([[startRow], endPosition]) if endPosition.row is lastRow and endPosition.column > 0 and not @buffer.lineEndingForRow(endPosition.row) lines = "#{lines}\n" + @buffer.deleteRows(startRow, endRow) - @buffer.insert([startRow - 1], lines) - @foldBufferRow(foldedRow) for foldedRow in foldedRows + # Make sure the inserted text doesn't go into an existing fold + if fold = @displayBuffer.largestFoldStartingAtBufferRow(insertPosition.row) + @destroyFoldsContainingBufferRow(insertPosition.row) + foldedRows.push(insertPosition.row + endRow - startRow + fold.getBufferRange().getRowCount()) - @setSelectedBufferRange(selection.translate([-1]), preserveFolds: true) + @buffer.insert(insertPosition, lines) - # Public: Moves the selected line down one row. + # Restore folds that existed before the lines were moved + for foldedRow in foldedRows when 0 <= foldedRow <= @getLastBufferRow() + @foldBufferRow(foldedRow) + + @setSelectedBufferRange(selection.translate([-insertDelta]), preserveFolds: true) + + # Public: Moves the selected lines down one screen row. moveLineDown: -> selection = @getSelectedBufferRange() lastRow = @buffer.getLastRow() @@ -686,13 +687,21 @@ class Editor extends Model rows = [selection.end.row..selection.start.row] if selection.start.row isnt selection.end.row and selection.end.column is 0 rows.shift() unless @isFoldedAtBufferRow(selection.end.row) + + # Move line around the fold that is directly below the selection + followingScreenRow = @screenPositionForBufferPosition([selection.end.row]).translate([1]) + followingBufferRow = @bufferPositionForScreenPosition(followingScreenRow).row + if fold = @largestFoldContainingBufferRow(followingBufferRow) + insertDelta = fold.getBufferRange().getRowCount() + else + insertDelta = 1 + for row in rows - screenRow = @screenPositionForBufferPosition([row]).row - if @isFoldedAtScreenRow(screenRow) - bufferRange = @bufferRangeForScreenRange([[screenRow], [screenRow + 1]]) + if fold = @displayBuffer.largestFoldStartingAtBufferRow(row) + bufferRange = fold.getBufferRange() startRow = bufferRange.start.row - endRow = bufferRange.end.row - 1 - foldedRows.push(endRow + 1) + endRow = bufferRange.end.row + foldedRows.push(endRow + insertDelta) else startRow = row endRow = row @@ -703,14 +712,23 @@ class Editor extends Model endPosition = [endRow + 1] lines = @buffer.getTextInRange([[startRow], endPosition]) @buffer.deleteRows(startRow, endRow) - insertPosition = Point.min([startRow + 1], @buffer.getEofPosition()) + + insertPosition = Point.min([startRow + insertDelta], @buffer.getEofPosition()) if insertPosition.row is @buffer.getLastRow() and insertPosition.column > 0 lines = "\n#{lines}" + + # Make sure the inserted text doesn't go into an existing fold + if fold = @displayBuffer.largestFoldStartingAtBufferRow(insertPosition.row) + @destroyFoldsContainingBufferRow(insertPosition.row) + foldedRows.push(insertPosition.row + fold.getBufferRange().getRowCount()) + @buffer.insert(insertPosition, lines) - @foldBufferRow(foldedRow) for foldedRow in foldedRows + # Restore folds that existed before the lines were moved + for foldedRow in foldedRows when 0 <= foldedRow <= @getLastBufferRow() + @foldBufferRow(foldedRow) - @setSelectedBufferRange(selection.translate([1]), preserveFolds: true) + @setSelectedBufferRange(selection.translate([insertDelta]), preserveFolds: true) # Public: Duplicates the current line. # @@ -739,11 +757,9 @@ class Editor extends Model @setCursorScreenPosition(@getCursorScreenPosition().translate([1])) @foldCurrentRow() if cursorRowFolded - # Private: mutateSelectedText: (fn) -> @transact => fn(selection) for selection in @getSelections() - # Private: replaceSelectedText: (options={}, fn) -> {selectWordIfEmpty} = options @mutateSelectedText (selection) -> @@ -787,7 +803,9 @@ class Editor extends Model destroyMarker: (args...) -> @displayBuffer.destroyMarker(args...) - # Public: {Delegates to: DisplayBuffer.getMarkerCount} + # Public: Get the number of markers in this editor's buffer. + # + # Returns a {Number}. getMarkerCount: -> @buffer.getMarkerCount() @@ -826,10 +844,8 @@ class Editor extends Model # Public: Creates a new selection at the given marker. # - # * marker: - # The {DisplayBufferMarker} to highlight - # * options: - # + A hash of options that pertain to the {Selection} constructor. + # marker - The {DisplayBufferMarker} to highlight + # options - An {Object} that pertains to the {Selection} constructor. # # Returns the new {Selection}. addSelection: (marker, options={}) -> @@ -850,10 +866,8 @@ class Editor extends Model # Public: Given a buffer range, this adds a new selection for it. # - # * bufferRange: - # A {Range} in the buffer - # * options: - # + A hash of options for {.markBufferRange} + # bufferRange - A {Range} in the buffer. + # options - An options {Object} for {.markBufferRange}. # # Returns the new {Selection}. addSelectionForBufferRange: (bufferRange, options={}) -> @@ -863,20 +877,16 @@ class Editor extends Model # Public: Given a buffer range, this removes all previous selections and # creates a new selection for it. # - # * bufferRange: - # A {Range} in the buffer - # * options: - # + A hash of options for {.setSelectedBufferRanges} + # bufferRange - A {Range} in the buffer. + # options - An options {Object} for {.setSelectedBufferRanges}. setSelectedBufferRange: (bufferRange, options) -> @setSelectedBufferRanges([bufferRange], options) # Public: Given an array of buffer ranges, this removes all previous # selections and creates new selections for them. # - # * bufferRange: - # A {Range} in the buffer - # * options: - # + A hash of options for {.setSelectedBufferRanges} + # bufferRange - A {Range} in the buffer. + # options - An options {Object} for {.setSelectedBufferRanges}. setSelectedBufferRanges: (bufferRanges, options={}) -> throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length @@ -893,7 +903,7 @@ class Editor extends Model # Public: Unselects a given selection. # - # * selection - The {Selection} to remove. + # selection - The {Selection} to remove. removeSelection: (selection) -> _.remove(@selections, selection) @@ -904,9 +914,7 @@ class Editor extends Model @consolidateSelections() @getSelection().clear() - # Public: - # - # Removes all but one cursor (if there are multiple cursors) + # Removes all but one cursor (if there are multiple cursors). consolidateSelections: -> selections = @getSelections() if selections.length > 1 @@ -943,8 +951,7 @@ class Editor extends Model # Public: Determines if a given buffer range is included in a {Selection}. # - # * bufferRange: - # The {Range} you're checking against + # bufferRange - The {Range} you're checking against. # # Returns a {Boolean}. selectionIntersectsBufferRange: (bufferRange) -> @@ -953,10 +960,8 @@ class Editor extends Model # Public: Moves every local cursor to a given screen position. # - # * position: - # An {Array} of two numbers: the screen row, and the screen column. - # * options: - # An object with properties based on {Cursor.setScreenPosition} + # position - An {Array} of two numbers: the screen row, and the screen column. + # options - An {Object} with properties based on {Cursor.setScreenPosition}. setCursorScreenPosition: (position, options) -> @moveCursors (cursor) -> cursor.setScreenPosition(position, options) @@ -975,10 +980,8 @@ class Editor extends Model # Public: Moves every cursor to a given buffer position. # - # * position: - # An {Array} of two numbers: the buffer row, and the buffer column. - # * options: - # + An object with properties based on {Cursor.setBufferPosition} + # position - An {Array} of two numbers: the buffer row, and the buffer column. + # options - An object with properties based on {Cursor.setBufferPosition}. setCursorBufferPosition: (position, options) -> @moveCursors (cursor) -> cursor.setBufferPosition(position, options) @@ -1021,9 +1024,8 @@ class Editor extends Model # Public: Returns the word under the most recently added local {Cursor}. # - # * options: - # + An object with properties based on - # {Cursor.getBeginningOfCurrentWordBufferPosition}. + # options - An object with properties based on + # {Cursor.getBeginningOfCurrentWordBufferPosition}. getWordUnderCursor: (options) -> @getTextInBufferRange(@getCursor().getCurrentWordBufferRange(options)) @@ -1091,7 +1093,6 @@ class Editor extends Model moveCursorToNextWordBoundary: -> @moveCursors (cursor) -> cursor.moveToNextWordBoundary() - # Internal: Executes given function on all local cursors. moveCursors: (fn) -> fn(cursor) for cursor in @getCursors() @mergeCursors() @@ -1099,8 +1100,7 @@ class Editor extends Model # Public: Selects the text from the current cursor position to a given screen # position. # - # * position: - # An instance of {Point}, with a given `row` and `column`. + # position - An instance of {Point}, with a given `row` and `column`. selectToScreenPosition: (position) -> lastSelection = @getLastSelection() lastSelection.selectToScreenPosition(position) @@ -1259,8 +1259,6 @@ class Editor extends Model @setSelectedBufferRange(range) range - # Public: - # # FIXME: Not sure how to describe what this does. mergeCursors: -> positions = [] @@ -1271,27 +1269,21 @@ class Editor extends Model else positions.push(position) - # Public: - # # FIXME: Not sure how to describe what this does. expandSelectionsForward: (fn) -> @mergeIntersectingSelections => fn(selection) for selection in @getSelections() - # Public: - # # FIXME: Not sure how to describe what this does. expandSelectionsBackward: (fn) -> @mergeIntersectingSelections isReversed: true, => fn(selection) for selection in @getSelections() - # Public: - # # FIXME: No idea what this does. finalizeSelections: -> selection.finalize() for selection in @getSelections() - # Private: Merges intersecting selections. If passed a function, it executes + # Merges intersecting selections. If passed a function, it executes # the function with merging suppressed, then merges intersecting selections # afterward. mergeIntersectingSelections: (args...) -> @@ -1315,7 +1307,6 @@ class Editor extends Model _.reduce(@getSelections(), reducer, []) - # Private: preserveCursorPositionOnBufferReload: -> cursorPosition = null @subscribe @buffer, "will-reload", => @@ -1336,7 +1327,6 @@ class Editor extends Model reloadGrammar: -> @displayBuffer.reloadGrammar() - # Private: shouldAutoIndent: -> atom.config.get("editor.autoIndent") @@ -1347,32 +1337,24 @@ class Editor extends Model # undo stack remains relevant. transact: (fn) -> @buffer.transact(fn) - # Private: beginTransaction: -> @buffer.beginTransaction() - # Private: commitTransaction: -> @buffer.commitTransaction() - # Private: abortTransaction: -> @buffer.abortTransaction() - # Private: inspect: -> "" - # Private: logScreenLines: (start, end) -> @displayBuffer.logLines(start, end) - # Private: handleGrammarChange: -> @unfoldAll() @emit 'grammar-changed' - # Private: handleMarkerCreated: (marker) => if marker.matchesAttributes(@getSelectionMarkerAttributes()) @addSelection(marker) - # Private: getSelectionMarkerAttributes: -> type: 'selection', editorId: @id, invalidate: 'never' diff --git a/src/file.coffee b/src/file.coffee index bb09cc4f2..b4fffe060 100644 --- a/src/file.coffee +++ b/src/file.coffee @@ -5,6 +5,7 @@ Q = require 'q' {Emitter} = require 'emissary' _ = require 'underscore-plus' fs = require 'fs-plus' +runas = require 'runas' # Public: Represents an individual file. # @@ -25,16 +26,14 @@ class File # Public: Creates a new file. # - # * path: - # A String containing the absolute path to the file - # * symlink: - # A Boolean indicating if the path is a symlink (default: false) + # path - A {String} containing the absolute path to the file + # symlink - A {Boolean} indicating if the path is a symlink (default: false). constructor: (@path, @symlink=false) -> throw new Error("#{@path} is a directory") if fs.isDirectorySync(@path) @handleEventSubscriptions() - # Private: Subscribes to file system notifications when necessary. + # Subscribes to file system notifications when necessary. handleEventSubscriptions: -> eventNames = ['contents-changed', 'moved', 'removed'] @@ -49,24 +48,24 @@ class File subscriptionsEmpty = _.every eventNames, (eventName) => @getSubscriptionCount(eventName) is 0 @unsubscribeFromNativeChangeEvents() if subscriptionsEmpty - # Private: Sets the path for the file. + # Sets the path for the file. setPath: (@path) -> - # Public: Returns the path for the file. + # Public: Returns the {String} path for the file. getPath: -> @path - # Public: Return the filename without any directory information. + # Public: Return the {String} filename without any directory information. getBaseName: -> path.basename(@path) # Public: Overwrites the file with the given String. write: (text) -> previouslyExisted = @exists() + @writeFileWithPrivilegeEscalationSync(@getPath(), text) @cachedContents = text - fs.writeFileSync(@getPath(), text) @subscribeToNativeChangeEvents() if not previouslyExisted and @hasSubscriptions() - # Private: Deprecated + # Deprecated readSync: (flushCache) -> if not @exists() @cachedContents = null @@ -80,9 +79,8 @@ class File # Public: Reads the contents of the file. # - # * flushCache: - # A Boolean indicating whether to require a direct read or if a cached - # copy is acceptable. + # flushCache - A {Boolean} indicating whether to require a direct read or if + # a cached copy is acceptable. # # Returns a promise that resovles to a String. read: (flushCache) -> @@ -118,7 +116,6 @@ class File exists: -> fs.existsSync(@getPath()) - # Private: setDigest: (contents) -> @digest = crypto.createHash('sha1').update(contents ? '').digest('hex') @@ -126,7 +123,21 @@ class File getDigest: -> @digest ? @setDigest(@readSync()) - # Private: + # Writes the text to specified path. + # + # Privilege escalation would be asked when current user doesn't have + # permission to the path. + writeFileWithPrivilegeEscalationSync: (path, text) -> + try + fs.writeFileSync(path, text) + catch error + if error.code is 'EACCES' and process.platform is 'darwin' + authopen = '/usr/libexec/authopen' # man 1 authopen + unless runas(authopen, ['-w', '-c', path], stdin: text) is 0 + throw error + else + throw error + handleNativeChangeEvent: (eventType, path) -> if eventType is "delete" @unsubscribeFromNativeChangeEvents() @@ -139,11 +150,9 @@ class File @read(true).done (newContents) => @emit 'contents-changed' unless oldContents == newContents - # Private: detectResurrectionAfterDelay: -> _.delay (=> @detectResurrection()), 50 - # Private: detectResurrection: -> if @exists() @subscribeToNativeChangeEvents() @@ -152,13 +161,11 @@ class File @cachedContents = null @emit "removed" - # Private: subscribeToNativeChangeEvents: -> unless @watchSubscription? @watchSubscription = pathWatcher.watch @path, (eventType, path) => @handleNativeChangeEvent(eventType, path) - # Private: unsubscribeFromNativeChangeEvents: -> if @watchSubscription? @watchSubscription.close() diff --git a/src/fold.coffee b/src/fold.coffee index 32de3f988..e2d113fc2 100644 --- a/src/fold.coffee +++ b/src/fold.coffee @@ -1,6 +1,6 @@ {Point, Range} = require 'text-buffer' -# Private: Represents a fold that collapses multiple buffer lines into a single +# Represents a fold that collapses multiple buffer lines into a single # line on the screen. # # Their creation is managed by the {DisplayBuffer}. @@ -10,8 +10,6 @@ class Fold displayBuffer: null marker: null - ### Internal ### - constructor: (@displayBuffer, @marker) -> @id = @marker.id @displayBuffer.foldsByMarkerId[@marker.id] = this diff --git a/src/git.coffee b/src/git.coffee index b22466461..7143b4e69 100644 --- a/src/git.coffee +++ b/src/git.coffee @@ -27,17 +27,19 @@ class Git Emitter.includeInto(this) Subscriber.includeInto(this) - # Private: Creates a new `Git` instance. + # Public: Creates a new Git instance. # - # * path: The path to the git repository to open - # * options: - # + refreshOnWindowFocus: - # A Boolean that identifies if the windows should refresh + # path - The path to the Git repository to open. + # options - An object with the following keys (default: {}): + # :refreshOnWindowFocus - `true` to refresh the index and statuses when the + # window is focused. + # + # Returns a Git instance or null if the repository could not be opened. @open: (path, options) -> return null unless path try new Git(path, options) - catch e + catch null @exists: (path) -> @@ -47,20 +49,6 @@ class Git else false - path: null - statuses: null - upstream: null - branch: null - statusTask: null - - # Private: Creates a new `Git` object. - # - # * path: The {String} representing the path to your git working directory - # * options: - # + refreshOnWindowFocus: If `true`, {#refreshIndex} and {#refreshStatus} - # are called on focus - # + project: A project that supplies buffers that will be monitored for - # save and reload events to trigger status refreshes. constructor: (path, options={}) -> @repo = GitUtils.open(path) unless @repo? @@ -80,7 +68,7 @@ class Git if @project? @subscribe @project.eachBuffer (buffer) => @subscribeToBuffer(buffer) - # Private: Subscribes to buffer events. + # Subscribes to buffer events. subscribeToBuffer: (buffer) -> @subscribe buffer, 'saved reloaded path-changed', => if path = buffer.getPath() @@ -100,29 +88,29 @@ class Git @unsubscribe() - # Private: Returns the corresponding {Repository} + # Returns the corresponding {Repository} getRepo: -> unless @repo? throw new Error("Repository has been destroyed") @repo - # Public: Reread the index to update any values that have changed since the + # Reread the index to update any values that have changed since the # last time the index was read. refreshIndex: -> @getRepo().refreshIndex() - # Public: Returns the path of the repository. + # Public: Returns the {String} path of the repository. getPath: -> @path ?= fs.absolute(@getRepo().getPath()) - # Public: Returns the working directory of the repository. + # Public: Returns the {String} working directory path of the repository. getWorkingDirectory: -> @getRepo().getWorkingDirectory() - # Public: Returns the status of a single path in the repository. + # Public: Get the status of a single path in the repository. # - # * path: - # A String defining a relative path + # path - A {String} repository-relative path. # - # Returns a {Number}, FIXME representing what? + # Returns a {Number} representing the status. This value can be passed to + # {.isStatusModified} or {.isStatusNew} to get more information. getPathStatus: (path) -> currentPathStatus = @statuses[path] ? 0 pathStatus = @getRepo().getStatus(@relativize(path)) ? 0 @@ -134,7 +122,9 @@ class Git @emit 'status-changed', path, pathStatus pathStatus - # Public: Returns true if the given path is ignored. + # Public: Is the given path ignored? + # + # Returns a {Boolean}. isPathIgnored: (path) -> @getRepo().isIgnored(@relativize(path)) # Public: Returns true if the given status indicates modification. @@ -163,22 +153,22 @@ class Git # `refs/remotes`. It also shortens the SHA-1 of a detached `HEAD` to 7 # characters. # - # Returns a String. + # Returns a {String}. getShortHead: -> @getRepo().getShortHead() # Public: Restore the contents of a path in the working directory and index # to the version at `HEAD`. # # This is essentially the same as running: + # # ``` # git reset HEAD -- # git checkout HEAD -- # ``` # - # * path: - # The String path to checkout + # path - The {String} path to checkout. # - # Returns a Boolean that's true if the method was successful. + # Returns a {Boolean} that's true if the method was successful. checkoutHead: (path) -> headCheckedOut = @getRepo().checkoutHead(@relativize(path)) @getPathStatus(path) if headCheckedOut @@ -186,10 +176,9 @@ class Git # Public: Checks out a branch in your repository. # - # * reference: - # The String reference to checkout - # * create: - # A Boolean value which, if true creates the new reference if it doesn't exist. + # reference - The String reference to checkout + # create - A Boolean value which, if true creates the new reference if it + # doesn't exist. # # Returns a Boolean that's true if the method was successful. checkoutReference: (reference, create) -> @@ -200,27 +189,26 @@ class Git # This compares the working directory contents of the path to the `HEAD` # version. # - # * path: - # The String path to check + # path - The {String} path to check. # - # Returns an object with two keys, `added` and `deleted`. These will always - # be greater than 0. + # Returns an {Object} with the following keys: + # :added - The {Number} of added lines. + # :deleted - The {Number} of deleted lines. getDiffStats: (path) -> @getRepo().getDiffStats(@relativize(path)) - # Public: Identifies if a path is a submodule. + # Public: Is the given path a submodule in the repository? # - # * path: - # The String path to check + # path - The {String} path to check. # - # Returns a Boolean. + # Returns a {Boolean}. isSubmodule: (path) -> @getRepo().isSubmodule(@relativize(path)) - # Public: Retrieves the status of a directory. + # Public: Get the status of a directory in the repository's working directory. # - # * path: - # The String path to check + # path - The {String} path to check. # - # Returns a Number representing the status. + # Returns a {Number} representing the status. This value can be passed to + # {.isStatusModified} or {.isStatusNew} to get more information. getDirectoryStatus: (directoryPath) -> {sep} = require 'path' directoryPath = "#{directoryPath}#{sep}" @@ -232,16 +220,14 @@ class Git # Public: Retrieves the line diffs comparing the `HEAD` version of the given # path and the given text. # - # This is similar to the commit numbers reported by `git status` when a - # remote tracking branch exists. + # path - The {String} path relative to the repository. + # text - The {String} to compare against the `HEAD` contents # - # * path: - # The String path (relative to the repository) - # * text: - # The String to compare against the `HEAD` contents - # - # Returns an object with two keys, `ahead` and `behind`. These will always be - # greater than zero. + # Returns an {Array} of hunk {Object}s with the following keys: + # :oldStart - The line {Number} of the old hunk. + # :newStart - The line {Number} of the new hunk. + # :oldLines - The {Number} of lines in the old hunk. + # :newLines - The {Number} of lines in the new hunk getLineDiffs: (path, text) -> # Ignore eol of line differences on windows so that files checked in as # LF don't report every line modified when the text contains CRLF endings. @@ -257,7 +243,7 @@ class Git # Public: Returns the upstream branch for the current HEAD, or null if there # is no upstream branch for the current HEAD. # - # Returns a String branch name such as `refs/remotes/origin/master` + # Returns a {String} branch name such as `refs/remotes/origin/master`. getUpstreamBranch: -> @getRepo().getUpstreamBranch() # Public: Returns the current SHA for the given reference. @@ -265,19 +251,21 @@ class Git # Public: Gets all the local and remote references. # - # Returns an object with three keys: `heads`, `remotes`, and `tags`. Each key - # can be an array of strings containing the reference names. + # Returns an {Object} with the following keys: + # :heads - An {Array} of head reference names. + # :remotes - An {Array} of remote reference names. + # :tags - An {Array} of tag reference names. getReferences: -> @getRepo().getReferences() # Public: Returns the number of commits behind the current branch is from the - # default remote branch. + # its upstream remote branch. getAheadBehindCount: (reference) -> @getRepo().getAheadBehindCount(reference) # Public: Returns true if the given branch exists. hasBranch: (branch) -> @getReferenceTarget("refs/heads/#{branch}")? - # Private: Refreshes the current git status in an outside process and - # asynchronously updates the relevant properties. + # Refreshes the current git status in an outside process and asynchronously + # updates the relevant properties. refreshStatus: -> @statusTask = Task.once require.resolve('./repository-status-handler'), @getPath(), ({statuses, upstream, branch}) => statusesUnchanged = _.isEqual(statuses, @statuses) and _.isEqual(upstream, @upstream) and _.isEqual(branch, @branch) diff --git a/src/gutter-view.coffee b/src/gutter-view.coffee index cae0d51c0..aabc515fc 100644 --- a/src/gutter-view.coffee +++ b/src/gutter-view.coffee @@ -2,14 +2,11 @@ {Range} = require 'text-buffer' _ = require 'underscore-plus' -# Private: Represents the portion of the {EditorView} containing row numbers. +# Represents the portion of the {EditorView} containing row numbers. # # The gutter also indicates if rows are folded. module.exports = class GutterView extends View - - ### Internal ### - @content: -> @div class: 'gutter', => @div outlet: 'lineNumbers', class: 'line-numbers' @@ -51,8 +48,6 @@ class GutterView extends View $(document).on "mousemove.gutter-#{editorView.id}", moveHandler $(document).one "mouseup.gutter-#{editorView.id}", => $(document).off 'mousemove', moveHandler - ### Public ### - # Retrieves the containing {EditorView}. # # Returns an {EditorView}. @@ -138,8 +133,6 @@ class GutterView extends View el.classList.remove(klass) if hasClass classesRemoved - ### Internal ### - updateLineNumbers: (changes, startScreenRow, endScreenRow) -> # Check if we have something already rendered that overlaps the requested range updateAllLines = not (startScreenRow? and endScreenRow?) @@ -223,7 +216,7 @@ class GutterView extends View html - # Private: Called to update the 'foldable' class of line numbers when there's + # Called to update the 'foldable' class of line numbers when there's # a change to the display buffer that doesn't regenerate all the line numbers # anyway. updateFoldableClasses: (changes) -> diff --git a/src/key-binding.coffee b/src/key-binding.coffee index 1f3ad1d65..46b359858 100644 --- a/src/key-binding.coffee +++ b/src/key-binding.coffee @@ -2,8 +2,6 @@ _ = require 'underscore-plus' fs = require 'fs-plus' {specificity} = require 'clear-cut' -### Internal ### - module.exports = class KeyBinding @parser: null diff --git a/src/keymap.coffee b/src/keymap.coffee index 3c3a8ec44..8fccf060d 100644 --- a/src/keymap.coffee +++ b/src/keymap.coffee @@ -9,19 +9,23 @@ File = require './file' Modifiers = ['alt', 'control', 'ctrl', 'shift', 'cmd'] -# Internal: Associates keymaps with actions. +# Public: Associates keybindings with commands. # -# Keymaps are defined in a CSON format. A typical keymap looks something like this: +# An instance of this class is always available as the `atom.keymap` global. +# +# Keymaps are defined in a CSON/JSON format. A typical keymap looks something +# like this: # # ```cson # 'body': -# 'ctrl-l': 'package:do-something' -#'.someClass': -# 'enter': 'package:confirm' +# 'ctrl-l': 'package:do-something' +# '.someClass': +# 'enter': 'package:confirm' # ``` # -# As a key, you define the DOM element you want to work on, using CSS notation. For that -# key, you define one or more key:value pairs, associating keystrokes with a command to execute. +# As a key, you define the DOM element you want to work on, using CSS notation. +# For that key, you define one or more key:value pairs, associating keystrokes +# with a command to execute. module.exports = class Keymap Emitter.includeInto(this) @@ -39,10 +43,8 @@ class Keymap # Public: Returns a array of {KeyBinding}s (sorted by selector specificity) # that match a keystroke and element. # - # * keystroke: - # The string representing the keys pressed (e.g. ctrl-P). - # * element: - # The DOM node that will match a {KeyBinding}'s selector. + # keystroke - The {String} representing the keys pressed (e.g. ctrl-P). + # element - The DOM node that will match a {KeyBinding}'s selector. keyBindingsForKeystrokeMatchingElement: (keystroke, element) -> keyBindings = @keyBindingsForKeystroke(keystroke) @keyBindingsMatchingElement(element, keyBindings) @@ -50,41 +52,37 @@ class Keymap # Public: Returns a array of {KeyBinding}s (sorted by selector specificity) # that match a command. # - # * command: - # The string representing the command (tree-view:toggle) - # * element: - # The DOM node that will match a {KeyBinding}'s selector. + # command - The {String} representing the command (tree-view:toggle). + # element - The DOM node that will match a {KeyBinding}'s selector. keyBindingsForCommandMatchingElement: (command, element) -> keyBindings = @keyBindingsForCommand(command) @keyBindingsMatchingElement(element, keyBindings) # Public: Returns an array of {KeyBinding}s that match a keystroke - # * keystroke: - # The string representing the keys pressed (e.g. ctrl-P) + # + # keystroke: The {String} representing the keys pressed (e.g. ctrl-P) keyBindingsForKeystroke: (keystroke) -> keystroke = KeyBinding.normalizeKeystroke(keystroke) @keyBindings.filter (keyBinding) -> keyBinding.matches(keystroke) # Public: Returns an array of {KeyBinding}s that match a command - # * keystroke: - # The string representing the keys pressed (e.g. ctrl-P) + # + # keystroke - The {String} representing the keys pressed (e.g. ctrl-P) keyBindingsForCommand: (command) -> @keyBindings.filter (keyBinding) -> keyBinding.command == command # Public: Returns a array of {KeyBinding}s (sorted by selector specificity) # whos selector matches the element. # - # * element: - # The DOM node that will match a {KeyBinding}'s selector. + # element - The DOM node that will match a {KeyBinding}'s selector. keyBindingsMatchingElement: (element, keyBindings=@keyBindings) -> keyBindings = keyBindings.filter ({selector}) -> $(element).closest(selector).length > 0 keyBindings.sort (a, b) -> a.compare(b) # Public: Returns a keystroke string derived from an event. - # * event: - # A DOM or jQuery event - # * previousKeystroke: - # An optional string used for multiKeystrokes + # + # event - A DOM or jQuery event. + # previousKeystroke - An optional string used for multiKeystrokes. keystrokeStringForEvent: (event, previousKeystroke) -> if event.originalEvent.keyIdentifier.indexOf('U+') == 0 hexCharCode = event.originalEvent.keyIdentifier[2..] diff --git a/src/language-mode.coffee b/src/language-mode.coffee index da539e08e..c5dae9e0c 100644 --- a/src/language-mode.coffee +++ b/src/language-mode.coffee @@ -3,30 +3,19 @@ _ = require 'underscore-plus' {OnigRegExp} = require 'oniguruma' {Emitter, Subscriber} = require 'emissary' -### Internal ### - module.exports = class LanguageMode Emitter.includeInto(this) Subscriber.includeInto(this) - buffer: null - grammar: null - editor: null - currentGrammarScore: null - - ### Internal ### - - destroy: -> - @unsubscribe() - - ### Public ### - # Sets up a `LanguageMode` for the given {Editor}. # # editor - The {Editor} to associate with constructor: (@editor) -> - @buffer = @editor.buffer + {@buffer} = @editor + + destroy: -> + @unsubscribe() toggleLineCommentForBufferRow: (row) -> @toggleLineCommentsForBufferRows(row, row) @@ -187,7 +176,7 @@ class LanguageMode isFoldableAtBufferRow: (bufferRow) -> @isFoldableCodeAtBufferRow(bufferRow) or @isFoldableCommentAtBufferRow(bufferRow) - # Private: Returns a {Boolean} indicating whether the given buffer row starts + # Returns a {Boolean} indicating whether the given buffer row starts # a a foldable row range due to the code's indentation patterns. isFoldableCodeAtBufferRow: (bufferRow) -> return false if @editor.isBufferRowBlank(bufferRow) or @isLineCommentedAtBufferRow(bufferRow) @@ -195,14 +184,14 @@ class LanguageMode return false unless nextNonEmptyRow? @editor.indentationForBufferRow(nextNonEmptyRow) > @editor.indentationForBufferRow(bufferRow) - # Private: Returns a {Boolean} indicating whether the given buffer row starts + # Returns a {Boolean} indicating whether the given buffer row starts # a foldable row range due to being the start of a multi-line comment. isFoldableCommentAtBufferRow: (bufferRow) -> @isLineCommentedAtBufferRow(bufferRow) and @isLineCommentedAtBufferRow(bufferRow + 1) and not @isLineCommentedAtBufferRow(bufferRow - 1) - # Private: Returns a {Boolean} indicating whether the line at the given buffer + # Returns a {Boolean} indicating whether the line at the given buffer # row is a comment. isLineCommentedAtBufferRow: (bufferRow) -> return false unless 0 <= bufferRow <= @editor.getLastBufferRow() diff --git a/src/less-compile-cache.coffee b/src/less-compile-cache.coffee index ad9024faa..feccbac4a 100644 --- a/src/less-compile-cache.coffee +++ b/src/less-compile-cache.coffee @@ -5,7 +5,7 @@ LessCache = require 'less-cache' tmpDir = if process.platform is 'win32' then os.tmpdir() else '/tmp' -# Private: {LessCache} wrapper used by {ThemeManager} to read stylesheets. +# {LessCache} wrapper used by {ThemeManager} to read stylesheets. module.exports = class LessCompileCache Subscriber.includeInto(this) diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee index aaf55bf1e..82e8512a7 100644 --- a/src/menu-manager.coffee +++ b/src/menu-manager.coffee @@ -8,61 +8,81 @@ fs = require 'fs-plus' # Public: Provides a registry for menu items that you'd like to appear in the # application menu. # -# Should be accessed via `atom.menu`. +# An instance of this class is always available as the `atom.menu` global. module.exports = class MenuManager - # Private: constructor: ({@resourcePath}) -> + @pendingUpdateOperation = null @template = [] atom.keymap.on 'bundled-keymaps-loaded', => @loadPlatformItems() # Public: Adds the given item definition to the existing template. # - # * item: - # An object which describes a menu item as defined by - # https://github.com/atom/atom-shell/blob/master/docs/api/browser/menu.md + # ## Example + # ```coffee + # atom.menu.add [ + # { + # label: 'Hello' + # submenu : [{label: 'World!', command: 'hello:world'}] + # } + # ] + # ``` + # + # items - An {Array} of menu item {Object}s containing the keys: + # :label - The {String} menu label. + # :submenu - An optional {Array} of sub menu items. + # :command - An optional {String} command to trigger when the item is + # clicked. # # Returns nothing. add: (items) -> @merge(@template, item) for item in items @update() - # Private: Should the binding for the given selector be included in the menu + # Should the binding for the given selector be included in the menu # commands. # - # * selector: A String selector to check. + # selector - A {String} selector to check. # # Returns true to include the selector, false otherwise. includeSelector: (selector) -> return true if document.body.webkitMatchesSelector(selector) - # Simulate an .editor element attached to a body element that has the same - # classes as the current body element. + # Simulate an .editor element attached to a .workspace element attached to + # a body element that has the same classes as the current body element. unless @testEditor? + testBody = document.createElement('body') + testBody.classList.add(@classesForElement(document.body)...) + + testWorkspace = document.createElement('body') + workspaceClasses = @classesForElement(document.body.querySelector('.workspace')) ? ['.workspace'] + testWorkspace.classList.add(workspaceClasses...) + + testBody.appendChild(testWorkspace) + @testEditor = document.createElement('div') @testEditor.classList.add('editor') - testBody = document.createElement('body') - testBody.classList.add(document.body.classList.toString().split(' ')...) - testBody.appendChild(@testEditor) + testWorkspace.appendChild(@testEditor) @testEditor.webkitMatchesSelector(selector) # Public: Refreshes the currently visible menu. update: -> - keystrokesByCommand = {} - for binding in atom.keymap.getKeyBindings() when @includeSelector(binding.selector) - keystrokesByCommand[binding.command] ?= [] - keystrokesByCommand[binding.command].push binding.keystroke - @sendToBrowserProcess(@template, keystrokesByCommand) + clearImmediate(@pendingUpdateOperation) if @pendingUpdateOperation? + @pendingUpdateOperation = setImmediate => + keystrokesByCommand = {} + for binding in atom.keymap.getKeyBindings() when @includeSelector(binding.selector) + keystrokesByCommand[binding.command] ?= [] + keystrokesByCommand[binding.command].push binding.keystroke + @sendToBrowserProcess(@template, keystrokesByCommand) - # Private: loadPlatformItems: -> menusDirPath = path.join(@resourcePath, 'menus') platformMenuPath = fs.resolve(menusDirPath, process.platform, ['cson', 'json']) {menu} = CSON.readFileSync(platformMenuPath) @add(menu) - # Private: Merges an item in a submenu aware way such that new items are always + # Merges an item in a submenu aware way such that new items are always # appended to the bottom of existing menus where possible. merge: (menu, item) -> item = _.deepClone(item) @@ -72,7 +92,7 @@ class MenuManager else menu.push(item) unless _.find(menu, (i) => @normalizeLabel(i.label) == @normalizeLabel(item.label)) - # Private: OSX can't handle displaying accelerators for multiple keystrokes. + # OSX can't handle displaying accelerators for multiple keystrokes. # If they are sent across, it will stop processing accelerators for the rest # of the menu items. filterMultipleKeystroke: (keystrokesByCommand) -> @@ -85,12 +105,10 @@ class MenuManager filtered[key].push(binding) filtered - # Private: sendToBrowserProcess: (template, keystrokesByCommand) -> keystrokesByCommand = @filterMultipleKeystroke(keystrokesByCommand) ipc.sendChannel 'update-application-menu', template, keystrokesByCommand - # Private: normalizeLabel: (label) -> return undefined unless label? @@ -98,3 +116,7 @@ class MenuManager label.replace(/\&/g, '') else label + + # Get an {Array} of {String} classes for the given element. + classesForElement: (element) -> + element?.classList.toString().split(' ') ? [] diff --git a/src/package-manager.coffee b/src/package-manager.coffee index e94a69785..52ea5fedc 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -1,11 +1,14 @@ {Emitter} = require 'emissary' fs = require 'fs-plus' _ = require 'underscore-plus' +Q = require 'q' Package = require './package' path = require 'path' # Public: Package manager for coordinating the lifecycle of Atom packages. # +# An instance of this class is always available as the `atom.packages` global. +# # Packages can be loaded, activated, and deactivated, and unloaded: # * Loading a package reads and parses the package's metadata and resources # such as keymaps, menus, stylesheets, etc. @@ -17,13 +20,10 @@ path = require 'path' # # Packages can also be enabled/disabled via the `core.disabledPackages` config # settings and also by calling `enablePackage()/disablePackage()`. -# -# An instance of this class is globally available via `atom.packages`. module.exports = class PackageManager Emitter.includeInto(this) - # Private: constructor: ({configDirPath, devMode, @resourcePath}) -> @packageDirPaths = [path.join(configDirPath, "packages")] if devMode @@ -32,7 +32,6 @@ class PackageManager @loadedPackages = {} @activePackages = {} @packageStates = {} - @observingDisabledPackages = false @packageActivators = [] @registerPackageActivator(this, ['atom', 'textmate']) @@ -47,11 +46,9 @@ class PackageManager getPackageDirPaths: -> _.clone(@packageDirPaths) - # Private: getPackageState: (name) -> @packageStates[name] - # Private: setPackageState: (name, state) -> @packageStates[name] = state @@ -67,44 +64,44 @@ class PackageManager pack?.disable() pack - # Private: Activate all the packages that should be activated. + # Activate all the packages that should be activated. activate: -> for [activator, types] in @packageActivators packages = @getLoadedPackagesForTypes(types) activator.activatePackages(packages) @emit 'activated' - # Private: another type of package manager can handle other package types. + # another type of package manager can handle other package types. # See ThemeManager registerPackageActivator: (activator, types) -> @packageActivators.push([activator, types]) - # Private: activatePackages: (packages) -> @activatePackage(pack.name) for pack in packages @observeDisabledPackages() - # Private: Activate a single package by name - activatePackage: (name, options) -> - return pack if pack = @getActivePackage(name) - if pack = @loadPackage(name, options) - @activePackages[pack.name] = pack - pack.activate(options) - pack + # Activate a single package by name + activatePackage: (name) -> + if pack = @getActivePackage(name) + Q(pack) + else + pack = @loadPackage(name) + pack.activate().then => + @activePackages[pack.name] = pack + pack - # Private: Deactivate all packages + # Deactivate all packages deactivatePackages: -> - @deactivatePackage(pack.name) for pack in @getActivePackages() + @deactivatePackage(pack.name) for pack in @getLoadedPackages() @unobserveDisabledPackages() - # Private: Deactivate the package with the given name + # Deactivate the package with the given name deactivatePackage: (name) -> - if pack = @getActivePackage(name) + pack = @getLoadedPackage(name) + if @isPackageActive(name) @setPackageState(pack.name, state) if state = pack.serialize?() - pack.deactivate() - delete @activePackages[pack.name] - else - throw new Error("No active package for name '#{name}'") + pack.deactivate() + delete @activePackages[pack.name] # Public: Get an array of all the active packages getActivePackages: -> @@ -118,17 +115,12 @@ class PackageManager isPackageActive: (name) -> @getActivePackage(name)? - # Private: unobserveDisabledPackages: -> - return unless @observingDisabledPackages - atom.config.unobserve('core.disabledPackages') - @observingDisabledPackages = false + @disabledPackagesSubscription?.off() + @disabledPackagesSubscription = null - # Private: observeDisabledPackages: -> - return if @observingDisabledPackages - - atom.config.observe 'core.disabledPackages', callNow: false, (disabledPackages, {previous}) => + @disabledPackagesSubscription ?= atom.config.observe 'core.disabledPackages', callNow: false, (disabledPackages, {previous}) => packagesToEnable = _.difference(previous, disabledPackages) packagesToDisable = _.difference(disabledPackages, previous) @@ -136,10 +128,7 @@ class PackageManager @activatePackage(packageName) for packageName in packagesToEnable null - @observingDisabledPackages = true - - # Private: - loadPackages: (options) -> + loadPackages: -> # Ensure atom exports is already in the require cache so the load time # of the first package isn't skewed by being the first to require atom require '../exports/atom' @@ -147,27 +136,24 @@ class PackageManager packagePaths = @getAvailablePackagePaths() packagePaths = packagePaths.filter (packagePath) => not @isPackageDisabled(path.basename(packagePath)) packagePaths = _.uniq packagePaths, (packagePath) -> path.basename(packagePath) - @loadPackage(packagePath, options) for packagePath in packagePaths + @loadPackage(packagePath) for packagePath in packagePaths @emit 'loaded' - # Private: - loadPackage: (nameOrPath, options) -> + loadPackage: (nameOrPath) -> if packagePath = @resolvePackagePath(nameOrPath) name = path.basename(nameOrPath) return pack if pack = @getLoadedPackage(name) - pack = Package.load(packagePath, options) + pack = Package.load(packagePath) @loadedPackages[pack.name] = pack if pack? pack else throw new Error("Could not resolve '#{nameOrPath}' to a package path") - # Private: unloadPackages: -> @unloadPackage(name) for name in _.keys(@loadedPackages) null - # Private: unloadPackage: (name) -> if @isPackageActive(name) throw new Error("Tried to unload active package '#{name}'") @@ -189,9 +175,9 @@ class PackageManager getLoadedPackages: -> _.values(@loadedPackages) - # Private: Get packages for a certain package type + # Get packages for a certain package type # - # * types: an {Array} of {String}s like ['atom', 'textmate'] + # types - an {Array} of {String}s like ['atom', 'textmate']. getLoadedPackagesForTypes: (types) -> pack for pack in @getLoadedPackages() when pack.getType() in types @@ -209,7 +195,6 @@ class PackageManager isPackageDisabled: (name) -> _.include(atom.config.get('core.disabledPackages') ? [], name) - # Private: hasAtomEngine: (packagePath) -> metadata = Package.loadMetadata(packagePath, true) metadata?.engines?.atom? @@ -218,7 +203,6 @@ class PackageManager isBundledPackage: (name) -> @getPackageDependencies().hasOwnProperty(name) - # Private: getPackageDependencies: -> unless @packageDependencies? try diff --git a/src/package.coffee b/src/package.coffee index ca4450bc6..7618e80c5 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -1,7 +1,6 @@ CSON = require 'season' {basename, join} = require 'path' -### Internal ### module.exports = class Package @build: (path) -> @@ -23,9 +22,9 @@ class Package pack - @load: (path, options) -> + @load: (path) -> pack = @build(path) - pack?.load(options) + pack?.load() pack @loadMetadata: (path, ignoreErrors=false) -> @@ -44,9 +43,6 @@ class Package constructor: (@path) -> @name = basename(@path) - isActive: -> - atom.packages.isPackageActive(@name) - enable: -> atom.config.removeAtKeyPath('core.disabledPackages', @metadata.name) @@ -54,9 +50,8 @@ class Package atom.config.pushAtKeyPath('core.disabledPackages', @metadata.name) isTheme: -> - !!@metadata?.theme + @metadata?.theme? - # Private: measure: (key, fn) -> startTime = Date.now() value = fn() diff --git a/src/pane-axis-view.coffee b/src/pane-axis-view.coffee index f478691f6..3039600c0 100644 --- a/src/pane-axis-view.coffee +++ b/src/pane-axis-view.coffee @@ -1,7 +1,6 @@ {View} = require './space-pen-extensions' PaneView = null -### Internal ### module.exports = class PaneAxisView extends View initialize: (@model) -> diff --git a/src/pane-column-view.coffee b/src/pane-column-view.coffee index ac9d1df6e..fca1938e2 100644 --- a/src/pane-column-view.coffee +++ b/src/pane-column-view.coffee @@ -2,7 +2,6 @@ _ = require 'underscore-plus' PaneAxisView = require './pane-axis-view' -# Internal: module.exports = class PaneColumnView extends PaneAxisView diff --git a/src/pane-container-view.coffee b/src/pane-container-view.coffee index 8d0d1b0e8..093c3e29d 100644 --- a/src/pane-container-view.coffee +++ b/src/pane-container-view.coffee @@ -3,7 +3,7 @@ Delegator = require 'delegato' PaneView = require './pane-view' PaneContainer = require './pane-container' -# Private: Manages the list of panes within a {WorkspaceView} +# Manages the list of panes within a {WorkspaceView} module.exports = class PaneContainerView extends View Delegator.includeInto(this) @@ -27,8 +27,6 @@ class PaneContainerView extends View viewClass = model.getViewClass() model._view ?= new viewClass(model) - ### Public ### - getRoot: -> @children().first().view() @@ -98,3 +96,50 @@ class PaneContainerView extends View focusPreviousPane: -> @model.activatePreviousPane() + + focusPaneAbove: -> + @nearestPaneInDirection('above')?.focus() + + focusPaneBelow: -> + @nearestPaneInDirection('below')?.focus() + + focusPaneOnLeft: -> + @nearestPaneInDirection('left')?.focus() + + focusPaneOnRight: -> + @nearestPaneInDirection('right')?.focus() + + nearestPaneInDirection: (direction) -> + distance = (pointA, pointB) -> + x = pointB.x - pointA.x + y = pointB.y - pointA.y + Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) + + pane = @getActivePane() + box = @boundingBoxForPane(pane) + panes = @getPanes() + .filter (otherPane) => + otherBox = @boundingBoxForPane(otherPane) + switch direction + when 'left' then otherBox.right.x <= box.left.x + when 'right' then otherBox.left.x >= box.right.x + when 'above' then otherBox.bottom.y <= box.top.y + when 'below' then otherBox.top.y >= box.bottom.y + .sort (paneA, paneB) => + boxA = @boundingBoxForPane(paneA) + boxB = @boundingBoxForPane(paneB) + switch direction + when 'left' then distance(box.left, boxA.right) - distance(box.left, boxB.right) + when 'right' then distance(box.right, boxA.left) - distance(box.right, boxB.left) + when 'above' then distance(box.top, boxA.bottom) - distance(box.top, boxB.bottom) + when 'below' then distance(box.bottom, boxA.top) - distance(box.bottom, boxB.top) + + panes[0] + + boundingBoxForPane: (pane) -> + boundingBox = pane[0].getBoundingClientRect() + + left: {x: boundingBox.left, y: boundingBox.top} + right: {x: boundingBox.right, y: boundingBox.top} + top: {x: boundingBox.left, y: boundingBox.top} + bottom: {x: boundingBox.left, y: boundingBox.bottom} diff --git a/src/pane-container.coffee b/src/pane-container.coffee index de339c8a9..a02afed9e 100644 --- a/src/pane-container.coffee +++ b/src/pane-container.coffee @@ -86,6 +86,6 @@ class PaneContainer extends Model itemDestroyed: (item) -> @emit 'item-destroyed', item - # Private: Called by Model superclass when destroyed + # Called by Model superclass when destroyed destroyed: -> pane.destroy() for pane in @getPanes() diff --git a/src/pane-row-view.coffee b/src/pane-row-view.coffee index 8808ce36c..1ad73d318 100644 --- a/src/pane-row-view.coffee +++ b/src/pane-row-view.coffee @@ -2,8 +2,6 @@ _ = require 'underscore-plus' PaneAxisView = require './pane-axis-view' -### Internal ### - module.exports = class PaneRowView extends PaneAxisView @content: -> diff --git a/src/pane-view.coffee b/src/pane-view.coffee index bf1835cb0..09cc2a62b 100644 --- a/src/pane-view.coffee +++ b/src/pane-view.coffee @@ -27,11 +27,10 @@ class PaneView extends View 'destroyItem', 'destroyItems', 'destroyActiveItem', 'destroyInactiveItems', 'saveActiveItem', 'saveActiveItemAs', 'saveItem', 'saveItemAs', 'saveItems', 'itemForUri', 'activateItemForUri', 'promptToSaveItem', 'copyActiveItem', 'isActive', - 'activate', toProperty: 'model' + 'activate', 'getActiveItem', toProperty: 'model' previousActiveItem: null - # Private: initialize: (args...) -> if args[0] instanceof Pane @model = args[0] @@ -97,7 +96,6 @@ class PaneView extends View # Deprecated: Use ::activatePreviousItem showPreviousItem: -> @activatePreviousItem() - # Private: afterAttach: (onDom) -> @focus() if @model.focused and onDom @@ -167,11 +165,9 @@ class PaneView extends View @unsubscribe(item) if typeof item.off is 'function' @trigger 'pane:before-item-destroyed', [item] - # Private: activeItemTitleChanged: => @trigger 'pane:active-item-title-changed' - # Private: viewForItem: (item) -> return unless item? if item instanceof $ @@ -184,7 +180,6 @@ class PaneView extends View @viewsByItem.set(item, view) view - # Private: @::accessor 'activeView', -> @viewForItem(@activeItem) splitLeft: (items...) -> @model.splitLeft({items})._view @@ -195,14 +190,15 @@ class PaneView extends View splitDown: (items...) -> @model.splitDown({items})._view - # Public: + # Public: Get the container view housing this pane. + # + # Returns a {View}. getContainer: -> @closest('.panes').view() beforeRemove: -> @model.destroy() unless @model.isDestroyed() - # Private: remove: (selector, keepData) -> return super if keepData @unsubscribe() diff --git a/src/pane.coffee b/src/pane.coffee index 4375ce588..2e320a3c6 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -3,6 +3,7 @@ {Model, Sequence} = require 'theorist' Serializable = require 'serializable' PaneAxis = require './pane-axis' +Editor = require './editor' PaneView = null # Public: A container for multiple items, one of which is *active* at a given @@ -27,7 +28,6 @@ class Pane extends Model .map((activePane) => activePane is this) .distinctUntilChanged() - # Private: constructor: (params) -> super @@ -43,31 +43,31 @@ class Pane extends Model @activate() if params?.active - # Private: Called by the Serializable mixin during serialization. + # Called by the Serializable mixin during serialization. serializeParams: -> items: compact(@items.map((item) -> item.serialize?())) activeItemUri: @activeItem?.getUri?() focused: @focused active: @active - # Private: Called by the Serializable mixin during deserialization. + # Called by the Serializable mixin during deserialization. deserializeParams: (params) -> {items, activeItemUri} = params params.items = compact(items.map (itemState) -> atom.deserializers.deserialize(itemState)) params.activeItem = find params.items, (item) -> item.getUri?() is activeItemUri params - # Private: Called by the view layer to construct a view for this model. + # Called by the view layer to construct a view for this model. getViewClass: -> PaneView ?= require './pane-view' isActive: -> @active - # Private: Called by the view layer to indicate that the pane has gained focus. + # Called by the view layer to indicate that the pane has gained focus. focus: -> @focused = true @activate() unless @isActive() - # Private: Called by the view layer to indicate that the pane has lost focus. + # Called by the view layer to indicate that the pane has lost focus. blur: -> @focused = false true # if this is called from an event handler, don't cancel it @@ -78,13 +78,25 @@ class Pane extends Model @container?.activePane = this @emit 'activated' - # Private: getPanes: -> [this] - # Public: + # Public: Get the items in this pane. + # + # Returns an {Array} of items. getItems: -> @items.slice() + # Public: Get the active pane item in this pane. + # + # Returns a pane item. + getActiveItem: -> + @activeItem + + # Public: Returns an {Editor} if the pane item is an {Editor}, or null + # otherwise. + getActiveEditor: -> + @activeItem if @activeItem instanceof Editor + # Public: Returns the item at the specified index. itemAtIndex: (index) -> @items[index] @@ -105,15 +117,15 @@ class Pane extends Model else @activateItemAtIndex(@items.length - 1) - # Public: Returns the index of the current active item. + # Returns the index of the current active item. getActiveItemIndex: -> @items.indexOf(@activeItem) - # Public: Makes the item at the given index active. + # Makes the item at the given index active. activateItemAtIndex: (index) -> @activateItem(@itemAtIndex(index)) - # Public: Makes the given item active, adding the item if necessary. + # Makes the given item active, adding the item if necessary. activateItem: (item) -> if item? @addItem(item) @@ -121,11 +133,9 @@ class Pane extends Model # Public: Adds the item to the pane. # - # * item: - # The item to add. It can be a model with an associated view or a view. - # * index: - # An optional index at which to add the item. If omitted, the item is - # added after the current active item. + # item - The item to add. It can be a model with an associated view or a view. + # index - An optional index at which to add the item. If omitted, the item is + # added after the current active item. # # Returns the added item addItem: (item, index=@getActiveItemIndex() + 1) -> @@ -138,12 +148,11 @@ class Pane extends Model # Public: Adds the given items to the pane. # - # * items: - # An {Array} of items to add. Items can be models with associated views - # or views. Any items that are already present in items will not be added. - # * index: - # An optional index at which to add the item. If omitted, the item is - # added after the current active item. + # items - An {Array} of items to add. Items can be models with associated + # views or views. Any items that are already present in items will + # not be added. + # index - An optional index at which to add the item. If omitted, the item is + # added after the current active item. # # Returns an {Array} of the added items addItems: (items, index=@getActiveItemIndex() + 1) -> @@ -151,7 +160,6 @@ class Pane extends Model @addItem(item, index + i) for item, i in items items - # Private: removeItem: (item, destroying) -> index = @items.indexOf(item) return if index is -1 @@ -207,7 +215,7 @@ class Pane extends Model destroy: -> super unless @container?.isAlive() and @container?.getPanes().length is 1 - # Private: Called by model superclass. + # Called by model superclass. destroyed: -> @container.activateNextPane() if @isActive() item.destroy?() for item in @items.slice() @@ -238,8 +246,9 @@ class Pane extends Model # Public: Saves the specified item. # - # * item: The item to save. - # * nextAction: An optional function which will be called after the item is saved. + # item - The item to save. + # nextAction - An optional function which will be called after the item is + # saved. saveItem: (item, nextAction) -> if item?.getUri?() item.save?() @@ -249,8 +258,9 @@ class Pane extends Model # Public: Saves the given item at a prompted-for location. # - # * item: The item to save. - # * nextAction: An optional function which will be called after the item is saved. + # item - The item to save. + # nextAction - An optional function which will be called after the item is + # saved. saveItemAs: (item, nextAction) -> return unless item?.saveAs? @@ -279,15 +289,14 @@ class Pane extends Model else false - # Private: copyActiveItem: -> if @activeItem? @activeItem.copy?() ? atom.deserializers.deserialize(@activeItem.serialize()) # Public: Creates a new pane to the left of the receiver. # - # * params: - # + items: An optional array of items with which to construct the new pane. + # params - An object with keys: + # :items - An optional array of items with which to construct the new pane. # # Returns the new {Pane}. splitLeft: (params) -> @@ -295,8 +304,8 @@ class Pane extends Model # Public: Creates a new pane to the right of the receiver. # - # * params: - # + items: An optional array of items with which to construct the new pane. + # params - An object with keys: + # :items - An optional array of items with which to construct the new pane. # # Returns the new {Pane}. splitRight: (params) -> @@ -304,8 +313,8 @@ class Pane extends Model # Public: Creates a new pane above the receiver. # - # * params: - # + items: An optional array of items with which to construct the new pane. + # params - An object with keys: + # :items - An optional array of items with which to construct the new pane. # # Returns the new {Pane}. splitUp: (params) -> @@ -313,14 +322,13 @@ class Pane extends Model # Public: Creates a new pane below the receiver. # - # * params: - # + items: An optional array of items with which to construct the new pane. + # params - An object with keys: + # :items - An optional array of items with which to construct the new pane. # # Returns the new {Pane}. splitDown: (params) -> @split('vertical', 'after', params) - # Private: split: (orientation, side, params) -> if @parent.orientation isnt orientation @parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this]})) @@ -333,7 +341,7 @@ class Pane extends Model newPane.activate() newPane - # Private: If the parent is a horizontal axis, returns its first child; + # If the parent is a horizontal axis, returns its first child; # otherwise this pane. findLeftmostSibling: -> if @parent.orientation is 'horizontal' @@ -341,7 +349,7 @@ class Pane extends Model else this - # Private: If the parent is a horizontal axis, returns its last child; + # If the parent is a horizontal axis, returns its last child; # otherwise returns a new pane created by splitting this pane rightward. findOrCreateRightmostSibling: -> if @parent.orientation is 'horizontal' diff --git a/src/pasteboard.coffee b/src/pasteboard.coffee deleted file mode 100644 index 2f97333d8..000000000 --- a/src/pasteboard.coffee +++ /dev/null @@ -1,36 +0,0 @@ -clipboard = require 'clipboard' -crypto = require 'crypto' - -# Public: Represents the clipboard used for copying and pasting in Atom. -# -# A pasteboard instance is always available under the `atom.pasteboard` global. -module.exports = -class Pasteboard - signatureForMetadata: null - - # Creates an `md5` hash of some text. - # - # text - A {String} to encrypt. - # - # Returns an encrypted {String}. - md5: (text) -> - crypto.createHash('md5').update(text, 'utf8').digest('hex') - - # Public: Write the given text to the clipboard. - # - # text - A {String} to store. - # metadata - An {Object} of additional info to associate with the text. - write: (text, metadata) -> - @signatureForMetadata = @md5(text) - @metadata = metadata - clipboard.writeText(text) - - # Public: Read the text from the clipboard. - # - # Returns an {Array}. The first element is the saved text and the second is - # any metadata associated with the text. - read: -> - text = clipboard.readText() - value = [text] - value.push(@metadata) if @signatureForMetadata == @md5(text) - value diff --git a/src/project.coffee b/src/project.coffee index 62f5f0dbe..d93ef4e4c 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -16,7 +16,7 @@ Git = require './git' # Public: Represents a project that's opened in Atom. # -# There is always a project available under the `atom.project` global. +# An instance of this class is always available as the `atom.project` global. module.exports = class Project extends Model atom.deserializers.add(this) @@ -30,11 +30,12 @@ class Project extends Model constructor: ({path, @buffers}={}) -> @buffers ?= [] + @openers = [] + for buffer in @buffers do (buffer) => buffer.once 'destroyed', => @removeBuffer(buffer) - @openers = [] @editors = [] @setPath(path) @@ -46,36 +47,16 @@ class Project extends Model params.buffers = params.buffers.map (bufferState) -> atom.deserializers.deserialize(bufferState) params - # Public: Register an opener for project files. - # - # An {Editor} will be used if no openers return a value. - # - # ## Example: - # ```coffeescript - # atom.project.registerOpener (filePath) -> - # if path.extname(filePath) is '.toml' - # return new TomlEditor(filePath) - # ``` - # - # * opener: A function to be called when a path is being opened. - registerOpener: (opener) -> @openers.push(opener) - - # Public: Remove a previously registered opener. - unregisterOpener: (opener) -> _.remove(@openers, opener) - - # Private: destroyed: -> editor.destroy() for editor in @getEditors() buffer.destroy() for buffer in @getBuffers() @destroyRepo() - # Private: destroyRepo: -> if @repo? @repo.destroy() @repo = null - # Private: destroyUnretainedBuffers: -> buffer.destroy() for buffer in @getBuffers() when not buffer.isRetained() @@ -111,8 +92,7 @@ class Project extends Model # the path is already absolute or if it is prefixed with a scheme, it is # returned unchanged. # - # * uri: - # The String name of the path to convert + # uri - The {String} name of the path to convert. # # Returns a String. resolve: (uri) -> @@ -133,71 +113,53 @@ class Project extends Model contains: (pathToCheck) -> @rootDirectory?.contains(pathToCheck) ? false - # Public: Given a path to a file, this constructs and associates a new + # Given a path to a file, this constructs and associates a new # {Editor}, showing the file. # - # * filePath: - # The {String} path of the file to associate with - # * options: - # Options that you can pass to the {Editor} constructor + # filePath - The {String} path of the file to associate with. + # options - Options that you can pass to the {Editor} constructor. # # Returns a promise that resolves to an {Editor}. open: (filePath, options={}) -> filePath = @resolve(filePath) - resource = null - _.find @openers, (opener) -> resource = opener(filePath, options) + @bufferForPath(filePath).then (buffer) => + @buildEditorForBuffer(buffer, options) - if resource - Q(resource) - else - @bufferForPath(filePath).then (buffer) => - @buildEditorForBuffer(buffer, options) - - # Private: Only be used in specs + # Deprecated openSync: (filePath, options={}) -> filePath = @resolve(filePath) - for opener in @openers - return resource if resource = opener(filePath, options) - @buildEditorForBuffer(@bufferForPathSync(filePath), options) - # Public: Retrieves all {Editor}s for all open files. - # - # Returns an {Array} of {Editor}s. - getEditors: -> - new Array(@editors...) - - # Public: Add the given {Editor}. + # Add the given {Editor}. addEditor: (editor) -> @editors.push editor @emit 'editor-created', editor - # Public: Return and removes the given {Editor}. + # Return and removes the given {Editor}. removeEditor: (editor) -> _.remove(@editors, editor) - # Private: Retrieves all the {TextBuffer}s in the project; that is, the + # Retrieves all the {TextBuffer}s in the project; that is, the # buffers for all open files. # # Returns an {Array} of {TextBuffer}s. getBuffers: -> @buffers.slice() - # Private: Is the buffer for the given path modified? + # Is the buffer for the given path modified? isPathModified: (filePath) -> @findBufferForPath(@resolve(filePath))?.isModified() - # Private: findBufferForPath: (filePath) -> _.find @buffers, (buffer) -> buffer.getPath() == filePath - # Private: Only to be used in specs + # Only to be used in specs bufferForPathSync: (filePath) -> absoluteFilePath = @resolve(filePath) existingBuffer = @findBufferForPath(absoluteFilePath) if filePath existingBuffer ? @buildBufferSync(absoluteFilePath) - # Private: Given a file path, this retrieves or creates a new {TextBuffer}. + # Given a file path, this retrieves or creates a new {TextBuffer}. # # If the `filePath` already has a `buffer`, that value is used instead. Otherwise, # `text` is used as the contents of the new buffer. @@ -210,21 +172,20 @@ class Project extends Model existingBuffer = @findBufferForPath(absoluteFilePath) if absoluteFilePath Q(existingBuffer ? @buildBuffer(absoluteFilePath)) - # Private: bufferForId: (id) -> _.find @buffers, (buffer) -> buffer.id is id - # Private: DEPRECATED + # DEPRECATED buildBufferSync: (absoluteFilePath) -> buffer = new TextBuffer({filePath: absoluteFilePath}) @addBuffer(buffer) buffer.loadSync() buffer - # Private: Given a file path, this sets its {TextBuffer}. + # Given a file path, this sets its {TextBuffer}. # - # absoluteFilePath - A {String} representing a path - # text - The {String} text to use as a buffer + # absoluteFilePath - A {String} representing a path. + # text - The {String} text to use as a buffer. # # Returns a promise that resolves to the {TextBuffer}. buildBuffer: (absoluteFilePath) -> @@ -234,38 +195,33 @@ class Project extends Model .then((buffer) -> buffer) .catch(=> @removeBuffer(buffer)) - # Private: addBuffer: (buffer, options={}) -> @addBufferAtIndex(buffer, @buffers.length, options) buffer.once 'destroyed', => @removeBuffer(buffer) - # Private: addBufferAtIndex: (buffer, index, options={}) -> @buffers.splice(index, 0, buffer) buffer.once 'destroyed', => @removeBuffer(buffer) @emit 'buffer-created', buffer buffer - # Private: Removes a {TextBuffer} association from the project. + # Removes a {TextBuffer} association from the project. # # Returns the removed {TextBuffer}. removeBuffer: (buffer) -> index = @buffers.indexOf(buffer) @removeBufferAtIndex(index) unless index is -1 - # Private: removeBufferAtIndex: (index, options={}) -> [buffer] = @buffers.splice(index, 1) buffer?.destroy() # Public: Performs a search across all the files in the project. # - # * regex: - # A RegExp to search with - # * options: - # - paths: an {Array} of glob patterns to search within - # * iterator: - # A Function callback on each file found + # regex - A {RegExp} to search with. + # options - An optional options {Object} (default: {}): + # :paths - An {Array} of glob patterns to search within + # iterator - A {Function} callback on each file found scan: (regex, options={}, iterator) -> if _.isFunction(options) iterator = options @@ -304,10 +260,11 @@ class Project extends Model # Public: Performs a replace across all the specified files in the project. # - # * regex: A RegExp to search with - # * replacementText: Text to replace all matches of regex with - # * filePaths: List of file path strings to run the replace on. - # * iterator: A Function callback on each file with replacements. `({filePath, replacements}) ->` + # regex - A {RegExp} to search with. + # replacementText - Text to replace all matches of regex with + # filePaths - List of file path strings to run the replace on. + # iterator - A {Function} callback on each file with replacements: + # `({filePath, replacements}) ->`. replace: (regex, replacementText, filePaths, iterator) -> deferred = Q.defer() @@ -339,18 +296,11 @@ class Project extends Model deferred.promise - # Private: buildEditorForBuffer: (buffer, editorOptions) -> editor = new Editor(_.extend({buffer}, editorOptions)) @addEditor(editor) editor - # Private: - eachEditor: (callback) -> - callback(editor) for editor in @getEditors() - @on 'editor-created', (editor) -> callback(editor) - - # Private: eachBuffer: (args...) -> subscriber = args.shift() if args.length > 1 callback = args.shift() @@ -360,3 +310,20 @@ class Project extends Model subscriber.subscribe this, 'buffer-created', (buffer) -> callback(buffer) else @on 'buffer-created', (buffer) -> callback(buffer) + + # Deprecated: delegate + registerOpener: (opener) -> + @openers.push(opener) + + # Deprecated: delegate + unregisterOpener: (opener) -> + _.remove(@openers, opener) + + # Deprecated: delegate + eachEditor: (callback) -> + callback(editor) for editor in @getEditors() + @on 'editor-created', (editor) -> callback(editor) + + # Deprecated: delegate + getEditors: -> + new Array(@editors...) diff --git a/src/scroll-view.coffee b/src/scroll-view.coffee index 7f09cf73d..c2e1aa7b1 100644 --- a/src/scroll-view.coffee +++ b/src/scroll-view.coffee @@ -2,8 +2,14 @@ # Public: Represents a view that scrolls. # -# This `View` subclass listens to events such as `page-up`, `page-down`, -# `move-to-top`, and `move-to-bottom`. +# Subclasses must call `super` if overriding the `initialize` method or else +# the following events won't be handled by the ScrollView. +# +# ## Events +# * `core:page-up` +# * `core:page-down` +# * `core:move-to-top` +# * `core:move-to-bottom` # # ## Requiring in packages # @@ -12,8 +18,6 @@ # ``` module.exports = class ScrollView extends View - - # Internal: The constructor. initialize: -> @on 'core:page-up', => @pageUp() @on 'core:page-down', => @pageDown() diff --git a/src/select-list-view.coffee b/src/select-list-view.coffee index fc2c52db5..9cf1885ae 100644 --- a/src/select-list-view.coffee +++ b/src/select-list-view.coffee @@ -12,8 +12,6 @@ fuzzyFilter = require('fuzzaldrin').filter # ``` module.exports = class SelectListView extends View - - # Private: @content: -> @div class: @viewClass(), => @subview 'miniEditor', new EditorView(mini: true) @@ -23,7 +21,6 @@ class SelectListView extends View @span class: 'badge', outlet: 'loadingBadge' @ol class: 'list-group', outlet: 'list' - # Private: @viewClass: -> 'select-list' maxItems: Infinity @@ -59,7 +56,6 @@ class SelectListView extends View @confirmSelection() if $(e.target).closest('li').hasClass('selected') e.preventDefault() - # Private: schedulePopulateList: -> clearTimeout(@scheduleTimeout) populateCallback = => @@ -68,14 +64,14 @@ class SelectListView extends View # Public: Set the array of items to display in the list. # - # * array: The array of model elements to display in the list. + # array - The {Array} of model elements to display in the list. setArray: (@array=[]) -> @populateList() @setLoading() # Public: Set the error message to display. # - # * message: The error message. + # message - The {String} error message (default: ''). setError: (message='') -> if message.length is 0 @error.text('').hide() @@ -85,7 +81,7 @@ class SelectListView extends View # Public: Set the loading message to display. # - # * message: The loading message. + # message - The {String} loading message (default: ''). setLoading: (message='') -> if message.length is 0 @loading.text("") @@ -135,30 +131,26 @@ class SelectListView extends View # # Subclasses may override this method to customize the message. # - # * itemCount: The number of items in the array specified to {.setArray} - # * filteredItemCount: The number of items that pass the fuzzy filter test. + # itemCount - The {Number} of items in the array specified to {.setArray} + # filteredItemCount - The {Number} of items that pass the fuzzy filter test. getEmptyMessage: (itemCount, filteredItemCount) -> 'No matches found' - # Private: selectPreviousItem: -> item = @getSelectedItem().prev() item = @list.find('li:last') unless item.length @selectItem(item) - # Private: selectNextItem: -> item = @getSelectedItem().next() item = @list.find('li:first') unless item.length @selectItem(item) - # Private: selectItem: (item) -> return unless item.length @list.find('.selected').removeClass('selected') item.addClass 'selected' @scrollToItem(item) - # Private: scrollToItem: (item) -> scrollTop = @list.scrollTop() desiredTop = item.position().top + scrollTop @@ -181,7 +173,6 @@ class SelectListView extends View getSelectedElement: -> @getSelectedItem().data('select-list-element') - # Private: confirmSelection: -> element = @getSelectedElement() if element? @@ -193,25 +184,21 @@ class SelectListView extends View # # This method should be overridden by subclasses. # - # * element: The selected model element. + # element - The selected model element. confirmed: (element) -> - # Private: attach: -> @storeFocusedElement() - # Private: storeFocusedElement: -> @previouslyFocusedElement = $(':focus') - # Private: restoreFocus: -> if @previouslyFocusedElement?.isOnDom() @previouslyFocusedElement.focus() else atom.workspaceView.focus() - # Private: cancelled: -> @miniEditor.getEditor().setText('') @miniEditor.updateDisplay() diff --git a/src/selection-view.coffee b/src/selection-view.coffee index beea12f47..e0413b30c 100644 --- a/src/selection-view.coffee +++ b/src/selection-view.coffee @@ -1,7 +1,6 @@ {Point, Range} = require 'text-buffer' {View, $$} = require './space-pen-extensions' -# Internal: module.exports = class SelectionView extends View diff --git a/src/selection.coffee b/src/selection.coffee index 915f3fb0b..6f5e50788 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -14,7 +14,6 @@ class Selection wordwise: false needsAutoscroll: null - # Private: constructor: ({@cursor, @marker, @editor}) -> @cursor.selection = this @marker.on 'changed', => @screenRangeChanged() @@ -23,18 +22,15 @@ class Selection @editor.removeSelection(this) @emit 'destroyed' unless @editor.isDestroyed() - # Private: destroy: -> @marker.destroy() - # Private: finalize: -> @initialScreenRange = null unless @initialScreenRange?.isEqual(@getScreenRange()) if @isEmpty() @wordwise = false @linewise = false - # Private: clearAutoscroll: -> @needsAutoscroll = null @@ -59,10 +55,8 @@ class Selection # Public: Modifies the screen range for the selection. # - # * screenRange: - # The new {Range} to use - # * options: - # + A hash of options matching those found in {.setBufferRange} + # screenRange - The new {Range} to use. + # options - A hash of options matching those found in {.setBufferRange}. setScreenRange: (screenRange, options) -> @setBufferRange(@editor.bufferRangeForScreenRange(screenRange), options) @@ -72,13 +66,11 @@ class Selection # Public: Modifies the buffer {Range} for the selection. # - # * screenRange: - # The new {Range} to select - # * options - # + preserveFolds: - # if `true`, the fold settings are preserved after the selection moves - # + autoscroll: - # if `true`, the {Editor} scrolls to the new selection + # screenRange - The new {Range} to select. + # options - An {Object} with the keys: + # :preserveFolds - if `true`, the fold settings are preserved after the + # selection moves. + # :autoscroll - if `true`, the {Editor} scrolls to the new selection. setBufferRange: (bufferRange, options={}) -> bufferRange = Range.fromObject(bufferRange) @needsAutoscroll = options.autoscroll @@ -128,8 +120,7 @@ class Selection # Public: Selects an entire line in the buffer. # - # * row: - # The line Number to select (default: the row of the cursor) + # row - The line {Number} to select (default: the row of the cursor). selectLine: (row=@cursor.getBufferPosition().row) -> range = @editor.bufferRangeForBufferRow(row, includeNewline: true) @setBufferRange(@getBufferRange().union(range)) @@ -148,8 +139,7 @@ class Selection # Public: Selects the text from the current cursor position to a given screen # position. # - # * position: - # An instance of {Point}, with a given `row` and `column`. + # position - An instance of {Point}, with a given `row` and `column`. selectToScreenPosition: (position) -> @modifySelection => if @initialScreenRange @@ -168,8 +158,7 @@ class Selection # Public: Selects the text from the current cursor position to a given buffer # position. # - # * position: - # An instance of {Point}, with a given `row` and `column`. + # position - An instance of {Point}, with a given `row` and `column`. selectToBufferPosition: (position) -> @modifySelection => @cursor.setBufferPosition(position) @@ -259,8 +248,6 @@ class Selection @editor.addSelectionForBufferRange(range, goalBufferRange: range) break - # Public: - # # FIXME: I have no idea what this does. getGoalBufferRange: -> @marker.getAttributes().goalBufferRange @@ -285,20 +272,14 @@ class Selection # Public: Replaces text at the current selection. # - # * text: - # A {String} representing the text to add - # * options - # + select: - # if `true`, selects the newly added text - # + autoIndent: - # if `true`, indents all inserted text appropriately - # + autoIndentNewline: - # if `true`, indent newline appropriately - # + autoDecreaseIndent: - # if `true`, decreases indent level appropriately (for example, when a - # closing bracket is inserted) - # + undo: - # if `skip`, skips the undo stack for this operation. + # text - A {String} representing the text to add + # options - An {Object} with keys: + # :select - if `true`, selects the newly added text. + # :autoIndent - if `true`, indents all inserted text appropriately. + # :autoIndentNewline - if `true`, indent newline appropriately. + # :autoDecreaseIndent - if `true`, decreases indent level appropriately + # (for example, when a closing bracket is inserted). + # :undo - if `skip`, skips the undo stack for this operation. insertText: (text, options={}) -> oldBufferRange = @getBufferRange() @editor.destroyFoldsContainingBufferRow(oldBufferRange.end.row) @@ -326,10 +307,8 @@ class Selection # Public: Indents the given text to the suggested level based on the grammar. # - # * text: - # The string to indent within the selection. - # * indentBasis: - # The beginning indent level. + # text - The {String} to indent within the selection. + # indentBasis - The beginning indent level. normalizeIndents: (text, indentBasis) -> textPrecedingCursor = @cursor.getCurrentBufferLine()[0...@cursor.getBufferColumn()] isCursorInsideExistingLine = /\S/.test(textPrecedingCursor) @@ -357,10 +336,9 @@ class Selection # Public: Indents the selection. # - # * options - A hash with one key, - # + autoIndent: - # If `true`, the indentation is performed appropriately. Otherwise, - # {Editor.getTabText} is used + # options - A {Object} with the keys: + # :autoIndent - If `true`, the indentation is performed appropriately. + # Otherwise, {Editor.getTabText} is used. indent: ({ autoIndent }={})-> { row, column } = @cursor.getBufferPosition() @@ -505,35 +483,25 @@ class Selection @editor.toggleLineCommentsForBufferRows(@getBufferRowRange()...) # Public: Cuts the selection until the end of the line. - # - # * maintainPasteboard: - # ? - cutToEndOfLine: (maintainPasteboard) -> + cutToEndOfLine: (maintainClipboard) -> @selectToEndOfLine() if @isEmpty() - @cut(maintainPasteboard) + @cut(maintainClipboard) - # Public: Copies the selection to the pasteboard and then deletes it. - # - # * maintainPasteboard: - # ? - cut: (maintainPasteboard=false) -> - @copy(maintainPasteboard) + # Public: Copies the selection to the clipboard and then deletes it. + cut: (maintainClipboard=false) -> + @copy(maintainClipboard) @delete() - # Public: Copies the current selection to the pasteboard. - # - # * maintainPasteboard: - # ? - copy: (maintainPasteboard=false) -> + # Public: Copies the current selection to the clipboard. + copy: (maintainClipboard=false) -> return if @isEmpty() text = @editor.buffer.getTextInRange(@getBufferRange()) - if maintainPasteboard - [currentText, metadata] = atom.pasteboard.read() - text = currentText + '\n' + text + if maintainClipboard + text = "#{atom.clipboard.read()}\n#{text}" else metadata = { indentBasis: @editor.indentationForBufferRow(@getBufferRange().start.row) } - atom.pasteboard.write(text, metadata) + atom.clipboard.write(text, metadata) # Public: Creates a fold containing the current selection. fold: -> @@ -541,14 +509,13 @@ class Selection @editor.createFold(range.start.row, range.end.row) @cursor.setBufferPosition([range.end.row + 1, 0]) - # Public: ? modifySelection: (fn) -> @retainSelection = true @plantTail() fn() @retainSelection = false - # Private: Sets the marker's tail to the same position as the marker's head. + # Sets the marker's tail to the same position as the marker's head. # # This only works if there isn't already a tail position. # @@ -558,8 +525,7 @@ class Selection # Public: Identifies if a selection intersects with a given buffer range. # - # * bufferRange: - # A {Range} to check against + # bufferRange - A {Range} to check against. # # Returns a Boolean. intersectsBufferRange: (bufferRange) -> @@ -567,8 +533,7 @@ class Selection # Public: Identifies if a selection intersects with another selection. # - # * otherSelection: - # A {Selection} to check against + # otherSelection - A {Selection} to check against. # # Returns a Boolean. intersectsWith: (otherSelection) -> @@ -577,10 +542,8 @@ class Selection # Public: Combines the given selection into this selection and then destroys # the given selection. # - # * otherSelection: - # A {Selection} to merge with - # * options - # + A hash of options matching those found in {.setBufferRange} + # otherSelection - A {Selection} to merge with. + # options - A hash of options matching those found in {.setBufferRange}. merge: (otherSelection, options) -> myGoalBufferRange = @getGoalBufferRange() otherGoalBufferRange = otherSelection.getGoalBufferRange() @@ -596,12 +559,10 @@ class Selection # # See {Range.compare} for more details. # - # * otherSelection: - # A {Selection} to compare with. + # otherSelection - A {Selection} to compare against. compare: (otherSelection) -> @getBufferRange().compare(otherSelection.getBufferRange()) - # Private: screenRangeChanged: -> screenRange = @getScreenRange() @emit 'screen-range-changed', screenRange diff --git a/src/space-pen-extensions.coffee b/src/space-pen-extensions.coffee index abaa242cb..04e411114 100644 --- a/src/space-pen-extensions.coffee +++ b/src/space-pen-extensions.coffee @@ -1,18 +1,13 @@ _ = require 'underscore-plus' spacePen = require 'space-pen' {Subscriber} = require 'emissary' -ConfigObserver = require './config-observer' -ConfigObserver.includeInto(spacePen.View) Subscriber.includeInto(spacePen.View) jQuery = spacePen.jQuery originalCleanData = jQuery.cleanData jQuery.cleanData = (elements) -> - for element in elements - if view = jQuery(element).view() - view.unobserveConfig() - view.unsubscribe() + jQuery(element).view()?.unsubscribe() for element in elements originalCleanData(elements) tooltipDefaults = diff --git a/src/syntax.coffee b/src/syntax.coffee index af1a6278a..881d5fa2a 100644 --- a/src/syntax.coffee +++ b/src/syntax.coffee @@ -8,14 +8,13 @@ Token = require './token' # Public: Syntax class holding the grammars used for tokenizing. # +# An instance of this class is always available as the `atom.syntax` global. +# # The Syntax class also contains properties for things such as the # language-specific comment regexes. -# -# There is always a syntax object available under the `atom.syntax` global. module.exports = class Syntax extends GrammarRegistry Subscriber.includeInto(this) - atom.deserializers.add(this) @deserialize: ({grammarOverridesByPath}) -> @@ -62,8 +61,8 @@ class Syntax extends GrammarRegistry # console.log(comment) # '# ' # ``` # - # * scope: An {Array} of {String} scopes. - # * keyPath: A {String} key path. + # scope - An {Array} of {String} scopes. + # keyPath - A {String} key path. # # Returns a {String} property value or undefined. getProperty: (scope, keyPath) -> diff --git a/src/task.coffee b/src/task.coffee index 538bb9f98..8e2b57277 100644 --- a/src/task.coffee +++ b/src/task.coffee @@ -24,18 +24,16 @@ class Task # Public: A helper method to easily launch and run a task once. # - # * taskPath: - # The path to the Coffeescript/Javascript file which exports a single - # function to execute. - # * args: - # The Array of arguments to pass to the exported function. + # taskPath - The {String} path to the CoffeeScript/JavaScript file which + # exports a single {Function} to execute. + # args - The arguments to pass to the exported function. @once: (taskPath, args...) -> task = new Task(taskPath) task.once 'task:completed', -> task.terminate() task.start(args...) task - # Public: Called upon task completion. + # Called upon task completion. # # It receives the same arguments that were passed to the task. # @@ -45,9 +43,8 @@ class Task # Public: Creates a task. # - # * taskPath: - # The path to the Coffeescript/Javascript file that exports a single - # function to execute. + # taskPath - The {String} path to the CoffeeScript/JavaScript file that + # exports a single {Function} to execute. constructor: (taskPath) -> coffeeCacheRequire = "require('#{require.resolve('./coffee-cache')}').register();" coffeeScriptRequire = "require('#{require.resolve('coffee-script')}').register();" @@ -73,7 +70,7 @@ class Task @handleEvents() - # Private: Routes messages from the child to the appropriate event. + # Routes messages from the child to the appropriate event. handleEvents: -> @childProcess.removeAllListeners() @childProcess.on 'message', ({event, args}) => @@ -81,21 +78,21 @@ class Task # Public: Starts the task. # - # * args: - # The Array of arguments to pass to the function exported by the script. If - # the last argument is a function, its removed from the array and called - # upon completion (and replaces the complete function on the task instance). - start: (args...) -> + # args - The arguments to pass to the function exported by this task's script. + # callback - An optional {Function} to call when the task completes. + start: (args..., callback) -> throw new Error("Cannot start terminated process") unless @childProcess? @handleEvents() - @callback = args.pop() if _.isFunction(args[args.length - 1]) + if _.isFunction(callback) + @callback = callback + else + args = arguments @send({event: 'start', args}) # Public: Send message to the task. # - # * message: - # The message to send + # message - The message to send to the task. send: (message) -> throw new Error("Cannot send message to terminated process") unless @childProcess? @childProcess.send(message) diff --git a/src/text-buffer.coffee b/src/text-buffer.coffee index 19ea180b9..c884e7e0e 100644 --- a/src/text-buffer.coffee +++ b/src/text-buffer.coffee @@ -8,7 +8,7 @@ TextBufferCore = require 'text-buffer' File = require './file' -# Private: Represents the contents of a file. +# Represents the contents of a file. # # The `TextBuffer` is often associated with a {File}. However, this is not always # the case, as a `TextBuffer` could be an unsaved chunk of text. @@ -40,7 +40,6 @@ class TextBuffer extends TextBufferCore @load() if loadWhenAttached - # Private: serializeParams: -> params = super _.extend params, @@ -48,7 +47,6 @@ class TextBuffer extends TextBufferCore modifiedWhenLastPersisted: @isModified() digestWhenLastPersisted: @file?.getDigest() - # Private: deserializeParams: (params) -> params = super(params) params.loadWhenAttached = true @@ -71,8 +69,6 @@ class TextBuffer extends TextBufferCore @clearUndoStack() this - ### Internal ### - handleTextChange: (event) => @conflict = false if @conflict and !@isModified() @scheduleModifiedEvents() @@ -127,8 +123,6 @@ class TextBuffer extends TextBufferCore @file.on "moved", => @emit "path-changed", this - ### Public ### - # Identifies if the buffer belongs to multiple editors. # # For example, if the {EditorView} was split. @@ -145,11 +139,11 @@ class TextBuffer extends TextBufferCore @emitModifiedStatusChanged(false) @emit 'reloaded' - # Private: Rereads the contents of the file, and stores them in the cache. + # Rereads the contents of the file, and stores them in the cache. updateCachedDiskContentsSync: -> @cachedDiskContents = @file?.readSync() ? "" - # Private: Rereads the contents of the file, and stores them in the cache. + # Rereads the contents of the file, and stores them in the cache. updateCachedDiskContents: -> Q(@file?.read() ? "").then (contents) => @cachedDiskContents = contents @@ -171,29 +165,20 @@ class TextBuffer extends TextBufferCore # Sets the path for the file. # - # path - A {String} representing the new file path - setPath: (path) -> - return if path == @getPath() + # filePath - A {String} representing the new file path + setPath: (filePath) -> + return if filePath == @getPath() @file?.off() - if path - @file = new File(path) + if filePath + @file = new File(filePath) @subscribeToFile() else @file = null @emit "path-changed", this - # Retrieves the current buffer's file extension. - # - # Returns a {String}. - getExtension: -> - if @getPath() - @getPath().split('/').pop().split('.').pop() - else - null - # Deprecated: Use ::getEndPosition instead getEofPosition: -> @getEndPosition() @@ -203,14 +188,15 @@ class TextBuffer extends TextBufferCore # Saves the buffer at a specific path. # - # path - The path to save at. - saveAs: (path) -> - unless path then throw new Error("Can't save buffer with no file path") + # filePath - The path to save at. + saveAs: (filePath) -> + unless filePath then throw new Error("Can't save buffer with no file path") @emit 'will-be-saved', this - @setPath(path) - @cachedDiskContents = @getText() + @setPath(filePath) @file.write(@getText()) + @cachedDiskContents = @getText() + @conflict = false @emitModifiedStatusChanged(false) @emit 'saved', this @@ -227,7 +213,10 @@ class TextBuffer extends TextBufferCore else not @isEmpty() - # Identifies if a buffer is in a git conflict with `HEAD`. + # Is the buffer's text in conflict with the text on disk? + # + # This occurs when the buffer's file changes on disk while the buffer has + # unsaved changes. # # Returns a {Boolean}. isInConflict: -> @conflict @@ -390,8 +379,6 @@ class TextBuffer extends TextBufferCore return match[0][0] != '\t' undefined - ### Internal ### - change: (oldRange, newText, options={}) -> @setTextInRange(oldRange, newText, options.normalizeLineEndings) diff --git a/src/text-mate-package.coffee b/src/text-mate-package.coffee index 3daf8d353..5e39ab53b 100644 --- a/src/text-mate-package.coffee +++ b/src/text-mate-package.coffee @@ -2,9 +2,7 @@ Package = require './package' path = require 'path' _ = require 'underscore-plus' fs = require 'fs-plus' -async = require 'async' - -### Internal ### +Q = require 'q' module.exports = class TextMatePackage extends Package @@ -12,13 +10,12 @@ class TextMatePackage extends Package packageName = path.basename(packageName) /(^language-.+)|((\.|_|-)tmbundle$)/.test(packageName) - @getLoadQueue: -> - return @loadQueue if @loadQueue - @loadQueue = async.queue (pack, done) -> - pack.loadGrammars -> - pack.loadScopedProperties(done) - - @loadQueue + @addToActivationPromise = (pack) -> + @activationPromise ?= Q() + @activationPromise = @activationPromise.then => + pack.loadGrammars() + .then -> pack.loadScopedProperties() + .fail (error) -> console.log pack.name, error constructor: -> super @@ -28,21 +25,16 @@ class TextMatePackage extends Package getType: -> 'textmate' - load: ({sync}={}) -> + load: -> @measure 'loadTime', => @metadata = Package.loadMetadata(@path, true) - if sync - @loadGrammarsSync() - @loadScopedPropertiesSync() - else - TextMatePackage.getLoadQueue().push(this) + activate: ({sync, immediate}={})-> + TextMatePackage.addToActivationPromise(this) - activate: -> - @measure 'activateTime', => - grammar.activate() for grammar in @grammars - for { selector, properties } in @scopedProperties - atom.syntax.addProperties(@path, selector, properties) + activateSync: -> + @loadGrammarsSync() + @loadScopedPropertiesSync() activateConfig: -> # noop @@ -52,33 +44,35 @@ class TextMatePackage extends Package legalGrammarExtensions: ['plist', 'tmLanguage', 'tmlanguage', 'json', 'cson'] - loadGrammars: (done) -> + loadGrammars: -> + deferred = Q.defer() fs.isDirectory @getSyntaxesPath(), (isDirectory) => - if isDirectory - fs.list @getSyntaxesPath(), @legalGrammarExtensions, (error, paths) => - if error? - console.log("Error loading grammars of TextMate package '#{@path}':", error.stack, error) - done() - else - async.eachSeries(paths, @loadGrammarAtPath, done) - else - done() + return deferred.resolve() unless isDirectory - loadGrammarAtPath: (grammarPath, done) => + fs.list @getSyntaxesPath(), @legalGrammarExtensions, (error, paths) => + if error? + console.log("Error loading grammars of TextMate package '#{@path}':", error.stack, error) + deferred.resolve() + else + promises = paths.map (path) => @loadGrammarAtPath(path) + Q.all(promises).then -> deferred.resolve() + + deferred.promise + + loadGrammarAtPath: (grammarPath) -> + deferred = Q.defer() atom.syntax.readGrammar grammarPath, (error, grammar) => if error? console.log("Error loading grammar at path '#{grammarPath}':", error.stack ? error) else @addGrammar(grammar) - done?() + deferred.resolve() - loadGrammarsSync: -> - for grammarPath in fs.listSync(@getSyntaxesPath(), @legalGrammarExtensions) - @addGrammar(atom.syntax.readGrammarSync(grammarPath)) + deferred.promise addGrammar: (grammar) -> @grammars.push(grammar) - grammar.activate() if @isActive() + grammar.activate() getGrammars: -> @grammars @@ -96,23 +90,7 @@ class TextMatePackage extends Package else path.join(@path, "Preferences") - loadScopedPropertiesSync: -> - for grammar in @getGrammars() - if properties = @propertiesFromTextMateSettings(grammar) - selector = atom.syntax.cssSelectorFromScopeSelector(grammar.scopeName) - @scopedProperties.push({selector, properties}) - - for preferencePath in fs.listSync(@getPreferencesPath()) - {scope, settings} = fs.readObjectSync(preferencePath) - if properties = @propertiesFromTextMateSettings(settings) - selector = atom.syntax.cssSelectorFromScopeSelector(scope) if scope? - @scopedProperties.push({selector, properties}) - - if @isActive() - for {selector, properties} in @scopedProperties - atom.syntax.addProperties(@path, selector, properties) - - loadScopedProperties: (callback) -> + loadScopedProperties: -> scopedProperties = [] for grammar in @getGrammars() @@ -120,38 +98,37 @@ class TextMatePackage extends Package selector = atom.syntax.cssSelectorFromScopeSelector(grammar.scopeName) scopedProperties.push({selector, properties}) - preferenceObjects = [] - done = => + @loadTextMatePreferenceObjects().then (preferenceObjects=[]) => for {scope, settings} in preferenceObjects if properties = @propertiesFromTextMateSettings(settings) selector = atom.syntax.cssSelectorFromScopeSelector(scope) if scope? scopedProperties.push({selector, properties}) @scopedProperties = scopedProperties - if @isActive() - for {selector, properties} in @scopedProperties - atom.syntax.addProperties(@path, selector, properties) - callback?() - @loadTextMatePreferenceObjects(preferenceObjects, done) + for {selector, properties} in @scopedProperties + atom.syntax.addProperties(@path, selector, properties) - loadTextMatePreferenceObjects: (preferenceObjects, done) -> + loadTextMatePreferenceObjects: -> + deferred = Q.defer() fs.isDirectory @getPreferencesPath(), (isDirectory) => - return done() unless isDirectory - + return deferred.resolve() unless isDirectory fs.list @getPreferencesPath(), (error, paths) => if error? console.log("Error loading preferences of TextMate package '#{@path}':", error.stack, error) - done() - return + deferred.resolve() + else + promises = paths.map (path) => @loadPreferencesAtPath(path) + Q.all(promises).then (preferenceObjects) -> deferred.resolve(preferenceObjects) - loadPreferencesAtPath = (preferencePath, done) -> - fs.readObject preferencePath, (error, preferences) => - if error? - console.warn("Failed to parse preference at path '#{preferencePath}'", error.stack, error) - else - preferenceObjects.push(preferences) - done() - async.eachSeries paths, loadPreferencesAtPath, done + deferred.promise + + loadPreferencesAtPath: (preferencePath) -> + deferred = Q.defer() + fs.readObject preferencePath, (error, preference) -> + if error? + console.warn("Failed to parse preference at path '#{preferencePath}'", error.stack, error) + deferred.resolve(preference) + deferred.promise propertiesFromTextMateSettings: (textMateSettings) -> if textMateSettings.shellVariables @@ -169,3 +146,24 @@ class TextMatePackage extends Package completions: textMateSettings.completions ) { editor: editorProperties } if _.size(editorProperties) > 0 + + # Deprecated + loadGrammarsSync: -> + for grammarPath in fs.listSync(@getSyntaxesPath(), @legalGrammarExtensions) + @addGrammar(atom.syntax.readGrammarSync(grammarPath)) + + # Deprecated + loadScopedPropertiesSync: -> + for grammar in @getGrammars() + if properties = @propertiesFromTextMateSettings(grammar) + selector = atom.syntax.cssSelectorFromScopeSelector(grammar.scopeName) + @scopedProperties.push({selector, properties}) + + for preferencePath in fs.listSync(@getPreferencesPath()) + {scope, settings} = fs.readObjectSync(preferencePath) + if properties = @propertiesFromTextMateSettings(settings) + selector = atom.syntax.cssSelectorFromScopeSelector(scope) if scope? + @scopedProperties.push({selector, properties}) + + for {selector, properties} in @scopedProperties + atom.syntax.addProperties(@path, selector, properties) diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index b26d366bf..08ecc0d47 100644 --- a/src/theme-manager.coffee +++ b/src/theme-manager.coffee @@ -3,6 +3,7 @@ path = require 'path' _ = require 'underscore-plus' {Emitter} = require 'emissary' fs = require 'fs-plus' +Q = require 'q' {$} = require './space-pen-extensions' AtomPackage = require './atom-package' @@ -10,7 +11,7 @@ File = require './file' # Public: Handles loading and activating available themes. # -# A ThemeManager instance is always available under the `atom.themes` global. +# An instance of this class is always available as the `atom.themes` global. module.exports = class ThemeManager Emitter.includeInto(this) @@ -41,7 +42,7 @@ class ThemeManager activatePackages: (themePackages) -> @activateThemes() - # Private: Get the enabled theme names from the config. + # Get the enabled theme names from the config. # # Returns an array of theme names in the order that they should be activated. getEnabledThemeNames: -> @@ -53,19 +54,22 @@ class ThemeManager themeNames.reverse() activateThemes: -> + deferred = Q.defer() + # atom.config.observe runs the callback once, then on subsequent changes. atom.config.observe 'core.themes', => @deactivateThemes() @refreshLessCache() # Update cache for packages in core.themes config - for themeName in @getEnabledThemeNames() - @packageManager.activatePackage(themeName) + promises = @getEnabledThemeNames().map (themeName) => @packageManager.activatePackage(themeName) + Q.all(promises).then => + @refreshLessCache() # Update cache again now that @getActiveThemes() is populated + @loadUserStylesheet() + @reloadBaseStylesheets() + @emit('reloaded') + deferred.resolve() - @refreshLessCache() # Update cache again now that @getActiveThemes() is populated - @loadUserStylesheet() - @reloadBaseStylesheets() - - @emit('reloaded') + deferred.promise deactivateThemes: -> @unwatchUserStylesheet() @@ -77,7 +81,7 @@ class ThemeManager # Public: Set the list of enabled themes. # - # * enabledThemeNames: An {Array} of {String} theme names. + # enabledThemeNames - An {Array} of {String} theme names. setEnabledThemes: (enabledThemeNames) -> atom.config.set('core.themes', enabledThemeNames) @@ -91,7 +95,7 @@ class ThemeManager if themePath = @packageManager.resolvePackagePath(themeName) themePaths.push(path.join(themePath, AtomPackage.stylesheetsDir)) - themePath for themePath in themePaths when fs.isDirectorySync(themePath) + themePaths.filter (themePath) -> fs.isDirectorySync(themePath) # Public: Returns the {String} path to the user's stylesheet under ~/.atom getUserStylesheetPath: -> @@ -140,8 +144,9 @@ class ThemeManager # # This supports both CSS and LESS stylsheets. # - # * stylesheetPath: A {String} path to the stylesheet that can be an absolute - # path or a relative path that will be resolved against the load path. + # stylesheetPath - A {String} path to the stylesheet that can be an absolute + # path or a relative path that will be resolved against the + # load path. # # Returns the absolute path to the required stylesheet. requireStylesheet: (stylesheetPath, ttype = 'bundled', htmlElement) -> diff --git a/src/theme-package.coffee b/src/theme-package.coffee index 2115276fd..2d4f611a6 100644 --- a/src/theme-package.coffee +++ b/src/theme-package.coffee @@ -1,11 +1,8 @@ +Q = require 'q' AtomPackage = require './atom-package' -Package = require './package' - -### Internal: Loads and resolves packages. ### module.exports = class ThemePackage extends AtomPackage - getType: -> 'theme' getStylesheetType: -> 'theme' @@ -25,6 +22,11 @@ class ThemePackage extends AtomPackage this activate: -> + return @activationDeferred.promise if @activationDeferred? + + @activationDeferred = Q.defer() @measure 'activateTime', => @loadStylesheets() @activateNow() + + @activationDeferred.promise diff --git a/src/token.coffee b/src/token.coffee index 058f73fb5..366c6a394 100644 --- a/src/token.coffee +++ b/src/token.coffee @@ -12,7 +12,7 @@ WhitespaceRegex = /\S/ MaxTokenLength = 20000 -# Private: Represents a single unit of text as selected by a grammar. +# Represents a single unit of text as selected by a grammar. module.exports = class Token value: null @@ -21,15 +21,11 @@ class Token isAtomic: null isHardTab: null - ### Internal ### - constructor: ({@value, @scopes, @isAtomic, @bufferDelta, @isHardTab}) -> @screenDelta = @value.length @bufferDelta ?= @screenDelta @hasSurrogatePair = textUtils.hasSurrogatePair(@value) - ### Public ### - isEqual: (other) -> @value == other.value and _.isEqual(@scopes, other.scopes) and !!@isAtomic == !!other.isAtomic diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index f6a4c3681..7ee7d1682 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -5,8 +5,6 @@ Serializable = require 'serializable' TokenizedLine = require './tokenized-line' Token = require './token' -### Internal ### - module.exports = class TokenizedBuffer extends Model Serializable.includeInto(this) diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee index 826fdfd5c..75a09497c 100644 --- a/src/tokenized-line.coffee +++ b/src/tokenized-line.coffee @@ -1,7 +1,5 @@ _ = require 'underscore-plus' -### Internal ### - module.exports = class TokenizedLine constructor: ({tokens, @lineEnding, @ruleStack, @startBufferColumn, @fold, tabLength}) -> diff --git a/src/window-bootstrap.coffee b/src/window-bootstrap.coffee index dc8c46c42..42ec7cd91 100644 --- a/src/window-bootstrap.coffee +++ b/src/window-bootstrap.coffee @@ -1,9 +1,6 @@ # Like sands through the hourglass, so are the days of our lives. startTime = Date.now() -# Start the crash reporter before anything else. -require('crash-reporter').start(productName: 'Atom', companyName: 'GitHub') - require './window' Atom = require './atom' diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index 751c7ca19..8cf637cc6 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -5,7 +5,7 @@ shell = require 'shell' {Subscriber} = require 'emissary' fs = require 'fs-plus' -# Private: Handles low-level events related to the window. +# Handles low-level events related to the window. module.exports = class WindowEventHandler Subscriber.includeInto(this) @@ -75,7 +75,7 @@ class WindowEventHandler @handleNativeKeybindings() - # Private: Wire commands that should be handled by the native menu + # Wire commands that should be handled by the native menu # for elements with the `.native-key-bindings` class. handleNativeKeybindings: -> menu = null diff --git a/src/window.coffee b/src/window.coffee index 891515ed9..9554218ca 100644 --- a/src/window.coffee +++ b/src/window.coffee @@ -1,9 +1,8 @@ # Public: Measure how long a function takes to run. # -# * description: -# A {String} description that will be logged to the console. -# * fn: -# A {Function} to measure the duration of. +# description - A {String} description that will be logged to the console when +# the function completes. +# fn - A {Function} to measure the duration of. # # Returns the value returned by the given function. window.measure = (description, fn) -> @@ -15,13 +14,11 @@ window.measure = (description, fn) -> # Public: Create a dev tools profile for a function. # -# * description: -# A {String} descrption that will be available in the Profiles tab of the dev -# tools. -# * fn: -# A {Function} to profile. +# description - A {String} description that will be available in the Profiles +# tab of the dev tools. +# fn - A {Function} to profile. # -# Return the value returned by the given function. +# Returns the value returned by the given function. window.profile = (description, fn) -> measure description, -> console.profile(description) diff --git a/src/workspace-view.coffee b/src/workspace-view.coffee index a45b86b04..a83253a89 100644 --- a/src/workspace-view.coffee +++ b/src/workspace-view.coffee @@ -16,6 +16,9 @@ Editor = require './editor' # Public: The container for the entire Atom application. # +# An instance of this class is always available as the `atom.workspaceView` +# global. +# # ## Commands # # * `application:about` - Opens the about dialog. @@ -48,7 +51,7 @@ class WorkspaceView extends View Delegator.includeInto(this) @delegatesProperty 'fullScreen', 'destroyedItemUris', toProperty: 'model' - @delegatesMethods 'open', 'openSync', 'openSingletonSync', 'reopenItemSync', + @delegatesMethods 'open', 'openSync', 'reopenItemSync', 'saveActivePaneItem', 'saveActivePaneItemAs', 'saveAll', 'destroyActivePaneItem', 'destroyActivePane', 'increaseFontSize', 'decreaseFontSize', toProperty: 'model' @@ -63,14 +66,12 @@ class WorkspaceView extends View audioBeep: true destroyEmptyPanes: false - # Private: @content: -> @div class: 'workspace', tabindex: -1, => @div class: 'horizontal', outlet: 'horizontal', => @div class: 'vertical', outlet: 'vertical', => @div class: 'panes', outlet: 'panes' - # Private: initialize: (@model) -> @model ?= new Workspace @@ -106,6 +107,7 @@ class WorkspaceView extends View @command 'application:zoom', -> ipc.sendChannel('command', 'application:zoom') @command 'application:bring-all-windows-to-front', -> ipc.sendChannel('command', 'application:bring-all-windows-to-front') @command 'application:open-your-config', -> ipc.sendChannel('command', 'application:open-your-config') + @command 'application:open-your-init-script', -> ipc.sendChannel('command', 'application:open-your-init-script') @command 'application:open-your-keymap', -> ipc.sendChannel('command', 'application:open-your-keymap') @command 'application:open-your-snippets', -> ipc.sendChannel('command', 'application:open-your-snippets') @command 'application:open-your-stylesheet', -> ipc.sendChannel('command', 'application:open-your-stylesheet') @@ -115,9 +117,14 @@ class WorkspaceView extends View @command 'window:run-package-specs', => ipc.sendChannel('run-package-specs', path.join(atom.project.getPath(), 'spec')) @command 'window:increase-font-size', => @increaseFontSize() @command 'window:decrease-font-size', => @decreaseFontSize() + @command 'window:reset-font-size', => @model.resetFontSize() @command 'window:focus-next-pane', => @focusNextPane() @command 'window:focus-previous-pane', => @focusPreviousPane() + @command 'window:focus-pane-above', => @focusPaneAbove() + @command 'window:focus-pane-below', => @focusPaneBelow() + @command 'window:focus-pane-on-left', => @focusPaneOnLeft() + @command 'window:focus-pane-on-right', => @focusPaneOnRight() @command 'window:save-all', => @saveAll() @command 'window:toggle-invisibles', => atom.config.toggle("editor.showInvisibles") @@ -135,23 +142,22 @@ class WorkspaceView extends View showErrorDialog = (error) -> installDirectory = CommandInstaller.getInstallDirectory() atom.confirm - message: error.message - detailedMessage: "Make sure #{installDirectory} exists and is writable. Run 'sudo mkdir -p #{installDirectory} && sudo chown $USER #{installDirectory}' to fix this problem." + message: "Failed to install shell commands" + detailedMessage: error.message resourcePath = atom.getLoadSettings().resourcePath - CommandInstaller.installAtomCommand resourcePath, (error) => + CommandInstaller.installAtomCommand resourcePath, true, (error) => if error? - showDialog(error) + showErrorDialog(error) else - CommandInstaller.installApmCommand resourcePath, (error) => + CommandInstaller.installApmCommand resourcePath, true, (error) => if error? - showDialog(error) + showErrorDialog(error) else atom.confirm message: "Commands installed." detailedMessage: "The shell commands `atom` and `apm` are installed." - # Private: handleFocus: (e) -> if @getActivePane() @getActivePane().focus() @@ -166,7 +172,6 @@ class WorkspaceView extends View $(document.body).focus() true - # Private: afterAttach: (onDom) -> @focus() if onDom @@ -188,7 +193,7 @@ class WorkspaceView extends View setTitle: (title) -> document.title = title - # Private: Returns an Array of all of the application's {EditorView}s. + # Returns an Array of all of the application's {EditorView}s. getEditorViews: -> @panes.find('.pane > .item-views > .editor').map(-> $(this).view()).toArray() @@ -225,9 +230,13 @@ class WorkspaceView extends View @horizontal.append(element) # Public: Returns the currently focused {PaneView}. - getActivePane: -> + getActivePaneView: -> @panes.getActivePane() + # Deprecated: Returns the currently focused {PaneView}. + getActivePane: -> + @getActivePaneView() + # Public: Returns the currently focused item from within the focused {PaneView} getActivePaneItem: -> @model.activePaneItem @@ -242,6 +251,18 @@ class WorkspaceView extends View # Public: Focuses the next pane by id. focusNextPane: -> @model.activateNextPane() + # Public: Focuses the pane directly above the active pane. + focusPaneAbove: -> @panes.focusPaneAbove() + + # Public: Focuses the pane directly below the active pane. + focusPaneBelow: -> @panes.focusPaneBelow() + + # Public: Focuses the pane directly to the left of the active pane. + focusPaneOnLeft: -> @panes.focusPaneOnLeft() + + # Public: Focuses the pane directly to the right of the active pane. + focusPaneOnRight: -> @panes.focusPaneOnRight() + # Public: # # FIXME: Difference between active and focused pane? @@ -266,11 +287,11 @@ class WorkspaceView extends View @on('editor:attached', attachedCallback) off: => @off('editor:attached', attachedCallback) - # Private: Called by SpacePen + # Called by SpacePen beforeRemove: -> @model.destroy() - # Private: Destroys everything. + # Destroys everything. remove: -> editorView.remove() for editorView in @getEditorViews() super diff --git a/src/workspace.coffee b/src/workspace.coffee index 21663bb59..8b8138b67 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -7,8 +7,9 @@ PaneContainer = require './pane-container' Pane = require './pane' # Public: Represents the view state of the entire window, including the panes at -# the center and panels around the periphery. You can access the singleton -# instance via `atom.workspace`. +# the center and panels around the periphery. +# +# An instance of this class is always available as the `atom.workspace` global. module.exports = class Workspace extends Model atom.deserializers.add(this) @@ -23,11 +24,10 @@ class Workspace extends Model fullScreen: false destroyedItemUris: -> [] - # Private: constructor: -> super @subscribe @paneContainer, 'item-destroyed', @onPaneItemDestroyed - atom.project.registerOpener (filePath) => + @registerOpener (filePath) => switch filePath when 'atom://.atom/stylesheet' @open(atom.themes.getUserStylesheetPath()) @@ -35,93 +35,126 @@ class Workspace extends Model @open(atom.keymap.getUserKeymapPath()) when 'atom://.atom/config' @open(atom.config.getUserConfigPath()) + when 'atom://.atom/init-script' + @open(atom.getUserInitScriptPath()) - # Private: Called by the Serializable mixin during deserialization + # Called by the Serializable mixin during deserialization deserializeParams: (params) -> params.paneContainer = PaneContainer.deserialize(params.paneContainer) params - # Private: Called by the Serializable mixin during serialization. + # Called by the Serializable mixin during serialization. serializeParams: -> paneContainer: @paneContainer.serialize() fullScreen: atom.isFullScreen() + # Public: Calls callback for every existing {Editor} and for all new {Editors} + # that are created. + # + # callback - A {Function} with an {Editor} as its only argument + eachEditor: (callback) -> + atom.project.eachEditor(callback) + + # Public: Returns an {Array} of all open {Editor}s. + getEditors: -> + atom.project.getEditors() + # Public: Asynchronously opens a given a filepath in Atom. # - # * filePath: A file path - # * options - # + initialLine: The buffer line number to open to. + # uri - A {String} uri. + # options - An options {Object} (default: {}). + # :initialLine - A {Number} indicating which line number to open to. + # :split - A {String} ('left' or 'right') that opens the filePath in a new + # pane or an existing one if it exists. + # :changeFocus - A {Boolean} that allows the filePath to be opened without + # changing focus. + # :searchAllPanes - A {Boolean} that will open existing editors from any pane + # if the uri is already open (default: false) # # Returns a promise that resolves to the {Editor} for the file URI. - open: (filePath, options={}) -> - changeFocus = options.changeFocus ? true - filePath = atom.project.resolve(filePath) - initialLine = options.initialLine - activePane = @activePane + open: (uri, options={}) -> + searchAllPanes = options.searchAllPanes + split = options.split + uri = atom.project.relativize(uri) ? '' - editor = activePane.itemForUri(atom.project.relativize(filePath)) if activePane and filePath - promise = atom.project.open(filePath, {initialLine}) if not editor + pane = switch split + when 'left' + @activePane.findLeftmostSibling() + when 'right' + @activePane.findOrCreateRightmostSibling() + else + if searchAllPanes + @paneContainer.paneForUri(uri) ? @activePane + else + @activePane - Q(editor ? promise) - .then (editor) => - if not activePane - activePane = new Pane(items: [editor]) - @paneContainer.root = activePane + @openUriInPane(uri, pane, options) - @itemOpened(editor) - activePane.activateItem(editor) - activePane.activate() if changeFocus - @emit "uri-opened" - editor - .catch (error) -> - console.error(error.stack ? error) - - # Private: Only used in specs + # Only used in specs openSync: (uri, options={}) -> {initialLine} = options # TODO: Remove deprecated changeFocus option activatePane = options.activatePane ? options.changeFocus ? true - uri = atom.project.relativize(uri) + uri = atom.project.relativize(uri) ? '' - if uri? - editor = @activePane.itemForUri(uri) ? atom.project.openSync(uri, {initialLine}) - else - editor = atom.project.openSync() + item = @activePane.itemForUri(uri) + if uri + item ?= opener(atom.project.resolve(uri), options) for opener in @getOpeners() when !item + item ?= atom.project.openSync(uri, {initialLine}) - @activePane.activateItem(editor) - @itemOpened(editor) + @activePane.activateItem(item) + @itemOpened(item) @activePane.activate() if activatePane - editor + item - # Public: Synchronously open an editor for the given URI or activate an existing - # editor in any pane if one already exists. - openSingletonSync: (uri, options={}) -> - {initialLine, split} = options - # TODO: Remove deprecated changeFocus option - activatePane = options.activatePane ? options.changeFocus ? true - uri = atom.project.relativize(uri) + openUriInPane: (uri, pane, options={}) -> + changeFocus = options.changeFocus ? true - if pane = @paneContainer.paneForUri(uri) - editor = pane.itemForUri(uri) - else - pane = switch split - when 'left' - @activePane.findLeftmostSibling() - when 'right' - @activePane.findOrCreateRightmostSibling() - else - @activePane - editor = atom.project.openSync(uri, {initialLine}) + item = pane.itemForUri(uri) + if uri + item ?= opener(atom.project.resolve(uri), options) for opener in @getOpeners() when !item + item ?= atom.project.open(uri, options) - pane.activateItem(editor) - pane.activate() if activatePane - editor + Q(item) + .then (item) => + if not pane + pane = new Pane(items: [item]) + @paneContainer.root = pane + @itemOpened(item) + pane.activateItem(item) + pane.activate() if changeFocus + @emit "uri-opened" + item + .catch (error) -> + console.error(error.stack ? error) # Public: Reopens the last-closed item uri if it hasn't already been reopened. reopenItemSync: -> if uri = @destroyedItemUris.pop() @openSync(uri) + # Public: Register an opener for a uri. + # + # An {Editor} will be used if no openers return a value. + # + # ## Example + # ```coffeescript + # atom.project.registerOpener (uri) -> + # if path.extname(uri) is '.toml' + # return new TomlEditor(uri) + # ``` + # + # opener - A {Function} to be called when a path is being opened. + registerOpener: (opener) -> + atom.project.registerOpener(opener) + + # Public: Remove a registered opener. + unregisterOpener: (opener) -> + atom.project.unregisterOpener(opener) + + getOpeners: -> + atom.project.openers + # Public: save the active item. saveActivePaneItem: -> @activePane?.saveActiveItem() @@ -138,6 +171,11 @@ class Workspace extends Model destroyActivePane: -> @activePane?.destroy() + # Public: Returns an {Editor} if the active pane item is an {Editor}, + # or null otherwise. + getActiveEditor: -> + @activePane?.getActiveEditor() + increaseFontSize: -> atom.config.set("editor.fontSize", atom.config.get("editor.fontSize") + 1) @@ -145,16 +183,19 @@ class Workspace extends Model fontSize = atom.config.get("editor.fontSize") atom.config.set("editor.fontSize", fontSize - 1) if fontSize > 1 - # Private: Removes the item's uri from the list of potential items to reopen. + resetFontSize: -> + atom.config.restoreDefault("editor.fontSize") + + # Removes the item's uri from the list of potential items to reopen. itemOpened: (item) -> if uri = item.getUri?() remove(@destroyedItemUris, uri) - # Private: Adds the destroyed item's uri to the list of items to reopen. + # Adds the destroyed item's uri to the list of items to reopen. onPaneItemDestroyed: (item) => if uri = item.getUri?() @destroyedItemUris.push(uri) - # Private: Called by Model superclass when destroyed + # Called by Model superclass when destroyed destroyed: -> @paneContainer.destroy() diff --git a/static/index.html b/static/index.html index ebc054082..990e08915 100644 --- a/static/index.html +++ b/static/index.html @@ -6,15 +6,28 @@