diff --git a/atom.sh b/atom.sh index 53de6a493..6ee2b9990 100755 --- a/atom.sh +++ b/atom.sh @@ -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 diff --git a/benchmark/benchmark-bootstrap.coffee b/benchmark/benchmark-bootstrap.coffee index 5d74df7de..8d9a0ba4e 100644 --- a/benchmark/benchmark-bootstrap.coffee +++ b/benchmark/benchmark-bootstrap.coffee @@ -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() diff --git a/benchmark/benchmark-helper.coffee b/benchmark/benchmark-helper.coffee index aa91f76d7..5b94da991 100644 --- a/benchmark/benchmark-helper.coffee +++ b/benchmark/benchmark-helper.coffee @@ -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 diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee index 7a15c846c..b5401d385 100644 --- a/benchmark/benchmark-suite.coffee +++ b/benchmark/benchmark-suite.coffee @@ -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') diff --git a/package.json b/package.json index 144bd0651..c0ec3ea0e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "atom", - "version": "29.0.0", + "version": "31.0.0", "main": "./src/main.js", "repository": { "type": "git", @@ -16,7 +16,7 @@ "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", @@ -46,17 +46,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.3.0", + "exception-reporting": "0.4.0", "find-and-replace": "0.24.0", "fuzzy-finder": "0.7.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,7 +66,7 @@ "markdown-preview": "0.6.0", "metrics": "0.8.0", "package-generator": "0.10.0", - "release-notes": "0.2.0", + "release-notes": "0.3.0", "settings-view": "0.27.0", "snippets": "0.6.0", "spell-check": "0.6.0", @@ -75,9 +75,9 @@ "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.8.0", + "tree-view": "0.10.0", "ui-demo": "0.8.0", "whitespace": "0.5.0", "wrap-guide": "0.3.0", diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index 820d56654..48de79f12 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -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" @@ -904,7 +904,8 @@ describe "Editor", -> it "moves the hiddenInput to the same position with cursor's view", -> editor.setCursorScreenPosition(row: 2, column: 2) - expect(editor.getCursorView().offset()).toEqual(editor.hiddenInput.offset()) + expect(editor.getCursorView()[0].style.left).toEqual(editor.hiddenInput[0].style.left) + expect(editor.getCursorView()[0].style.top).toEqual(editor.hiddenInput[0].style.top) describe "when the editor is using a variable-width font", -> beforeEach -> @@ -1107,8 +1108,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 +1124,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 +1880,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 +2204,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() diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index a902fc2b4..db907b685 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -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] = [] @@ -50,7 +51,9 @@ beforeEach -> bindingSetsByFirstKeystrokeToRestore = _.clone(keymap.bindingSetsByFirstKeystroke) # 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') diff --git a/src/atom-application.coffee b/src/atom-application.coffee index d33a9255b..9747fe509 100644 --- a/src/atom-application.coffee +++ b/src/atom-application.coffee @@ -51,7 +51,7 @@ class AtomApplication version: null constructor: (options) -> - {@resourcePath, @version} = options + {@resourcePath, @version, @devMode} = options global.atomApplication = this @pidsToOpenWindows = {} @@ -147,7 +147,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() @@ -221,9 +221,10 @@ class AtomApplication # + 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 + 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 @@ -251,9 +252,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/ 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: @@ -261,15 +264,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}) + 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. # diff --git a/src/atom-window.coffee b/src/atom-window.coffee index 350aac6f5..4cd34b85e 100644 --- a/src/atom-window.coffee +++ b/src/atom-window.coffee @@ -30,9 +30,8 @@ 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 @@ -55,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 diff --git a/src/atom.coffee b/src/atom.coffee index 4eb2b34d6..dbf62b991 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -27,6 +27,9 @@ class Atom initialize: -> @unsubscribe() + {devMode, resourcePath} = atom.getLoadSettings() + configDirPath = @getConfigDirPath() + Config = require './config' Keymap = require './keymap' PackageManager = require './package-manager' @@ -36,9 +39,9 @@ class Atom ContextMenuManager = require './context-menu-manager' MenuManager = require './menu-manager' - @config = new Config() + @config = new Config({configDirPath, resourcePath}) @keymap = new Keymap() - @packages = new PackageManager() + @packages = new PackageManager({devMode, configDirPath, resourcePath}) #TODO Remove once packages have been updated to not touch atom.packageStates directly @__defineGetter__ 'packageStates', => @packages.packageStates @@ -46,7 +49,7 @@ class Atom @subscribe @packages, 'loaded', => @watchThemes() @themes = new ThemeManager() - @contextMenu = new ContextMenuManager(@getLoadSettings().devMode) + @contextMenu = new ContextMenuManager(devMode) @menu = new MenuManager() @pasteboard = new Pasteboard() @syntax = deserialize(@getWindowState('syntax')) ? new Syntax() @@ -215,6 +218,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' @@ -246,7 +253,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 diff --git a/src/config.coffee b/src/config.coffee index 5ee609147..8c9c6e71d 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -7,8 +7,6 @@ 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 @@ -35,28 +33,27 @@ class Config configFileHasErrors: null # Private: Created during initialization, available as `global.config` - constructor: -> - @configDirPath = configDirPath - @bundledKeymapsDirPath = path.join(resourcePath, "keymaps") + constructor: ({@configDirPath, @resourcePath}={}) -> + @bundledKeymapsDirPath = path.join(@resourcePath, "keymaps") @bundledMenusDirPath = path.join(resourcePath, "menus") - @nodeModulesDirPath = path.join(resourcePath, "node_modules") + @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) -> @@ -68,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) diff --git a/src/cursor-view.coffee b/src/cursor-view.coffee index 3c15c235d..5cef2af46 100644 --- a/src/cursor-view.coffee +++ b/src/cursor-view.coffee @@ -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 diff --git a/src/editor.coffee b/src/editor.coffee index ee105b165..4ced66eea 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -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 @@ -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', => @@ -961,6 +971,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 +990,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 +1149,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 +1208,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 +1225,15 @@ 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 (endRow >= @firstRenderedScreenRow and endRow <= @lastRenderedScreenRow) + syncCursorAnimations: -> for cursorView in @getCursorViews() do (cursorView) -> cursorView.resetBlinking() @@ -1229,10 +1265,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 +1295,7 @@ class Editor extends View emptyLineChanges - computeIntactRanges: -> + computeIntactRanges: (renderFrom, renderTo) -> return [] if !@firstRenderedScreenRow? and !@lastRenderedScreenRow? intactRanges = [{start: @firstRenderedScreenRow, end: @lastRenderedScreenRow, domStart: 0}] @@ -1293,6 +1330,9 @@ class Editor extends View domStart: range.domStart + change.end + 1 - range.start ) intactRanges = newIntactRanges + + @truncateIntactRanges(intactRanges, renderFrom, renderTo) + @pendingChanges = [] intactRanges @@ -1312,35 +1352,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 +1388,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 +1409,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 +1445,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 +1540,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 +1699,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("") - - popScope = -> - scopeStack.pop() - line.push("") - - attributePairs = [] - attributePairs.push "#{attributeName}=\"#{value}\"" for attributeName, value of attributes - line.push("
") + attributePairs = '' + attributePairs += " #{attributeName}=\"#{value}\"" for attributeName, value of attributes + line.push("
") if text == '' html = Editor.buildEmptyLineHtml(showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, activeEditSession, mini) @@ -1625,39 +1712,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("") if fold line.push('
') 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("") + + @popScope: (line, scopeStack) -> + scopeStack.pop() + line.push("") + @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 = [""] + indentLevelHtml = "" for characterPosition in [0...tabLength] - if invisible = eolInvisibles.shift() - indentLevelHtml.push("#{invisible}") + if invisible = eolInvisibles[indentCharIndex++] + indentLevelHtml += "#{invisible}" else - indentLevelHtml.push(' ') - indentLevelHtml.push("") - indentGuideHtml.push(indentLevelHtml.join('')) + indentLevelHtml += ' ' + indentLevelHtml += "" + indentGuideHtml += indentLevelHtml for invisible in eolInvisibles - indentGuideHtml.push("#{invisible}") + indentGuideHtml += "#{invisible}" - return indentGuideHtml.join('') + return indentGuideHtml if htmlEolInvisibles.length > 0 htmlEolInvisibles diff --git a/src/git.coffee b/src/git.coffee index 14e7620fd..c10e50fb1 100644 --- a/src/git.coffee +++ b/src/git.coffee @@ -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 diff --git a/src/gutter.coffee b/src/gutter.coffee index d1c3ac8b1..1e38daa7a 100644 --- a/src/gutter.coffee +++ b/src/gutter.coffee @@ -62,6 +62,76 @@ 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].childNodes + + # 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) -> @@ -78,30 +148,35 @@ class Gutter extends View @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(' ', maxDigits - rowValue.length) - @raw("#{rowValuePadding}#{rowValue}") - - lastScreenRow = row - + @lineNumbers[0].innerHTML = @buildLineElementsHtml(startScreenRow, endScreenRow) @firstScreenRow = startScreenRow @lastScreenRow = endScreenRow @highlightedRows = null @highlightLines() + 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(' ', maxDigits - rowValue.length) + + html += """
#{rowValuePadding}#{rowValue}
""" + + lastScreenRow = row + + html + removeLineHighlights: -> return unless @highlightedLineNumbers for line in @highlightedLineNumbers diff --git a/src/jquery-extensions.coffee b/src/jquery-extensions.coffee index c749b1ce5..76ee60178 100644 --- a/src/jquery-extensions.coffee +++ b/src/jquery-extensions.coffee @@ -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') diff --git a/src/package-manager.coffee b/src/package-manager.coffee index cc08c6f56..a2f026b2d 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -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 diff --git a/src/repository-status-handler.coffee b/src/repository-status-handler.coffee index 9eda7e2f6..0a9c08f7c 100644 --- a/src/repository-status-handler.coffee +++ b/src/repository-status-handler.coffee @@ -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} diff --git a/src/token.coffee b/src/token.coffee index 141e6a756..81b5b2556 100644 --- a/src/token.coffee +++ b/src/token.coffee @@ -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, '&') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(//g, '>') 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 - "#{match}" + "#{@escapeString(match)}" 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 - "#{match}" - 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 - "#{match}" + 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 = "#{match[0]}" + + 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 = "#{match[0]}" + + 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 '&' + when '"' then '"' + when "'" then ''' + when '<' then '<' + when '>' then '>' + else match diff --git a/vendor/apm b/vendor/apm index 162824eb1..fcb19e296 160000 --- a/vendor/apm +++ b/vendor/apm @@ -1 +1 @@ -Subproject commit 162824eb1a73c154e3bf823c591fa42f8ebcaa37 +Subproject commit fcb19e296ca8979a28d2d503c2650f4ef381c8be