Merge remote-tracking branch 'origin/master' into cj-async-file-open

Conflicts:
	package.json
This commit is contained in:
probablycorey 2013-10-10 11:15:20 -07:00
commit 29a9a9d2ed
40 changed files with 1106 additions and 420 deletions

View File

@ -54,6 +54,7 @@ module.exports = (grunt) ->
glob_to_multiple:
expand: true
src: [
'menus/*.cson'
'keymaps/*.cson'
'static/**/*.cson'
]

View File

@ -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,5 +1,9 @@
require '../src/window'
Atom = require '../src/atom'
window.atom = new 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')

63
menus/base.cson Normal file
View File

@ -0,0 +1,63 @@
'menu': [
{
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' }
]
}
{
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,6 +1,6 @@
{
"name": "atom",
"version": "28.0.0",
"version": "31.0.0",
"main": "./src/main.js",
"repository": {
"type": "git",
@ -9,14 +9,14 @@
"bugs": {
"url": "https://github.com/atom/atom/issues"
},
"atomShellVersion": "0.5.3",
"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.6.0",
"first-mate": "0.2.0",
"git-utils": "0.25.0",
"git-utils": "0.26.0",
"guid": "0.0.10",
"jasmine-focused": "~0.14.0",
"mkdirp": "0.3.5",
@ -28,8 +28,9 @@
"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.4.0",
"scandal": "0.5.0",
"season": "0.13.0",
"semver": "1.1.4",
"space-pen": "1.3.0",
@ -37,7 +38,6 @@
"telepath": "0.8.1",
"temp": "0.5.0",
"underscore": "1.4.4",
"q": "0.9.7",
"atom-light-ui": "0.4.0",
"atom-light-syntax": "0.4.0",
@ -48,17 +48,17 @@
"archive-view": "0.8.0",
"autocomplete": "0.6.0",
"autoflow": "0.3.0",
"bookmarks": "0.4.0",
"bracket-matcher": "0.5.0",
"collaboration": "0.19.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.2.0",
"find-and-replace": "0.22.0",
"fuzzy-finder": "0.7.0",
"exception-reporting": "0.4.0",
"find-and-replace": "0.24.0",
"fuzzy-finder": "0.8.0",
"gfm": "0.5.0",
"git-diff": "0.4.0",
"git-diff": "0.5.0",
"gists": "0.3.0",
"github-sign-in": "0.7.0",
"go-to-line": "0.4.0",
@ -66,24 +66,23 @@
"image-view": "0.6.0",
"link": "0.4.0",
"markdown-preview": "0.6.0",
"metrics": "0.7.0",
"metrics": "0.8.0",
"package-generator": "0.10.0",
"release-notes": "0.2.0",
"settings-view": "0.26.0",
"release-notes": "0.3.0",
"settings-view": "0.27.0",
"snippets": "0.6.0",
"spell-check": "0.6.0",
"status-bar": "0.8.0",
"status-bar": "0.9.0",
"symbols-view": "0.8.0",
"tabs": "0.5.0",
"terminal": "0.10.0",
"timecop": "0.5.0",
"to-the-hubs": "0.4.0",
"to-the-hubs": "0.6.0",
"toml": "0.3.0",
"tree-view": "0.7.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": "1.0.0",
"css-tmbundle": "1.0.0",
@ -118,6 +117,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

@ -151,7 +151,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", ->
@ -161,6 +163,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"
@ -173,6 +177,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()

View File

@ -119,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")
@ -146,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"
@ -1107,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')
@ -1123,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", ->
@ -1878,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)
@ -2160,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

@ -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

@ -2,7 +2,7 @@ try
require '../src/window'
Atom = require '../src/atom'
window.atom = new Atom()
window.atom.show()
window.atom.show() unless atom.getLoadSettings().exitWhenDone
{runSpecSuite} = require './jasmine-helper'
document.title = "Spec Suite"

View File

@ -18,6 +18,7 @@ atom.themes.loadBaseStylesheets()
atom.themes.requireStylesheet '../static/jasmine'
fixturePackagesPath = path.resolve(__dirname, './fixtures/packages')
atom.packages.packageDirPaths.unshift(fixturePackagesPath)
atom.keymap.loadBundledKeymaps()
[bindingSetsToRestore, bindingSetsByFirstKeystrokeToRestore] = []
@ -49,8 +50,13 @@ beforeEach ->
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`
config = new Config()
config = new Config
resourcePath: window.resourcePath
configDirPath: atom.getConfigDirPath()
config.packageDirPaths.unshift(fixturePackagesPath)
spyOn(config, 'load')
spyOn(config, 'save')

View File

@ -18,11 +18,13 @@ class ApplicationMenu
# 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
@menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(@menu)
@ -77,81 +79,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]
@translateTemplate template, keystrokesByCommand
# Private: Combines a menu template with the appropriate keystrokes.
#
# * template:
@ -193,7 +120,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'
Menu = require 'menu'
autoUpdater = require 'auto-updater'
app = require 'app'
@ -34,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
@ -51,7 +50,8 @@ class AtomApplication
resourcePath: null
version: null
constructor: ({@resourcePath, pathsToOpen, urlsToOpen, @version, test, pidToKillWhenClosed, devMode, newWindow, specDirectory}) ->
constructor: (options) ->
{@resourcePath, @version, @devMode} = options
global.atomApplication = this
@pidsToOpenWindows = {}
@ -66,6 +66,10 @@ 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, specDirectory})
else if pathsToOpen.length > 0
@ -94,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
@ -126,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)
@ -144,7 +148,7 @@ 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()
@ -162,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})
@ -192,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
@ -217,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
@ -234,7 +252,7 @@ class AtomApplication
else
resourcePath = @resourcePath
bootstrapScript = require.resolve('./window-bootstrap')
openedWindow = new AtomWindow({pathToOpen, initialLine, bootstrapScript, resourcePath, devMode})
openedWindow = new AtomWindow({pathToOpen, initialLine, bootstrapScript, resourcePath, devMode, initialSize})
if pidToKillWhenClosed?
@pidsToOpenWindows[pidToKillWhenClosed] = openedWindow
@ -248,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:
@ -258,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.
#

View File

@ -90,8 +90,9 @@ class AtomPackage extends Package
@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)
@ -169,7 +170,7 @@ 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
atom.keymap.remove(keymapPath) for [keymapPath] in @keymaps
atom.themes.removeStylesheet(stylesheetPath) for [stylesheetPath] in @stylesheets
@stylesheetsActivated = false

View File

@ -30,13 +30,13 @@ 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)
@ -54,6 +54,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
@ -98,16 +100,23 @@ class AtomWindow
@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:')
@ -115,14 +124,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

@ -27,6 +27,9 @@ class Atom
initialize: ->
@unsubscribe()
{devMode, resourcePath} = atom.getLoadSettings()
configDirPath = @getConfigDirPath()
Config = require './config'
Keymap = require './keymap'
PackageManager = require './package-manager'
@ -34,8 +37,11 @@ class Atom
Syntax = require './syntax'
ThemeManager = require './theme-manager'
ContextMenuManager = require './context-menu-manager'
MenuManager = require './menu-manager'
@packages = new PackageManager()
@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
@ -43,10 +49,9 @@ class Atom
@subscribe @packages, 'loaded', => @watchThemes()
@themes = new ThemeManager()
@contextMenu = new ContextMenuManager(@getLoadSettings().devMode)
@config = new Config()
@contextMenu = new ContextMenuManager(devMode)
@menu = new MenuManager()
@pasteboard = new Pasteboard()
@keymap = new Keymap()
@syntax = deserialize(@getWindowState('syntax')) ? new Syntax()
getCurrentWindow: ->
@ -83,17 +88,21 @@ class Atom
else
browserWindow.center()
restoreDimensions: (defaultDimensions={width: 800, height: 600})->
restoreDimensions: ->
dimensions = @getWindowState().getObject('dimensions')
dimensions = defaultDimensions unless dimensions?.width and dimensions?.height
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: ->
@loadSettings ?= _.clone(@getCurrentWindow().loadSettings)
_.clone(@loadSettings)
@loadSettings ?= _.deepClone(@getCurrentWindow().loadSettings)
_.deepClone(@loadSettings)
deserializeProject: ->
Project = require './project'
@ -213,6 +222,10 @@ class Atom
getHomeDirPath: ->
app.getHomeDir()
# Public: Get the directory path to Atom's configuration area.
getConfigDirPath: ->
@configDirPath ?= fsUtils.absolute('~/.atom')
getWindowStatePath: ->
switch @windowMode
when 'spec'
@ -244,7 +257,7 @@ class 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

View File

@ -26,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,24 +7,22 @@ path = require 'path'
async = require 'async'
pathWatcher = require 'pathwatcher'
configDirPath = fsUtils.absolute("~/.atom")
# 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
@ -35,27 +33,27 @@ class Config
configFileHasErrors: null
# Private: Created during initialization, available as `global.config`
constructor: ->
@configDirPath = configDirPath
@bundledKeymapsDirPath = path.join(resourcePath, "keymaps")
@nodeModulesDirPath = path.join(resourcePath, "node_modules")
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')
path.join(@resourcePath, 'static', 'variables')
path.join(@resourcePath, 'static')
]
@packageDirPaths = [path.join(configDirPath, "packages")]
@packageDirPaths = [path.join(@configDirPath, "packages")]
if atom.getLoadSettings().devMode
@packageDirPaths.unshift(path.join(configDirPath, "dev", "packages"))
@packageDirPaths.unshift(path.join(@configDirPath, "dev", "packages"))
@userPackageDirPaths = _.clone(@packageDirPaths)
@userStoragePath = path.join(configDirPath, "storage")
@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) ->
@ -67,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

@ -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'
@ -643,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)
@ -703,7 +708,12 @@ class Editor extends View
@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', =>
@ -727,6 +737,9 @@ class Editor extends View
@hiddenInput.val(lastInput)
false
bringHiddenInputIntoView: ->
@hiddenInput.css(top: @scrollTop(), left: @scrollLeft())
selectOnMousemoveUntilMouseup: ->
lastMoveEvent = null
moveHandler = (event = lastMoveEvent) =>
@ -841,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())
@ -961,6 +975,9 @@ class Editor extends View
# fontSize - A {Number} indicating the font size in pixels.
setFontSize: (fontSize) ->
@css('font-size', "#{fontSize}px}")
@clearCharacterWidthCache()
if @isOnDom()
@redraw()
else
@ -977,6 +994,9 @@ class Editor extends View
# fontFamily - A {String} identifying the CSS `font-family`,
setFontFamily: (fontFamily='') ->
@css('font-family', fontFamily)
@clearCharacterWidthCache()
@redraw()
# Gets the font family for the editor.
@ -1133,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
@ -1184,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
@ -1195,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()
@ -1229,10 +1271,11 @@ class Editor extends View
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
@ -1258,7 +1301,7 @@ class Editor extends View
emptyLineChanges
computeIntactRanges: ->
computeIntactRanges: (renderFrom, renderTo) ->
return [] if !@firstRenderedScreenRow? and !@lastRenderedScreenRow?
intactRanges = [{start: @firstRenderedScreenRow, end: @lastRenderedScreenRow, domStart: 0}]
@ -1293,6 +1336,9 @@ class Editor extends View
domStart: range.domStart + change.end + 1 - range.start
)
intactRanges = newIntactRanges
@truncateIntactRanges(intactRanges, renderFrom, renderTo)
@pendingChanges = []
intactRanges
@ -1312,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
@ -1348,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
@ -1369,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 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.
#
@ -1401,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
@ -1496,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)
@ -1591,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)
@ -1625,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

@ -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

@ -38,18 +38,16 @@ $.fn.isVisible = ->
!@isHidden()
$.fn.isHidden = ->
# Implementation taken from jQuery's `:hidden` expression code:
# https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js
#
# We were using a pseudo selector: @is(':hidden'). But jQuery's pseudo
# selector code checks the element's webkitMatchesSelector, which is always
# false, and is really really really slow.
# 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
elem = this[0]
return null unless elem
elem.offsetWidth <= 0 and elem.offsetHeight <= 0
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

@ -46,7 +46,7 @@ delegate.browserMainParts.preMainMessageLoopRun = ->
require('coffee-script')
if args.devMode
require(path.join(args.resourcePath, 'src', 'coffee-cache'))
require(path.join(args.resourcePath, 'src', 'coffee-cache')).register()
module.globalPaths.push(path.join(args.resourcePath, 'src'))
else
appSrcPath = path.resolve(process.argv[0], "../../Resources/app/src")

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

@ -0,0 +1,53 @@
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 = []
@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

View File

@ -8,7 +8,11 @@ module.exports =
class PackageManager
_.extend @prototype, EventEmitter
constructor: ->
constructor: ({configDirPath, devMode, @resourcePath}) ->
@packageDirPaths = [path.join(configDirPath, "packages")]
if devMode
@packageDirPaths.unshift(path.join(configDirPath, "dev", "packages"))
@loadedPackages = {}
@activePackages = {}
@packageStates = {}
@ -83,10 +87,10 @@ class PackageManager
resolvePackagePath: (name) ->
return name if fsUtils.isDirectorySync(name)
packagePath = fsUtils.resolve(config.packageDirPaths..., name)
packagePath = fsUtils.resolve(@packageDirPaths..., name)
return packagePath if fsUtils.isDirectorySync(packagePath)
packagePath = path.join(window.resourcePath, 'node_modules', name)
packagePath = path.join(@resourcePath, 'node_modules', name)
return packagePath if @isInternalPackage(packagePath)
isInternalPackage: (packagePath) ->
@ -108,11 +112,11 @@ class PackageManager
getAvailablePackagePaths: ->
packagePaths = []
for packageDirPath in config.packageDirPaths
for packageDirPath in @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'))
for packagePath in fsUtils.listSync(path.join(@resourcePath, 'node_modules'))
packagePaths.push(packagePath) if @isInternalPackage(packagePath)
_.uniq(packagePaths)
@ -122,8 +126,8 @@ class PackageManager
getAvailablePackageMetadata: ->
packages = []
for packagePath in atom.getAvailablePackagePaths()
for packagePath in @getAvailablePackagePaths()
name = path.basename(packagePath)
metadata = atom.getLoadedPackage(name)?.metadata ? Package.loadMetadata(packagePath, true)
metadata = @getLoadedPackage(name)?.metadata ? Package.loadMetadata(packagePath, true)
packages.push(metadata)
packages

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

@ -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,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

@ -38,7 +38,7 @@ window.setUpEnvironment = (windowMode) ->
window.setUpDefaultEvents = ->
windowEventHandler = new WindowEventHandler
atom.keymap.loadBundledKeymaps()
ipc.sendChannel 'update-application-menu', atom.keymap.keystrokesByCommandForSelector('body')
atom.menu.update()
# This method is only called when opening a real application window
window.startEditorWindow = ->
@ -56,7 +56,7 @@ window.startEditorWindow = ->
atom.packages.activatePackages()
atom.keymap.loadUserKeymaps()
atom.requireUserInitScript()
ipc.sendChannel 'update-application-menu', atom.keymap.keystrokesByCommandForSelector('body')
atom.menu.update()
$(window).on 'unload', ->
$(document.body).hide()
unloadEditorWindow()

View File

@ -8,11 +8,7 @@
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');
}

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 952b3221bb9755d478b10a0ab0384855f80abffe
Subproject commit fcb19e296ca8979a28d2d503c2650f4ef381c8be