diff --git a/build/package.json b/build/package.json index ad483c75f..da11dc97a 100644 --- a/build/package.json +++ b/build/package.json @@ -8,7 +8,7 @@ "dependencies": { "asar": "^0.8.0", "async": "~0.2.9", - "donna": "1.0.10", + "donna": "^1.0.12", "formidable": "~1.0.14", "fs-plus": "2.x", "github-releases": "~0.3.0", diff --git a/build/tasks/spec-task.coffee b/build/tasks/spec-task.coffee index b0e695798..27465efdf 100644 --- a/build/tasks/spec-task.coffee +++ b/build/tasks/spec-task.coffee @@ -54,14 +54,14 @@ module.exports = (grunt) -> if process.platform in ['darwin', 'linux'] options = cmd: appPath - args: ['--test', "--resource-path=#{resourcePath}", "--spec-directory=#{path.join(packagePath, 'spec')}"] + args: ['--test', "--resource-path=#{resourcePath}", path.join(packagePath, 'spec')] opts: cwd: packagePath env: _.extend({}, process.env, ATOM_PATH: rootDir) else if process.platform is 'win32' options = cmd: process.env.comspec - args: ['/c', appPath, '--test', "--resource-path=#{resourcePath}", "--spec-directory=#{path.join(packagePath, 'spec')}", "--log-file=ci.log"] + args: ['/c', appPath, '--test', "--resource-path=#{resourcePath}", "--log-file=ci.log", path.join(packagePath, 'spec')] opts: cwd: packagePath env: _.extend({}, process.env, ATOM_PATH: rootDir) @@ -95,7 +95,7 @@ module.exports = (grunt) -> if process.platform in ['darwin', 'linux'] options = cmd: appPath - args: ['--test', "--resource-path=#{resourcePath}", "--spec-directory=#{coreSpecsPath}"] + args: ['--test', "--resource-path=#{resourcePath}", coreSpecsPath] opts: env: _.extend({}, process.env, ATOM_INTEGRATION_TESTS_ENABLED: true @@ -104,7 +104,7 @@ module.exports = (grunt) -> else if process.platform is 'win32' options = cmd: process.env.comspec - args: ['/c', appPath, '--test', "--resource-path=#{resourcePath}", "--spec-directory=#{coreSpecsPath}", '--log-file=ci.log'] + args: ['/c', appPath, '--test', "--resource-path=#{resourcePath}", '--log-file=ci.log', coreSpecsPath] opts: env: _.extend({}, process.env, ATOM_INTEGRATION_TESTS_ENABLED: true diff --git a/exports/atom.coffee b/exports/atom.coffee index 1ceb60fa9..c3601e1cc 100644 --- a/exports/atom.coffee +++ b/exports/atom.coffee @@ -2,6 +2,7 @@ TextBuffer = require 'text-buffer' {Point, Range} = TextBuffer {File, Directory} = require 'pathwatcher' {Emitter, Disposable, CompositeDisposable} = require 'event-kit' +Grim = require 'grim' module.exports = BufferedNodeProcess: require '../src/buffered-node-process' @@ -21,4 +22,20 @@ module.exports = # only be exported when not running as a child node process unless process.env.ATOM_SHELL_INTERNAL_RUN_AS_NODE module.exports.Task = require '../src/task' - module.exports.TextEditor = require '../src/text-editor' + + TextEditor = (params) -> + atom.workspace.buildTextEditor(params) + + TextEditor.prototype = require('../src/text-editor').prototype + + Object.defineProperty module.exports, 'TextEditor', + enumerable: true + get: -> + Grim.deprecate """ + The `TextEditor` constructor is no longer public. + + To construct a text editor, use `atom.workspace.buildTextEditor()`. + To check if an object is a text editor, look for for the existence of + a public method that you're using (e.g. `::getText`). + """ + TextEditor diff --git a/keymaps/darwin.cson b/keymaps/darwin.cson index 87c1eb9da..bf2e43a33 100644 --- a/keymaps/darwin.cson +++ b/keymaps/darwin.cson @@ -18,7 +18,6 @@ 'ctrl-d': 'core:delete' # Atom Specific - 'cmd-alt-ctrl-s': 'application:run-all-specs' 'enter': 'core:confirm' 'escape': 'core:cancel' 'up': 'core:move-up' diff --git a/keymaps/linux.cson b/keymaps/linux.cson index 9e739521c..28f43a8fc 100644 --- a/keymaps/linux.cson +++ b/keymaps/linux.cson @@ -9,7 +9,6 @@ 'ctrl-alt-r': 'window:reload' 'ctrl-shift-i': 'window:toggle-dev-tools' 'ctrl-alt-p': 'window:run-package-specs' - 'ctrl-alt-s': 'application:run-all-specs' 'ctrl-shift-o': 'application:open-folder' 'ctrl-alt-o': 'application:add-project-folder' 'ctrl-shift-pageup': 'pane:move-item-left' diff --git a/keymaps/win32.cson b/keymaps/win32.cson index b3643207f..f052a64fc 100644 --- a/keymaps/win32.cson +++ b/keymaps/win32.cson @@ -15,7 +15,6 @@ 'ctrl-alt-r': 'window:reload' 'ctrl-alt-i': 'window:toggle-dev-tools' 'ctrl-alt-p': 'window:run-package-specs' - 'ctrl-alt-s': 'application:run-all-specs' 'ctrl-shift-o': 'application:open-folder' 'ctrl-alt-o': 'application:add-project-folder' 'ctrl-shift-left': 'pane:move-item-left' diff --git a/menus/darwin.cson b/menus/darwin.cson index 27318eb47..6fff290e2 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -164,7 +164,6 @@ label: 'Developer' submenu: [ { label: 'Open In Dev Mode…', command: 'application:open-dev' } - { label: 'Run Atom Specs', command: 'application:run-all-specs' } { label: 'Run Package Specs', command: 'window:run-package-specs' } { label: 'Toggle Developer Tools', command: 'window:toggle-dev-tools' } ] diff --git a/menus/linux.cson b/menus/linux.cson index 354a5ec96..fa831b4a4 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -121,7 +121,6 @@ label: 'Developer' submenu: [ { label: 'Open In &Dev Mode…', command: 'application:open-dev' } - { label: 'Run &Atom Specs', command: 'application:run-all-specs' } { label: 'Run Package &Specs', command: 'window:run-package-specs' } { label: 'Toggle Developer &Tools', command: 'window:toggle-dev-tools' } ] diff --git a/menus/win32.cson b/menus/win32.cson index 791e9d68d..04da3d388 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -120,7 +120,6 @@ label: 'Developer' submenu: [ { label: 'Open In &Dev Mode…', command: 'application:open-dev' } - { label: 'Run &Atom Specs', command: 'application:run-all-specs' } { label: 'Run Package &Specs', command: 'window:run-package-specs' } { label: 'Toggle Developer &Tools', command: 'window:toggle-dev-tools' } ] diff --git a/package.json b/package.json index 9191f39db..4b89dc162 100644 --- a/package.json +++ b/package.json @@ -15,14 +15,15 @@ "electronVersion": "0.30.7", "dependencies": { "async": "0.2.6", - "atom-keymap": "^6.0.0", + "atom-keymap": "^6.1.0", "babel-core": "^5.8.21", "bootstrap": "^3.3.4", "clear-cut": "^2.0.1", "coffee-script": "1.8.0", "color": "^0.7.3", "event-kit": "^1.3.0", - "first-mate": "^5.0.0", + "find-parent-dir": "^0.3.0", + "first-mate": "^5.1.0", "fs-plus": "^2.8.0", "fstream": "0.1.24", "fuzzaldrin": "^2.1", @@ -40,13 +41,14 @@ "pathwatcher": "^6.2", "property-accessors": "^1.1.3", "random-words": "0.0.1", + "resolve": "^1.1.6", "runas": "^3.1", "scandal": "^2.2", "scoped-property-store": "^0.17.0", "scrollbar-style": "^3.2", "season": "^5.3", "semver": "^4.3.3", - "service-hub": "^0.6.2", + "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "stacktrace-parser": "0.1.1", "temp": "0.8.1", @@ -73,10 +75,10 @@ "autocomplete-atom-api": "0.9.2", "autocomplete-css": "0.11.0", "autocomplete-html": "0.7.2", - "autocomplete-plus": "2.22.0", + "autocomplete-plus": "2.23.0", "autocomplete-snippets": "1.7.1", "autoflow": "0.26.0", - "autosave": "0.22.0", + "autosave": "0.23.0", "background-tips": "0.26.0", "bookmarks": "0.38.0", "bracket-matcher": "0.78.0", @@ -85,8 +87,8 @@ "dev-live-reload": "0.47.0", "encoding-selector": "0.21.0", "exception-reporting": "0.37.0", - "find-and-replace": "0.185.0", - "fuzzy-finder": "0.90.0", + "find-and-replace": "0.187.1", + "fuzzy-finder": "0.91.0", "git-diff": "0.56.0", "go-to-line": "0.30.0", "grammar-selector": "0.47.0", @@ -95,26 +97,26 @@ "keybinding-resolver": "0.33.0", "line-ending-selector": "0.2.0", "link": "0.31.0", - "markdown-preview": "0.154.0", + "markdown-preview": "0.155.0", "metrics": "0.52.0", "notifications": "0.59.0", "open-on-github": "0.38.0", "package-generator": "0.40.0", "release-notes": "0.53.0", "settings-view": "0.230.1", - "snippets": "0.100.0", + "snippets": "0.101.0", "spell-check": "0.61.0", "status-bar": "0.79.0", "styleguide": "0.44.0", "symbols-view": "0.109.0", - "tabs": "0.84.0", + "tabs": "0.85.0", "timecop": "0.33.0", - "tree-view": "0.190.0", + "tree-view": "0.192.2", "update-package-dependencies": "0.10.0", - "welcome": "0.30.0", + "welcome": "0.31.0", "whitespace": "0.31.0", "wrap-guide": "0.38.0", - "language-c": "0.48.0", + "language-c": "0.49.0", "language-clojure": "0.18.0", "language-coffee-script": "0.42.0", "language-csharp": "0.11.0", @@ -125,14 +127,14 @@ "language-html": "0.42.0", "language-hyperlink": "0.14.0", "language-java": "0.16.0", - "language-javascript": "0.96.0", + "language-javascript": "0.97.0", "language-json": "0.17.0", "language-less": "0.28.2", "language-make": "0.19.0", "language-mustache": "0.13.0", "language-objective-c": "0.15.0", "language-perl": "0.30.0", - "language-php": "0.30.0", + "language-php": "0.31.0", "language-property-list": "0.8.0", "language-python": "0.40.0", "language-ruby": "0.60.0", diff --git a/spec/atom-spec.coffee b/spec/atom-environment-spec.coffee similarity index 67% rename from spec/atom-spec.coffee rename to spec/atom-environment-spec.coffee index 31265c9a6..f9dbc02f9 100644 --- a/spec/atom-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -1,13 +1,20 @@ -Exec = require('child_process').exec +_ = require "underscore-plus" path = require 'path' +temp = require 'temp' Package = require '../src/package' ThemeManager = require '../src/theme-manager' -_ = require "underscore-plus" -temp = require "temp" +AtomEnvironment = require '../src/atom-environment' -describe "the `atom` global", -> +describe "AtomEnvironment", -> describe 'window sizing methods', -> describe '::getPosition and ::setPosition', -> + originalPosition = null + beforeEach -> + originalPosition = atom.getPosition() + + afterEach -> + atom.setPosition(originalPosition.x, originalPosition.y) + it 'sets the position of the window, and can retrieve the position just set', -> atom.setPosition(22, 45) expect(atom.getPosition()).toEqual x: 22, y: 45 @@ -31,28 +38,8 @@ describe "the `atom` global", -> version = '36b5518' expect(atom.isReleasedVersion()).toBe false - describe "when an update becomes available", -> - subscription = null - - afterEach -> - subscription?.dispose() - - it "invokes onUpdateAvailable listeners", -> - updateAvailableHandler = jasmine.createSpy("update-available-handler") - subscription = atom.onUpdateAvailable updateAvailableHandler - - autoUpdater = require('remote').require('auto-updater') - autoUpdater.emit 'update-downloaded', null, "notes", "version" - - waitsFor -> - updateAvailableHandler.callCount > 0 - - runs -> - {releaseVersion} = updateAvailableHandler.mostRecentCall.args[0] - expect(releaseVersion).toBe 'version' - describe "loading default config", -> - it 'loads the default core config', -> + it 'loads the default core config schema', -> expect(atom.config.get('core.excludeVcsIgnoredPaths')).toBe true expect(atom.config.get('core.followSymlinks')).toBe true expect(atom.config.get('editor.showInvisibles')).toBe false @@ -139,7 +126,7 @@ describe "the `atom` global", -> expect(result).toBe false expect(errors.length).toBe 1 expect(errors[0].message).toBe "Assertion failed: a == b" - expect(errors[0].stack).toContain('atom-spec') + expect(errors[0].stack).toContain('atom-environment-spec') describe "if passed a callback function", -> it "calls the callback with the assertion failure's error object", -> @@ -154,30 +141,34 @@ describe "the `atom` global", -> expect(errors).toEqual [] describe "saving and loading", -> - afterEach -> atom.mode = "spec" + beforeEach -> + atom.enablePersistence = true + + afterEach -> + atom.enablePersistence = false it "selects the state based on the current project paths", -> - Atom = atom.constructor [dir1, dir2] = [temp.mkdirSync("dir1-"), temp.mkdirSync("dir2-")] - loadSettings = _.extend Atom.getLoadSettings(), + loadSettings = _.extend atom.getLoadSettings(), initialPaths: [dir1] windowState: null - spyOn(Atom, 'getLoadSettings').andCallFake -> loadSettings - spyOn(Atom.getStorageFolder(), 'getPath').andReturn(temp.mkdirSync("storage-dir-")) + spyOn(atom, 'getLoadSettings').andCallFake -> loadSettings + spyOn(atom.getStorageFolder(), 'getPath').andReturn(temp.mkdirSync("storage-dir-")) - atom.mode = "editor" atom.state.stuff = "cool" atom.project.setPaths([dir1, dir2]) - atom.saveSync.originalValue.call(atom) + atom.saveStateSync() - atom1 = Atom.loadOrCreate("editor") - expect(atom1.state.stuff).toBeUndefined() + atom.state = {} + atom.loadStateSync() + expect(atom.state.stuff).toBeUndefined() loadSettings.initialPaths = [dir2, dir1] - atom2 = Atom.loadOrCreate("editor") - expect(atom2.state.stuff).toBe("cool") + atom.state = {} + atom.loadStateSync() + expect(atom.state.stuff).toBe("cool") describe "openInitialEmptyEditorIfNecessary", -> describe "when there are no paths set", -> @@ -225,28 +216,92 @@ describe "the `atom` global", -> describe "::unloadEditorWindow()", -> it "saves the serialized state of the window so it can be deserialized after reload", -> - workspaceState = atom.workspace.serialize() - syntaxState = atom.grammars.serialize() - projectState = atom.project.serialize() + atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, window, document}) + spyOn(atomEnvironment, 'saveStateSync') - atom.unloadEditorWindow() + workspaceState = atomEnvironment.workspace.serialize() + grammarsState = {grammarOverridesByPath: atomEnvironment.grammars.grammarOverridesByPath} + projectState = atomEnvironment.project.serialize() - expect(atom.state.workspace).toEqual workspaceState - expect(atom.state.grammars).toEqual syntaxState - expect(atom.state.project).toEqual projectState - expect(atom.saveSync).toHaveBeenCalled() + atomEnvironment.unloadEditorWindow() - describe "::removeEditorWindow()", -> + expect(atomEnvironment.state.workspace).toEqual workspaceState + expect(atomEnvironment.state.grammars).toEqual grammarsState + expect(atomEnvironment.state.project).toEqual projectState + expect(atomEnvironment.saveStateSync).toHaveBeenCalled() + + atomEnvironment.destroy() + + describe "::destroy()", -> it "unsubscribes from all buffers", -> + atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, window, document}) + waitsForPromise -> - atom.workspace.open("sample.js") + atomEnvironment.workspace.open("sample.js") runs -> - buffer = atom.workspace.getActivePaneItem().buffer - pane = atom.workspace.getActivePane() + buffer = atomEnvironment.workspace.getActivePaneItem().buffer + pane = atomEnvironment.workspace.getActivePane() pane.splitRight(copyActiveItem: true) - expect(atom.workspace.getTextEditors().length).toBe 2 + expect(atomEnvironment.workspace.getTextEditors().length).toBe 2 - atom.removeEditorWindow() + atomEnvironment.destroy() expect(buffer.getSubscriptionCount()).toBe 0 + + describe "::openLocations(locations) (called via IPC from browser process)", -> + beforeEach -> + spyOn(atom.workspace, 'open') + atom.project.setPaths([]) + + describe "when the opened path exists", -> + it "adds it to the project's paths", -> + pathToOpen = __filename + atom.openLocations([{pathToOpen}]) + expect(atom.project.getPaths()[0]).toBe __dirname + + describe "when the opened path does not exist but its parent directory does", -> + it "adds the parent directory to the project paths", -> + pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt') + atom.openLocations([{pathToOpen}]) + expect(atom.project.getPaths()[0]).toBe __dirname + + describe "when the opened path is a file", -> + it "opens it in the workspace", -> + pathToOpen = __filename + atom.openLocations([{pathToOpen}]) + expect(atom.workspace.open.mostRecentCall.args[0]).toBe __filename + + describe "when the opened path is a directory", -> + it "does not open it in the workspace", -> + pathToOpen = __dirname + atom.openLocations([{pathToOpen}]) + expect(atom.workspace.open.callCount).toBe 0 + + describe "when the opened path is a uri", -> + it "adds it to the project's paths as is", -> + pathToOpen = 'remote://server:7644/some/dir/path' + atom.openLocations([{pathToOpen}]) + expect(atom.project.getPaths()[0]).toBe pathToOpen + + describe "::updateAvailable(info) (called via IPC from browser process)", -> + subscription = null + + afterEach -> + subscription?.dispose() + + it "invokes onUpdateAvailable listeners", -> + atom.listenForUpdates() + + updateAvailableHandler = jasmine.createSpy("update-available-handler") + subscription = atom.onUpdateAvailable updateAvailableHandler + + autoUpdater = require('remote').require('auto-updater') + autoUpdater.emit 'update-downloaded', null, "notes", "version" + + waitsFor -> + updateAvailableHandler.callCount > 0 + + runs -> + {releaseVersion} = updateAvailableHandler.mostRecentCall.args[0] + expect(releaseVersion).toBe 'version' diff --git a/spec/command-installer-spec.coffee b/spec/command-installer-spec.coffee index 972494ec3..584dc193e 100644 --- a/spec/command-installer-spec.coffee +++ b/spec/command-installer-spec.coffee @@ -22,7 +22,8 @@ describe "CommandInstaller on #darwin", -> describe "when using a stable version of atom", -> beforeEach -> - installer = new CommandInstaller("2.0.2") + confirm = -> + installer = new CommandInstaller("2.0.2", confirm) it "symlinks the atom command as 'atom'", -> installedAtomPath = path.join(installationPath, 'atom') diff --git a/spec/command-registry-spec.coffee b/spec/command-registry-spec.coffee index f8194aae6..ecdd42fd6 100644 --- a/spec/command-registry-spec.coffee +++ b/spec/command-registry-spec.coffee @@ -16,6 +16,7 @@ describe "CommandRegistry", -> document.querySelector('#jasmine-content').appendChild(parent) registry = new CommandRegistry + registry.attach(parent) afterEach -> registry.destroy() @@ -277,3 +278,18 @@ describe "CommandRegistry", -> {name: 'namespace:command-2', displayName: 'Namespace: Command 2'} {name: 'namespace:command-1', displayName: 'Namespace: Command 1'} ] + + describe "::attach(rootNode)", -> + it "adds event listeners for any previously-added commands", -> + registry2 = new CommandRegistry + + commandSpy = jasmine.createSpy('command-callback') + registry2.add '.grandchild', 'command-1', commandSpy + + grandchild.dispatchEvent(new CustomEvent('command-1', bubbles: true)) + expect(commandSpy).not.toHaveBeenCalled() + + registry2.attach(parent) + + grandchild.dispatchEvent(new CustomEvent('command-1', bubbles: true)) + expect(commandSpy).toHaveBeenCalled() diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 7a2e9e5a0..bb9ab89a8 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -7,10 +7,16 @@ describe "Config", -> dotAtomPath = null beforeEach -> + spyOn(atom.config, "load") + spyOn(atom.config, "save") dotAtomPath = temp.path('dot-atom-dir') atom.config.configDirPath = dotAtomPath + atom.config.enablePersistence = true atom.config.configFilePath = path.join(atom.config.configDirPath, "atom.config.cson") + afterEach -> + atom.config.enablePersistence = false + describe ".get(keyPath, {scope, sources, excludeSources})", -> it "allows a key path's value to be read", -> expect(atom.config.set("foo.bar.baz", 42)).toBe true diff --git a/spec/context-menu-manager-spec.coffee b/spec/context-menu-manager-spec.coffee index 4cf33acc6..ddced8452 100644 --- a/spec/context-menu-manager-spec.coffee +++ b/spec/context-menu-manager-spec.coffee @@ -5,7 +5,7 @@ describe "ContextMenuManager", -> beforeEach -> {resourcePath} = atom.getLoadSettings() - contextMenu = new ContextMenuManager({resourcePath}) + contextMenu = new ContextMenuManager({resourcePath, keymapManager: atom.keymaps}) parent = document.createElement("div") child = document.createElement("div") diff --git a/spec/custom-gutter-component-spec.coffee b/spec/custom-gutter-component-spec.coffee index 4b7d81fba..5504e9c40 100644 --- a/spec/custom-gutter-component-spec.coffee +++ b/spec/custom-gutter-component-spec.coffee @@ -7,7 +7,7 @@ describe "CustomGutterComponent", -> beforeEach -> mockGutterContainer = {} gutter = new Gutter(mockGutterContainer, {name: 'test-gutter'}) - gutterComponent = new CustomGutterComponent({gutter}) + gutterComponent = new CustomGutterComponent({gutter, views: atom.views}) it "creates a gutter DOM node with only an empty 'custom-decorations' child node when it is initialized", -> expect(gutterComponent.getDomNode().classList.contains('gutter')).toBe true diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee index 19eccd58d..3e1178749 100644 --- a/spec/display-buffer-spec.coffee +++ b/spec/display-buffer-spec.coffee @@ -7,7 +7,10 @@ describe "DisplayBuffer", -> tabLength = 2 buffer = atom.project.bufferForPathSync('sample.js') - displayBuffer = new DisplayBuffer({buffer, tabLength}) + displayBuffer = new DisplayBuffer({ + buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars, + packageManager: atom.packages, assert: -> + }) changeHandler = jasmine.createSpy 'changeHandler' displayBuffer.onDidChange changeHandler @@ -49,7 +52,10 @@ describe "DisplayBuffer", -> it "updates the display buffer prior to invoking change handlers registered on the buffer", -> buffer.onDidChange -> expect(displayBuffer2.tokenizedLineForScreenRow(0).text).toBe "testing" - displayBuffer2 = new DisplayBuffer({buffer, tabLength}) + displayBuffer2 = new DisplayBuffer({ + buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars, + packageManager: atom.packages, assert: -> + }) buffer.setText("testing") describe "soft wrapping", -> @@ -263,7 +269,10 @@ describe "DisplayBuffer", -> describe "when a newline is inserted, deleted, and re-inserted at the end of a wrapped line (regression)", -> it "correctly renders the original wrapped line", -> buffer = atom.project.buildBufferSync(null, '') - displayBuffer = new DisplayBuffer({buffer, tabLength, editorWidthInChars: 30}) + displayBuffer = new DisplayBuffer({ + buffer, tabLength, editorWidthInChars: 30, config: atom.config, + grammarRegistry: atom.grammars, packageManager: atom.packages, assert: -> + }) displayBuffer.setDefaultCharWidth(1) displayBuffer.setSoftWrapped(true) @@ -326,7 +335,10 @@ describe "DisplayBuffer", -> displayBuffer.destroy() buffer.release() buffer = atom.project.bufferForPathSync('two-hundred.txt') - displayBuffer = new DisplayBuffer({buffer, tabLength}) + displayBuffer = new DisplayBuffer({ + buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars, + packageManager: atom.packages, assert: -> + }) displayBuffer.onDidChange changeHandler describe "when folds are created and destroyed", -> @@ -440,7 +452,10 @@ describe "DisplayBuffer", -> describe "when there is another display buffer pointing to the same buffer", -> it "does not consider folds to be nested inside of folds from the other display buffer", -> - otherDisplayBuffer = new DisplayBuffer({buffer, tabLength}) + otherDisplayBuffer = new DisplayBuffer({ + buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars, + packageManager: atom.packages, assert: -> + }) otherDisplayBuffer.createFold(1, 5) displayBuffer.createFold(2, 4) @@ -1188,7 +1203,10 @@ describe "DisplayBuffer", -> describe 'when there are multiple DisplayBuffers for a buffer', -> describe 'when a marker is created', -> it 'the second display buffer will not emit a marker-created event when the marker has been deleted in the first marker-created event', -> - displayBuffer2 = new DisplayBuffer({buffer, tabLength}) + displayBuffer2 = new DisplayBuffer({ + buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars, + packageManager: atom.packages, assert: -> + }) displayBuffer.onDidCreateMarker markerCreated1 = jasmine.createSpy().andCallFake (marker) -> marker.destroy() displayBuffer2.onDidCreateMarker markerCreated2 = jasmine.createSpy() diff --git a/spec/git-repository-provider-spec.coffee b/spec/git-repository-provider-spec.coffee index 13a0724d6..bbbfb4b03 100644 --- a/spec/git-repository-provider-spec.coffee +++ b/spec/git-repository-provider-spec.coffee @@ -6,11 +6,15 @@ GitRepository = require '../src/git-repository' GitRepositoryProvider = require '../src/git-repository-provider' describe "GitRepositoryProvider", -> + provider = null + + beforeEach -> + provider = new GitRepositoryProvider(atom.project, atom.config, atom.confirm) + describe ".repositoryForDirectory(directory)", -> describe "when specified a Directory with a Git repository", -> it "returns a Promise that resolves to a GitRepository", -> waitsForPromise -> - provider = new GitRepositoryProvider atom.project directory = new Directory path.join(__dirname, 'fixtures', 'git', 'master.git') provider.repositoryForDirectory(directory).then (result) -> expect(result).toBeInstanceOf GitRepository @@ -19,7 +23,6 @@ describe "GitRepositoryProvider", -> expect(result.getType()).toBe 'git' it "returns the same GitRepository for different Directory objects in the same repo", -> - provider = new GitRepositoryProvider atom.project firstRepo = null secondRepo = null @@ -38,7 +41,6 @@ describe "GitRepositoryProvider", -> describe "when specified a Directory without a Git repository", -> it "returns a Promise that resolves to null", -> waitsForPromise -> - provider = new GitRepositoryProvider atom.project directory = new Directory temp.mkdirSync('dir') provider.repositoryForDirectory(directory).then (result) -> expect(result).toBe null @@ -46,7 +48,6 @@ describe "GitRepositoryProvider", -> describe "when specified a Directory with an invalid Git repository", -> it "returns a Promise that resolves to null", -> waitsForPromise -> - provider = new GitRepositoryProvider atom.project dirPath = temp.mkdirSync('dir') fs.writeFileSync(path.join(dirPath, '.git', 'objects'), '') fs.writeFileSync(path.join(dirPath, '.git', 'HEAD'), '') @@ -59,7 +60,6 @@ describe "GitRepositoryProvider", -> describe "when specified a Directory with a valid gitfile-linked repository", -> it "returns a Promise that resolves to a GitRepository", -> waitsForPromise -> - provider = new GitRepositoryProvider atom.project gitDirPath = path.join(__dirname, 'fixtures', 'git', 'master.git') workDirPath = temp.mkdirSync('git-workdir') fs.writeFileSync(path.join(workDirPath, '.git'), 'gitdir: ' + gitDirPath+'\n') @@ -75,8 +75,6 @@ describe "GitRepositoryProvider", -> directory = null provider = null beforeEach -> - provider = new GitRepositoryProvider atom.project - # An implementation of Directory that does not implement existsSync(). subdirectory = {} directory = diff --git a/spec/git-spec.coffee b/spec/git-spec.coffee index a72cd47cd..31ac176f7 100644 --- a/spec/git-spec.coffee +++ b/spec/git-spec.coffee @@ -124,8 +124,10 @@ describe "GitRepository", -> [filePath, editor] = [] beforeEach -> + spyOn(atom, "confirm") + workingDirPath = copyRepository() - repo = new GitRepository(workingDirPath) + repo = new GitRepository(workingDirPath, {project: atom.project, config: atom.config, confirm: atom.confirm}) filePath = path.join(workingDirPath, 'a.txt') fs.writeFileSync(filePath, 'ch ch changes') @@ -136,7 +138,7 @@ describe "GitRepository", -> editor = atom.workspace.getActiveTextEditor() it "displays a confirmation dialog by default", -> - spyOn(atom, 'confirm').andCallFake ({buttons}) -> buttons.OK() + atom.confirm.andCallFake ({buttons}) -> buttons.OK() atom.config.set('editor.confirmCheckoutHeadRevision', true) repo.checkoutHeadForEditor(editor) @@ -144,7 +146,6 @@ describe "GitRepository", -> expect(fs.readFileSync(filePath, 'utf8')).toBe '' it "does not display a dialog when confirmation is disabled", -> - spyOn(atom, 'confirm') atom.config.set('editor.confirmCheckoutHeadRevision', false) repo.checkoutHeadForEditor(editor) @@ -277,7 +278,8 @@ describe "GitRepository", -> atom.workspace.open('file.txt') runs -> - project2 = Project.deserialize(atom.project.serialize()) + project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + project2.deserialize(atom.project.serialize(), atom.deserializers) buffer = project2.getBuffers()[0] waitsFor -> diff --git a/spec/grammars-spec.coffee b/spec/grammars-spec.coffee index 960ce7d52..82a29892a 100644 --- a/spec/grammars-spec.coffee +++ b/spec/grammars-spec.coffee @@ -24,18 +24,9 @@ describe "the `grammars` global", -> atom.packages.deactivatePackages() atom.packages.unloadPackages() - describe "serialization", -> - it "remembers grammar overrides by path", -> - filePath = '/foo/bar/file.js' - expect(atom.grammars.selectGrammar(filePath).name).not.toBe 'Ruby' - atom.grammars.setGrammarOverrideForPath(filePath, 'source.ruby') - grammars2 = atom.deserializers.deserialize(atom.grammars.serialize()) - grammars2.addGrammar(grammar) for grammar in atom.grammars.grammars when grammar isnt atom.grammars.nullGrammar - expect(grammars2.selectGrammar(filePath).name).toBe 'Ruby' - describe ".selectGrammar(filePath)", -> it "always returns a grammar", -> - registry = new GrammarRegistry() + registry = new GrammarRegistry(config: atom.config) expect(registry.selectGrammar().scopeName).toBe 'text.plain.null-grammar' it "selects the text.plain grammar over the null grammar", -> diff --git a/spec/gutter-container-component-spec.coffee b/spec/gutter-container-component-spec.coffee index 595067de5..73a9d0f6c 100644 --- a/spec/gutter-container-component-spec.coffee +++ b/spec/gutter-container-component-spec.coffee @@ -26,7 +26,7 @@ describe "GutterContainerComponent", -> domElementPool = new DOMElementPool mockEditor = {} mockMouseDown = -> - gutterContainerComponent = new GutterContainerComponent({editor: mockEditor, onMouseDown: mockMouseDown, domElementPool}) + gutterContainerComponent = new GutterContainerComponent({editor: mockEditor, onMouseDown: mockMouseDown, domElementPool, views: atom.views}) it "creates a DOM node with no child gutter nodes when it is initialized", -> expect(gutterContainerComponent.getDomNode() instanceof HTMLElement).toBe true diff --git a/spec/integration/helpers/start-atom.coffee b/spec/integration/helpers/start-atom.coffee index 6a7144141..b8768f532 100644 --- a/spec/integration/helpers/start-atom.coffee +++ b/spec/integration/helpers/start-atom.coffee @@ -91,7 +91,8 @@ buildAtomClient = (args, env) -> cb(null) .addCommand "treeViewRootDirectories", (cb) -> - @execute(-> + @waitForExist('.tree-view', 10000) + .execute(-> for element in document.querySelectorAll(".tree-view .project-root > .header .name") element.dataset.path , cb) @@ -104,7 +105,8 @@ buildAtomClient = (args, env) -> .then ({value: newWindowHandles}) -> [newWindowHandle] = difference(newWindowHandles, oldWindowHandles) return done() unless newWindowHandle - @window(newWindowHandle, done) + @window(newWindowHandle) + .waitForExist('atom-workspace', 10000, done) .addCommand "startAnotherAtom", (args, env, done) -> @call -> @@ -168,7 +170,11 @@ module.exports = (args, env, fn) -> jasmine.getEnv().currentSpec.fail(new Error(err.response?.body?.value?.message)) finish() - fn(client.init()).then(finish) + fn( + client.init() + .waitUntil((-> @windowHandles().then ({value}) -> value.length > 0), 10000) + .waitForExist("atom-workspace", 10000) + ).then(finish) , 30000) waitsFor("webdriver to stop", chromeDriverDown, 15000) diff --git a/spec/integration/startup-spec.coffee b/spec/integration/startup-spec.coffee index 74dd80f9e..36342470d 100644 --- a/spec/integration/startup-spec.coffee +++ b/spec/integration/startup-spec.coffee @@ -28,8 +28,6 @@ describe "Starting Atom", -> it "opens the parent directory and creates an empty text editor", -> runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: atomHome}, (client) -> client - .waitForWindowCount(1, 1000) - .waitForExist("atom-workspace", 5000) .waitForPaneItemCount(1, 1000) .treeViewRootDirectories() @@ -54,8 +52,6 @@ describe "Starting Atom", -> runAtom ["#{filePath}:3"], {ATOM_HOME: atomHome}, (client) -> client - .waitForWindowCount(1, 1000) - .waitForExist("atom-workspace", 5000) .waitForPaneItemCount(1, 1000) .waitForExist("atom-text-editor", 5000) .then (exists) -> expect(exists).toBe true @@ -79,8 +75,6 @@ describe "Starting Atom", -> runAtom ["#{filePath}:2:2"], {ATOM_HOME: atomHome}, (client) -> client - .waitForWindowCount(1, 1000) - .waitForExist("atom-workspace", 5000) .waitForPaneItemCount(1, 1000) .waitForExist("atom-text-editor", 5000) .then (exists) -> expect(exists).toBe true @@ -97,8 +91,6 @@ describe "Starting Atom", -> filePath = path.join(tempDirPath, "new-file") runAtom ["#{filePath}: "], {ATOM_HOME: atomHome}, (client) -> client - .waitForWindowCount(1, 1000) - .waitForExist("atom-workspace", 5000) .waitForPaneItemCount(1, 1000) .waitForExist("atom-text-editor", 5000) .then (exists) -> expect(exists).toBe true @@ -113,8 +105,6 @@ describe "Starting Atom", -> runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: atomHome}, (client) -> client - .waitForWindowCount(1, 1000) - .waitForExist("atom-workspace", 5000) .waitForPaneItemCount(1, 5000) # Opening another file reuses the same window and does not change the @@ -131,7 +121,6 @@ describe "Starting Atom", -> .waitForNewWindow(-> @startAnotherAtom([otherTempDirPath], ATOM_HOME: atomHome) , 5000) - .waitForExist("atom-workspace", 5000) .waitForPaneItemCount(0, 1000) .treeViewRootDirectories() .then ({value}) -> expect(value).toEqual([otherTempDirPath]) @@ -140,14 +129,12 @@ describe "Starting Atom", -> it "remembers the state of the window", -> runAtom [tempDirPath], {ATOM_HOME: atomHome}, (client) -> client - .waitForExist("atom-workspace", 5000) .waitForPaneItemCount(0, 3000) .execute -> atom.workspace.open() .waitForPaneItemCount(1, 3000) runAtom [tempDirPath], {ATOM_HOME: atomHome}, (client) -> client - .waitForExist("atom-workspace", 5000) .waitForPaneItemCount(1, 5000) describe "opening multiple directories simultaneously", -> @@ -157,14 +144,12 @@ describe "Starting Atom", -> runAtom [tempDirPath, otherTempDirPath], {ATOM_HOME: atomHome}, (client) -> client - .waitForExist("atom-workspace", 5000) .treeViewRootDirectories() .then ({value}) -> expect(value).toEqual([tempDirPath, otherTempDirPath]) # Opening one of those directories again reuses the same window and # does not change the project paths. .startAnotherAtom([nestedDir], ATOM_HOME: atomHome) - .waitForExist("atom-workspace", 5000) .treeViewRootDirectories() .then ({value}) -> expect(value).toEqual([tempDirPath, otherTempDirPath]) @@ -172,7 +157,6 @@ describe "Starting Atom", -> it "reuses that window to open a directory", -> runAtom [], {ATOM_HOME: atomHome}, (client) -> client - .waitForExist("atom-workspace") .treeViewRootDirectories() .then ({value}) -> expect(value).toEqual([]) @@ -188,7 +172,6 @@ describe "Starting Atom", -> it "opens a new window with a single untitled buffer", -> runAtom [], {ATOM_HOME: atomHome}, (client) -> client - .waitForExist("atom-workspace") .waitForPaneItemCount(1, 5000) # Opening with no file paths always creates a new window, even if @@ -196,7 +179,6 @@ describe "Starting Atom", -> .waitForNewWindow(-> @startAnotherAtom([], ATOM_HOME: atomHome) , 5000) - .waitForExist("atom-workspace") .waitForPaneItemCount(1, 5000) it "doesn't open a new window if openEmptyEditorOnStart is disabled", -> @@ -207,17 +189,14 @@ describe "Starting Atom", -> runAtom [], {ATOM_HOME: atomHome}, (client) -> client - .waitForExist("atom-workspace") .waitForPaneItemCount(0, 5000) it "reopens any previously opened windows", -> runAtom [tempDirPath], {ATOM_HOME: atomHome}, (client) -> client - .waitForExist("atom-workspace") .waitForNewWindow(-> @startAnotherAtom([otherTempDirPath], ATOM_HOME: atomHome) , 5000) - .waitForExist("atom-workspace") runAtom [], {ATOM_HOME: atomHome}, (client) -> windowProjectPaths = [] @@ -226,12 +205,10 @@ describe "Starting Atom", -> .waitForWindowCount(2, 10000) .then ({value: windowHandles}) -> @window(windowHandles[0]) - .waitForExist("atom-workspace") .treeViewRootDirectories() .then ({value: directories}) -> windowProjectPaths.push(directories) .window(windowHandles[1]) - .waitForExist("atom-workspace") .treeViewRootDirectories() .then ({value: directories}) -> windowProjectPaths.push(directories) @@ -246,7 +223,5 @@ describe "Starting Atom", -> remoteDirectory = 'remote://server:3437/some/directory/path' runAtom [remoteDirectory], {ATOM_HOME: atomHome}, (client) -> client - .waitForWindowCount(1, 1000) - .waitForExist("atom-workspace", 5000) .treeViewRootDirectories() .then ({value}) -> expect(value).toEqual([remoteDirectory]) diff --git a/spec/jasmine-helper.coffee b/spec/jasmine-helper.coffee deleted file mode 100644 index 74e4a0384..000000000 --- a/spec/jasmine-helper.coffee +++ /dev/null @@ -1,59 +0,0 @@ -fs = require 'fs' - -module.exports.runSpecSuite = (specSuite, logFile, logErrors=true) -> - window[key] = value for key, value of require '../vendor/jasmine' - - {TerminalReporter} = require 'jasmine-tagged' - - disableFocusMethods() if process.env.JANKY_SHA1 or process.env.CI - - TimeReporter = require './time-reporter' - timeReporter = new TimeReporter() - - logStream = fs.openSync(logFile, 'w') if logFile? - log = (str) -> - if logStream? - fs.writeSync(logStream, str) - else - process.stderr.write(str) - - if atom.getLoadSettings().exitWhenDone - reporter = new TerminalReporter - print: (str) -> - log(str) - onComplete: (runner) -> - fs.closeSync(logStream) if logStream? - if process.env.JANKY_SHA1 or process.env.CI - grim = require 'grim' - - if grim.getDeprecationsLength() > 0 - grim.logDeprecations() - return atom.exit(1) - - if runner.results().failedCount > 0 - atom.exit(1) - else - atom.exit(0) - else - AtomReporter = require './atom-reporter' - reporter = new AtomReporter() - - require specSuite - - jasmineEnv = jasmine.getEnv() - jasmineEnv.addReporter(reporter) - jasmineEnv.addReporter(timeReporter) - jasmineEnv.setIncludedTags([process.platform]) - - jasmineContent = document.createElement('div') - jasmineContent.setAttribute('id', 'jasmine-content') - document.body.appendChild(jasmineContent) - - jasmineEnv.execute() - -disableFocusMethods = -> - ['fdescribe', 'ffdescribe', 'fffdescribe', 'fit', 'ffit', 'fffit'].forEach (methodName) -> - focusMethod = window[methodName] - window[methodName] = (description) -> - error = new Error('Focused spec is running on CI') - focusMethod description, -> throw error diff --git a/spec/jasmine-test-runner.coffee b/spec/jasmine-test-runner.coffee new file mode 100644 index 000000000..7ef34ce54 --- /dev/null +++ b/spec/jasmine-test-runner.coffee @@ -0,0 +1,111 @@ +fs = require 'fs' +_ = require 'underscore-plus' +fs = require 'fs-plus' +path = require 'path' +ipc = require 'ipc' + +module.exports = ({logFile, headless, testPaths, buildAtomEnvironment}) -> + window[key] = value for key, value of require '../vendor/jasmine' + require 'jasmine-tagged' + + # Allow document.title to be assigned in specs without screwing up spec window title + documentTitle = null + Object.defineProperty document, 'title', + get: -> documentTitle + set: (title) -> documentTitle = title + + ApplicationDelegate = require '../src/application-delegate' + applicationDelegate = new ApplicationDelegate() + applicationDelegate.setRepresentedFilename = -> + applicationDelegate.setWindowDocumentEdited = -> + window.atom = buildAtomEnvironment({ + applicationDelegate, window, document, + configDirPath: process.env.ATOM_HOME + enablePersistence: false + }) + + require './spec-helper' + disableFocusMethods() if process.env.JANKY_SHA1 or process.env.CI + requireSpecs(testPath) for testPath in testPaths + + setSpecType('user') + + resolveWithExitCode = null + promise = new Promise (resolve, reject) -> resolveWithExitCode = resolve + jasmineEnv = jasmine.getEnv() + jasmineEnv.addReporter(buildReporter({logFile, headless, resolveWithExitCode})) + TimeReporter = require './time-reporter' + jasmineEnv.addReporter(new TimeReporter()) + jasmineEnv.setIncludedTags([process.platform]) + + jasmineContent = document.createElement('div') + jasmineContent.setAttribute('id', 'jasmine-content') + + document.body.appendChild(jasmineContent) + + jasmineEnv.execute() + promise + +disableFocusMethods = -> + ['fdescribe', 'ffdescribe', 'fffdescribe', 'fit', 'ffit', 'fffit'].forEach (methodName) -> + focusMethod = window[methodName] + window[methodName] = (description) -> + error = new Error('Focused spec is running on CI') + focusMethod description, -> throw error + +requireSpecs = (testPath, specType) -> + if fs.isDirectorySync(testPath) + for testFilePath in fs.listTreeSync(testPath) when /-spec\.(coffee|js)$/.test testFilePath + require(testFilePath) + # Set spec directory on spec for setting up the project in spec-helper + setSpecDirectory(testPath) + else + require(testPath) + setSpecDirectory(path.dirname(testPath)) + +setSpecField = (name, value) -> + specs = jasmine.getEnv().currentRunner().specs() + return if specs.length is 0 + for index in [specs.length-1..0] + break if specs[index][name]? + specs[index][name] = value + +setSpecType = (specType) -> + setSpecField('specType', specType) + +setSpecDirectory = (specDirectory) -> + setSpecField('specDirectory', specDirectory) + +buildReporter = ({logFile, headless, resolveWithExitCode}) -> + if headless + buildTerminalReporter(logFile, resolveWithExitCode) + else + AtomReporter = require './atom-reporter' + reporter = new AtomReporter() + +buildTerminalReporter = (logFile, resolveWithExitCode) -> + logStream = fs.openSync(logFile, 'w') if logFile? + log = (str) -> + if logStream? + fs.writeSync(logStream, str) + else + ipc.send 'write-to-stderr', str + + {TerminalReporter} = require 'jasmine-tagged' + new TerminalReporter + print: (str) -> + log(str) + onComplete: (runner) -> + fs.closeSync(logStream) if logStream? + if process.env.JANKY_SHA1 or process.env.CI + grim = require 'grim' + + if grim.getDeprecationsLength() > 0 + grim.logDeprecations() + resolveWithExitCode(1) + return + + if runner.results().failedCount > 0 + resolveWithExitCode(1) + else + resolveWithExitCode(0) diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index 6320cc99f..1f5a01560 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -9,7 +9,7 @@ describe "LinesYardstick", -> atom.packages.activatePackage('language-javascript') waitsForPromise -> - atom.project.open('sample.js').then (o) -> editor = o + atom.workspace.open('sample.js').then (o) -> editor = o runs -> createdLineNodes = [] @@ -56,7 +56,7 @@ describe "LinesYardstick", -> textNodes editor.setLineHeightInPixels(14) - linesYardstick = new LinesYardstick(editor, mockPresenter, mockLineNodesProvider) + linesYardstick = new LinesYardstick(editor, mockPresenter, mockLineNodesProvider, atom.grammars) afterEach -> lineNode.remove() for lineNode in createdLineNodes diff --git a/spec/menu-manager-spec.coffee b/spec/menu-manager-spec.coffee index c047bbcfc..a2d76c1d0 100644 --- a/spec/menu-manager-spec.coffee +++ b/spec/menu-manager-spec.coffee @@ -5,7 +5,11 @@ describe "MenuManager", -> menu = null beforeEach -> - menu = new MenuManager(resourcePath: atom.getLoadSettings().resourcePath) + menu = new MenuManager( + resourcePath: atom.getLoadSettings().resourcePath + keymapManager: atom.keymaps + packageManager: atom.packages + ) describe "::add(items)", -> it "can add new menus that can be removed with the returned disposable", -> @@ -67,7 +71,7 @@ describe "MenuManager", -> atom.keymaps.add 'test', 'atom-workspace': 'ctrl-b': 'b' atom.keymaps.add 'test', 'atom-text-editor': 'ctrl-b': 'unset!' - waits 1 + waits 50 runs -> expect(menu.sendToBrowserProcess.argsForCall[0][1]['b']).toBeUndefined() diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index d1afd6ade..9a2edee0a 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -770,8 +770,6 @@ describe "PackageManager", -> atom.packages.deactivatePackages() atom.packages.unloadPackages() - GrammarRegistry = require '../src/grammar-registry' - atom.grammars = window.syntax = new GrammarRegistry() jasmine.restoreDeprecationsSnapshot() it "activates all the packages, and none of the themes", -> diff --git a/spec/package-spec.coffee b/spec/package-spec.coffee index bf059e0b5..63a80a7db 100644 --- a/spec/package-spec.coffee +++ b/spec/package-spec.coffee @@ -3,9 +3,22 @@ Package = require '../src/package' ThemePackage = require '../src/theme-package' describe "Package", -> + build = (constructor, path) -> + new constructor( + path: path, packageManager: atom.packages, config: atom.config, + styleManager: atom.styles, notificationManager: atom.notifications, + keymapManager: atom.keymaps, commandRegistry: atom.command, + grammarRegistry: atom.grammars, themeManager: atom.themes, + menuManager: atom.menu, contextMenuManager: atom.contextMenu, + devMode: false + ) + + buildPackage = (packagePath) -> build(Package, packagePath) + + buildThemePackage = (themePath) -> build(ThemePackage, themePath) + describe "when the package contains incompatible native modules", -> beforeEach -> - spyOn(atom, 'inDevMode').andReturn(false) items = {} spyOn(global.localStorage, 'setItem').andCallFake (key, item) -> items[key] = item; undefined spyOn(global.localStorage, 'getItem').andCallFake (key) -> items[key] ? null @@ -13,35 +26,34 @@ describe "Package", -> it "does not activate it", -> packagePath = atom.project.getDirectories()[0]?.resolve('packages/package-with-incompatible-native-module') - pack = new Package(packagePath) + pack = buildPackage(packagePath) expect(pack.isCompatible()).toBe false expect(pack.incompatibleModules[0].name).toBe 'native-module' expect(pack.incompatibleModules[0].path).toBe path.join(packagePath, 'node_modules', 'native-module') it "utilizes _atomModuleCache if present to determine the package's native dependencies", -> packagePath = atom.project.getDirectories()[0]?.resolve('packages/package-with-ignored-incompatible-native-module') - pack = new Package(packagePath) + pack = buildPackage(packagePath) expect(pack.getNativeModuleDependencyPaths().length).toBe(1) # doesn't see the incompatible module expect(pack.isCompatible()).toBe true packagePath = atom.project.getDirectories()[0]?.resolve('packages/package-with-cached-incompatible-native-module') - pack = new Package(packagePath) + pack = buildPackage(packagePath) expect(pack.isCompatible()).toBe false it "caches the incompatible native modules in local storage", -> packagePath = atom.project.getDirectories()[0]?.resolve('packages/package-with-incompatible-native-module') - expect(new Package(packagePath).isCompatible()).toBe false + expect(buildPackage(packagePath).isCompatible()).toBe false expect(global.localStorage.getItem.callCount).toBe 1 expect(global.localStorage.setItem.callCount).toBe 1 - expect(new Package(packagePath).isCompatible()).toBe false + expect(buildPackage(packagePath).isCompatible()).toBe false expect(global.localStorage.getItem.callCount).toBe 2 expect(global.localStorage.setItem.callCount).toBe 1 describe "::rebuild()", -> beforeEach -> - spyOn(atom, 'inDevMode').andReturn(false) items = {} spyOn(global.localStorage, 'setItem').andCallFake (key, item) -> items[key] = item; undefined spyOn(global.localStorage, 'getItem').andCallFake (key) -> items[key] ? null @@ -49,7 +61,7 @@ describe "Package", -> it "returns a promise resolving to the results of `apm rebuild`", -> packagePath = atom.project.getDirectories()[0]?.resolve('packages/package-with-index') - pack = new Package(packagePath) + pack = buildPackage(packagePath) rebuildCallbacks = [] spyOn(pack, 'runRebuildProcess').andCallFake ((callback) -> rebuildCallbacks.push(callback)) @@ -63,7 +75,7 @@ describe "Package", -> it "persists build failures in local storage", -> packagePath = atom.project.getDirectories()[0]?.resolve('packages/package-with-index') - pack = new Package(packagePath) + pack = buildPackage(packagePath) expect(pack.isCompatible()).toBe true expect(pack.getBuildFailureOutput()).toBeNull() @@ -79,7 +91,7 @@ describe "Package", -> expect(pack.isCompatible()).toBe false # A different package instance has the same failure output (simulates reload) - pack2 = new Package(packagePath) + pack2 = buildPackage(packagePath) expect(pack2.getBuildFailureOutput()).toBe 'It is broken' expect(pack2.isCompatible()).toBe false @@ -92,7 +104,7 @@ describe "Package", -> it "sets cached incompatible modules to an empty array when the rebuild completes (there may be a build error, but rebuilding *deletes* native modules)", -> packagePath = atom.project.getDirectories()[0]?.resolve('packages/package-with-incompatible-native-module') - pack = new Package(packagePath) + pack = buildPackage(packagePath) expect(pack.getIncompatibleNativeModules().length).toBeGreaterThan(0) @@ -118,14 +130,14 @@ describe "Package", -> it "loads and applies css", -> expect(getComputedStyle(editorElement).paddingBottom).not.toBe "1234px" themePath = atom.project.getDirectories()[0]?.resolve('packages/theme-with-index-css') - theme = new ThemePackage(themePath) + theme = buildThemePackage(themePath) theme.activate() expect(getComputedStyle(editorElement).paddingTop).toBe "1234px" it "parses, loads and applies less", -> expect(getComputedStyle(editorElement).paddingBottom).not.toBe "1234px" themePath = atom.project.getDirectories()[0]?.resolve('packages/theme-with-index-less') - theme = new ThemePackage(themePath) + theme = buildThemePackage(themePath) theme.activate() expect(getComputedStyle(editorElement).paddingTop).toBe "4321px" @@ -136,7 +148,7 @@ describe "Package", -> expect(getComputedStyle(editorElement).paddingBottom).not.toBe("103px") themePath = atom.project.getDirectories()[0]?.resolve('packages/theme-with-package-file') - theme = new ThemePackage(themePath) + theme = buildThemePackage(themePath) theme.activate() expect(getComputedStyle(editorElement).paddingTop).toBe("101px") expect(getComputedStyle(editorElement).paddingRight).toBe("102px") @@ -149,7 +161,7 @@ describe "Package", -> expect(getComputedStyle(editorElement).paddingBottom).not.toBe "30px" themePath = atom.project.getDirectories()[0]?.resolve('packages/theme-without-package-file') - theme = new ThemePackage(themePath) + theme = buildThemePackage(themePath) theme.activate() expect(getComputedStyle(editorElement).paddingTop).toBe "10px" expect(getComputedStyle(editorElement).paddingRight).toBe "20px" @@ -158,7 +170,7 @@ describe "Package", -> describe "reloading a theme", -> beforeEach -> themePath = atom.project.getDirectories()[0]?.resolve('packages/theme-with-package-file') - theme = new ThemePackage(themePath) + theme = buildThemePackage(themePath) theme.activate() it "reloads without readding to the stylesheets list", -> @@ -169,7 +181,7 @@ describe "Package", -> describe "events", -> beforeEach -> themePath = atom.project.getDirectories()[0]?.resolve('packages/theme-with-package-file') - theme = new ThemePackage(themePath) + theme = buildThemePackage(themePath) theme.activate() it "deactivated event fires on .deactivate()", -> @@ -182,7 +194,7 @@ describe "Package", -> beforeEach -> packagePath = atom.project.getDirectories()[0]?.resolve('packages/package-with-different-directory-name') - metadata = Package.loadMetadata(packagePath, true) + metadata = atom.packages.loadPackageMetadata(packagePath, true) it "uses the package name defined in package.json", -> expect(metadata.name).toBe 'package-with-a-totally-different-name' diff --git a/spec/pane-container-element-spec.coffee b/spec/pane-container-element-spec.coffee index 890baec38..98cfffd56 100644 --- a/spec/pane-container-element-spec.coffee +++ b/spec/pane-container-element-spec.coffee @@ -9,7 +9,7 @@ describe "PaneContainerElement", -> child.nodeName.toLowerCase() for child in paneAxisElement.children paneAxis = new PaneAxis - paneAxisElement = new PaneAxisElement().initialize(paneAxis) + paneAxisElement = new PaneAxisElement().initialize(paneAxis, atom) expect(childTagNames()).toEqual [] @@ -42,7 +42,7 @@ describe "PaneContainerElement", -> ] it "transfers focus to the next pane if a focused pane is removed", -> - container = new PaneContainer + container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom)) containerElement = atom.views.getView(container) leftPane = container.getActivePane() leftPaneElement = atom.views.getView(leftPane) @@ -58,10 +58,10 @@ describe "PaneContainerElement", -> describe "when a pane is split", -> it "builds appropriately-oriented atom-pane-axis elements", -> - container = new PaneContainer + container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom)) containerElement = atom.views.getView(container) - pane1 = container.getRoot() + pane1 = container.getActivePane() pane2 = pane1.splitRight() pane3 = pane2.splitDown() @@ -84,7 +84,7 @@ describe "PaneContainerElement", -> [container, containerElement] = [] beforeEach -> - container = new PaneContainer + container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom)) containerElement = atom.views.getView(container) document.querySelector('#jasmine-content').appendChild(containerElement) @@ -201,7 +201,7 @@ describe "PaneContainerElement", -> [leftPane, rightPane] = [] beforeEach -> - container = new PaneContainer + container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom)) leftPane = container.getActivePane() rightPane = leftPane.splitRight() @@ -252,8 +252,8 @@ describe "PaneContainerElement", -> element.tabIndex = -1 element - container = new PaneContainer - pane1 = container.getRoot() + container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom)) + pane1 = container.getActivePane() pane1.activateItem(buildElement('1')) pane4 = pane1.splitDown(items: [buildElement('4')]) pane7 = pane4.splitDown(items: [buildElement('7')]) diff --git a/spec/pane-container-spec.coffee b/spec/pane-container-spec.coffee index d0f2913aa..b94815e45 100644 --- a/spec/pane-container-spec.coffee +++ b/spec/pane-container-spec.coffee @@ -2,6 +2,16 @@ PaneContainer = require '../src/pane-container' Pane = require '../src/pane' describe "PaneContainer", -> + [confirm, params] = [] + + beforeEach -> + confirm = spyOn(atom.applicationDelegate, 'confirm').andReturn(0) + params = { + config: atom.config, + deserializerManager: atom.deserializers + applicationDelegate: atom.applicationDelegate + } + describe "serialization", -> [containerA, pane1A, pane2A, pane3A] = [] @@ -12,8 +22,9 @@ describe "PaneContainer", -> @deserialize: -> new this serialize: -> deserializer: 'Item' - pane1A = new Pane(items: [new Item]) - containerA = new PaneContainer(root: pane1A) + containerA = new PaneContainer(params) + pane1A = containerA.getActivePane() + pane1A.addItem(new Item) pane2A = pane1A.splitRight(items: [new Item]) pane3A = pane2A.splitDown(items: [new Item]) pane3A.focus() @@ -21,7 +32,8 @@ describe "PaneContainer", -> it "preserves the focused pane across serialization", -> expect(pane3A.focused).toBe true - containerB = PaneContainer.deserialize(containerA.serialize()) + containerB = new PaneContainer(params) + containerB.deserialize(containerA.serialize(), atom.deserializers) [pane1B, pane2B, pane3B] = containerB.getPanes() expect(pane3B.focused).toBe true @@ -29,7 +41,8 @@ describe "PaneContainer", -> pane3A.activate() expect(containerA.getActivePane()).toBe pane3A - containerB = PaneContainer.deserialize(containerA.serialize()) + containerB = new PaneContainer(params) + containerB.deserialize(containerA.serialize(), atom.deserializers) [pane1B, pane2B, pane3B] = containerB.getPanes() expect(containerB.getActivePane()).toBe pane3B @@ -37,7 +50,8 @@ describe "PaneContainer", -> pane3A.activate() state = containerA.serialize() state.activePaneId = -22 - containerB = atom.deserializers.deserialize(state) + containerB = new PaneContainer(params) + containerB.deserialize(state, atom.deserializers) expect(containerB.getActivePane()).toBe containerB.getPanes()[0] describe "if there are empty panes after deserialization", -> @@ -47,7 +61,8 @@ describe "PaneContainer", -> describe "if the 'core.destroyEmptyPanes' config option is false (the default)", -> it "leaves the empty panes intact", -> state = containerA.serialize() - containerB = atom.deserializers.deserialize(state) + containerB = new PaneContainer(params) + containerB.deserialize(state, atom.deserializers) [leftPane, column] = containerB.getRoot().getChildren() [topPane, bottomPane] = column.getChildren() @@ -60,14 +75,15 @@ describe "PaneContainer", -> atom.config.set('core.destroyEmptyPanes', true) state = containerA.serialize() - containerB = atom.deserializers.deserialize(state) + containerB = new PaneContainer(params) + containerB.deserialize(state, atom.deserializers) [leftPane, rightPane] = containerB.getRoot().getChildren() expect(leftPane.getItems().length).toBe 1 expect(rightPane.getItems().length).toBe 1 it "does not allow the root pane to be destroyed", -> - container = new PaneContainer + container = new PaneContainer(params) container.getRoot().destroy() expect(container.getRoot()).toBeDefined() expect(container.getRoot().isDestroyed()).toBe false @@ -76,7 +92,7 @@ describe "PaneContainer", -> [container, pane1, pane2] = [] beforeEach -> - container = new PaneContainer + container = new PaneContainer(params) pane1 = container.getRoot() it "returns the first pane if no pane has been made active", -> @@ -105,7 +121,8 @@ describe "PaneContainer", -> [container, pane1, pane2, observed] = [] beforeEach -> - container = new PaneContainer(root: new Pane(items: [new Object, new Object])) + container = new PaneContainer(params) + container.getRoot().addItems([new Object, new Object]) container.getRoot().splitRight(items: [new Object, new Object]) [pane1, pane2] = container.getPanes() @@ -147,7 +164,7 @@ describe "PaneContainer", -> describe "::observePanes()", -> it "invokes observers with all current and future panes", -> - container = new PaneContainer + container = new PaneContainer(params) container.getRoot().splitRight() [pane1, pane2] = container.getPanes() @@ -161,7 +178,8 @@ describe "PaneContainer", -> describe "::observePaneItems()", -> it "invokes observers with all current and future pane items", -> - container = new PaneContainer(root: new Pane(items: [new Object, new Object])) + container = new PaneContainer(params) + container.getRoot().addItems([new Object, new Object]) container.getRoot().splitRight(items: [new Object]) [pane1, pane2] = container.getPanes() observed = [] @@ -180,27 +198,27 @@ describe "PaneContainer", -> shouldPromptToSave: -> true getURI: -> 'test' - container = new PaneContainer + container = new PaneContainer(params) container.getRoot().splitRight() [pane1, pane2] = container.getPanes() pane1.addItem(new TestItem) pane2.addItem(new TestItem) it "returns true if the user saves all modified files when prompted", -> - spyOn(atom, "confirm").andReturn(0) + confirm.andReturn(0) saved = container.confirmClose() expect(saved).toBeTruthy() - expect(atom.confirm).toHaveBeenCalled() + expect(confirm).toHaveBeenCalled() it "returns false if the user cancels saving any modified file", -> - spyOn(atom, "confirm").andReturn(1) + confirm.andReturn(1) saved = container.confirmClose() expect(saved).toBeFalsy() - expect(atom.confirm).toHaveBeenCalled() + expect(confirm).toHaveBeenCalled() describe "::onDidAddPane(callback)", -> it "invokes the given callback when panes are added", -> - container = new PaneContainer + container = new PaneContainer(params) events = [] container.onDidAddPane (event) -> events.push(event) @@ -217,7 +235,7 @@ describe "PaneContainer", -> destroy: -> @_isDestroyed = true isDestroyed: -> @_isDestroyed - container = new PaneContainer + container = new PaneContainer(params) events = [] container.onWillDestroyPane (event) -> itemsDestroyed = (item.isDestroyed() for item in event.pane.getItems()) @@ -233,7 +251,7 @@ describe "PaneContainer", -> describe "::onDidDestroyPane(callback)", -> it "invokes the given callback when panes are destroyed", -> - container = new PaneContainer + container = new PaneContainer(params) events = [] container.onDidDestroyPane (event) -> events.push(event) @@ -248,7 +266,7 @@ describe "PaneContainer", -> describe "::onWillDestroyPaneItem() and ::onDidDestroyPaneItem", -> it "invokes the given callbacks when an item will be destroyed on any pane", -> - container = new PaneContainer + container = new PaneContainer(params) pane1 = container.getRoot() item1 = new Object item2 = new Object @@ -275,7 +293,7 @@ describe "PaneContainer", -> describe "::saveAll()", -> it "saves all open pane items", -> - container = new PaneContainer + container = new PaneContainer(params) pane1 = container.getRoot() pane2 = pane1.splitRight() diff --git a/spec/pane-element-spec.coffee b/spec/pane-element-spec.coffee index 40712ce90..3fcdc4ffb 100644 --- a/spec/pane-element-spec.coffee +++ b/spec/pane-element-spec.coffee @@ -4,8 +4,10 @@ describe "PaneElement", -> [paneElement, container, pane] = [] beforeEach -> - container = new PaneContainer - pane = container.getRoot() + spyOn(atom, "open") + + container = new PaneContainer(config: atom.config, confirm: atom.confirm.bind(atom)) + pane = container.getActivePane() paneElement = atom.views.getView(pane) describe "when the pane's active status changes", -> @@ -183,7 +185,6 @@ describe "PaneElement", -> describe "when a file is dragged to the pane", -> it "opens it", -> - spyOn(atom, "open") event = buildDragEvent("drop", [{path: "/fake1"}, {path: "/fake2"}]) paneElement.dispatchEvent(event) expect(atom.open.callCount).toBe 1 @@ -191,7 +192,6 @@ describe "PaneElement", -> describe "when a non-file is dragged to the pane", -> it "does nothing", -> - spyOn(atom, "open") event = buildDragEvent("drop", []) paneElement.dispatchEvent(event) expect(atom.open).not.toHaveBeenCalled() diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee index d293a6d2c..0c7a22b77 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -1,10 +1,11 @@ +{extend} = require 'underscore-plus' {Emitter} = require 'event-kit' Pane = require '../src/pane' PaneAxis = require '../src/pane-axis' PaneContainer = require '../src/pane-container' describe "Pane", -> - deserializerDisposable = null + [confirm, showSaveDialog, deserializerDisposable] = [] class Item @deserialize: ({name, uri}) -> new this(name, uri) @@ -19,18 +20,28 @@ describe "Pane", -> isDestroyed: -> @destroyed beforeEach -> + confirm = spyOn(atom.applicationDelegate, 'confirm') + showSaveDialog = spyOn(atom.applicationDelegate, 'showSaveDialog') deserializerDisposable = atom.deserializers.add(Item) afterEach -> deserializerDisposable.dispose() + paneParams = (params) -> + extend({ + applicationDelegate: atom.applicationDelegate, + config: atom.config, + deserializerManager: atom.deserializers, + notificationManager: atom.notifications + }, params) + describe "construction", -> it "sets the active item to the first item", -> - pane = new Pane(items: [new Item("A"), new Item("B")]) + pane = new Pane(paneParams(items: [new Item("A"), new Item("B")])) expect(pane.getActiveItem()).toBe pane.itemAtIndex(0) it "compacts the items array", -> - pane = new Pane(items: [undefined, new Item("A"), null, new Item("B")]) + pane = new Pane(paneParams(items: [undefined, new Item("A"), null, new Item("B")])) expect(pane.getItems().length).toBe 2 expect(pane.getActiveItem()).toBe pane.itemAtIndex(0) @@ -38,8 +49,8 @@ describe "Pane", -> [container, pane1, pane2] = [] beforeEach -> - container = new PaneContainer(root: new Pane) - container.getRoot().splitRight() + container = new PaneContainer(config: atom.config, applicationDelegate: atom.applicationDelegate) + container.getActivePane().splitRight() [pane1, pane2] = container.getPanes() it "changes the active pane on the container", -> @@ -76,14 +87,14 @@ describe "Pane", -> describe "::addItem(item, index)", -> it "adds the item at the given index", -> - pane = new Pane(items: [new Item("A"), new Item("B")]) + pane = new Pane(paneParams(items: [new Item("A"), new Item("B")])) [item1, item2] = pane.getItems() item3 = new Item("C") pane.addItem(item3, 1) expect(pane.getItems()).toEqual [item1, item3, item2] it "adds the item after the active item if no index is provided", -> - pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) + pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")])) [item1, item2, item3] = pane.getItems() pane.activateItem(item2) item4 = new Item("D") @@ -91,13 +102,13 @@ describe "Pane", -> expect(pane.getItems()).toEqual [item1, item2, item4, item3] it "sets the active item after adding the first item", -> - pane = new Pane + pane = new Pane(paneParams()) item = new Item("A") pane.addItem(item) expect(pane.getActiveItem()).toBe item it "invokes ::onDidAddItem() observers", -> - pane = new Pane(items: [new Item("A"), new Item("B")]) + pane = new Pane(paneParams(items: [new Item("A"), new Item("B")])) events = [] pane.onDidAddItem (event) -> events.push(event) @@ -107,13 +118,14 @@ describe "Pane", -> it "throws an exception if the item is already present on a pane", -> item = new Item("A") - pane1 = new Pane(items: [item]) - container = new PaneContainer(root: pane1) + container = new PaneContainer(config: atom.config, applicationDelegate: atom.applicationDelegate) + pane1 = container.getActivePane() + pane1.addItem(item) pane2 = pane1.splitRight() expect(-> pane2.addItem(item)).toThrow() it "throws an exception if the item isn't an object", -> - pane = new Pane(items: []) + pane = new Pane(paneParams(items: [])) expect(-> pane.addItem(null)).toThrow() expect(-> pane.addItem('foo')).toThrow() expect(-> pane.addItem(1)).toThrow() @@ -122,7 +134,7 @@ describe "Pane", -> pane = null beforeEach -> - pane = new Pane(items: [new Item("A"), new Item("B")]) + pane = new Pane(paneParams(items: [new Item("A"), new Item("B")])) it "changes the active item to the current item", -> expect(pane.getActiveItem()).toBe pane.itemAtIndex(0) @@ -143,7 +155,7 @@ describe "Pane", -> describe "::activateNextItem() and ::activatePreviousItem()", -> it "sets the active item to the next/previous item, looping around at either end", -> - pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) + pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")])) [item1, item2, item3] = pane.getItems() expect(pane.getActiveItem()).toBe item1 @@ -158,7 +170,7 @@ describe "Pane", -> describe "::moveItemRight() and ::moveItemLeft()", -> it "moves the active item to the right and left, without looping around at either end", -> - pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) + pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")])) [item1, item2, item3] = pane.getItems() pane.activateItemAtIndex(0) @@ -176,7 +188,7 @@ describe "Pane", -> describe "::activateItemAtIndex(index)", -> it "activates the item at the given index", -> - pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) + pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")])) [item1, item2, item3] = pane.getItems() pane.activateItemAtIndex(2) expect(pane.getActiveItem()).toBe item3 @@ -195,7 +207,7 @@ describe "Pane", -> [pane, item1, item2, item3] = [] beforeEach -> - pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) + pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")])) [item1, item2, item3] = pane.getItems() it "removes the item from the items list and destroyes it", -> @@ -259,7 +271,7 @@ describe "Pane", -> describe "when the item has a uri", -> it "saves the item before destroying it", -> itemURI = "test" - spyOn(atom, 'confirm').andReturn(0) + confirm.andReturn(0) pane.destroyItem(item1) expect(item1.save).toHaveBeenCalled() @@ -270,18 +282,18 @@ describe "Pane", -> it "presents a save-as dialog, then saves the item with the given uri before removing and destroying it", -> itemURI = null - spyOn(atom, 'showSaveDialogSync').andReturn("/selected/path") - spyOn(atom, 'confirm').andReturn(0) + showSaveDialog.andReturn("/selected/path") + confirm.andReturn(0) pane.destroyItem(item1) - expect(atom.showSaveDialogSync).toHaveBeenCalled() + expect(showSaveDialog).toHaveBeenCalled() expect(item1.saveAs).toHaveBeenCalledWith("/selected/path") expect(item1 in pane.getItems()).toBe false expect(item1.isDestroyed()).toBe true describe "if the [Don't Save] option is selected", -> it "removes and destroys the item without saving it", -> - spyOn(atom, 'confirm').andReturn(2) + confirm.andReturn(2) pane.destroyItem(item1) expect(item1.save).not.toHaveBeenCalled() @@ -290,7 +302,7 @@ describe "Pane", -> describe "if the [Cancel] option is selected", -> it "does not save, remove, or destroy the item", -> - spyOn(atom, 'confirm').andReturn(1) + confirm.andReturn(1) pane.destroyItem(item1) expect(item1.save).not.toHaveBeenCalled() @@ -315,19 +327,19 @@ describe "Pane", -> describe "::destroyActiveItem()", -> it "destroys the active item", -> - pane = new Pane(items: [new Item("A"), new Item("B")]) + pane = new Pane(paneParams(items: [new Item("A"), new Item("B")])) activeItem = pane.getActiveItem() pane.destroyActiveItem() expect(activeItem.isDestroyed()).toBe true expect(activeItem in pane.getItems()).toBe false it "does not throw an exception if there are no more items", -> - pane = new Pane + pane = new Pane(paneParams()) pane.destroyActiveItem() describe "::destroyItems()", -> it "destroys all items", -> - pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) + pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")])) [item1, item2, item3] = pane.getItems() pane.destroyItems() expect(item1.isDestroyed()).toBe true @@ -337,7 +349,7 @@ describe "Pane", -> describe "::observeItems()", -> it "invokes the observer with all current and future items", -> - pane = new Pane(items: [new Item, new Item]) + pane = new Pane(paneParams(items: [new Item, new Item])) [item1, item2] = pane.getItems() observed = [] @@ -350,14 +362,14 @@ describe "Pane", -> describe "when an item emits a destroyed event", -> it "removes it from the list of items", -> - pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) + pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")])) [item1, item2, item3] = pane.getItems() pane.itemAtIndex(1).destroy() expect(pane.getItems()).toEqual [item1, item3] describe "::destroyInactiveItems()", -> it "destroys all items but the active item", -> - pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) + pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")])) [item1, item2, item3] = pane.getItems() pane.activateItem(item2) pane.destroyInactiveItems() @@ -367,8 +379,8 @@ describe "Pane", -> pane = null beforeEach -> - pane = new Pane(items: [new Item("A")]) - spyOn(atom, 'showSaveDialogSync').andReturn('/selected/path') + pane = new Pane(paneParams(items: [new Item("A")])) + showSaveDialog.andReturn('/selected/path') describe "when the active item has a uri", -> beforeEach -> @@ -390,14 +402,14 @@ describe "Pane", -> it "opens a save dialog and saves the current item as the selected path", -> pane.getActiveItem().saveAs = jasmine.createSpy("saveAs") pane.saveActiveItem() - expect(atom.showSaveDialogSync).toHaveBeenCalled() + expect(showSaveDialog).toHaveBeenCalled() expect(pane.getActiveItem().saveAs).toHaveBeenCalledWith('/selected/path') describe "when the current item has no saveAs method", -> it "does nothing", -> expect(pane.getActiveItem().saveAs).toBeUndefined() pane.saveActiveItem() - expect(atom.showSaveDialogSync).not.toHaveBeenCalled() + expect(showSaveDialog).not.toHaveBeenCalled() describe "when the item's saveAs method throws a well-known IO error", -> notificationSpy = null @@ -422,22 +434,22 @@ describe "Pane", -> pane = null beforeEach -> - pane = new Pane(items: [new Item("A")]) - spyOn(atom, 'showSaveDialogSync').andReturn('/selected/path') + pane = new Pane(paneParams(items: [new Item("A")])) + showSaveDialog.andReturn('/selected/path') describe "when the current item has a saveAs method", -> it "opens the save dialog and calls saveAs on the item with the selected path", -> pane.getActiveItem().path = __filename pane.getActiveItem().saveAs = jasmine.createSpy("saveAs") pane.saveActiveItemAs() - expect(atom.showSaveDialogSync).toHaveBeenCalledWith(defaultPath: __filename) + expect(showSaveDialog).toHaveBeenCalledWith(defaultPath: __filename) expect(pane.getActiveItem().saveAs).toHaveBeenCalledWith('/selected/path') describe "when the current item does not have a saveAs method", -> it "does nothing", -> expect(pane.getActiveItem().saveAs).toBeUndefined() pane.saveActiveItemAs() - expect(atom.showSaveDialogSync).not.toHaveBeenCalled() + expect(showSaveDialog).not.toHaveBeenCalled() describe "when the item's saveAs method throws a well-known IO error", -> notificationSpy = null @@ -460,7 +472,7 @@ describe "Pane", -> 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")]) + pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C"), new Item("D")])) [item1, item2, item3] = pane.getItems() item1.uri = "a" item2.uri = "b" @@ -472,7 +484,7 @@ describe "Pane", -> [pane, item1, item2, item3, item4] = [] beforeEach -> - pane = new Pane(items: [new Item("A"), new Item("B"), new Item("C"), new Item("D")]) + pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C"), new Item("D")])) [item1, item2, item3, item4] = pane.getItems() it "moves the item to the given index and invokes ::onDidMoveItem observers", -> @@ -501,8 +513,9 @@ describe "Pane", -> [item1, item2, item3, item4, item5] = [] beforeEach -> - pane1 = new Pane(items: [new Item("A"), new Item("B"), new Item("C")]) - container = new PaneContainer(root: pane1) + container = new PaneContainer(config: atom.config, confirm: confirm) + pane1 = container.getActivePane() + pane1.addItems([new Item("A"), new Item("B"), new Item("C")]) pane2 = pane1.splitRight(items: [new Item("D"), new Item("E")]) [item1, item2, item3] = pane1.getItems() [item4, item5] = pane2.getItems() @@ -553,8 +566,9 @@ describe "Pane", -> [pane1, container] = [] beforeEach -> - pane1 = new Pane(items: [new Item("A")]) - container = new PaneContainer(root: pane1) + container = new PaneContainer(config: atom.config, confirm: confirm, deserializerManager: atom.deserializers) + pane1 = container.getActivePane() + pane1.addItem(new Item("A")) describe "::splitLeft(params)", -> describe "when the parent is the container root", -> @@ -652,32 +666,32 @@ describe "Pane", -> describe "::close()", -> it "prompts to save unsaved items before destroying the pane", -> - pane = new Pane(items: [new Item("A"), new Item("B")]) + pane = new Pane(paneParams(items: [new Item("A"), new Item("B")])) [item1, item2] = pane.getItems() item1.shouldPromptToSave = -> true item1.getURI = -> "/test/path" item1.save = jasmine.createSpy("save") - spyOn(atom, 'confirm').andReturn(0) + confirm.andReturn(0) pane.close() - expect(atom.confirm).toHaveBeenCalled() + expect(confirm).toHaveBeenCalled() expect(item1.save).toHaveBeenCalled() expect(pane.isDestroyed()).toBe true it "does not destroy the pane if cancel is called", -> - pane = new Pane(items: [new Item("A"), new Item("B")]) + pane = new Pane(paneParams(items: [new Item("A"), new Item("B")])) [item1, item2] = pane.getItems() item1.shouldPromptToSave = -> true item1.getURI = -> "/test/path" item1.save = jasmine.createSpy("save") - spyOn(atom, 'confirm').andReturn(1) + confirm.andReturn(1) pane.close() - expect(atom.confirm).toHaveBeenCalled() + expect(confirm).toHaveBeenCalled() expect(item1.save).not.toHaveBeenCalled() expect(pane.isDestroyed()).toBe false @@ -685,7 +699,7 @@ describe "Pane", -> [container, pane1, pane2] = [] beforeEach -> - container = new PaneContainer + container = new PaneContainer(config: atom.config, confirm: confirm) pane1 = container.root pane1.addItems([new Item("A"), new Item("B")]) pane2 = pane1.splitRight() @@ -732,18 +746,18 @@ describe "Pane", -> pane = null beforeEach -> - params = + pane = new Pane(paneParams( items: [new Item("A", "a"), new Item("B", "b"), new Item("C", "c")] flexScale: 2 - pane = new Pane(params) + )) it "can serialize and deserialize the pane and all its items", -> - newPane = Pane.deserialize(pane.serialize()) + newPane = Pane.deserialize(pane.serialize(), atom) expect(newPane.getItems()).toEqual pane.getItems() it "restores the active item on deserialization", -> pane.activateItemAtIndex(1) - newPane = Pane.deserialize(pane.serialize()) + newPane = Pane.deserialize(pane.serialize(), atom) expect(newPane.getActiveItem()).toEqual newPane.itemAtIndex(1) it "does not include items that cannot be deserialized", -> @@ -751,11 +765,11 @@ describe "Pane", -> unserializable = {} pane.activateItem(unserializable) - newPane = Pane.deserialize(pane.serialize()) + newPane = Pane.deserialize(pane.serialize(), atom) expect(newPane.getActiveItem()).toEqual pane.itemAtIndex(0) expect(newPane.getItems().length).toBe pane.getItems().length - 1 it "includes the pane's focus state in the serialized state", -> pane.focus() - newPane = Pane.deserialize(pane.serialize()) + newPane = Pane.deserialize(pane.serialize(), atom) expect(newPane.focused).toBe true diff --git a/spec/panel-container-element-spec.coffee b/spec/panel-container-element-spec.coffee index bd4ed6bd9..65577221a 100644 --- a/spec/panel-container-element-spec.coffee +++ b/spec/panel-container-element-spec.coffee @@ -20,10 +20,6 @@ describe "PanelContainerElement", -> beforeEach -> jasmineContent = document.body.querySelector('#jasmine-content') - atom.views.addViewProvider Panel, (model) -> - new PanelElement().initialize(model) - atom.views.addViewProvider PanelContainer, (model) -> - new PaneContainerElement().initialize(model) atom.views.addViewProvider TestPanelContainerItem, (model) -> new TestPanelContainerItemElement().initialize(model) diff --git a/spec/panel-element-spec.coffee b/spec/panel-element-spec.coffee index 4c166c617..73108e25d 100644 --- a/spec/panel-element-spec.coffee +++ b/spec/panel-element-spec.coffee @@ -18,8 +18,8 @@ describe "PanelElement", -> beforeEach -> jasmineContent = document.body.querySelector('#jasmine-content') - atom.views.addViewProvider Panel, (model) -> - new PanelElement().initialize(model) + atom.views.addViewProvider Panel, (model, env) -> + new PanelElement().initialize(model, env) atom.views.addViewProvider TestPanelItem, (model) -> new TestPanelItemElement().initialize(model) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 13dd3e70c..2c88aad50 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -16,44 +16,6 @@ describe "Project", -> # Wait for project's service consumers to be asynchronously added waits(1) - describe "when a new repository-provider is added", -> - it "uses it to create repositories for any directories that need one", -> - projectPath = temp.mkdirSync('atom-project') - atom.project.setPaths([projectPath]) - expect(atom.project.getRepositories()).toEqual [null] - expect(atom.project.repositoryProviders.length).toEqual 1 - - dummyRepository = {destroy: -> null} - - atom.packages.serviceHub.provide("atom.repository-provider", "0.1.0", { - repositoryForDirectory: (directory) -> Promise.resolve(dummyRepository) - repositoryForDirectorySync: (directory) -> dummyRepository - }) - - repository = null - - waitsFor "repository to be updated", -> - repository = atom.project.getRepositories()[0] - - runs -> - expect(repository).toBe dummyRepository - - it "does not create any new repositories if every directory has a repository", -> - repositories = atom.project.getRepositories() - expect(repositories.length).toEqual 1 - [repository] = repositories - expect(repository).toBeTruthy() - - # Register a new RepositoryProvider. - dummyRepository = destroy: -> - repositoryProvider = - repositoryForDirectory: (directory) -> Promise.resolve(dummyRepository) - repositoryForDirectorySync: (directory) -> dummyRepository - atom.packages.serviceHub.provide( - "atom.repository-provider", "0.1.0", repositoryProvider) - - expect(atom.project.getRepositories()).toBe repositories - describe "serialization", -> deserializedProject = null @@ -66,16 +28,19 @@ describe "Project", -> runs -> expect(atom.project.getBuffers().length).toBe 1 - deserializedProject = Project.deserialize(atom.project.serialize()) + + deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + deserializedProject.deserialize(atom.project.serialize(), atom.deserializers) expect(deserializedProject.getBuffers().length).toBe 0 it "listens for destroyed events on deserialized buffers and removes them when they are destroyed", -> waitsForPromise -> - atom.project.open('a') + atom.workspace.open('a') runs -> expect(atom.project.getBuffers().length).toBe 1 - deserializedProject = Project.deserialize(atom.project.serialize()) + deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + deserializedProject.deserialize(atom.project.serialize(), atom.deserializers) expect(deserializedProject.getBuffers().length).toBe 1 deserializedProject.getBuffers()[0].destroy() @@ -86,12 +51,13 @@ describe "Project", -> pathToOpen = path.join(temp.mkdirSync(), 'file.txt') waitsForPromise -> - atom.project.open(pathToOpen) + atom.workspace.open(pathToOpen) runs -> expect(atom.project.getBuffers().length).toBe 1 fs.mkdirSync(pathToOpen) - deserializedProject = Project.deserialize(atom.project.serialize()) + deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + deserializedProject.deserialize(atom.project.serialize(), atom.deserializers) expect(deserializedProject.getBuffers().length).toBe 0 it "does not deserialize buffers when their path is inaccessible", -> @@ -99,12 +65,13 @@ describe "Project", -> fs.writeFileSync(pathToOpen, '') waitsForPromise -> - atom.project.open(pathToOpen) + atom.workspace.open(pathToOpen) runs -> expect(atom.project.getBuffers().length).toBe 1 fs.chmodSync(pathToOpen, '000') - deserializedProject = Project.deserialize(atom.project.serialize()) + deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + deserializedProject.deserialize(atom.project.serialize(), atom.deserializers) expect(deserializedProject.getBuffers().length).toBe 0 describe "when an editor is saved and the project has no path", -> @@ -115,7 +82,7 @@ describe "Project", -> editor = null waitsForPromise -> - atom.project.open().then (o) -> editor = o + atom.workspace.open().then (o) -> editor = o runs -> editor.saveAs(tempFile) @@ -125,7 +92,7 @@ describe "Project", -> editor = null beforeEach -> waitsForPromise -> - atom.project.open(require.resolve('./fixtures/dir/a')).then (o) -> editor = o + atom.workspace.open(require.resolve('./fixtures/dir/a')).then (o) -> editor = o it "creates a warning notification", -> atom.notifications.onDidAddNotification noteSpy = jasmine.createSpy() @@ -144,6 +111,106 @@ describe "Project", -> expect(notification.getMessage()).toContain '`resurrect`' expect(notification.getMessage()).toContain 'fixtures/dir/a' + describe "when a custom repository-provider service is provided", -> + [fakeRepositoryProvider, fakeRepository] = [] + + beforeEach -> + fakeRepository = {destroy: -> null} + fakeRepositoryProvider = { + repositoryForDirectory: (directory) -> Promise.resolve(fakeRepository) + repositoryForDirectorySync: (directory) -> fakeRepository + } + + it "uses it to create repositories for any directories that need one", -> + projectPath = temp.mkdirSync('atom-project') + atom.project.setPaths([projectPath]) + expect(atom.project.getRepositories()).toEqual [null] + + atom.packages.serviceHub.provide("atom.repository-provider", "0.1.0", fakeRepositoryProvider) + waitsFor -> atom.project.repositoryProviders.length > 1 + runs -> atom.project.getRepositories()[0] is fakeRepository + + it "does not create any new repositories if every directory has a repository", -> + repositories = atom.project.getRepositories() + expect(repositories.length).toEqual 1 + expect(repositories[0]).toBeTruthy() + + atom.packages.serviceHub.provide("atom.repository-provider", "0.1.0", fakeRepositoryProvider) + waitsFor -> atom.project.repositoryProviders.length > 1 + runs -> expect(atom.project.getRepositories()).toBe repositories + + it "stops using it to create repositories when the service is removed", -> + atom.project.setPaths([]) + + disposable = atom.packages.serviceHub.provide("atom.repository-provider", "0.1.0", fakeRepositoryProvider) + waitsFor -> atom.project.repositoryProviders.length > 1 + runs -> + disposable.dispose() + atom.project.addPath(temp.mkdirSync('atom-project')) + expect(atom.project.getRepositories()).toEqual [null] + + describe "when a custom directory-provider service is provided", -> + class DummyDirectory + constructor: (@path) -> + getPath: -> @path + getFile: -> {existsSync: -> false} + getSubdirectory: -> {existsSync: -> false} + isRoot: -> true + existsSync: -> @path.endsWith('does-exist') + contains: (filePath) -> filePath.startsWith(@path) + + serviceDisposable = null + + beforeEach -> + serviceDisposable = atom.packages.serviceHub.provide("atom.directory-provider", "0.1.0", { + directoryForURISync: (uri) -> + if uri.startsWith("ssh://") + new DummyDirectory(uri) + else + null + }) + + waitsFor -> + atom.project.directoryProviders.length > 0 + + it "uses the provider's custom directories for any paths that it handles", -> + localPath = temp.mkdirSync('local-path') + remotePath = "ssh://foreign-directory:8080/does-exist" + + atom.project.setPaths([localPath, remotePath]) + + directories = atom.project.getDirectories() + expect(directories[0].getPath()).toBe localPath + expect(directories[0] instanceof Directory).toBe true + expect(directories[1].getPath()).toBe remotePath + expect(directories[1] instanceof DummyDirectory).toBe true + + # It does not add new remote paths if their directories do not exist + # and they are contained by existing remote paths. + childRemotePath = remotePath + "/subdirectory/that/does-not-exist" + atom.project.addPath(childRemotePath) + expect(atom.project.getDirectories().length).toBe 2 + + # It does add new remote paths if their directories exist. + childRemotePath = remotePath + "/subdirectory/that/does-exist" + atom.project.addPath(childRemotePath) + directories = atom.project.getDirectories() + expect(directories[2].getPath()).toBe childRemotePath + expect(directories[2] instanceof DummyDirectory).toBe true + + # It does add new remote paths to be added if they are not contained by + # previous remote paths. + otherRemotePath = "ssh://other-foreign-directory:8080/" + atom.project.addPath(otherRemotePath) + directories = atom.project.getDirectories() + expect(directories[3].getPath()).toBe otherRemotePath + expect(directories[3] instanceof DummyDirectory).toBe true + + it "stops using the provider when the service is removed", -> + serviceDisposable.dispose() + atom.project.setPaths(["ssh://foreign-directory:8080/does-exist"]) + expect(atom.project.getDirectories()[0] instanceof Directory).toBe true + describe ".open(path)", -> [absolutePath, newBufferHandler] = [] @@ -156,7 +223,7 @@ describe "Project", -> it "returns a new edit session for the given path and emits 'buffer-created'", -> editor = null waitsForPromise -> - atom.project.open(absolutePath).then (o) -> editor = o + atom.workspace.open(absolutePath).then (o) -> editor = o runs -> expect(editor.buffer.getPath()).toBe absolutePath @@ -166,7 +233,7 @@ describe "Project", -> it "returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", -> editor = null waitsForPromise -> - atom.project.open(absolutePath).then (o) -> editor = o + atom.workspace.open(absolutePath).then (o) -> editor = o runs -> expect(editor.buffer.getPath()).toBe absolutePath @@ -177,17 +244,17 @@ describe "Project", -> editor = null waitsForPromise -> - atom.project.open(absolutePath).then (o) -> editor = o + atom.workspace.open(absolutePath).then (o) -> editor = o runs -> newBufferHandler.reset() waitsForPromise -> - atom.project.open(absolutePath).then ({buffer}) -> + atom.workspace.open(absolutePath).then ({buffer}) -> expect(buffer).toBe editor.buffer waitsForPromise -> - atom.project.open('a').then ({buffer}) -> + atom.workspace.open('a').then ({buffer}) -> expect(buffer).toBe editor.buffer expect(newBufferHandler).not.toHaveBeenCalled() @@ -195,7 +262,7 @@ describe "Project", -> it "returns a new edit session and emits 'buffer-created'", -> editor = null waitsForPromise -> - atom.project.open().then (o) -> editor = o + atom.workspace.open().then (o) -> editor = o runs -> expect(editor.buffer.getPath()).toBeUndefined() @@ -305,68 +372,6 @@ describe "Project", -> expect(directories.length).toBe 1 expect(directories[0].getPath()).toBe path.normalize(nonLocalFsDirectory) - describe "when a custom directory provider has been added", -> - describe "when custom provider handles the given path", -> - it "creates a directory using that provider", -> - class DummyDirectory - constructor: (@path) -> - getPath: -> @path - getFile: -> {existsSync: -> false} - getSubdirectory: -> {existsSync: -> false} - isRoot: -> true - existsSync: -> /does-exist/.test(@path) - off: -> - contains: (filePath) -> filePath.startsWith(@path) - - atom.packages.serviceHub.provide("atom.directory-provider", "0.1.0", { - directoryForURISync: (uri) -> - if uri.startsWith("ssh://") - new DummyDirectory(uri) - else - null - }) - - localPath = temp.mkdirSync('local-path') - remotePath = "ssh://foreign-directory:8080/exists" - - atom.project.setPaths([localPath, remotePath]) - - directories = atom.project.getDirectories() - expect(directories[0].getPath()).toBe localPath - expect(directories[0] instanceof Directory).toBe true - expect(directories[1].getPath()).toBe remotePath - expect(directories[1] instanceof DummyDirectory).toBe true - - # Make sure that DummyDirectory.contains() is honored. - remotePathSubdirectory = remotePath + "a/subdirectory" - atom.project.addPath(remotePathSubdirectory) - expect(atom.project.getDirectories().length).toBe 2 - - # Make sure that a new DummyDirectory that is not contained by the first - # DummyDirectory can be added. - otherRemotePath = "ssh://other-foreign-directory:8080/" - atom.project.addPath(otherRemotePath) - newDirectories = atom.project.getDirectories() - expect(newDirectories.length).toBe 3 - otherDummyDirectory = newDirectories[2] - expect(otherDummyDirectory.getPath()).toBe otherRemotePath - expect(otherDummyDirectory instanceof DummyDirectory).toBe true - - describe "when a custom provider does not handle the path", -> - it "creates a local directory for the path", -> - directoryProvider = - directoryForURISync: (uri) -> null - directoryForURI: (uri) -> throw new Error("This should not be called.") - - atom.packages.serviceHub.provide( - "atom.directory-provider", "0.1.0", directoryProvider) - - tmp = temp.mkdirSync() - atom.project.setPaths([tmp]) - directories = atom.project.getDirectories() - expect(directories.length).toBe 1 - expect(directories[0].getPath()).toBe tmp - describe ".addPath(path)", -> it "calls callbacks registered with ::onDidChangePaths", -> onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') diff --git a/spec/random-editor-spec.coffee b/spec/random-editor-spec.coffee index d235ebc25..3924a8412 100644 --- a/spec/random-editor-spec.coffee +++ b/spec/random-editor-spec.coffee @@ -15,7 +15,7 @@ describe "TextEditor", -> it "properly renders soft-wrapped lines when randomly mutated", -> times 10, (i) -> buffer = new TextBuffer - editor = new TextEditor({buffer}) + editor = atom.workspace.buildTextEditor({buffer}) editor.setEditorWidthInChars(80) tokenizedBuffer = editor.displayBuffer.tokenizedBuffer steps = [] @@ -80,7 +80,7 @@ describe "TextEditor", -> text getReferenceScreenLines = -> - referenceEditor = new TextEditor({}) + referenceEditor = atom.workspace.buildTextEditor() referenceEditor.setEditorWidthInChars(80) referenceEditor.setText(editor.getText()) referenceEditor.setSoftWrapped(editor.isSoftWrapped()) diff --git a/spec/sample-with-comments.js b/spec/sample-with-comments.js new file mode 100644 index 000000000..66dc9051d --- /dev/null +++ b/spec/sample-with-comments.js @@ -0,0 +1 @@ +undefined \ No newline at end of file diff --git a/spec/sample.js b/spec/sample.js new file mode 100644 index 000000000..66dc9051d --- /dev/null +++ b/spec/sample.js @@ -0,0 +1 @@ +undefined \ No newline at end of file diff --git a/spec/selection-spec.coffee b/spec/selection-spec.coffee index e81f7906f..ec40e32cc 100644 --- a/spec/selection-spec.coffee +++ b/spec/selection-spec.coffee @@ -5,7 +5,7 @@ describe "Selection", -> beforeEach -> buffer = atom.project.bufferForPathSync('sample.js') - editor = new TextEditor(buffer: buffer, tabLength: 2) + editor = atom.workspace.buildTextEditor(buffer: buffer, tabLength: 2) selection = editor.getLastSelection() afterEach -> diff --git a/spec/spec-bootstrap.coffee b/spec/spec-bootstrap.coffee deleted file mode 100644 index 5158c9bee..000000000 --- a/spec/spec-bootstrap.coffee +++ /dev/null @@ -1,30 +0,0 @@ -# Start the crash reporter before anything else. -require('crash-reporter').start(productName: 'Atom', companyName: 'GitHub') - -path = require 'path' - -try - require '../src/window' - Atom = require '../src/atom' - window.atom = Atom.loadOrCreate('spec') - - # Show window synchronously so a focusout doesn't fire on input elements - # that are focused in the very first spec run. - atom.getCurrentWindow().show() unless atom.getLoadSettings().exitWhenDone - - {runSpecSuite} = require './jasmine-helper' - - # Add 'exports' to module search path. - exportsPath = path.join(atom.getLoadSettings().resourcePath, 'exports') - require('module').globalPaths.push(exportsPath) - # Still set NODE_PATH since tasks may need it. - process.env.NODE_PATH = exportsPath - - document.title = "Spec Suite" - runSpecSuite './spec-suite', atom.getLoadSettings().logFile -catch error - if atom?.getLoadSettings().exitWhenDone - console.error(error.stack ? error) - atom.exit(1) - else - throw error diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 085db7835..9a2ef83ce 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -1,6 +1,4 @@ require '../src/window' -atom.initialize() -atom.restoreWindowDimensions() require 'jasmine-json' require '../vendor/jasmine-jquery' @@ -8,45 +6,27 @@ path = require 'path' _ = require 'underscore-plus' fs = require 'fs-plus' Grim = require 'grim' -KeymapManager = require '../src/keymap-extensions' +pathwatcher = require 'pathwatcher' +FindParentDir = require 'find-parent-dir' -Config = require '../src/config' {Point} = require 'text-buffer' -Project = require '../src/project' Workspace = require '../src/workspace' -ServiceHub = require 'service-hub' TextEditor = require '../src/text-editor' TextEditorElement = require '../src/text-editor-element' TokenizedBuffer = require '../src/tokenized-buffer' TextEditorComponent = require '../src/text-editor-component' -pathwatcher = require 'pathwatcher' clipboard = require '../src/safe-clipboard' -atom.themes.loadBaseStylesheets() -atom.themes.requireStylesheet '../static/jasmine' -atom.themes.initialLoadComplete = true +jasmineStyle = document.createElement('style') +jasmineStyle.textContent = atom.themes.loadStylesheet(atom.themes.resolveStylesheet('../static/jasmine')) +document.head.appendChild(jasmineStyle) fixturePackagesPath = path.resolve(__dirname, './fixtures/packages') atom.packages.packageDirPaths.unshift(fixturePackagesPath) -atom.keymaps.loadBundledKeymaps() -keyBindingsToRestore = atom.keymaps.getKeyBindings() -commandsToRestore = atom.commands.getSnapshot() -styleElementsToRestore = atom.styles.getSnapshot() - -window.addEventListener 'core:close', -> window.close() -window.addEventListener 'beforeunload', -> - atom.storeWindowDimensions() - atom.saveSync() document.querySelector('html').style.overflow = 'auto' document.body.style.overflow = 'auto' -# Allow document.title to be assigned in specs without screwing up spec window title -documentTitle = null -Object.defineProperty document, 'title', - get: -> documentTitle - set: (title) -> documentTitle = title - Set.prototype.jasmineToString = -> result = "Set {" first = true @@ -73,46 +53,27 @@ if process.env.CI else jasmine.getEnv().defaultTimeoutInterval = 5000 -specPackageName = null -specPackagePath = null -specProjectPath = null -isCoreSpec = false +{resourcePath, testPaths} = atom.getLoadSettings() -{specDirectory, resourcePath} = atom.getLoadSettings() +if specPackagePath = FindParentDir.sync(testPaths[0], 'package.json') + packageMetadata = require(path.join(specPackagePath, 'package.json')) + specPackageName = packageMetadata.name -if specDirectory - specPackagePath = path.resolve(specDirectory, '..') - try - specPackageName = JSON.parse(fs.readFileSync(path.join(specPackagePath, 'package.json')))?.name +if specDirectory = FindParentDir.sync(testPaths[0], 'fixtures') specProjectPath = path.join(specDirectory, 'fixtures') - -isCoreSpec = specDirectory is fs.realpathSync(__dirname) +else + specProjectPath = path.join(__dirname, 'fixtures') beforeEach -> documentTitle = null - projectPath = specProjectPath ? path.join(@specDirectory, 'fixtures') - atom.packages.serviceHub = new ServiceHub - atom.project = new Project(paths: [projectPath]) - atom.workspace = new Workspace() - atom.keymaps.keyBindings = _.clone(keyBindingsToRestore) - atom.commands.restoreSnapshot(commandsToRestore) - atom.styles.restoreSnapshot(styleElementsToRestore) - atom.views.clearDocumentRequests() - atom.workspaceParentSelectorctor = '#jasmine-content' + atom.project.setPaths([specProjectPath]) window.resetTimeouts() spyOn(_._, "now").andCallFake -> window.now spyOn(window, "setTimeout").andCallFake window.fakeSetTimeout spyOn(window, "clearTimeout").andCallFake window.fakeClearTimeout - atom.packages.packageStates = {} - - serializedWindowState = null - - spyOn(atom, 'saveSync') - atom.grammars.clearGrammarOverrides() - spy = spyOn(atom.packages, 'resolvePackagePath').andCallFake (packageName) -> if specPackageName and packageName is specPackageName resolvePackagePath(specPackagePath) @@ -123,28 +84,20 @@ beforeEach -> # prevent specs from modifying Atom's menus spyOn(atom.menu, 'sendToBrowserProcess') - # reset config before each spec; don't load or save from/to `config.json` - spyOn(Config::, 'load') - spyOn(Config::, 'save') - config = new Config({resourcePath, configDirPath: atom.getConfigDirPath()}) - atom.config = config - atom.loadConfig() - config.set "core.destroyEmptyPanes", false - config.set "editor.fontFamily", "Courier" - config.set "editor.fontSize", 16 - config.set "editor.autoIndent", false - config.set "core.disabledPackages", ["package-that-throws-an-exception", + # reset config before each spec + atom.config.set "core.destroyEmptyPanes", false + atom.config.set "editor.fontFamily", "Courier" + atom.config.set "editor.fontSize", 16 + atom.config.set "editor.autoIndent", false + atom.config.set "core.disabledPackages", ["package-that-throws-an-exception", "package-with-broken-package-json", "package-with-broken-keymap"] - config.set "editor.useShadowDOM", true + atom.config.set "editor.useShadowDOM", true advanceClock(1000) window.setTimeout.reset() - config.load.reset() - config.save.reset() # make editor display updates synchronous TextEditorElement::setUpdatedSynchronously(true) - spyOn(atom, "setRepresentedFilename") spyOn(pathwatcher.File.prototype, "detectResurrectionAfterDelay").andCallFake -> @detectResurrection() spyOn(TextEditor.prototype, "shouldPromptToSave").andReturn false @@ -159,25 +112,10 @@ beforeEach -> addCustomMatchers(this) afterEach -> - atom.packages.deactivatePackages() - atom.menu.template = [] - atom.contextMenu.clear() - atom.notifications.clear() - - atom.workspace?.destroy() - atom.workspace = null - delete atom.state.workspace - - atom.project?.destroy() - atom.project = null - - atom.themes.removeStylesheet('global-editor-styles') - - delete atom.state.packageStates + atom.reset() document.getElementById('jasmine-content').innerHTML = '' unless window.debugContent - jasmine.unspy(atom, 'saveSync') ensureNoPathSubscriptions() waits(0) # yield to ui thread to make screen update more frequently diff --git a/spec/spec-suite.coffee b/spec/spec-suite.coffee deleted file mode 100644 index 817de7986..000000000 --- a/spec/spec-suite.coffee +++ /dev/null @@ -1,55 +0,0 @@ -_ = require 'underscore-plus' -fs = require 'fs-plus' -path = require 'path' -require './spec-helper' - -requireSpecs = (specDirectory, specType) -> - for specFilePath in fs.listTreeSync(specDirectory) when /-spec\.(coffee|js)$/.test specFilePath - require specFilePath - - # Set spec directory on spec for setting up the project in spec-helper - setSpecDirectory(specDirectory) - -setSpecField = (name, value) -> - specs = jasmine.getEnv().currentRunner().specs() - return if specs.length is 0 - for index in [specs.length-1..0] - break if specs[index][name]? - specs[index][name] = value - -setSpecType = (specType) -> - setSpecField('specType', specType) - -setSpecDirectory = (specDirectory) -> - setSpecField('specDirectory', specDirectory) - -runAllSpecs = -> - {resourcePath} = atom.getLoadSettings() - - requireSpecs(path.join(resourcePath, 'spec')) - setSpecType('core') - - fixturesPackagesPath = path.join(__dirname, 'fixtures', 'packages') - packagePaths = atom.packages.getAvailablePackageNames().map (packageName) -> - atom.packages.resolvePackagePath(packageName) - packagePaths = _.groupBy packagePaths, (packagePath) -> - if packagePath.indexOf("#{fixturesPackagesPath}#{path.sep}") is 0 - 'fixtures' - else if packagePath.indexOf("#{resourcePath}#{path.sep}") is 0 - 'bundled' - else - 'user' - - # Run bundled package specs - requireSpecs(path.join(packagePath, 'spec')) for packagePath in packagePaths.bundled ? [] - setSpecType('bundled') - - # Run user package specs - requireSpecs(path.join(packagePath, 'spec')) for packagePath in packagePaths.user ? [] - setSpecType('user') - -if specDirectory = atom.getLoadSettings().specDirectory - requireSpecs(specDirectory) - setSpecType('user') -else - runAllSpecs() diff --git a/spec/style-manager-spec.coffee b/spec/style-manager-spec.coffee index 7e601bcc1..d0a1cfe13 100644 --- a/spec/style-manager-spec.coffee +++ b/spec/style-manager-spec.coffee @@ -4,7 +4,7 @@ describe "StyleManager", -> [manager, addEvents, removeEvents, updateEvents] = [] beforeEach -> - manager = new StyleManager + manager = new StyleManager(configDirPath: atom.getConfigDirPath()) addEvents = [] removeEvents = [] updateEvents = [] diff --git a/spec/styles-element-spec.coffee b/spec/styles-element-spec.coffee index 7d2fe722a..b1a57938c 100644 --- a/spec/styles-element-spec.coffee +++ b/spec/styles-element-spec.coffee @@ -6,6 +6,7 @@ describe "StylesElement", -> beforeEach -> element = new StylesElement + element.initialize(atom.styles) document.querySelector('#jasmine-content').appendChild(element) addedStyleElements = [] removedStyleElements = [] @@ -99,8 +100,8 @@ describe "StylesElement", -> it "defers selector upgrade until the element is attached", -> element = new StylesElement + element.initialize(atom.styles) element.setAttribute('context', 'atom-text-editor') - element.initialize() atom.styles.addStyleSheet ".editor {background: black;}", context: 'atom-text-editor' expect(element.firstChild.sheet).toBeNull() diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 45e1dbb6b..b0f3ace51 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -27,7 +27,7 @@ describe "TextEditorComponent", -> fn() waitsForPromise -> - atom.project.open('sample.js').then (o) -> editor = o + atom.workspace.open('sample.js').then (o) -> editor = o runs -> contentNode = document.querySelector('#jasmine-content') @@ -35,7 +35,7 @@ describe "TextEditorComponent", -> wrapperNode = new TextEditorElement() wrapperNode.tileSize = tileSize - wrapperNode.initialize(editor) + wrapperNode.initialize(editor, atom) wrapperNode.setUpdatedSynchronously(false) jasmine.attachToDOM(wrapperNode) @@ -54,6 +54,10 @@ describe "TextEditorComponent", -> component.measureDimensions() nextAnimationFrame() + # Mutating the DOM in the previous frame causes a document poll; clear it here + waits 0 + runs -> nextAnimationFrame() + afterEach -> contentNode.style.width = '' @@ -2914,7 +2918,7 @@ describe "TextEditorComponent", -> wrapperNode = new TextEditorElement() wrapperNode.tileSize = tileSize - wrapperNode.initialize(editor) + wrapperNode.initialize(editor, atom) hiddenParent.appendChild(wrapperNode) {component} = wrapperNode @@ -3208,7 +3212,7 @@ describe "TextEditorComponent", -> waitsForPromise -> atom.packages.activatePackage('language-coffee-script') waitsForPromise -> - atom.project.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o + atom.workspace.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o afterEach: -> atom.packages.deactivatePackages() diff --git a/spec/text-editor-element-spec.coffee b/spec/text-editor-element-spec.coffee index 148199425..e7d5831eb 100644 --- a/spec/text-editor-element-spec.coffee +++ b/spec/text-editor-element-spec.coffee @@ -1,5 +1,6 @@ TextEditorElement = require '../src/text-editor-element' TextEditor = require '../src/text-editor' +{Disposable} = require 'event-kit' # The rest of text-editor-component-spec will be moved to this file when React # is eliminated. This covers only concerns related to the wrapper element for now @@ -33,7 +34,7 @@ describe "TextEditorElement", -> describe "when the model is assigned", -> it "adds the 'mini' attribute if .isMini() returns true on the model", -> element = new TextEditorElement - model = new TextEditor(mini: true) + model = atom.workspace.buildTextEditor(mini: true) element.setModel(model) expect(element.hasAttribute('mini')).toBe true @@ -67,7 +68,7 @@ describe "TextEditorElement", -> describe "when the editor is detached from the DOM and then reattached", -> it "does not render duplicate line numbers", -> - editor = new TextEditor + editor = atom.workspace.buildTextEditor() editor.setText('1\n2\n3') element = atom.views.getView(editor) @@ -80,7 +81,7 @@ describe "TextEditorElement", -> expect(element.shadowRoot.querySelectorAll('.line-number').length).toBe initialCount it "does not render duplicate decorations in custom gutters", -> - editor = new TextEditor + editor = atom.workspace.buildTextEditor() editor.setText('1\n2\n3') editor.addGutter({name: 'test-gutter'}) marker = editor.markBufferRange([[0, 0], [2, 0]]) @@ -159,6 +160,7 @@ describe "TextEditorElement", -> initialThemeLoadComplete spyOn(atom.themes, 'onDidChangeActiveThemes').andCallFake (fn) -> themeReloadCallback = fn + new Disposable atom.config.set("editor.useShadowDOM", false) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index acb9c29ae..1804b7b6b 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -18,7 +18,7 @@ describe "TextEditorPresenter", -> spyOn(window, "clearInterval").andCallFake window.fakeClearInterval buffer = new TextBuffer(filePath: require.resolve('./fixtures/sample.js')) - editor = new TextEditor({buffer}) + editor = atom.workspace.buildTextEditor({buffer}) waitsForPromise -> buffer.load() afterEach -> @@ -40,6 +40,7 @@ describe "TextEditorPresenter", -> verticalScrollbarWidth: 10 scrollTop: 0 scrollLeft: 0 + config: atom.config presenter = new TextEditorPresenter(params) presenter.setLinesYardstick(new FakeLinesYardstick(editor, presenter)) @@ -2860,7 +2861,7 @@ describe "TextEditorPresenter", -> performSetup = -> buffer = new TextBuffer - editor = new TextEditor({buffer}) + editor = atom.workspace.buildTextEditor({buffer}) editor.setEditorWidthInChars(80) presenterParams = model: editor diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index daba04714..dd01b816f 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -12,7 +12,7 @@ describe "TextEditor", -> beforeEach -> waitsForPromise -> - atom.project.open('sample.js', autoIndent: false).then (o) -> editor = o + atom.workspace.open('sample.js', autoIndent: false).then (o) -> editor = o runs -> buffer = editor.buffer @@ -27,11 +27,11 @@ describe "TextEditor", -> editor1 = null waitsForPromise -> - atom.project.open(pathToOpen).then (o) -> editor1 = o + atom.workspace.open(pathToOpen).then (o) -> editor1 = o runs -> fs.mkdirSync(pathToOpen) - expect(TextEditor.deserialize(editor1.serialize())).toBeUndefined() + expect(TextEditor.deserialize(editor1.serialize(), atom)).toBeUndefined() it "restores selections and folds based on markers in the buffer", -> editor.setSelectedBufferRange([[1, 2], [3, 4]]) @@ -39,7 +39,7 @@ describe "TextEditor", -> editor.foldBufferRow(4) expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - editor2 = TextEditor.deserialize(editor.serialize()) + editor2 = TextEditor.deserialize(editor.serialize(), atom) expect(editor2.id).toBe editor.id expect(editor2.getBuffer().getPath()).toBe editor.getBuffer().getPath() @@ -52,7 +52,7 @@ describe "TextEditor", -> atom.config.set('editor.showInvisibles', true) previousInvisibles = editor.tokenizedLineForScreenRow(0).invisibles - editor2 = TextEditor.deserialize(editor.serialize()) + editor2 = TextEditor.deserialize(editor.serialize(), atom) expect(previousInvisibles).toBeDefined() expect(editor2.displayBuffer.tokenizedLineForScreenRow(0).invisibles).toEqual previousInvisibles @@ -62,7 +62,7 @@ describe "TextEditor", -> state = editor.serialize() atom.config.set('editor.invisibles', eol: '?') - editor2 = TextEditor.deserialize(state) + editor2 = TextEditor.deserialize(state, atom) expect(editor.tokenizedLineForScreenRow(0).invisibles.eol).toBe '?' @@ -71,7 +71,7 @@ describe "TextEditor", -> editor = null waitsForPromise -> - atom.workspace.open('sample.js', largeFileMode: true).then (o) -> editor = o + atom.workspace.openTextFile('sample.js', largeFileMode: true).then (o) -> editor = o runs -> buffer = editor.getBuffer() @@ -114,7 +114,7 @@ describe "TextEditor", -> atom.config.set('core.fileEncoding', 'utf16le') waitsForPromise -> - atom.workspace.open('a').then (o) -> editor1 = o + atom.workspace.open('dir/a').then (o) -> editor1 = o runs -> expect(editor1.getTabLength()).toBe 4 @@ -128,7 +128,7 @@ describe "TextEditor", -> atom.config.set('core.fileEncoding', 'macroman') waitsForPromise -> - atom.workspace.open('b').then (o) -> editor2 = o + atom.workspace.open('dir/b').then (o) -> editor2 = o runs -> expect(editor2.getTabLength()).toBe 8 @@ -144,13 +144,13 @@ describe "TextEditor", -> atom.config.set('core.fileEncoding', 'macroman', scopeSelector: '.js') waitsForPromise -> - atom.workspace.open('a').then (o) -> editor1 = o + atom.workspace.open('dir/a').then (o) -> editor1 = o runs -> expect(editor1.getEncoding()).toBe 'utf16le' waitsForPromise -> - atom.workspace.open('test.js').then (o) -> editor2 = o + atom.workspace.open('sample-with-comments.js').then (o) -> editor2 = o runs -> expect(editor2.getEncoding()).toBe 'macroman' @@ -1339,7 +1339,7 @@ describe "TextEditor", -> waitsForPromise -> atom.packages.activatePackage('language-coffee-script') waitsForPromise -> - atom.project.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o + atom.workspace.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o it 'selects the correct surrounding word for the given scoped setting', -> coffeeEditor.setCursorBufferPosition [0, 9] # in the middle of quicksort @@ -1541,7 +1541,7 @@ describe "TextEditor", -> it "takes atomic tokens into account", -> waitsForPromise -> - atom.project.open('sample-with-tabs-and-leading-comment.coffee', autoIndent: false).then (o) -> editor = o + atom.workspace.open('sample-with-tabs-and-leading-comment.coffee', autoIndent: false).then (o) -> editor = o runs -> editor.setSelectedBufferRange([[2, 1], [2, 3]]) @@ -1654,7 +1654,7 @@ describe "TextEditor", -> it "takes atomic tokens into account", -> waitsForPromise -> - atom.project.open('sample-with-tabs-and-leading-comment.coffee', autoIndent: false).then (o) -> editor = o + atom.workspace.open('sample-with-tabs-and-leading-comment.coffee', autoIndent: false).then (o) -> editor = o runs -> editor.setSelectedBufferRange([[3, 1], [3, 2]]) @@ -1780,9 +1780,11 @@ describe "TextEditor", -> it "does not share selections between different edit sessions for the same buffer", -> editor2 = null waitsForPromise -> - atom.project.open('sample.js').then (o) -> editor2 = o + atom.workspace.getActivePane().splitRight() + atom.workspace.open(editor.getPath()).then (o) -> editor2 = o runs -> + expect(editor2.getText()).toBe(editor.getText()) editor.setSelectedBufferRanges([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) editor2.setSelectedBufferRanges([[[8, 7], [6, 5]], [[4, 3], [2, 1]]]) expect(editor2.getSelectedBufferRanges()).not.toEqual editor.getSelectedBufferRanges() @@ -3685,7 +3687,7 @@ describe "TextEditor", -> editor.destroy() waitsForPromise -> - atom.project.open('sample-with-tabs-and-leading-comment.coffee').then (o) -> editor = o + atom.workspace.open('sample-with-tabs-and-leading-comment.coffee').then (o) -> editor = o runs -> expect(editor.softTabs).toBe true @@ -3758,7 +3760,7 @@ describe "TextEditor", -> editor.destroy() waitsForPromise -> - atom.project.open('sample-with-tabs-and-leading-comment.coffee').then (o) -> editor = o + atom.workspace.open('sample-with-tabs-and-leading-comment.coffee').then (o) -> editor = o runs -> expect(editor.softTabs).toBe true @@ -3813,7 +3815,7 @@ describe "TextEditor", -> waitsForPromise -> atom.packages.activatePackage('language-coffee-script') waitsForPromise -> - atom.project.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o + atom.workspace.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o afterEach: -> atom.packages.deactivatePackages() @@ -4016,7 +4018,7 @@ describe "TextEditor", -> waitsForPromise -> atom.packages.activatePackage('language-coffee-script') waitsForPromise -> - atom.project.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o + atom.workspace.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o runs -> atom.config.set('editor.autoIndent', true, scopeSelector: '.source.js') @@ -4166,7 +4168,8 @@ describe "TextEditor", -> editor2 = null waitsForPromise -> - atom.project.open('sample.js', autoIndent: false).then (o) -> editor2 = o + atom.workspace.getActivePane().splitRight() + atom.workspace.open('sample.js', autoIndent: false).then (o) -> editor2 = o runs -> expect(editor.shouldPromptToSave()).toBeFalsy() @@ -4420,11 +4423,10 @@ describe "TextEditor", -> describe '.get/setPlaceholderText()', -> it 'can be created with placeholderText', -> - TextBuffer = require 'text-buffer' - newEditor = new TextEditor - buffer: new TextBuffer + newEditor = atom.workspace.buildTextEditor( mini: true placeholderText: 'yep' + ) expect(newEditor.getPlaceholderText()).toBe 'yep' it 'models placeholderText and emits an event when changed', -> @@ -4452,7 +4454,7 @@ describe "TextEditor", -> describe "when there's no repository for the editor's file", -> it "doesn't do anything", -> - editor = new TextEditor({}) + editor = atom.workspace.buildTextEditor() editor.setText("stuff") editor.checkoutHeadRevision() diff --git a/spec/token-iterator-spec.coffee b/spec/token-iterator-spec.coffee index ee8ac25e5..f876d30d1 100644 --- a/spec/token-iterator-spec.coffee +++ b/spec/token-iterator-spec.coffee @@ -24,7 +24,9 @@ describe "TokenIterator", -> end x x """) - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert + }) tokenizedBuffer.setGrammar(grammar) tokenIterator = tokenizedBuffer.tokenizedLineForRow(1).getTokenIterator() diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index 4945b37ec..e87125195 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -27,7 +27,9 @@ describe "TokenizedBuffer", -> describe "when the buffer is destroyed", -> beforeEach -> buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert + }) startTokenizing(tokenizedBuffer) it "stops tokenization", -> @@ -39,7 +41,9 @@ describe "TokenizedBuffer", -> describe "when the buffer contains soft-tabs", -> beforeEach -> buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert + }) startTokenizing(tokenizedBuffer) tokenizedBuffer.onDidChange changeHandler = jasmine.createSpy('changeHandler') @@ -345,7 +349,9 @@ describe "TokenizedBuffer", -> runs -> buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee') - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert + }) startTokenizing(tokenizedBuffer) afterEach -> @@ -450,7 +456,9 @@ describe "TokenizedBuffer", -> 'abc\uD835\uDF97def' //\uD835\uDF97xyz """ - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert + }) fullyTokenize(tokenizedBuffer) afterEach -> @@ -487,7 +495,7 @@ describe "TokenizedBuffer", -> tokenizedHandler = jasmine.createSpy("tokenized handler") waitsForPromise -> - atom.project.open('sample.js').then (o) -> editor = o + atom.workspace.open('sample.js').then (o) -> editor = o runs -> tokenizedBuffer = editor.displayBuffer.tokenizedBuffer @@ -500,7 +508,7 @@ describe "TokenizedBuffer", -> tokenizedHandler = jasmine.createSpy("tokenized handler") waitsForPromise -> - atom.project.open('sample.js').then (o) -> editor = o + atom.workspace.open('sample.js').then (o) -> editor = o runs -> tokenizedBuffer = editor.displayBuffer.tokenizedBuffer @@ -518,7 +526,7 @@ describe "TokenizedBuffer", -> tokenizedHandler = jasmine.createSpy("tokenized handler") waitsForPromise -> - atom.project.open('coffee.coffee').then (o) -> editor = o + atom.workspace.open('coffee.coffee').then (o) -> editor = o runs -> tokenizedBuffer = editor.displayBuffer.tokenizedBuffer @@ -544,7 +552,9 @@ describe "TokenizedBuffer", -> runs -> buffer = atom.project.bufferForPathSync() buffer.setText "
<%= User.find(2).full_name %>
" - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert + }) tokenizedBuffer.setGrammar(atom.grammars.selectGrammar('test.erb')) fullyTokenize(tokenizedBuffer) @@ -566,7 +576,9 @@ describe "TokenizedBuffer", -> it "returns the correct token (regression)", -> buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert + }) fullyTokenize(tokenizedBuffer) expect(tokenizedBuffer.tokenForPosition([1, 0]).scopes).toEqual ["source.js"] expect(tokenizedBuffer.tokenForPosition([1, 1]).scopes).toEqual ["source.js"] @@ -575,7 +587,10 @@ describe "TokenizedBuffer", -> describe ".bufferRangeForScopeAtPosition(selector, position)", -> beforeEach -> buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, + packageManager: atom.packages, assert: atom.assert + }) fullyTokenize(tokenizedBuffer) describe "when the selector does not match the token at the position", -> @@ -595,7 +610,9 @@ describe "TokenizedBuffer", -> it "updates the tab length of the tokenized lines", -> buffer = atom.project.bufferForPathSync('sample.js') buffer.setText('\ttest') - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert + }) fullyTokenize(tokenizedBuffer) expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' ' atom.config.set('editor.tabLength', 6) @@ -604,7 +621,9 @@ describe "TokenizedBuffer", -> it "does not allow the tab length to be less than 1", -> buffer = atom.project.bufferForPathSync('sample.js') buffer.setText('\ttest') - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert + }) fullyTokenize(tokenizedBuffer) expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' ' atom.config.set('editor.tabLength', 1) @@ -617,7 +636,9 @@ describe "TokenizedBuffer", -> it "updates the tokens with the appropriate invisible characters", -> buffer = new TextBuffer(text: " \t a line with tabs\tand \tspaces \t ") - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert + }) fullyTokenize(tokenizedBuffer) atom.config.set("editor.showInvisibles", true) @@ -630,7 +651,9 @@ describe "TokenizedBuffer", -> it "assigns endOfLineInvisibles to tokenized lines", -> buffer = new TextBuffer(text: "a line that ends in a carriage-return-line-feed \r\na line that ends in just a line-feed\na line with no ending") - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert + }) atom.config.set('editor.showInvisibles', true) atom.config.set("editor.invisibles", cr: 'R', eol: 'N') @@ -651,7 +674,9 @@ describe "TokenizedBuffer", -> describe "leading and trailing whitespace", -> beforeEach -> buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert + }) fullyTokenize(tokenizedBuffer) it "assigns ::firstNonWhitespaceIndex on tokens that have leading whitespace", -> @@ -709,7 +734,9 @@ describe "TokenizedBuffer", -> describe ".indentLevel on tokenized lines", -> beforeEach -> buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert + }) fullyTokenize(tokenizedBuffer) describe "when the line is non-empty", -> @@ -804,7 +831,9 @@ describe "TokenizedBuffer", -> buffer = atom.project.bufferForPathSync('sample.js') buffer.insert [10, 0], " // multi-line\n // comment\n // block\n" buffer.insert [0, 0], "// multi-line\n// comment\n// block\n" - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert + }) fullyTokenize(tokenizedBuffer) tokenizedBuffer.onDidChange (change) -> delete change.bufferChange @@ -881,7 +910,9 @@ describe "TokenizedBuffer", -> buffer = atom.project.bufferForPathSync('sample.will-use-the-null-grammar') buffer.setText('a\nb\nc') - tokenizedBuffer = new TokenizedBuffer({buffer}) + tokenizedBuffer = new TokenizedBuffer({ + buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert + }) tokenizeCallback = jasmine.createSpy('onDidTokenize') tokenizedBuffer.onDidTokenize(tokenizeCallback) @@ -905,7 +936,7 @@ describe "TokenizedBuffer", -> registration = atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', -> called = true) waitsForPromise -> - atom.project.open('sample.js', autoIndent: false).then (o) -> + atom.workspace.open('sample.js', autoIndent: false).then (o) -> editor = o waitsForPromise -> diff --git a/spec/tokenized-line-spec.coffee b/spec/tokenized-line-spec.coffee index 2914ec089..f1dce7b9e 100644 --- a/spec/tokenized-line-spec.coffee +++ b/spec/tokenized-line-spec.coffee @@ -7,7 +7,7 @@ describe "TokenizedLine", -> describe "::isOnlyWhitespace()", -> beforeEach -> waitsForPromise -> - atom.project.open('coffee.coffee').then (o) -> editor = o + atom.workspace.open('coffee.coffee').then (o) -> editor = o it "returns true when the line is only whitespace", -> expect(editor.tokenizedLineForScreenRow(3).isOnlyWhitespace()).toBe true diff --git a/spec/tooltip-manager-spec.coffee b/spec/tooltip-manager-spec.coffee index ec30ef2e3..bf0462ebf 100644 --- a/spec/tooltip-manager-spec.coffee +++ b/spec/tooltip-manager-spec.coffee @@ -8,7 +8,7 @@ describe "TooltipManager", -> ctrlY = _.humanizeKeystroke("ctrl-y") beforeEach -> - manager = new TooltipManager + manager = new TooltipManager(keymapManager: atom.keymaps) element = document.createElement('div') element.classList.add('foo') jasmine.attachToDOM(element) diff --git a/spec/window-event-handler-spec.coffee b/spec/window-event-handler-spec.coffee index 345c0fd8c..3148942b4 100644 --- a/spec/window-event-handler-spec.coffee +++ b/spec/window-event-handler-spec.coffee @@ -4,11 +4,13 @@ fs = require 'fs-plus' temp = require 'temp' TextEditor = require '../src/text-editor' WindowEventHandler = require '../src/window-event-handler' +ipc = require 'ipc' -describe "Window", -> +describe "WindowEventHandler", -> [projectPath, windowEventHandler] = [] beforeEach -> + atom.uninstallWindowEventHandler() spyOn(atom, 'hide') initialPath = atom.project.getPaths()[0] spyOn(atom, 'getLoadSettings').andCallFake -> @@ -16,12 +18,12 @@ describe "Window", -> loadSettings.initialPath = initialPath loadSettings atom.project.destroy() - atom.windowEventHandler.unsubscribe() - windowEventHandler = new WindowEventHandler + windowEventHandler = new WindowEventHandler({atomEnvironment: atom, applicationDelegate: atom.applicationDelegate, window, document}) projectPath = atom.project.getPaths()[0] afterEach -> windowEventHandler.unsubscribe() + atom.installWindowEventHandler() describe "when the window is loaded", -> it "doesn't have .is-blurred on the body tag", -> @@ -51,64 +53,25 @@ describe "Window", -> describe "beforeunload event", -> beforeEach -> jasmine.unspy(TextEditor.prototype, "shouldPromptToSave") + spyOn(ipc, 'send') describe "when pane items are modified", -> - it "prompts user to save and calls atom.workspace.confirmClose", -> - editor = null - spyOn(atom.workspace, 'confirmClose').andCallThrough() - spyOn(atom, "confirm").andReturn(2) + editor = null + beforeEach -> + waitsForPromise -> atom.workspace.open("sample.js").then (o) -> editor = o + runs -> editor.insertText("I look different, I feel different.") - waitsForPromise -> - atom.workspace.open("sample.js").then (o) -> editor = o + it "prompts the user to save them, and allows the unload to continue if they confirm", -> + spyOn(atom.workspace, 'confirmClose').andReturn(true) + window.dispatchEvent(new CustomEvent('beforeunload')) + expect(atom.workspace.confirmClose).toHaveBeenCalled() + expect(ipc.send).not.toHaveBeenCalledWith('did-cancel-window-unload') - runs -> - editor.insertText("I look different, I feel different.") - window.dispatchEvent(new CustomEvent('beforeunload')) - expect(atom.workspace.confirmClose).toHaveBeenCalled() - expect(atom.confirm).toHaveBeenCalled() - - it "prompts user to save and handler returns true if don't save", -> - editor = null - spyOn(atom, "confirm").andReturn(2) - - waitsForPromise -> - atom.workspace.open("sample.js").then (o) -> editor = o - - runs -> - editor.insertText("I look different, I feel different.") - window.dispatchEvent(new CustomEvent('beforeunload')) - expect(atom.confirm).toHaveBeenCalled() - - it "prompts user to save and handler returns false if dialog is canceled", -> - editor = null - spyOn(atom, "confirm").andReturn(1) - waitsForPromise -> - atom.workspace.open("sample.js").then (o) -> editor = o - - runs -> - editor.insertText("I look different, I feel different.") - window.dispatchEvent(new CustomEvent('beforeunload')) - expect(atom.confirm).toHaveBeenCalled() - - describe "when the same path is modified in multiple panes", -> - it "prompts to save the item", -> - return - 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.dispatchEvent(new CustomEvent('beforeunload')) - expect(atom.workspace.confirmClose).toHaveBeenCalled() - expect(atom.confirm.callCount).toBe 1 - expect(fs.readFileSync(filePath, 'utf8')).toBe 'world' + it "cancels the unload if the user selects cancel", -> + spyOn(atom.workspace, 'confirmClose').andReturn(false) + window.dispatchEvent(new CustomEvent('beforeunload')) + expect(atom.workspace.confirmClose).toHaveBeenCalled() + expect(ipc.send).toHaveBeenCalledWith('did-cancel-window-unload') describe "when a link is clicked", -> it "opens the http/https links in an external application", -> @@ -225,62 +188,6 @@ describe "Window", -> elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true)) expect(document.activeElement.tabIndex).toBe 7 - describe "the window:open-locations event", -> - beforeEach -> - spyOn(atom.workspace, 'open') - atom.project.setPaths([]) - - describe "when the opened path exists", -> - it "adds it to the project's paths", -> - pathToOpen = __filename - atom.getCurrentWindow().send 'message', 'open-locations', [{pathToOpen}] - - waitsFor -> - atom.project.getPaths().length is 1 - - runs -> - expect(atom.project.getPaths()[0]).toBe __dirname - - describe "when the opened path does not exist but its parent directory does", -> - it "adds the parent directory to the project paths", -> - pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt') - atom.getCurrentWindow().send 'message', 'open-locations', [{pathToOpen}] - - waitsFor -> - atom.project.getPaths().length is 1 - - runs -> - expect(atom.project.getPaths()[0]).toBe __dirname - - describe "when the opened path is a file", -> - it "opens it in the workspace", -> - pathToOpen = __filename - atom.getCurrentWindow().send 'message', 'open-locations', [{pathToOpen}] - - waitsFor -> - atom.workspace.open.callCount is 1 - - runs -> - expect(atom.workspace.open.mostRecentCall.args[0]).toBe __filename - - - describe "when the opened path is a directory", -> - it "does not open it in the workspace", -> - pathToOpen = __dirname - atom.getCurrentWindow().send 'message', 'open-locations', [{pathToOpen}] - expect(atom.workspace.open.callCount).toBe 0 - - describe "when the opened path is a uri", -> - it "adds it to the project's paths as is", -> - pathToOpen = 'remote://server:7644/some/dir/path' - atom.getCurrentWindow().send 'message', 'open-locations', [{pathToOpen}] - - waitsFor -> - atom.project.getPaths().length is 1 - - runs -> - expect(atom.project.getPaths()[0]).toBe pathToOpen - describe "when keydown events occur on the document", -> it "dispatches the event via the KeymapManager and CommandRegistry", -> dispatchedCommands = [] diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index c60508640..5850100e0 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -1,6 +1,7 @@ path = require 'path' temp = require 'temp' Workspace = require '../src/workspace' +Project = require '../src/project' Pane = require '../src/pane' platform = require './spec-helper-platform' _ = require 'underscore-plus' @@ -8,11 +9,14 @@ fstream = require 'fstream' fs = require 'fs-plus' describe "Workspace", -> - workspace = null + [workspace, setDocumentEdited] = [] beforeEach -> + workspace = atom.workspace + workspace.resetFontSize() + spyOn(atom.applicationDelegate, "confirm") + setDocumentEdited = spyOn(atom.applicationDelegate, 'setWindowDocumentEdited') atom.project.setPaths([atom.project.getDirectories()[0]?.resolve('dir')]) - atom.workspace = workspace = new Workspace waits(1) describe "serialization", -> @@ -21,8 +25,16 @@ describe "Workspace", -> projectState = atom.project.serialize() atom.workspace.destroy() atom.project.destroy() - atom.project = atom.deserializers.deserialize(projectState) - atom.workspace = Workspace.deserialize(workspaceState) + atom.project = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm.bind(atom)}) + atom.project.deserialize(projectState, atom.deserializers) + atom.workspace = new Workspace({ + config: atom.config, project: atom.project, packageManager: atom.packages, + grammarRegistry: atom.grammars, deserializerManager: atom.deserializers, + notificationManager: atom.notifications, clipboard: atom.clipboard, + applicationDelegate: atom.applicationDelegate, + viewRegistry: atom.views, assert: atom.assert.bind(atom), + }) + atom.workspace.deserialize(workspaceState, atom.deserializers) describe "when the workspace contains text editors", -> it "constructs the view with the same panes", -> @@ -333,7 +345,8 @@ describe "Workspace", -> describe "when the file is over 20MB", -> it "prompts the user to make sure they want to open a file this big", -> spyOn(fs, 'getSizeSync').andReturn 20 * 1048577 # 20MB - spyOn(atom, 'confirm').andCallFake -> selectedButtonIndex + atom.applicationDelegate.confirm.andCallFake -> selectedButtonIndex + atom.applicationDelegate.confirm() selectedButtonIndex = 1 # cancel editor = null @@ -342,16 +355,16 @@ describe "Workspace", -> runs -> expect(editor).toBeUndefined() - expect(atom.confirm).toHaveBeenCalled() + expect(atom.applicationDelegate.confirm).toHaveBeenCalled() - atom.confirm.reset() + atom.applicationDelegate.confirm.reset() selectedButtonIndex = 0 # open the file waitsForPromise -> workspace.open('sample.js').then (e) -> editor = e runs -> - expect(atom.confirm).toHaveBeenCalled() + expect(atom.applicationDelegate.confirm).toHaveBeenCalled() expect(editor.displayBuffer.largeFileMode).toBe true describe "when passed a path that matches a custom opener", -> @@ -614,7 +627,13 @@ describe "Workspace", -> spyOn(jsPackage, 'loadGrammarsSync') spyOn(coffeePackage, 'loadGrammarsSync') - workspace2 = Workspace.deserialize(state) + workspace2 = new Workspace({ + config: atom.config, project: atom.project, packageManager: atom.packages, + notificationManager: atom.notifications, deserializerManager: atom.deserializers, + clipboard: atom.clipboard, viewRegistry: atom.views, grammarRegistry: atom.grammars, + applicationDelegate: atom.applicationDelegate, assert: atom.assert.bind(atom) + }) + workspace2.deserialize(state, atom.deserializers) expect(jsPackage.loadGrammarsSync.callCount).toBe 1 expect(coffeePackage.loadGrammarsSync.callCount).toBe 1 @@ -666,7 +685,13 @@ describe "Workspace", -> it "updates the title to contain the project's path", -> document.title = null - workspace2 = Workspace.deserialize(atom.workspace.serialize()) + workspace2 = new Workspace({ + config: atom.config, project: atom.project, packageManager: atom.packages, + notificationManager: atom.notifications, deserializerManager: atom.deserializers, + clipboard: atom.clipboard, viewRegistry: atom.views, grammarRegistry: atom.grammars, + applicationDelegate: atom.applicationDelegate, assert: atom.assert.bind(atom) + }) + workspace2.deserialize(atom.workspace.serialize(), atom.deserializers) item = atom.workspace.getActivePaneItem() expect(document.title).toBe "#{item.getTitle()} - #{atom.project.getPaths()[0]} - Atom" workspace2.destroy() @@ -679,15 +704,14 @@ describe "Workspace", -> waitsForPromise -> atom.workspace.open('b') runs -> [item1, item2] = atom.workspace.getPaneItems() - spyOn(atom, 'setDocumentEdited') - it "calls atom.setDocumentEdited when the active item changes", -> + it "calls setDocumentEdited when the active item changes", -> expect(atom.workspace.getActivePaneItem()).toBe item2 item1.insertText('a') expect(item1.isModified()).toBe true atom.workspace.getActivePane().activateNextItem() - expect(atom.setDocumentEdited).toHaveBeenCalledWith(true) + expect(setDocumentEdited).toHaveBeenCalledWith(true) it "calls atom.setDocumentEdited when the active item's modified status changes", -> expect(atom.workspace.getActivePaneItem()).toBe item2 @@ -695,13 +719,13 @@ describe "Workspace", -> advanceClock(item2.getBuffer().getStoppedChangingDelay()) expect(item2.isModified()).toBe true - expect(atom.setDocumentEdited).toHaveBeenCalledWith(true) + expect(setDocumentEdited).toHaveBeenCalledWith(true) item2.undo() advanceClock(item2.getBuffer().getStoppedChangingDelay()) expect(item2.isModified()).toBe false - expect(atom.setDocumentEdited).toHaveBeenCalledWith(false) + expect(setDocumentEdited).toHaveBeenCalledWith(false) describe "adding panels", -> class TestItem @@ -956,7 +980,7 @@ describe "Workspace", -> results = [] waitsForPromise -> - atom.project.open('a').then (o) -> + atom.workspace.open('a').then (o) -> editor = o editor.setText("Elephant") @@ -974,7 +998,7 @@ describe "Workspace", -> results = [] waitsForPromise -> - atom.project.open(temp.openSync().path).then (o) -> + atom.workspace.open(temp.openSync().path).then (o) -> editor = o editor.setText("Elephant") @@ -1070,6 +1094,9 @@ describe "Workspace", -> search: (directory, regex, options) -> fakeSearch = new FakeSearch(options) }) + waitsFor -> + atom.workspace.directorySearchers.length > 0 + it "can override the DefaultDirectorySearcher on a per-directory basis", -> foreignFilePath = 'ssh://foreign-directory:8080/hello.txt' numPathsSearchedInDir2 = 1 @@ -1193,7 +1220,7 @@ describe "Workspace", -> results = [] waitsForPromise -> - atom.project.open('sample.js').then (o) -> editor = o + atom.workspace.open('sample.js').then (o) -> editor = o runs -> expect(editor.isModified()).toBeFalsy() @@ -1214,7 +1241,7 @@ describe "Workspace", -> results = [] waitsForPromise -> - atom.project.open('sample-with-comments.js').then (o) -> editor = o + atom.workspace.open('sample-with-comments.js').then (o) -> editor = o waitsForPromise -> atom.workspace.replace /items/gi, 'items', [commentFilePath], (result) -> @@ -1229,7 +1256,7 @@ describe "Workspace", -> results = [] waitsForPromise -> - atom.project.open('sample.js').then (o) -> editor = o + atom.workspace.open('sample.js').then (o) -> editor = o runs -> editor.buffer.setTextInRange([[0, 0], [0, 0]], 'omg') diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee new file mode 100644 index 000000000..f2b425137 --- /dev/null +++ b/src/application-delegate.coffee @@ -0,0 +1,165 @@ +_ = require 'underscore-plus' +ipc = require 'ipc' +remote = require 'remote' +shell = require 'shell' +{Disposable} = require 'event-kit' +{getWindowLoadSettings, setWindowLoadSettings} = require './window-load-settings-helpers' + +module.exports = +class ApplicationDelegate + open: (params) -> + ipc.send('open', params) + + pickFolder: (callback) -> + responseChannel = "atom-pick-folder-response" + ipc.on responseChannel, (path) -> + ipc.removeAllListeners(responseChannel) + callback(path) + ipc.send("pick-folder", responseChannel) + + getCurrentWindow: -> + remote.getCurrentWindow() + + closeWindow: -> + ipc.send("call-window-method", "close") + + getWindowSize: -> + [width, height] = remote.getCurrentWindow().getSize() + {width, height} + + setWindowSize: (width, height) -> + remote.getCurrentWindow().setSize(width, height) + + getWindowPosition: -> + [x, y] = remote.getCurrentWindow().getPosition() + {x, y} + + setWindowPosition: (x, y) -> + ipc.send("call-window-method", "setPosition", x, y) + + centerWindow: -> + ipc.send("call-window-method", "center") + + focusWindow: -> + ipc.send("call-window-method", "focus") + + showWindow: -> + ipc.send("call-window-method", "show") + + hideWindow: -> + ipc.send("call-window-method", "hide") + + restartWindow: -> + ipc.send("call-window-method", "restart") + + isWindowMaximized: -> + remote.getCurrentWindow().isMaximized() + + maximizeWindow: -> + ipc.send("call-window-method", "maximize") + + isWindowFullScreen: -> + remote.getCurrentWindow().isFullScreen() + + setWindowFullScreen: (fullScreen=false) -> + ipc.send("call-window-method", "setFullScreen", fullScreen) + + openWindowDevTools: -> + remote.getCurrentWindow().openDevTools() + + toggleWindowDevTools: -> + remote.getCurrentWindow().toggleDevTools() + + executeJavaScriptInWindowDevTools: (code) -> + remote.getCurrentWindow().executeJavaScriptInDevTools(code) + + setWindowDocumentEdited: (edited) -> + ipc.send("call-window-method", "setDocumentEdited", edited) + + setRepresentedFilename: (filename) -> + ipc.send("call-window-method", "setRepresentedFilename", filename) + + setRepresentedDirectoryPaths: (paths) -> + loadSettings = getWindowLoadSettings() + loadSettings['initialPaths'] = paths + setWindowLoadSettings(loadSettings) + + setAutoHideWindowMenuBar: (autoHide) -> + ipc.send("call-window-method", "setAutoHideMenuBar", autoHide) + + setWindowMenuBarVisibility: (visible) -> + remote.getCurrentWindow().setMenuBarVisibility(visible) + + getPrimaryDisplayWorkAreaSize: -> + screen = remote.require 'screen' + screen.getPrimaryDisplay().workAreaSize + + confirm: ({message, detailedMessage, buttons}) -> + buttons ?= {} + if _.isArray(buttons) + buttonLabels = buttons + else + buttonLabels = Object.keys(buttons) + + dialog = remote.require('dialog') + chosen = dialog.showMessageBox(remote.getCurrentWindow(), { + type: 'info' + message: message + detail: detailedMessage + buttons: buttonLabels + }) + + if _.isArray(buttons) + chosen + else + callback = buttons[buttonLabels[chosen]] + callback?() + + showMessageDialog: (params) -> + + showSaveDialog: (params) -> + if _.isString(params) + params = defaultPath: params + else + params = _.clone(params) + params.title ?= 'Save File' + params.defaultPath ?= getWindowLoadSettings().initialPaths[0] + dialog = remote.require('dialog') + dialog.showSaveDialog remote.getCurrentWindow(), params + + playBeepSound: -> + shell.beep() + + onDidOpenLocations: (callback) -> + outerCallback = (message, detail) -> + if message is 'open-locations' + callback(detail) + + ipc.on('message', outerCallback) + new Disposable -> + ipc.removeEventListener('message', outerCallback) + + onUpdateAvailable: (callback) -> + outerCallback = (message, detail) -> + if message is 'update-available' + callback(detail) + + ipc.on('message', outerCallback) + new Disposable -> + ipc.removeEventListener('message', outerCallback) + + onApplicationMenuCommand: (callback) -> + ipc.on('command', callback) + new Disposable -> + ipc.removeEventListener('command', callback) + + onContextMenuCommand: (callback) -> + ipc.on('context-command', callback) + new Disposable -> + ipc.removeEventListener('context-command', callback) + + didCancelWindowUnload: -> + ipc.send('did-cancel-window-unload') + + openExternal: (url) -> + shell.openExternal(url) diff --git a/src/atom.coffee b/src/atom-environment.coffee similarity index 58% rename from src/atom.coffee rename to src/atom-environment.coffee index 59d7765ac..f60f2ec97 100644 --- a/src/atom.coffee +++ b/src/atom-environment.coffee @@ -1,9 +1,5 @@ crypto = require 'crypto' -ipc = require 'ipc' -os = require 'os' path = require 'path' -remote = require 'remote' -shell = require 'shell' _ = require 'underscore-plus' {deprecate} = require 'grim' @@ -14,83 +10,52 @@ Model = require './model' WindowEventHandler = require './window-event-handler' StylesElement = require './styles-element' StorageFolder = require './storage-folder' +{getWindowLoadSettings} = require './window-load-settings-helpers' +registerDefaultCommands = require './register-default-commands' + +DeserializerManager = require './deserializer-manager' +ViewRegistry = require './view-registry' +NotificationManager = require './notification-manager' +Config = require './config' +KeymapManager = require './keymap-extensions' +TooltipManager = require './tooltip-manager' +CommandRegistry = require './command-registry' +GrammarRegistry = require './grammar-registry' +StyleManager = require './style-manager' +PackageManager = require './package-manager' +ThemeManager = require './theme-manager' +MenuManager = require './menu-manager' +ContextMenuManager = require './context-menu-manager' +CommandInstaller = require './command-installer' +Clipboard = require './clipboard' +Project = require './project' +Workspace = require './workspace' +PanelContainer = require './panel-container' +Panel = require './panel' +PaneContainer = require './pane-container' +PaneAxis = require './pane-axis' +Pane = require './pane' +Project = require './project' +TextEditor = require './text-editor' +TextBuffer = require 'text-buffer' +Gutter = require './gutter' + +WorkspaceElement = require './workspace-element' +PanelContainerElement = require './panel-container-element' +PanelElement = require './panel-element' +PaneContainerElement = require './pane-container-element' +PaneAxisElement = require './pane-axis-element' +PaneElement = require './pane-element' +TextEditorElement = require './text-editor-element' +{createGutterView} = require './gutter-component-helpers' # Essential: Atom global for dealing with packages, themes, menus, and the window. # # An instance of this class is always available as the `atom` global. module.exports = -class Atom extends Model +class AtomEnvironment extends Model @version: 1 # Increment this when the serialization format changes - # Load or create the Atom environment in the given mode. - # - # * `mode` A {String} mode that is either 'editor' or 'spec' depending on the - # kind of environment you want to build. - # - # Returns an Atom instance, fully initialized - @loadOrCreate: (mode) -> - startTime = Date.now() - atom = @deserialize(@loadState(mode)) ? new this({mode, @version}) - atom.deserializeTimings.atom = Date.now() - startTime - atom - - # Deserializes the Atom environment from a state object - @deserialize: (state) -> - new this(state) if state?.version is @version - - # Loads and returns the serialized state corresponding to this window - # if it exists; otherwise returns undefined. - @loadState: (mode) -> - if stateKey = @getStateKey(@getLoadSettings().initialPaths, mode) - if state = @getStorageFolder().load(stateKey) - return state - - if windowState = @getLoadSettings().windowState - try - JSON.parse(@getLoadSettings().windowState) - catch error - console.warn "Error parsing window state: #{statePath} #{error.stack}", error - - # Returns the path where the state for the current window will be - # located if it exists. - @getStateKey: (paths, mode) -> - if mode is 'spec' - 'spec' - else if mode is 'editor' and paths?.length > 0 - sha1 = crypto.createHash('sha1').update(paths.slice().sort().join("\n")).digest('hex') - "editor-#{sha1}" - else - null - - # Get the directory path to Atom's configuration area. - # - # Returns the absolute path to ~/.atom - @getConfigDirPath: -> - @configDirPath ?= process.env.ATOM_HOME - - @getStorageFolder: -> - @storageFolder ?= new StorageFolder(@getConfigDirPath()) - - # Returns the load settings hash associated with the current window. - @getLoadSettings: -> - @loadSettings ?= JSON.parse(decodeURIComponent(location.hash.substr(1))) - cloned = _.deepClone(@loadSettings) - # The loadSettings.windowState could be large, request it only when needed. - cloned.__defineGetter__ 'windowState', => - @getCurrentWindow().loadSettings.windowState - cloned.__defineSetter__ 'windowState', (value) => - @getCurrentWindow().loadSettings.windowState = value - cloned - - @updateLoadSetting: (key, value) -> - @getLoadSettings() - @loadSettings[key] = value - location.hash = encodeURIComponent(JSON.stringify(@loadSettings)) - - @getCurrentWindow: -> - remote.getCurrentWindow() - - workspaceParentSelectorctor: 'body' lastUncaughtError: null ### @@ -150,122 +115,197 @@ class Atom extends Model ### # Call .loadOrCreate instead - constructor: (@state) -> - @emitter = new Emitter - @disposables = new CompositeDisposable - {@mode} = @state - DeserializerManager = require './deserializer-manager' - @deserializers = new DeserializerManager() - @deserializeTimings = {} + constructor: (params={}) -> + {@applicationDelegate, @window, @document, configDirPath, @enablePersistence} = params - # Sets up the basic services that should be available in all modes - # (both spec and application). - # - # Call after this instance has been assigned to the `atom` global. - initialize: -> - window.onerror = => - @lastUncaughtError = Array::slice.call(arguments) - [message, url, line, column, originalError] = @lastUncaughtError - - {line, column} = mapSourcePosition({source: url, line, column}) - - eventObject = {message, url, line, column, originalError} - - openDevTools = true - eventObject.preventDefault = -> openDevTools = false - - @emitter.emit 'will-throw-error', eventObject - - if openDevTools - @openDevTools() - @executeJavaScriptInDevTools('DevToolsAPI.showConsole()') - - @emitter.emit 'did-throw-error', {message, url, line, column, originalError} - - @disposables?.dispose() - @disposables = new CompositeDisposable - - @displayWindow() unless @inSpecMode() - - @setBodyPlatformClass() + @state = {version: @constructor.version} @loadTime = null - - Config = require './config' - KeymapManager = require './keymap-extensions' - ViewRegistry = require './view-registry' - CommandRegistry = require './command-registry' - TooltipManager = require './tooltip-manager' - NotificationManager = require './notification-manager' - PackageManager = require './package-manager' - Clipboard = require './clipboard' - GrammarRegistry = require './grammar-registry' - ThemeManager = require './theme-manager' - StyleManager = require './style-manager' - ContextMenuManager = require './context-menu-manager' - MenuManager = require './menu-manager' {devMode, safeMode, resourcePath} = @getLoadSettings() - configDirPath = @getConfigDirPath() - # Add 'exports' to module search path. - exportsPath = path.join(resourcePath, 'exports') - require('module').globalPaths.push(exportsPath) - # Still set NODE_PATH since tasks may need it. - process.env.NODE_PATH = exportsPath + @emitter = new Emitter + @disposables = new CompositeDisposable - # Make react.js faster - process.env.NODE_ENV ?= 'production' unless devMode + @deserializers = new DeserializerManager(this) + @deserializeTimings = {} + + @views = new ViewRegistry(this) - @config = new Config({configDirPath, resourcePath}) - @keymaps = new KeymapManager({configDirPath, resourcePath}) - @keymaps.subscribeToFileReadFailure() - @tooltips = new TooltipManager @notifications = new NotificationManager + + @config = new Config({configDirPath, resourcePath, notificationManager: @notifications, @enablePersistence}) + @setConfigSchema() + + @keymaps = new KeymapManager({configDirPath, resourcePath, notificationManager: @notifications}) + + @tooltips = new TooltipManager(keymapManager: @keymaps) + @commands = new CommandRegistry - @views = new ViewRegistry - @registerViewProviders() - @packages = new PackageManager({devMode, configDirPath, resourcePath, safeMode}) - @styles = new StyleManager - document.head.appendChild(new StylesElement) - @themes = new ThemeManager({packageManager: @packages, configDirPath, resourcePath, safeMode}) - @contextMenu = new ContextMenuManager({resourcePath, devMode}) - @menu = new MenuManager({resourcePath}) + @commands.attach(@window) + + @grammars = new GrammarRegistry({@config}) + + @styles = new StyleManager({configDirPath}) + + @packages = new PackageManager({ + devMode, configDirPath, resourcePath, safeMode, @config, styleManager: @styles, + commandRegistry: @commands, keymapManager: @keymaps, notificationManager: @notifications, + grammarRegistry: @grammars + }) + + @themes = new ThemeManager({ + packageManager: @packages, configDirPath, resourcePath, safeMode, @config, + styleManager: @styles, notificationManager: @notifications, viewRegistry: @views + }) + + @menu = new MenuManager({resourcePath, keymapManager: @keymaps, packageManager: @packages}) + + @contextMenu = new ContextMenuManager({resourcePath, devMode, keymapManager: @keymaps}) + + @packages.setMenuManager(@menu) + @packages.setContextMenuManager(@contextMenu) + @packages.setThemeManager(@themes) + @clipboard = new Clipboard() - @grammars = @deserializers.deserialize(@state.grammars ? @state.syntax) ? new GrammarRegistry() - @disposables.add @packages.onDidActivateInitialPackages => @watchThemes() - Project = require './project' - TextBuffer = require 'text-buffer' + @project = new Project({notificationManager: @notifications, packageManager: @packages, @config}) + + @commandInstaller = new CommandInstaller(@getVersion(), @applicationDelegate) + + @workspace = new Workspace({ + @config, @project, packageManager: @packages, grammarRegistry: @grammars, deserializerManager: @deserializers, + notificationManager: @notifications, @applicationDelegate, @clipboard, viewRegistry: @views, assert: @assert.bind(this) + }) + @themes.workspace = @workspace + + @config.load() + + @themes.loadBaseStylesheets() + @initialStyleElements = @styles.getSnapshot() + @themes.initialLoadComplete = true + @setBodyPlatformClass() + + @stylesElement = @styles.buildStylesElement() + @document.head.appendChild(@stylesElement) + + @keymaps.subscribeToFileReadFailure() + @keymaps.loadBundledKeymaps() + + @registerDefaultCommands() + @registerDefaultOpeners() + @registerDefaultDeserializers() + @registerDefaultViewProviders() + + @installUncaughtErrorHandler() + @installWindowEventHandler() + + @observeAutoHideMenuBar() + + setConfigSchema: -> + @config.setSchema null, {type: 'object', properties: _.clone(require('./config-schema'))} + + registerDefaultDeserializers: -> + @deserializers.add(Workspace) + @deserializers.add(PaneContainer) + @deserializers.add(PaneAxis) + @deserializers.add(Pane) + @deserializers.add(Project) + @deserializers.add(TextEditor) @deserializers.add(TextBuffer) - TokenizedBuffer = require './tokenized-buffer' - DisplayBuffer = require './display-buffer' - TextEditor = require './text-editor' - @windowEventHandler = new WindowEventHandler + registerDefaultCommands: -> + registerDefaultCommands({commandRegistry: @commands, @config, @commandInstaller}) - # Register the core views as early as possible in case they are needed for - # package deserialization. - registerViewProviders: -> - Gutter = require './gutter' - Pane = require './pane' - PaneElement = require './pane-element' - PaneContainer = require './pane-container' - PaneContainerElement = require './pane-container-element' - PaneAxis = require './pane-axis' - PaneAxisElement = require './pane-axis-element' - TextEditor = require './text-editor' - TextEditorElement = require './text-editor-element' - {createGutterView} = require './gutter-component-helpers' + registerDefaultViewProviders: -> + @views.addViewProvider Workspace, (model, env) -> + new WorkspaceElement().initialize(model, env) + @views.addViewProvider PanelContainer, (model, env) -> + new PanelContainerElement().initialize(model, env) + @views.addViewProvider Panel, (model, env) -> + new PanelElement().initialize(model, env) + @views.addViewProvider PaneContainer, (model, env) -> + new PaneContainerElement().initialize(model, env) + @views.addViewProvider PaneAxis, (model, env) -> + new PaneAxisElement().initialize(model, env) + @views.addViewProvider Pane, (model, env) -> + new PaneElement().initialize(model, env) + @views.addViewProvider TextEditor, (model, env) -> + new TextEditorElement().initialize(model, env) + @views.addViewProvider(Gutter, createGutterView) - atom.views.addViewProvider PaneContainer, (model) -> - new PaneContainerElement().initialize(model) - atom.views.addViewProvider PaneAxis, (model) -> - new PaneAxisElement().initialize(model) - atom.views.addViewProvider Pane, (model) -> - new PaneElement().initialize(model) - atom.views.addViewProvider TextEditor, (model) -> - new TextEditorElement().initialize(model) - atom.views.addViewProvider(Gutter, createGutterView) + registerDefaultOpeners: -> + @workspace.addOpener (uri) => + switch uri + when 'atom://.atom/stylesheet' + @workspace.open(@styles.getUserStyleSheetPath()) + when 'atom://.atom/keymap' + @workspace.open(@keymaps.getUserKeymapPath()) + when 'atom://.atom/config' + @workspace.open(@config.getUserConfigPath()) + when 'atom://.atom/init-script' + @workspace.open(@getUserInitScriptPath()) + + registerDefaultTargetForKeymaps: -> + @keymaps.defaultTarget = @views.getView(@workspace) + + observeAutoHideMenuBar: -> + @disposables.add @config.onDidChange 'core.autoHideMenuBar', ({newValue}) => + @setAutoHideMenuBar(newValue) + @setAutoHideMenuBar(true) if @config.get('core.autoHideMenuBar') + + reset: -> + @deserializers.clear() + @registerDefaultDeserializers() + + @config.clear() + @setConfigSchema() + + @keymaps.clear() + @keymaps.loadBundledKeymaps() + + @commands.clear() + @registerDefaultCommands() + + @styles.restoreSnapshot(@initialStyleElements) + + @menu.clear() + + @clipboard.reset() + + @notifications.clear() + + @contextMenu.clear() + + @packages.reset() + + @workspace.reset(@packages) + @registerDefaultOpeners() + + @project.reset(@packages) + + @workspace.subscribeToEvents() + + @grammars.clear() + + @views.clear() + @registerDefaultViewProviders() + + @state.packageStates = {} + delete @state.workspace + + destroy: -> + return if not @project + + @disposables.dispose() + @workspace?.destroy() + @workspace = null + @themes.workspace = null + @project?.destroy() + @project = null + @commands.clear() + @stylesElement.remove() + + @uninstallWindowEventHandler() ### Section: Event Subscription @@ -341,12 +381,6 @@ class Atom extends Model isReleasedVersion: -> not /\w{7}/.test(@getVersion()) # Check if the release is a 7-character SHA prefix - # Public: Get the directory path to Atom's configuration area. - # - # Returns the absolute path to `~/.atom`. - getConfigDirPath: -> - @constructor.getConfigDirPath() - # Public: Get the time taken to completely load the current window. # # This time include things like loading and activating packages, creating @@ -361,7 +395,7 @@ class Atom extends Model # # Returns an {Object} containing all the load setting key/value pairs. getLoadSettings: -> - @constructor.getLoadSettings() + getWindowLoadSettings() ### Section: Managing The Atom Window @@ -372,7 +406,7 @@ class Atom extends Model # Calling this method without an options parameter will open a prompt to pick # a file/folder to open in the new window. # - # * `options` An {Object} with the following keys: + # * `params` An {Object} with the following keys: # * `pathsToOpen` An {Array} of {String} paths to open. # * `newWindow` A {Boolean}, true to always open a new window instead of # reusing existing windows depending on the paths to open. @@ -381,8 +415,8 @@ class Atom extends Model # repository and also loads all the packages in ~/.atom/dev/packages # * `safeMode` A {Boolean}, true to open the window in safe mode. Safe # mode prevents all packages installed to ~/.atom/packages from loading. - open: (options) -> - ipc.send('open', options) + open: (params) -> + @applicationDelegate.open(params) # Extended: Prompt the user to select one or more folders. # @@ -390,87 +424,81 @@ class Atom extends Model # * `paths` An {Array} of {String} paths that the user selected, or `null` # if the user dismissed the dialog. pickFolder: (callback) -> - responseChannel = "atom-pick-folder-response" - ipc.on responseChannel, (path) -> - ipc.removeAllListeners(responseChannel) - callback(path) - ipc.send("pick-folder", responseChannel) + @applicationDelegate.pickFolder(callback) # Essential: Close the current window. close: -> - @getCurrentWindow().close() + @applicationDelegate.closeWindow() # Essential: Get the size of current window. # # Returns an {Object} in the format `{width: 1000, height: 700}` getSize: -> - [width, height] = @getCurrentWindow().getSize() - {width, height} + @applicationDelegate.getWindowSize() # Essential: Set the size of current window. # # * `width` The {Number} of pixels. # * `height` The {Number} of pixels. setSize: (width, height) -> - @getCurrentWindow().setSize(width, height) + @applicationDelegate.setWindowSize(width, height) # Essential: Get the position of current window. # # Returns an {Object} in the format `{x: 10, y: 20}` getPosition: -> - [x, y] = @getCurrentWindow().getPosition() - {x, y} + @applicationDelegate.getWindowPosition() # Essential: Set the position of current window. # # * `x` The {Number} of pixels. # * `y` The {Number} of pixels. setPosition: (x, y) -> - ipc.send('call-window-method', 'setPosition', x, y) + @applicationDelegate.setWindowPosition(x, y) # Extended: Get the current window getCurrentWindow: -> - @constructor.getCurrentWindow() + @applicationDelegate.getCurrentWindow() # Extended: Move current window to the center of the screen. center: -> - ipc.send('call-window-method', 'center') + @applicationDelegate.centerWindow() # Extended: Focus the current window. focus: -> - ipc.send('call-window-method', 'focus') - window.focus() + @applicationDelegate.focusWindow() + @window.focus() # Extended: Show the current window. show: -> - ipc.send('call-window-method', 'show') + @applicationDelegate.showWindow() # Extended: Hide the current window. hide: -> - ipc.send('call-window-method', 'hide') + @applicationDelegate.hideWindow() # Extended: Reload the current window. reload: -> - ipc.send('call-window-method', 'restart') + @applicationDelegate.restartWindow() # Extended: Returns a {Boolean} that is `true` if the current window is maximized. isMaximized: -> - @getCurrentWindow().isMaximized() + @applicationDelegate.isWindowMaximized() maximize: -> - ipc.send('call-window-method', 'maximize') + @applicationDelegate.maximizeWindow() # Extended: Returns a {Boolean} that is `true` if the current window is in full screen mode. isFullScreen: -> - @getCurrentWindow().isFullScreen() + @applicationDelegate.isWindowFullScreen() # Extended: Set the full screen state of the current window. setFullScreen: (fullScreen=false) -> - ipc.send('call-window-method', 'setFullScreen', fullScreen) + @applicationDelegate.setWindowFullScreen(fullScreen) if fullScreen - document.body.classList.add("fullscreen") + @document.body.classList.add("fullscreen") else - document.body.classList.remove("fullscreen") + @document.body.classList.remove("fullscreen") # Extended: Toggle the full screen state of the current window. toggleFullScreen: -> @@ -546,8 +574,7 @@ class Atom extends Model if @isValidDimensions(dimensions) dimensions else - screen = remote.require 'screen' - {width, height} = screen.getPrimaryDisplay().workAreaSize + {width, height} = @applicationDelegate.getPrimaryDisplayWorkAreaSize() {x: 0, y: 0, width: Math.min(1024, width), height} restoreWindowDimensions: -> @@ -565,37 +592,34 @@ class Atom extends Model return if @inSpecMode() workspaceElement = @views.getView(@workspace) - backgroundColor = window.getComputedStyle(workspaceElement)['background-color'] - window.localStorage.setItem('atom:window-background-color', backgroundColor) + backgroundColor = @window.getComputedStyle(workspaceElement)['background-color'] + @window.localStorage.setItem('atom:window-background-color', backgroundColor) # Call this method when establishing a real application window. startEditorWindow: -> - {safeMode} = @getLoadSettings() - - CommandInstaller = require './command-installer' - - commandInstaller = new CommandInstaller(@getVersion()) - commandInstaller.installAtomCommand false, (error) -> + @commandInstaller.installAtomCommand false, (error) -> console.warn error.message if error? - commandInstaller.installApmCommand false, (error) -> + @commandInstaller.installApmCommand false, (error) -> console.warn error.message if error? - @loadConfig() - @keymaps.loadBundledKeymaps() - @themes.loadBaseStylesheets() + @disposables.add(@applicationDelegate.onDidOpenLocations(@openLocations.bind(this))) + @disposables.add(@applicationDelegate.onApplicationMenuCommand(@dispatchApplicationMenuCommand.bind(this))) + @disposables.add(@applicationDelegate.onContextMenuCommand(@dispatchContextMenuCommand.bind(this))) + @listenForUpdates() + + @registerDefaultTargetForKeymaps() + @packages.loadPackages() - @deserializeEditorWindow() + + @document.body.appendChild(@views.getView(@workspace)) @watchProjectPath() @packages.activate() @keymaps.loadUserKeymap() - @requireUserInitScript() unless safeMode + @requireUserInitScript() unless @getLoadSettings().safeMode @menu.update() - @disposables.add @config.onDidChange 'core.autoHideMenuBar', ({newValue}) => - @setAutoHideMenuBar(newValue) - @setAutoHideMenuBar(true) if @config.get('core.autoHideMenuBar') @openInitialEmptyEditorIfNecessary() @@ -603,36 +627,56 @@ class Atom extends Model return if not @project @storeWindowBackground() - @state.grammars = @grammars.serialize() + @state.grammars = {grammarOverridesByPath: @grammars.grammarOverridesByPath} @state.project = @project.serialize() @state.workspace = @workspace.serialize() @packages.deactivatePackages() @state.packageStates = @packages.packageStates - @saveSync() - @windowState = null - - removeEditorWindow: -> - return if not @project - - @workspace?.destroy() - @workspace = null - @project?.destroy() - @project = null - - @windowEventHandler?.unsubscribe() + @state.fullScreen = @isFullScreen() + @saveStateSync() openInitialEmptyEditorIfNecessary: -> return unless @config.get('core.openEmptyEditorOnStart') if @getLoadSettings().initialPaths?.length is 0 and @workspace.getPaneItems().length is 0 @workspace.open(null) + installUncaughtErrorHandler: -> + @previousWindowErrorHandler = @window.onerror + @window.onerror = => + @lastUncaughtError = Array::slice.call(arguments) + [message, url, line, column, originalError] = @lastUncaughtError + + {line, column} = mapSourcePosition({source: url, line, column}) + + eventObject = {message, url, line, column, originalError} + + openDevTools = true + eventObject.preventDefault = -> openDevTools = false + + @emitter.emit 'will-throw-error', eventObject + + if openDevTools + @openDevTools() + @executeJavaScriptInDevTools('DevToolsAPI.showConsole()') + + @emitter.emit 'did-throw-error', {message, url, line, column, originalError} + + uninstallUncaughtErrorHandler: -> + @window.onerror = @previousWindowErrorHandler + + installWindowEventHandler: -> + @windowEventHandler = new WindowEventHandler({atomEnvironment: this, @applicationDelegate, @window, @document}) + + uninstallWindowEventHandler: -> + @windowEventHandler?.unsubscribe() + ### Section: Messaging the User ### # Essential: Visually and audibly trigger a beep. beep: -> - shell.beep() if @config.get('core.audioBeep') + @applicationDelegate.playBeepSound() if @config.get('core.audioBeep') @emitter.emit 'did-beep' # Essential: A flexible way to open a dialog akin to an alert dialog. @@ -655,25 +699,8 @@ class Atom extends Model # button names and the values are callbacks to invoke when clicked. # # Returns the chosen button index {Number} if the buttons option was an array. - confirm: ({message, detailedMessage, buttons}={}) -> - buttons ?= {} - if _.isArray(buttons) - buttonLabels = buttons - else - buttonLabels = Object.keys(buttons) - - dialog = remote.require('dialog') - chosen = dialog.showMessageBox @getCurrentWindow(), - type: 'info' - message: message - detail: detailedMessage - buttons: buttonLabels - - if _.isArray(buttons) - chosen - else - callback = buttons[buttonLabels[chosen]] - callback?() + confirm: (params={}) -> + @applicationDelegate.confirm(params) ### Section: Managing the Dev Tools @@ -681,15 +708,15 @@ class Atom extends Model # Extended: Open the dev tools for the current window. openDevTools: -> - ipc.send('call-window-method', 'openDevTools') + @applicationDelegate.openWindowDevTools() # Extended: Toggle the visibility of the dev tools for the current window. toggleDevTools: -> - ipc.send('call-window-method', 'toggleDevTools') + @applicationDelegate.toggleWindowDevTools() # Extended: Execute code in dev tools. executeJavaScriptInDevTools: (code) -> - ipc.send('call-window-method', 'executeJavaScriptInDevTools', code) + @applicationDelegate.executeJavaScriptInWindowDevTools(code) ### Section: Private @@ -706,62 +733,19 @@ class Atom extends Model false - deserializeProject: -> - Project = require './project' - - startTime = Date.now() - @project ?= @deserializers.deserialize(@state.project) ? new Project() - @deserializeTimings.project = Date.now() - startTime - - deserializeWorkspace: -> - Workspace = require './workspace' - - startTime = Date.now() - @workspace = Workspace.deserialize(@state.workspace) ? new Workspace - @deserializeTimings.workspace = Date.now() - startTime - - workspaceElement = @views.getView(@workspace) - @keymaps.defaultTarget = workspaceElement - document.querySelector(@workspaceParentSelectorctor).appendChild(workspaceElement) - - deserializePackageStates: -> - @packages.packageStates = @state.packageStates ? {} - delete @state.packageStates - - deserializeEditorWindow: -> - @deserializePackageStates() - @deserializeProject() - @deserializeWorkspace() - - loadConfig: -> - @config.setSchema null, {type: 'object', properties: _.clone(require('./config-schema'))} - @config.load() - loadThemes: -> @themes.load() - watchThemes: -> - @themes.onDidChangeActiveThemes => - # Only reload stylesheets from non-theme packages - for pack in @packages.getActivePackages() when pack.getType() isnt 'theme' - pack.reloadStylesheets?() - return - # Notify the browser project of the window's current project path watchProjectPath: -> @disposables.add @project.onDidChangePaths => - @constructor.updateLoadSetting('initialPaths', @project.getPaths()) - - exit: (status) -> - app = remote.require('app') - app.emit('will-exit') - remote.process.exit(status) + @applicationDelegate.setRepresentedDirectoryPaths(@project.getPaths()) setDocumentEdited: (edited) -> - ipc.send('call-window-method', 'setDocumentEdited', edited) + @applicationDelegate.setWindowDocumentEdited?(edited) setRepresentedFilename: (filename) -> - ipc.send('call-window-method', 'setRepresentedFilename', filename) + @applicationDelegate.setWindowRepresentedFilename?(filename) addProjectFolder: -> @pickFolder (selectedPaths = []) => @@ -771,27 +755,61 @@ class Atom extends Model callback(showSaveDialogSync()) showSaveDialogSync: (options={}) -> - if _.isString(options) - options = defaultPath: options - else - options = _.clone(options) - currentWindow = @getCurrentWindow() - dialog = remote.require('dialog') - options.title ?= 'Save File' - options.defaultPath ?= @project?.getPaths()[0] - dialog.showSaveDialog currentWindow, options + @applicationDelegate.showSaveDialog(options) - saveSync: -> - if storageKey = @constructor.getStateKey(@project?.getPaths(), @mode) - @constructor.getStorageFolder().store(storageKey, @state) + saveStateSync: -> + return unless @enablePersistence + + if storageKey = @getStateKey(@project?.getPaths()) + @getStorageFolder().store(storageKey, @state) else @getCurrentWindow().loadSettings.windowState = JSON.stringify(@state) - crashMainProcess: -> - remote.process.crash() + loadStateSync: -> + return unless @enablePersistence - crashRenderProcess: -> - process.crash() + startTime = Date.now() + + if stateKey = @getStateKey(@getLoadSettings().initialPaths) + if state = @getStorageFolder().load(stateKey) + @state = state + + if not @state? and windowState = @getLoadSettings().windowState + try + if state = JSON.parse(@getLoadSettings().windowState) + @state = state + catch error + console.warn "Error parsing window state: #{statePath} #{error.stack}", error + + @deserializeTimings.atom = Date.now() - startTime + + if grammarOverridesByPath = @state.grammars?.grammarOverridesByPath + @grammars.grammarOverridesByPath = grammarOverridesByPath + + @setFullScreen(@state.fullScreen) + + @packages.packageStates = @state.packageStates ? {} + + startTime = Date.now() + @project.deserialize(@state.project, @deserializers) if @state.project? + @deserializeTimings.project = Date.now() - startTime + + startTime = Date.now() + @workspace.deserialize(@state.workspace, @deserializers) if @state.workspace? + @deserializeTimings.workspace = Date.now() - startTime + + getStateKey: (paths) -> + if paths?.length > 0 + sha1 = crypto.createHash('sha1').update(paths.slice().sort().join("\n")).digest('hex') + "editor-#{sha1}" + else + null + + getConfigDirPath: -> + @configDirPath ?= process.env.ATOM_HOME + + getStorageFolder: -> + @storageFolder ?= new StorageFolder(@getConfigDirPath()) getUserInitScriptPath: -> initScriptPath = fs.resolve(@getConfigDirPath(), 'init', ['js', 'coffee']) @@ -802,44 +820,52 @@ class Atom extends Model try require(userInitScriptPath) if fs.isFileSync(userInitScriptPath) catch error - atom.notifications.addError "Failed to load `#{userInitScriptPath}`", + @notifications.addError "Failed to load `#{userInitScriptPath}`", detail: error.message dismissable: true - # Require the module with the given globals. - # - # The globals will be set on the `window` object and removed after the - # require completes. - # - # * `id` The {String} module name or path. - # * `globals` An optional {Object} to set as globals during require. - requireWithGlobals: (id, globals={}) -> - existingGlobals = {} - for key, value of globals - existingGlobals[key] = window[key] - window[key] = value - - require(id) - - for key, value of existingGlobals - if value is undefined - delete window[key] - else - window[key] = value - return - onUpdateAvailable: (callback) -> @emitter.on 'update-available', callback updateAvailable: (details) -> @emitter.emit 'update-available', details + listenForUpdates: -> + @disposables.add(@applicationDelegate.onUpdateAvailable(@updateAvailable.bind(this))) + setBodyPlatformClass: -> - document.body.classList.add("platform-#{process.platform}") + @document.body.classList.add("platform-#{process.platform}") setAutoHideMenuBar: (autoHide) -> - ipc.send('call-window-method', 'setAutoHideMenuBar', autoHide) - ipc.send('call-window-method', 'setMenuBarVisibility', not autoHide) + @applicationDelegate.setAutoHideWindowMenuBar(autoHide) + @applicationDelegate.setWindowMenuBarVisibility(not autoHide) + + dispatchApplicationMenuCommand: (command, arg) -> + activeElement = @document.activeElement + # Use the workspace element if body has focus + if activeElement is @document.body and workspaceElement = @views.getView(@workspace) + activeElement = workspaceElement + @commands.dispatch(activeElement, command, arg) + + dispatchContextMenuCommand: (command, args...) -> + @commands.dispatch(@contextMenu.activeElement, command, args) + + openLocations: (locations) -> + needsProjectPaths = @project?.getPaths().length is 0 + + for {pathToOpen, initialLine, initialColumn} in locations + if pathToOpen? and needsProjectPaths + if fs.existsSync(pathToOpen) + @project.addPath(pathToOpen) + else if fs.existsSync(path.dirname(pathToOpen)) + @project.addPath(path.dirname(pathToOpen)) + else + @project.addPath(pathToOpen) + + unless fs.isDirectorySync(pathToOpen) + @workspace?.open(pathToOpen, {initialLine, initialColumn}) + + return # Preserve this deprecation until 2.0. Sorry. Should have removed Q sooner. Promise.prototype.done = (callback) -> diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index df4254acf..b8e3e7bc2 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -16,6 +16,8 @@ net = require 'net' url = require 'url' {EventEmitter} = require 'events' _ = require 'underscore-plus' +FindParentDir = null +Resolve = null LocationSuffixRegExp = /(:\d+)(:\d+)?$/ @@ -63,7 +65,7 @@ class AtomApplication exit: (status) -> app.exit(status) constructor: (options) -> - {@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath} = options + {@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, timeout} = options @socketPath = null if options.test @@ -87,9 +89,9 @@ class AtomApplication else @loadState() or @openPath(options) - openWithOptions: ({pathsToOpen, executedFrom, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, specDirectory, logFile, profileStartup}) -> + openWithOptions: ({pathsToOpen, executedFrom, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, profileStartup, timeout}) -> if test - @runSpecs({exitWhenDone: true, @resourcePath, specDirectory, logFile}) + @runTests({headless: true, @resourcePath, executedFrom, pathsToOpen, logFile, timeout}) else if pathsToOpen.length > 0 @openPaths({pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup}) else if urlsToOpen.length > 0 @@ -163,7 +165,6 @@ class AtomApplication devMode: @focusedWindow()?.devMode safeMode: @focusedWindow()?.safeMode - @on 'application:run-all-specs', -> @runSpecs(exitWhenDone: false, resourcePath: @devResourcePath, safeMode: @focusedWindow()?.safeMode) @on 'application:quit', -> app.quit() @on 'application:new-window', -> @openPath(_.extend(windowDimensions: @focusedWindow()?.getDimensions(), getLoadSettings())) @on 'application:new-file', -> (@focusedWindow() ? this).openPath() @@ -218,11 +219,6 @@ class AtomApplication @killAllProcesses() @deleteSocketFile() - app.on 'will-exit', => - @saveState(false) - @killAllProcesses() - @deleteSocketFile() - app.on 'open-file', (event, pathToOpen) => event.preventDefault() @openPath({pathToOpen}) @@ -253,8 +249,8 @@ class AtomApplication win = BrowserWindow.fromWebContents(event.sender) @applicationMenu.update(win, template, keystrokesByCommand) - ipc.on 'run-package-specs', (event, specDirectory) => - @runSpecs({resourcePath: @devResourcePath, specDirectory: specDirectory, exitWhenDone: false}) + ipc.on 'run-package-specs', (event, packageSpecPath) => + @runTests({resourcePath: @devResourcePath, pathsToOpen: [packageSpecPath], headless: false}) ipc.on 'command', (event, command) => @emit(command) @@ -271,13 +267,19 @@ class AtomApplication @promptForPath "folder", (selectedPaths) -> event.sender.send(responseChannel, selectedPaths) - ipc.on 'cancel-window-close', => + ipc.on 'did-cancel-window-unload', => @quitting = false clipboard = require '../safe-clipboard' ipc.on 'write-text-to-selection-clipboard', (event, selectedText) -> clipboard.writeText(selectedText, 'selection') + ipc.on 'write-to-stdout', (event, output) -> + process.stdout.write(output) + + ipc.on 'write-to-stderr', (event, output) -> + process.stderr.write(output) + # Public: Executes the given command. # # If it isn't handled globally, delegate to the currently focused window. @@ -395,12 +397,12 @@ class AtomApplication else if devMode try - bootstrapScript = require.resolve(path.join(@devResourcePath, 'src', 'window-bootstrap')) + windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window')) resourcePath = @devResourcePath - bootstrapScript ?= require.resolve('../window-bootstrap') + windowInitializationScript ?= require.resolve('../initialize-application-window') resourcePath ?= @resourcePath - openedWindow = new AtomWindow({locationsToOpen, bootstrapScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup}) + openedWindow = new AtomWindow({locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup}) if pidToKillWhenClosed? @pidsToOpenWindows[pidToKillWhenClosed] = openedWindow @@ -475,9 +477,9 @@ class AtomApplication if pack? if pack.urlMain packagePath = @packages.resolvePackagePath(packageName) - bootstrapScript = path.resolve(packagePath, pack.urlMain) + windowInitializationScript = path.resolve(packagePath, pack.urlMain) windowDimensions = @focusedWindow()?.getDimensions() - new AtomWindow({bootstrapScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions}) + new AtomWindow({windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions}) else console.log "Package '#{pack.name}' does not have a url main: #{urlToOpen}" else @@ -486,25 +488,64 @@ class AtomApplication # Opens up a new {AtomWindow} to run specs within. # # options - - # :exitWhenDone - A Boolean that, if true, will close the window upon + # :headless - A Boolean that, if true, will close the window upon # completion. # :resourcePath - The path to include specs from. # :specPath - The directory to load specs from. # :safeMode - A Boolean that, if true, won't run specs from ~/.atom/packages # and ~/.atom/dev/packages, defaults to false. - runSpecs: ({exitWhenDone, resourcePath, specDirectory, logFile, safeMode}) -> + runTests: ({headless, resourcePath, executedFrom, pathsToOpen, logFile, safeMode, timeout}) -> if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath) resourcePath = @resourcePath - try - bootstrapScript = require.resolve(path.resolve(@devResourcePath, 'spec', 'spec-bootstrap')) - catch error - bootstrapScript = require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'spec-bootstrap')) + timeoutInSeconds = Number.parseFloat(timeout) + unless Number.isNaN(timeoutInSeconds) + timeoutHandler = -> + console.log "The test suite has timed out because it has been running for more than #{timeoutInSeconds} seconds." + process.exit(124) # Use the same exit code as the UNIX timeout util. + setTimeout(timeoutHandler, timeoutInSeconds * 1000) + try + windowInitializationScript = require.resolve(path.resolve(@devResourcePath, 'src', 'initialize-test-window')) + catch error + windowInitializationScript = require.resolve(path.resolve(__dirname, '..', '..', 'src', 'initialize-test-window')) + + testPaths = [] + if pathsToOpen? + for pathToOpen in pathsToOpen + testPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen))) + + if testPaths.length is 0 + process.stderr.write 'Error: Specify at least one test path\n\n' + process.exit(1) + + legacyTestRunnerPath = @resolveLegacyTestRunnerPath() + testRunnerPath = @resolveTestRunnerPath(testPaths[0]) isSpec = true devMode = true safeMode ?= false - new AtomWindow({bootstrapScript, resourcePath, exitWhenDone, isSpec, devMode, specDirectory, logFile, safeMode}) + new AtomWindow({windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode}) + + resolveTestRunnerPath: (testPath) -> + FindParentDir ?= require 'find-parent-dir' + + if packageRoot = FindParentDir.sync(testPath, 'package.json') + packageMetadata = require(path.join(packageRoot, 'package.json')) + if packageMetadata.atomTestRunner + Resolve ?= require('resolve') + if testRunnerPath = Resolve.sync(packageMetadata.atomTestRunner, basedir: packageRoot, extensions: Object.keys(require.extensions)) + return testRunnerPath + else + process.stderr.write "Error: Could not resolve test runner path '#{packageMetadata.atomTestRunner}'" + process.exit(1) + + @resolveLegacyTestRunnerPath() + + resolveLegacyTestRunnerPath: -> + try + require.resolve(path.resolve(@devResourcePath, 'spec', 'jasmine-test-runner')) + catch error + require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'jasmine-test-runner')) locationForPathToOpen: (pathToOpen, executedFrom='') -> return {pathToOpen} unless pathToOpen diff --git a/src/browser/atom-window.coffee b/src/browser/atom-window.coffee index e341f25d3..3be46b5fc 100644 --- a/src/browser/atom-window.coffee +++ b/src/browser/atom-window.coffee @@ -19,7 +19,7 @@ class AtomWindow isSpec: null constructor: (settings={}) -> - {@resourcePath, pathToOpen, locationsToOpen, @isSpec, @exitWhenDone, @safeMode, @devMode} = settings + {@resourcePath, pathToOpen, locationsToOpen, @isSpec, @headless, @safeMode, @devMode} = settings locationsToOpen ?= [{pathToOpen}] if pathToOpen locationsToOpen ?= [] @@ -29,6 +29,10 @@ class AtomWindow 'web-preferences': 'direct-write': true 'subpixel-font-scaling': true + + if @isSpec + options['web-preferences']['page-visibility'] = true + # Don't set icon on Windows so the exe's ico will be used as window and # taskbar's icon. See https://github.com/atom/atom/issues/4811 for more. if process.platform is 'linux' @@ -131,7 +135,7 @@ class AtomWindow @browserWindow.destroy() if chosen is 0 @browserWindow.webContents.on 'crashed', => - global.atomApplication.exit(100) if @exitWhenDone + global.atomApplication.exit(100) if @headless chosen = dialog.showMessageBox @browserWindow, type: 'warning' diff --git a/src/browser/main.coffee b/src/browser/main.coffee index f0709037d..6a141cf1a 100644 --- a/src/browser/main.coffee +++ b/src/browser/main.coffee @@ -99,9 +99,9 @@ parseCommandLine = -> options.alias('n', 'new-window').boolean('n').describe('n', 'Open a new window.') options.boolean('profile-startup').describe('profile-startup', 'Create a profile of the startup execution time.') options.alias('r', 'resource-path').string('r').describe('r', 'Set the path to the Atom source directory and enable dev-mode.') - options.alias('s', 'spec-directory').string('s').describe('s', 'Set the directory from which to run package specs (default: Atom\'s spec directory).') options.boolean('safe').describe('safe', 'Do not load packages from ~/.atom/packages or ~/.atom/dev/packages.') options.alias('t', 'test').boolean('t').describe('t', 'Run the specified specs and exit with error code on failures.') + options.string('timeout').describe('timeout', 'When in test mode, waits until the specified time (in minutes) and kills the process (exit code: 130).') options.alias('v', 'version').boolean('v').describe('v', 'Print the version.') options.alias('w', 'wait').boolean('w').describe('w', 'Wait for window to be closed before returning.') options.string('socket-path') @@ -121,7 +121,7 @@ parseCommandLine = -> safeMode = args['safe'] pathsToOpen = args._ test = args['test'] - specDirectory = args['spec-directory'] + timeout = args['timeout'] newWindow = args['new-window'] pidToKillWhenClosed = args['pid'] if args['wait'] logFile = args['log-file'] @@ -133,18 +133,7 @@ parseCommandLine = -> if args['resource-path'] devMode = true resourcePath = args['resource-path'] - else - # Set resourcePath based on the specDirectory if running specs on atom core - if specDirectory? - packageDirectoryPath = path.join(specDirectory, '..') - packageManifestPath = path.join(packageDirectoryPath, 'package.json') - if fs.statSyncNoException(packageManifestPath) - try - packageManifest = JSON.parse(fs.readFileSync(packageManifestPath)) - resourcePath = packageDirectoryPath if packageManifest.name is 'atom' - - if devMode - resourcePath ?= devResourcePath + resourcePath ?= devResourcePath if devMode unless fs.statSyncNoException(resourcePath) resourcePath = path.dirname(path.dirname(__dirname)) @@ -157,7 +146,7 @@ parseCommandLine = -> devResourcePath = normalizeDriveLetterName(devResourcePath) {resourcePath, devResourcePath, pathsToOpen, urlsToOpen, executedFrom, test, - version, pidToKillWhenClosed, devMode, safeMode, newWindow, specDirectory, - logFile, socketPath, profileStartup} + version, pidToKillWhenClosed, devMode, safeMode, newWindow, + logFile, socketPath, profileStartup, timeout} start() diff --git a/src/clipboard.coffee b/src/clipboard.coffee index 84ff9ab3b..9511bdda9 100644 --- a/src/clipboard.coffee +++ b/src/clipboard.coffee @@ -14,8 +14,12 @@ clipboard = require './safe-clipboard' # ``` module.exports = class Clipboard - metadata: null - signatureForMetadata: null + constructor: -> + @reset() + + reset: -> + @metadata = null + @signatureForMetadata = null # Creates an `md5` hash of some text. # diff --git a/src/command-installer.coffee b/src/command-installer.coffee index 729aef84c..3d366037c 100644 --- a/src/command-installer.coffee +++ b/src/command-installer.coffee @@ -26,7 +26,7 @@ symlinkCommandWithPrivilegeSync = (sourcePath, destinationPath) -> module.exports = class CommandInstaller - constructor: (@appVersion) -> + constructor: (@appVersion, @applicationDelegate) -> getInstallDirectory: -> "/usr/local/bin" @@ -36,7 +36,7 @@ class CommandInstaller installShellCommandsInteractively: -> showErrorDialog = (error) -> - atom.confirm + @applicationDelegate.confirm message: "Failed to install shell commands" detailedMessage: error.message @@ -48,7 +48,7 @@ class CommandInstaller if error? showErrorDialog(error) else - atom.confirm + @applicationDelegate.confirm message: "Commands installed." detailedMessage: "The shell commands `atom` and `apm` are installed." diff --git a/src/command-registry.coffee b/src/command-registry.coffee index 4cf002c7e..db2cf498d 100644 --- a/src/command-registry.coffee +++ b/src/command-registry.coffee @@ -44,15 +44,23 @@ SequenceCount = 0 # ``` module.exports = class CommandRegistry - constructor: (@rootNode) -> + constructor: -> + @rootNode = null + @clear() + + clear: -> @registeredCommands = {} @selectorBasedListenersByCommandName = {} @inlineListenersByCommandName = {} @emitter = new Emitter + attach: (@rootNode) -> + @commandRegistered(command) for command of @selectorBasedListenersByCommandName + @commandRegistered(command) for command of @inlineListenersByCommandName + destroy: -> for commandName of @registeredCommands - window.removeEventListener(commandName, @handleCommandEvent, true) + @rootNode.removeEventListener(commandName, @handleCommandEvent, true) return # Public: Add one or more command listeners associated with a selector. @@ -253,8 +261,8 @@ class CommandRegistry matched commandRegistered: (commandName) -> - unless @registeredCommands[commandName] - window.addEventListener(commandName, @handleCommandEvent, true) + if @rootNode? and not @registeredCommands[commandName] + @rootNode.addEventListener(commandName, @handleCommandEvent, true) @registeredCommands[commandName] = true class SelectorBasedListener diff --git a/src/config.coffee b/src/config.coffee index b0579716b..cb09a8d83 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -334,7 +334,13 @@ class Config value # Created during initialization, available as `atom.config` - constructor: ({@configDirPath, @resourcePath}={}) -> + constructor: ({@configDirPath, @resourcePath, @notificationManager, @enablePersistence}={}) -> + if @enablePersistence? + @configFilePath = fs.resolve(@configDirPath, 'config', ['json', 'cson']) + @configFilePath ?= path.join(@configDirPath, 'config.cson') + @clear() + + clear: -> @emitter = new Emitter @schema = type: 'object' @@ -343,11 +349,8 @@ class Config @settings = {} @scopedSettingsStore = new ScopedPropertyStore @configFileHasErrors = false - @configFilePath = fs.resolve(@configDirPath, 'config', ['json', 'cson']) - @configFilePath ?= path.join(@configDirPath, 'config.cson') @transactDepth = 0 @savePending = false - @requestLoad = _.debounce(@loadUserConfig, 100) @requestSave = => @savePending = true @@ -357,6 +360,8 @@ class Config @save() debouncedSave = _.debounce(save, 100) + shouldNotAccessFileSystem: -> not @enablePersistence + ### Section: Config Subscription ### @@ -727,7 +732,7 @@ class Config ### initializeConfigDirectory: (done) -> - return if fs.existsSync(@configDirPath) + return if fs.existsSync(@configDirPath) or @shouldNotAccessFileSystem() fs.makeTreeSync(@configDirPath) @@ -743,6 +748,8 @@ class Config fs.traverseTree(templateConfigDirPath, onConfigDirFile, (path) -> true) loadUserConfig: -> + return if @shouldNotAccessFileSystem() + unless fs.existsSync(@configFilePath) fs.makeTreeSync(path.dirname(@configFilePath)) CSON.writeFileSync(@configFilePath, {}) @@ -766,6 +773,8 @@ class Config @notifyFailure(message, detail) observeUserConfig: -> + return if @shouldNotAccessFileSystem() + try @watchSubscription ?= pathWatcher.watch @configFilePath, (eventType) => @requestLoad() if eventType is 'change' and @watchSubscription? @@ -782,9 +791,11 @@ class Config @watchSubscription = null notifyFailure: (errorMessage, detail) -> - atom.notifications.addError(errorMessage, {detail, dismissable: true}) + @notificationManager.addError(errorMessage, {detail, dismissable: true}) save: -> + return if @shouldNotAccessFileSystem() + allSettings = {'*': @settings} allSettings = _.extend allSettings, @scopedSettingsStore.propertiesForSource(@getUserConfigPath()) try diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index 0ca005d8a..869076beb 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -4,6 +4,7 @@ CSON = require 'season' fs = require 'fs-plus' {calculateSpecificity, validateSelector} = require 'clear-cut' {Disposable} = require 'event-kit' +remote = require 'remote' MenuHelpers = require './menu-helpers' platformContextMenu = require('../package.json')?._atomMenu?['context-menu'] @@ -40,11 +41,11 @@ platformContextMenu = require('../package.json')?._atomMenu?['context-menu'] # {::add} for more information. module.exports = class ContextMenuManager - constructor: ({@resourcePath, @devMode}) -> + constructor: ({@resourcePath, @devMode, @keymapManager}) -> @definitions = {'.overlayer': []} # TODO: Remove once color picker package stops touching private data @clear() - atom.keymaps.onDidLoadBundledKeymaps => @loadPlatformItems() + @keymapManager.onDidLoadBundledKeymaps => @loadPlatformItems() loadPlatformItems: -> if platformContextMenu? @@ -175,7 +176,7 @@ class ContextMenuManager menuTemplate = @templateForEvent(event) return unless menuTemplate?.length > 0 - atom.getCurrentWindow().emit('context-menu', menuTemplate) + remote.getCurrentWindow().emit('context-menu', menuTemplate) return clear: -> diff --git a/src/cursor.coffee b/src/cursor.coffee index 84396448c..c9ebca8c7 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -16,7 +16,7 @@ class Cursor extends Model visible: true # Instantiated by a {TextEditor} - constructor: ({@editor, @marker, id}) -> + constructor: ({@editor, @marker, @config, id}) -> @emitter = new Emitter @assignId(id) @@ -158,7 +158,7 @@ class Cursor extends Model [before, after] = @editor.getTextInBufferRange(range) return false if /\s/.test(before) or /\s/.test(after) - nonWordCharacters = atom.config.get('editor.nonWordCharacters', scope: @getScopeDescriptor()).split('') + nonWordCharacters = @config.get('editor.nonWordCharacters', scope: @getScopeDescriptor()).split('') _.contains(nonWordCharacters, before) isnt _.contains(nonWordCharacters, after) # Public: Returns whether this cursor is between a word's start and end. @@ -605,7 +605,7 @@ class Cursor extends Model # Returns a {RegExp}. wordRegExp: ({includeNonWordCharacters}={}) -> includeNonWordCharacters ?= true - nonWordCharacters = atom.config.get('editor.nonWordCharacters', scope: @getScopeDescriptor()) + nonWordCharacters = @config.get('editor.nonWordCharacters', scope: @getScopeDescriptor()) segments = ["^[\t ]*$"] segments.push("[^\\s#{_.escapeRegExp(nonWordCharacters)}]+") if includeNonWordCharacters @@ -620,7 +620,7 @@ class Cursor extends Model # # Returns a {RegExp}. subwordRegExp: (options={}) -> - nonWordCharacters = atom.config.get('editor.nonWordCharacters', scope: @getScopeDescriptor()) + nonWordCharacters = @config.get('editor.nonWordCharacters', scope: @getScopeDescriptor()) lowercaseLetters = 'a-z\\u00DF-\\u00F6\\u00F8-\\u00FF' uppercaseLetters = 'A-Z\\u00C0-\\u00D6\\u00D8-\\u00DE' snakeCamelSegment = "[#{uppercaseLetters}]?[#{lowercaseLetters}]+" diff --git a/src/custom-gutter-component.coffee b/src/custom-gutter-component.coffee index 32710ea46..9fd25fd3f 100644 --- a/src/custom-gutter-component.coffee +++ b/src/custom-gutter-component.coffee @@ -6,12 +6,12 @@ module.exports = class CustomGutterComponent - constructor: ({@gutter}) -> + constructor: ({@gutter, @views}) -> @decorationNodesById = {} @decorationItemsById = {} @visible = true - @domNode = atom.views.getView(@gutter) + @domNode = @views.getView(@gutter) @decorationsNode = @domNode.firstChild # Clear the contents in case the domNode is being reused. @decorationsNode.innerHTML = '' diff --git a/src/deserializer-manager.coffee b/src/deserializer-manager.coffee index 62c7e4401..7f6cb0f65 100644 --- a/src/deserializer-manager.coffee +++ b/src/deserializer-manager.coffee @@ -21,7 +21,7 @@ # ``` module.exports = class DeserializerManager - constructor: -> + constructor: (@atomEnvironment) -> @deserializers = {} # Public: Register the given class(es) as deserializers. @@ -29,7 +29,10 @@ class DeserializerManager # * `deserializers` One or more deserializers to register. A deserializer can # be any object with a `.name` property and a `.deserialize()` method. A # common approach is to register a *constructor* as the deserializer for its - # instances by adding a `.deserialize()` class method. + # instances by adding a `.deserialize()` class method. When your method is + # called, it will be passed serialized state as the first argument and the + # {Atom} environment object as the second argument, which is useful if you + # wish to avoid referencing the `atom` global. add: (deserializers...) -> @deserializers[deserializer.name] = deserializer for deserializer in deserializers new Disposable => @@ -39,15 +42,13 @@ class DeserializerManager # Public: Deserialize the state and params. # # * `state` The state {Object} to deserialize. - # * `params` The params {Object} to pass as the second arguments to the - # deserialize method of the deserializer. - deserialize: (state, params) -> + deserialize: (state) -> return unless state? if deserializer = @get(state) stateVersion = state.get?('version') ? state.version return if deserializer.version? and deserializer.version isnt stateVersion - deserializer.deserialize(state, params) + deserializer.deserialize(state, @atomEnvironment) else console.warn "No deserializer found for", state @@ -59,3 +60,6 @@ class DeserializerManager name = state.get?('deserializer') ? state.deserializer @deserializers[name] + + clear: -> + @deserializers = {} diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 2a93aa951..b3a5a628c 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -26,17 +26,29 @@ class DisplayBuffer extends Model height: null width: null - @deserialize: (state) -> - state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer) + @deserialize: (state, atomEnvironment) -> + state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment) + state.config = atomEnvironment.config + state.assert = atomEnvironment.assert + state.grammarRegistry = atomEnvironment.grammars + state.packageManager = atomEnvironment.packages new this(state) - constructor: ({tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, ignoreInvisibles, @largeFileMode}={}) -> + constructor: (params={}) -> super + { + tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, ignoreInvisibles, + @largeFileMode, @config, @assert, @grammarRegistry, @packageManager + } = params + @emitter = new Emitter @disposables = new CompositeDisposable - @tokenizedBuffer ?= new TokenizedBuffer({tabLength, buffer, ignoreInvisibles, @largeFileMode}) + @tokenizedBuffer ?= new TokenizedBuffer({ + tabLength, buffer, ignoreInvisibles, @largeFileMode, @config, + @grammarRegistry, @packageManager, @assert + }) @buffer = @tokenizedBuffer.buffer @charWidthsByScope = {} @markers = {} @@ -61,29 +73,29 @@ class DisplayBuffer extends Model oldConfigSettings = @configSettings @configSettings = - scrollPastEnd: atom.config.get('editor.scrollPastEnd', scope: scopeDescriptor) - softWrap: atom.config.get('editor.softWrap', scope: scopeDescriptor) - softWrapAtPreferredLineLength: atom.config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor) - softWrapHangingIndent: atom.config.get('editor.softWrapHangingIndent', scope: scopeDescriptor) - preferredLineLength: atom.config.get('editor.preferredLineLength', scope: scopeDescriptor) + scrollPastEnd: @config.get('editor.scrollPastEnd', scope: scopeDescriptor) + softWrap: @config.get('editor.softWrap', scope: scopeDescriptor) + softWrapAtPreferredLineLength: @config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor) + softWrapHangingIndent: @config.get('editor.softWrapHangingIndent', scope: scopeDescriptor) + preferredLineLength: @config.get('editor.preferredLineLength', scope: scopeDescriptor) - subscriptions.add atom.config.onDidChange 'editor.softWrap', scope: scopeDescriptor, ({newValue}) => + subscriptions.add @config.onDidChange 'editor.softWrap', scope: scopeDescriptor, ({newValue}) => @configSettings.softWrap = newValue @updateWrappedScreenLines() - subscriptions.add atom.config.onDidChange 'editor.softWrapHangingIndent', scope: scopeDescriptor, ({newValue}) => + subscriptions.add @config.onDidChange 'editor.softWrapHangingIndent', scope: scopeDescriptor, ({newValue}) => @configSettings.softWrapHangingIndent = newValue @updateWrappedScreenLines() - subscriptions.add atom.config.onDidChange 'editor.softWrapAtPreferredLineLength', scope: scopeDescriptor, ({newValue}) => + subscriptions.add @config.onDidChange 'editor.softWrapAtPreferredLineLength', scope: scopeDescriptor, ({newValue}) => @configSettings.softWrapAtPreferredLineLength = newValue @updateWrappedScreenLines() if @isSoftWrapped() - subscriptions.add atom.config.onDidChange 'editor.preferredLineLength', scope: scopeDescriptor, ({newValue}) => + subscriptions.add @config.onDidChange 'editor.preferredLineLength', scope: scopeDescriptor, ({newValue}) => @configSettings.preferredLineLength = newValue - @updateWrappedScreenLines() if @isSoftWrapped() and atom.config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor) + @updateWrappedScreenLines() if @isSoftWrapped() and @config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor) - subscriptions.add atom.config.observe 'editor.scrollPastEnd', scope: scopeDescriptor, (value) => + subscriptions.add @config.observe 'editor.scrollPastEnd', scope: scopeDescriptor, (value) => @configSettings.scrollPastEnd = value @updateWrappedScreenLines() if oldConfigSettings? and not _.isEqual(oldConfigSettings, @configSettings) @@ -97,7 +109,10 @@ class DisplayBuffer extends Model largeFileMode: @largeFileMode copy: -> - newDisplayBuffer = new DisplayBuffer({@buffer, tabLength: @getTabLength(), @largeFileMode}) + newDisplayBuffer = new DisplayBuffer({ + @buffer, tabLength: @getTabLength(), @largeFileMode, @config, @assert, + @grammarRegistry, @packageManager + }) for marker in @findMarkers(displayBufferId: @id) marker.copy(displayBufferId: newDisplayBuffer.id) @@ -1080,8 +1095,8 @@ class DisplayBuffer extends Model tokenizedLinesCount = @tokenizedBuffer.getLineCount() bufferLinesCount = @buffer.getLineCount() - atom.assert screenLinesCount is tokenizedLinesCount, "Display buffer line count out of sync with tokenized buffer", (error) -> + @assert screenLinesCount is tokenizedLinesCount, "Display buffer line count out of sync with tokenized buffer", (error) -> error.metadata = {screenLinesCount, tokenizedLinesCount, bufferLinesCount} - atom.assert screenLinesCount is bufferLinesCount, "Display buffer line count out of sync with buffer", (error) -> + @assert screenLinesCount is bufferLinesCount, "Display buffer line count out of sync with buffer", (error) -> error.metadata = {screenLinesCount, tokenizedLinesCount, bufferLinesCount} diff --git a/src/git-repository-provider.coffee b/src/git-repository-provider.coffee index 3b4df5d2b..593324d0c 100644 --- a/src/git-repository-provider.coffee +++ b/src/git-repository-provider.coffee @@ -48,7 +48,7 @@ isValidGitDirectorySync = (directory) -> module.exports = class GitRepositoryProvider - constructor: (@project) -> + constructor: (@project, @config) -> # Keys are real paths that end in `.git`. # Values are the corresponding GitRepository objects. @pathToRepository = {} @@ -75,7 +75,7 @@ class GitRepositoryProvider gitDirPath = gitDir.getPath() repo = @pathToRepository[gitDirPath] unless repo - repo = GitRepository.open(gitDirPath, project: @project) + repo = GitRepository.open(gitDirPath, {@project, @config}) return null unless repo repo.onDidDestroy(=> delete @pathToRepository[gitDirPath]) @pathToRepository[gitDirPath] = repo diff --git a/src/git-repository.coffee b/src/git-repository.coffee index 064b86dd3..1663f9ad4 100644 --- a/src/git-repository.coffee +++ b/src/git-repository.coffee @@ -80,7 +80,7 @@ class GitRepository for submodulePath, submoduleRepo of @repo.submodules submoduleRepo.upstream = {ahead: 0, behind: 0} - {@project, refreshOnWindowFocus} = options + {@project, @config, refreshOnWindowFocus} = options refreshOnWindowFocus ?= true if refreshOnWindowFocus @@ -443,25 +443,10 @@ class GitRepository # Subscribes to editor view event. checkoutHeadForEditor: (editor) -> - filePath = editor.getPath() - return unless filePath - - fileName = basename(filePath) - - checkoutHead = => + if filePath = editor.getPath() editor.buffer.reload() if editor.buffer.isModified() @checkoutHead(filePath) - if atom.config.get('editor.confirmCheckoutHeadRevision') - atom.confirm - message: 'Confirm Checkout HEAD Revision' - detailedMessage: "Are you sure you want to discard all changes to \"#{fileName}\" since the last Git commit?" - buttons: - OK: checkoutHead - Cancel: null - else - checkoutHead() - # Returns the corresponding {Repository} getRepo: (path) -> if @repo? diff --git a/src/grammar-registry.coffee b/src/grammar-registry.coffee index 109e719fc..4a5352275 100644 --- a/src/grammar-registry.coffee +++ b/src/grammar-registry.coffee @@ -14,19 +14,9 @@ PathSplitRegex = new RegExp("[/.]") # language-specific comment regexes. See {::getProperty} for more details. module.exports = class GrammarRegistry extends FirstMate.GrammarRegistry - @deserialize: ({grammarOverridesByPath}) -> - grammarRegistry = new GrammarRegistry() - grammarRegistry.grammarOverridesByPath = grammarOverridesByPath - grammarRegistry - - atom.deserializers.add(this) - - constructor: -> + constructor: ({@config}={}) -> super(maxTokensPerLine: 100) - serialize: -> - {deserializer: @constructor.name, @grammarOverridesByPath} - createToken: (value, scopes) -> new Token({value, scopes}) # Extended: Select a grammar for the given file path and file contents. @@ -70,7 +60,7 @@ class GrammarRegistry extends FirstMate.GrammarRegistry pathScore = -1 fileTypes = grammar.fileTypes - if customFileTypes = atom.config.get('core.customFileTypes')?[grammar.scopeName] + if customFileTypes = @config.get('core.customFileTypes')?[grammar.scopeName] fileTypes = fileTypes.concat(customFileTypes) for fileType, i in fileTypes @@ -133,6 +123,3 @@ class GrammarRegistry extends FirstMate.GrammarRegistry clearGrammarOverrides: -> @grammarOverridesByPath = {} undefined - - clearObservers: -> - @emitter = new Emitter diff --git a/src/gutter-container-component.coffee b/src/gutter-container-component.coffee index 40e2c8c26..c5538324b 100644 --- a/src/gutter-container-component.coffee +++ b/src/gutter-container-component.coffee @@ -7,7 +7,7 @@ LineNumberGutterComponent = require './line-number-gutter-component' module.exports = class GutterContainerComponent - constructor: ({@onLineNumberGutterMouseDown, @editor, @domElementPool}) -> + constructor: ({@onLineNumberGutterMouseDown, @editor, @domElementPool, @views}) -> # An array of objects of the form: {name: {String}, component: {Object}} @gutterComponents = [] @gutterComponentsByGutterName = {} @@ -39,10 +39,10 @@ class GutterContainerComponent gutterComponent = @gutterComponentsByGutterName[gutter.name] if not gutterComponent if gutter.name is 'line-number' - gutterComponent = new LineNumberGutterComponent({onMouseDown: @onLineNumberGutterMouseDown, @editor, gutter, @domElementPool}) + gutterComponent = new LineNumberGutterComponent({onMouseDown: @onLineNumberGutterMouseDown, @editor, gutter, @domElementPool, @views}) @lineNumberGutterComponent = gutterComponent else - gutterComponent = new CustomGutterComponent({gutter}) + gutterComponent = new CustomGutterComponent({gutter, @views}) if visible then gutterComponent.showNode() else gutterComponent.hideNode() # Pass the gutter only the state that it needs. diff --git a/src/initialize-application-window.coffee b/src/initialize-application-window.coffee new file mode 100644 index 000000000..acf50bc5d --- /dev/null +++ b/src/initialize-application-window.coffee @@ -0,0 +1,34 @@ +# Like sands through the hourglass, so are the days of our lives. + +path = require 'path' +require './window' +{getWindowLoadSettings} = require './window-load-settings-helpers' + +{resourcePath, isSpec, devMode} = getWindowLoadSettings() + +# Add application-specific exports to module search path. +exportsPath = path.join(resourcePath, 'exports') +require('module').globalPaths.push(exportsPath) +process.env.NODE_PATH = exportsPath + +# Make React faster +process.env.NODE_ENV ?= 'production' unless devMode + +AtomEnvironment = require './atom-environment' +ApplicationDelegate = require './application-delegate' +window.atom = new AtomEnvironment({ + window, document, + applicationDelegate: new ApplicationDelegate, + configDirPath: process.env.ATOM_HOME + enablePersistence: true +}) + +atom.displayWindow() +atom.loadStateSync() +atom.startEditorWindow() + +# Workaround for focus getting cleared upon window creation +windowFocused = -> + window.removeEventListener('focus', windowFocused) + setTimeout (-> document.querySelector('atom-workspace').focus()), 0 +window.addEventListener('focus', windowFocused) diff --git a/src/initialize-test-window.coffee b/src/initialize-test-window.coffee new file mode 100644 index 000000000..ad15429ff --- /dev/null +++ b/src/initialize-test-window.coffee @@ -0,0 +1,59 @@ +# Start the crash reporter before anything else. +require('crash-reporter').start(productName: 'Atom', companyName: 'GitHub') +remote = require 'remote' + +exitWithStatusCode = (status) -> + remote.require('app').emit('will-quit') + remote.process.exit(status) + +try + path = require 'path' + ipc = require 'ipc' + {getWindowLoadSettings} = require './window-load-settings-helpers' + AtomEnvironment = require '../src/atom-environment' + ApplicationDelegate = require '../src/application-delegate' + + # Show window synchronously so a focusout doesn't fire on input elements + # that are focused in the very first spec run. + remote.getCurrentWindow().show() unless getWindowLoadSettings().headless + + handleKeydown = (event) -> + # Reload: cmd-r / ctrl-r + if (event.metaKey or event.ctrlKey) and event.keyCode is 82 + ipc.send('call-window-method', 'restart') + + # Toggle Dev Tools: cmd-alt-i / ctrl-alt-i + if (event.metaKey or event.ctrlKey) and event.altKey and event.keyCode is 73 + ipc.send('call-window-method', 'toggleDevTools') + + # Reload: cmd-w / ctrl-w + if (event.metaKey or event.ctrlKey) and event.keyCode is 87 + ipc.send('call-window-method', 'close') + + window.addEventListener('keydown', handleKeydown, true) + + # Add 'exports' to module search path. + exportsPath = path.join(getWindowLoadSettings().resourcePath, 'exports') + require('module').globalPaths.push(exportsPath) + process.env.NODE_PATH = exportsPath # Set NODE_PATH env variable since tasks may need it. + + document.title = "Spec Suite" + + legacyTestRunner = require(getWindowLoadSettings().legacyTestRunnerPath) + testRunner = require(getWindowLoadSettings().testRunnerPath) + promise = testRunner({ + logFile: getWindowLoadSettings().logFile + headless: getWindowLoadSettings().headless + testPaths: getWindowLoadSettings().testPaths + buildAtomEnvironment: (params) -> new AtomEnvironment(params) + buildDefaultApplicationDelegate: (params) -> new ApplicationDelegate() + legacyTestRunner: legacyTestRunner + }) + + promise.then(exitWithStatusCode) if getWindowLoadSettings().headless +catch error + if getWindowLoadSettings().headless + console.error(error.stack ? error) + exitWithStatusCode(1) + else + throw error diff --git a/src/keymap-extensions.coffee b/src/keymap-extensions.coffee index da77f0156..82f2e8b99 100644 --- a/src/keymap-extensions.coffee +++ b/src/keymap-extensions.coffee @@ -20,6 +20,8 @@ KeymapManager::loadBundledKeymaps = -> @emitter.emit 'did-load-bundled-keymaps' KeymapManager::getUserKeymapPath = -> + return "" unless @configDirPath? + if userKeymapPath = CSON.resolve(path.join(@configDirPath, 'keymap')) userKeymapPath else @@ -41,11 +43,11 @@ KeymapManager::loadUserKeymap = -> [this document][watches] for more info. [watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path """ - atom.notifications.addError(message, {dismissable: true}) + @notificationManager.addError(message, {dismissable: true}) else detail = error.path stack = error.stack - atom.notifications.addFatalError(error.message, {detail, stack, dismissable: true}) + @notificationManager.addFatalError(error.message, {detail, stack, dismissable: true}) KeymapManager::subscribeToFileReadFailure = -> @onDidFailToReadFile (error) => @@ -57,6 +59,6 @@ KeymapManager::subscribeToFileReadFailure = -> else error.message - atom.notifications.addError(message, {detail, dismissable: true}) + @notificationManager.addError(message, {detail, dismissable: true}) module.exports = KeymapManager diff --git a/src/language-mode.coffee b/src/language-mode.coffee index e60e85f7f..55707bad3 100644 --- a/src/language-mode.coffee +++ b/src/language-mode.coffee @@ -8,7 +8,7 @@ class LanguageMode # Sets up a `LanguageMode` for the given {TextEditor}. # # editor - The {TextEditor} to associate with - constructor: (@editor) -> + constructor: (@editor, @config) -> {@buffer} = @editor destroy: -> @@ -327,7 +327,7 @@ class LanguageMode @editor.setIndentationForBufferRow(bufferRow, desiredIndentLevel) getRegexForProperty: (scopeDescriptor, property) -> - if pattern = atom.config.get(property, scope: scopeDescriptor) + if pattern = @config.get(property, scope: scopeDescriptor) new OnigRegExp(pattern) increaseIndentRegexForScopeDescriptor: (scopeDescriptor) -> @@ -343,8 +343,8 @@ class LanguageMode @getRegexForProperty(scopeDescriptor, 'editor.foldEndPattern') commentStartAndEndStringsForScope: (scope) -> - commentStartEntry = atom.config.getAll('editor.commentStart', {scope})[0] - commentEndEntry = _.find atom.config.getAll('editor.commentEnd', {scope}), (entry) -> + commentStartEntry = @config.getAll('editor.commentStart', {scope})[0] + commentEndEntry = _.find @config.getAll('editor.commentEnd', {scope}), (entry) -> entry.scopeSelector is commentStartEntry.scopeSelector commentStartString = commentStartEntry?.value commentEndString = commentEndEntry?.value diff --git a/src/line-number-gutter-component.coffee b/src/line-number-gutter-component.coffee index b4a5bc168..bb66ff144 100644 --- a/src/line-number-gutter-component.coffee +++ b/src/line-number-gutter-component.coffee @@ -7,12 +7,12 @@ module.exports = class LineNumberGutterComponent extends TiledComponent dummyLineNumberNode: null - constructor: ({@onMouseDown, @editor, @gutter, @domElementPool}) -> + constructor: ({@onMouseDown, @editor, @gutter, @domElementPool, @views}) -> @visible = true @dummyLineNumberComponent = LineNumbersTileComponent.createDummy(@domElementPool) - @domNode = atom.views.getView(@gutter) + @domNode = @views.getView(@gutter) @lineNumbersNode = @domNode.firstChild @lineNumbersNode.innerHTML = '' diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 98b274fae..b5af56885 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -21,7 +21,7 @@ module.exports = class LinesComponent extends TiledComponent placeholderTextDiv: null - constructor: ({@presenter, @useShadowDOM, @domElementPool}) -> + constructor: ({@presenter, @useShadowDOM, @domElementPool, @assert, @grammars}) -> @domNode = document.createElement('div') @domNode.classList.add('lines') @tilesNode = document.createElement("div") @@ -72,7 +72,7 @@ class LinesComponent extends TiledComponent @oldState.indentGuidesVisible = @newState.indentGuidesVisible - buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter, @domElementPool}) + buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter, @domElementPool, @assert, @grammars}) buildEmptyState: -> {tiles: {}} diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 619acc56a..6b4ac80ba 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -13,8 +13,8 @@ cloneObject = (object) -> module.exports = class LinesTileComponent - constructor: ({@presenter, @id, @domElementPool}) -> - @tokenIterator = new TokenIterator + constructor: ({@presenter, @id, @domElementPool, @assert, grammars}) -> + @tokenIterator = new TokenIterator(grammarRegistry: grammars) @measuredLines = new Set @lineNodesByLineId = {} @screenRowsByLineId = {} diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 0dc3e7602..3254451f1 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -3,8 +3,8 @@ TokenIterator = require './token-iterator' module.exports = class LinesYardstick - constructor: (@model, @presenter, @lineNodesProvider) -> - @tokenIterator = new TokenIterator + constructor: (@model, @presenter, @lineNodesProvider, grammarRegistry) -> + @tokenIterator = new TokenIterator({grammarRegistry}) @rangeForMeasurement = document.createRange() @invalidateCache() diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee index b138f2da2..fa78d3cd6 100644 --- a/src/menu-manager.coffee +++ b/src/menu-manager.coffee @@ -59,12 +59,12 @@ platformMenu = require('../package.json')?._atomMenu?.menu # See {::add} for more info about adding menu's directly. module.exports = class MenuManager - constructor: ({@resourcePath}) -> + constructor: ({@resourcePath, @keymapManager, @packageManager}) -> @pendingUpdateOperation = null @template = [] - atom.keymaps.onDidLoadBundledKeymaps => @loadPlatformItems() - atom.keymaps.onDidReloadKeymap => @update() - atom.packages.onDidActivateInitialPackages => @sortPackagesMenu() + @keymapManager.onDidLoadBundledKeymaps => @loadPlatformItems() + @keymapManager.onDidReloadKeymap => @update() + @packageManager.onDidActivateInitialPackages => @sortPackagesMenu() # Public: Adds the given items to the application menu. # @@ -96,6 +96,10 @@ class MenuManager @unmerge(@template, item) for item in items @update() + clear: -> + @template = [] + @update() + # Should the binding for the given selector be included in the menu # commands. # @@ -143,7 +147,7 @@ class MenuManager includedBindings = [] unsetKeystrokes = new Set - for binding in atom.keymaps.getKeyBindings() when @includeSelector(binding.selector) + for binding in @keymapManager.getKeyBindings() when @includeSelector(binding.selector) includedBindings.push(binding) if binding.command is 'unset!' unsetKeystrokes.add(binding.keystrokes) @@ -191,7 +195,10 @@ class MenuManager # Get an {Array} of {String} classes for the given element. classesForElement: (element) -> - element?.classList.toString().split(' ') ? [] + if classList = element?.classList + Array::slice.apply(classList) + else + [] sortPackagesMenu: -> packagesMenu = _.find @template, ({label}) -> MenuHelpers.normalizeLabel(label) is 'Packages' diff --git a/src/overlay-manager.coffee b/src/overlay-manager.coffee index 7858cb8a2..83787ad10 100644 --- a/src/overlay-manager.coffee +++ b/src/overlay-manager.coffee @@ -1,6 +1,6 @@ module.exports = class OverlayManager - constructor: (@presenter, @container) -> + constructor: (@presenter, @container, @views) -> @overlaysById = {} render: (state) -> @@ -28,7 +28,7 @@ class OverlayManager @presenter.setOverlayDimensions(decorationId, itemView.offsetWidth, itemView.offsetHeight, contentMargin) renderOverlay: (state, decorationId, {item, pixelPosition, class: klass}) -> - itemView = atom.views.getView(item) + itemView = @views.getView(item) cachedOverlay = @overlaysById[decorationId] unless overlayNode = cachedOverlay?.overlayNode overlayNode = document.createElement('atom-overlay') diff --git a/src/package-manager.coffee b/src/package-manager.coffee index 7e4a19470..f6cef9465 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -1,8 +1,10 @@ path = require 'path' +normalizePackageData = null _ = require 'underscore-plus' {Emitter} = require 'event-kit' fs = require 'fs-plus' +CSON = require 'season' ServiceHub = require 'service-hub' Package = require './package' @@ -26,15 +28,21 @@ ThemePackage = require './theme-package' # settings and also by calling `enablePackage()/disablePackage()`. module.exports = class PackageManager - constructor: ({configDirPath, @devMode, safeMode, @resourcePath}) -> + constructor: (params) -> + { + configDirPath, @devMode, safeMode, @resourcePath, @config, @styleManager, + @notificationManager, @keymapManager, @commandRegistry, @grammarRegistry + } = params + @emitter = new Emitter @activationHookEmitter = new Emitter @packageDirPaths = [] - unless safeMode + if configDirPath? and not safeMode if @devMode @packageDirPaths.push(path.join(configDirPath, "dev", "packages")) @packageDirPaths.push(path.join(configDirPath, "packages")) + @packagesCache = require('../package.json')?._atomPackages ? {} @loadedPackages = {} @activePackages = {} @packageStates = {} @@ -43,6 +51,17 @@ class PackageManager @packageActivators = [] @registerPackageActivator(this, ['atom', 'textmate']) + setContextMenuManager: (@contextMenuManager) -> + + setMenuManager: (@menuManager) -> + + setThemeManager: (@themeManager) -> + + reset: -> + @serviceHub.clear() + @deactivatePackages() + @packageStates = {} + ### Section: Event Subscription ### @@ -185,7 +204,7 @@ class PackageManager # # Returns a {Boolean}. isPackageDisabled: (name) -> - _.include(atom.config.get('core.disabledPackages') ? [], name) + _.include(@config.get('core.disabledPackages') ? [], name) ### Section: Accessing active packages @@ -269,7 +288,7 @@ class PackageManager packages = [] for packagePath in @getAvailablePackagePaths() name = path.basename(packagePath) - metadata = @getLoadedPackage(name)?.metadata ? Package.loadMetadata(packagePath, true) + metadata = @getLoadedPackage(name)?.metadata ? @loadPackageMetadata(packagePath, true) packages.push(metadata) packages @@ -292,7 +311,7 @@ class PackageManager @packageDependencies hasAtomEngine: (packagePath) -> - metadata = Package.loadMetadata(packagePath, true) + metadata = @loadPackageMetadata(packagePath, true) metadata?.engines?.atom? unobserveDisabledPackages: -> @@ -300,7 +319,7 @@ class PackageManager @disabledPackagesSubscription = null observeDisabledPackages: -> - @disabledPackagesSubscription ?= atom.config.onDidChange 'core.disabledPackages', ({newValue, oldValue}) => + @disabledPackagesSubscription ?= @config.onDidChange 'core.disabledPackages', ({newValue, oldValue}) => packagesToEnable = _.difference(oldValue, newValue) packagesToDisable = _.difference(newValue, oldValue) @@ -313,7 +332,7 @@ class PackageManager @packagesWithKeymapsDisabledSubscription = null observePackagesWithKeymapsDisabled: -> - @packagesWithKeymapsDisabledSubscription ?= atom.config.onDidChange 'core.packagesWithKeymapsDisabled', ({newValue, oldValue}) => + @packagesWithKeymapsDisabledSubscription ?= @config.onDidChange 'core.packagesWithKeymapsDisabled', ({newValue, oldValue}) => keymapsToEnable = _.difference(oldValue, newValue) keymapsToDisable = _.difference(newValue, oldValue) @@ -340,7 +359,7 @@ class PackageManager return pack if pack = @getLoadedPackage(name) try - metadata = Package.loadMetadata(packagePath) ? {} + metadata = @loadPackageMetadata(packagePath) ? {} catch error @handleMetadataError(error, packagePath) return null @@ -350,10 +369,15 @@ class PackageManager console.warn "Could not load #{metadata.name}@#{metadata.version} because it uses deprecated APIs that have been removed." return null + options = { + path: packagePath, metadata, packageManager: this, @config, @styleManager, + @commandRegistry, @keymapManager, @devMode, @notificationManager, + @grammarRegistry, @themeManager, @menuManager, @contextMenuManager + } if metadata.theme - pack = new ThemePackage(packagePath, metadata) + pack = new ThemePackage(options) else - pack = new Package(packagePath, metadata) + pack = new Package(options) pack.load() @loadedPackages[pack.name] = pack @emitter.emit 'did-load-package', pack @@ -392,7 +416,7 @@ class PackageManager activatePackages: (packages) -> promises = [] - atom.config.transact => + @config.transact => for pack in packages promise = @activatePackage(pack.name) promises.push(promise) unless pack.hasActivationCommands() @@ -423,7 +447,7 @@ class PackageManager # Deactivate all packages deactivatePackages: -> - atom.config.transact => + @config.transact => @deactivatePackage(pack.name) for pack in @getLoadedPackages() return @unobserveDisabledPackages() @@ -443,7 +467,7 @@ class PackageManager detail = "#{error.message} in #{metadataPath}" stack = "#{error.stack}\n at #{metadataPath}:1:1" message = "Failed to load the #{path.basename(packagePath)} package" - atom.notifications.addError(message, {stack, detail, dismissable: true}) + @notificationManager.addError(message, {stack, detail, dismissable: true}) uninstallDirectory: (directory) -> symlinkPromise = new Promise (resolve) -> @@ -456,3 +480,40 @@ class PackageManager [isSymLink, isDir] = values if not isSymLink and isDir fs.remove directory, -> + + reloadActivePackageStyleSheets: -> + for pack in @getActivePackages() when pack.getType() isnt 'theme' + pack.reloadStylesheets?() + return + + isBundledPackagePath: (packagePath) -> + if @devMode + return false unless @resourcePath.startsWith("#{process.resourcesPath}#{path.sep}") + + @resourcePathWithTrailingSlash ?= "#{@resourcePath}#{path.sep}" + packagePath?.startsWith(@resourcePathWithTrailingSlash) + + loadPackageMetadata: (packagePath, ignoreErrors=false) -> + packageName = path.basename(packagePath) + if @isBundledPackagePath(packagePath) + metadata = @packagesCache[packageName]?.metadata + unless metadata? + if metadataPath = CSON.resolve(path.join(packagePath, 'package')) + try + metadata = CSON.readFileSync(metadataPath) + @normalizePackageMetadata(metadata) + catch error + throw error unless ignoreErrors + + metadata ?= {} + unless typeof metadata.name is 'string' and metadata.name.length > 0 + metadata.name = packageName + + metadata + + normalizePackageMetadata: (metadata) -> + unless metadata?._id + normalizePackageData ?= require 'normalize-package-data' + normalizePackageData(metadata) + if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string' + metadata.repository.url = metadata.repository.url.replace(/^git\+/, '') diff --git a/src/package.coffee b/src/package.coffee index 0163f8bcd..00963ef28 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -1,5 +1,4 @@ path = require 'path' -normalizePackageData = null _ = require 'underscore-plus' async = require 'async' @@ -11,44 +10,10 @@ ModuleCache = require './module-cache' ScopedProperties = require './scoped-properties' BufferedProcess = require './buffered-process' -packagesCache = require('../package.json')?._atomPackages ? {} - # Extended: Loads and activates a package's main module and resources such as # stylesheets, keymaps, grammar, editor properties, and menus. module.exports = class Package - @isBundledPackagePath: (packagePath) -> - if atom.packages.devMode - return false unless atom.packages.resourcePath.startsWith("#{process.resourcesPath}#{path.sep}") - - @resourcePathWithTrailingSlash ?= "#{atom.packages.resourcePath}#{path.sep}" - packagePath?.startsWith(@resourcePathWithTrailingSlash) - - @normalizeMetadata: (metadata) -> - unless metadata?._id - normalizePackageData ?= require 'normalize-package-data' - normalizePackageData(metadata) - if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string' - metadata.repository.url = metadata.repository.url.replace(/^git\+/, '') - - @loadMetadata: (packagePath, ignoreErrors=false) -> - packageName = path.basename(packagePath) - if @isBundledPackagePath(packagePath) - metadata = packagesCache[packageName]?.metadata - unless metadata? - if metadataPath = CSON.resolve(path.join(packagePath, 'package')) - try - metadata = CSON.readFileSync(metadataPath) - @normalizeMetadata(metadata) - catch error - throw error unless ignoreErrors - - metadata ?= {} - unless typeof metadata.name is 'string' and metadata.name.length > 0 - metadata.name = packageName - - metadata - keymaps: null menus: null stylesheets: null @@ -64,10 +29,16 @@ class Package Section: Construction ### - constructor: (@path, @metadata) -> + constructor: (params) -> + { + @path, @metadata, @packageManager, @config, @styleManager, @commandRegistry, + @keymapManager, @devMode, @notificationManager, @grammarRegistry, @themeManager, + @menuManager, @contextMenuManager + } = params + @emitter = new Emitter - @metadata ?= Package.loadMetadata(@path) - @bundledPackage = Package.isBundledPackagePath(@path) + @metadata ?= @packageManager.loadPackageMetadata(@path) + @bundledPackage = @packageManager.isBundledPackagePath(@path) @name = @metadata?.name ? path.basename(@path) ModuleCache.add(@path, @metadata) @reset() @@ -89,10 +60,10 @@ class Package ### enable: -> - atom.config.removeAtKeyPath('core.disabledPackages', @name) + @config.removeAtKeyPath('core.disabledPackages', @name) disable: -> - atom.config.pushAtKeyPath('core.disabledPackages', @name) + @config.pushAtKeyPath('core.disabledPackages', @name) isTheme: -> @metadata?.theme? @@ -132,7 +103,6 @@ class Package @activationPromise ?= new Promise (resolve, reject) => @resolveActivationPromise = resolve - @rejectActivationPromise = reject @measure 'activateTime', => try @activateResources() @@ -150,7 +120,7 @@ class Package @activateConfig() @activateStylesheets() if @mainModule? and not @mainActivated - @mainModule.activate?(atom.packages.getPackageState(@name) ? {}) + @mainModule.activate?(@packageManager.getPackageState(@name) ? {}) @mainActivated = true @activateServices() catch error @@ -164,7 +134,7 @@ class Package @requireMainModule() unless @mainModule? if @mainModule? if @mainModule.config? and typeof @mainModule.config is 'object' - atom.config.setSchema @name, {type: 'object', properties: @mainModule.config} + @config.setSchema @name, {type: 'object', properties: @mainModule.config} @mainModule.activateConfig?() @configActivated = true @@ -182,13 +152,13 @@ class Package else context = undefined - @stylesheetDisposables.add(atom.styles.addStyleSheet(source, {sourcePath, priority, context})) + @stylesheetDisposables.add(@styleManager.addStyleSheet(source, {sourcePath, priority, context})) @stylesheetsActivated = true activateResources: -> @activationDisposables = new CompositeDisposable - keymapIsDisabled = _.include(atom.config.get("core.packagesWithKeymapsDisabled") ? [], @name) + keymapIsDisabled = _.include(@config.get("core.packagesWithKeymapsDisabled") ? [], @name) if keymapIsDisabled @deactivateKeymaps() else @@ -197,14 +167,14 @@ class Package for [menuPath, map] in @menus when map['context-menu']? try itemsBySelector = map['context-menu'] - @activationDisposables.add(atom.contextMenu.add(itemsBySelector)) + @activationDisposables.add(@contextMenuManager.add(itemsBySelector)) catch error if error.code is 'EBADSELECTOR' error.message += " in #{menuPath}" error.stack += "\n at #{menuPath}:1:1" throw error - @activationDisposables.add(atom.menu.add(map['menu'])) for [menuPath, map] in @menus when map['menu']? + @activationDisposables.add(@menuManager.add(map['menu'])) for [menuPath, map] in @menus when map['menu']? unless @grammarsActivated grammar.activate() for grammar in @grammars @@ -218,8 +188,8 @@ class Package @keymapDisposables = new CompositeDisposable() - @keymapDisposables.add(atom.keymaps.add(keymapPath, map)) for [keymapPath, map] in @keymaps - atom.menu.update() + @keymapDisposables.add(@keymapManager.add(keymapPath, map)) for [keymapPath, map] in @keymaps + @menuManager.update() @keymapActivated = true @@ -227,7 +197,7 @@ class Package return if not @keymapActivated @keymapDisposables?.dispose() - atom.menu.update() + @menuManager.update() @keymapActivated = false @@ -243,24 +213,24 @@ class Package for version, methodName of versions if typeof @mainModule[methodName] is 'function' servicesByVersion[version] = @mainModule[methodName]() - @activationDisposables.add atom.packages.serviceHub.provide(name, servicesByVersion) + @activationDisposables.add @packageManager.serviceHub.provide(name, servicesByVersion) for name, {versions} of @metadata.consumedServices for version, methodName of versions if typeof @mainModule[methodName] is 'function' - @activationDisposables.add atom.packages.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule)) + @activationDisposables.add @packageManager.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule)) return loadKeymaps: -> - if @bundledPackage and packagesCache[@name]? - @keymaps = (["#{atom.packages.resourcePath}#{path.sep}#{keymapPath}", keymapObject] for keymapPath, keymapObject of packagesCache[@name].keymaps) + if @bundledPackage and @packageManager.packagesCache[@name]? + @keymaps = (["#{@packageManager.resourcePath}#{path.sep}#{keymapPath}", keymapObject] for keymapPath, keymapObject of @packageManager.packagesCache[@name].keymaps) else @keymaps = @getKeymapPaths().map (keymapPath) -> [keymapPath, CSON.readFileSync(keymapPath) ? {}] return loadMenus: -> - if @bundledPackage and packagesCache[@name]? - @menus = (["#{atom.packages.resourcePath}#{path.sep}#{menuPath}", menuObject] for menuPath, menuObject of packagesCache[@name].menus) + if @bundledPackage and @packageManager.packagesCache[@name]? + @menus = (["#{@packageManager.resourcePath}#{path.sep}#{menuPath}", menuObject] for menuPath, menuObject of @packageManager.packagesCache[@name].menus) else @menus = @getMenuPaths().map (menuPath) -> [menuPath, CSON.readFileSync(menuPath) ? {}] return @@ -280,8 +250,8 @@ class Package fs.listSync(menusDirPath, ['cson', 'json']) loadStylesheets: -> - @stylesheets = @getStylesheetPaths().map (stylesheetPath) -> - [stylesheetPath, atom.themes.loadStylesheet(stylesheetPath, true)] + @stylesheets = @getStylesheetPaths().map (stylesheetPath) => + [stylesheetPath, @themeManager.loadStylesheet(stylesheetPath, true)] getStylesheetsPath: -> path.join(@path, 'styles') @@ -304,7 +274,7 @@ class Package grammarPaths = fs.listSync(grammarsDirPath, ['json', 'cson']) for grammarPath in grammarPaths try - grammar = atom.grammars.readGrammarSync(grammarPath) + grammar = @grammarRegistry.readGrammarSync(grammarPath) grammar.packageName = @name grammar.bundledPackage = @bundledPackage @grammars.push(grammar) @@ -319,11 +289,11 @@ class Package return Promise.resolve() if @grammarsLoaded loadGrammar = (grammarPath, callback) => - atom.grammars.readGrammar grammarPath, (error, grammar) => + @grammarRegistry.readGrammar grammarPath, (error, grammar) => if error? detail = "#{error.message} in #{grammarPath}" stack = "#{error.stack}\n at #{grammarPath}:1:1" - atom.notifications.addFatalError("Failed to load a #{@name} package grammar", {stack, detail, dismissable: true}) + @notificationManager.addFatalError("Failed to load a #{@name} package grammar", {stack, detail, dismissable: true}) else grammar.packageName = @name grammar.bundledPackage = @bundledPackage @@ -343,11 +313,11 @@ class Package @settings = [] loadSettingsFile = (settingsPath, callback) => - ScopedProperties.load settingsPath, (error, settings) => + ScopedProperties.load settingsPath, @config, (error, settings) => if error? detail = "#{error.message} in #{settingsPath}" stack = "#{error.stack}\n at #{settingsPath}:1:1" - atom.notifications.addFatalError("Failed to load the #{@name} package settings", {stack, detail, dismissable: true}) + @notificationManager.addFatalError("Failed to load the #{@name} package settings", {stack, detail, dismissable: true}) else @settings.push(settings) settings.activate() if @settingsActivated @@ -370,10 +340,8 @@ class Package console.error "Error serializing package '#{@name}'", e.stack deactivate: -> - @rejectActivationPromise?() @activationPromise = null @resolveActivationPromise = null - @rejectActivationPromise = null @activationCommandSubscriptions?.dispose() @deactivateResources() @deactivateConfig() @@ -430,9 +398,9 @@ class Package return @mainModulePath if @resolvedMainModulePath @resolvedMainModulePath = true - if @bundledPackage and packagesCache[@name]? - if packagesCache[@name].main - @mainModulePath = "#{atom.packages.resourcePath}#{path.sep}#{packagesCache[@name].main}" + if @bundledPackage and @packageManager.packagesCache[@name]? + if @packageManager.packagesCache[@name].main + @mainModulePath = "#{@packageManager.resourcePath}#{path.sep}#{@packageManager.packagesCache[@name].main}" else @mainModulePath = null else @@ -466,7 +434,7 @@ class Package # Add dummy command so it appears in menu. # The real command will be registered on package activation try - @activationCommandSubscriptions.add atom.commands.add selector, command, -> + @activationCommandSubscriptions.add @commandRegistry.add selector, command, -> catch error if error.code is 'EBADSELECTOR' metadataPath = path.join(@path, 'package.json') @@ -474,7 +442,7 @@ class Package error.stack += "\n at #{metadataPath}:1:1" throw error - @activationCommandSubscriptions.add atom.commands.onWillDispatch (event) => + @activationCommandSubscriptions.add @commandRegistry.onWillDispatch (event) => return unless event.type is command currentTarget = event.target while currentTarget @@ -505,7 +473,7 @@ class Package @activationHookSubscriptions = new CompositeDisposable for hook in @getActivationHooks() do (hook) => - @activationHookSubscriptions.add(atom.packages.onDidTriggerActivationHook(hook, => @activateNow())) if hook? and _.isString(hook) and hook.trim().length > 0 + @activationHookSubscriptions.add(@packageManager.onDidTriggerActivationHook(hook, => @activateNow())) if hook? and _.isString(hook) and hook.trim().length > 0 return @@ -567,7 +535,7 @@ class Package isCompatible: -> return @compatible if @compatible? - if @path.indexOf(path.join(atom.packages.resourcePath, 'node_modules') + path.sep) is 0 + if @path.indexOf(path.join(@packageManager.resourcePath, 'node_modules') + path.sep) is 0 # Bundled packages are always considered compatible @compatible = true else if @getMainModulePath() @@ -603,7 +571,7 @@ class Package stderr = '' stdout = '' new BufferedProcess({ - command: atom.packages.getApmPath() + command: @packageManager.getApmPath() args: ['rebuild', '--no-color'] options: {cwd: @path} stderr: (output) -> stderr += output @@ -625,7 +593,7 @@ class Package # This information is cached in local storage on a per package/version basis # to minimize the impact on startup time. getIncompatibleNativeModules: -> - unless atom.inDevMode() + unless @devMode try if arrayAsString = global.localStorage.getItem(@getIncompatibleNativeModulesStorageKey()) return JSON.parse(arrayAsString) @@ -666,4 +634,4 @@ class Package detail = error.message stack = error.stack ? error - atom.notifications.addFatalError(message, {stack, detail, dismissable: true}) + @notificationManager.addFatalError(message, {stack, detail, dismissable: true}) diff --git a/src/pane-axis-element.coffee b/src/pane-axis-element.coffee index 91f27858e..eaa26a9fe 100644 --- a/src/pane-axis-element.coffee +++ b/src/pane-axis-element.coffee @@ -8,7 +8,9 @@ class PaneAxisElement extends HTMLElement detachedCallback: -> @subscriptions.dispose() - initialize: (@model) -> + initialize: (@model, {@views}) -> + throw new Error("Must pass a views parameter when initializing TextEditorElements") unless @views? + @subscriptions.add @model.onDidAddChild(@childAdded.bind(this)) @subscriptions.add @model.onDidRemoveChild(@childRemoved.bind(this)) @subscriptions.add @model.onDidReplaceChild(@childReplaced.bind(this)) @@ -27,7 +29,7 @@ class PaneAxisElement extends HTMLElement element?.nodeName.toLowerCase() is 'atom-pane-resize-handle' childAdded: ({child, index}) -> - view = atom.views.getView(child) + view = @views.getView(child) @insertBefore(view, @children[index * 2]) prevElement = view.previousSibling @@ -43,7 +45,7 @@ class PaneAxisElement extends HTMLElement @insertBefore(resizeHandle, nextElement) childRemoved: ({child}) -> - view = atom.views.getView(child) + view = @views.getView(child) siblingView = view.previousSibling # make sure next sibling view is pane resize view if siblingView? and @isPaneResizeHandleElement(siblingView) diff --git a/src/pane-axis.coffee b/src/pane-axis.coffee index 57d07d8a9..d104e984b 100644 --- a/src/pane-axis.coffee +++ b/src/pane-axis.coffee @@ -4,19 +4,16 @@ Model = require './model' module.exports = class PaneAxis extends Model - atom.deserializers.add(this) - parent: null container: null orientation: null - @deserialize: (state, params) -> - container = params?.container - state.container = container - state.children = state.children.map (childState) -> atom.deserializers.deserialize(childState, {container}) + @deserialize: (state, {deserializers}) -> + state.children = state.children.map (childState) -> + deserializers.deserialize(childState) new this(state) - constructor: ({@container, @orientation, children, flexScale}={}) -> + constructor: ({@orientation, children, flexScale}={}) -> @emitter = new Emitter @subscriptionsByChild = new WeakMap @subscriptions = new CompositeDisposable @@ -43,7 +40,10 @@ class PaneAxis extends Model getContainer: -> @container - setContainer: (@container) -> @container + setContainer: (container) -> + if container and container isnt @container + @container = container + child.setContainer(container) for child in @children getOrientation: -> @orientation diff --git a/src/pane-container-element.coffee b/src/pane-container-element.coffee index b3d4e0036..9da452558 100644 --- a/src/pane-container-element.coffee +++ b/src/pane-container-element.coffee @@ -7,7 +7,9 @@ class PaneContainerElement extends HTMLElement @subscriptions = new CompositeDisposable @classList.add 'panes' - initialize: (@model) -> + initialize: (@model, {@views}) -> + throw new Error("Must pass a views parameter when initializing PaneContainerElements") unless @views? + @subscriptions.add @model.observeRoot(@rootChanged.bind(this)) this @@ -15,7 +17,7 @@ class PaneContainerElement extends HTMLElement focusedElement = document.activeElement if @hasFocus() @firstChild?.remove() if root? - view = atom.views.getView(root) + view = @views.getView(root) @appendChild(view) focusedElement?.focus() @@ -40,7 +42,7 @@ class PaneContainerElement extends HTMLElement y = pointB.y - pointA.y Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) - paneView = atom.views.getView(@model.getActivePane()) + paneView = @views.getView(@model.getActivePane()) box = @boundingBoxForPaneView(paneView) paneViews = _.toArray(@querySelectorAll('atom-pane')) diff --git a/src/pane-container.coffee b/src/pane-container.coffee index a8d1fa5d2..8ad082a5f 100644 --- a/src/pane-container.coffee +++ b/src/pane-container.coffee @@ -7,46 +7,37 @@ ItemRegistry = require './item-registry' module.exports = class PaneContainer extends Model - atom.deserializers.add(this) - - @version: 1 - + serializationVersion: 1 root: null stoppedChangingActivePaneItemDelay: 100 stoppedChangingActivePaneItemTimeout: null - @deserialize: (state) -> - container = Object.create(@prototype) # allows us to pass a self reference to our child before invoking constructor - state.root = atom.deserializers.deserialize(state.root, {container}) - state.destroyEmptyPanes = atom.config.get('core.destroyEmptyPanes') - state.activePane = find state.root.getPanes(), (pane) -> pane.id is state.activePaneId - @call(container, state) # run constructor - container - constructor: (params) -> super - @activePane = params?.activePane - + {@config, applicationDelegate, notificationManager, deserializerManager} = params @emitter = new Emitter @subscriptions = new CompositeDisposable - @itemRegistry = new ItemRegistry - @setRoot(params?.root ? new Pane) - @setActivePane(@getPanes()[0]) unless @getActivePane() - - @destroyEmptyPanes() if params?.destroyEmptyPanes - + @setRoot(new Pane({container: this, @config, applicationDelegate, notificationManager, deserializerManager})) + @setActivePane(@getRoot()) @monitorActivePaneItem() @monitorPaneItems() serialize: (params) -> deserializer: 'PaneContainer' - version: @constructor.version + version: @serializationVersion root: @root?.serialize() activePaneId: @activePane.id + deserialize: (state, deserializerManager) -> + return unless state.version is @serializationVersion + @setRoot(deserializerManager.deserialize(state.root)) + activePane = find @getRoot().getPanes(), (pane) -> pane.id is state.activePaneId + @setActivePane(activePane ? @getPanes()[0]) + @destroyEmptyPanes() if @config.get('core.destroyEmptyPanes') + onDidChangeRoot: (fn) -> @emitter.on 'did-change-root', fn diff --git a/src/pane-element.coffee b/src/pane-element.coffee index c6ded79c4..59f003300 100644 --- a/src/pane-element.coffee +++ b/src/pane-element.coffee @@ -44,14 +44,17 @@ class PaneElement extends HTMLElement event.stopPropagation() @getModel().activate() pathsToOpen = Array::map.call event.dataTransfer.files, (file) -> file.path - atom.open({pathsToOpen}) if pathsToOpen.length > 0 + @open({pathsToOpen}) if pathsToOpen.length > 0 @addEventListener 'focus', handleFocus, true @addEventListener 'blur', handleBlur, true @addEventListener 'dragover', handleDragOver @addEventListener 'drop', handleDrop - initialize: (@model) -> + initialize: (@model, {@views, @open}) -> + throw new Error("Must pass a views parameter when initializing PaneElements") unless @views? + throw new Error("Must pass a open parameter when initializing PaneElements") unless @open? + @subscriptions.add @model.onDidActivate(@activated.bind(this)) @subscriptions.add @model.observeActive(@activeStatusChanged.bind(this)) @subscriptions.add @model.observeActiveItem(@activeItemChanged.bind(this)) @@ -78,7 +81,7 @@ class PaneElement extends HTMLElement return unless item? hasFocus = @hasFocus() - itemView = atom.views.getView(item) + itemView = @views.getView(item) if itemPath = item.getPath?() @dataset.activeItemName = path.basename(itemPath) @@ -109,7 +112,7 @@ class PaneElement extends HTMLElement itemView.style.display = 'none' itemRemoved: ({item, index, destroyed}) -> - if viewToRemove = atom.views.getView(item) + if viewToRemove = @views.getView(item) viewToRemove.remove() paneDestroyed: -> @@ -118,35 +121,9 @@ class PaneElement extends HTMLElement flexScaleChanged: (flexScale) -> @style.flexGrow = flexScale - getActiveView: -> atom.views.getView(@model.getActiveItem()) + getActiveView: -> @views.getView(@model.getActiveItem()) hasFocus: -> this is document.activeElement or @contains(document.activeElement) -atom.commands.add 'atom-workspace', - 'pane:show-next-item': -> @getModel().getActivePane().activateNextItem() - 'pane:show-previous-item': -> @getModel().getActivePane().activatePreviousItem() - 'pane:show-item-1': -> @getModel().getActivePane().activateItemAtIndex(0) - 'pane:show-item-2': -> @getModel().getActivePane().activateItemAtIndex(1) - 'pane:show-item-3': -> @getModel().getActivePane().activateItemAtIndex(2) - 'pane:show-item-4': -> @getModel().getActivePane().activateItemAtIndex(3) - 'pane:show-item-5': -> @getModel().getActivePane().activateItemAtIndex(4) - '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:move-item-right': -> @getModel().getActivePane().moveItemRight() - 'pane:move-item-left': -> @getModel().getActivePane().moveItemLeft() - -atom.commands.add 'atom-pane', - 'pane:save-items': -> @getModel().saveItems() - 'pane:split-left': -> @getModel().splitLeft(copyActiveItem: true) - 'pane:split-right': -> @getModel().splitRight(copyActiveItem: true) - 'pane:split-up': -> @getModel().splitUp(copyActiveItem: true) - 'pane:split-down': -> @getModel().splitDown(copyActiveItem: true) - 'pane:close': -> @getModel().close() - 'pane:close-other-items': -> @getModel().destroyInactiveItems() - 'pane:increase-size': -> @getModel().increaseSize() - 'pane:decrease-size': -> @getModel().decreaseSize() - module.exports = PaneElement = document.registerElement 'atom-pane', prototype: PaneElement.prototype diff --git a/src/pane.coffee b/src/pane.coffee index a146b66fc..5f0d8dc1b 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -10,30 +10,31 @@ TextEditor = require './text-editor' # the default configuration, tabs are also displayed for each item. module.exports = class Pane extends Model - atom.deserializers.add(this) - container: undefined activeItem: undefined focused: false - @deserialize: (state, params) -> + @deserialize: (state, {deserializers, applicationDelegate, config, notifications}) -> {items, activeItemURI, activeItemUri} = state - state.container = params?.container activeItemURI ?= activeItemUri - state.items = compact(items.map (itemState) -> atom.deserializers.deserialize(itemState)) + state.items = compact(items.map (itemState) -> deserializers.deserialize(itemState)) state.activeItem = find state.items, (item) -> if typeof item.getURI is 'function' itemURI = item.getURI() itemURI is activeItemURI - - new this(state) + new Pane(extend(state, { + deserializerManager: deserializers, + notificationManager: notifications, + config, applicationDelegate + })) constructor: (params) -> super - @container = params?.container - @activeItem = params?.activeItem - @focused = params?.focused + { + @activeItem, @focused, @applicationDelegate, @notificationManager, @config, + @deserializerManager + } = params @emitter = new Emitter @itemSubscriptions = new WeakMap @@ -61,7 +62,7 @@ class Pane extends Model getContainer: -> @container setContainer: (container) -> - unless container is @container + if container and container isnt @container @container = container container.didAddPane({pane: this}) @@ -395,7 +396,7 @@ class Pane extends Model @items.splice(index, 1) @emitter.emit 'did-remove-item', {item, index, destroyed: not moved, moved} @container?.didDestroyPaneItem({item, index, pane: this}) unless moved - @destroy() if @items.length is 0 and atom.config.get('core.destroyEmptyPanes') + @destroy() if @items.length is 0 and @config.get('core.destroyEmptyPanes') # Public: Move the given item to the given index. # @@ -461,7 +462,7 @@ class Pane extends Model else return true - chosen = atom.confirm + chosen = @applicationDelegate.confirm message: "'#{item.getTitle?() ? uri}' has changes, do you want to save them?" detailedMessage: "Your changes will be lost if you close this item without saving." buttons: ["Save", "Cancel", "Don't Save"] @@ -514,7 +515,7 @@ class Pane extends Model saveOptions = item.getSaveDialogOptions?() ? {} saveOptions.defaultPath ?= item.getPath() - newItemPath = atom.showSaveDialogSync(saveOptions) + newItemPath = @applicationDelegate.showSaveDialog(saveOptions) if newItemPath try item.saveAs(newItemPath) @@ -554,7 +555,7 @@ class Pane extends Model copyActiveItem: -> if @activeItem? - @activeItem.copy?() ? atom.deserializers.deserialize(@activeItem.serialize()) + @activeItem.copy?() ? @deserializerManager.deserialize(@activeItem.serialize()) ### Section: Lifecycle @@ -646,7 +647,7 @@ class Pane extends Model @parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this], @flexScale})) @setFlexScale(1) - newPane = new @constructor(params) + newPane = new Pane(extend({@applicationDelegate, @deserializerManager, @config}, params)) switch side when 'before' then @parent.insertChildBefore(this, newPane) when 'after' then @parent.insertChildAfter(this, newPane) @@ -688,12 +689,12 @@ class Pane extends Model handleSaveError: (error, item) -> itemPath = error.path ? item?.getPath?() - addWarningWithPath = (message, options) -> + addWarningWithPath = (message, options) => message = "#{message} '#{itemPath}'" if itemPath - atom.notifications.addWarning(message, options) + @notificationManager.addWarning(message, options) if error.code is 'EISDIR' or error.message?.endsWith?('is a directory') - atom.notifications.addWarning("Unable to save file: #{error.message}") + @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'] @@ -716,6 +717,6 @@ class Pane extends Model addWarningWithPath('Unable to save file: Invalid seek') 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") + @notificationManager.addWarning("Unable to save file: A directory in the path '#{fileName}' could not be written to") else throw error diff --git a/src/panel-container-element.coffee b/src/panel-container-element.coffee index e1459e6d6..40ce7e352 100644 --- a/src/panel-container-element.coffee +++ b/src/panel-container-element.coffee @@ -4,7 +4,9 @@ class PanelContainerElement extends HTMLElement createdCallback: -> @subscriptions = new CompositeDisposable - initialize: (@model) -> + initialize: (@model, {@views}) -> + throw new Error("Must pass a views parameter when initializing PanelContainerElements") unless @views? + @subscriptions.add @model.onDidAddPanel(@panelAdded.bind(this)) @subscriptions.add @model.onDidRemovePanel(@panelRemoved.bind(this)) @subscriptions.add @model.onDidDestroy(@destroyed.bind(this)) @@ -14,7 +16,7 @@ class PanelContainerElement extends HTMLElement getModel: -> @model panelAdded: ({panel, index}) -> - panelElement = atom.views.getView(panel) + panelElement = @views.getView(panel) panelElement.classList.add(@model.getLocation()) if @model.isModal() panelElement.classList.add("overlay", "from-top") @@ -33,7 +35,7 @@ class PanelContainerElement extends HTMLElement @hideAllPanelsExcept(panel) if visible panelRemoved: ({panel, index}) -> - @removeChild(atom.views.getView(panel)) + @removeChild(@views.getView(panel)) destroyed: -> @subscriptions.dispose() diff --git a/src/panel-element.coffee b/src/panel-element.coffee index 287d34940..8d6aadae4 100644 --- a/src/panel-element.coffee +++ b/src/panel-element.coffee @@ -5,7 +5,9 @@ class PanelElement extends HTMLElement createdCallback: -> @subscriptions = new CompositeDisposable - initialize: (@model) -> + initialize: (@model, {@views}) -> + throw new Error("Must pass a views parameter when initializing PanelElements") unless @views? + @appendChild(@getItemView()) @classList.add(@model.getClassName().split(' ')...) if @model.getClassName()? @@ -17,7 +19,7 @@ class PanelElement extends HTMLElement @model ?= new Panel getItemView: -> - atom.views.getView(@getModel().getItem()) + @views.getView(@getModel().getItem()) attachedCallback: -> @visibleChanged(@getModel().isVisible()) diff --git a/src/project.coffee b/src/project.coffee index b3f05a942..935e3a213 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -3,7 +3,7 @@ url = require 'url' _ = require 'underscore-plus' fs = require 'fs-plus' -{Emitter} = require 'event-kit' +{Emitter, Disposable} = require 'event-kit' TextBuffer = require 'text-buffer' DefaultDirectoryProvider = require './default-directory-provider' @@ -17,68 +17,35 @@ GitRepositoryProvider = require './git-repository-provider' # An instance of this class is always available as the `atom.project` global. module.exports = class Project extends Model - atom.deserializers.add(this) - ### Section: Construction and Destruction ### - @deserialize: (state) -> - state.buffers = _.compact state.buffers.map (bufferState) -> - # Check that buffer's file path is accessible - return if fs.isDirectorySync(bufferState.filePath) - if bufferState.filePath - try - fs.closeSync(fs.openSync(bufferState.filePath, 'r')) - catch error - return unless error.code is 'ENOENT' - - atom.deserializers.deserialize(bufferState) - - new this(state) - - constructor: ({path, paths, @buffers}={}) -> + constructor: ({@notificationManager, packageManager, config}) -> @emitter = new Emitter - @buffers ?= [] + @buffers = [] + @paths = [] @rootDirectories = [] @repositories = [] - @directoryProviders = [] @defaultDirectoryProvider = new DefaultDirectoryProvider() - atom.packages.serviceHub.consume( - 'atom.directory-provider', - '^0.1.0', - (provider) => @directoryProviders.unshift(provider)) - - # Mapping from the real path of a {Directory} to a {Promise} that resolves - # to either a {Repository} or null. Ideally, the {Directory} would be used - # as the key; however, there can be multiple {Directory} objects created for - # the same real path, so it is not a good key. @repositoryPromisesByPath = new Map() - - @repositoryProviders = [new GitRepositoryProvider(this)] - atom.packages.serviceHub.consume( - 'atom.repository-provider', - '^0.1.0', - (provider) => - @repositoryProviders.push(provider) - - # If a path in getPaths() does not have a corresponding Repository, try - # to assign one by running through setPaths() again now that - # @repositoryProviders has been updated. - if null in @repositories - @setPaths(@getPaths()) - ) - - @subscribeToBuffer(buffer) for buffer in @buffers - - paths ?= _.compact([path]) - @setPaths(paths) + @repositoryProviders = [new GitRepositoryProvider(this, config)] + @consumeServices(packageManager) destroyed: -> - buffer.destroy() for buffer in @getBuffers() + buffer.destroy() for buffer in @buffers @setPaths([]) + reset: (packageManager) -> + @emitter.dispose() + @emitter = new Emitter + + buffer?.destroy() for buffer in @buffers + @buffers = [] + @setPaths([]) + @consumeServices(packageManager) + destroyUnretainedBuffers: -> buffer.destroy() for buffer in @getBuffers() when not buffer.isRetained() return @@ -87,6 +54,22 @@ class Project extends Model Section: Serialization ### + deserialize: (state, deserializerManager) -> + states.paths = [state.path] if state.path? # backward compatibility + + @buffers = _.compact state.buffers.map (bufferState) -> + # Check that buffer's file path is accessible + return if fs.isDirectorySync(bufferState.filePath) + if bufferState.filePath + try + fs.closeSync(fs.openSync(bufferState.filePath, 'r')) + catch error + return unless error.code is 'ENOENT' + deserializerManager.deserialize(bufferState) + + @subscribeToBuffer(buffer) for buffer in @buffers + @setPaths(state.paths) + serialize: -> deserializer: 'Project' paths: @getPaths() @@ -291,39 +274,25 @@ class Project extends Model Section: Private ### - # Given a path to a file, this constructs and associates a new - # {TextEditor}, showing the file. - # - # * `filePath` The {String} path of the file to associate with. - # * `options` Options that you can pass to the {TextEditor} constructor. - # - # Returns a promise that resolves to an {TextEditor}. - open: (filePath, options={}) -> - filePath = @resolvePath(filePath) + consumeServices: ({serviceHub}) -> + serviceHub.consume( + 'atom.directory-provider', + '^0.1.0', + (provider) => + @directoryProviders.unshift(provider) + new Disposable => + @directoryProviders.splice(@directoryProviders.indexOf(provider), 1) + ) - if filePath? - try - fs.closeSync(fs.openSync(filePath, 'r')) - catch error - # allow ENOENT errors to create an editor for paths that dont exist - throw error unless error.code is 'ENOENT' - - absoluteFilePath = @resolvePath(filePath) - - fileSize = fs.getSizeSync(absoluteFilePath) - - if fileSize >= 20 * 1048576 # 20MB - choice = atom.confirm - message: 'Atom will be unresponsive during the loading of very large files.' - detailedMessage: "Do you still want to load this file?" - buttons: ["Proceed", "Cancel"] - if choice is 1 - error = new Error - error.code = 'CANCELLED' - throw error - - @bufferForPath(absoluteFilePath).then (buffer) => - @buildEditorForBuffer(buffer, _.extend({fileSize}, options)) + serviceHub.consume( + 'atom.repository-provider', + '^0.1.0', + (provider) => + @repositoryProviders.push(provider) + @setPaths(@getPaths()) if null in @repositories + new Disposable => + @repositoryProviders.splice(@repositoryProviders.indexOf(provider), 1) + ) # Retrieves all the {TextBuffer}s in the project; that is, the # buffers for all open files. @@ -404,11 +373,6 @@ class Project extends Model [buffer] = @buffers.splice(index, 1) buffer?.destroy() - buildEditorForBuffer: (buffer, editorOptions) -> - largeFileMode = editorOptions.fileSize >= 2 * 1048576 # 2MB - editor = new TextEditor(_.extend({buffer, largeFileMode, registerEditor: true}, editorOptions)) - editor - eachBuffer: (args...) -> subscriber = args.shift() if args.length > 1 callback = args.shift() @@ -421,9 +385,9 @@ class Project extends Model subscribeToBuffer: (buffer) -> buffer.onDidDestroy => @removeBuffer(buffer) - buffer.onWillThrowWatchError ({error, handle}) -> + buffer.onWillThrowWatchError ({error, handle}) => handle() - atom.notifications.addWarning """ + @notificationManager.addWarning """ Unable to read file after file `#{error.eventType}` event. Make sure you have permission to access `#{buffer.getPath()}`. """, diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee new file mode 100644 index 000000000..80b7ff6c6 --- /dev/null +++ b/src/register-default-commands.coffee @@ -0,0 +1,212 @@ +ipc = require 'ipc' + +module.exports = ({commandRegistry, commandInstaller, config}) -> + commandRegistry.add 'atom-workspace', + 'pane:show-next-item': -> @getModel().getActivePane().activateNextItem() + 'pane:show-previous-item': -> @getModel().getActivePane().activatePreviousItem() + 'pane:show-item-1': -> @getModel().getActivePane().activateItemAtIndex(0) + 'pane:show-item-2': -> @getModel().getActivePane().activateItemAtIndex(1) + 'pane:show-item-3': -> @getModel().getActivePane().activateItemAtIndex(2) + 'pane:show-item-4': -> @getModel().getActivePane().activateItemAtIndex(3) + 'pane:show-item-5': -> @getModel().getActivePane().activateItemAtIndex(4) + '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:move-item-right': -> @getModel().getActivePane().moveItemRight() + 'pane:move-item-left': -> @getModel().getActivePane().moveItemLeft() + 'window:increase-font-size': -> @getModel().increaseFontSize() + 'window:decrease-font-size': -> @getModel().decreaseFontSize() + 'window:reset-font-size': -> @getModel().resetFontSize() + 'application:about': -> ipc.send('command', 'application:about') + 'application:show-preferences': -> ipc.send('command', 'application:show-settings') + 'application:show-settings': -> ipc.send('command', 'application:show-settings') + 'application:quit': -> ipc.send('command', 'application:quit') + 'application:hide': -> ipc.send('command', 'application:hide') + 'application:hide-other-applications': -> ipc.send('command', 'application:hide-other-applications') + 'application:install-update': -> ipc.send('command', 'application:install-update') + 'application:unhide-all-applications': -> ipc.send('command', 'application:unhide-all-applications') + 'application:new-window': -> ipc.send('command', 'application:new-window') + 'application:new-file': -> ipc.send('command', 'application:new-file') + 'application:open': -> ipc.send('command', 'application:open') + 'application:open-file': -> ipc.send('command', 'application:open-file') + 'application:open-folder': -> ipc.send('command', 'application:open-folder') + 'application:open-dev': -> ipc.send('command', 'application:open-dev') + 'application:open-safe': -> ipc.send('command', 'application:open-safe') + 'application:add-project-folder': -> atom.addProjectFolder() + 'application:minimize': -> ipc.send('command', 'application:minimize') + 'application:zoom': -> ipc.send('command', 'application:zoom') + 'application:bring-all-windows-to-front': -> ipc.send('command', 'application:bring-all-windows-to-front') + 'application:open-your-config': -> ipc.send('command', 'application:open-your-config') + 'application:open-your-init-script': -> ipc.send('command', 'application:open-your-init-script') + 'application:open-your-keymap': -> ipc.send('command', 'application:open-your-keymap') + 'application:open-your-snippets': -> ipc.send('command', 'application:open-your-snippets') + 'application:open-your-stylesheet': -> ipc.send('command', 'application:open-your-stylesheet') + 'application:open-license': -> @getModel().openLicense() + 'window:run-package-specs': -> @runPackageSpecs() + 'window:focus-next-pane': -> @getModel().activateNextPane() + 'window:focus-previous-pane': -> @getModel().activatePreviousPane() + 'window:focus-pane-above': -> @focusPaneViewAbove() + 'window:focus-pane-below': -> @focusPaneViewBelow() + 'window:focus-pane-on-left': -> @focusPaneViewOnLeft() + 'window:focus-pane-on-right': -> @focusPaneViewOnRight() + 'window:save-all': -> @getModel().saveAll() + 'window:toggle-invisibles': -> config.set("editor.showInvisibles", not config.get("editor.showInvisibles")) + 'window:log-deprecation-warnings': -> Grim.logDeprecations() + 'window:toggle-auto-indent': -> config.set("editor.autoIndent", not config.get("editor.autoIndent")) + 'pane:reopen-closed-item': -> @getModel().reopenItem() + 'core:close': -> @getModel().destroyActivePaneItemOrEmptyPane() + 'core:save': -> @getModel().saveActivePaneItem() + 'core:save-as': -> @getModel().saveActivePaneItemAs() + + if process.platform is 'darwin' + commandRegistry.add 'atom-workspace', 'window:install-shell-commands', -> + commandInstaller.installShellCommandsInteractively() + + commandRegistry.add 'atom-pane', + 'pane:save-items': -> @getModel().saveItems() + 'pane:split-left': -> @getModel().splitLeft(copyActiveItem: true) + 'pane:split-right': -> @getModel().splitRight(copyActiveItem: true) + 'pane:split-up': -> @getModel().splitUp(copyActiveItem: true) + 'pane:split-down': -> @getModel().splitDown(copyActiveItem: true) + 'pane:close': -> @getModel().close() + 'pane:close-other-items': -> @getModel().destroyInactiveItems() + 'pane:increase-size': -> @getModel().increaseSize() + 'pane:decrease-size': -> @getModel().decreaseSize() + + commandRegistry.add 'atom-text-editor', stopEventPropagation( + 'core:undo': -> @undo() + 'core:redo': -> @redo() + 'core:move-left': -> @moveLeft() + 'core:move-right': -> @moveRight() + 'core:select-left': -> @selectLeft() + 'core:select-right': -> @selectRight() + 'core:select-up': -> @selectUp() + 'core:select-down': -> @selectDown() + 'core:select-all': -> @selectAll() + 'editor:select-word': -> @selectWordsContainingCursors() + 'editor:consolidate-selections': (event) -> event.abortKeyBinding() unless @consolidateSelections() + 'editor:move-to-beginning-of-next-paragraph': -> @moveToBeginningOfNextParagraph() + 'editor:move-to-beginning-of-previous-paragraph': -> @moveToBeginningOfPreviousParagraph() + 'editor:move-to-beginning-of-screen-line': -> @moveToBeginningOfScreenLine() + 'editor:move-to-beginning-of-line': -> @moveToBeginningOfLine() + 'editor:move-to-end-of-screen-line': -> @moveToEndOfScreenLine() + 'editor:move-to-end-of-line': -> @moveToEndOfLine() + 'editor:move-to-first-character-of-line': -> @moveToFirstCharacterOfLine() + 'editor:move-to-beginning-of-word': -> @moveToBeginningOfWord() + 'editor:move-to-end-of-word': -> @moveToEndOfWord() + 'editor:move-to-beginning-of-next-word': -> @moveToBeginningOfNextWord() + 'editor:move-to-previous-word-boundary': -> @moveToPreviousWordBoundary() + 'editor:move-to-next-word-boundary': -> @moveToNextWordBoundary() + 'editor:move-to-previous-subword-boundary': -> @moveToPreviousSubwordBoundary() + 'editor:move-to-next-subword-boundary': -> @moveToNextSubwordBoundary() + 'editor:select-to-beginning-of-next-paragraph': -> @selectToBeginningOfNextParagraph() + 'editor:select-to-beginning-of-previous-paragraph': -> @selectToBeginningOfPreviousParagraph() + 'editor:select-to-end-of-line': -> @selectToEndOfLine() + 'editor:select-to-beginning-of-line': -> @selectToBeginningOfLine() + 'editor:select-to-end-of-word': -> @selectToEndOfWord() + 'editor:select-to-beginning-of-word': -> @selectToBeginningOfWord() + 'editor:select-to-beginning-of-next-word': -> @selectToBeginningOfNextWord() + 'editor:select-to-next-word-boundary': -> @selectToNextWordBoundary() + 'editor:select-to-previous-word-boundary': -> @selectToPreviousWordBoundary() + 'editor:select-to-next-subword-boundary': -> @selectToNextSubwordBoundary() + 'editor:select-to-previous-subword-boundary': -> @selectToPreviousSubwordBoundary() + 'editor:select-to-first-character-of-line': -> @selectToFirstCharacterOfLine() + 'editor:select-line': -> @selectLinesContainingCursors() + ) + + commandRegistry.add 'atom-text-editor', stopEventPropagationAndGroupUndo(config, + 'core:backspace': -> @backspace() + 'core:delete': -> @delete() + 'core:cut': -> @cutSelectedText() + 'core:copy': -> @copySelectedText() + 'core:paste': -> @pasteText() + 'editor:delete-to-previous-word-boundary': -> @deleteToPreviousWordBoundary() + 'editor:delete-to-next-word-boundary': -> @deleteToNextWordBoundary() + 'editor:delete-to-beginning-of-word': -> @deleteToBeginningOfWord() + 'editor:delete-to-beginning-of-line': -> @deleteToBeginningOfLine() + 'editor:delete-to-end-of-line': -> @deleteToEndOfLine() + 'editor:delete-to-end-of-word': -> @deleteToEndOfWord() + 'editor:delete-to-beginning-of-subword': -> @deleteToBeginningOfSubword() + 'editor:delete-to-end-of-subword': -> @deleteToEndOfSubword() + 'editor:delete-line': -> @deleteLine() + 'editor:cut-to-end-of-line': -> @cutToEndOfLine() + 'editor:cut-to-end-of-buffer-line': -> @cutToEndOfBufferLine() + 'editor:transpose': -> @transpose() + 'editor:upper-case': -> @upperCase() + 'editor:lower-case': -> @lowerCase() + 'editor:copy-selection': -> @copyOnlySelectedText() + ) + + commandRegistry.add 'atom-text-editor:not([mini])', stopEventPropagation( + 'core:move-up': -> @moveUp() + 'core:move-down': -> @moveDown() + 'core:move-to-top': -> @moveToTop() + 'core:move-to-bottom': -> @moveToBottom() + 'core:page-up': -> @pageUp() + 'core:page-down': -> @pageDown() + 'core:select-to-top': -> @selectToTop() + 'core:select-to-bottom': -> @selectToBottom() + 'core:select-page-up': -> @selectPageUp() + 'core:select-page-down': -> @selectPageDown() + 'editor:add-selection-below': -> @addSelectionBelow() + 'editor:add-selection-above': -> @addSelectionAbove() + 'editor:split-selections-into-lines': -> @splitSelectionsIntoLines() + 'editor:toggle-soft-tabs': -> @toggleSoftTabs() + 'editor:toggle-soft-wrap': -> @toggleSoftWrapped() + 'editor:fold-all': -> @foldAll() + 'editor:unfold-all': -> @unfoldAll() + 'editor:fold-current-row': -> @foldCurrentRow() + 'editor:unfold-current-row': -> @unfoldCurrentRow() + 'editor:fold-selection': -> @foldSelectedLines() + 'editor:fold-at-indent-level-1': -> @foldAllAtIndentLevel(0) + 'editor:fold-at-indent-level-2': -> @foldAllAtIndentLevel(1) + 'editor:fold-at-indent-level-3': -> @foldAllAtIndentLevel(2) + 'editor:fold-at-indent-level-4': -> @foldAllAtIndentLevel(3) + 'editor:fold-at-indent-level-5': -> @foldAllAtIndentLevel(4) + 'editor:fold-at-indent-level-6': -> @foldAllAtIndentLevel(5) + 'editor:fold-at-indent-level-7': -> @foldAllAtIndentLevel(6) + 'editor:fold-at-indent-level-8': -> @foldAllAtIndentLevel(7) + 'editor:fold-at-indent-level-9': -> @foldAllAtIndentLevel(8) + 'editor:log-cursor-scope': -> @logCursorScope() + 'editor:copy-path': -> @copyPathToClipboard() + 'editor:toggle-indent-guide': -> config.set('editor.showIndentGuide', not config.get('editor.showIndentGuide')) + 'editor:toggle-line-numbers': -> config.set('editor.showLineNumbers', not config.get('editor.showLineNumbers')) + 'editor:scroll-to-cursor': -> @scrollToCursorPosition() + ) + + commandRegistry.add 'atom-text-editor:not([mini])', stopEventPropagationAndGroupUndo(config, + 'editor:indent': -> @indent() + 'editor:auto-indent': -> @autoIndentSelectedRows() + 'editor:indent-selected-rows': -> @indentSelectedRows() + 'editor:outdent-selected-rows': -> @outdentSelectedRows() + 'editor:newline': -> @insertNewline() + 'editor:newline-below': -> @insertNewlineBelow() + 'editor:newline-above': -> @insertNewlineAbove() + 'editor:toggle-line-comments': -> @toggleLineCommentsInSelection() + 'editor:checkout-head-revision': -> @checkoutHeadRevision() + 'editor:move-line-up': -> @moveLineUp() + 'editor:move-line-down': -> @moveLineDown() + 'editor:duplicate-lines': -> @duplicateLines() + 'editor:join-lines': -> @joinLines() + ) + +stopEventPropagation = (commandListeners) -> + newCommandListeners = {} + for commandName, commandListener of commandListeners + do (commandListener) -> + newCommandListeners[commandName] = (event) -> + event.stopPropagation() + commandListener.call(@getModel(), event) + newCommandListeners + +stopEventPropagationAndGroupUndo = (config, commandListeners) -> + newCommandListeners = {} + for commandName, commandListener of commandListeners + do (commandListener) -> + newCommandListeners[commandName] = (event) -> + event.stopPropagation() + model = @getModel() + model.transact config.get('editor.undoGroupingInterval'), -> + commandListener.call(model, event) + newCommandListeners diff --git a/src/scoped-properties.coffee b/src/scoped-properties.coffee index 8bf4c5ed3..c7257083e 100644 --- a/src/scoped-properties.coffee +++ b/src/scoped-properties.coffee @@ -3,21 +3,21 @@ CSON = require 'season' module.exports = class ScopedProperties - @load: (scopedPropertiesPath, callback) -> + @load: (scopedPropertiesPath, config, callback) -> CSON.readFile scopedPropertiesPath, (error, scopedProperties={}) -> if error? callback(error) else - callback(null, new ScopedProperties(scopedPropertiesPath, scopedProperties)) + callback(null, new ScopedProperties(scopedPropertiesPath, scopedProperties, config)) - constructor: (@path, @scopedProperties) -> + constructor: (@path, @scopedProperties, @config) -> activate: -> for selector, properties of @scopedProperties - atom.config.set(null, properties, scopeSelector: selector, source: @path) + @config.set(null, properties, scopeSelector: selector, source: @path) return deactivate: -> for selector of @scopedProperties - atom.config.unset(null, scopeSelector: selector, source: @path) + @config.unset(null, scopeSelector: selector, source: @path) return diff --git a/src/selection.coffee b/src/selection.coffee index 80929cac7..2ba66ebb0 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -14,7 +14,7 @@ class Selection extends Model initialScreenRange: null wordwise: false - constructor: ({@cursor, @marker, @editor, id}) -> + constructor: ({@cursor, @marker, @editor, id, @clipboard}) -> @emitter = new Emitter @assignId(id) @@ -611,7 +611,7 @@ class Selection extends Model startLevel = @editor.indentLevelForLine(precedingText) if maintainClipboard - {text: clipboardText, metadata} = atom.clipboard.readWithMetadata() + {text: clipboardText, metadata} = @clipboard.readWithMetadata() metadata ?= {} unless metadata.selections? metadata.selections = [{ @@ -624,9 +624,9 @@ class Selection extends Model indentBasis: startLevel, fullLine: fullLine }) - atom.clipboard.write([clipboardText, selectionText].join("\n"), metadata) + @clipboard.write([clipboardText, selectionText].join("\n"), metadata) else - atom.clipboard.write(selectionText, { + @clipboard.write(selectionText, { indentBasis: startLevel, fullLine: fullLine }) diff --git a/src/storage-folder.coffee b/src/storage-folder.coffee index bf969dee2..da8af3f2e 100644 --- a/src/storage-folder.coffee +++ b/src/storage-folder.coffee @@ -4,12 +4,16 @@ fs = require "fs-plus" module.exports = class StorageFolder constructor: (containingPath) -> - @path = path.join(containingPath, "storage") + @path = path.join(containingPath, "storage") if containingPath? store: (name, object) -> + return unless @path? + fs.writeFileSync(@pathForKey(name), JSON.stringify(object), 'utf8') load: (name) -> + return unless @path? + statePath = @pathForKey(name) try stateString = fs.readFileSync(statePath, 'utf8') diff --git a/src/style-manager.coffee b/src/style-manager.coffee index cfe86b3fe..8f932d229 100644 --- a/src/style-manager.coffee +++ b/src/style-manager.coffee @@ -1,6 +1,7 @@ fs = require 'fs-plus' path = require 'path' {Emitter, Disposable} = require 'event-kit' +StylesElement = require './styles-element' # Extended: A singleton instance of this class available via `atom.styles`, # which you can use to globally query and observe the set of active style @@ -9,7 +10,7 @@ path = require 'path' # which clone and attach style elements in different contexts. module.exports = class StyleManager - constructor: -> + constructor: ({@configDirPath}) -> @emitter = new Emitter @styleElements = [] @styleElementsBySourcePath = {} @@ -154,6 +155,11 @@ class StyleManager return + buildStylesElement: -> + stylesElement = new StylesElement + stylesElement.initialize(this) + stylesElement + ### Section: Paths ### @@ -162,8 +168,10 @@ class StyleManager # # Returns a {String}. getUserStyleSheetPath: -> - stylesheetPath = fs.resolve(path.join(atom.getConfigDirPath(), 'styles'), ['css', 'less']) + return "" unless @configDirPath? + + stylesheetPath = fs.resolve(path.join(@configDirPath, 'styles'), ['css', 'less']) if fs.isFileSync(stylesheetPath) stylesheetPath else - path.join(atom.getConfigDirPath(), 'styles.less') + path.join(@configDirPath, 'styles.less') diff --git a/src/styles-element.coffee b/src/styles-element.coffee index fc3b888cf..d1e6bf3d9 100644 --- a/src/styles-element.coffee +++ b/src/styles-element.coffee @@ -14,6 +14,7 @@ class StylesElement extends HTMLElement @emitter.on 'did-update-style-element', callback createdCallback: -> + @subscriptions = new CompositeDisposable @emitter = new Emitter @styleElementClonesByOriginalElement = new WeakMap @@ -21,31 +22,29 @@ class StylesElement extends HTMLElement if @context is 'atom-text-editor' for styleElement in @children @upgradeDeprecatedSelectors(styleElement) - @initialize() + + @context = @getAttribute('context') ? undefined detachedCallback: -> @subscriptions.dispose() - @subscriptions = null + @subscriptions = new CompositeDisposable attributeChangedCallback: (attrName, oldVal, newVal) -> @contextChanged() if attrName is 'context' - initialize: -> - return if @subscriptions? + initialize: (@styleManager) -> + throw new Error("Must pass a styleManager parameter when initializing a StylesElement") unless @styleManager? - @subscriptions = new CompositeDisposable - @context = @getAttribute('context') ? undefined - - @subscriptions.add atom.styles.observeStyleElements(@styleElementAdded.bind(this)) - @subscriptions.add atom.styles.onDidRemoveStyleElement(@styleElementRemoved.bind(this)) - @subscriptions.add atom.styles.onDidUpdateStyleElement(@styleElementUpdated.bind(this)) + @subscriptions.add @styleManager.observeStyleElements(@styleElementAdded.bind(this)) + @subscriptions.add @styleManager.onDidRemoveStyleElement(@styleElementRemoved.bind(this)) + @subscriptions.add @styleManager.onDidUpdateStyleElement(@styleElementUpdated.bind(this)) contextChanged: -> return unless @subscriptions? @styleElementRemoved(child) for child in Array::slice.call(@children) @context = @getAttribute('context') - @styleElementAdded(styleElement) for styleElement in atom.styles.getStyleElements() + @styleElementAdded(styleElement) for styleElement in @styleManager.getStyleElements() return styleElementAdded: (styleElement) -> diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 3dd9f317c..8a6d885c2 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -38,15 +38,15 @@ class TextEditorComponent Object.defineProperty @prototype, "domNode", get: -> @domNodeValue set: (domNode) -> - atom.assert domNode?, "TextEditorComponent::domNode was set to null." + @assert domNode?, "TextEditorComponent::domNode was set to null." @domNodeValue = domNode - constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, tileSize}) -> + constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, tileSize, @views, @themes, @config, @workspace, @assert, @grammars}) -> @tileSize = tileSize if tileSize? @disposables = new CompositeDisposable @observeConfig() - @setScrollSensitivity(atom.config.get('editor.scrollSensitivity')) + @setScrollSensitivity(@config.get('editor.scrollSensitivity')) @presenter = new TextEditorPresenter model: @editor @@ -58,6 +58,7 @@ class TextEditorComponent cursorBlinkPeriod: @cursorBlinkPeriod cursorBlinkResumeDelay: @cursorBlinkResumeDelay stoppedScrollingDelay: 200 + config: @config @presenter.onDidUpdateState(@requestUpdate) @@ -70,10 +71,10 @@ class TextEditorComponent insertionPoint = document.createElement('content') insertionPoint.setAttribute('select', 'atom-overlay') @domNode.appendChild(insertionPoint) - @overlayManager = new OverlayManager(@presenter, @hostElement) + @overlayManager = new OverlayManager(@presenter, @hostElement, @views) else @domNode.classList.add('editor-contents') - @overlayManager = new OverlayManager(@presenter, @domNode) + @overlayManager = new OverlayManager(@presenter, @domNode, @views) @scrollViewNode = document.createElement('div') @scrollViewNode.classList.add('scroll-view') @@ -82,10 +83,10 @@ class TextEditorComponent @hiddenInputComponent = new InputComponent @scrollViewNode.appendChild(@hiddenInputComponent.getDomNode()) - @linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM, @domElementPool}) + @linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM, @domElementPool, @assert, @grammars}) @scrollViewNode.appendChild(@linesComponent.getDomNode()) - @linesYardstick = new LinesYardstick(@editor, @presenter, @linesComponent) + @linesYardstick = new LinesYardstick(@editor, @presenter, @linesComponent, @grammars) @presenter.setLinesYardstick(@linesYardstick) @horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll}) @@ -103,11 +104,11 @@ class TextEditorComponent @disposables.add @stylesElement.onDidAddStyleElement @onStylesheetsChanged @disposables.add @stylesElement.onDidUpdateStyleElement @onStylesheetsChanged @disposables.add @stylesElement.onDidRemoveStyleElement @onStylesheetsChanged - unless atom.themes.isInitialLoadComplete() - @disposables.add atom.themes.onDidChangeActiveThemes @onAllThemesLoaded + unless @themes.isInitialLoadComplete() + @disposables.add @themes.onDidChangeActiveThemes @onAllThemesLoaded @disposables.add scrollbarStyle.onDidChangePreferredScrollbarStyle @refreshScrollbars - @disposables.add atom.views.pollDocument(@pollDOM) + @disposables.add @views.pollDocument(@pollDOM) @updateSync() @checkForVisibilityChange() @@ -177,7 +178,7 @@ class TextEditorComponent @overlayManager?.measureOverlays() mountGutterContainerComponent: -> - @gutterContainerComponent = new GutterContainerComponent({@editor, @onLineNumberGutterMouseDown, @domElementPool}) + @gutterContainerComponent = new GutterContainerComponent({@editor, @onLineNumberGutterMouseDown, @domElementPool, @views}) @domNode.insertBefore(@gutterContainerComponent.getDomNode(), @domNode.firstChild) becameVisible: -> @@ -204,10 +205,10 @@ class TextEditorComponent @updateSync() else unless @updateRequested @updateRequested = true - atom.views.updateDocument => + @views.updateDocument => @updateRequested = false @updateSync() if @canUpdate() - atom.views.readDocument(@readAfterUpdateSync) + @views.readDocument(@readAfterUpdateSync) canUpdate: -> @mounted and @editor.isAlive() @@ -275,13 +276,13 @@ class TextEditorComponent timeoutId = setTimeout(writeSelectedTextToSelectionClipboard) observeConfig: -> - @disposables.add atom.config.onDidChange 'editor.fontSize', => + @disposables.add @config.onDidChange 'editor.fontSize', => @sampleFontStyling() @invalidateCharacterWidths() - @disposables.add atom.config.onDidChange 'editor.fontFamily', => + @disposables.add @config.onDidChange 'editor.fontFamily', => @sampleFontStyling() @invalidateCharacterWidths() - @disposables.add atom.config.onDidChange 'editor.lineHeight', => + @disposables.add @config.onDidChange 'editor.lineHeight', => @sampleFontStyling() @invalidateCharacterWidths() @@ -294,7 +295,7 @@ class TextEditorComponent @disposables.add(@scopedConfigDisposables) scope = @editor.getRootScopeDescriptor() - @scopedConfigDisposables.add atom.config.observe 'editor.scrollSensitivity', {scope}, @setScrollSensitivity + @scopedConfigDisposables.add @config.observe 'editor.scrollSensitivity', {scope}, @setScrollSensitivity focused: -> if @mounted @@ -354,11 +355,11 @@ class TextEditorComponent {wheelDeltaX, wheelDeltaY} = event # Ctrl+MouseWheel adjusts font size. - if event.ctrlKey and atom.config.get('editor.zoomFontWhenCtrlScrolling') + if event.ctrlKey and @config.get('editor.zoomFontWhenCtrlScrolling') if wheelDeltaY > 0 - atom.workspace.increaseFontSize() + @workspace.increaseFontSize() else if wheelDeltaY < 0 - atom.workspace.decreaseFontSize() + @workspace.decreaseFontSize() event.preventDefault() return @@ -543,7 +544,7 @@ class TextEditorComponent onStylesheetsChanged: (styleElement) => return unless @performedInitialMeasurement - return unless atom.themes.isInitialLoadComplete() + return unless @themes.isInitialLoadComplete() # This delay prevents the styling from going haywire when stylesheets are # reloaded in dev mode. It seems like a workaround for a browser bug, but @@ -650,7 +651,7 @@ class TextEditorComponent isVisible: -> # Investigating an exception that occurs here due to ::domNode being null. - atom.assert @domNode?, "TextEditorComponent::domNode was null.", (error) => + @assert @domNode?, "TextEditorComponent::domNode was null.", (error) => error.metadata = {@initialized} @domNode? and (@domNode.offsetHeight > 0 or @domNode.offsetWidth > 0) @@ -848,7 +849,7 @@ class TextEditorComponent @presenter.characterWidthsChanged() setShowIndentGuide: (showIndentGuide) -> - atom.config.set("editor.showIndentGuide", showIndentGuide) + @config.set("editor.showIndentGuide", showIndentGuide) setScrollSensitivity: (scrollSensitivity) => if scrollSensitivity = parseInt(scrollSensitivity) diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 405cf30e4..9fe0e9091 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -4,6 +4,7 @@ Path = require 'path' TextBuffer = require 'text-buffer' TextEditor = require './text-editor' TextEditorComponent = require './text-editor-component' +StylesElement = require './styles-element' ShadowStyleSheet = null @@ -18,29 +19,38 @@ class TextEditorElement extends HTMLElement logicalDisplayBuffer: true createdCallback: -> + # Use globals when the following instance variables aren't set. + @config = atom.config + @themes = atom.themes + @workspace = atom.workspace + @assert = atom.assert + @views = atom.views + @styles = atom.styles + @grammars = atom.grammars + @emitter = new Emitter @subscriptions = new CompositeDisposable - @initializeContent() + @addEventListener 'focus', @focused.bind(this) @addEventListener 'blur', @blurred.bind(this) - initializeContent: (attributes) -> @classList.add('editor') @setAttribute('tabindex', -1) - if atom.config.get('editor.useShadowDOM') + initializeContent: (attributes) -> + if @config.get('editor.useShadowDOM') @useShadowDOM = true unless ShadowStyleSheet? ShadowStyleSheet = document.createElement('style') - ShadowStyleSheet.textContent = atom.themes.loadLessStylesheet(require.resolve('../static/text-editor-shadow.less')) + ShadowStyleSheet.textContent = @themes.loadLessStylesheet(require.resolve('../static/text-editor-shadow.less')) @createShadowRoot() @shadowRoot.appendChild(ShadowStyleSheet.cloneNode(true)) - @stylesElement = document.createElement('atom-styles') + @stylesElement = new StylesElement + @stylesElement.initialize(@styles) @stylesElement.setAttribute('context', 'atom-text-editor') - @stylesElement.initialize() @rootElement = document.createElement('div') @rootElement.classList.add('editor--private') @@ -56,7 +66,7 @@ class TextEditorElement extends HTMLElement attachedCallback: -> @buildModel() unless @getModel()? - atom.assert(@model.isAlive(), "Attaching a view for a destroyed editor") + @assert(@model.isAlive(), "Attaching a view for a destroyed editor") @mountComponent() unless @component? @listenForComponentEvents() @component.checkForVisibilityChange() @@ -76,7 +86,15 @@ class TextEditorElement extends HTMLElement @subscriptions.add @component.onDidChangeScrollLeft => @emitter.emit("did-change-scroll-left", arguments...) - initialize: (model) -> + initialize: (model, {@views, @config, @themes, @workspace, @assert, @styles, @grammars}) -> + throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @views? + throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @config? + throw new Error("Must pass a themes parameter when initializing TextEditorElements") unless @themes? + throw new Error("Must pass a workspace parameter when initializing TextEditorElements") unless @workspace? + throw new Error("Must pass a assert parameter when initializing TextEditorElements") unless @assert? + throw new Error("Must pass a styles parameter when initializing TextEditorElements") unless @styles? + throw new Error("Must pass a grammars parameter when initializing TextEditorElements") unless @grammars? + @setModel(model) this @@ -85,6 +103,7 @@ class TextEditorElement extends HTMLElement return if model.isDestroyed() @model = model + @initializeContent() @mountComponent() @addGrammarScopeAttribute() @addMiniAttribute() if @model.isMini() @@ -99,7 +118,7 @@ class TextEditorElement extends HTMLElement @model ? @buildModel() buildModel: -> - @setModel(new TextEditor( + @setModel(@workspace.buildTextEditor( buffer: new TextBuffer(@textContent) softWrapped: false tabLength: 2 @@ -117,6 +136,12 @@ class TextEditorElement extends HTMLElement editor: @model tileSize: @tileSize useShadowDOM: @useShadowDOM + views: @views + themes: @themes + config: @config + workspace: @workspace + assert: @assert + grammars: @grammars ) @rootElement.appendChild(@component.getDomNode()) @@ -313,141 +338,4 @@ class TextEditorElement extends HTMLElement getHeight: -> @offsetHeight -stopEventPropagation = (commandListeners) -> - newCommandListeners = {} - for commandName, commandListener of commandListeners - do (commandListener) -> - newCommandListeners[commandName] = (event) -> - event.stopPropagation() - commandListener.call(@getModel(), event) - newCommandListeners - -stopEventPropagationAndGroupUndo = (commandListeners) -> - newCommandListeners = {} - for commandName, commandListener of commandListeners - do (commandListener) -> - newCommandListeners[commandName] = (event) -> - event.stopPropagation() - model = @getModel() - model.transact atom.config.get('editor.undoGroupingInterval'), -> - commandListener.call(model, event) - newCommandListeners - -atom.commands.add 'atom-text-editor', stopEventPropagation( - 'core:undo': -> @undo() - 'core:redo': -> @redo() - 'core:move-left': -> @moveLeft() - 'core:move-right': -> @moveRight() - 'core:select-left': -> @selectLeft() - 'core:select-right': -> @selectRight() - 'core:select-up': -> @selectUp() - 'core:select-down': -> @selectDown() - 'core:select-all': -> @selectAll() - 'editor:select-word': -> @selectWordsContainingCursors() - 'editor:consolidate-selections': (event) -> event.abortKeyBinding() unless @consolidateSelections() - 'editor:move-to-beginning-of-next-paragraph': -> @moveToBeginningOfNextParagraph() - 'editor:move-to-beginning-of-previous-paragraph': -> @moveToBeginningOfPreviousParagraph() - 'editor:move-to-beginning-of-screen-line': -> @moveToBeginningOfScreenLine() - 'editor:move-to-beginning-of-line': -> @moveToBeginningOfLine() - 'editor:move-to-end-of-screen-line': -> @moveToEndOfScreenLine() - 'editor:move-to-end-of-line': -> @moveToEndOfLine() - 'editor:move-to-first-character-of-line': -> @moveToFirstCharacterOfLine() - 'editor:move-to-beginning-of-word': -> @moveToBeginningOfWord() - 'editor:move-to-end-of-word': -> @moveToEndOfWord() - 'editor:move-to-beginning-of-next-word': -> @moveToBeginningOfNextWord() - 'editor:move-to-previous-word-boundary': -> @moveToPreviousWordBoundary() - 'editor:move-to-next-word-boundary': -> @moveToNextWordBoundary() - 'editor:move-to-previous-subword-boundary': -> @moveToPreviousSubwordBoundary() - 'editor:move-to-next-subword-boundary': -> @moveToNextSubwordBoundary() - 'editor:select-to-beginning-of-next-paragraph': -> @selectToBeginningOfNextParagraph() - 'editor:select-to-beginning-of-previous-paragraph': -> @selectToBeginningOfPreviousParagraph() - 'editor:select-to-end-of-line': -> @selectToEndOfLine() - 'editor:select-to-beginning-of-line': -> @selectToBeginningOfLine() - 'editor:select-to-end-of-word': -> @selectToEndOfWord() - 'editor:select-to-beginning-of-word': -> @selectToBeginningOfWord() - 'editor:select-to-beginning-of-next-word': -> @selectToBeginningOfNextWord() - 'editor:select-to-next-word-boundary': -> @selectToNextWordBoundary() - 'editor:select-to-previous-word-boundary': -> @selectToPreviousWordBoundary() - 'editor:select-to-next-subword-boundary': -> @selectToNextSubwordBoundary() - 'editor:select-to-previous-subword-boundary': -> @selectToPreviousSubwordBoundary() - 'editor:select-to-first-character-of-line': -> @selectToFirstCharacterOfLine() - 'editor:select-line': -> @selectLinesContainingCursors() -) - -atom.commands.add 'atom-text-editor', stopEventPropagationAndGroupUndo( - 'core:backspace': -> @backspace() - 'core:delete': -> @delete() - 'core:cut': -> @cutSelectedText() - 'core:copy': -> @copySelectedText() - 'core:paste': -> @pasteText() - 'editor:delete-to-previous-word-boundary': -> @deleteToPreviousWordBoundary() - 'editor:delete-to-next-word-boundary': -> @deleteToNextWordBoundary() - 'editor:delete-to-beginning-of-word': -> @deleteToBeginningOfWord() - 'editor:delete-to-beginning-of-line': -> @deleteToBeginningOfLine() - 'editor:delete-to-end-of-line': -> @deleteToEndOfLine() - 'editor:delete-to-end-of-word': -> @deleteToEndOfWord() - 'editor:delete-to-beginning-of-subword': -> @deleteToBeginningOfSubword() - 'editor:delete-to-end-of-subword': -> @deleteToEndOfSubword() - 'editor:delete-line': -> @deleteLine() - 'editor:cut-to-end-of-line': -> @cutToEndOfLine() - 'editor:cut-to-end-of-buffer-line': -> @cutToEndOfBufferLine() - 'editor:transpose': -> @transpose() - 'editor:upper-case': -> @upperCase() - 'editor:lower-case': -> @lowerCase() - 'editor:copy-selection': -> @copyOnlySelectedText() -) - -atom.commands.add 'atom-text-editor:not([mini])', stopEventPropagation( - 'core:move-up': -> @moveUp() - 'core:move-down': -> @moveDown() - 'core:move-to-top': -> @moveToTop() - 'core:move-to-bottom': -> @moveToBottom() - 'core:page-up': -> @pageUp() - 'core:page-down': -> @pageDown() - 'core:select-to-top': -> @selectToTop() - 'core:select-to-bottom': -> @selectToBottom() - 'core:select-page-up': -> @selectPageUp() - 'core:select-page-down': -> @selectPageDown() - 'editor:add-selection-below': -> @addSelectionBelow() - 'editor:add-selection-above': -> @addSelectionAbove() - 'editor:split-selections-into-lines': -> @splitSelectionsIntoLines() - 'editor:toggle-soft-tabs': -> @toggleSoftTabs() - 'editor:toggle-soft-wrap': -> @toggleSoftWrapped() - 'editor:fold-all': -> @foldAll() - 'editor:unfold-all': -> @unfoldAll() - 'editor:fold-current-row': -> @foldCurrentRow() - 'editor:unfold-current-row': -> @unfoldCurrentRow() - 'editor:fold-selection': -> @foldSelectedLines() - 'editor:fold-at-indent-level-1': -> @foldAllAtIndentLevel(0) - 'editor:fold-at-indent-level-2': -> @foldAllAtIndentLevel(1) - 'editor:fold-at-indent-level-3': -> @foldAllAtIndentLevel(2) - 'editor:fold-at-indent-level-4': -> @foldAllAtIndentLevel(3) - 'editor:fold-at-indent-level-5': -> @foldAllAtIndentLevel(4) - 'editor:fold-at-indent-level-6': -> @foldAllAtIndentLevel(5) - 'editor:fold-at-indent-level-7': -> @foldAllAtIndentLevel(6) - 'editor:fold-at-indent-level-8': -> @foldAllAtIndentLevel(7) - 'editor:fold-at-indent-level-9': -> @foldAllAtIndentLevel(8) - 'editor:log-cursor-scope': -> @logCursorScope() - 'editor:copy-path': -> @copyPathToClipboard() - 'editor:toggle-indent-guide': -> atom.config.set('editor.showIndentGuide', not atom.config.get('editor.showIndentGuide')) - 'editor:toggle-line-numbers': -> atom.config.set('editor.showLineNumbers', not atom.config.get('editor.showLineNumbers')) - 'editor:scroll-to-cursor': -> @scrollToCursorPosition() -) - -atom.commands.add 'atom-text-editor:not([mini])', stopEventPropagationAndGroupUndo( - 'editor:indent': -> @indent() - 'editor:auto-indent': -> @autoIndentSelectedRows() - 'editor:indent-selected-rows': -> @indentSelectedRows() - 'editor:outdent-selected-rows': -> @outdentSelectedRows() - 'editor:newline': -> @insertNewline() - 'editor:newline-below': -> @insertNewlineBelow() - 'editor:newline-above': -> @insertNewlineAbove() - 'editor:toggle-line-comments': -> @toggleLineCommentsInSelection() - 'editor:checkout-head-revision': -> @checkoutHeadRevision() - 'editor:move-line-up': -> @moveLineUp() - 'editor:move-line-down': -> @moveLineDown() - 'editor:duplicate-lines': -> @duplicateLines() - 'editor:join-lines': -> @joinLines() -) - module.exports = TextEditorElement = document.registerElement 'atom-text-editor', prototype: TextEditorElement.prototype diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 985ca1c95..f416f171c 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -13,7 +13,7 @@ class TextEditorPresenter minimumReflowInterval: 200 constructor: (params) -> - {@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft, @scrollColumn, @scrollRow, @boundingClientRect, @windowWidth, @windowHeight, @gutterWidth} = 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 @@ -225,9 +225,9 @@ class TextEditorPresenter observeConfig: -> configParams = {scope: @model.getRootScopeDescriptor()} - @scrollPastEnd = atom.config.get('editor.scrollPastEnd', configParams) - @showLineNumbers = atom.config.get('editor.showLineNumbers', configParams) - @showIndentGuide = atom.config.get('editor.showIndentGuide', configParams) + @scrollPastEnd = @config.get('editor.scrollPastEnd', configParams) + @showLineNumbers = @config.get('editor.showLineNumbers', configParams) + @showIndentGuide = @config.get('editor.showIndentGuide', configParams) if @configDisposables? @configDisposables?.dispose() @@ -236,19 +236,19 @@ class TextEditorPresenter @configDisposables = new CompositeDisposable @disposables.add(@configDisposables) - @configDisposables.add atom.config.onDidChange 'editor.showIndentGuide', configParams, ({newValue}) => + @configDisposables.add @config.onDidChange 'editor.showIndentGuide', configParams, ({newValue}) => @showIndentGuide = newValue @shouldUpdateContentState = true @emitDidUpdateState() - @configDisposables.add atom.config.onDidChange 'editor.scrollPastEnd', configParams, ({newValue}) => + @configDisposables.add @config.onDidChange 'editor.scrollPastEnd', configParams, ({newValue}) => @scrollPastEnd = newValue @shouldUpdateVerticalScrollState = true @shouldUpdateScrollbarsState = true @updateScrollHeight() @emitDidUpdateState() - @configDisposables.add atom.config.onDidChange 'editor.showLineNumbers', configParams, ({newValue}) => + @configDisposables.add @config.onDidChange 'editor.showLineNumbers', configParams, ({newValue}) => @showLineNumbers = newValue @shouldUpdateLineNumberGutterState = true @shouldUpdateGutterOrderState = true diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 2610c5970..4c6a0f514 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -54,10 +54,7 @@ GutterContainer = require './gutter-container' # soft wraps and folds to ensure your code interacts with them correctly. module.exports = class TextEditor extends Model - atom.deserializers.add(this) - callDisplayBufferCreatedHook: false - registerEditor: false buffer: null languageMode: null cursors: null @@ -67,9 +64,9 @@ class TextEditor extends Model selectionFlashDuration: 500 gutterContainer: null - @deserialize: (state) -> + @deserialize: (state, atomEnvironment) -> try - displayBuffer = DisplayBuffer.deserialize(state.displayBuffer) + displayBuffer = DisplayBuffer.deserialize(state.displayBuffer, atomEnvironment) catch error if error.syscall is 'read' return # Error reading the file, don't deserialize an editor for it @@ -77,19 +74,46 @@ class TextEditor extends Model throw error state.displayBuffer = displayBuffer - state.registerEditor = true + state.config = atomEnvironment.config + state.notificationManager = atomEnvironment.notifications + state.packageManager = atomEnvironment.packages + state.clipboard = atomEnvironment.clipboard + state.viewRegistry = atomEnvironment.views + state.grammarRegistry = atomEnvironment.grammars + state.project = atomEnvironment.project + state.assert = atomEnvironment.assert.bind(atomEnvironment) + state.applicationDelegate = atomEnvironment.applicationDelegate new this(state) - constructor: ({@softTabs, @scrollRow, @scrollColumn, initialLine, initialColumn, tabLength, softWrapped, @displayBuffer, buffer, registerEditor, suppressCursorCreation, @mini, @placeholderText, lineNumberGutterVisible, largeFileMode}={}) -> + constructor: (params={}) -> super + { + @softTabs, @scrollRow, @scrollColumn, initialLine, initialColumn, tabLength, + softWrapped, @displayBuffer, buffer, suppressCursorCreation, @mini, @placeholderText, + lineNumberGutterVisible, largeFileMode, @config, @notificationManager, @packageManager, + @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate + } = params + + throw new Error("Must pass a config parameter when constructing TextEditors") unless @config? + throw new Error("Must pass a notificationManager parameter when constructing TextEditors") unless @notificationManager? + throw new Error("Must pass a packageManager parameter when constructing TextEditors") unless @packageManager? + throw new Error("Must pass a clipboard parameter when constructing TextEditors") unless @clipboard? + throw new Error("Must pass a viewRegistry parameter when constructing TextEditors") unless @viewRegistry? + throw new Error("Must pass a grammarRegistry parameter when constructing TextEditors") unless @grammarRegistry? + 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? + @emitter = new Emitter @disposables = new CompositeDisposable @cursors = [] @selections = [] buffer ?= new TextBuffer - @displayBuffer ?= new DisplayBuffer({buffer, tabLength, softWrapped, ignoreInvisibles: @mini, largeFileMode}) + @displayBuffer ?= new DisplayBuffer({ + buffer, tabLength, softWrapped, ignoreInvisibles: @mini, largeFileMode, + @config, @assert, @grammarRegistry, @packageManager + }) @buffer = @displayBuffer.buffer for marker in @findMarkers(@getSelectionMarkerAttributes()) @@ -105,9 +129,9 @@ class TextEditor extends Model initialColumn = Math.max(parseInt(initialColumn) or 0, 0) @addCursorAtBufferPosition([initialLine, initialColumn]) - @languageMode = new LanguageMode(this) + @languageMode = new LanguageMode(this, @config) - @setEncoding(atom.config.get('core.fileEncoding', scope: @getRootScopeDescriptor())) + @setEncoding(@config.get('core.fileEncoding', scope: @getRootScopeDescriptor())) @gutterContainer = new GutterContainer(this) @lineNumberGutter = @gutterContainer.addGutter @@ -115,8 +139,6 @@ class TextEditor extends Model priority: 0 visible: lineNumberGutterVisible - atom.workspace?.editorAdded(this) if registerEditor - serialize: -> deserializer: 'TextEditor' id: @id @@ -128,8 +150,8 @@ class TextEditor extends Model subscribeToBuffer: -> @buffer.retain() @disposables.add @buffer.onDidChangePath => - unless atom.project.getPaths().length > 0 - atom.project.setPaths([path.dirname(@getPath())]) + unless @project.getPaths().length > 0 + @project.setPaths([path.dirname(@getPath())]) @emitter.emit 'did-change-title', @getTitle() @emitter.emit 'did-change-path', @getPath() @disposables.add @buffer.onDidChangeEncoding => @@ -148,7 +170,7 @@ class TextEditor extends Model subscribeToTabTypeConfig: -> @tabTypeSubscription?.dispose() - @tabTypeSubscription = atom.config.observe 'editor.tabType', scope: @getRootScopeDescriptor(), => + @tabTypeSubscription = @config.observe 'editor.tabType', scope: @getRootScopeDescriptor(), => @softTabs = @shouldUseSoftTabs(defaultValue: @softTabs) destroyed: -> @@ -429,12 +451,12 @@ class TextEditor extends Model onDidChangeScrollTop: (callback) -> Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollTop instead.") - atom.views.getView(this).onDidChangeScrollTop(callback) + @viewRegistry.getView(this).onDidChangeScrollTop(callback) onDidChangeScrollLeft: (callback) -> Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollLeft instead.") - atom.views.getView(this).onDidChangeScrollLeft(callback) + @viewRegistry.getView(this).onDidChangeScrollLeft(callback) onDidRequestAutoscroll: (callback) -> @displayBuffer.onDidRequestAutoscroll(callback) @@ -456,7 +478,11 @@ class TextEditor extends Model copy: -> displayBuffer = @displayBuffer.copy() softTabs = @getSoftTabs() - newEditor = new TextEditor({@buffer, displayBuffer, @tabLength, softTabs, suppressCursorCreation: true, registerEditor: true}) + newEditor = new TextEditor({ + @buffer, displayBuffer, @tabLength, softTabs, suppressCursorCreation: true, + @config, @notificationManager, @packageManager, @clipboard, @viewRegistry, + @grammarRegistry, @project, @assert, @applicationDelegate + }) for marker in @findMarkers(editorId: @id) marker.copy(editorId: newEditor.id, preserveFolds: true) newEditor @@ -557,7 +583,7 @@ class TextEditor extends Model getLongTitle: -> if sessionPath = @getPath() fileName = path.basename(sessionPath) - directory = atom.project.relativize(path.dirname(sessionPath)) + directory = @project.relativize(path.dirname(sessionPath)) directory = if directory.length > 0 then directory else path.basename(path.dirname(sessionPath)) "#{fileName} - #{directory}" else @@ -585,7 +611,7 @@ class TextEditor extends Model # Copies the current file path to the native clipboard. copyPathToClipboard: -> if filePath = @getPath() - atom.clipboard.write(filePath) + @clipboard.write(filePath) ### Section: File Operations @@ -594,14 +620,14 @@ class TextEditor extends Model # Essential: Saves the editor's text buffer. # # See {TextBuffer::save} for more details. - save: -> @buffer.save(backup: atom.config.get('editor.backUpBeforeSaving')) + save: -> @buffer.save(backup: @config.get('editor.backUpBeforeSaving')) # Essential: Saves the editor's text buffer as the given path. # # See {TextBuffer::saveAs} for more details. # # * `filePath` A {String} path. - saveAs: (filePath) -> @buffer.saveAs(filePath, backup: atom.config.get('editor.backUpBeforeSaving')) + saveAs: (filePath) -> @buffer.saveAs(filePath, backup: @config.get('editor.backUpBeforeSaving')) # Determine whether the user should be prompted to save before closing # this editor. @@ -617,9 +643,20 @@ class TextEditor extends Model checkoutHeadRevision: -> if filePath = this.getPath() - atom.project.repositoryForDirectory(new Directory(path.dirname(filePath))) - .then (repository) => - repository?.checkoutHeadForEditor(this) + checkoutHead = => + @project.repositoryForDirectory(new Directory(path.dirname(filePath))) + .then (repository) => + repository?.checkoutHeadForEditor(this) + + if @config.get('editor.confirmCheckoutHeadRevision') + @applicationDelegate.confirm + message: 'Confirm Checkout HEAD Revision' + detailedMessage: "Are you sure you want to discard all changes to \"#{path.basename(filePath)}\" since the last Git commit?" + buttons: + OK: checkoutHead + Cancel: null + else + checkoutHead() else Promise.resolve(false) @@ -748,7 +785,7 @@ class TextEditor extends Model return false unless @emitWillInsertTextEvent(text) groupingInterval = if options.groupUndo - atom.config.get('editor.undoGroupingInterval') + @config.get('editor.undoGroupingInterval') else 0 @@ -1742,7 +1779,7 @@ class TextEditor extends Model # Add a cursor based on the given {Marker}. addCursor: (marker) -> - cursor = new Cursor(editor: this, marker: marker) + cursor = new Cursor(editor: this, marker: marker, config: @config) @cursors.push(cursor) @decorateMarker(marker, type: 'line-number', class: 'cursor-line') @decorateMarker(marker, type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true) @@ -2225,7 +2262,7 @@ class TextEditor extends Model unless marker.getProperties().preserveFolds @destroyFoldsContainingBufferRange(marker.getBufferRange()) cursor = @addCursor(marker) - selection = new Selection(_.extend({editor: this, marker, cursor}, options)) + selection = new Selection(_.extend({editor: this, marker, cursor, @clipboard}, options)) @selections.push(selection) selectionBufferRange = selection.getBufferRange() @mergeIntersectingSelections(preserveFolds: marker.getProperties().preserveFolds) @@ -2382,10 +2419,10 @@ class TextEditor extends Model # # Returns a {Boolean} shouldUseSoftTabs: ({defaultValue}) -> - tabType = atom.config.get('editor.tabType', scope: @getRootScopeDescriptor()) + tabType = @config.get('editor.tabType', scope: @getRootScopeDescriptor()) switch tabType when 'auto' - @usesSoftTabs() ? defaultValue ? atom.config.get('editor.softTabs') ? true + @usesSoftTabs() ? defaultValue ? @config.get('editor.softTabs') ? true when 'hard' false when 'soft' @@ -2561,7 +2598,7 @@ class TextEditor extends Model list = list.map (item) -> "* #{item}" content = "Scopes at Cursor\n#{list.join('\n')}" - atom.notifications.addInfo(content, dismissable: true) + @notificationManager.addInfo(content, dismissable: true) # {Delegates to: DisplayBuffer.tokenForBufferPosition} tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition) @@ -2613,7 +2650,7 @@ class TextEditor extends Model # # * `options` (optional) See {Selection::insertText}. pasteText: (options={}) -> - {text: clipboardText, metadata} = atom.clipboard.readWithMetadata() + {text: clipboardText, metadata} = @clipboard.readWithMetadata() return false unless @emitWillInsertTextEvent(clipboardText) metadata ?= {} @@ -2866,24 +2903,24 @@ class TextEditor extends Model scrollToTop: -> Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.") - atom.views.getView(this).scrollToTop() + @viewRegistry.getView(this).scrollToTop() scrollToBottom: -> Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.") - atom.views.getView(this).scrollToBottom() + @viewRegistry.getView(this).scrollToBottom() scrollToScreenRange: (screenRange, options) -> @displayBuffer.scrollToScreenRange(screenRange, options) getHorizontalScrollbarHeight: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getHorizontalScrollbarHeight instead.") - atom.views.getView(this).getHorizontalScrollbarHeight() + @viewRegistry.getView(this).getHorizontalScrollbarHeight() getVerticalScrollbarWidth: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getVerticalScrollbarWidth instead.") - atom.views.getView(this).getVerticalScrollbarWidth() + @viewRegistry.getView(this).getVerticalScrollbarWidth() pageUp: -> @moveUp(@getRowsPerPage()) @@ -2908,10 +2945,10 @@ class TextEditor extends Model ### shouldAutoIndent: -> - atom.config.get("editor.autoIndent", scope: @getRootScopeDescriptor()) + @config.get("editor.autoIndent", scope: @getRootScopeDescriptor()) shouldAutoIndentOnPaste: -> - atom.config.get("editor.autoIndentOnPaste", scope: @getRootScopeDescriptor()) + @config.get("editor.autoIndentOnPaste", scope: @getRootScopeDescriptor()) ### Section: Event Handlers @@ -2950,19 +2987,19 @@ class TextEditor extends Model getFirstVisibleScreenRow: -> deprecate("This is now a view method. Call TextEditorElement::getFirstVisibleScreenRow instead.") - atom.views.getView(this).getVisibleRowRange()[0] + @viewRegistry.getView(this).getVisibleRowRange()[0] getLastVisibleScreenRow: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getLastVisibleScreenRow instead.") - atom.views.getView(this).getVisibleRowRange()[1] + @viewRegistry.getView(this).getVisibleRowRange()[1] pixelPositionForBufferPosition: (bufferPosition) -> Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead") - atom.views.getView(this).pixelPositionForBufferPosition(bufferPosition) + @viewRegistry.getView(this).pixelPositionForBufferPosition(bufferPosition) pixelPositionForScreenPosition: (screenPosition) -> Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead") - atom.views.getView(this).pixelPositionForScreenPosition(screenPosition) + @viewRegistry.getView(this).pixelPositionForScreenPosition(screenPosition) getSelectionMarkerAttributes: -> {type: 'selection', editorId: @id, invalidate: 'never', maintainHistory: true} @@ -2991,7 +3028,7 @@ class TextEditor extends Model @displayBuffer.setHeight(height) else Grim.deprecate("This is now a view method. Call TextEditorElement::setHeight instead.") - atom.views.getView(this).setHeight(height) + @viewRegistry.getView(this).setHeight(height) getHeight: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getHeight instead.") @@ -3004,7 +3041,7 @@ class TextEditor extends Model @displayBuffer.setWidth(width) else Grim.deprecate("This is now a view method. Call TextEditorElement::setWidth instead.") - atom.views.getView(this).setWidth(width) + @viewRegistry.getView(this).setWidth(width) getWidth: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getWidth instead.") @@ -3019,77 +3056,77 @@ class TextEditor extends Model getScrollTop: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollTop instead.") - atom.views.getView(this).getScrollTop() + @viewRegistry.getView(this).getScrollTop() setScrollTop: (scrollTop) -> Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollTop instead.") - atom.views.getView(this).setScrollTop(scrollTop) + @viewRegistry.getView(this).setScrollTop(scrollTop) getScrollBottom: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollBottom instead.") - atom.views.getView(this).getScrollBottom() + @viewRegistry.getView(this).getScrollBottom() setScrollBottom: (scrollBottom) -> Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollBottom instead.") - atom.views.getView(this).setScrollBottom(scrollBottom) + @viewRegistry.getView(this).setScrollBottom(scrollBottom) getScrollLeft: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollLeft instead.") - atom.views.getView(this).getScrollLeft() + @viewRegistry.getView(this).getScrollLeft() setScrollLeft: (scrollLeft) -> Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollLeft instead.") - atom.views.getView(this).setScrollLeft(scrollLeft) + @viewRegistry.getView(this).setScrollLeft(scrollLeft) getScrollRight: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollRight instead.") - atom.views.getView(this).getScrollRight() + @viewRegistry.getView(this).getScrollRight() setScrollRight: (scrollRight) -> Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollRight instead.") - atom.views.getView(this).setScrollRight(scrollRight) + @viewRegistry.getView(this).setScrollRight(scrollRight) getScrollHeight: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollHeight instead.") - atom.views.getView(this).getScrollHeight() + @viewRegistry.getView(this).getScrollHeight() getScrollWidth: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollWidth instead.") - atom.views.getView(this).getScrollWidth() + @viewRegistry.getView(this).getScrollWidth() getVisibleRowRange: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getVisibleRowRange instead.") - atom.views.getView(this).getVisibleRowRange() + @viewRegistry.getView(this).getVisibleRowRange() intersectsVisibleRowRange: (startRow, endRow) -> Grim.deprecate("This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.") - atom.views.getView(this).intersectsVisibleRowRange(startRow, endRow) + @viewRegistry.getView(this).intersectsVisibleRowRange(startRow, endRow) selectionIntersectsVisibleRowRange: (selection) -> Grim.deprecate("This is now a view method. Call TextEditorElement::selectionIntersectsVisibleRowRange instead.") - atom.views.getView(this).selectionIntersectsVisibleRowRange(selection) + @viewRegistry.getView(this).selectionIntersectsVisibleRowRange(selection) screenPositionForPixelPosition: (pixelPosition) -> Grim.deprecate("This is now a view method. Call TextEditorElement::screenPositionForPixelPosition instead.") - atom.views.getView(this).screenPositionForPixelPosition(pixelPosition) + @viewRegistry.getView(this).screenPositionForPixelPosition(pixelPosition) pixelRectForScreenRange: (screenRange) -> Grim.deprecate("This is now a view method. Call TextEditorElement::pixelRectForScreenRange instead.") - atom.views.getView(this).pixelRectForScreenRange(screenRange) + @viewRegistry.getView(this).pixelRectForScreenRange(screenRange) ### Section: Utility diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index 0d329cea7..649478841 100644 --- a/src/theme-manager.coffee +++ b/src/theme-manager.coffee @@ -9,12 +9,14 @@ fs = require 'fs-plus' # An instance of this class is always available as the `atom.themes` global. module.exports = class ThemeManager - constructor: ({@packageManager, @resourcePath, @configDirPath, @safeMode}) -> + constructor: ({@packageManager, @resourcePath, @configDirPath, @safeMode, @config, @styleManager, @notificationManager, @viewRegistry}) -> @emitter = new Emitter @styleSheetDisposablesBySourcePath = {} @lessCache = null @initialLoadComplete = false @packageManager.registerPackageActivator(this, ['theme']) + @packageManager.onDidActivateInitialPackages => + @onDidChangeActiveThemes => @packageManager.reloadActivePackageStyleSheets() ### Section: Event Subscription @@ -66,21 +68,21 @@ class ThemeManager ### warnForNonExistentThemes: -> - themeNames = atom.config.get('core.themes') ? [] + themeNames = @config.get('core.themes') ? [] themeNames = [themeNames] unless _.isArray(themeNames) for themeName in themeNames - unless themeName and typeof themeName is 'string' and atom.packages.resolvePackagePath(themeName) + unless themeName and typeof themeName is 'string' and @packageManager.resolvePackagePath(themeName) console.warn("Enabled theme '#{themeName}' is not installed.") # Public: Get the enabled theme names from the config. # # Returns an array of theme names in the order that they should be activated. getEnabledThemeNames: -> - themeNames = atom.config.get('core.themes') ? [] + themeNames = @config.get('core.themes') ? [] themeNames = [themeNames] unless _.isArray(themeNames) - themeNames = themeNames.filter (themeName) -> + themeNames = themeNames.filter (themeName) => if themeName and typeof themeName is 'string' - return true if atom.packages.resolvePackagePath(themeName) + return true if @packageManager.resolvePackagePath(themeName) false # Use a built-in syntax and UI theme any time the configured themes are not @@ -139,7 +141,7 @@ class ThemeManager loadUserStylesheet: -> @unwatchUserStylesheet() - userStylesheetPath = atom.styles.getUserStyleSheetPath() + userStylesheetPath = @styleManager.getUserStyleSheetPath() return unless fs.isFileSync(userStylesheetPath) try @@ -158,14 +160,14 @@ class ThemeManager [this document][watches] for more info. [watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path """ - atom.notifications.addError(message, dismissable: true) + @notificationManager.addError(message, dismissable: true) try userStylesheetContents = @loadStylesheet(userStylesheetPath, true) catch return - @userStyleSheetDisposable = atom.styles.addStyleSheet(userStylesheetContents, sourcePath: userStylesheetPath, priority: 2) + @userStyleSheetDisposable = @styleManager.addStyleSheet(userStylesheetContents, sourcePath: userStylesheetPath, priority: 2) loadBaseStylesheets: -> @requireStylesheet('../static/bootstrap') @@ -221,22 +223,22 @@ class ThemeManager message = "Error loading Less stylesheet: `#{lessStylesheetPath}`" detail = error.message - atom.notifications.addError(message, {detail, dismissable: true}) + @notificationManager.addError(message, {detail, dismissable: true}) throw error removeStylesheet: (stylesheetPath) -> @styleSheetDisposablesBySourcePath[stylesheetPath]?.dispose() applyStylesheet: (path, text) -> - @styleSheetDisposablesBySourcePath[path] = atom.styles.addStyleSheet(text, sourcePath: path) + @styleSheetDisposablesBySourcePath[path] = @styleManager.addStyleSheet(text, sourcePath: path) stringToId: (string) -> string.replace(/\\/g, '/') activateThemes: -> new Promise (resolve) => - # atom.config.observe runs the callback once, then on subsequent changes. - atom.config.observe 'core.themes', => + # @config.observe runs the callback once, then on subsequent changes. + @config.observe 'core.themes', => @deactivateThemes() @warnForNonExistentThemes() @@ -268,13 +270,13 @@ class ThemeManager isInitialLoadComplete: -> @initialLoadComplete addActiveThemeClasses: -> - if workspaceElement = atom.views.getView(atom.workspace) + if workspaceElement = @viewRegistry.getView(@workspace) for pack in @getActiveThemes() workspaceElement.classList.add("theme-#{pack.name}") return removeActiveThemeClasses: -> - workspaceElement = atom.views.getView(atom.workspace) + workspaceElement = @viewRegistry.getView(@workspace) for pack in @getActiveThemes() workspaceElement.classList.remove("theme-#{pack.name}") return diff --git a/src/theme-package.coffee b/src/theme-package.coffee index 6d0a88052..084728869 100644 --- a/src/theme-package.coffee +++ b/src/theme-package.coffee @@ -7,10 +7,10 @@ class ThemePackage extends Package getStyleSheetPriority: -> 1 enable: -> - atom.config.unshiftAtKeyPath('core.themes', @name) + @config.unshiftAtKeyPath('core.themes', @name) disable: -> - atom.config.removeAtKeyPath('core.themes', @name) + @config.removeAtKeyPath('core.themes', @name) load: -> @loadTime = 0 diff --git a/src/token-iterator.coffee b/src/token-iterator.coffee index 120f62fe4..92529d0e9 100644 --- a/src/token-iterator.coffee +++ b/src/token-iterator.coffee @@ -3,7 +3,7 @@ module.exports = class TokenIterator - constructor: (line) -> + constructor: ({@grammarRegistry}, line) -> @reset(line) if line? reset: (@line) -> @@ -12,7 +12,7 @@ class TokenIterator @bufferEnd = @bufferStart @screenStart = 0 @screenEnd = 0 - @scopes = @line.openScopes.map (id) -> atom.grammars.scopeForId(id) + @scopes = @line.openScopes.map (id) => @grammarRegistry.scopeForId(id) @scopeStarts = @scopes.slice() @scopeEnds = [] this @@ -32,7 +32,7 @@ class TokenIterator while @index < tags.length tag = tags[@index] if tag < 0 - scope = atom.grammars.scopeForId(tag) + scope = @grammarRegistry.scopeForId(tag) if tag % 2 is 0 if @scopeStarts[@scopeStarts.length - 1] is scope @scopeStarts.pop() diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 8721b0547..2df29a31c 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -21,17 +21,26 @@ class TokenizedBuffer extends Model configSettings: null changeCount: 0 - @deserialize: (state) -> - state.buffer = atom.project.bufferForPathSync(state.bufferPath) + @deserialize: (state, atomEnvironment) -> + state.buffer = atomEnvironment.project.bufferForPathSync(state.bufferPath) + state.config = atomEnvironment.config + state.grammarRegistry = atomEnvironment.grammars + state.packageManager = atomEnvironment.packages + state.assert = atomEnvironment.assert new this(state) - constructor: ({@buffer, @tabLength, @ignoreInvisibles, @largeFileMode}) -> + constructor: (params) -> + { + @buffer, @tabLength, @ignoreInvisibles, @largeFileMode, @config, + @grammarRegistry, @packageManager, @assert + } = params + @emitter = new Emitter @disposables = new CompositeDisposable - @tokenIterator = new TokenIterator + @tokenIterator = new TokenIterator({@grammarRegistry}) - @disposables.add atom.grammars.onDidAddGrammar(@grammarAddedOrUpdated) - @disposables.add atom.grammars.onDidUpdateGrammar(@grammarAddedOrUpdated) + @disposables.add @grammarRegistry.onDidAddGrammar(@grammarAddedOrUpdated) + @disposables.add @grammarRegistry.onDidUpdateGrammar(@grammarAddedOrUpdated) @disposables.add @buffer.preemptDidChange (e) => @handleBufferChange(e) @disposables.add @buffer.onDidChangePath (@bufferPath) => @reloadGrammar() @@ -65,7 +74,7 @@ class TokenizedBuffer extends Model if grammar.injectionSelector? @retokenizeLines() if @hasTokenForSelector(grammar.injectionSelector) else - newScore = atom.grammars.getGrammarScore(grammar, @buffer.getPath(), @getGrammarSelectionContent()) + newScore = @grammarRegistry.getGrammarScore(grammar, @buffer.getPath(), @getGrammarSelectionContent()) @setGrammar(grammar, newScore) if newScore > @currentGrammarScore setGrammar: (grammar, score) -> @@ -73,7 +82,7 @@ class TokenizedBuffer extends Model @grammar = grammar @rootScopeDescriptor = new ScopeDescriptor(scopes: [@grammar.scopeName]) - @currentGrammarScore = score ? atom.grammars.getGrammarScore(grammar, @buffer.getPath(), @getGrammarSelectionContent()) + @currentGrammarScore = score ? @grammarRegistry.getGrammarScore(grammar, @buffer.getPath(), @getGrammarSelectionContent()) @grammarUpdateDisposable?.dispose() @grammarUpdateDisposable = @grammar.onDidUpdate => @retokenizeLines() @@ -81,33 +90,33 @@ class TokenizedBuffer extends Model scopeOptions = {scope: @rootScopeDescriptor} @configSettings = - tabLength: atom.config.get('editor.tabLength', scopeOptions) - invisibles: atom.config.get('editor.invisibles', scopeOptions) - showInvisibles: atom.config.get('editor.showInvisibles', scopeOptions) + tabLength: @config.get('editor.tabLength', scopeOptions) + invisibles: @config.get('editor.invisibles', scopeOptions) + showInvisibles: @config.get('editor.showInvisibles', scopeOptions) if @configSubscriptions? @configSubscriptions.dispose() @disposables.remove(@configSubscriptions) @configSubscriptions = new CompositeDisposable - @configSubscriptions.add atom.config.onDidChange 'editor.tabLength', scopeOptions, ({newValue}) => + @configSubscriptions.add @config.onDidChange 'editor.tabLength', scopeOptions, ({newValue}) => @configSettings.tabLength = newValue @retokenizeLines() ['invisibles', 'showInvisibles'].forEach (key) => - @configSubscriptions.add atom.config.onDidChange "editor.#{key}", scopeOptions, ({newValue}) => + @configSubscriptions.add @config.onDidChange "editor.#{key}", scopeOptions, ({newValue}) => oldInvisibles = @getInvisiblesToShow() @configSettings[key] = newValue @retokenizeLines() unless _.isEqual(@getInvisiblesToShow(), oldInvisibles) @disposables.add(@configSubscriptions) @retokenizeLines() - atom.packages.triggerActivationHook("#{grammar.packageName}:grammar-used") + @packageManager.triggerActivationHook("#{grammar.packageName}:grammar-used") @emitter.emit 'did-change-grammar', grammar getGrammarSelectionContent: -> @buffer.getTextInRange([[0, 0], [10, 0]]) reloadGrammar: -> - if grammar = atom.grammars.selectGrammar(@buffer.getPath(), @getGrammarSelectionContent()) + if grammar = @grammarRegistry.selectGrammar(@buffer.getPath(), @getGrammarSelectionContent()) @setGrammar(grammar) else throw new Error("No grammar found for path: #{path}") @@ -155,7 +164,7 @@ class TokenizedBuffer extends Model tokenizeNextChunk: -> # Short circuit null grammar which can just use the placeholder tokens - if @grammar is atom.grammars.nullGrammar and @firstInvalidRow()? + if @grammar is @grammarRegistry.nullGrammar and @firstInvalidRow()? @invalidRows = [] @markTokenizationComplete() return @@ -291,7 +300,7 @@ class TokenizedBuffer extends Model # undefined. This should paper over the problem but we want to figure out # what is happening: tokenizedLine = @tokenizedLineForRow(row) - atom.assert tokenizedLine?, "TokenizedLine is undefined", (error) => + @assert tokenizedLine?, "TokenizedLine is undefined", (error) => error.metadata = { row: row rowCount: @tokenizedLines.length @@ -391,7 +400,7 @@ class TokenizedBuffer extends Model loop break if scopes.pop() is matchingStartTag if scopes.length is 0 - atom.assert false, "Encountered an unmatched scope end tag.", (error) => + @assert false, "Encountered an unmatched scope end tag.", (error) => error.metadata = { grammarScopeName: @grammar.scopeName unmatchedEndTag: @grammar.scopeForId(tag) @@ -472,13 +481,13 @@ class TokenizedBuffer extends Model position = Point.fromObject(position) {openScopes, tags} = @tokenizedLineForRow(position.row) - scopes = openScopes.map (tag) -> atom.grammars.scopeForId(tag) + scopes = openScopes.map (tag) => @grammarRegistry.scopeForId(tag) startColumn = 0 for tag, tokenIndex in tags if tag < 0 if tag % 2 is -1 - scopes.push(atom.grammars.scopeForId(tag)) + scopes.push(@grammarRegistry.scopeForId(tag)) else scopes.pop() else @@ -498,7 +507,7 @@ class TokenizedBuffer extends Model if tag % 2 is -1 startScopes.pop() else - startScopes.push(atom.grammars.scopeForId(tag)) + startScopes.push(@grammarRegistry.scopeForId(tag)) else break unless selectorMatchesAnyScope(selector, startScopes) startColumn -= tag @@ -508,7 +517,7 @@ class TokenizedBuffer extends Model tag = tags[endTokenIndex] if tag < 0 if tag % 2 is -1 - endScopes.push(atom.grammars.scopeForId(tag)) + endScopes.push(@grammarRegistry.scopeForId(tag)) else endScopes.pop() else diff --git a/src/tooltip-manager.coffee b/src/tooltip-manager.coffee index 21b8b07c6..247437535 100644 --- a/src/tooltip-manager.coffee +++ b/src/tooltip-manager.coffee @@ -54,6 +54,8 @@ class TooltipManager placement: 'auto top' viewportPadding: 2 + constructor: ({@keymapManager}) -> + # Essential: Add a tooltip to the given element. # # * `target` An `HTMLElement` @@ -81,7 +83,7 @@ class TooltipManager {keyBindingCommand, keyBindingTarget} = options if keyBindingCommand? - bindings = atom.keymaps.findKeyBindings(command: keyBindingCommand, target: keyBindingTarget) + bindings = @keymapManager.findKeyBindings(command: keyBindingCommand, target: keyBindingTarget) keystroke = getKeystroke(bindings) if options.title? and keystroke? options.title += " " + getKeystroke(bindings) diff --git a/src/view-registry.coffee b/src/view-registry.coffee index 6af7bd024..3a46aa87a 100644 --- a/src/view-registry.coffee +++ b/src/view-registry.coffee @@ -49,15 +49,15 @@ class ViewRegistry debouncedPerformDocumentPoll: null minimumPollInterval: 200 - constructor: -> + constructor: (@atomEnvironment) -> + @observer = new MutationObserver(@requestDocumentPoll) + @clear() + + clear: -> @views = new WeakMap @providers = [] - @documentWriters = [] - @documentReaders = [] - @documentPollers = [] - - @observer = new MutationObserver(@requestDocumentPoll) @debouncedPerformDocumentPoll = _.throttle(@performDocumentPoll, @minimumPollInterval).bind(this) + @clearDocumentRequests() # Essential: Add a provider that will be used to construct views in the # workspace's view layer based on model objects in its model layer. @@ -159,7 +159,7 @@ class ViewRegistry else if object?.jquery object[0] else if provider = @findProvider(object) - element = provider.createView?(object) + element = provider.createView?(object, @atomEnvironment) unless element? element = new provider.viewConstructor element.initialize?(object) ? element.setModel?(object) diff --git a/src/window-bootstrap.coffee b/src/window-bootstrap.coffee deleted file mode 100644 index 886ba26dc..000000000 --- a/src/window-bootstrap.coffee +++ /dev/null @@ -1,13 +0,0 @@ -# Like sands through the hourglass, so are the days of our lives. -require './window' - -Atom = require './atom' -window.atom = Atom.loadOrCreate('editor') -atom.initialize() -atom.startEditorWindow() - -# Workaround for focus getting cleared upon window creation -windowFocused = -> - window.removeEventListener('focus', windowFocused) - setTimeout (-> document.querySelector('atom-workspace').focus()), 0 -window.addEventListener('focus', windowFocused) diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index 53c2aa2e2..bff5ee5b7 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -1,45 +1,38 @@ path = require 'path' {Disposable, CompositeDisposable} = require 'event-kit' -ipc = require 'ipc' -shell = require 'shell' fs = require 'fs-plus' listen = require './delegated-listener' -# Handles low-level events related to the window. +# Handles low-level events related to the @window. module.exports = class WindowEventHandler - constructor: -> + constructor: ({@atomEnvironment, @applicationDelegate, @window, @document}) -> @reloadRequested = false @subscriptions = new CompositeDisposable - @on(ipc, 'message', @handleIPCMessage) - @on(ipc, 'command', @handleIPCCommand) - @on(ipc, 'context-command', @handleIPCContextCommand) + @previousOnbeforeunloadHandler = @window.onbeforeunload + @window.onbeforeunload = @handleWindowBeforeunload + @addEventListener(@window, 'focus', @handleWindowFocus) + @addEventListener(@window, 'blur', @handleWindowBlur) - @previousOnbeforeunloadHandler = window.onbeforeunload - window.onbeforeunload = @handleWindowBeforeunload - @addEventListener(window, 'focus', @handleWindowFocus) - @addEventListener(window, 'blur', @handleWindowBlur) - @addEventListener(window, 'unload', @handleWindowUnload) + @addEventListener(@document, 'keydown', @handleDocumentKeydown) + @addEventListener(@document, 'drop', @handleDocumentDrop) + @addEventListener(@document, 'dragover', @handleDocumentDragover) + @addEventListener(@document, 'contextmenu', @handleDocumentContextmenu) + @subscriptions.add listen(@document, 'click', 'a', @handleLinkClick) + @subscriptions.add listen(@document, 'submit', 'form', @handleFormSubmit) - @addEventListener(document, 'keydown', @handleDocumentKeydown) - @addEventListener(document, 'drop', @handleDocumentDrop) - @addEventListener(document, 'dragover', @handleDocumentDragover) - @addEventListener(document, 'contextmenu', @handleDocumentContextmenu) - @subscriptions.add listen(document, 'click', 'a', @handleLinkClick) - @subscriptions.add listen(document, 'submit', 'form', @handleFormSubmit) - - @subscriptions.add atom.commands.add window, + @subscriptions.add @atomEnvironment.commands.add @window, 'window:toggle-full-screen': @handleWindowToggleFullScreen 'window:close': @handleWindowClose 'window:reload': @handleWindowReload 'window:toggle-dev-tools': @handleWindowToggleDevTools if process.platform in ['win32', 'linux'] - @subscriptions.add atom.commands.add window, - 'window:toggle-menu-bar': @handleWindowToggleMenuBar + @subscriptions.add @atomEnvironment.commands.add @window, + '@window:toggle-menu-bar': @handleWindowToggleMenuBar - @subscriptions.add atom.commands.add document, + @subscriptions.add @atomEnvironment.commands.add @document, 'core:focus-next': @handleFocusNext 'core:focus-previous': @handleFocusPrevious @@ -49,9 +42,9 @@ class WindowEventHandler # `.native-key-bindings` class. handleNativeKeybindings: -> bindCommandToAction = (command, action) => - @addEventListener document, command, (event) -> + @addEventListener @document, command, (event) => if event.target.webkitMatchesSelector('.native-key-bindings') - atom.getCurrentWindow().webContents[action]() + @applicationDelegate.getCurrentWindow().webContents[action]() bindCommandToAction('core:copy', 'copy') bindCommandToAction('core:paste', 'paste') @@ -61,7 +54,7 @@ class WindowEventHandler bindCommandToAction('core:cut', 'cut') unsubscribe: -> - window.onbeforeunload = @previousOnbeforeunloadHandler + @window.onbeforeunload = @previousOnbeforeunloadHandler @subscriptions.dispose() on: (target, eventName, handler) -> @@ -74,8 +67,8 @@ class WindowEventHandler target.addEventListener(eventName, handler) @subscriptions.add(new Disposable(-> target.removeEventListener(eventName, handler))) - handleDocumentKeydown: (event) -> - atom.keymaps.handleKeyboardEvent(event) + handleDocumentKeydown: (event) => + @atomEnvironment.keymaps.handleKeyboardEvent(event) event.stopImmediatePropagation() handleDrop: (event) -> @@ -88,14 +81,14 @@ class WindowEventHandler event.dataTransfer.dropEffect = 'none' eachTabIndexedElement: (callback) -> - for element in document.querySelectorAll('[tabindex]') + for element in @document.querySelectorAll('[tabindex]') continue if element.disabled continue unless element.tabIndex >= 0 callback(element, element.tabIndex) return handleFocusNext: => - focusedTabIndex = document.activeElement.tabIndex ? -Infinity + focusedTabIndex = @document.activeElement.tabIndex ? -Infinity nextElement = null nextTabIndex = Infinity @@ -116,7 +109,7 @@ class WindowEventHandler lowestElement.focus() handleFocusPrevious: => - focusedTabIndex = document.activeElement.tabIndex ? Infinity + focusedTabIndex = @document.activeElement.tabIndex ? Infinity previousElement = null previousTabIndex = -Infinity @@ -136,91 +129,61 @@ class WindowEventHandler else if highestElement? highestElement.focus() - handleIPCMessage: (message, detail) -> - switch message - when 'open-locations' - needsProjectPaths = atom.project?.getPaths().length is 0 - - for {pathToOpen, initialLine, initialColumn} in detail - if pathToOpen? and needsProjectPaths - if fs.existsSync(pathToOpen) - atom.project.addPath(pathToOpen) - else if fs.existsSync(path.dirname(pathToOpen)) - atom.project.addPath(path.dirname(pathToOpen)) - else - atom.project.addPath(pathToOpen) - - unless fs.isDirectorySync(pathToOpen) - atom.workspace?.open(pathToOpen, {initialLine, initialColumn}) - return - when 'update-available' - atom.updateAvailable(detail) - - handleIPCCommand: (command, args...) -> - activeElement = document.activeElement - # Use the workspace element view if body has focus - if activeElement is document.body and workspaceElement = atom.views.getView(atom.workspace) - activeElement = workspaceElement - - atom.commands.dispatch(activeElement, command, args[0]) - - handleIPCContextCommand: (command, args...) -> - atom.commands.dispatch(atom.contextMenu.activeElement, command, args) - handleWindowFocus: -> - document.body.classList.remove('is-blurred') + @document.body.classList.remove('is-blurred') - handleWindowBlur: -> - document.body.classList.add('is-blurred') - atom.storeDefaultWindowDimensions() + handleWindowBlur: => + @document.body.classList.add('is-blurred') + @atomEnvironment.storeDefaultWindowDimensions() handleWindowBeforeunload: => - confirmed = atom.workspace?.confirmClose(windowCloseRequested: true) - atom.hide() if confirmed and not @reloadRequested and atom.getCurrentWindow().isWebViewFocused() + confirmed = @atomEnvironment.workspace?.confirmClose(windowCloseRequested: true) + if confirmed and not @reloadRequested and not @atomEnvironment.inSpecMode() and @atomEnvironment.getCurrentWindow().isWebViewFocused() + @atomEnvironment.hide() @reloadRequested = false - atom.storeDefaultWindowDimensions() - atom.storeWindowDimensions() + @atomEnvironment.storeDefaultWindowDimensions() + @atomEnvironment.storeWindowDimensions() if confirmed - atom.unloadEditorWindow() + @atomEnvironment.unloadEditorWindow() else - ipc.send('cancel-window-close') + @applicationDelegate.didCancelWindowUnload() confirmed - handleWindowUnload: -> - atom.removeEditorWindow() + handleWindowUnload: => + @atomEnvironment.destroy() - handleWindowToggleFullScreen: -> - atom.toggleFullScreen() + handleWindowToggleFullScreen: => + @atomEnvironment.toggleFullScreen() - handleWindowClose: -> - atom.close() + handleWindowClose: => + @atomEnvironment.close() - handleWindowReload: -> + handleWindowReload: => @reloadRequested = true - atom.reload() + @atomEnvironment.reload() - handleWindowToggleDevTools: -> - atom.toggleDevTools() + handleWindowToggleDevTools: => + @atomEnvironment.toggleDevTools() - handleWindowToggleMenuBar: -> - atom.config.set('core.autoHideMenuBar', not atom.config.get('core.autoHideMenuBar')) + handleWindowToggleMenuBar: => + @atomEnvironment.config.set('core.autoHideMenuBar', not @atomEnvironment.config.get('core.autoHideMenuBar')) - if atom.config.get('core.autoHideMenuBar') + if @atomEnvironment.config.get('core.autoHideMenuBar') detail = "To toggle, press the Alt key or execute the window:toggle-menu-bar command" - atom.notifications.addInfo('Menu bar hidden', {detail}) + @atomEnvironment.notifications.addInfo('Menu bar hidden', {detail}) handleLinkClick: (event) -> event.preventDefault() - location = event.currentTarget?.getAttribute('href') - if location and location[0] isnt '#' and /^https?:\/\//.test(location) - shell.openExternal(location) + uri = event.currentTarget?.getAttribute('href') + if uri and uri[0] isnt '#' and /^https?:\/\//.test(uri) + @applicationDelegate.openExternal(uri) handleFormSubmit: (event) -> # Prevent form submits from changing the current window's URL event.preventDefault() - handleDocumentContextmenu: (event) -> + handleDocumentContextmenu: (event) => event.preventDefault() - atom.contextMenu.showForEvent(event) + @atomEnvironment.contextMenu.showForEvent(event) diff --git a/src/window-load-settings-helpers.coffee b/src/window-load-settings-helpers.coffee new file mode 100644 index 000000000..59ee2f382 --- /dev/null +++ b/src/window-load-settings-helpers.coffee @@ -0,0 +1,20 @@ +remote = require 'remote' +_ = require 'underscore-plus' + +windowLoadSettings = null + +exports.getWindowLoadSettings = -> + windowLoadSettings ?= JSON.parse(window.decodeURIComponent(window.location.hash.substr(1))) + clone = _.deepClone(windowLoadSettings) + + # The windowLoadSettings.windowState could be large, request it only when needed. + clone.__defineGetter__ 'windowState', -> + remote.getCurrentWindow().loadSettings.windowState + clone.__defineSetter__ 'windowState', (value) -> + remote.getCurrentWindow().loadSettings.windowState = value + + clone + +exports.setWindowLoadSettings = (settings) -> + windowLoadSettings = settings + location.hash = encodeURIComponent(JSON.stringify(settings)) diff --git a/src/workspace-element.coffee b/src/workspace-element.coffee index 378969481..fb4b19dc3 100644 --- a/src/workspace-element.coffee +++ b/src/workspace-element.coffee @@ -8,18 +8,11 @@ module.exports = class WorkspaceElement extends HTMLElement globalTextEditorStyleSheet: null - createdCallback: -> - @subscriptions = new CompositeDisposable - @initializeContent() - @observeScrollbarStyle() - @observeTextEditorFontConfig() - attachedCallback: -> @focus() detachedCallback: -> @subscriptions.dispose() - @model.destroy() initializeContent: -> @classList.add 'workspace' @@ -46,31 +39,42 @@ class WorkspaceElement extends HTMLElement observeTextEditorFontConfig: -> @updateGlobalTextEditorStyleSheet() - @subscriptions.add atom.config.onDidChange 'editor.fontSize', @updateGlobalTextEditorStyleSheet.bind(this) - @subscriptions.add atom.config.onDidChange 'editor.fontFamily', @updateGlobalTextEditorStyleSheet.bind(this) - @subscriptions.add atom.config.onDidChange 'editor.lineHeight', @updateGlobalTextEditorStyleSheet.bind(this) + @subscriptions.add @config.onDidChange 'editor.fontSize', @updateGlobalTextEditorStyleSheet.bind(this) + @subscriptions.add @config.onDidChange 'editor.fontFamily', @updateGlobalTextEditorStyleSheet.bind(this) + @subscriptions.add @config.onDidChange 'editor.lineHeight', @updateGlobalTextEditorStyleSheet.bind(this) updateGlobalTextEditorStyleSheet: -> styleSheetSource = """ atom-text-editor { - font-size: #{atom.config.get('editor.fontSize')}px; - font-family: #{atom.config.get('editor.fontFamily')}; - line-height: #{atom.config.get('editor.lineHeight')}; + font-size: #{@config.get('editor.fontSize')}px; + font-family: #{@config.get('editor.fontFamily')}; + line-height: #{@config.get('editor.lineHeight')}; } """ - atom.styles.addStyleSheet(styleSheetSource, sourcePath: 'global-text-editor-styles') + @styles.addStyleSheet(styleSheetSource, sourcePath: 'global-text-editor-styles') - initialize: (@model) -> - @paneContainer = atom.views.getView(@model.paneContainer) + initialize: (@model, {@views, @workspace, @project, @config, @styles}) -> + throw new Error("Must pass a views parameter when initializing WorskpaceElements") unless @views? + throw new Error("Must pass a workspace parameter when initializing WorskpaceElements") unless @workspace? + throw new Error("Must pass a project parameter when initializing WorskpaceElements") unless @project? + throw new Error("Must pass a config parameter when initializing WorskpaceElements") unless @config? + throw new Error("Must pass a styles parameter when initializing WorskpaceElements") unless @styles? + + @subscriptions = new CompositeDisposable + @initializeContent() + @observeScrollbarStyle() + @observeTextEditorFontConfig() + + @paneContainer = @views.getView(@model.paneContainer) @verticalAxis.appendChild(@paneContainer) @addEventListener 'focus', @handleFocus.bind(this) @panelContainers = - top: atom.views.getView(@model.panelContainers.top) - left: atom.views.getView(@model.panelContainers.left) - right: atom.views.getView(@model.panelContainers.right) - bottom: atom.views.getView(@model.panelContainers.bottom) - modal: atom.views.getView(@model.panelContainers.modal) + top: @views.getView(@model.panelContainers.top) + left: @views.getView(@model.panelContainers.left) + right: @views.getView(@model.panelContainers.right) + bottom: @views.getView(@model.panelContainers.bottom) + modal: @views.getView(@model.panelContainers.modal) @horizontalAxis.insertBefore(@panelContainers.left, @verticalAxis) @horizontalAxis.appendChild(@panelContainers.right) @@ -96,59 +100,10 @@ class WorkspaceElement extends HTMLElement focusPaneViewOnRight: -> @paneContainer.focusPaneViewOnRight() runPackageSpecs: -> - if activePath = atom.workspace.getActivePaneItem()?.getPath?() - [projectPath] = atom.project.relativizePath(activePath) + if activePath = @workspace.getActivePaneItem()?.getPath?() + [projectPath] = @project.relativizePath(activePath) else - [projectPath] = atom.project.getPaths() + [projectPath] = @project.getPaths() ipc.send('run-package-specs', path.join(projectPath, 'spec')) if projectPath -atom.commands.add 'atom-workspace', - 'window:increase-font-size': -> @getModel().increaseFontSize() - 'window:decrease-font-size': -> @getModel().decreaseFontSize() - 'window:reset-font-size': -> @getModel().resetFontSize() - 'application:about': -> ipc.send('command', 'application:about') - 'application:run-all-specs': -> ipc.send('command', 'application:run-all-specs') - 'application:show-preferences': -> ipc.send('command', 'application:show-settings') - 'application:show-settings': -> ipc.send('command', 'application:show-settings') - 'application:quit': -> ipc.send('command', 'application:quit') - 'application:hide': -> ipc.send('command', 'application:hide') - 'application:hide-other-applications': -> ipc.send('command', 'application:hide-other-applications') - 'application:install-update': -> ipc.send('command', 'application:install-update') - 'application:unhide-all-applications': -> ipc.send('command', 'application:unhide-all-applications') - 'application:new-window': -> ipc.send('command', 'application:new-window') - 'application:new-file': -> ipc.send('command', 'application:new-file') - 'application:open': -> ipc.send('command', 'application:open') - 'application:open-file': -> ipc.send('command', 'application:open-file') - 'application:open-folder': -> ipc.send('command', 'application:open-folder') - 'application:open-dev': -> ipc.send('command', 'application:open-dev') - 'application:open-safe': -> ipc.send('command', 'application:open-safe') - 'application:add-project-folder': -> atom.addProjectFolder() - 'application:minimize': -> ipc.send('command', 'application:minimize') - 'application:zoom': -> ipc.send('command', 'application:zoom') - 'application:bring-all-windows-to-front': -> ipc.send('command', 'application:bring-all-windows-to-front') - 'application:open-your-config': -> ipc.send('command', 'application:open-your-config') - 'application:open-your-init-script': -> ipc.send('command', 'application:open-your-init-script') - 'application:open-your-keymap': -> ipc.send('command', 'application:open-your-keymap') - 'application:open-your-snippets': -> ipc.send('command', 'application:open-your-snippets') - 'application:open-your-stylesheet': -> ipc.send('command', 'application:open-your-stylesheet') - 'application:open-license': -> @getModel().openLicense() - 'window:run-package-specs': -> @runPackageSpecs() - 'window:focus-next-pane': -> @getModel().activateNextPane() - 'window:focus-previous-pane': -> @getModel().activatePreviousPane() - 'window:focus-pane-above': -> @focusPaneViewAbove() - 'window:focus-pane-below': -> @focusPaneViewBelow() - 'window:focus-pane-on-left': -> @focusPaneViewOnLeft() - 'window:focus-pane-on-right': -> @focusPaneViewOnRight() - 'window:save-all': -> @getModel().saveAll() - 'window:toggle-invisibles': -> atom.config.set("editor.showInvisibles", not atom.config.get("editor.showInvisibles")) - 'window:log-deprecation-warnings': -> Grim.logDeprecations() - 'window:toggle-auto-indent': -> atom.config.set("editor.autoIndent", not atom.config.get("editor.autoIndent")) - 'pane:reopen-closed-item': -> @getModel().reopenItem() - 'core:close': -> @getModel().destroyActivePaneItemOrEmptyPane() - 'core:save': -> @getModel().saveActivePaneItem() - 'core:save-as': -> @getModel().saveActivePaneItemAs() - -if process.platform is 'darwin' - atom.commands.add 'atom-workspace', 'window:install-shell-commands', -> @getModel().installShellCommands() - module.exports = WorkspaceElement = document.registerElement 'atom-workspace', prototype: WorkspaceElement.prototype diff --git a/src/workspace.coffee b/src/workspace.coffee index dbd781bd3..b66445bbf 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -9,10 +9,7 @@ TextEditor = require './text-editor' PaneContainer = require './pane-container' Pane = require './pane' Panel = require './panel' -PanelElement = require './panel-element' PanelContainer = require './panel-container' -PanelContainerElement = require './panel-container-element' -WorkspaceElement = require './workspace-element' Task = require './task' # Essential: Represents the state of the user interface for the entire window. @@ -26,36 +23,24 @@ Task = require './task' # module.exports = class Workspace extends Model - atom.deserializers.add(this) - - @deserialize: (state) -> - return unless state? - - for packageName in state.packagesWithActiveGrammars ? [] - atom.packages.getLoadedPackage(packageName)?.loadGrammarsSync() - - state.paneContainer = PaneContainer.deserialize(state.paneContainer) - new this(state) - constructor: (params) -> super - @paneContainer = params?.paneContainer - @fullScreen = params?.fullScreen ? false - @destroyedItemURIs = params?.destroyedItemURIs ? [] + { + @packageManager, @config, @project, @grammarRegistry, @notificationManager, + @clipboard, @viewRegistry, @grammarRegistry, @applicationDelegate, @assert, + @deserializerManager + } = params @emitter = new Emitter @openers = [] + @destroyedItemURIs = [] - @paneContainer ?= new PaneContainer() + @paneContainer = new PaneContainer({@config, @applicationDelegate, @notificationManager, @deserializerManager}) @paneContainer.onDidDestroyPaneItem(@didDestroyPaneItem) - @directorySearchers = [] @defaultDirectorySearcher = new DefaultDirectorySearcher() - atom.packages.serviceHub.consume( - 'atom.directory-searcher', - '^0.1.0', - (provider) => @directorySearchers.unshift(provider)) + @consumeServices(@packageManager) @panelContainers = top: new PanelContainer({location: 'top'}) @@ -64,69 +49,80 @@ class Workspace extends Model bottom: new PanelContainer({location: 'bottom'}) modal: new PanelContainer({location: 'modal'}) + @subscribeToEvents() + + reset: (@packageManager) -> + @emitter.dispose() + @emitter = new Emitter + + @paneContainer.destroy() + panelContainer.destroy() for panelContainer in @panelContainers + + @paneContainer = new PaneContainer({@config, @applicationDelegate, @notificationManager, @deserializerManager}) + @paneContainer.onDidDestroyPaneItem(@didDestroyPaneItem) + + @panelContainers = + top: new PanelContainer({location: 'top'}) + left: new PanelContainer({location: 'left'}) + right: new PanelContainer({location: 'right'}) + bottom: new PanelContainer({location: 'bottom'}) + modal: new PanelContainer({location: 'modal'}) + + @originalFontSize = null + @openers = [] + @destroyedItemURIs = [] + @consumeServices(@packageManager) + + subscribeToEvents: -> @subscribeToActiveItem() - - @addOpener (filePath) -> - switch filePath - when 'atom://.atom/stylesheet' - atom.project.open(atom.styles.getUserStyleSheetPath()) - when 'atom://.atom/keymap' - atom.project.open(atom.keymaps.getUserKeymapPath()) - when 'atom://.atom/config' - atom.project.open(atom.config.getUserConfigPath()) - when 'atom://.atom/init-script' - atom.project.open(atom.getUserInitScriptPath()) - - atom.views.addViewProvider Workspace, (model) -> - new WorkspaceElement().initialize(model) - - atom.views.addViewProvider PanelContainer, (model) -> - new PanelContainerElement().initialize(model) - - atom.views.addViewProvider Panel, (model) -> - new PanelElement().initialize(model) - @subscribeToFontSize() + consumeServices: ({serviceHub}) -> + @directorySearchers = [] + serviceHub.consume( + 'atom.directory-searcher', + '^0.1.0', + (provider) => @directorySearchers.unshift(provider)) + # Called by the Serializable mixin during serialization. serialize: -> deserializer: 'Workspace' paneContainer: @paneContainer.serialize() - fullScreen: atom.isFullScreen() packagesWithActiveGrammars: @getPackageNamesWithActiveGrammars() + destroyedItemURIs: @destroyedItemURIs.slice() + + deserialize: (state, deserializerManager) -> + for packageName in state.packagesWithActiveGrammars ? [] + @packageManager.getLoadedPackage(packageName)?.loadGrammarsSync() + if state.destroyedItemURIs? + @destroyedItemURIs = state.destroyedItemURIs + @paneContainer.deserialize(state.paneContainer, deserializerManager) getPackageNamesWithActiveGrammars: -> packageNames = [] - addGrammar = ({includedGrammarScopes, packageName}={}) -> + addGrammar = ({includedGrammarScopes, packageName}={}) => return unless packageName # Prevent cycles return if packageNames.indexOf(packageName) isnt -1 packageNames.push(packageName) for scopeName in includedGrammarScopes ? [] - addGrammar(atom.grammars.grammarForScopeName(scopeName)) + addGrammar(@grammarRegistry.grammarForScopeName(scopeName)) return editors = @getTextEditors() addGrammar(editor.getGrammar()) for editor in editors if editors.length > 0 - for grammar in atom.grammars.getGrammars() when grammar.injectionSelector + for grammar in @grammarRegistry.getGrammars() when grammar.injectionSelector addGrammar(grammar) _.uniq(packageNames) - editorAdded: (editor) -> - - installShellCommands: -> - CommandInstaller = require('./command-installer') - commandInstaller = new CommandInstaller(atom.getVersion()) - commandInstaller.installShellCommandsInteractively() - subscribeToActiveItem: -> @updateWindowTitle() @updateDocumentEdited() - atom.project.onDidChangePaths @updateWindowTitle + @project.onDidChangePaths @updateWindowTitle @observeActivePaneItem (item) => @updateWindowTitle() @@ -156,7 +152,7 @@ class Workspace extends Model # open. updateWindowTitle: => appName = 'Atom' - projectPaths = atom.project?.getPaths() ? [] + projectPaths = @project.getPaths() ? [] if item = @getActivePaneItem() itemPath = item.getPath?() itemTitle = item.getTitle?() @@ -167,19 +163,19 @@ class Workspace extends Model if item? and projectPath? document.title = "#{itemTitle} - #{projectPath} - #{appName}" - atom.setRepresentedFilename(itemPath ? projectPath) + @applicationDelegate.setRepresentedFilename(itemPath ? projectPath) else if projectPath? document.title = "#{projectPath} - #{appName}" - atom.setRepresentedFilename(projectPath) + @applicationDelegate.setRepresentedFilename(projectPath) else document.title = "#{itemTitle} - #{appName}" - atom.setRepresentedFilename("") + @applicationDelegate.setRepresentedFilename("") # On OS X, fades the application window's proxy icon when the current file # has been modified. updateDocumentEdited: => modified = @getActivePaneItem()?.isModified?() ? false - atom.setDocumentEdited(modified) + @applicationDelegate.setWindowDocumentEdited(modified) ### Section: Event Subscription @@ -391,6 +387,8 @@ class Workspace extends Model # item will be opened in the rightmost pane of the current active pane's row. # * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on # containing pane. Defaults to `true`. + # * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem} + # on containing pane. Defaults to `true`. # * `searchAllPanes` A {Boolean}. If `true`, the workspace will attempt to # activate an existing item for the given URI on any pane. # If `false`, only the active pane will be searched for @@ -400,7 +398,7 @@ class Workspace extends Model open: (uri, options={}) -> searchAllPanes = options.searchAllPanes split = options.split - uri = atom.project.resolvePath(uri) + uri = @project.resolvePath(uri) pane = @paneContainer.paneForURI(uri) if searchAllPanes pane ?= switch split @@ -429,50 +427,51 @@ class Workspace extends Model # initially. Defaults to `0`. # * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on # the containing pane. Defaults to `true`. + # * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem} + # on containing pane. Defaults to `true`. openSync: (uri='', options={}) -> {initialLine, initialColumn} = options activatePane = options.activatePane ? true + activateItem = options.activateItem ? true - uri = atom.project.resolvePath(uri) + uri = @project.resolvePath(uri) item = @getActivePane().itemForURI(uri) if uri item ?= opener(uri, options) for opener in @getOpeners() when not item - item ?= atom.project.openSync(uri, {initialLine, initialColumn}) + item ?= @project.openSync(uri, {initialLine, initialColumn}) - @getActivePane().activateItem(item) + @getActivePane().activateItem(item) if activateItem @itemOpened(item) @getActivePane().activate() if activatePane item openURIInPane: (uri, pane, options={}) -> activatePane = options.activatePane ? true + activateItem = options.activateItem ? true if uri? item = pane.itemForURI(uri) item ?= opener(uri, options) for opener in @getOpeners() when not item try - item ?= atom.project.open(uri, options) + item ?= @openTextFile(uri, options) catch error switch error.code when 'CANCELLED' return Promise.resolve() when 'EACCES' - atom.notifications.addWarning("Permission denied '#{error.path}'") + @notificationManager.addWarning("Permission denied '#{error.path}'") return Promise.resolve() when 'EPERM', 'EBUSY', 'ENXIO', 'EIO', 'ENOTCONN', 'UNKNOWN', 'ECONNRESET', 'EINVAL' - atom.notifications.addWarning("Unable to open '#{error.path ? uri}'", detail: error.message) + @notificationManager.addWarning("Unable to open '#{error.path ? uri}'", detail: error.message) return Promise.resolve() else throw error Promise.resolve(item) .then (item) => - if not pane - pane = new Pane(items: [item]) - @paneContainer.root = pane @itemOpened(item) - pane.activateItem(item) + pane.activateItem(item) if activateItem pane.activate() if activatePane initialLine = initialColumn = 0 @@ -487,6 +486,42 @@ class Workspace extends Model @emitter.emit 'did-open', {uri, pane, item, index} item + openTextFile: (uri, options) -> + filePath = @project.resolvePath(uri) + + if filePath? + try + fs.closeSync(fs.openSync(filePath, 'r')) + catch error + # allow ENOENT errors to create an editor for paths that dont exist + throw error unless error.code is 'ENOENT' + + fileSize = fs.getSizeSync(filePath) + + largeFileMode = fileSize >= 2 * 1048576 # 2MB + if fileSize >= 20 * 1048576 # 20MB + choice = @applicationDelegate.confirm + message: 'Atom will be unresponsive during the loading of very large files.' + detailedMessage: "Do you still want to load this file?" + buttons: ["Proceed", "Cancel"] + if choice is 1 + error = new Error + error.code = 'CANCELLED' + throw error + + @project.bufferForPath(filePath, options).then (buffer) => + @buildTextEditor(_.extend({buffer, largeFileMode}, options)) + + # Extended: Create a new text editor. + # + # Returns a {TextEditor}. + buildTextEditor: (params) -> + params = _.extend({ + @config, @notificationManager, @packageManager, @clipboard, @viewRegistry, + @grammarRegistry, @project, @assert, @applicationDelegate + }, params) + new TextEditor(params) + # Public: Asynchronously reopens the last-closed item's URI if it hasn't already been # reopened. # @@ -640,20 +675,20 @@ class Workspace extends Model # Increase the editor font size by 1px. increaseFontSize: -> - atom.config.set("editor.fontSize", atom.config.get("editor.fontSize") + 1) + @config.set("editor.fontSize", @config.get("editor.fontSize") + 1) # Decrease the editor font size by 1px. decreaseFontSize: -> - fontSize = atom.config.get("editor.fontSize") - atom.config.set("editor.fontSize", fontSize - 1) if fontSize > 1 + fontSize = @config.get("editor.fontSize") + @config.set("editor.fontSize", fontSize - 1) if fontSize > 1 # Restore to the window's original editor font size. resetFontSize: -> if @originalFontSize - atom.config.set("editor.fontSize", @originalFontSize) + @config.set("editor.fontSize", @originalFontSize) subscribeToFontSize: -> - atom.config.onDidChange 'editor.fontSize', ({oldValue}) => + @config.onDidChange 'editor.fontSize', ({oldValue}) => @originalFontSize ?= oldValue # Removes the item's uri from the list of potential items to reopen. @@ -831,7 +866,7 @@ class Workspace extends Model # Find a searcher for every Directory in the project. Each searcher that is matched # will be associated with an Array of Directory objects in the Map. directoriesForSearcher = new Map() - for directory in atom.project.getDirectories() + for directory in @project.getDirectories() searcher = @defaultDirectorySearcher for directorySearcher in @directorySearchers if directorySearcher.canSearchDirectory(directory) @@ -862,15 +897,15 @@ class Workspace extends Model # Kick off all of the searches and unify them into one Promise. allSearches = [] - directoriesForSearcher.forEach (directories, searcher) -> + directoriesForSearcher.forEach (directories, searcher) => searchOptions = inclusions: options.paths or [] includeHidden: true - excludeVcsIgnores: atom.config.get('core.excludeVcsIgnoredPaths') - exclusions: atom.config.get('core.ignoredNames') - follow: atom.config.get('core.followSymlinks') - didMatch: (result) -> - iterator(result) unless atom.project.isPathModified(result.filePath) + excludeVcsIgnores: @config.get('core.excludeVcsIgnoredPaths') + exclusions: @config.get('core.ignoredNames') + follow: @config.get('core.followSymlinks') + didMatch: (result) => + iterator(result) unless @project.isPathModified(result.filePath) didError: (error) -> iterator(null, error) didSearchPaths: (count) -> onPathsSearched(searcher, count) @@ -878,9 +913,9 @@ class Workspace extends Model allSearches.push(directorySearcher) searchPromise = Promise.all(allSearches) - for buffer in atom.project.getBuffers() when buffer.isModified() + for buffer in @project.getBuffers() when buffer.isModified() filePath = buffer.getPath() - continue unless atom.project.contains(filePath) + continue unless @project.contains(filePath) matches = [] buffer.scan regex, (match) -> matches.push match iterator {filePath, matches} if matches.length > 0 @@ -926,8 +961,8 @@ class Workspace extends Model # # Returns a `Promise`. replace: (regex, replacementText, filePaths, iterator) -> - new Promise (resolve, reject) -> - openPaths = (buffer.getPath() for buffer in atom.project.getBuffers()) + new Promise (resolve, reject) => + openPaths = (buffer.getPath() for buffer in @project.getBuffers()) outOfProcessPaths = _.difference(filePaths, openPaths) inProcessFinished = not openPaths.length @@ -946,7 +981,7 @@ class Workspace extends Model task.on 'replace:path-replaced', iterator task.on 'replace:file-error', (error) -> iterator(null, error) - for buffer in atom.project.getBuffers() + for buffer in @project.getBuffers() continue unless buffer.getPath() in filePaths replacements = buffer.replace(regex, replacementText, iterator) iterator({filePath: buffer.getPath(), replacements}) if replacements diff --git a/static/index.js b/static/index.js index 11527e963..9bd1f7b20 100644 --- a/static/index.js +++ b/static/index.js @@ -76,7 +76,7 @@ setupVmCompatibility() setupCsonCache(CompileCache.getCacheDirectory()) - require(loadSettings.bootstrapScript) + require(loadSettings.windowInitializationScript) require('ipc').sendChannel('window-command', 'window:loaded') } @@ -168,7 +168,7 @@ var backgroundStylesheet = document.createElement('style') backgroundStylesheet.type = 'text/css' - backgroundStylesheet.innerText = 'html, body { background: ' + backgroundColor + '; }' + backgroundStylesheet.innerText = 'html, body { background: ' + backgroundColor + ' !important; }' document.head.appendChild(backgroundStylesheet) // Remove once the page loads