Merge branch 'master' into some-files-are-just-too-evil

This commit is contained in:
Matt Colyer 2013-10-11 13:43:12 -07:00
commit 6a37f9dad4
81 changed files with 2730 additions and 1247 deletions

View File

@ -1,3 +0,0 @@
tags
docs/api
.git

View File

@ -1,3 +1,5 @@
* Improved: Faster and better looking find and replace
* Improved: Double-click selection behavior between word/non-word
* Added: Solarized theme now bundled by default
* Added: Base16 Tomorrow Dark theme now bundled by default

View File

@ -16,8 +16,6 @@
styleguides
* Include thoughtfully worded [Jasmine](http://pivotal.github.com/jasmine/)
specs
* Style new elements in both the light and dark default themes when
appropriate
* Add 3rd-party packages as a `package.json` dependency
* Commit messages are in the present tense
* Commit messages that improve the format of the code start with :lipstick:

View File

@ -54,6 +54,7 @@ module.exports = (grunt) ->
glob_to_multiple:
expand: true
src: [
'menus/*.cson'
'keymaps/*.cson'
'static/**/*.cson'
]
@ -88,6 +89,8 @@ module.exports = (grunt) ->
level: 'error'
max_line_length:
level: 'ignore'
indentation:
level: 'ignore'
src: [
'dot-atom/**/*.coffee'
'exports/**/*.coffee'

View File

@ -2,7 +2,7 @@
![atom](https://s3.amazonaws.com/speakeasy/apps/icons/27/medium/7db16e44-ba57-11e2-8c6f-981faf658e00.png)
Check out our [guides](https://atom-docs.githubapp.com/v20.0/index.html) and [API documentation](https://atom-docs.githubapp.com/v20.0/api/index.html).
Check out our [guides](https://atom-docs.githubapp.com/v26.0/index.html) and [API documentation](https://atom-docs.githubapp.com/v26.0/api/index.html).
## Installing

View File

@ -9,7 +9,7 @@ if [ ! -d $ATOM_PATH ]; then
exit 1
fi
while getopts ":whvft-:" opt; do
while getopts ":wtfvhs-:" opt; do
case "$opt" in
-)
case "${OPTARG}" in
@ -32,6 +32,7 @@ done
if [ $EXPECT_OUTPUT ]; then
$ATOM_BINARY --executed-from="$(pwd)" --pid=$$ $@
exit $?
else
open -a $ATOM_PATH -n --args --executed-from="$(pwd)" --pid=$$ $@
fi

View File

@ -1,4 +1,9 @@
require '../src/atom'
require '../src/window'
Atom = require '../src/atom'
atom = new Atom()
atom.show() unless atom.getLoadSettings().exitWhenDone
window.atom = atom
{runSpecSuite} = require '../spec/jasmine-helper'
atom.openDevTools()

View File

@ -1,11 +1,9 @@
require '../spec/spec-helper'
$ = require 'jquery'
_ = require 'underscore'
{Point} = require 'telepath'
Project = require 'project'
fsUtils = require 'fs-utils'
TokenizedBuffer = require 'tokenized-buffer'
{$, _, Point, fs} = require 'atom'
Project = require '../src/project'
fsUtils = require '../src/fs-utils'
TokenizedBuffer = require '../src/tokenized-buffer'
defaultCount = 100
window.pbenchmark = (args...) -> window.benchmark(args..., profile: true)
@ -13,7 +11,7 @@ window.fbenchmark = (args...) -> window.benchmark(args..., focused: true)
window.fpbenchmark = (args...) -> window.benchmark(args..., profile: true, focused: true)
window.pfbenchmark = window.fpbenchmark
window.benchmarkFixturesProject = new Project(fsUtils.resolveOnLoadPath('benchmark/fixtures'))
window.benchmarkFixturesProject = new Project(fsUtils.resolveOnLoadPath('../benchmark/fixtures'))
beforeEach ->
window.project = window.benchmarkFixturesProject

View File

@ -1,8 +1,6 @@
require './benchmark-helper'
$ = require 'jquery'
_ = require 'underscore'
TokenizedBuffer = require 'tokenized-buffer'
RootView = require 'root-view'
{$, _, RootView} = require 'atom'
TokenizedBuffer = require '../src/tokenized-buffer'
describe "editor.", ->
editor = null
@ -12,7 +10,6 @@ describe "editor.", ->
window.rootView = new RootView
window.rootView.attachToDom()
rootView.width(1024)
rootView.height(768)
rootView.open() # open blank editor
@ -62,6 +59,116 @@ describe "editor.", ->
editor.insertText('"')
editor.backspace()
describe "empty-vs-set-innerHTML.", ->
[firstRow, lastRow] = []
beforeEach ->
firstRow = editor.getFirstVisibleScreenRow()
lastRow = editor.getLastVisibleScreenRow()
benchmark "build-gutter-html.", 1000, ->
editor.gutter.renderLineNumbers(null, firstRow, lastRow)
benchmark "set-innerHTML.", 1000, ->
editor.gutter.renderLineNumbers(null, firstRow, lastRow)
editor.gutter.lineNumbers[0].innerHtml = ''
benchmark "empty.", 1000, ->
editor.gutter.renderLineNumbers(null, firstRow, lastRow)
editor.gutter.lineNumbers.empty()
describe "positionLeftForLineAndColumn.", ->
line = null
beforeEach ->
editor.scrollTop(2000)
editor.resetDisplay()
line = editor.lineElementForScreenRow(106)[0]
describe "one-line.", ->
beforeEach ->
editor.clearCharacterWidthCache()
benchmark "uncached", 5000, ->
editor.positionLeftForLineAndColumn(line, 106, 82)
editor.clearCharacterWidthCache()
benchmark "cached", 5000, ->
editor.positionLeftForLineAndColumn(line, 106, 82)
describe "multiple-lines.", ->
[firstRow, lastRow] = []
beforeEach ->
firstRow = editor.getFirstVisibleScreenRow()
lastRow = editor.getLastVisibleScreenRow()
benchmark "cache-entire-visible-area", 100, ->
for i in [firstRow..lastRow]
line = editor.lineElementForScreenRow(i)[0]
editor.positionLeftForLineAndColumn(line, i, Math.max(0, editor.lineLengthForBufferRow(i)))
describe "text-rendering.", ->
beforeEach ->
editor.scrollTop(2000)
benchmark "resetDisplay", 50, ->
editor.resetDisplay()
benchmark "htmlForScreenRows", 1000, ->
lastRow = editor.getLastScreenRow()
editor.htmlForScreenRows(0, lastRow)
benchmark "htmlForScreenRows.htmlParsing", 50, ->
lastRow = editor.getLastScreenRow()
html = editor.htmlForScreenRows(0, lastRow)
div = document.createElement('div')
div.innerHTML = html
describe "gutter-api.", ->
describe "getLineNumberElementsForClass.", ->
beforeEach ->
editor.gutter.addClassToLine(20, 'omgwow')
editor.gutter.addClassToLine(40, 'omgwow')
benchmark "DOM", 20000, ->
editor.gutter.getLineNumberElementsForClass('omgwow')
benchmark "getLineNumberElement.DOM", 20000, ->
editor.gutter.getLineNumberElement(12)
benchmark "toggle-class", 2000, ->
editor.gutter.addClassToLine(40, 'omgwow')
editor.gutter.removeClassFromLine(40, 'omgwow')
describe "find-then-unset.", ->
classes = ['one', 'two', 'three', 'four']
benchmark "single-class", 200, ->
editor.gutter.addClassToLine(30, 'omgwow')
editor.gutter.addClassToLine(40, 'omgwow')
editor.gutter.removeClassFromAllLines('omgwow')
benchmark "multiple-class", 200, ->
editor.gutter.addClassToLine(30, 'one')
editor.gutter.addClassToLine(30, 'two')
editor.gutter.addClassToLine(40, 'two')
editor.gutter.addClassToLine(40, 'three')
editor.gutter.addClassToLine(40, 'four')
for klass in classes
editor.gutter.removeClassFromAllLines(klass)
describe "line-htmlification.", ->
div = null
html = null
beforeEach ->
lastRow = editor.getLastScreenRow()
html = editor.htmlForScreenRows(0, lastRow)
div = document.createElement('div')
benchmark "setInnerHTML", 1, ->
div.innerHTML = html
describe "9000-line-file.", ->
benchmark "opening.", 5, ->
rootView.open('huge.js')

View File

@ -24,6 +24,26 @@ my-package/
index.coffee
```
## Publishing
Atom bundles a command line utility called [apm](http://github.com/atom/apm)
which can be used to publish Atom packages to the public registry.
Once your package is written and ready for distribution you can run the
following to publish your package:
```sh
cd my-package
apm publish minor
```
This will update your `package.json` to have a new minor `version`, commit
the change, create a new [Git tag](http://git-scm.com/book/en/Git-Basics-Tagging),
and then upload the package to the registry.
Run `apm help publish` to see all the available options and `apm help` to see
all the other available commands.
## package.json
Similar to [npm packages][npm], Atom packages

View File

@ -0,0 +1,63 @@
## 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.

View File

@ -1 +0,0 @@
All themes in this directory will be automatically loaded

63
menus/base.cson Normal file
View File

@ -0,0 +1,63 @@
'menu': [
{
label: 'Atom'
submenu: [
{ label: 'About Atom', command: 'application:about' }
{ label: "VERSION", enabled: false }
{ label: "Install update", command: 'application:install-update', visible: false }
{ type: 'separator' }
{ label: 'Preferences...', command: 'application:show-settings' }
{ label: 'Hide Atom', command: 'application:hide' }
{ label: 'Hide Others', command: 'application:hide-other-applications' }
{ label: 'Show All', command: 'application:unhide-all-applications' }
{ type: 'separator' }
{ label: 'Run Atom Specs', command: 'application:run-all-specs' }
{ type: 'separator' }
{ label: 'Quit', command: 'application:quit' }
]
}
{
label: 'File'
submenu: [
{ label: 'New Window', command: 'application:new-window' }
{ label: 'New File', command: 'application:new-file' }
{ type: 'separator' }
{ label: 'Open...', command: 'application:open' }
{ label: 'Open In Dev Mode...', command: 'application:open-dev' }
{ type: 'separator' }
{ label: 'Close Window', command: 'window:close' }
]
}
{
label: 'Edit'
submenu: [
{ label: 'Undo', command: 'core:undo' }
{ label: 'Redo', command: 'core:redo' }
{ type: 'separator' }
{ label: 'Cut', command: 'core:cut' }
{ label: 'Copy', command: 'core:copy' }
{ label: 'Paste', command: 'core:paste' }
{ label: 'Select All', command: 'core:select-all' }
]
}
{
label: 'View'
submenu: [
{ label: 'Reload', command: 'window:reload' }
{ label: 'Toggle Full Screen', command: 'window:toggle-full-screen' }
{ label: 'Toggle Developer Tools', command: 'window:toggle-dev-tools' }
]
}
{
label: 'Window'
submenu: [
{ label: 'Minimize', command: 'application:minimize' }
{ label: 'Zoom', command: 'application:zoom' }
{ type: 'separator' }
{ label: 'Bring All to Front', command: 'application:bring-all-windows-to-front' }
]
}
]

View File

@ -1,7 +1,7 @@
{
"name": "atom",
"version": "26.0.0",
"main": "./src/main.js",
"version": "31.0.0",
"main": "./src/browser/main.js",
"repository": {
"type": "git",
"url": "https://github.com/atom/atom.git"
@ -9,14 +9,14 @@
"bugs": {
"url": "https://github.com/atom/atom/issues"
},
"atomShellVersion": "0.5.0",
"atomShellVersion": "0.6.0",
"dependencies": {
"async": "0.2.6",
"bootstrap": "git://github.com/twbs/bootstrap.git#v3.0.0",
"coffee-script": "1.6.2",
"coffeestack": "0.4.0",
"first-mate": "0.1.0",
"git-utils": "0.24.0",
"coffeestack": "0.6.0",
"first-mate": "0.2.0",
"git-utils": "0.26.0",
"guid": "0.0.10",
"jasmine-focused": "~0.14.0",
"mkdirp": "0.3.5",
@ -28,70 +28,71 @@
"pathwatcher": "0.5.0",
"pegjs": "0.7.0",
"plist": "git://github.com/nathansobo/node-plist.git",
"q": "0.9.7",
"rimraf": "2.1.4",
"scandal": "0.2.0",
"scandal": "0.5.0",
"season": "0.13.0",
"semver": "1.1.4",
"space-pen": "1.2.0",
"tantamount": "0.3.0",
"telepath": "0.6.0",
"space-pen": "1.3.0",
"tantamount": "0.5.0",
"telepath": "0.8.1",
"temp": "0.5.0",
"underscore": "1.4.4",
"atom-light-ui": "0.3.0",
"atom-light-syntax": "0.2.0",
"atom-dark-ui": "0.3.0",
"atom-dark-syntax": "0.2.0",
"base16-tomorrow-dark-theme": "0.1.0",
"solarized-dark-syntax": "0.1.0",
"archive-view": "0.7.0",
"autocomplete": "0.5.0",
"autoflow": "0.2.0",
"bookmarks": "0.3.0",
"bracket-matcher": "0.4.0",
"collaboration": "0.16.0",
"command-logger": "0.3.0",
"command-palette": "0.3.0",
"editor-stats": "0.2.0",
"exception-reporting": "0.1.0",
"find-and-replace": "0.14.1",
"fuzzy-finder": "0.5.0",
"gfm": "0.4.0",
"git-diff": "0.3.0",
"gists": "0.2.0",
"github-sign-in": "0.4.0",
"go-to-line": "0.3.0",
"grammar-selector": "0.4.0",
"image-view": "0.5.0",
"link": "0.2.0",
"markdown-preview": "0.3.0",
"metrics": "0.3.0",
"package-generator": "0.8.0",
"settings-view": "0.23.0",
"snippets": "0.5.0",
"spell-check": "0.5.0",
"status-bar": "0.7.0",
"symbols-view": "0.5.0",
"tabs": "0.4.0",
"terminal": "0.9.0",
"timecop": "0.4.0",
"to-the-hubs": "0.3.0",
"toml": "0.2.0",
"tree-view": "0.6.0",
"ui-demo": "0.7.0",
"whitespace": "0.4.0",
"wrap-guide": "0.2.0",
"atom-light-ui": "0.4.0",
"atom-light-syntax": "0.4.0",
"atom-dark-ui": "0.4.0",
"atom-dark-syntax": "0.4.0",
"base16-tomorrow-dark-theme": "0.2.0",
"solarized-dark-syntax": "0.3.0",
"archive-view": "0.8.0",
"autocomplete": "0.6.0",
"autoflow": "0.3.0",
"bookmarks": "0.5.0",
"bracket-matcher": "0.6.0",
"collaboration": "0.21.0",
"command-logger": "0.4.0",
"command-palette": "0.4.0",
"editor-stats": "0.3.0",
"exception-reporting": "0.4.0",
"find-and-replace": "0.24.2",
"fuzzy-finder": "0.10.0",
"gfm": "0.5.0",
"git-diff": "0.6.1",
"gists": "0.3.0",
"github-sign-in": "0.7.0",
"go-to-line": "0.4.0",
"grammar-selector": "0.5.0",
"image-view": "0.6.0",
"link": "0.4.0",
"markdown-preview": "0.6.0",
"metrics": "0.8.0",
"package-generator": "0.10.0",
"release-notes": "0.3.0",
"settings-view": "0.27.0",
"snippets": "0.6.0",
"spell-check": "0.6.0",
"status-bar": "0.10.1",
"symbols-view": "0.8.0",
"tabs": "0.5.0",
"terminal": "0.10.0",
"timecop": "0.5.0",
"to-the-hubs": "0.6.0",
"toml": "0.3.0",
"tree-view": "0.10.0",
"ui-demo": "0.8.0",
"whitespace": "0.5.0",
"wrap-guide": "0.3.0",
"c-tmbundle": "1.0.0",
"coffee-script-tmbundle": "7.0.0",
"coffee-script-tmbundle": "1.0.0",
"css-tmbundle": "1.0.0",
"git-tmbundle": "1.0.0",
"go-tmbundle": "1.0.0",
"html-tmbundle": "1.0.0",
"hyperlink-helper-tmbundle": "1.0.0",
"java-tmbundle": "1.0.0",
"javascript-tmbundle": "1.0.0",
"javascript-tmbundle": "2.0.0",
"json-tmbundle": "1.0.0",
"less-tmbundle": "1.0.0",
"make-tmbundle": "1.0.0",
@ -117,6 +118,7 @@
},
"devDependencies": {
"biscotto": "0.0.17",
"formidable": "~1.0.14",
"fstream": "0.1.24",
"grunt": "~0.4.1",
"grunt-cli": "~0.1.9",

View File

@ -7,6 +7,12 @@ cd "$(dirname "$0")/.."
rm -rf ~/.atom
git clean -dff
ATOM_CREDENTIALS_FILE=/var/lib/jenkins/config/atomcredentials
if [ -f $ATOM_CREDENTIALS_FILE ]; then
. $ATOM_CREDENTIALS_FILE
export ATOM_ACCESS_TOKEN=$ATOM_ACCESS_TOKEN # make it visibile to grunt.
fi
./script/bootstrap
./node_modules/.bin/apm clean
./node_modules/.bin/grunt ci --stack --no-color

View File

@ -92,7 +92,7 @@ class AtomReporter extends View
clearTimeout @timeoutId if @timeoutId?
@specPopup.show()
spec = _.find(window.timedSpecs, (spec) -> description is spec.name)
spec = _.find(window.timedSpecs, ({fullName}) -> description is fullName)
description = "#{description} #{spec.time}ms" if spec
@specPopup.text description
{left, top} = element.offset()
@ -113,7 +113,7 @@ class AtomReporter extends View
specCount = "#{@completeSpecCount - @skippedCount}/#{@totalSpecCount - @skippedCount} (#{@skippedCount} skipped)"
else
specCount = "#{@completeSpecCount}/#{@totalSpecCount}"
@specCount.text specCount
@specCount[0].textContent = specCount
updateStatusView: (spec) ->
if @failedCount > 0
@ -127,7 +127,7 @@ class AtomReporter extends View
time = "#{Math.round((spec.endedAt - @startedAt.getTime()) / 10)}"
time = "0#{time}" if time.length < 3
@time.text "#{time[0...-2]}.#{time[-2..]}s"
@time[0].textContent = "#{time[0...-2]}.#{time[-2..]}s"
addSpecs: (specs) ->
coreSpecs = 0

View File

@ -6,39 +6,26 @@ describe "the `atom` global", ->
beforeEach ->
window.rootView = new RootView
describe "base stylesheet loading", ->
beforeEach ->
rootView.append $$ -> @div class: 'editor'
rootView.attachToDom()
atom.themes.load()
atom.watchThemes()
afterEach ->
atom.themes.unload()
config.set('core.themes', [])
atom.reloadBaseStylesheets()
it "loads the correct values from the theme's ui-variables file", ->
config.set('core.themes', ['theme-with-ui-variables'])
# an override loaded in the base css
expect(rootView.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 "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.loadPackage('package-with-activation-events')
spyOn(pack, 'activateStylesheets').andCallThrough()
expect(pack.mainModule).toBeNull()
object = 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", ->
config.set("core.disabledPackages", [])
expect(-> atom.loadPackage("package-with-broken-package-json")).not.toThrow()
it "continues if the package has an invalid keymap", ->
config.set("core.disabledPackages", [])
expect(-> atom.loadPackage("package-with-broken-keymap")).not.toThrow()
describe ".unloadPackage(name)", ->
describe "when the package is active", ->
@ -172,7 +159,9 @@ describe "the `atom` global", ->
expect(keymap.bindingsForElement(element3)['ctrl-y']).toBeUndefined()
describe "menu loading", ->
beforeEach -> atom.contextMenu.definitions = []
beforeEach ->
atom.contextMenu.definitions = []
atom.menu.template = []
describe "when the metadata does not contain a 'menus' manifest", ->
it "loads all the .cson/.json files in the menus directory", ->
@ -182,6 +171,8 @@ describe "the `atom` global", ->
atom.activatePackage("package-with-menus")
expect(atom.menu.template[0].label).toBe "Second to Last"
expect(atom.menu.template[1].label).toBe "Last"
expect(atom.contextMenu.definitionsForElement(element)[0].label).toBe "Menu item 1"
expect(atom.contextMenu.definitionsForElement(element)[1].label).toBe "Menu item 2"
expect(atom.contextMenu.definitionsForElement(element)[2].label).toBe "Menu item 3"
@ -194,6 +185,8 @@ describe "the `atom` global", ->
atom.activatePackage("package-with-menus-manifest")
expect(atom.menu.template[0].label).toBe "Second to Last"
expect(atom.menu.template[1].label).toBe "Last"
expect(atom.contextMenu.definitionsForElement(element)[0].label).toBe "Menu item 2"
expect(atom.contextMenu.definitionsForElement(element)[1].label).toBe "Menu item 1"
expect(atom.contextMenu.definitionsForElement(element)[2]).toBeUndefined()
@ -205,15 +198,15 @@ describe "the `atom` global", ->
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(stylesheetElementForId(one)).not.toExist()
expect(stylesheetElementForId(two)).not.toExist()
expect(stylesheetElementForId(three)).not.toExist()
expect(atom.themes.stylesheetElementForId(one)).not.toExist()
expect(atom.themes.stylesheetElementForId(two)).not.toExist()
expect(atom.themes.stylesheetElementForId(three)).not.toExist()
atom.activatePackage("package-with-stylesheets-manifest")
expect(stylesheetElementForId(one)).toExist()
expect(stylesheetElementForId(two)).toExist()
expect(stylesheetElementForId(three)).not.toExist()
expect(atom.themes.stylesheetElementForId(one)).toExist()
expect(atom.themes.stylesheetElementForId(two)).toExist()
expect(atom.themes.stylesheetElementForId(three)).not.toExist()
expect($('#jasmine-content').css('font-size')).toBe '1px'
describe "when the metadata does not contain a 'stylesheets' manifest", ->
@ -221,14 +214,14 @@ describe "the `atom` global", ->
one = require.resolve("./fixtures/packages/package-with-stylesheets/stylesheets/1.css")
two = require.resolve("./fixtures/packages/package-with-stylesheets/stylesheets/2.less")
three = require.resolve("./fixtures/packages/package-with-stylesheets/stylesheets/3.css")
expect(stylesheetElementForId(one)).not.toExist()
expect(stylesheetElementForId(two)).not.toExist()
expect(stylesheetElementForId(three)).not.toExist()
expect(atom.themes.stylesheetElementForId(one)).not.toExist()
expect(atom.themes.stylesheetElementForId(two)).not.toExist()
expect(atom.themes.stylesheetElementForId(three)).not.toExist()
atom.activatePackage("package-with-stylesheets")
expect(stylesheetElementForId(one)).toExist()
expect(stylesheetElementForId(two)).toExist()
expect(stylesheetElementForId(three)).toExist()
expect(atom.themes.stylesheetElementForId(one)).toExist()
expect(atom.themes.stylesheetElementForId(two)).toExist()
expect(atom.themes.stylesheetElementForId(three)).toExist()
expect($('#jasmine-content').css('font-size')).toBe '3px'
describe "grammar loading", ->
@ -298,8 +291,8 @@ describe "the `atom` global", ->
atom.activatePackage('package-with-serialize-error', immediate: true)
atom.activatePackage('package-with-serialization', immediate: true)
atom.deactivatePackages()
expect(atom.packageStates['package-with-serialize-error']).toBeUndefined()
expect(atom.packageStates['package-with-serialization']).toEqual someNumber: 1
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", ->
@ -320,9 +313,9 @@ describe "the `atom` global", ->
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(stylesheetElementForId(one)).not.toExist()
expect(stylesheetElementForId(two)).not.toExist()
expect(stylesheetElementForId(three)).not.toExist()
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.activatePackage("package-with-scoped-properties")

View File

@ -181,7 +181,6 @@ describe "Config", ->
expect(fs.exists(config.configDirPath)).toBeTruthy()
expect(fs.exists(path.join(config.configDirPath, 'packages'))).toBeTruthy()
expect(fs.exists(path.join(config.configDirPath, 'snippets'))).toBeTruthy()
expect(fs.exists(path.join(config.configDirPath, 'themes'))).toBeTruthy()
expect(fs.isFileSync(path.join(config.configDirPath, 'config.cson'))).toBeTruthy()
describe ".loadUserConfig()", ->

View File

@ -0,0 +1,33 @@
DeserializerManager = require '../src/deserializer-manager'
describe ".deserialize(state)", ->
deserializer = null
class Foo
@deserialize: ({name}) -> new Foo(name)
constructor: (@name) ->
beforeEach ->
deserializer = new DeserializerManager()
deserializer.add(Foo)
it "calls deserialize on the deserializer for the given state object, or returns undefined if one can't be found", ->
spyOn(console, 'warn')
object = deserializer.deserialize({ deserializer: 'Foo', name: 'Bar' })
expect(object.name).toBe 'Bar'
expect(deserializer.deserialize({ deserializer: 'Bogus' })).toBeUndefined()
describe "when the deserializer has a version", ->
beforeEach ->
Foo.version = 2
describe "when the deserialized state has a matching version", ->
it "attempts to deserialize the state", ->
object = deserializer.deserialize({ deserializer: 'Foo', version: 2, name: 'Bar' })
expect(object.name).toBe 'Bar'
describe "when the deserialized state has a non-matching version", ->
it "returns undefined", ->
expect(deserializer.deserialize({ deserializer: 'Foo', version: 3, name: 'Bar' })).toBeUndefined()
expect(deserializer.deserialize({ deserializer: 'Foo', version: 1, name: 'Bar' })).toBeUndefined()
expect(deserializer.deserialize({ deserializer: 'Foo', name: 'Bar' })).toBeUndefined()

View File

@ -13,6 +13,8 @@ describe "Editor", ->
editor.lineOverdraw = 2
editor.isFocused = true
editor.enableKeymap()
editor.calculateHeightInLines = ->
Math.ceil(@height() / @lineHeight)
editor.attachToDom = ({ heightInLines, widthInChars } = {}) ->
heightInLines ?= @getBuffer().getLineCount()
@height(getLineHeight() * heightInLines)
@ -117,21 +119,21 @@ describe "Editor", ->
it "updates the rendered lines, cursors, selections, scroll position, and event subscriptions to match the given edit session", ->
editor.attachToDom(heightInLines: 5, widthInChars: 30)
editor.setCursorBufferPosition([3, 5])
editor.setCursorBufferPosition([6, 13])
editor.scrollToBottom()
editor.scrollLeft(150)
previousScrollHeight = editor.verticalScrollbar.prop('scrollHeight')
previousScrollTop = editor.scrollTop()
previousScrollLeft = editor.scrollLeft()
newEditSession.setScrollTop(120)
newEditSession.setScrollTop(900)
newEditSession.setSelectedBufferRange([[40, 0], [43, 1]])
editor.edit(newEditSession)
{ firstRenderedScreenRow, lastRenderedScreenRow } = editor
expect(editor.lineElementForScreenRow(firstRenderedScreenRow).text()).toBe newBuffer.lineForRow(firstRenderedScreenRow)
expect(editor.lineElementForScreenRow(lastRenderedScreenRow).text()).toBe newBuffer.lineForRow(editor.lastRenderedScreenRow)
expect(editor.scrollTop()).toBe 120
expect(editor.scrollTop()).toBe 900
expect(editor.scrollLeft()).toBe 0
expect(editor.getSelectionView().regions[0].position().top).toBe 40 * editor.lineHeight
editor.insertText("hello")
@ -144,9 +146,9 @@ describe "Editor", ->
expect(editor.verticalScrollbar.prop('scrollHeight')).toBe previousScrollHeight
expect(editor.scrollTop()).toBe previousScrollTop
expect(editor.scrollLeft()).toBe previousScrollLeft
expect(editor.getCursorView().position()).toEqual { top: 3 * editor.lineHeight, left: 5 * editor.charWidth }
expect(editor.getCursorView().position()).toEqual { top: 6 * editor.lineHeight, left: 13 * editor.charWidth }
editor.insertText("goodbye")
expect(editor.lineElementForScreenRow(3).text()).toMatch /^ vgoodbyear/
expect(editor.lineElementForScreenRow(6).text()).toMatch /^ currentgoodbye/
it "triggers alert if edit session's buffer goes into conflict with changes on disk", ->
filePath = "/tmp/atom-changed-file.txt"
@ -286,15 +288,13 @@ describe "Editor", ->
describe "font family", ->
beforeEach ->
expect(editor.css('font-family')).not.toBe 'Courier'
expect(editor.css('font-family')).toBe 'Courier'
it "when there is no config in fontFamily don't set it", ->
expect($("head style.font-family")).not.toExist()
atom.config.set('editor.fontFamily', null)
expect(editor.css('font-family')).toBe ''
describe "when the font family changes", ->
afterEach ->
editor.clearFontFamily()
it "updates the font family of editors and recalculates dimensions critical to cursor positioning", ->
editor.attachToDom(12)
lineHeightBefore = editor.lineHeight
@ -303,7 +303,6 @@ describe "Editor", ->
config.set("editor.fontFamily", "PCMyungjo")
expect(editor.css('font-family')).toBe 'PCMyungjo'
expect($("head style.editor-font-family").text()).toMatch "{font-family: PCMyungjo}"
expect(editor.charWidth).not.toBe charWidthBefore
expect(editor.getCursorView().position()).toEqual { top: 5 * editor.lineHeight, left: 6 * editor.charWidth }
@ -317,8 +316,7 @@ describe "Editor", ->
expect(editor.css('font-size')).not.toBe "10px"
it "sets the initial font size based on the value from config", ->
expect($("head style.font-size")).toExist()
expect($("head style.font-size").text()).toMatch "{font-size: #{config.get('editor.fontSize')}px}"
expect(editor.css('font-size')).toBe "#{config.get('editor.fontSize')}px"
describe "when the font size changes", ->
it "updates the font sizes of editors and recalculates dimensions critical to cursor positioning", ->
@ -404,9 +402,6 @@ describe "Editor", ->
beforeEach ->
editor.setFontFamily('sans-serif')
afterEach ->
editor.clearFontFamily()
it "positions the cursor to the clicked row and column", ->
{top, left} = editor.pixelOffsetForScreenPosition([3, 30])
editor.renderedLines.trigger mousedownEvent(pageX: left, pageY: top)
@ -440,6 +435,29 @@ describe "Editor", ->
editor.renderedLines.trigger mousedownEvent(editor: editor, point: [3, 12], originalEvent: {detail: 1}, shiftKey: true)
expect(editor.getSelectedBufferRange()).toEqual [[3, 10], [3, 12]]
describe "when clicking between a word and a non-word", ->
it "selects the word", ->
expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0)
editor.renderedLines.trigger mousedownEvent(editor: editor, point: [1, 21], originalEvent: {detail: 1})
editor.renderedLines.trigger 'mouseup'
editor.renderedLines.trigger mousedownEvent(editor: editor, point: [1, 21], originalEvent: {detail: 2})
editor.renderedLines.trigger 'mouseup'
expect(editor.getSelectedText()).toBe "function"
editor.setCursorBufferPosition([0, 0])
editor.renderedLines.trigger mousedownEvent(editor: editor, point: [1, 22], originalEvent: {detail: 1})
editor.renderedLines.trigger 'mouseup'
editor.renderedLines.trigger mousedownEvent(editor: editor, point: [1, 22], originalEvent: {detail: 2})
editor.renderedLines.trigger 'mouseup'
expect(editor.getSelectedText()).toBe "items"
editor.setCursorBufferPosition([0, 0])
editor.renderedLines.trigger mousedownEvent(editor: editor, point: [0, 28], originalEvent: {detail: 1})
editor.renderedLines.trigger 'mouseup'
editor.renderedLines.trigger mousedownEvent(editor: editor, point: [0, 28], originalEvent: {detail: 2})
editor.renderedLines.trigger 'mouseup'
expect(editor.getSelectedText()).toBe "{"
describe "triple/quardruple/etc-click", ->
it "selects the line under the cursor", ->
expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0)
@ -892,9 +910,6 @@ describe "Editor", ->
beforeEach ->
editor.setFontFamily('sans-serif')
afterEach ->
editor.clearFontFamily()
it "correctly positions the cursor", ->
editor.setCursorBufferPosition([3, 30])
expect(editor.getCursorView().position()).toEqual {top: 3 * editor.lineHeight, left: 178}
@ -1092,8 +1107,8 @@ describe "Editor", ->
expect(span0.children('span:eq(2)')).toMatchSelector '.meta.brace.curly.js'
expect(span0.children('span:eq(2)').text()).toBe "{"
line12 = editor.renderedLines.find('.line:eq(11)')
expect(line12.find('span:eq(2)')).toMatchSelector '.keyword'
line12 = editor.renderedLines.find('.line:eq(11)').children('span:eq(0)')
expect(line12.children('span:eq(1)')).toMatchSelector '.keyword'
it "wraps hard tabs in a span", ->
editor.setText('\t<- hard tab')
@ -1108,12 +1123,13 @@ describe "Editor", ->
expect(span0_0).toMatchSelector '.leading-whitespace'
expect(span0_0.text()).toBe ' '
it "wraps trailing whitespace in a span", ->
editor.setText('trailing whitespace -> ')
line0 = editor.renderedLines.find('.line:first')
span0_last = line0.children('span:eq(0)').children('span:last')
expect(span0_last).toMatchSelector '.trailing-whitespace'
expect(span0_last.text()).toBe ' '
describe "when the line has trailing whitespace", ->
it "wraps trailing whitespace in a span", ->
editor.setText('trailing whitespace -> ')
line0 = editor.renderedLines.find('.line:first')
span0_last = line0.children('span:eq(0)').children('span:last')
expect(span0_last).toMatchSelector '.trailing-whitespace'
expect(span0_last.text()).toBe ' '
describe "when lines are updated in the buffer", ->
it "syntax highlights the updated lines", ->
@ -1863,13 +1879,55 @@ describe "Editor", ->
# doesn't allow regular editors to set grammars
expect(-> editor.setGrammar()).toThrow()
describe "when config.editor.showLineNumbers is false", ->
it "doesn't render any line numbers", ->
expect(editor.gutter.lineNumbers).toBeVisible()
config.set("editor.showLineNumbers", false)
expect(editor.gutter.lineNumbers).not.toBeVisible()
describe "using gutter's api", ->
it "can get all the line number elements", ->
elements = editor.gutter.getLineNumberElements()
len = editor.gutter.lastScreenRow - editor.gutter.firstScreenRow + 1
expect(elements).toHaveLength(len)
it "can get a single line number element", ->
element = editor.gutter.getLineNumberElement(3)
expect(element).toBeTruthy()
it "returns falsy when there is no line element", ->
expect(editor.gutter.getLineNumberElement(42)).toHaveLength 0
it "can add and remove classes to all the line numbers", ->
wasAdded = editor.gutter.addClassToAllLines('heyok')
expect(wasAdded).toBe true
elements = editor.gutter.getLineNumberElementsForClass('heyok')
expect($(elements)).toHaveClass('heyok')
editor.gutter.removeClassFromAllLines('heyok')
expect($(editor.gutter.getLineNumberElements())).not.toHaveClass('heyok')
it "can add and remove classes from a single line number", ->
wasAdded = editor.gutter.addClassToLine(3, 'heyok')
expect(wasAdded).toBe true
element = editor.gutter.getLineNumberElement(2)
expect($(element)).not.toHaveClass('heyok')
it "can fetch line numbers by their class", ->
editor.gutter.addClassToLine(1, 'heyok')
editor.gutter.addClassToLine(3, 'heyok')
elements = editor.gutter.getLineNumberElementsForClass('heyok')
expect(elements.length).toBe 2
expect($(elements[0])).toHaveClass 'line-number-1'
expect($(elements[0])).toHaveClass 'heyok'
expect($(elements[1])).toHaveClass 'line-number-3'
expect($(elements[1])).toHaveClass 'heyok'
describe "gutter line highlighting", ->
beforeEach ->
editor.attachToDom(heightInLines: 5.5)
@ -2145,10 +2203,21 @@ describe "Editor", ->
expect(editor.pixelPositionForBufferPosition([2,7])).toEqual top: 0, left: 0
describe "when the editor is attached and visible", ->
it "returns the top and left pixel positions", ->
beforeEach ->
editor.attachToDom()
it "returns the top and left pixel positions", ->
expect(editor.pixelPositionForBufferPosition([2,7])).toEqual top: 40, left: 70
it "caches the left position", ->
editor.renderedLines.css('font-size', '16px')
expect(editor.pixelPositionForBufferPosition([2,8])).toEqual top: 40, left: 80
# make characters smaller
editor.renderedLines.css('font-size', '15px')
expect(editor.pixelPositionForBufferPosition([2,8])).toEqual top: 40, left: 80
describe "when clicking in the gutter", ->
beforeEach ->
editor.attachToDom()

View File

@ -0,0 +1 @@
INVALID

View File

@ -0,0 +1 @@
INVALID

View File

@ -1,3 +1,7 @@
"context-menu":
".test-1":
"Menu item 1": "command-1"
'menu': [
{ 'label': 'Last' }
]
'context-menu':
'.test-1':
'Menu item 1': 'command-1'

View File

@ -1,3 +1,7 @@
"context-menu":
".test-1":
"Menu item 2": "command-2"
'menu': [
{ 'label': 'Second to Last' }
]
'context-menu':
'.test-1':
'Menu item 2': 'command-2'

View File

@ -1,3 +1,3 @@
"context-menu":
".test-1":
"Menu item 3": "command-3"
'context-menu':
'.test-1':
'Menu item 3': 'command-3'

View File

@ -1,3 +1,7 @@
"context-menu":
".test-1":
"Menu item 1": "command-1"
'menu': [
{ 'label': 'Second to Last' }
]
'context-menu':
'.test-1':
'Menu item 1': 'command-1'

View File

@ -1,3 +1,7 @@
"context-menu":
".test-1":
"Menu item 2": "command-2"
'menu': [
{ 'label': 'Last' }
]
'context-menu':
'.test-1':
'Menu item 2': 'command-2'

View File

@ -1,3 +1,3 @@
"context-menu":
".test-1":
"Menu item 3": "command-3"
'context-menu':
'.test-1':
'Menu item 3': 'command-3'

View File

@ -347,3 +347,35 @@ describe "Keymap", ->
bindings = keymap.bindingsForElement(fragment.find('.grandchild-node'))
expect(Object.keys(bindings).length).toBe 1
expect(bindings['g']).toEqual "command-and-grandchild-node"
describe ".getAllKeyMappings", ->
it "returns the all bindings", ->
keymap.bindKeys '~/.atom/packages/dummy/keymaps/a.cson', '.command-mode', 'k': 'c'
mappings = keymap.getAllKeyMappings()
expect(mappings.length).toBe 1
expect(mappings[0].source).toEqual 'dummy'
expect(mappings[0].keystrokes).toEqual 'k'
expect(mappings[0].command).toEqual 'c'
expect(mappings[0].selector).toEqual '.command-mode'
describe ".determineSource", ->
describe "for a package", ->
it "returns '<package-name>'", ->
expect(keymap.determineSource('~/.atom/packages/dummy/keymaps/a.cson')).toEqual 'dummy'
describe "for a linked package", ->
it "returns '<package-name>'", ->
expect(keymap.determineSource('/Users/john/github/dummy/keymaps/a.cson')).toEqual 'dummy'
describe "for a user defined keymap", ->
it "returns 'User'", ->
expect(keymap.determineSource('~/.atom/keymaps/a.cson')).toEqual 'User'
describe "for a core keymap", ->
it "returns 'Core'", ->
expect(keymap.determineSource('/Applications/Atom.app/.../node_modules/dummy/keymaps/a.cson')).toEqual 'Core'
describe "for a linked core keymap", ->
it "returns 'Core'", ->
expect(keymap.determineSource('/Users/john/github/atom/keymaps/a.cson')).toEqual 'Core'

View File

@ -94,43 +94,41 @@ describe "Pane", ->
expect(editor.activeEditSession).toBe editSession1
describe "when a valid view has already been appended for another item", ->
describe "when the view has a setModel method", ->
it "recycles the existing view by assigning the selected item to it", ->
pane.showItem(editSession1)
pane.showItem(editSession2)
expect(pane.itemViews.find('.editor').length).toBe 1
editor = pane.activeView
expect(editor.css('display')).toBe ''
expect(editor.activeEditSession).toBe editSession2
it "multiple views are created for multiple items", ->
pane.showItem(editSession1)
pane.showItem(editSession2)
expect(pane.itemViews.find('.editor').length).toBe 2
editor = pane.activeView
expect(editor.css('display')).toBe ''
expect(editor.activeEditSession).toBe editSession2
describe "when the view does not have a setModel method", ->
it "creates a new view with the item", ->
initialViewCount = pane.itemViews.find('.test-view').length
it "creates a new view with the item", ->
initialViewCount = pane.itemViews.find('.test-view').length
model1 =
id: 'test-model-1'
text: 'Test Model 1'
serialize: -> {@id, @text}
getViewClass: -> TestView
model1 =
id: 'test-model-1'
text: 'Test Model 1'
serialize: -> {@id, @text}
getViewClass: -> TestView
model2 =
id: 'test-model-2'
text: 'Test Model 2'
serialize: -> {@id, @text}
getViewClass: -> TestView
model2 =
id: 'test-model-2'
text: 'Test Model 2'
serialize: -> {@id, @text}
getViewClass: -> TestView
pane.showItem(model1)
pane.showItem(model2)
expect(pane.itemViews.find('.test-view').length).toBe initialViewCount + 2
pane.showItem(model1)
pane.showItem(model2)
expect(pane.itemViews.find('.test-view').length).toBe initialViewCount + 2
pane.showPreviousItem()
expect(pane.itemViews.find('.test-view').length).toBe initialViewCount + 2
pane.showPreviousItem()
expect(pane.itemViews.find('.test-view').length).toBe initialViewCount + 2
pane.removeItem(model2)
expect(pane.itemViews.find('.test-view').length).toBe initialViewCount + 1
pane.removeItem(model2)
expect(pane.itemViews.find('.test-view').length).toBe initialViewCount + 1
pane.removeItem(model1)
expect(pane.itemViews.find('.test-view').length).toBe initialViewCount
pane.removeItem(model1)
expect(pane.itemViews.find('.test-view').length).toBe initialViewCount
describe "when showing a view item", ->
it "appends it to the itemViews div if it hasn't already been appended and shows it", ->
@ -237,6 +235,7 @@ describe "Pane", ->
describe "when the item is a model", ->
it "removes the associated view only when all items that require it have been removed", ->
pane.showItem(editSession1)
pane.showItem(editSession2)
pane.removeItem(editSession2)
expect(pane.itemViews.find('.editor')).toExist()

View File

@ -40,12 +40,12 @@ describe "Project", ->
describe "when an edit session is saved and the project has no path", ->
it "sets the project's path to the saved file's parent directory", ->
tempFile = temp.openSync().path
project.setPath(undefined)
expect(project.getPath()).toBeUndefined()
editSession = project.open()
editSession.saveAs('/tmp/atom-test-save-sets-project-path')
expect(project.getPath()).toBe '/tmp'
fs.remove('/tmp/atom-test-save-sets-project-path')
editSession.saveAs(tempFile)
expect(project.getPath()).toBe path.dirname(tempFile)
describe "when an edit session is deserialized", ->
it "emits an 'edit-session-created' event and stores the edit session", ->
@ -118,6 +118,95 @@ describe "Project", ->
expect(project.open(pathToOpen, hey: "there")).toEqual { foo: pathToOpen, options: {hey: "there"} }
expect(project.open("bar://baz")).toEqual { bar: "bar://baz" }
describe ".openAsync(path)", ->
[fooOpener, barOpener, absolutePath, newBufferHandler, newEditSessionHandler] = []
beforeEach ->
absolutePath = require.resolve('./fixtures/dir/a')
newBufferHandler = jasmine.createSpy('newBufferHandler')
project.on 'buffer-created', newBufferHandler
newEditSessionHandler = jasmine.createSpy('newEditSessionHandler')
project.on 'edit-session-created', newEditSessionHandler
fooOpener = (pathToOpen, options) -> { foo: pathToOpen, options } if pathToOpen?.match(/\.foo/)
barOpener = (pathToOpen) -> { bar: pathToOpen } if pathToOpen?.match(/^bar:\/\//)
project.registerOpener(fooOpener)
project.registerOpener(barOpener)
afterEach ->
project.unregisterOpener(fooOpener)
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 'edit-session-created' events", ->
editSession = null
waitsForPromise ->
project.openAsync(absolutePath).then (o) -> editSession = o
runs ->
expect(editSession.buffer.getPath()).toBe absolutePath
expect(newBufferHandler).toHaveBeenCalledWith editSession.buffer
expect(newEditSessionHandler).toHaveBeenCalledWith editSession
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 'edit-session-created' events", ->
editSession = null
waitsForPromise ->
project.openAsync(absolutePath).then (o) -> editSession = o
runs ->
expect(editSession.buffer.getPath()).toBe absolutePath
expect(newBufferHandler).toHaveBeenCalledWith editSession.buffer
expect(newEditSessionHandler).toHaveBeenCalledWith editSession
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 'edit-session-created' event", ->
editSession = null
waitsForPromise ->
project.openAsync(absolutePath).then (o) -> editSession = o
runs ->
newBufferHandler.reset()
expect(project.open(absolutePath).buffer).toBe editSession.buffer
expect(project.open('a').buffer).toBe editSession.buffer
expect(newBufferHandler).not.toHaveBeenCalled()
expect(newEditSessionHandler).toHaveBeenCalledWith editSession
describe "when not passed a path", ->
it "returns a new edit session and emits 'buffer-created' and 'edit-session-created' events", ->
editSession = null
waitsForPromise ->
project.openAsync().then (o) -> editSession = o
runs ->
expect(editSession.buffer.getPath()).toBeUndefined()
expect(newBufferHandler).toHaveBeenCalledWith(editSession.buffer)
expect(newEditSessionHandler).toHaveBeenCalledWith editSession
describe "when passed a path that matches a custom opener", ->
it "returns the resource returned by the custom opener", ->
waitsForPromise ->
pathToOpen = project.resolve('a.foo')
project.openAsync(pathToOpen, hey: "there").then (item) ->
expect(item).toEqual { foo: pathToOpen, options: {hey: "there"} }
waitsForPromise ->
project.openAsync("bar://baz").then (item) ->
expect(item).toEqual { bar: "bar://baz" }
it "returns number of read bytes as progress indicator", ->
filePath = project.resolve 'a'
totalBytes = 0
promise = project.openAsync(filePath)
promise.progress (bytesRead) -> totalBytes = bytesRead
waitsForPromise ->
promise
runs ->
expect(totalBytes).toBe fs.statSync(filePath).size
describe ".bufferForPath(path)", ->
describe "when opening a previously opened path", ->
it "does not create a new buffer", ->
@ -132,6 +221,34 @@ describe "Project", ->
buffer = project.bufferForPath("a").retain().release()
expect(project.bufferForPath("a").retain().release()).not.toBe buffer
describe ".bufferForPathAsync(path)", ->
[buffer] = []
beforeEach ->
waitsForPromise ->
project.bufferForPathAsync("a").then (o) ->
buffer = o
buffer.retain()
afterEach ->
buffer.release()
describe "when opening a previously opened path", ->
it "does not create a new buffer", ->
waitsForPromise ->
project.bufferForPathAsync("a").then (anotherBuffer) ->
expect(anotherBuffer).toBe buffer
waitsForPromise ->
project.bufferForPathAsync("b").then (anotherBuffer) ->
expect(anotherBuffer).not.toBe buffer
it "creates a new buffer if the previous buffer was destroyed", ->
buffer.release()
waitsForPromise ->
project.bufferForPathAsync("b").then (anotherBuffer) ->
expect(anotherBuffer).not.toBe buffer
describe ".resolve(uri)", ->
describe "when passed an absolute or relative path", ->
it "returns an absolute path based on the project's root", ->
@ -388,7 +505,7 @@ describe "Project", ->
expect(matches.length).toBe 1
it "includes files and folders that begin with a '.'", ->
projectPath = '/tmp/atom-tests/folder-with-dot-file'
projectPath = temp.mkdirSync()
filePath = path.join(projectPath, '.text')
fs.writeSync(filePath, 'match this')
project.setPath(projectPath)

View File

@ -1,4 +1,5 @@
{$, $$, fs, RootView, View} = require 'atom'
Q = require 'q'
path = require 'path'
Pane = require '../src/pane'
@ -32,7 +33,7 @@ describe "RootView", ->
editor1 = rootView.getActiveView()
buffer = editor1.getBuffer()
editor1.splitRight()
expect(rootView.getActiveView()).toBe rootView.getEditors()[1]
expect(rootView.getActiveView()).toBe rootView.getEditors()[2]
refreshRootViewAndProject()
@ -133,7 +134,7 @@ describe "RootView", ->
window.keymap.bindKeys('*', 'x': 'foo-command')
describe "when a keydown event is triggered on the RootView", ->
describe "when a keydown event is triggered in the RootView", ->
it "triggers matching keybindings for that event", ->
event = keydownEvent 'x', target: rootView[0]
@ -168,10 +169,10 @@ describe "RootView", ->
expect(rootView.title).toBe "#{item.getTitle()} - #{project.getPath()}"
describe "when the last pane item is removed", ->
it "update the title to contain the project's path", ->
it "updates the title to contain the project's path", ->
rootView.getActivePane().remove()
expect(rootView.getActivePaneItem()).toBeUndefined()
expect(rootView.title).toBe "atom - #{project.getPath()}"
expect(rootView.title).toBe project.getPath()
describe "when an inactive pane's item changes", ->
it "does not update the title", ->
@ -181,6 +182,13 @@ describe "RootView", ->
pane.showNextItem()
expect(rootView.title).toBe initialTitle
describe "when the root view is deserialized", ->
it "updates the title to contain the project's path", ->
rootView2 = atom.deserializers.deserialize(rootView.serialize())
item = rootView.getActivePaneItem()
expect(rootView2.title).toBe "#{item.getTitle()} - #{project.getPath()}"
rootView2.remove()
describe "font size adjustment", ->
it "increases/decreases font size when increase/decrease-font-size events are triggered", ->
fontSizeBefore = config.get('editor.fontSize')
@ -198,7 +206,7 @@ describe "RootView", ->
rootView.trigger 'window:decrease-font-size'
expect(config.get('editor.fontSize')).toBe 1
describe ".open(path, options)", ->
describe ".open(filePath, options)", ->
describe "when there is no active pane", ->
beforeEach ->
spyOn(Pane.prototype, 'focus')
@ -238,7 +246,7 @@ describe "RootView", ->
initialItemCount = activePane.getItems().length
describe "when called with no path", ->
it "opens an edit session with an empty buffer as an item on the active pane and focuses it", ->
it "opens an edit session with an empty buffer as an item in the active pane and focuses it", ->
editSession = rootView.open()
expect(activePane.getItems().length).toBe initialItemCount + 1
expect(activePane.activeItem).toBe editSession
@ -247,7 +255,7 @@ describe "RootView", ->
describe "when called with a path", ->
describe "when the active pane already has an edit session item for the path being opened", ->
it "shows the existing edit session on the pane", ->
it "shows the existing edit session in the pane", ->
previousEditSession = activePane.activeItem
editSession = rootView.open('b')
@ -272,6 +280,106 @@ describe "RootView", ->
editSession = rootView.open('b', changeFocus: false)
expect(activePane.focus).not.toHaveBeenCalled()
describe ".openAsync(filePath)", ->
beforeEach ->
spyOn(Pane.prototype, 'focus')
describe "when there is no active pane", ->
beforeEach ->
rootView.getActivePane().remove()
expect(rootView.getActivePane()).toBeUndefined()
describe "when called with no path", ->
it "creates a empty edit session as an item on a new pane, and focuses the pane", ->
editSession = null
waitsForPromise ->
rootView.openAsync().then (o) -> editSession = o
runs ->
expect(rootView.getActivePane().activeItem).toBe editSession
expect(editSession.getPath()).toBeUndefined()
expect(rootView.getActivePane().focus).toHaveBeenCalled()
it "can create multiple empty edit sessions as items on a pane", ->
editSession1 = null
editSession2 = null
waitsForPromise ->
rootView.openAsync()
.then (o) ->
editSession1 = o
rootView.openAsync()
.then (o) ->
editSession2 = o
runs ->
expect(rootView.getActivePane().getItems().length).toBe 2
expect(editSession1).not.toBe editSession2
describe "when called with a path", ->
it "creates an edit session for the given path as an item on a new pane, and focuses the pane", ->
editSession = null
waitsForPromise ->
rootView.openAsync('b').then (o) -> editSession = o
runs ->
expect(rootView.getActivePane().activeItem).toBe editSession
expect(editSession.getPath()).toBe require.resolve('./fixtures/dir/b')
expect(rootView.getActivePane().focus).toHaveBeenCalled()
describe "when there is an active pane", ->
[activePane] = []
beforeEach ->
activePane = rootView.getActivePane()
describe "when called with no path", ->
it "opens an edit session with an empty buffer as an item in the active pane and focuses it", ->
editSession = null
waitsForPromise ->
rootView.openAsync().then (o) -> editSession = o
runs ->
expect(activePane.getItems().length).toBe 2
expect(activePane.activeItem).toBe editSession
expect(editSession.getPath()).toBeUndefined()
expect(activePane.focus).toHaveBeenCalled()
describe "when called with a path", ->
describe "when the active pane already has an item for the given path", ->
it "shows the existing edit session in the pane", ->
previousEditSession = activePane.activeItem
editSession = null
waitsForPromise ->
rootView.openAsync('b').then (o) -> editSession = o
runs ->
expect(activePane.activeItem).toBe editSession
expect(editSession).not.toBe previousEditSession
waitsForPromise ->
rootView.openAsync(previousEditSession.getPath()).then (o) -> editSession = o
runs ->
expect(editSession).toBe previousEditSession
expect(activePane.activeItem).toBe editSession
expect(activePane.focus).toHaveBeenCalled()
describe "when the active pane does not have an existing item for the given path", ->
it "creates a new edit session for the given path in the active pane", ->
editSession = null
waitsForPromise ->
rootView.openAsync('b').then (o) -> editSession = o
runs ->
expect(activePane.activeItem).toBe editSession
expect(activePane.getItems().length).toBe 2
expect(activePane.focus).toHaveBeenCalled()
describe "window:toggle-invisibles event", ->
it "shows/hides invisibles in all open and future editors", ->
rootView.height(200)

View File

@ -1,11 +1,17 @@
try
require '../src/atom'
require '../src/window'
atom.show()
Atom = require '../src/atom'
window.atom = new Atom()
window.atom.show() unless atom.getLoadSettings().exitWhenDone
{runSpecSuite} = require './jasmine-helper'
document.title = "Spec Suite"
runSpecSuite './spec-suite'
catch e
console.error(e.stack ? e)
catch error
unless atom.getLoadSettings().exitWhenDone
atom.getCurrentWindow().setSize(800, 600)
atom.getCurrentWindow().center()
atom.openDevTools()
console.error(error.stack ? error)
atom.exit(1) if atom.getLoadSettings().exitWhenDone

View File

@ -14,48 +14,60 @@ TokenizedBuffer = require '../src/tokenized-buffer'
pathwatcher = require 'pathwatcher'
clipboard = require 'clipboard'
atom.loadBaseStylesheets()
requireStylesheet '../static/jasmine'
atom.themes.loadBaseStylesheets()
atom.themes.requireStylesheet '../static/jasmine'
fixturePackagesPath = path.resolve(__dirname, './fixtures/packages')
config.packageDirPaths.unshift(fixturePackagesPath)
keymap.loadBundledKeymaps()
atom.packages.packageDirPaths.unshift(fixturePackagesPath)
atom.keymap.loadBundledKeymaps()
[bindingSetsToRestore, bindingSetsByFirstKeystrokeToRestore] = []
$(window).on 'core:close', -> window.close()
$(window).on 'unload', ->
atom.windowMode = 'spec'
atom.getWindowState().set('dimensions', atom.getDimensions())
atom.saveWindowState()
$('html,body').css('overflow', 'auto')
jasmine.getEnv().addEqualityTester(_.isEqual) # Use underscore's definition of equality for toEqual assertions
jasmine.getEnv().defaultTimeoutInterval = 5000
specDirectory = atom.getLoadSettings().specDirectory ? __dirname
specProjectPath = path.join(specDirectory, 'fixtures')
beforeEach ->
$.fx.off = true
specDirectory = atom.getLoadSettings().specDirectory ? __dirname
window.project = new Project(path.join(specDirectory, 'fixtures'))
atom.project = new Project(specProjectPath)
window.project = atom.project
window.resetTimeouts()
atom.packageStates = {}
atom.packages.packageStates = {}
spyOn(atom, 'saveWindowState')
syntax.clearGrammarOverrides()
syntax.clearProperties()
atom.syntax.clearGrammarOverrides()
atom.syntax.clearProperties()
# used to reset keymap after each spec
bindingSetsToRestore = _.clone(keymap.bindingSets)
bindingSetsByFirstKeystrokeToRestore = _.clone(keymap.bindingSetsByFirstKeystroke)
# prevent specs from modifying Atom's menus
spyOn(atom.menu, 'sendToBrowserProcess')
# reset config before each spec; don't load or save from/to `config.json`
window.config = new Config()
config = new Config
resourcePath: window.resourcePath
configDirPath: atom.getConfigDirPath()
config.packageDirPaths.unshift(fixturePackagesPath)
spyOn(config, 'load')
spyOn(config, 'save')
config.set "editor.fontFamily", "Courier"
config.set "editor.fontSize", 16
config.set "editor.autoIndent", false
config.set "core.disabledPackages", ["package-that-throws-an-exception"]
config.set "core.disabledPackages", ["package-that-throws-an-exception",
"package-with-broken-package-json", "package-with-broken-keymap"]
config.save.reset()
atom.config = config
window.config = config
# make editor display updates synchronous
spyOn(Editor.prototype, 'requestDisplayUpdate').andCallFake -> @updateDisplay()
@ -78,12 +90,17 @@ afterEach ->
keymap.bindingSets = bindingSetsToRestore
keymap.bindingSetsByFirstKeystroke = bindingSetsByFirstKeystrokeToRestore
atom.deactivatePackages()
if rootView?
rootView.remove?()
window.rootView = null
if project?
project.destroy()
window.project = null
window.rootView?.remove?()
atom.rootView?.remove?() if atom.rootView isnt window.rootView
window.rootView = null
atom.rootView = null
window.project?.destroy?()
atom.project?.destroy?() if atom.project isnt window.project
window.project = null
atom.project = null
$('#jasmine-content').empty() unless window.debugContent
delete atom.windowState
jasmine.unspy(atom, 'saveWindowState')

View File

@ -1,5 +1,6 @@
{_, fs} = require 'atom'
path = require 'path'
temp = require 'temp'
{Site} = require 'telepath'
describe 'TextBuffer', ->
@ -80,15 +81,14 @@ describe 'TextBuffer', ->
filePath = null
beforeEach ->
filePath = "/tmp/tmp.txt"
fs.writeSync(filePath, "first")
buffer.release()
filePath = temp.openSync('atom').path
fs.writeSync(filePath, "first")
buffer = project.bufferForPath(filePath).retain()
afterEach ->
buffer.release()
buffer = null
fs.remove(filePath) if fs.exists(filePath)
it "does not trigger a change event when Atom modifies the file", ->
buffer.insert([0,0], "HELLO!")
@ -176,7 +176,7 @@ describe 'TextBuffer', ->
it "resumes watching of the file when it is re-saved", ->
bufferToDelete.save()
expect(bufferToDelete.fileExists()).toBeTruthy()
expect(fs.exists(bufferToDelete.getPath())).toBeTruthy()
expect(bufferToDelete.isInConflict()).toBeFalsy()
fs.writeSync(filePath, 'moo')
@ -946,7 +946,9 @@ describe 'TextBuffer', ->
describe "when the serialized buffer was unsaved and had no path", ->
it "restores the previous unsaved state of the buffer", ->
buffer.setPath(undefined)
buffer.release()
buffer = project.bufferForPath()
buffer.setText("abc")
state = buffer.serialize()

View File

@ -1,4 +1,5 @@
{$} = require 'atom'
path = require 'path'
{$, $$, fs, RootView} = require 'atom'
ThemeManager = require '../src/theme-manager'
AtomPackage = require '../src/atom-package'
@ -23,6 +24,10 @@ describe "ThemeManager", ->
expect(paths[0]).toContain 'atom-dark-ui'
expect(paths[1]).toContain 'atom-light-ui'
it "ignores themes that cannot be resolved to a directory", ->
config.set('core.themes', ['definitely-not-a-theme'])
expect(-> themeManager.getImportPaths()).not.toThrow()
describe "when the core.themes config value changes", ->
it "add/removes stylesheets to reflect the new config value", ->
themeManager.on 'reloaded', reloadHandler = jasmine.createSpy()
@ -69,3 +74,81 @@ describe "ThemeManager", ->
expect(loadHandler).toHaveBeenCalled()
expect(loadHandler.mostRecentCall.args[0]).toBeInstanceOf AtomPackage
describe "requireStylesheet(path)", ->
it "synchronously loads css at the given path and installs a style tag for it in the head", ->
cssPath = project.resolve('css.css')
lengthBefore = $('head style').length
themeManager.requireStylesheet(cssPath)
expect($('head style').length).toBe lengthBefore + 1
element = $('head style[id*="css.css"]')
expect(element.attr('id')).toBe cssPath
expect(element.text()).toBe fs.read(cssPath)
# doesn't append twice
themeManager.requireStylesheet(cssPath)
expect($('head style').length).toBe lengthBefore + 1
$('head style[id*="css.css"]').remove()
it "synchronously loads and parses less files at the given path and installs a style tag for it in the head", ->
lessPath = project.resolve('sample.less')
lengthBefore = $('head style').length
themeManager.requireStylesheet(lessPath)
expect($('head style').length).toBe lengthBefore + 1
element = $('head style[id*="sample.less"]')
expect(element.attr('id')).toBe lessPath
expect(element.text()).toBe """
#header {
color: #4d926f;
}
h2 {
color: #4d926f;
}
"""
# doesn't append twice
themeManager.requireStylesheet(lessPath)
expect($('head style').length).toBe lengthBefore + 1
$('head style[id*="sample.less"]').remove()
it "supports requiring css and less stylesheets without an explicit extension", ->
themeManager.requireStylesheet path.join(__dirname, 'fixtures', 'css')
expect($('head style[id*="css.css"]').attr('id')).toBe project.resolve('css.css')
themeManager.requireStylesheet path.join(__dirname, 'fixtures', 'sample')
expect($('head style[id*="sample.less"]').attr('id')).toBe project.resolve('sample.less')
$('head style[id*="css.css"]').remove()
$('head style[id*="sample.less"]').remove()
describe ".removeStylesheet(path)", ->
it "removes styling applied by given stylesheet path", ->
cssPath = require.resolve('./fixtures/css.css')
expect($(document.body).css('font-weight')).not.toBe("bold")
themeManager.requireStylesheet(cssPath)
expect($(document.body).css('font-weight')).toBe("bold")
themeManager.removeStylesheet(cssPath)
expect($(document.body).css('font-weight')).not.toBe("bold")
describe "base stylesheet loading", ->
beforeEach ->
window.rootView = new RootView
rootView.append $$ -> @div class: 'editor'
rootView.attachToDom()
themeManager.load()
it "loads the correct values from the theme's ui-variables file", ->
config.set('core.themes', ['theme-with-ui-variables'])
# an override loaded in the base css
expect(rootView.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"

View File

@ -53,6 +53,7 @@ class TimeReporter extends jasmine.Reporter
window.timedSpecs.push
description: @description
time: duration
fullName: spec.getFullName()
if timedSuites[@suite]
window.timedSuites[@suite] += duration

View File

@ -26,6 +26,18 @@ describe "TokenizedBuffer", ->
expect(tokenizedBuffer2.buffer).toBe tokenizedBuffer1.buffer
expect(tokenizedBuffer2.getTabLength()).toBe tokenizedBuffer1.getTabLength()
describe "when the buffer is destroyed", ->
beforeEach ->
buffer = project.bufferForPath('sample.js')
tokenizedBuffer = new TokenizedBuffer({buffer})
startTokenizing(tokenizedBuffer)
it "stops tokenization", ->
tokenizedBuffer.destroy()
spyOn(tokenizedBuffer, 'tokenizeNextChunk')
advanceClock()
expect(tokenizedBuffer.tokenizeNextChunk).not.toHaveBeenCalled()
describe "when the buffer contains soft-tabs", ->
beforeEach ->
buffer = project.bufferForPath('sample.js')

View File

@ -7,7 +7,8 @@ describe "Window", ->
beforeEach ->
spyOn(atom, 'hide')
atom.getLoadSettings().initialPath = project.getPath()
atom.getLoadSettings() # Causes atom.loadSettings to be initialized
atom.loadSettings.initialPath = project.getPath()
project.destroy()
windowEventHandler = new WindowEventHandler()
window.deserializeEditorWindow()
@ -75,66 +76,6 @@ describe "Window", ->
expect(window.onbeforeunload(new Event('beforeunload'))).toBeFalsy()
expect(atom.confirmSync).toHaveBeenCalled()
describe "requireStylesheet(path)", ->
it "synchronously loads css at the given path and installs a style tag for it in the head", ->
cssPath = project.resolve('css.css')
lengthBefore = $('head style').length
requireStylesheet(cssPath)
expect($('head style').length).toBe lengthBefore + 1
element = $('head style[id*="css.css"]')
expect(element.attr('id')).toBe cssPath
expect(element.text()).toBe fs.read(cssPath)
# doesn't append twice
requireStylesheet(cssPath)
expect($('head style').length).toBe lengthBefore + 1
$('head style[id*="css.css"]').remove()
it "synchronously loads and parses less files at the given path and installs a style tag for it in the head", ->
lessPath = project.resolve('sample.less')
lengthBefore = $('head style').length
requireStylesheet(lessPath)
expect($('head style').length).toBe lengthBefore + 1
element = $('head style[id*="sample.less"]')
expect(element.attr('id')).toBe lessPath
expect(element.text()).toBe """
#header {
color: #4d926f;
}
h2 {
color: #4d926f;
}
"""
# doesn't append twice
requireStylesheet(lessPath)
expect($('head style').length).toBe lengthBefore + 1
$('head style[id*="sample.less"]').remove()
it "supports requiring css and less stylesheets without an explicit extension", ->
requireStylesheet path.join(__dirname, 'fixtures', 'css')
expect($('head style[id*="css.css"]').attr('id')).toBe project.resolve('css.css')
requireStylesheet path.join(__dirname, 'fixtures', 'sample')
expect($('head style[id*="sample.less"]').attr('id')).toBe project.resolve('sample.less')
$('head style[id*="css.css"]').remove()
$('head style[id*="sample.less"]').remove()
describe ".removeStylesheet(path)", ->
it "removes styling applied by given stylesheet path", ->
cssPath = require.resolve('./fixtures/css.css')
expect($(document.body).css('font-weight')).not.toBe("bold")
requireStylesheet(cssPath)
expect($(document.body).css('font-weight')).toBe("bold")
removeStylesheet(cssPath)
expect($(document.body).css('font-weight')).not.toBe("bold")
describe ".unloadEditorWindow()", ->
it "saves the serialized state of the window so it can be deserialized after reload", ->
rootViewState = rootView.serialize()
@ -156,38 +97,6 @@ describe "Window", ->
expect(buffer.subscriptionCount()).toBe 0
describe ".deserialize(state)", ->
class Foo
@deserialize: ({name}) -> new Foo(name)
constructor: (@name) ->
beforeEach ->
registerDeserializer(Foo)
afterEach ->
unregisterDeserializer(Foo)
it "calls deserialize on the deserializer for the given state object, or returns undefined if one can't be found", ->
spyOn(console, 'warn')
object = deserialize({ deserializer: 'Foo', name: 'Bar' })
expect(object.name).toBe 'Bar'
expect(deserialize({ deserializer: 'Bogus' })).toBeUndefined()
describe "when the deserializer has a version", ->
beforeEach ->
Foo.version = 2
describe "when the deserialized state has a matching version", ->
it "attempts to deserialize the state", ->
object = deserialize({ deserializer: 'Foo', version: 2, name: 'Bar' })
expect(object.name).toBe 'Bar'
describe "when the deserialized state has a non-matching version", ->
it "returns undefined", ->
expect(deserialize({ deserializer: 'Foo', version: 3, name: 'Bar' })).toBeUndefined()
expect(deserialize({ deserializer: 'Foo', version: 1, name: 'Bar' })).toBeUndefined()
expect(deserialize({ deserializer: 'Foo', name: 'Bar' })).toBeUndefined()
describe "drag and drop", ->
buildDragEvent = (type, files) ->
dataTransfer =
@ -206,7 +115,7 @@ describe "Window", ->
it "opens it", ->
spyOn(atom, "open")
event = buildDragEvent("drop", [ {path: "/fake1"}, {path: "/fake2"} ])
window.onDrop(event)
$(document).trigger(event)
expect(atom.open.callCount).toBe 1
expect(atom.open.argsForCall[0][0]).toEqual pathsToOpen: ['/fake1', '/fake2']
@ -214,7 +123,7 @@ describe "Window", ->
it "does nothing", ->
spyOn(atom, "open")
event = buildDragEvent("drop", [])
window.onDrop(event)
$(document).trigger(event)
expect(atom.open).not.toHaveBeenCalled()
describe "when a link is clicked", ->

View File

@ -28,26 +28,28 @@ class AtomPackage extends Package
getType: -> 'atom'
load: ->
@metadata = {}
@stylesheets = []
@keymaps = []
@menus = []
@grammars = []
@scopedProperties = []
@measure 'loadTime', =>
try
@metadata = Package.loadMetadata(@path)
if @isTheme()
@stylesheets = []
@keymaps = []
@menus = []
@grammars = []
@scopedProperties = []
else
@loadKeymaps()
@loadMenus()
@loadStylesheets()
@loadGrammars()
@loadScopedProperties()
return if @isTheme()
if @metadata.activationEvents?
@registerDeferredDeserializers()
else
@requireMainModule()
@loadKeymaps()
@loadMenus()
@loadStylesheets()
@loadGrammars()
@loadScopedProperties()
if @metadata.activationEvents?
@registerDeferredDeserializers()
else
@requireMainModule()
catch e
console.warn "Failed to load package named '#{@name}'", e.stack ? e
@ -82,12 +84,17 @@ class AtomPackage extends Package
@configActivated = true
activateStylesheets: ->
return if @stylesheetsActivated
type = if @metadata.theme then 'theme' else 'bundled'
applyStylesheet(stylesheetPath, content, type) for [stylesheetPath, content] in @stylesheets
for [stylesheetPath, content] in @stylesheets
atom.themes.applyStylesheet(stylesheetPath, content, type)
@stylesheetsActivated = true
activateResources: ->
keymap.add(keymapPath, map) for [keymapPath, map] in @keymaps
atom.keymap.add(keymapPath, map) for [keymapPath, map] in @keymaps
atom.contextMenu.add(menuPath, map['context-menu']) for [menuPath, map] in @menus
atom.menu.add(map.menu) for [menuPath, map] in @menus when map.menu
syntax.addGrammar(grammar) for grammar in @grammars
for [scopedPropertiesPath, selector, properties] in @scopedProperties
syntax.addProperties(scopedPropertiesPath, selector, properties)
@ -113,7 +120,8 @@ class AtomPackage extends Package
fsUtils.listSync(menusDirPath, ['cson', 'json'])
loadStylesheets: ->
@stylesheets = @getStylesheetPaths().map (stylesheetPath) -> [stylesheetPath, loadStylesheet(stylesheetPath)]
@stylesheets = @getStylesheetPaths().map (stylesheetPath) ->
[stylesheetPath, atom.themes.loadStylesheet(stylesheetPath)]
getStylesheetsPath: ->
path.join(@path, @constructor.stylesheetsDir)
@ -164,18 +172,19 @@ class AtomPackage extends Package
deactivateResources: ->
syntax.removeGrammar(grammar) for grammar in @grammars
syntax.removeProperties(scopedPropertiesPath) for [scopedPropertiesPath] in @scopedProperties
keymap.remove(keymapPath) for [keymapPath] in @keymaps
removeStylesheet(stylesheetPath) for [stylesheetPath] in @stylesheets
atom.keymap.remove(keymapPath) for [keymapPath] in @keymaps
atom.themes.removeStylesheet(stylesheetPath) for [stylesheetPath] in @stylesheets
@stylesheetsActivated = false
reloadStylesheets: ->
oldSheets = _.clone(@stylesheets)
@loadStylesheets()
removeStylesheet(stylesheetPath) for [stylesheetPath] in oldSheets
atom.themes.removeStylesheet(stylesheetPath) for [stylesheetPath] in oldSheets
@reloadStylesheet(stylesheetPath, content) for [stylesheetPath, content] in @stylesheets
reloadStylesheet: (stylesheetPath, content) ->
type = if @metadata.theme then 'theme' else 'bundled'
window.applyStylesheet(stylesheetPath, content, type)
atom.themes.applyStylesheet(stylesheetPath, content, type)
requireMainModule: ->
return @mainModule if @mainModule?
@ -194,7 +203,9 @@ class AtomPackage extends Package
registerDeferredDeserializers: ->
for deserializerName in @metadata.deferredDeserializers ? []
registerDeferredDeserializer deserializerName, => @requireMainModule()
registerDeferredDeserializer deserializerName, =>
@activateStylesheets()
@requireMainModule()
subscribeToActivationEvents: ->
return unless @metadata.activationEvents?

View File

@ -4,157 +4,159 @@ _ = require './underscore-extensions'
Package = require './package'
ipc = require 'ipc'
remote = require 'remote'
shell = require 'shell'
crypto = require 'crypto'
path = require 'path'
dialog = remote.require 'dialog'
app = remote.require 'app'
{Document} = require 'telepath'
ThemeManager = require './theme-manager'
ContextMenuManager = require './context-menu-manager'
DeserializerManager = require './deserializer-manager'
Subscriber = require './subscriber'
window.atom =
loadedPackages: {}
activePackages: {}
packageStates: {}
themes: new ThemeManager()
contextMenu: new ContextMenuManager(remote.getCurrentWindow().loadSettings.devMode)
# Public: Atom global for dealing with packages, themes, menus, and the window.
#
# An instance of this class is always available as the `atom` global.
module.exports =
class Atom
_.extend @prototype, Subscriber
constructor: ->
@rootViewParentSelector = 'body'
@deserializers = new DeserializerManager()
initialize: ->
@unsubscribe()
{devMode, resourcePath} = atom.getLoadSettings()
configDirPath = @getConfigDirPath()
Config = require './config'
Keymap = require './keymap'
PackageManager = require './package-manager'
Pasteboard = require './pasteboard'
Syntax = require './syntax'
ThemeManager = require './theme-manager'
ContextMenuManager = require './context-menu-manager'
MenuManager = require './menu-manager'
@config = new Config({configDirPath, resourcePath})
@keymap = new Keymap()
@packages = new PackageManager({devMode, configDirPath, resourcePath})
#TODO Remove once packages have been updated to not touch atom.packageStates directly
@__defineGetter__ 'packageStates', => @packages.packageStates
@__defineSetter__ 'packageStates', (packageStates) => @packages.packageStates = packageStates
@subscribe @packages, 'loaded', => @watchThemes()
@themes = new ThemeManager()
@contextMenu = new ContextMenuManager(devMode)
@menu = new MenuManager()
@pasteboard = new Pasteboard()
@syntax = deserialize(@getWindowState('syntax')) ? new Syntax()
getCurrentWindow: ->
remote.getCurrentWindow()
# Public: Get the dimensions of this window.
#
# Returns an object with x, y, width, and height keys.
getDimensions: ->
browserWindow = @getCurrentWindow()
[x, y] = browserWindow.getPosition()
[width, height] = browserWindow.getSize()
{x, y, width, height}
# Public: Set the dimensions of the window.
#
# The window will be centered if either the x or y coordinate is not set
# in the dimensions parameter.
#
# * dimensions:
# + x:
# The new x coordinate.
# + y:
# The new y coordinate.
# + width:
# The new width.
# + height:
# The new height.
setDimensions: ({x, y, width, height}) ->
browserWindow = @getCurrentWindow()
browserWindow.setSize(width, height)
if x? and y?
browserWindow.setPosition(x, y)
else
browserWindow.center()
restoreDimensions: ->
dimensions = @getWindowState().getObject('dimensions')
unless dimensions?.width and dimensions?.height
{height, width} = @getLoadSettings().initialSize ? {}
height ?= screen.availHeight
width ?= Math.min(screen.availWidth, 1024)
dimensions = {width, height}
@setDimensions(dimensions)
# Public: Get the load settings for the current window.
#
# Returns an object containing all the load setting key/value pairs.
getLoadSettings: ->
remote.getCurrentWindow().loadSettings
@loadSettings ?= _.deepClone(@getCurrentWindow().loadSettings)
_.deepClone(@loadSettings)
getPackageState: (name) ->
@packageStates[name]
deserializeProject: ->
Project = require './project'
state = @getWindowState()
@project = deserialize(state.get('project'))
unless @project?
@project = new Project(@getLoadSettings().initialPath)
state.set('project', @project.getState())
setPackageState: (name, state) ->
@packageStates[name] = state
deserializeRootView: ->
RootView = require './root-view'
state = @getWindowState()
@rootView = deserialize(state.get('rootView'))
unless @rootView?
@rootView = new RootView()
state.set('rootView', @rootView.getState())
$(@rootViewParentSelector).append(@rootView)
activatePackages: ->
@activatePackage(pack.name) for pack in @getLoadedPackages()
deserializePackageStates: ->
state = @getWindowState()
@packages.packageStates = state.getObject('packageStates') ? {}
state.remove('packageStates')
activatePackage: (name, options) ->
if pack = @loadPackage(name, options)
@activePackages[pack.name] = pack
pack.activate(options)
pack
deactivatePackages: ->
@deactivatePackage(pack.name) for pack in @getActivePackages()
deactivatePackage: (name) ->
if pack = @getActivePackage(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}'")
getActivePackage: (name) ->
@activePackages[name]
isPackageActive: (name) ->
@getActivePackage(name)?
getActivePackages: ->
_.values(@activePackages)
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'
@loadPackage(name) for name in @getAvailablePackageNames() when not @isPackageDisabled(name)
@watchThemes()
loadPackage: (name, options) ->
if @isPackageDisabled(name)
return console.warn("Tried to load disabled package '#{name}'")
if packagePath = @resolvePackagePath(name)
return pack if pack = @getLoadedPackage(name)
pack = Package.load(packagePath, options)
if pack.metadata.theme
@themes.register(pack)
else
@loadedPackages[pack.name] = pack
pack
else
throw new Error("Could not resolve '#{name}' to a package path")
unloadPackage: (name) ->
if @isPackageActive(name)
throw new Error("Tried to unload active package '#{name}'")
if pack = @getLoadedPackage(name)
delete @loadedPackages[pack.name]
else
throw new Error("No loaded package for name '#{name}'")
resolvePackagePath: (name) ->
return name if fsUtils.isDirectorySync(name)
packagePath = fsUtils.resolve(config.packageDirPaths..., name)
return packagePath if fsUtils.isDirectorySync(packagePath)
packagePath = path.join(window.resourcePath, 'node_modules', name)
return packagePath if @isInternalPackage(packagePath)
isInternalPackage: (packagePath) ->
{engines} = Package.loadMetadata(packagePath, true)
engines?.atom?
getLoadedPackage: (name) ->
@loadedPackages[name]
isPackageLoaded: (name) ->
@getLoadedPackage(name)?
getLoadedPackages: ->
_.values(@loadedPackages)
isPackageDisabled: (name) ->
_.include(config.get('core.disabledPackages') ? [], name)
getAvailablePackagePaths: ->
packagePaths = []
for packageDirPath in config.packageDirPaths
for packagePath in fsUtils.listSync(packageDirPath)
packagePaths.push(packagePath) if fsUtils.isDirectorySync(packagePath)
for packagePath in fsUtils.listSync(path.join(window.resourcePath, 'node_modules'))
packagePaths.push(packagePath) if @isInternalPackage(packagePath)
_.uniq(packagePaths)
getAvailablePackageNames: ->
_.uniq _.map @getAvailablePackagePaths(), (packagePath) -> path.basename(packagePath)
getAvailablePackageMetadata: ->
packages = []
for packagePath in atom.getAvailablePackagePaths()
name = path.basename(packagePath)
metadata = atom.getLoadedPackage(name)?.metadata ? Package.loadMetadata(packagePath, true)
packages.push(metadata)
packages
#TODO Remove theses once packages have been migrated
getPackageState: (args...) -> @packages.getPackageState(args...)
setPackageState: (args...) -> @packages.setPackageState(args...)
activatePackages: (args...) -> @packages.activatePackages(args...)
activatePackage: (args...) -> @packages.activatePackage(args...)
deactivatePackages: (args...) -> @packages.deactivatePackages(args...)
deactivatePackage: (args...) -> @packages.deactivatePackage(args...)
getActivePackage: (args...) -> @packages.getActivePackage(args...)
isPackageActive: (args...) -> @packages.isPackageActive(args...)
getActivePackages: (args...) -> @packages.getActivePackages(args...)
loadPackages: (args...) -> @packages.loadPackages(args...)
loadPackage: (args...) -> @packages.loadPackage(args...)
unloadPackage: (args...) -> @packages.unloadPackage(args...)
resolvePackagePath: (args...) -> @packages.resolvePackagePath(args...)
isInternalPackage: (args...) -> @packages.isInternalPackage(args...)
getLoadedPackage: (args...) -> @packages.getLoadedPackage(args...)
isPackageLoaded: (args...) -> @packages.isPackageLoaded(args...)
getLoadedPackages: (args...) -> @packages.getLoadedPackages(args...)
isPackageDisabled: (args...) -> @packages.isPackageDisabled(args...)
getAvailablePackagePaths: (args...) -> @packages.getAvailablePackagePaths(args...)
getAvailablePackageNames: (args...) -> @packages.getAvailablePackageNames(args...)
getAvailablePackageMetadata: (args...)-> @packages.getAvailablePackageMetadata(args...)
loadThemes: ->
@themes.load()
watchThemes: ->
@themes.on 'reloaded', =>
@reloadBaseStylesheets()
pack.reloadStylesheets?() for name, pack of @loadedPackages
pack.reloadStylesheets?() for name, pack of @packages.getActivePackages()
null
loadBaseStylesheets: ->
requireStylesheet('bootstrap/less/bootstrap')
@reloadBaseStylesheets()
reloadBaseStylesheets: ->
requireStylesheet('../static/atom')
if nativeStylesheetPath = fsUtils.resolveOnLoadPath(process.platform, ['css', 'less'])
requireStylesheet(nativeStylesheetPath)
open: (options) ->
ipc.sendChannel('open', options)
@ -169,7 +171,7 @@ window.atom =
chosen = @confirmSync(message, detailedMessage, buttons)
callbacks[chosen]?()
confirmSync: (message, detailedMessage, buttons, browserWindow = null) ->
confirmSync: (message, detailedMessage, buttons, browserWindow=@getCurrentWindow()) ->
dialog.showMessageBox browserWindow,
type: 'info'
message: message
@ -181,46 +183,49 @@ window.atom =
showSaveDialogSync: (defaultPath) ->
defaultPath ?= project?.getPath()
currentWindow = remote.getCurrentWindow()
currentWindow = @getCurrentWindow()
dialog.showSaveDialog currentWindow, {title: 'Save File', defaultPath}
openDevTools: ->
remote.getCurrentWindow().openDevTools()
@getCurrentWindow().openDevTools()
toggleDevTools: ->
remote.getCurrentWindow().toggleDevTools()
@getCurrentWindow().toggleDevTools()
reload: ->
remote.getCurrentWindow().restart()
@getCurrentWindow().restart()
focus: ->
remote.getCurrentWindow().focus()
@getCurrentWindow().focus()
$(window).focus()
show: ->
remote.getCurrentWindow().show()
@getCurrentWindow().show()
hide: ->
remote.getCurrentWindow().hide()
@getCurrentWindow().hide()
close: ->
remote.getCurrentWindow().close()
@getCurrentWindow().close()
exit: (status) ->
app.exit(status)
exit: (status) -> app.exit(status)
toggleFullScreen: ->
@setFullScreen(!@isFullScreen())
setFullScreen: (fullScreen=false) ->
remote.getCurrentWindow().setFullScreen(fullScreen)
@getCurrentWindow().setFullScreen(fullScreen)
isFullScreen: ->
remote.getCurrentWindow().isFullScreen()
@getCurrentWindow().isFullScreen()
getHomeDirPath: ->
app.getHomeDir()
# Public: Get the directory path to Atom's configuration area.
getConfigDirPath: ->
@configDirPath ?= fsUtils.absolute('~/.atom')
getWindowStatePath: ->
switch @windowMode
when 'spec'
@ -232,7 +237,7 @@ window.atom =
filename = "editor-#{sha1}"
if filename
path.join(config.userStoragePath, filename)
path.join(@config.userStoragePath, filename)
else
null
@ -252,13 +257,13 @@ window.atom =
documentStateJson = @getLoadSettings().windowState
try
documentState = JSON.parse(documentStateJson) if documentStateJson?
documentState = JSON.parse(documentStateJson) if documentStateJson
catch error
console.warn "Error parsing window state: #{windowStatePath}", error.stack, error
doc = Document.deserialize(state: documentState) if documentState?
doc ?= Document.create()
window.site = doc.site # TODO: Remove this when everything is using telepath models
@site = doc.site # TODO: Remove this when everything is using telepath models
doc
saveWindowState: ->
@ -266,7 +271,7 @@ window.atom =
if windowStatePath = @getWindowStatePath()
windowState.saveSync(path: windowStatePath)
else
@getLoadSettings().windowState = JSON.stringify(windowState.serialize())
@getCurrentWindow().loadSettings.windowState = JSON.stringify(windowState.serialize())
getWindowState: (keyPath) ->
@windowState ?= @loadWindowState()
@ -281,9 +286,26 @@ window.atom =
crashRenderProcess: ->
process.crash()
beep: ->
shell.beep()
requireUserInitScript: ->
userInitScriptPath = path.join(config.configDirPath, "user.coffee")
userInitScriptPath = path.join(@config.configDirPath, "user.coffee")
try
require userInitScriptPath if fsUtils.isFileSync(userInitScriptPath)
catch error
console.error "Failed to load `#{userInitScriptPath}`", error.stack, error
requireWithGlobals: (id, globals={}) ->
existingGlobals = {}
for key, value of globals
existingGlobals[key] = window[key]
window[key] = value
require(id)
for key, value of existingGlobals
if value is undefined
delete window[key]
else
window[key] = value

View File

@ -23,6 +23,18 @@ class BindingSet
@selector = selector.replace(/!important/g, '')
@commandsByKeystrokes = @normalizeCommandsByKeystrokes(commandsByKeystrokes)
# Private:
getName: ->
@name
# Private:
getSelector: ->
@selector
# Private:
getCommandsByKeystrokes: ->
@commandsByKeystrokes
commandForEvent: (event) ->
for keystrokes, command of @commandsByKeystrokes
return command if event.keystrokes == keystrokes

View File

@ -1,3 +1,4 @@
app = require 'app'
ipc = require 'ipc'
Menu = require 'menu'
_ = require 'underscore'
@ -9,20 +10,22 @@ _ = require 'underscore'
module.exports =
class ApplicationMenu
version: null
devMode: null
menu: null
constructor: (@version, @devMode) ->
constructor: (@version) ->
@menu = Menu.buildFromTemplate @getDefaultTemplate()
Menu.setApplicationMenu @menu
# Public: Updates the entire menu with the given keybindings.
#
# * template:
# The Object which describes the menu to display.
# * keystrokesByCommand:
# An Object where the keys are commands and the values are Arrays containing
# the keystrokes.
update: (keystrokesByCommand) ->
template = @getTemplate(keystrokesByCommand)
update: (template, keystrokesByCommand) ->
@translateTemplate(template, keystrokesByCommand)
@substituteVersion(template)
@menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(@menu)
@ -32,11 +35,24 @@ class ApplicationMenu
# A complete menu configuration object for atom-shell's menu API.
#
# Returns an Array of native menu items.
allItems: (menu=@menu) ->
flattenMenuItems: (menu) ->
items = []
for index, item of menu.items or {}
items.push(item)
items = items.concat(@allItems(item.submenu)) if item.submenu
items = items.concat(@flattenMenuItems(item.submenu)) if item.submenu
items
# Private: Flattens the given menu template into an single Array.
#
# * template:
# An object describing the menu item.
#
# Returns an Array of native menu items.
flattenMenuTemplate: (template) ->
items = []
for item in template
items.push(item)
items = items.concat(@flattenMenuTemplate(item.submenu)) if item.submenu
items
# Public: Used to make all window related menu items are active.
@ -45,9 +61,14 @@ class ApplicationMenu
# If true enables all window specific items, if false disables all window
# specific items.
enableWindowSpecificItems: (enable) ->
for item in @allItems()
for item in @flattenMenuItems(@menu)
item.enabled = enable if item.metadata?['windowSpecific']
# Private: 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
@ -58,10 +79,9 @@ class ApplicationMenu
# * quitAndUpdateCallback:
# Function to call when the install menu item has been clicked.
showDownloadUpdateItem: (newVersion, quitAndUpdateCallback) ->
downloadUpdateItem = _.find @allItems(), (item) -> item.label == 'Install update'
if downloadUpdateItem
downloadUpdateItem.visible = true
downloadUpdateItem.click = quitAndUpdateCallback
if (item = _.find(@flattenMenuItems(@menu), (i) -> i.label == 'Install update'))
item.visible = true
item.click = quitAndUpdateCallback
# Private: Default list of menu items.
#
@ -77,82 +97,6 @@ class ApplicationMenu
]
]
# Private: The complete list of menu items.
#
# * keystrokesByCommand:
# An Object where the keys are commands and the values are Arrays containing
# the keystrokes.
#
# Returns a complete menu configuration Object for use with atom-shell's
# native menu API.
getTemplate: (keystrokesByCommand) ->
atomMenu =
label: 'Atom'
submenu: [
{ label: 'About Atom', command: 'application:about' }
{ label: "Version #{@version}", enabled: false }
{ label: "Install update", command: 'application:install-update', visible: false }
{ type: 'separator' }
{ label: 'Preferences...', command: 'application:show-settings' }
{ label: 'Hide Atom', command: 'application:hide' }
{ label: 'Hide Others', command: 'application:hide-other-applications' }
{ label: 'Show All', command: 'application:unhide-all-applications' }
{ type: 'separator' }
{ label: 'Run Atom Specs', command: 'application:run-all-specs' }
{ type: 'separator' }
{ label: 'Quit', command: 'application:quit' }
]
fileMenu =
label: 'File'
submenu: [
{ label: 'New Window', command: 'application:new-window' }
{ label: 'New File', command: 'application:new-file' }
{ type: 'separator' }
{ label: 'Open...', command: 'application:open' }
{ label: 'Open In Dev Mode...', command: 'application:open-dev' }
{ type: 'separator' }
{ label: 'Close Window', command: 'window:close' }
]
editMenu =
label: 'Edit'
submenu: [
{ label: 'Undo', command: 'core:undo' }
{ label: 'Redo', command: 'core:redo' }
{ type: 'separator' }
{ label: 'Cut', command: 'core:cut' }
{ label: 'Copy', command: 'core:copy' }
{ label: 'Paste', command: 'core:paste' }
{ label: 'Select All', command: 'core:select-all' }
]
viewMenu =
label: 'View'
submenu: [
{ label: 'Reload', command: 'window:reload' }
{ label: 'Toggle Full Screen', command: 'window:toggle-full-screen' }
{ label: 'Toggle Developer Tools', command: 'window:toggle-dev-tools' }
]
windowMenu =
label: 'Window'
submenu: [
{ label: 'Minimize', command: 'application:minimize' }
{ label: 'Zoom', command: 'application:zoom' }
{ type: 'separator' }
{ label: 'Bring All to Front', command: 'application:bring-all-windows-to-front' }
]
devMenu =
label: '\uD83D\uDC80' # Skull emoji
submenu: [ { label: 'In Development Mode', enabled: false } ]
template = [atomMenu, fileMenu, editMenu, viewMenu, windowMenu]
template.push devMenu if @devMode
@translateTemplate template, keystrokesByCommand
# Private: Combines a menu template with the appropriate keystrokes.
#
# * template:
@ -194,7 +138,7 @@ class ApplicationMenu
modifiers = modifiers.map (modifier) ->
modifier.replace(/shift/ig, "Shift")
.replace(/meta/ig, "Command")
.replace(/ctrl/ig, "MacCtrl")
.replace(/ctrl/ig, "Ctrl")
.replace(/alt/ig, "Alt")
keys = modifiers.concat([key.toUpperCase()])

View File

@ -1,7 +1,6 @@
AtomWindow = require 'atom-window'
ApplicationMenu = require 'application-menu'
AtomProtocolHandler = require 'atom-protocol-handler'
BrowserWindow = require 'browser-window'
AtomWindow = require './atom-window'
ApplicationMenu = require './application-menu'
AtomProtocolHandler = require './atom-protocol-handler'
Menu = require 'menu'
autoUpdater = require 'auto-updater'
app = require 'app'
@ -24,6 +23,7 @@ socketPath = '/tmp/atom.sock'
module.exports =
class AtomApplication
_.extend @prototype, EventEmitter.prototype
updateVersion: null
# Public: The entry point into the Atom application.
@open: (options) ->
@ -33,7 +33,7 @@ class AtomApplication
# take a few seconds to trigger 'error' event, it could be a bug of node
# or atom-shell, before it's fixed we check the existence of socketPath to
# speedup startup.
if not fs.existsSync socketPath
if (not fs.existsSync socketPath) or options.test
createAtomApplication()
return
@ -50,14 +50,15 @@ class AtomApplication
resourcePath: null
version: null
constructor: ({@resourcePath, pathsToOpen, urlsToOpen, @version, test, pidToKillWhenClosed, devMode, newWindow}) ->
constructor: (options) ->
{@resourcePath, @version, @devMode} = options
global.atomApplication = this
@pidsToOpenWindows = {}
@pathsToOpen ?= []
@windows = []
@applicationMenu = new ApplicationMenu(@version, devMode)
@applicationMenu = new ApplicationMenu(@version)
@atomProtocolHandler = new AtomProtocolHandler(@resourcePath)
@listenForArgumentsFromNewProcess()
@ -65,8 +66,12 @@ class AtomApplication
@handleEvents()
@checkForUpdates()
@openWithOptions(options)
# Private: Opens a new window based on the options provided.
openWithOptions: ({pathsToOpen, urlsToOpen, test, pidToKillWhenClosed, devMode, newWindow, specDirectory}) ->
if test
@runSpecs({exitWhenDone: true, @resourcePath})
@runSpecs({exitWhenDone: true, @resourcePath, specDirectory})
else if pathsToOpen.length > 0
@openPaths({pathsToOpen, pidToKillWhenClosed, newWindow, devMode})
else if urlsToOpen.length > 0
@ -93,8 +98,7 @@ class AtomApplication
fs.unlinkSync socketPath if fs.existsSync(socketPath)
server = net.createServer (connection) =>
connection.on 'data', (data) =>
options = JSON.parse(data)
@openPaths(options)
@openWithOptions(JSON.parse(data))
server.listen socketPath
server.on 'error', (error) -> console.error 'Application server failed', error
@ -125,7 +129,8 @@ class AtomApplication
@on 'application:hide', -> Menu.sendActionToFirstResponder('hide:')
@on 'application:hide-other-applications', -> Menu.sendActionToFirstResponder('hideOtherApplications:')
@on 'application:unhide-all-applications', -> Menu.sendActionToFirstResponder('unhideAllApplications:')
@on 'application:new-window', -> @openPath()
@on 'application:new-window', ->
@openPath(initialSize: @getFocusedWindowSize())
@on 'application:new-file', -> (@focusedWindow() ? this).openPath()
@on 'application:open', -> @promptForPath()
@on 'application:open-dev', -> @promptForPath(devMode: true)
@ -143,11 +148,13 @@ class AtomApplication
app.on 'open-url', (event, urlToOpen) =>
event.preventDefault()
@openUrl(urlToOpen)
@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) =>
@ -159,8 +166,8 @@ class AtomApplication
else
@promptForPath()
ipc.once 'update-application-menu', (processId, routingId, keystrokesByCommand) =>
@applicationMenu.update(keystrokesByCommand)
ipc.on 'update-application-menu', (processId, routingId, template, keystrokesByCommand) =>
@applicationMenu.update(template, keystrokesByCommand)
ipc.on 'run-package-specs', (processId, routingId, specDirectory) =>
@runSpecs({resourcePath: global.devResourcePath, specDirectory: specDirectory, exitWhenDone: false})
@ -189,6 +196,17 @@ class AtomApplication
focusedWindow: ->
_.find @windows, (atomWindow) -> atomWindow.isFocused()
# Public: Get the height and width of the focused window.
#
# Returns an object with height and width keys or null if there is no
# focused window.
getFocusedWindowSize: ->
if focusedWindow = @focusedWindow()
[width, height] = focusedWindow.getSize()
{width, height}
else
null
# Public: Opens multiple paths, in existing windows if possible.
#
# * options
@ -214,10 +232,13 @@ class AtomApplication
# Boolean of whether this should be opened in a new window.
# + devMode:
# Boolean to control the opened window's dev mode.
openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode}={}) ->
[basename, initialLine] = path.basename(pathToOpen).split(':')
pathToOpen = "#{path.dirname(pathToOpen)}/#{basename}"
initialLine -= 1 if initialLine # Convert line numbers to a base of 0
# + initialSize:
# Object with height and width keys.
openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, initialSize}={}) ->
if pathToOpen
[basename, initialLine] = path.basename(pathToOpen).split(':')
pathToOpen = "#{path.dirname(pathToOpen)}/#{basename}"
initialLine -= 1 if initialLine # Convert line numbers to a base of 0
unless devMode
existingWindow = @windowForPath(pathToOpen) unless pidToKillWhenClosed or newWindow
@ -230,8 +251,8 @@ class AtomApplication
bootstrapScript = require.resolve(path.join(global.devResourcePath, 'src', 'window-bootstrap'))
else
resourcePath = @resourcePath
bootstrapScript = require.resolve('./window-bootstrap')
openedWindow = new AtomWindow({pathToOpen, initialLine, bootstrapScript, resourcePath, devMode})
bootstrapScript = require.resolve('../window-bootstrap')
openedWindow = new AtomWindow({pathToOpen, initialLine, bootstrapScript, resourcePath, devMode, initialSize})
if pidToKillWhenClosed?
@pidsToOpenWindows[pidToKillWhenClosed] = openedWindow
@ -245,9 +266,11 @@ class AtomApplication
console.log("Killing process #{pid} failed: #{error.code}")
delete @pidsToOpenWindows[pid]
# Private: Handles an atom:// url.
# Private: Open an atom:// url.
#
# Currently only supports atom://session/<session-id> urls.
# 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
# that package's `urlMain` as the bootstrap script.
#
# * options
# + urlToOpen:
@ -255,15 +278,25 @@ class AtomApplication
# + devMode:
# Boolean to control the opened window's dev mode.
openUrl: ({urlToOpen, devMode}) ->
parsedUrl = url.parse(urlToOpen)
if parsedUrl.host is 'session'
sessionId = parsedUrl.path.split('/')[1]
console.log "Joining session #{sessionId}"
if sessionId
bootstrapScript = 'collaboration/lib/bootstrap'
new AtomWindow({bootstrapScript, @resourcePath, sessionId, devMode})
unless @packages?
PackageManager = require '../package-manager'
fsUtils = require '../fs-utils'
@packages = new PackageManager
configDirPath: fsUtils.absolute('~/.atom')
devMode: devMode
resourcePath: @resourcePath
packageName = url.parse(urlToOpen).host
pack = _.find @packages.getAvailablePackageMetadata(), ({name}) -> name is packageName
if pack?
if pack.urlMain
packagePath = @packages.resolvePackagePath(packageName)
bootstrapScript = path.resolve(packagePath, pack.urlMain)
new AtomWindow({bootstrapScript, @resourcePath, devMode, urlToOpen, initialSize: getFocusedWindowSize()})
else
console.log "Package '#{pack.name}' does not have a url main: #{urlToOpen}"
else
console.log "Opening unknown url #{urlToOpen}"
console.log "Opening unknown url: #{urlToOpen}"
# Private: Opens up a new {AtomWindow} to run specs within.
#
@ -281,7 +314,7 @@ class AtomApplication
try
bootstrapScript = require.resolve(path.resolve(global.devResourcePath, 'spec', 'spec-bootstrap'))
catch error
bootstrapScript = require.resolve(path.resolve(__dirname, '..', 'spec', 'spec-bootstrap'))
bootstrapScript = require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'spec-bootstrap'))
isSpec = true
devMode = true
@ -291,9 +324,9 @@ class AtomApplication
try
bootstrapScript = require.resolve(path.resolve(global.devResourcePath, 'benchmark', 'benchmark-bootstrap'))
catch error
bootstrapScript = require.resolve(path.resolve(__dirname, '..', 'benchmark', 'benchmark-bootstrap'))
bootstrapScript = require.resolve(path.resolve(__dirname, '..', '..', 'benchmark', 'benchmark-bootstrap'))
isSpec = true # Needed because this flag adds the spec directory to the NODE_PATH
isSpec = true
new AtomWindow({bootstrapScript, @resourcePath, isSpec})
# Private: Opens a native dialog to prompt the user for a path.
@ -307,3 +340,8 @@ class AtomApplication
promptForPath: ({devMode}={}) ->
pathsToOpen = dialog.showOpenDialog title: 'Open', properties: ['openFile', 'openDirectory', 'multiSelections', 'createDirectory']
@openPaths({pathsToOpen, devMode})
# Public: If an update is available, it returns the new version string
# otherwise it returns null.
getUpdateVersion: ->
@updateVersion

View File

@ -1,8 +1,6 @@
BrowserWindow = require 'browser-window'
Menu = require 'menu'
MenuItem = require 'menu-item'
ContextMenu = require 'context-menu'
app = require 'app'
ContextMenu = require './context-menu'
dialog = require 'dialog'
ipc = require 'ipc'
path = require 'path'
@ -31,20 +29,18 @@ class AtomWindow
loadSettings = _.extend({}, settings)
loadSettings.windowState ?= ''
loadSettings.initialPath = pathToOpen
try
if fs.statSync(pathToOpen).isFile()
loadSettings.initialPath = path.dirname(pathToOpen)
if fs.statSyncNoException(pathToOpen).isFile?()
loadSettings.initialPath = path.dirname(pathToOpen)
@browserWindow.loadSettings = loadSettings
@browserWindow.once 'window:loaded', => @loaded = true
@browserWindow.loadUrl "file://#{@resourcePath}/static/index.html"
@browserWindow.focusOnWebView() if @isSpec
@openPath(pathToOpen, initialLine)
setupNodePath: (resourcePath) ->
paths = ['exports', 'node_modules']
paths = paths.map (relativePath) -> path.resolve(resourcePath, relativePath)
process.env['NODE_PATH'] = paths.join path.delimiter
process.env['NODE_PATH'] = path.resolve(resourcePath, 'exports')
getInitialPath: ->
@browserWindow.loadSettings.initialPath
@ -57,6 +53,8 @@ class AtomWindow
false
else if pathToCheck is initialPath
true
else if fs.statSyncNoException(pathToCheck).isDirectory?()
false
else if pathToCheck.indexOf(path.join(initialPath, path.sep)) is 0
true
else
@ -85,7 +83,7 @@ class AtomWindow
when 1 then @browserWindow.restart()
@browserWindow.on 'context-menu', (menuTemplate) =>
new ContextMenu(menuTemplate)
new ContextMenu(menuTemplate, @browserWindow)
if @isSpec
# Spec window's web view should always have focus
@ -96,20 +94,28 @@ class AtomWindow
if @loaded
@focus()
@sendCommand('window:open-path', {pathToOpen, initialLine})
@sendCommand('window:update-available', global.atomApplication.getUpdateVersion()) if global.atomApplication.getUpdateVersion()
else
@browserWindow.once 'window:loaded', => @openPath(pathToOpen, initialLine)
sendCommand: (command, args...) ->
if @handlesAtomCommands()
@sendAtomCommand(command, args...)
if @isSpecWindow()
unless @sendCommandToFirstResponder(command)
switch command
when 'window:reload' then @reload()
when 'window:toggle-dev-tools' then @toggleDevTools()
when 'window:close' then @close()
else if @isWebViewFocused()
@sendCommandToBrowserWindow(command, args...)
else
@sendNativeCommand(command)
unless @sendCommandToFirstResponder(command)
@sendCommandToBrowserWindow(command, args...)
sendAtomCommand: (command, args...) ->
sendCommandToBrowserWindow: (command, args...) ->
action = if args[0]?.contextCommand then 'context-command' else 'command'
ipc.sendChannel @browserWindow.getProcessId(), @browserWindow.getRoutingId(), action, command, args...
sendNativeCommand: (command) ->
sendCommandToFirstResponder: (command) ->
switch command
when 'core:undo' then Menu.sendActionToFirstResponder('undo:')
when 'core:redo' then Menu.sendActionToFirstResponder('redo:')
@ -117,14 +123,15 @@ class AtomWindow
when 'core:cut' then Menu.sendActionToFirstResponder('cut:')
when 'core:paste' then Menu.sendActionToFirstResponder('paste:')
when 'core:select-all' then Menu.sendActionToFirstResponder('selectAll:')
when 'window:reload' then @reload()
when 'window:toggle-dev-tools' then @toggleDevTools()
when 'window:close' then @close()
else return false
true
close: -> @browserWindow.close()
focus: -> @browserWindow.focus()
getSize: -> @browserWindow.getSize()
handlesAtomCommands: ->
not @isSpecWindow() and @isWebViewFocused()

View File

@ -1,12 +1,11 @@
Menu = require 'menu'
BrowserWindow = require 'browser-window'
module.exports =
class ContextMenu
constructor: (template) ->
constructor: (template, browserWindow) ->
template = @createClickHandlers(template)
menu = Menu.buildFromTemplate(template)
menu.popup(BrowserWindow.getFocusedWindow())
menu.popup(browserWindow)
# Private: It's necessary to build the event handlers in this process, otherwise
# closures are drug across processes and failed to be garbage collected

View File

@ -14,6 +14,10 @@ dialog = require 'dialog'
console.log = (args...) ->
nslog(args.map((arg) -> JSON.stringify(arg)).join(" "))
process.on 'uncaughtException', (error={}) ->
nslog(error.message) if error.message?
nslog(error.stack) if error.stack?
delegate.browserMainParts.preMainMessageLoopRun = ->
args = parseCommandLine()
@ -46,13 +50,10 @@ delegate.browserMainParts.preMainMessageLoopRun = ->
require('coffee-script')
if args.devMode
require(path.join(args.resourcePath, 'src', 'coffee-cache'))
module.globalPaths.push(path.join(args.resourcePath, 'src'))
require(path.join(args.resourcePath, 'src', 'coffee-cache')).register()
AtomApplication = require path.join(args.resourcePath, 'src', 'browser', 'atom-application')
else
appSrcPath = path.resolve(process.argv[0], "../../Resources/app/src")
module.globalPaths.push(appSrcPath)
AtomApplication = require 'atom-application'
AtomApplication = require './atom-application'
AtomApplication.open(args)
console.log("App load time: #{new Date().getTime() - startTime}ms")
@ -76,11 +77,12 @@ parseCommandLine = ->
Usage: atom [options] [file ...]
"""
options.alias('d', 'dev').boolean('d').describe('d', 'Run in development mode.')
options.alias('f', 'foreground').boolean('f').describe('f', 'Keep the browser process in the foreground.')
options.alias('h', 'help').boolean('h').describe('h', 'Print this usage message.')
options.alias('n', 'new-window').boolean('n').describe('n', 'Open a new window.')
options.alias('t', 'test').boolean('t').describe('t', 'Run the Atom specs and exit with error code on failures.')
options.alias('s', 'spec-directory').string('s').describe('s', 'Set the directory from which specs are loaded (default: Atom\'s spec directory).')
options.alias('t', 'test').boolean('t').describe('t', 'Run the specified specs and exit with error code on failures.')
options.alias('v', 'version').boolean('v').describe('v', 'Print the version.')
options.alias('f', 'foreground').boolean('f').describe('f', 'Keep the browser process in the foreground.')
options.alias('w', 'wait').boolean('w').describe('w', 'Wait for window to be closed before returning.')
args = options.argv
@ -97,6 +99,7 @@ parseCommandLine = ->
pathsToOpen = args._
pathsToOpen = [executedFrom] if executedFrom and pathsToOpen.length is 0
test = args['test']
specDirectory = args['spec-directory']
newWindow = args['new-window']
pidToKillWhenClosed = args['pid'] if args['wait']
@ -110,6 +113,6 @@ parseCommandLine = ->
fs.statSync resourcePath
catch e
devMode = false
resourcePath = path.dirname(__dirname)
resourcePath = path.dirname(path.dirname(__dirname))
{resourcePath, pathsToOpen, executedFrom, test, version, pidToKillWhenClosed, devMode, newWindow}
{resourcePath, pathsToOpen, executedFrom, test, version, pidToKillWhenClosed, devMode, newWindow, specDirectory}

View File

@ -15,8 +15,9 @@ getCachePath = (coffee) ->
path.join(coffeeCacheDir, "#{digest}.coffee")
getCachedJavaScript = (cachePath) ->
try
fs.readFileSync(cachePath, 'utf8') if fs.statSync(cachePath).isFile()
if stat = fs.statSyncNoException(cachePath)
try
fs.readFileSync(cachePath, 'utf8') if stat.isFile()
compileCoffeeScript = (coffee, filePath, cachePath) ->
js = CoffeeScript.compile(coffee, filename: filePath)
@ -25,10 +26,16 @@ compileCoffeeScript = (coffee, filePath, cachePath) ->
fs.writeFileSync(cachePath, js)
js
require.extensions['.coffee'] = (module, filePath) ->
requireCoffeeScript = (module, filePath) ->
coffee = fs.readFileSync(filePath, 'utf8')
cachePath = getCachePath(coffee)
js = getCachedJavaScript(cachePath) ? compileCoffeeScript(coffee, filePath, cachePath)
module._compile(js, filePath)
module.exports = {cacheDir}
module.exports =
cacheDir: cacheDir
register: ->
Object.defineProperty(require.extensions, '.coffee', {
writable: false
value: requireCoffeeScript
})

View File

@ -7,58 +7,53 @@ path = require 'path'
async = require 'async'
pathWatcher = require 'pathwatcher'
configDirPath = fsUtils.absolute("~/.atom")
nodeModulesDirPath = path.join(resourcePath, "node_modules")
bundledKeymapsDirPath = path.join(resourcePath, "keymaps")
userPackagesDirPath = path.join(configDirPath, "packages")
userPackageDirPaths = [userPackagesDirPath]
userPackageDirPaths.unshift(path.join(configDirPath, "dev", "packages")) if atom.getLoadSettings().devMode
userStoragePath = path.join(configDirPath, "storage")
# 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 `global.config`
# referenced using `atom.config`
#
# ### 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
# global.config.set('myplugin.key', 'value')
# global.observe 'myplugin.key', ->
# console.log 'My configuration changed:', global.config.get('myplugin.key')
# atom.config.set('myplugin.key', 'value')
# atom.config.observe 'myplugin.key', ->
# console.log 'My configuration changed:', atom.config.get('myplugin.key')
# ```
module.exports =
class Config
_.extend @prototype, EventEmitter
configDirPath: configDirPath
bundledPackageDirPaths: [nodeModulesDirPath]
bundledKeymapsDirPath: bundledKeymapsDirPath
nodeModulesDirPath: nodeModulesDirPath
packageDirPaths: _.clone(userPackageDirPaths)
userPackageDirPaths: userPackageDirPaths
userStoragePath: userStoragePath
lessSearchPaths: [
path.join(resourcePath, 'static', 'variables')
path.join(resourcePath, 'static')
]
defaultSettings: null
settings: null
configFileHasErrors: null
# Private: Created during initialization, available as `global.config`
constructor: ->
constructor: ({@configDirPath, @resourcePath}={}) ->
@bundledKeymapsDirPath = path.join(@resourcePath, "keymaps")
@bundledMenusDirPath = path.join(resourcePath, "menus")
@nodeModulesDirPath = path.join(@resourcePath, "node_modules")
@bundledPackageDirPaths = [@nodeModulesDirPath]
@lessSearchPaths = [
path.join(@resourcePath, 'static', 'variables')
path.join(@resourcePath, 'static')
]
@packageDirPaths = [path.join(@configDirPath, "packages")]
if atom.getLoadSettings().devMode
@packageDirPaths.unshift(path.join(@configDirPath, "dev", "packages"))
@userPackageDirPaths = _.clone(@packageDirPaths)
@userStoragePath = path.join(@configDirPath, "storage")
@defaultSettings =
core: _.clone(require('./root-view').configDefaults)
editor: _.clone(require('./editor').configDefaults)
@settings = {}
@configFilePath = fsUtils.resolve(configDirPath, 'config', ['json', 'cson'])
@configFilePath ?= path.join(configDirPath, 'config.cson')
@configFilePath = fsUtils.resolve(@configDirPath, 'config', ['json', 'cson'])
@configFilePath ?= path.join(@configDirPath, 'config.cson')
# Private:
initializeConfigDirectory: (done) ->
@ -70,7 +65,7 @@ class Config
fsUtils.copy(sourcePath, destinationPath, callback)
queue.drain = done
templateConfigDirPath = fsUtils.resolve(window.resourcePath, 'dot-atom')
templateConfigDirPath = fsUtils.resolve(@resourcePath, 'dot-atom')
onConfigDirFile = (sourcePath) =>
relativePath = sourcePath.substring(templateConfigDirPath.length + 1)
destinationPath = path.join(@configDirPath, relativePath)

View File

@ -54,6 +54,14 @@ class CursorView extends View
@setVisible(@cursor.isVisible() and not @editor.isFoldedAtScreenRow(screenPosition.row))
# Override for speed. The base function checks the computedStyle
isHidden: ->
style = this[0].style
if style.display == 'none' or not @isOnDom()
true
else
false
needsAutoscroll: ->
@cursor.needsAutoscroll

View File

@ -102,10 +102,21 @@ class Cursor
# Public: Returns the visibility of the cursor.
isVisible: -> @visible
# Public: Returns a RegExp of what the cursor considers a "word"
wordRegExp: ->
nonWordCharacters = config.get("editor.nonWordCharacters")
new RegExp("^[\t ]*$|[^\\s#{_.escapeRegExp(nonWordCharacters)}]+|[#{_.escapeRegExp(nonWordCharacters)}]+", "g")
# 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.
#
# Returns a RegExp.
wordRegExp: ({includeNonWordCharacters}={})->
includeNonWordCharacters ?= true
nonWordCharacters = config.get('editor.nonWordCharacters')
segments = ["^[\t ]*$"]
segments.push("[^\\s#{_.escapeRegExp(nonWordCharacters)}]+")
if includeNonWordCharacters
segments.push("[#{_.escapeRegExp(nonWordCharacters)}]+")
new RegExp(segments.join("|"), "g")
# Public: Identifies if this cursor is the last in the {EditSession}.
#
@ -126,6 +137,25 @@ class Cursor
range = [[row, Math.min(0, column - 1)], [row, Math.max(0, column + 1)]]
/^\s+$/.test @editSession.getTextInBufferRange(range)
# Public: Returns whether the cursor is currently between a word and non-word
# character. The non-word characters are defined by the
# `editor.nonWordCharacters` config value.
#
# This method returns false if the character before or after the cursor is
# whitespace.
#
# Returns a Boolean.
isBetweenWordAndNonWord: ->
return false if @isAtBeginningOfLine() or @isAtEndOfLine()
{row, column} = @getBufferPosition()
range = [[row, column - 1], [row, column + 1]]
[before, after] = @editSession.getTextInBufferRange(range)
return false if /\s/.test(before) or /\s/.test(after)
nonWordCharacters = config.get('editor.nonWordCharacters').split('')
_.contains(nonWordCharacters, before) isnt _.contains(nonWordCharacters, after)
# Public: Returns whether this cursor is between a word's start and end.
isInsideWord: ->
{row, column} = @getBufferPosition()
@ -280,6 +310,9 @@ class Cursor
# * 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.
#
# Returns a {Range}.
getBeginningOfCurrentWordBufferPosition: (options = {}) ->
@ -289,7 +322,7 @@ class Cursor
scanRange = [[previousNonBlankRow, 0], currentBufferPosition]
beginningOfWordPosition = null
@editSession.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) =>
@editSession.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, stop}) =>
if range.end.isGreaterThanOrEqual(currentBufferPosition) or allowPrevious
beginningOfWordPosition = range.start
if not beginningOfWordPosition?.isEqual(currentBufferPosition)
@ -297,7 +330,7 @@ class Cursor
beginningOfWordPosition or currentBufferPosition
# Public: Retrieves buffer position of previous word boundry. It might be on
# Public: Retrieves buffer position of previous word boundary. It might be on
# the current word, or the previous word.
getPreviousWordBoundaryBufferPosition: (options = {}) ->
currentBufferPosition = @getBufferPosition()
@ -319,7 +352,7 @@ class Cursor
beginningOfWordPosition or currentBufferPosition
# Public: Retrieves buffer position of the next word boundry. It might be on
# Public: Retrieves buffer position of the next word boundary. It might be on
# the current word, or the previous word.
getMoveNextWordBoundaryBufferPosition: (options = {}) ->
currentBufferPosition = @getBufferPosition()
@ -345,6 +378,9 @@ class Cursor
# * 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.
#
# Returns a {Range}.
getEndOfCurrentWordBufferPosition: (options = {}) ->
@ -353,7 +389,7 @@ class Cursor
scanRange = [currentBufferPosition, @editSession.getEofBufferPosition()]
endOfWordPosition = null
@editSession.scanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) =>
@editSession.scanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, stop}) =>
if range.start.isLessThanOrEqual(currentBufferPosition) or allowNext
endOfWordPosition = range.end
if not endOfWordPosition?.isEqual(currentBufferPosition)

View File

@ -0,0 +1,44 @@
{Document} = require 'telepath'
# Public: Manages the deserializers used for serialized state
module.exports =
class DeserializerManager
constructor: ->
@deserializers = {}
@deferredDeserializers = {}
# Public: Add a deserializer.
add: (klasses...) ->
@deserializers[klass.name] = klass for klass in klasses
# Public: Add a deferred deserializer.
addDeferred: (name, fn) ->
@deferredDeserializers[name] = fn
# Public: Remove a deserializer.
remove: (klasses...) ->
delete @deserializers[klass.name] for klass in klasses
# Public: Deserialize the state and params.
deserialize: (state, params) ->
return unless state?
if deserializer = @get(state)
stateVersion = state.get?('version') ? state.version
return if deserializer.version? and deserializer.version isnt stateVersion
if (state instanceof Document) and not deserializer.acceptsDocuments
state = state.toObject()
deserializer.deserialize(state, params)
else
console.warn "No deserializer found for", state
# Public: Get the deserializer for the state.
get: (state) ->
return unless state?
name = state.get?('deserializer') ? state.deserializer
if @deferredDeserializers[name]
@deferredDeserializers[name]()
delete @deferredDeserializers[name]
@deserializers[name]

View File

@ -9,11 +9,16 @@ fsUtils = require './fs-utils'
$ = require './jquery-extensions'
_ = require './underscore-extensions'
MeasureRange = document.createRange()
TextNodeFilter = { acceptNode: -> NodeFilter.FILTER_ACCEPT }
NoScope = ['no-scope']
# Private: Represents the entire visual pane in Atom.
#
# The Editor manages the {EditSession}, which manages the file buffers.
module.exports =
class Editor extends View
@characterWidthCache: {}
@configDefaults:
fontSize: 20
showInvisibles: false
@ -37,11 +42,11 @@ class Editor extends View
_.extend(attributes, params.attributes) if params.attributes
@div attributes, =>
@subview 'gutter', new Gutter
@input class: 'hidden-input', outlet: 'hiddenInput'
@div class: 'scroll-view', outlet: 'scrollView', =>
@div class: 'overlayer', outlet: 'overlayer'
@div class: 'lines', outlet: 'renderedLines'
@div class: 'underlayer', outlet: 'underlayer'
@div class: 'underlayer', outlet: 'underlayer', =>
@input class: 'hidden-input', outlet: 'hiddenInput'
@div class: 'vertical-scrollbar', outlet: 'verticalScrollbar', =>
@div outlet: 'verticalScrollbarContent'
@ -498,6 +503,11 @@ class Editor extends View
# {Delegates to: EditSession.getScreenLineCount}
getScreenLineCount: -> @activeEditSession.getScreenLineCount()
# Private:
setHeightInLines: (heightInLines)->
heightInLines ?= @calculateHeightInLines()
@heightInLines = heightInLines if heightInLines
# {Delegates to: EditSession.setEditorWidthInChars}
setWidthInChars: (widthInChars) ->
widthInChars ?= @calculateWidthInChars()
@ -638,9 +648,9 @@ class Editor extends View
@addClass 'is-focused'
@hiddenInput.on 'focusout', =>
@bringHiddenInputIntoView()
@isFocused = false
@removeClass 'is-focused'
@hiddenInput.offset(top: 0, left: 0)
@underlayer.on 'mousedown', (e) =>
@renderedLines.trigger(e)
@ -696,8 +706,14 @@ class Editor extends View
handleInputEvents: ->
@on 'cursor:moved', =>
return unless @isFocused
cursorView = @getCursorView()
@hiddenInput.offset(cursorView.offset()) if cursorView.is(':visible')
if cursorView.isVisible()
# This is an order of magnitude faster than checking .offset().
style = cursorView[0].style
@hiddenInput[0].style.top = style.top
@hiddenInput[0].style.left = style.left
selectedText = null
@hiddenInput.on 'compositionstart', =>
@ -721,6 +737,9 @@ class Editor extends View
@hiddenInput.val(lastInput)
false
bringHiddenInputIntoView: ->
@hiddenInput.css(top: @scrollTop(), left: @scrollLeft())
selectOnMousemoveUntilMouseup: ->
lastMoveEvent = null
moveHandler = (event = lastMoveEvent) =>
@ -746,6 +765,7 @@ class Editor extends View
@calculateDimensions()
@setWidthInChars()
@subscribe $(window), "resize.editor-#{@id}", =>
@setHeightInLines()
@setWidthInChars()
@requestDisplayUpdate()
@focus() if @isFocused
@ -834,6 +854,7 @@ class Editor extends View
@underlayer.css('top', -scrollTop)
@overlayer.css('top', -scrollTop)
@gutter.lineNumbers.css('top', -scrollTop)
if options?.adjustVerticalScrollbar ? true
@verticalScrollbar.scrollTop(scrollTop)
@activeEditSession.setScrollTop(@scrollTop())
@ -936,6 +957,9 @@ class Editor extends View
calculateWidthInChars: ->
Math.floor(@scrollView.width() / @charWidth)
calculateHeightInLines: ->
Math.ceil($(window).height() / @lineHeight)
# Enables/disables soft wrap on the editor.
#
# softWrap - A {Boolean} which, if `true`, enables soft wrap
@ -950,13 +974,9 @@ class Editor extends View
#
# fontSize - A {Number} indicating the font size in pixels.
setFontSize: (fontSize) ->
headTag = $("head")
styleTag = headTag.find("style.font-size")
if styleTag.length == 0
styleTag = $$ -> @style class: 'font-size'
headTag.append styleTag
@css('font-size', "#{fontSize}px}")
styleTag.text(".editor {font-size: #{fontSize}px}")
@clearCharacterWidthCache()
if @isOnDom()
@redraw()
@ -972,17 +992,10 @@ class Editor extends View
# Sets the font family for the editor.
#
# fontFamily - A {String} identifying the CSS `font-family`,
setFontFamily: (fontFamily) ->
headTag = $("head")
styleTag = headTag.find("style.editor-font-family")
setFontFamily: (fontFamily='') ->
@css('font-family', fontFamily)
if fontFamily?
if styleTag.length == 0
styleTag = $$ -> @style class: 'editor-font-family'
headTag.append styleTag
styleTag.text(".editor {font-family: #{fontFamily}}")
else
styleTag.remove()
@clearCharacterWidthCache()
@redraw()
@ -991,11 +1004,7 @@ class Editor extends View
# Returns a {String} identifying the CSS `font-family`,
getFontFamily: -> @css("font-family")
# Clears the CSS `font-family` property from the editor.
clearFontFamily: ->
$('head style.editor-font-family').remove()
# Clears the CSS `font-family` property from the editor.
# Redraw the editor
redraw: ->
return unless @hasParent()
return unless @attached
@ -1124,6 +1133,7 @@ class Editor extends View
@charWidth = charRect.width
@charHeight = charRect.height
fragment.remove()
@setHeightInLines()
updateLayerDimensions: ->
height = @lineHeight * @getScreenLineCount()
@ -1143,6 +1153,14 @@ class Editor extends View
@layerMinWidth = minWidth
@trigger 'editor:min-width-changed'
# Override for speed. The base function checks computedStyle, unnecessary here.
isHidden: ->
style = this[0].style
if style.display == 'none' or not @isOnDom()
true
else
false
clearRenderedLines: ->
@renderedLines.empty()
@firstRenderedScreenRow = null
@ -1174,7 +1192,7 @@ class Editor extends View
updateDisplay: (options={}) ->
return unless @attached and @activeEditSession
return if @activeEditSession.destroyed
unless @isVisible()
unless @isOnDom() and @isVisible()
@redrawOnReattach = true
return
@ -1194,9 +1212,15 @@ class Editor extends View
for cursorView in @getCursorViews()
if cursorView.needsRemoval
cursorView.remove()
else if cursorView.needsUpdate
else if @shouldUpdateCursor(cursorView)
cursorView.updateDisplay()
shouldUpdateCursor: (cursorView) ->
return false unless cursorView.needsUpdate
pos = cursorView.getScreenPosition()
pos.row >= @firstRenderedScreenRow and pos.row <= @lastRenderedScreenRow
updateSelectionViews: ->
if @newSelections.length > 0
@addSelectionView(selection) for selection in @newSelections when not selection.destroyed
@ -1205,9 +1229,17 @@ class Editor extends View
for selectionView in @getSelectionViews()
if selectionView.needsRemoval
selectionView.remove()
else
else if @shouldUpdateSelection(selectionView)
selectionView.updateDisplay()
shouldUpdateSelection: (selectionView) ->
screenRange = selectionView.getScreenRange()
startRow = screenRange.start.row
endRow = screenRange.end.row
(startRow >= @firstRenderedScreenRow and startRow <= @lastRenderedScreenRow) or # startRow in range
(endRow >= @firstRenderedScreenRow and endRow <= @lastRenderedScreenRow) or # endRow in range
(startRow <= @firstRenderedScreenRow and endRow >= @lastRenderedScreenRow) # selection surrounds the rendered items
syncCursorAnimations: ->
for cursorView in @getCursorViews()
do (cursorView) -> cursorView.resetBlinking()
@ -1226,23 +1258,24 @@ class Editor extends View
updateRenderedLines: ->
firstVisibleScreenRow = @getFirstVisibleScreenRow()
lastVisibleScreenRow = @getLastVisibleScreenRow()
lastScreenRowToRender = firstVisibleScreenRow + @heightInLines - 1
lastScreenRow = @getLastScreenRow()
if @firstRenderedScreenRow? and firstVisibleScreenRow >= @firstRenderedScreenRow and lastVisibleScreenRow <= @lastRenderedScreenRow
if @firstRenderedScreenRow? and firstVisibleScreenRow >= @firstRenderedScreenRow and lastScreenRowToRender <= @lastRenderedScreenRow
renderFrom = Math.min(lastScreenRow, @firstRenderedScreenRow)
renderTo = Math.min(lastScreenRow, @lastRenderedScreenRow)
else
renderFrom = Math.min(lastScreenRow, Math.max(0, firstVisibleScreenRow - @lineOverdraw))
renderTo = Math.min(lastScreenRow, lastVisibleScreenRow + @lineOverdraw)
renderTo = Math.min(lastScreenRow, lastScreenRowToRender + @lineOverdraw)
if @pendingChanges.length == 0 and @firstRenderedScreenRow and @firstRenderedScreenRow <= renderFrom and renderTo <= @lastRenderedScreenRow
return
@gutter.updateLineNumbers(@pendingChanges, renderFrom, renderTo)
intactRanges = @computeIntactRanges()
@pendingChanges = []
@truncateIntactRanges(intactRanges, renderFrom, renderTo)
changes = @pendingChanges
intactRanges = @computeIntactRanges(renderFrom, renderTo)
@gutter.updateLineNumbers(changes, renderFrom, renderTo)
@clearDirtyRanges(intactRanges)
@fillDirtyRanges(intactRanges, renderFrom, renderTo)
@firstRenderedScreenRow = renderFrom
@ -1268,7 +1301,7 @@ class Editor extends View
emptyLineChanges
computeIntactRanges: ->
computeIntactRanges: (renderFrom, renderTo) ->
return [] if !@firstRenderedScreenRow? and !@lastRenderedScreenRow?
intactRanges = [{start: @firstRenderedScreenRow, end: @lastRenderedScreenRow, domStart: 0}]
@ -1303,6 +1336,9 @@ class Editor extends View
domStart: range.domStart + change.end + 1 - range.start
)
intactRanges = newIntactRanges
@truncateIntactRanges(intactRanges, renderFrom, renderTo)
@pendingChanges = []
intactRanges
@ -1322,35 +1358,35 @@ class Editor extends View
intactRanges.sort (a, b) -> a.domStart - b.domStart
clearDirtyRanges: (intactRanges) ->
renderedLines = @renderedLines[0]
killLine = (line) ->
next = line.nextSibling
renderedLines.removeChild(line)
next
if intactRanges.length == 0
@renderedLines.empty()
else if currentLine = renderedLines.firstChild
@renderedLines[0].innerHTML = ''
else if currentLine = @renderedLines[0].firstChild
domPosition = 0
for intactRange in intactRanges
while intactRange.domStart > domPosition
currentLine = killLine(currentLine)
currentLine = @clearLine(currentLine)
domPosition++
for i in [intactRange.start..intactRange.end]
currentLine = currentLine.nextSibling
domPosition++
while currentLine
currentLine = killLine(currentLine)
currentLine = @clearLine(currentLine)
clearLine: (lineElement) ->
next = lineElement.nextSibling
@renderedLines[0].removeChild(lineElement)
next
fillDirtyRanges: (intactRanges, renderFrom, renderTo) ->
renderedLines = @renderedLines[0]
nextIntact = intactRanges.shift()
currentLine = renderedLines.firstChild
i = 0
nextIntact = intactRanges[i]
currentLine = @renderedLines[0].firstChild
row = renderFrom
while row <= renderTo
if row == nextIntact?.end + 1
nextIntact = intactRanges.shift()
nextIntact = intactRanges[++i]
if !nextIntact or row < nextIntact.start
if nextIntact
dirtyRangeEnd = nextIntact.start - 1
@ -1358,7 +1394,7 @@ class Editor extends View
dirtyRangeEnd = renderTo
for lineElement in @buildLineElementsForScreenRows(row, dirtyRangeEnd)
renderedLines.insertBefore(lineElement, currentLine)
@renderedLines[0].insertBefore(lineElement, currentLine)
row++
else
currentLine = currentLine.nextSibling
@ -1379,14 +1415,18 @@ class Editor extends View
#
# Returns a {Number}.
getFirstVisibleScreenRow: ->
Math.floor(@scrollTop() / @lineHeight)
screenRow = Math.floor(@scrollTop() / @lineHeight)
screenRow = 0 if isNaN(screenRow)
screenRow
# Retrieves the number of the row that is visible and currently at the top of the editor.
# Retrieves the number of the row that is visible and currently at the bottom of the editor.
#
# Returns a {Number}.
getLastVisibleScreenRow: ->
calculatedRow = Math.ceil((@scrollTop() + @scrollView.height()) / @lineHeight) - 1
Math.max(0, Math.min(@getScreenLineCount() - 1, calculatedRow))
screenRow = Math.max(0, Math.min(@getScreenLineCount() - 1, calculatedRow))
screenRow = 0 if isNaN(screenRow)
screenRow
# Given a row number, identifies if it is currently visible.
#
@ -1411,11 +1451,11 @@ class Editor extends View
new Array(div.children...)
htmlForScreenRows: (startRow, endRow) ->
htmlLines = []
htmlLines = ''
screenRow = startRow
for line in @activeEditSession.linesForScreenRows(startRow, endRow)
htmlLines.push(@htmlForScreenLine(line, screenRow++))
htmlLines.join('\n\n')
htmlLines += @htmlForScreenLine(line, screenRow++)
htmlLines
htmlForScreenLine: (screenLine, screenRow) ->
{ tokens, text, lineEnding, fold, isSoftWrapped } = screenLine
@ -1506,28 +1546,92 @@ class Editor extends View
unless existingLineElement
lineElement = @buildLineElementForScreenRow(actualRow)
@renderedLines.append(lineElement)
left = @positionLeftForLineAndColumn(lineElement, column)
left = @positionLeftForLineAndColumn(lineElement, actualRow, column)
unless existingLineElement
@renderedLines[0].removeChild(lineElement)
{ top: row * @lineHeight, left }
positionLeftForLineAndColumn: (lineElement, column) ->
return 0 if column is 0
delta = 0
iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, acceptNode: -> NodeFilter.FILTER_ACCEPT)
while textNode = iterator.nextNode()
nextDelta = delta + textNode.textContent.length
if nextDelta >= column
offset = column - delta
break
delta = nextDelta
positionLeftForLineAndColumn: (lineElement, screenRow, column) ->
return 0 if column == 0
range = document.createRange()
range.setEnd(textNode, offset)
range.collapse()
leftPixels = range.getClientRects()[0].left - Math.floor(@scrollView.offset().left) + Math.floor(@scrollLeft())
range.detach()
leftPixels
bufferRow = @bufferRowsForScreenRows(screenRow, screenRow)[0] ? screenRow
tokenizedLine = @activeEditSession.displayBuffer.tokenizedBuffer.tokenizedLines[bufferRow]
left = 0
index = 0
for token in tokenizedLine.tokens
for char in token.value
return left if index >= column
val = @getCharacterWidthCache(token.scopes, char)
if val?
left += val
else
return @measureToColumn(lineElement, tokenizedLine, column)
index++
left
scopesForColumn: (tokenizedLine, column) ->
index = 0
for token in tokenizedLine.tokens
for char in token.value
return token.scopes if index == column
index++
null
measureToColumn: (lineElement, tokenizedLine, column) ->
left = oldLeft = index = 0
iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, TextNodeFilter)
returnLeft = null
while textNode = iterator.nextNode()
content = textNode.textContent
for char, i in content
# Dont return right away, finish caching the whole line
returnLeft = left if index == column
oldLeft = left
scopes = @scopesForColumn(tokenizedLine, index)
cachedVal = @getCharacterWidthCache(scopes, char)
if cachedVal?
left = oldLeft + cachedVal
else
# i + 1 to measure to the end of the current character
MeasureRange.setEnd(textNode, i + 1)
MeasureRange.collapse()
rects = MeasureRange.getClientRects()
return 0 if rects.length == 0
left = rects[0].left - Math.floor(@scrollView.offset().left) + Math.floor(@scrollLeft())
@setCharacterWidthCache(scopes, char, left - oldLeft) if scopes?
index++
returnLeft ? left
getCharacterWidthCache: (scopes, char) ->
scopes ?= NoScope
obj = Editor.characterWidthCache
for scope in scopes
obj = obj[scope]
return null unless obj?
obj[char]
setCharacterWidthCache: (scopes, char, val) ->
scopes ?= NoScope
obj = Editor.characterWidthCache
for scope in scopes
obj[scope] ?= {}
obj = obj[scope]
obj[char] = val
clearCharacterWidthCache: ->
Editor.characterWidthCache = {}
pixelOffsetForScreenPosition: (position) ->
{top, left} = @pixelPositionForScreenPosition(position)
@ -1601,30 +1705,9 @@ class Editor extends View
scopeStack = []
line = []
updateScopeStack = (desiredScopes) ->
excessScopes = scopeStack.length - desiredScopes.length
_.times(excessScopes, popScope) if excessScopes > 0
# pop until common prefix
for i in [scopeStack.length..0]
break if _.isEqual(scopeStack[0...i], desiredScopes[0...i])
popScope()
# push on top of common prefix until scopeStack == desiredScopes
for j in [i...desiredScopes.length]
pushScope(desiredScopes[j])
pushScope = (scope) ->
scopeStack.push(scope)
line.push("<span class=\"#{scope.replace(/\./g, ' ')}\">")
popScope = ->
scopeStack.pop()
line.push("</span>")
attributePairs = []
attributePairs.push "#{attributeName}=\"#{value}\"" for attributeName, value of attributes
line.push("<div #{attributePairs.join(' ')}>")
attributePairs = ''
attributePairs += " #{attributeName}=\"#{value}\"" for attributeName, value of attributes
line.push("<div #{attributePairs}>")
if text == ''
html = Editor.buildEmptyLineHtml(showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, activeEditSession, mini)
@ -1635,39 +1718,64 @@ class Editor extends View
lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0
position = 0
for token in tokens
updateScopeStack(token.scopes)
@updateScopeStack(line, scopeStack, token.scopes)
hasLeadingWhitespace = position < firstNonWhitespacePosition
hasTrailingWhitespace = position + token.value.length > firstTrailingWhitespacePosition
hasIndentGuide = not mini and showIndentGuide and (hasLeadingWhitespace or lineIsWhitespaceOnly)
line.push(token.getValueAsHtml({invisibles, hasLeadingWhitespace, hasTrailingWhitespace, hasIndentGuide}))
position += token.value.length
popScope() while scopeStack.length > 0
@popScope(line, scopeStack) while scopeStack.length > 0
line.push(htmlEolInvisibles) unless text == ''
line.push("<span class='fold-marker'/>") if fold
line.push('</div>')
line.join('')
@updateScopeStack: (line, scopeStack, desiredScopes) ->
excessScopes = scopeStack.length - desiredScopes.length
if excessScopes > 0
@popScope(line, scopeStack) while excessScopes--
# pop until common prefix
for i in [scopeStack.length..0]
break if _.isEqual(scopeStack[0...i], desiredScopes[0...i])
@popScope(line, scopeStack)
# push on top of common prefix until scopeStack == desiredScopes
for j in [i...desiredScopes.length]
@pushScope(line, scopeStack, desiredScopes[j])
null
@pushScope: (line, scopeStack, scope) ->
scopeStack.push(scope)
line.push("<span class=\"#{scope.replace(/\./g, ' ')}\">")
@popScope: (line, scopeStack) ->
scopeStack.pop()
line.push("</span>")
@buildEmptyLineHtml: (showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, activeEditSession, mini) ->
indentCharIndex = 0
if not mini and showIndentGuide
if indentation > 0
tabLength = activeEditSession.getTabLength()
indentGuideHtml = []
indentGuideHtml = ''
for level in [0...indentation]
indentLevelHtml = ["<span class='indent-guide'>"]
indentLevelHtml = "<span class='indent-guide'>"
for characterPosition in [0...tabLength]
if invisible = eolInvisibles.shift()
indentLevelHtml.push("<span class='invisible-character'>#{invisible}</span>")
if invisible = eolInvisibles[indentCharIndex++]
indentLevelHtml += "<span class='invisible-character'>#{invisible}</span>"
else
indentLevelHtml.push(' ')
indentLevelHtml.push("</span>")
indentGuideHtml.push(indentLevelHtml.join(''))
indentLevelHtml += ' '
indentLevelHtml += "</span>"
indentGuideHtml += indentLevelHtml
for invisible in eolInvisibles
indentGuideHtml.push("<span class='invisible-character'>#{invisible}</span>")
indentGuideHtml += "<span class='invisible-character'>#{invisible}</span>"
return indentGuideHtml.join('')
return indentGuideHtml
if htmlEolInvisibles.length > 0
htmlEolInvisibles

View File

@ -1,5 +1,5 @@
Q = require 'q'
EventEmitter = require './event-emitter'
fs = require 'fs'
path = require 'path'
fsUtils = require './fs-utils'
pathWatcher = require 'pathwatcher'
@ -23,9 +23,7 @@ class File
# * symlink:
# A Boolean indicating if the path is a symlink (default: false)
constructor: (@path, @symlink=false) ->
try
if fs.statSync(@path).isDirectorySync()
throw new Error("#{@path} is a directory")
throw new Error("#{@path} is a directory") if fsUtils.isDirectorySync(@path)
# Private: Sets the path for the file.
setPath: (@path) ->
@ -44,14 +42,8 @@ class File
fsUtils.writeSync(@getPath(), text)
@subscribeToNativeChangeEvents() if not previouslyExisted and @subscriptionCount() > 0
# Public: Reads the contents of the file.
#
# * flushCache:
# A Boolean indicating whether to require a direct read or if a cached
# copy is acceptable.
#
# Returns a String.
read: (flushCache)->
# Private: Deprecated
read: (flushCache) ->
if not @exists()
@cachedContents = null
else if not @cachedContents? or flushCache
@ -59,6 +51,39 @@ class File
else
@cachedContents
# Public: Reads the contents of the file.
#
# * 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.
readAsync: (flushCache) ->
if not @exists()
promise = Q(null)
else if not @cachedContents? or flushCache
deferred = Q.defer()
promise = deferred.promise
content = []
bytesRead = 0
readStream = fsUtils.createReadStream @getPath(), encoding: 'utf8'
readStream.on 'data', (chunk) ->
content.push(chunk)
bytesRead += chunk.length
deferred.notify(bytesRead)
readStream.on 'end', ->
deferred.resolve(content.join())
readStream.on 'error', (error) ->
deferred.reject(error)
else
promise = Q(@cachedContents)
promise.then (contents) ->
@cachedContents = contents
# Public: Returns whether a file exists.
exists: ->
fsUtils.exists(@getPath())

View File

@ -25,14 +25,14 @@ fsExtensions =
# Returns true if a file or folder at the specified path exists.
exists: (pathToCheck) ->
pathToCheck? and fs.existsSync(pathToCheck)
pathToCheck? and fs.statSyncNoException(pathToCheck) isnt false
# Returns true if the specified path is a directory that exists.
isDirectorySync: (directoryPath) ->
return false unless directoryPath?.length > 0
try
fs.statSync(directoryPath).isDirectory()
catch e
if stat = fs.statSyncNoException(directoryPath)
stat.isDirectory()
else
false
isDirectory: (directoryPath, done) ->
@ -50,17 +50,17 @@ fsExtensions =
# Returns true if the specified path is a regular file that exists.
isFileSync: (filePath) ->
return false unless filePath?.length > 0
try
fs.statSync(filePath).isFile()
catch e
if stat = fs.statSyncNoException(filePath)
stat.isFile()
else
false
# Returns true if the specified path is executable.
isExecutableSync: (pathToCheck) ->
return false unless pathToCheck?.length > 0
try
(fs.statSync(pathToCheck).mode & 0o777 & 1) isnt 0
catch e
if stat = fs.statSyncNoException(pathToCheck)
(stat.mode & 0o777 & 1) isnt 0
else
false
# Returns an array with the paths of the files and folders
@ -156,8 +156,8 @@ fsExtensions =
childPath = path.join(directoryPath, file)
stats = fs.lstatSync(childPath)
if stats.isSymbolicLink()
try
stats = fs.statSync(childPath)
if linkStats = fs.statSyncNoException(childPath)
stats = linkStats
if stats.isDirectory()
traverse(childPath, onFile, onDirectory) if onDirectory(childPath)
else if stats.isFile()

View File

@ -44,6 +44,7 @@ class Git
path: null
statuses: null
upstream: null
branch: null
statusTask: null
# Private: Creates a new `Git` object.
@ -142,6 +143,12 @@ class Git
# Public: Determine if the given path is new.
isPathNew: (path) -> @isStatusNew(@getPathStatus(path))
# Public: Is the project at the root of this repository?
#
# Returns true if at the root, false if in a subfolder of the repository.
isProjectAtRoot: ->
@projectAtRoot ?= project.relativize(@getWorkingDirectory()) is ''
# Public: Makes a path relative to the repository's working directory.
relativize: (path) -> @getRepo().relativize(path)
@ -171,6 +178,15 @@ class Git
@getPathStatus(path) if headCheckedOut
headCheckedOut
# 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.
#
# Returns a {Boolean} that's `true` if the method was successful.
checkoutReference: (reference, create) ->
@getRepo().checkoutReference(reference, create)
# Public: Retrieves the number of lines added and removed to a path.
#
# This compares the working directory contents of the path to the `HEAD`
@ -239,6 +255,12 @@ class Git
# Public: ?
getReferenceTarget: (reference) -> @getRepo().getReferenceTarget(reference)
# 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.
getReferences: -> @getRepo().getReferences()
# Public: ?
getAheadBehindCount: (reference) -> @getRepo().getAheadBehindCount(reference)
@ -247,8 +269,9 @@ class Git
# Private:
refreshStatus: ->
@statusTask = Task.once require.resolve('./repository-status-handler'), @getPath(), ({statuses, upstream}) =>
statusesUnchanged = _.isEqual(statuses, @statuses) and _.isEqual(upstream, @upstream)
@statusTask = Task.once require.resolve('./repository-status-handler'), @getPath(), ({statuses, upstream, branch}) =>
statusesUnchanged = _.isEqual(statuses, @statuses) and _.isEqual(upstream, @upstream) and _.isEqual(branch, @branch)
@statuses = statuses
@upstream = upstream
@branch = branch
@trigger 'statuses-changed' unless statusesUnchanged

View File

@ -15,8 +15,11 @@ class Gutter extends View
@div class: 'gutter', =>
@div outlet: 'lineNumbers', class: 'line-numbers'
firstScreenRow: Infinity
lastScreenRow: -1
firstScreenRow: null
lastScreenRow: null
initialize: ->
@elementBuilder = document.createElement('div')
afterAttach: (onDom) ->
return if @attached or not onDom
@ -62,46 +65,157 @@ class Gutter extends View
setShowLineNumbers: (showLineNumbers) ->
if showLineNumbers then @lineNumbers.show() else @lineNumbers.hide()
# Get all the line-number divs.
#
# Returns a list of {HTMLElement}s.
getLineNumberElements: ->
@lineNumbers[0].children
# Get all the line-number divs.
#
# Returns a list of {HTMLElement}s.
getLineNumberElementsForClass: (klass) ->
@lineNumbers[0].getElementsByClassName(klass)
# Get a single line-number div.
#
# * bufferRow: 0 based line number
#
# Returns a list of {HTMLElement}s that correspond to the bufferRow. More than
# one in the list indicates a wrapped line.
getLineNumberElement: (bufferRow) ->
@getLineNumberElementsForClass("line-number-#{bufferRow}")
# Add a class to all line-number divs.
#
# * klass: string class name
#
# Returns true if the class was added to any lines
addClassToAllLines: (klass)->
elements = @getLineNumberElements()
el.classList.add(klass) for el in elements
!!elements.length
# Remove a class from all line-number divs.
#
# * klass: string class name. Can only be one class name. i.e. 'my-class'
#
# Returns true if the class was removed from any lines
removeClassFromAllLines: (klass)->
# This is faster than calling $.removeClass on all lines, and faster than
# making a new array and iterating through it.
elements = @getLineNumberElementsForClass(klass)
willRemoveClasses = !!elements.length
elements[0].classList.remove(klass) while elements.length > 0
willRemoveClasses
# Add a class to a single line-number div
#
# * bufferRow: 0 based line number
# * klass: string class name
#
# Returns true if there were lines the class was added to
addClassToLine: (bufferRow, klass)->
elements = @getLineNumberElement(bufferRow)
el.classList.add(klass) for el in elements
!!elements.length
# Remove a class from a single line-number div
#
# * bufferRow: 0 based line number
# * klass: string class name
#
# Returns true if there were lines the class was removed from
removeClassFromLine: (bufferRow, klass)->
classesRemoved = false
elements = @getLineNumberElement(bufferRow)
for el in elements
hasClass = el.classList.contains(klass)
classesRemoved |= hasClass
el.classList.remove(klass) if hasClass
classesRemoved
### Internal ###
updateLineNumbers: (changes, renderFrom, renderTo) ->
if renderFrom < @firstScreenRow or renderTo > @lastScreenRow
performUpdate = true
else if @getEditor().getLastScreenRow() < @lastScreenRow
performUpdate = true
updateLineNumbers: (changes, startScreenRow, endScreenRow) ->
# Check if we have something already rendered that overlaps the requested range
updateAllLines = not (startScreenRow? and endScreenRow?)
updateAllLines |= endScreenRow <= @firstScreenRow or startScreenRow >= @lastScreenRow
for change in changes
# When there is a change to the bufferRow -> screenRow map (i.e. a fold),
# then rerender everything.
if (change.screenDelta or change.bufferDelta) and change.screenDelta != change.bufferDelta
updateAllLines = true
break
if updateAllLines
@lineNumbers[0].innerHTML = @buildLineElementsHtml(startScreenRow, endScreenRow)
else
for change in changes
if change.screenDelta or change.bufferDelta
performUpdate = true
break
# When scrolling or adding/removing lines, we just add/remove lines from the ends.
if startScreenRow < @firstScreenRow
@prependLineElements(@buildLineElements(startScreenRow, @firstScreenRow-1))
else if startScreenRow != @firstScreenRow
@removeLineElements(startScreenRow - @firstScreenRow)
@renderLineNumbers(renderFrom, renderTo) if performUpdate
renderLineNumbers: (startScreenRow, endScreenRow) ->
editor = @getEditor()
maxDigits = editor.getLineCount().toString().length
rows = editor.bufferRowsForScreenRows(startScreenRow, endScreenRow)
cursorScreenRow = editor.getCursorScreenPosition().row
@lineNumbers[0].innerHTML = $$$ ->
for row in rows
if row == lastScreenRow
rowValue = ''
else
rowValue = (row + 1).toString()
classes = ['line-number']
classes.push('fold') if editor.isFoldedAtBufferRow(row)
@div linenumber: row, class: classes.join(' '), =>
rowValuePadding = _.multiplyString('&nbsp;', maxDigits - rowValue.length)
@raw("#{rowValuePadding}#{rowValue}")
lastScreenRow = row
if endScreenRow > @lastScreenRow
@appendLineElements(@buildLineElements(@lastScreenRow+1, endScreenRow))
else if endScreenRow != @lastScreenRow
@removeLineElements(endScreenRow - @lastScreenRow)
@firstScreenRow = startScreenRow
@lastScreenRow = endScreenRow
@highlightedRows = null
@highlightLines()
prependLineElements: (lineElements) ->
anchor = @lineNumbers[0].children[0]
return appendLineElements(lineElements) unless anchor?
@lineNumbers[0].insertBefore(lineElements[0], anchor) while lineElements.length > 0
null # defeat coffeescript array return
appendLineElements: (lineElements) ->
@lineNumbers[0].appendChild(lineElements[0]) while lineElements.length > 0
null # defeat coffeescript array return
removeLineElements: (numberOfElements) ->
children = @getLineNumberElements()
# children is a live NodeList, so remove from the desired end {numberOfElements} times
if numberOfElements < 0
@lineNumbers[0].removeChild(children[children.length-1]) while numberOfElements++
else if numberOfElements > 0
@lineNumbers[0].removeChild(children[0]) while numberOfElements--
null # defeat coffeescript array return
buildLineElements: (startScreenRow, endScreenRow) ->
@elementBuilder.innerHTML = @buildLineElementsHtml(startScreenRow, endScreenRow)
@elementBuilder.children
buildLineElementsHtml: (startScreenRow, endScreenRow) =>
editor = @getEditor()
maxDigits = editor.getLineCount().toString().length
rows = editor.bufferRowsForScreenRows(startScreenRow, endScreenRow)
html = ''
for row in rows
if row == lastScreenRow
rowValue = ''
else
rowValue = (row + 1).toString()
classes = "line-number line-number-#{row}"
classes += ' fold' if editor.isFoldedAtBufferRow(row)
rowValuePadding = _.multiplyString('&nbsp;', maxDigits - rowValue.length)
html += """<div class="#{classes}">#{rowValuePadding}#{rowValue}</div>"""
lastScreenRow = row
html
removeLineHighlights: ->
return unless @highlightedLineNumbers
for line in @highlightedLineNumbers

View File

@ -35,7 +35,19 @@ $.fn.isOnDom = ->
@closest(document.body).length is 1
$.fn.isVisible = ->
@is(':visible')
!@isHidden()
$.fn.isHidden = ->
# We used to check @is(':hidden'). But this is much faster than the
# offsetWidth/offsetHeight check + all the pseudo selector mess in jquery.
style = this[0].style
if style.display == 'none' or not @isOnDom()
true
else if style.display
false
else
getComputedStyle(this[0]).display == 'none'
$.fn.isDisabled = ->
!!@attr('disabled')

View File

@ -4,6 +4,7 @@ fsUtils = require './fs-utils'
path = require 'path'
CSON = require 'season'
BindingSet = require './binding-set'
EventEmitter = require './event-emitter'
# Internal: Associates keymaps with actions.
#
@ -20,6 +21,8 @@ BindingSet = require './binding-set'
# key, you define one or more key:value pairs, associating keystrokes with a command to execute.
module.exports =
class Keymap
_.extend @prototype, EventEmitter
bindingSets: null
nextBindingSetIndex: 0
bindingSetsByFirstKeystroke: null
@ -31,6 +34,7 @@ class Keymap
loadBundledKeymaps: ->
@loadDirectory(config.bundledKeymapsDirPath)
@trigger('bundled-keymaps-loaded')
loadUserKeymaps: ->
@loadDirectory(path.join(config.configDirPath, 'keymaps'))
@ -54,6 +58,42 @@ class Keymap
keystroke = keystrokes.split(' ')[0]
_.remove(@bindingSetsByFirstKeystroke[keystroke], bindingSet)
# Public: Returns an array of objects that represent every keystroke to
# command mapping. Each object contains the following keys `source`,
# `selector`, `command`, `keystrokes`.
getAllKeyMappings: ->
mappings = []
for bindingSet in @bindingSets
selector = bindingSet.getSelector()
source = @determineSource(bindingSet.getName())
for keystrokes, command of bindingSet.getCommandsByKeystrokes()
mappings.push {keystrokes, command, selector, source}
mappings
# Private: Returns a user friendly description of where a keybinding was
# loaded from.
#
# * filePath:
# The absolute path from which the keymap was loaded
#
# Returns one of:
# * `Core` indicates it comes from a bundled package.
# * `User` indicates that it was defined by a user.
# * `<package-name>` the package which defined it.
# * `Unknown` if an invalid path was passed in.
determineSource: (filePath) ->
return 'Unknown' unless filePath
pathParts = filePath.split(path.sep)
if _.contains(pathParts, 'node_modules') or _.contains(pathParts, 'atom') or _.contains(pathParts, 'src')
'Core'
else if _.contains(pathParts, '.atom') and _.contains(pathParts, 'keymaps') and !_.contains(pathParts, 'packages')
'User'
else
packageNameIndex = pathParts.length - 3
pathParts[packageNameIndex]
bindKeys: (args...) ->
name = args.shift() if args.length > 2
[selector, bindings] = args

52
src/menu-manager.coffee Normal file
View File

@ -0,0 +1,52 @@
path = require 'path'
_ = require 'underscore'
ipc = require 'ipc'
CSON = require 'season'
fsUtils = require './fs-utils'
# Public: Provides a registry for menu items that you'd like to appear in the
# application menu.
#
# Should be accessed via `atom.menu`.
module.exports =
class MenuManager
# Private:
constructor: ->
@template = []
atom.keymap.on 'bundled-keymaps-loaded', => @loadCoreItems()
# 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
#
# Returns nothing.
add: (items) ->
@merge(@template, item) for item in items
@update()
# Public: Refreshes the currently visible menu.
update: ->
@sendToBrowserProcess(@template, atom.keymap.keystrokesByCommandForSelector('body'))
# Private
loadCoreItems: ->
menuPaths = fsUtils.listSync(atom.config.bundledMenusDirPath, ['cson', 'json'])
for menuPath in menuPaths
data = CSON.readFileSync(menuPath)
@add(data.menu)
# Private: 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) ->
if item.submenu? and match = _.find(menu, (o) -> o.submenu? and o.label == item.label)
@merge(match.submenu, i) for i in item.submenu
else
menu.push(item)
# Private
sendToBrowserProcess: (template, keystrokesByCommand) ->
ipc.sendChannel 'update-application-menu', template, keystrokesByCommand

134
src/package-manager.coffee Normal file
View File

@ -0,0 +1,134 @@
EventEmitter = require './event-emitter'
fsUtils = require './fs-utils'
_ = require './underscore-extensions'
Package = require './package'
path = require 'path'
module.exports =
class PackageManager
_.extend @prototype, EventEmitter
constructor: ({configDirPath, devMode, @resourcePath}) ->
@packageDirPaths = [path.join(configDirPath, "packages")]
if devMode
@packageDirPaths.unshift(path.join(configDirPath, "dev", "packages"))
@loadedPackages = {}
@activePackages = {}
@packageStates = {}
getPackageState: (name) ->
@packageStates[name]
setPackageState: (name, state) ->
@packageStates[name] = state
activatePackages: ->
@activatePackage(pack.name) for pack in @getLoadedPackages()
activatePackage: (name, options) ->
if pack = @loadPackage(name, options)
@activePackages[pack.name] = pack
pack.activate(options)
pack
deactivatePackages: ->
@deactivatePackage(pack.name) for pack in @getActivePackages()
deactivatePackage: (name) ->
if pack = @getActivePackage(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}'")
getActivePackage: (name) ->
@activePackages[name]
isPackageActive: (name) ->
@getActivePackage(name)?
getActivePackages: ->
_.values(@activePackages)
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'
@loadPackage(name) for name in @getAvailablePackageNames() when not @isPackageDisabled(name)
@trigger 'loaded'
loadPackage: (name, options) ->
if @isPackageDisabled(name)
return console.warn("Tried to load disabled package '#{name}'")
if packagePath = @resolvePackagePath(name)
return pack if pack = @getLoadedPackage(name)
pack = Package.load(packagePath, options)
if pack.metadata?.theme
atom.themes.register(pack)
else
@loadedPackages[pack.name] = pack
pack
else
throw new Error("Could not resolve '#{name}' to a package path")
unloadPackage: (name) ->
if @isPackageActive(name)
throw new Error("Tried to unload active package '#{name}'")
if pack = @getLoadedPackage(name)
delete @loadedPackages[pack.name]
else
throw new Error("No loaded package for name '#{name}'")
resolvePackagePath: (name) ->
return name if fsUtils.isDirectorySync(name)
packagePath = fsUtils.resolve(@packageDirPaths..., name)
return packagePath if fsUtils.isDirectorySync(packagePath)
packagePath = path.join(@resourcePath, 'node_modules', name)
return packagePath if @isInternalPackage(packagePath)
isInternalPackage: (packagePath) ->
{engines} = Package.loadMetadata(packagePath, true)
engines?.atom?
getLoadedPackage: (name) ->
@loadedPackages[name]
isPackageLoaded: (name) ->
@getLoadedPackage(name)?
getLoadedPackages: ->
_.values(@loadedPackages)
isPackageDisabled: (name) ->
_.include(config.get('core.disabledPackages') ? [], name)
getAvailablePackagePaths: ->
packagePaths = []
for packageDirPath in @packageDirPaths
for packagePath in fsUtils.listSync(packageDirPath)
packagePaths.push(packagePath) if fsUtils.isDirectorySync(packagePath)
for packagePath in fsUtils.listSync(path.join(@resourcePath, 'node_modules'))
packagePaths.push(packagePath) if @isInternalPackage(packagePath)
_.uniq(packagePaths)
getAvailablePackageNames: ->
_.uniq _.map @getAvailablePackagePaths(), (packagePath) -> path.basename(packagePath)
getAvailablePackageMetadata: ->
packages = []
for packagePath in @getAvailablePackagePaths()
name = path.basename(packagePath)
metadata = @getLoadedPackage(name)?.metadata ? Package.loadMetadata(packagePath, true)
packages.push(metadata)
packages

View File

@ -28,7 +28,6 @@ class Pane extends View
activeItem: null
items: null
viewsByClassName: null # Views with a setModel() method are stored here
viewsByItem: null # Views without a setModel() method are stored here
# Private:
@ -53,7 +52,6 @@ class Pane extends View
return if site is @state.site.id
@showItemForUri(newValue) if key is 'activeItemUri'
@viewsByClassName = {}
@viewsByItem = new WeakMap()
activeItemUri = @state.get('activeItemUri')
unless activeItemUri? and @showItemForUri(activeItemUri)
@ -224,12 +222,11 @@ class Pane extends View
# Public: Prompt the user to save the given item.
promptToSaveItem: (item) ->
uri = item.getUri()
currentWindow = require('remote').getCurrentWindow()
chosen = atom.confirmSync(
"'#{item.getTitle?() ? item.getUri()}' has changes, do you want to save them?"
"Your changes will be lost if you close this item without saving."
["Save", "Cancel", "Don't Save"]
currentWindow
atom.getCurrentWindow()
)
switch chosen
when 0 then @saveItem(item, -> true)
@ -323,24 +320,23 @@ class Pane extends View
cleanupItemView: (item) ->
if item instanceof $
viewToRemove = item
else
if viewToRemove = @viewsByItem.get(item)
@viewsByItem.delete(item)
else
viewClass = item.getViewClass()
otherItemsForView = @items.filter (i) -> i.getViewClass?() is viewClass
unless otherItemsForView.length
viewToRemove = @viewsByClassName[viewClass.name]
viewToRemove?.setModel(null)
delete @viewsByClassName[viewClass.name]
else if viewToRemove = @viewsByItem.get(item)
@viewsByItem.delete(item)
if @items.length > 0
if @isMovingItem and item is viewToRemove
viewToRemove?.detach()
else if @isMovingItem and viewToRemove?.setModel
viewToRemove.setModel(null) # dont want to destroy the model, so set to null
viewToRemove.remove()
else
viewToRemove?.remove()
else
viewToRemove?.detach() if @isMovingItem and item is viewToRemove
if @isMovingItem and item is viewToRemove
viewToRemove?.detach()
else if @isMovingItem and viewToRemove?.setModel
viewToRemove.setModel(null) # dont want to destroy the model, so set to null
@parent().view().removeChild(this, updateState: false)
# Private:
@ -351,14 +347,8 @@ class Pane extends View
view
else
viewClass = item.getViewClass()
if view = @viewsByClassName[viewClass.name]
view.setModel(item)
else
view = new viewClass(item)
if _.isFunction(view.setModel)
@viewsByClassName[viewClass.name] = view
else
@viewsByItem.set(item, view)
view = new viewClass(item)
@viewsByItem.set(item, view)
view
# Private:

View File

@ -1,6 +1,7 @@
fsUtils = require './fs-utils'
path = require 'path'
url = require 'url'
Q = require 'q'
_ = require './underscore-extensions'
$ = require './jquery-extensions'
@ -165,6 +166,8 @@ class Project
#
# Returns a String.
resolve: (uri) ->
return unless uri
if uri?.match(/[A-Za-z0-9+-.]+:\/\//) # leave path alone if it has a scheme
uri
else
@ -187,9 +190,21 @@ class Project
# * editSessionOptions:
# Options that you can pass to the {EditSession} constructor
#
# Returns an {EditSession}.
# Returns a promise that resolves to an {EditSession}.
openAsync: (filePath, options={}) ->
filePath = @resolve(filePath)
resource = null
_.find @openers, (opener) -> resource = opener(filePath, options)
if resource
Q(resource)
else
@bufferForPathAsync(filePath).then (buffer) =>
editSession = @buildEditSessionForBuffer(buffer, options)
# Private: Only be used in specs
open: (filePath, options={}) ->
filePath = @resolve(filePath) if filePath?
filePath = @resolve(filePath)
for opener in @openers
return resource if resource = opener(filePath, options)
@ -217,6 +232,15 @@ class Project
getBuffers: ->
new Array(@buffers...)
# Private: Only to be used in specs
bufferForPath: (filePath, text) ->
absoluteFilePath = @resolve(filePath)
if filePath
existingBuffer = _.find @buffers, (buffer) -> buffer.getPath() == absoluteFilePath
existingBuffer ? @buildBuffer(absoluteFilePath, text)
# Private: Given a file path, this retrieves or creates a new {TextBuffer}.
#
# If the `filePath` already has a `buffer`, that value is used instead. Otherwise,
@ -225,32 +249,37 @@ class Project
# filePath - A {String} representing a path. If `null`, an "Untitled" buffer is created.
# text - The {String} text to use as a buffer, if the file doesn't have any contents
#
# Returns the {TextBuffer}.
bufferForPath: (filePath, text) ->
if filePath?
filePath = @resolve(filePath)
if filePath
buffer = _.find @buffers, (buffer) -> buffer.getPath() == filePath
buffer or @buildBuffer(filePath, text)
else
@buildBuffer(null, text)
# Returns a promise that resolves to the {TextBuffer}.
bufferForPathAsync: (filePath, text) ->
absoluteFilePath = @resolve(filePath)
if absoluteFilePath
existingBuffer = _.find @buffers, (buffer) -> buffer.getPath() == absoluteFilePath
Q(existingBuffer ? @buildBufferAsync(absoluteFilePath, text))
# Private:
bufferForId: (id) ->
_.find @buffers, (buffer) -> buffer.id is id
# Private: Given a file path, this sets its {TextBuffer}.
#
# filePath - A {String} representing a path
# text - The {String} text to use as a buffer
#
# Returns the {TextBuffer}.
buildBuffer: (filePath, initialText) ->
filePath = @resolve(filePath) if filePath?
buffer = new TextBuffer({project: this, filePath, initialText})
# Private: DEPRECATED
buildBuffer: (absoluteFilePath, initialText) ->
buffer = new TextBuffer({project: this, filePath: absoluteFilePath, initialText})
buffer.load()
@addBuffer(buffer)
buffer
# Private: Given a file path, this sets its {TextBuffer}.
#
# absoluteFilePath - A {String} representing a path
# text - The {String} text to use as a buffer
#
# Returns a promise that resolves to the {TextBuffer}.
buildBufferAsync: (absoluteFilePath, initialText) ->
buffer = new TextBuffer({project: this, filePath: absoluteFilePath, initialText})
buffer.loadAsync().then (buffer) =>
@addBuffer(buffer)
buffer
# Private:
addBuffer: (buffer, options={}) ->
@addBufferAtIndex(buffer, @buffers.length, options)
@ -302,6 +331,10 @@ class Project
task.on 'scan:result-found', (result) =>
iterator(result)
if _.isFunction(options.onPathsSearched)
task.on 'scan:paths-searched', (numberOfPathsSearched) ->
options.onPathsSearched(numberOfPathsSearched)
deferred
# Private:

View File

@ -9,9 +9,11 @@ module.exports = (repoPath) ->
for filePath, status of repo.getStatus()
statuses[path.join(workingDirectoryPath, filePath)] = status
upstream = repo.getAheadBehindCount()
branch = repo.getHead()
repo.release()
else
upstream = {}
statuses = {}
branch = null
{statuses, upstream}
{statuses, upstream, branch}

View File

@ -1,5 +1,7 @@
fs = require 'fs'
ipc = require 'ipc'
path = require 'path'
Q = require 'q'
$ = require './jquery-extensions'
{$$, View} = require './space-pen-extensions'
fsUtils = require './fs-utils'
@ -65,6 +67,8 @@ class RootView extends View
# Private:
initialize: (state={}) ->
@prepend($$ -> @div class: 'dev-mode') if atom.getLoadSettings().devMode
if state instanceof telepath.Document
@state = state
panes = deserialize(state.get('panes'))
@ -77,15 +81,14 @@ class RootView extends View
@panes.replaceWith(panes)
@panes = panes
@updateTitle()
@on 'focus', (e) => @handleFocus(e)
@subscribe $(window), 'focus', (e) =>
@handleFocus(e) if document.activeElement is document.body
project.on 'path-changed', => @updateTitle()
@on 'pane:became-active', => @updateTitle()
@on 'pane:active-item-changed', '.active.pane', => @updateTitle()
@on 'pane:removed', => @updateTitle() unless @getActivePane()
@on 'pane-container:active-pane-item-changed', => @updateTitle()
@on 'pane:active-item-title-changed', '.active.pane', => @updateTitle()
@command 'application:about', -> ipc.sendChannel('command', 'application:about')
@ -126,7 +129,8 @@ class RootView extends View
@command 'pane:reopen-closed-item', =>
@panes.reopenItem()
_.nextTick => atom.setFullScreen(@state.get('fullScreen'))
if @state.get('fullScreen')
_.nextTick => atom.setFullScreen(true)
# Private:
serialize: ->
@ -160,26 +164,47 @@ class RootView extends View
confirmClose: ->
@panes.confirmClose()
# Public: Opens a given a filepath in Atom.
# Public: Asynchronously opens a given a filepath in Atom.
#
# * path: A file path
# * filePath: A file path
# * options
# + initialLine:
# The buffer line number to open to.
# + initialLine: The buffer line number to open to.
#
# Returns the {EditSession} for the file URI.
open: (path, options = {}) ->
# Returns a promise that resolves to the {EditSession} for the file URI.
openAsync: (filePath, options={}) ->
filePath = project.resolve(filePath)
initialLine = options.initialLine
activePane = @getActivePane()
editSession = activePane.itemForUri(project.relativize(filePath)) if activePane and filePath
promise = project.openAsync(filePath, {initialLine}) if not editSession
fileSize = 0
fileSize = fs.statSync(filePath).size if fsUtils.exists(filePath)
Q(editSession ? promise)
.then (editSession) =>
if not activePane
activePane = new Pane(editSession)
@panes.setRoot(activePane)
activePane.showItem(editSession)
activePane.focus()
editSession
# Private: DEPRECATED Synchronously Opens a given a filepath in Atom.
open: (filePath, options = {}) ->
changeFocus = options.changeFocus ? true
initialLine = options.initialLine
path = project.relativize(path)
filePath = project.relativize(filePath)
if activePane = @getActivePane()
if path
editSession = activePane.itemForUri(path) ? project.open(path, {initialLine})
if filePath
editSession = activePane.itemForUri(filePath) ? project.open(filePath, {initialLine})
else
editSession = project.open()
activePane.showItem(editSession)
else
editSession = project.open(path, {initialLine})
editSession = project.open(filePath, {initialLine})
activePane = new Pane(editSession)
@panes.setRoot(activePane)
@ -192,7 +217,7 @@ class RootView extends View
if item = @getActivePaneItem()
@setTitle("#{item.getTitle?() ? 'untitled'} - #{projectPath}")
else
@setTitle("atom - #{projectPath}")
@setTitle(projectPath)
else
@setTitle('untitled')
@ -277,5 +302,5 @@ class RootView extends View
# Private: Destroys everything.
remove: ->
editor.remove() for editor in @getEditors()
project.destroy()
project?.destroy()
super

View File

@ -3,13 +3,23 @@
module.exports = (rootPath, regexSource, options) ->
callback = @async()
PATHS_COUNTER_SEARCHED_CHUNK = 50
pathsSearched = 0
searcher = new PathSearcher()
scanner = new PathScanner(rootPath, options)
searcher.on 'results-found', (result) ->
emit('scan:result-found', result)
scanner.on 'path-found', ->
pathsSearched++
if pathsSearched % PATHS_COUNTER_SEARCHED_CHUNK == 0
emit('scan:paths-searched', pathsSearched)
flags = "g"
flags += "i" if options.ignoreCase
regex = new RegExp(regexSource, flags)
search regex, scanner, searcher, callback
search regex, scanner, searcher, ->
emit('scan:paths-searched', pathsSearched)
callback()

View File

@ -115,6 +115,8 @@ class Selection
selectWord: ->
options = {}
options.wordRegex = /[\t ]*/ if @cursor.isSurroundedByWhitespace()
if @cursor.isBetweenWordAndNonWord()
options.includeNonWordCharacters = false
@setBufferRange(@cursor.getCurrentWordBufferRange(options))
@wordwise = true

View File

@ -44,15 +44,11 @@ class Task
# function to execute.
constructor: (taskPath) ->
coffeeScriptRequire = "require('#{require.resolve('coffee-script')}');"
coffeeCacheRequire = "require('#{require.resolve('./coffee-cache')}');"
coffeeCacheRequire = "require('#{require.resolve('./coffee-cache')}').register();"
taskBootstrapRequire = "require('#{require.resolve('./task-bootstrap')}');"
bootstrap = """
#{coffeeScriptRequire}
#{coffeeCacheRequire}
Object.defineProperty(require.extensions, '.coffee', {
writable: false,
value: require.extensions['.coffee']
});
#{taskBootstrapRequire}
"""

View File

@ -1,12 +1,15 @@
_ = require './underscore-extensions'
telepath = require 'telepath'
{Point, Range} = telepath
fsUtils = require './fs-utils'
File = require './file'
EventEmitter = require './event-emitter'
Subscriber = require './subscriber'
guid = require 'guid'
Q = require 'q'
{P} = require 'scandal'
telepath = require 'telepath'
_ = require './underscore-extensions'
fsUtils = require './fs-utils'
EventEmitter = require './event-emitter'
File = require './file'
Subscriber = require './subscriber'
{Point, Range} = telepath
# Private: Represents the contents of a file.
#
@ -22,7 +25,9 @@ class TextBuffer
registerDeserializer(this)
@deserialize: (state, params) ->
new this(state, params)
buffer = new this(state, params)
buffer.load()
buffer
stoppedChangingDelay: 300
stoppedChangingTimeout: null
@ -43,29 +48,35 @@ class TextBuffer
@id = @state.get('id')
filePath = @state.get('relativePath')
@text = @state.get('text')
reloadFromDisk = @state.get('isModified') is false
@loadFromDisk = @state.get('isModified') == false
else
{@project, filePath, initialText} = optionsOrState
@text = site.createDocument(initialText ? '', shareStrings: true)
reloadFromDisk = true
@id = guid.create().toString()
@state = site.createDocument
id: @id
deserializer: @constructor.name
version: @constructor.version
text: @text
@loadFromDisk = not initialText
@subscribe @text, 'changed', @handleTextChange
@subscribe @text, 'marker-created', (marker) => @trigger 'marker-created', marker
@subscribe @text, 'markers-updated', => @trigger 'markers-updated'
if filePath
@setPath(@project.resolve(filePath))
if fsUtils.exists(@getPath())
@updateCachedDiskContents()
@reload() if reloadFromDisk and @isModified()
@setPath(@project.resolve(filePath)) if @project
load: ->
@updateCachedDiskContents()
@reload() if @loadFromDisk and @isModified()
@text.clearUndoStack()
loadAsync: ->
@updateCachedDiskContentsAsync().then =>
@reload() if @loadFromDisk and @isModified()
@text.clearUndoStack()
this
### Internal ###
handleTextChange: (event) =>
@ -81,6 +92,7 @@ class TextBuffer
@unsubscribe()
@destroyed = true
@project?.removeBuffer(this)
@trigger 'destroyed'
isRetained: -> @refcount > 0
@ -104,16 +116,16 @@ class TextBuffer
subscribeToFile: ->
@file.on "contents-changed", =>
if @isModified()
@conflict = true
@updateCachedDiskContents()
@trigger "contents-conflicted"
else
@reload()
@conflict = true if @isModified()
@updateCachedDiskContentsAsync().done =>
if @conflict
@trigger "contents-conflicted"
else
@reload()
@file.on "removed", =>
@updateCachedDiskContents()
@triggerModifiedStatusChanged(@isModified())
@updateCachedDiskContentsAsync().done =>
@triggerModifiedStatusChanged(@isModified())
@file.on "moved", =>
@state.set('relativePath', @project.relativize(@getPath()))
@ -130,19 +142,21 @@ class TextBuffer
# Reloads a file in the {EditSession}.
#
# Essentially, this performs a force read of the file.
# Sets the buffer's content to the cached disk contents
reload: ->
@trigger 'will-reload'
@updateCachedDiskContents()
@setText(@cachedDiskContents)
@triggerModifiedStatusChanged(false)
@trigger 'reloaded'
# Rereads the contents of the file, and stores them in the cache.
#
# Essentially, this performs a force read of the file on disk.
# Private: Rereads the contents of the file, and stores them in the cache.
updateCachedDiskContents: ->
@cachedDiskContents = @file.read()
@cachedDiskContents = @file?.read() ? ""
# Private: Rereads the contents of the file, and stores them in the cache.
updateCachedDiskContentsAsync: ->
Q(@file?.readAsync() ? "").then (contents) =>
@cachedDiskContents = contents
# Gets the file's basename--that is, the file without any directory information.
#
@ -162,9 +176,6 @@ class TextBuffer
getRelativePath: ->
@state.get('relativePath')
setRelativePath: (relativePath) ->
@setPath(@project.resolve(relativePath))
# Sets the path for the file.
#
# path - A {String} representing the new file path
@ -172,9 +183,13 @@ class TextBuffer
return if path == @getPath()
@file?.off()
@file = new File(path)
@file.read() if @file.exists()
@subscribeToFile()
if path
@file = new File(path)
@subscribeToFile()
else
@file = null
@state.set('relativePath', @project.relativize(path))
@trigger "path-changed", this
@ -396,7 +411,10 @@ class TextBuffer
# Returns a {Boolean}.
isModified: ->
if @file
@getText() != @cachedDiskContents
if @file.exists()
@getText() != @cachedDiskContents
else
true
else
not @isEmpty()
@ -609,12 +627,6 @@ class TextBuffer
return unless path
@project.getRepo()?.checkoutHead(path)
# Checks to see if a file exists.
#
# Returns a {Boolean}.
fileExists: ->
@file? && @file.exists()
### Internal ###
transact: (fn) -> @text.transact fn

View File

@ -4,7 +4,7 @@ Package = require './package'
AtomPackage = require './atom-package'
_ = require './underscore-extensions'
$ = require './jquery-extensions'
fsUtils = require './fs-utils'
# Private: Handles discovering and loading available themes.
@ -15,6 +15,7 @@ class ThemeManager
constructor: ->
@loadedThemes = []
@activeThemes = []
@lessCache = null
# Internal-only:
register: (theme) ->
@ -33,9 +34,85 @@ class ThemeManager
getLoadedThemes: ->
_.clone(@loadedThemes)
# Internal-only:
loadBaseStylesheets: ->
@requireStylesheet('bootstrap/less/bootstrap')
@reloadBaseStylesheets()
# Internal-only:
reloadBaseStylesheets: ->
@requireStylesheet('../static/atom')
if nativeStylesheetPath = fsUtils.resolveOnLoadPath(process.platform, ['css', 'less'])
@requireStylesheet(nativeStylesheetPath)
# Internal-only:
stylesheetElementForId: (id) ->
$("""head style[id="#{id}"]""")
# Internal-only:
resolveStylesheet: (stylesheetPath) ->
if path.extname(stylesheetPath).length > 0
fsUtils.resolveOnLoadPath(stylesheetPath)
else
fsUtils.resolveOnLoadPath(stylesheetPath, ['css', 'less'])
# Public: resolves and applies the stylesheet specified by the path.
#
# * stylesheetPath: String. Can be an absolute path or the name of a CSS or
# LESS file in the stylesheets path.
#
# Returns the absolute path to the stylesheet
requireStylesheet: (stylesheetPath) ->
if fullPath = @resolveStylesheet(stylesheetPath)
content = @loadStylesheet(fullPath)
@applyStylesheet(fullPath, content)
else
throw new Error("Could not find a file at path '#{stylesheetPath}'")
fullPath
# Internal-only:
loadStylesheet: (stylesheetPath) ->
if path.extname(stylesheetPath) is '.less'
@loadLessStylesheet(stylesheetPath)
else
fsUtils.read(stylesheetPath)
# Internal-only:
loadLessStylesheet: (lessStylesheetPath) ->
unless lessCache?
LessCompileCache = require './less-compile-cache'
@lessCache = new LessCompileCache()
try
@lessCache.read(lessStylesheetPath)
catch e
console.error """
Error compiling less stylesheet: #{lessStylesheetPath}
Line number: #{e.line}
#{e.message}
"""
# Internal-only:
removeStylesheet: (stylesheetPath) ->
unless fullPath = @resolveStylesheet(stylesheetPath)
throw new Error("Could not find a file at path '#{stylesheetPath}'")
@stylesheetElementForId(fullPath).remove()
# Internal-only:
applyStylesheet: (id, text, ttype = 'bundled') ->
styleElement = @stylesheetElementForId(id)
if styleElement.length
styleElement.text(text)
else
if $("head style.#{ttype}").length
$("head style.#{ttype}:last").after "<style class='#{ttype}' id='#{id}'>#{text}</style>"
else
$("head").append "<style class='#{ttype}' id='#{id}'>#{text}</style>"
# Internal-only:
unload: ->
removeStylesheet(@userStylesheetPath) if @userStylesheetPath?
@removeStylesheet(@userStylesheetPath) if @userStylesheetPath?
theme.deactivate() while theme = @activeThemes.pop()
# Internal-only:
@ -50,7 +127,7 @@ class ThemeManager
@activateTheme(themeName) for themeName in themeNames
@loadUserStylesheet()
@reloadBaseStylesheets()
@trigger('reloaded')
# Private:
@ -107,8 +184,10 @@ class ThemeManager
if @activeThemes.length > 0
themePaths = (theme.getStylesheetsPath() for theme in @activeThemes when theme)
else
themeNames = config.get('core.themes')
themePaths = (path.join(@resolveThemePath(themeName), AtomPackage.stylesheetsDir) for themeName in themeNames)
themePaths = []
for themeName in config.get('core.themes') ? []
if themePath = @resolveThemePath(themeName)
themePaths.push(path.join(themePath, AtomPackage.stylesheetsDir))
themePath for themePath in themePaths when fsUtils.isDirectorySync(themePath)
@ -116,5 +195,5 @@ class ThemeManager
loadUserStylesheet: ->
if userStylesheetPath = @getUserStylesheetPath()
@userStylesheetPath = userStylesheetPath
userStylesheetContents = loadStylesheet(userStylesheetPath)
applyStylesheet(userStylesheetPath, userStylesheetContents, 'userTheme')
userStylesheetContents = @loadStylesheet(userStylesheetPath)
@applyStylesheet(userStylesheetPath, userStylesheetContents, 'userTheme')

View File

@ -1,7 +1,14 @@
_ = require './underscore-extensions'
textUtils = require './text-utils'
whitespaceRegexesByTabLength = {}
WhitespaceRegexesByTabLength = {}
LeadingWhitespaceRegex = /^[ ]+/
TrailingWhitespaceRegex = /[ ]+$/
EscapeRegex = /[&"'<>]/g
CharacterRegex = /./g
StartCharacterRegex = /^./
StartDotRegex = /^\.?/
WhitespaceRegex = /\S/
# Private: Represents a single unit of text as selected by a grammar.
module.exports =
@ -33,7 +40,7 @@ class Token
[new Token(value: value1, scopes: @scopes), new Token(value: value2, scopes: @scopes)]
whitespaceRegexForTabLength: (tabLength) ->
whitespaceRegexesByTabLength[tabLength] ?= new RegExp("([ ]{#{tabLength}})|(\t)|([^\t]+)", "g")
WhitespaceRegexesByTabLength[tabLength] ?= new RegExp("([ ]{#{tabLength}})|(\t)|([^\t]+)", "g")
breakOutAtomicTokens: (tabLength, breakOutLeadingWhitespace) ->
if @hasSurrogatePair
@ -112,10 +119,10 @@ class Token
)
isOnlyWhitespace: ->
not /\S/.test(@value)
not WhitespaceRegex.test(@value)
matchesScopeSelector: (selector) ->
targetClasses = selector.replace(/^\.?/, '').split('.')
targetClasses = selector.replace(StartDotRegex, '').split('.')
_.any @scopes, (scope) ->
scopeClasses = scope.split('.')
_.isSubset(targetClasses, scopeClasses)
@ -123,39 +130,59 @@ class Token
getValueAsHtml: ({invisibles, hasLeadingWhitespace, hasTrailingWhitespace, hasIndentGuide})->
invisibles ?= {}
html = @value
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
if @isHardTab
classes = []
classes.push('indent-guide') if hasIndentGuide
classes.push('invisible-character') if invisibles.tab
classes.push('hard-tab')
classes = classes.join(' ')
html = html.replace /^./, (match) ->
classes = 'hard-tab'
classes += ' indent-guide' if hasIndentGuide
classes += ' invisible-character' if invisibles.tab
html = html.replace StartCharacterRegex, (match) =>
match = invisibles.tab ? match
"<span class='#{classes}'>#{match}</span>"
"<span class='#{classes}'>#{@escapeString(match)}</span>"
else
if hasLeadingWhitespace
classes = []
classes.push('indent-guide') if hasIndentGuide
classes.push('invisible-character') if invisibles.space
classes.push('leading-whitespace')
classes = classes.join(' ')
html = html.replace /^[ ]+/, (match) ->
match = match.replace(/./g, invisibles.space) if invisibles.space
"<span class='#{classes}'>#{match}</span>"
if hasTrailingWhitespace
classes = []
classes.push('indent-guide') if hasIndentGuide and not hasLeadingWhitespace
classes.push('invisible-character') if invisibles.space
classes.push('trailing-whitespace')
classes = classes.join(' ')
html = html.replace /[ ]+$/, (match) ->
match = match.replace(/./g, invisibles.space) if invisibles.space
"<span class='#{classes}'>#{match}</span>"
startIndex = 0
endIndex = html.length
leadingHtml = ''
trailingHtml = ''
if hasLeadingWhitespace and match = LeadingWhitespaceRegex.exec(html)
classes = 'leading-whitespace'
classes += ' indent-guide' if hasIndentGuide
classes += ' invisible-character' if invisibles.space
match[0] = match[0].replace(CharacterRegex, invisibles.space) if invisibles.space
leadingHtml = "<span class='#{classes}'>#{match[0]}</span>"
startIndex = match[0].length
if hasTrailingWhitespace and match = TrailingWhitespaceRegex.exec(html)
classes = 'trailing-whitespace'
classes += ' indent-guide' if hasIndentGuide and not hasLeadingWhitespace
classes += ' invisible-character' if invisibles.space
match[0] = match[0].replace(CharacterRegex, invisibles.space) if invisibles.space
trailingHtml = "<span class='#{classes}'>#{match[0]}</span>"
endIndex = match.index
html = leadingHtml + @escapeString(html, startIndex, endIndex) + trailingHtml
html
escapeString: (str, startIndex, endIndex) ->
strLength = str.length
startIndex ?= 0
endIndex ?= strLength
str = str.slice(startIndex, endIndex) if startIndex > 0 or endIndex < strLength
str.replace(EscapeRegex, @escapeStringReplace)
escapeStringReplace: (match) ->
switch match
when '&' then '&amp;'
when '"' then '&quot;'
when "'" then '&#39;'
when '<' then '&lt;'
when '>' then '&gt;'
else match

View File

@ -30,6 +30,8 @@ class TokenizedBuffer
constructor: (optionsOrState) ->
if optionsOrState instanceof telepath.Document
@state = optionsOrState
# TODO: This needs to be made async, but should wait until the new Telepath changes land
@buffer = project.bufferForPath(optionsOrState.get('bufferPath'))
else
{ @buffer, tabLength } = optionsOrState
@ -99,11 +101,11 @@ class TokenizedBuffer
@trigger "changed", { start: 0, end: lastRow, delta: 0 }
tokenizeInBackground: ->
return if not @visible or @pendingChunk
return if not @visible or @pendingChunk or @destroyed
@pendingChunk = true
_.defer =>
@pendingChunk = false
@tokenizeNextChunk()
@tokenizeNextChunk() unless @destroyed
tokenizeNextChunk: ->
rowsRemaining = @chunkSize
@ -249,6 +251,7 @@ class TokenizedBuffer
destroy: ->
@unsubscribe()
@destroyed = true
iterateTokensInBufferRange: (bufferRange, iterator) ->
bufferRange = Range.fromObject(bufferRange)

View File

@ -1,9 +1,10 @@
# Like sands through the hourglass, so are the days of our lives.
startTime = new Date().getTime()
require './atom'
require './window'
Atom = require './atom'
window.atom = new Atom()
window.setUpEnvironment('editor')
window.startEditorWindow()
console.log "Window load time: #{new Date().getTime() - startTime}ms"

View File

@ -1,7 +1,7 @@
$ = require './jquery-extensions'
_ = require './underscore-extensions'
ipc = require 'ipc'
remote = require 'remote'
shell = require 'shell'
Subscriber = require './subscriber'
fsUtils = require './fs-utils'
@ -20,27 +20,44 @@ class WindowEventHandler
$(atom.contextMenu.activeElement).trigger(command, args...)
@subscribe $(window), 'focus', -> $("body").removeClass('is-blurred')
@subscribe $(window), 'blur', -> $("body").addClass('is-blurred')
@subscribe $(window), 'window:open-path', (event, {pathToOpen, initialLine}) ->
rootView?.open(pathToOpen, {initialLine}) unless fsUtils.isDirectorySync(pathToOpen)
unless fsUtils.isDirectorySync(pathToOpen)
atom.rootView?.openAsync(pathToOpen, {initialLine})
@subscribe $(window), 'beforeunload', =>
confirmed = rootView?.confirmClose()
atom.hide() if confirmed and not @reloadRequested and remote.getCurrentWindow().isWebViewFocused()
confirmed = atom.rootView?.confirmClose()
atom.hide() if confirmed and not @reloadRequested and atom.getCurrentWindow().isWebViewFocused()
@reloadRequested = false
confirmed
@subscribe $(window), 'unload', ->
atom.getWindowState().set('dimensions', atom.getDimensions())
@subscribeToCommand $(window), 'window:toggle-full-screen', => atom.toggleFullScreen()
@subscribeToCommand $(window), 'window:close', => atom.close()
@subscribeToCommand $(window), 'window:reload', =>
@reloadRequested = true
atom.reload()
@subscribeToCommand $(window), 'window:toggle-dev-tools', => atom.toggleDevTools()
@subscribeToCommand $(document), 'core:focus-next', @focusNext
@subscribeToCommand $(document), 'core:focus-previous', @focusPrevious
@subscribe $(document), 'keydown', keymap.handleKeyEvent
@subscribe $(document), 'keydown', atom.keymap.handleKeyEvent
@subscribe $(document), 'drop', (e) ->
e.preventDefault()
e.stopPropagation()
pathsToOpen = _.pluck(e.originalEvent.dataTransfer.files, 'path')
atom.open({pathsToOpen}) if pathsToOpen.length > 0
@subscribe $(document), 'drop', onDrop
@subscribe $(document), 'dragover', (e) ->
e.preventDefault()
e.stopPropagation()
@ -54,7 +71,7 @@ class WindowEventHandler
openLink: (event) =>
location = $(event.target).attr('href')
if location and location[0] isnt '#' and /^https?:\/\//.test(location)
require('shell').openExternal(location)
shell.openExternal(location)
false
eachTabIndexedElement: (callback) ->

View File

@ -1,17 +1,9 @@
fsUtils = require './fs-utils'
path = require 'path'
telepath = require 'telepath'
$ = require './jquery-extensions'
_ = require './underscore-extensions'
remote = require 'remote'
ipc = require 'ipc'
WindowEventHandler = require './window-event-handler'
deserializers = {}
deferredDeserializers = {}
defaultWindowDimensions = {width: 800, height: 600}
lessCache = null
### Internal ###
windowEventHandler = null
@ -28,19 +20,14 @@ displayWindow = ->
# This method is called in any window needing a general environment, including specs
window.setUpEnvironment = (windowMode) ->
atom.windowMode = windowMode
window.resourcePath = remote.getCurrentWindow().loadSettings.resourcePath
Config = require './config'
Syntax = require './syntax'
Pasteboard = require './pasteboard'
Keymap = require './keymap'
window.rootViewParentSelector = 'body'
window.config = new Config
window.syntax = deserialize(atom.getWindowState('syntax')) ? new Syntax
window.pasteboard = new Pasteboard
window.keymap = new Keymap()
window.resourcePath = atom.getLoadSettings().resourcePath
atom.initialize()
#TODO remove once all packages use the atom global
window.config = atom.config
window.syntax = atom.syntax
window.pasteboard = atom.pasteboard
window.keymap = atom.keymap
window.site = atom.site
# Set up the default event handlers and menus for a non-editor windows.
#
@ -50,8 +37,8 @@ window.setUpEnvironment = (windowMode) ->
# This should only be called after setUpEnvironment() has been called.
window.setUpDefaultEvents = ->
windowEventHandler = new WindowEventHandler
keymap.loadBundledKeymaps()
ipc.sendChannel 'update-application-menu', keymap.keystrokesByCommandForSelector('body')
atom.keymap.loadBundledKeymaps()
atom.menu.update()
# This method is only called when opening a real application window
window.startEditorWindow = ->
@ -60,16 +47,16 @@ window.startEditorWindow = ->
windowEventHandler = new WindowEventHandler
restoreDimensions()
config.load()
keymap.loadBundledKeymaps()
atom.loadBaseStylesheets()
atom.loadPackages()
atom.loadThemes()
atom.config.load()
atom.keymap.loadBundledKeymaps()
atom.themes.loadBaseStylesheets()
atom.packages.loadPackages()
atom.themes.load()
deserializeEditorWindow()
atom.activatePackages()
keymap.loadUserKeymaps()
atom.packages.activatePackages()
atom.keymap.loadUserKeymaps()
atom.requireUserInitScript()
ipc.sendChannel 'update-application-menu', keymap.keystrokesByCommandForSelector('body')
atom.menu.update()
$(window).on 'unload', ->
$(document.body).hide()
unloadEditorWindow()
@ -78,189 +65,67 @@ window.startEditorWindow = ->
displayWindow()
window.unloadEditorWindow = ->
return if not project and not rootView
return if not atom.project and not atom.rootView
windowState = atom.getWindowState()
windowState.set('project', project.serialize())
windowState.set('syntax', syntax.serialize())
windowState.set('rootView', rootView.serialize())
atom.deactivatePackages()
windowState.set('packageStates', atom.packageStates)
windowState.set('project', atom.project.serialize())
windowState.set('syntax', atom.syntax.serialize())
windowState.set('rootView', atom.rootView.serialize())
atom.packages.deactivatePackages()
windowState.set('packageStates', atom.packages.packageStates)
atom.saveWindowState()
rootView.remove()
project.destroy()
atom.rootView.remove()
atom.project.destroy()
windowEventHandler?.unsubscribe()
lessCache?.destroy()
window.rootView = null
window.project = null
window.installAtomCommand = (callback) ->
installAtomCommand = (callback) ->
commandPath = path.join(window.resourcePath, 'atom.sh')
require('./command-installer').install(commandPath, callback)
window.installApmCommand = (callback) ->
installApmCommand = (callback) ->
commandPath = path.join(window.resourcePath, 'node_modules', '.bin', 'apm')
require('./command-installer').install(commandPath, callback)
window.onDrop = (e) ->
e.preventDefault()
e.stopPropagation()
pathsToOpen = _.pluck(e.originalEvent.dataTransfer.files, 'path')
atom.open({pathsToOpen}) if pathsToOpen.length > 0
window.deserializeEditorWindow = ->
RootView = require './root-view'
Project = require './project'
atom.deserializePackageStates()
atom.deserializeProject()
window.project = atom.project
atom.deserializeRootView()
window.rootView = atom.rootView
windowState = atom.getWindowState()
window.getDimensions = -> atom.getDimensions()
atom.packageStates = windowState.getObject('packageStates') ? {}
windowState.remove('packageStates')
window.setDimensions = (args...) -> atom.setDimensions(args...)
window.project = deserialize(windowState.get('project'))
unless window.project?
window.project = new Project(atom.getLoadSettings().initialPath)
windowState.set('project', window.project.getState())
window.rootView = deserialize(windowState.get('rootView'))
unless window.rootView?
window.rootView = new RootView()
windowState.set('rootView', window.rootView.getState())
$(rootViewParentSelector).append(rootView)
project.on 'path-changed', ->
projectPath = project.getPath()
atom.getLoadSettings().initialPath = projectPath
window.stylesheetElementForId = (id) ->
$("""head style[id="#{id}"]""")
window.resolveStylesheet = (stylesheetPath) ->
if path.extname(stylesheetPath).length > 0
fsUtils.resolveOnLoadPath(stylesheetPath)
else
fsUtils.resolveOnLoadPath(stylesheetPath, ['css', 'less'])
# Public: resolves and applies the stylesheet specified by the path.
#
# * stylesheetPath: String. Can be an absolute path or the name of a CSS or
# LESS file in the stylesheets path.
#
# Returns the absolute path to the stylesheet
window.requireStylesheet = (stylesheetPath) ->
if fullPath = window.resolveStylesheet(stylesheetPath)
content = window.loadStylesheet(fullPath)
window.applyStylesheet(fullPath, content)
else
throw new Error("Could not find a file at path '#{stylesheetPath}'")
fullPath
window.loadStylesheet = (stylesheetPath) ->
if path.extname(stylesheetPath) is '.less'
loadLessStylesheet(stylesheetPath)
else
fsUtils.read(stylesheetPath)
window.loadLessStylesheet = (lessStylesheetPath) ->
unless lessCache?
LessCompileCache = require './less-compile-cache'
lessCache = new LessCompileCache()
try
lessCache.read(lessStylesheetPath)
catch e
console.error """
Error compiling less stylesheet: #{lessStylesheetPath}
Line number: #{e.line}
#{e.message}
"""
window.removeStylesheet = (stylesheetPath) ->
unless fullPath = window.resolveStylesheet(stylesheetPath)
throw new Error("Could not find a file at path '#{stylesheetPath}'")
window.stylesheetElementForId(fullPath).remove()
window.applyStylesheet = (id, text, ttype = 'bundled') ->
styleElement = window.stylesheetElementForId(id)
if styleElement.length
styleElement.text(text)
else
if $("head style.#{ttype}").length
$("head style.#{ttype}:last").after "<style class='#{ttype}' id='#{id}'>#{text}</style>"
else
$("head").append "<style class='#{ttype}' id='#{id}'>#{text}</style>"
window.getDimensions = ->
browserWindow = remote.getCurrentWindow()
[x, y] = browserWindow.getPosition()
[width, height] = browserWindow.getSize()
{x, y, width, height}
window.setDimensions = ({x, y, width, height}) ->
browserWindow = remote.getCurrentWindow()
browserWindow.setSize(width, height)
if x? and y?
browserWindow.setPosition(x, y)
else
browserWindow.center()
window.restoreDimensions = ->
dimensions = atom.getWindowState().getObject('dimensions')
dimensions = defaultWindowDimensions unless dimensions?.width and dimensions?.height
window.setDimensions(dimensions)
$(window).on 'unload', -> atom.getWindowState().set('dimensions', window.getDimensions())
window.restoreDimensions = (args...) -> atom.restoreDimensions(args...)
window.onerror = ->
atom.openDevTools()
window.registerDeserializers = (args...) ->
registerDeserializer(arg) for arg in args
window.registerDeserializer = (klass) ->
deserializers[klass.name] = klass
window.registerDeferredDeserializer = (name, fn) ->
deferredDeserializers[name] = fn
window.unregisterDeserializer = (klass) ->
delete deserializers[klass.name]
window.deserialize = (state, params) ->
return unless state?
if deserializer = getDeserializer(state)
stateVersion = state.get?('version') ? state.version
return if deserializer.version? and deserializer.version isnt stateVersion
if (state instanceof telepath.Document) and not deserializer.acceptsDocuments
state = state.toObject()
deserializer.deserialize(state, params)
else
console.warn "No deserializer found for", state
window.getDeserializer = (state) ->
return unless state?
name = state.get?('deserializer') ? state.deserializer
if deferredDeserializers[name]
deferredDeserializers[name]()
delete deferredDeserializers[name]
deserializers[name]
window.requireWithGlobals = (id, globals={}) ->
existingGlobals = {}
for key, value of globals
existingGlobals[key] = window[key]
window[key] = value
require(id)
for key, value of existingGlobals
if value is undefined
delete window[key]
else
window[key] = value
atom.deserializers.add(args...)
window.registerDeserializer = (args...) ->
atom.deserializers.add(args...)
window.registerDeferredDeserializer = (args...) ->
atom.deserializers.addDeferred(args...)
window.unregisterDeserializer = (args...) ->
atom.deserializers.remove(args...)
window.deserialize = (args...) ->
atom.deserializers.deserialize(args...)
window.getDeserializer = (args...) ->
atom.deserializers.get(args...)
window.requireWithGlobals = (args...) ->
atom.requireWithGlobals(args...)
# 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.
#
# Returns the value returned by the given function.
window.measure = (description, fn) ->
start = new Date().getTime()
value = fn()
@ -268,6 +133,15 @@ window.measure = (description, fn) ->
console.log description, result
value
# 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.
#
# Return the value returned by the given function.
window.profile = (description, fn) ->
measure description, ->
console.profile(description)

View File

@ -8,15 +8,13 @@
var currentWindow = require('remote').getCurrentWindow();
try {
require('coffee-script');
require('../src/coffee-cache');
Object.defineProperty(require.extensions, '.coffee', {
writable: false,
value: require.extensions['.coffee']
});
require('../src/coffee-cache').register();
require(currentWindow.loadSettings.bootstrapScript);
currentWindow.emit('window:loaded');
}
catch (error) {
currentWindow.setSize(800, 600);
currentWindow.center();
currentWindow.show();
currentWindow.openDevTools();
console.error(error.stack || error);

View File

@ -37,6 +37,23 @@ h6 {
-webkit-flex: 1;
-webkit-flex-flow: column;
}
.dev-mode {
&:before {
content: ""; // This is not a space, it is a skull and crossbones
}
padding: @component-icon-padding;
position: absolute;
top: 0;
right: 0;
font-family: Wingdings;
font-size: 25px;
z-index: 1000;
opacity: 0.75;
color: @text-color-highlight;
}
}
#panes {

View File

@ -1,3 +1,4 @@
@import "ui-variables";
@import "octicon-mixins";
.select-list {
@ -22,7 +23,7 @@
position: relative;
overflow-y: auto;
max-height: 312px;
margin: 0;
margin: @component-padding 0 0 0;
padding: 0;
li {

View File

@ -2,10 +2,67 @@ fs = require 'fs'
path = require 'path'
request = require 'request'
formidable = require 'formidable'
module.exports = (grunt) ->
{spawn, mkdir, rm, cp} = require('./task-helpers')(grunt)
accessToken = null
getTokenFromKeychain = (callback) ->
accessToken ?= process.env['ATOM_ACCESS_TOKEN']
if accessToken
callback(null, accessToken)
return
spawn {cmd: 'security', args: ['-q', 'find-generic-password', '-ws', 'GitHub API Token']}, (error, result, code) ->
accessToken = result unless error?
callback(error, accessToken)
callAtomShellReposApi = (path, callback) ->
getTokenFromKeychain (error, accessToken) ->
if error
callback(error)
return
options =
url: "https://api.github.com/repos/atom/atom-shell#{path}"
proxy: process.env.http_proxy || process.env.https_proxy
headers:
authorization: "token #{accessToken}"
accept: 'application/vnd.github.manifold-preview'
'user-agent': 'Atom'
request options, (error, response, body) ->
if not error?
body = JSON.parse(body)
error = new Error(body.message) if response.statusCode != 200
callback(error, response, body)
findReleaseIdFromAtomShellVersion = (version, callback) ->
callAtomShellReposApi '/releases', (error, response, data) ->
if error?
grunt.log.error('GitHub API failed to access atom-shell releases')
callback(error)
else
for release in data when release.tag_name is version
callback(null, release.id)
return
grunt.log.error("There is no #{version} release of atom-shell")
callback(false)
getAtomShellDownloadUrl = (version, releaseId, callback) ->
callAtomShellReposApi "/releases/#{releaseId}/assets", (error, response, data) ->
if error?
grunt.log.error("Cannot get assets of atom-shell's #{version} release")
callback(error)
else
filename = "atom-shell-#{version}-#{process.platform}.zip"
for asset in data when asset.name is filename and asset.state is 'uploaded'
callback(null, asset.url)
return
grunt.log.error("Cannot get url of atom-shell's release asset")
callback(false)
getAtomShellVersion = ->
versionPath = path.join('atom-shell', 'version')
if grunt.file.isFile(versionPath)
@ -18,28 +75,69 @@ module.exports = (grunt) ->
isAtomShellVersionCached = (version) ->
grunt.file.isFile(getCachePath(version), 'version')
downloadAtomShell = (version, callback) ->
getDownloadOptions = (version, url, callback) ->
options =
url: "https://gh-contractor-zcbenz.s3.amazonaws.com/atom-shell/#{version}/atom-shell-#{version}-darwin.zip"
url: url
followRedirect: false
proxy: process.env.http_proxy || process.env.https_proxy
inputStream = request(options)
inputStream.on 'response', (response) ->
if response.statusCode is 200
grunt.log.writeln("Downloading atom-shell version #{version.cyan}")
cacheDirectory = getCachePath(version)
rm(cacheDirectory)
mkdir(cacheDirectory)
cacheFile = path.join(cacheDirectory, 'atom-shell.zip')
outputStream = fs.createWriteStream(cacheFile)
outputStream.on 'close', -> callback(null, cacheFile)
inputStream.pipe(outputStream)
else
if response.statusCode is 404
grunt.log.error("atom-shell #{version.cyan} not found")
# Only set headers for GitHub host, the url could also be a S3 link and
# setting headers for it would make the request fail.
if require('url').parse(url).hostname is 'api.github.com'
getTokenFromKeychain (error, accessToken) ->
options.headers =
authorization: "token #{accessToken}"
accept: 'application/octet-stream'
'user-agent': 'Atom'
callback(error, options)
else
callback(null, options)
downloadAtomShell = (version, url, callback) ->
getDownloadOptions version, url, (error, options) ->
if error
callback(error)
return
inputStream = request(options)
inputStream.on 'response', (response) ->
if response.statusCode is 302
# Manually handle redirection so headers would not be sent for S3.
downloadAtomShell(version, response.headers.location, callback)
else if response.statusCode is 200
grunt.log.writeln("Downloading atom-shell version #{version.cyan}")
cacheDirectory = getCachePath(version)
rm(cacheDirectory)
mkdir(cacheDirectory)
form = new formidable.IncomingForm()
form.uploadDir = cacheDirectory
form.maxFieldsSize = 100 * 1024 * 1024
form.on 'file', (name, file) ->
cacheFile = path.join(cacheDirectory, 'atom-shell.zip')
fs.renameSync(file.path, cacheFile)
callback(null, cacheFile)
form.parse response, (error) ->
if error
grunt.log.error("atom-shell #{version.cyan} failed to download")
else
grunt.log.error("atom-shell #{version.cyan} request failed")
callback(false)
if response.statusCode is 404
grunt.log.error("atom-shell #{version.cyan} not found")
else
grunt.log.error("atom-shell #{version.cyan} request failed")
callback(false)
downloadAtomShellOfVersion = (version, callback) ->
findReleaseIdFromAtomShellVersion version, (error, releaseId) ->
if error?
callback(error)
else
getAtomShellDownloadUrl version, releaseId, (error, url) ->
if error?
callback(error)
else
downloadAtomShell version, url, callback
unzipAtomShell = (zipPath, callback) ->
grunt.log.writeln('Unzipping atom-shell')
@ -48,7 +146,7 @@ module.exports = (grunt) ->
spawn {cmd: 'unzip', args: [zipPath, '-d', directoryPath]}, (error) ->
rm(zipPath)
callback(error)
rebuildNativeModules = (previousVersion, callback) ->
newVersion = getAtomShellVersion()
if newVersion and newVersion isnt previousVersion
@ -74,11 +172,13 @@ module.exports = (grunt) ->
installAtomShell(atomShellVersion)
rebuildNativeModules(currentAtomShellVersion, done)
else
downloadAtomShell atomShellVersion, (error, zipPath) ->
if zipPath?
downloadAtomShellOfVersion atomShellVersion, (error, zipPath) ->
if error?
done(error)
else if zipPath?
unzipAtomShell zipPath, (error) ->
if error?
done(false)
done(error)
else
grunt.log.writeln("Installing atom-shell #{atomShellVersion.cyan}")
installAtomShell(atomShellVersion)

2
vendor/apm vendored

@ -1 +1 @@
Subproject commit 7c301ec6dcbe12e4d22695e761cf37bc986672dc
Subproject commit fcb19e296ca8979a28d2d503c2650f4ef381c8be