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

-[](https://travis-ci.org/atom/atom)
+[](https://travis-ci.org/atom/atom) [](https://ci.appveyor.com/project/Atom/atom)
[](https://david-dm.org/atom/atom)
[](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