diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b4519bcf1..0aa249c18 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,7 +50,7 @@ To get a sense for the packages that are bundled with Atom, you can go to Settin Here's a list of the big ones: -* [atom/atom](https://github.com/atom/atom) - Atom Core! The core editor component is responsible for basic text editing (e.g. cursors, selections, scrolling), text indentation, wrapping, and folding, text rendering, editor rendering, file system operations (e.g. saving), and installation and auto-updating. You should also use this repository for feedback related to the [core API](https://atom.io/docs/api/latest/Notification) and for large, overarching design proposals. +* [atom/atom](https://github.com/atom/atom) - Atom Core! The core editor component is responsible for basic text editing (e.g. cursors, selections, scrolling), text indentation, wrapping, and folding, text rendering, editor rendering, file system operations (e.g. saving), and installation and auto-updating. You should also use this repository for feedback related to the [core API](https://atom.io/docs/api/latest) and for large, overarching design proposals. * [tree-view](https://github.com/atom/tree-view) - file and directory listing on the left of the UI. * [fuzzy-finder](https://github.com/atom/fuzzy-finder) - the quick file opener. * [find-and-replace](https://github.com/atom/find-and-replace) - all search and replace functionality. @@ -82,7 +82,7 @@ Before creating bug reports, please check [this list](#before-submitting-a-bug-r #### Before Submitting A Bug Report -* **Check the [debugging guide](https://atom.io/docs/latest/hacking-atom-debugging).** You might be able to find the cause of the problem and fix things yourself. Most importantly, check if you can reproduce the problem [in the latest version of Atom](https://atom.io/docs/latest/hacking-atom-debugging#update-to-the-latest-version), if the problem happens when you run Atom in [safe mode](https://atom.io/docs/latest/hacking-atom-debugging#check-if-the-problem-shows-up-in-safe-mode), and if you can get the desired behavior by changing [Atom's or packages' config settings](https://atom.io/docs/latest/hacking-atom-debugging#check-atom-and-package-settings). +* **Check the [debugging guide](http://flight-manual.atom.io/hacking-atom/sections/debugging/).** You might be able to find the cause of the problem and fix things yourself. Most importantly, check if you can reproduce the problem [in the latest version of Atom](http://flight-manual.atom.io/hacking-atom/sections/debugging/#update-to-the-latest-version), if the problem happens when you run Atom in [safe mode](http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-if-the-problem-shows-up-in-safe-mode), and if you can get the desired behavior by changing [Atom's or packages' config settings](http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-atom-and-package-settings). * **Check the [FAQs on the forum](https://discuss.atom.io/c/faq)** for a list of common questions and problems. * **Determine [which repository the problem should be reported in](#atom-and-packages)**. * **Perform a [cursory search](https://github.com/issues?q=+is%3Aissue+user%3Aatom)** to see if the problem has already been reported. If it has, add a comment to the existing issue instead of opening a new one. @@ -100,13 +100,13 @@ Explain the problem and include additional details to help maintainers reproduce * **Explain which behavior you expected to see instead and why.** * **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. If you use the keyboard while following the steps, **record the GIF with the [Keybinding Resolver](https://github.com/atom/keybinding-resolver) shown**. You can use [this tool](http://www.cockos.com/licecap/) to record GIFs on OSX and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. * **If you're reporting that Atom crashed**, include a crash report with a stack trace from the operating system. On OSX, the crash report will be available in `Console.app` under "Diagnostic and usage information" > "User diagnostic reports". Include the crash report in the issue in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines), a [file attachment](https://help.github.com/articles/file-attachments-on-issues-and-pull-requests/), or put it in a [gist](https://gist.github.com/) and provide link to that gist. -* **If the problem is related to performance**, include a [CPU profile capture and a screenshot](https://atom.io/docs/latest/hacking-atom-debugging#diagnose-performance-problems-with-the-dev-tools-cpu-profiler) with your report. +* **If the problem is related to performance**, include a [CPU profile capture and a screenshot](http://flight-manual.atom.io/hacking-atom/sections/debugging/#diagnose-performance-problems-with-the-dev-tools-cpu-profiler) with your report. * **If the Chrome's developer tools pane is shown without you triggering it**, that normally means that an exception was thrown. The Console tab will include an entry for the exception. Expand the exception so that the stack trace is visible, and provide the full exception and stack trace in a [code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines) and as a screenshot. * **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below. Provide more context by answering these questions: -* **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)?** +* **Can you reproduce the problem in [safe mode](http://flight-manual.atom.io/hacking-atom/sections/debugging/#diagnose-runtime-performance-problems-with-the-dev-tools-cpu-profiler)?** * **Did the problem start happening recently** (e.g. after updating to a new version of Atom) or was this always a problem? * If the problem started happening recently, **can you reproduce the problem in an older version of Atom?** What's the most recent version in which the problem doesn't happen? You can download older versions of Atom from [the releases page](https://github.com/atom/atom/releases). * **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens. @@ -118,7 +118,7 @@ Include details about your configuration and environment: * **What's the name and version of the OS you're using**? * **Are you running Atom in a virtual machine?** If so, which VM software are you using and which operating systems and versions are used for the host and the guest? * **Which [packages](#atom-and-packages) do you have installed?** You can get that list by running `apm list --installed`. -* **Are you using [local configuration files](https://atom.io/docs/latest/using-atom-basic-customization)** `config.cson`, `keymap.cson`, `snippets.cson`, `styles.less` and `init.coffee` to customize Atom? If so, provide the contents of those files, preferably in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines) or with a link to a [gist](https://gist.github.com/). +* **Are you using [local configuration files](http://flight-manual.atom.io/using-atom/sections/basic-customization/)** `config.cson`, `keymap.cson`, `snippets.cson`, `styles.less` and `init.coffee` to customize Atom? If so, provide the contents of those files, preferably in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines) or with a link to a [gist](https://gist.github.com/). * **Are you using Atom with multiple monitors?** If so, can you reproduce the problem when you use a single monitor? * **Which keyboard layout are you using?** Are you using a US layout or some other layout? @@ -166,7 +166,7 @@ Before creating enhancement suggestions, please check [this list](#before-submit #### Before Submitting An Enhancement Suggestion -* **Check the [debugging guide](https://atom.io/docs/latest/hacking-atom-debugging)** for tips — you might discover that the enhancement is already available. Most importantly, check if you're using [the latest version of Atom](https://atom.io/docs/latest/hacking-atom-debugging#update-to-the-latest-version) and if you can get the desired behavior by changing [Atom's or packages' config settings](https://atom.io/docs/latest/hacking-atom-debugging#check-atom-and-package-settings). +* **Check the [debugging guide](http://flight-manual.atom.io/hacking-atom/sections/debugging/)** for tips — you might discover that the enhancement is already available. Most importantly, check if you're using [the latest version of Atom](http://flight-manual.atom.io/hacking-atom/sections/debugging/#update-to-the-latest-version) and if you can get the desired behavior by changing [Atom's or packages' config settings](http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-atom-and-package-settings). * **Check if there's already [a package](https://atom.io/packages) which provides that enhancement.** * **Determine [which repository the enhancement should be suggested in](#atom-and-packages).** * **Perform a [cursory search](https://github.com/issues?q=+is%3Aissue+user%3Aatom)** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. @@ -376,7 +376,7 @@ Please open an issue on `atom/atom` if you have suggestions for new labels, and | `windows` | [search][search-atom-repo-label-windows] | [search][search-atom-org-label-windows] | Related to Atom running on Windows. | | `linux` | [search][search-atom-repo-label-linux] | [search][search-atom-org-label-linux] | Related to Atom running on Linux. | | `mac` | [search][search-atom-repo-label-mac] | [search][search-atom-org-label-mac] | Related to Atom running on OSX. | -| `documentation` | [search][search-atom-repo-label-documentation] | [search][search-atom-org-label-documentation] | Related to any type of documentation (e.g. [API documentation](https://atom.io/docs/api/latest/Atom) and the [flight manual](https://atom.io/docs/latest/)). | +| `documentation` | [search][search-atom-repo-label-documentation] | [search][search-atom-org-label-documentation] | Related to any type of documentation (e.g. [API documentation](https://atom.io/docs/api/latest/) and the [flight manual](http://flight-manual.atom.io/)). | | `performance` | [search][search-atom-repo-label-performance] | [search][search-atom-org-label-performance] | Related to performance. | | `security` | [search][search-atom-repo-label-security] | [search][search-atom-org-label-security] | Related to security. | | `ui` | [search][search-atom-repo-label-ui] | [search][search-atom-org-label-ui] | Related to visual design. | diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 8144a4b62..5b3ea42f4 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -2,7 +2,7 @@ * [ ] 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 [debugging guide](http://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? @@ -25,4 +25,4 @@ For more information on how to write a good [bug report](https://github.com/atom ### Versions -You can get this information from executing `atom --version` and `apm --version` at the command line. +You can get this information from executing `atom --version` and `apm --version` at the command line. Also, please include the OS and what version of the OS you're running. diff --git a/README.md b/README.md index dcda2146c..20e940689 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,22 @@ Currently only a 64-bit version is available. The Linux version does not currently automatically update so you will need to repeat these steps to upgrade to future releases. +### Archive extraction + +An archive is available for people who don't want to install `atom` as root. + +This version enables you to install multiple Atom versions in parallel. It has been built on Ubuntu 64-bit, +but should be compatible with other Linux distributions. + +1. Install dependencies (on Ubuntu): `sudo apt install git gconf2 gconf-service libgtk2.0-0 libudev1 libgcrypt20 +libnotify4 libxtst6 libnss3 python gvfs-bin xdg-utils libcap2` +2. Download `atom-amd64.tar.gz` from the [Atom releases page](https://github.com/atom/atom/releases/latest). +3. Run `tar xf atom-amd64.tar.gz` in the directory where you want to extract the Atom folder. +4. Launch Atom using the installed `atom` command from the newly extracted directory. + +The Linux version does not currently automatically update so you will need to +repeat these steps to upgrade to future releases. + ## Building * [Linux](docs/build-instructions/linux.md) diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index d9375d05c..f8ee607e5 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -285,6 +285,7 @@ module.exports = (grunt) -> ciTasks.push('dump-symbols') if process.platform is 'darwin' ciTasks.push('set-version', 'check-licenses', 'lint', 'generate-asar') ciTasks.push('mkdeb') if process.platform is 'linux' + ciTasks.push('mktar') if process.platform is 'linux' ciTasks.push('codesign:exe') if process.platform is 'win32' and not process.env.CI ciTasks.push('create-windows-installer:installer') if process.platform is 'win32' ciTasks.push('test') if process.platform is 'darwin' diff --git a/build/certs/AtomDevTestSignKey.p12 b/build/certs/AtomDevTestSignKey.p12 new file mode 100644 index 000000000..a93e9a9f0 Binary files /dev/null and b/build/certs/AtomDevTestSignKey.p12 differ diff --git a/build/tasks/codesign-task.coffee b/build/tasks/codesign-task.coffee index 2a061742b..559d41bbf 100644 --- a/build/tasks/codesign-task.coffee +++ b/build/tasks/codesign-task.coffee @@ -1,24 +1,46 @@ path = require 'path' +fs = require 'fs' +request = require 'request' module.exports = (grunt) -> {spawn} = require('./task-helpers')(grunt) + signUsingWindowsSDK = (exeToSign, callback) -> + {WIN_P12KEY_PASSWORD, WIN_P12KEY_URL} = process.env + if WIN_P12KEY_URL? + grunt.log.ok("Obtaining signing key") + downloadedKeyFile = path.resolve(__dirname, 'DownloadedSignKey.p12') + downloadFile WIN_P12KEY_URL, downloadedKeyFile, (done) -> + signUsingWindowsSDKTool exeToSign, downloadedKeyFile, WIN_P12KEY_PASSWORD, (done) -> + fs.unlinkSync(downloadedKeyFile) + callback() + else + signUsingWindowsSDKTool exeToSign, path.resolve(__dirname, '..', 'certs', 'AtomDevTestSignKey.p12'), 'password', callback + + signUsingWindowsSDKTool = (exeToSign, keyFilePath, password, callback) -> + grunt.log.ok("Signing #{exeToSign}") + args = ['sign', '/v', '/p', password, '/f', keyFilePath, exeToSign] + spawn {cmd: 'C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v7.1A\\bin\\signtool.exe', args: args}, callback + + signUsingJanky = (exeToSign, callback) -> + spawn {cmd: process.env.JANKY_SIGNTOOL, args: [exeToSign]}, callback + + signWindowsExecutable = if process.env.JANKY_SIGNTOOL then signUsingJanky else signUsingWindowsSDK + 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' atomExePath = path.join(grunt.config.get('atom.shellAppDir'), 'atom.exe') - spawn {cmd, args: [atomExePath]}, (error) -> + signWindowsExecutable atomExePath, (error) -> return done(error) if error? updateExePath = path.resolve(__dirname, '..', 'node_modules', 'grunt-electron-installer', 'vendor', 'Update.exe') - spawn {cmd, args: [updateExePath]}, (error) -> done(error) + signWindowsExecutable updateExePath, (error) -> done(error) 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) + signWindowsExecutable atomSetupExePath, (error) -> done(error) grunt.registerTask 'codesign:app', 'CodeSign Atom.app', -> done = @async() @@ -26,14 +48,24 @@ module.exports = (grunt) -> unlockKeychain (error) -> return done(error) if error? - cmd = 'codesign' args = ['--deep', '--force', '--verbose', '--sign', 'Developer ID Application: GitHub', grunt.config.get('atom.shellAppDir')] - spawn {cmd, args}, (error) -> done(error) + spawn {cmd: 'codesign', args: args}, (error) -> done(error) unlockKeychain = (callback) -> return callback() unless process.env.XCODE_KEYCHAIN - cmd = 'security' {XCODE_KEYCHAIN_PASSWORD, XCODE_KEYCHAIN} = process.env args = ['unlock-keychain', '-p', XCODE_KEYCHAIN_PASSWORD, XCODE_KEYCHAIN] - spawn {cmd, args}, (error) -> callback(error) + spawn {cmd: 'security', args: args}, (error) -> callback(error) + + downloadFile = (sourceUrl, targetPath, callback) -> + options = { + url: sourceUrl + headers: { + 'User-Agent': 'Atom Signing Key build task', + 'Accept': 'application/vnd.github.VERSION.raw' + } + } + request(options) + .pipe(fs.createWriteStream(targetPath)) + .on('finish', callback) diff --git a/build/tasks/license-overrides.coffee b/build/tasks/license-overrides.coffee index aa136eb37..be56934a2 100644 --- a/build/tasks/license-overrides.coffee +++ b/build/tasks/license-overrides.coffee @@ -123,10 +123,10 @@ module.exports = (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ - 'tweetnacl@0.13.2': + 'tweetnacl@0.14.3': repository: 'https://github.com/dchest/tweetnacl-js' license: 'Public Domain' - source: 'https://github.com/dchest/tweetnacl-js/blob/2f328394f74d83564634fb89ea2798caa3a4edb9/README.md says public domain.' + source: 'https://github.com/dchest/tweetnacl-js/blob/1042c9c65dc8f1dcb9e981d962d7dbbcf58f1fdc/COPYING.txt says public domain.' 'json-schema@0.2.2': repository: 'https://github.com/kriszyp/json-schema' license: 'BSD' diff --git a/build/tasks/mktar-task.coffee b/build/tasks/mktar-task.coffee new file mode 100644 index 000000000..f5edbb4be --- /dev/null +++ b/build/tasks/mktar-task.coffee @@ -0,0 +1,30 @@ +path = require 'path' + +module.exports = (grunt) -> + {spawn, fillTemplate} = require('./task-helpers')(grunt) + + grunt.registerTask 'mktar', 'Create an archive', -> + done = @async() + + appFileName = grunt.config.get('atom.appFileName') + buildDir = grunt.config.get('atom.buildDir') + shellAppDir = grunt.config.get('atom.shellAppDir') + {version, description} = grunt.config.get('atom.metadata') + + if process.arch is 'ia32' + arch = 'i386' + else if process.arch is 'x64' + arch = 'amd64' + else + return done("Unsupported arch #{process.arch}") + + iconPath = path.join(shellAppDir, 'resources', 'app.asar.unpacked', 'resources', 'atom.png') + + cmd = path.join('script', 'mktar') + args = [appFileName, version, arch, iconPath, buildDir] + spawn {cmd, args}, (error) -> + if error? + done(error) + else + grunt.log.ok "Created " + path.join(buildDir, "#{appFileName}-#{version}-#{arch}.tar.gz") + done() diff --git a/build/tasks/publish-build-task.coffee b/build/tasks/publish-build-task.coffee index de46eb4fe..19061db02 100644 --- a/build/tasks/publish-build-task.coffee +++ b/build/tasks/publish-build-task.coffee @@ -85,13 +85,13 @@ getAssets = -> arch = 'amd64' # Check for a Debian build - sourcePath = "#{buildDir}/#{appFileName}-#{version}-#{arch}.deb" + sourcePath = path.join(buildDir, "#{appFileName}-#{version}-#{arch}.deb") assetName = "atom-#{arch}.deb" # Check for a Fedora build unless fs.isFileSync(sourcePath) rpmName = fs.readdirSync("#{buildDir}/rpm")[0] - sourcePath = "#{buildDir}/rpm/#{rpmName}" + sourcePath = path.join(buildDir, "rpm", rpmName) if process.arch is 'ia32' arch = 'i386' else @@ -99,10 +99,17 @@ getAssets = -> assetName = "atom.#{arch}.rpm" cp sourcePath, path.join(buildDir, assetName) + assets = [{assetName, sourcePath}] - [ - {assetName, sourcePath} - ] + # Check for an archive build on a debian build machine. + # We could provide a Fedora version if some libraries are not compatible + sourcePath = path.join(buildDir, "#{appFileName}-#{version}-#{arch}.tar.gz") + if fs.isFileSync(sourcePath) + assetName = "atom-#{arch}.tar.gz" + cp sourcePath, path.join(buildDir, assetName) + assets.push({assetName, sourcePath}) + + assets logError = (message, error, details) -> grunt.log.error(message) diff --git a/build/tasks/spec-task.coffee b/build/tasks/spec-task.coffee index 40b7ad5ce..c1067231c 100644 --- a/build/tasks/spec-task.coffee +++ b/build/tasks/spec-task.coffee @@ -17,20 +17,6 @@ module.exports = (grunt) -> packageSpecQueue = null - logDeprecations = (label, {stderr}={}) -> - return unless process.env.JANKY_SHA1 or process.env.CI - stderr ?= '' - deprecatedStart = stderr.indexOf('Calls to deprecated functions') - return if deprecatedStart is -1 - - grunt.log.error(label) - stderr = stderr.substring(deprecatedStart) - stderr = stderr.replace(/^\s*\[[^\]]+\]\s+/gm, '') - stderr = stderr.replace(/source: .*$/gm, '') - stderr = stderr.replace(/^"/gm, '') - stderr = stderr.replace(/",\s*$/gm, '') - grunt.log.error(stderr) - getAppPath = -> contentsDir = grunt.config.get('atom.contentsDir') switch process.platform @@ -57,14 +43,14 @@ module.exports = (grunt) -> args: ['--test', "--resource-path=#{resourcePath}", path.join(packagePath, 'spec')] opts: cwd: packagePath - env: _.extend({}, process.env, ATOM_PATH: rootDir) + env: _.extend({}, process.env, ELECTRON_ENABLE_LOGGING: true, ATOM_PATH: rootDir) else if process.platform is 'win32' options = cmd: process.env.comspec args: ['/c', appPath, '--test', "--resource-path=#{resourcePath}", "--log-file=ci.log", path.join(packagePath, 'spec')] opts: cwd: packagePath - env: _.extend({}, process.env, ATOM_PATH: rootDir) + env: _.extend({}, process.env, ELECTRON_ENABLE_LOGGING: true, ATOM_PATH: rootDir) grunt.log.ok "Launching #{path.basename(packagePath)} specs." spawn options, (error, results, code) -> @@ -74,7 +60,6 @@ module.exports = (grunt) -> fs.unlinkSync(path.join(packagePath, 'ci.log')) failedPackages.push path.basename(packagePath) if error - logDeprecations("#{path.basename(packagePath)} Specs", results) callback() modulesDirectory = path.resolve('node_modules') @@ -87,7 +72,7 @@ module.exports = (grunt) -> packageSpecQueue.concurrency = Math.max(1, concurrency - 1) packageSpecQueue.drain = -> callback(null, failedPackages) - runCoreSpecs = (callback, logOutput = false) -> + runCoreSpecs = (callback) -> appPath = getAppPath() resourcePath = process.cwd() coreSpecsPath = path.resolve('spec') @@ -97,21 +82,16 @@ module.exports = (grunt) -> cmd: appPath args: ['--test', "--resource-path=#{resourcePath}", coreSpecsPath, "--user-data-dir=#{temp.mkdirSync('atom-user-data-dir')}"] opts: - env: _.extend({}, process.env, - ATOM_INTEGRATION_TESTS_ENABLED: true - ) + env: _.extend({}, process.env, {ELECTRON_ENABLE_LOGGING: true, ATOM_INTEGRATION_TESTS_ENABLED: true}) + stdio: 'inherit' else if process.platform is 'win32' options = cmd: process.env.comspec args: ['/c', appPath, '--test', "--resource-path=#{resourcePath}", '--log-file=ci.log', coreSpecsPath] opts: - env: _.extend({}, process.env, - ATOM_INTEGRATION_TESTS_ENABLED: true - ) - - if logOutput - options.opts.stdio = 'inherit' + env: _.extend({}, process.env, {ELECTRON_ENABLE_LOGGING: true, ATOM_INTEGRATION_TESTS_ENABLED: true}) + stdio: 'inherit' grunt.log.ok "Launching core specs." spawn options, (error, results, code) -> @@ -121,7 +101,6 @@ module.exports = (grunt) -> else # TODO: Restore concurrency on Windows packageSpecQueue?.concurrency = concurrency - logDeprecations('Core Specs', results) callback(null, error) @@ -134,17 +113,11 @@ module.exports = (grunt) -> else async.parallel - # If we're just running the core specs then we won't have any output to - # indicate the tests actually *are* running. This upsets Travis: - # https://github.com/atom/atom/issues/10837. So pass the test output - # through. - runCoreSpecsWithLogging = (callback) -> runCoreSpecs(callback, true) - specs = if process.env.ATOM_SPECS_TASK is 'packages' [runPackageSpecs] else if process.env.ATOM_SPECS_TASK is 'core' - [runCoreSpecsWithLogging] + [runCoreSpecs] else [runCoreSpecs, runPackageSpecs] diff --git a/docs/build-instructions/linux.md b/docs/build-instructions/linux.md index 1d746bcd6..126604c49 100644 --- a/docs/build-instructions/linux.md +++ b/docs/build-instructions/linux.md @@ -6,8 +6,8 @@ Ubuntu LTS 12.04 64-bit is the recommended platform. * OS with 64-bit or 32-bit architecture * C++ toolchain - * [Git](http://git-scm.com/) - * [Node.js](http://nodejs.org/download/) (0.10.x or above) + * [Git](https://git-scm.com/) + * [Node.js](https://nodejs.org/en/download/) (0.10.x or above) * [npm](https://www.npmjs.com/) v1.4.x or above (automatically bundled with Node.js) * `npm -v` to check the version. * `npm config set python /usr/bin/python2 -g` to ensure that gyp uses python2. @@ -74,18 +74,24 @@ If you have problems with permissions don't forget to prefix with `sudo` To use the newly installed Atom, quit and restart all running Atom instances. -5. *Optionally*, you may generate distributable packages of Atom at `out`. Currently, `.deb` and `.rpm` package types are supported. To create a `.deb` package run: +5. *Optionally*, you may generate distributable packages of Atom at `out`. Currently, `.deb` and `.rpm` package types are supported, as well as a `.tar.gz` archive. To create a `.deb` package run: ```sh script/grunt mkdeb ``` - To create an `.rpm` package run + To create a `.rpm` package run ```sh script/grunt mkrpm ``` + To create a `.tar.gz` archive run + + ```sh + script/grunt mktar + ``` + ## Advanced Options ### Custom build directory diff --git a/docs/build-instructions/os-x.md b/docs/build-instructions/os-x.md index 76da080c9..d9e15808b 100644 --- a/docs/build-instructions/os-x.md +++ b/docs/build-instructions/os-x.md @@ -3,7 +3,7 @@ ## Requirements * OS X 10.8 or later - * [Node.js](http://nodejs.org/download/) (0.10.x or above) + * [Node.js](https://nodejs.org/en/download/) (0.10.x or above) * Command Line Tools for [Xcode](https://developer.apple.com/xcode/downloads/) (run `xcode-select --install` to install) ## Instructions diff --git a/docs/build-instructions/windows.md b/docs/build-instructions/windows.md index fdd55fdde..3ec28f139 100644 --- a/docs/build-instructions/windows.md +++ b/docs/build-instructions/windows.md @@ -3,7 +3,7 @@ ## Requirements ### General - * [Node.js](http://nodejs.org/en/download/) v4.x + * [Node.js](https://nodejs.org/en/download/) v4.x * [Python](https://www.python.org/downloads/) v2.7.x * The python.exe must be available at `%SystemDrive%\Python27\python.exe`. If it is installed elsewhere, you can create a symbolic link to the @@ -14,29 +14,27 @@ You can use either: - * [Visual Studio 2013 Update 5](http://www.visualstudio.com/en-us/downloads/download-visual-studio-vs) (Express or better) on Windows 7, 8 or 10 - * [Visual Studio 2015](http://www.visualstudio.com/en-us/downloads/download-visual-studio-vs) (Community or better) with Windows 8 or 10 + * [Visual Studio 2013 Update 5](https://www.visualstudio.com/en-us/downloads/download-visual-studio-vs) (Express or better) on Windows 7, 8 or 10 + * [Visual Studio 2015](https://www.visualstudio.com/en-us/downloads/download-visual-studio-vs) (Community or better) with Windows 8 or 10 Whichever version you use, ensure that: * The default installation folder is chosen so the build tools can find it * Visual C++ support is installed - * You set the `GYP_MSVS_VERSION` environment variable to the Visual Studio version (`2013` or `2015`), e.g. , e.g. ``[Environment]::SetEnvironmentVariable("GYP_MSVS_VERSION", "2015", "User")`` in PowerShell or set it in Windows advanced system settings control panel. - * The git command is in your path + * A `git` command is in your path + * If you have both VS2013 and VS2015 installed set the `GYP_MSVS_VERSION` environment variable to the Visual Studio version (`2013` or `2015`) you wish to use, e.g. ``[Environment]::SetEnvironmentVariable("GYP_MSVS_VERSION", "2015", "User")`` in PowerShell or set it in Windows advanced system settings control panel. ## Instructions You can run these commands using Command Prompt, PowerShell or Git Shell via [GitHub Desktop](https://desktop.github.com/). These instructions will assume the use of Bash from Git Shell - if you are using Command Prompt use a backslash instead: i.e. `script\build`. -**VS2015 + Git Shell users** should note that the default path supplied with Git Shell includes reference to an older version of msbuild that will fail. It is recommended you use a PowerShell window that has git in the path at this time. - ```bash cd C:\ git clone https://github.com/atom/atom/ cd atom script/build ``` -This will create the Atom application in the `Program Files` folder. +This will create the Atom application in the `out\Atom` folder as well as copy it to a folder named `Atom` within `Program Files`. ### `script/build` Options * `--install-dir` - Creates the final built application in this directory. Example (trailing slash is optional): @@ -47,6 +45,7 @@ This will create the Atom application in the `Program Files` folder. ```bash ./script/build --build-dir Z:\Some\Temporary\Directory\ ``` + * `--no-install` - Skips the installation task after building. * `--verbose` - Verbose mode. A lot more information output. ## Do I have to use GitHub Desktop? @@ -63,37 +62,34 @@ If none of this works, do install Github Desktop and use its Git Shell as it mak ### Common Errors * `node is not recognized` - * If you just installed Node.js, you'll need to restart your PowerShell/Command Prompt/Git Shell before the node command is available on your Path. -* `script/build` outputs only the Node.js and Python versions before returning +* `msbuild.exe failed with exit code: 1` + * Ensure you have Visual C++ support installed. Go into Add/Remove Programs, select Visual Studio and press Modify and then check the Visual C++ box. +* `script/build` stops with no error or warning shortly after displaying the versions of node, npm and Python + * Make sure that the path where you have checked out Atom does not include a space. e.g. use `c:\atom` and not `c:\my stuff\atom` + +* `script/build` outputs only the Node.js and Python versions before returning * Try moving the repository to `C:\atom`. Most likely, the path is too long. See [issue #2200](https://github.com/atom/atom/issues/2200). * `error MSB4025: The project file could not be loaded. Invalid character in the given encoding.` - * This can occur because your home directory (`%USERPROFILE%`) has non-ASCII characters in it. This is a bug in [gyp](https://code.google.com/p/gyp/) which is used to build native Node.js modules and there is no known workaround. * https://github.com/TooTallNate/node-gyp/issues/297 * https://code.google.com/p/gyp/issues/detail?id=393 -* `script/build` stops at installing runas with `Failed at the runas@x.y.z install script.` +* `'node_modules\.bin\npm' is not recognized as an internal or external command, operable program or batch file.` + * This occurs if the previous build left things in a bad state. Run `script\clean` and then `script\build` again. +* `script/build` stops at installing runas with `Failed at the runas@x.y.z install script.` * See the next item. * `error MSB8020: The build tools for Visual Studio 201? (Platform Toolset = 'v1?0') cannot be found.` - - * If you're building Atom with Visual Studio 2013 or above make sure the `GYP_MSVS_VERSION` environment variable is set, and then re-run `script/build` after a clean: - - ```bash - $env:GYP_MSVS_VERSION='2013' # '2015' if using Visual Studio 2015, and so on - script/clean - script/build - ``` - * If you are using Visual Studio 2013 or above and the build fails with some other error message this environment variable might still be required and ensure you have Visual C++ language support installed. + * If you're building Atom with Visual Studio 2013 try setting the `GYP_MSVS_VERSION` environment variable to 2013 and then `script/clean` followed by `script/build` (re-open your command prompt or Powershell window if you set it using the GUI) * Other `node-gyp` errors on first build attempt, even though the right Node.js and Python versions are installed. * Do try the build command one more time, as experience shows it often works on second try in many of these cases. diff --git a/docs/native-profiling.md b/docs/native-profiling.md new file mode 100644 index 000000000..f9f9160a5 --- /dev/null +++ b/docs/native-profiling.md @@ -0,0 +1,18 @@ +# Profiling the Atom Render Process on OS X with Instruments + +![Instruments](https://cloud.githubusercontent.com/assets/1789/14193295/d503db7a-f760-11e5-88bf-fe417c0cd913.png) + +* Determine the version of Electron for your version of Atom. + * Open the dev tools with `alt-cmd-i` + * Evaluate `process.versions.electron` in the console. +* Based on this version, download the appropriate Electron symbols from the [releases](https://github.com/atom/electron/releases) page. + * The file name should look like `electron-v0.X.Y-darwin-x64-dsym.zip`. + * Decompress these symbols in your `~/Downloads` directory. +* Now create a time profile in Instruments. + * Open `Instruments.app`. + * Select `Time Profiler` + * In Atom, determine the pid to attach to by evaluating `process.pid` in the dev tools console. + * Attach to this pid via the menu at the upper left corner of the Instruments profiler. + * Click record, do your thing. + * Click stop. + * The symbols should have been automatically located by Instruments (via Spotlight or something?), giving you a readable profile. diff --git a/dot-atom/keymap.cson b/dot-atom/keymap.cson index 10ad345d4..fd7c4f96e 100644 --- a/dot-atom/keymap.cson +++ b/dot-atom/keymap.cson @@ -18,15 +18,15 @@ # 'ctrl-p': 'core:move-down' # # You can find more information about keymaps in these guides: -# * https://atom.io/docs/latest/using-atom-basic-customization#customizing-key-bindings -# * https://atom.io/docs/latest/behind-atom-keymaps-in-depth +# * http://flight-manual.atom.io/using-atom/sections/basic-customization/#_customizing_keybindings +# * http://flight-manual.atom.io/behind-atom/sections/keymaps-in-depth/ # # If you're having trouble with your keybindings not working, try the # Keybinding Resolver: `Cmd+.` on OS X and `Ctrl+.` on other platforms. See the # Debugging Guide for more information: -# * https://atom.io/docs/latest/hacking-atom-debugging#check-the-keybindings +# * http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-the-keybindings # # This file uses CoffeeScript Object Notation (CSON). # If you are unfamiliar with CSON, you can read more about it in the # Atom Flight Manual: -# https://atom.io/docs/latest/using-atom-basic-customization#cson +# http://flight-manual.atom.io/using-atom/sections/basic-customization/#_cson diff --git a/keymaps/darwin.cson b/keymaps/darwin.cson index 819e0079e..953fb0c48 100644 --- a/keymaps/darwin.cson +++ b/keymaps/darwin.cson @@ -130,6 +130,8 @@ # Atom Specific 'ctrl-W': 'editor:select-word' + 'cmd-ctrl-left': 'editor:move-selection-left' + 'cmd-ctrl-right': 'editor:move-selection-right' # Sublime Parity 'cmd-a': 'core:select-all' diff --git a/keymaps/linux.cson b/keymaps/linux.cson index e676ed5ab..7d67e2ce5 100644 --- a/keymaps/linux.cson +++ b/keymaps/linux.cson @@ -14,6 +14,8 @@ 'ctrl-shift-pageup': 'pane:move-item-left' 'ctrl-shift-pagedown': 'pane:move-item-right' 'f11': 'window:toggle-full-screen' + 'alt-shift-left': 'editor:move-selection-left' + 'alt-shift-right': 'editor:move-selection-right' # Sublime Parity 'ctrl-,': 'application:show-settings' diff --git a/keymaps/win32.cson b/keymaps/win32.cson index 5869a3ed8..10450ec18 100644 --- a/keymaps/win32.cson +++ b/keymaps/win32.cson @@ -20,6 +20,8 @@ 'ctrl-shift-left': 'pane:move-item-left' 'ctrl-shift-right': 'pane:move-item-right' 'f11': 'window:toggle-full-screen' + 'alt-shift-left': 'editor:move-selection-left' + 'alt-shift-right': 'editor:move-selection-right' # Sublime Parity 'ctrl-,': 'application:show-settings' diff --git a/menus/darwin.cson b/menus/darwin.cson index 7fa2aaf6d..bb3ce0acf 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -75,6 +75,13 @@ { label: 'Join Lines', command: 'editor:join-lines' } ] } + { + label: 'Columns', + submenu: [ + { label: 'Move Selection Left', command: 'editor:move-selection-left' } + { label: 'Move Selection Right', command: 'editor:move-selection-right' } + ] + } { label: 'Text', submenu: [ @@ -221,10 +228,10 @@ {label: 'Delete', command: 'core:delete'} {label: 'Select All', command: 'core:select-all'} {type: 'separator'} - {label: 'Split Up', command: 'pane:split-up'} - {label: 'Split Down', command: 'pane:split-down'} - {label: 'Split Left', command: 'pane:split-left'} - {label: 'Split Right', command: 'pane:split-right'} + {label: 'Split Up', command: 'pane:split-up-and-copy-active-item'} + {label: 'Split Down', command: 'pane:split-down-and-copy-active-item'} + {label: 'Split Left', command: 'pane:split-left-and-copy-active-item'} + {label: 'Split Right', command: 'pane:split-right-and-copy-active-item'} {label: 'Close Pane', command: 'pane:close'} {type: 'separator'} ] diff --git a/menus/linux.cson b/menus/linux.cson index 7cfd72885..b84fc8053 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -48,6 +48,13 @@ { label: '&Join Lines', command: 'editor:join-lines' } ] } + { + label: 'Columns', + submenu: [ + { label: 'Move Selection &Left', command: 'editor:move-selection-left' } + { label: 'Move Selection &Right', command: 'editor:move-selection-right' } + ] + } { label: 'Text', submenu: [ @@ -197,10 +204,10 @@ {label: 'Delete', command: 'core:delete'} {label: 'Select All', command: 'core:select-all'} {type: 'separator'} - {label: 'Split Up', command: 'pane:split-up'} - {label: 'Split Down', command: 'pane:split-down'} - {label: 'Split Left', command: 'pane:split-left'} - {label: 'Split Right', command: 'pane:split-right'} + {label: 'Split Up', command: 'pane:split-up-and-copy-active-item'} + {label: 'Split Down', command: 'pane:split-down-and-copy-active-item'} + {label: 'Split Left', command: 'pane:split-left-and-copy-active-item'} + {label: 'Split Right', command: 'pane:split-right-and-copy-active-item'} {label: 'Close Pane', command: 'pane:close'} {type: 'separator'} ] diff --git a/menus/win32.cson b/menus/win32.cson index 349e3e064..323db5d18 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -56,6 +56,13 @@ { label: '&Join Lines', command: 'editor:join-lines' } ] } + { + label: 'Columns', + submenu: [ + { label: 'Move Selection &Left', command: 'editor:move-selection-left' } + { label: 'Move Selection &Right', command: 'editor:move-selection-right' } + ] + } { label: 'Text', submenu: [ @@ -200,10 +207,10 @@ {label: 'Delete', command: 'core:delete'} {label: 'Select All', command: 'core:select-all'} {type: 'separator'} - {label: 'Split Up', command: 'pane:split-up'} - {label: 'Split Down', command: 'pane:split-down'} - {label: 'Split Left', command: 'pane:split-left'} - {label: 'Split Right', command: 'pane:split-right'} + {label: 'Split Up', command: 'pane:split-up-and-copy-active-item'} + {label: 'Split Down', command: 'pane:split-down-and-copy-active-item'} + {label: 'Split Left', command: 'pane:split-left-and-copy-active-item'} + {label: 'Split Right', command: 'pane:split-right-and-copy-active-item'} {label: 'Close Pane', command: 'pane:close'} {type: 'separator'} ] diff --git a/package.json b/package.json index 1e529898e..93774e26f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "1.8.0-dev", + "version": "1.9.0-dev", "description": "A hackable text editor for the 21st Century.", "main": "./src/browser/main.js", "repository": { @@ -15,7 +15,7 @@ "electronVersion": "0.36.8", "dependencies": { "async": "0.2.6", - "atom-keymap": "^6.3.1", + "atom-keymap": "6.3.2", "babel-core": "^5.8.21", "bootstrap": "^3.3.4", "cached-run-in-this-context": "0.4.1", @@ -37,7 +37,7 @@ "less-cache": "0.23", "line-top-index": "0.2.0", "marked": "^0.3.4", - "nodegit": "0.12.0", + "nodegit": "0.12.2", "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.4.3", + "text-buffer": "8.4.6", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" @@ -66,32 +66,32 @@ "atom-light-ui": "0.43.0", "base16-tomorrow-dark-theme": "1.1.0", "base16-tomorrow-light-theme": "1.1.1", - "one-dark-ui": "1.3.0", - "one-light-ui": "1.3.0", + "one-dark-ui": "1.3.1", + "one-light-ui": "1.3.1", "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.4.2", + "solarized-dark-syntax": "1.0.2", + "solarized-light-syntax": "1.0.2", + "about": "1.5.0", "archive-view": "0.61.1", "autocomplete-atom-api": "0.10.0", - "autocomplete-css": "0.11.0", + "autocomplete-css": "0.11.1", "autocomplete-html": "0.7.2", - "autocomplete-plus": "2.29.1", + "autocomplete-plus": "2.29.2", "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.81.0", + "bookmarks": "0.39.0", + "bracket-matcher": "0.82.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.4", - "fuzzy-finder": "1.0.3", + "exception-reporting": "0.38.1", + "fuzzy-finder": "1.0.4", "git-diff": "1.0.1", + "find-and-replace": "0.198.0", "go-to-line": "0.30.0", "grammar-selector": "0.48.1", "image-view": "0.57.0", @@ -101,28 +101,28 @@ "link": "0.31.1", "markdown-preview": "0.158.0", "metrics": "0.53.1", - "notifications": "0.63.1", - "open-on-github": "1.0.1", + "notifications": "0.63.2", + "open-on-github": "1.1.0", "package-generator": "1.0.0", "settings-view": "0.235.1", "snippets": "1.0.2", - "spell-check": "0.67.0", - "status-bar": "1.2.0", + "spell-check": "0.67.1", + "status-bar": "1.2.3", "styleguide": "0.45.2", "symbols-view": "0.112.0", - "tabs": "0.92.0", + "tabs": "0.92.2", "timecop": "0.33.1", - "tree-view": "0.203.3", + "tree-view": "0.206.0", "update-package-dependencies": "0.10.0", "welcome": "0.34.0", "whitespace": "0.32.2", "wrap-guide": "0.38.1", - "language-c": "0.51.2", + "language-c": "0.51.3", "language-clojure": "0.20.0", "language-coffee-script": "0.46.1", - "language-csharp": "0.12.0", - "language-css": "0.36.0", - "language-gfm": "0.85.0", + "language-csharp": "0.12.1", + "language-css": "0.36.1", + "language-gfm": "0.86.0", "language-git": "0.12.1", "language-go": "0.42.0", "language-html": "0.44.1", @@ -130,24 +130,24 @@ "language-java": "0.17.0", "language-javascript": "0.116.0", "language-json": "0.18.0", - "language-less": "0.29.1", - "language-make": "0.21.0", + "language-less": "0.29.3", + "language-make": "0.21.1", "language-mustache": "0.13.0", "language-objective-c": "0.15.1", - "language-perl": "0.32.0", + "language-perl": "0.34.0", "language-php": "0.37.0", "language-property-list": "0.8.0", "language-python": "0.43.1", - "language-ruby": "0.68.4", + "language-ruby": "0.68.5", "language-ruby-on-rails": "0.25.0", "language-sass": "0.46.0", "language-shellscript": "0.21.1", "language-source": "0.9.0", - "language-sql": "0.20.0", + "language-sql": "0.21.0", "language-text": "0.7.1", "language-todo": "0.27.0", "language-toml": "0.18.0", - "language-xml": "0.34.4", + "language-xml": "0.34.5", "language-yaml": "0.25.2" }, "private": true, diff --git a/script/mktar b/script/mktar new file mode 100755 index 000000000..986063f9a --- /dev/null +++ b/script/mktar @@ -0,0 +1,39 @@ +#!/bin/bash +# mktar name version arch icon-path build-root-path + +set -e + +SCRIPT=`readlink -f "$0"` +ROOT=`readlink -f $(dirname $SCRIPT)/..` +cd $ROOT + +NAME="$1" +VERSION="$2" +ARCH="$3" +ICON_FILE="$4" +BUILD_ROOT_PATH="$5" +FILE_MODE=755 + +TAR_PATH=$BUILD_ROOT_PATH +ATOM_PATH="$BUILD_ROOT_PATH/Atom" + +TARGET_ROOT="`mktemp -d`" +chmod $FILE_MODE "$TARGET_ROOT" +NAME_IN_TAR="$NAME-$VERSION-$ARCH" +TARGET="$TARGET_ROOT/$NAME_IN_TAR" + +# Copy executable and resources +cp -a "$ATOM_PATH" "$TARGET" + +# Copy icon file +cp "$ICON_FILE" "$TARGET/$NAME.png" + +# Remove executable bit from .node files +find "$TARGET" -type f -name "*.node" -exec chmod a-x {} \; + +# Create the archive +pushd "$TARGET_ROOT" +tar caf "$TAR_PATH/$NAME_IN_TAR.tar.gz" "$NAME_IN_TAR" +popd + +rm -rf "$TARGET_ROOT" diff --git a/script/utils/child-process-wrapper.js b/script/utils/child-process-wrapper.js index 2afc57934..55e6733d1 100644 --- a/script/utils/child-process-wrapper.js +++ b/script/utils/child-process-wrapper.js @@ -17,7 +17,7 @@ exports.safeExec = function(command, options, callback) { var child = childProcess.exec(command, options, function(error, stdout, stderr) { if (error) process.exit(error.code || 1); - else + else if (callback) callback(null); }); child.stderr.pipe(process.stderr); diff --git a/spec/auto-update-manager-spec.js b/spec/auto-update-manager-spec.js index 6f7dbbb1a..91b52192c 100644 --- a/spec/auto-update-manager-spec.js +++ b/spec/auto-update-manager-spec.js @@ -64,6 +64,16 @@ describe('AutoUpdateManager (renderer)', () => { }) }) + describe('::onUpdateError', () => { + it('subscribes to "update-error" event', () => { + const spy = jasmine.createSpy('spy') + autoUpdateManager.onUpdateError(spy) + electronAutoUpdater.emit('error', {}, 'an error message') + waitsFor(() => spy.callCount === 1) + runs(() => expect(autoUpdateManager.getErrorMessage()).toBe('an error message')) + }) + }) + describe('::platformSupportsUpdates', () => { let state, releaseChannel it('returns true on OS X and Windows when in stable', () => { diff --git a/spec/buffered-process-spec.coffee b/spec/buffered-process-spec.coffee index 1f524d66a..643a2d411 100644 --- a/spec/buffered-process-spec.coffee +++ b/spec/buffered-process-spec.coffee @@ -88,14 +88,15 @@ describe "BufferedProcess", -> describe "when the explorer command is spawned on Windows", -> it "doesn't quote arguments of the form /root,C...", -> new BufferedProcess({command: 'explorer.exe', args: ['/root,C:\\foo']}) - expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe '"explorer.exe /root,C:\\foo"' + expect(ChildProcess.spawn.argsForCall[0][1][3]).toBe '"explorer.exe /root,C:\\foo"' it "spawns the command using a cmd.exe wrapper", -> new BufferedProcess({command: 'dir'}) expect(path.basename(ChildProcess.spawn.argsForCall[0][0])).toBe 'cmd.exe' expect(ChildProcess.spawn.argsForCall[0][1][0]).toBe '/s' - expect(ChildProcess.spawn.argsForCall[0][1][1]).toBe '/c' - expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe '"dir"' + expect(ChildProcess.spawn.argsForCall[0][1][1]).toBe '/d' + expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe '/c' + expect(ChildProcess.spawn.argsForCall[0][1][3]).toBe '"dir"' it "calls the specified stdout, stderr, and exit callbacks", -> stdout = '' diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js index 900d81bfb..0442248e1 100644 --- a/spec/git-repository-async-spec.js +++ b/spec/git-repository-async-spec.js @@ -230,9 +230,7 @@ describe('GitRepositoryAsync', () => { }) }) - // @joshaber: Disabling for now. There seems to be some race with path - // subscriptions leading to intermittent test failures, e.g.: https://travis-ci.org/atom/atom/jobs/102702554 - xdescribe('.checkoutHeadForEditor(editor)', () => { + describe('.checkoutHeadForEditor(editor)', () => { let filePath let editor diff --git a/spec/integration/startup-spec.coffee b/spec/integration/startup-spec.coffee index f6b0e1cf3..7c2503e69 100644 --- a/spec/integration/startup-spec.coffee +++ b/spec/integration/startup-spec.coffee @@ -268,6 +268,36 @@ describe "Starting Atom", -> [otherTempDirPath] ].sort() + it "doesn't reopen any previously opened windows if restorePreviousWindowsOnStart is disabled", -> + runAtom [tempDirPath], {ATOM_HOME: atomHome}, (client) -> + client + .waitForExist("atom-workspace") + .waitForNewWindow(-> + @startAnotherAtom([otherTempDirPath], ATOM_HOME: atomHome) + , 5000) + .waitForExist("atom-workspace") + + configPath = path.join(atomHome, 'config.cson') + config = CSON.readFileSync(configPath) + config['*'].core = {restorePreviousWindowsOnStart: false} + CSON.writeFileSync(configPath, config) + + runAtom [], {ATOM_HOME: atomHome}, (client) -> + windowProjectPaths = [] + + client + .waitForWindowCount(1, 10000) + .then ({value: windowHandles}) -> + @window(windowHandles[0]) + .waitForExist("atom-workspace") + .treeViewRootDirectories() + .then ({value: directories}) -> windowProjectPaths.push(directories) + + .call -> + expect(windowProjectPaths).toEqual [ + [] + ] + describe "opening a remote directory", -> it "opens the parent directory and creates an empty text editor", -> remoteDirectory = 'remote://server:3437/some/directory/path' diff --git a/spec/jasmine-test-runner.coffee b/spec/jasmine-test-runner.coffee index 5b5d4e225..dd8386f5d 100644 --- a/spec/jasmine-test-runner.coffee +++ b/spec/jasmine-test-runner.coffee @@ -1,3 +1,4 @@ +Grim = require 'grim' _ = require 'underscore-plus' fs = require 'fs-plus' path = require 'path' @@ -96,13 +97,10 @@ buildTerminalReporter = (logFile, resolveWithExitCode) -> log(str) onComplete: (runner) -> fs.closeSync(logStream) if logStream? - if process.env.JANKY_SHA1 or process.env.CI - grim = require 'grim' - - if grim.getDeprecationsLength() > 0 - grim.logDeprecations() - resolveWithExitCode(1) - return + if Grim.getDeprecationsLength() > 0 + Grim.logDeprecations() + resolveWithExitCode(1) + return if runner.results().failedCount > 0 resolveWithExitCode(1) diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 6a1610a8a..f68005cf5 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -66,6 +66,9 @@ describe "PackageManager", -> expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load the package-with-broken-package-json package") expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-broken-package-json" + it "returns null if the package name or path starts with a dot", -> + expect(atom.packages.loadPackage("/Users/user/.atom/packages/.git")).toBeNull() + it "normalizes short repository urls in package.json", -> {metadata} = atom.packages.loadPackage("package-with-short-url-package-json") expect(metadata.repository.type).toBe "git" diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee index d0b191f38..9969b1dcc 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -262,6 +262,26 @@ describe "Pane", -> pane.setPendingItem("fake item two") expect(callbackCalled).toBeTruthy() + it "isn't called when a pending item is replaced with a new one", -> + pane = null + pendingSpy = jasmine.createSpy("onItemDidTerminatePendingState") + destroySpy = jasmine.createSpy("onWillDestroyItem") + + waitsForPromise -> + atom.workspace.open('sample.txt', pending: true).then -> + pane = atom.workspace.getActivePane() + + runs -> + pane.onItemDidTerminatePendingState pendingSpy + pane.onWillDestroyItem destroySpy + + waitsForPromise -> + atom.workspace.open('sample.js', pending: true) + + runs -> + expect(destroySpy).toHaveBeenCalled() + expect(pendingSpy).not.toHaveBeenCalled() + 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")])) diff --git a/spec/resource-pool-spec.js b/spec/resource-pool-spec.js new file mode 100644 index 000000000..27893360a --- /dev/null +++ b/spec/resource-pool-spec.js @@ -0,0 +1,66 @@ +/** @babel */ + +import ResourcePool from '../src/resource-pool' + +import {it} from './async-spec-helpers' + +describe('ResourcePool', () => { + let queue + + beforeEach(() => { + queue = new ResourcePool([{}]) + }) + + describe('.enqueue', () => { + it('calls the enqueued function', async () => { + let called = false + await queue.enqueue(() => { + called = true + return Promise.resolve() + }) + expect(called).toBe(true) + }) + + it('forwards values from the inner promise', async () => { + const result = await queue.enqueue(() => Promise.resolve(42)) + expect(result).toBe(42) + }) + + it('forwards errors from the inner promise', async () => { + let threw = false + try { + await queue.enqueue(() => Promise.reject(new Error('down with the sickness'))) + } catch (e) { + threw = true + } + expect(threw).toBe(true) + }) + + it('continues to dequeue work after a promise has been rejected', async () => { + try { + await queue.enqueue(() => Promise.reject(new Error('down with the sickness'))) + } catch (e) {} + + const result = await queue.enqueue(() => Promise.resolve(42)) + expect(result).toBe(42) + }) + + it('queues up work', async () => { + let resolve = null + queue.enqueue(() => { + return new Promise((resolve_, reject) => { + resolve = resolve_ + }) + }) + + expect(queue.getQueueDepth()).toBe(0) + + queue.enqueue(() => new Promise((resolve, reject) => {})) + + expect(queue.getQueueDepth()).toBe(1) + resolve() + + waitsFor(() => queue.getQueueDepth() === 0) + }) + }) +}) diff --git a/spec/selection-spec.coffee b/spec/selection-spec.coffee index 319e2d438..18095d6f8 100644 --- a/spec/selection-spec.coffee +++ b/spec/selection-spec.coffee @@ -91,3 +91,13 @@ describe "Selection", -> expect(buffer.lineForRow(0)).toBe " " expect(buffer.lineForRow(1)).toBe " " expect(buffer.lineForRow(2)).toBe "" + + it "auto-indents if only a newline is inserted", -> + selection.setBufferRange [[2, 0], [3, 0]] + selection.insertText("\n", autoIndent: true) + expect(buffer.lineForRow(2)).toBe " " + + it "auto-indents if only a carriage return + newline is inserted", -> + selection.setBufferRange [[2, 0], [3, 0]] + selection.insertText("\r\n", autoIndent: true) + expect(buffer.lineForRow(2)).toBe " " diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 758a232aa..1194f2f76 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -112,14 +112,14 @@ afterEach -> document.getElementById('jasmine-content').innerHTML = '' unless window.debugContent - ensureNoPathSubscriptions() + warnIfLeakingPathSubscriptions() waits(0) # yield to ui thread to make screen update more frequently -ensureNoPathSubscriptions = -> +warnIfLeakingPathSubscriptions = -> watchedPaths = pathwatcher.getWatchedPaths() - pathwatcher.closeAllWatchers() if watchedPaths.length > 0 - throw new Error("Leaking subscriptions for paths: " + watchedPaths.join(", ")) + console.error("WARNING: Leaking subscriptions for paths: " + watchedPaths.join(", ")) + pathwatcher.closeAllWatchers() ensureNoDeprecatedFunctionsCalled = -> deprecations = Grim.getDeprecations() diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 1d1e4eb9f..b031cba33 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -3745,6 +3745,21 @@ describe('TextEditorComponent', function () { return event } + function buildKeydownEvent ({keyCode, target}) { + let event = new KeyboardEvent('keydown') + Object.defineProperty(event, 'keyCode', { + get: function () { + return keyCode + } + }) + Object.defineProperty(event, 'target', { + get: function () { + return target + } + }) + return event + } + let inputNode beforeEach(function () { @@ -3767,11 +3782,12 @@ describe('TextEditorComponent', function () { expect(editor.lineTextForBufferRow(0)).toBe('xyvar quicksort = function () {') }) - it('replaces the last character if the length of the input\'s value does not increase, as occurs with the accented character menu', async function () { - componentNode.dispatchEvent(buildTextInputEvent({ - data: 'u', - target: inputNode - })) + it('replaces the last character if a keypress event is bracketed by keydown events with matching keyCodes, which occurs when the accented character menu is shown', async function () { + componentNode.dispatchEvent(buildKeydownEvent({keyCode: 85, target: inputNode})) + componentNode.dispatchEvent(buildTextInputEvent({data: 'u', target: inputNode})) + componentNode.dispatchEvent(new KeyboardEvent('keypress')) + componentNode.dispatchEvent(buildKeydownEvent({keyCode: 85, target: inputNode})) + componentNode.dispatchEvent(new KeyboardEvent('keyup')) await nextViewUpdatePromise() expect(editor.lineTextForBufferRow(0)).toBe('uvar quicksort = function () {') diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 9d2a2a58c..4de7168b7 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -4561,6 +4561,136 @@ describe "TextEditor", -> expect(cursor1.getBufferPosition()).toEqual [0, 0] expect(cursor3.getBufferPosition()).toEqual [1, 2] + describe ".moveSelectionLeft()", -> + it "moves one active selection on one line one column to the left", -> + editor.setSelectedBufferRange [[0, 4], [0, 13]] + expect(editor.getSelectedText()).toBe 'quicksort' + + editor.moveSelectionLeft() + + expect(editor.getSelectedText()).toBe 'quicksort' + expect(editor.getSelectedBufferRange()).toEqual [[0, 3], [0, 12]] + + it "moves multiple active selections on one line one column to the left", -> + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) + selections = editor.getSelections() + + expect(selections[0].getText()).toBe 'quicksort' + expect(selections[1].getText()).toBe 'function' + + editor.moveSelectionLeft() + + expect(selections[0].getText()).toBe 'quicksort' + expect(selections[1].getText()).toBe 'function' + expect(editor.getSelectedBufferRanges()).toEqual [[[0, 3], [0, 12]], [[0, 15], [0, 23]]] + + it "moves multiple active selections on multiple lines one column to the left", -> + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) + selections = editor.getSelections() + + expect(selections[0].getText()).toBe 'quicksort' + expect(selections[1].getText()).toBe 'sort' + + editor.moveSelectionLeft() + + expect(selections[0].getText()).toBe 'quicksort' + expect(selections[1].getText()).toBe 'sort' + expect(editor.getSelectedBufferRanges()).toEqual [[[0, 3], [0, 12]], [[1, 5], [1, 9]]] + + describe "when a selection is at the first column of a line", -> + it "does not change the selection", -> + editor.setSelectedBufferRanges([[[0, 0], [0, 3]], [[1, 0], [1, 3]]]) + selections = editor.getSelections() + + expect(selections[0].getText()).toBe 'var' + expect(selections[1].getText()).toBe ' v' + + editor.moveSelectionLeft() + editor.moveSelectionLeft() + + expect(selections[0].getText()).toBe 'var' + expect(selections[1].getText()).toBe ' v' + expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 3]], [[1, 0], [1, 3]]] + + describe "when multiple selections are active on one line", -> + it "does not change the selection", -> + editor.setSelectedBufferRanges([[[0, 0], [0, 3]], [[0, 4], [0, 13]]]) + selections = editor.getSelections() + + expect(selections[0].getText()).toBe 'var' + expect(selections[1].getText()).toBe 'quicksort' + + editor.moveSelectionLeft() + + expect(selections[0].getText()).toBe 'var' + expect(selections[1].getText()).toBe 'quicksort' + expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 3]], [[0, 4], [0, 13]]] + + describe ".moveSelectionRight()", -> + it "moves one active selection on one line one column to the right", -> + editor.setSelectedBufferRange [[0, 4], [0, 13]] + expect(editor.getSelectedText()).toBe 'quicksort' + + editor.moveSelectionRight() + + expect(editor.getSelectedText()).toBe 'quicksort' + expect(editor.getSelectedBufferRange()).toEqual [[0, 5], [0, 14]] + + it "moves multiple active selections on one line one column to the right", -> + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) + selections = editor.getSelections() + + expect(selections[0].getText()).toBe 'quicksort' + expect(selections[1].getText()).toBe 'function' + + editor.moveSelectionRight() + + expect(selections[0].getText()).toBe 'quicksort' + expect(selections[1].getText()).toBe 'function' + expect(editor.getSelectedBufferRanges()).toEqual [[[0, 5], [0, 14]], [[0, 17], [0, 25]]] + + it "moves multiple active selections on multiple lines one column to the right", -> + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) + selections = editor.getSelections() + + expect(selections[0].getText()).toBe 'quicksort' + expect(selections[1].getText()).toBe 'sort' + + editor.moveSelectionRight() + + expect(selections[0].getText()).toBe 'quicksort' + expect(selections[1].getText()).toBe 'sort' + expect(editor.getSelectedBufferRanges()).toEqual [[[0, 5], [0, 14]], [[1, 7], [1, 11]]] + + describe "when a selection is at the last column of a line", -> + it "does not change the selection", -> + editor.setSelectedBufferRanges([[[2, 34], [2, 40]], [[5, 22], [5, 30]]]) + selections = editor.getSelections() + + expect(selections[0].getText()).toBe 'items;' + expect(selections[1].getText()).toBe 'shift();' + + editor.moveSelectionRight() + editor.moveSelectionRight() + + expect(selections[0].getText()).toBe 'items;' + expect(selections[1].getText()).toBe 'shift();' + expect(editor.getSelectedBufferRanges()).toEqual [[[2, 34], [2, 40]], [[5, 22], [5, 30]]] + + describe "when multiple selections are active on one line", -> + it "does not change the selection", -> + editor.setSelectedBufferRanges([[[2, 27], [2, 33]], [[2, 34], [2, 40]]]) + selections = editor.getSelections() + + expect(selections[0].getText()).toBe 'return' + expect(selections[1].getText()).toBe 'items;' + + editor.moveSelectionRight() + + expect(selections[0].getText()).toBe 'return' + expect(selections[1].getText()).toBe 'items;' + expect(editor.getSelectedBufferRanges()).toEqual [[[2, 27], [2, 33]], [[2, 34], [2, 40]]] + describe 'reading text', -> it '.lineTextForScreenRow(row)', -> editor.foldBufferRow(4) diff --git a/spec/workspace-element-spec.coffee b/spec/workspace-element-spec.coffee index cf773b4ef..efb1d1b26 100644 --- a/spec/workspace-element-spec.coffee +++ b/spec/workspace-element-spec.coffee @@ -47,9 +47,14 @@ describe "WorkspaceElement", -> it "updates the font-family based on the 'editor.fontFamily' config value", -> initialCharWidth = editor.getDefaultCharWidth() - expect(getComputedStyle(editorElement).fontFamily).toBe atom.config.get('editor.fontFamily') + fontFamily = atom.config.get('editor.fontFamily') + fontFamily += ", 'Apple Color Emoji'" if process.platform is 'darwin' + expect(getComputedStyle(editorElement).fontFamily).toBe fontFamily + atom.config.set('editor.fontFamily', 'sans-serif') - expect(getComputedStyle(editorElement).fontFamily).toBe atom.config.get('editor.fontFamily') + fontFamily = atom.config.get('editor.fontFamily') + fontFamily += ", 'Apple Color Emoji'" if process.platform is 'darwin' + expect(getComputedStyle(editorElement).fontFamily).toBe fontFamily expect(editor.getDefaultCharWidth()).not.toBe initialCharWidth it "updates the line-height based on the 'editor.lineHeight' config value", -> diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index 3aff9e457..aee02ee8e 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -1,5 +1,5 @@ _ = require 'underscore-plus' -{ipcRenderer, remote, shell, webFrame} = require 'electron' +{screen, ipcRenderer, remote, shell, webFrame} = require 'electron' ipcHelpers = require './ipc-helpers' {Disposable} = require 'event-kit' {getWindowLoadSettings, setWindowLoadSettings} = require './window-load-settings-helpers' @@ -211,6 +211,14 @@ class ApplicationDelegate new Disposable -> ipcRenderer.removeListener('message', outerCallback) + onUpdateError: (callback) -> + outerCallback = (event, message, detail) -> + callback(detail) if message is 'update-error' + + ipcRenderer.on('message', outerCallback) + new Disposable -> + ipcRenderer.removeListener('message', outerCallback) + onApplicationMenuCommand: (callback) -> outerCallback = (event, args...) -> callback(args...) @@ -233,14 +241,28 @@ class ApplicationDelegate openExternal: (url) -> shell.openExternal(url) - disablePinchToZoom: -> - webFrame.setZoomLevelLimits(1, 1) + disableZoom: -> + outerCallback = -> + webFrame.setZoomLevelLimits(1, 1) + + outerCallback() + # Set the limits every time a display is added or removed, otherwise the + # configuration gets reset to the default, which allows zooming the + # webframe. + screen.on('display-added', outerCallback) + screen.on('display-removed', outerCallback) + new Disposable -> + screen.removeListener('display-added', outerCallback) + screen.removeListener('display-removed', outerCallback) checkForUpdate: -> - ipcRenderer.send('check-for-update') + ipcRenderer.send('command', 'application:check-for-update') restartAndInstallUpdate: -> - ipcRenderer.send('install-update') + ipcRenderer.send('command', 'application:install-update') getAutoUpdateManagerState: -> ipcRenderer.sendSync('get-auto-update-manager-state') + + getAutoUpdateManagerErrorMessage: -> + ipcRenderer.sendSync('get-auto-update-manager-error') diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 6ef46dc2a..ffff564ba 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -208,7 +208,7 @@ class AtomEnvironment extends Model @stylesElement = @styles.buildStylesElement() @document.head.appendChild(@stylesElement) - @applicationDelegate.disablePinchToZoom() + @disposables.add(@applicationDelegate.disableZoom()) @keymaps.subscribeToFileReadFailure() @keymaps.loadBundledKeymaps() diff --git a/src/auto-update-manager.js b/src/auto-update-manager.js index 62cc03f85..fb6325a26 100644 --- a/src/auto-update-manager.js +++ b/src/auto-update-manager.js @@ -20,6 +20,9 @@ export default class AutoUpdateManager { }), applicationDelegate.onUpdateNotAvailable(() => { this.emitter.emit('update-not-available') + }), + applicationDelegate.onUpdateError(() => { + this.emitter.emit('update-error') }) ) } @@ -41,6 +44,10 @@ export default class AutoUpdateManager { return this.applicationDelegate.getAutoUpdateManagerState() } + getErrorMessage () { + return this.applicationDelegate.getAutoUpdateManagerErrorMessage() + } + platformSupportsUpdates () { return atom.getReleaseChannel() !== 'dev' && this.getState() !== 'unsupported' } @@ -67,6 +74,10 @@ export default class AutoUpdateManager { return this.emitter.on('update-not-available', callback) } + onUpdateError (callback) { + return this.emitter.on('update-error', callback) + } + getPlatform () { return process.platform } diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index 4767c9065..69eff1845 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -3,6 +3,7 @@ ApplicationMenu = require './application-menu' AtomProtocolHandler = require './atom-protocol-handler' AutoUpdateManager = require './auto-update-manager' StorageFolder = require '../storage-folder' +Config = require '../config' ipcHelpers = require '../ipc-helpers' {BrowserWindow, Menu, app, dialog, ipcMain, shell} = require 'electron' fs = require 'fs-plus' @@ -70,7 +71,11 @@ class AtomApplication @pidsToOpenWindows = {} @windows = [] - @autoUpdateManager = new AutoUpdateManager(@version, options.test, @resourcePath) + @config = new Config({configDirPath: process.env.ATOM_HOME, @resourcePath, enablePersistence: true}) + @config.setSchema null, {type: 'object', properties: _.clone(require('../config-schema'))} + @config.load() + + @autoUpdateManager = new AutoUpdateManager(@version, options.test, @resourcePath, @config) @applicationMenu = new ApplicationMenu(@version, @autoUpdateManager) @atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode) @@ -305,12 +310,12 @@ 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 'get-auto-update-manager-error', (event) => + event.returnValue = @autoUpdateManager.getErrorMessage() + ipcMain.on 'execute-javascript-in-dev-tools', (event, code) -> event.sender.devToolsWebContents?.executeJavaScript(code) @@ -461,6 +466,7 @@ class AtomApplication openedWindow.restore() else openedWindow.focus() + openedWindow.replaceEnvironment(env) else if devMode try @@ -510,7 +516,8 @@ class AtomApplication @storageFolder.storeSync('application.json', states) loadState: (options) -> - if (states = @storageFolder.load('application.json'))?.length > 0 + restorePreviousState = @config.get('core.restorePreviousWindowsOnStart') ? true + if restorePreviousState and (states = @storageFolder.load('application.json'))?.length > 0 for state in states @openWithOptions(_.extend(options, { initialPaths: state.initialPaths diff --git a/src/browser/atom-window.coffee b/src/browser/atom-window.coffee index 33c64da7d..60e6d0553 100644 --- a/src/browser/atom-window.coffee +++ b/src/browser/atom-window.coffee @@ -68,6 +68,7 @@ class AtomWindow @loaded = true @setLoadSettings(loadSettings) + @env = loadSettings.env if loadSettings.env? @browserWindow.focusOnWebView() if @isSpec @browserWindow.temporaryState = {windowDimensions} if windowDimensions? @@ -169,6 +170,9 @@ class AtomWindow else @browserWindow.once 'window:loaded', => @openLocations(locationsToOpen) + replaceEnvironment: (env) -> + @browserWindow.webContents.send 'environment', env + sendMessage: (message, detail) -> @browserWindow.webContents.send 'message', message, detail diff --git a/src/browser/auto-update-manager.coffee b/src/browser/auto-update-manager.coffee index c8c57cb01..602284b07 100644 --- a/src/browser/auto-update-manager.coffee +++ b/src/browser/auto-update-manager.coffee @@ -1,6 +1,5 @@ autoUpdater = null _ = require 'underscore-plus' -Config = require '../config' {EventEmitter} = require 'events' path = require 'path' @@ -16,13 +15,10 @@ module.exports = class AutoUpdateManager _.extend @prototype, EventEmitter.prototype - constructor: (@version, @testMode, resourcePath) -> + constructor: (@version, @testMode, resourcePath, @config) -> @state = IdleState @iconPath = path.resolve(__dirname, '..', '..', 'resources', 'atom.png') @feedUrl = "https://atom.io/api/updates?version=#{@version}" - @config = new Config({configDirPath: process.env.ATOM_HOME, resourcePath, enablePersistence: true}) - @config.setSchema null, {type: 'object', properties: _.clone(require('../config-schema'))} - @config.load() process.nextTick => @setupAutoUpdater() setupAutoUpdater: -> @@ -32,7 +28,8 @@ class AutoUpdateManager {autoUpdater} = require 'electron' autoUpdater.on 'error', (event, message) => - @setState(ErrorState) + @setState(ErrorState, message) + @emitWindowEvent('update-error') console.error "Error Downloading Update: #{message}" autoUpdater.setFeedURL @feedUrl @@ -82,14 +79,18 @@ class AutoUpdateManager atomWindow.sendMessage(eventName, payload) return - setState: (state) -> + setState: (state, errorMessage) -> return if @state is state @state = state + @errorMessage = errorMessage @emit 'state-changed', @state getState: -> @state + getErrorMessage: -> + @errorMessage + scheduleUpdateCheck: -> # Only schedule update check periodically if running in release version and # and there is no existing scheduled update check. diff --git a/src/buffered-node-process.coffee b/src/buffered-node-process.coffee index 3b4916b24..406775277 100644 --- a/src/buffered-node-process.coffee +++ b/src/buffered-node-process.coffee @@ -47,6 +47,7 @@ class BufferedNodeProcess extends BufferedProcess options ?= {} options.env ?= Object.create(process.env) options.env['ELECTRON_RUN_AS_NODE'] = 1 + options.env['ELECTRON_NO_ATTACH_CONSOLE'] = 1 args = args?.slice() ? [] args.unshift(command) diff --git a/src/buffered-process.coffee b/src/buffered-process.coffee index 53934c02d..59f2a7e9a 100644 --- a/src/buffered-process.coffee +++ b/src/buffered-process.coffee @@ -67,7 +67,7 @@ class BufferedProcess cmdArgs.unshift("\"#{command}\"") else cmdArgs.unshift(command) - cmdArgs = ['/s', '/c', "\"#{cmdArgs.join(' ')}\""] + cmdArgs = ['/s', '/d', '/c', "\"#{cmdArgs.join(' ')}\""] cmdOptions = _.clone(options) cmdOptions.windowsVerbatimArguments = true @spawn(@getCmdPath(), cmdArgs, cmdOptions) diff --git a/src/environment-helpers.js b/src/environment-helpers.js index 00c112bda..2d1bd5b60 100644 --- a/src/environment-helpers.js +++ b/src/environment-helpers.js @@ -58,7 +58,7 @@ function getFromShell () { 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')) { + if (shell.endsWith('csh') || shell.endsWith('tcsh') || shell.endsWith('fish')) { return false } return true @@ -67,9 +67,20 @@ function needsPatching (options = { platform: process.platform, env: process.env return false } +// Fix for #11302 because `process.env` on Windows is a magic object that offers case-insensitive +// environment variable matching. By always cloning to `process.env` we prevent breaking the +// underlying functionality. +function clone (to, from) { + for (var key in to) { + delete to[key] + } + + Object.assign(to, from) +} + function normalize (options = {}) { if (options && options.env) { - process.env = options.env + clone(process.env, options.env) } if (!options.env) { @@ -85,10 +96,18 @@ function normalize (options = {}) { // in #4126. Retain the original in case someone needs it. let shellEnv = getFromShell() if (shellEnv && shellEnv.PATH) { - process._originalEnv = process.env - process.env = shellEnv + process._originalEnv = Object.assign({}, process.env) + clone(process.env, shellEnv) } } } -export default { getFromShell, needsPatching, normalize } +function replace (env) { + if (!env || !env.PATH) { + return + } + + clone(process.env, env) +} + +export default { getFromShell, needsPatching, normalize, replace } diff --git a/src/git-repository-async.js b/src/git-repository-async.js index 17d293c14..aacd482f7 100644 --- a/src/git-repository-async.js +++ b/src/git-repository-async.js @@ -3,6 +3,7 @@ import fs from 'fs-plus' import path from 'path' import Git from 'nodegit' +import ResourcePool from './resource-pool' import {Emitter, CompositeDisposable, Disposable} from 'event-kit' const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.WT_TYPECHANGE | Git.Status.STATUS.INDEX_TYPECHANGE @@ -38,7 +39,8 @@ export default class GitRepositoryAsync { } constructor (_path, options = {}) { - Git.enableThreadSafety() + // We'll serialize our access manually. + Git.setThreadSafetyStatus(Git.THREAD_SAFETY.DISABLED) this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() @@ -50,6 +52,11 @@ export default class GitRepositoryAsync { this._openExactPath = options.openExactPath || false this.repoPromise = this.openRepository() + // NB: We don't currently _use_ the pooled object. But by giving it one + // thing, we're really just serializing all the work. Down the road, we + // could open multiple connections to the repository. + this.repoPool = new ResourcePool([this.repoPromise]) + this.isCaseInsensitive = fs.isCaseInsensitive() this.upstream = {} this.submodules = {} @@ -81,6 +88,7 @@ export default class GitRepositoryAsync { this.emitter.dispose() this.emitter = null } + if (this.subscriptions) { this.subscriptions.dispose() this.subscriptions = null @@ -260,10 +268,12 @@ export default class GitRepositoryAsync { // Public: Returns a {Promise} which resolves to whether the given branch // exists. hasBranch (branch) { - return this.getRepo() - .then(repo => repo.getBranch(branch)) - .then(branch => branch != null) - .catch(_ => false) + return this.repoPool.enqueue(() => { + return this.getRepo() + .then(repo => repo.getBranch(branch)) + .then(branch => branch != null) + .catch(_ => false) + }) } // Public: Retrieves a shortened version of the HEAD reference value. @@ -277,9 +287,11 @@ export default class GitRepositoryAsync { // // Returns a {Promise} which resolves to a {String}. getShortHead (_path) { - return this.getRepo(_path) - .then(repo => repo.getCurrentBranch()) - .then(branch => branch.shorthand()) + return this.repoPool.enqueue(() => { + return this.getRepo(_path) + .then(repo => repo.getCurrentBranch()) + .then(branch => branch.shorthand()) + }) } // Public: Is the given path a submodule in the repository? @@ -289,14 +301,18 @@ export default class GitRepositoryAsync { // Returns a {Promise} that resolves true if the given path is a submodule in // the repository. isSubmodule (_path) { - return this.getRepo() - .then(repo => repo.openIndex()) - .then(index => Promise.all([index, this.relativizeToWorkingDirectory(_path)])) - .then(([index, relativePath]) => { - const entry = index.getByPath(relativePath) - if (!entry) return false + return this.relativizeToWorkingDirectory(_path) + .then(relativePath => { + return this.repoPool.enqueue(() => { + return this.getRepo() + .then(repo => repo.index()) + .then(index => { + const entry = index.getByPath(relativePath) + if (!entry) return false - return entry.mode === submoduleMode + return entry.mode === submoduleMode + }) + }) }) } @@ -311,16 +327,18 @@ export default class GitRepositoryAsync { // * `ahead` The {Number} of commits ahead. // * `behind` The {Number} of commits behind. getAheadBehindCount (reference, _path) { - return this.getRepo(_path) - .then(repo => Promise.all([repo, repo.getBranch(reference)])) - .then(([repo, local]) => { - const upstream = Git.Branch.upstream(local) - return Promise.all([repo, local, upstream]) - }) - .then(([repo, local, upstream]) => { - return Git.Graph.aheadBehind(repo, local.target(), upstream.target()) - }) - .catch(_ => ({ahead: 0, behind: 0})) + return this.repoPool.enqueue(() => { + return this.getRepo(_path) + .then(repo => Promise.all([repo, repo.getBranch(reference)])) + .then(([repo, local]) => { + const upstream = Git.Branch.upstream(local) + return Promise.all([repo, local, upstream]) + }) + .then(([repo, local, upstream]) => { + return Git.Graph.aheadBehind(repo, local.target(), upstream.target()) + }) + .catch(_ => ({ahead: 0, behind: 0})) + }) } // Public: Get the cached ahead/behind commit counts for the current branch's @@ -352,10 +370,12 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to the {String} git configuration value // specified by the key. getConfigValue (key, _path) { - return this.getRepo(_path) - .then(repo => repo.configSnapshot()) - .then(config => config.getStringBuf(key)) - .catch(_ => null) + return this.repoPool.enqueue(() => { + return this.getRepo(_path) + .then(repo => repo.configSnapshot()) + .then(config => config.getStringBuf(key)) + .catch(_ => null) + }) } // Public: Get the URL for the 'origin' remote. @@ -378,9 +398,11 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {String} branch name such as // `refs/remotes/origin/master`. getUpstreamBranch (_path) { - return this.getRepo(_path) - .then(repo => repo.getCurrentBranch()) - .then(branch => Git.Branch.upstream(branch)) + return this.repoPool.enqueue(() => { + return this.getRepo(_path) + .then(repo => repo.getCurrentBranch()) + .then(branch => Git.Branch.upstream(branch)) + }) } // Public: Gets all the local and remote references. @@ -393,23 +415,25 @@ export default class GitRepositoryAsync { // * `remotes` An {Array} of remote reference names. // * `tags` An {Array} of tag reference names. getReferences (_path) { - return this.getRepo(_path) - .then(repo => repo.getReferences(Git.Reference.TYPE.LISTALL)) - .then(refs => { - const heads = [] - const remotes = [] - const tags = [] - for (const ref of refs) { - if (ref.isTag()) { - tags.push(ref.name()) - } else if (ref.isRemote()) { - remotes.push(ref.name()) - } else if (ref.isBranch()) { - heads.push(ref.name()) + return this.repoPool.enqueue(() => { + return this.getRepo(_path) + .then(repo => repo.getReferences(Git.Reference.TYPE.LISTALL)) + .then(refs => { + const heads = [] + const remotes = [] + const tags = [] + for (const ref of refs) { + if (ref.isTag()) { + tags.push(ref.name()) + } else if (ref.isRemote()) { + remotes.push(ref.name()) + } else if (ref.isBranch()) { + heads.push(ref.name()) + } } - } - return {heads, remotes, tags} - }) + return {heads, remotes, tags} + }) + }) } // Public: Get the SHA for the given reference. @@ -421,9 +445,11 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to the current {String} SHA for the // given reference. getReferenceTarget (reference, _path) { - return this.getRepo(_path) - .then(repo => Git.Reference.nameToId(repo, reference)) - .then(oid => oid.tostrS()) + return this.repoPool.enqueue(() => { + return this.getRepo(_path) + .then(repo => Git.Reference.nameToId(repo, reference)) + .then(oid => oid.tostrS()) + }) } // Reading Status @@ -460,12 +486,17 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {Boolean} that's true if the `path` // is ignored. isPathIgnored (_path) { - return Promise.all([this.getRepo(), this.getWorkingDirectory()]) - .then(([repo, wd]) => { - const relativePath = this.relativize(_path, wd) - return Git.Ignore.pathIsIgnored(repo, relativePath) + return this.getWorkingDirectory() + .then(wd => { + return this.repoPool.enqueue(() => { + return this.getRepo() + .then(repo => { + const relativePath = this.relativize(_path, wd) + return Git.Ignore.pathIsIgnored(repo, relativePath) + }) + .then(ignored => Boolean(ignored)) + }) }) - .then(ignored => Boolean(ignored)) } // Get the status of a directory in the repository's working directory. @@ -501,8 +532,8 @@ export default class GitRepositoryAsync { // status bit for the path. refreshStatusForPath (_path) { let relativePath - return Promise.all([this.getRepo(), this.getWorkingDirectory()]) - .then(([repo, wd]) => { + return this.getWorkingDirectory() + .then(wd => { relativePath = this.relativize(_path, wd) return this._getStatus([relativePath]) }) @@ -609,34 +640,39 @@ export default class GitRepositoryAsync { // * `added` The {Number} of added lines. // * `deleted` The {Number} of deleted lines. getDiffStats (_path) { - return this.getRepo(_path) - .then(repo => Promise.all([repo, repo.getHeadCommit()])) - .then(([repo, headCommit]) => Promise.all([repo, headCommit.getTree(), this.getWorkingDirectory(_path)])) - .then(([repo, tree, wd]) => { - const options = new Git.DiffOptions() - 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)) - .then(lines => { - const stats = {added: 0, deleted: 0} - for (const line of lines) { - const origin = line.origin() - if (origin === Git.Diff.LINE.ADDITION) { - stats.added++ - } else if (origin === Git.Diff.LINE.DELETION) { - stats.deleted++ - } - } - return stats + return this.getWorkingDirectory(_path) + .then(wd => { + return this.repoPool.enqueue(() => { + return this.getRepo(_path) + .then(repo => Promise.all([repo, repo.getHeadCommit()])) + .then(([repo, headCommit]) => Promise.all([repo, headCommit.getTree()])) + .then(([repo, tree]) => { + const options = new Git.DiffOptions() + options.contextLines = 0 + options.flags = Git.Diff.OPTION.DISABLE_PATHSPEC_MATCH + options.pathspec = this.relativize(_path, 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)) + .then(lines => { + const stats = {added: 0, deleted: 0} + for (const line of lines) { + const origin = line.origin() + if (origin === Git.Diff.LINE.ADDITION) { + stats.added++ + } else if (origin === Git.Diff.LINE.DELETION) { + stats.deleted++ + } + } + return stats + }) + }) }) } @@ -652,24 +688,29 @@ export default class GitRepositoryAsync { // * `oldLines` The {Number} of lines in the old hunk. // * `newLines` The {Number} of lines in the new hunk getLineDiffs (_path, text) { - let relativePath = null - 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)) - .then(entry => entry.getBlob()) - .then(blob => { - const options = new Git.DiffOptions() - options.contextLines = 0 - 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 this._diffBlobToBuffer(blob, text, options) + return this.getWorkingDirectory(_path) + .then(wd => { + let relativePath = null + return this.repoPool.enqueue(() => { + return this.getRepo(_path) + .then(repo => { + relativePath = this.relativize(_path, wd) + return repo.getHeadCommit() + }) + .then(commit => commit.getEntry(relativePath)) + .then(entry => entry.getBlob()) + .then(blob => { + const options = new Git.DiffOptions() + options.contextLines = 0 + 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 this._diffBlobToBuffer(blob, text, options) + }) + }) }) } @@ -691,12 +732,17 @@ export default class GitRepositoryAsync { // Returns a {Promise} that resolves or rejects depending on whether the // method was successful. checkoutHead (_path) { - return Promise.all([this.getRepo(_path), this.getWorkingDirectory(_path)]) - .then(([repo, wd]) => { - const checkoutOptions = new Git.CheckoutOptions() - checkoutOptions.paths = [this.relativize(_path, wd)] - checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH - return Git.Checkout.head(repo, checkoutOptions) + return this.getWorkingDirectory(_path) + .then(wd => { + return this.repoPool.enqueue(() => { + return this.getRepo(_path) + .then(repo => { + const checkoutOptions = new Git.CheckoutOptions() + checkoutOptions.paths = [this.relativize(_path, wd)] + checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH + return Git.Checkout.head(repo, checkoutOptions) + }) + }) }) .then(() => this.refreshStatusForPath(_path)) } @@ -709,17 +755,19 @@ export default class GitRepositoryAsync { // // Returns a {Promise} that resolves if the method was successful. checkoutReference (reference, create) { - return this.getRepo() - .then(repo => repo.checkoutBranch(reference)) - .catch(error => { - if (create) { - return this._createBranch(reference) - .then(_ => this.checkoutReference(reference, false)) - } else { - throw error - } - }) - .then(_ => null) + return this.repoPool.enqueue(() => { + return this.getRepo() + .then(repo => repo.checkoutBranch(reference)) + }) + .catch(error => { + if (create) { + return this._createBranch(reference) + .then(_ => this.checkoutReference(reference, false)) + } else { + throw error + } + }) + .then(_ => null) } // Private @@ -745,9 +793,11 @@ export default class GitRepositoryAsync { // Returns a {Promise} which resolves to a {NodeGit.Ref} reference to the // created branch. _createBranch (name) { - return this.getRepo() - .then(repo => Promise.all([repo, repo.getHeadCommit()])) - .then(([repo, commit]) => repo.createBranch(name, commit)) + return this.repoPool.enqueue(() => { + return this.getRepo() + .then(repo => Promise.all([repo, repo.getHeadCommit()])) + .then(([repo, commit]) => repo.createBranch(name, commit)) + }) } // Get all the hunks in the diff. @@ -804,14 +854,16 @@ export default class GitRepositoryAsync { // 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 => { - const changed = branchName !== this.branch - this.branch = branchName - return changed - }) + return this.repoPool.enqueue(() => { + return this.getRepo() + .then(repo => repo.getCurrentBranch()) + .then(ref => ref.name()) + .then(branchName => { + const changed = branchName !== this.branch + this.branch = branchName + return changed + }) + }) } // Refresh the cached ahead/behind count with the given branch. @@ -1077,18 +1129,20 @@ export default class GitRepositoryAsync { // // Returns a {Promise} which resolves to an {Array} of {NodeGit.StatusFile} // statuses for the paths. - _getStatus (paths, repo) { - return this.getRepo() - .then(repo => { - const opts = { - flags: Git.Status.OPT.INCLUDE_UNTRACKED | Git.Status.OPT.RECURSE_UNTRACKED_DIRS - } + _getStatus (paths) { + return this.repoPool.enqueue(() => { + return this.getRepo() + .then(repo => { + const opts = { + flags: Git.Status.OPT.INCLUDE_UNTRACKED | Git.Status.OPT.RECURSE_UNTRACKED_DIRS + } - if (paths) { - opts.pathspec = paths - } + if (paths) { + opts.pathspec = paths + } - return repo.getStatusExt(opts) - }) + return repo.getStatusExt(opts) + }) + }) } } diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 30d99791d..a04124b78 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -166,7 +166,12 @@ class GitRepository # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeStatuses: (callback) -> - @async.onDidChangeStatuses callback + @async.onDidChangeStatuses -> + # Defer the callback to the next tick so that we've reset + # `@statusesByPath` by the time it's called. Otherwise reads from within + # the callback could be inconsistent. + # See https://github.com/atom/atom/issues/11396 + process.nextTick callback ### Section: Repository Details diff --git a/src/initialize-application-window.coffee b/src/initialize-application-window.coffee index ea811f515..463bdd48e 100644 --- a/src/initialize-application-window.coffee +++ b/src/initialize-application-window.coffee @@ -4,11 +4,12 @@ module.exports = ({blobStore}) -> path = require 'path' require './window' {getWindowLoadSettings} = require './window-load-settings-helpers' - + {ipcRenderer} = require 'electron' {resourcePath, isSpec, devMode, env} = getWindowLoadSettings() # Set baseline environment environmentHelpers.normalize({env: env}) + env = process.env # Add application-specific exports to module search path. exportsPath = path.join(resourcePath, 'exports') @@ -25,7 +26,7 @@ module.exports = ({blobStore}) -> applicationDelegate: new ApplicationDelegate, configDirPath: process.env.ATOM_HOME enablePersistence: true - env: env + env: process.env }) atom.startEditorWindow().then -> @@ -35,3 +36,6 @@ module.exports = ({blobStore}) -> window.removeEventListener('focus', windowFocused) setTimeout (-> document.querySelector('atom-workspace').focus()), 0 window.addEventListener('focus', windowFocused) + ipcRenderer.on('environment', (event, env) -> + environmentHelpers.replace(env) + ) diff --git a/src/package-manager.coffee b/src/package-manager.coffee index 0e76a762f..8f2924358 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -367,6 +367,8 @@ class PackageManager @emitter.emit 'did-load-initial-packages' loadPackage: (nameOrPath) -> + return null if path.basename(nameOrPath)[0].match /^\./ # primarily to skip .git folder + return pack if pack = @getLoadedPackage(nameOrPath) if packagePath = @resolvePackagePath(nameOrPath) diff --git a/src/pane.coffee b/src/pane.coffee index 3ff62993c..e8858a2b9 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -442,15 +442,16 @@ class Pane extends Model if typeof item.onDidTerminatePendingState is "function" itemSubscriptions.add item.onDidTerminatePendingState => @clearPendingItem() if @getPendingItem() is item - itemSubscriptions.add item.onDidDestroy => @removeItem(item, false) @subscriptionsPerItem.set item, itemSubscriptions @items.splice(index, 0, item) lastPendingItem = @getPendingItem() + replacingPendingItem = lastPendingItem? and not moved + @pendingItem = null if replacingPendingItem @setPendingItem(item) if pending @emitter.emit 'did-add-item', {item, index, moved} - @destroyItem(lastPendingItem) if lastPendingItem? and not moved + @destroyItem(lastPendingItem) if replacingPendingItem @setActiveItem(item) unless @getActiveItem()? item @@ -458,7 +459,8 @@ class Pane extends Model if @pendingItem isnt item mostRecentPendingItem = @pendingItem @pendingItem = item - @emitter.emit 'item-did-terminate-pending-state', mostRecentPendingItem + if mostRecentPendingItem? + @emitter.emit 'item-did-terminate-pending-state', mostRecentPendingItem getPendingItem: => @pendingItem or null diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index dacb1d228..bb3630117 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -207,6 +207,8 @@ module.exports = ({commandRegistry, commandInstaller, config}) -> 'editor:checkout-head-revision': -> @checkoutHeadRevision() 'editor:move-line-up': -> @moveLineUp() 'editor:move-line-down': -> @moveLineDown() + 'editor:move-selection-left': -> @moveSelectionLeft() + 'editor:move-selection-right': -> @moveSelectionRight() 'editor:duplicate-lines': -> @duplicateLines() 'editor:join-lines': -> @joinLines() ) diff --git a/src/resource-pool.js b/src/resource-pool.js new file mode 100644 index 000000000..ae7cb71d0 --- /dev/null +++ b/src/resource-pool.js @@ -0,0 +1,57 @@ +/** @babel */ + +// Manages a pool of some resource. +export default class ResourcePool { + constructor (pool) { + this.pool = pool + + this.queue = [] + } + + // Enqueue the given function. The function will be given an object from the + // pool. The function must return a {Promise}. + enqueue (fn) { + let resolve = null + let reject = null + const wrapperPromise = new Promise((resolve_, reject_) => { + resolve = resolve_ + reject = reject_ + }) + + this.queue.push(this.wrapFunction(fn, resolve, reject)) + + this.dequeueIfAble() + + return wrapperPromise + } + + wrapFunction (fn, resolve, reject) { + return (resource) => { + const promise = fn(resource) + promise + .then(result => { + resolve(result) + this.taskDidComplete(resource) + }, error => { + reject(error) + this.taskDidComplete(resource) + }) + } + } + + taskDidComplete (resource) { + this.pool.push(resource) + + this.dequeueIfAble() + } + + dequeueIfAble () { + if (!this.pool.length || !this.queue.length) return + + const fn = this.queue.shift() + const resource = this.pool.shift() + fn(resource) + } + + getQueueDepth () { return this.queue.length } +} diff --git a/src/selection.coffee b/src/selection.coffee index e208ea55a..2937baaee 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -378,7 +378,8 @@ class Selection extends Model indentAdjustment = @editor.indentLevelForLine(precedingText) - options.indentBasis @adjustIndent(remainingLines, indentAdjustment) - if options.autoIndent and NonWhitespaceRegExp.test(text) and not NonWhitespaceRegExp.test(precedingText) and remainingLines.length > 0 + textIsAutoIndentable = text is '\n' or text is '\r\n' or NonWhitespaceRegExp.test(text) + if options.autoIndent and textIsAutoIndentable and not NonWhitespaceRegExp.test(precedingText) and remainingLines.length > 0 autoIndentFirstLine = true firstLine = precedingText + firstInsertedLine desiredIndentLevel = @editor.languageMode.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 9b091100d..488b74d09 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -247,9 +247,50 @@ class TextEditorComponent @scrollViewNode.addEventListener 'mousedown', @onMouseDown @scrollViewNode.addEventListener 'scroll', @onScrollViewScroll + @detectAccentedCharacterMenu() @listenForIMEEvents() @trackSelectionClipboard() if process.platform is 'linux' + detectAccentedCharacterMenu: -> + # We need to get clever to detect when the accented character menu is + # opened on OS X. Usually, every keydown event that could cause input is + # followed by a corresponding keypress. However, pressing and holding + # long enough to open the accented character menu causes additional keydown + # events to fire that aren't followed by their own keypress and textInput + # events. + # + # Therefore, we assume the accented character menu has been deployed if, + # before observing any keyup event, we observe events in the following + # sequence: + # + # keydown(keyCode: X), keypress, keydown(keyCode: X) + # + # The keyCode X must be the same in the keydown events that bracket the + # keypress, meaning we're *holding* the _same_ key we intially pressed. + # Got that? + lastKeydown = null + lastKeydownBeforeKeypress = null + + @domNode.addEventListener 'keydown', (event) => + if lastKeydownBeforeKeypress + if lastKeydownBeforeKeypress.keyCode is event.keyCode + @openedAccentedCharacterMenu = true + lastKeydownBeforeKeypress = null + else + lastKeydown = event + + @domNode.addEventListener 'keypress', => + lastKeydownBeforeKeypress = lastKeydown + lastKeydown = null + + # This cancels the accented character behavior if we type a key normally + # with the menu open. + @openedAccentedCharacterMenu = false + + @domNode.addEventListener 'keyup', -> + lastKeydownBeforeKeypress = null + lastKeydown = null + listenForIMEEvents: -> # The IME composition events work like this: # @@ -266,6 +307,9 @@ class TextEditorComponent checkpoint = null @domNode.addEventListener 'compositionstart', => + if @openedAccentedCharacterMenu + @editor.selectLeft() + @openedAccentedCharacterMenu = false checkpoint = @editor.createCheckpoint() @domNode.addEventListener 'compositionupdate', (event) => @editor.insertText(event.data, select: true) @@ -321,24 +365,21 @@ class TextEditorComponent onTextInput: (event) => event.stopPropagation() - - # If we prevent the insertion of a space character, then the browser - # interprets the spacebar keypress as a page-down command. - event.preventDefault() unless event.data is ' ' + event.preventDefault() return unless @isInputEnabled() - inputNode = event.target + # Workaround of the accented character suggestion feature in OS X. + # This will only occur when the user is not composing in IME mode. + # When the user selects a modified character from the OSX menu, `textInput` + # will occur twice, once for the initial character, and once for the + # modified character. However, only a single keypress will have fired. If + # this is the case, select backward to replace the original character. + if @openedAccentedCharacterMenu + @editor.selectLeft() + @openedAccentedCharacterMenu = false - # Work around of the accented character suggestion feature in OS X. - # Text input fires before a character is inserted, and if the browser is - # replacing the previous un-accented character with an accented variant, it - # will select backward over it. - selectedLength = inputNode.selectionEnd - inputNode.selectionStart - @editor.selectLeft() if selectedLength is 1 - - insertedRange = @editor.insertText(event.data, groupUndo: true) - inputNode.value = event.data if insertedRange + @editor.insertText(event.data, groupUndo: true) onVerticalScroll: (scrollTop) => return if @updateRequested or scrollTop is @presenter.getScrollTop() diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 7d8d955e9..0d1b3795e 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1074,6 +1074,50 @@ class TextEditor extends Model @autoIndentSelectedRows() if @shouldAutoIndent() @scrollToBufferPosition([newSelectionRanges[0].start.row - 1, 0]) + # Move any active selections one column to the left. + moveSelectionLeft: -> + selections = @getSelectedBufferRanges() + noSelectionAtStartOfLine = selections.every((selection) -> + selection.start.column isnt 0 + ) + + translationDelta = [0, -1] + translatedRanges = [] + + if noSelectionAtStartOfLine + @transact => + for selection in selections + charToLeftOfSelection = new Range(selection.start.translate(translationDelta), selection.start) + charTextToLeftOfSelection = @buffer.getTextInRange(charToLeftOfSelection) + + @buffer.insert(selection.end, charTextToLeftOfSelection) + @buffer.delete(charToLeftOfSelection) + translatedRanges.push(selection.translate(translationDelta)) + + @setSelectedBufferRanges(translatedRanges) + + # Move any active selections one column to the right. + moveSelectionRight: -> + selections = @getSelectedBufferRanges() + noSelectionAtEndOfLine = selections.every((selection) => + selection.end.column isnt @buffer.lineLengthForRow(selection.end.row) + ) + + translationDelta = [0, 1] + translatedRanges = [] + + if noSelectionAtEndOfLine + @transact => + for selection in selections + charToRightOfSelection = new Range(selection.end, selection.end.translate(translationDelta)) + charTextToRightOfSelection = @buffer.getTextInRange(charToRightOfSelection) + + @buffer.delete(charToRightOfSelection) + @buffer.insert(selection.start, charTextToRightOfSelection) + translatedRanges.push(selection.translate(translationDelta)) + + @setSelectedBufferRanges(translatedRanges) + # Duplicate the most recent cursor's current line. duplicateLines: -> @transact => @@ -1507,7 +1551,7 @@ class TextEditor extends Model # # * `markerLayer` A {TextEditorMarkerLayer} or {MarkerLayer} to decorate. # * `decorationParams` The same parameters that are passed to - # {decorateMarker}, except the `type` cannot be `overlay` or `gutter`. + # {TextEditor::decorateMarker}, except the `type` cannot be `overlay` or `gutter`. # # This API is experimental and subject to change on any release. # diff --git a/src/workspace-element.coffee b/src/workspace-element.coffee index 8f6dca48a..02ff2e5b4 100644 --- a/src/workspace-element.coffee +++ b/src/workspace-element.coffee @@ -44,10 +44,15 @@ class WorkspaceElement extends HTMLElement @subscriptions.add @config.onDidChange 'editor.lineHeight', @updateGlobalTextEditorStyleSheet.bind(this) updateGlobalTextEditorStyleSheet: -> + fontFamily = @config.get('editor.fontFamily') + # TODO: There is a bug in how some emojis (e.g. ❤️) are rendered on OSX. + # This workaround should be removed once we update to Chromium 51, where the + # problem was fixed. + fontFamily += ', "Apple Color Emoji"' if process.platform is 'darwin' styleSheetSource = """ atom-text-editor { font-size: #{@config.get('editor.fontSize')}px; - font-family: #{@config.get('editor.fontFamily')}; + font-family: #{fontFamily}; line-height: #{@config.get('editor.lineHeight')}; } """