From a3c48df25b85c1dc32da0c37eef7eb663c931e81 Mon Sep 17 00:00:00 2001 From: Adam Roben Date: Thu, 28 Feb 2013 13:55:48 -0500 Subject: [PATCH 001/308] Move all code and resources into Atom.framework All our native code now gets built into Atom.framework. Atom.app and Atom Helper.app both link against this framework. All resources other than a couple of main-bundle-only ones (e.g., atom.icns) go into Atom.framework. Note that this means that there's no compile- or link-time separation between main process code and helper process code. We could introduce a compile-time separation by building main process and helper process code into separate static libraries with mutually exclusive include paths, if we want. Atom.framework exports a single symbol: AtomMain(). Atom.app and Atom Helper.app contain a single source file: main.cpp. main() just calls AtomMain(). All frameworks are placed in Atom.app/Contents/Frameworks. We now link against all frameworks using @rpath-based install names, which allows Atom.app and Atom Helper.app to find them automatically based on their own LD_RUNPATH_SEARCH_PATH settings. We use install_name_tool at build time on each of our three binaries (Atom.app, Atom Helper.app, Atom.framework) to set the install names. By reducing duplication of code and resources between Atom.app and Atom Helper.app (and the EH/NP copies of Atom Helper.app), this reduces the size of the total installed Atom.app bundle from 145MB to 82MB. By compiling .coffee and .cson files only once, clean build time drops from 114 seconds to 79 seconds on my MacBook Pro. --- atom.gyp | 208 ++++++++++++----------- native/atom_cef_app.h | 4 - native/atom_main.h | 1 + native/{main_mac.mm => atom_main_mac.mm} | 18 +- native/atom_window_controller.mm | 4 +- native/mac/framework-info.plist | 28 +++ native/main.cpp | 5 + native/main_helper_mac.mm | 8 - 8 files changed, 159 insertions(+), 117 deletions(-) create mode 100644 native/atom_main.h rename native/{main_mac.mm => atom_main_mac.mm} (91%) create mode 100644 native/mac/framework-info.plist create mode 100644 native/main.cpp delete mode 100644 native/main_helper_mac.mm diff --git a/atom.gyp b/atom.gyp index a4d09f442..511d1584a 100644 --- a/atom.gyp +++ b/atom.gyp @@ -16,6 +16,22 @@ 'toolkit_uses_gtk%': 0, }], ], + 'fix_framework_link_command': [ + 'install_name_tool', + '-change', + '@executable_path/libcef.dylib', + '@rpath/Chromium Embedded Framework.framework/Libraries/libcef.dylib', + '-change', + '@executable_path/../Frameworks/CocoaOniguruma.framework/Versions/A/CocoaOniguruma', + '@rpath/CocoaOniguruma.framework/Versions/A/CocoaOniguruma', + '-change', + '@loader_path/../Frameworks/Sparkle.framework/Versions/A/Sparkle', + '@rpath/Sparkle.framework/Versions/A/Sparkle', + '-change', + '@executable_path/libgit2.0.17.0.dylib', + '@rpath/libgit2.framework/Libraries/libgit2.0.17.0.dylib', + '${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}' + ], }, 'includes': [ 'cef/cef_paths2.gypi', @@ -45,39 +61,20 @@ 'mac_bundle': 1, 'msvs_guid': 'D22C6F51-AA2D-457C-B579-6C97A96C724D', 'dependencies': [ - 'libcef_dll_wrapper', + 'atom_framework', ], - 'defines': [ - 'USING_CEF_SHARED', - ], - 'include_dirs': [ '.', 'cef', 'git2' ], 'mac_framework_dirs': [ 'native/frameworks' ], - 'libraries': [ 'native/frameworks/CocoaOniguruma.framework', 'native/frameworks/Sparkle.framework'], 'sources': [ - '<@(includes_common)', - '<@(includes_wrapper)', - 'native/main_mac.mm', - 'native/atom_application.h', - 'native/atom_application.mm', - 'native/atom_cef_app.h', - 'native/atom_window_controller.h', - 'native/atom_window_controller.mm', - 'native/atom_cef_client_mac.mm', - 'native/atom_cef_client.cpp', - 'native/atom_cef_client.h', - 'native/message_translation.cpp', - 'native/message_translation.h', + 'native/main.cpp', ], 'mac_bundle_resources': [ 'native/mac/atom.icns', 'native/mac/file.icns', 'native/mac/speakeasy.pem', - 'native/mac/English.lproj/MainMenu.xib', - 'native/mac/English.lproj/AtomWindow.xib', ], 'xcode_settings': { 'INFOPLIST_FILE': 'native/mac/info.plist', - 'OTHER_LDFLAGS': ['-Wl,-headerpad_max_install_names'], # Necessary to avoid an "install_name_tool: changing install names or rpaths can't be redone" error. + 'LD_RUNPATH_SEARCH_PATHS': '@executable_path/../Frameworks', }, 'conditions': [ ['CODE_SIGN' , { @@ -142,6 +139,7 @@ { 'destination': '<(PRODUCT_DIR)/Atom.app/Contents/Frameworks', 'files': [ + '<(PRODUCT_DIR)/Atom.framework', 'native/frameworks/CocoaOniguruma.framework', 'native/frameworks/Sparkle.framework', ], @@ -154,12 +152,6 @@ }, ], 'postbuilds': [ - { - 'postbuild_name': 'Copy and Compile Static Files', - 'action': [ - 'script/copy-files-to-bundle' - ], - }, { 'postbuild_name': 'Copy Helper App', 'action': [ @@ -172,11 +164,7 @@ { 'postbuild_name': 'Fix Framework Link', 'action': [ - 'install_name_tool', - '-change', - '@executable_path/libcef.dylib', - '@executable_path/../Frameworks/Chromium Embedded Framework.framework/Libraries/libcef.dylib', - '${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}' + '<@(fix_framework_link_command)', ], }, { @@ -231,6 +219,87 @@ }], ], }, + { + 'target_name': 'atom_framework', + 'product_name': 'Atom', + 'type': 'shared_library', + 'mac_bundle': 1, + 'dependencies': [ + 'libcef_dll_wrapper', + ], + 'defines': [ + 'USING_CEF_SHARED', + ], + 'xcode_settings': { + 'INFOPLIST_FILE': 'native/mac/framework-info.plist', + 'LD_DYLIB_INSTALL_NAME': '@rpath/Atom.framework/Atom', + }, + 'include_dirs': [ '.', 'cef', 'git2' ], + 'mac_framework_dirs': [ 'native/frameworks' ], + 'sources': [ + '<@(includes_common)', + '<@(includes_wrapper)', + 'native/atom_application.h', + 'native/atom_application.mm', + 'native/atom_cef_app.h', + 'native/atom_cef_app.h', + 'native/atom_cef_client.cpp', + 'native/atom_cef_client.h', + 'native/atom_cef_client_mac.mm', + 'native/atom_cef_render_process_handler.h', + 'native/atom_cef_render_process_handler.mm', + 'native/atom_window_controller.h', + 'native/atom_window_controller.mm', + 'native/atom_main.h', + 'native/atom_main_mac.mm', + 'native/message_translation.cpp', + 'native/message_translation.cpp', + 'native/message_translation.h', + 'native/message_translation.h', + 'native/path_watcher.h', + 'native/path_watcher.mm', + 'native/v8_extensions/atom.h', + 'native/v8_extensions/atom.mm', + 'native/v8_extensions/git.h', + 'native/v8_extensions/git.mm', + 'native/v8_extensions/native.h', + 'native/v8_extensions/native.mm', + 'native/v8_extensions/onig_reg_exp.h', + 'native/v8_extensions/onig_reg_exp.mm', + 'native/v8_extensions/onig_scanner.h', + 'native/v8_extensions/onig_scanner.mm', + 'native/v8_extensions/readtags.c', + 'native/v8_extensions/readtags.h', + 'native/v8_extensions/tags.h', + 'native/v8_extensions/tags.mm', + ], + 'link_settings': { + 'libraries': [ + '$(SDKROOT)/System/Library/Frameworks/AppKit.framework', + 'git2/frameworks/libgit2.0.17.0.dylib', + 'native/frameworks/CocoaOniguruma.framework', + 'native/frameworks/Sparkle.framework', + ], + }, + 'mac_bundle_resources': [ + 'native/mac/English.lproj/AtomWindow.xib', + 'native/mac/English.lproj/MainMenu.xib', + ], + 'postbuilds': [ + { + 'postbuild_name': 'Copy and Compile Static Files', + 'action': [ + 'script/copy-files-to-bundle' + ], + }, + { + 'postbuild_name': 'Fix Framework Link', + 'action': [ + '<@(fix_framework_link_command)', + ], + }, + ], + }, { 'target_name': 'libcef_dll_wrapper', 'type': 'static_library', @@ -271,46 +340,15 @@ 'product_name': 'Atom Helper', 'mac_bundle': 1, 'dependencies': [ - 'libcef_dll_wrapper', + 'atom_framework', ], 'defines': [ 'USING_CEF_SHARED', 'PROCESS_HELPER_APP', ], - 'include_dirs': [ '.', 'cef', 'git2' ], 'mac_framework_dirs': [ 'native/frameworks' ], - 'link_settings': { - 'libraries': [ - '$(SDKROOT)/System/Library/Frameworks/AppKit.framework', - ], - }, - 'libraries': [ - 'native/frameworks/CocoaOniguruma.framework', - 'git2/frameworks/libgit2.0.17.0.dylib', - ], 'sources': [ - 'native/atom_cef_app.h', - 'native/atom_cef_render_process_handler.h', - 'native/atom_cef_render_process_handler.mm', - 'native/message_translation.cpp', - 'native/message_translation.h', - 'native/path_watcher.mm', - 'native/path_watcher.h', - 'native/main_helper_mac.mm', - 'native/v8_extensions/native.mm', - 'native/v8_extensions/native.h', - 'native/v8_extensions/onig_reg_exp.mm', - 'native/v8_extensions/onig_reg_exp.h', - 'native/v8_extensions/onig_scanner.mm', - 'native/v8_extensions/onig_scanner.h', - 'native/v8_extensions/atom.mm', - 'native/v8_extensions/atom.h', - 'native/v8_extensions/git.mm', - 'native/v8_extensions/git.h', - 'native/v8_extensions/readtags.h', - 'native/v8_extensions/readtags.c', - 'native/v8_extensions/tags.h', - 'native/v8_extensions/tags.mm', + 'native/main.cpp', ], # TODO(mark): For now, don't put any resources into this app. Its # resources directory will be a symbolic link to the browser app's @@ -320,45 +358,13 @@ ], 'xcode_settings': { 'INFOPLIST_FILE': 'native/mac/helper-info.plist', - 'OTHER_LDFLAGS': ['-Wl,-headerpad_max_install_names'], # Necessary to avoid an "install_name_tool: changing install names or rpaths can't be redone" error. + 'LD_RUNPATH_SEARCH_PATHS': '@executable_path/../../..', }, - 'copies': [ - { - 'destination': '<(PRODUCT_DIR)/Atom Helper.app/Contents/Frameworks', - 'files': [ - 'native/frameworks/CocoaOniguruma.framework', - ], - }, - ], 'postbuilds': [ { - # The framework defines its load-time path - # (DYLIB_INSTALL_NAME_BASE) relative to the main executable - # (chrome). A different relative path needs to be used in - # atom_helper_app. - 'postbuild_name': 'Fix CEF Framework Link', + 'postbuild_name': 'Fix Framework Link', 'action': [ - 'install_name_tool', - '-change', - '@executable_path/libcef.dylib', - '@executable_path/../../../../Frameworks/Chromium Embedded Framework.framework/Libraries/libcef.dylib', - '${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}' - ], - }, - { - 'postbuild_name': 'Fix libgit2 Framework Link', - 'action': [ - 'install_name_tool', - '-change', - '@executable_path/libgit2.0.17.0.dylib', - '@executable_path/../../../../Frameworks/libgit2.framework/Libraries/libgit2.0.17.0.dylib', - '${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}' - ], - }, - { - 'postbuild_name': 'Copy and Compile Static Files', - 'action': [ - 'script/copy-files-to-bundle' + '<@(fix_framework_link_command)', ], }, ], diff --git a/native/atom_cef_app.h b/native/atom_cef_app.h index faa5e9a15..5cd8ce3ac 100644 --- a/native/atom_cef_app.h +++ b/native/atom_cef_app.h @@ -4,17 +4,13 @@ #include "include/cef_app.h" -#ifdef PROCESS_HELPER_APP #include "atom_cef_render_process_handler.h" -#endif class AtomCefApp : public CefApp { -#ifdef PROCESS_HELPER_APP virtual CefRefPtr GetRenderProcessHandler() OVERRIDE { return CefRefPtr(new AtomCefRenderProcessHandler); } -#endif IMPLEMENT_REFCOUNTING(AtomCefApp); }; diff --git a/native/atom_main.h b/native/atom_main.h new file mode 100644 index 000000000..a90926e32 --- /dev/null +++ b/native/atom_main.h @@ -0,0 +1 @@ +__attribute__((visibility("default"))) int AtomMain(int argc, char* argv[]); diff --git a/native/main_mac.mm b/native/atom_main_mac.mm similarity index 91% rename from native/main_mac.mm rename to native/atom_main_mac.mm index 8ccb8f887..6c148fc84 100644 --- a/native/main_mac.mm +++ b/native/atom_main_mac.mm @@ -1,3 +1,5 @@ +#import "atom_main.h" +#import "atom_cef_app.h" #import "include/cef_application_mac.h" #import "native/atom_application.h" #include @@ -10,7 +12,19 @@ void listenForPathToOpen(int fd, NSString *socketPath); void activateOpenApp(); BOOL isAppAlreadyOpen(); -int main(int argc, char* argv[]) { +int AtomMain(int argc, char* argv[]) { + { + // See if we're being run as a secondary process. + + CefMainArgs main_args(argc, argv); + CefRefPtr app(new AtomCefApp); + int exitCode = CefExecuteProcess(main_args, app); + if (exitCode >= 0) + return exitCode; + } + + // We're the main process. + @autoreleasepool { handleBeingOpenedAgain(argc, argv); @@ -18,7 +32,7 @@ int main(int argc, char* argv[]) { AtomApplication *application = [AtomApplication applicationWithArguments:argv count:argc]; NSString *mainNibName = [infoDictionary objectForKey:@"NSMainNibFile"]; - NSNib *mainNib = [[NSNib alloc] initWithNibNamed:mainNibName bundle:[NSBundle mainBundle]]; + NSNib *mainNib = [[NSNib alloc] initWithNibNamed:mainNibName bundle:[NSBundle bundleWithIdentifier:@"com.github.atom.framework"]]; [mainNib instantiateNibWithOwner:application topLevelObjects:nil]; CefRunMessageLoop(); diff --git a/native/atom_window_controller.mm b/native/atom_window_controller.mm index ebed667ee..5d2588c45 100644 --- a/native/atom_window_controller.mm +++ b/native/atom_window_controller.mm @@ -45,7 +45,7 @@ } if (alwaysUseBundleResourcePath || !_resourcePath) { - _resourcePath = [[NSBundle mainBundle] resourcePath]; + _resourcePath = [[NSBundle bundleForClass:self.class] resourcePath]; } [_resourcePath retain]; @@ -119,7 +119,7 @@ // have the correct initial size based on the frame's last stored size. // HACK: I hate this and want to place this code directly in windowDidLoad - (void)attachWebView { - NSURL *url = [[NSBundle mainBundle] resourceURL]; + NSURL *url = [[NSBundle bundleForClass:self.class] resourceURL]; NSMutableString *urlString = [NSMutableString string]; [urlString appendString:[[url URLByAppendingPathComponent:@"static/index.html"] absoluteString]]; [urlString appendFormat:@"?bootstrapScript=%@", [self encodeUrlParam:_bootstrapScript]]; diff --git a/native/mac/framework-info.plist b/native/mac/framework-info.plist new file mode 100644 index 000000000..fab48f695 --- /dev/null +++ b/native/mac/framework-info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + + CFBundleIdentifier + com.github.atom.framework + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSPrincipalClass + + + diff --git a/native/main.cpp b/native/main.cpp new file mode 100644 index 000000000..43e9b291d --- /dev/null +++ b/native/main.cpp @@ -0,0 +1,5 @@ +#include "atom_main.h" + +int main(int argc, char* argv[]) { + return AtomMain(argc, argv); +} diff --git a/native/main_helper_mac.mm b/native/main_helper_mac.mm deleted file mode 100644 index 9e0fa0689..000000000 --- a/native/main_helper_mac.mm +++ /dev/null @@ -1,8 +0,0 @@ -#include "include/cef_app.h" -#include "atom_cef_app.h" - -int main(int argc, char* argv[]) { - CefMainArgs main_args(argc, argv); - CefRefPtr app(new AtomCefApp); - return CefExecuteProcess(main_args, app); // Execute the secondary process. -} From 225aca016b78bbb96b026eda00707cd944237c86 Mon Sep 17 00:00:00 2001 From: Adam Roben Date: Fri, 1 Mar 2013 16:12:02 -0500 Subject: [PATCH 002/308] Preserve symlinks during `rake install` This prevents resources within Atom.framework from getting duplicated within the bundle. This shrinks a `rake install`-ed Atom.app from 124MB to 82MB. --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 2b846e966..86d9f979b 100644 --- a/Rakefile +++ b/Rakefile @@ -42,7 +42,7 @@ task :install => [:clean, :build] do # Install Atom.app dest_path = "/Applications/#{File.basename(path)}" `rm -rf #{dest_path}` - `cp -r #{path} #{File.expand_path(dest_path)}` + `cp -a #{path} #{File.expand_path(dest_path)}` # Install atom cli if File.directory?("/opt/boxen") From 8a3b395613c31a1a0d253401cbd420b02fa6c26a Mon Sep 17 00:00:00 2001 From: Adam Roben Date: Fri, 1 Mar 2013 16:26:57 -0500 Subject: [PATCH 003/308] Use gyp's copies facility instead of cp -r This is more portable, and ensures that we won't screw up symlinks like cp -r would. --- atom.gyp | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/atom.gyp b/atom.gyp index 511d1584a..d96036267 100644 --- a/atom.gyp +++ b/atom.gyp @@ -139,6 +139,7 @@ { 'destination': '<(PRODUCT_DIR)/Atom.app/Contents/Frameworks', 'files': [ + '<(PRODUCT_DIR)/Atom Helper.app', '<(PRODUCT_DIR)/Atom.framework', 'native/frameworks/CocoaOniguruma.framework', 'native/frameworks/Sparkle.framework', @@ -150,32 +151,20 @@ 'git2/frameworks/libgit2.0.17.0.dylib', ], }, - ], - 'postbuilds': [ { - 'postbuild_name': 'Copy Helper App', - 'action': [ - 'cp', - '-r', - '${BUILT_PRODUCTS_DIR}/Atom Helper.app', - '${BUILT_PRODUCTS_DIR}/Atom.app/Contents/Frameworks', + 'destination': '<(PRODUCT_DIR)/Atom.app/Contents/Frameworks/Chromium Embedded Framework.framework', + 'files': [ + 'cef/Resources', ], }, + ], + 'postbuilds': [ { 'postbuild_name': 'Fix Framework Link', 'action': [ '<@(fix_framework_link_command)', ], }, - { - 'postbuild_name': 'Copy Framework Resources Directory', - 'action': [ - 'cp', - '-r', - 'cef/Resources', - '${BUILT_PRODUCTS_DIR}/Atom.app/Contents/Frameworks/Chromium Embedded Framework.framework/' - ], - }, { # This postbuid step is responsible for creating the following # helpers: From 82bce082efa971d053f6a7d2d32bbd50b61f2305 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Fri, 1 Mar 2013 09:33:57 -0800 Subject: [PATCH 004/308] Always call `stringByStandardizingPath` on the resource path stringByStandardizingPath has an interesting quirk that causes it to remove `/private` from the path if the result still indicates an existing file or directory --- native/atom_window_controller.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/atom_window_controller.mm b/native/atom_window_controller.mm index ebed667ee..1fdcc720b 100644 --- a/native/atom_window_controller.mm +++ b/native/atom_window_controller.mm @@ -35,7 +35,6 @@ _resourcePath = [atomApplication.arguments objectForKey:@"resource-path"]; if (!alwaysUseBundleResourcePath && !_resourcePath) { NSString *defaultRepositoryPath = @"~/github/atom"; - defaultRepositoryPath = [defaultRepositoryPath stringByStandardizingPath]; if ([defaultRepositoryPath characterAtIndex:0] == '/') { BOOL isDir = false; BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:defaultRepositoryPath isDirectory:&isDir]; @@ -47,6 +46,7 @@ if (alwaysUseBundleResourcePath || !_resourcePath) { _resourcePath = [[NSBundle mainBundle] resourcePath]; } + _resourcePath = [_resourcePath stringByStandardizingPath]; [_resourcePath retain]; if (!background) { From 5e1701f8dbddd4953d9ab4a76404b6b4d1c898b0 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Fri, 1 Mar 2013 10:19:53 -0800 Subject: [PATCH 005/308] Call stringByStandardizingPath on moved paths --- native/path_watcher.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/path_watcher.mm b/native/path_watcher.mm index 2bf5907e4..e5cc65afe 100644 --- a/native/path_watcher.mm +++ b/native/path_watcher.mm @@ -245,7 +245,7 @@ static NSMutableArray *gPathWatchers; char pathBuffer[MAXPATHLEN]; fcntl((int)event.ident, F_GETPATH, &pathBuffer); close(event.ident); - newPath = [NSString stringWithUTF8String:pathBuffer]; + newPath = [[NSString stringWithUTF8String:pathBuffer] stringByStandardizingPath]; if (!newPath) { NSLog(@"WARNING: Ignoring rename event for deleted file '%@'", path); continue; From 595cf71d9309f6266c75d59c3a796cace6988664 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Fri, 1 Mar 2013 11:40:27 -0800 Subject: [PATCH 006/308] Return absolute path for Git repo --- spec/app/git-spec.coffee | 4 ++-- src/app/git.coffee | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/app/git-spec.coffee b/spec/app/git-spec.coffee index fc9c85f46..522d38d8e 100644 --- a/spec/app/git-spec.coffee +++ b/spec/app/git-spec.coffee @@ -21,11 +21,11 @@ describe "Git", -> describe ".getPath()", -> it "returns the repository path for a .git directory path", -> repo = new Git(require.resolve('fixtures/git/master.git/HEAD')) - expect(repo.getPath()).toBe require.resolve('fixtures/git/master.git') + '/' + expect(repo.getPath()).toBe require.resolve('fixtures/git/master.git') it "returns the repository path for a repository path", -> repo = new Git(require.resolve('fixtures/git/master.git')) - expect(repo.getPath()).toBe require.resolve('fixtures/git/master.git') + '/' + expect(repo.getPath()).toBe require.resolve('fixtures/git/master.git') describe ".getHead()", -> it "returns a branch name for a non-empty repository", -> diff --git a/src/app/git.coffee b/src/app/git.coffee index 866e2977f..1c9fffc0a 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -1,10 +1,10 @@ _ = require 'underscore' +fs = require 'fs' Subscriber = require 'subscriber' GitRepository = require 'git-repository' module.exports = class Git - @open: (path, options) -> try new Git(path, options) @@ -37,7 +37,8 @@ class Git refreshIndex: -> @getRepo().refreshIndex() - getPath: -> @getRepo().getPath() + getPath: -> + @path ?= fs.absolute(@getRepo().getPath()) destroy: -> @getRepo().destroy() @@ -45,8 +46,7 @@ class Git @unsubscribe() getWorkingDirectory: -> - repoPath = @getPath() - repoPath?.substring(0, repoPath.length - 6) + @getPath()?.replace(/\/\.git\/?/, '') getHead: -> @getRepo().getHead() ? '' From 2966cdb0338e3e583398be8c174ac119d6a6c2c2 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Fri, 1 Mar 2013 11:41:09 -0800 Subject: [PATCH 007/308] wip --- native/atom_window_controller.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/atom_window_controller.mm b/native/atom_window_controller.mm index 1fdcc720b..d10dc254e 100644 --- a/native/atom_window_controller.mm +++ b/native/atom_window_controller.mm @@ -34,7 +34,7 @@ _resourcePath = [atomApplication.arguments objectForKey:@"resource-path"]; if (!alwaysUseBundleResourcePath && !_resourcePath) { - NSString *defaultRepositoryPath = @"~/github/atom"; + NSString *defaultRepositoryPath = @"/tmp/atom"; if ([defaultRepositoryPath characterAtIndex:0] == '/') { BOOL isDir = false; BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:defaultRepositoryPath isDirectory:&isDir]; From b7976cac688c4bd355eab43340345452f4ac7ed8 Mon Sep 17 00:00:00 2001 From: probablycorey Date: Fri, 1 Mar 2013 13:40:26 -0800 Subject: [PATCH 008/308] Use the /Applications directory to test cwd on Child Processes --- spec/stdlib/child-process-spec.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/stdlib/child-process-spec.coffee b/spec/stdlib/child-process-spec.coffee index e528aebec..e594d2b7f 100644 --- a/spec/stdlib/child-process-spec.coffee +++ b/spec/stdlib/child-process-spec.coffee @@ -134,11 +134,11 @@ describe 'Child Processes', -> waitsForPromise -> options = - cwd: fixturesProject.getPath() + cwd: "/Applications" stdout: (data) -> output.push(data) stderr: (data) -> ChildProcess.exec("pwd", options) runs -> - expect(output.join('')).toBe "#{fixturesProject.getPath()}\n" + expect(output.join('')).toBe "/Applications\n" From 4e971b085e6ae98c78bf5f17df8088922707b72f Mon Sep 17 00:00:00 2001 From: probablycorey Date: Fri, 1 Mar 2013 13:45:24 -0800 Subject: [PATCH 009/308] Now that specs can handle symlinks, put atom-build in /tmp --- .gitignore | 1 - Rakefile | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 15d8caaf4..537481ed0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ .project .svn .nvm-version -atom-build atom.xcodeproj build .xcodebuild-info diff --git a/Rakefile b/Rakefile index 2b846e966..eeb475144 100644 --- a/Rakefile +++ b/Rakefile @@ -1,5 +1,5 @@ ATOM_SRC_PATH = File.dirname(__FILE__) -BUILD_DIR = 'atom-build' +BUILD_DIR = '/tmp/atom-build' desc "Build Atom via `xcodebuild`" task :build => "create-xcode-project" do From 591aba3faf37a8c553276e5fb7cdb9a978a0d5c6 Mon Sep 17 00:00:00 2001 From: probablycorey Date: Fri, 1 Mar 2013 14:09:43 -0800 Subject: [PATCH 010/308] Revert "wip" This reverts commit 2966cdb0338e3e583398be8c174ac119d6a6c2c2. --- native/atom_window_controller.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/atom_window_controller.mm b/native/atom_window_controller.mm index d10dc254e..1fdcc720b 100644 --- a/native/atom_window_controller.mm +++ b/native/atom_window_controller.mm @@ -34,7 +34,7 @@ _resourcePath = [atomApplication.arguments objectForKey:@"resource-path"]; if (!alwaysUseBundleResourcePath && !_resourcePath) { - NSString *defaultRepositoryPath = @"/tmp/atom"; + NSString *defaultRepositoryPath = @"~/github/atom"; if ([defaultRepositoryPath characterAtIndex:0] == '/') { BOOL isDir = false; BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:defaultRepositoryPath isDirectory:&isDir]; From 467e30aeb1f8cb673e7db1d8e0424c06cefbf20b Mon Sep 17 00:00:00 2001 From: probablycorey Date: Fri, 1 Mar 2013 14:55:44 -0800 Subject: [PATCH 011/308] Remove worker log message --- native/atom_cef_render_process_handler.mm | 1 - 1 file changed, 1 deletion(-) diff --git a/native/atom_cef_render_process_handler.mm b/native/atom_cef_render_process_handler.mm index fe49a8935..d36a6c235 100644 --- a/native/atom_cef_render_process_handler.mm +++ b/native/atom_cef_render_process_handler.mm @@ -34,7 +34,6 @@ void AtomCefRenderProcessHandler::OnWorkerContextCreated(int worker_id, void AtomCefRenderProcessHandler::OnWorkerContextReleased(int worker_id, const CefString& url, CefRefPtr context) { - NSLog(@"Web worker context released"); } void AtomCefRenderProcessHandler::OnWorkerUncaughtException(int worker_id, From 5e25d3634c2fceb467d60312cb0d91bb17783cfb Mon Sep 17 00:00:00 2001 From: probablycorey Date: Fri, 1 Mar 2013 14:56:42 -0800 Subject: [PATCH 012/308] Set CFBundleTypeRole to Editor This gets ride of an annoying console warning. --- native/mac/info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/native/mac/info.plist b/native/mac/info.plist index 708d0b78f..c295d2e6a 100644 --- a/native/mac/info.plist +++ b/native/mac/info.plist @@ -7,6 +7,8 @@ CFBundleDocumentTypes + CFBundleTypeRole + Editor CFBundleTypeIconFile file.icns LSItemContentTypes From d6309ec31c60dfdf2c545d13f7de2797430a6333 Mon Sep 17 00:00:00 2001 From: probablycorey Date: Fri, 1 Mar 2013 15:00:20 -0800 Subject: [PATCH 013/308] Add autorelease pool wrappers around CefV8Handler::Execute methods This removes all BlahBlahBlah was not autoreleased console warnings. --- native/v8_extensions/atom.mm | 34 ++++---- native/v8_extensions/git.mm | 114 ++++++++++++------------- native/v8_extensions/native.mm | 28 +++---- native/v8_extensions/onig_reg_exp.mm | 50 +++++------ native/v8_extensions/onig_scanner.mm | 24 +++--- native/v8_extensions/tags.mm | 120 ++++++++++++++------------- 6 files changed, 189 insertions(+), 181 deletions(-) diff --git a/native/v8_extensions/atom.mm b/native/v8_extensions/atom.mm index 403cac409..0acb51d82 100644 --- a/native/v8_extensions/atom.mm +++ b/native/v8_extensions/atom.mm @@ -22,24 +22,26 @@ namespace v8_extensions { const CefV8ValueList& arguments, CefRefPtr& retval, CefString& exception) { - CefRefPtr browser = CefV8Context::GetCurrentContext()->GetBrowser(); + @autoreleasepool { + CefRefPtr browser = CefV8Context::GetCurrentContext()->GetBrowser(); - if (name == "sendMessageToBrowserProcess") { - if (arguments.size() == 0 || !arguments[0]->IsString()) { - exception = "You must supply a message name"; - return false; + if (name == "sendMessageToBrowserProcess") { + if (arguments.size() == 0 || !arguments[0]->IsString()) { + exception = "You must supply a message name"; + return false; + } + + CefString name = arguments[0]->GetStringValue(); + CefRefPtr message = CefProcessMessage::Create(name); + + if (arguments.size() > 1 && arguments[1]->IsArray()) { + TranslateList(arguments[1], message->GetArgumentList()); + } + + browser->SendProcessMessage(PID_BROWSER, message); + return true; } - - CefString name = arguments[0]->GetStringValue(); - CefRefPtr message = CefProcessMessage::Create(name); - - if (arguments.size() > 1 && arguments[1]->IsArray()) { - TranslateList(arguments[1], message->GetArgumentList()); - } - - browser->SendProcessMessage(PID_BROWSER, message); - return true; + return false; } - return false; }; } diff --git a/native/v8_extensions/git.mm b/native/v8_extensions/git.mm index d2cda0a29..4c7bbb056 100644 --- a/native/v8_extensions/git.mm +++ b/native/v8_extensions/git.mm @@ -210,72 +210,74 @@ namespace v8_extensions { const CefV8ValueList& arguments, CefRefPtr& retval, CefString& exception) { - if (name == "getRepository") { - GitRepository *repository = new GitRepository(arguments[0]->GetStringValue().ToString().c_str()); - if (repository->Exists()) { - CefRefPtr userData = repository; - retval = CefV8Value::CreateObject(NULL); - retval->SetUserData(userData); - } else { - retval = CefV8Value::CreateNull(); + @autoreleasepool { + if (name == "getRepository") { + GitRepository *repository = new GitRepository(arguments[0]->GetStringValue().ToString().c_str()); + if (repository->Exists()) { + CefRefPtr userData = repository; + retval = CefV8Value::CreateObject(NULL); + retval->SetUserData(userData); + } else { + retval = CefV8Value::CreateNull(); + } + return true; } - return true; - } - if (name == "getHead") { - GitRepository *userData = (GitRepository *)object->GetUserData().get(); - retval = userData->GetHead(); - return true; - } + if (name == "getHead") { + GitRepository *userData = (GitRepository *)object->GetUserData().get(); + retval = userData->GetHead(); + return true; + } - if (name == "getPath") { - GitRepository *userData = (GitRepository *)object->GetUserData().get(); - retval = userData->GetPath(); - return true; - } + if (name == "getPath") { + GitRepository *userData = (GitRepository *)object->GetUserData().get(); + retval = userData->GetPath(); + return true; + } - if (name == "isIgnored") { - GitRepository *userData = (GitRepository *)object->GetUserData().get(); - retval = userData->IsIgnored(arguments[0]->GetStringValue().ToString().c_str()); - return true; - } + if (name == "isIgnored") { + GitRepository *userData = (GitRepository *)object->GetUserData().get(); + retval = userData->IsIgnored(arguments[0]->GetStringValue().ToString().c_str()); + return true; + } - if (name == "getStatus") { - GitRepository *userData = (GitRepository *)object->GetUserData().get(); - retval = userData->GetStatus(arguments[0]->GetStringValue().ToString().c_str()); - return true; - } + if (name == "getStatus") { + GitRepository *userData = (GitRepository *)object->GetUserData().get(); + retval = userData->GetStatus(arguments[0]->GetStringValue().ToString().c_str()); + return true; + } - if (name == "checkoutHead") { - GitRepository *userData = (GitRepository *)object->GetUserData().get(); - retval = userData->CheckoutHead(arguments[0]->GetStringValue().ToString().c_str()); - return true; - } + if (name == "checkoutHead") { + GitRepository *userData = (GitRepository *)object->GetUserData().get(); + retval = userData->CheckoutHead(arguments[0]->GetStringValue().ToString().c_str()); + return true; + } - if (name == "getDiffStats") { - GitRepository *userData = (GitRepository *)object->GetUserData().get(); - retval = userData->GetDiffStats(arguments[0]->GetStringValue().ToString().c_str()); - return true; - } + if (name == "getDiffStats") { + GitRepository *userData = (GitRepository *)object->GetUserData().get(); + retval = userData->GetDiffStats(arguments[0]->GetStringValue().ToString().c_str()); + return true; + } - if (name == "isSubmodule") { - GitRepository *userData = (GitRepository *)object->GetUserData().get(); - retval = userData->IsSubmodule(arguments[0]->GetStringValue().ToString().c_str()); - return true; - } + if (name == "isSubmodule") { + GitRepository *userData = (GitRepository *)object->GetUserData().get(); + retval = userData->IsSubmodule(arguments[0]->GetStringValue().ToString().c_str()); + return true; + } - if (name == "refreshIndex") { - GitRepository *userData = (GitRepository *)object->GetUserData().get(); - userData->RefreshIndex(); - return true; - } + if (name == "refreshIndex") { + GitRepository *userData = (GitRepository *)object->GetUserData().get(); + userData->RefreshIndex(); + return true; + } - if (name == "destroy") { - GitRepository *userData = (GitRepository *)object->GetUserData().get(); - userData->Destroy(); - return true; - } + if (name == "destroy") { + GitRepository *userData = (GitRepository *)object->GetUserData().get(); + userData->Destroy(); + return true; + } - return false; + return false; + } } } diff --git a/native/v8_extensions/native.mm b/native/v8_extensions/native.mm index b5a7e3449..21f7e6c3a 100644 --- a/native/v8_extensions/native.mm +++ b/native/v8_extensions/native.mm @@ -48,6 +48,7 @@ namespace v8_extensions { const CefV8ValueList& arguments, CefRefPtr& retval, CefString& exception) { + @autoreleasepool { if (name == "exists") { std::string cc_value = arguments[0]->GetStringValue().ToString(); const char *path = cc_value.c_str(); @@ -526,10 +527,8 @@ namespace v8_extensions { NSString *word = stringFromCefV8Value(arguments[0]); NSSpellChecker *spellChecker = [NSSpellChecker sharedSpellChecker]; @synchronized(spellChecker) { - @autoreleasepool { - NSRange range = [spellChecker checkSpellingOfString:word startingAt:0]; - retval = CefV8Value::CreateBool(range.length > 0); - } + NSRange range = [spellChecker checkSpellingOfString:word startingAt:0]; + retval = CefV8Value::CreateBool(range.length > 0); } return true; } @@ -538,23 +537,22 @@ namespace v8_extensions { NSString *misspelling = stringFromCefV8Value(arguments[0]); NSSpellChecker *spellChecker = [NSSpellChecker sharedSpellChecker]; @synchronized(spellChecker) { - @autoreleasepool { - NSString *language = [spellChecker language]; - NSRange range; - range.location = 0; - range.length = [misspelling length]; - NSArray *guesses = [spellChecker guessesForWordRange:range inString:misspelling language:language inSpellDocumentWithTag:0]; - CefRefPtr v8Guesses = CefV8Value::CreateArray([guesses count]); - for (int i = 0; i < [guesses count]; i++) { - v8Guesses->SetValue(i, CefV8Value::CreateString([[guesses objectAtIndex:i] UTF8String])); - } - retval = v8Guesses; + NSString *language = [spellChecker language]; + NSRange range; + range.location = 0; + range.length = [misspelling length]; + NSArray *guesses = [spellChecker guessesForWordRange:range inString:misspelling language:language inSpellDocumentWithTag:0]; + CefRefPtr v8Guesses = CefV8Value::CreateArray([guesses count]); + for (int i = 0; i < [guesses count]; i++) { + v8Guesses->SetValue(i, CefV8Value::CreateString([[guesses objectAtIndex:i] UTF8String])); } + retval = v8Guesses; } return true; } return false; + } }; NSString *stringFromCefV8Value(const CefRefPtr& value) { diff --git a/native/v8_extensions/onig_reg_exp.mm b/native/v8_extensions/onig_reg_exp.mm index 6f8b5edcd..21b59b342 100644 --- a/native/v8_extensions/onig_reg_exp.mm +++ b/native/v8_extensions/onig_reg_exp.mm @@ -73,32 +73,34 @@ bool OnigRegExp::Execute(const CefString& name, CefRefPtr& retval, CefString& exception) { - if (name == "search") { - CefRefPtr string = arguments[0]; - CefRefPtr index = arguments.size() > 1 ? arguments[1] : CefV8Value::CreateInt(0); - OnigRegExpUserData *userData = (OnigRegExpUserData *)object->GetUserData().get(); - retval = userData->Search(string, index); - return true; - } - else if (name == "test") { - CefRefPtr string = arguments[0]; - CefRefPtr index = arguments.size() > 1 ? arguments[1] : CefV8Value::CreateInt(0); - OnigRegExpUserData *userData = (OnigRegExpUserData *)object->GetUserData().get(); - retval = userData->Test(string, index); - return true; - } - else if (name == "buildOnigRegExp") { - CefRefPtr pattern = arguments[0]; - CefRefPtr userData = new OnigRegExpUserData(pattern); - if (!userData->m_regex) { - exception = std::string("Failed to create OnigRegExp from pattern '") + pattern->GetStringValue().ToString() + "'"; + @autoreleasepool { + if (name == "search") { + CefRefPtr string = arguments[0]; + CefRefPtr index = arguments.size() > 1 ? arguments[1] : CefV8Value::CreateInt(0); + OnigRegExpUserData *userData = (OnigRegExpUserData *)object->GetUserData().get(); + retval = userData->Search(string, index); + return true; + } + else if (name == "test") { + CefRefPtr string = arguments[0]; + CefRefPtr index = arguments.size() > 1 ? arguments[1] : CefV8Value::CreateInt(0); + OnigRegExpUserData *userData = (OnigRegExpUserData *)object->GetUserData().get(); + retval = userData->Test(string, index); + return true; + } + else if (name == "buildOnigRegExp") { + CefRefPtr pattern = arguments[0]; + CefRefPtr userData = new OnigRegExpUserData(pattern); + if (!userData->m_regex) { + exception = std::string("Failed to create OnigRegExp from pattern '") + pattern->GetStringValue().ToString() + "'"; + } + retval = CefV8Value::CreateObject(NULL); + retval->SetUserData((CefRefPtr)userData); + return true; } - retval = CefV8Value::CreateObject(NULL); - retval->SetUserData((CefRefPtr)userData); - return true; - } - return false; + return false; + } } } // namespace v8_extensions \ No newline at end of file diff --git a/native/v8_extensions/onig_scanner.mm b/native/v8_extensions/onig_scanner.mm index 9d429de97..3a8db417f 100644 --- a/native/v8_extensions/onig_scanner.mm +++ b/native/v8_extensions/onig_scanner.mm @@ -152,18 +152,20 @@ bool OnigScanner::Execute(const CefString& name, const CefV8ValueList& arguments, CefRefPtr& retval, CefString& exception) { - if (name == "findNextMatch") { - OnigScannerUserData *userData = (OnigScannerUserData *)object->GetUserData().get(); - retval = userData->FindNextMatch(arguments[0], arguments[1]); - return true; - } - else if (name == "buildScanner") { - retval = CefV8Value::CreateObject(NULL); - retval->SetUserData(new OnigScannerUserData(arguments[0])); - return true; - } + @autoreleasepool { + if (name == "findNextMatch") { + OnigScannerUserData *userData = (OnigScannerUserData *)object->GetUserData().get(); + retval = userData->FindNextMatch(arguments[0], arguments[1]); + return true; + } + else if (name == "buildScanner") { + retval = CefV8Value::CreateObject(NULL); + retval->SetUserData(new OnigScannerUserData(arguments[0])); + return true; + } - return false; + return false; + } } } // namespace v8_extensions diff --git a/native/v8_extensions/tags.mm b/native/v8_extensions/tags.mm index 74e46f275..2e826d323 100644 --- a/native/v8_extensions/tags.mm +++ b/native/v8_extensions/tags.mm @@ -37,78 +37,80 @@ namespace v8_extensions { CefRefPtr& retval, CefString& exception) { - if (name == "find") { - std::string path = arguments[0]->GetStringValue().ToString(); - std::string tag = arguments[1]->GetStringValue().ToString(); - tagFileInfo info; - tagFile* tagFile; - tagFile = tagsOpen(path.c_str(), &info); - if (info.status.opened) { - tagEntry entry; - std::vector> entries; - if (tagsFind(tagFile, &entry, tag.c_str(), TAG_FULLMATCH | TAG_OBSERVECASE) == TagSuccess) { - entries.push_back(ParseEntry(entry)); - while (tagsFindNext(tagFile, &entry) == TagSuccess) { - entries.push_back(ParseEntry(entry)); - } - } - - retval = CefV8Value::CreateArray(entries.size()); - for (int i = 0; i < entries.size(); i++) { - retval->SetValue(i, entries[i]); - } - tagsClose(tagFile); - } - return true; - } - - if (name == "getAllTagsAsync") { - std::string path = arguments[0]->GetStringValue().ToString(); - CefRefPtr callback = arguments[1]; - CefRefPtr context = CefV8Context::GetCurrentContext(); - - dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - dispatch_async(queue, ^{ + @autoreleasepool { + if (name == "find") { + std::string path = arguments[0]->GetStringValue().ToString(); + std::string tag = arguments[1]->GetStringValue().ToString(); tagFileInfo info; tagFile* tagFile; tagFile = tagsOpen(path.c_str(), &info); - std::vector entries; - if (info.status.opened) { tagEntry entry; - while (tagsNext(tagFile, &entry) == TagSuccess) { - entry.name = strdup(entry.name); - entry.file = strdup(entry.file); - if (entry.address.pattern) { - entry.address.pattern = strdup(entry.address.pattern); + std::vector> entries; + if (tagsFind(tagFile, &entry, tag.c_str(), TAG_FULLMATCH | TAG_OBSERVECASE) == TagSuccess) { + entries.push_back(ParseEntry(entry)); + while (tagsFindNext(tagFile, &entry) == TagSuccess) { + entries.push_back(ParseEntry(entry)); } - entries.push_back(entry); + } + + retval = CefV8Value::CreateArray(entries.size()); + for (int i = 0; i < entries.size(); i++) { + retval->SetValue(i, entries[i]); } tagsClose(tagFile); } + return true; + } - dispatch_queue_t mainQueue = dispatch_get_main_queue(); - dispatch_async(mainQueue, ^{ - context->Enter(); - CefRefPtr v8Tags = CefV8Value::CreateArray(entries.size()); - for (int i = 0; i < entries.size(); i++) { - v8Tags->SetValue(i, ParseEntry(entries[i])); - free((void*)entries[i].name); - free((void*)entries[i].file); - if (entries[i].address.pattern) { - free((void*)entries[i].address.pattern); + if (name == "getAllTagsAsync") { + std::string path = arguments[0]->GetStringValue().ToString(); + CefRefPtr callback = arguments[1]; + CefRefPtr context = CefV8Context::GetCurrentContext(); + + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(queue, ^{ + tagFileInfo info; + tagFile* tagFile; + tagFile = tagsOpen(path.c_str(), &info); + std::vector entries; + + if (info.status.opened) { + tagEntry entry; + while (tagsNext(tagFile, &entry) == TagSuccess) { + entry.name = strdup(entry.name); + entry.file = strdup(entry.file); + if (entry.address.pattern) { + entry.address.pattern = strdup(entry.address.pattern); + } + entries.push_back(entry); } + tagsClose(tagFile); } - CefV8ValueList callbackArgs; - callbackArgs.push_back(v8Tags); - callback->ExecuteFunction(callback, callbackArgs); - context->Exit(); - }); - }); - return true; - } - return false; + dispatch_queue_t mainQueue = dispatch_get_main_queue(); + dispatch_async(mainQueue, ^{ + context->Enter(); + CefRefPtr v8Tags = CefV8Value::CreateArray(entries.size()); + for (int i = 0; i < entries.size(); i++) { + v8Tags->SetValue(i, ParseEntry(entries[i])); + free((void*)entries[i].name); + free((void*)entries[i].file); + if (entries[i].address.pattern) { + free((void*)entries[i].address.pattern); + } + } + CefV8ValueList callbackArgs; + callbackArgs.push_back(v8Tags); + callback->ExecuteFunction(callback, callbackArgs); + context->Exit(); + }); + }); + return true; + } + + return false; + } } } From 32d57892a2b136e0afe45596aa5169e531e474e5 Mon Sep 17 00:00:00 2001 From: probablycorey Date: Fri, 1 Mar 2013 16:58:31 -0800 Subject: [PATCH 014/308] Use correct resource path in dev mode --- native/atom_window_controller.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/atom_window_controller.mm b/native/atom_window_controller.mm index 1fdcc720b..cdf7355e3 100644 --- a/native/atom_window_controller.mm +++ b/native/atom_window_controller.mm @@ -34,7 +34,7 @@ _resourcePath = [atomApplication.arguments objectForKey:@"resource-path"]; if (!alwaysUseBundleResourcePath && !_resourcePath) { - NSString *defaultRepositoryPath = @"~/github/atom"; + NSString *defaultRepositoryPath = [@"~/github/atom" stringByStandardizingPath]; if ([defaultRepositoryPath characterAtIndex:0] == '/') { BOOL isDir = false; BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:defaultRepositoryPath isDirectory:&isDir]; From 7952dfc196347b2782d56988c92f06d0374326f6 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Sun, 3 Mar 2013 11:07:50 -0800 Subject: [PATCH 015/308] Add command after first argument This forces the object to be interpreted as the second argument to the callWorkerMethod function instead of as the first argument to function returned from callWorkerMethod. Closes #338 --- src/stdlib/task.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stdlib/task.coffee b/src/stdlib/task.coffee index 1210f4999..3177e50be 100644 --- a/src/stdlib/task.coffee +++ b/src/stdlib/task.coffee @@ -24,7 +24,7 @@ class Task error: -> console.error(arguments...) startWorker: -> - @callWorkerMethod 'start' + @callWorkerMethod 'start', globals: resourcePath: window.resourcePath navigator: From b0e7abac60eb2a679291484cff851dd8fdd76e0b Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Sun, 3 Mar 2013 11:54:07 -0800 Subject: [PATCH 016/308] Update image in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 11eee545e..1af29105f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Atom — Futuristic Text Editing -![atom](http://f.cl.ly/items/3h1L1O333p1d0W3D2K3r/atom-sketch.jpg) +![atom](https://f.cloud.github.com/assets/1300064/208230/4cefbca4-821a-11e2-8139-92c0328abf68.png) Check out our [documentation on the docs tab](https://github.com/github/atom/docs). From 6511d0e1112d188c1943362b2995b08839eac799 Mon Sep 17 00:00:00 2001 From: Adam Roben Date: Mon, 4 Mar 2013 11:21:29 -0500 Subject: [PATCH 017/308] Replace ## in package-generator templates with __ This is a workaround for . A future change will cause gyp to generate Makefiles to compile .coffee/.cson files to .js/.json. Makefiles use # as the comment character, and gyp isn't smart enough to escape the #. So now we don't use # in filenames to work around this bug. --- .../lib/package-generator-view.coffee | 2 +- ...kage-name##.cson => __package-name__.cson} | 2 +- .../template/lib/##package-name##.coffee | 13 ------------ ...ew.coffee => __package-name__-view.coffee} | 10 ++++----- .../template/lib/__package-name__.coffee | 13 ++++++++++++ .../package-generator/template/package.cson | 4 ++-- .../spec/##package-name##-spec.coffee | 5 ----- .../spec/##package-name##-view-spec.coffee | 21 ------------------- .../spec/__package-name__-spec.coffee | 5 +++++ .../spec/__package-name__-view-spec.coffee | 21 +++++++++++++++++++ .../template/stylesheets/##package-name##.css | 2 -- .../template/stylesheets/__package-name__.css | 2 ++ 12 files changed, 50 insertions(+), 50 deletions(-) rename src/packages/package-generator/template/keymaps/{##package-name##.cson => __package-name__.cson} (51%) delete mode 100644 src/packages/package-generator/template/lib/##package-name##.coffee rename src/packages/package-generator/template/lib/{##package-name##-view.coffee => __package-name__-view.coffee} (56%) create mode 100644 src/packages/package-generator/template/lib/__package-name__.coffee delete mode 100644 src/packages/package-generator/template/spec/##package-name##-spec.coffee delete mode 100644 src/packages/package-generator/template/spec/##package-name##-view-spec.coffee create mode 100644 src/packages/package-generator/template/spec/__package-name__-spec.coffee create mode 100644 src/packages/package-generator/template/spec/__package-name__-view-spec.coffee delete mode 100644 src/packages/package-generator/template/stylesheets/##package-name##.css create mode 100644 src/packages/package-generator/template/stylesheets/__package-name__.css diff --git a/src/packages/package-generator/lib/package-generator-view.coffee b/src/packages/package-generator/lib/package-generator-view.coffee index cc3f8100d..d1ca900cc 100644 --- a/src/packages/package-generator/lib/package-generator-view.coffee +++ b/src/packages/package-generator/lib/package-generator-view.coffee @@ -73,7 +73,7 @@ class PackageGeneratorView extends View fs.write(sourcePath, content) replacePackageNamePlaceholders: (string, packageName) -> - placeholderRegex = /##(?:(package-name)|([pP]ackageName)|(package_name))##/g + placeholderRegex = /__(?:(package-name)|([pP]ackageName)|(package_name))__/g string = string.replace placeholderRegex, (match, dash, camel, underscore) -> if dash _.dasherize(packageName) diff --git a/src/packages/package-generator/template/keymaps/##package-name##.cson b/src/packages/package-generator/template/keymaps/__package-name__.cson similarity index 51% rename from src/packages/package-generator/template/keymaps/##package-name##.cson rename to src/packages/package-generator/template/keymaps/__package-name__.cson index c85d84e8a..b3213a6a9 100644 --- a/src/packages/package-generator/template/keymaps/##package-name##.cson +++ b/src/packages/package-generator/template/keymaps/__package-name__.cson @@ -1,3 +1,3 @@ # DOCUMENT: link to keymap documentation 'body': - 'meta-alt-ctrl-o': '##package-name##:toggle' \ No newline at end of file + 'meta-alt-ctrl-o': '__package-name__:toggle' diff --git a/src/packages/package-generator/template/lib/##package-name##.coffee b/src/packages/package-generator/template/lib/##package-name##.coffee deleted file mode 100644 index 3f36f47f5..000000000 --- a/src/packages/package-generator/template/lib/##package-name##.coffee +++ /dev/null @@ -1,13 +0,0 @@ -##PackageName##View = require '##package-name##/lib/##package-name##-view' - -module.exports = - ##packageName##View: null - - activate: (state) -> - @##packageName##View = new ##PackageName##View(state.##packageName##ViewState) - - deactivate: -> - @##packageName##View.destroy() - - serialize: -> - ##packageName##ViewState: @##packageName##View.serialize() diff --git a/src/packages/package-generator/template/lib/##package-name##-view.coffee b/src/packages/package-generator/template/lib/__package-name__-view.coffee similarity index 56% rename from src/packages/package-generator/template/lib/##package-name##-view.coffee rename to src/packages/package-generator/template/lib/__package-name__-view.coffee index 681e3668f..215b8fdbf 100644 --- a/src/packages/package-generator/template/lib/##package-name##-view.coffee +++ b/src/packages/package-generator/template/lib/__package-name__-view.coffee @@ -1,13 +1,13 @@ {$$, View} = require 'space-pen' module.exports = -class ##PackageName##View extends View +class __PackageName__View extends View @content: -> - @div class: '##package-name## overlay from-top', => - @div "The ##PackageName## package is Alive! It's ALIVE!", class: "message" + @div class: '__package-name__ overlay from-top', => + @div "The __PackageName__ package is Alive! It's ALIVE!", class: "message" initialize: (serializeState) -> - rootView.command "##package-name##:toggle", => @toggle() + rootView.command "__package-name__:toggle", => @toggle() # Returns an object that can be retrieved when package is activated serialize: -> @@ -17,7 +17,7 @@ class ##PackageName##View extends View @detach() toggle: -> - console.log "##PackageName##View was toggled!" + console.log "__PackageName__View was toggled!" if @hasParent() @detach() else diff --git a/src/packages/package-generator/template/lib/__package-name__.coffee b/src/packages/package-generator/template/lib/__package-name__.coffee new file mode 100644 index 000000000..e619c5b84 --- /dev/null +++ b/src/packages/package-generator/template/lib/__package-name__.coffee @@ -0,0 +1,13 @@ +__PackageName__View = require '__package-name__/lib/__package-name__-view' + +module.exports = + __packageName__View: null + + activate: (state) -> + @__packageName__View = new __PackageName__View(state.__packageName__ViewState) + + deactivate: -> + @__packageName__View.destroy() + + serialize: -> + __packageName__ViewState: @__packageName__View.serialize() diff --git a/src/packages/package-generator/template/package.cson b/src/packages/package-generator/template/package.cson index 4f80472b5..7374ea007 100644 --- a/src/packages/package-generator/template/package.cson +++ b/src/packages/package-generator/template/package.cson @@ -1,2 +1,2 @@ -'main': 'lib/##package-name##' -'activationEvents': ['##package-name##:toggle'] \ No newline at end of file +'main': 'lib/__package-name__' +'activationEvents': ['__package-name__:toggle'] diff --git a/src/packages/package-generator/template/spec/##package-name##-spec.coffee b/src/packages/package-generator/template/spec/##package-name##-spec.coffee deleted file mode 100644 index afb95afa2..000000000 --- a/src/packages/package-generator/template/spec/##package-name##-spec.coffee +++ /dev/null @@ -1,5 +0,0 @@ -##PackageName## = require '##package-name##/lib/##package-name##' - -describe "##PackageName##", -> - it "has one valid test", -> - expect("life").toBe "easy" diff --git a/src/packages/package-generator/template/spec/##package-name##-view-spec.coffee b/src/packages/package-generator/template/spec/##package-name##-view-spec.coffee deleted file mode 100644 index e1e4cbf40..000000000 --- a/src/packages/package-generator/template/spec/##package-name##-view-spec.coffee +++ /dev/null @@ -1,21 +0,0 @@ -##PackageName##View = require '##package-name##/lib/##package-name##-view' -RootView = require 'root-view' - -# This spec is focused because it starts with an `f`. Remove the `f` -# to unfocus the spec. -# -# Press meta-alt-ctrl-s to run the specs -fdescribe "##PackageName##View", -> - ##packageName## = null - - beforeEach -> - window.rootView = new RootView - ##packageName## = window.loadPackage('##packageName##', activateImmediately: true) - - describe "when the ##package-name##:toggle event is triggered", -> - it "attaches and then detaches the view", -> - expect(rootView.find('.##package-name##')).not.toExist() - rootView.trigger '##package-name##:toggle' - expect(rootView.find('.##package-name##')).toExist() - rootView.trigger '##package-name##:toggle' - expect(rootView.find('.##package-name##')).not.toExist() diff --git a/src/packages/package-generator/template/spec/__package-name__-spec.coffee b/src/packages/package-generator/template/spec/__package-name__-spec.coffee new file mode 100644 index 000000000..3bc7b1091 --- /dev/null +++ b/src/packages/package-generator/template/spec/__package-name__-spec.coffee @@ -0,0 +1,5 @@ +__PackageName__ = require '__package-name__/lib/__package-name__' + +describe "__PackageName__", -> + it "has one valid test", -> + expect("life").toBe "easy" diff --git a/src/packages/package-generator/template/spec/__package-name__-view-spec.coffee b/src/packages/package-generator/template/spec/__package-name__-view-spec.coffee new file mode 100644 index 000000000..a8b62caf7 --- /dev/null +++ b/src/packages/package-generator/template/spec/__package-name__-view-spec.coffee @@ -0,0 +1,21 @@ +__PackageName__View = require '__package-name__/lib/__package-name__-view' +RootView = require 'root-view' + +# This spec is focused because it starts with an `f`. Remove the `f` +# to unfocus the spec. +# +# Press meta-alt-ctrl-s to run the specs +fdescribe "__PackageName__View", -> + __packageName__ = null + + beforeEach -> + window.rootView = new RootView + __packageName__ = window.loadPackage('__packageName__', activateImmediately: true) + + describe "when the __package-name__:toggle event is triggered", -> + it "attaches and then detaches the view", -> + expect(rootView.find('.__package-name__')).not.toExist() + rootView.trigger '__package-name__:toggle' + expect(rootView.find('.__package-name__')).toExist() + rootView.trigger '__package-name__:toggle' + expect(rootView.find('.__package-name__')).not.toExist() diff --git a/src/packages/package-generator/template/stylesheets/##package-name##.css b/src/packages/package-generator/template/stylesheets/##package-name##.css deleted file mode 100644 index 4562c6786..000000000 --- a/src/packages/package-generator/template/stylesheets/##package-name##.css +++ /dev/null @@ -1,2 +0,0 @@ -.##package-name## { -} \ No newline at end of file diff --git a/src/packages/package-generator/template/stylesheets/__package-name__.css b/src/packages/package-generator/template/stylesheets/__package-name__.css new file mode 100644 index 000000000..f6fe86ba8 --- /dev/null +++ b/src/packages/package-generator/template/stylesheets/__package-name__.css @@ -0,0 +1,2 @@ +.__package-name__ { +} From 83ee2d23b3f612c30fc15fbd8a8690b789a3c93e Mon Sep 17 00:00:00 2001 From: Adam Roben Date: Mon, 4 Mar 2013 11:32:06 -0500 Subject: [PATCH 018/308] Use gyp's rules functionality to compile .coffee/.cson files Instead of finding and compiling all .coffee/.cson files in script/copy-files-to-bundle, we now tell gyp how to do this for us. It works like this: 1. Rakefile invokes the new script/generate-sources-gypi script to generate sources.gypi. This file lists all the .coffee/.cson files in the src, static, and vendor directories, as well as a new compiled_sources_dir variable that specifies where the compiled versions of the files should be placed. 2. atom.gyp includes sources.gypi. 3. atom.gyp has a new target, generated_sources, which contains all the .coffee/.cson files, and uses two rules to tell gyp how to compile them. The rules invoke the new script/compile-coffee and script/compile-cson files once for each file. 4. gyp generates one Makefile for each rule to actually perform the compilation. 5. script/copy-files-to-bundle now takes the compiled_sources_dir variable as an argument, and copies files both from there and from the repository into the Resources directory. By putting the compilation into a different target, we can do it in parallel with compiling/linking our binaries. And gyp automatically runs make using -j$(sysctl -n hw.ncpu), so compilation of .coffee/.cson files happens in parallel, too. These changes reduce clean build time on my MacBook Pro from 55 seconds to 46 seconds. --- .gitignore | 1 + Rakefile | 1 + atom.gyp | 49 ++++++++++++++++++++++++++++++++++-- script/compile-coffee | 16 ++++++++++++ script/compile-cson | 16 ++++++++++++ script/copy-files-to-bundle | 37 +++------------------------ script/generate-sources-gypi | 32 +++++++++++++++++++++++ 7 files changed, 116 insertions(+), 36 deletions(-) create mode 100755 script/compile-coffee create mode 100755 script/compile-cson create mode 100755 script/generate-sources-gypi diff --git a/.gitignore b/.gitignore index 537481ed0..678779b26 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ node_modules npm-debug.log /tags /cef/ +/sources.gypi diff --git a/Rakefile b/Rakefile index aecb9e2ad..f2b56654f 100644 --- a/Rakefile +++ b/Rakefile @@ -14,6 +14,7 @@ end desc "Create xcode project from gyp file" task "create-xcode-project" => "update-cef" do `rm -rf atom.xcodeproj` + `script/generate-sources-gypi` `gyp --depth=. -D CODE_SIGN="#{ENV['CODE_SIGN']}" atom.gyp` end diff --git a/atom.gyp b/atom.gyp index d96036267..1c8f40997 100644 --- a/atom.gyp +++ b/atom.gyp @@ -36,6 +36,7 @@ 'includes': [ 'cef/cef_paths2.gypi', 'git2/libgit2.gypi', + 'sources.gypi', ], 'target_defaults': { 'default_configuration': 'Debug', @@ -214,6 +215,7 @@ 'type': 'shared_library', 'mac_bundle': 1, 'dependencies': [ + 'generated_sources', 'libcef_dll_wrapper', ], 'defines': [ @@ -276,9 +278,10 @@ ], 'postbuilds': [ { - 'postbuild_name': 'Copy and Compile Static Files', + 'postbuild_name': 'Copy Static Files', 'action': [ - 'script/copy-files-to-bundle' + 'script/copy-files-to-bundle', + '<(compiled_sources_dir_xcode)', ], }, { @@ -311,6 +314,48 @@ ], } }, + { + 'target_name': 'generated_sources', + 'type': 'none', + 'sources': [ + '<@(coffee_sources)', + '<@(cson_sources)', + ], + 'rules': [ + { + 'rule_name': 'coffee', + 'extension': 'coffee', + 'inputs': [ + 'script/compile-coffee', + ], + 'outputs': [ + '<(compiled_sources_dir)/<(RULE_INPUT_DIRNAME)/<(RULE_INPUT_ROOT).js', + ], + 'action': [ + 'sh', + 'script/compile-coffee', + '<(RULE_INPUT_PATH)', + '<(compiled_sources_dir)/<(RULE_INPUT_DIRNAME)/<(RULE_INPUT_ROOT).js', + ], + }, + { + 'rule_name': 'cson2json', + 'extension': 'cson', + 'inputs': [ + 'script/compile-cson', + ], + 'outputs': [ + '<(compiled_sources_dir)/<(RULE_INPUT_DIRNAME)/<(RULE_INPUT_ROOT).json', + ], + 'action': [ + 'sh', + 'script/compile-cson', + '<(RULE_INPUT_PATH)', + '<(compiled_sources_dir)/<(RULE_INPUT_DIRNAME)/<(RULE_INPUT_ROOT).json', + ], + }, + ], + }, ], 'conditions': [ ['os_posix==1 and OS!="mac" and OS!="android" and gcc_version==46', { diff --git a/script/compile-coffee b/script/compile-coffee new file mode 100755 index 000000000..d4a266265 --- /dev/null +++ b/script/compile-coffee @@ -0,0 +1,16 @@ +#!/bin/sh + +set -e + +# Because of the way xcodebuild invokes external scripts we need to load +# 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. + +if [ -e /opt/github/env.sh ]; then + source /opt/github/env.sh +fi + +INPUT_FILE="${1}" +OUTPUT_FILE="${2}" + +node_modules/.bin/coffee -c -p "${INPUT_FILE}" > "${OUTPUT_FILE}" diff --git a/script/compile-cson b/script/compile-cson new file mode 100755 index 000000000..4f4211087 --- /dev/null +++ b/script/compile-cson @@ -0,0 +1,16 @@ +#!/bin/sh + +set -e + +# Because of the way xcodebuild invokes external scripts we need to load +# 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. + +if [ -e /opt/github/env.sh ]; then + source /opt/github/env.sh +fi + +INPUT_FILE="${1}" +OUTPUT_FILE="${2}" + +node_modules/.bin/cson2json "${INPUT_FILE}" > "${OUTPUT_FILE}" diff --git a/script/copy-files-to-bundle b/script/copy-files-to-bundle index 67dfed4c4..c4eed5d02 100755 --- a/script/copy-files-to-bundle +++ b/script/copy-files-to-bundle @@ -1,41 +1,10 @@ #!/bin/sh # This can only be run by xcode or xcodebuild! -# Because of the way xcodebuild invokes external scripts we need to load -# 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. - -if [ -f /opt/github/env.sh ]; then - source /opt/github/env.sh -fi +set -e +COMPILED_SOURCES_DIR="${1}" RESOUCES_PATH="$BUILT_PRODUCTS_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH" -DIRS="src static vendor" - -# Compile .coffee files into bundle -COFFEE_FILES=$(find $DIRS -type file -name '*.coffee') -for COFFEE_FILE in $COFFEE_FILES; do - JS_FILE=$(echo "$RESOUCES_PATH/$COFFEE_FILE" | sed 's/.coffee/.js/' ) - OUTPUT_PATH="$RESOUCES_PATH/$(dirname "$COFFEE_FILE")" - - if [ $COFFEE_FILE -nt "$JS_FILE" ]; then - mkdir -p "$OUTPUT_PATH" - node_modules/.bin/coffee -c -o "$OUTPUT_PATH" "$COFFEE_FILE" || exit 1 - fi -done; - -# Compile .cson files into bundle -CSON_FILES=$(find $DIRS -type file -name '*.cson') -for CSON_FILE in $CSON_FILES; do - JSON_FILE=$(echo "$RESOUCES_PATH/$CSON_FILE" | sed 's/.cson/.json/' ) - OUTPUT_PATH="$RESOUCES_PATH/$(dirname "$CSON_FILE")" - - if [ $CSON_FILE -nt "$JSON_FILE" ]; then - mkdir -p "$OUTPUT_PATH" - node_modules/.bin/cson2json "$CSON_FILE" > "$JSON_FILE" - fi -done; - # Copy non-coffee files into bundle -rsync --archive --recursive --exclude="src/**/*.coffee" --exclude="src/**/*.cson" src static vendor spec benchmark themes dot-atom atom.sh "$RESOUCES_PATH" +rsync --archive --recursive --exclude="src/**/*.coffee" --exclude="src/**/*.cson" src static vendor spec benchmark themes dot-atom atom.sh "${COMPILED_SOURCES_DIR}/" "$RESOUCES_PATH" diff --git a/script/generate-sources-gypi b/script/generate-sources-gypi new file mode 100755 index 000000000..266fde180 --- /dev/null +++ b/script/generate-sources-gypi @@ -0,0 +1,32 @@ +#!/bin/sh + +set -e + +cd "$(dirname $0)/.." + +DIRS="src static vendor" + +find_files() { + find ${DIRS} -type file -name ${1} +} + +file_list() { + while read file; do + echo " '${file}'," + done +} + +cat > sources.gypi < Date: Mon, 4 Mar 2013 11:52:03 -0500 Subject: [PATCH 019/308] Only source /opt/github/env.sh if needed If node is already in PATH and functional, we don't need to spend a bunch of time running all of Boxen's setup scripts. This reduces clean build time on my MacBook Pro from 46 seconds to 31 seconds. --- script/compile-coffee | 4 +--- script/compile-cson | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/script/compile-coffee b/script/compile-coffee index d4a266265..298d9f53c 100755 --- a/script/compile-coffee +++ b/script/compile-coffee @@ -6,9 +6,7 @@ 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. -if [ -e /opt/github/env.sh ]; then - source /opt/github/env.sh -fi +node --version > /dev/null 2>&1 || source /opt/github/env.sh INPUT_FILE="${1}" OUTPUT_FILE="${2}" diff --git a/script/compile-cson b/script/compile-cson index 4f4211087..7b4d4c430 100755 --- a/script/compile-cson +++ b/script/compile-cson @@ -6,9 +6,7 @@ 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. -if [ -e /opt/github/env.sh ]; then - source /opt/github/env.sh -fi +node --version > /dev/null 2>&1 || source /opt/github/env.sh INPUT_FILE="${1}" OUTPUT_FILE="${2}" From fd2d1f2dfccc20beba0793ed21b4421c2d5ce2cf Mon Sep 17 00:00:00 2001 From: probablycorey Date: Mon, 4 Mar 2013 09:42:54 -0800 Subject: [PATCH 020/308] Peg CoffeeScript to 1.5.x --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1d8e2a164..9d0c4f864 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version" : "0.0.0", "dependencies": { - "coffee-script": "1.x", + "coffee-script": "1.5", "cson": "1.x" }, From 04ec1b01be9a00c6652c51109c49e2468fbcf216 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Mon, 4 Mar 2013 14:45:24 -0600 Subject: [PATCH 021/308] Fallback to /usr/local/bin/atom --- Rakefile | 6 +++++- src/app/window.coffee | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Rakefile b/Rakefile index f2b56654f..3dd8b43fb 100644 --- a/Rakefile +++ b/Rakefile @@ -48,8 +48,12 @@ task :install => [:clean, :build] do # Install atom cli if File.directory?("/opt/boxen") cli_path = "/opt/boxen/bin/atom" - else + elsif File.directory?("/opt/github") cli_path = "/opt/github/bin/atom" + elsif File.directory?("/usr/local") + cli_path = "/usr/local/bin/atom" + else + raise "Missing directory for `atom` binary" end FileUtils.cp("#{ATOM_SRC_PATH}/atom.sh", cli_path) diff --git a/src/app/window.coffee b/src/app/window.coffee index d814ba22c..30745a8a2 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -42,8 +42,12 @@ window.setUpEnvironment = -> window.startup = -> if fs.isDirectory('/opt/boxen') installAtomCommand('/opt/boxen/bin/atom') - else + else if fs.isDirectory('/opt/github') installAtomCommand('/opt/github/bin/atom') + else if fs.isDirectory('/usr/local') + installAtomCommand('/usr/local/bin/atom') + else + throw "Missing directory for `atom` binary" handleWindowEvents() config.load() From 0fe570fc476b2ac1ea429f65007c04cddd96d384 Mon Sep 17 00:00:00 2001 From: probablycorey Date: Mon, 4 Mar 2013 15:44:49 -0800 Subject: [PATCH 022/308] Log warning instead of throwing error when installing atom binary --- src/app/window.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/window.coffee b/src/app/window.coffee index 30745a8a2..29a87d712 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -47,7 +47,7 @@ window.startup = -> else if fs.isDirectory('/usr/local') installAtomCommand('/usr/local/bin/atom') else - throw "Missing directory for `atom` binary" + console.warn "Failed to install `atom` binary" handleWindowEvents() config.load() From 30d4cb81b8ac6a8d6deb78e23e8ce84e94a8c1b1 Mon Sep 17 00:00:00 2001 From: probablycorey Date: Mon, 4 Mar 2013 16:40:02 -0800 Subject: [PATCH 023/308] Use Courier as the default font for specs Fixes #323 --- spec/app/editor-spec.coffee | 8 ++++---- spec/spec-helper.coffee | 1 + src/packages/wrap-guide/spec/wrap-guide-spec.coffee | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index f7a553cab..1f80bed79 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -558,7 +558,7 @@ describe "Editor", -> lineHeightBefore = editor.lineHeight charWidthBefore = editor.charWidth - config.set("editor.fontFamily", "Courier") + config.set("editor.fontFamily", "Consolas") editor.setCursorScreenPosition [5, 6] expect(editor.charWidth).not.toBe charWidthBefore expect(editor.getCursorView().position()).toEqual { top: 5 * editor.lineHeight, left: 6 * editor.charWidth } @@ -618,9 +618,9 @@ describe "Editor", -> it "updates the gutter width and font size", -> rootView.attachToDom() - config.set("editor.fontSize", 16 * 4) - expect(editor.gutter.css('font-size')).toBe "#{16 * 4}px" - expect(editor.gutter.width()).toBe(64 + editor.gutter.calculateLineNumberPadding()) + config.set("editor.fontSize", 20) + expect(editor.gutter.css('font-size')).toBe "20px" + expect(editor.gutter.width()).toBe(editor.charWidth * 2 + editor.gutter.calculateLineNumberPadding()) it "updates lines if there are unrendered lines", -> editor.attachToDom(heightInLines: 5) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index c750bf498..7e197d483 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -43,6 +43,7 @@ beforeEach -> window.config = new Config() spyOn(config, 'load') spyOn(config, 'save') + config.set "editor.fontFamily", "Courier" config.set "editor.fontSize", 16 config.set "editor.autoIndent", false config.set "core.disabledPackages", ["package-that-throws-an-exception"] diff --git a/src/packages/wrap-guide/spec/wrap-guide-spec.coffee b/src/packages/wrap-guide/spec/wrap-guide-spec.coffee index 706032321..391fe54ae 100644 --- a/src/packages/wrap-guide/spec/wrap-guide-spec.coffee +++ b/src/packages/wrap-guide/spec/wrap-guide-spec.coffee @@ -31,7 +31,8 @@ describe "WrapGuide", -> it "updates the wrap guide position", -> initial = wrapGuide.position().left expect(initial).toBeGreaterThan(0) - rootView.trigger('window:increase-font-size') + fontSize = config.get("editor.fontSize") + config.set("editor.fontSize", fontSize * 2) expect(wrapGuide.position().left).toBeGreaterThan(initial) expect(wrapGuide).toBeVisible() From 41e1ce4a09f045dee7382e2f5582087520893627 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Mon, 4 Mar 2013 18:27:51 -0800 Subject: [PATCH 024/308] Remove cson2json dependency The latest release does not work because of CoffeeScript changes so use a simple compile script to convert CSON files to JSON files when buildilng. Closes #348 --- package.json | 3 +-- script/compile-cson | 2 +- script/compile-cson.coffee | 23 +++++++++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 script/compile-cson.coffee diff --git a/package.json b/package.json index 9d0c4f864..1893b5e86 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,7 @@ "version" : "0.0.0", "dependencies": { - "coffee-script": "1.5", - "cson": "1.x" + "coffee-script": "1.5" }, "scripts": { diff --git a/script/compile-cson b/script/compile-cson index 7b4d4c430..bc3a79ff3 100755 --- a/script/compile-cson +++ b/script/compile-cson @@ -11,4 +11,4 @@ node --version > /dev/null 2>&1 || source /opt/github/env.sh INPUT_FILE="${1}" OUTPUT_FILE="${2}" -node_modules/.bin/cson2json "${INPUT_FILE}" > "${OUTPUT_FILE}" +node_modules/.bin/coffee script/compile-cson.coffee -- "${INPUT_FILE}" "${OUTPUT_FILE}" diff --git a/script/compile-cson.coffee b/script/compile-cson.coffee new file mode 100644 index 000000000..42d35c93d --- /dev/null +++ b/script/compile-cson.coffee @@ -0,0 +1,23 @@ +fs = require 'fs' +{exec} = require 'child_process' + +inputFile = process.argv[2] +unless inputFile?.length > 0 + console.error("Input file must be first argument") + process.exit(1) + +outputFile = process.argv[3] +unless outputFile?.length > 0 + console.error("Output file must be second arguments") + process.exit(1) + +contents = fs.readFileSync(inputFile)?.toString() ? '' +exec "node_modules/.bin/coffee -bcp #{inputFile}", (error, stdout, stderr) -> + if error + console.error(error) + process.exit(1) + json = eval(stdout.toString()) ? {} + if json isnt Object(json) + console.error("CSON file does not contain valid JSON") + process.exit(1) + fs.writeFileSync(outputFile, JSON.stringify(json, null, 2)) From 69f79b940bfd04be6dc1d52a97942d50dd634215 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 4 Mar 2013 20:05:42 -0800 Subject: [PATCH 025/308] Update nof task for new package generator naming scheme --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 3dd8b43fb..704999599 100644 --- a/Rakefile +++ b/Rakefile @@ -112,7 +112,7 @@ task :benchmark do end task :nof do - system %{find . -name *spec.coffee | grep --invert-match --regexp "#{BUILD_DIR}\\|##package-name##" | xargs sed -E -i "" "s/f+(it|describe) +(['\\"])/\\1 \\2/g"} + system %{find . -name *spec.coffee | grep --invert-match --regexp "#{BUILD_DIR}\\|__package-name__" | xargs sed -E -i "" "s/f+(it|describe) +(['\\"])/\\1 \\2/g"} end task :tags do From 4add7b62136131852799fca79c365338ac81021b Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 27 Feb 2013 13:07:57 -0800 Subject: [PATCH 026/308] Support getting status of entire repository New Git.getAllStatuses() method returns all non-ignored status entries in the repository. --- native/v8_extensions/git.mm | 27 ++++++++++++++++++++++++++- spec/app/git-spec.coffee | 22 ++++++++++++++++++++++ src/app/git.coffee | 3 +++ src/stdlib/git-repository.coffee | 1 + 4 files changed, 52 insertions(+), 1 deletion(-) diff --git a/native/v8_extensions/git.mm b/native/v8_extensions/git.mm index 4c7bbb056..3a8c13885 100644 --- a/native/v8_extensions/git.mm +++ b/native/v8_extensions/git.mm @@ -8,6 +8,14 @@ namespace v8_extensions { private: git_repository *repo; + static int CollectStatus(const char *path, unsigned int status, void *payload) { + if ((status & GIT_STATUS_IGNORED) == 0) { + std::map *statuses = (std::map *) payload; + statuses->insert(std::pair(path, status)); + } + return 0; + } + public: GitRepository(const char *pathInRepo) { if (git_repository_open_ext(&repo, pathInRepo, 0, NULL) != GIT_OK) { @@ -55,6 +63,17 @@ namespace v8_extensions { return CefV8Value::CreateNull(); } + CefRefPtr GetStatuses() { + std::map statuses; + git_status_foreach(repo, CollectStatus, &statuses); + std::map::iterator iter = statuses.begin(); + CefRefPtr v8Statuses = CefV8Value::CreateObject(NULL); + for (; iter != statuses.end(); ++iter) { + v8Statuses->SetValue(iter->first, CefV8Value::CreateInt(iter->second), V8_PROPERTY_ATTRIBUTE_NONE); + } + return v8Statuses; + } + CefRefPtr IsIgnored(const char *path) { int ignored; if (git_ignore_path_is_ignored(&ignored, repo, path) == GIT_OK) { @@ -190,7 +209,7 @@ namespace v8_extensions { void Git::CreateContextBinding(CefRefPtr context) { const char* methodNames[] = { "getRepository", "getHead", "getPath", "isIgnored", "getStatus", "checkoutHead", - "getDiffStats", "isSubmodule", "refreshIndex", "destroy" + "getDiffStats", "isSubmodule", "refreshIndex", "destroy", "getStatuses" }; CefRefPtr nativeObject = CefV8Value::CreateObject(NULL); @@ -277,6 +296,12 @@ namespace v8_extensions { return true; } + if (name == "getStatuses") { + GitRepository *userData = (GitRepository *)object->GetUserData().get(); + retval = userData->GetStatuses(); + return true; + } + return false; } } diff --git a/spec/app/git-spec.coffee b/spec/app/git-spec.coffee index 522d38d8e..892d3cbcd 100644 --- a/spec/app/git-spec.coffee +++ b/spec/app/git-spec.coffee @@ -147,3 +147,25 @@ describe "Git", -> expect(repo.getDiffStats(path)).toEqual {added: 0, deleted: 0} fs.write(path, "#{originalPathText} edited line") expect(repo.getDiffStats(path)).toEqual {added: 1, deleted: 1} + + describe ".getStatuses()", -> + [newPath, modifiedPath, cleanPath, originalModifiedPathText] = [] + + beforeEach -> + repo = new Git(require.resolve('fixtures/git/working-dir')) + modifiedPath = fixturesProject.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') + fs.write(newPath, '') + + afterEach -> + fs.write(modifiedPath, originalModifiedPathText) + fs.remove(newPath) if fs.exists(newPath) + + it "returns status information for all new and modified files", -> + fs.write(modifiedPath, 'making this path modified') + statuses = repo.getAllStatuses() + expect(statuses[cleanPath]).toBeUndefined() + expect(repo.isStatusNew(statuses[repo.relativize(newPath)])).toBeTruthy() + expect(repo.isStatusModified(statuses[repo.relativize(modifiedPath)])).toBeTruthy() diff --git a/src/app/git.coffee b/src/app/git.coffee index 1c9fffc0a..292d4fc69 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -54,6 +54,9 @@ class Git getPathStatus: (path) -> pathStatus = @getRepo().getStatus(@relativize(path)) + getAllStatuses: (path) -> + @getRepo().getStatuses() + isPathIgnored: (path) -> @getRepo().isIgnored(@relativize(path)) diff --git a/src/stdlib/git-repository.coffee b/src/stdlib/git-repository.coffee index 7a52403b4..2386ba868 100644 --- a/src/stdlib/git-repository.coffee +++ b/src/stdlib/git-repository.coffee @@ -10,6 +10,7 @@ class GitRepository getHead: $git.getHead getPath: $git.getPath getStatus: $git.getStatus + getStatuses: $git.getStatuses isIgnored: $git.isIgnored checkoutHead: $git.checkoutHead getDiffStats: $git.getDiffStats From 50bc1aac74d212e2909e4927211e5dd3e3c58ca9 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 27 Feb 2013 14:28:30 -0800 Subject: [PATCH 027/308] Add task to refresh status of repository By default this will occur when the window gains focus and the Git class can now be subscribed to so listeners can become notified when the status of a repository changes. --- spec/app/git-spec.coffee | 18 +++++++++++++----- src/app/git.coffee | 19 +++++++++++++------ src/app/repository-status-handler.coffee | 16 ++++++++++++++++ src/app/repository-status-task.coffee | 17 +++++++++++++++++ .../lib/load-paths-handler.coffee | 2 +- 5 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 src/app/repository-status-handler.coffee create mode 100644 src/app/repository-status-task.coffee diff --git a/spec/app/git-spec.coffee b/spec/app/git-spec.coffee index 892d3cbcd..c618f2ead 100644 --- a/spec/app/git-spec.coffee +++ b/spec/app/git-spec.coffee @@ -148,7 +148,7 @@ describe "Git", -> fs.write(path, "#{originalPathText} edited line") expect(repo.getDiffStats(path)).toEqual {added: 1, deleted: 1} - describe ".getStatuses()", -> + describe ".refreshStatuses()", -> [newPath, modifiedPath, cleanPath, originalModifiedPathText] = [] beforeEach -> @@ -165,7 +165,15 @@ describe "Git", -> it "returns status information for all new and modified files", -> fs.write(modifiedPath, 'making this path modified') - statuses = repo.getAllStatuses() - expect(statuses[cleanPath]).toBeUndefined() - expect(repo.isStatusNew(statuses[repo.relativize(newPath)])).toBeTruthy() - expect(repo.isStatusModified(statuses[repo.relativize(modifiedPath)])).toBeTruthy() + statusHandler = jasmine.createSpy('statusHandler') + repo.on 'statuses-changed', statusHandler + repo.refreshStatuses() + + waitsFor -> + statusHandler.callCount > 0 + + runs -> + statuses = repo.statuses + expect(statuses[cleanPath]).toBeUndefined() + expect(repo.isStatusNew(statuses[newPath])).toBeTruthy() + expect(repo.isStatusModified(statuses[modifiedPath])).toBeTruthy() diff --git a/src/app/git.coffee b/src/app/git.coffee index 292d4fc69..c3d9cf2b2 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -1,7 +1,9 @@ _ = require 'underscore' fs = require 'fs' Subscriber = require 'subscriber' +EventEmitter = require 'event-emitter' GitRepository = require 'git-repository' +RepositoryStatusTask = require 'repository-status-task' module.exports = class Git @@ -23,12 +25,16 @@ class Git working_dir_typechange: 1 << 10 ignore: 1 << 14 + statuses: {} + constructor: (path, options={}) -> @repo = GitRepository.open(path) - refreshIndexOnFocus = options.refreshIndexOnFocus ? true - if refreshIndexOnFocus + refreshOnWindowFocus = options.refreshOnWindowFocus ? true + if refreshOnWindowFocus $ = require 'jquery' - @subscribe $(window), 'focus', => @refreshIndex() + @subscribe $(window), 'focus', => + @refreshIndex() + @refreshStatuses() getRepo: -> unless @repo? @@ -54,9 +60,6 @@ class Git getPathStatus: (path) -> pathStatus = @getRepo().getStatus(@relativize(path)) - getAllStatuses: (path) -> - @getRepo().getStatuses() - isPathIgnored: (path) -> @getRepo().isIgnored(@relativize(path)) @@ -104,4 +107,8 @@ class Git isSubmodule: (path) -> @getRepo().isSubmodule(@relativize(path)) + refreshStatuses: -> + new RepositoryStatusTask(this).start() + _.extend Git.prototype, Subscriber +_.extend Git.prototype, EventEmitter diff --git a/src/app/repository-status-handler.coffee b/src/app/repository-status-handler.coffee new file mode 100644 index 000000000..bd2d2fe70 --- /dev/null +++ b/src/app/repository-status-handler.coffee @@ -0,0 +1,16 @@ +Git = require 'git' +fs = require 'fs' + +module.exports = + loadStatuses: (path) -> + repo = Git.open(path) + if repo? + workingDirectoryPath = repo.getWorkingDirectory() + statuses = {} + for path, status of repo.getRepo().getStatuses() + statuses[fs.join(workingDirectoryPath, path)] = status + repo.destroy() + else + statuses = {} + + callTaskMethod('statusesLoaded', statuses) diff --git a/src/app/repository-status-task.coffee b/src/app/repository-status-task.coffee new file mode 100644 index 000000000..f4abbd0c9 --- /dev/null +++ b/src/app/repository-status-task.coffee @@ -0,0 +1,17 @@ +Task = require 'task' +_ = require 'underscore' + +module.exports = +class RepositoryStatusTask extends Task + + constructor: (@repo) -> + super('repository-status-handler') + + started: -> + @callWorkerMethod('loadStatuses', @repo.getPath()) + + statusesLoaded: (statuses) -> + @done() + unless _.isEqual(statuses, @repo.statuses) + @repo.statuses = statuses + @repo.trigger 'statuses-changed' diff --git a/src/packages/fuzzy-finder/lib/load-paths-handler.coffee b/src/packages/fuzzy-finder/lib/load-paths-handler.coffee index 723f302d1..9ef442c99 100644 --- a/src/packages/fuzzy-finder/lib/load-paths-handler.coffee +++ b/src/packages/fuzzy-finder/lib/load-paths-handler.coffee @@ -5,7 +5,7 @@ module.exports = loadPaths: (rootPath, ignoredNames, excludeGitIgnoredPaths) -> if excludeGitIgnoredPaths Git = require 'git' - repo = Git.open(rootPath, refreshIndexOnFocus: false) + repo = Git.open(rootPath, refreshOnWindowFocus: false) paths = [] isIgnored = (path) -> From c6e89d33f894d8e45dfc3ff8a30e5c3c28c90c87 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 27 Feb 2013 14:48:41 -0800 Subject: [PATCH 028/308] Git.refreshStatuses() -> Git.refreshStatus() --- spec/app/git-spec.coffee | 4 ++-- src/app/git.coffee | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/app/git-spec.coffee b/spec/app/git-spec.coffee index c618f2ead..2a9522d3a 100644 --- a/spec/app/git-spec.coffee +++ b/spec/app/git-spec.coffee @@ -148,7 +148,7 @@ describe "Git", -> fs.write(path, "#{originalPathText} edited line") expect(repo.getDiffStats(path)).toEqual {added: 1, deleted: 1} - describe ".refreshStatuses()", -> + describe ".refreshStatus()", -> [newPath, modifiedPath, cleanPath, originalModifiedPathText] = [] beforeEach -> @@ -167,7 +167,7 @@ describe "Git", -> fs.write(modifiedPath, 'making this path modified') statusHandler = jasmine.createSpy('statusHandler') repo.on 'statuses-changed', statusHandler - repo.refreshStatuses() + repo.refreshStatus() waitsFor -> statusHandler.callCount > 0 diff --git a/src/app/git.coffee b/src/app/git.coffee index c3d9cf2b2..a73fe41b4 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -34,7 +34,7 @@ class Git $ = require 'jquery' @subscribe $(window), 'focus', => @refreshIndex() - @refreshStatuses() + @refreshStatus() getRepo: -> unless @repo? @@ -107,7 +107,7 @@ class Git isSubmodule: (path) -> @getRepo().isSubmodule(@relativize(path)) - refreshStatuses: -> + refreshStatus: -> new RepositoryStatusTask(this).start() _.extend Git.prototype, Subscriber From 083562e1aa11af499b18f92264835464350a33ff Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 27 Feb 2013 14:52:16 -0800 Subject: [PATCH 029/308] Abort status task when destroyed --- src/app/git.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/git.coffee b/src/app/git.coffee index a73fe41b4..5284b7e5a 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -47,6 +47,7 @@ class Git @path ?= fs.absolute(@getRepo().getPath()) destroy: -> + @statusTask?.abort() @getRepo().destroy() @repo = null @unsubscribe() @@ -108,7 +109,7 @@ class Git @getRepo().isSubmodule(@relativize(path)) refreshStatus: -> - new RepositoryStatusTask(this).start() + @statusTask = new RepositoryStatusTask(this).start() _.extend Git.prototype, Subscriber _.extend Git.prototype, EventEmitter From 68b61d71c6e66944bbcdc398654b6690b0a10ae2 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 27 Feb 2013 15:36:08 -0800 Subject: [PATCH 030/308] Trigger event when path status changes --- spec/app/git-spec.coffee | 26 ++++++++++++++++++++++++++ src/app/git.coffee | 5 +++++ 2 files changed, 31 insertions(+) diff --git a/spec/app/git-spec.coffee b/spec/app/git-spec.coffee index 2a9522d3a..2697e9723 100644 --- a/spec/app/git-spec.coffee +++ b/spec/app/git-spec.coffee @@ -148,6 +148,32 @@ describe "Git", -> fs.write(path, "#{originalPathText} edited line") expect(repo.getDiffStats(path)).toEqual {added: 1, deleted: 1} + describe ".getPathStatus(path)", -> + [path, originalPathText] = [] + + beforeEach -> + repo = new Git(require.resolve('fixtures/git/working-dir')) + path = require.resolve('fixtures/git/working-dir/file.txt') + originalPathText = fs.read(path) + + afterEach -> + fs.write(path, originalPathText) + + it "trigger a status-changed event when the new status differs from the last cached one", -> + statusHandler = jasmine.createSpy("statusHandler") + repo.on 'status-changed', statusHandler + status = repo.getPathStatus(path) + expect(statusHandler.callCount).toBe 1 + expect(statusHandler.argsForCall[0][0..1]).toEqual [path, status] + + fs.write(path, '') + status = repo.getPathStatus(path) + expect(statusHandler.callCount).toBe 2 + expect(statusHandler.argsForCall[1][0..1]).toEqual [path, status] + + repo.getPathStatus(path) + expect(statusHandler.callCount).toBe 2 + describe ".refreshStatus()", -> [newPath, modifiedPath, cleanPath, originalModifiedPathText] = [] diff --git a/src/app/git.coffee b/src/app/git.coffee index 5284b7e5a..8e24a4b97 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -59,7 +59,12 @@ class Git @getRepo().getHead() ? '' getPathStatus: (path) -> + currentPathStatus = @statuses[path] pathStatus = @getRepo().getStatus(@relativize(path)) + @statuses[path] = pathStatus + if currentPathStatus isnt pathStatus + @trigger 'status-changed', path, pathStatus + pathStatus isPathIgnored: (path) -> @getRepo().isIgnored(@relativize(path)) From 910be149abd39c6b3f7af650ef8b0850a0182e81 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 27 Feb 2013 16:31:51 -0800 Subject: [PATCH 031/308] Show status icons in fuzzy finder --- .../fuzzy-finder/lib/fuzzy-finder-view.coffee | 37 ++++++++++++------- .../spec/fuzzy-finder-spec.coffee | 35 +++++++++++++++++- static/fuzzy-finder.css | 12 ++++++ 3 files changed, 70 insertions(+), 14 deletions(-) diff --git a/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee b/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee index 8b5c56e7e..32e663766 100644 --- a/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee +++ b/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee @@ -39,19 +39,30 @@ class FuzzyFinderView extends SelectList itemForElement: (path) -> $$ -> @li => - ext = fs.extension(path) - if fs.isReadmePath(path) - typeClass = 'readme-name' - else if fs.isCompressedExtension(ext) - typeClass = 'compressed-name' - else if fs.isImageExtension(ext) - typeClass = 'image-name' - else if fs.isPdfExtension(ext) - typeClass = 'pdf-name' - else if fs.isBinaryExtension(ext) - typeClass = 'binary-name' - else - typeClass = 'text-name' + typeClass = null + repo = project.repo + if repo? + status = project.repo?.statuses[project.resolve(path)] + if repo.isStatusNew(status) + typeClass = 'new' + else if repo.isStatusModified(status) + typeClass = 'modified' + + unless typeClass + ext = fs.extension(path) + if fs.isReadmePath(path) + typeClass = 'readme-name' + else if fs.isCompressedExtension(ext) + typeClass = 'compressed-name' + else if fs.isImageExtension(ext) + typeClass = 'image-name' + else if fs.isPdfExtension(ext) + typeClass = 'pdf-name' + else if fs.isBinaryExtension(ext) + typeClass = 'binary-name' + else + typeClass = 'text-name' + @span fs.base(path), class: "file label #{typeClass}" if folder = fs.directory(path) @span " - #{folder}/", class: 'directory' diff --git a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee index 43d7065f1..a5eafb5b0 100644 --- a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee +++ b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee @@ -376,7 +376,6 @@ describe 'FuzzyFinder', -> runs -> expect(finderView.find('.error').text().length).toBeGreaterThan 0 - describe "opening a path into a split", -> beforeEach -> rootView.attachToDom() @@ -425,3 +424,37 @@ describe 'FuzzyFinder', -> expect(editor.splitUp).toHaveBeenCalled() expect(rootView.getActiveEditor()).not.toBe editor expect(rootView.getActiveEditor().getPath()).toBe editor.getPath() + + describe "git status decorations", -> + [originalText, originalPath, editor, newPath] = [] + + beforeEach -> + editor = rootView.getActiveEditor() + originalText = editor.getText() + originalPath = editor.getPath() + newPath = project.resolve('newsample.js') + fs.write(newPath, '') + + afterEach -> + fs.write(originalPath, originalText) + fs.remove(newPath) if fs.exists(newPath) + + describe "when a modified file is shown in the list", -> + it "displays the modified icon", -> + editor.setText('modified') + editor.save() + project.repo?.getPathStatus(editor.getPath()) + + rootView.trigger 'fuzzy-finder:toggle-buffer-finder' + expect(finderView.find('.file.modified').length).toBe 1 + expect(finderView.find('.file.modified').text()).toBe 'sample.js' + + + describe "when a new file is shown in the list", -> + it "displays the new icon", -> + rootView.open('newsample.js') + project.repo?.getPathStatus(editor.getPath()) + + rootView.trigger 'fuzzy-finder:toggle-buffer-finder' + expect(finderView.find('.file.new').length).toBe 1 + expect(finderView.find('.file.new').text()).toBe 'newsample.js' diff --git a/static/fuzzy-finder.css b/static/fuzzy-finder.css index 410269890..6d5b7d8d8 100644 --- a/static/fuzzy-finder.css +++ b/static/fuzzy-finder.css @@ -16,6 +16,18 @@ color: #9d9d9d; } +.fuzzy-finder .file.new:before { + position: relative; + top: 1px; + content: "\f06b"; +} + +.fuzzy-finder .file.modified:before { + position: relative; + top: 1px; + content: "\f06d"; +} + .fuzzy-finder .file.text-name:before { content: "\f011"; } From 94449f772c1c01ea71cc7c6e0c71873fccee99a0 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 27 Feb 2013 17:48:13 -0800 Subject: [PATCH 032/308] Initialize statuses and task variables correctly --- src/app/git.coffee | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/git.coffee b/src/app/git.coffee index 8e24a4b97..f73517a33 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -8,6 +8,7 @@ RepositoryStatusTask = require 'repository-status-task' module.exports = class Git @open: (path, options) -> + return null unless path try new Git(path, options) catch e @@ -25,9 +26,10 @@ class Git working_dir_typechange: 1 << 10 ignore: 1 << 14 - statuses: {} + statuses: null constructor: (path, options={}) -> + @statuses = {} @repo = GitRepository.open(path) refreshOnWindowFocus = options.refreshOnWindowFocus ? true if refreshOnWindowFocus @@ -114,7 +116,8 @@ class Git @getRepo().isSubmodule(@relativize(path)) refreshStatus: -> - @statusTask = new RepositoryStatusTask(this).start() + @statusTask = new RepositoryStatusTask(this) + @statusTask.start() _.extend Git.prototype, Subscriber _.extend Git.prototype, EventEmitter From 219a8581fd08c4b68013ca638e44c9d25143ff4e Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 27 Feb 2013 17:49:08 -0800 Subject: [PATCH 033/308] :lipstick: --- src/app/repository-status-task.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/repository-status-task.coffee b/src/app/repository-status-task.coffee index f4abbd0c9..b7535e38a 100644 --- a/src/app/repository-status-task.coffee +++ b/src/app/repository-status-task.coffee @@ -3,7 +3,6 @@ _ = require 'underscore' module.exports = class RepositoryStatusTask extends Task - constructor: (@repo) -> super('repository-status-handler') From 2ec4d558bab44fc5b614d0652b97f2b620205222 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 27 Feb 2013 17:50:37 -0800 Subject: [PATCH 034/308] Make project's Git repository a window global This allows it to operate independently of the project and mirror the availability of the root view and project. --- spec/app/editor-spec.coffee | 1 + spec/spec-helper.coffee | 9 +++++++++ src/app/buffer.coffee | 4 +--- src/app/project.coffee | 7 +------ src/app/window.coffee | 6 ++++++ .../fuzzy-finder/lib/fuzzy-finder-view.coffee | 9 ++++----- .../fuzzy-finder/spec/fuzzy-finder-spec.coffee | 4 ++-- src/packages/status-bar/lib/status-bar-view.coffee | 5 ++--- src/packages/tree-view/lib/directory-view.coffee | 11 +++++------ src/packages/tree-view/lib/file-view.coffee | 11 +++++------ 10 files changed, 36 insertions(+), 31 deletions(-) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index 1f80bed79..52a8a396a 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -8,6 +8,7 @@ $ = require 'jquery' {$$} = require 'space-pen' _ = require 'underscore' fs = require 'fs' +Git = require 'git' describe "Editor", -> [buffer, editor, cachedLineHeight] = [] diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 7e197d483..910c8d238 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -14,6 +14,7 @@ Editor = require 'editor' TokenizedBuffer = require 'tokenized-buffer' fs = require 'fs' RootView = require 'root-view' +Git = require 'git' requireStylesheet "jasmine.css" fixturePackagesPath = require.resolve('fixtures/packages') require.paths.unshift(fixturePackagesPath) @@ -31,6 +32,11 @@ beforeEach -> jQuery.fx.off = true window.fixturesProject = new Project(require.resolve('fixtures')) window.project = fixturesProject + window.git = Git.open(fixturesProject.getPath()) + window.project.on 'path-changed', -> + window.git?.destroy() + window.git = Git.open(window.project.getPath()) + window.resetTimeouts() atom.atomPackageStates = {} atom.loadedPackages = [] @@ -72,6 +78,9 @@ afterEach -> if project? project.destroy() window.project = null + if git? + git.destroy() + window.git = null $('#jasmine-content').empty() ensureNoPathSubscriptions() waits(0) # yield to ui thread to make screen update more frequently diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee index 0cf84f7a7..d5bdaf19c 100644 --- a/src/app/buffer.coffee +++ b/src/app/buffer.coffee @@ -419,12 +419,10 @@ class Buffer return match[0][0] != '\t' undefined - getRepo: -> @project?.repo - checkoutHead: -> path = @getPath() return unless path - if @getRepo()?.checkoutHead(path) + if git?.checkoutHead(path) @trigger 'git-status-changed' scheduleStoppedChangingEvent: -> diff --git a/src/app/project.coffee b/src/app/project.coffee index 9b8aa380d..7aeb0757d 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -7,7 +7,6 @@ EditSession = require 'edit-session' EventEmitter = require 'event-emitter' Directory = require 'directory' ChildProcess = require 'child-process' -Git = require 'git' module.exports = class Project @@ -35,8 +34,6 @@ class Project grammarOverridesByPath: @grammarOverridesByPath destroy: -> - @repo?.destroy() - @repo = null editSession.destroy() for editSession in @getEditSessions() addGrammarOverrideForPath: (path, grammar) -> @@ -60,10 +57,8 @@ class Project if path? directory = if fs.isDirectory(path) then path else fs.directory(path) @rootDirectory = new Directory(directory) - @repo = Git.open(path) else @rootDirectory = null - @repo = null @trigger "path-changed" @@ -85,7 +80,7 @@ class Project @ignoreRepositoryPath(path) ignoreRepositoryPath: (path) -> - config.get("core.hideGitIgnoredFiles") and @repo?.isPathIgnored(fs.join(@getPath(), path)) + config.get("core.hideGitIgnoredFiles") and git?.isPathIgnored(fs.join(@getPath(), path)) resolve: (filePath) -> filePath = fs.join(@getPath(), filePath) unless filePath[0] == '/' diff --git a/src/app/window.coffee b/src/app/window.coffee index 29a87d712..cd10df014 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -91,6 +91,7 @@ window.handleWindowEvents = -> window.buildProjectAndRootView = -> RootView = require 'root-view' Project = require 'project' + Git = require 'git' pathToOpen = atom.getPathToOpen() windowState = atom.getRootViewStateForPath(pathToOpen) ? {} @@ -102,6 +103,11 @@ window.buildProjectAndRootView = -> $(rootViewParentSelector).append(rootView) + window.git = Git.open(project.getPath()) + project.on 'path-changed', -> + window.git?.destroy() + window.git = Git.open(project.getPath()) + window.stylesheetElementForId = (id) -> $("head style[id='#{id}']") diff --git a/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee b/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee index 32e663766..4330aad74 100644 --- a/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee +++ b/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee @@ -40,12 +40,11 @@ class FuzzyFinderView extends SelectList $$ -> @li => typeClass = null - repo = project.repo - if repo? - status = project.repo?.statuses[project.resolve(path)] - if repo.isStatusNew(status) + if git? + status = git.statuses[project.resolve(path)] + if git.isStatusNew(status) typeClass = 'new' - else if repo.isStatusModified(status) + else if git.isStatusModified(status) typeClass = 'modified' unless typeClass diff --git a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee index a5eafb5b0..89251317b 100644 --- a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee +++ b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee @@ -443,7 +443,7 @@ describe 'FuzzyFinder', -> it "displays the modified icon", -> editor.setText('modified') editor.save() - project.repo?.getPathStatus(editor.getPath()) + git.getPathStatus(editor.getPath()) rootView.trigger 'fuzzy-finder:toggle-buffer-finder' expect(finderView.find('.file.modified').length).toBe 1 @@ -453,7 +453,7 @@ describe 'FuzzyFinder', -> describe "when a new file is shown in the list", -> it "displays the new icon", -> rootView.open('newsample.js') - project.repo?.getPathStatus(editor.getPath()) + git.getPathStatus(editor.getPath()) rootView.trigger 'fuzzy-finder:toggle-buffer-finder' expect(finderView.find('.file.new').length).toBe 1 diff --git a/src/packages/status-bar/lib/status-bar-view.coffee b/src/packages/status-bar/lib/status-bar-view.coffee index ffd51486b..717cabeaa 100644 --- a/src/packages/status-bar/lib/status-bar-view.coffee +++ b/src/packages/status-bar/lib/status-bar-view.coffee @@ -68,7 +68,7 @@ class StatusBarView extends View @branchArea.hide() return unless path - head = @buffer.getRepo()?.getShortHead() or '' + head = git?.getShortHead() or '' @branchLabel.text(head) @branchArea.show() if head @@ -78,8 +78,7 @@ class StatusBarView extends View return unless path @gitStatusIcon.addClass('git-status octicons') - git = @buffer.getRepo() - return unless git + return unless git? status = git.getPathStatus(path) if git.isStatusModified(status) diff --git a/src/packages/tree-view/lib/directory-view.coffee b/src/packages/tree-view/lib/directory-view.coffee index 29bd8e767..bb1401399 100644 --- a/src/packages/tree-view/lib/directory-view.coffee +++ b/src/packages/tree-view/lib/directory-view.coffee @@ -22,22 +22,21 @@ class DirectoryView extends View @expand() if isExpanded @disclosureArrow.on 'click', => @toggleExpansion() - repo = @project.repo iconClass = 'directory-icon' - if repo? + if git? path = @directory.getPath() if parent - @directoryName.addClass('ignored') if repo.isPathIgnored(path) - iconClass = 'submodule-icon' if repo.isSubmodule(path) + @directoryName.addClass('ignored') if git.isPathIgnored(path) + iconClass = 'submodule-icon' if git.isSubmodule(path) else - iconClass = 'repository-icon' if path is repo.getWorkingDirectory() + iconClass = 'repository-icon' if path is git.getWorkingDirectory() @directoryName.addClass(iconClass) getPath: -> @directory.path isPathIgnored: (path) -> - config.get("core.hideGitIgnoredFiles") and @project.repo?.isPathIgnored(path) + config.get("core.hideGitIgnoredFiles") and git?.isPathIgnored(path) buildEntries: -> @unwatchDescendantEntries() diff --git a/src/packages/tree-view/lib/file-view.coffee b/src/packages/tree-view/lib/file-view.coffee index 0b29d9ee5..606398c79 100644 --- a/src/packages/tree-view/lib/file-view.coffee +++ b/src/packages/tree-view/lib/file-view.coffee @@ -34,17 +34,16 @@ class FileView extends View updateStatus: -> @removeClass('ignored modified new') - repo = @project.repo - return unless repo? + return unless git? path = @getPath() - if repo.isPathIgnored(path) + if git.isPathIgnored(path) @addClass('ignored') else - status = repo.getPathStatus(path) - if repo.isStatusModified(status) + status = git.getPathStatus(path) + if git.isStatusModified(status) @addClass('modified') - else if repo.isStatusNew(status) + else if git.isStatusNew(status) @addClass('new') getPath: -> From 4fe6db240b78b9841dcd0b06b09c60e5abdf5bd4 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 27 Feb 2013 18:37:39 -0800 Subject: [PATCH 035/308] Consolidate Git status checking Now the status bar and tree view both listen for status change events and use the cached information available from the git object to update their views. --- spec/app/editor-spec.coffee | 16 +++++++++------- src/app/git.coffee | 7 +++++++ src/app/project.coffee | 10 ++++++++-- src/app/window.coffee | 2 ++ .../status-bar/lib/status-bar-view.coffee | 9 ++++++--- .../status-bar/spec/status-bar-spec.coffee | 14 ++++++-------- src/packages/tree-view/lib/file-view.coffee | 10 +++++++--- .../tree-view/spec/tree-view-spec.coffee | 9 ++------- 8 files changed, 47 insertions(+), 30 deletions(-) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index 52a8a396a..d38fe73b2 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -137,16 +137,18 @@ describe "Editor", -> describe ".remove()", -> it "removes subscriptions from all edit session buffers", -> - previousEditSession = editor.activeEditSession - otherEditSession = project.buildEditSessionForPath(project.resolve('sample.txt')) - expect(previousEditSession.buffer.subscriptionCount()).toBeGreaterThan 1 + editSession1 = editor.activeEditSession + subscriberCount1 = editSession1.buffer.subscriptionCount() + editSession2 = project.buildEditSessionForPath(project.resolve('sample.txt')) + expect(subscriberCount1).toBeGreaterThan 1 - editor.edit(otherEditSession) - expect(otherEditSession.buffer.subscriptionCount()).toBeGreaterThan 1 + editor.edit(editSession2) + subscriberCount2 = editSession2.buffer.subscriptionCount() + expect(subscriberCount2).toBeGreaterThan 1 editor.remove() - expect(previousEditSession.buffer.subscriptionCount()).toBe 0 - expect(otherEditSession.buffer.subscriptionCount()).toBe 0 + 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", -> diff --git a/src/app/git.coffee b/src/app/git.coffee index f73517a33..b85829c01 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -38,6 +38,13 @@ class Git @refreshIndex() @refreshStatus() + project?.eachBuffer this, (buffer) => + bufferStatusHandler = => + path = buffer.getPath() + @getPathStatus(path) if path + @subscribe buffer, 'saved', bufferStatusHandler + @subscribe buffer, 'reloaded', bufferStatusHandler + getRepo: -> unless @repo? throw new Error("Repository has been destroyed") diff --git a/src/app/project.coffee b/src/app/project.coffee index 7aeb0757d..d31c12238 100644 --- a/src/app/project.coffee +++ b/src/app/project.coffee @@ -128,9 +128,15 @@ class Project buffers.push editSession.buffer buffers - eachBuffer: (callback) -> + eachBuffer: (args...) -> + subscriber = args.shift() if args.length > 1 + callback = args.shift() + callback(buffer) for buffer in @getBuffers() - @on 'buffer-created', (buffer) -> callback(buffer) + if subscriber + subscriber.subscribe this, 'buffer-created', (buffer) -> callback(buffer) + else + @on 'buffer-created', (buffer) -> callback(buffer) bufferForPath: (filePath) -> if filePath? diff --git a/src/app/window.coffee b/src/app/window.coffee index cd10df014..a4c514b15 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -69,9 +69,11 @@ window.shutdown = -> rootView: rootView.serialize() rootView.deactivate() project.destroy() + git?.destroy() $(window).off('focus blur before') window.rootView = null window.project = null + window.git = null window.installAtomCommand = (commandPath) -> return if fs.exists(commandPath) diff --git a/src/packages/status-bar/lib/status-bar-view.coffee b/src/packages/status-bar/lib/status-bar-view.coffee index 717cabeaa..c095e7f39 100644 --- a/src/packages/status-bar/lib/status-bar-view.coffee +++ b/src/packages/status-bar/lib/status-bar-view.coffee @@ -32,9 +32,13 @@ class StatusBarView extends View @updateCursorPositionText() @subscribe @editor, 'cursor:moved', => @updateCursorPositionText() - @subscribe $(window), 'focus', => @updateStatusBar() @subscribe @grammarName, 'click', => @editor.trigger 'editor:select-grammar' @subscribe @editor, 'editor:grammar-changed', => @updateGrammarText() + if git? + @subscribe git, 'status-changed', (path, status) => + @updateStatusBar() if path is @buffer?.getPath() + @subscribe git, 'statuses-changed', => + @updateStatusBar() @subscribeToBuffer() @@ -42,7 +46,6 @@ class StatusBarView extends View @buffer?.off '.status-bar' @buffer = @editor.getBuffer() @buffer.on 'contents-modified.status-bar', (e) => @updateBufferHasModifiedText(e.differsFromDisk) - @buffer.on 'saved.status-bar', => @updateStatusBar() @buffer.on 'git-status-changed.status-bar', => @updateStatusBar() @updateStatusBar() @@ -80,7 +83,7 @@ class StatusBarView extends View @gitStatusIcon.addClass('git-status octicons') return unless git? - status = git.getPathStatus(path) + status = git.statuses[path] if git.isStatusModified(status) @gitStatusIcon.addClass('modified-status-icon') stats = git.getDiffStats(path) diff --git a/src/packages/status-bar/spec/status-bar-spec.coffee b/src/packages/status-bar/spec/status-bar-spec.coffee index 177a2b6a2..85e2d7361 100644 --- a/src/packages/status-bar/spec/status-bar-spec.coffee +++ b/src/packages/status-bar/spec/status-bar-spec.coffee @@ -130,6 +130,8 @@ describe "StatusBar", -> path = require.resolve('fixtures/git/working-dir/file.txt') newPath = fs.join(require.resolve('fixtures/git/working-dir'), 'new.txt') fs.write(newPath, "I'm new here") + git.getPathStatus(path) + git.getPathStatus(newPath) originalPathText = fs.read(path) rootView.attachToDom() @@ -139,6 +141,7 @@ describe "StatusBar", -> it "displays the modified icon for a changed file", -> fs.write(path, "i've changed for the worse") + git.getPathStatus(path) rootView.open(path) expect(statusBar.gitStatusIcon).toHaveClass('modified-status-icon') @@ -152,22 +155,17 @@ describe "StatusBar", -> it "updates when a git-status-changed event occurs", -> fs.write(path, "i've changed for the worse") + git.getPathStatus(path) rootView.open(path) expect(statusBar.gitStatusIcon).toHaveClass('modified-status-icon') fs.write(path, originalPathText) + git.getPathStatus(path) rootView.getActiveEditor().getBuffer().trigger 'git-status-changed' expect(statusBar.gitStatusIcon).not.toHaveClass('modified-status-icon') - it "updates when the window receives focus", -> - fs.write(path, "i've changed for the worse") - rootView.open(path) - expect(statusBar.gitStatusIcon).toHaveClass('modified-status-icon') - fs.write(path, originalPathText) - $(window).trigger 'focus' - expect(statusBar.gitStatusIcon).not.toHaveClass('modified-status-icon') - it "displays the diff stat for modified files", -> fs.write(path, "i've changed for the worse") + git.getPathStatus(path) rootView.open(path) expect(statusBar.gitStatusIcon).toHaveText('+1,-1') diff --git a/src/packages/tree-view/lib/file-view.coffee b/src/packages/tree-view/lib/file-view.coffee index 606398c79..51b94d362 100644 --- a/src/packages/tree-view/lib/file-view.coffee +++ b/src/packages/tree-view/lib/file-view.coffee @@ -14,8 +14,6 @@ class FileView extends View file: null initialize: ({@file, @project} = {}) -> - @subscribe $(window), 'focus', => @updateStatus() - extension = fs.extension(@getPath()) if fs.isReadmePath(@getPath()) @fileName.addClass('readme-icon') @@ -30,6 +28,12 @@ class FileView extends View else @fileName.addClass('text-icon') + if git? + git.on 'status-changed', (path, status) => + @updateStatus() if path is @getPath() + git.on 'statuses-changed', => + @updateStatus() + @updateStatus() updateStatus: -> @@ -40,7 +44,7 @@ class FileView extends View if git.isPathIgnored(path) @addClass('ignored') else - status = git.getPathStatus(path) + status = git.statuses[path] if git.isStatusModified(status) @addClass('modified') else if git.isStatusNew(status) diff --git a/src/packages/tree-view/spec/tree-view-spec.coffee b/src/packages/tree-view/spec/tree-view-spec.coffee index 73c91d24b..70dd15e98 100644 --- a/src/packages/tree-view/spec/tree-view-spec.coffee +++ b/src/packages/tree-view/spec/tree-view-spec.coffee @@ -933,9 +933,11 @@ describe "TreeView", -> config.set "core.hideGitIgnoredFiles", false ignoreFile = fs.join(require.resolve('fixtures/tree-view'), '.gitignore') fs.write(ignoreFile, 'tree-view.js') + git.getPathStatus(ignoreFile) modifiedFile = fs.join(require.resolve('fixtures/tree-view'), 'tree-view.txt') originalFileContent = fs.read(modifiedFile) fs.write modifiedFile, 'ch ch changes' + git.getPathStatus(modifiedFile) treeView.updateRoot() afterEach -> @@ -946,13 +948,6 @@ describe "TreeView", -> it "adds a custom style", -> expect(treeView.find('.file:contains(tree-view.txt)')).toHaveClass 'modified' - describe "when the window gains focus after the contents are restored to a clean state", -> - it "removes the custom style", -> - expect(treeView.find('.file:contains(tree-view.txt)')).toHaveClass 'modified' - fs.write modifiedFile, originalFileContent - $(window).trigger 'focus' - expect(treeView.find('.file:contains(tree-view.txt)')).not.toHaveClass 'modified' - describe "when a file is new", -> it "adds a custom style", -> expect(treeView.find('.file:contains(.gitignore)')).toHaveClass 'new' From 2e7e4b3ee45e3cb9ef7590710208c9500c9438eb Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 27 Feb 2013 18:38:57 -0800 Subject: [PATCH 036/308] :lipstick: --- src/app/event-emitter.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/event-emitter.coffee b/src/app/event-emitter.coffee index 8481ca607..3abcb74ac 100644 --- a/src/app/event-emitter.coffee +++ b/src/app/event-emitter.coffee @@ -83,4 +83,3 @@ module.exports = for name, handlers of @eventHandlersByEventName count += handlers.length count - From 587a6552ea5f9cf49f82c4b6c3fe5b0bab353e48 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 27 Feb 2013 18:41:01 -0800 Subject: [PATCH 037/308] Use subscribe instead of on --- src/packages/tree-view/lib/file-view.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/packages/tree-view/lib/file-view.coffee b/src/packages/tree-view/lib/file-view.coffee index 51b94d362..f8d7f5115 100644 --- a/src/packages/tree-view/lib/file-view.coffee +++ b/src/packages/tree-view/lib/file-view.coffee @@ -29,9 +29,9 @@ class FileView extends View @fileName.addClass('text-icon') if git? - git.on 'status-changed', (path, status) => + @subscribe git, 'status-changed', (path, status) => @updateStatus() if path is @getPath() - git.on 'statuses-changed', => + @subscribe git, 'statuses-changed', => @updateStatus() @updateStatus() From 77a5f4775a6a671076ae074b044c63d484f88656 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 27 Feb 2013 19:36:21 -0800 Subject: [PATCH 038/308] Show Git status indicators on directories Closes #301 --- src/app/git.coffee | 7 ++++++ .../tree-view/lib/directory-view.coffee | 23 +++++++++++++++++-- .../tree-view/spec/tree-view-spec.coffee | 23 ++++++++++++++++--- themes/atom-dark-ui/tree-view.css | 2 +- themes/atom-light-ui/tree-view.css | 5 ++-- 5 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/app/git.coffee b/src/app/git.coffee index b85829c01..d450147a5 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -126,5 +126,12 @@ class Git @statusTask = new RepositoryStatusTask(this) @statusTask.start() + getDirectoryStatus: (directoryPath) -> + directoryPath = "#{directoryPath}/" + directoryStatus = 0 + for path, status of @statuses + directoryStatus |= status if path.indexOf(directoryPath) is 0 + directoryStatus + _.extend Git.prototype, Subscriber _.extend Git.prototype, EventEmitter diff --git a/src/packages/tree-view/lib/directory-view.coffee b/src/packages/tree-view/lib/directory-view.coffee index bb1401399..c6f561c1d 100644 --- a/src/packages/tree-view/lib/directory-view.coffee +++ b/src/packages/tree-view/lib/directory-view.coffee @@ -26,12 +26,31 @@ class DirectoryView extends View if git? path = @directory.getPath() if parent - @directoryName.addClass('ignored') if git.isPathIgnored(path) - iconClass = 'submodule-icon' if git.isSubmodule(path) + if git.isSubmodule(path) + iconClass = 'submodule-icon' + else + @subscribe git, 'status-changed', (path, status) => + @updateStatus() if path.substring("#{@getPath()}/") is 0 + @subscribe git, 'statuses-changed', => + @updateStatus() + @updateStatus() else iconClass = 'repository-icon' if path is git.getWorkingDirectory() + @directoryName.addClass(iconClass) + updateStatus: -> + @removeClass('ignored modified new') + path = @directory.getPath() + if git.isPathIgnored(path) + @addClass('ignored') + else + status = git.getDirectoryStatus(path) + if git.isStatusModified(status) + @addClass('modified') + else if git.isStatusNew(status) + @addClass('new') + getPath: -> @directory.path diff --git a/src/packages/tree-view/spec/tree-view-spec.coffee b/src/packages/tree-view/spec/tree-view-spec.coffee index 70dd15e98..c364bf527 100644 --- a/src/packages/tree-view/spec/tree-view-spec.coffee +++ b/src/packages/tree-view/spec/tree-view-spec.coffee @@ -927,31 +927,48 @@ describe "TreeView", -> expect(treeView.find('.file:contains(tree-view.js)').length).toBe 1 describe "Git status decorations", -> - [ignoreFile, modifiedFile, originalFileContent] = [] + [ignoreFile, newFile, modifiedFile, originalFileContent] = [] beforeEach -> config.set "core.hideGitIgnoredFiles", false ignoreFile = fs.join(require.resolve('fixtures/tree-view'), '.gitignore') fs.write(ignoreFile, 'tree-view.js') git.getPathStatus(ignoreFile) - modifiedFile = fs.join(require.resolve('fixtures/tree-view'), 'tree-view.txt') + + newFile = fs.join(require.resolve('fixtures/tree-view/dir2'), 'new2') + fs.write(newFile, '') + git.getPathStatus(newFile) + + modifiedFile = fs.join(require.resolve('fixtures/tree-view/dir1'), 'file1') originalFileContent = fs.read(modifiedFile) fs.write modifiedFile, 'ch ch changes' git.getPathStatus(modifiedFile) + treeView.updateRoot() + treeView.root.entries.find('.directory:contains(dir1)').view().expand() + treeView.root.entries.find('.directory:contains(dir2)').view().expand() afterEach -> fs.remove(ignoreFile) if fs.exists(ignoreFile) + fs.remove(newFile) if fs.exists(newFile) fs.write modifiedFile, originalFileContent describe "when a file is modified", -> it "adds a custom style", -> - expect(treeView.find('.file:contains(tree-view.txt)')).toHaveClass 'modified' + expect(treeView.find('.file:contains(file1)')).toHaveClass 'modified' + + describe "when a directory if modified", -> + it "adds a custom style", -> + expect(treeView.find('.directory:contains(dir1)')).toHaveClass 'modified' describe "when a file is new", -> it "adds a custom style", -> expect(treeView.find('.file:contains(.gitignore)')).toHaveClass 'new' + describe "when a directory is new", -> + it "adds a custom style", -> + expect(treeView.find('.directory:contains(dir2)')).toHaveClass 'new' + describe "when a file is ignored", -> it "adds a custom style", -> expect(treeView.find('.file:contains(tree-view.js)')).toHaveClass 'ignored' diff --git a/themes/atom-dark-ui/tree-view.css b/themes/atom-dark-ui/tree-view.css index 26851216e..f1d8a9796 100644 --- a/themes/atom-dark-ui/tree-view.css +++ b/themes/atom-dark-ui/tree-view.css @@ -29,7 +29,7 @@ text-shadow: 0 -1px 0 #7E4521; } -.tree-view .directory .header { +.tree-view .directory { color: #bebebe; } diff --git a/themes/atom-light-ui/tree-view.css b/themes/atom-light-ui/tree-view.css index ff18a5990..5ab39df36 100644 --- a/themes/atom-light-ui/tree-view.css +++ b/themes/atom-light-ui/tree-view.css @@ -34,7 +34,7 @@ text-shadow: 0 1px 0 #000; } -.tree-view .directory .header { +.tree-view .directory { color: #262626; } @@ -46,7 +46,6 @@ color: #7e8692; } - .tree-view .entry:hover, .tree-view .directory .header:hover .name, .tree-view .directory .header:hover .disclosure-arrow { @@ -83,4 +82,4 @@ .tree-view-dialog .prompt { color: #333; -} \ No newline at end of file +} From 31690d16ecfdc4e089ac7ff3d6bfc20c6a4e0d17 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 27 Feb 2013 20:05:40 -0800 Subject: [PATCH 039/308] Remove unused import --- spec/app/editor-spec.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index d38fe73b2..0d46cfba9 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -8,7 +8,6 @@ $ = require 'jquery' {$$} = require 'space-pen' _ = require 'underscore' fs = require 'fs' -Git = require 'git' describe "Editor", -> [buffer, editor, cachedLineHeight] = [] From 3852b7212b1d192d0b3268cbc72b4991b0bb7b34 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 27 Feb 2013 20:16:20 -0800 Subject: [PATCH 040/308] Remove git-status-changed event from Buffer This is now fired as a status-changed event from the Git class when the checkout completes normally and the status of the path changes. --- spec/app/git-spec.coffee | 11 +++++++++++ src/app/buffer.coffee | 3 +-- src/app/git.coffee | 4 +++- src/packages/status-bar/lib/status-bar-view.coffee | 1 - src/packages/status-bar/spec/status-bar-spec.coffee | 3 +-- src/packages/tabs/lib/tab.coffee | 1 - 6 files changed, 16 insertions(+), 7 deletions(-) diff --git a/spec/app/git-spec.coffee b/spec/app/git-spec.coffee index 2697e9723..27a72d694 100644 --- a/spec/app/git-spec.coffee +++ b/spec/app/git-spec.coffee @@ -126,6 +126,17 @@ describe "Git", -> expect(fs.read(path2)).toBe('path 2 is edited') expect(repo.isPathModified(path2)).toBeTruthy() + it "fires a status-changed event if the checkout completes successfully", -> + statusHandler = jasmine.createSpy('statusHandler') + repo.on 'status-changed', statusHandler + fs.write(path1, '') + repo.checkoutHead(path1) + expect(statusHandler.callCount).toBe 1 + expect(statusHandler.argsForCall[0][0..1]).toEqual [path1, 0] + + repo.checkoutHead(path1) + expect(statusHandler.callCount).toBe 1 + describe ".destroy()", -> it "throws an exception when any method is called after it is called", -> repo = new Git(require.resolve('fixtures/git/master.git/HEAD')) diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee index d5bdaf19c..092f0d797 100644 --- a/src/app/buffer.coffee +++ b/src/app/buffer.coffee @@ -422,8 +422,7 @@ class Buffer checkoutHead: -> path = @getPath() return unless path - if git?.checkoutHead(path) - @trigger 'git-status-changed' + git?.checkoutHead(path) scheduleStoppedChangingEvent: -> clearTimeout(@stoppedChangingTimeout) if @stoppedChangingTimeout diff --git a/src/app/git.coffee b/src/app/git.coffee index d450147a5..a015d474a 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -114,7 +114,9 @@ class Git return head checkoutHead: (path) -> - @getRepo().checkoutHead(@relativize(path)) + headCheckedOut = @getRepo().checkoutHead(@relativize(path)) + @getPathStatus(path) if headCheckedOut + headCheckedOut getDiffStats: (path) -> @getRepo().getDiffStats(@relativize(path)) ? added: 0, deleted: 0 diff --git a/src/packages/status-bar/lib/status-bar-view.coffee b/src/packages/status-bar/lib/status-bar-view.coffee index c095e7f39..15a8be49a 100644 --- a/src/packages/status-bar/lib/status-bar-view.coffee +++ b/src/packages/status-bar/lib/status-bar-view.coffee @@ -46,7 +46,6 @@ class StatusBarView extends View @buffer?.off '.status-bar' @buffer = @editor.getBuffer() @buffer.on 'contents-modified.status-bar', (e) => @updateBufferHasModifiedText(e.differsFromDisk) - @buffer.on 'git-status-changed.status-bar', => @updateStatusBar() @updateStatusBar() updateStatusBar: -> diff --git a/src/packages/status-bar/spec/status-bar-spec.coffee b/src/packages/status-bar/spec/status-bar-spec.coffee index 85e2d7361..f2d73b07b 100644 --- a/src/packages/status-bar/spec/status-bar-spec.coffee +++ b/src/packages/status-bar/spec/status-bar-spec.coffee @@ -153,14 +153,13 @@ describe "StatusBar", -> rootView.open(newPath) expect(statusBar.gitStatusIcon).toHaveClass('new-status-icon') - it "updates when a git-status-changed event occurs", -> + it "updates when a status-changed event occurs", -> fs.write(path, "i've changed for the worse") git.getPathStatus(path) rootView.open(path) expect(statusBar.gitStatusIcon).toHaveClass('modified-status-icon') fs.write(path, originalPathText) git.getPathStatus(path) - rootView.getActiveEditor().getBuffer().trigger 'git-status-changed' expect(statusBar.gitStatusIcon).not.toHaveClass('modified-status-icon') it "displays the diff stat for modified files", -> diff --git a/src/packages/tabs/lib/tab.coffee b/src/packages/tabs/lib/tab.coffee index bcc055f1d..9a7e8e3ab 100644 --- a/src/packages/tabs/lib/tab.coffee +++ b/src/packages/tabs/lib/tab.coffee @@ -13,7 +13,6 @@ class Tab extends View @subscribe @buffer, 'path-changed', => @updateFileName() @subscribe @buffer, 'contents-modified', => @updateModifiedStatus() @subscribe @buffer, 'saved', => @updateModifiedStatus() - @subscribe @buffer, 'git-status-changed', => @updateModifiedStatus() @subscribe @editor, 'editor:edit-session-added', => @updateFileName() @subscribe @editor, 'editor:edit-session-removed', => @updateFileName() @updateFileName() From 77bc42bd45445be41522764ff1c8928db038f38d Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 28 Feb 2013 12:16:19 -0800 Subject: [PATCH 041/308] Put status indicator on right side of fuzzy finder Closes #313 --- .../fuzzy-finder/lib/fuzzy-finder-view.coffee | 32 +++++++++---------- .../spec/fuzzy-finder-spec.coffee | 8 ++--- static/fuzzy-finder.css | 19 ++++++++--- themes/atom-dark-ui/select-list.css | 8 +++++ themes/atom-light-ui/select-list.css | 8 +++++ 5 files changed, 50 insertions(+), 25 deletions(-) diff --git a/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee b/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee index 4330aad74..43d6f6b36 100644 --- a/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee +++ b/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee @@ -39,28 +39,26 @@ class FuzzyFinderView extends SelectList itemForElement: (path) -> $$ -> @li => - typeClass = null if git? status = git.statuses[project.resolve(path)] if git.isStatusNew(status) - typeClass = 'new' + @div class: 'status new' else if git.isStatusModified(status) - typeClass = 'modified' + @div class: 'status modified' - unless typeClass - ext = fs.extension(path) - if fs.isReadmePath(path) - typeClass = 'readme-name' - else if fs.isCompressedExtension(ext) - typeClass = 'compressed-name' - else if fs.isImageExtension(ext) - typeClass = 'image-name' - else if fs.isPdfExtension(ext) - typeClass = 'pdf-name' - else if fs.isBinaryExtension(ext) - typeClass = 'binary-name' - else - typeClass = 'text-name' + ext = fs.extension(path) + if fs.isReadmePath(path) + typeClass = 'readme-name' + else if fs.isCompressedExtension(ext) + typeClass = 'compressed-name' + else if fs.isImageExtension(ext) + typeClass = 'image-name' + else if fs.isPdfExtension(ext) + typeClass = 'pdf-name' + else if fs.isBinaryExtension(ext) + typeClass = 'binary-name' + else + typeClass = 'text-name' @span fs.base(path), class: "file label #{typeClass}" if folder = fs.directory(path) diff --git a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee index 89251317b..304fe5c7f 100644 --- a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee +++ b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee @@ -446,8 +446,8 @@ describe 'FuzzyFinder', -> git.getPathStatus(editor.getPath()) rootView.trigger 'fuzzy-finder:toggle-buffer-finder' - expect(finderView.find('.file.modified').length).toBe 1 - expect(finderView.find('.file.modified').text()).toBe 'sample.js' + expect(finderView.find('.status.modified').length).toBe 1 + expect(finderView.find('.status.modified').closest('li').find('.file').text()).toBe 'sample.js' describe "when a new file is shown in the list", -> @@ -456,5 +456,5 @@ describe 'FuzzyFinder', -> git.getPathStatus(editor.getPath()) rootView.trigger 'fuzzy-finder:toggle-buffer-finder' - expect(finderView.find('.file.new').length).toBe 1 - expect(finderView.find('.file.new').text()).toBe 'newsample.js' + expect(finderView.find('.status.new').length).toBe 1 + expect(finderView.find('.status.new').closest('li').find('.file').text()).toBe 'newsample.js' diff --git a/static/fuzzy-finder.css b/static/fuzzy-finder.css index 6d5b7d8d8..3c8a4f3f1 100644 --- a/static/fuzzy-finder.css +++ b/static/fuzzy-finder.css @@ -16,15 +16,26 @@ color: #9d9d9d; } -.fuzzy-finder .file.new:before { +.fuzzy-finder .status { + font-family: 'Octicons Regular'; + font-size: 16px; + width: 16px; + height: 16px; + margin-left: 5px; + -webkit-font-smoothing: antialiased; + color: #9d9d9d; + float: right; +} + +.fuzzy-finder .status.new:before { position: relative; - top: 1px; + top: 3px; content: "\f06b"; } -.fuzzy-finder .file.modified:before { +.fuzzy-finder .status.modified:before { position: relative; - top: 1px; + top: 3px; content: "\f06d"; } diff --git a/themes/atom-dark-ui/select-list.css b/themes/atom-dark-ui/select-list.css index 789ac5313..41740ab83 100644 --- a/themes/atom-dark-ui/select-list.css +++ b/themes/atom-dark-ui/select-list.css @@ -38,3 +38,11 @@ .select-list ol .selected .directory { color: #ccc; } + +.select-list .modified { + color: #f78a46; +} + +.select-list .new { + color: #5293d8; +} diff --git a/themes/atom-light-ui/select-list.css b/themes/atom-light-ui/select-list.css index 2bd0a4e9d..54a8934aa 100644 --- a/themes/atom-light-ui/select-list.css +++ b/themes/atom-light-ui/select-list.css @@ -31,3 +31,11 @@ .select-list ol .selected .directory { color: #333; } + +.select-list .modified { + color: #f78a46; +} + +.select-list .new { + color: #5293d8; +} From 4c773439d28e23c740aff3785d1258e949f297ec Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 28 Feb 2013 12:25:55 -0800 Subject: [PATCH 042/308] Bind meta-T to display modified/untracked files --- .../fuzzy-finder/keymaps/fuzzy-finder.cson | 1 + .../fuzzy-finder/lib/fuzzy-finder-view.coffee | 16 +++++++++++ .../fuzzy-finder/lib/fuzzy-finder.coffee | 2 ++ .../spec/fuzzy-finder-spec.coffee | 28 +++++++++++++++++++ 4 files changed, 47 insertions(+) diff --git a/src/packages/fuzzy-finder/keymaps/fuzzy-finder.cson b/src/packages/fuzzy-finder/keymaps/fuzzy-finder.cson index c4267c9e2..97c3e01b3 100644 --- a/src/packages/fuzzy-finder/keymaps/fuzzy-finder.cson +++ b/src/packages/fuzzy-finder/keymaps/fuzzy-finder.cson @@ -2,3 +2,4 @@ 'meta-t': 'fuzzy-finder:toggle-file-finder' 'meta-b': 'fuzzy-finder:toggle-buffer-finder' 'ctrl-.': 'fuzzy-finder:find-under-cursor' + 'meta-T': 'fuzzy-finder:toggle-git-status-finder' diff --git a/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee b/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee index 43d6f6b36..3eddc0c8e 100644 --- a/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee +++ b/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee @@ -103,6 +103,15 @@ class FuzzyFinderView extends SelectList @populateOpenBufferPaths() @attach() if @paths?.length + toggleGitFinder: -> + if @hasParent() + @cancel() + else + return unless project.getPath()? and git? + @allowActiveEditorChange = false + @populateGitStatusPaths() + @attach() + findUnderCursor: -> if @hasParent() @cancel() @@ -126,6 +135,13 @@ class FuzzyFinderView extends SelectList @attach() @miniEditor.setText(currentWord) + populateGitStatusPaths: -> + projectRelativePaths = [] + for path, status of git.statuses + continue unless fs.isFile(path) + projectRelativePaths.push(project.relativize(path)) + @setArray(projectRelativePaths) + populateProjectPaths: (options = {}) -> if @projectPaths?.length > 0 listedItems = diff --git a/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee b/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee index a3de8b204..c6fbe92aa 100644 --- a/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee +++ b/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee @@ -12,6 +12,8 @@ module.exports = @createView().toggleBufferFinder() rootView.command 'fuzzy-finder:find-under-cursor', => @createView().findUnderCursor() + rootView.command 'fuzzy-finder:toggle-git-status-finder', => + @createView().toggleGitFinder() if project.getPath()? LoadPathsTask = require 'fuzzy-finder/lib/load-paths-task' diff --git a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee index 304fe5c7f..f322d4613 100644 --- a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee +++ b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee @@ -215,6 +215,34 @@ describe 'FuzzyFinder', -> expect(editor2.getPath()).toBe expectedPath expect(editor2.isFocused).toBeTruthy() + describe "git-status-finder behavior", -> + [originalText, originalPath, newPath] = [] + + beforeEach -> + editor = rootView.getActiveEditor() + originalText = editor.getText() + originalPath = editor.getPath() + fs.write(originalPath, 'making a change for the better') + git.getPathStatus(originalPath) + + newPath = project.resolve('newsample.js') + fs.write(newPath, '') + git.getPathStatus(newPath) + + afterEach -> + fs.write(originalPath, originalText) + fs.remove(newPath) if fs.exists(newPath) + + it "displays all new and modified paths", -> + expect(rootView.find('.fuzzy-finder')).not.toExist() + rootView.trigger 'fuzzy-finder:toggle-git-status-finder' + expect(rootView.find('.fuzzy-finder')).toExist() + + expect(finderView.find('.file').length).toBe 2 + + expect(finderView.find('.status.modified').length).toBe 1 + expect(finderView.find('.status.new').length).toBe 1 + describe "common behavior between file and buffer finder", -> describe "when the fuzzy finder is cancelled", -> describe "when an editor is open", -> From 7838f3741fa381f6ef1b781ca3f9a8ec2ddd7cac Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 28 Feb 2013 15:38:07 -0800 Subject: [PATCH 043/308] Update status finder keybinding to meta-B --- src/packages/fuzzy-finder/keymaps/fuzzy-finder.cson | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/fuzzy-finder/keymaps/fuzzy-finder.cson b/src/packages/fuzzy-finder/keymaps/fuzzy-finder.cson index 97c3e01b3..60a302d3d 100644 --- a/src/packages/fuzzy-finder/keymaps/fuzzy-finder.cson +++ b/src/packages/fuzzy-finder/keymaps/fuzzy-finder.cson @@ -2,4 +2,4 @@ 'meta-t': 'fuzzy-finder:toggle-file-finder' 'meta-b': 'fuzzy-finder:toggle-buffer-finder' 'ctrl-.': 'fuzzy-finder:find-under-cursor' - 'meta-T': 'fuzzy-finder:toggle-git-status-finder' + 'meta-B': 'fuzzy-finder:toggle-git-status-finder' From 4384c69dcf42f53ba04a49b83d241acbb6078cab Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 28 Feb 2013 15:44:05 -0800 Subject: [PATCH 044/308] Delete cached statuses unless non-zero --- src/app/git.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/git.coffee b/src/app/git.coffee index a015d474a..0a7859858 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -70,7 +70,10 @@ class Git getPathStatus: (path) -> currentPathStatus = @statuses[path] pathStatus = @getRepo().getStatus(@relativize(path)) - @statuses[path] = pathStatus + if pathStatus > 0 + @statuses[path] = pathStatus + else + delete @statuses[path] if currentPathStatus isnt pathStatus @trigger 'status-changed', path, pathStatus pathStatus From 3703877ae8c70cedb2934afe7f4c07fe8a6fcfc9 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 28 Feb 2013 15:45:00 -0800 Subject: [PATCH 045/308] Default status flags to 0 --- src/app/git.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/git.coffee b/src/app/git.coffee index 0a7859858..f065c209b 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -81,7 +81,7 @@ class Git isPathIgnored: (path) -> @getRepo().isIgnored(@relativize(path)) - isStatusModified: (status) -> + isStatusModified: (status=0) -> modifiedFlags = @statusFlags.working_dir_modified | @statusFlags.working_dir_delete | @statusFlags.working_dir_typechange | @@ -93,7 +93,7 @@ class Git isPathModified: (path) -> @isStatusModified(@getPathStatus(path)) - isStatusNew: (status) -> + isStatusNew: (status=0) -> newFlags = @statusFlags.working_dir_new | @statusFlags.index_new (status & newFlags) > 0 From fd82f3f8a43c13c4797022acd2995876df24893a Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 28 Feb 2013 16:12:53 -0800 Subject: [PATCH 046/308] Add method to get ahead/behind commit counts --- native/v8_extensions/git.mm | 110 ++++++++++++++++++++++++++++++- src/app/git.coffee | 3 + src/stdlib/git-repository.coffee | 1 + 3 files changed, 113 insertions(+), 1 deletion(-) diff --git a/native/v8_extensions/git.mm b/native/v8_extensions/git.mm index 3a8c13885..70a58069e 100644 --- a/native/v8_extensions/git.mm +++ b/native/v8_extensions/git.mm @@ -1,6 +1,7 @@ #import "git.h" #import "include/git2.h" #import +#import namespace v8_extensions { @@ -74,6 +75,106 @@ namespace v8_extensions { return v8Statuses; } + int getCommitCount(const git_oid* fromCommit, const git_oid* toCommit) { + int count = 0; + git_revwalk *revWalk; + if (git_revwalk_new(&revWalk, repo) == GIT_OK) { + git_revwalk_push(revWalk, fromCommit); + git_revwalk_hide(revWalk, toCommit); + git_oid currentCommit; + while (git_revwalk_next(¤tCommit, revWalk) == GIT_OK) + count++; + git_revwalk_free(revWalk); + } + return count; + } + + void getShortBranchName(const char** out, const char* branchName) { + *out = NULL; + if (branchName == NULL) + return; + int branchNameLength = strlen(branchName); + if (branchNameLength < 12) + return; + if (strncmp("refs/heads/", branchName, 11) != 0) + return; + + int shortNameLength = branchNameLength - 11; + char* shortName = (char*) malloc(sizeof(char) * shortNameLength + 1); + shortName[shortNameLength] = '\0'; + strncpy(shortName, &branchName[11], shortNameLength); + *out = shortName; + } + + void getUpstreamBranch(const char** out, git_reference *branch) { + *out = NULL; + git_config *config; + if (git_repository_config(&config, repo) != GIT_OK) + return; + + const char* branchName = git_reference_name(branch); + const char* shortBranchName; + getShortBranchName(&shortBranchName, branchName); + if (shortBranchName == NULL) + return; + + int shortBranchNameLength = strlen(shortBranchName); + char* remoteKey = (char*) malloc(sizeof(char) * shortBranchNameLength + 15); + remoteKey[shortBranchNameLength + 14] = '\0'; + sprintf(remoteKey, "branch.%s.remote", shortBranchName); + char* mergeKey = (char*) malloc(sizeof(char) * shortBranchNameLength + 14); + mergeKey[shortBranchNameLength + 13] = '\0'; + sprintf(mergeKey, "branch.%s.merge", shortBranchName); + free((char*)shortBranchName); + + const char *remote; + const char *merge; + if (git_config_get_string(&remote, config, remoteKey) == GIT_OK + && git_config_get_string(&merge, config, mergeKey) == GIT_OK) { + const char* shortMergeBranchName; + getShortBranchName(&shortMergeBranchName, merge); + if (shortMergeBranchName != NULL) { + int shortMergeBranchNameLength = strlen(shortMergeBranchName); + int updateRefLength = strlen(remote) + shortMergeBranchNameLength + 15; + char* upstreamBranch = (char*) malloc(sizeof(char) * updateRefLength); + sprintf(upstreamBranch, "refs/remotes/%s/%s", remote, shortMergeBranchName); + free((char*)shortMergeBranchName); + *out = upstreamBranch; + } + } + + free(remoteKey); + free(mergeKey); + git_config_free(config); + } + + CefRefPtr GetAheadBehindCounts() { + CefRefPtr result = CefV8Value::CreateObject(NULL); + git_reference *head; + if (git_repository_head(&head, repo) == GIT_OK) { + const char* upstreamBranchName; + getUpstreamBranch(&upstreamBranchName, head); + if (upstreamBranchName != NULL) { + git_reference *upstream; + if (git_reference_lookup(&upstream, repo, upstreamBranchName) == GIT_OK) { + const git_oid* headSha = git_reference_target(head); + const git_oid* upstreamSha = git_reference_target(upstream); + git_oid mergeBase; + if (git_merge_base(&mergeBase, repo, headSha, upstreamSha) == GIT_OK) { + int ahead = getCommitCount(headSha, &mergeBase); + result->SetValue("ahead", CefV8Value::CreateInt(ahead), V8_PROPERTY_ATTRIBUTE_NONE); + int behind = getCommitCount(upstreamSha, &mergeBase); + result->SetValue("behind", CefV8Value::CreateInt(behind), V8_PROPERTY_ATTRIBUTE_NONE); + } + git_reference_free(upstream); + } + free((char*)upstreamBranchName); + } + git_reference_free(head); + } + return result; + } + CefRefPtr IsIgnored(const char *path) { int ignored; if (git_ignore_path_is_ignored(&ignored, repo, path) == GIT_OK) { @@ -209,7 +310,8 @@ namespace v8_extensions { void Git::CreateContextBinding(CefRefPtr context) { const char* methodNames[] = { "getRepository", "getHead", "getPath", "isIgnored", "getStatus", "checkoutHead", - "getDiffStats", "isSubmodule", "refreshIndex", "destroy", "getStatuses" + "getDiffStats", "isSubmodule", "refreshIndex", "destroy", "getStatuses", + "getAheadBehindCounts" }; CefRefPtr nativeObject = CefV8Value::CreateObject(NULL); @@ -302,6 +404,12 @@ namespace v8_extensions { return true; } + if (name == "getAheadBehindCounts") { + GitRepository *userData = (GitRepository *)object->GetUserData().get(); + retval = userData->GetAheadBehindCounts(); + return true; + } + return false; } } diff --git a/src/app/git.coffee b/src/app/git.coffee index f065c209b..e8fa7463b 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -138,5 +138,8 @@ class Git directoryStatus |= status if path.indexOf(directoryPath) is 0 directoryStatus + getAheadBehindCounts: -> + @getRepo().getAheadBehindCounts() + _.extend Git.prototype, Subscriber _.extend Git.prototype, EventEmitter diff --git a/src/stdlib/git-repository.coffee b/src/stdlib/git-repository.coffee index 2386ba868..b842630cc 100644 --- a/src/stdlib/git-repository.coffee +++ b/src/stdlib/git-repository.coffee @@ -17,3 +17,4 @@ class GitRepository isSubmodule: $git.isSubmodule refreshIndex: $git.refreshIndex destroy: $git.destroy + getAheadBehindCounts: $git.getAheadBehindCounts From e3ebda7d300b1adac037cf4007923b00fdd6525c Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 1 Mar 2013 14:01:43 -0800 Subject: [PATCH 047/308] Show commits ahead/behind upstream in status bar --- src/app/git.coffee | 2 ++ src/app/repository-status-handler.coffee | 4 +++- src/app/repository-status-task.coffee | 9 +++++---- src/packages/status-bar/lib/status-bar-view.coffee | 12 ++++++++++++ static/status-bar.css | 14 ++++++++++++++ 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/app/git.coffee b/src/app/git.coffee index e8fa7463b..1a9420cb7 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -27,9 +27,11 @@ class Git ignore: 1 << 14 statuses: null + upstream: null constructor: (path, options={}) -> @statuses = {} + @upstream = {ahead: 0, behind: 0} @repo = GitRepository.open(path) refreshOnWindowFocus = options.refreshOnWindowFocus ? true if refreshOnWindowFocus diff --git a/src/app/repository-status-handler.coffee b/src/app/repository-status-handler.coffee index bd2d2fe70..b01cdf437 100644 --- a/src/app/repository-status-handler.coffee +++ b/src/app/repository-status-handler.coffee @@ -9,8 +9,10 @@ module.exports = statuses = {} for path, status of repo.getRepo().getStatuses() statuses[fs.join(workingDirectoryPath, path)] = status + upstream = repo.getAheadBehindCounts() ? {ahead: 0, behind: 0} repo.destroy() else + upstream = {} statuses = {} - callTaskMethod('statusesLoaded', statuses) + callTaskMethod('statusesLoaded', {statuses, upstream}) diff --git a/src/app/repository-status-task.coffee b/src/app/repository-status-task.coffee index b7535e38a..0e5f746a5 100644 --- a/src/app/repository-status-task.coffee +++ b/src/app/repository-status-task.coffee @@ -9,8 +9,9 @@ class RepositoryStatusTask extends Task started: -> @callWorkerMethod('loadStatuses', @repo.getPath()) - statusesLoaded: (statuses) -> + statusesLoaded: ({statuses, upstream}) -> @done() - unless _.isEqual(statuses, @repo.statuses) - @repo.statuses = statuses - @repo.trigger 'statuses-changed' + statusesUnchanged = _.isEqual(statuses, @repo.statuses) and _.isEqual(upstream, @repo.upstream) + @repo.statuses = statuses + @repo.upstream = upstream + @repo.trigger 'statuses-changed' unless statusesUnchanged diff --git a/src/packages/status-bar/lib/status-bar-view.coffee b/src/packages/status-bar/lib/status-bar-view.coffee index 15a8be49a..cf287e518 100644 --- a/src/packages/status-bar/lib/status-bar-view.coffee +++ b/src/packages/status-bar/lib/status-bar-view.coffee @@ -17,6 +17,8 @@ class StatusBarView extends View @span class: 'git-branch', outlet: 'branchArea', => @span class: 'octicons branch-icon' @span class: 'branch-label', outlet: 'branchLabel' + @span class: 'octicons commits-ahead-label', outlet: 'commitsAhead' + @span class: 'octicons commits-behind-label', outlet: 'commitsBehind' @span class: 'git-status', outlet: 'gitStatusIcon' @span class: 'file-info', => @span class: 'current-path', outlet: 'currentPath' @@ -82,6 +84,16 @@ class StatusBarView extends View @gitStatusIcon.addClass('git-status octicons') return unless git? + if git.upstream.ahead > 0 + @commitsAhead.text(git.upstream.ahead).show() + else + @commitsAhead.hide() + + if git.upstream.behind > 0 + @commitsBehind.text(git.upstream.behind).show() + else + @commitsBehind.hide() + status = git.statuses[path] if git.isStatusModified(status) @gitStatusIcon.addClass('modified-status-icon') diff --git a/static/status-bar.css b/static/status-bar.css index 4cc898c41..d7822f50c 100644 --- a/static/status-bar.css +++ b/static/status-bar.css @@ -55,3 +55,17 @@ .status-bar .new-status-icon:before { content: "\f26b"; } + +.status-bar .commits-behind-label:before { + margin-top: -3px; + margin-left: 3px; + margin-right: 1px; + content: "\f03f"; +} + +.status-bar .commits-ahead-label:before { + margin-top: -3px; + margin-left: 3px; + margin-right: 1px; + content: "\f03d"; +} From ab5043f8900e76ed7a390cd80ad7b69886c1334d Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 1 Mar 2013 16:37:17 -0800 Subject: [PATCH 048/308] Add method to get line diffs for path and text --- native/v8_extensions/git.mm | 71 +++++++++++++++++++++++++++++++- src/app/git.coffee | 3 ++ src/stdlib/git-repository.coffee | 1 + 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/native/v8_extensions/git.mm b/native/v8_extensions/git.mm index 70a58069e..5b306d605 100644 --- a/native/v8_extensions/git.mm +++ b/native/v8_extensions/git.mm @@ -1,7 +1,6 @@ #import "git.h" #import "include/git2.h" #import -#import namespace v8_extensions { @@ -17,6 +16,13 @@ namespace v8_extensions { return 0; } + static int CollectDiffHunk(const git_diff_delta *delta, const git_diff_range *range, + const char *header, size_t header_len, void *payload) { + std::vector *ranges = (std::vector *) payload; + ranges->push_back(*range); + return 0; + } + public: GitRepository(const char *pathInRepo) { if (git_repository_open_ext(&repo, pathInRepo, 0, NULL) != GIT_OK) { @@ -243,6 +249,7 @@ namespace v8_extensions { git_diff_list *diffs; int diffStatus = git_diff_tree_to_workdir(&diffs, repo, tree, &options); + git_tree_free(tree); free(copiedPath); if (diffStatus != GIT_OK || git_diff_num_deltas(diffs) != 1) { return CefV8Value::CreateNull(); @@ -282,6 +289,58 @@ namespace v8_extensions { return result; } + CefRefPtr GetLineDiffs(const char *path, const char *text) { + git_reference *head; + if (git_repository_head(&head, repo) != GIT_OK) + return CefV8Value::CreateNull(); + + const git_oid* sha = git_reference_target(head); + git_commit *commit; + int commitStatus = git_commit_lookup(&commit, repo, sha); + git_reference_free(head); + if (commitStatus != GIT_OK) + return CefV8Value::CreateNull(); + + git_tree *tree; + int treeStatus = git_commit_tree(&tree, commit); + git_commit_free(commit); + if (treeStatus != GIT_OK) + return CefV8Value::CreateNull(); + + git_tree_entry* treeEntry; + git_tree_entry_bypath(&treeEntry, tree, path); + git_blob *blob = NULL; + if (treeEntry != NULL) { + const git_oid *blobSha = git_tree_entry_id(treeEntry); + if (blobSha == NULL || git_blob_lookup(&blob, repo, blobSha) != GIT_OK) + blob = NULL; + } + git_tree_free(tree); + if (blob == NULL) + return CefV8Value::CreateNull(); + + int size = strlen(text); + git_diff_options options = GIT_DIFF_OPTIONS_INIT; + options.context_lines = 1; + std::vector ranges; + if (git_diff_blob_to_buffer(blob, text, size, NULL, NULL, CollectDiffHunk, NULL, &ranges) == GIT_OK) { + CefRefPtr v8Ranges = CefV8Value::CreateArray(ranges.size()); + for(int i = 0; i < ranges.size(); i++) { + CefRefPtr v8Range = CefV8Value::CreateArray(4); + v8Range->SetValue(0, CefV8Value::CreateInt(ranges[i].old_start)); + v8Range->SetValue(1, CefV8Value::CreateInt(ranges[i].old_lines)); + v8Range->SetValue(2, CefV8Value::CreateInt(ranges[i].new_start)); + v8Range->SetValue(3, CefV8Value::CreateInt(ranges[i].new_lines)); + v8Ranges->SetValue(i, v8Range); + } + git_blob_free(blob); + return v8Ranges; + } else { + git_blob_free(blob); + return CefV8Value::CreateNull(); + } + } + CefRefPtr IsSubmodule(const char *path) { BOOL isSubmodule = false; git_index* index; @@ -311,7 +370,7 @@ namespace v8_extensions { const char* methodNames[] = { "getRepository", "getHead", "getPath", "isIgnored", "getStatus", "checkoutHead", "getDiffStats", "isSubmodule", "refreshIndex", "destroy", "getStatuses", - "getAheadBehindCounts" + "getAheadBehindCounts", "getLineDiffs" }; CefRefPtr nativeObject = CefV8Value::CreateObject(NULL); @@ -410,6 +469,14 @@ namespace v8_extensions { return true; } + if (name == "getLineDiffs") { + GitRepository *userData = (GitRepository *)object->GetUserData().get(); + std::string path = arguments[0]->GetStringValue().ToString(); + std::string text = arguments[1]->GetStringValue().ToString(); + retval = userData->GetLineDiffs(path.c_str(), text.c_str()); + return true; + } + return false; } } diff --git a/src/app/git.coffee b/src/app/git.coffee index 1a9420cb7..d5ab39f31 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -143,5 +143,8 @@ class Git getAheadBehindCounts: -> @getRepo().getAheadBehindCounts() + getLineDiffs: (path, text) -> + @getRepo().getLineDiffs(@relativize(path), path) + _.extend Git.prototype, Subscriber _.extend Git.prototype, EventEmitter diff --git a/src/stdlib/git-repository.coffee b/src/stdlib/git-repository.coffee index b842630cc..4a1291e8b 100644 --- a/src/stdlib/git-repository.coffee +++ b/src/stdlib/git-repository.coffee @@ -18,3 +18,4 @@ class GitRepository refreshIndex: $git.refreshIndex destroy: $git.destroy getAheadBehindCounts: $git.getAheadBehindCounts + getLineDiffs: $git.getLineDiffs From 72851486d82e092e45611445c38700018f8e2273 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 1 Mar 2013 16:38:48 -0800 Subject: [PATCH 049/308] :lipstick: --- native/v8_extensions/git.mm | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/native/v8_extensions/git.mm b/native/v8_extensions/git.mm index 5b306d605..a6c379be8 100644 --- a/native/v8_extensions/git.mm +++ b/native/v8_extensions/git.mm @@ -81,7 +81,7 @@ namespace v8_extensions { return v8Statuses; } - int getCommitCount(const git_oid* fromCommit, const git_oid* toCommit) { + int GetCommitCount(const git_oid* fromCommit, const git_oid* toCommit) { int count = 0; git_revwalk *revWalk; if (git_revwalk_new(&revWalk, repo) == GIT_OK) { @@ -95,7 +95,7 @@ namespace v8_extensions { return count; } - void getShortBranchName(const char** out, const char* branchName) { + void GetShortBranchName(const char** out, const char* branchName) { *out = NULL; if (branchName == NULL) return; @@ -120,7 +120,7 @@ namespace v8_extensions { const char* branchName = git_reference_name(branch); const char* shortBranchName; - getShortBranchName(&shortBranchName, branchName); + GetShortBranchName(&shortBranchName, branchName); if (shortBranchName == NULL) return; @@ -138,7 +138,7 @@ namespace v8_extensions { if (git_config_get_string(&remote, config, remoteKey) == GIT_OK && git_config_get_string(&merge, config, mergeKey) == GIT_OK) { const char* shortMergeBranchName; - getShortBranchName(&shortMergeBranchName, merge); + GetShortBranchName(&shortMergeBranchName, merge); if (shortMergeBranchName != NULL) { int shortMergeBranchNameLength = strlen(shortMergeBranchName); int updateRefLength = strlen(remote) + shortMergeBranchNameLength + 15; @@ -167,9 +167,9 @@ namespace v8_extensions { const git_oid* upstreamSha = git_reference_target(upstream); git_oid mergeBase; if (git_merge_base(&mergeBase, repo, headSha, upstreamSha) == GIT_OK) { - int ahead = getCommitCount(headSha, &mergeBase); + int ahead = GetCommitCount(headSha, &mergeBase); result->SetValue("ahead", CefV8Value::CreateInt(ahead), V8_PROPERTY_ATTRIBUTE_NONE); - int behind = getCommitCount(upstreamSha, &mergeBase); + int behind = GetCommitCount(upstreamSha, &mergeBase); result->SetValue("behind", CefV8Value::CreateInt(behind), V8_PROPERTY_ATTRIBUTE_NONE); } git_reference_free(upstream); From 14a73337ce730ab02eae47094aefb6c5d1f7e064 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 1 Mar 2013 16:40:03 -0800 Subject: [PATCH 050/308] Remove unused diff options --- native/v8_extensions/git.mm | 2 -- 1 file changed, 2 deletions(-) diff --git a/native/v8_extensions/git.mm b/native/v8_extensions/git.mm index a6c379be8..9221c842a 100644 --- a/native/v8_extensions/git.mm +++ b/native/v8_extensions/git.mm @@ -320,8 +320,6 @@ namespace v8_extensions { return CefV8Value::CreateNull(); int size = strlen(text); - git_diff_options options = GIT_DIFF_OPTIONS_INIT; - options.context_lines = 1; std::vector ranges; if (git_diff_blob_to_buffer(blob, text, size, NULL, NULL, CollectDiffHunk, NULL, &ranges) == GIT_OK) { CefRefPtr v8Ranges = CefV8Value::CreateArray(ranges.size()); From 1be0b817de4e32cececb438c93597f51a4505ac1 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 1 Mar 2013 16:40:51 -0800 Subject: [PATCH 051/308] Specify text as second parameter --- src/app/git.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/git.coffee b/src/app/git.coffee index d5ab39f31..d1f213c28 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -144,7 +144,7 @@ class Git @getRepo().getAheadBehindCounts() getLineDiffs: (path, text) -> - @getRepo().getLineDiffs(@relativize(path), path) + @getRepo().getLineDiffs(@relativize(path), text) _.extend Git.prototype, Subscriber _.extend Git.prototype, EventEmitter From c14aa3b86f4f00492cb84ec08848a16b7d8fb68b Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 1 Mar 2013 18:42:41 -0800 Subject: [PATCH 052/308] Default status to 0 if undefined This keeps the status-changed event from triggering the first time the status is requested as it goes from undefined to 0 which should be treated as no change. --- spec/app/git-spec.coffee | 12 +++++------- src/app/git.coffee | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/spec/app/git-spec.coffee b/spec/app/git-spec.coffee index 27a72d694..36ca195e1 100644 --- a/spec/app/git-spec.coffee +++ b/spec/app/git-spec.coffee @@ -127,9 +127,10 @@ describe "Git", -> expect(repo.isPathModified(path2)).toBeTruthy() it "fires a status-changed event if the checkout completes successfully", -> + fs.write(path1, '') + repo.getPathStatus(path1) statusHandler = jasmine.createSpy('statusHandler') repo.on 'status-changed', statusHandler - fs.write(path1, '') repo.checkoutHead(path1) expect(statusHandler.callCount).toBe 1 expect(statusHandler.argsForCall[0][0..1]).toEqual [path1, 0] @@ -173,17 +174,14 @@ describe "Git", -> it "trigger a status-changed event when the new status differs from the last cached one", -> statusHandler = jasmine.createSpy("statusHandler") repo.on 'status-changed', statusHandler + fs.write(path, '') status = repo.getPathStatus(path) expect(statusHandler.callCount).toBe 1 expect(statusHandler.argsForCall[0][0..1]).toEqual [path, status] - fs.write(path, '') + fs.write(path, 'abc') status = repo.getPathStatus(path) - expect(statusHandler.callCount).toBe 2 - expect(statusHandler.argsForCall[1][0..1]).toEqual [path, status] - - repo.getPathStatus(path) - expect(statusHandler.callCount).toBe 2 + expect(statusHandler.callCount).toBe 1 describe ".refreshStatus()", -> [newPath, modifiedPath, cleanPath, originalModifiedPathText] = [] diff --git a/src/app/git.coffee b/src/app/git.coffee index d1f213c28..427017239 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -70,8 +70,8 @@ class Git @getRepo().getHead() ? '' getPathStatus: (path) -> - currentPathStatus = @statuses[path] - pathStatus = @getRepo().getStatus(@relativize(path)) + currentPathStatus = @statuses[path] ? 0 + pathStatus = @getRepo().getStatus(@relativize(path)) ? 0 if pathStatus > 0 @statuses[path] = pathStatus else From c9ef846727897b4c75be15622404c1d505364f55 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 1 Mar 2013 19:08:31 -0800 Subject: [PATCH 053/308] Only replace .git segment if trailing --- src/app/git.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/git.coffee b/src/app/git.coffee index 427017239..bece56306 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -64,7 +64,7 @@ class Git @unsubscribe() getWorkingDirectory: -> - @getPath()?.replace(/\/\.git\/?/, '') + @getPath()?.replace(/\/\.git\/?$/, '') getHead: -> @getRepo().getHead() ? '' From 885da83df2aa852ca0a063d89cf54939637d1b10 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Sun, 3 Mar 2013 11:17:05 -0800 Subject: [PATCH 054/308] :lipstick: --- native/v8_extensions/git.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/native/v8_extensions/git.mm b/native/v8_extensions/git.mm index 9221c842a..3c870983f 100644 --- a/native/v8_extensions/git.mm +++ b/native/v8_extensions/git.mm @@ -112,7 +112,7 @@ namespace v8_extensions { *out = shortName; } - void getUpstreamBranch(const char** out, git_reference *branch) { + void GetUpstreamBranch(const char** out, git_reference *branch) { *out = NULL; git_config *config; if (git_repository_config(&config, repo) != GIT_OK) @@ -159,7 +159,7 @@ namespace v8_extensions { git_reference *head; if (git_repository_head(&head, repo) == GIT_OK) { const char* upstreamBranchName; - getUpstreamBranch(&upstreamBranchName, head); + GetUpstreamBranch(&upstreamBranchName, head); if (upstreamBranchName != NULL) { git_reference *upstream; if (git_reference_lookup(&upstream, repo, upstreamBranchName) == GIT_OK) { From 060ed27a2fa9c3e4a20c97aa57b0088e679e7535 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Sun, 3 Mar 2013 11:22:46 -0800 Subject: [PATCH 055/308] Set context lines to 1 for line diffs --- native/v8_extensions/git.mm | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/native/v8_extensions/git.mm b/native/v8_extensions/git.mm index 3c870983f..88252769c 100644 --- a/native/v8_extensions/git.mm +++ b/native/v8_extensions/git.mm @@ -321,14 +321,16 @@ namespace v8_extensions { int size = strlen(text); std::vector ranges; - if (git_diff_blob_to_buffer(blob, text, size, NULL, NULL, CollectDiffHunk, NULL, &ranges) == GIT_OK) { + git_diff_options options = GIT_DIFF_OPTIONS_INIT; + options.context_lines = 1; + if (git_diff_blob_to_buffer(blob, text, size, &options, NULL, CollectDiffHunk, NULL, &ranges) == GIT_OK) { CefRefPtr v8Ranges = CefV8Value::CreateArray(ranges.size()); for(int i = 0; i < ranges.size(); i++) { - CefRefPtr v8Range = CefV8Value::CreateArray(4); - v8Range->SetValue(0, CefV8Value::CreateInt(ranges[i].old_start)); - v8Range->SetValue(1, CefV8Value::CreateInt(ranges[i].old_lines)); - v8Range->SetValue(2, CefV8Value::CreateInt(ranges[i].new_start)); - v8Range->SetValue(3, CefV8Value::CreateInt(ranges[i].new_lines)); + CefRefPtr v8Range = CefV8Value::CreateObject(NULL); + v8Range->SetValue("oldStart", CefV8Value::CreateInt(ranges[i].old_start), V8_PROPERTY_ATTRIBUTE_NONE); + v8Range->SetValue("oldLines", CefV8Value::CreateInt(ranges[i].old_lines), V8_PROPERTY_ATTRIBUTE_NONE); + v8Range->SetValue("newStart", CefV8Value::CreateInt(ranges[i].new_start), V8_PROPERTY_ATTRIBUTE_NONE); + v8Range->SetValue("newLines", CefV8Value::CreateInt(ranges[i].new_lines), V8_PROPERTY_ATTRIBUTE_NONE); v8Ranges->SetValue(i, v8Range); } git_blob_free(blob); From 165dffc15d9e0789ffeea7572348290fde17780a Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Sun, 3 Mar 2013 11:23:49 -0800 Subject: [PATCH 056/308] Always return array from Git.getLineDiffs() --- src/app/git.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/git.coffee b/src/app/git.coffee index bece56306..0d47cefb5 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -144,7 +144,7 @@ class Git @getRepo().getAheadBehindCounts() getLineDiffs: (path, text) -> - @getRepo().getLineDiffs(@relativize(path), text) + @getRepo().getLineDiffs(@relativize(path), text) ? [] _.extend Git.prototype, Subscriber _.extend Git.prototype, EventEmitter From a1570a77e0767eba0a04afad3a3fbe3a969501a7 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Sun, 3 Mar 2013 11:29:41 -0800 Subject: [PATCH 057/308] Add back saved event handler --- src/packages/status-bar/lib/status-bar-view.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/packages/status-bar/lib/status-bar-view.coffee b/src/packages/status-bar/lib/status-bar-view.coffee index cf287e518..c681fe4ea 100644 --- a/src/packages/status-bar/lib/status-bar-view.coffee +++ b/src/packages/status-bar/lib/status-bar-view.coffee @@ -48,6 +48,7 @@ class StatusBarView extends View @buffer?.off '.status-bar' @buffer = @editor.getBuffer() @buffer.on 'contents-modified.status-bar', (e) => @updateBufferHasModifiedText(e.differsFromDisk) + @buffer.on 'saved.status-bar', => @updateStatusBar() @updateStatusBar() updateStatusBar: -> From 8b7e3c10928eda4418d85bd34fee4ac4d9b8d2e1 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Sun, 3 Mar 2013 11:55:37 -0800 Subject: [PATCH 058/308] Remove unused imports --- src/app/gutter.coffee | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/app/gutter.coffee b/src/app/gutter.coffee index b541595b2..19b46ca0c 100644 --- a/src/app/gutter.coffee +++ b/src/app/gutter.coffee @@ -1,9 +1,5 @@ {View, $$, $$$} = require 'space-pen' - -$ = require 'jquery' -_ = require 'underscore' Range = require 'range' -Point = require 'point' module.exports = class Gutter extends View From 673e8c948fe7866c1204ab77c02e2c73f5a356c4 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 4 Mar 2013 19:05:55 -0800 Subject: [PATCH 059/308] Prevent repository config from leaking --- native/v8_extensions/git.mm | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/native/v8_extensions/git.mm b/native/v8_extensions/git.mm index 88252769c..c1e835992 100644 --- a/native/v8_extensions/git.mm +++ b/native/v8_extensions/git.mm @@ -114,9 +114,6 @@ namespace v8_extensions { void GetUpstreamBranch(const char** out, git_reference *branch) { *out = NULL; - git_config *config; - if (git_repository_config(&config, repo) != GIT_OK) - return; const char* branchName = git_reference_name(branch); const char* shortBranchName; @@ -133,6 +130,10 @@ namespace v8_extensions { sprintf(mergeKey, "branch.%s.merge", shortBranchName); free((char*)shortBranchName); + git_config *config; + if (git_repository_config(&config, repo) != GIT_OK) + return; + const char *remote; const char *merge; if (git_config_get_string(&remote, config, remoteKey) == GIT_OK From 5005aa0c7c8696439009b73d928c708428736045 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 4 Mar 2013 19:10:01 -0800 Subject: [PATCH 060/308] Add null terminator to upstream branch string --- native/v8_extensions/git.mm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/native/v8_extensions/git.mm b/native/v8_extensions/git.mm index c1e835992..8b06e9962 100644 --- a/native/v8_extensions/git.mm +++ b/native/v8_extensions/git.mm @@ -141,9 +141,9 @@ namespace v8_extensions { const char* shortMergeBranchName; GetShortBranchName(&shortMergeBranchName, merge); if (shortMergeBranchName != NULL) { - int shortMergeBranchNameLength = strlen(shortMergeBranchName); - int updateRefLength = strlen(remote) + shortMergeBranchNameLength + 15; - char* upstreamBranch = (char*) malloc(sizeof(char) * updateRefLength); + int updateBranchLength = strlen(remote) + strlen(shortMergeBranchName) + 14; + char* upstreamBranch = (char*) malloc(sizeof(char) * (updateBranchLength + 1)); + upstreamBranch[updateBranchLength] = '\0'; sprintf(upstreamBranch, "refs/remotes/%s/%s", remote, shortMergeBranchName); free((char*)shortMergeBranchName); *out = upstreamBranch; From 11b5fc14dc57b42e1f977e36b3aa9651edc6aa80 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 4 Mar 2013 19:16:15 -0800 Subject: [PATCH 061/308] Check that remote length is non-zero --- native/v8_extensions/git.mm | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/native/v8_extensions/git.mm b/native/v8_extensions/git.mm index 8b06e9962..721b1436f 100644 --- a/native/v8_extensions/git.mm +++ b/native/v8_extensions/git.mm @@ -138,15 +138,18 @@ namespace v8_extensions { const char *merge; if (git_config_get_string(&remote, config, remoteKey) == GIT_OK && git_config_get_string(&merge, config, mergeKey) == GIT_OK) { - const char* shortMergeBranchName; - GetShortBranchName(&shortMergeBranchName, merge); - if (shortMergeBranchName != NULL) { - int updateBranchLength = strlen(remote) + strlen(shortMergeBranchName) + 14; - char* upstreamBranch = (char*) malloc(sizeof(char) * (updateBranchLength + 1)); - upstreamBranch[updateBranchLength] = '\0'; - sprintf(upstreamBranch, "refs/remotes/%s/%s", remote, shortMergeBranchName); + int remoteLength = strlen(remote); + if (remoteLength > 0) { + const char *shortMergeBranchName; + GetShortBranchName(&shortMergeBranchName, merge); + if (shortMergeBranchName != NULL) { + int updateBranchLength = remoteLength + strlen(shortMergeBranchName) + 14; + char* upstreamBranch = (char*) malloc(sizeof(char) * (updateBranchLength + 1)); + sprintf(upstreamBranch, "refs/remotes/%s/%s", remote, shortMergeBranchName); + upstreamBranch[updateBranchLength] = '\0'; + *out = upstreamBranch; + } free((char*)shortMergeBranchName); - *out = upstreamBranch; } } From a389d572193ba0a6ab22b5f7cff2235fc6da2883 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 4 Mar 2013 19:19:38 -0800 Subject: [PATCH 062/308] Remove unneeded null terminators These are already added by sprintf --- native/v8_extensions/git.mm | 3 --- 1 file changed, 3 deletions(-) diff --git a/native/v8_extensions/git.mm b/native/v8_extensions/git.mm index 721b1436f..184558e61 100644 --- a/native/v8_extensions/git.mm +++ b/native/v8_extensions/git.mm @@ -123,10 +123,8 @@ namespace v8_extensions { int shortBranchNameLength = strlen(shortBranchName); char* remoteKey = (char*) malloc(sizeof(char) * shortBranchNameLength + 15); - remoteKey[shortBranchNameLength + 14] = '\0'; sprintf(remoteKey, "branch.%s.remote", shortBranchName); char* mergeKey = (char*) malloc(sizeof(char) * shortBranchNameLength + 14); - mergeKey[shortBranchNameLength + 13] = '\0'; sprintf(mergeKey, "branch.%s.merge", shortBranchName); free((char*)shortBranchName); @@ -146,7 +144,6 @@ namespace v8_extensions { int updateBranchLength = remoteLength + strlen(shortMergeBranchName) + 14; char* upstreamBranch = (char*) malloc(sizeof(char) * (updateBranchLength + 1)); sprintf(upstreamBranch, "refs/remotes/%s/%s", remote, shortMergeBranchName); - upstreamBranch[updateBranchLength] = '\0'; *out = upstreamBranch; } free((char*)shortMergeBranchName); From 60c2829af4264374d842b09a175d263a28a90a21 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 4 Mar 2013 21:47:21 -0800 Subject: [PATCH 063/308] Move ahead/behind defaults to Git class --- src/app/git.coffee | 2 +- src/app/repository-status-handler.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/git.coffee b/src/app/git.coffee index 0d47cefb5..761084c57 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -141,7 +141,7 @@ class Git directoryStatus getAheadBehindCounts: -> - @getRepo().getAheadBehindCounts() + @getRepo().getAheadBehindCounts() ? ahead: 0, behind: 0 getLineDiffs: (path, text) -> @getRepo().getLineDiffs(@relativize(path), text) ? [] diff --git a/src/app/repository-status-handler.coffee b/src/app/repository-status-handler.coffee index b01cdf437..5503396ce 100644 --- a/src/app/repository-status-handler.coffee +++ b/src/app/repository-status-handler.coffee @@ -9,7 +9,7 @@ module.exports = statuses = {} for path, status of repo.getRepo().getStatuses() statuses[fs.join(workingDirectoryPath, path)] = status - upstream = repo.getAheadBehindCounts() ? {ahead: 0, behind: 0} + upstream = repo.getAheadBehindCounts() repo.destroy() else upstream = {} From eb00623807d304207e6061bc487876cc816c477b Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 4 Mar 2013 21:56:14 -0800 Subject: [PATCH 064/308] Return null when no merge base is found --- native/v8_extensions/git.mm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/native/v8_extensions/git.mm b/native/v8_extensions/git.mm index 184558e61..488c9d506 100644 --- a/native/v8_extensions/git.mm +++ b/native/v8_extensions/git.mm @@ -156,7 +156,6 @@ namespace v8_extensions { } CefRefPtr GetAheadBehindCounts() { - CefRefPtr result = CefV8Value::CreateObject(NULL); git_reference *head; if (git_repository_head(&head, repo) == GIT_OK) { const char* upstreamBranchName; @@ -168,10 +167,12 @@ namespace v8_extensions { const git_oid* upstreamSha = git_reference_target(upstream); git_oid mergeBase; if (git_merge_base(&mergeBase, repo, headSha, upstreamSha) == GIT_OK) { + CefRefPtr result = CefV8Value::CreateObject(NULL); int ahead = GetCommitCount(headSha, &mergeBase); result->SetValue("ahead", CefV8Value::CreateInt(ahead), V8_PROPERTY_ATTRIBUTE_NONE); int behind = GetCommitCount(upstreamSha, &mergeBase); result->SetValue("behind", CefV8Value::CreateInt(behind), V8_PROPERTY_ATTRIBUTE_NONE); + return result; } git_reference_free(upstream); } @@ -179,7 +180,8 @@ namespace v8_extensions { } git_reference_free(head); } - return result; + + return CefV8Value::CreateNull(); } CefRefPtr IsIgnored(const char *path) { From 464aed92cb130fe2afd04a0731b47932a728dab3 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 4 Mar 2013 22:02:56 -0800 Subject: [PATCH 065/308] Don't return before freeing references --- native/v8_extensions/git.mm | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/native/v8_extensions/git.mm b/native/v8_extensions/git.mm index 488c9d506..960b44da2 100644 --- a/native/v8_extensions/git.mm +++ b/native/v8_extensions/git.mm @@ -156,6 +156,7 @@ namespace v8_extensions { } CefRefPtr GetAheadBehindCounts() { + CefRefPtr result = NULL; git_reference *head; if (git_repository_head(&head, repo) == GIT_OK) { const char* upstreamBranchName; @@ -167,12 +168,11 @@ namespace v8_extensions { const git_oid* upstreamSha = git_reference_target(upstream); git_oid mergeBase; if (git_merge_base(&mergeBase, repo, headSha, upstreamSha) == GIT_OK) { - CefRefPtr result = CefV8Value::CreateObject(NULL); + result = CefV8Value::CreateObject(NULL); int ahead = GetCommitCount(headSha, &mergeBase); result->SetValue("ahead", CefV8Value::CreateInt(ahead), V8_PROPERTY_ATTRIBUTE_NONE); int behind = GetCommitCount(upstreamSha, &mergeBase); result->SetValue("behind", CefV8Value::CreateInt(behind), V8_PROPERTY_ATTRIBUTE_NONE); - return result; } git_reference_free(upstream); } @@ -181,7 +181,10 @@ namespace v8_extensions { git_reference_free(head); } - return CefV8Value::CreateNull(); + if (result != NULL) + return result; + else + return CefV8Value::CreateNull(); } CefRefPtr IsIgnored(const char *path) { From 887e285cce0ddf91255304904be6d77730558da8 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 4 Mar 2013 22:07:22 -0800 Subject: [PATCH 066/308] :lipstick: --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1af29105f..715e0b5d4 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,14 @@ Check out our [documentation on the docs tab](https://github.com/github/atom/doc ## Building from source -Requirements +### Requirements -**Mountain Lion** + * Mountain Lion + * The Setup™ or Boxen + * Xcode (available in the App Store) -**The Setup™** +### Installation -**Xcode** (Get Xcode from the App Store (ugh, I know)) + 1. `gh-setup atom` -1. gh-setup atom - -2. cd ~/github/atom && `rake install` + 2. `cd ~/github/atom && rake install` From 5ddea28d809f2be549b57baf2d9918a1753b9e68 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 5 Mar 2013 08:35:09 -0800 Subject: [PATCH 067/308] Wrap objects inside arrays in {} --- spec/stdlib/cson-spec.coffee | 4 ++++ src/stdlib/cson.coffee | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/spec/stdlib/cson-spec.coffee b/spec/stdlib/cson-spec.coffee index 361eaf330..1ed14b3a9 100644 --- a/spec/stdlib/cson-spec.coffee +++ b/spec/stdlib/cson-spec.coffee @@ -62,6 +62,10 @@ describe "CSON", -> it "formats the undefined value as null", -> expect(CSON.stringify(['a', undefined, 'b'])).toBe "[\n 'a'\n null\n 'b'\n]" + describe "when the array contains an object", -> + it "wraps the object in {}", -> + expect(CSON.stringify([{a:'b', a1: 'b1'}, {c: 'd'}])).toBe "[\n {\n 'a': 'b'\n 'a1': 'b1'\n }\n {\n 'c': 'd'\n }\n]" + describe "when formatting an object", -> describe "when the object is empty", -> it "returns the empty string", -> diff --git a/src/stdlib/cson.coffee b/src/stdlib/cson.coffee index 63158f006..b2797d7c4 100644 --- a/src/stdlib/cson.coffee +++ b/src/stdlib/cson.coffee @@ -21,7 +21,8 @@ module.exports = cson = '[\n' for value in array - cson += @stringifyIndent(indentLevel + 2) + indent = @stringifyIndent(indentLevel + 2) + cson += indent if _.isString(value) cson += @stringifyString(value) else if _.isBoolean(value) @@ -33,7 +34,7 @@ module.exports = else if _.isArray(value) cson += @stringifyArray(value, indentLevel + 2) else if _.isObject(value) - cson += @stringifyObject(value, indentLevel + 2) + cson += "{\n#{@stringifyObject(value, indentLevel + 4)}\n#{indent}}" else throw new Error("Unrecognized type for array value: #{value}") cson += '\n' From 1c09a1352beb1987da6b7b3e09480df9c1ad1b24 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 5 Mar 2013 08:46:17 -0800 Subject: [PATCH 068/308] Return {} when object is empty --- spec/stdlib/cson-spec.coffee | 4 ++-- src/stdlib/cson.coffee | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/stdlib/cson-spec.coffee b/spec/stdlib/cson-spec.coffee index 1ed14b3a9..a1207203c 100644 --- a/spec/stdlib/cson-spec.coffee +++ b/spec/stdlib/cson-spec.coffee @@ -68,8 +68,8 @@ describe "CSON", -> describe "when formatting an object", -> describe "when the object is empty", -> - it "returns the empty string", -> - expect(CSON.stringify({})).toBe "" + it "returns {}", -> + expect(CSON.stringify({})).toBe "{}" it "returns formatted CSON", -> expect(CSON.stringify(a: {b: 'c'})).toBe "'a':\n 'b': 'c'" diff --git a/src/stdlib/cson.coffee b/src/stdlib/cson.coffee index b2797d7c4..382d8600a 100644 --- a/src/stdlib/cson.coffee +++ b/src/stdlib/cson.coffee @@ -41,6 +41,8 @@ module.exports = "#{cson}#{@stringifyIndent(indentLevel)}]" stringifyObject: (object, indentLevel=0) -> + return '{}' if _.isEmpty(object) + cson = '' prefix = '' for key, value of object From 8c1ec19797da12a32b69debb3e5961dbaf436ae6 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Kevin Sawicki Date: Tue, 5 Mar 2013 09:24:41 -0800 Subject: [PATCH 069/308] Always destroy misspelling markers --- .../spell-check/lib/misspelling-view.coffee | 5 +++++ .../spell-check/spec/spell-check-spec.coffee | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/packages/spell-check/lib/misspelling-view.coffee b/src/packages/spell-check/lib/misspelling-view.coffee index eeb537671..ba57e4164 100644 --- a/src/packages/spell-check/lib/misspelling-view.coffee +++ b/src/packages/spell-check/lib/misspelling-view.coffee @@ -39,6 +39,11 @@ class MisspellingView extends View getScreenRange: -> new Range(@startPosition, @endPosition) + unsubscribe: -> + super + + @editSession.destroyMarker(@marker) + containsCursor: -> cursor = @editor.getCursorScreenPosition() @getScreenRange().containsPoint(cursor, exclusive: false) diff --git a/src/packages/spell-check/spec/spell-check-spec.coffee b/src/packages/spell-check/spec/spell-check-spec.coffee index abdb98ac3..bf3fba917 100644 --- a/src/packages/spell-check/spec/spell-check-spec.coffee +++ b/src/packages/spell-check/spec/spell-check-spec.coffee @@ -96,3 +96,19 @@ describe "Spell check", -> expect(editor.find('.corrections').length).toBe 1 expect(editor.find('.corrections li').length).toBe 0 expect(editor.find('.corrections .error').text()).toBe "No corrections found" + + describe "when the edit session is destroyed", -> + it "destroys all misspelling markers", -> + editor.setText("mispelling") + config.set('spell-check.grammars', ['source.js']) + + waitsFor -> + editor.find('.misspelling').length > 0 + + runs -> + expect(editor.find('.misspelling').length).toBe 1 + view = editor.find('.misspelling').view() + buffer = editor.getBuffer() + expect(buffer.getMarkerPosition(view.marker)).not.toBeUndefined() + editor.destroyEditSessions() + expect(buffer.getMarkerPosition(view.marker)).toBeUndefined() From 17fc679b252be1937db9fc5ed35128e1c4cf2be4 Mon Sep 17 00:00:00 2001 From: probablycorey Date: Tue, 5 Mar 2013 09:43:38 -0800 Subject: [PATCH 070/308] Trigger resize event on editor before wrap guide specs --- spec/app/editor-spec.coffee | 2 +- src/packages/wrap-guide/spec/wrap-guide-spec.coffee | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index 0d46cfba9..06eaa4a5a 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -9,7 +9,7 @@ $ = require 'jquery' _ = require 'underscore' fs = require 'fs' -describe "Editor", -> +fdescribe "Editor", -> [buffer, editor, cachedLineHeight] = [] getLineHeight = -> diff --git a/src/packages/wrap-guide/spec/wrap-guide-spec.coffee b/src/packages/wrap-guide/spec/wrap-guide-spec.coffee index 391fe54ae..5944b7692 100644 --- a/src/packages/wrap-guide/spec/wrap-guide-spec.coffee +++ b/src/packages/wrap-guide/spec/wrap-guide-spec.coffee @@ -11,6 +11,7 @@ describe "WrapGuide", -> editor = rootView.getActiveEditor() wrapGuide = rootView.find('.wrap-guide').view() editor.width(editor.charWidth * wrapGuide.getDefaultColumn() * 2) + editor.trigger 'resize' describe "@initialize", -> it "appends a wrap guide to all existing and new editors", -> From 37e4091723c51abc89f7ce547e3c5b7b6ad9f0ae Mon Sep 17 00:00:00 2001 From: probablycorey Date: Tue, 5 Mar 2013 10:13:18 -0800 Subject: [PATCH 071/308] :shit: --- spec/app/editor-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index 06eaa4a5a..0d46cfba9 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -9,7 +9,7 @@ $ = require 'jquery' _ = require 'underscore' fs = require 'fs' -fdescribe "Editor", -> +describe "Editor", -> [buffer, editor, cachedLineHeight] = [] getLineHeight = -> From 94099358f3abe5a413b3bdd4502c0bc366638803 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Kevin Sawicki Date: Tue, 5 Mar 2013 10:19:24 -0800 Subject: [PATCH 072/308] Inserting whitespace never auto-outdents Closes #340 Shout out to @nathansobo --- spec/app/edit-session-spec.coffee | 21 +++++++++++++++------ src/app/selection.coffee | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 51a1fa12f..fce910c73 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -793,12 +793,21 @@ describe "EditSession", -> expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) describe "when the preceding does not match an auto-indent pattern", -> - it "auto-decreases the indentation of the line to be one level below that of the preceding line", -> - editSession.setCursorBufferPosition([3, Infinity]) - editSession.insertText('\n', autoIndent: true) - expect(editSession.indentationForBufferRow(4)).toBe editSession.indentationForBufferRow(3) - editSession.insertText(' }', autoIndent: true) - expect(editSession.indentationForBufferRow(4)).toBe editSession.indentationForBufferRow(3) - 1 + describe "when the inserted text is whitespace", -> + it "does not auto-decreases the indentation", -> + editSession.setCursorBufferPosition([12, 0]) + editSession.insertText(' ', autoIndent: true) + expect(editSession.lineForBufferRow(12)).toBe ' };' + editSession.insertText('\t\t', autoIndent: true) + expect(editSession.lineForBufferRow(12)).toBe ' \t\t};' + + describe "when the inserted text is non-whitespace", -> + it "auto-decreases the indentation of the line to be one level below that of the preceding line", -> + editSession.setCursorBufferPosition([3, Infinity]) + editSession.insertText('\n', autoIndent: true) + expect(editSession.indentationForBufferRow(4)).toBe editSession.indentationForBufferRow(3) + editSession.insertText(' }', autoIndent: true) + expect(editSession.indentationForBufferRow(4)).toBe editSession.indentationForBufferRow(3) - 1 describe "when the current line does not match an auto-outdent pattern", -> it "leaves the line unchanged", -> diff --git a/src/app/selection.coffee b/src/app/selection.coffee index 62a513984..9377498a2 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -164,7 +164,7 @@ class Selection if options.autoIndent if text == '\n' @editSession.autoIndentBufferRow(newBufferRange.end.row) - else + else if /\S/.test(text) @editSession.autoDecreaseIndentForRow(newBufferRange.start.row) newBufferRange From 7eee81cd6a92eb5cc97e97ef6b1f6d6867531645 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Kevin Sawicki Date: Tue, 5 Mar 2013 10:44:15 -0800 Subject: [PATCH 073/308] Return early when clipping tokenless screenline Closes #337 --- spec/app/display-buffer-spec.coffee | 8 ++++++++ src/app/screen-line.coffee | 2 ++ 2 files changed, 10 insertions(+) diff --git a/spec/app/display-buffer-spec.coffee b/spec/app/display-buffer-spec.coffee index 6f87a7ddb..fe5dc83f7 100644 --- a/spec/app/display-buffer-spec.coffee +++ b/spec/app/display-buffer-spec.coffee @@ -1,5 +1,6 @@ DisplayBuffer = require 'display-buffer' Buffer = require 'buffer' +_ = require 'underscore' describe "DisplayBuffer", -> [editSession, displayBuffer, buffer, changeHandler, tabLength] = [] @@ -55,6 +56,13 @@ describe "DisplayBuffer", -> describe "when the buffer changes", -> describe "when buffer lines are updated", -> + describe "when whitespace is added after the max line length", -> + it "adds whitespace to the end of the current line and wraps an empty line", -> + fiftyCharacters = _.multiplyString("x", 50) + editSession.buffer.setText(fiftyCharacters) + editSession.setCursorBufferPosition([0, 51]) + editSession.insertText(" ") + describe "when the update makes a soft-wrapped line shorter than the max line length", -> it "rewraps the line and emits a change event", -> buffer.delete([[6, 24], [6, 42]]) diff --git a/src/app/screen-line.coffee b/src/app/screen-line.coffee index 0c8822499..739d23eff 100644 --- a/src/app/screen-line.coffee +++ b/src/app/screen-line.coffee @@ -12,6 +12,8 @@ class ScreenLine new ScreenLine({@tokens, @ruleStack, @bufferRows, @startBufferColumn, @fold}) clipScreenColumn: (column, options={}) -> + return 0 if @tokens.length == 0 + { skipAtomicTokens } = options column = Math.min(column, @getMaxScreenColumn()) From 91347f14f2ff2e64dc71afd1040bca0f5b4009dc Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 5 Mar 2013 14:27:19 -0800 Subject: [PATCH 074/308] Build libgit2 with thread safe option enabled --- git2/frameworks/libgit2.0.17.0.dylib | Bin 1047484 -> 1048612 bytes native/v8_extensions/git.mm | 1 + script/update-libgit2 | 3 ++- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/git2/frameworks/libgit2.0.17.0.dylib b/git2/frameworks/libgit2.0.17.0.dylib index f76d63a47b15f721880b4ee53ccbd3d8330ee392..96d777c4d3ef8a1c52869bda9857384183fc6ce6 100755 GIT binary patch delta 267855 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¥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 M*?9U<&+2CX1y~<5)&Kwi delta 263456 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 oQ0o+{;hBmAYL z0e#&|lw^+!Zp_rJlF|zuU0+G@?K$RxEZxT%cwWM9bdZ;&mg=n@GQmCR$$xk$Wf;sB zz9H3O`mDV=FxsG4CoN;~*OAdQVk$XTR%+?yQeb7Jo;Y%jx>Z)ZT^pknY2GO_y_iBX zD=Uqw^#UW)@9XU^7A~vVi@vU`)YZ+Q%axVzt`B&v0P-}@Pt4$nmK35c&se3)ET&#< zD}Z3_gtf&|n_%e)D zGnp?9=x&ta6)Jm8Wo%Q?NYnum`EnMcdi$Wa%<}B*&sPu81g#-B$=A6E?!%=xJ zJrxx~@%b=%Uga^0UD9|s6eEfVH*gzvQ*tX2tlbvLlHjM&fJxQC%h7JJ#MXhs=#PzK z4d$YfV;QY8KEj$anU|$Irz_Zu6^{pagebX+(nyy|pH@-oL`?+!{*>}D`&2_YskxtW z$)>0e)Zzfe9)3RNq+VQ^%9;9~rt&IEsO}32by6lqO@TLz8m?-;vI}vEEY*|;v z8%k}HJ_;3JcwFQ=a_&r}^G=GFn17lcI4M2!m`qJsJB=ddjNKnnm#PR(XF5sqsw#sW zXR0`CMfa;JA-b=qy0a2mBZZq{Cc2-W*vg*{AJNrvs<5tlpQa&S)rx$5ai4{=5+uq- z&;@5DOgEO^I4dy?$GwK5)C)UY?u)we84m5}m*1z3RX0ril4*1`Wx1Ch#Cv_`yt*vq zCzhG>YO3$G$<0Mc6rB&!co$`~mMdT8LfE2wHCKLZvbrco#f~D{p;us>IhTs{O19XT zM%`VNmJQV4d+bH@k76xxHj$GEXs_TiI7jCKag{u-&btp1maI&ps3kd4!p5E)R zS|Wk{4}6){mlR>H7Q8U!tRKI>YjvfC&O2b-KXb&O3YjBd9IdXQ1p3_?%Nbyt^|qGR z-)VVnjisA4lq7NcDD`$zQblyVBI>S|Ok; zN5}`}4AL&o-s0SxGaMPG!-wbq zN{AO8pv38-nh^b3tS{@+h~qoI{a*%KTbK z56I5n>?A4GxKi_>K?vnbeo8~}+hls>rv!-nv*hQm_=|JLsl7iI)cvW{*B{7yw4CPo zW2OJ{2m0Dy@vOCFx!euI&+^3u^tpUj0d3qe#D%v$R-HK?P^s(zynw+Wjs=w^Xam!6bgNAVPiYv@EB z#YcGdpkM1K%>&!);ekrqgZRH{^zoR9faDp}S7oJjw5wzqPoxfYm7wrF>i{$Mg>%#k zEYynA-)FgTk!9cKjCyq`kW2fOWLjBQX)T8BrptAe;Ph0q1b&hI5ddX~X3KXO>0u8} zMA)k^yu`pKuKWbnZpjF2qK+nK@1PdX%I1ZUKlTcF`!TaGqfeg&EUF(XvlPOD3rdcb zA6;>^HNFBu96Od-_GQtcdP>Wx)?E_KZdRR<&eT)t_Bw+E7LgeWL`QlT3efeG-JCrs z`kxTs!@fQYK@u#oQKF)Cd|20qV(Kft=^?Tty6b8knWT1is(lG8Mr2Dp*H3F&!$kqTYk`I&r>TkLl0|jI$7nYK5LO3-Kej)h;_T@QGLZT{awUP zl>@j5%umLCeBCVq%MU;%-@yOW{FE-QJcDG?Bwc zlhnel;{NY~q-y3gE3?{343d-+G6$)0LO6Q^+I(5xI{v(8#R%CUJgA>+&5ia2DIe6_ z$RscCwYP4Ep@q2vB7^?wL+gXFfZyvuZG)9YVs8(c7_8LO=k?$nK?)29j$0N#QZuEG zRTHx~XbNVGcDisEyG~x+KSPs@hfzCt^dMfUhoDFq_LvzAPFj%-e4^(bX$eLCe$`4RAZ_;4KO1$^27cK@^T<7hncs1K1z2RP{iPJ&GK9vy8b zYSQr#rAAelF@jp*U0X7VehE<~q))tp&c_BZXDL7v&>TZn=+A@Z2Xth^vA|f(+85W0 znudgxuqz0D@s)ch?cDCm3X>XUAb>T_`Vjx?$O9OnfjoGDlGEgy$qHgqwU&{)|X6ZC=l%mQ~?!Uq;@8R;&q!h9L8p3{CLq zU}j3VTp>1yQP}bf`VcE8Qv*ZrF7^}o$Y36{}?!7TMs(LekvrXoA-ml_Yv)(kPu~NHrKIE}YbNVwwXtTG96;vUP#p42y7soQ9DH@RK+kO<|= zr2$08KcUx6fq5Pu3Q8H^JXdmRIc*Kc&a~z{Iu)+ePHzq0Ma-CG+=U*WGBAj}QdDX( z8_%8$98PIn&&rl0kTf>4|0EdKkcQ8#c;pfiOsX4V_9m zp2sU_r!rb#DBf%lb|tw&yygMCH4n) z;>ej!Up7@Dby0M`snVm$ebmm}<}YgB44RMcXkarXK)0FZG*hCg{JEKXq|e$&hnp#N zd?#gaP5N8}S}D(b6Rcmb2oEK}On-&~y46L5Qp4s-c<=|Z7!FyoVTN5^vK{guy2c0U z)=0I+?AiPwn=+bXpI3h~t!}O~*Y%{c%@x1q=jHmQ&+UX_nQunw7w|SmS_V0qDsw>B z^Nj2V;GLRw|5gw3kAS>g{S#_BN%3%dxrz1PQqP?!b=xJIXi9{#*Y`g#@{`jSe%+BuP5OGv5{mr=)-%3$3M+SpPVAm(qRS1qA+ zY5%$+rmW$%Ib&bs{+L!uO`U6uIbeP(bYeO1i?Mh6dh~gXm_MC&Z#{t zQ<#YRt*5{!wAFY$b&XQG)#%9Ss-ytQwzC!wqis=2xUL&rjZ!*_--eJ+Yl!q;523-W zmHMJx3w3R!)=HyD7nlOPTXyA8=o|QSuhfGzA5c@-Q2VA0Zj-eS}(7 zt7xThyI>LGB8{+Ou$J_Mf1-_=84 z5AFNAhrs3)Vt3>41f3YZf_!6?AJWe@0_Kbb#fI;Awig>Pd2z^-8UUl$d}9D}VINM{ zoKaktsoXKC7c;fg0Q@z_3^Ezd7aJ~f!D7Qz{xK(Px0sq$l4GJdx-qZg+N7h~?pBgL z);>_qjxm|vO;l~y6Z&w6xZcwHIH$KrQ!7`bSi01w^&PQSdw}5Bs(jUOrAi_eazw`6ZrSI=y-+!+B=E&5jOJ+p?-0qzk1L3zpL~==l7L4h5(6hCqp3>(#TXRd zY60e6I8L8$PRuv=3ZMxFrLF^*1TJO1Mav9IJ-@2F8)MU6*cb7b-sT^3PkTfUHuoAt zR}D&#-akSPq05qVFHtfx?Vg=J3#S>t!ue-9k>qYV zr&(t|`G_XQDz#(V!T5paFVC%n2~^J@6L9X7SzL)R zbvmzNI#lXKr(>bi**=;6idDkW{kcw6I+G*U`hyCYQpPX;wymU~q-yNf&6~}VYdrS5 zdomH8H+QkOrhpmo>a-jv3~6+T7lhwg<;64o%BTiW!q>do1bYrnKWQ($gPq4>1&i@a2UabdayT zV8vqTB4E2qWpTKIx$JTX#MR`1Z?sio~^bkRiCJ2HBrP#%e*b#dLdoR%_D8>Rx zk<0li)-ka}qefFSnnW-~tPxF&8e{CS#Uw{CQKK;$3*T?{eGf6;-{(oVckArz?9A-! z>}-e?afyMKj6=$JR&zQNypz1dCpy80AA+^G#zua>l@o2ap}@VX!U+zm#rK+Iia!RQHgBv_^%c)lbcvVho zj*jOaP41y;oZYC`Fe2`~+lW5uAtsp%YSEIOq6Qu4D#}sQcu|GUcE$DjLGhxK8;@-N z%aQ#?d*X5MlU_pY6GV-QbrIT`$JLff;)QRWZC^OXb)_j&@f|0HB}>P`jH8K)`{~Sy z+V&JJ-DkE{szgq8ke4m)BU}1wWa}xyOqc1?o}xnl2WEn}g2$7qtv!SBImdd?5uA}j z)e}TrQzW%ZfM)!4JxWXv0kz^jcl1BE39rml*xPaHnCz|T%IS}7XnleRDyME=(18R| zGjJoGB1MUQjI0mvo6jOuRPuYgO4lx?7YV}OoVA$zdqL?KM00zInL1Lerwm}$W|OHm zjOacT-dp%oyi`ZWNe@csEqv4Z=R07sHDZ`7IRPMO%mDC~#Yb2f^Y=NW)U)-^1p|ad zca>(n{RnWBQ8x}FdYU?GC|V0taafLIL~ALsDZK5#5sDE_zc7vu=fQtIvX{W<&lB@W znp)K#L4N5y=z>Zez?2HAY6isFTvk7uxd)#+cAkCA?O|@%Q+o${j4xp-i+WUqy$n=* zI~*P^WseUr;n%=8O6M~IMUef z{)o@ACDRtOAP-K}$FF6Jcqi9-uxpZ4P#|HV%GCx*@?Iy%rBV*13+^=+hMm40e}5 z{bdE~WqZ-@%y7?TotPt|&K8Fv&_DlmxAlko3AD}du=Vk_c`UN}W%Kfz6=<8~ZEcl} z(w@BFt1mSB>jzMd`)GXZY+9%uEx*H@bwqz`Ncz*l{$idfj$ZT^wW9*T@3r)aH$jmw zZ7_bdtXxbwkAG(~eKjHofRj!0V^U}zNc{$gR!zbinVdSY=IBVqV*FCmR86ql)(~Zm ztVEmUimEuQq~imya9Re_Zv#YVS~L_2rpKRHK4z&RIoe-pj^XZP(0asO5C#YMKXsrnlnXS#J1UWWh|U6^@?PBEjajo2&z2Np2NP z?+z5flk)@h#ZilaiCfE~zwLvC_*kX{7~jc=Hm@LxRr)dCi%sud2mbA<^dp4+QoD6b zL^o_lkHy_}dK8OO?A98}^FZP@a;ps!DRPkLQSlx`4Uc9rWepOwD*lb&q5RpCb`KIE zX}uZr2SsbKL~;#?ZM9ux`A05wvQ7bVPbl?ecD9YsgVHOSzGE3v8;3~46Vv8{8rw}9qXaYqf}RZ776Okp~p3i4JK`l5uWtx5aDLN zp^rUTxB0{vBW2S|^d*}cqa8^i09Vip zlSI9iHI$jkF2i)rre75A1-C9Ht2gE_Rp9Wz)Bb5oRlgN4OI1MZR$Q;gjH%j zTM=V*d+!!Bf4E5U+ocj<_Vlp-)*N#)%vs?fd8OTB7Cjmcq_&x*bjjLbvr2VIRtCKB zS-b;qLnyQ})J~}vt@=mGD@CuR(EQP&DeWF10!M88?N!t-X<-QO;+`BTUNHxyTl^fa zZrT3K0fuma6>@|rz~7&FQ;Mrr0FOpf9a-?YNIvJc$pL53DNXFeS z|G^zb`*P}P1k*1gfxBAdJPNq;d>vT<9vYvA*e0<8Oz;>v6| zwKJldAk8*h8J#m<2S}Zr!8x5CRWKul*Kju`;i)QF*~3`k4tt&c9V7f}xw8RH8R@Tp z^B6JN63al_h+2#l?akpcXx>;69sB4u;3}ia?80b_TH5sgbgb{KW~t=Tj>k`@`(wq} zs*n1BG#({K8N_&hn!F3eF>Eoi? z^yPRF-gmVdQ{9|~d>OjDYN z5zy|G#Cq7M`2-Qz&%F{@2O7P@C|?aLEB1j=#^`TM{hy;hp^{5S{{=p+7afl`Df8^E z2{3zvN7AhcVx~Dif(A~6cf&VR6gujCI^`8~Y{lox>SfSp)5}*)ezpAADfD0>&d+0v z?2uQpJ*QCYB$0@1+NMdO(2UQClSQa+!8PMFa;`|3|Tr?tI{W-_Dj~<*w+Uo#hi1vZCLZ_zJQn<5L|m>6R+@ zaODVhx2$1_s!W9qCQPsIqjG;Z4 z`77udiI#O<>B&^I|1&PQpCx5s)@`~wO?X?2B>h-apa+CnomLCt8bqj9WI+(`= z&gr2~UjjW8?UeyKB`xZpxu_`}m=0gMiS);G5!SdpVux}aO$`(>{zf&_$x?K{5y32( z@31y;evH}4@hoL7wS8UmGW(CC<*$o&zDEEKbdbX2K;7Y&H@x+jeJvSFcU~7Ubuv&F zLZ_mAeJCjXQL#TvLs#(HF_bi}!P9}soHho*pha-aT4;`KN6CFK87Od1*A4Y1gT~PE z86vaN>d{7%yJ}P5OswpMqw`zO6dopX>}cwnEaIA0!6-UQTLNBo)7Dghi<70}H_WGj z%Z`V6U2EBg4aJc8>RuO=&mKUhl0_$zg*;P4{SXt#7c(C}ikXU=*3p^wbILci5BLZB zMxOtd#-xaLrYf`z6@s1g3j0U?zY2$4^$MNnL5gS9&5d}LkoJEe-BO-=WoMzMZM*@PE@lf6-( zLs2Vd?bd}o7QGSbKW7&gV=)TrjF$c#4Sz$#xX0k}Z<_a(OhMcXVTa7F)fcDKrMBH{msm(k% zM~qFN!Sh6-Iln69&l5G%(g&B~D20EjXTB!mSB5&i8|nCUgah2+K8*gj-ykjhoA30F zU|3Mx>N)cpxb%bAhYdlNZXhLJ*4s-Bq^YiOrZYP{IUy6M|9lwGx*(YoaTHh@N6&i7 z4vabtvgI`Ri+b-w%w=n9vpao`@?mwjZ=hYnODi5BSOYo!NYT=R@qbIlLm0oSJ>7&U zn=hh#m&Ai`j~VDXP#&6>ZeIDU{B!EQ00zMBl(Im~4eNVSACMNOMCN?ZJNWk@HW(v? z*9L~R1<#YGiLj~z2B9#n$sD(-FmIg)Q9>G2<0pidUgAvKXkVIWP`MoUr5eLe`YTOD z`ZdPW*T4s?h5k(M0|%lRox|mNQ=4?r*!y;WRf2OXz0B?CO>d@)W`WJWK?MgT<%GYh zzDh|&t3fx@#Z}WgRIm`I?QTFThR{sQ?tX%<|0z8EqR=>`g1?qG>IAClx*W@Vdw*}L zu?PzM)h>)JW*U#E`yx@(_fB7a=`-o}Y<`_O3IYv@-u)k)CAWB=mGes+lS3#AU#9)}U;)UOcNqf|cHVuHeE3f?jBV4Fpdr@;jsH z{Bze|XcO_7(f3TJ=d>hKjHxp7l#Vw}Oz%T4GO?uZl6MwdD+?ae^enN(|K2lb_Tdb* zUO8ib7ql%btB=?hJfp~LsCQ#T{*Y`@$>h22j%rRF!fJ5`xz*(I^u}TlQt@aZvhaPL zwl9WRdM2G-EHX{Es9z48bwBP&r*q&A@RXdbqK~;&PfE0k8Rkk={E{M9k~Yl+A+1qZuC3lY5l4R0M~3=xx;DlSq${;tkBBcw$cC zD4l)CE7&`@(8;BuU*K;q8`{bvVD^HB;y`w+59N|6QdXn8Hc`&}uoVqiCISNw=p$7o zS4K!Q8ZE8|OxogaXlwTFr?;00zpCeB6x%{<`5WAGv%jfu8GPYRU8hIOL}ODl)z1|* zDvj%;)~dyx=1d8>IQ^I#N0afl3Zy3%F`Rm;+FLolpq05Iw00xK+Eb_Kp5hR=Uuo8F zz68)LA9b&@O@Gq0Tqr7iW9VtF2uSSxMSqpl5g_xMsk3mxOb&5e|7OPRw#@_+_e>KhgWzZPJ(2;HyFSr6QQ#wYd3 zBXI;e7?+jQ=4`Cd9Fs>tM;fsb$I{cvq&=WFSBi)_sETc!62&|0SZM6zr|-a?NToU} zY!(--TpsO3k5>w>YAxX$&Tu;BD8Bp)s`+Z$7bH6p^C_YXB+BiftR6C!f9 zK}`0~*TAZ1r;BUE5Yr#jh(uVmXKl>v<^2&e$W6Q&O#jeS5{+u)A;Z{X{L~h0>&&XU z=>>gGSS*(=kex)ZYZ!`w*SXvGIlUmSwIZhO+kZ1xNreZz+MtA?G9x5;7W<;kMhM1a zW%aSNrMYWGN7EfTxK`Bg1_cx~1I62aF98~6%c6Sx@ly%iUn`Q*J_KPH*9Bt#1ZMQ> z!Cl_PBx1agc;kfsdM^U`bV_83y3h*HcN)PzPpY4M51~e3J8T}gj1sdIaa4qx)SSY# zTXw|;pcBlHWy&dMrm9n5oh|I7>{hn&$oF!zqZGl6-yIQTGL$-)g#Oeo<2`1o__he} zZkNb6raLO1T^KtbwHsqfDQ9apf=+wM8a2isf)yNl9g3ZP4+#Fe3AFHS;hmbOT zsMTOCdv2MUwH%qO&KI}RvKN=h>{`x|%iapbHMP98oQsvjY{-sV=Xkb@gMODQ;MwHF z*dma`OYb)06%{ACu}-uw&%8>$>*3iFr{ZzI{Gf-oopl~e0BW(8U4amE*eB%vQP3Bn zqda6XTJB^#PRX>%fZ5J(DFIpk|@yYeVzsbzCyj6!cuYQ~SC_)&h2J<@YRq*0| zfeK!)DJ7t|CqsIG0yc>1{b%fciK&l%;uVxEVOXgHJ_vwReQ-rK?_-hcngF)hZlD1t z`<;X62gW}I+<`S<6S^JnGqP?F-Ju2ixIqNfs`#igXzr1%^9VGdze}K8714}}A!PrN z0yc_>xL;uPVEVG6Y2!UjvnMB63*kHZScf_?1Sw}zJK&iOe=U2#W*vC2Nd?efPie(Q z5!&GLU%==w_y?$%M@ySw{}HDYip_X9o*(`LziIj_&lC< zo;|stf7scs^lXpCPwDymaMk#niUllsK|QG)fh=nRe;AzAh9NLBn^V>9)Raoz5y1_T zVA8;5K$$X1ElbBQfJx~B>pO@=gW(^(O$nQX=gRL7dsniaS6jE?w#7Xta}zvJ-nc}c zZ4%AW+RtJdvK~ndv+4Kxb~IH0>#rX8;kXe6^xa4ECNTfI+J9;chSR)ia&zl7V+Z%s z&nz;M`TjmDkJ_@2K0+H1+l~nee*z{8r0aJU492n@wW4U!X6(^oN!yH#Z#$SrjV8Yy z2eMi`6q|w;c-cLZeMz*%=i$Z9sIjf)FnxS5zTg^@F2a<-GGX9s9iT*#%f4GEQ^XOM zw0A1yqoVPenBGC@B)DGG+1nN;ClAn{9S3yWHtQT%g{_-V+jm9H`0L!WzBNr|E zZWt#97yv)}0a}m_S3fKva%U8B7%f>BDy+(Hh^P%lKRGpIvHDuv8jK zh+6*3j_6i6zN2JtghYzkBD`vUhWj7d((e|cmH}imCIl;J)xqR){Hj)CEjyT|Zh`l* z2U)jZhqHwCY!MMAFS@x!c-GB3%q^$(Lw`oYnYnD@$07K`l=4=7FQ_>W{TkKWD!jbb z9Kb}xKg803wp;-7Gd7y{?Dr^st7s9Ey;Gs36n14hv%Bd%^ffrd&1FXO_xOFlJ9sY% zt+%2>TZK=b`=EW@;)d40)LXx3?~WSQivV(t!C5YILaCN^TVF)jNrM?p`+<2yDwJ-; zAa_%PZ6Y9~>Q|@$R?0=~pPgh@sBgHsT5egiJh_?n58GCwxLHaK9;!p+1zIjFEiTg5N3C>-C}H z_eAp=A05Zs{68O}?33S8tL?(O<%A%{8(dTl7LVZ`es9P`lzPv=&YVc~dDB=}TK4?+ z(HzS4jHIR8;dEh%pzYgXi#Yi$>uu`j%@=i8#5DQ|I&%R!y18A%AflH44p84pir*pp zs~!GcG1Muxtb5Qexxq~|e}|~iX~1;G#~l5YodZSN!D_0j>;??ToM`5F+5xcxUFZfn zHT%2q!l1TaHHD6&uhP{W*o_A^rx!bfucsfEF5S&4aphE(8onOiv%9d%5z76`o2133VfF#ST#&Mwz2=KY+BPsCe1U@- z2J6h%VP_n7wB*NSX54FMWC{DEaJuon@Mw{(%A}qir@s_1u0>p?-kw$UfFJ>kCx8I< zm-!Orn?cmz0})dFJ;ylIF^IE5%Jv`{|A7dtUUi;PUeCj7ak5ViqAeeY`mG9d)F?3j z!jWn5@b{f?hJu&jFXGK?0|}*tFijg!2=j2m?(rwNd?@_&)^!e4ilOfVsr82<#&SW& z4V2*Gp4#Fiv=|lY=bTbg?`6w!Vh)Qof9(ful6TO$n9H+H9CrznbKt@^boxW#S$&80 z3jAtp9)a}lhwzU*>40CKAC)(%f!b#eFu*@>i_3smiW@ry^q*R+{Mp0xT7ix-s@BVK zsg_f5B8y6xPp*)O^Jla|84!jR(?D36FJu(9a*? zqV!LLsOnD9yuw0&Var)cy><%E@;#w5+}lF0?G!a??>MP2)?HhGowa^$0V?kk1r6Yc zA>+qhppSN9mw&tmo!BXY$m;?4e4Cx3lC$3GsY6PwQO!mg&Dy_J&Gvk?*%gsxn%#Du zrtCtq&OOMwON7WwfJnP`fxCVDBc0wQnwJ}G%QtAWoSO2525Una@d^9q7Sg;=L>;e~x?E~bMF=HJ78EJ9 zmdBl?FF(PVGN8To34Dt$z-8)F5i#;T<&_F!Cya0IcKa{b1;gh9xb4JzGDugi4?#>A z_#ek-xpA(>9vCl)@*W;URVF^CoFr-FR(_w@FsqE0p5+p1^r{FLI0&21IB^{J?r zwtJuwG!82yQwubq;Ui!uP}i0W-aRLn**kn<4;HQBGI4JBVhAVerXr zAn;7496PBYhd=?8Vg#?KJJ#i`DOh=oTi1F2aa?)sUJ zQevKH5EVOxYxh8HZFxQqV|7RMyEpOg=IZx9>-cva{%svbU*y36)00Z_a67GTO$y5w z0UrK=jOXN$n)WA3u{fR|NQ3i5BXhq%TAD99nkRin=ki4eLV-TX2R(iAE&1&ezAgKA zR@Q}HIHKT+m^Lk|7yrTf$uXIujcu8rq}lgAGhxdKt824ww$DO`*84doW9gMS!ei5L7{#Didt>x z+2=UlTfCpVz5rVH`BA$sgkNZ{8&F!ZZg6>&It#v~-o#UMwO=&~zO{}PeIfd|KgRJ1 zjFFlujlKqbL%)6@`kI3eP|TO2Q$+nQpOxTI_tM^Ndjq*$uz`iG^uHD3F}+!SNVTPJ zr`=zQklM*U(Ao688vHtuqgiY61VZXLWYpek^yEvNgPfx1uf(JYi&i5WgL7m%U4z5} z9p)T2O!CFYSwZ6>%ydTrD$bsWY%H0N9F{1)s_v$*N3@iTb!C58gXiCp`46*=LhAho zZcR?Mntay$A<~9-o{m%}TlVNrRrh!mm02)LmUN`sSEG5%hc9=z#Ty!9IlKE6Y|KjV zPk#+TIXMJ?#n~#9HKbKw#4kQZ6ZZ?hK;5UE&*LbefZ3+p*uvO;fY7NhwwL}VQU5bY|1(DaGeZ9} zMgKEF|6>gKuRRo1AUxCd9aCcrVssv>ZUiSPngV`oZ*a*NYZ31FD(}n~c)%Mh(dlbU zK`eqGVkgY_C~CoFAQTPmnSQ@48+?YoVF}lkX01?kV3>WbTGN220dVR7nsFZ-cEcCN zRS=^+ZO{|CL_U&g>?< zR3H+P!vXPY2*m)mc|TihVj4(F9j_Rt{!hCpuv-^*XZg{|KKUX`hkZPsfo;{Ich>sv zfKywXjS%`LuBh)`_ zuVi=Bv#U6=TcUQctK+NwG&)_c(ZQ|tvn=Zdp~T< z7_sCG#UyLW$CwMf7Ds0eA#TioPsx4=Hu)r~cNi`QFB;MA!y?4;_z~7G&N$S8RPjOj z`7i?ZY<)!aj)-pNL62zG5qNFj)W_LMD=f1{8;DkJS`Zdl@-hDvf$AJ3*4=DARXjAPk2Qg(` zHISQ0CywE8r};&CbPSZS{W^ISilzPv#}0Q;hk;eUs2aY94i}2xw6NPc0ygj>*Qqf_ zAs+lb>yfs$4}6;}ao3o`tly&+ElwpI1@~vO7sq?c99|rXzn8ntEv6=b@iH^c17__( zw^Uk;hO7QqUo#*AY>F~y%yH-vNe^k^apB?C4Cz{yGl-S(@zxdEb{uQy!B6z7Xs1lf(aBOCdbPNOQqL;fiTgPww(;8tr&4#WS=coTNxA1GT~UfcKte_0k| z5ED+Rm7E2QNbOtoEgq|dfXc}}5nVDJU&fbPsbya2p$Bf#-EV|B{AR5mz(~R)_1KNw;~P!*RO?%&DHXQ9;kPmAvwcUxnWO9S!(a)bC#Df?EF2?*Fl5 zsoQ&x|4A8Y?|2fjCVLGJ84w!quq6KSEiRurfcrVHjdw1_iu!9jo%~h=wuoE>r!Ftf z&PFyO0rG6{=j@fLCY+H<>)d8LuwlC!N3~CihG|8xaHIWUJiu9ll$w3R(ZbXS9#?7y zW0=Fj)F%SCC@g%O!k^Y7a{{$&HEyPdym~Lk&F4sVD+Xr02tJ_pmv)z}45Px@^=9}P zsykmi+=7waE3V0EkG8bw0HC3a`qb$pJw6F<=E<8W{5#P#?PE6Gus3#zmb)^8f4i#R z<+AuUuLdo*g8I$$t>t!C<=IZ6Gaawfs+Xxwv@wBBSvmMU;GM=`x zWUdB`8Bd2*{BLaKgX-BFbhc6J=#uk^r9wxRr?E`wEuGkO7Q_=-d z-(@Q{YD_)P;%UVN5o+o{pW}~t{|>r&K}37Ce}RVxjw#{DT71$$M(*uPRPUmQZ0`;v z0oIhSxc{snU=Ql46K<~ahY*c=Y`^s9xY3E%%TUJD17);9Oec^$!5`APi(-gZXB2=~ z@Udx1%o>v&MXr~y<=i=hMqa{DB6DfMCE*=7U@(TKETRn025eN-p0B3zcCzG*r@Tud z$nxL;69e|5SO%7?U+C9MxbQa%(S*qLd*Ms<&Wozl^s=bcsxLDq^ypwtM+=~Bsn@M} z6rI+9in6#F{+a;5HTCp2LtpQGT6GzF<<+y$f-czE5h9enxGG#5RZrF-FoN>XR&0N-z_Al&z(st!P(h#v?3s%^&Y>q)MMNXH z7TQvITmLhBWT=-?2iMOw=>mN>#$$_9NpC^X#~kg&+Z8auRmh^*KZ(G)tLc9?ps5B@ z7G=Josl1ahH8DKsIFCg6qQ2YVB|l6FCU>Sob? zz3J_nBD`^9w5W@_NNyVoD;Wjhjmz-A&Y6X*k@9S4c;^m@9%ty^n-G27R#M0<5#(jz zLOM5@heEo8Een#>H-NMiGalKv? zc2ahdjAMxm`Vu8ljCZ|Atfa+EG-|j^o8^)WVL;%e2_8 zGQY4RNYzhNWEWjsa2yGv!f*B!*r*qY&PdKI!j||d9V-%DO*_f$cTvmxn;}XP7{(F~ z$iTX6$E(!scib_!NV9*(J&lW0@Vl4+5Tc6V=yHYT7mLB>rYGrQvFMamg+ROu@J<`T zFX=CN{%Qxm)$jgYQ9r$TlxOgh+dhuJhMSk+Yvx*)5!9FqEpNc{sm7nT32K!7uJdYC zF$`N}pUHK#W0lc{JHo}|2uiTc$A0w#tUeAU@gJkw(l%p$<02|z;Dh%;iNl?X-a&o7 ze3@Nqkz!ylv@sOCw}W=#WUmD8I7qFzTIa--d-6QOPi|CNQ6gv{ob%@emy}FC7|t~E z(nN>8o$T)hk?WtJyT=Eq>7SzA#ER+Y;J>HR+Tw2$RlmHbc)z2L$qpzulYLp_Y0Jv- z&Jg0k@S<3-40cb8B5?g^XTyqTCF3l24_fLXq~zlIV|3^Ge}7d=FG$Oe8iV>ecT#`_ zd_Zn@MT3z2dI?W$aaS&(*ql-rs-r>Z4l#>^(rM&f(J0b8Q6WKXTt=XlaS65RDrMtT zm+rfAvaWCZyONIHMdXU($yENH$P2R+oBm&rEia^Tkgb~PuUmRpv5BhvXzo-6Z=qv` zRFthj?>(pS{M6EI6K(p@9PHOsW&V#STamX-`PY9mUpBStdy6>$JXXuuA8&ya1%_UE zYpD_E>kgI|jr_7vlaIU2)UM`nQPjv=8}z zS%1Gqm;M&dOq=u1KZ382`wXTPeI`|JA)CqNG463(y^8h3u82;C!{xU~$WT6#hiJfK z(ZX`QDH}TOD}qYT8A2N$i|*#WSycR3EJ*CcJ%ed#`|zHV+J8GVk@w%hX-cd%yrk|^ zE!NDq`0HCe8^slHf_!`$xteoG-V08=~6vF@j?u@Hf7qx8WVsx{6`s&F+C(gQ?q7(Ns_2CBF_SOP-4T?z^}QmXHH;Kdh&r&)@?b zOm93Bk$QHSnVYemzIp~9oC9?2nP~6#NUx18d8hy28cg3l7j<3L z)hIrPP46^#%?lWIXAYr&7b3}P_IXCMB@;22pu@Q8BKYV9TJZv>VDDU@{1>8jTuT%( zo=F_8_EvR~!0bH;e~Z3iYUI~3nU$)EsCR)IO=)AKwc*LBPC+HYbA*$g5Ps!&jEXfv4AOoI3IP`}DyJVwKoyKC z`<%j2plz>eH18QV#k$O1Tsc*n-c@N}|7np%JB!gy#=%Nx%GuGBOMkt0ElX8FqtWwN z4Nz8WkCz}!(LsRbr7pl~S?#z7UHMXS;%Tzd3)uJ#H(5O5tO@{l_i4^xh-motd>Uhx zb)wxCU<(LTE;kthQNTOj0NN?WDWDa2x=a?*j+ZyI<(>w7z)GM`wC5!vPMka3?8K=XFaC_76<0*ar=zJ8BRW3Ybod>u|y2H~=a& z$7CrpgFbsoHq@K0U{DXUa^LmZ@}^RCg0CeAEXyRhm%Gk_NmN(Dby7}mR!Tx_x8vFY zS7+(4W++%W*|2N?kaiboK?T{dGG^F;iMto*bOq@b*n64|g#n%Y&;%eO%hBqmbE%TE z?Ber8As4T8=iq3_N!y?(!7s#qXbMermOg0<08jB?z`@ke5=HDAzEhH57O-qAzZ`~{ zOO@#uZ%szEbDf%+fznhJ8B~D6L%MxR#T&-scvem+~sT(bI z!R)5fE*IIult52iWRT|w`~RC=ydxAS%mD;bXpg z#q}NJu|g*gh`P}=b^ZwHpp@<5HtgaagP<24bu~NDmCCZUcQP)XDW2-M1)%i6h65?G ziu7)FuBIB=9F7&P8|V*L$Iy;wu_Y}Y&kRzkRKX%{LKi}cD}DSXrB#uB9Zg=Ua;Q;r zl#V7ZsFW%0hQ3)P34smt$alb(u6Fu_09krxAA8dEDzdJ3W~cuJWSapLXpyz6UkCt5 z9Fs3~lwJ?dlpOJ-p%xifeGkGYK&FF6f8$@uRp7ZNEw{)zl^bD0V{~-fgN|9`c=PE? z6yYX)18Twbp}37M<6`yBOJ(e_9Gc)JYneM-rzLJuOS=`PhOZwy<$Z(0KVBvdiTw9s z$MMit5H+k$w=hnEzFf@c;lriB^%lV3XIV;Xs~OAZ+B{=L3s{^fcMj~1zG8X3ZUxt$ zLu;$bwWf{~onxx%s}#2ytZy8fxPsWzQ-Z7 zA);w>>QMZ|Rby-^zR%2m?k>GeW;jrIdC1nS?S~W_ic=bJ8DCSUz1$a0z{~4Y#4kGF zx*AJ-2MRW1(sB=(j0*teJ>|?U1+4cWdiN{we0he3z@LtvWOE$i%|D1#=7Y-hFMKp} zqZB-np+MD&=#Z!M?o#DHoyBJ0xITunjfKX< z7-#<9Ux&9{lj?Frzs^C*Sf8_#1;Fq@)=L1&j@^_~3t`Dxj6?=DdJ$tEeiz(V(IPIo z@y~%6*?(Ocxp28fbf>!Xi@q?D$1tbDKGZ2GZmDg|Ylj+83AoVjzk>a>3dv+Ae@ouQ zjBlUW7f+#o==?WbDK?Xv_$)#8I$-#Bh}&Q52>RXQC?eKlakS@-;u{YVHu5&p1^WvZwx!opdng>pSV= zMt<3ybelZW%7&4(@}CFDiYD)#c_@73|4mRc3J8>~P37_v1Es&of5sr|eZ-2iSHfw8 ztxbzU?AiLe;@(pQgq-<@-U^ZdY5SU#t|FYIIoT)7hOGw&Q0hgC3H;Z%80Gn?!a*T! zId4q?+X^)JOW)5_`Vj=V0qxpzkY~m2WjGtJ+GnJVaY~7?4VQ@L)Z&Q#hU&dfO6S?? z7XcojzRS+?X{7Kh7ve9TCmU#3ivjdH8e2<7HOP3`ge^hsDqb(sNP>D`Zn)9NKsr)O zHfUJ4OckBFyvkJ3sjHk(<@p{82$t<7+r?-^u=H?FO#uRzRqjsnf@Pqvl;ElAVCmhU zc7MezQmprIi_c=54mrUg8Gb47I%HB8iZgA)Uca! z=1oMTF%bP5&?vE1bw00-jZ1k`LO<><6%HZFs&o(T$mmF>C%C(5*^i?2iDbR2x()(z z+*I_kN5klNh-@8qAzSfs*8*k?)_X;BS>;M`UO3L}dC7qi+bnEd>`M6p9N!+*m9IB> z^&yHYhQiBdOaXPPEo1Ap$T)7cc6Q4+)|e}3v+Ho?Bp7=EO}k=W8(wIb1?sb3pwDZ| zniU5w;GQj6-_gz5vX)1Wwh&T$_NJkACVETOo8%QL8>ac-Fq3(g06b>XS z*rQ%Y6q`rJ5u8UiFhN{%VT~QeT7E+TfH*~+tznBlX~+2o1DlC!3|O)JHhlKdu5=?* zdQKW@U|GX!@j!H&MCSa?@GjDpHg>`UzdWsfg2K~0Bny)!Y{ z?{zghr5KDF!EjG<0|+~!SwErSVbUw22Rgt~C>dCXLCu8}(C6!)k~Fn>{Rp5sqPv1z z>0?y#=nBSr9Lv)dg%tb4g06o0LY$qGna@xJRn-)r-nP4>q-v0x$Q9p?nP8TG|r zC58+$_+Oy4jQK~M_EBmbStIru)D6rt0vV{AY!O2p@fdKu4)k^#vyPZs2zjO7RRBRQ zK_^fF+>9FghtRn?vSICbm2Am80+@|coK7kUL7mDW8UT!DZ@flfb-~7tQjfZ_e~X+C z!GQP-9Tj4gpqZsobQTn=1kD(v@YE&ZuhHQ@gXwZz>DTQG1AU-8UaxIFVz5#=0A`#1 zDx5pF)(Kt^Jm7N>cFmMhb&j>EIw)-jfss?GT|Md1s({;DX6}zR-;2H4D2yq3d7bN6klHk8u7BH)tA2J!ynL+ z`m%PfYpgT<*FMiGzOp|xn92l@5~$_*gzE?Z_(~h?I~5XE={tO5@lj_tXE-_Kbqgm` zxC}6V*Mow>Wt|oo13_I^QW`z#?4AxID8E^!EiGSLAxf=0t#d0L+~nq}pYq^K>2T^z zoJ@ZVzWu=+i4PAYarK|kEGDthO$lT2Vu+walnB~fuC`6 zhjDRBf7{ynWZ?-XP!ZNh+!Om7aI`Os`-~^uV9yuZ)1!v)3p-nz{34{EWejTax%y|! zDLY3}=Li|>@fRElORFj8)!YfRAVP+je{VzE`R}lFIujwis^-?g&{&@6KBxnF(&Gpj zR7tE;i>#dei}4f^DH}I`i1^p6?`K_s5f3adrHW$Rt)Th61w)sc!@$7;*7uTIGWn3JEwg-HJK7p08<-1P(GRH5Ir%>ClQ4n7@O9%#`esITT4#Kz&aB_^ z)uW=~^1}@-sf4PMfR-M3T1VFR`l|yo*lj%doT;;v=RIj*wCw7&LS=sNYUYNXbdxiq zRpzc&Gh6qh_Kl>U`B7UM-bluH9d6IfUw^gGck#4`3pH&=r%|YWA64l1tA&!{mBenI zkv0MW31)!MwY>9xQIp29g?U&4O>Zot%uid;mc}y7v({`Sb#kUNw-~0bo!7+CQ)8Iu z9>&tYjb(JLDlmA0X@=VNdO(0{dG$kHMclp7)U%0fQl~4<;vRLvx`44jFF6z?i??aR zTJbi(*xAioL?1Sh{&m{3y)Jb&tlmuE=d(Z){g+tdath;eb#hke1zB{jiS+8 z)cy>my*}slky00y4R~4O`36P9d*Bj*Ra1!jr|2%@m{Ja|@{=XFa%1ej_m9K~;*^A* z5l@LtWwh@wXjy=yE-s&HORL@ir(`n|dTH`Gz28)h4`5Rxb`1dZTG3|wwtqQ-m(Nu= z$CMOpq=wC8Kfi8$m1>a^trP~`EBTSm;5yUBW-_c^`$4>Cc%3n*XOHX8i}q^9(Qy67 zUEI~;Uz8_5-EAhlLu@$evBs?}jAM%?UIE#}e=_C_a!PhJP*mhf4V%kawfC;)`B4## zI}YdV?kel_L24IP^gTGX{!P!4%)S+`n{+GbD0-*4^r@r$j?u>F7F|WtO7&5{UOty+ zOF0S!AoFXw*j(1{G|{dQwa11xH9EWD)eV=*sOJoQta#l2(#wh#7uGum>EX8yWmwhegs^KzSvY`AmGpx5t|c>)Hnos`Ri>`PzDhrW9<|Q2 zvfW1wn@1tZisy>wRI|qAYI@}JBVFO_hi9H3jaMmkLBC?<2L_9R&L~*4lpo)Ls>J%SZhT`+OE0I2)n*c(_|EBj07E_`>n6b*n z%OGW;u>_i8g}{xbbQ1GAQABGQ&?NLHXq<7mj^|NPpm?u-$wiMBC^;+$aQ5OV#&ADF zoscdfTWeWEREjdM?A^?hzG^KeRLZ%iX8$$&>A@7yMkb~m!urG9H?S5K%>u*I@_hYX z5vKnOs)g%xoL2CP=5R?w&#NS59ob0R>{D2C@r#M43XXobf>M_^0BXcD`+T2!p4$5nz*nWhPz%OG$&A-j9Pqe8_5Z!IPPj&c_ z5zC_3h+X&y9|kM+eZc%yNjtX(RHdD)(WLDfMZx9LpGKs3INo>G?^@PFMFefg>;4~P zng>&2JL%^g^0^{-et;*MS7?7%HtQ@MM7hXKn?4AW2zGmBHCFX?Q=xT!rY$Lx)!?Rk zxn61|EpHlHE&`u+Y?)w)VfuEpZ$XR_U7J%@2U*v$bUuJTV!Uh+)t>fu0FM|)7dpsVo-42M;4N9K|G~YNQRZ}us>Div z_alrANI?fh1`VLru`;kc56|9>M#swNpxDMJqYh)#u7tN>Xvj%h^OT2h3AUQNSQ+l! ztsiq3bp}?_7AV5ora;@nw)Af-6vg_VQE*53mU&YHD(EO<{I+B&(Hxpls2;zbz8sUP zo1fnAL!NPRfVrxmsd3OyL;Y!G9CWi*HR*7iTw@L~(~wSbviZ85a2>d@d7+7(capK@ zX<^Dx;iAG2yK3pPeQ88z=~;!Mn4;(KVY7X5E6V6BgX{n5ua;yLUA=7H3e^3W*V#iZ zRF^-o6rx~*Fa8`ZfF=)feqH*lvkW!AA4--k(x>T4JboAxcQUz_mbb?pSkU!DY!035 z-n~op!(RNVtZuNz#(R$s4ex@JrL~bXs|#e?7(aTui}dww76WYM{qnLI-kjN&_oDB* zz~rzQr(-%nPUI3yG5R9!G97CVg9Yp6(^E$Fw$ie%8)bPM5lujo%4!b({Wmy+6&GYw zR~c^jM8={r8U7!Gm~^vqb#)pW-<+G!9Wzj|N^{EXD)E5h82X^AjHrtT(y;y}ahuL9 zvBrIv0pjCB+)7}GVKZPbTfIi+ZnAcSCm)|2v+1sQb~~G#lv#Rb3n2bpk$|Y;+8gny zWozyGvn@0!UIv;Vh~i~E^K>`LkH;S6MpwMFB{g&RK$_Z9w(<-ft`=<$ix-aV z4TKzDOa(nx1A-%^*_CP>d3 zeLajJIv9$6w6M7@O-%s0rs7~I0f!!2=mXShxCkUwDaivn;ey3DFJqYiUZDm$8*$x~ z3@l<%!~kmaI6?Y*{e~b^ETP!R0{*17VTu?AQ+O}g)cj^68q-UL`+Q_kM4)SBX}3TG z`l~D*8_@^7WQ^}(gnv>IjC1*dd&V7SbgRyS4`;3EEgQQ3;-ZGZ&{X2}63~U(^_KO` z=1`j2Th{P=kqM51nPl!}NUQsqw63?T*ZcyqpjcIcpa@$kG0dk}JfpV|5lHP%?4Un&M zZv(OOeY}9C50sJS+Y9pF9Vo|`%;N&cd9VyJzvoHy2g`1iEBLXnR_B>hylBy2`EH#q z{)&EDYqR~BZVer2R2UYYZeUY_Wl>AHG>9 zzw%Id%Vb(j_Y!3%+8`W4pdDNKnkp*rR&qawUyQzH zj`W~PBW3-Xijq^^xycV}vHN4`HVi3m)uR3*Wpe)w$!Zb@r9W+G$vT66wCqzi_2IFf zGnU{&RU)~mmc8uhD;X2?4Cn*pAH9;1KmntqkNMbBYB@><4qn046guniFP+^e#MzrV z^q0o0sb;H^jN-g#lq^1{WgpYKgi6CHjG^MgMhLVYX40plWJhmpCNe;iK|IhkGYp#i zxG`Bq%NeGAv}m*(la{WhcUQw)q=q^9y51mH8?UmH!?o=2CofZb>#5bX>@5#orpD+^ zwZkcl4_FEwiy6&zpgL=Lg^%$|rBTbKQGN@d#E9o>%|TJ9m)f zl16lPtc)=~`iCszWP*8E6pa`MW#qaWEgJ{wZ|p)}jFUy?8j-YZynMslU=;;TkWDPv zvtUk(J8HVRg6`cW%o-*uUOXea2h#9)~nK8J)?0RN5EQk z+avVW6q(a9n#-!b&)$8t@1Sr8MZ%nno#+j1_b|O-=i=|}qXETK@W}@g@>fok-A$&h zbp16MS}h;j?U$hml8P3aU*vmFlU8Tb)(LCAEw4ILOPo-tN`s3jRu=*yI%##zUEgocYip_ZHmU110r8KG( zdSkw9XkK=cKEPkM>L}2;#XmFdD%Azm?DJ*&psxp-oJ!*El*HRpt~e_0L7LlT_`F!?_nKr_>1 zea~M9D2%B~6vqA0O9SZBG?{2h$#+W!)w@sn2}PhaqGQGU|ENx{FM{&&_D{57kxX?j zxuTjV&2Ltn!ZPHi=3CY1YK9zFxoIEdvpuz(eOfh2$dnt*XWaAu%#`O$zV7`^PVi;F z_?N{k{oj#%#9cM+41d(?Pd{bjQHE1Jsoi2I_$}NhYq4zS>)x+Sg%M>d^rIV#fE4$am6%!Ljw zk?H0)T&SZaXPV2o(C3;AYP7M7;yfPqOBJyuF(*SioAhT+iV*_I%^It&MyIM$%1F1T zB2``rdhbB7OQp^Hr!$>dDznVp&d|VRvbni|Gc8{Rb6cM?z%3*nR>7W5s*B3@I~7zH zMJhB!Y#`67H|}6@$z&YfGM`Pk%}iFq*(pfj*&#CL%3$;M;}n)F!F25hqk<;M-eY8R*nmZmQmz7`_>kmY<)VZGmGXpOkTwN*s8%@2ExL z;JsX2N%O$Vw-mlg*6?r3iAPLNdA;a&hFFJSeqs0fnMSRW!JVIQr}|C`d8}SI)la!< zKU4HnIk|F|#y-pc4wCfk6qmAB{^KdXank=r%*wu3=;SI{*Jt2C=D~RGHU3NO=;Ed={YL%p(DME`L0^;X{xavjBggy=g{+mGD^>W$s8jtL znzdH;^e9besd8ZLK+%h9WS*vH+@*tec-8~l3aMJ@fBfj|eu^WWDqaHy=ag%0M*y5|CUQ zpNw_Ged5wh2G0%}i&l!`aXz3w}YqL!{Ts@6VwLZ7??27c-&U3*8?iP&`t`Tso)ebgbul#COwXa=QB z<+@L($tEa4Nr!37CTv}gd`7lSGN|gN&lC~YFpjvQey07Kq?g(IXF9(L?0D&S)MT@4 z;@SIGKGgQ9 znr!f{2}m(KlQuuZ9_CY?O7-ePaVtikwlv$Q8?Dz3v1K?2C7j~7K%{klNHe!Uke>d8 z4s4NOk>&AzRM`dl>zSvpr>aU=cJ}M9<{m#o)wjy&=DOcd&Q`dCZ)0aOrbqpx@AFHF z5@Hs&|Aq>-N?*&&-TFi+*Mjob$-Y&FR&IktFn};^F=d+Qh70`8BDTqLX7An9beoJB z^5>UqwYY6qtAH!+zN&_Y}j#hjmrLw*1UD~+={sTikrEhk~ z!05jcl_9D!L-q)*7u{Di*`Dt3sw&alg!QACvzN*5eVB!=Z=ry^$E#K=Hn%w2W_8ph`#F~E&n15a^>^nsE-LP*WK~X_P$6ef#VHRd! zn2EEnxFrZ80}O;k8Wk`gDk4HaWDpR7A|L_=+(2BSq6~@}6%j?{{!jI3i0^mreZKpA zcjl?euTGs>x~jUmx~fu<{kGrWxG6qzU^fTfjLlpeBf1zk`9aIIP_?t0ar0WeVUwfk zsFU=Wyt2euXGYtan$;FrxagAJSkJ-^Tbs%o#HjG{Jte?Z(-S`d77Av{5C(Kb!LD zH^zzA^!eJ}6~?4Z7_6^fZ*1R$$J?^?2#kKgdc2jMFuvcUk82qdIoE%!%T%AZaedWJ7?De(Ltx~_C-tzw;|L7~Dyiy;d-Ll;Hu~NUW+1AbIntD~t z_=5G&TCB8der4RY1!KxCYmE(CFuBt6OXJ`cy;ytn3uD+D7_ncz)_D63-PhzVF1|#C zH;%zY#YoSzb7{q`DmK?xYYf_o9<29TBYUepNbA4Wh-}qg)~;A<+^|i*PW!N#v1pq< z%ysfOn)YM*sRbBT;dObf@$EK!gl(YFV>|X^6r&s5t`BMPc~fL- ztEwpuirS7b%2jKPh8`ivQ*Lic+ zHaBd9Y+5s|>w079PJM{he7$jer#>vGBRrq}HW?j!#mstEydh#jhAXZKaU1BogCAM0 zS%mEaZqW|4_^(c1q-E;pmvlP*SEuep9d~JS#M6e84fXo3(vSFoMl+4sq2B9_H)Ggp z6yALP!gXr+ppH1G%J|jzH>S7lJ5+*lCw%prc+#5r-SXD?3)faW8n!O%W>%|s1gXtl zf+%m@rMDe{6TD|D5#JBInqqELQ!_1tcdD^&intSZf$bo^sA+yNu5QeDbC=$@>G|i) z0-JqJ1L-umva7z@QTgL8-DT4*Uus-XvFcEst&2)~)95(`KQ+GRZT+35nH4J1{&=H*!|IaJHO+d) zShZWfv;KQ8^VJvd)mO(n$n4OzmyO!*=r;@+`y4#pRCT|(jQMr!C%*8*q>5|R%x%Rr zd%5xt{fGm%ZZf|#egASU>Fqxn3*OPYHC(p^CH`ng8n>EAIls;L;2ph-?MdU`cl3ev zIq0b6C|Zf2O8?Qgeh=0Q&Qzj=&G~J0Qw-l`%-W-Sn@!_v@GwzDTlMx8D014H`cbLL(g2H927UE@723@ez(p=k;bRi;-uwAm&^{T%~7vj^84<6 zRLrx8^HcyzRlgiE4(`)$ zu62GUQJ$1kB8;G z>y3r`_1qfYEX19SXXpwXWzp+$KaL5lmnP) z$p{%=9MGG$?cEv|VEztV6>c1C9Qk1#_Yi3473$R+Uy5yUP;XOvM^irPAA2?`LRT*~ zZaAo4d);}QCc(w=G}^E1THS{)wqZX5mfFl+tkrVyY(qR~o%|p!$4OQkV(g_5*X9^U z4`TJ_wv~qC5T*(6dBq{TqWt!vG5(PLjQix{W|{ZE@CS|d6>ivwZT2-0ZgADZM*H{m z7j46g1Mlm%)$6_FzopUrU884QuV3$;{~@pTu5o8vk7&J@80X@8?^?Sy^4&Us}nL3hcWBXr^5K_u->-K|BT`Cy{O_4e%M`n^oV}Lspbdw+K2o!{uAZQ7t&#)cDk zG~x5F6Z)3MqkJ58X2r~BgMbRv>}De{qJm6hIBj_I2A?iC$)9dumOx|?Hs=d0ilt=$}-+Oz@vjUGK5Z`J#*AM%fz7Eg9HCv)GpO6_t-zOLVm zZ@3!o4|Fs)X7zUT(K^0v#Ckir20Ok<)O+dHj=L~Fuy@zbb*7&D8@niaFCEnJ4Wjnw z4=(JgYSeov^dTLsr*D26XHukZzA$_GRt}1tGQB|@`pZk9r>oFYzfr+|Xw-XpGZlPT zwe-#B(l>uLd+9S)vcdnobZh;erR(}%(zTKe{=Z*uga5O1oBThfYbATb|9v^PNZ))u zee>Sg(;rs_^}kob#%p~YUAq2%2~e${qXTNT($mk;&Q)`NC%y+x&kpVBRO!3c@sz!0 zm-Z|S##%7D^ z>~CeP9PQ}cZ8x0dMhML(y>d~Vx0H)l``NWexp>AQy>d>SeXS~O_c-jfZXdQpklALC zInE=>Wd@n!{8+inAk&>kTN<8w9c#4(^^Gs?b+ptL*RTBNUdJ;wZEpR_r|);nb82JK zj4s)Zc3LRS$jEl&YKzk?WnB!>`=dI#>lab*AZ-S zo}+=5o5s!-mA&#D`>F+PXK`yhI;UdCcfT{ttl06sg6S1If=gHu0z?3B@{p)vM?=;( z#{oM!DdurN#g6L~tA1(6c*Xo=tJpD}`Ozg6JDyX*JS9vh&Ib3X*s)tV{L14sPW#RK z6faYZtz(EKs2JOn@UT65w(`{n9rYdBg-GRF6CEqkyKTdFiKI^qJq-S#h3xN~ea)(h znM1JA(enLgPUXBAj@kBJcUM6;-;Eiyiup6qK+5OyGkhg<6$s+_UFG1R7Yo@wk|=-8It885lz_b2eq-F2zax!jn)$nmuH za?t4Zlw(nPXMFDxCs1RPU)Sv$UoNkF|0zdRZEg3Y$~T^OY_-)KGZEex_$(OnVC7>k zINE61#z~bcUqY2?8)q0T*EsrX0}fT*xyG?U*D#u^zTPoNJNu?_&3eb>TG|d{BD)*y zFgEgMryZ3)t#@p#qZO4@uG!&u!UiyU#vDG)v%^>ra}3c2>@W_;90RqhcU0Ee<@f~( z#g!M{atyR->4z!@?S=+RrtES2;L!fO#mGJ2=*OSS<@4hMj^1!r);#DKZL5*Dh6uk^ z9a{N}QFzF4nYLw(G5?UGm9}(E<%UC!p}Mx)Uis@0JVx}HekWD_^?~Cvt=dN56A8o~4s(`Y6hP37e?9pKtC+?x-NJM?Qzp|*Ob;>*XPU}1h-og9hiMs82M|%^lFuCJ z*H*sW#(A)DWpt1e{leW>IWsC>yvo_7dmT?ZqCV(E*xd7u@kNodLohyyXy$k)%^c&T z5X^KK{&N@VR1P`+dB*FM48T07Q*4}05qu``89!m82#lu}J~t5se$dDZ6RWUMbc{ys zZP0&HC+}wHeymf%?j(OLCnfS>o^?>{Sx1mY>~qkqF!#aa!USL*f|(987iI~}N|?1U zn_*%w`(ZwW`2wa2=2w`%VVv(e$OY33rX5TVm;o?X!Q2RQCyXCv984+9WA6qXG!w7@ zW+}{zFj1JTFuP$6!5oG88s;?2Ss2>ypc*g@U|PX+g6Rcw1T5!+)Jyo1b{3T>MiW zVsSh-H!yXW^^uq2tO>T#stbny{N8nqScy~5VmJSBm_8|Cu1pCt+zdk$;un8>%+OVS z6ArZb8b}A3$0&l*mG31h0Y86Dr?LLKI!)($^;Qr)%@Sh9r z1l*h==Y18*VJ*z-Fxy~u!Mp>rALf0Sk6{upRWQH6`~{;mcTx?Q`Y^4UJF8JAfTy{0 ze$R%%LBKO8Nlt4h_;=x}z-NW8P2uZ>xt?wUIj@5n;pP^f9G||{K$=N#infp>*oxaKp!e>d$ z>25MV@y>`G4?$WW^5Njru!|wL{vs(n=}I&n&IkUUNMZjFCBIR5G`Rhe(qbt6C;^tG z@bh3yMp6`?e~H{R)Jbo_usl8#m)$6M6KeEoTq#UTn65B`VN(B=-iQz|Yha=c%TxT>JwV*CC1&T<+$)06Ri@yEOYZqSn=WqV)azSbDPiaL zY#H?S#e)m~)nFq1nej-5%Va0H@o%yYxr@Am@%+Ij%PkI3NZ1V`8_StAv^)nE5cz!mNaO872y|4Q4mYewYtnK7}~}lY}`tnv)7<-{Yk5FeNZEVP1vV z0`mb(6-=#rQ6wm?qlRCm&0W%IJ1hWO^0L)h~XJBgIk9c7E zzzm1UfXTeynM;L$Aj}+?WiV@C-iA2}^CQf^Fm)bqQahL-Fr#5gVV;B80`n!z8JJo= zCv}3k31&3RM3|{C3t*NnJY2W=2CrXV9CB8xyTEq!9oGjufok~IZyOeH8_h$`JL-Hk zt8wTdEno|1I7WbsM?%igSIxB5_4;nR?MC0#x8Lo{DlIOcFg-XYRhV0xlf%Mrs<1F; z63Z4rX1KG{+8C~>&IZP)sm>Zhms)yRMTLbqS*5HOv1C?ct1Q9ToKjy-adA;GtF7kf z{C}l4)8SZal-trRU%=+ zr`hwGTW|IK=h~CTkZI0lgU+z$Z6k*D^j(`@n5k;x+(pqyx%9s;O6~|`XO^n!2^sUJ zIo(aBYY2aLU(Z0_O~XcAV#xEl{((4D4N?1~wS=w&o zO*ap}re7b<*cq0N&CK_elonqsymLyJQ<|BbU7TN-s|xsUk!4L%%`s%>a5<$pg%dfN z>Eh%Z6y;`TPGR9pC7i%AUvZEnb6ApJh!!6xEh=P5m?Z`Ih2z<=NI8m7PG%UEEm5+P zoE)XG)O7uii-@rFQiEQloH-M+^32;;IfHOd2yl#RE$@KJWmKFKiYBVU+azw>m#n=- zImc(`tGKqCu9DImGp?BF=Is8bO7H_w$kBw%z(r|Gj%=S2mldXZu>61#dJ^Z2#o=un z7@O8?5M&>+ZUE(9n#(Fjm9wlgD=$;!w~Aq_g{?%#3;8MYq}J4v4#0nkZJSRKk+(hnBNcRrOJEqANmz!3RIP=q^(|aYDJL zOt5P7gy}B9lTJ0ADt2=dhk?MIEY> zSy~EeS~7+{uw~K5cJ6X~bg)Ko4t-gtls++Tnd5BGxx$rC3tdR;=SGD^r8z!&sc{jl zZW5sK)+KbLC!Uvxx|ff>H|EZ9_76r!;H@=z7p%|(Sm8-{W5vB(IED7v=%5XfM=?|k zO|?N@P&CFz-`a{Z3$yZkn6@K(apoj?Qp+##jlpB7cnZC!W#OJcs=38Q6KJhQ8VUiO zR4F~9mE=E^LlF%F-xU}xQcT&wBtA$VQ{dZ&?2CaNMc_0uNm^@1KJxhSzR_M!Zz1a%Prc&{0e?99cBSF#%7o%&|H2lB1M%Is%!+T$tZD z3QP0Q{TKRB6*(oP(0)5l3%8>L~hc7>yDxCB*B2B2kEO^pM3!On+ zrAj7rTwR~CL_CqA9P}jOtdUEL(u#A?L6zju#x$-Fv>jX_n$nRp9+}b6 zG#(GplhttPC1}l`R5L3SZ8YFBha)Si<3Z`8U#kasu(zZ}Q2+%)%WI%{;)2$uNri7o zLuL4AU3C=38I_yg`O*;)I(6EUMhDX1bC!LQ%1-x|E;ECSc(9VKRH0;FHYZz?!9W6 z7}jLdYR=zzl?&LY8YJ?a8FX=V(lBiJwOa39bPlISg{Rs%vyWriNrPxsusRPa5I#Ab zJwh|sj?H8{TCP-Qu^pd-0QQn8{1l~q)hB0NuBI{0?-l}-ud(2KPoUde$uH$K`?i-#CK`nWbZRgUQ=m3um)@=S|rtEVpQsEunz z+oIjIRiW&!jeM(k%pP-Y?Sg!?{}K%Ed@P}jP)J@>ptbNX%(>{h1-_x@)%C1mJE82@ z7UuYNCEua=bIz(+a^KhHF-Ruf-I53$`+@yqr`ezD16_EK`|$EQB{UBu5c`q+lfSVY z{!`gAFd1T3cl8@}3NlMd>0q6#f*h3C2X#=Ecwp?U!&i6=p(6`;_B5vKgtEg65!w+b zD#=H8H^oQO(sSv#%O|1qJz8r6u%CJq!aP^|+mnnL7y-lA~7|sL=)OZ4l&=&&mvx&@Tn&6^k*{@w$@=QExxhL!!%(sGU{L*Oi!k6sxc5=#K`d#|UT2d=M0*f`gZ3`Djx;^|Y;MfT!_44amWH4o)!xeqApYw+o%I^7*Y(Ht4+^!@Qkft&!n&o!VI5IXjz z@{K9YU1>Ho81hXD%1~w(R1$BgplXotVFUW1fx3t2%LeoPAhLxRAcKjnHId&=3 ztcDaueiEOmv_4bbr`e9eCMOz49>p8tcFM_|fZnBHZgCKo@OwkNjnl$LW}A4r5x0XE z8X?7>5J*>ner}jc=Nqb~qr5l3I}C5s%FmoUmqbM)o`vA?+;i}aCsn$?vYkAq>}fp9 z7+%bF6w9>WHPtX{u5pjMw~;xb`K~)I+Nz3g8u7TvXARCyA==nUHC6dNX3#mDZw%fe zsG_k7j2E+<($f6gDYT+73gMS(HgnASEW3W=aLIEB=fpj6tg#HCg7jnKA`Df2Yb;M9 zyuRhqpN(_rLSq?1u55yC8TVpi)gG5NL1ZhcBeK{FY{!+IfNhpSxQX=ti=h}gs~MAYTrbXKwyvI^LANLYUAYbpeOrRUI!6&2q6KWj2}3&E=Wzqj#I%o2Db*o=Fd`OV3Ji~g(`W~egh>t^a&tD32+ z`>B~Kl{3vyeMi)_eW0#Q*%4(&VPAai&PKvHw7vy0{E@l}dZsy@MUus4yQgULNkV6u z>p_Z}T6lzwr?;EaqLwJbj}cIpxr?8go5l7_OAPEWT!31nrTTagsxj~k}{ z@)qhNV`Df$H~Mo*|fRj`8;JB3ho%&z1V7xMhv7cHy-)QJ{6aPnR9 z`W`WaPjlFqvg5xnhkj){@*5}4BQBcRlI9|7$v;@0_>1l6-->6{0EaiSo!o@QrfRuC zI)#fV&C8uoN>8^c020y%oRv{Q*}YRkWg4S|MM1Iko7!Y=`!%l$9NOPjL_*q@hE4kaoAi3l3fj zeYBs$9e{_Odz>#@sRr??JMnn-np-yg&^nj?X`NqKl2cqt5jT=J zsv`ePC6`cHNAvxobq+0pZ$e49`OS4pFYpY+Nr;itCu_ovmb0B)RMX~D{d{m86e7=J zhm%8Hp1Z7&+m^3rJpJNUfZl>OBF z)jw@AvuPeOme`=eZe~032HVk{(5jV7$J%0`+7?4Bn$fP1Hn$6!{p;uLO6XKO9z3CF z==Jtlw5>g^`m^>0G`B;5R&_AnLg|MNX7PUCR_gb!_Q>hGoC6-mhuUT0p@diQb|`h0 z>Rg$4e~Y%`;RNpPm)m1y0y8DFzCGSk^Rt6KmGd~FNvddg9_~=PqGI$u=e^r<qj{@NS=pgi>2iYt15!*4?+=5WlXWEw(6<~t)%MR)ye&KxmrXu+b zndO&9n#rmB#Zls^K+8Lr^Df6&^BF;gPu1r7dWP-DGPXl26+h2* zVlCUrSCwR)Vr3_`GRL;zSQovd7*eG(ZfhyL-2SfwQl31sxHX5l5(uyc2YgSbOeqoVGgcFiT>@xJ<##4 zxDa@tuuSi4TGW|lbIeD$knqag8LcHqzjn?<70&O5^%L%XvsIqI#!C(Ewtby5@hJZo z2`4xZ>JaZ1YN77C&bV<@BX|N)hCfgx1{)J^w6Tj-CSP|cqiJ1L*>mKy66IsPtWm5h zN;8N_mae%M^Zr{`zQ3}uNb_JBM$o>jF%<8D_Q8+D68aI>6jw2Wb1KOmX#t17OotikiA zU-rNgdMg4&x3fp`O}67ZmG>C>w+9a_SHe5Ci#-x=DW$z^NA|NFJqkNW$zv+_Ig?9i z-Q~0i0TQ1n)vqtg_gqc~ICI~qz-QSG|G~cI^kWI80x|6R0H1>^A$g=+X8cso%! zD?KG>_&is*%Y%90y&m;=J?hEJ&(5I}oKgsvdod}n4OM(z}do+e@(BVNfd=B@g*mc z_*x}+f_*dTKrgK47iVIIXZ{e}0T{;}?^RlcJK@RRxO2G!`@L5-j}OlGvfdV__C_QK zj$B`0J2cgWsB&pp?~=TtvVv@Lk|mf!vEJO`)STVA-aLNZ1A&j*!`9w(0I|fUyAVDy zo9);fWzR!gwF-)8S)U?$y^reh_xDi~5b-``^l_hj`k@b(JU_1I^(~=i`{vT){fcu` zkGZqoB;|3WUuJd?!=q$BWPq0=*7wQEEX~In>}&my>oDh6z5VU(!)^Hhgvt4wd}smN zVc6;k@ogV-W&dPf>CZlD3W=WTt8Ny~M|oe|FDDR5R0WSKyGN1(=-gFQ_T}l) z5<1#%N?{g#(T_{@`+j6MC!^@AzGd{Ik$BPBto^HqB(#w85P^-ht2P$=-4~tX8yvq9 z8s6X1n*?9#Cq=uoKMGQXNhmwCNQF1pPOf#?@S0OVE1?wGsxEFj+X-byb~A_GVLP@5 z3EFuaq$bhA{V{E1PC6{^kNXCNxEgVU4zOtqzF+Me zjAo=3X`Ua%9U(n&MbRWoJ{Qv$SD-*nT*3Mq2AOXQZw$tZ1|1osDj_)t{$~f3(%*wH zOZn7b?i&t3orf_e2H}oG$AYFf2(`a}vyf1BQrV$}oDm*-&l_CiGvDFp!eHc?3@}bk1*p3uGwW*WYP0im~T6=E950`5Av1I z@9Oj?v@s!rSduE1*di5|!FKFvWiPGE?Ju;99ns~SQ8j70=?Zf=cobpdE7&u#lI_s* zD%1;XhhJqoq3mc>ag6OGY|`+!Ea86Qi6NM}%^Z_okYAdQf$VJ5Rrn)Ti^4`{S3D9~ zTrni82=DMO!83A7#bMh2unMmzzcnPUv=oCquIW%cgbr1(9oxWmd@uhbUvz}=*Ba-L zT5fO17ONJwhiq}1arGTeDbQZ*Fb-U%jNQTJPC(CZuk1(fXnO22yu)No5!6IL70#;rX_9u z2m(V@_e5+l~@?9eo6i`X(^$bvUsJ7!C2 zVW%flKY*Bw{!!*zxAG5_ANxMIyC2CY|k2=@du)JoB-RSHd474L^ zgKOt~;7PC-j4vh=b-=!K;Yeqqd%>&|?n*R8*xMbujfE3Eh+Y#8d58{yIi5gYZ0;3# zpg+-XA`cD3DB6bo#2jgS7*TV4GP|xO>WfbfoB+AXe{0Xbu*#>7*nAt(kQr-a1x9@ zSIND3C@|qLc(iZ=JYG1IPxP>GU>v@BChRF7dfMb5`3sS2@rV}@Z55^f8j%&CnCQ4A zFTpp9EIIfO;RIO6iX4|*yp*V(u&WFWTsQ;l5srYp4%9yf2;+-!8RFr_K9C&YD7aKO z37#&TF_{Qo7E)CfdK!(+V*D=PZee;BEnhf+pWZkx>{*WLcA8-m!Ix3r;-ED|S6ciE zic~lTo($%U`qvW85_Yf0*DHi+Bfe-Q%wKytEF1&>D4YcAX=XghN>nj8$dS6Y5M3@F ze(-g|5%5@HdIMi$0COV^Z$&1AJ@|p%XGJFf<}b&omcJ9#AoAoMqVI)cd$D;M>l3Qf z_u>9;4hIMD@5g;F9+4wNHwb$_AetcT2hR|F_lHCag%jZCguNdVZ4&k$C5j2VaDLNA z!pSd)PAEqGyS^gqE08Pu8cz#MF>{78zJZ@`7~EVqbb_d}u>V`4{+9lCM5BZgr%-*u z(eH`!g~LCgDuq42qCEx0;XZ>e09Z=rh*k=F|3oJO=Hl|$uq{dCe(={KkK>e5dkr(w zq}!;OaE8N1-NCBeIBj&LCCAr*ZxmMj?;Vy0G#?O-fQyCs>k^L%^H(kA3cEVk=xO0F zc(ofI$aYAl=#;pZm7s;J9tbhU77mW^%} zj?A`Ew#9Ss{CyM-jwlI7g%tqbm0Tt4nTz_f0xYl*PqV267U94bOJ6;IO?hasjq2Ai zIR@?|>^E#QSU3s3$z-ey;mC|J;z2lMq!i2ghRg|v!H0xB z*e>^-upewomkfcM3%hVSPXBc2|08hRV0qv>FZT%3i#D1h90e}|bLKoRp@oY)fun5p zh&;9ix24E4uv_np$P=%k%3Nk9TpMiEP&gihqq8_@qm71IO5ods{owJ!yjd?K?0pTl zpQW?OMz0Ac!AFF7L*FT3sa11<8 zIJyQ9ss zAK9=oM6Tvz8~rI90@tW-%ERDh!Vz#s;RLw1u=^7mT_fxT-zn?|X9mR)hGV>N3_M9V z0e&3JSI=K-UM|dEbKWEz#h1F?7k&31=re@<;FH3H5&vJp9{etXqk$P`FxgO})^Kp0 z1sZ8IKsW@>5DtUK3P-??3P-^Ug=65=!g25p;RN`YFg4cbH(@upW<&J9OCW9I7z??2M#lFQm#o@+p z)#M6C!Bd6(%{5vf9BHZ1PB16z$Ir%}5RQQzjZL}Rtx*RsC*TJU5qTKw6LwvuQIK`G z|97|5XsLLFz}wgZ@&tIF6#%DdSBX4=-=nZK5uJ`2H5JalFD!b5W8iCrli&x0-JLa> zWa)#0hNT3@Ixt^Z23~^>h&<9&qi;o?1XpWnChWzzb1j8q;DKNc8|{G-6?wv=QK7J_ zmqs&$li;PMKE9jLPovjN2e}7pwBO<(8YP6|;Io$eN{y;FGZXWII|zrt!-SLI+k`W& zMzIU~!H)>XhHF&O4EMjPr)!WS@$g=&(aXY7@aw{n>ohtdoCJRdR$)hI^oOu(BzCVh zH{}sMI-$!ZBO|WZb4vmT>4!jUE?H+@;Y1OMbUT%Z0t8HG0F6 z--FvsI0im1oN+&@sfC%LFn9==_hCbRp_S z*!2v$Ug7Yw8f6P7!Ipktxkk^3JO++hI^ey+-sf-!2`9j3gfZhpb=_trf+0A1iX#HP zSvUsH5e~1=XtJ<(C4S_^lE0|YVldZhYz^)jVgD;=n4%L}r_s9>uSX*lPJmClrT9svHxJfPWB<{;1KP z!k(Yd(At?9N`N~H`+wGGkja?3hvOFUa3?j&w3NW5!U^yb!meLXf5HLq2I2T`cv=dF z@g;{(g+utN!;iv|v-nz0dox4cKQwC4K4>~(=QL_B9M!j73&kazIFHN;XZ(%K z2}i**gkAq=^sI0Uyv36Liw;USegWeKVRw-1^oKaS_{x#9gVdj9r-s6Aa93e3c$lys ze7A56Tq;aRkL$pLT^ z;VAerVGn+Iu(NOk+*>#a9%AWKx6`%4{_6H1-6f6~Jo1I(;D>}0;90_v8g_ct(g&{- z4%f8PPB1?UBDL-GzHp#}ojw!xU|RK*==;IdI`PGz{=Hr7)Im8gUh8V7k;2r?PI+Jz zDR`2w*JGz?!k%9EmY8s|uboy}`UCCsny`B?idxv^wbM724)|P99MP-oRI9U@So9h@ zH52ySV5g2??pR#6qv(Wbw4Ls^^f579B=T@3GG}qNot6s6z?+1#erVaW@T31ObKUJK@ni9BqleO;vg zch9ubF;Vi*veWlg;IN%8Sd6dZ)bDBrjy+|k?qIH(48u;>2z&9Jo4Z6Ou*^;qg+0&N zX`yg*1*%5${VxRVv_l*j2kdl2*mcNGRl+gMomcB-CKftuCpVa{!2hA0t`K>~Cw9sZ zjvckrSWEwyou&#$z)OVvpW107v+Dohcvn2U$L;i`aPmu(gmB<1G!|j{+D>WRB}3oZ zsR5WX6hCRFcEb3LL>eMG;qUEqv&aKK*eOTkp$m2j7K8lA2LvrY_rtH_hr=`>b2V}wo*i%#Hron{D!usrg#uxpe~ zYeYYEvrg{_2Z9;MohZ5Q(CHhCv7Yjqa2R|+0)+3^sgcLbsQUq(ItqvUI`sx~MiV(Y z4HgcJ)#+N{Xr4}^M4yjLy9ccLe?B)YPdxZswaLPM@HF8-fll*;SBHwVLqR2 zm#_!?zHqEar_U^%fQ}>L%nOJFuyp^YIQTHP^qz8M59)N8upit@m=AdyF3bnN-C^kz z>y$0*Db=Y&I8lZRuyiKsWPnw5gV!iI>OT(0o1)~IgtcJd05~BW2LCOb0N3edW|StQ zC0jg2r#`~2hjhA5I1avB*o)<`eBluIp`bW29?@x@6#)FQ72q+QwhG6hEl>*?_!k$Gs zd4v<-;llB!bh<@2x>%POXI9QS_?4#Sw*Lq;LZ46Arzm z(*#Qno(|@#alfw95@A31Md2_wD*AlJ;x1u6ck#F|pTGEr$(a9t8~1&GDXw>PYANh} zU#A|z0kBs%6xZo?OaB9%GKIq*>NH+B@{vwcz?{(pc&f~oZ8f9d}{U+7dR z9^NXQ-jcwPb2@z>9QsqI??or{uTC_;OdttP7tSD@zX;|kNa_x{TsWhagN9i8;G0Aq z54v#5q&TQ94rdg0gM-2zaD{LHyi_<0ULza_ZxME3>H8hw1o(a7jQS4xM%dE;2S?(X8CcZD;+p9+V-r-h^7zl4+Ev_a;DdAs7m zggxCHbQ!pg8vl34>5}3RyBzf-oapJG+msT9-n|{<7v|H%0>XT1*khI)Cx6WWbKQHq z4th#Bd_9i66b{{hQz?ZLH#%rnP#pf7QBT5A@aMvTJ00|cu=_4tnXvb62UQ3zJr3$A906Y~90z9z2fg>>98GZqz@=6I@KoU>crKW4r;rZ~P~=fRu0}YV z>7Z@G?uj@KRoMTqgFXbSTk#PGeJPxL6bF6|M*pwIbkiL4yLh;!JIHZ`8Nff&L3P2b z;|=35P~pS^oTDlnSmdCg7C(zKRfWUg2ZSTwTwxsWN`=CSXM+x!Bo11N`U7)LJ=<}W zRseh(e3P(yCyp!?j)FfIPJn+BrWg)=9b#tM4Q?u&0q!mw01s#8{y(`Jr@+Ed2Yrh7 zAaX4n0_O`yz-7V-@MMb*JLqBIBS)Cp@R8lTHdB&2Z9h!U?c_sHuMr+(0;v;}+WpC&0ahPlK-#_TJ&7 z+ra2h>9gRSPRbR>TAUC#g}FL1GzKTn3iI*D&j|C8$ghCwKqms;DVzWw5_V7Uq+w{}G-8 zu6q?ST?^F&ZYCT9w-r7O?h%9oKN~|K42P}~PGaKWZs9O`#c`GnN@TJnM?;(~>}iKB zSQg)cePF`LJBU6Ij@^ZQV8Y(JamL8iX1c*-2ChULQE+eJ82B1t|7c`LnEcq^C+zm2 z0SUYA!FDX+#C=3B3din8hJ?NM5*-i@tRVXGYV`kn^|T7xA*}!_u}yinnLGCj*q$kz zTuanWm|i6sB<$IUZCApH*Kkm;unRg9h27wV!rpaw-4~9(iu!+B93dzj6ZV6D5%z$! zYs?H~fSU z@kqQ&^s=xYyi+)doE{Z+LH}n<{uthBN0^z2O~g(b;p7zjjE1mlnvF7q!#7~Bk#M{a zC-sg%{c{5hT#F4x;*sc&4MtXgM{M-EuHl+xAuuZz|^=87!k=S-6 z93O6@tAxWlv9U?mjomXv;GpV!-nCI!JUj=mhetS?u+cVQ|5rBpMA(IGH7A9`r?G3! z(m#d+aBq;S2Y0jha~lm8_Izd|pT&nU{+=ifFCv{I96oNNrIylJWXRI_$VP7q2f)XK z6A1gOunYRO8_i63-bbkjXB@Us8)5p%M!giH{{7fKbDiaZD;_Nz`@%+L!v3T9!4qLO zB3dQv`OrphSUTX3gu~#ZFd=N(C^JLx@38?**#9j`C@2mW99IeBoH@EzID$Pq6D&FS zqJB+{!Xo$li7i+b*TT}PaMGsH$HE!sZ1l6R3-MID$;?pDh5bKnaRmOxt|j3p<^)Fx zhrwfn69ceVY4Jd8$q^1*fmLzg`2E;RV(Hw9y#m4+w%(jQh}u;XSk zcZm$F(FyY&{Px1(EG*FpXN>aY?V>OyC?1s)_;aC>de_s+uBo}*o zgk4aI3wyv-!hSH}$;fw&YobPtg}vZz!tu$-kg$Im7Nmt^*by;F*!8qV^O?E-4?KfR zh)1YUqe_dh&0?Q$tQ18j?0*ysh{DN-u`@=PreL=Np8x8W&ey1ka1`8KH~{tvhrz)+ z#1VmGoW+P}vTz)km@DkZK8{two?`3=vE<0yUQ1qtEm#&mhIL`#jPclxC7gH#>%zCH zj0W*#SdChl4vMbAJ}}`p z8byS|Z=;GWc@Ro*aZr6M#RT9S>B9ayvHrVU95DpmF6@HRVd3!gSc(%4+-0Y~Ejg~B-knk^ zxUx>dZs-gZCS3h!;mA#Pnk?)Yg>_`bsDCdK+bSMza9lW!t2`m>YL2TH_BOFo4GgFG z>OGC^)Lb~$$WDEQU69`*OyF_CN$5-yc0;}_D2^yRHd!9vcPx2pJAEviK*FblBQ5N7 zUf7?FwPTFw)qM|cE$jt*EXLCANMV1jo&3T<8i$Po;)r3*ceZfy5j#C6?1B7s;V2^7 zC!A4^5)uwW=e%$LYr8JIGjN6yd01T+juqIczi{$FJB=7E{l6!Kda{&|p*-Oz0#C65 zfaeOw!7GKm(~&vh#G`iFBkV#vUkE1ySUnYvBco05vcj1QKZy0;UgB^gcQ*+85g<$0 zgVpcHgfkH75=#z#LpTckgThH9@TIUHoD>dUty2xW0dc}^gzd=8{eKk8;zPwFhAX>S zIF0~0R)81m^oVd|5i%j{S!<^a!hyLq`an2bj=M%UQDG+=UdcFN7s56Yjz5R>-#+39 zFUAVCuxGiQa)sU7?DVj3WP_a+Sv(7C`Ii1dG+5zSZ;ieYcK1c^iT6KFIEphhTMB!? zSD1|X{|FKmj{p+L5%#Y`v0HK^w$PGe{eGQrVh%bhVK?}cuxFW_><^d;Cvh>&Eq)#i zP&kS>M?E0@e+Z5oO9@5#m~a?ZUvBXZJ1w{Pb2~*X#f<9b03SJ}Z#|DI*!meMjsln1Y zh3AWK;%9920IUAr{|h=0yw`I*@oxon66W6vx<)vR%>s8=a>9DCupbFb7WO*v`&`1Y zG@X_SXJ|UTE*x=SU066-iuK>GEss*HSXmw&b*h^w_18nEF2d9StJ%Ww>N*t)XLQo( z35y|LDjWcB6!w7k3y0e1^o8OeVJ-w40mQ>qO{ZF8q$Ci)E$nWIwO!#PbVdrhpmV=) zpb-|Hh2zL{g|N4+PAh~H&_5^~0-p?uBMOHt%S<>@Q>P}veoSrl5OyQiS6MpHzuS_7 z#|yilAF|}w`4AS44%O)u;rL*k_6i5X_zM0f;t2QCsY=*85DUq|$^JUdS1>b|0d6Fm z?28Qu!f|k4VM07t3nw7IP1t=E7CMEakWb9E#{Y247LN?Pr7jcpgI^Qo-*(w2%)bfq zxiAgK_r-#?y!I1Kq;!U=GCu9=~LU#AwresDKt?*9`vV84cV#51tTMA&mTR5bE2{U0STsu!?RLPsA z@D(YXk;3<<@Yp=u{|Ko9PfYRPZ!}x-ms0qR6h557pQiAM6i%k_zbRZjU*hNfr-|uM zCDAK|uS?;q6fR5QIVRh=R3a(-atd!u;TB{+6h4%~pQZ4r6#m6z3H-%fIaGMmqJcawE@HHtsDuwSz;rqv%jZkGGC&dFNS#u*)@`)+@XbR6r zVIzfKOySBD-jl)!i@6b2Sq|_oDNF^IGEp~$+of=i6z-qGH>L3XDU8G9FJ9Ob@Wn>> zc#6mD6keFZ&!q4xDZJHWbr-#r!Us}xK2G5;E^%`@eaS)RQkW(fJI**81?x_@l;fr; z+%1Lsr0}p5zCML-OX2%cI6H+4Qn=LQ8&n&3G{s{^3eQX7r&4%X3cq@hjWHGJ9Sr>s zXS>?iib2?ELG58W7=8b6wl(@xq&H~)B-~her|V!wz>I{!CI-Uh1;VBU8U=H+v7#cq zxv};S=Wb*A-1HlpVD+4q!7PV)4kiMFKd<5a)7hZ*O{OaZN$6Um=e+dBM#i7c5k_`J zdb|JsGgJFNnW@}$-nmXo$00C2KL6f_<8ASK3dUuN(%V$tz9@aZ)5va@)}(TzBkj6s zl`Cqe?W$oMZj#o@C~A^6rt<41X$gDfhK=b-XXQt2(`GiT({rGQd=v0PLnS@>dU_f+ zzn@-uW1j)N`Vx-AE-A%P!ad;XN4}zBoXFav&%i$L!O?ytIX(LH>)#uUL$?_FAb=(2 zz#{Hvq{q`&+dTt%8e8M(f9w7F<0tL69!_s#@97z6e0ey%anqiC5LTU_jx$>eaVmWN z#GD=jd-m#InSLbQWvkx{zgK`0GRLZ8_e*;8_w?^=+%zQZ&1QUTd1(%EpEChJZB{_) zxMk#}Z|};+AEvMDSou*-T63-Pvi!6!og2J&>5BttGwVg#a6J+=e8jB7NSlR^xtbZ9 z0%`5)ZRXm>-^c2~_rW+1yzDV4Az38!P|(7r=BO|FPS?aDLs!3s==`SD$~0j{iz)sSo~TI{e0;F`^hD zt6`0o|B`9yiB9acbG)vyG~HNQoHo-jh5Z+{tk==VC`oJQ2q|YKoRN~WYT7=#v8E)g zneB1ooszV!b={?D>`#8VJhx#hO&eM_G|?2hBnu(q7Kp2dA2-F-R9*|}*D|)3rnS=| zj~ic?rrlhRyFC62n_An?lW}!f+LU4Z+nCX((yb+`2U( z!z{1*D@A9(W)tG;1f554fsR14YIJ^n&-#XEVp=uV=Et*F-f?2^=L=8Hxc!~N6HSc~ z6Vu}D!#KKqG7|5MXk0MWkO=>Eg|Xb7UOU_{z2OG$?M@^7WSXma=`Cqg`rC+sL$Uy6 zXt8csI;tVsJC`8zu|?f#ZsaRoQdE}1;)iL>d`CnM2e)2^h_tFGyJ z;hOTljnKTbPT6Nr5yLL*9yV>S#+ug*?ohTH>ZNT*&VH;(m8T5(nf;ins-p?+s#bs- z%m1iqgwGLsujWXR#A_jZs8nVER4Sv)RMJ~d$8QbJOY4@aH%LiI1>nSXT}o^j4xdqp zEi%DfH5=VPaARFKt!<65 zlW#0Pd1Lw6s>h8l!f8#f7=L5=uOlmN&ggk|MERFhv+CkUME@_o-a8=58Z0 z!J}7c3W|yfDmH8g=n=sZyAex_#+YabbZHfOUf&YX8tG28oI zl*{o&TU*A({Nve;^N(8s(`@~-sogv^NO>ol2F+8Ox{TsBKANWnJG=B#>HcV=Uv4_U z$X4xP;>$123dP`~jdbe`*}@YW^Z#fR>@3!**7UsH7z>CMdhSQ*)lmd;m0hrVtGGiAkcpF?o0F~2wmjPw9tXB0I>O<*Grwc^%Ivn`lp zW?SYAjrG#3mnuTFUfhx1Cao90r(5sJ?vuC|WdLfnyDsfRfeX|I=GW8maI8?a=9TOHTK|ol&WuGHRvNc#+y9^$RYlQ7oT}GMn1_ekwEZ zRp%6&<!7hBFr96 zPDP#YPc|G2{zAhhIS9cEyMRqq`De($K&M$t((FTeatn8MD-e<@&$iuWYl-ZK7SpU< zMN&yuPl3{D_Wprpdqx=~2Xn~B6uCt8^PIsswpJkFbp|qrx-3y!xc!F3##)An6-d`= z8bcym=jEE6vOKi8*7_A)s+g^RqcfWsR(v-qUZVQepUp_w{k9|6kmb|9_n0wrT=O3P zW7)<3x`BlYx~o`0-Ta5y!DRM?4^dVg8p+I;+Q*Rg+{bki z`lBVedjuU>rbb7+%JSZN6f-$TFy3`STtKa9#j-&=C!UBD7H;%cWH+kwsrtH-*NkkR zs$G=$(RAulwX@&r6}(#1unYqq0tK-5X}PVS;b*F9=#iMpGV3&d)qOAj)0XtBqa3U3 zS^XhG{)h+W;-mO=h$Xo>P(xyc?8;)PtinwerO=GeRh!$6k$UOBnv&mg)!cl4fL3y? z><0zWY%A=9hU9h>vgNnHV>x1YoQjykkjKJAW{sq|%heHz)t4SFS05`qIy2x9a{EFZ zs(jg)-uyy+Pig8$cfU|uD*e5w{z`RA&4#Z&mkjjlO0{0@UkJg51Tb3dm+L?4T zwY7m&lWt6zJKVbU5QDK#a+U>OD>VB{H74XDK&9DIosOnBqY?Ww;b1>`%A3CXQnf2T z4x>S<)PBt-4|9c@SG&=@$q84dYL4X=5nnd(%bBH_noH2W)cqOx*5J^e~u z6Z#kEk!I}@0aBZ0^}W~HL9=S82!9PO;8jMa$l=9R!+H7;#xIY zIXakLU8~w$hGx1tQLlAw5#+T_-DDbZ6ek>bC9u*! z5xOH+AxJn8kg!?w+6>SWXY$;K~p5<)dQZf=T6kLSYg4S3IumYQ{=j9*-{ioU5 zMVSp(G>8nX%OEm;twuU;d+bbaeXYJ`7)(EXttR?*3T8@VTtyp@4_y13k0@e;I##(m zkUrj^MkxP$Oj|a9CIeN9-l$Gc-XBr0WTP5jP=0WwE#IigL9KxW^zb;@pdaIS1 z;$%{9oerpMNM_D>f&qb;%m0P6BA!do0}N@C}FGG$k2|4Z&e#A*P4)hs~Vup zcB8NG%}2$p4!Lhr!-L}I=n@zTGm9IiyR0YPkPVwq$~LvFa`a-8MDOXG;)n>o~+220cSWQmyU4;87E* zzg-=q{5^>BwyPa{-V}_1^^j@y49tA)k&#YsSBJO!U^!=`kGPs!ngaG*kFzQ1iG|1R zR8^T46{j8ey_UWdJVw4#JXRbKM!H;>XFHm|AL@^$k`v*&U`Nv(4kP zFv{7fzGD0y#wg6Z195a|r&_n+MFdqaI7~}TpWmY?p?23kyMedeqaS|E)&oE>nHm;g zTj1ZFx)i8&l$1_1ra-M1`a4!kENNwXA(pEu+Cc6>i7(vD%WzU_+E}1A4~vb^u+-8z zwPLd0mEOQYTN1+QS%KQpd2~~3gi3`!W$aRo${8yS*ri4rKBnoruqFAUiN;sWc)E8* ztOw@6Fn$ifGgi4$$42BJ??u*V`?8!MPM5*-$1b&5`pqzH{rM4RP8gGf9tBJSzYp<) zjt#^Pch_Uae&eXQea+V2dYkip$13_areh4wv}0aApN`axX6{x8IG<{)Qc_ppR}0M) z>&@H!I{r#kyH)?dM+hA>+xiwG5Bej7$$=zmnM}GujrXXjVS{!ue^^+3dxa;@`5kD$ zK25TkP%D(??on&kU4b+RS*({VQ<-0H^V(e|xq^#ObC-$gqoxhCL_m5#rj#fIP zs|;yx43jClJ*S&?P{+M$l%Wm1wpZ=qte{SOk?KFRBy}z5!M&5uwrfbm@bg4G?`QiB zC{=3*5C1_>XO7Gizi9J-3%>l2p6*kVoZoJ!k4ZpUiAxlBEbnjZ$6Xg-lMH-LUZJ-N)ut_m zU*n#lxtzx7*83=uZjYY3O)Ih#KWV0v)BAY}pz|N&9X-ENk4_Y--JIt)Q0c%Kqd)DN zZS9ds@#|@8@6o4R>|v_1dEB5|rgOSd?M&uNRMmINqFY zvp|cil4%SWdjjiaE}^_KgzD~Bdnv~b)8zeXWYlU9g;`@PZbrbUG_@sru2r&<05;+bx!NV5*{>|u|JWmTPvCZ|yRK`0?n$pym>s-@1##Y@!es2ZcZ97%(Zs$o8_ zb_7-+gqK`f8`8|4P$#m$epJmhD3jY$tz&9GC9FM-I;M6~O54#|{yh>#w~nbP0pGOK zYX-|dgA7iyE@W*hqmC%qOO;4lPuU3(%02Ayo1m za2(l&!oOGJmDn}~FMqFgFeryx)BGQ>2+1HS`9aO}+0{y~86ltfG9Dh;K?SXTRBIa) zLo4cgLQPVBPoO0y)H$KeTDm%A*rQTlff4C#uA1_Az=0|1j1QoWrE0UlHxqPVb>$Xs zty7HxRG=JA!@S*n>9bNbRr$`Ju9vEr%CZ)e{F6G|yK{4v7$$2vL^FWNSDP2?{7J20 zQ11Iu(Mh$7Pgye^X0QzN)DT?aO97|UG$pn*y?RP5RK~WV$kS>ErEx17ep-E7x#UeJ zPph4Mj>YK}17xjQTE*GeA^fbiQ^MnDB>!%Uqc49}yD9s<=-$t2iqBe{a^(^Cmv`K` zg5PK_>Uu^kP&RokK8yS%P4xUAc^T3XLpH#Z`kz%>DStMmxo4q}%xOa3pH-v7_L;Cc zw>g$o+w4%f@ynKEChu|+nab3dK#NwUB+CoJYmDRBRUVI}9%ZVZyO~q0&YF5ib9WfS z>EklhyK~JKDmgu`G!(~ntJQn_I$CnQT#IS1^426e!`h13W>RDNwG7Mp+FGQZQ``D1 zLrtB>uNk!-y;6(%oKt7znytso2c)Ojc+6}~_AE)(Ec#|^@4$3>izB>`(8Nk3tu|OP zDo#}dlgNAOVufUicZ5W0NLGA#Ig6v0CrD=5sM|q~%T%yvg?XY-1uIl7#Gy6U_ zn3|~H9<^46t%kWSFtVA-qRtl zU72Tvlato5e$X*;JzO4fn(F0Cq~axtI=KRHK4!JYcDv6|y;vt}E8^YQgr%iDs+a2|ujj6mnS^G!OX~vR{EhX~sJY?{>e`qlnWi zM(`daeD{G{x&@Pe zQTrMUeg)tEs`?uYz6HNthwg)uCRF1lHp2JkP~)5G34>R`KR4A-MY&a*YTr^fD!o4_ z`0f_Ae9HcO`u%scZoPs)v~DXjWAT+=^D{O_ke@Pjs78)b*2|ZUqJ-OOLM`Qmw0@%~ z`?mUq^7T3T^|t!Cp(ed|NByLp3~+VoJY#C>s-_lG{7KMNGf(5}SB1y(*YvK``L5bW zS-YB++*LDbtvSoInq+2<>2`zuxT`h_s`O)7o)alM;vuiKVw#0Z3Y=uTAGNxt_Awl% z5AUg|&bCK2==43%r{i&|x~GN(g!*$e=ny&e;))%N?Xh?!wYaY~4&3I8?X7O}3i>y9 z+)T>3ueNvosbQW|;mjmZrg9=t3mgm@d0fe*`hTeP zbHC9tbLu5ok37#zIFQRK(Gz+9KHJi6%0zEir>(N04_?gn{IaJyhnbO#KKlP#afPGe zE=NU0E5S{_P^i^L9r^+kih+>^%S_|SL!Gi>AgN$2KJLX(t%qK$@(=16nVn4ae@8>NFg z8yv)herL-cfFfpFwi$Ay^K@7qzV@Qzhbk#APo#?vu|`iiM~Rg%ttED&NtJ4YPWMWA zjn0a~px>1XuIRgg6Z)j#3B=+*7&mu-K6OD`HmI2Y9kb0->0+fix?T+S9!rafCJtCLioD(t1gn^DALp)Db-WPO@Hph6nf_F>NKNJpr%t6`#4xO$)Mt(N{ig3i~8`av;n)#nH3BAs@z0rpTvirr{~&K2--Q z+xyb_rw~c=chJ+PYKAf*g}VKt2Kku>Y2xy?%G94*TdtUG)5_#;0}FEgfqBO8q+rQ2 zwV%PKC`l{h1=SLo_q5Y;WAYAzfLdt(k&+A|Sh;_OG7TaqWb)@cf3jRt<0~s%u-z!b zUJ4+u%G={;okl2GO|YAI?nIDG~CPGhH@_1h;KSV4-k-GleQ5&}e#_<&)or z3DiLmEgB8Nu^_A}`A5)SwxVHAtS=Myw`eAY(CU@#jS2LbA|jOBT-vXQSoaHxW7JP7 z`lvTph7{l=W!WYeP}nu&$;;-U>NW7Mv0BTk##0|>kr0&pz1F3u>g=!e?2pION@o$J ze07nIIg2pmOb$tBw71pZXm6dt!DSPXqP3UXm4>>AAAPEvkdRl{6XMp%0waNIjkl?j ztBB~}zKC1)Xn?L`qj%?ulf#V6@d><-nTpebvx;?h>x@Kxw(vc!ETj@PkE73BMSUgU zPbzU0@k-QMdIFfa-Z*~+<{Aw-Z`(1KC48rUQ{pS-%i7zH*~l?N<@0rvm7i#PIMZx9 zgSYJ%=E#R$qrsBF*h6B%O+X%dTDb1>yvuNi@MJ_DEv+HEx^KnK3<+(jE3e$Ay*EhL zfkBu7lR{X6^{2eOoSmiZoC9zMtio4gBfa4o8|ZNj(I|Q;oOCEuGpPt&t152~VU61I zRejf84zdPZt3g{*j3GYoAIHHL1uD7egXcDuCaa=TyZgN98N0TKbzc@v_4c!HvCL*b z@8YBzjHty&Q;+dvW*;p0aq(LiB22QWtLT9$Ix2goQgb2dwV4DN#}MAcujbn{2tMUEw0$lhe(=M2FGg<4syn$Pmj~A>}T@le&iz! zlND|THBP- za!!!;Fka5o9yG_Iyo)QQkee-U@PotgasDqCx?5B1HEf`--9^1Rr(V}4+pIE7HmrbE z2*PYz5Lu1uu7fMlUONny^PXlq4~NG*c7?$e@E8ZaL5kt~-Sla^b`*7~ zCE{LM^#~|~z0b3y&u-bpy*-KJ&YG50DIekJ4M@IKA9)R8hy&tO4Pw@={SkmD2S1`> zz>B^Be_7HlZbTcm^YU+)c3DmDj6Ul{)koxK5^;?_`tPD&If~xKID;vGeu4#MWcLL$ z-X!`deJbggNwiSnD(RU?j8b+!q*0!tjmUc9sMg;*T*|E+OI+SjkydrWat#*Lx#@OGF;GcD zEvC*BA6|45+Ul<;uteVEr2Gq6evSqQA}_9($zDjsUcxtR$zh(gniD@0kDzF8jiNTo zTG_5tn|z%ye|BjGTU$nHk`}Rreyot0`!t24E`yd>v%Ez>P~gXqRnc0C{V2%l7}L^M zDZ^XLR}%lAYu;j~()0l>^#R>(C(?Z%5&z2812w=}AVq3vWtTva6@q=R&Er!o)o4!4 zfUO?4D?7;+_nD0!n{DF-+L&RsEy~8=$z|VYn0#-xrOwF9jxoT3=2#P4hto7)k?CGO z6fQA#=gH3Z>4vX}QGRSoo_?a8;VNbLi3sQLrwWbt6J6rR+|$_3(!p%cT!SeC39hyA z4NmVLO+DeXpEvQKaz8)>7mpiCH~mCo#r+=n`-=l{uDD4S+}!lPYJY?q3tp_Oo_ik6#?Q+3yy7qmRI6L7-^_2F{z()=9zuA_tMEV@% z@qjpgcQ^G37fH?~$7*OnW3(|`h}u1F;&_fdJ;H!892B-xuZM2bQs;7#gZN30GU zKL))sr8+{-`j*BY=WWBF56HK!7_9vHGEJ;2UR7Gpq%(D)n(TO)jBv3klV{M9DA8WI z*^ACbiEO1XmwH8uhRVomG%XrRWkxSr8ZACmTD?xK>WMfpwArlR`)-D0Ea76w6~+hCs=s@r^QzQJF|TGS-q#`;um{r;&%I-{f~C@0p$N~_e}=RJ^iFRy%i(E z++JRx_q6S1S{NhhMucvx7Cw@Bq$olgVZU7X1)+ofb&1Z#h;f*tuuu1dW^(io#z@;} zzG>5zH9yt?PFSf&L(#+dBeY=%i;d~DrXiRk?E;-@2&SU*6yHcBc(?S@^x)8H*1X$| zvKxtP_lD>71#~nPQ0jiSW-DqQE4*qw#?e#^m>W;R$>-?hSn%Vyy*x+#z4y09-(}7@ zy<+C`ir2YfSG{5=S3FWi&W*9ag}y}f8)L80*F-SK}CyMje}o0A=!e8qh=}2DDiZEiOh^ze=n6Lr1`bj#^Lan+Q_A?n22; zMO|h485-SG4660+2U-t+$W8h9XDV$f#w+K4rp|HVtR>3Yh6ff%5WMEi}s!BfCNB85gBFs8P5zU|(Z8}Zenu*=Y z=O?K~a}i^JkZ&&fD(2PneseKQx%v~y=3gMFMOp40w7 z{F6B+=*yOnk(DRNrIqLrKj}vt=*_>L(;~w*U1&}SBkhL|2{lO3PO>5Cax%E9=#&509 zQ4Ut1^IZ1o4;0)6{CD+xYS%`DEBn8v%r>I4GVllbq77zk{10@zjYtpP@|}*sg70yn zxU8Df*2<;dQPZ}V)o*>zRP?98ZL$6~{Ej|vEBuwR4z#nah*eH@pzCeLgop_^eSnow zNBcGH1!%v*4?v~&w`55a6)4q#aN>be!(C6El?N$7n)( z(Lo7aMqAp8j^SrY80wS(QZr0D*4WJQ{Yd5zS!z1eEtnc~5Pj;vaSM7PHbNr0<+a#WH3O>4y^af*J|s1vBiTMl~qf$rGVo$ zi{;iOG@+B558dr3QaWtY+pxIl;pMCF3inBd^=Vpzi)r?$Q3iSBh=zq})-*J)VR9Es zFgwr-j{uO^=6O&rVM*eSE;{T&UY&&xZ8QsE90?a41hJ_V4M>7mTXC4CCW+z7#zS-^ zN%V}%PtdZ*qEy9%)LFN)ebPo8frgsDfF;7rc9n-+D@-ZYyG5{TI7!nX8roSTcl6e4 zUBC|(k_{`FpjTwAL%9o~!t+NOZy@67^ z1F~Kzs>5`3Ka9a{bI;NiFgDY2v+y_0Y@uPdwFR|H#v=XMA$lWOg!w(p*EFn;k;{&7 ztT4CaHTpDJ)N8SRJ_cvR_2fA&^$_~jF4J;RqxiOkyuGm?@SsL&ycWWH;eZ3#lLx30 zoNt^AuOkTM(t^6ChaUye6IYySy(TZO(6q5)nF?4*`&c5D_rgE!dF%oDG)1Jh ztbGtb?b!y|fT4qNMxlf7^sa})5^VJueln~NE9x=BS*x^2HtBMJ!cxWHTIt0u&vmdC z&1rV3*yFVdX#fKgJYvf*vc=T93)UXXep=H7a-;u#I@?8b3;SiiR?PI#4E0h+w zG{`J!D?JM-+br65u3~Se-7gScR7AL9?V(jZV)G*sTr>DFcyP#I9F*T+0qeGxN_NH*l2}-?4T#GJXYfnUtOygjzlR!i6Kz6I@4?~t|NF3v$6lJ*P53xJ-s4h0 zbJR5krTcE`lz|03em7-ih(PCFyIcygKV)bA!-9gpx(lPhdC-y?)N33pHo-kboHBnB zrS}xSDT}sK^IjsuYt#;{13YnAM)lu8^Lk=Et=2b*&pY^Ks1JmXScec2jwSEgydiO^=<uIc5_L=_g)Mvg;J=?I)TUy#D&; zKgQtJH}tH(cu86H4ZSo#3{k>1()t0SL-gehC~usX7%KZSF_W*W9*dAPGUHi~<-;mSS^a!tp5-JqYXl2h*wRAQ7ru z4Wm(mL~L+n820zPWXTo86Iw$Ys@n%)v|*44ul4tOt!si2xVxUt4H7L4h2;5)u(`+Q z)&SKi5m`^kwB{8NnYJpHL;npHH_g^j{tLh|58Ab4#iHV9%8!n;9Hc?=7)w%%S=3b| zqfJ-f!sRcmC$GW6$L&t2{$9MEnhX|^uY@|l>xbVs^7f`QI$(3yTLvdQZlnQr7zh#6 zl<>X-%n)=1O{%-t#8DA}oQaN{6zI!?MOeZry)Il3*DIPM|3I*gfV*B(B*FHKs)5L= z@2Glz9sM&{OjW*EOH(qTyk74^>odh%YV21|{k%@*XZ0HIL51Mas66&Fv0I zk$2Tl;fJ8qA}4T?{2F(Ez_zle*tD}4zq1y*fDP;k+ux=0!$q3^x=A&h5GY-wt^Mx$ z!k_RZ-5VkNsQarT$kYJ!;md>q)UJc9{UuF)6&w9F@6wJ}MF;fRx{-w30h?SvoXE+aG z6&vz4jUNS_Y~>1CI7*}|{>$l?Q6fi~Fpe@`7x~Jqu?5#(7w!h-;#>4!H25X-a|(Jx ztWw_jjP}0)2Swaxi^qsE<2h`i@y^}fi3pG zNa7C{(=X#hw8;tX!u(@!RrP=K)%Ha?XPkIj z1e(?@dJd(7{B#jjzKu>5ji=V*MWb2`Fg^$YLTJt-xo9y>7!Qc;NB%Fw;|q0&d&Ub7 z({X(L#DP}_xo;tz882Syy#d!}{}-#YLAifNi8s73d8HhxNH&Fcf^={#aC3F))V7^- zR;X;IfrQ=ObtMwP0nRdfAx)YfR`|6-mbpP0Y`rkY@hOB#`O5-|nFtN4c%e>%(YW{f zziII2{1<3YKK})bFV3f(6EQpfdV?NL6l?u9B2#amm{$>QV8DFZI!UzkzdHPXN9V11 z`slnd?>Wr+a_~HAnI#7GZifp0MTO&Y|Np5lR((!|gD8p+3{YVJ5;ZDpnoGZCiCNyi zA`2vd(+Z9&SPJLOr5uYG+4s)S|7mc_$NzVOsk(^{(4%TRZ*n3E>rI-Fs5LqCV`@HG z%=BK2EHt?pO=<`2Qa+}mlf@P9)gNi;YmvT|iPiffDtJdsQ9fB%&>~wz8kBQ(>YpPT zg}yYnhErwA%=Ru>SIkv7Pq$+imeV~f`{g`4Ezc3tl!kT+dl$01&{mN6uK2+fUos@O z88CNLho-AcT03+%lqmAi7u^L@YqOv*^lfaaQT~0qwHFVADLW;DS~B zpl~DWKNQ(PV{$aOru=vWbjS_;SjmE3Z_lPgJILQMr(l;|xGSL&ip?x6Kzy58x^2V& zEhwi*v;QBm(6x_+pU>ihd^jsLkkN!;@2=c$A>&-}d6Us_$IZxyF(l=+i&C;YaDFOI z9(awtB^Fm?3^1UBIIWt^@g%z=>F2qypLgjGeSW7~KSST9$??7$t;4^4RXlDt}iC#S^V<9Bege&ysLeWwYlD_2Mt2gNa|L(j= zK8wWs-1-H2-MJtROhumPBXovCH=&rDQtz=;Za+aA^{fcQjm1?p&4L!@7Y&UOl+lS; zDOciC_Z5B!)BHD$-G7Pjw;>d880crVCdB|ek2j)e3vU3VDHS5MJ%Y%`>+r*8Obv8^ zFM*Qh0L4=$<-foqjlo~qOT!n7&9R9!K}GAlG7t}_W$JHN^|z1pH>tn<^d+??(bjD7 zezC?QFYV1yqmBONsL}090JYA$hx(O=J+qo=k%>U9HS?by_Y)8V1r=%SAkX%9e|! zF$2LBUYM^H_j$kndfyjjG3xTP;#oY@o0_f=YYh*nYz4ep!`D*dFGRh-@~ccXORQO2 za#TN(U{fwia(5>3MMjwA6f@_Aph8-LSi21%oo-0Lya%lvWeuaz!BODpKM(7zc zM^Nk<5n^bXj^4BUB1!>uv4l%*gCMoObaIDNhf)@FE&wH}ub zbKrJ$n2y`JVHCa^q_9_K%+fRBGmrqAvW{DgRsT=izWY zT&Zih(`mN!sK5;TGoyvykjZ_&B~R66`30{dJ6qUT{Yx_K+aZE-ld*D}3i?D1PBj&z zM8%FuYEx>d&45rn7S(YI9r|-dG1z=6U{s97S`0I~qeKWwRJ0kDf0Exh!Y+YR_%f7( z6*z~VP#14_M&*vE%!TyEkmaWp94+fRGLYnnq!-%r&~my06OUix*?uozDuMW^D&K^u z$Z9}E2Nq6Pnhadi2A$KCii=rNap^+B{xce3?m%R&kOw=^zMY~`>mu0mK@K?dHv^{+ zLUf$a^d@A$KDZV+yO1+h%R$rHn4Acs@B;9iEsVP1H~x|7dA)Y1r%{+errT22rUQVy z5zGautDD26U;=#+Zgm&%{3zhf6DSfwi-FaOTM+Dn z<=k#eQrD)ZsN)_?bGK9U+8(i5DSU})?uA|J^OvaMUU1onmuTc(`0x&;6)fK?Tnt|E zLDg<&$S&8f8`YrY6Wqd7K8Y=V_NH_DM7{8z1G$VNZl)b3uN-S;A<-fVM)ppPD69~> zrCw093&CeC@v#72?skIzk}A8z7%a`On((qJzgw!UXBGs#>IsAL^##roiVUx|>l0bM z>>gu)Lud_FCSJRCw$vkCZ27MNM&;qwu;|Y}!JYN+D+U&(l<(8+tH&b`6m%;FVoXj1 z&Oeb;eRb}cg;6r&3=izM_mPi-G=f_}aAoM00Pb3dIWe=;l+S`6_A&%)hAs`D_C+GW zaGoX=iPk}HcSSX}4C?vl^w_qP+v?a1lHt)RpcRAr#kZe?g7?So)p)Vrsy* zXbhv##gL5q=%->4Wd0?9aV)%tR^%BxBlsNl9Hc}rabD}m(e8I~wT|Wjwba*%7h$$F zH`{s($7MB-+ZZ#bDg@u7AHVIjC1O>Z9*jgsSa0)vXW@{{oXUI!65uIn)708hd1S=d@@FcI4T zMO-idBKFXT_?I_b0w5(SoZJqiahG()*}QuVzfmL!GGb zu&^}WzYF&RTQH0qkDyDCGoS!4I3~jmJ%bSz)>g*arNT!`G#;XnM?_LcH?C;VD>_W1 ztx%v;Zahc_j&P9mK{|5;#-f)>3Zjk*e`n>@RBCrzOjJJKPMePlYrwTcjYG0Jzynak zHcY0OcVa=$Z$+FzaZjZ8zQe*cx-A|3PJE~Q-KJo{_u?;up_J@DiuaYYKgsWeSfUI~ zrA;S9x_3~j=4(0jz%=OiA0|&%+zR1QDy}Kx0yu7^hhoRk+n+G4pZU|UpG2yHL!)bc zf^YcRHTvf#EWs_CQ|w8k-)%;dPKw1Xo-jTfggXtFMP26$GH%M{I7Mu><}>s0>)fj# zl)fwCsTf(0+Mj}xzwZ?qcuK_ds@Tl})iSsIYxIp_q;REZ9R{;BzxwMiN_i*&V@)(Z zBvg|B6tAlcIOV{_AhdzTO?iP%o`U9cd5@6 z`tCHkGdhl*oEAw+bR4z+S-g?xe@R1KZ?;ORmo?O@n_W`(|J5wc4$!rqMV*W{3Rn!J z&frkS6xM#%V(cBn;Mb*4>0Tg4LmeV)>Uu|*W~jb|iw?O+JND1d||h+GcH^0uRBrlPT(~I2`b}iKgHDgCDSN?tX1E9@>Pe z&WgI;Zb(hDy@TZu>mH`xpm!dF=R;m?Q-^D zGS^<{^c*IWzwYA)5yF z1%%FLKetL`ZP$*!q$MI7r$ro;*{v5~Vq`6-pljzuQ$sj;oyUSVtr2xQk6oHy8ReW8 zz5R;I5N~bYwgOln7#MC)_zAM<+IbP`editC;U{B5JSmRTFTX>c7eq+dvWECvHSX-d zT@NcJ;v~|v*>d_tEMOQK2`(hS7325M0unTNEy9i+->HeAW5-}N9L#pAsMREHLk8i{+}wMRa#sh`xu z5mfCy2Ikg&WkE zNtLcN^*Tm7_CBq@E<#eTpg7Z~kpo_ZVqZs@K%)Q4(5We{1Dtu=g=U#C^Mn}*qzlqnCPZT=*8;*`l38&k?A>!74iz06b z-?!#0!PnB+S;m9mcJ~={09?F;ws|bk&znes2`sCoRKu%jgZ|t%V8v^bTACFPW-FCm z4_H_ol2%yJ;DFXT&BpGiQ(-!vR$7vJ6(#VR2~}YmFJx%4Od9{@;yHX#zzVtVC@cr8^>5R%`xY~h zg5%KLtgzyuz%mrz>m`Es8zE@`A>m!3*KUec;io^+6p^UBDG?mk6^GNq(8YHx&+C>PN|OKXFIC?x0d z15XR>GN1QqQI4k%{RzKEA(FNa$F?hF%^CknR|^+lw+-#xQL#W zi*{;vShr>4LQ1+N{0E+ZkA%l36HZdz6{Xup2<*_N!^2_!pU{@;98koi&PO!16OQ7V z^9~_`FcpTU68Q$!EW{OL;qDbJLFZyxW%%Q1RdxbLQ5+^ z;-rOCSRn#)31TO{vV1TvX!g`nfCalHFVG9Wi$g=aV9eZ)Ye0Dvt;i_fu07{#8aIl= z0yf{MF~>@I?h(N7LUaK00mi-tT%PB~zp!tzGX;o*X|So!@4|D?ieRJ}*n?A=q+9U7 zPc}}>dmNKxt!{lCs#Y}RQZrgRSyomjkKtr&;PP2}MNU>ob_jz1_`N9F9 zclC$a@&?`f9U*l4=acC+_=zNSye<6QY6hZByQ|aRt~B(vhzwexb+wRBYE|>f7PKa} z(^6E+bp<$_Ju=Pq!>q!X$&{lYSl*OCC)2DGsLDr`cq3zv{s7xn?S4w##gdBSb};64 zx5dJQ3(e_{98*JA!f{<0Z^*yfgjN>gVi;T!hHVEDEuN6|HDL#+4+PSze^o@Kw}LBt zl6H|yhAks7%ZN;02=$7ZX?X|S@KOUMlk%qda+m)8oXLIwPo3t`v^!88&(5Py@1XB) zH|hI3A|gQEsFr7?kY^xeRmE93&7Yj_!Z~r#pE}(Yott5xQ>{~@d?Aaof?1%B#7%M# z(0IrFw(b5zc!rf1Y^YViOpq=CwEeEg%>B&I#c57nZIB6pe<7Hvg3sT3%TpQbipVl@ ziu4Dp86aMk%hBXO%xCS$5tnn77Su<=Y#t9Jc*)l|!M=Ac66ClX%nMthUb353Qity< z+vBY$#sFIgWAm+V!IFCj&~NDE3o_yKe#j)u3$lBKZgO`Fmaz`=_wfdg!SsZg7>{ltEs!=}&lYt32uWp9sjE zWurz9MIFyv8yY>;bTCi?cFcu%=!rS6sRureKD#fCdEGzHHWbHe!imXGg_d}E9DLDn{6*rQOzR$#kMW84Uzp`sB4WFPiTR&0V znJZSB_DFL>HH(n!|gK< zeL*d$Ma>_J!Tzb2bne0A1ikeorqG>MKL*tzJ?QVpuw0njDfBN9)O*Qn7sqjvSbWn7 z>PWxp&dfp%I_dg5Zzrmxy%Lhc<~k0y)9Nd2X;!3kbH~&eaf>-^%?xxo-IkDXk*t4# zrL){=?O%vP8gqyK{0rL!x=gK}h%ld4mw8Kx$Y%uM1QnygxC=D;iHP=i2e7n=tw(a0 zJALv5NDiw>`<}q&@ls8uLsv1Xb4f~s%bQM}OI#wF;5?PK*#6U*41YtqwsNO{zr{<< zm*F%wd(`#{gdQjTtGdsVK;DQ98(7qhZ z(j4M9-PV5HIRruCGMmI{Fm$v+8iI~mP^YKp#fn+f@2U7*Sz@G=f5f<^b8tPPirO1O@6LD`C4iu6`^)jb=wht7QO6b}*@fVG54Em__C3v*Ka&y9?5|v zi~Nic?dl+d1r6?JbN>%$%^BJ{9P9-&4h^fzm5e3No)72k_ZCesrZgN1)RU}Te8Irh zhAdE4xM*Wx@qqcBl>w*nBu=)vEV^QBmeg0v_0_|3En^^SwdGRg9mN8Pz~8(no=#c4 zA#eFS#B^Luh(vTfNlUpS5jp*`!5BaKxYjC5;jFqzc~Aezvb`XS`^;d8?@=~W8>4T2 zv=%_sQg~7^8FZfxO<=ob#}`c`8|9 zGQ^p?+Zk-{r9F9B+^RcXvEv1hTXHpQe)$DWeFk%^=UPKnBlJoZ{|hw#h4Q^n9-Z(6 zhUKt4SFKe{v{Ny98k^`DRWs;>Vr=Ey?>ATddcJUHqp@B}Yo3tV6Tqxkz+29NWP&3A zQ%%3tPVgY^a;EOi#x}lv!76!=uUT;QI7=ObkW(^ zUa2{qLR^dq&hFP;wd?h0#%;Hnq8M&~UGk5XG{wc(+i;mqxESrq_31Rk)!47`pZLrg zHUisWL$AKgBVplK$e;NE1Lt-Vb7K6p!=RcZM! zZ`yXJtoLDUL%4oR>QTe!kGsP(vfjIlOK^F{J5GT1JPBpBxT0aT?CGHwJI=*WigmnV z*$9Q;J#(dq1mR1CHH*W**^Gu4=+JZn!^RbwajSdA#A*%6o%lq+zlW>|s2 zjk*crklYEVk9nRoUSDV+)_KB~a4K%f46N=;7J@feY(9|hT||fDla7Lepic0N>8|7J z5`UpWzt|ab|1C#(_(A`YJy1oX0P7zcp5l{$Wfj44_xY--T@+(9HdPX)&_JWHj`Ow4 zuG(Sv8X#4vJWNZCm)*GV z#)T|4o@3$qE^Tx(w)DRHL|e#fSNG;x4n1@;rZs(L)|ZkJECIDqMqf(2wI^Om_P;34 zBi^C0HI22s)BX!sVh$~8~b&YqBySnhVR^UFJt_tr5UjfD3s5OR2J59!Z3 zc4GTqkGMKzh07L|4y^yxu-`ygXe!Nq2CHgQSlhWDnmd=;c^W7C?%DaQs$A3h5jdfd zQWnrrPh*GR(a7PenQeWJz;1!X?@m$`?0_CgXDQ6f*iqTElZJX3n<)u@(gH8Y0K->Q z=w-a8TzWv8yp7$Ixew^6x3RO5{(w697~l4|aNn7GXxf=5OYhS@ALBWt*8?J7<8R8C z`!wCpIGJ7>WAvn1_=n>}u6P!hoxcs#IKWuj z*%=S%0mgLoI!xhme@9v#VDt+W|7Z-+3p-acx<11k=!sc5gEz+>N%RwnB%eWf@Ju+0 z@HPcSBC3mAfD@gCswcN;$F-booiqVBL(ap>T#dpGXKD~=3|AI*q?ZDXQw^0=5{N1N zXGgjfXsoA9ts?Ir8nzn(e(UHo6j3brh9jIflF+6lS>auoY25GYho=q)4lpXK$ z8o^4&pJ0Gguat-O>9q<`D?J2Y7HDAjC__RJP}6w$mfUEenYD3syck1EYa63GT!sq; zv>wb2&)>#W33=rsNTkvBmA;l;x1IEdD9L% z@n=%lCHDomjQ#LtTN`ZNIr6Ehq9N$<4Oi-8WwKi;xInrsG6QGw6sE#yd~CM!(UgeC zXomeEFj|pwgt4*G>;^T9Fg8#Ob0{;ym>D>KKHuTW-Db$QD}rH0agsZKqmw9Qvi*jy z>%x?PP=DM)cZ1v_jlmh&ha6(4;OEmn-@2dCPW&2Ovh$veY~e7)*49*B8|<0TKx;F@HGtm zygb6eDz87q{5?6IF4i%|D^+XBRM!~dIiot)$hleLscl_jy4MZdyaLhh;09S>C8}%8 zRz90UUQxz@N~c%pttex#Qtxe=9cApT{A8s|QO1=@V=K*!hS2{oBd`z%_t zfH}Dp_Y2}Vn}x_%`g0sVuir}7>ls@&?{Y;`mGCK%$%T%AC`W)Fw&*2nQA%KXXJ6II zK$)_5${vN(qrTBEVgRmM#f}CMs67T4a3xG!^_hDFKRd~H=N8PWZ~WHa*7_3a0!G{= z8qmPlN=dj(^BNfYDwQ^p4UA=7KAa8ww7aVu-zG}4sW=)tKyJJM;e#ttSK}8KctQgz z;}CUiU*~XG1e&d=?WHwGy;W-S5Wsx#z6O{=k<=c%h=sCv5_&M#p;ckNn!vG z`#C&opTA&nwE-#z%k`lk3~UVpVWlat#(qIHxrep|)nQ))o*AY@c~WY+*{{pS7T!bW#>71+}PN}``vT+8n6LmY9~H0wi%Vz^ZI3ou==4Oi=I z64>`do<*8M44*^W8;LI7+qGkRtRjvxZ=H*jJ+BbV1}+Dw|=MEF;1GgB=`=aS3Ak^{dAuMk=E zZ4(h`Tsw+xH4#;#s&M-KOJ z9xOVh|O}=O<}#9Omb@Q8Bu~u+Tpaq-+NO;a}gPI^aL7Xo!8s(t<$q_+)R_2iyFbLF*(?>hskCfuxEnv z-yZ`uTNiPx`)GG_(Zle7eB;GJZ7V>=v(ju5eHst0tNU^KAzq9z-r7s?Eg-@qouJt* zL<4xG{H_H^W6*Ir-vZ42+$r?Dg$Oae-b}$QLEP6|qINAskFp`h(K|;cgCaF|WZFvl zuBE74wYiz4wli|r2MU@4Rq_+PLm?~aUP}l~Kd+#s38Go3eXF`m3PU;T0%&|zIeRZN z5wEmP3*@s^v@$_di@gh?x`#D}pvX)Vo2sHy%5gR|WO?yh_Ow%kE2--Hzzz@}xn&3a znII|{E|Z5zq&0p1iDIwcpv&{N3i_XX>cXmzbhh&*76)7p_}@rm4eRf;+9c|g+6Jo| zS@w7O!6fRJT;C8MzmTDocy7Eko?f*Q!N!B*sak8%Q42h*&%N(qn%r8nX?gsR{+)+k z^HvnwN;V8S^P^NffEId|In<1;Ue&!m7d~7$ZRXV>anYLN9|w$dCSWqnb&O zjOOp6o=Mnd7o4DpNh032rinR8{}ZiL6@7E*aFS@2dL=={o<(BO_h#E=d#@5cr?L;f z#!Czn{9UW>dp*-J&a{O6Xrei|SeI6#%nBBt(;gYS_4<{z=XI$X@iLD{`tHDL8VED{_b)IfWaHhmY4}F~aEHl!}r?y79Zwl-f?zF@_GJ zDec76xZcB9-6SkGym zePkEfVTb9u6PCko2BX(|q&W`;H$rHctyFnho+8Es&4nE%)41vQh0pVp*i7>zI2r>+ zr;ntnsUoCVqBBQxR}S^K8~mX&&8jkzY1TAfSrw_Oj}S?84=`T4X?m*oCN^;wQlJK( znh5mGo)!k}4-UiiWAEU0_U4adGvs*lx>neP+o;)BRvtka9U#Iz-${!*hzeCM59gNR zL>z?0mJFNj#yq# zchEAV2b^c1@=^1J9d9cv*g^RyRP&R0Z%(kx1xc&e(LQ^Fw<*;N+gn_1MSh(`E74{- zFYzi(=h1*pqHg)Ju?i0tt^gAI@#f~XoYr*`k=`9PGY1}Z9#r9QIl9&f*weHXmFg_2 zwtNb9G1WE$>y<5Ya?&xOtoL#Il-rEMq^Gk{fMJlrG23t?7k0#-gJFzQleg30&LAFl zr_ub*BH8%WbvoZ!#2R~kPu^WbP)H`ONckK$*%CvbqS@bzk)=}tSFEl@F@s?kNU2>! z8{^;GXhj!MqlHIHPWHHz{q}sTHopu53u*6s6{(EBp{$sFgc;p;&`q}AM*ns}>tlQ= zva1NKbC>G^Pmz#lAN^Bu?5`<4ZHw4(w+%0;-IL+IOa<6ShAcx_T}6g_vlSjIs+JDS zVw*}Szr(yRCY(#>nQCO7eTno#`f;R(iTt&%a5jSkwt3?`87B7OK`Ad+UZRl++zN2(6{}x`WuouciF%Vu0Zn zs@_91@{-M&9af{QN9jF8!-VH?fDTkwt}Rja2KnM1X}p0!1;;G&AM5lu*i8R#=DHuz z`5t0IpJx+vCNvN*V+jvySluhc>x@uGpnkZHhrTF_!GXtX+@a91Z)Z+Q9$1Sivy(Q3 zA=3|>X*4piRk(thZx*fVDFPw_u>XMlB#XSpp-SkcPT@ykaBzFlWgR-QATxj#XWQ1;kyMITPY9t{5b{PY?0s5zIb@ zx1ZhQnvVdm?8Fe?FkLAq+aV)dA38h+Ev5avM2dU)r5;rO11%6{e)=cxjf&b*@;pU2(3@I=v~R4^ZDHlLqt=6>Hu5QkAuSSy)|7 z%w1Pu`}#MOz)jG)J|fyUr59c71D0y&msF##7+`$y6J_@mb;`G?4|=@@N&vHCFWzZ# zlT~|DUSAQIln8yH;_Gx|8Jc6mxXrg5_V^lM!7#;H)a&w+g_P7Yl*e@|uE zwN#}a$mO1ul-N%+HqKp1Q~Qa8a;@M&OTqh3g_g3`W;)#uSp94h{n=0SF~VIyyEM_+ zIB*56O@r>IpKCd=WYh=b6`08j;URs~Uj+CzWrC|027_aYri=YWjd~YX z>#L+8%Byz2SEio^2vb%q^c12_vSa5m zAd8YE+Pe-_Plrlv;9QDNhirG}Gn$z$!fUL+O$q1`l4j4i z*~!$pJsk2m(fN~bRekpyI*~3yeHY@h=&W9yBRV;pUZ;Z}`>h8B4HVVN<@eytWb%S! zTU8~ebLfzZwfD>(*=TIG_LF*v70s8s{Hf6hWj2a55=9g!3>2qf|F-!yFybV6rp zt43#KGt@mEXVPbbM3n!>h%MSL2f~T6s{{RNQ_&z1W4zLhLNlNY`@9?V%7FMXqZ`f6 z5M2y~^izhIWIVTmQU{Cr#&aEL#$YkpIH&{NA1rPK-T+^d{p`6l6zp%9T27H@@4IpI z#}E;2m`OfEvEd(DPj!Zh_P%W)ePi;l-jrpeC*=$kseWH{1tMmg#m2|{Gvk)mdb%?d zveB%gROUU=F!(;4-vfiQ?&8nnm?F5)Qdu#zd_MR7I!b>JY{d4XH03?fCvNp|rh!i; zhoeg|sPT!Lr=`9ct~y7MW7jEk@yR&@{E};?GlX&{{e+3_MQ# zS@dWaxPs1Wsr+zJyHuCz5OjVRM5f`wui4K_oPybLS(g>o*CuhbA)JSbZ-!Eq{!Nym1ycH(c0_B z=J3+&$qQn86FM;p8{?8D^l+5u5a+!br>f5^)?@J6vajk&tStF_6-N!XZc|Y!IO?v7 zde54xqLy>iPpfG1XtBaLWfl315&l{)44(;hpH(~Sj}fT`AZ_yLW#h>>OHEz|dzigL$TU0vo+#TGH5e0#iYo8lOk z;Pk%#RL*Hd)2)yZ%U_{2R#5cEmubIM)J@!WRpGq_gbnwztY=+1LV0f~s9ue`dF5Rqp|MgN&MtsYPHCW@NIMJ=eyL=jQhN8Q1JbWjoj9^;=-t}sn} zw1ieo6s~*ov@N0HiK1ugCYHFpb@^49o<64fe#q%NBL94dtUt)xhn34*Q2=NfJHQZ zsu*S5Y}Z%Pf_Qx;Ewt0?sSrfdV4pe--EL&3?$bmgW4C6saGKa!tsUI+nu-e`2jKnV zU?rh>$_T7^yC^Ad<`((Zw*$xj|%@tL>eAs24GN{$&)66U}+_-%({WuNpK^A9; z>BjHUX~rzkTYJKrK0351hi=Xif%SfJe$UGR917YD11;g-bp8cenr9cBMMz1K!+TNa zY*E)Z%|Kmei$-$G_RMrV`^)-qTzQ`9E0YcUL};7f!dOO-;{dnuD`J{N^+=%><_{m#6k zr|a`F}0x#EwC*1J%o8Dt3qi#hmEEcOKf1_P40d*0B_9M zfhNrZxNRo7I8U?;*i@Hi9UDuQ+E4X^(?%wWm@g_-orG&I7>4CzpPdQDP&Z6Qyjy~s zo7JCDZVzS57lXBbBe;TMxp5|)n=h(~c{6pE*Fyiz7ga<$J~?*>s& zw${PPj(s7FjBF7yV1Wpz{Q0xu;-%_8KHOotW;WYiVA5u!)3OENQFDL0QY^twb z&Gl9h-(naY!6Zf6^t1a%Xim4>2m9~=j}h);<<+;bTjUJHI(_SGk&imExPrWQ^}tPe zy(8t?L^Jn>1DwW(YtL)`_1@ghaXs_vZFfWGuAB4${GtW`*NqOyY`dxPLQ!9IK?W!S zKj}*&7lM_#IFf7&MYY~uGteo%Kys$&^Wb ze+Am;kSFe_i_JGe`$7gj2nx&H&Xe;xsQzz~GdhsL4#i+u2aUFifF=?yE#=`;ikElZ z^1R5TTb<=Rk;29upL_=w&Xs-7CYEdbexN;e(WK5klr_zQABSMI!{3po6lI@MP&7}S zr-Gl|#G0q?Pf?{s&}%hZL@A3zkLnuA@dj`Rowg@^G}I*Es!7kwdvod2ba;{IWVD?o ze}{-FKhKW7BmdkMP90>mDb(8`+Ev)CZe#f@_=X41hD(@vILg}Xz_wwYOHUl4Ud(2^ zNGO;0z*Y&Nv{*gpXtqbn^CJ~1Zj%3DfU4juWCTNr*9B2ari1z}7G|$l7$fTN9MF@# zT`U62g@!9RWp55s;yw z{e+}>6vIa=XJrbGI1#f+{vJm5C6KEBnM8+{h!o?YiR8Ofhg>V*eQ3 zlMv(b7dZ^cQP)xpuT^b-O~k^DedIHrrFW%ti0ws}mV*6#-IIKmiGKAj|Hq?|tGhYP zE#Xwj(fu-SD`0)yae#xiWU+2`d%G=txD0l5F#NE4SrCVfm0T(C)#n zRTPEkhv(+lN0azn%hC|O7I1#M7aq*GVmV!W44{fVHsLy>EZz)`@542u)j|yg9vtTX ziat( zTxoN$Im}!DO^h3i+mvG#{Q*rihzmW?S@)&R?qtHtiT5r=mE5YpeMabm_$6b#2kqGGqSoO}!yCIAgQ@nKXQ?~+Slyt-=aT~hTOSxbp zEC^`aHBUc0OxgsafHxFOZg*TaZmpNr6WEU?c!1A575qz6$a)9HgihSi|1Ym_pc~>LolI0O;VeLs8-;7>mH< z@Pg#o#rR5Kg?>V^PY2wsgMpLX`%X6iz>sgYdH;e?QO%+C-}KX#q_&>d;~{ByALD#> z@16KbR$DPf355R7XAX?Wkm9arz&=F6{65uYxJw6Ch@k#__Nn2jORzb%O>kz(WZO^t zcmXhmDMl7@n{9s;olUjfAm!S$+iF4C;f%-n29XMAi>U^^8*P^-j!#xR4j_Bx%p>FBQ2#lM# zJ*_y=$+-972lu)fXGtM1eudc;j2WX*RruX{#BC6sWfC2jvYpirDdxir&n;MX!1-7-CP9) ziu+i4u}Um4dXA+TV8so4JX#pvo#5K@Y6%fCjcMl3Zz8n8qC ziceRLvd96;F%ZhKC|-0h=bB5&<%o9Y(x1C>`tsK*`8f&~?Q}&}L?m_lP$ahSLp%nD zg*CadJU0@xvU>3g1o(_Z*})w~7Dz}}6^Dp(gD+jV=8mLGAHoJ=@JQ;k22B2*ku-CS zsMx?TTB+#H$(3A9jI51sSIa*>MiUNy^<&FN|A)1wV{5RjM31CbYal`ujG&Ppi5ccy z!yy_$G*`}?Ci^C0OP`G>xEHaUW;4r3S$Bl0eLos;)js5rsy&VyQMJELrpUFTO@qIX z7ib$PccKn-$^1^CM_^abrDI%H6|yDMXKO`t#Y?J?WkgY;PG$AKa`Nn5ZR!hf$?q<;`l>08A_44(B*XKwc@)V2dY5zU*RgI#8|k?5e>bg%F<;Bt=k}ygIXgFie?sp zJmty3{DGHS`Qz|la{pLF`X5%M;6fT6+8-66MaaE_$^5bC&}g~J?<`#T-NJtOfE=$p zc3KZXAIh4mhcbK!o%mSPZ`w%Z)tw0J&0uJO=w~PF`s{V-}sJ!2vGs$;t`kq%*)!BKSJ;Uk0hB`JtoIYyrC7X z-E()_lfk$ZoIwXZ5f$U_4ni+7m>b2mj%!X=WqI_G>F2Q$fy(g!CtR1OkWXd8wy|H0 zOJ2_)zfVPQi;t01I1r+yV(gZyjOrw`a3j>GDl!X^uDPCFi0orIJGwY?5KZ`0MEDOu zSNVF7;YKo$I5}dXubefAwtgzY18egz?U|l@iJQ-B4F%@#W3uHS`ukH6Z|Fx+pNV-{ zq1V~F$5Dp$Nxaqu@TemVG|Om~T#V1VG8masVxLVqrS3TuvVxQgY(@coj8Cza$7CH~ z82H>>IB#dLD&=$!NGzZlC2WU_GQf5^A16F8P)&fEcoo!E)fdk86`RyXgoR zR)+cD0y&Cdlat5z>ML8fq>-PCYPGuHfB`_-C6o2QQu#ccM-1M(sUEXR)-OX_J{P@W z2DemNF6VVcD6XxKK(PAXMScMD32e`ydbQ%YWRG4d&RsD?o_%TQB|IzKZ=&`u*{QyecBr>Y^yTvx=1@|PA z2fM4SXcP^|W3$LG_|fpqBBZPj)DU}NNKsr=l>zA%Me8;Tlh@8PMiaQW{3gv^y@BjE z2{CA6sr(+Uj1gEMztpJF7h-5k_9$$0C6=mp<{`dxyTX(IXyQI(f^y@DtceJ9UVK*` zzx)P!Jo?kQFGP6PIf!~k2hos$GZS$;9z&RX!UEGCcLaR0?O(^7BZ5=3dZa^d0m{-W}RF{^|;Ti6ug zB^Ch6!&0I9mbWe4k>3N)l&ayZ9F8ogH2cWAT(pw~rW;?0OFmJ3-SHY?0R#KEtaG1^ zd@Vvs|Dznb0?b7v>F=*avezn@jGoynvW-AWcCjCORRRq`j7b;{0&P}os z3b9&-7tHZ)s>`b+ZZL6`OXW*YV!g}7TSd6|{ui|izG_5K+eCEh9SxW-o}TY3r*g@h zJP4(rlJCo;jJh(flopII&-Yx~yiL?AS9nd;+Yu^k z9NHX@a%$Qmow;oNU*poJ^W&r6fkT1Lj{!#+p`0HR+^FvNqITyiz1SA2=zT=XDwh~r ziso^+>^=?`PQs$)&q1X)Tj4P0r#E_r&))ciVu5!X7+bOaq*54t`n@pq!MzLNfq+zeJo}mZB$!Tkhcg|{C^6e?I9_}+t*|QEPb(Y-y-!-(~JogVBEO%vU!a~Ae(gojeQ05#;QPT>1m2PfjHVtLTYo`{1F zn^h3n3eN);#jX7k?U}F#MK!i+sOdm34cjSdW%Yp)v4^QR)lWGhv9>qalbm3L!rB|e z;Vl?>h8}juyw~DT^aac_h}lCKu^XXs9GVRqx*|^wMWH(>_R*`jI`m1oQeT>4Yfv~) z!FLwA1WGo`jRjRx_UqSH4;*1wud!VhC%x@Ot>lan^HU7Zif`(7QHv^rMluexIf7P$ zO$Wg);Hfdw!GG`zYbI-rqG`??mL5t8H`mvh;sM^as6YO&k-2lsS@6E{{|?&w{~h$p zZmvPMI`0~E%r4iU7rVO#-J+wuSXsJGbm3*(8@z-oX779q?hRhbPrAXl2N!|w;|#Q@ zy!>M)e!OH0uFyk1?%>V_Jw9_ELLLQ-JSV_`Oz7-O*rCd$X58uM2pJ57>s-gft%E(} zCf@Yk2*5MjH6|XOi1^`JcGM3BZYGK_S-l4jx=FT2vVMJ7t9^({adOnA@D2h?!y%rr zf5*1?7mt7W_=l@{#r(|mdi=vR$KsLzBm8t0GdLl06tlfs8va4^SWLOw!Lol9PKUPR zWF@pC6>b+1bbB|h&G!W~lWqTidY)zS@*zk?Pat&*bb^VLmxf zYeTJo6F9a8s9zI25Ob@G5S#ttwPi}8eR8lV_J(C#vDq(sGUl$GsrW}zcCq`USOAAb z?~WPG_oW(ek_U^n0=chw7oIx~8qf0Fap>80o;wb8F!0=Q=yeLXO)MG)*f?B-fN(jx zE?w9oYQgp4^F2^ocS)hZy&`0ICFJCtls8e{6xpHi``F5`Nn&BaM=z|EFjBth2Zthx zrXQE*P_x=1W8P6Tc>!`2@UIU3u`vM?3zO~mZiYx&VGH_puc%Y2{B!1dgF+y`6yzgQ zF&qaqKnImV`bn}9=%%%&;=Q7KjIPB4_IW{5+ z&RgM&_h|~+B&wIM_&;f-@wIekr+wl_gLrUG0pwU1g&lx#Mum#h>42zSV=r`UuGf*j zR9PSkq}U$IL9hpb0p^83`T+UNOI&HPzDn>|ALt@q(sEkHnbm3WT#Q;*W;&ITPvw53 zVE&o+^^g;4(aQs{qKW3nH$$jxrVW1yKe8T#?AOCgs}71yO=`4N=aoq!W& zn*nfgzM@^%pT5W!2aRcoH1d#W9RBNRRY^_UumDzni@Qmn|G|E)DZmc00n1zNYE8!v ziCU(nZzpxVUh)t8@I{_0vI`0qRdUVw8a?@#N|sGj#Z@+zrv`^b<+5j6gY9uCse`E@xUnu?D=Cw8g=)jSGo*_r_~@~AjTD^7`k#*Z*2*7oBb6l7UTCFgQr-^N%6 z{Sp)s$!_@NTk7gkVSie>UDTx3$3)Mh(=GKmJc3_!wE*rWaj)P2%pQE@XOH<4I>n(tyn1CCy&I4LrfNR`cz34>&j`io%pt{GQKn!*s(TzSX z>UQ!iK#lh93uHU&BwUBCn2ZK0f;i>L->vHG&=(XePk!#p_N9e^D$#jvXKbVNw75!( z3^~43cwB_~@B0oi_}9Q@my#F&cT zK{(*yomQ27-~)j1fzU!9x_v^V^y!5PrFU}$0!up}4D`h@2WBA_cz@#&@NywK2-;c^ z3BKxy79>=7jD+zIToA%l!Z<+47rNk~rs9&yeC}^rcv3X!r0So6RP@2p5-S)w>o@gx zc7_XC4?K!n3)*(a>6iM zxHgdnN7PIY#Fhz%R_8S;@q>sm{TmCG_J-UdnfAbn%#pS<>imHO3>Wm~V(~|8O{yzpgI9i8RPfG(!?J`NY+YJlN^4X1qwJ$LRD~_^5P>-`w8C2ZTf9RA_)tySQL!1XyI|)s+xLgzM^@!1yGOy> zyjH4sM%4F(Opv24!KJc_a8e8Ec}7&J-i$|hxhT}t;>|;<#dCqoZxqYQv9#ihh$>sg zS)DEErX4G)?OpmArRzKcAD&`s4!`9ya^C``9O}R$9wN2I#n3g<;$it_eT-#TicmvOS+7C8F+{G|0n|L1gdVR`C0Y0 z8x8aS-}#x|lwSWR>WF{Zt8RQ&ilWYnDgpEG_><0_Uv|0b`$bl6N<+?y=%&q_i8|U? z;1~*+Iga-e56qc$D7w)Ak&GU1n@-K6b7uu!Q))tYQAwm58f2WXZ%^lGGXP)T(EpF{ z4%8cO@Rxu~vT+kiIVa{CgZ`xR=U^Rp+=I%T7aap9dFcJ*JNl08H<@Ch_06NndS28k zKRFu11xiQC$nreAD%Cw``+3nQYaJ3=A1l$E6=QE5(j!v7ls-n$?| z%Ps{E3JA!C9B+?~EkSE9hyX(bZNDIb#g#@1Y2obU`UO$J@20zJLa^H%l|P9(hFHq~NmTTW*KO{dH#Xpr>E2Hws9l9y zYJi@mgKC5$og;i;{S%-(0nswWR#&ZaI8Shm@3*}Qk^X0VbsAEqi=tkm^`O0G$NmJ= zCR^it3<#WPeA9a?+?PEobmPR!QDD&2fAr`Wc8%i~g^%|sMCzN#3!h^09f$Md7Z-(> z@o_^Em&AO74=uX{Rmr;Yly^x)mh?eK;IQS%C0tkg)Sc@5ESi@cgC~uhj^ZF+$a1o*qISR+^-;_*4nAYmgFgCoPVdJw{;H^|{ZRZ$ z;rPF!Xf?j0w&H3POL7o^@5x>Nsj+{_bei#D@?f>A2jN711@du&M@e24t&QJ}qI%bG zbs_rCS1?DMROb!&{2tMWYdBoI@S0v+6REz7Uf`-5)C&4yC|QPj{US_lGcvehWnGf! zllX|w&nlpij@z9B#saV%F6^chRQo0135P<}P?J?tFfDAx`iTQI=;klN8b0N7oFh(QF>wXmjjWe#1;kwAIvgv1@J-EiZ zR#g2Uly@i|-EEuNVSezWW!FWR|KCU0Pyz26SndO;YN_1ZPiL-+ie~5x~Q#v{o)nR_BVJyF8l^&yGa`!D zytVzfI~n=afCy=tZ-pA34SrY;HxZ|T74SuF5I=KoeQ`%qaK zT!}i|6g9HS>g25>DuUF2#%9Un=dU1|)K1QR2``yAA~!wd{_)MP%hqu0dE;2NRC0!_ z?}1L*J*uf3tS5T25&bN`ujO*{z=9Ue!~eqQX7GND%@`UO39!1+!#&7VZXspb9XID{S6Y_^=KNWzN2ZyZ(@4Q_D_Mb_KCi4Rz1rOJ){C( z0}l5YOpSlXm6x&oXvy!Qkxv;#EffcyR`|3go%aRJp5K_yHq6%g0`0Zi|M^KqNpo9A?%zck0W4v3T(a8+)@638)F% zX)}%C$d>+2gZ{JfpMUA28Wi(~nBe=@SAYl$;=1H;nD+c3g3CUC z>_R&1$sU#x9u#$3t5yPuW9H~ zH08?#oL8LucCWs`j##8KUVa2G$8ZQ~u!@veZ(GZ3UHitieCdk9?C`4+}-qU zyoBPZ^|8cgH%gVl-|&huq{yuL>_b;y7H#EbCgUli{kiBdE@|%BL;py4J2IW6_(CzN zN#!ce|JbwItsEVP6*7Vv-$-h<~w{e4lZ)v42}7HG1rGJ3i;)#>P*Jb07wmW9S*S+6qY zE(`UYm7BUKY-NG(ti1Msw%vzMvArNdN>V!XjDjABcE%owH0*&GRq~t9P}3)L^#N?n zzimxEe~Q`0x-scxAyLm4s1uT50;5&RPU*NV13f|HQZO(^OyF3pBjpy`iA zP#Ft$a2^tG_cbce=Z{50!#{N8vFMc56;~g1*?cm~=6J~q*ZOA`K;E?Lj;E*lqgT$S zAjcuBD_U61X0~Sqrxi8?H|cVC!`~mj=lXW4pk6>(5G<#qy-QSTyDFkhe~GH@EAf=* ziNC;9|Lji?sH%JZzTVvp$eKm<{}us}0iYyUgb7$Um=r!M&%pt&5|fMN2qf}-CA%Vw z1?h70A+r80nv~kS0YKZggSP%HT2*Q2rw#}AnTn4j*>0!CzMKFro{cbs8+2nzgcEi==vb?W1~ znO<1-1g-rS`VA*qfcBQhbnjm{WAd&^rYE9tg(6>!Gp}$w=8_=_ zsmtMAel6Lbh*aZ|k9J;tBH&4`gD)jK6A^*`mSHVUe_!X>vU8W}`+%~aiSEK{jcQ@W zpLFY)$TxmbjXwQPEHegFgL`N(#kYdDnnov4_w=T`=OQ#J9bRP`Wj{5xsvYVbc?V=lTbOP~TBn;3gykkRbSXiIv8 zfwQH!_p6n(;iYKZyw^3x3+wSctn6jn?s@f9vFllNIf7H|9;dFlI`PQLYTTH6rpea?Ff~#_eMiX9%bYHWkL#ZR%%lhwILBG8c z?O<70@3n}|x{0=BoKQJ@=320IK|!K3E!-^`tS8~b8``PVyLipOws8Rxn7oXEcvlG5 zl(-G2;*xFEOa=2VBNcIM8R#pAdt%aIBFL=hjXO$14ttrV3Ud$Mb#u!CF@&>THYUK? zp2Ppc6~OxWp~@h^QWm5e7Ob$zv(4Xy8jJjs3seAM-y%;wLzu{9T-^MB6wh-N z=YT}N_2Nyl{`Qxtg9Ke6UdiiI}>b`BhBo{v!^8fEWUm;Mv=K%kw7vKFF zyoz!x1*bfJ|M9w*e&{6vO4xQ@cH7=dD`(8I3}cjmMV;&2jw;5bea(VY-4K?Z4KI(cp)GQQ@7# z4zn!>Y&TY8)Ct=mvuy>-EIK`QHjJGr*KDU*C}y@>v|#AHvR@B~niy`5+sS~R6l^w} z#2&*1VQ=m9JY@5GS_~5;6>HAU9~OIf(oWp|q^$& zdR)6xf}#$T^3PJ1atDIZhXTU0&Dv5eW)%Ut*wvM`;P!yU9I&4P_5scdP2R3Yh_O(!o72DRD z@weE)du_2}F#crk=f7?~ORVS*a!29|*9gt#ET{_S&gVYbJ9WZZ+#+v7ugxp$9xTn@ zXVe3;KCTWV-i##yqv-CScQ<5ny|a1fWG_|m$RkYHc%tWT zL4(N=Z9184S$fzVd2A~9rpbgz;FZr@FOU@b+H}=l$1wcFR`kiP%zfk3KT%Pl{c9dB zjzfa+lNerTJHGc5d;L96f6u??KJsem|9u@%8Lz!ygX$a>&jJ~#hM$9qerK7bW`&sFVdD%A&hQl@$m8d%rQTGCsI}sd?YJg`L zvOhfwoaMlzq5A!37_yl2Z+T$+Xv1OAe6f&xka!pd5|Co4Uv6V;hlyaZyip7-MhNeD!3Qd z{Im98$ml)re4MA*`EHNB`z$%nSr6V5;`B`^j_H8_BG~eeRE2Owa#v=#Dm-d!Q8@So zx@T9f`M|@V$~q8P)i#f_mgh2qb8`(-IXZ5}QlGh_nqZWL}s1wEubXF@f4bU36l!50hev^edfpVc{rDh-I<5zNwomRrI;>%AO(-ZoL}gtE@M1G zq?W~H=9P4nG3-=TVKlAL32uT37_Q8M@#$|ayz`%ebs$$`Pk08b&}n3jS`>ERAubid z;R%~-BKRYM0EJ2{Kwr53`m;;+7#?bMNEtb5wbM2P@B{Z%S*m-TS>X(&f@xXna34+@xHyeHL>lfC_*fm$WL=J9|H;8GPpV zR9T$R=!L;uvkb#AAR9`x^wKIBGO3T3Rxb-yDh+++h637qaaGqXG5l|vqS{4Tfm)Ir zsnc;sUxRIJa~`>RHI^+^_Tme>783(~?<(|rhIx2HXcXlFQ=pSDfQIP}P%Y;iRzLKC zWpI1{3eQb{v#QFZU$O@n;ElDty?Ikv_Jt9F=}n)>QV79Wb#U_R*W<9a=;HWa&oMi^ z`#t;tQuMVcQ0VCF!WASG{`M~;#f;oz^O*(0cA=;3M;fG{6G$B@-pmWtFRz7EfB)ng zRnEfJ>6?tnwlkKxrh<}+9k5r)JV}}5wb{nWztEHNT1xMeJ9HYe8ufEOAdIlQxXl?o zhokup=XtnlWDMl+4i$zYAlN$YQDNQgaUH@iYhjC<4<^$ZKdo|~V^bC4GR685LFOT` zF@V6vw(ex#&MWGm3H(iviWPnj3W!^Z+|Vu7*#;^ zDrk+2%{3ZSL2F<8o$0x=PGOU%KZtx584Twl<&`D zRnec<$TvufGWNYst%J0-#&Y**QIKZWLZ2Gl{_^SN5&D#>SJ7gP@1LQ5RkZd2|2|S= z$g&ytjlm8~3+5TS`Do{XD%uJ|z2t{LQ+T%FLo>PPG|R6UDi;QSRz|#=I_C7wS=F>2 z?mowgRE@kFRhHt!&Od5sNd{xN%~Ur8{jTyWbq>)c8-H=5Qz2R#_8U?@6rjGeg3SE; z_#}PzB{WVSgld8Q8=&)7da1#zp8^bz$;G=UFI1~xe7=Wnhia+Dz7}d!OB!UG9p-=Pr-|EZ-x(Z?w`}S2d)#^mXMeU}YWtJFE)yD~{2XT3Y8e*YBzUz%}Tu zBTi|e@F5l{P(H=Bohc*Na|F5CIi^QKrjGVRJ$t}yz?&apIkm-g8dqEEBpPL^sy{nQ zCu(a|j1%vXtgW>S3WX}b)rrk~8PMT>5kF9JL>#5oVOog)ABAso4A*mfJAtN$X-)lC zzmrr&PntS`j)!TH{@vb5x;$RhYo4&vJ6x-R8Mr`o>j0T=ou{-qT9$FkCAwZmtI%-r zIOK!m{y)~!gf$@;{T=$TTQhhw5Tc>NP?QFKQb0J0C-Xy+m-?ZJo;u=$r7P8 zHZC8t@rww};1xEO$HoV1yYgs9T`k9V11?oz(4Rwy$DnKGQTuvYTkoqESUy90!4L@9 zcBT5xyn0%)(NLPSNUe@>*IsH8sZ|qOMyjUMms9^pEyDn-zep|Am@twGBQd~FPgA8R zZJ=@d9-0xQ)zn5osm8lyw_)@}l-ATZ@+SQrrL8df4Wp?Iwd2NiXDG3eHo!ObJ=Lf8 zhp9g8-A&sXY4eSrcBkfzwP4|+M~mGwq%jJ=KZIU3Mq}R%rh3s@6;U09F_Ox=c6N={ zj0U5vEA?!m^)+72pzoW2CcGL1FTUDvQGJM>ZwGa4s+mi4{vKlY#A;KGKMo+DW?F0G!U5E+8P?3u0W`T8*5J90w6>Yn+jyrxJ!^((+R~ru$7x;t zx}ugM%d9vz%l#C`GzGI+v?5MxVT_+e7vr>s#_pd`y%t&(s?}V(W++d_c%=Pwj#|Y7 ze2oU6v{jX6Xb-0E;?akw4O}8b9Gt;DtTMen^=W}ZgV*o;u!UCEX#DPd`Z__Y?G(n=a?WG(^uX0xmUOEBYW zoPSiTZ?>-6g=dhGAMxEzuH5}?t29o&|Jb0({-W+{xF&`1N zkTucu1#IWfmZK%@v?@`HFw~|aiCWuu@xL`qSoD9^6!Cx6)MKGu(!BGN6Ofb0ZCHqt!QF-b^doXsr!#bhV9EC7{Z2h6g-l z!Tic8P3Ou^tEfy{Eh6ZX)vwq_9#-~lvu4c4qmGIYI}LwNb`rF2X-(!yuV#kv$wQq=EMCreyo;A{9zNQ<3H9sj zBc8s>Ck)$AV#mL=j671bW|b};;{6d^4(`pP?j*;g*ElPMVOk05ouUmg<}aiDDInRy zh6DT2fwO7M0a)8|*o#D}R^2$R6V*-CYS*yAc3YnmADsE@!Y)(%Kl_%LL)KKSPwB6h z;?#uw9j^G8uBK|;y;2duSIpw#4=tJ9>mL~c$XA_b#CZ` zumgFF}3cfwQhC^qvYE< zH+Y#`LCZKuzyX34LQoulC;>$Ij1Sep!{gx+6~Tl3W+~-$)cg(pbg84(DXs?)9qc*-i0Ey8`;`V#tk zXj*5@$2%Jf+$kgNT0*rtBSU3Zh7p{h=GzRtme9P;TDWolI{L1&R@*puI$iCob!#+c z2H=`KEnaTUBMX6>Ra^?$e!gRrF@ zhsOPWf3-Zke_BYDx@w8WF~=yqt2V*E2C~2x{BhitsD>;lK8f+A7Vf;;Rr}6h^y^0- zcGtQaU#+0W-L-^@dq3nC(yZkVR>4%tvN$5l&U<9@`1He_?Rsd<4V8zkem8v|o&;9u z9T82-o{PXLc8@5P#rDEgNscR1;A*VBj1R$g*~edqLaE!N=Pbm42tjS+!6&kh77GIq#aIasSDvS7sr9Byf& z27@(+A%XS}#)Z3;AJOH(+G695c{FT@7G``2=M+Q01%6F*Yls$LOdLqhhiKJ|hwD@2 zq1qIQH~RHGEzmgWD|-5# zR@GRlD+LVG78$3kr_Y9IHT}Cm%jlv?va#yivbpr@Fs)zE{?GJ5VebHq(LW&9TfOD# zIn;ePxcAgKv|+dwnNS+G#C!-{6PeI2xW$YPgy|_q-ArK8UD8KKoq8~vs_%Ti|r^_>;8(JPpW+;Pqd-1Q3HL)7gPQWZJm@& zoI}q>XkqTBXT7Gg(}e%dn338GL#g~ooK(zRO3g-V;Uxrocamv|R-UGg#;L>2Ikb4R zR>P1>-;LJ%ym}#PWF9|TH2OU{H(IOYJ$?foFu(%DlDsp4o{a|5w3)-l)B)5P|FR;$+Jv-+;VUw|4*H_GFBW#jQ6 z6@303p$B%!8hT)XtT>*Ij@8V@8`H>VoHoYCzD`XrE)QUW2Th|zzIk~Yl$bC%8u94eKSWf zndN;RM_(Qf@EFB-ZM08=x;&@(skX;heWnb0HD0URxGN&@*&H5Zm1YCNW-tqzdvkD* z!0yg)D#A)mXFP7zS6@tN6SQ7gPZzVa^xT|%$PbQOXDZpL_&_)lv0qfuQ~7B>CFJ2O za-$qJQib)C5WD`teeR?OfpeA!9^Y?rF%+CUvNmZjNy2q1T-SMe0LLGA7RpuZPgya- zSs$x+WOIC=qh{`?eFdv|%#Iu$2VcK=2w&ZqTGg6k;R#7uws1Dtc{Idr3h3Oxy0mr*NG4tfJTOJ8?p$}(!$VZXroL)*R0N*il%Jj~E?%Ioyc0voe<}o! zdF#nMRhv<|>2jl6_*wP{-C_h8Cpfr zr3?4QBPf%)&d^N8OtXcSPDS6C<_T$`Asy$O{<-I1DdxIrzjo<4;eL#H7g^X`j^-8oJv%8eLSS!u$@=4u>~4d(6AisrMu^d5|mr$S;0L!D~4vx z(<1z1%=#XN!%H30JiZ+cZU_(31UhDHa#6pcKoGQvIKY6qs1|j$XnyTw;b6uqwE>!K z17eD<0{S@c&?6i@6M(b}qE4hZl0KCk_%#8Z{7v$c-mi+YU%axWx^mhF#L&0%wQ!m^ zUyCuQa8Ki-dE~i33kk`~z!kUh2N~Sz<$%Q#E>vN`)=rO@r19E!Yjyx-4S(t-k5GLfLh=i3;EihCdjZ*Uz;`<9mun3 zAr%0YE`hAGsr%QPj`ob9=SVXi=t02?wNUrkLlvL`Xuv{ExaUIl%)cZmbi~a%1vYu! ztl(AJbO4L4xNxJQbyje7T>}{u3N}aNX`6aK4>8AOa1Uit_V+p z%g_8U0q3gmUYTfbWNl%}K4VOT^B@LkyCf`#>>0Z}CXT~WgZugd5BXa*qlof5>mhfi zQ?wmiYX1q8Zr7r-?v__@JUb9AtG=mfqPl}4&wl;NQU*-9PtH7aq3|C5$X@9TZ}`T^ zf>Mmnk?{CvF?wNwxd|9`duyor6B3|_4fL728wC(=wD3)(VEOZGP_%;9!H9*Od!{G5 zu}W=j9~uK)KGWuJpxrdZmI$$?yXJV4>Kh*oHB{Y$dYr{=3r>Qj`N{>esrm-Zuk3_( z>Se}h5KUgB)vC}YL1`i(VPqEs%flAd2o;^w-=;XSNDJ_(hZmpTfThF$y0=KHXY8Iq z6&>39WI*Mf)9EU2Qhdj<=1GN%Xe^jAMQms>vdD~akzrmQNJRspNU(D+5%1W*) z1;a#~G?-zT3X+C<~sbeg|JTUlQ9b$_b;KrV8#R{YLP zid?ES(MtGpXP?VIThX|s=xkyq`f#b1X&eB3&N6L|KFHlE_Pwrx6X=s=TD-OpaOZLs z6Wv<|BtO%EeBak5HM34vY+y&;FJ~jn605x?Lno8JVhd7j|EBu1x{uowDL!qUsd1<$` z-ju!^d(4BrG<&%gUY`5S@YG2Yc)JZPX#aApQUaE&?u6t%E})p=N?PI417QENJrmm^ zws_8yxA5V@w0duv=!*;XyI#_x`=0((cZHT%)&-l7rT~2%-VgfF>=jxBX(mWz zn!MPZDz3ynH}*a1xKgVV<_^0hY&G|B0h}9e<#ux-iMh!{Fh zYXE2}7Kl626Wg-Ge;9r+sH21QJYYxln|L&A6%IfC2%*nbY1J#-!KoNm(LmL&%T~sk z^vf!(a!?1mCi$>Z5KFDr-<&-dasS)2-{3~GS8EVXKGeL-t=cF(i**FR zpu>NAI?r4T>Hs>yRbCD>D;QDh1lOb%^Q1adZt0HVU_?u_rawN^YRCNDkC9BD`{71>EFI6TR{6u=B@j zsN<>9TCJ-19ykT{)Mp7~TB}tu+7c*jtri>BA;ATYn@`-0)+iTks;^hLlfBv6tAr)c zfwdrkOS;gFwOXfY9tLM$WFyYt*nb>`4w72&oK;!Xk~*xzp~86mb3#k9tOHf)(2!|R zv23EEu{GBotDiP?=J(&&0OT0nt_w@0W9!gTaSOV;PHXMEr-iG3Eax@uOk+OQ8qo0d zT1e2wVk58U%c-gdySN9f zfk*iN(e)j0RUJXU=bU@5$k~fv0mOo+Sg>M2MJ!-MQ9()U8kIz2H$jEi1!E}+(oZ{Kt0Sp2)Z z?Z5pi+>HAd4>Y4&eHQ);mc{hJiQ{R7$c z{HK2&5p-i2`uAHf)nBeh`SrysPo`8kGoByN0JpO@9UwK$orDL|lI56cfx)zWxjsSL z*@}WzK-!qvih8erWb2={lFgu*2n?9`hMUf39qVBb11m;`Q5Jny z-siNUt1IbiIo z>_1DJS*&K3708lPA9gb0%SEt4kgYf?L0XxS4A0CDEs@-UB3R$n3;{eqc_H!gM(1w^BXqrPW`H!*iEx5`UF5%8#RXnV5$Fxqsus zUfSETa7Bt;gROLO3!1Y=@2ypD;ds7AuccN3!v_q0X)HU#T&qjGUjb_UhMt({QndxO zTBi@v_O|4s=<+SnPzT`c+!2hgVp1tnX$1`bkiPO2X-;9>tS@QO7;kJN(eIOl&4A<*df-I$UYfJ9ua)D!Dl{x-#9T)0Q>T@SulbVj;Jsyze%s= zlx^%~;vwrS5O3^yEXzGtiH2^{%V?eI(D$44%GJ8{VgA#(@2%a0)la|i2urMjV|3YW zLztD@YtfZW`lqg&L28aO%?JvQ4jD>PC0a^x)hPr3$84nMV?k zzStyh)0-8WhIlBCf|h??SkC%**uK%jwFiCa!8Uz>=G&Ng>{2v%JJzPpE;f-jK?H=fO~KBf%!ECQlZ`Yu8J?6$`#fptjZtn&mgbg zk@3m7Nfv>0XSd$o5>j6t26lr(uM0@}fIh!7E<*M*sGbK`m84 zfEFMUU3IHTUu5Xz?I*sJ$99ap_2|b8{YP~Xl|2A4+>g~L<^cA4OKY(#*pD6_08$(} zM8yv3U%7VAl7&33Mu`U@a4xdAMq^fnTP6G~umd(1sT|QzArCEou5c=PzU5E9WmZ{aGPQ2H=BiQ-+ zf1qkd^fOnaYLtvar)6T? z?gVCG4;6x1jK*f!Y-SQ=<32=Ub0vQBl&f*B(75qOiX0bQ$8K8Xilglby}7F0@#lr8 zq<07s<60#%)j;5rmt=#tmZx1O^{~!M%+K$ONaVb361WKl5cHUFp{ra2aOi_IYDbAY zr@h|Fpy^ENm#lb!aJ99OClR4mVVU$reD z9+kA!e?9KSl+5uM5F4E9fFnp4Ob zy>#u$X4=XBO8y_gq1P|7JR@o0-lqVE*mYF=Kou zx_w3uwDj+$&5u(?S2Br?4|LjqofLe%4*{a5BOP(*q_h# zKL?@9K;5*(RGURvmqVH#7V$CcY2eFxp%%4l|HL?ab0It_o6h7J*yA# zC?!9!L*|)bj44h3oYf=TPd1T5X7Z^^eJJuA#`Qs2b6VoHkJ6o2WXe{ba^b#?aa!@H z&8X2{5bLf6PNqptxL8M0?!iNnz=N!BxrD?m!wyR&ZOc3$sLOdvO~ZqNhX97fGB zVmi>*=k-KwVj0Rkk0t(1W7hDCY~;pB&GP2u(yXur^?i4!$S@8|0&* zW(13EQ^wO5$f1>eNs}(>4WyJrp7-$G%a>A#9=ES?t!6cyy{LPZOh<{h7SikYR8C-C z^iQ$HmZT3Cb)Oz-Tpv?}8iho3q51i^->&a2gp}ELoXHW}h-2p! z#JX_o;eTVRo}%HGz;61TqM7_#@)T{jr28~#wiCIA50PcMNbzh=M`v}<|3Flif+!}g zIgOlAcmJRd$nATDllaLbP>Hw-0WWlE%=WK!$)ljldh5VI7@h!E&;!_)i|mGJNaL+k z&mAqPoKX`WX33s)&(ZqJdMVN67J@(lji_66_Od=+n|YQxT+u7}&X6i;H;w2_=5^v= zzx6e22bzmWRHMjX&_9 zk`-o=DqxVq8q&h6x=+RaAEZD9Z~eI-^lcp=25X;hf&oT&<{O1bb1 zZfrvt3t!TKYfw_P)6Hvo&B~#{+%Q(F0{6sX`!mm({IR+@FH^jVZLs7~-Roe;-xV`U z|Ngpe*N*%{^RMFwswJJht_POAinR$8EY7Qd6MB2pB+ze-Xm|3xp?h0aX?fJ>26l?Y zi?IyMi-zCOOBKIe6kD`lwry(2E(a|;N{epjUZD|zQbeckfY~72yffkPQC@ZsOD8ki z-@RavxX)m#kpK&Lb*VONysk|zP}hKl&SpF_qF`j(WXmI9af-LZM%!FT>O+wR)Ty~M zDdzEIqxm~7H@+2#e54rxq}`wvs2(* zJy@@YOnfYn^O#27)f?0aKLmck8(p#oNQU>Lk+D{YV4{87NGr^^Y?`UCzK0nM=#4h) zNUvbg!bKpJakeFuy{A{N*ONcaKsdzX21ubrdlOBu;lAIY)pX7{h;s*yOh9C^?Wa5o zjk|~Q^7gdwo?b&OPbco_Wj!n9abF5@{s|emQK1>Vzo%ES&qgsOQFw}%5%23&T<6V| z1s`Ziz3%JfgFZz9#-9F=N#%50qbRUoBkeYK__xHGNMm@)GmJ5JjiI&=v;|pRz08ur zn$q?A7^`?!dV^r??K>*-K=;vOAkSyOm3v6dAL!l<_q}6*zO-s#jD5qfUWX6`=GtIR z64a*LzZHhAENJztCY16(59nG2g~@*8;AT`oPm6IN(@vvn8wL}TgMv7FK?b}HXs*WQ zcwB!CL^o3zOM=PG(976M9{@;>W3$p2T?;a>(MTFXUm1E6?WBujg1gK~W9%vr$AAd+ zMi$(ymqaL{P14OalaI1^30!Ty(J!0{$stZ|7@$)yg+1C|u5U@*u>T+Du@BQ2Ph3Ng zm<>)+{Qv4$lz}=x6Rs0+0B}EEJdefjy z@;qmcK9Z3wKccqNhODg?x&pF*>(sdYZVLV~jk+&s{ohbJ8EK<;f9pQ&oCRDepvxt> zfO=*D-=cs^T)_GJ&eN?+#{7R!_SMln+a&u&CXFnNA3z9H$}=H@`sCDa*0q>R1|DG{blS@v|9vmqWjr0j!k=N2NbTJj!v2OlGTW znCCVtjH2VPmJ%Q7Zk_tzoP;Or4Is?FQ2zL8TrLi4jdl091MV~U)E6{p2Rf3mix+o1 z)RBM#6!4rj-tSC6b25y)XLRe4?xl4Or#FuvVy_=gr5@|$dKLdq%A&@FJm#Uoci_%( zVaS2~3dFS&o-w{-)87(OyaI?|T{q*?j9whiz3wDEW=A8Q(Y(iCGpDOO65jOTv0l3P zYBZ3!Mu-`u7}Ypej%qy7t3r9F^Ap`Kvw+fa(jTjrT=*Q0As^lvlBeP_4G~VM2{MyXwg&1;6A0yr?_yI{FpjE)BU~g zK?N0?v*fuMH!uqyyk)rAy5-Q1&-92P(;mLh&)&?G7>8SkM;|&)Lp2{hm0TR2IV%Xh zwgbTp&ERVP26u%09wXBX#>RG=EF^ia!F6R{V_3@qjD?RV?78mgbAKw*&ywlgr@R9w zAXrAF-|V1o5Ut(cOw*q071c0W`&{p+ee<63pX+VCEfsj(98+^QQ<8!rCqAT@7kY(K zSFWO($$K*Zg~B87I@I2DhDS=!z0I`XgOC3*eZIPR(vo9s{bjfH0YAJ=eIlT9W2;h?Bvz{5Xue_InOm8Fr>aL|z-i5tC z&j2ipFmE&!bFm(@2ljCY)7KGRkawjdl-!qy0ML)G585&&s znS<2NGL5fr^u`tx`U(@O)nC-@6)xUgdp)KlPe3Gr_-XXT zYrSDZ0dzBFVs@hW_ORq><{sulPO;>uJ(F*#|Awe|uS>>aE-ZdUXI|^UE=SXmxkGhw zf1`UhN|W#a$%w+uRV;_R*~rKVVNw!dTzW~Mt>BkNt)}Q6#q_NWa<9G!WJpKsK$HU zuShN|)0#_Lx-sQ0^>~llQ|sJm&U^5IL)_{7d%dHN*LZaM03LM0ZtW}Jzq8=Q8>x97 zW{uZs>XN5d_UN<0In`yeZ$F_Q@*taknL~{}=w(_|ZD5k>Yz7->8UO;ChLH8@ay_j>WQy=M>>NVyP0zD+tKFVnQ zh%SE6Ta>coSg;~Afwkvt+?`B;<&BC9Lh4EGtVe+7|lBwjTGlp@{3pQA}vmvi=4 zM$lRwIxJds(=eVXK?B#|2j&f(#h7PNF`$V+pHOGuzU`_~R540_kfGH9ruoK)n{%^7 zK#}t|B}L;F7!O{Ow<9*(8KswiLMQ!}m+$A!a>ePFo)G2#VTyIWDz4z|mYLk{F{ zJ%mQ$GbM*^sp4Mw%1{?$TdTDTO!`*>Troi1@PlO2|E33;@E4)@#B!Wc>66vuWfA4H zt~;rrMO4w66{Q%9DDO9}1_LI}zo3}&05FPKgdY@hDw~#AL`eBi9J`}yy!-e^j=%@w z)G|4Z!uhQ4tb_6_B0yW|psH5UKH*?is+K9Yz*j?gZo-a)5CMr(5`_ z#)H`q8oqKnTxvh-3j^{Oz~iE>(fC0tR-tzJC@nC^XuDnR&+$;5JjTc3-3TGL{*;fM z)_%V78BqkY2#Oo#eq)G17Mt+b!gDCVCMLN0nmyb9fHv7gX|32*`okugXmQ6$FC^+~ z%`Q=BA@PZpolR5ud)Z&KyO1!-z}zr&?xwEBfT0RBZfXX+ypZlcO8Z?zD{X*5E`>#C z-FwRzn_ik5iynm-(cPys1U5&eNMmTgw4@_O?Yk`8gKdXbMg)y1EEZ|;Tgb;%)X~mu zrbzz%<~K@k6;-wP&9uf-=kOj^mTahD2U7UGH^i1(iy2wY~pkJZ5)Ivx*U{U2B`tUH25V5 zljrhwuI2YlN`)onPmCW7)t{XDZ2yOT6k?awGM(DmMO`gzBTckpI(Zg0#i5KngEl(0 z*+mmo`*kBdcM~-#J%Z4RHBp$Edx~c&V8ETHdoqujvqe|dQ_CWvp_>X39Dv3hI&y|) z6cH7*Ityt-5itkXV7av#bWR0;Ok6Ujbr{`2Yw;|Mg`jQo=?j zr56(ow0g71C?=wcKY)mwuUxXZ5K}d8Bt;Y#{%)ZgB`)mAS#GRYMPrH!Hs8CrxCpY$ zzh)sdP7jz@LKJUwoY2SmC$QhcOLjPB14Vj?H%RbANoIDgoKJi%_m@%he;ro`%9|2qcP^NkXswb*cy2Lb4?{3JE@6tlEj z>*$oH=vl1BJVte#|GkMy$^~_)e~}t{iAtJV4t?Pze%3}@ruSZAnpSNsP4O1d-Dey& z7nRWfA~4##RS~ob>h%~tqtz>kBh34^L3P*;1fGOmZd0HZquJ7MTI(r zRx$Y9rA-~MheI$e?{h6^#l-oI;7W=KX;iL63b!14|<%kE)at9kto-%>)Dg_?X~yIZ+fP(XDdg z6MG{*YQe_oj`8>;1^S6lZD>A?@Dt6ob&$6BiB6j5NpdYOO8ayhF0s-1y1`Y(c)s8B z#RqCy9;9U9TN++obkV}!((&>b;*>o4w1ViW)z71~6-12o+?AtjWp{vwl%uf+9CkPP3^Ep}g|DcNzgT7Y>7<3e_#1|PfC8fufdaf1@i6%P zZO(RX3a@S0GMTKz$ZhZWMv#;;zBDTBd=KkqPcqyC!qf4ehH(4`Wlk@^rvBNo9M1yL z=^n%Yf8CBjIE5#?*g8IRW+UmFO2SsYm6_PXS?L<2H4Z_pDS5e&pg}umZY5EpJnY1q zAS#9_1_biva*XVP(D%FOb|rC7kH@suh|^Y0B1wv8bu;6hee_!`(NxRZLr-go zv6ai4vD3}i=X-&zm>>DsM|hQiHhmDE*G!>VwS`~8)d{ksPDq=xBT9V|75w1ak2l2j znUSA!n*POAk zh==3oTwPJM<}UN|BofI}Gxgw*gZ%;yOn%d3{KrPmGt=xDM^);HN$OBqQx62FEuE|< zI)z-kV#1)YgbGi!!_JVZQOWU&6R@w0P0r9_AJZ+Q9`!|C4`qyOkPljbzMtDc3+f9W zOG8HZf_I zVK|UW!kGU&1xr3S&RKs@(}u#o#=8}!Y+%@5fJ2HckIl^oPvv^!j&k~6S)`IAEp^SH zl!l^i<-IE<@8gs$%tU@^UzFFFrHI01>J+!{chJ3tBF$sKC<&Zg{Gg+kx6<53pw&|x z^mijsB_TvNDXkQ9WbbgIa1d;BgMwfc>^y&`#A3iCb_OSPpTTCT(CcoPGJs3CESE7X za0xH4qvO$2j$@%BHz-@mMxs(-<&K~J>gSJZ&t?2s`w(QEQiFwWk!ILAAtx?Qp#3jt zPq6UuxrhrP=rcd%*kqDCYT!bR+?Dh=7<_(4<;0 z<$L)oJR&6}7n~G^lUsLY7}X9Dep=Z$`ZPrNCR}$hhevjt-#dI@T!AbFc`tFMl(}q< z=40R8r~kkGWoXM6>-Xndbxo(UDnJEsI)bnaW;8rv4hzKsS{Bom#yjmwX; zp}rw6=y`|;{wxFIYj*#E;cvEqwbs!F<^=HsACrnBM)#@CHgEyP?uim8T&D5Oauk3z z+<<%)2$Z{pCN>d4CD$zFa$yE-#vZeAj8evy*>tjrD4SrBP0Z=IudyuuqLGKE>Ua_O z2DpYjjuY?ik92OTF}m2*r=mW9Ga}#_&(pv(1f~QXK^k5PUxC5(<2oW1a=5L+mXDYm z?^H0rjencq;1a=8xvaQsy#HwGksE!N(YU4}Cg8wd+-~<7uE0E0sUR6wHrCC=zh?jl zfA@Gs)@C9}bN`EGHUr}~dKhhQCVb287-EV%B%UVk9mp*PzlR^=`eJ(8Ow`h%VyS#{ zAc33pe<8suLw_ofpfKl3nL8RkACaK>cg_LeB8}laB@)PTzcT%?9G--`yf^~riUH?B zxTV{3xy%%U2bE_5RB_$s9&q1I`I(vk~t3=_mH=)}W^S6Ao2Gx^;cTo&EA!tqrrQB>6` z-=Wd1MNrKSFgT705yU$txzQ|gj!TcjL09pDeuWH@{rYJIU1=?*sB5WDgcx91xynMR zr*u!c5g|%y{lBNz5yHQI=)eN-wOWq-!yXI(@B(dEMep;I!?%XsCqT7gH)%o(J;Fhe3;xifL&aMg`o$`KWsQ)i2B+{(I%t zCL^6!ZXp|R)VA;D_Jyelj-?%iyNZtf)=7Ngdh~M%)!6y;p_3?A z_RKt>k`zGS0yzYcg3#K1>Of2jW9vLe<=A|uB!5p=;iF9-M;E&a-;#^_V4Yw3=tDlFuGYkM zeuZV8vopx8o9OR#{1zjC`;4C$P%H@Tn`<1tN=e;BcaQH^gR@PH^sf+_QwfJa^XXYP zv98z|?Cio4J5=!F^S3Hd$t&m6rtYG4k!G`@K~aZMFawu4>0x(K!_sM7{_VS@$XKV1t?IOql@dw{q{Y zpbKC!AMr`y3nG(VN4&~xlhO&caUprwAi_?)8uov&5a_jU!X& zQcqE}()-^y(0$4UZV48@*Z7tLc{Hw2WYRc~91r98Ose#`I8fy!`hnTNaQ*^AJbA(b z#eGU=NdtqPbpiy~>L3n!+| zM|gY9jl^K);#dKYD3sW)LLyF)4*Wp9`iL-X;t!6rKBA(kbx)?lG2&{)AE3p?EJS=z zM6xL$(?KpV`wBj*2h;CgV3sxNPWQhMeodx0$p7i|y(MbDc2pt+As`=|(tb7U+plXIPF$e>ulFL95kx&z|n6Q_vDkK7d zJ(!xN1nEeT^Gj69#7F=ll_|^X`Mg@oR|L`}AIu|Mu{$$uyw8Bz+RAlkFibpSB|SQo zcL*qo2}HW_gBinezN1qi)%`c-v(G-{w?!IAam;5;M*PJ{8}E<3zaNJivmkAiXz$KU z?c}44xG_V~80YcDC1d|ExpdPQ0A{DFIn8D&x z%hIV9T0C4YXPLZvK0O&MLaH51WJ0xt$?^^~EkFAKSa)XtI-dx}Cg}z=C3=*hPD8{x zk708^t}S0gCa;@M?n4Ev9J=#Tcf{BTE@3G64C`DPJXCzI86)ZCP*FpxR+jvSi67l} z&Eecp75sAgM^OiyF8Y{hl{3`zbDGI%!_^< zF3PnX(LxUXx^X~!zAN+pctpTFn0ym#w(4wJ5u$gq&&|}w2T1gV<{b_`O)Eq@j47LCAop=cM9N;q3?p$E z6OF?tE6un&jNXnARq9;$RW4BRg~Af^YU6ELGbtlN9i+|)ENEGAv#v5@>9didy&6YL zMv79}ahz<96n@p+o6C037~`Dr*_Xid$-{^Vi9q>Hp&ZR;p40kM;W+V0(NBBI8Mw45 z!{LeXY!H1OCu)>^(SvvV4~!bP%q>Z~l;QL37h`8{S`i2CYo5*&J7=>B5moYG>8=8|7LgHKDU-ozitF*8b(F$p} z+l`_BQoM~J6#1>_D4I&DgroNQgK6!zINz@mPQ6Bnisd4g%E?sKoH@ENns|YB-XrP| zM~g?{5Nt}gqtIyKt7_fKP~|aVyye8Vk{R=&-D89(aqtsNrdi!3t%nQ-Oq+E8T^S<+ zieK(84_W`h`@f*gh&a}tyvAZ*KQD|LjRpC6AJ2*oWg#|+2{VsYKQ8Gr8_PZ)8$m0_ ziVAuLaFXHoLpM4%R@AF`@vHaw`|Tzv>R=}AG{T9mIp0s7P!e1Sn@9m6Y!c55RH=l- zP*jm3b@-Dc;^JK8pivE?3zplAKrF;O$@l?mV?QPU6?Ew%NBJu=4C9Zm=e=cEcV{|& zF!92l=|M7_t0g_Z7M186jXxC6A?zW^LN%w6HY7Ep2 z;doNZ8AHpaLlWZGk4{e)Yg~ubl7&9*M~O4UzqYdcR_W~<*OUyt{ajZ6VNfuM9oDGr~69QAU{(ig|#y}+uzY+zE z$@l;<7}k%pA4Fp?)R4lWja7M4*8Fi)n}NXzt3;8rMWrUCBcKJ9+_@x7f%rw_ zV-dlVvu(L!kFn@m@wux6C1 zQYYg?q{lqbRy#JBR?ZXAt}n5pleR_KF1Ua5^KFt|wsE33)2iQ{+2u2Rf8ry@5)p98 zc^p`!pe39ABvRVD(3d1y+sDaI78LI6Oxp-YyLUR0m=6|hLIBm74*}q*3iRoG@mgE< z8MXUataJNzF641ZY>4lbQH(6VhNofgD@3JODvyIB1QN*tz#$)+>uH|S{ z@&H+AFWT`d&Pj4cklO+=L0wA83&azj?;y3oP<~W!x?M_k`)UW8vrv@x*#Sc;(vq$7 z}Y!ppgB&w@J9BUVe zR;sq(3wpL#)Gv0V^atk?TTJSEWzQ?^Xp|Hm~QCHO*UDk@5 zTEeJIN!pDq5UDtkai=h6WSSYo$hg)Slp=%T-HuwAeic}rO$kbI#t$>I)IgR*XHX9_ z$Ol2O&Y;$25IE>&v~`21pjM`f8^EdB1QpvTLbTI5b=W8>dFnC1*8Ib5Cg*v?-;@lA z@v09cZA8+JIvv_5S`;1Khm#!QClVh+EwPYJ-kU`A+D#A)({&N~QJL|#%$xxTGMO5= zx&HSFM1<3=O`;c$0FI_)@#sn|#tm2*$T_x@b{+0`yp!I|ts6U~llzku<9e zEU4~H7oK&iA?V-tqvUQMd;O&6ZEz(AIVvI==*wfCbdbde+1_N`EP80$dpTk@qra|6 zHi<4Lf@%2{2u)_%=;#*lg=;@ExM?ue*(wgWyludn!+Laf92R!BZK8ww`tIB;r)`JH z4QR+Vv9j>hAl7D>=s`;wxq3SSw~HuE%O6MyJ8+`*0J72k?o|&wbNBGPGmzHpfbe*K zXBZn09khLQDR8Hlpq&|DrgaSXH|>@IGOaq3itG~Ow7zvHc^5iB{b4Ua`05G$Ss?6T zOXyGTyG6JfL7jJlA?X|FNZu`0Xw@V8vhvHJHhY0I_zV0bO1;;=jr-T9AVog*%)Pr? zJtW`)>h-0b`$V|*z5^}Y2MrBJ4QjX_o6Lqa$gy8c(Kc77rWvAweNB6lc9k$DSEnHv z!pD7z83NHYZVC6SPIEG#KNe1z8PK{~QH|OhK<+-T>C6G_f1d_W-T_h9$M%}R*OG10 zy)yoYG>a@}tPG%tgOC`Xdd1`R%oui+CLM&#+;*FOK8Q2Ld)MjmK@nQ!SLoj`b*+X= zq5>d6f&@i1Oyx~z{!k`ANKq^v*_OM;3pPdqwU&5`)8|}gw0LFYT%zZP zM1r>KD#abfVL+*?wEVEBZ~3X3Q*ie5u&9x6Il}B_S@{AUCgtttvKI}j6i}T<+y^{` z=R5#}l^bUkH>d3IrtuR(KzYOVv;3}2z)dhm>Bd3?MyjuwR;oDh!+AxrOm-+reGf*1 z&q|CjwBU&FNw}ypSJA{j%-wMTP3VCpK<3RW8O|c2()m@jaP&C6I464+5rAbE8kGMCOw$ z6)69dn3%Btp+qdh7tXlzvnN|KX>6uwTVr6RRO>?- z^9{QP-)ej0o)nHLpM8ZVz|mGLsRaC98IS*<+)RSaaQG0+vKJm*9a*L~0`Lu8fG)cTxgT51_SbNI3p zaJ%y*e#i=zP*Gp%X0B=+hFD%;;Xhv-X}3dYs}%!Q^d<&%t0;U!8eU{@OAH|@9vpdPnuaP7Gymd zOrKo9A~?f~`deyy?}$FVi;xg7<0esn|F||Y~hL+1aBlU;RyFP^yGs0!Sd9{ zgk*3Mzn2YBU{EwFLtnEQ*%^i*LG5AQFs>s5lL3jxZBKA7&5Mi0bo%2jQL;<~!T{9L zD4^Xs-~?;Dn5k=>n0HaMZh8DJ_n}>*sN~k@de=ZC+<_pF0#sI2&a9vVW`J?I5ZB!? z6lpGeOk=gYr{s&GOxL|1gRWxQp)Abapv;efM-UkHvUN)hwz-T@VtRXQca$> zOOYlAhiftvO)x$!^d5Kr!CSX2N&c5arB3VaNGLSniJfBef_Y+Gxjt5)g2A@&XCeh> zy=;@h(E9}l$6<4k4bZ^RdeG`MX0qRG?`h#B;ajeJVZ>b>&0Co`XWT>-Ax$&Q^S01F zymL!G4{~*%cUjESx)h`Nm&Gzm9WQzJ#k=M!V>5PTSRAo^fC2UyMWsIG!0c)mV(4QI z3{M&r8tJvv=4|;y&iWsDA$fP<~#j$ed zZ}J-X1R{G+7JT)q#ODXt&R_HL*dPUWJZc6Q2hrL3f`?_#`huI zJZld?1o7DLZjTjw@*z(KONbwJye_I~i?36{brI1ZKAUsL_{tGZz2Gl@o${_@SFzw8eRe~{Xmvw-5g_PrmT2m8&Noy>t{<3?eA!5R~fNKD3~$h7*t__~B2zKvT|Bs9vRv93lx zFGsZpkdb%~`O};pRwqGpz6L|FwOmV4k8Ba5*EUmR!W6>dY|&1uu!A0Ei*6<7KgX^t zq@c?0%v8mZ>hr%vFP{PU1_X8(dIs|rxIc*Y8BtKOKKHly-jWJE={`A7SZaEn{>~BI z%Z@1u&`tjRPwdhIpMcGI}#P}frrzef;2oiBlBqKW6@CSlSx+}1Mi=nA(tl*vF>_L^`AiQQMCxo zdjdgiN}iRf1JC03V+;7cQm;(`EX&2e9MhEkg@(p@Gos5v|S@0cCd!DJY+aB#?Pbzz<8oZbHM~NXw?jNaEHRf%3?{S9arv zCc%t;v$f1?;c;*+HN@>O}DSxL5)@0`M$({XU;!irGuh zofqJvKA52cUsIuf#C7jCtC)(6ZpxP%z(ttX=&jr6?LVTo=MN7cxzJmDC8f~W z!J;(Hcqv*&WY~C=M|wo2Y?z4o!D8nP2#QQ@hx7Yqc+a9MuXIQf)czO`WUp0_gtCdZ z>310R=6Q%Q_yLu8h5K)py{YLd5nSENSt``*xYUTGGlqpr#W1MPxa@*LR~)4!uS5y^ z#oO=l8AbDrz4z(BD^c3zTWEV3>u%GXSE9P-XZ#U-pJ8UcYY`H3^&VAxEta{r@Z!nk ze7e!+IbC=yY6QCcH}D3onMWmkRTz0$ejmZFxZn5uPp-YssLmVQ4f|y?^?M^ys^=_Y zfV$6sjwOEMcNb8Y&kxJd_#8%BvZo@>IQNvwycM0?=a&Fz%w^QY_~t1kycL~Y3(MvP zx!$4cZy}Bydx&1W6)&~?qx9sRIO=-ipe$@tF71CW8tI2Ou)yu6aXpRHJW;`QxEb9d zmulpRc`j?8nFHt19YTofd1Aaa`8M_ZfSWGY_tAzA;*|E?4cz4fjk1fA&W7rJ#>x%oIm*Xtiozq*ZxL z5t_Y&7Jh?%*6eG&fBr?{TJ-{4+rg~h@>SHsV)u7#x<{7&Ae*LJ>|H$gLKMW@i{(#E z>mGV!vHNKUcayu-?yIfeO||)V-frq(wGUN89DA(xuEn+Ehsm>~{S$4g) zo;vv0=W6q&(0w2K0IlW}>QKtwQ~PoStuJLCu035&}}^;d>A}9|lNRA&g_@8$Ub7H?`j@ zffjxFnZ1mv`Z^Li+ShB^&goRMi@l|`co2>3VxOSBnC5ua#lB6|nogxP-R$3LPy18( z?)KZ7TYrbGhuxxTg})-br~QFeuAk$1PkU2U+nPvqd!aL)iFCY|JzD!c!BM)mJxYs= zcQhMfe~NR8tyE{2y|?P-NF8Pm_R!`$a-5uC_fS>Uac!b~WFal(F?~AK-axx>&yh6M zUfH5;z`KVt?3J`>{T!EO*ehGL-Z!?V*gw~_#Ji5(Gwpt=*6edfVyb3wa9Lc5kJ{yXxf)u9Pj7Ze=DTHaC-Ved)@L{MNFh*(@=R*>p(~(Qp1ia z{1zA+g|eNh;IJ&R?{d>xd(!@O_S#xaPs&|qpQs%x;fP&tpQe^rxdkh4QdrE-Vq;IqbYc9npe4{;4n_tXvbfdkxs(oLSUTm^=(a!Cp$aH(Cmb#OEOt+8Le(`V= z-fXv6w2iQzwiTeB06O3Bx z7?5HA+S0fR1osh1)4O5k9+5JA2XEz)rpL%HTXi!o){I+^b7{nlHsfmhIouD~gI%?s zR#Mwj_R8ABg*4)ny|XrV6&*iizo0c+L20M$$L%AR!pKjV$&h;tT0!w=>?^gfU#V;+ z+FWy~V{@i`h0wZ?quVw6G1axnOm2l|&`*wl8}^Qx7C6T-G0T1&a9Bxg?%1npIj)Y8 zckEScns${I7=X~9SLg!&uD?P>vhn-f6>81DJ+C+tv+YTC?Rla@ytGfSXmJzh`!}F7 zg-_7cH}(+i{BbJz7Qai5J37C$VV{>bwO^Kx*Hn(7{O{`-sepUC!yjM=P9$+mqTe-b-y^x!qw)g?%7jpB}5*|3p zxwyq!wRDSPo^aFDgdd=GpI_-m>846|wse1z?mX$vm+mjpT_D{>(oK`@Qt2+2?n>#d zmhM{Vu9xmc>84BfH|cJb?)C)vvqQSOq`ODD`=py8-GkCSB;6y@Jto}~()~lar=^=I z-E-3YQ@Vdi_mXt4NcWm_Z%8-mYy$WGw*0s&-TTrtr2DsYA4>PJbaSQqOu8?m`%=2E zrTbR8@1^@ey2?2&Uz4s?x`m`$Si1T-H389f`B6l=9?~r)-4fFEl5R=qmXdB6>H137 zPr4PP>o48P(yc1pYSOJC-CEKOlx|(=)`y#rU#WroXe8an(rqH$X3`CnZVTzQlx{2O zMo712k0x?`m~9&Thx*$t;5lk1pYY2j(dDR;LPmV~LeJD$B7tN*~+?8Dud z{DEuP_Y?aLvG05KU0`1#`|h#tTlQHXgw7tyJ`eVN$v!{!bz@&0_O)kU82fONpFi+Z z_BCc-4EqA#C&2qPdn9AntjvQ zH=TXs**A}UBiOfseFNFIjeWh@cbI*h*mr?_ZP|C5eWC1o%Dx8hDTF*r8Y$RhDw4K8jK1WD{3@uVP-T{tTD|{Ni#)Lnk%YbsG@m zsA+8#B`Z>~Wkw;sy<*L3kA`+YgQFESCR(v%L@Ukrlxj;atty%2Ra>UNs>VVPlUr3) zJs{u-tfOK~Rn4O=Uh1fa?@(3C4MqAEsuGO{?W5bEoSv#BwWq3P^i-{(eN;8E4}N1* zH7!Q9WW}gT;sDi>I8e0)4pMDtgF(b&hoa(Psx@|)YK#3^)zZFEtx7B^9jU76ajGRS zUe!|LRmE?Vs`)`VB5|y$WkAOydZKE}O;oKJlT=mt4y*omNdF!3PC?_Rs%q|3)e<^Q z)nccs7LOT_?j@mtNvax?gfj4STzZPC#LP-iZGN*6IUlW>kKXX_&#Id7vug4C1qCfs zZ5fMD*kTlx2LBRNxKvfMma3Y^GS!;6OvNy%w%Fy!yBttjp=zls@VgRmD^(?J70Ozr zDj923Tl890@!JTfZd9$Qn^arkZz>j%EvS5lYR%oHYB9UfqTf|D_IK49a|o3jQnj=r zXvArNm}NB=RW0xm3cZQUS?JX*RSmtPYUy`WH50$FkTrYUS8a-+TGO*t4cFc+8Goak zzmYyiRbn5hcz;FCe$5Wj!hZ>&R>c})19I3DYhoeAk_oCET^O^Wu%ab`iOh5ZHhTd3 zK_9X_6>Fv!5X(o=GD<0GXlccgS{mWLikj<-aL|h^KgALVGNkw`TB^TdQ7S3cv`UJS zQC-nOYbc6GEkzBig?zOXTTE>w!IFxHZlZ&L-9ea-APQLx6~(U!zMBHOLlvtMrYPwx z6g9I2rrsxt8uJMniC@2#iY>P#(u6B&M!2HIv{Eb?tuSj^D@tgDVpH1Sx4ojJflkD9 zK$)Nsq0x#G`x(mqOwl|#D%OmS2}sl#E$pJ$61$?JUYI_;(Ubm)Eo*>ci5>_@3{)(B zgOGWUV)Yn|o(~4J1}myEM6qTLRg~P}iZyEl8Z;8+#Gyf>6fJWU;zy$qAT+UKP~jLf zZVckbqWp2FXS`xbouH_h2@@4dU;-l0kn~B4C2JBge5U}D0lvwKB^1Oca|#+VRk3+Y zM4%Z#Fu;67QPYknR*$0?pTilTb}na;t+N)C2*ksVYi+RgJE%S`zEyyS{4mXn^_O0Eru_mcWLpEwGWQ<~D*q zSXDh5BVS`w*ckIWM73mwsG3I;)shagJW5kl^@9{4w<+d5?By$=s+tz6T9hy}Dh%`l zBs;T>YE6j6^8>Na%t-61TGG0q@NTLlw})!Y?1_ebu3BPxsW#AYE7R|oFH|kHFDmbc z%D+PRAoLD&Tp6Njp+i(_7Jf5^0wTjyEqxd&04dLkRV^OFRY|@xN2%6~v8tLi7G;B6 z$4pieY#!58C3PWMx)8lwq*~G!shZznBv_2^G!&Ais<|M&X-m+7=S=LYy3PTGi5=WH^J#@MYDOc@>r3RMpg* zsx=nmI3r6{((j_t_mK9UYK=C~Sdievzf~plscMORswx?;REzQ&J%5c(;5Rn`p6EBK zE%%LTO??Z{zEjoEcPKay73U#?qN!PmrbJsbHQk~qsa9YbcFnO+JPIwO*>YjtD%4f8 zq(VnCP1n?DC~##8P0ht`pqpk%$3`^^_eE0OH7%={ret_)ng( zJ>uFjV zbg$DIXx6}n8hAQ162E?pG&Qx6W>bQZA4>6QA(}O<3Ce7$Sz?=Nwv1-zP$Hif%p0PxHtyg8e7ocHkIVl(K&{Gwb^#O(dW+s-RpW~Yi$Lo@DkZ&&X|k&-VU zOfOUG`22R>T_U9@{VngF_jdKPtmQc6FPkaV-jnm*aB&-X)`9jTU}8q-!Ut;OZd#Yu*f%|FNdWtZR&sS9iB5u9^rj1 z9+3~>agy2lW`3u<7_pyuwYlt22ad}rl|kAkQkv2m=AlNC;%<6eADJmTB)-5Wh_4qi zd8wF!lu~Bq4oR`pKu%RPyt*QjHDg@q3p2B=>{&A=@)^9YGap&$eQ@+q#-T~7yy6a9R{e{<` z7uX3cUw70g&vz6bk!;E`guGK;ld(?nBtz*m9QY2AT(4>N?0Fi=Q5Nkqo4@q-_70Q3 zlNsz{j`?bs9I2gfLT~{srgZ^!uStzSvdy~*CW`YmdrVn0=MoPe-h->$_oH0pk!H}QnZ5J!+v`A4vY8qB zqj(p#faO~xP)klVrSHuL4@xyQMx9% ziC$ZVbDU0Q#n0Xzc^Sr=Z$6kTMLR|Gnnf%1DkJ8h=2|7k9HUepl3uuBO3lqfH^}*w z!WW`)qIL3U^&ISIcK;$fbe%ue^U&41QcUh~e2gl8n@GvdIJCw%uXUvOB`DpE3t@mr z`@6vFT+{G#@98G8IzKPBE2M_H=T3Pw#1jyYw&K;4MwpqSr6SduBijRkj%mf6o{&yM z`EMzQnM-L1OQT4BN)wKrm7HKtv*lOs@H{7g7?P-bY~iKU04IJFairacM+><_Q^(PL^LqxH%Drv3rRm%>wFifg`ft1Fhssb;QP zDo=yu6x79gG|)gu{$uLY*y+%g94(eLl;rbN+8;;}w;!!_Cj}Acc}NTOnxS+9&h#51 z6%T?}BikX!@t$wl{U9NT$y=_5Tt})eu-FnVHwKl%E5oN@JDNGAh50 zw~-XQ3hJk#J{~MmC8ck1?r#(+JQSgSg0xYj!gC?*w~}X%NSSlZg1=?&q14BeW*W_ayy9}i)60A{Bflhf z0jRO*byI%N9N$8-woZPJJZB-~`u12nnmw62P5ru(pQ%MKbwRbDmFgx+4Yb@hGxZne zH_1ybhMUy!+T+%mHBBiGk?s*Gx!82^NY3Uih4;_sT5HvsS_a8pW?l1_id489Qq*hK z4$JRi8~E@e(C$Q=R7dh)(<(>_)9^2;?gohDf6UCSC;O>b?MB+Lzjkmdf!obP+r?9l zCHA=KpDUgvPr`KJ(e#&7lJ;8w8J1CO*YfRw0<4 z-c6J`n1(&!tXr^SJy5vrPiA* zYMQhGCV3-nwX{fbaRr`*vc|NTgDz?1b5N$B#(6`WGL)`H&+?E+g`40d-?`A+?{0G8 zRa5fXQ{vI*fXV0OZQioqnwQ}%)v9LrHZjFsHJjB`^d?Lj&`1rI3o!Yn>Eabr@mnzc ziUZlG*JhPjp{7Z1!_-*2-O_W+c#rqCm-;)j75WTR{9{Bar&Nj^EEOrW)y&P8SW4c9 zcd;g`lgZ@g`}2xFfcChlRUw^CEgQ*?P5%OMFaH##3(Ve!r4&`{1hzJV%H;wpC(50u z-g-_Vo`}=9PU0Cw=`!T3))X;H?`qg-m5ZRmKa0a19Yk8AzoM+5|hWR~m5*gFwc+KBkFn^1yPYZVTTS%XonYHpy zH{nmEagKcl?FAgC?|WcxJ@ax)MtmFxrBy}+rKLEiq2B!C?N#H{Amlsd{61)ODyY1F z^!g9%hSTk+XY~3nr!?8_!n0MR)NZq)k)Dzt;oSyR(C%mQPtXe{wg>X%NR(b3m6Udy zUVb^UL`$h5v+z1Wv!}?VkvxAi_UE-K$2Eu2wf4B0>eH-wwfo3=hy1Ie@gh^Jo7^5{ ziFe}2hDn-Be#P#j8?9qD^F>Nfx(d>ZB2`nm$X+VmeIhx(Bj78x1be&5(UeoV!3Ok9 z5Gnn;+1^yLq-a096MBuyrJ=dMP3-Xl&|gCl(u=?7Afy5uqjv0bDe)#l`;0XV$dbHL zq-shRqe9pwlEZ+5QQqswCGMwGg>tZ>nH26#)Na7JEE6~9klEc#5?=8aQX{{{(yCA_ zA%e8<`TSyrWn$swMF1>#K>0Bo^wjP;TR^nA4LiErFXta zzQYJOLhodG8Ms=3ZBKKuf1A-QwO!1Sxe~JE+G{K?L6onGX)O>VdeOlvAttpD4#}&m zi?GAclhe8;MQJ>`Vc9YH{@R(k2hC-z^84m(st5mVwzAghFq~($w334_JPwvJRLA?o zG>Ou5&HYX$+*&#z32FiKFSNaNrHddf6e&$F`DCDN!$rza8g1kE>=DV=z;y9T21E+5 z>#SKcN{Y1}M3E0tcXXIstd9;;F0I#NOlK6akB;dSxyA;#D=wkYhr8mVG1-*qU9lTr zD2`n3ie0Z8(Gk!JA=1!HX)F0ud^}RdvaA(D5v2gG!F3|}DajmC9nRGjbQGm?a1FMU z5+9+|9RWv*G>Os-oX~|LB`EcWmo}%BMMqJ$O5!Dm^$YW=Ep3 zY1t!ErXezN60%6!-a>l$Z7;>6ZLf2J+1ycDob2&tcSp(oO^p!hPTc<<5*J@%NQ2Rl z>mxT$MNJ?LHD5iCJFQ?-v!#=szCu_YM*i;5J1t75;Vt;82`p7|BYe_wsrSz z3(rQ8_ESO;^Xw5R+tv*2A`!Tzy=L!LDQaV6W>l&RB4~h{VNnuPemJIW4|au@?UhP1 zd=$0bafscHwqCA;p>(^Go_oDN_t{9Pooc}EmXRv z*{-JQlVJMNo(zvROR+v?Xm>Hy^oQva)cjK#pjnQe3i&TwMA|Bi8USh9vh$^{G z#|;oE!~N20sz|ASN-IT5Q1T<-R*~EU%iVVe8O#(VcBUB}&})#+MR3+3WLN3}Y2h)F zTi=Ql`R9)Jh!mpK6alpc3Q}sXQhR-%6g<%`v*i&fNvSs^t$R|GWF$qN270vaNtT$QeRR*~xO=uw1+~IS>ug3@u4kMg`Ok$k znsJI0qSS${nU&1Yz7psD^Wc3Es-rPd!}v!)lG}f64VH>jPU#x-rM8Ncrqmw0IdqKF z*-D;nTDgoc3r?2v<9gqT2$qVeD1_axA$vol5=v2w@`(~ZA5Iq{Eq$aW*+eM_X{tym zO6)_g5~=V41l)oFA5oGQLYZv_6-$S8|3yICla>bxQtf0fhV&h3>-i#8UIJ;iN}Y_a zpWK_2jDpr1eefB*Kzk&$aIHrlM{Cz)J129}>}l0Dj6314pN_m?r3 z;1yK7A~vtNUDnXsJ3%G^MYYm{per>h|SQ!@Dux@gW+0-qleH)uFnuXhMGlx zOW!E9KEF-wctn|t;gSGxIvTV2lEg_9%!&bW?lw(;=d(D|yVO()X~NN6R8rb$D^B|$ zF*Ffg<00t{KslwER;p*K*NF$s4KL;Q)>^9QDukGdc-u=gFo{wEr(3I(G^J)b+|<>x zRHRMQ%-VsHjj3sfV4!^@lb0lJ!IQm`8L;xXMG-NN{WPNcs7Q{pn9^&gn(ZKAOG#&% zdQYTt*fG){_U!AyRLsQQ2H?`tV@=G0l+eSsBjmZWp;RJ|Y@t!ouO6P8JX=aS zE#E1vl#V%1VyfF9zIDe)=9mU;m*{D_79oK;8N-$2r6oFmHTW1w3aCCi7NwfuPB&lm zhLVXQZ#E)%d&Fr{3{sAL1ng}u4PbB{BzcBqA03NSG9S{<5`$i+J6Ul!iGPZg<7qyOKAtIDSe7w`$&;ucf#vc z8<%IHNQHNq?Zf24P2LUD3VpOVB+rQh?bBo2UT&gvAL3i61$q&@zR>t=TT`{z)E_R< z#h1a-4QHp16vXlhNH?2tU+e6jdx51W3EIi?>5nXAS&BN+L@WlrIROxYR(EX zbcD1AlW2JlM?6(b&V2~aah+8n6;qP2MtwTJL!=-jP7u)o=SuQ`T2J8sk^J=flV?Jt z8cOd%(%xNcCH5$7Km%!*gXc+$_7J4qN3Z+bhat&?37th$NNk0Enhw@GkHF&!8?|kk ziykq}FOZzfJn9Cv0}8PVq_18>jo0*2$tYL40#}H3kbRFK2jf-Y0A9Q z)QpS&Q(7rfgc2{*t;a~8>V;!RO^^5LgX+T7*v}di49&ebCHb;^e|JDLQ30`+ooj8|T*)0t6UhxU2E}TsNNEO~fy~uPF?X%$f3arn z<48nX9qQ_I1n)s<(OR&09Xy(A^I@O$7adUsZGcip<%?tLKP_n~>JMWeP4Xr91Y(<~ z)sf7lx(mU9Fj0;>YP*8M{=o}(okGzQ$@;B z+Gi&j+8H~W-bDQ0p>Hitk+-0nto=)w{3kX0oGp;}WMY^|NlGixo6@nqJm(#_eGN@| zSh;zdsCn&Gn^WNAhCMHh9@v7hBj|g(Hj;#Hor-@1T{_i#7ZS!rg}wu6OuVpO`IU zHMyU{)Wm!>NE>#^ri#xXp|<3yx0!le~;E>g*ze1tUaBU zHi~$rJuds`a?Vb(V4TDiqiF=LMD4I9DAgdHwkUpKx?CyV!3^SMXGRO4D;1(j*rRzy zuR^nT8m>J*mEC4sfArLB+?iD|p0~4jKt@;tRk($764Wz%3J)+3mMGnHhR(KkDRL@8d$G4w$jx11s-h$l543CCQc6mO(nv@n zMaumZ0Wa5idUk41fJzj{|Drg>C`FEWz>%P~iFd%iA)XkD>U_zgIh3x#-n74vq$I<) zT3zd{d=;gWZQGU8t(KWSBfpFBe|{JOS1rraJ)Staq_qt%D+cLbEF4%!vU(qq)oLo1}mUIfxZ z%TQ{BoNO;AB~9rL?Ra5wV$ngfeTF<4DgFz)xc}&06O?$z9V2!`p>Yv+leNQt9W?j- z?CqWxJQB)I74#oL+ey??<50@QJ@Ps&NR;NG*4rUc@!v?$o9OQ~l7f&v3~8#JlP*8( zaB^!MGMCMgr%hpbQ=Kkr&A66Q1*hr(&%;HPAvQs;DNSTwPDfvpl6h1?>LO)W8zHGm9V%T>w~rKQe-qPujzs4a!dZ^7qtpvxsi86o!;7N$n!`t?1?qik zAE{sGw19LE>Y=G3r76i6h|cC%B~tZ?X3F)llUQqb<2MrCMsiU!;-_X6RgrAg;f)m!*P_zs0n_2-1Apnepl{YN#_L$w*DX1d*z`nl3j;1ljH| z%~fxi&3uDa7Cp?08|1_l_JFM`W@PKID3uHCqe8C^o(6lu=?=| z<$Qg_->rcRhZa$h9F*s|ImLZVmz%^fc?uj)K$Vs_f_6S|oG6!y4zEOrolu$@Nx^j` z87S0Bb(lz%^tzq3u}Dra_A(!mc2q`DdKtB0_6_MhW~g;S=cT=rZC7ILSVfQh$MiYl z{SY}vh$2Le5QUDQ9)kQZ@DM60-x1wbR9Qyc9V1>Y^3jRj{Fr!-O|AEmL{Uf6zA zkjhL{?=Ol|IVJg(j^4iS5vhWby(g6s^ZTTMk}KKGslle!RT7;jl8LtDV}QW3P^991 zX7{}O9QqE~u30}2mwhD_ z`B8w5MCbrelF|z1ut@#^2zLo9Igvt?cC(TbDfcvZZNMG3PN9ucYHgqBg>TavKYhQ3 zEN3C$V7aK{l!o)csYsP)!HWYO8%2to4Qa1FTEO&vk4~>GmM1{B%fY42g*||uWQ~z} zVABY**_uKy^}vkCz=!#HwL+$Uxl{us=fflq`t-qJ5s?K)o330+&2huOpK36pPu=<<`E zMwz;^KqkyM7rC1LHqCRR6xdA{nH3A<=rb3?v>yB0rN#RaNSx)|QVQTEN`IpI)Uq5K z1+O)zBNKPZIoeNcC5A|~G*7w|ZXemw?9tL(INH=-D7*H_$7FOl9W|8BPAH_rqM!qu z2};kRbZix=NIqqx%c%&c1DpX$OKl!`ba1m`tQmcm>?Rn7w?X!;6H_H6b~ty5l%gb+ zf&JZ|6vE0YP5-+kg4lROz@~b>m|Q8>J}vRSDAFkT%$+VzAdB{il&17B64gx#UC9(P z^d5;bq;Hjx1p}!0`aCd5seZ1RTae$;WbTn1Ei8lIl^BK(5}!I|Rzf(Q@2{WG_tnVK znbTW!fWBfLw3{$3Qpj9rA`_o?tloQXE-o%(zz4OW3p! z$jMqdT$-W8kr?gBrzovMVbU?2;9Uqf&Ya&xstzsARJNjhNp?hU+jln{^D$#V9JBv) z3@wrTD5qBo6sLU}-`!@x5;=-2P2J?X>QWm@L3WkP>~DVTFiznj>}8(TWIgjD#J{yO zWq(asyx6Q*DzOEZ!0`-Qyq7PNeUGBVD|nR@(eP;htZ<2$vP>e#EJFluqCrec z47FtLt&z64Ov-Tia+puWrJ^^;6_n1flAYkXX}OuQTrTP=nz-veT81e#K*@*@e>!H2 z(pQ0f3`B3+Pm~^$HY|Dz8>6(%b|&qBN723JfYA()4{GZ&&Ghm}PR-F5C&{wzNc&#L zJ4R0u_1-oaIW-TM-4zl#wHEHj;m#;Q+c$ znNST}gT7GiQ@~?pnAbdM)~=BK256dq@kYJ5nv{h18O$i+4@V9pH?L?tT-Z^`C9PN? z+1nlMq2&J4OM58$dCWfitf!iI+mWC31b94{KIZA81$Vuf`+($^?`a%)7khu;(K4O; zjM=TG*mE#-)MxwMw0AM*c_>>@#SIh5w;9qRD|zOt*JiWjK?$FE5vFDMrPe+%m82l? z7D;;@#gulU1Ejr<3?)utSSXSsA7RzyBh=1sX!(EL)PG2m`36iFv$Mb4Nl>c54a@|o zigVvIYpwTN@ZMp1d3El=!*X30y$wyeQQG9z(Q2Af2i(I9(CT>$yzauok)(#KcpsV! zxJyXSDsiij(bI?JzP;jOnEKjlFQ@WjGvyI29F$o&w4X4E5^r%!j^L47+@gO<1El!m zQsM}#b`xs0VUL%fOQz+{N3Wi^;l#pIiL}Qs^Q&1F9_Scbrn8dEtBHqN;Pjy!&nm#m+9z?0{D*j5?)>9P5b>kW)2>5y=cu{Y`K=u^rlx zU2-1EWu&)K&Riu;Y22)8OB(zF@vlZTsjc6pFU-(2lE8EZCJsgF08f@u?PIFqm-8AH;CX2+}O%# z-A`pT??6S0?1Ixdkmie2PU%xL4Wb0Th0_&q+9%Q|O6Ni9CUr@K(v^@Vh*U|bC!ZyV zl=u#DU4n{Zn@CkPX7iJp;2&TbWFznlmO5qs52njgV)Fk8(~lTo)bZtNO6Rk{h?Kj> zY*FvXFYvwt-Mo=f-N@K%|D>2wdtt))t*8CO#NI>Z)D!d1%9*s+>{g%fAMknX$b{_b z|C!P2#gf?%i*zB5Ja-HqfM!pAr@Ro6w;=&yx1;6BmHOj0B=}DcTb?M{SzCoBFY7(d z&SeZ9#IB?owQp9~Afq7iQuY2Dv_nGMW2kKO1|)b0?x+~NTC?~mEy4*IEK>Pj@EV7! zODmZar8uOGA{G7(uZuNH?f7$$N+3LHtxcL&^VBDww$SBbb~r>TrgR!Q z01HJbp|lW>ZOR)UkS*>#^Bga*DaZY3ZHP{kO-q59`;ufp&<9WS5NrZm=}sK@7};9| zy=FpMDpH10KU;CxpDWfJkGNPb){(6Dod9V7ytLLXYz*nEBmLVt-ZIhC)QfPCtB#S@ znMLcQOq4W5$On(q;7KadNw;H&a`8+mgwylLQLVwVls-2f{3?C-SaW!+!fD%2Y=^$P ziw)5Z*U&2CCOVE;bp*8?vu{zUY;G33Dmm%2M6`A?fQd=4@P-^wragAD4^^)ngRSfUts9QA zy&Qlm{paX0IUV8kC~u^QQ%%FV(%+RwTY0s*ncZ({SF{^EW3C4+P4XK!p@z|4<;u!sko$u5_lY5ACZOiqADu z-j?VS=fQiX){Az0qGmXhecD5Awrq;b2dx-kx@?gwtB~(!>hcXzu2F|VkkT?zdAbRG zF28k5%QsPV8HJ98HU;}HG0oqRnaR0VnknzdUVZYFZ(V+|5qY#+$v48ci%VYBbudM6 zfoW)G4y4$T9zx0WX2n*CZ_`a?_f{=0^Wjl}vSAZ;DOWmNhNL@Ba5 zd>J4f(+~AgIu-o}Khgg5yq*2|gDVA5><7r1apTbvEoWQ%p+N@R4QZuF&H|)`FX!5- zUX*HME}zzIg?E|-@5`|z?}7K5=9YRoXtl^3P&u;{@0*mp^ooF+{0#8c62P zSp{W~2F7%$_@j{I#|dwvYN}psntv?8BWq!~9=~m#AZKdRlV*XM5*uMUMdw&GGYhKa z2i4oOntB=f|4^|ukaG}u1=7_R`fk3%d%Q=_O=ZeV`9z}1z6N^_dyaCdUo*Qu(Yxxm zA-`;I^m2-}m?@u%xBnf;?_dX7m2IN56}6joutHnmH6Gcfl}wzHj1bCO$LeYaD`h^Y zmg-R3mlUm+o0%WVz`#b4vhp!JUF0(Q0+qO{bkSKE7#L#cV@I~^NN0i%MyETmpw11wqg$?8Mm{qX_BK0 z{sieqTW!e;+Vbn$+Sd6Q`WrfK(aenAmY;7TpX=boZ!j#^2fv+ia`&0FpG$%Q`(6L9 zbfTPZhxGcZ{xZ#X-=rb-I3qDc5$u0%?&&9&&w;%l@B7yf8_L=p&y`7eSA^V=x$<+Ts?>IWcB(K&hkDtk&C-P zZiCtKE@DhBuZY${JUlEUc0_ts$q98OAMWe6iL}47S@5OgM7#&0zDT`WFDfo<7t^j9(3#batEd0v@A_Rx|4X=KG|sn$h{t7Hez_9OJos&XUi3Hb zKb_p2wIBR{C!#C$e0~q&jtYAY#z3a~3c_%<3AZu37tCeyeD#?$Mk$L1dJ8c{M+?Jy-_CO{lx$H)v1L z?k3C4p@lpxOS!$D+ef%vgKgA#g2L0>KF96L+*Wa$=Jqpgzvgx~x4&|GkXuh<9GH*W zLT=pyXlq&Cv%F6Vhl-u(@H@E+`44iBe{t_)ShN3R^8e`H{}@*LK=OXt&rp{AJ1VHd z*>up?>IphSk72bBC+m9$)PBJ+toDn^I$KNaqmN;=UqRLeK&la=7Cd)hN}y zLIdmWIPd<8KRkvtf=`cO4e~WP)LgBOvxl4{?`P-%WZ#d?ZL?EOfnMZIOmcFz@eLS zAq8JPluOC6^AY3Fs9t5MD{QuBr zP}gpeE-~_!fB9#~i~HOCHjwW~AV8wOvR)+=1BoxIK~EuH2qFP~1;!Ry_o6 z8E(Jhc8{s`bAIb+qy%R7ApF;|CApT@;PC(3?xX#>|3`S&9{acbYPY4b2i3qF#O3$*I%s-|qj%{ucFW67`~4mXBIE@(~O9vW?4i9iJ#i+2i=-zoT47 z>^>*d2>!Y9x7v^8m=imO{X$MH*RCXQ60bAXHp^|DOA${a zZvEW$vRi4L2T~Zp?HF#SaXXjWCEQkW`x3YBar*_g`?&Rv#*RC4Tf*&ZZsWQgE@kmK z3R}6|#jSH0BIw9%FK&l(dnvb5xV`o=&-z~1c$XG|2><&+p0a;g^FnQM4S?$jmK4S1$C-+g0{jO!| z#syn++ub4FGkf30d2fUCyOm=;k^kV|LHnqd=B=?Zg7Z6rzZ z#95`K8ekK6`nYKpQx>O5;?!8BG`nG-nty#jp$szuWfr`yA-wlF<1Yx4B6iE7SR zVaE8YOUo2%ES8NcJN8Jk5NC}aH@kE^(!Jl6CfOaw>gkTlX3<*i;_S%us74#7FyR_| z)D^@dN1dQBwRF0=R}!Zmv&ZBS4Jp`kgym~C5++5SH6BT-B1a_+YEP3Tm5SBGvKdnq zGe__m%Qcp-pXQ$1EO_?#8PjdH?N=_FjFS+{6}V=)>szjP-K@!FdWIZRbX>ujqQqHp zSDPLwtLy|>#ilz>&e&<=reE*KcZuJXFK}e#n_y1nI`h*)&$-@vrQUY3cAZ!uqdNti z8)a6g7JtO9AJcVgwYj@N!H|yeHA*S@l#_&6XD#w=kvdLhy{?lowk3Z*V}7~IbAF@r zvpGVs=XITU!LDC2VQ;~RoWxeM-doTtn%t^tYHLodv8B_;&zLZI`c=**d2wo5bSFm- zcxzoPiU&o@sMm~%6K6}=*&&=&pW5SRTr<7Q`MvJg$rGG~9;d=1JA79It8kmgSu8?a z1WAEir|ddo*U97GQ$zd{iTGOQDe+8vW=7oOnVXxIFF}jp2lK%_o)f%3C}el*I`gC1 zdyl76Zd?Y~B;k9EKkg~Yjs2vQ_}PTwp2FPNFG{hjiNrmvi?jQbl7DELPU`L4ktf&l zJ$VxOeyhvpE+p=Qx=zbOP>JPls~k3O#XYS$CU2>&_W$TQsSn9=B>1;^648$mNBUOP z)3=%97I|9bC+@Jm3v?Y{Xiiz=X_KG0%hF=&x718rUB~5v&Wk0B;-4tRJ~ahPJWU&AKeZk^b)Egn z{w?z!mUy~0I%L7FQ+Ay>RM%X;#B)tf>~@n~;^~o}$ao}F(yr4P(`~6|SV7`<)e?W` zI`yZ=+`iN^GbbA}2bOwT`*o--2c>4o@luP7yQi|pTv&1cIz9j;8tWLycRQ*v`A zOMQ6Y1TD$;H5RQxE!lf@om#2(u~MZ(v@Nan_7! zW3QVr>uRliQ{s_aWx~rnEl-bjl!g5dVoY;Hjm*t)lbK~z>+xy1fq@YR1R9Y$Zp{|qLb)DX2*T31dd_PjG^G(Qj zdexJ61PT1tlp9amCb9Q&YoWqf(NvD~eYM3tHYwv7d|Kv!Qu1%>Z`WyAJ2{%pdkSYr zlQ@2ov#d}8+>)n`Sl^|8#}=DG6`mGR+dSRUOs??To7r-CSF;({O)s70tZp`T_T;&x z&hyP8rKMAySDH!vD|Je5>-1)`sWWC=Ep5U(%_OBOG{WqCHrkbWIlbZOJkf0OY;l`i z>TD3N_)4`W>^g1NvHSC!`jRTy$odhpw`su%mnGz_Mq1G2?mTaXv%2|2=g;P-lAmdD ziwW1rm3u^JGhLU#s$1=iV01?-J`Q zIlj_y)10STT*tqDYaz$?xOIw18lBf#I7?0xX`M)Qrj4`xil188yf8oC>uDN|J!LZ^ zsq4(sh`qd}^RUDonN>P*@*HPn%SqCqk@HX~2}sD><|K%<4T&U~)x_-*xNohmnPuJDxhOW&uIykFN@yUsjdl?Qd5k+qW}xir&xz15@{ zv&u@Rk9FQ|C9V&e8O;ltbxb{^Zs|vL9eYgI@zuIc+I8BlvyYo6n-`26m6dNyizTV+ z#6}yjoXtO4mpV)QqFY7J)Lnj_xO+uTi7dv?bsgWS>-6{aYmK#6@DpN>|6Jb;U+L-J zEB)PZV$6Q8>sXC^#@b#y3)@VdK3NLU;x=+HqKfTDUB~}8&OE=;b57Ij0d+;)x4+F; z=WlV0A3Dx7yWi7wO5%_jXH1>oY;LP1GSyagFPg+<*IB!c{blzyBjSABR(gJN5^KaS zE~*^oVYS)5;-R+Euf4mS989c%renUYv$xs57n&#U_l$13Oh%$|v`snOZrrS^q)q?2 zy*Ms6tsn68thL;Xc))XFljRNM=x1n4{$WSSp5+=oz1-Y=V!^<^Sxa%FTGFo5vUYN$ zzxH+qDe986tr94HuR6pZ)^$o=Ink=1Wi<1WQsQNa{i>-mu5@k}zu4;OXTc!8^M=${k4tydtIk%be;Ltu7A^Y)~*x#^zY@3iTmY7#s7xeBZAJnxOAbrm1QUN*Nc9tr2>I^xL^TjK^RKup1n&TewT-hjN zDYneUXV-~k=9Y&%&2o~<%*uzP9Z6X=z0AD&kf%vBxm-PyM)zQowXLhBG1FDf!oAkC z!bbIwt}}L>cv$~VJ)-NxDqW{n>pJ_4uH(cfQH?nL1H^?3cj&_f9o}!9`X32vG0(G$fR-8oi}?- zC@quLO)`JC#FhL(T{HXCV?ya1=ZhYb%be$Xiu8vX68mjT2dw!@=dT{x9z3MxgVyku zuCuq~>#pbPIu+M-c99HF=8P?!Q|3I;v#YZ~0wk8GA!Cjh-t6hTBYLbt^?0SOV{6Rb zM?5FWcQK7`mFLW^U#OU{>-3izP~!U_aGkR=AiMZhjp^OG&it5fZeHbSo0H9&C)*XY z>lE8-<^4ANpZ0INP9D&|vvwUjm~Zy1@|@$39oDF&wpr7A#&yn9y(JxY%D0&1Cp)@M z++)VIFE}kfbB|K2QrGDba<~ex?m1Fw@ulLbR zd*@^c_q_UK>^duJd+qG%W3L_OyS@_rW)tsF&^AB5Ssjyho!)Fx9SVlF$ykcLpurI0 zU|%V}n^fuDu~$?{Z!4%J4ct`cp;ILKuT+YEqw9=a zr+?JH6MJ->`l-MySnX-oJo~F+`VVXRpRP0WeX@?Kl_#>q9- z`*Ew;bz+^*nHVi<8{^nS;sB9&U|41{!rKP zk9D2erR(%Ab5G}j*3tBSrL3%_>!3|<;yoX$ztO4dr{#&cphncB(P**e)e ziQ1{0py}SFpm}|NFp{bls~t(zlIh02SaI(`Mz60?|$Ks&*uvS0x?e{S=+DR3kLjwP^?y>RzUs_ z1XDSwT5(TGVhX2frE2+n$vXa6t)#i2d%UQt>i>k@G5eo8~7D))bV$R zAm6#Q2?+NB{{ROkauQe{8`=cQN0W6qL3XA)jLAonb&-#DTTTrG4-!^?`KY>GbcT-l z2g#EyM`f-KgYve*FN=I&({lWB@I^xgr9ujMW&Nv9~(!o#uoSY!Ov$H>mLu*b<^ zvJ3^;G89@0`zCU5ndPXHrjUf-{NM&K2vASP;lIIS#s)Qu#P5af&-s|^Yg$?HMk`?NR~I$!~ZkQe_8rc@M|k1%b6~J4!)EuUtG6K zDLGBPhMfEcb{WA!4({154KKi|&S?LpOQ^iJ!nShGIGNGg!wz>~;P-_*>VX ze1P`s|6p$+0}vXYZ$G#x~>z2HS>^l+mzsaH2;3hIO zq5hFJ;2z{Sd9Yp6A-N zD$vh)rPnO0bN1SPqqfbG_t=;FjdGif!&_a_v*$Zw(&h7_y7Pbkfonz1OnY1U#W5_9T897b9 zlbj(x1dcjc3eVBO(O+=MvX$&3e@*t2{~`y;O&j9?!sMRh2zdlKL7r||%3qqo-E{Ej zk8EUloa`sREvz{ZB!5qikew#5$I0!4ZOunMoAwNOl-jlYh4j6bvdm?G2zde7R}c0_ z$;n*so8&}&@HfJmz)%BlZc}iI+?nk6!ah{kw*O%Y`4@6yaYWC&(WP zYYt_}*+M!0dPI?C2$0td4ry{XVSB{QVIM?JkT0iyq$TWgY0tI-FDCo_;I(958*nPh z2omifd`k9r2LDVBbOqOGjs#}N&B;zT*t?Vca0ulQA!l4B@NiK5jC&S)PSkGaKTtdza zfc;Xk|1|IT=I$l2h(Tz_)wmhexN zz&?naA&(Q*0|=ihc5VM|rjR%f0pjHBh2WGr5H^dTmql_Z_+4^%8F(in2v&f9a`|5HU*tI1*9HfaS^;}2a_CWTXUkFPQmui| zj}AfdS>*T=uwN>y2@E|6E~7m|p6Bv<*zY07o&(=6tl|Bez|X5)+kfBl5Z+Qj9mv(> z$P2J%$)T6PhlMr5?91RrZNceRz}{4H62o@rD^AH#kkIYI7A z4u1msdE_+til`gGr*OE293$UKPLV6ffok}#A&1D%lN02R$-&RzzlZGG0gnDn!M_ti z!wxv2;1}SIWamq8F*!v(hwRJ1ei=FR6?i&1@iq8Xvh$7Qs8c~948d7TPLW?B$9Exu z9pnu8cXHx8*mFDL0G#i^Ey(^Fa2ImgvYh{Z6tZ*}LH7NC0Hx#r`9^YrTtQBgH@o3? zBm6t$)Q{jD!5-xhspk*z>PcMh;+`eF73(A&)`1f2zdZG@C)o`k(1=} z$o?$b|4ZBmM9^iN8-P58oF?Bu4(x^hUF0D70dk1^r0f4H{9keHzq0-Rkb?g=ID8|l zHCOm|@NeXhz5qg(BGzo=24r78?9GJrvJQ}o$r&H)7rWe0A5qG3J%!lu;0j?)K;Q)M zYL}aUH;^NR;1oI368s@K+Y0;*IjoOVWjR2OMcYF-p)+>q>i{kyhdO}=kOQ5;LtXzK z;7f${h@Dfw*OFtWg70$uPXj+ldo*$egbfrTXMwkp6XacF-`TML<=V+jx*!4m64*PF z6X$@>7PgJix#00&x&I#q;U+qy&jXi}{rZ$ymIuf|@-t-L2-vr}d?9#;u;xhcGVp%d zV^@Lmx@v;t`b$lQ(A)}gFE|4%Q>jo0Bj7WIZ5HdhC}p{v_RuxpYsi6X!MD>tF&F$8 zIea7db!Bb;(>Fu-#*IMc{_FBHIlKy-(+vsAJPIx($5w-j$<7*ZNLUk`dK^5J_T-b` zn6PdCgGmU64!-r^^<+P}iku~X?fO3r|3AqoazS?{SRVB2(wywy1TN|>^`91sIE7Q_ z5Pty@P44H~o4|f4IjnOtWSLHXpG^0cCF=agb&v_~x)^ej{J0xHCVlJjEIBRn zv~_u#9BB>S&hU=RQr6{na)w+tfCR^7`mJq$PoNN!xv9FeBkNpMU3v;@md0fMq%LQZ zeKIdnmrKZDnaillc!rP5tUq0@CI|b0uO-L&3v2s-6C-fn5wn2ozAL7JoRzt7x>S<= zGEGgF4dl>3@T;yrIZY0p4*OTGU0C<;?4{rY;c%Fo7zA$63pwCB1Kf<9Aa^D^XTp9u zIY2(293fvxPM)drZ)7Q>;2(?#ZY3wkOUb^oVSj|28Uo%x4wZo464pYQByXoZP5y}- zJSU0(PH!Ax{2Z{4oFsQ6`-j3lkQ^kBB!|z1eF8Z(41Ar-=Yh+~q3Cc3_fm+H*SZcP zV1I?2A%8}WoDchMa+Z8hSaTqA0qn<}1WxcJFHOnOB!%`Ak{2UDPjZSJBnK~n{Q`1= zJf55(&m{Xt!~ZrnJb4K?>O?Mw!-I5it^ltk2gomwQ{=7W;8^&7MUIUF|3S`>bNe6% z{1aerYgx*lPbYcF(uWQ~@(6NvBJ5#u{3>u6IW`G=iyMA2_-=CgYVZoOGZp-#vbO(z z3Mo1S$nTMZx(1w%>*|ghsbToX>vDV+x~}UAwXXdY)w}NzQB#( z8t_DNiX0@w(pDgEJk2pl(9y+9Mg~R=1|83xn!g^T+ZwFVAW8_cB337(v zL*?+_OAg)zJ}!C+5|F$HLML*18TfRv&w$S(2Oa@mK@O86?QE8w5Q$$*Dgl<$@eTro#rCwDR8#I zAwY)+`E+vjW7vn&Ke-(|mhAf)JdOTI^3Ak6-^0F;9I!0s{{ad?Iy^-VkzXRm$y>=O z@^-RQgB{e6gXG`IfgfN$Oin0k`*%V=mVw=H=tz$J0zQQt+6z95?Ef8nF*!pnB}e{% zeKt8szLo6zAKU-CC?rJC#gJ3v1UYyR_Vr}{A@Cby=P&R_WFL9Eu$HOpVc2)O;dAQh z@jK3+6aqPQE!Xdl90`(Jk|X3U&KI*&e*j8UsLdR0`Da!I)e|B(`4_d z*g+-$dkb=+7q|;K-5cD`wVwnYLXJlCi!icWL?J^SM-KLdeKt8lj*-)+z`mI56oXfh z1LXDO82M#kIVDji(+>f*x)GcT{+gU2|45F6U_ap6F9O#YfO8zX1neV+M}b?ALzjX( z3b)exfB#qrz37l44~z4iV zlWWK^@~Aoyf*P+nD_%NgVtdARFO zzLf0z1^?2h8v%uga4Ss^`9|8^moF}GBdFuCJ2bAH{HSZ^yBD8!?PUFRj2>{B9Ni*< z?$Gfd!fH1Hop&$Gw{8UFtZUDOz1Hc7&`-{DIS+Q9vL0-Ea0_y}tL3QEjzWm5+ULUF z-wi*vp54DQ#C5m<4k2=kJf0l95%yUw-vplL`jeNCQ{)H9iJRg7f@{Ay3gJ@<89M9{ zZly;|`yZ~I>^Xxu5JUK8!mYegsPwlQvUDZ8ulemqUP}9U8a`jf9itR3rjR0!CHv$Z zCAv%zE~q6{sodA=ax*z3quRPGB|FmH*5xsBMp{W-HoD=Zr=`nVr*T?p<>j*-tM`x$;TIWr9YGss!;Ensz^MBKR(HlKcoc`wHxv$uWli=xjOvnt<4|aM(=;KLgYo0=w_O zus0Hta*lp)KICo|8rn3CkJVNnCw`)oc}i{ z_%A?!?d0@@;JxG+b0}{pvy|ML9ASd{l0yR#{yef11WzFQPZgH^J2$%lWInzw%UuUf zmw%j`*bMtta*DI%YsirgVE-RE@FDnwbCKXMXVbSMCw6fDe;?Oj3jz!y2g#FMeh>B- zIlC2nA34J5_)nAL++h_tO#f=K?>o-_uW=*beEq}Z1i7&^8hV-fxq}{L=Q||eG_w54 z-sa$WE7mm~61B9Kf z!vXN`u2f;Hf-WFL1h*tO^6*c=^04*UfF8RQ5bV%-6j`F|81po7y15j;uG zl3ypsy1@RK>wgY-H#u}V*crhr4}y;;$H;9g%k>vvfRpGDBo88oPeTMDa`J5OIC5+_ zc(xnjbN0MXY$>bpUMsk?E=zKZ^sQGC&0hCcIz+a zzk%FG`yG4|IlK>i208Q_I7D`6pG?lueiJ#)$!dDTqx%c>sg3skF$&3n zBFOS0+20!c5joZd{EO>fA6#4Rh&00Vap30UB>5z=pFE5l7uNoVQ%b?t5E1-`oM{VQ z?s7lyI&y#!rpRI1x08d6Z@UNkMcVV9PdOgE0yv0xr-hATlO&$;b_;*^r0EKcEnx1bI{=PKkt07j--v zji>~nGrSZ;&+YGf#`^o$CMEs+&hNZ>FTJpy-4hq-Ur9&6Jcc^RgF{$8Hyt*Oqw;b% zj1FtyKo*tnfXx>25x8kI`3Kn9-A#de2-q;f9wjtF1vX2^QhpZP^e>$QMX>iVxdb-D zD_|%5F6?k4upa>%d+EVND?qhn;ccT-aTR^?k(% zIN-Uk8LooElc<4Bum-;md#6yj54OS0vV0zuUm}b9e_(-NyR3i(gzb*dZ%SCGUl8Xz(cPh0noG_^Qk=qx$VK ze?B=@{2;?PL`|5$K*N#i{|rj7BEf-#S#SX5)v^JsnA{9|ZB)Mz_F)1i;2_#-g#++i z;mGwrga(r4(g;0B7zBqg!%?uWU%c_Lp%h6YzYGpy31$uKK>e+781;A0#r0poipS$f zP+T;lR=DsT@*l7VzrU}}qXCC;j-*RhQGNmJSxKG=E9=P4dE)pR$Ha#@7g03x~u2jQ*7)2$(NYg|o2t61ffbd{1td~22#2oV`kyHSm~pZ6WvVa__F);W6Aq$$6C8$j!nPY!{~+v$rBBYU;eZ<>x*`J@ zaZ(wL*a4@(Ubq1ET%!(V!J&5Y5;%C5ydL(Tego`Q5cmiIFB&)r2O{6kir?jemEJUi zUu8bZ9MC;cU1Nu8`%CFO(H!wJcAg{B5vzBzgZLT>qWu;IvFYg;v-nZd+|U zeuuT;!L$h?pF81rf7X6EzES3BA0Uuo|YF#M1?zAE|<4PqX~Jd)YUT=2x= z+94iAPXZB7XMUNvl(~Z3@oC{?enW5HBF^mKn^JPQA3 zllm7A6Vm+K>(z!>Roo}sUMlvGu-lyrR=O7I!y437|Evb}QBVJZJ?i;xdP{QR5Pf-a zVyXY9%|IlI8J6O%kv?%sv context) { diff --git a/script/update-libgit2 b/script/update-libgit2 index 301b1f24c..3960f37c0 100755 --- a/script/update-libgit2 +++ b/script/update-libgit2 @@ -3,7 +3,8 @@ # From root of libgit2 repo: # mkdir build # cd build -# cmake .. -DCMAKE_INSTALL_PREFIX=~/repos/atom/git2 -DCMAKE_OSX_ARCHITECTURES="i386;x86_64" -DCMAKE_BUILD_TYPE=Release +# cmake .. -DCMAKE_INSTALL_PREFIX=~/github/atom/git2 -DCMAKE_OSX_ARCHITECTURES="i386;x86_64" -DCMAKE_BUILD_TYPE=Release -DTHREADSAFE=1 -DBUILD_CLAR=OFF + # cmake --build . --target install # # From root of atom repo: From 5f2bd9edd7cf1c381d898faa74cf87f7b70a5b72 Mon Sep 17 00:00:00 2001 From: Adam Roben Date: Mon, 4 Mar 2013 15:13:00 -0500 Subject: [PATCH 075/308] Add a prebuild script for Constructicon Constructicon will run this just before building the project, so this is our chance to install the node modules we need and create the .xcodeproj. --- script/constructicon/prebuild | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100755 script/constructicon/prebuild diff --git a/script/constructicon/prebuild b/script/constructicon/prebuild new file mode 100755 index 000000000..23ac36fb5 --- /dev/null +++ b/script/constructicon/prebuild @@ -0,0 +1,8 @@ +#!/bin/sh + +set -e + +cd "$(dirname "$0")/../.." + +npm install +rake create-xcode-project From caf0dec598f132006c40e4bf8797db12eadc92f9 Mon Sep 17 00:00:00 2001 From: Adam Roben Date: Mon, 4 Mar 2013 15:21:21 -0500 Subject: [PATCH 076/308] Make the prebuild script noisy while we debug --- script/constructicon/prebuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/constructicon/prebuild b/script/constructicon/prebuild index 23ac36fb5..2e7417cf7 100755 --- a/script/constructicon/prebuild +++ b/script/constructicon/prebuild @@ -1,6 +1,6 @@ #!/bin/sh -set -e +set -ex cd "$(dirname "$0")/../.." From ebaa344164cd7109d5a7f06b54fd61413828b6e9 Mon Sep 17 00:00:00 2001 From: Adam Roben Date: Mon, 4 Mar 2013 15:38:39 -0500 Subject: [PATCH 077/308] Put Constructicon's node in PATH --- script/constructicon/prebuild | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/constructicon/prebuild b/script/constructicon/prebuild index 2e7417cf7..477bf609a 100755 --- a/script/constructicon/prebuild +++ b/script/constructicon/prebuild @@ -4,5 +4,7 @@ set -ex cd "$(dirname "$0")/../.." +export PATH="/usr/local/Cellar/node/0.8.21/bin:${PATH}" + npm install rake create-xcode-project From aae5ebc81041b131c2e9756c0fc0a697710af8fb Mon Sep 17 00:00:00 2001 From: Adam Roben Date: Mon, 4 Mar 2013 15:52:58 -0500 Subject: [PATCH 078/308] Ensure gyp is in Constructicon's PATH --- script/constructicon/prebuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/constructicon/prebuild b/script/constructicon/prebuild index 477bf609a..f0bffa13b 100755 --- a/script/constructicon/prebuild +++ b/script/constructicon/prebuild @@ -4,7 +4,7 @@ set -ex cd "$(dirname "$0")/../.." -export PATH="/usr/local/Cellar/node/0.8.21/bin:${PATH}" +export PATH="/usr/local/Cellar/node/0.8.21/bin:/usr/local/bin:${PATH}" npm install rake create-xcode-project From 01e0e886e60b875b6df1ce60b4ffe253594333d7 Mon Sep 17 00:00:00 2001 From: Adam Roben Date: Mon, 4 Mar 2013 15:53:12 -0500 Subject: [PATCH 079/308] Turn on code signing in Constructicon --- script/constructicon/prebuild | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/constructicon/prebuild b/script/constructicon/prebuild index f0bffa13b..d6338333a 100755 --- a/script/constructicon/prebuild +++ b/script/constructicon/prebuild @@ -7,4 +7,4 @@ cd "$(dirname "$0")/../.." export PATH="/usr/local/Cellar/node/0.8.21/bin:/usr/local/bin:${PATH}" npm install -rake create-xcode-project +rake setup-codesigning create-xcode-project From 28d4ea0456d711017208071dcc966d3915fb8568 Mon Sep 17 00:00:00 2001 From: Adam Roben Date: Mon, 4 Mar 2013 16:38:30 -0500 Subject: [PATCH 080/308] Don't source env.sh unless it exists --- script/compile-coffee | 6 +++++- script/compile-cson | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/script/compile-coffee b/script/compile-coffee index 298d9f53c..4ec30577b 100755 --- a/script/compile-coffee +++ b/script/compile-coffee @@ -6,7 +6,11 @@ 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 +if node --version > /dev/null 2>&1; then + # cool +elif [ -e /opt/github/env.sh ]; then + source /opt/github/env.sh +fi INPUT_FILE="${1}" OUTPUT_FILE="${2}" diff --git a/script/compile-cson b/script/compile-cson index bc3a79ff3..20f8f7534 100755 --- a/script/compile-cson +++ b/script/compile-cson @@ -6,7 +6,11 @@ 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 +if node --version > /dev/null 2>&1; then + # cool +elif [ -e /opt/github/env.sh ]; then + source /opt/github/env.sh +fi INPUT_FILE="${1}" OUTPUT_FILE="${2}" From 7b32560ce2f0ba6d53d130195ef4cb1009eeb7e4 Mon Sep 17 00:00:00 2001 From: Adam Roben Date: Mon, 4 Mar 2013 16:41:36 -0500 Subject: [PATCH 081/308] Fix syntax errors --- script/compile-coffee | 10 +++++----- script/compile-cson | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/script/compile-coffee b/script/compile-coffee index 4ec30577b..cb70ce654 100755 --- a/script/compile-coffee +++ b/script/compile-coffee @@ -6,11 +6,11 @@ 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. -if node --version > /dev/null 2>&1; then - # cool -elif [ -e /opt/github/env.sh ]; then - source /opt/github/env.sh -fi +node --version > /dev/null 2>&1 || { + if [ -e /opt/github/env.sh ]; then + source /opt/github/env.sh + fi +} INPUT_FILE="${1}" OUTPUT_FILE="${2}" diff --git a/script/compile-cson b/script/compile-cson index 20f8f7534..6f7cad53d 100755 --- a/script/compile-cson +++ b/script/compile-cson @@ -6,11 +6,11 @@ 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. -if node --version > /dev/null 2>&1; then - # cool -elif [ -e /opt/github/env.sh ]; then - source /opt/github/env.sh -fi +node --version > /dev/null 2>&1 || { + if [ -e /opt/github/env.sh ]; then + source /opt/github/env.sh + fi +} INPUT_FILE="${1}" OUTPUT_FILE="${2}" From 9e1b97577341a7e65e98e00e96756f63d78c02cf Mon Sep 17 00:00:00 2001 From: Adam Roben Date: Mon, 4 Mar 2013 16:45:40 -0500 Subject: [PATCH 082/308] Try finding node where it's installed on Constructicon --- script/compile-coffee | 3 +++ script/compile-cson | 3 +++ 2 files changed, 6 insertions(+) diff --git a/script/compile-coffee b/script/compile-coffee index cb70ce654..f3d19392c 100755 --- a/script/compile-coffee +++ b/script/compile-coffee @@ -9,6 +9,9 @@ set -e 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 } diff --git a/script/compile-cson b/script/compile-cson index 6f7cad53d..7168fd19d 100755 --- a/script/compile-cson +++ b/script/compile-cson @@ -9,6 +9,9 @@ set -e 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 } From 5421dddec69f629c15b9138fe1a9e033d693c356 Mon Sep 17 00:00:00 2001 From: Adam Roben Date: Mon, 4 Mar 2013 16:51:10 -0500 Subject: [PATCH 083/308] info.plist -> Atom-Info.plist for Constructicon --- atom.gyp | 2 +- native/mac/{info.plist => Atom-Info.plist} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename native/mac/{info.plist => Atom-Info.plist} (100%) diff --git a/atom.gyp b/atom.gyp index 1c8f40997..2e0a2ba90 100644 --- a/atom.gyp +++ b/atom.gyp @@ -74,7 +74,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': [ 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 From 9b2468a48458dba0196a1a24fba211641e4ae95f Mon Sep 17 00:00:00 2001 From: Adam Roben Date: Mon, 4 Mar 2013 16:52:19 -0500 Subject: [PATCH 084/308] Update prebuilt-cef * prebuilt-cef c24e35c...3ced0be (1): > Use [[:space:]] instead of \s --- prebuilt-cef | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prebuilt-cef b/prebuilt-cef index c24e35c3e..3ced0be18 160000 --- a/prebuilt-cef +++ b/prebuilt-cef @@ -1 +1 @@ -Subproject commit c24e35c3ed60952f9e3ed6b3a73e17f0714d147b +Subproject commit 3ced0be18717e65fcf4d832bded37156cd0710e7 From cea04758a58da61baf42c39898030342b025ff58 Mon Sep 17 00:00:00 2001 From: Adam Roben Date: Mon, 4 Mar 2013 16:56:24 -0500 Subject: [PATCH 085/308] Print the environment for Constructicon --- atom.gyp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/atom.gyp b/atom.gyp index 2e0a2ba90..8a13a0ec6 100644 --- a/atom.gyp +++ b/atom.gyp @@ -182,6 +182,12 @@ 'Atom', ], }, + { + 'postbuild_name': 'Print env for Constructicon', + 'action': [ + 'env', + ], + }, ], 'link_settings': { 'libraries': [ From 8394852f07c40673fdcc7ffe7d32b34bc3a83017 Mon Sep 17 00:00:00 2001 From: Adam Roben Date: Mon, 4 Mar 2013 17:07:08 -0500 Subject: [PATCH 086/308] Remove the :package rake task We don't need this anymore since Constructicon takes care of packaging the app. --- Rakefile | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Rakefile b/Rakefile index 704999599..310a11104 100644 --- a/Rakefile +++ b/Rakefile @@ -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 From 192f8841bb933a2626c91895c8433897cb6ae791 Mon Sep 17 00:00:00 2001 From: Adam Roben Date: Mon, 4 Mar 2013 18:00:11 -0500 Subject: [PATCH 087/308] Let Rakefile take care of running npm --- script/constructicon/prebuild | 1 - 1 file changed, 1 deletion(-) diff --git a/script/constructicon/prebuild b/script/constructicon/prebuild index d6338333a..734ca7c58 100755 --- a/script/constructicon/prebuild +++ b/script/constructicon/prebuild @@ -6,5 +6,4 @@ cd "$(dirname "$0")/../.." export PATH="/usr/local/Cellar/node/0.8.21/bin:/usr/local/bin:${PATH}" -npm install rake setup-codesigning create-xcode-project From ae1757aa4aa4e17c04cf3545dac32326dcd0de23 Mon Sep 17 00:00:00 2001 From: Adam Roben Date: Mon, 4 Mar 2013 18:08:16 -0500 Subject: [PATCH 088/308] Add an empty changelog for Constructicon --- CHANGELOG.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb From 716a78a77425c8beff5b9e83716f83507e416fe4 Mon Sep 17 00:00:00 2001 From: probablycorey Date: Mon, 4 Mar 2013 15:36:07 -0800 Subject: [PATCH 089/308] Default all targets to Release --- atom.gyp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atom.gyp b/atom.gyp index 8a13a0ec6..417fe2de1 100644 --- a/atom.gyp +++ b/atom.gyp @@ -39,7 +39,7 @@ 'sources.gypi', ], 'target_defaults': { - 'default_configuration': 'Debug', + 'default_configuration': 'Release', 'configurations': { 'Debug': { 'defines': ['DEBUG=1'], From 0c067b55ba2e9fb8de77aea3b17402b2e95fce8c Mon Sep 17 00:00:00 2001 From: probablycorey Date: Mon, 4 Mar 2013 15:38:06 -0800 Subject: [PATCH 090/308] Make rake install build with default configuration --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 310a11104..e26ae7a7b 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}" From 081e3a459fd08c75093df1f2abb043dc7a17dbca Mon Sep 17 00:00:00 2001 From: probablycorey Date: Tue, 5 Mar 2013 15:55:38 -0800 Subject: [PATCH 091/308] Replace Consolas with PCMyungjo in spec --- spec/app/editor-spec.coffee | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index 0d46cfba9..1f37a838e 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -554,13 +554,12 @@ describe "Editor", -> 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") + config.set("editor.fontFamily", "PCMyungjo") + editor.setCursorScreenPosition [5, 6] expect(editor.charWidth).not.toBe charWidthBefore expect(editor.getCursorView().position()).toEqual { top: 5 * editor.lineHeight, left: 6 * editor.charWidth } From 0a21ef4a181859dce496d9de9d9e6c0e177f7873 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Kevin Sawicki Date: Tue, 5 Mar 2013 10:58:37 -0800 Subject: [PATCH 092/308] Remove unused code --- native/atom_window_controller.mm | 1 - 1 file changed, 1 deletion(-) diff --git a/native/atom_window_controller.mm b/native/atom_window_controller.mm index eaf40f2c5..f81331743 100644 --- a/native/atom_window_controller.mm +++ b/native/atom_window_controller.mm @@ -69,7 +69,6 @@ - (id)initDevWithPath:(NSString *)path { _pathToOpen = [path retain]; - AtomApplication *atomApplication = (AtomApplication *)[AtomApplication sharedApplication]; return [self initWithBootstrapScript:@"window-bootstrap" background:NO alwaysUseBundleResourcePath:false]; } From ad3782753bd71a838206a639c92f5c63be4c7f55 Mon Sep 17 00:00:00 2001 From: "Corey Johnson, Kevin Sawicki & Nathan Sobo" Date: Tue, 5 Mar 2013 13:58:58 -0800 Subject: [PATCH 093/308] Display :skull: in window bar when Atom is in dev mode Closes #350 --- native/atom_window_controller.mm | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/native/atom_window_controller.mm b/native/atom_window_controller.mm index f81331743..89f6d17cd 100644 --- a/native/atom_window_controller.mm +++ b/native/atom_window_controller.mm @@ -43,9 +43,15 @@ } } + NSString *bundleResourcePath = [[NSBundle bundleForClass:self.class] resourcePath]; if (alwaysUseBundleResourcePath || !_resourcePath) { - _resourcePath = [[NSBundle bundleForClass:self.class] resourcePath]; + _resourcePath = bundleResourcePath; } + + if (![_resourcePath isEqualToString:bundleResourcePath]) { + [self displayDevIcon]; + } + _resourcePath = [_resourcePath stringByStandardizingPath]; [_resourcePath retain]; @@ -201,6 +207,28 @@ return YES; } +- (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_disabled = false; From 8e11ca58e525f6d51747e6a903df218913b7014a Mon Sep 17 00:00:00 2001 From: "Corey Johnson, Kevin Sawicki & Nathan Sobo" Date: Tue, 5 Mar 2013 14:03:23 -0800 Subject: [PATCH 094/308] Add isDevMode method --- native/atom_window_controller.mm | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/native/atom_window_controller.mm b/native/atom_window_controller.mm index 89f6d17cd..eed179a3c 100644 --- a/native/atom_window_controller.mm +++ b/native/atom_window_controller.mm @@ -43,12 +43,11 @@ } } - NSString *bundleResourcePath = [[NSBundle bundleForClass:self.class] resourcePath]; if (alwaysUseBundleResourcePath || !_resourcePath) { - _resourcePath = bundleResourcePath; + _resourcePath = [[NSBundle bundleForClass:self.class] resourcePath]; } - if (![_resourcePath isEqualToString:bundleResourcePath]) { + if (![self isDevMode]) { [self displayDevIcon]; } @@ -207,6 +206,11 @@ 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; From 0dfd3597fb31db5a3ced93fb4c5ae07f8a767dc6 Mon Sep 17 00:00:00 2001 From: probablycorey Date: Tue, 5 Mar 2013 12:48:45 -0800 Subject: [PATCH 095/308] add visual indicator for dev mode --- static/atom.css | 10 +++++++++- static/index.html | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/static/atom.css b/static/atom.css index bfe56f43b..187f53b74 100644 --- a/static/atom.css +++ b/static/atom.css @@ -10,6 +10,14 @@ html, body { position: relative; } +.is-dev-mode #root-view:before { + content: ""; + height: 3px; + display: block; + background-image: -webkit-linear-gradient(#ffc833, #ebac00); + border-bottom: 1px solid #000; +} + #root-view #horizontal { display: -webkit-flex; height: 100%; @@ -72,4 +80,4 @@ html, body { position: relative; display: inline-block; padding-left: 19px; -} \ No newline at end of file +} diff --git a/static/index.html b/static/index.html index 0e27bf115..75da64b8a 100644 --- a/static/index.html +++ b/static/index.html @@ -23,6 +23,11 @@ console.error(error.stack || error); } } + + document.addEventListener('DOMContentLoaded', function() { + if(window.location.params.devMode == "true") + document.body.setAttribute('class', 'is-dev-mode') + }) From d6ae5a17783f183640457ad1f8c057256235a642 Mon Sep 17 00:00:00 2001 From: probablycorey Date: Tue, 5 Mar 2013 14:10:01 -0800 Subject: [PATCH 096/308] Set atom.devMode --- native/atom_window_controller.mm | 4 +++- src/app/atom.coffee | 1 + static/index.html | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/native/atom_window_controller.mm b/native/atom_window_controller.mm index eed179a3c..90bd4af8a 100644 --- a/native/atom_window_controller.mm +++ b/native/atom_window_controller.mm @@ -128,6 +128,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) @@ -208,7 +210,7 @@ - (bool)isDevMode { NSString *bundleResourcePath = [[NSBundle bundleForClass:self.class] resourcePath]; - return [_resourcePath isEqualToString:bundleResourcePath]; + return ![_resourcePath isEqualToString:bundleResourcePath]; } - (void)displayDevIcon { diff --git a/src/app/atom.coffee b/src/app/atom.coffee index b5149622e..6939a071f 100644 --- a/src/app/atom.coffee +++ b/src/app/atom.coffee @@ -10,6 +10,7 @@ originalSendMessageToBrowserProcess = atom.sendMessageToBrowserProcess _.extend atom, exitWhenDone: window.location.params.exitWhenDone + devMode: window.location.params.devMode loadedThemes: [] pendingBrowserProcessCallbacks: {} loadedPackages: [] diff --git a/static/index.html b/static/index.html index 75da64b8a..a78a5a823 100644 --- a/static/index.html +++ b/static/index.html @@ -25,8 +25,9 @@ } document.addEventListener('DOMContentLoaded', function() { - if(window.location.params.devMode == "true") + if (window.location.params.devMode) { document.body.setAttribute('class', 'is-dev-mode') + } }) From 9331b3beed7c387e33d329fe798764f1eaba6624 Mon Sep 17 00:00:00 2001 From: probablycorey Date: Tue, 5 Mar 2013 14:16:30 -0800 Subject: [PATCH 097/308] Add .dev-mode class to root view --- src/app/root-view.coffee | 1 + static/atom.css | 2 +- static/index.html | 6 ------ 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 558143889..4ef9662b8 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -34,6 +34,7 @@ class RootView extends View title: null initialize: -> + @addClass('dev-mode') if atom.devMode @command 'toggle-dev-tools', => atom.toggleDevTools() @on 'focus', (e) => @handleFocus(e) @subscribe $(window), 'focus', (e) => diff --git a/static/atom.css b/static/atom.css index 187f53b74..f029ddc77 100644 --- a/static/atom.css +++ b/static/atom.css @@ -10,7 +10,7 @@ html, body { position: relative; } -.is-dev-mode #root-view:before { +#root-view.dev-mode:before { content: ""; height: 3px; display: block; diff --git a/static/index.html b/static/index.html index a78a5a823..0e27bf115 100644 --- a/static/index.html +++ b/static/index.html @@ -23,12 +23,6 @@ console.error(error.stack || error); } } - - document.addEventListener('DOMContentLoaded', function() { - if (window.location.params.devMode) { - document.body.setAttribute('class', 'is-dev-mode') - } - }) From 1b403d2920e066485124d97e85427efb348cd55d Mon Sep 17 00:00:00 2001 From: probablycorey Date: Tue, 5 Mar 2013 14:20:34 -0800 Subject: [PATCH 098/308] Fix logic mistake --- native/atom_window_controller.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/atom_window_controller.mm b/native/atom_window_controller.mm index 90bd4af8a..1d9acd5d2 100644 --- a/native/atom_window_controller.mm +++ b/native/atom_window_controller.mm @@ -47,7 +47,7 @@ _resourcePath = [[NSBundle bundleForClass:self.class] resourcePath]; } - if (![self isDevMode]) { + if ([self isDevMode]) { [self displayDevIcon]; } From 57b0151cd28cd5f33a987bafded62d5cb9a623be Mon Sep 17 00:00:00 2001 From: probablycorey Date: Tue, 5 Mar 2013 16:08:22 -0800 Subject: [PATCH 099/308] Remove the dev-mode css style, for now :soon: --- src/app/root-view.coffee | 1 - static/atom.css | 8 -------- 2 files changed, 9 deletions(-) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 4ef9662b8..558143889 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -34,7 +34,6 @@ class RootView extends View title: null initialize: -> - @addClass('dev-mode') if atom.devMode @command 'toggle-dev-tools', => atom.toggleDevTools() @on 'focus', (e) => @handleFocus(e) @subscribe $(window), 'focus', (e) => diff --git a/static/atom.css b/static/atom.css index f029ddc77..f16aa95be 100644 --- a/static/atom.css +++ b/static/atom.css @@ -10,14 +10,6 @@ html, body { position: relative; } -#root-view.dev-mode:before { - content: ""; - height: 3px; - display: block; - background-image: -webkit-linear-gradient(#ffc833, #ebac00); - border-bottom: 1px solid #000; -} - #root-view #horizontal { display: -webkit-flex; height: 100%; From b76ab87a96ac3e4ab8a08e3e312f0858306390ee Mon Sep 17 00:00:00 2001 From: Adam Roben Date: Wed, 6 Mar 2013 08:29:55 -0500 Subject: [PATCH 100/308] Use HTTPS for prebuilt-cef Fixes #361. --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 8af55a04d8529b1c0095fb08ce2a3e11a06603bb Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Wed, 6 Mar 2013 14:59:12 -0800 Subject: [PATCH 101/308] Use a single worker for status refreshes There still appear to be crashes occurring when using libgit2 from multiple workers at the same time. So only start a new status worker once the current one completes if a refresh was requested while a worker was running. Closes #367 --- spec/app/git-spec.coffee | 20 ++++++++++++++++++++ src/app/git.coffee | 19 ++++++++++++++++--- src/stdlib/task.coffee | 6 ++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/spec/app/git-spec.coffee b/spec/app/git-spec.coffee index 36ca195e1..04c5596fc 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 @@ -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/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/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 From d509195aab449798219ee8b0f1fabe262a61474c Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 7 Mar 2013 08:34:05 -0800 Subject: [PATCH 102/308] Free keys when config open fails --- native/v8_extensions/git.mm | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/native/v8_extensions/git.mm b/native/v8_extensions/git.mm index 36229345e..f7804d7ee 100644 --- a/native/v8_extensions/git.mm +++ b/native/v8_extensions/git.mm @@ -129,8 +129,11 @@ namespace v8_extensions { 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; From 9fe1be7fe0fc081dcf66757d148cf395b2cde598 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 7 Mar 2013 08:54:29 -0800 Subject: [PATCH 103/308] Add parens around string length --- native/v8_extensions/git.mm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/native/v8_extensions/git.mm b/native/v8_extensions/git.mm index f7804d7ee..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,9 +122,9 @@ 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); From beaeac4425f05533519ac98539441178c3312ed5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 16 Feb 2013 15:00:29 -0700 Subject: [PATCH 104/308] Rename `buildEditSessionForPath` to `buildEditSession` Since this is the more external method, having a shorter name is more convenient. The former `buildEditSession` method took a Buffer, and is now called `buildEditSessionForBuffer`. --- benchmark/benchmark-suite.coffee | 2 +- spec/app/display-buffer-spec.coffee | 4 +- spec/app/edit-session-spec.coffee | 12 ++--- spec/app/editor-spec.coffee | 54 +++++++++---------- spec/app/language-mode-spec.coffee | 10 ++-- spec/app/project-spec.coffee | 18 +++---- spec/app/root-view-spec.coffee | 10 ++-- spec/app/tokenized-buffer-spec.coffee | 12 ++--- src/app/edit-session.coffee | 4 +- src/app/project.coffee | 6 +-- src/app/root-view.coffee | 4 +- .../spec/autocomplete-spec.coffee | 2 +- .../spec/command-interpreter-spec.coffee | 4 +- .../fuzzy-finder/lib/fuzzy-finder-view.coffee | 2 +- .../spec/fuzzy-finder-spec.coffee | 2 +- 15 files changed, 73 insertions(+), 73 deletions(-) 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/spec/app/display-buffer-spec.coffee b/spec/app/display-buffer-spec.coffee index fe5dc83f7..de7404eee 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 = fixturesProject.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 = fixturesProject.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..cbd7a90e9 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -9,7 +9,7 @@ describe "EditSession", -> buffer.setText(buffer.getText().replace(/[ ]{2}/g, "\t")) beforeEach -> - editSession = fixturesProject.buildEditSessionForPath('sample.js', autoIndent: false) + editSession = fixturesProject.buildEditSession('sample.js', autoIndent: false) buffer = editSession.buffer lineLengths = buffer.getLines().map (line) -> line.length @@ -1715,7 +1715,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 = fixturesProject.buildEditSession(null, autoIndent: false) editSession.setSelectedBufferRange([[4, 5], [4, 5]]) editSession.toggleLineCommentsInSelection() expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {" @@ -1793,7 +1793,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 = fixturesProject.buildEditSession(editSession.getPath()) otherEditSession.setSelectedBufferRange([[2, 2], [3, 3]]) otherEditSession.delete() @@ -1986,13 +1986,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 = fixturesProject.buildEditSession('sample.js', softTabs: false) expect(editSession.softTabs).toBeTruthy() - editSession = fixturesProject.buildEditSessionForPath('sample-with-tabs.coffee', softTabs: true) + editSession = fixturesProject.buildEditSession('sample-with-tabs.coffee', softTabs: true) expect(editSession.softTabs).toBeFalsy() - editSession = fixturesProject.buildEditSessionForPath(null, softTabs: false) + editSession = fixturesProject.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 1f37a838e..c8b0572ff 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -14,7 +14,7 @@ describe "Editor", -> getLineHeight = -> return cachedLineHeight if cachedLineHeight? - editorForMeasurement = new Editor(editSession: project.buildEditSessionForPath('sample.js')) + editorForMeasurement = new Editor(editSession: project.buildEditSession('sample.js')) editorForMeasurement.attachToDom() cachedLineHeight = editorForMeasurement.lineHeight editorForMeasurement.remove() @@ -46,7 +46,7 @@ describe "Editor", -> rootView.height(8 * editor.lineHeight) rootView.width(50 * editor.charWidth) - editor.edit(project.buildEditSessionForPath('two-hundred.txt')) + editor.edit(project.buildEditSession('two-hundred.txt')) editor.setCursorScreenPosition([5, 1]) editor.scrollTop(1.5 * editor.lineHeight) editor.scrollView.scrollLeft(44) @@ -75,7 +75,7 @@ describe "Editor", -> 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')) + editor.edit(project.buildEditSession('/tmp/delete-me')) fs.remove('/tmp/delete-me') newEditor = editor.copy() expect(console.warn).toHaveBeenCalled() @@ -117,7 +117,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") @@ -138,7 +138,7 @@ describe "Editor", -> it "removes subscriptions from all edit session buffers", -> editSession1 = editor.activeEditSession subscriberCount1 = editSession1.buffer.subscriptionCount() - editSession2 = project.buildEditSessionForPath(project.resolve('sample.txt')) + editSession2 = project.buildEditSession(project.resolve('sample.txt')) expect(subscriberCount1).toBeGreaterThan 1 editor.edit(editSession2) @@ -151,17 +151,17 @@ describe "Editor", -> describe "when 'close' is triggered", -> it "adds a closed session path to the array", -> - editor.edit(project.buildEditSessionForPath()) + editor.edit(project.buildEditSession()) 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.edit(project.buildEditSession(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()) + editor.edit(project.buildEditSession()) editSession = editor.activeEditSession spyOn(editSession.buffer, 'isModified').andReturn false spyOn(editSession, 'destroy').andCallThrough() @@ -172,7 +172,7 @@ describe "Editor", -> 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()) + editor.edit(project.buildEditSession()) editSession = editor.activeEditSession index = editor.getActiveEditSessionIndex() spyOn(editSession.buffer, 'isModified').andReturn false @@ -220,7 +220,7 @@ describe "Editor", -> otherEditSession = null beforeEach -> - otherEditSession = project.buildEditSessionForPath() + otherEditSession = project.buildEditSession() 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", -> @@ -257,11 +257,11 @@ describe "Editor", -> expect(editor.lineElementForScreenRow(0).text()).toBe 'def' it "removes the opened session from the closed sessions array", -> - editor.edit(project.buildEditSessionForPath('sample.txt')) + editor.edit(project.buildEditSession('sample.txt')) expect(editor.closedEditSessions.length).toBe 0 editor.trigger "core:close" expect(editor.closedEditSessions.length).toBe 1 - editor.edit(project.buildEditSessionForPath('sample.txt')) + editor.edit(project.buildEditSession('sample.txt')) expect(editor.closedEditSessions.length).toBe 0 describe "switching edit sessions", -> @@ -270,10 +270,10 @@ describe "Editor", -> beforeEach -> session0 = editor.activeEditSession - editor.edit(project.buildEditSessionForPath('sample.txt')) + editor.edit(project.buildEditSession('sample.txt')) session1 = editor.activeEditSession - editor.edit(project.buildEditSessionForPath('two-hundred.txt')) + editor.edit(project.buildEditSession('two-hundred.txt')) session2 = editor.activeEditSession describe ".setActiveEditSessionIndex(index)", -> @@ -304,7 +304,7 @@ describe "Editor", -> 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) + editSession = project.buildEditSession(path) editor.edit editSession editSession.insertText("a buffer change") @@ -379,7 +379,7 @@ describe "Editor", -> describe "when the current buffer has no path", -> selectedFilePath = null beforeEach -> - editor.edit(project.buildEditSessionForPath()) + editor.edit(project.buildEditSession()) expect(editor.getPath()).toBeUndefined() editor.getBuffer().setText 'Save me to a new path' @@ -459,7 +459,7 @@ describe "Editor", -> 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.edit project.buildEditSession("sample.txt") editor.splitUp() expect(fakePane.splitUp).toHaveBeenCalled() [newEditor] = fakePane.splitUp.argsForCall[0] @@ -506,7 +506,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 +514,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() @@ -1374,7 +1374,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) @@ -1410,7 +1410,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) @@ -1706,7 +1706,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 @@ -1987,7 +1987,7 @@ 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)) + editor.edit(fixturesProject.buildEditSession(null)) expect(editor.gutter.lineNumbers.find('.line-number').length).toBe 1 editor.setActiveEditSessionIndex(0) @@ -2121,7 +2121,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() @@ -2212,9 +2212,9 @@ describe "Editor", -> 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()) + editor.edit(project.buildEditSession('sample.txt')) + editor.edit(project.buildEditSession('two-hundred.txt')) + editor.edit(project.buildEditSession()) paths = editor.getOpenBufferPaths().map (path) -> project.relativize(path) expect(paths).toEqual = ['sample.js', 'sample.txt', 'two-hundred.txt'] diff --git a/spec/app/language-mode-spec.coffee b/spec/app/language-mode-spec.coffee index a68f778dc..1d53922f2 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 = fixturesProject.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 = fixturesProject.buildEditSession('js', autoIndent: false) expect(jsEditSession.languageMode.grammar.name).toBe "JavaScript" jsEditSession.destroy() describe "javascript", -> beforeEach -> - editSession = fixturesProject.buildEditSessionForPath('sample.js', autoIndent: false) + editSession = fixturesProject.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 = fixturesProject.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 = fixturesProject.buildEditSession('css.css', autoIndent: false) { buffer, languageMode } = editSession describe ".toggleLineCommentsForBufferRows(start, end)", -> diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index 09549eb98..db5eaaf92 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -12,8 +12,8 @@ describe "Project", -> describe "when editSession 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 +24,7 @@ describe "Project", -> anotherEditSession.destroy() expect(project.editSessions.length).toBe 0 - describe ".buildEditSessionForPath(path)", -> + describe ".buildEditSession(path)", -> [absolutePath, newBufferHandler, newEditSessionHandler] = [] beforeEach -> absolutePath = require.resolve('fixtures/dir/a') @@ -35,30 +35,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..62c9dfaea 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -43,10 +43,10 @@ describe "RootView", -> editor2 = editor1.splitRight() editor3 = editor2.splitRight() editor4 = editor2.splitDown() - editor2.edit(project.buildEditSessionForPath('b')) - editor3.edit(project.buildEditSessionForPath('../sample.js')) + editor2.edit(project.buildEditSession('b')) + editor3.edit(project.buildEditSession('../sample.js')) editor3.setCursorScreenPosition([2, 4]) - editor4.edit(project.buildEditSessionForPath('../sample.txt')) + editor4.edit(project.buildEditSession('../sample.txt')) editor4.setCursorScreenPosition([0, 2]) rootView.attachToDom() editor2.focus() @@ -404,7 +404,7 @@ describe "RootView", -> editor2 = rootView.getActiveEditor().splitLeft() path = project.resolve('b') - editor2.edit(project.buildEditSessionForPath(path)) + editor2.edit(project.buildEditSession(path)) expect(pathChangeHandler).toHaveBeenCalled() expect(rootView.getTitle()).toBe "#{fs.base(editor2.getPath())} – #{project.getPath()}" @@ -584,7 +584,7 @@ describe "RootView", -> expect(buffer1.isModified()).toBe(true) editor2 = editor1.splitRight() - editor2.edit(project.buildEditSessionForPath('atom-temp2.txt')) + editor2.edit(project.buildEditSession('atom-temp2.txt')) buffer2 = editor2.activeEditSession.buffer expect(buffer2.getText()).toBe("file2") expect(buffer2.isModified()).toBe(false) diff --git a/spec/app/tokenized-buffer-spec.coffee b/spec/app/tokenized-buffer-spec.coffee index 944628904..52e48cf58 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 = fixturesProject.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 = fixturesProject.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 = fixturesProject.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 = fixturesProject.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 = fixturesProject.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 = fixturesProject.buildEditSession('function.mm', autoIndent: false) buffer = editSession.buffer tokenizedBuffer = editSession.displayBuffer.tokenizedBuffer editSession.setVisible(true) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index b160d5468..4e06fde11 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -16,10 +16,10 @@ class EditSession @deserialize: (state, project) -> 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) 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..d01ac9c03 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -95,7 +95,7 @@ class RootView extends View allowActiveEditorChange = options.allowActiveEditorChange ? false unless editSession = @openInExistingEditor(path, allowActiveEditorChange, changeFocus) - editSession = project.buildEditSessionForPath(path) + editSession = project.buildEditSession(path) editor = new Editor({editSession}) pane = new Pane(editor) @panes.append(pane) @@ -121,7 +121,7 @@ class RootView extends View @makeEditorActive(editor, changeFocus) return editSession - editSession = project.buildEditSessionForPath(path) + editSession = project.buildEditSession(path) activeEditor.edit(editSession) editSession diff --git a/src/packages/autocomplete/spec/autocomplete-spec.coffee b/src/packages/autocomplete/spec/autocomplete-spec.coffee index 8d019d69b..6912fd5b4 100644 --- a/src/packages/autocomplete/spec/autocomplete-spec.coffee +++ b/src/packages/autocomplete/spec/autocomplete-spec.coffee @@ -40,7 +40,7 @@ describe "AutocompleteView", -> beforeEach -> window.rootView = new RootView - editor = new Editor(editSession: fixturesProject.buildEditSessionForPath('sample.js')) + editor = new Editor(editSession: fixturesProject.buildEditSession('sample.js')) window.loadPackage('autocomplete') autocomplete = new AutocompleteView(editor) miniEditor = autocomplete.miniEditor diff --git a/src/packages/command-panel/spec/command-interpreter-spec.coffee b/src/packages/command-panel/spec/command-interpreter-spec.coffee index 289d87709..c9f590346 100644 --- a/src/packages/command-panel/spec/command-interpreter-spec.coffee +++ b/src/packages/command-panel/spec/command-interpreter-spec.coffee @@ -11,7 +11,7 @@ describe "CommandInterpreter", -> beforeEach -> project = new Project(fixturesProject.resolve('dir/')) interpreter = new CommandInterpreter(fixturesProject) - editSession = fixturesProject.buildEditSessionForPath('sample.js') + editSession = fixturesProject.buildEditSession('sample.js') buffer = editSession.buffer afterEach -> @@ -428,7 +428,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/fuzzy-finder/lib/fuzzy-finder-view.coffee b/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee index 3eddc0c8e..480ecab33 100644 --- a/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee +++ b/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee @@ -73,7 +73,7 @@ class FuzzyFinderView extends SelectList editor = rootView.getActiveEditor() if editor - fn(editor, project.buildEditSessionForPath(path)) + fn(editor, project.buildEditSession(path)) else @openPath(path) diff --git a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee index f322d4613..469b0fe96 100644 --- a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee +++ b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee @@ -163,7 +163,7 @@ describe 'FuzzyFinder', -> describe "when the active editor only contains edit sessions for anonymous buffers", -> it "does not open", -> editor = rootView.getActiveEditor() - editor.edit(project.buildEditSessionForPath()) + editor.edit(project.buildEditSession()) editor.loadPreviousEditSession() editor.destroyActiveEditSession() expect(editor.getOpenBufferPaths().length).toBe 0 From 5b58751a149a3bcf8b2375bc22b5cb4456fbc30c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 16 Feb 2013 16:02:43 -0700 Subject: [PATCH 105/308] :lipstick: --- src/app/pane.coffee | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 5b0e630f6..b44e317a5 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -17,11 +17,10 @@ class Pane extends View adjustDimensions: -> # do nothing - horizontalGridUnits: -> - 1 + horizontalGridUnits: -> 1 + + verticalGridUnits: -> 1 - verticalGridUnits: -> - 1 splitUp: (view) -> @split(view, 'column', 'before') From 68b05a5d8df3f79654d3533a455332624fa2c4f7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 16 Feb 2013 16:03:51 -0700 Subject: [PATCH 106/308] Allow for panes to exist without a rootView (for testing purposes) --- src/app/pane.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/pane.coffee b/src/app/pane.coffee index b44e317a5..df096af7b 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -42,7 +42,7 @@ class Pane extends View pane = new Pane(view) this[side](pane) - rootView.adjustPaneDimensions() + rootView?.adjustPaneDimensions() view.focus?() pane @@ -54,7 +54,7 @@ class Pane extends View if parentAxis.children().length == 1 sibling = parentAxis.children().detach() parentAxis.replaceWith(sibling) - rootView.adjustPaneDimensions() + rootView?.adjustPaneDimensions() buildPaneAxis: (axis) -> switch axis From 2bdc077d2a783fc92e66083cd7c89dba85dc12d7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 18 Feb 2013 10:45:02 -0700 Subject: [PATCH 107/308] Construct Pane w/ multiple items. Show first item on construction. --- spec/app/pane-spec.coffee | 17 +++++++++++++++++ src/app/pane.coffee | 11 ++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 spec/app/pane-spec.coffee diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee new file mode 100644 index 000000000..8693d67fc --- /dev/null +++ b/spec/app/pane-spec.coffee @@ -0,0 +1,17 @@ +Editor = require 'editor' +Pane = require 'pane' +{$$} = require 'space-pen' + +describe "Pane", -> + [view1, view2, editSession1, editSession2, pane] = [] + + beforeEach -> + view1 = $$ -> @div id: 'view-1', 'View 1' + view2 = $$ -> @div id: 'view-1', 'View 1' + editSession1 = project.buildEditSession('sample.js') + editSession2 = project.buildEditSession('sample.txt') + pane = new Pane(view1, editSession1, view2, editSession2) + + describe ".initialize(items...)", -> + it "displays the first item in the pane", -> + expect(pane.itemViews.find(view1)).toExist() diff --git a/src/app/pane.coffee b/src/app/pane.coffee index df096af7b..7ed290cd7 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -6,11 +6,20 @@ 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)) + initialize: (@items...) -> + @viewsByItem = new WeakMap + @showItem(@items[0]) + + showItem: (item) -> + @itemViews.children().hide() + @itemViews.append(item) unless @itemViews.children(item).length + item.show() + serialize: -> deserializer: "Pane" wrappedView: @wrappedView?.serialize() From 372393d9ca5fdca1e0d523024c99916f08f0380e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 18 Feb 2013 11:15:33 -0700 Subject: [PATCH 108/308] Allow panes to have model objects as items in addition to views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The problem I've been struggling with is that we need to potentially assign tabs both to EditSessions and also to other views added by extensions, like a markdown preview view. EditSessions are however not actually views… instead they plug into editors. The solution is to have the pane ask a model object what view should be used to render it. When asked to show a non-view item, the pane constructs and appends a view for that item or recycles an appropriate view that it has already appended. --- spec/app/pane-spec.coffee | 31 +++++++++++++++++++++++++++++-- src/app/edit-session.coffee | 3 +++ src/app/editor.coffee | 10 +++++++++- src/app/pane.coffee | 21 ++++++++++++++++++--- 4 files changed, 59 insertions(+), 6 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 8693d67fc..7286be09c 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -7,11 +7,38 @@ describe "Pane", -> beforeEach -> view1 = $$ -> @div id: 'view-1', 'View 1' - view2 = $$ -> @div id: 'view-1', 'View 1' + view2 = $$ -> @div id: 'view-2', 'View 2' editSession1 = project.buildEditSession('sample.js') editSession2 = project.buildEditSession('sample.txt') pane = new Pane(view1, editSession1, view2, editSession2) describe ".initialize(items...)", -> it "displays the first item in the pane", -> - expect(pane.itemViews.find(view1)).toExist() + expect(pane.itemViews.find('#view-1')).toExist() + + describe ".showItem(item)", -> + it "hides all item views except the one being shown", -> + pane.showItem(view2) + expect(view1.css('display')).toBe 'none' + expect(view2.css('display')).toBe '' + + 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.itemViews.find('.editor').view() + 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.itemViews.find('.editor').view() + 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 show it", -> + expect(pane.itemViews.find('#view-2')).not.toExist() + pane.showItem(view2) + expect(pane.itemViews.find('#view-2')).toExist() diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 4e06fde11..eaedcfcdd 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -52,6 +52,9 @@ class EditSession @subscribe @displayBuffer, "changed", (e) => @trigger 'screen-lines-changed', e + getViewClass: -> + require 'editor' + destroy: -> throw new Error("Edit session already destroyed") if @destroyed @destroyed = true diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 5b5bc6502..03f7766ed 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -66,7 +66,12 @@ class Editor extends View editor.isFocused = state.isFocused editor - initialize: ({editSession, @mini, deserializing} = {}) -> + initialize: (editSessionOrOptions) -> + if editSessionOrOptions instanceof EditSession + editSession = editSessionOrOptions + else + {editSession, @mini, deserializing} = (options ? {}) + requireStylesheet 'editor.css' @id = Editor.nextEditorId++ @@ -485,6 +490,9 @@ class Editor extends View index = @pushEditSession(editSession) if index == -1 @setActiveEditSessionIndex(index) + setModel: (editSession) -> + @edit(editSession) + pushEditSession: (editSession) -> index = @editSessions.length @editSessions.push(editSession) diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 7ed290cd7..a6e983936 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -1,4 +1,5 @@ {View} = require 'space-pen' +$ = require 'jquery' PaneRow = require 'pane-row' PaneColumn = require 'pane-column' @@ -12,13 +13,27 @@ class Pane extends View new Pane(deserialize(wrappedView)) initialize: (@items...) -> - @viewsByItem = new WeakMap + @viewsByClassName = {} @showItem(@items[0]) showItem: (item) -> @itemViews.children().hide() - @itemViews.append(item) unless @itemViews.children(item).length - item.show() + view = @viewForItem(item) + unless view.parent().is(@itemViews) + @itemViews.append(view) + view.show() + + viewForItem: (item) -> + if item instanceof $ + item + else + viewClass = item.getViewClass() + if view = @viewsByClassName[viewClass.name] + view.setModel(item) + view + else + @viewsByClassName[viewClass.name] = new viewClass(item) + serialize: -> deserializer: "Pane" From ef0c62f532420ef3b4c8dbaaf5744222bfe24ede Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 18 Feb 2013 11:44:51 -0700 Subject: [PATCH 109/308] Add show next / previous item. --- spec/app/pane-spec.coffee | 16 +++++++++++++++- src/app/pane.coffee | 28 +++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 7286be09c..9edd5216a 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -17,10 +17,12 @@ describe "Pane", -> expect(pane.itemViews.find('#view-1')).toExist() describe ".showItem(item)", -> - it "hides all item views except the one being shown", -> + it "hides all item views except the one being shown and sets the currentItem", -> + expect(pane.currentItem).toBe view1 pane.showItem(view2) expect(view1.css('display')).toBe 'none' expect(view2.css('display')).toBe '' + expect(pane.currentItem).toBe view2 describe "when showing a model item", -> describe "when no view has yet been appended for that item", -> @@ -42,3 +44,15 @@ describe "Pane", -> expect(pane.itemViews.find('#view-2')).not.toExist() pane.showItem(view2) expect(pane.itemViews.find('#view-2')).toExist() + + describe "pane:show-next-item and pane:show-preview-item", -> + it "advances forward/backward through the pane's items, looping around at either end", -> + expect(pane.currentItem).toBe view1 + pane.trigger 'pane:show-previous-item' + expect(pane.currentItem).toBe editSession2 + pane.trigger 'pane:show-previous-item' + expect(pane.currentItem).toBe view2 + pane.trigger 'pane:show-next-item' + expect(pane.currentItem).toBe editSession2 + pane.trigger 'pane:show-next-item' + expect(pane.currentItem).toBe view1 diff --git a/src/app/pane.coffee b/src/app/pane.coffee index a6e983936..b5b6cccaa 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -12,15 +12,42 @@ class Pane extends View @deserialize: ({wrappedView}) -> new Pane(deserialize(wrappedView)) + currentItem: null + items: null + initialize: (@items...) -> @viewsByClassName = {} @showItem(@items[0]) + @command 'pane:show-next-item', @showNextItem + @command 'pane:show-previous-item', @showPreviousItem + + showNextItem: => + index = @getCurrentItemIndex() + if index < @items.length - 1 + @showItemAtIndex(index + 1) + else + @showItemAtIndex(0) + + showPreviousItem: => + index = @getCurrentItemIndex() + if index > 0 + @showItemAtIndex(index - 1) + else + @showItemAtIndex(@items.length - 1) + + getCurrentItemIndex: -> + @items.indexOf(@currentItem) + + showItemAtIndex: (index) -> + @showItem(@items[index]) + showItem: (item) -> @itemViews.children().hide() view = @viewForItem(item) unless view.parent().is(@itemViews) @itemViews.append(view) + @currentItem = item view.show() viewForItem: (item) -> @@ -34,7 +61,6 @@ class Pane extends View else @viewsByClassName[viewClass.name] = new viewClass(item) - serialize: -> deserializer: "Pane" wrappedView: @wrappedView?.serialize() From 41f18ee6a2754009057c12372ea775cdc5d3b801 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 18 Feb 2013 15:23:23 -0700 Subject: [PATCH 110/308] Add `Pane.removeItem` --- spec/app/pane-spec.coffee | 25 +++++++++++++++++++++++++ src/app/pane.coffee | 23 +++++++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 9edd5216a..6d090371b 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -45,6 +45,31 @@ describe "Pane", -> pane.showItem(view2) expect(pane.itemViews.find('#view-2')).toExist() + 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.currentItem).toBe editSession1 + + pane.showItem(editSession2) + pane.removeItem(editSession2) + expect(pane.getItems()).toEqual [editSession1, view2] + expect(pane.currentItem).toBe editSession1 + + 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 "pane:show-next-item and pane:show-preview-item", -> it "advances forward/backward through the pane's items, looping around at either end", -> expect(pane.currentItem).toBe view1 diff --git a/src/app/pane.coffee b/src/app/pane.coffee index b5b6cccaa..f94779bd5 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -1,5 +1,6 @@ {View} = require 'space-pen' $ = require 'jquery' +_ = require 'underscore' PaneRow = require 'pane-row' PaneColumn = require 'pane-column' @@ -22,6 +23,9 @@ class Pane extends View @command 'pane:show-next-item', @showNextItem @command 'pane:show-previous-item', @showPreviousItem + getItems: -> + new Array(@items...) + showNextItem: => index = @getCurrentItemIndex() if index < @items.length - 1 @@ -50,6 +54,21 @@ class Pane extends View @currentItem = item view.show() + removeItem: (item) -> + @showNextItem() if item is @currentItem and @items.length > 1 + _.remove(@items, item) + @cleanupItemView(item) + + cleanupItemView: (item) -> + if item instanceof $ + item.remove() + else + viewClass = item.getViewClass() + otherItemsForView = @items.filter (i) -> i.getViewClass?() is viewClass + unless otherItemsForView.length + @viewsByClassName[viewClass.name].remove() + delete @viewsByClassName[viewClass.name] + viewForItem: (item) -> if item instanceof $ item @@ -57,9 +76,9 @@ class Pane extends View viewClass = item.getViewClass() if view = @viewsByClassName[viewClass.name] view.setModel(item) - view else - @viewsByClassName[viewClass.name] = new viewClass(item) + view = @viewsByClassName[viewClass.name] = new viewClass(item) + view serialize: -> deserializer: "Pane" From d89a7eb52241f56bad1886d6581feaba9c7b57b9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 18 Feb 2013 15:38:43 -0700 Subject: [PATCH 111/308] When showing an item on a pane, add it to the items list if needed --- spec/app/pane-spec.coffee | 10 ++++++++++ src/app/pane.coffee | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 6d090371b..3921542ef 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -24,6 +24,16 @@ describe "Pane", -> expect(view2.css('display')).toBe '' expect(pane.currentItem).toBe view2 + describe "when the given item isn't yet in the items list on the pane", -> + it "adds it to the items list after the current item", -> + view3 = $$ -> @div id: 'view-3', "View 3" + pane.showItem(editSession1) + expect(pane.getCurrentItemIndex()).toBe 1 + pane.showItem(view3) + expect(pane.getItems()).toEqual [view1, editSession1, view3, view2, editSession2] + expect(pane.currentItem).toBe view3 + expect(pane.getCurrentItemIndex()).toBe 2 + 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", -> diff --git a/src/app/pane.coffee b/src/app/pane.coffee index f94779bd5..2d121f7ff 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -47,6 +47,7 @@ class Pane extends View @showItem(@items[index]) showItem: (item) -> + @addItem(item) @itemViews.children().hide() view = @viewForItem(item) unless view.parent().is(@itemViews) @@ -54,6 +55,11 @@ class Pane extends View @currentItem = item view.show() + addItem: (item) -> + return if _.include(@items, item) + @items.splice(@getCurrentItemIndex() + 1, 0, item) + item + removeItem: (item) -> @showNextItem() if item is @currentItem and @items.length > 1 _.remove(@items, item) From 77bf3e4d7445f93f75211379cf2768a270aed93a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 18 Feb 2013 16:12:06 -0700 Subject: [PATCH 112/308] Get root view pane specs passing --- spec/app/root-view-spec.coffee | 369 +++++++++++++++++---------------- src/app/pane.coffee | 6 +- src/app/root-view.coffee | 44 ++-- 3 files changed, 203 insertions(+), 216 deletions(-) diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index 62c9dfaea..43bd19a9f 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -141,199 +141,18 @@ describe "RootView", -> it "surrenders focus to the body", -> expect(document.activeElement).toBe $('body')[0] - describe "panes", -> + fdescribe "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 + view1 = pane1.find('.editor').view() view2 = new DummyView(2) view3 = new DummyView(3) pane2 = pane1.splitDown(view2) @@ -352,6 +171,190 @@ describe "RootView", -> rootView.focusNextPane() expect(view1.focus).toHaveBeenCalled() + describe "pane layout", -> + beforeEach -> + rootView.attachToDom() + rootView.width(800) + rootView.height(600) + 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).view() + expect(rightPane[0]).toBe pane2[0] + expect(leftPane.attr('id')).toBe 'pane-1' + expect(rightPane.currentItem).toBe newPaneContent + + 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).view() + expect(leftPane[0]).toBe pane2[0] + expect(rightPane.attr('id')).toBe 'pane-1' + expect(leftPane.currentItem).toBe + + 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).view() + expect(topPane[0]).toBe pane2[0] + expect(bottomPane.attr('id')).toBe 'pane-1' + expect(topPane.currentItem).toBe newPaneContent + + 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).view() + expect(bottomPane[0]).toBe pane2[0] + expect(topPane.attr('id')).toBe 'pane-1' + expect(bottomPane.currentItem).toBe newPaneContent + + 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.showItem($("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 "keymap wiring", -> commandHandler = null beforeEach -> diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 2d121f7ff..dd9a4ac10 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -22,6 +22,7 @@ class Pane extends View @command 'pane:show-next-item', @showNextItem @command 'pane:show-previous-item', @showPreviousItem + @on 'focus', => @viewForCurrentItem().focus() getItems: -> new Array(@items...) @@ -86,6 +87,9 @@ class Pane extends View view = @viewsByClassName[viewClass.name] = new viewClass(item) view + viewForCurrentItem: -> + @viewForItem(@currentItem) + serialize: -> deserializer: "Pane" wrappedView: @wrappedView?.serialize() @@ -118,7 +122,7 @@ class Pane extends View pane = new Pane(view) this[side](pane) rootView?.adjustPaneDimensions() - view.focus?() + pane.focus() pane remove: (selector, keepData) -> diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index d01ac9c03..5449f4925 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -91,39 +91,16 @@ class RootView extends View @remove() open: (path, options = {}) -> - changeFocus = options.changeFocus ? true - allowActiveEditorChange = options.allowActiveEditorChange ? false - - unless editSession = @openInExistingEditor(path, allowActiveEditorChange, changeFocus) - editSession = project.buildEditSession(path) - editor = new Editor({editSession}) - pane = new Pane(editor) - @panes.append(pane) - if changeFocus - editor.focus() + if activePane = @getActivePane() + if existingItem = activePane.itemForPath(path) + activePane.showItem(existingItem) else - @makeEditorActive(editor, changeFocus) + activePane.showItem(project.buildEditSession(path)) + else + activePane = new Pane(project.buildEditSession(path)) + @panes.append(activePane) - 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.buildEditSession(path) - activeEditor.edit(editSession) - editSession + activePane.focus() if options.changeFocus editorFocused: (editor) -> @makeEditorActive(editor) if @panes.containsElement(editor) @@ -177,6 +154,9 @@ class RootView extends View getOpenBufferPaths: -> _.uniq(_.flatten(@getEditors().map (editor) -> editor.getOpenBufferPaths())) + getActivePane: -> + @panes.find('.pane.active').view() ? @panes.find('.pane:first').view() + getActiveEditor: -> if (editor = @panes.find('.editor.active')).length editor.view() @@ -190,7 +170,7 @@ class RootView extends View panes = @panes.find('.pane') currentIndex = panes.toArray().indexOf(@getFocusedPane()[0]) nextIndex = (currentIndex + 1) % panes.length - panes.eq(nextIndex).view().wrappedView.focus() + panes.eq(nextIndex).view().focus() getFocusedPane: -> @panes.find('.pane:has(:focus)') From 62729c42eeb81e021a4b3145754d75366477d2cc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 18 Feb 2013 16:22:09 -0700 Subject: [PATCH 113/308] Panes destroy their items when they are removed As a consequence of these changes, editors will no longer need to listen for destruction of their edit sessions. An editor will eventually only ever be displaying a single edit session, and the editor will destroy that edit session when it is removed. Panes will be responsible for supporting multiple edit sessions, and they will automatically remove the editor when they have no more edit session items. --- spec/app/pane-spec.coffee | 10 ++++++++++ src/app/editor.coffee | 2 +- src/app/pane.coffee | 4 ++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 3921542ef..e4a551906 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -80,6 +80,10 @@ describe "Pane", -> pane.removeItem(editSession1) expect(pane.itemViews.find('.editor')).not.toExist() + it "calls destroy on the model", -> + pane.removeItem(editSession2) + expect(editSession2.destroyed).toBeTruthy() + describe "pane:show-next-item and pane:show-preview-item", -> it "advances forward/backward through the pane's items, looping around at either end", -> expect(pane.currentItem).toBe view1 @@ -91,3 +95,9 @@ describe "Pane", -> expect(pane.currentItem).toBe editSession2 pane.trigger 'pane:show-next-item' expect(pane.currentItem).toBe view1 + + describe ".remove()", -> + it "destroys all the pane's items", -> + pane.remove() + expect(editSession1.destroyed).toBeTruthy() + expect(editSession2.destroyed).toBeTruthy() diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 03f7766ed..dc4bddd85 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -498,7 +498,7 @@ class Editor extends View @editSessions.push(editSession) @closedEditSessions = @closedEditSessions.filter ({path})-> path isnt editSession.getPath() - editSession.on 'destroyed', => @editSessionDestroyed(editSession) +# editSession.on 'destroyed', => @editSessionDestroyed(editSession) @trigger 'editor:edit-session-added', [editSession, index] index diff --git a/src/app/pane.coffee b/src/app/pane.coffee index dd9a4ac10..774654b76 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -64,6 +64,7 @@ class Pane extends View removeItem: (item) -> @showNextItem() if item is @currentItem and @items.length > 1 _.remove(@items, item) + item.destroy?() @cleanupItemView(item) cleanupItemView: (item) -> @@ -135,6 +136,9 @@ class Pane extends View parentAxis.replaceWith(sibling) rootView?.adjustPaneDimensions() + afterRemove: -> + item.destroy?() for item in @getItems() + buildPaneAxis: (axis) -> switch axis when 'row' then new PaneRow From c6729e9df1709229a92c4cbad54b5485db1111ad Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 18 Feb 2013 16:22:44 -0700 Subject: [PATCH 114/308] Ignore redundant destructions of EditSessions --- src/app/edit-session.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index eaedcfcdd..e0c771135 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -56,7 +56,7 @@ class EditSession require 'editor' destroy: -> - throw new Error("Edit session already destroyed") if @destroyed + return if @destroyed @destroyed = true @unsubscribe() @buffer.release() From 829bfa0a102fa96389c751e2ba83ba0047791d5b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 18 Feb 2013 16:37:13 -0700 Subject: [PATCH 115/308] Add `Pane.itemForPath` --- spec/app/pane-spec.coffee | 5 +++++ src/app/pane.coffee | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index e4a551906..e928bdd45 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -101,3 +101,8 @@ describe "Pane", -> pane.remove() expect(editSession1.destroyed).toBeTruthy() expect(editSession2.destroyed).toBeTruthy() + + describe ".itemForPath(path)", -> + it "returns the item for which a call to .getPath() returns the given path", -> + expect(pane.itemForPath(editSession1.getPath())).toBe editSession1 + expect(pane.itemForPath(editSession2.getPath())).toBe editSession2 \ No newline at end of file diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 774654b76..006fd2d2a 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -67,6 +67,9 @@ class Pane extends View item.destroy?() @cleanupItemView(item) + itemForPath: (path) -> + _.detect @items, (item) -> item.getPath?() is path + cleanupItemView: (item) -> if item instanceof $ item.remove() @@ -101,7 +104,6 @@ class Pane extends View verticalGridUnits: -> 1 - splitUp: (view) -> @split(view, 'column', 'before') From bee1efed5c8cc7fae2402c60c4309cea1ce3d5f8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 18 Feb 2013 17:28:29 -0700 Subject: [PATCH 116/308] Make `RootView.open` work with new Pane behavior Still a bit of a WIP. Panes don't yet take the "active" class correctly when focused. --- spec/app/root-view-spec.coffee | 105 +++++++++------------------------ src/app/root-view.coffee | 11 ++-- 2 files changed, 36 insertions(+), 80 deletions(-) diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index 43bd19a9f..c13a8aa47 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -474,101 +474,54 @@ describe "RootView", -> rootView.trigger 'window:decrease-font-size' expect(editor.getFontSize()).toBe 1 - describe ".open(path, options)", -> - describe "when there is no active editor", -> + fdescribe ".open(path, options)", -> + describe "when there is no active pane", -> beforeEach -> - rootView.getActiveEditor().destroyActiveEditSession() - expect(rootView.getActiveEditor()).toBeUndefined() + 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", -> editSession = rootView.open() - expect(rootView.getActiveEditor()).toBeDefined() - expect(rootView.getActiveEditor().getPath()).toBeUndefined() - expect(editSession).toBe rootView.getActiveEditor().activeEditSession + expect(rootView.getActivePane().currentItem).toBe editSession + expect(editSession.getPath()).toBeUndefined() describe "when called with a path", -> it "opens a buffer with the given path in a new editor", -> 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().currentItem).toBe editSession + expect(editSession.getPath()).toBe require.resolve('fixtures/dir/b') - describe "when there is an active editor", -> + describe "when there is an active pane", -> + [activePane, initialItemCount] = [] beforeEach -> - expect(rootView.getActiveEditor()).toBeDefined() + activePane = rootView.getActivePane() + 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 in the active pane", -> editSession = rootView.open() - expect(rootView.getActiveEditor().getPath()).toBeUndefined() - expect(editSession).toBe rootView.getActiveEditor().activeEditSession + expect(activePane.getItems().length).toBe initialItemCount + 1 + expect(activePane.currentItem).toBe editSession + expect(editSession.getPath()).toBeUndefined() 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.currentItem - describe "when allowActiveEditorChange is false (the default)", -> - activeEditor = null - beforeEach -> - activeEditor = rootView.getActiveEditor() + editSession = rootView.open('b') + expect(activePane.currentItem).toBe editSession - 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('a') + expect(editSession).not.toBe previousEditSession + expect(activePane.currentItem).toBe editSession - editSession = rootView.open('b') - expect(activeEditor.activeEditSession).not.toBe previousEditSession - expect(editSession).toBe rootView.getActiveEditor().activeEditSession - - editSession = rootView.open('a') - expect(activeEditor.activeEditSession).toBe previousEditSession - expect(editSession).toBe previousEditSession - - 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 "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.currentItem).toBe editSession describe ".saveAll()", -> it "saves all open editors", -> diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 5449f4925..a1914bdfe 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -92,15 +92,18 @@ class RootView extends View open: (path, options = {}) -> if activePane = @getActivePane() - if existingItem = activePane.itemForPath(path) - activePane.showItem(existingItem) + if editSession = activePane.itemForPath(path) + activePane.showItem(editSession) else - activePane.showItem(project.buildEditSession(path)) + editSession = project.buildEditSession(path) + activePane.showItem(editSession) else - activePane = new Pane(project.buildEditSession(path)) + editSession = project.buildEditSession(path) + activePane = new Pane(editSession) @panes.append(activePane) activePane.focus() if options.changeFocus + editSession editorFocused: (editor) -> @makeEditorActive(editor) if @panes.containsElement(editor) From 568fcf441e9a3023a97efbe0c632eb695760e2ec Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 20 Feb 2013 16:01:08 -0700 Subject: [PATCH 117/308] Pane serializes its serializable items --- spec/app/pane-spec.coffee | 7 ++++++- src/app/pane.coffee | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index e928bdd45..2ada044da 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -105,4 +105,9 @@ describe "Pane", -> describe ".itemForPath(path)", -> it "returns the item for which a call to .getPath() returns the given path", -> expect(pane.itemForPath(editSession1.getPath())).toBe editSession1 - expect(pane.itemForPath(editSession2.getPath())).toBe editSession2 \ No newline at end of file + expect(pane.itemForPath(editSession2.getPath())).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] diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 006fd2d2a..64c186093 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -10,8 +10,8 @@ class Pane extends View @div class: 'pane', => @div class: 'item-views', outlet: 'itemViews' - @deserialize: ({wrappedView}) -> - new Pane(deserialize(wrappedView)) + @deserialize: ({items}) -> + new Pane(items.map((item) -> deserialize(item))...) currentItem: null items: null @@ -96,7 +96,7 @@ class Pane extends View serialize: -> deserializer: "Pane" - wrappedView: @wrappedView?.serialize() + items: _.compact(@getItems().map (item) -> item.serialize?()) adjustDimensions: -> # do nothing From 281a28bb0e451ad73d35da1b6aa584dd8595ba2d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 20 Feb 2013 16:42:16 -0700 Subject: [PATCH 118/308] Add spec for pane focusing the its current item view when it's focused --- spec/app/pane-spec.coffee | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 2ada044da..d856b14df 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -102,6 +102,13 @@ describe "Pane", -> expect(editSession1.destroyed).toBeTruthy() expect(editSession2.destroyed).toBeTruthy() + describe ".focus()", -> + it "focuses the current item", -> + focusHandler = jasmine.createSpy("focusHandler") + pane.currentItem.on 'focus', focusHandler + pane.focus() + expect(focusHandler).toHaveBeenCalled() + describe ".itemForPath(path)", -> it "returns the item for which a call to .getPath() returns the given path", -> expect(pane.itemForPath(editSession1.getPath())).toBe editSession1 From 45eec6a8ff0bed5ab49b7ff67f71dd9b9a5c9795 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 20 Feb 2013 16:43:29 -0700 Subject: [PATCH 119/308] Get more specs passing. Failing specs due to missing features. --- spec/app/editor-spec.coffee | 213 +-------------------------------- spec/app/pane-spec.coffee | 20 ++++ spec/app/root-view-spec.coffee | 10 +- src/app/edit-session.coffee | 2 +- src/app/editor.coffee | 14 +-- src/app/root-view.coffee | 3 +- static/atom.css | 6 + 7 files changed, 47 insertions(+), 221 deletions(-) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index c8b0572ff..1aa43f983 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -149,73 +149,6 @@ describe "Editor", -> 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.buildEditSession()) - editSession = editor.activeEditSession - expect(editor.closedEditSessions.length).toBe 0 - editor.trigger "core:close" - expect(editor.closedEditSessions.length).toBe 0 - editor.edit(project.buildEditSession(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.buildEditSession()) - 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.buildEditSession()) - 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() - describe ".edit(editSession)", -> otherEditSession = null @@ -469,10 +402,10 @@ describe "Editor", -> 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() + editor.splitUp() + editor.splitDown() + editor.splitLeft() + editor.splitRight() describe "editor:attached event", -> it 'only triggers an editor:attached event when it is first added to the DOM', -> @@ -545,10 +478,8 @@ describe "Editor", -> 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' @@ -602,6 +533,7 @@ describe "Editor", -> expect(editor.lineHeight).toBeGreaterThan lineHeightBefore expect(editor.charWidth).toBeGreaterThan charWidthBefore expect(editor.getCursorView().position()).toEqual { top: 5 * editor.lineHeight, left: 6 * editor.charWidth } + expect(editor.activeEditSession.buffer).toBe buffer expect(editor.renderedLines.outerHeight()).toBe buffer.getLineCount() * editor.lineHeight expect(editor.verticalScrollbarContent.height()).toBe buffer.getLineCount() * editor.lineHeight @@ -1418,6 +1350,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 -> @@ -2371,42 +2304,6 @@ describe "Editor", -> 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] = [] @@ -2769,104 +2666,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/pane-spec.coffee b/spec/app/pane-spec.coffee index d856b14df..17073177e 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -118,3 +118,23 @@ describe "Pane", -> it "can serialize and deserialize the pane and all its serializable items", -> newPane = deserialize(pane.serialize()) expect(newPane.getItems()).toEqual [editSession1, editSession2] + +# This relates to confirming the closing of a tab +# +# 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() diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index c13a8aa47..5be73e8e9 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -10,7 +10,6 @@ describe "RootView", -> pathToOpen = null beforeEach -> - project.destroy() project.setPath(project.resolve('dir')) pathToOpen = project.resolve('a') window.rootView = new RootView @@ -18,7 +17,7 @@ describe "RootView", -> rootView.open(pathToOpen) rootView.focus() - describe "@deserialize()", -> + xdescribe "@deserialize()", -> viewState = null describe "when the serialized RootView has an unsaved buffer", -> @@ -27,10 +26,11 @@ describe "RootView", -> editor1 = rootView.getActiveEditor() buffer = editor1.getBuffer() editor1.splitRight() + viewState = rootView.serialize() rootView.deactivate() - window.rootView = RootView.deserialize(viewState) + rootView.focus() expect(rootView.getEditors().length).toBe 2 expect(rootView.getActiveEditor().getText()).toBe buffer.getText() @@ -141,7 +141,7 @@ describe "RootView", -> it "surrenders focus to the body", -> expect(document.activeElement).toBe $('body')[0] - fdescribe "panes", -> + describe "panes", -> [pane1, newPaneContent] = [] beforeEach -> @@ -474,7 +474,7 @@ describe "RootView", -> rootView.trigger 'window:decrease-font-size' expect(editor.getFontSize()).toBe 1 - fdescribe ".open(path, options)", -> + describe ".open(path, options)", -> describe "when there is no active pane", -> beforeEach -> rootView.getActivePane().remove() diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index e0c771135..14a3b11e2 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -14,7 +14,7 @@ module.exports = class EditSession registerDeserializer(this) - @deserialize: (state, project) -> + @deserialize: (state) -> if fs.exists(state.buffer) session = project.buildEditSession(state.buffer) else diff --git a/src/app/editor.coffee b/src/app/editor.coffee index dc4bddd85..9187e8d8f 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -70,7 +70,7 @@ class Editor extends View if editSessionOrOptions instanceof EditSession editSession = editSessionOrOptions else - {editSession, @mini, deserializing} = (options ? {}) + {editSession, @mini, deserializing} = (editSessionOrOptions ? {}) requireStylesheet 'editor.css' @@ -793,19 +793,19 @@ class Editor extends View new Editor { editSession: editSession ? @activeEditSession.copy() } splitLeft: (editSession) -> - @pane()?.splitLeft(@newSplitEditor(editSession)).wrappedView + @pane()?.splitLeft(@newSplitEditor(editSession)).currentItem splitRight: (editSession) -> - @pane()?.splitRight(@newSplitEditor(editSession)).wrappedView + @pane()?.splitRight(@newSplitEditor(editSession)).currentItem splitUp: (editSession) -> - @pane()?.splitUp(@newSplitEditor(editSession)).wrappedView + @pane()?.splitUp(@newSplitEditor(editSession)).currentItem splitDown: (editSession) -> - @pane()?.splitDown(@newSplitEditor(editSession)).wrappedView + @pane()?.splitDown(@newSplitEditor(editSession)).currentItem pane: -> - @parent('.pane').view() + @closest('.pane').view() promptToSaveDirtySession: (session, callback) -> path = session.getPath() @@ -821,7 +821,7 @@ class Editor extends 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: -> diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index a1914bdfe..7f8a72858 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -120,6 +120,7 @@ class RootView extends View 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() @@ -144,7 +145,7 @@ class RootView extends View document.title = @title getEditors: -> - @panes.find('.pane > .editor').map(-> $(this).view()).toArray() + @panes.find('.pane > .item-views > .editor').map(-> $(this).view()).toArray() getModifiedBuffers: -> modifiedBuffers = [] diff --git a/static/atom.css b/static/atom.css index f16aa95be..003390b38 100644 --- a/static/atom.css +++ b/static/atom.css @@ -55,6 +55,12 @@ html, body { box-sizing: border-box; } +#root-view #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"); From 0c2a5f273c48c3efef630773f4e510d39abbe51e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 20 Feb 2013 20:53:34 -0700 Subject: [PATCH 120/308] Enhance pane split methods. Spec them in pane-spec. When a pane is split, it attempts to make a copy of its current item if no items are passed to the split method. When splitting, multiple items can also be passed to the constructor of the new pane. --- spec/app/pane-spec.coffee | 59 ++++++++++++++++++++++++++++++++++++++- src/app/pane.coffee | 24 +++++++++------- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 17073177e..f00bcfd2d 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -3,14 +3,16 @@ Pane = require 'pane' {$$} = require 'space-pen' describe "Pane", -> - [view1, view2, editSession1, editSession2, pane] = [] + [container, view1, view2, editSession1, editSession2, pane] = [] beforeEach -> + container = $$ -> @div id: 'panes' view1 = $$ -> @div id: 'view-1', 'View 1' view2 = $$ -> @div id: 'view-2', '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", -> @@ -109,6 +111,61 @@ describe "Pane", -> pane.focus() expect(focusHandler).toHaveBeenCalled() + describe "split methods", -> + [view3, view4] = [] + beforeEach -> + 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 current item if none are given + pane2 = pane.splitRight() + expect(container.find('.row .pane').toArray()).toEqual [pane[0], pane2[0]] + expect(pane2.items).toEqual [editSession1] + expect(pane2.currentItem).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 current 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.currentItem).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 current 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.currentItem).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 current 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.currentItem).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]] + describe ".itemForPath(path)", -> it "returns the item for which a call to .getPath() returns the given path", -> expect(pane.itemForPath(editSession1.getPath())).toBe editSession1 diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 64c186093..b2c1ca5e1 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -104,30 +104,34 @@ class Pane extends View 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 = [@copyCurrentItem()] unless items.length + pane = new Pane(items...) this[side](pane) rootView?.adjustPaneDimensions() pane.focus() pane + copyCurrentItem: -> + deserialize(@currentItem.serialize()) + remove: (selector, keepData) -> return super if keepData # find parent elements before removing from dom From 19e2cab920ba868f5d377e6879813a08d07c2209 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 20 Feb 2013 20:55:38 -0700 Subject: [PATCH 121/308] :lipstick: --- spec/app/pane-spec.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index f00bcfd2d..bdcc1d246 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -86,7 +86,7 @@ describe "Pane", -> pane.removeItem(editSession2) expect(editSession2.destroyed).toBeTruthy() - describe "pane:show-next-item and pane:show-preview-item", -> + 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.currentItem).toBe view1 pane.trigger 'pane:show-previous-item' @@ -104,8 +104,8 @@ describe "Pane", -> expect(editSession1.destroyed).toBeTruthy() expect(editSession2.destroyed).toBeTruthy() - describe ".focus()", -> - it "focuses the current item", -> + describe "when the pane is focused", -> + it "focuses the current item view", -> focusHandler = jasmine.createSpy("focusHandler") pane.currentItem.on 'focus', focusHandler pane.focus() From 9ecb03e470e41c9439e936a2a4dfa3aca214d2ae Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 20 Feb 2013 20:59:25 -0700 Subject: [PATCH 122/308] Rename PaneGrid to PaneAxis PaneGrid is a superclass of PaneRow and PaneColumn. These are both a type of axis for the pane layout system. --- spec/app/pane-container-spec.coffee | 0 src/app/{pane-grid.coffee => pane-axis.coffee} | 2 +- src/app/pane-column.coffee | 4 ++-- src/app/pane-row.coffee | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 spec/app/pane-container-spec.coffee rename src/app/{pane-grid.coffee => pane-axis.coffee} (95%) diff --git a/spec/app/pane-container-spec.coffee b/spec/app/pane-container-spec.coffee new file mode 100644 index 000000000..e69de29bb 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-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' From fee835f8995236112f5d64c3f2c86aea4c08d0f0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 20 Feb 2013 22:28:43 -0700 Subject: [PATCH 123/308] Add a PaneContainer subview for RootView PaneContainer is responsible for all pane-related logic. Laying them out, switching focus between them, etc. This should help make RootView simpler and keep pane-layout related tests in their own focused area. --- spec/app/pane-container-spec.coffee | 47 +++++++ spec/app/pane-spec.coffee | 83 ++++++++++++- spec/app/root-view-spec.coffee | 184 ---------------------------- src/app/pane-container.coffee | 41 +++++++ src/app/pane.coffee | 18 +-- src/app/root-view.coffee | 32 ++--- src/app/window.coffee | 3 + static/atom.css | 10 +- 8 files changed, 192 insertions(+), 226 deletions(-) create mode 100644 src/app/pane-container.coffee diff --git a/spec/app/pane-container-spec.coffee b/spec/app/pane-container-spec.coffee index e69de29bb..8d2bf21e1 100644 --- a/spec/app/pane-container-spec.coffee +++ b/spec/app/pane-container-spec.coffee @@ -0,0 +1,47 @@ +PaneContainer = require 'pane-container' +Pane = require 'pane' +{View} = require 'space-pen' +$ = require 'jquery' + +describe "PaneContainer", -> + [TestView, container, pane1, pane2, pane3] = [] + + beforeEach -> + class TestView extends View + registerDeserializer(this) + @deserialize: ({myText}) -> new TestView(myText) + @content: -> @div tabindex: -1 + initialize: (@myText) -> @text(@myText) + serialize: -> deserializer: 'TestView', myText: @myText + + 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.currentItem).toMatchSelector ':focus' + container.focusNextPane() + expect(pane2.currentItem).toMatchSelector ':focus' + container.focusNextPane() + expect(pane3.currentItem).toMatchSelector ':focus' + container.focusNextPane() + expect(pane1.currentItem).toMatchSelector ':focus' + + 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 index bdcc1d246..a856fcc82 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -1,12 +1,13 @@ -Editor = require 'editor' +PaneContainer = require 'pane-container' Pane = require 'pane' {$$} = require 'space-pen' +$ = require 'jquery' describe "Pane", -> [container, view1, view2, editSession1, editSession2, pane] = [] beforeEach -> - container = $$ -> @div id: 'panes' + container = new PaneContainer view1 = $$ -> @div id: 'view-1', 'View 1' view2 = $$ -> @div id: 'view-2', 'View 2' editSession1 = project.buildEditSession('sample.js') @@ -52,7 +53,7 @@ describe "Pane", -> 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 show it", -> + 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() @@ -112,8 +113,9 @@ describe "Pane", -> expect(focusHandler).toHaveBeenCalled() describe "split methods", -> - [view3, view4] = [] + [pane1, view3, view4] = [] beforeEach -> + pane1 = pane pane.showItem(editSession1) view3 = $$ -> @div id: 'view-3', 'View 3' view4 = $$ -> @div id: 'view-4', 'View 4' @@ -121,8 +123,8 @@ describe "Pane", -> 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 current item if none are given - pane2 = pane.splitRight() - expect(container.find('.row .pane').toArray()).toEqual [pane[0], pane2[0]] + pane2 = pane1.splitRight() + expect(container.find('.row .pane').toArray()).toEqual [pane1[0], pane2[0]] expect(pane2.items).toEqual [editSession1] expect(pane2.currentItem).not.toBe editSession1 # it's a copy @@ -166,6 +168,75 @@ describe "Pane", -> 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 ".itemForPath(path)", -> it "returns the item for which a call to .getPath() returns the given path", -> expect(pane.itemForPath(editSession1.getPath())).toBe editSession1 diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index 5be73e8e9..8b1d6f691 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -171,190 +171,6 @@ describe "RootView", -> rootView.focusNextPane() expect(view1.focus).toHaveBeenCalled() - describe "pane layout", -> - beforeEach -> - rootView.attachToDom() - rootView.width(800) - rootView.height(600) - 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).view() - expect(rightPane[0]).toBe pane2[0] - expect(leftPane.attr('id')).toBe 'pane-1' - expect(rightPane.currentItem).toBe newPaneContent - - 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).view() - expect(leftPane[0]).toBe pane2[0] - expect(rightPane.attr('id')).toBe 'pane-1' - expect(leftPane.currentItem).toBe - - 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).view() - expect(topPane[0]).toBe pane2[0] - expect(bottomPane.attr('id')).toBe 'pane-1' - expect(topPane.currentItem).toBe newPaneContent - - 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).view() - expect(bottomPane[0]).toBe pane2[0] - expect(topPane.attr('id')).toBe 'pane-1' - expect(bottomPane.currentItem).toBe newPaneContent - - 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.showItem($("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 "keymap wiring", -> commandHandler = null beforeEach -> diff --git a/src/app/pane-container.coffee b/src/app/pane-container.coffee new file mode 100644 index 000000000..87bc09f98 --- /dev/null +++ b/src/app/pane-container.coffee @@ -0,0 +1,41 @@ +{View} = require 'space-pen' +$ = 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' + + serialize: -> + deserializer: 'PaneContainer' + root: @getRoot()?.serialize() + + focusNextPane: -> + panes = @getPanes() + currentIndex = panes.indexOf(@getFocusedPane()) + nextIndex = (currentIndex + 1) % panes.length + panes[nextIndex].focus() + + getRoot: -> + @children().first().view() + + getPanes: -> + @find('.pane').toArray().map (node)-> $(node).view() + + getFocusedPane: -> + @find('.pane:has(:focus)').view() + + adjustPaneDimensions: -> + if root = @getRoot() + root.css(width: '100%', height: '100%', top: 0, left: 0) + root.adjustDimensions() + + afterAttach: -> + @adjustPaneDimensions() diff --git a/src/app/pane.coffee b/src/app/pane.coffee index b2c1ca5e1..ebad81482 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -125,27 +125,31 @@ class Pane extends View items = [@copyCurrentItem()] unless items.length pane = new Pane(items...) this[side](pane) - rootView?.adjustPaneDimensions() + @getContainer().adjustPaneDimensions() pane.focus() pane + buildPaneAxis: (axis) -> + switch axis + when 'row' then new PaneRow + when 'column' then new PaneColumn + + getContainer: -> + @closest('#panes').view() + copyCurrentItem: -> deserialize(@currentItem.serialize()) remove: (selector, keepData) -> return super if keepData # find parent elements before removing from dom + container = @getContainer() parentAxis = @parent('.row, .column') super if parentAxis.children().length == 1 sibling = parentAxis.children().detach() parentAxis.replaceWith(sibling) - rootView?.adjustPaneDimensions() + container.adjustPaneDimensions() afterRemove: -> item.destroy?() for item in @getItems() - - buildPaneAxis: (axis) -> - switch axis - when 'row' then new PaneRow - when 'column' then new PaneColumn diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 7f8a72858..a557fa866 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -10,6 +10,7 @@ Project = require 'project' Pane = require 'pane' PaneColumn = require 'pane-column' PaneRow = require 'pane-row' +PaneContainer = require 'pane-container' module.exports = class RootView extends View @@ -19,17 +20,16 @@ class RootView extends View 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 + panes = deserialize(panesViewState) if panesViewState?.deserializer is 'PaneContainer' + new RootView({panes}) title: null @@ -67,7 +67,7 @@ class RootView extends View serialize: -> deserializer: 'RootView' - panesViewState: @panes.children().view()?.serialize() + panesViewState: @panes.serialize() packageStates: atom.serializeAtomPackages() handleFocus: (e) -> @@ -170,24 +170,8 @@ class RootView extends View getActiveEditSession: -> @getActiveEditor()?.activeEditSession - focusNextPane: -> - panes = @panes.find('.pane') - currentIndex = panes.toArray().indexOf(@getFocusedPane()[0]) - nextIndex = (currentIndex + 1) % panes.length - panes.eq(nextIndex).view().focus() - - 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() diff --git a/src/app/window.coffee b/src/app/window.coffee index a4c514b15..2ff52830c 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -151,6 +151,9 @@ 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) diff --git a/static/atom.css b/static/atom.css index 003390b38..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,7 +55,7 @@ html, body { box-sizing: border-box; } -#root-view #panes .pane .item-views { +#panes .pane .item-views { -webkit-flex: 1; display: -webkit-flex; -webkit-flex-flow: column; From 7d147dd2ce891aac35156b2d53168c5661e66150 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 21 Feb 2013 10:52:21 -0700 Subject: [PATCH 124/308] Make Pane handle split commands instead of Editor --- spec/app/editor-spec.coffee | 23 ----------------------- src/app/editor.coffee | 15 ++++----------- src/app/keymaps/atom.cson | 6 ++++++ src/app/keymaps/editor.cson | 4 ---- src/app/pane.coffee | 4 ++++ 5 files changed, 14 insertions(+), 38 deletions(-) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index 1aa43f983..edcb4d13c 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -384,29 +384,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.buildEditSession("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() - editor.splitDown() - editor.splitLeft() - editor.splitRight() - describe "editor:attached event", -> it 'only triggers an editor:attached event when it is first added to the DOM', -> openHandler = jasmine.createSpy('openHandler') diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 9187e8d8f..1bc4ed8db 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -172,10 +172,6 @@ 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] @@ -789,20 +785,17 @@ class Editor extends View @updateLayerDimensions() @requestDisplayUpdate() - newSplitEditor: (editSession) -> - new Editor { editSession: editSession ? @activeEditSession.copy() } - splitLeft: (editSession) -> - @pane()?.splitLeft(@newSplitEditor(editSession)).currentItem + @pane()?.splitLeft() splitRight: (editSession) -> - @pane()?.splitRight(@newSplitEditor(editSession)).currentItem + @pane()?.splitRight() splitUp: (editSession) -> - @pane()?.splitUp(@newSplitEditor(editSession)).currentItem + @pane()?.splitUp() splitDown: (editSession) -> - @pane()?.splitDown(@newSplitEditor(editSession)).currentItem + @pane()?.splitDown() pane: -> @closest('.pane').view() diff --git a/src/app/keymaps/atom.cson b/src/app/keymaps/atom.cson index 680f25c17..d2a16ebff 100644 --- a/src/app/keymaps/atom.cson +++ b/src/app/keymaps/atom.cson @@ -30,6 +30,12 @@ 'ctrl-tab': 'window:focus-next-pane' 'ctrl-meta-f': 'window:toggle-full-screen' +'.pane': + 'ctrl-|': 'pane:split-right' + 'ctrl-w v': 'pane:split-right' + 'ctrl--': 'pane:split-down' + 'ctrl-w s': 'pane:split-down' + '.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..b072a462b 100644 --- a/src/app/keymaps/editor.cson +++ b/src/app/keymaps/editor.cson @@ -15,10 +15,6 @@ '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' diff --git a/src/app/pane.coffee b/src/app/pane.coffee index ebad81482..2cf3f7ef5 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -22,6 +22,10 @@ class Pane extends View @command 'pane:show-next-item', @showNextItem @command 'pane:show-previous-item', @showPreviousItem + @command 'pane:split-left', => @splitLeft() + @command 'pane:split-right', => @splitRight() + @command 'pane:split-up', => @splitUp() + @command 'pane:split-down', => @splitDown() @on 'focus', => @viewForCurrentItem().focus() getItems: -> From 11a702a2a62549aa5ef0aebd143dad2a2aaf12ae Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 21 Feb 2013 11:12:36 -0700 Subject: [PATCH 125/308] Remove pane when its last item is removed --- spec/app/pane-spec.coffee | 4 ++++ src/app/pane.coffee | 1 + 2 files changed, 5 insertions(+) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index a856fcc82..bb5c0b430 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -69,6 +69,10 @@ describe "Pane", -> expect(pane.getItems()).toEqual [editSession1, view2] expect(pane.currentItem).toBe editSession1 + it "removes the pane when its last item is removed", -> + pane.removeItem(item) for item in pane.getItems() + expect(pane.hasParent()).toBeFalsy() + describe "when the item is a view", -> it "removes the item from the 'item-views' div", -> expect(view1.parent()).toMatchSelector pane.itemViews diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 2cf3f7ef5..3f6a8c2ba 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -70,6 +70,7 @@ class Pane extends View _.remove(@items, item) item.destroy?() @cleanupItemView(item) + @remove() unless @items.length itemForPath: (path) -> _.detect @items, (item) -> item.getPath?() is path From bd8ec81b1ec86da953a6dd729a6606ba5624adbb Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 21 Feb 2013 11:13:10 -0700 Subject: [PATCH 126/308] Make Pane close the current item when handling 'core:close' event --- spec/app/pane-spec.coffee | 6 ++++++ src/app/pane.coffee | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index bb5c0b430..6004ced05 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -91,6 +91,12 @@ describe "Pane", -> pane.removeItem(editSession2) expect(editSession2.destroyed).toBeTruthy() + describe "core:close", -> + it "removes the current item", -> + initialItemCount = pane.getItems().length + pane.trigger 'core:close' + expect(pane.getItems().length).toBe initialItemCount - 1 + 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.currentItem).toBe view1 diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 3f6a8c2ba..4b9a20357 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -20,6 +20,7 @@ class Pane extends View @viewsByClassName = {} @showItem(@items[0]) + @command 'core:close', @removeCurrentItem @command 'pane:show-next-item', @showNextItem @command 'pane:show-previous-item', @showPreviousItem @command 'pane:split-left', => @splitLeft() @@ -65,6 +66,9 @@ class Pane extends View @items.splice(@getCurrentItemIndex() + 1, 0, item) item + removeCurrentItem: => + @removeItem(@currentItem) + removeItem: (item) -> @showNextItem() if item is @currentItem and @items.length > 1 _.remove(@items, item) From 8f980a0f203217de90a646745d0f444e6b3cd8a8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 21 Feb 2013 11:28:55 -0700 Subject: [PATCH 127/308] Replace Editor's next/previous edit session bindings w/ pane bindings --- spec/app/pane-spec.coffee | 1 + src/app/editor.coffee | 3 --- src/app/keymaps/atom.cson | 4 ++++ src/app/keymaps/editor.cson | 4 ---- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 6004ced05..3c249e749 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -50,6 +50,7 @@ describe "Pane", -> pane.showItem(editSession2) expect(pane.itemViews.find('.editor').length).toBe 1 editor = pane.itemViews.find('.editor').view() + expect(editor.css('display')).toBe '' expect(editor.activeEditSession).toBe editSession2 describe "when showing a view item", -> diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 1bc4ed8db..fc69b0c37 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -160,7 +160,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 @@ -172,7 +171,6 @@ class Editor extends View 'editor:fold-current-row': @foldCurrentRow 'editor:unfold-current-row': @unfoldCurrentRow 'editor:fold-selection': @foldSelection - '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] @@ -182,7 +180,6 @@ class Editor extends View '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 diff --git a/src/app/keymaps/atom.cson b/src/app/keymaps/atom.cson index d2a16ebff..d3427fb45 100644 --- a/src/app/keymaps/atom.cson +++ b/src/app/keymaps/atom.cson @@ -31,6 +31,10 @@ 'ctrl-meta-f': 'window:toggle-full-screen' '.pane': + 'meta-{': 'pane:show-previous-item' + 'meta-}': 'pane:show-next-item' + 'alt-meta-left': 'pane:show-previous-item' + 'alt-meta-right': 'pane:show-next-item' 'ctrl-|': 'pane:split-right' 'ctrl-w v': 'pane:split-right' 'ctrl--': 'pane:split-down' diff --git a/src/app/keymaps/editor.cson b/src/app/keymaps/editor.cson index b072a462b..e30e84e9b 100644 --- a/src/app/keymaps/editor.cson +++ b/src/app/keymaps/editor.cson @@ -18,10 +18,6 @@ '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' From 2ba63e608f5524d37c68c2db808e732762793a32 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 21 Feb 2013 11:30:48 -0700 Subject: [PATCH 128/308] Don't allow core:close event to bubble out of Pane --- spec/app/pane-spec.coffee | 7 ++++++- src/app/pane.coffee | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 3c249e749..499452b31 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -93,11 +93,16 @@ describe "Pane", -> expect(editSession2.destroyed).toBeTruthy() describe "core:close", -> - it "removes the current item", -> + it "removes the current item and does not bubble the event", -> + containerCloseHandler = jasmine.createSpy("containerCloseHandler") + container.on 'core:close', containerCloseHandler + initialItemCount = pane.getItems().length pane.trigger 'core:close' expect(pane.getItems().length).toBe initialItemCount - 1 + expect(containerCloseHandler).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.currentItem).toBe view1 diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 4b9a20357..14a1158bc 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -68,6 +68,7 @@ class Pane extends View removeCurrentItem: => @removeItem(@currentItem) + false removeItem: (item) -> @showNextItem() if item is @currentItem and @items.length > 1 From 486baa393b74191ed471e423d45a26433078923a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 21 Feb 2013 11:56:47 -0700 Subject: [PATCH 129/308] PaneContainer.getActivePane returns the most recently focused pane --- spec/app/pane-container-spec.coffee | 24 +++++++++++++++++++++++- src/app/pane-container.coffee | 12 ++++++++++++ src/app/pane.coffee | 4 +++- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/spec/app/pane-container-spec.coffee b/spec/app/pane-container-spec.coffee index 8d2bf21e1..bca9ed793 100644 --- a/spec/app/pane-container-spec.coffee +++ b/spec/app/pane-container-spec.coffee @@ -1,6 +1,6 @@ PaneContainer = require 'pane-container' Pane = require 'pane' -{View} = require 'space-pen' +{View, $$} = require 'space-pen' $ = require 'jquery' describe "PaneContainer", -> @@ -35,6 +35,28 @@ describe "PaneContainer", -> container.focusNextPane() expect(pane1.currentItem).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 "serialization", -> it "can be serialized and deserialized, and correctly adjusts dimensions of deserialized panes after attach", -> newContainer = deserialize(container.serialize()) diff --git a/src/app/pane-container.coffee b/src/app/pane-container.coffee index 87bc09f98..4e91b3b35 100644 --- a/src/app/pane-container.coffee +++ b/src/app/pane-container.coffee @@ -13,6 +13,11 @@ class PaneContainer extends View @content: -> @div id: 'panes' + initialize: -> + @on 'focusin', (e) => + focusedPane = $(e.target).closest('.pane').view() + @setActivePane(focusedPane) + serialize: -> deserializer: 'PaneContainer' root: @getRoot()?.serialize() @@ -32,6 +37,13 @@ class PaneContainer extends View getFocusedPane: -> @find('.pane:has(:focus)').view() + getActivePane: -> + @find('.pane.active').view() ? @find('.pane:first').view() + + setActivePane: (pane) -> + @find('.pane').removeClass('active') + pane.addClass('active') + adjustPaneDimensions: -> if root = @getRoot() root.css(width: '100%', height: '100%', top: 0, left: 0) diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 14a1158bc..acafb25a1 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -27,7 +27,9 @@ class Pane extends View @command 'pane:split-right', => @splitRight() @command 'pane:split-up', => @splitUp() @command 'pane:split-down', => @splitDown() - @on 'focus', => @viewForCurrentItem().focus() + @on 'focus', => + @viewForCurrentItem().focus() + false getItems: -> new Array(@items...) From 4e12882478c78c9367f2489ed7fa3d2d35cadcea Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 21 Feb 2013 11:59:00 -0700 Subject: [PATCH 130/308] Delegate getActivePane to the PaneContainer in RootView --- src/app/root-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index a557fa866..4fa03aefb 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -159,7 +159,7 @@ class RootView extends View _.uniq(_.flatten(@getEditors().map (editor) -> editor.getOpenBufferPaths())) getActivePane: -> - @panes.find('.pane.active').view() ? @panes.find('.pane:first').view() + @panes.getActivePane() getActiveEditor: -> if (editor = @panes.find('.editor.active')).length From d310fb366f32bd0324039ea176ede0d0d5eb7464 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Thu, 21 Feb 2013 14:38:03 -0700 Subject: [PATCH 131/308] Remove references to RootView from editor spec --- spec/app/editor-spec.coffee | 230 +++++++++--------------------------- 1 file changed, 57 insertions(+), 173 deletions(-) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index edcb4d13c..a5cf751cb 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' @@ -12,6 +11,19 @@ fs = require 'fs' describe "Editor", -> [buffer, editor, cachedLineHeight] = [] + beforeEach -> + editor = new Editor(project.buildEditSession('sample.js')) + buffer = editor.getBuffer() + editor.lineOverdraw = 2 + editor.isFocused = true + editor.enableKeymap() + editor.attachToDom = ({ heightInLines, widthInChars } = {}) -> + heightInLines ?= this.getBuffer().getLineCount() + this.height(getLineHeight() * heightInLines) + this.width(@charWidth * widthInChars) if widthInChars + $('#jasmine-content').append(this) + + getLineHeight = -> return cachedLineHeight if cachedLineHeight? editorForMeasurement = new Editor(editSession: project.buildEditSession('sample.js')) @@ -20,66 +32,11 @@ describe "Editor", -> editorForMeasurement.remove() cachedLineHeight - beforeEach -> - window.rootView = new RootView - rootView.open('sample.js') - editor = rootView.getActiveEditor() - buffer = editor.getBuffer() - - editor.attachToDom = ({ heightInLines } = {}) -> - heightInLines ?= this.getBuffer().getLineCount() - this.height(getLineHeight() * heightInLines) - $('#jasmine-content').append(this) - - editor.lineOverdraw = 2 - editor.enableKeymap() - editor.isFocused = true - describe "construction", -> it "throws an error if no editor session is given unless deserializing", -> 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.buildEditSession('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.buildEditSession('/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", -> expect(editor.lineHeight).toBeNull() @@ -189,14 +146,6 @@ describe "Editor", -> editor.insertText("def\n") expect(editor.lineElementForScreenRow(0).text()).toBe 'def' - it "removes the opened session from the closed sessions array", -> - editor.edit(project.buildEditSession('sample.txt')) - expect(editor.closedEditSessions.length).toBe 0 - editor.trigger "core:close" - expect(editor.closedEditSessions.length).toBe 1 - editor.edit(project.buildEditSession('sample.txt')) - expect(editor.closedEditSessions.length).toBe 0 - describe "switching edit sessions", -> [session0, session1, session2] = [] @@ -293,9 +242,7 @@ describe "Editor", -> project.setPath('/tmp') tempFilePath = '/tmp/atom-temp.txt' fs.write(tempFilePath, "") - rootView.open(tempFilePath) - editor = rootView.getActiveEditor() - expect(editor.getPath()).toBe tempFilePath + editor.edit(project.buildEditSession(tempFilePath)) afterEach -> expect(fs.remove(tempFilePath)) @@ -313,8 +260,6 @@ describe "Editor", -> selectedFilePath = null beforeEach -> editor.edit(project.buildEditSession()) - - expect(editor.getPath()).toBeUndefined() editor.getBuffer().setText 'Save me to a new path' spyOn(atom, 'showSaveDialog').andCallFake (callback) -> callback(selectedFilePath) @@ -451,27 +396,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", -> editor.attachToDom(12) - lineHeightBefore = editor.lineHeight charWidthBefore = editor.charWidth - config.set("editor.fontFamily", "PCMyungjo") - 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 -> @@ -483,24 +422,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] @@ -510,14 +434,17 @@ describe "Editor", -> expect(editor.lineHeight).toBeGreaterThan lineHeightBefore expect(editor.charWidth).toBeGreaterThan charWidthBefore expect(editor.getCursorView().position()).toEqual { top: 5 * editor.lineHeight, left: 6 * editor.charWidth } - expect(editor.activeEditSession.buffer).toBe buffer 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') @@ -527,7 +454,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()) @@ -540,22 +467,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 -> @@ -1627,7 +1557,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() @@ -1658,21 +1588,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" @@ -2169,30 +2089,13 @@ 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) @@ -2253,7 +2156,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] @@ -2270,32 +2173,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 ".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' @@ -2309,12 +2199,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() @@ -2324,8 +2208,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() From ad62f896bc1833d51a90a69dbe796adc501e260f Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Thu, 21 Feb 2013 14:47:26 -0700 Subject: [PATCH 132/308] Make Pane maintain a currentView pointer based on its current item --- spec/app/pane-spec.coffee | 6 ++++-- src/app/pane.coffee | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 499452b31..f0f0d99aa 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -41,7 +41,8 @@ describe "Pane", -> 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.itemViews.find('.editor').view() + editor = pane.currentView + expect(editor.css('display')).toBe '' expect(editor.activeEditSession).toBe editSession1 describe "when a valid view has already been appended for another item", -> @@ -49,7 +50,7 @@ describe "Pane", -> pane.showItem(editSession1) pane.showItem(editSession2) expect(pane.itemViews.find('.editor').length).toBe 1 - editor = pane.itemViews.find('.editor').view() + editor = pane.currentView expect(editor.css('display')).toBe '' expect(editor.activeEditSession).toBe editSession2 @@ -58,6 +59,7 @@ describe "Pane", -> expect(pane.itemViews.find('#view-2')).not.toExist() pane.showItem(view2) expect(pane.itemViews.find('#view-2')).toExist() + expect(pane.currentView).toBe view2 describe ".removeItem(item)", -> it "removes the item from the items list and shows the next item if it was showing", -> diff --git a/src/app/pane.coffee b/src/app/pane.coffee index acafb25a1..69ce2af62 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -58,10 +58,10 @@ class Pane extends View @addItem(item) @itemViews.children().hide() view = @viewForItem(item) - unless view.parent().is(@itemViews) - @itemViews.append(view) + @itemViews.append(view) unless view.parent().is(@itemViews) @currentItem = item - view.show() + @currentView = view + @currentView.show() addItem: (item) -> return if _.include(@items, item) From 4a6f05ae4ed5ade66549fb0093372a4a0ec42714 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Thu, 21 Feb 2013 16:22:19 -0700 Subject: [PATCH 133/308] Trigger 'pane:active-item-changed' on Pane This event is triggered when the item changes on the active pane, or when a different pane becomes active. Also: Pane now sets itself as the active pane, rather than letting PaneContainer handle the focusin event. --- spec/app/pane-spec.coffee | 33 +++++++++++++++++++++++++++++++++ src/app/pane-container.coffee | 8 -------- src/app/pane.coffee | 20 +++++++++++++++++--- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index f0f0d99aa..331e414e4 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -27,6 +27,27 @@ describe "Pane", -> expect(view2.css('display')).toBe '' expect(pane.currentItem).toBe view2 + it "triggers 'pane:active-item-changed' if the pane is active and the item isn't already the currentItem", -> + pane.makeActive() + itemChangedHandler = jasmine.createSpy("itemChangedHandler") + container.on 'pane:active-item-changed', itemChangedHandler + + expect(pane.currentItem).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() + + pane.makeInactive() + pane.showItem(editSession2) + expect(itemChangedHandler).not.toHaveBeenCalled() + describe "when the given item isn't yet in the items list on the pane", -> it "adds it to the items list after the current item", -> view3 = $$ -> @div id: 'view-3', "View 3" @@ -130,6 +151,18 @@ describe "Pane", -> pane.focus() expect(focusHandler).toHaveBeenCalled() + it "triggers 'pane:active-item-changed' if it was not previously active", -> + itemChangedHandler = jasmine.createSpy("itemChangedHandler") + container.on 'pane:active-item-changed', itemChangedHandler + + expect(pane.isActive()).toBeFalsy() + pane.focusin() + expect(pane.isActive()).toBeTruthy() + pane.focusin() + + expect(itemChangedHandler.callCount).toBe 1 + expect(itemChangedHandler.argsForCall[0][1]).toBe pane.currentItem + describe "split methods", -> [pane1, view3, view4] = [] beforeEach -> diff --git a/src/app/pane-container.coffee b/src/app/pane-container.coffee index 4e91b3b35..ca4c2f254 100644 --- a/src/app/pane-container.coffee +++ b/src/app/pane-container.coffee @@ -13,11 +13,6 @@ class PaneContainer extends View @content: -> @div id: 'panes' - initialize: -> - @on 'focusin', (e) => - focusedPane = $(e.target).closest('.pane').view() - @setActivePane(focusedPane) - serialize: -> deserializer: 'PaneContainer' root: @getRoot()?.serialize() @@ -40,9 +35,6 @@ class PaneContainer extends View getActivePane: -> @find('.pane.active').view() ? @find('.pane:first').view() - setActivePane: (pane) -> - @find('.pane').removeClass('active') - pane.addClass('active') adjustPaneDimensions: -> if root = @getRoot() diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 69ce2af62..cb5e3636d 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -27,9 +27,21 @@ class Pane extends View @command 'pane:split-right', => @splitRight() @command 'pane:split-up', => @splitUp() @command 'pane:split-down', => @splitDown() - @on 'focus', => - @viewForCurrentItem().focus() - false + @on 'focus', => @currentView.focus(); false + @on 'focusin', => @makeActive() + + makeActive: -> + for pane in @getContainer().getPanes() when pane isnt this + pane.makeInactive() + wasActive = @isActive() + @addClass('active') + @trigger 'pane:active-item-changed', [@currentItem] unless wasActive + + makeInactive: -> + @removeClass('active') + + isActive: -> + @hasClass('active') getItems: -> new Array(@items...) @@ -55,6 +67,7 @@ class Pane extends View @showItem(@items[index]) showItem: (item) -> + return if item is @currentItem @addItem(item) @itemViews.children().hide() view = @viewForItem(item) @@ -62,6 +75,7 @@ class Pane extends View @currentItem = item @currentView = view @currentView.show() + @trigger 'pane:active-item-changed', [item] if @isActive() addItem: (item) -> return if _.include(@items, item) From 58228f7ff7054c5346eeae91034cf989c5c44a15 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Thu, 21 Feb 2013 16:23:16 -0700 Subject: [PATCH 134/308] Remove RootView.activeKeybindings method. It was dead code. --- spec/app/root-view-spec.coffee | 25 ------------------------- src/app/root-view.coffee | 3 --- 2 files changed, 28 deletions(-) diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index 8b1d6f691..e3b05e280 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -186,31 +186,6 @@ describe "RootView", -> rootView.trigger(event) expect(commandHandler).toHaveBeenCalled() - describe ".activeKeybindings()", -> - originalKeymap = null - keymap = null - editor = null - - beforeEach -> - rootView.attachToDom() - editor = rootView.getActiveEditor() - keymap = new (require 'keymap') - originalKeymap = window.keymap - window.keymap = keymap - - afterEach -> - window.keymap = originalKeymap - - it "returns all keybindings available for focused element", -> - editor.on 'test-event-a', => # nothing - - keymap.bindKeys ".editor", - "meta-a": "test-event-a" - "meta-b": "test-event-b" - - keybindings = rootView.activeKeybindings() - expect(Object.keys(keybindings).length).toBe 2 - expect(keybindings["meta-a"]).toEqual "test-event-a" describe "when the path of the active editor changes", -> it "changes the title and emits an root-view:active-path-changed event", -> diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 4fa03aefb..c462de3a0 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -124,9 +124,6 @@ class RootView extends View 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" From 80e736d4ee7150ea3a62d222ef22b86852a63f92 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Thu, 21 Feb 2013 16:25:51 -0700 Subject: [PATCH 135/308] Add RootView.getActiveView and .getActivePaneItem They will replace getActiveEditor/getActiveEditSession --- spec/app/grammar-view-spec.coffee | 4 +--- src/app/pane-container.coffee | 5 +++++ src/app/root-view.coffee | 8 ++++++-- 3 files changed, 12 insertions(+), 5 deletions(-) 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/src/app/pane-container.coffee b/src/app/pane-container.coffee index ca4c2f254..aae999567 100644 --- a/src/app/pane-container.coffee +++ b/src/app/pane-container.coffee @@ -35,6 +35,11 @@ class PaneContainer extends View getActivePane: -> @find('.pane.active').view() ? @find('.pane:first').view() + getActivePaneItem: -> + @getActivePane()?.currentItem + + getActiveView: -> + @getActivePane()?.currentView adjustPaneDimensions: -> if root = @getRoot() diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index c462de3a0..d1f49294f 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -71,8 +71,8 @@ class RootView extends View packageStates: atom.serializeAtomPackages() handleFocus: (e) -> - if @getActiveEditor() - @getActiveEditor().focus() + if @getActivePane() + @getActivePane().focus() false else @setTitle(null) @@ -163,9 +163,13 @@ class RootView extends View editor.view() else @panes.find('.editor:first').view() + getActivePaneItem: -> + @panes.getActivePaneItem() getActiveEditSession: -> @getActiveEditor()?.activeEditSession + getActiveView: -> + @panes.getActiveView() focusNextPane: -> @panes.focusNextPane() getFocusedPane: -> @panes.getFocusedPane() From 517c5022d3b40568864ca264728ed058bd54a87a Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Thu, 21 Feb 2013 16:26:31 -0700 Subject: [PATCH 136/308] Provide a default param for RootView@content when not deserializing --- src/app/root-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index d1f49294f..fc67bad4b 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -20,7 +20,7 @@ class RootView extends View ignoredNames: [".git", ".svn", ".DS_Store"] disabledPackages: [] - @content: ({panes}) -> + @content: ({panes}={}) -> @div id: 'root-view', => @div id: 'horizontal', outlet: 'horizontal', => @div id: 'vertical', outlet: 'vertical', => From 75229808de2f7f65e927a07c3daffcb0b9f8bdab Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Thu, 21 Feb 2013 16:27:41 -0700 Subject: [PATCH 137/308] Add Editor.getModel --- src/app/editor.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index fc69b0c37..6b3912414 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -483,6 +483,9 @@ class Editor extends View index = @pushEditSession(editSession) if index == -1 @setActiveEditSessionIndex(index) + getModel: -> + @activeEditSession + setModel: (editSession) -> @edit(editSession) From 161ed69ef0b8057c0370ff3898635ad9a83a8a8f Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Thu, 21 Feb 2013 17:42:51 -0700 Subject: [PATCH 138/308] When a pane is removed, focus/activate the next pane --- spec/app/pane-spec.coffee | 60 +++++++++++++++++++++++++++++++++-- spec/spec-helper.coffee | 2 +- src/app/pane-container.coffee | 14 ++++++-- src/app/pane.coffee | 6 ++++ 4 files changed, 77 insertions(+), 5 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 331e414e4..b24f9540c 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -8,8 +8,8 @@ describe "Pane", -> beforeEach -> container = new PaneContainer - view1 = $$ -> @div id: 'view-1', 'View 1' - view2 = $$ -> @div id: 'view-2', 'View 2' + 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) @@ -144,6 +144,62 @@ describe "Pane", -> expect(editSession1.destroyed).toBeTruthy() expect(editSession2.destroyed).toBeTruthy() + 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 current item view", -> focusHandler = jasmine.createSpy("focusHandler") diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 910c8d238..485521129 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -73,7 +73,7 @@ afterEach -> keymap.bindingSets = bindingSetsToRestore keymap.bindingSetsByFirstKeystrokeToRestore = bindingSetsByFirstKeystrokeToRestore if rootView? - rootView.deactivate() + rootView.deactivate?() window.rootView = null if project? project.destroy() diff --git a/src/app/pane-container.coffee b/src/app/pane-container.coffee index aae999567..f9471d0b8 100644 --- a/src/app/pane-container.coffee +++ b/src/app/pane-container.coffee @@ -19,9 +19,19 @@ class PaneContainer extends View focusNextPane: -> panes = @getPanes() - currentIndex = panes.indexOf(@getFocusedPane()) + 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].focus() + panes[nextIndex].makeActive() getRoot: -> @children().first().view() diff --git a/src/app/pane.coffee b/src/app/pane.coffee index cb5e3636d..99f3d6d32 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -171,7 +171,13 @@ class Pane extends View # find parent elements before removing from dom container = @getContainer() parentAxis = @parent('.row, .column') + if @is(':has(:focus)') + rootView?.focus() unless container.focusNextPane() + else if @isActive() + container.makeNextPaneActive() + super + if parentAxis.children().length == 1 sibling = parentAxis.children().detach() parentAxis.replaceWith(sibling) From 3ae9c10ff51a0d6fcd2cc8c0643f4821f37d6754 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Thu, 21 Feb 2013 17:43:18 -0700 Subject: [PATCH 139/308] Trigger 'pane:active-item-changed' w/ null when last pane is removed --- spec/app/pane-spec.coffee | 7 +++++++ src/app/pane.coffee | 1 + 2 files changed, 8 insertions(+) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index b24f9540c..d761c364c 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -187,6 +187,13 @@ describe "Pane", -> expect(container.getPanes().length).toBe 1 window.rootView = focus: jasmine.createSpy("rootView.focus") + it "triggers a 'pane:active-item-changed' event with null", -> + itemChangedHandler = jasmine.createSpy("itemChangedHandler") + container.on 'pane:active-item-changed', itemChangedHandler + pane.remove() + expect(itemChangedHandler).toHaveBeenCalled() + expect(itemChangedHandler.argsForCall[0][1]).toBeNull() + describe "when the removed pane is focused", -> it "calls focus on rootView so we don't lose focus", -> container.attachToDom() diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 99f3d6d32..cad027c9b 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -182,6 +182,7 @@ class Pane extends View sibling = parentAxis.children().detach() parentAxis.replaceWith(sibling) container.adjustPaneDimensions() + container.trigger 'pane:active-item-changed', [null] unless container.getActivePaneItem() afterRemove: -> item.destroy?() for item in @getItems() From d6b85cf7e87690fe7625fee60b368b51d072af8e Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Thu, 21 Feb 2013 18:22:55 -0700 Subject: [PATCH 140/308] Base title updates on pane:active-item-changed events --- spec/app/root-view-spec.coffee | 71 ++++++++-------------------------- spec/spec-helper.coffee | 2 +- src/app/edit-session.coffee | 3 ++ src/app/root-view.coffee | 32 ++++++--------- 4 files changed, 32 insertions(+), 76 deletions(-) diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index e3b05e280..bb78737ed 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -179,69 +179,30 @@ 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 "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 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 + describe "when the project has a path", -> + describe "when there is no active pane item", -> + it "sets the title to the project's path", -> + rootView.getActivePane().remove() + expect(rootView.getActivePaneItem()).toBeUndefined() + expect(rootView.title).toBe project.getPath() - editor1 = rootView.getActiveEditor() - expect(rootView.getTitle()).toBe "#{fs.base(editor1.getPath())} – #{project.getPath()}" - - editor2 = rootView.getActiveEditor().splitLeft() - - path = project.resolve('b') - editor2.edit(project.buildEditSession(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 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()}" describe "font size adjustment", -> editor = null diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 485521129..92c752871 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -56,7 +56,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() diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 14a3b11e2..5bc18faba 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -55,6 +55,9 @@ class EditSession getViewClass: -> require 'editor' + getTitle: -> + fs.base(@getPath()) + destroy: -> return if @destroyed @destroyed = true diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index fc67bad4b..cc0682e93 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -39,12 +39,8 @@ 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:active-item-changed', => @updateTitle() @command 'window:increase-font-size', => config.set("editor.fontSize", config.get("editor.fontSize") + 1) @@ -124,22 +120,18 @@ class RootView extends View if not previousActiveEditor or editor.getPath() != previousActiveEditor.getPath() @trigger 'root-view:active-path-changed', editor.getPath() - 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 > .item-views > .editor').map(-> $(this).view()).toArray() From 26c63edf3388e124d738a56d1c2433b52d83331a Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Thu, 21 Feb 2013 18:30:25 -0700 Subject: [PATCH 141/308] Assert against config for font-size changing events instead of editor --- spec/app/root-view-spec.coffee | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index bb78737ed..b676435cd 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -205,26 +205,21 @@ describe "RootView", -> expect(rootView.title).toBe "#{item.getTitle()} - #{project.getPath()}" 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 pane", -> From 2bf51637983668e8a2b8e6ad39a1c3b1e82fb6ec Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Thu, 21 Feb 2013 18:31:00 -0700 Subject: [PATCH 142/308] Kill pane specs on root view --- spec/app/root-view-spec.coffee | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index b676435cd..3c467ec4f 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -141,35 +141,6 @@ describe "RootView", -> it "surrenders focus to the body", -> expect(document.activeElement).toBe $('body')[0] - describe "panes", -> - [pane1, newPaneContent] = [] - - beforeEach -> - pane1 = rootView.find('.pane').view() - - 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.find('.editor').view() - 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() describe "keymap wiring", -> commandHandler = null From 6304bac23367cab01ac0c27f4f32884a85f46be2 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Thu, 21 Feb 2013 18:32:03 -0700 Subject: [PATCH 143/308] Remove RootView.getActiveEditor / getActiveEditSession MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There's a bunch broken because of this… to be continued. --- spec/app/root-view-spec.coffee | 39 +++++++++++++++++----------------- src/app/root-view.coffee | 8 ------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index 3c467ec4f..3e9ec8fa1 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -109,38 +109,39 @@ describe "RootView", -> 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 + rootView.focus() + expect(document.activeElement).toBe document.body describe "keymap wiring", -> commandHandler = null diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index cc0682e93..371f49fa8 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -49,7 +49,6 @@ 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', => @@ -150,16 +149,9 @@ class RootView extends View getActivePane: -> @panes.getActivePane() - getActiveEditor: -> - if (editor = @panes.find('.editor.active')).length - editor.view() - else - @panes.find('.editor:first').view() getActivePaneItem: -> @panes.getActivePaneItem() - getActiveEditSession: -> - @getActiveEditor()?.activeEditSession getActiveView: -> @panes.getActiveView() From 106c6c395816e127ac3ee42543210989fa7b19d3 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Thu, 21 Feb 2013 18:34:33 -0700 Subject: [PATCH 144/308] Return 'untitled' from EditSession.getPath if its path is null --- src/app/edit-session.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 5bc18faba..5ba7eda49 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -56,7 +56,10 @@ class EditSession require 'editor' getTitle: -> - fs.base(@getPath()) + if path = @getPath() + fs.base(path) + else + 'untitled' destroy: -> return if @destroyed From 062adae714afef594a3cacd667a43716b65f7d5f Mon Sep 17 00:00:00 2001 From: Kevin Sawicki & Nathan Sobo Date: Thu, 21 Feb 2013 18:38:31 -0700 Subject: [PATCH 145/308] Return the new pane's currentView when splitting the editor --- spec/app/root-view-spec.coffee | 2 +- src/app/editor.coffee | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index 3e9ec8fa1..43c89cec0 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -251,7 +251,7 @@ describe "RootView", -> fs.write(file2, "file2") rootView.open(file1) - editor1 = rootView.getActiveEditor() + editor1 = rootView.getActiveView() buffer1 = editor1.activeEditSession.buffer expect(buffer1.getText()).toBe("file1") expect(buffer1.isModified()).toBe(false) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 6b3912414..d2ece3c63 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -786,16 +786,16 @@ class Editor extends View @requestDisplayUpdate() splitLeft: (editSession) -> - @pane()?.splitLeft() + @pane()?.splitLeft().currentView splitRight: (editSession) -> - @pane()?.splitRight() + @pane()?.splitRight().currentView splitUp: (editSession) -> - @pane()?.splitUp() + @pane()?.splitUp().currentView splitDown: (editSession) -> - @pane()?.splitDown() + @pane()?.splitDown().currentView pane: -> @closest('.pane').view() From ff899e9c1b9fb01ef6f754d3fa7d62c167145ab0 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 21 Feb 2013 17:49:42 -0800 Subject: [PATCH 146/308] Replace RootView.getActiveEditor() with getActiveView() --- spec/app/root-view-spec.coffee | 12 ++++++------ src/packages/wrap-guide/spec/wrap-guide-spec.coffee | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index 43c89cec0..85be2c4e0 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -277,7 +277,7 @@ describe "RootView", -> 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 " " @@ -311,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 @@ -323,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 -> @@ -339,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 @@ -353,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/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' From 8660670ae3bd16c02e4f465df17d900bb8475045 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 21 Feb 2013 18:01:02 -0800 Subject: [PATCH 147/308] Replace RootView.getActiveEditor() with getActiveView() --- .../autocomplete/spec/autocomplete-spec.coffee | 4 ++-- src/packages/autoflow/spec/autoflow-spec.coffee | 2 +- .../spec/bracket-matcher-spec.coffee | 2 +- .../command-logger/spec/command-logger-spec.coffee | 2 +- src/packages/gists/lib/gists.coffee | 2 +- src/packages/gists/spec/gists-spec.coffee | 2 +- src/packages/go-to-line/lib/go-to-line-view.coffee | 4 ++-- .../go-to-line/spec/go-to-line-spec.coffee | 2 +- .../lib/markdown-preview-view.coffee | 4 ++-- .../spec/markdown-preview-spec.coffee | 14 +++++++------- .../spec/strip-trailing-whitespace-spec.coffee | 2 +- 11 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/packages/autocomplete/spec/autocomplete-spec.coffee b/src/packages/autocomplete/spec/autocomplete-spec.coffee index 6912fd5b4..d85b8b507 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() 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/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..6e5ca6f0e 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,7 +46,7 @@ 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() @@ -55,7 +55,7 @@ describe "MarkdownPreview", -> describe "core:cancel event", -> it "removes markdown preview", -> rootView.open('file.md') - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() expect(rootView.find('.markdown-preview')).not.toExist() editor.trigger('markdown-preview:toggle') @@ -68,7 +68,7 @@ describe "MarkdownPreview", -> it "removes the markdown preview view", -> rootView.attachToDom() rootView.open('file.md') - editor = rootView.getActiveEditor() + editor = rootView.getActiveView() expect(rootView.find('.markdown-preview')).not.toExist() editor.trigger('markdown-preview:toggle') @@ -79,6 +79,6 @@ describe "MarkdownPreview", -> describe "when no editor is open", -> it "does not attach", -> - expect(rootView.getActiveEditor()).toBeFalsy() + expect(rootView.getActiveView()).toBeFalsy() rootView.trigger('markdown-preview:toggle') expect(rootView.find('.markdown-preview')).not.toExist() 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..e1fc838de 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) From a6bf7f876d5df28b7b2fb2413ad1a9df2c86482f Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Thu, 21 Feb 2013 18:07:04 -0800 Subject: [PATCH 148/308] Replace RootView.getActiveEditor() with getActiveView() --- spec/app/atom-package-spec.coffee | 2 +- .../packages/package-with-activation-events/main.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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: -> From 20590f590e6ef3276a2ca9726a3838f775a04d96 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 25 Feb 2013 11:36:24 -0700 Subject: [PATCH 149/308] Make window specs pass without getActiveEditor/getEditors --- spec/app/window-spec.coffee | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/spec/app/window-spec.coffee b/spec/app/window-spec.coffee index 32f9397f1..787a80734 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() From 1902a0c5530e82db0242806b6d0a55270e94d87a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 25 Feb 2013 11:52:53 -0700 Subject: [PATCH 150/308] Get CommandPanel specs passing without getActiveEditor/EditSession --- .../lib/command-panel-view.coffee | 8 ++-- .../spec/command-panel-spec.coffee | 38 +++++++++---------- 2 files changed, 22 insertions(+), 24 deletions(-) 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-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() From 3382a542b3d032f000b465afb8015f16b1e3f0f4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 25 Feb 2013 11:55:53 -0700 Subject: [PATCH 151/308] Get CommandPalette specs to pass without getActiveEditor --- .../command-palette/spec/command-palette-spec.coffee | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 From 892ff0c51fb4fc4b642798acba6bf98a07972399 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 25 Feb 2013 15:27:09 -0700 Subject: [PATCH 152/308] Add PaneContainer.eachPane It calls the given callback with all current and future panes --- spec/app/pane-container-spec.coffee | 15 +++++++++++++++ src/app/pane-container.coffee | 6 ++++++ src/app/pane.coffee | 5 +++++ src/app/root-view.coffee | 3 +++ 4 files changed, 29 insertions(+) diff --git a/spec/app/pane-container-spec.coffee b/spec/app/pane-container-spec.coffee index bca9ed793..3a20ee3e4 100644 --- a/spec/app/pane-container-spec.coffee +++ b/spec/app/pane-container-spec.coffee @@ -57,6 +57,21 @@ describe "PaneContainer", -> 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 "serialization", -> it "can be serialized and deserialized, and correctly adjusts dimensions of deserialized panes after attach", -> newContainer = deserialize(container.serialize()) diff --git a/src/app/pane-container.coffee b/src/app/pane-container.coffee index f9471d0b8..adac275b6 100644 --- a/src/app/pane-container.coffee +++ b/src/app/pane-container.coffee @@ -39,6 +39,12 @@ class PaneContainer extends View getPanes: -> @find('.pane').toArray().map (node)-> $(node).view() + 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() diff --git a/src/app/pane.coffee b/src/app/pane.coffee index cad027c9b..9e018ab3a 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -30,6 +30,11 @@ class Pane extends View @on 'focus', => @currentView.focus(); false @on 'focusin', => @makeActive() + afterAttach: -> + return if @attached + @attached = true + @trigger 'pane:attached' + makeActive: -> for pane in @getContainer().getPanes() when pane isnt this pane.makeInactive() diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 371f49fa8..f958aa5ba 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -166,6 +166,9 @@ class RootView extends View saveAll: -> editor.save() for editor in @getEditors() + eachPane: (callback) -> + @panes.eachPane(callback) + eachEditor: (callback) -> callback(editor) for editor in @getEditors() @on 'editor:attached', (e, editor) -> callback(editor) From 15d8a6cada8887dcdded426c93082ddae94fb582 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 25 Feb 2013 16:26:50 -0700 Subject: [PATCH 153/308] Simplify pane:active-item-changed events Panes now trigger an event every time their active item changes, regardless of whether the pane itself is active. Panes also trigger events when the become active and when they are removed. The rootView now scopes its active-item-changed event listener only to active panes, and also listens to listens to pane activation and removal events to update the title when switching active panes and removing the last pane. --- spec/app/pane-spec.coffee | 29 ++++++++++++----------------- spec/app/root-view-spec.coffee | 27 ++++++++++++++++++++++----- src/app/pane.coffee | 6 +++--- src/app/root-view.coffee | 4 +++- 4 files changed, 40 insertions(+), 26 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index d761c364c..308a5e7d8 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -27,7 +27,7 @@ describe "Pane", -> expect(view2.css('display')).toBe '' expect(pane.currentItem).toBe view2 - it "triggers 'pane:active-item-changed' if the pane is active and the item isn't already the currentItem", -> + it "triggers 'pane:active-item-changed' if the item isn't already the currentItem", -> pane.makeActive() itemChangedHandler = jasmine.createSpy("itemChangedHandler") container.on 'pane:active-item-changed', itemChangedHandler @@ -44,10 +44,6 @@ describe "Pane", -> expect(itemChangedHandler.argsForCall[0][1]).toBe editSession1 itemChangedHandler.reset() - pane.makeInactive() - pane.showItem(editSession2) - expect(itemChangedHandler).not.toHaveBeenCalled() - describe "when the given item isn't yet in the items list on the pane", -> it "adds it to the items list after the current item", -> view3 = $$ -> @div id: 'view-3', "View 3" @@ -144,6 +140,13 @@ describe "Pane", -> 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] = [] @@ -187,13 +190,6 @@ describe "Pane", -> expect(container.getPanes().length).toBe 1 window.rootView = focus: jasmine.createSpy("rootView.focus") - it "triggers a 'pane:active-item-changed' event with null", -> - itemChangedHandler = jasmine.createSpy("itemChangedHandler") - container.on 'pane:active-item-changed', itemChangedHandler - pane.remove() - expect(itemChangedHandler).toHaveBeenCalled() - expect(itemChangedHandler.argsForCall[0][1]).toBeNull() - describe "when the removed pane is focused", -> it "calls focus on rootView so we don't lose focus", -> container.attachToDom() @@ -214,17 +210,16 @@ describe "Pane", -> pane.focus() expect(focusHandler).toHaveBeenCalled() - it "triggers 'pane:active-item-changed' if it was not previously active", -> - itemChangedHandler = jasmine.createSpy("itemChangedHandler") - container.on 'pane:active-item-changed', itemChangedHandler + 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(itemChangedHandler.callCount).toBe 1 - expect(itemChangedHandler.argsForCall[0][1]).toBe pane.currentItem + expect(becameActiveHandler.callCount).toBe 1 describe "split methods", -> [pane1, view3, view4] = [] diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index 85be2c4e0..cc56ee87e 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -165,17 +165,34 @@ describe "RootView", -> expect(rootView.title).toBe 'untitled' describe "when the project has a path", -> - describe "when there is no active pane item", -> - it "sets the title to the project's path", -> - rootView.getActivePane().remove() - expect(rootView.getActivePaneItem()).toBeUndefined() - expect(rootView.title).toBe project.getPath() + beforeEach -> + rootView.open('b') 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()}" + 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()}" + + 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 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", -> it "increases/decreases font size when increase/decrease-font-size events are triggered", -> fontSizeBefore = config.get('editor.fontSize') diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 9e018ab3a..5a95cf5a4 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -40,7 +40,7 @@ class Pane extends View pane.makeInactive() wasActive = @isActive() @addClass('active') - @trigger 'pane:active-item-changed', [@currentItem] unless wasActive + @trigger 'pane:became-active' unless wasActive makeInactive: -> @removeClass('active') @@ -80,7 +80,7 @@ class Pane extends View @currentItem = item @currentView = view @currentView.show() - @trigger 'pane:active-item-changed', [item] if @isActive() + @trigger 'pane:active-item-changed', [item] addItem: (item) -> return if _.include(@items, item) @@ -187,7 +187,7 @@ class Pane extends View sibling = parentAxis.children().detach() parentAxis.replaceWith(sibling) container.adjustPaneDimensions() - container.trigger 'pane:active-item-changed', [null] unless container.getActivePaneItem() + container.trigger 'pane:removed', [this] afterRemove: -> item.destroy?() for item in @getItems() diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index f958aa5ba..4515f50e2 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -40,7 +40,9 @@ class RootView extends View @handleFocus(e) if document.activeElement is document.body project.on 'path-changed', => @updateTitle() - @on 'pane:active-item-changed', => @updateTitle() + @on 'pane:became-active', => @updateTitle() + @on 'pane:active-item-changed', '.active.pane', => @updateTitle() + @on 'pane:removed', => @updateTitle() unless @getActivePane() @command 'window:increase-font-size', => config.set("editor.fontSize", config.get("editor.fontSize") + 1) From a40d05f6ee364d367f503e4efa09850dfc31e097 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 25 Feb 2013 16:29:29 -0700 Subject: [PATCH 154/308] Rename Pane.currentItem/View to activeItem/View --- spec/app/pane-container-spec.coffee | 8 ++-- spec/app/pane-spec.coffee | 60 ++++++++++++++--------------- src/app/pane.coffee | 40 +++++++++---------- 3 files changed, 54 insertions(+), 54 deletions(-) diff --git a/spec/app/pane-container-spec.coffee b/spec/app/pane-container-spec.coffee index 3a20ee3e4..063d3598b 100644 --- a/spec/app/pane-container-spec.coffee +++ b/spec/app/pane-container-spec.coffee @@ -27,13 +27,13 @@ describe "PaneContainer", -> it "focuses the pane following the focused pane or the first pane if no pane has focus", -> container.attachToDom() container.focusNextPane() - expect(pane1.currentItem).toMatchSelector ':focus' + expect(pane1.activeItem).toMatchSelector ':focus' container.focusNextPane() - expect(pane2.currentItem).toMatchSelector ':focus' + expect(pane2.activeItem).toMatchSelector ':focus' container.focusNextPane() - expect(pane3.currentItem).toMatchSelector ':focus' + expect(pane3.activeItem).toMatchSelector ':focus' container.focusNextPane() - expect(pane1.currentItem).toMatchSelector ':focus' + expect(pane1.activeItem).toMatchSelector ':focus' describe ".getActivePane()", -> it "returns the most-recently focused pane", -> diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 308a5e7d8..0d2abd87e 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -20,19 +20,19 @@ describe "Pane", -> expect(pane.itemViews.find('#view-1')).toExist() describe ".showItem(item)", -> - it "hides all item views except the one being shown and sets the currentItem", -> - expect(pane.currentItem).toBe view1 + 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.currentItem).toBe view2 + expect(pane.activeItem).toBe view2 - it "triggers 'pane:active-item-changed' if the item isn't already the currentItem", -> + 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.currentItem).toBe view1 + expect(pane.activeItem).toBe view1 pane.showItem(view2) pane.showItem(view2) expect(itemChangedHandler.callCount).toBe 1 @@ -45,20 +45,20 @@ describe "Pane", -> itemChangedHandler.reset() describe "when the given item isn't yet in the items list on the pane", -> - it "adds it to the items list after the current item", -> + it "adds it to the items list after the active item", -> view3 = $$ -> @div id: 'view-3', "View 3" pane.showItem(editSession1) - expect(pane.getCurrentItemIndex()).toBe 1 + expect(pane.getActiveItemIndex()).toBe 1 pane.showItem(view3) expect(pane.getItems()).toEqual [view1, editSession1, view3, view2, editSession2] - expect(pane.currentItem).toBe view3 - expect(pane.getCurrentItemIndex()).toBe 2 + expect(pane.activeItem).toBe view3 + expect(pane.getActiveItemIndex()).toBe 2 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.currentView + editor = pane.activeView expect(editor.css('display')).toBe '' expect(editor.activeEditSession).toBe editSession1 @@ -67,7 +67,7 @@ describe "Pane", -> pane.showItem(editSession1) pane.showItem(editSession2) expect(pane.itemViews.find('.editor').length).toBe 1 - editor = pane.currentView + editor = pane.activeView expect(editor.css('display')).toBe '' expect(editor.activeEditSession).toBe editSession2 @@ -76,18 +76,18 @@ describe "Pane", -> expect(pane.itemViews.find('#view-2')).not.toExist() pane.showItem(view2) expect(pane.itemViews.find('#view-2')).toExist() - expect(pane.currentView).toBe view2 + expect(pane.activeView).toBe view2 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.currentItem).toBe editSession1 + expect(pane.activeItem).toBe editSession1 pane.showItem(editSession2) pane.removeItem(editSession2) expect(pane.getItems()).toEqual [editSession1, view2] - expect(pane.currentItem).toBe editSession1 + expect(pane.activeItem).toBe editSession1 it "removes the pane when its last item is removed", -> pane.removeItem(item) for item in pane.getItems() @@ -112,7 +112,7 @@ describe "Pane", -> expect(editSession2.destroyed).toBeTruthy() describe "core:close", -> - it "removes the current item and does not bubble the event", -> + it "removes the active item and does not bubble the event", -> containerCloseHandler = jasmine.createSpy("containerCloseHandler") container.on 'core:close', containerCloseHandler @@ -124,15 +124,15 @@ describe "Pane", -> 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.currentItem).toBe view1 + expect(pane.activeItem).toBe view1 pane.trigger 'pane:show-previous-item' - expect(pane.currentItem).toBe editSession2 + expect(pane.activeItem).toBe editSession2 pane.trigger 'pane:show-previous-item' - expect(pane.currentItem).toBe view2 + expect(pane.activeItem).toBe view2 pane.trigger 'pane:show-next-item' - expect(pane.currentItem).toBe editSession2 + expect(pane.activeItem).toBe editSession2 pane.trigger 'pane:show-next-item' - expect(pane.currentItem).toBe view1 + expect(pane.activeItem).toBe view1 describe ".remove()", -> it "destroys all the pane's items", -> @@ -204,9 +204,9 @@ describe "Pane", -> expect(rootView.focus).not.toHaveBeenCalled() describe "when the pane is focused", -> - it "focuses the current item view", -> + it "focuses the active item view", -> focusHandler = jasmine.createSpy("focusHandler") - pane.currentItem.on 'focus', focusHandler + pane.activeItem.on 'focus', focusHandler pane.focus() expect(focusHandler).toHaveBeenCalled() @@ -231,11 +231,11 @@ describe "Pane", -> 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 current item if none are given + # 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.currentItem).not.toBe editSession1 # it's a copy + expect(pane2.activeItem).not.toBe editSession1 # it's a copy pane3 = pane2.splitRight(view3, view4) expect(pane3.getItems()).toEqual [view3, view4] @@ -243,11 +243,11 @@ describe "Pane", -> 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 current item if none are given + # 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.currentItem).not.toBe editSession1 # it's a copy + expect(pane2.activeItem).not.toBe editSession1 # it's a copy pane3 = pane2.splitLeft(view3, view4) expect(pane3.getItems()).toEqual [view3, view4] @@ -255,11 +255,11 @@ describe "Pane", -> 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 current item if none are given + # 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.currentItem).not.toBe editSession1 # it's a copy + expect(pane2.activeItem).not.toBe editSession1 # it's a copy pane3 = pane2.splitDown(view3, view4) expect(pane3.getItems()).toEqual [view3, view4] @@ -267,11 +267,11 @@ describe "Pane", -> 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 current item if none are given + # 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.currentItem).not.toBe editSession1 # it's a copy + expect(pane2.activeItem).not.toBe editSession1 # it's a copy pane3 = pane2.splitUp(view3, view4) expect(pane3.getItems()).toEqual [view3, view4] diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 5a95cf5a4..1fad3f6fa 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -13,21 +13,21 @@ class Pane extends View @deserialize: ({items}) -> new Pane(items.map((item) -> deserialize(item))...) - currentItem: null + activeItem: null items: null initialize: (@items...) -> @viewsByClassName = {} @showItem(@items[0]) - @command 'core:close', @removeCurrentItem + @command 'core:close', @removeActiveItem @command 'pane:show-next-item', @showNextItem @command 'pane:show-previous-item', @showPreviousItem @command 'pane:split-left', => @splitLeft() @command 'pane:split-right', => @splitRight() @command 'pane:split-up', => @splitUp() @command 'pane:split-down', => @splitDown() - @on 'focus', => @currentView.focus(); false + @on 'focus', => @activeView.focus(); false @on 'focusin', => @makeActive() afterAttach: -> @@ -52,47 +52,47 @@ class Pane extends View new Array(@items...) showNextItem: => - index = @getCurrentItemIndex() + index = @getActiveItemIndex() if index < @items.length - 1 @showItemAtIndex(index + 1) else @showItemAtIndex(0) showPreviousItem: => - index = @getCurrentItemIndex() + index = @getActiveItemIndex() if index > 0 @showItemAtIndex(index - 1) else @showItemAtIndex(@items.length - 1) - getCurrentItemIndex: -> - @items.indexOf(@currentItem) + getActiveItemIndex: -> + @items.indexOf(@activeItem) showItemAtIndex: (index) -> @showItem(@items[index]) showItem: (item) -> - return if item is @currentItem + return if item is @activeItem @addItem(item) @itemViews.children().hide() view = @viewForItem(item) @itemViews.append(view) unless view.parent().is(@itemViews) - @currentItem = item - @currentView = view - @currentView.show() + @activeItem = item + @activeView = view + @activeView.show() @trigger 'pane:active-item-changed', [item] addItem: (item) -> return if _.include(@items, item) - @items.splice(@getCurrentItemIndex() + 1, 0, item) + @items.splice(@getActiveItemIndex() + 1, 0, item) item - removeCurrentItem: => - @removeItem(@currentItem) + removeActiveItem: => + @removeItem(@activeItem) false removeItem: (item) -> - @showNextItem() if item is @currentItem and @items.length > 1 + @showNextItem() if item is @activeItem and @items.length > 1 _.remove(@items, item) item.destroy?() @cleanupItemView(item) @@ -122,8 +122,8 @@ class Pane extends View view = @viewsByClassName[viewClass.name] = new viewClass(item) view - viewForCurrentItem: -> - @viewForItem(@currentItem) + viewForActiveItem: -> + @viewForItem(@activeItem) serialize: -> deserializer: "Pane" @@ -153,7 +153,7 @@ class Pane extends View .insertBefore(this) .append(@detach()) - items = [@copyCurrentItem()] unless items.length + items = [@copyActiveItem()] unless items.length pane = new Pane(items...) this[side](pane) @getContainer().adjustPaneDimensions() @@ -168,8 +168,8 @@ class Pane extends View getContainer: -> @closest('#panes').view() - copyCurrentItem: -> - deserialize(@currentItem.serialize()) + copyActiveItem: -> + deserialize(@activeItem.serialize()) remove: (selector, keepData) -> return super if keepData From dd120663b7e0ef9018ecad2cecb8f0d9b355128b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 25 Feb 2013 17:11:29 -0700 Subject: [PATCH 155/308] Pane emits 'pane:item-added' events --- spec/app/pane-spec.coffee | 12 ++- src/app/pane.coffee | 4 +- src/packages/tabs/lib/tab-bar-view.coffee | 100 ++++++++++++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 src/packages/tabs/lib/tab-bar-view.coffee diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 0d2abd87e..963ce359f 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -45,15 +45,25 @@ describe "Pane", -> itemChangedHandler.reset() describe "when the given item isn't yet in the items list on the pane", -> - it "adds it to the items list after the active item", -> + 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", -> diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 1fad3f6fa..547160157 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -84,7 +84,9 @@ class Pane extends View addItem: (item) -> return if _.include(@items, item) - @items.splice(@getActiveItemIndex() + 1, 0, item) + index = @getActiveItemIndex() + 1 + @items.splice(index, 0, item) + @trigger 'pane:item-added', [item, index] item removeActiveItem: => 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..d98d12bc1 --- /dev/null +++ b/src/packages/tabs/lib/tab-bar-view.coffee @@ -0,0 +1,100 @@ +$ = require 'jquery' +SortableList = require 'sortable-list' +TabView = require './tab-view' + +module.exports = +class TabBarView extends SortableList + @content: -> + @ul class: "tabs #{@viewClass()}" + + initialize: (@pane) -> + super + + @addTabForItem(item) for item in @pane.getItems() + + +# @addTabForEditSession(editSession) for editSession in @editor.editSessions +# +# @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) + + @on 'click', '.tab', (e) => + @editor.setActiveEditSessionIndex($(e.target).closest('.tab').index()) + @editor.focus() + + @on 'click', '.tab .close-icon', (e) => + index = $(e.target).closest('.tab').index() + @editor.destroyEditSessionIndex(index) + false + + @pane.prepend(this) + + addTabForItem: (item) -> + tabView = new TabView(item, @pane) + @append(tabView) + @setActiveTabView(tabView) if item is @pane.currentItem + + setActiveTabView: (tabView) -> + unless tabView.hasClass('active') + @find(".tab.active").removeClass('active') + tabView.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(".#{TabBarView.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() + 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() From 5240d9989fd0b87e0eb73833e9d4805c10e2e350 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 25 Feb 2013 17:15:00 -0700 Subject: [PATCH 156/308] Pane emits 'pane:item-removed' events --- spec/app/pane-spec.coffee | 7 +++++++ src/app/pane.coffee | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 963ce359f..d263cff4b 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -99,6 +99,13 @@ describe "Pane", -> 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] + it "removes the pane when its last item is removed", -> pane.removeItem(item) for item in pane.getItems() expect(pane.hasParent()).toBeFalsy() diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 547160157..eadccaa50 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -94,10 +94,14 @@ class Pane extends View false removeItem: (item) -> + index = @items.indexOf(item) + return if index == -1 + @showNextItem() if item is @activeItem and @items.length > 1 _.remove(@items, item) item.destroy?() @cleanupItemView(item) + @trigger 'pane:item-removed', [item, index] @remove() unless @items.length itemForPath: (path) -> From 0c24843e52a57c65fcff285afed04f01f89aa5d8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 25 Feb 2013 17:41:00 -0700 Subject: [PATCH 157/308] Start converting tabs package to work with new panes / pane-items --- src/packages/tabs/lib/tab-bar-view.coffee | 46 +++++-- src/packages/tabs/lib/tab-view.coffee | 128 ++++++------------- src/packages/tabs/lib/tab.coffee | 40 ------ src/packages/tabs/lib/tabs.coffee | 5 + src/packages/tabs/package.cson | 2 +- src/packages/tabs/spec/tabs-spec.coffee | 147 ++++++++++------------ 6 files changed, 145 insertions(+), 223 deletions(-) delete mode 100644 src/packages/tabs/lib/tab.coffee create mode 100644 src/packages/tabs/lib/tabs.coffee diff --git a/src/packages/tabs/lib/tab-bar-view.coffee b/src/packages/tabs/lib/tab-bar-view.coffee index d98d12bc1..773c9ddde 100644 --- a/src/packages/tabs/lib/tab-bar-view.coffee +++ b/src/packages/tabs/lib/tab-bar-view.coffee @@ -1,4 +1,5 @@ $ = require 'jquery' +_ = require 'underscore' SortableList = require 'sortable-list' TabView = require './tab-view' @@ -9,14 +10,16 @@ class TabBarView extends SortableList initialize: (@pane) -> super - @addTabForItem(item) for item in @pane.getItems() + @pane.on 'pane:item-added', (e, item, index) => @addTabForItem(item, index) + @pane.on 'pane:item-removed', (e, item) => @removeTabForItem(item) + @pane.on 'pane:active-item-changed', => @updateActiveTab() + + @updateActiveTab() -# @addTabForEditSession(editSession) for editSession in @editor.editSessions -# # @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) => @@ -29,26 +32,45 @@ class TabBarView extends SortableList # fromTab.insertBefore(toTab) @on 'click', '.tab', (e) => - @editor.setActiveEditSessionIndex($(e.target).closest('.tab').index()) - @editor.focus() + tab = $(e.target).closest('.tab').view() + @pane.showItem(tab.item) + @pane.focus() @on 'click', '.tab .close-icon', (e) => - index = $(e.target).closest('.tab').index() - @editor.destroyEditSessionIndex(index) + tab = $(e.target).closest('.tab').view() + @pane.removeItem(tab.item) false @pane.prepend(this) - addTabForItem: (item) -> + addTabForItem: (item, index) -> tabView = new TabView(item, @pane) - @append(tabView) - @setActiveTabView(tabView) if item is @pane.currentItem + followingTab = @tabAtIndex(index) if index? + if followingTab + tabView.insertBefore(followingTab) + else + @append(tabView) - setActiveTabView: (tabView) -> + 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)) + removeTabAtIndex: (index) -> @find(".tab:eq(#{index})").remove() diff --git a/src/packages/tabs/lib/tab-view.coffee b/src/packages/tabs/lib/tab-view.coffee index 166233094..351fa15ed 100644 --- a/src/packages/tabs/lib/tab-view.coffee +++ b/src/packages/tabs/lib/tab-view.coffee @@ -1,100 +1,44 @@ -$ = 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) -> + @title.text(@item.getTitle()) - @addTabForEditSession(editSession) for editSession in @editor.editSessions - @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) +# @buffer = @editSession.buffer +# @subscribe @buffer, 'path-changed', => @updateFileName() +# @subscribe @buffer, 'contents-modified', => @updateModifiedStatus() +# @subscribe @buffer, 'saved', => @updateModifiedStatus() +# @subscribe @buffer, 'git-status-changed', => @updateModifiedStatus() +# @subscribe @editor, 'editor:edit-session-added', => @updateFileName() +# @subscribe @editor, 'editor:edit-session-removed', => @updateFileName() +# @updateFileName() +# @updateModifiedStatus() - @on 'click', '.tab', (e) => - @editor.setActiveEditSessionIndex($(e.target).closest('.tab').index()) - @editor.focus() - - @on 'click', '.tab .close-icon', (e) => - index = $(e.target).closest('.tab').index() - @editor.destroyEditSessionIndex(index) - false - - 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 @buffer.isModified() + @toggleClass('file-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('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/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..07ad016a9 100644 --- a/src/packages/tabs/spec/tabs-spec.coffee +++ b/src/packages/tabs/spec/tabs-spec.coffee @@ -1,93 +1,99 @@ $ = 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 +fdescribe "TabBarView", -> + [item1, item2, editSession1, editSession2, 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 + @content: (title) -> @div title + initialize: (@title) -> + getTitle: -> @title - it "highlights the tab for the current active edit session", -> - expect(editor.getActiveEditSessionIndex()).toBe 1 - expect(tabs.find('.tab:eq(1)')).toHaveClass 'active' + beforeEach -> + 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() + 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 - 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' + 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() - editor.setActiveEditSessionIndex(1) - expect(tabs.find('.active').length).toBe 1 - expect(tabs.find('.tab:eq(1)')).toHaveClass 'active' + it "highlights the tab for the active pane item", -> + expect(tabBar.find('.tab:eq(2)')).toHaveClass 'active' - 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' + 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' - 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' + pane.showItem(item2) + expect(tabBar.find('.active').length).toBe 1 + expect(tabBar.find('.tab:eq(2)')).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() + describe "when a new item is added to the pane", -> + ffit "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' - 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 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] + + tabBar.tabAtIndex(2).click() + expect(pane.activeItem).toBe pane.getItems()[2] + + expect(pane.focus.callCount).toBe 2 + + describe "when a tab's close icon is clicked", -> + it "removes the tab's item from the pane", -> + tabBar.tabForItem(item1).find('.close-icon').click() + expect(pane.getItems().length).toBe 2 + expect(pane.getItems().indexOf(item1)).toBe -1 + expect(tabBar.getTabs().length).toBe 2 + expect(tabBar.find('.tab:contains(Item 1)')).not.toExist() describe "when a file name associated with a tab changes", -> [buffer, oldPath, newPath] = [] @@ -110,21 +116,6 @@ describe "TabView", -> waitsFor "file to be renamed", -> tabFileName.text() == "renamed-file.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 - - 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 - describe "when two tabs have the same file name", -> [tempPath] = [] From de8198084cb8365419265620d1023f14a6556a25 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 25 Feb 2013 17:54:41 -0700 Subject: [PATCH 158/308] EditSession emits 'title-changed' events when its buffer path changes --- spec/app/edit-session-spec.coffee | 14 ++++++++++++++ src/app/edit-session.coffee | 4 +++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index cbd7a90e9..3889dc35d 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -16,6 +16,20 @@ describe "EditSession", -> afterEach -> fixturesProject.destroy() + describe "title", -> + 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' + + 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()", -> it "returns the most recently created cursor", -> diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 5ba7eda49..2d72d39c5 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -43,7 +43,9 @@ class EditSession @addCursorAtScreenPosition([0, 0]) @buffer.retain() - @subscribe @buffer, "path-changed", => @trigger "path-changed" + @subscribe @buffer, "path-changed", => + @trigger "title-changed" + @trigger "path-changed" @subscribe @buffer, "contents-conflicted", => @trigger "contents-conflicted" @subscribe @buffer, "markers-updated", => @mergeCursors() From 8898f81fc3c4105e709f7ee5d5a7d46d4c9011b1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 25 Feb 2013 18:11:19 -0700 Subject: [PATCH 159/308] Add `$.fn.views` method to space pane, which returns an array of views --- vendor/space-pen.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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) -> From 3456b2db3cfd05df182a31a9fe3d9e2b9051c8e4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 25 Feb 2013 18:12:45 -0700 Subject: [PATCH 160/308] Tabs try to use an item's `longTitle` if two tab titles are the same This will replace edit-session-specific functionality that displayed the file's parent directory when two files with the same name were open. --- src/packages/tabs/lib/tab-view.coffee | 24 +++++++++-- src/packages/tabs/spec/tabs-spec.coffee | 53 +++++++++---------------- 2 files changed, 40 insertions(+), 37 deletions(-) diff --git a/src/packages/tabs/lib/tab-view.coffee b/src/packages/tabs/lib/tab-view.coffee index 351fa15ed..ab75a9c03 100644 --- a/src/packages/tabs/lib/tab-view.coffee +++ b/src/packages/tabs/lib/tab-view.coffee @@ -1,3 +1,4 @@ +$ = require 'jquery' {View} = require 'space-pen' fs = require 'fs' @@ -9,9 +10,8 @@ class TabView extends View @span class: 'close-icon' initialize: (@item, @pane) -> - @title.text(@item.getTitle()) - - + @item.on? 'title-changed', => @updateTitle() + @updateTitle() # @buffer = @editSession.buffer # @subscribe @buffer, 'path-changed', => @updateFileName() # @subscribe @buffer, 'contents-modified', => @updateModifiedStatus() @@ -22,6 +22,24 @@ class TabView extends View # @updateFileName() # @updateModifiedStatus() + updateTitle: -> + return if @updatingTitle + @updatingTitle = true + + 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 + + @title.text(title) + @updatingTitle = false + + getSiblingTabs: -> + @siblings('.tab').views() + updateModifiedStatus: -> if @buffer.isModified() @toggleClass('file-modified') unless @isModified diff --git a/src/packages/tabs/spec/tabs-spec.coffee b/src/packages/tabs/spec/tabs-spec.coffee index 07ad016a9..22ff046fd 100644 --- a/src/packages/tabs/spec/tabs-spec.coffee +++ b/src/packages/tabs/spec/tabs-spec.coffee @@ -28,6 +28,7 @@ fdescribe "TabBarView", -> @content: (title) -> @div title initialize: (@title) -> getTitle: -> @title + getLongTitle: -> @longTitle beforeEach -> item1 = new TestView('Item 1') @@ -62,7 +63,7 @@ fdescribe "TabBarView", -> expect(tabBar.find('.tab:eq(2)')).toHaveClass 'active' describe "when a new item is added to the pane", -> - ffit "adds a tab for the new item at the same index as the item in 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) @@ -95,44 +96,28 @@ fdescribe "TabBarView", -> expect(tabBar.getTabs().length).toBe 2 expect(tabBar.find('.tab:contains(Item 1)')).not.toExist() - describe "when a file name associated with a tab changes", -> - [buffer, oldPath, newPath] = [] - - 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) - - afterEach -> - fs.remove(newPath) if fs.exists(newPath) - - 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 two tabs have the same file name", -> - [tempPath] = [] + 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' - beforeEach -> - tempPath = '/tmp/sample.js' - fs.write(tempPath, 'sample') + expect(tabBar.tabForItem(item1)).toHaveText "Grumpy Old Man" + expect(tabBar.tabForItem(item2)).toHaveText "Jolly Old Man" - afterEach -> - fs.remove(tempPath) if fs.exists(tempPath) + item2.longTitle = undefined + item2.trigger 'title-changed' - 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' + expect(tabBar.tabForItem(item1)).toHaveText "Grumpy Old Man" + expect(tabBar.tabForItem(item2)).toHaveText "Old Man" 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", -> From 21990cf98637f8b06153773903680f01283d97d5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 25 Feb 2013 18:26:59 -0700 Subject: [PATCH 161/308] Add EditSession.getLongTitle --- spec/app/edit-session-spec.coffee | 15 +++++++++++---- src/app/edit-session.coffee | 8 ++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 3889dc35d..4c1bdc4a0 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -17,10 +17,17 @@ describe "EditSession", -> fixturesProject.destroy() describe "title", -> - 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 ".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") diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 2d72d39c5..f6e6b344a 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -63,6 +63,14 @@ class EditSession else 'untitled' + getLongTitle: -> + if path = @getPath() + fileName = fs.base(path) + directory = fs.base(fs.directory(path)) + "#{fileName} - #{directory}" + else + 'untitled' + destroy: -> return if @destroyed @destroyed = true From 887b5ea007b0cba7124638fe4d4adfea003979ba Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 25 Feb 2013 19:03:06 -0700 Subject: [PATCH 162/308] Add Pane.moveItem(item, index) This is the precursor to supporting drag/drop of tabs within and between panes. --- spec/app/pane-spec.coffee | 23 +++++++++++++++++++++++ src/app/pane.coffee | 6 ++++++ 2 files changed, 29 insertions(+) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index d263cff4b..884399bf8 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -128,6 +128,29 @@ describe "Pane", -> pane.removeItem(editSession2) expect(editSession2.destroyed).toBeTruthy() + 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 "core:close", -> it "removes the active item and does not bubble the event", -> containerCloseHandler = jasmine.createSpy("containerCloseHandler") diff --git a/src/app/pane.coffee b/src/app/pane.coffee index eadccaa50..e01ec8fee 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -104,6 +104,12 @@ class Pane extends View @trigger 'pane:item-removed', [item, index] @remove() unless @items.length + moveItem: (item, newIndex) -> + oldIndex = @items.indexOf(item) + @items.splice(oldIndex, 1) + @items.splice(newIndex, 0, item) + @trigger 'pane:item-moved', [item, newIndex] + itemForPath: (path) -> _.detect @items, (item) -> item.getPath?() is path From 465bb146598b35fd95814297ed0b51074b5d90c4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 25 Feb 2013 19:12:49 -0700 Subject: [PATCH 163/308] Reflect pane item order in tab bar --- src/packages/tabs/lib/tab-bar-view.coffee | 14 +++++++++++--- src/packages/tabs/spec/tabs-spec.coffee | 21 +++++++++------------ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/packages/tabs/lib/tab-bar-view.coffee b/src/packages/tabs/lib/tab-bar-view.coffee index 773c9ddde..930ecc981 100644 --- a/src/packages/tabs/lib/tab-bar-view.coffee +++ b/src/packages/tabs/lib/tab-bar-view.coffee @@ -13,6 +13,7 @@ class TabBarView extends SortableList @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() @@ -44,12 +45,19 @@ class TabBarView extends SortableList @pane.prepend(this) addTabForItem: (item, index) -> - tabView = new TabView(item, @pane) + @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 - tabView.insertBefore(followingTab) + tab.insertBefore(followingTab) else - @append(tabView) + @append(tab) removeTabForItem: (item) -> @tabForItem(item).remove() diff --git a/src/packages/tabs/spec/tabs-spec.coffee b/src/packages/tabs/spec/tabs-spec.coffee index 22ff046fd..03029cb35 100644 --- a/src/packages/tabs/spec/tabs-spec.coffee +++ b/src/packages/tabs/spec/tabs-spec.coffee @@ -119,18 +119,15 @@ fdescribe "TabBarView", -> expect(tabBar.tabForItem(item1)).toHaveText "Grumpy Old Man" expect(tabBar.tabForItem(item2)).toHaveText "Old Man" - 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" - - 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", -> From 1d0cd16cd17972d8d8c49884761874bc2911c6ea Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 25 Feb 2013 19:20:19 -0700 Subject: [PATCH 164/308] :lipstick: --- src/packages/tabs/spec/tabs-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/tabs/spec/tabs-spec.coffee b/src/packages/tabs/spec/tabs-spec.coffee index 03029cb35..089b76c5b 100644 --- a/src/packages/tabs/spec/tabs-spec.coffee +++ b/src/packages/tabs/spec/tabs-spec.coffee @@ -22,7 +22,7 @@ describe "Tabs package main", -> expect(rootView.panes.find('.pane > .tabs').length).toBe 2 fdescribe "TabBarView", -> - [item1, item2, editSession1, editSession2, pane, tabBar] = [] + [item1, item2, editSession1, pane, tabBar] = [] class TestView extends View @content: (title) -> @div title From 4a7e5b74c61a6a443d6b638036b8c51b7ad303fd Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 26 Feb 2013 10:47:26 -0700 Subject: [PATCH 165/308] Make sure a pane view is showing before assigning its model object --- src/app/pane.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/pane.coffee b/src/app/pane.coffee index e01ec8fee..fecf57fc5 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -74,12 +74,12 @@ class Pane extends View showItem: (item) -> return if item is @activeItem @addItem(item) - @itemViews.children().hide() view = @viewForItem(item) + @itemViews.children().not(view).hide() @itemViews.append(view) unless view.parent().is(@itemViews) + view.show() @activeItem = item @activeView = view - @activeView.show() @trigger 'pane:active-item-changed', [item] addItem: (item) -> From 61fa393e03973c5267c2b38f9e356285c83eace1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 26 Feb 2013 11:41:41 -0700 Subject: [PATCH 166/308] Add indexOfPane and paneAtIndex to PaneContainer --- src/app/pane-container.coffee | 8 +++++++- src/app/root-view.coffee | 13 ++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/app/pane-container.coffee b/src/app/pane-container.coffee index adac275b6..1e179c085 100644 --- a/src/app/pane-container.coffee +++ b/src/app/pane-container.coffee @@ -37,7 +37,13 @@ class PaneContainer extends View @children().first().view() getPanes: -> - @find('.pane').toArray().map (node)-> $(node).view() + @find('.pane').views() + + indexOfPane: (pane) -> + @getPanes().indexOf(pane.view()) + + paneAtIndex: (index) -> + @getPanes()[index] eachPane: (callback) -> callback(pane) for pane in @getPanes() diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 4515f50e2..83bedbf86 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -171,6 +171,12 @@ class RootView extends View eachPane: (callback) -> @panes.eachPane(callback) + getPanes: -> + @panes.getPanes() + + indexOfPane: (pane) -> + @panes.indexOfPane(pane) + eachEditor: (callback) -> callback(editor) for editor in @getEditors() @on 'editor:attached', (e, editor) -> callback(editor) @@ -181,10 +187,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 From 2e2ff3a1d094f96a2f243e37ac7f396cecbd157b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 26 Feb 2013 14:19:33 -0700 Subject: [PATCH 167/308] Add Pane.destroyItem and rename removeActiveItem -> destroyActiveItem Pane.removeItem removes an item, but no longer tries to call destroy on it. This will facilitate moving items between panes. --- spec/app/pane-spec.coffee | 14 +++++++++----- src/app/pane.coffee | 12 ++++++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 884399bf8..bf2420eaa 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -88,6 +88,12 @@ describe "Pane", -> expect(pane.itemViews.find('#view-2')).toExist() expect(pane.activeView).toBe view2 + describe ".destroyItem(item)", -> + it "removes the item and destroys it if it's a model", -> + pane.destroyItem(editSession2) + expect(pane.getItems().indexOf(editSession2)).toBe -1 + expect(editSession2.destroyed).toBeTruthy() + describe ".removeItem(item)", -> it "removes the item from the items list and shows the next item if it was showing", -> pane.removeItem(view1) @@ -124,10 +130,6 @@ describe "Pane", -> pane.removeItem(editSession1) expect(pane.itemViews.find('.editor')).not.toExist() - it "calls destroy on the model", -> - pane.removeItem(editSession2) - expect(editSession2.destroyed).toBeTruthy() - 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") @@ -152,13 +154,15 @@ describe "Pane", -> itemMovedHandler.reset() describe "core:close", -> - it "removes the active item and does not bubble the event", -> + 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() diff --git a/src/app/pane.coffee b/src/app/pane.coffee index fecf57fc5..83a970949 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -20,7 +20,7 @@ class Pane extends View @viewsByClassName = {} @showItem(@items[0]) - @command 'core:close', @removeActiveItem + @command 'core:close', @destroyActiveItem @command 'pane:show-next-item', @showNextItem @command 'pane:show-previous-item', @showPreviousItem @command 'pane:split-left', => @splitLeft() @@ -89,17 +89,21 @@ class Pane extends View @trigger 'pane:item-added', [item, index] item - removeActiveItem: => - @removeItem(@activeItem) + destroyActiveItem: => + @destroyItem(@activeItem) false + destroyItem: (item) -> + @removeItem(item) + item.destroy?() + removeItem: (item) -> index = @items.indexOf(item) return if index == -1 @showNextItem() if item is @activeItem and @items.length > 1 _.remove(@items, item) - item.destroy?() + @cleanupItemView(item) @trigger 'pane:item-removed', [item, index] @remove() unless @items.length From 7aba839dacf516934f55df60fb349119e213398e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 26 Feb 2013 14:23:36 -0700 Subject: [PATCH 168/308] Fix exception when pane items with no view are removed from the pane --- src/app/pane.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 83a970949..ad2521f3d 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -124,7 +124,7 @@ class Pane extends View viewClass = item.getViewClass() otherItemsForView = @items.filter (i) -> i.getViewClass?() is viewClass unless otherItemsForView.length - @viewsByClassName[viewClass.name].remove() + @viewsByClassName[viewClass.name]?.remove() delete @viewsByClassName[viewClass.name] viewForItem: (item) -> From 47621bd3b2cc9c3e51b3f9f178da7bcd0cd208ac Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 26 Feb 2013 14:25:00 -0700 Subject: [PATCH 169/308] Call Pane.destroyItem when close icon is clicked on a tab --- src/packages/tabs/lib/tab-bar-view.coffee | 2 +- src/packages/tabs/spec/tabs-spec.coffee | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/packages/tabs/lib/tab-bar-view.coffee b/src/packages/tabs/lib/tab-bar-view.coffee index 930ecc981..0372052a7 100644 --- a/src/packages/tabs/lib/tab-bar-view.coffee +++ b/src/packages/tabs/lib/tab-bar-view.coffee @@ -39,7 +39,7 @@ class TabBarView extends SortableList @on 'click', '.tab .close-icon', (e) => tab = $(e.target).closest('.tab').view() - @pane.removeItem(tab.item) + @pane.destroyItem(tab.item) false @pane.prepend(this) diff --git a/src/packages/tabs/spec/tabs-spec.coffee b/src/packages/tabs/spec/tabs-spec.coffee index 089b76c5b..5f528bad5 100644 --- a/src/packages/tabs/spec/tabs-spec.coffee +++ b/src/packages/tabs/spec/tabs-spec.coffee @@ -89,12 +89,13 @@ fdescribe "TabBarView", -> expect(pane.focus.callCount).toBe 2 describe "when a tab's close icon is clicked", -> - it "removes the tab's item from the pane", -> - tabBar.tabForItem(item1).find('.close-icon').click() + 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(item1)).toBe -1 + expect(pane.getItems().indexOf(editSession1)).toBe -1 + expect(editSession1.destroyed).toBeTruthy() expect(tabBar.getTabs().length).toBe 2 - expect(tabBar.find('.tab:contains(Item 1)')).not.toExist() + expect(tabBar.find('.tab:contains(sample.js)')).not.toExist() describe "when a tab item's title changes", -> it "updates the title of the item's tab", -> From 28141e315eb8440bce28d4bf5d511764caaa290a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 26 Feb 2013 19:05:10 -0700 Subject: [PATCH 170/308] Make shouldAllowDrag method work properly --- src/app/sortable-list.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/sortable-list.coffee b/src/app/sortable-list.coffee index 08d727646..f105e834b 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' From 916c5caa3a8c37b2688268c234c50a9f27dd3d32 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 26 Feb 2013 19:05:53 -0700 Subject: [PATCH 171/308] :lipstick: --- src/app/sortable-list.coffee | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/sortable-list.coffee b/src/app/sortable-list.coffee index f105e834b..ebdc675a1 100644 --- a/src/app/sortable-list.coffee +++ b/src/app/sortable-list.coffee @@ -47,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') From 9655fa8898526e17bd7a15e4dbe3e0ad22239530 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 26 Feb 2013 19:06:28 -0700 Subject: [PATCH 172/308] Implement shouldAllowDrag in positive logic for tabs --- src/packages/tabs/lib/tab-bar-view.coffee | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/packages/tabs/lib/tab-bar-view.coffee b/src/packages/tabs/lib/tab-bar-view.coffee index 0372052a7..4aed71f65 100644 --- a/src/packages/tabs/lib/tab-bar-view.coffee +++ b/src/packages/tabs/lib/tab-bar-view.coffee @@ -86,9 +86,8 @@ class TabBarView extends SortableList 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) + shouldAllowDrag: -> + (@paneContainer.getPanes().length > 1) or (@pane.getItems().length > 1) onDragStart: (event) => super From 0238061fa2adf3e25017c1cc2ecb24feb1928876 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 26 Feb 2013 19:07:19 -0700 Subject: [PATCH 173/308] Make tab drag & drop work with new panes system --- src/app/pane.coffee | 4 + src/packages/tabs/lib/tab-bar-view.coffee | 43 +++--- src/packages/tabs/spec/tabs-spec.coffee | 153 +++++++++++++--------- 3 files changed, 111 insertions(+), 89 deletions(-) diff --git a/src/app/pane.coffee b/src/app/pane.coffee index ad2521f3d..ba4488c6d 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -114,6 +114,10 @@ class Pane extends View @items.splice(newIndex, 0, item) @trigger 'pane:item-moved', [item, newIndex] + moveItemToPane: (item, pane, index) -> + @removeItem(item) + pane.addItem(item, index) + itemForPath: (path) -> _.detect @items, (item) -> item.getPath?() is path diff --git a/src/packages/tabs/lib/tab-bar-view.coffee b/src/packages/tabs/lib/tab-bar-view.coffee index 4aed71f65..aa4fb47f7 100644 --- a/src/packages/tabs/lib/tab-bar-view.coffee +++ b/src/packages/tabs/lib/tab-bar-view.coffee @@ -10,6 +10,8 @@ class TabBarView extends SortableList initialize: (@pane) -> super + + @paneContainer = @pane.getContainer() @addTabForItem(item) for item in @pane.getItems() @pane.on 'pane:item-added', (e, item, index) => @addTabForItem(item, index) @@ -91,39 +93,26 @@ class TabBarView extends SortableList onDragStart: (event) => super - pane = $(event.target).closest('.pane') - paneIndex = rootView.indexOfPane(pane) + paneIndex = @paneContainer.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' + 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 - 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(".#{TabBarView.viewClass()} .sortable:eq(#{previousDraggedTabIndex})") - - if draggedTab.is(droppedNearTab) - fromEditor.focus() - return - - if fromPaneIndex == toPaneIndex - droppedNearTab = @getSortableElement(event) - fromIndex = draggedTab.index() - toIndex = droppedNearTab.index() + if toPane is fromPane toIndex++ if fromIndex > toIndex - fromEditor.moveEditSessionToIndex(fromIndex, toIndex) - fromEditor.focus() + toPane.moveItem(item, toIndex) 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() + fromPane.moveItemToPane(item, toPane, toIndex) + toPane.showItem(item) + toPane.focus() diff --git a/src/packages/tabs/spec/tabs-spec.coffee b/src/packages/tabs/spec/tabs-spec.coffee index 5f528bad5..7a70d19f9 100644 --- a/src/packages/tabs/spec/tabs-spec.coffee +++ b/src/packages/tabs/spec/tabs-spec.coffee @@ -21,16 +21,19 @@ describe "Tabs package main", -> expect(rootView.find('.pane').length).toBe 2 expect(rootView.panes.find('.pane > .tabs').length).toBe 2 -fdescribe "TabBarView", -> +describe "TabBarView", -> [item1, item2, editSession1, pane, tabBar] = [] class TestView extends View + @deserialize: ({title, longTitle}) -> new TestView(title, longTitle) @content: (title) -> @div title - initialize: (@title) -> + initialize: (@title, @longTitle) -> getTitle: -> @title getLongTitle: -> @longTitle + serialize: -> { deserializer: 'TestView', @title, @longTitle } beforeEach -> + registerDeserializer(TestView) item1 = new TestView('Item 1') item2 = new TestView('Item 2') editSession1 = project.buildEditSession('sample.js') @@ -40,6 +43,9 @@ fdescribe "TabBarView", -> paneContainer.append(pane) tabBar = new TabBarView(pane) + afterEach -> + unregisterDeserializer(TestView) + describe ".initialize(pane)", -> it "creates a tab for each item on the tab bar's parent pane", -> expect(pane.getItems().length).toBe 3 @@ -131,75 +137,98 @@ fdescribe "TabBarView", -> 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() From d69335f08dad2c1b00c2acb18f26faf5ab81fb70 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 26 Feb 2013 19:07:42 -0700 Subject: [PATCH 174/308] Kill dead code --- src/packages/tabs/lib/tab-bar-view.coffee | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/packages/tabs/lib/tab-bar-view.coffee b/src/packages/tabs/lib/tab-bar-view.coffee index aa4fb47f7..fbbab1330 100644 --- a/src/packages/tabs/lib/tab-bar-view.coffee +++ b/src/packages/tabs/lib/tab-bar-view.coffee @@ -21,19 +21,6 @@ class TabBarView extends SortableList @updateActiveTab() -# @setActiveTab(@editor.getActiveEditSessionIndex()) - -# @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) - @on 'click', '.tab', (e) => tab = $(e.target).closest('.tab').view() @pane.showItem(tab.item) @@ -81,13 +68,6 @@ class TabBarView extends SortableList updateActiveTab: -> @setActiveTab(@tabForItem(@pane.activeItem)) - removeTabAtIndex: (index) -> - @find(".tab:eq(#{index})").remove() - - containsEditSession: (editor, editSession) -> - for session in editor.editSessions - return true if editSession.getPath() is session.getPath() - shouldAllowDrag: -> (@paneContainer.getPanes().length > 1) or (@pane.getItems().length > 1) From fe0d3cad36b1bf1d6c85d2954f37da5417f154ed Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 27 Feb 2013 12:25:02 -0700 Subject: [PATCH 175/308] Remove multiple edit session handling from Editor --- spec/app/editor-spec.coffee | 217 ++++++++++++------------------------ src/app/editor.coffee | 163 ++++----------------------- 2 files changed, 91 insertions(+), 289 deletions(-) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index a5cf751cb..0f9334ade 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -9,33 +9,41 @@ _ = require 'underscore' fs = require 'fs' describe "Editor", -> - [buffer, editor, cachedLineHeight] = [] + [buffer, editor, editSession, cachedLineHeight, cachedCharWidth] = [] beforeEach -> - editor = new Editor(project.buildEditSession('sample.js')) - buffer = editor.getBuffer() + 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(@charWidth * widthInChars) if widthInChars + this.width(getCharWidth() * widthInChars) if widthInChars $('#jasmine-content').append(this) - getLineHeight = -> return cachedLineHeight if cachedLineHeight? + calcDimensions() + cachedLineHeight + + getCharWidth = -> + return cachedCharWidth if cachedCharWidth? + calcDimensions() + cachedCharWidth + + calcDimensions = -> editorForMeasurement = new Editor(editSession: project.buildEditSession('sample.js')) editorForMeasurement.attachToDom() cachedLineHeight = editorForMeasurement.lineHeight + cachedCharWidth = editorForMeasurement.charWidth editorForMeasurement.remove() - cachedLineHeight 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 "when the editor is attached to the dom", -> it "calculates line height and char width and updates the pixel position of the cursor", -> @@ -92,147 +100,67 @@ 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.buildEditSession(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 + expect(editor.activeEditSession.destroyed).toBeTruthy() describe ".edit(editSession)", -> - otherEditSession = null + [newEditSession, newBuffer] = [] beforeEach -> - otherEditSession = project.buildEditSession() + 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 + console.log editor.getCursorView().css('left') + 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' - - describe "switching edit sessions", -> - [session0, session1, session2] = [] - - beforeEach -> - session0 = editor.activeEditSession - - editor.edit(project.buildEditSession('sample.txt')) - session1 = editor.activeEditSession - - editor.edit(project.buildEditSession('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.buildEditSession(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 + runs -> + expect(atom.confirm).toHaveBeenCalled() describe ".save()", -> describe "when the current buffer has a path", -> @@ -1817,11 +1745,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.buildEditSession(null)) + emptyEditSession = fixturesProject.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", -> @@ -2040,14 +1971,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.buildEditSession('sample.txt')) - editor.edit(project.buildEditSession('two-hundred.txt')) - editor.edit(project.buildEditSession()) - 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() diff --git a/src/app/editor.coffee b/src/app/editor.coffee index d2ece3c63..bb7ad1ee1 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -49,8 +49,6 @@ class Editor extends View lineCache: null isFocused: false activeEditSession: null - closedEditSessions: null - editSessions: null attached: false lineOverdraw: 10 pendingChanges: null @@ -59,10 +57,7 @@ class Editor extends View 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 = new Editor(mini: state.mini, editSession: deserialize(state.editSession)) editor.isFocused = state.isFocused editor @@ -70,7 +65,7 @@ class Editor extends View if editSessionOrOptions instanceof EditSession editSession = editSessionOrOptions else - {editSession, @mini, deserializing} = (editSessionOrOptions ? {}) + {editSession, @mini} = (editSessionOrOptions ? {}) requireStylesheet 'editor.css' @@ -81,8 +76,6 @@ class Editor extends View @handleEvents() @cursorViews = [] @selectionViews = [] - @editSessions = [] - @closedEditSessions = [] @pendingChanges = [] @newCursors = [] @newSelections = [] @@ -96,14 +89,13 @@ 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") + else + throw new Error("Must supply an EditSession or mini: true") serialize: -> @saveScrollPositionForActiveEditSession() deserializer: "Editor" - editSessions: @editSessions.map (session) -> session.serialize() - activeEditSessionIndex: @getActiveEditSessionIndex() + editSession: @activeEditSession.serialize() isFocused: @isFocused copy: -> @@ -171,20 +163,9 @@ class Editor extends View 'editor:fold-current-row': @foldCurrentRow 'editor:unfold-current-row': @unfoldCurrentRow 'editor:fold-selection': @foldSelection - '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: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 @@ -434,10 +415,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 @@ -479,92 +457,17 @@ class Editor extends View @trigger 'editor:attached', [this] edit: (editSession) -> - index = @editSessions.indexOf(editSession) - index = @pushEditSession(editSession) if index == -1 - @setActiveEditSessionIndex(index) - - getModel: -> - @activeEditSession - - setModel: (editSession) -> - @edit(editSession) - - 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", => @@ -575,11 +478,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( @@ -589,30 +499,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() @@ -819,17 +705,10 @@ class Editor extends View 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] From fab3b4564ece9d634fb0154a34d7e87379199907 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 27 Feb 2013 12:36:27 -0700 Subject: [PATCH 176/308] Add Pane.moveItemToPane specs. Fix bug moving the last edit session. --- spec/app/pane-spec.coffee | 25 +++++++++++++++++++++++++ src/app/editor.coffee | 2 +- src/app/pane.coffee | 4 +++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index bf2420eaa..d680624f2 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -153,6 +153,31 @@ describe "Pane", -> 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") diff --git a/src/app/editor.coffee b/src/app/editor.coffee index bb7ad1ee1..9819aecea 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -705,7 +705,7 @@ class Editor extends View afterRemove: -> @removed = true - @activeEditSession.destroy() + @activeEditSession?.destroy() $(window).off(".editor-#{@id}") $(document).off(".editor-#{@id}") diff --git a/src/app/pane.coffee b/src/app/pane.coffee index ba4488c6d..e88ccb7c6 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -128,7 +128,9 @@ class Pane extends View viewClass = item.getViewClass() otherItemsForView = @items.filter (i) -> i.getViewClass?() is viewClass unless otherItemsForView.length - @viewsByClassName[viewClass.name]?.remove() + view = @viewsByClassName[viewClass.name] + view?.setModel(null) + view?.remove() delete @viewsByClassName[viewClass.name] viewForItem: (item) -> From 2bfc73afaaac5895016933c6bab0dc134bc14087 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 27 Feb 2013 14:15:38 -0700 Subject: [PATCH 177/308] These should have been renamed w/ currentView/Item -> activeView/Item --- spec/app/root-view-spec.coffee | 16 ++++++++-------- src/app/editor.coffee | 8 ++++---- src/app/pane-container.coffee | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index cc56ee87e..aeede0fca 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -217,15 +217,15 @@ describe "RootView", -> 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 "opens and returns an edit session for an empty buffer in a new editor", -> editSession = rootView.open() - expect(rootView.getActivePane().currentItem).toBe editSession + expect(rootView.getActivePane().activeItem).toBe editSession expect(editSession.getPath()).toBeUndefined() describe "when called with a path", -> it "opens a buffer with the given path in a new editor", -> editSession = rootView.open('b') - expect(rootView.getActivePane().currentItem).toBe editSession + expect(rootView.getActivePane().activeItem).toBe editSession expect(editSession.getPath()).toBe require.resolve('fixtures/dir/b') describe "when there is an active pane", -> @@ -238,26 +238,26 @@ describe "RootView", -> it "opens an edit session with an empty buffer in the active pane", -> editSession = rootView.open() expect(activePane.getItems().length).toBe initialItemCount + 1 - expect(activePane.currentItem).toBe editSession + expect(activePane.activeItem).toBe editSession expect(editSession.getPath()).toBeUndefined() describe "when called with a path", -> 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.currentItem + previousEditSession = activePane.activeItem editSession = rootView.open('b') - expect(activePane.currentItem).toBe editSession + expect(activePane.activeItem).toBe editSession editSession = rootView.open('a') expect(editSession).not.toBe previousEditSession - expect(activePane.currentItem).toBe editSession + expect(activePane.activeItem).toBe editSession 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.currentItem).toBe editSession + expect(activePane.activeItem).toBe editSession describe ".saveAll()", -> it "saves all open editors", -> diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 9819aecea..1b4e8fe28 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -672,16 +672,16 @@ class Editor extends View @requestDisplayUpdate() splitLeft: (editSession) -> - @pane()?.splitLeft().currentView + @pane()?.splitLeft().activeView splitRight: (editSession) -> - @pane()?.splitRight().currentView + @pane()?.splitRight().activeView splitUp: (editSession) -> - @pane()?.splitUp().currentView + @pane()?.splitUp().activeView splitDown: (editSession) -> - @pane()?.splitDown().currentView + @pane()?.splitDown().activeView pane: -> @closest('.pane').view() diff --git a/src/app/pane-container.coffee b/src/app/pane-container.coffee index 1e179c085..12c88cc09 100644 --- a/src/app/pane-container.coffee +++ b/src/app/pane-container.coffee @@ -58,10 +58,10 @@ class PaneContainer extends View @find('.pane.active').view() ? @find('.pane:first').view() getActivePaneItem: -> - @getActivePane()?.currentItem + @getActivePane()?.activeItem getActiveView: -> - @getActivePane()?.currentView + @getActivePane()?.activeView adjustPaneDimensions: -> if root = @getRoot() From 279ebc095886827c8d3f4aa56e4f89af6142bfab Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 27 Feb 2013 14:23:33 -0700 Subject: [PATCH 178/308] Make RootView.getModifiedBuffers work w/ new system Eventually, this should probably become getModifiedPaneItems so that all kinds of items are given an opportunity to participate in the saving system. --- src/app/root-view.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 83bedbf86..1a0b59a44 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -11,6 +11,7 @@ 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 @@ -139,10 +140,9 @@ class RootView extends View 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: -> From a2ddd10d313c176312345d6d0e12a691afab1ef4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 27 Feb 2013 15:25:16 -0700 Subject: [PATCH 179/308] Get fuzzy-finder specs passing w/ new panes Could still probably use some cleanup and I'm not sure everything is working correctly with regards to focus. --- src/app/root-view.coffee | 1 + .../fuzzy-finder/lib/fuzzy-finder-view.coffee | 33 ++-- .../spec/fuzzy-finder-spec.coffee | 171 +++++++++--------- 3 files changed, 97 insertions(+), 108 deletions(-) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 1a0b59a44..cdb14c3dd 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -89,6 +89,7 @@ class RootView extends View @remove() open: (path, options = {}) -> + path = project.resolve(path) if path? if activePane = @getActivePane() if editSession = activePane.itemForPath(path) activePane.showItem(editSession) diff --git a/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee b/src/packages/fuzzy-finder/lib/fuzzy-finder-view.coffee index 480ecab33..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.buildEditSession(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 469b0fe96..423827379 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,9 +72,9 @@ 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') @@ -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", -> @@ -156,24 +147,21 @@ describe 'FuzzyFinder', -> 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.buildEditSession()) - 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,15 +170,15 @@ 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", -> + describe "when the active pane has an item for the selected path", -> + it "switches to the item for the selected path", -> expectedPath = fixturesProject.resolve('sample.txt') finderView.confirmed('sample.txt') @@ -199,21 +187,20 @@ 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') 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] = [] @@ -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') From 5b0f5727dc0435293df3664121bcd471012c8c43 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 27 Feb 2013 15:37:59 -0700 Subject: [PATCH 180/308] Fix GFM grammar spec --- src/packages/gfm.tmbundle/spec/gfm-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/gfm.tmbundle/spec/gfm-spec.coffee b/src/packages/gfm.tmbundle/spec/gfm-spec.coffee index db307ab65..8f07e640e 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 = fixturesProject.buildEditSession('gfm.md') editSession.insertNewlineBelow() expect(editSession.buffer.lineForRow(1)).toBe ' ' From 2790e5d12b2f946b264ed32ff49cdf5add6358bb Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 27 Feb 2013 15:38:12 -0700 Subject: [PATCH 181/308] Fix package generator spec --- .../package-generator/spec/package-generator-spec.coffee | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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") From 8dc3afbccedab36fe7125152093e33ccebd06bf3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 27 Feb 2013 15:38:20 -0700 Subject: [PATCH 182/308] Fix snippets spec --- src/packages/snippets/spec/snippets-spec.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/packages/snippets/spec/snippets-spec.coffee b/src/packages/snippets/spec/snippets-spec.coffee index c6e997d35..45701ccf3 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() From 2b53655934bddefdd9223a230c5fceffc99a9ac4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 27 Feb 2013 15:38:43 -0700 Subject: [PATCH 183/308] Fix status bar spec --- src/packages/status-bar/spec/status-bar-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/status-bar/spec/status-bar-spec.coffee b/src/packages/status-bar/spec/status-bar-spec.coffee index f2d73b07b..8f22d6d31 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() From 7145136cd9b58e21a651e58331826828555d9752 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 27 Feb 2013 15:39:27 -0700 Subject: [PATCH 184/308] Fix symbols view Makes a lot of assumptions about getActiveView being an editor. We'll need to revisit this. --- .../symbols-view/lib/symbols-view.coffee | 6 ++--- .../spec/symbols-view-spec.coffee | 22 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) 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() From 31f7d6669f060639dc5090b32843365a596e1aff Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 27 Feb 2013 16:20:33 -0700 Subject: [PATCH 185/308] Use project global in project spec --- spec/app/project-spec.coffee | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index db5eaaf92..ad5b01151 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -3,12 +3,8 @@ fs = require 'fs' _ = require 'underscore' describe "Project", -> - project = null beforeEach -> - project = new Project(require.resolve('fixtures/dir')) - - afterEach -> - project.destroy() + project.setPath(project.resolve('dir')) describe "when editSession is destroyed", -> it "removes edit session and calls destroy on buffer (if buffer is not referenced by other edit sessions)", -> From 9a93694a4c7e80fe179fb0a68a75825b053aa7d7 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 27 Feb 2013 16:20:43 -0700 Subject: [PATCH 186/308] :lipstick: --- spec/app/project-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index ad5b01151..143d83d8c 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -6,7 +6,7 @@ describe "Project", -> beforeEach -> project.setPath(project.resolve('dir')) - 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.buildEditSession("a") anotherEditSession = project.buildEditSession("a") From 5291924bcc1d22c40d8ff3321a4285b467215d76 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 27 Feb 2013 16:21:42 -0700 Subject: [PATCH 187/308] Set the project path when its first edit session is saved --- spec/app/project-spec.coffee | 10 ++++++++++ src/app/edit-session.coffee | 1 + 2 files changed, 11 insertions(+) diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee index 143d83d8c..136e61a85 100644 --- a/spec/app/project-spec.coffee +++ b/spec/app/project-spec.coffee @@ -20,6 +20,16 @@ describe "Project", -> anotherEditSession.destroy() expect(project.editSessions.length).toBe 0 + 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 -> diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index f6e6b344a..1c065611d 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -44,6 +44,7 @@ class EditSession @buffer.retain() @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" From 6157a75868ad5d3d167632febe3d0c0c782e6fe0 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 27 Feb 2013 16:54:06 -0700 Subject: [PATCH 188/308] Spec changeFocus option of RootView.open and default it to true --- spec/app/root-view-spec.coffee | 30 +++++++++++++++++++++++++----- src/app/root-view.coffee | 3 ++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index aeede0fca..450f80a36 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -4,6 +4,7 @@ Project = require 'project' RootView = require 'root-view' Buffer = require 'buffer' Editor = require 'editor' +Pane = require 'pane' {View, $$} = require 'space-pen' describe "RootView", -> @@ -213,33 +214,43 @@ describe "RootView", -> describe ".open(path, options)", -> describe "when there is no active pane", -> beforeEach -> + spyOn(Pane.prototype, 'focus') rootView.getActivePane().remove() expect(rootView.getActivePane()).toBeUndefined() describe "when called with no path", -> - it "opens and 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.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.getActivePane().activeItem).toBe editSession expect(editSession.getPath()).toBe require.resolve('fixtures/dir/b') + expect(rootView.getActivePane().focus).toHaveBeenCalled() + + 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 -> activePane = rootView.getActivePane() + spyOn(activePane, 'focus') initialItemCount = activePane.getItems().length describe "when called with no path", -> - it "opens an edit session with an empty buffer in the active pane", -> + it "opens an edit session with an empty buffer as an item on the active pane and focuses it", -> editSession = rootView.open() 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", -> describe "when the active pane already has an edit session item for the path being opened", -> @@ -248,16 +259,25 @@ describe "RootView", -> editSession = rootView.open('b') expect(activePane.activeItem).toBe editSession - - editSession = rootView.open('a') expect(editSession).not.toBe previousEditSession + + editSession = rootView.open(previousEditSession.getPath()) + expect(editSession).toBe previousEditSession expect(activePane.activeItem).toBe editSession + expect(activePane.focus).toHaveBeenCalled() + 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 changeFocus option is false", -> + it "does not focus the active pane", -> + editSession = rootView.open('b', changeFocus: false) + expect(activePane.focus).not.toHaveBeenCalled() describe ".saveAll()", -> it "saves all open editors", -> diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index cdb14c3dd..094e9bf0b 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -89,6 +89,7 @@ class RootView extends View @remove() open: (path, options = {}) -> + changeFocus = options.changeFocus ? true path = project.resolve(path) if path? if activePane = @getActivePane() if editSession = activePane.itemForPath(path) @@ -101,7 +102,7 @@ class RootView extends View activePane = new Pane(editSession) @panes.append(activePane) - activePane.focus() if options.changeFocus + activePane.focus() if changeFocus editSession editorFocused: (editor) -> From 3c9793d80384ab65e001dac0e184a02d3d4b63d8 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 27 Feb 2013 16:57:05 -0700 Subject: [PATCH 189/308] Fix TreeView specs --- src/packages/tree-view/lib/tree-view.coffee | 9 +-- src/packages/tree-view/lib/tree.coffee | 2 +- .../tree-view/spec/tree-view-spec.coffee | 55 +++++++++---------- 3 files changed, 33 insertions(+), 33 deletions(-) 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] = [] From 80859b0a9fffcb3b77114c11572378530acf6128 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 27 Feb 2013 17:04:11 -0700 Subject: [PATCH 190/308] Fix CSS for .file-name -> .title class rename --- static/tabs.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/tabs.css b/static/tabs.css index 29133287a..dc1aa6988 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; From 52b649dca50d7f8efd74e3a448717e3005aac799 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 27 Feb 2013 18:17:52 -0700 Subject: [PATCH 191/308] Preserve focus when switching between pane items If the pane is currently focused, when showing a view associated with a new item, focus that view. --- spec/app/pane-spec.coffee | 9 +++++++++ src/app/pane.coffee | 2 ++ 2 files changed, 11 insertions(+) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index d680624f2..78ae84e22 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -44,6 +44,15 @@ describe "Pane", -> 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 -> diff --git a/src/app/pane.coffee b/src/app/pane.coffee index e88ccb7c6..0f5e9d96b 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -73,11 +73,13 @@ class Pane extends View showItem: (item) -> return if item is @activeItem + isFocused = @is(':has(:focus)') @addItem(item) 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] From ae95c04bbce05ce703dabfc71c6ab3e8704ded96 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 27 Feb 2013 18:21:24 -0700 Subject: [PATCH 192/308] Focus next pane when removing the last pane item of a focused pane Previously, removing the last pane item also ruined our ability to determine if the pane had focus. Now, if we're removing the last item, we instead just go ahead and remove the entire pane. Remove contains logic to switch focus to the next pane if its active view is focused, which works as intended if we leave the active view in place. --- spec/app/pane-spec.coffee | 16 +++++++++++++--- src/app/pane.coffee | 23 +++++++++++++++-------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 78ae84e22..987258ae7 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -121,9 +121,19 @@ describe "Pane", -> expect(itemRemovedHandler).toHaveBeenCalled() expect(itemRemovedHandler.argsForCall[0][1..2]).toEqual [editSession1, 1] - it "removes the pane when its last item is removed", -> - pane.removeItem(item) for item in pane.getItems() - expect(pane.hasParent()).toBeFalsy() + 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", -> diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 0f5e9d96b..0c4886f76 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -105,10 +105,8 @@ class Pane extends View @showNextItem() if item is @activeItem and @items.length > 1 _.remove(@items, item) - @cleanupItemView(item) @trigger 'pane:item-removed', [item, index] - @remove() unless @items.length moveItem: (item, newIndex) -> oldIndex = @items.indexOf(item) @@ -125,16 +123,20 @@ class Pane extends View cleanupItemView: (item) -> if item instanceof $ - item.remove() + viewToRemove = item else viewClass = item.getViewClass() otherItemsForView = @items.filter (i) -> i.getViewClass?() is viewClass unless otherItemsForView.length - view = @viewsByClassName[viewClass.name] - view?.setModel(null) - view?.remove() + 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 @@ -197,19 +199,24 @@ class Pane extends View remove: (selector, keepData) -> return super if keepData + # find parent elements before removing from dom container = @getContainer() parentAxis = @parent('.row, .column') + if @is(':has(:focus)') - rootView?.focus() unless container.focusNextPane() + container.focusNextPane() or rootView?.focus() else if @isActive() container.makeNextPaneActive() super if parentAxis.children().length == 1 - sibling = parentAxis.children().detach() + sibling = parentAxis.children() + siblingFocused = sibling.is(':has(:focus)') + sibling.detach() parentAxis.replaceWith(sibling) + sibling.focus() if siblingFocused container.adjustPaneDimensions() container.trigger 'pane:removed', [this] From 3bf31e440db19086b54f5e46b57f06990140ee35 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 27 Feb 2013 18:22:21 -0700 Subject: [PATCH 193/308] Remove code for setting the active editor from root view Supplanted by "active pane" --- src/app/editor.coffee | 1 - src/app/root-view.coffee | 20 -------------------- 2 files changed, 21 deletions(-) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 1b4e8fe28..2cbe1bf00 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -346,7 +346,6 @@ class Editor extends View false @hiddenInput.on 'focus', => - rootView?.editorFocused(this) @isFocused = true @addClass 'is-focused' diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 094e9bf0b..1e4d349db 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -105,26 +105,6 @@ class RootView extends View activePane.focus() if changeFocus 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() - - updateTitle: -> if projectPath = project.getPath() if item = @getActivePaneItem() From c1e226d6a388ca396236c752ad355d6c7e0aecd2 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 27 Feb 2013 18:22:32 -0700 Subject: [PATCH 194/308] Kill unused event --- src/app/editor.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 2cbe1bf00..d82734fdc 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -171,7 +171,6 @@ class Editor extends View '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 From 15144514bb86e78404f23f6edadd18da01ec5420 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 27 Feb 2013 18:26:21 -0700 Subject: [PATCH 195/308] Don't update editor's display if its edit session is null or destroyed --- src/app/editor.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index d82734fdc..736d745f9 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -811,7 +811,8 @@ class Editor extends View @pendingDisplayUpdate = false updateDisplay: (options={}) -> - return unless @attached + return unless @attached and @activeEditSession + return if @activeEditSession.destroyed @updateRenderedLines() @highlightCursorLine() @updateCursorViews() From 5d9e20afa4899d41cbb1b01741e58991fe844c30 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 27 Feb 2013 18:26:46 -0700 Subject: [PATCH 196/308] Make Editor.getPath return null if edit session is null --- src/app/editor.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 736d745f9..7cf99e066 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -321,7 +321,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) From 5bba4cd9f7f30d193040364b5f2687d502f95935 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 27 Feb 2013 18:29:08 -0700 Subject: [PATCH 197/308] Kill dead tab view code --- src/packages/tabs/lib/tab-view.coffee | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/packages/tabs/lib/tab-view.coffee b/src/packages/tabs/lib/tab-view.coffee index ab75a9c03..27eceb727 100644 --- a/src/packages/tabs/lib/tab-view.coffee +++ b/src/packages/tabs/lib/tab-view.coffee @@ -12,15 +12,6 @@ class TabView extends View initialize: (@item, @pane) -> @item.on? 'title-changed', => @updateTitle() @updateTitle() -# @buffer = @editSession.buffer -# @subscribe @buffer, 'path-changed', => @updateFileName() -# @subscribe @buffer, 'contents-modified', => @updateModifiedStatus() -# @subscribe @buffer, 'saved', => @updateModifiedStatus() -# @subscribe @buffer, 'git-status-changed', => @updateModifiedStatus() -# @subscribe @editor, 'editor:edit-session-added', => @updateFileName() -# @subscribe @editor, 'editor:edit-session-removed', => @updateFileName() -# @updateFileName() -# @updateModifiedStatus() updateTitle: -> return if @updatingTitle From 29566d55c61fe8d46910333b722e127a37fae1af Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 28 Feb 2013 07:02:37 -0700 Subject: [PATCH 198/308] Scope split-view bindings on body so fuzzy-finder can open in splits Previously, they were scoped on .pane, but fuzzy-finder isn't inside a pane and still needs to be able to respond to split events. --- src/app/keymaps/atom.cson | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/app/keymaps/atom.cson b/src/app/keymaps/atom.cson index d3427fb45..4dbe961a1 100644 --- a/src/app/keymaps/atom.cson +++ b/src/app/keymaps/atom.cson @@ -30,16 +30,17 @@ 'ctrl-tab': 'window:focus-next-pane' 'ctrl-meta-f': 'window:toggle-full-screen' -'.pane': - 'meta-{': 'pane:show-previous-item' - 'meta-}': 'pane:show-next-item' - 'alt-meta-left': 'pane:show-previous-item' - 'alt-meta-right': 'pane:show-next-item' 'ctrl-|': 'pane:split-right' 'ctrl-w v': 'pane:split-right' 'ctrl--': 'pane:split-down' 'ctrl-w s': 'pane:split-down' +'.pane': + 'meta-{': 'pane:show-previous-item' + 'meta-}': 'pane:show-next-item' + 'alt-meta-left': 'pane:show-previous-item' + 'alt-meta-right': 'pane:show-next-item' + '.tool-panel': 'meta-escape': 'tool-panel:unfocus' 'escape': 'core:close' From 24c9f11cc96de1f064821e5112822a9a8f34ffa4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 28 Feb 2013 08:18:57 -0700 Subject: [PATCH 199/308] Trigger modified-status-changed on buffers/edit sessions --- spec/app/buffer-spec.coffee | 57 ++++++++++++++++++++++++-- src/app/buffer-change-operation.coffee | 2 +- src/app/buffer.coffee | 9 +++- src/app/edit-session.coffee | 1 + 4 files changed, 63 insertions(+), 6 deletions(-) diff --git a/spec/app/buffer-spec.coffee b/spec/app/buffer-spec.coffee index 3510d46f4..77c6845f4 100644 --- a/spec/app/buffer-spec.coffee +++ b/spec/app/buffer-spec.coffee @@ -180,23 +180,72 @@ describe 'Buffer', -> waitsFor 'change event', -> changeHandler.callCount > 0 - describe ".isModified()", -> - it "returns true when user changes buffer", -> + describe "modified status", -> + it "reports a modified status of true 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 a modified status of 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 a modified status of 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") expect(buffer.isModified()).toBe true + modifiedHandler.reset() buffer.save() + expect(modifiedHandler).toHaveBeenCalledWith(false) + expect(buffer.isModified()).toBe false + + it "reports a modified status of 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") + expect(buffer.isModified()).toBe true + modifiedHandler.reset() + + buffer.reload() + expect(modifiedHandler).toHaveBeenCalledWith(false) expect(buffer.isModified()).toBe false it "returns false for an empty buffer with no path", -> diff --git a/src/app/buffer-change-operation.coffee b/src/app/buffer-change-operation.coffee index b3ffda35e..a27539c77 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.scheduleModifiedStatusChangedEvent() @resumeMarkerObservation() @buffer.trigger 'markers-updated' diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee index 092f0d797..4958c03b9 100644 --- a/src/app/buffer.coffee +++ b/src/app/buffer.coffee @@ -69,6 +69,7 @@ class Buffer @file.on "removed", => @updateCachedDiskContents() + @trigger "modified-status-changed", @isModified() @trigger "contents-modified", {differsFromDisk: true} @file.on "moved", => @@ -78,6 +79,7 @@ class Buffer @trigger 'will-reload' @updateCachedDiskContents() @setText(@cachedDiskContents) + @trigger 'modified-status-changed', false @trigger 'reloaded' updateCachedDiskContents: -> @@ -252,6 +254,7 @@ class Buffer @setPath(path) @cachedDiskContents = @getText() @file.write(@getText()) + @trigger 'modified-status-changed', false @trigger 'saved' isModified: -> @@ -424,10 +427,14 @@ class Buffer return unless path git?.checkoutHead(path) - scheduleStoppedChangingEvent: -> + scheduleModifiedStatusChangedEvent: -> clearTimeout(@stoppedChangingTimeout) if @stoppedChangingTimeout stoppedChangingCallback = => @stoppedChangingTimeout = null + modifiedStatus = @isModified() + unless modifiedStatus is @previousModifiedStatus + @previousModifiedStatus = modifiedStatus + @trigger 'modified-status-changed', modifiedStatus @trigger 'contents-modified', {differsFromDisk: @isModified()} @stoppedChangingTimeout = setTimeout(stoppedChangingCallback, @stoppedChangingDelay) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 1c065611d..46c9094f3 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -49,6 +49,7 @@ class EditSession @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() From a1dc2cfc2d89218a8a0b4aafa1f897550db76575 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 28 Feb 2013 08:20:11 -0700 Subject: [PATCH 200/308] Tabs indicate when their items are modified --- src/packages/tabs/lib/tab-view.coffee | 8 +++++--- src/packages/tabs/spec/tabs-spec.coffee | 24 +++++++++++++++++++++++- static/tabs.css | 8 ++++---- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/packages/tabs/lib/tab-view.coffee b/src/packages/tabs/lib/tab-view.coffee index 27eceb727..9bfee397e 100644 --- a/src/packages/tabs/lib/tab-view.coffee +++ b/src/packages/tabs/lib/tab-view.coffee @@ -11,7 +11,9 @@ class TabView extends View initialize: (@item, @pane) -> @item.on? 'title-changed', => @updateTitle() + @item.on? 'modified-status-changed', => @updateModifiedStatus() @updateTitle() + @updateModifiedStatus() updateTitle: -> return if @updatingTitle @@ -32,11 +34,11 @@ class TabView extends View @siblings('.tab').views() updateModifiedStatus: -> - if @buffer.isModified() - @toggleClass('file-modified') unless @isModified + if @item.isModified?() + @addClass('modified') unless @isModified @isModified = true else - @removeClass('file-modified') if @isModified + @removeClass('modified') if @isModified @isModified = false updateFileName: -> diff --git a/src/packages/tabs/spec/tabs-spec.coffee b/src/packages/tabs/spec/tabs-spec.coffee index 7a70d19f9..d8d32e390 100644 --- a/src/packages/tabs/spec/tabs-spec.coffee +++ b/src/packages/tabs/spec/tabs-spec.coffee @@ -76,6 +76,12 @@ describe "TabBarView", -> 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) @@ -108,7 +114,7 @@ describe "TabBarView", -> editSession1.buffer.setPath('/this/is-a/test.txt') expect(tabBar.tabForItem(editSession1)).toHaveText 'test.txt' - describe "when two tabs have the same file name", -> + 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" @@ -126,6 +132,22 @@ describe "TabBarView", -> expect(tabBar.tabForItem(item1)).toHaveText "Grumpy Old Man" expect(tabBar.tabForItem(item2)).toHaveText "Old Man" + 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' + + editSession1.insertText('x') + advanceClock(editSession1.buffer.stoppedChangingDelay) + expect(editSession1.isModified()).toBeTruthy() + expect(tab).toHaveClass 'modified' + + editSession1.undo() + advanceClock(editSession1.buffer.stoppedChangingDelay) + expect(editSession1.isModified()).toBeFalsy() + expect(tab).not.toHaveClass 'modified' + 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"] diff --git a/static/tabs.css b/static/tabs.css index dc1aa6988..bb262bba1 100644 --- a/static/tabs.css +++ b/static/tabs.css @@ -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; } From 298a963148f2961b3e6a2d883515cb501ba7593d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 28 Feb 2013 08:35:32 -0700 Subject: [PATCH 201/308] Rework Buffer's 'contents-modified' event This event now fires whenever the content of the buffer changes (after a rate-limiting delay) with a single boolean indicating the modified status of the buffer. There's now a separate event called 'modified-status-changed' to indicate events that change the boolean value of the isModified method, so we don't need to fire 'contents-modified' when the underlying file is deleted for instance. --- spec/app/buffer-spec.coffee | 67 +++++-------------- src/app/buffer-change-operation.coffee | 2 +- src/app/buffer.coffee | 5 +- .../status-bar/lib/status-bar-view.coffee | 6 +- 4 files changed, 24 insertions(+), 56 deletions(-) diff --git a/spec/app/buffer-spec.coffee b/spec/app/buffer-spec.coffee index 77c6845f4..0b69b2fa1 100644 --- a/spec/app/buffer-spec.coffee +++ b/spec/app/buffer-spec.coffee @@ -1105,61 +1105,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/src/app/buffer-change-operation.coffee b/src/app/buffer-change-operation.coffee index a27539c77..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.scheduleModifiedStatusChangedEvent() + @buffer.scheduleModifiedEvents() @resumeMarkerObservation() @buffer.trigger 'markers-updated' diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee index 4958c03b9..e8ccc91db 100644 --- a/src/app/buffer.coffee +++ b/src/app/buffer.coffee @@ -70,7 +70,6 @@ class Buffer @file.on "removed", => @updateCachedDiskContents() @trigger "modified-status-changed", @isModified() - @trigger "contents-modified", {differsFromDisk: true} @file.on "moved", => @trigger "path-changed", this @@ -427,15 +426,15 @@ class Buffer return unless path git?.checkoutHead(path) - scheduleModifiedStatusChangedEvent: -> + scheduleModifiedEvents: -> clearTimeout(@stoppedChangingTimeout) if @stoppedChangingTimeout stoppedChangingCallback = => @stoppedChangingTimeout = null modifiedStatus = @isModified() + @trigger 'contents-modified', modifiedStatus unless modifiedStatus is @previousModifiedStatus @previousModifiedStatus = modifiedStatus @trigger 'modified-status-changed', modifiedStatus - @trigger 'contents-modified', {differsFromDisk: @isModified()} @stoppedChangingTimeout = setTimeout(stoppedChangingCallback, @stoppedChangingDelay) fileExists: -> 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 From 43b41e9ed96f96a5ad2fe9636e69f1977dcfedfe Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 28 Feb 2013 08:53:58 -0700 Subject: [PATCH 202/308] Fix spell check spec --- src/packages/spell-check/spec/spell-check-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/spell-check/spec/spell-check-spec.coffee b/src/packages/spell-check/spec/spell-check-spec.coffee index bf3fba917..b860e650c 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.") From d5654cf0df5cfc73d8e0ec71f04b08558168e8ec Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 28 Feb 2013 09:33:55 -0700 Subject: [PATCH 203/308] :lipstick: --- spec/app/editor-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index 0f9334ade..bf7140c65 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -271,7 +271,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" From 9f7b804a6ca35a3e93c8e2aa4f9f1907cba6a92b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 28 Feb 2013 09:46:34 -0700 Subject: [PATCH 204/308] Panes prompt to save modified items before destroying them --- spec/app/pane-spec.coffee | 68 ++++++++++++++++++++++++++++++++++++--- src/app/editor.coffee | 11 ------- src/app/pane.coffee | 29 +++++++++++++++-- 3 files changed, 91 insertions(+), 17 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 987258ae7..83e999bf9 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -98,10 +98,70 @@ describe "Pane", -> expect(pane.activeView).toBe view2 describe ".destroyItem(item)", -> - it "removes the item and destroys it if it's a model", -> - pane.destroyItem(editSession2) - expect(pane.getItems().indexOf(editSession2)).toBe -1 - expect(editSession2.destroyed).toBeTruthy() + 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 path", -> + 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 path", -> + it "presents a save-as dialog, then saves the item with the given path 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", -> diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 7cf99e066..85cc1298a 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -684,17 +684,6 @@ class Editor extends View pane: -> @closest('.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 - ) - remove: (selector, keepData) -> return super if keepData or @removed @trigger 'editor:will-be-removed' diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 0c4886f76..6829b33df 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -96,8 +96,33 @@ class Pane extends View false destroyItem: (item) -> - @removeItem(item) - item.destroy?() + reallyDestroyItem = => + @removeItem(item) + item.destroy?() + + if item.isModified?() + @promptToSaveItem(item, reallyDestroyItem) + else + reallyDestroyItem() + + promptToSaveItem: (item, nextAction) -> + path = item.getPath() + 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 + ) + + saveItem: (item, nextAction) -> + if item.getPath() + item.save() + nextAction() + else + atom.showSaveDialog (path) -> + item.saveAs(path) + nextAction() removeItem: (item) -> index = @items.indexOf(item) From 54fc9efdcbaa959eb27c236f93ffb7770bfd63b2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 28 Feb 2013 10:11:06 -0700 Subject: [PATCH 205/308] Eliminate fixturesProject global. Use project global instead. --- spec/app/display-buffer-spec.coffee | 4 ++-- spec/app/edit-session-spec.coffee | 15 ++++++--------- spec/app/editor-spec.coffee | 2 +- spec/app/language-mode-spec.coffee | 10 +++++----- spec/app/text-mate-grammar-spec.coffee | 2 +- spec/app/theme-spec.coffee | 6 +++--- spec/app/tokenized-buffer-spec.coffee | 12 ++++++------ spec/spec-helper.coffee | 4 ++-- .../autocomplete/spec/autocomplete-spec.coffee | 2 +- .../spec/command-interpreter-spec.coffee | 9 ++++----- .../fuzzy-finder/spec/fuzzy-finder-spec.coffee | 6 +++--- src/packages/gfm.tmbundle/spec/gfm-spec.coffee | 2 +- src/packages/snippets/spec/snippets-spec.coffee | 2 +- 13 files changed, 36 insertions(+), 40 deletions(-) diff --git a/spec/app/display-buffer-spec.coffee b/spec/app/display-buffer-spec.coffee index de7404eee..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.buildEditSession('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.buildEditSession('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 4c1bdc4a0..1a4d3754d 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -9,13 +9,10 @@ describe "EditSession", -> buffer.setText(buffer.getText().replace(/[ ]{2}/g, "\t")) beforeEach -> - editSession = fixturesProject.buildEditSession('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", -> @@ -1736,7 +1733,7 @@ describe "EditSession", -> it "does not explode if the current language mode has no comment regex", -> editSession.destroy() - editSession = fixturesProject.buildEditSession(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) {" @@ -1814,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.buildEditSession(editSession.getPath()) + otherEditSession = project.buildEditSession(editSession.getPath()) otherEditSession.setSelectedBufferRange([[2, 2], [3, 3]]) otherEditSession.delete() @@ -2007,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.buildEditSession('sample.js', softTabs: false) + editSession = project.buildEditSession('sample.js', softTabs: false) expect(editSession.softTabs).toBeTruthy() - editSession = fixturesProject.buildEditSession('sample-with-tabs.coffee', softTabs: true) + editSession = project.buildEditSession('sample-with-tabs.coffee', softTabs: true) expect(editSession.softTabs).toBeFalsy() - editSession = fixturesProject.buildEditSession(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 bf7140c65..ef747e6e2 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -1745,7 +1745,7 @@ 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", -> - emptyEditSession = fixturesProject.buildEditSession(null) + emptyEditSession = project.buildEditSession(null) editor.edit(emptyEditSession) expect(editor.gutter.lineNumbers.find('.line-number').length).toBe 1 diff --git a/spec/app/language-mode-spec.coffee b/spec/app/language-mode-spec.coffee index 1d53922f2..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.buildEditSession('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.buildEditSession('js', autoIndent: false) + jsEditSession = project.buildEditSession('js', autoIndent: false) expect(jsEditSession.languageMode.grammar.name).toBe "JavaScript" jsEditSession.destroy() describe "javascript", -> beforeEach -> - editSession = fixturesProject.buildEditSession('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.buildEditSession('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.buildEditSession('css.css', autoIndent: false) + editSession = project.buildEditSession('css.css', autoIndent: false) { buffer, languageMode } = editSession describe ".toggleLineCommentsForBufferRows(start, end)", -> 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 52e48cf58..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.buildEditSession('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.buildEditSession('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.buildEditSession('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.buildEditSession('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.buildEditSession('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.buildEditSession('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/spec-helper.coffee b/spec/spec-helper.coffee index 92c752871..fc93ecc61 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -30,8 +30,8 @@ jasmine.getEnv().defaultTimeoutInterval = 5000 beforeEach -> jQuery.fx.off = true - window.fixturesProject = new Project(require.resolve('fixtures')) - window.project = fixturesProject + + window.project = new Project(require.resolve('fixtures')) window.git = Git.open(fixturesProject.getPath()) window.project.on 'path-changed', -> window.git?.destroy() diff --git a/src/packages/autocomplete/spec/autocomplete-spec.coffee b/src/packages/autocomplete/spec/autocomplete-spec.coffee index d85b8b507..9bfa9af7f 100644 --- a/src/packages/autocomplete/spec/autocomplete-spec.coffee +++ b/src/packages/autocomplete/spec/autocomplete-spec.coffee @@ -40,7 +40,7 @@ describe "AutocompleteView", -> beforeEach -> window.rootView = new RootView - editor = new Editor(editSession: fixturesProject.buildEditSession('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/command-panel/spec/command-interpreter-spec.coffee b/src/packages/command-panel/spec/command-interpreter-spec.coffee index c9f590346..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.buildEditSession('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 diff --git a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee index 423827379..4530b8e9b 100644 --- a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee +++ b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee @@ -78,7 +78,7 @@ describe 'FuzzyFinder', -> 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 @@ -179,7 +179,7 @@ describe 'FuzzyFinder', -> describe "when the active pane has an item for the selected path", -> it "switches to the item for the selected path", -> - expectedPath = fixturesProject.resolve('sample.txt') + expectedPath = project.resolve('sample.txt') finderView.confirmed('sample.txt') expect(finderView.hasParent()).toBeFalsy() @@ -195,7 +195,7 @@ describe 'FuzzyFinder', -> expect(rootView.getActiveView()).toBe editor1 - expectedPath = fixturesProject.resolve('sample.txt') + expectedPath = project.resolve('sample.txt') finderView.confirmed('sample.txt') expect(finderView.hasParent()).toBeFalsy() diff --git a/src/packages/gfm.tmbundle/spec/gfm-spec.coffee b/src/packages/gfm.tmbundle/spec/gfm-spec.coffee index 8f07e640e..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.buildEditSession('gfm.md') + editSession = project.buildEditSession('gfm.md') editSession.insertNewlineBelow() expect(editSession.buffer.lineForRow(1)).toBe ' ' diff --git a/src/packages/snippets/spec/snippets-spec.coffee b/src/packages/snippets/spec/snippets-spec.coffee index 45701ccf3..b77b2d80b 100644 --- a/src/packages/snippets/spec/snippets-spec.coffee +++ b/src/packages/snippets/spec/snippets-spec.coffee @@ -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 From 6ae684d609dd4fa3d0bb5681d5f1d60a4b52c562 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 28 Feb 2013 10:14:55 -0700 Subject: [PATCH 206/308] Kill commented specs that were used as a reminder --- spec/app/pane-spec.coffee | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 83e999bf9..7285ab778 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -503,23 +503,3 @@ describe "Pane", -> it "can serialize and deserialize the pane and all its serializable items", -> newPane = deserialize(pane.serialize()) expect(newPane.getItems()).toEqual [editSession1, editSession2] - -# This relates to confirming the closing of a tab -# -# 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() From 699e780e99429a2290ffd94c7ab17e85fef78dc4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 28 Feb 2013 10:48:32 -0700 Subject: [PATCH 207/308] Handle save in panes with new 'core:save' event --- spec/app/pane-spec.coffee | 36 ++++++++++++++++++++++++++++++++++++ src/app/editor.coffee | 8 -------- src/app/keymaps/atom.cson | 1 + src/app/keymaps/editor.cson | 1 - src/app/pane.coffee | 12 ++++++++---- 5 files changed, 45 insertions(+), 13 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 7285ab778..96973f95e 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -270,6 +270,42 @@ describe "Pane", -> expect(containerCloseHandler).not.toHaveBeenCalled() + describe "core:save", -> + describe "when the current item has a path", -> + 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 path", -> + 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 "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 diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 85cc1298a..f9b65b835 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -152,7 +152,6 @@ class Editor extends View 'core:select-down': @selectDown 'core:select-to-top': @selectToTop 'core:select-to-bottom': @selectToBottom - 'editor:save': @save 'editor:save-as': @saveAs 'editor:newline-below': @insertNewlineBelow 'editor:newline-above': @insertNewlineAbove @@ -611,13 +610,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 diff --git a/src/app/keymaps/atom.cson b/src/app/keymaps/atom.cson index 4dbe961a1..eb6b1628e 100644 --- a/src/app/keymaps/atom.cson +++ b/src/app/keymaps/atom.cson @@ -1,4 +1,5 @@ 'body': + 'meta-s': 'core:save' 'enter': 'core:confirm' 'escape': 'core:cancel' 'meta-w': 'core:close' diff --git a/src/app/keymaps/editor.cson b/src/app/keymaps/editor.cson index e30e84e9b..fc8c859d4 100644 --- a/src/app/keymaps/editor.cson +++ b/src/app/keymaps/editor.cson @@ -2,7 +2,6 @@ 'meta-T': 'editor:undo-close-session' '.editor': - 'meta-s': 'editor:save' 'meta-S': 'editor:save-as' 'enter': 'editor:newline' 'meta-enter': 'editor:newline-below' diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 6829b33df..3295daa20 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -21,6 +21,7 @@ class Pane extends View @showItem(@items[0]) @command 'core:close', @destroyActiveItem + @command 'core:save', @saveActiveItem @command 'pane:show-next-item', @showNextItem @command 'pane:show-previous-item', @showPreviousItem @command 'pane:split-left', => @splitLeft() @@ -115,14 +116,17 @@ class Pane extends View "Don't Save", nextAction ) + saveActiveItem: => + @saveItem(@activeItem) + saveItem: (item, nextAction) -> - if item.getPath() + if item.getPath?() item.save() - nextAction() - else + nextAction?() + else if item.saveAs? atom.showSaveDialog (path) -> item.saveAs(path) - nextAction() + nextAction?() removeItem: (item) -> index = @items.indexOf(item) From 59a06acc0b72a21de47f88e0d715e2b13f389089 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 28 Feb 2013 17:49:11 -0700 Subject: [PATCH 208/308] Fire 'modified-status-changed' events on changes after save/reload Buffer keeps state about the value with which it fired the last modified-status-changed event so that it doesn't fire it twice with the same boolean value. Every piece of code that triggers the event also needs to set this state, so now everything goes through the `triggerModifiedStatusChanged` method. --- spec/app/buffer-spec.coffee | 23 +++++++++++++++++++---- src/app/buffer.coffee | 15 +++++++++------ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/spec/app/buffer-spec.coffee b/spec/app/buffer-spec.coffee index 0b69b2fa1..84b58ddeb 100644 --- a/spec/app/buffer-spec.coffee +++ b/spec/app/buffer-spec.coffee @@ -181,7 +181,7 @@ describe 'Buffer', -> changeHandler.callCount > 0 describe "modified status", -> - it "reports a modified status of true after the user changes buffer", -> + 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 @@ -203,7 +203,7 @@ describe 'Buffer', -> advanceClock(buffer.stoppedChangingDelay) expect(modifiedHandler).toHaveBeenCalledWith(false) - it "reports a modified status of true after the underlying file is deleted", -> + 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') @@ -216,7 +216,7 @@ describe 'Buffer', -> waitsFor "modified status to change", -> modifiedHandler.callCount runs -> expect(buffer.isModified()).toBe true - it "reports a modified status of false after a modified buffer is saved", -> + it "reports the modified status changing to false after a modified buffer is saved", -> filePath = "/tmp/atom-tmp-file" fs.write(filePath, '') buffer.release() @@ -225,14 +225,22 @@ describe 'Buffer', -> 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() - it "reports a modified status of false after a modified buffer is reloaded", -> + 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() @@ -241,12 +249,19 @@ describe 'Buffer', -> 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() diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee index e8ccc91db..00d0483e4 100644 --- a/src/app/buffer.coffee +++ b/src/app/buffer.coffee @@ -69,7 +69,7 @@ class Buffer @file.on "removed", => @updateCachedDiskContents() - @trigger "modified-status-changed", @isModified() + @triggerModifiedStatusChanged(@isModified()) @file.on "moved", => @trigger "path-changed", this @@ -78,7 +78,7 @@ class Buffer @trigger 'will-reload' @updateCachedDiskContents() @setText(@cachedDiskContents) - @trigger 'modified-status-changed', false + @triggerModifiedStatusChanged(false) @trigger 'reloaded' updateCachedDiskContents: -> @@ -253,7 +253,7 @@ class Buffer @setPath(path) @cachedDiskContents = @getText() @file.write(@getText()) - @trigger 'modified-status-changed', false + @triggerModifiedStatusChanged(false) @trigger 'saved' isModified: -> @@ -432,11 +432,14 @@ class Buffer @stoppedChangingTimeout = null modifiedStatus = @isModified() @trigger 'contents-modified', modifiedStatus - unless modifiedStatus is @previousModifiedStatus - @previousModifiedStatus = modifiedStatus - @trigger 'modified-status-changed', modifiedStatus + @triggerModifiedStatusChanged(modifiedStatus) @stoppedChangingTimeout = setTimeout(stoppedChangingCallback, @stoppedChangingDelay) + triggerModifiedStatusChanged: (modifiedStatus) -> + return if modifiedStatus is @previousModifiedStatus + @previousModifiedStatus = modifiedStatus + @trigger 'modified-status-changed', modifiedStatus + fileExists: -> @file.exists() From 3f9ee08e767229cbcf87add6eebb225e6fc7d27e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 28 Feb 2013 18:00:04 -0700 Subject: [PATCH 209/308] Handle save-as on pane. Replace 'editor:save-as' w/ 'core:save-as' Pane will only show the saveAs dialog if the item has a `saveAs` method. --- spec/app/pane-spec.coffee | 21 +++++++++++++++++++++ src/app/editor.coffee | 7 ------- src/app/keymaps/atom.cson | 1 + src/app/keymaps/editor.cson | 1 - src/app/pane.coffee | 13 +++++++++++-- 5 files changed, 33 insertions(+), 10 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 96973f95e..d657e9b5c 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -306,6 +306,27 @@ describe "Pane", -> 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 diff --git a/src/app/editor.coffee b/src/app/editor.coffee index f9b65b835..0f900eb47 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -152,7 +152,6 @@ class Editor extends View 'core:select-down': @selectDown 'core:select-to-top': @selectToTop 'core:select-to-bottom': @selectToBottom - 'editor:save-as': @saveAs 'editor:newline-below': @insertNewlineBelow 'editor:newline-above': @insertNewlineAbove 'editor:toggle-soft-tabs': @toggleSoftTabs @@ -610,12 +609,6 @@ class Editor extends View @removeClass 'soft-wrap' $(window).off 'resize', @_setSoftWrapColumn - saveAs: (session=@activeEditSession, onSuccess) -> - atom.showSaveDialog (path) => - if path - session.saveAs(path) - onSuccess?() - autosave: -> @save() if @getPath()? diff --git a/src/app/keymaps/atom.cson b/src/app/keymaps/atom.cson index eb6b1628e..29e83880a 100644 --- a/src/app/keymaps/atom.cson +++ b/src/app/keymaps/atom.cson @@ -1,5 +1,6 @@ 'body': 'meta-s': 'core:save' + 'meta-S': 'core:save-as' 'enter': 'core:confirm' 'escape': 'core:cancel' 'meta-w': 'core:close' diff --git a/src/app/keymaps/editor.cson b/src/app/keymaps/editor.cson index fc8c859d4..066385507 100644 --- a/src/app/keymaps/editor.cson +++ b/src/app/keymaps/editor.cson @@ -2,7 +2,6 @@ 'meta-T': 'editor:undo-close-session' '.editor': - 'meta-S': 'editor:save-as' 'enter': 'editor:newline' 'meta-enter': 'editor:newline-below' 'meta-shift-enter': 'editor:newline-above' diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 3295daa20..0696b24da 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -22,6 +22,7 @@ class Pane extends View @command 'core:close', @destroyActiveItem @command 'core:save', @saveActiveItem + @command 'core:save-as', @saveActiveItemAs @command 'pane:show-next-item', @showNextItem @command 'pane:show-previous-item', @showPreviousItem @command 'pane:split-left', => @splitLeft() @@ -119,12 +120,20 @@ class Pane extends View saveActiveItem: => @saveItem(@activeItem) + saveActiveItemAs: => + @saveItemAs(@activeItem) + saveItem: (item, nextAction) -> if item.getPath?() item.save() nextAction?() - else if item.saveAs? - atom.showSaveDialog (path) -> + else + @saveItemAs(item, nextAction) + + saveItemAs: (item, nextAction) -> + return unless item.saveAs? + atom.showSaveDialog (path) => + if path item.saveAs(path) nextAction?() From 685df18a3a919a4f775eb243eaba12fbcf36da2d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 28 Feb 2013 18:07:01 -0700 Subject: [PATCH 210/308] Fix breakages due to save method moving to Pane (except saveAll specs) --- spec/app/editor-spec.coffee | 51 +------------------ src/app/editor.coffee | 8 +-- .../status-bar/spec/status-bar-spec.coffee | 2 +- .../strip-trailing-whitespace-spec.coffee | 12 ++--- 4 files changed, 12 insertions(+), 61 deletions(-) diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index ef747e6e2..04e6c9b57 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -139,7 +139,6 @@ describe "Editor", -> expect(editor.verticalScrollbar.prop('scrollHeight')).toBe previousScrollHeight expect(editor.scrollTop()).toBe previousScrollTop expect(editor.scrollView.scrollLeft()).toBe previousScrollLeft - console.log editor.getCursorView().css('left') expect(editor.getCursorView().position()).toEqual { top: 3 * editor.lineHeight, left: 5 * editor.charWidth } editor.insertText("goodbye") expect(editor.lineElementForScreenRow(3).text()).toMatch /^ vgoodbyear/ @@ -162,54 +161,6 @@ describe "Editor", -> runs -> expect(atom.confirm).toHaveBeenCalled() - describe ".save()", -> - describe "when the current buffer has a path", -> - tempFilePath = null - - beforeEach -> - project.setPath('/tmp') - tempFilePath = '/tmp/atom-temp.txt' - fs.write(tempFilePath, "") - editor.edit(project.buildEditSession(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.buildEditSession()) - 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() - describe ".scrollTop(n)", -> beforeEach -> editor.attachToDom(heightInLines: 5) @@ -2025,7 +1976,7 @@ describe "Editor", -> 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 diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 0f900eb47..a87790158 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -348,7 +348,7 @@ class Editor extends View @hiddenInput.on 'focusout', => @isFocused = false - @autosave() if config.get "editor.autosave" +# @autosave() if config.get "editor.autosave" @removeClass 'is-focused' @underlayer.on 'click', (e) => @@ -456,7 +456,7 @@ class Editor extends View return if editSession is @activeEditSession if @activeEditSession - @autosave() if config.get "editor.autosave" +# @autosave() if config.get "editor.autosave" @saveScrollPositionForActiveEditSession() @activeEditSession.off(".editor") @@ -609,8 +609,8 @@ class Editor extends View @removeClass 'soft-wrap' $(window).off 'resize', @_setSoftWrapColumn - autosave: -> - @save() if @getPath()? +# autosave: -> +# @save() if @getPath()? setFontSize: (fontSize) -> headTag = $("head") diff --git a/src/packages/status-bar/spec/status-bar-spec.coffee b/src/packages/status-bar/spec/status-bar-spec.coffee index 8f22d6d31..f7a88cf4d 100644 --- a/src/packages/status-bar/spec/status-bar-spec.coffee +++ b/src/packages/status-bar/spec/status-bar-spec.coffee @@ -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 e1fc838de..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 @@ -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" From bb15389b664303a86469705a590f8519d3c033be Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 1 Mar 2013 10:24:21 -0700 Subject: [PATCH 211/308] Add 'Pane.saveItems' and corresponding event --- src/app/pane.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 0696b24da..12ff4d919 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -23,6 +23,7 @@ class Pane extends View @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:split-left', => @splitLeft() @@ -137,6 +138,9 @@ class Pane extends View item.saveAs(path) nextAction?() + saveItems: => + @saveItem(item) for item in @getItems() + removeItem: (item) -> index = @items.indexOf(item) return if index == -1 From fff5d5158f7069de9f5b891754c9d977ecb5cc35 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 1 Mar 2013 10:35:48 -0700 Subject: [PATCH 212/308] Pass items through in editor's pane-splitting convenience methods --- src/app/editor.coffee | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index a87790158..a9e7f5751 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -654,17 +654,17 @@ class Editor extends View @updateLayerDimensions() @requestDisplayUpdate() - splitLeft: (editSession) -> - @pane()?.splitLeft().activeView + splitLeft: (items...) -> + @pane()?.splitLeft(items...).activeView - splitRight: (editSession) -> - @pane()?.splitRight().activeView + splitRight: (items...) -> + @pane()?.splitRight(items...).activeView - splitUp: (editSession) -> - @pane()?.splitUp().activeView + splitUp: (items...) -> + @pane()?.splitUp(items...).activeView - splitDown: (editSession) -> - @pane()?.splitDown().activeView + splitDown: (items...) -> + @pane()?.splitDown(items...).activeView pane: -> @closest('.pane').view() From da986b6a6c82888245d59f7c5aacede307c9e147 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 1 Mar 2013 10:37:36 -0700 Subject: [PATCH 213/308] Fix RootView.saveAll() --- spec/app/root-view-spec.coffee | 3 +-- src/app/root-view.coffee | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index 450f80a36..25cb21e6d 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -295,8 +295,7 @@ describe "RootView", -> buffer1.setText('edited1') expect(buffer1.isModified()).toBe(true) - editor2 = editor1.splitRight() - editor2.edit(project.buildEditSession('atom-temp2.txt')) + editor2 = editor1.splitRight(project.buildEditSession('atom-temp2.txt')) buffer2 = editor2.activeEditSession.buffer expect(buffer2.getText()).toBe("file2") expect(buffer2.isModified()).toBe(false) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 1e4d349db..0246861d9 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -148,7 +148,7 @@ class RootView extends View super saveAll: -> - editor.save() for editor in @getEditors() + pane.saveItems() for pane in @getPanes() eachPane: (callback) -> @panes.eachPane(callback) From 48c693d75681b70801942b9980df000729e4aefc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 1 Mar 2013 14:37:46 -0700 Subject: [PATCH 214/308] Add 'pane:close' event, which destroys all pane items. Still have some issues with the presentation order of dialogs with multiple unsaved buffers and no paths. But for the 99% case this works as is. --- spec/app/pane-spec.coffee | 9 +++++++++ src/app/pane.coffee | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index d657e9b5c..89130e17c 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -270,6 +270,15 @@ describe "Pane", -> expect(containerCloseHandler).not.toHaveBeenCalled() + describe "pane:close", -> + it "destroys all items and removes the pane", -> + pane.showItem(editSession1) + initialItemCount = pane.getItems().length + pane.trigger 'pane:close' + expect(pane.hasParent()).toBeFalsy() + expect(editSession2.destroyed).toBeTruthy() + expect(editSession1.destroyed).toBeTruthy() + describe "core:save", -> describe "when the current item has a path", -> describe "when the current item has a save method", -> diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 12ff4d919..1cbe2c312 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -30,6 +30,7 @@ class Pane extends View @command 'pane:split-right', => @splitRight() @command 'pane:split-up', => @splitUp() @command 'pane:split-down', => @splitDown() + @command 'pane:close', => @destroyItems() @on 'focus', => @activeView.focus(); false @on 'focusin', => @makeActive() @@ -108,6 +109,9 @@ class Pane extends View else reallyDestroyItem() + destroyItems: -> + @destroyItem(item) for item in @getItems() + promptToSaveItem: (item, nextAction) -> path = item.getPath() atom.confirm( From f0398f2331c72feda0613c031c8651a97a67a596 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 1 Mar 2013 15:52:21 -0700 Subject: [PATCH 215/308] Ensure modal dialogs are presented in a coherent order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modal dialogs can be presented while other modal dialogs are already being displayed. Previously, dialogs were always displayed in the order they were requested. But say you have two untitled buffers in a pane and you close all items… You'll display prompt dialogs for both buffers asking the user if they want to save. If the user answers yes to the first dialog, they should see the path selection dialog before they see the save prompt for the second buffer. This commit uses a stack of queues to store deferred dialogs and allow dialogs presented by the dismissal of another dialog to take precedence over other pending dialogs. --- spec/app/atom-spec.coffee | 69 +++++++++++++++++++++++++++++++++++++++ spec/spec-helper.coffee | 2 ++ src/app/atom.coffee | 50 ++++++++++++++++++++++++---- 3 files changed, 114 insertions(+), 7 deletions(-) diff --git a/spec/app/atom-spec.coffee b/spec/app/atom-spec.coffee index 261c86d4f..675443a19 100644 --- a/spec/app/atom-spec.coffee +++ b/spec/app/atom-spec.coffee @@ -141,3 +141,72 @@ 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)]() + + atom.sendMessageToBrowserProcess.simulatePathSelection = (path) -> + callback = @argsForCall[0][2] + @reset() + callback(path) + + 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/spec-helper.coffee b/spec/spec-helper.coffee index fc93ecc61..bbe236034 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -83,6 +83,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 6939a071f..597fb60cb 100644 --- a/src/app/atom.coffee +++ b/src/app/atom.coffee @@ -16,6 +16,8 @@ _.extend atom, loadedPackages: [] activatedAtomPackages: [] atomPackageStates: {} + presentingModal: false + pendingModals: [[]] getPathToOpen: -> @getWindowState('pathToOpen') ? window.location.params.pathToOpen @@ -102,15 +104,49 @@ _.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 + @presentModal(fn) if fn = @shiftPendingModal() + + 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') From e4bf73b41c83037bf19ce971b262b744ecaeb861 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 1 Mar 2013 16:27:13 -0700 Subject: [PATCH 216/308] Give the view a chance to update before presenting next dialog --- spec/app/atom-spec.coffee | 2 ++ src/app/atom.coffee | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/app/atom-spec.coffee b/spec/app/atom-spec.coffee index 675443a19..e614a2ff7 100644 --- a/spec/app/atom-spec.coffee +++ b/spec/app/atom-spec.coffee @@ -150,11 +150,13 @@ describe "the `atom` global", -> 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") diff --git a/src/app/atom.coffee b/src/app/atom.coffee index 597fb60cb..2e4635c18 100644 --- a/src/app/atom.coffee +++ b/src/app/atom.coffee @@ -131,7 +131,8 @@ _.extend atom, @pendingModals.push([]) # prioritize any modals presented during dismiss callback fn?(args...) @presentingModal = false - @presentModal(fn) if fn = @shiftPendingModal() + 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. From 7ebce683c651c0b437e1b3e0d4367361ab9dc0c8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 1 Mar 2013 16:44:27 -0700 Subject: [PATCH 217/308] Move saveAll and specs to PaneContainer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit And simplify the specs… we don't *really* need to save. We can just ensure that save is called on everything. --- spec/app/pane-container-spec.coffee | 12 ++++++++++++ spec/app/root-view-spec.coffee | 30 ----------------------------- src/app/pane-container.coffee | 3 +++ src/app/root-view.coffee | 2 +- 4 files changed, 16 insertions(+), 31 deletions(-) diff --git a/spec/app/pane-container-spec.coffee b/spec/app/pane-container-spec.coffee index 063d3598b..6c1d8179a 100644 --- a/spec/app/pane-container-spec.coffee +++ b/spec/app/pane-container-spec.coffee @@ -13,6 +13,8 @@ describe "PaneContainer", -> @content: -> @div tabindex: -1 initialize: (@myText) -> @text(@myText) serialize: -> deserializer: 'TestView', myText: @myText + getPath: -> "/tmp/hi" + save: -> @saved = true container = new PaneContainer pane1 = new Pane(new TestView('1')) @@ -72,6 +74,16 @@ describe "PaneContainer", -> pane4.splitDown() expect(panes).toEqual [] + 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()) diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index 25cb21e6d..6604f7521 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -279,36 +279,6 @@ describe "RootView", -> editSession = rootView.open('b', changeFocus: false) expect(activePane.focus).not.toHaveBeenCalled() - 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.getActiveView() - 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(project.buildEditSession('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 "window:toggle-invisibles event", -> it "shows/hides invisibles in all open and future editors", -> rootView.height(200) diff --git a/src/app/pane-container.coffee b/src/app/pane-container.coffee index 12c88cc09..423ebab29 100644 --- a/src/app/pane-container.coffee +++ b/src/app/pane-container.coffee @@ -36,6 +36,9 @@ class PaneContainer extends View getRoot: -> @children().first().view() + saveAll: -> + pane.saveItems() for pane in @getPanes() + getPanes: -> @find('.pane').views() diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 0246861d9..0ce0b04ba 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -148,7 +148,7 @@ class RootView extends View super saveAll: -> - pane.saveItems() for pane in @getPanes() + @panes.saveAll() eachPane: (callback) -> @panes.eachPane(callback) From 4b8d786d2add7330dc0eb8f851b689885a57013d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 1 Mar 2013 17:25:58 -0700 Subject: [PATCH 218/308] :lipstick: --- spec/app/pane-spec.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 89130e17c..d4066292e 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -273,7 +273,6 @@ describe "Pane", -> describe "pane:close", -> it "destroys all items and removes the pane", -> pane.showItem(editSession1) - initialItemCount = pane.getItems().length pane.trigger 'pane:close' expect(pane.hasParent()).toBeFalsy() expect(editSession2.destroyed).toBeTruthy() From f23d9091f2b2f70830e1336c3df719839c74219b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 1 Mar 2013 17:26:16 -0700 Subject: [PATCH 219/308] Add pane:close-other-items --- spec/app/pane-spec.coffee | 7 +++++++ src/app/pane.coffee | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index d4066292e..103aa13ba 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -278,6 +278,13 @@ describe "Pane", -> 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 path", -> describe "when the current item has a save method", -> diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 1cbe2c312..3978ca696 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -31,6 +31,7 @@ class Pane extends View @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() @@ -112,6 +113,9 @@ class Pane extends View destroyItems: -> @destroyItem(item) for item in @getItems() + destroyInactiveItems: -> + @destroyItem(item) for item in @getItems() when item isnt @activeItem + promptToSaveItem: (item, nextAction) -> path = item.getPath() atom.confirm( From d97e91bdcb5baabd88d77098f86aba97abffe2a7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 1 Mar 2013 17:36:16 -0700 Subject: [PATCH 220/308] Make meta-# bindings work with new panes --- spec/app/pane-spec.coffee | 9 +++++++++ src/app/keymaps/atom.cson | 10 +++++++++- src/app/keymaps/editor.cson | 9 --------- src/app/pane.coffee | 18 ++++++++++++++++-- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 103aa13ba..f1344ba31 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -354,6 +354,15 @@ describe "Pane", -> 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 ".remove()", -> it "destroys all the pane's items", -> pane.remove() diff --git a/src/app/keymaps/atom.cson b/src/app/keymaps/atom.cson index 29e83880a..c621867c3 100644 --- a/src/app/keymaps/atom.cson +++ b/src/app/keymaps/atom.cson @@ -37,11 +37,19 @@ 'ctrl--': 'pane:split-down' 'ctrl-w s': 'pane:split-down' -'.pane': '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' '.tool-panel': 'meta-escape': 'tool-panel:unfocus' diff --git a/src/app/keymaps/editor.cson b/src/app/keymaps/editor.cson index 066385507..8b2e3d8ad 100644 --- a/src/app/keymaps/editor.cson +++ b/src/app/keymaps/editor.cson @@ -16,15 +16,6 @@ 'shift-tab': 'editor:outdent-selected-rows' 'meta-[': 'editor:outdent-selected-rows' 'meta-]': 'editor:indent-selected-rows' - '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.coffee b/src/app/pane.coffee index 3978ca696..0a77a8a1d 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -26,6 +26,17 @@ class Pane extends View @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() @@ -74,10 +85,13 @@ class Pane extends View @items.indexOf(@activeItem) showItemAtIndex: (index) -> - @showItem(@items[index]) + @showItem(@itemAtIndex(index)) + + itemAtIndex: (index) -> + @items[index] showItem: (item) -> - return if item is @activeItem + return if !item? or item is @activeItem isFocused = @is(':has(:focus)') @addItem(item) view = @viewForItem(item) From f2e5fcc9020c3aae6aff0f9193e8bf31e254fb4b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 1 Mar 2013 18:14:28 -0700 Subject: [PATCH 221/308] Move autosave from editor into panes --- docs/getting-started.md | 2 +- docs/internals/configuration.md | 4 +-- spec/app/pane-spec.coffee | 62 +++++++++++++++++++++++++++++++++ src/app/editor.coffee | 6 ---- src/app/pane.coffee | 12 +++++++ 5 files changed, 77 insertions(+), 9 deletions(-) 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/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index f1344ba31..887050f7f 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -575,6 +575,68 @@ describe "Pane", -> expect(container.children('.pane').length).toBe 1 expect(pane1.outerWidth()).toBe container.width() + describe "autosave", -> + [initialActiveItem, initialActiveItemPath] = [] + + beforeEach -> + initialActiveItem = pane.activeItem + initialActiveItemPath = null + pane.activeItem.getPath = -> initialActiveItemPath + 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 path", -> + 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() + + initialActiveItemPath = '/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 path", -> + 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) + initialActiveItemPath = '/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 path", -> + # 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.getPath = -> undefined + view2.save = -> + pane.destroyItem(view2) + expect(pane.saveItem).not.toHaveBeenCalled() + + initialActiveItemPath = '/tmp/hi' + pane.destroyItem(initialActiveItem) + expect(initialActiveItem.save).toHaveBeenCalled() + describe ".itemForPath(path)", -> it "returns the item for which a call to .getPath() returns the given path", -> expect(pane.itemForPath(editSession1.getPath())).toBe editSession1 diff --git a/src/app/editor.coffee b/src/app/editor.coffee index a9e7f5751..5e5d99fd8 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: "./\\()\"':,.;<>~!@#$%^&*|+=[]{}`~?-" @@ -348,7 +347,6 @@ class Editor extends View @hiddenInput.on 'focusout', => @isFocused = false -# @autosave() if config.get "editor.autosave" @removeClass 'is-focused' @underlayer.on 'click', (e) => @@ -456,7 +454,6 @@ class Editor extends View return if editSession is @activeEditSession if @activeEditSession -# @autosave() if config.get "editor.autosave" @saveScrollPositionForActiveEditSession() @activeEditSession.off(".editor") @@ -609,9 +606,6 @@ class Editor extends View @removeClass 'soft-wrap' $(window).off 'resize', @_setSoftWrapColumn -# autosave: -> -# @save() if @getPath()? - setFontSize: (fontSize) -> headTag = $("head") styleTag = headTag.find("style.font-size") diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 0a77a8a1d..ba61be3eb 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -45,6 +45,7 @@ class Pane extends View @command 'pane:close-other-items', => @destroyInactiveItems() @on 'focus', => @activeView.focus(); false @on 'focusin', => @makeActive() + @on 'focusout', => @autosaveActiveItem() afterAttach: -> return if @attached @@ -92,6 +93,9 @@ class Pane extends View showItem: (item) -> return if !item? or item is @activeItem + + @autosaveActiveItem() if @activeItem + isFocused = @is(':has(:focus)') @addItem(item) view = @viewForItem(item) @@ -119,6 +123,8 @@ class Pane extends View @removeItem(item) item.destroy?() + @autosaveItem(item) + if item.isModified?() @promptToSaveItem(item, reallyDestroyItem) else @@ -163,6 +169,12 @@ class Pane extends View saveItems: => @saveItem(item) for item in @getItems() + autosaveActiveItem: -> + @autosaveItem(@activeItem) + + autosaveItem: (item) -> + @saveItem(item) if config.get('core.autosave') and item.getPath?() + removeItem: (item) -> index = @items.indexOf(item) return if index == -1 From 96fefe94f0a92ba5e16e2a0bdc2006245b21c941 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 5 Mar 2013 12:04:14 -0800 Subject: [PATCH 222/308] Fix broken specs after rebase --- spec/app/git-spec.coffee | 6 +++--- spec/spec-helper.coffee | 3 +-- src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee | 4 ++-- src/packages/spell-check/spec/spell-check-spec.coffee | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/spec/app/git-spec.coffee b/spec/app/git-spec.coffee index 04c5596fc..fcdb120d8 100644 --- a/spec/app/git-spec.coffee +++ b/spec/app/git-spec.coffee @@ -189,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 -> diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index bbe236034..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.project = new Project(require.resolve('fixtures')) - window.git = Git.open(fixturesProject.getPath()) + window.git = Git.open(project.getPath()) window.project.on 'path-changed', -> window.git?.destroy() window.git = Git.open(window.project.getPath()) diff --git a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee index 4530b8e9b..dcad26a9e 100644 --- a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee +++ b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee @@ -206,7 +206,7 @@ describe 'FuzzyFinder', -> [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') @@ -461,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/spell-check/spec/spell-check-spec.coffee b/src/packages/spell-check/spec/spell-check-spec.coffee index b860e650c..f2b4f4f3a 100644 --- a/src/packages/spell-check/spec/spell-check-spec.coffee +++ b/src/packages/spell-check/spec/spell-check-spec.coffee @@ -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() From 5ad53bb32ca17b9e1e337befc036d4a7dec337ba Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 6 Mar 2013 11:57:17 -0800 Subject: [PATCH 223/308] Add restoreItem to Pane container --- spec/app/pane-container-spec.coffee | 60 +++++++++++++++++++++++++++-- spec/app/pane-spec.coffee | 38 +++++++++--------- src/app/edit-session.coffee | 7 ++++ src/app/pane-container.coffee | 22 +++++++++++ src/app/pane.coffee | 13 ++++--- src/app/root-view.coffee | 2 +- src/app/window.coffee | 5 ++- 7 files changed, 117 insertions(+), 30 deletions(-) diff --git a/spec/app/pane-container-spec.coffee b/spec/app/pane-container-spec.coffee index 6c1d8179a..ae9cb619f 100644 --- a/spec/app/pane-container-spec.coffee +++ b/spec/app/pane-container-spec.coffee @@ -1,6 +1,7 @@ PaneContainer = require 'pane-container' Pane = require 'pane' {View, $$} = require 'space-pen' +_ = require 'underscore' $ = require 'jquery' describe "PaneContainer", -> @@ -9,12 +10,13 @@ describe "PaneContainer", -> beforeEach -> class TestView extends View registerDeserializer(this) - @deserialize: ({myText}) -> new TestView(myText) + @deserialize: ({name}) -> new TestView(name) @content: -> @div tabindex: -1 - initialize: (@myText) -> @text(@myText) - serialize: -> deserializer: 'TestView', myText: @myText - getPath: -> "/tmp/hi" + 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')) @@ -74,6 +76,56 @@ describe "PaneContainer", -> pane4.splitDown() expect(panes).toEqual [] + describe ".restoreItem()", -> + 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.restoreItem()).toBeTruthy() + expect(pane1.activeItem).toEqual item4 + + expect(container.restoreItem()).toBeTruthy() + expect(pane1.activeItem).toEqual item3 + + expect(container.restoreItem()).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.restoreItem() + + 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.restoreItem()).toBeTruthy() + expect(_.pluck(pane1.getItems(), 'name')).toEqual ['1', '4', '3'] + expect(pane1.activeItem).toEqual item3 + + expect(container.restoreItem()).toBeFalsy() + expect(pane1.activeItem).toEqual item3 + describe ".saveAll()", -> it "saves all open pane items", -> pane1.showItem(new TestView('4')) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 887050f7f..b25db976d 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -125,7 +125,7 @@ describe "Pane", -> expect(editSession2.destroyed).toBeFalsy() describe "if the [Save] option is selected", -> - describe "when the item has a path", -> + describe "when the item has a uri", -> it "saves the item before removing and destroying it", -> atom.confirm.selectOption('Save') @@ -133,8 +133,8 @@ describe "Pane", -> expect(pane.getItems().indexOf(editSession2)).toBe -1 expect(editSession2.destroyed).toBeTruthy() - describe "when the item has no path", -> - it "presents a save-as dialog, then saves the item with the given path before removing and destroying it", -> + 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') @@ -286,7 +286,7 @@ describe "Pane", -> expect(pane.getItems()).toEqual [editSession1] describe "core:save", -> - describe "when the current item has a path", -> + 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') @@ -299,7 +299,7 @@ describe "Pane", -> expect(pane.activeItem.save).toBeUndefined() pane.trigger 'core:save' - describe "when the current item has no path", -> + describe "when the current item has no uri", -> beforeEach -> spyOn(atom, 'showSaveDialog') @@ -576,17 +576,17 @@ describe "Pane", -> expect(pane1.outerWidth()).toBe container.width() describe "autosave", -> - [initialActiveItem, initialActiveItemPath] = [] + [initialActiveItem, initialActiveItemUri] = [] beforeEach -> initialActiveItem = pane.activeItem - initialActiveItemPath = null - pane.activeItem.getPath = -> initialActiveItemPath + 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 path", -> + 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() @@ -596,12 +596,12 @@ describe "Pane", -> expect(pane.saveItem).not.toHaveBeenCalled() expect(pane.activeItem.save).not.toHaveBeenCalled() - initialActiveItemPath = '/tmp/hi' + 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 path", -> + 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() @@ -614,12 +614,12 @@ describe "Pane", -> expect(initialActiveItem.save).not.toHaveBeenCalled() pane.showItem(initialActiveItem) - initialActiveItemPath = '/tmp/hi' + 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 path", -> + 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) @@ -628,19 +628,19 @@ describe "Pane", -> expect(pane.saveItem).not.toHaveBeenCalled() config.set("core.autosave", true) - view2.getPath = -> undefined + view2.getUri = -> undefined view2.save = -> pane.destroyItem(view2) expect(pane.saveItem).not.toHaveBeenCalled() - initialActiveItemPath = '/tmp/hi' + initialActiveItemUri = '/tmp/hi' pane.destroyItem(initialActiveItem) expect(initialActiveItem.save).toHaveBeenCalled() - describe ".itemForPath(path)", -> - it "returns the item for which a call to .getPath() returns the given path", -> - expect(pane.itemForPath(editSession1.getPath())).toBe editSession1 - expect(pane.itemForPath(editSession2.getPath())).toBe editSession2 + 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", -> diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 46c9094f3..ba6ba0401 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -25,6 +25,12 @@ class EditSession session.setCursorScreenPosition(state.cursorScreenPosition) session + @identifiedBy: 'path' + + @deserializesToSameObject: (state, editSession) -> + state.path + + scrollTop: 0 scrollLeft: 0 languageMode: null @@ -151,6 +157,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/pane-container.coffee b/src/app/pane-container.coffee index 423ebab29..2db350200 100644 --- a/src/app/pane-container.coffee +++ b/src/app/pane-container.coffee @@ -1,4 +1,5 @@ {View} = require 'space-pen' +Pane = require 'pane' $ = require 'jquery' module.exports = @@ -13,6 +14,9 @@ class PaneContainer extends View @content: -> @div id: 'panes' + initialize: -> + @destroyedItemStates = [] + serialize: -> deserializer: 'PaneContainer' root: @getRoot()?.serialize() @@ -33,6 +37,24 @@ class PaneContainer extends View nextIndex = (currentIndex + 1) % panes.length panes[nextIndex].makeActive() + restoreItem: -> + 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() diff --git a/src/app/pane.coffee b/src/app/pane.coffee index ba61be3eb..f061212a7 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -111,6 +111,7 @@ class Pane extends View return if _.include(@items, item) index = @getActiveItemIndex() + 1 @items.splice(index, 0, item) + @getContainer().itemAdded(item) @trigger 'pane:item-added', [item, index] item @@ -119,8 +120,10 @@ class Pane extends View false destroyItem: (item) -> + container = @getContainer() reallyDestroyItem = => @removeItem(item) + container.itemDestroyed(item) item.destroy?() @autosaveItem(item) @@ -137,7 +140,7 @@ class Pane extends View @destroyItem(item) for item in @getItems() when item isnt @activeItem promptToSaveItem: (item, nextAction) -> - path = item.getPath() + 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." @@ -153,7 +156,7 @@ class Pane extends View @saveItemAs(@activeItem) saveItem: (item, nextAction) -> - if item.getPath?() + if item.getUri?() item.save() nextAction?() else @@ -173,7 +176,7 @@ class Pane extends View @autosaveItem(@activeItem) autosaveItem: (item) -> - @saveItem(item) if config.get('core.autosave') and item.getPath?() + @saveItem(item) if config.get('core.autosave') and item.getUri?() removeItem: (item) -> index = @items.indexOf(item) @@ -194,8 +197,8 @@ class Pane extends View @removeItem(item) pane.addItem(item, index) - itemForPath: (path) -> - _.detect @items, (item) -> item.getPath?() is path + itemForUri: (uri) -> + _.detect @items, (item) -> item.getUri?() is uri cleanupItemView: (item) -> if item instanceof $ diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 0ce0b04ba..1e3eaf3d9 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -92,7 +92,7 @@ class RootView extends View changeFocus = options.changeFocus ? true path = project.resolve(path) if path? if activePane = @getActivePane() - if editSession = activePane.itemForPath(path) + if editSession = activePane.itemForUri(path) activePane.showItem(editSession) else editSession = project.buildEditSession(path) diff --git a/src/app/window.coffee b/src/app/window.coffee index 2ff52830c..109a5096d 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -155,7 +155,10 @@ window.unregisterDeserializer = (klass) -> delete deserializers[klass.name] window.deserialize = (state) -> - deserializers[state?.deserializer]?.deserialize(state) + getDeserializer(state)?.deserialize(state) + +window.getDeserializer = (state) -> + deserializers[state?.deserializer] window.measure = (description, fn) -> start = new Date().getTime() From ffb8bcd71deef960bc69901fec7a0801f63bc6c5 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 6 Mar 2013 13:45:56 -0800 Subject: [PATCH 224/308] Add pane:reopen-closed-item command --- src/app/keymaps/atom.cson | 1 + src/app/keymaps/editor.cson | 3 --- src/app/root-view.coffee | 3 +++ 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/keymaps/atom.cson b/src/app/keymaps/atom.cson index c621867c3..04cc02488 100644 --- a/src/app/keymaps/atom.cson +++ b/src/app/keymaps/atom.cson @@ -50,6 +50,7 @@ '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' diff --git a/src/app/keymaps/editor.cson b/src/app/keymaps/editor.cson index 8b2e3d8ad..8b192fece 100644 --- a/src/app/keymaps/editor.cson +++ b/src/app/keymaps/editor.cson @@ -1,6 +1,3 @@ -'body': - 'meta-T': 'editor:undo-close-session' - '.editor': 'enter': 'editor:newline' 'meta-enter': 'editor:newline-below' diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 1e3eaf3d9..7fdff2cc3 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -63,6 +63,9 @@ class RootView extends View @command 'window:toggle-auto-indent-on-paste', => config.set("editor.autoIndentOnPaste", !config.get("editor.autoIndentOnPaste")) + @command 'pane:reopen-closed-item', => + @panes.restoreItem() + serialize: -> deserializer: 'RootView' panesViewState: @panes.serialize() From c1d19c4c5ce63edb3178a7f74bbf1b184e09c7bb Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 6 Mar 2013 13:52:27 -0800 Subject: [PATCH 225/308] Rename restoreItem to reopenItem on pane container --- spec/app/pane-container-spec.coffee | 14 +++++++------- src/app/pane-container.coffee | 2 +- src/app/root-view.coffee | 4 +++- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/spec/app/pane-container-spec.coffee b/spec/app/pane-container-spec.coffee index ae9cb619f..56ea53336 100644 --- a/spec/app/pane-container-spec.coffee +++ b/spec/app/pane-container-spec.coffee @@ -76,7 +76,7 @@ describe "PaneContainer", -> pane4.splitDown() expect(panes).toEqual [] - describe ".restoreItem()", -> + describe ".reopenItem()", -> describe "when there is an active pane", -> it "reconstructs and shows the last-closed pane item", -> expect(container.getActivePane()).toBe pane3 @@ -88,13 +88,13 @@ describe "PaneContainer", -> pane3.destroyItem(item4) expect(container.getActivePane()).toBe pane1 - expect(container.restoreItem()).toBeTruthy() + expect(container.reopenItem()).toBeTruthy() expect(pane1.activeItem).toEqual item4 - expect(container.restoreItem()).toBeTruthy() + expect(container.reopenItem()).toBeTruthy() expect(pane1.activeItem).toEqual item3 - expect(container.restoreItem()).toBeFalsy() + expect(container.reopenItem()).toBeFalsy() expect(pane1.activeItem).toEqual item3 describe "when there is no active pane", -> @@ -105,7 +105,7 @@ describe "PaneContainer", -> pane3.destroyItem(item3) expect(container.getActivePane()).toBeUndefined() - container.restoreItem() + container.reopenItem() expect(container.getActivePane().activeItem).toEqual item3 @@ -119,11 +119,11 @@ describe "PaneContainer", -> expect(container.getActivePane()).toBe pane1 pane1.showItem(new TestView('4')) - expect(container.restoreItem()).toBeTruthy() + expect(container.reopenItem()).toBeTruthy() expect(_.pluck(pane1.getItems(), 'name')).toEqual ['1', '4', '3'] expect(pane1.activeItem).toEqual item3 - expect(container.restoreItem()).toBeFalsy() + expect(container.reopenItem()).toBeFalsy() expect(pane1.activeItem).toEqual item3 describe ".saveAll()", -> diff --git a/src/app/pane-container.coffee b/src/app/pane-container.coffee index 2db350200..6fc367e56 100644 --- a/src/app/pane-container.coffee +++ b/src/app/pane-container.coffee @@ -37,7 +37,7 @@ class PaneContainer extends View nextIndex = (currentIndex + 1) % panes.length panes[nextIndex].makeActive() - restoreItem: -> + reopenItem: -> if lastItemState = @destroyedItemStates.pop() if activePane = @getActivePane() activePane.showItem(deserialize(lastItemState)) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 7fdff2cc3..ffdaaf2bd 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -58,13 +58,15 @@ class RootView extends View 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.restoreItem() + @panes.reopenItem() serialize: -> deserializer: 'RootView' From d4fc718e8e50d43563b1a138e9475cf0e6c09704 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 6 Mar 2013 14:12:02 -0800 Subject: [PATCH 226/308] Update window title when a pane item's title changes --- spec/app/pane-spec.coffee | 18 ++++++++++++++++++ spec/app/root-view-spec.coffee | 8 +++++++- src/app/pane.coffee | 8 +++++++- src/app/root-view.coffee | 3 +-- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index b25db976d..97c27dd8b 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -363,6 +363,24 @@ describe "Pane", -> 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() diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index 6604f7521..d4c8a31c4 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -159,7 +159,7 @@ describe "RootView", -> rootView.trigger(event) expect(commandHandler).toHaveBeenCalled() - describe "title", -> + describe "window title", -> describe "when the project has no path", -> it "sets the title to 'untitled'", -> project.setPath(undefined) @@ -174,6 +174,12 @@ describe "RootView", -> item = rootView.getActivePaneItem() expect(rootView.title).toBe "#{item.getTitle()} - #{project.getPath()}" + 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()}" + 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() diff --git a/src/app/pane.coffee b/src/app/pane.coffee index f061212a7..54b9e375d 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -94,10 +94,13 @@ class Pane extends View showItem: (item) -> return if !item? or item is @activeItem - @autosaveActiveItem() if @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) @@ -107,6 +110,9 @@ class Pane extends View @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 diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index ffdaaf2bd..6129f8fcd 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -32,8 +32,6 @@ class RootView extends View panes = deserialize(panesViewState) if panesViewState?.deserializer is 'PaneContainer' new RootView({panes}) - title: null - initialize: -> @command 'toggle-dev-tools', => atom.toggleDevTools() @on 'focus', (e) => @handleFocus(e) @@ -44,6 +42,7 @@ class RootView extends View @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) From 4f0bf9020bb1ebc8149b311f200f0698c2fe3732 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 6 Mar 2013 16:05:26 -0800 Subject: [PATCH 227/308] Fix pane focus and active item serialization Also: Un-x root view serialization specs --- spec/app/pane-spec.coffee | 27 ++++++++++++++++++ spec/app/root-view-spec.coffee | 52 +++++++++++++--------------------- src/app/pane.coffee | 18 ++++++++++-- 3 files changed, 62 insertions(+), 35 deletions(-) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 97c27dd8b..3c62fc797 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -664,3 +664,30 @@ describe "Pane", -> 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/root-view-spec.coffee b/spec/app/root-view-spec.coffee index d4c8a31c4..9cbd75837 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -18,39 +18,40 @@ describe "RootView", -> rootView.open(pathToOpen) rootView.focus() - xdescribe "@deserialize()", -> + describe "@deserialize()", -> viewState = null 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 = 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.buildEditSession('b')) - editor3.edit(project.buildEditSession('../sample.js')) - editor3.setCursorScreenPosition([2, 4]) - editor4.edit(project.buildEditSession('../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() @@ -82,11 +83,11 @@ 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() @@ -96,19 +97,6 @@ describe "RootView", -> 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 view", -> it "hands off focus to the active view", -> diff --git a/src/app/pane.coffee b/src/app/pane.coffee index 54b9e375d..ef29ce72f 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -10,8 +10,11 @@ class Pane extends View @div class: 'pane', => @div class: 'item-views', outlet: 'itemViews' - @deserialize: ({items}) -> - new Pane(items.map((item) -> deserialize(item))...) + @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 @@ -47,7 +50,11 @@ class Pane extends View @on 'focusin', => @makeActive() @on 'focusout', => @autosaveActiveItem() - afterAttach: -> + afterAttach: (onDom) -> + if @focusOnAttach and onDom + @focusOnAttach = null + @focus() + return if @attached @attached = true @trigger 'pane:attached' @@ -206,6 +213,9 @@ class Pane extends View itemForUri: (uri) -> _.detect @items, (item) -> item.getUri?() is uri + showItemForUri: (uri) -> + @showItem(@itemForUri(uri)) + cleanupItemView: (item) -> if item instanceof $ viewToRemove = item @@ -238,6 +248,8 @@ class Pane extends View serialize: -> deserializer: "Pane" + focused: @is(':has(:focus)') + activeItemUri: @activeItem.getUri?() if typeof @activeItem.serialize is 'function' items: _.compact(@getItems().map (item) -> item.serialize?()) adjustDimensions: -> # do nothing From 8333f14ef8d82ade7832c5bcde47bb325601ab52 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 6 Mar 2013 16:06:17 -0800 Subject: [PATCH 228/308] Throw away serialized state if its version doesn't match deserializer --- spec/app/window-spec.coffee | 31 +++++++++++++++++++++++++++++++ src/app/window.coffee | 4 +++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/spec/app/window-spec.coffee b/spec/app/window-spec.coffee index 787a80734..12f79248f 100644 --- a/spec/app/window-spec.coffee +++ b/spec/app/window-spec.coffee @@ -130,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/src/app/window.coffee b/src/app/window.coffee index 109a5096d..321a60f56 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -155,7 +155,9 @@ window.unregisterDeserializer = (klass) -> delete deserializers[klass.name] window.deserialize = (state) -> - getDeserializer(state)?.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] From 6257bcf0f5bb7b6e4514e28f60f567f7e06f602f Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 6 Mar 2013 16:12:29 -0800 Subject: [PATCH 229/308] Rename root view serialization keys --- src/app/root-view.coffee | 10 +++++----- .../fuzzy-finder/spec/fuzzy-finder-spec.coffee | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 6129f8fcd..9ab1b3f45 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -27,9 +27,9 @@ class RootView extends View @div id: 'vertical', outlet: 'vertical', => @subview 'panes', panes ? new PaneContainer - @deserialize: ({ panesViewState, packageStates, projectPath }) -> - atom.atomPackageStates = packageStates ? {} - panes = deserialize(panesViewState) if panesViewState?.deserializer is 'PaneContainer' + @deserialize: ({ panes, packages, projectPath }) -> + atom.atomPackageStates = packages ? {} + panes = deserialize(panes) if panes?.deserializer is 'PaneContainer' new RootView({panes}) initialize: -> @@ -69,8 +69,8 @@ class RootView extends View serialize: -> deserializer: 'RootView' - panesViewState: @panes.serialize() - packageStates: atom.serializeAtomPackages() + panes: @panes.serialize() + packages: atom.serializeAtomPackages() handleFocus: (e) -> if @getActivePane() diff --git a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee index dcad26a9e..ffaae8cb0 100644 --- a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee +++ b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee @@ -142,7 +142,7 @@ 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 From c3456dd5ac4ff19bbea8e37a68d57b5e901795ff Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 6 Mar 2013 17:03:28 -0800 Subject: [PATCH 230/308] Remove serialization methods from editor --- src/app/editor.coffee | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 5e5d99fd8..1e2c0b464 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -55,11 +55,6 @@ class Editor extends View newSelections: null redrawOnReattach: false - @deserialize: (state) -> - editor = new Editor(mini: state.mini, editSession: deserialize(state.editSession)) - editor.isFocused = state.isFocused - editor - initialize: (editSessionOrOptions) -> if editSessionOrOptions instanceof EditSession editSession = editSessionOrOptions @@ -91,15 +86,6 @@ class Editor extends View else throw new Error("Must supply an EditSession or mini: true") - serialize: -> - @saveScrollPositionForActiveEditSession() - deserializer: "Editor" - editSession: @activeEditSession.serialize() - isFocused: @isFocused - - copy: -> - Editor.deserialize(@serialize(), rootView) - bindKeys: -> editorBindings = 'core:move-left': @moveCursorLeft From dba7c08f59b68957a05e381f2dd6a110b3f0c355 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Wed, 6 Mar 2013 17:22:46 -0800 Subject: [PATCH 231/308] Add serialization version to root view --- src/app/root-view.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 9ab1b3f45..c907db4af 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -17,6 +17,8 @@ module.exports = class RootView extends View registerDeserializers(this, Pane, PaneRow, PaneColumn, Editor) + @version: 1 + @configDefaults: ignoredNames: [".git", ".svn", ".DS_Store"] disabledPackages: [] @@ -68,6 +70,7 @@ class RootView extends View @panes.reopenItem() serialize: -> + version: @constructor.version deserializer: 'RootView' panes: @panes.serialize() packages: atom.serializeAtomPackages() From cac6c854d2ffc1617e8c3cabe7bc623dacda5ce0 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 7 Mar 2013 10:25:51 -0800 Subject: [PATCH 232/308] :lipstick: --- src/app/edit-session.coffee | 1 - .../spec/markdown-preview-spec.coffee | 52 +++++++++---------- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index ba6ba0401..0d48a1f4e 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -30,7 +30,6 @@ class EditSession @deserializesToSameObject: (state, editSession) -> state.path - scrollTop: 0 scrollLeft: 0 languageMode: null diff --git a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee index 6e5ca6f0e..7704768ee 100644 --- a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee +++ b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee @@ -52,33 +52,33 @@ describe "MarkdownPreview", -> 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.getActiveView() - 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.getActiveView() - 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.getActiveView()).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() From 91b5c3e9c7c2f0ad2b0ae1c2d23cb8ac13128507 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 7 Mar 2013 11:07:03 -0800 Subject: [PATCH 233/308] Make refresh work again by fixing version in RootView.serialize --- src/app/root-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index c907db4af..2d3b8fa83 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -70,7 +70,7 @@ class RootView extends View @panes.reopenItem() serialize: -> - version: @constructor.version + version: RootView.version deserializer: 'RootView' panes: @panes.serialize() packages: atom.serializeAtomPackages() From 39fabaa34400bbf84873060792b190da17639e57 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 7 Mar 2013 11:09:39 -0800 Subject: [PATCH 234/308] Update RootView.deserialize specs so they break on a version mismatch --- spec/app/root-view-spec.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index 9cbd75837..89c8ef8c1 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -31,7 +31,7 @@ describe "RootView", -> viewState = rootView.serialize() rootView.deactivate() - window.rootView = RootView.deserialize(viewState) + window.rootView = deserialize(viewState) rootView.attachToDom() expect(rootView.getEditors().length).toBe 2 @@ -55,7 +55,7 @@ describe "RootView", -> viewState = rootView.serialize() rootView.deactivate() - window.rootView = RootView.deserialize(viewState) + window.rootView = deserialize(viewState) rootView.attachToDom() expect(rootView.getEditors().length).toBe 4 @@ -92,7 +92,7 @@ describe "RootView", -> viewState = rootView.serialize() rootView.deactivate() - window.rootView = RootView.deserialize(viewState) + window.rootView = deserialize(viewState) rootView.attachToDom() expect(rootView.getEditors().length).toBe 0 From 17f4d6f0645871a8fb59ca4f6efda5f45e4b9874 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 7 Mar 2013 14:25:14 -0800 Subject: [PATCH 235/308] EditSession reloads its grammar on the 'grammars-loaded' event --- spec/app/edit-session-spec.coffee | 13 +++++++++++++ src/app/edit-session.coffee | 2 ++ 2 files changed, 15 insertions(+) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 1a4d3754d..023700307 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -2033,6 +2033,19 @@ describe "EditSession", -> editSession.buffer.reload() expect(editSession.getCursorScreenPosition()).toEqual [0,1] + describe "when the 'grammars-loaded' event is triggered on the syntax global", -> + it "reloads the edit session's grammar and re-tokenizes the buffer if it changes", -> + editSession.destroy() + grammarToReturn = syntax.grammarByFileTypeSuffix('txt') + spyOn(syntax, 'grammarForFilePath').andCallFake -> grammarToReturn + + editSession = project.buildEditSession('sample.js', autoIndent: false) + expect(editSession.lineForScreenRow(0).tokens).toHaveLength 1 + + grammarToReturn = syntax.grammarByFileTypeSuffix('js') + syntax.trigger 'grammars-loaded' + expect(editSession.lineForScreenRow(0).tokens.length).toBeGreaterThan 1 + describe "auto-indent", -> describe "editor.autoIndent", -> it "auto-indents newlines if editor.autoIndent is true", -> diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 0d48a1f4e..6b5964ca1 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -61,6 +61,8 @@ class EditSession @subscribe @displayBuffer, "changed", (e) => @trigger 'screen-lines-changed', e + @subscribe syntax, 'grammars-loaded', => @reloadGrammar() + getViewClass: -> require 'editor' From 0375d7f45a34b5e263075b8323a8871ff70d4582 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 7 Mar 2013 14:25:34 -0800 Subject: [PATCH 236/308] :lipstick: --- src/app/edit-session.coffee | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 6b5964ca1..0104a297f 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -843,11 +843,10 @@ class EditSession getGrammar: -> @languageMode.grammar reloadGrammar: -> - grammarChanged = @languageMode.reloadGrammar() - if grammarChanged + if @languageMode.reloadGrammar() @unfoldAll() @displayBuffer.tokenizedBuffer.resetScreenLines() - grammarChanged + true getDebugSnapshot: -> [ From 98c9012bdb9f9b28ed2da7db05db1e3960864b20 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 7 Mar 2013 14:31:04 -0800 Subject: [PATCH 237/308] :shit: --- spec/app/edit-session-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 023700307..d9f7be7f6 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -2040,7 +2040,7 @@ describe "EditSession", -> spyOn(syntax, 'grammarForFilePath').andCallFake -> grammarToReturn editSession = project.buildEditSession('sample.js', autoIndent: false) - expect(editSession.lineForScreenRow(0).tokens).toHaveLength 1 + expect(editSession.lineForScreenRow(0).tokens.length).toBe 1 grammarToReturn = syntax.grammarByFileTypeSuffix('js') syntax.trigger 'grammars-loaded' From ff50bc2e6f6179e8e7aa82f337a42ebf6427e13d Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 7 Mar 2013 11:04:17 -0800 Subject: [PATCH 238/308] Add toBeInstanceOf and toHaveLength jasmine matchers --- spec/spec-helper.coffee | 14 ++++ .../spec/markdown-preview-spec.coffee | 66 +++++++++++++++++-- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 4632db296..151ca8488 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -68,6 +68,8 @@ beforeEach -> spyOn($native, 'writeToPasteboard').andCallFake (text) -> pasteboardContent = text spyOn($native, 'readFromPasteboard').andCallFake -> pasteboardContent + addCustomMatchers(this) + afterEach -> keymap.bindingSets = bindingSetsToRestore keymap.bindingSetsByFirstKeystrokeToRestore = bindingSetsByFirstKeystrokeToRestore @@ -121,6 +123,18 @@ jasmine.unspy = (object, methodName) -> throw new Error("Not a spy") unless object[methodName].originalValue? object[methodName] = object[methodName].originalValue +addCustomMatchers = (spec) -> + spec.addMatchers + toBeInstanceOf: (expected) -> + notText = if @isNot then " not" else "" + this.message = => "Expected #{jasmine.pp(@actual)} to#{notText} be instance of #{expected.name} class" + @actual instanceof expected + + toHaveLength: (expected) -> + notText = if @isNot then " not" else "" + this.message = => "Expected object with length #{@actual.length} to#{notText} have length #{expected}" + @actual.length == expected + window.keyIdentifierForKey = (key) -> if key.length > 1 # named key key diff --git a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee index 7704768ee..8f56b2c88 100644 --- a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee +++ b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee @@ -1,14 +1,72 @@ $ = require 'jquery' RootView = require 'root-view' -MarkdownPreview = require 'markdown-preview/lib/markdown-preview-view' +MarkdownPreviewView = require 'markdown-preview/lib/markdown-preview-view' _ = require 'underscore' -describe "MarkdownPreview", -> +describe "MarkdownPreviewView", -> beforeEach -> project.setPath(project.resolve('markdown')) window.rootView = new RootView window.loadPackage("markdown-preview") - spyOn(MarkdownPreview.prototype, 'loadHtml') + spyOn(MarkdownPreviewView.prototype, 'loadHtml') + + fdescribe "markdown-preview:show", -> + beforeEach -> + rootView.open("file.markdown") + + describe "when the active item is an edit session", -> + beforeEach -> + rootView.attachToDom() + + describe "when a preview item has not been created for the edit session's uri", -> + describe "when there is more than one pane", -> + it "shows a markdown preview for the current buffer on the next pane", -> + rootView.getActivePane().splitRight() + [pane1, pane2] = rootView.getPanes() + pane1.focus() + + rootView.getActiveView().trigger 'markdown-preview:show' + + preview = pane2.activeItem + expect(preview).toBeInstanceOf(MarkdownPreviewView) + expect(preview.buffer).toBe rootView.getActivePaneItem().buffer + expect(pane1).toMatchSelector(':has(:focus)') + + describe "when there is only one pane", -> + it "splits the current pane to the right with a markdown preview for the current buffer", -> + expect(rootView.getPanes()).toHaveLength 1 + + rootView.getActiveView().trigger 'markdown-preview:show' + + expect(rootView.getPanes()).toHaveLength 2 + [pane1, pane2] = rootView.getPanes() + + expect(pane2.items).toHaveLength 1 + preview = pane2.activeItem + expect(preview).toBeInstanceOf(MarkdownPreviewView) + expect(preview.buffer).toBe rootView.getActivePaneItem().buffer + expect(pane1).toMatchSelector(':has(:focus)') + + describe "when a preview item has already been created for the edit session's uri", -> + it "updates and shows the existing preview item if it isn't displayed", -> + rootView.getActiveView().trigger 'markdown-preview:show' + [pane1, pane2] = rootView.getPanes() + pane2.focus() + expect(rootView.getActivePane()).toBe pane2 + preview = pane2.activeItem + expect(preview).toBeInstanceOf(MarkdownPreviewView) + rootView.open() + expect(pane2.activeItem).not.toBe preview + pane1.focus() + + rootView.getActiveView().trigger 'markdown-preview:show' + expect(rootView.getPanes()).toHaveLength 2 + expect(pane2.getItems()).toHaveLength 2 + expect(pane2.activeItem).toBe preview + expect(pane1).toMatchSelector(':has(:focus)') + + describe "when the active item is not an edit session ", -> + it "logs a warning to the console saying that it isn't possible to preview the item", -> describe "markdown-preview:toggle event", -> it "toggles on/off a preview for a .md file", -> @@ -50,7 +108,7 @@ describe "MarkdownPreview", -> 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() + expect(MarkdownPreviewView.prototype.loadHtml).not.toHaveBeenCalled() describe "core:cancel event", -> it "removes markdown preview", -> From d84614866a35e4f463142e09fe8af53de7b3f0cd Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 7 Mar 2013 12:21:20 -0800 Subject: [PATCH 239/308] Add Pane.getNextPane --- spec/app/pane-spec.coffee | 8 ++++++++ src/app/pane.coffee | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee index 3c62fc797..96f379acd 100644 --- a/spec/app/pane-spec.coffee +++ b/spec/app/pane-spec.coffee @@ -450,6 +450,14 @@ describe "Pane", -> pane.remove() expect(rootView.focus).not.toHaveBeenCalled() + describe ".getNextPane()", -> + it "returns the next pane if one exists, wrapping around from the last pane to the first", -> + pane.showItem(editSession1) + expect(pane.getNextPane()).toBeUndefined + pane2 = pane.splitRight() + expect(pane.getNextPane()).toBe pane2 + expect(pane2.getNextPane()).toBe pane + describe "when the pane is focused", -> it "focuses the active item view", -> focusHandler = jasmine.createSpy("focusHandler") diff --git a/src/app/pane.coffee b/src/app/pane.coffee index ef29ce72f..b0134f552 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -72,6 +72,12 @@ class Pane extends View isActive: -> @hasClass('active') + getNextPane: -> + panes = @getContainer()?.getPanes() + return unless panes.length > 1 + nextIndex = (panes.indexOf(this) + 1) % panes.length + panes[nextIndex] + getItems: -> new Array(@items...) From e26d2e5637c1911a8fe95da281f2860fda608c6a Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 7 Mar 2013 14:53:42 -0800 Subject: [PATCH 240/308] WIP: Preview markdown in next pane, splitting current pane if needed --- .../keymaps/markdown-preview.cson | 5 +-- .../lib/markdown-preview-view.coffee | 35 ++++++++++++++----- src/packages/markdown-preview/package.cson | 2 +- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/packages/markdown-preview/keymaps/markdown-preview.cson b/src/packages/markdown-preview/keymaps/markdown-preview.cson index d98f00093..af52d2f0c 100644 --- a/src/packages/markdown-preview/keymaps/markdown-preview.cson +++ b/src/packages/markdown-preview/keymaps/markdown-preview.cson @@ -1,5 +1,2 @@ '.editor': - 'ctrl-m': 'markdown-preview:toggle' - -'.markdown-preview': - 'ctrl-m': 'markdown-preview:toggle' + 'ctrl-m': 'markdown-preview:show' diff --git a/src/packages/markdown-preview/lib/markdown-preview-view.coffee b/src/packages/markdown-preview/lib/markdown-preview-view.coffee index b78827a83..4292cf072 100644 --- a/src/packages/markdown-preview/lib/markdown-preview-view.coffee +++ b/src/packages/markdown-preview/lib/markdown-preview-view.coffee @@ -1,23 +1,40 @@ -ScrollView = require 'scroll-view' fs = require 'fs' $ = require 'jquery' +ScrollView = require 'scroll-view' {$$$} = require 'space-pen' module.exports = class MarkdownPreviewView extends ScrollView @activate: -> @instance = new MarkdownPreviewView + rootView.command 'markdown-preview:show', '.editor', => @show() + + @show: -> + activePane = rootView.getActivePane() + editSession = activePane.activeItem + if nextPane = activePane.getNextPane() + if preview = nextPane.itemForUri("markdown-preview:#{editSession.getPath()}") + nextPane.showItem(preview) + else + nextPane.showItem(new MarkdownPreviewView(editSession.buffer)) + else + activePane.splitRight(new MarkdownPreviewView(editSession.buffer)) + activePane.focus() @content: -> @div class: 'markdown-preview', tabindex: -1, => @div class: 'markdown-body', outlet: 'markdownBody' - initialize: -> + initialize: (@buffer) -> super rootView.command 'markdown-preview:toggle', => @toggle() - @on 'blur', => @detach() unless document.activeElement is this[0] - @command 'core:cancel', => @detach() + + getTitle: -> + "Markdown Preview" + + getUri: -> + "markdown-preview:#{@buffer.getPath()}" toggle: -> if @hasParent() @@ -33,11 +50,11 @@ class MarkdownPreviewView extends ScrollView @focus() detach: -> - return if @detaching - @detaching = true - super - rootView.focus() - @detaching = false +# return if @detaching +# @detaching = true +# super +# rootView.focus() +# @detaching = false getActiveText: -> rootView.getActiveView()?.getText() diff --git a/src/packages/markdown-preview/package.cson b/src/packages/markdown-preview/package.cson index deea08f07..ce5b1ff39 100644 --- a/src/packages/markdown-preview/package.cson +++ b/src/packages/markdown-preview/package.cson @@ -1,3 +1,3 @@ 'main': 'lib/markdown-preview-view' 'activationEvents': - 'markdown-preview:toggle': '.editor' + 'markdown-preview:show': '.editor' From 0f1ffdaee85afc7fb990e255a568cce928e7015b Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 7 Mar 2013 15:34:28 -0800 Subject: [PATCH 241/308] Set the window title to 'untitled' when the active item has no title --- src/app/root-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 2d3b8fa83..659b331c9 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -115,7 +115,7 @@ class RootView extends View updateTitle: -> if projectPath = project.getPath() if item = @getActivePaneItem() - @setTitle("#{item.getTitle()} - #{projectPath}") + @setTitle("#{item.getTitle?() ? 'untitled'} - #{projectPath}") else @setTitle(projectPath) else From 7e33bd17e09336b48732d8b2f999c7d9a1a15b37 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Thu, 7 Mar 2013 15:49:33 -0800 Subject: [PATCH 242/308] Make markdown preview views fit into panes and work w/ a single buffer --- spec/fixtures/markdown/file.markdown | 3 + .../lib/markdown-preview-view.coffee | 82 +---- .../lib/markdown-preview.coffee | 24 ++ src/packages/markdown-preview/package.cson | 2 +- .../spec/markdown-preview-spec.coffee | 85 +---- .../spec/markdown-preview-view-spec.coffee | 34 ++ .../stylesheets/markdown-preview.css | 314 +++++++++--------- 7 files changed, 233 insertions(+), 311 deletions(-) create mode 100644 src/packages/markdown-preview/lib/markdown-preview.coffee create mode 100644 src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee diff --git a/spec/fixtures/markdown/file.markdown b/spec/fixtures/markdown/file.markdown index e69de29bb..0eec6a120 100644 --- a/spec/fixtures/markdown/file.markdown +++ b/spec/fixtures/markdown/file.markdown @@ -0,0 +1,3 @@ +## File.markdown + +:cool: \ No newline at end of file diff --git a/src/packages/markdown-preview/lib/markdown-preview-view.coffee b/src/packages/markdown-preview/lib/markdown-preview-view.coffee index 4292cf072..1575d9e24 100644 --- a/src/packages/markdown-preview/lib/markdown-preview-view.coffee +++ b/src/packages/markdown-preview/lib/markdown-preview-view.coffee @@ -5,62 +5,21 @@ ScrollView = require 'scroll-view' module.exports = class MarkdownPreviewView extends ScrollView - @activate: -> - @instance = new MarkdownPreviewView - rootView.command 'markdown-preview:show', '.editor', => @show() - - @show: -> - activePane = rootView.getActivePane() - editSession = activePane.activeItem - if nextPane = activePane.getNextPane() - if preview = nextPane.itemForUri("markdown-preview:#{editSession.getPath()}") - nextPane.showItem(preview) - else - nextPane.showItem(new MarkdownPreviewView(editSession.buffer)) - else - activePane.splitRight(new MarkdownPreviewView(editSession.buffer)) - activePane.focus() - @content: -> - @div class: 'markdown-preview', tabindex: -1, => - @div class: 'markdown-body', outlet: 'markdownBody' + @div class: 'markdown-preview', tabindex: -1 initialize: (@buffer) -> super - - rootView.command 'markdown-preview:toggle', => @toggle() + @fetchRenderedMarkdown() getTitle: -> - "Markdown Preview" + "Markdown Preview – #{@buffer.getBaseName()}" getUri: -> "markdown-preview:#{@buffer.getPath()}" - toggle: -> - if @hasParent() - @detach() - else - @attach() - - attach: -> - return unless @isMarkdownEditor() - rootView.append(this) - @markdownBody.html(@getLoadingHtml()) - @loadHtml() - @focus() - - detach: -> -# return if @detaching -# @detaching = true -# super -# rootView.focus() -# @detaching = false - - getActiveText: -> - rootView.getActiveView()?.getText() - - getErrorHtml: (error) -> - $$$ -> + setErrorHtml: -> + @html $$$ -> @h2 'Previewing Markdown Failed' @h3 'Possible Reasons' @ul => @@ -69,29 +28,18 @@ class MarkdownPreviewView extends ScrollView @a 'github.com', href: 'https://github.com' @span '.' - getLoadingHtml: -> - $$$ -> - @div class: 'markdown-spinner', 'Loading Markdown...' + setLoading: -> + @html($$$ -> @div class: 'markdown-spinner', 'Loading Markdown...') - loadHtml: (text) -> - payload = - mode: 'markdown' - text: @getActiveText() - request = + fetchRenderedMarkdown: (text) -> + @setLoading() + $.ajax url: 'https://api.github.com/markdown' type: 'POST' dataType: 'html' contentType: 'application/json; charset=UTF-8' - data: JSON.stringify(payload) - success: (html) => @setHtml(html) - error: (jqXhr, error) => @setHtml(@getErrorHtml(error)) - $.ajax(request) - - setHtml: (html) -> - @markdownBody.html(html) if @hasParent() - - isMarkdownEditor: (path) -> - editor = rootView.getActiveView() - return unless editor? - return true if editor.getGrammar().scopeName is 'source.gfm' - path and fs.isMarkdownExtension(fs.extension(path)) + data: JSON.stringify + mode: 'markdown' + text: @buffer.getText() + success: (html) => @html(html) + error: => @setErrorHtml() diff --git a/src/packages/markdown-preview/lib/markdown-preview.coffee b/src/packages/markdown-preview/lib/markdown-preview.coffee new file mode 100644 index 000000000..588545e83 --- /dev/null +++ b/src/packages/markdown-preview/lib/markdown-preview.coffee @@ -0,0 +1,24 @@ +EditSession = require 'edit-session' +MarkdownPreviewView = require 'markdown-preview/lib/markdown-preview-view' + +module.exports = + activate: -> + rootView.command 'markdown-preview:show', '.editor', => @show() + + show: -> + activePane = rootView.getActivePane() + item = activePane.activeItem + + if not item instanceof EditSession + console.warn("Can not render markdown for #{item.getUri()}") + return + + editSession = item + if nextPane = activePane.getNextPane() + if preview = nextPane.itemForUri("markdown-preview:#{editSession.getPath()}") + nextPane.showItem(preview) + else + nextPane.showItem(new MarkdownPreviewView(editSession.buffer)) + else + activePane.splitRight(new MarkdownPreviewView(editSession.buffer)) + activePane.focus() \ No newline at end of file diff --git a/src/packages/markdown-preview/package.cson b/src/packages/markdown-preview/package.cson index ce5b1ff39..dbd69aef5 100644 --- a/src/packages/markdown-preview/package.cson +++ b/src/packages/markdown-preview/package.cson @@ -1,3 +1,3 @@ -'main': 'lib/markdown-preview-view' +'main': 'lib/markdown-preview' 'activationEvents': 'markdown-preview:show': '.editor' diff --git a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee index 8f56b2c88..8337ea46f 100644 --- a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee +++ b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee @@ -1,16 +1,15 @@ -$ = require 'jquery' RootView = require 'root-view' MarkdownPreviewView = require 'markdown-preview/lib/markdown-preview-view' -_ = require 'underscore' +{$$} = require 'space-pen' -describe "MarkdownPreviewView", -> +describe "MarkdownPreview package", -> beforeEach -> project.setPath(project.resolve('markdown')) window.rootView = new RootView window.loadPackage("markdown-preview") - spyOn(MarkdownPreviewView.prototype, 'loadHtml') + spyOn(MarkdownPreviewView.prototype, 'fetchRenderedMarkdown') - fdescribe "markdown-preview:show", -> + describe "markdown-preview:show", -> beforeEach -> rootView.open("file.markdown") @@ -64,79 +63,3 @@ describe "MarkdownPreviewView", -> expect(pane2.getItems()).toHaveLength 2 expect(pane2.activeItem).toBe preview expect(pane1).toMatchSelector(':has(:focus)') - - describe "when the active item is not an edit session ", -> - it "logs a warning to the console saying that it isn't possible to preview the item", -> - - describe "markdown-preview:toggle event", -> - it "toggles on/off a preview for a .md file", -> - 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(rootView.find('.markdown-preview')).toExist() - expect(markdownPreviewView.loadHtml).toHaveBeenCalled() - markdownPreviewView.trigger('markdown-preview:toggle') - expect(rootView.find('.markdown-preview')).not.toExist() - - it "displays a preview for a .markdown file", -> - rootView.open('file.markdown') - editor = rootView.getActiveView() - expect(rootView.find('.markdown-preview')).not.toExist() - editor.trigger('markdown-preview:toggle') - expect(rootView.find('.markdown-preview')).toExist() - markdownPreviewView = rootView.find('.markdown-preview')?.view() - expect(markdownPreviewView.loadHtml).toHaveBeenCalled() - - 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.getActiveView() - project.addGrammarOverrideForPath(editor.getPath(), gfmGrammar) - editor.reloadGrammar() - expect(rootView.find('.markdown-preview')).not.toExist() - editor.trigger('markdown-preview:toggle') - expect(rootView.find('.markdown-preview')).toExist() - markdownPreviewView = rootView.find('.markdown-preview')?.view() - expect(markdownPreviewView.loadHtml).toHaveBeenCalled() - - it "does not display a preview for non-markdown file", -> - rootView.open('file.js') - editor = rootView.getActiveView() - expect(rootView.find('.markdown-preview')).not.toExist() - editor.trigger('markdown-preview:toggle') - expect(rootView.find('.markdown-preview')).not.toExist() - expect(MarkdownPreviewView.prototype.loadHtml).not.toHaveBeenCalled() - - 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() - - 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() - - 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/markdown-preview/spec/markdown-preview-view-spec.coffee b/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee new file mode 100644 index 000000000..59d3202ae --- /dev/null +++ b/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee @@ -0,0 +1,34 @@ +MarkdownPreviewView = require 'markdown-preview/lib/markdown-preview-view' +$ = require 'jquery' +{$$$} = require 'space-pen' + +describe "MarkdownPreviewView", -> + [buffer, preview] = [] + + beforeEach -> + spyOn($, 'ajax') + project.setPath(project.resolve('markdown')) + buffer = project.bufferForPath('file.markdown') + preview = new MarkdownPreviewView(buffer) + + afterEach -> + buffer.release() + + describe "on construction", -> + ajaxArgs = null + + beforeEach -> + ajaxArgs = $.ajax.argsForCall[0][0] + + it "shows a loading spinner and fetches the rendered markdown", -> + expect(preview.find('.markdown-spinner')).toExist() + expect($.ajax).toHaveBeenCalled() + + expect(JSON.parse(ajaxArgs.data).text).toBe buffer.getText() + + ajaxArgs.success($$$ -> @div "WWII", class: 'private-ryan') + expect(preview.find(".private-ryan")).toExist() + + it "shows an error message on error", -> + ajaxArgs.error() + expect(preview.text()).toContain "Failed" diff --git a/src/packages/markdown-preview/stylesheets/markdown-preview.css b/src/packages/markdown-preview/stylesheets/markdown-preview.css index 1138dc1b7..9eb41bd60 100644 --- a/src/packages/markdown-preview/stylesheets/markdown-preview.css +++ b/src/packages/markdown-preview/stylesheets/markdown-preview.css @@ -2,38 +2,28 @@ font-family: "Helvetica Neue", Helvetica, sans-serif; font-size: 14px; line-height: 1.6; - position: absolute; - width: 100%; - height: 100%; - top: 0px; - left: 0px; background-color: #fff; overflow: auto; - z-index: 3; - box-sizing: border-box; padding: 20px; + -webkit-flex: 1; } -.markdown-body { - min-width: 680px; -} - -.markdown-body pre, -.markdown-body code, -.markdown-body tt { +.markdown-preview pre, +.markdown-preview code, +.markdown-preview tt { font-size: 12px; font-family: Consolas, "Liberation Mono", Courier, monospace; } -.markdown-body a { +.markdown-preview a { color: #4183c4; } -.markdown-body ol > li { +.markdown-preview ol > li { list-style-type: decimal; } -.markdown-body ul > li { +.markdown-preview ul > li { list-style-type: disc; } @@ -50,17 +40,17 @@ /* this code below was copied from https://github.com/assets/stylesheets/primer/components/markdown.css */ /* we really need to get primer in here somehow. */ -.markdown-body { +.markdown-preview { font-size: 14px; line-height: 1.6; overflow: hidden; } - .markdown-body > *:first-child { + .markdown-preview > *:first-child { margin-top: 0 !important; } - .markdown-body > *:last-child { + .markdown-preview > *:last-child { margin-bottom: 0 !important; } - .markdown-body a.absent { + .markdown-preview a.absent { color: #c00; } - .markdown-body a.anchor { + .markdown-preview a.anchor { display: block; padding-left: 30px; margin-left: -30px; @@ -69,130 +59,130 @@ top: 0; left: 0; bottom: 0; } - .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { + .markdown-preview h1, .markdown-preview h2, .markdown-preview h3, .markdown-preview h4, .markdown-preview h5, .markdown-preview h6 { margin: 20px 0 10px; padding: 0; font-weight: bold; -webkit-font-smoothing: antialiased; cursor: text; position: relative; } - .markdown-body h1 .mini-icon-link, .markdown-body h2 .mini-icon-link, .markdown-body h3 .mini-icon-link, .markdown-body h4 .mini-icon-link, .markdown-body h5 .mini-icon-link, .markdown-body h6 .mini-icon-link { + .markdown-preview h1 .mini-icon-link, .markdown-preview h2 .mini-icon-link, .markdown-preview h3 .mini-icon-link, .markdown-preview h4 .mini-icon-link, .markdown-preview h5 .mini-icon-link, .markdown-preview h6 .mini-icon-link { display: none; color: #000; } - .markdown-body h1:hover a.anchor, .markdown-body h2:hover a.anchor, .markdown-body h3:hover a.anchor, .markdown-body h4:hover a.anchor, .markdown-body h5:hover a.anchor, .markdown-body h6:hover a.anchor { + .markdown-preview h1:hover a.anchor, .markdown-preview h2:hover a.anchor, .markdown-preview h3:hover a.anchor, .markdown-preview h4:hover a.anchor, .markdown-preview h5:hover a.anchor, .markdown-preview h6:hover a.anchor { text-decoration: none; line-height: 1; padding-left: 0; margin-left: -22px; top: 15%; } - .markdown-body h1:hover a.anchor .mini-icon-link, .markdown-body h2:hover a.anchor .mini-icon-link, .markdown-body h3:hover a.anchor .mini-icon-link, .markdown-body h4:hover a.anchor .mini-icon-link, .markdown-body h5:hover a.anchor .mini-icon-link, .markdown-body h6:hover a.anchor .mini-icon-link { + .markdown-preview h1:hover a.anchor .mini-icon-link, .markdown-preview h2:hover a.anchor .mini-icon-link, .markdown-preview h3:hover a.anchor .mini-icon-link, .markdown-preview h4:hover a.anchor .mini-icon-link, .markdown-preview h5:hover a.anchor .mini-icon-link, .markdown-preview h6:hover a.anchor .mini-icon-link { display: inline-block; } - .markdown-body h1 tt, .markdown-body h1 code, .markdown-body h2 tt, .markdown-body h2 code, .markdown-body h3 tt, .markdown-body h3 code, .markdown-body h4 tt, .markdown-body h4 code, .markdown-body h5 tt, .markdown-body h5 code, .markdown-body h6 tt, .markdown-body h6 code { + .markdown-preview h1 tt, .markdown-preview h1 code, .markdown-preview h2 tt, .markdown-preview h2 code, .markdown-preview h3 tt, .markdown-preview h3 code, .markdown-preview h4 tt, .markdown-preview h4 code, .markdown-preview h5 tt, .markdown-preview h5 code, .markdown-preview h6 tt, .markdown-preview h6 code { font-size: inherit; } - .markdown-body h1 { + .markdown-preview h1 { font-size: 28px; color: #000; } - .markdown-body h2 { + .markdown-preview h2 { font-size: 24px; border-bottom: 1px solid #ccc; color: #000; } - .markdown-body h3 { + .markdown-preview h3 { font-size: 18px; } - .markdown-body h4 { + .markdown-preview h4 { font-size: 16px; } - .markdown-body h5 { + .markdown-preview h5 { font-size: 14px; } - .markdown-body h6 { + .markdown-preview h6 { color: #777; font-size: 14px; } - .markdown-body p, - .markdown-body blockquote, - .markdown-body ul, .markdown-body ol, .markdown-body dl, - .markdown-body table, - .markdown-body pre { + .markdown-preview p, + .markdown-preview blockquote, + .markdown-preview ul, .markdown-preview ol, .markdown-preview dl, + .markdown-preview table, + .markdown-preview pre { margin: 15px 0; } - .markdown-body hr { + .markdown-preview hr { background: transparent url("https://a248.e.akamai.net/assets.github.com/assets/primer/markdown/dirty-shade-0e7d81b119cc9beae17b0c98093d121fa0050a74.png") repeat-x 0 0; border: 0 none; color: #ccc; height: 4px; padding: 0; } - .markdown-body > h2:first-child, .markdown-body > h1:first-child, .markdown-body > h1:first-child + h2, .markdown-body > h3:first-child, .markdown-body > h4:first-child, .markdown-body > h5:first-child, .markdown-body > h6:first-child { + .markdown-preview > h2:first-child, .markdown-preview > h1:first-child, .markdown-preview > h1:first-child + h2, .markdown-preview > h3:first-child, .markdown-preview > h4:first-child, .markdown-preview > h5:first-child, .markdown-preview > h6:first-child { margin-top: 0; padding-top: 0; } - .markdown-body a:first-child h1, .markdown-body a:first-child h2, .markdown-body a:first-child h3, .markdown-body a:first-child h4, .markdown-body a:first-child h5, .markdown-body a:first-child h6 { + .markdown-preview a:first-child h1, .markdown-preview a:first-child h2, .markdown-preview a:first-child h3, .markdown-preview a:first-child h4, .markdown-preview a:first-child h5, .markdown-preview a:first-child h6 { margin-top: 0; padding-top: 0; } - .markdown-body h1 + p, - .markdown-body h2 + p, - .markdown-body h3 + p, - .markdown-body h4 + p, - .markdown-body h5 + p, - .markdown-body h6 + p { + .markdown-preview h1 + p, + .markdown-preview h2 + p, + .markdown-preview h3 + p, + .markdown-preview h4 + p, + .markdown-preview h5 + p, + .markdown-preview h6 + p { margin-top: 0; } - .markdown-body li p.first { + .markdown-preview li p.first { display: inline-block; } - .markdown-body ul, .markdown-body ol { + .markdown-preview ul, .markdown-preview ol { padding-left: 30px; } - .markdown-body ul.no-list, .markdown-body ol.no-list { + .markdown-preview ul.no-list, .markdown-preview ol.no-list { list-style-type: none; padding: 0; } - .markdown-body ul li > :first-child, - .markdown-body ul li ul:first-of-type, .markdown-body ol li > :first-child, - .markdown-body ol li ul:first-of-type { + .markdown-preview ul li > :first-child, + .markdown-preview ul li ul:first-of-type, .markdown-preview ol li > :first-child, + .markdown-preview ol li ul:first-of-type { margin-top: 0px; } - .markdown-body ul ul, - .markdown-body ul ol, - .markdown-body ol ol, - .markdown-body ol ul { + .markdown-preview ul ul, + .markdown-preview ul ol, + .markdown-preview ol ol, + .markdown-preview ol ul { margin-bottom: 0; } - .markdown-body dl { + .markdown-preview dl { padding: 0; } - .markdown-body dl dt { + .markdown-preview dl dt { font-size: 14px; font-weight: bold; font-style: italic; padding: 0; margin: 15px 0 5px; } - .markdown-body dl dt:first-child { + .markdown-preview dl dt:first-child { padding: 0; } - .markdown-body dl dt > :first-child { + .markdown-preview dl dt > :first-child { margin-top: 0px; } - .markdown-body dl dt > :last-child { + .markdown-preview dl dt > :last-child { margin-bottom: 0px; } - .markdown-body dl dd { + .markdown-preview dl dd { margin: 0 0 15px; padding: 0 15px; } - .markdown-body dl dd > :first-child { + .markdown-preview dl dd > :first-child { margin-top: 0px; } - .markdown-body dl dd > :last-child { + .markdown-preview dl dd > :last-child { margin-bottom: 0px; } - .markdown-body blockquote { + .markdown-preview blockquote { border-left: 4px solid #DDD; padding: 0 15px; color: #777; } - .markdown-body blockquote > :first-child { + .markdown-preview blockquote > :first-child { margin-top: 0px; } - .markdown-body blockquote > :last-child { + .markdown-preview blockquote > :last-child { margin-bottom: 0px; } - .markdown-body table th { + .markdown-preview table th { font-weight: bold; } - .markdown-body table th, .markdown-body table td { + .markdown-preview table th, .markdown-preview table td { border: 1px solid #ccc; padding: 6px 13px; } - .markdown-body table tr { + .markdown-preview table tr { border-top: 1px solid #ccc; background-color: #fff; } - .markdown-body table tr:nth-child(2n) { + .markdown-preview table tr:nth-child(2n) { background-color: #f8f8f8; } - .markdown-body img { + .markdown-preview img { max-width: 100%; -moz-box-sizing: border-box; box-sizing: border-box; } - .markdown-body span.frame { + .markdown-preview span.frame { display: block; overflow: hidden; } - .markdown-body span.frame > span { + .markdown-preview span.frame > span { border: 1px solid #ddd; display: block; float: left; @@ -200,70 +190,70 @@ margin: 13px 0 0; padding: 7px; width: auto; } - .markdown-body span.frame span img { + .markdown-preview span.frame span img { display: block; float: left; } - .markdown-body span.frame span span { + .markdown-preview span.frame span span { clear: both; color: #333; display: block; padding: 5px 0 0; } - .markdown-body span.align-center { + .markdown-preview span.align-center { display: block; overflow: hidden; clear: both; } - .markdown-body span.align-center > span { + .markdown-preview span.align-center > span { display: block; overflow: hidden; margin: 13px auto 0; text-align: center; } - .markdown-body span.align-center span img { + .markdown-preview span.align-center span img { margin: 0 auto; text-align: center; } - .markdown-body span.align-right { + .markdown-preview span.align-right { display: block; overflow: hidden; clear: both; } - .markdown-body span.align-right > span { + .markdown-preview span.align-right > span { display: block; overflow: hidden; margin: 13px 0 0; text-align: right; } - .markdown-body span.align-right span img { + .markdown-preview span.align-right span img { margin: 0; text-align: right; } - .markdown-body span.float-left { + .markdown-preview span.float-left { display: block; margin-right: 13px; overflow: hidden; float: left; } - .markdown-body span.float-left span { + .markdown-preview span.float-left span { margin: 13px 0 0; } - .markdown-body span.float-right { + .markdown-preview span.float-right { display: block; margin-left: 13px; overflow: hidden; float: right; } - .markdown-body span.float-right > span { + .markdown-preview span.float-right > span { display: block; overflow: hidden; margin: 13px auto 0; text-align: right; } - .markdown-body code, .markdown-body tt { + .markdown-preview code, .markdown-preview tt { margin: 0 2px; padding: 0px 5px; border: 1px solid #eaeaea; background-color: #f8f8f8; border-radius: 3px; } - .markdown-body code { + .markdown-preview code { white-space: nowrap; } - .markdown-body pre > code { + .markdown-preview pre > code { margin: 0; padding: 0; white-space: pre; border: none; background: transparent; } - .markdown-body .highlight pre, .markdown-body pre { + .markdown-preview .highlight pre, .markdown-preview pre { background-color: #f8f8f8; border: 1px solid #ccc; font-size: 13px; @@ -271,168 +261,168 @@ overflow: auto; padding: 6px 10px; border-radius: 3px; } - .markdown-body pre code, .markdown-body pre tt { + .markdown-preview pre code, .markdown-preview pre tt { margin: 0; padding: 0; background-color: transparent; border: none; } /* this code was copied from https://github.com/assets/stylesheets/primer/components/pygments.css */ -/* the .markdown-body class was then added to all rules */ -.markdown-body .highlight { +/* the .markdown-preview class was then added to all rules */ +.markdown-preview .highlight { background: #ffffff; } - .markdown-body .highlight .c { + .markdown-preview .highlight .c { color: #999988; font-style: italic; } - .markdown-body .highlight .err { + .markdown-preview .highlight .err { color: #a61717; background-color: #e3d2d2; } - .markdown-body .highlight .k { + .markdown-preview .highlight .k { font-weight: bold; } - .markdown-body .highlight .o { + .markdown-preview .highlight .o { font-weight: bold; } - .markdown-body .highlight .cm { + .markdown-preview .highlight .cm { color: #999988; font-style: italic; } - .markdown-body .highlight .cp { + .markdown-preview .highlight .cp { color: #999999; font-weight: bold; } - .markdown-body .highlight .c1 { + .markdown-preview .highlight .c1 { color: #999988; font-style: italic; } - .markdown-body .highlight .cs { + .markdown-preview .highlight .cs { color: #999999; font-weight: bold; font-style: italic; } - .markdown-body .highlight .gd { + .markdown-preview .highlight .gd { color: #000000; background-color: #ffdddd; } - .markdown-body .highlight .gd .x { + .markdown-preview .highlight .gd .x { color: #000000; background-color: #ffaaaa; } - .markdown-body .highlight .ge { + .markdown-preview .highlight .ge { font-style: italic; } - .markdown-body .highlight .gr { + .markdown-preview .highlight .gr { color: #aa0000; } - .markdown-body .highlight .gh { + .markdown-preview .highlight .gh { color: #999999; } - .markdown-body .highlight .gi { + .markdown-preview .highlight .gi { color: #000000; background-color: #ddffdd; } - .markdown-body .highlight .gi .x { + .markdown-preview .highlight .gi .x { color: #000000; background-color: #aaffaa; } - .markdown-body .highlight .go { + .markdown-preview .highlight .go { color: #888888; } - .markdown-body .highlight .gp { + .markdown-preview .highlight .gp { color: #555555; } - .markdown-body .highlight .gs { + .markdown-preview .highlight .gs { font-weight: bold; } - .markdown-body .highlight .gu { + .markdown-preview .highlight .gu { color: #800080; font-weight: bold; } - .markdown-body .highlight .gt { + .markdown-preview .highlight .gt { color: #aa0000; } - .markdown-body .highlight .kc { + .markdown-preview .highlight .kc { font-weight: bold; } - .markdown-body .highlight .kd { + .markdown-preview .highlight .kd { font-weight: bold; } - .markdown-body .highlight .kn { + .markdown-preview .highlight .kn { font-weight: bold; } - .markdown-body .highlight .kp { + .markdown-preview .highlight .kp { font-weight: bold; } - .markdown-body .highlight .kr { + .markdown-preview .highlight .kr { font-weight: bold; } - .markdown-body .highlight .kt { + .markdown-preview .highlight .kt { color: #445588; font-weight: bold; } - .markdown-body .highlight .m { + .markdown-preview .highlight .m { color: #009999; } - .markdown-body .highlight .s { + .markdown-preview .highlight .s { color: #d14; } - .markdown-body .highlight .na { + .markdown-preview .highlight .na { color: #008080; } - .markdown-body .highlight .nb { + .markdown-preview .highlight .nb { color: #0086B3; } - .markdown-body .highlight .nc { + .markdown-preview .highlight .nc { color: #445588; font-weight: bold; } - .markdown-body .highlight .no { + .markdown-preview .highlight .no { color: #008080; } - .markdown-body .highlight .ni { + .markdown-preview .highlight .ni { color: #800080; } - .markdown-body .highlight .ne { + .markdown-preview .highlight .ne { color: #990000; font-weight: bold; } - .markdown-body .highlight .nf { + .markdown-preview .highlight .nf { color: #990000; font-weight: bold; } - .markdown-body .highlight .nn { + .markdown-preview .highlight .nn { color: #555555; } - .markdown-body .highlight .nt { + .markdown-preview .highlight .nt { color: #000080; } - .markdown-body .highlight .nv { + .markdown-preview .highlight .nv { color: #008080; } - .markdown-body .highlight .ow { + .markdown-preview .highlight .ow { font-weight: bold; } - .markdown-body .highlight .w { + .markdown-preview .highlight .w { color: #bbbbbb; } - .markdown-body .highlight .mf { + .markdown-preview .highlight .mf { color: #009999; } - .markdown-body .highlight .mh { + .markdown-preview .highlight .mh { color: #009999; } - .markdown-body .highlight .mi { + .markdown-preview .highlight .mi { color: #009999; } - .markdown-body .highlight .mo { + .markdown-preview .highlight .mo { color: #009999; } - .markdown-body .highlight .sb { + .markdown-preview .highlight .sb { color: #d14; } - .markdown-body .highlight .sc { + .markdown-preview .highlight .sc { color: #d14; } - .markdown-body .highlight .sd { + .markdown-preview .highlight .sd { color: #d14; } - .markdown-body .highlight .s2 { + .markdown-preview .highlight .s2 { color: #d14; } - .markdown-body .highlight .se { + .markdown-preview .highlight .se { color: #d14; } - .markdown-body .highlight .sh { + .markdown-preview .highlight .sh { color: #d14; } - .markdown-body .highlight .si { + .markdown-preview .highlight .si { color: #d14; } - .markdown-body .highlight .sx { + .markdown-preview .highlight .sx { color: #d14; } - .markdown-body .highlight .sr { + .markdown-preview .highlight .sr { color: #009926; } - .markdown-body .highlight .s1 { + .markdown-preview .highlight .s1 { color: #d14; } - .markdown-body .highlight .ss { + .markdown-preview .highlight .ss { color: #990073; } - .markdown-body .highlight .bp { + .markdown-preview .highlight .bp { color: #999999; } - .markdown-body .highlight .vc { + .markdown-preview .highlight .vc { color: #008080; } - .markdown-body .highlight .vg { + .markdown-preview .highlight .vg { color: #008080; } - .markdown-body .highlight .vi { + .markdown-preview .highlight .vi { color: #008080; } - .markdown-body .highlight .il { + .markdown-preview .highlight .il { color: #009999; } - .markdown-body .highlight .gc { + .markdown-preview .highlight .gc { color: #999; background-color: #EAF2F5; } -.type-csharp .markdown-body .highlight .k { +.type-csharp .markdown-preview .highlight .k { color: #0000FF; } -.type-csharp .markdown-body .highlight .kt { +.type-csharp .markdown-preview .highlight .kt { color: #0000FF; } -.type-csharp .markdown-body .highlight .nf { +.type-csharp .markdown-preview .highlight .nf { color: #000000; font-weight: normal; } -.type-csharp .markdown-body .highlight .nc { +.type-csharp .markdown-preview .highlight .nc { color: #2B91AF; } -.type-csharp .markdown-body .highlight .nn { +.type-csharp .markdown-preview .highlight .nn { color: #000000; } -.type-csharp .markdown-body .highlight .s { +.type-csharp .markdown-preview .highlight .s { color: #A31515; } -.type-csharp .markdown-body .highlight .sc { +.type-csharp .markdown-preview .highlight .sc { color: #A31515; } From f432ad350f06713e6e84dd2d131a6a4f2f72746c Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Fri, 8 Mar 2013 09:16:10 -0800 Subject: [PATCH 243/308] Exclude package-generator templates from coffee compilation Closes #359 --- script/generate-sources-gypi | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/generate-sources-gypi b/script/generate-sources-gypi index 266fde180..f890ee16f 100755 --- a/script/generate-sources-gypi +++ b/script/generate-sources-gypi @@ -5,9 +5,10 @@ set -e cd "$(dirname $0)/.." DIRS="src static vendor" +EXCLUDE_DIRS="src/packages/package-generator/template" find_files() { - find ${DIRS} -type file -name ${1} + find ${DIRS} -type file -name ${1} | grep -v ${EXCLUDE_DIRS} } file_list() { From 709d9738efb24f497f857fc36625e4cfd3fa40cc Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Fri, 8 Mar 2013 09:27:44 -0800 Subject: [PATCH 244/308] Revert "Exclude package-generator templates from coffee compilation" This reverts commit f432ad350f06713e6e84dd2d131a6a4f2f72746c. --- script/generate-sources-gypi | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/script/generate-sources-gypi b/script/generate-sources-gypi index f890ee16f..266fde180 100755 --- a/script/generate-sources-gypi +++ b/script/generate-sources-gypi @@ -5,10 +5,9 @@ set -e cd "$(dirname $0)/.." DIRS="src static vendor" -EXCLUDE_DIRS="src/packages/package-generator/template" find_files() { - find ${DIRS} -type file -name ${1} | grep -v ${EXCLUDE_DIRS} + find ${DIRS} -type file -name ${1} } file_list() { From 5cd3dfce8e6af6c5456986e6728c25a6211901ee Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Fri, 8 Mar 2013 09:28:07 -0800 Subject: [PATCH 245/308] Add .template extension to package-generator template files Acutally Closes #359 --- src/packages/package-generator/lib/package-generator-view.coffee | 1 + .../{__package-name__.cson => __package-name__.cson.template} | 0 ...-name__-view.coffee => __package-name__-view.coffee.template} | 0 ...{__package-name__.coffee => __package-name__.coffee.template} | 0 ...-name__-spec.coffee => __package-name__-spec.coffee.template} | 0 ...ew-spec.coffee => __package-name__-view-spec.coffee.template} | 0 .../{__package-name__.css => __package-name__.css.template} | 0 7 files changed, 1 insertion(+) rename src/packages/package-generator/template/keymaps/{__package-name__.cson => __package-name__.cson.template} (100%) rename src/packages/package-generator/template/lib/{__package-name__-view.coffee => __package-name__-view.coffee.template} (100%) rename src/packages/package-generator/template/lib/{__package-name__.coffee => __package-name__.coffee.template} (100%) rename src/packages/package-generator/template/spec/{__package-name__-spec.coffee => __package-name__-spec.coffee.template} (100%) rename src/packages/package-generator/template/spec/{__package-name__-view-spec.coffee => __package-name__-view-spec.coffee.template} (100%) rename src/packages/package-generator/template/stylesheets/{__package-name__.css => __package-name__.css.template} (100%) diff --git a/src/packages/package-generator/lib/package-generator-view.coffee b/src/packages/package-generator/lib/package-generator-view.coffee index d1ca900cc..a38315446 100644 --- a/src/packages/package-generator/lib/package-generator-view.coffee +++ b/src/packages/package-generator/lib/package-generator-view.coffee @@ -62,6 +62,7 @@ class PackageGeneratorView extends View for path in fs.listTree(templatePath) relativePath = path.replace(templatePath, "") relativePath = relativePath.replace(/^\//, '') + relativePath = relativePath.replace(/\.template$/, '') relativePath = @replacePackageNamePlaceholders(relativePath, packageName) sourcePath = fs.join(@getPackagePath(), relativePath) diff --git a/src/packages/package-generator/template/keymaps/__package-name__.cson b/src/packages/package-generator/template/keymaps/__package-name__.cson.template similarity index 100% rename from src/packages/package-generator/template/keymaps/__package-name__.cson rename to src/packages/package-generator/template/keymaps/__package-name__.cson.template diff --git a/src/packages/package-generator/template/lib/__package-name__-view.coffee b/src/packages/package-generator/template/lib/__package-name__-view.coffee.template similarity index 100% rename from src/packages/package-generator/template/lib/__package-name__-view.coffee rename to src/packages/package-generator/template/lib/__package-name__-view.coffee.template diff --git a/src/packages/package-generator/template/lib/__package-name__.coffee b/src/packages/package-generator/template/lib/__package-name__.coffee.template similarity index 100% rename from src/packages/package-generator/template/lib/__package-name__.coffee rename to src/packages/package-generator/template/lib/__package-name__.coffee.template diff --git a/src/packages/package-generator/template/spec/__package-name__-spec.coffee b/src/packages/package-generator/template/spec/__package-name__-spec.coffee.template similarity index 100% rename from src/packages/package-generator/template/spec/__package-name__-spec.coffee rename to src/packages/package-generator/template/spec/__package-name__-spec.coffee.template diff --git a/src/packages/package-generator/template/spec/__package-name__-view-spec.coffee b/src/packages/package-generator/template/spec/__package-name__-view-spec.coffee.template similarity index 100% rename from src/packages/package-generator/template/spec/__package-name__-view-spec.coffee rename to src/packages/package-generator/template/spec/__package-name__-view-spec.coffee.template diff --git a/src/packages/package-generator/template/stylesheets/__package-name__.css b/src/packages/package-generator/template/stylesheets/__package-name__.css.template similarity index 100% rename from src/packages/package-generator/template/stylesheets/__package-name__.css rename to src/packages/package-generator/template/stylesheets/__package-name__.css.template From 1561f22853f644579b3b066474fcdc27429991d0 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Fri, 8 Mar 2013 10:26:44 -0800 Subject: [PATCH 246/308] Clean project before tests are run --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index e26ae7a7b..5893ed201 100644 --- a/Rakefile +++ b/Rakefile @@ -83,7 +83,7 @@ task :clean do end desc "Run the specs" -task :test => ["update-cef", "clone-default-bundles", "build"] do +task :test => ["clean", "update-cef", "clone-default-bundles", "build"] do `pkill Atom` if path = application_path() cmd = "#{path}/Contents/MacOS/Atom --test --resource-path=#{ATOM_SRC_PATH} 2> /dev/null" From 22d1336aa006f2ad036a091280d9b32c097d450f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 7 Mar 2013 16:52:03 -0800 Subject: [PATCH 247/308] Apply correct flexbox styling to `.pane > .item-views > *` Markdown preview was overflowing because the min-height of flexbox items is automatically set to min-content. Setting it to 0 ensures that the item doesn't expand beyond its flex size. Moving this styling to atom.css ensures that people don't have to work too hard to fit their views into panes. --- .../markdown-preview/stylesheets/markdown-preview.css | 8 ++------ static/atom.css | 5 +++++ static/editor.css | 1 - 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/packages/markdown-preview/stylesheets/markdown-preview.css b/src/packages/markdown-preview/stylesheets/markdown-preview.css index 9eb41bd60..e45e5f436 100644 --- a/src/packages/markdown-preview/stylesheets/markdown-preview.css +++ b/src/packages/markdown-preview/stylesheets/markdown-preview.css @@ -3,9 +3,8 @@ font-size: 14px; line-height: 1.6; background-color: #fff; - overflow: auto; + overflow: scroll; padding: 20px; - -webkit-flex: 1; } .markdown-preview pre, @@ -40,10 +39,7 @@ /* this code below was copied from https://github.com/assets/stylesheets/primer/components/markdown.css */ /* we really need to get primer in here somehow. */ -.markdown-preview { - font-size: 14px; - line-height: 1.6; - overflow: hidden; } +.markdown-preview {} .markdown-preview > *:first-child { margin-top: 0 !important; } .markdown-preview > *:last-child { diff --git a/static/atom.css b/static/atom.css index 6cfb79269..7013037a3 100644 --- a/static/atom.css +++ b/static/atom.css @@ -61,6 +61,11 @@ html, body { -webkit-flex-flow: column; } +#panes .pane .item-views > * { + -webkit-flex: 1; + min-height: 0; +} + @font-face { font-family: 'Octicons Regular'; src: url("octicons-regular-webfont.woff") format("woff"); diff --git a/static/editor.css b/static/editor.css index 1b6c35a74..c88b3f1f2 100644 --- a/static/editor.css +++ b/static/editor.css @@ -6,7 +6,6 @@ z-index: 0; font-family: Inconsolata, Monaco, Courier; line-height: 1.3; - -webkit-flex: 1; } .editor.mini { From cf6c46ba3ac55fed987f24ca4a8f8a7ca0219312 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Fri, 8 Mar 2013 11:10:13 -0800 Subject: [PATCH 248/308] Tree view deselects entry when active item has no path --- src/packages/tree-view/lib/tree-view.coffee | 11 ++++++-- .../tree-view/spec/tree-view-spec.coffee | 28 ++++++++++++------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/packages/tree-view/lib/tree-view.coffee b/src/packages/tree-view/lib/tree-view.coffee index bf1df2b2e..5a6ea480f 100644 --- a/src/packages/tree-view/lib/tree-view.coffee +++ b/src/packages/tree-view/lib/tree-view.coffee @@ -127,8 +127,10 @@ class TreeView extends ScrollView @root = null selectActiveFile: -> - activeFilePath = rootView.getActiveView()?.getPath() - @selectEntryForPath(activeFilePath) if activeFilePath + if activeFilePath = rootView.getActiveView()?.getPath?() + @selectEntryForPath(activeFilePath) + else + @deselect() revealActiveFile: -> @attach() @@ -290,9 +292,12 @@ class TreeView extends ScrollView return false unless entry.get(0) entry = entry.view() unless entry instanceof View @selectedPath = entry.getPath() - @treeViewList.find('.selected').removeClass('selected') + @deselect() entry.addClass('selected') + deselect: -> + @treeViewList.find('.selected').removeClass('selected') + scrollTop: (top) -> if top @treeViewList.scrollTop(top) diff --git a/src/packages/tree-view/spec/tree-view-spec.coffee b/src/packages/tree-view/spec/tree-view-spec.coffee index fc2f9fe81..2bd5ac3d6 100644 --- a/src/packages/tree-view/spec/tree-view-spec.coffee +++ b/src/packages/tree-view/spec/tree-view-spec.coffee @@ -1,4 +1,5 @@ $ = require 'jquery' +{$$} = require 'space-pen' _ = require 'underscore' TreeView = require 'tree-view/lib/tree-view' RootView = require 'root-view' @@ -301,18 +302,25 @@ describe "TreeView", -> expect(subdir).toHaveClass 'expanded' expect(rootView.getActiveView().isFocused).toBeFalsy() - describe "when a new file is opened in the active editor", -> - 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')) + describe "when the active item changes on the active pane", -> + describe "when the item has a path", -> + it "selects the entry with that path in the tree view if it is visible", -> + sampleJs.click() + rootView.open(require.resolve('fixtures/tree-view/tree-view.txt')) - expect(sampleTxt).toHaveClass 'selected' - expect(treeView.find('.selected').length).toBe 1 + expect(sampleTxt).toHaveClass 'selected' + expect(treeView.find('.selected').length).toBe 1 - 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' + it "selects the path's parent dir if its 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 the item has no path", -> + it "deselects the previously selected entry", -> + sampleJs.click() + rootView.getActivePane().showItem($$ -> @div('hello')) + expect(rootView.find('.selected')).not.toExist() describe "when a different editor becomes active", -> it "selects the file in that is open in that editor", -> From 8ca8841f9ed90489fb40c4064129a2356079f33f Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Fri, 8 Mar 2013 11:14:44 -0800 Subject: [PATCH 249/308] Make command-panel's editor have -webkit-flex: 1 We moved the setting of flex on editors to the panes stylesheet previously, but here is another context where we need flex to be set. --- static/command-panel.css | 1 + 1 file changed, 1 insertion(+) diff --git a/static/command-panel.css b/static/command-panel.css index 8f2098259..a169d2759 100644 --- a/static/command-panel.css +++ b/static/command-panel.css @@ -122,6 +122,7 @@ .command-panel .prompt-and-editor .editor { position: relative; + -webkit-flex: 1; } .command-panel .prompt-and-editor { From 06c9a3ac8603270a3b48bb9bc206e4e7e7071a74 Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Fri, 8 Mar 2013 11:45:20 -0800 Subject: [PATCH 250/308] Remove empty panes when PaneContainer deserializes --- spec/app/pane-container-spec.coffee | 7 +++++++ src/app/pane-container.coffee | 5 +++++ src/app/pane.coffee | 7 ++++--- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/spec/app/pane-container-spec.coffee b/spec/app/pane-container-spec.coffee index 56ea53336..c448ca3d2 100644 --- a/spec/app/pane-container-spec.coffee +++ b/spec/app/pane-container-spec.coffee @@ -146,3 +146,10 @@ describe "PaneContainer", -> 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 + + it "removes empty panes on deserialization", -> + # only deserialize pane 1's view successfully + TestView.deserialize = ({name}) -> new TestView(name) if name is '1' + newContainer = deserialize(container.serialize()) + expect(newContainer.find('.row, .column')).not.toExist() + expect(newContainer.find('> :contains(1)')).toExist() diff --git a/src/app/pane-container.coffee b/src/app/pane-container.coffee index 6fc367e56..225716d0c 100644 --- a/src/app/pane-container.coffee +++ b/src/app/pane-container.coffee @@ -9,6 +9,7 @@ class PaneContainer extends View @deserialize: ({root}) -> container = new PaneContainer container.append(deserialize(root)) if root + container.removeEmptyPanes() container @content: -> @@ -93,5 +94,9 @@ class PaneContainer extends View root.css(width: '100%', height: '100%', top: 0, left: 0) root.adjustDimensions() + removeEmptyPanes: -> + for pane in @getPanes() when pane.getItems().length == 0 + pane.remove() + afterAttach: -> @adjustPaneDimensions() diff --git a/src/app/pane.coffee b/src/app/pane.coffee index b0134f552..240540042 100644 --- a/src/app/pane.coffee +++ b/src/app/pane.coffee @@ -11,7 +11,8 @@ class Pane extends View @div class: 'item-views', outlet: 'itemViews' @deserialize: ({items, focused, activeItemUri}) -> - pane = new Pane(items.map((item) -> deserialize(item))...) + deserializedItems = _.compact(items.map((item) -> deserialize(item))) + pane = new Pane(deserializedItems...) pane.showItemForUri(activeItemUri) if activeItemUri pane.focusOnAttach = true if focused pane @@ -21,7 +22,7 @@ class Pane extends View initialize: (@items...) -> @viewsByClassName = {} - @showItem(@items[0]) + @showItem(@items[0]) if @items.length > 0 @command 'core:close', @destroyActiveItem @command 'core:save', @saveActiveItem @@ -46,7 +47,7 @@ class Pane extends View @command 'pane:split-down', => @splitDown() @command 'pane:close', => @destroyItems() @command 'pane:close-other-items', => @destroyInactiveItems() - @on 'focus', => @activeView.focus(); false + @on 'focus', => @activeView?.focus(); false @on 'focusin', => @makeActive() @on 'focusout', => @autosaveActiveItem() From f3910ba34e993beba65d229525cb84334f2e26be Mon Sep 17 00:00:00 2001 From: Corey Johnson & Nathan Sobo Date: Fri, 8 Mar 2013 11:59:49 -0800 Subject: [PATCH 251/308] WIP: make markdown preview view serializable --- .../markdown-preview/lib/markdown-preview-view.coffee | 9 +++++++++ .../spec/markdown-preview-view-spec.coffee | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/src/packages/markdown-preview/lib/markdown-preview-view.coffee b/src/packages/markdown-preview/lib/markdown-preview-view.coffee index 1575d9e24..f06842de6 100644 --- a/src/packages/markdown-preview/lib/markdown-preview-view.coffee +++ b/src/packages/markdown-preview/lib/markdown-preview-view.coffee @@ -5,6 +5,11 @@ ScrollView = require 'scroll-view' module.exports = class MarkdownPreviewView extends ScrollView + registerDeserializer(this) + + @deserialize: ({path}) -> + new MarkdownPreviewView(project.bufferForPath(path)) + @content: -> @div class: 'markdown-preview', tabindex: -1 @@ -12,6 +17,10 @@ class MarkdownPreviewView extends ScrollView super @fetchRenderedMarkdown() + serialize: -> + deserializer: 'MarkdownPreviewView' + path: @buffer.getPath() + getTitle: -> "Markdown Preview – #{@buffer.getBaseName()}" diff --git a/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee b/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee index 59d3202ae..7d98a70dc 100644 --- a/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee +++ b/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee @@ -32,3 +32,8 @@ describe "MarkdownPreviewView", -> it "shows an error message on error", -> ajaxArgs.error() expect(preview.text()).toContain "Failed" + + describe "serialization", -> + fit "reassociates with the same buffer when deserialized", -> + newPreview = deserialize(preview.serialize()) + expect(newPreview.buffer).toBe buffer From 110d3719bb2a85e4034978bb0b03d7fc56a22530 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 8 Mar 2013 13:15:41 -0800 Subject: [PATCH 252/308] Use actual root path length Previously a one was added just to use for the length with the null byte when creating the root path passed to fts_open. Closes #391 --- native/v8_extensions/native.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/native/v8_extensions/native.mm b/native/v8_extensions/native.mm index 21f7e6c3a..71a975c8f 100644 --- a/native/v8_extensions/native.mm +++ b/native/v8_extensions/native.mm @@ -166,8 +166,8 @@ namespace v8_extensions { } else if (name == "traverseTree") { std::string argument = arguments[0]->GetStringValue().ToString(); - int rootPathLength = argument.size() + 1; - char rootPath[rootPathLength]; + int rootPathLength = argument.size(); + char rootPath[rootPathLength + 1]; strcpy(rootPath, argument.c_str()); char * const paths[] = {rootPath, NULL}; From 8cf32149b7e9dda31eea36125ae6259a932dfacb Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 8 Mar 2013 13:42:31 -0800 Subject: [PATCH 253/308] Return absolute paths from $native.traverseTree() Previously relative paths were generated even though things like fs.list() and fs.listTree() would just recombine them with the root path. Closes #391 --- native/v8_extensions/native.mm | 9 ++------- spec/stdlib/fs-spec.coffee | 12 +++++++----- src/app/config.coffee | 12 ++++++------ .../fuzzy-finder/lib/load-paths-handler.coffee | 2 ++ src/stdlib/fs.coffee | 6 +++--- 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/native/v8_extensions/native.mm b/native/v8_extensions/native.mm index 71a975c8f..b34b68145 100644 --- a/native/v8_extensions/native.mm +++ b/native/v8_extensions/native.mm @@ -166,8 +166,7 @@ namespace v8_extensions { } else if (name == "traverseTree") { std::string argument = arguments[0]->GetStringValue().ToString(); - int rootPathLength = argument.size(); - char rootPath[rootPathLength + 1]; + char rootPath[argument.size() + 1]; strcpy(rootPath, argument.c_str()); char * const paths[] = {rootPath, NULL}; @@ -191,12 +190,8 @@ namespace v8_extensions { continue; } - int pathLength = entry->fts_pathlen - rootPathLength; - char relative[pathLength + 1]; - relative[pathLength] = '\0'; - strncpy(relative, entry->fts_path + rootPathLength, pathLength); args.clear(); - args.push_back(CefV8Value::CreateString(relative)); + args.push_back(CefV8Value::CreateString(entry->fts_path)); if (isFile) { onFile->ExecuteFunction(onFile, args); } diff --git a/spec/stdlib/fs-spec.coffee b/spec/stdlib/fs-spec.coffee index b0a6fae5f..94445d011 100644 --- a/spec/stdlib/fs-spec.coffee +++ b/spec/stdlib/fs-spec.coffee @@ -86,7 +86,7 @@ describe "fs", -> it "calls fn for every path in the tree at the given path", -> paths = [] onPath = (path) -> - paths.push(fs.join(fixturesDir, path)) + paths.push(path) true fs.traverseTree fixturesDir, onPath, onPath expect(paths).toEqual fs.listTree(fixturesDir) @@ -106,14 +106,16 @@ describe "fs", -> expect(path).not.toMatch /\/dir\// it "returns entries if path is a symlink", -> + symlinkPath = fs.join(fixturesDir, 'symlink-to-dir') symlinkPaths = [] - onSymlinkPath = (path) -> symlinkPaths.push(path) + onSymlinkPath = (path) -> symlinkPaths.push(path.substring(symlinkPath.length + 1)) + regularPath = fs.join(fixturesDir, 'dir') paths = [] - onPath = (path) -> paths.push(path) + onPath = (path) -> paths.push(path.substring(regularPath.length + 1)) - fs.traverseTree(fs.join(fixturesDir, 'symlink-to-dir'), onSymlinkPath, onSymlinkPath) - fs.traverseTree(fs.join(fixturesDir, 'dir'), onPath, onPath) + fs.traverseTree(symlinkPath, onSymlinkPath, onSymlinkPath) + fs.traverseTree(regularPath, onPath, onPath) expect(symlinkPaths).toEqual(paths) diff --git a/src/app/config.coffee b/src/app/config.coffee index 2940b33f4..7c22c362e 100644 --- a/src/app/config.coffee +++ b/src/app/config.coffee @@ -37,16 +37,16 @@ class Config templateConfigDirPath = fs.resolve(window.resourcePath, 'dot-atom') onConfigDirFile = (path) => - templatePath = fs.join(templateConfigDirPath, path) - configPath = fs.join(@configDirPath, path) - fs.write(configPath, fs.read(templatePath)) + relativePath = path.substring(templateConfigDirPath.length + 1) + configPath = fs.join(@configDirPath, relativePath) + fs.write(configPath, fs.read(path)) fs.traverseTree(templateConfigDirPath, onConfigDirFile, (path) -> true) configThemeDirPath = fs.join(@configDirPath, 'themes') onThemeDirFile = (path) -> - templatePath = fs.join(bundledThemesDirPath, path) - configPath = fs.join(configThemeDirPath, path) - fs.write(configPath, fs.read(templatePath)) + relativePath = path.substring(bundledThemesDirPath.length + 1) + configPath = fs.join(configThemeDirPath, relativePath) + fs.write(configPath, fs.read(path)) fs.traverseTree(bundledThemesDirPath, onThemeDirFile, (path) -> true) load: -> diff --git a/src/packages/fuzzy-finder/lib/load-paths-handler.coffee b/src/packages/fuzzy-finder/lib/load-paths-handler.coffee index 9ef442c99..ac7c338dc 100644 --- a/src/packages/fuzzy-finder/lib/load-paths-handler.coffee +++ b/src/packages/fuzzy-finder/lib/load-paths-handler.coffee @@ -13,8 +13,10 @@ module.exports = return true if _.contains(ignoredNames, segment) repo?.isPathIgnored(fs.join(rootPath, path)) onFile = (path) -> + path = path.substring(rootPath.length + 1) paths.push(path) unless isIgnored(path) onDirectory = (path) -> + path = path.substring(rootPath.length + 1) not isIgnored(path) fs.traverseTree(rootPath, onFile, onDirectory) diff --git a/src/stdlib/fs.coffee b/src/stdlib/fs.coffee index cac22e5fe..5d9ffe226 100644 --- a/src/stdlib/fs.coffee +++ b/src/stdlib/fs.coffee @@ -63,11 +63,11 @@ module.exports = paths = [] if extensions onPath = (path) => - paths.push(@join(rootPath, path)) if _.contains(extensions, @extension(path)) + paths.push(path) if _.contains(extensions, @extension(path)) false else onPath = (path) => - paths.push(@join(rootPath, path)) + paths.push(path) false @traverseTree(rootPath, onPath, onPath) paths @@ -75,7 +75,7 @@ module.exports = listTree: (rootPath) -> paths = [] onPath = (path) => - paths.push(@join(rootPath, path)) + paths.push(path) true @traverseTree(rootPath, onPath, onPath) paths From 6dd9d011aa446e910739e6eafed47080b662eaa3 Mon Sep 17 00:00:00 2001 From: Ben Burkert Date: Mon, 11 Mar 2013 10:58:52 -0300 Subject: [PATCH 254/308] The CSON library requires the underscore-extensions library. --- src/stdlib/cson.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stdlib/cson.coffee b/src/stdlib/cson.coffee index 382d8600a..84832f35f 100644 --- a/src/stdlib/cson.coffee +++ b/src/stdlib/cson.coffee @@ -1,3 +1,4 @@ +require 'underscore-extensions' _ = require 'underscore' module.exports = From 0b674978dbe6e52d42b1c8588dbd4e0d66b51b58 Mon Sep 17 00:00:00 2001 From: probablycorey Date: Thu, 28 Feb 2013 14:27:56 -0800 Subject: [PATCH 255/308] Require will parse .less files into css --- spec/fixtures/sample-with-error.less | 1 + spec/fixtures/sample.less | 8 + spec/stdlib/require-spec.coffee | 19 + src/stdlib/require.coffee | 7 + vendor/less.js | 5078 ++++++++++++++++++++++++++ 5 files changed, 5113 insertions(+) create mode 100644 spec/fixtures/sample-with-error.less create mode 100644 spec/fixtures/sample.less create mode 100644 spec/stdlib/require-spec.coffee create mode 100644 vendor/less.js diff --git a/spec/fixtures/sample-with-error.less b/spec/fixtures/sample-with-error.less new file mode 100644 index 000000000..4396e25cf --- /dev/null +++ b/spec/fixtures/sample-with-error.less @@ -0,0 +1 @@ +#header { \ No newline at end of file diff --git a/spec/fixtures/sample.less b/spec/fixtures/sample.less new file mode 100644 index 000000000..a076a9d01 --- /dev/null +++ b/spec/fixtures/sample.less @@ -0,0 +1,8 @@ +@color: #4D926F; + +#header { + color: @color; +} +h2 { + color: @color; +} \ No newline at end of file diff --git a/spec/stdlib/require-spec.coffee b/spec/stdlib/require-spec.coffee new file mode 100644 index 000000000..a4b5004a4 --- /dev/null +++ b/spec/stdlib/require-spec.coffee @@ -0,0 +1,19 @@ +{less} = require('less') + +describe "require", -> + describe "files with a `.less` extension", -> + it "parses valid files into css", -> + output = require(project.resolve("sample.less")) + expect(output).toBe """ + #header { + color: #4d926f; + } + h2 { + color: #4d926f; + } + + """ + + it "throws an error when parsing invalid file", -> + functionWithError = (-> require(project.resolve("sample-with-error.less"))) + expect(functionWithError).toThrow() \ No newline at end of file diff --git a/src/stdlib/require.coffee b/src/stdlib/require.coffee index d7e8b87dd..42f0f05b3 100644 --- a/src/stdlib/require.coffee +++ b/src/stdlib/require.coffee @@ -66,6 +66,13 @@ exts = evaluated = exts.js(file, compiled) $native.write(cacheFilePath, compiled) if writeToCache evaluated + less: (file) -> + output = "" + (new less.Parser).parse __read(file), (e, tree) -> + throw new Error(e.message, file, e.line) if e + output = tree.toCSS() + output + getPath = (path) -> path = resolve(path) diff --git a/vendor/less.js b/vendor/less.js new file mode 100644 index 000000000..59629068d --- /dev/null +++ b/vendor/less.js @@ -0,0 +1,5078 @@ +// Modified +// +// Added +// module.exports.less = window.less = less = {} +// less.tree = tree = {} +// less.mode = 'browser' +// +// LESS - Leaner CSS v1.4.0 +// http://lesscss.org +// +// Copyright (c) 2009-2013, Alexis Sellier +// Licensed under the Apache 2.0 License. +// +(function (window, undefined) { +// +// Stub out `require` in the browser +// +function require(arg) { + return window.less[arg.split('/')[1]]; +}; + +// ecma-5.js +// +// -- kriskowal Kris Kowal Copyright (C) 2009-2010 MIT License +// -- tlrobinson Tom Robinson +// dantman Daniel Friesen + +// +// Array +// +if (!Array.isArray) { + Array.isArray = function(obj) { + return Object.prototype.toString.call(obj) === "[object Array]" || + (obj instanceof Array); + }; +} +if (!Array.prototype.forEach) { + Array.prototype.forEach = function(block, thisObject) { + var len = this.length >>> 0; + for (var i = 0; i < len; i++) { + if (i in this) { + block.call(thisObject, this[i], i, this); + } + } + }; +} +if (!Array.prototype.map) { + Array.prototype.map = function(fun /*, thisp*/) { + var len = this.length >>> 0; + var res = new Array(len); + var thisp = arguments[1]; + + for (var i = 0; i < len; i++) { + if (i in this) { + res[i] = fun.call(thisp, this[i], i, this); + } + } + return res; + }; +} +if (!Array.prototype.filter) { + Array.prototype.filter = function (block /*, thisp */) { + var values = []; + var thisp = arguments[1]; + for (var i = 0; i < this.length; i++) { + if (block.call(thisp, this[i])) { + values.push(this[i]); + } + } + return values; + }; +} +if (!Array.prototype.reduce) { + Array.prototype.reduce = function(fun /*, initial*/) { + var len = this.length >>> 0; + var i = 0; + + // no value to return if no initial value and an empty array + if (len === 0 && arguments.length === 1) throw new TypeError(); + + if (arguments.length >= 2) { + var rv = arguments[1]; + } else { + do { + if (i in this) { + rv = this[i++]; + break; + } + // if array contains no values, no initial value to return + if (++i >= len) throw new TypeError(); + } while (true); + } + for (; i < len; i++) { + if (i in this) { + rv = fun.call(null, rv, this[i], i, this); + } + } + return rv; + }; +} +if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function (value /*, fromIndex */ ) { + var length = this.length; + var i = arguments[1] || 0; + + if (!length) return -1; + if (i >= length) return -1; + if (i < 0) i += length; + + for (; i < length; i++) { + if (!Object.prototype.hasOwnProperty.call(this, i)) { continue } + if (value === this[i]) return i; + } + return -1; + }; +} + +// +// Object +// +if (!Object.keys) { + Object.keys = function (object) { + var keys = []; + for (var name in object) { + if (Object.prototype.hasOwnProperty.call(object, name)) { + keys.push(name); + } + } + return keys; + }; +} + +// +// String +// +if (!String.prototype.trim) { + String.prototype.trim = function () { + return String(this).replace(/^\s\s*/, '').replace(/\s\s*$/, ''); + }; +} +var less, tree, charset; + +module.exports.less = window.less = less = {} +less.tree = tree = {} +less.mode = 'browser' +// +// less.js - parser +// +// A relatively straight-forward predictive parser. +// There is no tokenization/lexing stage, the input is parsed +// in one sweep. +// +// To make the parser fast enough to run in the browser, several +// optimization had to be made: +// +// - Matching and slicing on a huge input is often cause of slowdowns. +// The solution is to chunkify the input into smaller strings. +// The chunks are stored in the `chunks` var, +// `j` holds the current chunk index, and `current` holds +// the index of the current chunk in relation to `input`. +// This gives us an almost 4x speed-up. +// +// - In many cases, we don't need to match individual tokens; +// for example, if a value doesn't hold any variables, operations +// or dynamic references, the parser can effectively 'skip' it, +// treating it as a literal. +// An example would be '1px solid #000' - which evaluates to itself, +// we don't need to know what the individual components are. +// The drawback, of course is that you don't get the benefits of +// syntax-checking on the CSS. This gives us a 50% speed-up in the parser, +// and a smaller speed-up in the code-gen. +// +// +// Token matching is done with the `$` function, which either takes +// a terminal string or regexp, or a non-terminal function to call. +// It also takes care of moving all the indices forwards. +// +// +less.Parser = function Parser(env) { + var input, // LeSS input string + i, // current index in `input` + j, // current chunk + temp, // temporarily holds a chunk's state, for backtracking + memo, // temporarily holds `i`, when backtracking + furthest, // furthest index the parser has gone to + chunks, // chunkified input + current, // index of current chunk, in `input` + parser; + + var that = this; + + // Top parser on an import tree must be sure there is one "env" + // which will then be passed around by reference. + if (!(env instanceof tree.parseEnv)) { + env = new tree.parseEnv(env); + } + + if (!env.currentDirectory && env.filename) { + // only works for node, only used for node + env.currentDirectory = env.filename.replace(/[^\/\\]*$/, ""); + } + + // This function is called after all files + // have been imported through `@import`. + var finish = function () {}; + + var imports = this.imports = { + paths: env.paths || [], // Search paths, when importing + queue: [], // Files which haven't been imported yet + files: env.files, // Holds the imported parse trees + contents: env.contents, // Holds the imported file contents + mime: env.mime, // MIME type of .less files + error: null, // Error in parsing/evaluating an import + push: function (path, callback) { + var that = this; + this.queue.push(path); + + // + // Import a file asynchronously + // + less.Parser.importer(path, this.paths, function (e, root, fullPath) { + that.queue.splice(that.queue.indexOf(path), 1); // Remove the path from the queue + + var imported = fullPath in that.files; + + that.files[fullPath] = root; // Store the root + + if (e && !that.error) { that.error = e; } + + callback(e, root, imported); + + if (that.queue.length === 0) { finish(that.error); } // Call `finish` if we're done importing + }, env); + } + }; + + function save() { temp = chunks[j], memo = i, current = i; } + function restore() { chunks[j] = temp, i = memo, current = i; } + + function sync() { + if (i > current) { + chunks[j] = chunks[j].slice(i - current); + current = i; + } + } + function isWhitespace(c) { + // Could change to \s? + var code = c.charCodeAt(0); + return code === 32 || code === 10 || code === 9; + } + // + // Parse from a token, regexp or string, and move forward if match + // + function $(tok) { + var match, args, length, index, k; + + // + // Non-terminal + // + if (tok instanceof Function) { + return tok.call(parser.parsers); + // + // Terminal + // + // Either match a single character in the input, + // or match a regexp in the current chunk (chunk[j]). + // + } else if (typeof(tok) === 'string') { + match = input.charAt(i) === tok ? tok : null; + length = 1; + sync (); + } else { + sync (); + + if (match = tok.exec(chunks[j])) { + length = match[0].length; + } else { + return null; + } + } + + // The match is confirmed, add the match length to `i`, + // and consume any extra white-space characters (' ' || '\n') + // which come after that. The reason for this is that LeSS's + // grammar is mostly white-space insensitive. + // + if (match) { + skipWhitespace(length); + + if(typeof(match) === 'string') { + return match; + } else { + return match.length === 1 ? match[0] : match; + } + } + } + + function skipWhitespace(length) { + var oldi = i, oldj = j, + endIndex = i + chunks[j].length, + mem = i += length; + + while (i < endIndex) { + if (! isWhitespace(input.charAt(i))) { break } + i++; + } + chunks[j] = chunks[j].slice(length + (i - mem)); + current = i; + + if (chunks[j].length === 0 && j < chunks.length - 1) { j++ } + + return oldi !== i || oldj !== j; + } + + function expect(arg, msg) { + var result = $(arg); + if (! result) { + error(msg || (typeof(arg) === 'string' ? "expected '" + arg + "' got '" + input.charAt(i) + "'" + : "unexpected token")); + } else { + return result; + } + } + + function error(msg, type) { + var e = new Error(msg); + e.index = i; + e.type = type || 'Syntax'; + throw e; + } + + // Same as $(), but don't change the state of the parser, + // just return the match. + function peek(tok) { + if (typeof(tok) === 'string') { + return input.charAt(i) === tok; + } else { + if (tok.test(chunks[j])) { + return true; + } else { + return false; + } + } + } + + function getInput(e, env) { + if (e.filename && env.filename && (e.filename !== env.filename)) { + return parser.imports.contents[e.filename]; + } else { + return input; + } + } + + function getLocation(index, input) { + for (var n = index, column = -1; + n >= 0 && input.charAt(n) !== '\n'; + n--) { column++ } + + return { line: typeof(index) === 'number' ? (input.slice(0, index).match(/\n/g) || "").length : null, + column: column }; + } + + function getFileName(e) { + if(less.mode === 'browser' || less.mode === 'rhino') + return e.filename; + else + return require('path').resolve(e.filename); + } + + function getDebugInfo(index, inputStream, e) { + return { + lineNumber: getLocation(index, inputStream).line + 1, + fileName: getFileName(e) + }; + } + + function LessError(e, env) { + var input = getInput(e, env), + loc = getLocation(e.index, input), + line = loc.line, + col = loc.column, + lines = input.split('\n'); + + this.type = e.type || 'Syntax'; + this.message = e.message; + this.filename = e.filename || env.filename; + this.index = e.index; + this.line = typeof(line) === 'number' ? line + 1 : null; + this.callLine = e.call && (getLocation(e.call, input).line + 1); + this.callExtract = lines[getLocation(e.call, input).line]; + this.stack = e.stack; + this.column = col; + this.extract = [ + lines[line - 1], + lines[line], + lines[line + 1] + ]; + } + + this.env = env = env || {}; + + // The optimization level dictates the thoroughness of the parser, + // the lower the number, the less nodes it will create in the tree. + // This could matter for debugging, or if you want to access + // the individual nodes in the tree. + this.optimization = ('optimization' in this.env) ? this.env.optimization : 1; + + this.env.filename = this.env.filename || null; + + // + // The Parser + // + return parser = { + + imports: imports, + // + // Parse an input string into an abstract syntax tree, + // call `callback` when done. + // + parse: function (str, callback) { + var root, start, end, zone, line, lines, buff = [], c, error = null; + + i = j = current = furthest = 0; + input = str.replace(/\r\n/g, '\n'); + + // Remove potential UTF Byte Order Mark + input = input.replace(/^\uFEFF/, ''); + + // Split the input into chunks. + chunks = (function (chunks) { + var j = 0, + skip = /(?:@\{[\w-]+\}|[^"'`\{\}\/\(\)\\])+/g, + comment = /\/\*(?:[^*]|\*+[^\/*])*\*+\/|\/\/.*/g, + string = /"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'|`((?:[^`]|\\.)*)`/g, + level = 0, + match, + chunk = chunks[0], + inParam; + + for (var i = 0, c, cc; i < input.length;) { + skip.lastIndex = i; + if (match = skip.exec(input)) { + if (match.index === i) { + i += match[0].length; + chunk.push(match[0]); + } + } + c = input.charAt(i); + comment.lastIndex = string.lastIndex = i; + + if (match = string.exec(input)) { + if (match.index === i) { + i += match[0].length; + chunk.push(match[0]); + continue; + } + } + + if (!inParam && c === '/') { + cc = input.charAt(i + 1); + if (cc === '/' || cc === '*') { + if (match = comment.exec(input)) { + if (match.index === i) { + i += match[0].length; + chunk.push(match[0]); + continue; + } + } + } + } + + switch (c) { + case '{': if (! inParam) { level ++; chunk.push(c); break } + case '}': if (! inParam) { level --; chunk.push(c); chunks[++j] = chunk = []; break } + case '(': if (! inParam) { inParam = true; chunk.push(c); break } + case ')': if ( inParam) { inParam = false; chunk.push(c); break } + default: chunk.push(c); + } + + i++; + } + if (level != 0) { + error = new(LessError)({ + index: i-1, + type: 'Parse', + message: (level > 0) ? "missing closing `}`" : "missing opening `{`", + filename: env.filename + }, env); + } + + return chunks.map(function (c) { return c.join('') });; + })([[]]); + + if (error) { + return callback(new(LessError)(error, env)); + } + + // Start with the primary rule. + // The whole syntax tree is held under a Ruleset node, + // with the `root` property set to true, so no `{}` are + // output. The callback is called when the input is parsed. + try { + root = new(tree.Ruleset)([], $(this.parsers.primary)); + root.root = true; + } catch (e) { + return callback(new(LessError)(e, env)); + } + + root.toCSS = (function (evaluate) { + var line, lines, column; + + return function (options, variables) { + options = options || {}; + var importError, + evalEnv = new tree.evalEnv(options); + + // + // Allows setting variables with a hash, so: + // + // `{ color: new(tree.Color)('#f01') }` will become: + // + // new(tree.Rule)('@color', + // new(tree.Value)([ + // new(tree.Expression)([ + // new(tree.Color)('#f01') + // ]) + // ]) + // ) + // + if (typeof(variables) === 'object' && !Array.isArray(variables)) { + variables = Object.keys(variables).map(function (k) { + var value = variables[k]; + + if (! (value instanceof tree.Value)) { + if (! (value instanceof tree.Expression)) { + value = new(tree.Expression)([value]); + } + value = new(tree.Value)([value]); + } + return new(tree.Rule)('@' + k, value, false, 0); + }); + evalEnv.frames = [new(tree.Ruleset)(null, variables)]; + } + + try { + var css = evaluate.call(this, evalEnv) + .toCSS([], { + compress: options.compress || false, + dumpLineNumbers: env.dumpLineNumbers, + strictUnits: options.strictUnits === false ? false : true}); + } catch (e) { + throw new(LessError)(e, env); + } + + if (options.yuicompress && less.mode === 'node') { + return require('ycssmin').cssmin(css); + } else if (options.compress) { + return css.replace(/(\s)+/g, "$1"); + } else { + return css; + } + }; + })(root.eval); + + // If `i` is smaller than the `input.length - 1`, + // it means the parser wasn't able to parse the whole + // string, so we've got a parsing error. + // + // We try to extract a \n delimited string, + // showing the line where the parse error occured. + // We split it up into two parts (the part which parsed, + // and the part which didn't), so we can color them differently. + if (i < input.length - 1) { + i = furthest; + lines = input.split('\n'); + line = (input.slice(0, i).match(/\n/g) || "").length + 1; + + for (var n = i, column = -1; n >= 0 && input.charAt(n) !== '\n'; n--) { column++ } + + error = { + type: "Parse", + message: "Unrecognised input", + index: i, + filename: env.filename, + line: line, + column: column, + extract: [ + lines[line - 2], + lines[line - 1], + lines[line] + ] + }; + } + + finish = function (e) { + e = error || e || parser.imports.error; + + if (e) { + if (!(e instanceof LessError)) { + e = new(LessError)(e, env); + } + + callback(e); + } + else { + callback(null, root); + } + }; + + if (this.imports.queue.length === 0) { + finish(); + } + }, + + // + // Here in, the parsing rules/functions + // + // The basic structure of the syntax tree generated is as follows: + // + // Ruleset -> Rule -> Value -> Expression -> Entity + // + // Here's some LESS code: + // + // .class { + // color: #fff; + // border: 1px solid #000; + // width: @w + 4px; + // > .child {...} + // } + // + // And here's what the parse tree might look like: + // + // Ruleset (Selector '.class', [ + // Rule ("color", Value ([Expression [Color #fff]])) + // Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]])) + // Rule ("width", Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]])) + // Ruleset (Selector [Element '>', '.child'], [...]) + // ]) + // + // In general, most rules will try to parse a token with the `$()` function, and if the return + // value is truly, will return a new node, of the relevant type. Sometimes, we need to check + // first, before parsing, that's when we use `peek()`. + // + parsers: { + // + // The `primary` rule is the *entry* and *exit* point of the parser. + // The rules here can appear at any level of the parse tree. + // + // The recursive nature of the grammar is an interplay between the `block` + // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule, + // as represented by this simplified grammar: + // + // primary → (ruleset | rule)+ + // ruleset → selector+ block + // block → '{' primary '}' + // + // Only at one point is the primary rule not called from the + // block rule: at the root level. + // + primary: function () { + var node, root = []; + + while ((node = $(this.extendRule) || $(this.mixin.definition) || $(this.rule) || $(this.ruleset) || + $(this.mixin.call) || $(this.comment) || $(this.directive)) + || $(/^[\s\n]+/) || $(/^;+/)) { + node && root.push(node); + } + return root; + }, + + // We create a Comment node for CSS comments `/* */`, + // but keep the LeSS comments `//` silent, by just skipping + // over them. + comment: function () { + var comment; + + if (input.charAt(i) !== '/') return; + + if (input.charAt(i + 1) === '/') { + return new(tree.Comment)($(/^\/\/.*/), true); + } else if (comment = $(/^\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/)) { + return new(tree.Comment)(comment); + } + }, + + // + // Entities are tokens which can be found inside an Expression + // + entities: { + // + // A string, which supports escaping " and ' + // + // "milky way" 'he\'s the one!' + // + quoted: function () { + var str, j = i, e; + + if (input.charAt(j) === '~') { j++, e = true } // Escaped strings + if (input.charAt(j) !== '"' && input.charAt(j) !== "'") return; + + e && $('~'); + + if (str = $(/^"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'/)) { + return new(tree.Quoted)(str[0], str[1] || str[2], e); + } + }, + + // + // A catch-all word, such as: + // + // black border-collapse + // + keyword: function () { + var k; + + if (k = $(/^[_A-Za-z-][_A-Za-z0-9-]*/)) { + if (tree.colors.hasOwnProperty(k)) { + // detect named color + return new(tree.Color)(tree.colors[k].slice(1)); + } else { + return new(tree.Keyword)(k); + } + } + }, + + // + // A function call + // + // rgb(255, 0, 255) + // + // We also try to catch IE's `alpha()`, but let the `alpha` parser + // deal with the details. + // + // The arguments are parsed with the `entities.arguments` parser. + // + call: function () { + var name, nameLC, args, alpha_ret, index = i; + + if (! (name = /^([\w-]+|%|progid:[\w\.]+)\(/.exec(chunks[j]))) return; + + name = name[1]; + nameLC = name.toLowerCase(); + + if (nameLC === 'url') { return null } + else { i += name.length } + + if (nameLC === 'alpha') { + alpha_ret = $(this.alpha); + if(typeof alpha_ret !== 'undefined') { + return alpha_ret; + } + } + + $('('); // Parse the '(' and consume whitespace. + + args = $(this.entities.arguments); + + if (! $(')')) { + return; + } + + if (name) { return new(tree.Call)(name, args, index, env.filename, env.rootpath, env.currentDirectory); } + }, + arguments: function () { + var args = [], arg; + + while (arg = $(this.entities.assignment) || $(this.expression)) { + args.push(arg); + if (! $(',')) { break } + } + return args; + }, + literal: function () { + return $(this.entities.dimension) || + $(this.entities.color) || + $(this.entities.quoted) || + $(this.entities.unicodeDescriptor); + }, + + // Assignments are argument entities for calls. + // They are present in ie filter properties as shown below. + // + // filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* ) + // + + assignment: function () { + var key, value; + if ((key = $(/^\w+(?=\s?=)/i)) && $('=') && (value = $(this.entity))) { + return new(tree.Assignment)(key, value); + } + }, + + // + // Parse url() tokens + // + // We use a specific rule for urls, because they don't really behave like + // standard function calls. The difference is that the argument doesn't have + // to be enclosed within a string, so it can't be parsed as an Expression. + // + url: function () { + var value; + + if (input.charAt(i) !== 'u' || !$(/^url\(/)) return; + value = $(this.entities.quoted) || $(this.entities.variable) || + $(/^(?:(?:\\[\(\)'"])|[^\(\)'"])+/) || ""; + + expect(')'); + + return new(tree.URL)((value.value != null || value instanceof tree.Variable) + ? value : new(tree.Anonymous)(value), env.rootpath); + }, + + // + // A Variable entity, such as `@fink`, in + // + // width: @fink + 2px + // + // We use a different parser for variable definitions, + // see `parsers.variable`. + // + variable: function () { + var name, index = i; + + if (input.charAt(i) === '@' && (name = $(/^@@?[\w-]+/))) { + return new(tree.Variable)(name, index, env.filename); + } + }, + + // A variable entity useing the protective {} e.g. @{var} + variableCurly: function () { + var name, curly, index = i; + + if (input.charAt(i) === '@' && (curly = $(/^@\{([\w-]+)\}/))) { + return new(tree.Variable)("@" + curly[1], index, env.filename); + } + }, + + // + // A Hexadecimal color + // + // #4F3C2F + // + // `rgb` and `hsl` colors are parsed through the `entities.call` parser. + // + color: function () { + var rgb; + + if (input.charAt(i) === '#' && (rgb = $(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/))) { + return new(tree.Color)(rgb[1]); + } + }, + + // + // A Dimension, that is, a number and a unit + // + // 0.5em 95% + // + dimension: function () { + var value, c = input.charCodeAt(i); + //Is the first char of the dimension 0-9, '.', '+' or '-' + if ((c > 57 || c < 43) || c === 47 || c == 44) return; + + if (value = $(/^([+-]?\d*\.?\d+)(%|[a-z]+)?/)) { + return new(tree.Dimension)(value[1], value[2]); + } + }, + + // + // A unicode descriptor, as is used in unicode-range + // + // U+0?? or U+00A1-00A9 + // + unicodeDescriptor: function () { + var ud; + + if (ud = $(/^U\+[0-9a-fA-F?]+(\-[0-9a-fA-F?]+)?/)) { + return new(tree.UnicodeDescriptor)(ud[0]); + } + }, + + // + // JavaScript code to be evaluated + // + // `window.location.href` + // + javascript: function () { + var str, j = i, e; + + if (input.charAt(j) === '~') { j++, e = true } // Escaped strings + if (input.charAt(j) !== '`') { return } + + e && $('~'); + + if (str = $(/^`([^`]*)`/)) { + return new(tree.JavaScript)(str[1], i, e); + } + } + }, + + // + // The variable part of a variable definition. Used in the `rule` parser + // + // @fink: + // + variable: function () { + var name; + + if (input.charAt(i) === '@' && (name = $(/^(@[\w-]+)\s*:/))) { return name[1] } + }, + + // + // extend syntax - used to extend selectors + // + extend: function(isRule) { + var elements = [], e, args, index = i; + + if (!$(isRule ? /^&:extend\(/ : /^:extend\(/)) { return; } + + while (e = $(/^[#.](?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+/)) { + elements.push(new(tree.Element)(null, e, i)); + } + + expect(/^\)/); + + if (isRule) { + expect(/^;/); + } + + return new(tree.Extend)(elements, index); + }, + + // + // extendRule - used in a rule to extend all the parent selectors + // + extendRule: function() { + return this.extend(true); + }, + + // + // Mixins + // + mixin: { + // + // A Mixin call, with an optional argument list + // + // #mixins > .square(#fff); + // .rounded(4px, black); + // .button; + // + // The `while` loop is there because mixins can be + // namespaced, but we only support the child and descendant + // selector for now. + // + call: function () { + var elements = [], e, c, argsSemiColon = [], argsComma = [], args, delim, arg, nameLoop, expressions, isSemiColonSeperated, expressionContainsNamed, index = i, s = input.charAt(i), name, value, important = false; + + if (s !== '.' && s !== '#') { return } + + save(); // stop us absorbing part of an invalid selector + + while (e = $(/^[#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/)) { + elements.push(new(tree.Element)(c, e, i)); + c = $('>'); + } + if ($('(')) { + expressions = []; + while (arg = $(this.expression)) { + nameLoop = null; + arg.throwAwayComments(); + value = arg; + + // Variable + if (arg.value.length == 1) { + var val = arg.value[0]; + if (val instanceof tree.Variable) { + if ($(':')) { + if (expressions.length > 0) { + if (isSemiColonSeperated) { + error("Cannot mix ; and , as delimiter types"); + } + expressionContainsNamed = true; + } + value = expect(this.expression); + nameLoop = (name = val.name); + } + } + } + + expressions.push(value); + + argsComma.push({ name: nameLoop, value: value }); + + if ($(',')) { + continue; + } + + if ($(';') || isSemiColonSeperated) { + + if (expressionContainsNamed) { + error("Cannot mix ; and , as delimiter types"); + } + + isSemiColonSeperated = true; + + if (expressions.length > 1) { + value = new(tree.Value)(expressions); + } + argsSemiColon.push({ name: name, value: value }); + + name = null; + expressions = []; + expressionContainsNamed = false; + } + } + + expect(')'); + } + + args = isSemiColonSeperated ? argsSemiColon : argsComma; + + if ($(this.important)) { + important = true; + } + + if (elements.length > 0 && ($(';') || peek('}'))) { + return new(tree.mixin.Call)(elements, args, index, env.filename, important); + } + + restore(); + }, + + // + // A Mixin definition, with a list of parameters + // + // .rounded (@radius: 2px, @color) { + // ... + // } + // + // Until we have a finer grained state-machine, we have to + // do a look-ahead, to make sure we don't have a mixin call. + // See the `rule` function for more information. + // + // We start by matching `.rounded (`, and then proceed on to + // the argument list, which has optional default values. + // We store the parameters in `params`, with a `value` key, + // if there is a value, such as in the case of `@radius`. + // + // Once we've got our params list, and a closing `)`, we parse + // the `{...}` block. + // + definition: function () { + var name, params = [], match, ruleset, param, value, cond, variadic = false; + if ((input.charAt(i) !== '.' && input.charAt(i) !== '#') || + peek(/^[^{]*\}/)) return; + + save(); + + if (match = $(/^([#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/)) { + name = match[1]; + + do { + $(this.comment); + if (input.charAt(i) === '.' && $(/^\.{3}/)) { + variadic = true; + params.push({ variadic: true }); + break; + } else if (param = $(this.entities.variable) || $(this.entities.literal) + || $(this.entities.keyword)) { + // Variable + if (param instanceof tree.Variable) { + if ($(':')) { + value = expect(this.expression, 'expected expression'); + params.push({ name: param.name, value: value }); + } else if ($(/^\.{3}/)) { + params.push({ name: param.name, variadic: true }); + variadic = true; + break; + } else { + params.push({ name: param.name }); + } + } else { + params.push({ value: param }); + } + } else { + break; + } + } while ($(',') || $(';')) + + // .mixincall("@{a}"); + // looks a bit like a mixin definition.. so we have to be nice and restore + if (!$(')')) { + furthest = i; + restore(); + } + + $(this.comment); + + if ($(/^when/)) { // Guard + cond = expect(this.conditions, 'expected condition'); + } + + ruleset = $(this.block); + + if (ruleset) { + return new(tree.mixin.Definition)(name, params, ruleset, cond, variadic); + } else { + restore(); + } + } + } + }, + + // + // Entities are the smallest recognized token, + // and can be found inside a rule's value. + // + entity: function () { + return $(this.entities.literal) || $(this.entities.variable) || $(this.entities.url) || + $(this.entities.call) || $(this.entities.keyword) ||$(this.entities.javascript) || + $(this.comment); + }, + + // + // A Rule terminator. Note that we use `peek()` to check for '}', + // because the `block` rule will be expecting it, but we still need to make sure + // it's there, if ';' was ommitted. + // + end: function () { + return $(';') || peek('}'); + }, + + // + // IE's alpha function + // + // alpha(opacity=88) + // + alpha: function () { + var value; + + if (! $(/^\(opacity=/i)) return; + if (value = $(/^\d+/) || $(this.entities.variable)) { + expect(')'); + return new(tree.Alpha)(value); + } + }, + + // + // A Selector Element + // + // div + // + h1 + // #socks + // input[type="text"] + // + // Elements are the building blocks for Selectors, + // they are made out of a `Combinator` (see combinator rule), + // and an element name, such as a tag a class, or `*`. + // + element: function () { + var e, t, c, v; + + c = $(this.combinator); + + e = $(/^(?:\d+\.\d+|\d+)%/) || $(/^(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/) || + $('*') || $('&') || $(this.attribute) || $(/^\([^()@]+\)/) || $(/^[\.#](?=@)/) || $(this.entities.variableCurly); + + if (! e) { + if ($('(')) { + if ((v = (//$(this.entities.variableCurly) || + $(this.selector))) && + $(')')) { + e = new(tree.Paren)(v); + } + } + } + + if (e) { return new(tree.Element)(c, e, i) } + }, + + // + // Combinators combine elements together, in a Selector. + // + // Because our parser isn't white-space sensitive, special care + // has to be taken, when parsing the descendant combinator, ` `, + // as it's an empty space. We have to check the previous character + // in the input, to see if it's a ` ` character. More info on how + // we deal with this in *combinator.js*. + // + combinator: function () { + var match, c = input.charAt(i); + + if (c === '>' || c === '+' || c === '~' || c === '|') { + i++; + while (input.charAt(i).match(/\s/)) { i++ } + return new(tree.Combinator)(c); + } else if (input.charAt(i - 1).match(/\s/)) { + return new(tree.Combinator)(" "); + } else { + return new(tree.Combinator)(null); + } + }, + + // + // A CSS Selector + // + // .class > div + h1 + // li a:hover + // + // Selectors are made out of one or more Elements, see above. + // + selector: function () { + var sel, e, elements = [], c, match, extend; + + while ((extend = $(this.extend)) || (e = $(this.element))) { + if (!e) { + break; + } + c = input.charAt(i); + elements.push(e) + e = null; + if (c === '{' || c === '}' || c === ';' || c === ',' || c === ')') { break } + } + + if (elements.length > 0) { return new(tree.Selector)(elements, extend) } + if (extend) { error("Extend must be used to extend a selector"); } + }, + attribute: function () { + var attr = '', key, val, op; + + if (! $('[')) return; + + if (key = $(/^(?:[_A-Za-z0-9-]|\\.)+/) || $(this.entities.quoted)) { + if ((op = $(/^[|~*$^]?=/)) && + (val = $(this.entities.quoted) || $(/^[\w-]+/))) { + attr = [key, op, val.toCSS ? val.toCSS() : val].join(''); + } else { attr = key } + } + + if (! $(']')) return; + + if (attr) { return "[" + attr + "]" } + }, + + // + // The `block` rule is used by `ruleset` and `mixin.definition`. + // It's a wrapper around the `primary` rule, with added `{}`. + // + block: function () { + var content; + if ($('{') && (content = $(this.primary)) && $('}')) { + return content; + } + }, + + // + // div, .class, body > p {...} + // + ruleset: function () { + var selectors = [], s, rules, match, debugInfo; + + save(); + + if (env.dumpLineNumbers) + debugInfo = getDebugInfo(i, input, env); + + while (s = $(this.selector)) { + selectors.push(s); + $(this.comment); + if (! $(',')) { break } + $(this.comment); + } + + if (selectors.length > 0 && (rules = $(this.block))) { + var ruleset = new(tree.Ruleset)(selectors, rules, env.strictImports); + if (env.dumpLineNumbers) + ruleset.debugInfo = debugInfo; + return ruleset; + } else { + // Backtrack + furthest = i; + restore(); + } + }, + rule: function () { + var name, value, c = input.charAt(i), important, match; + save(); + + if (c === '.' || c === '#' || c === '&') { return } + + if (name = $(this.variable) || $(this.property)) { + if (!env.compress && (name.charAt(0) != '@') && (match = /^([^@+\/'"*`(;{}-]*);/.exec(chunks[j]))) { + i += match[0].length - 1; + value = new(tree.Anonymous)(match[1]); + } else { + value = $(this.value); + } + important = $(this.important); + + if (value && $(this.end)) { + return new(tree.Rule)(name, value, important, memo); + } else { + furthest = i; + restore(); + } + } + }, + + // + // An @import directive + // + // @import "lib"; + // + // Depending on our environemnt, importing is done differently: + // In the browser, it's an XHR request, in Node, it would be a + // file-system operation. The function used for importing is + // stored in `import`, which we pass to the Import constructor. + // + "import": function () { + var path, features, index = i; + + save(); + + var dir = $(/^@import(?:-(once|multiple))?\s+/); + + if (dir && (path = $(this.entities.quoted) || $(this.entities.url))) { + features = $(this.mediaFeatures); + if ($(';')) { + features = features && new(tree.Value)(features); + var importOnce = dir[1] !== 'multiple'; + return new(tree.Import)(path, imports, features, importOnce, index, env.rootpath); + } + } + + restore(); + }, + + mediaFeature: function () { + var e, p, nodes = []; + + do { + if (e = $(this.entities.keyword)) { + nodes.push(e); + } else if ($('(')) { + p = $(this.property); + e = $(this.value); + if ($(')')) { + if (p && e) { + nodes.push(new(tree.Paren)(new(tree.Rule)(p, e, null, i, true))); + } else if (e) { + nodes.push(new(tree.Paren)(e)); + } else { + return null; + } + } else { return null } + } + } while (e); + + if (nodes.length > 0) { + return new(tree.Expression)(nodes); + } + }, + + mediaFeatures: function () { + var e, features = []; + + do { + if (e = $(this.mediaFeature)) { + features.push(e); + if (! $(',')) { break } + } else if (e = $(this.entities.variable)) { + features.push(e); + if (! $(',')) { break } + } + } while (e); + + return features.length > 0 ? features : null; + }, + + media: function () { + var features, rules, media, debugInfo; + + if (env.dumpLineNumbers) + debugInfo = getDebugInfo(i, input, env); + + if ($(/^@media/)) { + features = $(this.mediaFeatures); + + if (rules = $(this.block)) { + media = new(tree.Media)(rules, features); + if(env.dumpLineNumbers) + media.debugInfo = debugInfo; + return media; + } + } + }, + + // + // A CSS Directive + // + // @charset "utf-8"; + // + directive: function () { + var name, value, rules, identifier, e, nodes, nonVendorSpecificName, + hasBlock, hasIdentifier, hasExpression; + + if (input.charAt(i) !== '@') return; + + if (value = $(this['import']) || $(this.media)) { + return value; + } + + save(); + + name = $(/^@[a-z-]+/); + + if (!name) return; + + nonVendorSpecificName = name; + if (name.charAt(1) == '-' && name.indexOf('-', 2) > 0) { + nonVendorSpecificName = "@" + name.slice(name.indexOf('-', 2) + 1); + } + + switch(nonVendorSpecificName) { + case "@font-face": + hasBlock = true; + break; + case "@viewport": + case "@top-left": + case "@top-left-corner": + case "@top-center": + case "@top-right": + case "@top-right-corner": + case "@bottom-left": + case "@bottom-left-corner": + case "@bottom-center": + case "@bottom-right": + case "@bottom-right-corner": + case "@left-top": + case "@left-middle": + case "@left-bottom": + case "@right-top": + case "@right-middle": + case "@right-bottom": + hasBlock = true; + break; + case "@page": + case "@document": + case "@supports": + case "@keyframes": + hasBlock = true; + hasIdentifier = true; + break; + case "@namespace": + hasExpression = true; + break; + } + + if (hasIdentifier) { + name += " " + ($(/^[^{]+/) || '').trim(); + } + + if (hasBlock) + { + if (rules = $(this.block)) { + return new(tree.Directive)(name, rules); + } + } else { + if ((value = hasExpression ? $(this.expression) : $(this.entity)) && $(';')) { + var directive = new(tree.Directive)(name, value); + if (env.dumpLineNumbers) { + directive.debugInfo = getDebugInfo(i, input, env); + } + return directive; + } + } + + restore(); + }, + + // + // A Value is a comma-delimited list of Expressions + // + // font-family: Baskerville, Georgia, serif; + // + // In a Rule, a Value represents everything after the `:`, + // and before the `;`. + // + value: function () { + var e, expressions = [], important; + + while (e = $(this.expression)) { + expressions.push(e); + if (! $(',')) { break } + } + + if (expressions.length > 0) { + return new(tree.Value)(expressions); + } + }, + important: function () { + if (input.charAt(i) === '!') { + return $(/^! *important/); + } + }, + sub: function () { + var a, e; + + if ($('(')) { + if (a = $(this.addition)) { + e = new(tree.Expression)([a]); + expect(')'); + e.parens = true; + return e; + } + } + }, + multiplication: function () { + var m, a, op, operation, isSpaced, expression = []; + if (m = $(this.operand)) { + isSpaced = isWhitespace(input.charAt(i - 1)); + while (!peek(/^\/[*\/]/) && (op = ($('/') || $('*')))) { + if (a = $(this.operand)) { + m.parensInOp = true; + a.parensInOp = true; + operation = new(tree.Operation)(op, [operation || m, a], isSpaced); + isSpaced = isWhitespace(input.charAt(i - 1)); + } else { + break; + } + } + return operation || m; + } + }, + addition: function () { + var m, a, op, operation, isSpaced; + if (m = $(this.multiplication)) { + isSpaced = isWhitespace(input.charAt(i - 1)); + while ((op = $(/^[-+]\s+/) || (!isSpaced && ($('+') || $('-')))) && + (a = $(this.multiplication))) { + m.parensInOp = true; + a.parensInOp = true; + operation = new(tree.Operation)(op, [operation || m, a], isSpaced); + isSpaced = isWhitespace(input.charAt(i - 1)); + } + return operation || m; + } + }, + conditions: function () { + var a, b, index = i, condition; + + if (a = $(this.condition)) { + while ($(',') && (b = $(this.condition))) { + condition = new(tree.Condition)('or', condition || a, b, index); + } + return condition || a; + } + }, + condition: function () { + var a, b, c, op, index = i, negate = false; + + if ($(/^not/)) { negate = true } + expect('('); + if (a = $(this.addition) || $(this.entities.keyword) || $(this.entities.quoted)) { + if (op = $(/^(?:>=|=<|[<=>])/)) { + if (b = $(this.addition) || $(this.entities.keyword) || $(this.entities.quoted)) { + c = new(tree.Condition)(op, a, b, index, negate); + } else { + error('expected expression'); + } + } else { + c = new(tree.Condition)('=', a, new(tree.Keyword)('true'), index, negate); + } + expect(')'); + return $(/^and/) ? new(tree.Condition)('and', c, $(this.condition)) : c; + } + }, + + // + // An operand is anything that can be part of an operation, + // such as a Color, or a Variable + // + operand: function () { + var negate, p = input.charAt(i + 1); + + if (input.charAt(i) === '-' && (p === '@' || p === '(')) { negate = $('-') } + var o = $(this.sub) || $(this.entities.dimension) || + $(this.entities.color) || $(this.entities.variable) || + $(this.entities.call); + + if (negate) { + o.parensInOp = true; + o = new(tree.Negative)(o); + } + + return o; + }, + + // + // Expressions either represent mathematical operations, + // or white-space delimited Entities. + // + // 1px solid black + // @var * 2 + // + expression: function () { + var e, delim, entities = [], d; + + while (e = $(this.addition) || $(this.entity)) { + entities.push(e); + // operations do not allow keyword "/" dimension (e.g. small/20px) so we support that here + if (!peek(/^\/[\/*]/) && (delim = $('/'))) { + entities.push(new(tree.Anonymous)(delim)); + } + } + if (entities.length > 0) { + return new(tree.Expression)(entities); + } + }, + property: function () { + var name; + + if (name = $(/^(\*?-?[_a-z0-9-]+)\s*:/)) { + return name[1]; + } + } + } + }; +}; + +if (less.mode === 'browser' || less.mode === 'rhino') { + // + // Used by `@import` directives + // + less.Parser.importer = function (path, paths, callback, env) { + if (!/^([a-z-]+:)?\//.test(path) && paths.length > 0) { + path = paths[0] + path; + } + // We pass `true` as 3rd argument, to force the reload of the import. + // This is so we can get the syntax tree as opposed to just the CSS output, + // as we need this to evaluate the current stylesheet. + loadStyleSheet(env.toSheet(path), + function (e, root, data, sheet, _, path) { + callback.call(null, e, root, path); + }, true); + }; +} + +(function (tree) { + +tree.functions = { + rgb: function (r, g, b) { + return this.rgba(r, g, b, 1.0); + }, + rgba: function (r, g, b, a) { + var rgb = [r, g, b].map(function (c) { return scaled(c, 256); }); + a = number(a); + return new(tree.Color)(rgb, a); + }, + hsl: function (h, s, l) { + return this.hsla(h, s, l, 1.0); + }, + hsla: function (h, s, l, a) { + h = (number(h) % 360) / 360; + s = number(s); l = number(l); a = number(a); + + var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s; + var m1 = l * 2 - m2; + + return this.rgba(hue(h + 1/3) * 255, + hue(h) * 255, + hue(h - 1/3) * 255, + a); + + function hue(h) { + h = h < 0 ? h + 1 : (h > 1 ? h - 1 : h); + if (h * 6 < 1) return m1 + (m2 - m1) * h * 6; + else if (h * 2 < 1) return m2; + else if (h * 3 < 2) return m1 + (m2 - m1) * (2/3 - h) * 6; + else return m1; + } + }, + + hsv: function(h, s, v) { + return this.hsva(h, s, v, 1.0); + }, + + hsva: function(h, s, v, a) { + h = ((number(h) % 360) / 360) * 360; + s = number(s); v = number(v); a = number(a); + + var i, f; + i = Math.floor((h / 60) % 6); + f = (h / 60) - i; + + var vs = [v, + v * (1 - s), + v * (1 - f * s), + v * (1 - (1 - f) * s)]; + var perm = [[0, 3, 1], + [2, 0, 1], + [1, 0, 3], + [1, 2, 0], + [3, 1, 0], + [0, 1, 2]]; + + return this.rgba(vs[perm[i][0]] * 255, + vs[perm[i][1]] * 255, + vs[perm[i][2]] * 255, + a); + }, + + hue: function (color) { + return new(tree.Dimension)(Math.round(color.toHSL().h)); + }, + saturation: function (color) { + return new(tree.Dimension)(Math.round(color.toHSL().s * 100), '%'); + }, + lightness: function (color) { + return new(tree.Dimension)(Math.round(color.toHSL().l * 100), '%'); + }, + hsvhue: function(color) { + return new(tree.Dimension)(Math.round(color.toHSV().h)); + }, + hsvsaturation: function (color) { + return new(tree.Dimension)(Math.round(color.toHSV().s * 100), '%'); + }, + hsvvalue: function (color) { + return new(tree.Dimension)(Math.round(color.toHSV().v * 100), '%'); + }, + red: function (color) { + return new(tree.Dimension)(color.rgb[0]); + }, + green: function (color) { + return new(tree.Dimension)(color.rgb[1]); + }, + blue: function (color) { + return new(tree.Dimension)(color.rgb[2]); + }, + alpha: function (color) { + return new(tree.Dimension)(color.toHSL().a); + }, + luma: function (color) { + return new(tree.Dimension)(Math.round(color.luma() * color.alpha * 100), '%'); + }, + saturate: function (color, amount) { + var hsl = color.toHSL(); + + hsl.s += amount.value / 100; + hsl.s = clamp(hsl.s); + return hsla(hsl); + }, + desaturate: function (color, amount) { + var hsl = color.toHSL(); + + hsl.s -= amount.value / 100; + hsl.s = clamp(hsl.s); + return hsla(hsl); + }, + lighten: function (color, amount) { + var hsl = color.toHSL(); + + hsl.l += amount.value / 100; + hsl.l = clamp(hsl.l); + return hsla(hsl); + }, + darken: function (color, amount) { + var hsl = color.toHSL(); + + hsl.l -= amount.value / 100; + hsl.l = clamp(hsl.l); + return hsla(hsl); + }, + fadein: function (color, amount) { + var hsl = color.toHSL(); + + hsl.a += amount.value / 100; + hsl.a = clamp(hsl.a); + return hsla(hsl); + }, + fadeout: function (color, amount) { + var hsl = color.toHSL(); + + hsl.a -= amount.value / 100; + hsl.a = clamp(hsl.a); + return hsla(hsl); + }, + fade: function (color, amount) { + var hsl = color.toHSL(); + + hsl.a = amount.value / 100; + hsl.a = clamp(hsl.a); + return hsla(hsl); + }, + spin: function (color, amount) { + var hsl = color.toHSL(); + var hue = (hsl.h + amount.value) % 360; + + hsl.h = hue < 0 ? 360 + hue : hue; + + return hsla(hsl); + }, + // + // Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein + // http://sass-lang.com + // + mix: function (color1, color2, weight) { + if (!weight) { + weight = new(tree.Dimension)(50); + } + var p = weight.value / 100.0; + var w = p * 2 - 1; + var a = color1.toHSL().a - color2.toHSL().a; + + var w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0; + var w2 = 1 - w1; + + var rgb = [color1.rgb[0] * w1 + color2.rgb[0] * w2, + color1.rgb[1] * w1 + color2.rgb[1] * w2, + color1.rgb[2] * w1 + color2.rgb[2] * w2]; + + var alpha = color1.alpha * p + color2.alpha * (1 - p); + + return new(tree.Color)(rgb, alpha); + }, + greyscale: function (color) { + return this.desaturate(color, new(tree.Dimension)(100)); + }, + contrast: function (color, dark, light, threshold) { + // filter: contrast(3.2); + // should be kept as is, so check for color + if (!color.rgb) { + return null; + } + if (typeof light === 'undefined') { + light = this.rgba(255, 255, 255, 1.0); + } + if (typeof dark === 'undefined') { + dark = this.rgba(0, 0, 0, 1.0); + } + //Figure out which is actually light and dark! + if (dark.luma() > light.luma()) { + var t = light; + light = dark; + dark = t; + } + if (typeof threshold === 'undefined') { + threshold = 0.43; + } else { + threshold = number(threshold); + } + if ((color.luma() * color.alpha) < threshold) { + return light; + } else { + return dark; + } + }, + e: function (str) { + return new(tree.Anonymous)(str instanceof tree.JavaScript ? str.evaluated : str); + }, + escape: function (str) { + return new(tree.Anonymous)(encodeURI(str.value).replace(/=/g, "%3D").replace(/:/g, "%3A").replace(/#/g, "%23").replace(/;/g, "%3B").replace(/\(/g, "%28").replace(/\)/g, "%29")); + }, + '%': function (quoted /* arg, arg, ...*/) { + var args = Array.prototype.slice.call(arguments, 1), + str = quoted.value; + + for (var i = 0; i < args.length; i++) { + str = str.replace(/%[sda]/i, function(token) { + var value = token.match(/s/i) ? args[i].value : args[i].toCSS(); + return token.match(/[A-Z]$/) ? encodeURIComponent(value) : value; + }); + } + str = str.replace(/%%/g, '%'); + return new(tree.Quoted)('"' + str + '"', str); + }, + unit: function (val, unit) { + return new(tree.Dimension)(val.value, unit ? unit.toCSS() : ""); + }, + convert: function (val, unit) { + return val.convertTo(unit.value); + }, + round: function (n, f) { + var fraction = typeof(f) === "undefined" ? 0 : f.value; + return this._math(function(num) { return num.toFixed(fraction); }, null, n); + }, + pi: function () { + return new(tree.Dimension)(Math.PI); + }, + mod: function(a, b) { + return new(tree.Dimension)(a.value % b.value, a.unit); + }, + pow: function(x, y) { + if (typeof x === "number" && typeof y === "number") { + x = new(tree.Dimension)(x); + y = new(tree.Dimension)(y); + } else if (!(x instanceof tree.Dimension) || !(y instanceof tree.Dimension)) { + throw { type: "Argument", message: "arguments must be numbers" }; + } + + return new(tree.Dimension)(Math.pow(x.value, y.value), x.unit); + }, + _math: function (fn, unit, n) { + if (n instanceof tree.Dimension) { + return new(tree.Dimension)(fn(parseFloat(n.value)), unit == null ? n.unit : unit); + } else if (typeof(n) === 'number') { + return fn(n); + } else { + throw { type: "Argument", message: "argument must be a number" }; + } + }, + argb: function (color) { + return new(tree.Anonymous)(color.toARGB()); + + }, + percentage: function (n) { + return new(tree.Dimension)(n.value * 100, '%'); + }, + color: function (n) { + if (n instanceof tree.Quoted) { + return new(tree.Color)(n.value.slice(1)); + } else { + throw { type: "Argument", message: "argument must be a string" }; + } + }, + iscolor: function (n) { + return this._isa(n, tree.Color); + }, + isnumber: function (n) { + return this._isa(n, tree.Dimension); + }, + isstring: function (n) { + return this._isa(n, tree.Quoted); + }, + iskeyword: function (n) { + return this._isa(n, tree.Keyword); + }, + isurl: function (n) { + return this._isa(n, tree.URL); + }, + ispixel: function (n) { + return (n instanceof tree.Dimension) && n.unit.is('px') ? tree.True : tree.False; + }, + ispercentage: function (n) { + return (n instanceof tree.Dimension) && n.unit.is('%') ? tree.True : tree.False; + }, + isem: function (n) { + return (n instanceof tree.Dimension) && n.unit.is('em') ? tree.True : tree.False; + }, + _isa: function (n, Type) { + return (n instanceof Type) ? tree.True : tree.False; + }, + + /* Blending modes */ + + multiply: function(color1, color2) { + var r = color1.rgb[0] * color2.rgb[0] / 255; + var g = color1.rgb[1] * color2.rgb[1] / 255; + var b = color1.rgb[2] * color2.rgb[2] / 255; + return this.rgb(r, g, b); + }, + screen: function(color1, color2) { + var r = 255 - (255 - color1.rgb[0]) * (255 - color2.rgb[0]) / 255; + var g = 255 - (255 - color1.rgb[1]) * (255 - color2.rgb[1]) / 255; + var b = 255 - (255 - color1.rgb[2]) * (255 - color2.rgb[2]) / 255; + return this.rgb(r, g, b); + }, + overlay: function(color1, color2) { + var r = color1.rgb[0] < 128 ? 2 * color1.rgb[0] * color2.rgb[0] / 255 : 255 - 2 * (255 - color1.rgb[0]) * (255 - color2.rgb[0]) / 255; + var g = color1.rgb[1] < 128 ? 2 * color1.rgb[1] * color2.rgb[1] / 255 : 255 - 2 * (255 - color1.rgb[1]) * (255 - color2.rgb[1]) / 255; + var b = color1.rgb[2] < 128 ? 2 * color1.rgb[2] * color2.rgb[2] / 255 : 255 - 2 * (255 - color1.rgb[2]) * (255 - color2.rgb[2]) / 255; + return this.rgb(r, g, b); + }, + softlight: function(color1, color2) { + var t = color2.rgb[0] * color1.rgb[0] / 255; + var r = t + color1.rgb[0] * (255 - (255 - color1.rgb[0]) * (255 - color2.rgb[0]) / 255 - t) / 255; + t = color2.rgb[1] * color1.rgb[1] / 255; + var g = t + color1.rgb[1] * (255 - (255 - color1.rgb[1]) * (255 - color2.rgb[1]) / 255 - t) / 255; + t = color2.rgb[2] * color1.rgb[2] / 255; + var b = t + color1.rgb[2] * (255 - (255 - color1.rgb[2]) * (255 - color2.rgb[2]) / 255 - t) / 255; + return this.rgb(r, g, b); + }, + hardlight: function(color1, color2) { + var r = color2.rgb[0] < 128 ? 2 * color2.rgb[0] * color1.rgb[0] / 255 : 255 - 2 * (255 - color2.rgb[0]) * (255 - color1.rgb[0]) / 255; + var g = color2.rgb[1] < 128 ? 2 * color2.rgb[1] * color1.rgb[1] / 255 : 255 - 2 * (255 - color2.rgb[1]) * (255 - color1.rgb[1]) / 255; + var b = color2.rgb[2] < 128 ? 2 * color2.rgb[2] * color1.rgb[2] / 255 : 255 - 2 * (255 - color2.rgb[2]) * (255 - color1.rgb[2]) / 255; + return this.rgb(r, g, b); + }, + difference: function(color1, color2) { + var r = Math.abs(color1.rgb[0] - color2.rgb[0]); + var g = Math.abs(color1.rgb[1] - color2.rgb[1]); + var b = Math.abs(color1.rgb[2] - color2.rgb[2]); + return this.rgb(r, g, b); + }, + exclusion: function(color1, color2) { + var r = color1.rgb[0] + color2.rgb[0] * (255 - color1.rgb[0] - color1.rgb[0]) / 255; + var g = color1.rgb[1] + color2.rgb[1] * (255 - color1.rgb[1] - color1.rgb[1]) / 255; + var b = color1.rgb[2] + color2.rgb[2] * (255 - color1.rgb[2] - color1.rgb[2]) / 255; + return this.rgb(r, g, b); + }, + average: function(color1, color2) { + var r = (color1.rgb[0] + color2.rgb[0]) / 2; + var g = (color1.rgb[1] + color2.rgb[1]) / 2; + var b = (color1.rgb[2] + color2.rgb[2]) / 2; + return this.rgb(r, g, b); + }, + negation: function(color1, color2) { + var r = 255 - Math.abs(255 - color2.rgb[0] - color1.rgb[0]); + var g = 255 - Math.abs(255 - color2.rgb[1] - color1.rgb[1]); + var b = 255 - Math.abs(255 - color2.rgb[2] - color1.rgb[2]); + return this.rgb(r, g, b); + }, + tint: function(color, amount) { + return this.mix(this.rgb(255,255,255), color, amount); + }, + shade: function(color, amount) { + return this.mix(this.rgb(0, 0, 0), color, amount); + }, + extract: function(values, index) { + index = index.value - 1; // (1-based index) + return values.value[index]; + }, + + "data-uri": function(mimetypeNode, filePathNode) { + + if (typeof window !== 'undefined') { + return new tree.URL(filePathNode || mimetypeNode, this.rootpath).eval(this.env); + } + + var mimetype = mimetypeNode.value; + var filePath = (filePathNode && filePathNode.value); + + var fs = require("fs"), + path = require("path"), + useBase64 = false; + + if (arguments.length < 2) { + filePath = mimetype; + } + + if (this.currentDirectory && this.env.isPathRelative(filePath)) { + filePath = path.join(this.currentDirectory, filePath); + } + + // detect the mimetype if not given + if (arguments.length < 2) { + var mime; + try { + mime = require('mime'); + } catch (ex) { + mime = tree._mime; + } + + mimetype = mime.lookup(filePath); + + // use base 64 unless it's an ASCII or UTF-8 format + var charset = mime.charsets.lookup(mimetype); + useBase64 = ['US-ASCII', 'UTF-8'].indexOf(charset) < 0; + if (useBase64) mimetype += ';base64'; + } + else { + useBase64 = /;base64$/.test(mimetype) + } + + var buf = fs.readFileSync(filePath); + + // IE8 cannot handle a data-uri larger than 32KB. If this is exceeded + // and the --ieCompat flag is enabled, return a normal url() instead. + var DATA_URI_MAX_KB = 32, + fileSizeInKB = parseInt((buf.length / 1024), 10); + if (fileSizeInKB >= DATA_URI_MAX_KB) { + // the url() must be relative, not an absolute file path + filePath = path.relative(this.currentDirectory, filePath); + + if (this.env.ieCompat !== false) { + if (!this.env.silent) { + console.warn("Skipped data-uri embedding of %s because its size (%dKB) exceeds IE8-safe %dKB!", filePath, fileSizeInKB, DATA_URI_MAX_KB); + } + + return new tree.URL(filePathNode || mimetypeNode, this.rootpath).eval(this.env); + } else if (!this.env.silent) { + // if explicitly disabled (via --no-ie-compat on CLI, or env.ieCompat === false), merely warn + console.warn("WARNING: Embedding %s (%dKB) exceeds IE8's data-uri size limit of %dKB!", filePath, fileSizeInKB, DATA_URI_MAX_KB); + } + } + + buf = useBase64 ? buf.toString('base64') + : encodeURIComponent(buf); + + var uri = "'data:" + mimetype + ',' + buf + "'"; + return new(tree.URL)(new(tree.Anonymous)(uri)); + } +}; + +// these static methods are used as a fallback when the optional 'mime' dependency is missing +tree._mime = { + // this map is intentionally incomplete + // if you want more, install 'mime' dep + _types: { + '.htm' : 'text/html', + '.html': 'text/html', + '.gif' : 'image/gif', + '.jpg' : 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png' : 'image/png' + }, + lookup: function (filepath) { + var ext = require('path').extname(filepath), + type = tree._mime._types[ext]; + if (type === undefined) { + throw new Error('Optional dependency "mime" is required for ' + ext); + } + return type; + }, + charsets: { + lookup: function (type) { + // assumes all text types are UTF-8 + return type && (/^text\//).test(type) ? 'UTF-8' : ''; + } + } +}; + +var mathFunctions = [{name:"ceil"}, {name:"floor"}, {name: "sqrt"}, {name:"abs"}, + {name:"tan", unit: ""}, {name:"sin", unit: ""}, {name:"cos", unit: ""}, + {name:"atan", unit: "rad"}, {name:"asin", unit: "rad"}, {name:"acos", unit: "rad"}], + createMathFunction = function(name, unit) { + return function(n) { + if (unit != null) { + n = n.unify(); + } + return this._math(Math[name], unit, n); + }; + }; + +for(var i = 0; i < mathFunctions.length; i++) { + tree.functions[mathFunctions[i].name] = createMathFunction(mathFunctions[i].name, mathFunctions[i].unit); +} + +function hsla(color) { + return tree.functions.hsla(color.h, color.s, color.l, color.a); +} + +function scaled(n, size) { + if (n instanceof tree.Dimension && n.unit.is('%')) { + return parseFloat(n.value * size / 100); + } else { + return number(n); + } +} + +function number(n) { + if (n instanceof tree.Dimension) { + return parseFloat(n.unit.is('%') ? n.value / 100 : n.value); + } else if (typeof(n) === 'number') { + return n; + } else { + throw { + error: "RuntimeError", + message: "color functions take numbers as parameters" + }; + } +} + +function clamp(val) { + return Math.min(1, Math.max(0, val)); +} + +tree.functionCall = function(env, rootpath, currentDirectory) { + this.env = env; + this.rootpath = rootpath; + this.currentDirectory = currentDirectory; +}; + +tree.functionCall.prototype = tree.functions; + +})(require('./tree')); +(function (tree) { + tree.colors = { + 'aliceblue':'#f0f8ff', + 'antiquewhite':'#faebd7', + 'aqua':'#00ffff', + 'aquamarine':'#7fffd4', + 'azure':'#f0ffff', + 'beige':'#f5f5dc', + 'bisque':'#ffe4c4', + 'black':'#000000', + 'blanchedalmond':'#ffebcd', + 'blue':'#0000ff', + 'blueviolet':'#8a2be2', + 'brown':'#a52a2a', + 'burlywood':'#deb887', + 'cadetblue':'#5f9ea0', + 'chartreuse':'#7fff00', + 'chocolate':'#d2691e', + 'coral':'#ff7f50', + 'cornflowerblue':'#6495ed', + 'cornsilk':'#fff8dc', + 'crimson':'#dc143c', + 'cyan':'#00ffff', + 'darkblue':'#00008b', + 'darkcyan':'#008b8b', + 'darkgoldenrod':'#b8860b', + 'darkgray':'#a9a9a9', + 'darkgrey':'#a9a9a9', + 'darkgreen':'#006400', + 'darkkhaki':'#bdb76b', + 'darkmagenta':'#8b008b', + 'darkolivegreen':'#556b2f', + 'darkorange':'#ff8c00', + 'darkorchid':'#9932cc', + 'darkred':'#8b0000', + 'darksalmon':'#e9967a', + 'darkseagreen':'#8fbc8f', + 'darkslateblue':'#483d8b', + 'darkslategray':'#2f4f4f', + 'darkslategrey':'#2f4f4f', + 'darkturquoise':'#00ced1', + 'darkviolet':'#9400d3', + 'deeppink':'#ff1493', + 'deepskyblue':'#00bfff', + 'dimgray':'#696969', + 'dimgrey':'#696969', + 'dodgerblue':'#1e90ff', + 'firebrick':'#b22222', + 'floralwhite':'#fffaf0', + 'forestgreen':'#228b22', + 'fuchsia':'#ff00ff', + 'gainsboro':'#dcdcdc', + 'ghostwhite':'#f8f8ff', + 'gold':'#ffd700', + 'goldenrod':'#daa520', + 'gray':'#808080', + 'grey':'#808080', + 'green':'#008000', + 'greenyellow':'#adff2f', + 'honeydew':'#f0fff0', + 'hotpink':'#ff69b4', + 'indianred':'#cd5c5c', + 'indigo':'#4b0082', + 'ivory':'#fffff0', + 'khaki':'#f0e68c', + 'lavender':'#e6e6fa', + 'lavenderblush':'#fff0f5', + 'lawngreen':'#7cfc00', + 'lemonchiffon':'#fffacd', + 'lightblue':'#add8e6', + 'lightcoral':'#f08080', + 'lightcyan':'#e0ffff', + 'lightgoldenrodyellow':'#fafad2', + 'lightgray':'#d3d3d3', + 'lightgrey':'#d3d3d3', + 'lightgreen':'#90ee90', + 'lightpink':'#ffb6c1', + 'lightsalmon':'#ffa07a', + 'lightseagreen':'#20b2aa', + 'lightskyblue':'#87cefa', + 'lightslategray':'#778899', + 'lightslategrey':'#778899', + 'lightsteelblue':'#b0c4de', + 'lightyellow':'#ffffe0', + 'lime':'#00ff00', + 'limegreen':'#32cd32', + 'linen':'#faf0e6', + 'magenta':'#ff00ff', + 'maroon':'#800000', + 'mediumaquamarine':'#66cdaa', + 'mediumblue':'#0000cd', + 'mediumorchid':'#ba55d3', + 'mediumpurple':'#9370d8', + 'mediumseagreen':'#3cb371', + 'mediumslateblue':'#7b68ee', + 'mediumspringgreen':'#00fa9a', + 'mediumturquoise':'#48d1cc', + 'mediumvioletred':'#c71585', + 'midnightblue':'#191970', + 'mintcream':'#f5fffa', + 'mistyrose':'#ffe4e1', + 'moccasin':'#ffe4b5', + 'navajowhite':'#ffdead', + 'navy':'#000080', + 'oldlace':'#fdf5e6', + 'olive':'#808000', + 'olivedrab':'#6b8e23', + 'orange':'#ffa500', + 'orangered':'#ff4500', + 'orchid':'#da70d6', + 'palegoldenrod':'#eee8aa', + 'palegreen':'#98fb98', + 'paleturquoise':'#afeeee', + 'palevioletred':'#d87093', + 'papayawhip':'#ffefd5', + 'peachpuff':'#ffdab9', + 'peru':'#cd853f', + 'pink':'#ffc0cb', + 'plum':'#dda0dd', + 'powderblue':'#b0e0e6', + 'purple':'#800080', + 'red':'#ff0000', + 'rosybrown':'#bc8f8f', + 'royalblue':'#4169e1', + 'saddlebrown':'#8b4513', + 'salmon':'#fa8072', + 'sandybrown':'#f4a460', + 'seagreen':'#2e8b57', + 'seashell':'#fff5ee', + 'sienna':'#a0522d', + 'silver':'#c0c0c0', + 'skyblue':'#87ceeb', + 'slateblue':'#6a5acd', + 'slategray':'#708090', + 'slategrey':'#708090', + 'snow':'#fffafa', + 'springgreen':'#00ff7f', + 'steelblue':'#4682b4', + 'tan':'#d2b48c', + 'teal':'#008080', + 'thistle':'#d8bfd8', + 'tomato':'#ff6347', + // 'transparent':'rgba(0,0,0,0)', + 'turquoise':'#40e0d0', + 'violet':'#ee82ee', + 'wheat':'#f5deb3', + 'white':'#ffffff', + 'whitesmoke':'#f5f5f5', + 'yellow':'#ffff00', + 'yellowgreen':'#9acd32' + }; +})(require('./tree')); +(function (tree) { + +tree.Alpha = function (val) { + this.value = val; +}; +tree.Alpha.prototype = { + toCSS: function () { + return "alpha(opacity=" + + (this.value.toCSS ? this.value.toCSS() : this.value) + ")"; + }, + eval: function (env) { + if (this.value.eval) { this.value = this.value.eval(env) } + return this; + } +}; + +})(require('../tree')); +(function (tree) { + +tree.Anonymous = function (string) { + this.value = string.value || string; +}; +tree.Anonymous.prototype = { + toCSS: function () { + return this.value; + }, + eval: function () { return this }, + compare: function (x) { + if (!x.toCSS) { + return -1; + } + + var left = this.toCSS(), + right = x.toCSS(); + + if (left === right) { + return 0; + } + + return left < right ? -1 : 1; + } +}; + +})(require('../tree')); +(function (tree) { + +tree.Assignment = function (key, val) { + this.key = key; + this.value = val; +}; +tree.Assignment.prototype = { + toCSS: function () { + return this.key + '=' + (this.value.toCSS ? this.value.toCSS() : this.value); + }, + eval: function (env) { + if (this.value.eval) { + return new(tree.Assignment)(this.key, this.value.eval(env)); + } + return this; + } +}; + +})(require('../tree'));(function (tree) { + +// +// A function call node. +// +tree.Call = function (name, args, index, filename, rootpath, currentDirectory) { + this.name = name; + this.args = args; + this.index = index; + this.filename = filename; + this.rootpath = rootpath; + this.currentDirectory = currentDirectory; +}; +tree.Call.prototype = { + // + // When evaluating a function call, + // we either find the function in `tree.functions` [1], + // in which case we call it, passing the evaluated arguments, + // if this returns null or we cannot find the function, we + // simply print it out as it appeared originally [2]. + // + // The *functions.js* file contains the built-in functions. + // + // The reason why we evaluate the arguments, is in the case where + // we try to pass a variable to a function, like: `saturate(@color)`. + // The function should receive the value, not the variable. + // + eval: function (env) { + var args = this.args.map(function (a) { return a.eval(env); }), + nameLC = this.name.toLowerCase(), + result, func; + + if (nameLC in tree.functions) { // 1. + try { + func = new tree.functionCall(env, this.rootpath, this.currentDirectory); + result = func[nameLC].apply(func, args); + if (result != null) { + return result; + } + } catch (e) { + throw { type: e.type || "Runtime", + message: "error evaluating function `" + this.name + "`" + + (e.message ? ': ' + e.message : ''), + index: this.index, filename: this.filename }; + } + } + + // 2. + return new(tree.Anonymous)(this.name + + "(" + args.map(function (a) { return a.toCSS(env); }).join(', ') + ")"); + }, + + toCSS: function (env) { + return this.eval(env).toCSS(); + } +}; + +})(require('../tree')); +(function (tree) { +// +// RGB Colors - #ff0014, #eee +// +tree.Color = function (rgb, a) { + // + // The end goal here, is to parse the arguments + // into an integer triplet, such as `128, 255, 0` + // + // This facilitates operations and conversions. + // + if (Array.isArray(rgb)) { + this.rgb = rgb; + } else if (rgb.length == 6) { + this.rgb = rgb.match(/.{2}/g).map(function (c) { + return parseInt(c, 16); + }); + } else { + this.rgb = rgb.split('').map(function (c) { + return parseInt(c + c, 16); + }); + } + this.alpha = typeof(a) === 'number' ? a : 1; +}; +tree.Color.prototype = { + eval: function () { return this }, + luma: function () { return (0.2126 * this.rgb[0] / 255) + (0.7152 * this.rgb[1] / 255) + (0.0722 * this.rgb[2] / 255); }, + + // + // If we have some transparency, the only way to represent it + // is via `rgba`. Otherwise, we use the hex representation, + // which has better compatibility with older browsers. + // Values are capped between `0` and `255`, rounded and zero-padded. + // + toCSS: function (env, doNotCompress) { + var compress = env && env.compress && !doNotCompress; + if (this.alpha < 1.0) { + return "rgba(" + this.rgb.map(function (c) { + return Math.round(c); + }).concat(this.alpha).join(',' + (compress ? '' : ' ')) + ")"; + } else { + var color = this.rgb.map(function (i) { + i = Math.round(i); + i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16); + return i.length === 1 ? '0' + i : i; + }).join(''); + + if (compress) { + color = color.split(''); + + // Convert color to short format + if (color[0] == color[1] && color[2] == color[3] && color[4] == color[5]) { + color = color[0] + color[2] + color[4]; + } else { + color = color.join(''); + } + } + + return '#' + color; + } + }, + + // + // Operations have to be done per-channel, if not, + // channels will spill onto each other. Once we have + // our result, in the form of an integer triplet, + // we create a new Color node to hold the result. + // + operate: function (env, op, other) { + var result = []; + + if (! (other instanceof tree.Color)) { + other = other.toColor(); + } + + for (var c = 0; c < 3; c++) { + result[c] = tree.operate(env, op, this.rgb[c], other.rgb[c]); + } + return new(tree.Color)(result, this.alpha + other.alpha); + }, + + toHSL: function () { + var r = this.rgb[0] / 255, + g = this.rgb[1] / 255, + b = this.rgb[2] / 255, + a = this.alpha; + + var max = Math.max(r, g, b), min = Math.min(r, g, b); + var h, s, l = (max + min) / 2, d = max - min; + + if (max === min) { + h = s = 0; + } else { + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + return { h: h * 360, s: s, l: l, a: a }; + }, + //Adapted from http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript + toHSV: function () { + var r = this.rgb[0] / 255, + g = this.rgb[1] / 255, + b = this.rgb[2] / 255, + a = this.alpha; + + var max = Math.max(r, g, b), min = Math.min(r, g, b); + var h, s, v = max; + + var d = max - min; + if (max === 0) { + s = 0; + } else { + s = d / max; + } + + if (max === min) { + h = 0; + } else { + switch(max){ + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + return { h: h * 360, s: s, v: v, a: a }; + }, + toARGB: function () { + var argb = [Math.round(this.alpha * 255)].concat(this.rgb); + return '#' + argb.map(function (i) { + i = Math.round(i); + i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16); + return i.length === 1 ? '0' + i : i; + }).join(''); + }, + compare: function (x) { + if (!x.rgb) { + return -1; + } + + return (x.rgb[0] === this.rgb[0] && + x.rgb[1] === this.rgb[1] && + x.rgb[2] === this.rgb[2] && + x.alpha === this.alpha) ? 0 : -1; + } +}; + + +})(require('../tree')); +(function (tree) { + +tree.Comment = function (value, silent) { + this.value = value; + this.silent = !!silent; +}; +tree.Comment.prototype = { + toCSS: function (env) { + return env.compress ? '' : this.value; + }, + eval: function () { return this } +}; + +})(require('../tree')); +(function (tree) { + +tree.Condition = function (op, l, r, i, negate) { + this.op = op.trim(); + this.lvalue = l; + this.rvalue = r; + this.index = i; + this.negate = negate; +}; +tree.Condition.prototype.eval = function (env) { + var a = this.lvalue.eval(env), + b = this.rvalue.eval(env); + + var i = this.index, result; + + var result = (function (op) { + switch (op) { + case 'and': + return a && b; + case 'or': + return a || b; + default: + if (a.compare) { + result = a.compare(b); + } else if (b.compare) { + result = b.compare(a); + } else { + throw { type: "Type", + message: "Unable to perform comparison", + index: i }; + } + switch (result) { + case -1: return op === '<' || op === '=<'; + case 0: return op === '=' || op === '>=' || op === '=<'; + case 1: return op === '>' || op === '>='; + } + } + })(this.op); + return this.negate ? !result : result; +}; + +})(require('../tree')); +(function (tree) { + +// +// A number with a unit +// +tree.Dimension = function (value, unit) { + this.value = parseFloat(value); + this.unit = (unit && unit instanceof tree.Unit) ? unit : + new(tree.Unit)(unit ? [unit] : undefined); +}; + +tree.Dimension.prototype = { + eval: function (env) { + return this; + }, + toColor: function () { + return new(tree.Color)([this.value, this.value, this.value]); + }, + toCSS: function (env) { + if ((!env || env.strictUnits !== false) && !this.unit.isSingular()) { + throw new Error("Multiple units in dimension. Correct the units or use the unit function. Bad unit: "+this.unit.toString()); + } + + var value = this.value, + strValue = String(value); + + if (value !== 0 && value < 0.000001 && value > -0.000001) { + // would be output 1e-6 etc. + strValue = value.toFixed(20).replace(/0+$/, ""); + } + + if (env && env.compress) { + // Zero values doesn't need a unit + if (value === 0 && !this.unit.isAngle()) { + return strValue; + } + + // Float values doesn't need a leading zero + if (value > 0 && value < 1) { + strValue = (strValue).substr(1); + } + } + + return this.unit.isEmpty() ? strValue : (strValue + this.unit.toCSS()); + }, + + // In an operation between two Dimensions, + // we default to the first Dimension's unit, + // so `1px + 2` will yield `3px`. + operate: function (env, op, other) { + var value = tree.operate(env, op, this.value, other.value), + unit = this.unit.clone(); + + if (op === '+' || op === '-') { + if (unit.numerator.length === 0 && unit.denominator.length === 0) { + unit.numerator = other.unit.numerator.slice(0); + unit.denominator = other.unit.denominator.slice(0); + } else if (other.unit.numerator.length == 0 && unit.denominator.length == 0) { + // do nothing + } else { + other = other.convertTo(this.unit.usedUnits()); + + if(env.strictUnits !== false && other.unit.toString() !== unit.toString()) { + throw new Error("Incompatible units. Change the units or use the unit function. Bad units: '" + unit.toString() + + "' and '" + other.unit.toString() + "'."); + } + + value = tree.operate(env, op, this.value, other.value); + } + } else if (op === '*') { + unit.numerator = unit.numerator.concat(other.unit.numerator).sort(); + unit.denominator = unit.denominator.concat(other.unit.denominator).sort(); + unit.cancel(); + } else if (op === '/') { + unit.numerator = unit.numerator.concat(other.unit.denominator).sort(); + unit.denominator = unit.denominator.concat(other.unit.numerator).sort(); + unit.cancel(); + } + return new(tree.Dimension)(value, unit); + }, + + compare: function (other) { + if (other instanceof tree.Dimension) { + var a = this.unify(), b = other.unify(), + aValue = a.value, bValue = b.value; + + if (bValue > aValue) { + return -1; + } else if (bValue < aValue) { + return 1; + } else { + if (!b.unit.isEmpty() && a.unit.compare(b) !== 0) { + return -1; + } + return 0; + } + } else { + return -1; + } + }, + + unify: function () { + return this.convertTo({ length: 'm', duration: 's', angle: 'rad' }); + }, + + convertTo: function (conversions) { + var value = this.value, unit = this.unit.clone(), + i, groupName, group, conversion, targetUnit, derivedConversions = {}; + + if (typeof conversions === 'string') { + for(i in tree.UnitConversions) { + if (tree.UnitConversions[i].hasOwnProperty(conversions)) { + derivedConversions = {}; + derivedConversions[i] = conversions; + } + } + conversions = derivedConversions; + } + + for (groupName in conversions) { + if (conversions.hasOwnProperty(groupName)) { + targetUnit = conversions[groupName]; + group = tree.UnitConversions[groupName]; + + unit.map(function (atomicUnit, denominator) { + if (group.hasOwnProperty(atomicUnit)) { + if (denominator) { + value = value / (group[atomicUnit] / group[targetUnit]); + } else { + value = value * (group[atomicUnit] / group[targetUnit]); + } + + return targetUnit; + } + + return atomicUnit; + }); + } + } + + unit.cancel(); + + return new(tree.Dimension)(value, unit); + } +}; + +// http://www.w3.org/TR/css3-values/#absolute-lengths +tree.UnitConversions = { + length: { + 'm': 1, + 'cm': 0.01, + 'mm': 0.001, + 'in': 0.0254, + 'pt': 0.0254 / 72, + 'pc': 0.0254 / 72 * 12 + }, + duration: { + 's': 1, + 'ms': 0.001 + }, + angle: { + 'rad': 1/(2*Math.PI), + 'deg': 1/360, + 'grad': 1/400, + 'turn': 1 + } +}; + +tree.Unit = function (numerator, denominator) { + this.numerator = numerator ? numerator.slice(0).sort() : []; + this.denominator = denominator ? denominator.slice(0).sort() : []; +}; + +tree.Unit.prototype = { + clone: function () { + return new tree.Unit(this.numerator.slice(0), this.denominator.slice(0)); + }, + + toCSS: function () { + if (this.numerator.length >= 1) { + return this.numerator[0]; + } + if (this.denominator.length >= 1) { + return this.denominator[0]; + } + return ""; + }, + + toString: function () { + var i, returnStr = this.numerator.join("*"); + for (i = 0; i < this.denominator.length; i++) { + returnStr += "/" + this.denominator[i]; + } + return returnStr; + }, + + compare: function (other) { + return this.is(other.toCSS()) ? 0 : -1; + }, + + is: function (unitString) { + return this.toCSS() === unitString; + }, + + isAngle: function () { + return tree.UnitConversions.angle.hasOwnProperty(this.toCSS()); + }, + + isEmpty: function () { + return this.numerator.length == 0 && this.denominator.length == 0; + }, + + isSingular: function() { + return this.numerator.length <= 1 && this.denominator.length == 0; + }, + + map: function(callback) { + var i; + + for (i = 0; i < this.numerator.length; i++) { + this.numerator[i] = callback(this.numerator[i], false); + } + + for (i = 0; i < this.denominator.length; i++) { + this.denominator[i] = callback(this.denominator[i], true); + } + }, + + usedUnits: function() { + var group, groupName, result = {}; + + for (groupName in tree.UnitConversions) { + if (tree.UnitConversions.hasOwnProperty(groupName)) { + group = tree.UnitConversions[groupName]; + + this.map(function (atomicUnit) { + if (group.hasOwnProperty(atomicUnit) && !result[groupName]) { + result[groupName] = atomicUnit; + } + + return atomicUnit; + }); + } + } + + return result; + }, + + cancel: function () { + var counter = {}, atomicUnit, i; + + for (i = 0; i < this.numerator.length; i++) { + atomicUnit = this.numerator[i]; + counter[atomicUnit] = (counter[atomicUnit] || 0) + 1; + } + + for (i = 0; i < this.denominator.length; i++) { + atomicUnit = this.denominator[i]; + counter[atomicUnit] = (counter[atomicUnit] || 0) - 1; + } + + this.numerator = []; + this.denominator = []; + + for (atomicUnit in counter) { + if (counter.hasOwnProperty(atomicUnit)) { + var count = counter[atomicUnit]; + + if (count > 0) { + for (i = 0; i < count; i++) { + this.numerator.push(atomicUnit); + } + } else if (count < 0) { + for (i = 0; i < -count; i++) { + this.denominator.push(atomicUnit); + } + } + } + } + + this.numerator.sort(); + this.denominator.sort(); + } +}; + +})(require('../tree')); +(function (tree) { + +tree.Directive = function (name, value) { + this.name = name; + + if (Array.isArray(value)) { + this.ruleset = new(tree.Ruleset)([], value); + this.ruleset.allowImports = true; + } else { + this.value = value; + } +}; +tree.Directive.prototype = { + toCSS: function (ctx, env) { + if (this.ruleset) { + this.ruleset.root = true; + return this.name + (env.compress ? '{' : ' {\n ') + + this.ruleset.toCSS(ctx, env).trim().replace(/\n/g, '\n ') + + (env.compress ? '}': '\n}\n'); + } else { + return this.name + ' ' + this.value.toCSS() + ';\n'; + } + }, + eval: function (env) { + var evaldDirective = this; + if (this.ruleset) { + env.frames.unshift(this); + evaldDirective = new(tree.Directive)(this.name); + evaldDirective.ruleset = this.ruleset.eval(env); + env.frames.shift(); + } + return evaldDirective; + }, + variable: function (name) { return tree.Ruleset.prototype.variable.call(this.ruleset, name) }, + find: function () { return tree.Ruleset.prototype.find.apply(this.ruleset, arguments) }, + rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.ruleset) } +}; + +})(require('../tree')); +(function (tree) { + +tree.Element = function (combinator, value, index) { + this.combinator = combinator instanceof tree.Combinator ? + combinator : new(tree.Combinator)(combinator); + + if (typeof(value) === 'string') { + this.value = value.trim(); + } else if (value) { + this.value = value; + } else { + this.value = ""; + } + this.index = index; +}; +tree.Element.prototype.eval = function (env) { + return new(tree.Element)(this.combinator, + this.value.eval ? this.value.eval(env) : this.value, + this.index); +}; +tree.Element.prototype.toCSS = function (env) { + var value = (this.value.toCSS ? this.value.toCSS(env) : this.value); + if (value == '' && this.combinator.value.charAt(0) == '&') { + return ''; + } else { + return this.combinator.toCSS(env || {}) + value; + } +}; + +tree.Combinator = function (value) { + if (value === ' ') { + this.value = ' '; + } else { + this.value = value ? value.trim() : ""; + } +}; +tree.Combinator.prototype.toCSS = function (env) { + return { + '' : '', + ' ' : ' ', + ':' : ' :', + '+' : env.compress ? '+' : ' + ', + '~' : env.compress ? '~' : ' ~ ', + '>' : env.compress ? '>' : ' > ', + '|' : env.compress ? '|' : ' | ' + }[this.value]; +}; + +})(require('../tree')); +(function (tree) { + +tree.Expression = function (value) { this.value = value; }; +tree.Expression.prototype = { + eval: function (env) { + var returnValue, + inParenthesis = this.parens && !this.parensInOp, + doubleParen = false; + if (inParenthesis) { + env.inParenthesis(); + } + if (this.value.length > 1) { + returnValue = new(tree.Expression)(this.value.map(function (e) { + return e.eval(env); + })); + } else if (this.value.length === 1) { + if (this.value[0].parens && !this.value[0].parensInOp) { + doubleParen = true; + } + returnValue = this.value[0].eval(env); + } else { + returnValue = this; + } + if (inParenthesis) { + env.outOfParenthesis(); + } + if (this.parens && this.parensInOp && !(env.isMathsOn()) && !doubleParen) { + returnValue = new(tree.Paren)(returnValue); + } + return returnValue; + }, + toCSS: function (env) { + return this.value.map(function (e) { + return e.toCSS ? e.toCSS(env) : ''; + }).join(' '); + }, + throwAwayComments: function () { + this.value = this.value.filter(function(v) { + return !(v instanceof tree.Comment); + }); + } +}; + +})(require('../tree')); +(function (tree) { + +tree.Extend = function Extend(elements, index) { + this.selector = new(tree.Selector)(elements); + this.index = index; +}; + +tree.Extend.prototype.eval = function Extend_eval(env, selectors) { + var selfSelectors = findSelfSelectors(selectors || env.selectors), + targetValue = this.selector.elements[0].value; + + env.frames.forEach(function(frame) { + frame.rulesets().forEach(function(rule) { + rule.selectors.forEach(function(selector) { + selector.elements.forEach(function(element, idx) { + if (element.value === targetValue) { + selfSelectors.forEach(function(_selector) { + _selector.elements[0] = new tree.Element( + element.combinator, + _selector.elements[0].value, + _selector.elements[0].index + ); + rule.selectors.push(new tree.Selector( + selector.elements + .slice(0, idx) + .concat(_selector.elements) + .concat(selector.elements.slice(idx + 1)) + )); + }); + } + }); + }); + }); + }); + return this; +}; + +function findSelfSelectors(selectors) { + var ret = []; + + (function loop(elem, i) { + if (selectors[i] && selectors[i].length) { + selectors[i].forEach(function(s) { + loop(s.elements.concat(elem), i + 1); + }); + } + else { + ret.push({ elements: elem }); + } + })([], 0); + + return ret; +} + + +})(require('../tree')); +(function (tree) { +// +// CSS @import node +// +// The general strategy here is that we don't want to wait +// for the parsing to be completed, before we start importing +// the file. That's because in the context of a browser, +// most of the time will be spent waiting for the server to respond. +// +// On creation, we push the import path to our import queue, though +// `import,push`, we also pass it a callback, which it'll call once +// the file has been fetched, and parsed. +// +tree.Import = function (path, imports, features, once, index, rootpath) { + var that = this; + + this.once = once; + this.index = index; + this._path = path; + this.features = features; + this.rootpath = rootpath; + + // The '.less' extension is optional + if (path instanceof tree.Quoted) { + this.path = /(\.[a-z]*$)|([\?;].*)$/.test(path.value) ? path.value : path.value + '.less'; + } else { + this.path = path.value.value || path.value; + } + + this.css = /css([\?;].*)?$/.test(this.path); + + // Only pre-compile .less files + if (! this.css) { + imports.push(this.path, function (e, root, imported) { + if (e) { e.index = index; } + if (imported && that.once) { that.skip = imported; } + that.root = root || new(tree.Ruleset)([], []); + }); + } +}; + +// +// The actual import node doesn't return anything, when converted to CSS. +// The reason is that it's used at the evaluation stage, so that the rules +// it imports can be treated like any other rules. +// +// In `eval`, we make sure all Import nodes get evaluated, recursively, so +// we end up with a flat structure, which can easily be imported in the parent +// ruleset. +// +tree.Import.prototype = { + toCSS: function (env) { + var features = this.features ? ' ' + this.features.toCSS(env) : ''; + + if (this.css) { + // Add the base path if the import is relative + if (typeof this._path.value === "string" && !/^(?:[a-z-]+:|\/)/.test(this._path.value)) { + this._path.value = this.rootpath + this._path.value; + } + return "@import " + this._path.toCSS() + features + ';\n'; + } else { + return ""; + } + }, + eval: function (env) { + var ruleset, features = this.features && this.features.eval(env); + + if (this.skip) { return []; } + + if (this.css) { + return new(tree.Import)(this._path, null, features, this.once, this.index, this.rootpath); + } else { + ruleset = new(tree.Ruleset)([], this.root.rules.slice(0)); + + ruleset.evalImports(env); + + return this.features ? new(tree.Media)(ruleset.rules, this.features.value) : ruleset.rules; + } + } +}; + +})(require('../tree')); +(function (tree) { + +tree.JavaScript = function (string, index, escaped) { + this.escaped = escaped; + this.expression = string; + this.index = index; +}; +tree.JavaScript.prototype = { + eval: function (env) { + var result, + that = this, + context = {}; + + var expression = this.expression.replace(/@\{([\w-]+)\}/g, function (_, name) { + return tree.jsify(new(tree.Variable)('@' + name, that.index).eval(env)); + }); + + try { + expression = new(Function)('return (' + expression + ')'); + } catch (e) { + throw { message: "JavaScript evaluation error: `" + expression + "`" , + index: this.index }; + } + + for (var k in env.frames[0].variables()) { + context[k.slice(1)] = { + value: env.frames[0].variables()[k].value, + toJS: function () { + return this.value.eval(env).toCSS(); + } + }; + } + + try { + result = expression.call(context); + } catch (e) { + throw { message: "JavaScript evaluation error: '" + e.name + ': ' + e.message + "'" , + index: this.index }; + } + if (typeof(result) === 'string') { + return new(tree.Quoted)('"' + result + '"', result, this.escaped, this.index); + } else if (Array.isArray(result)) { + return new(tree.Anonymous)(result.join(', ')); + } else { + return new(tree.Anonymous)(result); + } + } +}; + +})(require('../tree')); + +(function (tree) { + +tree.Keyword = function (value) { this.value = value }; +tree.Keyword.prototype = { + eval: function () { return this }, + toCSS: function () { return this.value }, + compare: function (other) { + if (other instanceof tree.Keyword) { + return other.value === this.value ? 0 : 1; + } else { + return -1; + } + } +}; + +tree.True = new(tree.Keyword)('true'); +tree.False = new(tree.Keyword)('false'); + +})(require('../tree')); +(function (tree) { + +tree.Media = function (value, features) { + var selectors = this.emptySelectors(); + + this.features = new(tree.Value)(features); + this.ruleset = new(tree.Ruleset)(selectors, value); + this.ruleset.allowImports = true; +}; +tree.Media.prototype = { + toCSS: function (ctx, env) { + var features = this.features.toCSS(env); + + this.ruleset.root = (ctx.length === 0 || ctx[0].multiMedia); + return '@media ' + features + (env.compress ? '{' : ' {\n ') + + this.ruleset.toCSS(ctx, env).trim().replace(/\n/g, '\n ') + + (env.compress ? '}': '\n}\n'); + }, + eval: function (env) { + if (!env.mediaBlocks) { + env.mediaBlocks = []; + env.mediaPath = []; + } + + var media = new(tree.Media)([], []); + if(this.debugInfo) { + this.ruleset.debugInfo = this.debugInfo; + media.debugInfo = this.debugInfo; + } + var strictMathsBypass = false; + if (env.strictMaths === false) { + strictMathsBypass = true; + env.strictMaths = true; + } + try { + media.features = this.features.eval(env); + } + finally { + if (strictMathsBypass) { + env.strictMaths = false; + } + } + + env.mediaPath.push(media); + env.mediaBlocks.push(media); + + env.frames.unshift(this.ruleset); + media.ruleset = this.ruleset.eval(env); + env.frames.shift(); + + env.mediaPath.pop(); + + return env.mediaPath.length === 0 ? media.evalTop(env) : + media.evalNested(env) + }, + variable: function (name) { return tree.Ruleset.prototype.variable.call(this.ruleset, name) }, + find: function () { return tree.Ruleset.prototype.find.apply(this.ruleset, arguments) }, + rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.ruleset) }, + emptySelectors: function() { + var el = new(tree.Element)('', '&', 0); + return [new(tree.Selector)([el])]; + }, + + evalTop: function (env) { + var result = this; + + // Render all dependent Media blocks. + if (env.mediaBlocks.length > 1) { + var selectors = this.emptySelectors(); + result = new(tree.Ruleset)(selectors, env.mediaBlocks); + result.multiMedia = true; + } + + delete env.mediaBlocks; + delete env.mediaPath; + + return result; + }, + evalNested: function (env) { + var i, value, + path = env.mediaPath.concat([this]); + + // Extract the media-query conditions separated with `,` (OR). + for (i = 0; i < path.length; i++) { + value = path[i].features instanceof tree.Value ? + path[i].features.value : path[i].features; + path[i] = Array.isArray(value) ? value : [value]; + } + + // Trace all permutations to generate the resulting media-query. + // + // (a, b and c) with nested (d, e) -> + // a and d + // a and e + // b and c and d + // b and c and e + this.features = new(tree.Value)(this.permute(path).map(function (path) { + path = path.map(function (fragment) { + return fragment.toCSS ? fragment : new(tree.Anonymous)(fragment); + }); + + for(i = path.length - 1; i > 0; i--) { + path.splice(i, 0, new(tree.Anonymous)("and")); + } + + return new(tree.Expression)(path); + })); + + // Fake a tree-node that doesn't output anything. + return new(tree.Ruleset)([], []); + }, + permute: function (arr) { + if (arr.length === 0) { + return []; + } else if (arr.length === 1) { + return arr[0]; + } else { + var result = []; + var rest = this.permute(arr.slice(1)); + for (var i = 0; i < rest.length; i++) { + for (var j = 0; j < arr[0].length; j++) { + result.push([arr[0][j]].concat(rest[i])); + } + } + return result; + } + }, + bubbleSelectors: function (selectors) { + this.ruleset = new(tree.Ruleset)(selectors.slice(0), [this.ruleset]); + } +}; + +})(require('../tree')); +(function (tree) { + +tree.mixin = {}; +tree.mixin.Call = function (elements, args, index, filename, important) { + this.selector = new(tree.Selector)(elements); + this.arguments = args; + this.index = index; + this.filename = filename; + this.important = important; +}; +tree.mixin.Call.prototype = { + eval: function (env) { + var mixins, mixin, args, rules = [], match = false, i, m, f, isRecursive, isOneFound; + + args = this.arguments && this.arguments.map(function (a) { + return { name: a.name, value: a.value.eval(env) }; + }); + + for (i = 0; i < env.frames.length; i++) { + if ((mixins = env.frames[i].find(this.selector)).length > 0) { + isOneFound = true; + for (m = 0; m < mixins.length; m++) { + mixin = mixins[m]; + isRecursive = false; + for(f = 0; f < env.frames.length; f++) { + if ((!(mixin instanceof tree.mixin.Definition)) && mixin === (env.frames[f].originalRuleset || env.frames[f])) { + isRecursive = true; + break; + } + } + if (isRecursive) { + continue; + } + if (mixin.matchArgs(args, env)) { + if (!mixin.matchCondition || mixin.matchCondition(args, env)) { + try { + Array.prototype.push.apply( + rules, mixin.eval(env, args, this.important).rules); + } catch (e) { + throw { message: e.message, index: this.index, filename: this.filename, stack: e.stack }; + } + } + match = true; + } + } + if (match) { + return rules; + } + } + } + if (isOneFound) { + throw { type: 'Runtime', + message: 'No matching definition was found for `' + + this.selector.toCSS().trim() + '(' + + (args ? args.map(function (a) { + var argValue = ""; + if (a.name) { + argValue += a.name + ":"; + } + if (a.value.toCSS) { + argValue += a.value.toCSS(); + } else { + argValue += "???"; + } + return argValue; + }).join(', ') : "") + ")`", + index: this.index, filename: this.filename }; + } else { + throw { type: 'Name', + message: this.selector.toCSS().trim() + " is undefined", + index: this.index, filename: this.filename }; + } + } +}; + +tree.mixin.Definition = function (name, params, rules, condition, variadic) { + this.name = name; + this.selectors = [new(tree.Selector)([new(tree.Element)(null, name)])]; + this.params = params; + this.condition = condition; + this.variadic = variadic; + this.arity = params.length; + this.rules = rules; + this._lookups = {}; + this.required = params.reduce(function (count, p) { + if (!p.name || (p.name && !p.value)) { return count + 1 } + else { return count } + }, 0); + this.parent = tree.Ruleset.prototype; + this.frames = []; +}; +tree.mixin.Definition.prototype = { + toCSS: function () { return "" }, + variable: function (name) { return this.parent.variable.call(this, name) }, + variables: function () { return this.parent.variables.call(this) }, + find: function () { return this.parent.find.apply(this, arguments) }, + rulesets: function () { return this.parent.rulesets.apply(this) }, + + evalParams: function (env, mixinEnv, args, evaldArguments) { + var frame = new(tree.Ruleset)(null, []), + varargs, arg, + params = this.params.slice(0), + i, j, val, name, isNamedFound, argIndex; + + mixinEnv = new tree.evalEnv(mixinEnv, [frame].concat(mixinEnv.frames)); + + if (args) { + args = args.slice(0); + + for(i = 0; i < args.length; i++) { + arg = args[i]; + if (name = (arg && arg.name)) { + isNamedFound = false; + for(j = 0; j < params.length; j++) { + if (!evaldArguments[j] && name === params[j].name) { + evaldArguments[j] = arg.value.eval(env); + frame.rules.unshift(new(tree.Rule)(name, arg.value.eval(env))); + isNamedFound = true; + break; + } + } + if (isNamedFound) { + args.splice(i, 1); + i--; + continue; + } else { + throw { type: 'Runtime', message: "Named argument for " + this.name + + ' ' + args[i].name + ' not found' }; + } + } + } + } + argIndex = 0; + for (i = 0; i < params.length; i++) { + if (evaldArguments[i]) continue; + + arg = args && args[argIndex]; + + if (name = params[i].name) { + if (params[i].variadic && args) { + varargs = []; + for (j = argIndex; j < args.length; j++) { + varargs.push(args[j].value.eval(env)); + } + frame.rules.unshift(new(tree.Rule)(name, new(tree.Expression)(varargs).eval(env))); + } else { + val = arg && arg.value; + if (val) { + val = val.eval(env); + } else if (params[i].value) { + val = params[i].value.eval(mixinEnv); + frame.resetCache(); + } else { + throw { type: 'Runtime', message: "wrong number of arguments for " + this.name + + ' (' + args.length + ' for ' + this.arity + ')' }; + } + + frame.rules.unshift(new(tree.Rule)(name, val)); + evaldArguments[i] = val; + } + } + + if (params[i].variadic && args) { + for (j = argIndex; j < args.length; j++) { + evaldArguments[j] = args[j].value.eval(env); + } + } + argIndex++; + } + + return frame; + }, + eval: function (env, args, important) { + var _arguments = [], + mixinFrames = this.frames.concat(env.frames), + frame = this.evalParams(env, new(tree.evalEnv)(env, mixinFrames), args, _arguments), + context, rules, start, ruleset; + + frame.rules.unshift(new(tree.Rule)('@arguments', new(tree.Expression)(_arguments).eval(env))); + + rules = important ? + this.parent.makeImportant.apply(this).rules : this.rules.slice(0); + + ruleset = new(tree.Ruleset)(null, rules).eval(new(tree.evalEnv)(env, + [this, frame].concat(mixinFrames))); + ruleset.originalRuleset = this; + return ruleset; + }, + matchCondition: function (args, env) { + + if (this.condition && !this.condition.eval( + new(tree.evalEnv)(env, + [this.evalParams(env, new(tree.evalEnv)(env, this.frames.concat(env.frames)), args, [])] + .concat(env.frames)))) { + return false; + } + return true; + }, + matchArgs: function (args, env) { + var argsLength = (args && args.length) || 0, len, frame; + + if (! this.variadic) { + if (argsLength < this.required) { return false } + if (argsLength > this.params.length) { return false } + if ((this.required > 0) && (argsLength > this.params.length)) { return false } + } + + len = Math.min(argsLength, this.arity); + + for (var i = 0; i < len; i++) { + if (!this.params[i].name && !this.params[i].variadic) { + if (args[i].value.eval(env).toCSS() != this.params[i].value.eval(env).toCSS()) { + return false; + } + } + } + return true; + } +}; + +})(require('../tree')); +(function (tree) { + +tree.Negative = function (node) { + this.value = node; +}; +tree.Negative.prototype = { + toCSS: function (env) { + return '-' + this.value.toCSS(env); + }, + eval: function (env) { + if (env.isMathsOn()) { + return (new(tree.Operation)('*', [new(tree.Dimension)(-1), this.value])).eval(env); + } + return new(tree.Negative)(this.value.eval(env)); + } +}; + +})(require('../tree')); +(function (tree) { + +tree.Operation = function (op, operands, isSpaced) { + this.op = op.trim(); + this.operands = operands; + this.isSpaced = isSpaced; +}; +tree.Operation.prototype.eval = function (env) { + var a = this.operands[0].eval(env), + b = this.operands[1].eval(env), + temp; + + if (env.isMathsOn()) { + if (a instanceof tree.Dimension && b instanceof tree.Color) { + if (this.op === '*' || this.op === '+') { + temp = b, b = a, a = temp; + } else { + throw { type: "Operation", + message: "Can't substract or divide a color from a number" }; + } + } + if (!a.operate) { + throw { type: "Operation", + message: "Operation on an invalid type" }; + } + + return a.operate(env, this.op, b); + } else { + return new(tree.Operation)(this.op, [a, b], this.isSpaced); + } +}; +tree.Operation.prototype.toCSS = function (env) { + var separator = this.isSpaced ? " " : ""; + return this.operands[0].toCSS() + separator + this.op + separator + this.operands[1].toCSS(); +}; + +tree.operate = function (env, op, a, b) { + switch (op) { + case '+': return a + b; + case '-': return a - b; + case '*': return a * b; + case '/': return a / b; + } +}; + +})(require('../tree')); + +(function (tree) { + +tree.Paren = function (node) { + this.value = node; +}; +tree.Paren.prototype = { + toCSS: function (env) { + return '(' + this.value.toCSS(env).trim() + ')'; + }, + eval: function (env) { + return new(tree.Paren)(this.value.eval(env)); + } +}; + +})(require('../tree')); +(function (tree) { + +tree.Quoted = function (str, content, escaped, i) { + this.escaped = escaped; + this.value = content || ''; + this.quote = str.charAt(0); + this.index = i; +}; +tree.Quoted.prototype = { + toCSS: function () { + if (this.escaped) { + return this.value; + } else { + return this.quote + this.value + this.quote; + } + }, + eval: function (env) { + var that = this; + var value = this.value.replace(/`([^`]+)`/g, function (_, exp) { + return new(tree.JavaScript)(exp, that.index, true).eval(env).value; + }).replace(/@\{([\w-]+)\}/g, function (_, name) { + var v = new(tree.Variable)('@' + name, that.index).eval(env, true); + return (v instanceof tree.Quoted) ? v.value : v.toCSS(); + }); + return new(tree.Quoted)(this.quote + value + this.quote, value, this.escaped, this.index); + }, + compare: function (x) { + if (!x.toCSS) { + return -1; + } + + var left = this.toCSS(), + right = x.toCSS(); + + if (left === right) { + return 0; + } + + return left < right ? -1 : 1; + } +}; + +})(require('../tree')); +(function (tree) { + +tree.Rule = function (name, value, important, index, inline) { + this.name = name; + this.value = (value instanceof tree.Value) ? value : new(tree.Value)([value]); + this.important = important ? ' ' + important.trim() : ''; + this.index = index; + this.inline = inline || false; + + if (name.charAt(0) === '@') { + this.variable = true; + } else { this.variable = false } +}; +tree.Rule.prototype.toCSS = function (env) { + if (this.variable) { return "" } + else { + return this.name + (env.compress ? ':' : ': ') + + this.value.toCSS(env) + + this.important + (this.inline ? "" : ";"); + } +}; + +tree.Rule.prototype.eval = function (env) { + var strictMathsBypass = false; + if (this.name === "font" && env.strictMaths === false) { + strictMathsBypass = true; + env.strictMaths = true; + } + try { + return new(tree.Rule)(this.name, + this.value.eval(env), + this.important, + this.index, this.inline); + } + finally { + if (strictMathsBypass) { + env.strictMaths = false; + } + } +}; + +tree.Rule.prototype.makeImportant = function () { + return new(tree.Rule)(this.name, + this.value, + "!important", + this.index, this.inline); +}; + +})(require('../tree')); +(function (tree) { + +tree.Ruleset = function (selectors, rules, strictImports) { + this.selectors = selectors; + this.rules = rules; + this._lookups = {}; + this.strictImports = strictImports; +}; +tree.Ruleset.prototype = { + eval: function (env) { + var selectors = this.selectors && this.selectors.map(function (s) { return s.eval(env) }); + var ruleset = new(tree.Ruleset)(selectors, this.rules.slice(0), this.strictImports); + var rules; + + ruleset.originalRuleset = this; + ruleset.root = this.root; + ruleset.allowImports = this.allowImports; + + if(this.debugInfo) { + ruleset.debugInfo = this.debugInfo; + } + + // push the current ruleset to the frames stack + env.frames.unshift(ruleset); + + // currrent selectors + if (!env.selectors) { + env.selectors = []; + } + env.selectors.unshift(this.selectors); + + // Evaluate imports + if (ruleset.root || ruleset.allowImports || !ruleset.strictImports) { + ruleset.evalImports(env); + } + + // Store the frames around mixin definitions, + // so they can be evaluated like closures when the time comes. + for (var i = 0; i < ruleset.rules.length; i++) { + if (ruleset.rules[i] instanceof tree.mixin.Definition) { + ruleset.rules[i].frames = env.frames.slice(0); + } + } + + var mediaBlockCount = (env.mediaBlocks && env.mediaBlocks.length) || 0; + + // Evaluate mixin calls. + for (var i = 0; i < ruleset.rules.length; i++) { + if (ruleset.rules[i] instanceof tree.mixin.Call) { + rules = ruleset.rules[i].eval(env).filter(function(r) { + if ((r instanceof tree.Rule) && r.variable) { + // do not pollute the scope if the variable is + // already there. consider returning false here + // but we need a way to "return" variable from mixins + return !(ruleset.variable(r.name)); + } + return true; + }); + ruleset.rules.splice.apply(ruleset.rules, [i, 1].concat(rules)); + i += rules.length-1; + ruleset.resetCache(); + } + } + + if (this.selectors) { + for (var i = 0; i < this.selectors.length; i++) { + if (this.selectors[i].extend) { + this.selectors[i].extend.eval(env, [[this.selectors[i]]].concat(env.selectors.slice(1))); + } + } + } + + // Evaluate everything else + for (var i = 0, rule; i < ruleset.rules.length; i++) { + rule = ruleset.rules[i]; + + if (! (rule instanceof tree.mixin.Definition)) { + ruleset.rules[i] = rule.eval ? rule.eval(env) : rule; + } + } + + // Pop the stack + env.frames.shift(); + env.selectors.shift(); + + if (env.mediaBlocks) { + for(var i = mediaBlockCount; i < env.mediaBlocks.length; i++) { + env.mediaBlocks[i].bubbleSelectors(selectors); + } + } + + return ruleset; + }, + evalImports: function(env) { + var i, rules; + for (i = 0; i < this.rules.length; i++) { + if (this.rules[i] instanceof tree.Import) { + rules = this.rules[i].eval(env); + if (typeof rules.length === "number") { + this.rules.splice.apply(this.rules, [i, 1].concat(rules)); + i+= rules.length-1; + } else { + this.rules.splice(i, 1, rules); + } + this.resetCache(); + } + } + }, + makeImportant: function() { + return new tree.Ruleset(this.selectors, this.rules.map(function (r) { + if (r.makeImportant) { + return r.makeImportant(); + } else { + return r; + } + }), this.strictImports); + }, + matchArgs: function (args) { + return !args || args.length === 0; + }, + resetCache: function () { + this._rulesets = null; + this._variables = null; + this._lookups = {}; + }, + variables: function () { + if (this._variables) { return this._variables } + else { + return this._variables = this.rules.reduce(function (hash, r) { + if (r instanceof tree.Rule && r.variable === true) { + hash[r.name] = r; + } + return hash; + }, {}); + } + }, + variable: function (name) { + return this.variables()[name]; + }, + rulesets: function () { + return this.rules.filter(function (r) { + return (r instanceof tree.Ruleset) || (r instanceof tree.mixin.Definition); + }); + }, + find: function (selector, self) { + self = self || this; + var rules = [], rule, match, + key = selector.toCSS(); + + if (key in this._lookups) { return this._lookups[key] } + + this.rulesets().forEach(function (rule) { + if (rule !== self) { + for (var j = 0; j < rule.selectors.length; j++) { + if (match = selector.match(rule.selectors[j])) { + if (selector.elements.length > rule.selectors[j].elements.length) { + Array.prototype.push.apply(rules, rule.find( + new(tree.Selector)(selector.elements.slice(1)), self)); + } else { + rules.push(rule); + } + break; + } + } + } + }); + return this._lookups[key] = rules; + }, + // + // Entry point for code generation + // + // `context` holds an array of arrays. + // + toCSS: function (context, env) { + var css = [], // The CSS output + rules = [], // node.Rule instances + _rules = [], // + rulesets = [], // node.Ruleset instances + paths = [], // Current selectors + selector, // The fully rendered selector + debugInfo, // Line number debugging + rule; + + if (! this.root) { + this.joinSelectors(paths, context, this.selectors); + } + + // Compile rules and rulesets + for (var i = 0; i < this.rules.length; i++) { + rule = this.rules[i]; + + if (rule.rules || (rule instanceof tree.Media)) { + rulesets.push(rule.toCSS(paths, env)); + } else if (rule instanceof tree.Directive) { + var cssValue = rule.toCSS(paths, env); + // Output only the first @charset definition as such - convert the others + // to comments in case debug is enabled + if (rule.name === "@charset") { + // Only output the debug info together with subsequent @charset definitions + // a comment (or @media statement) before the actual @charset directive would + // be considered illegal css as it has to be on the first line + if (env.charset) { + if (rule.debugInfo) { + rulesets.push(tree.debugInfo(env, rule)); + rulesets.push(new tree.Comment("/* "+cssValue.replace(/\n/g, "")+" */\n").toCSS(env)); + } + continue; + } + env.charset = true; + } + rulesets.push(cssValue); + } else if (rule instanceof tree.Comment) { + if (!rule.silent) { + if (this.root) { + rulesets.push(rule.toCSS(env)); + } else { + rules.push(rule.toCSS(env)); + } + } + } else { + if (rule.toCSS && !rule.variable) { + rules.push(rule.toCSS(env)); + } else if (rule.value && !rule.variable) { + rules.push(rule.value.toString()); + } + } + } + + // Remove last semicolon + if (env.compress && rules.length) { + rule = rules[rules.length - 1]; + if (rule.charAt(rule.length - 1) === ';') { + rules[rules.length - 1] = rule.substring(0, rule.length - 1); + } + } + + rulesets = rulesets.join(''); + + // If this is the root node, we don't render + // a selector, or {}. + // Otherwise, only output if this ruleset has rules. + if (this.root) { + css.push(rules.join(env.compress ? '' : '\n')); + } else { + if (rules.length > 0) { + debugInfo = tree.debugInfo(env, this); + selector = paths.map(function (p) { + return p.map(function (s) { + return s.toCSS(env); + }).join('').trim(); + }).join(env.compress ? ',' : ',\n'); + + // Remove duplicates + for (var i = rules.length - 1; i >= 0; i--) { + if (_rules.indexOf(rules[i]) === -1) { + _rules.unshift(rules[i]); + } + } + rules = _rules; + + css.push(debugInfo + selector + + (env.compress ? '{' : ' {\n ') + + rules.join(env.compress ? '' : '\n ') + + (env.compress ? '}' : '\n}\n')); + } + } + css.push(rulesets); + + return css.join('') + (env.compress ? '\n' : ''); + }, + + joinSelectors: function (paths, context, selectors) { + for (var s = 0; s < selectors.length; s++) { + this.joinSelector(paths, context, selectors[s]); + } + }, + + joinSelector: function (paths, context, selector) { + + var i, j, k, + hasParentSelector, newSelectors, el, sel, parentSel, + newSelectorPath, afterParentJoin, newJoinedSelector, + newJoinedSelectorEmpty, lastSelector, currentElements, + selectorsMultiplied; + + for (i = 0; i < selector.elements.length; i++) { + el = selector.elements[i]; + if (el.value === '&') { + hasParentSelector = true; + } + } + + if (!hasParentSelector) { + if (context.length > 0) { + for(i = 0; i < context.length; i++) { + paths.push(context[i].concat(selector)); + } + } + else { + paths.push([selector]); + } + return; + } + + // The paths are [[Selector]] + // The first list is a list of comma seperated selectors + // The inner list is a list of inheritance seperated selectors + // e.g. + // .a, .b { + // .c { + // } + // } + // == [[.a] [.c]] [[.b] [.c]] + // + + // the elements from the current selector so far + currentElements = []; + // the current list of new selectors to add to the path. + // We will build it up. We initiate it with one empty selector as we "multiply" the new selectors + // by the parents + newSelectors = [[]]; + + for (i = 0; i < selector.elements.length; i++) { + el = selector.elements[i]; + // non parent reference elements just get added + if (el.value !== "&") { + currentElements.push(el); + } else { + // the new list of selectors to add + selectorsMultiplied = []; + + // merge the current list of non parent selector elements + // on to the current list of selectors to add + if (currentElements.length > 0) { + this.mergeElementsOnToSelectors(currentElements, newSelectors); + } + + // loop through our current selectors + for(j = 0; j < newSelectors.length; j++) { + sel = newSelectors[j]; + // if we don't have any parent paths, the & might be in a mixin so that it can be used + // whether there are parents or not + if (context.length == 0) { + // the combinator used on el should now be applied to the next element instead so that + // it is not lost + if (sel.length > 0) { + sel[0].elements = sel[0].elements.slice(0); + sel[0].elements.push(new(tree.Element)(el.combinator, '', 0)); //new Element(el.Combinator, "")); + } + selectorsMultiplied.push(sel); + } + else { + // and the parent selectors + for(k = 0; k < context.length; k++) { + parentSel = context[k]; + // We need to put the current selectors + // then join the last selector's elements on to the parents selectors + + // our new selector path + newSelectorPath = []; + // selectors from the parent after the join + afterParentJoin = []; + newJoinedSelectorEmpty = true; + + //construct the joined selector - if & is the first thing this will be empty, + // if not newJoinedSelector will be the last set of elements in the selector + if (sel.length > 0) { + newSelectorPath = sel.slice(0); + lastSelector = newSelectorPath.pop(); + newJoinedSelector = new(tree.Selector)(lastSelector.elements.slice(0)); + newJoinedSelectorEmpty = false; + } + else { + newJoinedSelector = new(tree.Selector)([]); + } + + //put together the parent selectors after the join + if (parentSel.length > 1) { + afterParentJoin = afterParentJoin.concat(parentSel.slice(1)); + } + + if (parentSel.length > 0) { + newJoinedSelectorEmpty = false; + + // join the elements so far with the first part of the parent + newJoinedSelector.elements.push(new(tree.Element)(el.combinator, parentSel[0].elements[0].value, 0)); + newJoinedSelector.elements = newJoinedSelector.elements.concat(parentSel[0].elements.slice(1)); + } + + if (!newJoinedSelectorEmpty) { + // now add the joined selector + newSelectorPath.push(newJoinedSelector); + } + + // and the rest of the parent + newSelectorPath = newSelectorPath.concat(afterParentJoin); + + // add that to our new set of selectors + selectorsMultiplied.push(newSelectorPath); + } + } + } + + // our new selectors has been multiplied, so reset the state + newSelectors = selectorsMultiplied; + currentElements = []; + } + } + + // if we have any elements left over (e.g. .a& .b == .b) + // add them on to all the current selectors + if (currentElements.length > 0) { + this.mergeElementsOnToSelectors(currentElements, newSelectors); + } + + for(i = 0; i < newSelectors.length; i++) { + if (newSelectors[i].length > 0) { + paths.push(newSelectors[i]); + } + } + }, + + mergeElementsOnToSelectors: function(elements, selectors) { + var i, sel; + + if (selectors.length == 0) { + selectors.push([ new(tree.Selector)(elements) ]); + return; + } + + for(i = 0; i < selectors.length; i++) { + sel = selectors[i]; + + // if the previous thing in sel is a parent this needs to join on to it + if (sel.length > 0) { + sel[sel.length - 1] = new(tree.Selector)(sel[sel.length - 1].elements.concat(elements)); + } + else { + sel.push(new(tree.Selector)(elements)); + } + } + } +}; +})(require('../tree')); +(function (tree) { + +tree.Selector = function (elements, extend) { + this.elements = elements; + this.extend = extend; +}; +tree.Selector.prototype.match = function (other) { + var elements = this.elements, + len = elements.length, + oelements, olen, max, i; + + oelements = other.elements.slice( + (other.elements.length && other.elements[0].value === "&") ? 1 : 0); + olen = oelements.length; + max = Math.min(len, olen) + + if (olen === 0 || len < olen) { + return false; + } else { + for (i = 0; i < max; i++) { + if (elements[i].value !== oelements[i].value) { + return false; + } + } + } + return true; +}; +tree.Selector.prototype.eval = function (env) { + return new(tree.Selector)(this.elements.map(function (e) { + return e.eval(env); + }), this.extend); +}; +tree.Selector.prototype.toCSS = function (env) { + if (this._css) { return this._css } + + if (this.elements[0].combinator.value === "") { + this._css = ' '; + } else { + this._css = ''; + } + + this._css += this.elements.map(function (e) { + if (typeof(e) === 'string') { + return ' ' + e.trim(); + } else { + return e.toCSS(env); + } + }).join(''); + + return this._css; +}; + +})(require('../tree')); +(function (tree) { + +tree.UnicodeDescriptor = function (value) { + this.value = value; +}; +tree.UnicodeDescriptor.prototype = { + toCSS: function (env) { + return this.value; + }, + eval: function () { return this } +}; + +})(require('../tree')); +(function (tree) { + +tree.URL = function (val, rootpath) { + this.value = val; + this.rootpath = rootpath; +}; +tree.URL.prototype = { + toCSS: function () { + return "url(" + this.value.toCSS() + ")"; + }, + eval: function (ctx) { + var val = this.value.eval(ctx), rootpath; + + // Add the base path if the URL is relative + if (this.rootpath && typeof val.value === "string" && ctx.isPathRelative(val.value)) { + rootpath = this.rootpath; + if (!val.quote) { + rootpath = rootpath.replace(/[\(\)'"\s]/g, function(match) { return "\\"+match; }); + } + val.value = rootpath + val.value; + } + + return new(tree.URL)(val, null); + } +}; + +})(require('../tree')); +(function (tree) { + +tree.Value = function (value) { + this.value = value; + this.is = 'value'; +}; +tree.Value.prototype = { + eval: function (env) { + if (this.value.length === 1) { + return this.value[0].eval(env); + } else { + return new(tree.Value)(this.value.map(function (v) { + return v.eval(env); + })); + } + }, + toCSS: function (env) { + return this.value.map(function (e) { + return e.toCSS(env); + }).join(env.compress ? ',' : ', '); + } +}; + +})(require('../tree')); +(function (tree) { + +tree.Variable = function (name, index, file) { this.name = name, this.index = index, this.file = file }; +tree.Variable.prototype = { + eval: function (env) { + var variable, v, name = this.name; + + if (name.indexOf('@@') == 0) { + name = '@' + new(tree.Variable)(name.slice(1)).eval(env).value; + } + + if (this.evaluating) { + throw { type: 'Name', + message: "Recursive variable definition for " + name, + filename: this.file, + index: this.index }; + } + + this.evaluating = true; + + if (variable = tree.find(env.frames, function (frame) { + if (v = frame.variable(name)) { + return v.value.eval(env); + } + })) { + this.evaluating = false; + return variable; + } + else { + throw { type: 'Name', + message: "variable " + name + " is undefined", + filename: this.file, + index: this.index }; + } + } +}; + +})(require('../tree')); +(function (tree) { + +tree.debugInfo = function(env, ctx) { + var result=""; + if (env.dumpLineNumbers && !env.compress) { + switch(env.dumpLineNumbers) { + case 'comments': + result = tree.debugInfo.asComment(ctx); + break; + case 'mediaquery': + result = tree.debugInfo.asMediaQuery(ctx); + break; + case 'all': + result = tree.debugInfo.asComment(ctx)+tree.debugInfo.asMediaQuery(ctx); + break; + } + } + return result; +}; + +tree.debugInfo.asComment = function(ctx) { + return '/* line ' + ctx.debugInfo.lineNumber + ', ' + ctx.debugInfo.fileName + ' */\n'; +}; + +tree.debugInfo.asMediaQuery = function(ctx) { + return '@media -sass-debug-info{filename{font-family:' + + ('file://' + ctx.debugInfo.fileName).replace(/[\/:.]/g, '\\$&') + + '}line{font-family:\\00003' + ctx.debugInfo.lineNumber + '}}\n'; +}; + +tree.find = function (obj, fun) { + for (var i = 0, r; i < obj.length; i++) { + if (r = fun.call(obj, obj[i])) { return r } + } + return null; +}; +tree.jsify = function (obj) { + if (Array.isArray(obj.value) && (obj.value.length > 1)) { + return '[' + obj.value.map(function (v) { return v.toCSS(false) }).join(', ') + ']'; + } else { + return obj.toCSS(false); + } +}; + +})(require('./tree')); +(function (tree) { + + var parseCopyProperties = [ + 'paths', // paths to search for imports on + 'optimization', // option - optimization level (for the chunker) + 'filename', // current filename, used for error reporting + 'files', // list of files that have been imported, used for import-once + 'contents', // browser-only, contents of all the files + 'rootpath', // current rootpath to append to all url's + 'relativeUrls', // option - whether to adjust URL's to be relative + 'strictImports', // option - + 'dumpLineNumbers', // option - whether to dump line numbers + 'compress', // option - whether to compress + 'mime', // browser only - mime type for sheet import + 'entryPath', // browser only - path of entry less file + 'rootFilename', // browser only - href of the entry less file + 'currentDirectory' // node only - the current directory + ]; + + tree.parseEnv = function(options) { + copyFromOriginal(options, this, parseCopyProperties); + + if (!this.contents) { this.contents = {}; } + if (!this.rootpath) { this.rootpath = ''; } + if (!this.files) { this.files = {}; } + }; + + tree.parseEnv.prototype.toSheet = function (path) { + var env = new tree.parseEnv(this); + env.href = path; + //env.title = path; + env.type = this.mime; + return env; + }; + + var evalCopyProperties = [ + 'silent', // whether to swallow errors and warnings + 'verbose', // whether to log more activity + 'compress', // whether to compress + 'ieCompat', // whether to enforce IE compatibility (IE8 data-uri) + 'strictMaths', // whether maths has to be within parenthesis + 'strictUnits' // whether units need to evaluate correctly + ]; + + tree.evalEnv = function(options, frames) { + copyFromOriginal(options, this, evalCopyProperties); + + this.frames = frames || []; + }; + + tree.evalEnv.prototype.inParenthesis = function () { + if (!this.parensStack) { + this.parensStack = []; + } + this.parensStack.push(true); + }; + + tree.evalEnv.prototype.outOfParenthesis = function () { + this.parensStack.pop(); + }; + + tree.evalEnv.prototype.isMathsOn = function () { + return this.strictMaths === false ? true : (this.parensStack && this.parensStack.length); + }; + + tree.evalEnv.prototype.isPathRelative = function (path) { + return !/^(?:[a-z-]+:|\/)/.test(path); + }; + + //todo - do the same for the toCSS env + //tree.toCSSEnv = function (options) { + //}; + + var copyFromOriginal = function(original, destination, propertiesToCopy) { + if (!original) { return; } + + for(var i = 0; i < propertiesToCopy.length; i++) { + if (original.hasOwnProperty(propertiesToCopy[i])) { + destination[propertiesToCopy[i]] = original[propertiesToCopy[i]]; + } + } + } +})(require('./tree'));// +// browser.js - client-side engine +// + +var isFileProtocol = /^(file|chrome(-extension)?|resource|qrc|app):/.test(location.protocol); + +less.env = less.env || (location.hostname == '127.0.0.1' || + location.hostname == '0.0.0.0' || + location.hostname == 'localhost' || + location.port.length > 0 || + isFileProtocol ? 'development' + : 'production'); + +// Load styles asynchronously (default: false) +// +// This is set to `false` by default, so that the body +// doesn't start loading before the stylesheets are parsed. +// Setting this to `true` can result in flickering. +// +less.async = less.async || false; +less.fileAsync = less.fileAsync || false; + +// Interval between watch polls +less.poll = less.poll || (isFileProtocol ? 1000 : 1500); + +//Setup user functions +if (less.functions) { + for(var func in less.functions) { + less.tree.functions[func] = less.functions[func]; + } +} + +var dumpLineNumbers = /!dumpLineNumbers:(comments|mediaquery|all)/.exec(location.hash); +if (dumpLineNumbers) { + less.dumpLineNumbers = dumpLineNumbers[1]; +} + +// +// Watch mode +// +less.watch = function () { + if (!less.watchMode ){ + less.env = 'development'; + initRunningMode(); + } + return this.watchMode = true +}; + +less.unwatch = function () {clearInterval(less.watchTimer); return this.watchMode = false; }; + +function initRunningMode(){ + if (less.env === 'development') { + less.optimization = 0; + less.watchTimer = setInterval(function () { + if (less.watchMode) { + loadStyleSheets(function (e, root, _, sheet, env) { + if (e) { + error(e, sheet.href); + } else if (root) { + createCSS(root.toCSS(less), sheet, env.lastModified); + } + }); + } + }, less.poll); + } else { + less.optimization = 3; + } +} + +if (/!watch/.test(location.hash)) { + less.watch(); +} + +var cache = null; + +if (less.env != 'development') { + try { + cache = (typeof(window.localStorage) === 'undefined') ? null : window.localStorage; + } catch (_) {} +} + +// +// Get all tags with the 'rel' attribute set to "stylesheet/less" +// +var links = document.getElementsByTagName('link'); +var typePattern = /^text\/(x-)?less$/; + +less.sheets = []; + +for (var i = 0; i < links.length; i++) { + if (links[i].rel === 'stylesheet/less' || (links[i].rel.match(/stylesheet/) && + (links[i].type.match(typePattern)))) { + less.sheets.push(links[i]); + } +} + +// +// With this function, it's possible to alter variables and re-render +// CSS without reloading less-files +// +var session_cache = ''; +less.modifyVars = function(record) { + var str = session_cache; + for (name in record) { + str += ((name.slice(0,1) === '@')? '' : '@') + name +': '+ + ((record[name].slice(-1) === ';')? record[name] : record[name] +';'); + } + new(less.Parser)(new less.tree.parseEnv(less)).parse(str, function (e, root) { + if (e) { + error(e, "session_cache"); + } else { + createCSS(root.toCSS(less), less.sheets[less.sheets.length - 1]); + } + }); +}; + +less.refresh = function (reload) { + var startTime, endTime; + startTime = endTime = new(Date); + + loadStyleSheets(function (e, root, _, sheet, env) { + if (e) { + return error(e, sheet.href); + } + if (env.local) { + log("loading " + sheet.href + " from cache."); + } else { + log("parsed " + sheet.href + " successfully."); + createCSS(root.toCSS(less), sheet, env.lastModified); + } + log("css for " + sheet.href + " generated in " + (new(Date) - endTime) + 'ms'); + (env.remaining === 0) && log("css generated in " + (new(Date) - startTime) + 'ms'); + endTime = new(Date); + }, reload); + + loadStyles(); +}; +less.refreshStyles = loadStyles; + +less.refresh(less.env === 'development'); + +function loadStyles() { + var styles = document.getElementsByTagName('style'); + for (var i = 0; i < styles.length; i++) { + if (styles[i].type.match(typePattern)) { + var env = new less.tree.parseEnv(less); + env.filename = document.location.href.replace(/#.*$/, ''); + + new(less.Parser)(env).parse(styles[i].innerHTML || '', function (e, cssAST) { + if (e) { + return error(e, "inline"); + } + var css = cssAST.toCSS(less); + var style = styles[i]; + style.type = 'text/css'; + if (style.styleSheet) { + style.styleSheet.cssText = css; + } else { + style.innerHTML = css; + } + }); + } + } +} + +function loadStyleSheets(callback, reload) { + for (var i = 0; i < less.sheets.length; i++) { + loadStyleSheet(less.sheets[i], callback, reload, less.sheets.length - (i + 1)); + } +} + +function pathDiff(url, baseUrl) { + // diff between two paths to create a relative path + + var urlParts = extractUrlParts(url), + baseUrlParts = extractUrlParts(baseUrl), + i, max, urlDirectories, baseUrlDirectories, diff = ""; + if (urlParts.hostPart !== baseUrlParts.hostPart) { + return ""; + } + max = Math.max(baseUrlParts.directories.length, urlParts.directories.length); + for(i = 0; i < max; i++) { + if (baseUrlParts.directories[i] !== urlParts.directories[i]) { break; } + } + baseUrlDirectories = baseUrlParts.directories.slice(i); + urlDirectories = urlParts.directories.slice(i); + for(i = 0; i < baseUrlDirectories.length-1; i++) { + diff += "../"; + } + for(i = 0; i < urlDirectories.length-1; i++) { + diff += urlDirectories[i] + "/"; + } + return diff; +} + +function extractUrlParts(url, baseUrl) { + // urlParts[1] = protocol&hostname || / + // urlParts[2] = / if path relative to host base + // urlParts[3] = directories + // urlParts[4] = filename + // urlParts[5] = parameters + + var urlPartsRegex = /^((?:[a-z-]+:)?\/+?(?:[^\/\?#]*\/)|([\/\\]))?((?:[^\/\\\?#]*[\/\\])*)([^\/\\\?#]*)([#\?].*)?$/, + urlParts = url.match(urlPartsRegex), + returner = {}, directories = [], i, baseUrlParts; + + if (!urlParts) { + throw new Error("Could not parse sheet href - '"+url+"'"); + } + + // Stylesheets in IE don't always return the full path + if (!urlParts[1] || urlParts[2]) { + baseUrlParts = baseUrl.match(urlPartsRegex); + if (!baseUrlParts) { + throw new Error("Could not parse page url - '"+baseUrl+"'"); + } + urlParts[1] = baseUrlParts[1]; + if (!urlParts[2]) { + urlParts[3] = baseUrlParts[3] + urlParts[3]; + } + } + + if (urlParts[3]) { + directories = urlParts[3].replace("\\", "/").split("/"); + + for(i = 0; i < directories.length; i++) { + if (directories[i] === ".." && i > 0) { + directories.splice(i-1, 2); + i -= 2; + } + } + } + + returner.hostPart = urlParts[1]; + returner.directories = directories; + returner.path = urlParts[1] + directories.join("/"); + returner.fileUrl = returner.path + (urlParts[4] || ""); + returner.url = returner.fileUrl + (urlParts[5] || ""); + return returner; +} + +function loadStyleSheet(sheet, callback, reload, remaining) { + + // sheet may be set to the stylesheet for the initial load or a collection of properties including + // some env variables for imports + var hrefParts = extractUrlParts(sheet.href, window.location.href); + var href = hrefParts.url; + var css = cache && cache.getItem(href); + var timestamp = cache && cache.getItem(href + ':timestamp'); + var styles = { css: css, timestamp: timestamp }; + var env; + + if (sheet instanceof less.tree.parseEnv) { + env = new less.tree.parseEnv(sheet); + } else { + env = new less.tree.parseEnv(less); + env.entryPath = hrefParts.path; + env.mime = sheet.type; + } + + if (env.relativeUrls) { + //todo - this relies on option being set on less object rather than being passed in as an option + // - need an originalRootpath + if (less.rootpath) { + env.rootpath = extractUrlParts(less.rootpath + pathDiff(hrefParts.path, env.entryPath)).path; + } else { + env.rootpath = hrefParts.path; + } + } else { + if (!less.rootpath) { + env.rootpath = env.entryPath; + } + } + + xhr(href, sheet.type, function (data, lastModified) { + // Store data this session + session_cache += data.replace(/@import .+?;/ig, ''); + + if (!reload && styles && lastModified && + (new(Date)(lastModified).valueOf() === + new(Date)(styles.timestamp).valueOf())) { + // Use local copy + createCSS(styles.css, sheet); + callback(null, null, data, sheet, { local: true, remaining: remaining }, href); + } else { + // Use remote copy (re-parse) + try { + env.contents[href] = data; // Updating content cache + env.paths = [hrefParts.path]; + env.filename = href; + env.rootFilename = env.rootFilename || href; + new(less.Parser)(env).parse(data, function (e, root) { + if (e) { return callback(e, null, null, sheet); } + try { + callback(e, root, data, sheet, { local: false, lastModified: lastModified, remaining: remaining }, href); + //TODO - there must be a better way? A generic less-to-css function that can both call error + //and removeNode where appropriate + //should also add tests + if (env.rootFilename === href) { + removeNode(document.getElementById('less-error-message:' + extractId(href))); + } + } catch (e) { + callback(e, null, null, sheet); + } + }); + } catch (e) { + callback(e, null, null, sheet); + } + } + }, function (status, url) { + callback({ type: 'File', message: "'" + url + "' wasn't found (" + status + ")" }, null, null, sheet); + }); +} + +function extractId(href) { + return href.replace(/^[a-z-]+:\/+?[^\/]+/, '' ) // Remove protocol & domain + .replace(/^\//, '' ) // Remove root / + .replace(/\.[a-zA-Z]+$/, '' ) // Remove simple extension + .replace(/[^\.\w-]+/g, '-') // Replace illegal characters + .replace(/\./g, ':'); // Replace dots with colons(for valid id) +} + +function createCSS(styles, sheet, lastModified) { + // Strip the query-string + var href = sheet.href || ''; + + // If there is no title set, use the filename, minus the extension + var id = 'less:' + (sheet.title || extractId(href)); + + // If this has already been inserted into the DOM, we may need to replace it + var oldCss = document.getElementById(id); + var keepOldCss = false; + + // Create a new stylesheet node for insertion or (if necessary) replacement + var css = document.createElement('style'); + css.setAttribute('type', 'text/css'); + if (sheet.media) { + css.setAttribute('media', sheet.media); + } + css.id = id; + + if (css.styleSheet) { // IE + try { + css.styleSheet.cssText = styles; + } catch (e) { + throw new(Error)("Couldn't reassign styleSheet.cssText."); + } + } else { + css.appendChild(document.createTextNode(styles)); + + // If new contents match contents of oldCss, don't replace oldCss + keepOldCss = (oldCss !== null && oldCss.childNodes.length > 0 && css.childNodes.length > 0 && + oldCss.firstChild.nodeValue === css.firstChild.nodeValue); + } + + var head = document.getElementsByTagName('head')[0]; + + // If there is no oldCss, just append; otherwise, only append if we need + // to replace oldCss with an updated stylesheet + if (oldCss == null || keepOldCss === false) { + var nextEl = sheet && sheet.nextSibling || null; + (nextEl || document.getElementsByTagName('head')[0]).parentNode.insertBefore(css, nextEl); + } + if (oldCss && keepOldCss === false) { + head.removeChild(oldCss); + } + + // Don't update the local store if the file wasn't modified + if (lastModified && cache) { + log('saving ' + href + ' to cache.'); + try { + cache.setItem(href, styles); + cache.setItem(href + ':timestamp', lastModified); + } catch(e) { + //TODO - could do with adding more robust error handling + log('failed to save'); + } + } +} + +function xhr(url, type, callback, errback) { + var xhr = getXMLHttpRequest(); + var async = isFileProtocol ? less.fileAsync : less.async; + + if (typeof(xhr.overrideMimeType) === 'function') { + xhr.overrideMimeType('text/css'); + } + xhr.open('GET', url, async); + xhr.setRequestHeader('Accept', type || 'text/x-less, text/css; q=0.9, */*; q=0.5'); + xhr.send(null); + + if (isFileProtocol && !less.fileAsync) { + if (xhr.status === 0 || (xhr.status >= 200 && xhr.status < 300)) { + callback(xhr.responseText); + } else { + errback(xhr.status, url); + } + } else if (async) { + xhr.onreadystatechange = function () { + if (xhr.readyState == 4) { + handleResponse(xhr, callback, errback); + } + }; + } else { + handleResponse(xhr, callback, errback); + } + + function handleResponse(xhr, callback, errback) { + if (xhr.status >= 200 && xhr.status < 300) { + callback(xhr.responseText, + xhr.getResponseHeader("Last-Modified")); + } else if (typeof(errback) === 'function') { + errback(xhr.status, url); + } + } +} + +function getXMLHttpRequest() { + if (window.XMLHttpRequest) { + return new(XMLHttpRequest); + } else { + try { + return new(ActiveXObject)("MSXML2.XMLHTTP.3.0"); + } catch (e) { + log("browser doesn't support AJAX."); + return null; + } + } +} + +function removeNode(node) { + return node && node.parentNode.removeChild(node); +} + +function log(str) { + if (less.env == 'development' && typeof(console) !== "undefined") { console.log('less: ' + str) } +} + +function error(e, rootHref) { + var id = 'less-error-message:' + extractId(rootHref || ""); + var template = '
  • {content}
  • '; + var elem = document.createElement('div'), timer, content, error = []; + var filename = e.filename || rootHref; + var filenameNoPath = filename.match(/([^\/]+(\?.*)?)$/)[1]; + + elem.id = id; + elem.className = "less-error-message"; + + content = '

    ' + (e.type || "Syntax") + "Error: " + (e.message || 'There is an error in your .less file') + + '

    ' + '

    in ' + filenameNoPath + " "; + + var errorline = function (e, i, classname) { + if (e.extract[i] != undefined) { + error.push(template.replace(/\{line\}/, (parseInt(e.line) || 0) + (i - 1)) + .replace(/\{class\}/, classname) + .replace(/\{content\}/, e.extract[i])); + } + }; + + if (e.extract) { + errorline(e, 0, ''); + errorline(e, 1, 'line'); + errorline(e, 2, ''); + content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':

    ' + + '
      ' + error.join('') + '
    '; + } else if (e.stack) { + content += '
    ' + e.stack.split('\n').slice(1).join('
    '); + } + elem.innerHTML = content; + + // CSS for error messages + createCSS([ + '.less-error-message ul, .less-error-message li {', + 'list-style-type: none;', + 'margin-right: 15px;', + 'padding: 4px 0;', + 'margin: 0;', + '}', + '.less-error-message label {', + 'font-size: 12px;', + 'margin-right: 15px;', + 'padding: 4px 0;', + 'color: #cc7777;', + '}', + '.less-error-message pre {', + 'color: #dd6666;', + 'padding: 4px 0;', + 'margin: 0;', + 'display: inline-block;', + '}', + '.less-error-message pre.line {', + 'color: #ff0000;', + '}', + '.less-error-message h3 {', + 'font-size: 20px;', + 'font-weight: bold;', + 'padding: 15px 0 5px 0;', + 'margin: 0;', + '}', + '.less-error-message a {', + 'color: #10a', + '}', + '.less-error-message .error {', + 'color: red;', + 'font-weight: bold;', + 'padding-bottom: 2px;', + 'border-bottom: 1px dashed red;', + '}' + ].join('\n'), { title: 'error-message' }); + + elem.style.cssText = [ + "font-family: Arial, sans-serif", + "border: 1px solid #e00", + "background-color: #eee", + "border-radius: 5px", + "-webkit-border-radius: 5px", + "-moz-border-radius: 5px", + "color: #e00", + "padding: 15px", + "margin-bottom: 15px" + ].join(';'); + + if (less.env == 'development') { + timer = setInterval(function () { + if (document.body) { + if (document.getElementById(id)) { + document.body.replaceChild(elem, document.getElementById(id)); + } else { + document.body.insertBefore(elem, document.body.firstChild); + } + clearInterval(timer); + } + }, 10); + } +} +// amd.js +// +// Define Less as an AMD module. +if (typeof define === "function" && define.amd) { + define(function () { return less; } ); +} +})(window); \ No newline at end of file From b502c811cbbf3a2e0aee488aec176c88d8f1d090 Mon Sep 17 00:00:00 2001 From: Justin Palmer Date: Mon, 4 Mar 2013 11:12:20 -0800 Subject: [PATCH 256/308] move less parsing to requireStylesheet --- spec/app/window-spec.coffee | 20 ++++++++++++++++++++ src/app/window.coffee | 14 ++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/spec/app/window-spec.coffee b/spec/app/window-spec.coffee index 12f79248f..0ae136754 100644 --- a/spec/app/window-spec.coffee +++ b/spec/app/window-spec.coffee @@ -1,5 +1,6 @@ $ = require 'jquery' fs = require 'fs' +{less} = require 'less' describe "Window", -> projectPath = null @@ -78,6 +79,25 @@ describe "Window", -> requireStylesheet('atom.css') expect($('head style').length).toBe lengthBefore + 1 + it "synchronously loads and parses less files at the given path and installs a style tag for it in the head", -> + $('head style[id*="markdown.less"]').remove() + lengthBefore = $('head style').length + requireStylesheet('markdown.less') + expect($('head style').length).toBe lengthBefore + 1 + + styleElt = $('head style[id*="markdown.less"]') + + fullPath = require.resolve('markdown.less') + expect(styleElt.attr('id')).toBe fullPath + + (new less.Parser).parse __read(fullPath), (e, tree) -> + throw new Error(e.message, file, e.line) if e + expect(styleElt.text()).toBe tree.toCSS() + + # doesn't append twice + requireStylesheet('markdown.less') + expect($('head style').length).toBe lengthBefore + 1 + describe ".disableStyleSheet(path)", -> it "removes styling applied by given stylesheet path", -> cssPath = require.resolve(fs.join("fixtures", "css.css")) diff --git a/src/app/window.coffee b/src/app/window.coffee index 321a60f56..3fafdd953 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -1,6 +1,7 @@ fs = require 'fs' $ = require 'jquery' ChildProcess = require 'child-process' +{less} = require 'less' require 'jquery-extensions' require 'underscore-extensions' require 'space-pen-extensions' @@ -33,7 +34,7 @@ window.setUpEnvironment = -> requireStylesheet 'overlay.css' requireStylesheet 'popover-list.css' requireStylesheet 'notification.css' - requireStylesheet 'markdown.css' + requireStylesheet 'markdown.less' if nativeStylesheetPath = require.resolve("#{platform}.css") requireStylesheet(nativeStylesheetPath) @@ -114,8 +115,17 @@ window.stylesheetElementForId = (id) -> $("head style[id='#{id}']") window.requireStylesheet = (path) -> + console.log path if fullPath = require.resolve(path) - window.applyStylesheet(fullPath, fs.read(fullPath)) + content = "" + if fs.extension(fullPath) == '.less' + (new less.Parser).parse __read(fullPath), (e, tree) -> + throw new Error(e.message, file, e.line) if e + content = tree.toCSS() + else + content = fs.read(fullPath) + + window.applyStylesheet(fullPath, content) unless fullPath throw new Error("Could not find a file at path '#{path}'") From 0e2ada4a916099e6a78eb30e7ee1eeda98b90097 Mon Sep 17 00:00:00 2001 From: Justin Palmer Date: Mon, 4 Mar 2013 11:12:33 -0800 Subject: [PATCH 257/308] markdown.css -> markdown.less --- static/markdown.css | 23 ----------------------- static/markdown.less | 25 +++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 23 deletions(-) delete mode 100644 static/markdown.css create mode 100644 static/markdown.less diff --git a/static/markdown.css b/static/markdown.css deleted file mode 100644 index c41f233ed..000000000 --- a/static/markdown.css +++ /dev/null @@ -1,23 +0,0 @@ -.source.gfm { - -webkit-font-smoothing: antialiased; -} - -.gfm .markup.heading { - font-weight: bold; -} - -.gfm .bold { - font-weight: bold; -} - -.gfm .italic { - font-style: italic; -} - -.gfm .comment.quote { - font-style: italic; -} - -.gfm .raw { - -webkit-font-smoothing: subpixel-antialiased; -} \ No newline at end of file diff --git a/static/markdown.less b/static/markdown.less new file mode 100644 index 000000000..e97b8ce25 --- /dev/null +++ b/static/markdown.less @@ -0,0 +1,25 @@ +.source { + .gfm { + -webkit-font-smoothing: antialiased; + + .markup.heading { + font-weight: bold; + } + + .bold { + font-weight: bold; + } + + .italic { + font-style: italic; + } + + .comment.quote { + font-style: italic; + } + + .raw { + -webkit-font-smoothing: subpixel-antialiased; + } + } +} From b33494eada55452ed4fac4daf5d0198f1cbed737 Mon Sep 17 00:00:00 2001 From: Justin Palmer Date: Mon, 4 Mar 2013 11:14:10 -0800 Subject: [PATCH 258/308] -console.log --- src/app/window.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/window.coffee b/src/app/window.coffee index 3fafdd953..14bb504f0 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -115,7 +115,6 @@ window.stylesheetElementForId = (id) -> $("head style[id='#{id}']") window.requireStylesheet = (path) -> - console.log path if fullPath = require.resolve(path) content = "" if fs.extension(fullPath) == '.less' From 3d3947722f66585ee1e50fc8ac187cb0fd30b586 Mon Sep 17 00:00:00 2001 From: Justin Palmer Date: Mon, 4 Mar 2013 11:58:40 -0800 Subject: [PATCH 259/308] autocomplete.css -> autocomplete.less --- .../stylesheets/{autocomplete.css => autocomplete.less} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/packages/autocomplete/stylesheets/{autocomplete.css => autocomplete.less} (100%) diff --git a/src/packages/autocomplete/stylesheets/autocomplete.css b/src/packages/autocomplete/stylesheets/autocomplete.less similarity index 100% rename from src/packages/autocomplete/stylesheets/autocomplete.css rename to src/packages/autocomplete/stylesheets/autocomplete.less From bd0751e17ee846447e2e34df8430488ddcec5061 Mon Sep 17 00:00:00 2001 From: Justin Palmer Date: Mon, 4 Mar 2013 11:58:55 -0800 Subject: [PATCH 260/308] braket-matcher.css -> bracket-matcher.less --- .../stylesheets/{bracket-matcher.css => bracket-matcher.less} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/packages/bracket-matcher/stylesheets/{bracket-matcher.css => bracket-matcher.less} (100%) diff --git a/src/packages/bracket-matcher/stylesheets/bracket-matcher.css b/src/packages/bracket-matcher/stylesheets/bracket-matcher.less similarity index 100% rename from src/packages/bracket-matcher/stylesheets/bracket-matcher.css rename to src/packages/bracket-matcher/stylesheets/bracket-matcher.less From 3821a492fc0693d7b6ddbeb6d83238cb0d1308cb Mon Sep 17 00:00:00 2001 From: Justin Palmer Date: Mon, 4 Mar 2013 11:59:13 -0800 Subject: [PATCH 261/308] command-logger.css -> command-logger.less --- .../stylesheets/{command-logger.css => command-logger.less} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/packages/command-logger/stylesheets/{command-logger.css => command-logger.less} (100%) diff --git a/src/packages/command-logger/stylesheets/command-logger.css b/src/packages/command-logger/stylesheets/command-logger.less similarity index 100% rename from src/packages/command-logger/stylesheets/command-logger.css rename to src/packages/command-logger/stylesheets/command-logger.less From 03642a292303a992dcee99c6bc14f566602028ee Mon Sep 17 00:00:00 2001 From: Justin Palmer Date: Mon, 4 Mar 2013 11:59:26 -0800 Subject: [PATCH 262/308] editor-stats.css -> editor-stats.less --- .../editor-stats/stylesheets/editor-stats.css | 45 ------------------- .../stylesheets/editor-stats.less | 45 +++++++++++++++++++ 2 files changed, 45 insertions(+), 45 deletions(-) delete mode 100644 src/packages/editor-stats/stylesheets/editor-stats.css create mode 100644 src/packages/editor-stats/stylesheets/editor-stats.less diff --git a/src/packages/editor-stats/stylesheets/editor-stats.css b/src/packages/editor-stats/stylesheets/editor-stats.css deleted file mode 100644 index a7d5e3ff2..000000000 --- a/src/packages/editor-stats/stylesheets/editor-stats.css +++ /dev/null @@ -1,45 +0,0 @@ -.editor-stats-wrapper { - padding: 5px; - box-sizing: border-box; - border-top: 1px solid rgba(255, 255, 255, 0.05); - z-index: 9999; -} - -.editor-stats { - height: 50px; - width: 100%; - background: #1d1f21; - border: 1px solid rgba(0, 0, 0, 0.3); - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - border-right: 1px solid rgba(255, 255, 255, 0.1); -} - -.editor-stats rect.bar { - fill: rgba(255, 255, 255, 0.2); - shape-rendering: crispedges; -} - -.editor-stats rect.bar.max { - fill: rgba(0, 163, 255, 1); -} - -.editor-stats text { - font-size: 10px; - fill: rgba(255, 255, 255, 0.2); - font-family: Courier; -} - -.editor-stats .minor text { - display: none; -} - -.editor-stats line { - stroke: #ccc; - stroke-opacity: 0.05; - stroke-width: 1px; - shape-rendering: crispedges; -} - -.editor-stats path.domain { - fill: none; -} diff --git a/src/packages/editor-stats/stylesheets/editor-stats.less b/src/packages/editor-stats/stylesheets/editor-stats.less new file mode 100644 index 000000000..26efaafde --- /dev/null +++ b/src/packages/editor-stats/stylesheets/editor-stats.less @@ -0,0 +1,45 @@ +.editor-stats-wrapper { + padding: 5px; + box-sizing: border-box; + border-top: 1px solid rgba(255, 255, 255, 0.05); + z-index: 9999; +} + +.editor-stats { + height: 50px; + width: 100%; + background: #1d1f21; + border: 1px solid rgba(0, 0, 0, 0.3); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + border-right: 1px solid rgba(255, 255, 255, 0.1); + + .bar { + fill: rgba(255, 255, 255, 0.2); + shape-rendering: crispedges; + + &.max { + fill: rgba(0, 163, 255, 1); + } + } + + text { + font-size: 10px; + fill: rgba(255, 255, 255, 0.2); + font-family: Courier; + } + + .minor text { + display: none; + } + + line { + stroke: #ccc; + stroke-opacity: 0.05; + stroke-width: 1px; + shape-rendering: crispedges; + } + + path.domain { + display: none; + } +} From dcfee2d9d93722094a7571a4db34fee2b598d620 Mon Sep 17 00:00:00 2001 From: Justin Palmer Date: Mon, 4 Mar 2013 12:00:08 -0800 Subject: [PATCH 263/308] markdown preview to less. also seperate out pygments colors --- .../stylesheets/markdown-preview.css | 438 ------------------ .../stylesheets/markdown-preview.less | 385 +++++++++++++++ .../stylesheets/pygments.less | 201 ++++++++ 3 files changed, 586 insertions(+), 438 deletions(-) delete mode 100644 src/packages/markdown-preview/stylesheets/markdown-preview.css create mode 100644 src/packages/markdown-preview/stylesheets/markdown-preview.less create mode 100644 src/packages/markdown-preview/stylesheets/pygments.less diff --git a/src/packages/markdown-preview/stylesheets/markdown-preview.css b/src/packages/markdown-preview/stylesheets/markdown-preview.css deleted file mode 100644 index 1138dc1b7..000000000 --- a/src/packages/markdown-preview/stylesheets/markdown-preview.css +++ /dev/null @@ -1,438 +0,0 @@ -.markdown-preview { - font-family: "Helvetica Neue", Helvetica, sans-serif; - font-size: 14px; - line-height: 1.6; - position: absolute; - width: 100%; - height: 100%; - top: 0px; - left: 0px; - background-color: #fff; - overflow: auto; - z-index: 3; - box-sizing: border-box; - padding: 20px; -} - -.markdown-body { - min-width: 680px; -} - -.markdown-body pre, -.markdown-body code, -.markdown-body tt { - font-size: 12px; - font-family: Consolas, "Liberation Mono", Courier, monospace; -} - -.markdown-body a { - color: #4183c4; -} - -.markdown-body ol > li { - list-style-type: decimal; -} - -.markdown-body ul > li { - list-style-type: disc; -} - -.markdown-spinner { - margin: auto; - background-image: url(images/octocat-spinner-128.gif); - background-repeat: no-repeat; - background-size: 64px; - background-position: top center; - padding-top: 70px; - text-align: center; -} - - -/* this code below was copied from https://github.com/assets/stylesheets/primer/components/markdown.css */ -/* we really need to get primer in here somehow. */ -.markdown-body { - font-size: 14px; - line-height: 1.6; - overflow: hidden; } - .markdown-body > *:first-child { - margin-top: 0 !important; } - .markdown-body > *:last-child { - margin-bottom: 0 !important; } - .markdown-body a.absent { - color: #c00; } - .markdown-body a.anchor { - display: block; - padding-left: 30px; - margin-left: -30px; - cursor: pointer; - position: absolute; - top: 0; - left: 0; - bottom: 0; } - .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { - margin: 20px 0 10px; - padding: 0; - font-weight: bold; - -webkit-font-smoothing: antialiased; - cursor: text; - position: relative; } - .markdown-body h1 .mini-icon-link, .markdown-body h2 .mini-icon-link, .markdown-body h3 .mini-icon-link, .markdown-body h4 .mini-icon-link, .markdown-body h5 .mini-icon-link, .markdown-body h6 .mini-icon-link { - display: none; - color: #000; } - .markdown-body h1:hover a.anchor, .markdown-body h2:hover a.anchor, .markdown-body h3:hover a.anchor, .markdown-body h4:hover a.anchor, .markdown-body h5:hover a.anchor, .markdown-body h6:hover a.anchor { - text-decoration: none; - line-height: 1; - padding-left: 0; - margin-left: -22px; - top: 15%; } - .markdown-body h1:hover a.anchor .mini-icon-link, .markdown-body h2:hover a.anchor .mini-icon-link, .markdown-body h3:hover a.anchor .mini-icon-link, .markdown-body h4:hover a.anchor .mini-icon-link, .markdown-body h5:hover a.anchor .mini-icon-link, .markdown-body h6:hover a.anchor .mini-icon-link { - display: inline-block; } - .markdown-body h1 tt, .markdown-body h1 code, .markdown-body h2 tt, .markdown-body h2 code, .markdown-body h3 tt, .markdown-body h3 code, .markdown-body h4 tt, .markdown-body h4 code, .markdown-body h5 tt, .markdown-body h5 code, .markdown-body h6 tt, .markdown-body h6 code { - font-size: inherit; } - .markdown-body h1 { - font-size: 28px; - color: #000; } - .markdown-body h2 { - font-size: 24px; - border-bottom: 1px solid #ccc; - color: #000; } - .markdown-body h3 { - font-size: 18px; } - .markdown-body h4 { - font-size: 16px; } - .markdown-body h5 { - font-size: 14px; } - .markdown-body h6 { - color: #777; - font-size: 14px; } - .markdown-body p, - .markdown-body blockquote, - .markdown-body ul, .markdown-body ol, .markdown-body dl, - .markdown-body table, - .markdown-body pre { - margin: 15px 0; } - .markdown-body hr { - background: transparent url("https://a248.e.akamai.net/assets.github.com/assets/primer/markdown/dirty-shade-0e7d81b119cc9beae17b0c98093d121fa0050a74.png") repeat-x 0 0; - border: 0 none; - color: #ccc; - height: 4px; - padding: 0; } - .markdown-body > h2:first-child, .markdown-body > h1:first-child, .markdown-body > h1:first-child + h2, .markdown-body > h3:first-child, .markdown-body > h4:first-child, .markdown-body > h5:first-child, .markdown-body > h6:first-child { - margin-top: 0; - padding-top: 0; } - .markdown-body a:first-child h1, .markdown-body a:first-child h2, .markdown-body a:first-child h3, .markdown-body a:first-child h4, .markdown-body a:first-child h5, .markdown-body a:first-child h6 { - margin-top: 0; - padding-top: 0; } - .markdown-body h1 + p, - .markdown-body h2 + p, - .markdown-body h3 + p, - .markdown-body h4 + p, - .markdown-body h5 + p, - .markdown-body h6 + p { - margin-top: 0; } - .markdown-body li p.first { - display: inline-block; } - .markdown-body ul, .markdown-body ol { - padding-left: 30px; } - .markdown-body ul.no-list, .markdown-body ol.no-list { - list-style-type: none; - padding: 0; } - .markdown-body ul li > :first-child, - .markdown-body ul li ul:first-of-type, .markdown-body ol li > :first-child, - .markdown-body ol li ul:first-of-type { - margin-top: 0px; } - .markdown-body ul ul, - .markdown-body ul ol, - .markdown-body ol ol, - .markdown-body ol ul { - margin-bottom: 0; } - .markdown-body dl { - padding: 0; } - .markdown-body dl dt { - font-size: 14px; - font-weight: bold; - font-style: italic; - padding: 0; - margin: 15px 0 5px; } - .markdown-body dl dt:first-child { - padding: 0; } - .markdown-body dl dt > :first-child { - margin-top: 0px; } - .markdown-body dl dt > :last-child { - margin-bottom: 0px; } - .markdown-body dl dd { - margin: 0 0 15px; - padding: 0 15px; } - .markdown-body dl dd > :first-child { - margin-top: 0px; } - .markdown-body dl dd > :last-child { - margin-bottom: 0px; } - .markdown-body blockquote { - border-left: 4px solid #DDD; - padding: 0 15px; - color: #777; } - .markdown-body blockquote > :first-child { - margin-top: 0px; } - .markdown-body blockquote > :last-child { - margin-bottom: 0px; } - .markdown-body table th { - font-weight: bold; } - .markdown-body table th, .markdown-body table td { - border: 1px solid #ccc; - padding: 6px 13px; } - .markdown-body table tr { - border-top: 1px solid #ccc; - background-color: #fff; } - .markdown-body table tr:nth-child(2n) { - background-color: #f8f8f8; } - .markdown-body img { - max-width: 100%; - -moz-box-sizing: border-box; - box-sizing: border-box; } - .markdown-body span.frame { - display: block; - overflow: hidden; } - .markdown-body span.frame > span { - border: 1px solid #ddd; - display: block; - float: left; - overflow: hidden; - margin: 13px 0 0; - padding: 7px; - width: auto; } - .markdown-body span.frame span img { - display: block; - float: left; } - .markdown-body span.frame span span { - clear: both; - color: #333; - display: block; - padding: 5px 0 0; } - .markdown-body span.align-center { - display: block; - overflow: hidden; - clear: both; } - .markdown-body span.align-center > span { - display: block; - overflow: hidden; - margin: 13px auto 0; - text-align: center; } - .markdown-body span.align-center span img { - margin: 0 auto; - text-align: center; } - .markdown-body span.align-right { - display: block; - overflow: hidden; - clear: both; } - .markdown-body span.align-right > span { - display: block; - overflow: hidden; - margin: 13px 0 0; - text-align: right; } - .markdown-body span.align-right span img { - margin: 0; - text-align: right; } - .markdown-body span.float-left { - display: block; - margin-right: 13px; - overflow: hidden; - float: left; } - .markdown-body span.float-left span { - margin: 13px 0 0; } - .markdown-body span.float-right { - display: block; - margin-left: 13px; - overflow: hidden; - float: right; } - .markdown-body span.float-right > span { - display: block; - overflow: hidden; - margin: 13px auto 0; - text-align: right; } - .markdown-body code, .markdown-body tt { - margin: 0 2px; - padding: 0px 5px; - border: 1px solid #eaeaea; - background-color: #f8f8f8; - border-radius: 3px; } - .markdown-body code { - white-space: nowrap; } - .markdown-body pre > code { - margin: 0; - padding: 0; - white-space: pre; - border: none; - background: transparent; } - .markdown-body .highlight pre, .markdown-body pre { - background-color: #f8f8f8; - border: 1px solid #ccc; - font-size: 13px; - line-height: 19px; - overflow: auto; - padding: 6px 10px; - border-radius: 3px; } - .markdown-body pre code, .markdown-body pre tt { - margin: 0; - padding: 0; - background-color: transparent; - border: none; } - -/* this code was copied from https://github.com/assets/stylesheets/primer/components/pygments.css */ -/* the .markdown-body class was then added to all rules */ -.markdown-body .highlight { - background: #ffffff; } - .markdown-body .highlight .c { - color: #999988; - font-style: italic; } - .markdown-body .highlight .err { - color: #a61717; - background-color: #e3d2d2; } - .markdown-body .highlight .k { - font-weight: bold; } - .markdown-body .highlight .o { - font-weight: bold; } - .markdown-body .highlight .cm { - color: #999988; - font-style: italic; } - .markdown-body .highlight .cp { - color: #999999; - font-weight: bold; } - .markdown-body .highlight .c1 { - color: #999988; - font-style: italic; } - .markdown-body .highlight .cs { - color: #999999; - font-weight: bold; - font-style: italic; } - .markdown-body .highlight .gd { - color: #000000; - background-color: #ffdddd; } - .markdown-body .highlight .gd .x { - color: #000000; - background-color: #ffaaaa; } - .markdown-body .highlight .ge { - font-style: italic; } - .markdown-body .highlight .gr { - color: #aa0000; } - .markdown-body .highlight .gh { - color: #999999; } - .markdown-body .highlight .gi { - color: #000000; - background-color: #ddffdd; } - .markdown-body .highlight .gi .x { - color: #000000; - background-color: #aaffaa; } - .markdown-body .highlight .go { - color: #888888; } - .markdown-body .highlight .gp { - color: #555555; } - .markdown-body .highlight .gs { - font-weight: bold; } - .markdown-body .highlight .gu { - color: #800080; - font-weight: bold; } - .markdown-body .highlight .gt { - color: #aa0000; } - .markdown-body .highlight .kc { - font-weight: bold; } - .markdown-body .highlight .kd { - font-weight: bold; } - .markdown-body .highlight .kn { - font-weight: bold; } - .markdown-body .highlight .kp { - font-weight: bold; } - .markdown-body .highlight .kr { - font-weight: bold; } - .markdown-body .highlight .kt { - color: #445588; - font-weight: bold; } - .markdown-body .highlight .m { - color: #009999; } - .markdown-body .highlight .s { - color: #d14; } - .markdown-body .highlight .na { - color: #008080; } - .markdown-body .highlight .nb { - color: #0086B3; } - .markdown-body .highlight .nc { - color: #445588; - font-weight: bold; } - .markdown-body .highlight .no { - color: #008080; } - .markdown-body .highlight .ni { - color: #800080; } - .markdown-body .highlight .ne { - color: #990000; - font-weight: bold; } - .markdown-body .highlight .nf { - color: #990000; - font-weight: bold; } - .markdown-body .highlight .nn { - color: #555555; } - .markdown-body .highlight .nt { - color: #000080; } - .markdown-body .highlight .nv { - color: #008080; } - .markdown-body .highlight .ow { - font-weight: bold; } - .markdown-body .highlight .w { - color: #bbbbbb; } - .markdown-body .highlight .mf { - color: #009999; } - .markdown-body .highlight .mh { - color: #009999; } - .markdown-body .highlight .mi { - color: #009999; } - .markdown-body .highlight .mo { - color: #009999; } - .markdown-body .highlight .sb { - color: #d14; } - .markdown-body .highlight .sc { - color: #d14; } - .markdown-body .highlight .sd { - color: #d14; } - .markdown-body .highlight .s2 { - color: #d14; } - .markdown-body .highlight .se { - color: #d14; } - .markdown-body .highlight .sh { - color: #d14; } - .markdown-body .highlight .si { - color: #d14; } - .markdown-body .highlight .sx { - color: #d14; } - .markdown-body .highlight .sr { - color: #009926; } - .markdown-body .highlight .s1 { - color: #d14; } - .markdown-body .highlight .ss { - color: #990073; } - .markdown-body .highlight .bp { - color: #999999; } - .markdown-body .highlight .vc { - color: #008080; } - .markdown-body .highlight .vg { - color: #008080; } - .markdown-body .highlight .vi { - color: #008080; } - .markdown-body .highlight .il { - color: #009999; } - .markdown-body .highlight .gc { - color: #999; - background-color: #EAF2F5; } - -.type-csharp .markdown-body .highlight .k { - color: #0000FF; } -.type-csharp .markdown-body .highlight .kt { - color: #0000FF; } -.type-csharp .markdown-body .highlight .nf { - color: #000000; - font-weight: normal; } -.type-csharp .markdown-body .highlight .nc { - color: #2B91AF; } -.type-csharp .markdown-body .highlight .nn { - color: #000000; } -.type-csharp .markdown-body .highlight .s { - color: #A31515; } -.type-csharp .markdown-body .highlight .sc { - color: #A31515; } diff --git a/src/packages/markdown-preview/stylesheets/markdown-preview.less b/src/packages/markdown-preview/stylesheets/markdown-preview.less new file mode 100644 index 000000000..db4a68195 --- /dev/null +++ b/src/packages/markdown-preview/stylesheets/markdown-preview.less @@ -0,0 +1,385 @@ +.markdown-preview { + font-family: "Helvetica Neue", Helvetica, sans-serif; + font-size: 14px; + line-height: 1.6; + position: absolute; + width: 100%; + height: 100%; + top: 0px; + left: 0px; + background-color: #fff; + overflow: auto; + z-index: 3; + box-sizing: border-box; + padding: 20px; +} + +// This is styling for generic markdownized text. Anything you put in a +// container with .markdown-body on it should render generally well. It also +// includes some GitHub Flavored Markdown specific styling (like @mentions) +.markdown-body { + + font-size: 14px; + line-height: 1.6; + overflow: hidden; + + & > *:first-child { + margin-top: 0 !important; + } + + & > *:last-child { + margin-bottom: 0 !important; + } + + // Link Colors + a.absent { + color: #c00; + } + + a.anchor { + display: block; + padding-left: 30px; + margin-left: -30px; + cursor: pointer; + position: absolute; + top: 0; + left: 0; + bottom: 0; + } + + // Headings + h1, h2, h3, h4, h5, h6 { + margin: 20px 0 10px; + padding: 0; + font-weight: bold; + -webkit-font-smoothing: antialiased; + cursor: text; + position: relative; + + .mini-icon-link { + display: none; + color: #000; + } + + &:hover a.anchor { + text-decoration: none; + line-height: 1; + padding-left: 0; + margin-left: -22px; + top: 15%; + + .mini-icon-link { + display: inline-block; + } + } + tt, code { + font-size: inherit; + } + } + + h1 { + font-size: 28px; + color: #000; + } + + h2 { + font-size: 24px; + border-bottom: 1px solid #ccc; + color: #000; + } + + h3 { + font-size: 18px; + } + + h4 { + font-size: 16px; + } + + h5 { + font-size: 14px; + } + + h6 { + color: #777; + font-size: 14px; + } + + p, + blockquote, + ul, ol, dl, + table, + pre { + margin: 15px 0; + } + + hr { + background: transparent; + border: 0 none; + color: #ccc; + height: 4px; + padding: 0; + } + + & > h2:first-child, + & > h1:first-child, + & > h1:first-child + h2, + & > h3:first-child, + & > h4:first-child, + & > h5:first-child, + & > h6:first-child { + margin-top: 0; + padding-top: 0; + } + + // fixes margin on shit like: + // + //

    The Heading

    + a:first-child { + h1, h2, h3, h4, h5, h6 { + margin-top: 0; + padding-top: 0; + } + } + + h1 + p, + h2 + p, + h3 + p, + h4 + p, + h5 + p, + h6 + p { + margin-top: 0; + } + + // ReST first graf in nested list + li p.first { + display: inline-block; + } + + // Lists, Blockquotes & Such + ul, ol { + padding-left: 30px; + + &.no-list { + list-style-type: none; + padding: 0; + } + + li > :first-child, + li ul:first-of-type { + margin-top: 0px; + } + } + + ul ul, + ul ol, + ol ol, + ol ul { + margin-bottom: 0; + } + + dl { + padding: 0; + } + + dl dt { + font-size: 14px; + font-weight: bold; + font-style: italic; + padding: 0; + margin: 15px 0 5px; + + &:first-child { + padding: 0; + } + & > :first-child { + margin-top: 0px; + } + + & > :last-child { + margin-bottom: 0px; + } + } + + dl dd { + margin: 0 0 15px; + padding: 0 15px; + & > :first-child { + margin-top: 0px; + } + + & > :last-child { + margin-bottom: 0px; + } + } + + blockquote { + border-left: 4px solid #DDD; + padding: 0 15px; + color: #777; + + & > :first-child { + margin-top: 0px; + } + + & > :last-child { + margin-bottom: 0px; + } + } + + // Tables + table { + + th { + font-weight: bold; + } + + th, td { + border: 1px solid #ccc; + padding: 6px 13px; + } + + tr { + border-top: 1px solid #ccc; + background-color: #fff; + + &:nth-child(2n) { + background-color: #f8f8f8; + } + } + } + + // Images & Stuff + img { + max-width: 100%; + @include box-sizing(); + } + + // Gollum Image Tags + + // Framed + span.frame { + display: block; + overflow: hidden; + + & > span { + border: 1px solid #ddd; + display: block; + float: left; + overflow: hidden; + margin: 13px 0 0; + padding: 7px; + width: auto; + } + + span img { + display: block; + float: left; + } + + span span { + clear: both; + color: #333; + display: block; + padding: 5px 0 0; + } + } + + span.align-center { + display: block; + overflow: hidden; + clear: both; + + & > span { + display: block; + overflow: hidden; + margin: 13px auto 0; + text-align: center; + } + + span img { + margin: 0 auto; + text-align: center; + } + } + + span.align-right { + display: block; + overflow: hidden; + clear: both; + + & > span { + display: block; + overflow: hidden; + margin: 13px 0 0; + text-align: right; + } + + span img { + margin: 0; + text-align: right; + } + } + + span.float-left { + display: block; + margin-right: 13px; + overflow: hidden; + float: left; + + span { + margin: 13px 0 0; + } + } + + span.float-right { + display: block; + margin-left: 13px; + overflow: hidden; + float: right; + + & > span { + display: block; + overflow: hidden; + margin: 13px auto 0; + text-align: right; + } + } + + // Inline code snippets + code, tt { + margin: 0 2px; + padding: 0px 5px; + border: 1px solid #eaeaea; + background-color: #f8f8f8; + border-radius:3px; + } + + code { white-space: nowrap; } + + // Code tags within code blocks (
    s)
    +  pre > code {
    +    margin: 0;
    +    padding: 0;
    +    white-space: pre;
    +    border: none;
    +    background: transparent;
    +  }
    +
    +  .highlight pre, pre {
    +    background-color: #f8f8f8;
    +    border: 1px solid #ccc;
    +    font-size: 13px;
    +    line-height: 19px;
    +    overflow: auto;
    +    padding: 6px 10px;
    +    border-radius:3px;
    +  }
    +
    +  pre code, pre tt {
    +    margin: 0;
    +    padding: 0;
    +    background-color: transparent;
    +    border: none;
    +  }
    +}
    diff --git a/src/packages/markdown-preview/stylesheets/pygments.less b/src/packages/markdown-preview/stylesheets/pygments.less
    new file mode 100644
    index 000000000..f3faab07a
    --- /dev/null
    +++ b/src/packages/markdown-preview/stylesheets/pygments.less
    @@ -0,0 +1,201 @@
    +.highlight  {
    +  background: #ffffff;
    +
    +  // Comment
    +  .c { color: #999988; font-style: italic }
    +
    +  // Error
    +  .err { color: #a61717; background-color: #e3d2d2 }
    +
    +  // Keyword
    +  .k { font-weight: bold }
    +
    +  // Operator
    +  .o { font-weight: bold }
    +
    +  // Comment.Multiline
    +  .cm { color: #999988; font-style: italic }
    +
    +  // Comment.Preproc
    +  .cp { color: #999999; font-weight: bold }
    +
    +  // Comment.Single
    +  .c1 { color: #999988; font-style: italic }
    +
    +  // Comment.Special
    +  .cs { color: #999999; font-weight: bold; font-style: italic }
    +
    +  // Generic.Deleted
    +  .gd { color: #000000; background-color: #ffdddd }
    +
    +  // Generic.Deleted.Specific
    +  .gd .x { color: #000000; background-color: #ffaaaa }
    +
    +  // Generic.Emph
    +  .ge { font-style: italic }
    +
    +  // Generic.Error
    +  .gr { color: #aa0000 }
    +
    +  // Generic.Heading
    +  .gh { color: #999999 }
    +
    +  // Generic.Inserted
    +  .gi { color: #000000; background-color: #ddffdd }
    +
    +  // Generic.Inserted.Specific
    +  .gi .x { color: #000000; background-color: #aaffaa }
    +
    +  // Generic.Output
    +  .go { color: #888888 }
    +
    +  // Generic.Prompt
    +  .gp { color: #555555 }
    +
    +  // Generic.Strong
    +  .gs { font-weight: bold }
    +
    +  // Generic.Subheading
    +  .gu { color: #800080; font-weight: bold; }
    +
    +  // Generic.Traceback
    +  .gt { color: #aa0000 }
    +
    +  // Keyword.Constant
    +  .kc { font-weight: bold }
    +
    +  // Keyword.Declaration
    +  .kd { font-weight: bold }
    +
    +  // Keyword.Namespace
    +  .kn { font-weight: bold }
    +
    +  // Keyword.Pseudo
    +  .kp { font-weight: bold }
    +
    +  // Keyword.Reserved
    +  .kr { font-weight: bold }
    +
    +  // Keyword.Type
    +  .kt { color: #445588; font-weight: bold }
    +
    +  // Literal.Number
    +  .m { color: #009999 }
    +
    +  // Literal.String
    +  .s { color: #d14 }
    +
    +  // Name
    +  .n { color: #333333 }
    +
    +  // Name.Attribute
    +  .na { color: #008080 }
    +
    +  // Name.Builtin
    +  .nb { color: #0086B3 }
    +
    +  // Name.Class
    +  .nc { color: #445588; font-weight: bold }
    +
    +  // Name.Constant
    +  .no { color: #008080 }
    +
    +  // Name.Entity
    +  .ni { color: #800080 }
    +
    +  // Name.Exception
    +  .ne { color: #990000; font-weight: bold }
    +
    +  // Name.Function
    +  .nf { color: #990000; font-weight: bold }
    +
    +  // Name.Namespace
    +  .nn { color: #555555 }
    +
    +  // Name.Tag
    +  .nt { color: #000080 }
    +
    +  // Name.Variable
    +  .nv { color: #008080 }
    +
    +  // Operator.Word
    +  .ow { font-weight: bold }
    +
    +  // Text.Whitespace
    +  .w { color: #bbbbbb }
    +
    +  // Literal.Number.Float
    +  .mf { color: #009999 }
    +
    +  // Literal.Number.Hex
    +  .mh { color: #009999 }
    +
    +  // Literal.Number.Integer
    +  .mi { color: #009999 }
    +
    +  // Literal.Number.Oct
    +  .mo { color: #009999 }
    +
    +  // Literal.String.Backtick
    +  .sb { color: #d14 }
    +
    +  // Literal.String.Char
    +  .sc { color: #d14 }
    +
    +  // Literal.String.Doc
    +  .sd { color: #d14 }
    +
    +  // Literal.String.Double
    +  .s2 { color: #d14 }
    +
    +  // Literal.String.Escape
    +  .se { color: #d14 }
    +
    +  // Literal.String.Heredoc
    +  .sh { color: #d14 }
    +
    +  // Literal.String.Interpol
    +  .si { color: #d14 }
    +
    +  // Literal.String.Other
    +  .sx { color: #d14 }
    +
    +  // Literal.String.Regex
    +  .sr { color: #009926 }
    +
    +  // Literal.String.Single
    +  .s1 { color: #d14 }
    +
    +  // Literal.String.Symbol
    +  .ss { color: #990073 }
    +
    +  // Name.Builtin.Pseudo
    +  .bp { color: #999999 }
    +
    +  // Name.Variable.Class
    +  .vc { color: #008080 }
    +
    +  // Name.Variable.Global
    +  .vg { color: #008080 }
    +
    +  // Name.Variable.Instance
    +  .vi { color: #008080 }
    +
    +  // Literal.Number.Integer.Long
    +  .il { color: #009999 }
    +
    +  .gc {
    +    color: #999;
    +    background-color: #EAF2F5;
    +  }
    +}
    +
    +.type-csharp .highlight {
    +  .k { color: #0000FF }
    +  .kt { color: #0000FF }
    +  .nf { color: #000000; font-weight: normal }
    +  .nc { color: #2B91AF }
    +  .nn { color: #000000 }
    +  .s { color: #A31515 }
    +  .sc { color: #A31515 }
    +}
    
    From 758c9f116f847e7274a52d63eed18bafe4cd64c0 Mon Sep 17 00:00:00 2001
    From: Justin Palmer 
    Date: Mon, 4 Mar 2013 12:05:06 -0800
    Subject: [PATCH 264/308] package-generator.css -> package-generator.less
    
    ---
     .../stylesheets/{package-generator.css => package-generator.less} | 0
     1 file changed, 0 insertions(+), 0 deletions(-)
     rename src/packages/package-generator/stylesheets/{package-generator.css => package-generator.less} (100%)
    
    diff --git a/src/packages/package-generator/stylesheets/package-generator.css b/src/packages/package-generator/stylesheets/package-generator.less
    similarity index 100%
    rename from src/packages/package-generator/stylesheets/package-generator.css
    rename to src/packages/package-generator/stylesheets/package-generator.less
    
    From 7ad67e50a66282ff6635efef41a3abe8709a3e23 Mon Sep 17 00:00:00 2001
    From: Justin Palmer 
    Date: Mon, 4 Mar 2013 12:05:24 -0800
    Subject: [PATCH 265/308] spell-check.css -> spell-check.less
    
    ---
     .../spell-check/stylesheets/{spell-check.css => spell-check.less} | 0
     1 file changed, 0 insertions(+), 0 deletions(-)
     rename src/packages/spell-check/stylesheets/{spell-check.css => spell-check.less} (100%)
    
    diff --git a/src/packages/spell-check/stylesheets/spell-check.css b/src/packages/spell-check/stylesheets/spell-check.less
    similarity index 100%
    rename from src/packages/spell-check/stylesheets/spell-check.css
    rename to src/packages/spell-check/stylesheets/spell-check.less
    
    From 6fd0118d2c92adc6400005cf8e1d07e06209effe Mon Sep 17 00:00:00 2001
    From: Justin Palmer 
    Date: Mon, 4 Mar 2013 12:05:40 -0800
    Subject: [PATCH 266/308] tree-view.css -> tree-view.less
    
    ---
     .../tree-view/stylesheets/tree-view.css       | 57 -------------------
     .../tree-view/stylesheets/tree-view.less      | 57 +++++++++++++++++++
     2 files changed, 57 insertions(+), 57 deletions(-)
     delete mode 100644 src/packages/tree-view/stylesheets/tree-view.css
     create mode 100644 src/packages/tree-view/stylesheets/tree-view.less
    
    diff --git a/src/packages/tree-view/stylesheets/tree-view.css b/src/packages/tree-view/stylesheets/tree-view.css
    deleted file mode 100644
    index 723b85f2e..000000000
    --- a/src/packages/tree-view/stylesheets/tree-view.css
    +++ /dev/null
    @@ -1,57 +0,0 @@
    -.tree-view-wrapper {
    -  position: relative;
    -  height: 100%;
    -  cursor: default;
    -  -webkit-user-select: none;
    -  min-width: 50px;
    -  z-index: 2;
    -}
    -
    -.tree-view {
    -  position: relative;
    -  cursor: default;
    -  -webkit-user-select: none;
    -  overflow: auto;
    -  height: 100%;
    -}
    -
    -.tree-view-wrapper .tree-view-resizer {
    -  position: absolute;
    -  top: 0;
    -  right: 0;
    -  bottom: 0;
    -  width: 10px;
    -  cursor: col-resize;
    -  z-index: 3;
    -}
    -
    -.tree-view .entry {
    -  text-wrap: none;
    -  white-space: nowrap;
    -}
    -
    -.tree-view .entry > .header,
    -.tree-view .entry > .name {
    -  z-index: 1;
    -  position: relative;
    -  display: inline-block;
    -}
    -
    -.tree-view .selected > .highlight {
    -  position: absolute;
    -  left: 0;
    -  right: 0;
    -  height: 24px;
    -}
    -
    -.tree-view .disclosure-arrow {
    -  display: inline-block;
    -}
    -
    -.tree-view-dialog {
    -  position: absolute;
    -  bottom: 0;
    -  left: 0;
    -  right: 0;
    -  z-index: 99;
    -}
    diff --git a/src/packages/tree-view/stylesheets/tree-view.less b/src/packages/tree-view/stylesheets/tree-view.less
    new file mode 100644
    index 000000000..33c9db860
    --- /dev/null
    +++ b/src/packages/tree-view/stylesheets/tree-view.less
    @@ -0,0 +1,57 @@
    +.tree-view-wrapper {
    +  position: relative;
    +  height: 100%;
    +  cursor: default;
    +  -webkit-user-select: none;
    +  min-width: 50px;
    +  z-index: 2;
    +
    +  .tree-view-resizer {
    +    position: absolute;
    +    top: 0;
    +    right: 0;
    +    bottom: 0;
    +    width: 10px;
    +    cursor: col-resize;
    +    z-index: 3;
    +  }
    +}
    +
    +.tree-view {
    +  position: relative;
    +  cursor: default;
    +  -webkit-user-select: none;
    +  overflow: auto;
    +  height: 100%;
    +
    +  .entry {
    +    text-wrap: none;
    +    white-space: nowrap;
    +
    +    & > .header,
    +    > .name {
    +      z-index: 1;
    +      position: relative;
    +      display: inline-block;
    +    }
    +  }
    +
    +  .selected > .highlight {
    +    position: absolute;
    +    left: 0;
    +    right: 0;
    +    height: 24px;
    +  }
    +
    +  .disclosure-arrow {
    +    display: inline-block;
    +  }
    +}
    +
    +.tree-view-dialog {
    +  position: absolute;
    +  bottom: 0;
    +  left: 0;
    +  right: 0;
    +  z-index: 99;
    +}
    
    From e1a9362448c871d29633e502c2256535be474f1f Mon Sep 17 00:00:00 2001
    From: Justin Palmer 
    Date: Mon, 4 Mar 2013 12:05:57 -0800
    Subject: [PATCH 267/308] wrap-guide.css -> wrap-guide.less
    
    ---
     .../wrap-guide/stylesheets/{wrap-guide.css => wrap-guide.less}    | 0
     1 file changed, 0 insertions(+), 0 deletions(-)
     rename src/packages/wrap-guide/stylesheets/{wrap-guide.css => wrap-guide.less} (100%)
    
    diff --git a/src/packages/wrap-guide/stylesheets/wrap-guide.css b/src/packages/wrap-guide/stylesheets/wrap-guide.less
    similarity index 100%
    rename from src/packages/wrap-guide/stylesheets/wrap-guide.css
    rename to src/packages/wrap-guide/stylesheets/wrap-guide.less
    
    From b9604d1baa2794697a52d09ffa0befc0dcd5adb5 Mon Sep 17 00:00:00 2001
    From: Justin Palmer 
    Date: Tue, 5 Mar 2013 13:24:22 -0800
    Subject: [PATCH 268/308] reset.css -> reset.less
    
    ---
     src/app/window.coffee            | 2 +-
     static/{reset.css => reset.less} | 0
     2 files changed, 1 insertion(+), 1 deletion(-)
     rename static/{reset.css => reset.less} (100%)
    
    diff --git a/src/app/window.coffee b/src/app/window.coffee
    index 14bb504f0..3cab24751 100644
    --- a/src/app/window.coffee
    +++ b/src/app/window.coffee
    @@ -24,7 +24,7 @@ window.setUpEnvironment = ->
       $(document).on 'keydown', keymap.handleKeyEvent
       keymap.bindDefaultKeys()
     
    -  requireStylesheet 'reset.css'
    +  requireStylesheet 'reset.less'
       requireStylesheet 'atom.css'
       requireStylesheet 'tabs.css'
       requireStylesheet 'tree-view.css'
    diff --git a/static/reset.css b/static/reset.less
    similarity index 100%
    rename from static/reset.css
    rename to static/reset.less
    
    From 2b66b033e03871888665fef540a1a8d0462cd447 Mon Sep 17 00:00:00 2001
    From: Justin Palmer 
    Date: Tue, 5 Mar 2013 13:37:41 -0800
    Subject: [PATCH 269/308] add less parsing helper to spec helper
    
    ---
     spec/spec-helper.coffee | 8 ++++++++
     1 file changed, 8 insertions(+)
    
    diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee
    index 4632db296..c03a9b6ed 100644
    --- a/spec/spec-helper.coffee
    +++ b/spec/spec-helper.coffee
    @@ -4,6 +4,7 @@ window.setUpEnvironment()
     nakedLoad 'jasmine-jquery'
     $ = require 'jquery'
     _ = require 'underscore'
    +{less} = require 'less'
     Keymap = require 'keymap'
     Config = require 'config'
     Point = require 'point'
    @@ -213,6 +214,13 @@ window.setEditorHeightInLines = (editor, heightInChars, charHeight=editor.lineHe
       editor.height(charHeight * heightInChars + editor.renderedLines.position().top)
       $(window).trigger 'resize' # update editor's on-screen lines
     
    +window.parseLessFile = (path) ->
    +  content = ""
    +  (new less.Parser).parse __read(path), (e, tree) ->
    +    throw new Error(e.message, file, e.line) if e
    +    content = tree.toCSS()
    +  content
    +
     $.fn.resultOfTrigger = (type) ->
       event = $.Event(type)
       this.trigger(event)
    
    From a448a79ae67ca732496b400c1329e04e39bd4677 Mon Sep 17 00:00:00 2001
    From: Justin Palmer 
    Date: Tue, 5 Mar 2013 13:37:56 -0800
    Subject: [PATCH 270/308] atom.css -> atom.less
    
    ---
     spec/app/window-spec.coffee | 17 ++++----
     src/app/window.coffee       |  2 +-
     static/atom.css             | 81 -------------------------------------
     static/atom.less            | 75 ++++++++++++++++++++++++++++++++++
     4 files changed, 83 insertions(+), 92 deletions(-)
     delete mode 100644 static/atom.css
     create mode 100644 static/atom.less
    
    diff --git a/spec/app/window-spec.coffee b/spec/app/window-spec.coffee
    index 0ae136754..dac3771aa 100644
    --- a/spec/app/window-spec.coffee
    +++ b/spec/app/window-spec.coffee
    @@ -64,19 +64,19 @@ describe "Window", ->
     
       describe "requireStylesheet(path)", ->
         it "synchronously loads the stylesheet at the given path and installs a style tag for it in the head", ->
    -      $('head style[id*="atom.css"]').remove()
    +      $('head style[id*="atom.less"]').remove()
           lengthBefore = $('head style').length
    -      requireStylesheet('atom.css')
    +      requireStylesheet('atom.less')
           expect($('head style').length).toBe lengthBefore + 1
     
    -      styleElt = $('head style[id*="atom.css"]')
    +      styleElt = $('head style[id*="atom.less"]')
     
    -      fullPath = require.resolve('atom.css')
    +      fullPath = require.resolve('atom.less')
           expect(styleElt.attr('id')).toBe fullPath
    -      expect(styleElt.text()).toBe fs.read(fullPath)
    +      expect(styleElt.text()).toBe parseLessFile(fullPath)
     
           # doesn't append twice
    -      requireStylesheet('atom.css')
    +      requireStylesheet('atom.less')
           expect($('head style').length).toBe lengthBefore + 1
     
         it  "synchronously loads and parses less files at the given path and installs a style tag for it in the head", ->
    @@ -89,10 +89,7 @@ describe "Window", ->
     
           fullPath = require.resolve('markdown.less')
           expect(styleElt.attr('id')).toBe fullPath
    -
    -      (new less.Parser).parse __read(fullPath), (e, tree) ->
    -        throw new Error(e.message, file, e.line) if e
    -        expect(styleElt.text()).toBe tree.toCSS()
    +      expect(styleElt.text()).toBe parseLessFile(fullPath)
     
           # doesn't append twice
           requireStylesheet('markdown.less')
    diff --git a/src/app/window.coffee b/src/app/window.coffee
    index 3cab24751..4a548b5bd 100644
    --- a/src/app/window.coffee
    +++ b/src/app/window.coffee
    @@ -25,7 +25,7 @@ window.setUpEnvironment = ->
       keymap.bindDefaultKeys()
     
       requireStylesheet 'reset.less'
    -  requireStylesheet 'atom.css'
    +  requireStylesheet 'atom.less'
       requireStylesheet 'tabs.css'
       requireStylesheet 'tree-view.css'
       requireStylesheet 'status-bar.css'
    diff --git a/static/atom.css b/static/atom.css
    deleted file mode 100644
    index 6cfb79269..000000000
    --- a/static/atom.css
    +++ /dev/null
    @@ -1,81 +0,0 @@
    -html, body {
    -  width: 100%;
    -  height: 100%;
    -  overflow: hidden;
    -}
    -
    -#root-view {
    -  height: 100%;
    -  overflow: hidden;
    -  position: relative;
    -}
    -
    -#root-view #horizontal {
    -  display: -webkit-flex;
    -  height: 100%;
    -}
    -
    -#root-view #vertical {
    -  display: -webkit-flex;
    -  -webkit-flex: 1;
    -  -webkit-flex-flow: column;
    -}
    -
    -#panes {
    -  position: relative;
    -  -webkit-flex: 1;
    -}
    -
    -#panes .column {
    -  position: absolute;
    -  top: 0;
    -  bottom: 0;
    -  left: 0;
    -  right: 0;
    -  overflow-y: hidden;
    -}
    -
    -#panes .row {
    -  position: absolute;
    -  top: 0;
    -  bottom: 0;
    -  left: 0;
    -  right: 0;
    -  overflow-x: hidden;
    -}
    -
    -#panes .pane {
    -  position: absolute;
    -  display: -webkit-flex;
    -  -webkit-flex-flow: column;
    -  top: 0;
    -  bottom: 0;
    -  left: 0;
    -  right: 0;
    -  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");
    -  font-weight: normal;
    -  font-style: normal;
    -}
    -
    -.is-loading {
    -  background-image: url(images/spinner.svg);
    -  background-repeat: no-repeat;
    -  width: 14px;
    -  height: 14px;
    -  opacity: 0.5;
    -  background-size: contain;
    -  position: relative;
    -  display: inline-block;
    -  padding-left: 19px;
    -}
    diff --git a/static/atom.less b/static/atom.less
    new file mode 100644
    index 000000000..c02f90d90
    --- /dev/null
    +++ b/static/atom.less
    @@ -0,0 +1,75 @@
    +html, body {
    +  width: 100%;
    +  height: 100%;
    +  overflow: hidden;
    +}
    +
    +#root-view {
    +  height: 100%;
    +  overflow: hidden;
    +  position: relative;
    +
    +  #horizontal {
    +    display: -webkit-flex;
    +    height: 100%;
    +  }
    +
    +  #vertical {
    +    display: -webkit-flex;
    +    -webkit-flex: 1;
    +    -webkit-flex-flow: column;
    +  }
    +
    +  #panes {
    +    position: relative;
    +    -webkit-flex: 1;
    +
    +    .column {
    +      position: absolute;
    +      top: 0;
    +      bottom: 0;
    +      left: 0;
    +      right: 0;
    +      overflow-y: hidden;
    +    }
    +
    +    .row {
    +      position: absolute;
    +      top: 0;
    +      bottom: 0;
    +      left: 0;
    +      right: 0;
    +      overflow-x: hidden;
    +    }
    +
    +    .pane {
    +      position: absolute;
    +      display: -webkit-flex;
    +      -webkit-flex-flow: column;
    +      top: 0;
    +      bottom: 0;
    +      left: 0;
    +      right: 0;
    +      box-sizing: border-box;
    +    }
    +  }
    +}
    +
    +@font-face {
    +  font-family: 'Octicons Regular';
    +  src: url("octicons-regular-webfont.woff") format("woff");
    +  font-weight: normal;
    +  font-style: normal;
    +}
    +
    +.is-loading {
    +  background-image: url(images/spinner.svg);
    +  background-repeat: no-repeat;
    +  width: 14px;
    +  height: 14px;
    +  opacity: 0.5;
    +  background-size: contain;
    +  position: relative;
    +  display: inline-block;
    +  padding-left: 19px;
    +}
    
    From 392b9cfeabc1689c52a986127d8738b5be983f8a Mon Sep 17 00:00:00 2001
    From: Justin Palmer 
    Date: Tue, 5 Mar 2013 13:47:35 -0800
    Subject: [PATCH 271/308] command-panel.css -> command-panel.less
    
    ---
     src/app/window.coffee     |   2 +-
     static/command-panel.css  | 134 --------------------------------------
     static/command-panel.less | 131 +++++++++++++++++++++++++++++++++++++
     3 files changed, 132 insertions(+), 135 deletions(-)
     delete mode 100644 static/command-panel.css
     create mode 100644 static/command-panel.less
    
    diff --git a/src/app/window.coffee b/src/app/window.coffee
    index 4a548b5bd..6ab360ad0 100644
    --- a/src/app/window.coffee
    +++ b/src/app/window.coffee
    @@ -29,7 +29,7 @@ window.setUpEnvironment = ->
       requireStylesheet 'tabs.css'
       requireStylesheet 'tree-view.css'
       requireStylesheet 'status-bar.css'
    -  requireStylesheet 'command-panel.css'
    +  requireStylesheet 'command-panel.less'
       requireStylesheet 'fuzzy-finder.css'
       requireStylesheet 'overlay.css'
       requireStylesheet 'popover-list.css'
    diff --git a/static/command-panel.css b/static/command-panel.css
    deleted file mode 100644
    index 8f2098259..000000000
    --- a/static/command-panel.css
    +++ /dev/null
    @@ -1,134 +0,0 @@
    -.command-panel {
    -  position: relative;
    -  padding: 0;
    -}
    -
    -.command-panel .is-loading {
    -  display: block;
    -  margin: 0 auto 10px auto;
    -  width: 100px;
    -  background-color: #111;
    -  background-size: auto;
    -  background-position: 5px 5px;
    -  padding: 5px 5px 10px 30px;
    -  border-radius: 3px;
    -  border: 1px solid rgba(255, 255, 255,  0.1);
    -  border-top: 1px solid rgba(0, 0, 0, 1);
    -  border-left: 1px solid rgba(0, 0, 0, 1);
    -}
    -
    -.command-panel .preview-count {
    -  display: inline-block;
    -  margin-top: 4px;
    -  font-size: 11px;
    -}
    -
    -.command-panel .preview-list {
    -  max-height: 300px;
    -  overflow: auto;
    -  margin: 0 0 10px 0;
    -  position: relative;
    -  cursor: default;
    -}
    -
    -.command-panel .header:after {
    -  content: ".";
    -  display: block;
    -  visibility: hidden;
    -  clear: both;
    -  height: 0;
    -}
    -
    -.command-panel .expand-collapse {
    -  float: right;
    -}
    -
    -.command-panel .expand-collapse li {
    -  display: inline-block;
    -  cursor: pointer;
    -  font-size: 11px;
    -  margin-left: 5px;
    -  padding: 5px 10px;
    -  border-radius: 3px;
    -}
    -
    -.command-panel .preview-count,
    -.command-panel .expand-collapse {
    -  -webkit-user-select: none;
    -}
    -
    -.command-panel .preview-list .path {
    -  position: relative;
    -  -webkit-user-select: none;
    -}
    -
    -.command-panel .preview-list .path-details:before {
    -  font-family: 'Octicons Regular';
    -  font-size: 12px;
    -  width: 12px;
    -  height: 12px;
    -  margin-right: 5px;
    -  margin-left: 5px;
    -  -webkit-font-smoothing: antialiased;
    -  content: "\f05b";
    -  position: relative;
    -  top: 0;
    -}
    -
    -.command-panel .preview-list .is-collapsed .path-details:before {
    -  content: "\f05a";
    -}
    -
    -.command-panel .preview-list .path-name:before {
    -  font-family: 'Octicons Regular';
    -  font-size: 16px;
    -  width: 16px;
    -  height: 16px;
    -  margin-right: 5px;
    -  -webkit-font-smoothing: antialiased;
    -  content: "\f011";
    -  position: relative;
    -  top: 1px;
    -}
    -
    -.command-panel .preview-list .path.readme .path-name:before {
    -  content: "\f007";
    -}
    -
    -.command-panel .preview-list .operation {
    -  padding-top: 2px;
    -  padding-bottom: 2px;
    -  padding-left: 10px;
    -}
    -
    -.command-panel .preview-list .line-number {
    -  margin-right: 1ex;
    -  text-align: right;
    -  display: inline-block;
    -}
    -
    -.command-panel .preview-list .path-match-number {
    -  padding-left: 8px;
    -}
    -
    -.command-panel .preview-list .preview {
    -  word-break: break-all;
    -}
    -
    -.command-panel .preview-list .preview .match {
    -  -webkit-border-radius: 2px;
    -  padding: 1px;
    -}
    -
    -.command-panel .prompt-and-editor .editor {
    -  position: relative;
    -}
    -
    -.command-panel .prompt-and-editor {
    -  display: -webkit-flex;
    -}
    -
    -.error-messages {
    -  padding: 5px 1em;
    -  color: white;
    -}
    diff --git a/static/command-panel.less b/static/command-panel.less
    new file mode 100644
    index 000000000..18fa88fab
    --- /dev/null
    +++ b/static/command-panel.less
    @@ -0,0 +1,131 @@
    +.command-panel {
    +  position: relative;
    +  padding: 0;
    +
    +  .is-loading {
    +    display: block;
    +    margin: 0 auto 10px auto;
    +    width: 100px;
    +    background-color: #111111;
    +    background-size: auto;
    +    background-position: 5px 5px;
    +    padding: 5px 5px 10px 30px;
    +    border-radius: 3px;
    +    border: 1px solid rgba(255,255,255,0.1);
    +    border-top: 1px solid rgba(0,0,0,1);
    +    border-left: 1px solid rgba(0,0,0,1);
    +  }
    +
    +  .preview-count {
    +    display: inline-block;
    +    margin-top: 4px;
    +    font-size: 11px;
    +    -webkit-user-select: none;
    +  }
    +
    +  .preview-list {
    +    max-height: 300px;
    +    overflow: auto;
    +    margin: 0 0 10px 0;
    +    position: relative;
    +    cursor: default;
    +
    +    .path {
    +      position: relative;
    +      -webkit-user-select: none;
    +    }
    +
    +    .path-details:before {
    +      font-family: 'Octicons Regular';
    +      font-size: 12px;
    +      width: 12px;
    +      height: 12px;
    +      margin-right: 5px;
    +      margin-left: 5px;
    +      -webkit-font-smoothing: antialiased;
    +      content: "\f05b";
    +      position: relative;
    +      top: 0;
    +    }
    +
    +    .is-collapsed .path-details:before {
    +      content: "\f05a";
    +    }
    +
    +    .path-name:before {
    +      font-family: 'Octicons Regular';
    +      font-size: 16px;
    +      width: 16px;
    +      height: 16px;
    +      margin-right: 5px;
    +      -webkit-font-smoothing: antialiased;
    +      content: "\f011";
    +      position: relative;
    +      top: 1px;
    +    }
    +
    +    .path.readme .path-name:before {
    +      content: "\f007";
    +    }
    +
    +    .operation {
    +      padding-top: 2px;
    +      padding-bottom: 2px;
    +      padding-left: 10px;
    +    }
    +
    +    .line-number {
    +      margin-right: 1ex;
    +      text-align: right;
    +      display: inline-block;
    +    }
    +
    +    .path-match-number {
    +      padding-left: 8px;
    +    }
    +
    +    .preview {
    +      word-break: break-all;
    +
    +      .match {
    +        -webkit-border-radius: 2px;
    +        padding: 1px;
    +      }
    +    }
    +  }
    +
    +  .header:after {
    +    content: ".";
    +    display: block;
    +    visibility: hidden;
    +    clear: both;
    +    height: 0;
    +  }
    +
    +  .expand-collapse {
    +    float: right;
    +    -webkit-user-select: none;
    +
    +    li {
    +      display: inline-block;
    +      cursor: pointer;
    +      font-size: 11px;
    +      margin-left: 5px;
    +      padding: 5px 10px;
    +      border-radius: 3px;
    +    }
    +  }
    +
    +  .prompt-and-editor {
    +    display: -webkit-flex;
    +
    +    .editor {
    +      position: relative;
    +    }
    +  }
    +}
    +
    +.error-messages {
    +  padding: 5px 1em;
    +  color: white;
    +}
    
    From 0624ba6d3fc3929c03ed07846d45af3fc72626c6 Mon Sep 17 00:00:00 2001
    From: Justin Palmer 
    Date: Tue, 5 Mar 2013 15:10:52 -0800
    Subject: [PATCH 272/308] rename remaining static css files to less
    
    ---
     benchmark/benchmark-helper.coffee              |  3 +--
     spec/spec-helper.coffee                        |  2 +-
     src/app/editor.coffee                          |  2 +-
     src/app/select-list.coffee                     |  2 +-
     src/app/window.coffee                          | 14 +++++++-------
     static/{editor.css => editor.less}             |  0
     static/{fuzzy-finder.css => fuzzy-finder.less} |  0
     static/{jasmine.css => jasmine.less}           |  0
     static/{notification.css => notification.less} |  0
     static/{overlay.css => overlay.less}           |  0
     static/{popover-list.css => popover-list.less} |  0
     static/{select-list.css => select-list.less}   |  0
     static/{status-bar.css => status-bar.less}     |  0
     static/{tabs.css => tabs.less}                 |  0
     static/{tree-view.css => tree-view.less}       |  0
     15 files changed, 11 insertions(+), 12 deletions(-)
     rename static/{editor.css => editor.less} (100%)
     rename static/{fuzzy-finder.css => fuzzy-finder.less} (100%)
     rename static/{jasmine.css => jasmine.less} (100%)
     rename static/{notification.css => notification.less} (100%)
     rename static/{overlay.css => overlay.less} (100%)
     rename static/{popover-list.css => popover-list.less} (100%)
     rename static/{select-list.css => select-list.less} (100%)
     rename static/{status-bar.css => status-bar.less} (100%)
     rename static/{tabs.css => tabs.less} (100%)
     rename static/{tree-view.css => tree-view.less} (100%)
    
    diff --git a/benchmark/benchmark-helper.coffee b/benchmark/benchmark-helper.coffee
    index dff4cee35..b9ec9e3aa 100644
    --- a/benchmark/benchmark-helper.coffee
    +++ b/benchmark/benchmark-helper.coffee
    @@ -7,7 +7,7 @@ Config = require 'config'
     Project = require 'project'
     
     require 'window'
    -requireStylesheet "jasmine.css"
    +requireStylesheet "jasmine.less"
     
     # Load TextMate bundles, which specs rely on (but not other packages)
     atom.loadTextMatePackages()
    @@ -127,4 +127,3 @@ $.fn.textInput = (data) ->
       event = document.createEvent 'TextEvent'
       event.initTextEvent('textInput', true, true, window, data)
       this.each -> this.dispatchEvent(event)
    -
    diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee
    index c03a9b6ed..c5c846112 100644
    --- a/spec/spec-helper.coffee
    +++ b/spec/spec-helper.coffee
    @@ -16,7 +16,7 @@ TokenizedBuffer = require 'tokenized-buffer'
     fs = require 'fs'
     RootView = require 'root-view'
     Git = require 'git'
    -requireStylesheet "jasmine.css"
    +requireStylesheet "jasmine.less"
     fixturePackagesPath = require.resolve('fixtures/packages')
     require.paths.unshift(fixturePackagesPath)
     keymap.loadBundledKeymaps()
    diff --git a/src/app/editor.coffee b/src/app/editor.coffee
    index 1e2c0b464..a8380d7af 100644
    --- a/src/app/editor.coffee
    +++ b/src/app/editor.coffee
    @@ -61,7 +61,7 @@ class Editor extends View
         else
           {editSession, @mini} = (editSessionOrOptions ? {})
     
    -    requireStylesheet 'editor.css'
    +    requireStylesheet 'editor.less'
     
         @id = Editor.nextEditorId++
         @lineCache = []
    diff --git a/src/app/select-list.coffee b/src/app/select-list.coffee
    index 297c3fa35..e2441deff 100644
    --- a/src/app/select-list.coffee
    +++ b/src/app/select-list.coffee
    @@ -20,7 +20,7 @@ class SelectList extends View
       cancelling: false
     
       initialize: ->
    -    requireStylesheet 'select-list.css'
    +    requireStylesheet 'select-list.less'
     
         @miniEditor.getBuffer().on 'changed', => @schedulePopulateList()
         @miniEditor.on 'focusout', => @cancel() unless @cancelling
    diff --git a/src/app/window.coffee b/src/app/window.coffee
    index 6ab360ad0..5b1614085 100644
    --- a/src/app/window.coffee
    +++ b/src/app/window.coffee
    @@ -26,14 +26,14 @@ window.setUpEnvironment = ->
     
       requireStylesheet 'reset.less'
       requireStylesheet 'atom.less'
    -  requireStylesheet 'tabs.css'
    -  requireStylesheet 'tree-view.css'
    -  requireStylesheet 'status-bar.css'
    +  requireStylesheet 'tabs.less'
    +  requireStylesheet 'tree-view.less'
    +  requireStylesheet 'status-bar.less'
       requireStylesheet 'command-panel.less'
    -  requireStylesheet 'fuzzy-finder.css'
    -  requireStylesheet 'overlay.css'
    -  requireStylesheet 'popover-list.css'
    -  requireStylesheet 'notification.css'
    +  requireStylesheet 'fuzzy-finder.less'
    +  requireStylesheet 'overlay.less'
    +  requireStylesheet 'popover-list.less'
    +  requireStylesheet 'notification.less'
       requireStylesheet 'markdown.less'
     
       if nativeStylesheetPath = require.resolve("#{platform}.css")
    diff --git a/static/editor.css b/static/editor.less
    similarity index 100%
    rename from static/editor.css
    rename to static/editor.less
    diff --git a/static/fuzzy-finder.css b/static/fuzzy-finder.less
    similarity index 100%
    rename from static/fuzzy-finder.css
    rename to static/fuzzy-finder.less
    diff --git a/static/jasmine.css b/static/jasmine.less
    similarity index 100%
    rename from static/jasmine.css
    rename to static/jasmine.less
    diff --git a/static/notification.css b/static/notification.less
    similarity index 100%
    rename from static/notification.css
    rename to static/notification.less
    diff --git a/static/overlay.css b/static/overlay.less
    similarity index 100%
    rename from static/overlay.css
    rename to static/overlay.less
    diff --git a/static/popover-list.css b/static/popover-list.less
    similarity index 100%
    rename from static/popover-list.css
    rename to static/popover-list.less
    diff --git a/static/select-list.css b/static/select-list.less
    similarity index 100%
    rename from static/select-list.css
    rename to static/select-list.less
    diff --git a/static/status-bar.css b/static/status-bar.less
    similarity index 100%
    rename from static/status-bar.css
    rename to static/status-bar.less
    diff --git a/static/tabs.css b/static/tabs.less
    similarity index 100%
    rename from static/tabs.css
    rename to static/tabs.less
    diff --git a/static/tree-view.css b/static/tree-view.less
    similarity index 100%
    rename from static/tree-view.css
    rename to static/tree-view.less
    
    From 050c376e87c45d80635e5c27783b3da3d33e3fc4 Mon Sep 17 00:00:00 2001
    From: Justin Palmer 
    Date: Tue, 5 Mar 2013 15:23:21 -0800
    Subject: [PATCH 273/308] remove old less require spec
    
    ---
     spec/stdlib/require-spec.coffee | 19 -------------------
     1 file changed, 19 deletions(-)
     delete mode 100644 spec/stdlib/require-spec.coffee
    
    diff --git a/spec/stdlib/require-spec.coffee b/spec/stdlib/require-spec.coffee
    deleted file mode 100644
    index a4b5004a4..000000000
    --- a/spec/stdlib/require-spec.coffee
    +++ /dev/null
    @@ -1,19 +0,0 @@
    -{less} = require('less')
    -
    -describe "require", ->
    -  describe "files with a `.less` extension", ->
    -    it "parses valid files into css", ->
    -      output = require(project.resolve("sample.less"))
    -      expect(output).toBe """
    -        #header {
    -          color: #4d926f;
    -        }
    -        h2 {
    -          color: #4d926f;
    -        }
    -
    -      """
    -
    -    it "throws an error when parsing invalid file", ->
    -      functionWithError = (-> require(project.resolve("sample-with-error.less")))
    -      expect(functionWithError).toThrow()
    \ No newline at end of file
    
    From 9acd401b9ec4a2975f959870973e4fd4b7b8556c Mon Sep 17 00:00:00 2001
    From: probablycorey 
    Date: Tue, 5 Mar 2013 17:03:28 -0800
    Subject: [PATCH 274/308] Use fixture files to requireStylesheet
    
    ---
     spec/app/window-spec.coffee | 40 ++++++++++++++++++++++---------------
     spec/spec-helper.coffee     |  8 --------
     2 files changed, 24 insertions(+), 24 deletions(-)
    
    diff --git a/spec/app/window-spec.coffee b/spec/app/window-spec.coffee
    index dac3771aa..5687cb3d4 100644
    --- a/spec/app/window-spec.coffee
    +++ b/spec/app/window-spec.coffee
    @@ -63,37 +63,45 @@ describe "Window", ->
           expect(atom.confirm).toHaveBeenCalled()
     
       describe "requireStylesheet(path)", ->
    -    it "synchronously loads the stylesheet at the given path and installs a style tag for it in the head", ->
    -      $('head style[id*="atom.less"]').remove()
    +    it "synchronously loads css at the given path and installs a style tag for it in the head", ->
    +      cssPath = project.resolve('css.css')
           lengthBefore = $('head style').length
    -      requireStylesheet('atom.less')
    +
    +      requireStylesheet(cssPath)
           expect($('head style').length).toBe lengthBefore + 1
     
    -      styleElt = $('head style[id*="atom.less"]')
    -
    -      fullPath = require.resolve('atom.less')
    -      expect(styleElt.attr('id')).toBe fullPath
    -      expect(styleElt.text()).toBe parseLessFile(fullPath)
    +      element = $('head style[id*="css.css"]')
    +      expect(element.attr('id')).toBe cssPath
    +      expect(element.text()).toBe fs.read(cssPath)
     
           # doesn't append twice
    -      requireStylesheet('atom.less')
    +      requireStylesheet(cssPath)
           expect($('head style').length).toBe lengthBefore + 1
     
    +      $('head style[id*="css.css"]').remove()
    +
         it  "synchronously loads and parses less files at the given path and installs a style tag for it in the head", ->
    -      $('head style[id*="markdown.less"]').remove()
    +      lessPath = project.resolve('sample.less')
           lengthBefore = $('head style').length
    -      requireStylesheet('markdown.less')
    +      requireStylesheet(lessPath)
           expect($('head style').length).toBe lengthBefore + 1
     
    -      styleElt = $('head style[id*="markdown.less"]')
    +      element = $('head style[id*="sample.less"]')
    +      expect(element.attr('id')).toBe lessPath
    +      expect(element.text()).toBe """
    +      #header {
    +        color: #4d926f;
    +      }
    +      h2 {
    +        color: #4d926f;
    +      }
     
    -      fullPath = require.resolve('markdown.less')
    -      expect(styleElt.attr('id')).toBe fullPath
    -      expect(styleElt.text()).toBe parseLessFile(fullPath)
    +      """
     
           # doesn't append twice
    -      requireStylesheet('markdown.less')
    +      requireStylesheet(lessPath)
           expect($('head style').length).toBe lengthBefore + 1
    +      $('head style[id*="sample.less"]').remove()
     
       describe ".disableStyleSheet(path)", ->
         it "removes styling applied by given stylesheet path", ->
    diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee
    index c5c846112..e220facdc 100644
    --- a/spec/spec-helper.coffee
    +++ b/spec/spec-helper.coffee
    @@ -4,7 +4,6 @@ window.setUpEnvironment()
     nakedLoad 'jasmine-jquery'
     $ = require 'jquery'
     _ = require 'underscore'
    -{less} = require 'less'
     Keymap = require 'keymap'
     Config = require 'config'
     Point = require 'point'
    @@ -214,13 +213,6 @@ window.setEditorHeightInLines = (editor, heightInChars, charHeight=editor.lineHe
       editor.height(charHeight * heightInChars + editor.renderedLines.position().top)
       $(window).trigger 'resize' # update editor's on-screen lines
     
    -window.parseLessFile = (path) ->
    -  content = ""
    -  (new less.Parser).parse __read(path), (e, tree) ->
    -    throw new Error(e.message, file, e.line) if e
    -    content = tree.toCSS()
    -  content
    -
     $.fn.resultOfTrigger = (type) ->
       event = $.Event(type)
       this.trigger(event)
    
    From 478a376c97b7a5d3b1bc300ac0d972d5a8d446a7 Mon Sep 17 00:00:00 2001
    From: Corey Johnson & Nathan Sobo 
    Date: Fri, 8 Mar 2013 16:27:14 -0800
    Subject: [PATCH 275/308] Less files work in atom themes
    
    ---
     spec/app/theme-spec.coffee                    | 25 +++++++++++--------
     spec/fixtures/themes/theme-stylesheet.less    |  5 ++++
     .../theme-with-package-file/package.json      |  2 +-
     .../themes/theme-with-package-file/second.css |  5 ----
     .../theme-with-package-file/second.less       |  7 ++++++
     .../themes/theme-without-package-file/c.css   |  3 ---
     .../themes/theme-without-package-file/c.less  |  5 ++++
     src/app/atom-theme.coffee                     |  6 ++---
     src/app/theme.coffee                          |  2 +-
     src/app/window.coffee                         | 21 +++++++++-------
     10 files changed, 49 insertions(+), 32 deletions(-)
     create mode 100644 spec/fixtures/themes/theme-stylesheet.less
     delete mode 100644 spec/fixtures/themes/theme-with-package-file/second.css
     create mode 100644 spec/fixtures/themes/theme-with-package-file/second.less
     delete mode 100644 spec/fixtures/themes/theme-without-package-file/c.css
     create mode 100644 spec/fixtures/themes/theme-without-package-file/c.less
    
    diff --git a/spec/app/theme-spec.coffee b/spec/app/theme-spec.coffee
    index 518482292..53349c8ba 100644
    --- a/spec/app/theme-spec.coffee
    +++ b/spec/app/theme-spec.coffee
    @@ -20,8 +20,21 @@ describe "@load(name)", ->
           expect($(".editor").css("background-color")).toBe("rgb(20, 20, 20)")
     
       describe "AtomTheme", ->
    +    describe "when the theme is a file", ->
    +      it "loads and applies css", ->
    +        expect($(".editor").css("padding-bottom")).not.toBe "1234px"
    +        themePath = project.resolve('themes/theme-stylesheet.css')
    +        theme = Theme.load(themePath)
    +        expect($(".editor").css("padding-top")).toBe "1234px"
    +
    +      it "parses, loads and applies less", ->
    +        expect($(".editor").css("padding-bottom")).not.toBe "1234px"
    +        themePath = project.resolve('themes/theme-stylesheet.less')
    +        theme = Theme.load(themePath)
    +        expect($(".editor").css("padding-top")).toBe "4321px"
    +
         describe "when the theme contains a package.json file", ->
    -      it "loads and applies css from package.json in the correct order", ->
    +      it "loads and applies stylesheets from package.json in the correct order", ->
             expect($(".editor").css("padding-top")).not.toBe("101px")
             expect($(".editor").css("padding-right")).not.toBe("102px")
             expect($(".editor").css("padding-bottom")).not.toBe("103px")
    @@ -32,16 +45,8 @@ describe "@load(name)", ->
             expect($(".editor").css("padding-right")).toBe("102px")
             expect($(".editor").css("padding-bottom")).toBe("103px")
     
    -    describe "when the theme is a CSS file", ->
    -      it "loads and applies the stylesheet", ->
    -        expect($(".editor").css("padding-bottom")).not.toBe "1234px"
    -
    -        themePath = project.resolve('themes/theme-stylesheet.css')
    -        theme = Theme.load(themePath)
    -        expect($(".editor").css("padding-top")).toBe "1234px"
    -
         describe "when the theme does not contain a package.json file and is a directory", ->
    -      it "loads all CSS files in the directory", ->
    +      it "loads all stylesheet files in the directory", ->
             expect($(".editor").css("padding-top")).not.toBe "10px"
             expect($(".editor").css("padding-right")).not.toBe "20px"
             expect($(".editor").css("padding-bottom")).not.toBe "30px"
    diff --git a/spec/fixtures/themes/theme-stylesheet.less b/spec/fixtures/themes/theme-stylesheet.less
    new file mode 100644
    index 000000000..29e0d80c6
    --- /dev/null
    +++ b/spec/fixtures/themes/theme-stylesheet.less
    @@ -0,0 +1,5 @@
    +@padding: 4321px;
    +
    +.editor {
    +  padding-top: @padding;
    +}
    diff --git a/spec/fixtures/themes/theme-with-package-file/package.json b/spec/fixtures/themes/theme-with-package-file/package.json
    index 9add36774..9dc6565c6 100644
    --- a/spec/fixtures/themes/theme-with-package-file/package.json
    +++ b/spec/fixtures/themes/theme-with-package-file/package.json
    @@ -1,3 +1,3 @@
     {
    -  "stylesheets": ["first.css", "second.css", "last.css"]
    +  "stylesheets": ["first.css", "second.less", "last.css"]
     }
    \ No newline at end of file
    diff --git a/spec/fixtures/themes/theme-with-package-file/second.css b/spec/fixtures/themes/theme-with-package-file/second.css
    deleted file mode 100644
    index 3ddf03add..000000000
    --- a/spec/fixtures/themes/theme-with-package-file/second.css
    +++ /dev/null
    @@ -1,5 +0,0 @@
    -.editor {
    -/*  padding-top: 102px;*/
    -  padding-right: 102px;
    -  padding-bottom: 102px;
    -}
    \ No newline at end of file
    diff --git a/spec/fixtures/themes/theme-with-package-file/second.less b/spec/fixtures/themes/theme-with-package-file/second.less
    new file mode 100644
    index 000000000..71fad0d44
    --- /dev/null
    +++ b/spec/fixtures/themes/theme-with-package-file/second.less
    @@ -0,0 +1,7 @@
    +@number: 102px;
    +
    +.editor {
    +/*  padding-top: 102px;*/
    +  padding-right: @number;
    +  padding-bottom: @number;
    +}
    \ No newline at end of file
    diff --git a/spec/fixtures/themes/theme-without-package-file/c.css b/spec/fixtures/themes/theme-without-package-file/c.css
    deleted file mode 100644
    index 017dea2af..000000000
    --- a/spec/fixtures/themes/theme-without-package-file/c.css
    +++ /dev/null
    @@ -1,3 +0,0 @@
    -.editor {
    -  padding-bottom: 30px;
    -}
    diff --git a/spec/fixtures/themes/theme-without-package-file/c.less b/spec/fixtures/themes/theme-without-package-file/c.less
    new file mode 100644
    index 000000000..91b80c92f
    --- /dev/null
    +++ b/spec/fixtures/themes/theme-without-package-file/c.less
    @@ -0,0 +1,5 @@
    +@number: 30px;
    +
    +.editor {
    +  padding-bottom: @number;
    +}
    diff --git a/src/app/atom-theme.coffee b/src/app/atom-theme.coffee
    index 6a3e1e379..04563b3c7 100644
    --- a/src/app/atom-theme.coffee
    +++ b/src/app/atom-theme.coffee
    @@ -5,10 +5,10 @@ module.exports =
     class AtomTheme extends Theme
     
       loadStylesheet: (stylesheetPath)->
    -    @stylesheets[stylesheetPath] = fs.read(stylesheetPath)
    +    @stylesheets[stylesheetPath] = window.loadStylesheet(stylesheetPath)
     
       load: ->
    -    if fs.extension(@path) is '.css'
    +    if fs.extension(@path) in ['.css', '.less']
           @loadStylesheet(@path)
         else
           metadataPath = fs.resolveExtension(fs.join(@path, 'package'), ['cson', 'json'])
    @@ -17,6 +17,6 @@ class AtomTheme extends Theme
             if stylesheetNames
               @loadStylesheet(fs.join(@path, name)) for name in stylesheetNames
           else
    -        @loadStylesheet(stylesheetPath) for stylesheetPath in fs.list(@path, ['.css'])
    +        @loadStylesheet(stylesheetPath) for stylesheetPath in fs.list(@path, ['.css', '.less'])
     
         super
    diff --git a/src/app/theme.coffee b/src/app/theme.coffee
    index b16a7ef1c..ba35e23e1 100644
    --- a/src/app/theme.coffee
    +++ b/src/app/theme.coffee
    @@ -11,7 +11,7 @@ class Theme
         if fs.exists(name)
           path = name
         else
    -      path = fs.resolve(config.themeDirPaths..., name, ['', '.tmTheme', '.css'])
    +      path = fs.resolve(config.themeDirPaths..., name, ['', '.tmTheme', '.css', 'less'])
     
         throw new Error("No theme exists named '#{name}'") unless path
     
    diff --git a/src/app/window.coffee b/src/app/window.coffee
    index 5b1614085..7d1558bd6 100644
    --- a/src/app/window.coffee
    +++ b/src/app/window.coffee
    @@ -116,18 +116,21 @@ window.stylesheetElementForId = (id) ->
     
     window.requireStylesheet = (path) ->
       if fullPath = require.resolve(path)
    -    content = ""
    -    if fs.extension(fullPath) == '.less'
    -      (new less.Parser).parse __read(fullPath), (e, tree) ->
    -        throw new Error(e.message, file, e.line) if e
    -        content = tree.toCSS()
    -    else
    -      content = fs.read(fullPath)
    -
    +    content = window.loadStylesheet(fullPath)
         window.applyStylesheet(fullPath, content)
    -  unless fullPath
    +  else
    +    console.log "bad", path
         throw new Error("Could not find a file at path '#{path}'")
     
    +window.loadStylesheet = (path) ->
    +  content = fs.read(path)
    +  if fs.extension(path) == '.less'
    +    (new less.Parser).parse content, (e, tree) ->
    +      throw new Error(e.message, file, e.line) if e
    +      content = tree.toCSS()
    +
    +  content
    +
     window.removeStylesheet = (path) ->
       unless fullPath = require.resolve(path)
         throw new Error("Could not find a file at path '#{path}'")
    
    From 7c47b7f8d41d219fff2f869f14765305c78ab6df Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Mon, 11 Mar 2013 09:47:34 -0700
    Subject: [PATCH 276/308] Pane styling isn't a child of the .root-view class
    
    Panes aren't attached to a root view in spes.
    ---
     static/atom.less | 68 ++++++++++++++++++++++++++----------------------
     1 file changed, 37 insertions(+), 31 deletions(-)
    
    diff --git a/static/atom.less b/static/atom.less
    index c02f90d90..0ab8cae9a 100644
    --- a/static/atom.less
    +++ b/static/atom.less
    @@ -19,39 +19,45 @@ html, body {
         -webkit-flex: 1;
         -webkit-flex-flow: column;
       }
    +}
     
    -  #panes {
    -    position: relative;
    +#panes {
    +  position: relative;
    +  -webkit-flex: 1;
    +
    +  .column {
    +    position: absolute;
    +    top: 0;
    +    bottom: 0;
    +    left: 0;
    +    right: 0;
    +    overflow-y: hidden;
    +  }
    +
    +  .row {
    +    position: absolute;
    +    top: 0;
    +    bottom: 0;
    +    left: 0;
    +    right: 0;
    +    overflow-x: hidden;
    +  }
    +
    +  .pane {
    +    position: absolute;
    +    display: -webkit-flex;
    +    -webkit-flex-flow: column;
    +    top: 0;
    +    bottom: 0;
    +    left: 0;
    +    right: 0;
    +    box-sizing: border-box;
    +  }
    +
    +  .pane .item-views {
         -webkit-flex: 1;
    -
    -    .column {
    -      position: absolute;
    -      top: 0;
    -      bottom: 0;
    -      left: 0;
    -      right: 0;
    -      overflow-y: hidden;
    -    }
    -
    -    .row {
    -      position: absolute;
    -      top: 0;
    -      bottom: 0;
    -      left: 0;
    -      right: 0;
    -      overflow-x: hidden;
    -    }
    -
    -    .pane {
    -      position: absolute;
    -      display: -webkit-flex;
    -      -webkit-flex-flow: column;
    -      top: 0;
    -      bottom: 0;
    -      left: 0;
    -      right: 0;
    -      box-sizing: border-box;
    -    }
    +    display: -webkit-flex;
    +    -webkit-flex-flow: column;
       }
     }
     
    
    From 634702005dcdd78f94c1379ff4867586d2db11c6 Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Mon, 11 Mar 2013 10:24:00 -0700
    Subject: [PATCH 277/308] :lipstick:
    
    ---
     native/atom_main_mac.mm | 16 ++++++----------
     1 file changed, 6 insertions(+), 10 deletions(-)
    
    diff --git a/native/atom_main_mac.mm b/native/atom_main_mac.mm
    index 6c148fc84..1f418f9f0 100644
    --- a/native/atom_main_mac.mm
    +++ b/native/atom_main_mac.mm
    @@ -13,18 +13,14 @@ void activateOpenApp();
     BOOL isAppAlreadyOpen();
     
     int AtomMain(int argc, char* argv[]) {
    -  {
    -    // See if we're being run as a secondary process.
    -
    -    CefMainArgs main_args(argc, argv);
    -    CefRefPtr app(new AtomCefApp);
    -    int exitCode = CefExecuteProcess(main_args, app);
    -    if (exitCode >= 0)
    -      return exitCode;
    -  }
    +  // Check if we're being run as a secondary process.
    +  CefMainArgs main_args(argc, argv);
    +  CefRefPtr app(new AtomCefApp);
    +  int exitCode = CefExecuteProcess(main_args, app);
    +  if (exitCode >= 0)
    +    return exitCode;
     
       // We're the main process.
    -
       @autoreleasepool {
         handleBeingOpenedAgain(argc, argv);
     
    
    From 8fec1e82eebcad2bd330c7cb2ea419f7b9f47e91 Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Mon, 11 Mar 2013 10:26:46 -0700
    Subject: [PATCH 278/308] Use instantiateWithOwner:topLevelObjects
    
    Removes deprecation warnings
    ---
     native/atom_main_mac.mm | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/native/atom_main_mac.mm b/native/atom_main_mac.mm
    index 1f418f9f0..ed1f3c3ca 100644
    --- a/native/atom_main_mac.mm
    +++ b/native/atom_main_mac.mm
    @@ -29,7 +29,7 @@ int AtomMain(int argc, char* argv[]) {
     
         NSString *mainNibName = [infoDictionary objectForKey:@"NSMainNibFile"];
         NSNib *mainNib = [[NSNib alloc] initWithNibNamed:mainNibName bundle:[NSBundle bundleWithIdentifier:@"com.github.atom.framework"]];
    -    [mainNib instantiateNibWithOwner:application topLevelObjects:nil];
    +    [mainNib instantiateWithOwner:application topLevelObjects:nil];
     
         CefRunMessageLoop();
       }
    
    From 8247e56bef0cdf9c86b41ba842aa4b5c18adaf83 Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Mon, 11 Mar 2013 10:49:01 -0700
    Subject: [PATCH 279/308] Fix objective-c compiler warning
    
    ---
     native/atom_application.h | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/native/atom_application.h b/native/atom_application.h
    index cef8719b1..d2f79686b 100644
    --- a/native/atom_application.h
    +++ b/native/atom_application.h
    @@ -18,6 +18,7 @@ class AtomCefClient;
     + (CefSettings)createCefSettings;
     + (NSDictionary *)parseArguments:(char **)argv count:(int)argc;
     - (void)open:(NSString *)path;
    +- (void)openDev:(NSString *)path;
     - (void)open:(NSString *)path pidToKillWhenWindowCloses:(NSNumber *)pid;
     - (IBAction)runSpecs:(id)sender;
     - (IBAction)runBenchmarks:(id)sender;
    
    From a39217416421c90676fa9f3a70840abd769dd641 Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Mon, 11 Mar 2013 09:08:29 -0700
    Subject: [PATCH 280/308] Un-f
    
    ---
     .../markdown-preview/spec/markdown-preview-view-spec.coffee     | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee b/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee
    index 7d98a70dc..42cac9b84 100644
    --- a/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee
    +++ b/src/packages/markdown-preview/spec/markdown-preview-view-spec.coffee
    @@ -34,6 +34,6 @@ describe "MarkdownPreviewView", ->
           expect(preview.text()).toContain "Failed"
     
       describe "serialization", ->
    -    fit "reassociates with the same buffer when deserialized", ->
    +    it "reassociates with the same buffer when deserialized", ->
           newPreview = deserialize(preview.serialize())
           expect(newPreview.buffer).toBe buffer
    
    From 10d0fdf2d7ca1328335a0523e8e6546d95912e2b Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Mon, 11 Mar 2013 15:08:19 -0600
    Subject: [PATCH 281/308] Require a deferred package early if needed when
     deserializing panes
    
    The requiring of a package's main module is now decoupled from package
    activation. Non-deferred packages will always be required before the
    panes are deserialized. This allows the package to register any
    deserializers for objects displayed in the panes.
    
    Deferred packages can contain a 'deferredDeserializers' array in their
    package.cson. If we attempt to deserialize an object with a deserializer
    in the list, the package's main module will be required first so it has
    a chance to register the deserializer. But the package still won't be
    activated until an activation event occurs.
    
    We may want to add an additional optional hook called 'load' which is
    called at require time. We would not guarantee that the rootView
    global would exist, but we could give the package a chance to register
    deserializers etc. For now, registering deserializers is a side-effect
    of requiring the package.
    ---
     spec/app/atom-package-spec.coffee             |   4 +-
     spec/app/atom-spec.coffee                     |   9 +-
     spec/spec-helper.coffee                       |   4 +-
     src/app/atom-package.coffee                   | 117 ++++++++++--------
     src/app/atom.coffee                           |   3 +
     src/app/text-mate-package.coffee              |   2 +
     src/app/window.coffee                         |  13 +-
     src/packages/markdown-preview/package.cson    |   1 +
     .../spec/markdown-preview-spec.coffee         |   2 +-
     .../spell-check/spec/spell-check-spec.coffee  |   2 +-
     10 files changed, 97 insertions(+), 60 deletions(-)
    
    diff --git a/spec/app/atom-package-spec.coffee b/spec/app/atom-package-spec.coffee
    index 7b75b8ede..8154b08b4 100644
    --- a/spec/app/atom-package-spec.coffee
    +++ b/spec/app/atom-package-spec.coffee
    @@ -3,7 +3,7 @@ AtomPackage = require 'atom-package'
     fs = require 'fs'
     
     describe "AtomPackage", ->
    -  describe ".load()", ->
    +  describe ".activate()", ->
         beforeEach ->
           window.rootView = new RootView
     
    @@ -15,6 +15,7 @@ describe "AtomPackage", ->
             packageMainModule = require 'fixtures/packages/package-with-activation-events/main'
             spyOn(packageMainModule, 'activate').andCallThrough()
             pack.load()
    +        pack.activate()
     
           it "defers activating the package until an activation event bubbles to the root view", ->
             expect(packageMainModule.activate).not.toHaveBeenCalled()
    @@ -44,6 +45,7 @@ describe "AtomPackage", ->
     
               expect(packageMainModule.activate).not.toHaveBeenCalled()
               pack.load()
    +          pack.activate()
               expect(packageMainModule.activate).toHaveBeenCalled()
     
           describe "when the package doesn't have an index.coffee", ->
    diff --git a/spec/app/atom-spec.coffee b/spec/app/atom-spec.coffee
    index e614a2ff7..717a75426 100644
    --- a/spec/app/atom-spec.coffee
    +++ b/spec/app/atom-spec.coffee
    @@ -103,7 +103,7 @@ describe "the `atom` global", ->
             expect(atom.activatedAtomPackages.length).toBe 0
     
         describe "serialization", ->
    -      it "uses previous serialization state on unactivated packages", ->
    +      it "uses previous serialization state on packages whose activation has been deferred", ->
             atom.atomPackageStates['package-with-activation-events'] = {previousData: 'exists'}
             unactivatedPackage = window.loadPackage('package-with-activation-events')
             activatedPackage = window.loadPackage('package-with-module')
    @@ -115,7 +115,8 @@ describe "the `atom` global", ->
                 'previousData': 'exists'
     
             # ensure serialization occurs when the packageis activated
    -        unactivatedPackage.activatePackageMain()
    +        unactivatedPackage.deferActivation = false
    +        unactivatedPackage.activate()
             expect(atom.serializeAtomPackages()).toEqual
               'package-with-module':
                 'someNumber': 1
    @@ -124,8 +125,8 @@ describe "the `atom` global", ->
     
           it "absorbs exceptions that are thrown by the package module's serialize methods", ->
             spyOn(console, 'error')
    -        window.loadPackage('package-with-module')
    -        window.loadPackage('package-with-serialize-error', activateImmediately: true)
    +        window.loadPackage('package-with-module', activateImmediately: true)
    +        window.loadPackage('package-with-serialize-error',  activateImmediately: true)
     
             packageStates = atom.serializeAtomPackages()
             expect(packageStates['package-with-module']).toEqual someNumber: 1
    diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee
    index 151ca8488..b56f9ca78 100644
    --- a/spec/spec-helper.coffee
    +++ b/spec/spec-helper.coffee
    @@ -88,12 +88,14 @@ afterEach ->
       atom.presentingModal = false
       waits(0) # yield to ui thread to make screen update more frequently
     
    -window.loadPackage = (name, options) ->
    +window.loadPackage = (name, options={}) ->
       Package = require 'package'
       packagePath = _.find atom.getPackagePaths(), (packagePath) -> fs.base(packagePath) == name
       if pack = Package.build(packagePath)
         pack.load(options)
         atom.loadedPackages.push(pack)
    +    pack.deferActivation = false if options.activateImmediately
    +    pack.activate()
       pack
     
     # Specs rely on TextMate bundles (but not atom packages)
    diff --git a/src/app/atom-package.coffee b/src/app/atom-package.coffee
    index 4c3195b0e..1fd4d9dbf 100644
    --- a/src/app/atom-package.coffee
    +++ b/src/app/atom-package.coffee
    @@ -7,66 +7,21 @@ module.exports =
     class AtomPackage extends Package
       metadata: null
       packageMain: null
    +  deferActivation: false
     
    -  load: ({activateImmediately}={}) ->
    +  load: ->
         try
           @loadMetadata()
           @loadKeymaps()
           @loadStylesheets()
    -      if @metadata.activationEvents and not activateImmediately
    -        @subscribeToActivationEvents()
    +      if @deferActivation = @metadata.activationEvents?
    +        @registerDeferredDeserializers()
           else
    -        @activatePackageMain()
    +        @requirePackageMain()
         catch e
           console.warn "Failed to load package named '#{@name}'", e.stack
         this
     
    -  disableEventHandlersOnBubblePath: (event) ->
    -    bubblePathEventHandlers = []
    -    disabledHandler = ->
    -    element = $(event.target)
    -    while element.length
    -      if eventHandlers = element.data('events')?[event.type]
    -        for eventHandler in eventHandlers
    -          eventHandler.disabledHandler = eventHandler.handler
    -          eventHandler.handler = disabledHandler
    -          bubblePathEventHandlers.push(eventHandler)
    -      element = element.parent()
    -    bubblePathEventHandlers
    -
    -  restoreEventHandlersOnBubblePath: (eventHandlers) ->
    -    for eventHandler in eventHandlers
    -      eventHandler.handler = eventHandler.disabledHandler
    -      delete eventHandler.disabledHandler
    -
    -  unsubscribeFromActivationEvents: (activateHandler) ->
    -    if _.isArray(@metadata.activationEvents)
    -      rootView.off(event, activateHandler) for event in @metadata.activationEvents
    -    else
    -      rootView.off(event, selector, activateHandler) for event, selector of @metadata.activationEvents
    -
    -  subscribeToActivationEvents: () ->
    -    activateHandler = (event) =>
    -      bubblePathEventHandlers = @disableEventHandlersOnBubblePath(event)
    -      @activatePackageMain()
    -      $(event.target).trigger(event)
    -      @restoreEventHandlersOnBubblePath(bubblePathEventHandlers)
    -      @unsubscribeFromActivationEvents(activateHandler)
    -
    -    if _.isArray(@metadata.activationEvents)
    -      rootView.command(event, activateHandler) for event in @metadata.activationEvents
    -    else
    -      rootView.command(event, selector, activateHandler) for event, selector of @metadata.activationEvents
    -
    -  activatePackageMain: ->
    -    mainPath = @path
    -    mainPath = fs.join(mainPath, @metadata.main) if @metadata.main
    -    mainPath = require.resolve(mainPath)
    -    if fs.isFile(mainPath)
    -      @packageMain = require(mainPath)
    -      config.setDefaults(@name, @packageMain.configDefaults)
    -      atom.activateAtomPackage(this)
    -
       loadMetadata: ->
         if metadataPath = fs.resolveExtension(fs.join(@path, 'package'), ['cson', 'json'])
           @metadata = fs.readObject(metadataPath)
    @@ -86,3 +41,65 @@ class AtomPackage extends Package
         stylesheetDirPath = fs.join(@path, 'stylesheets')
         for stylesheetPath in fs.list(stylesheetDirPath)
           requireStylesheet(stylesheetPath)
    +
    +  activate: ->
    +    if @deferActivation
    +      @subscribeToActivationEvents()
    +    else
    +      try
    +        if @requirePackageMain()
    +          config.setDefaults(@name, @packageMain.configDefaults)
    +          atom.activateAtomPackage(this)
    +      catch e
    +        console.warn "Failed to activate package named '#{@name}'", e.stack
    +
    +  requirePackageMain: ->
    +    return @packageMain if @packageMain
    +    mainPath = @path
    +    mainPath = fs.join(mainPath, @metadata.main) if @metadata.main
    +    mainPath = require.resolve(mainPath)
    +    @packageMain = require(mainPath) if fs.isFile(mainPath)
    +
    +  registerDeferredDeserializers: ->
    +    for deserializerName in @metadata.deferredDeserializers ? []
    +      registerDeferredDeserializer deserializerName, => @requirePackageMain()
    +
    +  subscribeToActivationEvents: () ->
    +    return unless @metadata.activationEvents?
    +
    +    activateHandler = (event) =>
    +      bubblePathEventHandlers = @disableEventHandlersOnBubblePath(event)
    +      @deferActivation = false
    +      @activate()
    +      $(event.target).trigger(event)
    +      @restoreEventHandlersOnBubblePath(bubblePathEventHandlers)
    +      @unsubscribeFromActivationEvents(activateHandler)
    +
    +    if _.isArray(@metadata.activationEvents)
    +      rootView.command(event, activateHandler) for event in @metadata.activationEvents
    +    else
    +      rootView.command(event, selector, activateHandler) for event, selector of @metadata.activationEvents
    +
    +  unsubscribeFromActivationEvents: (activateHandler) ->
    +    if _.isArray(@metadata.activationEvents)
    +      rootView.off(event, activateHandler) for event in @metadata.activationEvents
    +    else
    +      rootView.off(event, selector, activateHandler) for event, selector of @metadata.activationEvents
    +
    +  disableEventHandlersOnBubblePath: (event) ->
    +    bubblePathEventHandlers = []
    +    disabledHandler = ->
    +    element = $(event.target)
    +    while element.length
    +      if eventHandlers = element.data('events')?[event.type]
    +        for eventHandler in eventHandlers
    +          eventHandler.disabledHandler = eventHandler.handler
    +          eventHandler.handler = disabledHandler
    +          bubblePathEventHandlers.push(eventHandler)
    +      element = element.parent()
    +    bubblePathEventHandlers
    +
    +  restoreEventHandlersOnBubblePath: (eventHandlers) ->
    +    for eventHandler in eventHandlers
    +      eventHandler.handler = eventHandler.disabledHandler
    +      delete eventHandler.disabledHandler
    diff --git a/src/app/atom.coffee b/src/app/atom.coffee
    index 2e4635c18..472a6dcb4 100644
    --- a/src/app/atom.coffee
    +++ b/src/app/atom.coffee
    @@ -61,6 +61,9 @@ _.extend atom,
     
         new LoadTextMatePackagesTask(textMatePackages).start() if textMatePackages.length > 0
     
    +  activatePackages: ->
    +    pack.activate() for pack in @loadedPackages
    +
       getLoadedPackages: ->
         _.clone(@loadedPackages)
     
    diff --git a/src/app/text-mate-package.coffee b/src/app/text-mate-package.coffee
    index c4901cc02..2929e1883 100644
    --- a/src/app/text-mate-package.coffee
    +++ b/src/app/text-mate-package.coffee
    @@ -31,6 +31,8 @@ class TextMatePackage extends Package
           console.warn "Failed to load package at '#{@path}'", e.stack
         this
     
    +  activate: -> # no-op
    +
       getGrammars: -> @grammars
     
       readGrammars: ->
    diff --git a/src/app/window.coffee b/src/app/window.coffee
    index 321a60f56..1a58bce09 100644
    --- a/src/app/window.coffee
    +++ b/src/app/window.coffee
    @@ -6,6 +6,7 @@ require 'underscore-extensions'
     require 'space-pen-extensions'
     
     deserializers = {}
    +deferredDeserializers = {}
     
     # This method is called in any window needing a general environment, including specs
     window.setUpEnvironment = ->
    @@ -52,10 +53,11 @@ window.startup = ->
       handleWindowEvents()
       config.load()
       atom.loadTextPackage()
    -  buildProjectAndRootView()
       keymap.loadBundledKeymaps()
       atom.loadThemes()
       atom.loadPackages()
    +  buildProjectAndRootView()
    +  atom.activatePackages()
       keymap.loadUserKeymaps()
       atom.requireUserInitScript()
       $(window).on 'beforeunload', -> shutdown(); false
    @@ -151,6 +153,9 @@ window.registerDeserializers = (args...) ->
     window.registerDeserializer = (klass) ->
       deserializers[klass.name] = klass
     
    +window.registerDeferredDeserializer = (name, fn) ->
    +  deferredDeserializers[name] = fn
    +
     window.unregisterDeserializer = (klass) ->
       delete deserializers[klass.name]
     
    @@ -160,7 +165,11 @@ window.deserialize = (state) ->
         deserializer.deserialize(state)
     
     window.getDeserializer = (state) ->
    -  deserializers[state?.deserializer]
    +  name = state?.deserializer
    +  if deferredDeserializers[name]
    +    deferredDeserializers[name]()
    +    delete deferredDeserializers[name]
    +  deserializers[name]
     
     window.measure = (description, fn) ->
       start = new Date().getTime()
    diff --git a/src/packages/markdown-preview/package.cson b/src/packages/markdown-preview/package.cson
    index dbd69aef5..29925172d 100644
    --- a/src/packages/markdown-preview/package.cson
    +++ b/src/packages/markdown-preview/package.cson
    @@ -1,3 +1,4 @@
     'main': 'lib/markdown-preview'
     'activationEvents':
       'markdown-preview:show': '.editor'
    +'deferredDeserializers': ['MarkdownPreviewView']
    diff --git a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee
    index 8337ea46f..4bb3dbdf8 100644
    --- a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee
    +++ b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee
    @@ -6,7 +6,7 @@ describe "MarkdownPreview package", ->
       beforeEach ->
         project.setPath(project.resolve('markdown'))
         window.rootView = new RootView
    -    window.loadPackage("markdown-preview")
    +    window.loadPackage("markdown-preview", activateImmediately: true)
         spyOn(MarkdownPreviewView.prototype, 'fetchRenderedMarkdown')
     
       describe "markdown-preview:show", ->
    diff --git a/src/packages/spell-check/spec/spell-check-spec.coffee b/src/packages/spell-check/spec/spell-check-spec.coffee
    index f2b4f4f3a..0756dbdeb 100644
    --- a/src/packages/spell-check/spec/spell-check-spec.coffee
    +++ b/src/packages/spell-check/spec/spell-check-spec.coffee
    @@ -7,7 +7,7 @@ describe "Spell check", ->
         window.rootView = new RootView
         rootView.open('sample.js')
         config.set('spell-check.grammars', [])
    -    window.loadPackage('spell-check')
    +    window.loadPackage('spell-check', activateImmediately: true)
         rootView.attachToDom()
         editor = rootView.getActiveView()
     
    
    From 50b61f3a00537fe05d1771e106acdd7e4cb28544 Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Mon, 11 Mar 2013 15:20:51 -0600
    Subject: [PATCH 282/308] Test requiring of package main module via deferred
     deserializer
    
    ---
     spec/app/atom-package-spec.coffee             | 24 ++++++++++++++-----
     .../main.coffee                               |  7 ++++++
     .../package.cson                              |  1 +
     3 files changed, 26 insertions(+), 6 deletions(-)
    
    diff --git a/spec/app/atom-package-spec.coffee b/spec/app/atom-package-spec.coffee
    index 8154b08b4..6a0b898bc 100644
    --- a/spec/app/atom-package-spec.coffee
    +++ b/spec/app/atom-package-spec.coffee
    @@ -3,18 +3,30 @@ AtomPackage = require 'atom-package'
     fs = require 'fs'
     
     describe "AtomPackage", ->
    +  [packageMainModule, pack] = []
    +
    +  beforeEach ->
    +    pack = new AtomPackage(fs.resolve(config.packageDirPaths..., 'package-with-activation-events'))
    +    pack.load()
    +
    +  describe ".load()", ->
    +    describe "if the package's metadata has a `deferredDeserializers` array", ->
    +      it "requires the package's main module attempting to use deserializers named in the array", ->
    +        expect(pack.packageMain).toBeNull()
    +        object = deserialize(deserializer: 'Foo', data: "Hello")
    +        expect(object.constructor.name).toBe 'Foo'
    +        expect(object.data).toBe 'Hello'
    +        expect(pack.packageMain).toBeDefined()
    +        expect(pack.packageMain.activateCallCount).toBe 0
    +
       describe ".activate()", ->
         beforeEach ->
           window.rootView = new RootView
    +      packageMainModule = require 'fixtures/packages/package-with-activation-events/main'
    +      spyOn(packageMainModule, 'activate').andCallThrough()
     
         describe "when the package metadata includes activation events", ->
    -      [packageMainModule, pack] = []
    -
           beforeEach ->
    -        pack = new AtomPackage(fs.resolve(config.packageDirPaths..., 'package-with-activation-events'))
    -        packageMainModule = require 'fixtures/packages/package-with-activation-events/main'
    -        spyOn(packageMainModule, 'activate').andCallThrough()
    -        pack.load()
             pack.activate()
     
           it "defers activating the package until an activation event bubbles to the root view", ->
    diff --git a/spec/fixtures/packages/package-with-activation-events/main.coffee b/spec/fixtures/packages/package-with-activation-events/main.coffee
    index a860be2bb..d57ca7c24 100644
    --- a/spec/fixtures/packages/package-with-activation-events/main.coffee
    +++ b/spec/fixtures/packages/package-with-activation-events/main.coffee
    @@ -1,7 +1,14 @@
    +class Foo
    +  registerDeserializer(this)
    +  @deserialize: ({data}) -> new Foo(data)
    +  constructor: (@data) ->
    +
     module.exports =
    +  activateCallCount: 0
       activationEventCallCount: 0
     
       activate: ->
    +    @activateCallCount++
         rootView.getActiveView()?.command 'activation-event', =>
           @activationEventCallCount++
     
    diff --git a/spec/fixtures/packages/package-with-activation-events/package.cson b/spec/fixtures/packages/package-with-activation-events/package.cson
    index 80903d6f4..42d3eb78d 100644
    --- a/spec/fixtures/packages/package-with-activation-events/package.cson
    +++ b/spec/fixtures/packages/package-with-activation-events/package.cson
    @@ -1,2 +1,3 @@
     'activationEvents': ['activation-event']
    +'deferredDeserializers': ['Foo']
     'main': 'main'
    
    From bf7fc39434ec6b2c3798b8d9e92f60907c8861d2 Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Mon, 11 Mar 2013 15:28:28 -0600
    Subject: [PATCH 283/308] Rename AtomPackage.packageMain to .mainModule
    
    ---
     spec/app/atom-package-spec.coffee                         | 6 +++---
     spec/app/atom-spec.coffee                                 | 8 ++++----
     src/app/atom-package.coffee                               | 8 ++++----
     src/app/atom.coffee                                       | 6 +++---
     .../command-logger/spec/command-logger-spec.coffee        | 2 +-
     src/packages/command-panel/spec/command-panel-spec.coffee | 2 +-
     src/packages/editor-stats/spec/editor-stats-spec.coffee   | 2 +-
     src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee   | 2 +-
     src/packages/tree-view/spec/tree-view-spec.coffee         | 6 +++---
     9 files changed, 21 insertions(+), 21 deletions(-)
    
    diff --git a/spec/app/atom-package-spec.coffee b/spec/app/atom-package-spec.coffee
    index 6a0b898bc..469eb59e4 100644
    --- a/spec/app/atom-package-spec.coffee
    +++ b/spec/app/atom-package-spec.coffee
    @@ -12,12 +12,12 @@ describe "AtomPackage", ->
       describe ".load()", ->
         describe "if the package's metadata has a `deferredDeserializers` array", ->
           it "requires the package's main module attempting to use deserializers named in the array", ->
    -        expect(pack.packageMain).toBeNull()
    +        expect(pack.mainModule).toBeNull()
             object = deserialize(deserializer: 'Foo', data: "Hello")
             expect(object.constructor.name).toBe 'Foo'
             expect(object.data).toBe 'Hello'
    -        expect(pack.packageMain).toBeDefined()
    -        expect(pack.packageMain.activateCallCount).toBe 0
    +        expect(pack.mainModule).toBeDefined()
    +        expect(pack.mainModule.activateCallCount).toBe 0
     
       describe ".activate()", ->
         beforeEach ->
    diff --git a/spec/app/atom-spec.coffee b/spec/app/atom-spec.coffee
    index 717a75426..3b007f125 100644
    --- a/spec/app/atom-spec.coffee
    +++ b/spec/app/atom-spec.coffee
    @@ -84,22 +84,22 @@ describe "the `atom` global", ->
         describe "activation", ->
           it "calls activate on the package main with its previous state", ->
             pack = window.loadPackage('package-with-module')
    -        spyOn(pack.packageMain, 'activate')
    +        spyOn(pack.mainModule, 'activate')
     
             serializedState = rootView.serialize()
             rootView.deactivate()
             RootView.deserialize(serializedState)
             window.loadPackage('package-with-module')
     
    -        expect(pack.packageMain.activate).toHaveBeenCalledWith(someNumber: 1)
    +        expect(pack.mainModule.activate).toHaveBeenCalledWith(someNumber: 1)
     
         describe "deactivation", ->
           it "deactivates and removes the package module from the package module map", ->
             pack = window.loadPackage('package-with-module')
             expect(atom.activatedAtomPackages.length).toBe 1
    -        spyOn(pack.packageMain, "deactivate").andCallThrough()
    +        spyOn(pack.mainModule, "deactivate").andCallThrough()
             atom.deactivateAtomPackages()
    -        expect(pack.packageMain.deactivate).toHaveBeenCalled()
    +        expect(pack.mainModule.deactivate).toHaveBeenCalled()
             expect(atom.activatedAtomPackages.length).toBe 0
     
         describe "serialization", ->
    diff --git a/src/app/atom-package.coffee b/src/app/atom-package.coffee
    index 1fd4d9dbf..b671dc045 100644
    --- a/src/app/atom-package.coffee
    +++ b/src/app/atom-package.coffee
    @@ -6,7 +6,7 @@ $ = require 'jquery'
     module.exports =
     class AtomPackage extends Package
       metadata: null
    -  packageMain: null
    +  mainModule: null
       deferActivation: false
     
       load: ->
    @@ -48,17 +48,17 @@ class AtomPackage extends Package
         else
           try
             if @requirePackageMain()
    -          config.setDefaults(@name, @packageMain.configDefaults)
    +          config.setDefaults(@name, @mainModule.configDefaults)
               atom.activateAtomPackage(this)
           catch e
             console.warn "Failed to activate package named '#{@name}'", e.stack
     
       requirePackageMain: ->
    -    return @packageMain if @packageMain
    +    return @mainModule if @mainModule
         mainPath = @path
         mainPath = fs.join(mainPath, @metadata.main) if @metadata.main
         mainPath = require.resolve(mainPath)
    -    @packageMain = require(mainPath) if fs.isFile(mainPath)
    +    @mainModule = require(mainPath) if fs.isFile(mainPath)
     
       registerDeferredDeserializers: ->
         for deserializerName in @metadata.deferredDeserializers ? []
    diff --git a/src/app/atom.coffee b/src/app/atom.coffee
    index 472a6dcb4..315f8314e 100644
    --- a/src/app/atom.coffee
    +++ b/src/app/atom.coffee
    @@ -24,10 +24,10 @@ _.extend atom,
     
       activateAtomPackage: (pack) ->
         @activatedAtomPackages.push(pack)
    -    pack.packageMain.activate(@atomPackageStates[pack.name] ? {})
    +    pack.mainModule.activate(@atomPackageStates[pack.name] ? {})
     
       deactivateAtomPackages: ->
    -    pack.packageMain.deactivate?() for pack in @activatedAtomPackages
    +    pack.mainModule.deactivate?() for pack in @activatedAtomPackages
         @activatedAtomPackages = []
     
       serializeAtomPackages: ->
    @@ -35,7 +35,7 @@ _.extend atom,
         for pack in @loadedPackages
           if pack in @activatedAtomPackages
             try
    -          packageStates[pack.name] = pack.packageMain.serialize?()
    +          packageStates[pack.name] = pack.mainModule.serialize?()
             catch e
               console?.error("Exception serializing '#{pack.name}' package's module\n", e.stack)
           else
    diff --git a/src/packages/command-logger/spec/command-logger-spec.coffee b/src/packages/command-logger/spec/command-logger-spec.coffee
    index 4305f9ada..8feec78bd 100644
    --- a/src/packages/command-logger/spec/command-logger-spec.coffee
    +++ b/src/packages/command-logger/spec/command-logger-spec.coffee
    @@ -7,7 +7,7 @@ describe "CommandLogger", ->
       beforeEach ->
         window.rootView = new RootView
         rootView.open('sample.js')
    -    commandLogger = window.loadPackage('command-logger').packageMain
    +    commandLogger = window.loadPackage('command-logger').mainModule
         commandLogger.eventLog = {}
         editor = rootView.getActiveView()
     
    diff --git a/src/packages/command-panel/spec/command-panel-spec.coffee b/src/packages/command-panel/spec/command-panel-spec.coffee
    index 213d14191..c6d9f7997 100644
    --- a/src/packages/command-panel/spec/command-panel-spec.coffee
    +++ b/src/packages/command-panel/spec/command-panel-spec.coffee
    @@ -11,7 +11,7 @@ describe "CommandPanel", ->
         rootView.enableKeymap()
         editSession = rootView.getActivePaneItem()
         buffer = editSession.buffer
    -    commandPanelMain = window.loadPackage('command-panel', activateImmediately: true).packageMain
    +    commandPanelMain = window.loadPackage('command-panel', activateImmediately: true).mainModule
         commandPanel = commandPanelMain.commandPanelView
         commandPanel.history = []
         commandPanel.historyIndex = 0
    diff --git a/src/packages/editor-stats/spec/editor-stats-spec.coffee b/src/packages/editor-stats/spec/editor-stats-spec.coffee
    index 5d8708e42..9ce2360a2 100644
    --- a/src/packages/editor-stats/spec/editor-stats-spec.coffee
    +++ b/src/packages/editor-stats/spec/editor-stats-spec.coffee
    @@ -17,7 +17,7 @@ describe "EditorStats", ->
       beforeEach ->
         window.rootView = new RootView
         rootView.open('sample.js')
    -    editorStats = window.loadPackage('editor-stats').packageMain.stats
    +    editorStats = window.loadPackage('editor-stats').mainModule.stats
     
       describe "when a keyup event is triggered", ->
         beforeEach ->
    diff --git a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee
    index ffaae8cb0..bde11cca9 100644
    --- a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee
    +++ b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee
    @@ -13,7 +13,7 @@ describe 'FuzzyFinder', ->
         window.rootView = new RootView
         rootView.open('sample.js')
         rootView.enableKeymap()
    -    finderView = window.loadPackage("fuzzy-finder").packageMain.createView()
    +    finderView = window.loadPackage("fuzzy-finder").mainModule.createView()
     
       describe "file-finder behavior", ->
         describe "toggling", ->
    diff --git a/src/packages/tree-view/spec/tree-view-spec.coffee b/src/packages/tree-view/spec/tree-view-spec.coffee
    index 2bd5ac3d6..d48f5577c 100644
    --- a/src/packages/tree-view/spec/tree-view-spec.coffee
    +++ b/src/packages/tree-view/spec/tree-view-spec.coffee
    @@ -50,7 +50,7 @@ describe "TreeView", ->
             rootView.deactivate()
             window.rootView = new RootView()
             rootView.open()
    -        treeView = window.loadPackage("tree-view").packageMain.createView()
    +        treeView = window.loadPackage("tree-view").mainModule.createView()
     
           it "does not attach to the root view or create a root node when initialized", ->
             expect(treeView.hasParent()).toBeFalsy()
    @@ -76,13 +76,13 @@ describe "TreeView", ->
             rootView.deactivate()
             window.rootView = new RootView
             rootView.open('tree-view.js')
    -        treeView = window.loadPackage("tree-view").packageMain.createView()
    +        treeView = window.loadPackage("tree-view").mainModule.createView()
             expect(treeView.hasParent()).toBeFalsy()
             expect(treeView.root).toExist()
     
         describe "when the root view is opened to a directory", ->
           it "attaches to the root view", ->
    -        treeView = window.loadPackage("tree-view").packageMain.createView()
    +        treeView = window.loadPackage("tree-view").mainModule.createView()
             expect(treeView.hasParent()).toBeTruthy()
             expect(treeView.root).toExist()
     
    
    From 194ac13f4342e8e36744356afc721ae9dcab1072 Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Mon, 11 Mar 2013 16:05:20 -0600
    Subject: [PATCH 284/308] :lipstick:
    
    ---
     src/app/atom-package.coffee | 8 ++++----
     1 file changed, 4 insertions(+), 4 deletions(-)
    
    diff --git a/src/app/atom-package.coffee b/src/app/atom-package.coffee
    index b671dc045..8a0e312c9 100644
    --- a/src/app/atom-package.coffee
    +++ b/src/app/atom-package.coffee
    @@ -17,7 +17,7 @@ class AtomPackage extends Package
           if @deferActivation = @metadata.activationEvents?
             @registerDeferredDeserializers()
           else
    -        @requirePackageMain()
    +        @requireMainModule()
         catch e
           console.warn "Failed to load package named '#{@name}'", e.stack
         this
    @@ -47,13 +47,13 @@ class AtomPackage extends Package
           @subscribeToActivationEvents()
         else
           try
    -        if @requirePackageMain()
    +        if @requireMainModule()
               config.setDefaults(@name, @mainModule.configDefaults)
               atom.activateAtomPackage(this)
           catch e
             console.warn "Failed to activate package named '#{@name}'", e.stack
     
    -  requirePackageMain: ->
    +  requireMainModule: ->
         return @mainModule if @mainModule
         mainPath = @path
         mainPath = fs.join(mainPath, @metadata.main) if @metadata.main
    @@ -62,7 +62,7 @@ class AtomPackage extends Package
     
       registerDeferredDeserializers: ->
         for deserializerName in @metadata.deferredDeserializers ? []
    -      registerDeferredDeserializer deserializerName, => @requirePackageMain()
    +      registerDeferredDeserializer deserializerName, => @requireMainModule()
     
       subscribeToActivationEvents: () ->
         return unless @metadata.activationEvents?
    
    From 2aefd8ca46ba21e4e0df75598e3a15bcbdb60c88 Mon Sep 17 00:00:00 2001
    From: Corey Johnson & Nathan Sobo 
    Date: Mon, 11 Mar 2013 16:52:15 -0600
    Subject: [PATCH 285/308] Set overflow hidden on status bar
    
    The octicon was causing overflow, which was making the entire view
    scroll because the status bar was bigger than the height used by
    flexbox.
    ---
     static/status-bar.css | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/static/status-bar.css b/static/status-bar.css
    index d7822f50c..f30dd48c8 100644
    --- a/static/status-bar.css
    +++ b/static/status-bar.css
    @@ -5,6 +5,7 @@
       position: relative;
       -webkit-user-select: none;
       cursor: default;
    +  overflow: hidden;
     }
     
     .status-bar .git-branch {
    
    From 140b22737ea3a750baef21fd83605669acf07889 Mon Sep 17 00:00:00 2001
    From: Corey Johnson & Nathan Sobo 
    Date: Mon, 11 Mar 2013 16:56:44 -0600
    Subject: [PATCH 286/308] Refetch rendered markdown when triggering preview a
     subsequent time
    
    ---
     src/packages/markdown-preview/lib/markdown-preview.coffee       | 1 +
     src/packages/markdown-preview/spec/markdown-preview-spec.coffee | 2 ++
     2 files changed, 3 insertions(+)
    
    diff --git a/src/packages/markdown-preview/lib/markdown-preview.coffee b/src/packages/markdown-preview/lib/markdown-preview.coffee
    index 588545e83..b30bbb621 100644
    --- a/src/packages/markdown-preview/lib/markdown-preview.coffee
    +++ b/src/packages/markdown-preview/lib/markdown-preview.coffee
    @@ -17,6 +17,7 @@ module.exports =
         if nextPane = activePane.getNextPane()
           if preview = nextPane.itemForUri("markdown-preview:#{editSession.getPath()}")
             nextPane.showItem(preview)
    +        preview.fetchRenderedMarkdown()
           else
             nextPane.showItem(new MarkdownPreviewView(editSession.buffer))
         else
    diff --git a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee
    index 4bb3dbdf8..9a7ed6e33 100644
    --- a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee
    +++ b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee
    @@ -58,7 +58,9 @@ describe "MarkdownPreview package", ->
               expect(pane2.activeItem).not.toBe preview
               pane1.focus()
     
    +          preview.fetchRenderedMarkdown.reset()
               rootView.getActiveView().trigger 'markdown-preview:show'
    +          expect(preview.fetchRenderedMarkdown).toHaveBeenCalled()
               expect(rootView.getPanes()).toHaveLength 2
               expect(pane2.getItems()).toHaveLength 2
               expect(pane2.activeItem).toBe preview
    
    From 7e03880bd09b86eb139d66dd83323b3ac5fa0fd7 Mon Sep 17 00:00:00 2001
    From: Corey Johnson & Nathan Sobo 
    Date: Mon, 11 Mar 2013 17:31:17 -0600
    Subject: [PATCH 287/308] Add $.fn.scrollUp and .scrollDown
    
    These scroll the element by a small amount up or down.
    ---
     spec/stdlib/jquery-extensions-spec.coffee | 30 +++++++++++++++++++++++
     src/stdlib/jquery-extensions.coffee       |  6 +++++
     2 files changed, 36 insertions(+)
    
    diff --git a/spec/stdlib/jquery-extensions-spec.coffee b/spec/stdlib/jquery-extensions-spec.coffee
    index 6b781370b..4bdd8fd11 100644
    --- a/spec/stdlib/jquery-extensions-spec.coffee
    +++ b/spec/stdlib/jquery-extensions-spec.coffee
    @@ -1,4 +1,5 @@
     $ = require 'jquery'
    +_ = require 'underscore'
     {View, $$} = require 'space-pen'
     
     describe 'jQuery extensions', ->
    @@ -76,6 +77,35 @@ describe 'jQuery extensions', ->
             'a1': "A1: Waste perfectly-good steak"
             'a2': null
     
    +  describe "$.fn.scrollUp/Down/ToTop/ToBottom", ->
    +    it "scrolls the element in the specified way if possible", ->
    +      view = $$ -> @div => _.times 20, => @div('A')
    +      view.css(height: 100, width: 100, overflow: 'scroll')
    +      view.attachToDom()
    +
    +      view.scrollUp()
    +      expect(view.scrollTop()).toBe 0
    +
    +      view.scrollDown()
    +      expect(view.scrollTop()).toBeGreaterThan 0
    +      previousScrollTop = view.scrollTop()
    +      view.scrollDown()
    +      expect(view.scrollTop()).toBeGreaterThan previousScrollTop
    +
    +      view.scrollToBottom()
    +      expect(view.scrollTop()).toBe view.prop('scrollHeight') - 100
    +      previousScrollTop = view.scrollTop()
    +      view.scrollDown()
    +      expect(view.scrollTop()).toBe previousScrollTop
    +      view.scrollUp()
    +      expect(view.scrollTop()).toBeLessThan previousScrollTop
    +      previousScrollTop = view.scrollTop()
    +      view.scrollUp()
    +      expect(view.scrollTop()).toBeLessThan previousScrollTop
    +
    +      view.scrollToTop()
    +      expect(view.scrollTop()).toBe 0
    +
       describe "Event.prototype", ->
         class GrandchildView extends View
           @content: -> @div class: 'grandchild'
    diff --git a/src/stdlib/jquery-extensions.coffee b/src/stdlib/jquery-extensions.coffee
    index 089363577..fe403c2a9 100644
    --- a/src/stdlib/jquery-extensions.coffee
    +++ b/src/stdlib/jquery-extensions.coffee
    @@ -7,6 +7,12 @@ $.fn.scrollBottom = (newValue) ->
       else
         @scrollTop() + @height()
     
    +$.fn.scrollDown = ->
    +  @scrollTop(@scrollTop() + $(window).height() / 20)
    +
    +$.fn.scrollUp = ->
    +  @scrollTop(@scrollTop() - $(window).height() / 20)
    +
     $.fn.scrollToTop = ->
       @scrollTop(0)
     
    
    From b01a4aa041c6aaecef80cadb5277e86770b0906e Mon Sep 17 00:00:00 2001
    From: Corey Johnson & Nathan Sobo 
    Date: Mon, 11 Mar 2013 17:34:59 -0600
    Subject: [PATCH 288/308] Allow markdown preview view to be scrolled with
     core:move-up/move-down
    
    ---
     src/packages/markdown-preview/lib/markdown-preview-view.coffee | 2 ++
     1 file changed, 2 insertions(+)
    
    diff --git a/src/packages/markdown-preview/lib/markdown-preview-view.coffee b/src/packages/markdown-preview/lib/markdown-preview-view.coffee
    index f06842de6..a65c09eba 100644
    --- a/src/packages/markdown-preview/lib/markdown-preview-view.coffee
    +++ b/src/packages/markdown-preview/lib/markdown-preview-view.coffee
    @@ -16,6 +16,8 @@ class MarkdownPreviewView extends ScrollView
       initialize: (@buffer) ->
         super
         @fetchRenderedMarkdown()
    +    @on 'core:move-up', => @scrollUp()
    +    @on 'core:move-down', => @scrollDown()
     
       serialize: ->
         deserializer: 'MarkdownPreviewView'
    
    From 214209d2da82591f0bbb1ac81b2e6a413fe4819d Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Mon, 11 Mar 2013 18:35:13 -0600
    Subject: [PATCH 289/308] Add docs about serialization
    
    ---
     docs/internals/serialization.md | 98 +++++++++++++++++++++++++++++++++
     1 file changed, 98 insertions(+)
     create mode 100644 docs/internals/serialization.md
    
    diff --git a/docs/internals/serialization.md b/docs/internals/serialization.md
    new file mode 100644
    index 000000000..ec6a2aba3
    --- /dev/null
    +++ b/docs/internals/serialization.md
    @@ -0,0 +1,98 @@
    +## Serialization in Atom
    +
    +When a window is refreshed or restored from a previous session, the view and its
    +associated objects are *deserialized* from a JSON representation that was stored
    +during the window's previous shutdown. For your own views and objects to be
    +compatible with refreshing, you'll need to make them play nicely with the
    +serializing and deserializing.
    +
    +### Package Serialization Hook
    +
    +Your package's main module can optionally include a `serialize` method, which
    +will be called before your package is deactivated. You should return JSON, which
    +will be handed back to you as an argument to `activate` next time it is called.
    +In the following example, the package keeps an instance of `MyObject` in the
    +same state across refreshes.
    +
    +```coffee-script
    +module.exports =
    +  activate: (state) ->
    +    @myObject =
    +      if state
    +        deserialize(state)
    +      else
    +        new MyObject("Hello")
    +
    +  serialize: ->
    +    @myObject.serialize()
    +```
    +
    +### Serialization Methods
    +
    +```coffee-script
    +class MyObject
    +  registerDeserializer(this)
    +  @deserialize: ({data}) -> new MyObject(data)
    +  constructor: (@data) ->
    +  serialize: -> { deserializer: 'MyObject', data: @data }
    +```
    +
    +#### .serialize()
    +Objects that you want to serialize should implement `.serialize()`. This method
    +should return a serializable object, and it must contain a key named
    +`deserializer` whose value is the name of a registered deserializer that can
    +convert the rest of the data to an object. It's usually just the name of the
    +class itself.
    +
    +#### @deserialize(data)
    +The other side of the coin is the `deserialize` method, which is usually a
    +class-level method on the same class that implements `serialize`. This method's
    +job is to convert a state object returned from a previous call `serialize` back
    +into a genuine object.
    +
    +#### registerDeserializer(klass)
    +You need to call the global `registerDeserializer` method with your class in
    +order to make it available to the deserialization system. Now you can call the
    +global `deserialize` method with state returned from `serialize`, and your
    +class's `deserialize` method will be selected automatically.
    +
    +### Versioning
    +
    +```coffee-script
    +class MyObject
    +  @version: 2
    +  @deserialize: (state) -> ...
    +  serialize: -> { version: MyObject.version, ... }
    +```
    +
    +Your serializable class can optionally have a version class-level `@version`
    +property and include a `version` key in its serialized state. When
    +deserializing, Atom will only attempt to call deserialize if the two versions
    +match, and otherwise return undefined. We plan on implementing a migration
    +system in the future, but this at least protects you from improperly
    +deserializing old state. If you find yourself in dire need of the migration
    +system, let us know.
    +
    +### Deferred Package Deserializers
    +
    +If your package defers loading on startup with an `activationEvents` property in
    +its `package.cson`, your deserializers won't be loaded until your package is
    +activated. If you want to deserialize an object from your package on startup,
    +this could be a problem.
    +
    +The solution is to also supply a `deferredDeserializers` array in your
    +`package.cson` with the names of all your deserializers. When Atom attempts to
    +deserialize some state whose `deserializer` matches one of these names, it will
    +load your package first so it can register any necessary deserializers before
    +proceeding.
    +
    +For example, the markdown preview package doesn't fully load until a preview is
    +triggered. But if you refresh a window with a preview pane, it loads the
    +markdown package early so Atom can deserialize the view correctly.
    +
    +```coffee-script
    +# markdown-preview/package.cson
    +'activationEvents': 'markdown-preview:toggle': '.editor'
    +'deferredDeserializers': ['MarkdownPreviewView']
    +...
    +```
    
    From 35419e8d80a657a904b69ffdff90ecb437cc5c44 Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Mon, 11 Mar 2013 19:59:18 -0600
    Subject: [PATCH 290/308] :lipstick:
    
    ---
     docs/internals/serialization.md | 13 ++++++-------
     1 file changed, 6 insertions(+), 7 deletions(-)
    
    diff --git a/docs/internals/serialization.md b/docs/internals/serialization.md
    index ec6a2aba3..ce1d7a78e 100644
    --- a/docs/internals/serialization.md
    +++ b/docs/internals/serialization.md
    @@ -65,13 +65,12 @@ class MyObject
       serialize: -> { version: MyObject.version, ... }
     ```
     
    -Your serializable class can optionally have a version class-level `@version`
    -property and include a `version` key in its serialized state. When
    -deserializing, Atom will only attempt to call deserialize if the two versions
    -match, and otherwise return undefined. We plan on implementing a migration
    -system in the future, but this at least protects you from improperly
    -deserializing old state. If you find yourself in dire need of the migration
    -system, let us know.
    +Your serializable class can optionally have a class-level `@version` property
    +and include a `version` key in its serialized state. When deserializing, Atom
    +will only attempt to call deserialize if the two versions match, and otherwise
    +return undefined. We plan on implementing a migration system in the future, but
    +this at least protects you from improperly deserializing old state. If you find
    +yourself in dire need of the migration system, let us know.
     
     ### Deferred Package Deserializers
     
    
    From 66467b35712e24599ab0c0a5a769663a34857263 Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Mon, 11 Mar 2013 20:17:24 -0600
    Subject: [PATCH 291/308] Fix markdown preview height. Some styles crept back
     in during merge.
    
    ---
     .../markdown-preview/stylesheets/markdown-preview.less      | 6 ------
     1 file changed, 6 deletions(-)
    
    diff --git a/src/packages/markdown-preview/stylesheets/markdown-preview.less b/src/packages/markdown-preview/stylesheets/markdown-preview.less
    index b79aedb3e..608389a88 100644
    --- a/src/packages/markdown-preview/stylesheets/markdown-preview.less
    +++ b/src/packages/markdown-preview/stylesheets/markdown-preview.less
    @@ -2,14 +2,8 @@
       font-family: "Helvetica Neue", Helvetica, sans-serif;
       font-size: 14px;
       line-height: 1.6;
    -  position: absolute;
    -  width: 100%;
    -  height: 100%;
    -  top: 0px;
    -  left: 0px;
       background-color: #fff;
       overflow: scroll;
    -  z-index: 3;
       box-sizing: border-box;
       padding: 20px;
     }
    
    From 7c04aaf536b131876fc40df6837c642dd2ed1ea1 Mon Sep 17 00:00:00 2001
    From: Kevin Sawicki 
    Date: Mon, 11 Mar 2013 19:45:01 -0700
    Subject: [PATCH 292/308] Only set project paths if non-empty
    
    ---
     src/packages/fuzzy-finder/lib/fuzzy-finder.coffee | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee b/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee
    index c6fbe92aa..fbb180020 100644
    --- a/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee
    +++ b/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee
    @@ -37,7 +37,7 @@ module.exports =
         unless @fuzzyFinderView
           FuzzyFinderView  = require 'fuzzy-finder/lib/fuzzy-finder-view'
           @fuzzyFinderView = new FuzzyFinderView()
    -      if @projectPaths? and not @fuzzyFinderView.projectPaths?
    +      if @projectPaths?.length > 0 and not @fuzzyFinderView.projectPaths?
             @fuzzyFinderView.projectPaths = @projectPaths
             @fuzzyFinderView.reloadProjectPaths = false
         @fuzzyFinderView
    
    From 9330276ae97c923f8fe8fa8b2472644787b1ab08 Mon Sep 17 00:00:00 2001
    From: Kevin Sawicki 
    Date: Mon, 11 Mar 2013 19:45:38 -0700
    Subject: [PATCH 293/308] Abort task when view is created
    
    ---
     src/packages/fuzzy-finder/lib/fuzzy-finder.coffee | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee b/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee
    index fbb180020..d90ff497d 100644
    --- a/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee
    +++ b/src/packages/fuzzy-finder/lib/fuzzy-finder.coffee
    @@ -35,6 +35,7 @@ module.exports =
     
       createView:  ->
         unless @fuzzyFinderView
    +      @loadPathsTask?.abort()
           FuzzyFinderView  = require 'fuzzy-finder/lib/fuzzy-finder-view'
           @fuzzyFinderView = new FuzzyFinderView()
           if @projectPaths?.length > 0 and not @fuzzyFinderView.projectPaths?
    
    From 21cdde11889d3d0be458c9c7aeb9f11f506730bf Mon Sep 17 00:00:00 2001
    From: "Kevin R. Barnes" 
    Date: Tue, 12 Mar 2013 09:40:05 -0700
    Subject: [PATCH 294/308] Update relative link for key bindings
    
    ---
     docs/getting-started.md | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/docs/getting-started.md b/docs/getting-started.md
    index 507b6c653..b5abb2d63 100644
    --- a/docs/getting-started.md
    +++ b/docs/getting-started.md
    @@ -11,7 +11,7 @@ always hit `meta-p` to bring up a list of commands that are relevant to the
     currently focused UI element. If there is a key binding for a given command, it
     is also displayed. This is a great way to explore the system and get to know the
     key commands interactively. If you'd like to add or change a binding for a
    -command, refer to the [keymaps](#keymaps) section to learn how.
    +command, refer to the [key bindings](#customizing-key-bindings) section to learn how.
     
     ![Command Palette](http://f.cl.ly/items/32041o3w471F3C0F0V2O/Screen%20Shot%202013-02-13%20at%207.27.41%20PM.png)
     
    
    From 9860f32d4cc5d185998569aa3ddd62b3c53aa019 Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Tue, 12 Mar 2013 15:41:52 -0700
    Subject: [PATCH 295/308] Add cancel callback to promptToSaveItem
    
    ---
     src/app/pane.coffee | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/src/app/pane.coffee b/src/app/pane.coffee
    index 240540042..c1397b89e 100644
    --- a/src/app/pane.coffee
    +++ b/src/app/pane.coffee
    @@ -159,13 +159,13 @@ class Pane extends View
       destroyInactiveItems: ->
         @destroyItem(item) for item in @getItems() when item isnt @activeItem
     
    -  promptToSaveItem: (item, nextAction) ->
    +  promptToSaveItem: (item, nextAction, cancelAction) ->
         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
    +      "Cancel", cancelAction
           "Don't Save", nextAction
         )
     
    
    From 44d4dc7e60255857d1c1d3695a1de43992a8288f Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Tue, 12 Mar 2013 15:42:25 -0700
    Subject: [PATCH 296/308] Use URI in prompt message if pane item doesn't have a
     title
    
    ---
     src/app/pane.coffee | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/src/app/pane.coffee b/src/app/pane.coffee
    index c1397b89e..3bd9c9f4c 100644
    --- a/src/app/pane.coffee
    +++ b/src/app/pane.coffee
    @@ -162,7 +162,7 @@ class Pane extends View
       promptToSaveItem: (item, nextAction, cancelAction) ->
         uri = item.getUri()
         atom.confirm(
    -      "'#{item.getTitle()}' has changes, do you want to save them?"
    +      "'#{item.getTitle?() ? item.getUri()}' has changes, do you want to save them?"
           "Your changes will be lost if close this item without saving."
           "Save", => @saveItem(item, nextAction)
           "Cancel", cancelAction
    
    From d916962a80a5d0c2544f9b3fc5816557ab7730d5 Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Tue, 12 Mar 2013 15:43:37 -0700
    Subject: [PATCH 297/308] Defer window close events until all modified pane
     items are handled
    
    If the user presses cancel, the close event is canceled
    ---
     spec/app/pane-container-spec.coffee | 37 +++++++++++++++++++++++++++++
     spec/app/window-spec.coffee         | 21 +++++++++++++++-
     src/app/pane-container.coffee       | 17 +++++++++++++
     src/app/root-view.coffee            |  3 +++
     src/app/window.coffee               |  6 ++++-
     5 files changed, 82 insertions(+), 2 deletions(-)
    
    diff --git a/spec/app/pane-container-spec.coffee b/spec/app/pane-container-spec.coffee
    index c448ca3d2..708beb081 100644
    --- a/spec/app/pane-container-spec.coffee
    +++ b/spec/app/pane-container-spec.coffee
    @@ -136,6 +136,43 @@ describe "PaneContainer", ->
             for item in pane.getItems()
               expect(item.saved).toBeTruthy()
     
    +  describe ".confirmClose()", ->
    +    it "resolves the returned promise after modified files are saved", ->
    +      pane1.itemAtIndex(0).isModified = -> true
    +      pane2.itemAtIndex(0).isModified = -> true
    +      spyOn(atom, "confirm").andCallFake (a, b, c, d, e, f, g, noSaveFn) -> noSaveFn()
    +
    +      promiseHandler = jasmine.createSpy("promiseHandler")
    +      failedPromiseHandler = jasmine.createSpy("failedPromiseHandler")
    +      promise = container.confirmClose()
    +      promise.done promiseHandler
    +      promise.fail failedPromiseHandler
    +
    +      waitsFor ->
    +        promiseHandler.wasCalled
    +
    +      runs ->
    +        expect(failedPromiseHandler).not.toHaveBeenCalled()
    +        expect(atom.confirm).toHaveBeenCalled()
    +
    +    it "rejects the returned promise if the user cancels saving", ->
    +      pane1.itemAtIndex(0).isModified = -> true
    +      pane2.itemAtIndex(0).isModified = -> true
    +      spyOn(atom, "confirm").andCallFake (a, b, c, d, e, cancelFn, f, g) -> cancelFn()
    +
    +      promiseHandler = jasmine.createSpy("promiseHandler")
    +      failedPromiseHandler = jasmine.createSpy("failedPromiseHandler")
    +      promise = container.confirmClose()
    +      promise.done promiseHandler
    +      promise.fail failedPromiseHandler
    +
    +      waitsFor ->
    +        failedPromiseHandler.wasCalled
    +
    +      runs ->
    +        expect(promiseHandler).not.toHaveBeenCalled()
    +        expect(atom.confirm).toHaveBeenCalled()
    +
       describe "serialization", ->
         it "can be serialized and deserialized, and correctly adjusts dimensions of deserialized panes after attach", ->
           newContainer = deserialize(container.serialize())
    diff --git a/spec/app/window-spec.coffee b/spec/app/window-spec.coffee
    index 5687cb3d4..20a93ca55 100644
    --- a/spec/app/window-spec.coffee
    +++ b/spec/app/window-spec.coffee
    @@ -35,7 +35,7 @@ describe "Window", ->
             $(window).trigger 'focus'
             expect($("body")).not.toHaveClass("is-blurred")
     
    -  describe ".close()", ->
    +  describe "window close events", ->
         it "is triggered by the 'core:close' event", ->
           spyOn window, 'close'
           $(window).trigger 'core:close'
    @@ -46,6 +46,25 @@ describe "Window", ->
           $(window).trigger 'window:close'
           expect(window.close).toHaveBeenCalled()
     
    +    describe "when modified buffers exist", ->
    +      it "prompts user to save and aborts if prompt is canceled", ->
    +        spyOn(window, 'close')
    +        spyOn(atom, "confirm").andCallFake (a, b, c, d, e, cancel) -> cancel()
    +        editSession = rootView.open("sample.js")
    +        editSession.insertText("I look different, I feel different.")
    +        $(window).trigger 'window:close'
    +        expect(window.close).not.toHaveBeenCalled()
    +        expect(atom.confirm).toHaveBeenCalled()
    +
    +      it "prompts user to save and closes", ->
    +        spyOn(window, 'close')
    +        spyOn(atom, "confirm").andCallFake (a, b, c, d, e, f, g, noSave) -> noSave()
    +        editSession = rootView.open("sample.js")
    +        editSession.insertText("I look different, I feel different.")
    +        $(window).trigger 'window:close'
    +        expect(window.close).toHaveBeenCalled()
    +        expect(atom.confirm).toHaveBeenCalled()
    +
       describe ".reload()", ->
         beforeEach ->
           spyOn($native, "reload")
    diff --git a/src/app/pane-container.coffee b/src/app/pane-container.coffee
    index 225716d0c..2118af2b0 100644
    --- a/src/app/pane-container.coffee
    +++ b/src/app/pane-container.coffee
    @@ -62,6 +62,23 @@ class PaneContainer extends View
       saveAll: ->
         pane.saveItems() for pane in @getPanes()
     
    +  confirmClose: ->
    +    deferred = $.Deferred()
    +    modifiedItems = []
    +    for pane in @getPanes()
    +      modifiedItems.push(item) for item in pane.getItems() when item.isModified?()
    +
    +    cancel = => deferred.reject()
    +    saveNextModifiedItem = =>
    +      if modifiedItems.length == 0
    +        deferred.resolve()
    +      else
    +        item = modifiedItems.pop()
    +        @paneAtIndex(0).promptToSaveItem item, saveNextModifiedItem, cancel
    +
    +    saveNextModifiedItem()
    +    deferred.promise()
    +
       getPanes: ->
         @find('.pane').views()
     
    diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee
    index 659b331c9..9c2eaae40 100644
    --- a/src/app/root-view.coffee
    +++ b/src/app/root-view.coffee
    @@ -75,6 +75,9 @@ class RootView extends View
         panes: @panes.serialize()
         packages: atom.serializeAtomPackages()
     
    +  confirmClose: ->
    +    @panes.confirmClose()
    +
       handleFocus: (e) ->
         if @getActivePane()
           @getActivePane().focus()
    diff --git a/src/app/window.coffee b/src/app/window.coffee
    index 562dbe288..ded8aaefb 100644
    --- a/src/app/window.coffee
    +++ b/src/app/window.coffee
    @@ -88,10 +88,10 @@ window.installAtomCommand = (commandPath) ->
     
     window.handleWindowEvents = ->
       $(window).on 'core:close', => window.close()
    -  $(window).command 'window:close', => window.close()
       $(window).command 'window:toggle-full-screen', => atom.toggleFullScreen()
       $(window).on 'focus', -> $("body").removeClass('is-blurred')
       $(window).on 'blur',  -> $("body").addClass('is-blurred')
    +  $(window).command 'window:close', => confirmClose()
     
     window.buildProjectAndRootView = ->
       RootView = require 'root-view'
    @@ -189,3 +189,7 @@ window.measure = (description, fn) ->
       result = new Date().getTime() - start
       console.log description, result
       value
    +
    +
    +confirmClose = ->
    +  rootView.confirmClose().done -> window.close()
    \ No newline at end of file
    
    From 4755233f92ddb01115c88efe31c10aae58065086 Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Tue, 12 Mar 2013 15:54:07 -0700
    Subject: [PATCH 298/308] :lipstick:
    
    ---
     spec/app/window-spec.coffee | 36 ++++++++++++++++--------------------
     1 file changed, 16 insertions(+), 20 deletions(-)
    
    diff --git a/spec/app/window-spec.coffee b/spec/app/window-spec.coffee
    index 20a93ca55..4abcf6d5b 100644
    --- a/spec/app/window-spec.coffee
    +++ b/spec/app/window-spec.coffee
    @@ -35,28 +35,15 @@ describe "Window", ->
             $(window).trigger 'focus'
             expect($("body")).not.toHaveClass("is-blurred")
     
    -  describe "window close events", ->
    -    it "is triggered by the 'core:close' event", ->
    -      spyOn window, 'close'
    -      $(window).trigger 'core:close'
    -      expect(window.close).toHaveBeenCalled()
    -
    -    it "is triggered by the 'window:close event'", ->
    -      spyOn window, 'close'
    -      $(window).trigger 'window:close'
    -      expect(window.close).toHaveBeenCalled()
    -
    -    describe "when modified buffers exist", ->
    -      it "prompts user to save and aborts if prompt is canceled", ->
    -        spyOn(window, 'close')
    -        spyOn(atom, "confirm").andCallFake (a, b, c, d, e, cancel) -> cancel()
    -        editSession = rootView.open("sample.js")
    -        editSession.insertText("I look different, I feel different.")
    +  describe "window:close event", ->
    +    describe "when no pane items are modified", ->
    +      it "calls window.close", ->
    +        spyOn window, 'close'
             $(window).trigger 'window:close'
    -        expect(window.close).not.toHaveBeenCalled()
    -        expect(atom.confirm).toHaveBeenCalled()
    +        expect(window.close).toHaveBeenCalled()
     
    -      it "prompts user to save and closes", ->
    +    describe "when pane items are are modified", ->
    +      it "prompts user to save and and calls window.close", ->
             spyOn(window, 'close')
             spyOn(atom, "confirm").andCallFake (a, b, c, d, e, f, g, noSave) -> noSave()
             editSession = rootView.open("sample.js")
    @@ -65,6 +52,15 @@ describe "Window", ->
             expect(window.close).toHaveBeenCalled()
             expect(atom.confirm).toHaveBeenCalled()
     
    +      it "prompts user to save and aborts if dialog is canceled", ->
    +        spyOn(window, 'close')
    +        spyOn(atom, "confirm").andCallFake (a, b, c, d, e, cancel) -> cancel()
    +        editSession = rootView.open("sample.js")
    +        editSession.insertText("I look different, I feel different.")
    +        $(window).trigger 'window:close'
    +        expect(window.close).not.toHaveBeenCalled()
    +        expect(atom.confirm).toHaveBeenCalled()
    +
       describe ".reload()", ->
         beforeEach ->
           spyOn($native, "reload")
    
    From f7f034ad2ac52fbd1ebc2a7389645fc802d0a4bf Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Tue, 12 Mar 2013 15:54:22 -0700
    Subject: [PATCH 299/308] Remove core:close event from window
    
    ---
     src/app/window.coffee | 1 -
     1 file changed, 1 deletion(-)
    
    diff --git a/src/app/window.coffee b/src/app/window.coffee
    index ded8aaefb..bba97a751 100644
    --- a/src/app/window.coffee
    +++ b/src/app/window.coffee
    @@ -87,7 +87,6 @@ window.installAtomCommand = (commandPath) ->
         ChildProcess.exec("chmod u+x '#{commandPath}'")
     
     window.handleWindowEvents = ->
    -  $(window).on 'core:close', => window.close()
       $(window).command 'window:toggle-full-screen', => atom.toggleFullScreen()
       $(window).on 'focus', -> $("body").removeClass('is-blurred')
       $(window).on 'blur',  -> $("body").addClass('is-blurred')
    
    From c236325c1a5f2753cbc08be834d734174eda0029 Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Tue, 12 Mar 2013 16:47:26 -0700
    Subject: [PATCH 300/308] Log errors (instead of crashing) when the config file
     cannot be parsed
    
    Also, config won't overwrite changes to config.cson when the file can not be parsed. Closes #401
    ---
     spec/app/config-spec.coffee | 19 +++++++++++++++++++
     src/app/config.coffee       | 11 +++++++++--
     2 files changed, 28 insertions(+), 2 deletions(-)
    
    diff --git a/spec/app/config-spec.coffee b/spec/app/config-spec.coffee
    index 7062c4042..39aae22fe 100644
    --- a/spec/app/config-spec.coffee
    +++ b/spec/app/config-spec.coffee
    @@ -1,3 +1,4 @@
    +Config = require 'config'
     fs = require 'fs'
     
     describe "Config", ->
    @@ -133,3 +134,21 @@ describe "Config", ->
             expect(fs.isFile(fs.join(config.configDirPath, 'themes/atom-light-ui/package.cson'))).toBeTruthy()
             expect(fs.isFile(fs.join(config.configDirPath, 'themes/atom-dark-syntax.css'))).toBeTruthy()
             expect(fs.isFile(fs.join(config.configDirPath, 'themes/atom-light-syntax.css'))).toBeTruthy()
    +
    +  describe "when the config file is not parseable", ->
    +    beforeEach ->
    +     config.configDirPath = '/tmp/dot-atom-dir'
    +     config.configFilePath = fs.join(config.configDirPath, "config.cson")
    +     expect(fs.exists(config.configDirPath)).toBeFalsy()
    +
    +    afterEach ->
    +      fs.remove('/tmp/dot-atom-dir') if fs.exists('/tmp/dot-atom-dir')
    +
    +    it "logs an error to the console and does not overwrite the config file", ->
    +      config.save.reset()
    +      spyOn(console, 'error')
    +      fs.write(config.configFilePath, "{{{{{")
    +      config.loadUserConfig()
    +      config.set("hair", "blonde") # trigger a save
    +      expect(console.error).toHaveBeenCalled()
    +      expect(config.save).not.toHaveBeenCalled()
    \ No newline at end of file
    diff --git a/src/app/config.coffee b/src/app/config.coffee
    index 7c22c362e..dc79df9ee 100644
    --- a/src/app/config.coffee
    +++ b/src/app/config.coffee
    @@ -20,6 +20,7 @@ class Config
       userPackagesDirPath: userPackagesDirPath
       defaultSettings: null
       settings: null
    +  configFileHasErrors: null
     
       constructor: ->
         @defaultSettings =
    @@ -55,8 +56,13 @@ class Config
     
       loadUserConfig: ->
         if fs.exists(@configFilePath)
    -      userConfig = fs.readObject(@configFilePath)
    -      _.extend(@settings, userConfig)
    +      try
    +        userConfig = fs.readObject(@configFilePath)
    +        _.extend(@settings, userConfig)
    +      catch e
    +        @configFileHasErrors = true
    +        console.error "Failed to load user config '#{@configFilePath}'", e.message
    +        console.error e.stack
     
       get: (keyPath) ->
         _.valueForKeyPath(@settings, keyPath) ?
    @@ -92,6 +98,7 @@ class Config
         subscription
     
       update: ->
    +    return if @configFileHasErrors
         @save()
         @trigger 'updated'
     
    
    From 3ec74f32113d2d30a805f8de0bd2e5215222dc03 Mon Sep 17 00:00:00 2001
    From: Corey Johnson 
    Date: Tue, 12 Mar 2013 16:55:28 -0700
    Subject: [PATCH 301/308] Move `toExistOnDisk` matcher to spec helper
    
    ---
     spec/spec-helper.coffee                                     | 5 +++++
     .../package-generator/spec/package-generator-spec.coffee    | 6 ------
     2 files changed, 5 insertions(+), 6 deletions(-)
    
    diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee
    index 1dd2c4a18..8518fa6c3 100644
    --- a/spec/spec-helper.coffee
    +++ b/spec/spec-helper.coffee
    @@ -137,6 +137,11 @@ addCustomMatchers = (spec) ->
           this.message = => "Expected object with length #{@actual.length} to#{notText} have length #{expected}"
           @actual.length == expected
     
    +    toExistOnDisk: (expected) ->
    +      notText = this.isNot and " not" or ""
    +      @message = -> return "Expected path '" + @actual + "'" + notText + " to exist."
    +      fs.exists(@actual)
    +
     window.keyIdentifierForKey = (key) ->
       if key.length > 1 # named key
         key
    diff --git a/src/packages/package-generator/spec/package-generator-spec.coffee b/src/packages/package-generator/spec/package-generator-spec.coffee
    index cae5d5d54..c9dd0c727 100644
    --- a/src/packages/package-generator/spec/package-generator-spec.coffee
    +++ b/src/packages/package-generator/spec/package-generator-spec.coffee
    @@ -37,12 +37,6 @@ describe 'Package Generator', ->
           packagePath = "/tmp/atom-packages/#{packageName}"
           fs.remove(packagePath) if fs.exists(packagePath)
     
    -      @addMatchers
    -        toExistOnDisk: (expected) ->
    -          notText = this.isNot and " not" or ""
    -          @message = -> return "Expected path '" + @actual + "'" + notText + " to exist."
    -          fs.exists(@actual)
    -
         afterEach ->
           fs.remove(packagePath) if fs.exists(packagePath)
     
    
    From 2b35eaa41404da6e19c9efdcd7c8360506bfb91a Mon Sep 17 00:00:00 2001
    From: Kevin Sawicki 
    Date: Wed, 13 Mar 2013 08:39:52 -0700
    Subject: [PATCH 302/308] Update python bundle to 70dd4be
    
    Adds .gypi to file types
    ---
     vendor/packages/python.tmbundle | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/vendor/packages/python.tmbundle b/vendor/packages/python.tmbundle
    index df88cd66d..70dd4be1f 160000
    --- a/vendor/packages/python.tmbundle
    +++ b/vendor/packages/python.tmbundle
    @@ -1 +1 @@
    -Subproject commit df88cd66d00ed44b1d1a212a347334bb8308299c
    +Subproject commit 70dd4be1f12d6e5b2f9238f04e38567f7cebfe4c
    
    From 460577d9ae0a71bd65eb7ad327ea6b2d12c0bc4f Mon Sep 17 00:00:00 2001
    From: John Barnette 
    Date: Wed, 13 Mar 2013 19:40:35 -0700
    Subject: [PATCH 303/308] Spike optional doc: key for command
    
    This isn't working yet.
    ---
     src/stdlib/jquery-extensions.coffee     | 33 +++++++++++++++++++------
     src/stdlib/underscore-extensions.coffee |  8 ++++--
     2 files changed, 32 insertions(+), 9 deletions(-)
    
    diff --git a/src/stdlib/jquery-extensions.coffee b/src/stdlib/jquery-extensions.coffee
    index fe403c2a9..95c882471 100644
    --- a/src/stdlib/jquery-extensions.coffee
    +++ b/src/stdlib/jquery-extensions.coffee
    @@ -59,7 +59,12 @@ $.fn.trueHeight = ->
     $.fn.trueWidth = ->
       this[0].getBoundingClientRect().width
     
    -$.fn.document = (eventDescriptions) ->
    +$.fn.document = (eventDescriptions, optionalDoc) ->
    +  if optionalDoc
    +    eventName = eventDescriptions
    +    eventDescriptions = {}
    +    eventDescriptions[eventName] = optionalDoc
    +
       @data('documentation', {}) unless @data('documentation')
       _.extend(@data('documentation'), eventDescriptions)
     
    @@ -75,12 +80,26 @@ $.fn.events = ->
       else
         events
     
    -$.fn.command = (args...) ->
    -  eventName = args[0]
    -  documentation = {}
    -  documentation[eventName] = _.humanizeEventName(eventName)
    -  @document(documentation)
    -  @on(args...)
    +# Valid calling styles:
    +# command(eventName, handler)
    +# command(eventName, selector, handler)
    +# command(eventName, options, handler)
    +# command(eventName, selector, options, handler)
    +$.fn.command = (eventName, selector, options, handler) ->
    +  if not options? and not handler?
    +    handler  = selector
    +    selector = null
    +  else if not handler?
    +    handler = options
    +    options = null
    +
    +  if selector? and typeof(selector) is 'object'
    +    handler  = options
    +    options  = selector
    +    selector = null
    +
    +  @document(eventName, _.humanizeEventName(eventName, options?["xxx"]))
    +  @on(eventName, selector, options?['data'], handler)
     
     $.fn.iconSize = (size) ->
       @width(size).height(size).css('font-size', size)
    diff --git a/src/stdlib/underscore-extensions.coffee b/src/stdlib/underscore-extensions.coffee
    index df5865b24..824424099 100644
    --- a/src/stdlib/underscore-extensions.coffee
    +++ b/src/stdlib/underscore-extensions.coffee
    @@ -52,10 +52,14 @@ _.mixin
         regex = RegExp('[' + specials.join('\\') + ']', 'g')
         string.replace(regex, "\\$&");
     
    -  humanizeEventName: (eventName) ->
    +  humanizeEventName: (eventName, optionalDocString) ->
    +    return "GitHub" if eventName.toLowerCase() is "github"
    +
         if /:/.test(eventName)
           [namespace, name] = eventName.split(':')
    -      return "#{@humanizeEventName(namespace)}: #{@humanizeEventName(name)}"
    +      return "#{@humanizeEventName(namespace)}: #{@humanizeEventName(name, optionalDocString)}"
    +
    +    return optionalDocString if not _.isEmpty(optionalDocString)
     
         words = eventName.split('-')
         words.map(_.capitalize).join(' ')
    
    From eb5d0fe3f5bbb774aaee1c9fc5d6528b90adb466 Mon Sep 17 00:00:00 2001
    From: John Barnette 
    Date: Wed, 13 Mar 2013 21:30:00 -0700
    Subject: [PATCH 304/308] Actually make doc: work
    
    ---
     src/stdlib/jquery-extensions.coffee     | 10 ++--------
     src/stdlib/underscore-extensions.coffee | 16 +++++++++-------
     2 files changed, 11 insertions(+), 15 deletions(-)
    
    diff --git a/src/stdlib/jquery-extensions.coffee b/src/stdlib/jquery-extensions.coffee
    index 95c882471..80397f975 100644
    --- a/src/stdlib/jquery-extensions.coffee
    +++ b/src/stdlib/jquery-extensions.coffee
    @@ -80,13 +80,8 @@ $.fn.events = ->
       else
         events
     
    -# Valid calling styles:
    -# command(eventName, handler)
    -# command(eventName, selector, handler)
    -# command(eventName, options, handler)
    -# command(eventName, selector, options, handler)
     $.fn.command = (eventName, selector, options, handler) ->
    -  if not options? and not handler?
    +  if not options?
         handler  = selector
         selector = null
       else if not handler?
    @@ -94,11 +89,10 @@ $.fn.command = (eventName, selector, options, handler) ->
         options = null
     
       if selector? and typeof(selector) is 'object'
    -    handler  = options
         options  = selector
         selector = null
     
    -  @document(eventName, _.humanizeEventName(eventName, options?["xxx"]))
    +  @document(eventName, _.humanizeEventName(eventName, options?["doc"]))
       @on(eventName, selector, options?['data'], handler)
     
     $.fn.iconSize = (size) ->
    diff --git a/src/stdlib/underscore-extensions.coffee b/src/stdlib/underscore-extensions.coffee
    index 824424099..f490b7715 100644
    --- a/src/stdlib/underscore-extensions.coffee
    +++ b/src/stdlib/underscore-extensions.coffee
    @@ -52,17 +52,16 @@ _.mixin
         regex = RegExp('[' + specials.join('\\') + ']', 'g')
         string.replace(regex, "\\$&");
     
    -  humanizeEventName: (eventName, optionalDocString) ->
    +  humanizeEventName: (eventName, eventDoc) ->
         return "GitHub" if eventName.toLowerCase() is "github"
     
    -    if /:/.test(eventName)
    -      [namespace, name] = eventName.split(':')
    -      return "#{@humanizeEventName(namespace)}: #{@humanizeEventName(name, optionalDocString)}"
    +    [namespace, event]  = eventName.split(':')
    +    return _.capitalize(namespace) unless event?
     
    -    return optionalDocString if not _.isEmpty(optionalDocString)
    +    namespaceDoc   = _.undasherize(namespace)
    +    eventDoc     ||= _.undasherize(event)
     
    -    words = eventName.split('-')
    -    words.map(_.capitalize).join(' ')
    +    "#{namespaceDoc}: #{eventDoc}"
     
       capitalize: (word) ->
         word[0].toUpperCase() + word[1..]
    @@ -84,6 +83,9 @@ _.mixin
           else
             "-"
     
    +  undasherize: (string) ->
    +    string.split('-').map(_.capitalize).join(' ')
    +
       underscore: (string) ->
         string = string[0].toLowerCase() + string[1..]
         string.replace /([A-Z])|(-)/g, (m, letter, dash) ->
    
    From 25839c5cf52bf3092dad99441c249441f23c8e5a Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Thu, 14 Mar 2013 11:15:55 -0600
    Subject: [PATCH 305/308] Add spec coverage for `$.fn.command`
    
    ---
     spec/stdlib/jquery-extensions-spec.coffee | 41 +++++++++++++++++++++++
     1 file changed, 41 insertions(+)
    
    diff --git a/spec/stdlib/jquery-extensions-spec.coffee b/spec/stdlib/jquery-extensions-spec.coffee
    index 4bdd8fd11..f0962a200 100644
    --- a/spec/stdlib/jquery-extensions-spec.coffee
    +++ b/spec/stdlib/jquery-extensions-spec.coffee
    @@ -77,6 +77,47 @@ describe 'jQuery extensions', ->
             'a1': "A1: Waste perfectly-good steak"
             'a2': null
     
    +  describe "$.fn.command(eventName, [selector, options,] handler)", ->
    +    [view, handler] = []
    +
    +    beforeEach ->
    +      view = $$ ->
    +        @div class: 'a', =>
    +          @div class: 'b'
    +          @div class: 'c'
    +      handler = jasmine.createSpy("commandHandler")
    +
    +    it "binds the handler to the given event / selector for all argument combinations", ->
    +      view.command 'test:foo', handler
    +      view.trigger 'test:foo'
    +      expect(handler).toHaveBeenCalled()
    +      handler.reset()
    +
    +      view.command 'test:bar', '.b', handler
    +      view.find('.b').trigger 'test:bar'
    +      view.find('.c').trigger 'test:bar'
    +      expect(handler.callCount).toBe 1
    +      handler.reset()
    +
    +      view.command 'test:baz', doc: 'Spaz', handler
    +      view.trigger 'test:baz'
    +      expect(handler).toHaveBeenCalled()
    +      handler.reset()
    +
    +      view.command 'test:quux', '.c', doc: 'Lorem', handler
    +      view.find('.b').trigger 'test:quux'
    +      view.find('.c').trigger 'test:quux'
    +      expect(handler.callCount).toBe 1
    +
    +    it "passes the 'data' option through when binding the event handler", ->
    +      view.command 'test:foo', data: "bar", handler
    +      view.trigger 'test:foo'
    +      expect(handler.argsForCall[0][0].data).toBe 'bar'
    +
    +    it "sets a custom docstring if the 'doc' option is specified", ->
    +      view.command 'test:foo', doc: "Foo!", handler
    +      expect(view.events()).toEqual 'test:foo': 'Test: Foo!'
    +
       describe "$.fn.scrollUp/Down/ToTop/ToBottom", ->
         it "scrolls the element in the specified way if possible", ->
           view = $$ -> @div => _.times 20, => @div('A')
    
    From 634117ed66edd0be5ab4001c58732b17cd36dc34 Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Thu, 14 Mar 2013 11:34:28 -0600
    Subject: [PATCH 306/308] Make `$.fn.document` always take event name / doc
     string args
    
    It's simpler and we don't use the other syntax right now.
    ---
     spec/stdlib/jquery-extensions-spec.coffee | 14 ++++++--------
     src/stdlib/jquery-extensions.coffee       |  9 +++------
     2 files changed, 9 insertions(+), 14 deletions(-)
    
    diff --git a/spec/stdlib/jquery-extensions-spec.coffee b/spec/stdlib/jquery-extensions-spec.coffee
    index f0962a200..3b7ce522c 100644
    --- a/spec/stdlib/jquery-extensions-spec.coffee
    +++ b/spec/stdlib/jquery-extensions-spec.coffee
    @@ -42,7 +42,7 @@ describe 'jQuery extensions', ->
             element.trigger 'foo'
             expect(events).toEqual [2,1,3]
     
    -  describe "$.fn.events() and $.fn.document", ->
    +  describe "$.fn.events() and $.fn.document(...)", ->
         it "returns a list of all events being listened for on the target node or its ancestors, along with their documentation string", ->
           view = $$ ->
             @div id: 'a', =>
    @@ -50,20 +50,18 @@ describe 'jQuery extensions', ->
                 @div id: 'c'
               @div id: 'd'
     
    -      view.document
    -        'a1': "This is event A2"
    -        'b2': "This is event b2"
    +      view.document 'a1', "This is event A2"
    +      view.document 'b2', "This is event b2"
     
    -      view.document 'a1': "A1: Waste perfectly-good steak"
    +      view.document 'a1', "A1: Waste perfectly-good steak"
           view.on 'a1', ->
           view.on 'a2', ->
           view.on 'b1', -> # should not appear as a duplicate
     
           divB = view.find('#b')
     
    -      divB.document
    -        'b1': "B1: Super-sonic bomber"
    -        'b2': "B2: Looks evil. Kinda is."
    +      divB.document 'b1', "B1: Super-sonic bomber"
    +      divB.document 'b2', "B2: Looks evil. Kinda is."
           divB.on 'b1', ->
           divB.on 'b2', ->
     
    diff --git a/src/stdlib/jquery-extensions.coffee b/src/stdlib/jquery-extensions.coffee
    index 80397f975..70d24fc3a 100644
    --- a/src/stdlib/jquery-extensions.coffee
    +++ b/src/stdlib/jquery-extensions.coffee
    @@ -59,12 +59,9 @@ $.fn.trueHeight = ->
     $.fn.trueWidth = ->
       this[0].getBoundingClientRect().width
     
    -$.fn.document = (eventDescriptions, optionalDoc) ->
    -  if optionalDoc
    -    eventName = eventDescriptions
    -    eventDescriptions = {}
    -    eventDescriptions[eventName] = optionalDoc
    -
    +$.fn.document = (eventName, docString) ->
    +  eventDescriptions = {}
    +  eventDescriptions[eventName] = docString
       @data('documentation', {}) unless @data('documentation')
       _.extend(@data('documentation'), eventDescriptions)
     
    
    From a1882ffd1f1cc2b1cfb66cb5d7f03c898a1264bf Mon Sep 17 00:00:00 2001
    From: Nathan Sobo 
    Date: Thu, 14 Mar 2013 11:35:06 -0600
    Subject: [PATCH 307/308] Move "GitHub" special-case to `_.capitalize`. Add
     specs.
    
    ---
     spec/stdlib/jquery-extensions-spec.coffee |  4 ++++
     src/stdlib/underscore-extensions.coffee   | 11 ++++++-----
     2 files changed, 10 insertions(+), 5 deletions(-)
    
    diff --git a/spec/stdlib/jquery-extensions-spec.coffee b/spec/stdlib/jquery-extensions-spec.coffee
    index 3b7ce522c..b803de4f1 100644
    --- a/spec/stdlib/jquery-extensions-spec.coffee
    +++ b/spec/stdlib/jquery-extensions-spec.coffee
    @@ -116,6 +116,10 @@ describe 'jQuery extensions', ->
           view.command 'test:foo', doc: "Foo!", handler
           expect(view.events()).toEqual 'test:foo': 'Test: Foo!'
     
    +    it "capitalizes the 'github' prefix how we like it", ->
    +      view.command 'github:spelling', handler
    +      expect(view.events()).toEqual 'github:spelling': 'GitHub: Spelling'
    +
       describe "$.fn.scrollUp/Down/ToTop/ToBottom", ->
         it "scrolls the element in the specified way if possible", ->
           view = $$ -> @div => _.times 20, => @div('A')
    diff --git a/src/stdlib/underscore-extensions.coffee b/src/stdlib/underscore-extensions.coffee
    index f490b7715..09c79c4f5 100644
    --- a/src/stdlib/underscore-extensions.coffee
    +++ b/src/stdlib/underscore-extensions.coffee
    @@ -53,18 +53,19 @@ _.mixin
         string.replace(regex, "\\$&");
     
       humanizeEventName: (eventName, eventDoc) ->
    -    return "GitHub" if eventName.toLowerCase() is "github"
    -
         [namespace, event]  = eventName.split(':')
         return _.capitalize(namespace) unless event?
     
    -    namespaceDoc   = _.undasherize(namespace)
    -    eventDoc     ||= _.undasherize(event)
    +    namespaceDoc = _.undasherize(namespace)
    +    eventDoc ?= _.undasherize(event)
     
         "#{namespaceDoc}: #{eventDoc}"
     
       capitalize: (word) ->
    -    word[0].toUpperCase() + word[1..]
    +    if word.toLowerCase() is 'github'
    +      'GitHub'
    +    else
    +      word[0].toUpperCase() + word[1..]
     
       pluralize: (count=0, singular, plural=singular+'s') ->
         if count is 1
    
    From 09259a579d4bb8d68a4a58851c3904aaa3742f7b Mon Sep 17 00:00:00 2001
    From: John Barnette 
    Date: Thu, 14 Mar 2013 18:06:55 -0700
    Subject: [PATCH 308/308] Let's not accidentally publish this :heart:
    
    ---
     package.json | 2 ++
     1 file changed, 2 insertions(+)
    
    diff --git a/package.json b/package.json
    index 1893b5e86..6633d19bd 100644
    --- a/package.json
    +++ b/package.json
    @@ -6,6 +6,8 @@
         "coffee-script": "1.5"
       },
     
    +  "private": true,
    +
       "scripts": {
         "preinstall": "true"
       }