diff --git a/.gitmodules b/.gitmodules index d843b2761..5e7fd303a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -51,7 +51,7 @@ url = https://github.com/mmcgrana/textmate-clojure [submodule "prebuilt-cef"] path = prebuilt-cef - url = git@github.com:github/prebuilt-cef.git + url = https://github.com/github/prebuilt-cef [submodule "vendor/packages/yaml.tmbundle"] path = vendor/packages/yaml.tmbundle url = https://github.com/textmate/yaml.tmbundle.git diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/Rakefile b/Rakefile index 8e07e69e0..403cc250a 100644 --- a/Rakefile +++ b/Rakefile @@ -3,7 +3,7 @@ BUILD_DIR = '/tmp/atom-build' desc "Build Atom via `xcodebuild`" task :build => "create-xcode-project" do - command = "xcodebuild -target Atom -configuration Release SYMROOT=#{BUILD_DIR}" + command = "xcodebuild -target Atom SYMROOT=#{BUILD_DIR}" output = `#{command}` if $?.exitstatus != 0 $stderr.puts "Error #{$?.exitstatus}:\n#{output}" @@ -64,18 +64,6 @@ task :install => [:clean, :build] do puts "\033[32mAtom is installed at `#{dest_path}`. Atom cli is installed at `#{cli_path}`\033[0m" end -desc "Package up the app for speakeasy" -task :package => ["setup-codesigning", "build"] do - path = application_path() - exit 1 if not path - - dest_path = '/tmp/atom-for-speakeasy/Atom.tar.bz2' - `mkdir -p $(dirname #{dest_path})` - `rm -rf #{dest_path}` - `tar --directory $(dirname #{path}) -jcf #{dest_path} $(basename #{path})` - `open $(dirname #{dest_path})` -end - task "setup-codesigning" do ENV['CODE_SIGN'] = "Developer ID Application: GitHub" end diff --git a/atom.gyp b/atom.gyp index 93be1d16f..068a571f9 100644 --- a/atom.gyp +++ b/atom.gyp @@ -36,7 +36,7 @@ 'sources.gypi', ], 'target_defaults': { - 'default_configuration': 'Debug', + 'default_configuration': 'Release', 'configurations': { 'Debug': { 'defines': ['DEBUG=1'], @@ -71,7 +71,7 @@ 'native/mac/speakeasy.pem', ], 'xcode_settings': { - 'INFOPLIST_FILE': 'native/mac/info.plist', + 'INFOPLIST_FILE': 'native/mac/Atom-Info.plist', 'LD_RUNPATH_SEARCH_PATHS': '@executable_path/../Frameworks', }, 'conditions': [ @@ -178,6 +178,12 @@ 'Atom', ], }, + { + 'postbuild_name': 'Print env for Constructicon', + 'action': [ + 'env', + ], + }, ], 'link_settings': { 'libraries': [ diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee index d77958db7..c0d142a6d 100644 --- a/benchmark/benchmark-suite.coffee +++ b/benchmark/benchmark-suite.coffee @@ -107,7 +107,7 @@ describe "TokenizedBuffer.", -> [languageMode, buffer] = [] beforeEach -> - editSession = benchmarkFixturesProject.buildEditSessionForPath('medium.coffee') + editSession = benchmarkFixturesProject.buildEditSession('medium.coffee') { languageMode, buffer } = editSession benchmark "construction", 20, -> diff --git a/docs/getting-started.md b/docs/getting-started.md index 969272d2f..507b6c653 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -157,10 +157,10 @@ its own namespace. - hideGitIgnoredFiles: Whether files in the .gitignore should be hidden - ignoredNames: File names to ignore across all of atom (not fully implemented) - themes: An array of theme names to load, in cascading order + - autosave: Save a resource when its view loses focus - editor - autoIndent: Enable/disable basic auto-indent (defaults to true) - autoIndentOnPaste: Enable/disable auto-indented pasted text (defaults to false) - - autosave: Save a file when an editor loses focus - nonWordCharacters: A string of non-word characters to define word boundaries - fontSize - fontFamily diff --git a/docs/internals/configuration.md b/docs/internals/configuration.md index c25b0155d..d1df1d358 100644 --- a/docs/internals/configuration.md +++ b/docs/internals/configuration.md @@ -7,7 +7,7 @@ read config settings. You can read a value from `config` with `config.get`: ```coffeescript # read a value with `config.get` -@autosave() if config.get "editor.autosave" +@autosave() if config.get "core.autosave" ``` Or you can use `observeConfig` to track changes from a view object. @@ -47,7 +47,7 @@ the following way: ```coffeescript # basic key update -config.set("editor.autosave", true) +config.set("core.autosave", true) # if you mutate a config key, you'll need to call `config.update` to inform # observers of the change diff --git a/native/atom_window_controller.mm b/native/atom_window_controller.mm index 5e0375a35..7a74fdf22 100644 --- a/native/atom_window_controller.mm +++ b/native/atom_window_controller.mm @@ -46,6 +46,11 @@ if (alwaysUseBundleResourcePath || !_resourcePath) { _resourcePath = [[NSBundle bundleForClass:self.class] resourcePath]; } + + if ([self isDevMode]) { + [self displayDevIcon]; + } + _resourcePath = [_resourcePath stringByStandardizingPath]; [_resourcePath retain]; @@ -72,7 +77,6 @@ - (id)initDevWithPath:(NSString *)path { _pathToOpen = [path retain]; - AtomApplication *atomApplication = (AtomApplication *)[AtomApplication sharedApplication]; return [self initWithBootstrapScript:@"window-bootstrap" background:NO alwaysUseBundleResourcePath:false]; } @@ -127,6 +131,8 @@ [urlString appendString:[[url URLByAppendingPathComponent:@"static/index.html"] absoluteString]]; [urlString appendFormat:@"?bootstrapScript=%@", [self encodeUrlParam:_bootstrapScript]]; [urlString appendFormat:@"&resourcePath=%@", [self encodeUrlParam:_resourcePath]]; + if ([self isDevMode]) + [urlString appendFormat:@"&devMode=1"]; if (_exitWhenDone) [urlString appendString:@"&exitWhenDone=1"]; if (_pathToOpen) @@ -205,6 +211,33 @@ return YES; } +- (bool)isDevMode { + NSString *bundleResourcePath = [[NSBundle bundleForClass:self.class] resourcePath]; + return ![_resourcePath isEqualToString:bundleResourcePath]; +} + +- (void)displayDevIcon { + NSView *themeFrame = [self.window.contentView superview]; + NSButton *fullScreenButton = nil; + for (NSView *view in themeFrame.subviews) { + if (![view isKindOfClass:NSButton.class]) continue; + NSButton *button = (NSButton *)view; + if (button.action != @selector(toggleFullScreen:)) continue; + fullScreenButton = button; + break; + } + + NSButton *devButton = [[NSButton alloc] init]; + [devButton setTitle:@"\xF0\x9F\x92\x80"]; + devButton.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin; + devButton.buttonType = NSMomentaryChangeButton; + devButton.bordered = NO; + [devButton sizeToFit]; + devButton.frame = NSMakeRect(fullScreenButton.frame.origin.x - devButton.frame.size.width - 5, fullScreenButton.frame.origin.y, devButton.frame.size.width, devButton.frame.size.height); + + [[self.window.contentView superview] addSubview:devButton]; +} + - (void)populateBrowserSettings:(CefBrowserSettings &)settings { CefString(&settings.default_encoding) = "UTF-8"; settings.remote_fonts = STATE_ENABLED; diff --git a/native/mac/info.plist b/native/mac/Atom-Info.plist similarity index 100% rename from native/mac/info.plist rename to native/mac/Atom-Info.plist diff --git a/native/v8_extensions/git.mm b/native/v8_extensions/git.mm index 36229345e..49aa98bf1 100644 --- a/native/v8_extensions/git.mm +++ b/native/v8_extensions/git.mm @@ -106,7 +106,7 @@ namespace v8_extensions { return; int shortNameLength = branchNameLength - 11; - char* shortName = (char*) malloc(sizeof(char) * shortNameLength + 1); + char* shortName = (char*) malloc(sizeof(char) * (shortNameLength + 1)); shortName[shortNameLength] = '\0'; strncpy(shortName, &branchName[11], shortNameLength); *out = shortName; @@ -122,15 +122,18 @@ namespace v8_extensions { return; int shortBranchNameLength = strlen(shortBranchName); - char* remoteKey = (char*) malloc(sizeof(char) * shortBranchNameLength + 15); + char* remoteKey = (char*) malloc(sizeof(char) * (shortBranchNameLength + 15)); sprintf(remoteKey, "branch.%s.remote", shortBranchName); - char* mergeKey = (char*) malloc(sizeof(char) * shortBranchNameLength + 14); + char* mergeKey = (char*) malloc(sizeof(char) * (shortBranchNameLength + 14)); sprintf(mergeKey, "branch.%s.merge", shortBranchName); free((char*)shortBranchName); git_config *config; - if (git_repository_config(&config, repo) != GIT_OK) + if (git_repository_config(&config, repo) != GIT_OK) { + free(remoteKey); + free(mergeKey); return; + } const char *remote; const char *merge; diff --git a/script/compile-coffee b/script/compile-coffee index 298d9f53c..f3d19392c 100755 --- a/script/compile-coffee +++ b/script/compile-coffee @@ -6,7 +6,14 @@ set -e # The Setup's environment ourselves. If this isn't done, things like the # node shim won't be able to find the stuff they need. -node --version > /dev/null 2>&1 || source /opt/github/env.sh +node --version > /dev/null 2>&1 || { + if [ -e /opt/github/env.sh ]; then + source /opt/github/env.sh + else + # Try Constructicon's PATH. + export PATH="/usr/local/Cellar/node/0.8.21/bin:${PATH}" + fi +} INPUT_FILE="${1}" OUTPUT_FILE="${2}" diff --git a/script/compile-cson b/script/compile-cson index bc3a79ff3..7168fd19d 100755 --- a/script/compile-cson +++ b/script/compile-cson @@ -6,7 +6,14 @@ set -e # The Setup's environment ourselves. If this isn't done, things like the # node shim won't be able to find the stuff they need. -node --version > /dev/null 2>&1 || source /opt/github/env.sh +node --version > /dev/null 2>&1 || { + if [ -e /opt/github/env.sh ]; then + source /opt/github/env.sh + else + # Try Constructicon's PATH. + export PATH="/usr/local/Cellar/node/0.8.21/bin:${PATH}" + fi +} INPUT_FILE="${1}" OUTPUT_FILE="${2}" diff --git a/script/constructicon/prebuild b/script/constructicon/prebuild new file mode 100755 index 000000000..734ca7c58 --- /dev/null +++ b/script/constructicon/prebuild @@ -0,0 +1,9 @@ +#!/bin/sh + +set -ex + +cd "$(dirname "$0")/../.." + +export PATH="/usr/local/Cellar/node/0.8.21/bin:/usr/local/bin:${PATH}" + +rake setup-codesigning create-xcode-project diff --git a/spec/app/atom-package-spec.coffee b/spec/app/atom-package-spec.coffee index 067f2e0e4..7b75b8ede 100644 --- a/spec/app/atom-package-spec.coffee +++ b/spec/app/atom-package-spec.coffee @@ -23,7 +23,7 @@ describe "AtomPackage", -> it "triggers the activation event on all handlers registered during activation", -> rootView.open('sample.js') - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() eventHandler = jasmine.createSpy("activation-event") editor.command 'activation-event', eventHandler editor.trigger 'activation-event' diff --git a/spec/app/atom-spec.coffee b/spec/app/atom-spec.coffee index 261c86d4f..e614a2ff7 100644 --- a/spec/app/atom-spec.coffee +++ b/spec/app/atom-spec.coffee @@ -141,3 +141,74 @@ describe "the `atom` global", -> runs -> expect(versionHandler.argsForCall[0][0]).toMatch /^\d+\.\d+(\.\d+)?$/ + + describe "modal native dialogs", -> + beforeEach -> + spyOn(atom, 'sendMessageToBrowserProcess') + atom.sendMessageToBrowserProcess.simulateConfirmation = (buttonText) -> + labels = @argsForCall[0][1][2...] + callbacks = @argsForCall[0][2] + @reset() + callbacks[labels.indexOf(buttonText)]() + advanceClock 50 + + atom.sendMessageToBrowserProcess.simulatePathSelection = (path) -> + callback = @argsForCall[0][2] + @reset() + callback(path) + advanceClock 50 + + it "only presents one native dialog at a time", -> + confirmHandler = jasmine.createSpy("confirmHandler") + selectPathHandler = jasmine.createSpy("selectPathHandler") + + atom.confirm "Are you happy?", "really, truly happy?", "Yes", confirmHandler, "No" + atom.confirm "Are you happy?", "really, truly happy?", "Yes", confirmHandler, "No" + atom.showSaveDialog(selectPathHandler) + atom.showSaveDialog(selectPathHandler) + + expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 + atom.sendMessageToBrowserProcess.simulateConfirmation("Yes") + expect(confirmHandler).toHaveBeenCalled() + + expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 + atom.sendMessageToBrowserProcess.simulateConfirmation("No") + + expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 + atom.sendMessageToBrowserProcess.simulatePathSelection('/selected/path') + expect(selectPathHandler).toHaveBeenCalledWith('/selected/path') + selectPathHandler.reset() + + expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 + + it "prioritizes dialogs presented as the result of dismissing other dialogs before any previously deferred dialogs", -> + atom.confirm "A1", "", "Next", -> + atom.confirm "B1", "", "Next", -> + atom.confirm "C1", "", "Next", -> + atom.confirm "C2", "", "Next", -> + atom.confirm "B2", "", "Next", -> + atom.confirm "A2", "", "Next", -> + + expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 + expect(atom.sendMessageToBrowserProcess.argsForCall[0][1][0]).toBe "A1" + atom.sendMessageToBrowserProcess.simulateConfirmation('Next') + + expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 + expect(atom.sendMessageToBrowserProcess.argsForCall[0][1][0]).toBe "B1" + atom.sendMessageToBrowserProcess.simulateConfirmation('Next') + + expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 + expect(atom.sendMessageToBrowserProcess.argsForCall[0][1][0]).toBe "C1" + atom.sendMessageToBrowserProcess.simulateConfirmation('Next') + + expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 + expect(atom.sendMessageToBrowserProcess.argsForCall[0][1][0]).toBe "C2" + atom.sendMessageToBrowserProcess.simulateConfirmation('Next') + + expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 + expect(atom.sendMessageToBrowserProcess.argsForCall[0][1][0]).toBe "B2" + atom.sendMessageToBrowserProcess.simulateConfirmation('Next') + + expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 + expect(atom.sendMessageToBrowserProcess.argsForCall[0][1][0]).toBe "A2" + atom.sendMessageToBrowserProcess.simulateConfirmation('Next') diff --git a/spec/app/buffer-spec.coffee b/spec/app/buffer-spec.coffee index 3510d46f4..84b58ddeb 100644 --- a/spec/app/buffer-spec.coffee +++ b/spec/app/buffer-spec.coffee @@ -180,24 +180,88 @@ describe 'Buffer', -> waitsFor 'change event', -> changeHandler.callCount > 0 - describe ".isModified()", -> - it "returns true when user changes buffer", -> + describe "modified status", -> + it "reports the modified status changing to true or false after the user changes buffer", -> + modifiedHandler = jasmine.createSpy("modifiedHandler") + buffer.on 'modified-status-changed', modifiedHandler + expect(buffer.isModified()).toBeFalsy() buffer.insert([0,0], "hi") expect(buffer.isModified()).toBe true - it "returns false after modified buffer is saved", -> + advanceClock(buffer.stoppedChangingDelay) + expect(modifiedHandler).toHaveBeenCalledWith(true) + + modifiedHandler.reset() + buffer.insert([0,2], "ho") + advanceClock(buffer.stoppedChangingDelay) + expect(modifiedHandler).not.toHaveBeenCalled() + + modifiedHandler.reset() + buffer.undo() + buffer.undo() + advanceClock(buffer.stoppedChangingDelay) + expect(modifiedHandler).toHaveBeenCalledWith(false) + + it "reports the modified status changing to true after the underlying file is deleted", -> + buffer.release() + filePath = "/tmp/atom-tmp-file" + fs.write(filePath, 'delete me') + buffer = new Buffer(filePath) + modifiedHandler = jasmine.createSpy("modifiedHandler") + buffer.on 'modified-status-changed', modifiedHandler + + fs.remove(filePath) + + waitsFor "modified status to change", -> modifiedHandler.callCount + runs -> expect(buffer.isModified()).toBe true + + it "reports the modified status changing to false after a modified buffer is saved", -> filePath = "/tmp/atom-tmp-file" fs.write(filePath, '') buffer.release() buffer = new Buffer(filePath) - expect(buffer.isModified()).toBe false + modifiedHandler = jasmine.createSpy("modifiedHandler") + buffer.on 'modified-status-changed', modifiedHandler buffer.insert([0,0], "hi") + advanceClock(buffer.stoppedChangingDelay) expect(buffer.isModified()).toBe true + modifiedHandler.reset() buffer.save() + + expect(modifiedHandler).toHaveBeenCalledWith(false) expect(buffer.isModified()).toBe false + modifiedHandler.reset() + + buffer.insert([0, 0], 'x') + advanceClock(buffer.stoppedChangingDelay) + expect(modifiedHandler).toHaveBeenCalledWith(true) + expect(buffer.isModified()).toBe true + + it "reports the modified status changing to false after a modified buffer is reloaded", -> + filePath = "/tmp/atom-tmp-file" + fs.write(filePath, '') + buffer.release() + buffer = new Buffer(filePath) + modifiedHandler = jasmine.createSpy("modifiedHandler") + buffer.on 'modified-status-changed', modifiedHandler + + buffer.insert([0,0], "hi") + advanceClock(buffer.stoppedChangingDelay) + expect(buffer.isModified()).toBe true + modifiedHandler.reset() + + buffer.reload() + expect(modifiedHandler).toHaveBeenCalledWith(false) + expect(buffer.isModified()).toBe false + modifiedHandler.reset() + + buffer.insert([0, 0], 'x') + advanceClock(buffer.stoppedChangingDelay) + expect(modifiedHandler).toHaveBeenCalledWith(true) + expect(buffer.isModified()).toBe true it "returns false for an empty buffer with no path", -> buffer.release() @@ -1056,61 +1120,30 @@ describe 'Buffer', -> expect(buffer.isEmpty()).toBeFalsy() describe "'contents-modified' event", -> - describe "when the buffer is deleted", -> - it "triggers the contents-modified event", -> - delay = buffer.stoppedChangingDelay - path = "/tmp/atom-file-to-delete.txt" - fs.write(path, 'delete me') - bufferToDelete = new Buffer(path) - contentsModifiedHandler = jasmine.createSpy("contentsModifiedHandler") - bufferToDelete.on 'contents-modified', contentsModifiedHandler + it "triggers the 'contents-modified' event with the current modified status when the buffer changes, rate-limiting events with a delay", -> + delay = buffer.stoppedChangingDelay + contentsModifiedHandler = jasmine.createSpy("contentsModifiedHandler") + buffer.on 'contents-modified', contentsModifiedHandler - expect(bufferToDelete.getPath()).toBe path - expect(bufferToDelete.isModified()).toBeFalsy() - expect(contentsModifiedHandler).not.toHaveBeenCalled() + buffer.insert([0, 0], 'a') + expect(contentsModifiedHandler).not.toHaveBeenCalled() - removeHandler = jasmine.createSpy('removeHandler') - bufferToDelete.file.on 'removed', removeHandler - fs.remove(path) - waitsFor "file to be removed", -> - removeHandler.callCount > 0 + advanceClock(delay / 2) - runs -> - expect(contentsModifiedHandler).toHaveBeenCalledWith(differsFromDisk:true) - bufferToDelete.destroy() + buffer.insert([0, 0], 'b') + expect(contentsModifiedHandler).not.toHaveBeenCalled() - describe "when the buffer text has been changed", -> - it "triggers the contents-modified event 'stoppedChangingDelay' ms after the last buffer change", -> - delay = buffer.stoppedChangingDelay - contentsModifiedHandler = jasmine.createSpy("contentsModifiedHandler") - buffer.on 'contents-modified', contentsModifiedHandler + advanceClock(delay / 2) + expect(contentsModifiedHandler).not.toHaveBeenCalled() - buffer.insert([0, 0], 'a') - expect(contentsModifiedHandler).not.toHaveBeenCalled() + advanceClock(delay / 2) + expect(contentsModifiedHandler).toHaveBeenCalledWith(true) - advanceClock(delay / 2) - - buffer.insert([0, 0], 'b') - expect(contentsModifiedHandler).not.toHaveBeenCalled() - - advanceClock(delay / 2) - expect(contentsModifiedHandler).not.toHaveBeenCalled() - - advanceClock(delay / 2) - expect(contentsModifiedHandler).toHaveBeenCalled() - - it "triggers the contents-modified event with data about whether its contents differ from the contents on disk", -> - delay = buffer.stoppedChangingDelay - contentsModifiedHandler = jasmine.createSpy("contentsModifiedHandler") - buffer.on 'contents-modified', contentsModifiedHandler - - buffer.insert([0, 0], 'a') - advanceClock(delay) - expect(contentsModifiedHandler).toHaveBeenCalledWith(differsFromDisk:true) - - buffer.delete([[0, 0], [0, 1]], '') - advanceClock(delay) - expect(contentsModifiedHandler).toHaveBeenCalledWith(differsFromDisk:false) + contentsModifiedHandler.reset() + buffer.undo() + buffer.undo() + advanceClock(delay) + expect(contentsModifiedHandler).toHaveBeenCalledWith(false) describe ".append(text)", -> it "adds text to the end of the buffer", -> diff --git a/spec/app/display-buffer-spec.coffee b/spec/app/display-buffer-spec.coffee index fe5dc83f7..2381d76e2 100644 --- a/spec/app/display-buffer-spec.coffee +++ b/spec/app/display-buffer-spec.coffee @@ -6,7 +6,7 @@ describe "DisplayBuffer", -> [editSession, displayBuffer, buffer, changeHandler, tabLength] = [] beforeEach -> tabLength = 2 - editSession = fixturesProject.buildEditSessionForPath('sample.js', { tabLength }) + editSession = project.buildEditSession('sample.js', { tabLength }) { buffer, displayBuffer } = editSession changeHandler = jasmine.createSpy 'changeHandler' displayBuffer.on 'changed', changeHandler @@ -228,7 +228,7 @@ describe "DisplayBuffer", -> editSession2 = null beforeEach -> - editSession2 = fixturesProject.buildEditSessionForPath('two-hundred.txt') + editSession2 = project.buildEditSession('two-hundred.txt') { buffer, displayBuffer } = editSession2 displayBuffer.on 'changed', changeHandler diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index fce910c73..1a4d3754d 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -9,12 +9,30 @@ describe "EditSession", -> buffer.setText(buffer.getText().replace(/[ ]{2}/g, "\t")) beforeEach -> - editSession = fixturesProject.buildEditSessionForPath('sample.js', autoIndent: false) + editSession = project.buildEditSession('sample.js', autoIndent: false) buffer = editSession.buffer lineLengths = buffer.getLines().map (line) -> line.length - afterEach -> - fixturesProject.destroy() + describe "title", -> + describe ".getTitle()", -> + it "uses the basename of the buffer's path as its title, or 'untitled' if the path is undefined", -> + expect(editSession.getTitle()).toBe 'sample.js' + buffer.setPath(undefined) + expect(editSession.getTitle()).toBe 'untitled' + + describe ".getLongTitle()", -> + it "appends the name of the containing directory to the basename of the file", -> + expect(editSession.getLongTitle()).toBe 'sample.js - fixtures' + buffer.setPath(undefined) + expect(editSession.getLongTitle()).toBe 'untitled' + + it "emits 'title-changed' events when the underlying buffer path", -> + titleChangedHandler = jasmine.createSpy("titleChangedHandler") + editSession.on 'title-changed', titleChangedHandler + + buffer.setPath('/foo/bar/baz.txt') + buffer.setPath(undefined) + expect(titleChangedHandler.callCount).toBe 2 describe "cursor", -> describe ".getCursor()", -> @@ -1715,7 +1733,7 @@ describe "EditSession", -> it "does not explode if the current language mode has no comment regex", -> editSession.destroy() - editSession = fixturesProject.buildEditSessionForPath(null, autoIndent: false) + editSession = project.buildEditSession(null, autoIndent: false) editSession.setSelectedBufferRange([[4, 5], [4, 5]]) editSession.toggleLineCommentsInSelection() expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {" @@ -1793,7 +1811,7 @@ describe "EditSession", -> expect(editSession.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 18], [1, 18]]] it "restores selected ranges even when the change occurred in another edit session", -> - otherEditSession = fixturesProject.buildEditSessionForPath(editSession.getPath()) + otherEditSession = project.buildEditSession(editSession.getPath()) otherEditSession.setSelectedBufferRange([[2, 2], [3, 3]]) otherEditSession.delete() @@ -1986,13 +2004,13 @@ describe "EditSession", -> describe "soft-tabs detection", -> it "assign soft / hard tabs based on the contents of the buffer, or uses the default if unknown", -> - editSession = fixturesProject.buildEditSessionForPath('sample.js', softTabs: false) + editSession = project.buildEditSession('sample.js', softTabs: false) expect(editSession.softTabs).toBeTruthy() - editSession = fixturesProject.buildEditSessionForPath('sample-with-tabs.coffee', softTabs: true) + editSession = project.buildEditSession('sample-with-tabs.coffee', softTabs: true) expect(editSession.softTabs).toBeFalsy() - editSession = fixturesProject.buildEditSessionForPath(null, softTabs: false) + editSession = project.buildEditSession(null, softTabs: false) expect(editSession.softTabs).toBeFalsy() describe ".indentLevelForLine(line)", -> diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index 0d46cfba9..04e6c9b57 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -1,4 +1,3 @@ -RootView = require 'root-view' EditSession = require 'edit-session' Buffer = require 'buffer' Editor = require 'editor' @@ -10,75 +9,41 @@ _ = require 'underscore' fs = require 'fs' describe "Editor", -> - [buffer, editor, cachedLineHeight] = [] + [buffer, editor, editSession, cachedLineHeight, cachedCharWidth] = [] + + beforeEach -> + editSession = project.buildEditSession('sample.js') + buffer = editSession.buffer + editor = new Editor(editSession) + editor.lineOverdraw = 2 + editor.isFocused = true + editor.enableKeymap() + editor.attachToDom = ({ heightInLines, widthInChars } = {}) -> + heightInLines ?= this.getBuffer().getLineCount() + this.height(getLineHeight() * heightInLines) + this.width(getCharWidth() * widthInChars) if widthInChars + $('#jasmine-content').append(this) getLineHeight = -> return cachedLineHeight if cachedLineHeight? - editorForMeasurement = new Editor(editSession: project.buildEditSessionForPath('sample.js')) - editorForMeasurement.attachToDom() - cachedLineHeight = editorForMeasurement.lineHeight - editorForMeasurement.remove() + calcDimensions() cachedLineHeight - beforeEach -> - window.rootView = new RootView - rootView.open('sample.js') - editor = rootView.getActiveEditor() - buffer = editor.getBuffer() + getCharWidth = -> + return cachedCharWidth if cachedCharWidth? + calcDimensions() + cachedCharWidth - editor.attachToDom = ({ heightInLines } = {}) -> - heightInLines ?= this.getBuffer().getLineCount() - this.height(getLineHeight() * heightInLines) - $('#jasmine-content').append(this) - - editor.lineOverdraw = 2 - editor.enableKeymap() - editor.isFocused = true + calcDimensions = -> + editorForMeasurement = new Editor(editSession: project.buildEditSession('sample.js')) + editorForMeasurement.attachToDom() + cachedLineHeight = editorForMeasurement.lineHeight + cachedCharWidth = editorForMeasurement.charWidth + editorForMeasurement.remove() describe "construction", -> - it "throws an error if no editor session is given unless deserializing", -> + it "throws an error if no edit session is given", -> expect(-> new Editor).toThrow() - expect(-> new Editor(deserializing: true)).not.toThrow() - - describe ".copy()", -> - it "builds a new editor with the same edit sessions, cursor position, and scroll position as the receiver", -> - rootView.attachToDom() - rootView.height(8 * editor.lineHeight) - rootView.width(50 * editor.charWidth) - - editor.edit(project.buildEditSessionForPath('two-hundred.txt')) - editor.setCursorScreenPosition([5, 1]) - editor.scrollTop(1.5 * editor.lineHeight) - editor.scrollView.scrollLeft(44) - - # proves this test covers serialization and deserialization - spyOn(editor, 'serialize').andCallThrough() - spyOn(Editor, 'deserialize').andCallThrough() - - newEditor = editor.copy() - expect(editor.serialize).toHaveBeenCalled() - expect(Editor.deserialize).toHaveBeenCalled() - - expect(newEditor.getBuffer()).toBe editor.getBuffer() - expect(newEditor.getCursorScreenPosition()).toEqual editor.getCursorScreenPosition() - expect(newEditor.editSessions).toEqual(editor.editSessions) - expect(newEditor.activeEditSession).toEqual(editor.activeEditSession) - expect(newEditor.getActiveEditSessionIndex()).toEqual(editor.getActiveEditSessionIndex()) - - newEditor.height(editor.height()) - newEditor.width(editor.width()) - - newEditor.attachToDom() - expect(newEditor.scrollTop()).toBe editor.scrollTop() - expect(newEditor.scrollView.scrollLeft()).toBe 44 - - it "does not blow up if no file exists for a previous edit session, but prints a warning", -> - spyOn(console, 'warn') - fs.write('/tmp/delete-me') - editor.edit(project.buildEditSessionForPath('/tmp/delete-me')) - fs.remove('/tmp/delete-me') - newEditor = editor.copy() - expect(console.warn).toHaveBeenCalled() describe "when the editor is attached to the dom", -> it "calculates line height and char width and updates the pixel position of the cursor", -> @@ -117,7 +82,7 @@ describe "Editor", -> it "triggers an alert", -> path = "/tmp/atom-changed-file.txt" fs.write(path, "") - editSession = project.buildEditSessionForPath(path) + editSession = project.buildEditSession(path) editor.edit(editSession) editor.insertText("now the buffer is modified") @@ -135,274 +100,66 @@ describe "Editor", -> expect(atom.confirm).toHaveBeenCalled() describe ".remove()", -> - it "removes subscriptions from all edit session buffers", -> - editSession1 = editor.activeEditSession - subscriberCount1 = editSession1.buffer.subscriptionCount() - editSession2 = project.buildEditSessionForPath(project.resolve('sample.txt')) - expect(subscriberCount1).toBeGreaterThan 1 - - editor.edit(editSession2) - subscriberCount2 = editSession2.buffer.subscriptionCount() - expect(subscriberCount2).toBeGreaterThan 1 - + it "destroys the edit session", -> editor.remove() - expect(editSession1.buffer.subscriptionCount()).toBeLessThan subscriberCount1 - expect(editSession2.buffer.subscriptionCount()).toBeLessThan subscriberCount2 - - describe "when 'close' is triggered", -> - it "adds a closed session path to the array", -> - editor.edit(project.buildEditSessionForPath()) - editSession = editor.activeEditSession - expect(editor.closedEditSessions.length).toBe 0 - editor.trigger "core:close" - expect(editor.closedEditSessions.length).toBe 0 - editor.edit(project.buildEditSessionForPath(project.resolve('sample.txt'))) - editor.trigger "core:close" - expect(editor.closedEditSessions.length).toBe 1 - - it "closes the active edit session and loads next edit session", -> - editor.edit(project.buildEditSessionForPath()) - editSession = editor.activeEditSession - spyOn(editSession.buffer, 'isModified').andReturn false - spyOn(editSession, 'destroy').andCallThrough() - spyOn(editor, "remove").andCallThrough() - editor.trigger "core:close" - expect(editSession.destroy).toHaveBeenCalled() - expect(editor.remove).not.toHaveBeenCalled() - expect(editor.getBuffer()).toBe buffer - - it "triggers the 'editor:edit-session-removed' event with the edit session and its former index", -> - editor.edit(project.buildEditSessionForPath()) - editSession = editor.activeEditSession - index = editor.getActiveEditSessionIndex() - spyOn(editSession.buffer, 'isModified').andReturn false - - editSessionRemovedHandler = jasmine.createSpy('editSessionRemovedHandler') - editor.on 'editor:edit-session-removed', editSessionRemovedHandler - editor.trigger "core:close" - - expect(editSessionRemovedHandler).toHaveBeenCalled() - expect(editSessionRemovedHandler.argsForCall[0][1..2]).toEqual [editSession, index] - - it "calls remove on the editor if there is one edit session and mini is false", -> - editSession = editor.activeEditSession - expect(editor.mini).toBeFalsy() - expect(editor.editSessions.length).toBe 1 - spyOn(editor, 'remove').andCallThrough() - editor.trigger 'core:close' - spyOn(editSession, 'destroy').andCallThrough() - expect(editor.remove).toHaveBeenCalled() - - miniEditor = new Editor(mini: true) - spyOn(miniEditor, 'remove').andCallThrough() - miniEditor.trigger 'core:close' - expect(miniEditor.remove).not.toHaveBeenCalled() - - describe "when buffer is modified", -> - it "triggers an alert and does not close the session", -> - spyOn(editor, 'remove').andCallThrough() - spyOn(atom, 'confirm') - editor.insertText("I AM CHANGED!") - editor.trigger "core:close" - expect(editor.remove).not.toHaveBeenCalled() - expect(atom.confirm).toHaveBeenCalled() - - it "doesn't trigger an alert if the buffer is opened in multiple sessions", -> - spyOn(editor, 'remove').andCallThrough() - spyOn(atom, 'confirm') - editor.insertText("I AM CHANGED!") - editor.splitLeft() - editor.trigger "core:close" - expect(editor.remove).toHaveBeenCalled() - expect(atom.confirm).not.toHaveBeenCalled() + expect(editor.activeEditSession.destroyed).toBeTruthy() describe ".edit(editSession)", -> - otherEditSession = null + [newEditSession, newBuffer] = [] beforeEach -> - otherEditSession = project.buildEditSessionForPath() + newEditSession = project.buildEditSession('two-hundred.txt') + newBuffer = newEditSession.buffer - describe "when the edit session wasn't previously assigned to this editor", -> - it "adds edit session to editor and triggers the 'editor:edit-session-added' event", -> - editSessionAddedHandler = jasmine.createSpy('editSessionAddedHandler') - editor.on 'editor:edit-session-added', editSessionAddedHandler + it "updates the rendered lines, cursors, selections, scroll position, and event subscriptions to match the given edit session", -> + editor.attachToDom(heightInLines: 5, widthInChars: 30) + editor.setCursorBufferPosition([3, 5]) + editor.scrollToBottom() + editor.scrollView.scrollLeft(150) + previousScrollHeight = editor.verticalScrollbar.prop('scrollHeight') + previousScrollTop = editor.scrollTop() + previousScrollLeft = editor.scrollView.scrollLeft() - originalEditSessionCount = editor.editSessions.length - editor.edit(otherEditSession) - expect(editor.activeEditSession).toBe otherEditSession - expect(editor.editSessions.length).toBe originalEditSessionCount + 1 + newEditSession.scrollTop = 120 + newEditSession.setSelectedBufferRange([[40, 0], [43, 1]]) - expect(editSessionAddedHandler).toHaveBeenCalled() - expect(editSessionAddedHandler.argsForCall[0][1..2]).toEqual [otherEditSession, originalEditSessionCount] + editor.edit(newEditSession) + { firstRenderedScreenRow, lastRenderedScreenRow } = editor + expect(editor.lineElementForScreenRow(firstRenderedScreenRow).text()).toBe newBuffer.lineForRow(firstRenderedScreenRow) + expect(editor.lineElementForScreenRow(lastRenderedScreenRow).text()).toBe newBuffer.lineForRow(editor.lastRenderedScreenRow) + expect(editor.scrollTop()).toBe 120 + expect(editor.getSelectionView().regions[0].position().top).toBe 40 * editor.lineHeight + editor.insertText("hello") + expect(editor.lineElementForScreenRow(40).text()).toBe "hello3" - describe "when the edit session was previously assigned to this editor", -> - it "restores the previous edit session associated with the editor", -> - previousEditSession = editor.activeEditSession + editor.edit(editSession) + { firstRenderedScreenRow, lastRenderedScreenRow } = editor + expect(editor.lineElementForScreenRow(firstRenderedScreenRow).text()).toBe buffer.lineForRow(firstRenderedScreenRow) + expect(editor.lineElementForScreenRow(lastRenderedScreenRow).text()).toBe buffer.lineForRow(editor.lastRenderedScreenRow) + expect(editor.verticalScrollbar.prop('scrollHeight')).toBe previousScrollHeight + expect(editor.scrollTop()).toBe previousScrollTop + expect(editor.scrollView.scrollLeft()).toBe previousScrollLeft + expect(editor.getCursorView().position()).toEqual { top: 3 * editor.lineHeight, left: 5 * editor.charWidth } + editor.insertText("goodbye") + expect(editor.lineElementForScreenRow(3).text()).toMatch /^ vgoodbyear/ - editor.edit(otherEditSession) - expect(editor.activeEditSession).not.toBe previousEditSession + it "triggers alert if edit session's buffer goes into conflict with changes on disk", -> + path = "/tmp/atom-changed-file.txt" + fs.write(path, "") + tempEditSession = project.buildEditSession(path) + editor.edit(tempEditSession) + tempEditSession.insertText("a buffer change") - editor.edit(previousEditSession) - expect(editor.activeEditSession).toBe previousEditSession + spyOn(atom, "confirm") - it "handles buffer manipulation correctly after switching to a new edit session", -> - editor.attachToDom() - editor.insertText("abc\n") - expect(editor.lineElementForScreenRow(0).text()).toBe 'abc' + contentsConflictedHandler = jasmine.createSpy("contentsConflictedHandler") + tempEditSession.on 'contents-conflicted', contentsConflictedHandler + fs.write(path, "a file change") + waitsFor -> + contentsConflictedHandler.callCount > 0 - editor.edit(otherEditSession) - expect(editor.lineElementForScreenRow(0).html()).toBe ' ' - - editor.insertText("def\n") - expect(editor.lineElementForScreenRow(0).text()).toBe 'def' - - it "removes the opened session from the closed sessions array", -> - editor.edit(project.buildEditSessionForPath('sample.txt')) - expect(editor.closedEditSessions.length).toBe 0 - editor.trigger "core:close" - expect(editor.closedEditSessions.length).toBe 1 - editor.edit(project.buildEditSessionForPath('sample.txt')) - expect(editor.closedEditSessions.length).toBe 0 - - describe "switching edit sessions", -> - [session0, session1, session2] = [] - - beforeEach -> - session0 = editor.activeEditSession - - editor.edit(project.buildEditSessionForPath('sample.txt')) - session1 = editor.activeEditSession - - editor.edit(project.buildEditSessionForPath('two-hundred.txt')) - session2 = editor.activeEditSession - - describe ".setActiveEditSessionIndex(index)", -> - it "restores the buffer, cursors, selections, and scroll position of the edit session associated with the index", -> - editor.attachToDom(heightInLines: 10) - editor.setSelectedBufferRange([[40, 0], [43, 1]]) - expect(editor.getSelection().getScreenRange()).toEqual [[40, 0], [43, 1]] - previousScrollHeight = editor.verticalScrollbar.prop('scrollHeight') - - editor.scrollTop(750) - expect(editor.scrollTop()).toBe 750 - - editor.setActiveEditSessionIndex(0) - expect(editor.getBuffer()).toBe session0.buffer - - editor.setActiveEditSessionIndex(2) - expect(editor.getBuffer()).toBe session2.buffer - expect(editor.getCursorScreenPosition()).toEqual [43, 1] - expect(editor.verticalScrollbar.prop('scrollHeight')).toBe previousScrollHeight - expect(editor.scrollTop()).toBe 750 - expect(editor.getSelection().getScreenRange()).toEqual [[40, 0], [43, 1]] - expect(editor.getSelectionView().find('.region')).toExist() - - editor.setActiveEditSessionIndex(0) - editor.activeEditSession.selectToEndOfLine() - expect(editor.getSelectionView().find('.region')).toExist() - - it "triggers alert if edit session's buffer goes into conflict with changes on disk", -> - path = "/tmp/atom-changed-file.txt" - fs.write(path, "") - editSession = project.buildEditSessionForPath(path) - editor.edit editSession - editSession.insertText("a buffer change") - - spyOn(atom, "confirm") - - contentsConflictedHandler = jasmine.createSpy("contentsConflictedHandler") - editSession.on 'contents-conflicted', contentsConflictedHandler - fs.write(path, "a file change") - waitsFor -> - contentsConflictedHandler.callCount > 0 - - runs -> - expect(atom.confirm).toHaveBeenCalled() - - it "emits an editor:active-edit-session-changed event with the edit session and its index", -> - activeEditSessionChangeHandler = jasmine.createSpy('activeEditSessionChangeHandler') - editor.on 'editor:active-edit-session-changed', activeEditSessionChangeHandler - - editor.setActiveEditSessionIndex(2) - expect(activeEditSessionChangeHandler).toHaveBeenCalled() - expect(activeEditSessionChangeHandler.argsForCall[0][1..2]).toEqual [editor.activeEditSession, 2] - activeEditSessionChangeHandler.reset() - - editor.setActiveEditSessionIndex(0) - expect(activeEditSessionChangeHandler.argsForCall[0][1..2]).toEqual [editor.activeEditSession, 0] - activeEditSessionChangeHandler.reset() - - describe ".loadNextEditSession()", -> - it "loads the next editor state and wraps to beginning when end is reached", -> - expect(editor.activeEditSession).toBe session2 - editor.loadNextEditSession() - expect(editor.activeEditSession).toBe session0 - editor.loadNextEditSession() - expect(editor.activeEditSession).toBe session1 - editor.loadNextEditSession() - expect(editor.activeEditSession).toBe session2 - - describe ".loadPreviousEditSession()", -> - it "loads the next editor state and wraps to beginning when end is reached", -> - expect(editor.activeEditSession).toBe session2 - editor.loadPreviousEditSession() - expect(editor.activeEditSession).toBe session1 - editor.loadPreviousEditSession() - expect(editor.activeEditSession).toBe session0 - editor.loadPreviousEditSession() - expect(editor.activeEditSession).toBe session2 - - describe ".save()", -> - describe "when the current buffer has a path", -> - tempFilePath = null - - beforeEach -> - project.setPath('/tmp') - tempFilePath = '/tmp/atom-temp.txt' - fs.write(tempFilePath, "") - rootView.open(tempFilePath) - editor = rootView.getActiveEditor() - expect(editor.getPath()).toBe tempFilePath - - afterEach -> - expect(fs.remove(tempFilePath)) - - it "saves the current buffer to disk", -> - editor.getBuffer().setText 'Edited!' - expect(fs.read(tempFilePath)).not.toBe "Edited!" - - editor.save() - - expect(fs.exists(tempFilePath)).toBeTruthy() - expect(fs.read(tempFilePath)).toBe 'Edited!' - - describe "when the current buffer has no path", -> - selectedFilePath = null - beforeEach -> - editor.edit(project.buildEditSessionForPath()) - - expect(editor.getPath()).toBeUndefined() - editor.getBuffer().setText 'Save me to a new path' - spyOn(atom, 'showSaveDialog').andCallFake (callback) -> callback(selectedFilePath) - - it "presents a 'save as' dialog", -> - editor.save() - expect(atom.showSaveDialog).toHaveBeenCalled() - - describe "when a path is chosen", -> - it "saves the buffer to the chosen path", -> - selectedFilePath = '/tmp/temp.txt' - - editor.save() - - expect(fs.exists(selectedFilePath)).toBeTruthy() - expect(fs.read(selectedFilePath)).toBe 'Save me to a new path' - - describe "when dialog is cancelled", -> - it "does not save the buffer", -> - selectedFilePath = null - editor.save() - expect(fs.exists(selectedFilePath)).toBeFalsy() + runs -> + expect(atom.confirm).toHaveBeenCalled() describe ".scrollTop(n)", -> beforeEach -> @@ -451,29 +208,6 @@ describe "Editor", -> editor.scrollTop(50) expect(editor.scrollTop()).toBe 50 - describe "split methods", -> - describe "when inside a pane", -> - fakePane = null - beforeEach -> - fakePane = { splitUp: jasmine.createSpy('splitUp').andReturn({}), remove: -> } - spyOn(editor, 'pane').andReturn(fakePane) - - it "calls the corresponding split method on the containing pane with a new editor containing a copy of the active edit session", -> - editor.edit project.buildEditSessionForPath("sample.txt") - editor.splitUp() - expect(fakePane.splitUp).toHaveBeenCalled() - [newEditor] = fakePane.splitUp.argsForCall[0] - expect(newEditor.editSessions.length).toEqual 1 - expect(newEditor.activeEditSession.buffer).toBe editor.activeEditSession.buffer - newEditor.remove() - - describe "when not inside a pane", -> - it "does not split the editor, but doesn't throw an exception", -> - editor.splitUp().remove() - editor.splitDown().remove() - editor.splitLeft().remove() - editor.splitRight().remove() - describe "editor:attached event", -> it 'only triggers an editor:attached event when it is first added to the DOM', -> openHandler = jasmine.createSpy('openHandler') @@ -488,7 +222,7 @@ describe "Editor", -> editor.attachToDom() expect(openHandler).not.toHaveBeenCalled() - describe "editor-path-changed event", -> + describe "editor:path-changed event", -> path = null beforeEach -> path = "/tmp/something.txt" @@ -506,7 +240,7 @@ describe "Editor", -> it "emits event when editor receives a new buffer", -> eventHandler = jasmine.createSpy('eventHandler') editor.on 'editor:path-changed', eventHandler - editor.edit(project.buildEditSessionForPath(path)) + editor.edit(project.buildEditSession(path)) expect(eventHandler).toHaveBeenCalled() it "stops listening to events on previously set buffers", -> @@ -514,7 +248,7 @@ describe "Editor", -> oldBuffer = editor.getBuffer() editor.on 'editor:path-changed', eventHandler - editor.edit(project.buildEditSessionForPath(path)) + editor.edit(project.buildEditSession(path)) expect(eventHandler).toHaveBeenCalled() eventHandler.reset() @@ -541,30 +275,21 @@ describe "Editor", -> afterEach -> editor.clearFontFamily() - it "updates the font family on new and existing editors", -> - rootView.attachToDom() - rootView.height(200) - rootView.width(200) - - config.set("editor.fontFamily", "Courier") - newEditor = editor.splitRight() - - expect($("head style.editor-font-family").text()).toMatch "{font-family: Courier}" - expect(editor.css('font-family')).toBe 'Courier' - expect(newEditor.css('font-family')).toBe 'Courier' - it "updates the font family of editors and recalculates dimensions critical to cursor positioning", -> - rootView.attachToDom() - rootView.height(200) - rootView.width(200) - + editor.attachToDom(12) lineHeightBefore = editor.lineHeight charWidthBefore = editor.charWidth - config.set("editor.fontFamily", "Consolas") editor.setCursorScreenPosition [5, 6] + + config.set("editor.fontFamily", "PCMyungjo") + expect(editor.css('font-family')).toBe 'PCMyungjo' + expect($("head style.editor-font-family").text()).toMatch "{font-family: PCMyungjo}" expect(editor.charWidth).not.toBe charWidthBefore expect(editor.getCursorView().position()).toEqual { top: 5 * editor.lineHeight, left: 6 * editor.charWidth } - expect(editor.verticalScrollbarContent.height()).toBe buffer.getLineCount() * editor.lineHeight + + newEditor = new Editor(editor.activeEditSession.copy()) + newEditor.attachToDom() + expect(newEditor.css('font-family')).toBe 'PCMyungjo' describe "font size", -> beforeEach -> @@ -576,24 +301,9 @@ describe "Editor", -> expect($("head style.font-size").text()).toMatch "{font-size: #{config.get('editor.fontSize')}px}" describe "when the font size changes", -> - it "updates the font family on new and existing editors", -> - rootView.attachToDom() - rootView.height(200) - rootView.width(200) - - config.set("editor.fontSize", 20) - newEditor = editor.splitRight() - - expect($("head style.font-size").text()).toMatch "{font-size: 20px}" - expect(editor.css('font-size')).toBe '20px' - expect(newEditor.css('font-size')).toBe '20px' - it "updates the font sizes of editors and recalculates dimensions critical to cursor positioning", -> - rootView.attachToDom() - rootView.height(200) - rootView.width(200) - config.set("editor.fontSize", 10) + editor.attachToDom() lineHeightBefore = editor.lineHeight charWidthBefore = editor.charWidth editor.setCursorScreenPosition [5, 6] @@ -606,10 +316,14 @@ describe "Editor", -> expect(editor.renderedLines.outerHeight()).toBe buffer.getLineCount() * editor.lineHeight expect(editor.verticalScrollbarContent.height()).toBe buffer.getLineCount() * editor.lineHeight + newEditor = new Editor(editor.activeEditSession.copy()) + newEditor.attachToDom() + expect(editor.css('font-size')).toBe '30px' + it "updates the position and size of selection regions", -> - rootView.attachToDom() config.set("editor.fontSize", 10) editor.setSelectedBufferRange([[5, 2], [5, 7]]) + editor.attachToDom() config.set("editor.fontSize", 30) selectionRegion = editor.find('.region') @@ -619,7 +333,7 @@ describe "Editor", -> expect(selectionRegion.width()).toBe 5 * editor.charWidth it "updates the gutter width and font size", -> - rootView.attachToDom() + editor.attachToDom() config.set("editor.fontSize", 20) expect(editor.gutter.css('font-size')).toBe "20px" expect(editor.gutter.width()).toBe(editor.charWidth * 2 + editor.gutter.calculateLineNumberPadding()) @@ -632,22 +346,25 @@ describe "Editor", -> config.set("editor.fontSize", 10) expect(editor.renderedLines.find(".line").length).toBeGreaterThan originalLineCount - describe "when the editor is detached", -> + describe "when the font size changes while editor is detached", -> it "redraws the editor according to the new font size when it is reattached", -> - rootView.attachToDom() - rootView.height(200) - rootView.width(200) + editor.setCursorScreenPosition([4, 2]) + editor.attachToDom() + initialLineHeight = editor.lineHeight + initialCharWidth = editor.charWidth + initialCursorPosition = editor.getCursorView().position() + initialScrollbarHeight = editor.verticalScrollbarContent.height() + editor.detach() - newEditor = editor.splitRight() - newEditorParent = newEditor.parent() - newEditor.detach() config.set("editor.fontSize", 10) - newEditorParent.append(newEditor) + expect(editor.lineHeight).toBe initialLineHeight + expect(editor.charWidth).toBe initialCharWidth - expect(newEditor.lineHeight).toBe editor.lineHeight - expect(newEditor.charWidth).toBe editor.charWidth - expect(newEditor.getCursorView().position()).toEqual editor.getCursorView().position() - expect(newEditor.verticalScrollbarContent.height()).toBe editor.verticalScrollbarContent.height() + editor.attachToDom() + expect(editor.lineHeight).not.toBe initialLineHeight + expect(editor.charWidth).not.toBe initialCharWidth + expect(editor.getCursorView().position()).not.toEqual initialCursorPosition + expect(editor.verticalScrollbarContent.height()).not.toBe initialScrollbarHeight describe "mouse events", -> beforeEach -> @@ -1375,7 +1092,7 @@ describe "Editor", -> expect(editor.bufferPositionForScreenPosition(editor.getCursorScreenPosition())).toEqual [3, 60] it "does not wrap the lines of any newly assigned buffers", -> - otherEditSession = project.buildEditSessionForPath() + otherEditSession = project.buildEditSession() otherEditSession.buffer.setText([1..100].join('')) editor.edit(otherEditSession) expect(editor.renderedLines.find('.line').length).toBe(1) @@ -1411,7 +1128,7 @@ describe "Editor", -> expect(editor.getCursorScreenPosition()).toEqual [11, 0] it "calls .setSoftWrapColumn() when the editor is attached because now its dimensions are available to calculate it", -> - otherEditor = new Editor(editSession: project.buildEditSessionForPath('sample.js')) + otherEditor = new Editor(editSession: project.buildEditSession('sample.js')) spyOn(otherEditor, 'setSoftWrapColumn') otherEditor.setSoftWrap(true) @@ -1419,6 +1136,7 @@ describe "Editor", -> otherEditor.simulateDomAttachment() expect(otherEditor.setSoftWrapColumn).toHaveBeenCalled() + otherEditor.remove() describe "when some lines at the end of the buffer are not visible on screen", -> beforeEach -> @@ -1707,7 +1425,7 @@ describe "Editor", -> describe "when autoscrolling at the end of the document", -> it "renders lines properly", -> - editor.edit(project.buildEditSessionForPath('two-hundred.txt')) + editor.edit(project.buildEditSession('two-hundred.txt')) editor.attachToDom(heightInLines: 5.5) expect(editor.renderedLines.find('.line').length).toBe 8 @@ -1718,7 +1436,7 @@ describe "Editor", -> describe "when line has a character that could push it to be too tall (regression)", -> it "does renders the line at a consistent height", -> - rootView.attachToDom() + editor.attachToDom() buffer.insert([0, 0], "–") expect(editor.find('.line:eq(0)').outerHeight()).toBe editor.find('.line:eq(1)').outerHeight() @@ -1749,21 +1467,11 @@ describe "Editor", -> expect(editor.find('.line').html()).toBe 'var' it "allows invisible glyphs to be customized via config.editor.invisibles", -> - rootView.height(200) - rootView.attachToDom() - rightEditor = rootView.getActiveEditor() - rightEditor.setText(" \t ") - leftEditor = rightEditor.splitLeft() - - config.set "editor.showInvisibles", true - config.set "editor.invisibles", - eol: ";" - space: "_" - tab: "tab" - config.update() - - expect(rightEditor.find(".line:first").text()).toBe "_tab _;" - expect(leftEditor.find(".line:first").text()).toBe "_tab _;" + editor.setText(" \t ") + editor.attachToDom() + config.set("editor.showInvisibles", true) + config.set("editor.invisibles", eol: ";", space: "_", tab: "tab") + expect(editor.find(".line:first").text()).toBe "_tab _;" it "displays trailing carriage return using a visible non-empty value", -> editor.setText "a line that ends with a carriage return\r\n" @@ -1988,11 +1696,14 @@ describe "Editor", -> describe "when the switching from an edit session for a long buffer to an edit session for a short buffer", -> it "updates the line numbers to reflect the shorter buffer", -> - editor.edit(fixturesProject.buildEditSessionForPath(null)) + emptyEditSession = project.buildEditSession(null) + editor.edit(emptyEditSession) expect(editor.gutter.lineNumbers.find('.line-number').length).toBe 1 - editor.setActiveEditSessionIndex(0) - editor.setActiveEditSessionIndex(1) + editor.edit(editSession) + expect(editor.gutter.lineNumbers.find('.line-number').length).toBeGreaterThan 1 + + editor.edit(emptyEditSession) expect(editor.gutter.lineNumbers.find('.line-number').length).toBe 1 describe "when the editor is mini", -> @@ -2122,7 +1833,7 @@ describe "Editor", -> describe "folding", -> beforeEach -> - editSession = project.buildEditSessionForPath('two-hundred.txt') + editSession = project.buildEditSession('two-hundred.txt') buffer = editSession.buffer editor.edit(editSession) editor.attachToDom() @@ -2211,14 +1922,6 @@ describe "Editor", -> editor.scrollTop(0) expect(editor.lineElementForScreenRow(2)).toMatchSelector('.fold.selected') - describe ".getOpenBufferPaths()", -> - it "returns the paths of all non-anonymous buffers with edit sessions on this editor", -> - editor.edit(project.buildEditSessionForPath('sample.txt')) - editor.edit(project.buildEditSessionForPath('two-hundred.txt')) - editor.edit(project.buildEditSessionForPath()) - paths = editor.getOpenBufferPaths().map (path) -> project.relativize(path) - expect(paths).toEqual = ['sample.js', 'sample.txt', 'two-hundred.txt'] - describe "paging up and down", -> beforeEach -> editor.attachToDom() @@ -2260,37 +1963,20 @@ describe "Editor", -> expect(editor.getCursor().getScreenPosition().row).toBe(0) expect(editor.getFirstVisibleScreenRow()).toBe(0) - describe "when autosave is enabled", -> - it "autosaves the current buffer when the editor loses focus or switches edit sessions", -> - config.set "editor.autosave", true - rootView.attachToDom() - editor2 = editor.splitRight() - spyOn(editor2.activeEditSession, 'save') - - editor.focus() - expect(editor2.activeEditSession.save).toHaveBeenCalled() - - editSession = editor.activeEditSession - spyOn(editSession, 'save') - rootView.open('sample.txt') - expect(editSession.save).toHaveBeenCalled() - describe ".checkoutHead()", -> [path, originalPathText] = [] beforeEach -> - path = require.resolve('fixtures/git/working-dir/file.txt') + path = project.resolve('git/working-dir/file.txt') originalPathText = fs.read(path) - rootView.open(path) - editor = rootView.getActiveEditor() - editor.attachToDom() + editor.edit(project.buildEditSession(path)) afterEach -> fs.write(path, originalPathText) it "restores the contents of the editor to the HEAD revision", -> editor.setText('') - editor.save() + editor.getBuffer().save() fileChangeHandler = jasmine.createSpy('fileChange') editor.getBuffer().file.on 'contents-changed', fileChangeHandler @@ -2344,7 +2030,7 @@ describe "Editor", -> describe "when clicking below the last line", -> beforeEach -> - rootView.attachToDom() + editor.attachToDom() it "move the cursor to the end of the file", -> expect(editor.getCursorScreenPosition()).toEqual [0,0] @@ -2361,68 +2047,19 @@ describe "Editor", -> editor.underlayer.trigger event expect(editor.getSelection().getScreenRange()).toEqual [[0,0], [12,2]] - describe ".destroyEditSessionIndex(index)", -> - it "prompts to save dirty buffers before closing", -> - editor.setText("I'm dirty") - rootView.open('sample.txt') - expect(editor.getEditSessions().length).toBe 2 - spyOn(atom, "confirm") - editor.destroyEditSessionIndex(0) - expect(atom.confirm).toHaveBeenCalled() - expect(editor.getEditSessions().length).toBe 2 - expect(editor.getEditSessions()[0].buffer.isModified()).toBeTruthy() - - describe ".destroyInactiveEditSessions()", -> - it "destroys every edit session except the active one", -> - rootView.open('sample.txt') - cssSession = rootView.open('css.css') - rootView.open('coffee.coffee') - rootView.open('hello.rb') - expect(editor.getEditSessions().length).toBe 5 - editor.setActiveEditSessionIndex(2) - editor.destroyInactiveEditSessions() - expect(editor.getActiveEditSessionIndex()).toBe 0 - expect(editor.getEditSessions().length).toBe 1 - expect(editor.getEditSessions()[0]).toBe cssSession - - it "prompts to save dirty buffers before destroying", -> - editor.setText("I'm dirty") - dirtySession = editor.activeEditSession - rootView.open('sample.txt') - expect(editor.getEditSessions().length).toBe 2 - spyOn(atom, "confirm") - editor.destroyInactiveEditSessions() - expect(atom.confirm).toHaveBeenCalled() - expect(editor.getEditSessions().length).toBe 2 - expect(editor.getEditSessions()[0].buffer.isModified()).toBeTruthy() - - describe ".destroyAllEditSessions()", -> - it "destroys every edit session", -> - rootView.open('sample.txt') - rootView.open('css.css') - rootView.open('coffee.coffee') - rootView.open('hello.rb') - expect(editor.getEditSessions().length).toBe 5 - editor.setActiveEditSessionIndex(2) - editor.destroyAllEditSessions() - expect(editor.pane()).toBeUndefined() - expect(editor.getEditSessions().length).toBe 0 - describe ".reloadGrammar()", -> [path] = [] beforeEach -> path = "/tmp/grammar-change.txt" fs.write(path, "var i;") - rootView.attachToDom() afterEach -> - project.removeGrammarOverrideForPath(path) fs.remove(path) if fs.exists(path) it "updates all the rendered lines when the grammar changes", -> - rootView.open(path) - editor = rootView.getActiveEditor() + editor.edit(project.buildEditSession(path)) + expect(editor.getGrammar().name).toBe 'Plain Text' jsGrammar = syntax.grammarForFilePath('/tmp/js.js') expect(jsGrammar.name).toBe 'JavaScript' @@ -2436,12 +2073,6 @@ describe "Editor", -> expect(line0.tokens.length).toBe 3 expect(line0.tokens[0]).toEqual(value: 'var', scopes: ['source.js', 'storage.modifier.js']) - line0 = editor.renderedLines.find('.line:first') - span0 = line0.children('span:eq(0)') - expect(span0).toMatchSelector '.source.js' - expect(span0.children('span:eq(0)')).toMatchSelector '.storage.modifier.js' - expect(span0.children('span:eq(0)').text()).toBe 'var' - it "doesn't update the rendered lines when the grammar doesn't change", -> expect(editor.getGrammar().name).toBe 'JavaScript' spyOn(editor, 'updateDisplay').andCallThrough() @@ -2451,8 +2082,8 @@ describe "Editor", -> expect(editor.getGrammar().name).toBe 'JavaScript' it "emits an editor:grammar-changed event when updated", -> - rootView.open(path) - editor = rootView.getActiveEditor() + editor.edit(project.buildEditSession(path)) + eventHandler = jasmine.createSpy('eventHandler') editor.on('editor:grammar-changed', eventHandler) editor.reloadGrammar() @@ -2770,104 +2401,6 @@ describe "Editor", -> expect(buffer.lineForRow(15)).toBeUndefined() expect(editor.getCursorBufferPosition()).toEqual [13, 0] - describe ".moveEditSessionToIndex(fromIndex, toIndex)", -> - describe "when the edit session moves to a later index", -> - it "updates the edit session order", -> - jsPath = editor.getPath() - rootView.open("sample.txt") - txtPath = editor.getPath() - expect(editor.editSessions[0].getPath()).toBe jsPath - expect(editor.editSessions[1].getPath()).toBe txtPath - editor.moveEditSessionToIndex(0, 1) - expect(editor.editSessions[0].getPath()).toBe txtPath - expect(editor.editSessions[1].getPath()).toBe jsPath - - it "fires an editor:edit-session-order-changed event", -> - eventHandler = jasmine.createSpy("eventHandler") - rootView.open("sample.txt") - editor.on "editor:edit-session-order-changed", eventHandler - editor.moveEditSessionToIndex(0, 1) - expect(eventHandler).toHaveBeenCalled() - - it "sets the moved session as the editor's active session", -> - jsPath = editor.getPath() - rootView.open("sample.txt") - txtPath = editor.getPath() - expect(editor.activeEditSession.getPath()).toBe txtPath - editor.moveEditSessionToIndex(0, 1) - expect(editor.activeEditSession.getPath()).toBe jsPath - - describe "when the edit session moves to an earlier index", -> - it "updates the edit session order", -> - jsPath = editor.getPath() - rootView.open("sample.txt") - txtPath = editor.getPath() - expect(editor.editSessions[0].getPath()).toBe jsPath - expect(editor.editSessions[1].getPath()).toBe txtPath - editor.moveEditSessionToIndex(1, 0) - expect(editor.editSessions[0].getPath()).toBe txtPath - expect(editor.editSessions[1].getPath()).toBe jsPath - - it "fires an editor:edit-session-order-changed event", -> - eventHandler = jasmine.createSpy("eventHandler") - rootView.open("sample.txt") - editor.on "editor:edit-session-order-changed", eventHandler - editor.moveEditSessionToIndex(1, 0) - expect(eventHandler).toHaveBeenCalled() - - it "sets the moved session as the editor's active session", -> - jsPath = editor.getPath() - rootView.open("sample.txt") - txtPath = editor.getPath() - expect(editor.activeEditSession.getPath()).toBe txtPath - editor.moveEditSessionToIndex(1, 0) - expect(editor.activeEditSession.getPath()).toBe txtPath - - describe ".moveEditSessionToEditor(fromIndex, toEditor, toIndex)", -> - it "closes the edit session in the source editor", -> - jsPath = editor.getPath() - rootView.open("sample.txt") - txtPath = editor.getPath() - rightEditor = editor.splitRight() - expect(editor.editSessions[0].getPath()).toBe jsPath - expect(editor.editSessions[1].getPath()).toBe txtPath - editor.moveEditSessionToEditor(0, rightEditor, 1) - expect(editor.editSessions[0].getPath()).toBe txtPath - expect(editor.editSessions[1]).toBeUndefined() - - it "opens the edit session in the destination editor at the target index", -> - jsPath = editor.getPath() - rootView.open("sample.txt") - txtPath = editor.getPath() - rightEditor = editor.splitRight() - expect(rightEditor.editSessions[0].getPath()).toBe txtPath - expect(rightEditor.editSessions[1]).toBeUndefined() - editor.moveEditSessionToEditor(0, rightEditor, 0) - expect(rightEditor.editSessions[0].getPath()).toBe jsPath - expect(rightEditor.editSessions[1].getPath()).toBe txtPath - - describe "when editor:undo-close-session is triggered", -> - describe "when an edit session is opened back up after it is closed", -> - it "is removed from the undo stack and not reopened when the event is triggered", -> - rootView.open('sample.txt') - expect(editor.getPath()).toBe fixturesProject.resolve('sample.txt') - editor.trigger "core:close" - expect(editor.closedEditSessions.length).toBe 1 - rootView.open('sample.txt') - expect(editor.closedEditSessions.length).toBe 0 - editor.trigger 'editor:undo-close-session' - expect(editor.getPath()).toBe fixturesProject.resolve('sample.txt') - - it "opens the closed session back up at the previous index", -> - rootView.open('sample.txt') - editor.loadPreviousEditSession() - expect(editor.getPath()).toBe fixturesProject.resolve('sample.js') - editor.trigger "core:close" - expect(editor.getPath()).toBe fixturesProject.resolve('sample.txt') - editor.trigger 'editor:undo-close-session' - expect(editor.getPath()).toBe fixturesProject.resolve('sample.js') - expect(editor.getActiveEditSessionIndex()).toBe 0 - describe "editor:save-debug-snapshot", -> it "saves the state of the rendered lines, the display buffer, and the buffer to a file of the user's choosing", -> saveDialogCallback = null diff --git a/spec/app/git-spec.coffee b/spec/app/git-spec.coffee index 36ca195e1..fcdb120d8 100644 --- a/spec/app/git-spec.coffee +++ b/spec/app/git-spec.coffee @@ -1,5 +1,6 @@ Git = require 'git' fs = require 'fs' +Task = require 'task' describe "Git", -> repo = null @@ -188,10 +189,10 @@ describe "Git", -> beforeEach -> repo = new Git(require.resolve('fixtures/git/working-dir')) - modifiedPath = fixturesProject.resolve('git/working-dir/file.txt') + modifiedPath = project.resolve('git/working-dir/file.txt') originalModifiedPathText = fs.read(modifiedPath) - newPath = fixturesProject.resolve('git/working-dir/untracked.txt') - cleanPath = fixturesProject.resolve('git/working-dir/other.txt') + newPath = project.resolve('git/working-dir/untracked.txt') + cleanPath = project.resolve('git/working-dir/other.txt') fs.write(newPath, '') afterEach -> @@ -212,3 +213,22 @@ describe "Git", -> expect(statuses[cleanPath]).toBeUndefined() expect(repo.isStatusNew(statuses[newPath])).toBeTruthy() expect(repo.isStatusModified(statuses[modifiedPath])).toBeTruthy() + + it "only starts a single web worker at a time and schedules a restart if one is already running", => + fs.write(modifiedPath, 'making this path modified') + statusHandler = jasmine.createSpy('statusHandler') + repo.on 'statuses-changed', statusHandler + + spyOn(Task.prototype, "start").andCallThrough() + repo.refreshStatus() + expect(Task.prototype.start.callCount).toBe 1 + repo.refreshStatus() + expect(Task.prototype.start.callCount).toBe 1 + repo.refreshStatus() + expect(Task.prototype.start.callCount).toBe 1 + + waitsFor -> + statusHandler.callCount > 0 + + runs -> + expect(Task.prototype.start.callCount).toBe 2 diff --git a/spec/app/grammar-view-spec.coffee b/spec/app/grammar-view-spec.coffee index afa03839d..45157aab0 100644 --- a/spec/app/grammar-view-spec.coffee +++ b/spec/app/grammar-view-spec.coffee @@ -7,10 +7,8 @@ describe "GrammarView", -> beforeEach -> window.rootView = new RootView - project.removeGrammarOverrideForPath('sample.js') rootView.open('sample.js') - editor = rootView.getActiveEditor() - rootView.attachToDom() + editor = rootView.getActiveView() textGrammar = _.find syntax.grammars, (grammar) -> grammar.name is 'Plain Text' expect(textGrammar).toBeTruthy() jsGrammar = _.find syntax.grammars, (grammar) -> grammar.name is 'JavaScript' diff --git a/spec/app/language-mode-spec.coffee b/spec/app/language-mode-spec.coffee index a68f778dc..5b2704947 100644 --- a/spec/app/language-mode-spec.coffee +++ b/spec/app/language-mode-spec.coffee @@ -10,18 +10,18 @@ describe "LanguageMode", -> describe "common behavior", -> beforeEach -> - editSession = fixturesProject.buildEditSessionForPath('sample.js', autoIndent: false) + editSession = project.buildEditSession('sample.js', autoIndent: false) { buffer, languageMode } = editSession describe "language detection", -> it "uses the file name as the file type if it has no extension", -> - jsEditSession = fixturesProject.buildEditSessionForPath('js', autoIndent: false) + jsEditSession = project.buildEditSession('js', autoIndent: false) expect(jsEditSession.languageMode.grammar.name).toBe "JavaScript" jsEditSession.destroy() describe "javascript", -> beforeEach -> - editSession = fixturesProject.buildEditSessionForPath('sample.js', autoIndent: false) + editSession = project.buildEditSession('sample.js', autoIndent: false) { buffer, languageMode } = editSession describe ".toggleLineCommentsForBufferRows(start, end)", -> @@ -63,7 +63,7 @@ describe "LanguageMode", -> describe "coffeescript", -> beforeEach -> - editSession = fixturesProject.buildEditSessionForPath('coffee.coffee', autoIndent: false) + editSession = project.buildEditSession('coffee.coffee', autoIndent: false) { buffer, languageMode } = editSession describe ".toggleLineCommentsForBufferRows(start, end)", -> @@ -98,7 +98,7 @@ describe "LanguageMode", -> describe "css", -> beforeEach -> - editSession = fixturesProject.buildEditSessionForPath('css.css', autoIndent: false) + editSession = project.buildEditSession('css.css', autoIndent: false) { buffer, languageMode } = editSession describe ".toggleLineCommentsForBufferRows(start, end)", -> diff --git a/spec/app/pane-container-spec.coffee b/spec/app/pane-container-spec.coffee new file mode 100644 index 000000000..56ea53336 --- /dev/null +++ b/spec/app/pane-container-spec.coffee @@ -0,0 +1,148 @@ +PaneContainer = require 'pane-container' +Pane = require 'pane' +{View, $$} = require 'space-pen' +_ = require 'underscore' +$ = require 'jquery' + +describe "PaneContainer", -> + [TestView, container, pane1, pane2, pane3] = [] + + beforeEach -> + class TestView extends View + registerDeserializer(this) + @deserialize: ({name}) -> new TestView(name) + @content: -> @div tabindex: -1 + initialize: (@name) -> @text(@name) + serialize: -> { deserializer: 'TestView', @name } + getUri: -> "/tmp/#{@name}" + save: -> @saved = true + isEqual: (other) -> @name is other.name + + container = new PaneContainer + pane1 = new Pane(new TestView('1')) + container.append(pane1) + pane2 = pane1.splitRight(new TestView('2')) + pane3 = pane2.splitDown(new TestView('3')) + + afterEach -> + unregisterDeserializer(TestView) + + describe ".focusNextPane()", -> + it "focuses the pane following the focused pane or the first pane if no pane has focus", -> + container.attachToDom() + container.focusNextPane() + expect(pane1.activeItem).toMatchSelector ':focus' + container.focusNextPane() + expect(pane2.activeItem).toMatchSelector ':focus' + container.focusNextPane() + expect(pane3.activeItem).toMatchSelector ':focus' + container.focusNextPane() + expect(pane1.activeItem).toMatchSelector ':focus' + + describe ".getActivePane()", -> + it "returns the most-recently focused pane", -> + focusStealer = $$ -> @div tabindex: -1, "focus stealer" + focusStealer.attachToDom() + container.attachToDom() + + pane2.focus() + expect(container.getFocusedPane()).toBe pane2 + expect(container.getActivePane()).toBe pane2 + + focusStealer.focus() + expect(container.getFocusedPane()).toBeUndefined() + expect(container.getActivePane()).toBe pane2 + + pane3.focus() + expect(container.getFocusedPane()).toBe pane3 + expect(container.getActivePane()).toBe pane3 + + # returns the first pane if none have been set to active + container.find('.pane.active').removeClass('active') + expect(container.getActivePane()).toBe pane1 + + describe ".eachPane(callback)", -> + it "runs the callback with all current and future panes until the subscription is cancelled", -> + panes = [] + subscription = container.eachPane (pane) -> panes.push(pane) + expect(panes).toEqual [pane1, pane2, pane3] + + panes = [] + pane4 = pane3.splitRight() + expect(panes).toEqual [pane4] + + panes = [] + subscription.cancel() + pane4.splitDown() + expect(panes).toEqual [] + + describe ".reopenItem()", -> + describe "when there is an active pane", -> + it "reconstructs and shows the last-closed pane item", -> + expect(container.getActivePane()).toBe pane3 + item3 = pane3.activeItem + item4 = new TestView('4') + pane3.showItem(item4) + + pane3.destroyItem(item3) + pane3.destroyItem(item4) + expect(container.getActivePane()).toBe pane1 + + expect(container.reopenItem()).toBeTruthy() + expect(pane1.activeItem).toEqual item4 + + expect(container.reopenItem()).toBeTruthy() + expect(pane1.activeItem).toEqual item3 + + expect(container.reopenItem()).toBeFalsy() + expect(pane1.activeItem).toEqual item3 + + describe "when there is no active pane", -> + it "attaches a new pane with the reconstructed last pane item", -> + pane1.remove() + pane2.remove() + item3 = pane3.activeItem + pane3.destroyItem(item3) + expect(container.getActivePane()).toBeUndefined() + + container.reopenItem() + + expect(container.getActivePane().activeItem).toEqual item3 + + it "does not reopen an item that is already open", -> + item3 = pane3.activeItem + item4 = new TestView('4') + pane3.showItem(item4) + pane3.destroyItem(item3) + pane3.destroyItem(item4) + + expect(container.getActivePane()).toBe pane1 + pane1.showItem(new TestView('4')) + + expect(container.reopenItem()).toBeTruthy() + expect(_.pluck(pane1.getItems(), 'name')).toEqual ['1', '4', '3'] + expect(pane1.activeItem).toEqual item3 + + expect(container.reopenItem()).toBeFalsy() + expect(pane1.activeItem).toEqual item3 + + describe ".saveAll()", -> + it "saves all open pane items", -> + pane1.showItem(new TestView('4')) + + container.saveAll() + + for pane in container.getPanes() + for item in pane.getItems() + expect(item.saved).toBeTruthy() + + describe "serialization", -> + it "can be serialized and deserialized, and correctly adjusts dimensions of deserialized panes after attach", -> + newContainer = deserialize(container.serialize()) + expect(newContainer.find('.row > :contains(1)')).toExist() + expect(newContainer.find('.row > .column > :contains(2)')).toExist() + expect(newContainer.find('.row > .column > :contains(3)')).toExist() + + newContainer.height(200).width(300).attachToDom() + expect(newContainer.find('.row > :contains(1)').width()).toBe 150 + expect(newContainer.find('.row > .column > :contains(2)').height()).toBe 100 diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee new file mode 100644 index 000000000..3c62fc797 --- /dev/null +++ b/spec/app/pane-spec.coffee @@ -0,0 +1,693 @@ +PaneContainer = require 'pane-container' +Pane = require 'pane' +{$$} = require 'space-pen' +$ = require 'jquery' + +describe "Pane", -> + [container, view1, view2, editSession1, editSession2, pane] = [] + + beforeEach -> + container = new PaneContainer + view1 = $$ -> @div id: 'view-1', tabindex: -1, 'View 1' + view2 = $$ -> @div id: 'view-2', tabindex: -1, 'View 2' + editSession1 = project.buildEditSession('sample.js') + editSession2 = project.buildEditSession('sample.txt') + pane = new Pane(view1, editSession1, view2, editSession2) + container.append(pane) + + describe ".initialize(items...)", -> + it "displays the first item in the pane", -> + expect(pane.itemViews.find('#view-1')).toExist() + + describe ".showItem(item)", -> + it "hides all item views except the one being shown and sets the activeItem", -> + expect(pane.activeItem).toBe view1 + pane.showItem(view2) + expect(view1.css('display')).toBe 'none' + expect(view2.css('display')).toBe '' + expect(pane.activeItem).toBe view2 + + it "triggers 'pane:active-item-changed' if the item isn't already the activeItem", -> + pane.makeActive() + itemChangedHandler = jasmine.createSpy("itemChangedHandler") + container.on 'pane:active-item-changed', itemChangedHandler + + expect(pane.activeItem).toBe view1 + pane.showItem(view2) + pane.showItem(view2) + expect(itemChangedHandler.callCount).toBe 1 + expect(itemChangedHandler.argsForCall[0][1]).toBe view2 + itemChangedHandler.reset() + + pane.showItem(editSession1) + expect(itemChangedHandler).toHaveBeenCalled() + expect(itemChangedHandler.argsForCall[0][1]).toBe editSession1 + itemChangedHandler.reset() + + describe "if the pane's active view is focused before calling showItem", -> + it "focuses the new active view", -> + container.attachToDom() + pane.focus() + expect(pane.activeView).not.toBe view2 + expect(pane.activeView).toMatchSelector ':focus' + pane.showItem(view2) + expect(view2).toMatchSelector ':focus' + + describe "when the given item isn't yet in the items list on the pane", -> + view3 = null + beforeEach -> + view3 = $$ -> @div id: 'view-3', "View 3" + pane.showItem(editSession1) + expect(pane.getActiveItemIndex()).toBe 1 + + it "adds it to the items list after the active item", -> + pane.showItem(view3) + expect(pane.getItems()).toEqual [view1, editSession1, view3, view2, editSession2] + expect(pane.activeItem).toBe view3 + expect(pane.getActiveItemIndex()).toBe 2 + + it "triggers the 'item-added' event with the item and its index before the 'active-item-changed' event", -> + events = [] + container.on 'pane:item-added', (e, item, index) -> events.push(['pane:item-added', item, index]) + container.on 'pane:active-item-changed', (e, item) -> events.push(['pane:active-item-changed', item]) + pane.showItem(view3) + expect(events).toEqual [['pane:item-added', view3, 2], ['pane:active-item-changed', view3]] + + describe "when showing a model item", -> + describe "when no view has yet been appended for that item", -> + it "appends and shows a view to display the item based on its `.getViewClass` method", -> + pane.showItem(editSession1) + editor = pane.activeView + expect(editor.css('display')).toBe '' + expect(editor.activeEditSession).toBe editSession1 + + describe "when a valid view has already been appended for another item", -> + it "recycles the existing view by assigning the selected item to it", -> + pane.showItem(editSession1) + pane.showItem(editSession2) + expect(pane.itemViews.find('.editor').length).toBe 1 + editor = pane.activeView + expect(editor.css('display')).toBe '' + expect(editor.activeEditSession).toBe editSession2 + + describe "when showing a view item", -> + it "appends it to the itemViews div if it hasn't already been appended and shows it", -> + expect(pane.itemViews.find('#view-2')).not.toExist() + pane.showItem(view2) + expect(pane.itemViews.find('#view-2')).toExist() + expect(pane.activeView).toBe view2 + + describe ".destroyItem(item)", -> + describe "if the item is not modified", -> + it "removes the item and tries to call destroy on it", -> + pane.destroyItem(editSession2) + expect(pane.getItems().indexOf(editSession2)).toBe -1 + expect(editSession2.destroyed).toBeTruthy() + + describe "if the item is modified", -> + beforeEach -> + spyOn(atom, 'confirm') + spyOn(atom, 'showSaveDialog') + spyOn(editSession2, 'save') + spyOn(editSession2, 'saveAs') + + atom.confirm.selectOption = (buttonText) -> + for arg, i in @argsForCall[0] when arg is buttonText + @argsForCall[0][i + 1]?() + + editSession2.insertText('a') + expect(editSession2.isModified()).toBeTruthy() + pane.destroyItem(editSession2) + + it "presents a dialog with the option to save the item first", -> + expect(atom.confirm).toHaveBeenCalled() + expect(pane.getItems().indexOf(editSession2)).not.toBe -1 + expect(editSession2.destroyed).toBeFalsy() + + describe "if the [Save] option is selected", -> + describe "when the item has a uri", -> + it "saves the item before removing and destroying it", -> + atom.confirm.selectOption('Save') + + expect(editSession2.save).toHaveBeenCalled() + expect(pane.getItems().indexOf(editSession2)).toBe -1 + expect(editSession2.destroyed).toBeTruthy() + + describe "when the item has no uri", -> + it "presents a save-as dialog, then saves the item with the given uri before removing and destroying it", -> + editSession2.buffer.setPath(undefined) + + atom.confirm.selectOption('Save') + + expect(atom.showSaveDialog).toHaveBeenCalled() + + atom.showSaveDialog.argsForCall[0][0]("/selected/path") + + expect(editSession2.saveAs).toHaveBeenCalledWith("/selected/path") + expect(pane.getItems().indexOf(editSession2)).toBe -1 + expect(editSession2.destroyed).toBeTruthy() + + describe "if the [Don't Save] option is selected", -> + it "removes and destroys the item without saving it", -> + atom.confirm.selectOption("Don't Save") + + expect(editSession2.save).not.toHaveBeenCalled() + expect(pane.getItems().indexOf(editSession2)).toBe -1 + expect(editSession2.destroyed).toBeTruthy() + + describe "if the [Cancel] option is selected", -> + it "does not save, remove, or destroy the item", -> + atom.confirm.selectOption("Cancel") + + expect(editSession2.save).not.toHaveBeenCalled() + expect(pane.getItems().indexOf(editSession2)).not.toBe -1 + expect(editSession2.destroyed).toBeFalsy() + + describe ".removeItem(item)", -> + it "removes the item from the items list and shows the next item if it was showing", -> + pane.removeItem(view1) + expect(pane.getItems()).toEqual [editSession1, view2, editSession2] + expect(pane.activeItem).toBe editSession1 + + pane.showItem(editSession2) + pane.removeItem(editSession2) + expect(pane.getItems()).toEqual [editSession1, view2] + expect(pane.activeItem).toBe editSession1 + + it "triggers 'pane:item-removed' with the item and its former index", -> + itemRemovedHandler = jasmine.createSpy("itemRemovedHandler") + pane.on 'pane:item-removed', itemRemovedHandler + pane.removeItem(editSession1) + expect(itemRemovedHandler).toHaveBeenCalled() + expect(itemRemovedHandler.argsForCall[0][1..2]).toEqual [editSession1, 1] + + describe "when removing the last item", -> + it "removes the pane", -> + pane.removeItem(item) for item in pane.getItems() + expect(pane.hasParent()).toBeFalsy() + + describe "when the pane is focused", -> + it "shifts focus to the next pane", -> + container.attachToDom() + pane2 = pane.splitRight($$ -> @div class: 'view-3', tabindex: -1, 'View 3') + pane.focus() + expect(pane).toMatchSelector(':has(:focus)') + pane.removeItem(item) for item in pane.getItems() + expect(pane2).toMatchSelector ':has(:focus)' + + describe "when the item is a view", -> + it "removes the item from the 'item-views' div", -> + expect(view1.parent()).toMatchSelector pane.itemViews + pane.removeItem(view1) + expect(view1.parent()).not.toMatchSelector pane.itemViews + + describe "when the item is a model", -> + it "removes the associated view only when all items that require it have been removed", -> + pane.showItem(editSession2) + pane.removeItem(editSession2) + expect(pane.itemViews.find('.editor')).toExist() + pane.removeItem(editSession1) + expect(pane.itemViews.find('.editor')).not.toExist() + + describe ".moveItem(item, index)", -> + it "moves the item to the given index and emits a 'pane:item-moved' event with the item and the new index", -> + itemMovedHandler = jasmine.createSpy("itemMovedHandler") + pane.on 'pane:item-moved', itemMovedHandler + + pane.moveItem(view1, 2) + expect(pane.getItems()).toEqual [editSession1, view2, view1, editSession2] + expect(itemMovedHandler).toHaveBeenCalled() + expect(itemMovedHandler.argsForCall[0][1..2]).toEqual [view1, 2] + itemMovedHandler.reset() + + pane.moveItem(editSession1, 3) + expect(pane.getItems()).toEqual [view2, view1, editSession2, editSession1] + expect(itemMovedHandler).toHaveBeenCalled() + expect(itemMovedHandler.argsForCall[0][1..2]).toEqual [editSession1, 3] + itemMovedHandler.reset() + + pane.moveItem(editSession1, 1) + expect(pane.getItems()).toEqual [view2, editSession1, view1, editSession2] + expect(itemMovedHandler).toHaveBeenCalled() + expect(itemMovedHandler.argsForCall[0][1..2]).toEqual [editSession1, 1] + itemMovedHandler.reset() + + describe ".moveItemToPane(item, pane, index)", -> + [pane2, view3] = [] + + beforeEach -> + view3 = $$ -> @div id: 'view-3', "View 3" + pane2 = pane.splitRight(view3) + + it "moves the item to the given pane at the given index", -> + pane.moveItemToPane(view1, pane2, 1) + expect(pane.getItems()).toEqual [editSession1, view2, editSession2] + expect(pane2.getItems()).toEqual [view3, view1] + + describe "when it is the last item on the source pane", -> + it "removes the source pane, but does not destroy the item", -> + pane.removeItem(view1) + pane.removeItem(view2) + pane.removeItem(editSession2) + + expect(pane.getItems()).toEqual [editSession1] + pane.moveItemToPane(editSession1, pane2, 1) + + expect(pane.hasParent()).toBeFalsy() + expect(pane2.getItems()).toEqual [view3, editSession1] + expect(editSession1.destroyed).toBeFalsy() + + describe "core:close", -> + it "destroys the active item and does not bubble the event", -> + containerCloseHandler = jasmine.createSpy("containerCloseHandler") + container.on 'core:close', containerCloseHandler + + pane.showItem(editSession1) + initialItemCount = pane.getItems().length + pane.trigger 'core:close' + expect(pane.getItems().length).toBe initialItemCount - 1 + expect(editSession1.destroyed).toBeTruthy() + + expect(containerCloseHandler).not.toHaveBeenCalled() + + describe "pane:close", -> + it "destroys all items and removes the pane", -> + pane.showItem(editSession1) + pane.trigger 'pane:close' + expect(pane.hasParent()).toBeFalsy() + expect(editSession2.destroyed).toBeTruthy() + expect(editSession1.destroyed).toBeTruthy() + + describe "pane:close-other-items", -> + it "destroys all items except the current", -> + pane.showItem(editSession1) + pane.trigger 'pane:close-other-items' + expect(editSession2.destroyed).toBeTruthy() + expect(pane.getItems()).toEqual [editSession1] + + describe "core:save", -> + describe "when the current item has a uri", -> + describe "when the current item has a save method", -> + it "saves the current item", -> + spyOn(editSession2, 'save') + pane.showItem(editSession2) + pane.trigger 'core:save' + expect(editSession2.save).toHaveBeenCalled() + + describe "when the current item has no save method", -> + it "does nothing", -> + expect(pane.activeItem.save).toBeUndefined() + pane.trigger 'core:save' + + describe "when the current item has no uri", -> + beforeEach -> + spyOn(atom, 'showSaveDialog') + + describe "when the current item has a saveAs method", -> + it "opens a save dialog and saves the current item as the selected path", -> + spyOn(editSession2, 'saveAs') + editSession2.buffer.setPath(undefined) + pane.showItem(editSession2) + + pane.trigger 'core:save' + + expect(atom.showSaveDialog).toHaveBeenCalled() + atom.showSaveDialog.argsForCall[0][0]('/selected/path') + expect(editSession2.saveAs).toHaveBeenCalledWith('/selected/path') + + describe "when the current item has no saveAs method", -> + it "does nothing", -> + expect(pane.activeItem.saveAs).toBeUndefined() + pane.trigger 'core:save' + expect(atom.showSaveDialog).not.toHaveBeenCalled() + + describe "core:save-as", -> + beforeEach -> + spyOn(atom, 'showSaveDialog') + + describe "when the current item has a saveAs method", -> + it "opens the save dialog and calls saveAs on the item with the selected path", -> + spyOn(editSession2, 'saveAs') + pane.showItem(editSession2) + + pane.trigger 'core:save-as' + + expect(atom.showSaveDialog).toHaveBeenCalled() + atom.showSaveDialog.argsForCall[0][0]('/selected/path') + expect(editSession2.saveAs).toHaveBeenCalledWith('/selected/path') + + describe "when the current item does not have a saveAs method", -> + it "does nothing", -> + expect(pane.activeItem.saveAs).toBeUndefined() + pane.trigger 'core:save-as' + expect(atom.showSaveDialog).not.toHaveBeenCalled() + + describe "pane:show-next-item and pane:show-previous-item", -> + it "advances forward/backward through the pane's items, looping around at either end", -> + expect(pane.activeItem).toBe view1 + pane.trigger 'pane:show-previous-item' + expect(pane.activeItem).toBe editSession2 + pane.trigger 'pane:show-previous-item' + expect(pane.activeItem).toBe view2 + pane.trigger 'pane:show-next-item' + expect(pane.activeItem).toBe editSession2 + pane.trigger 'pane:show-next-item' + expect(pane.activeItem).toBe view1 + + describe "pane:show-item-N events", -> + it "shows the (n-1)th item if it exists", -> + pane.trigger 'pane:show-item-2' + expect(pane.activeItem).toBe pane.itemAtIndex(1) + pane.trigger 'pane:show-item-1' + expect(pane.activeItem).toBe pane.itemAtIndex(0) + pane.trigger 'pane:show-item-9' # don't fail on out-of-bounds indices + expect(pane.activeItem).toBe pane.itemAtIndex(0) + + describe "when the title of the active item changes", -> + it "emits pane:active-item-title-changed", -> + activeItemTitleChangedHandler = jasmine.createSpy("activeItemTitleChangedHandler") + pane.on 'pane:active-item-title-changed', activeItemTitleChangedHandler + + expect(pane.activeItem).toBe view1 + + view2.trigger 'title-changed' + expect(activeItemTitleChangedHandler).not.toHaveBeenCalled() + + view1.trigger 'title-changed' + expect(activeItemTitleChangedHandler).toHaveBeenCalled() + activeItemTitleChangedHandler.reset() + + pane.showItem(view2) + view2.trigger 'title-changed' + expect(activeItemTitleChangedHandler).toHaveBeenCalled() + + describe ".remove()", -> + it "destroys all the pane's items", -> + pane.remove() + expect(editSession1.destroyed).toBeTruthy() + expect(editSession2.destroyed).toBeTruthy() + + it "triggers a 'pane:removed' event with the pane", -> + removedHandler = jasmine.createSpy("removedHandler") + container.on 'pane:removed', removedHandler + pane.remove() + expect(removedHandler).toHaveBeenCalled() + expect(removedHandler.argsForCall[0][1]).toBe pane + + describe "when there are other panes", -> + [paneToLeft, paneToRight] = [] + + beforeEach -> + pane.showItem(editSession1) + paneToLeft = pane.splitLeft() + paneToRight = pane.splitRight() + container.attachToDom() + + describe "when the removed pane is focused", -> + it "activates and focuses the next pane", -> + pane.focus() + pane.remove() + expect(paneToLeft.isActive()).toBeFalsy() + expect(paneToRight.isActive()).toBeTruthy() + expect(paneToRight).toMatchSelector ':has(:focus)' + + describe "when the removed pane is active but not focused", -> + it "activates the next pane, but does not focus it", -> + $(document.activeElement).blur() + expect(pane).not.toMatchSelector ':has(:focus)' + pane.makeActive() + pane.remove() + expect(paneToLeft.isActive()).toBeFalsy() + expect(paneToRight.isActive()).toBeTruthy() + expect(paneToRight).not.toMatchSelector ':has(:focus)' + + describe "when the removed pane is not active", -> + it "does not affect the active pane or the focus", -> + paneToLeft.focus() + expect(paneToLeft.isActive()).toBeTruthy() + expect(paneToRight.isActive()).toBeFalsy() + + pane.remove() + expect(paneToLeft.isActive()).toBeTruthy() + expect(paneToRight.isActive()).toBeFalsy() + expect(paneToLeft).toMatchSelector ':has(:focus)' + + describe "when it is the last pane", -> + beforeEach -> + expect(container.getPanes().length).toBe 1 + window.rootView = focus: jasmine.createSpy("rootView.focus") + + describe "when the removed pane is focused", -> + it "calls focus on rootView so we don't lose focus", -> + container.attachToDom() + pane.focus() + pane.remove() + expect(rootView.focus).toHaveBeenCalled() + + describe "when the removed pane is not focused", -> + it "does not call focus on root view", -> + expect(pane).not.toMatchSelector ':has(:focus)' + pane.remove() + expect(rootView.focus).not.toHaveBeenCalled() + + describe "when the pane is focused", -> + it "focuses the active item view", -> + focusHandler = jasmine.createSpy("focusHandler") + pane.activeItem.on 'focus', focusHandler + pane.focus() + expect(focusHandler).toHaveBeenCalled() + + it "triggers 'pane:became-active' if it was not previously active", -> + becameActiveHandler = jasmine.createSpy("becameActiveHandler") + container.on 'pane:became-active', becameActiveHandler + + expect(pane.isActive()).toBeFalsy() + pane.focusin() + expect(pane.isActive()).toBeTruthy() + pane.focusin() + + expect(becameActiveHandler.callCount).toBe 1 + + describe "split methods", -> + [pane1, view3, view4] = [] + beforeEach -> + pane1 = pane + pane.showItem(editSession1) + view3 = $$ -> @div id: 'view-3', 'View 3' + view4 = $$ -> @div id: 'view-4', 'View 4' + + describe "splitRight(items...)", -> + it "builds a row if needed, then appends a new pane after itself", -> + # creates the new pane with a copy of the active item if none are given + pane2 = pane1.splitRight() + expect(container.find('.row .pane').toArray()).toEqual [pane1[0], pane2[0]] + expect(pane2.items).toEqual [editSession1] + expect(pane2.activeItem).not.toBe editSession1 # it's a copy + + pane3 = pane2.splitRight(view3, view4) + expect(pane3.getItems()).toEqual [view3, view4] + expect(container.find('.row .pane').toArray()).toEqual [pane[0], pane2[0], pane3[0]] + + describe "splitRight(items...)", -> + it "builds a row if needed, then appends a new pane before itself", -> + # creates the new pane with a copy of the active item if none are given + pane2 = pane.splitLeft() + expect(container.find('.row .pane').toArray()).toEqual [pane2[0], pane[0]] + expect(pane2.items).toEqual [editSession1] + expect(pane2.activeItem).not.toBe editSession1 # it's a copy + + pane3 = pane2.splitLeft(view3, view4) + expect(pane3.getItems()).toEqual [view3, view4] + expect(container.find('.row .pane').toArray()).toEqual [pane3[0], pane2[0], pane[0]] + + describe "splitDown(items...)", -> + it "builds a column if needed, then appends a new pane after itself", -> + # creates the new pane with a copy of the active item if none are given + pane2 = pane.splitDown() + expect(container.find('.column .pane').toArray()).toEqual [pane[0], pane2[0]] + expect(pane2.items).toEqual [editSession1] + expect(pane2.activeItem).not.toBe editSession1 # it's a copy + + pane3 = pane2.splitDown(view3, view4) + expect(pane3.getItems()).toEqual [view3, view4] + expect(container.find('.column .pane').toArray()).toEqual [pane[0], pane2[0], pane3[0]] + + describe "splitUp(items...)", -> + it "builds a column if needed, then appends a new pane before itself", -> + # creates the new pane with a copy of the active item if none are given + pane2 = pane.splitUp() + expect(container.find('.column .pane').toArray()).toEqual [pane2[0], pane[0]] + expect(pane2.items).toEqual [editSession1] + expect(pane2.activeItem).not.toBe editSession1 # it's a copy + + pane3 = pane2.splitUp(view3, view4) + expect(pane3.getItems()).toEqual [view3, view4] + expect(container.find('.column .pane').toArray()).toEqual [pane3[0], pane2[0], pane[0]] + + it "lays out nested panes by equally dividing their containing row / column", -> + container.width(520).height(240).attachToDom() + pane1.showItem($("1")) + pane1 + .splitLeft($("2")) + .splitUp($("3")) + .splitLeft($("4")) + .splitDown($("5")) + + row1 = container.children(':eq(0)') + expect(row1.children().length).toBe 2 + column1 = row1.children(':eq(0)').view() + pane1 = row1.children(':eq(1)').view() + expect(column1.outerWidth()).toBe Math.round(2/3 * container.width()) + expect(column1.outerHeight()).toBe container.height() + expect(pane1.outerWidth()).toBe Math.round(1/3 * container.width()) + expect(pane1.outerHeight()).toBe container.height() + expect(Math.round(pane1.position().left)).toBe column1.outerWidth() + + expect(column1.children().length).toBe 2 + row2 = column1.children(':eq(0)').view() + pane2 = column1.children(':eq(1)').view() + expect(row2.outerWidth()).toBe column1.outerWidth() + expect(row2.height()).toBe 2/3 * container.height() + expect(pane2.outerWidth()).toBe column1.outerWidth() + expect(pane2.outerHeight()).toBe 1/3 * container.height() + expect(pane2.position().top).toBe row2.height() + + expect(row2.children().length).toBe 2 + column3 = row2.children(':eq(0)').view() + pane3 = row2.children(':eq(1)').view() + expect(column3.outerWidth()).toBe Math.round(1/3 * container.width()) + expect(column3.outerHeight()).toBe row2.outerHeight() + # the built in rounding seems to be rounding x.5 down, but we need to go up. this sucks. + expect(Math.round(pane3.trueWidth())).toBe Math.round(1/3 * container.width()) + expect(pane3.height()).toBe row2.outerHeight() + expect(Math.round(pane3.position().left)).toBe column3.width() + + expect(column3.children().length).toBe 2 + pane4 = column3.children(':eq(0)').view() + pane5 = column3.children(':eq(1)').view() + expect(pane4.outerWidth()).toBe column3.width() + expect(pane4.outerHeight()).toBe 1/3 * container.height() + expect(pane5.outerWidth()).toBe column3.width() + expect(pane5.position().top).toBe pane4.outerHeight() + expect(pane5.outerHeight()).toBe 1/3 * container.height() + + pane5.remove() + expect(column3.parent()).not.toExist() + expect(pane2.outerHeight()).toBe Math.floor(1/2 * container.height()) + expect(pane3.outerHeight()).toBe Math.floor(1/2 * container.height()) + expect(pane4.outerHeight()).toBe Math.floor(1/2 * container.height()) + + pane4.remove() + expect(row2.parent()).not.toExist() + expect(pane1.outerWidth()).toBe Math.floor(1/2 * container.width()) + expect(pane2.outerWidth()).toBe Math.floor(1/2 * container.width()) + expect(pane3.outerWidth()).toBe Math.floor(1/2 * container.width()) + + pane3.remove() + expect(column1.parent()).not.toExist() + expect(pane2.outerHeight()).toBe container.height() + + pane2.remove() + expect(row1.parent()).not.toExist() + expect(container.children().length).toBe 1 + expect(container.children('.pane').length).toBe 1 + expect(pane1.outerWidth()).toBe container.width() + + describe "autosave", -> + [initialActiveItem, initialActiveItemUri] = [] + + beforeEach -> + initialActiveItem = pane.activeItem + initialActiveItemUri = null + pane.activeItem.getUri = -> initialActiveItemUri + pane.activeItem.save = jasmine.createSpy("activeItem.save") + spyOn(pane, 'saveItem').andCallThrough() + + describe "when the active view loses focus", -> + it "saves the item if core.autosave is true and the item has a uri", -> + pane.activeView.trigger 'focusout' + expect(pane.saveItem).not.toHaveBeenCalled() + expect(pane.activeItem.save).not.toHaveBeenCalled() + + config.set('core.autosave', true) + pane.activeView.trigger 'focusout' + expect(pane.saveItem).not.toHaveBeenCalled() + expect(pane.activeItem.save).not.toHaveBeenCalled() + + initialActiveItemUri = '/tmp/hi' + pane.activeView.trigger 'focusout' + expect(pane.activeItem.save).toHaveBeenCalled() + + describe "when an item becomes inactive", -> + it "saves the item if core.autosave is true and the item has a uri", -> + expect(view2).not.toBe pane.activeItem + expect(pane.saveItem).not.toHaveBeenCalled() + expect(initialActiveItem.save).not.toHaveBeenCalled() + pane.showItem(view2) + + pane.showItem(initialActiveItem) + config.set('core.autosave', true) + pane.showItem(view2) + expect(pane.saveItem).not.toHaveBeenCalled() + expect(initialActiveItem.save).not.toHaveBeenCalled() + + pane.showItem(initialActiveItem) + initialActiveItemUri = '/tmp/hi' + pane.showItem(view2) + expect(initialActiveItem.save).toHaveBeenCalled() + + describe "when an item is destroyed", -> + it "saves the item if core.autosave is true and the item has a uri", -> + # doesn't have to be the active item + expect(view2).not.toBe pane.activeItem + pane.showItem(view2) + + pane.destroyItem(editSession1) + expect(pane.saveItem).not.toHaveBeenCalled() + + config.set("core.autosave", true) + view2.getUri = -> undefined + view2.save = -> + pane.destroyItem(view2) + expect(pane.saveItem).not.toHaveBeenCalled() + + initialActiveItemUri = '/tmp/hi' + pane.destroyItem(initialActiveItem) + expect(initialActiveItem.save).toHaveBeenCalled() + + describe ".itemForUri(uri)", -> + it "returns the item for which a call to .getUri() returns the given uri", -> + expect(pane.itemForUri(editSession1.getUri())).toBe editSession1 + expect(pane.itemForUri(editSession2.getUri())).toBe editSession2 + + describe "serialization", -> + it "can serialize and deserialize the pane and all its serializable items", -> + newPane = deserialize(pane.serialize()) + expect(newPane.getItems()).toEqual [editSession1, editSession2] + + it "restores the active item on deserialization if it serializable", -> + pane.showItem(editSession2) + newPane = deserialize(pane.serialize()) + expect(newPane.activeItem).toEqual editSession2 + + it "defaults to the first item on deserialization if the active item was not serializable", -> + expect(view2.serialize?()).toBeFalsy() + pane.showItem(view2) + newPane = deserialize(pane.serialize()) + expect(newPane.activeItem).toEqual editSession1 + + it "focuses the pane after attach only if had focus when serialized", -> + container.attachToDom() + + pane.focus() + state = pane.serialize() + pane.remove() + newPane = deserialize(state) + container.append(newPane) + expect(newPane).toMatchSelector(':has(:focus)') + + $(document.activeElement).blur() + state = newPane.serialize() + newPane.remove() + newerPane = deserialize(state) + expect(newerPane).not.toMatchSelector(':has(:focus)') diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index 09549eb98..136e61a85 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -3,17 +3,13 @@ fs = require 'fs' _ = require 'underscore' describe "Project", -> - project = null beforeEach -> - project = new Project(require.resolve('fixtures/dir')) + project.setPath(project.resolve('dir')) - afterEach -> - project.destroy() - - describe "when editSession is destroyed", -> + describe "when an edit session is destroyed", -> it "removes edit session and calls destroy on buffer (if buffer is not referenced by other edit sessions)", -> - editSession = project.buildEditSessionForPath("a") - anotherEditSession = project.buildEditSessionForPath("a") + editSession = project.buildEditSession("a") + anotherEditSession = project.buildEditSession("a") expect(project.editSessions.length).toBe 2 expect(editSession.buffer).toBe anotherEditSession.buffer @@ -24,7 +20,17 @@ describe "Project", -> anotherEditSession.destroy() expect(project.editSessions.length).toBe 0 - describe ".buildEditSessionForPath(path)", -> + describe "when an edit session is saved and the project has no path", -> + it "sets the project's path to the saved file's parent directory", -> + path = project.resolve('a') + project.setPath(undefined) + expect(project.getPath()).toBeUndefined() + editSession = project.buildEditSession() + editSession.saveAs('/tmp/atom-test-save-sets-project-path') + expect(project.getPath()).toBe '/tmp' + fs.remove('/tmp/atom-test-save-sets-project-path') + + describe ".buildEditSession(path)", -> [absolutePath, newBufferHandler, newEditSessionHandler] = [] beforeEach -> absolutePath = require.resolve('fixtures/dir/a') @@ -35,30 +41,30 @@ describe "Project", -> describe "when given an absolute path that hasn't been opened previously", -> it "returns a new edit session for the given path and emits 'buffer-created' and 'edit-session-created' events", -> - editSession = project.buildEditSessionForPath(absolutePath) + editSession = project.buildEditSession(absolutePath) expect(editSession.buffer.getPath()).toBe absolutePath expect(newBufferHandler).toHaveBeenCalledWith editSession.buffer expect(newEditSessionHandler).toHaveBeenCalledWith editSession describe "when given a relative path that hasn't been opened previously", -> it "returns a new edit session for the given path (relative to the project root) and emits 'buffer-created' and 'edit-session-created' events", -> - editSession = project.buildEditSessionForPath('a') + editSession = project.buildEditSession('a') expect(editSession.buffer.getPath()).toBe absolutePath expect(newBufferHandler).toHaveBeenCalledWith editSession.buffer expect(newEditSessionHandler).toHaveBeenCalledWith editSession describe "when passed the path to a buffer that has already been opened", -> it "returns a new edit session containing previously opened buffer and emits a 'edit-session-created' event", -> - editSession = project.buildEditSessionForPath(absolutePath) + editSession = project.buildEditSession(absolutePath) newBufferHandler.reset() - expect(project.buildEditSessionForPath(absolutePath).buffer).toBe editSession.buffer - expect(project.buildEditSessionForPath('a').buffer).toBe editSession.buffer + expect(project.buildEditSession(absolutePath).buffer).toBe editSession.buffer + expect(project.buildEditSession('a').buffer).toBe editSession.buffer expect(newBufferHandler).not.toHaveBeenCalled() expect(newEditSessionHandler).toHaveBeenCalledWith editSession describe "when not passed a path", -> it "returns a new edit session and emits 'buffer-created' and 'edit-session-created' events", -> - editSession = project.buildEditSessionForPath() + editSession = project.buildEditSession() expect(editSession.buffer.getPath()).toBeUndefined() expect(newBufferHandler).toHaveBeenCalledWith(editSession.buffer) expect(newEditSessionHandler).toHaveBeenCalledWith editSession diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index 15205bbc1..89c8ef8c1 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -4,13 +4,13 @@ Project = require 'project' RootView = require 'root-view' Buffer = require 'buffer' Editor = require 'editor' +Pane = require 'pane' {View, $$} = require 'space-pen' describe "RootView", -> pathToOpen = null beforeEach -> - project.destroy() project.setPath(project.resolve('dir')) pathToOpen = project.resolve('a') window.rootView = new RootView @@ -23,37 +23,39 @@ describe "RootView", -> describe "when the serialized RootView has an unsaved buffer", -> it "constructs the view with the same panes", -> + rootView.attachToDom() rootView.open() - editor1 = rootView.getActiveEditor() + editor1 = rootView.getActiveView() buffer = editor1.getBuffer() editor1.splitRight() viewState = rootView.serialize() rootView.deactivate() - window.rootView = RootView.deserialize(viewState) - rootView.focus() + window.rootView = deserialize(viewState) + rootView.attachToDom() + expect(rootView.getEditors().length).toBe 2 - expect(rootView.getActiveEditor().getText()).toBe buffer.getText() - expect(rootView.getTitle()).toBe "untitled – #{project.getPath()}" + expect(rootView.getActiveView().getText()).toBe buffer.getText() + expect(rootView.title).toBe "untitled - #{project.getPath()}" describe "when the serialized RootView has a project", -> describe "when there are open editors", -> it "constructs the view with the same panes", -> - editor1 = rootView.getActiveEditor() - editor2 = editor1.splitRight() - editor3 = editor2.splitRight() - editor4 = editor2.splitDown() - editor2.edit(project.buildEditSessionForPath('b')) - editor3.edit(project.buildEditSessionForPath('../sample.js')) - editor3.setCursorScreenPosition([2, 4]) - editor4.edit(project.buildEditSessionForPath('../sample.txt')) - editor4.setCursorScreenPosition([0, 2]) rootView.attachToDom() - editor2.focus() + pane1 = rootView.getActivePane() + pane2 = pane1.splitRight() + pane3 = pane2.splitRight() + pane4 = pane2.splitDown() + pane2.showItem(project.buildEditSession('b')) + pane3.showItem(project.buildEditSession('../sample.js')) + pane3.activeItem.setCursorScreenPosition([2, 4]) + pane4.showItem(project.buildEditSession('../sample.txt')) + pane4.activeItem.setCursorScreenPosition([0, 2]) + pane2.focus() viewState = rootView.serialize() rootView.deactivate() - window.rootView = RootView.deserialize(viewState) + window.rootView = deserialize(viewState) rootView.attachToDom() expect(rootView.getEditors().length).toBe 4 @@ -81,276 +83,54 @@ describe "RootView", -> expect(editor3.isFocused).toBeFalsy() expect(editor4.isFocused).toBeFalsy() - expect(rootView.getTitle()).toBe "#{fs.base(editor2.getPath())} – #{project.getPath()}" + expect(rootView.title).toBe "#{fs.base(editor2.getPath())} - #{project.getPath()}" describe "where there are no open editors", -> it "constructs the view with no open editors", -> - rootView.getActiveEditor().remove() + rootView.getActivePane().remove() expect(rootView.getEditors().length).toBe 0 viewState = rootView.serialize() rootView.deactivate() - window.rootView = RootView.deserialize(viewState) + window.rootView = deserialize(viewState) rootView.attachToDom() expect(rootView.getEditors().length).toBe 0 - describe "when a pane's wrapped view cannot be deserialized", -> - it "renders an empty pane", -> - viewState = - panesViewState: - deserializer: "Pane", - wrappedView: - deserializer: "BogusView" - - rootView.deactivate() - window.rootView = RootView.deserialize(viewState) - expect(rootView.find('.pane').length).toBe 1 - expect(rootView.find('.pane').children().length).toBe 0 - describe "focus", -> - describe "when there is an active editor", -> - it "hands off focus to the active editor", -> - rootView.attachToDom() - - rootView.open() # create an editor - expect(rootView).not.toMatchSelector(':focus') - expect(rootView.getActiveEditor().isFocused).toBeTruthy() - + describe "when there is an active view", -> + it "hands off focus to the active view", -> + editor = rootView.getActiveView() + editor.isFocused = false rootView.focus() - expect(rootView).not.toMatchSelector(':focus') - expect(rootView.getActiveEditor().isFocused).toBeTruthy() + expect(editor.isFocused).toBeTruthy() - describe "when there is no active editor", -> + describe "when there is no active view", -> beforeEach -> - rootView.getActiveEditor().remove() + rootView.getActivePane().remove() + expect(rootView.getActiveView()).toBeUndefined() rootView.attachToDom() + expect(document.activeElement).toBe document.body describe "when are visible focusable elements (with a -1 tabindex)", -> it "passes focus to the first focusable element", -> - rootView.horizontal.append $$ -> - @div "One", id: 'one', tabindex: -1 - @div "Two", id: 'two', tabindex: -1 + focusable1 = $$ -> @div "One", id: 'one', tabindex: -1 + focusable2 = $$ -> @div "Two", id: 'two', tabindex: -1 + rootView.horizontal.append(focusable1, focusable2) + expect(document.activeElement).toBe document.body rootView.focus() - expect(rootView).not.toMatchSelector(':focus') - expect(rootView.find('#one')).toMatchSelector(':focus') - expect(rootView.find('#two')).not.toMatchSelector(':focus') + expect(document.activeElement).toBe focusable1[0] describe "when there are no visible focusable elements", -> it "surrenders focus to the body", -> - expect(document.activeElement).toBe $('body')[0] + focusable = $$ -> @div "One", id: 'one', tabindex: -1 + rootView.horizontal.append(focusable) + focusable.hide() + expect(document.activeElement).toBe document.body - describe "panes", -> - [pane1, newPaneContent] = [] - - beforeEach -> - rootView.attachToDom() - rootView.width(800) - rootView.height(600) - pane1 = rootView.find('.pane').view() - pane1.attr('id', 'pane-1') - newPaneContent = $("
New pane content
") - spyOn(newPaneContent, 'focus') - - describe "vertical splits", -> - describe "when .splitRight(view) is called on a pane", -> - it "places a new pane to the right of the current pane in a .row div", -> - expect(rootView.panes.find('.row')).not.toExist() - - pane2 = pane1.splitRight(newPaneContent) - expect(newPaneContent.focus).toHaveBeenCalled() - - expect(rootView.panes.find('.row')).toExist() - expect(rootView.panes.find('.row .pane').length).toBe 2 - [leftPane, rightPane] = rootView.panes.find('.row .pane').map -> $(this) - expect(rightPane[0]).toBe pane2[0] - expect(leftPane.attr('id')).toBe 'pane-1' - expect(rightPane.html()).toBe "
New pane content
" - - expectedColumnWidth = Math.floor(rootView.panes.width() / 2) - expect(leftPane.outerWidth()).toBe expectedColumnWidth - expect(rightPane.position().left).toBe expectedColumnWidth - expect(rightPane.outerWidth()).toBe expectedColumnWidth - - pane2.remove() - - expect(rootView.panes.find('.row')).not.toExist() - expect(rootView.panes.find('.pane').length).toBe 1 - expect(pane1.outerWidth()).toBe rootView.panes.width() - - describe "when splitLeft(view) is called on a pane", -> - it "places a new pane to the left of the current pane in a .row div", -> - expect(rootView.find('.row')).not.toExist() - - pane2 = pane1.splitLeft(newPaneContent) - expect(newPaneContent.focus).toHaveBeenCalled() - - expect(rootView.find('.row')).toExist() - expect(rootView.find('.row .pane').length).toBe 2 - [leftPane, rightPane] = rootView.find('.row .pane').map -> $(this) - expect(leftPane[0]).toBe pane2[0] - expect(rightPane.attr('id')).toBe 'pane-1' - expect(leftPane.html()).toBe "
New pane content
" - - expectedColumnWidth = Math.floor(rootView.panes.width() / 2) - expect(leftPane.outerWidth()).toBe expectedColumnWidth - expect(rightPane.position().left).toBe expectedColumnWidth - expect(rightPane.outerWidth()).toBe expectedColumnWidth - - pane2.remove() - - expect(rootView.panes.find('.row')).not.toExist() - expect(rootView.panes.find('.pane').length).toBe 1 - expect(pane1.outerWidth()).toBe rootView.panes.width() - expect(pane1.position().left).toBe 0 - - describe "horizontal splits", -> - describe "when splitUp(view) is called on a pane", -> - it "places a new pane above the current pane in a .column div", -> - expect(rootView.find('.column')).not.toExist() - - pane2 = pane1.splitUp(newPaneContent) - expect(newPaneContent.focus).toHaveBeenCalled() - - expect(rootView.find('.column')).toExist() - expect(rootView.find('.column .pane').length).toBe 2 - [topPane, bottomPane] = rootView.find('.column .pane').map -> $(this) - expect(topPane[0]).toBe pane2[0] - expect(bottomPane.attr('id')).toBe 'pane-1' - expect(topPane.html()).toBe "
New pane content
" - - expectedRowHeight = Math.floor(rootView.panes.height() / 2) - expect(topPane.outerHeight()).toBe expectedRowHeight - expect(bottomPane.position().top).toBe expectedRowHeight - expect(bottomPane.outerHeight()).toBe expectedRowHeight - - pane2.remove() - - expect(rootView.panes.find('.column')).not.toExist() - expect(rootView.panes.find('.pane').length).toBe 1 - expect(pane1.outerHeight()).toBe rootView.panes.height() - expect(pane1.position().top).toBe 0 - - describe "when splitDown(view) is called on a pane", -> - it "places a new pane below the current pane in a .column div", -> - expect(rootView.find('.column')).not.toExist() - - pane2 = pane1.splitDown(newPaneContent) - expect(newPaneContent.focus).toHaveBeenCalled() - - expect(rootView.find('.column')).toExist() - expect(rootView.find('.column .pane').length).toBe 2 - [topPane, bottomPane] = rootView.find('.column .pane').map -> $(this) - expect(bottomPane[0]).toBe pane2[0] - expect(topPane.attr('id')).toBe 'pane-1' - expect(bottomPane.html()).toBe "
New pane content
" - - expectedRowHeight = Math.floor(rootView.panes.height() / 2) - expect(topPane.outerHeight()).toBe expectedRowHeight - expect(bottomPane.position().top).toBe expectedRowHeight - expect(bottomPane.outerHeight()).toBe expectedRowHeight - - pane2.remove() - - expect(rootView.panes.find('.column')).not.toExist() - expect(rootView.panes.find('.pane').length).toBe 1 - expect(pane1.outerHeight()).toBe rootView.panes.height() - - describe "layout of nested vertical and horizontal splits", -> - it "lays out rows and columns with a consistent width", -> - pane1.html("1") - - pane1 - .splitLeft("2") - .splitUp("3") - .splitLeft("4") - .splitDown("5") - - row1 = rootView.panes.children(':eq(0)') - expect(row1.children().length).toBe 2 - column1 = row1.children(':eq(0)').view() - pane1 = row1.children(':eq(1)').view() - expect(column1.outerWidth()).toBe Math.round(2/3 * rootView.panes.width()) - expect(column1.outerHeight()).toBe rootView.height() - expect(pane1.outerWidth()).toBe Math.round(1/3 * rootView.panes.width()) - expect(pane1.outerHeight()).toBe rootView.height() - expect(Math.round(pane1.position().left)).toBe column1.outerWidth() - - expect(column1.children().length).toBe 2 - row2 = column1.children(':eq(0)').view() - pane2 = column1.children(':eq(1)').view() - expect(row2.outerWidth()).toBe column1.outerWidth() - expect(row2.height()).toBe 2/3 * rootView.panes.height() - expect(pane2.outerWidth()).toBe column1.outerWidth() - expect(pane2.outerHeight()).toBe 1/3 * rootView.panes.height() - expect(pane2.position().top).toBe row2.height() - - expect(row2.children().length).toBe 2 - column3 = row2.children(':eq(0)').view() - pane3 = row2.children(':eq(1)').view() - expect(column3.outerWidth()).toBe Math.round(1/3 * rootView.panes.width()) - expect(column3.outerHeight()).toBe row2.outerHeight() - # the built in rounding seems to be rounding x.5 down, but we need to go up. this sucks. - expect(Math.round(pane3.trueWidth())).toBe Math.round(1/3 * rootView.panes.width()) - expect(pane3.height()).toBe row2.outerHeight() - expect(Math.round(pane3.position().left)).toBe column3.width() - - expect(column3.children().length).toBe 2 - pane4 = column3.children(':eq(0)').view() - pane5 = column3.children(':eq(1)').view() - expect(pane4.outerWidth()).toBe column3.width() - expect(pane4.outerHeight()).toBe 1/3 * rootView.panes.height() - expect(pane5.outerWidth()).toBe column3.width() - expect(pane5.position().top).toBe pane4.outerHeight() - expect(pane5.outerHeight()).toBe 1/3 * rootView.panes.height() - - pane5.remove() - - expect(column3.parent()).not.toExist() - expect(pane2.outerHeight()).toBe Math.floor(1/2 * rootView.panes.height()) - expect(pane3.outerHeight()).toBe Math.floor(1/2 * rootView.panes.height()) - expect(pane4.outerHeight()).toBe Math.floor(1/2 * rootView.panes.height()) - - pane4.remove() - expect(row2.parent()).not.toExist() - expect(pane1.outerWidth()).toBe Math.floor(1/2 * rootView.panes.width()) - expect(pane2.outerWidth()).toBe Math.floor(1/2 * rootView.panes.width()) - expect(pane3.outerWidth()).toBe Math.floor(1/2 * rootView.panes.width()) - - pane3.remove() - expect(column1.parent()).not.toExist() - expect(pane2.outerHeight()).toBe rootView.panes.height() - - pane2.remove() - expect(row1.parent()).not.toExist() - expect(rootView.panes.children().length).toBe 1 - expect(rootView.panes.children('.pane').length).toBe 1 - expect(pane1.outerWidth()).toBe rootView.panes.width() - - describe ".focusNextPane()", -> - it "focuses the wrapped view of the pane after the currently focused pane", -> - class DummyView extends View - @content: (number) -> @div(number, tabindex: -1) - - view1 = pane1.wrappedView - view2 = new DummyView(2) - view3 = new DummyView(3) - pane2 = pane1.splitDown(view2) - pane3 = pane2.splitRight(view3) - rootView.attachToDom() - view1.focus() - - spyOn(view1, 'focus').andCallThrough() - spyOn(view2, 'focus').andCallThrough() - spyOn(view3, 'focus').andCallThrough() - - rootView.focusNextPane() - expect(view2.focus).toHaveBeenCalled() - rootView.focusNextPane() - expect(view3.focus).toHaveBeenCalled() - rootView.focusNextPane() - expect(view1.focus).toHaveBeenCalled() + rootView.focus() + expect(document.activeElement).toBe document.body describe "keymap wiring", -> commandHandler = null @@ -360,249 +140,144 @@ describe "RootView", -> window.keymap.bindKeys('*', 'x': 'foo-command') - describe "when a keydown event is triggered on the RootView (not originating from Ace)", -> + describe "when a keydown event is triggered on the RootView", -> it "triggers matching keybindings for that event", -> event = keydownEvent 'x', target: rootView[0] rootView.trigger(event) expect(commandHandler).toHaveBeenCalled() - describe ".activeKeybindings()", -> - originalKeymap = null - keymap = null - editor = null + describe "window title", -> + describe "when the project has no path", -> + it "sets the title to 'untitled'", -> + project.setPath(undefined) + expect(rootView.title).toBe 'untitled' + describe "when the project has a path", -> beforeEach -> - rootView.attachToDom() - editor = rootView.getActiveEditor() - keymap = new (require 'keymap') - originalKeymap = window.keymap - window.keymap = keymap + rootView.open('b') - afterEach -> - window.keymap = originalKeymap + describe "when there is an active pane item", -> + it "sets the title to the pane item's title plus the project path", -> + item = rootView.getActivePaneItem() + expect(rootView.title).toBe "#{item.getTitle()} - #{project.getPath()}" - it "returns all keybindings available for focused element", -> - editor.on 'test-event-a', => # nothing + describe "when the title of the active pane item changes", -> + it "updates the window title based on the item's new title", -> + editSession = rootView.getActivePaneItem() + editSession.buffer.setPath('/tmp/hi') + expect(rootView.title).toBe "#{editSession.getTitle()} - #{project.getPath()}" - keymap.bindKeys ".editor", - "meta-a": "test-event-a" - "meta-b": "test-event-b" + describe "when the active pane's item changes", -> + it "updates the title to the new item's title plus the project path", -> + rootView.getActivePane().showNextItem() + item = rootView.getActivePaneItem() + expect(rootView.title).toBe "#{item.getTitle()} - #{project.getPath()}" - keybindings = rootView.activeKeybindings() - expect(Object.keys(keybindings).length).toBe 2 - expect(keybindings["meta-a"]).toEqual "test-event-a" + describe "when the last pane item is removed", -> + it "sets the title to the project's path", -> + rootView.getActivePane().remove() + expect(rootView.getActivePaneItem()).toBeUndefined() + expect(rootView.title).toBe project.getPath() - describe "when the path of the active editor changes", -> - it "changes the title and emits an root-view:active-path-changed event", -> - pathChangeHandler = jasmine.createSpy 'pathChangeHandler' - rootView.on 'root-view:active-path-changed', pathChangeHandler - - editor1 = rootView.getActiveEditor() - expect(rootView.getTitle()).toBe "#{fs.base(editor1.getPath())} – #{project.getPath()}" - - editor2 = rootView.getActiveEditor().splitLeft() - - path = project.resolve('b') - editor2.edit(project.buildEditSessionForPath(path)) - expect(pathChangeHandler).toHaveBeenCalled() - expect(rootView.getTitle()).toBe "#{fs.base(editor2.getPath())} – #{project.getPath()}" - - pathChangeHandler.reset() - editor1.getBuffer().saveAs("/tmp/should-not-be-title.txt") - expect(pathChangeHandler).not.toHaveBeenCalled() - expect(rootView.getTitle()).toBe "#{fs.base(editor2.getPath())} – #{project.getPath()}" - - it "sets the project path to the directory of the editor if it was previously unassigned", -> - project.setPath(undefined) - window.rootView = new RootView - rootView.open() - expect(project.getPath()?).toBeFalsy() - rootView.getActiveEditor().getBuffer().saveAs('/tmp/ignore-me') - expect(project.getPath()).toBe '/tmp' - - describe "when editors are focused", -> - it "triggers 'root-view:active-path-changed' events if the path of the active editor actually changes", -> - pathChangeHandler = jasmine.createSpy 'pathChangeHandler' - rootView.on 'root-view:active-path-changed', pathChangeHandler - - editor1 = rootView.getActiveEditor() - editor2 = rootView.getActiveEditor().splitLeft() - - rootView.open(require.resolve('fixtures/sample.txt')) - expect(pathChangeHandler).toHaveBeenCalled() - pathChangeHandler.reset() - - editor1.focus() - expect(pathChangeHandler).toHaveBeenCalled() - pathChangeHandler.reset() - - rootView.focus() - expect(pathChangeHandler).not.toHaveBeenCalled() - - editor2.edit(editor1.activeEditSession.copy()) - editor2.focus() - expect(pathChangeHandler).not.toHaveBeenCalled() - - describe "when the last editor is removed", -> - it "updates the title to the project path", -> - rootView.getEditors()[0].remove() - expect(rootView.getTitle()).toBe project.getPath() + describe "when an inactive pane's item changes", -> + it "does not update the title", -> + pane = rootView.getActivePane() + pane.splitRight() + initialTitle = rootView.title + pane.showNextItem() + expect(rootView.title).toBe initialTitle describe "font size adjustment", -> - editor = null - beforeEach -> - editor = rootView.getActiveEditor() - editor.attachToDom() - it "increases/decreases font size when increase/decrease-font-size events are triggered", -> - fontSizeBefore = editor.getFontSize() + fontSizeBefore = config.get('editor.fontSize') rootView.trigger 'window:increase-font-size' - expect(editor.getFontSize()).toBe fontSizeBefore + 1 + expect(config.get('editor.fontSize')).toBe fontSizeBefore + 1 rootView.trigger 'window:increase-font-size' - expect(editor.getFontSize()).toBe fontSizeBefore + 2 + expect(config.get('editor.fontSize')).toBe fontSizeBefore + 2 rootView.trigger 'window:decrease-font-size' - expect(editor.getFontSize()).toBe fontSizeBefore + 1 + expect(config.get('editor.fontSize')).toBe fontSizeBefore + 1 rootView.trigger 'window:decrease-font-size' - expect(editor.getFontSize()).toBe fontSizeBefore + expect(config.get('editor.fontSize')).toBe fontSizeBefore it "does not allow the font size to be less than 1", -> config.set("editor.fontSize", 1) rootView.trigger 'window:decrease-font-size' - expect(editor.getFontSize()).toBe 1 + expect(config.get('editor.fontSize')).toBe 1 describe ".open(path, options)", -> - describe "when there is no active editor", -> + describe "when there is no active pane", -> beforeEach -> - rootView.getActiveEditor().destroyActiveEditSession() - expect(rootView.getActiveEditor()).toBeUndefined() + spyOn(Pane.prototype, 'focus') + rootView.getActivePane().remove() + expect(rootView.getActivePane()).toBeUndefined() describe "when called with no path", -> - it "opens / returns an edit session for an empty buffer in a new editor", -> + it "creates a empty edit session as an item on a new pane, and focuses the pane", -> editSession = rootView.open() - expect(rootView.getActiveEditor()).toBeDefined() - expect(rootView.getActiveEditor().getPath()).toBeUndefined() - expect(editSession).toBe rootView.getActiveEditor().activeEditSession + expect(rootView.getActivePane().activeItem).toBe editSession + expect(editSession.getPath()).toBeUndefined() + expect(rootView.getActivePane().focus).toHaveBeenCalled() describe "when called with a path", -> - it "opens a buffer with the given path in a new editor", -> + it "creates an edit session for the given path as an item on a new pane, and focuses the pane", -> editSession = rootView.open('b') - expect(rootView.getActiveEditor()).toBeDefined() - expect(rootView.getActiveEditor().getPath()).toBe require.resolve('fixtures/dir/b') - expect(editSession).toBe rootView.getActiveEditor().activeEditSession + expect(rootView.getActivePane().activeItem).toBe editSession + expect(editSession.getPath()).toBe require.resolve('fixtures/dir/b') + expect(rootView.getActivePane().focus).toHaveBeenCalled() - describe "when there is an active editor", -> + describe "when the changeFocus option is false", -> + it "does not focus the new pane", -> + editSession = rootView.open('b', changeFocus: false) + expect(rootView.getActivePane().focus).not.toHaveBeenCalled() + + describe "when there is an active pane", -> + [activePane, initialItemCount] = [] beforeEach -> - expect(rootView.getActiveEditor()).toBeDefined() + activePane = rootView.getActivePane() + spyOn(activePane, 'focus') + initialItemCount = activePane.getItems().length describe "when called with no path", -> - it "opens an empty buffer in the active editor", -> + it "opens an edit session with an empty buffer as an item on the active pane and focuses it", -> editSession = rootView.open() - expect(rootView.getActiveEditor().getPath()).toBeUndefined() - expect(editSession).toBe rootView.getActiveEditor().activeEditSession + expect(activePane.getItems().length).toBe initialItemCount + 1 + expect(activePane.activeItem).toBe editSession + expect(editSession.getPath()).toBeUndefined() + expect(activePane.focus).toHaveBeenCalled() describe "when called with a path", -> - [editor1, editor2] = [] - beforeEach -> - rootView.attachToDom() - editor1 = rootView.getActiveEditor() - editor2 = editor1.splitRight() - rootView.open('b') - editor2.loadPreviousEditSession() - editor1.focus() + describe "when the active pane already has an edit session item for the path being opened", -> + it "shows the existing edit session on the pane", -> + previousEditSession = activePane.activeItem - describe "when allowActiveEditorChange is false (the default)", -> - activeEditor = null - beforeEach -> - activeEditor = rootView.getActiveEditor() + editSession = rootView.open('b') + expect(activePane.activeItem).toBe editSession + expect(editSession).not.toBe previousEditSession - describe "when the active editor has an edit session for the given path", -> - it "re-activates the existing edit session", -> - expect(activeEditor.getPath()).toBe require.resolve('fixtures/dir/a') - previousEditSession = activeEditor.activeEditSession + editSession = rootView.open(previousEditSession.getPath()) + expect(editSession).toBe previousEditSession + expect(activePane.activeItem).toBe editSession - editSession = rootView.open('b') - expect(activeEditor.activeEditSession).not.toBe previousEditSession - expect(editSession).toBe rootView.getActiveEditor().activeEditSession + expect(activePane.focus).toHaveBeenCalled() - editSession = rootView.open('a') - expect(activeEditor.activeEditSession).toBe previousEditSession - expect(editSession).toBe previousEditSession + describe "when the active pane does not have an edit session item for the path being opened", -> + it "creates a new edit session for the given path in the active editor", -> + editSession = rootView.open('b') + expect(activePane.items.length).toBe 2 + expect(activePane.activeItem).toBe editSession + expect(activePane.focus).toHaveBeenCalled() - describe "when the active editor does not have an edit session for the given path", -> - it "creates a new edit session for the given path in the active editor", -> - editSession = rootView.open('b') - expect(activeEditor.editSessions.length).toBe 2 - expect(editSession).toBe rootView.getActiveEditor().activeEditSession - - describe "when the 'allowActiveEditorChange' option is true", -> - describe "when the active editor has an edit session for the given path", -> - it "re-activates the existing edit session regardless of whether any other editor also has an edit session for the path", -> - activeEditor = rootView.getActiveEditor() - expect(activeEditor.getPath()).toBe require.resolve('fixtures/dir/a') - previousEditSession = activeEditor.activeEditSession - - editSession = rootView.open('b') - expect(activeEditor.activeEditSession).not.toBe previousEditSession - expect(editSession).toBe activeEditor.activeEditSession - - editSession = rootView.open('a', allowActiveEditorChange: true) - expect(activeEditor.activeEditSession).toBe previousEditSession - expect(editSession).toBe activeEditor.activeEditSession - - describe "when the active editor does *not* have an edit session for the given path", -> - describe "when another editor has an edit session for the path", -> - it "focuses the other editor and activates its edit session for the path", -> - expect(rootView.getActiveEditor()).toBe editor1 - editSession = rootView.open('b', allowActiveEditorChange: true) - expect(rootView.getActiveEditor()).toBe editor2 - expect(editor2.getPath()).toBe require.resolve('fixtures/dir/b') - expect(editSession).toBe rootView.getActiveEditor().activeEditSession - - describe "when no other editor has an edit session for the path either", -> - it "creates a new edit session for the path on the current active editor", -> - path = require.resolve('fixtures/sample.js') - editSession = rootView.open(path, allowActiveEditorChange: true) - expect(rootView.getActiveEditor()).toBe editor1 - expect(editor1.getPath()).toBe path - expect(editSession).toBe rootView.getActiveEditor().activeEditSession - - describe ".saveAll()", -> - it "saves all open editors", -> - project.setPath('/tmp') - file1 = '/tmp/atom-temp1.txt' - file2 = '/tmp/atom-temp2.txt' - fs.write(file1, "file1") - fs.write(file2, "file2") - rootView.open(file1) - - editor1 = rootView.getActiveEditor() - buffer1 = editor1.activeEditSession.buffer - expect(buffer1.getText()).toBe("file1") - expect(buffer1.isModified()).toBe(false) - buffer1.setText('edited1') - expect(buffer1.isModified()).toBe(true) - - editor2 = editor1.splitRight() - editor2.edit(project.buildEditSessionForPath('atom-temp2.txt')) - buffer2 = editor2.activeEditSession.buffer - expect(buffer2.getText()).toBe("file2") - expect(buffer2.isModified()).toBe(false) - buffer2.setText('edited2') - expect(buffer2.isModified()).toBe(true) - - rootView.saveAll() - - expect(buffer1.isModified()).toBe(false) - expect(fs.read(buffer1.getPath())).toBe("edited1") - expect(buffer2.isModified()).toBe(false) - expect(fs.read(buffer2.getPath())).toBe("edited2") + describe "when the changeFocus option is false", -> + it "does not focus the active pane", -> + editSession = rootView.open('b', changeFocus: false) + expect(activePane.focus).not.toHaveBeenCalled() describe "window:toggle-invisibles event", -> it "shows/hides invisibles in all open and future editors", -> rootView.height(200) rootView.attachToDom() - rightEditor = rootView.getActiveEditor() + rightEditor = rootView.getActiveView() rightEditor.setText(" \t ") leftEditor = rightEditor.splitLeft() expect(rightEditor.find(".line:first").text()).toBe " " @@ -636,7 +311,7 @@ describe "RootView", -> count++ rootView.eachEditor(callback) expect(count).toBe 1 - expect(callbackEditor).toBe rootView.getActiveEditor() + expect(callbackEditor).toBe rootView.getActiveView() it "invokes the callback for new editor", -> count = 0 @@ -648,9 +323,9 @@ describe "RootView", -> rootView.eachEditor(callback) count = 0 callbackEditor = null - rootView.getActiveEditor().splitRight() + rootView.getActiveView().splitRight() expect(count).toBe 1 - expect(callbackEditor).toBe rootView.getActiveEditor() + expect(callbackEditor).toBe rootView.getActiveView() describe ".eachBuffer(callback)", -> beforeEach -> @@ -664,7 +339,7 @@ describe "RootView", -> count++ rootView.eachBuffer(callback) expect(count).toBe 1 - expect(callbackBuffer).toBe rootView.getActiveEditor().getBuffer() + expect(callbackBuffer).toBe rootView.getActiveView().getBuffer() it "invokes the callback for new buffer", -> count = 0 @@ -678,4 +353,4 @@ describe "RootView", -> callbackBuffer = null rootView.open(require.resolve('fixtures/sample.txt')) expect(count).toBe 1 - expect(callbackBuffer).toBe rootView.getActiveEditor().getBuffer() + expect(callbackBuffer).toBe rootView.getActiveView().getBuffer() diff --git a/spec/app/text-mate-grammar-spec.coffee b/spec/app/text-mate-grammar-spec.coffee index 91f220ecb..07d06ca02 100644 --- a/spec/app/text-mate-grammar-spec.coffee +++ b/spec/app/text-mate-grammar-spec.coffee @@ -262,7 +262,7 @@ describe "TextMateGrammar", -> describe "when the grammar is CSON", -> it "loads the grammar and correctly parses a keyword", -> spyOn(syntax, 'addGrammar') - pack = new TextMatePackage(fixturesProject.resolve("packages/package-with-a-cson-grammar.tmbundle")) + pack = new TextMatePackage(project.resolve("packages/package-with-a-cson-grammar.tmbundle")) pack.load() grammar = pack.grammars[0] expect(grammar).toBeTruthy() diff --git a/spec/app/theme-spec.coffee b/spec/app/theme-spec.coffee index 5afebc983..518482292 100644 --- a/spec/app/theme-spec.coffee +++ b/spec/app/theme-spec.coffee @@ -26,7 +26,7 @@ describe "@load(name)", -> expect($(".editor").css("padding-right")).not.toBe("102px") expect($(".editor").css("padding-bottom")).not.toBe("103px") - themePath = fixturesProject.resolve('themes/theme-with-package-file') + themePath = project.resolve('themes/theme-with-package-file') theme = Theme.load(themePath) expect($(".editor").css("padding-top")).toBe("101px") expect($(".editor").css("padding-right")).toBe("102px") @@ -36,7 +36,7 @@ describe "@load(name)", -> it "loads and applies the stylesheet", -> expect($(".editor").css("padding-bottom")).not.toBe "1234px" - themePath = fixturesProject.resolve('themes/theme-stylesheet.css') + themePath = project.resolve('themes/theme-stylesheet.css') theme = Theme.load(themePath) expect($(".editor").css("padding-top")).toBe "1234px" @@ -46,7 +46,7 @@ describe "@load(name)", -> expect($(".editor").css("padding-right")).not.toBe "20px" expect($(".editor").css("padding-bottom")).not.toBe "30px" - themePath = fixturesProject.resolve('themes/theme-without-package-file') + themePath = project.resolve('themes/theme-without-package-file') theme = Theme.load(themePath) expect($(".editor").css("padding-top")).toBe "10px" expect($(".editor").css("padding-right")).toBe "20px" diff --git a/spec/app/tokenized-buffer-spec.coffee b/spec/app/tokenized-buffer-spec.coffee index 944628904..86bd50520 100644 --- a/spec/app/tokenized-buffer-spec.coffee +++ b/spec/app/tokenized-buffer-spec.coffee @@ -18,7 +18,7 @@ describe "TokenizedBuffer", -> describe "when the buffer contains soft-tabs", -> beforeEach -> - editSession = fixturesProject.buildEditSessionForPath('sample.js', autoIndent: false) + editSession = project.buildEditSession('sample.js', autoIndent: false) buffer = editSession.buffer tokenizedBuffer = editSession.displayBuffer.tokenizedBuffer editSession.setVisible(true) @@ -299,7 +299,7 @@ describe "TokenizedBuffer", -> describe "when the buffer contains hard-tabs", -> beforeEach -> tabLength = 2 - editSession = fixturesProject.buildEditSessionForPath('sample-with-tabs.coffee', { tabLength }) + editSession = project.buildEditSession('sample-with-tabs.coffee', { tabLength }) buffer = editSession.buffer tokenizedBuffer = editSession.displayBuffer.tokenizedBuffer editSession.setVisible(true) @@ -328,7 +328,7 @@ describe "TokenizedBuffer", -> describe "when a Git commit message file is tokenized", -> beforeEach -> - editSession = fixturesProject.buildEditSessionForPath('COMMIT_EDITMSG', autoIndent: false) + editSession = project.buildEditSession('COMMIT_EDITMSG', autoIndent: false) buffer = editSession.buffer tokenizedBuffer = editSession.displayBuffer.tokenizedBuffer editSession.setVisible(true) @@ -355,7 +355,7 @@ describe "TokenizedBuffer", -> describe "when a C++ source file is tokenized", -> beforeEach -> - editSession = fixturesProject.buildEditSessionForPath('includes.cc', autoIndent: false) + editSession = project.buildEditSession('includes.cc', autoIndent: false) buffer = editSession.buffer tokenizedBuffer = editSession.displayBuffer.tokenizedBuffer editSession.setVisible(true) @@ -386,7 +386,7 @@ describe "TokenizedBuffer", -> describe "when a Ruby source file is tokenized", -> beforeEach -> - editSession = fixturesProject.buildEditSessionForPath('hello.rb', autoIndent: false) + editSession = project.buildEditSession('hello.rb', autoIndent: false) buffer = editSession.buffer tokenizedBuffer = editSession.displayBuffer.tokenizedBuffer editSession.setVisible(true) @@ -403,7 +403,7 @@ describe "TokenizedBuffer", -> describe "when an Objective-C source file is tokenized", -> beforeEach -> - editSession = fixturesProject.buildEditSessionForPath('function.mm', autoIndent: false) + editSession = project.buildEditSession('function.mm', autoIndent: false) buffer = editSession.buffer tokenizedBuffer = editSession.displayBuffer.tokenizedBuffer editSession.setVisible(true) diff --git a/spec/app/window-spec.coffee b/spec/app/window-spec.coffee index 32f9397f1..12f79248f 100644 --- a/spec/app/window-spec.coffee +++ b/spec/app/window-spec.coffee @@ -46,16 +46,17 @@ describe "Window", -> expect(window.close).toHaveBeenCalled() describe ".reload()", -> - it "returns false when no buffers are modified", -> + beforeEach -> spyOn($native, "reload") + + it "returns false when no buffers are modified", -> window.reload() expect($native.reload).toHaveBeenCalled() - it "shows alert when a modifed buffer exists", -> + it "shows an alert when a modifed buffer exists", -> rootView.open('sample.js') - rootView.getActiveEditor().insertText("hi") + rootView.getActiveView().insertText("hi") spyOn(atom, "confirm") - spyOn($native, "reload") window.reload() expect($native.reload).not.toHaveBeenCalled() expect(atom.confirm).toHaveBeenCalled() @@ -103,13 +104,13 @@ describe "Window", -> it "unsubscribes from all buffers", -> rootView.open('sample.js') - editor1 = rootView.getActiveEditor() - editor2 = editor1.splitRight() - expect(window.rootView.getEditors().length).toBe 2 + buffer = rootView.getActivePaneItem().buffer + rootView.getActivePane().splitRight() + expect(window.rootView.find('.editor').length).toBe 2 window.shutdown() - expect(editor1.getBuffer().subscriptionCount()).toBe 0 + expect(buffer.subscriptionCount()).toBe 0 it "only serializes window state the first time it is called", -> deactivateSpy = spyOn(atom, "setRootViewStateForPath").andCallThrough() @@ -129,3 +130,34 @@ describe "Window", -> window.installAtomCommand(commandPath) expect(fs.exists(commandPath)).toBeTruthy() expect(fs.read(commandPath).length).toBeGreaterThan 1 + + describe ".deserialize(state)", -> + class Foo + @deserialize: ({name}) -> new Foo(name) + constructor: (@name) -> + + beforeEach -> + registerDeserializer(Foo) + + afterEach -> + unregisterDeserializer(Foo) + + it "calls deserialize on the deserializer for the given state object, or returns undefined if one can't be found", -> + object = deserialize({ deserializer: 'Foo', name: 'Bar' }) + expect(object.name).toBe 'Bar' + expect(deserialize({ deserializer: 'Bogus' })).toBeUndefined() + + describe "when the deserializer has a version", -> + beforeEach -> + Foo.version = 2 + + describe "when the deserialized state has a matching version", -> + it "attempts to deserialize the state", -> + object = deserialize({ deserializer: 'Foo', version: 2, name: 'Bar' }) + expect(object.name).toBe 'Bar' + + describe "when the deserialized state has a non-matching version", -> + it "returns undefined", -> + expect(deserialize({ deserializer: 'Foo', version: 3, name: 'Bar' })).toBeUndefined() + expect(deserialize({ deserializer: 'Foo', version: 1, name: 'Bar' })).toBeUndefined() + expect(deserialize({ deserializer: 'Foo', name: 'Bar' })).toBeUndefined() diff --git a/spec/fixtures/packages/package-with-activation-events/main.coffee b/spec/fixtures/packages/package-with-activation-events/main.coffee index a591812bd..a860be2bb 100644 --- a/spec/fixtures/packages/package-with-activation-events/main.coffee +++ b/spec/fixtures/packages/package-with-activation-events/main.coffee @@ -2,7 +2,7 @@ module.exports = activationEventCallCount: 0 activate: -> - rootView.getActiveEditor()?.command 'activation-event', => + rootView.getActiveView()?.command 'activation-event', => @activationEventCallCount++ serialize: -> diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 910c8d238..4632db296 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -30,9 +30,8 @@ jasmine.getEnv().defaultTimeoutInterval = 5000 beforeEach -> jQuery.fx.off = true - window.fixturesProject = new Project(require.resolve('fixtures')) - window.project = fixturesProject - window.git = Git.open(fixturesProject.getPath()) + window.project = new Project(require.resolve('fixtures')) + window.git = Git.open(project.getPath()) window.project.on 'path-changed', -> window.git?.destroy() window.git = Git.open(window.project.getPath()) @@ -56,7 +55,7 @@ beforeEach -> # make editor display updates synchronous spyOn(Editor.prototype, 'requestDisplayUpdate').andCallFake -> @updateDisplay() - spyOn(RootView.prototype, 'updateWindowTitle').andCallFake -> + spyOn(RootView.prototype, 'setTitle').andCallFake (@title) -> spyOn(window, "setTimeout").andCallFake window.fakeSetTimeout spyOn(window, "clearTimeout").andCallFake window.fakeClearTimeout spyOn(File.prototype, "detectResurrectionAfterDelay").andCallFake -> @detectResurrection() @@ -73,7 +72,7 @@ afterEach -> keymap.bindingSets = bindingSetsToRestore keymap.bindingSetsByFirstKeystrokeToRestore = bindingSetsByFirstKeystrokeToRestore if rootView? - rootView.deactivate() + rootView.deactivate?() window.rootView = null if project? project.destroy() @@ -83,6 +82,8 @@ afterEach -> window.git = null $('#jasmine-content').empty() ensureNoPathSubscriptions() + atom.pendingModals = [[]] + atom.presentingModal = false waits(0) # yield to ui thread to make screen update more frequently window.loadPackage = (name, options) -> diff --git a/src/app/atom.coffee b/src/app/atom.coffee index b5149622e..2e4635c18 100644 --- a/src/app/atom.coffee +++ b/src/app/atom.coffee @@ -10,11 +10,14 @@ originalSendMessageToBrowserProcess = atom.sendMessageToBrowserProcess _.extend atom, exitWhenDone: window.location.params.exitWhenDone + devMode: window.location.params.devMode loadedThemes: [] pendingBrowserProcessCallbacks: {} loadedPackages: [] activatedAtomPackages: [] atomPackageStates: {} + presentingModal: false + pendingModals: [[]] getPathToOpen: -> @getWindowState('pathToOpen') ? window.location.params.pathToOpen @@ -101,15 +104,50 @@ _.extend atom, @sendMessageToBrowserProcess('newWindow', args) confirm: (message, detailedMessage, buttonLabelsAndCallbacks...) -> - args = [message, detailedMessage] - callbacks = [] - while buttonLabelsAndCallbacks.length - args.push(buttonLabelsAndCallbacks.shift()) - callbacks.push(buttonLabelsAndCallbacks.shift()) - @sendMessageToBrowserProcess('confirm', args, callbacks) + wrapCallback = (callback) => => @dismissModal(callback) + @presentModal => + args = [message, detailedMessage] + callbacks = [] + while buttonLabelsAndCallbacks.length + do => + buttonLabel = buttonLabelsAndCallbacks.shift() + buttonCallback = buttonLabelsAndCallbacks.shift() + args.push(buttonLabel) + callbacks.push(=> @dismissModal(buttonCallback)) + @sendMessageToBrowserProcess('confirm', args, callbacks) showSaveDialog: (callback) -> - @sendMessageToBrowserProcess('showSaveDialog', [], callback) + @presentModal => + @sendMessageToBrowserProcess('showSaveDialog', [], (path) => @dismissModal(callback, path)) + + presentModal: (fn) -> + if @presentingModal + @pushPendingModal(fn) + else + @presentingModal = true + fn() + + dismissModal: (fn, args...) -> + @pendingModals.push([]) # prioritize any modals presented during dismiss callback + fn?(args...) + @presentingModal = false + if fn = @shiftPendingModal() + _.delay (=> @presentModal(fn)), 50 # let view update before next dialog + + pushPendingModal: (fn) -> + # pendingModals is a stack of queues. enqueue to top of stack. + stackSize = @pendingModals.length + @pendingModals[stackSize - 1].push(fn) + + shiftPendingModal: -> + # pop pendingModals stack if its top queue is empty, otherwise shift off the topmost queue + stackSize = @pendingModals.length + currentQueueSize = @pendingModals[stackSize - 1].length + if stackSize > 1 and currentQueueSize == 0 + @pendingModals.pop() + @shiftPendingModal() + else + @pendingModals[stackSize - 1].shift() toggleDevTools: -> @sendMessageToBrowserProcess('toggleDevTools') diff --git a/src/app/buffer-change-operation.coffee b/src/app/buffer-change-operation.coffee index b3ffda35e..cbbf68b0a 100644 --- a/src/app/buffer-change-operation.coffee +++ b/src/app/buffer-change-operation.coffee @@ -73,7 +73,7 @@ class BufferChangeOperation event = { oldRange, newRange, oldText, newText } @updateMarkers(event) @buffer.trigger 'changed', event - @buffer.scheduleStoppedChangingEvent() + @buffer.scheduleModifiedEvents() @resumeMarkerObservation() @buffer.trigger 'markers-updated' diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee index 092f0d797..00d0483e4 100644 --- a/src/app/buffer.coffee +++ b/src/app/buffer.coffee @@ -69,7 +69,7 @@ class Buffer @file.on "removed", => @updateCachedDiskContents() - @trigger "contents-modified", {differsFromDisk: true} + @triggerModifiedStatusChanged(@isModified()) @file.on "moved", => @trigger "path-changed", this @@ -78,6 +78,7 @@ class Buffer @trigger 'will-reload' @updateCachedDiskContents() @setText(@cachedDiskContents) + @triggerModifiedStatusChanged(false) @trigger 'reloaded' updateCachedDiskContents: -> @@ -252,6 +253,7 @@ class Buffer @setPath(path) @cachedDiskContents = @getText() @file.write(@getText()) + @triggerModifiedStatusChanged(false) @trigger 'saved' isModified: -> @@ -424,13 +426,20 @@ class Buffer return unless path git?.checkoutHead(path) - scheduleStoppedChangingEvent: -> + scheduleModifiedEvents: -> clearTimeout(@stoppedChangingTimeout) if @stoppedChangingTimeout stoppedChangingCallback = => @stoppedChangingTimeout = null - @trigger 'contents-modified', {differsFromDisk: @isModified()} + modifiedStatus = @isModified() + @trigger 'contents-modified', modifiedStatus + @triggerModifiedStatusChanged(modifiedStatus) @stoppedChangingTimeout = setTimeout(stoppedChangingCallback, @stoppedChangingDelay) + triggerModifiedStatusChanged: (modifiedStatus) -> + return if modifiedStatus is @previousModifiedStatus + @previousModifiedStatus = modifiedStatus + @trigger 'modified-status-changed', modifiedStatus + fileExists: -> @file.exists() diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index b160d5468..0d48a1f4e 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -14,17 +14,22 @@ module.exports = class EditSession registerDeserializer(this) - @deserialize: (state, project) -> + @deserialize: (state) -> if fs.exists(state.buffer) - session = project.buildEditSessionForPath(state.buffer) + session = project.buildEditSession(state.buffer) else console.warn "Could not build edit session for path '#{state.buffer}' because that file no longer exists" if state.buffer - session = project.buildEditSessionForPath(null) + session = project.buildEditSession(null) session.setScrollTop(state.scrollTop) session.setScrollLeft(state.scrollLeft) session.setCursorScreenPosition(state.cursorScreenPosition) session + @identifiedBy: 'path' + + @deserializesToSameObject: (state, editSession) -> + state.path + scrollTop: 0 scrollLeft: 0 languageMode: null @@ -43,17 +48,38 @@ class EditSession @addCursorAtScreenPosition([0, 0]) @buffer.retain() - @subscribe @buffer, "path-changed", => @trigger "path-changed" + @subscribe @buffer, "path-changed", => + @project.setPath(fs.directory(@getPath())) unless @project.getPath()? + @trigger "title-changed" + @trigger "path-changed" @subscribe @buffer, "contents-conflicted", => @trigger "contents-conflicted" @subscribe @buffer, "markers-updated", => @mergeCursors() + @subscribe @buffer, "modified-status-changed", => @trigger "modified-status-changed" @preserveCursorPositionOnBufferReload() @subscribe @displayBuffer, "changed", (e) => @trigger 'screen-lines-changed', e + getViewClass: -> + require 'editor' + + getTitle: -> + if path = @getPath() + fs.base(path) + else + 'untitled' + + getLongTitle: -> + if path = @getPath() + fileName = fs.base(path) + directory = fs.base(fs.directory(path)) + "#{fileName} - #{directory}" + else + 'untitled' + destroy: -> - throw new Error("Edit session already destroyed") if @destroyed + return if @destroyed @destroyed = true @unsubscribe() @buffer.release() @@ -130,6 +156,7 @@ class EditSession saveAs: (path) -> @buffer.saveAs(path) getFileExtension: -> @buffer.getExtension() getPath: -> @buffer.getPath() + getUri: -> @getPath() isBufferRowBlank: (bufferRow) -> @buffer.isRowBlank(bufferRow) nextNonBlankBufferRow: (bufferRow) -> @buffer.nextNonBlankRow(bufferRow) getEofBufferPosition: -> @buffer.getEofPosition() diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 5b5bc6502..1e2c0b464 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -16,7 +16,6 @@ class Editor extends View fontSize: 20 showInvisibles: false showIndentGuide: false - autosave: false autoIndent: true autoIndentOnPaste: false nonWordCharacters: "./\\()\"':,.;<>~!@#$%^&*|+=[]{}`~?-" @@ -49,8 +48,6 @@ class Editor extends View lineCache: null isFocused: false activeEditSession: null - closedEditSessions: null - editSessions: null attached: false lineOverdraw: 10 pendingChanges: null @@ -58,15 +55,12 @@ class Editor extends View newSelections: null redrawOnReattach: false - @deserialize: (state) -> - editor = new Editor(mini: state.mini, deserializing: true) - editSessions = state.editSessions.map (state) -> EditSession.deserialize(state, project) - editor.pushEditSession(editSession) for editSession in editSessions - editor.setActiveEditSessionIndex(state.activeEditSessionIndex) - editor.isFocused = state.isFocused - editor + initialize: (editSessionOrOptions) -> + if editSessionOrOptions instanceof EditSession + editSession = editSessionOrOptions + else + {editSession, @mini} = (editSessionOrOptions ? {}) - initialize: ({editSession, @mini, deserializing} = {}) -> requireStylesheet 'editor.css' @id = Editor.nextEditorId++ @@ -76,8 +70,6 @@ class Editor extends View @handleEvents() @cursorViews = [] @selectionViews = [] - @editSessions = [] - @closedEditSessions = [] @pendingChanges = [] @newCursors = [] @newSelections = [] @@ -91,18 +83,8 @@ class Editor extends View tabLength: 2 softTabs: true ) - else if not deserializing - throw new Error("Editor must be constructed with an 'editSession' or 'mini: true' param") - - serialize: -> - @saveScrollPositionForActiveEditSession() - deserializer: "Editor" - editSessions: @editSessions.map (session) -> session.serialize() - activeEditSessionIndex: @getActiveEditSessionIndex() - isFocused: @isFocused - - copy: -> - Editor.deserialize(@serialize(), rootView) + else + throw new Error("Must supply an EditSession or mini: true") bindKeys: -> editorBindings = @@ -155,9 +137,6 @@ class Editor extends View 'core:select-down': @selectDown 'core:select-to-top': @selectToTop 'core:select-to-bottom': @selectToBottom - 'core:close': @destroyActiveEditSession - 'editor:save': @save - 'editor:save-as': @saveAs 'editor:newline-below': @insertNewlineBelow 'editor:newline-above': @insertNewlineAbove 'editor:toggle-soft-tabs': @toggleSoftTabs @@ -167,32 +146,14 @@ class Editor extends View 'editor:fold-current-row': @foldCurrentRow 'editor:unfold-current-row': @unfoldCurrentRow 'editor:fold-selection': @foldSelection - 'editor:split-left': @splitLeft - 'editor:split-right': @splitRight - 'editor:split-up': @splitUp - 'editor:split-down': @splitDown - 'editor:show-next-buffer': @loadNextEditSession - 'editor:show-buffer-1': => @setActiveEditSessionIndex(0) if @editSessions[0] - 'editor:show-buffer-2': => @setActiveEditSessionIndex(1) if @editSessions[1] - 'editor:show-buffer-3': => @setActiveEditSessionIndex(2) if @editSessions[2] - 'editor:show-buffer-4': => @setActiveEditSessionIndex(3) if @editSessions[3] - 'editor:show-buffer-5': => @setActiveEditSessionIndex(4) if @editSessions[4] - 'editor:show-buffer-6': => @setActiveEditSessionIndex(5) if @editSessions[5] - 'editor:show-buffer-7': => @setActiveEditSessionIndex(6) if @editSessions[6] - 'editor:show-buffer-8': => @setActiveEditSessionIndex(7) if @editSessions[7] - 'editor:show-buffer-9': => @setActiveEditSessionIndex(8) if @editSessions[8] - 'editor:show-previous-buffer': @loadPreviousEditSession 'editor:toggle-line-comments': @toggleLineCommentsInSelection 'editor:log-cursor-scope': @logCursorScope 'editor:checkout-head-revision': @checkoutHead - 'editor:close-other-edit-sessions': @destroyInactiveEditSessions - 'editor:close-all-edit-sessions': @destroyAllEditSessions 'editor:select-grammar': @selectGrammar 'editor:copy-path': @copyPathToPasteboard 'editor:move-line-up': @moveLineUp 'editor:move-line-down': @moveLineDown 'editor:duplicate-line': @duplicateLine - 'editor:undo-close-session': @undoDestroySession 'editor:toggle-indent-guide': => config.set('editor.showIndentGuide', !config.get('editor.showIndentGuide')) 'editor:save-debug-snapshot': @saveDebugSnapshot @@ -343,7 +304,7 @@ class Editor extends View checkoutHead: -> @getBuffer().checkoutHead() setText: (text) -> @getBuffer().setText(text) getText: -> @getBuffer().getText() - getPath: -> @getBuffer().getPath() + getPath: -> @activeEditSession?.getPath() getLineCount: -> @getBuffer().getLineCount() getLastBufferRow: -> @getBuffer().getLastRow() getTextInRange: (range) -> @getBuffer().getTextInRange(range) @@ -367,13 +328,11 @@ class Editor extends View false @hiddenInput.on 'focus', => - rootView?.editorFocused(this) @isFocused = true @addClass 'is-focused' @hiddenInput.on 'focusout', => @isFocused = false - @autosave() if config.get "editor.autosave" @removeClass 'is-focused' @underlayer.on 'click', (e) => @@ -436,10 +395,7 @@ class Editor extends View e.pageX = @renderedLines.offset().left onMouseDown(e) - @subscribe syntax, 'grammars-loaded', => - @reloadGrammar() - for session in @editSessions - session.reloadGrammar() unless session is @activeEditSession + @subscribe syntax, 'grammars-loaded', => @reloadGrammar() @scrollView.on 'scroll', => if @scrollView.scrollLeft() == 0 @@ -481,86 +437,16 @@ class Editor extends View @trigger 'editor:attached', [this] edit: (editSession) -> - index = @editSessions.indexOf(editSession) - index = @pushEditSession(editSession) if index == -1 - @setActiveEditSessionIndex(index) - - pushEditSession: (editSession) -> - index = @editSessions.length - @editSessions.push(editSession) - @closedEditSessions = @closedEditSessions.filter ({path})-> - path isnt editSession.getPath() - editSession.on 'destroyed', => @editSessionDestroyed(editSession) - @trigger 'editor:edit-session-added', [editSession, index] - index - - getBuffer: -> @activeEditSession.buffer - - undoDestroySession: -> - return unless @closedEditSessions.length > 0 - - {path, index} = @closedEditSessions.pop() - rootView.open(path) - activeIndex = @getActiveEditSessionIndex() - @moveEditSessionToIndex(activeIndex, index) if index < activeIndex - - destroyActiveEditSession: -> - @destroyEditSessionIndex(@getActiveEditSessionIndex()) - - destroyEditSessionIndex: (index, callback) -> - return if @mini - - editSession = @editSessions[index] - destroySession = => - path = editSession.getPath() - @closedEditSessions.push({path, index}) if path - editSession.destroy() - callback?(index) - - if editSession.isModified() and not editSession.hasEditors() - @promptToSaveDirtySession(editSession, destroySession) - else - destroySession() - - destroyInactiveEditSessions: -> - destroyIndex = (index) => - index++ if index is @getActiveEditSessionIndex() - @destroyEditSessionIndex(index, destroyIndex) if @editSessions[index] - destroyIndex(0) - - destroyAllEditSessions: -> - destroyIndex = (index) => - @destroyEditSessionIndex(index, destroyIndex) if @editSessions[index] - destroyIndex(0) - - editSessionDestroyed: (editSession) -> - index = @editSessions.indexOf(editSession) - @loadPreviousEditSession() if index is @getActiveEditSessionIndex() and @editSessions.length > 1 - _.remove(@editSessions, editSession) - @trigger 'editor:edit-session-removed', [editSession, index] - @remove() if @editSessions.length is 0 - - loadNextEditSession: -> - nextIndex = (@getActiveEditSessionIndex() + 1) % @editSessions.length - @setActiveEditSessionIndex(nextIndex) - - loadPreviousEditSession: -> - previousIndex = @getActiveEditSessionIndex() - 1 - previousIndex = @editSessions.length - 1 if previousIndex < 0 - @setActiveEditSessionIndex(previousIndex) - - getActiveEditSessionIndex: -> - return index for session, index in @editSessions when session == @activeEditSession - - setActiveEditSessionIndex: (index) -> - throw new Error("Edit session not found") unless @editSessions[index] + return if editSession is @activeEditSession if @activeEditSession - @autosave() if config.get "editor.autosave" @saveScrollPositionForActiveEditSession() @activeEditSession.off(".editor") - @activeEditSession = @editSessions[index] + @activeEditSession = editSession + + return unless @activeEditSession? + @activeEditSession.setVisible(true) @activeEditSession.on "contents-conflicted.editor", => @@ -571,11 +457,18 @@ class Editor extends View @trigger 'editor:path-changed' @trigger 'editor:path-changed' - @trigger 'editor:active-edit-session-changed', [@activeEditSession, index] @resetDisplay() if @attached and @activeEditSession.buffer.isInConflict() - setTimeout(( =>@showBufferConflictAlert(@activeEditSession)), 0) # Display after editSession has a chance to display + _.defer => @showBufferConflictAlert(@activeEditSession) # Display after editSession has a chance to display + + getModel: -> + @activeEditSession + + setModel: (editSession) -> + @edit(editSession) + + getBuffer: -> @activeEditSession.buffer showBufferConflictAlert: (editSession) -> atom.confirm( @@ -585,30 +478,6 @@ class Editor extends View "Cancel" ) - moveEditSessionToIndex: (fromIndex, toIndex) -> - return if fromIndex is toIndex - editSession = @editSessions.splice(fromIndex, 1) - @editSessions.splice(toIndex, 0, editSession[0]) - @trigger 'editor:edit-session-order-changed', [editSession, fromIndex, toIndex] - @setActiveEditSessionIndex(toIndex) - - moveEditSessionToEditor: (fromIndex, toEditor, toIndex) -> - fromEditSession = @editSessions[fromIndex] - toEditSession = fromEditSession.copy() - @destroyEditSessionIndex(fromIndex) - toEditor.edit(toEditSession) - toEditor.moveEditSessionToIndex(toEditor.getActiveEditSessionIndex(), toIndex) - - activateEditSessionForPath: (path) -> - for editSession, index in @editSessions - if editSession.buffer.getPath() == path - @setActiveEditSessionIndex(index) - return @activeEditSession - false - - getOpenBufferPaths: -> - editSession.buffer.getPath() for editSession in @editSessions when editSession.buffer.getPath()? - scrollTop: (scrollTop, options={}) -> return @cachedScrollTop or 0 unless scrollTop? maxScrollTop = @verticalScrollbar.prop('scrollHeight') - @verticalScrollbar.height() @@ -723,22 +592,6 @@ class Editor extends View @removeClass 'soft-wrap' $(window).off 'resize', @_setSoftWrapColumn - save: (session=@activeEditSession, onSuccess) -> - if @getPath() - session.save() - onSuccess?() - else - @saveAs(session, onSuccess) - - saveAs: (session=@activeEditSession, onSuccess) -> - atom.showSaveDialog (path) => - if path - session.saveAs(path) - onSuccess?() - - autosave: -> - @save() if @getPath()? - setFontSize: (fontSize) -> headTag = $("head") styleTag = headTag.find("style.font-size") @@ -781,54 +634,33 @@ class Editor extends View @updateLayerDimensions() @requestDisplayUpdate() - newSplitEditor: (editSession) -> - new Editor { editSession: editSession ? @activeEditSession.copy() } + splitLeft: (items...) -> + @pane()?.splitLeft(items...).activeView - splitLeft: (editSession) -> - @pane()?.splitLeft(@newSplitEditor(editSession)).wrappedView + splitRight: (items...) -> + @pane()?.splitRight(items...).activeView - splitRight: (editSession) -> - @pane()?.splitRight(@newSplitEditor(editSession)).wrappedView + splitUp: (items...) -> + @pane()?.splitUp(items...).activeView - splitUp: (editSession) -> - @pane()?.splitUp(@newSplitEditor(editSession)).wrappedView - - splitDown: (editSession) -> - @pane()?.splitDown(@newSplitEditor(editSession)).wrappedView + splitDown: (items...) -> + @pane()?.splitDown(items...).activeView pane: -> - @parent('.pane').view() - - promptToSaveDirtySession: (session, callback) -> - path = session.getPath() - filename = if path then fs.base(path) else "untitled buffer" - atom.confirm( - "'#{filename}' has changes, do you want to save them?" - "Your changes will be lost if you don't save them" - "Save", => @save(session, callback), - "Cancel", null - "Don't Save", callback - ) + @closest('.pane').view() remove: (selector, keepData) -> return super if keepData or @removed @trigger 'editor:will-be-removed' - if @pane() then @pane().remove() else super + super rootView?.focus() afterRemove: -> @removed = true - @destroyEditSessions() + @activeEditSession?.destroy() $(window).off(".editor-#{@id}") $(document).off(".editor-#{@id}") - getEditSessions: -> - new Array(@editSessions...) - - destroyEditSessions: -> - for session in @getEditSessions() - session.destroy() - getCursorView: (index) -> index ?= @cursorViews.length - 1 @cursorViews[index] @@ -933,7 +765,8 @@ class Editor extends View @pendingDisplayUpdate = false updateDisplay: (options={}) -> - return unless @attached + return unless @attached and @activeEditSession + return if @activeEditSession.destroyed @updateRenderedLines() @highlightCursorLine() @updateCursorViews() diff --git a/src/app/git.coffee b/src/app/git.coffee index 761084c57..b9f469b1c 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -28,6 +28,7 @@ class Git statuses: null upstream: null + statusTask: null constructor: (path, options={}) -> @statuses = {} @@ -58,7 +59,11 @@ class Git @path ?= fs.absolute(@getRepo().getPath()) destroy: -> - @statusTask?.abort() + if @statusTask? + @statusTask.abort() + @statusTask.off() + @statusTask = null + @getRepo().destroy() @repo = null @unsubscribe() @@ -130,8 +135,16 @@ class Git @getRepo().isSubmodule(@relativize(path)) refreshStatus: -> - @statusTask = new RepositoryStatusTask(this) - @statusTask.start() + if @statusTask? + @statusTask.off() + @statusTask.one 'task-completed', => + @statusTask = null + @refreshStatus() + else + @statusTask = new RepositoryStatusTask(this) + @statusTask.one 'task-completed', => + @statusTask = null + @statusTask.start() getDirectoryStatus: (directoryPath) -> directoryPath = "#{directoryPath}/" diff --git a/src/app/keymaps/atom.cson b/src/app/keymaps/atom.cson index 680f25c17..04cc02488 100644 --- a/src/app/keymaps/atom.cson +++ b/src/app/keymaps/atom.cson @@ -1,4 +1,6 @@ 'body': + 'meta-s': 'core:save' + 'meta-S': 'core:save-as' 'enter': 'core:confirm' 'escape': 'core:cancel' 'meta-w': 'core:close' @@ -30,6 +32,26 @@ 'ctrl-tab': 'window:focus-next-pane' 'ctrl-meta-f': 'window:toggle-full-screen' + 'ctrl-|': 'pane:split-right' + 'ctrl-w v': 'pane:split-right' + 'ctrl--': 'pane:split-down' + 'ctrl-w s': 'pane:split-down' + + 'meta-{': 'pane:show-previous-item' + 'meta-}': 'pane:show-next-item' + 'alt-meta-left': 'pane:show-previous-item' + 'alt-meta-right': 'pane:show-next-item' + 'meta-1': 'pane:show-item-1' + 'meta-2': 'pane:show-item-2' + 'meta-3': 'pane:show-item-3' + 'meta-4': 'pane:show-item-4' + 'meta-5': 'pane:show-item-5' + 'meta-6': 'pane:show-item-6' + 'meta-7': 'pane:show-item-7' + 'meta-8': 'pane:show-item-8' + 'meta-9': 'pane:show-item-9' + 'meta-T': 'pane:reopen-closed-item' + '.tool-panel': 'meta-escape': 'tool-panel:unfocus' 'escape': 'core:close' diff --git a/src/app/keymaps/editor.cson b/src/app/keymaps/editor.cson index 467119136..8b192fece 100644 --- a/src/app/keymaps/editor.cson +++ b/src/app/keymaps/editor.cson @@ -1,9 +1,4 @@ -'body': - 'meta-T': 'editor:undo-close-session' - '.editor': - 'meta-s': 'editor:save' - 'meta-S': 'editor:save-as' 'enter': 'editor:newline' 'meta-enter': 'editor:newline-below' 'meta-shift-enter': 'editor:newline-above' @@ -15,26 +10,9 @@ 'ctrl-{': 'editor:fold-all' 'ctrl-}': 'editor:unfold-all' 'alt-meta-ctrl-f': 'editor:fold-selection' - 'ctrl-|': 'editor:split-right' - 'ctrl-w v': 'editor:split-right' - 'ctrl--': 'editor:split-down' - 'ctrl-w s': 'editor:split-down' 'shift-tab': 'editor:outdent-selected-rows' 'meta-[': 'editor:outdent-selected-rows' 'meta-]': 'editor:indent-selected-rows' - 'meta-{': 'editor:show-previous-buffer' - 'meta-}': 'editor:show-next-buffer' - 'alt-meta-left': 'editor:show-previous-buffer' - 'alt-meta-right': 'editor:show-next-buffer' - 'meta-1': 'editor:show-buffer-1' - 'meta-2': 'editor:show-buffer-2' - 'meta-3': 'editor:show-buffer-3' - 'meta-4': 'editor:show-buffer-4' - 'meta-5': 'editor:show-buffer-5' - 'meta-6': 'editor:show-buffer-6' - 'meta-7': 'editor:show-buffer-7' - 'meta-8': 'editor:show-buffer-8' - 'meta-9': 'editor:show-buffer-9' 'meta-/': 'editor:toggle-line-comments' 'ctrl-W': 'editor:select-word' 'meta-alt-p': 'editor:log-cursor-scope' diff --git a/src/app/pane-grid.coffee b/src/app/pane-axis.coffee similarity index 95% rename from src/app/pane-grid.coffee rename to src/app/pane-axis.coffee index 3d54fd64d..9aa4fda81 100644 --- a/src/app/pane-grid.coffee +++ b/src/app/pane-axis.coffee @@ -2,7 +2,7 @@ $ = require 'jquery' {View} = require 'space-pen' module.exports = -class PaneGrid extends View +class PaneAxis extends View @deserialize: ({children}) -> childViews = children.map (child) -> deserialize(child) new this(childViews) diff --git a/src/app/pane-column.coffee b/src/app/pane-column.coffee index f00c7ed23..43ba40cbb 100644 --- a/src/app/pane-column.coffee +++ b/src/app/pane-column.coffee @@ -1,9 +1,9 @@ $ = require 'jquery' _ = require 'underscore' -PaneGrid = require 'pane-grid' +PaneAxis = require 'pane-axis' module.exports = -class PaneColumn extends PaneGrid +class PaneColumn extends PaneAxis @content: -> @div class: 'column' diff --git a/src/app/pane-container.coffee b/src/app/pane-container.coffee new file mode 100644 index 000000000..6fc367e56 --- /dev/null +++ b/src/app/pane-container.coffee @@ -0,0 +1,97 @@ +{View} = require 'space-pen' +Pane = require 'pane' +$ = require 'jquery' + +module.exports = +class PaneContainer extends View + registerDeserializer(this) + + @deserialize: ({root}) -> + container = new PaneContainer + container.append(deserialize(root)) if root + container + + @content: -> + @div id: 'panes' + + initialize: -> + @destroyedItemStates = [] + + serialize: -> + deserializer: 'PaneContainer' + root: @getRoot()?.serialize() + + focusNextPane: -> + panes = @getPanes() + if panes.length > 1 + currentIndex = panes.indexOf(@getFocusedPane()) + nextIndex = (currentIndex + 1) % panes.length + panes[nextIndex].focus() + true + else + false + + makeNextPaneActive: -> + panes = @getPanes() + currentIndex = panes.indexOf(@getActivePane()) + nextIndex = (currentIndex + 1) % panes.length + panes[nextIndex].makeActive() + + reopenItem: -> + if lastItemState = @destroyedItemStates.pop() + if activePane = @getActivePane() + activePane.showItem(deserialize(lastItemState)) + true + else + @append(new Pane(deserialize(lastItemState))) + + itemDestroyed: (item) -> + state = item.serialize?() + state.uri ?= item.getUri?() + @destroyedItemStates.push(state) if state? + + itemAdded: (item) -> + itemUri = item.getUri?() + @destroyedItemStates = @destroyedItemStates.filter (itemState) -> + itemState.uri isnt itemUri + + getRoot: -> + @children().first().view() + + saveAll: -> + pane.saveItems() for pane in @getPanes() + + getPanes: -> + @find('.pane').views() + + indexOfPane: (pane) -> + @getPanes().indexOf(pane.view()) + + paneAtIndex: (index) -> + @getPanes()[index] + + eachPane: (callback) -> + callback(pane) for pane in @getPanes() + paneAttached = (e) -> callback($(e.target).view()) + @on 'pane:attached', paneAttached + cancel: => @off 'pane:attached', paneAttached + + getFocusedPane: -> + @find('.pane:has(:focus)').view() + + getActivePane: -> + @find('.pane.active').view() ? @find('.pane:first').view() + + getActivePaneItem: -> + @getActivePane()?.activeItem + + getActiveView: -> + @getActivePane()?.activeView + + adjustPaneDimensions: -> + if root = @getRoot() + root.css(width: '100%', height: '100%', top: 0, left: 0) + root.adjustDimensions() + + afterAttach: -> + @adjustPaneDimensions() diff --git a/src/app/pane-row.coffee b/src/app/pane-row.coffee index c729e0b9a..ce7a09f82 100644 --- a/src/app/pane-row.coffee +++ b/src/app/pane-row.coffee @@ -1,9 +1,9 @@ $ = require 'jquery' _ = require 'underscore' -PaneGrid = require 'pane-grid' +PaneAxis = require 'pane-axis' module.exports = -class PaneRow extends PaneGrid +class PaneRow extends PaneAxis @content: -> @div class: 'row' diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 5b0e630f6..ef29ce72f 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -1,4 +1,6 @@ {View} = require 'space-pen' +$ = require 'jquery' +_ = require 'underscore' PaneRow = require 'pane-row' PaneColumn = require 'pane-column' @@ -6,58 +8,314 @@ module.exports = class Pane extends View @content: (wrappedView) -> @div class: 'pane', => - @subview 'wrappedView', wrappedView if wrappedView + @div class: 'item-views', outlet: 'itemViews' - @deserialize: ({wrappedView}) -> - new Pane(deserialize(wrappedView)) + @deserialize: ({items, focused, activeItemUri}) -> + pane = new Pane(items.map((item) -> deserialize(item))...) + pane.showItemForUri(activeItemUri) if activeItemUri + pane.focusOnAttach = true if focused + pane + + activeItem: null + items: null + + initialize: (@items...) -> + @viewsByClassName = {} + @showItem(@items[0]) + + @command 'core:close', @destroyActiveItem + @command 'core:save', @saveActiveItem + @command 'core:save-as', @saveActiveItemAs + @command 'pane:save-items', @saveItems + @command 'pane:show-next-item', @showNextItem + @command 'pane:show-previous-item', @showPreviousItem + + @command 'pane:show-item-1', => @showItemAtIndex(0) + @command 'pane:show-item-2', => @showItemAtIndex(1) + @command 'pane:show-item-3', => @showItemAtIndex(2) + @command 'pane:show-item-4', => @showItemAtIndex(3) + @command 'pane:show-item-5', => @showItemAtIndex(4) + @command 'pane:show-item-6', => @showItemAtIndex(5) + @command 'pane:show-item-7', => @showItemAtIndex(6) + @command 'pane:show-item-8', => @showItemAtIndex(7) + @command 'pane:show-item-9', => @showItemAtIndex(8) + + @command 'pane:split-left', => @splitLeft() + @command 'pane:split-right', => @splitRight() + @command 'pane:split-up', => @splitUp() + @command 'pane:split-down', => @splitDown() + @command 'pane:close', => @destroyItems() + @command 'pane:close-other-items', => @destroyInactiveItems() + @on 'focus', => @activeView.focus(); false + @on 'focusin', => @makeActive() + @on 'focusout', => @autosaveActiveItem() + + afterAttach: (onDom) -> + if @focusOnAttach and onDom + @focusOnAttach = null + @focus() + + return if @attached + @attached = true + @trigger 'pane:attached' + + makeActive: -> + for pane in @getContainer().getPanes() when pane isnt this + pane.makeInactive() + wasActive = @isActive() + @addClass('active') + @trigger 'pane:became-active' unless wasActive + + makeInactive: -> + @removeClass('active') + + isActive: -> + @hasClass('active') + + getItems: -> + new Array(@items...) + + showNextItem: => + index = @getActiveItemIndex() + if index < @items.length - 1 + @showItemAtIndex(index + 1) + else + @showItemAtIndex(0) + + showPreviousItem: => + index = @getActiveItemIndex() + if index > 0 + @showItemAtIndex(index - 1) + else + @showItemAtIndex(@items.length - 1) + + getActiveItemIndex: -> + @items.indexOf(@activeItem) + + showItemAtIndex: (index) -> + @showItem(@itemAtIndex(index)) + + itemAtIndex: (index) -> + @items[index] + + showItem: (item) -> + return if !item? or item is @activeItem + + if @activeItem + @activeItem.off? 'title-changed', @activeItemTitleChanged + @autosaveActiveItem() + + isFocused = @is(':has(:focus)') + @addItem(item) + item.on? 'title-changed', @activeItemTitleChanged + view = @viewForItem(item) + @itemViews.children().not(view).hide() + @itemViews.append(view) unless view.parent().is(@itemViews) + view.show() + view.focus() if isFocused + @activeItem = item + @activeView = view + @trigger 'pane:active-item-changed', [item] + + activeItemTitleChanged: => + @trigger 'pane:active-item-title-changed' + + addItem: (item) -> + return if _.include(@items, item) + index = @getActiveItemIndex() + 1 + @items.splice(index, 0, item) + @getContainer().itemAdded(item) + @trigger 'pane:item-added', [item, index] + item + + destroyActiveItem: => + @destroyItem(@activeItem) + false + + destroyItem: (item) -> + container = @getContainer() + reallyDestroyItem = => + @removeItem(item) + container.itemDestroyed(item) + item.destroy?() + + @autosaveItem(item) + + if item.isModified?() + @promptToSaveItem(item, reallyDestroyItem) + else + reallyDestroyItem() + + destroyItems: -> + @destroyItem(item) for item in @getItems() + + destroyInactiveItems: -> + @destroyItem(item) for item in @getItems() when item isnt @activeItem + + promptToSaveItem: (item, nextAction) -> + uri = item.getUri() + atom.confirm( + "'#{item.getTitle()}' has changes, do you want to save them?" + "Your changes will be lost if close this item without saving." + "Save", => @saveItem(item, nextAction) + "Cancel", null + "Don't Save", nextAction + ) + + saveActiveItem: => + @saveItem(@activeItem) + + saveActiveItemAs: => + @saveItemAs(@activeItem) + + saveItem: (item, nextAction) -> + if item.getUri?() + item.save() + nextAction?() + else + @saveItemAs(item, nextAction) + + saveItemAs: (item, nextAction) -> + return unless item.saveAs? + atom.showSaveDialog (path) => + if path + item.saveAs(path) + nextAction?() + + saveItems: => + @saveItem(item) for item in @getItems() + + autosaveActiveItem: -> + @autosaveItem(@activeItem) + + autosaveItem: (item) -> + @saveItem(item) if config.get('core.autosave') and item.getUri?() + + removeItem: (item) -> + index = @items.indexOf(item) + return if index == -1 + + @showNextItem() if item is @activeItem and @items.length > 1 + _.remove(@items, item) + @cleanupItemView(item) + @trigger 'pane:item-removed', [item, index] + + moveItem: (item, newIndex) -> + oldIndex = @items.indexOf(item) + @items.splice(oldIndex, 1) + @items.splice(newIndex, 0, item) + @trigger 'pane:item-moved', [item, newIndex] + + moveItemToPane: (item, pane, index) -> + @removeItem(item) + pane.addItem(item, index) + + itemForUri: (uri) -> + _.detect @items, (item) -> item.getUri?() is uri + + showItemForUri: (uri) -> + @showItem(@itemForUri(uri)) + + cleanupItemView: (item) -> + if item instanceof $ + viewToRemove = item + else + viewClass = item.getViewClass() + otherItemsForView = @items.filter (i) -> i.getViewClass?() is viewClass + unless otherItemsForView.length + viewToRemove = @viewsByClassName[viewClass.name] + viewToRemove?.setModel(null) + delete @viewsByClassName[viewClass.name] + + if @items.length > 0 + viewToRemove?.remove() + else + @remove() + + viewForItem: (item) -> + if item instanceof $ + item + else + viewClass = item.getViewClass() + if view = @viewsByClassName[viewClass.name] + view.setModel(item) + else + view = @viewsByClassName[viewClass.name] = new viewClass(item) + view + + viewForActiveItem: -> + @viewForItem(@activeItem) serialize: -> deserializer: "Pane" - wrappedView: @wrappedView?.serialize() + focused: @is(':has(:focus)') + activeItemUri: @activeItem.getUri?() if typeof @activeItem.serialize is 'function' + items: _.compact(@getItems().map (item) -> item.serialize?()) adjustDimensions: -> # do nothing - horizontalGridUnits: -> - 1 + horizontalGridUnits: -> 1 - verticalGridUnits: -> - 1 + verticalGridUnits: -> 1 - splitUp: (view) -> - @split(view, 'column', 'before') + splitUp: (items...) -> + @split(items, 'column', 'before') - splitDown: (view) -> - @split(view, 'column', 'after') + splitDown: (items...) -> + @split(items, 'column', 'after') - splitLeft: (view) -> - @split(view, 'row', 'before') + splitLeft: (items...) -> + @split(items, 'row', 'before') - splitRight: (view) -> - @split(view, 'row', 'after') + splitRight: (items...) -> + @split(items, 'row', 'after') - split: (view, axis, side) -> + split: (items, axis, side) -> unless @parent().hasClass(axis) @buildPaneAxis(axis) .insertBefore(this) .append(@detach()) - pane = new Pane(view) + items = [@copyActiveItem()] unless items.length + pane = new Pane(items...) this[side](pane) - rootView.adjustPaneDimensions() - view.focus?() + @getContainer().adjustPaneDimensions() + pane.focus() pane - remove: (selector, keepData) -> - return super if keepData - # find parent elements before removing from dom - parentAxis = @parent('.row, .column') - super - if parentAxis.children().length == 1 - sibling = parentAxis.children().detach() - parentAxis.replaceWith(sibling) - rootView.adjustPaneDimensions() - buildPaneAxis: (axis) -> switch axis when 'row' then new PaneRow when 'column' then new PaneColumn + + getContainer: -> + @closest('#panes').view() + + copyActiveItem: -> + deserialize(@activeItem.serialize()) + + remove: (selector, keepData) -> + return super if keepData + + # find parent elements before removing from dom + container = @getContainer() + parentAxis = @parent('.row, .column') + + if @is(':has(:focus)') + container.focusNextPane() or rootView?.focus() + else if @isActive() + container.makeNextPaneActive() + + super + + if parentAxis.children().length == 1 + sibling = parentAxis.children() + siblingFocused = sibling.is(':has(:focus)') + sibling.detach() + parentAxis.replaceWith(sibling) + sibling.focus() if siblingFocused + container.adjustPaneDimensions() + container.trigger 'pane:removed', [this] + + afterRemove: -> + item.destroy?() for item in @getItems() diff --git a/src/app/project.coffee b/src/app/project.coffee index d31c12238..fac99ce8d 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -95,10 +95,10 @@ class Project getSoftWrap: -> @softWrap setSoftWrap: (@softWrap) -> - buildEditSessionForPath: (filePath, editSessionOptions={}) -> - @buildEditSession(@bufferForPath(filePath), editSessionOptions) + buildEditSession: (filePath, editSessionOptions={}) -> + @buildEditSessionForBuffer(@bufferForPath(filePath), editSessionOptions) - buildEditSession: (buffer, editSessionOptions) -> + buildEditSessionForBuffer: (buffer, editSessionOptions) -> options = _.extend(@defaultEditSessionOptions(), editSessionOptions) options.project = this options.buffer = buffer diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 558143889..2d3b8fa83 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -10,28 +10,29 @@ Project = require 'project' Pane = require 'pane' PaneColumn = require 'pane-column' PaneRow = require 'pane-row' +PaneContainer = require 'pane-container' +EditSession = require 'edit-session' module.exports = class RootView extends View registerDeserializers(this, Pane, PaneRow, PaneColumn, Editor) + @version: 1 + @configDefaults: ignoredNames: [".git", ".svn", ".DS_Store"] disabledPackages: [] - @content: -> + @content: ({panes}={}) -> @div id: 'root-view', => @div id: 'horizontal', outlet: 'horizontal', => @div id: 'vertical', outlet: 'vertical', => - @div id: 'panes', outlet: 'panes' + @subview 'panes', panes ? new PaneContainer - @deserialize: ({ panesViewState, packageStates, projectPath }) -> - atom.atomPackageStates = packageStates ? {} - rootView = new RootView - rootView.setRootPane(deserialize(panesViewState)) if panesViewState - rootView - - title: null + @deserialize: ({ panes, packages, projectPath }) -> + atom.atomPackageStates = packages ? {} + panes = deserialize(panes) if panes?.deserializer is 'PaneContainer' + new RootView({panes}) initialize: -> @command 'toggle-dev-tools', => atom.toggleDevTools() @@ -39,12 +40,11 @@ class RootView extends View @subscribe $(window), 'focus', (e) => @handleFocus(e) if document.activeElement is document.body - @on 'root-view:active-path-changed', (e, path) => - if path - project.setPath(path) unless project.getRootDirectory() - @setTitle(fs.base(path)) - else - @setTitle("untitled") + project.on 'path-changed', => @updateTitle() + @on 'pane:became-active', => @updateTitle() + @on 'pane:active-item-changed', '.active.pane', => @updateTitle() + @on 'pane:removed', => @updateTitle() unless @getActivePane() + @on 'pane:active-item-title-changed', '.active.pane', => @updateTitle() @command 'window:increase-font-size', => config.set("editor.fontSize", config.get("editor.fontSize") + 1) @@ -53,26 +53,31 @@ class RootView extends View fontSize = config.get "editor.fontSize" config.set("editor.fontSize", fontSize - 1) if fontSize > 1 - @command 'window:focus-next-pane', => @focusNextPane() @command 'window:save-all', => @saveAll() @command 'window:toggle-invisibles', => config.set("editor.showInvisibles", !config.get("editor.showInvisibles")) @command 'window:toggle-ignored-files', => config.set("core.hideGitIgnoredFiles", not config.core.hideGitIgnoredFiles) + @command 'window:toggle-auto-indent', => config.set("editor.autoIndent", !config.get("editor.autoIndent")) + @command 'window:toggle-auto-indent-on-paste', => config.set("editor.autoIndentOnPaste", !config.get("editor.autoIndentOnPaste")) + @command 'pane:reopen-closed-item', => + @panes.reopenItem() + serialize: -> + version: RootView.version deserializer: 'RootView' - panesViewState: @panes.children().view()?.serialize() - packageStates: atom.serializeAtomPackages() + panes: @panes.serialize() + packages: atom.serializeAtomPackages() handleFocus: (e) -> - if @getActiveEditor() - @getActiveEditor().focus() + if @getActivePane() + @getActivePane().focus() false else @setTitle(null) @@ -92,118 +97,57 @@ class RootView extends View open: (path, options = {}) -> changeFocus = options.changeFocus ? true - allowActiveEditorChange = options.allowActiveEditorChange ? false - - unless editSession = @openInExistingEditor(path, allowActiveEditorChange, changeFocus) - editSession = project.buildEditSessionForPath(path) - editor = new Editor({editSession}) - pane = new Pane(editor) - @panes.append(pane) - if changeFocus - editor.focus() + path = project.resolve(path) if path? + if activePane = @getActivePane() + if editSession = activePane.itemForUri(path) + activePane.showItem(editSession) else - @makeEditorActive(editor, changeFocus) + editSession = project.buildEditSession(path) + activePane.showItem(editSession) + else + editSession = project.buildEditSession(path) + activePane = new Pane(editSession) + @panes.append(activePane) + activePane.focus() if changeFocus editSession - openInExistingEditor: (path, allowActiveEditorChange, changeFocus) -> - if activeEditor = @getActiveEditor() - activeEditor.focus() if changeFocus - - path = project.resolve(path) if path - - if editSession = activeEditor.activateEditSessionForPath(path) - return editSession - - if allowActiveEditorChange - for editor in @getEditors() - if editSession = editor.activateEditSessionForPath(path) - @makeEditorActive(editor, changeFocus) - return editSession - - editSession = project.buildEditSessionForPath(path) - activeEditor.edit(editSession) - editSession - - editorFocused: (editor) -> - @makeEditorActive(editor) if @panes.containsElement(editor) - - makeEditorActive: (editor, focus) -> - if focus - editor.focus() - return - - previousActiveEditor = @panes.find('.editor.active').view() - previousActiveEditor?.removeClass('active').off('.root-view') - editor.addClass('active') - - if not editor.mini - editor.on 'editor:path-changed.root-view', => - @trigger 'root-view:active-path-changed', editor.getPath() - if not previousActiveEditor or editor.getPath() != previousActiveEditor.getPath() - @trigger 'root-view:active-path-changed', editor.getPath() - - activeKeybindings: -> - keymap.bindingsForElement(document.activeElement) - - getTitle: -> - @title or "untitled" + updateTitle: -> + if projectPath = project.getPath() + if item = @getActivePaneItem() + @setTitle("#{item.getTitle()} - #{projectPath}") + else + @setTitle(projectPath) + else + @setTitle('untitled') setTitle: (title) -> - projectPath = project.getPath() - if not projectPath - @title = "untitled" - else if title - @title = "#{title} – #{projectPath}" - else - @title = projectPath - - @updateWindowTitle() - - updateWindowTitle: -> - document.title = @title + document.title = title getEditors: -> - @panes.find('.pane > .editor').map(-> $(this).view()).toArray() + @panes.find('.pane > .item-views > .editor').map(-> $(this).view()).toArray() getModifiedBuffers: -> modifiedBuffers = [] - for editor in @getEditors() - for session in editor.editSessions - modifiedBuffers.push session.buffer if session.buffer.isModified() - + for pane in @getPanes() + for item in pane.getItems() when item instanceof EditSession + modifiedBuffers.push item.buffer if item.buffer.isModified() modifiedBuffers getOpenBufferPaths: -> _.uniq(_.flatten(@getEditors().map (editor) -> editor.getOpenBufferPaths())) - getActiveEditor: -> - if (editor = @panes.find('.editor.active')).length - editor.view() - else - @panes.find('.editor:first').view() + getActivePane: -> + @panes.getActivePane() - getActiveEditSession: -> - @getActiveEditor()?.activeEditSession + getActivePaneItem: -> + @panes.getActivePaneItem() - focusNextPane: -> - panes = @panes.find('.pane') - currentIndex = panes.toArray().indexOf(@getFocusedPane()[0]) - nextIndex = (currentIndex + 1) % panes.length - panes.eq(nextIndex).view().wrappedView.focus() + getActiveView: -> + @panes.getActiveView() - getFocusedPane: -> - @panes.find('.pane:has(:focus)') - - setRootPane: (pane) -> - @panes.empty() - @panes.append(pane) - @adjustPaneDimensions() - - adjustPaneDimensions: -> - rootPane = @panes.children().first().view() - rootPane?.css(width: '100%', height: '100%', top: 0, left: 0) - rootPane?.adjustDimensions() + focusNextPane: -> @panes.focusNextPane() + getFocusedPane: -> @panes.getFocusedPane() remove: -> editor.remove() for editor in @getEditors() @@ -211,7 +155,16 @@ class RootView extends View super saveAll: -> - editor.save() for editor in @getEditors() + @panes.saveAll() + + eachPane: (callback) -> + @panes.eachPane(callback) + + getPanes: -> + @panes.getPanes() + + indexOfPane: (pane) -> + @panes.indexOfPane(pane) eachEditor: (callback) -> callback(editor) for editor in @getEditors() @@ -223,10 +176,3 @@ class RootView extends View eachBuffer: (callback) -> project.eachBuffer(callback) - indexOfPane: (pane) -> - index = -1 - for p, idx in @panes.find('.pane') - if pane.is(p) - index = idx - break - index diff --git a/src/app/sortable-list.coffee b/src/app/sortable-list.coffee index 08d727646..ebdc675a1 100644 --- a/src/app/sortable-list.coffee +++ b/src/app/sortable-list.coffee @@ -14,7 +14,9 @@ class SortableList extends View @on 'drop', '.sortable', @onDrop onDragStart: (event) => - return false if !@shouldAllowDrag(event) + unless @shouldAllowDrag(event) + event.preventDefault() + return el = @getSortableElement(event) el.addClass 'is-dragging' @@ -45,9 +47,8 @@ class SortableList extends View true getDroppedElement: (event) -> - idx = event.originalEvent.dataTransfer.getData 'sortable-index' - @find ".sortable:eq(#{idx})" + index = event.originalEvent.dataTransfer.getData('sortable-index') + @find(".sortable:eq(#{index})") getSortableElement: (event) -> - el = $(event.target) - if !el.hasClass('sortable') then el.closest('.sortable') else el + $(event.target).closest('.sortable') diff --git a/src/app/window.coffee b/src/app/window.coffee index a4c514b15..321a60f56 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -151,8 +151,16 @@ window.registerDeserializers = (args...) -> window.registerDeserializer = (klass) -> deserializers[klass.name] = klass +window.unregisterDeserializer = (klass) -> + delete deserializers[klass.name] + window.deserialize = (state) -> - deserializers[state?.deserializer]?.deserialize(state) + if deserializer = getDeserializer(state) + return if deserializer.version? and deserializer.version isnt state.version + deserializer.deserialize(state) + +window.getDeserializer = (state) -> + deserializers[state?.deserializer] window.measure = (description, fn) -> start = new Date().getTime() diff --git a/src/packages/autocomplete/spec/autocomplete-spec.coffee b/src/packages/autocomplete/spec/autocomplete-spec.coffee index 8d019d69b..9bfa9af7f 100644 --- a/src/packages/autocomplete/spec/autocomplete-spec.coffee +++ b/src/packages/autocomplete/spec/autocomplete-spec.coffee @@ -17,8 +17,8 @@ describe "Autocomplete", -> autocompletePackage = window.loadPackage("autocomplete") expect(AutocompleteView.prototype.initialize).not.toHaveBeenCalled() - leftEditor = rootView.getActiveEditor() - rightEditor = rootView.getActiveEditor().splitRight() + leftEditor = rootView.getActiveView() + rightEditor = leftEditor.splitRight() leftEditor.trigger 'autocomplete:attach' expect(leftEditor.find('.autocomplete')).toExist() @@ -40,7 +40,7 @@ describe "AutocompleteView", -> beforeEach -> window.rootView = new RootView - editor = new Editor(editSession: fixturesProject.buildEditSessionForPath('sample.js')) + editor = new Editor(editSession: project.buildEditSession('sample.js')) window.loadPackage('autocomplete') autocomplete = new AutocompleteView(editor) miniEditor = autocomplete.miniEditor diff --git a/src/packages/autoflow/spec/autoflow-spec.coffee b/src/packages/autoflow/spec/autoflow-spec.coffee index d717bcf85..190ac238c 100644 --- a/src/packages/autoflow/spec/autoflow-spec.coffee +++ b/src/packages/autoflow/spec/autoflow-spec.coffee @@ -7,7 +7,7 @@ describe "Autoflow package", -> window.rootView = new RootView rootView.open() window.loadPackage 'autoflow' - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() config.set('editor.preferredLineLength', 30) describe "autoflow:reflow-paragraph", -> diff --git a/src/packages/bracket-matcher/spec/bracket-matcher-spec.coffee b/src/packages/bracket-matcher/spec/bracket-matcher-spec.coffee index ff56fec57..099be0926 100644 --- a/src/packages/bracket-matcher/spec/bracket-matcher-spec.coffee +++ b/src/packages/bracket-matcher/spec/bracket-matcher-spec.coffee @@ -8,7 +8,7 @@ describe "bracket matching", -> rootView.open('sample.js') window.loadPackage('bracket-matcher') rootView.attachToDom() - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() editSession = editor.activeEditSession buffer = editSession.buffer diff --git a/src/packages/command-logger/spec/command-logger-spec.coffee b/src/packages/command-logger/spec/command-logger-spec.coffee index 7210c171f..4305f9ada 100644 --- a/src/packages/command-logger/spec/command-logger-spec.coffee +++ b/src/packages/command-logger/spec/command-logger-spec.coffee @@ -9,7 +9,7 @@ describe "CommandLogger", -> rootView.open('sample.js') commandLogger = window.loadPackage('command-logger').packageMain commandLogger.eventLog = {} - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() describe "when a command is triggered", -> it "records the number of times the command is triggered", -> diff --git a/src/packages/command-palette/spec/command-palette-spec.coffee b/src/packages/command-palette/spec/command-palette-spec.coffee index 2b115bc68..414c53996 100644 --- a/src/packages/command-palette/spec/command-palette-spec.coffee +++ b/src/packages/command-palette/spec/command-palette-spec.coffee @@ -19,8 +19,8 @@ describe "CommandPalette", -> describe "when command-palette:toggle is triggered on the root view", -> it "shows a list of all valid command descriptions, names, and keybindings for the previously focused element", -> - keyBindings = _.losslessInvert(keymap.bindingsForElement(rootView.getActiveEditor())) - for eventName, description of rootView.getActiveEditor().events() + keyBindings = _.losslessInvert(keymap.bindingsForElement(rootView.getActiveView())) + for eventName, description of rootView.getActiveView().events() eventLi = palette.list.children("[data-event-name='#{eventName}']") if description expect(eventLi).toExist() @@ -32,7 +32,7 @@ describe "CommandPalette", -> expect(eventLi).not.toExist() it "displays all commands registerd on the window", -> - editorEvents = rootView.getActiveEditor().events() + editorEvents = rootView.getActiveView().events() windowEvents = $(window).events() expect(_.isEmpty(windowEvents)).toBeFalsy() for eventName, description of windowEvents @@ -60,19 +60,19 @@ describe "CommandPalette", -> expect(palette.hasParent()).toBeTruthy() palette.trigger 'command-palette:toggle' expect(palette.hasParent()).toBeFalsy() - expect(rootView.getActiveEditor().isFocused).toBeTruthy() + expect(rootView.getActiveView().isFocused).toBeTruthy() describe "when the command palette is cancelled", -> it "focuses the root view and detaches the command palette", -> expect(palette.hasParent()).toBeTruthy() palette.cancel() expect(palette.hasParent()).toBeFalsy() - expect(rootView.getActiveEditor().isFocused).toBeTruthy() + expect(rootView.getActiveView().isFocused).toBeTruthy() describe "when an command selection is confirmed", -> it "detaches the palette, then focuses the previously focused element and emits the selected command on it", -> eventHandler = jasmine.createSpy 'eventHandler' - activeEditor = rootView.getActiveEditor() + activeEditor = rootView.getActiveView() {eventName} = palette.array[5] activeEditor.preempt eventName, eventHandler diff --git a/src/packages/command-panel/lib/command-panel-view.coffee b/src/packages/command-panel/lib/command-panel-view.coffee index 3ede83809..2430c7ae0 100644 --- a/src/packages/command-panel/lib/command-panel-view.coffee +++ b/src/packages/command-panel/lib/command-panel-view.coffee @@ -120,7 +120,7 @@ class CommandPanelView extends View @errorMessages.empty() try - @commandInterpreter.eval(command, rootView.getActiveEditSession()).done ({operationsToPreview, errorMessages}) => + @commandInterpreter.eval(command, rootView.getActivePaneItem()).done ({operationsToPreview, errorMessages}) => @loadingMessage.hide() @history.push(command) @historyIndex = @history.length @@ -155,12 +155,12 @@ class CommandPanelView extends View @miniEditor.setText(@history[@historyIndex] or '') repeatRelativeAddress: -> - @commandInterpreter.repeatRelativeAddress(rootView.getActiveEditSession()) + @commandInterpreter.repeatRelativeAddress(rootView.getActivePaneItem()) repeatRelativeAddressInReverse: -> - @commandInterpreter.repeatRelativeAddressInReverse(rootView.getActiveEditSession()) + @commandInterpreter.repeatRelativeAddressInReverse(rootView.getActivePaneItem()) setSelectionAsLastRelativeAddress: -> - selection = rootView.getActiveEditor().getSelectedText() + selection = rootView.getActiveView().getSelectedText() regex = _.escapeRegExp(selection) @commandInterpreter.lastRelativeAddress = new CompositeCommand([new RegexAddress(regex)]) diff --git a/src/packages/command-panel/spec/command-interpreter-spec.coffee b/src/packages/command-panel/spec/command-interpreter-spec.coffee index 289d87709..c4602b59f 100644 --- a/src/packages/command-panel/spec/command-interpreter-spec.coffee +++ b/src/packages/command-panel/spec/command-interpreter-spec.coffee @@ -6,12 +6,11 @@ EditSession = require 'edit-session' _ = require 'underscore' describe "CommandInterpreter", -> - [project, interpreter, editSession, buffer] = [] + [interpreter, editSession, buffer] = [] beforeEach -> - project = new Project(fixturesProject.resolve('dir/')) - interpreter = new CommandInterpreter(fixturesProject) - editSession = fixturesProject.buildEditSessionForPath('sample.js') + interpreter = new CommandInterpreter(project) + editSession = project.buildEditSession('sample.js') buffer = editSession.buffer afterEach -> @@ -418,7 +417,7 @@ describe "CommandInterpreter", -> describe "X x/regex/", -> it "returns selection operations for all regex matches in all the project's files", -> editSession.destroy() - project = new Project(fixturesProject.resolve('dir/')) + project.setPath(project.resolve('dir')) interpreter = new CommandInterpreter(project) operationsToPreview = null @@ -428,7 +427,7 @@ describe "CommandInterpreter", -> runs -> expect(operationsToPreview.length).toBeGreaterThan 3 for operation in operationsToPreview - editSession = project.buildEditSessionForPath(operation.getPath()) + editSession = project.buildEditSession(operation.getPath()) editSession.setSelectedBufferRange(operation.execute(editSession)) expect(editSession.getSelectedText()).toMatch /a+/ editSession.destroy() diff --git a/src/packages/command-panel/spec/command-panel-spec.coffee b/src/packages/command-panel/spec/command-panel-spec.coffee index e6005c4be..213d14191 100644 --- a/src/packages/command-panel/spec/command-panel-spec.coffee +++ b/src/packages/command-panel/spec/command-panel-spec.coffee @@ -3,14 +3,14 @@ CommandPanelView = require 'command-panel/lib/command-panel-view' _ = require 'underscore' describe "CommandPanel", -> - [editor, buffer, commandPanel] = [] + [editSession, buffer, commandPanel] = [] beforeEach -> window.rootView = new RootView rootView.open('sample.js') rootView.enableKeymap() - editor = rootView.getActiveEditor() - buffer = editor.activeEditSession.buffer + editSession = rootView.getActivePaneItem() + buffer = editSession.buffer commandPanelMain = window.loadPackage('command-panel', activateImmediately: true).packageMain commandPanel = commandPanelMain.commandPanelView commandPanel.history = [] @@ -219,41 +219,41 @@ describe "CommandPanel", -> it "repeats the last search command if there is one", -> rootView.trigger 'command-panel:repeat-relative-address' - editor.setCursorScreenPosition([4, 0]) + editSession.setCursorScreenPosition([4, 0]) commandPanel.execute("/current") - expect(editor.getSelection().getBufferRange()).toEqual [[5,6], [5,13]] + expect(editSession.getSelectedBufferRange()).toEqual [[5,6], [5,13]] rootView.trigger 'command-panel:repeat-relative-address' - expect(editor.getSelection().getBufferRange()).toEqual [[6,6], [6,13]] + expect(editSession.getSelectedBufferRange()).toEqual [[6,6], [6,13]] commandPanel.execute('s/r/R/g') rootView.trigger 'command-panel:repeat-relative-address' - expect(editor.getSelection().getBufferRange()).toEqual [[6,34], [6,41]] + expect(editSession.getSelectedBufferRange()).toEqual [[6,34], [6,41]] commandPanel.execute('0') commandPanel.execute('/sort/ s/r/R/') # this contains a substitution... won't be repeated rootView.trigger 'command-panel:repeat-relative-address' - expect(editor.getSelection().getBufferRange()).toEqual [[3,31], [3,38]] + expect(editSession.getSelectedBufferRange()).toEqual [[3,31], [3,38]] describe "when command-panel:repeat-relative-address-in-reverse is triggered on the root view", -> it "it repeats the last relative address in the reverse direction", -> rootView.trigger 'command-panel:repeat-relative-address-in-reverse' - editor.setCursorScreenPosition([6, 0]) + editSession.setCursorScreenPosition([6, 0]) commandPanel.execute("/current") - expect(editor.getSelection().getBufferRange()).toEqual [[6,6], [6,13]] + expect(editSession.getSelectedBufferRange()).toEqual [[6,6], [6,13]] rootView.trigger 'command-panel:repeat-relative-address-in-reverse' - expect(editor.getSelection().getBufferRange()).toEqual [[5,6], [5,13]] + expect(editSession.getSelectedBufferRange()).toEqual [[5,6], [5,13]] describe "when command-panel:set-selection-as-regex-address is triggered on the root view", -> it "sets the @lastRelativeAddress to a RegexAddress of the current selection", -> rootView.open(require.resolve('fixtures/sample.js')) - rootView.getActiveEditor().setSelectedBufferRange([[1,21],[1,28]]) + rootView.getActivePaneItem().setSelectedBufferRange([[1,21],[1,28]]) commandInterpreter = commandPanel.commandInterpreter expect(commandInterpreter.lastRelativeAddress).toBeUndefined() @@ -267,7 +267,7 @@ describe "CommandPanel", -> commandPanel.miniEditor.setText("foo") commandPanel.miniEditor.setCursorBufferPosition([0, 0]) - rootView.getActiveEditor().trigger "command-panel:find-in-file" + rootView.getActiveView().trigger "command-panel:find-in-file" expect(commandPanel.attach).toHaveBeenCalled() expect(commandPanel.parent).not.toBeEmpty() expect(commandPanel.miniEditor.getText()).toBe "/" @@ -297,8 +297,8 @@ describe "CommandPanel", -> describe "when the command returns operations to be previewed", -> beforeEach -> + rootView.getActivePane().remove() rootView.attachToDom() - editor.remove() rootView.trigger 'command-panel:toggle' waitsForPromise -> commandPanel.execute('X x/quicksort/') @@ -350,16 +350,14 @@ describe "CommandPanel", -> expect(commandPanel).toBeVisible() expect(commandPanel.errorMessages).not.toBeVisible() - describe "when the command contains an escaped character", -> it "executes the command with the escaped character (instead of as a backslash followed by the character)", -> rootView.trigger 'command-panel:toggle' editSession = rootView.open(require.resolve 'fixtures/sample-with-tabs.coffee') - editor.edit(editSession) commandPanel.miniEditor.setText "/\\tsell" commandPanel.miniEditor.hiddenInput.trigger keydownEvent('enter') - expect(editor.getSelectedBufferRange()).toEqual [[3,1],[3,6]] + expect(editSession.getSelectedBufferRange()).toEqual [[3,1],[3,6]] describe "when move-up and move-down are triggerred on the editor", -> it "navigates forward and backward through the command history", -> @@ -470,11 +468,11 @@ describe "CommandPanel", -> previewList.trigger 'core:confirm' - editSession = rootView.getActiveEditSession() + editSession = rootView.getActivePaneItem() expect(editSession.buffer.getPath()).toBe project.resolve(operation.getPath()) expect(editSession.getSelectedBufferRange()).toEqual operation.getBufferRange() expect(editSession.getSelectedBufferRange()).toEqual operation.getBufferRange() - expect(editor.isScreenRowVisible(editor.getCursorScreenRow())).toBeTruthy() + expect(rootView.getActiveView().isScreenRowVisible(editSession.getCursorScreenRow())).toBeTruthy() expect(previewList.focus).toHaveBeenCalled() expect(executeHandler).not.toHaveBeenCalled() @@ -496,7 +494,7 @@ describe "CommandPanel", -> previewList.find('li.operation:eq(4) span').mousedown() expect(previewList.getSelectedOperation()).toBe operation - editSession = rootView.getActiveEditSession() + editSession = rootView.getActivePaneItem() expect(editSession.buffer.getPath()).toBe project.resolve(operation.getPath()) expect(editSession.getSelectedBufferRange()).toEqual operation.getBufferRange() expect(previewList.focus).toHaveBeenCalled() diff --git a/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee b/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee index 3eddc0c8e..d12f6f29a 100644 --- a/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee +++ b/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee @@ -22,19 +22,18 @@ class FuzzyFinderView extends SelectList @subscribe $(window), 'focus', => @reloadProjectPaths = true @observeConfig 'fuzzy-finder.ignoredNames', => @reloadProjectPaths = true - rootView.eachEditor (editor) -> - editor.activeEditSession.lastOpened = (new Date) - 1 - editor.on 'editor:active-edit-session-changed', (e, editSession, index) -> - editSession.lastOpened = (new Date) - 1 + rootView.eachPane (pane) -> + pane.activeItem.lastOpened = (new Date) - 1 + pane.on 'pane:active-item-changed', (e, item) -> item.lastOpened = (new Date) - 1 - @miniEditor.command 'editor:split-left', => - @splitOpenPath (editor, session) -> editor.splitLeft(session) - @miniEditor.command 'editor:split-right', => - @splitOpenPath (editor, session) -> editor.splitRight(session) - @miniEditor.command 'editor:split-down', => - @splitOpenPath (editor, session) -> editor.splitDown(session) - @miniEditor.command 'editor:split-up', => - @splitOpenPath (editor, session) -> editor.splitUp(session) + @miniEditor.command 'pane:split-left', => + @splitOpenPath (pane, session) -> pane.splitLeft(session) + @miniEditor.command 'pane:split-right', => + @splitOpenPath (pane, session) -> pane.splitRight(session) + @miniEditor.command 'pane:split-down', => + @splitOpenPath (pane, session) -> pane.splitDown(session) + @miniEditor.command 'pane:split-up', => + @splitOpenPath (pane, session) -> pane.splitUp(session) itemForElement: (path) -> $$ -> @@ -70,10 +69,8 @@ class FuzzyFinderView extends SelectList splitOpenPath: (fn) -> path = @getSelectedElement() return unless path - - editor = rootView.getActiveEditor() - if editor - fn(editor, project.buildEditSessionForPath(path)) + if pane = rootView.getActivePane() + fn(pane, project.buildEditSession(path)) else @openPath(path) @@ -118,7 +115,7 @@ class FuzzyFinderView extends SelectList else return unless project.getPath()? @allowActiveEditorChange = false - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() currentWord = editor.getWordUnderCursor(wordRegex: @filenameRegex) if currentWord.length == 0 @@ -177,7 +174,7 @@ class FuzzyFinderView extends SelectList editSession.getPath()? editSessions = _.sortBy editSessions, (editSession) => - if editSession is rootView.getActiveEditSession() + if editSession is rootView.getActivePaneItem() 0 else -(editSession.lastOpened or 1) diff --git a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee index f322d4613..ffaae8cb0 100644 --- a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee +++ b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee @@ -21,8 +21,8 @@ describe 'FuzzyFinder', -> it "shows the FuzzyFinder or hides it and returns focus to the active editor if it already showing", -> rootView.attachToDom() expect(rootView.find('.fuzzy-finder')).not.toExist() - rootView.find('.editor').trigger 'editor:split-right' - [editor1, editor2] = rootView.find('.editor').map -> $(this).view() + rootView.getActiveView().splitRight() + [editor1, editor2] = rootView.getEditors() expect(rootView.find('.fuzzy-finder')).not.toExist() rootView.trigger 'fuzzy-finder:toggle-file-finder' @@ -72,13 +72,13 @@ describe 'FuzzyFinder', -> describe "when a path selection is confirmed", -> it "opens the file associated with that path in the editor", -> rootView.attachToDom() - editor1 = rootView.getActiveEditor() + editor1 = rootView.getActiveView() editor2 = editor1.splitRight() - expect(rootView.getActiveEditor()).toBe editor2 + expect(rootView.getActiveView()).toBe editor2 rootView.trigger 'fuzzy-finder:toggle-file-finder' finderView.confirmed('dir/a') - expectedPath = fixturesProject.resolve('dir/a') + expectedPath = project.resolve('dir/a') expect(finderView.hasParent()).toBeFalsy() expect(editor1.getPath()).not.toBe expectedPath @@ -88,26 +88,26 @@ describe 'FuzzyFinder', -> describe "when the selected path isn't a file that exists", -> it "leaves the the tree view open, doesn't open the path in the editor, and displays an error", -> rootView.attachToDom() - path = rootView.getActiveEditor().getPath() + path = rootView.getActiveView().getPath() rootView.trigger 'fuzzy-finder:toggle-file-finder' finderView.confirmed('dir/this/is/not/a/file.txt') expect(finderView.hasParent()).toBeTruthy() - expect(rootView.getActiveEditor().getPath()).toBe path + expect(rootView.getActiveView().getPath()).toBe path expect(finderView.find('.error').text().length).toBeGreaterThan 0 advanceClock(2000) expect(finderView.find('.error').text().length).toBe 0 describe "buffer-finder behavior", -> describe "toggling", -> - describe "when the active editor contains edit sessions for buffers with paths", -> + describe "when there are pane items with paths", -> beforeEach -> rootView.open('sample.txt') - it "shows the FuzzyFinder or hides it, returning focus to the active editor if", -> + it "shows the FuzzyFinder if it isn't showing, or hides it and returns focus to the active editor", -> rootView.attachToDom() expect(rootView.find('.fuzzy-finder')).not.toExist() - rootView.find('.editor').trigger 'editor:split-right' - [editor1, editor2] = rootView.find('.editor').map -> $(this).view() + rootView.getActiveView().splitRight() + [editor1, editor2] = rootView.getEditors() rootView.trigger 'fuzzy-finder:toggle-buffer-finder' expect(rootView.find('.fuzzy-finder')).toExist() @@ -122,26 +122,17 @@ describe 'FuzzyFinder', -> rootView.trigger 'fuzzy-finder:toggle-buffer-finder' expect(finderView.miniEditor.getText()).toBe '' - it "lists the paths of the current open buffers by most recently modified", -> + it "lists the paths of the current items, sorted by most recently opened but with the current item last", -> rootView.attachToDom() rootView.open 'sample-with-tabs.coffee' rootView.trigger 'fuzzy-finder:toggle-buffer-finder' - children = finderView.list.children('li') - expect(children.get(0).outerText).toBe "sample.txt" - expect(children.get(1).outerText).toBe "sample.js" - expect(children.get(2).outerText).toBe "sample-with-tabs.coffee" + expect(_.pluck(finderView.list.children('li'), 'outerText')).toEqual ['sample.txt', 'sample.js', 'sample-with-tabs.coffee'] + rootView.trigger 'fuzzy-finder:toggle-buffer-finder' rootView.open 'sample.txt' rootView.trigger 'fuzzy-finder:toggle-buffer-finder' - children = finderView.list.children('li') - expect(children.get(0).outerText).toBe "sample-with-tabs.coffee" - expect(children.get(1).outerText).toBe "sample.js" - expect(children.get(2).outerText).toBe "sample.txt" - expect(finderView.list.children('li').length).toBe 3 - expect(finderView.list.find("li:contains(sample.js)")).toExist() - expect(finderView.list.find("li:contains(sample.txt)")).toExist() - expect(finderView.list.find("li:contains(sample-with-tabs.coffee)")).toExist() + expect(_.pluck(finderView.list.children('li'), 'outerText')).toEqual ['sample-with-tabs.coffee', 'sample.js', 'sample.txt'] expect(finderView.list.children().first()).toHaveClass 'selected' it "serializes the list of paths and their last opened time", -> @@ -151,29 +142,26 @@ describe 'FuzzyFinder', -> rootView.trigger 'fuzzy-finder:toggle-buffer-finder' rootView.open() - states = rootView.serialize().packageStates + states = rootView.serialize().packages states = _.map states['fuzzy-finder'], (path, time) -> [ path, time ] states = _.sortBy states, (path, time) -> -time paths = [ 'sample-with-tabs.coffee', 'sample.txt', 'sample.js' ] + for [time, path] in states expect(_.last path.split '/').toBe paths.shift() expect(time).toBeGreaterThan 50000 - describe "when the active editor only contains edit sessions for anonymous buffers", -> + describe "when there are only panes with anonymous items", -> it "does not open", -> - editor = rootView.getActiveEditor() - editor.edit(project.buildEditSessionForPath()) - editor.loadPreviousEditSession() - editor.destroyActiveEditSession() - expect(editor.getOpenBufferPaths().length).toBe 0 + rootView.getActivePane().remove() + rootView.open() rootView.trigger 'fuzzy-finder:toggle-buffer-finder' expect(rootView.find('.fuzzy-finder')).not.toExist() - describe "when there is no active editor", -> + describe "when there are no pane items", -> it "does not open", -> - rootView.getActiveEditor().destroyActiveEditSession() - expect(rootView.getActiveEditor()).toBeUndefined() + rootView.getActivePane().remove() rootView.trigger 'fuzzy-finder:toggle-buffer-finder' expect(rootView.find('.fuzzy-finder')).not.toExist() @@ -182,16 +170,16 @@ describe 'FuzzyFinder', -> beforeEach -> rootView.attachToDom() - editor1 = rootView.getActiveEditor() + editor1 = rootView.getActiveView() editor2 = editor1.splitRight() - expect(rootView.getActiveEditor()).toBe editor2 + expect(rootView.getActiveView()).toBe editor2 rootView.open('sample.txt') - editor2.loadPreviousEditSession() + editor2.trigger 'pane:show-previous-item' rootView.trigger 'fuzzy-finder:toggle-buffer-finder' - describe "when there is an edit session for the confirmed path in the active editor", -> - it "switches the active editor to the edit session for the selected path", -> - expectedPath = fixturesProject.resolve('sample.txt') + describe "when the active pane has an item for the selected path", -> + it "switches to the item for the selected path", -> + expectedPath = project.resolve('sample.txt') finderView.confirmed('sample.txt') expect(finderView.hasParent()).toBeFalsy() @@ -199,27 +187,26 @@ describe 'FuzzyFinder', -> expect(editor2.getPath()).toBe expectedPath expect(editor2.isFocused).toBeTruthy() - describe "when there is NO edit session for the confirmed path on the active editor, but there is one on another editor", -> - it "focuses the editor that contains an edit session for the selected path", -> + describe "when the active pane does not have an item for the selected path", -> + it "adds a new item to the active pane for the selcted path", -> rootView.trigger 'fuzzy-finder:toggle-buffer-finder' editor1.focus() rootView.trigger 'fuzzy-finder:toggle-buffer-finder' - expect(rootView.getActiveEditor()).toBe editor1 + expect(rootView.getActiveView()).toBe editor1 - expectedPath = fixturesProject.resolve('sample.txt') + expectedPath = project.resolve('sample.txt') finderView.confirmed('sample.txt') expect(finderView.hasParent()).toBeFalsy() - expect(editor1.getPath()).not.toBe expectedPath - expect(editor2.getPath()).toBe expectedPath - expect(editor2.isFocused).toBeTruthy() + expect(editor1.getPath()).toBe expectedPath + expect(editor1.isFocused).toBeTruthy() describe "git-status-finder behavior", -> [originalText, originalPath, newPath] = [] beforeEach -> - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() originalText = editor.getText() originalPath = editor.getPath() fs.write(originalPath, 'making a change for the better') @@ -248,7 +235,7 @@ describe 'FuzzyFinder', -> describe "when an editor is open", -> it "detaches the finder and focuses the previously focused element", -> rootView.attachToDom() - activeEditor = rootView.getActiveEditor() + activeEditor = rootView.getActiveView() activeEditor.focus() rootView.trigger 'fuzzy-finder:toggle-file-finder' @@ -265,7 +252,7 @@ describe 'FuzzyFinder', -> describe "when no editors are open", -> it "detaches the finder and focuses the previously focused element", -> rootView.attachToDom() - rootView.getActiveEditor().destroyActiveEditSession() + rootView.getActivePane().remove() inputView = $$ -> @input() rootView.append(inputView) @@ -351,7 +338,7 @@ describe 'FuzzyFinder', -> editor = null beforeEach -> - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() rootView.attachToDom() it "opens the fuzzy finder window when there are multiple matches", -> @@ -405,59 +392,63 @@ describe 'FuzzyFinder', -> expect(finderView.find('.error').text().length).toBeGreaterThan 0 describe "opening a path into a split", -> - beforeEach -> - rootView.attachToDom() + it "opens the path by splitting the active editor left", -> + expect(rootView.getPanes().length).toBe 1 + pane = rootView.getActivePane() + spyOn(pane, "splitLeft").andCallThrough() - describe "when an editor is active", -> - it "opens the path by splitting the active editor left", -> - editor = rootView.getActiveEditor() - spyOn(editor, "splitLeft").andCallThrough() - expect(rootView.find('.editor').length).toBe 1 - rootView.trigger 'fuzzy-finder:toggle-buffer-finder' - finderView.miniEditor.trigger 'editor:split-left' - expect(rootView.find('.editor').length).toBe 2 - expect(editor.splitLeft).toHaveBeenCalled() - expect(rootView.getActiveEditor()).not.toBe editor - expect(rootView.getActiveEditor().getPath()).toBe editor.getPath() + rootView.trigger 'fuzzy-finder:toggle-buffer-finder' + path = finderView.getSelectedElement() + finderView.miniEditor.trigger 'pane:split-left' - it "opens the path by splitting the active editor right", -> - editor = rootView.getActiveEditor() - spyOn(editor, "splitRight").andCallThrough() - expect(rootView.find('.editor').length).toBe 1 - rootView.trigger 'fuzzy-finder:toggle-buffer-finder' - finderView.miniEditor.trigger 'editor:split-right' - expect(rootView.find('.editor').length).toBe 2 - expect(editor.splitRight).toHaveBeenCalled() - expect(rootView.getActiveEditor()).not.toBe editor - expect(rootView.getActiveEditor().getPath()).toBe editor.getPath() + expect(rootView.getPanes().length).toBe 2 + expect(pane.splitLeft).toHaveBeenCalled() + expect(rootView.getActiveView().getPath()).toBe project.resolve(path) - it "opens the path by splitting the active editor down", -> - editor = rootView.getActiveEditor() - spyOn(editor, "splitDown").andCallThrough() - expect(rootView.find('.editor').length).toBe 1 - rootView.trigger 'fuzzy-finder:toggle-buffer-finder' - finderView.miniEditor.trigger 'editor:split-down' - expect(rootView.find('.editor').length).toBe 2 - expect(editor.splitDown).toHaveBeenCalled() - expect(rootView.getActiveEditor()).not.toBe editor - expect(rootView.getActiveEditor().getPath()).toBe editor.getPath() + it "opens the path by splitting the active editor right", -> + expect(rootView.getPanes().length).toBe 1 + pane = rootView.getActivePane() + spyOn(pane, "splitRight").andCallThrough() - it "opens the path by splitting the active editor up", -> - editor = rootView.getActiveEditor() - spyOn(editor, "splitUp").andCallThrough() - expect(rootView.find('.editor').length).toBe 1 - rootView.trigger 'fuzzy-finder:toggle-buffer-finder' - finderView.miniEditor.trigger 'editor:split-up' - expect(rootView.find('.editor').length).toBe 2 - expect(editor.splitUp).toHaveBeenCalled() - expect(rootView.getActiveEditor()).not.toBe editor - expect(rootView.getActiveEditor().getPath()).toBe editor.getPath() + rootView.trigger 'fuzzy-finder:toggle-buffer-finder' + path = finderView.getSelectedElement() + finderView.miniEditor.trigger 'pane:split-right' + + expect(rootView.getPanes().length).toBe 2 + expect(pane.splitRight).toHaveBeenCalled() + expect(rootView.getActiveView().getPath()).toBe project.resolve(path) + + it "opens the path by splitting the active editor up", -> + expect(rootView.getPanes().length).toBe 1 + pane = rootView.getActivePane() + spyOn(pane, "splitUp").andCallThrough() + + rootView.trigger 'fuzzy-finder:toggle-buffer-finder' + path = finderView.getSelectedElement() + finderView.miniEditor.trigger 'pane:split-up' + + expect(rootView.getPanes().length).toBe 2 + expect(pane.splitUp).toHaveBeenCalled() + expect(rootView.getActiveView().getPath()).toBe project.resolve(path) + + it "opens the path by splitting the active editor down", -> + expect(rootView.getPanes().length).toBe 1 + pane = rootView.getActivePane() + spyOn(pane, "splitDown").andCallThrough() + + rootView.trigger 'fuzzy-finder:toggle-buffer-finder' + path = finderView.getSelectedElement() + finderView.miniEditor.trigger 'pane:split-down' + + expect(rootView.getPanes().length).toBe 2 + expect(pane.splitDown).toHaveBeenCalled() + expect(rootView.getActiveView().getPath()).toBe project.resolve(path) describe "git status decorations", -> [originalText, originalPath, editor, newPath] = [] beforeEach -> - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() originalText = editor.getText() originalPath = editor.getPath() newPath = project.resolve('newsample.js') @@ -470,7 +461,7 @@ describe 'FuzzyFinder', -> describe "when a modified file is shown in the list", -> it "displays the modified icon", -> editor.setText('modified') - editor.save() + editor.activeEditSession.save() git.getPathStatus(editor.getPath()) rootView.trigger 'fuzzy-finder:toggle-buffer-finder' diff --git a/src/packages/gfm.tmbundle/spec/gfm-spec.coffee b/src/packages/gfm.tmbundle/spec/gfm-spec.coffee index db307ab65..97bc98300 100644 --- a/src/packages/gfm.tmbundle/spec/gfm-spec.coffee +++ b/src/packages/gfm.tmbundle/spec/gfm-spec.coffee @@ -136,6 +136,6 @@ describe "GitHub Flavored Markdown grammar", -> describe "auto indent", -> it "indents newlines entered after list lines", -> config.set('editor.autoIndent', true) - editSession = fixturesProject.buildEditSessionForPath('gfm.md') + editSession = project.buildEditSession('gfm.md') editSession.insertNewlineBelow() expect(editSession.buffer.lineForRow(1)).toBe ' ' diff --git a/src/packages/gists/lib/gists.coffee b/src/packages/gists/lib/gists.coffee index 4cc241fcd..6df96a01d 100644 --- a/src/packages/gists/lib/gists.coffee +++ b/src/packages/gists/lib/gists.coffee @@ -9,7 +9,7 @@ class Gists rootView.command 'gist:create', '.editor', => @createGist() createGist: (editor) -> - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() return unless editor? gist = { public: false, files: {} } diff --git a/src/packages/gists/spec/gists-spec.coffee b/src/packages/gists/spec/gists-spec.coffee index 430b727ad..3091f8ed6 100644 --- a/src/packages/gists/spec/gists-spec.coffee +++ b/src/packages/gists/spec/gists-spec.coffee @@ -8,7 +8,7 @@ describe "Gists package", -> window.rootView = new RootView rootView.open('sample.js') window.loadPackage('gists') - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() spyOn($, 'ajax') describe "when gist:create is triggered on an editor", -> diff --git a/src/packages/go-to-line/lib/go-to-line-view.coffee b/src/packages/go-to-line/lib/go-to-line-view.coffee index 1f24afd39..d1da9b764 100644 --- a/src/packages/go-to-line/lib/go-to-line-view.coffee +++ b/src/packages/go-to-line/lib/go-to-line-view.coffee @@ -38,7 +38,7 @@ class GoToLineView extends View confirm: -> lineNumber = @miniEditor.getText() - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() @detach() @@ -51,5 +51,5 @@ class GoToLineView extends View attach: -> @previouslyFocusedElement = $(':focus') rootView.append(this) - @message.text("Enter a line number 1-#{rootView.getActiveEditor().getLineCount()}") + @message.text("Enter a line number 1-#{rootView.getActiveView().getLineCount()}") @miniEditor.focus() diff --git a/src/packages/go-to-line/spec/go-to-line-spec.coffee b/src/packages/go-to-line/spec/go-to-line-spec.coffee index fe0c8b7df..bddef9426 100644 --- a/src/packages/go-to-line/spec/go-to-line-spec.coffee +++ b/src/packages/go-to-line/spec/go-to-line-spec.coffee @@ -8,7 +8,7 @@ describe 'GoToLine', -> window.rootView = new RootView rootView.open('sample.js') rootView.enableKeymap() - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() goToLine = GoToLineView.activate() editor.setCursorBufferPosition([1,0]) diff --git a/src/packages/markdown-preview/lib/markdown-preview-view.coffee b/src/packages/markdown-preview/lib/markdown-preview-view.coffee index 0560030a0..b78827a83 100644 --- a/src/packages/markdown-preview/lib/markdown-preview-view.coffee +++ b/src/packages/markdown-preview/lib/markdown-preview-view.coffee @@ -40,7 +40,7 @@ class MarkdownPreviewView extends ScrollView @detaching = false getActiveText: -> - rootView.getActiveEditor()?.getText() + rootView.getActiveView()?.getText() getErrorHtml: (error) -> $$$ -> @@ -74,7 +74,7 @@ class MarkdownPreviewView extends ScrollView @markdownBody.html(html) if @hasParent() isMarkdownEditor: (path) -> - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() return unless editor? return true if editor.getGrammar().scopeName is 'source.gfm' path and fs.isMarkdownExtension(fs.extension(path)) diff --git a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee index a195f907d..7704768ee 100644 --- a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee +++ b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee @@ -13,7 +13,7 @@ describe "MarkdownPreview", -> describe "markdown-preview:toggle event", -> it "toggles on/off a preview for a .md file", -> rootView.open('file.md') - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() expect(rootView.find('.markdown-preview')).not.toExist() editor.trigger('markdown-preview:toggle') @@ -25,7 +25,7 @@ describe "MarkdownPreview", -> it "displays a preview for a .markdown file", -> rootView.open('file.markdown') - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() expect(rootView.find('.markdown-preview')).not.toExist() editor.trigger('markdown-preview:toggle') expect(rootView.find('.markdown-preview')).toExist() @@ -35,7 +35,7 @@ describe "MarkdownPreview", -> it "displays a preview for a file with the source.gfm grammar scope", -> gfmGrammar = _.find syntax.grammars, (grammar) -> grammar.scopeName is 'source.gfm' rootView.open('file.js') - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() project.addGrammarOverrideForPath(editor.getPath(), gfmGrammar) editor.reloadGrammar() expect(rootView.find('.markdown-preview')).not.toExist() @@ -46,39 +46,39 @@ describe "MarkdownPreview", -> it "does not display a preview for non-markdown file", -> rootView.open('file.js') - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() expect(rootView.find('.markdown-preview')).not.toExist() editor.trigger('markdown-preview:toggle') expect(rootView.find('.markdown-preview')).not.toExist() expect(MarkdownPreview.prototype.loadHtml).not.toHaveBeenCalled() - describe "core:cancel event", -> - it "removes markdown preview", -> - rootView.open('file.md') - editor = rootView.getActiveEditor() - expect(rootView.find('.markdown-preview')).not.toExist() - editor.trigger('markdown-preview:toggle') + describe "core:cancel event", -> + it "removes markdown preview", -> + rootView.open('file.md') + editor = rootView.getActiveView() + expect(rootView.find('.markdown-preview')).not.toExist() + editor.trigger('markdown-preview:toggle') - markdownPreviewView = rootView.find('.markdown-preview')?.view() - expect(markdownPreviewView).toExist() - markdownPreviewView.trigger('core:cancel') - expect(rootView.find('.markdown-preview')).not.toExist() + markdownPreviewView = rootView.find('.markdown-preview')?.view() + expect(markdownPreviewView).toExist() + markdownPreviewView.trigger('core:cancel') + expect(rootView.find('.markdown-preview')).not.toExist() - describe "when the editor receives focus", -> - it "removes the markdown preview view", -> - rootView.attachToDom() - rootView.open('file.md') - editor = rootView.getActiveEditor() - expect(rootView.find('.markdown-preview')).not.toExist() - editor.trigger('markdown-preview:toggle') + describe "when the editor receives focus", -> + it "removes the markdown preview view", -> + rootView.attachToDom() + rootView.open('file.md') + editor = rootView.getActiveView() + expect(rootView.find('.markdown-preview')).not.toExist() + editor.trigger('markdown-preview:toggle') - markdownPreviewView = rootView.find('.markdown-preview') - editor.focus() - expect(markdownPreviewView).toExist() - expect(rootView.find('.markdown-preview')).not.toExist() + markdownPreviewView = rootView.find('.markdown-preview') + editor.focus() + expect(markdownPreviewView).toExist() + expect(rootView.find('.markdown-preview')).not.toExist() - describe "when no editor is open", -> - it "does not attach", -> - expect(rootView.getActiveEditor()).toBeFalsy() - rootView.trigger('markdown-preview:toggle') - expect(rootView.find('.markdown-preview')).not.toExist() + describe "when no editor is open", -> + it "does not attach", -> + expect(rootView.getActiveView()).toBeFalsy() + rootView.trigger('markdown-preview:toggle') + expect(rootView.find('.markdown-preview')).not.toExist() diff --git a/src/packages/package-generator/spec/package-generator-spec.coffee b/src/packages/package-generator/spec/package-generator-spec.coffee index 690443457..cae5d5d54 100644 --- a/src/packages/package-generator/spec/package-generator-spec.coffee +++ b/src/packages/package-generator/spec/package-generator-spec.coffee @@ -21,11 +21,11 @@ describe 'Package Generator', -> rootView.trigger("package-generator:generate") packageGeneratorView = rootView.find(".package-generator").view() expect(packageGeneratorView.miniEditor.isFocused).toBeTruthy() - expect(rootView.getActiveEditor().isFocused).toBeFalsy() + expect(rootView.getActiveView().isFocused).toBeFalsy() packageGeneratorView.trigger("core:cancel") expect(packageGeneratorView.hasParent()).toBeFalsy() - expect(rootView.getActiveEditor().isFocused).toBeTruthy() + expect(rootView.getActiveView().isFocused).toBeTruthy() describe "when a package is generated", -> [packageName, packagePath] = [] @@ -58,6 +58,7 @@ describe 'Package Generator', -> expect(fs.join(fs.directory(packagePath), "camel-case-is-for-the-birds")).toExistOnDisk() it "correctly lays out the package files and closes the package generator view", -> + rootView.attachToDom() rootView.trigger("package-generator:generate") packageGeneratorView = rootView.find(".package-generator").view() expect(packageGeneratorView.hasParent()).toBeTruthy() @@ -73,7 +74,7 @@ describe 'Package Generator', -> expect("#{packagePath}/stylesheets/#{packageName}.css").toExistOnDisk() expect(packageGeneratorView.hasParent()).toBeFalsy() - expect(rootView.getActiveEditor().isFocused).toBeTruthy() + expect(rootView.getActiveView().isFocused).toBeTruthy() it "replaces instances of packageName placeholders in template files", -> rootView.trigger("package-generator:generate") diff --git a/src/packages/snippets/spec/snippets-spec.coffee b/src/packages/snippets/spec/snippets-spec.coffee index c6e997d35..b77b2d80b 100644 --- a/src/packages/snippets/spec/snippets-spec.coffee +++ b/src/packages/snippets/spec/snippets-spec.coffee @@ -21,8 +21,8 @@ describe "Snippets extension", -> window.loadPackage("snippets") - editor = rootView.getActiveEditor() - editSession = rootView.getActiveEditSession() + editor = rootView.getActiveView() + editSession = rootView.getActivePaneItem() buffer = editor.getBuffer() rootView.simulateDomAttachment() rootView.enableKeymap() @@ -300,7 +300,7 @@ describe "Snippets extension", -> jasmine.unspy(LoadSnippetsTask.prototype, 'loadTextMateSnippets') snippets.loaded = false task = new LoadSnippetsTask(snippets) - task.packages = [Package.build(fixturesProject.resolve('packages/package-with-a-cson-grammar.tmbundle'))] + task.packages = [Package.build(project.resolve('packages/package-with-a-cson-grammar.tmbundle'))] task.start() waitsFor "CSON snippets to load", 5000, -> snippets.loaded diff --git a/src/packages/spell-check/spec/spell-check-spec.coffee b/src/packages/spell-check/spec/spell-check-spec.coffee index bf3fba917..f2b4f4f3a 100644 --- a/src/packages/spell-check/spec/spell-check-spec.coffee +++ b/src/packages/spell-check/spec/spell-check-spec.coffee @@ -9,7 +9,7 @@ describe "Spell check", -> config.set('spell-check.grammars', []) window.loadPackage('spell-check') rootView.attachToDom() - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() it "decorates all misspelled words", -> editor.setText("This middle of thiss sentencts has issues.") @@ -110,5 +110,5 @@ describe "Spell check", -> view = editor.find('.misspelling').view() buffer = editor.getBuffer() expect(buffer.getMarkerPosition(view.marker)).not.toBeUndefined() - editor.destroyEditSessions() + editor.remove() expect(buffer.getMarkerPosition(view.marker)).toBeUndefined() diff --git a/src/packages/status-bar/lib/status-bar-view.coffee b/src/packages/status-bar/lib/status-bar-view.coffee index c681fe4ea..f58d4dba0 100644 --- a/src/packages/status-bar/lib/status-bar-view.coffee +++ b/src/packages/status-bar/lib/status-bar-view.coffee @@ -47,7 +47,7 @@ class StatusBarView extends View subscribeToBuffer: -> @buffer?.off '.status-bar' @buffer = @editor.getBuffer() - @buffer.on 'contents-modified.status-bar', (e) => @updateBufferHasModifiedText(e.differsFromDisk) + @buffer.on 'modified-status-changed.status-bar', (isModified) => @updateBufferHasModifiedText(isModified) @buffer.on 'saved.status-bar', => @updateStatusBar() @updateStatusBar() @@ -60,8 +60,8 @@ class StatusBarView extends View updateGrammarText: -> @grammarName.text(@editor.getGrammar().name) - updateBufferHasModifiedText: (differsFromDisk)-> - if differsFromDisk + updateBufferHasModifiedText: (isModified)-> + if isModified @bufferModified.text('*') unless @isModified @isModified = true else diff --git a/src/packages/status-bar/spec/status-bar-spec.coffee b/src/packages/status-bar/spec/status-bar-spec.coffee index f2d73b07b..f7a88cf4d 100644 --- a/src/packages/status-bar/spec/status-bar-spec.coffee +++ b/src/packages/status-bar/spec/status-bar-spec.coffee @@ -12,7 +12,7 @@ describe "StatusBar", -> rootView.open('sample.js') rootView.simulateDomAttachment() StatusBar.activate() - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() statusBar = rootView.find('.status-bar').view() buffer = editor.getBuffer() @@ -63,7 +63,7 @@ describe "StatusBar", -> editor.insertText("\n") advanceClock(buffer.stoppedChangingDelay) expect(statusBar.bufferModified.text()).toBe '*' - editor.save() + editor.getBuffer().save() expect(statusBar.bufferModified.text()).toBe '' it "disables the buffer modified indicator if the content matches again", -> diff --git a/src/packages/strip-trailing-whitespace/spec/strip-trailing-whitespace-spec.coffee b/src/packages/strip-trailing-whitespace/spec/strip-trailing-whitespace-spec.coffee index f76f5b38d..17277b059 100644 --- a/src/packages/strip-trailing-whitespace/spec/strip-trailing-whitespace-spec.coffee +++ b/src/packages/strip-trailing-whitespace/spec/strip-trailing-whitespace-spec.coffee @@ -12,7 +12,7 @@ describe "StripTrailingWhitespace", -> window.loadPackage('strip-trailing-whitespace') rootView.focus() - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() afterEach -> fs.remove(path) if fs.exists(path) @@ -23,7 +23,7 @@ describe "StripTrailingWhitespace", -> # works for buffers that are already open when extension is initialized editor.insertText("foo \nbar\t \n\nbaz") - editor.save() + editor.getBuffer().save() expect(editor.getText()).toBe "foo\nbar\n\nbaz" # works for buffers that are opened after extension is initialized @@ -47,25 +47,25 @@ describe "StripTrailingWhitespace", -> it "adds a trailing newline when there is no trailing newline", -> editor.insertText "foo" - editor.save() + editor.getBuffer().save() expect(editor.getText()).toBe "foo\n" it "removes extra trailing newlines and only keeps one", -> editor.insertText "foo\n\n\n\n" - editor.save() + editor.getBuffer().save() expect(editor.getText()).toBe "foo\n" it "leaves a buffer with a single trailing newline untouched", -> editor.insertText "foo\nbar\n" - editor.save() + editor.getBuffer().save() expect(editor.getText()).toBe "foo\nbar\n" it "leaves an empty buffer untouched", -> editor.insertText "" - editor.save() + editor.getBuffer().save() expect(editor.getText()).toBe "" it "leaves a buffer that is a single newline untouched", -> editor.insertText "\n" - editor.save() + editor.getBuffer().save() expect(editor.getText()).toBe "\n" diff --git a/src/packages/symbols-view/lib/symbols-view.coffee b/src/packages/symbols-view/lib/symbols-view.coffee index fb87d78de..10f347b66 100644 --- a/src/packages/symbols-view/lib/symbols-view.coffee +++ b/src/packages/symbols-view/lib/symbols-view.coffee @@ -43,7 +43,7 @@ class SymbolsView extends SelectList populateFileSymbols: -> tags = [] callback = (tag) -> tags.push tag - path = rootView.getActiveEditor().getPath() + path = rootView.getActiveView().getPath() @list.empty() @setLoading("Generating symbols...") new TagGenerator(path, callback).generate().done => @@ -91,7 +91,7 @@ class SymbolsView extends SelectList @moveToPosition(position) if position moveToPosition: (position) -> - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() editor.scrollToBufferPosition(position, center: true) editor.setCursorBufferPosition(position) editor.moveCursorToFirstCharacterOfLine() @@ -111,7 +111,7 @@ class SymbolsView extends SelectList return new Point(index, 0) if pattern is $.trim(line) goToDeclaration: -> - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() matches = TagReader.find(editor) return unless matches.length diff --git a/src/packages/symbols-view/spec/symbols-view-spec.coffee b/src/packages/symbols-view/spec/symbols-view-spec.coffee index dcc5c5556..30d9f69be 100644 --- a/src/packages/symbols-view/spec/symbols-view-spec.coffee +++ b/src/packages/symbols-view/spec/symbols-view-spec.coffee @@ -19,7 +19,7 @@ describe "SymbolsView", -> describe "when tags can be generated for a file", -> it "initially displays all JavaScript functions with line numbers", -> rootView.open('sample.js') - rootView.getActiveEditor().trigger "symbols-view:toggle-file-symbols" + rootView.getActiveView().trigger "symbols-view:toggle-file-symbols" symbolsView = rootView.find('.symbols-view').view() expect(symbolsView.find('.loading')).toHaveText 'Generating symbols...' @@ -39,7 +39,7 @@ describe "SymbolsView", -> it "displays error when no tags match text in mini-editor", -> rootView.open('sample.js') - rootView.getActiveEditor().trigger "symbols-view:toggle-file-symbols" + rootView.getActiveView().trigger "symbols-view:toggle-file-symbols" symbolsView = rootView.find('.symbols-view').view() waitsFor -> @@ -66,7 +66,7 @@ describe "SymbolsView", -> describe "when tags can't be generated for a file", -> it "shows an error message when no matching tags are found", -> rootView.open('sample.txt') - rootView.getActiveEditor().trigger "symbols-view:toggle-file-symbols" + rootView.getActiveView().trigger "symbols-view:toggle-file-symbols" symbolsView = rootView.find('.symbols-view').view() setErrorSpy = spyOn(symbolsView, "setError").andCallThrough() @@ -93,14 +93,14 @@ describe "SymbolsView", -> runs -> rootView.open('sample.js') - expect(rootView.getActiveEditor().getCursorBufferPosition()).toEqual [0,0] + expect(rootView.getActiveView().getCursorBufferPosition()).toEqual [0,0] expect(rootView.find('.symbols-view')).not.toExist() symbolsView = SymbolsView.activate() symbolsView.setArray(tags) symbolsView.attach() expect(rootView.find('.symbols-view')).toExist() symbolsView.confirmed(tags[1]) - expect(rootView.getActiveEditor().getCursorBufferPosition()).toEqual [1,2] + expect(rootView.getActiveView().getCursorBufferPosition()).toEqual [1,2] describe "TagGenerator", -> it "generates tags for all JavaScript functions", -> @@ -136,29 +136,29 @@ describe "SymbolsView", -> describe "go to declaration", -> it "doesn't move the cursor when no declaration is found", -> rootView.open("tagged.js") - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() editor.setCursorBufferPosition([0,2]) editor.trigger 'symbols-view:go-to-declaration' expect(editor.getCursorBufferPosition()).toEqual [0,2] it "moves the cursor to the declaration", -> rootView.open("tagged.js") - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() editor.setCursorBufferPosition([6,24]) editor.trigger 'symbols-view:go-to-declaration' expect(editor.getCursorBufferPosition()).toEqual [2,0] it "displays matches when more than one exists and opens the selected match", -> rootView.open("tagged.js") - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() editor.setCursorBufferPosition([8,14]) editor.trigger 'symbols-view:go-to-declaration' symbolsView = rootView.find('.symbols-view').view() expect(symbolsView.list.children('li').length).toBe 2 expect(symbolsView).toBeVisible() symbolsView.confirmed(symbolsView.array[0]) - expect(rootView.getActiveEditor().getPath()).toBe project.resolve("tagged-duplicate.js") - expect(rootView.getActiveEditor().getCursorBufferPosition()).toEqual [0,4] + expect(rootView.getActiveView().getPath()).toBe project.resolve("tagged-duplicate.js") + expect(rootView.getActiveView().getCursorBufferPosition()).toEqual [0,4] describe "when the tag is in a file that doesn't exist", -> renamedPath = null @@ -173,7 +173,7 @@ describe "SymbolsView", -> it "doesn't display the tag", -> rootView.open("tagged.js") - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() editor.setCursorBufferPosition([8,14]) editor.trigger 'symbols-view:go-to-declaration' symbolsView = rootView.find('.symbols-view').view() diff --git a/src/packages/tabs/lib/tab-bar-view.coffee b/src/packages/tabs/lib/tab-bar-view.coffee new file mode 100644 index 000000000..fbbab1330 --- /dev/null +++ b/src/packages/tabs/lib/tab-bar-view.coffee @@ -0,0 +1,98 @@ +$ = require 'jquery' +_ = require 'underscore' +SortableList = require 'sortable-list' +TabView = require './tab-view' + +module.exports = +class TabBarView extends SortableList + @content: -> + @ul class: "tabs #{@viewClass()}" + + initialize: (@pane) -> + super + + @paneContainer = @pane.getContainer() + @addTabForItem(item) for item in @pane.getItems() + + @pane.on 'pane:item-added', (e, item, index) => @addTabForItem(item, index) + @pane.on 'pane:item-moved', (e, item, index) => @moveItemTabToIndex(item, index) + @pane.on 'pane:item-removed', (e, item) => @removeTabForItem(item) + @pane.on 'pane:active-item-changed', => @updateActiveTab() + + @updateActiveTab() + + @on 'click', '.tab', (e) => + tab = $(e.target).closest('.tab').view() + @pane.showItem(tab.item) + @pane.focus() + + @on 'click', '.tab .close-icon', (e) => + tab = $(e.target).closest('.tab').view() + @pane.destroyItem(tab.item) + false + + @pane.prepend(this) + + addTabForItem: (item, index) -> + @insertTabAtIndex(new TabView(item, @pane), index) + + moveItemTabToIndex: (item, index) -> + tab = @tabForItem(item) + tab.detach() + @insertTabAtIndex(tab, index) + + insertTabAtIndex: (tab, index) -> + followingTab = @tabAtIndex(index) if index? + if followingTab + tab.insertBefore(followingTab) + else + @append(tab) + + removeTabForItem: (item) -> + @tabForItem(item).remove() + + getTabs: -> + @children('.tab').toArray().map (elt) -> $(elt).view() + + tabAtIndex: (index) -> + @children(".tab:eq(#{index})").view() + + tabForItem: (item) -> + _.detect @getTabs(), (tab) -> tab.item is item + + setActiveTab: (tabView) -> + unless tabView.hasClass('active') + @find(".tab.active").removeClass('active') + tabView.addClass('active') + + updateActiveTab: -> + @setActiveTab(@tabForItem(@pane.activeItem)) + + shouldAllowDrag: -> + (@paneContainer.getPanes().length > 1) or (@pane.getItems().length > 1) + + onDragStart: (event) => + super + pane = $(event.target).closest('.pane') + paneIndex = @paneContainer.indexOfPane(pane) + event.originalEvent.dataTransfer.setData 'from-pane-index', paneIndex + + onDrop: (event) => + super + + dataTransfer = event.originalEvent.dataTransfer + fromIndex = parseInt(dataTransfer.getData('sortable-index')) + fromPaneIndex = parseInt(dataTransfer.getData('from-pane-index')) + fromPane = @paneContainer.paneAtIndex(fromPaneIndex) + toIndex = @getSortableElement(event).index() + toPane = $(event.target).closest('.pane').view() + draggedTab = fromPane.find(".tabs .sortable:eq(#{fromIndex})").view() + item = draggedTab.item + + if toPane is fromPane + toIndex++ if fromIndex > toIndex + toPane.moveItem(item, toIndex) + else + fromPane.moveItemToPane(item, toPane, toIndex) + toPane.showItem(item) + toPane.focus() diff --git a/src/packages/tabs/lib/tab-view.coffee b/src/packages/tabs/lib/tab-view.coffee index 166233094..9bfee397e 100644 --- a/src/packages/tabs/lib/tab-view.coffee +++ b/src/packages/tabs/lib/tab-view.coffee @@ -1,100 +1,55 @@ $ = require 'jquery' -SortableList = require 'sortable-list' -Tab = require './tab' +{View} = require 'space-pen' +fs = require 'fs' module.exports = -class TabView extends SortableList - @activate: -> - rootView.eachEditor (editor) => - @prependToEditorPane(editor) if editor.attached - - @prependToEditorPane: (editor) -> - if pane = editor.pane() - pane.prepend(new TabView(editor)) - +class TabView extends View @content: -> - @ul class: "tabs #{@viewClass()}" + @li class: 'tab sortable', => + @span class: 'title', outlet: 'title' + @span class: 'close-icon' - initialize: (@editor) -> - super + initialize: (@item, @pane) -> + @item.on? 'title-changed', => @updateTitle() + @item.on? 'modified-status-changed', => @updateModifiedStatus() + @updateTitle() + @updateModifiedStatus() - @addTabForEditSession(editSession) for editSession in @editor.editSessions + updateTitle: -> + return if @updatingTitle + @updatingTitle = true - @setActiveTab(@editor.getActiveEditSessionIndex()) - @editor.on 'editor:active-edit-session-changed', (e, editSession, index) => @setActiveTab(index) - @editor.on 'editor:edit-session-added', (e, editSession) => @addTabForEditSession(editSession) - @editor.on 'editor:edit-session-removed', (e, editSession, index) => @removeTabAtIndex(index) - @editor.on 'editor:edit-session-order-changed', (e, editSession, fromIndex, toIndex) => - fromTab = @find(".tab:eq(#{fromIndex})") - toTab = @find(".tab:eq(#{toIndex})") - fromTab.detach() - if fromIndex < toIndex - fromTab.insertAfter(toTab) - else - fromTab.insertBefore(toTab) + title = @item.getTitle() + useLongTitle = false + for tab in @getSiblingTabs() + if tab.item.getTitle() is title + tab.updateTitle() + useLongTitle = true + title = @item.getLongTitle?() ? title if useLongTitle - @on 'click', '.tab', (e) => - @editor.setActiveEditSessionIndex($(e.target).closest('.tab').index()) - @editor.focus() + @title.text(title) + @updatingTitle = false - @on 'click', '.tab .close-icon', (e) => - index = $(e.target).closest('.tab').index() - @editor.destroyEditSessionIndex(index) - false + getSiblingTabs: -> + @siblings('.tab').views() - addTabForEditSession: (editSession) -> - @append(new Tab(editSession, @editor)) - - setActiveTab: (index) -> - @find(".tab.active").removeClass('active') - @find(".tab:eq(#{index})").addClass('active') - - removeTabAtIndex: (index) -> - @find(".tab:eq(#{index})").remove() - - containsEditSession: (editor, editSession) -> - for session in editor.editSessions - return true if editSession.getPath() is session.getPath() - - shouldAllowDrag: (event) -> - panes = rootView.find('.pane') - !(panes.length == 1 && panes.find('.sortable').length == 1) - - onDragStart: (event) => - super - - pane = $(event.target).closest('.pane') - paneIndex = rootView.indexOfPane(pane) - event.originalEvent.dataTransfer.setData 'from-pane-index', paneIndex - - onDrop: (event) => - super - - droppedNearTab = @getSortableElement(event) - transfer = event.originalEvent.dataTransfer - previousDraggedTabIndex = transfer.getData 'sortable-index' - - fromPaneIndex = ~~transfer.getData 'from-pane-index' - toPaneIndex = rootView.indexOfPane($(event.target).closest('.pane')) - fromPane = $(rootView.find('.pane')[fromPaneIndex]) - fromEditor = fromPane.find('.editor').view() - draggedTab = fromPane.find(".#{TabView.viewClass()} .sortable:eq(#{previousDraggedTabIndex})") - - if draggedTab.is(droppedNearTab) - fromEditor.focus() - return - - if fromPaneIndex == toPaneIndex - droppedNearTab = @getSortableElement(event) - fromIndex = draggedTab.index() - toIndex = droppedNearTab.index() - toIndex++ if fromIndex > toIndex - fromEditor.moveEditSessionToIndex(fromIndex, toIndex) - fromEditor.focus() + updateModifiedStatus: -> + if @item.isModified?() + @addClass('modified') unless @isModified + @isModified = true else - toEditor = rootView.find(".pane:eq(#{toPaneIndex}) > .editor").view() - if @containsEditSession(toEditor, fromEditor.editSessions[draggedTab.index()]) - fromEditor.focus() - else - fromEditor.moveEditSessionToEditor(draggedTab.index(), toEditor, droppedNearTab.index() + 1) - toEditor.focus() + @removeClass('modified') if @isModified + @isModified = false + + updateFileName: -> + fileNameText = @editSession.buffer.getBaseName() + if fileNameText? + duplicates = @editor.getEditSessions().filter (session) -> fileNameText is session.buffer.getBaseName() + if duplicates.length > 1 + directory = fs.base(fs.directory(@editSession.getPath())) + fileNameText = "#{fileNameText} - #{directory}" if directory + else + fileNameText = 'untitled' + + @fileName.text(fileNameText) + @fileName.attr('title', @editSession.getPath()) diff --git a/src/packages/tabs/lib/tab.coffee b/src/packages/tabs/lib/tab.coffee deleted file mode 100644 index 9a7e8e3ab..000000000 --- a/src/packages/tabs/lib/tab.coffee +++ /dev/null @@ -1,40 +0,0 @@ -{View} = require 'space-pen' -fs = require 'fs' - -module.exports = -class Tab extends View - @content: (editSession) -> - @li class: 'tab sortable', => - @span class: 'file-name', outlet: 'fileName' - @span class: 'close-icon' - - initialize: (@editSession, @editor) -> - @buffer = @editSession.buffer - @subscribe @buffer, 'path-changed', => @updateFileName() - @subscribe @buffer, 'contents-modified', => @updateModifiedStatus() - @subscribe @buffer, 'saved', => @updateModifiedStatus() - @subscribe @editor, 'editor:edit-session-added', => @updateFileName() - @subscribe @editor, 'editor:edit-session-removed', => @updateFileName() - @updateFileName() - @updateModifiedStatus() - - updateModifiedStatus: -> - if @buffer.isModified() - @toggleClass('file-modified') unless @isModified - @isModified = true - else - @removeClass('file-modified') if @isModified - @isModified = false - - updateFileName: -> - fileNameText = @editSession.buffer.getBaseName() - if fileNameText? - duplicates = @editor.getEditSessions().filter (session) -> fileNameText is session.buffer.getBaseName() - if duplicates.length > 1 - directory = fs.base(fs.directory(@editSession.getPath())) - fileNameText = "#{fileNameText} - #{directory}" if directory - else - fileNameText = 'untitled' - - @fileName.text(fileNameText) - @fileName.attr('title', @editSession.getPath()) diff --git a/src/packages/tabs/lib/tabs.coffee b/src/packages/tabs/lib/tabs.coffee new file mode 100644 index 000000000..ba2da6b3a --- /dev/null +++ b/src/packages/tabs/lib/tabs.coffee @@ -0,0 +1,5 @@ +TabBarView = require './tab-bar-view' + +module.exports = + activate: -> + rootView.eachPane (pane) => new TabBarView(pane) diff --git a/src/packages/tabs/package.cson b/src/packages/tabs/package.cson index 0e40dfd74..1c24d65ba 100644 --- a/src/packages/tabs/package.cson +++ b/src/packages/tabs/package.cson @@ -1 +1 @@ -'main': 'lib/tab-view' +'main': 'lib/tabs' diff --git a/src/packages/tabs/spec/tabs-spec.coffee b/src/packages/tabs/spec/tabs-spec.coffee index 00efe0324..d8d32e390 100644 --- a/src/packages/tabs/spec/tabs-spec.coffee +++ b/src/packages/tabs/spec/tabs-spec.coffee @@ -1,231 +1,256 @@ $ = require 'jquery' _ = require 'underscore' RootView = require 'root-view' +Pane = require 'pane' +PaneContainer = require 'pane-container' +TabBarView = require 'tabs/lib/tab-bar-view' fs = require 'fs' +{View} = require 'space-pen' -describe "TabView", -> - [editor, buffer, tabs] = [] - +describe "Tabs package main", -> beforeEach -> window.rootView = new RootView rootView.open('sample.js') - rootView.open('sample.txt') - rootView.simulateDomAttachment() window.loadPackage("tabs") - editor = rootView.getActiveEditor() - tabs = rootView.find('.tabs').view() - describe "@activate", -> - it "appends a status bear to all existing and new editors", -> + describe ".activate()", -> + it "appends a tab bar all existing and new panes", -> expect(rootView.panes.find('.pane').length).toBe 1 expect(rootView.panes.find('.pane > .tabs').length).toBe 1 - editor.splitRight() + rootView.getActivePane().splitRight() expect(rootView.find('.pane').length).toBe 2 expect(rootView.panes.find('.pane > .tabs').length).toBe 2 - describe ".initialize()", -> - it "creates a tab for each edit session on the editor to which the tab-strip belongs", -> - expect(editor.editSessions.length).toBe 2 - expect(tabs.find('.tab').length).toBe 2 +describe "TabBarView", -> + [item1, item2, editSession1, pane, tabBar] = [] - expect(tabs.find('.tab:eq(0) .file-name').text()).toBe editor.editSessions[0].buffer.getBaseName() - expect(tabs.find('.tab:eq(1) .file-name').text()).toBe editor.editSessions[1].buffer.getBaseName() + class TestView extends View + @deserialize: ({title, longTitle}) -> new TestView(title, longTitle) + @content: (title) -> @div title + initialize: (@title, @longTitle) -> + getTitle: -> @title + getLongTitle: -> @longTitle + serialize: -> { deserializer: 'TestView', @title, @longTitle } - it "highlights the tab for the current active edit session", -> - expect(editor.getActiveEditSessionIndex()).toBe 1 - expect(tabs.find('.tab:eq(1)')).toHaveClass 'active' + beforeEach -> + registerDeserializer(TestView) + item1 = new TestView('Item 1') + item2 = new TestView('Item 2') + editSession1 = project.buildEditSession('sample.js') + paneContainer = new PaneContainer + pane = new Pane(item1, editSession1, item2) + pane.showItem(item2) + paneContainer.append(pane) + tabBar = new TabBarView(pane) - it "sets the title on each tab to be the full path of the edit session", -> - expect(tabs.find('.tab:eq(0) .file-name').attr('title')).toBe editor.editSessions[0].getPath() - expect(tabs.find('.tab:eq(1) .file-name').attr('title')).toBe editor.editSessions[1].getPath() + afterEach -> + unregisterDeserializer(TestView) - describe "when the active edit session changes", -> - it "highlights the tab for the newly-active edit session", -> - editor.setActiveEditSessionIndex(0) - expect(tabs.find('.active').length).toBe 1 - expect(tabs.find('.tab:eq(0)')).toHaveClass 'active' + describe ".initialize(pane)", -> + it "creates a tab for each item on the tab bar's parent pane", -> + expect(pane.getItems().length).toBe 3 + expect(tabBar.find('.tab').length).toBe 3 - editor.setActiveEditSessionIndex(1) - expect(tabs.find('.active').length).toBe 1 - expect(tabs.find('.tab:eq(1)')).toHaveClass 'active' + expect(tabBar.find('.tab:eq(0) .title').text()).toBe item1.getTitle() + expect(tabBar.find('.tab:eq(1) .title').text()).toBe editSession1.getTitle() + expect(tabBar.find('.tab:eq(2) .title').text()).toBe item2.getTitle() - describe "when a new edit session is created", -> - it "adds a tab for the new edit session", -> - rootView.open('two-hundred.txt') - expect(tabs.find('.tab').length).toBe 3 - expect(tabs.find('.tab:eq(2) .file-name').text()).toBe 'two-hundred.txt' + it "highlights the tab for the active pane item", -> + expect(tabBar.find('.tab:eq(2)')).toHaveClass 'active' - describe "when the edit session's buffer has an undefined path", -> - it "makes the tab text 'untitled'", -> - rootView.open() - expect(tabs.find('.tab').length).toBe 3 - expect(tabs.find('.tab:eq(2) .file-name').text()).toBe 'untitled' + describe "when the active pane item changes", -> + it "highlights the tab for the new active pane item", -> + pane.showItem(item1) + expect(tabBar.find('.active').length).toBe 1 + expect(tabBar.find('.tab:eq(0)')).toHaveClass 'active' - it "removes the tab's title", -> - rootView.open() - expect(tabs.find('.tab').length).toBe 3 - expect(tabs.find('.tab:eq(2) .file-name').attr('title')).toBeUndefined() + pane.showItem(item2) + expect(tabBar.find('.active').length).toBe 1 + expect(tabBar.find('.tab:eq(2)')).toHaveClass 'active' - describe "when an edit session is removed", -> - it "removes the tab for the removed edit session", -> - editor.setActiveEditSessionIndex(0) - editor.destroyActiveEditSession() - expect(tabs.find('.tab').length).toBe 1 - expect(tabs.find('.tab:eq(0) .file-name').text()).toBe 'sample.txt' + describe "when a new item is added to the pane", -> + it "adds a tab for the new item at the same index as the item in the pane", -> + pane.showItem(item1) + item3 = new TestView('Item 3') + pane.showItem(item3) + expect(tabBar.find('.tab').length).toBe 4 + expect(tabBar.tabAtIndex(1).find('.title')).toHaveText 'Item 3' + + it "adds the 'modified' class to the new tab if the item is initially modified", -> + editSession2 = project.buildEditSession('sample.txt') + editSession2.insertText('x') + pane.showItem(editSession2) + expect(tabBar.tabForItem(editSession2)).toHaveClass 'modified' + + describe "when an item is removed from the pane", -> + it "removes the item's tab from the tab bar", -> + pane.removeItem(item2) + expect(tabBar.getTabs().length).toBe 2 + expect(tabBar.find('.tab:contains(Item 2)')).not.toExist() describe "when a tab is clicked", -> - it "activates the associated edit session", -> - expect(editor.getActiveEditSessionIndex()).toBe 1 - tabs.find('.tab:eq(0)').click() - expect(editor.getActiveEditSessionIndex()).toBe 0 - tabs.find('.tab:eq(1)').click() - expect(editor.getActiveEditSessionIndex()).toBe 1 + it "shows the associated item on the pane and focuses the pane", -> + spyOn(pane, 'focus') - it "focuses the associated editor", -> - rootView.attachToDom() - expect(editor).toMatchSelector ":has(:focus)" - editor.splitRight() - expect(editor).not.toMatchSelector ":has(:focus)" - tabs.find('.tab:eq(0)').click() - expect(editor).toMatchSelector ":has(:focus)" + tabBar.tabAtIndex(0).click() + expect(pane.activeItem).toBe pane.getItems()[0] - describe "when a file name associated with a tab changes", -> - [buffer, oldPath, newPath] = [] + tabBar.tabAtIndex(2).click() + expect(pane.activeItem).toBe pane.getItems()[2] - beforeEach -> - buffer = editor.editSessions[0].buffer - oldPath = "/tmp/file-to-rename.txt" - newPath = "/tmp/renamed-file.txt" - fs.write(oldPath, "this old path") - rootView.open(oldPath) + expect(pane.focus.callCount).toBe 2 - afterEach -> - fs.remove(newPath) if fs.exists(newPath) + describe "when a tab's close icon is clicked", -> + it "destroys the tab's item on the pane", -> + tabBar.tabForItem(editSession1).find('.close-icon').click() + expect(pane.getItems().length).toBe 2 + expect(pane.getItems().indexOf(editSession1)).toBe -1 + expect(editSession1.destroyed).toBeTruthy() + expect(tabBar.getTabs().length).toBe 2 + expect(tabBar.find('.tab:contains(sample.js)')).not.toExist() - it "updates the file name in the tab", -> - tabFileName = tabs.find('.tab:eq(2) .file-name') - expect(tabFileName).toExist() - editor.setActiveEditSessionIndex(0) - fs.move(oldPath, newPath) - waitsFor "file to be renamed", -> - tabFileName.text() == "renamed-file.txt" + describe "when a tab item's title changes", -> + it "updates the title of the item's tab", -> + editSession1.buffer.setPath('/this/is-a/test.txt') + expect(tabBar.tabForItem(editSession1)).toHaveText 'test.txt' - describe "when the close icon is clicked", -> - it "closes the selected non-active edit session", -> - activeSession = editor.activeEditSession - expect(editor.getActiveEditSessionIndex()).toBe 1 - tabs.find('.tab .close-icon:eq(0)').click() - expect(editor.getActiveEditSessionIndex()).toBe 0 - expect(editor.activeEditSession).toBe activeSession + describe "when two tabs have the same title", -> + it "displays the long title on the tab if it's available from the item", -> + item1.title = "Old Man" + item1.longTitle = "Grumpy Old Man" + item1.trigger 'title-changed' + item2.title = "Old Man" + item2.longTitle = "Jolly Old Man" + item2.trigger 'title-changed' - it "closes the selected active edit session", -> - firstSession = editor.getEditSessions()[0] - expect(editor.getActiveEditSessionIndex()).toBe 1 - tabs.find('.tab .close-icon:eq(1)').click() - expect(editor.getActiveEditSessionIndex()).toBe 0 - expect(editor.activeEditSession).toBe firstSession + expect(tabBar.tabForItem(item1)).toHaveText "Grumpy Old Man" + expect(tabBar.tabForItem(item2)).toHaveText "Jolly Old Man" - describe "when two tabs have the same file name", -> - [tempPath] = [] + item2.longTitle = undefined + item2.trigger 'title-changed' - beforeEach -> - tempPath = '/tmp/sample.js' - fs.write(tempPath, 'sample') + expect(tabBar.tabForItem(item1)).toHaveText "Grumpy Old Man" + expect(tabBar.tabForItem(item2)).toHaveText "Old Man" - afterEach -> - fs.remove(tempPath) if fs.exists(tempPath) + describe "when a tab item's modified status changes", -> + it "adds or removes the 'modified' class to the tab based on the status", -> + tab = tabBar.tabForItem(editSession1) + expect(editSession1.isModified()).toBeFalsy() + expect(tab).not.toHaveClass 'modified' - it "displays the parent folder name after the file name", -> - expect(tabs.find('.tab:eq(0) .file-name').text()).toBe 'sample.js' - rootView.open(tempPath) - expect(tabs.find('.tab:eq(0) .file-name').text()).toBe 'sample.js - fixtures' - expect(tabs.find('.tab:last .file-name').text()).toBe 'sample.js - tmp' - editor.destroyActiveEditSession() - expect(tabs.find('.tab:eq(0) .file-name').text()).toBe 'sample.js' + editSession1.insertText('x') + advanceClock(editSession1.buffer.stoppedChangingDelay) + expect(editSession1.isModified()).toBeTruthy() + expect(tab).toHaveClass 'modified' - describe "when an editor:edit-session-order-changed event is triggered", -> - it "updates the order of the tabs to match the new edit session order", -> - expect(tabs.find('.tab:eq(0) .file-name').text()).toBe "sample.js" - expect(tabs.find('.tab:eq(1) .file-name').text()).toBe "sample.txt" + editSession1.undo() + advanceClock(editSession1.buffer.stoppedChangingDelay) + expect(editSession1.isModified()).toBeFalsy() + expect(tab).not.toHaveClass 'modified' - editor.moveEditSessionToIndex(0, 1) - expect(tabs.find('.tab:eq(0) .file-name').text()).toBe "sample.txt" - expect(tabs.find('.tab:eq(1) .file-name').text()).toBe "sample.js" - - editor.moveEditSessionToIndex(1, 0) - expect(tabs.find('.tab:eq(0) .file-name').text()).toBe "sample.js" - expect(tabs.find('.tab:eq(1) .file-name').text()).toBe "sample.txt" + describe "when a pane item moves to a new index", -> + it "updates the order of the tabs to match the new item order", -> + expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "sample.js", "Item 2"] + pane.moveItem(item2, 1) + expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "Item 2", "sample.js"] + pane.moveItem(editSession1, 0) + expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["sample.js", "Item 1", "Item 2"] + pane.moveItem(item1, 2) + expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["sample.js", "Item 2", "Item 1"] describe "dragging and dropping tabs", -> - describe "when the tab is dropped onto itself", -> - it "doesn't move the edit session and focuses the editor", -> - expect(tabs.find('.tab:eq(1) .file-name').text()).toBe "sample.txt" + buildDragEvents = (dragged, dropTarget) -> + dataTransfer = + data: {} + setData: (key, value) -> @data[key] = value + getData: (key) -> @data[key] - sortableElement = [tabs.find('.tab:eq(0)')] - spyOn(tabs, 'getSortableElement').andCallFake -> sortableElement[0] - event = $.Event() - event.target = tabs[0] - event.originalEvent = - dataTransfer: - data: {} - setData: (key, value) -> @data[key] = value - getData: (key) -> @data[key] + dragStartEvent = $.Event() + dragStartEvent.target = dragged[0] + dragStartEvent.originalEvent = { dataTransfer } - editor.hiddenInput.focusout() - tabs.onDragStart(event) - tabs.onDrop(event) + dropEvent = $.Event() + dropEvent.target = dropTarget[0] + dropEvent.originalEvent = { dataTransfer } - expect(tabs.find('.tab:eq(0) .file-name').text()).toBe "sample.js" - expect(tabs.find('.tab:eq(1) .file-name').text()).toBe "sample.txt" - expect(editor.isFocused).toBeTruthy() + [dragStartEvent, dropEvent] - describe "when a tab is dragged from and dropped onto the same editor", -> - it "moves the edit session, updates the order of the tabs, and focuses the editor", -> - expect(tabs.find('.tab:eq(0) .file-name').text()).toBe "sample.js" - expect(tabs.find('.tab:eq(1) .file-name').text()).toBe "sample.txt" + describe "when a tab is dragged within the same pane", -> + describe "when it is dropped on tab that's later in the list", -> + it "moves the tab and its item, shows the tab's item, and focuses the pane", -> + expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "sample.js", "Item 2"] + expect(pane.getItems()).toEqual [item1, editSession1, item2] + expect(pane.activeItem).toBe item2 + spyOn(pane, 'focus') - sortableElement = [tabs.find('.tab:eq(0)')] - spyOn(tabs, 'getSortableElement').andCallFake -> sortableElement[0] - event = $.Event() - event.target = tabs[0] - event.originalEvent = - dataTransfer: - data: {} - setData: (key, value) -> @data[key] = value - getData: (key) -> @data[key] + [dragStartEvent, dropEvent] = buildDragEvents(tabBar.tabAtIndex(0), tabBar.tabAtIndex(1)) + tabBar.onDragStart(dragStartEvent) + tabBar.onDrop(dropEvent) - editor.hiddenInput.focusout() - tabs.onDragStart(event) - sortableElement = [tabs.find('.tab:eq(1)')] - tabs.onDrop(event) + expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["sample.js", "Item 1", "Item 2"] + expect(pane.getItems()).toEqual [editSession1, item1, item2] + expect(pane.activeItem).toBe item1 + expect(pane.focus).toHaveBeenCalled() - expect(tabs.find('.tab:eq(0) .file-name').text()).toBe "sample.txt" - expect(tabs.find('.tab:eq(1) .file-name').text()).toBe "sample.js" - expect(editor.isFocused).toBeTruthy() + describe "when it is dropped on a tab that's earlier in the list", -> + it "moves the tab and its item, shows the tab's item, and focuses the pane", -> + expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "sample.js", "Item 2"] + expect(pane.getItems()).toEqual [item1, editSession1, item2] + expect(pane.activeItem).toBe item2 + spyOn(pane, 'focus') - describe "when a tab is dragged from one editor and dropped onto another editor", -> - it "moves the edit session, updates the order of the tabs, and focuses the destination editor", -> - leftTabs = tabs - rightEditor = editor.splitRight() - rightTabs = rootView.find('.tabs:last').view() + [dragStartEvent, dropEvent] = buildDragEvents(tabBar.tabAtIndex(2), tabBar.tabAtIndex(0)) + tabBar.onDragStart(dragStartEvent) + tabBar.onDrop(dropEvent) - sortableElement = [leftTabs.find('.tab:eq(0)')] - spyOn(tabs, 'getSortableElement').andCallFake -> sortableElement[0] - event = $.Event() - event.target = leftTabs - event.originalEvent = - dataTransfer: - data: {} - setData: (key, value) -> @data[key] = value - getData: (key) -> @data[key] + expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "Item 2", "sample.js"] + expect(pane.getItems()).toEqual [item1, item2, editSession1] + expect(pane.activeItem).toBe item2 + expect(pane.focus).toHaveBeenCalled() - rightEditor.hiddenInput.focusout() - tabs.onDragStart(event) + describe "when it is dropped on itself", -> + it "doesn't move the tab or item, but does make it the active item and focuses the pane", -> + expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "sample.js", "Item 2"] + expect(pane.getItems()).toEqual [item1, editSession1, item2] + expect(pane.activeItem).toBe item2 + spyOn(pane, 'focus') - event.target = rightTabs - sortableElement = [rightTabs.find('.tab:eq(0)')] - tabs.onDrop(event) + [dragStartEvent, dropEvent] = buildDragEvents(tabBar.tabAtIndex(0), tabBar.tabAtIndex(0)) + tabBar.onDragStart(dragStartEvent) + tabBar.onDrop(dropEvent) - expect(rightTabs.find('.tab:eq(0) .file-name').text()).toBe "sample.txt" - expect(rightTabs.find('.tab:eq(1) .file-name').text()).toBe "sample.js" - expect(rightEditor.isFocused).toBeTruthy() + expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "sample.js", "Item 2"] + expect(pane.getItems()).toEqual [item1, editSession1, item2] + expect(pane.activeItem).toBe item1 + expect(pane.focus).toHaveBeenCalled() + + describe "when a tab is dragged to a different pane", -> + [pane2, tabBar2, item2b] = [] + + beforeEach -> + pane2 = pane.splitRight() + [item2b] = pane2.getItems() + tabBar2 = new TabBarView(pane2) + + it "removes the tab and item from their original pane and moves them to the target pane", -> + expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "sample.js", "Item 2"] + expect(pane.getItems()).toEqual [item1, editSession1, item2] + expect(pane.activeItem).toBe item2 + + expect(tabBar2.getTabs().map (tab) -> tab.text()).toEqual ["Item 2"] + expect(pane2.getItems()).toEqual [item2b] + expect(pane2.activeItem).toBe item2b + spyOn(pane2, 'focus') + + [dragStartEvent, dropEvent] = buildDragEvents(tabBar.tabAtIndex(0), tabBar2.tabAtIndex(0)) + tabBar.onDragStart(dragStartEvent) + tabBar.onDrop(dropEvent) + + expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["sample.js", "Item 2"] + expect(pane.getItems()).toEqual [editSession1, item2] + expect(pane.activeItem).toBe item2 + + expect(tabBar2.getTabs().map (tab) -> tab.text()).toEqual ["Item 2", "Item 1"] + expect(pane2.getItems()).toEqual [item2b, item1] + expect(pane2.activeItem).toBe item1 + expect(pane2.focus).toHaveBeenCalled() diff --git a/src/packages/tree-view/lib/tree-view.coffee b/src/packages/tree-view/lib/tree-view.coffee index ee4ebf0c3..bf1df2b2e 100644 --- a/src/packages/tree-view/lib/tree-view.coffee +++ b/src/packages/tree-view/lib/tree-view.coffee @@ -40,7 +40,7 @@ class TreeView extends ScrollView else @selectActiveFile() - rootView.on 'root-view:active-path-changed', => @selectActiveFile() + rootView.on 'pane:active-item-changed pane:became-active', => @selectActiveFile() project.on 'path-changed', => @updateRoot() @observeConfig 'core.hideGitIgnoredFiles', => @updateRoot() @@ -98,7 +98,7 @@ class TreeView extends ScrollView @openSelectedEntry(false) if entry instanceof FileView when 2 if entry.is('.selected.file') - rootView.getActiveEditor().focus() + rootView.getActiveView().focus() else if entry.is('.selected.directory') entry.toggleExpansion() @@ -119,6 +119,7 @@ class TreeView extends ScrollView updateRoot: -> @root?.remove() + if rootDirectory = project.getRootDirectory() @root = new DirectoryView(directory: rootDirectory, isExpanded: true, project: project) @treeViewList.append(@root) @@ -126,14 +127,14 @@ class TreeView extends ScrollView @root = null selectActiveFile: -> - activeFilePath = rootView.getActiveEditor()?.getPath() + activeFilePath = rootView.getActiveView()?.getPath() @selectEntryForPath(activeFilePath) if activeFilePath revealActiveFile: -> @attach() @focus() - return unless activeFilePath = rootView.getActiveEditor()?.getPath() + return unless activeFilePath = rootView.getActiveView()?.getPath() activePathComponents = project.relativize(activeFilePath).split('/') currentPath = project.getPath().replace(/\/$/, '') diff --git a/src/packages/tree-view/lib/tree.coffee b/src/packages/tree-view/lib/tree.coffee index cd43e4467..df3cb45c3 100644 --- a/src/packages/tree-view/lib/tree.coffee +++ b/src/packages/tree-view/lib/tree.coffee @@ -2,7 +2,7 @@ module.exports = treeView: null activate: (@state) -> - @state.attached ?= true unless rootView.getActiveEditSession() + @state.attached ?= true unless rootView.getActivePaneItem() @createView() if @state.attached rootView.command 'tree-view:toggle', => @createView().toggle() diff --git a/src/packages/tree-view/spec/tree-view-spec.coffee b/src/packages/tree-view/spec/tree-view-spec.coffee index c364bf527..fc2f9fe81 100644 --- a/src/packages/tree-view/spec/tree-view-spec.coffee +++ b/src/packages/tree-view/spec/tree-view-spec.coffee @@ -65,7 +65,7 @@ describe "TreeView", -> describe "when the project is assigned a path because a new buffer is saved", -> it "creates a root directory view but does not attach to the root view", -> - rootView.getActiveEditSession().saveAs("/tmp/test.txt") + rootView.getActivePaneItem().saveAs("/tmp/test.txt") expect(treeView.hasParent()).toBeFalsy() expect(treeView.root.getPath()).toBe require.resolve('/tmp') expect(treeView.root.parent()).toMatchSelector(".tree-view") @@ -174,14 +174,14 @@ describe "TreeView", -> describe "if the current file has no path", -> it "shows and focuses the tree view, but does not attempt to select a specific file", -> rootView.open() - expect(rootView.getActiveEditSession().getPath()).toBeUndefined() + expect(rootView.getActivePaneItem().getPath()).toBeUndefined() rootView.trigger 'tree-view:reveal-active-file' expect(treeView.hasParent()).toBeTruthy() expect(treeView.focus).toHaveBeenCalled() describe "if there is no editor open", -> it "shows and focuses the tree view, but does not attempt to select a specific file", -> - expect(rootView.getActiveEditSession()).toBeUndefined() + expect(rootView.getActivePaneItem()).toBeUndefined() rootView.trigger 'tree-view:reveal-active-file' expect(treeView.hasParent()).toBeTruthy() expect(treeView.focus).toHaveBeenCalled() @@ -195,7 +195,7 @@ describe "TreeView", -> treeView.trigger 'tool-panel:unfocus' expect(treeView).toBeVisible() expect(treeView.find(".tree-view")).not.toMatchSelector(':focus') - expect(rootView.getActiveEditor().isFocused).toBeTruthy() + expect(rootView.getActiveView().isFocused).toBeTruthy() describe "when core:close is triggered on the tree view", -> it "detaches the TreeView, focuses the RootView and does not bubble the core:close event", -> @@ -262,28 +262,28 @@ describe "TreeView", -> describe "when a file is single-clicked", -> it "selects the files and opens it in the active editor, without changing focus", -> - expect(rootView.getActiveEditor()).toBeUndefined() + expect(rootView.getActiveView()).toBeUndefined() sampleJs.trigger clickEvent(originalEvent: { detail: 1 }) expect(sampleJs).toHaveClass 'selected' - expect(rootView.getActiveEditor().getPath()).toBe require.resolve('fixtures/tree-view/tree-view.js') - expect(rootView.getActiveEditor().isFocused).toBeFalsy() + expect(rootView.getActiveView().getPath()).toBe require.resolve('fixtures/tree-view/tree-view.js') + expect(rootView.getActiveView().isFocused).toBeFalsy() sampleTxt.trigger clickEvent(originalEvent: { detail: 1 }) expect(sampleTxt).toHaveClass 'selected' expect(treeView.find('.selected').length).toBe 1 - expect(rootView.getActiveEditor().getPath()).toBe require.resolve('fixtures/tree-view/tree-view.txt') - expect(rootView.getActiveEditor().isFocused).toBeFalsy() + expect(rootView.getActiveView().getPath()).toBe require.resolve('fixtures/tree-view/tree-view.txt') + expect(rootView.getActiveView().isFocused).toBeFalsy() describe "when a file is double-clicked", -> it "selects the file and opens it in the active editor on the first click, then changes focus to the active editor on the second", -> sampleJs.trigger clickEvent(originalEvent: { detail: 1 }) expect(sampleJs).toHaveClass 'selected' - expect(rootView.getActiveEditor().getPath()).toBe require.resolve('fixtures/tree-view/tree-view.js') - expect(rootView.getActiveEditor().isFocused).toBeFalsy() + expect(rootView.getActiveView().getPath()).toBe require.resolve('fixtures/tree-view/tree-view.js') + expect(rootView.getActiveView().isFocused).toBeFalsy() sampleJs.trigger clickEvent(originalEvent: { detail: 2 }) - expect(rootView.getActiveEditor().isFocused).toBeTruthy() + expect(rootView.getActiveView().isFocused).toBeTruthy() describe "when a directory is single-clicked", -> it "is selected", -> @@ -299,26 +299,25 @@ describe "TreeView", -> expect(subdir).toHaveClass 'selected' subdir.trigger clickEvent(originalEvent: { detail: 2 }) expect(subdir).toHaveClass 'expanded' - expect(rootView.getActiveEditor().isFocused).toBeFalsy() + expect(rootView.getActiveView().isFocused).toBeFalsy() describe "when a new file is opened in the active editor", -> - it "is selected in the tree view if the file's entry visible", -> + it "selects the file in the tree view if the file's entry visible", -> sampleJs.click() rootView.open(require.resolve('fixtures/tree-view/tree-view.txt')) expect(sampleTxt).toHaveClass 'selected' expect(treeView.find('.selected').length).toBe 1 - it "selected a file's parent dir if the file's entry is not visible", -> - rootView.open(require.resolve('fixtures/tree-view/dir1/sub-dir1/sub-file1')) - + it "selects the file's parent dir if the file's entry is not visible", -> + rootView.open('dir1/sub-dir1/sub-file1') dirView = treeView.root.find('.directory:contains(dir1)').view() expect(dirView).toHaveClass 'selected' describe "when a different editor becomes active", -> it "selects the file in that is open in that editor", -> sampleJs.click() - leftEditor = rootView.getActiveEditor() + leftEditor = rootView.getActiveView() rightEditor = leftEditor.splitRight() sampleTxt.click() @@ -569,8 +568,8 @@ describe "TreeView", -> it "opens the file in the editor and focuses it", -> treeView.root.find('.file:contains(tree-view.js)').click() treeView.root.trigger 'tree-view:open-selected-entry' - expect(rootView.getActiveEditor().getPath()).toBe require.resolve('fixtures/tree-view/tree-view.js') - expect(rootView.getActiveEditor().isFocused).toBeTruthy() + expect(rootView.getActiveView().getPath()).toBe require.resolve('fixtures/tree-view/tree-view.js') + expect(rootView.getActiveView().isFocused).toBeTruthy() describe "when a directory is selected", -> it "expands or collapses the directory", -> @@ -586,7 +585,7 @@ describe "TreeView", -> describe "when nothing is selected", -> it "does nothing", -> treeView.root.trigger 'tree-view:open-selected-entry' - expect(rootView.getActiveEditor()).toBeUndefined() + expect(rootView.getActiveView()).toBeUndefined() describe "file modification", -> [dirView, fileView, rootDirPath, dirPath, filePath] = [] @@ -650,7 +649,7 @@ describe "TreeView", -> expect(fs.exists(newPath)).toBeTruthy() expect(fs.isFile(newPath)).toBeTruthy() expect(addDialog.parent()).not.toExist() - expect(rootView.getActiveEditor().getPath()).toBe newPath + expect(rootView.getActiveView().getPath()).toBe newPath waitsFor "tree view to be updated", -> dirView.entries.find("> .file").length > 1 @@ -680,9 +679,9 @@ describe "TreeView", -> expect(fs.exists(newPath)).toBeTruthy() expect(fs.isDirectory(newPath)).toBeTruthy() expect(addDialog.parent()).not.toExist() - expect(rootView.getActiveEditor().getPath()).not.toBe newPath + expect(rootView.getActiveView().getPath()).not.toBe newPath expect(treeView.find(".tree-view")).toMatchSelector(':focus') - expect(rootView.getActiveEditor().isFocused).toBeFalsy() + expect(rootView.getActiveView().isFocused).toBeFalsy() expect(dirView.find('.directory.selected:contains(new)').length).toBe(1) it "selects the created directory", -> @@ -693,9 +692,9 @@ describe "TreeView", -> expect(fs.exists(newPath)).toBeTruthy() expect(fs.isDirectory(newPath)).toBeTruthy() expect(addDialog.parent()).not.toExist() - expect(rootView.getActiveEditor().getPath()).not.toBe newPath + expect(rootView.getActiveView().getPath()).not.toBe newPath expect(treeView.find(".tree-view")).toMatchSelector(':focus') - expect(rootView.getActiveEditor().isFocused).toBeFalsy() + expect(rootView.getActiveView().isFocused).toBeFalsy() expect(dirView.find('.directory.selected:contains(new2)').length).toBe(1) describe "when a file or directory already exists at the given path", -> @@ -722,7 +721,7 @@ describe "TreeView", -> rootView.attachToDom() rootView.focus() expect(addDialog.parent()).not.toExist() - expect(rootView.getActiveEditor().isFocused).toBeTruthy() + expect(rootView.getActiveView().isFocused).toBeTruthy() describe "when a directory is selected", -> it "opens an add dialog with the directory's path populated", -> @@ -839,7 +838,7 @@ describe "TreeView", -> rootView.attachToDom() rootView.focus() expect(moveDialog.parent()).not.toExist() - expect(rootView.getActiveEditor().isFocused).toBeTruthy() + expect(rootView.getActiveView().isFocused).toBeTruthy() describe "when a file is selected that's name starts with a '.'", -> [dotFilePath, dotFileView, moveDialog] = [] diff --git a/src/packages/wrap-guide/spec/wrap-guide-spec.coffee b/src/packages/wrap-guide/spec/wrap-guide-spec.coffee index 5944b7692..ff14f7c27 100644 --- a/src/packages/wrap-guide/spec/wrap-guide-spec.coffee +++ b/src/packages/wrap-guide/spec/wrap-guide-spec.coffee @@ -8,7 +8,7 @@ describe "WrapGuide", -> rootView.open('sample.js') window.loadPackage('wrap-guide') rootView.attachToDom() - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() wrapGuide = rootView.find('.wrap-guide').view() editor.width(editor.charWidth * wrapGuide.getDefaultColumn() * 2) editor.trigger 'resize' diff --git a/src/stdlib/task.coffee b/src/stdlib/task.coffee index 3177e50be..eb3e2f3f5 100644 --- a/src/stdlib/task.coffee +++ b/src/stdlib/task.coffee @@ -1,3 +1,6 @@ +_ = require 'underscore' +EventEmitter = require 'event-emitter' + module.exports = class Task aborted: false @@ -49,3 +52,6 @@ class Task @abort() @worker?.terminate() @worker = null + @trigger 'task-completed' + +_.extend Task.prototype, EventEmitter diff --git a/static/atom.css b/static/atom.css index bfe56f43b..6cfb79269 100644 --- a/static/atom.css +++ b/static/atom.css @@ -21,12 +21,12 @@ html, body { -webkit-flex-flow: column; } -#root-view #panes { +#panes { position: relative; -webkit-flex: 1; } -#root-view #panes .column { +#panes .column { position: absolute; top: 0; bottom: 0; @@ -35,7 +35,7 @@ html, body { overflow-y: hidden; } -#root-view #panes .row { +#panes .row { position: absolute; top: 0; bottom: 0; @@ -44,7 +44,7 @@ html, body { overflow-x: hidden; } -#root-view #panes .pane { +#panes .pane { position: absolute; display: -webkit-flex; -webkit-flex-flow: column; @@ -55,6 +55,12 @@ html, body { box-sizing: border-box; } +#panes .pane .item-views { + -webkit-flex: 1; + display: -webkit-flex; + -webkit-flex-flow: column; +} + @font-face { font-family: 'Octicons Regular'; src: url("octicons-regular-webfont.woff") format("woff"); @@ -72,4 +78,4 @@ html, body { position: relative; display: inline-block; padding-left: 19px; -} \ No newline at end of file +} diff --git a/static/tabs.css b/static/tabs.css index 29133287a..bb262bba1 100644 --- a/static/tabs.css +++ b/static/tabs.css @@ -23,7 +23,7 @@ -webkit-flex: 2; } -.tab .file-name { +.tab .title { display: block; overflow: hidden; white-space: nowrap; @@ -52,7 +52,7 @@ color: #fff; } -.tab.file-modified .close-icon { +.tab.modified .close-icon { top: 11px; width: 5px; height: 5px; @@ -61,11 +61,11 @@ border-radius: 12px; } -.tab.file-modified .close-icon:before { +.tab.modified .close-icon:before { content: ""; } -.tab.file-modified:hover .close-icon { +.tab.modified:hover .close-icon { border: none; width: 12px; height: 12px; @@ -73,7 +73,7 @@ top: 5px; } -.tab.file-modified:hover .close-icon:before { +.tab.modified:hover .close-icon:before { content: "\f081"; color: #66a6ff; } diff --git a/vendor/space-pen.coffee b/vendor/space-pen.coffee index 08b715960..f734350e0 100644 --- a/vendor/space-pen.coffee +++ b/vendor/space-pen.coffee @@ -162,7 +162,8 @@ class Builder options.attributes = arg options -jQuery.fn.view = -> this.data('view') +jQuery.fn.view = -> @data('view') +jQuery.fn.views = -> @toArray().map (elt) -> jQuery(elt).view() # Trigger attach event when views are added to the DOM callAttachHook = (element) ->