Merge remote-tracking branch 'master' into wl-update-language-javascript

# Conflicts:
#	package.json
This commit is contained in:
Wliu
2016-03-29 19:01:57 -04:00
75 changed files with 1684 additions and 461 deletions

1
.gitignore vendored
View File

@@ -15,3 +15,4 @@ debug.log
docs/output
docs/includes
spec/fixtures/evil-files/
out/

View File

@@ -1,8 +1,8 @@
### Prerequisites
* [ ] Can you reproduce the problem in [safe mode](https://atom.io/docs/latest/hacking-atom-debugging#check-if-the-problem-shows-up-in-safe-mode)?
* [ ] Are you running the [latest version of Atom](https://atom.io/docs/latest/hacking-atom-debugging#update-to-the-latest-version)?
* [ ] Did you check the [debugging guide](https://atom.io/docs/latest/hacking-atom-debugging)?
* [ ] Can you reproduce the problem in [safe mode](http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-if-the-problem-shows-up-in-safe-mode)?
* [ ] Are you running the [latest version of Atom](http://flight-manual.atom.io/hacking-atom/sections/debugging/#update-to-the-latest-version)?
* [ ] Did you check the [debugging guide](flight-manual.atom.io/hacking-atom/sections/debugging/)?
* [ ] Did you check the [FAQs on Discuss](https://discuss.atom.io/c/faq)?
* [ ] Are you reporting to the [correct repository](https://github.com/atom/atom/blob/master/CONTRIBUTING.md#atom-and-packages)?
* [ ] Did you [perform a cursory search](https://github.com/issues?q=is%3Aissue+user%3Aatom+-repo%3Aatom%2Felectron) to see if your bug or enhancement is already reported?

View File

@@ -6,6 +6,6 @@
"url": "https://github.com/atom/atom.git"
},
"dependencies": {
"atom-package-manager": "1.6.0"
"atom-package-manager": "1.9.2"
}
}

View File

@@ -4,8 +4,6 @@ if [ "$(uname)" == 'Darwin' ]; then
OS='Mac'
elif [ "$(expr substr $(uname -s) 1 5)" == 'Linux' ]; then
OS='Linux'
elif [ "$(expr substr $(uname -s) 1 10)" == 'MINGW32_NT' ]; then
OS='Cygwin'
else
echo "Your platform ($(uname -a)) is not supported."
exit 1

View File

@@ -34,23 +34,10 @@ module.exports = (grunt) ->
grunt.file.setBase(path.resolve('..'))
# Options
[defaultChannel, releaseBranch] = getDefaultChannelAndReleaseBranch(packageJson.version)
installDir = grunt.option('install-dir')
buildDir = grunt.option('build-dir')
buildDir ?= 'out'
buildDir = path.resolve(buildDir)
channel = grunt.option('channel')
releasableBranches = ['stable', 'beta']
if process.env.APPVEYOR and not process.env.APPVEYOR_PULL_REQUEST_NUMBER
channel ?= process.env.APPVEYOR_REPO_BRANCH if process.env.APPVEYOR_REPO_BRANCH in releasableBranches
if process.env.TRAVIS and not process.env.TRAVIS_PULL_REQUEST
channel ?= process.env.TRAVIS_BRANCH if process.env.TRAVIS_BRANCH in releasableBranches
if process.env.JANKY_BRANCH
channel ?= process.env.JANKY_BRANCH if process.env.JANKY_BRANCH in releasableBranches
channel ?= 'dev'
buildDir = path.resolve(grunt.option('build-dir') ? 'out')
channel = grunt.option('channel') ? defaultChannel
metadata = packageJson
appName = packageJson.productName
@@ -189,7 +176,7 @@ module.exports = (grunt) ->
pkg: grunt.file.readJSON('package.json')
atom: {
appName, channel, metadata,
appName, channel, metadata, releaseBranch,
appFileName, apmFileName,
appDir, buildDir, contentsDir, installDir, shellAppDir, symbolsDir,
}
@@ -310,3 +297,20 @@ module.exports = (grunt) ->
unless process.platform is 'linux' or grunt.option('no-install')
defaultTasks.push 'install'
grunt.registerTask('default', defaultTasks)
getDefaultChannelAndReleaseBranch = (version) ->
if version.match(/dev/) or isBuildingPR()
channel = 'dev'
releaseBranch = null
else
if version.match(/beta/)
channel = 'beta'
else
channel = 'stable'
minorVersion = version.match(/^\d\.\d/)[0]
releaseBranch = "#{minorVersion}-releases"
[channel, releaseBranch]
isBuildingPR = ->
process.env.APPVEYOR_PULL_REQUEST_NUMBER? or process.env.TRAVIS_PULL_REQUEST?

View File

@@ -3,7 +3,7 @@ path = require 'path'
module.exports = (grunt) ->
{spawn} = require('./task-helpers')(grunt)
grunt.registerTask 'codesign:exe', 'Codesign atom.exe and Update.exe', ->
grunt.registerTask 'codesign:exe', 'CodeSign Atom.exe and Update.exe', ->
done = @async()
spawn {cmd: 'taskkill', args: ['/F', '/IM', 'atom.exe']}, ->
cmd = process.env.JANKY_SIGNTOOL ? 'signtool'
@@ -14,13 +14,13 @@ module.exports = (grunt) ->
updateExePath = path.resolve(__dirname, '..', 'node_modules', 'grunt-electron-installer', 'vendor', 'Update.exe')
spawn {cmd, args: [updateExePath]}, (error) -> done(error)
grunt.registerTask 'codesign:installer', 'Codesign AtomSetup.exe', ->
grunt.registerTask 'codesign:installer', 'CodeSign AtomSetup.exe', ->
done = @async()
cmd = process.env.JANKY_SIGNTOOL ? 'signtool'
atomSetupExePath = path.resolve(grunt.config.get('atom.buildDir'), 'installer', 'AtomSetup.exe')
spawn {cmd, args: [atomSetupExePath]}, (error) -> done(error)
grunt.registerTask 'codesign:app', 'Codesign Atom.app', ->
grunt.registerTask 'codesign:app', 'CodeSign Atom.app', ->
done = @async()
unlockKeychain (error) ->

View File

@@ -31,14 +31,9 @@ module.exports = (gruntObject) ->
cp path.join(docsOutputDir, 'api.json'), path.join(buildDir, 'atom-api.json')
grunt.registerTask 'upload-assets', 'Upload the assets to a GitHub release', ->
channel = grunt.config.get('atom.channel')
switch channel
when 'stable'
isPrerelease = false
when 'beta'
isPrerelease = true
else
return
releaseBranch = grunt.config.get('atom.releaseBranch')
isPrerelease = grunt.config.get('atom.channel') is 'beta'
return unless releaseBranch?
doneCallback = @async()
startTime = Date.now()
@@ -55,7 +50,7 @@ module.exports = (gruntObject) ->
zipAssets buildDir, assets, (error) ->
return done(error) if error?
getAtomDraftRelease isPrerelease, channel, (error, release) ->
getAtomDraftRelease isPrerelease, releaseBranch, (error, release) ->
return done(error) if error?
assetNames = (asset.assetName for asset in assets)
deleteExistingAssets release, assetNames, (error) ->

View File

@@ -5,9 +5,7 @@ module.exports = (grunt) ->
{spawn} = require('./task-helpers')(grunt)
getVersion = (callback) ->
releasableBranches = ['stable', 'beta']
channel = grunt.config.get('atom.channel')
shouldUseCommitHash = if channel in releasableBranches then false else true
shouldUseCommitHash = grunt.config.get('atom.channel') is 'dev'
inRepository = fs.existsSync(path.resolve(__dirname, '..', '..', '.git'))
{version} = require(path.join(grunt.config.get('atom.appDir'), 'package.json'))
if shouldUseCommitHash and inRepository

View File

@@ -1,7 +1,7 @@
{
"name": "atom",
"productName": "Atom",
"version": "1.7.0-dev",
"version": "1.8.0-dev",
"description": "A hackable text editor for the 21st Century.",
"main": "./src/browser/main.js",
"repository": {
@@ -37,7 +37,7 @@
"less-cache": "0.23",
"line-top-index": "0.2.0",
"marked": "^0.3.4",
"nodegit": "0.11.5",
"nodegit": "0.12.0",
"normalize-package-data": "^2.0.0",
"nslog": "^3",
"oniguruma": "^5",
@@ -54,7 +54,7 @@
"service-hub": "^0.7.0",
"source-map-support": "^0.3.2",
"temp": "0.8.1",
"text-buffer": "8.3.2",
"text-buffer": "8.4.3",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.6",
"yargs": "^3.23.0"
@@ -66,61 +66,61 @@
"atom-light-ui": "0.43.0",
"base16-tomorrow-dark-theme": "1.1.0",
"base16-tomorrow-light-theme": "1.1.1",
"one-dark-ui": "1.2.0",
"one-light-ui": "1.2.0",
"one-dark-ui": "1.3.0",
"one-light-ui": "1.3.0",
"one-dark-syntax": "1.2.0",
"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.2",
"archive-view": "0.61.1",
"autocomplete-atom-api": "0.10.0",
"autocomplete-css": "0.11.0",
"autocomplete-html": "0.7.2",
"autocomplete-plus": "2.29.0",
"autocomplete-plus": "2.29.1",
"autocomplete-snippets": "1.10.0",
"autoflow": "0.27.0",
"autosave": "0.23.1",
"background-tips": "0.26.0",
"bookmarks": "0.38.2",
"bracket-matcher": "0.80.1",
"bracket-matcher": "0.81.0",
"command-palette": "0.38.0",
"deprecation-cop": "0.54.1",
"dev-live-reload": "0.47.0",
"encoding-selector": "0.21.0",
"exception-reporting": "0.37.0",
"find-and-replace": "0.197.2",
"fuzzy-finder": "1.0.2",
"git-diff": "1.0.0",
"find-and-replace": "0.197.4",
"fuzzy-finder": "1.0.3",
"git-diff": "1.0.1",
"go-to-line": "0.30.0",
"grammar-selector": "0.48.1",
"image-view": "0.56.0",
"incompatible-packages": "0.25.1",
"image-view": "0.57.0",
"incompatible-packages": "0.26.1",
"keybinding-resolver": "0.35.0",
"line-ending-selector": "0.3.1",
"link": "0.31.0",
"markdown-preview": "0.157.3",
"line-ending-selector": "0.4.1",
"link": "0.31.1",
"markdown-preview": "0.158.0",
"metrics": "0.53.1",
"notifications": "0.62.3",
"open-on-github": "1.0.0",
"package-generator": "0.41.1",
"settings-view": "0.232.4",
"snippets": "1.0.1",
"notifications": "0.63.1",
"open-on-github": "1.0.1",
"package-generator": "1.0.0",
"settings-view": "0.235.1",
"snippets": "1.0.2",
"spell-check": "0.67.0",
"status-bar": "1.1.0",
"status-bar": "1.2.0",
"styleguide": "0.45.2",
"symbols-view": "0.111.1",
"tabs": "0.90.2",
"symbols-view": "0.112.0",
"tabs": "0.92.0",
"timecop": "0.33.1",
"tree-view": "0.201.4",
"tree-view": "0.203.3",
"update-package-dependencies": "0.10.0",
"welcome": "0.34.0",
"whitespace": "0.32.2",
"wrap-guide": "0.38.1",
"language-c": "0.51.1",
"language-clojure": "0.19.1",
"language-c": "0.51.2",
"language-clojure": "0.20.0",
"language-coffee-script": "0.46.1",
"language-csharp": "0.11.0",
"language-csharp": "0.12.0",
"language-css": "0.36.0",
"language-gfm": "0.85.0",
"language-git": "0.12.1",
@@ -129,26 +129,26 @@
"language-hyperlink": "0.16.0",
"language-java": "0.17.0",
"language-javascript": "0.111.0",
"language-json": "0.17.4",
"language-less": "0.29.0",
"language-json": "0.18.0",
"language-less": "0.29.1",
"language-make": "0.21.0",
"language-mustache": "0.13.0",
"language-objective-c": "0.15.1",
"language-perl": "0.32.0",
"language-php": "0.37.0",
"language-property-list": "0.8.0",
"language-python": "0.43.0",
"language-ruby": "0.68.1",
"language-python": "0.43.1",
"language-ruby": "0.68.4",
"language-ruby-on-rails": "0.25.0",
"language-sass": "0.45.0",
"language-shellscript": "0.21.0",
"language-sass": "0.46.0",
"language-shellscript": "0.21.1",
"language-source": "0.9.0",
"language-sql": "0.20.0",
"language-text": "0.7.0",
"language-text": "0.7.1",
"language-todo": "0.27.0",
"language-toml": "0.18.0",
"language-xml": "0.34.3",
"language-yaml": "0.25.1"
"language-xml": "0.34.4",
"language-yaml": "0.25.2"
},
"private": true,
"scripts": {

View File

@@ -22,31 +22,13 @@ FOR %%a IN (%*) DO (
)
)
rem Getting the process ID in cmd of the current cmd process: http://superuser.com/questions/881789/identify-and-kill-batch-script-started-before
set T=%TEMP%\atomCmdProcessId-%time::=%.tmp
wmic process where (Name="WMIC.exe" AND CommandLine LIKE "%%%TIME%%%") get ParentProcessId /value | find "ParentProcessId" >%T%
set /P A=<%T%
set PID=%A:~16%
del %T%
IF "%EXPECT_OUTPUT%"=="YES" (
SET ELECTRON_ENABLE_LOGGING=YES
IF "%WAIT%"=="YES" (
"%~dp0\..\..\atom.exe" --pid=%PID% %*
rem If the wait flag is set, don't exit this process until Atom tells it to.
goto waitLoop
)
ELSE (
powershell -noexit "%~dp0\..\..\atom.exe" --pid=$pid %* ; wait-event
) ELSE (
"%~dp0\..\..\atom.exe" %*
)
) ELSE (
"%~dp0\..\app\apm\bin\node.exe" "%~dp0\atom.js" %*
)
goto end
:waitLoop
sleep 1
goto waitLoop
:end

View File

@@ -1,49 +1,2 @@
#!/bin/sh
while getopts ":fhtvw-:" opt; do
case "$opt" in
-)
case "${OPTARG}" in
wait)
WAIT=1
;;
help|version)
REDIRECT_STDERR=1
EXPECT_OUTPUT=1
;;
foreground|test)
EXPECT_OUTPUT=1
;;
esac
;;
w)
WAIT=1
;;
h|v)
REDIRECT_STDERR=1
EXPECT_OUTPUT=1
;;
f|t)
EXPECT_OUTPUT=1
;;
esac
done
directory=$(dirname "$0")
WINPS=`ps | grep -i $$`
PID=`echo $WINPS | cut -d' ' -f 4`
if [ $EXPECT_OUTPUT ]; then
export ELECTRON_ENABLE_LOGGING=1
"$directory/../../atom.exe" --executed-from="$(pwd)" --pid=$PID "$@"
else
"$directory/../app/apm/bin/node.exe" "$directory/atom.js" "$@"
fi
# If the wait flag is set, don't exit this process until Atom tells it to.
if [ $WAIT ]; then
while true; do
sleep 1
done
fi
$(dirname "$0")/atom.cmd "$@"

View File

@@ -2,9 +2,24 @@
var cp = require('./utils/child-process-wrapper.js');
var runGrunt = require('./utils/run-grunt.js');
var path = require('path');
var fs = require('fs');
process.chdir(path.dirname(__dirname));
if (process.platform === 'win32') {
process.env['PATH'] = process.env['PATH']
.split(';')
.filter(function(p) {
if (fs.existsSync(path.resolve(p, 'msbuild.exe'))) {
console.log('Excluding "' + p + '" from PATH to avoid msbuild.exe mismatch')
return false;
} else {
return true;
}
})
.join(';');
}
cp.safeExec('node script/bootstrap', function() {
// build/node_modules/.bin/grunt "$@"
var args = process.argv.slice(2);

View File

@@ -1,11 +1,10 @@
#!/usr/bin/env node
var cp = require('./utils/child-process-wrapper.js');
var childProcess = require('./utils/child-process-wrapper.js');
var fs = require('fs');
var path = require('path');
var os = require('os');
var isWindows = process.platform === 'win32';
var removeCommand = isWindows ? 'rmdir /S /Q ' : 'rm -rf ';
var productName = require('../package.json').productName;
process.chdir(path.dirname(__dirname));
@@ -13,10 +12,10 @@ var home = process.env[isWindows ? 'USERPROFILE' : 'HOME'];
var tmpdir = os.tmpdir();
// Windows: Use START as a way to ignore error if Atom.exe isnt running
var killatom = isWindows ? 'START taskkill /F /IM ' + productName + '.exe' : 'pkill -9 ' + productName + ' || true';
var killAtomCommand = isWindows ? 'START taskkill /F /IM ' + productName + '.exe' : 'pkill -9 ' + productName + ' || true';
//childProcess.safeExec(killAtomCommand);
var commands = [
killatom,
var pathsToRemove = [
[__dirname, '..', 'node_modules'],
[__dirname, '..', 'build', 'node_modules'],
[__dirname, '..', 'apm', 'node_modules'],
@@ -32,37 +31,30 @@ var commands = [
[home, '.atom', 'electron'],
[tmpdir, 'atom-build'],
[tmpdir, 'atom-cached-atom-shells'],
];
var run = function() {
var next = commands.shift();
if (!next)
process.exit(0);
].map(function(pathSegments) {
return path.resolve.apply(null, pathSegments);
});
if (Array.isArray(next)) {
var pathToRemove = path.resolve.apply(path.resolve, next);
if (fs.existsSync(pathToRemove)) {
if (isWindows) {
removeFolderRecursive(pathToRemove);
} else {
next = removeCommand + pathToRemove;
cp.safeExec(next, run);
}
}
else {
return run();
}
pathsToRemove.forEach(function(pathToRemove) {
if (fs.existsSync(pathToRemove)) {
removePath(pathToRemove);
}
else
cp.safeExec(next, run);
};
run();
});
function removePath(pathToRemove) {
if (isWindows) {
removePathOnWindows(pathToRemove);
} else {
childProcess.safeExec('rm -rf ' + pathToRemove);
}
}
// Windows has a 260-char path limit for rmdir etc. Just recursively delete in Node.
var removeFolderRecursive = function(folderPath) {
function removePathOnWindows(folderPath) {
fs.readdirSync(folderPath).forEach(function(entry, index) {
var entryPath = path.join(folderPath, entry);
if (fs.lstatSync(entryPath).isDirectory()) {
removeFolderRecursive(entryPath);
removePathOnWindows(entryPath);
} else {
fs.unlinkSync(entryPath);
}

View File

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

View File

@@ -4,6 +4,7 @@ temp = require 'temp'
Package = require '../src/package'
ThemeManager = require '../src/theme-manager'
AtomEnvironment = require '../src/atom-environment'
StorageFolder = require '../src/storage-folder'
describe "AtomEnvironment", ->
describe 'window sizing methods', ->
@@ -172,25 +173,70 @@ describe "AtomEnvironment", ->
waitsForPromise ->
atom.saveState().then ->
atom.loadState().then (state) ->
expect(state).toBeNull()
expect(state).toBeFalsy()
waitsForPromise ->
loadSettings.initialPaths = [dir2, dir1]
atom.loadState().then (state) ->
expect(state).toEqual({stuff: 'cool'})
it "saves state on keydown and mousedown events", ->
it "loads state from the storage folder when it can't be found in atom.stateStore", ->
jasmine.useRealClock()
storageFolderState = {foo: 1, bar: 2}
serializedState = {someState: 42}
loadSettings = _.extend(atom.getLoadSettings(), {initialPaths: [temp.mkdirSync("project-directory")]})
spyOn(atom, 'getLoadSettings').andReturn(loadSettings)
spyOn(atom, 'serialize').andReturn(serializedState)
spyOn(atom, 'getStorageFolder').andReturn(new StorageFolder(temp.mkdirSync("config-directory")))
atom.project.setPaths(atom.getLoadSettings().initialPaths)
waitsForPromise ->
atom.stateStore.connect()
runs ->
atom.getStorageFolder().storeSync(atom.getStateKey(loadSettings.initialPaths), storageFolderState)
waitsForPromise ->
atom.loadState().then (state) -> expect(state).toEqual(storageFolderState)
waitsForPromise ->
atom.saveState()
waitsForPromise ->
atom.loadState().then (state) -> expect(state).toEqual(serializedState)
it "saves state on keydown, mousedown, and when the editor window unloads", ->
spyOn(atom, 'saveState')
keydown = new KeyboardEvent('keydown')
atom.document.dispatchEvent(keydown)
advanceClock atom.saveStateDebounceInterval
expect(atom.saveState).toHaveBeenCalled()
expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false})
expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true})
atom.saveState.reset()
mousedown = new MouseEvent('mousedown')
atom.document.dispatchEvent(mousedown)
advanceClock atom.saveStateDebounceInterval
expect(atom.saveState).toHaveBeenCalled()
expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false})
expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true})
atom.saveState.reset()
atom.unloadEditorWindow()
mousedown = new MouseEvent('mousedown')
atom.document.dispatchEvent(mousedown)
advanceClock atom.saveStateDebounceInterval
expect(atom.saveState).toHaveBeenCalledWith({isUnloading: true})
expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: false})
it "serializes the project state with all the options supplied in saveState", ->
spyOn(atom.project, 'serialize').andReturn({foo: 42})
waitsForPromise -> atom.saveState({anyOption: 'any option'})
runs ->
expect(atom.project.serialize.calls.length).toBe(1)
expect(atom.project.serialize.mostRecentCall.args[0]).toEqual({anyOption: 'any option'})
describe "openInitialEmptyEditorIfNecessary", ->
describe "when there are no paths set", ->
@@ -328,3 +374,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'

View File

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

View File

@@ -97,7 +97,7 @@ describe "BufferedProcess", ->
expect(ChildProcess.spawn.argsForCall[0][1][1]).toBe '/c'
expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe '"dir"'
it "calls the specified stdout, stderr, and exit callbacks ", ->
it "calls the specified stdout, stderr, and exit callbacks", ->
stdout = ''
stderr = ''
exitCallback = jasmine.createSpy('exit callback')
@@ -114,3 +114,30 @@ describe "BufferedProcess", ->
runs ->
expect(stderr).toContain 'apm - Atom Package Manager'
expect(stdout).toEqual ''
it "calls the specified stdout callback only with whole lines", ->
exitCallback = jasmine.createSpy('exit callback')
baseContent = "There are dozens of us! Dozens! It's as Ann as the nose on Plain's face. Can you believe that the only reason the club is going under is because it's in a terrifying neighborhood? She calls it a Mayonegg. Waiting for the Emmys. BTW did you know won 6 Emmys and was still canceled early by Fox? COME ON. I'll buy you a hundred George Michaels that you can teach to drive! Never once touched my per diem. I'd go to Craft Service, get some raw veggies, bacon, Cup-A-Soup…baby, I got a stew goin'"
content = (baseContent for _ in [1..200]).join('\n')
stdout = ''
endLength = 10
outputAlwaysEndsWithStew = true
process = new BufferedProcess
command: '/bin/echo'
args: [content]
options: {}
stdout: (lines) ->
stdout += lines
end = baseContent.substr(baseContent.length - endLength, endLength)
lineEndsWithStew = lines.substr(lines.length - endLength, endLength) is end
expect(lineEndsWithStew).toBeTrue
outputAlwaysEndsWithStew = outputAlwaysEndsWithStew and lineEndsWithStew
exit: exitCallback
waitsFor -> exitCallback.callCount is 1
runs ->
expect(outputAlwaysEndsWithStew).toBeTrue
expect(stdout).toBe content += '\n'

View File

@@ -1621,6 +1621,16 @@ describe "Config", ->
expect(color.toHexString()).toBe '#ff0000'
expect(color.toRGBAString()).toBe 'rgba(255, 0, 0, 1)'
color.red = 11
color.green = 11
color.blue = 124
color.alpha = 1
atom.config.set('foo.bar.aColor', color)
color = atom.config.get('foo.bar.aColor')
expect(color.toHexString()).toBe '#0b0b7c'
expect(color.toRGBAString()).toBe 'rgba(11, 11, 124, 1)'
it 'coerces various types to a color object', ->
atom.config.set('foo.bar.aColor', 'red')
expect(atom.config.get('foo.bar.aColor')).toEqual {red: 255, green: 0, blue: 0, alpha: 1}

View File

@@ -0,0 +1,160 @@
'use babel'
/* eslint-env jasmine */
import child_process from 'child_process'
import environmentHelpers from '../src/environment-helpers'
import os from 'os'
describe('Environment handling', () => {
let originalEnv
let options
beforeEach(() => {
originalEnv = process.env
delete process._originalEnv
options = {
platform: process.platform,
env: Object.assign({}, process.env)
}
})
afterEach(() => {
process.env = originalEnv
delete process._originalEnv
})
describe('on OSX, when PWD is not set', () => {
beforeEach(() => {
options.platform = 'darwin'
})
describe('needsPatching', () => {
it('returns true if PWD is unset', () => {
delete options.env.PWD
expect(environmentHelpers.needsPatching(options)).toBe(true)
options.env.PWD = undefined
expect(environmentHelpers.needsPatching(options)).toBe(true)
options.env.PWD = null
expect(environmentHelpers.needsPatching(options)).toBe(true)
options.env.PWD = false
expect(environmentHelpers.needsPatching(options)).toBe(true)
})
it('returns false if PWD is set', () => {
options.env.PWD = 'xterm'
expect(environmentHelpers.needsPatching(options)).toBe(false)
})
})
describe('normalize', () => {
it('changes process.env if PWD is unset', () => {
if (process.platform === 'win32') {
return
}
delete options.env.PWD
environmentHelpers.normalize(options)
expect(process._originalEnv).toBeDefined()
expect(process._originalEnv).toBeTruthy()
expect(process.env).toBeDefined()
expect(process.env).toBeTruthy()
expect(process.env.PWD).toBeDefined()
expect(process.env.PWD).toBeTruthy()
expect(process.env.PATH).toBeDefined()
expect(process.env.PATH).toBeTruthy()
expect(process.env.ATOM_HOME).toBeDefined()
expect(process.env.ATOM_HOME).toBeTruthy()
})
})
})
describe('on a platform other than OSX', () => {
beforeEach(() => {
options.platform = 'penguin'
})
describe('needsPatching', () => {
it('returns false if PWD is set or unset', () => {
delete options.env.PWD
expect(environmentHelpers.needsPatching(options)).toBe(false)
options.env.PWD = undefined
expect(environmentHelpers.needsPatching(options)).toBe(false)
options.env.PWD = null
expect(environmentHelpers.needsPatching(options)).toBe(false)
options.env.PWD = false
expect(environmentHelpers.needsPatching(options)).toBe(false)
options.env.PWD = '/'
expect(environmentHelpers.needsPatching(options)).toBe(false)
})
it('returns false for linux', () => {
options.platform = 'linux'
options.PWD = '/'
expect(environmentHelpers.needsPatching(options)).toBe(false)
})
it('returns false for windows', () => {
options.platform = 'win32'
options.PWD = 'c:\\'
expect(environmentHelpers.needsPatching(options)).toBe(false)
})
})
describe('normalize', () => {
it('does not change the environment', () => {
if (process.platform === 'win32') {
return
}
delete options.env.PWD
environmentHelpers.normalize(options)
expect(process._originalEnv).toBeUndefined()
expect(process.env).toBeDefined()
expect(process.env).toBeTruthy()
expect(process.env.PATH).toBeDefined()
expect(process.env.PATH).toBeTruthy()
expect(process.env.PWD).toBeUndefined()
expect(process.env.PATH).toBe(originalEnv.PATH)
expect(process.env.ATOM_HOME).toBeDefined()
expect(process.env.ATOM_HOME).toBeTruthy()
})
})
})
describe('getFromShell', () => {
describe('when things are configured properly', () => {
beforeEach(() => {
spyOn(child_process, 'spawnSync').andReturn({
stdout: 'FOO=BAR' + os.EOL + 'TERM=xterm-something' + os.EOL +
'PATH=/usr/bin:/bin:/usr/sbin:/sbin:/crazy/path'
})
})
it('returns an object containing the information from the user\'s shell environment', () => {
let env = environmentHelpers.getFromShell()
expect(env.FOO).toEqual('BAR')
expect(env.TERM).toEqual('xterm-something')
expect(env.PATH).toEqual('/usr/bin:/bin:/usr/sbin:/sbin:/crazy/path')
})
})
describe('when an error occurs launching the shell', () => {
beforeEach(() => {
spyOn(child_process, 'spawnSync').andReturn({
error: new Error('testing when an error occurs')
})
})
it('returns undefined', () => {
expect(environmentHelpers.getFromShell()).toBeUndefined()
})
it('leaves the environment as-is when normalize() is called', () => {
options.platform = 'darwin'
delete options.env.PWD
expect(environmentHelpers.needsPatching(options)).toBe(true)
environmentHelpers.normalize(options)
expect(process.env).toBeDefined()
expect(process._originalEnv).toBeUndefined()
})
})
})
})

View File

@@ -0,0 +1,8 @@
{
"name": "package-with-a-git-prefixed-git-repo-url",
"repository": {
"type": "git",
"url": "git+https://github.com/example/repo.git"
},
"_id": "this is here to simulate the URL being already normalized by npm. we still need to stript git+ from the beginning and .git from the end."
}

View File

@@ -9,12 +9,23 @@ var quicksort = function () {
// Wowza
if (items.length <= 1) return items;
var pivot = items.shift(), current, left = [], right = [];
/*
This is a multiline comment block with
an empty line inside of it.
Awesome.
*/
while(items.length > 0) {
current = items.shift();
current < pivot ? left.push(current) : right.push(current);
}
// This is a collection of
// single line comments
// ...with an empty line
// among it, geez!
return sort(left).concat(pivot).concat(sort(right));
};
// this is a single-line comment
return sort(Array.apply(this, arguments));
};
};

View File

@@ -422,6 +422,44 @@ describe('GitRepositoryAsync', () => {
expect(repo.isStatusModified(status)).toBe(true)
expect(repo.isStatusNew(status)).toBe(false)
})
it('emits did-change-statuses if the status changes', async () => {
const someNewPath = path.join(workingDirectory, 'MyNewJSFramework.md')
fs.writeFileSync(someNewPath, '')
const statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatuses(statusHandler)
await repo.refreshStatus()
waitsFor('the onDidChangeStatuses handler to be called', () => statusHandler.callCount > 0)
})
it('emits did-change-statuses if the branch changes', async () => {
const statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatuses(statusHandler)
repo._refreshBranch = jasmine.createSpy('_refreshBranch').andCallFake(() => {
return Promise.resolve(true)
})
await repo.refreshStatus()
waitsFor('the onDidChangeStatuses handler to be called', () => statusHandler.callCount > 0)
})
it('emits did-change-statuses if the ahead/behind changes', async () => {
const statusHandler = jasmine.createSpy('statusHandler')
repo.onDidChangeStatuses(statusHandler)
repo._refreshAheadBehindCount = jasmine.createSpy('_refreshAheadBehindCount').andCallFake(() => {
return Promise.resolve(true)
})
await repo.refreshStatus()
waitsFor('the onDidChangeStatuses handler to be called', () => statusHandler.callCount > 0)
})
})
describe('.isProjectAtRoot()', () => {
@@ -541,7 +579,7 @@ describe('GitRepositoryAsync', () => {
await atom.workspace.open('file.txt')
project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
project2.deserialize(atom.project.serialize(), atom.deserializers)
project2.deserialize(atom.project.serialize({isUnloading: true}))
const repo = project2.getRepositories()[0].async
waitsForPromise(() => repo.refreshStatus())
@@ -676,7 +714,7 @@ describe('GitRepositoryAsync', () => {
repo = GitRepositoryAsync.open(workingDirectory)
})
it('returns 0, 0 for a branch with no upstream', async () => {
it('returns 1, 0 for a branch which is ahead by 1', async () => {
await repo.refreshStatus()
const {ahead, behind} = await repo.getCachedUpstreamAheadBehindCount('You-Dont-Need-jQuery')

View File

@@ -347,7 +347,7 @@ describe "GitRepository", ->
runs ->
project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
project2.deserialize(atom.project.serialize(), atom.deserializers)
project2.deserialize(atom.project.serialize({isUnloading: false}))
buffer = project2.getBuffers()[0]
waitsFor ->

View File

@@ -430,7 +430,7 @@ describe "LanguageMode", ->
languageMode.foldAll()
fold1 = editor.tokenizedLineForScreenRow(0).fold
expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 19]
expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 30]
fold1.destroy()
fold2 = editor.tokenizedLineForScreenRow(1).fold
@@ -441,6 +441,14 @@ describe "LanguageMode", ->
fold4 = editor.tokenizedLineForScreenRow(3).fold
expect([fold4.getStartRow(), fold4.getEndRow()]).toEqual [6, 8]
fold5 = editor.tokenizedLineForScreenRow(6).fold
expect([fold5.getStartRow(), fold5.getEndRow()]).toEqual [11, 16]
fold5.destroy()
fold6 = editor.tokenizedLineForScreenRow(13).fold
expect([fold6.getStartRow(), fold6.getEndRow()]).toEqual [21, 22]
fold6.destroy()
describe ".foldAllAtIndentLevel()", ->
it "folds every foldable range at a given indentLevel", ->
languageMode.foldAllAtIndentLevel(2)
@@ -450,19 +458,48 @@ describe "LanguageMode", ->
fold1.destroy()
fold2 = editor.tokenizedLineForScreenRow(11).fold
expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [11, 14]
expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [11, 16]
fold2.destroy()
fold3 = editor.tokenizedLineForScreenRow(17).fold
expect([fold3.getStartRow(), fold3.getEndRow()]).toEqual [17, 20]
fold3.destroy()
fold4 = editor.tokenizedLineForScreenRow(21).fold
expect([fold4.getStartRow(), fold4.getEndRow()]).toEqual [21, 22]
fold4.destroy()
fold5 = editor.tokenizedLineForScreenRow(24).fold
expect([fold5.getStartRow(), fold5.getEndRow()]).toEqual [24, 25]
fold5.destroy()
it "does not fold anything but the indentLevel", ->
languageMode.foldAllAtIndentLevel(0)
fold1 = editor.tokenizedLineForScreenRow(0).fold
expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 19]
expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 30]
fold1.destroy()
fold2 = editor.tokenizedLineForScreenRow(5).fold
expect(fold2).toBeFalsy()
describe ".isFoldableAtBufferRow(bufferRow)", ->
it "returns true if the line starts a multi-line comment", ->
expect(languageMode.isFoldableAtBufferRow(1)).toBe true
expect(languageMode.isFoldableAtBufferRow(6)).toBe true
expect(languageMode.isFoldableAtBufferRow(8)).toBe false
expect(languageMode.isFoldableAtBufferRow(11)).toBe true
expect(languageMode.isFoldableAtBufferRow(15)).toBe false
expect(languageMode.isFoldableAtBufferRow(17)).toBe true
expect(languageMode.isFoldableAtBufferRow(21)).toBe true
expect(languageMode.isFoldableAtBufferRow(24)).toBe true
expect(languageMode.isFoldableAtBufferRow(28)).toBe false
it "does not return true for a line in the middle of a comment that's followed by an indented line", ->
expect(languageMode.isFoldableAtBufferRow(7)).toBe false
editor.buffer.insert([8, 0], ' ')
expect(languageMode.isFoldableAtBufferRow(7)).toBe false
describe "css", ->
beforeEach ->
waitsForPromise ->

View File

@@ -17,6 +17,20 @@ describe "PackageManager", ->
beforeEach ->
workspaceElement = atom.views.getView(atom.workspace)
describe "::getApmPath()", ->
it "returns the path to the apm command", ->
apmPath = path.join(process.resourcesPath, "app", "apm", "bin", "apm")
if process.platform is 'win32'
apmPath += ".cmd"
expect(atom.packages.getApmPath()).toBe apmPath
describe "when the core.apmPath setting is set", ->
beforeEach ->
atom.config.set("core.apmPath", "/path/to/apm")
it "returns the value of the core.apmPath config setting", ->
expect(atom.packages.getApmPath()).toBe "/path/to/apm"
describe "::loadPackage(name)", ->
beforeEach ->
atom.config.set("core.disabledPackages", [])
@@ -55,12 +69,17 @@ describe "PackageManager", ->
it "normalizes short repository urls in package.json", ->
{metadata} = atom.packages.loadPackage("package-with-short-url-package-json")
expect(metadata.repository.type).toBe "git"
expect(metadata.repository.url).toBe "https://github.com/example/repo.git"
expect(metadata.repository.url).toBe "https://github.com/example/repo"
{metadata} = atom.packages.loadPackage("package-with-invalid-url-package-json")
expect(metadata.repository.type).toBe "git"
expect(metadata.repository.url).toBe "foo"
it "trims git+ from the beginning and .git from the end of repository URLs, even if npm already normalized them ", ->
{metadata} = atom.packages.loadPackage("package-with-prefixed-and-suffixed-repo-url")
expect(metadata.repository.type).toBe "git"
expect(metadata.repository.url).toBe "https://github.com/example/repo"
it "returns null if the package is not found in any package directory", ->
spyOn(console, 'warn')
expect(atom.packages.loadPackage("this-package-cannot-be-found")).toBeNull()

View File

@@ -0,0 +1,34 @@
PaneAxis = require '../src/pane-axis'
PaneContainer = require '../src/pane-container'
Pane = require '../src/pane'
buildPane = ->
new Pane({
applicationDelegate: atom.applicationDelegate,
config: atom.config,
deserializerManager: atom.deserializers,
notificationManager: atom.notifications
})
describe "PaneAxisElement", ->
it "correctly subscribes and unsubscribes to the underlying model events on attach/detach", ->
container = new PaneContainer(config: atom.config, applicationDelegate: atom.applicationDelegate)
axis = new PaneAxis
axis.setContainer(container)
axisElement = atom.views.getView(axis)
panes = [buildPane(), buildPane(), buildPane()]
jasmine.attachToDOM(axisElement)
axis.addChild(panes[0])
expect(axisElement.children[0]).toBe(atom.views.getView(panes[0]))
axisElement.remove()
axis.addChild(panes[1])
expect(axisElement.children[2]).toBeUndefined()
jasmine.attachToDOM(axisElement)
expect(axisElement.children[2]).toBe(atom.views.getView(panes[1]))
axis.addChild(panes[2])
expect(axisElement.children[4]).toBe(atom.views.getView(panes[2]))

View File

@@ -1,5 +1,6 @@
{extend} = require 'underscore-plus'
{Emitter} = require 'event-kit'
Grim = require 'grim'
Pane = require '../src/pane'
PaneAxis = require '../src/pane-axis'
PaneContainer = require '../src/pane-container'
@@ -92,7 +93,7 @@ describe "Pane", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B")]))
[item1, item2] = pane.getItems()
item3 = new Item("C")
pane.addItem(item3, 1)
pane.addItem(item3, index: 1)
expect(pane.getItems()).toEqual [item1, item3, item2]
it "adds the item after the active item if no index is provided", ->
@@ -115,7 +116,7 @@ describe "Pane", ->
pane.onDidAddItem (event) -> events.push(event)
item = new Item("C")
pane.addItem(item, 1)
pane.addItem(item, index: 1)
expect(events).toEqual [{item, index: 1, moved: false}]
it "throws an exception if the item is already present on a pane", ->
@@ -132,13 +133,56 @@ describe "Pane", ->
expect(-> pane.addItem('foo')).toThrow()
expect(-> pane.addItem(1)).toThrow()
it "destroys any existing pending item if the new item is pending", ->
it "destroys any existing pending item", ->
pane = new Pane(paneParams(items: []))
itemA = new Item("A")
itemB = new Item("B")
pane.addItem(itemA, undefined, false, true)
pane.addItem(itemB, undefined, false, true)
expect(itemA.isDestroyed()).toBe true
itemC = new Item("C")
pane.addItem(itemA, pending: false)
pane.addItem(itemB, pending: true)
pane.addItem(itemC, pending: false)
expect(itemB.isDestroyed()).toBe true
it "adds the new item before destroying any existing pending item", ->
eventOrder = []
pane = new Pane(paneParams(items: []))
itemA = new Item("A")
itemB = new Item("B")
pane.addItem(itemA, pending: true)
pane.onDidAddItem ({item}) ->
eventOrder.push("add") if item is itemB
pane.onDidRemoveItem ({item}) ->
eventOrder.push("remove") if item is itemA
pane.addItem(itemB)
waitsFor ->
eventOrder.length is 2
runs ->
expect(eventOrder).toEqual ["add", "remove"]
describe "when using the old API of ::addItem(item, index)", ->
beforeEach ->
spyOn Grim, "deprecate"
it "supports the older public API", ->
pane = new Pane(paneParams(items: []))
itemA = new Item("A")
itemB = new Item("B")
itemC = new Item("C")
pane.addItem(itemA, 0)
pane.addItem(itemB, 0)
pane.addItem(itemC, 0)
expect(pane.getItems()).toEqual [itemC, itemB, itemA]
it "shows a deprecation warning", ->
pane = new Pane(paneParams(items: []))
pane.addItem(new Item(), 2)
expect(Grim.deprecate).toHaveBeenCalledWith "Pane::addItem(item, 2) is deprecated in favor of Pane::addItem(item, {index: 2})"
describe "::activateItem(item)", ->
pane = null
@@ -172,17 +216,52 @@ describe "Pane", ->
itemD = new Item("D")
it "replaces the active item if it is pending", ->
pane.activateItem(itemC, true)
pane.activateItem(itemC, pending: true)
expect(pane.getItems().map (item) -> item.name).toEqual ['A', 'C', 'B']
pane.activateItem(itemD, true)
pane.activateItem(itemD, pending: true)
expect(pane.getItems().map (item) -> item.name).toEqual ['A', 'D', 'B']
it "adds the item after the active item if it is not pending", ->
pane.activateItem(itemC, true)
pane.activateItem(itemC, pending: true)
pane.activateItemAtIndex(2)
pane.activateItem(itemD, true)
pane.activateItem(itemD, pending: true)
expect(pane.getItems().map (item) -> item.name).toEqual ['A', 'B', 'D']
describe "::setPendingItem", ->
pane = null
beforeEach ->
pane = atom.workspace.getActivePane()
it "changes the pending item", ->
expect(pane.getPendingItem()).toBeNull()
pane.setPendingItem("fake item")
expect(pane.getPendingItem()).toEqual "fake item"
describe "::onItemDidTerminatePendingState callback", ->
pane = null
callbackCalled = false
beforeEach ->
pane = atom.workspace.getActivePane()
callbackCalled = false
it "is called when the pending item changes", ->
pane.setPendingItem("fake item one")
pane.onItemDidTerminatePendingState (item) ->
callbackCalled = true
expect(item).toEqual "fake item one"
pane.setPendingItem("fake item two")
expect(callbackCalled).toBeTruthy()
it "has access to the new pending item via ::getPendingItem", ->
pane.setPendingItem("fake item one")
pane.onItemDidTerminatePendingState (item) ->
callbackCalled = true
expect(pane.getPendingItem()).toEqual "fake item two"
pane.setPendingItem("fake item two")
expect(callbackCalled).toBeTruthy()
describe "::activateNextRecentlyUsedItem() and ::activatePreviousRecentlyUsedItem()", ->
it "sets the active item to the next/previous item in the itemStack, looping around at either end", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C"), new Item("D"), new Item("E")]))
@@ -302,6 +381,10 @@ describe "Pane", ->
expect(pane.itemStack).toEqual [item2]
expect(pane.getActiveItem()).toBe item2
pane.destroyItem(item2)
expect(pane.itemStack).toEqual []
expect(pane.getActiveItem()).toBeUndefined()
it "invokes ::onWillDestroyItem() observers before destroying the item", ->
events = []
pane.onWillDestroyItem (event) ->
@@ -643,6 +726,23 @@ describe "Pane", ->
expect(pane2.isDestroyed()).toBe true
expect(item4.isDestroyed()).toBe false
describe "when the item being moved is pending", ->
it "is made permanent in the new pane", ->
item6 = new Item("F")
pane1.addItem(item6, pending: true)
expect(pane1.getPendingItem()).toEqual item6
pane1.moveItemToPane(item6, pane2, 0)
expect(pane2.getPendingItem()).not.toEqual item6
describe "when the target pane has a pending item", ->
it "does not destroy the pending item", ->
item6 = new Item("F")
pane1.addItem(item6, pending: true)
expect(pane1.getPendingItem()).toEqual item6
pane2.moveItemToPane(item5, pane1, 0)
expect(pane1.getPendingItem()).toEqual item6
describe "split methods", ->
[pane1, item1, container] = []

View File

@@ -21,6 +21,14 @@ describe "Project", ->
afterEach ->
deserializedProject?.destroy()
it "does not deserialize paths to non directories", ->
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
state = atom.project.serialize()
state.paths.push('/directory/that/does/not/exist')
state.paths.push(path.join(__dirname, 'fixtures', 'sample.js'))
deserializedProject.deserialize(state, atom.deserializers)
expect(deserializedProject.getPaths()).toEqual(atom.project.getPaths())
it "does not include unretained buffers in the serialized state", ->
waitsForPromise ->
atom.project.bufferForPath('a')
@@ -29,7 +37,7 @@ describe "Project", ->
expect(atom.project.getBuffers().length).toBe 1
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
deserializedProject.deserialize(atom.project.serialize(), atom.deserializers)
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
expect(deserializedProject.getBuffers().length).toBe 0
it "listens for destroyed events on deserialized buffers and removes them when they are destroyed", ->
@@ -39,7 +47,7 @@ describe "Project", ->
runs ->
expect(atom.project.getBuffers().length).toBe 1
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
deserializedProject.deserialize(atom.project.serialize(), atom.deserializers)
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
expect(deserializedProject.getBuffers().length).toBe 1
deserializedProject.getBuffers()[0].destroy()
@@ -56,7 +64,7 @@ describe "Project", ->
expect(atom.project.getBuffers().length).toBe 1
fs.mkdirSync(pathToOpen)
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
deserializedProject.deserialize(atom.project.serialize(), atom.deserializers)
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
expect(deserializedProject.getBuffers().length).toBe 0
it "does not deserialize buffers when their path is inaccessible", ->
@@ -70,9 +78,26 @@ describe "Project", ->
expect(atom.project.getBuffers().length).toBe 1
fs.chmodSync(pathToOpen, '000')
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
deserializedProject.deserialize(atom.project.serialize(), atom.deserializers)
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
expect(deserializedProject.getBuffers().length).toBe 0
it "serializes marker layers only if Atom is quitting", ->
waitsForPromise ->
atom.workspace.open('a')
runs ->
bufferA = atom.project.getBuffers()[0]
layerA = bufferA.addMarkerLayer(maintainHistory: true)
markerA = layerA.markPosition([0, 3])
notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
notQuittingProject.deserialize(atom.project.serialize({isUnloading: false}))
expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).toBeUndefined()
quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
quittingProject.deserialize(atom.project.serialize({isUnloading: true}))
expect(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).not.toBeUndefined()
describe "when an editor is saved and the project has no path", ->
it "sets the project's path to the saved file's parent directory", ->
tempFile = temp.openSync().path

View File

@@ -5,25 +5,25 @@ path = require 'path'
temp = require 'temp'
SquirrelUpdate = require '../src/browser/squirrel-update'
describe "Windows squirrel updates", ->
describe "Windows Squirrel Update", ->
tempHomeDirectory = null
originalSpawn = ChildProcess.spawn
harmlessSpawn = ->
# Just spawn something that won't actually modify the host
if process.platform is 'win32'
originalSpawn('dir')
else
originalSpawn('ls')
beforeEach ->
# Prevent the actually home directory from being manipulated
# Prevent the actual home directory from being manipulated
tempHomeDirectory = temp.mkdirSync('atom-temp-home-')
spyOn(fs, 'getHomeDirectory').andReturn(tempHomeDirectory)
# Prevent any commands from actually running and affecting the host
originalSpawn = ChildProcess.spawn
spyOn(ChildProcess, 'spawn').andCallFake (command, args) ->
if path.basename(command) is 'Update.exe' and args?[0] is '--createShortcut'
fs.writeFileSync(path.join(tempHomeDirectory, 'Desktop', 'Atom.lnk'), '')
# Just spawn something that won't actually modify the host
if process.platform is 'win32'
originalSpawn('dir')
else
originalSpawn('ls')
harmlessSpawn()
it "ignores errors spawning Squirrel", ->
jasmine.unspy(ChildProcess, 'spawn')
@@ -67,28 +67,55 @@ describe "Windows squirrel updates", ->
runs ->
expect(SquirrelUpdate.handleStartupEvent(app, '--not-squirrel')).toBe false
it "keeps the desktop shortcut deleted on updates if it was previously deleted after install", ->
desktopShortcutPath = path.join(tempHomeDirectory, 'Desktop', 'Atom.lnk')
expect(fs.existsSync(desktopShortcutPath)).toBe false
app = quit: jasmine.createSpy('quit')
expect(SquirrelUpdate.handleStartupEvent(app, '--squirrel-install')).toBe true
waitsFor ->
app.quit.callCount is 1
runs ->
app.quit.reset()
expect(fs.existsSync(desktopShortcutPath)).toBe true
fs.removeSync(desktopShortcutPath)
expect(fs.existsSync(desktopShortcutPath)).toBe false
expect(SquirrelUpdate.handleStartupEvent(app, '--squirrel-updated')).toBe true
waitsFor ->
app.quit.callCount is 1
runs ->
describe "Desktop shortcut", ->
desktopShortcutPath = '/non/existing/path'
beforeEach ->
desktopShortcutPath = path.join(tempHomeDirectory, 'Desktop', 'Atom.lnk')
jasmine.unspy(ChildProcess, 'spawn')
spyOn(ChildProcess, 'spawn').andCallFake (command, args) ->
if path.basename(command) is 'Update.exe' and args?[0] is '--createShortcut'
fs.writeFileSync(path.join(tempHomeDirectory, 'Desktop', 'Atom.lnk'), '')
harmlessSpawn()
else
throw new Error("API not mocked")
it "does not exist before install", ->
expect(fs.existsSync(desktopShortcutPath)).toBe false
describe "on install", ->
beforeEach ->
app = quit: jasmine.createSpy('quit')
SquirrelUpdate.handleStartupEvent(app, '--squirrel-install')
waitsFor ->
app.quit.callCount is 1
it "creates desktop shortcut", ->
expect(fs.existsSync(desktopShortcutPath)).toBe true
describe "when shortcut is deleted and then app is updated", ->
beforeEach ->
fs.removeSync(desktopShortcutPath)
expect(fs.existsSync(desktopShortcutPath)).toBe false
app = quit: jasmine.createSpy('quit')
SquirrelUpdate.handleStartupEvent(app, '--squirrel-updated')
waitsFor ->
app.quit.callCount is 1
it "does not recreate shortcut", ->
expect(fs.existsSync(desktopShortcutPath)).toBe false
describe "when shortcut is kept and app is updated", ->
beforeEach ->
app = quit: jasmine.createSpy('quit')
SquirrelUpdate.handleStartupEvent(app, '--squirrel-updated')
waitsFor ->
app.quit.callCount is 1
it "still has desktop shortcut", ->
expect(fs.existsSync(desktopShortcutPath)).toBe true
describe ".restartAtom", ->
it "quits the app and spawns a new one", ->

View File

@@ -1840,17 +1840,22 @@ describe('TextEditorComponent', function () {
expect(component.lineNodeForScreenRow(2).dataset.screenRow).toBe("2")
})
it('measures block decorations taking into account both top and bottom margins', async function () {
it('measures block decorations taking into account both top and bottom margins of the element and its children', async function () {
let [item, blockDecoration] = createBlockDecorationBeforeScreenRow(0, {className: "decoration-1"})
let child = document.createElement("div")
child.style.height = "7px"
child.style.width = "30px"
child.style.marginBottom = "20px"
item.appendChild(child)
atom.styles.addStyleSheet(
'atom-text-editor .decoration-1 { width: 30px; height: 30px; margin-top: 10px; margin-bottom: 5px; }',
'atom-text-editor .decoration-1 { width: 30px; margin-top: 10px; }',
{context: 'atom-text-editor'}
)
await nextAnimationFramePromise() // causes the DOM to update and to retrieve new styles
await nextAnimationFramePromise() // applies the changes
expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 30 + 10 + 5 + "px")
expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 10 + 7 + 20 + "px")
expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)")
expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px")
expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`)

View File

@@ -635,16 +635,28 @@ describe "TextEditorPresenter", ->
expectStateUpdate presenter, -> presenter.setExplicitHeight(500)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe 500
it "adds the computed clientHeight to the computed scrollHeight if editor.scrollPastEnd is true", ->
presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10)
expectStateUpdate presenter, -> presenter.setScrollTop(300)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
describe "scrollPastEnd", ->
it "adds the computed clientHeight to the computed scrollHeight if editor.scrollPastEnd is true", ->
presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10)
expectStateUpdate presenter, -> presenter.setScrollTop(300)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", true)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight + presenter.clientHeight - (presenter.lineHeight * 3)
expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", true)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight + presenter.clientHeight - (presenter.lineHeight * 3)
expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
it "doesn't add the computed clientHeight to the computed scrollHeight if editor.scrollPastEnd is true but the presenter is created with scrollPastEnd as false", ->
presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10, scrollPastEnd: false)
expectStateUpdate presenter, -> presenter.setScrollTop(300)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", true)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
describe ".scrollTop", ->
it "tracks the value of ::scrollTop", ->

View File

@@ -0,0 +1,38 @@
TextEditorRegistry = require '../src/text-editor-registry'
describe "TextEditorRegistry", ->
[registry, editor] = []
beforeEach ->
registry = new TextEditorRegistry
describe "when a TextEditor is added", ->
it "gets added to the list of registered editors", ->
editor = {}
registry.add(editor)
expect(registry.editors.size).toBe 1
expect(registry.editors.has(editor)).toBe(true)
it "returns a Disposable that can unregister the editor", ->
editor = {}
disposable = registry.add(editor)
expect(registry.editors.size).toBe 1
disposable.dispose()
expect(registry.editors.size).toBe 0
describe "when the registry is observed", ->
it "calls the callback for current and future editors until unsubscribed", ->
[editor1, editor2, editor3] = [{}, {}, {}]
registry.add(editor1)
subscription = registry.observe spy = jasmine.createSpy()
expect(spy.calls.length).toBe 1
registry.add(editor2)
expect(spy.calls.length).toBe 2
expect(spy.argsForCall[0][0]).toBe editor1
expect(spy.argsForCall[1][0]).toBe editor2
subscription.dispose()
registry.add(editor3)
expect(spy.calls.length).toBe 2

View File

@@ -2132,20 +2132,31 @@ describe "TextEditor", ->
editor.splitSelectionsIntoLines()
expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 3]]]
describe ".consolidateSelections()", ->
it "destroys all selections but the least recent, returning true if any selections were destroyed", ->
editor.setSelectedBufferRange([[3, 16], [3, 21]])
selection1 = editor.getLastSelection()
describe "::consolidateSelections()", ->
makeMultipleSelections = ->
selection.setBufferRange [[3, 16], [3, 21]]
selection2 = editor.addSelectionForBufferRange([[3, 25], [3, 34]])
selection3 = editor.addSelectionForBufferRange([[8, 4], [8, 10]])
selection4 = editor.addSelectionForBufferRange([[1, 6], [1, 10]])
expect(editor.getSelections()).toEqual [selection, selection2, selection3, selection4]
[selection, selection2, selection3, selection4]
it "destroys all selections but the oldest selection and autoscrolls to it, returning true if any selections were destroyed", ->
[selection1] = makeMultipleSelections()
autoscrollEvents = []
editor.onDidRequestAutoscroll (event) -> autoscrollEvents.push(event)
expect(editor.getSelections()).toEqual [selection1, selection2, selection3]
expect(editor.consolidateSelections()).toBeTruthy()
expect(editor.getSelections()).toEqual [selection1]
expect(selection1.isEmpty()).toBeFalsy()
expect(editor.consolidateSelections()).toBeFalsy()
expect(editor.getSelections()).toEqual [selection1]
expect(autoscrollEvents).toEqual([
{screenRange: selection1.getScreenRange(), options: {center: true, reversed: false}}
])
describe "when the cursor is moved while there is a selection", ->
makeSelection = -> selection.setBufferRange [[1, 2], [1, 5]]
@@ -5817,3 +5828,30 @@ describe "TextEditor", ->
screenRange: marker1.getRange(),
rangeIsReversed: false
}
describe "when the editor is constructed with the showInvisibles option set to false", ->
beforeEach ->
atom.workspace.destroyActivePane()
waitsForPromise ->
atom.workspace.open('sample.js', showInvisibles: false).then (o) -> editor = o
it "ignores invisibles even if editor.showInvisibles is true", ->
atom.config.set('editor.showInvisibles', true)
invisibles = editor.tokenizedLineForScreenRow(0).invisibles
expect(invisibles).toBe(null)
describe "when the editor is constructed with the grammar option set", ->
beforeEach ->
atom.workspace.destroyActivePane()
waitsForPromise ->
atom.packages.activatePackage('language-coffee-script')
waitsForPromise ->
atom.workspace.open('sample.js', grammar: atom.grammars.grammarForScopeName('source.coffee')).then (o) -> editor = o
it "sets the grammar", ->
expect(editor.getGrammar().name).toBe 'CoffeeScript'
describe "::getElement", ->
it "returns an element", ->
expect(editor.getElement() instanceof HTMLElement).toBe(true)

View File

@@ -28,6 +28,12 @@ describe "TooltipManager", ->
hover element, ->
expect(document.body.querySelector(".tooltip")).toHaveText("Title")
it "creates a tooltip immediately if the trigger type is manual", ->
disposable = manager.add element, title: "Title", trigger: "manual"
expect(document.body.querySelector(".tooltip")).toHaveText("Title")
disposable.dispose()
expect(document.body.querySelector(".tooltip")).toBeNull()
it "allows jQuery elements to be passed as the target", ->
element2 = document.createElement('div')
jasmine.attachToDOM(element2)

View File

@@ -23,6 +23,15 @@ describe "ViewRegistry", ->
component = new TestComponent
expect(registry.getView(component)).toBe component.element
describe "when passed an object with a getElement function", ->
it "returns the return value of getElement if it's an instance of HTMLElement", ->
class TestComponent
getElement: ->
@myElement ?= document.createElement('div')
component = new TestComponent
expect(registry.getView(component)).toBe component.myElement
describe "when passed a model object", ->
describe "when a view provider is registered matching the object's constructor", ->
it "constructs a view element and assigns the model on it", ->

View File

@@ -22,11 +22,11 @@ describe "Workspace", ->
describe "serialization", ->
simulateReload = ->
workspaceState = atom.workspace.serialize()
projectState = atom.project.serialize()
projectState = atom.project.serialize({isUnloading: true})
atom.workspace.destroy()
atom.project.destroy()
atom.project = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm.bind(atom)})
atom.project.deserialize(projectState, atom.deserializers)
atom.project.deserialize(projectState)
atom.workspace = new Workspace({
config: atom.config, project: atom.project, packageManager: atom.packages,
grammarRegistry: atom.grammars, deserializerManager: atom.deserializers,
@@ -604,6 +604,53 @@ describe "Workspace", ->
runs ->
expect(pane.getPendingItem()).toBeNull()
describe "when opening will switch from a pending tab to a permanent tab", ->
it "keeps the pending tab open", ->
editor1 = null
editor2 = null
waitsForPromise ->
atom.workspace.open('sample.txt').then (o) ->
editor1 = o
waitsForPromise ->
atom.workspace.open('sample2.txt', pending: true).then (o) ->
editor2 = o
runs ->
pane = atom.workspace.getActivePane()
pane.activateItem(editor1)
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 destroy 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()

View File

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

View File

@@ -11,6 +11,7 @@ Model = require './model'
WindowEventHandler = require './window-event-handler'
StylesElement = require './styles-element'
StateStore = require './state-store'
StorageFolder = require './storage-folder'
{getWindowLoadSettings, setWindowLoadSettings} = require './window-load-settings-helpers'
registerDefaultCommands = require './register-default-commands'
@@ -40,6 +41,8 @@ Project = require './project'
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'
@@ -111,6 +114,12 @@ class AtomEnvironment extends Model
# Public: A {Workspace} instance
workspace: null
# Public: A {TextEditorRegistry} instance
textEditors: null
# Private: An {AutoUpdateManager} instance
autoUpdater: null
saveStateDebounceInterval: 1000
###
@@ -119,8 +128,9 @@ class AtomEnvironment extends Model
# Call .loadOrCreate instead
constructor: (params={}) ->
{@blobStore, @applicationDelegate, @window, @document, configDirPath, @enablePersistence, onlyLoadBaseStyleSheets} = params
{@blobStore, @applicationDelegate, @window, @document, @configDirPath, @enablePersistence, onlyLoadBaseStyleSheets} = params
@unloaded = false
@loadTime = null
{devMode, safeMode, resourcePath, clearWindowState} = @getLoadSettings()
@@ -129,7 +139,9 @@ class AtomEnvironment extends Model
@stateStore = new StateStore('AtomEnvironments', 1)
@stateStore.clear() if clearWindowState
if clearWindowState
@getStorageFolder().clear()
@stateStore.clear()
@deserializers = new DeserializerManager(this)
@deserializeTimings = {}
@@ -138,10 +150,10 @@ class AtomEnvironment extends Model
@notifications = new NotificationManager
@config = new Config({configDirPath, resourcePath, notificationManager: @notifications, @enablePersistence})
@config = new Config({@configDirPath, resourcePath, notificationManager: @notifications, @enablePersistence})
@setConfigSchema()
@keymaps = new KeymapManager({configDirPath, resourcePath, notificationManager: @notifications})
@keymaps = new KeymapManager({@configDirPath, resourcePath, notificationManager: @notifications})
@tooltips = new TooltipManager(keymapManager: @keymaps)
@@ -150,16 +162,16 @@ class AtomEnvironment extends Model
@grammars = new GrammarRegistry({@config})
@styles = new StyleManager({configDirPath})
@styles = new StyleManager({@configDirPath})
@packages = new PackageManager({
devMode, configDirPath, resourcePath, safeMode, @config, styleManager: @styles,
devMode, @configDirPath, resourcePath, safeMode, @config, styleManager: @styles,
commandRegistry: @commands, keymapManager: @keymaps, notificationManager: @notifications,
grammarRegistry: @grammars, deserializerManager: @deserializers, viewRegistry: @views
})
@themes = new ThemeManager({
packageManager: @packages, configDirPath, resourcePath, safeMode, @config,
packageManager: @packages, @configDirPath, resourcePath, safeMode, @config,
styleManager: @styles, notificationManager: @notifications, viewRegistry: @views
})
@@ -183,6 +195,9 @@ class AtomEnvironment extends Model
})
@themes.workspace = @workspace
@textEditors = new TextEditorRegistry
@autoUpdater = new AutoUpdateManager({@applicationDelegate})
@config.load()
@themes.loadBaseStylesheets()
@@ -219,7 +234,8 @@ class AtomEnvironment extends Model
checkPortableHomeWritable()
attachSaveStateListeners: ->
debouncedSaveState = _.debounce((=> @saveState()), @saveStateDebounceInterval)
saveState = => @saveState({isUnloading: false}) unless @unloaded
debouncedSaveState = _.debounce(saveState, @saveStateDebounceInterval)
@document.addEventListener('mousedown', debouncedSaveState, true)
@document.addEventListener('keydown', debouncedSaveState, true)
@disposables.add new Disposable =>
@@ -254,8 +270,6 @@ class AtomEnvironment extends Model
new PaneAxisElement().initialize(model, env)
@views.addViewProvider Pane, (model, env) ->
new PaneElement().initialize(model, env)
@views.addViewProvider TextEditor, (model, env) ->
new TextEditorElement().initialize(model, env)
@views.addViewProvider(Gutter, createGutterView)
registerDefaultOpeners: ->
@@ -327,6 +341,7 @@ class AtomEnvironment extends Model
@commands.clear()
@stylesElement.remove()
@config.unobserveUserConfig()
@autoUpdater.destroy()
@uninstallWindowEventHandler()
@@ -405,6 +420,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
@@ -654,7 +679,7 @@ class AtomEnvironment extends Model
@document.body.appendChild(@views.getView(@workspace))
@backgroundStylesheet?.remove()
@watchProjectPath()
@watchProjectPaths()
@packages.activate()
@keymaps.loadUserKeymap()
@@ -664,9 +689,9 @@ class AtomEnvironment extends Model
@openInitialEmptyEditorIfNecessary()
serialize: ->
serialize: (options) ->
version: @constructor.version
project: @project.serialize()
project: @project.serialize(options)
workspace: @workspace.serialize()
packageStates: @packages.serialize()
grammars: {grammarOverridesByPath: @grammars.grammarOverridesByPath}
@@ -676,9 +701,11 @@ class AtomEnvironment extends Model
unloadEditorWindow: ->
return if not @project
@saveState({isUnloading: true})
@storeWindowBackground()
@packages.deactivatePackages()
@saveBlobStoreSync()
@unloaded = true
openInitialEmptyEditorIfNecessary: ->
return unless @config.get('core.openEmptyEditorOnStart')
@@ -786,7 +813,7 @@ class AtomEnvironment extends Model
@themes.load()
# Notify the browser project of the window's current project path
watchProjectPath: ->
watchProjectPaths: ->
@disposables.add @project.onDidChangePaths =>
@applicationDelegate.setRepresentedDirectoryPaths(@project.getPaths())
@@ -811,12 +838,14 @@ class AtomEnvironment extends Model
@blobStore.save()
saveState: ->
saveState: (options) ->
return Promise.resolve() unless @enablePersistence
new Promise (resolve, reject) =>
window.requestIdleCallback =>
state = @serialize()
return if not @project
state = @serialize(options)
savePromise =
if storageKey = @getStateKey(@project?.getPaths())
@stateStore.save(storageKey, state)
@@ -824,11 +853,15 @@ class AtomEnvironment extends Model
@applicationDelegate.setTemporaryWindowState(state)
savePromise.catch(reject).then(resolve)
loadState: ->
if @enablePersistence
if stateKey = @getStateKey(@getLoadSettings().initialPaths)
@stateStore.load(stateKey)
@stateStore.load(stateKey).then (state) =>
if state
state
else
# TODO: remove this when every user has migrated to the IndexedDb state store.
@getStorageFolder().load(stateKey)
else
@applicationDelegate.getTemporaryWindowState()
else
@@ -857,6 +890,9 @@ class AtomEnvironment extends Model
else
null
getStorageFolder: ->
@storageFolder ?= new StorageFolder(@getConfigDirPath())
getConfigDirPath: ->
@configDirPath ?= process.env.ATOM_HOME
@@ -873,6 +909,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
@@ -880,7 +917,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}")

View File

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

View File

@@ -26,7 +26,10 @@ class BlockDecorationsComponent
for id, blockDecorationState of @oldState.blockDecorations
unless @newState.blockDecorations.hasOwnProperty(id)
@blockDecorationNodesById[id].remove()
blockDecorationNode = @blockDecorationNodesById[id]
blockDecorationNode.previousSibling.remove()
blockDecorationNode.nextSibling.remove()
blockDecorationNode.remove()
delete @blockDecorationNodesById[id]
delete @oldState.blockDecorations[id]
@@ -41,19 +44,27 @@ class BlockDecorationsComponent
for decorationId, blockDecorationNode of @blockDecorationNodesById
style = getComputedStyle(blockDecorationNode)
decoration = @newState.blockDecorations[decorationId].decoration
marginBottom = parseInt(style.marginBottom) ? 0
marginTop = parseInt(style.marginTop) ? 0
@presenter.setBlockDecorationDimensions(
decoration,
blockDecorationNode.offsetWidth,
blockDecorationNode.offsetHeight + marginTop + marginBottom
)
topRuler = blockDecorationNode.previousSibling
bottomRuler = blockDecorationNode.nextSibling
width = blockDecorationNode.offsetWidth
height = bottomRuler.offsetTop - topRuler.offsetTop
@presenter.setBlockDecorationDimensions(decoration, width, height)
createAndAppendBlockDecorationNode: (id) ->
blockDecorationState = @newState.blockDecorations[id]
blockDecorationClass = "atom--block-decoration-#{id}"
topRuler = document.createElement("div")
blockDecorationNode = @views.getView(blockDecorationState.decoration.getProperties().item)
blockDecorationNode.id = "atom--block-decoration-#{id}"
bottomRuler = document.createElement("div")
topRuler.classList.add(blockDecorationClass)
blockDecorationNode.classList.add(blockDecorationClass)
bottomRuler.classList.add(blockDecorationClass)
@container.appendChild(topRuler)
@container.appendChild(blockDecorationNode)
@container.appendChild(bottomRuler)
@blockDecorationNodesById[id] = blockDecorationNode
@updateBlockDecorationNode(id)
@@ -63,9 +74,13 @@ class BlockDecorationsComponent
blockDecorationNode = @blockDecorationNodesById[id]
if newBlockDecorationState.isVisible
blockDecorationNode.previousSibling.classList.remove("atom--invisible-block-decoration")
blockDecorationNode.classList.remove("atom--invisible-block-decoration")
blockDecorationNode.nextSibling.classList.remove("atom--invisible-block-decoration")
else
blockDecorationNode.previousSibling.classList.add("atom--invisible-block-decoration")
blockDecorationNode.classList.add("atom--invisible-block-decoration")
blockDecorationNode.nextSibling.classList.add("atom--invisible-block-decoration")
if oldBlockDecorationState.screenRow isnt newBlockDecorationState.screenRow
blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow

View File

@@ -85,16 +85,16 @@ class AtomApplication
else
@loadState(options) or @openPath(options)
openWithOptions: ({pathsToOpen, executedFrom, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, profileStartup, timeout, clearWindowState, addToLastWindow}) ->
openWithOptions: ({initialPaths, pathsToOpen, executedFrom, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, profileStartup, timeout, clearWindowState, addToLastWindow, env}) ->
if test
@runTests({headless: true, devMode, @resourcePath, executedFrom, pathsToOpen, logFile, timeout})
@runTests({headless: true, devMode, @resourcePath, executedFrom, pathsToOpen, logFile, timeout, env})
else if pathsToOpen.length > 0
@openPaths({pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState, addToLastWindow})
@openPaths({initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState, addToLastWindow, env})
else if urlsToOpen.length > 0
@openUrl({urlToOpen, devMode, safeMode}) for urlToOpen in urlsToOpen
@openUrl({urlToOpen, devMode, safeMode, env}) for urlToOpen in urlsToOpen
else
# Always open a editor window if this is the first instance of Atom.
@openPath({pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState, addToLastWindow})
@openPath({initialPaths, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState, addToLastWindow, env})
# Public: Removes the {AtomWindow} from the global window list.
removeWindow: (window) ->
@@ -134,7 +134,8 @@ class AtomApplication
@deleteSocketFile()
server = net.createServer (connection) =>
connection.on 'data', (data) =>
@openWithOptions(JSON.parse(data))
options = JSON.parse(data)
@openWithOptions(options)
server.listen @socketPath
server.on 'error', (error) -> console.error 'Application server failed', error
@@ -304,6 +305,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 [
@@ -409,8 +419,8 @@ class AtomApplication
# :profileStartup - Boolean to control creating a profile of the startup time.
# :window - {AtomWindow} to open file paths in.
# :addToLastWindow - Boolean of whether this should be opened in last focused window.
openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow} = {}) ->
@openPaths({pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow})
openPath: ({initialPaths, pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow, env} = {}) ->
@openPaths({initialPaths, pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow, env})
# Public: Opens multiple paths, in existing windows if possible.
#
@@ -423,7 +433,7 @@ class AtomApplication
# :windowDimensions - Object with height and width keys.
# :window - {AtomWindow} to open file paths in.
# :addToLastWindow - Boolean of whether this should be opened in last focused window.
openPaths: ({pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window, clearWindowState, addToLastWindow}={}) ->
openPaths: ({initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window, clearWindowState, addToLastWindow, env}={}) ->
devMode = Boolean(devMode)
safeMode = Boolean(safeMode)
clearWindowState = Boolean(clearWindowState)
@@ -460,7 +470,7 @@ class AtomApplication
windowInitializationScript ?= require.resolve('../initialize-application-window')
resourcePath ?= @resourcePath
windowDimensions ?= @getDimensionsForNewWindow()
openedWindow = new AtomWindow({locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState})
openedWindow = new AtomWindow({initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState, env})
if pidToKillWhenClosed?
@pidsToOpenWindows[pidToKillWhenClosed] = openedWindow
@@ -503,7 +513,8 @@ class AtomApplication
if (states = @storageFolder.load('application.json'))?.length > 0
for state in states
@openWithOptions(_.extend(options, {
pathsToOpen: state.initialPaths
initialPaths: state.initialPaths
pathsToOpen: state.initialPaths.filter (directoryPath) -> fs.isDirectorySync(directoryPath)
urlsToOpen: []
devMode: @devMode
safeMode: @safeMode
@@ -522,7 +533,7 @@ class AtomApplication
# :urlToOpen - The atom:// url to open.
# :devMode - Boolean to control the opened window's dev mode.
# :safeMode - Boolean to control the opened window's safe mode.
openUrl: ({urlToOpen, devMode, safeMode}) ->
openUrl: ({urlToOpen, devMode, safeMode, env}) ->
unless @packages?
PackageManager = require '../package-manager'
@packages = new PackageManager
@@ -537,7 +548,7 @@ class AtomApplication
packagePath = @packages.resolvePackagePath(packageName)
windowInitializationScript = path.resolve(packagePath, pack.urlMain)
windowDimensions = @getDimensionsForNewWindow()
new AtomWindow({windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions})
new AtomWindow({windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env})
else
console.log "Package '#{pack.name}' does not have a url main: #{urlToOpen}"
else
@@ -552,7 +563,7 @@ class AtomApplication
# :specPath - The directory to load specs from.
# :safeMode - A Boolean that, if true, won't run specs from ~/.atom/packages
# and ~/.atom/dev/packages, defaults to false.
runTests: ({headless, resourcePath, executedFrom, pathsToOpen, logFile, safeMode, timeout}) ->
runTests: ({headless, resourcePath, executedFrom, pathsToOpen, logFile, safeMode, timeout, env}) ->
if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath)
resourcePath = @resourcePath
@@ -582,7 +593,7 @@ class AtomApplication
devMode = true
isSpec = true
safeMode ?= false
new AtomWindow({windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode})
new AtomWindow({windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode, env})
resolveTestRunnerPath: (testPath) ->
FindParentDir ?= require 'find-parent-dir'

View File

@@ -17,7 +17,7 @@ class AtomWindow
isSpec: null
constructor: (settings={}) ->
{@resourcePath, pathToOpen, locationsToOpen, @isSpec, @headless, @safeMode, @devMode} = settings
{@resourcePath, initialPaths, pathToOpen, locationsToOpen, @isSpec, @headless, @safeMode, @devMode} = settings
locationsToOpen ?= [{pathToOpen}] if pathToOpen
locationsToOpen ?= []
@@ -47,13 +47,7 @@ class AtomWindow
loadSettings.safeMode ?= false
loadSettings.atomHome = process.env.ATOM_HOME
loadSettings.clearWindowState ?= false
# Only send to the first non-spec window created
if @constructor.includeShellLoadTime and not @isSpec
@constructor.includeShellLoadTime = false
loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime
loadSettings.initialPaths =
loadSettings.initialPaths ?=
for {pathToOpen} in locationsToOpen when pathToOpen
if fs.statSyncNoException(pathToOpen).isFile?()
path.dirname(pathToOpen)
@@ -62,6 +56,13 @@ class AtomWindow
loadSettings.initialPaths.sort()
# Only send to the first non-spec window created
if @constructor.includeShellLoadTime and not @isSpec
@constructor.includeShellLoadTime = false
loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime
@browserWindow.loadSettings = loadSettings
@browserWindow.once 'window:loaded', =>
@emit 'window:loaded'
@loaded = true

View File

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

View File

@@ -13,6 +13,7 @@ console.log = require 'nslog'
start = ->
args = parseCommandLine()
args.env = process.env
setupAtomHome(args)
setupCompileCache()
return if handleStartupEventWithSquirrel()

View File

@@ -46,7 +46,7 @@ class BufferedNodeProcess extends BufferedProcess
options ?= {}
options.env ?= Object.create(process.env)
options.env['ATOM_SHELL_INTERNAL_RUN_AS_NODE'] = 1
options.env['ELECTRON_RUN_AS_NODE'] = 1
args = args?.slice() ? []
args.unshift(command)

View File

@@ -111,11 +111,13 @@ class BufferedProcess
stream.on 'data', (data) =>
return if @killed
bufferedLength = buffered.length
buffered += data
lastNewlineIndex = buffered.lastIndexOf('\n')
lastNewlineIndex = data.lastIndexOf('\n')
if lastNewlineIndex isnt -1
onLines(buffered.substring(0, lastNewlineIndex + 1))
buffered = buffered.substring(lastNewlineIndex + 1)
lineLength = lastNewlineIndex + bufferedLength + 1
onLines(buffered.substring(0, lineLength))
buffered = buffered.substring(lineLength)
stream.on 'close', =>
return if @killed

View File

@@ -85,5 +85,5 @@ parseAlpha = (alpha) ->
numberToHexString = (number) ->
hex = number.toString(16)
hex = "0#{hex}" if number < 10
hex = "0#{hex}" if number < 16
hex

View File

@@ -319,6 +319,23 @@ ScopeDescriptor = require './scope-descriptor'
# * line breaks - `line breaks<br/>`
# * ~~strikethrough~~ - `~~strikethrough~~`
#
# #### order
#
# The settings view orders your settings alphabetically. You can override this
# ordering with the order key.
#
# ```coffee
# config:
# zSetting:
# type: 'integer'
# default: 4
# order: 1
# aSetting:
# type: 'integer'
# default: 4
# order: 2
# ```
#
# ## Best practices
#
# * Don't depend on (or write to) configuration keys outside of your keypath.

View File

@@ -16,8 +16,8 @@ class DefaultDirectoryProvider
# * `null` if the given URI is not compatibile with this provider.
directoryForURISync: (uri) ->
normalizedPath = path.normalize(uri)
{protocol} = url.parse(uri)
directoryPath = if protocol?
{host} = url.parse(uri)
directoryPath = if host
uri
else if not fs.isDirectorySync(normalizedPath) and fs.isDirectorySync(path.dirname(normalizedPath))
path.dirname(normalizedPath)
@@ -26,7 +26,7 @@ class DefaultDirectoryProvider
# TODO: Stop normalizing the path in pathwatcher's Directory.
directory = new Directory(directoryPath)
if protocol?
if host
directory.path = directoryPath
if fs.isCaseInsensitive()
directory.lowerCasePath = directoryPath.toLowerCase()

View File

@@ -0,0 +1,94 @@
'use babel'
import {spawnSync} from 'child_process'
import os from 'os'
// Gets a dump of the user's configured shell environment.
//
// Returns the output of the `env` command or `undefined` if there was an error.
function getRawShellEnv () {
let shell = getUserShell()
// The `-ilc` set of options was tested to work with the OS X v10.11
// default-installed versions of bash, zsh, sh, and ksh. It *does not*
// work with csh or tcsh.
let results = spawnSync(shell, ['-ilc', 'env'], {encoding: 'utf8'})
if (results.error || !results.stdout || results.stdout.length <= 0) {
return
}
return results.stdout
}
function getUserShell () {
if (process.env.SHELL) {
return process.env.SHELL
}
return '/bin/bash'
}
// Gets the user's configured shell environment.
//
// Returns a copy of the user's shell enviroment.
function getFromShell () {
let shellEnvText = getRawShellEnv()
if (!shellEnvText) {
return
}
let env = {}
for (let line of shellEnvText.split(os.EOL)) {
if (line.includes('=')) {
let components = line.split('=')
if (components.length === 2) {
env[components[0]] = components[1]
} else {
let k = components.shift()
let v = components.join('=')
env[k] = v
}
}
}
return env
}
function needsPatching (options = { platform: process.platform, env: process.env }) {
if (options.platform === 'darwin' && !options.env.PWD) {
let shell = getUserShell()
if (shell.endsWith('csh') || shell.endsWith('tcsh')) {
return false
}
return true
}
return false
}
function normalize (options = {}) {
if (options && options.env) {
process.env = options.env
}
if (!options.env) {
options.env = process.env
}
if (!options.platform) {
options.platform = process.platform
}
if (needsPatching(options)) {
// Patch the `process.env` on startup to fix the problem first documented
// in #4126. Retain the original in case someone needs it.
let shellEnv = getFromShell()
if (shellEnv && shellEnv.PATH) {
process._originalEnv = process.env
process.env = shellEnv
}
}
}
export default { getFromShell, needsPatching, normalize }

View File

@@ -15,6 +15,12 @@ const submoduleMode = 57344 // TODO: compose this from libgit2 constants
// Just using this for _.isEqual and _.object, we should impl our own here
import _ from 'underscore-plus'
// For the most part, this class behaves the same as `GitRepository`, with a few
// notable differences:
// * Errors are generally propagated out to the caller instead of being
// swallowed within `GitRepositoryAsync`.
// * Methods accepting a path shouldn't be given a null path, unless it is
// specifically allowed as noted in the method's documentation.
export default class GitRepositoryAsync {
static open (path, options = {}) {
// QUESTION: Should this wrap Git.Repository and reject with a nicer message?
@@ -37,6 +43,7 @@ export default class GitRepositoryAsync {
this.emitter = new Emitter()
this.subscriptions = new CompositeDisposable()
this.pathStatusCache = {}
this.path = null
// NB: These needs to happen before the following .openRepository call.
this.openedPath = _path
@@ -136,13 +143,25 @@ export default class GitRepositoryAsync {
// Public: Returns a {Promise} which resolves to the {String} path of the
// repository.
getPath () {
return this.getRepo().then(repo => repo.path().replace(/\/$/, ''))
return this.getRepo().then(repo => {
if (!this.path) {
this.path = repo.path().replace(/\/$/, '')
}
return this.path
})
}
// Public: Returns a {Promise} which resolves to the {String} working
// directory path of the repository.
getWorkingDirectory () {
return this.getRepo().then(repo => repo.workdir())
getWorkingDirectory (_path) {
return this.getRepo(_path).then(repo => {
if (!repo.cachedWorkdir) {
repo.cachedWorkdir = repo.workdir()
}
return repo.cachedWorkdir
})
}
// Public: Returns a {Promise} that resolves to true if at the root, false if
@@ -151,9 +170,8 @@ export default class GitRepositoryAsync {
if (!this.project) return Promise.resolve(false)
if (!this.projectAtRoot) {
this.projectAtRoot = this.getRepo()
.then(repo => this.project.relativize(repo.workdir()))
.then(relativePath => relativePath === '')
this.projectAtRoot = this.getWorkingDirectory()
.then(wd => this.project.relativize(wd) === '')
}
return this.projectAtRoot
@@ -165,8 +183,8 @@ export default class GitRepositoryAsync {
//
// Returns a {Promise} which resolves to the relative {String} path.
relativizeToWorkingDirectory (_path) {
return this.getRepo()
.then(repo => this.relativize(_path, repo.workdir()))
return this.getWorkingDirectory()
.then(wd => this.relativize(_path, wd))
}
// Public: Makes a path relative to the repository's working directory.
@@ -442,9 +460,9 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to a {Boolean} that's true if the `path`
// is ignored.
isPathIgnored (_path) {
return this.getRepo()
.then(repo => {
const relativePath = this.relativize(_path, repo.workdir())
return Promise.all([this.getRepo(), this.getWorkingDirectory()])
.then(([repo, wd]) => {
const relativePath = this.relativize(_path, wd)
return Git.Ignore.pathIsIgnored(repo, relativePath)
})
.then(ignored => Boolean(ignored))
@@ -483,9 +501,9 @@ export default class GitRepositoryAsync {
// status bit for the path.
refreshStatusForPath (_path) {
let relativePath
return this.getRepo()
.then(repo => {
relativePath = this.relativize(_path, repo.workdir())
return Promise.all([this.getRepo(), this.getWorkingDirectory()])
.then(([repo, wd]) => {
relativePath = this.relativize(_path, wd)
return this._getStatus([relativePath])
})
.then(statuses => {
@@ -591,12 +609,20 @@ export default class GitRepositoryAsync {
// * `added` The {Number} of added lines.
// * `deleted` The {Number} of deleted lines.
getDiffStats (_path) {
return this.getRepo()
return this.getRepo(_path)
.then(repo => Promise.all([repo, repo.getHeadCommit()]))
.then(([repo, headCommit]) => Promise.all([repo, headCommit.getTree()]))
.then(([repo, tree]) => {
.then(([repo, headCommit]) => Promise.all([repo, headCommit.getTree(), this.getWorkingDirectory(_path)]))
.then(([repo, tree, wd]) => {
const options = new Git.DiffOptions()
options.pathspec = this.relativize(_path, repo.workdir())
options.contextLines = 0
options.flags = Git.Diff.OPTION.DISABLE_PATHSPEC_MATCH
options.pathspec = this.relativize(_path, wd)
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))
@@ -627,9 +653,9 @@ export default class GitRepositoryAsync {
// * `newLines` The {Number} of lines in the new hunk
getLineDiffs (_path, text) {
let relativePath = null
return this.getRepo()
.then(repo => {
relativePath = this.relativize(_path, repo.workdir())
return Promise.all([this.getRepo(_path), this.getWorkingDirectory(_path)])
.then(([repo, wd]) => {
relativePath = this.relativize(_path, wd)
return repo.getHeadCommit()
})
.then(commit => commit.getEntry(relativePath))
@@ -665,10 +691,10 @@ export default class GitRepositoryAsync {
// Returns a {Promise} that resolves or rejects depending on whether the
// method was successful.
checkoutHead (_path) {
return this.getRepo()
.then(repo => {
return Promise.all([this.getRepo(_path), this.getWorkingDirectory(_path)])
.then(([repo, wd]) => {
const checkoutOptions = new Git.CheckoutOptions()
checkoutOptions.paths = [this.relativize(_path, repo.workdir())]
checkoutOptions.paths = [this.relativize(_path, wd)]
checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH
return Git.Checkout.head(repo, checkoutOptions)
})
@@ -775,12 +801,17 @@ export default class GitRepositoryAsync {
// Get the current branch and update this.branch.
//
// Returns a {Promise} which resolves to the {String} branch name.
// Returns a {Promise} which resolves to a {boolean} indicating whether the
// branch name changed.
_refreshBranch () {
return this.getRepo()
.then(repo => repo.getCurrentBranch())
.then(ref => ref.name())
.then(branchName => this.branch = branchName)
.then(branchName => {
const changed = branchName !== this.branch
this.branch = branchName
return changed
})
}
// Refresh the cached ahead/behind count with the given branch.
@@ -788,10 +819,15 @@ export default class GitRepositoryAsync {
// * `branchName` The {String} name of the branch whose ahead/behind should be
// used for the refresh.
//
// Returns a {Promise} which will resolve to {null}.
// Returns a {Promise} which will resolve to a {boolean} indicating whether
// the ahead/behind count changed.
_refreshAheadBehindCount (branchName) {
return this.getAheadBehindCount(branchName)
.then(counts => this.upstream = counts)
.then(counts => {
const changed = !_.isEqual(counts, this.upstream)
this.upstream = counts
return changed
})
}
// Get the status for this repository.
@@ -850,13 +886,14 @@ export default class GitRepositoryAsync {
// submodule names with {GitRepositoryAsync} values.
async _refreshSubmodules () {
const repo = await this.getRepo()
const wd = await this.getWorkingDirectory()
const submoduleNames = await repo.getSubmoduleNames()
for (const name of submoduleNames) {
const alreadyExists = Boolean(this.submodules[name])
if (alreadyExists) continue
const submodule = await Git.Submodule.lookup(repo, name)
const absolutePath = path.join(repo.workdir(), submodule.path())
const absolutePath = path.join(wd, submodule.path())
const submoduleRepo = GitRepositoryAsync.open(absolutePath, {openExactPath: true, refreshOnWindowFocus: false})
this.submodules[name] = submoduleRepo
}
@@ -897,15 +934,15 @@ export default class GitRepositoryAsync {
// Refresh the cached status.
//
// Returns a {Promise} which will resolve to {null}.
// Returns a {Promise} which will resolve to a {boolean} indicating whether
// any statuses changed.
_refreshStatus () {
return Promise.all([this._getRepositoryStatus(), this._getSubmoduleStatuses()])
.then(([repositoryStatus, submoduleStatus]) => {
const statusesByPath = _.extend({}, repositoryStatus, submoduleStatus)
if (!_.isEqual(this.pathStatusCache, statusesByPath) && this.emitter != null) {
this.emitter.emit('did-change-statuses')
}
const changed = !_.isEqual(this.pathStatusCache, statusesByPath)
this.pathStatusCache = statusesByPath
return changed
})
}
@@ -915,11 +952,17 @@ export default class GitRepositoryAsync {
refreshStatus () {
const status = this._refreshStatus()
const branch = this._refreshBranch()
const aheadBehind = branch.then(branchName => this._refreshAheadBehindCount(branchName))
const aheadBehind = branch.then(() => this._refreshAheadBehindCount(this.branch))
this._refreshingPromise = this._refreshingPromise.then(_ => {
return Promise.all([status, branch, aheadBehind])
.then(_ => null)
.then(([statusChanged, branchChanged, aheadBehindChanged]) => {
if (this.emitter && (statusChanged || branchChanged || aheadBehindChanged)) {
this.emitter.emit('did-change-statuses')
}
return null
})
// Because all these refresh steps happen asynchronously, it's entirely
// possible the repository was destroyed while we were working. In which
// case we should just swallow the error.

View File

@@ -83,11 +83,12 @@ class GitRepository
asyncOptions.subscribeToBuffers = false
@async = GitRepositoryAsync.open(path, asyncOptions)
@statuses = {}
@upstream = {ahead: 0, behind: 0}
for submodulePath, submoduleRepo of @repo.submodules
submoduleRepo.upstream = {ahead: 0, behind: 0}
@statusesByPath = {}
{@project, @config, refreshOnWindowFocus} = options
refreshOnWindowFocus ?= true
@@ -165,7 +166,7 @@ class GitRepository
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeStatuses: (callback) ->
@emitter.on 'did-change-statuses', callback
@async.onDidChangeStatuses callback
###
Section: Repository Details
@@ -317,7 +318,7 @@ class GitRepository
getDirectoryStatus: (directoryPath) ->
directoryPath = "#{@relativize(directoryPath)}/"
directoryStatus = 0
for path, status of @statuses
for path, status of _.extend({}, @async.getCachedPathStatuses(), @statusesByPath)
directoryStatus |= status if path.indexOf(directoryPath) is 0
directoryStatus
@@ -328,18 +329,26 @@ class GitRepository
# Returns a {Number} representing the status. This value can be passed to
# {::isStatusModified} or {::isStatusNew} to get more information.
getPathStatus: (path) ->
repo = @getRepo(path)
relativePath = @relativize(path)
# This is a bit particular. If a package calls `getPathStatus` like this:
# - change the file
# - getPathStatus
# - change the file
# - getPathStatus
# We need to preserve the guarantee that each call to `getPathStatus` will
# synchronously emit 'did-change-status'. So we need to keep a cache of the
# statuses found from this call.
currentPathStatus = @getCachedRelativePathStatus(relativePath) ? 0
# Trigger events emitted on the async repo as well
@async.refreshStatusForPath(path)
repo = @getRepo(path)
relativePath = @relativize(path)
currentPathStatus = @statuses[relativePath] ? 0
pathStatus = repo.getStatus(repo.relativize(path)) ? 0
pathStatus = 0 if repo.isStatusIgnored(pathStatus)
if pathStatus > 0
@statuses[relativePath] = pathStatus
else
delete @statuses[relativePath]
@statusesByPath[relativePath] = pathStatus
if currentPathStatus isnt pathStatus
@emitter.emit 'did-change-status', {path, pathStatus}
@@ -351,7 +360,11 @@ class GitRepository
#
# Returns a status {Number} or null if the path is not in the cache.
getCachedPathStatus: (path) ->
@statuses[@relativize(path)]
relativePath = @relativize(path)
@getCachedRelativePathStatus(relativePath)
getCachedRelativePathStatus: (relativePath) ->
@statusesByPath[relativePath] ? @async.getCachedPathStatuses()[relativePath]
# Public: Returns true if the given status indicates modification.
#
@@ -478,24 +491,16 @@ class GitRepository
#
# Returns a promise that resolves when the repository has been refreshed.
refreshStatus: ->
asyncRefresh = @async.refreshStatus()
asyncRefresh = @async.refreshStatus().then =>
@statusesByPath = {}
@branch = @async?.branch
syncRefresh = new Promise (resolve, reject) =>
@handlerPath ?= require.resolve('./repository-status-handler')
relativeProjectPaths = @project?.getPaths()
.map (path) => @relativize(path)
.map (path) -> if path.length > 0 then path + '/**' else '*'
@statusTask?.terminate()
@statusTask = Task.once @handlerPath, @getPath(), relativeProjectPaths, ({statuses, upstream, branch, submodules}) =>
statusesUnchanged = _.isEqual(statuses, @statuses) and
_.isEqual(upstream, @upstream) and
_.isEqual(branch, @branch) and
_.isEqual(submodules, @submodules)
@statuses = statuses
@statusTask = Task.once @handlerPath, @getPath(), ({upstream, submodules}) =>
@upstream = upstream
@branch = branch
@submodules = submodules
for submodulePath, submoduleRepo of @getRepo().submodules
@@ -503,7 +508,4 @@ class GitRepository
resolve()
unless statusesUnchanged
@emitter.emit 'did-change-statuses'
return Promise.all([asyncRefresh, syncRefresh])

View File

@@ -1,10 +1,14 @@
# Like sands through the hourglass, so are the days of our lives.
module.exports = ({blobStore}) ->
environmentHelpers = require('./environment-helpers')
path = require 'path'
require './window'
{getWindowLoadSettings} = require './window-load-settings-helpers'
{resourcePath, isSpec, devMode} = getWindowLoadSettings()
{resourcePath, isSpec, devMode, env} = getWindowLoadSettings()
# Set baseline environment
environmentHelpers.normalize({env: env})
# Add application-specific exports to module search path.
exportsPath = path.join(resourcePath, 'exports')
@@ -21,6 +25,7 @@ module.exports = ({blobStore}) ->
applicationDelegate: new ApplicationDelegate,
configDirPath: process.env.ATOM_HOME
enablePersistence: true
env: env
})
atom.startEditorWindow().then ->

View File

@@ -147,13 +147,11 @@ class LanguageMode
if bufferRow > 0
for currentRow in [bufferRow-1..0] by -1
break if @buffer.isRowBlank(currentRow)
break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
startRow = currentRow
if bufferRow < @buffer.getLastRow()
for currentRow in [bufferRow+1..@buffer.getLastRow()] by 1
break if @buffer.isRowBlank(currentRow)
break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
endRow = currentRow

View File

@@ -149,7 +149,7 @@ class LinesTileComponent
if newLineState.screenRow isnt oldLineState.screenRow
insertionPoint.dataset.screenRow = newLineState.screenRow
precedingBlockDecorationsSelector = newLineState.precedingBlockDecorations.map((d) -> "#atom--block-decoration-#{d.id}").join(',')
precedingBlockDecorationsSelector = newLineState.precedingBlockDecorations.map((d) -> ".atom--block-decoration-#{d.id}").join(',')
if precedingBlockDecorationsSelector isnt oldLineState.precedingBlockDecorationsSelector
insertionPoint.setAttribute("select", precedingBlockDecorationsSelector)
@@ -180,7 +180,7 @@ class LinesTileComponent
if newLineState.screenRow isnt oldLineState.screenRow
insertionPoint.dataset.screenRow = newLineState.screenRow
followingBlockDecorationsSelector = newLineState.followingBlockDecorations.map((d) -> "#atom--block-decoration-#{d.id}").join(',')
followingBlockDecorationsSelector = newLineState.followingBlockDecorations.map((d) -> ".atom--block-decoration-#{d.id}").join(',')
if followingBlockDecorationsSelector isnt oldLineState.followingBlockDecorationsSelector
insertionPoint.setAttribute("select", followingBlockDecorationsSelector)

View File

@@ -3,6 +3,9 @@ Notification = require '../src/notification'
# Public: A notification manager used to create {Notification}s to be shown
# to the user.
#
# An instance of this class is always available as the `atom.notifications`
# global.
module.exports =
class NotificationManager
constructor: ->

View File

@@ -128,8 +128,12 @@ class PackageManager
# Public: Get the path to the apm command.
#
# Uses the value of the `core.apmPath` config setting if it exists.
#
# Return a {String} file path to apm.
getApmPath: ->
configPath = atom.config.get('core.apmPath')
return configPath if configPath
return @apmPath if @apmPath?
commandName = 'apm'
@@ -541,11 +545,12 @@ class PackageManager
unless typeof metadata.name is 'string' and metadata.name.length > 0
metadata.name = packageName
if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string'
metadata.repository.url = metadata.repository.url.replace(/(^git\+)|(\.git$)/g, '')
metadata
normalizePackageMetadata: (metadata) ->
unless metadata?._id
normalizePackageData ?= require 'normalize-package-data'
normalizePackageData(metadata)
if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string'
metadata.repository.url = metadata.repository.url.replace(/^git\+/, '')

View File

@@ -2,20 +2,18 @@
PaneResizeHandleElement = require './pane-resize-handle-element'
class PaneAxisElement extends HTMLElement
createdCallback: ->
@subscriptions = new CompositeDisposable
attachedCallback: ->
@subscriptions ?= @subscribeToModel()
@childAdded({child, index}) for child, index in @model.getChildren()
detachedCallback: ->
@subscriptions.dispose()
@subscriptions = null
@childRemoved({child}) for child in @model.getChildren()
initialize: (@model, {@views}) ->
throw new Error("Must pass a views parameter when initializing TextEditorElements") unless @views?
@subscriptions.add @model.onDidAddChild(@childAdded.bind(this))
@subscriptions.add @model.onDidRemoveChild(@childRemoved.bind(this))
@subscriptions.add @model.onDidReplaceChild(@childReplaced.bind(this))
@subscriptions.add @model.observeFlexScale(@flexScaleChanged.bind(this))
@subscriptions ?= @subscribeToModel()
@childAdded({child, index}) for child, index in @model.getChildren()
switch @model.getOrientation()
@@ -25,6 +23,14 @@ class PaneAxisElement extends HTMLElement
@classList.add('vertical', 'pane-column')
this
subscribeToModel: ->
subscriptions = new CompositeDisposable
subscriptions.add @model.onDidAddChild(@childAdded.bind(this))
subscriptions.add @model.onDidRemoveChild(@childRemoved.bind(this))
subscriptions.add @model.onDidReplaceChild(@childReplaced.bind(this))
subscriptions.add @model.observeFlexScale(@flexScaleChanged.bind(this))
subscriptions
isPaneResizeHandleElement: (element) ->
element?.nodeName.toLowerCase() is 'atom-pane-resize-handle'

View File

@@ -1,3 +1,4 @@
Grim = require 'grim'
{find, compact, extend, last} = require 'underscore-plus'
{CompositeDisposable, Emitter} = require 'event-kit'
Model = require './model'
@@ -307,6 +308,7 @@ class Pane extends Model
# Add item (or move item) to the end of the itemStack
addItemToStack: (newItem) ->
return unless newItem?
index = @itemStack.indexOf(newItem)
@itemStack.splice(index, 1) unless index is -1
@itemStack.push(newItem)
@@ -393,39 +395,47 @@ class Pane extends Model
# Public: Make the given item *active*, causing it to be displayed by
# the pane's view.
#
# * `pending` (optional) {Boolean} indicating that the item should be added
# in a pending state if it does not yet exist in the pane. Existing pending
# items in a pane are replaced with new pending items when they are opened.
activateItem: (item, pending=false) ->
# * `options` (optional) {Object}
# * `pending` (optional) {Boolean} indicating that the item should be added
# in a pending state if it does not yet exist in the pane. Existing pending
# items in a pane are replaced with new pending items when they are opened.
activateItem: (item, options={}) ->
if item?
if @getPendingItem() is @activeItem
index = @getActiveItemIndex()
else
index = @getActiveItemIndex() + 1
@addItem(item, index, false, pending)
@addItem(item, extend({}, options, {index: index}))
@setActiveItem(item)
# Public: Add the given item to the pane.
#
# * `item` The item to add. It can be a model with an associated view or a
# view.
# * `index` (optional) {Number} indicating the index at which to add the item.
# If omitted, the item is added after the current active item.
# * `pending` (optional) {Boolean} indicating that the item should be
# added in a pending state. Existing pending items in a pane are replaced with
# new pending items when they are opened.
# * `options` (optional) {Object}
# * `index` (optional) {Number} indicating the index at which to add the item.
# If omitted, the item is added after the current active item.
# * `pending` (optional) {Boolean} indicating that the item should be
# added in a pending state. Existing pending items in a pane are replaced with
# new pending items when they are opened.
#
# Returns the added item.
addItem: (item, index=@getActiveItemIndex() + 1, moved=false, pending=false) ->
addItem: (item, options={}) ->
# Backward compat with old API:
# addItem(item, index=@getActiveItemIndex() + 1)
if typeof options is "number"
Grim.deprecate("Pane::addItem(item, #{options}) is deprecated in favor of Pane::addItem(item, {index: #{options}})")
options = index: options
index = options.index ? @getActiveItemIndex() + 1
moved = options.moved ? false
pending = options.pending ? false
throw new Error("Pane items must be objects. Attempted to add item #{item}.") unless item? and typeof item is 'object'
throw new Error("Adding a pane item with URI '#{item.getURI?()}' that has already been destroyed") if item.isDestroyed?()
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)
@@ -436,13 +446,19 @@ class Pane extends Model
@subscriptionsPerItem.set item, itemSubscriptions
@items.splice(index, 0, item)
lastPendingItem = @getPendingItem()
@setPendingItem(item) if pending
@emitter.emit 'did-add-item', {item, index, moved}
@destroyItem(lastPendingItem) if lastPendingItem? and not moved
@setActiveItem(item) unless @getActiveItem()?
item
setPendingItem: (item) =>
@pendingItem = item if @pendingItem isnt item
@emitter.emit 'did-terminate-pending-state' if not item
if @pendingItem isnt item
mostRecentPendingItem = @pendingItem
@pendingItem = item
@emitter.emit 'item-did-terminate-pending-state', mostRecentPendingItem
getPendingItem: =>
@pendingItem or null
@@ -450,8 +466,8 @@ class Pane extends Model
clearPendingItem: =>
@setPendingItem(null)
onDidTerminatePendingState: (callback) =>
@emitter.on 'did-terminate-pending-state', callback
onItemDidTerminatePendingState: (callback) =>
@emitter.on 'item-did-terminate-pending-state', callback
# Public: Add the given items to the pane.
#
@@ -464,7 +480,7 @@ class Pane extends Model
# Returns an {Array} of added items.
addItems: (items, index=@getActiveItemIndex() + 1) ->
items = items.filter (item) => not (item in @items)
@addItem(item, index + i, false) for item, i in items
@addItem(item, {index: index + i}) for item, i in items
items
removeItem: (item, moved) ->
@@ -513,7 +529,7 @@ class Pane extends Model
# given pane.
moveItemToPane: (item, pane, index) ->
@removeItem(item, true)
pane.addItem(item, index, true)
pane.addItem(item, {index: index, moved: true})
# Public: Destroy the active item and activate the next item.
destroyActiveItem: ->

View File

@@ -54,8 +54,9 @@ class Project extends Model
Section: Serialization
###
deserialize: (state, deserializerManager) ->
deserialize: (state) ->
state.paths = [state.path] if state.path? # backward compatibility
state.paths = state.paths.filter (directoryPath) -> fs.isDirectorySync(directoryPath)
@buffers = _.compact state.buffers.map (bufferState) ->
# Check that buffer's file path is accessible
@@ -65,15 +66,15 @@ class Project extends Model
fs.closeSync(fs.openSync(bufferState.filePath, 'r'))
catch error
return unless error.code is 'ENOENT'
deserializerManager.deserialize(bufferState)
TextBuffer.deserialize(bufferState)
@subscribeToBuffer(buffer) for buffer in @buffers
@setPaths(state.paths)
serialize: ->
serialize: (options={}) ->
deserializer: 'Project'
paths: @getPaths()
buffers: _.compact(@buffers.map (buffer) -> buffer.serialize() if buffer.isRetained())
buffers: _.compact(@buffers.map (buffer) -> buffer.serialize({markerLayers: options.isUnloading is true}) if buffer.isRetained())
###
Section: Event Subscription

View File

@@ -5,32 +5,15 @@ module.exports = (repoPath, paths = []) ->
repo = Git.open(repoPath)
upstream = {}
statuses = {}
submodules = {}
branch = null
if repo?
# Statuses in main repo
workingDirectoryPath = repo.getWorkingDirectory()
repoStatus = (if paths.length > 0 then repo.getStatusForPaths(paths) else repo.getStatus())
for filePath, status of repoStatus
statuses[filePath] = status
# Statuses in submodules
for submodulePath, submoduleRepo of repo.submodules
submodules[submodulePath] =
branch: submoduleRepo.getHead()
upstream: submoduleRepo.getAheadBehindCount()
workingDirectoryPath = submoduleRepo.getWorkingDirectory()
for filePath, status of submoduleRepo.getStatus()
absolutePath = path.join(workingDirectoryPath, filePath)
# Make path relative to parent repository
relativePath = repo.relativize(absolutePath)
statuses[relativePath] = status
upstream = repo.getAheadBehindCount()
branch = repo.getHead()
repo.release()
{statuses, upstream, branch, submodules}
{upstream, submodules}

View File

@@ -810,11 +810,11 @@ class Selection extends Model
@wordwise = false
@linewise = false
autoscroll: ->
autoscroll: (options) ->
if @marker.hasTail()
@editor.scrollToScreenRange(@getScreenRange(), reversed: @isReversed())
@editor.scrollToScreenRange(@getScreenRange(), Object.assign({reversed: @isReversed()}, options))
else
@cursor.autoscroll()
@cursor.autoscroll(options)
clearAutoscroll: ->

View File

@@ -24,16 +24,13 @@ class StateStore {
}
save (key, value) {
// Serialize values using JSON.stringify, as it seems way faster than IndexedDB structured clone.
// (Ref.: https://bugs.chromium.org/p/chromium/issues/detail?id=536620)
let jsonValue = JSON.stringify(value)
return new Promise((resolve, reject) => {
this.dbPromise.then(db => {
if (db == null) resolve()
var request = db.transaction(['states'], 'readwrite')
.objectStore('states')
.put({value: jsonValue, storedAt: new Date().toString(), isJSON: true}, key)
.put({value: value, storedAt: new Date().toString()}, key)
request.onsuccess = resolve
request.onerror = reject
@@ -52,9 +49,8 @@ class StateStore {
request.onsuccess = (event) => {
let result = event.target.result
if (result) {
// TODO: remove this when state will be serialized only via JSON.
resolve(result.isJSON ? JSON.parse(result.value) : result.value)
if (result && !result.isJSON) {
resolve(result.value)
} else {
resolve(null)
}

View File

@@ -6,6 +6,14 @@ class StorageFolder
constructor: (containingPath) ->
@path = path.join(containingPath, "storage") if containingPath?
clear: ->
return unless @path?
try
fs.removeSync(@path)
catch error
console.warn "Error deleting #{@path}", error.stack, error
storeSync: (name, object) ->
return unless @path?

View File

@@ -43,7 +43,7 @@ class TextEditorComponent
@assert domNode?, "TextEditorComponent::domNode was set to null."
@domNodeValue = domNode
constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, tileSize, @views, @themes, @config, @workspace, @assert, @grammars}) ->
constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, tileSize, @views, @themes, @config, @workspace, @assert, @grammars, scrollPastEnd}) ->
@tileSize = tileSize if tileSize?
@disposables = new CompositeDisposable
@@ -61,6 +61,7 @@ class TextEditorComponent
stoppedScrollingDelay: 200
config: @config
lineTopIndex: lineTopIndex
scrollPastEnd: scrollPastEnd
@presenter.onDidUpdateState(@requestUpdate)

View File

@@ -17,6 +17,8 @@ class TextEditorElement extends HTMLElement
focusOnAttach: false
hasTiledRendering: true
logicalDisplayBuffer: true
scrollPastEnd: true
autoHeight: true
createdCallback: ->
# Use globals when the following instance variables aren't set.
@@ -38,6 +40,9 @@ class TextEditorElement extends HTMLElement
@setAttribute('tabindex', -1)
initializeContent: (attributes) ->
unless @autoHeight
@style.height = "100%"
if @config.get('editor.useShadowDOM')
@useShadowDOM = true
@@ -86,8 +91,8 @@ class TextEditorElement extends HTMLElement
@subscriptions.add @component.onDidChangeScrollLeft =>
@emitter.emit("did-change-scroll-left", arguments...)
initialize: (model, {@views, @config, @themes, @workspace, @assert, @styles, @grammars}) ->
throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @views?
initialize: (model, {@views, @config, @themes, @workspace, @assert, @styles, @grammars}, @autoHeight = true, @scrollPastEnd = true) ->
throw new Error("Must pass a views parameter when initializing TextEditorElements") unless @views?
throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @config?
throw new Error("Must pass a themes parameter when initializing TextEditorElements") unless @themes?
throw new Error("Must pass a workspace parameter when initializing TextEditorElements") unless @workspace?
@@ -143,6 +148,7 @@ class TextEditorElement extends HTMLElement
workspace: @workspace
assert: @assert
grammars: @grammars
scrollPastEnd: @scrollPastEnd
)
@rootElement.appendChild(@component.getDomNode())

View File

@@ -13,7 +13,7 @@ class TextEditorPresenter
minimumReflowInterval: 200
constructor: (params) ->
{@model, @config, @lineTopIndex} = params
{@model, @config, @lineTopIndex, scrollPastEnd} = params
{@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @tileSize} = params
{@contentFrameWidth} = params
@@ -42,6 +42,8 @@ class TextEditorPresenter
@startReflowing() if @continuousReflow
@updating = false
@scrollPastEndOverride = scrollPastEnd ? true
setLinesYardstick: (@linesYardstick) ->
getLinesYardstick: -> @linesYardstick
@@ -661,7 +663,7 @@ class TextEditorPresenter
return unless @contentHeight? and @clientHeight?
contentHeight = @contentHeight
if @scrollPastEnd
if @scrollPastEnd and @scrollPastEndOverride
extraScrollHeight = @clientHeight - (@lineHeight * 3)
contentHeight += extraScrollHeight if extraScrollHeight > 0
scrollHeight = Math.max(contentHeight, @height)

View File

@@ -0,0 +1,40 @@
{Emitter, Disposable} = require 'event-kit'
# Experimental: This global registry tracks registered `TextEditors`.
#
# If you want to add functionality to a wider set of text editors than just
# those appearing within workspace panes, use `atom.textEditors.observe` to
# invoke a callback for all current and future registered text editors.
#
# If you want packages to be able to add functionality to your non-pane text
# editors (such as a search field in a custom user interface element), register
# them for observation via `atom.textEditors.add`. **Important:** When you're
# done using your editor, be sure to call `dispose` on the returned disposable
# to avoid leaking editors.
module.exports =
class TextEditorRegistry
constructor: ->
@editors = new Set
@emitter = new Emitter
# Register a `TextEditor`.
#
# * `editor` The editor to register.
#
# Returns a {Disposable} on which `.dispose()` can be called to remove the
# added editor. To avoid any memory leaks this should be called when the
# editor is destroyed.
add: (editor) ->
@editors.add(editor)
@emitter.emit 'did-add-editor', editor
new Disposable => @editors.delete(editor)
# Invoke the given callback with all the current and future registered
# `TextEditors`.
#
# * `callback` {Function} to be called with current and future text editors.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
observe: (callback) ->
@editors.forEach(callback)
@emitter.on 'did-add-editor', callback

View File

@@ -11,6 +11,7 @@ Selection = require './selection'
TextMateScopeSelector = require('first-mate').ScopeSelector
{Directory} = require "pathwatcher"
GutterContainer = require './gutter-container'
TextEditorElement = require './text-editor-element'
# Essential: This class represents all essential editing state for a single
# {TextBuffer}, including cursor and selection positions, folds, and soft wraps.
@@ -61,6 +62,10 @@ class TextEditor extends Model
suppressSelectionMerging: false
selectionFlashDuration: 500
gutterContainer: null
editorElement: null
Object.defineProperty @prototype, "element",
get: -> @getElement()
@deserialize: (state, atomEnvironment) ->
try
@@ -82,7 +87,11 @@ class TextEditor extends Model
state.project = atomEnvironment.project
state.assert = atomEnvironment.assert.bind(atomEnvironment)
state.applicationDelegate = atomEnvironment.applicationDelegate
new this(state)
editor = new this(state)
if state.registered
disposable = atomEnvironment.textEditors.add(editor)
editor.onDidDestroy -> disposable.dispose()
editor
constructor: (params={}) ->
super
@@ -92,7 +101,7 @@ class TextEditor extends Model
softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation,
@mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config,
@notificationManager, @packageManager, @clipboard, @viewRegistry, @grammarRegistry,
@project, @assert, @applicationDelegate
@project, @assert, @applicationDelegate, grammar, showInvisibles, @autoHeight, @scrollPastEnd
} = params
throw new Error("Must pass a config parameter when constructing TextEditors") unless @config?
@@ -111,11 +120,15 @@ class TextEditor extends Model
@cursors = []
@cursorsByMarkerId = new Map
@selections = []
@autoHeight ?= true
@scrollPastEnd ?= true
@hasTerminatedPendingState = false
showInvisibles ?= true
buffer ?= new TextBuffer
@displayBuffer ?= new DisplayBuffer({
buffer, tabLength, softWrapped, ignoreInvisibles: @mini, largeFileMode,
buffer, tabLength, softWrapped, ignoreInvisibles: @mini or not showInvisibles, largeFileMode,
@config, @assert, @grammarRegistry, @packageManager
})
@buffer = @displayBuffer.buffer
@@ -144,6 +157,9 @@ class TextEditor extends Model
priority: 0
visible: lineNumberGutterVisible
if grammar?
@setGrammar(grammar)
serialize: ->
deserializer: 'TextEditor'
id: @id
@@ -152,6 +168,7 @@ class TextEditor extends Model
firstVisibleScreenColumn: @getFirstVisibleScreenColumn()
displayBuffer: @displayBuffer.serialize()
selectionsMarkerLayerId: @selectionsMarkerLayer.id
registered: atom.textEditors.editors.has this
subscribeToBuffer: ->
@buffer.retain()
@@ -2466,6 +2483,7 @@ class TextEditor extends Model
selections = @getSelections()
if selections.length > 1
selection.destroy() for selection in selections[1...(selections.length)]
selections[0].autoscroll(center: true)
true
else
false
@@ -2920,6 +2938,7 @@ class TextEditor extends Model
# Extended: Unfold all existing folds.
unfoldAll: ->
@languageMode.unfoldAll()
@scrollToCursorPosition()
# Extended: Fold all foldable lines at the given indent level.
#
@@ -3138,6 +3157,10 @@ class TextEditor extends Model
Section: TextEditor Rendering
###
# Get the Element for the editor.
getElement: ->
@editorElement ?= new TextEditorElement().initialize(this, atom, @autoHeight, @scrollPastEnd)
# Essential: Retrieves the greyed out placeholder of a mini editor.
#
# Returns a {String}.
@@ -3212,7 +3235,7 @@ class TextEditor extends Model
setFirstVisibleScreenRow: (screenRow, fromView) ->
unless fromView
maxScreenRow = @getScreenLineCount() - 1
unless @config.get('editor.scrollPastEnd')
unless @config.get('editor.scrollPastEnd') and @scrollPastEnd
height = @displayBuffer.getHeight()
lineHeightInPixels = @displayBuffer.getLineHeightInPixels()
if height? and lineHeightInPixels?

View File

@@ -498,7 +498,6 @@ class TokenizedLine
while iterator.next()
scopes = iterator.getScopes()
continue if scopes.length is 1
continue unless NonWhitespaceRegex.test(iterator.getText())
for scope in scopes
if CommentScopeRegex.test(scope)
@isCommentLine = true

View File

@@ -63,6 +63,8 @@ class TooltipManager
# full list of options. You can also supply the following additional options:
# * `title` A {String} or {Function} to use for the text in the tip. If
# given a function, `this` will be set to the `target` element.
# * `trigger` A {String} that's the same as Bootstrap 'click | hover | focus
# | manual', except 'manual' will show the tooltip immediately.
# * `keyBindingCommand` A {String} containing a command name. If you specify
# this option and a key binding exists that matches the command, it will
# be appended to the title or rendered alone if no title is specified.

View File

@@ -64,7 +64,9 @@ Tooltip.prototype.init = function (element, options) {
if (trigger === 'click') {
this.disposables.add(listen(this.element, 'click', this.options.selector, this.toggle.bind(this)))
} else if (trigger !== 'manual') {
} else if (trigger === 'manual') {
this.show()
} else {
var eventIn, eventOut
if (trigger === 'hover') {

View File

@@ -171,6 +171,11 @@ class ViewRegistry
if object instanceof HTMLElement
return object
if typeof object?.getElement is 'function'
element = object.getElement()
if element instanceof HTMLElement
return element
if object?.element instanceof HTMLElement
return object.element

View File

@@ -143,7 +143,6 @@ class WindowEventHandler
@reloadRequested = false
@atomEnvironment.storeWindowDimensions()
@atomEnvironment.saveState()
if confirmed
@atomEnvironment.unloadEditorWindow()
else

View File

@@ -43,6 +43,12 @@ class Workspace extends Model
@defaultDirectorySearcher = new DefaultDirectorySearcher()
@consumeServices(@packageManager)
# One cannot simply .bind here since it could be used as a component with
# Etch, in which case it'd be `new`d. And when it's `new`d, `this` is always
# the newly created object.
realThis = this
@buildTextEditor = -> Workspace.prototype.buildTextEditor.apply(realThis, arguments)
@panelContainers =
top: new PanelContainer({location: 'top'})
left: new PanelContainer({location: 'left'})
@@ -503,7 +509,7 @@ class Workspace extends Model
return item if pane.isDestroyed()
@itemOpened(item)
pane.activateItem(item, options.pending) if activateItem
pane.activateItem(item, {pending: options.pending}) if activateItem
pane.activate() if activatePane
initialLine = initialColumn = 0
@@ -542,7 +548,10 @@ class Workspace extends Model
throw error
@project.bufferForPath(filePath, options).then (buffer) =>
@buildTextEditor(_.extend({buffer, largeFileMode}, options))
editor = @buildTextEditor(_.extend({buffer, largeFileMode}, options))
disposable = atom.textEditors.add(editor)
editor.onDidDestroy -> disposable.dispose()
editor
# Public: Returns a {Boolean} that is `true` if `object` is a `TextEditor`.
#