diff --git a/.node-version b/.node-version deleted file mode 100644 index 87a1cf595..000000000 --- a/.node-version +++ /dev/null @@ -1 +0,0 @@ -v0.12.0 diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..c5ff09782 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +cache = ~/.atom/.npm diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8d215e1fe..8170b0121 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -98,7 +98,7 @@ Explain the problem and include additional details to help maintainers reproduce * **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. * **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) or put it in a [gist](https://gist.github.com/) and provide link to that gist. +* **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 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. diff --git a/README.md b/README.md index 580f52294..168124ac5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![Atom](https://cloud.githubusercontent.com/assets/72919/2874231/3af1db48-d3dd-11e3-98dc-6066f8bc766f.png) -[![Build Status](https://travis-ci.org/atom/atom.svg?branch=master)](https://travis-ci.org/atom/atom) +[![Build Status](https://travis-ci.org/atom/atom.svg?branch=master)](https://travis-ci.org/atom/atom) [![Build status](https://ci.appveyor.com/api/projects/status/1tkktwh654w07eim?svg=true)](https://ci.appveyor.com/project/Atom/atom) [![Dependency Status](https://david-dm.org/atom/atom.svg)](https://david-dm.org/atom/atom) [![Join the Atom Community on Slack](http://atom-slack.herokuapp.com/badge.svg)](http://atom-slack.herokuapp.com/) diff --git a/apm/.npmrc b/apm/.npmrc new file mode 100644 index 000000000..c5ff09782 --- /dev/null +++ b/apm/.npmrc @@ -0,0 +1 @@ +cache = ~/.atom/.npm diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..8e33549c7 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,35 @@ +version: "{build}" + +skip_tags: true +clone_folder: c:\projects\atom +clone_depth: 10 + +platform: + - x86 + +environment: + global: + ATOM_DEV_RESOURCE_PATH: c:\projects\atom + ATOM_ACCESS_TOKEN: + secure: Q7vxmSq0bVCLTTRPzXw5ZhPTe7XYhWxX0tQV6neEkddTH6pZkOYNmSCG6VnMX2f+ + + matrix: + - NODE_VERSION: 0.10.35 + +install: + - SET PATH=C:\Program Files\Atom\resources\cli;%PATH% + - ps: Install-Product node $env:NODE_VERSION $env:PLATFORM + +build_script: + - cd %APPVEYOR_BUILD_FOLDER% + - C:\projects\atom\script\cibuild.cmd + +test: off +deploy: off +artifacts: + - path: atom-build + +cache: + - '%USERPROFILE%\.atom\.apm' + - '%USERPROFILE%\.atom\.node-gyp\.atom' + - '%USERPROFILE%\.atom\.npm' diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index a67bee93b..8dd1c573b 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -39,7 +39,16 @@ module.exports = (grunt) -> disableAutoUpdate = grunt.option('no-auto-update') ? false channel = grunt.option('channel') - channel ?= process.env.JANKY_BRANCH if process.env.JANKY_BRANCH in ['stable', 'beta'] + releasableBranches = ['stable', 'beta'] + if process.env.APPVEYOR and not process.env.APPVEYOR_PULL_REQUEST_NUMBER + channel ?= process.env.APPVEYOR_REPO_BRANCH if process.env.APPVEYOR_REPO_BRANCH in releasableBranches + + if process.env.TRAVIS and not process.env.TRAVIS_PULL_REQUEST + channel ?= process.env.TRAVIS_BRANCH if process.env.TRAVIS_BRANCH in releasableBranches + + if process.env.JANKY_BRANCH + channel ?= process.env.JANKY_BRANCH if process.env.JANKY_BRANCH in releasableBranches + channel ?= 'dev' metadata = packageJson @@ -270,16 +279,20 @@ module.exports = (grunt) -> grunt.registerTask('lint', ['standard', 'coffeelint', 'csslint', 'lesslint']) grunt.registerTask('test', ['shell:kill-atom', 'run-specs']) - ciTasks = ['output-disk-space', 'download-electron', 'download-electron-chromedriver', 'build'] + ciTasks = [] + ciTasks.push('output-disk-space') unless process.env.CI + ciTasks.push('download-electron') + ciTasks.push('download-electron-chromedriver') + ciTasks.push('build') ciTasks.push('dump-symbols') if process.platform isnt 'win32' ciTasks.push('set-version', 'check-licenses', 'lint', 'generate-asar') ciTasks.push('mkdeb') if process.platform is 'linux' - ciTasks.push('codesign:exe') if process.platform is 'win32' and not process.env.TRAVIS + 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' - ciTasks.push('codesign:installer') if process.platform is 'win32' and not process.env.TRAVIS - ciTasks.push('codesign:app') if process.platform is 'darwin' and not process.env.TRAVIS - ciTasks.push('publish-build') unless process.env.TRAVIS + ciTasks.push('codesign:installer') if process.platform is 'win32' and not process.env.CI + ciTasks.push('codesign:app') if process.platform is 'darwin' and not process.env.CI + ciTasks.push('publish-build') unless process.env.CI grunt.registerTask('ci', ciTasks) defaultTasks = ['download-electron', 'download-electron-chromedriver', 'build', 'set-version', 'generate-asar'] diff --git a/build/tasks/publish-build-task.coffee b/build/tasks/publish-build-task.coffee index d1640a42b..fc96121ae 100644 --- a/build/tasks/publish-build-task.coffee +++ b/build/tasks/publish-build-task.coffee @@ -9,7 +9,6 @@ request = require 'request' grunt = null -commitSha = process.env.JANKY_SHA1 token = process.env.ATOM_ACCESS_TOKEN defaultHeaders = Authorization: "token #{token}" @@ -31,8 +30,8 @@ module.exports = (gruntObject) -> cp path.join(docsOutputDir, 'api.json'), path.join(buildDir, 'atom-api.json') grunt.registerTask 'upload-assets', 'Upload the assets to a GitHub release', -> - branchName = process.env.JANKY_BRANCH - switch branchName + channel = grunt.config.get('atom.channel') + switch channel when 'stable' isPrerelease = false when 'beta' @@ -55,7 +54,7 @@ module.exports = (gruntObject) -> zipAssets buildDir, assets, (error) -> return done(error) if error? - getAtomDraftRelease isPrerelease, branchName, (error, release) -> + getAtomDraftRelease isPrerelease, channel, (error, release) -> return done(error) if error? assetNames = (asset.assetName for asset in assets) deleteExistingAssets release, assetNames, (error) -> diff --git a/build/tasks/set-version-task.coffee b/build/tasks/set-version-task.coffee index 2cc148011..28abb6493 100644 --- a/build/tasks/set-version-task.coffee +++ b/build/tasks/set-version-task.coffee @@ -5,18 +5,20 @@ module.exports = (grunt) -> {spawn} = require('./task-helpers')(grunt) getVersion = (callback) -> - onBuildMachine = process.env.JANKY_SHA1 and process.env.JANKY_BRANCH in ['stable', 'beta'] + releasableBranches = ['stable', 'beta'] + channel = grunt.config.get('atom.channel') + shouldUseCommitHash = if channel in releasableBranches then false else true inRepository = fs.existsSync(path.resolve(__dirname, '..', '..', '.git')) {version} = require(path.join(grunt.config.get('atom.appDir'), 'package.json')) - if onBuildMachine or not inRepository - callback(null, version) - else + if shouldUseCommitHash and inRepository cmd = 'git' args = ['rev-parse', '--short', 'HEAD'] spawn {cmd, args}, (error, {stdout}={}, code) -> commitHash = stdout?.trim?() combinedVersion = "#{version}-#{commitHash}" callback(error, combinedVersion) + else + callback(null, version) grunt.registerTask 'set-version', 'Set the version in the plist and package.json', -> done = @async() diff --git a/build/tasks/spec-task.coffee b/build/tasks/spec-task.coffee index 27465efdf..892c92696 100644 --- a/build/tasks/spec-task.coffee +++ b/build/tasks/spec-task.coffee @@ -6,8 +6,7 @@ async = require 'async' # TODO: This should really be parallel on every platform, however: # - On Windows, our fixtures step on each others toes. -# - On Travis, Mac workers haven't enough horsepower. -if process.env.TRAVIS or process.platform is 'win32' +if process.platform is 'win32' concurrency = 1 else concurrency = 2 diff --git a/package.json b/package.json index 62139af29..569d9ac59 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "1.3.0-dev", + "version": "1.4.0-dev", "description": "A hackable text editor for the 21st Century.", "main": "./src/browser/main.js", "repository": { @@ -15,7 +15,7 @@ "electronVersion": "0.34.3", "dependencies": { "async": "0.2.6", - "atom-keymap": "^6.1.0", + "atom-keymap": "^6.1.1", "babel-core": "^5.8.21", "bootstrap": "^3.3.4", "cached-run-in-this-context": "0.4.0", @@ -53,7 +53,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "^8.0.3", + "text-buffer": "^8.0.4", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" @@ -98,7 +98,7 @@ "keybinding-resolver": "0.33.0", "line-ending-selector": "0.3.0", "link": "0.31.0", - "markdown-preview": "0.156.1", + "markdown-preview": "0.156.2", "metrics": "0.53.0", "notifications": "0.61.0", "open-on-github": "0.40.0", @@ -109,12 +109,12 @@ "spell-check": "0.63.0", "status-bar": "0.80.0", "styleguide": "0.45.0", - "symbols-view": "0.110.0", + "symbols-view": "0.110.1", "tabs": "0.88.0", "timecop": "0.33.0", "tree-view": "0.198.0", "update-package-dependencies": "0.10.0", - "welcome": "0.32.0", + "welcome": "0.33.0", "whitespace": "0.32.0", "wrap-guide": "0.38.1", "language-c": "0.49.0", @@ -134,11 +134,11 @@ "language-make": "0.20.0", "language-mustache": "0.13.0", "language-objective-c": "0.15.0", - "language-perl": "0.30.0", + "language-perl": "0.31.0", "language-php": "0.34.0", "language-property-list": "0.8.0", "language-python": "0.41.0", - "language-ruby": "0.61.0", + "language-ruby": "0.62.0", "language-ruby-on-rails": "0.24.0", "language-sass": "0.43.0", "language-shellscript": "0.20.0", diff --git a/resources/mac/atom-Info.plist b/resources/mac/atom-Info.plist index e5337551b..e6dbdb8a2 100644 --- a/resources/mac/atom-Info.plist +++ b/resources/mac/atom-Info.plist @@ -214,6 +214,20 @@ LSHandlerRank Alternate + + CFBundleTypeExtensions + + COMMIT_EDITMSG + + CFBundleTypeIconFile + file.icns + CFBundleTypeName + Commit message + CFBundleTypeRole + Editor + LSHandlerRank + Alternate + CFBundleTypeExtensions @@ -228,6 +242,21 @@ LSHandlerRank Alternate + + CFBundleTypeExtensions + + clj + cljs + + CFBundleTypeIconFile + file.icns + CFBundleTypeName + Clojure source + CFBundleTypeRole + Editor + LSHandlerRank + Alternate + CFBundleTypeExtensions @@ -422,6 +451,20 @@ LSHandlerRank Alternate + + CFBundleTypeExtensions + + go + + CFBundleTypeIconFile + file.icns + CFBundleTypeName + Go source + CFBundleTypeRole + Editor + LSHandlerRank + Alternate + CFBundleTypeExtensions @@ -624,6 +667,20 @@ LSHandlerRank Alternate + + CFBundleTypeExtensions + + less + + CFBundleTypeIconFile + file.icns + CFBundleTypeName + Less source + CFBundleTypeRole + Editor + LSHandlerRank + Alternate + CFBundleTypeExtensions @@ -702,6 +759,20 @@ LSHandlerRank Alternate + + CFBundleTypeExtensions + + mk + + CFBundleTypeIconFile + file.icns + CFBundleTypeName + Makefile source + CFBundleTypeRole + Editor + LSHandlerRank + Alternate + CFBundleTypeExtensions @@ -809,6 +880,21 @@ LSHandlerRank Alternate + + CFBundleTypeExtensions + + mustache + hbs + + CFBundleTypeIconFile + file.icns + CFBundleTypeName + Mustache document + CFBundleTypeRole + Editor + LSHandlerRank + Alternate + CFBundleTypeExtensions @@ -998,6 +1084,7 @@ CFBundleTypeExtensions rhtml + erb CFBundleTypeIconFile file.icns @@ -1039,6 +1126,21 @@ LSHandlerRank Alternate + + CFBundleTypeExtensions + + sass + scss + + CFBundleTypeIconFile + file.icns + CFBundleTypeName + Sass source + CFBundleTypeRole + Editor + LSHandlerRank + Alternate + CFBundleTypeExtensions @@ -1243,6 +1345,20 @@ LSHandlerRank Alternate + + CFBundleTypeExtensions + + toml + + CFBundleTypeIconFile + file.icns + CFBundleTypeName + TOML file + CFBundleTypeRole + Editor + LSHandlerRank + Alternate + CFBundleTypeExtensions diff --git a/resources/win/apm.cmd b/resources/win/apm.cmd new file mode 100644 index 000000000..510168983 --- /dev/null +++ b/resources/win/apm.cmd @@ -0,0 +1,3 @@ +@echo off + +"%~dp0\..\app\apm\bin\node.exe" "%~dp0\..\app\apm\lib\cli.js" %* diff --git a/script/cibuild b/script/cibuild index d366af79a..c1aedddc8 100755 --- a/script/cibuild +++ b/script/cibuild @@ -22,12 +22,18 @@ function loadEnvironmentVariables(filePath) { } function readEnvironmentVariables() { - if (process.platform === 'win32') { - loadEnvironmentVariables(path.resolve('/jenkins/config/atomcredentials')); - } else if (process.platform === 'darwin') { - loadEnvironmentVariables('/var/lib/jenkins/config/atomcredentials'); - loadEnvironmentVariables('/var/lib/jenkins/config/xcodekeychain'); - } else if (process.platform === 'linux') { + if (process.env.JANKY_SHA1) { + if (process.platform === 'win32') { + loadEnvironmentVariables(path.resolve('/jenkins/config/atomcredentials')); + } else if (process.platform === 'darwin') { + loadEnvironmentVariables('/var/lib/jenkins/config/atomcredentials'); + loadEnvironmentVariables('/var/lib/jenkins/config/xcodekeychain'); + } + } +} + +function setEnvironmentVariables() { + if (process.platform === 'linux') { // Use Clang for building native code, the GCC on Precise is too old. process.env.CC = 'clang'; process.env.CXX = 'clang++'; @@ -81,6 +87,7 @@ function removeTempFolders() { } readEnvironmentVariables(); +setEnvironmentVariables(); removeNodeModules(); removeTempFolders(); cp.safeExec.bind(global, 'npm install npm --loglevel error', {cwd: path.resolve(__dirname, '..', 'build')}, function() { diff --git a/script/cibuild.cmd b/script/cibuild.cmd new file mode 100644 index 000000000..79197d1b1 --- /dev/null +++ b/script/cibuild.cmd @@ -0,0 +1,5 @@ +@IF EXIST "%~dp0\node.exe" ( + "%~dp0\node.exe" "%~dp0\cibuild" %* +) ELSE ( + node "%~dp0\cibuild" %* +) diff --git a/spec/command-installer-spec.coffee b/spec/command-installer-spec.coffee index 584dc193e..84fd77a34 100644 --- a/spec/command-installer-spec.coffee +++ b/spec/command-installer-spec.coffee @@ -20,10 +20,45 @@ describe "CommandInstaller on #darwin", -> spyOn(CommandInstaller::, 'getResourcesDirectory').andReturn(resourcesPath) spyOn(CommandInstaller::, 'getInstallDirectory').andReturn(installationPath) + it "shows an error dialog when installing commands interactively fails", -> + appDelegate = jasmine.createSpyObj("appDelegate", ["confirm"]) + installer = new CommandInstaller("2.0.2", appDelegate) + spyOn(installer, "installAtomCommand").andCallFake (__, callback) -> callback(new Error("an error")) + + installer.installShellCommandsInteractively() + + expect(appDelegate.confirm).toHaveBeenCalledWith({ + message: "Failed to install shell commands" + detailedMessage: "an error" + }) + + appDelegate.confirm.reset() + installer.installAtomCommand.andCallFake (__, callback) -> callback() + spyOn(installer, "installApmCommand").andCallFake (__, callback) -> callback(new Error("another error")) + + installer.installShellCommandsInteractively() + + expect(appDelegate.confirm).toHaveBeenCalledWith({ + message: "Failed to install shell commands" + detailedMessage: "another error" + }) + + it "shows a success dialog when installing commands interactively succeeds", -> + appDelegate = jasmine.createSpyObj("appDelegate", ["confirm"]) + installer = new CommandInstaller("2.0.2", appDelegate) + spyOn(installer, "installAtomCommand").andCallFake (__, callback) -> callback() + spyOn(installer, "installApmCommand").andCallFake (__, callback) -> callback() + + installer.installShellCommandsInteractively() + + expect(appDelegate.confirm).toHaveBeenCalledWith({ + message: "Commands installed." + detailedMessage: "The shell commands `atom` and `apm` are installed." + }) + describe "when using a stable version of atom", -> beforeEach -> - confirm = -> - installer = new CommandInstaller("2.0.2", confirm) + installer = new CommandInstaller("2.0.2") it "symlinks the atom command as 'atom'", -> installedAtomPath = path.join(installationPath, 'atom') diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index bb9ab89a8..e00cee789 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -589,6 +589,59 @@ describe "Config", -> atom.config.transact -> expect(changeSpy).not.toHaveBeenCalled() + describe ".transactAsync(callback)", -> + changeSpy = null + + beforeEach -> + changeSpy = jasmine.createSpy('onDidChange callback') + atom.config.onDidChange("foo.bar.baz", changeSpy) + + it "allows only one change event for the duration of the given promise if it gets resolved", -> + promiseResult = null + transactionPromise = atom.config.transactAsync -> + atom.config.set("foo.bar.baz", 1) + atom.config.set("foo.bar.baz", 2) + atom.config.set("foo.bar.baz", 3) + Promise.resolve("a result") + + waitsForPromise -> transactionPromise.then (r) -> promiseResult = r + + runs -> + expect(promiseResult).toBe("a result") + expect(changeSpy.callCount).toBe(1) + expect(changeSpy.argsForCall[0][0]).toEqual(newValue: 3, oldValue: undefined) + + it "allows only one change event for the duration of the given promise if it gets rejected", -> + promiseError = null + transactionPromise = atom.config.transactAsync -> + atom.config.set("foo.bar.baz", 1) + atom.config.set("foo.bar.baz", 2) + atom.config.set("foo.bar.baz", 3) + Promise.reject("an error") + + waitsForPromise -> transactionPromise.catch (e) -> promiseError = e + + runs -> + expect(promiseError).toBe("an error") + expect(changeSpy.callCount).toBe(1) + expect(changeSpy.argsForCall[0][0]).toEqual(newValue: 3, oldValue: undefined) + + it "allows only one change event even when the given callback throws", -> + error = new Error("Oops!") + promiseError = null + transactionPromise = atom.config.transactAsync -> + atom.config.set("foo.bar.baz", 1) + atom.config.set("foo.bar.baz", 2) + atom.config.set("foo.bar.baz", 3) + throw error + + waitsForPromise -> transactionPromise.catch (e) -> promiseError = e + + runs -> + expect(promiseError).toBe(error) + expect(changeSpy.callCount).toBe(1) + expect(changeSpy.argsForCall[0][0]).toEqual(newValue: 3, oldValue: undefined) + describe ".getSources()", -> it "returns an array of all of the config's source names", -> expect(atom.config.getSources()).toEqual([]) diff --git a/spec/grammars-spec.coffee b/spec/grammars-spec.coffee index 82a29892a..a36a10170 100644 --- a/spec/grammars-spec.coffee +++ b/spec/grammars-spec.coffee @@ -142,9 +142,13 @@ describe "the `grammars` global", -> expect(atom.grammars.selectGrammar('Rakefile', '').scopeName).toBe 'source.coffee' expect(atom.grammars.selectGrammar('Cakefile', '').scopeName).toBe 'source.ruby' - it "favors grammars with matching first-line-regexps even if custom file types match the file", -> + it "favors user-defined file types over grammars with matching first-line-regexps", -> atom.config.set('core.customFileTypes', 'source.ruby': ['bootstrap']) - expect(atom.grammars.selectGrammar('bootstrap', '#!/usr/bin/env node').scopeName).toBe 'source.js' + expect(atom.grammars.selectGrammar('bootstrap', '#!/usr/bin/env node').scopeName).toBe 'source.ruby' + + describe "when there is a grammar with a first line pattern, the file type of the file is known, but from a different grammar", -> + it "favors file type over the matching pattern", -> + expect(atom.grammars.selectGrammar('foo.rb', '#!/usr/bin/env node').scopeName).toBe 'source.ruby' describe ".removeGrammar(grammar)", -> it "removes the grammar, so it won't be returned by selectGrammar", -> diff --git a/spec/integration/startup-spec.coffee b/spec/integration/startup-spec.coffee index 36342470d..799c7685f 100644 --- a/spec/integration/startup-spec.coffee +++ b/spec/integration/startup-spec.coffee @@ -4,7 +4,7 @@ return unless process.env.ATOM_INTEGRATION_TESTS_ENABLED # Integration tests require a fast machine and, for now, we cannot afford to # run them on Travis. -return if process.env.TRAVIS +return if process.env.CI fs = require "fs-plus" path = require "path" diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 88e730d44..4b5f3c26d 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -284,6 +284,7 @@ describe "PackageManager", -> expect(Package.prototype.requireMainModule.callCount).toBe 0 atom.packages.triggerActivationHook('language-fictitious:grammar-used') + atom.packages.triggerDeferredActivationHooks() waitsForPromise -> promise @@ -433,6 +434,13 @@ describe "PackageManager", -> runs -> expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)).toHaveLength 0 + describe "when setting core.packagesWithKeymapsDisabled", -> + it "ignores package names in the array that aren't loaded", -> + atom.packages.observePackagesWithKeymapsDisabled() + + expect(-> atom.config.set("core.packagesWithKeymapsDisabled", ["package-does-not-exist"])).not.toThrow() + expect(-> atom.config.set("core.packagesWithKeymapsDisabled", [])).not.toThrow() + describe "when the package's keymaps are disabled and re-enabled after it is activated", -> it "removes and re-adds the keymaps", -> element1 = createTestElement('test-1') diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee index 0c7a22b77..36803bde6 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -168,6 +168,15 @@ describe "Pane", -> pane.activateNextItem() expect(pane.getActiveItem()).toBe item1 + describe "::activateLastItem()", -> + it "sets the active item to the last item", -> + pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")])) + [item1, item2, item3] = pane.getItems() + + expect(pane.getActiveItem()).toBe item1 + pane.activateLastItem() + expect(pane.getActiveItem()).toBe item3 + describe "::moveItemRight() and ::moveItemLeft()", -> it "moves the active item to the right and left, without looping around at either end", -> pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")])) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 7376b5823..54899a1d6 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -25,27 +25,34 @@ describe "TextEditorPresenter", -> editor.destroy() buffer.destroy() - buildPresenter = (params={}) -> + buildPresenterWithoutMeasurements = (params={}) -> _.defaults params, model: editor - explicitHeight: 130 - contentFrameWidth: 500 - windowWidth: 500 - windowHeight: 130 - boundingClientRect: {left: 0, top: 0, width: 500, height: 130} - gutterWidth: 0 - lineHeight: 10 - baseCharacterWidth: 10 - horizontalScrollbarHeight: 10 - verticalScrollbarWidth: 10 - scrollTop: 0 - scrollLeft: 0 config: atom.config - + contentFrameWidth: 500 presenter = new TextEditorPresenter(params) presenter.setLinesYardstick(new FakeLinesYardstick(editor, presenter)) presenter + buildPresenter = (params={}) -> + presenter = buildPresenterWithoutMeasurements(params) + presenter.setScrollTop(params.scrollTop) if params.scrollTop? + presenter.setScrollLeft(params.scrollLeft) if params.scrollLeft? + presenter.setExplicitHeight(params.explicitHeight ? 130) + presenter.setWindowSize(params.windowWidth ? 500, params.windowHeight ? 130) + presenter.setBoundingClientRect(params.boundingClientRect ? { + left: 0 + top: 0 + width: 500 + height: 130 + }) + presenter.setGutterWidth(params.gutterWidth ? 0) + presenter.setLineHeight(params.lineHeight ? 10) + presenter.setBaseCharacterWidth(params.baseCharacterWidth ? 10) + presenter.setHorizontalScrollbarHeight(params.horizontalScrollbarHeight ? 10) + presenter.setVerticalScrollbarWidth(params.verticalScrollbarWidth ? 10) + presenter + expectValues = (actual, expected) -> for key, value of expected expect(actual[key]).toEqual value @@ -167,16 +174,14 @@ describe "TextEditorPresenter", -> expect(stateFn(presenter).tiles[12]).toBeDefined() it "is empty until all of the required measurements are assigned", -> - presenter = buildPresenter(explicitHeight: null, lineHeight: null, scrollTop: null) + presenter = buildPresenterWithoutMeasurements() expect(stateFn(presenter).tiles).toEqual({}) presenter.setExplicitHeight(25) expect(stateFn(presenter).tiles).toEqual({}) + # Sets scroll row from model's logical position presenter.setLineHeight(10) - expect(stateFn(presenter).tiles).toEqual({}) - - presenter.setScrollTop(0) expect(stateFn(presenter).tiles).not.toEqual({}) it "updates when ::scrollTop changes", -> @@ -619,6 +624,8 @@ describe "TextEditorPresenter", -> describe ".scrollingVertically", -> it "is true for ::stoppedScrollingDelay milliseconds following a changes to ::scrollTop", -> presenter = buildPresenter(scrollTop: 10, stoppedScrollingDelay: 200, explicitHeight: 100) + expect(presenter.getState().content.scrollingVertically).toBe true + advanceClock(300) expect(presenter.getState().content.scrollingVertically).toBe false expectStateUpdate presenter, -> presenter.setScrollTop(0) expect(presenter.getState().content.scrollingVertically).toBe true @@ -761,7 +768,8 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.scrollTop).toBe(10) it "corresponds to the passed logical coordinates when building the presenter", -> - presenter = buildPresenter(scrollRow: 4, lineHeight: 10, explicitHeight: 20) + editor.setFirstVisibleScreenRow(4) + presenter = buildPresenter(lineHeight: 10, explicitHeight: 20) expect(presenter.getState().content.scrollTop).toBe(40) it "tracks the value of ::scrollTop", -> @@ -775,11 +783,11 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> presenter.setScrollTop(50) presenter.getState() # commits scroll position - expect(editor.getScrollRow()).toBe(5) + expect(editor.getFirstVisibleScreenRow()).toBe 5 expectStateUpdate presenter, -> presenter.setScrollTop(57) presenter.getState() # commits scroll position - expect(editor.getScrollRow()).toBe(6) + expect(editor.getFirstVisibleScreenRow()).toBe 6 it "reassigns the scrollTop if it exceeds the max possible value after lines are removed", -> presenter = buildPresenter(scrollTop: 80, lineHeight: 10, explicitHeight: 50, horizontalScrollbarHeight: 0) @@ -888,7 +896,8 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.scrollLeft).toBe(50) it "corresponds to the passed logical coordinates when building the presenter", -> - presenter = buildPresenter(scrollColumn: 3, lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) + editor.setFirstVisibleScreenColumn(3) + presenter = buildPresenter(lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) expect(presenter.getState().content.scrollLeft).toBe(30) it "tracks the value of ::scrollLeft", -> @@ -902,11 +911,11 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> presenter.setScrollLeft(50) presenter.getState() # commits scroll position - expect(editor.getScrollColumn()).toBe(5) + expect(editor.getFirstVisibleScreenColumn()).toBe 5 expectStateUpdate presenter, -> presenter.setScrollLeft(57) presenter.getState() # commits scroll position - expect(editor.getScrollColumn()).toBe(6) + expect(editor.getFirstVisibleScreenColumn()).toBe 6 it "is always rounded to the nearest integer", -> presenter = buildPresenter(scrollLeft: 10, lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) @@ -1006,20 +1015,25 @@ describe "TextEditorPresenter", -> describe ".backgroundColor", -> it "is assigned to ::backgroundColor unless the editor is mini", -> - presenter = buildPresenter(backgroundColor: 'rgba(255, 0, 0, 0)') + presenter = buildPresenter() + presenter.setBackgroundColor('rgba(255, 0, 0, 0)') expect(presenter.getState().content.backgroundColor).toBe 'rgba(255, 0, 0, 0)' + editor.setMini(true) - presenter = buildPresenter(backgroundColor: 'rgba(255, 0, 0, 0)') + presenter = buildPresenter() + presenter.setBackgroundColor('rgba(255, 0, 0, 0)') expect(presenter.getState().content.backgroundColor).toBeNull() it "updates when ::backgroundColor changes", -> - presenter = buildPresenter(backgroundColor: 'rgba(255, 0, 0, 0)') + presenter = buildPresenter() + presenter.setBackgroundColor('rgba(255, 0, 0, 0)') expect(presenter.getState().content.backgroundColor).toBe 'rgba(255, 0, 0, 0)' expectStateUpdate presenter, -> presenter.setBackgroundColor('rgba(0, 0, 255, 0)') expect(presenter.getState().content.backgroundColor).toBe 'rgba(0, 0, 255, 0)' it "updates when ::mini changes", -> - presenter = buildPresenter(backgroundColor: 'rgba(255, 0, 0, 0)') + presenter = buildPresenter() + presenter.setBackgroundColor('rgba(255, 0, 0, 0)') expect(presenter.getState().content.backgroundColor).toBe 'rgba(255, 0, 0, 0)' expectStateUpdate presenter, -> editor.setMini(true) expect(presenter.getState().content.backgroundColor).toBeNull() @@ -1047,6 +1061,7 @@ describe "TextEditorPresenter", -> describe "[tileId].lines[lineId]", -> # line state objects it "includes the state for visible lines in a tile", -> presenter = buildPresenter(explicitHeight: 3, scrollTop: 4, lineHeight: 1, tileSize: 3, stoppedScrollingDelay: 200) + presenter.setExplicitHeight(3) expect(lineStateForScreenRow(presenter, 2)).toBeUndefined() @@ -1320,7 +1335,7 @@ describe "TextEditorPresenter", -> expect(stateForCursor(presenter, 4)).toBeUndefined() it "is empty until all of the required measurements are assigned", -> - presenter = buildPresenter(explicitHeight: null, lineHeight: null, scrollTop: null, baseCharacterWidth: null, horizontalScrollbarHeight: null) + presenter = buildPresenterWithoutMeasurements() expect(presenter.getState().content.cursors).toEqual({}) presenter.setExplicitHeight(25) @@ -1335,6 +1350,15 @@ describe "TextEditorPresenter", -> presenter.setBaseCharacterWidth(8) expect(presenter.getState().content.cursors).toEqual({}) + presenter.setBoundingClientRect(top: 0, left: 0, width: 500, height: 130) + expect(presenter.getState().content.cursors).toEqual({}) + + presenter.setWindowSize(500, 130) + expect(presenter.getState().content.cursors).toEqual({}) + + presenter.setVerticalScrollbarWidth(10) + expect(presenter.getState().content.cursors).toEqual({}) + presenter.setHorizontalScrollbarHeight(10) expect(presenter.getState().content.cursors).not.toEqual({}) @@ -1466,7 +1490,8 @@ describe "TextEditorPresenter", -> it "alternates between true and false twice per ::cursorBlinkPeriod when the editor is focused", -> cursorBlinkPeriod = 100 cursorBlinkResumeDelay = 200 - presenter = buildPresenter({cursorBlinkPeriod, cursorBlinkResumeDelay, focused: true}) + presenter = buildPresenter({cursorBlinkPeriod, cursorBlinkResumeDelay}) + presenter.setFocused(true) expect(presenter.getState().content.cursorsVisible).toBe true expectStateUpdate presenter, -> advanceClock(cursorBlinkPeriod / 2) @@ -1493,7 +1518,8 @@ describe "TextEditorPresenter", -> it "stops alternating for ::cursorBlinkResumeDelay when a cursor moves or a cursor is added", -> cursorBlinkPeriod = 100 cursorBlinkResumeDelay = 200 - presenter = buildPresenter({cursorBlinkPeriod, cursorBlinkResumeDelay, focused: true}) + presenter = buildPresenter({cursorBlinkPeriod, cursorBlinkResumeDelay}) + presenter.setFocused(true) expect(presenter.getState().content.cursorsVisible).toBe true expectStateUpdate presenter, -> advanceClock(cursorBlinkPeriod / 2) @@ -1643,7 +1669,7 @@ describe "TextEditorPresenter", -> [[0, 2], [2, 4]], ]) - presenter = buildPresenter(explicitHeight: null, lineHeight: null, scrollTop: null, baseCharacterWidth: null, tileSize: 2) + presenter = buildPresenterWithoutMeasurements(tileSize: 2) for tileId, tileState of presenter.getState().content.tiles expect(tileState.highlights).toEqual({}) @@ -1970,7 +1996,7 @@ describe "TextEditorPresenter", -> marker = editor.markBufferRange([[2, 13], [4, 14]], invalidate: 'touch') decoration = editor.decorateMarker(marker, {type: 'overlay', position: 'tail', item}) - presenter = buildPresenter(baseCharacterWidth: null, lineHeight: null, windowWidth: null, windowHeight: null, boundingClientRect: null) + presenter = buildPresenterWithoutMeasurements() expect(presenter.getState().content.overlays).toEqual({}) presenter.setBaseCharacterWidth(10) @@ -1982,6 +2008,12 @@ describe "TextEditorPresenter", -> presenter.setWindowSize(500, 100) expect(presenter.getState().content.overlays).toEqual({}) + presenter.setVerticalScrollbarWidth(10) + expect(presenter.getState().content.overlays).toEqual({}) + + presenter.setHorizontalScrollbarHeight(10) + expect(presenter.getState().content.overlays).toEqual({}) + presenter.setBoundingClientRect({top: 0, left: 0, height: 100, width: 500}) expect(presenter.getState().content.overlays).not.toEqual({}) @@ -2168,7 +2200,8 @@ describe "TextEditorPresenter", -> expect(editor.getRowsPerPage()).toBe(20) it "tracks the computed content height if ::autoHeight is true so the editor auto-expands vertically", -> - presenter = buildPresenter(explicitHeight: null, autoHeight: true) + presenter = buildPresenter(explicitHeight: null) + presenter.setAutoHeight(true) expect(presenter.getState().height).toBe editor.getScreenLineCount() * 10 expectStateUpdate presenter, -> presenter.setAutoHeight(false) @@ -2185,7 +2218,9 @@ describe "TextEditorPresenter", -> describe ".focused", -> it "tracks the value of ::focused", -> - presenter = buildPresenter(focused: false) + presenter = buildPresenter() + presenter.setFocused(false) + expect(presenter.getState().focused).toBe false expectStateUpdate presenter, -> presenter.setFocused(true) expect(presenter.getState().focused).toBe true @@ -2882,7 +2917,9 @@ describe "TextEditorPresenter", -> describe ".backgroundColor", -> it "is assigned to ::gutterBackgroundColor if present, and to ::backgroundColor otherwise", -> - presenter = buildPresenter(backgroundColor: "rgba(255, 0, 0, 0)", gutterBackgroundColor: "rgba(0, 255, 0, 0)") + presenter = buildPresenter() + presenter.setBackgroundColor("rgba(255, 0, 0, 0)") + presenter.setGutterBackgroundColor("rgba(0, 255, 0, 0)") expect(getStylesForGutterWithName(presenter, 'line-number').backgroundColor).toBe "rgba(0, 255, 0, 0)" expect(getStylesForGutterWithName(presenter, 'test-gutter').backgroundColor).toBe "rgba(0, 255, 0, 0)" diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 0ad43046b..39740ebd2 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -163,14 +163,8 @@ describe "TextEditor", -> expect(editor.getTitle()).toBe 'untitled' describe ".getLongTitle()", -> - it "appends the name of the containing directory to the basename of the file", -> - expect(editor.getLongTitle()).toBe 'sample.js - fixtures' - buffer.setPath(undefined) - expect(editor.getLongTitle()).toBe 'untitled' - - describe ".getUniqueTitle()", -> it "returns file name when there is no opened file with identical name", -> - expect(editor.getUniqueTitle()).toBe 'sample.js' + expect(editor.getLongTitle()).toBe 'sample.js' buffer.setPath(undefined) expect(editor.getLongTitle()).toBe 'untitled' @@ -183,8 +177,8 @@ describe "TextEditor", -> atom.workspace.open(path.join('sample-theme-2', 'readme')).then (o) -> editor2 = o runs -> - expect(editor1.getUniqueTitle()).toBe 'sample-theme-1/readme' - expect(editor2.getUniqueTitle()).toBe 'sample-theme-2/readme' + expect(editor1.getLongTitle()).toBe 'sample-theme-1/readme' + expect(editor2.getLongTitle()).toBe 'sample-theme-2/readme' it "or returns /.../ when opened files has identical file names", -> editor1 = null @@ -195,8 +189,8 @@ describe "TextEditor", -> atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'main.js')).then (o) -> editor2 = o runs -> - expect(editor1.getUniqueTitle()).toBe 'sample-theme-1/.../main.js' - expect(editor2.getUniqueTitle()).toBe 'sample-theme-2/.../main.js' + expect(editor1.getLongTitle()).toBe 'sample-theme-1/.../main.js' + expect(editor2.getLongTitle()).toBe 'sample-theme-2/.../main.js' it "notifies ::onDidChangeTitle observers when the underlying buffer path changes", -> @@ -5444,6 +5438,73 @@ describe "TextEditor", -> editor.selectPageUp() expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [12, 2]]] + describe "::setFirstVisibleScreenRow() and ::getFirstVisibleScreenRow()", -> + beforeEach -> + line = Array(9).join('0123456789') + editor.setText([1..100].map(-> line).join('\n')) + expect(editor.getLineCount()).toBe 100 + expect(editor.lineTextForBufferRow(0).length).toBe 80 + + describe "when the editor doesn't have a height and lineHeightInPixels", -> + it "does not affect the editor's visible row range", -> + expect(editor.getVisibleRowRange()).toBeNull() + + editor.setFirstVisibleScreenRow(1) + expect(editor.getFirstVisibleScreenRow()).toEqual 1 + + editor.setFirstVisibleScreenRow(3) + expect(editor.getFirstVisibleScreenRow()).toEqual 3 + + expect(editor.getVisibleRowRange()).toBeNull() + expect(editor.getLastVisibleScreenRow()).toBeNull() + + describe "when the editor has a height and lineHeightInPixels", -> + beforeEach -> + atom.config.set('editor.scrollPastEnd', true) + editor.setHeight(100, true) + editor.setLineHeightInPixels(10) + + it "updates the editor's visible row range", -> + editor.setFirstVisibleScreenRow(2) + expect(editor.getFirstVisibleScreenRow()).toEqual 2 + expect(editor.getLastVisibleScreenRow()).toBe 12 + expect(editor.getVisibleRowRange()).toEqual [2, 12] + + it "notifies ::onDidChangeFirstVisibleScreenRow observers", -> + changeCount = 0 + editor.onDidChangeFirstVisibleScreenRow -> changeCount++ + + editor.setFirstVisibleScreenRow(2) + expect(changeCount).toBe 1 + + editor.setFirstVisibleScreenRow(2) + expect(changeCount).toBe 1 + + editor.setFirstVisibleScreenRow(3) + expect(changeCount).toBe 2 + + it "ensures that the top row is less than the buffer's line count", -> + editor.setFirstVisibleScreenRow(102) + expect(editor.getFirstVisibleScreenRow()).toEqual 99 + expect(editor.getVisibleRowRange()).toEqual [99, 99] + + it "ensures that the left column is less than the length of the longest screen line", -> + editor.setFirstVisibleScreenRow(10) + expect(editor.getFirstVisibleScreenRow()).toEqual 10 + + editor.setText("\n\n\n") + + editor.setFirstVisibleScreenRow(10) + expect(editor.getFirstVisibleScreenRow()).toEqual 3 + + describe "when the 'editor.scrollPastEnd' option is set to false", -> + it "ensures that the bottom row is less than the buffer's line count", -> + atom.config.set('editor.scrollPastEnd', false) + + editor.setFirstVisibleScreenRow(95) + expect(editor.getFirstVisibleScreenRow()).toEqual 89 + expect(editor.getVisibleRowRange()).toEqual [89, 99] + describe '.get/setPlaceholderText()', -> it 'can be created with placeholderText', -> newEditor = atom.workspace.buildTextEditor( diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index f98e1b835..938010607 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -117,7 +117,7 @@ class AtomEnvironment extends Model # Call .loadOrCreate instead constructor: (params={}) -> - {@blobStore, @applicationDelegate, @window, @document, configDirPath, @enablePersistence} = params + {@blobStore, @applicationDelegate, @window, @document, configDirPath, @enablePersistence, onlyLoadBaseStyleSheets} = params @state = {version: @constructor.version} @@ -183,7 +183,7 @@ class AtomEnvironment extends Model @themes.loadBaseStylesheets() @initialStyleElements = @styles.getSnapshot() - @themes.initialLoadComplete = true + @themes.initialLoadComplete = true if onlyLoadBaseStyleSheets @setBodyPlatformClass() @stylesElement = @styles.buildStylesElement() diff --git a/src/command-installer.coffee b/src/command-installer.coffee index 5a6a7f94e..e37e6a0e6 100644 --- a/src/command-installer.coffee +++ b/src/command-installer.coffee @@ -35,7 +35,7 @@ class CommandInstaller process.resourcesPath installShellCommandsInteractively: -> - showErrorDialog = (error) -> + showErrorDialog = (error) => @applicationDelegate.confirm message: "Failed to install shell commands" detailedMessage: error.message diff --git a/src/config.coffee b/src/config.coffee index 663dbb777..888193059 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -671,17 +671,47 @@ class Config # # * `callback` {Function} to execute while suppressing calls to handlers. transact: (callback) -> - @transactDepth++ + @beginTransaction() try callback() finally - @transactDepth-- - @emitChangeEvent() + @endTransaction() ### Section: Internal methods used by core ### + # Private: Suppress calls to handler functions registered with {::onDidChange} + # and {::observe} for the duration of the {Promise} returned by `callback`. + # After the {Promise} is either resolved or rejected, handlers will be called + # once if the value for their key-path has changed. + # + # * `callback` {Function} that returns a {Promise}, which will be executed + # while suppressing calls to handlers. + # + # Returns a {Promise} that is either resolved or rejected according to the + # `{Promise}` returned by `callback`. If `callback` throws an error, a + # rejected {Promise} will be returned instead. + transactAsync: (callback) -> + @beginTransaction() + try + endTransaction = (fn) => (args...) => + @endTransaction() + fn(args...) + result = callback() + new Promise (resolve, reject) => + result.then(endTransaction(resolve)).catch(endTransaction(reject)) + catch error + @endTransaction() + Promise.reject(error) + + beginTransaction: -> + @transactDepth++ + + endTransaction: -> + @transactDepth-- + @emitChangeEvent() + pushAtKeyPath: (keyPath, value) -> arrayValue = @get(keyPath) ? [] result = arrayValue.push(value) diff --git a/src/grammar-registry.coffee b/src/grammar-registry.coffee index 4a5352275..383d4d776 100644 --- a/src/grammar-registry.coffee +++ b/src/grammar-registry.coffee @@ -36,21 +36,21 @@ class GrammarRegistry extends FirstMate.GrammarRegistry if score > highestScore or not bestMatch? bestMatch = grammar highestScore = score - else if score is highestScore and bestMatch?.bundledPackage - bestMatch = grammar unless grammar.bundledPackage bestMatch # Extended: Returns a {Number} representing how well the grammar matches the # `filePath` and `contents`. getGrammarScore: (grammar, filePath, contents) -> + return Infinity if @grammarOverrideForPath(filePath) is grammar.scopeName + contents = fs.readFileSync(filePath, 'utf8') if not contents? and fs.isFileSync(filePath) - if @grammarOverrideForPath(filePath) is grammar.scopeName - 2 + (filePath?.length ? 0) - else if @grammarMatchesContents(grammar, contents) - 1 + (filePath?.length ? 0) - else - @getGrammarPathScore(grammar, filePath) + score = @getGrammarPathScore(grammar, filePath) + if score > 0 and not grammar.bundledPackage + score += 0.25 + if @grammarMatchesContents(grammar, contents) + score += 0.125 + score getGrammarPathScore: (grammar, filePath) -> return -1 unless filePath diff --git a/src/initialize-test-window.coffee b/src/initialize-test-window.coffee index 72a071fb6..375581a96 100644 --- a/src/initialize-test-window.coffee +++ b/src/initialize-test-window.coffee @@ -58,6 +58,7 @@ module.exports = ({blobStore}) -> document.title = "Spec Suite" # Avoid throttling of test window by playing silence + # See related discussion in https://github.com/atom/atom/pull/9485 context = new AudioContext() source = context.createBufferSource() source.connect(context.destination) @@ -69,6 +70,7 @@ module.exports = ({blobStore}) -> buildAtomEnvironment = (params) -> params = cloneObject(params) params.blobStore = blobStore unless params.hasOwnProperty("blobStore") + params.onlyLoadBaseStyleSheets = true unless params.hasOwnProperty("onlyLoadBaseStyleSheets") new AtomEnvironment(params) promise = testRunner({ diff --git a/src/package-manager.coffee b/src/package-manager.coffee index 5c0df4b70..789b2eae5 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -37,6 +37,7 @@ class PackageManager @emitter = new Emitter @activationHookEmitter = new Emitter @packageDirPaths = [] + @deferredActivationHooks = [] if configDirPath? and not safeMode if @devMode @packageDirPaths.push(path.join(configDirPath, "dev", "packages")) @@ -336,8 +337,10 @@ class PackageManager keymapsToEnable = _.difference(oldValue, newValue) keymapsToDisable = _.difference(newValue, oldValue) - @getLoadedPackage(packageName).deactivateKeymaps() for packageName in keymapsToDisable when not @isPackageDisabled(packageName) - @getLoadedPackage(packageName).activateKeymaps() for packageName in keymapsToEnable when not @isPackageDisabled(packageName) + for packageName in keymapsToDisable when not @isPackageDisabled(packageName) + @getLoadedPackage(packageName)?.deactivateKeymaps() + for packageName in keymapsToEnable when not @isPackageDisabled(packageName) + @getLoadedPackage(packageName)?.activateKeymaps() null loadPackages: -> @@ -407,6 +410,7 @@ class PackageManager packages = @getLoadedPackagesForTypes(types) promises = promises.concat(activator.activatePackages(packages)) Promise.all(promises).then => + @triggerDeferredActivationHooks() @emitter.emit 'did-activate-initial-packages' # another type of package manager can handle other package types. @@ -416,11 +420,11 @@ class PackageManager activatePackages: (packages) -> promises = [] - @config.transact => + @config.transactAsync => for pack in packages promise = @activatePackage(pack.name) - promises.push(promise) unless pack.hasActivationCommands() - return + promises.push(promise) unless pack.activationShouldBeDeferred() + Promise.all(promises) @observeDisabledPackages() @observePackagesWithKeymapsDisabled() promises @@ -437,9 +441,17 @@ class PackageManager else Promise.reject(new Error("Failed to load package '#{name}'")) + triggerDeferredActivationHooks: -> + return unless @deferredActivationHooks? + @activationHookEmitter.emit(hook) for hook in @deferredActivationHooks + @deferredActivationHooks = null + triggerActivationHook: (hook) -> return new Error("Cannot trigger an empty activation hook") unless hook? and _.isString(hook) and hook.length > 0 - @activationHookEmitter.emit(hook) + if @deferredActivationHooks? + @deferredActivationHooks.push hook + else + @activationHookEmitter.emit(hook) onDidTriggerActivationHook: (hook, callback) -> return unless hook? and _.isString(hook) and hook.length > 0 diff --git a/src/pane.coffee b/src/pane.coffee index 92be02575..412fc5251 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -312,6 +312,9 @@ class Pane extends Model else @activateItemAtIndex(@items.length - 1) + activateLastItem: -> + @activateItemAtIndex(@items.length - 1) + # Public: Move the active tab to the right. moveItemRight: -> index = @getActiveItemIndex() @@ -722,7 +725,7 @@ class Pane extends Model @notificationManager.addWarning("Unable to save file: #{error.message}") else if error.code is 'EACCES' addWarningWithPath('Unable to save file: Permission denied') - else if error.code in ['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST'] + else if error.code in ['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST', 'ELOOP', 'EAGAIN'] addWarningWithPath('Unable to save file', detail: error.message) else if error.code is 'EROFS' addWarningWithPath('Unable to save file: Read-only file system') diff --git a/src/project.coffee b/src/project.coffee index 935e3a213..bb9c8be80 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -55,7 +55,7 @@ class Project extends Model ### deserialize: (state, deserializerManager) -> - states.paths = [state.path] if state.path? # backward compatibility + state.paths = [state.path] if state.path? # backward compatibility @buffers = _.compact state.buffers.map (bufferState) -> # Check that buffer's file path is accessible diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index 80b7ff6c6..6c838b8c0 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -12,7 +12,7 @@ module.exports = ({commandRegistry, commandInstaller, config}) -> 'pane:show-item-6': -> @getModel().getActivePane().activateItemAtIndex(5) 'pane:show-item-7': -> @getModel().getActivePane().activateItemAtIndex(6) 'pane:show-item-8': -> @getModel().getActivePane().activateItemAtIndex(7) - 'pane:show-item-9': -> @getModel().getActivePane().activateItemAtIndex(8) + 'pane:show-item-9': -> @getModel().getActivePane().activateLastItem() 'pane:move-item-right': -> @getModel().getActivePane().moveItemRight() 'pane:move-item-left': -> @getModel().getActivePane().moveItemLeft() 'window:increase-font-size': -> @getModel().increaseFontSize() diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 99938ef5f..430b0c0fd 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -50,10 +50,6 @@ class TextEditorComponent @presenter = new TextEditorPresenter model: @editor - scrollTop: 0 - scrollLeft: 0 - scrollRow: @editor.getScrollRow() - scrollColumn: @editor.getScrollColumn() tileSize: tileSize cursorBlinkPeriod: @cursorBlinkPeriod cursorBlinkResumeDelay: @cursorBlinkResumeDelay diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 018ef72e2..7bd66b87f 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -13,15 +13,12 @@ class TextEditorPresenter minimumReflowInterval: 200 constructor: (params) -> - {@model, @config, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft, @scrollColumn, @scrollRow, @boundingClientRect, @windowWidth, @windowHeight, @gutterWidth} = params - {horizontalScrollbarHeight, verticalScrollbarWidth} = params - {@lineHeight, @baseCharacterWidth, @backgroundColor, @gutterBackgroundColor, @tileSize} = params - {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @focused} = params - @measuredHorizontalScrollbarHeight = horizontalScrollbarHeight - @measuredVerticalScrollbarWidth = verticalScrollbarWidth - @gutterWidth ?= 0 - @tileSize ?= 6 + {@model, @config} = params + {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @tileSize} = params + {@contentFrameWidth} = params + @gutterWidth = 0 + @tileSize ?= 6 @realScrollTop = @scrollTop @realScrollLeft = @scrollLeft @disposables = new CompositeDisposable @@ -77,7 +74,6 @@ class TextEditorPresenter @updateVerticalDimensions() @updateScrollbarDimensions() - @restoreScrollPosition() @commitPendingLogicalScrollTopPosition() @commitPendingScrollTopPosition() @@ -218,6 +214,7 @@ class TextEditorPresenter @disposables.add @model.onDidAddCursor(@didAddCursor.bind(this)) @disposables.add @model.onDidRequestAutoscroll(@requestAutoscroll.bind(this)) + @disposables.add @model.onDidChangeFirstVisibleScreenRow(@didChangeFirstVisibleScreenRow.bind(this)) @observeCursor(cursor) for cursor in @model.getCursors() @disposables.add @model.onDidAddGutter(@didAddGutter.bind(this)) return @@ -778,8 +775,7 @@ class TextEditorPresenter if scrollTop isnt @realScrollTop and not Number.isNaN(scrollTop) @realScrollTop = scrollTop @scrollTop = Math.round(scrollTop) - @scrollRow = Math.round(@scrollTop / @lineHeight) - @model.setScrollRow(@scrollRow) + @model.setFirstVisibleScreenRow(Math.round(@scrollTop / @lineHeight), true) @updateStartRow() @updateEndRow() @@ -795,8 +791,7 @@ class TextEditorPresenter if scrollLeft isnt @realScrollLeft and not Number.isNaN(scrollLeft) @realScrollLeft = scrollLeft @scrollLeft = Math.round(scrollLeft) - @scrollColumn = Math.round(@scrollLeft / @baseCharacterWidth) - @model.setScrollColumn(@scrollColumn) + @model.setFirstVisibleScreenColumn(Math.round(@scrollLeft / @baseCharacterWidth)) @emitter.emit 'did-change-scroll-left', @scrollLeft @@ -1095,6 +1090,7 @@ class TextEditorPresenter setLineHeight: (lineHeight) -> unless @lineHeight is lineHeight @lineHeight = lineHeight + @restoreScrollTopIfNeeded() @model.setLineHeightInPixels(lineHeight) @shouldUpdateHeightState = true @shouldUpdateHorizontalScrollState = true @@ -1122,6 +1118,7 @@ class TextEditorPresenter @halfWidthCharWidth = halfWidthCharWidth @koreanCharWidth = koreanCharWidth @model.setDefaultCharWidth(baseCharacterWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) + @restoreScrollLeftIfNeeded() @characterWidthsChanged() characterWidthsChanged: -> @@ -1433,6 +1430,9 @@ class TextEditorPresenter @emitDidUpdateState() + didChangeFirstVisibleScreenRow: (screenRow) -> + @updateScrollTop(screenRow * @lineHeight) + getVerticalScrollMarginInPixels: -> Math.round(@model.getVerticalScrollMargin() * @lineHeight) @@ -1512,14 +1512,6 @@ class TextEditorPresenter @updateScrollTop(@pendingScrollTop) @pendingScrollTop = null - restoreScrollPosition: -> - return if @hasRestoredScrollPosition or not @hasPixelPositionRequirements() - - @setScrollTop(@scrollRow * @lineHeight) if @scrollRow? - @setScrollLeft(@scrollColumn * @baseCharacterWidth) if @scrollColumn? - - @hasRestoredScrollPosition = true - clearPendingScrollPosition: -> @pendingScrollLogicalPosition = null @pendingScrollTop = null @@ -1531,6 +1523,14 @@ class TextEditorPresenter canScrollTopTo: (scrollTop) -> @scrollTop isnt @constrainScrollTop(scrollTop) + restoreScrollTopIfNeeded: -> + unless @scrollTop? + @updateScrollTop(@model.getFirstVisibleScreenRow() * @lineHeight) + + restoreScrollLeftIfNeeded: -> + unless @scrollLeft? + @updateScrollLeft(@model.getFirstVisibleScreenColumn() * @baseCharacterWidth) + onDidChangeScrollTop: (callback) -> @emitter.on 'did-change-scroll-top', callback diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 630d5071c..84846a359 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -54,13 +54,11 @@ GutterContainer = require './gutter-container' # soft wraps and folds to ensure your code interacts with them correctly. module.exports = class TextEditor extends Model - callDisplayBufferCreatedHook: false buffer: null languageMode: null cursors: null selections: null suppressSelectionMerging: false - updateBatchDepth: 0 selectionFlashDuration: 500 gutterContainer: null @@ -90,7 +88,7 @@ class TextEditor extends Model super { - @softTabs, @scrollRow, @scrollColumn, initialLine, initialColumn, tabLength, + @softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, tabLength, softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation, @mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config, @notificationManager, @packageManager, @clipboard, @viewRegistry, @grammarRegistry, @@ -106,6 +104,8 @@ class TextEditor extends Model throw new Error("Must pass a project parameter when constructing TextEditors") unless @project? throw new Error("Must pass an assert parameter when constructing TextEditors") unless @assert? + @firstVisibleScreenRow ?= 0 + @firstVisibleScreenColumn ?= 0 @emitter = new Emitter @disposables = new CompositeDisposable @cursors = [] @@ -146,8 +146,8 @@ class TextEditor extends Model deserializer: 'TextEditor' id: @id softTabs: @softTabs - scrollRow: @getScrollRow() - scrollColumn: @getScrollColumn() + firstVisibleScreenRow: @getFirstVisibleScreenRow() + firstVisibleScreenColumn: @getFirstVisibleScreenColumn() displayBuffer: @displayBuffer.serialize() selectionsMarkerLayerId: @selectionsMarkerLayer.id @@ -453,6 +453,9 @@ class TextEditor extends Model onDidChangeCharacterWidths: (callback) -> @displayBuffer.onDidChangeCharacterWidths(callback) + onDidChangeFirstVisibleScreenRow: (callback, fromView) -> + @emitter.on 'did-change-first-visible-screen-row', callback + onDidChangeScrollTop: (callback) -> Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollTop instead.") @@ -583,18 +586,18 @@ class TextEditor extends Model else 'untitled' - # Essential: Get unique title for display in other parts of the UI - # such as the window title. + # Essential: Get unique title for display in other parts of the UI, such as + # the window title. # # If the editor's buffer is unsaved, its title is "untitled" # If the editor's buffer is saved, its unique title is formatted as one # of the following, # * "" when it is the only editing buffer with this file name. # * "/.../", where the "..." may be omitted - # if the the direct parent directory is already different. + # if the the direct parent directory is already different. # # Returns a {String} - getUniqueTitle: -> + getLongTitle: -> if sessionPath = @getPath() title = @getTitle() @@ -622,22 +625,6 @@ class TextEditor extends Model else 'untitled' - # Essential: Get the editor's long title for display in other parts of the UI - # such as the window title. - # - # If the editor's buffer is saved, its long title is formatted as - # " - ". If it is unsaved, its title is "untitled" - # - # Returns a {String}. - getLongTitle: -> - if sessionPath = @getPath() - fileName = path.basename(sessionPath) - directory = @project.relativize(path.dirname(sessionPath)) - directory = if directory.length > 0 then directory else path.basename(path.dirname(sessionPath)) - "#{fileName} - #{directory}" - else - 'untitled' - # Essential: Returns the {String} path of this editor's text buffer. getPath: -> @buffer.getPath() @@ -3130,14 +3117,6 @@ class TextEditor extends Model @placeholderText = placeholderText @emitter.emit 'did-change-placeholder-text', @placeholderText - getFirstVisibleScreenRow: -> - deprecate("This is now a view method. Call TextEditorElement::getFirstVisibleScreenRow instead.") - @viewRegistry.getView(this).getVisibleRowRange()[0] - - getLastVisibleScreenRow: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getLastVisibleScreenRow instead.") - @viewRegistry.getView(this).getVisibleRowRange()[1] - pixelPositionForBufferPosition: (bufferPosition) -> Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead") @viewRegistry.getView(this).pixelPositionForBufferPosition(bufferPosition) @@ -3192,11 +3171,40 @@ class TextEditor extends Model Grim.deprecate("This is now a view method. Call TextEditorElement::getWidth instead.") @displayBuffer.getWidth() - getScrollRow: -> @scrollRow - setScrollRow: (@scrollRow) -> + # Experimental: Scroll the editor such that the given screen row is at the + # top of the visible area. + setFirstVisibleScreenRow: (screenRow, fromView) -> + unless fromView + maxScreenRow = @getLineCount() - 1 + unless @config.get('editor.scrollPastEnd') + height = @displayBuffer.getHeight() + lineHeightInPixels = @displayBuffer.getLineHeightInPixels() + if height? and lineHeightInPixels? + maxScreenRow -= Math.floor(height / lineHeightInPixels) + screenRow = Math.max(Math.min(screenRow, maxScreenRow), 0) - getScrollColumn: -> @scrollColumn - setScrollColumn: (@scrollColumn) -> + unless screenRow is @firstVisibleScreenRow + @firstVisibleScreenRow = screenRow + @emitter.emit 'did-change-first-visible-screen-row', screenRow unless fromView + + getFirstVisibleScreenRow: -> @firstVisibleScreenRow + + getLastVisibleScreenRow: -> + height = @displayBuffer.getHeight() + lineHeightInPixels = @displayBuffer.getLineHeightInPixels() + if height? and lineHeightInPixels? + Math.min(@firstVisibleScreenRow + Math.floor(height / lineHeightInPixels), @getLineCount() - 1) + else + null + + getVisibleRowRange: -> + if lastVisibleScreenRow = @getLastVisibleScreenRow() + [@firstVisibleScreenRow, lastVisibleScreenRow] + else + null + + setFirstVisibleScreenColumn: (@firstVisibleScreenColumn) -> + getFirstVisibleScreenColumn: -> @firstVisibleScreenColumn getScrollTop: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollTop instead.") @@ -3253,11 +3261,6 @@ class TextEditor extends Model @viewRegistry.getView(this).getMaxScrollTop() - getVisibleRowRange: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getVisibleRowRange instead.") - - @viewRegistry.getView(this).getVisibleRowRange() - intersectsVisibleRowRange: (startRow, endRow) -> Grim.deprecate("This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.") diff --git a/src/workspace.coffee b/src/workspace.coffee index 80ef47c21..396008201 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -468,7 +468,7 @@ class Workspace extends Model when 'EACCES' @notificationManager.addWarning("Permission denied '#{error.path}'") return Promise.resolve() - when 'EPERM', 'EBUSY', 'ENXIO', 'EIO', 'ENOTCONN', 'UNKNOWN', 'ECONNRESET', 'EINVAL' + when 'EPERM', 'EBUSY', 'ENXIO', 'EIO', 'ENOTCONN', 'UNKNOWN', 'ECONNRESET', 'EINVAL', 'EMFILE', 'ENOTDIR' @notificationManager.addWarning("Unable to open '#{error.path ? uri}'", detail: error.message) return Promise.resolve() else