diff --git a/README.md b/README.md index 4fd06f851..7ebdf9252 100644 --- a/README.md +++ b/README.md @@ -24,18 +24,8 @@ Atom will automatically update when a new release is available. You can also download an `atom-windows.zip` file from the [releases page](https://github.com/atom/atom/releases/latest). The `.zip` version will not automatically update. -#### Uninstalling Chocolatey Version - -The recommended installation of Atom on Windows used to be using [Chocolatey](https://chocolatey.org/packages/Atom/). -This is no longer recommended now that the Atom Windows installer & auto-updater -exists. - -To switch from Chocolatey to the new installer: - * Upgrade to Atom 0.155 or above by running `cup Atom` - * Run `cuninst Atom` to uninstall the Chocolatey version of Atom - * This will not delete any of your installed packages or Atom config files. - * Download the latest [AtomSetup.exe installer](https://github.com/atom/atom/releases/latest). - * Double-click the downloaded file to install Atom +Using [chocolatey](https://chocolatey.org/)? Run `cinst Atom` to install +the latest version of Atom. ### Debian Linux (Ubuntu) diff --git a/apm/package.json b/apm/package.json index b90f3a05e..c070e36c3 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "0.134.0" + "atom-package-manager": "0.135.0" } } diff --git a/atom.sh b/atom.sh index 2f3d59c82..ecd7da052 100755 --- a/atom.sh +++ b/atom.sh @@ -75,9 +75,9 @@ elif [ $OS == 'Linux' ]; then SCRIPT=$(readlink -f "$0") USR_DIRECTORY=$(readlink -f $(dirname $SCRIPT)/..) ATOM_PATH="$USR_DIRECTORY/share/atom/atom" - DOT_ATOM_DIR="$HOME/.atom" + ATOM_HOME="${ATOM_HOME:-$HOME/.atom}" - mkdir -p "$DOT_ATOM_DIR" + mkdir -p "$ATOM_HOME" : ${TMPDIR:=/tmp} @@ -88,9 +88,9 @@ elif [ $OS == 'Linux' ]; then exit $? else ( - nohup "$ATOM_PATH" --executed-from="$(pwd)" --pid=$$ "$@" > "$DOT_ATOM_DIR/nohup.out" 2>&1 + nohup "$ATOM_PATH" --executed-from="$(pwd)" --pid=$$ "$@" > "$ATOM_HOME/nohup.out" 2>&1 if [ $? -ne 0 ]; then - cat "$DOT_ATOM_DIR/nohup.out" + cat "$ATOM_HOME/nohup.out" exit $? fi ) & diff --git a/build/tasks/build-task.coffee b/build/tasks/build-task.coffee index f49e1ac49..fcb1eb6a8 100644 --- a/build/tasks/build-task.coffee +++ b/build/tasks/build-task.coffee @@ -160,6 +160,9 @@ module.exports = (grunt) -> cp path.join('resources', 'win', 'atom.js'), path.join(shellAppDir, 'resources', 'cli', 'atom.js') cp path.join('resources', 'win', 'apm.sh'), path.join(shellAppDir, 'resources', 'cli', 'apm.sh') + if process.platform is 'linux' + cp path.join('resources', 'linux', 'icons'), path.join(buildDir, 'icons') + dependencies = ['compile', 'generate-license:save', 'generate-module-cache', 'compile-packages-slug'] dependencies.push('copy-info-plist') if process.platform is 'darwin' dependencies.push('set-exe-icon') if process.platform is 'win32' diff --git a/build/tasks/install-task.coffee b/build/tasks/install-task.coffee index ec99ad967..5131f512c 100644 --- a/build/tasks/install-task.coffee +++ b/build/tasks/install-task.coffee @@ -47,9 +47,9 @@ module.exports = (grunt) -> {description} = grunt.file.readJSON('package.json') iconName = path.join(shareDir, 'resources', 'app', 'resources', 'atom.png') - installDir = path.join(installDir, '.') # To prevent "Exec=/usr/local//share/atom/atom" + executable = path.join(shareDir, 'atom') template = _.template(String(fs.readFileSync(desktopFile))) - filled = template({description, installDir, iconName}) + filled = template({description, iconName, executable}) grunt.file.write(desktopInstallFile, filled) diff --git a/build/tasks/mkdeb-task.coffee b/build/tasks/mkdeb-task.coffee index cfe04f429..9fb8c7e4b 100644 --- a/build/tasks/mkdeb-task.coffee +++ b/build/tasks/mkdeb-task.coffee @@ -36,8 +36,9 @@ module.exports = (grunt) -> maintainer = 'GitHub ' installDir = '/usr' iconName = 'atom' + executable = path.join(installDir, 'share', 'atom', 'atom') getInstalledSize buildDir, (error, installedSize) -> - data = {name, version, description, section, arch, maintainer, installDir, iconName, installedSize} + data = {name, version, description, section, arch, maintainer, installDir, iconName, installedSize, executable} controlFilePath = fillTemplate(path.join('resources', 'linux', 'debian', 'control'), data) desktopFilePath = fillTemplate(path.join('resources', 'linux', 'atom.desktop'), data) icon = path.join('resources', 'atom.png') diff --git a/build/tasks/mkrpm-task.coffee b/build/tasks/mkrpm-task.coffee index 8590049b5..8436fea7a 100644 --- a/build/tasks/mkrpm-task.coffee +++ b/build/tasks/mkrpm-task.coffee @@ -32,9 +32,10 @@ module.exports = (grunt) -> installDir = grunt.config.get('atom.installDir') shareDir = path.join(installDir, 'share', 'atom') - iconName = path.join(shareDir, 'resources', 'app', 'resources', 'atom.png') + iconName = 'atom' + executable = 'atom' - data = {name, version, description, installDir, iconName} + data = {name, version, description, installDir, iconName, executable} specFilePath = fillTemplate(path.join('resources', 'linux', 'redhat', 'atom.spec'), data) desktopFilePath = fillTemplate(path.join('resources', 'linux', 'atom.desktop'), data) diff --git a/docs/advanced/keymaps.md b/docs/advanced/keymaps.md index 43d0c0e96..ee966a45d 100644 --- a/docs/advanced/keymaps.md +++ b/docs/advanced/keymaps.md @@ -16,7 +16,7 @@ keystrokes pass through `atom-text-editor` elements: 'ctrl-shift-e': 'editor:select-to-end-of-line' 'cmd-left': 'editor:move-to-first-character-of-line' -'atom-text-editor:not([mini])' +'atom-text-editor:not([mini])': 'cmd-alt-[': 'editor:fold-current-row' 'cmd-alt-]': 'editor:unfold-current-row' ``` diff --git a/docs/customizing-atom.md b/docs/customizing-atom.md index d2151f350..d56bd640a 100644 --- a/docs/customizing-atom.md +++ b/docs/customizing-atom.md @@ -100,6 +100,14 @@ namespaces: `core` and `editor`. You can open this file in an editor from the _Atom > Open Your Config_ menu. +### Custom Configuration Location + +You can override the location that Atom stores configuration files and folders +in by setting the `ATOM_HOME` environment variable. The `ATOM_HOME` path will be +used instead of `~/.atom` when it is set. + +This option can be useful when you want to make Atom portable across machines. + ### Configuration Key Reference - `core` diff --git a/docs/index.md b/docs/index.md index 87e198e09..e034e5784 100644 --- a/docs/index.md +++ b/docs/index.md @@ -25,5 +25,6 @@ ### Upgrading to 1.0 APIs +* [Upgrading Your Package](upgrading/upgrading-your-package.md) * [Upgrading Your UI Theme Or Package Selectors](upgrading/upgrading-your-ui-theme.md) * [Upgrading Your Syntax Theme](upgrading/upgrading-your-syntax-theme.md) diff --git a/docs/upgrading/upgrading-your-package.md b/docs/upgrading/upgrading-your-package.md index 9bde475ef..e7d603a36 100644 --- a/docs/upgrading/upgrading-your-package.md +++ b/docs/upgrading/upgrading-your-package.md @@ -1,4 +1,4 @@ -# Upgrading your package to 1.0 APIs +# Upgrading Your Package Atom is rapidly approaching 1.0. Much of the effort leading up to the 1.0 has been cleaning up APIs in an attempt to future proof, and make a more pleasant experience developing packages. @@ -63,7 +63,7 @@ When you are deprecation free and all done converting, upgrade the `engines` fie ```json { "engines": { - "atom": ">=0.174.0, <2.0.0" + "atom": ">=0.174.0 <2.0.0" } } ``` @@ -603,7 +603,7 @@ atom.workspaceView.command 'core:close core:cancel', -> ## Upgrading your stylesheet's selectors -Many selectors have changed, and we have introduced the [Shadow DOM][shadowdom] to the editor. See [Upgrading Your Package Selectors guide][upgrading-selectors] for more information in upgrading your package stylesheets. +Many selectors have changed, and we have introduced the [Shadow DOM][shadowdom] to the editor. See the [Upgrading Your UI Theme And Package Selectors guide][upgrading-selectors] for more information in upgrading your package stylesheets. ## Help us improve this guide! @@ -620,6 +620,6 @@ Did you hit something painful that wasn't in here? Want to reword some bit of it [texteditor]:https://atom.io/docs/api/latest/TextEditor [disposable]:https://atom.io/docs/api/latest/Disposable [commands-add]:https://atom.io/docs/api/latest/CommandRegistry#instance-add -[upgrading-selectors]:upgrading-your-ui-theme +[upgrading-selectors]:https://atom.io/docs/latest/upgrading/upgrading-your-ui-theme [shadowdom]:http://blog.atom.io/2014/11/18/avoiding-style-pollution-with-the-shadow-dom.html [guide]:https://github.com/atom/atom/blob/master/docs/upgrading/upgrading-your-package.md diff --git a/package.json b/package.json index f1db49733..3cb32c233 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "emissary": "^1.3.1", "event-kit": "^1.0.2", "first-mate": "^3.0.0", - "fs-plus": "^2.3.2", + "fs-plus": "^2.5", "fstream": "0.1.24", "fuzzaldrin": "^2.1", "git-utils": "^3.0.0", @@ -71,20 +71,20 @@ }, "packageDependencies": { "atom-dark-syntax": "0.26.0", - "atom-dark-ui": "0.46.0", + "atom-dark-ui": "0.47.0", "atom-light-syntax": "0.26.0", - "atom-light-ui": "0.40.0", + "atom-light-ui": "0.41.0", "base16-tomorrow-dark-theme": "0.25.0", "base16-tomorrow-light-theme": "0.8.0", - "one-dark-ui": "0.3.0", + "one-dark-ui": "0.4.0", "one-dark-syntax": "0.3.0", "one-light-syntax": "0.3.0", - "one-light-ui": "0.2.0", + "one-light-ui": "0.3.0", "solarized-dark-syntax": "0.32.0", "solarized-light-syntax": "0.19.0", "archive-view": "0.46.0", "autocomplete": "0.44.0", - "autoflow": "0.21.0", + "autoflow": "0.22.0", "autosave": "0.20.0", "background-tips": "0.22.0", "bookmarks": "0.35.0", @@ -103,8 +103,8 @@ "incompatible-packages": "0.21.0", "keybinding-resolver": "0.27.0", "link": "0.30.0", - "markdown-preview": "0.132.0", - "metrics": "0.41.0", + "markdown-preview": "0.134.0", + "metrics": "0.42.0", "notifications": "0.26.0", "open-on-github": "0.32.0", "package-generator": "0.37.0", @@ -116,20 +116,21 @@ "styleguide": "0.43.0", "symbols-view": "0.81.0", "tabs": "0.64.0", - "timecop": "0.28.0", + "timecop": "0.29.0", "tree-view": "0.154.0", "update-package-dependencies": "0.8.0", "welcome": "0.21.0", "whitespace": "0.28.0", "wrap-guide": "0.31.0", "language-c": "0.38.0", - "language-clojure": "0.10.0", + "language-clojure": "0.11.0", "language-coffee-script": "0.39.0", - "language-css": "0.27.0", + "language-csharp": "0.5.0", + "language-css": "0.28.0", "language-gfm": "0.63.0", "language-git": "0.10.0", "language-go": "0.21.0", - "language-html": "0.28.0", + "language-html": "0.29.0", "language-hyperlink": "0.12.2", "language-java": "0.14.0", "language-javascript": "0.56.0", @@ -142,9 +143,9 @@ "language-php": "0.20.0", "language-property-list": "0.8.0", "language-python": "0.30.0", - "language-ruby": "0.47.0", + "language-ruby": "0.48.0", "language-ruby-on-rails": "0.18.0", - "language-sass": "0.31.0", + "language-sass": "0.33.0", "language-shellscript": "0.12.0", "language-source": "0.9.0", "language-sql": "0.14.0", diff --git a/resources/linux/atom.desktop.in b/resources/linux/atom.desktop.in index db28e7395..1969e3f26 100644 --- a/resources/linux/atom.desktop.in +++ b/resources/linux/atom.desktop.in @@ -2,7 +2,7 @@ Name=Atom Comment=<%= description %> GenericName=Text Editor -Exec=<%= installDir %>/share/atom/atom %U +Exec=<%= executable %> %U Icon=<%= iconName %> Type=Application StartupNotify=true diff --git a/resources/linux/icons/1024.png b/resources/linux/icons/1024.png new file mode 100644 index 000000000..a09f04805 Binary files /dev/null and b/resources/linux/icons/1024.png differ diff --git a/resources/linux/icons/128.png b/resources/linux/icons/128.png new file mode 100644 index 000000000..78948fb24 Binary files /dev/null and b/resources/linux/icons/128.png differ diff --git a/resources/linux/icons/16.png b/resources/linux/icons/16.png new file mode 100644 index 000000000..52df06fa2 Binary files /dev/null and b/resources/linux/icons/16.png differ diff --git a/resources/linux/icons/24.png b/resources/linux/icons/24.png new file mode 100644 index 000000000..0bf12cbb0 Binary files /dev/null and b/resources/linux/icons/24.png differ diff --git a/resources/linux/icons/256.png b/resources/linux/icons/256.png new file mode 100644 index 000000000..a98caeb7b Binary files /dev/null and b/resources/linux/icons/256.png differ diff --git a/resources/linux/icons/32.png b/resources/linux/icons/32.png new file mode 100644 index 000000000..a960acf00 Binary files /dev/null and b/resources/linux/icons/32.png differ diff --git a/resources/linux/icons/48.png b/resources/linux/icons/48.png new file mode 100644 index 000000000..196717e27 Binary files /dev/null and b/resources/linux/icons/48.png differ diff --git a/resources/linux/icons/512.png b/resources/linux/icons/512.png new file mode 100644 index 000000000..f1d35d951 Binary files /dev/null and b/resources/linux/icons/512.png differ diff --git a/resources/linux/icons/64.png b/resources/linux/icons/64.png new file mode 100644 index 000000000..b9fcf78ab Binary files /dev/null and b/resources/linux/icons/64.png differ diff --git a/resources/linux/redhat/atom.spec.in b/resources/linux/redhat/atom.spec.in index 42c06970d..369aeea70 100644 --- a/resources/linux/redhat/atom.spec.in +++ b/resources/linux/redhat/atom.spec.in @@ -1,27 +1,35 @@ Name: <%= name %> Version: <%= version %> Release: 0.1%{?dist} -Summary: Atom is a hackable text editor for the 21st century +Summary: <%= description %> License: MIT URL: https://atom.io/ AutoReqProv: no # Avoid libchromiumcontent.so missing dependency -Prefix: /usr/local +Prefix: <%= installDir %> %description <%= description %> %install -mkdir -p %{buildroot}/usr/local/share/atom -cp -r /tmp/atom-build/Atom/* %{buildroot}/usr/local/share/atom -mkdir -p %{buildroot}/usr/local/bin/ -ln -sf ../share/atom/resources/app/apm/node_modules/.bin/apm %{buildroot}/usr/local/bin/apm -cp atom.sh %{buildroot}/usr/local/bin/atom -chmod 755 atom.sh -mkdir -p %{buildroot}/usr/local/share/applications/ -mv atom.desktop %{buildroot}/usr/local/share/applications/ +mkdir -p %{buildroot}/<%= installDir %>/share/atom/ +cp -r Atom/* %{buildroot}/<%= installDir %>/share/atom/ +mkdir -p %{buildroot}/<%= installDir %>/bin/ +ln -sf ../share/atom/resources/app/apm/node_modules/.bin/apm %{buildroot}/<%= installDir %>/bin/apm +cp atom.sh %{buildroot}/<%= installDir %>/bin/atom +chmod 755 %{buildroot}/<%= installDir %>/bin/atom +mkdir -p %{buildroot}/<%= installDir %>/share/applications/ +cp atom.desktop %{buildroot}/<%= installDir %>/share/applications/ + +# copy over icons in sizes that most desktop environments like +for i in 1024 512 256 128 64 48 32 24 16 +do + mkdir -p %{buildroot}/<%= installDir %>/share/icons/hicolor/${i}x${i}/apps + cp icons/${i}.png %{buildroot}/<%= installDir %>/share/icons/hicolor/${i}x${i}/apps/atom.png +done %files -/usr/local/bin/atom -/usr/local/bin/apm -/usr/local/share/atom/ -/usr/local/share/applications/atom.desktop +<%= installDir %>/bin/atom +<%= installDir %>/bin/apm +<%= installDir %>/share/atom/ +<%= installDir %>/share/applications/atom.desktop +<%= installDir %>/share/icons/hicolor/ diff --git a/script/mkrpm b/script/mkrpm index 356f0bfc4..c9ba2d7f7 100755 --- a/script/mkrpm +++ b/script/mkrpm @@ -11,7 +11,8 @@ ARCH=`uname -m` rpmdev-setuptree -cp -r $BUILD_DIRECTORY/Atom/* $RPM_BUILD_ROOT/BUILD +cp -r $BUILD_DIRECTORY/Atom $RPM_BUILD_ROOT/BUILD +cp -r $BUILD_DIRECTORY/icons $RPM_BUILD_ROOT/BUILD cp $SPEC_FILE $RPM_BUILD_ROOT/SPECS cp ./atom.sh $RPM_BUILD_ROOT/BUILD cp $DESKTOP_FILE $RPM_BUILD_ROOT/BUILD diff --git a/script/rpmbuild b/script/rpmbuild index 9405fd585..0e0f4882c 100755 --- a/script/rpmbuild +++ b/script/rpmbuild @@ -3,4 +3,4 @@ set -e script/build -script/grunt mkrpm publish-build --stack +script/grunt mkrpm publish-build --stack --install-dir /usr diff --git a/spec/buffered-node-process-spec.coffee b/spec/buffered-node-process-spec.coffee new file mode 100644 index 000000000..1ea6074c5 --- /dev/null +++ b/spec/buffered-node-process-spec.coffee @@ -0,0 +1,39 @@ +path = require 'path' +BufferedNodeProcess = require '../src/buffered-node-process' + +describe "BufferedNodeProcess", -> + it "executes the script in a new process", -> + exit = jasmine.createSpy('exitCallback') + output = '' + stdout = (lines) -> output += lines + error = '' + stderr = (lines) -> error += lines + args = ['hi'] + command = path.join(__dirname, 'fixtures', 'script.js') + + new BufferedNodeProcess({command, args, stdout, stderr, exit}) + + waitsFor -> + exit.callCount is 1 + + runs -> + expect(output).toBe 'hi' + expect(error).toBe '' + expect(args).toEqual ['hi'] + + it "suppresses deprecations in the new process", -> + exit = jasmine.createSpy('exitCallback') + output = '' + stdout = (lines) -> output += lines + error = '' + stderr = (lines) -> error += lines + command = path.join(__dirname, 'fixtures', 'script-with-deprecations.js') + + new BufferedNodeProcess({command, stdout, stderr, exit}) + + waitsFor -> + exit.callCount is 1 + + runs -> + expect(output).toBe 'hi' + expect(error).toBe '' diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 15dc3b8fa..4abd3b0db 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -666,6 +666,23 @@ describe "Config", -> foo: bar: 'coffee' + describe "when an error is thrown writing the file to disk", -> + addErrorHandler = null + beforeEach -> + atom.notifications.onDidAddNotification addErrorHandler = jasmine.createSpy() + + it "creates a notification", -> + jasmine.unspy CSON, 'writeFileSync' + spyOn(CSON, 'writeFileSync').andCallFake -> + error = new Error() + error.code = 'EPERM' + error.path = atom.config.getUserConfigPath() + throw error + + save = -> atom.config.save() + expect(save).not.toThrow() + expect(addErrorHandler.callCount).toBe 1 + describe ".loadUserConfig()", -> beforeEach -> expect(fs.existsSync(atom.config.configDirPath)).toBeFalsy() diff --git a/spec/fixtures/script-with-deprecations.js b/spec/fixtures/script-with-deprecations.js new file mode 100644 index 000000000..4abe4774a --- /dev/null +++ b/spec/fixtures/script-with-deprecations.js @@ -0,0 +1,2 @@ +require('fs').existsSync('hi'); +process.stdout.write('hi'); diff --git a/spec/fixtures/script.js b/spec/fixtures/script.js new file mode 100644 index 000000000..94972dfb5 --- /dev/null +++ b/spec/fixtures/script.js @@ -0,0 +1 @@ +process.stdout.write(process.argv[2]); diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee index 3477acc52..ac6e1d26f 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -383,6 +383,25 @@ describe "Pane", -> pane.saveActiveItem() expect(atom.showSaveDialogSync).not.toHaveBeenCalled() + describe "when the item's saveAs method throws a well-known IO error", -> + notificationSpy = null + beforeEach -> + atom.notifications.onDidAddNotification notificationSpy = jasmine.createSpy() + + it "creates a notification", -> + pane.getActiveItem().saveAs = -> + error = new Error("EACCES, permission denied '/foo'") + error.path = '/foo' + error.code = 'EACCES' + throw error + + pane.saveActiveItem() + expect(notificationSpy).toHaveBeenCalled() + notification = notificationSpy.mostRecentCall.args[0] + expect(notification.getType()).toBe 'warning' + expect(notification.getMessage()).toContain 'Permission denied' + expect(notification.getMessage()).toContain '/foo' + describe "::saveActiveItemAs()", -> pane = null @@ -404,6 +423,25 @@ describe "Pane", -> pane.saveActiveItemAs() expect(atom.showSaveDialogSync).not.toHaveBeenCalled() + describe "when the item's saveAs method throws a well-known IO error", -> + notificationSpy = null + beforeEach -> + atom.notifications.onDidAddNotification notificationSpy = jasmine.createSpy() + + it "creates a notification", -> + pane.getActiveItem().saveAs = -> + error = new Error("EACCES, permission denied '/foo'") + error.path = '/foo' + error.code = 'EACCES' + throw error + + pane.saveActiveItemAs() + expect(notificationSpy).toHaveBeenCalled() + notification = notificationSpy.mostRecentCall.args[0] + expect(notification.getType()).toBe 'warning' + expect(notification.getMessage()).toContain 'Permission denied' + expect(notification.getMessage()).toContain '/foo' + describe "::itemForURI(uri)", -> it "returns the item for which a call to .getURI() returns the given uri", -> pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C"), new Item("D")]) diff --git a/spec/window-spec.coffee b/spec/window-spec.coffee index 7e9370d50..1706e042e 100644 --- a/spec/window-spec.coffee +++ b/spec/window-spec.coffee @@ -1,5 +1,7 @@ {$, $$} = require '../src/space-pen-extensions' path = require 'path' +fs = require 'fs-plus' +temp = require 'temp' TextEditor = require '../src/text-editor' WindowEventHandler = require '../src/window-event-handler' @@ -54,7 +56,7 @@ describe "Window", -> jasmine.unspy(TextEditor.prototype, "shouldPromptToSave") beforeUnloadEvent = $.Event(new Event('beforeunload')) - describe "when pane items are are modified", -> + describe "when pane items are modified", -> it "prompts user to save and calls atom.workspace.confirmClose", -> editor = null spyOn(atom.workspace, 'confirmClose').andCallThrough() @@ -92,6 +94,25 @@ describe "Window", -> $(window).trigger(beforeUnloadEvent) expect(atom.confirm).toHaveBeenCalled() + describe "when the same path is modified in multiple panes", -> + it "prompts to save the item", -> + editor = null + filePath = path.join(temp.mkdirSync('atom-file'), 'file.txt') + fs.writeFileSync(filePath, 'hello') + spyOn(atom.workspace, 'confirmClose').andCallThrough() + spyOn(atom, 'confirm').andReturn(0) + + waitsForPromise -> + atom.workspace.open(filePath).then (o) -> editor = o + + runs -> + atom.workspace.getActivePane().splitRight(copyActiveItem: true) + editor.setText('world') + $(window).trigger(beforeUnloadEvent) + expect(atom.workspace.confirmClose).toHaveBeenCalled() + expect(atom.confirm.callCount).toBe 1 + expect(fs.readFileSync(filePath, 'utf8')).toBe 'world' + describe ".unloadEditorWindow()", -> it "saves the serialized state of the window so it can be deserialized after reload", -> workspaceState = atom.workspace.serialize() diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index b9642e17d..82526a299 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -955,9 +955,14 @@ describe "Workspace", -> expect(editor.isModified()).toBeTruthy() describe "::saveActivePaneItem()", -> + editor = null + beforeEach -> + waitsForPromise -> + atom.workspace.open('sample.js').then (o) -> editor = o + describe "when there is an error", -> it "emits a warning notification when the file cannot be saved", -> - spyOn(Pane::, 'saveActiveItem').andCallFake -> + spyOn(editor, 'save').andCallFake -> throw new Error("'/some/file' is a directory") atom.notifications.onDidAddNotification addedSpy = jasmine.createSpy() @@ -966,7 +971,7 @@ describe "Workspace", -> expect(addedSpy.mostRecentCall.args[0].getType()).toBe 'warning' it "emits a warning notification when the directory cannot be written to", -> - spyOn(Pane::, 'saveActiveItem').andCallFake -> + spyOn(editor, 'save').andCallFake -> throw new Error("ENOTDIR, not a directory '/Some/dir/and-a-file.js'") atom.notifications.onDidAddNotification addedSpy = jasmine.createSpy() @@ -975,7 +980,7 @@ describe "Workspace", -> expect(addedSpy.mostRecentCall.args[0].getType()).toBe 'warning' it "emits a warning notification when the user does not have permission", -> - spyOn(Pane::, 'saveActiveItem').andCallFake -> + spyOn(editor, 'save').andCallFake -> error = new Error("EACCES, permission denied '/Some/dir/and-a-file.js'") error.code = 'EACCES' error.path = '/Some/dir/and-a-file.js' @@ -987,14 +992,14 @@ describe "Workspace", -> expect(addedSpy.mostRecentCall.args[0].getType()).toBe 'warning' it "emits a warning notification when the operation is not permitted", -> - spyOn(Pane::, 'saveActiveItem').andCallFake -> + spyOn(editor, 'save').andCallFake -> error = new Error("EPERM, operation not permitted '/Some/dir/and-a-file.js'") error.code = 'EPERM' error.path = '/Some/dir/and-a-file.js' throw error it "emits a warning notification when the file is already open by another app", -> - spyOn(Pane::, 'saveActiveItem').andCallFake -> + spyOn(editor, 'save').andCallFake -> error = new Error("EBUSY, resource busy or locked '/Some/dir/and-a-file.js'") error.code = 'EBUSY' error.path = '/Some/dir/and-a-file.js' @@ -1009,7 +1014,7 @@ describe "Workspace", -> expect(notificaiton.getMessage()).toContain 'Unable to save' it "emits a warning notification when the file system is read-only", -> - spyOn(Pane::, 'saveActiveItem').andCallFake -> + spyOn(editor, 'save').andCallFake -> error = new Error("EROFS, read-only file system '/Some/dir/and-a-file.js'") error.code = 'EROFS' error.path = '/Some/dir/and-a-file.js' @@ -1024,7 +1029,7 @@ describe "Workspace", -> expect(notification.getMessage()).toContain 'Unable to save' it "emits a warning notification when the file cannot be saved", -> - spyOn(Pane::, 'saveActiveItem').andCallFake -> + spyOn(editor, 'save').andCallFake -> throw new Error("no one knows") save = -> atom.workspace.saveActivePaneItem() diff --git a/src/6to5.coffee b/src/6to5.coffee index f0e1dc7c1..15059ad62 100644 --- a/src/6to5.coffee +++ b/src/6to5.coffee @@ -96,7 +96,7 @@ getCachePath = (sourceCode) -> unless jsCacheDir? to5Version = require('6to5-core/package.json').version - cacheDir = path.join(fs.absolute('~/.atom'), 'compile-cache') + cacheDir = path.join(process.env.ATOM_HOME, 'compile-cache') jsCacheDir = path.join(cacheDir, 'js', '6to5', create6to5VersionAndOptionsDigest(to5Version, defaultOptions)) path.join(jsCacheDir, "#{digest}.js") diff --git a/src/atom.coffee b/src/atom.coffee index a1818b138..0036ba420 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -109,7 +109,7 @@ class Atom extends Model # # Returns the absolute path to ~/.atom @getConfigDirPath: -> - @configDirPath ?= fs.absolute('~/.atom') + @configDirPath ?= process.env.ATOM_HOME # Get the path to Atom's storage directory. # @@ -263,9 +263,6 @@ class Atom extends Model # Make react.js faster process.env.NODE_ENV ?= 'production' unless devMode - # Set Atom's home so packages don't have to guess it - process.env.ATOM_HOME = configDirPath - @config = new Config({configDirPath, resourcePath}) @keymaps = new KeymapManager({configDirPath, resourcePath}) @keymap = @keymaps # Deprecated diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index 9e34421d4..bd4303ae9 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -5,7 +5,7 @@ AutoUpdateManager = require './auto-update-manager' BrowserWindow = require 'browser-window' Menu = require 'menu' app = require 'app' -fs = require 'fs' +fs = require 'fs-plus' ipc = require 'ipc' path = require 'path' os = require 'os' @@ -342,6 +342,7 @@ class AtomApplication # :window - {AtomWindow} to open file paths in. openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, window}={}) -> {pathToOpen, initialLine, initialColumn} = @locationForPathToOpen(pathToOpen) + pathToOpen = fs.normalize(pathToOpen) unless pidToKillWhenClosed or newWindow pathToOpenStat = fs.statSyncNoException(pathToOpen) @@ -414,9 +415,8 @@ class AtomApplication openUrl: ({urlToOpen, devMode, safeMode}) -> unless @packages? PackageManager = require '../package-manager' - fs = require 'fs-plus' @packages = new PackageManager - configDirPath: fs.absolute('~/.atom') + configDirPath: process.env.ATOM_HOME devMode: devMode resourcePath: @resourcePath diff --git a/src/browser/main.coffee b/src/browser/main.coffee index 035b34db0..48ff85f3f 100644 --- a/src/browser/main.coffee +++ b/src/browser/main.coffee @@ -2,7 +2,7 @@ global.shellStartTime = Date.now() crashReporter = require 'crash-reporter' app = require 'app' -fs = require 'fs' +fs = require 'fs-plus' path = require 'path' optimist = require 'optimist' nslog = require 'nslog' @@ -14,6 +14,7 @@ process.on 'uncaughtException', (error={}) -> nslog(error.stack) if error.stack? start = -> + setupAtomHome() if process.platform is 'win32' SquirrelUpdate = require './squirrel-update' squirrelCommand = process.argv[1] @@ -42,10 +43,11 @@ start = -> cwd = args.executedFrom?.toString() or process.cwd() args.pathsToOpen = args.pathsToOpen.map (pathToOpen) -> + pathToOpen = fs.normalize(pathToOpen) if cwd - path.resolve(cwd, pathToOpen.toString()) + path.resolve(cwd, pathToOpen) else - path.resolve(pathToOpen.toString()) + path.resolve(pathToOpen) setupCoffeeScript() if args.devMode @@ -73,6 +75,14 @@ setupCoffeeScript = -> js = CoffeeScript.compile(coffee, filename: filePath) module._compile(js, filePath) +setupAtomHome = -> + return if process.env.ATOM_HOME + + atomHome = path.join(app.getHomeDir(), '.atom') + try + atomHome = fs.realpathSync(atomHome) + process.env.ATOM_HOME = atomHome + parseCommandLine = -> version = app.getVersion() options = optimist(process.argv[1..]) @@ -89,8 +99,12 @@ parseCommandLine = -> opened or a new window if it hasn't. Environment Variables: - ATOM_DEV_RESOURCE_PATH The path from which Atom loads source code in dev mode. - Defaults to `~/github/atom`. + + ATOM_DEV_RESOURCE_PATH The path from which Atom loads source code in dev mode. + Defaults to `~/github/atom`. + + ATOM_HOME The root path for all configuration files and folders. + Defaults to `~/.atom`. """ options.alias('d', 'dev').boolean('d').describe('d', 'Run in development mode.') options.alias('f', 'foreground').boolean('f').describe('f', 'Keep the browser process in the foreground.') diff --git a/src/buffered-node-process.coffee b/src/buffered-node-process.coffee index e9a3fbee4..bb1a1c655 100644 --- a/src/buffered-node-process.coffee +++ b/src/buffered-node-process.coffee @@ -48,5 +48,8 @@ class BufferedNodeProcess extends BufferedProcess options.env ?= Object.create(process.env) options.env['ATOM_SHELL_INTERNAL_RUN_AS_NODE'] = 1 + args = args?.slice() ? [] args.unshift(command) + args.unshift('--no-deprecation') + super({command: node, args, options, stdout, stderr, exit}) diff --git a/src/coffee-cache.coffee b/src/coffee-cache.coffee index 15892dd2f..1e06fd4d5 100644 --- a/src/coffee-cache.coffee +++ b/src/coffee-cache.coffee @@ -5,7 +5,7 @@ CoffeeScript = require 'coffee-script' CSON = require 'season' fs = require 'fs-plus' -cacheDir = path.join(fs.absolute('~/.atom'), 'compile-cache') +cacheDir = path.join(process.env.ATOM_HOME, 'compile-cache') stats = hits: 0 diff --git a/src/config.coffee b/src/config.coffee index d9695fb55..937bb1307 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -868,7 +868,12 @@ class Config save: -> allSettings = {'*': @settings} allSettings = _.extend allSettings, @scopedSettingsStore.propertiesForSource(@getUserConfigPath()) - CSON.writeFileSync(@configFilePath, allSettings) + try + CSON.writeFileSync(@configFilePath, allSettings) + catch error + message = "Failed to save `#{path.basename(@configFilePath)}`" + detail = error.message + @notifyFailure(message, detail) ### Section: Private methods managing global settings diff --git a/src/less-compile-cache.coffee b/src/less-compile-cache.coffee index e07072637..c70f312ee 100644 --- a/src/less-compile-cache.coffee +++ b/src/less-compile-cache.coffee @@ -1,14 +1,10 @@ path = require 'path' -fs = require 'fs-plus' LessCache = require 'less-cache' -{Subscriber} = require 'emissary' # {LessCache} wrapper used by {ThemeManager} to read stylesheets. module.exports = class LessCompileCache - Subscriber.includeInto(this) - - @cacheDir: path.join(require('./coffee-cache').cacheDir, 'less') + @cacheDir: path.join(process.env.ATOM_HOME, 'compile-cache', 'less') constructor: ({resourcePath, importPaths}) -> @lessSearchPaths = [ @@ -35,5 +31,3 @@ class LessCompileCache cssForFile: (stylesheetPath, lessContent) -> @cache.cssForFile(stylesheetPath, lessContent) - - destroy: -> @unsubscribe() diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee index 35c012936..1991cba95 100644 --- a/src/menu-manager.coffee +++ b/src/menu-manager.coffee @@ -109,17 +109,20 @@ class MenuManager # Simulate an atom-text-editor element attached to a atom-workspace element attached # to a body element that has the same classes as the current body element. unless @testEditor? - testBody = document.createElement('body') + # Use new document so that custom elements don't actually get created + testDocument = document.implementation.createDocument(document.namespaceURI, 'html') + + testBody = testDocument.createElement('body') testBody.classList.add(@classesForElement(document.body)...) - testWorkspace = document.createElement('div') + testWorkspace = testDocument.createElement('atom-workspace') workspaceClasses = @classesForElement(document.body.querySelector('atom-workspace')) workspaceClasses = ['workspace'] if workspaceClasses.length is 0 testWorkspace.classList.add(workspaceClasses...) testBody.appendChild(testWorkspace) - @testEditor = document.createElement('div') + @testEditor = testDocument.createElement('atom-text-editor') @testEditor.classList.add('editor') testWorkspace.appendChild(@testEditor) diff --git a/src/pane-container.coffee b/src/pane-container.coffee index f36169e3b..14fa25a44 100644 --- a/src/pane-container.coffee +++ b/src/pane-container.coffee @@ -152,12 +152,12 @@ class PaneContainer extends Model saveAll: -> pane.saveItems() for pane in @getPanes() - confirmClose: -> + confirmClose: (options) -> allSaved = true for pane in @getPanes() for item in pane.getItems() - unless pane.promptToSaveItem(item) + unless pane.promptToSaveItem(item, options) allSaved = false break diff --git a/src/pane.coffee b/src/pane.coffee index e2e61ca95..f1bc34abf 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -437,8 +437,8 @@ class Pane extends Model destroyInactiveItems: -> @destroyItem(item) for item in @getItems() when item isnt @activeItem - promptToSaveItem: (item) -> - return true unless item.shouldPromptToSave?() + promptToSaveItem: (item, options={}) -> + return true unless item.shouldPromptToSave?(options) if typeof item.getURI is 'function' uri = item.getURI() @@ -481,7 +481,10 @@ class Pane extends Model itemURI = item.getUri() if itemURI? - item.save?() + try + item.save?() + catch error + @handleSaveError(error) nextAction?() else @saveItemAs(item, nextAction) @@ -498,7 +501,10 @@ class Pane extends Model itemPath = item.getPath?() newItemPath = atom.showSaveDialogSync(itemPath) if newItemPath - item.saveAs(newItemPath) + try + item.saveAs(newItemPath) + catch error + @handleSaveError(error) nextAction?() # Public: Save all items. @@ -667,3 +673,18 @@ class Pane extends Model for item in @getItems() return false unless @promptToSaveItem(item) true + + handleSaveError: (error) -> + if error.message.endsWith('is a directory') + atom.notifications.addWarning("Unable to save file: #{error.message}") + else if error.code is 'EACCES' and error.path? + atom.notifications.addWarning("Unable to save file: Permission denied '#{error.path}'") + else if error.code in ['EPERM', 'EBUSY', 'UNKNOWN'] and error.path? + atom.notifications.addWarning("Unable to save file '#{error.path}'", detail: error.message) + else if error.code is 'EROFS' and error.path? + atom.notifications.addWarning("Unable to save file: Read-only file system '#{error.path}'") + else if errorMatch = /ENOTDIR, not a directory '([^']+)'/.exec(error.message) + fileName = errorMatch[1] + atom.notifications.addWarning("Unable to save file: A directory in the path '#{fileName}' could not be written to") + else + throw error diff --git a/src/text-editor.coffee b/src/text-editor.coffee index dbd1075c5..b86b97d17 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -641,7 +641,11 @@ class TextEditor extends Model # Determine whether the user should be prompted to save before closing # this editor. - shouldPromptToSave: -> @isModified() and not @buffer.hasMultipleEditors() + shouldPromptToSave: ({windowCloseRequested}={}) -> + if windowCloseRequested + @isModified() + else + @isModified() and not @buffer.hasMultipleEditors() ### Section: Reading Text diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index 37c04670b..d2a79b2a4 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -52,7 +52,7 @@ class WindowEventHandler @subscribe $(window), 'blur', -> document.body.classList.add('is-blurred') @subscribe $(window), 'beforeunload', => - confirmed = atom.workspace?.confirmClose() + confirmed = atom.workspace?.confirmClose(windowCloseRequested: true) atom.hide() if confirmed and not @reloadRequested and atom.getCurrentWindow().isWebViewFocused() @reloadRequested = false diff --git a/src/workspace.coffee b/src/workspace.coffee index fc2a12cdb..b2fc6fbf1 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -599,8 +599,8 @@ class Workspace extends Model saveAll: -> @paneContainer.saveAll() - confirmClose: -> - @paneContainer.confirmClose() + confirmClose: (options) -> + @paneContainer.confirmClose(options) # Save the active pane item. # @@ -609,7 +609,7 @@ class Workspace extends Model # {::saveActivePaneItemAs} # will be called instead. This method does nothing # if the active item does not implement a `.save` method. saveActivePaneItem: -> - @saveActivePaneItemAndReportErrors('saveActiveItem') + @getActivePane().saveActiveItem() # Prompt the user for a path and save the active pane item to it. # @@ -617,27 +617,7 @@ class Workspace extends Model # `.saveAs` on the item with the selected path. This method does nothing if # the active item does not implement a `.saveAs` method. saveActivePaneItemAs: -> - @saveActivePaneItemAndReportErrors('saveActiveItemAs') - - saveActivePaneItemAndReportErrors: (method) -> - try - @getActivePane()[method]() - catch error - if error.message.endsWith('is a directory') - atom.notifications.addWarning("Unable to save file: #{error.message}") - else if error.code is 'EACCES' and error.path? - atom.notifications.addWarning("Unable to save file: Permission denied '#{error.path}'") - else if error.code is 'EPERM' and error.path? - atom.notifications.addWarning("Unable to save file '#{error.path}'", detail: error.message) - else if error.code is 'EBUSY' and error.path? - atom.notifications.addWarning("Unable to save file '#{error.path}'", detail: error.message) - else if error.code is 'EROFS' and error.path? - atom.notifications.addWarning("Unable to save file: Read-only file system '#{error.path}'") - else if errorMatch = /ENOTDIR, not a directory '([^']+)'/.exec(error.message) - fileName = errorMatch[1] - atom.notifications.addWarning("Unable to save file: A directory in the path '#{fileName}' could not be written to") - else - throw error + @getActivePane().saveActiveItemAs() # Destroy (close) the active pane item. # diff --git a/static/index.js b/static/index.js index 12b149699..e5f84f794 100644 --- a/static/index.js +++ b/static/index.js @@ -13,6 +13,23 @@ window.onload = function() { var fs = require('fs'); var path = require('path'); + // Ensure ATOM_HOME is always set before anything else is required + if (!process.env.ATOM_HOME) { + var home; + if (process.platform === 'win32') { + home = process.env.USERPROFILE; + } else { + home = process.env.HOME; + } + var atomHome = path.join(home, '.atom'); + try { + atomHome = fs.realpathSync(atomHome); + } catch (error) { + // Ignore since the path might just not exist yet. + } + process.env.ATOM_HOME = atomHome; + } + // Skip "?loadSettings=". var rawLoadSettings = decodeURIComponent(location.search.substr(14)); var loadSettings;