diff --git a/.gitignore b/.gitignore index 6eec21c2a..bce6c56d3 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ debug.log docs/output docs/includes spec/fixtures/evil-files/ +out/ diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 73ff2f50d..8144a4b62 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -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? diff --git a/apm/package.json b/apm/package.json index 2e6b0b8ea..6623876f9 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "1.6.0" + "atom-package-manager": "1.9.2" } } diff --git a/atom.sh b/atom.sh index ef8dbcdc4..b68716bf4 100755 --- a/atom.sh +++ b/atom.sh @@ -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 diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index a34e1fbd4..d9375d05c 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -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? diff --git a/build/tasks/codesign-task.coffee b/build/tasks/codesign-task.coffee index 6c99795c0..2a061742b 100644 --- a/build/tasks/codesign-task.coffee +++ b/build/tasks/codesign-task.coffee @@ -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) -> diff --git a/build/tasks/publish-build-task.coffee b/build/tasks/publish-build-task.coffee index 4f8df6336..de46eb4fe 100644 --- a/build/tasks/publish-build-task.coffee +++ b/build/tasks/publish-build-task.coffee @@ -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) -> diff --git a/build/tasks/set-version-task.coffee b/build/tasks/set-version-task.coffee index fc2382476..c7a29b584 100644 --- a/build/tasks/set-version-task.coffee +++ b/build/tasks/set-version-task.coffee @@ -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 diff --git a/package.json b/package.json index c6d368aff..b4009a254 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/resources/win/atom.cmd b/resources/win/atom.cmd index c9bfdd5ba..a1af5cd53 100644 --- a/resources/win/atom.cmd +++ b/resources/win/atom.cmd @@ -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 diff --git a/resources/win/atom.sh b/resources/win/atom.sh index 0eaf193c0..7380bf122 100644 --- a/resources/win/atom.sh +++ b/resources/win/atom.sh @@ -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 "$@" diff --git a/script/build b/script/build index a40a02e13..ca5569d0f 100755 --- a/script/build +++ b/script/build @@ -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); diff --git a/script/clean b/script/clean index 0c947baf2..cc4933f95 100755 --- a/script/clean +++ b/script/clean @@ -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); } diff --git a/spec/async-spec-helpers.coffee b/spec/async-spec-helpers.coffee index 5f8e03ca3..6ed8a5a2b 100644 --- a/spec/async-spec-helpers.coffee +++ b/spec/async-spec-helpers.coffee @@ -19,7 +19,9 @@ exports.afterEach = (fn) -> waitsForPromise = (fn) -> promise = fn() - waitsFor 'spec promise to resolve', 30000, (done) -> + # This timeout is 3 minutes. We need to bump it back down once we fix backgrounding + # of the renderer process on CI. See https://github.com/atom/electron/issues/4317 + waitsFor 'spec promise to resolve', 3 * 60 * 1000, (done) -> promise.then( done, (error) -> diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 1f8eb08e7..5fd4b11f1 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -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' diff --git a/spec/auto-update-manager-spec.js b/spec/auto-update-manager-spec.js new file mode 100644 index 000000000..6f7dbbb1a --- /dev/null +++ b/spec/auto-update-manager-spec.js @@ -0,0 +1,115 @@ +'use babel' + +import AutoUpdateManager from '../src/auto-update-manager' +import {remote} from 'electron' +const electronAutoUpdater = remote.require('electron').autoUpdater + +describe('AutoUpdateManager (renderer)', () => { + let autoUpdateManager + + beforeEach(() => { + autoUpdateManager = new AutoUpdateManager({ + applicationDelegate: atom.applicationDelegate + }) + }) + + afterEach(() => { + autoUpdateManager.destroy() + }) + + describe('::onDidBeginCheckingForUpdate', () => { + it('subscribes to "did-begin-checking-for-update" event', () => { + const spy = jasmine.createSpy('spy') + autoUpdateManager.onDidBeginCheckingForUpdate(spy) + electronAutoUpdater.emit('checking-for-update') + waitsFor(() => { + return spy.callCount === 1 + }) + }) + }) + + describe('::onDidBeginDownloadingUpdate', () => { + it('subscribes to "did-begin-downloading-update" event', () => { + const spy = jasmine.createSpy('spy') + autoUpdateManager.onDidBeginDownloadingUpdate(spy) + electronAutoUpdater.emit('update-available') + waitsFor(() => { + return spy.callCount === 1 + }) + }) + }) + + describe('::onDidCompleteDownloadingUpdate', () => { + it('subscribes to "did-complete-downloading-update" event', () => { + const spy = jasmine.createSpy('spy') + autoUpdateManager.onDidCompleteDownloadingUpdate(spy) + electronAutoUpdater.emit('update-downloaded', null, null, '1.2.3') + waitsFor(() => { + return spy.callCount === 1 + }) + runs(() => { + expect(spy.mostRecentCall.args[0].releaseVersion).toBe('1.2.3') + }) + }) + }) + + describe('::onUpdateNotAvailable', () => { + it('subscribes to "update-not-available" event', () => { + const spy = jasmine.createSpy('spy') + autoUpdateManager.onUpdateNotAvailable(spy) + electronAutoUpdater.emit('update-not-available') + waitsFor(() => { + return spy.callCount === 1 + }) + }) + }) + + describe('::platformSupportsUpdates', () => { + let state, releaseChannel + it('returns true on OS X and Windows when in stable', () => { + spyOn(autoUpdateManager, 'getState').andCallFake(() => state) + spyOn(atom, 'getReleaseChannel').andCallFake(() => releaseChannel) + + state = 'idle' + releaseChannel = 'stable' + expect(autoUpdateManager.platformSupportsUpdates()).toBe(true) + + state = 'idle' + releaseChannel = 'dev' + expect(autoUpdateManager.platformSupportsUpdates()).toBe(false) + + state = 'unsupported' + releaseChannel = 'stable' + expect(autoUpdateManager.platformSupportsUpdates()).toBe(false) + + state = 'unsupported' + releaseChannel = 'dev' + expect(autoUpdateManager.platformSupportsUpdates()).toBe(false) + }) + }) + + describe('::destroy', () => { + it('unsubscribes from all events', () => { + const spy = jasmine.createSpy('spy') + const doneIndicator = jasmine.createSpy('spy') + atom.applicationDelegate.onUpdateNotAvailable(doneIndicator) + autoUpdateManager.onDidBeginCheckingForUpdate(spy) + autoUpdateManager.onDidBeginDownloadingUpdate(spy) + autoUpdateManager.onDidCompleteDownloadingUpdate(spy) + autoUpdateManager.onUpdateNotAvailable(spy) + autoUpdateManager.destroy() + electronAutoUpdater.emit('checking-for-update') + electronAutoUpdater.emit('update-available') + electronAutoUpdater.emit('update-downloaded', null, null, '1.2.3') + electronAutoUpdater.emit('update-not-available') + + waitsFor(() => { + return doneIndicator.callCount === 1 + }) + + runs(() => { + expect(spy.callCount).toBe(0) + }) + }) + }) +}) diff --git a/spec/buffered-process-spec.coffee b/spec/buffered-process-spec.coffee index 04cff0b6d..1f524d66a 100644 --- a/spec/buffered-process-spec.coffee +++ b/spec/buffered-process-spec.coffee @@ -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' diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index c7485ea65..acd9b112b 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -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} diff --git a/spec/environment-helpers-spec.js b/spec/environment-helpers-spec.js new file mode 100644 index 000000000..20ec15d9f --- /dev/null +++ b/spec/environment-helpers-spec.js @@ -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() + }) + }) + }) +}) diff --git a/spec/fixtures/packages/package-with-prefixed-and-suffixed-repo-url/package.json b/spec/fixtures/packages/package-with-prefixed-and-suffixed-repo-url/package.json new file mode 100644 index 000000000..ce57f7501 --- /dev/null +++ b/spec/fixtures/packages/package-with-prefixed-and-suffixed-repo-url/package.json @@ -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." +} diff --git a/spec/fixtures/sample-with-comments.js b/spec/fixtures/sample-with-comments.js index c10d42232..b40ddc890 100644 --- a/spec/fixtures/sample-with-comments.js +++ b/spec/fixtures/sample-with-comments.js @@ -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)); -}; \ No newline at end of file +}; diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index fa5b0d711..900d81bfb 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -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') diff --git a/spec/git-spec.coffee b/spec/git-spec.coffee index 22c40c19a..3afd4da75 100644 --- a/spec/git-spec.coffee +++ b/spec/git-spec.coffee @@ -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 -> diff --git a/spec/language-mode-spec.coffee b/spec/language-mode-spec.coffee index cd32e29c7..26bb19b0e 100644 --- a/spec/language-mode-spec.coffee +++ b/spec/language-mode-spec.coffee @@ -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 -> diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 0a2d614b3..6a1610a8a 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -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() diff --git a/spec/pane-axis-element-spec.coffee b/spec/pane-axis-element-spec.coffee new file mode 100644 index 000000000..702e9c5fc --- /dev/null +++ b/spec/pane-axis-element-spec.coffee @@ -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])) diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee index e5cbee1a6..d0b191f38 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -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] = [] diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 9d42f9a7e..499efd017 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -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 diff --git a/spec/squirrel-update-spec.coffee b/spec/squirrel-update-spec.coffee index b71a9b0c4..e3891ed1c 100644 --- a/spec/squirrel-update-spec.coffee +++ b/spec/squirrel-update-spec.coffee @@ -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", -> diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 37a9751e1..1d1e4eb9f 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -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)`) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 05ac87c0c..f8117af09 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -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", -> diff --git a/spec/text-editor-registry-spec.coffee b/spec/text-editor-registry-spec.coffee new file mode 100644 index 000000000..04665bef2 --- /dev/null +++ b/spec/text-editor-registry-spec.coffee @@ -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 diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 6f16f0cf7..9d2a2a58c 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -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) diff --git a/spec/tooltip-manager-spec.coffee b/spec/tooltip-manager-spec.coffee index 87082504a..d4bfc1bd6 100644 --- a/spec/tooltip-manager-spec.coffee +++ b/spec/tooltip-manager-spec.coffee @@ -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) diff --git a/spec/view-registry-spec.coffee b/spec/view-registry-spec.coffee index 16672b25d..68a482b48 100644 --- a/spec/view-registry-spec.coffee +++ b/spec/view-registry-spec.coffee @@ -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", -> diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index 78bbf2fdb..97139f6bb 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -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() diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index cc1f4c946..3aff9e457 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -166,8 +166,7 @@ class ApplicationDelegate onDidOpenLocations: (callback) -> outerCallback = (event, message, detail) -> - if message is 'open-locations' - callback(detail) + callback(detail) if message is 'open-locations' ipcRenderer.on('message', outerCallback) new Disposable -> @@ -175,8 +174,38 @@ class ApplicationDelegate onUpdateAvailable: (callback) -> outerCallback = (event, message, detail) -> - if message is 'update-available' - callback(detail) + # TODO: Yes, this is strange that `onUpdateAvailable` is listening for + # `did-begin-downloading-update`. We currently have no mechanism to know + # if there is an update, so begin of downloading is a good proxy. + callback(detail) if message is 'did-begin-downloading-update' + + ipcRenderer.on('message', outerCallback) + new Disposable -> + ipcRenderer.removeListener('message', outerCallback) + + onDidBeginDownloadingUpdate: (callback) -> + @onUpdateAvailable(callback) + + onDidBeginCheckingForUpdate: (callback) -> + outerCallback = (event, message, detail) -> + callback(detail) if message is 'checking-for-update' + + ipcRenderer.on('message', outerCallback) + new Disposable -> + ipcRenderer.removeListener('message', outerCallback) + + onDidCompleteDownloadingUpdate: (callback) -> + outerCallback = (event, message, detail) -> + # TODO: We could rename this event to `did-complete-downloading-update` + callback(detail) if message is 'update-available' + + ipcRenderer.on('message', outerCallback) + new Disposable -> + ipcRenderer.removeListener('message', outerCallback) + + onUpdateNotAvailable: (callback) -> + outerCallback = (event, message, detail) -> + callback(detail) if message is 'update-not-available' ipcRenderer.on('message', outerCallback) new Disposable -> @@ -206,3 +235,12 @@ class ApplicationDelegate disablePinchToZoom: -> webFrame.setZoomLevelLimits(1, 1) + + checkForUpdate: -> + ipcRenderer.send('check-for-update') + + restartAndInstallUpdate: -> + ipcRenderer.send('install-update') + + getAutoUpdateManagerState: -> + ipcRenderer.sendSync('get-auto-update-manager-state') diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index c42bf05aa..6ef46dc2a 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -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}") diff --git a/src/auto-update-manager.js b/src/auto-update-manager.js new file mode 100644 index 000000000..62cc03f85 --- /dev/null +++ b/src/auto-update-manager.js @@ -0,0 +1,73 @@ +'use babel' + +import {Emitter, CompositeDisposable} from 'event-kit' + +export default class AutoUpdateManager { + constructor ({applicationDelegate}) { + this.applicationDelegate = applicationDelegate + this.subscriptions = new CompositeDisposable() + this.emitter = new Emitter() + + this.subscriptions.add( + applicationDelegate.onDidBeginCheckingForUpdate(() => { + this.emitter.emit('did-begin-checking-for-update') + }), + applicationDelegate.onDidBeginDownloadingUpdate(() => { + this.emitter.emit('did-begin-downloading-update') + }), + applicationDelegate.onDidCompleteDownloadingUpdate((details) => { + this.emitter.emit('did-complete-downloading-update', details) + }), + applicationDelegate.onUpdateNotAvailable(() => { + this.emitter.emit('update-not-available') + }) + ) + } + + destroy () { + this.subscriptions.dispose() + this.emitter.dispose() + } + + checkForUpdate () { + this.applicationDelegate.checkForUpdate() + } + + restartAndInstallUpdate () { + this.applicationDelegate.restartAndInstallUpdate() + } + + getState () { + return this.applicationDelegate.getAutoUpdateManagerState() + } + + platformSupportsUpdates () { + return atom.getReleaseChannel() !== 'dev' && this.getState() !== 'unsupported' + } + + onDidBeginCheckingForUpdate (callback) { + return this.emitter.on('did-begin-checking-for-update', callback) + } + + onDidBeginDownloadingUpdate (callback) { + return this.emitter.on('did-begin-downloading-update', callback) + } + + onDidCompleteDownloadingUpdate (callback) { + return this.emitter.on('did-complete-downloading-update', callback) + } + + // TODO: When https://github.com/atom/electron/issues/4587 is closed, we can + // add an update-available event. + // onUpdateAvailable (callback) { + // return this.emitter.on('update-available', callback) + // } + + onUpdateNotAvailable (callback) { + return this.emitter.on('update-not-available', callback) + } + + getPlatform () { + return process.platform + } +} diff --git a/src/block-decorations-component.coffee b/src/block-decorations-component.coffee index 0cfa7974f..35aec3921 100644 --- a/src/block-decorations-component.coffee +++ b/src/block-decorations-component.coffee @@ -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 diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index 40b04c3a1..4767c9065 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -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' diff --git a/src/browser/atom-window.coffee b/src/browser/atom-window.coffee index 634242e0d..33c64da7d 100644 --- a/src/browser/atom-window.coffee +++ b/src/browser/atom-window.coffee @@ -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 diff --git a/src/browser/auto-update-manager.coffee b/src/browser/auto-update-manager.coffee index 2df338761..c8c57cb01 100644 --- a/src/browser/auto-update-manager.coffee +++ b/src/browser/auto-update-manager.coffee @@ -39,16 +39,24 @@ class AutoUpdateManager autoUpdater.on 'checking-for-update', => @setState(CheckingState) + @emitWindowEvent('checking-for-update') autoUpdater.on 'update-not-available', => @setState(NoUpdateAvailableState) + @emitWindowEvent('update-not-available') autoUpdater.on 'update-available', => @setState(DownladingState) + # We use sendMessage to send an event called 'update-available' in 'update-downloaded' + # once the update download is complete. This mismatch between the electron + # autoUpdater events is unfortunate but in the interest of not changing the + # one existing event handled by applicationDelegate + @emitWindowEvent('did-begin-downloading-update') + @emit('did-begin-download') autoUpdater.on 'update-downloaded', (event, releaseNotes, @releaseVersion) => @setState(UpdateAvailableState) - @emitUpdateAvailableEvent(@getWindows()...) + @emitUpdateAvailableEvent() @config.onDidChange 'core.automaticallyUpdate', ({newValue}) => if newValue @@ -64,10 +72,14 @@ class AutoUpdateManager when 'linux' @setState(UnsupportedState) - emitUpdateAvailableEvent: (windows...) -> + emitUpdateAvailableEvent: -> return unless @releaseVersion? - for atomWindow in windows - atomWindow.sendMessage('update-available', {@releaseVersion}) + @emitWindowEvent('update-available', {@releaseVersion}) + return + + emitWindowEvent: (eventName, payload) -> + for atomWindow in @getWindows() + atomWindow.sendMessage(eventName, payload) return setState: (state) -> diff --git a/src/browser/main.coffee b/src/browser/main.coffee index b4df62bd6..6bf8817f9 100644 --- a/src/browser/main.coffee +++ b/src/browser/main.coffee @@ -13,6 +13,7 @@ console.log = require 'nslog' start = -> args = parseCommandLine() + args.env = process.env setupAtomHome(args) setupCompileCache() return if handleStartupEventWithSquirrel() diff --git a/src/buffered-node-process.coffee b/src/buffered-node-process.coffee index bb1a1c655..3b4916b24 100644 --- a/src/buffered-node-process.coffee +++ b/src/buffered-node-process.coffee @@ -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) diff --git a/src/buffered-process.coffee b/src/buffered-process.coffee index c7097d711..53934c02d 100644 --- a/src/buffered-process.coffee +++ b/src/buffered-process.coffee @@ -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 diff --git a/src/color.coffee b/src/color.coffee index fc751ce42..b413b9e2c 100644 --- a/src/color.coffee +++ b/src/color.coffee @@ -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 diff --git a/src/config.coffee b/src/config.coffee index 348b7b94f..66f07516e 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -319,6 +319,23 @@ ScopeDescriptor = require './scope-descriptor' # * line breaks - `line breaks
` # * ~~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. diff --git a/src/default-directory-provider.coffee b/src/default-directory-provider.coffee index 6b05a582f..ed4e9ba36 100644 --- a/src/default-directory-provider.coffee +++ b/src/default-directory-provider.coffee @@ -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() diff --git a/src/environment-helpers.js b/src/environment-helpers.js new file mode 100644 index 000000000..00c112bda --- /dev/null +++ b/src/environment-helpers.js @@ -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 } diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 894b1216a..17d293c14 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -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. diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 0513c2293..30d99791d 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -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]) diff --git a/src/initialize-application-window.coffee b/src/initialize-application-window.coffee index cea4e1c3c..ea811f515 100644 --- a/src/initialize-application-window.coffee +++ b/src/initialize-application-window.coffee @@ -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 -> diff --git a/src/language-mode.coffee b/src/language-mode.coffee index dc5003cac..4824431bf 100644 --- a/src/language-mode.coffee +++ b/src/language-mode.coffee @@ -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 diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index defcc0d8a..f4a7313ca 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -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) diff --git a/src/notification-manager.coffee b/src/notification-manager.coffee index 46c781c20..3d8b1895c 100644 --- a/src/notification-manager.coffee +++ b/src/notification-manager.coffee @@ -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: -> diff --git a/src/package-manager.coffee b/src/package-manager.coffee index 33f8f86a3..0e76a762f 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -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\+/, '') diff --git a/src/pane-axis-element.coffee b/src/pane-axis-element.coffee index eaa26a9fe..07439b914 100644 --- a/src/pane-axis-element.coffee +++ b/src/pane-axis-element.coffee @@ -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' diff --git a/src/pane.coffee b/src/pane.coffee index 12abc5448..3ff62993c 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -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: -> diff --git a/src/project.coffee b/src/project.coffee index 008d81e3e..93a3ed496 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -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 diff --git a/src/repository-status-handler.coffee b/src/repository-status-handler.coffee index 2fda9a335..adae7bc4f 100644 --- a/src/repository-status-handler.coffee +++ b/src/repository-status-handler.coffee @@ -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} diff --git a/src/selection.coffee b/src/selection.coffee index c4046677b..e208ea55a 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -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: -> diff --git a/src/state-store.js b/src/state-store.js index 13c9a0462..a2d3b476b 100644 --- a/src/state-store.js +++ b/src/state-store.js @@ -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) } diff --git a/src/storage-folder.coffee b/src/storage-folder.coffee index 06beae56a..280eb8b5c 100644 --- a/src/storage-folder.coffee +++ b/src/storage-folder.coffee @@ -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? diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index bdd0befcd..9b091100d 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -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) diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 380417163..a0ec1b7fa 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -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()) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index a3504caa8..ef1b403c3 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -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) diff --git a/src/text-editor-registry.coffee b/src/text-editor-registry.coffee new file mode 100644 index 000000000..8a17335d4 --- /dev/null +++ b/src/text-editor-registry.coffee @@ -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 diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 5d55e0e48..7d8d955e9 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -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? diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee index bf6a6dd2b..c1ac4caff 100644 --- a/src/tokenized-line.coffee +++ b/src/tokenized-line.coffee @@ -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 diff --git a/src/tooltip-manager.coffee b/src/tooltip-manager.coffee index 247437535..90f0ab8e6 100644 --- a/src/tooltip-manager.coffee +++ b/src/tooltip-manager.coffee @@ -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. diff --git a/src/tooltip.js b/src/tooltip.js index 4ea952a64..ad5ce0cdd 100644 --- a/src/tooltip.js +++ b/src/tooltip.js @@ -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') { diff --git a/src/view-registry.coffee b/src/view-registry.coffee index ef7151353..5fbfba729 100644 --- a/src/view-registry.coffee +++ b/src/view-registry.coffee @@ -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 diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index ce08344c6..6c338320d 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -143,7 +143,6 @@ class WindowEventHandler @reloadRequested = false @atomEnvironment.storeWindowDimensions() - @atomEnvironment.saveState() if confirmed @atomEnvironment.unloadEditorWindow() else diff --git a/src/workspace.coffee b/src/workspace.coffee index 636ebfd69..b8ed79fd6 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -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`. #