diff --git a/.gitignore b/.gitignore index 6eec21c2a..bce6c56d3 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ debug.log docs/output docs/includes spec/fixtures/evil-files/ +out/ diff --git a/apm/package.json b/apm/package.json index 2e6b0b8ea..4b599bc39 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "1.6.0" + "atom-package-manager": "1.7.1" } } diff --git a/package.json b/package.json index 1c55268dd..8907f16b9 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "less-cache": "0.23", "line-top-index": "0.2.0", "marked": "^0.3.4", - "nodegit": "0.11.5", + "nodegit": "0.11.6", "normalize-package-data": "^2.0.0", "nslog": "^3", "oniguruma": "^5", @@ -72,7 +72,7 @@ "one-light-syntax": "1.2.0", "solarized-dark-syntax": "1.0.0", "solarized-light-syntax": "1.0.0", - "about": "1.3.1", + "about": "1.4.0", "archive-view": "0.61.1", "autocomplete-atom-api": "0.10.0", "autocomplete-css": "0.11.0", @@ -147,7 +147,7 @@ "language-text": "0.7.0", "language-todo": "0.27.0", "language-toml": "0.18.0", - "language-xml": "0.34.3", + "language-xml": "0.34.4", "language-yaml": "0.25.1" }, "private": true, diff --git a/spec/async-spec-helpers.coffee b/spec/async-spec-helpers.coffee index 5f8e03ca3..6ed8a5a2b 100644 --- a/spec/async-spec-helpers.coffee +++ b/spec/async-spec-helpers.coffee @@ -19,7 +19,9 @@ exports.afterEach = (fn) -> waitsForPromise = (fn) -> promise = fn() - waitsFor 'spec promise to resolve', 30000, (done) -> + # This timeout is 3 minutes. We need to bump it back down once we fix backgrounding + # of the renderer process on CI. See https://github.com/atom/electron/issues/4317 + waitsFor 'spec promise to resolve', 3 * 60 * 1000, (done) -> promise.then( done, (error) -> diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 1f8eb08e7..6685d4060 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -328,3 +328,18 @@ describe "AtomEnvironment", -> runs -> {releaseVersion} = updateAvailableHandler.mostRecentCall.args[0] expect(releaseVersion).toBe 'version' + + describe "::getReleaseChannel()", -> + [version] = [] + beforeEach -> + spyOn(atom, 'getVersion').andCallFake -> version + + it "returns the correct channel based on the version number", -> + version = '1.5.6' + expect(atom.getReleaseChannel()).toBe 'stable' + + version = '1.5.0-beta10' + expect(atom.getReleaseChannel()).toBe 'beta' + + version = '1.7.0-dev-5340c91' + expect(atom.getReleaseChannel()).toBe 'dev' diff --git a/spec/auto-update-manager-spec.js b/spec/auto-update-manager-spec.js new file mode 100644 index 000000000..6f7dbbb1a --- /dev/null +++ b/spec/auto-update-manager-spec.js @@ -0,0 +1,115 @@ +'use babel' + +import AutoUpdateManager from '../src/auto-update-manager' +import {remote} from 'electron' +const electronAutoUpdater = remote.require('electron').autoUpdater + +describe('AutoUpdateManager (renderer)', () => { + let autoUpdateManager + + beforeEach(() => { + autoUpdateManager = new AutoUpdateManager({ + applicationDelegate: atom.applicationDelegate + }) + }) + + afterEach(() => { + autoUpdateManager.destroy() + }) + + describe('::onDidBeginCheckingForUpdate', () => { + it('subscribes to "did-begin-checking-for-update" event', () => { + const spy = jasmine.createSpy('spy') + autoUpdateManager.onDidBeginCheckingForUpdate(spy) + electronAutoUpdater.emit('checking-for-update') + waitsFor(() => { + return spy.callCount === 1 + }) + }) + }) + + describe('::onDidBeginDownloadingUpdate', () => { + it('subscribes to "did-begin-downloading-update" event', () => { + const spy = jasmine.createSpy('spy') + autoUpdateManager.onDidBeginDownloadingUpdate(spy) + electronAutoUpdater.emit('update-available') + waitsFor(() => { + return spy.callCount === 1 + }) + }) + }) + + describe('::onDidCompleteDownloadingUpdate', () => { + it('subscribes to "did-complete-downloading-update" event', () => { + const spy = jasmine.createSpy('spy') + autoUpdateManager.onDidCompleteDownloadingUpdate(spy) + electronAutoUpdater.emit('update-downloaded', null, null, '1.2.3') + waitsFor(() => { + return spy.callCount === 1 + }) + runs(() => { + expect(spy.mostRecentCall.args[0].releaseVersion).toBe('1.2.3') + }) + }) + }) + + describe('::onUpdateNotAvailable', () => { + it('subscribes to "update-not-available" event', () => { + const spy = jasmine.createSpy('spy') + autoUpdateManager.onUpdateNotAvailable(spy) + electronAutoUpdater.emit('update-not-available') + waitsFor(() => { + return spy.callCount === 1 + }) + }) + }) + + describe('::platformSupportsUpdates', () => { + let state, releaseChannel + it('returns true on OS X and Windows when in stable', () => { + spyOn(autoUpdateManager, 'getState').andCallFake(() => state) + spyOn(atom, 'getReleaseChannel').andCallFake(() => releaseChannel) + + state = 'idle' + releaseChannel = 'stable' + expect(autoUpdateManager.platformSupportsUpdates()).toBe(true) + + state = 'idle' + releaseChannel = 'dev' + expect(autoUpdateManager.platformSupportsUpdates()).toBe(false) + + state = 'unsupported' + releaseChannel = 'stable' + expect(autoUpdateManager.platformSupportsUpdates()).toBe(false) + + state = 'unsupported' + releaseChannel = 'dev' + expect(autoUpdateManager.platformSupportsUpdates()).toBe(false) + }) + }) + + describe('::destroy', () => { + it('unsubscribes from all events', () => { + const spy = jasmine.createSpy('spy') + const doneIndicator = jasmine.createSpy('spy') + atom.applicationDelegate.onUpdateNotAvailable(doneIndicator) + autoUpdateManager.onDidBeginCheckingForUpdate(spy) + autoUpdateManager.onDidBeginDownloadingUpdate(spy) + autoUpdateManager.onDidCompleteDownloadingUpdate(spy) + autoUpdateManager.onUpdateNotAvailable(spy) + autoUpdateManager.destroy() + electronAutoUpdater.emit('checking-for-update') + electronAutoUpdater.emit('update-available') + electronAutoUpdater.emit('update-downloaded', null, null, '1.2.3') + electronAutoUpdater.emit('update-not-available') + + waitsFor(() => { + return doneIndicator.callCount === 1 + }) + + runs(() => { + expect(spy.callCount).toBe(0) + }) + }) + }) +}) diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index 2e15431b2..b39a07a6c 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -623,6 +623,34 @@ describe "Workspace", -> expect(pane.getItems().length).toBe 2 expect(pane.getItems()).toEqual [editor1, editor2] + describe "when replacing a pending item which is the last item in a second pane", -> + it "does not destory the pane even if core.destroyEmptyPanes is on", -> + atom.config.set('core.destroyEmptyPanes', true) + editor1 = null + editor2 = null + leftPane = atom.workspace.getActivePane() + rightPane = null + + waitsForPromise -> + atom.workspace.open('sample.js', pending: true, split: 'right').then (o) -> + editor1 = o + rightPane = atom.workspace.getActivePane() + spyOn rightPane, "destroyed" + + runs -> + expect(leftPane).not.toBe rightPane + expect(atom.workspace.getActivePane()).toBe rightPane + expect(atom.workspace.getActivePane().getItems().length).toBe 1 + expect(rightPane.getPendingItem()).toBe editor1 + + waitsForPromise -> + atom.workspace.open('sample.txt', pending: true).then (o) -> + editor2 = o + + runs -> + expect(rightPane.getPendingItem()).toBe editor2 + expect(rightPane.destroyed.callCount).toBe 0 + describe "::reopenItem()", -> it "opens the uri associated with the last closed pane that isn't currently open", -> pane = workspace.getActivePane() diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index cc1f4c946..3aff9e457 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -166,8 +166,7 @@ class ApplicationDelegate onDidOpenLocations: (callback) -> outerCallback = (event, message, detail) -> - if message is 'open-locations' - callback(detail) + callback(detail) if message is 'open-locations' ipcRenderer.on('message', outerCallback) new Disposable -> @@ -175,8 +174,38 @@ class ApplicationDelegate onUpdateAvailable: (callback) -> outerCallback = (event, message, detail) -> - if message is 'update-available' - callback(detail) + # TODO: Yes, this is strange that `onUpdateAvailable` is listening for + # `did-begin-downloading-update`. We currently have no mechanism to know + # if there is an update, so begin of downloading is a good proxy. + callback(detail) if message is 'did-begin-downloading-update' + + ipcRenderer.on('message', outerCallback) + new Disposable -> + ipcRenderer.removeListener('message', outerCallback) + + onDidBeginDownloadingUpdate: (callback) -> + @onUpdateAvailable(callback) + + onDidBeginCheckingForUpdate: (callback) -> + outerCallback = (event, message, detail) -> + callback(detail) if message is 'checking-for-update' + + ipcRenderer.on('message', outerCallback) + new Disposable -> + ipcRenderer.removeListener('message', outerCallback) + + onDidCompleteDownloadingUpdate: (callback) -> + outerCallback = (event, message, detail) -> + # TODO: We could rename this event to `did-complete-downloading-update` + callback(detail) if message is 'update-available' + + ipcRenderer.on('message', outerCallback) + new Disposable -> + ipcRenderer.removeListener('message', outerCallback) + + onUpdateNotAvailable: (callback) -> + outerCallback = (event, message, detail) -> + callback(detail) if message is 'update-not-available' ipcRenderer.on('message', outerCallback) new Disposable -> @@ -206,3 +235,12 @@ class ApplicationDelegate disablePinchToZoom: -> webFrame.setZoomLevelLimits(1, 1) + + checkForUpdate: -> + ipcRenderer.send('check-for-update') + + restartAndInstallUpdate: -> + ipcRenderer.send('install-update') + + getAutoUpdateManagerState: -> + ipcRenderer.sendSync('get-auto-update-manager-state') diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 709d54082..27f8be09e 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -41,6 +41,7 @@ TextEditor = require './text-editor' TextBuffer = require 'text-buffer' Gutter = require './gutter' TextEditorRegistry = require './text-editor-registry' +AutoUpdateManager = require './auto-update-manager' WorkspaceElement = require './workspace-element' PanelContainerElement = require './panel-container-element' @@ -115,6 +116,9 @@ class AtomEnvironment extends Model # Public: A {TextEditorRegistry} instance textEditors: null + # Private: An {AutoUpdateManager} instance + autoUpdater: null + saveStateDebounceInterval: 1000 ### @@ -188,6 +192,7 @@ class AtomEnvironment extends Model @themes.workspace = @workspace @textEditors = new TextEditorRegistry + @autoUpdater = new AutoUpdateManager({@applicationDelegate}) @config.load() @@ -331,6 +336,7 @@ class AtomEnvironment extends Model @commands.clear() @stylesElement.remove() @config.unobserveUserConfig() + @autoUpdater.destroy() @uninstallWindowEventHandler() @@ -409,6 +415,16 @@ class AtomEnvironment extends Model getVersion: -> @appVersion ?= @getLoadSettings().appVersion + # Returns the release channel as a {String}. Will return one of `'dev', 'beta', 'stable'` + getReleaseChannel: -> + version = @getVersion() + if version.indexOf('beta') > -1 + 'beta' + else if version.indexOf('dev') > -1 + 'dev' + else + 'stable' + # Public: Returns a {Boolean} that is `true` if the current version is an official release. isReleasedVersion: -> not /\w{7}/.test(@getVersion()) # Check if the release is a 7-character SHA prefix @@ -828,7 +844,6 @@ class AtomEnvironment extends Model @applicationDelegate.setTemporaryWindowState(state) savePromise.catch(reject).then(resolve) - loadState: -> if @enablePersistence if stateKey = @getStateKey(@getLoadSettings().initialPaths) @@ -877,6 +892,7 @@ class AtomEnvironment extends Model detail: error.message dismissable: true + # TODO: We should deprecate the update events here, and use `atom.autoUpdater` instead onUpdateAvailable: (callback) -> @emitter.on 'update-available', callback @@ -884,7 +900,8 @@ class AtomEnvironment extends Model @emitter.emit 'update-available', details listenForUpdates: -> - @disposables.add(@applicationDelegate.onUpdateAvailable(@updateAvailable.bind(this))) + # listen for updates available locally (that have been successfully downloaded) + @disposables.add(@autoUpdater.onDidCompleteDownloadingUpdate(@updateAvailable.bind(this))) setBodyPlatformClass: -> @document.body.classList.add("platform-#{process.platform}") diff --git a/src/auto-update-manager.js b/src/auto-update-manager.js new file mode 100644 index 000000000..62cc03f85 --- /dev/null +++ b/src/auto-update-manager.js @@ -0,0 +1,73 @@ +'use babel' + +import {Emitter, CompositeDisposable} from 'event-kit' + +export default class AutoUpdateManager { + constructor ({applicationDelegate}) { + this.applicationDelegate = applicationDelegate + this.subscriptions = new CompositeDisposable() + this.emitter = new Emitter() + + this.subscriptions.add( + applicationDelegate.onDidBeginCheckingForUpdate(() => { + this.emitter.emit('did-begin-checking-for-update') + }), + applicationDelegate.onDidBeginDownloadingUpdate(() => { + this.emitter.emit('did-begin-downloading-update') + }), + applicationDelegate.onDidCompleteDownloadingUpdate((details) => { + this.emitter.emit('did-complete-downloading-update', details) + }), + applicationDelegate.onUpdateNotAvailable(() => { + this.emitter.emit('update-not-available') + }) + ) + } + + destroy () { + this.subscriptions.dispose() + this.emitter.dispose() + } + + checkForUpdate () { + this.applicationDelegate.checkForUpdate() + } + + restartAndInstallUpdate () { + this.applicationDelegate.restartAndInstallUpdate() + } + + getState () { + return this.applicationDelegate.getAutoUpdateManagerState() + } + + platformSupportsUpdates () { + return atom.getReleaseChannel() !== 'dev' && this.getState() !== 'unsupported' + } + + onDidBeginCheckingForUpdate (callback) { + return this.emitter.on('did-begin-checking-for-update', callback) + } + + onDidBeginDownloadingUpdate (callback) { + return this.emitter.on('did-begin-downloading-update', callback) + } + + onDidCompleteDownloadingUpdate (callback) { + return this.emitter.on('did-complete-downloading-update', callback) + } + + // TODO: When https://github.com/atom/electron/issues/4587 is closed, we can + // add an update-available event. + // onUpdateAvailable (callback) { + // return this.emitter.on('update-available', callback) + // } + + onUpdateNotAvailable (callback) { + return this.emitter.on('update-not-available', callback) + } + + getPlatform () { + return process.platform + } +} diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index 40b04c3a1..fdfea5474 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -304,6 +304,15 @@ class AtomApplication ipcMain.on 'execute-javascript-in-dev-tools', (event, code) -> event.sender.devToolsWebContents?.executeJavaScript(code) + ipcMain.on 'check-for-update', => + @autoUpdateManager.check() + + ipcMain.on 'get-auto-update-manager-state', (event) => + event.returnValue = @autoUpdateManager.getState() + + ipcMain.on 'execute-javascript-in-dev-tools', (event, code) -> + event.sender.devToolsWebContents?.executeJavaScript(code) + setupDockMenu: -> if process.platform is 'darwin' dockMenu = Menu.buildFromTemplate [ diff --git a/src/browser/auto-update-manager.coffee b/src/browser/auto-update-manager.coffee index 2df338761..c8c57cb01 100644 --- a/src/browser/auto-update-manager.coffee +++ b/src/browser/auto-update-manager.coffee @@ -39,16 +39,24 @@ class AutoUpdateManager autoUpdater.on 'checking-for-update', => @setState(CheckingState) + @emitWindowEvent('checking-for-update') autoUpdater.on 'update-not-available', => @setState(NoUpdateAvailableState) + @emitWindowEvent('update-not-available') autoUpdater.on 'update-available', => @setState(DownladingState) + # We use sendMessage to send an event called 'update-available' in 'update-downloaded' + # once the update download is complete. This mismatch between the electron + # autoUpdater events is unfortunate but in the interest of not changing the + # one existing event handled by applicationDelegate + @emitWindowEvent('did-begin-downloading-update') + @emit('did-begin-download') autoUpdater.on 'update-downloaded', (event, releaseNotes, @releaseVersion) => @setState(UpdateAvailableState) - @emitUpdateAvailableEvent(@getWindows()...) + @emitUpdateAvailableEvent() @config.onDidChange 'core.automaticallyUpdate', ({newValue}) => if newValue @@ -64,10 +72,14 @@ class AutoUpdateManager when 'linux' @setState(UnsupportedState) - emitUpdateAvailableEvent: (windows...) -> + emitUpdateAvailableEvent: -> return unless @releaseVersion? - for atomWindow in windows - atomWindow.sendMessage('update-available', {@releaseVersion}) + @emitWindowEvent('update-available', {@releaseVersion}) + return + + emitWindowEvent: (eventName, payload) -> + for atomWindow in @getWindows() + atomWindow.sendMessage(eventName, payload) return setState: (state) -> diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 894b1216a..91b78e0c5 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -596,7 +596,15 @@ export default class GitRepositoryAsync { .then(([repo, headCommit]) => Promise.all([repo, headCommit.getTree()])) .then(([repo, tree]) => { const options = new Git.DiffOptions() + options.contextLines = 0 + options.flags = Git.Diff.OPTION.DISABLE_PATHSPEC_MATCH options.pathspec = this.relativize(_path, repo.workdir()) + if (process.platform === 'win32') { + // Ignore eol of line differences on windows so that files checked in + // as LF don't report every line modified when the text contains CRLF + // endings. + options.flags |= Git.Diff.OPTION.IGNORE_WHITESPACE_EOL + } return Git.Diff.treeToWorkdir(repo, tree, options) }) .then(diff => this._getDiffLines(diff)) diff --git a/src/pane.coffee b/src/pane.coffee index 25f421934..b944f763c 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -423,10 +423,6 @@ class Pane extends Model return if item in @items - pendingItem = @getPendingItem() - @destroyItem(pendingItem) if pendingItem? - @setPendingItem(item) if pending - if typeof item.onDidDestroy is 'function' itemSubscriptions = new CompositeDisposable itemSubscriptions.add item.onDidDestroy => @removeItem(item, false) @@ -437,6 +433,10 @@ class Pane extends Model @subscriptionsPerItem.set item, itemSubscriptions @items.splice(index, 0, item) + pendingItem = @getPendingItem() + @destroyItem(pendingItem) if pendingItem? + @setPendingItem(item) if pending + @emitter.emit 'did-add-item', {item, index, moved} @setActiveItem(item) unless @getActiveItem()? item