Merge branch 'master' into platform-keybindings

This commit is contained in:
Matt Colyer 2013-11-19 11:11:32 -08:00
commit 6157fcaf73
45 changed files with 706 additions and 716 deletions

View File

@ -8,6 +8,10 @@ _ = require 'underscore-plus'
packageJson = require './package.json'
module.exports = (grunt) ->
if not grunt.option('verbose')
grunt.log.writeln = (args...) -> grunt.log
grunt.log.write = (args...) -> grunt.log
[major, minor, patch] = packageJson.version.split('.')
if process.platform is 'win32'
appName = 'Atom'

View File

@ -1,4 +1,4 @@
{Document, Point, Range, Site} = require 'telepath'
{Document, Point, Range} = require 'telepath'
module.exports =
_: require 'underscore-plus'
@ -11,7 +11,6 @@ module.exports =
Git: require '../src/git'
Point: Point
Range: Range
Site: Site
# The following classes can't be used from a Task handler and should therefore
# only be exported when not running as a child node process
@ -22,7 +21,6 @@ unless process.env.ATOM_SHELL_INTERNAL_RUN_AS_NODE
module.exports.$$ = $$
module.exports.$$$ = $$$
module.exports.Editor = require '../src/editor'
module.exports.pathForRepositoryUrl = require('../src/project').pathForRepositoryUrl
module.exports.RootView = require '../src/root-view'
module.exports.SelectList = require '../src/select-list'
module.exports.ScrollView = require '../src/scroll-view'

172
menus/win32.cson Normal file
View File

@ -0,0 +1,172 @@
'menu': [
{
label: '&File'
submenu: [
{ label: 'New &Window', command: 'application:new-window' }
{ label: '&New File', command: 'application:new-file' }
{ label: '&Open...', command: 'application:open' }
{ label: 'Reopen Last &Item', command: 'pane:reopen-closed-item' }
{ type: 'separator' }
{ label: '&Preferences...', command: 'application:show-settings' }
{ type: 'separator' }
{ label: '&Save', command: 'core:save' }
{ label: 'Save &As...', command: 'core:save-as' }
{ label: 'Save A&ll', command: 'window:save-all' }
{ type: 'separator' }
{ label: '&Close Buffer', command: 'core:close' }
{ label: 'Close All &Buffers', command: 'pane:close' }
{ label: 'Clos&e Window', command: 'window:close' }
{ type: 'separator' }
{ label: 'E&xit', command: 'application:quit' }
]
}
{
label: '&Edit'
submenu: [
{ label: '&Undo', command: 'core:undo' }
{ label: '&Redo', command: 'core:redo' }
{ type: 'separator' }
{ label: '&Cut', command: 'core:cut' }
{ label: 'C&opy', command: 'core:copy' }
{ label: 'Copy Pat&h', command: 'editor:copy-path' }
{ label: '&Paste', command: 'core:paste' }
{ label: 'Select &All', command: 'core:select-all' }
{ type: 'separator' }
{ label: '&Toggle Comments', command: 'editor:toggle-line-comments' }
{
label: 'Lines',
submenu: [
{ label: '&Indent', command: 'editor:indent-selected-rows' }
{ label: '&Outdent', command: 'editor:outdent-selected-rows' }
{ label: '&Auto Indent', command: 'editor:auto-indent' }
{ type: 'separator' }
{ label: 'Move Line &Up', command: 'editor:move-line-up' }
{ label: 'Move Line &Down', command: 'editor:move-line-down' }
{ label: 'Du&plicate Line', command: 'editor:duplicate-line' }
{ label: 'D&elete Line', command: 'editor:delete-line' }
{ label: '&Join Lines', command: 'editor:join-line' }
]
}
{
label: 'Text',
submenu: [
{ label: '&Upper Case', command: 'editor:upper-case' }
{ label: '&Lower Case', command: 'editor:lower-case' }
{ type: 'separator' }
{ label: 'Delete to End of &Word', command: 'editor:delete-to-end-of-word' }
{ label: '&Delete Line', command: 'editor:delete-line' }
{ type: 'separator' }
{ label: '&Transpose', command: 'editor:transpose' }
]
}
{
label: 'Folding',
submenu: [
{ label: '&Fold', command: 'editor:fold-current-row' }
{ label: '&Unfold', command: 'editor:unfold-current-row' }
{ label: 'Unfold &All', command: 'editor:unfold-all' }
{ type: 'separator' }
{ label: 'Fol&d All', command: 'editor:fold-all' }
{ label: 'Fold Level 1', command: 'editor:fold-at-indent-level-1' }
{ label: 'Fold Level 2', command: 'editor:fold-at-indent-level-2' }
{ label: 'Fold Level 3', command: 'editor:fold-at-indent-level-3' }
{ label: 'Fold Level 4', command: 'editor:fold-at-indent-level-4' }
{ label: 'Fold Level 5', command: 'editor:fold-at-indent-level-5' }
{ label: 'Fold Level 6', command: 'editor:fold-at-indent-level-6' }
{ label: 'Fold Level 7', command: 'editor:fold-at-indent-level-7' }
{ label: 'Fold Level 8', command: 'editor:fold-at-indent-level-8' }
{ label: 'Fold Level 9', command: 'editor:fold-at-indent-level-9' }
]
}
]
}
{
label: '&View'
submenu: [
{ label: '&Reload', command: 'window:reload' }
{ label: 'Toggle &Full Screen', command: 'window:toggle-full-screen' }
{
label: 'Developer'
submenu: [
{ label: 'Open In &Dev Mode...', command: 'application:open-dev' }
{ label: 'Run &Atom Specs', command: 'application:run-all-specs' }
{ label: 'Run Package &Specs', command: 'window:run-package-specs' }
{ label: 'Toggle Developer &Tools', command: 'window:toggle-dev-tools' }
]
}
{ type: 'separator' }
{ label: 'Toggle Soft &Wrap', command: 'editor:toggle-soft-wrap' }
]
}
{
label: '&Selection'
submenu: [
{ label: 'Add Selection &Above', command: 'editor:add-selection-above' }
{ label: 'Add Selection &Below', command: 'editor:add-selection-below' }
{ type: 'separator' }
{ label: 'Select to &Top', command: 'core:select-to-top' }
{ label: 'Select to Botto&m', command: 'core:select-to-bottom' }
{ type: 'separator' }
{ label: 'Select &Line', command: 'editor:select-line' }
{ label: 'Select &Word', command: 'editor:select-word' }
{ label: 'Select to Beginning of W&ord', command: 'editor:select-to-beginning-of-word' }
{ label: 'Select to Beginning of L&ine', command: 'editor:select-to-beginning-of-line' }
{ label: 'Select to First &Character of Line', command: 'editor:select-to-first-character-of-line' }
{ label: 'Select to End of Wor&d', command: 'editor:select-to-end-of-word' }
{ label: 'Select to End of Lin&e', command: 'editor:select-to-end-of-line' }
]
}
{
label: '&Movement'
submenu: [
{ label: 'Move to &Top', command: 'core:move-to-top' }
{ label: 'Move to &Bottom', command: 'core:move-to-bottom' }
{ type: 'separator' }
{ label: 'Move to Beginning of &Line', command: 'editor:move-to-beginning-of-line' }
{ label: 'Move to &First Character of Line', command: 'editor:move-to-first-character-of-line' }
{ label: 'Move to &End of Line', command: 'editor:move-to-end-of-line' }
{ type: 'separator' }
{ label: 'Move to Beginning of &Word', command: 'editor:move-to-beginning-of-word' }
{ label: 'Move to End of Wor&d', command: 'editor:move-to-end-of-word' }
{ label: 'Move to &Next Word', command: 'editor:move-to-next-word-boundary' }
{ label: 'Move to &Previous Word', command: 'editor:move-to-previous-word-boundary' }
]
}
{
label: 'F&ind'
submenu: []
}
{
label: '&Packages'
submenu: []
}
{
label: '&Window'
submenu: [
{ label: 'Mi&nimize', command: 'application:minimize' }
{ label: 'Ma&ximize', command: 'application:zoom' }
{ type: 'separator' }
{ label: 'Bring &All to Front', command: 'application:bring-all-windows-to-front' }
]
}
{
label: '&Help'
submenu: [
{ label: '&About Atom...', command: 'application:about' }
{ label: "VERSION", enabled: false }
{ label: "Install &update", command: 'application:install-update', visible: false }
{ type: 'separator' }
{ label: '&Documentation', command: 'application:open-documentation' }
{ label: 'Report an &Issue', command: 'application:report-issue' }
{ type: 'separator' }
]
}
]

View File

@ -1,6 +1,6 @@
{
"name": "atom",
"version": "35.0.0",
"version": "39.0.0",
"main": "./src/browser/main.js",
"repository": {
"type": "git",
@ -13,10 +13,10 @@
"dependencies": {
"async": "0.2.6",
"bootstrap": "git://github.com/twbs/bootstrap.git#v3.0.0",
"clear-cut": "0.1.0",
"clear-cut": "0.2.0",
"coffee-script": "1.6.3",
"coffeestack": "0.6.0",
"emissary": "0.9.0",
"emissary": "0.17.0",
"first-mate": "0.5.0",
"fs-plus": "0.9.0",
"fuzzaldrin": "0.1.0",
@ -28,14 +28,14 @@
"nslog": "0.1.0",
"oniguruma": "0.24.0",
"optimist": "0.4.0",
"pathwatcher": "0.9.0",
"pathwatcher": "0.10.0",
"pegjs": "0.7.0",
"q": "0.9.7",
"scandal": "0.8.0",
"season": "0.14.0",
"semver": "1.1.4",
"space-pen": "2.0.0",
"telepath": "0.23.0",
"telepath": "0.38.0",
"temp": "0.5.0",
"underscore-plus": "0.3.0"
},
@ -52,7 +52,6 @@
"grunt-contrib-coffee": "~0.7.0",
"grunt-contrib-less": "~0.8.0",
"walkdir": "0.0.7",
"ws": "0.4.27",
"js-yaml": "~2.1.0",
"grunt-markdown": "~0.4.0",
"json-front-matter": "~0.1.3",
@ -65,9 +64,9 @@
"rimraf": "~2.2.2"
},
"packageDependencies": {
"atom-light-ui": "0.6.0",
"atom-light-ui": "0.7.0",
"atom-light-syntax": "0.6.0",
"atom-dark-ui": "0.6.0",
"atom-dark-ui": "0.7.0",
"atom-dark-syntax": "0.6.0",
"base16-tomorrow-dark-theme": "0.6.0",
"solarized-dark-syntax": "0.4.0",
@ -78,11 +77,11 @@
"bookmarks": "0.10.0",
"bracket-matcher": "0.11.0",
"command-logger": "0.6.0",
"command-palette": "0.7.0",
"command-palette": "0.8.0",
"dev-live-reload": "0.15.0",
"editor-stats": "0.5.0",
"exception-reporting": "0.7.0",
"find-and-replace": "0.40.0",
"find-and-replace": "0.41.0",
"fuzzy-finder": "0.20.0",
"gists": "0.6.0",
"git-diff": "0.13.0",
@ -90,16 +89,17 @@
"go-to-line": "0.8.0",
"grammar-selector": "0.8.0",
"image-view": "0.7.0",
"keybinding-resolver": "0.2.0",
"link": "0.7.0",
"markdown-preview": "0.15.0",
"metrics": "0.11.0",
"package-generator": "0.19.0",
"release-notes": "0.11.0",
"settings-view": "0.39.0",
"settings-view": "0.41.0",
"snippets": "0.13.0",
"spell-check": "0.13.0",
"status-bar": "0.16.0",
"styleguide": "0.9.0",
"styleguide": "0.10.0",
"symbols-view": "0.19.0",
"tabs": "0.8.0",
"terminal": "0.16.0",

View File

@ -5,9 +5,15 @@ var path = require('path');
// Executes an array of commands one by one.
function executeCommands(commands, done, index) {
index = (index == undefined ? 0 : index);
if (index < commands.length)
safeExec(commands[index], executeCommands.bind(this, commands, done, index + 1));
else
if (index < commands.length) {
var command = commands[index];
var options = null;
if (typeof command !== 'string') {
options = command.options;
command = command.command;
}
safeExec(command, options, executeCommands.bind(this, commands, done, index + 1));
} else
done(null);
}
@ -21,8 +27,8 @@ var echoNewLine = process.platform == 'win32' ? 'echo.' : 'echo';
var commands = [
'git submodule --quiet sync',
'git submodule --quiet update --recursive --init',
joinCommands('cd vendor/apm', 'npm install --silent .'),
'npm install --silent vendor/apm',
{command: joinCommands('cd vendor/apm', 'npm install --silent .'), options: {ignoreStdout: true}},
{command: 'npm install --silent vendor/apm', options: {ignoreStdout: true}},
echoNewLine,
'node node_modules/atom-package-manager/bin/apm clean',
'node node_modules/atom-package-manager/bin/apm install --silent',

View File

@ -10,7 +10,7 @@ exports.safeExec = function(command, options, callback) {
if (!options)
options = {};
// This needed to be increase for `apm test` runs that generate tons of failures
// This needed to be increased for `apm test` runs that generate many failures
// The default is 200KB.
options.maxBuffer = 1024 * 1024;
@ -21,7 +21,8 @@ exports.safeExec = function(command, options, callback) {
callback(null);
});
child.stderr.pipe(process.stderr);
child.stdout.pipe(process.stdout);
if (!options.ignoreStdout)
child.stdout.pipe(process.stdout);
}
// Same with safeExec but call child_process.spawn instead.

View File

@ -137,28 +137,28 @@ describe "the `atom` global", ->
element2 = $$ -> @div class: 'test-2'
element3 = $$ -> @div class: 'test-3'
expect(atom.keymap.bindingsForElement(element1)['ctrl-z']).toBeUndefined()
expect(atom.keymap.bindingsForElement(element2)['ctrl-z']).toBeUndefined()
expect(atom.keymap.bindingsForElement(element3)['ctrl-z']).toBeUndefined()
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', element1)).toHaveLength 0
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', element2)).toHaveLength 0
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', element3)).toHaveLength 0
atom.activatePackage("package-with-keymaps")
expect(atom.keymap.bindingsForElement(element1)['ctrl-z']).toBe "test-1"
expect(atom.keymap.bindingsForElement(element2)['ctrl-z']).toBe "test-2"
expect(atom.keymap.bindingsForElement(element3)['ctrl-z']).toBeUndefined()
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', element1)[0].command).toBe "test-1"
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', element2)[0].command).toBe "test-2"
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', element3)).toHaveLength 0
describe "when the metadata contains a 'keymaps' manifest", ->
it "loads only the keymaps specified by the manifest, in the specified order", ->
element1 = $$ -> @div class: 'test-1'
element3 = $$ -> @div class: 'test-3'
expect(atom.keymap.bindingsForElement(element1)['ctrl-z']).toBeUndefined()
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', element1)).toHaveLength 0
atom.activatePackage("package-with-keymaps-manifest")
expect(atom.keymap.bindingsForElement(element1)['ctrl-z']).toBe 'keymap-1'
expect(atom.keymap.bindingsForElement(element1)['ctrl-n']).toBe 'keymap-2'
expect(atom.keymap.bindingsForElement(element3)['ctrl-y']).toBeUndefined()
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', element1)[0].command).toBe 'keymap-1'
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-n', element1)[0].command).toBe 'keymap-2'
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-y', element3)).toHaveLength 0
describe "menu loading", ->
beforeEach ->
@ -317,8 +317,8 @@ describe "the `atom` global", ->
it "removes the package's keymaps", ->
atom.activatePackage('package-with-keymaps')
atom.deactivatePackage('package-with-keymaps')
expect(atom.keymap.bindingsForElement($$ -> @div class: 'test-1')['ctrl-z']).toBeUndefined()
expect(atom.keymap.bindingsForElement($$ -> @div class: 'test-2')['ctrl-z']).toBeUndefined()
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', $$ -> @div class: 'test-1')).toHaveLength 0
expect(atom.keymap.keyBindingsForKeystrokeMatchingElement('ctrl-z', $$ -> @div class: 'test-2')).toHaveLength 0
it "removes the package's stylesheets", ->
atom.activatePackage('package-with-stylesheets')

View File

@ -158,12 +158,12 @@ describe "Config", ->
expect(atom.config.get("foo.quux.y")).toBe 1
describe ".observe(keyPath)", ->
observeHandler = null
[observeHandler, observeSubscription] = []
beforeEach ->
observeHandler = jasmine.createSpy("observeHandler")
atom.config.set("foo.bar.baz", "value 1")
atom.config.observe "foo.bar.baz", observeHandler
observeSubscription = atom.config.observe "foo.bar.baz", observeHandler
it "fires the given callback with the current value at the keypath", ->
expect(observeHandler).toHaveBeenCalledWith("value 1")
@ -192,6 +192,12 @@ describe "Config", ->
atom.config.set("foo.bar.baz", "i'm back")
expect(observeHandler).toHaveBeenCalledWith("i'm back", {previous: undefined})
it "does not fire the callback once the observe subscription is off'ed", ->
observeHandler.reset() # clear the initial call
observeSubscription.off()
atom.config.set('foo.bar.baz', "value 2")
expect(observeHandler).not.toHaveBeenCalled()
describe ".initializeConfigDirectory()", ->
beforeEach ->
atom.config.configDirPath = dotAtomPath

View File

@ -2525,6 +2525,13 @@ describe "EditSession", ->
editSession.insertText("foo", indentBasis: 5)
expect(editSession.lineForBufferRow(5)).toBe " foo current = items.shift();"
it "does not adjust the whitespace if there are preceding characters", ->
copyText(" foo")
editSession.setCursorBufferPosition([5, 30])
editSession.pasteText()
expect(editSession.lineForBufferRow(5)).toBe " current = items.shift(); foo"
describe "when the inserted text contains newlines", ->
describe "when the cursor is preceded only by whitespace characters", ->
it "normalizes indented lines to the cursor's current indentation level", ->

View File

@ -2719,7 +2719,7 @@ describe "Editor", ->
describe "when the escape key is pressed on the editor", ->
it "clears multiple selections if there are any, and otherwise allows other bindings to be handled", ->
keymap.bindKeys '.editor', 'escape': 'test-event'
keymap.bindKeys 'name', '.editor', 'escape': 'test-event'
testEventHandler = jasmine.createSpy("testEventHandler")
editor.on 'test-event', testEventHandler

View File

@ -257,7 +257,8 @@ describe "Git", ->
it "subscribes to all the serialized buffers in the project", ->
project.openSync('sample.js')
project2 = deserialize(project.serialize())
#TODO Replace with atom.replicate().project when Atom is a telepath model
project2 = atom.replicate().get('project')
buffer = project2.getBuffers()[0]
waitsFor ->

View File

@ -21,25 +21,19 @@ describe "Keymap", ->
describe ".handleKeyEvent(event)", ->
deleteCharHandler = null
insertCharHandler = null
metaZHandler = null
beforeEach ->
keymap.bindKeys '.command-mode', 'x': 'deleteChar'
keymap.bindKeys '.insert-mode', 'x': 'insertChar'
keymap.bindKeys 'name', '.command-mode', 'x': 'deleteChar'
keymap.bindKeys 'name', '.insert-mode', 'x': 'insertChar'
keymap.bindKeys 'name', '.command-mode', 'meta-z': 'metaZPressed'
deleteCharHandler = jasmine.createSpy('deleteCharHandler')
insertCharHandler = jasmine.createSpy('insertCharHandler')
metaZHandler = jasmine.createSpy('metaZHandler')
fragment.on 'deleteChar', deleteCharHandler
fragment.on 'insertChar', insertCharHandler
it "adds a 'keystrokes' string to the event object", ->
event = keydownEvent('x', altKey: true, metaKey: true)
keymap.handleKeyEvent(event)
expect(event.keystrokes).toBe 'alt-meta-x'
event = keydownEvent(',', metaKey: true)
event.which = 188
keymap.handleKeyEvent(event)
expect(event.keystrokes).toBe 'meta-,'
fragment.on 'metaZPressed', metaZHandler
describe "when no binding matches the event's keystroke", ->
it "does not return false so the event continues to propagate", ->
@ -47,10 +41,11 @@ describe "Keymap", ->
describe "when a non-English keyboard language is used", ->
it "uses the physical character pressed instead of the character it maps to in the current language", ->
event = keydownEvent('U+03B6', metaKey: true) # This is the 'z' key using the Greek keyboard layout
event.which = 122
keymap.handleKeyEvent(event)
expect(event.keystrokes).toBe 'meta-z'
event = keydownEvent('U+03B6', metaKey: true, which: 122, target: fragment[0]) # This is the 'z' key using the Greek keyboard layout
result = keymap.handleKeyEvent(event)
expect(result).toBe(false)
expect(metaZHandler).toHaveBeenCalled()
describe "when at least one binding fully matches the event's keystroke", ->
describe "when the event's target node matches a selector with a matching binding", ->
@ -67,9 +62,6 @@ describe "Keymap", ->
keymap.handleKeyEvent(event)
expect(deleteCharHandler).not.toHaveBeenCalled()
expect(insertCharHandler).toHaveBeenCalled()
commandEvent = insertCharHandler.argsForCall[0][0]
expect(commandEvent.keyEvent).toBe event
expect(event.keystrokes).toBe 'x'
describe "when the event's target node *descends* from a selector with a matching binding", ->
it "triggers the command event associated with that binding on the target node and returns false", ->
@ -88,7 +80,7 @@ describe "Keymap", ->
describe "when the event's target node descends from multiple nodes that match selectors with a binding", ->
beforeEach ->
keymap.bindKeys '.child-node', 'x': 'foo'
keymap.bindKeys 'name', '.child-node', 'x': 'foo'
it "only triggers bindings on selectors associated with the closest ancestor node", ->
fooHandler = jasmine.createSpy 'fooHandler'
@ -125,10 +117,10 @@ describe "Keymap", ->
describe "when the event bubbles to a node that matches multiple selectors", ->
describe "when the matching selectors differ in specificity", ->
it "triggers the binding for the most specific selector", ->
keymap.bindKeys 'div .child-node', 'x': 'foo'
keymap.bindKeys '.command-mode .child-node !important', 'x': 'baz'
keymap.bindKeys '.command-mode .child-node', 'x': 'quux'
keymap.bindKeys '.child-node', 'x': 'bar'
keymap.bindKeys 'name', 'div .child-node', 'x': 'foo'
keymap.bindKeys 'name', '.command-mode .child-node !important', 'x': 'baz'
keymap.bindKeys 'name', '.command-mode .child-node', 'x': 'quux'
keymap.bindKeys 'name', '.child-node', 'x': 'bar'
fooHandler = jasmine.createSpy 'fooHandler'
barHandler = jasmine.createSpy 'barHandler'
@ -146,8 +138,8 @@ describe "Keymap", ->
describe "when the matching selectors have the same specificity", ->
it "triggers the bindings for the most recently declared selector", ->
keymap.bindKeys '.child-node', 'x': 'foo', 'y': 'baz'
keymap.bindKeys '.child-node', 'x': 'bar'
keymap.bindKeys 'name', '.child-node', 'x': 'foo', 'y': 'baz'
keymap.bindKeys 'name', '.child-node', 'x': 'bar'
fooHandler = jasmine.createSpy 'fooHandler'
barHandler = jasmine.createSpy 'barHandler'
@ -168,7 +160,8 @@ describe "Keymap", ->
describe "when the event's target is the document body", ->
it "triggers the mapped event on the rootView", ->
window.rootView = new RootView
keymap.bindKeys 'body', 'x': 'foo'
rootView.attachToDom()
keymap.bindKeys 'name', 'body', 'x': 'foo'
fooHandler = jasmine.createSpy("fooHandler")
rootView.on 'foo', fooHandler
@ -180,7 +173,7 @@ describe "Keymap", ->
describe "when the event matches a 'native!' binding", ->
it "returns true, allowing the browser's native key handling to process the event", ->
keymap.bindKeys '.grandchild-node', 'x': 'native!'
keymap.bindKeys 'name', '.grandchild-node', 'x': 'native!'
nativeHandler = jasmine.createSpy("nativeHandler")
fragment.on 'native!', nativeHandler
expect(keymap.handleKeyEvent(keydownEvent('x', target: fragment.find('.grandchild-node')[0]))).toBe true
@ -190,7 +183,7 @@ describe "Keymap", ->
[quitHandler, closeOtherWindowsHandler] = []
beforeEach ->
keymap.bindKeys "*",
keymap.bindKeys 'name', "*",
'ctrl-x ctrl-c': 'quit'
'ctrl-x 1': 'close-other-windows'
@ -220,7 +213,7 @@ describe "Keymap", ->
expect(closeOtherWindowsHandler).toHaveBeenCalled()
describe "when a second keystroke added to the first doesn't match any bindings", ->
it "clears the queued keystrokes without triggering any events", ->
it "clears the queued keystroke without triggering any events", ->
expect(keymap.handleKeyEvent(keydownEvent('x', target: fragment[0], ctrlKey: true))).toBe false
expect(keymap.handleKeyEvent(keydownEvent('c', target: fragment[0]))).toBe false
expect(quitHandler).not.toHaveBeenCalled()
@ -230,7 +223,7 @@ describe "Keymap", ->
describe "when the event's target node descends from multiple nodes that match selectors with a partial binding match", ->
it "allows any of the bindings to be triggered upon a second keystroke, favoring the most specific selector", ->
keymap.bindKeys ".grandchild-node", 'ctrl-x ctrl-c': 'more-specific-quit'
keymap.bindKeys 'name', ".grandchild-node", 'ctrl-x ctrl-c': 'more-specific-quit'
grandchildNode = fragment.find('.grandchild-node')[0]
moreSpecificQuitHandler = jasmine.createSpy('moreSpecificQuitHandler')
fragment.on 'more-specific-quit', moreSpecificQuitHandler
@ -254,39 +247,17 @@ describe "Keymap", ->
describe "when there is a complete binding with a more specific selector", ->
it "favors the more specific complete match", ->
describe "when a tab keystroke does not match any bindings", ->
it "returns false to prevent the browser from transferring focus", ->
expect(keymap.handleKeyEvent(keydownEvent('U+0009', target: fragment[0]))).toBe false
describe ".keystrokesByCommandForSelector(selector)", ->
it "returns a hash of all commands and their keybindings", ->
keymap.bindKeys 'body', 'a': 'letter'
keymap.bindKeys '.editor', 'b': 'letter'
keymap.bindKeys '.editor', '1': 'number'
keymap.bindKeys '.editor', 'meta-alt-1': 'number-with-modifiers'
expect(keymap.keystrokesByCommandForSelector()).toEqual
'letter': ['b', 'a']
'number': ['1']
'number-with-modifiers': ['alt-meta-1']
expect(keymap.keystrokesByCommandForSelector('.editor')).toEqual
'letter': ['b']
'number': ['1']
'number-with-modifiers': ['alt-meta-1']
describe ".bindKeys(selector, bindings)", ->
describe ".bindKeys(name, selector, bindings)", ->
it "normalizes the key patterns in the hash to put the modifiers in alphabetical order", ->
fooHandler = jasmine.createSpy('fooHandler')
fragment.on 'foo', fooHandler
keymap.bindKeys '*', 'ctrl-alt-delete': 'foo'
keymap.bindKeys 'name', '*', 'ctrl-alt-delete': 'foo'
result = keymap.handleKeyEvent(keydownEvent('delete', ctrlKey: true, altKey: true, target: fragment[0]))
expect(result).toBe(false)
expect(fooHandler).toHaveBeenCalled()
fooHandler.reset()
keymap.bindKeys '*', 'ctrl-alt--': 'foo'
keymap.bindKeys 'name', '*', 'ctrl-alt--': 'foo'
result = keymap.handleKeyEvent(keydownEvent('-', ctrlKey: true, altKey: true, target: fragment[0]))
expect(result).toBe(false)
expect(fooHandler).toHaveBeenCalled()
@ -299,15 +270,17 @@ describe "Keymap", ->
'.brown':
'ctrl-h': 'harvest'
expect(keymap.bindingsForElement($$ -> @div class: 'green')).toEqual { 'ctrl-c': 'cultivate' }
expect(keymap.bindingsForElement($$ -> @div class: 'brown')).toEqual { 'ctrl-h': 'harvest' }
keymap.add 'medical',
'.green':
'ctrl-v': 'vomit'
expect(keymap.keyBindingsMatchingElement($$ -> @div class: 'green')).toHaveLength 2
expect(keymap.keyBindingsMatchingElement($$ -> @div class: 'brown')).toHaveLength 1
keymap.remove('nature')
expect(keymap.bindingsForElement($$ -> @div class: 'green')).toEqual {}
expect(keymap.bindingsForElement($$ -> @div class: 'brown')).toEqual {}
expect(keymap.bindingSetsByFirstKeystroke['ctrl-c']).toEqual []
expect(keymap.bindingSetsByFirstKeystroke['ctrl-h']).toEqual []
expect(keymap.keyBindingsMatchingElement($$ -> @div class: 'green')).toHaveLength 1
expect(keymap.keyBindingsMatchingElement($$ -> @div class: 'brown')).toEqual []
describe ".keystrokeStringForEvent(event)", ->
describe "when no modifiers are pressed", ->
@ -332,54 +305,22 @@ describe "Keymap", ->
expect(keymap.keystrokeStringForEvent(keydownEvent('left', shiftKey: true))).toBe 'shift-left'
expect(keymap.keystrokeStringForEvent(keydownEvent('Left', shiftKey: true))).toBe 'shift-left'
describe ".bindingsForElement(element)", ->
describe ".keyBindingsMatchingElement(element)", ->
it "returns the matching bindings for the element", ->
keymap.bindKeys '.command-mode', 'c': 'c'
keymap.bindKeys '.grandchild-node', 'g': 'g'
keymap.bindKeys 'name', '.command-mode', 'c': 'c'
keymap.bindKeys 'name', '.grandchild-node', 'g': 'g'
bindings = keymap.bindingsForElement(fragment.find('.grandchild-node'))
expect(Object.keys(bindings).length).toBe 2
expect(bindings['c']).toEqual "c"
expect(bindings['g']).toEqual "g"
bindings = keymap.keyBindingsMatchingElement(fragment.find('.grandchild-node'))
expect(bindings).toHaveLength 2
expect(bindings[0].command).toEqual "g"
expect(bindings[1].command).toEqual "c"
describe "when multiple bindings match a keystroke", ->
it "only returns bindings that match the most specific selector", ->
keymap.bindKeys '.command-mode', 'g': 'command-mode'
keymap.bindKeys '.command-mode .grandchild-node', 'g': 'command-and-grandchild-node'
keymap.bindKeys '.grandchild-node', 'g': 'grandchild-node'
keymap.bindKeys 'name', '.command-mode', 'g': 'command-mode'
keymap.bindKeys 'name', '.command-mode .grandchild-node', 'g': 'command-and-grandchild-node'
keymap.bindKeys 'name', '.grandchild-node', 'g': 'grandchild-node'
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 path.join('~', '.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(path.join('~', '.atom', 'packages', 'dummy', 'keymaps', 'a.cson'))).toEqual 'dummy'
describe "for a linked package", ->
it "returns '<package-name>'", ->
expect(keymap.determineSource(path.join('Users', 'john', 'github', 'dummy', 'keymaps', 'a.cson'))).toEqual 'dummy'
describe "for a user defined keymap", ->
it "returns 'User'", ->
expect(keymap.determineSource(path.join('~', '.atom', 'keymaps', 'a.cson'))).toEqual 'User'
describe "for a core keymap", ->
it "returns 'Core'", ->
expect(keymap.determineSource(path.join('Applications', 'Atom.app', '..', 'node_modules', 'dummy', 'keymaps', 'a.cson'))).toEqual 'Core'
describe "for a linked core keymap", ->
it "returns 'Core'", ->
expect(keymap.determineSource(path.join('Users', 'john', 'github', 'atom', 'keymaps', 'a.cson'))).toEqual 'Core'
bindings = keymap.keyBindingsMatchingElement(fragment.find('.grandchild-node'))
expect(bindings).toHaveLength 3
expect(bindings[0].command).toEqual "command-and-grandchild-node"

View File

@ -699,11 +699,11 @@ describe "Pane", ->
it "focuses the pane after attach only if had focus when serialized", ->
reloadContainer = ->
projectState = project.serialize()
projectReplica = atom.replicate().get('project')
containerState = container.serialize()
container.remove()
project.destroy()
window.project = deserialize(projectState)
window.project = projectReplica
container = deserialize(containerState)
pane = container.getRoot()
container.attachToDom()

View File

@ -19,10 +19,21 @@ describe "Project", ->
it "destroys unretained buffers and does not include them in the serialized state", ->
project.bufferForPathSync('a')
expect(project.getBuffers().length).toBe 1
deserializedProject = deserialize(project.serialize())
project.getState().serializeForPersistence()
deserializedProject = atom.replicate().get('project')
expect(deserializedProject.getBuffers().length).toBe 0
expect(project.getBuffers().length).toBe 0
it "listens for destroyed events on deserialized buffers and removes them when they are destroyed", ->
project.openSync('a')
expect(project.getBuffers().length).toBe 1
project.getState().serializeForPersistence()
deserializedProject = atom.replicate().get('project')
expect(deserializedProject.getBuffers().length).toBe 1
deserializedProject.getBuffers()[0].destroy()
expect(deserializedProject.getBuffers().length).toBe 0
describe "when an edit session is destroyed", ->
it "removes edit session and calls destroy on buffer (if buffer is not referenced by other edit sessions)", ->
editSession = project.openSync("a")

View File

@ -20,10 +20,11 @@ describe "RootView", ->
refreshRootViewAndProject = ->
rootViewState = rootView.serialize()
projectState = project.serialize()
project.getState().serializeForPersistence()
project2 = atom.replicate().get('project')
rootView.remove()
project.destroy()
window.project = deserialize(projectState)
window.project = project2
window.rootView = deserialize(rootViewState)
rootView.attachToDom()
@ -136,7 +137,7 @@ describe "RootView", ->
commandHandler = jasmine.createSpy('commandHandler')
rootView.on('foo-command', commandHandler)
atom.keymap.bindKeys('*', 'x': 'foo-command')
atom.keymap.bindKeys('name', '*', 'x': 'foo-command')
describe "when a keydown event is triggered in the RootView", ->
it "triggers matching keybindings for that event", ->

View File

@ -4,7 +4,7 @@ describe "Selection", ->
[buffer, editSession, selection] = []
beforeEach ->
buffer = project.buildBufferSync('sample.js')
buffer = project.bufferForPathSync('sample.js')
editSession = new EditSession(buffer: buffer, tabLength: 2)
selection = editSession.getSelection()

View File

@ -23,7 +23,7 @@ atom.themes.requireStylesheet '../static/jasmine'
fixturePackagesPath = path.resolve(__dirname, './fixtures/packages')
atom.packages.packageDirPaths.unshift(fixturePackagesPath)
atom.keymap.loadBundledKeymaps()
[bindingSetsToRestore, bindingSetsByFirstKeystrokeToRestore] = []
keyBindingsToRestore = atom.keymap.getKeyBindings()
$(window).on 'core:close', -> window.close()
$(window).on 'unload', ->
@ -47,11 +47,10 @@ if specDirectory = atom.getLoadSettings().specDirectory
beforeEach ->
$.fx.off = true
if specProjectPath
atom.project = new Project(specProjectPath)
else
atom.project = new Project(path.join(@specDirectory, 'fixtures'))
projectPath = specProjectPath ? path.join(@specDirectory, 'fixtures')
atom.project = atom.getWindowState().set('project', new Project(path: projectPath))
window.project = atom.project
atom.keymap.keyBindings = _.clone(keyBindingsToRestore)
window.resetTimeouts()
atom.packages.packageStates = {}
@ -66,10 +65,6 @@ beforeEach ->
resolvePackagePath(packageName)
resolvePackagePath = _.bind(spy.originalValue, atom.packages)
# used to reset keymap after each spec
bindingSetsToRestore = _.clone(atom.keymap.bindingSets)
bindingSetsByFirstKeystrokeToRestore = _.clone(atom.keymap.bindingSetsByFirstKeystroke)
# prevent specs from modifying Atom's menus
spyOn(atom.menu, 'sendToBrowserProcess')
@ -108,8 +103,6 @@ beforeEach ->
addCustomMatchers(this)
afterEach ->
atom.keymap.bindingSets = bindingSetsToRestore
atom.keymap.bindingSetsByFirstKeystroke = bindingSetsByFirstKeystrokeToRestore
atom.deactivatePackages()
atom.menu.template = []

View File

@ -1,54 +1,53 @@
measure 'spec suite require time', ->
{_, fs, Git} = require 'atom'
path = require 'path'
require './spec-helper'
{_, fs, Git} = require 'atom'
path = require 'path'
require './spec-helper'
requireSpecs = (specDirectory, specType) ->
for specFilePath in fs.listTreeSync(specDirectory) when /-spec\.coffee$/.test specFilePath
require specFilePath
requireSpecs = (specDirectory, specType) ->
for specFilePath in fs.listTreeSync(specDirectory) when /-spec\.coffee$/.test specFilePath
require specFilePath
# Set spec directory on spec for setting up the project in spec-helper
setSpecDirectory(specDirectory)
# Set spec directory on spec for setting up the project in spec-helper
setSpecDirectory(specDirectory)
setSpecField = (name, value) ->
specs = jasmine.getEnv().currentRunner().specs()
return if specs.length is 0
for index in [specs.length-1..0]
break if specs[index][name]?
specs[index][name] = value
setSpecField = (name, value) ->
specs = jasmine.getEnv().currentRunner().specs()
return if specs.length is 0
for index in [specs.length-1..0]
break if specs[index][name]?
specs[index][name] = value
setSpecType = (specType) ->
setSpecField('specType', specType)
setSpecType = (specType) ->
setSpecField('specType', specType)
setSpecDirectory = (specDirectory) ->
setSpecField('specDirectory', specDirectory)
setSpecDirectory = (specDirectory) ->
setSpecField('specDirectory', specDirectory)
runAllSpecs = ->
# Only run core specs when resource path is the Atom repository
if Git.exists(window.resourcePath)
requireSpecs(path.join(window.resourcePath, 'spec'))
setSpecType('core')
runAllSpecs = ->
# Only run core specs when resource path is the Atom repository
if Git.exists(window.resourcePath)
requireSpecs(path.join(window.resourcePath, 'spec'))
setSpecType('core')
fixturesPackagesPath = path.join(__dirname, 'fixtures', 'packages')
packagePaths = atom.getAvailablePackageNames().map (packageName) -> atom.resolvePackagePath(packageName)
packagePaths = _.groupBy packagePaths, (packagePath) ->
if packagePath.indexOf("#{fixturesPackagesPath}#{path.sep}") is 0
'fixtures'
else if packagePath.indexOf("#{window.resourcePath}#{path.sep}") is 0
'bundled'
else
'user'
fixturesPackagesPath = path.join(__dirname, 'fixtures', 'packages')
packagePaths = atom.getAvailablePackageNames().map (packageName) -> atom.resolvePackagePath(packageName)
packagePaths = _.groupBy packagePaths, (packagePath) ->
if packagePath.indexOf("#{fixturesPackagesPath}#{path.sep}") is 0
'fixtures'
else if packagePath.indexOf("#{window.resourcePath}#{path.sep}") is 0
'bundled'
else
'user'
# Run bundled package specs
requireSpecs(path.join(packagePath, 'spec')) for packagePath in packagePaths.bundled ? []
setSpecType('bundled')
# Run bundled package specs
requireSpecs(path.join(packagePath, 'spec')) for packagePath in packagePaths.bundled ? []
setSpecType('bundled')
# Run user package specs
requireSpecs(path.join(packagePath, 'spec')) for packagePath in packagePaths.user ? []
setSpecType('user')
# Run user package specs
requireSpecs(path.join(packagePath, 'spec')) for packagePath in packagePaths.user ? []
setSpecType('user')
if specDirectory = atom.getLoadSettings().specDirectory
requireSpecs(specDirectory)
setSpecType('user')
else
runAllSpecs()
if specDirectory = atom.getLoadSettings().specDirectory
requireSpecs(specDirectory)
setSpecType('user')
else
runAllSpecs()

View File

@ -13,11 +13,11 @@ describe 'TextBuffer', ->
buffer = project.bufferForPathSync(filePath)
afterEach ->
buffer?.release()
buffer?.destroy()
describe 'constructor', ->
beforeEach ->
buffer.release()
buffer.destroy()
buffer = null
describe "when given a path", ->
@ -311,8 +311,8 @@ describe 'TextBuffer', ->
it "returns false until the buffer is fully loaded", ->
buffer.release()
filePath = temp.openSync('atom').path
buffer = new TextBuffer({project, filePath})
buffer = new TextBuffer({filePath: temp.openSync('atom').path})
project.addBuffer(buffer)
expect(buffer.isModified()).toBeFalsy()
@ -554,35 +554,6 @@ describe 'TextBuffer', ->
waitsFor ->
changeHandler.callCount > 0
describe ".getRelativePath()", ->
[filePath, newPath, bufferToChange, eventHandler] = []
beforeEach ->
filePath = path.join(__dirname, "fixtures", "atom-manipulate-me")
newPath = "#{filePath}-i-moved"
fs.writeFileSync(filePath, "")
bufferToChange = project.bufferForPathSync(filePath)
eventHandler = jasmine.createSpy('eventHandler')
bufferToChange.on 'path-changed', eventHandler
afterEach ->
bufferToChange.destroy()
fs.removeSync(filePath) if fs.existsSync(filePath)
fs.removeSync(newPath) if fs.existsSync(newPath)
it "updates when the text buffer's file is moved", ->
expect(bufferToChange.getRelativePath()).toBe('atom-manipulate-me')
jasmine.unspy(window, "setTimeout")
eventHandler.reset()
fs.moveSync(filePath, newPath)
waitsFor "buffer path change", ->
eventHandler.callCount > 0
runs ->
expect(bufferToChange.getRelativePath()).toBe('atom-manipulate-me-i-moved')
describe ".getTextInRange(range)", ->
describe "when range is empty", ->
it "returns an empty string", ->
@ -926,22 +897,28 @@ describe 'TextBuffer', ->
expect(buffer.getText()).toBe "\ninitialtexthello\n1\n2\n"
describe "serialization", ->
buffer2 = null
[buffer2, project2] = []
beforeEach ->
buffer.destroy()
filePath = temp.openSync('atom').path
fs.writeFileSync(filePath, "words")
buffer = project.bufferForPathSync(filePath)
afterEach ->
buffer2?.release()
project2?.destroy()
describe "when the serialized buffer had no unsaved changes", ->
it "loads the current contents of the file at the serialized path", ->
expect(buffer.isModified()).toBeFalsy()
state = buffer.serialize()
state.get('text').insertTextAtPoint([0, 0], 'simulate divergence of on-disk contents from serialized contents')
project2 = atom.replicate().get('project')
buffer2 = project2.getBuffers()[0]
buffer2 = deserialize(state, {project})
waitsFor ->
buffer2.cachedDiskContents
waitsForPromise ->
buffer2.load()
runs ->
expect(buffer2.isModified()).toBeFalsy()
@ -951,18 +928,11 @@ describe 'TextBuffer', ->
describe "when the serialized buffer had unsaved changes", ->
describe "when the disk contents were changed since serialization", ->
it "loads the disk contents instead of the previous unsaved state", ->
buffer.release()
filePath = temp.openSync('atom').path
fs.writeFileSync(filePath, "words")
{buffer} = project.openSync(filePath)
buffer.setText("BUFFER CHANGE")
state = buffer.serialize()
expect(state.getObject('text')).toBe 'BUFFER CHANGE'
fs.writeFileSync(filePath, "DISK CHANGE")
buffer2 = deserialize(state, {project})
project2 = atom.replicate().get('project')
buffer2 = project2.getBuffers()[0]
waitsFor ->
buffer2.cachedDiskContents
@ -976,14 +946,14 @@ describe 'TextBuffer', ->
it "restores the previous unsaved state of the buffer", ->
previousText = buffer.getText()
buffer.setText("abc")
buffer.retain()
state = buffer.serialize()
expect(state.getObject('text')).toBe 'abc'
buffer.getState().serializeForPersistence()
project2 = atom.replicate().get('project')
buffer2 = project2.getBuffers()[0]
buffer2 = deserialize(state, {project})
waitsFor ->
buffer2.cachedDiskContents
waitsForPromise ->
buffer2.load()
runs ->
expect(buffer2.getPath()).toBe(buffer.getPath())
@ -999,10 +969,10 @@ describe 'TextBuffer', ->
buffer = project.bufferForPathSync()
buffer.setText("abc")
state = buffer.serialize()
state = buffer.getState().clone()
expect(state.get('path')).toBeUndefined()
expect(state.getObject('text')).toBe 'abc'
buffer2 = deserialize(state)
buffer2 = project.addBuffer(new TextBuffer(state))
expect(buffer2.getPath()).toBeUndefined()
expect(buffer2.getText()).toBe("abc")

View File

@ -350,7 +350,7 @@ describe "TokenizedBuffer", ->
describe "when the buffer contains surrogate pairs", ->
beforeEach ->
atom.activatePackage('language-javascript', sync: true)
buffer = project.buildBufferSync 'sample-with-pairs.js'
buffer = project.bufferForPathSync 'sample-with-pairs.js'
buffer.setText """
'abc\uD835\uDF97def'
//\uD835\uDF97xyz

View File

@ -126,11 +126,10 @@ class Atom
deserializeProject: ->
Project = require './project'
state = @getWindowState()
@project = deserialize(state.get('project'))
unless @project?
@project = new Project(@getLoadSettings().initialPath)
state.set('project', @project.getState())
@project = @getWindowState('project')
unless @project instanceof Project
@project = new Project(path: @getLoadSettings().initialPath)
@setWindowState('project', @project)
deserializeRootView: ->
RootView = require './root-view'
@ -301,6 +300,7 @@ class Atom
doc = Document.deserialize(documentState) if documentState?
doc ?= Document.create()
doc.registerModelClasses(require('./text-buffer'), require('./project'))
# TODO: Remove this when everything is using telepath models
if @site?
@site.setRootDocument(doc)
@ -322,6 +322,10 @@ class Atom
else
@windowState
# Private: Returns a replicated copy of the current state.
replicate: ->
@getWindowState().replicate()
crashMainProcess: ->
remote.process.crash()
@ -333,7 +337,7 @@ class Atom
@rootView.trigger 'beep'
requireUserInitScript: ->
userInitScriptPath = path.join(@config.configDirPath, "user.coffee")
userInitScriptPath = path.join(@getConfigDirPath(), "user.coffee")
try
require userInitScriptPath if fs.isFileSync(userInitScriptPath)
catch error

View File

@ -1,66 +0,0 @@
{$} = require './space-pen-extensions'
_ = require 'underscore-plus'
fs = require 'fs-plus'
{specificity} = require 'clear-cut'
PEG = require 'pegjs'
### Internal ###
module.exports =
class BindingSet
@parser: null
selector: null
commandsByKeystrokes: null
parser: null
name: null
constructor: (selector, commandsByKeystrokes, @index, @name) ->
keystrokePattern = fs.readFileSync(require.resolve('./keystroke-pattern.pegjs'), 'utf8')
BindingSet.parser ?= PEG.buildParser(keystrokePattern)
@specificity = specificity(selector)
@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
null
matchesKeystrokePrefix: (event) ->
eventKeystrokes = event.keystrokes.split(' ')
for keystrokes, command of @commandsByKeystrokes
bindingKeystrokes = keystrokes.split(' ')
continue unless eventKeystrokes.length < bindingKeystrokes.length
return true if _.isEqual(eventKeystrokes, bindingKeystrokes[0...eventKeystrokes.length])
false
normalizeCommandsByKeystrokes: (commandsByKeystrokes) ->
normalizedCommandsByKeystrokes = {}
for keystrokes, command of commandsByKeystrokes
normalizedCommandsByKeystrokes[@normalizeKeystrokes(keystrokes)] = command
normalizedCommandsByKeystrokes
normalizeKeystrokes: (keystrokes) ->
normalizedKeystrokes = keystrokes.split(/\s+/).map (keystroke) =>
@normalizeKeystroke(keystroke)
normalizedKeystrokes.join(' ')
normalizeKeystroke: (keystroke) ->
keys = BindingSet.parser.parse(keystroke)
modifiers = keys[0...-1]
modifiers.sort()
[modifiers..., _.last(keys)].join('-')

View File

@ -22,7 +22,7 @@ class ApplicationMenu
# The Object which describes the menu to display.
# * keystrokesByCommand:
# An Object where the keys are commands and the values are Arrays containing
# the keystrokes.
# the keystroke.
update: (template, keystrokesByCommand) ->
@translateTemplate(template, keystrokesByCommand)
@substituteVersion(template)
@ -97,14 +97,14 @@ class ApplicationMenu
]
]
# Private: Combines a menu template with the appropriate keystrokes.
# Private: Combines a menu template with the appropriate keystroke.
#
# * template:
# An Object conforming to atom-shell's menu api but lacking accelerator and
# click properties.
# * keystrokesByCommand:
# An Object where the keys are commands and the values are Arrays containing
# the keystrokes.
# the keystroke.
#
# Returns a complete menu configuration object for atom-shell's menu API.
translateTemplate: (template, keystrokesByCommand) ->
@ -123,15 +123,15 @@ class ApplicationMenu
# The name of the command.
# * keystrokesByCommand:
# An Object where the keys are commands and the values are Arrays containing
# the keystrokes.
# the keystroke.
#
# Returns a String containing the keystroke in a format that can be interpreted
# by atom shell to provide nice icons where available.
acceleratorForCommand: (command, keystrokesByCommand) ->
keystroke = keystrokesByCommand[command]?[0]
return null unless keystroke
firstKeystroke = keystrokesByCommand[command]?[0]
return null unless firstKeystroke
modifiers = keystroke.split('-')
modifiers = firstKeystroke.split('-')
key = modifiers.pop()
modifiers.push("Shift") if key != key.toLowerCase()

View File

@ -62,7 +62,7 @@ delegate.browserMainParts.preMainMessageLoopRun = ->
AtomApplication = require './atom-application'
AtomApplication.open(args)
console.log("App load time: #{Date.now() - startTime}ms")
console.log("App load time: #{Date.now() - startTime}ms") unless args.test
global.devResourcePath = path.join(app.getHomeDir(), 'github', 'atom')

View File

@ -5,5 +5,5 @@ module.exports =
unobserveConfig: ->
if @configSubscriptions?
subscription.cancel() for keyPath, subscription of @configSubscriptions
subscription.off() for keyPath, subscription of @configSubscriptions
@configSubscriptions = null

View File

@ -216,8 +216,7 @@ class Config
callback(value, {previous})
eventName = "updated.#{keyPath.replace(/\./, '-')}"
subscription = { cancel: => @off eventName, updateCallback }
@on eventName, updateCallback
subscription = @on eventName, updateCallback
callback(value) if options.callNow ? true
subscription

View File

@ -466,3 +466,15 @@ class Cursor
# Returns an {Array} of {String}s.
getScopes: ->
@editSession.scopesForBufferPosition(@getBufferPosition())
# Public: Returns true if this cursor has no non-whitespace characters before
# its current position.
hasPrecedingCharactersOnLine: ->
bufferPosition = @getBufferPosition()
line = @editSession.lineForBufferRow(bufferPosition.row)
firstCharacterColumn = line.search(/\S/)
if firstCharacterColumn is -1
false
else
bufferPosition.column > firstCharacterColumn

View File

@ -365,7 +365,9 @@ class DisplayBuffer
setTabLength: (tabLength) ->
@tokenizedBuffer.setTabLength(tabLength)
# Retrieves the grammar for the buffer.
# Get the grammar for this buffer.
#
# Returns the current {TextMateGrammar} or the {NullGrammar}.
getGrammar: ->
@tokenizedBuffer.grammar

View File

@ -335,9 +335,6 @@ class EditSession
# {Delegates to: TextBuffer.getPath}
getPath: -> @buffer.getPath()
# {Delegates to: TextBuffer.getRelativePath}
getRelativePath: -> @buffer.getRelativePath()
# {Delegates to: TextBuffer.getText}
getText: -> @buffer.getText()
@ -567,8 +564,11 @@ class EditSession
pasteText: (options={}) ->
[text, metadata] = atom.pasteboard.read()
containsNewlines = text.indexOf('\n') isnt -1
if atom.config.get('editor.normalizeIndentOnPaste') and metadata
options.indentBasis ?= metadata.indentBasis
if !@getCursor().hasPrecedingCharactersOnLine() or containsNewlines
options.indentBasis ?= metadata.indentBasis
@insertText(text, options)

View File

@ -104,7 +104,7 @@ class Editor extends View
@edit(editSession)
else if @mini
@edit(new EditSession
buffer: new TextBuffer
buffer: TextBuffer.createAsRoot()
softWrap: false
tabLength: 2
softTabs: true
@ -586,8 +586,10 @@ class Editor extends View
@showIndentGuide = showIndentGuide
@resetDisplay()
# {Delegates to: TextBuffer.checkoutHead}
checkoutHead: -> @getBuffer().checkoutHead()
# Checkout the HEAD revision of this editor's file.
checkoutHead: ->
if path = @getPath()
atom.project.getRepo()?.checkoutHead(path)
# {Delegates to: EditSession.setText}
setText: (text) -> @activeEditSession.setText(text)
@ -598,9 +600,6 @@ class Editor extends View
# {Delegates to: EditSession.getPath}
getPath: -> @activeEditSession?.getPath()
# {Delegates to: EditSession.getRelativePath}
getRelativePath: -> @activeEditSession?.getRelativePath()
# {Delegates to: TextBuffer.getLineCount}
getLineCount: -> @getBuffer().getLineCount()
@ -1807,13 +1806,6 @@ class Editor extends View
else
'&nbsp;'
bindToKeyedEvent: (key, event, callback) ->
binding = {}
binding[key] = event
atom.keymap.bindKeys '.editor', binding
@on event, =>
callback(this, event)
replaceSelectedText: (replaceFn) ->
selection = @getSelection()
return false if selection.isEmpty()

View File

@ -71,8 +71,7 @@ class Git
@refreshStatus()
if project?
@subscribeToBuffer(buffer) for buffer in project.getBuffers()
@subscribe project, 'buffer-created', (buffer) => @subscribeToBuffer(buffer)
@subscribe project.buffers.onEach (buffer) => @subscribeToBuffer(buffer)
# Private: Subscribes to buffer events.
subscribeToBuffer: (buffer) ->

48
src/key-binding.coffee Normal file
View File

@ -0,0 +1,48 @@
{$} = require './space-pen-extensions'
_ = require 'underscore-plus'
fs = require 'fs-plus'
{specificity} = require 'clear-cut'
PEG = require 'pegjs'
### Internal ###
module.exports =
class KeyBinding
@parser: null
@currentIndex: 1
@normalizeKeystroke: (keystroke) ->
normalizedKeystroke = keystroke.split(/\s+/).map (keystroke) =>
keys = @getParser().parse(keystroke)
modifiers = keys[0...-1]
modifiers.sort()
[modifiers..., _.last(keys)].join('-')
normalizedKeystroke.join(' ')
@getParser: ->
if not KeyBinding.parser
keystrokePattern = fs.readFileSync(require.resolve('./keystroke-pattern.pegjs'), 'utf8')
KeyBinding.parser = PEG.buildParser(keystrokePattern)
KeyBinding.parser
constructor: (source, command, keystroke, selector) ->
@source = source
@command = command
@keystroke = KeyBinding.normalizeKeystroke(keystroke)
@selector = selector.replace(/!important/g, '')
@specificity = specificity(selector)
@index = KeyBinding.currentIndex++
matches: (keystroke) ->
multiKeystroke = /\s/.test keystroke
if multiKeystroke
keystroke == @keystroke
else
keystroke.split(' ')[0] == @keystroke.split(' ')[0]
compare: (keyBinding) ->
if keyBinding.specificity == @specificity
keyBinding.index - @index
else
keyBinding.specificity - @specificity

View File

@ -3,7 +3,7 @@ _ = require 'underscore-plus'
fs = require 'fs-plus'
path = require 'path'
CSON = require 'season'
BindingSet = require './binding-set'
KeyBinding = require './key-binding'
{Emitter} = require 'emissary'
Modifiers = ['alt', 'control', 'ctrl', 'shift', 'meta']
@ -25,166 +25,46 @@ module.exports =
class Keymap
Emitter.includeInto(this)
bindingSets: null
nextBindingSetIndex: 0
bindingSetsByFirstKeystroke: null
queuedKeystrokes: null
constructor: ({@resourcePath, @configDirPath})->
@bindingSets = []
@bindingSetsByFirstKeystroke = {}
@keyBindings = []
loadBundledKeymaps: ->
@loadDirectory(path.join(@resourcePath, 'keymaps'))
@emit('bundled-keymaps-loaded')
# Public: Returns an array of all {KeyBinding}s.
getKeyBindings: ->
_.clone(@keyBindings)
loadUserKeymap: ->
userKeymapPath = CSON.resolve(path.join(@configDirPath, 'keymap'))
@load(userKeymapPath) if userKeymapPath
loadDirectory: (directoryPath) ->
@load(filePath) for filePath in fs.listSync(directoryPath, ['.cson', '.json'])
load: (path) ->
@add(path, CSON.readFileSync(path))
add: (args...) ->
name = args.shift() if args.length > 1
keymap = args.shift()
for selector, bindings of keymap
@bindKeys(name, selector, bindings)
remove: (name) ->
for bindingSet in @bindingSets.filter((bindingSet) -> bindingSet.name is name)
_.remove(@bindingSets, bindingSet)
for keystrokes of bindingSet.commandsByKeystrokes
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.
# Public: Returns a array of {KeyBinding}s (sorted by selector specificity)
# that match a keystroke and element.
#
# * filePath:
# The absolute path from which the keymap was loaded
# * keystroke:
# The string representing the keys pressed (e.g. ctrl-P).
# * element:
# The DOM node that will match a {KeyBinding}'s selector.
keyBindingsForKeystrokeMatchingElement: (keystroke, element) ->
keyBindings = @keyBindingsForKeystroke(keystroke)
@keyBindingsMatchingElement(element, keyBindings)
# Public: Returns an array of {KeyBinding}s that match a keystroke
# * keystroke:
# The string representing the keys pressed (e.g. ctrl-P)
keyBindingsForKeystroke: (keystroke) ->
keystroke = KeyBinding.normalizeKeystroke(keystroke)
keyBindings = @keyBindings.filter (keyBinding) -> keyBinding.matches(keystroke)
# Public: Returns a array of {KeyBinding}s (sorted by selector specificity)
# whos selector matches the element.
#
# 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
# * element:
# The DOM node that will match a {KeyBinding}'s selector.
keyBindingsMatchingElement: (element, keyBindings=@keyBindings) ->
keyBindings = keyBindings.filter ({selector}) -> $(element).closest(selector).length > 0
keyBindings.sort (a, b) -> a.compare(b)
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
bindingSet = new BindingSet(selector, bindings, @nextBindingSetIndex++, name)
@bindingSets.unshift(bindingSet)
for keystrokes of bindingSet.commandsByKeystrokes
keystroke = keystrokes.split(' ')[0] # only index by first keystroke
@bindingSetsByFirstKeystroke[keystroke] ?= []
@bindingSetsByFirstKeystroke[keystroke].push(bindingSet)
unbindKeys: (selector, bindings) ->
bindingSet = _.detect @bindingSets, (bindingSet) ->
bindingSet.selector is selector and bindingSet.bindings is bindings
if bindingSet
_.remove(@bindingSets, bindingSet)
bindingsForElement: (element) ->
keystrokeMap = {}
currentNode = $(element)
while currentNode.length
bindingSets = @bindingSetsForNode(currentNode)
_.defaults(keystrokeMap, set.commandsByKeystrokes) for set in bindingSets
currentNode = currentNode.parent()
keystrokeMap
handleKeyEvent: (event) =>
event.keystrokes = @multiKeystrokeStringForEvent(event)
isMultiKeystroke = @queuedKeystrokes?
@queuedKeystrokes = null
firstKeystroke = event.keystrokes.split(' ')[0]
bindingSetsForFirstKeystroke = @bindingSetsByFirstKeystroke[firstKeystroke]
if bindingSetsForFirstKeystroke?
currentNode = $(event.target)
currentNode = rootView if currentNode is $('body')[0]
while currentNode.length
candidateBindingSets = @bindingSetsForNode(currentNode, bindingSetsForFirstKeystroke)
for bindingSet in candidateBindingSets
command = bindingSet.commandForEvent(event)
if command is 'native!'
return true
else if command
continue if @triggerCommandEvent(event, command)
return false
else if command == false
return false
if bindingSet.matchesKeystrokePrefix(event)
@queuedKeystrokes = event.keystrokes
return false
currentNode = currentNode.parent()
return false if isMultiKeystroke
return false if firstKeystroke is 'tab'
bindingSetsForNode: (node, candidateBindingSets = @bindingSets) ->
bindingSets = candidateBindingSets.filter (set) -> node.is(set.selector)
bindingSets.sort (a, b) ->
if b.specificity == a.specificity
b.index - a.index
else
b.specificity - a.specificity
triggerCommandEvent: (keyEvent, commandName) ->
keyEvent.target = rootView[0] if keyEvent.target == document.body and window.rootView
commandEvent = $.Event(commandName)
commandEvent.keyEvent = keyEvent
aborted = false
commandEvent.abortKeyBinding = ->
@stopImmediatePropagation()
aborted = true
$(keyEvent.target).trigger(commandEvent)
aborted
multiKeystrokeStringForEvent: (event) ->
currentKeystroke = @keystrokeStringForEvent(event)
if @queuedKeystrokes
if currentKeystroke in Modifiers
@queuedKeystrokes
else
@queuedKeystrokes + ' ' + currentKeystroke
else
currentKeystroke
keystrokeStringForEvent: (event) ->
# Public: Returns a keystroke string derived from an event.
# * event:
# A DOM or jQuery event
# * previousKeystroke:
# An optional string used for multiKeystrokes
keystrokeStringForEvent: (event, previousKeystroke) ->
if event.originalEvent.keyIdentifier.indexOf('U+') == 0
hexCharCode = event.originalEvent.keyIdentifier[2..]
charCode = parseInt(hexCharCode, 16)
@ -207,16 +87,73 @@ class Keymap
else
key = key.toLowerCase()
[modifiers..., key].join('-')
keystroke = [modifiers..., key].join('-')
keystrokesByCommandForSelector: (selector)->
keystrokesByCommand = {}
for bindingSet in @bindingSets
for keystroke, command of bindingSet.commandsByKeystrokes
continue if selector? and selector != bindingSet.selector
keystrokesByCommand[command] ?= []
keystrokesByCommand[command].push keystroke
keystrokesByCommand
if previousKeystroke
if keystroke in Modifiers
previousKeystroke
else
"#{previousKeystroke} #{keystroke}"
else
keystroke
loadBundledKeymaps: ->
@loadDirectory(path.join(@resourcePath, 'keymaps'))
@emit('bundled-keymaps-loaded')
loadUserKeymap: ->
userKeymapPath = CSON.resolve(path.join(@configDirPath, 'keymap'))
@load(userKeymapPath) if userKeymapPath
loadDirectory: (directoryPath) ->
@load(filePath) for filePath in fs.listSync(directoryPath, ['.cson', '.json'])
load: (path) ->
@add(path, CSON.readFileSync(path))
add: (source, keyMappingsBySelector) ->
for selector, keyMappings of keyMappingsBySelector
@bindKeys(source, selector, keyMappings)
remove: (source) ->
@keyBindings = @keyBindings.filter (keyBinding) -> keyBinding.source isnt source
bindKeys: (source, selector, keyMappings) ->
for keystroke, command of keyMappings
@keyBindings.push new KeyBinding(source, command, keystroke, selector)
handleKeyEvent: (event) ->
element = event.target
element = rootView if element == document.body
keystroke = @keystrokeStringForEvent(event, @queuedKeystroke)
keyBindings = @keyBindingsForKeystrokeMatchingElement(keystroke, element)
if keyBindings.length == 0 and @queuedKeystroke
@queuedKeystroke = null
return false
else
@queuedKeystroke = null
for keyBinding in keyBindings
partialMatch = keyBinding.keystroke isnt keystroke
if partialMatch
@queuedKeystroke = keystroke
shouldBubble = false
else
if keyBinding.command is 'native!'
shouldBubble = true
else if @triggerCommandEvent(element, keyBinding.command)
shouldBubble = false
break if shouldBubble?
shouldBubble ? true
triggerCommandEvent: (element, commandName) ->
commandEvent = $.Event(commandName)
commandEvent.abortKeyBinding = -> commandEvent.stopImmediatePropagation()
$(element).trigger(commandEvent)
not commandEvent.isImmediatePropagationStopped()
isAscii: (charCode) ->
0 <= charCode <= 127

View File

@ -14,7 +14,7 @@ class MenuManager
# Private:
constructor: ({@resourcePath}) ->
@template = []
atom.keymap.on 'bundled-keymaps-loaded', => @loadCoreItems()
atom.keymap.on 'bundled-keymaps-loaded', => @loadPlatformItems()
# Public: Adds the given item definition to the existing template.
#
@ -29,33 +29,34 @@ class MenuManager
# Public: Refreshes the currently visible menu.
update: ->
keystrokesByCommand = atom.keymap.keystrokesByCommandForSelector('body')
_.extend(keystrokesByCommand, atom.keymap.keystrokesByCommandForSelector('.editor'))
_.extend(keystrokesByCommand, atom.keymap.keystrokesByCommandForSelector('.editor:not(.mini)'))
keystrokesByCommand = {}
selectors = ['body', '.editor', '.editor:not(.mini)']
for binding in atom.keymap.getKeyBindings() when binding.selector in selectors
keystrokesByCommand[binding.command] ?= []
keystrokesByCommand[binding.command].push binding.keystroke
@sendToBrowserProcess(@template, keystrokesByCommand)
# Private
loadCoreItems: ->
loadPlatformItems: ->
menusDirPath = path.join(@resourcePath, 'menus')
menuPaths = fs.listSync(menusDirPath, ['cson', 'json'])
for menuPath in menuPaths
data = CSON.readFileSync(menuPath)
@add(data.menu)
platformMenuPath = fs.resolve(menusDirPath, process.platform, ['cson', 'json'])
data = CSON.readFileSync(platformMenuPath)
@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) ->
item = _.deepClone(item)
if item.submenu? and match = _.find(menu, (i) -> i.submenu? and i.label == item.label)
if item.submenu? and match = _.find(menu, (i) => i.submenu? and @normalizeLabel(i.label) == @normalizeLabel(item.label))
@merge(match.submenu, i) for i in item.submenu
else
menu.push(item) unless _.find(menu, (i) -> i.label == item.label)
menu.push(item) unless _.find(menu, (i) => @normalizeLabel(i.label) == @normalizeLabel(item.label))
# Private: OSX can't handle displaying accelerators for multiple keystrokes.
# If they are sent across, it will stop processing accelerators for the rest
# of the menu items.
filterMultipleKeystrokes: (keystrokesByCommand) ->
filterMultipleKeystroke: (keystrokesByCommand) ->
filtered = {}
for key, bindings of keystrokesByCommand
for binding in bindings
@ -67,5 +68,12 @@ class MenuManager
# Private
sendToBrowserProcess: (template, keystrokesByCommand) ->
keystrokesByCommand = @filterMultipleKeystrokes(keystrokesByCommand)
keystrokesByCommand = @filterMultipleKeystroke(keystrokesByCommand)
ipc.sendChannel 'update-application-menu', template, keystrokesByCommand
# Private
normalizeLabel: (label) ->
if process.platform is 'win32'
label.replace(/\&/g, '')
else
label

View File

@ -5,7 +5,6 @@ _ = require 'underscore-plus'
fs = require 'fs-plus'
Q = require 'q'
telepath = require 'telepath'
{Range} = telepath
TextBuffer = require './text-buffer'
EditSession = require './edit-session'
@ -19,16 +18,12 @@ Git = require './git'
# Ultimately, a project is a git directory that's been opened. It's a collection
# of directories and files that you can operate on.
module.exports =
class Project
class Project extends telepath.Model
Emitter.includeInto(this)
@acceptsDocuments: true
@version: 1
registerDeserializer(this)
# Private:
@deserialize: (state) -> new Project(state)
@properties
buffers: []
path: null
# Public: Find the local path for the given repository URL.
@pathForRepositoryUrl: (repoUrl) ->
@ -36,10 +31,18 @@ class Project
repoName = repoName.replace(/\.git$/, '')
path.join(atom.config.get('core.projectHome'), repoName)
rootDirectory: null
editSessions: null
ignoredPathRegexes: null
openers: null
# Private: Called by telepath.
attached: ->
for buffer in @buffers.getValues()
buffer.once 'destroyed', => @removeBuffer(buffer)
@openers = []
@editSessions = []
@setPath(@path)
# Private: Called by telepath.
beforePersistence: ->
@destroyUnretainedBuffers()
# Public:
registerOpener: (opener) -> @openers.push(opener)
@ -59,51 +62,10 @@ class Project
@repo.destroy()
@repo = null
# Public: Establishes a new project at a given path.
#
# path - The {String} name of the path
constructor: (pathOrState) ->
@openers = []
@editSessions = []
@buffers = []
if pathOrState instanceof telepath.Document
@state = pathOrState
if projectPath = @state.remove('path')
@setPath(projectPath)
else
@setPath(@constructor.pathForRepositoryUrl(@state.get('repoUrl')))
@state.get('buffers').each (bufferState) =>
if buffer = deserialize(bufferState, project: this)
@addBuffer(buffer, updateState: false)
else
@state = atom.site.createDocument(deserializer: @constructor.name, version: @constructor.version, buffers: [])
@setPath(pathOrState)
@state.get('buffers').on 'changed', ({index, insertedValues, removedValues, siteId}) =>
return if siteId is @state.siteId
for removedBuffer in removedValues
@removeBufferAtIndex(index, updateState: false)
for insertedBuffer, i in insertedValues
@addBufferAtIndex(deserialize(insertedBuffer, project: this), index + i, updateState: false)
# Private:
serialize: ->
state = @state.clone()
state.set('path', @getPath())
@destroyUnretainedBuffers()
state.set('buffers', buffer.serialize() for buffer in @getBuffers())
state
# Private:
destroyUnretainedBuffers: ->
buffer.destroy() for buffer in @getBuffers() when not buffer.isRetained()
# Public: ?
getState: -> @state
# Public: Returns the {Git} repository if available.
getRepo: -> @repo
@ -113,6 +75,7 @@ class Project
# Public: Sets the project's fullpath.
setPath: (projectPath) ->
@path = projectPath
@rootDirectory?.off()
@destroyRepo()
@ -125,9 +88,6 @@ class Project
else
@rootDirectory = null
if originUrl = @repo?.getOriginUrl()
@state.set('repoUrl', originUrl)
@emit "path-changed"
# Public: Returns the name of the root directory.
@ -220,20 +180,18 @@ class Project
#
# Returns an {Array} of {TextBuffer}s.
getBuffers: ->
new Array(@buffers...)
new Array(@buffers.getValues()...)
isPathModified: (filePath) ->
absoluteFilePath = @resolve(filePath)
existingBuffer = _.find @buffers, (buffer) -> buffer.getPath() == absoluteFilePath
existingBuffer?.isModified()
@findBufferForPath(@resolve(filePath))?.isModified()
findBufferForPath: (filePath) ->
_.find @buffers.getValues(), (buffer) -> buffer.getPath() == filePath
# Private: Only to be used in specs
bufferForPathSync: (filePath) ->
absoluteFilePath = @resolve(filePath)
if filePath
existingBuffer = _.find @buffers, (buffer) -> buffer.getPath() == absoluteFilePath
existingBuffer = @findBufferForPath(absoluteFilePath) if filePath
existingBuffer ? @buildBufferSync(absoluteFilePath)
# Private: Given a file path, this retrieves or creates a new {TextBuffer}.
@ -246,9 +204,7 @@ class Project
# Returns a promise that resolves to the {TextBuffer}.
bufferForPath: (filePath) ->
absoluteFilePath = @resolve(filePath)
if absoluteFilePath
existingBuffer = _.find @buffers, (buffer) -> buffer.getPath() == absoluteFilePath
existingBuffer = @findBufferForPath(absoluteFilePath) if absoluteFilePath
Q(existingBuffer ? @buildBuffer(absoluteFilePath))
# Private:
@ -257,9 +213,9 @@ class Project
# Private: DEPRECATED
buildBufferSync: (absoluteFilePath) ->
buffer = new TextBuffer({project: this, filePath: absoluteFilePath})
buffer.loadSync()
buffer = new TextBuffer({filePath: absoluteFilePath})
@addBuffer(buffer)
buffer.loadSync()
buffer
# Private: Given a file path, this sets its {TextBuffer}.
@ -269,10 +225,11 @@ class Project
#
# Returns a promise that resolves to the {TextBuffer}.
buildBuffer: (absoluteFilePath) ->
buffer = new TextBuffer({project: this, filePath: absoluteFilePath})
buffer.load().then (buffer) =>
@addBuffer(buffer)
buffer
buffer = new TextBuffer({filePath: absoluteFilePath})
@addBuffer(buffer)
buffer.load()
.then((buffer) -> buffer)
.catch(=> @removeBuffer(buffer))
# Private:
addBuffer: (buffer, options={}) ->
@ -280,9 +237,10 @@ class Project
# Private:
addBufferAtIndex: (buffer, index, options={}) ->
@buffers[index] = buffer
@state.get('buffers').insert(index, buffer.getState()) if options.updateState ? true
buffer = @buffers.insert(index, buffer)
buffer.once 'destroyed', => @removeBuffer(buffer)
@emit 'buffer-created', buffer
buffer
# Private: Removes a {TextBuffer} association from the project.
#
@ -294,7 +252,6 @@ class Project
# Private:
removeBufferAtIndex: (index, options={}) ->
[buffer] = @buffers.splice(index, 1)
@state.get('buffers')?.remove(index) if options.updateState ? true
buffer?.destroy()
# Public: Performs a search across all the files in the project.
@ -329,7 +286,7 @@ class Project
task.on 'scan:paths-searched', (numberOfPathsSearched) ->
options.onPathsSearched(numberOfPathsSearched)
for buffer in @buffers when buffer.isModified()
for buffer in @buffers.getValues() when buffer.isModified()
filePath = buffer.getPath()
matches = []
buffer.scan regex, (match) -> matches.push match
@ -346,7 +303,7 @@ class Project
replace: (regex, replacementText, filePaths, iterator) ->
deferred = Q.defer()
openPaths = (buffer.getPath() for buffer in @buffers)
openPaths = (buffer.getPath() for buffer in @buffers.getValues())
outOfProcessPaths = _.difference(filePaths, openPaths)
inProcessFinished = !openPaths.length
@ -364,7 +321,7 @@ class Project
task.on 'replace:path-replaced', iterator
for buffer in @buffers
for buffer in @buffers.getValues()
replacements = buffer.replace(regex, replacementText, iterator)
iterator({filePath: buffer.getPath(), replacements}) if replacements

View File

@ -1,32 +1,28 @@
crypto = require 'crypto'
_ = require 'underscore-plus'
{Emitter, Subscriber} = require 'emissary'
guid = require 'guid'
Q = require 'q'
{P} = require 'scandal'
telepath = require 'telepath'
_ = require 'underscore-plus'
File = require './file'
{Point, Range} = telepath
# Private: Represents the contents of a file.
#
# The `Buffer` is often associated with a {File}. However, this is not always
# the case, as a `Buffer` could be an unsaved chunk of text.
# The `TextBuffer` is often associated with a {File}. However, this is not always
# the case, as a `TextBuffer` could be an unsaved chunk of text.
module.exports =
class TextBuffer
class TextBuffer extends telepath.Model
Emitter.includeInto(this)
Subscriber.includeInto(this)
@acceptsDocuments: true
@version: 2
registerDeserializer(this)
@deserialize: (state, params) ->
buffer = new this(state, params)
buffer.load()
buffer
@properties
text: -> new telepath.String('', replicated: false)
filePath: null
relativePath: null
modifiedWhenLastPersisted: false
digestWhenLastPersisted: null
stoppedChangingDelay: 300
stoppedChangingTimeout: null
@ -36,34 +32,28 @@ class TextBuffer
file: null
refcount: 0
# Creates a new buffer.
#
# * optionsOrState - An {Object} or a telepath.Document
# + filePath - A {String} representing the file path
constructor: (optionsOrState={}, params={}) ->
if optionsOrState instanceof telepath.Document
{@project} = params
@state = optionsOrState
@id = @state.get('id')
filePath = @state.get('relativePath')
@text = @state.get('text')
@useSerializedText = @state.get('isModified') != false
else
{@project, filePath} = optionsOrState
@text = new telepath.String(initialText ? '', replicated: false)
@id = guid.create().toString()
@state = atom.site.createDocument
id: @id
deserializer: @constructor.name
version: @constructor.version
text: @text
constructor: ->
super
@loadWhenAttached = @getState()?
# Private: Called by telepath.
attached: ->
@loaded = false
@useSerializedText = @modifiedWhenLastPersisted != false
@subscribe @text, 'changed', @handleTextChange
@subscribe @text, 'marker-created', (marker) => @emit 'marker-created', marker
@subscribe @text, 'markers-updated', => @emit 'markers-updated'
@setPath(@project.resolve(filePath)) if @project
@setPath(@filePath)
@load() if @loadWhenAttached
# Private: Called by telepath.
beforePersistence: ->
@modifiedWhenLastPersisted = @isModified()
@digestWhenLastPersisted = @file?.getDigest()
loadSync: ->
@updateCachedDiskContentsSync()
@ -74,7 +64,7 @@ class TextBuffer
finishLoading: ->
@loaded = true
if @useSerializedText and @state.get('diskContentsDigest') == @file?.getDigest()
if @useSerializedText and @digestWhenLastPersisted is @file?.getDigest()
@emitModifiedStatusChanged(true)
else
@reload()
@ -92,10 +82,10 @@ class TextBuffer
destroy: ->
unless @destroyed
@cancelStoppedChangingTimeout()
@file?.off()
@unsubscribe()
@destroyed = true
@project?.removeBuffer(this)
@emit 'destroyed'
isRetained: -> @refcount > 0
@ -109,16 +99,6 @@ class TextBuffer
@destroy() unless @isRetained()
this
serialize: ->
state = @state.clone()
state.set('isModified', @isModified())
state.set('diskContentsDigest', @file.getDigest()) if @file
for marker in state.get('text').getMarkers() when marker.isRemote()
marker.destroy()
state
getState: -> @state
subscribeToFile: ->
@file.on "contents-changed", =>
@conflict = true if @isModified()
@ -140,7 +120,6 @@ class TextBuffer
@emitModifiedStatusChanged(@isModified())
@file.on "moved", =>
@state.set('relativePath', @project.relativize(@getPath()))
@emit "path-changed", this
### Public ###
@ -183,10 +162,7 @@ class TextBuffer
@file?.getPath()
getUri: ->
@getRelativePath()
getRelativePath: ->
@state.get('relativePath')
atom.project.relativize(@getPath())
# Sets the path for the file.
#
@ -202,7 +178,6 @@ class TextBuffer
else
@file = null
@state.set('relativePath', @project.relativize(path))
@emit "path-changed", this
# Retrieves the current buffer's file extension.
@ -411,12 +386,12 @@ class TextBuffer
saveAs: (path) ->
unless path then throw new Error("Can't save buffer with no file path")
@emit 'will-be-saved'
@emit 'will-be-saved', this
@setPath(path)
@cachedDiskContents = @getText()
@file.write(@getText())
@emitModifiedStatusChanged(false)
@emit 'saved'
@emit 'saved', this
# Identifies if the buffer was modified.
#
@ -653,12 +628,6 @@ class TextBuffer
return match[0][0] != '\t'
undefined
# Checks out the current `HEAD` revision of the file.
checkoutHead: ->
path = @getPath()
return unless path
@project.getRepo()?.checkoutHead(path)
### Internal ###
transact: (fn) -> @text.transact fn
@ -680,8 +649,11 @@ class TextBuffer
else
text
scheduleModifiedEvents: ->
cancelStoppedChangingTimeout: ->
clearTimeout(@stoppedChangingTimeout) if @stoppedChangingTimeout
scheduleModifiedEvents: ->
@cancelStoppedChangingTimeout()
stoppedChangingCallback = =>
@stoppedChangingTimeout = null
modifiedStatus = @isModified()
@ -700,7 +672,7 @@ class TextBuffer
console.log row, line, line.length
getDebugSnapshot: ->
lines = ['Buffer:']
lines = ['TextBuffer:']
for row in [0..@getLastRow()]
lines.push "#{row}: #{@lineForRow(row)}"
lines.join('\n')

View File

@ -36,7 +36,7 @@ class TokenizedBuffer
{ @buffer, tabLength } = optionsOrState
@state = atom.site.createDocument
deserializer: @constructor.name
bufferPath: @buffer.getRelativePath()
bufferPath: @buffer.getPath()
tabLength: tabLength ? atom.config.get('editor.tabLength') ? 2
@subscribe syntax, 'grammar-added grammar-updated', (grammar) =>
@ -48,7 +48,7 @@ class TokenizedBuffer
@on 'grammar-changed grammar-updated', => @resetTokenizedLines()
@subscribe @buffer, "changed", (e) => @handleBufferChange(e)
@subscribe @buffer, "path-changed", => @state.set('bufferPath', @buffer.getRelativePath())
@subscribe @buffer, "path-changed", => @state.set('bufferPath', @buffer.getPath())
@reloadGrammar()

View File

@ -54,7 +54,8 @@ class WindowEventHandler
@subscribeToCommand $(document), 'core:focus-previous', @focusPrevious
@subscribe $(document), 'keydown', atom.keymap.handleKeyEvent
@subscribe $(document), 'keydown', (event) ->
atom.keymap.handleKeyEvent(event)
@subscribe $(document), 'drop', (e) ->
e.preventDefault()

View File

@ -69,7 +69,7 @@ window.startEditorWindow = ->
window.unloadEditorWindow = ->
return if not atom.project and not atom.rootView
windowState = atom.getWindowState()
windowState.set('project', atom.project.serialize())
windowState.set('project', atom.project)
windowState.set('syntax', atom.syntax.serialize())
windowState.set('rootView', atom.rootView.serialize())
atom.packages.deactivatePackages()

View File

@ -30,16 +30,16 @@ module.exports = (grunt) ->
themeMains.push(mainPath) if grunt.file.isFile(mainPath)
importPaths.unshift(stylesheetsDir) if grunt.file.isDir(stylesheetsDir)
grunt.log.writeln("Building LESS cache for #{configuration.join(', ').yellow}")
grunt.verbose.writeln("Building LESS cache for #{configuration.join(', ').yellow}")
lessCache = new LessCache
cacheDir: directory
resourcePath: path.resolve('.')
importPaths: importPaths
for file in @filesSrc
grunt.log.writeln("File #{file.cyan} created in cache.")
grunt.verbose.writeln("File #{file.cyan} created in cache.")
lessCache.readFileSync(file)
for file in themeMains
grunt.log.writeln("File #{file.cyan} created in cache.")
grunt.verbose.writeln("File #{file.cyan} created in cache.")
lessCache.readFileSync(file)

View File

@ -84,7 +84,7 @@ module.exports = (grunt) ->
for name, version of packageDependencies
do (name, version) ->
tasks.push (callback) ->
grunt.log.writeln("Publishing #{name}@#{version}")
grunt.verbose.writeln("Publishing #{name}@#{version}")
tag = "v#{version}"
packageExists name, token, (error, exists) ->
if error?

View File

@ -2,6 +2,7 @@ fs = require 'fs'
path = require 'path'
_ = require 'underscore-plus'
async = require 'async'
module.exports = (grunt) ->
@ -10,7 +11,7 @@ module.exports = (grunt) ->
packageSpecQueue = null
runPackageSpecs = (callback) ->
passed = true
failedPackages = []
rootDir = grunt.config.get('atom.shellAppDir')
appDir = grunt.config.get('atom.appDir')
atomPath = path.join(appDir, 'atom.sh')
@ -23,10 +24,10 @@ module.exports = (grunt) ->
opts:
cwd: packagePath
env: _.extend({}, process.env, ATOM_PATH: rootDir)
grunt.log.writeln("Launching #{path.basename(packagePath)} specs.")
grunt.verbose.writeln "Launching #{path.basename(packagePath)} specs."
spawn options, (error, results, code) ->
grunt.log.writeln()
passed = passed and not error and code is 0
failedPackages.push path.basename(packagePath) if error
callback()
modulesDirectory = path.resolve('node_modules')
@ -37,7 +38,7 @@ module.exports = (grunt) ->
packageSpecQueue.push(packagePath)
packageSpecQueue.concurrency = 1
packageSpecQueue.drain = -> callback(null, passed)
packageSpecQueue.drain = -> callback(null, failedPackages)
runCoreSpecs = (callback) ->
contentsDir = grunt.config.get('atom.contentsDir')
@ -49,15 +50,20 @@ module.exports = (grunt) ->
cmd: appPath
args: ['--test', "--resource-path=#{resourcePath}", "--spec-directory=#{coreSpecsPath}"]
spawn options, (error, results, code) ->
grunt.log.writeln()
packageSpecQueue.concurrency = 2
callback(null, not error and code is 0)
callback(null, error)
grunt.registerTask 'run-specs', 'Run the specs', ->
done = @async()
startTime = Date.now()
async.parallel [runCoreSpecs, runPackageSpecs], (error, results) ->
[coreSpecPassed, packageSpecsPassed] = results
[coreSpecFailed, failedPackages] = results
elapsedTime = Math.round((Date.now() - startTime) / 100) / 10
grunt.log.writeln("Total spec time: #{elapsedTime}s")
done(coreSpecPassed and packageSpecsPassed)
grunt.verbose.writeln("Total spec time: #{elapsedTime}s")
failures = failedPackages
failures.push "atom core" if coreSpecFailed
grunt.log.error("[Error]".red + " #{failures.join(', ')} spec(s) failed") if failures.length > 0
done(!coreSpecFailed and failedPackages.length == 0)

View File

@ -23,7 +23,7 @@ module.exports = (grunt) ->
catch error
grunt.fatal(error)
grunt.log.writeln("Copied #{source.cyan} to #{destination.cyan}.")
grunt.verbose.writeln("Copied #{source.cyan} to #{destination.cyan}.")
mkdir: (args...) ->
grunt.file.mkdir(args...)
@ -32,9 +32,18 @@ module.exports = (grunt) ->
grunt.file.delete(args..., force: true) if grunt.file.exists(args...)
spawn: (options, callback) ->
grunt.util.spawn options, (error, results, code) ->
grunt.log.errorlns results.stderr if results.stderr
callback(error, results, code)
childProcess = require 'child_process'
stdout = []
stderr = []
error = null
proc = childProcess.spawn(options.cmd, options.args, options.opts)
proc.stdout.on 'data', (data) -> stdout.push(data.toString())
proc.stderr.on 'data', (data) -> stderr.push(data.toString())
proc.on 'exit', (exitCode, signal) ->
error = new Error(signal) if exitCode != 0
results = {stderr: stderr.join(''), stdout: stdout.join(''), code: exitCode}
grunt.log.error results.stderr if exitCode != 0
callback(error, results, exitCode)
isAtomPackage: (packagePath) ->
try

View File

@ -112,7 +112,7 @@ module.exports = (grunt) ->
# 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}")
grunt.verbose.writeln("Downloading atom-shell version #{version.cyan}")
cacheDirectory = getCachePath(version)
rm(cacheDirectory)
mkdir(cacheDirectory)
@ -146,7 +146,7 @@ module.exports = (grunt) ->
downloadAtomShell version, url, callback
unzipAtomShell = (zipPath, callback) ->
grunt.log.writeln('Unzipping atom-shell')
grunt.verbose.writeln('Unzipping atom-shell')
directoryPath = path.dirname(zipPath)
if process.platform is 'darwin'
@ -167,7 +167,7 @@ module.exports = (grunt) ->
rebuildNativeModules = (previousVersion, callback) ->
newVersion = getAtomShellVersion()
if newVersion and newVersion isnt previousVersion
grunt.log.writeln("Rebuilding native modules for new atom-shell version #{newVersion.cyan}.")
grunt.verbose.writeln("Rebuilding native modules for new atom-shell version #{newVersion.cyan}.")
cmd = path.join('node_modules', '.bin', 'apm')
cmd += ".cmd" if process.platform is 'win32'
spawn {cmd, args: ['rebuild']}, (error) -> callback(error)
@ -186,7 +186,7 @@ module.exports = (grunt) ->
currentAtomShellVersion = getAtomShellVersion()
if atomShellVersion isnt currentAtomShellVersion
if isAtomShellVersionCached(atomShellVersion)
grunt.log.writeln("Installing cached atom-shell #{atomShellVersion.cyan}")
grunt.verbose.writeln("Installing cached atom-shell #{atomShellVersion.cyan}")
installAtomShell(atomShellVersion)
rebuildNativeModules(currentAtomShellVersion, done)
else
@ -198,7 +198,7 @@ module.exports = (grunt) ->
if error?
done(error)
else
grunt.log.writeln("Installing atom-shell #{atomShellVersion.cyan}")
grunt.verbose.writeln("Installing atom-shell #{atomShellVersion.cyan}")
installAtomShell(atomShellVersion)
rebuildNativeModules(currentAtomShellVersion, done)
else