diff --git a/.atom/config.cson b/.atom/config.cson new file mode 100644 index 000000000..a93b83e66 --- /dev/null +++ b/.atom/config.cson @@ -0,0 +1,7 @@ +'editor': + 'fontSize': 16 +'core': + 'themes': [ + 'atom-dark-ui' + 'atom-dark-syntax' + ] diff --git a/.atom/packages/README.md b/.atom/packages/README.md new file mode 100644 index 000000000..540b6949c --- /dev/null +++ b/.atom/packages/README.md @@ -0,0 +1 @@ +All packages in this directory will be automatically loaded diff --git a/.atom/packages/Readme.md b/.atom/packages/Readme.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/.atom/themes/README.md b/.atom/themes/README.md new file mode 100644 index 000000000..dbedd5e13 --- /dev/null +++ b/.atom/themes/README.md @@ -0,0 +1 @@ +All themes in this directory will be automatically loaded diff --git a/.atom/atom.coffee b/.atom/user.coffee similarity index 100% rename from .atom/atom.coffee rename to .atom/user.coffee diff --git a/.atom/user.css b/.atom/user.css new file mode 100644 index 000000000..d63ad257e --- /dev/null +++ b/.atom/user.css @@ -0,0 +1,8 @@ +/* User styles */ +.tree-view { + +} + +.editor { + +} diff --git a/.gitmodules b/.gitmodules index caa40d8c9..6495d34a2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -43,3 +43,6 @@ [submodule "vendor/packages/property-list.tmbundle"] path = vendor/packages/property-list.tmbundle url = https://github.com/textmate/property-list.tmbundle.git +[submodule "vendor/packages/python.tmbundle"] + path = vendor/packages/python.tmbundle + url = https://github.com/textmate/python.tmbundle diff --git a/.pairs b/.pairs index c939edd3c..ec4f2417c 100644 --- a/.pairs +++ b/.pairs @@ -5,6 +5,7 @@ pairs: ks: Kevin Sawicki; kevin jc: Jerry Cheung; jerry bl: Brian Lopez; brian + jp: Justin Palmer; justin email: domain: github.com #global: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..0c48073b2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,22 @@ +# :rotating_light: Contributing to Atom :rotating_light: + +## Issues + * Include screenshots and animated GIFs whenever possible, they are immensely + helpful + * Include the behavior you expected to happen and other places you've seen + that behavior such as Emacs, vi, Xcode, etc. + * Check the Console app for stack traces to include if reporting a crash + * Check the Dev tools (`alt-cmd-i`) for errors and stack traces to include + +## Code + * Follow the [JavaScript](https://github.com/styleguide/javascript), + [CSS](https://github.com/styleguide/css), + and [Objective-C](https://github.com/github/objective-c-conventions) + styleguides + * Include thoughtfully worded [Jasmine](http://pivotal.github.com/jasmine/) + specs + * Style new elements in both the light and dark default themes when + appropriate + * New packages go in `src/packages/` + * Add 3rd-party packages by submoduling in `vendor/packages/` + * Commit messages are in the present tense diff --git a/README.md b/README.md index f6e23611e..04459faa3 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Requirements Atom is installed! Type `atom [path]` from the commmand line or find it in `/Applications/Atom.app` ## Your ~/.atom Directory -A basic ~/.atom directory is installed when you run `rake install`. Take a look at ~/.atom/atom.coffee for more information. +A basic ~/.atom directory is installed when you run `rake install`. Take a look at ~/.atom/user.coffee for more information. ## Basic Keyboard shortcuts Atom doesn't have much in the way of menus yet. Use these keyboard shortcuts to @@ -53,7 +53,7 @@ Most default OS X keybindings also work. ## Init Script -Atom will require `~/.atom/atom.coffee` whenever a window is opened or reloaded if it is present in your +Atom will require `~/.atom/user.coffee` whenever a window is opened or reloaded if it is present in your home directory. This is a rudimentary jumping off point for your own customizations. ## Command Panel diff --git a/Rakefile b/Rakefile index ee78786b2..e0dd5abd1 100644 --- a/Rakefile +++ b/Rakefile @@ -6,7 +6,7 @@ require 'erb' desc "Build Atom via `xcodebuild`" task :build => "create-project" do - command = "xcodebuild -target Atom configuration=Release SYMROOT=#{BUILD_DIR}" + command = "xcodebuild -target Atom -configuration Release SYMROOT=#{BUILD_DIR}" output = `#{command}` if $?.exitstatus != 0 $stderr.puts "Error #{$?.exitstatus}:\n#{output}" @@ -76,14 +76,26 @@ task "create-dot-atom" do dot_atom_template_path = ATOM_SRC_PATH + "/.atom" replace_dot_atom = false - next if File.exists?(DOT_ATOM_PATH) + + if File.exists?(DOT_ATOM_PATH) + user_config = "#{DOT_ATOM_PATH}/user.coffee" + old_user_config = "#{DOT_ATOM_PATH}/atom.coffee" + + if File.exists?(old_user_config) + `mv #{old_user_config} #{user_config}` + puts "\033[32mRenamed #{old_user_config} to #{user_config}\033[0m" + end + + next + end `rm -rf "#{DOT_ATOM_PATH}"` `mkdir "#{DOT_ATOM_PATH}"` - `cp "#{dot_atom_template_path}/atom.coffee" "#{DOT_ATOM_PATH}"` - `cp "#{dot_atom_template_path}/packages" "#{DOT_ATOM_PATH}"` + + `cp "#{dot_atom_template_path}/user.coffee" "#{DOT_ATOM_PATH}"` + `cp "#{dot_atom_template_path}/user.css" "#{DOT_ATOM_PATH}"` + `cp -r "#{dot_atom_template_path}/packages" "#{DOT_ATOM_PATH}"` `cp -r "#{ATOM_SRC_PATH}/themes" "#{DOT_ATOM_PATH}"` - `cp "#{ATOM_SRC_PATH}/vendor/themes/IR_Black.tmTheme" "#{DOT_ATOM_PATH}/themes"` end desc "Clone default bundles into vendor/bundles directory" @@ -113,7 +125,7 @@ end desc "Run the specs" task :test => ["clean", "clone-default-bundles"] do `pkill Atom` - Rake::Task["run"].invoke("--test") + Rake::Task["run"].invoke("--test --resource-path=#{ATOM_SRC_PATH}") end desc "Run the benchmarks" diff --git a/cef/Resources/cef.pak b/cef/Resources/cef.pak index f74178cd5..37fa7fb5e 100644 Binary files a/cef/Resources/cef.pak and b/cef/Resources/cef.pak differ diff --git a/cef/Resources/devtools_resources.pak b/cef/Resources/devtools_resources.pak index 4afa9320c..d56217fb7 100644 Binary files a/cef/Resources/devtools_resources.pak and b/cef/Resources/devtools_resources.pak differ diff --git a/cef/frameworks/ffmpegsumo.so b/cef/frameworks/ffmpegsumo.so index 77a27dc6a..1b77e36ea 100755 Binary files a/cef/frameworks/ffmpegsumo.so and b/cef/frameworks/ffmpegsumo.so differ diff --git a/cef/frameworks/libcef.dylib b/cef/frameworks/libcef.dylib index 772bb935c..edae81a14 100755 Binary files a/cef/frameworks/libcef.dylib and b/cef/frameworks/libcef.dylib differ diff --git a/cef/include/cef_version.h b/cef/include/cef_version.h index 8776f303f..d747de2f3 100644 --- a/cef/include/cef_version.h +++ b/cef/include/cef_version.h @@ -36,13 +36,13 @@ #define CEF_INCLUDE_CEF_VERSION_H_ #define CEF_VERSION_MAJOR 3 -#define CEF_REVISION 1050 +#define CEF_REVISION 1065 #define COPYRIGHT_YEAR 2013 #define CHROME_VERSION_MAJOR 25 #define CHROME_VERSION_MINOR 0 #define CHROME_VERSION_BUILD 1364 -#define CHROME_VERSION_PATCH 29 +#define CHROME_VERSION_PATCH 45 #define DO_MAKE_STRING(p) #p #define MAKE_STRING(p) DO_MAKE_STRING(p) diff --git a/docs/configuring-and-extending.md b/docs/configuring-and-extending.md index 7ecd60a04..35ece3c59 100644 --- a/docs/configuring-and-extending.md +++ b/docs/configuring-and-extending.md @@ -55,8 +55,8 @@ Or you can use `observeConfig` to track changes from a view object. ```coffeescript class MyView extends View initialize: -> - @observeConfig 'editor.lineHeight', (lineHeight) => - @adjustLineHeight(lineHeight) + @observeConfig 'editor.fontSize', () => + @adjustFontSize() ``` The `observeConfig` method will call the given callback immediately with the diff --git a/docs/packages/intro.md b/docs/packages/intro.md index 4f37c9446..7e4d20bfd 100644 --- a/docs/packages/intro.md +++ b/docs/packages/intro.md @@ -18,9 +18,9 @@ my-package/ index.coffee ``` -**NOTE: NPM behavior is partially implemented until we get a working Node.js +**NOTE:** NPM behavior is partially implemented until we get a working Node.js API built into Atom. The goal is to make Atom packages be a superset of NPM -packages** +packages #### package.json diff --git a/docs/styling.md b/docs/styling.md index 1632c4e54..47a3795ae 100644 --- a/docs/styling.md +++ b/docs/styling.md @@ -9,8 +9,8 @@ gutter. You can change the background color using the following CSS: ```css -.editor.focused .line.cursor-line, -.editor.focused .line-number.cursor-line { +.editor .line.cursor-line, +.editor .line-number.cursor-line { background-color: green; } ``` @@ -18,7 +18,7 @@ You can change the background color using the following CSS: You can change the line number foreground color using the following CSS: ```css -.editor.focused .line-number.cursor-line { +.editor .line-number.cursor-line { color: blue; } ``` diff --git a/git2/frameworks/libgit2.0.17.0.dylib b/git2/frameworks/libgit2.0.17.0.dylib index 6cee2cf63..f76d63a47 100755 Binary files a/git2/frameworks/libgit2.0.17.0.dylib and b/git2/frameworks/libgit2.0.17.0.dylib differ diff --git a/native/atom_cef_client.cpp b/native/atom_cef_client.cpp index 38c25470b..27badf291 100644 --- a/native/atom_cef_client.cpp +++ b/native/atom_cef_client.cpp @@ -80,6 +80,9 @@ bool AtomCefClient::OnProcessMessageReceived(CefRefPtr browser, else if (name == "show") { Show(browser); } + else if (name == "toggleFullScreen") { + ToggleFullScreen(browser); + } else { return false; } @@ -141,6 +144,8 @@ bool AtomCefClient::OnKeyEvent(CefRefPtr browser, ToggleDevTools(browser); } else if (event.modifiers == EVENTFLAG_COMMAND_DOWN && event.unmodified_character == '`') { FocusNextWindow(); + } else if (event.modifiers == (EVENTFLAG_COMMAND_DOWN | EVENTFLAG_SHIFT_DOWN) && event.unmodified_character == '~') { + FocusPreviousWindow(); } else { return false; diff --git a/native/atom_cef_client.h b/native/atom_cef_client.h index 75b787138..85d9a031b 100644 --- a/native/atom_cef_client.h +++ b/native/atom_cef_client.h @@ -105,6 +105,7 @@ class AtomCefClient : public CefClient, bool m_HandlePasteboardCommands = false; void FocusNextWindow(); + void FocusPreviousWindow(); void Open(std::string path); void Open(); void OpenUnstable(std::string path); @@ -123,6 +124,7 @@ class AtomCefClient : public CefClient, void Exit(int status); void Log(const char *message); void Show(CefRefPtr browser); + void ToggleFullScreen(CefRefPtr browser); IMPLEMENT_REFCOUNTING(AtomCefClient); IMPLEMENT_LOCKING(AtomCefClient); diff --git a/native/atom_cef_client_mac.mm b/native/atom_cef_client_mac.mm index c20bd5fbe..d2f250ec0 100644 --- a/native/atom_cef_client_mac.mm +++ b/native/atom_cef_client_mac.mm @@ -23,6 +23,24 @@ void AtomCefClient::FocusNextWindow() { } } +void AtomCefClient::FocusPreviousWindow() { + NSArray *windows = [NSApp windows]; + int count = [windows count]; + int start = [windows indexOfObject:[NSApp keyWindow]]; + + int i = start; + while (true) { + i = i - 1; + if (i == 0) i = count -1; + if (i == start) break; + NSWindow *window = [windows objectAtIndex:i]; + if ([window isVisible] && ![window isExcludedFromWindowsMenu]) { + [window makeKeyAndOrderFront:nil]; + break; + } + } +} + void AtomCefClient::Open(std::string path) { NSString *pathString = [NSString stringWithCString:path.c_str() encoding:NSUTF8StringEncoding]; [(AtomApplication *)[AtomApplication sharedApplication] open:pathString]; @@ -100,6 +118,10 @@ void AtomCefClient::Show(CefRefPtr browser) { [windowController.webView setHidden:NO]; } +void AtomCefClient::ToggleFullScreen(CefRefPtr browser) { + [[browser->GetHost()->GetWindowHandle() window] toggleFullScreen:nil]; +} + void AtomCefClient::ShowSaveDialog(int replyId, CefRefPtr browser) { CefRefPtr replyMessage = CefProcessMessage::Create("reply"); CefRefPtr replyArguments = replyMessage->GetArgumentList(); diff --git a/native/atom_cef_render_process_handler.h b/native/atom_cef_render_process_handler.h index c6d979e8b..973086245 100644 --- a/native/atom_cef_render_process_handler.h +++ b/native/atom_cef_render_process_handler.h @@ -16,10 +16,22 @@ class AtomCefRenderProcessHandler : public CefRenderProcessHandler { virtual bool OnProcessMessageReceived(CefRefPtr browser, CefProcessId source_process, CefRefPtr message) OVERRIDE; - + virtual void OnWorkerContextCreated(int worker_id, + const CefString& url, + CefRefPtr context) OVERRIDE; + virtual void OnWorkerContextReleased(int worker_id, + const CefString& url, + CefRefPtr context) OVERRIDE; + virtual void OnWorkerUncaughtException(int worker_id, + const CefString& url, + CefRefPtr context, + CefRefPtr exception, + CefRefPtr stackTrace) OVERRIDE; + void Reload(CefRefPtr browser); void Shutdown(CefRefPtr browser); bool CallMessageReceivedHandler(CefRefPtr context, CefRefPtr message); + void InjectExtensionsIntoV8Context(CefRefPtr context); IMPLEMENT_REFCOUNTING(AtomCefRenderProcessHandler); }; diff --git a/native/atom_cef_render_process_handler.mm b/native/atom_cef_render_process_handler.mm index 2c6196906..3f74f92aa 100644 --- a/native/atom_cef_render_process_handler.mm +++ b/native/atom_cef_render_process_handler.mm @@ -9,26 +9,44 @@ #import "path_watcher.h" #include + void AtomCefRenderProcessHandler::OnWebKitInitialized() { - new v8_extensions::Atom(); - new v8_extensions::Native(); - new v8_extensions::OnigRegExp(); - new v8_extensions::OnigScanner(); - new v8_extensions::Git(); - new v8_extensions::Tags(); } void AtomCefRenderProcessHandler::OnContextCreated(CefRefPtr browser, - CefRefPtr frame, - CefRefPtr context) { + CefRefPtr frame, + CefRefPtr context) { + InjectExtensionsIntoV8Context(context); } void AtomCefRenderProcessHandler::OnContextReleased(CefRefPtr browser, - CefRefPtr frame, - CefRefPtr context) { + CefRefPtr frame, + CefRefPtr context) { [PathWatcher removePathWatcherForContext:context]; } +void AtomCefRenderProcessHandler::OnWorkerContextCreated(int worker_id, + const CefString& url, + CefRefPtr context) { + InjectExtensionsIntoV8Context(context); +} + +void AtomCefRenderProcessHandler::OnWorkerContextReleased(int worker_id, + const CefString& url, + CefRefPtr context) { + NSLog(@"Web worker context released"); +} + +void AtomCefRenderProcessHandler::OnWorkerUncaughtException(int worker_id, + const CefString& url, + CefRefPtr context, + CefRefPtr exception, + CefRefPtr stackTrace) { + + std::string message = exception->GetMessage().ToString(); + NSLog(@"Exception throw in worker thread %s", message.c_str()); +} + bool AtomCefRenderProcessHandler::OnProcessMessageReceived(CefRefPtr browser, CefProcessId source_process, CefRefPtr message) { @@ -100,3 +118,13 @@ bool AtomCefRenderProcessHandler::CallMessageReceivedHandler(CefRefPtr context) { + // these objects are deleted when the context removes all references to them + (new v8_extensions::Atom())->CreateContextBinding(context); + (new v8_extensions::Native())->CreateContextBinding(context); + (new v8_extensions::Git())->CreateContextBinding(context); + (new v8_extensions::OnigRegExp())->CreateContextBinding(context); + (new v8_extensions::OnigScanner())->CreateContextBinding(context); + (new v8_extensions::Tags())->CreateContextBinding(context); +} diff --git a/native/atom_window_controller.mm b/native/atom_window_controller.mm index d61e46063..4a26e9601 100644 --- a/native/atom_window_controller.mm +++ b/native/atom_window_controller.mm @@ -69,6 +69,7 @@ [self initWithBootstrapScript:@"window-bootstrap" background:YES alwaysUseBundleResourcePath:stable]; [self.window setFrame:NSMakeRect(0, 0, 0, 0) display:NO]; [self.window setExcludedFromWindowsMenu:YES]; + [self.window setCollectionBehavior:NSWindowCollectionBehaviorStationary]; return self; } diff --git a/native/v8_extensions/atom.h b/native/v8_extensions/atom.h index 9aae075c0..05c2e0825 100644 --- a/native/v8_extensions/atom.h +++ b/native/v8_extensions/atom.h @@ -5,7 +5,7 @@ namespace v8_extensions { class Atom : public CefV8Handler { public: Atom(); - + void CreateContextBinding(CefRefPtr context); virtual bool Execute(const CefString& name, CefRefPtr object, const CefV8ValueList& arguments, @@ -14,5 +14,9 @@ namespace v8_extensions { // Provide the reference counting implementation for this class. IMPLEMENT_REFCOUNTING(Atom); + + private: + Atom(Atom const&); + void operator=(Atom const&); }; } diff --git a/native/v8_extensions/atom.js b/native/v8_extensions/atom.js deleted file mode 100644 index 5ec3a5922..000000000 --- a/native/v8_extensions/atom.js +++ /dev/null @@ -1,9 +0,0 @@ -(function () { - -native function sendMessageToBrowserProcess(name, array); - -this.atom = { - sendMessageToBrowserProcess: sendMessageToBrowserProcess -}; - -})(); diff --git a/native/v8_extensions/atom.mm b/native/v8_extensions/atom.mm index de6c7abe1..403cac409 100644 --- a/native/v8_extensions/atom.mm +++ b/native/v8_extensions/atom.mm @@ -7,9 +7,14 @@ namespace v8_extensions { Atom::Atom() : CefV8Handler() { - NSString *filePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"v8_extensions/atom.js"]; - NSString *extensionCode = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil]; - CefRegisterExtension("v8/atom", [extensionCode UTF8String], this); + } + + void Atom::CreateContextBinding(CefRefPtr context) { + CefRefPtr function = CefV8Value::CreateFunction("sendMessageToBrowserProcess", this); + CefRefPtr atomObject = CefV8Value::CreateObject(NULL); + atomObject->SetValue("sendMessageToBrowserProcess", function, V8_PROPERTY_ATTRIBUTE_NONE); + CefRefPtr global = context->GetGlobal(); + global->SetValue("atom", atomObject, V8_PROPERTY_ATTRIBUTE_NONE); } bool Atom::Execute(const CefString& name, diff --git a/native/v8_extensions/git.h b/native/v8_extensions/git.h index e7c0b1dce..74876fe11 100644 --- a/native/v8_extensions/git.h +++ b/native/v8_extensions/git.h @@ -2,19 +2,21 @@ #include "include/cef_v8.h" namespace v8_extensions { + class Git : public CefV8Handler { + public: + Git(); + void CreateContextBinding(CefRefPtr context); + virtual bool Execute(const CefString& name, + CefRefPtr object, + const CefV8ValueList& arguments, + CefRefPtr& retval, + CefString& exception) OVERRIDE; -class Git : public CefV8Handler { -public: - Git(); - - virtual bool Execute(const CefString& name, - CefRefPtr object, - const CefV8ValueList& arguments, - CefRefPtr& retval, - CefString& exception) OVERRIDE; - - // Provide the reference counting implementation for this class. - IMPLEMENT_REFCOUNTING(Git); -}; + // Provide the reference counting implementation for this class. + IMPLEMENT_REFCOUNTING(Git); + private: + Git(Git const&); + void operator=(Git const&); + }; } diff --git a/native/v8_extensions/git.js b/native/v8_extensions/git.js deleted file mode 100644 index fa4b1b49c..000000000 --- a/native/v8_extensions/git.js +++ /dev/null @@ -1,35 +0,0 @@ -var $git = {}; -(function() { - - native function getRepository(pathInRepo); - native function getHead(); - native function getPath(); - native function getStatus(path); - native function isIgnored(path); - native function checkoutHead(path); - native function getDiffStats(path); - native function isSubmodule(path); - native function refreshIndex(); - native function destroy(); - - function GitRepository(path) { - var repo = getRepository(path); - if (!repo) - throw new Error("No Git repository found searching path: " + path); - - repo.constructor = GitRepository; - repo.__proto__ = GitRepository.prototype; - return repo; - } - - GitRepository.prototype.getHead = getHead; - GitRepository.prototype.getPath = getPath; - GitRepository.prototype.getStatus = getStatus; - GitRepository.prototype.isIgnored = isIgnored; - GitRepository.prototype.checkoutHead = checkoutHead; - GitRepository.prototype.getDiffStats = getDiffStats; - GitRepository.prototype.isSubmodule = isSubmodule; - GitRepository.prototype.refreshIndex = refreshIndex; - GitRepository.prototype.destroy = destroy; - this.GitRepository = GitRepository; -})(); diff --git a/native/v8_extensions/git.mm b/native/v8_extensions/git.mm index b0d8cebe9..d2cda0a29 100644 --- a/native/v8_extensions/git.mm +++ b/native/v8_extensions/git.mm @@ -4,264 +4,278 @@ namespace v8_extensions { -class GitRepository : public CefBase { -private: - git_repository *repo; + class GitRepository : public CefBase { + private: + git_repository *repo; -public: - GitRepository(const char *pathInRepo) { - if (git_repository_open_ext(&repo, pathInRepo, 0, NULL) != GIT_OK) { - repo = NULL; + public: + GitRepository(const char *pathInRepo) { + if (git_repository_open_ext(&repo, pathInRepo, 0, NULL) != GIT_OK) { + repo = NULL; + } } - } - ~GitRepository() { - Destroy(); - } - - void Destroy() { - if (Exists()) { - git_repository_free(repo); - repo = NULL; + ~GitRepository() { + Destroy(); } - } - BOOL Exists() { - return repo != NULL; - } + void Destroy() { + if (Exists()) { + git_repository_free(repo); + repo = NULL; + } + } - CefRefPtr GetPath() { - return CefV8Value::CreateString(git_repository_path(repo)); - } + BOOL Exists() { + return repo != NULL; + } - CefRefPtr GetHead() { - git_reference *head; - if (git_repository_head(&head, repo) == GIT_OK) { - if (git_repository_head_detached(repo) == 1) { - const git_oid* sha = git_reference_target(head); - if (sha) { - char oid[GIT_OID_HEXSZ + 1]; - git_oid_tostr(oid, GIT_OID_HEXSZ + 1, sha); - git_reference_free(head); - return CefV8Value::CreateString(oid); + CefRefPtr GetPath() { + return CefV8Value::CreateString(git_repository_path(repo)); + } + + CefRefPtr GetHead() { + git_reference *head; + if (git_repository_head(&head, repo) == GIT_OK) { + if (git_repository_head_detached(repo) == 1) { + const git_oid* sha = git_reference_target(head); + if (sha) { + char oid[GIT_OID_HEXSZ + 1]; + git_oid_tostr(oid, GIT_OID_HEXSZ + 1, sha); + git_reference_free(head); + return CefV8Value::CreateString(oid); + } } + + CefRefPtr result = CefV8Value::CreateString(git_reference_name(head)); + git_reference_free(head); + return result; } - CefRefPtr result = CefV8Value::CreateString(git_reference_name(head)); + return CefV8Value::CreateNull(); + } + + CefRefPtr IsIgnored(const char *path) { + int ignored; + if (git_ignore_path_is_ignored(&ignored, repo, path) == GIT_OK) { + return CefV8Value::CreateBool(ignored == 1); + } + else { + return CefV8Value::CreateBool(false); + } + } + + CefRefPtr GetStatus(const char *path) { + unsigned int status = 0; + if (git_status_file(&status, repo, path) == GIT_OK) { + return CefV8Value::CreateInt(status); + } + else { + return CefV8Value::CreateInt(0); + } + } + + CefRefPtr CheckoutHead(const char *path) { + char *copiedPath = (char *)malloc(sizeof(char) * (strlen(path) + 1)); + strcpy(copiedPath, path); + git_checkout_opts options = GIT_CHECKOUT_OPTS_INIT; + options.checkout_strategy = GIT_CHECKOUT_FORCE; + git_strarray paths; + paths.count = 1; + paths.strings = &copiedPath; + options.paths = paths; + + int result = git_checkout_head(repo, &options); + free(copiedPath); + return CefV8Value::CreateBool(result == GIT_OK); + } + + CefRefPtr GetDiffStats(const char *path) { + 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); - return result; - } + if (commitStatus != GIT_OK) { + return CefV8Value::CreateNull(); + } - return CefV8Value::CreateNull(); - } + git_tree *tree; + int treeStatus = git_commit_tree(&tree, commit); + git_commit_free(commit); + if (treeStatus != GIT_OK) { + return CefV8Value::CreateNull(); + } - CefRefPtr IsIgnored(const char *path) { - int ignored; - if (git_ignore_path_is_ignored(&ignored, repo, path) == GIT_OK) { - return CefV8Value::CreateBool(ignored == 1); - } - else { - return CefV8Value::CreateBool(false); - } - } + char *copiedPath = (char *)malloc(sizeof(char) * (strlen(path) + 1)); + strcpy(copiedPath, path); + git_diff_options options = GIT_DIFF_OPTIONS_INIT; + git_strarray paths; + paths.count = 1; + paths.strings = &copiedPath; + options.pathspec = paths; + options.context_lines = 1; + options.flags = GIT_DIFF_DISABLE_PATHSPEC_MATCH; - CefRefPtr GetStatus(const char *path) { - unsigned int status = 0; - if (git_status_file(&status, repo, path) == GIT_OK) { - return CefV8Value::CreateInt(status); - } - else { - return CefV8Value::CreateInt(0); - } - } + git_diff_list *diffs; + int diffStatus = git_diff_tree_to_workdir(&diffs, repo, tree, &options); + free(copiedPath); + if (diffStatus != GIT_OK || git_diff_num_deltas(diffs) != 1) { + return CefV8Value::CreateNull(); + } - CefRefPtr CheckoutHead(const char *path) { - char *copiedPath = (char *)malloc(sizeof(char) * (strlen(path) + 1)); - strcpy(copiedPath, path); - git_checkout_opts options = GIT_CHECKOUT_OPTS_INIT; - options.checkout_strategy = GIT_CHECKOUT_FORCE; - git_strarray paths; - paths.count = 1; - paths.strings = &copiedPath; - options.paths = paths; + git_diff_patch *patch; + int patchStatus = git_diff_get_patch(&patch, NULL, diffs, 0); + git_diff_list_free(diffs); + if (patchStatus != GIT_OK) { + return CefV8Value::CreateNull(); + } - int result = git_checkout_head(repo, &options); - free(copiedPath); - return CefV8Value::CreateBool(result == GIT_OK); - } - - CefRefPtr GetDiffStats(const char *path) { - 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(); - } - - char *copiedPath = (char *)malloc(sizeof(char) * (strlen(path) + 1)); - strcpy(copiedPath, path); - git_diff_options options = GIT_DIFF_OPTIONS_INIT; - git_strarray paths; - paths.count = 1; - paths.strings = &copiedPath; - options.pathspec = paths; - options.context_lines = 1; - options.flags = GIT_DIFF_DISABLE_PATHSPEC_MATCH; - - git_diff_list *diffs; - int diffStatus = git_diff_tree_to_workdir(&diffs, repo, tree, &options); - free(copiedPath); - if (diffStatus != GIT_OK || git_diff_num_deltas(diffs) != 1) { - return CefV8Value::CreateNull(); - } - - git_diff_patch *patch; - int patchStatus = git_diff_get_patch(&patch, NULL, diffs, 0); - git_diff_list_free(diffs); - if (patchStatus != GIT_OK) { - return CefV8Value::CreateNull(); - } - - int added = 0; - int deleted = 0; - int hunks = git_diff_patch_num_hunks(patch); - for (int i = 0; i < hunks; i++) { - int lines = git_diff_patch_num_lines_in_hunk(patch, i); - for (int j = 0; j < lines; j++) { - char lineType; - if (git_diff_patch_get_line_in_hunk(&lineType, NULL, NULL, NULL, NULL, patch, i, j) == GIT_OK) { - switch (lineType) { - case GIT_DIFF_LINE_ADDITION: - added++; - break; - case GIT_DIFF_LINE_DELETION: - deleted++; - break; + int added = 0; + int deleted = 0; + int hunks = git_diff_patch_num_hunks(patch); + for (int i = 0; i < hunks; i++) { + int lines = git_diff_patch_num_lines_in_hunk(patch, i); + for (int j = 0; j < lines; j++) { + char lineType; + if (git_diff_patch_get_line_in_hunk(&lineType, NULL, NULL, NULL, NULL, patch, i, j) == GIT_OK) { + switch (lineType) { + case GIT_DIFF_LINE_ADDITION: + added++; + break; + case GIT_DIFF_LINE_DELETION: + deleted++; + break; + } } } } - } - git_diff_patch_free(patch); + git_diff_patch_free(patch); - CefRefPtr result = CefV8Value::CreateObject(NULL); - result->SetValue("added", CefV8Value::CreateInt(added), V8_PROPERTY_ATTRIBUTE_NONE); - result->SetValue("deleted", CefV8Value::CreateInt(deleted), V8_PROPERTY_ATTRIBUTE_NONE); - return result; + CefRefPtr result = CefV8Value::CreateObject(NULL); + result->SetValue("added", CefV8Value::CreateInt(added), V8_PROPERTY_ATTRIBUTE_NONE); + result->SetValue("deleted", CefV8Value::CreateInt(deleted), V8_PROPERTY_ATTRIBUTE_NONE); + return result; + } + + CefRefPtr IsSubmodule(const char *path) { + BOOL isSubmodule = false; + git_index* index; + if (git_repository_index(&index, repo) == GIT_OK) { + const git_index_entry *entry = git_index_get_bypath(index, path, 0); + isSubmodule = entry != NULL && (entry->mode & S_IFMT) == GIT_FILEMODE_COMMIT; + git_index_free(index); + } + return CefV8Value::CreateBool(isSubmodule); + } + + void RefreshIndex() { + git_index* index; + if (git_repository_index(&index, repo) == GIT_OK) { + git_index_read(index); + git_index_free(index); + } + } + + IMPLEMENT_REFCOUNTING(GitRepository); + }; + + Git::Git() : CefV8Handler() { } - CefRefPtr IsSubmodule(const char *path) { - BOOL isSubmodule = false; - git_index* index; - if (git_repository_index(&index, repo) == GIT_OK) { - const git_index_entry *entry = git_index_get_bypath(index, path, 0); - isSubmodule = entry != NULL && (entry->mode & S_IFMT) == GIT_FILEMODE_COMMIT; - git_index_free(index); + void Git::CreateContextBinding(CefRefPtr context) { + const char* methodNames[] = { + "getRepository", "getHead", "getPath", "isIgnored", "getStatus", "checkoutHead", + "getDiffStats", "isSubmodule", "refreshIndex", "destroy" + }; + + CefRefPtr nativeObject = CefV8Value::CreateObject(NULL); + int arrayLength = sizeof(methodNames) / sizeof(const char *); + for (int i = 0; i < arrayLength; i++) { + const char *functionName = methodNames[i]; + CefRefPtr function = CefV8Value::CreateFunction(functionName, this); + nativeObject->SetValue(functionName, function, V8_PROPERTY_ATTRIBUTE_NONE); } - return CefV8Value::CreateBool(isSubmodule); + + CefRefPtr global = context->GetGlobal(); + global->SetValue("$git", nativeObject, V8_PROPERTY_ATTRIBUTE_NONE); } - void RefreshIndex() { - git_index* index; - if (git_repository_index(&index, repo) == GIT_OK) { - git_index_read(index); - git_index_free(index); + bool Git::Execute(const CefString& name, + CefRefPtr object, + 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(); + } + 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 == "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 == "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 == "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 == "destroy") { + GitRepository *userData = (GitRepository *)object->GetUserData().get(); + userData->Destroy(); + return true; + } + + return false; } - - IMPLEMENT_REFCOUNTING(GitRepository); -}; - -Git::Git() : CefV8Handler() { - NSString *filePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"v8_extensions/git.js"]; - NSString *extensionCode = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil]; - CefRegisterExtension("v8/git", [extensionCode UTF8String], this); -} - -bool Git::Execute(const CefString& name, - CefRefPtr object, - 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(); - } - 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 == "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 == "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 == "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 == "destroy") { - GitRepository *userData = (GitRepository *)object->GetUserData().get(); - userData->Destroy(); - return true; - } - - return false; -} - } diff --git a/native/v8_extensions/native.h b/native/v8_extensions/native.h index e7a200493..24064429f 100644 --- a/native/v8_extensions/native.h +++ b/native/v8_extensions/native.h @@ -1,24 +1,23 @@ #include "include/cef_base.h" #include "include/cef_v8.h" - namespace v8_extensions { -class Native : public CefV8Handler { -public: - Native(); + class Native : public CefV8Handler { + public: + Native(); + void CreateContextBinding(CefRefPtr context); + virtual bool Execute(const CefString& name, + CefRefPtr object, + const CefV8ValueList& arguments, + CefRefPtr& retval, + CefString& exception) OVERRIDE; - virtual bool Execute(const CefString& name, - CefRefPtr object, - const CefV8ValueList& arguments, - CefRefPtr& retval, - CefString& exception) OVERRIDE; - - // Provide the reference counting implementation for this class. - IMPLEMENT_REFCOUNTING(Native); - -private: - std::string windowState; -}; + // Provide the reference counting implementation for this class. + IMPLEMENT_REFCOUNTING(Native); + private: + Native(Native const&); + void operator=(Native const&); + }; } diff --git a/native/v8_extensions/native.js b/native/v8_extensions/native.js deleted file mode 100644 index 9124d89fc..000000000 --- a/native/v8_extensions/native.js +++ /dev/null @@ -1,85 +0,0 @@ -var $native = {}; -(function() { - - native function exists(path); - $native.exists = exists; - - native function read(path); - $native.read = read; - - native function write(path, content); - $native.write = write; - - native function absolute(path); - $native.absolute = absolute; - - native function traverseTree(path, onFile, onDirectory); - $native.traverseTree = traverseTree; - - native function getAllFilePathsAsync(path, callback); - $native.getAllFilePathsAsync = getAllFilePathsAsync; - - native function isFile(path); - $native.isFile = isFile; - - native function isDirectory(path); - $native.isDirectory = isDirectory; - - native function remove(path); - $native.remove = remove; - - native function open(path); - $native.open = open; - - native function quit(); - $native.quit = quit; - - native function writeToPasteboard(text); - $native.writeToPasteboard = writeToPasteboard; - - native function readFromPasteboard(); - $native.readFromPasteboard = readFromPasteboard; - - native function watchPath(path); - $native.watchPath = watchPath; - - native function unwatchPath(path, callbackId); - $native.unwatchPath = unwatchPath; - - native function getWatchedPaths(); - $native.getWatchedPaths = getWatchedPaths; - - native function unwatchAllPaths(); - $native.unwatchAllPaths = unwatchAllPaths; - - native function makeDirectory(path); - $native.makeDirectory = makeDirectory; - - native function move(sourcePath, targetPath); - $native.move = move; - - native function moveToTrash(path); - $native.moveToTrash = moveToTrash; - - native function reload(); - $native.reload = reload; - - native function lastModified(path); - $native.lastModified = lastModified; - - native function md5ForPath(path); - $native.md5ForPath = md5ForPath; - - native function exec(command, options, callback); - $native.exec = exec; - - native function getPlatform(); - $native.getPlatform = getPlatform; - - native function setWindowState(state); - $native.setWindowState = setWindowState; - - native function getWindowState(); - $native.getWindowState = getWindowState; - -})(); diff --git a/native/v8_extensions/native.mm b/native/v8_extensions/native.mm index 0a66591ca..9ce5b4ee6 100644 --- a/native/v8_extensions/native.mm +++ b/native/v8_extensions/native.mm @@ -8,496 +8,522 @@ #import "path_watcher.h" #import - #include +static std::string windowState = "{}"; +static NSLock *windowStateLock = [[NSLock alloc] init]; + namespace v8_extensions { + using namespace std; -NSString *stringFromCefV8Value(const CefRefPtr& value) { - std::string cc_value = value->GetStringValue().ToString(); - return [NSString stringWithUTF8String:cc_value.c_str()]; -} + NSString *stringFromCefV8Value(const CefRefPtr& value); + void throwException(const CefRefPtr& global, CefRefPtr exception, NSString *message); -void throwException(const CefRefPtr& global, CefRefPtr exception, NSString *message) { - CefV8ValueList arguments; - - message = [message stringByAppendingFormat:@"\n%s", exception->GetMessage().ToString().c_str()]; - arguments.push_back(CefV8Value::CreateString(std::string([message UTF8String], [message lengthOfBytesUsingEncoding:NSUTF8StringEncoding]))); - - CefRefPtr console = global->GetValue("console"); - console->GetValue("error")->ExecuteFunction(console, arguments); -} - -Native::Native() : CefV8Handler() { - NSString *filePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"v8_extensions/native.js"]; - NSString *extensionCode = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil]; - windowState = "{}"; - CefRegisterExtension("v8/native", [extensionCode UTF8String], this); -} - -bool Native::Execute(const CefString& name, - CefRefPtr object, - const CefV8ValueList& arguments, - CefRefPtr& retval, - CefString& exception) { - if (name == "exists") { - std::string cc_value = arguments[0]->GetStringValue().ToString(); - const char *path = cc_value.c_str(); - retval = CefV8Value::CreateBool(access(path, F_OK) == 0); - - return true; + Native::Native() : CefV8Handler() { } - else if (name == "read") { - NSString *path = stringFromCefV8Value(arguments[0]); - NSError *error = nil; - NSStringEncoding *encoding = nil; - NSString *contents = [NSString stringWithContentsOfFile:path usedEncoding:encoding error:&error]; + void Native::CreateContextBinding(CefRefPtr context) { + const char* methodNames[] = { + "exists", "read", "write", "absolute", "getAllFilePathsAsync", "traverseTree", "isDirectory", + "isFile", "remove", "writeToPasteboard", "readFromPasteboard", "quit", "watchPath", "unwatchPath", + "getWatchedPaths", "unwatchAllPaths", "makeDirectory", "move", "moveToTrash", "reload", "lastModified", + "md5ForPath", "exec", "getPlatform", "setWindowState", "getWindowState" + }; - NSError *binaryFileError = nil; - if (error) { - contents = [NSString stringWithContentsOfFile:path encoding:NSASCIIStringEncoding error:&binaryFileError]; + CefRefPtr nativeObject = CefV8Value::CreateObject(NULL); + int arrayLength = sizeof(methodNames) / sizeof(const char *); + for (int i = 0; i < arrayLength; i++) { + const char *functionName = methodNames[i]; + CefRefPtr function = CefV8Value::CreateFunction(functionName, this); + nativeObject->SetValue(functionName, function, V8_PROPERTY_ATTRIBUTE_NONE); } - if (binaryFileError) { - exception = [[binaryFileError localizedDescription] UTF8String]; - } - else { - retval = CefV8Value::CreateString([contents UTF8String]); - } - - return true; + CefRefPtr global = context->GetGlobal(); + global->SetValue("$native", nativeObject, V8_PROPERTY_ATTRIBUTE_NONE); } - else if (name == "write") { - NSString *path = stringFromCefV8Value(arguments[0]); - NSString *content = stringFromCefV8Value(arguments[1]); - NSFileManager *fm = [NSFileManager defaultManager]; + bool Native::Execute(const CefString& name, + CefRefPtr object, + const CefV8ValueList& arguments, + CefRefPtr& retval, + CefString& exception) { + if (name == "exists") { + std::string cc_value = arguments[0]->GetStringValue().ToString(); + const char *path = cc_value.c_str(); + retval = CefV8Value::CreateBool(access(path, F_OK) == 0); - // Create parent directories if they don't exist - BOOL exists = [fm fileExistsAtPath:[path stringByDeletingLastPathComponent] isDirectory:nil]; - if (!exists) { - [fm createDirectoryAtPath:[path stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil]; + return true; } + else if (name == "read") { + NSString *path = stringFromCefV8Value(arguments[0]); - NSError *error = nil; - BOOL success = [content writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:&error]; + NSError *error = nil; + NSStringEncoding *encoding = nil; + NSString *contents = [NSString stringWithContentsOfFile:path usedEncoding:encoding error:&error]; - if (error) { - exception = [[error localizedDescription] UTF8String]; + NSError *binaryFileError = nil; + if (error) { + contents = [NSString stringWithContentsOfFile:path encoding:NSASCIIStringEncoding error:&binaryFileError]; + } + + if (binaryFileError) { + exception = [[binaryFileError localizedDescription] UTF8String]; + } + else { + retval = CefV8Value::CreateString([contents UTF8String]); + } + + return true; } - else if (!success) { - std::string exception = "Cannot write to '"; - exception += [path UTF8String]; - exception += "'"; + else if (name == "write") { + NSString *path = stringFromCefV8Value(arguments[0]); + NSString *content = stringFromCefV8Value(arguments[1]); + + NSFileManager *fm = [NSFileManager defaultManager]; + + // Create parent directories if they don't exist + BOOL exists = [fm fileExistsAtPath:[path stringByDeletingLastPathComponent] isDirectory:nil]; + if (!exists) { + [fm createDirectoryAtPath:[path stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil]; + } + + NSError *error = nil; + BOOL success = [content writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:&error]; + + if (error) { + exception = [[error localizedDescription] UTF8String]; + } + else if (!success) { + std::string exception = "Cannot write to '"; + exception += [path UTF8String]; + exception += "'"; + } + + return true; } + else if (name == "absolute") { + NSString *path = stringFromCefV8Value(arguments[0]); - return true; - } - else if (name == "absolute") { - NSString *path = stringFromCefV8Value(arguments[0]); + path = [path stringByStandardizingPath]; + if ([path characterAtIndex:0] == '/') { + retval = CefV8Value::CreateString([path UTF8String]); + } - path = [path stringByStandardizingPath]; - if ([path characterAtIndex:0] == '/') { - retval = CefV8Value::CreateString([path UTF8String]); + return true; } + else if (name == "getAllFilePathsAsync") { + std::string argument = arguments[0]->GetStringValue().ToString(); + CefRefPtr callback = arguments[1]; + CefRefPtr context = CefV8Context::GetCurrentContext(); - return true; - } - else if (name == "getAllFilePathsAsync") { - std::string argument = 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, ^{ + int rootPathLength = argument.size() + 1; + char rootPath[rootPathLength]; + strcpy(rootPath, argument.c_str()); + char * const treePaths[] = {rootPath, NULL}; - dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - dispatch_async(queue, ^{ + FTS *tree = fts_open(treePaths, FTS_COMFOLLOW | FTS_PHYSICAL| FTS_NOCHDIR | FTS_NOSTAT, NULL); + std::vector paths; + + if (tree != NULL) { + FTSENT *entry; + int arrayIndex = 0; + while ((entry = fts_read(tree)) != NULL) { + if (entry->fts_level == 0) { + continue; + } + + bool isFile = entry->fts_info == FTS_NSOK; + if (!isFile) { + continue; + } + + int pathLength = entry->fts_pathlen - rootPathLength; + char relative[pathLength + 1]; + relative[pathLength] = '\0'; + strncpy(relative, entry->fts_path + rootPathLength, pathLength); + paths.push_back(relative); + } + } + + dispatch_queue_t mainQueue = dispatch_get_main_queue(); + dispatch_async(mainQueue, ^{ + context->Enter(); + CefRefPtr v8Paths = CefV8Value::CreateArray(paths.size()); + for (int i = 0; i < paths.size(); i++) { + v8Paths->SetValue(i, CefV8Value::CreateString(paths[i])); + } + CefV8ValueList callbackArgs; + callbackArgs.push_back(v8Paths); + callback->ExecuteFunction(callback, callbackArgs); + context->Exit(); + }); + }); + return true; + } + else if (name == "traverseTree") { + std::string argument = arguments[0]->GetStringValue().ToString(); int rootPathLength = argument.size() + 1; char rootPath[rootPathLength]; strcpy(rootPath, argument.c_str()); - char * const treePaths[] = {rootPath, NULL}; + char * const paths[] = {rootPath, NULL}; - FTS *tree = fts_open(treePaths, FTS_COMFOLLOW | FTS_PHYSICAL| FTS_NOCHDIR | FTS_NOSTAT, NULL); - std::vector paths; + FTS *tree = fts_open(paths, FTS_COMFOLLOW | FTS_PHYSICAL| FTS_NOCHDIR | FTS_NOSTAT, NULL); + if (tree == NULL) { + return true; + } - if (tree != NULL) { - FTSENT *entry; - int arrayIndex = 0; - while ((entry = fts_read(tree)) != NULL) { - if (entry->fts_level == 0) { - continue; + CefRefPtr onFile = arguments[1]; + CefRefPtr onDir = arguments[2]; + CefV8ValueList args; + FTSENT *entry; + while ((entry = fts_read(tree)) != NULL) { + if (entry->fts_level == 0) { + continue; + } + + bool isFile = entry->fts_info == FTS_NSOK; + bool isDir = entry->fts_info == FTS_D; + if (!isFile && !isDir) { + 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)); + if (isFile) { + onFile->ExecuteFunction(onFile, args); + } + else { + CefRefPtr enterDir = onDir->ExecuteFunction(onDir, args); + if(enterDir != NULL && !enterDir->GetBoolValue()) { + fts_set(tree, entry, FTS_SKIP); } - - bool isFile = entry->fts_info == FTS_NSOK; - if (!isFile) { - continue; - } - - int pathLength = entry->fts_pathlen - rootPathLength; - char relative[pathLength + 1]; - relative[pathLength] = '\0'; - strncpy(relative, entry->fts_path + rootPathLength, pathLength); - paths.push_back(relative); } } - dispatch_queue_t mainQueue = dispatch_get_main_queue(); - dispatch_async(mainQueue, ^{ - context->Enter(); - CefRefPtr v8Paths = CefV8Value::CreateArray(paths.size()); - for (int i = 0; i < paths.size(); i++) { - v8Paths->SetValue(i, CefV8Value::CreateString(paths[i])); - } - CefV8ValueList callbackArgs; - callbackArgs.push_back(v8Paths); - callback->ExecuteFunction(callback, callbackArgs); - context->Exit(); - }); - }); - return true; - } - else if (name == "traverseTree") { - std::string argument = arguments[0]->GetStringValue().ToString(); - int rootPathLength = argument.size() + 1; - char rootPath[rootPathLength]; - strcpy(rootPath, argument.c_str()); - char * const paths[] = {rootPath, NULL}; + return true; + } + else if (name == "isDirectory") { + NSString *path = stringFromCefV8Value(arguments[0]); - FTS *tree = fts_open(paths, FTS_COMFOLLOW | FTS_PHYSICAL| FTS_NOCHDIR | FTS_NOSTAT, NULL); - if (tree == NULL) { + BOOL isDir = false; + BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]; + retval = CefV8Value::CreateBool(exists && isDir); + + return true; + } + else if (name == "isFile") { + NSString *path = stringFromCefV8Value(arguments[0]); + + BOOL isDir = false; + BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]; + retval = CefV8Value::CreateBool(exists && !isDir); + + return true; + } + else if (name == "remove") { + NSString *path = stringFromCefV8Value(arguments[0]); + + NSError *error = nil; + [[NSFileManager defaultManager] removeItemAtPath:path error:&error]; + + if (error) { + exception = [[error localizedDescription] UTF8String]; + } + + return true; + } + else if (name == "writeToPasteboard") { + NSString *text = stringFromCefV8Value(arguments[0]); + + NSPasteboard *pb = [NSPasteboard generalPasteboard]; + [pb declareTypes:[NSArray arrayWithObjects:NSStringPboardType, nil] owner:nil]; + [pb setString:text forType:NSStringPboardType]; + + return true; + } + else if (name == "readFromPasteboard") { + NSPasteboard *pb = [NSPasteboard generalPasteboard]; + NSArray *results = [pb readObjectsForClasses:[NSArray arrayWithObjects:[NSString class], nil] options:nil]; + if (results) { + retval = CefV8Value::CreateString([[results objectAtIndex:0] UTF8String]); + } + + return true; + } + else if (name == "quit") { + [NSApp terminate:nil]; + return true; + } + else if (name == "watchPath") { + NSString *path = stringFromCefV8Value(arguments[0]); + CefRefPtr function = arguments[1]; + + CefRefPtr context = CefV8Context::GetCurrentContext(); + + WatchCallback callback = ^(NSString *eventType, NSString *path) { + context->Enter(); + + CefV8ValueList args; + + args.push_back(CefV8Value::CreateString(std::string([eventType UTF8String], [eventType lengthOfBytesUsingEncoding:NSUTF8StringEncoding]))); + args.push_back(CefV8Value::CreateString(std::string([path UTF8String], [path lengthOfBytesUsingEncoding:NSUTF8StringEncoding]))); + function->ExecuteFunction(function, args); + + context->Exit(); + }; + + PathWatcher *pathWatcher = [PathWatcher pathWatcherForContext:CefV8Context::GetCurrentContext()]; + NSString *watchId = [pathWatcher watchPath:path callback:[[callback copy] autorelease]]; + if (watchId) { + retval = CefV8Value::CreateString([watchId UTF8String]); + } + else { + exception = std::string("Failed to watch path '") + std::string([path UTF8String]) + std::string("' (it may not exist)"); + } + + return true; + } + else if (name == "unwatchPath") { + NSString *path = stringFromCefV8Value(arguments[0]); + NSString *callbackId = stringFromCefV8Value(arguments[1]); + NSError *error = nil; + PathWatcher *pathWatcher = [PathWatcher pathWatcherForContext:CefV8Context::GetCurrentContext()]; + [pathWatcher unwatchPath:path callbackId:callbackId error:&error]; + + if (error) { + exception = [[error localizedDescription] UTF8String]; + } + + return true; + } + else if (name == "getWatchedPaths") { + PathWatcher *pathWatcher = [PathWatcher pathWatcherForContext:CefV8Context::GetCurrentContext()]; + NSArray *paths = [pathWatcher watchedPaths]; + + CefRefPtr pathsArray = CefV8Value::CreateArray([paths count]); + + for (int i = 0; i < [paths count]; i++) { + CefRefPtr path = CefV8Value::CreateString([[paths objectAtIndex:i] UTF8String]); + pathsArray->SetValue(i, path); + } + retval = pathsArray; + + return true; + } + else if (name == "unwatchAllPaths") { + PathWatcher *pathWatcher = [PathWatcher pathWatcherForContext:CefV8Context::GetCurrentContext()]; + [pathWatcher unwatchAllPaths]; + return true; + } + else if (name == "makeDirectory") { + NSString *path = stringFromCefV8Value(arguments[0]); + NSFileManager *fm = [NSFileManager defaultManager]; + NSError *error = nil; + [fm createDirectoryAtPath:path withIntermediateDirectories:NO attributes:nil error:&error]; + + if (error) { + exception = [[error localizedDescription] UTF8String]; + } + + return true; + } + else if (name == "move") { + NSString *sourcePath = stringFromCefV8Value(arguments[0]); + NSString *targetPath = stringFromCefV8Value(arguments[1]); + NSFileManager *fm = [NSFileManager defaultManager]; + + NSError *error = nil; + [fm moveItemAtPath:sourcePath toPath:targetPath error:&error]; + + if (error) { + exception = [[error localizedDescription] UTF8String]; + } + + return true; + } + else if (name == "moveToTrash") { + NSString *sourcePath = stringFromCefV8Value(arguments[0]); + bool success = [[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation + source:[sourcePath stringByDeletingLastPathComponent] + destination:@"" + files:[NSArray arrayWithObject:[sourcePath lastPathComponent]] + tag:nil]; + + if (!success) { + std::string exception = "Can not move "; + exception += [sourcePath UTF8String]; + exception += " to trash."; + } + + return true; + } + else if (name == "reload") { + CefV8Context::GetCurrentContext()->GetBrowser()->ReloadIgnoreCache(); + } + else if (name == "lastModified") { + NSString *path = stringFromCefV8Value(arguments[0]); + NSFileManager *fm = [NSFileManager defaultManager]; + + NSError *error = nil; + NSDictionary *attributes = [fm attributesOfItemAtPath:path error:&error]; + + if (error) { + exception = [[error localizedDescription] UTF8String]; + } + + NSDate *lastModified = [attributes objectForKey:NSFileModificationDate]; + retval = CefV8Value::CreateDate(CefTime([lastModified timeIntervalSince1970])); + return true; + } + else if (name == "md5ForPath") { + NSString *path = stringFromCefV8Value(arguments[0]); + unsigned char outputData[CC_MD5_DIGEST_LENGTH]; + + NSData *inputData = [[NSData alloc] initWithContentsOfFile:path]; + CC_MD5([inputData bytes], [inputData length], outputData); + [inputData release]; + + NSMutableString *hash = [[NSMutableString alloc] init]; + + for (NSUInteger i = 0; i < CC_MD5_DIGEST_LENGTH; i++) { + [hash appendFormat:@"%02x", outputData[i]]; + } + + retval = CefV8Value::CreateString([hash UTF8String]); + return true; + } + else if (name == "exec") { + NSString *command = stringFromCefV8Value(arguments[0]); + CefRefPtr options = arguments[1]; + CefRefPtr callback = arguments[2]; + + NSTask *task = [[NSTask alloc] init]; + [task setLaunchPath:@"/bin/sh"]; + [task setStandardInput:[NSFileHandle fileHandleWithNullDevice]]; + [task setArguments:[NSArray arrayWithObjects:@"-l", @"-c", command, nil]]; + + NSPipe *stdout = [NSPipe pipe]; + NSPipe *stderr = [NSPipe pipe]; + [task setStandardOutput:stdout]; + [task setStandardError:stderr]; + + CefRefPtr context = CefV8Context::GetCurrentContext(); + void (^outputHandle)(NSString *contents, CefRefPtr function) = nil; + void (^taskTerminatedHandle)(NSString *output, NSString *errorOutput) = nil; + + outputHandle = ^(NSString *contents, CefRefPtr function) { + context->Enter(); + + CefV8ValueList args; + args.push_back(CefV8Value::CreateString(std::string([contents UTF8String], [contents lengthOfBytesUsingEncoding:NSUTF8StringEncoding]))); + CefRefPtr retval = function->ExecuteFunction(function, args); + + if (function->HasException()) { + throwException(context->GetGlobal(), function->GetException(), @"Error thrown in OutputHandle"); + } + + context->Exit(); + }; + + taskTerminatedHandle = ^(NSString *output, NSString *errorOutput) { + context->Enter(); + + CefV8ValueList args; + args.push_back(CefV8Value::CreateInt([task terminationStatus])); + args.push_back(CefV8Value::CreateString([output UTF8String])); + args.push_back(CefV8Value::CreateString([errorOutput UTF8String])); + + callback->ExecuteFunction(callback, args); + + if (callback->HasException()) { + throwException(context->GetGlobal(), callback->GetException(), @"Error thrown in TaskTerminatedHandle"); + } + + context->Exit(); + + stdout.fileHandleForReading.writeabilityHandler = nil; + stderr.fileHandleForReading.writeabilityHandler = nil; + }; + + task.terminationHandler = ^(NSTask *) { + NSString *output = [[NSString alloc] initWithData:[[stdout fileHandleForReading] readDataToEndOfFile] encoding:NSUTF8StringEncoding]; + NSString *errorOutput = [[NSString alloc] initWithData:[[stderr fileHandleForReading] readDataToEndOfFile] encoding:NSUTF8StringEncoding]; + dispatch_sync(dispatch_get_main_queue(), ^() { + taskTerminatedHandle(output, errorOutput); + }); + [output release]; + [errorOutput release]; + }; + + CefRefPtr stdoutFunction = options->GetValue("stdout"); + if (stdoutFunction->IsFunction()) { + stdout.fileHandleForReading.writeabilityHandler = ^(NSFileHandle *fileHandle) { + NSData *data = [fileHandle availableData]; + NSString *contents = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + dispatch_sync(dispatch_get_main_queue(), ^() { + outputHandle(contents, stdoutFunction); + }); + [contents release]; + }; + } + + CefRefPtr stderrFunction = options->GetValue("stderr"); + if (stderrFunction->IsFunction()) { + stderr.fileHandleForReading.writeabilityHandler = ^(NSFileHandle *fileHandle) { + NSData *data = [fileHandle availableData]; + NSString *contents = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + dispatch_sync(dispatch_get_main_queue(), ^() { + outputHandle(contents, stderrFunction); + }); + [contents release]; + }; + } + + [task launch]; + + return true; + } + else if (name == "getPlatform") { + retval = CefV8Value::CreateString("mac"); return true; } - CefRefPtr onFile = arguments[1]; - CefRefPtr onDir = arguments[2]; - CefV8ValueList args; - FTSENT *entry; - while ((entry = fts_read(tree)) != NULL) { - if (entry->fts_level == 0) { - continue; - } - - bool isFile = entry->fts_info == FTS_NSOK; - bool isDir = entry->fts_info == FTS_D; - if (!isFile && !isDir) { - 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)); - if (isFile) { - onFile->ExecuteFunction(onFile, args); - } - else { - CefRefPtr enterDir = onDir->ExecuteFunction(onDir, args); - if(enterDir != NULL && !enterDir->GetBoolValue()) { - fts_set(tree, entry, FTS_SKIP); - } - } + else if (name == "setWindowState") { + [windowStateLock lock]; + windowState = arguments[0]->GetStringValue().ToString(); + [windowStateLock unlock]; + return true; } - return true; - } - else if (name == "isDirectory") { - NSString *path = stringFromCefV8Value(arguments[0]); - - BOOL isDir = false; - BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]; - retval = CefV8Value::CreateBool(exists && isDir); - - return true; - } - else if (name == "isFile") { - NSString *path = stringFromCefV8Value(arguments[0]); - - BOOL isDir = false; - BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]; - retval = CefV8Value::CreateBool(exists && !isDir); - - return true; - } - else if (name == "remove") { - NSString *path = stringFromCefV8Value(arguments[0]); - - NSError *error = nil; - [[NSFileManager defaultManager] removeItemAtPath:path error:&error]; - - if (error) { - exception = [[error localizedDescription] UTF8String]; + else if (name == "getWindowState") { + [windowStateLock lock]; + retval = CefV8Value::CreateString(windowState); + [windowStateLock unlock]; + return true; } - return true; - } - else if (name == "writeToPasteboard") { - NSString *text = stringFromCefV8Value(arguments[0]); + return false; + }; - NSPasteboard *pb = [NSPasteboard generalPasteboard]; - [pb declareTypes:[NSArray arrayWithObjects:NSStringPboardType, nil] owner:nil]; - [pb setString:text forType:NSStringPboardType]; - - return true; - } - else if (name == "readFromPasteboard") { - NSPasteboard *pb = [NSPasteboard generalPasteboard]; - NSArray *results = [pb readObjectsForClasses:[NSArray arrayWithObjects:[NSString class], nil] options:nil]; - if (results) { - retval = CefV8Value::CreateString([[results objectAtIndex:0] UTF8String]); - } - - return true; - } - else if (name == "quit") { - [NSApp terminate:nil]; - return true; - } - else if (name == "watchPath") { - NSString *path = stringFromCefV8Value(arguments[0]); - CefRefPtr function = arguments[1]; - - CefRefPtr context = CefV8Context::GetCurrentContext(); - - WatchCallback callback = ^(NSString *eventType, NSString *path) { - context->Enter(); - - CefV8ValueList args; - - args.push_back(CefV8Value::CreateString(std::string([eventType UTF8String], [eventType lengthOfBytesUsingEncoding:NSUTF8StringEncoding]))); - args.push_back(CefV8Value::CreateString(std::string([path UTF8String], [path lengthOfBytesUsingEncoding:NSUTF8StringEncoding]))); - function->ExecuteFunction(function, args); - - context->Exit(); - }; - - PathWatcher *pathWatcher = [PathWatcher pathWatcherForContext:CefV8Context::GetCurrentContext()]; - NSString *watchId = [pathWatcher watchPath:path callback:[[callback copy] autorelease]]; - if (watchId) { - retval = CefV8Value::CreateString([watchId UTF8String]); - } - else { - exception = std::string("Failed to watch path '") + std::string([path UTF8String]) + std::string("' (it may not exist)"); - } - - return true; - } - else if (name == "unwatchPath") { - NSString *path = stringFromCefV8Value(arguments[0]); - NSString *callbackId = stringFromCefV8Value(arguments[1]); - NSError *error = nil; - PathWatcher *pathWatcher = [PathWatcher pathWatcherForContext:CefV8Context::GetCurrentContext()]; - [pathWatcher unwatchPath:path callbackId:callbackId error:&error]; - - if (error) { - exception = [[error localizedDescription] UTF8String]; - } - - return true; - } - else if (name == "getWatchedPaths") { - PathWatcher *pathWatcher = [PathWatcher pathWatcherForContext:CefV8Context::GetCurrentContext()]; - NSArray *paths = [pathWatcher watchedPaths]; - - CefRefPtr pathsArray = CefV8Value::CreateArray([paths count]); - - for (int i = 0; i < [paths count]; i++) { - CefRefPtr path = CefV8Value::CreateString([[paths objectAtIndex:i] UTF8String]); - pathsArray->SetValue(i, path); - } - retval = pathsArray; - - return true; - } - else if (name == "unwatchAllPaths") { - PathWatcher *pathWatcher = [PathWatcher pathWatcherForContext:CefV8Context::GetCurrentContext()]; - [pathWatcher unwatchAllPaths]; - return true; - } - else if (name == "makeDirectory") { - NSString *path = stringFromCefV8Value(arguments[0]); - NSFileManager *fm = [NSFileManager defaultManager]; - NSError *error = nil; - [fm createDirectoryAtPath:path withIntermediateDirectories:NO attributes:nil error:&error]; - - if (error) { - exception = [[error localizedDescription] UTF8String]; - } - - return true; - } - else if (name == "move") { - NSString *sourcePath = stringFromCefV8Value(arguments[0]); - NSString *targetPath = stringFromCefV8Value(arguments[1]); - NSFileManager *fm = [NSFileManager defaultManager]; - - NSError *error = nil; - [fm moveItemAtPath:sourcePath toPath:targetPath error:&error]; - - if (error) { - exception = [[error localizedDescription] UTF8String]; - } - - return true; - } - else if (name == "moveToTrash") { - NSString *sourcePath = stringFromCefV8Value(arguments[0]); - bool success = [[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation - source:[sourcePath stringByDeletingLastPathComponent] - destination:@"" - files:[NSArray arrayWithObject:[sourcePath lastPathComponent]] - tag:nil]; - - if (!success) { - std::string exception = "Can not move "; - exception += [sourcePath UTF8String]; - exception += " to trash."; - } - - return true; - } - else if (name == "reload") { - CefV8Context::GetCurrentContext()->GetBrowser()->ReloadIgnoreCache(); - } - else if (name == "lastModified") { - NSString *path = stringFromCefV8Value(arguments[0]); - NSFileManager *fm = [NSFileManager defaultManager]; - - NSError *error = nil; - NSDictionary *attributes = [fm attributesOfItemAtPath:path error:&error]; - - if (error) { - exception = [[error localizedDescription] UTF8String]; - } - - NSDate *lastModified = [attributes objectForKey:NSFileModificationDate]; - retval = CefV8Value::CreateDate(CefTime([lastModified timeIntervalSince1970])); - return true; - } - else if (name == "md5ForPath") { - NSString *path = stringFromCefV8Value(arguments[0]); - unsigned char outputData[CC_MD5_DIGEST_LENGTH]; - - NSData *inputData = [[NSData alloc] initWithContentsOfFile:path]; - CC_MD5([inputData bytes], [inputData length], outputData); - [inputData release]; - - NSMutableString *hash = [[NSMutableString alloc] init]; - - for (NSUInteger i = 0; i < CC_MD5_DIGEST_LENGTH; i++) { - [hash appendFormat:@"%02x", outputData[i]]; - } - - retval = CefV8Value::CreateString([hash UTF8String]); - return true; - } - else if (name == "exec") { - NSString *command = stringFromCefV8Value(arguments[0]); - CefRefPtr options = arguments[1]; - CefRefPtr callback = arguments[2]; - - NSTask *task = [[NSTask alloc] init]; - [task setLaunchPath:@"/bin/sh"]; - [task setStandardInput:[NSFileHandle fileHandleWithNullDevice]]; - [task setArguments:[NSArray arrayWithObjects:@"-l", @"-c", command, nil]]; - - NSPipe *stdout = [NSPipe pipe]; - NSPipe *stderr = [NSPipe pipe]; - [task setStandardOutput:stdout]; - [task setStandardError:stderr]; - - CefRefPtr context = CefV8Context::GetCurrentContext(); - void (^outputHandle)(NSString *contents, CefRefPtr function) = nil; - void (^taskTerminatedHandle)(NSString *output, NSString *errorOutput) = nil; - - outputHandle = ^(NSString *contents, CefRefPtr function) { - context->Enter(); - - CefV8ValueList args; - args.push_back(CefV8Value::CreateString(std::string([contents UTF8String], [contents lengthOfBytesUsingEncoding:NSUTF8StringEncoding]))); - CefRefPtr retval = function->ExecuteFunction(function, args); - - if (function->HasException()) { - throwException(context->GetGlobal(), function->GetException(), @"Error thrown in OutputHandle"); - } - - context->Exit(); - }; - - taskTerminatedHandle = ^(NSString *output, NSString *errorOutput) { - context->Enter(); - - CefV8ValueList args; - args.push_back(CefV8Value::CreateInt([task terminationStatus])); - args.push_back(CefV8Value::CreateString([output UTF8String])); - args.push_back(CefV8Value::CreateString([errorOutput UTF8String])); - - callback->ExecuteFunction(callback, args); - - if (callback->HasException()) { - throwException(context->GetGlobal(), callback->GetException(), @"Error thrown in TaskTerminatedHandle"); - } - - context->Exit(); - - stdout.fileHandleForReading.writeabilityHandler = nil; - stderr.fileHandleForReading.writeabilityHandler = nil; - }; - - task.terminationHandler = ^(NSTask *) { - NSString *output = [[NSString alloc] initWithData:[[stdout fileHandleForReading] readDataToEndOfFile] encoding:NSUTF8StringEncoding]; - NSString *errorOutput = [[NSString alloc] initWithData:[[stderr fileHandleForReading] readDataToEndOfFile] encoding:NSUTF8StringEncoding]; - dispatch_sync(dispatch_get_main_queue(), ^() { - taskTerminatedHandle(output, errorOutput); - }); - [output release]; - [errorOutput release]; - }; - - CefRefPtr stdoutFunction = options->GetValue("stdout"); - if (stdoutFunction->IsFunction()) { - stdout.fileHandleForReading.writeabilityHandler = ^(NSFileHandle *fileHandle) { - NSData *data = [fileHandle availableData]; - NSString *contents = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - dispatch_sync(dispatch_get_main_queue(), ^() { - outputHandle(contents, stdoutFunction); - }); - [contents release]; - }; - } - - CefRefPtr stderrFunction = options->GetValue("stderr"); - if (stderrFunction->IsFunction()) { - stderr.fileHandleForReading.writeabilityHandler = ^(NSFileHandle *fileHandle) { - NSData *data = [fileHandle availableData]; - NSString *contents = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - dispatch_sync(dispatch_get_main_queue(), ^() { - outputHandle(contents, stderrFunction); - }); - [contents release]; - }; - } - - [task launch]; - - return true; - } - else if (name == "getPlatform") { - retval = CefV8Value::CreateString("mac"); - return true; + NSString *stringFromCefV8Value(const CefRefPtr& value) { + std::string cc_value = value->GetStringValue().ToString(); + return [NSString stringWithUTF8String:cc_value.c_str()]; } - else if (name == "setWindowState") { - windowState = arguments[0]->GetStringValue().ToString(); - return true; - } + void throwException(const CefRefPtr& global, CefRefPtr exception, NSString *message) { + CefV8ValueList arguments; - else if (name == "getWindowState") { - retval = CefV8Value::CreateString(windowState); - return true; - } + message = [message stringByAppendingFormat:@"\n%s", exception->GetMessage().ToString().c_str()]; + arguments.push_back(CefV8Value::CreateString(std::string([message UTF8String], [message lengthOfBytesUsingEncoding:NSUTF8StringEncoding]))); - return false; -}; + CefRefPtr console = global->GetValue("console"); + console->GetValue("error")->ExecuteFunction(console, arguments); + } } // namespace v8_extensions diff --git a/native/v8_extensions/onig_reg_exp.h b/native/v8_extensions/onig_reg_exp.h index 4a1725c48..4db0d0e32 100644 --- a/native/v8_extensions/onig_reg_exp.h +++ b/native/v8_extensions/onig_reg_exp.h @@ -3,18 +3,22 @@ namespace v8_extensions { -class OnigRegExp : public CefV8Handler { -public: - OnigRegExp(); - - virtual bool Execute(const CefString& name, - CefRefPtr object, - const CefV8ValueList& arguments, - CefRefPtr& retval, - CefString& exception) OVERRIDE; - - // Provide the reference counting implementation for this class. - IMPLEMENT_REFCOUNTING(OnigRegExp); -}; + class OnigRegExp : public CefV8Handler { + public: + OnigRegExp(); + void CreateContextBinding(CefRefPtr context); + virtual bool Execute(const CefString& name, + CefRefPtr object, + const CefV8ValueList& arguments, + CefRefPtr& retval, + CefString& exception) OVERRIDE; + + // Provide the reference counting implementation for this class. + IMPLEMENT_REFCOUNTING(OnigRegExp); + + private: + OnigRegExp(OnigRegExp const&); + void operator=(OnigRegExp const&); + }; } \ No newline at end of file diff --git a/native/v8_extensions/onig_reg_exp.js b/native/v8_extensions/onig_reg_exp.js deleted file mode 100644 index ae06d5e36..000000000 --- a/native/v8_extensions/onig_reg_exp.js +++ /dev/null @@ -1,19 +0,0 @@ -(function() { - native function buildOnigRegExp(source); - native function search(string, index); - native function test(string); - - function OnigRegExp(source) { - var regexp = buildOnigRegExp(source); - regexp.constructor = OnigRegExp; - regexp.__proto__ = OnigRegExp.prototype; - regexp.source = source; - return regexp; - } - - OnigRegExp.prototype.search = search; - OnigRegExp.prototype.test = test; - - this.OnigRegExp = OnigRegExp; -})(); - diff --git a/native/v8_extensions/onig_reg_exp.mm b/native/v8_extensions/onig_reg_exp.mm index ba1657b1f..6f8b5edcd 100644 --- a/native/v8_extensions/onig_reg_exp.mm +++ b/native/v8_extensions/onig_reg_exp.mm @@ -38,21 +38,33 @@ public: return resultArray; } - + CefRefPtr Test(CefRefPtr string, CefRefPtr index) { OnigResult *result = [m_regex search:stringFromCefV8Value(string) start:index->GetIntValue()]; return CefV8Value::CreateBool(result); } - + OnigRegexp *m_regex; IMPLEMENT_REFCOUNTING(OnigRegexpUserData); }; OnigRegExp::OnigRegExp() : CefV8Handler() { - NSString *filePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"v8_extensions/onig_reg_exp.js"]; - NSString *extensionCode = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil]; - CefRegisterExtension("v8/onig-reg-exp", [extensionCode UTF8String], this); +} + +void OnigRegExp::CreateContextBinding(CefRefPtr context) { + const char* methodNames[] = { "search", "test", "buildOnigRegExp" }; + + CefRefPtr nativeObject = CefV8Value::CreateObject(NULL); + int arrayLength = sizeof(methodNames) / sizeof(const char *); + for (int i = 0; i < arrayLength; i++) { + const char *functionName = methodNames[i]; + CefRefPtr function = CefV8Value::CreateFunction(functionName, this); + nativeObject->SetValue(functionName, function, V8_PROPERTY_ATTRIBUTE_NONE); + } + + CefRefPtr global = context->GetGlobal(); + global->SetValue("$onigRegExp", nativeObject, V8_PROPERTY_ATTRIBUTE_NONE); } bool OnigRegExp::Execute(const CefString& name, @@ -73,7 +85,7 @@ bool OnigRegExp::Execute(const CefString& name, CefRefPtr index = arguments.size() > 1 ? arguments[1] : CefV8Value::CreateInt(0); OnigRegExpUserData *userData = (OnigRegExpUserData *)object->GetUserData().get(); retval = userData->Test(string, index); - return true; + return true; } else if (name == "buildOnigRegExp") { CefRefPtr pattern = arguments[0]; diff --git a/native/v8_extensions/onig_scanner.h b/native/v8_extensions/onig_scanner.h index 462462dd1..33a2bb1af 100644 --- a/native/v8_extensions/onig_scanner.h +++ b/native/v8_extensions/onig_scanner.h @@ -2,19 +2,23 @@ #include "include/cef_v8.h" namespace v8_extensions { - + class OnigScanner : public CefV8Handler { public: OnigScanner(); - + void CreateContextBinding(CefRefPtr context); virtual bool Execute(const CefString& name, CefRefPtr object, const CefV8ValueList& arguments, CefRefPtr& retval, CefString& exception) OVERRIDE; - + // Provide the reference counting implementation for this class. IMPLEMENT_REFCOUNTING(OnigRegExp); + + private: + OnigScanner(OnigScanner const&); + void operator=(OnigScanner const&); }; - + } \ No newline at end of file diff --git a/native/v8_extensions/onig_scanner.js b/native/v8_extensions/onig_scanner.js deleted file mode 100644 index 9348ad48c..000000000 --- a/native/v8_extensions/onig_scanner.js +++ /dev/null @@ -1,17 +0,0 @@ -(function() { - native function buildScanner(sources); - native function findNextMatch(string, startPosition); - - function OnigScanner(sources) { - var scanner = buildScanner(sources); - scanner.constructor = OnigScanner; - scanner.__proto__ = OnigScanner.prototype; - scanner.sources = sources; - return scanner; - } - - OnigScanner.prototype.buildScanner = buildScanner; - OnigScanner.prototype.findNextMatch = findNextMatch; - - this.OnigScanner = OnigScanner; -})(); diff --git a/native/v8_extensions/onig_scanner.mm b/native/v8_extensions/onig_scanner.mm index b357ffeca..9d429de97 100644 --- a/native/v8_extensions/onig_scanner.mm +++ b/native/v8_extensions/onig_scanner.mm @@ -130,11 +130,22 @@ class OnigScannerUserData : public CefBase { }; OnigScanner::OnigScanner() : CefV8Handler() { - NSString *filePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"v8_extensions/onig_scanner.js"]; - NSString *extensionCode = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil]; - CefRegisterExtension("v8/onig-scanner", [extensionCode UTF8String], this); } +void OnigScanner::CreateContextBinding(CefRefPtr context) { + const char* methodNames[] = { "findNextMatch", "buildScanner" }; + + CefRefPtr nativeObject = CefV8Value::CreateObject(NULL); + int arrayLength = sizeof(methodNames) / sizeof(const char *); + for (int i = 0; i < arrayLength; i++) { + const char *functionName = methodNames[i]; + CefRefPtr function = CefV8Value::CreateFunction(functionName, this); + nativeObject->SetValue(functionName, function, V8_PROPERTY_ATTRIBUTE_NONE); + } + + CefRefPtr global = context->GetGlobal(); + global->SetValue("$onigScanner", nativeObject, V8_PROPERTY_ATTRIBUTE_NONE); +} bool OnigScanner::Execute(const CefString& name, CefRefPtr object, diff --git a/native/v8_extensions/tags.h b/native/v8_extensions/tags.h index 48cbcd34e..e8c6627bf 100644 --- a/native/v8_extensions/tags.h +++ b/native/v8_extensions/tags.h @@ -4,21 +4,23 @@ namespace v8_extensions { -class Tags : public CefV8Handler { -public: - Tags(); + class Tags : public CefV8Handler { + public: + Tags(); + void CreateContextBinding(CefRefPtr context); + virtual bool Execute(const CefString& name, + CefRefPtr object, + const CefV8ValueList& arguments, + CefRefPtr& retval, + CefString& exception) OVERRIDE; - virtual bool Execute(const CefString& name, - CefRefPtr object, - const CefV8ValueList& arguments, - CefRefPtr& retval, - CefString& exception) OVERRIDE; + // Provide the reference counting implementation for this class. + IMPLEMENT_REFCOUNTING(Tags); - // Provide the reference counting implementation for this class. - IMPLEMENT_REFCOUNTING(Tags); - -private: - CefRefPtr ParseEntry(tagEntry entry); -}; + private: + Tags(Tags const&); + void operator=(Tags const&); + CefRefPtr ParseEntry(tagEntry entry); + }; } diff --git a/native/v8_extensions/tags.js b/native/v8_extensions/tags.js deleted file mode 100644 index cdef853d0..000000000 --- a/native/v8_extensions/tags.js +++ /dev/null @@ -1,10 +0,0 @@ -var $tags = {}; -(function() { - - native function find(path, tag); - $tags.find = find; - - native function getAllTagsAsync(path, callback); - $tags.getAllTagsAsync = getAllTagsAsync; - -})(); diff --git a/native/v8_extensions/tags.mm b/native/v8_extensions/tags.mm index 000bbdc12..74e46f275 100644 --- a/native/v8_extensions/tags.mm +++ b/native/v8_extensions/tags.mm @@ -3,100 +3,112 @@ namespace v8_extensions { -Tags::Tags() : CefV8Handler() { - NSString *filePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"v8_extensions/tags.js"]; - NSString *extensionCode = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil]; - CefRegisterExtension("v8/tags", [extensionCode UTF8String], this); -} - -CefRefPtr Tags::ParseEntry(tagEntry entry) { - CefRefPtr tagEntry = CefV8Value::CreateObject(NULL); - tagEntry->SetValue("name", CefV8Value::CreateString(entry.name), V8_PROPERTY_ATTRIBUTE_NONE); - tagEntry->SetValue("file", CefV8Value::CreateString(entry.file), V8_PROPERTY_ATTRIBUTE_NONE); - if (entry.address.pattern) { - tagEntry->SetValue("pattern", CefV8Value::CreateString(entry.address.pattern), V8_PROPERTY_ATTRIBUTE_NONE); + Tags::Tags() : CefV8Handler() { } - return tagEntry; -} -bool Tags::Execute(const CefString& name, - CefRefPtr object, - const CefV8ValueList& arguments, - CefRefPtr& retval, - CefString& exception) { + void Tags::CreateContextBinding(CefRefPtr context) { + const char* methodNames[] = { "find", "getAllTagsAsync" }; - 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); + CefRefPtr nativeObject = CefV8Value::CreateObject(NULL); + int arrayLength = sizeof(methodNames) / sizeof(const char *); + for (int i = 0; i < arrayLength; i++) { + const char *functionName = methodNames[i]; + CefRefPtr function = CefV8Value::CreateFunction(functionName, this); + nativeObject->SetValue(functionName, function, V8_PROPERTY_ATTRIBUTE_NONE); } - return true; + + CefRefPtr global = context->GetGlobal(); + global->SetValue("$tags", nativeObject, V8_PROPERTY_ATTRIBUTE_NONE); } - if (name == "getAllTagsAsync") { - std::string path = arguments[0]->GetStringValue().ToString(); - CefRefPtr callback = arguments[1]; - CefRefPtr context = CefV8Context::GetCurrentContext(); + CefRefPtr Tags::ParseEntry(tagEntry entry) { + CefRefPtr tagEntry = CefV8Value::CreateObject(NULL); + tagEntry->SetValue("name", CefV8Value::CreateString(entry.name), V8_PROPERTY_ATTRIBUTE_NONE); + tagEntry->SetValue("file", CefV8Value::CreateString(entry.file), V8_PROPERTY_ATTRIBUTE_NONE); + if (entry.address.pattern) { + tagEntry->SetValue("pattern", CefV8Value::CreateString(entry.address.pattern), V8_PROPERTY_ATTRIBUTE_NONE); + } + return tagEntry; + } - dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - dispatch_async(queue, ^{ + bool Tags::Execute(const CefString& name, + CefRefPtr object, + const CefV8ValueList& arguments, + 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); - 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(); + + 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 true; + } + + return false; } - return false; -} - } diff --git a/script/fix-author b/script/fix-author new file mode 100755 index 000000000..bbbd3b3da --- /dev/null +++ b/script/fix-author @@ -0,0 +1,17 @@ +#!/bin/sh + +usage() { + echo "usage: $0 sha name email" + exit 1 +} + +if [ ! $3 ]; then + usage +fi + +git filter-branch -f --env-filter " +export GIT_AUTHOR_NAME='$2' +export GIT_AUTHOR_EMAIL='$3' +export GIT_COMMITTER_NAME='$2' +export GIT_COMMITTER_EMAIL='$3' +" -- $1..HEAD \ No newline at end of file diff --git a/script/update-cef b/script/update-cef index d58b03a0b..817c65575 100755 --- a/script/update-cef +++ b/script/update-cef @@ -1,7 +1,7 @@ #!/bin/sh usage() { - echo 'usage: `basename $0` 'chromium-dir' [cef-release-branch] 1>&2' + echo 'usage: update-cef chromium-dir [cef-release-branch] 1>&2' echo echo 'chromium-dir is the root of the chromium directory (i.e. ~/code/chromium)' echo 'CEF release branches can be found at http://code.google.com/p/chromiumembedded/wiki/BranchesAndBuilding' @@ -14,9 +14,13 @@ fi ATOM_ROOT=$(cd $(dirname $0); pwd -P)/.. CHROMIUM_DIR=$1 -CEF_DIR=$CHROMIUM_DIR/src/cef/ +CEF_DIR=$CHROMIUM_DIR/src/cef CEF_RELEASE_BRANCH=${2:-1271} -CEF_BINARY_PATH=$(echo "$CEF_DIR"/binary_distrib/cef_binary_*_macosx/) # Expand the path +CEF_REVISION=$(svnversion $CEF_DIR) +CEF_BINARY_PATH=$(echo ${CEF_DIR}/binary_distrib/cef_binary_3.${CEF_RELEASE_BRANCH}.${CEF_REVISION}_macosx/) # Expand the path + +echo "CEF_RELEASE_BRANCH=$CEF_RELEASE_BRANCH" +echo "CEF_BINARY_PATH=$CEF_BINARY_PATH" # Update and compile CEF CEF_AUTOMATE_SCRIPT_PATH=/tmp/cef-update diff --git a/script/update-libgit2 b/script/update-libgit2 index 7f6face0c..301b1f24c 100755 --- a/script/update-libgit2 +++ b/script/update-libgit2 @@ -1,4 +1,18 @@ #!/bin/sh +# 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 --build . --target install +# +# From root of atom repo: +# mv git2/lib/libgit2.0.17.0.dylib git2/frameworks/libgit2.0.17.0.dylib +# rm -fr git2/lib +# script/update-libgit2 + # update the id of the dylib install_name_tool -id @executable_path/libgit2.0.17.0.dylib git2/frameworks/libgit2.0.17.0.dylib + +# Verify @executable_path is in the output of: +otool -L git2/frameworks/libgit2.0.17.0.dylib diff --git a/spec/app/atom-spec.coffee b/spec/app/atom-spec.coffee index f09390343..73474a2a1 100644 --- a/spec/app/atom-spec.coffee +++ b/spec/app/atom-spec.coffee @@ -19,23 +19,27 @@ describe "the `atom` global", -> expect(rootView.activatePackage).toHaveBeenCalled() it "logs warning instead of throwing an exception if a package fails to load", -> + config.set("core.disabledPackages", []) spyOn(console, "warn") expect(-> atom.loadPackage("package-that-throws-an-exception")).not.toThrow() expect(console.warn).toHaveBeenCalled() describe "keymap loading", -> describe "when package.json does not contain a 'keymaps' manifest", -> - it "loads all keymaps in the directory", -> + it "loads all the .cson/.json files in the keymaps directory", -> element1 = $$ -> @div class: 'test-1' element2 = $$ -> @div class: 'test-2' + element3 = $$ -> @div class: 'test-3' expect(keymap.bindingsForElement(element1)['ctrl-z']).toBeUndefined() expect(keymap.bindingsForElement(element2)['ctrl-z']).toBeUndefined() + expect(keymap.bindingsForElement(element3)['ctrl-z']).toBeUndefined() atom.loadPackage("package-with-module") expect(keymap.bindingsForElement(element1)['ctrl-z']).toBe "test-1" expect(keymap.bindingsForElement(element2)['ctrl-z']).toBe "test-2" + expect(keymap.bindingsForElement(element3)['ctrl-z']).toBeUndefined() describe "when package.json contains a 'keymaps' manifest", -> it "loads only the keymaps specified by the manifest, in the specified order", -> @@ -55,3 +59,22 @@ describe "the `atom` global", -> expect(stylesheetElementForId(stylesheetPath).length).toBe 0 atom.loadPackage("package-with-module") expect(stylesheetElementForId(stylesheetPath).length).toBe 1 + + describe ".loadPackages()", -> + beforeEach -> + spyOn(syntax, 'addGrammar') + + it "terminates the worker when all packages have been loaded", -> + spyOn(Worker.prototype, 'terminate').andCallThrough() + eventHandler = jasmine.createSpy('eventHandler') + syntax.on 'grammars-loaded', eventHandler + disabledPackages = config.get("core.disabledPackages") + disabledPackages.push('textmate-package.tmbundle') + config.set "core.disabledPackages", disabledPackages + atom.loadPackages() + + waitsFor "all packages to load", 5000, -> eventHandler.callCount > 0 + + runs -> + expect(Worker.prototype.terminate).toHaveBeenCalled() + expect(Worker.prototype.terminate.calls.length).toBe 1 diff --git a/spec/app/buffer-spec.coffee b/spec/app/buffer-spec.coffee index a93bae30a..2a1eaa30c 100644 --- a/spec/app/buffer-spec.coffee +++ b/spec/app/buffer-spec.coffee @@ -465,6 +465,14 @@ describe 'Buffer', -> range = [[2,10], [4,10]] expect(buffer.getTextInRange(range)).toBe "ems.length <= 1) return items;\n var pivot = items.shift(), current, left = [], right = [];\n while(" + describe "when the range starts before the start of the buffer", -> + it "clips the range to the start of the buffer", -> + expect(buffer.getTextInRange([[-Infinity, -Infinity], [0, Infinity]])).toBe buffer.lineForRow(0) + + describe "when the range ends after the end of the buffer", -> + it "clips the range to the end of the buffer", -> + expect(buffer.getTextInRange([[12], [13, Infinity]])).toBe buffer.lineForRow(12) + describe ".scanInRange(range, regex, fn)", -> describe "when given a regex with a ignore case flag", -> it "does a case-insensitive search", -> @@ -833,3 +841,65 @@ describe 'Buffer', -> expect(buffer.getText()).toBe "a" buffer.append("b\nc"); expect(buffer.getText()).toBe "ab\nc" + + describe "line ending support", -> + describe ".lineEndingForRow(line)", -> + it "return the line ending for each buffer line", -> + buffer.setText("a\r\nb\nc") + expect(buffer.lineEndingForRow(0)).toBe '\r\n' + expect(buffer.lineEndingForRow(1)).toBe '\n' + expect(buffer.lineEndingForRow(2)).toBeUndefined() + + describe ".lineForRow(line)", -> + it "returns the line text without the line ending for both lf and crlf lines", -> + buffer.setText("a\r\nb\nc") + expect(buffer.lineForRow(0)).toBe 'a' + expect(buffer.lineForRow(1)).toBe 'b' + expect(buffer.lineForRow(2)).toBe 'c' + + describe ".getText()", -> + it "returns the text with the corrent line endings for each row", -> + buffer.setText("a\r\nb\nc") + expect(buffer.getText()).toBe "a\r\nb\nc" + buffer.setText("a\r\nb\nc\n") + expect(buffer.getText()).toBe "a\r\nb\nc\n" + + describe "when editing a line", -> + it "preserves the existing line ending", -> + buffer.setText("a\r\nb\nc") + buffer.insert([0, 1], "1") + expect(buffer.getText()).toBe "a1\r\nb\nc" + + describe "when inserting text with multiple lines", -> + describe "when the current line has a line ending", -> + it "uses the same line ending as the line where the text is inserted", -> + buffer.setText("a\r\n") + buffer.insert([0,1], "hello\n1\n\n2") + expect(buffer.getText()).toBe "ahello\r\n1\r\n\r\n2\r\n" + + describe "when the current line has no line ending (because it's the last line of the buffer)", -> + describe "when the buffer contains only a single line", -> + it "honors the line endings in the inserted text", -> + buffer.setText("initialtext") + buffer.append("hello\n1\r\n2\n") + expect(buffer.getText()).toBe "initialtexthello\n1\r\n2\n" + + describe "when the buffer contains a preceding line", -> + it "uses the line ending of the preceding line", -> + buffer.setText("\ninitialtext") + buffer.append("hello\n1\r\n2\n") + expect(buffer.getText()).toBe "\ninitialtexthello\n1\n2\n" + + describe ".clipPosition(position)", -> + describe "when the position is before the start of the buffer", -> + it "returns the first position in the buffer", -> + expect(buffer.clipPosition([-1,0])).toEqual [0,0] + expect(buffer.clipPosition([0,-1])).toEqual [0,0] + expect(buffer.clipPosition([-1,-1])).toEqual [0,0] + + describe "when the position is after the end of the buffer", -> + it "returns the last position in the buffer", -> + buffer.setText('some text') + expect(buffer.clipPosition([1, 0])).toEqual [0,9] + expect(buffer.clipPosition([0,10])).toEqual [0,9] + expect(buffer.clipPosition([10,Infinity])).toEqual [0,9] diff --git a/spec/app/config-spec.coffee b/spec/app/config-spec.coffee index 7edf47dcf..661ef3c78 100644 --- a/spec/app/config-spec.coffee +++ b/spec/app/config-spec.coffee @@ -22,17 +22,36 @@ describe "Config", -> spyOn(fs, 'write') jasmine.unspy config, 'save' - it "writes any non-default properties to the config.json in the user's .atom directory", -> - config.set("a.b.c", 1) - config.set("a.b.d", 2) - config.set("x.y.z", 3) - config.setDefaults("a.b", e: 4, f: 5) + describe "when ~/.atom/config.json exists", -> + it "writes any non-default properties to ~/.atom/config.json", -> + config.configFilePath = fs.join(config.configDirPath, "config.json") + config.set("a.b.c", 1) + config.set("a.b.d", 2) + config.set("x.y.z", 3) + config.setDefaults("a.b", e: 4, f: 5) - fs.write.reset() - config.save() + fs.write.reset() + config.save() - writtenConfig = JSON.parse(fs.write.argsForCall[0][1]) - expect(writtenConfig).toEqual config.settings + expect(fs.write.argsForCall[0][0]).toBe(fs.join(config.configDirPath, "config.json")) + writtenConfig = JSON.parse(fs.write.argsForCall[0][1]) + expect(writtenConfig).toEqual config.settings + + describe "when ~/.atom/config.json doesn't exist", -> + it "writes any non-default properties to ~/.atom/config.cson", -> + config.configFilePath = fs.join(config.configDirPath, "config.cson") + config.set("a.b.c", 1) + config.set("a.b.d", 2) + config.set("x.y.z", 3) + config.setDefaults("a.b", e: 4, f: 5) + + fs.write.reset() + config.save() + + expect(fs.write.argsForCall[0][0]).toBe(fs.join(config.configDirPath, "config.cson")) + {CoffeeScript} = require 'coffee-script' + writtenConfig = CoffeeScript.eval(fs.write.argsForCall[0][1], bare: true) + expect(writtenConfig).toEqual config.settings describe ".setDefaults(keyPath, defaults)", -> it "assigns any previously-unassigned keys to the object at the key path", -> diff --git a/spec/app/display-buffer-spec.coffee b/spec/app/display-buffer-spec.coffee index cd7ca484c..7444a3859 100644 --- a/spec/app/display-buffer-spec.coffee +++ b/spec/app/display-buffer-spec.coffee @@ -495,6 +495,20 @@ describe "DisplayBuffer", -> expect(displayBuffer.lineForRow(8).text).toMatch /^9-+/ expect(displayBuffer.lineForRow(10).fold).toBeDefined() + describe "when the line being deleted preceeds a fold", -> + describe "when the command is undone", -> + it "restores the line and preserves the fold", -> + editSession.setCursorBufferPosition([4]) + editSession.foldCurrentRow() + expect(editSession.isFoldedAtScreenRow(4)).toBeTruthy() + editSession.setCursorBufferPosition([3]) + editSession.deleteLine() + expect(editSession.isFoldedAtScreenRow(3)).toBeTruthy() + expect(buffer.lineForRow(3)).toBe ' while(items.length > 0) {' + editSession.undo() + expect(editSession.isFoldedAtScreenRow(4)).toBeTruthy() + expect(buffer.lineForRow(3)).toBe ' var pivot = items.shift(), current, left = [], right = [];' + describe ".clipScreenPosition(screenPosition, wrapBeyondNewlines: false, wrapAtSoftNewlines: false, skipAtomicTokens: false)", -> beforeEach -> displayBuffer.setSoftWrapColumn(50) diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 6f72cd8bc..c77ddd15c 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -276,17 +276,22 @@ describe "EditSession", -> editSession.moveCursorToBeginningOfWord() expect(cursor1.getBufferPosition()).toEqual [0, 4] - expect(cursor2.getBufferPosition()).toEqual [1, 10] + expect(cursor2.getBufferPosition()).toEqual [1, 11] expect(cursor3.getBufferPosition()).toEqual [2, 39] it "does not fail at position [0, 0]", -> editSession.setCursorBufferPosition([0, 0]) editSession.moveCursorToBeginningOfWord() - it "works when the preceding line is blank", -> + it "treats lines with only whitespace as a word", -> + editSession.setCursorBufferPosition([11, 0]) + editSession.moveCursorToBeginningOfWord() + expect(editSession.getCursorBufferPosition()).toEqual [10, 0] + + it "works when the current line is blank", -> editSession.setCursorBufferPosition([10, 0]) editSession.moveCursorToBeginningOfWord() - expect(editSession.getCursorBufferPosition()).toEqual [9, 0] + expect(editSession.getCursorBufferPosition()).toEqual [9, 2] describe ".moveCursorToEndOfWord()", -> it "moves the cursor to the end of the word", -> @@ -298,8 +303,8 @@ describe "EditSession", -> editSession.moveCursorToEndOfWord() expect(cursor1.getBufferPosition()).toEqual [0, 13] - expect(cursor2.getBufferPosition()).toEqual [1, 13] - expect(cursor3.getBufferPosition()).toEqual [3, 4] + expect(cursor2.getBufferPosition()).toEqual [1, 12] + expect(cursor3.getBufferPosition()).toEqual [3, 7] it "does not blow up when there is no next word", -> editSession.setCursorBufferPosition [Infinity, Infinity] @@ -307,6 +312,17 @@ describe "EditSession", -> editSession.moveCursorToEndOfWord() expect(editSession.getCursorBufferPosition()).toEqual endPosition + it "treats lines with only whitespace as a word", -> + editSession.setCursorBufferPosition([9, 4]) + editSession.moveCursorToEndOfWord() + expect(editSession.getCursorBufferPosition()).toEqual [10, 0] + + it "works when the current line is blank", -> + editSession.setCursorBufferPosition([10, 0]) + editSession.moveCursorToEndOfWord() + expect(editSession.getCursorBufferPosition()).toEqual [11, 8] + + describe ".getCurrentParagraphBufferRange()", -> it "returns the buffer range of the current paragraph, delimited by blank lines or the beginning / end of the file", -> buffer.setText """ @@ -518,13 +534,13 @@ describe "EditSession", -> expect(editSession.getCursors().length).toBe 2 [cursor1, cursor2] = editSession.getCursors() expect(cursor1.getBufferPosition()).toEqual [0,4] - expect(cursor2.getBufferPosition()).toEqual [3,44] + expect(cursor2.getBufferPosition()).toEqual [3,47] expect(editSession.getSelections().length).toBe 2 [selection1, selection2] = editSession.getSelections() expect(selection1.getBufferRange()).toEqual [[0,4], [0,13]] expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[3,44], [3,49]] + expect(selection2.getBufferRange()).toEqual [[3,47], [3,49]] expect(selection2.isReversed()).toBeTruthy() describe ".selectToEndOfWord()", -> @@ -537,40 +553,49 @@ describe "EditSession", -> expect(editSession.getCursors().length).toBe 2 [cursor1, cursor2] = editSession.getCursors() expect(cursor1.getBufferPosition()).toEqual [0,13] - expect(cursor2.getBufferPosition()).toEqual [3,51] + expect(cursor2.getBufferPosition()).toEqual [3,50] expect(editSession.getSelections().length).toBe 2 [selection1, selection2] = editSession.getSelections() expect(selection1.getBufferRange()).toEqual [[0,4], [0,13]] expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual [[3,48], [3,51]] + expect(selection2.getBufferRange()).toEqual [[3,48], [3,50]] expect(selection2.isReversed()).toBeFalsy() describe ".selectWord()", -> - describe "when the cursor is inside a word", -> - it "selects the entire word", -> - editSession.setCursorScreenPosition([0, 8]) - editSession.selectWord() - expect(editSession.getSelectedText()).toBe 'quicksort' + describe "when the cursor is inside a word", -> + it "selects the entire word", -> + editSession.setCursorScreenPosition([0, 8]) + editSession.selectWord() + expect(editSession.getSelectedText()).toBe 'quicksort' - describe "when the cursor is between two words", -> - it "selects both words", -> - editSession.setCursorScreenPosition([0, 4]) - editSession.selectWord() - expect(editSession.getSelectedText()).toBe ' quicksort' + describe "when the cursor is between two words", -> + it "selects the word the cursor is on", -> + editSession.setCursorScreenPosition([0, 4]) + editSession.selectWord() + expect(editSession.getSelectedText()).toBe 'quicksort' - describe "when the cursor is inside a region of whitespace", -> - it "selects the whitespace region", -> - editSession.setCursorScreenPosition([5, 2]) - editSession.selectWord() - expect(editSession.getSelectedBufferRange()).toEqual [[5, 0], [5, 6]] + editSession.setCursorScreenPosition([0, 3]) + editSession.selectWord() + expect(editSession.getSelectedText()).toBe 'var' - describe "when the cursor is at the end of the text", -> - it "select the previous word", -> - editSession.buffer.append 'word' - editSession.moveCursorToBottom() - editSession.selectWord() - expect(editSession.getSelectedBufferRange()).toEqual [[12, 2], [12, 6]] + + describe "when the cursor is inside a region of whitespace", -> + it "selects the whitespace region", -> + editSession.setCursorScreenPosition([5, 2]) + editSession.selectWord() + expect(editSession.getSelectedBufferRange()).toEqual [[5, 0], [5, 6]] + + editSession.setCursorScreenPosition([5, 0]) + editSession.selectWord() + expect(editSession.getSelectedBufferRange()).toEqual [[5, 0], [5, 6]] + + describe "when the cursor is at the end of the text", -> + it "select the previous word", -> + editSession.buffer.append 'word' + editSession.moveCursorToBottom() + editSession.selectWord() + expect(editSession.getSelectedBufferRange()).toEqual [[12, 2], [12, 6]] describe ".setSelectedBufferRanges(ranges)", -> it "clears existing selections and creates selections for each of the given ranges", -> @@ -1110,25 +1135,26 @@ describe "EditSession", -> describe "when no text is selected", -> it "deletes all text between the cursor and the beginning of the word", -> editSession.setCursorBufferPosition([1, 24]) - editSession.addCursorAtBufferPosition([2, 5]) + editSession.addCursorAtBufferPosition([3, 5]) [cursor1, cursor2] = editSession.getCursors() editSession.backspaceToBeginningOfWord() expect(buffer.lineForRow(1)).toBe ' var sort = function(ems) {' - expect(buffer.lineForRow(2)).toBe ' f (items.length <= 1) return items;' + expect(buffer.lineForRow(3)).toBe ' ar pivot = items.shift(), current, left = [], right = [];' expect(cursor1.getBufferPosition()).toEqual [1, 22] - expect(cursor2.getBufferPosition()).toEqual [2, 4] + expect(cursor2.getBufferPosition()).toEqual [3, 4] editSession.backspaceToBeginningOfWord() expect(buffer.lineForRow(1)).toBe ' var sort = functionems) {' - expect(buffer.lineForRow(2)).toBe 'f (items.length <= 1) return items;' + expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return itemsar pivot = items.shift(), current, left = [], right = [];' expect(cursor1.getBufferPosition()).toEqual [1, 21] - expect(cursor2.getBufferPosition()).toEqual [2, 0] + expect(cursor2.getBufferPosition()).toEqual [2, 39] editSession.backspaceToBeginningOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = emsf (items.length <= 1) return items;' + expect(buffer.lineForRow(1)).toBe ' var sort = ems) {' + expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return ar pivot = items.shift(), current, left = [], right = [];' expect(cursor1.getBufferPosition()).toEqual [1, 13] - expect(cursor2.getBufferPosition()).toEqual [1, 16] + expect(cursor2.getBufferPosition()).toEqual [2, 34] describe "when text is selected", -> it "deletes only selected text", -> @@ -1296,7 +1322,7 @@ describe "EditSession", -> expect(cursor2.getBufferPosition()).toEqual [2, 5] editSession.deleteToEndOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it' + expect(buffer.lineForRow(1)).toBe ' var sort = function(it {' expect(buffer.lineForRow(2)).toBe ' iitems.length <= 1) return items;' expect(cursor1.getBufferPosition()).toEqual [1, 24] expect(cursor2.getBufferPosition()).toEqual [2, 5] diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index dd67f2946..25620a25e 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -111,11 +111,9 @@ describe "Editor", -> editor.isFocused = false editor.hiddenInput.focus() expect(editor.isFocused).toBeTruthy() - expect(editor).toHaveClass('focused') editor.hiddenInput.focusout() expect(editor.isFocused).toBeFalsy() - expect(editor).not.toHaveClass('focused') describe "when the activeEditSession's file is modified on disk", -> it "triggers an alert", -> @@ -516,14 +514,61 @@ describe "Editor", -> editor.getBuffer().saveAs(path) expect(editor.getGrammar().name).toBe 'Plain Text' - describe "font size", -> - it "sets the initial font size based on the value from config", -> - config.set("editor.fontSize", 20) - newEditor = editor.splitRight() - expect(editor.css('font-size')).toBe '20px' - expect(newEditor.css('font-size')).toBe '20px' + describe "font family", -> + beforeEach -> + expect(editor.css('font-family')).not.toBe 'Courier' + + it "when there is no config in fontFamily don't set it", -> + expect($("head style.font-family")).not.toExist() + + describe "when the font family changes", -> + 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.font-family").text()).toMatch "{font-family: Courier}" + expect(editor.css('font-family')).toBe 'Courier' + expect(newEditor.css('font-family')).toBe 'Courier' + + it "updates the font family of editors and recalculates dimensions critical to cursor positioning", -> + rootView.attachToDom() + rootView.height(200) + rootView.width(200) + + lineHeightBefore = editor.lineHeight + charWidthBefore = editor.charWidth + config.set("editor.fontFamily", "Inconsolata") + editor.setCursorScreenPosition [5, 6] + 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 + + describe "font size", -> + beforeEach -> + expect(editor.css('font-size')).not.toBe "20px" + expect(editor.css('font-size')).not.toBe "10px" + + it "sets the initial font size based on the value from config", -> + expect($("head style.font-size")).toExist() + 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' - describe "when the font size changes on the view", -> it "updates the font sizes of editors and recalculates dimensions critical to cursor positioning", -> rootView.attachToDom() rootView.height(200) @@ -558,7 +603,7 @@ describe "Editor", -> rootView.attachToDom() config.set("editor.fontSize", 16 * 4) expect(editor.gutter.css('font-size')).toBe "#{16 * 4}px" - expect(editor.gutter.width()).toBe(64) + expect(editor.gutter.width()).toBe(64 + editor.gutter.calculateLineNumberPadding()) it "updates lines if there are unrendered lines", -> editor.attachToDom(heightInLines: 5) @@ -568,6 +613,23 @@ describe "Editor", -> config.set("editor.fontSize", 10) expect(editor.renderedLines.find(".line").length).toBeGreaterThan originalLineCount + describe "when the editor is detached", -> + it "updates the font-size correctly and recalculates the dimensions by placing the rendered lines on the DOM", -> + rootView.attachToDom() + rootView.height(200) + rootView.width(200) + + newEditor = editor.splitRight() + newEditorParent = newEditor.parent() + newEditor.detach() + config.set("editor.fontSize", 10) + newEditorParent.append(newEditor) + + 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() + describe "mouse events", -> beforeEach -> editor.attachToDom() @@ -1604,6 +1666,49 @@ describe "Editor", -> expect(rightEditor.find(".line:first").text()).toBe "_tab _;" expect(leftEditor.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" + editor.attachToDom() + + expect(config.get("editor.showInvisibles")).toBeFalsy() + expect(editor.renderedLines.find('.line:first').text()).toBe "a line that ends with a carriage return" + + config.set("editor.showInvisibles", true) + cr = editor.invisibles?.cr + expect(cr).toBeTruthy() + eol = editor.invisibles?.eol + expect(eol).toBeTruthy() + expect(editor.renderedLines.find('.line:first').text()).toBe "a line that ends with a carriage return#{cr}#{eol}" + + + describe "when wrapping is on", -> + it "doesn't show the end of line invisible at the end of lines broken due to wrapping", -> + editor.setSoftWrapColumn(6) + editor.setText "a line that wraps" + editor.attachToDom() + config.set "editor.showInvisibles", true + space = editor.invisibles?.space + expect(space).toBeTruthy() + eol = editor.invisibles?.eol + expect(eol).toBeTruthy() + expect(editor.renderedLines.find('.line:first').text()).toBe "a line#{space}" + expect(editor.renderedLines.find('.line:last').text()).toBe "wraps#{eol}" + + it "displays trailing carriage return using a visible non-empty value", -> + editor.setSoftWrapColumn(6) + editor.setText "a line that\r\n" + editor.attachToDom() + config.set "editor.showInvisibles", true + space = editor.invisibles?.space + expect(space).toBeTruthy() + cr = editor.invisibles?.cr + expect(cr).toBeTruthy() + eol = editor.invisibles?.eol + expect(eol).toBeTruthy() + expect(editor.renderedLines.find('.line:first').text()).toBe "a line#{space}" + expect(editor.renderedLines.find('.line:eq(1)').text()).toBe "that#{cr}#{eol}" + expect(editor.renderedLines.find('.line:last').text()).toBe "#{eol}" + describe "gutter rendering", -> beforeEach -> editor.attachToDom(heightInLines: 5.5) @@ -1687,6 +1792,11 @@ describe "Editor", -> fold.destroy() expect(editor.gutter.find('.line-number').length).toBe 13 + it "styles folded line numbers", -> + editor.createFold(3, 5) + expect(editor.gutter.find('.line-number.fold').length).toBe 1 + expect(editor.gutter.find('.line-number.fold:eq(0)').text()).toBe '4' + describe "when the scrollView is scrolled to the right", -> it "adds a drop shadow to the gutter", -> editor.attachToDom() @@ -1855,16 +1965,18 @@ describe "Editor", -> editor.attachToDom() describe "when a fold-selection event is triggered", -> - it "folds the lines covered by the selection into a single line with a fold class", -> + it "folds the lines covered by the selection into a single line with a fold class and marker", -> editor.getSelection().setBufferRange(new Range([4, 29], [7, 4])) editor.trigger 'editor:fold-selection' expect(editor.renderedLines.find('.line:eq(4)')).toHaveClass('fold') + expect(editor.renderedLines.find('.line:eq(4) > .fold-marker')).toExist() expect(editor.renderedLines.find('.line:eq(5)').text()).toBe '8' expect(editor.getSelection().isEmpty()).toBeTruthy() expect(editor.getCursorScreenPosition()).toEqual [5, 0] + describe "when a fold placeholder line is clicked", -> it "removes the associated fold and places the cursor at its beginning", -> editor.setCursorBufferPosition([3,0]) @@ -1873,6 +1985,7 @@ describe "Editor", -> editor.find('.fold.line').mousedown() expect(editor.find('.fold')).not.toExist() + expect(editor.find('.fold-marker')).not.toExist() expect(editor.renderedLines.find('.line:eq(4)').text()).toMatch /4-+/ expect(editor.renderedLines.find('.line:eq(5)').text()).toMatch /5/ @@ -2212,3 +2325,244 @@ describe "Editor", -> edited = editor.replaceSelectedText(replacer) expect(replaced).toBe true expect(edited).toBe false + + describe "when editor:copy-path is triggered", -> + it "copies the absolute path to the editor's file to the pasteboard", -> + editor.trigger 'editor:copy-path' + expect(pasteboard.read()[0]).toBe editor.getPath() + + describe "when editor:move-line-up is triggered", -> + describe "when there is no selection", -> + it "moves the line where the cursor is up", -> + editor.setCursorBufferPosition([1,0]) + editor.trigger 'editor:move-line-up' + expect(buffer.lineForRow(0)).toBe ' var sort = function(items) {' + expect(buffer.lineForRow(1)).toBe 'var quicksort = function () {' + + it "moves the cursor to the new row and the same column", -> + editor.setCursorBufferPosition([1,2]) + editor.trigger 'editor:move-line-up' + expect(editor.getCursorBufferPosition()).toEqual [0,2] + + describe "where there is a selection", -> + describe "when the selection falls inside the line", -> + it "maintains the selection", -> + editor.setSelectedBufferRange([[1, 2], [1, 5]]) + expect(editor.getSelectedText()).toBe 'var' + editor.trigger 'editor:move-line-up' + expect(editor.getSelectedBufferRange()).toEqual [[0, 2], [0, 5]] + expect(editor.getSelectedText()).toBe 'var' + + describe "where there are multiple lines selected", -> + it "moves the selected lines up", -> + editor.setSelectedBufferRange([[2, 0], [3, Infinity]]) + editor.trigger 'editor:move-line-up' + expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {' + expect(buffer.lineForRow(1)).toBe ' if (items.length <= 1) return items;' + expect(buffer.lineForRow(2)).toBe ' var pivot = items.shift(), current, left = [], right = [];' + expect(buffer.lineForRow(3)).toBe ' var sort = function(items) {' + + it "maintains the selection", -> + editor.setSelectedBufferRange([[2, 0], [3, 62]]) + editor.trigger 'editor:move-line-up' + expect(editor.getSelectedBufferRange()).toEqual [[1, 0], [2, 62]] + + describe "when the last line is selected", -> + it "moves the selected line up", -> + editor.setSelectedBufferRange([[12, 0], [12, Infinity]]) + editor.trigger 'editor:move-line-up' + expect(buffer.lineForRow(11)).toBe '};' + expect(buffer.lineForRow(12)).toBe ' return sort(Array.apply(this, arguments));' + + describe "when the last two lines are selected", -> + it "moves the selected lines up", -> + editor.setSelectedBufferRange([[11, 0], [12, Infinity]]) + editor.trigger 'editor:move-line-up' + expect(buffer.lineForRow(10)).toBe ' return sort(Array.apply(this, arguments));' + expect(buffer.lineForRow(11)).toBe '};' + expect(buffer.lineForRow(12)).toBe '' + + describe "when the cursor is on the first line", -> + it "does not move the line", -> + editor.setCursorBufferPosition([0,0]) + originalText = editor.getText() + editor.trigger 'editor:move-line-up' + expect(editor.getText()).toBe originalText + + describe "when the cursor is on the trailing newline", -> + it "does not move the line", -> + editor.moveCursorToBottom() + editor.insertNewline() + editor.moveCursorToBottom() + originalText = editor.getText() + editor.trigger 'editor:move-line-up' + expect(editor.getText()).toBe originalText + + describe "when the cursor is on a folded line", -> + it "moves all lines in the fold up and preserves the fold", -> + editor.setCursorBufferPosition([4, 0]) + editor.foldCurrentRow() + editor.trigger 'editor:move-line-up' + expect(buffer.lineForRow(3)).toBe ' while(items.length > 0) {' + expect(buffer.lineForRow(7)).toBe ' var pivot = items.shift(), current, left = [], right = [];' + expect(editor.getSelectedBufferRange()).toEqual [[3, 0], [3, 0]] + expect(editor.isFoldedAtScreenRow(3)).toBeTruthy() + + describe "when the selection contains a folded and unfolded line", -> + it "moves the selected lines up and preserves the fold", -> + editor.setCursorBufferPosition([4, 0]) + editor.foldCurrentRow() + editor.setCursorBufferPosition([3, 4]) + editor.selectDown() + expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() + editor.trigger 'editor:move-line-up' + expect(buffer.lineForRow(2)).toBe ' var pivot = items.shift(), current, left = [], right = [];' + expect(buffer.lineForRow(3)).toBe ' while(items.length > 0) {' + expect(editor.getSelectedBufferRange()).toEqual [[2, 4], [3, 0]] + expect(editor.isFoldedAtScreenRow(3)).toBeTruthy() + + describe "when an entire line is selected including the newline", -> + it "moves the selected line up", -> + editor.setCursorBufferPosition([1]) + editor.selectToEndOfLine() + editor.selectRight() + editor.trigger 'editor:move-line-up' + expect(buffer.lineForRow(0)).toBe ' var sort = function(items) {' + expect(buffer.lineForRow(1)).toBe 'var quicksort = function () {' + + describe "when editor:move-line-down is triggered", -> + describe "when there is no selection", -> + it "moves the line where the cursor is down", -> + editor.setCursorBufferPosition([0, 0]) + editor.trigger 'editor:move-line-down' + expect(buffer.lineForRow(0)).toBe ' var sort = function(items) {' + expect(buffer.lineForRow(1)).toBe 'var quicksort = function () {' + + it "moves the cursor to the new row and the same column", -> + editor.setCursorBufferPosition([0, 2]) + editor.trigger 'editor:move-line-down' + expect(editor.getCursorBufferPosition()).toEqual [1, 2] + + describe "when the cursor is on the last line", -> + it "does not move the line", -> + editor.moveCursorToBottom() + editor.trigger 'editor:move-line-down' + expect(buffer.lineForRow(12)).toBe '};' + expect(editor.getSelectedBufferRange()).toEqual [[12, 2], [12, 2]] + + describe "when the cursor is on the second to last line", -> + it "moves the line down", -> + editor.setCursorBufferPosition([11, 0]) + editor.trigger 'editor:move-line-down' + expect(buffer.lineForRow(11)).toBe '};' + expect(buffer.lineForRow(12)).toBe ' return sort(Array.apply(this, arguments));' + expect(buffer.lineForRow(13)).toBeUndefined() + + describe "when the cursor is on the second to last line and the last line is empty", -> + it "does not move the line", -> + editor.moveCursorToBottom() + editor.insertNewline() + editor.setCursorBufferPosition([12, 2]) + editor.trigger 'editor:move-line-down' + expect(buffer.lineForRow(12)).toBe '};' + expect(buffer.lineForRow(13)).toBe '' + expect(editor.getSelectedBufferRange()).toEqual [[12, 2], [12, 2]] + + describe "where there is a selection", -> + describe "when the selection falls inside the line", -> + it "maintains the selection", -> + editor.setSelectedBufferRange([[1, 2], [1, 5]]) + expect(editor.getSelectedText()).toBe 'var' + editor.trigger 'editor:move-line-down' + expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [2, 5]] + expect(editor.getSelectedText()).toBe 'var' + + describe "where there are multiple lines selected", -> + it "moves the selected lines down", -> + editor.setSelectedBufferRange([[2, 0], [3, Infinity]]) + editor.trigger 'editor:move-line-down' + expect(buffer.lineForRow(2)).toBe ' while(items.length > 0) {' + expect(buffer.lineForRow(3)).toBe ' if (items.length <= 1) return items;' + expect(buffer.lineForRow(4)).toBe ' var pivot = items.shift(), current, left = [], right = [];' + expect(buffer.lineForRow(5)).toBe ' current = items.shift();' + + it "maintains the selection", -> + editor.setSelectedBufferRange([[2, 0], [3, 62]]) + editor.trigger 'editor:move-line-down' + expect(editor.getSelectedBufferRange()).toEqual [[3, 0], [4, 62]] + + describe "when the cursor is on a folded line", -> + it "moves all lines in the fold down and preserves the fold", -> + editor.setCursorBufferPosition([4, 0]) + editor.foldCurrentRow() + editor.trigger 'editor:move-line-down' + expect(buffer.lineForRow(4)).toBe ' return sort(left).concat(pivot).concat(sort(right));' + expect(buffer.lineForRow(5)).toBe ' while(items.length > 0) {' + expect(editor.getSelectedBufferRange()).toEqual [[5, 0], [5, 0]] + expect(editor.isFoldedAtScreenRow(5)).toBeTruthy() + + describe "when the selection contains a folded and unfolded line", -> + it "moves the selected lines down and preserves the fold", -> + editor.setCursorBufferPosition([4, 0]) + editor.foldCurrentRow() + editor.setCursorBufferPosition([3, 4]) + editor.selectDown() + expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() + editor.trigger 'editor:move-line-down' + expect(buffer.lineForRow(3)).toBe ' return sort(left).concat(pivot).concat(sort(right));' + expect(buffer.lineForRow(4)).toBe ' var pivot = items.shift(), current, left = [], right = [];' + expect(buffer.lineForRow(5)).toBe ' while(items.length > 0) {' + expect(editor.getSelectedBufferRange()).toEqual [[4, 4], [5, 0]] + expect(editor.isFoldedAtScreenRow(5)).toBeTruthy() + + describe "when an entire line is selected including the newline", -> + it "moves the selected line down", -> + editor.setCursorBufferPosition([1]) + editor.selectToEndOfLine() + editor.selectRight() + editor.trigger 'editor:move-line-down' + expect(buffer.lineForRow(1)).toBe ' if (items.length <= 1) return items;' + expect(buffer.lineForRow(2)).toBe ' var sort = function(items) {' + + describe "when editor:duplicate-line is triggered", -> + describe "where there is no selection", -> + describe "when the cursor isn't on a folded line", -> + it "duplicates the current line below and moves the cursor down one row", -> + editor.setCursorBufferPosition([0, 5]) + editor.trigger 'editor:duplicate-line' + expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {' + expect(buffer.lineForRow(1)).toBe 'var quicksort = function () {' + expect(editor.getCursorBufferPosition()).toEqual [1, 5] + + describe "when the cursor is on a folded line", -> + it "duplicates the entire fold before and moves the cursor to the new fold", -> + editor.setCursorBufferPosition([4]) + editor.foldCurrentRow() + editor.trigger 'editor:duplicate-line' + expect(editor.getCursorScreenPosition()).toEqual [5] + expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() + expect(editor.isFoldedAtScreenRow(5)).toBeTruthy() + expect(buffer.lineForRow(8)).toBe ' while(items.length > 0) {' + expect(buffer.lineForRow(9)).toBe ' current = items.shift();' + expect(buffer.lineForRow(10)).toBe ' current < pivot ? left.push(current) : right.push(current);' + expect(buffer.lineForRow(11)).toBe ' }' + + describe "when the cursor is on the last line and it doesn't have a trailing newline", -> + it "inserts a newline and the duplicated line", -> + editor.moveCursorToBottom() + editor.trigger 'editor:duplicate-line' + expect(buffer.lineForRow(12)).toBe '};' + expect(buffer.lineForRow(13)).toBe '};' + expect(buffer.lineForRow(14)).toBeUndefined() + expect(editor.getCursorBufferPosition()).toEqual [13, 2] + + describe "when the cursor in on the last line and it is only a newline", -> + it "duplicates the current line below and moves the cursor down one row", -> + editor.moveCursorToBottom() + editor.insertNewline() + editor.moveCursorToBottom() + editor.trigger 'editor:duplicate-line' + expect(buffer.lineForRow(13)).toBe '' + expect(buffer.lineForRow(14)).toBe '' + expect(buffer.lineForRow(15)).toBeUndefined() + expect(editor.getCursorBufferPosition()).toEqual [14, 0] diff --git a/spec/app/language-mode-spec.coffee b/spec/app/language-mode-spec.coffee index 9e1cd8b71..775baefa1 100644 --- a/spec/app/language-mode-spec.coffee +++ b/spec/app/language-mode-spec.coffee @@ -308,16 +308,16 @@ describe "LanguageMode", -> expect(buffer.lineForRow(3)).toBe " font-weight: bold !important;" it "uncomments lines with leading whitespace", -> - buffer.replaceLines(2, 2, " /*width: 110%;*/") + buffer.change([[2, 0], [2, Infinity]], " /*width: 110%;*/") languageMode.toggleLineCommentsForBufferRows(2, 2) expect(buffer.lineForRow(2)).toBe " width: 110%;" it "uncomments lines with trailing whitespace", -> - buffer.replaceLines(2, 2, "/*width: 110%;*/ ") + buffer.change([[2, 0], [2, Infinity]], "/*width: 110%;*/ ") languageMode.toggleLineCommentsForBufferRows(2, 2) expect(buffer.lineForRow(2)).toBe "width: 110%; " it "uncomments lines with leading and trailing whitespace", -> - buffer.replaceLines(2, 2, " /*width: 110%;*/ ") + buffer.change([[2, 0], [2, Infinity]], " /*width: 110%;*/ ") languageMode.toggleLineCommentsForBufferRows(2, 2) expect(buffer.lineForRow(2)).toBe " width: 110%; " diff --git a/spec/app/point-spec.coffee b/spec/app/point-spec.coffee index 322bac5f2..d4d78715c 100644 --- a/spec/app/point-spec.coffee +++ b/spec/app/point-spec.coffee @@ -25,3 +25,9 @@ describe "Point", -> expect(new Point(5, 0).compare(new Point(6, 1))).toBe -1 expect(new Point(5, 5).compare(new Point(4, 1))).toBe 1 expect(new Point(5, 5).compare(new Point(5, 3))).toBe 1 + + describe ".translate(other)", -> + it "returns a translated point", -> + expect(new Point(1,2).translate([2,4])).toEqual [3,6] + expect(new Point(1,2).translate([-1])).toEqual [0,2] + expect(new Point(1,2).translate([0,-2])).toEqual [1,0] diff --git a/spec/app/range-spec.coffee b/spec/app/range-spec.coffee index 780962a5f..c73111ace 100644 --- a/spec/app/range-spec.coffee +++ b/spec/app/range-spec.coffee @@ -32,3 +32,8 @@ describe "Range", -> expect(new Range([2, 1], [3, 10]).union(new Range([1, 1], [2, 10]))).toEqual [[1, 1], [3, 10]] expect(new Range([2, 1], [3, 10]).union(new Range([2, 5], [3, 1]))).toEqual [[2, 1], [3, 10]] expect(new Range([2, 5], [3, 1]).union(new Range([2, 1], [3, 10]))).toEqual [[2, 1], [3, 10]] + + describe ".translate(startPoint, endPoint)", -> + it "returns a range translates by the specified start and end points", -> + expect(new Range([1, 1], [2, 10]).translate([1])).toEqual [[2, 1], [3, 10]] + expect(new Range([1, 1], [2, 10]).translate([1,2], [3,4])).toEqual [[2, 3], [5, 14]] diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee index 20b5bca36..b046bef30 100644 --- a/spec/app/root-view-spec.coffee +++ b/spec/app/root-view-spec.coffee @@ -567,9 +567,9 @@ describe "RootView", -> editor = null beforeEach -> editor = rootView.getActiveEditor() + editor.attachToDom() it "increases/decreases font size when increase/decrease-font-size events are triggered", -> - editor = rootView.getActiveEditor() fontSizeBefore = editor.getFontSize() rootView.trigger 'window:increase-font-size' expect(editor.getFontSize()).toBe fontSizeBefore + 1 diff --git a/spec/app/select-list-spec.coffee b/spec/app/select-list-spec.coffee index e4e4d32cc..a39c4a128 100644 --- a/spec/app/select-list-spec.coffee +++ b/spec/app/select-list-spec.coffee @@ -164,3 +164,16 @@ describe "SelectList", -> miniEditor.trigger 'focusout' expect(selectList.cancelled).toHaveBeenCalled() expect(selectList.detach).toHaveBeenCalled() + + describe "the core:move-to-top event", -> + it "scrolls to the top and selects the first element", -> + selectList.trigger 'core:move-down' + expect(list.find('li:eq(1)')).toHaveClass 'selected' + selectList.trigger 'core:move-to-top' + expect(list.find('li:first')).toHaveClass 'selected' + + describe "the core:move-to-bottom event", -> + it "scrolls to the bottom and selects the last element", -> + expect(list.find('li:first')).toHaveClass 'selected' + selectList.trigger 'core:move-to-bottom' + expect(list.find('li:last')).toHaveClass 'selected' diff --git a/spec/app/syntax-spec.coffee b/spec/app/syntax-spec.coffee index d15eb5c74..9660fcecb 100644 --- a/spec/app/syntax-spec.coffee +++ b/spec/app/syntax-spec.coffee @@ -23,8 +23,7 @@ describe "the `syntax` global", -> expect(syntax.grammarForFilePath("/tmp/.git/config").name).toBe "Git Config" it "uses plain text if no grammar can be found", -> - filePath = require.resolve("this-is-not-a-real-file") - expect(syntax.grammarForFilePath(filePath).name).toBe "Plain Text" + expect(syntax.grammarForFilePath("this-is-not-a-real-file").name).toBe "Plain Text" describe ".getProperty(scopeDescriptor)", -> it "returns the property with the most specific scope selector", -> diff --git a/spec/app/text-mate-theme-spec.coffee b/spec/app/text-mate-theme-spec.coffee index 5191ced55..b2f6e54fe 100644 --- a/spec/app/text-mate-theme-spec.coffee +++ b/spec/app/text-mate-theme-spec.coffee @@ -27,12 +27,12 @@ describe "TextMateTheme", -> 'color': '#F8F8F8' expect(rulesets[1]).toEqual - selector: '.editor.focused .cursor' + selector: '.editor.is-focused .cursor' properties: 'border-color': '#A7A7A7' expect(rulesets[2]).toEqual - selector: '.editor.focused .selection .region' + selector: '.editor.is-focused .selection .region' properties: 'background-color': "rgba(221, 240, 255, 0.2)" @@ -42,7 +42,7 @@ describe "TextMateTheme", -> selector: ".invalid.deprecated" properties: 'color': "#D2A8A1" - # 'font-style': 'italic' + 'font-style': 'italic' 'text-decoration': 'underline' expect(rulesets[13]).toEqual diff --git a/spec/app/window-spec.coffee b/spec/app/window-spec.coffee index 2471d6cc3..1c166db8f 100644 --- a/spec/app/window-spec.coffee +++ b/spec/app/window-spec.coffee @@ -5,6 +5,7 @@ describe "Window", -> [rootView] = [] beforeEach -> + window.setUpEventHandlers() window.attachRootView(require.resolve('fixtures')) rootView = window.rootView @@ -13,6 +14,25 @@ describe "Window", -> atom.setRootViewStateForPath(rootView.project.getPath(), null) $(window).off 'beforeunload' + describe "when the window is loaded", -> + it "doesn't have .is-blurred on the body tag", -> + expect($("body")).not.toHaveClass("is-blurred") + + describe "when the window is blurred", -> + beforeEach -> + $(window).trigger 'blur' + + afterEach -> + $('body').removeClass('is-blurred') + + it "adds the .is-blurred class on the body", -> + expect($("body")).toHaveClass("is-blurred") + + describe "when the window is focused again", -> + it "removes the .is-blurred class from the body", -> + $(window).trigger 'focus' + expect($("body")).not.toHaveClass("is-blurred") + describe ".close()", -> it "is triggered by the 'core:close' event", -> spyOn window, 'close' diff --git a/spec/fixtures/jquery-task-handler.coffee b/spec/fixtures/jquery-task-handler.coffee new file mode 100644 index 000000000..dbd91b107 --- /dev/null +++ b/spec/fixtures/jquery-task-handler.coffee @@ -0,0 +1,4 @@ +module.exports = + load: -> + $ = require 'jquery' + callTaskMethod('loaded', $?) diff --git a/spec/fixtures/packages/package-with-module/keymaps/keymap-3.cjson b/spec/fixtures/packages/package-with-module/keymaps/keymap-3.cjson new file mode 100644 index 000000000..3c95c85a5 --- /dev/null +++ b/spec/fixtures/packages/package-with-module/keymaps/keymap-3.cjson @@ -0,0 +1,2 @@ +".test-3": + "ctrl-z": "test-3" diff --git a/spec/fixtures/packages/textmate-package.tmbundle/Snippets/.hidden-file b/spec/fixtures/packages/textmate-package.tmbundle/Snippets/.hidden-file new file mode 100644 index 000000000..35b867918 --- /dev/null +++ b/spec/fixtures/packages/textmate-package.tmbundle/Snippets/.hidden-file @@ -0,0 +1 @@ +I am hidden so I shouldn't be loaded diff --git a/spec/fixtures/packages/textmate-package.tmbundle/Snippets/invalid.plist b/spec/fixtures/packages/textmate-package.tmbundle/Snippets/invalid.plist new file mode 100644 index 000000000..d6970c8b7 --- /dev/null +++ b/spec/fixtures/packages/textmate-package.tmbundle/Snippets/invalid.plist @@ -0,0 +1 @@ +I am not a valid plist but that shouldn't cause a crisis diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 01736d4af..1a4657488 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -35,6 +35,7 @@ beforeEach -> spyOn(config, 'save') config.set "editor.fontSize", 16 config.set "editor.autoIndent", false + config.set "core.disabledPackages", ["package-that-throws-an-exception"] # make editor display updates synchronous spyOn(Editor.prototype, 'requestDisplayUpdate').andCallFake -> @updateDisplay() @@ -84,7 +85,7 @@ jasmine.unspy = (object, methodName) -> throw new Error("Not a spy") unless object[methodName].originalValue? object[methodName] = object[methodName].originalValue -jasmine.getEnv().defaultTimeoutInterval = 500 +jasmine.getEnv().defaultTimeoutInterval = 1000 window.keyIdentifierForKey = (key) -> if key.length > 1 # named key diff --git a/spec/stdlib/cson-spec.coffee b/spec/stdlib/cson-spec.coffee new file mode 100644 index 000000000..361eaf330 --- /dev/null +++ b/spec/stdlib/cson-spec.coffee @@ -0,0 +1,86 @@ +CSON = require 'cson' + +describe "CSON", -> + describe "@stringify(object)", -> + describe "when the object is undefined", -> + it "throws an exception", -> + expect(-> CSON.stringify()).toThrow() + + describe "when the object is a function", -> + it "throws an exception", -> + expect(-> CSON.stringify(-> 'function')).toThrow() + + describe "when the object contains a function", -> + it "throws an exception", -> + expect(-> CSON.stringify(a: -> 'function')).toThrow() + + describe "when formatting an undefined key", -> + it "does not include the key in the formatted CSON", -> + expect(CSON.stringify(b: 1, c: undefined)).toBe "'b': 1" + + describe "when formatting a string", -> + it "returns formatted CSON", -> + expect(CSON.stringify(a: 'b')).toBe "'a': 'b'" + + it "escapes single quotes", -> + expect(CSON.stringify(a: "'b'")).toBe "'a': '\\\'b\\\''" + + it "doesn't escape double quotes", -> + expect(CSON.stringify(a: '"b"')).toBe "'a': '\"b\"'" + + it "escapes newlines", -> + expect(CSON.stringify("a\nb")).toBe "'a\\nb'" + + describe "when formatting a boolean", -> + it "returns formatted CSON", -> + expect(CSON.stringify(true)).toBe 'true' + expect(CSON.stringify(false)).toBe 'false' + expect(CSON.stringify(a: true)).toBe "'a': true" + expect(CSON.stringify(a: false)).toBe "'a': false" + + describe "when formatting a number", -> + it "returns formatted CSON", -> + expect(CSON.stringify(54321.012345)).toBe '54321.012345' + expect(CSON.stringify(a: 14)).toBe "'a': 14" + expect(CSON.stringify(a: 1.23)).toBe "'a': 1.23" + + describe "when formatting null", -> + it "returns formatted CSON", -> + expect(CSON.stringify(null)).toBe 'null' + expect(CSON.stringify(a: null)).toBe "'a': null" + + describe "when formatting an array", -> + describe "when the array is empty", -> + it "puts the array on a single line", -> + expect(CSON.stringify([])).toBe "[]" + + it "returns formatted CSON", -> + expect(CSON.stringify(a: ['b'])).toBe "'a': [\n 'b'\n]" + expect(CSON.stringify(a: ['b', 4])).toBe "'a': [\n 'b'\n 4\n]" + + describe "when the array has an undefined value", -> + it "formats the undefined value as null", -> + expect(CSON.stringify(['a', undefined, 'b'])).toBe "[\n 'a'\n null\n 'b'\n]" + + describe "when formatting an object", -> + describe "when the object is empty", -> + it "returns the empty string", -> + expect(CSON.stringify({})).toBe "" + + it "returns formatted CSON", -> + expect(CSON.stringify(a: {b: 'c'})).toBe "'a':\n 'b': 'c'" + + describe "when converting back to an object", -> + it "produces the original object", -> + object = + showInvisibles: true + fontSize: 20 + core: + themes: ['a', 'b'] + stripTrailingWhitespace: + singleTrailingNewline: true + + cson = CSON.stringify(object) + {CoffeeScript} = require 'coffee-script' + evaledObject = CoffeeScript.eval(cson, bare: true) + expect(evaledObject).toEqual object diff --git a/spec/stdlib/fs-spec.coffee b/spec/stdlib/fs-spec.coffee index ffcdaf31b..b0a6fae5f 100644 --- a/spec/stdlib/fs-spec.coffee +++ b/spec/stdlib/fs-spec.coffee @@ -128,3 +128,8 @@ describe "fs", -> describe ".md5ForPath(path)", -> it "returns the MD5 hash of the file at the given path", -> expect(fs.md5ForPath(require.resolve('fixtures/sample.js'))).toBe 'dd38087d0d7e3e4802a6d3f9b9745f2b' + + describe ".list(path, extensions)", -> + it "returns the paths with the specified extensions", -> + path = require.resolve('fixtures/css.css') + expect(fs.list(require.resolve('fixtures'), ['.css'])).toEqual [path] diff --git a/spec/stdlib/onig-reg-exp-spec.coffee b/spec/stdlib/onig-reg-exp-spec.coffee index 0b65ab120..14955c317 100644 --- a/spec/stdlib/onig-reg-exp-spec.coffee +++ b/spec/stdlib/onig-reg-exp-spec.coffee @@ -1,3 +1,5 @@ +OnigRegExp = require 'onig-reg-exp' + describe "OnigRegExp", -> describe ".search(string, index)", -> it "returns an array of the match and all capture groups", -> diff --git a/spec/stdlib/task-shell-spec.coffee b/spec/stdlib/task-shell-spec.coffee new file mode 100644 index 000000000..d1ccb8fc0 --- /dev/null +++ b/spec/stdlib/task-shell-spec.coffee @@ -0,0 +1,23 @@ +Task = require 'task' + +describe "Task shell", -> + describe "populating the window with fake properties", -> + describe "when jQuery is loaded in a web worker", -> + it "doesn't log to the console", -> + spyOn(console, 'log') + spyOn(console, 'error') + spyOn(console, 'warn') + class JQueryTask extends Task + constructor: -> super('fixtures/jquery-task-handler.coffee') + started: -> @callWorkerMethod('load') + loaded: (@jqueryLoaded) -> + + task = new JQueryTask() + task.start() + waitsFor "web worker to start and jquery to be required", 5000, -> + task.jqueryLoaded + runs -> + expect(task.jqueryLoaded).toBeTruthy() + expect(console.log).not.toHaveBeenCalled() + expect(console.error).not.toHaveBeenCalled() + expect(console.warn).not.toHaveBeenCalled() diff --git a/src/app/atom-package.coffee b/src/app/atom-package.coffee index 8fc6b4908..15a4e465a 100644 --- a/src/app/atom-package.coffee +++ b/src/app/atom-package.coffee @@ -16,7 +16,7 @@ class AtomPackage extends Package @loadMetadata() @loadKeymaps() @loadStylesheets() if @autoloadStylesheets - rootView.activatePackage(@name, this) unless @isDirectory + rootView?.activatePackage(@name, this) unless @isDirectory catch e console.warn "Failed to load package named '#{@name}'", e.stack this @@ -26,15 +26,12 @@ class AtomPackage extends Package @metadata = fs.readObject(metadataPath) loadKeymaps: -> - for keymapPath in @getKeymapPaths() - keymap.load(keymapPath) - - getKeymapPaths: -> if keymaps = @metadata?.keymaps - keymaps.map (relativePath) => + keymaps = keymaps.map (relativePath) => fs.resolve(@keymapsDirPath, relativePath, ['cson', 'json', '']) + keymap.load(keymapPath) for keymapPath in keymaps else - fs.list(@keymapsDirPath) + keymap.loadDirectory(@keymapsDirPath) loadStylesheets: -> for stylesheetPath in @getStylesheetPaths() diff --git a/src/app/atom-theme.coffee b/src/app/atom-theme.coffee index f955402b5..b52ed3111 100644 --- a/src/app/atom-theme.coffee +++ b/src/app/atom-theme.coffee @@ -3,9 +3,16 @@ Theme = require 'theme' module.exports = class AtomTheme extends Theme + + loadStylesheet: (stylesheetPath)-> + @stylesheets[stylesheetPath] = fs.read(stylesheetPath) + load: -> - json = fs.read(fs.join(@path, "package.json")) - for stylesheetName in JSON.parse(json).stylesheets - stylesheetPath = fs.join(@path, stylesheetName) - @stylesheets[stylesheetPath] = fs.read(stylesheetPath) + if /\.css$/.test(@path) + @loadStylesheet @path + else + json = fs.read(fs.join(@path, "package.json")) + for stylesheetName in JSON.parse(json).stylesheets + stylesheetPath = fs.join(@path, stylesheetName) + @loadStylesheet stylesheetPath super diff --git a/src/app/atom.coffee b/src/app/atom.coffee index f23ed3dcb..b080e03bb 100644 --- a/src/app/atom.coffee +++ b/src/app/atom.coffee @@ -3,6 +3,7 @@ _ = require 'underscore' Package = require 'package' TextMatePackage = require 'text-mate-package' Theme = require 'theme' +LoadTextMatePackagesTask = require 'load-text-mate-packages-task' messageIdCounter = 1 originalSendMessageToBrowserProcess = atom.sendMessageToBrowserProcess @@ -13,10 +14,20 @@ _.extend atom, pendingBrowserProcessCallbacks: {} loadPackages: -> - pack.load() for pack in @getPackages() + {packages, asyncTextMatePackages} = _.groupBy @getPackages(), (pack) -> + if pack instanceof TextMatePackage and pack.name isnt 'text.tmbundle' + 'asyncTextMatePackages' + else + 'packages' + + pack.load() for pack in packages + if asyncTextMatePackages.length + new LoadTextMatePackagesTask(asyncTextMatePackages).start() getPackages: -> - @getPackageNames().map((name) -> Package.build(name)).filter (pack) -> pack? + @packages ?= @getPackageNames().map((name) -> Package.build(name)) + .filter((pack) -> pack?) + new Array(@packages...) loadTextMatePackages: -> pack.load() for pack in @getTextMatePackages() @@ -39,15 +50,21 @@ _.extend atom, .filter (name) -> not _.contains(disabledPackages, name) loadThemes: -> - themeNames = config.get("core.themes") ? ['Atom - Dark', 'IR_Black'] + themeNames = config.get("core.themes") ? ['atom-dark-ui', 'atom-dark-syntax'] themeNames = [themeNames] unless _.isArray(themeNames) @loadTheme(themeName) for themeName in themeNames + @loadUserStylesheet() loadTheme: (name) -> @loadedThemes.push Theme.load(name) + loadUserStylesheet: -> + userStylesheetPath = fs.join(config.configDirPath, 'user.css') + if fs.isFile(userStylesheetPath) + applyStylesheet(userStylesheetPath, fs.read(userStylesheetPath), 'userTheme') + getAtomThemeStylesheets: -> - themeNames = config.get("core.themes") ? ['Atom - Dark', 'IR_Black'] + themeNames = config.get("core.themes") ? ['atom-dark-ui', 'atom-dark-syntax'] themeNames = [themeNames] unless _.isArray(themeNames) open: (args...) -> @@ -94,6 +111,9 @@ _.extend atom, endTracing: -> @sendMessageToBrowserProcess('endTracing') + toggleFullScreen: -> + @sendMessageToBrowserProcess('toggleFullScreen') + getRootViewStateForPath: (path) -> if json = localStorage[path] JSON.parse(json) diff --git a/src/app/buffer-change-operation.coffee b/src/app/buffer-change-operation.coffee index 6fd0f2654..06b241c75 100644 --- a/src/app/buffer-change-operation.coffee +++ b/src/app/buffer-change-operation.coffee @@ -1,4 +1,5 @@ Range = require 'range' +_ = require 'underscore' module.exports = class BufferChangeOperation @@ -8,7 +9,8 @@ class BufferChangeOperation newRange: null newText: null - constructor: ({@buffer, @oldRange, @newText}) -> + constructor: ({@buffer, @oldRange, @newText, @options}) -> + @options ?= {} do: -> @oldText = @buffer.getTextInRange(@oldRange) @@ -26,18 +28,39 @@ class BufferChangeOperation oldText: @newText newText: @oldText + splitLines: (text) -> + lines = text.split('\n') + lineEndings = [] + for line, index in lines + if _.endsWith(line, '\r') + lines[index] = line[...-1] + lineEndings[index] = '\r\n' + else + lineEndings[index] = '\n' + {lines, lineEndings} + changeBuffer: ({ oldRange, newRange, newText, oldText }) -> { prefix, suffix } = @buffer.prefixAndSuffixForRange(oldRange) - newTextLines = newText.split('\n') - if newTextLines.length == 1 - newTextLines = [prefix + newText + suffix] - else - lastLineIndex = newTextLines.length - 1 - newTextLines[0] = prefix + newTextLines[0] - newTextLines[lastLineIndex] += suffix + {lines, lineEndings} = @splitLines(newText) + lastLineIndex = lines.length - 1 - @buffer.replaceLines(oldRange.start.row, oldRange.end.row, newTextLines) + if lines.length == 1 + lines = [prefix + newText + suffix] + else + lines[0] = prefix + lines[0] + lines[lastLineIndex] += suffix + + startRow = oldRange.start.row + endRow = oldRange.end.row + + normalizeLineEndings = @options.normalizeLineEndings ? true + if normalizeLineEndings and suggestedLineEnding = @buffer.suggestedLineEndingForRow(startRow) + lineEndings[index] = suggestedLineEnding for index in [0..lastLineIndex] + @buffer.lines[startRow..endRow] = lines + @buffer.lineEndings[startRow..endRow] = lineEndings + @buffer.cachedMemoryContents = null + @buffer.conflict = false if @buffer.conflict and !@buffer.isModified() event = { oldRange, newRange, oldText, newText } @buffer.trigger 'changed', event @@ -47,11 +70,11 @@ class BufferChangeOperation calculateNewRange: (oldRange, newText) -> newRange = new Range(oldRange.start.copy(), oldRange.start.copy()) - newTextLines = newText.split('\n') - if newTextLines.length == 1 + {lines} = @splitLines(newText) + if lines.length == 1 newRange.end.column += newText.length else - lastLineIndex = newTextLines.length - 1 + lastLineIndex = lines.length - 1 newRange.end.row += lastLineIndex - newRange.end.column = newTextLines[lastLineIndex].length + newRange.end.column = lines[lastLineIndex].length newRange diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee index 9b205b7e0..62511d2f9 100644 --- a/src/app/buffer.coffee +++ b/src/app/buffer.coffee @@ -19,6 +19,7 @@ class Buffer cachedMemoryContents: null conflict: false lines: null + lineEndings: null file: null anchors: null anchorRanges: null @@ -29,6 +30,7 @@ class Buffer @anchors = [] @anchorRanges = [] @lines = [''] + @lineEndings = [] if path throw "Path '#{path}' does not exist" unless fs.exists(path) @@ -104,26 +106,28 @@ class Buffer null getText: -> - @cachedMemoryContents ?= @lines.join('\n') + @cachedMemoryContents ?= @getTextInRange(@getRange()) setText: (text) -> - @change(@getRange(), text) + @change(@getRange(), text, normalizeLineEndings: false) getRange: -> new Range([0, 0], [@getLastRow(), @getLastLine().length]) getTextInRange: (range) -> - range = Range.fromObject(range) + range = @clipRange(range) if range.start.row == range.end.row - return @lines[range.start.row][range.start.column...range.end.column] + return @lineForRow(range.start.row)[range.start.column...range.end.column] multipleLines = [] - multipleLines.push @lines[range.start.row][range.start.column..] # first line + multipleLines.push @lineForRow(range.start.row)[range.start.column..] # first line + multipleLines.push @lineEndingForRow(range.start.row) for row in [range.start.row + 1...range.end.row] - multipleLines.push @lines[row] # middle lines - multipleLines.push @lines[range.end.row][0...range.end.column] # last line + multipleLines.push @lineForRow(row) # middle lines + multipleLines.push @lineEndingForRow(row) + multipleLines.push @lineForRow(range.end.row)[0...range.end.column] # last line - return multipleLines.join '\n' + return multipleLines.join '' getLines: -> @lines @@ -131,6 +135,12 @@ class Buffer lineForRow: (row) -> @lines[row] + lineEndingForRow: (row) -> + @lineEndings[row] unless row is @getLastRow() + + suggestedLineEndingForRow: (row) -> + @lineEndingForRow(row) ? @lineEndingForRow(row - 1) + lineLengthForRow: (row) -> @lines[row].length @@ -184,7 +194,7 @@ class Buffer startPoint = [start, 0] endPoint = [end + 1, 0] - @change(new Range(startPoint, endPoint), '') + @delete(new Range(startPoint, endPoint)) append: (text) -> @insert(@getEofPosition(), text) @@ -195,30 +205,31 @@ class Buffer delete: (range) -> @change(range, '') - change: (oldRange, newText) -> + change: (oldRange, newText, options) -> oldRange = Range.fromObject(oldRange) - operation = new BufferChangeOperation({buffer: this, oldRange, newText}) + operation = new BufferChangeOperation({buffer: this, oldRange, newText, options}) range = @pushOperation(operation) range clipPosition: (position) -> - { row, column } = Point.fromObject(position) - row = 0 if row < 0 - column = 0 if column < 0 - row = Math.min(@getLastRow(), row) - column = Math.min(@lineLengthForRow(row), column) + position = Point.fromObject(position) + eofPosition = @getEofPosition() + if position.isGreaterThan(eofPosition) + eofPosition + else + row = Math.max(position.row, 0) + column = Math.max(position.column, 0) + column = Math.min(@lineLengthForRow(row), column) + new Point(row, column) - new Point(row, column) + clipRange: (range) -> + range = Range.fromObject(range) + new Range(@clipPosition(range.start), @clipPosition(range.end)) prefixAndSuffixForRange: (range) -> prefix: @lines[range.start.row][0...range.start.column] suffix: @lines[range.end.row][range.end.column..] - replaceLines: (startRow, endRow, newLines) -> - @lines[startRow..endRow] = newLines - @cachedMemoryContents = null - @conflict = false if @conflict and !@isModified() - pushOperation: (operation, editSession) -> if @undoManager @undoManager.pushOperation(operation, editSession) diff --git a/src/app/config.coffee b/src/app/config.coffee index ab1eae901..604853326 100644 --- a/src/app/config.coffee +++ b/src/app/config.coffee @@ -1,14 +1,9 @@ fs = require 'fs' _ = require 'underscore' EventEmitter = require 'event-emitter' -{$$} = require 'space-pen' -jQuery = require 'jquery' -Specificity = require 'specificity' -Theme = require 'theme' configDirPath = fs.absolute("~/.atom") -configJsonPath = fs.join(configDirPath, "config.json") -userInitScriptPath = fs.join(configDirPath, "atom.coffee") +userInitScriptPath = fs.join(configDirPath, "user.coffee") bundledPackagesDirPath = fs.join(resourcePath, "src/packages") bundledThemesDirPath = fs.join(resourcePath, "themes") vendoredPackagesDirPath = fs.join(resourcePath, "vendor/packages") @@ -31,6 +26,8 @@ class Config core: _.clone(require('root-view').configDefaults) editor: _.clone(require('editor').configDefaults) @settings = {} + @configFilePath = fs.resolve(configDirPath, 'config', ['json', 'cson']) + @configFilePath ?= fs.join(configDirPath, 'config.cson') load: -> @loadUserConfig() @@ -39,8 +36,8 @@ class Config atom.loadPackages() loadUserConfig: -> - if fs.exists(configJsonPath) - userConfig = JSON.parse(fs.read(configJsonPath)) + if fs.exists(@configFilePath) + userConfig = fs.readObject(@configFilePath) _.extend(@settings, userConfig) get: (keyPath) -> @@ -81,7 +78,7 @@ class Config @trigger 'updated' save: -> - fs.write(configJsonPath, JSON.stringify(@settings, undefined, 2) + "\n") + fs.writeObject(@configFilePath, @settings) requireUserInitScript: -> try diff --git a/src/app/cursor.coffee b/src/app/cursor.coffee index 4599705f3..8ac44739e 100644 --- a/src/app/cursor.coffee +++ b/src/app/cursor.coffee @@ -9,7 +9,6 @@ class Cursor screenPosition: null bufferPosition: null goalColumn: null - wordRegex: /(\w+)|([^\w\n]+)/g visible: true needsAutoscroll: false @@ -56,9 +55,18 @@ class Cursor isVisible: -> @visible + wordRegExp: -> + nonWordCharacters = config.get("editor.nonWordCharacters") + new RegExp("^[\t ]*$|[^\\s#{_.escapeRegExp(nonWordCharacters)}]+|[#{_.escapeRegExp(nonWordCharacters)}]+", "g") + isLastCursor: -> this == @editSession.getCursor() + isSurroundedByWhitespace: -> + {row, column} = @getBufferPosition() + range = [[row, Math.min(0, column - 1)], [row, Math.max(0, column + 1)]] + /^\s+$/.test @editSession.getTextInBufferRange(range) + autoscrolled: -> @needsAutoscroll = false @@ -147,14 +155,16 @@ class Cursor allowPrevious = options.allowPrevious ? true currentBufferPosition = @getBufferPosition() previousNonBlankRow = @editSession.buffer.previousNonBlankRow(currentBufferPosition.row) - previousLinesRange = [[previousNonBlankRow, 0], currentBufferPosition] + range = [[previousNonBlankRow, 0], currentBufferPosition] - beginningOfWordPosition = currentBufferPosition - @editSession.backwardsScanInRange (options.wordRegex || @wordRegex), previousLinesRange, (match, matchRange, { stop }) => + beginningOfWordPosition = null + @editSession.backwardsScanInRange (options.wordRegex ? @wordRegExp()), range, (match, matchRange, { stop }) => if matchRange.end.isGreaterThanOrEqual(currentBufferPosition) or allowPrevious beginningOfWordPosition = matchRange.start - stop() - beginningOfWordPosition + if not beginningOfWordPosition?.isEqual(currentBufferPosition) + stop() + + beginningOfWordPosition or currentBufferPosition getEndOfCurrentWordBufferPosition: (options = {}) -> allowNext = options.allowNext ? true @@ -162,11 +172,12 @@ class Cursor range = [currentBufferPosition, @editSession.getEofBufferPosition()] endOfWordPosition = null - @editSession.scanInRange (options.wordRegex || @wordRegex), range, (match, matchRange, { stop }) => - endOfWordPosition = matchRange.end - if not allowNext and matchRange.start.isGreaterThan(currentBufferPosition) - endOfWordPosition = currentBufferPosition - stop() + @editSession.scanInRange (options.wordRegex ? @wordRegExp()), range, (match, matchRange, { stop }) => + if matchRange.start.isLessThanOrEqual(currentBufferPosition) or allowNext + endOfWordPosition = matchRange.end + if not endOfWordPosition?.isEqual(currentBufferPosition) + stop() + endOfWordPosition or currentBufferPosition getCurrentWordBufferRange: (options={}) -> diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 8e64e2012..c0df1d6f0 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -18,7 +18,7 @@ class EditSession if fs.exists(state.buffer) session = project.buildEditSessionForPath(state.buffer) else - console.warn "Could not build edit session for path '#{state.buffer}' because that file no longer exists" + 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.setScrollTop(state.scrollTop) session.setScrollLeft(state.scrollLeft) @@ -211,6 +211,10 @@ class EditSession autoIndentSelectedRows: -> @mutateSelectedText (selection) -> selection.autoIndentSelectedRows() + normalizeTabsInBufferRange: (bufferRange) -> + return unless @softTabs + @scanInRange /\t/, bufferRange, (match, range, {replace}) => replace(@getTabText()) + cutToEndOfLine: -> maintainPasteboard = false @mutateSelectedText (selection) -> @@ -251,8 +255,9 @@ class EditSession undo: (editSession) -> editSession?.setSelectedBufferRanges(oldSelectedRanges) if fn - fn() + result = fn() @commit() if isNewTransaction + result commit: -> newSelectedRanges = @getSelectedBufferRanges() @@ -302,6 +307,13 @@ class EditSession fold.destroy() @setCursorBufferPosition([fold.startRow, 0]) + isFoldedAtCursorRow: -> + @isFoldedAtScreenRow(@getCursorScreenRow()) + + isFoldedAtBufferRow: (bufferRow) -> + screenRow = @screenPositionForBufferPosition([bufferRow]).row + @isFoldedAtScreenRow(screenRow) + isFoldedAtScreenRow: (screenRow) -> @lineForScreenRow(screenRow)?.fold? @@ -329,6 +341,99 @@ class EditSession toggleLineCommentsForBufferRows: (start, end) -> @languageMode.toggleLineCommentsForBufferRows(start, end) + moveLineUp: -> + selection = @getSelectedBufferRange() + return if selection.start.row is 0 + lastRow = @buffer.getLastRow() + return if selection.isEmpty() and selection.start.row is lastRow and @buffer.getLastLine() is '' + + @transact => + foldedRows = [] + rows = [selection.start.row..selection.end.row] + if selection.start.row isnt selection.end.row and selection.end.column is 0 + rows.pop() unless @isFoldedAtBufferRow(selection.end.row) + for row in rows + screenRow = @screenPositionForBufferPosition([row]).row + if @isFoldedAtScreenRow(screenRow) + bufferRange = @bufferRangeForScreenRange([[screenRow], [screenRow + 1]]) + startRow = bufferRange.start.row + endRow = bufferRange.end.row - 1 + foldedRows.push(endRow - 1) + else + startRow = row + endRow = row + + endPosition = Point.min([endRow + 1], @buffer.getEofPosition()) + lines = @buffer.getTextInRange([[startRow], endPosition]) + if endPosition.row is lastRow and endPosition.column > 0 and not @buffer.lineEndingForRow(endPosition.row) + lines = "#{lines}\n" + @buffer.deleteRows(startRow, endRow) + @buffer.insert([startRow - 1], lines) + + @foldBufferRow(foldedRow) for foldedRow in foldedRows + + @setSelectedBufferRange(selection.translate([-1]), preserveFolds: true) + + moveLineDown: -> + selection = @getSelectedBufferRange() + lastRow = @buffer.getLastRow() + return if selection.end.row is lastRow + return if selection.end.row is lastRow - 1 and @buffer.getLastLine() is '' + + @transact => + foldedRows = [] + rows = [selection.end.row..selection.start.row] + if selection.start.row isnt selection.end.row and selection.end.column is 0 + rows.shift() unless @isFoldedAtBufferRow(selection.end.row) + for row in rows + screenRow = @screenPositionForBufferPosition([row]).row + if @isFoldedAtScreenRow(screenRow) + bufferRange = @bufferRangeForScreenRange([[screenRow], [screenRow + 1]]) + startRow = bufferRange.start.row + endRow = bufferRange.end.row - 1 + foldedRows.push(endRow + 1) + else + startRow = row + endRow = row + + if endRow + 1 is lastRow + endPosition = [endRow, @buffer.lineLengthForRow(endRow)] + else + endPosition = [endRow + 1] + lines = @buffer.getTextInRange([[startRow], endPosition]) + @buffer.deleteRows(startRow, endRow) + insertPosition = Point.min([startRow + 1], @buffer.getEofPosition()) + if insertPosition.row is @buffer.getLastRow() and insertPosition.column > 0 + lines = "\n#{lines}" + @buffer.insert(insertPosition, lines) + + @foldBufferRow(foldedRow) for foldedRow in foldedRows + + @setSelectedBufferRange(selection.translate([1]), preserveFolds: true) + + duplicateLine: -> + return unless @getSelection().isEmpty() + + @transact => + cursorPosition = @getCursorBufferPosition() + cursorRowFolded = @isFoldedAtCursorRow() + if cursorRowFolded + screenRow = @screenPositionForBufferPosition(cursorPosition).row + bufferRange = @bufferRangeForScreenRange([[screenRow], [screenRow + 1]]) + else + bufferRange = new Range([cursorPosition.row], [cursorPosition.row + 1]) + + insertPosition = new Point(bufferRange.end.row) + if insertPosition.row >= @buffer.getLastRow() + @unfoldCurrentRow() if cursorRowFolded + @buffer.append("\n#{@getTextInBufferRange(bufferRange)}") + @foldCurrentRow() if cursorRowFolded + else + @buffer.insert(insertPosition, @getTextInBufferRange(bufferRange)) + + @setCursorScreenPosition(@getCursorScreenPosition().translate([1])) + @foldCurrentRow() if cursorRowFolded + mutateSelectedText: (fn) -> @transact => fn(selection) for selection in @getSelections() diff --git a/src/app/editor.coffee b/src/app/editor.coffee index f06487f45..a11807bb0 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -19,6 +19,7 @@ class Editor extends View autosave: false autoIndent: true autoIndentOnPaste: false + nonWordCharacters: "./\\()\"'-_:,.;<>~!@#$%^&*|+=[]{}`~?" @content: (params) -> @div class: @classes(params), tabindex: -1, => @@ -182,6 +183,10 @@ class Editor extends View 'editor:close-other-edit-sessions': @destroyInactiveEditSessions 'editor:close-all-edit-sessions': @destroyAllEditSessions 'editor:select-grammar': @selectGrammar + 'editor:copy-path': @copyPathToPasteboard + 'editor:move-line-up': @moveLineUp + 'editor:move-line-down': @moveLineDown + 'editor:duplicate-line': @duplicateLine documentation = {} for name, method of editorBindings @@ -203,6 +208,9 @@ class Editor extends View moveCursorToBeginningOfLine: -> @activeEditSession.moveCursorToBeginningOfLine() moveCursorToFirstCharacterOfLine: -> @activeEditSession.moveCursorToFirstCharacterOfLine() moveCursorToEndOfLine: -> @activeEditSession.moveCursorToEndOfLine() + moveLineUp: -> @activeEditSession.moveLineUp() + moveLineDown: -> @activeEditSession.moveLineDown() + duplicateLine: -> @activeEditSession.duplicateLine() setCursorScreenPosition: (position) -> @activeEditSession.setCursorScreenPosition(position) getCursorScreenPosition: -> @activeEditSession.getCursorScreenPosition() getCursorScreenRow: -> @activeEditSession.getCursorScreenRow() @@ -270,6 +278,8 @@ class Editor extends View destroyFold: (foldId) -> @activeEditSession.destroyFold(foldId) destroyFoldsContainingBufferRow: (bufferRow) -> @activeEditSession.destroyFoldsContainingBufferRow(bufferRow) isFoldedAtScreenRow: (screenRow) -> @activeEditSession.isFoldedAtScreenRow(screenRow) + isFoldedAtBufferRow: (bufferRow) -> @activeEditSession.isFoldedAtBufferRow(bufferRow) + isFoldedAtCursorRow: -> @activeEditSession.isFoldedAtCursorRow() lineForScreenRow: (screenRow) -> @activeEditSession.lineForScreenRow(screenRow) linesForScreenRows: (start, end) -> @activeEditSession.linesForScreenRows(start, end) @@ -310,9 +320,10 @@ class Editor extends View setInvisibles: (@invisibles={}) -> _.defaults @invisibles, - eol: '\u00ac', - space: '\u2022', + eol: '\u00ac' + space: '\u00b7' tab: '\u00bb' + cr: '\u00a4' @resetDisplay() checkoutHead: -> @getBuffer().checkoutHead() @@ -333,6 +344,7 @@ class Editor extends View @observeConfig 'editor.showInvisibles', (showInvisibles) => @setShowInvisibles(showInvisibles) @observeConfig 'editor.invisibles', (invisibles) => @setInvisibles(invisibles) @observeConfig 'editor.fontSize', (fontSize) => @setFontSize(fontSize) + @observeConfig 'editor.fontFamily', (fontFamily) => @setFontFamily(fontFamily) handleEvents: -> @on 'focus', => @@ -342,12 +354,12 @@ class Editor extends View @hiddenInput.on 'focus', => @rootView()?.editorFocused(this) @isFocused = true - @addClass 'focused' + @addClass 'is-focused' @hiddenInput.on 'focusout', => @isFocused = false - @removeClass 'focused' @autosave() if config.get "editor.autosave" + @removeClass 'is-focused' @underlayer.on 'click', (e) => return unless e.target is @underlayer[0] @@ -402,6 +414,11 @@ class Editor extends View @gutter.widthChanged = (newWidth) => @scrollView.css('left', newWidth + 'px') + syntax.on 'grammars-loaded', => + @reloadGrammar() + for session in @editSessions + session.reloadGrammar() unless session is @activeEditSession + @scrollView.on 'scroll', => if @scrollView.scrollLeft() == 0 @gutter.removeClass('drop-shadow') @@ -668,16 +685,38 @@ class Editor extends View autosave: -> @save() if @getPath()? - setFontSize: (@fontSize) -> - if fontSize? - @css('font-size', fontSize + 'px') - return unless @attached - @calculateDimensions() - @updatePaddingOfRenderedLines() - @updateLayerDimensions() - @requestDisplayUpdate() + setFontSize: (fontSize) -> + headTag = $("head") + styleTag = headTag.find("style.font-size") + if styleTag.length == 0 + styleTag = $$ -> @style class: 'font-size' + headTag.append styleTag - getFontSize: -> @fontSize + styleTag.text(".editor {font-size: #{fontSize}px}") + @redraw() + + getFontSize: -> + parseInt(@css("font-size")) + + setFontFamily: (fontFamily) -> + return if fontFamily == undefined + headTag = $("head") + styleTag = headTag.find("style.font-family") + if styleTag.length == 0 + styleTag = $$ -> @style class: 'font-family' + headTag.append styleTag + + styleTag.text(".editor {font-family: #{fontFamily}}") + @redraw() + + getFontFamily: -> @css("font-family") + + redraw: -> + return unless @attached + @calculateDimensions() + @updatePaddingOfRenderedLines() + @updateLayerDimensions() + @requestDisplayUpdate() newSplitEditor: (editSession) -> new Editor { editSession: editSession ? @activeEditSession.copy() } @@ -768,6 +807,10 @@ class Editor extends View @overlayer.append(view) calculateDimensions: -> + if not @isOnDom() + detachedEditorParent = _.last(@parents()) ? this + $(document.body).append(detachedEditorParent) + fragment = $('') line.join('') @@ -1154,3 +1202,7 @@ class Editor extends View @insertText(text, select: true) true + + copyPathToPasteboard: -> + path = @getPath() + pasteboard.write(path) if path? diff --git a/src/app/fold.coffee b/src/app/fold.coffee index 57bea3cfa..50e5d34ba 100644 --- a/src/app/fold.coffee +++ b/src/app/fold.coffee @@ -36,8 +36,8 @@ class Fold @displayBuffer.unregisterFold(@startRow, this) return - @updateStartRow(event) - @updateEndRow(event) + @startRow += @getRowDelta(event, @startRow) + @endRow += @getRowDelta(event, @endRow) if @startRow != oldStartRow @displayBuffer.unregisterFold(oldStartRow, this) @@ -49,26 +49,12 @@ class Fold isContainedByFold: (fold) -> @isContainedByRange(fold.getBufferRange()) - updateStartRow: (event) -> + getRowDelta: (event, row) -> { newRange, oldRange } = event - if oldRange.end.row < @startRow - delta = newRange.end.row - oldRange.end.row - else if newRange.end.row < @startRow - delta = newRange.end.row - @startRow + if oldRange.end.row <= row + newRange.end.row - oldRange.end.row + else if newRange.end.row < row + newRange.end.row - row else - delta = 0 - - @startRow += delta - - updateEndRow: (event) -> - { newRange, oldRange } = event - - if oldRange.end.row <= @endRow - delta = newRange.end.row - oldRange.end.row - else if newRange.end.row <= @endRow - delta = newRange.end.row - @endRow - else - delta = 0 - - @endRow += delta + 0 diff --git a/src/app/git.coffee b/src/app/git.coffee index a08595ffb..db1376b17 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -1,13 +1,13 @@ -$ = require 'jquery' _ = require 'underscore' Subscriber = require 'subscriber' +GitRepository = require 'git-repository' module.exports = class Git - @open: (path) -> + @open: (path, options) -> try - new Git(path) + new Git(path, options) catch e null @@ -23,9 +23,12 @@ class Git working_dir_typechange: 1 << 10 ignore: 1 << 14 - constructor: (path) -> + constructor: (path, options={}) -> @repo = new GitRepository(path) - @subscribe $(window), 'focus', => @refreshIndex() + refreshIndexOnFocus = options.refreshIndexOnFocus ? true + if refreshIndexOnFocus + $ = require 'jquery' + @subscribe $(window), 'focus', => @refreshIndex() getRepo: -> unless @repo? diff --git a/src/app/grammar-view.coffee b/src/app/grammar-view.coffee index bd0b73251..3465c45a7 100644 --- a/src/app/grammar-view.coffee +++ b/src/app/grammar-view.coffee @@ -4,7 +4,7 @@ SelectList = require 'select-list' module.exports = class GrammarView extends SelectList - @viewClass: -> "#{super} grammar-view" + @viewClass: -> "#{super} grammar-view from-top overlay" filterKey: 'name' @@ -12,7 +12,6 @@ class GrammarView extends SelectList @currentGrammar = @editor.getGrammar() @path = @editor.getPath() @autoDetect = name: 'Auto Detect' - requireStylesheet 'grammar-view.css' @command 'editor:select-grammar', => @cancel() false diff --git a/src/app/gutter.coffee b/src/app/gutter.coffee index e1e2f8a82..9f7de8f7a 100644 --- a/src/app/gutter.coffee +++ b/src/app/gutter.coffee @@ -58,16 +58,19 @@ class Gutter extends View @renderLineNumbers(renderFrom, renderTo) if performUpdate renderLineNumbers: (startScreenRow, endScreenRow) -> - rows = @editor().bufferRowsForScreenRows(startScreenRow, endScreenRow) + editor = @editor() + rows = editor.bufferRowsForScreenRows(startScreenRow, endScreenRow) - cursorScreenRow = @editor().getCursorScreenPosition().row + cursorScreenRow = editor.getCursorScreenPosition().row @lineNumbers[0].innerHTML = $$$ -> for row in rows if row == lastScreenRow rowValue = '•' else rowValue = row + 1 - @div {class: 'line-number'}, rowValue + classes = ['line-number'] + classes.push('fold') if editor.isFoldedAtBufferRow(row) + @div rowValue, class: classes.join(' ') lastScreenRow = row @calculateWidth() diff --git a/src/app/keymap.coffee b/src/app/keymap.coffee index 1918a429d..97cf4303b 100644 --- a/src/app/keymap.coffee +++ b/src/app/keymap.coffee @@ -35,7 +35,7 @@ class Keymap @loadDirectory(fs.join(config.configDirPath, 'keymaps')) loadDirectory: (directoryPath) -> - @load(filePath) for filePath in fs.list(directoryPath) + @load(filePath) for filePath in fs.list(directoryPath, ['.cson', '.json']) load: (path) -> @add(fs.readObject(path)) diff --git a/src/app/keymaps/atom.cson b/src/app/keymaps/atom.cson index a4f64a990..2d1d6c01e 100644 --- a/src/app/keymaps/atom.cson +++ b/src/app/keymaps/atom.cson @@ -27,6 +27,8 @@ 'meta-+': 'window:increase-font-size' 'meta--': 'window:decrease-font-size' 'ctrl-w w': 'window:focus-next-pane' + 'ctrl-tab': 'window:focus-next-pane' + 'ctrl-meta-f': 'window:toggle-full-screen' 'alt-meta-i': 'toggle-dev-tools' diff --git a/src/app/keymaps/editor.cson b/src/app/keymaps/editor.cson index 2f6da8a83..fdccfd939 100644 --- a/src/app/keymaps/editor.cson +++ b/src/app/keymaps/editor.cson @@ -37,3 +37,7 @@ 'alt-meta-w': 'editor:close-other-edit-sessions' 'meta-P': 'editor:close-all-edit-sessions' 'meta-L': 'editor:select-grammar' + 'ctrl-C': 'editor:copy-path' + 'ctrl-meta-up': 'editor:move-line-up' + 'ctrl-meta-down': 'editor:move-line-down' + 'meta-D': 'editor:duplicate-line' diff --git a/src/app/language-mode.coffee b/src/app/language-mode.coffee index 57be42c86..bfc6cb108 100644 --- a/src/app/language-mode.coffee +++ b/src/app/language-mode.coffee @@ -1,6 +1,7 @@ Range = require 'range' _ = require 'underscore' require 'underscore-extensions' +OnigRegExp = require 'onig-reg-exp' module.exports = class LanguageMode diff --git a/src/app/line-map.coffee b/src/app/line-map.coffee index eae630887..1a5a74147 100644 --- a/src/app/line-map.coffee +++ b/src/app/line-map.coffee @@ -133,6 +133,7 @@ class LineMap new Range(start, end) bufferRangeForScreenRange: (screenRange) -> + screenRange = Range.fromObject(screenRange) start = @bufferPositionForScreenPosition(screenRange.start) end = @bufferPositionForScreenPosition(screenRange.end) new Range(start, end) @@ -141,4 +142,3 @@ class LineMap for row in [start..end] line = @lineForScreenRow(row).text console.log row, line, line.length - diff --git a/src/app/load-text-mate-packages-handler.coffee b/src/app/load-text-mate-packages-handler.coffee new file mode 100644 index 000000000..c2e675bd1 --- /dev/null +++ b/src/app/load-text-mate-packages-handler.coffee @@ -0,0 +1,5 @@ +TextMatePackage = require 'text-mate-package' + +module.exports = + loadPackage: (name) -> + callTaskMethod('packageLoaded', new TextMatePackage(name).readGrammars()) diff --git a/src/app/load-text-mate-packages-task.coffee b/src/app/load-text-mate-packages-task.coffee new file mode 100644 index 000000000..0bb0ee805 --- /dev/null +++ b/src/app/load-text-mate-packages-task.coffee @@ -0,0 +1,26 @@ +Task = require 'task' + +module.exports = +class LoadTextMatePackagesTask extends Task + + constructor: (@packages) -> + super('load-text-mate-packages-handler') + + started: -> + @loadNextPackage() + + loadNextPackage: -> + unless @packages.length + @terminate() + syntax.trigger 'grammars-loaded' + return + + @package = @packages.shift() + @loadPackage(@package.name) + + loadPackage: (name) -> + @callWorkerMethod('loadPackage', name) + + packageLoaded: (grammars) -> + @package.loadGrammars(grammars) + @loadNextPackage() diff --git a/src/app/point.coffee b/src/app/point.coffee index a216cad0b..e471ad6c6 100644 --- a/src/app/point.coffee +++ b/src/app/point.coffee @@ -11,6 +11,14 @@ class Point new Point(row, column) + @min: (point1, point2) -> + point1 = @fromObject(point1) + point2 = @fromObject(point2) + if point1.isLessThanOrEqual(point2) + point1 + else + point2 + constructor: (@row=0, @column=0) -> copy: -> @@ -26,6 +34,10 @@ class Point new Point(row, column) + translate: (other) -> + other = Point.fromObject(other) + new Point(@row + other.row, @column + other.column) + splitAt: (column) -> if @row == 0 rightColumn = @column - column diff --git a/src/app/range.coffee b/src/app/range.coffee index 3464a3bce..136f52f15 100644 --- a/src/app/range.coffee +++ b/src/app/range.coffee @@ -48,6 +48,9 @@ class Range add: (point) -> new Range(@start.add(point), @end.add(point)) + translate: (startPoint, endPoint=startPoint) -> + new Range(@start.translate(startPoint), @end.translate(endPoint)) + intersectsWith: (otherRange) -> if @start.isLessThanOrEqual(otherRange.start) @end.isGreaterThanOrEqual(otherRange.start) diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee index 3a187d6a7..4063ee863 100644 --- a/src/app/root-view.coffee +++ b/src/app/root-view.coffee @@ -18,7 +18,7 @@ class RootView extends View disabledPackages: [] @content: -> - @div id: 'root-view', tabindex: -1, => + @div id: 'root-view', tabindex: 0, => @div id: 'horizontal', outlet: 'horizontal', => @div id: 'vertical', outlet: 'vertical', => @div id: 'panes', outlet: 'panes' diff --git a/src/app/screen-line.coffee b/src/app/screen-line.coffee index 31d93e879..a590d8e86 100644 --- a/src/app/screen-line.coffee +++ b/src/app/screen-line.coffee @@ -2,7 +2,7 @@ _ = require 'underscore' module.exports = class ScreenLine - constructor: ({tokens, @ruleStack, @bufferRows, @startBufferColumn, @fold, tabLength}) -> + constructor: ({tokens, @lineEnding, @ruleStack, @bufferRows, @startBufferColumn, @fold, tabLength}) -> @tokens = @breakOutAtomicTokens(tokens, tabLength) @bufferRows ?= 1 @startBufferColumn ?= 0 @@ -74,11 +74,13 @@ class ScreenLine bufferRows: 0 startBufferColumn: @startBufferColumn ruleStack: @ruleStack + lineEnding: @lineEnding ) rightFragment = new ScreenLine( tokens: rightTokens startBufferColumn: @startBufferColumn + column ruleStack: @ruleStack + lineEnding: @lineEnding ) [leftFragment, rightFragment] diff --git a/src/app/select-list.coffee b/src/app/select-list.coffee index 5bdb3258d..297c3fa35 100644 --- a/src/app/select-list.coffee +++ b/src/app/select-list.coffee @@ -26,6 +26,12 @@ class SelectList extends View @miniEditor.on 'focusout', => @cancel() unless @cancelling @on 'core:move-up', => @selectPreviousItem() @on 'core:move-down', => @selectNextItem() + @on 'core:move-to-top', => + @selectItem(@list.find('li:first')) + @list.scrollToTop() + @on 'core:move-to-bottom', => + @selectItem(@list.find('li:last')) + @list.scrollToBottom() @on 'core:confirm', => @confirmSelection() @on 'core:cancel', => @cancel() @@ -103,7 +109,7 @@ class SelectList extends View scrollToItem: (item) -> scrollTop = @list.scrollTop() desiredTop = item.position().top + scrollTop - desiredBottom = desiredTop + item.height() + desiredBottom = desiredTop + item.outerHeight() if desiredTop < scrollTop @list.scrollTop(desiredTop) diff --git a/src/app/selection.coffee b/src/app/selection.coffee index f30eea09a..abc702103 100644 --- a/src/app/selection.coffee +++ b/src/app/selection.coffee @@ -96,7 +96,10 @@ class Selection @screenRangeChanged() selectWord: -> - @setBufferRange(@cursor.getCurrentWordBufferRange()) + options = {} + options.wordRegex = /[\t ]*/ if @cursor.isSurroundedByWhitespace() + + @setBufferRange(@cursor.getCurrentWordBufferRange(options)) @wordwise = true @initialScreenRange = @getScreenRange() @@ -185,6 +188,8 @@ class Selection else @editSession.autoDecreaseIndentForRow(newBufferRange.start.row) + newBufferRange + indent: ({ autoIndent }={})-> { row, column } = @cursor.getBufferPosition() diff --git a/src/app/syntax.coffee b/src/app/syntax.coffee index 1ff5e8663..1488ddf67 100644 --- a/src/app/syntax.coffee +++ b/src/app/syntax.coffee @@ -3,6 +3,7 @@ jQuery = require 'jquery' Specificity = require 'specificity' {$$} = require 'space-pen' fs = require 'fs' +EventEmitter = require 'event-emitter' module.exports = class Syntax @@ -104,3 +105,5 @@ class Syntax deepestChild[0] else element[0] + +_.extend(Syntax.prototype, EventEmitter) diff --git a/src/app/text-mate-grammar.coffee b/src/app/text-mate-grammar.coffee index 1bd96d996..64ca52e8f 100644 --- a/src/app/text-mate-grammar.coffee +++ b/src/app/text-mate-grammar.coffee @@ -2,16 +2,18 @@ _ = require 'underscore' fs = require 'fs' plist = require 'plist' Token = require 'token' +OnigRegExp = require 'onig-reg-exp' +OnigScanner = require 'onig-scanner' module.exports = class TextMateGrammar - @loadFromPath: (path) -> - grammar = null + @readFromPath: (path) -> + grammarContent = null plist.parseString fs.read(path), (e, data) -> throw new Error(e) if e - grammar = new TextMateGrammar(data[0]) - throw new Error("Failed to load grammar at path `#{path}`") unless grammar - grammar + grammarContent = data[0] + throw new Error("Failed to load grammar at path `#{path}`") unless grammarContent + grammarContent name: null fileTypes: null @@ -24,6 +26,7 @@ class TextMateGrammar @initialRule = new Rule(this, {@scopeName, patterns}) @repository = {} @firstLineRegex = new OnigRegExp(firstLineMatch) if firstLineMatch + @fileTypes ?= [] for name, data of repository data = {patterns: [data], tempName: name} if data.begin? or data.match? diff --git a/src/app/text-mate-package.coffee b/src/app/text-mate-package.coffee index 23cc9143b..97eea6189 100644 --- a/src/app/text-mate-package.coffee +++ b/src/app/text-mate-package.coffee @@ -22,28 +22,41 @@ class TextMatePackage extends Package super @preferencesPath = fs.join(@path, "Preferences") @syntaxesPath = fs.join(@path, "Syntaxes") + @grammars = [] load: -> try - for grammar in @getGrammars() - syntax.addGrammar(grammar) - - for { selector, properties } in @getScopedProperties() - syntax.addProperties(selector, properties) + @loadGrammars() catch e console.warn "Failed to load package named '#{@name}'", e.stack this - getGrammars: -> - return @grammars if @grammars + getGrammars: -> @grammars + + readGrammars: -> + grammars = [] + for grammarPath in fs.list(@syntaxesPath) + try + grammars.push(TextMateGrammar.readFromPath(grammarPath)) + catch e + console.warn "Failed to load grammar at path '#{grammarPath}'", e.stack + grammars + + addGrammar: (rawGrammar) -> + grammar = new TextMateGrammar(rawGrammar) + @grammars.push(grammar) + syntax.addGrammar(grammar) + + loadGrammars: (rawGrammars) -> + rawGrammars = @readGrammars() unless rawGrammars? + @grammars = [] - if fs.exists(@syntaxesPath) - for grammarPath in fs.list(@syntaxesPath) - try - @grammars.push TextMateGrammar.loadFromPath(grammarPath) - catch e - console.warn "Failed to load grammar at path '#{grammarPath}'", e.stack - @grammars + @addGrammar(rawGrammar) for rawGrammar in rawGrammars + @loadScopedProperties() + + loadScopedProperties: -> + for { selector, properties } in @getScopedProperties() + syntax.addProperties(selector, properties) getScopedProperties: -> scopedProperties = [] diff --git a/src/app/text-mate-theme.coffee b/src/app/text-mate-theme.coffee index ee78cdea7..f2c1d1f92 100644 --- a/src/app/text-mate-theme.coffee +++ b/src/app/text-mate-theme.coffee @@ -44,17 +44,17 @@ class TextMateTheme extends Theme 'color': @translateColor(foreground) @rulesets.push - selector: '.editor.focused .cursor' + selector: '.editor.is-focused .cursor' properties: 'border-color': @translateColor(caret) @rulesets.push - selector: '.editor.focused .selection .region' + selector: '.editor.is-focused .selection .region' properties: 'background-color': @translateColor(selection) @rulesets.push - selector: '.editor.focused .line-number.cursor-line-no-selection, .editor.focused .line.cursor-line' + selector: '.editor.is-focused .line-number.cursor-line-no-selection, .editor.is-focused .line.cursor-line' properties: 'background-color': @translateColor(lineHighlight) @@ -75,8 +75,8 @@ class TextMateTheme extends Theme if fontStyle fontStyles = fontStyle.split(/\s+/) - # properties['font-weight'] = 'bold' if _.contains(fontStyles, 'bold') - # properties['font-style'] = 'italic' if _.contains(fontStyles, 'italic') + properties['font-weight'] = 'bold' if _.contains(fontStyles, 'bold') + properties['font-style'] = 'italic' if _.contains(fontStyles, 'italic') properties['text-decoration'] = 'underline' if _.contains(fontStyles, 'underline') properties['color'] = @translateColor(foreground) if foreground diff --git a/src/app/theme.coffee b/src/app/theme.coffee index 410add427..b16a7ef1c 100644 --- a/src/app/theme.coffee +++ b/src/app/theme.coffee @@ -1,5 +1,4 @@ -fs = require("fs") -_ = require 'underscore' +fs = require 'fs' module.exports = class Theme @@ -12,7 +11,7 @@ class Theme if fs.exists(name) path = name else - path = fs.resolve(config.themeDirPaths..., name, ['', '.tmTheme']) + path = fs.resolve(config.themeDirPaths..., name, ['', '.tmTheme', '.css']) throw new Error("No theme exists named '#{name}'") unless path diff --git a/src/app/tokenized-buffer.coffee b/src/app/tokenized-buffer.coffee index 6646fd54f..9e0930bf0 100644 --- a/src/app/tokenized-buffer.coffee +++ b/src/app/tokenized-buffer.coffee @@ -137,8 +137,9 @@ class TokenizedBuffer buildTokenizedScreenLineForRow: (row, ruleStack) -> line = @buffer.lineForRow(row) + lineEnding = @buffer.lineEndingForRow(row) { tokens, ruleStack } = @languageMode.tokenizeLine(line, ruleStack, row is 0) - new ScreenLine({tokens, ruleStack, @tabLength}) + new ScreenLine({tokens, ruleStack, @tabLength, lineEnding}) lineForScreenRow: (row) -> @linesForScreenRows(row, row)[0] diff --git a/src/app/window.coffee b/src/app/window.coffee index d990b28a4..15f7c319e 100644 --- a/src/app/window.coffee +++ b/src/app/window.coffee @@ -25,9 +25,14 @@ windowAdditions = @syntax = new Syntax @setUpKeymap() @pasteboard = new Pasteboard + @setUpEventHandlers() + setUpEventHandlers: -> $(window).on 'core:close', => @close() $(window).command 'window:close', => @close() + $(window).command 'window:toggle-full-screen', => atom.toggleFullScreen() + $(window).on 'focus', -> $("body").removeClass('is-blurred') + $(window).on 'blur', -> $("body").addClass('is-blurred') # This method is intended only to be run when starting a normal application # Note: RootView assigns itself on window on initialization so that @@ -49,8 +54,8 @@ windowAdditions = atom.setWindowState('pathToOpen', @rootView.project.getPath()) @rootView.deactivate() @rootView = null - $(window).unbind('focus') - $(window).unbind('blur') + $(window).off('focus') + $(window).off('blur') $(window).off('before') setUpKeymap: -> @@ -112,6 +117,14 @@ window.startup() requireStylesheet 'reset.css' requireStylesheet 'atom.css' +requireStylesheet 'tabs.css' +requireStylesheet 'tree-view.css' +requireStylesheet 'status-bar.css' +requireStylesheet 'command-panel.css' +requireStylesheet 'fuzzy-finder.css' +requireStylesheet 'overlay.css' +requireStylesheet 'popover-list.css' +requireStylesheet 'notification.css' if nativeStylesheetPath = require.resolve("#{platform}.css") requireStylesheet(nativeStylesheetPath) diff --git a/src/packages/autocomplete/src/autocomplete-view.coffee b/src/packages/autocomplete/src/autocomplete-view.coffee index 1e071ccf3..f2c200d44 100644 --- a/src/packages/autocomplete/src/autocomplete-view.coffee +++ b/src/packages/autocomplete/src/autocomplete-view.coffee @@ -8,7 +8,7 @@ class AutocompleteView extends SelectList rootView.eachEditor (editor) -> new AutocompleteView(editor) if editor.attached and not editor.mini - @viewClass: -> "autocomplete #{super}" + @viewClass: -> "autocomplete #{super} popover-list" editor: null currentBuffer: null @@ -106,7 +106,6 @@ class AutocompleteView extends SelectList setPosition: -> { left, top } = @editor.pixelPositionForScreenPosition(@originalCursorPosition) - height = @outerHeight() potentialTop = top + @editor.lineHeight potentialBottom = potentialTop - @editor.scrollTop() + height diff --git a/src/packages/bracket-matcher/index.coffee b/src/packages/bracket-matcher/index.coffee new file mode 100644 index 000000000..081ca9923 --- /dev/null +++ b/src/packages/bracket-matcher/index.coffee @@ -0,0 +1,120 @@ +AtomPackage = require 'atom-package' +_ = require 'underscore' +{$$} = require 'space-pen' +Range = require 'range' + +module.exports = +class BracketMatcher extends AtomPackage + startPairMatches: + '(': ')' + '[': ']' + '{': '}' + + endPairMatches: + ')': '(' + ']': '[' + '}': '{' + + pairHighlighted: false + + activate: (rootView) -> + rootView.eachEditor (editor) => @subscribeToEditor(editor) if editor.attached + + subscribeToEditor: (editor) -> + editor.on 'cursor:moved.bracket-matcher', => @updateMatch(editor) + editor.command 'editor:go-to-matching-bracket.bracket-matcher', => + @goToMatchingPair(editor) + editor.on 'editor:will-be-removed', => editor.off('.bracket-matcher') + + goToMatchingPair: (editor) -> + return unless @pairHighlighted + return unless underlayer = editor.pane()?.find('.underlayer') + + position = editor.getCursorBufferPosition() + previousPosition = position.translate([0, -1]) + startPosition = underlayer.find('.bracket-matcher:first').data('bufferPosition') + endPosition = underlayer.find('.bracket-matcher:last').data('bufferPosition') + + if position.isEqual(startPosition) + editor.setCursorBufferPosition(endPosition.translate([0, 1])) + else if previousPosition.isEqual(startPosition) + editor.setCursorBufferPosition(endPosition) + else if position.isEqual(endPosition) + editor.setCursorBufferPosition(startPosition.translate([0, 1])) + else if previousPosition.isEqual(endPosition) + editor.setCursorBufferPosition(startPosition) + + createView: (editor, bufferPosition) -> + pixelPosition = editor.pixelPositionForBufferPosition(bufferPosition) + view = $$ -> @div class: 'bracket-matcher' + view.data('bufferPosition', bufferPosition) + view.css('top', pixelPosition.top).css('left', pixelPosition.left) + view.width(editor.charWidth).height(editor.charHeight) + + findCurrentPair: (editor, buffer, matches) -> + position = editor.getCursorBufferPosition() + currentPair = buffer.getTextInRange(Range.fromPointWithDelta(position, 0, 1)) + unless matches[currentPair] + position = position.translate([0, -1]) + currentPair = buffer.getTextInRange(Range.fromPointWithDelta(position, 0, 1)) + matchingPair = matches[currentPair] + if matchingPair + {position, currentPair, matchingPair} + else + {} + + findMatchingEndPair: (buffer, startPairPosition, startPair, endPair) -> + scanRange = new Range(startPairPosition.translate([0, 1]), buffer.getEofPosition()) + regex = new RegExp("[#{_.escapeRegExp(startPair + endPair)}]", 'g') + endPairPosition = null + unpairedCount = 0 + buffer.scanInRange regex, scanRange, (match, range, {stop}) => + if match[0] is startPair + unpairedCount++ + else if match[0] is endPair + unpairedCount-- + endPairPosition = range.start + stop() if unpairedCount < 0 + endPairPosition + + findMatchingStartPair: (buffer, endPairPosition, startPair, endPair) -> + scanRange = new Range([0, 0], endPairPosition) + regex = new RegExp("[#{_.escapeRegExp(startPair + endPair)}]", 'g') + startPairPosition = null + unpairedCount = 0 + scanner = (match, range, {stop}) => + if match[0] is endPair + unpairedCount++ + else if match[0] is startPair + unpairedCount-- + startPairPosition = range.start + stop() if unpairedCount < 0 + buffer.scanInRange(regex, scanRange, scanner, true) + startPairPosition + + updateMatch: (editor) -> + return unless underlayer = editor.pane()?.find('.underlayer') + + underlayer.find('.bracket-matcher').remove() if @pairHighlighted + @pairHighlighted = false + + return unless editor.getSelection().isEmpty() + return if editor.isFoldedAtCursorRow() + + buffer = editor.getBuffer() + {position, currentPair, matchingPair} = @findCurrentPair(editor, buffer, @startPairMatches) + if position + matchPosition = @findMatchingEndPair(buffer, position, currentPair, matchingPair) + else + {position, currentPair, matchingPair} = @findCurrentPair(editor, buffer, @endPairMatches) + if position + matchPosition = @findMatchingStartPair(buffer, position, matchingPair, currentPair) + + if position? and matchPosition? + if position.isLessThan(matchPosition) + underlayer.append(@createView(editor, position)) + underlayer.append(@createView(editor, matchPosition)) + else + underlayer.append(@createView(editor, matchPosition)) + underlayer.append(@createView(editor, position)) + @pairHighlighted = true diff --git a/src/packages/bracket-matcher/keymaps/bracket-matcher.cson b/src/packages/bracket-matcher/keymaps/bracket-matcher.cson new file mode 100644 index 000000000..672df9be6 --- /dev/null +++ b/src/packages/bracket-matcher/keymaps/bracket-matcher.cson @@ -0,0 +1,2 @@ +'.editor': + 'ctrl-j': 'editor:go-to-matching-bracket' diff --git a/src/packages/bracket-matcher/spec/bracket-matcher-spec.coffee b/src/packages/bracket-matcher/spec/bracket-matcher-spec.coffee new file mode 100644 index 000000000..69e882502 --- /dev/null +++ b/src/packages/bracket-matcher/spec/bracket-matcher-spec.coffee @@ -0,0 +1,86 @@ +RootView = require 'root-view' + +describe "bracket matching", -> + [rootView, editor] = [] + + beforeEach -> + rootView = new RootView(require.resolve('fixtures/sample.js')) + atom.loadPackage('bracket-matcher') + rootView.attachToDom() + editor = rootView.getActiveEditor() + + afterEach -> + rootView.deactivate() + + describe "when the cursor is before a starting pair", -> + it "highlights the starting pair and ending pair", -> + editor.moveCursorToEndOfLine() + editor.moveCursorLeft() + expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 + expect(editor.underlayer.find('.bracket-matcher:first').position()).toEqual editor.pixelPositionForBufferPosition([0,28]) + expect(editor.underlayer.find('.bracket-matcher:last').position()).toEqual editor.pixelPositionForBufferPosition([12,0]) + + describe "when the cursor is after a starting pair", -> + it "highlights the starting pair and ending pair", -> + editor.moveCursorToEndOfLine() + expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 + expect(editor.underlayer.find('.bracket-matcher:first').position()).toEqual editor.pixelPositionForBufferPosition([0,28]) + expect(editor.underlayer.find('.bracket-matcher:last').position()).toEqual editor.pixelPositionForBufferPosition([12,0]) + + describe "when the cursor is before an ending pair", -> + it "highlights the starting pair and ending pair", -> + editor.moveCursorToBottom() + editor.moveCursorLeft() + editor.moveCursorLeft() + expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 + expect(editor.underlayer.find('.bracket-matcher:last').position()).toEqual editor.pixelPositionForBufferPosition([12,0]) + expect(editor.underlayer.find('.bracket-matcher:first').position()).toEqual editor.pixelPositionForBufferPosition([0,28]) + + describe "when the cursor is after an ending pair", -> + it "highlights the starting pair and ending pair", -> + editor.moveCursorToBottom() + editor.moveCursorLeft() + expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 + expect(editor.underlayer.find('.bracket-matcher:last').position()).toEqual editor.pixelPositionForBufferPosition([12,0]) + expect(editor.underlayer.find('.bracket-matcher:first').position()).toEqual editor.pixelPositionForBufferPosition([0,28]) + + describe "when the cursor is moved off a pair", -> + it "removes the starting pair and ending pair highlights", -> + editor.moveCursorToEndOfLine() + expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 + editor.moveCursorToBeginningOfLine() + expect(editor.underlayer.find('.bracket-matcher').length).toBe 0 + + describe "pair balancing", -> + describe "when a second starting pair preceeds the first ending pair", -> + it "advances to the second ending pair", -> + editor.setCursorBufferPosition([8,42]) + expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 + expect(editor.underlayer.find('.bracket-matcher:first').position()).toEqual editor.pixelPositionForBufferPosition([8,42]) + expect(editor.underlayer.find('.bracket-matcher:last').position()).toEqual editor.pixelPositionForBufferPosition([8,54]) + + describe "when editor:go-to-matching-bracket is triggered", -> + describe "when the cursor is before the starting pair", -> + it "moves the cursor to after the ending pair", -> + editor.moveCursorToEndOfLine() + editor.moveCursorLeft() + editor.trigger "editor:go-to-matching-bracket" + expect(editor.getCursorBufferPosition()).toEqual [12, 1] + + describe "when the cursor is after the starting pair", -> + it "moves the cursor to before the ending pair", -> + editor.moveCursorToEndOfLine() + editor.trigger "editor:go-to-matching-bracket" + expect(editor.getCursorBufferPosition()).toEqual [12, 0] + + describe "when the cursor is before the ending pair", -> + it "moves the cursor to after the starting pair", -> + editor.setCursorBufferPosition([12, 0]) + editor.trigger "editor:go-to-matching-bracket" + expect(editor.getCursorBufferPosition()).toEqual [0, 29] + + describe "when the cursor is after the ending pair", -> + it "moves the cursor to before the starting pair", -> + editor.setCursorBufferPosition([12, 1]) + editor.trigger "editor:go-to-matching-bracket" + expect(editor.getCursorBufferPosition()).toEqual [0, 28] diff --git a/src/packages/bracket-matcher/stylesheets/bracket-matcher.css b/src/packages/bracket-matcher/stylesheets/bracket-matcher.css new file mode 100644 index 000000000..8d77b10de --- /dev/null +++ b/src/packages/bracket-matcher/stylesheets/bracket-matcher.css @@ -0,0 +1,3 @@ +.bracket-matcher { + position: absolute; +} diff --git a/src/packages/command-logger/index.coffee b/src/packages/command-logger/index.coffee index 39dd9ad68..027035980 100644 --- a/src/packages/command-logger/index.coffee +++ b/src/packages/command-logger/index.coffee @@ -1,4 +1,5 @@ DeferredAtomPackage = require 'deferred-atom-package' +$ = require 'jquery' module.exports = class CommandLogger extends DeferredAtomPackage @@ -7,4 +8,25 @@ class CommandLogger extends DeferredAtomPackage instanceClass: 'command-logger/src/command-logger-view' - onLoadEvent: (event, instance) -> instance.toggle() + activate: (rootView, state={})-> + super + + @eventLog = state.eventLog ? {} + rootView.command 'command-logger:clear-data', => @eventLog = {} + + registerTriggeredEvent = (eventName) => + eventNameLog = @eventLog[eventName] + unless eventNameLog + eventNameLog = + count: 0 + name: eventName + @eventLog[eventName] = eventNameLog + eventNameLog.count++ + eventNameLog.lastRun = new Date().getTime() + originalTrigger = $.fn.trigger + $.fn.trigger = (eventName) -> + eventName = eventName.type if eventName.type + registerTriggeredEvent(eventName) if $(this).events()[eventName] + originalTrigger.apply(this, arguments) + + onLoadEvent: (event, instance) -> instance.toggle(@eventLog) diff --git a/src/packages/command-logger/spec/command-logger-spec.coffee b/src/packages/command-logger/spec/command-logger-spec.coffee index c01f24320..e2a06da2b 100644 --- a/src/packages/command-logger/spec/command-logger-spec.coffee +++ b/src/packages/command-logger/spec/command-logger-spec.coffee @@ -6,9 +6,8 @@ describe "CommandLogger", -> beforeEach -> rootView = new RootView(require.resolve('fixtures/sample.js')) - atom.loadPackage('command-logger').getInstance() + commandLogger = atom.loadPackage('command-logger') editor = rootView.getActiveEditor() - commandLogger = CommandLogger.instance afterEach -> rootView.deactivate() @@ -44,9 +43,11 @@ describe "CommandLogger", -> describe "when an event is ignored", -> it "does not create a node for that event", -> - commandLogger.ignoredEvents.push 'editor:delete-line' + commandLoggerView = commandLogger.getInstance() + commandLoggerView.ignoredEvents.push 'editor:delete-line' editor.trigger 'editor:delete-line' - nodes = commandLogger.createNodes() + commandLoggerView.eventLog = commandLogger.eventLog + nodes = commandLoggerView.createNodes() for node in nodes continue unless node.name is 'Editor' for child in node.children diff --git a/src/packages/command-logger/src/command-logger-view.coffee b/src/packages/command-logger/src/command-logger-view.coffee index 01496a2d0..9dfa03360 100644 --- a/src/packages/command-logger/src/command-logger-view.coffee +++ b/src/packages/command-logger/src/command-logger-view.coffee @@ -1,12 +1,11 @@ {$$$} = require 'space-pen' ScrollView = require 'scroll-view' -$ = require 'jquery' _ = require 'underscore' module.exports = class CommandLoggerView extends ScrollView @activate: (rootView, state) -> - @instance = new CommandLoggerView(rootView, state?.eventLog) + @instance = new CommandLoggerView(rootView) @content: (rootView) -> @div class: 'command-logger', tabindex: -1, => @@ -31,29 +30,13 @@ class CommandLoggerView extends ScrollView 'tree-view:directory-modified' ] - initialize: (@rootView, @eventLog={}) -> + initialize: (@rootView) -> super - @rootView.command 'command-logger:clear-data', => @eventLog = {} @command 'core:cancel', => @detach() + @on 'blur', => @detach() unless document.activeElement is this[0] - registerEvent = (eventName) => - eventNameLog = @eventLog[eventName] - unless eventNameLog - eventNameLog = - count: 0 - name: eventName - @eventLog[eventName] = eventNameLog - eventNameLog.count++ - eventNameLog.lastRun = new Date().getTime() - - originalTrigger = $.fn.trigger - $.fn.trigger = (eventName) -> - eventName = eventName.type if eventName.type - registerEvent(eventName) if $(this).events()[eventName] - originalTrigger.apply(this, arguments) - - toggle: -> + toggle: (@eventLog={}) -> if @hasParent() @detach() else @@ -161,10 +144,10 @@ class CommandLoggerView extends ScrollView .append('div') .style('width', "#{w}px") .style('height', "#{h}px") - .append('svg:svg') + .append('svg') .attr('width', w) .attr('height', h) - .append('svg:g') + .append('g') .attr('transform', 'translate(.5,.5)') nodes = treemap.nodes(root).filter((d) -> not d.children) @@ -172,17 +155,17 @@ class CommandLoggerView extends ScrollView cell = svg.selectAll('g') .data(nodes) .enter() - .append('svg:g') + .append('g') .attr('class', 'node') .attr('transform', (d) -> "translate(#{d.x},#{d.y})") .on('click', (d) -> if node is d.parent then zoom(root) else zoom(d.parent)) - cell.append('svg:rect') + cell.append('rect') .attr('width', (d) -> d.dx - 1) .attr('height', (d) -> d.dy - 1) .style('fill', (d) -> color(d.parent.name)) - cell.append('svg:foreignObject') + cell.append('foreignObject') .attr('width', (d) -> d.dx - 1) .attr('height', (d) -> d.dy - 1) .attr('class', 'foreign-object') @@ -198,8 +181,11 @@ class CommandLoggerView extends ScrollView @focus() detach: -> - super() + return if @detaching + @detaching = true + super @rootView.focus() + @detaching = false serialize: -> eventLog: @eventLog diff --git a/src/packages/command-palette/spec/command-palette-spec.coffee b/src/packages/command-palette/spec/command-palette-spec.coffee index 2f62d1b7b..1f56f1ab5 100644 --- a/src/packages/command-palette/spec/command-palette-spec.coffee +++ b/src/packages/command-palette/spec/command-palette-spec.coffee @@ -23,8 +23,8 @@ describe "CommandPalette", -> eventLi = palette.list.children("[data-event-name='#{eventName}']") if description expect(eventLi).toExist() - expect(eventLi.find('.event-name')).toHaveText(eventName) - expect(eventLi.find('.event-description')).toHaveText(description) + expect(eventLi.find('.label')).toHaveText(description) + expect(eventLi.find('.label').attr('title')).toBe(eventName) for binding in keyBindings[eventName] ? [] expect(eventLi.find(".key-binding:contains(#{binding})")).toExist() else @@ -39,8 +39,8 @@ describe "CommandPalette", -> description = editorEvents[eventName] unless description if description expect(eventLi).toExist() - expect(eventLi.find('.event-name')).toHaveText(eventName) - expect(eventLi.find('.event-description')).toHaveText(description) + expect(eventLi.find('.label')).toHaveText(description) + expect(eventLi.find('.label').attr('title')).toBe(eventName) else expect(eventLi).not.toExist() diff --git a/src/packages/command-palette/src/command-palette-view.coffee b/src/packages/command-palette/src/command-palette-view.coffee index d723afa54..90c083d0d 100644 --- a/src/packages/command-palette/src/command-palette-view.coffee +++ b/src/packages/command-palette/src/command-palette-view.coffee @@ -9,7 +9,7 @@ class CommandPaletteView extends SelectList @instance = new CommandPaletteView(rootView) @viewClass: -> - "#{super} command-palette" + "#{super} command-palette overlay from-top" filterKey: 'eventDescription' @@ -41,12 +41,10 @@ class CommandPaletteView extends SelectList keyBindings = @keyBindings $$ -> @li class: 'event', 'data-event-name': eventName, => - @div eventDescription, class: 'event-description' + @span eventDescription, class: 'label', title: eventName @div class: 'right', => - @div eventName, class: 'event-name' for binding in keyBindings[eventName] ? [] - @div binding, class: 'key-binding' - @div class: 'clear-float' + @kbd binding, class: 'key-binding' confirmed: ({eventName}) -> @cancel() diff --git a/src/packages/command-panel/spec/command-panel-spec.coffee b/src/packages/command-panel/spec/command-panel-spec.coffee index 635613912..c0d47fe45 100644 --- a/src/packages/command-panel/spec/command-panel-spec.coffee +++ b/src/packages/command-panel/spec/command-panel-spec.coffee @@ -408,7 +408,7 @@ describe "CommandPanel", -> expect(previewList.scrollBottom()).toBeCloseTo previewList.prop('scrollHeight'), -1 - previewList.trigger 'core:move-down' + _.times previewList.getOperations().length, -> previewList.trigger 'core:move-up' expect(previewList.scrollTop()).toBe 0 it "doesn't bubble up the event and the command panel text doesn't change", -> @@ -438,13 +438,13 @@ describe "CommandPanel", -> expect(previewList.getSelectedOperation()).toBe previewList.getOperations()[0] describe "when core:confirm is triggered on the preview list", -> - it "opens the operation's buffer, selects and scrolls to the search result, and focuses the active editor", -> + it "opens the operation's buffer, selects and scrolls to the search result, and refocuses the preview list", -> rootView.height(200) rootView.attachToDom() waitsForPromise -> commandPanel.execute('X x/apply/') # use apply because it is at the end of the file runs -> - spyOn(rootView, 'focus') + spyOn(previewList, 'focus') executeHandler = jasmine.createSpy('executeHandler') commandPanel.on 'core:confirm', executeHandler @@ -458,13 +458,13 @@ describe "CommandPanel", -> expect(editSession.getSelectedBufferRange()).toEqual operation.getBufferRange() expect(editSession.getSelectedBufferRange()).toEqual operation.getBufferRange() expect(editor.isScreenRowVisible(editor.getCursorScreenRow())).toBeTruthy() - expect(rootView.focus).toHaveBeenCalled() + expect(previewList.focus).toHaveBeenCalled() expect(executeHandler).not.toHaveBeenCalled() describe "when an operation in the preview list is clicked", -> - it "opens the operation's buffer, selects the search result, and focuses the active editor", -> - spyOn(rootView, 'focus') + it "opens the operation's buffer, selects the search result, and refocuses the preview list", -> + spyOn(previewList, 'focus') operation = previewList.getOperations()[4] previewList.find('li.operation:eq(4) span').mousedown() @@ -473,4 +473,4 @@ describe "CommandPanel", -> editSession = rootView.getActiveEditSession() expect(editSession.buffer.getPath()).toBe project.resolve(operation.getPath()) expect(editSession.getSelectedBufferRange()).toEqual operation.getBufferRange() - expect(rootView.focus).toHaveBeenCalled() + expect(previewList.focus).toHaveBeenCalled() diff --git a/src/packages/command-panel/src/command-panel-view.coffee b/src/packages/command-panel/src/command-panel-view.coffee index 7ccb74a48..d27711a70 100644 --- a/src/packages/command-panel/src/command-panel-view.coffee +++ b/src/packages/command-panel/src/command-panel-view.coffee @@ -51,7 +51,7 @@ class CommandPanelView extends View @previewList.hide() @previewCount.hide() @errorMessages.hide() - @prompt.iconSize(@miniEditor.fontSize) + @prompt.iconSize(@miniEditor.getFontSize()) serialize: -> text: @miniEditor.getText() diff --git a/src/packages/command-panel/src/preview-list.coffee b/src/packages/command-panel/src/preview-list.coffee index 34cbd240c..364a6c5b6 100644 --- a/src/packages/command-panel/src/preview-list.coffee +++ b/src/packages/command-panel/src/preview-list.coffee @@ -2,6 +2,7 @@ $ = require 'jquery' {$$$} = require 'space-pen' ScrollView = require 'scroll-view' _ = require 'underscore' +fs = require 'fs' module.exports = class PreviewList extends ScrollView @@ -34,7 +35,9 @@ class PreviewList extends ScrollView operation.index = index for operation, index in operations operationsByPath = _.groupBy(operations, (operation) -> operation.getPath()) for path, ops of operationsByPath - @li class: 'path', => + classes = ['path'] + classes.push('readme') if fs.isReadme(path) + @li class: classes.join(' '), => @span path @span "(#{ops.length})", class: 'path-match-number' for operation in ops @@ -83,7 +86,7 @@ class PreviewList extends ScrollView editSession = @rootView.open(operation.getPath()) bufferRange = operation.execute(editSession) editSession.setSelectedBufferRange(bufferRange, autoscroll: true) if bufferRange - @rootView.focus() + @focus() false getPathCount: -> diff --git a/src/packages/editor-stats/index.coffee b/src/packages/editor-stats/index.coffee new file mode 100644 index 000000000..e743c0f18 --- /dev/null +++ b/src/packages/editor-stats/index.coffee @@ -0,0 +1,16 @@ +DeferredAtomPackage = require 'deferred-atom-package' +Stats = require './src/stats' + +module.exports = +class EditorStats extends DeferredAtomPackage + loadEvents: ['editor-stats:toggle'] + instanceClass: 'editor-stats/src/editor-stats-view' + stats: new Stats + + activate: (rootView) -> + super + + rootView.on 'keydown', => @stats.track() + rootView.on 'mouseup', => @stats.track() + + onLoadEvent: (event, instance) -> instance.toggle(@stats) diff --git a/src/packages/editor-stats/keymaps/editor-stats.cson b/src/packages/editor-stats/keymaps/editor-stats.cson new file mode 100644 index 000000000..819b7c04f --- /dev/null +++ b/src/packages/editor-stats/keymaps/editor-stats.cson @@ -0,0 +1,2 @@ +'body': + 'meta-alt-s': 'editor-stats:toggle' diff --git a/src/packages/editor-stats/spec/editor-stats-spec.coffee b/src/packages/editor-stats/spec/editor-stats-spec.coffee new file mode 100644 index 000000000..ca274a252 --- /dev/null +++ b/src/packages/editor-stats/spec/editor-stats-spec.coffee @@ -0,0 +1,46 @@ +$ = require 'jquery' +RootView = require 'root-view' +EditorStats = require 'editor-stats/src/editor-stats-view' + +describe "EditorStats", -> + [rootView, editorStats, time] = [] + + simulateKeyUp = (key) -> + e = $.Event "keydown", keyCode: key.charCodeAt(0) + rootView.trigger(e) + + simulateClick = -> + e = $.Event "mouseup" + rootView.trigger(e) + + beforeEach -> + rootView = new RootView(require.resolve('fixtures/sample.js')) + + date = new Date() + mins = date.getMinutes() + hours = date.getHours() + + mins = if mins == 60 then '01' else mins + 1 + time = "#{hours}:#{mins}" + + editorStatsPackage = atom.loadPackage('editor-stats') + editorStatsPackage.getInstance() + editorStats = editorStatsPackage.stats + editorStats.clear() + + afterEach -> + rootView.deactivate() + + describe "when a keyup event is triggered", -> + it "records the number of times a keyup is triggered", -> + simulateKeyUp('a') + expect(editorStats.eventLog[time]).toBe 1 + simulateKeyUp('b') + expect(editorStats.eventLog[time]).toBe 2 + + describe "when a mouseup event is triggered", -> + it "records the number of times a mouseup is triggered", -> + simulateClick() + expect(editorStats.eventLog[time]).toBe 1 + simulateClick() + expect(editorStats.eventLog[time]).toBe 2 diff --git a/src/packages/editor-stats/src/editor-stats-view.coffee b/src/packages/editor-stats/src/editor-stats-view.coffee new file mode 100644 index 000000000..aea6ca303 --- /dev/null +++ b/src/packages/editor-stats/src/editor-stats-view.coffee @@ -0,0 +1,103 @@ +ScrollView = require 'scroll-view' +d3 = require 'd3.v3' +_ = require 'underscore' +$ = require 'jquery' + +module.exports = +class EditorStatsView extends ScrollView + @activate: (rootView, state) -> + @instance = new EditorStatsView(rootView) + + @content: (rootView) -> + @div class: 'editor-stats-wrapper', tabindex: -1, => + @div class: 'editor-stats', outlet: 'editorStats' + + pt: 15 + pl: 10 + pb: 3 + pr: 25 + + initialize: (@rootView) -> + super + + resizer = => + return unless @isOnDom() + @draw() + @update() + @subscribe $(window), 'resize', _.debounce(resizer, 300) + + draw: -> + @editorStats.empty() + @x ?= d3.scale.ordinal().domain d3.range(@stats.hours * 60) + @y ?= d3.scale.linear() + w = @rootView.vertical.width() + h = @height() + data = d3.entries @stats.eventLog + max = d3.max data, (d) -> d.value + + @x.rangeBands [0, w - @pl - @pr], 0.2 + @y.domain([0, max]).range [h - @pt - @pb, 0] + + @xaxis ?= d3.svg.axis().scale(@x).orient('top') + .tickSize(-h + @pt + @pb) + .tickFormat (d) => + d = new Date(@stats.startDate.getTime() + (d * 6e4)) + mins = d.getMinutes() + mins = "0#{mins}" if mins <= 9 + "#{d.getHours()}:#{mins}" + + vis = d3.select(@editorStats.get(0)).append('svg') + .attr('width', w) + .attr('height', h) + .append('g') + .attr('transform', "translate(#{@pl},#{@pt})") + + vis.append('g') + .attr('class', 'x axis') + .call(@xaxis) + .selectAll('g') + .classed('minor', (d, i) -> i % 5 == 0 && i % 15 != 0) + .style 'display', (d, i) -> + if i % 15 == 0 || i % 5 == 0 || i == data.length - 1 + 'block' + else + 'none' + + @bars = vis.selectAll('rect.bar') + .data(data) + .enter().append('rect') + .attr('x', (d, i) => @x i) + .attr('height', (d, i) => h - @y(d.value) - @pt - @pb) + .attr('y', (d) => @y(d.value)) + .attr('width', @x.rangeBand()) + .attr('class', 'bar') + + clearInterval(@updateInterval) + updater = => @update() if @isOnDom() + setTimeout(updater, 100) + @updateInterval = setInterval(updater, 5000) + + update: -> + newData = d3.entries @stats.eventLog + max = d3.max newData, (d) -> d.value + @y.domain [0, max] + h = @height() + @bars.data(newData).transition() + .attr('height', (d, i) => h - @y(d.value) - @pt - @pb) + .attr('y', (d, i) => @y(d.value)) + @bars.classed('max', (d, i) -> d.value == max) + + toggle: (@stats) -> + if @hasParent() + @detach() + else + @attach() + + attach: -> + @rootView.vertical.append(@) + @draw() + + detach: -> + super() + clearInterval(@updateInterval) + @rootView.focus() diff --git a/src/packages/editor-stats/src/stats.coffee b/src/packages/editor-stats/src/stats.coffee new file mode 100644 index 000000000..5e9c6700d --- /dev/null +++ b/src/packages/editor-stats/src/stats.coffee @@ -0,0 +1,29 @@ +module.exports = +class Stats + startDate: new Date + hours: 6 + eventLog: [] + + constructor: -> + date = new Date(@startDate) + future = new Date(date.getTime() + (36e5 * @hours)) + @eventLog[@time(date)] = 0 + + while date < future + @eventLog[@time(date)] = 0 + + clear: -> + @eventLog = [] + + track: -> + date = new Date + times = @time date + @eventLog[times] ?= 0 + @eventLog[times] += 1 + @eventLog.shift() if @eventLog.length > (@hours * 60) + + time: (date) -> + date.setTime(date.getTime() + 6e4) + hour = date.getHours() + minute = date.getMinutes() + "#{hour}:#{minute}" diff --git a/src/packages/editor-stats/stylesheets/editor-stats.css b/src/packages/editor-stats/stylesheets/editor-stats.css new file mode 100644 index 000000000..a7d5e3ff2 --- /dev/null +++ b/src/packages/editor-stats/stylesheets/editor-stats.css @@ -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); +} + +.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/fuzzy-finder/index.coffee b/src/packages/fuzzy-finder/index.coffee index 1d815057e..2f8cf6783 100644 --- a/src/packages/fuzzy-finder/index.coffee +++ b/src/packages/fuzzy-finder/index.coffee @@ -1,8 +1,8 @@ DeferredAtomPackage = require 'deferred-atom-package' +LoadPathsTask = require './src/load-paths-task' module.exports = class FuzzyFinder extends DeferredAtomPackage - loadEvents: [ 'fuzzy-finder:toggle-file-finder' 'fuzzy-finder:toggle-buffer-finder' @@ -11,7 +11,18 @@ class FuzzyFinder extends DeferredAtomPackage instanceClass: 'fuzzy-finder/src/fuzzy-finder-view' + activate: (rootView) -> + super + + if rootView.project.getPath()? + callback = (paths) => @projectPaths = paths + new LoadPathsTask(rootView, callback).start() + onLoadEvent: (event, instance) -> + if @projectPaths? and not instance.projectPaths? + instance.projectPaths = @projectPaths + instance.reloadProjectPaths = false + switch event.type when 'fuzzy-finder:toggle-file-finder' instance.toggleFileFinder() diff --git a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee index ad40e0010..76f8bd476 100644 --- a/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee +++ b/src/packages/fuzzy-finder/spec/fuzzy-finder-spec.coffee @@ -1,5 +1,6 @@ RootView = require 'root-view' FuzzyFinder = require 'fuzzy-finder/src/fuzzy-finder-view' +LoadPathsTask = require 'fuzzy-finder/src/load-paths-task' $ = require 'jquery' {$$} = require 'space-pen' fs = require 'fs' @@ -48,14 +49,13 @@ describe 'FuzzyFinder', -> expect(finder.find(".loading")).toBeVisible() expect(finder.find(".loading")).toHaveText "Indexing..." - waitsForPromise -> - rootView.project.getFilePaths().done (foundPaths) -> paths = foundPaths - - waitsFor -> - finder.list.children('li').length > 0 + waitsFor "all project paths to load", 5000, -> + if finder.projectPaths?.length > 0 + paths = finder.projectPaths + true runs -> - expect(finder.list.children('li').length).toBe paths.length, finder.maxResults + expect(finder.list.children('li').length).toBe paths.length for path in paths expect(finder.list.find("li:contains(#{fs.base(path)})")).toExist() expect(finder.list.children().first()).toHaveClass 'selected' @@ -222,15 +222,15 @@ describe 'FuzzyFinder', -> describe "cached file paths", -> it "caches file paths after first time", -> - spyOn(rootView.project, "getFilePaths").andCallThrough() + spyOn(LoadPathsTask.prototype, "start").andCallThrough() rootView.trigger 'fuzzy-finder:toggle-file-finder' waitsFor -> finder.list.children('li').length > 0 runs -> - expect(rootView.project.getFilePaths).toHaveBeenCalled() - rootView.project.getFilePaths.reset() + expect(finder.loadPathsTask.start).toHaveBeenCalled() + finder.loadPathsTask.start.reset() rootView.trigger 'fuzzy-finder:toggle-file-finder' rootView.trigger 'fuzzy-finder:toggle-file-finder' @@ -238,45 +238,44 @@ describe 'FuzzyFinder', -> finder.list.children('li').length > 0 runs -> - expect(rootView.project.getFilePaths).not.toHaveBeenCalled() + expect(finder.loadPathsTask.start).not.toHaveBeenCalled() it "doesn't cache buffer paths", -> - spyOn(rootView.project, "getFilePaths").andCallThrough() + spyOn(rootView, "getOpenBufferPaths").andCallThrough() rootView.trigger 'fuzzy-finder:toggle-buffer-finder' waitsFor -> finder.list.children('li').length > 0 runs -> - expect(rootView.project.getFilePaths).not.toHaveBeenCalled() - rootView.project.getFilePaths.reset() + expect(rootView.getOpenBufferPaths).toHaveBeenCalled() + rootView.getOpenBufferPaths.reset() + rootView.trigger 'fuzzy-finder:toggle-buffer-finder' rootView.trigger 'fuzzy-finder:toggle-buffer-finder' - rootView.trigger 'fuzzy-finder:toggle-file-finder' waitsFor -> finder.list.children('li').length > 0 runs -> - expect(rootView.project.getFilePaths).toHaveBeenCalled() + expect(rootView.getOpenBufferPaths).toHaveBeenCalled() it "busts the cache when the window gains focus", -> - spyOn(rootView.project, "getFilePaths").andCallThrough() + spyOn(LoadPathsTask.prototype, "start").andCallThrough() rootView.trigger 'fuzzy-finder:toggle-file-finder' waitsFor -> finder.list.children('li').length > 0 runs -> - expect(rootView.project.getFilePaths).toHaveBeenCalled() - rootView.project.getFilePaths.reset() + expect(finder.loadPathsTask.start).toHaveBeenCalled() + finder.loadPathsTask.start.reset() $(window).trigger 'focus' rootView.trigger 'fuzzy-finder:toggle-file-finder' rootView.trigger 'fuzzy-finder:toggle-file-finder' - expect(rootView.project.getFilePaths).toHaveBeenCalled() + expect(finder.loadPathsTask.start).toHaveBeenCalled() describe "path ignoring", -> it "ignores paths that match entries in config.fuzzyFinder.ignoredNames", -> - spyOn(rootView.project, "getFilePaths").andCallThrough() config.set("fuzzyFinder.ignoredNames", ["tree-view.js"]) rootView.trigger 'fuzzy-finder:toggle-file-finder' finder.maxItems = Infinity @@ -293,7 +292,6 @@ describe 'FuzzyFinder', -> beforeEach -> editor = rootView.getActiveEditor() rootView.attachToDom() - spyOn(rootView.project, "getFilePaths").andCallThrough() it "opens the fuzzy finder window when there are multiple matches", -> editor.setText("sample") diff --git a/src/packages/fuzzy-finder/src/fuzzy-finder-view.coffee b/src/packages/fuzzy-finder/src/fuzzy-finder-view.coffee index 2365fc4d7..17165a8c1 100644 --- a/src/packages/fuzzy-finder/src/fuzzy-finder-view.coffee +++ b/src/packages/fuzzy-finder/src/fuzzy-finder-view.coffee @@ -3,6 +3,7 @@ SelectList = require 'select-list' _ = require 'underscore' $ = require 'jquery' fs = require 'fs' +LoadPathsTask = require 'fuzzy-finder/src/load-paths-task' module.exports = class FuzzyFinderView extends SelectList @@ -12,7 +13,7 @@ class FuzzyFinderView extends SelectList @instance = new FuzzyFinderView(rootView) @viewClass: -> - [super, 'fuzzy-finder'].join(' ') + [super, 'fuzzy-finder', 'overlay', 'from-top'].join(' ') allowActiveEditorChange: null maxItems: 10 @@ -38,7 +39,9 @@ class FuzzyFinderView extends SelectList $$ -> @li => ext = fs.extension(path) - if fs.isCompressedExtension(ext) + if fs.isReadme(path) + typeClass = 'readme-name' + else if fs.isCompressedExtension(ext) typeClass = 'compressed-name' else if fs.isImageExtension(ext) typeClass = 'image-name' @@ -46,9 +49,9 @@ class FuzzyFinderView extends SelectList typeClass = 'pdf-name' else typeClass = 'text-name' - @span fs.base(path), class: "file #{typeClass}" + @span fs.base(path), class: "file label #{typeClass}" if folder = fs.directory(path) - @span "- #{folder}/", class: 'directory' + @span " - #{folder}/", class: 'directory' openPath: (path) -> @rootView.open(path, {@allowActiveEditorChange}) if path @@ -126,16 +129,9 @@ class FuzzyFinderView extends SelectList @setLoading("Indexing...") if @reloadProjectPaths - @rootView.project.getFilePaths().done (paths) => - ignoredNames = config.get("fuzzyFinder.ignoredNames") or [] - ignoredNames = ignoredNames.concat(config.get("core.ignoredNames") or []) + @loadPathsTask?.terminate() + callback = (paths) => @projectPaths = paths - if ignoredNames - @projectPaths = @projectPaths.filter (path) -> - for segment in path.split("/") - return false if _.contains(ignoredNames, segment) - return true - @reloadProjectPaths = false listedItems = if options.filter? @@ -146,6 +142,8 @@ class FuzzyFinderView extends SelectList @setArray(listedItems) options.done(listedItems) if options.done? + @loadPathsTask = new LoadPathsTask(@rootView, callback) + @loadPathsTask.start() populateOpenBufferPaths: -> @paths = @rootView.getOpenBufferPaths().map (path) => diff --git a/src/packages/fuzzy-finder/src/load-paths-handler.coffee b/src/packages/fuzzy-finder/src/load-paths-handler.coffee new file mode 100644 index 000000000..98c439d3d --- /dev/null +++ b/src/packages/fuzzy-finder/src/load-paths-handler.coffee @@ -0,0 +1,19 @@ +fs = require 'fs' +_ = require 'underscore' +Git = require 'git' + +module.exports = + loadPaths: (rootPath, ignoredNames, excludeGitIgnoredPaths) -> + paths = [] + repo = Git.open(rootPath, refreshIndexOnFocus: false) if excludeGitIgnoredPaths + isIgnored = (path) -> + for segment in path.split('/') + return true if _.contains(ignoredNames, segment) + repo?.isPathIgnored(fs.join(rootPath, path)) + onFile = (path) -> + paths.push(path) unless isIgnored(path) + onDirectory = (path) -> + not isIgnored(path) + fs.traverseTree(rootPath, onFile, onDirectory) + repo?.destroy() + callTaskMethod('pathsLoaded', paths) diff --git a/src/packages/fuzzy-finder/src/load-paths-task.coffee b/src/packages/fuzzy-finder/src/load-paths-task.coffee new file mode 100644 index 000000000..64bf095b5 --- /dev/null +++ b/src/packages/fuzzy-finder/src/load-paths-task.coffee @@ -0,0 +1,17 @@ +Task = require 'task' + +module.exports = +class LoadPathsTask extends Task + constructor: (@rootView, @callback)-> + super('fuzzy-finder/src/load-paths-handler') + + started: -> + ignoredNames = config.get('fuzzyFinder.ignoredNames') ? [] + ignoredNames = ignoredNames.concat(config.get('core.ignoredNames') ? []) + excludeGitIgnoredPaths = config.get('core.hideGitIgnoredFiles') + rootPath = @rootView.project.getPath() + @callWorkerMethod('loadPaths', rootPath, ignoredNames, excludeGitIgnoredPaths) + + pathsLoaded: (paths) -> + @terminate() + @callback(paths) diff --git a/src/packages/gists/lib/gists.coffee b/src/packages/gists/lib/gists.coffee index 434603636..b46e5beab 100644 --- a/src/packages/gists/lib/gists.coffee +++ b/src/packages/gists/lib/gists.coffee @@ -22,10 +22,10 @@ class Gists success: (response) => pasteboard.write(response.html_url) notification = $$ -> - @div class: 'gist-notification', => - @div class: 'message-area', => - @span "Gist #{response.id} created", class: 'message' - @br() - @span "The url is on your clipboard", class: 'clipboard' + @div class: 'notification', => + @span class: 'icon icon-gist mega-icon' + @div class: 'content', => + @h3 "Gist #{response.id} created", class: 'title' + @p "The url is on your clipboard", class: 'message' @rootView.append(notification.hide()) notification.fadeIn().delay(2000).fadeOut(complete: -> $(this).remove()) diff --git a/src/packages/gists/spec/gists-spec.coffee b/src/packages/gists/spec/gists-spec.coffee index 64227744a..056d6e412 100644 --- a/src/packages/gists/spec/gists-spec.coffee +++ b/src/packages/gists/spec/gists-spec.coffee @@ -44,8 +44,8 @@ describe "Gists package", -> expect(pasteboard.read()[0]).toBe 'https://gist.github.com/1' it "flashes that the Gist was created", -> - expect(rootView.find('.gist-notification')).toExist() - expect(rootView.find('.gist-notification .message').text()).toBe 'Gist 1 created' + expect(rootView.find('.notification')).toExist() + expect(rootView.find('.notification .title').text()).toBe 'Gist 1 created' advanceClock(2000) expect(rootView.find('.gist-notification')).not.toExist() 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 ce99ece31..f207e11eb 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 @@ -9,7 +9,7 @@ class GoToLineView extends View @activate: (rootView) -> new GoToLineView(rootView) @content: -> - @div class: 'go-to-line', => + @div class: 'go-to-line overlay from-top mini', => @subview 'miniEditor', new Editor(mini: true) @div class: 'message', outlet: 'message' diff --git a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee index 575990c57..49066159b 100644 --- a/src/packages/markdown-preview/spec/markdown-preview-spec.coffee +++ b/src/packages/markdown-preview/spec/markdown-preview-spec.coffee @@ -7,8 +7,6 @@ describe "MarkdownPreview", -> beforeEach -> rootView = new RootView(require.resolve('fixtures/markdown')) - atom.loadPackage("markdown-preview").getInstance() - markdownPreview = MarkdownPreview.instance afterEach -> rootView.deactivate() @@ -17,6 +15,7 @@ describe "MarkdownPreview", -> it "toggles on/off a preview for a .md file", -> rootView.open('file.md') editor = rootView.getActiveEditor() + markdownPreview = atom.loadPackage("markdown-preview").getInstance() expect(rootView.find('.markdown-preview')).not.toExist() spyOn(markdownPreview, 'loadHtml') editor.trigger('markdown-preview:toggle') @@ -30,6 +29,7 @@ describe "MarkdownPreview", -> it "displays a preview for a .markdown file", -> rootView.open('file.markdown') editor = rootView.getActiveEditor() + markdownPreview = atom.loadPackage("markdown-preview").getInstance() expect(rootView.find('.markdown-preview')).not.toExist() spyOn(markdownPreview, 'loadHtml') editor.trigger('markdown-preview:toggle') @@ -39,6 +39,7 @@ describe "MarkdownPreview", -> it "does not display a preview for non-markdown file", -> rootView.open('file.js') editor = rootView.getActiveEditor() + markdownPreview = atom.loadPackage("markdown-preview").getInstance() expect(rootView.find('.markdown-preview')).not.toExist() spyOn(markdownPreview, 'loadHtml') editor.trigger('markdown-preview:toggle') @@ -49,6 +50,7 @@ describe "MarkdownPreview", -> it "removes markdown preview", -> rootView.open('file.md') editor = rootView.getActiveEditor() + markdownPreview = atom.loadPackage("markdown-preview").getInstance() expect(rootView.find('.markdown-preview')).not.toExist() spyOn(markdownPreview, 'loadHtml') editor.trigger('markdown-preview:toggle') @@ -57,3 +59,17 @@ describe "MarkdownPreview", -> 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.open('file.md') + editor = rootView.getActiveEditor() + markdownPreview = atom.loadPackage("markdown-preview").getInstance() + expect(rootView.find('.markdown-preview')).not.toExist() + spyOn(markdownPreview, 'loadHtml') + editor.trigger('markdown-preview:toggle') + + markdownPreviewView = rootView.find('.markdown-preview') + expect(markdownPreviewView).toExist() + editor.focus() + expect(rootView.find('.markdown-preview')).not.toExist() diff --git a/src/packages/markdown-preview/src/markdown-preview-view.coffee b/src/packages/markdown-preview/src/markdown-preview-view.coffee index d7fb9fc6c..c044623ab 100644 --- a/src/packages/markdown-preview/src/markdown-preview-view.coffee +++ b/src/packages/markdown-preview/src/markdown-preview-view.coffee @@ -14,7 +14,10 @@ class MarkdownPreviewView extends ScrollView initialize: (@rootView) -> super - @command 'core:cancel', => @detach() + + @editor = @rootView.getActiveEditor() + @subscribe @editor, 'focus', => @detach() unless @detaching + @command 'core:cancel', => @detach() unless @detaching toggle: -> if @hasParent() @@ -30,14 +33,16 @@ class MarkdownPreviewView extends ScrollView @focus() detach: -> - super() + @detaching = true + super @rootView.focus() + @detaching = false getActivePath: -> - @rootView.getActiveEditor()?.getPath() + @editor.getPath() getActiveText: -> - @rootView.getActiveEditor()?.getText() + @editor.getText() getErrorHtml: (error) -> $$$ -> diff --git a/src/packages/snippets/index.coffee b/src/packages/snippets/index.coffee index 4f93ede37..6778eb145 100644 --- a/src/packages/snippets/index.coffee +++ b/src/packages/snippets/index.coffee @@ -1,17 +1,14 @@ AtomPackage = require 'atom-package' fs = require 'fs' -PEG = require 'pegjs' _ = require 'underscore' SnippetExpansion = require './src/snippet-expansion' Snippet = require './src/snippet' -require './src/package-extensions' +LoadSnippetsTask = require './src/load-snippets-task' module.exports = class Snippets extends AtomPackage - snippetsByExtension: {} - parser: PEG.buildParser(fs.read(require.resolve 'snippets/snippets.pegjs'), trackLineAndColumn: true) - userSnippetsDir: fs.join(config.configDirPath, 'snippets') + loaded: false activate: (@rootView) -> window.snippets = this @@ -19,10 +16,7 @@ class Snippets extends AtomPackage @rootView.on 'editor:attached', (e, editor) => @enableSnippetsInEditor(editor) loadAll: -> - for pack in atom.getPackages() - pack.loadSnippets() - - @loadDirectory(@userSnippetsDir) if fs.exists(@userSnippetsDir) + new LoadSnippetsTask(this).start() loadDirectory: (snippetsDirPath) -> for snippetsPath in fs.list(snippetsDirPath) when fs.base(snippetsPath).indexOf('.') isnt 0 @@ -39,12 +33,16 @@ class Snippets extends AtomPackage for selector, snippetsByName of snippetsBySelector snippetsByPrefix = {} for name, attributes of snippetsByName - { prefix, body } = attributes - bodyTree = @parser.parse(body) + { prefix, body, bodyTree } = attributes + # if `add` isn't called by the loader task (in specs for example), we need to parse the body + bodyTree ?= @getBodyParser().parse(body) snippet = new Snippet({name, prefix, bodyTree}) snippetsByPrefix[snippet.prefix] = snippet syntax.addProperties(selector, snippets: snippetsByPrefix) + getBodyParser: -> + require 'snippets/src/snippet-body-parser' + enableSnippetsInEditor: (editor) -> editor.command 'snippets:expand', (e) => editSession = editor.activeEditSession diff --git a/src/packages/snippets/keymaps/snippets-1.cson b/src/packages/snippets/keymaps/snippets-1.cson index c8bf63c51..ce982b6a2 100644 --- a/src/packages/snippets/keymaps/snippets-1.cson +++ b/src/packages/snippets/keymaps/snippets-1.cson @@ -1,2 +1,2 @@ -window.keymap.bindKeys '.editor' +'.editor': 'tab': 'snippets:expand' diff --git a/src/packages/snippets/keymaps/snippets-2.cson b/src/packages/snippets/keymaps/snippets-2.cson index 6b660c6fd..9cf7b49e5 100644 --- a/src/packages/snippets/keymaps/snippets-2.cson +++ b/src/packages/snippets/keymaps/snippets-2.cson @@ -1,6 +1,6 @@ # it's critical that these bindings be loaded after those snippets-1 so they # are later in the cascade, hence breaking the keymap into 2 files -window.keymap.bindKeys '.editor' +'.editor': 'tab': 'snippets:next-tab-stop' 'shift-tab': 'snippets:previous-tab-stop' diff --git a/src/packages/snippets/spec/snippets-spec.coffee b/src/packages/snippets/spec/snippets-spec.coffee index 942b7b4ea..c9582a336 100644 --- a/src/packages/snippets/spec/snippets-spec.coffee +++ b/src/packages/snippets/spec/snippets-spec.coffee @@ -1,21 +1,20 @@ Snippets = require 'snippets' Snippet = require 'snippets/src/snippet' +LoadSnippetsTask = require 'snippets/src/load-snippets-task' RootView = require 'root-view' Buffer = require 'buffer' Editor = require 'editor' _ = require 'underscore' fs = require 'fs' -AtomPackage = require 'atom-package' -TextMatePackage = require 'text-mate-package' describe "Snippets extension", -> - [buffer, editor] = [] + [buffer, editor, editSession] = [] beforeEach -> rootView = new RootView(require.resolve('fixtures/sample.js')) - spyOn(AtomPackage.prototype, 'loadSnippets') - spyOn(TextMatePackage.prototype, 'loadSnippets') + spyOn(LoadSnippetsTask.prototype, 'start') atom.loadPackage("snippets") editor = rootView.getActiveEditor() + editSession = rootView.getActiveEditSession() buffer = editor.getBuffer() rootView.simulateDomAttachment() rootView.enableKeymap() @@ -44,15 +43,7 @@ describe "Snippets extension", -> prefix: "t3" body: """ line 1 - line 2$1 - - """ - - "tab stop placeholders": - prefix: "t4" - body: """ - go here ${1:first - think a while}, and then here ${2:second} + \tline 2$1 """ @@ -67,6 +58,15 @@ describe "Snippets extension", -> ${2:placeholder ending second line} """ + "contains empty lines": + prefix: "t7" + body: """ + first line $1 + + + fourth line after blanks $2 + """ + describe "when the letters preceding the cursor trigger a snippet", -> describe "when the snippet contains no tab stops", -> it "replaces the prefix with the snippet text and places the cursor at its end", -> @@ -120,16 +120,6 @@ describe "Snippets extension", -> expect(buffer.lineForRow(2)).toBe "go here next:(abc) and finally go here:( )" expect(editor.activeEditSession.getAnchors().length).toBe anchorCountBefore - describe "when the tab stops have placeholder text", -> - it "auto-fills the placeholder text and highlights it when navigating to that tab stop", -> - editor.insertText 't4' - editor.trigger 'snippets:expand' - expect(buffer.lineForRow(0)).toBe 'go here first' - expect(buffer.lineForRow(1)).toBe 'think a while, and then here second' - expect(editor.getSelectedBufferRange()).toEqual [[0, 8], [1, 13]] - editor.trigger keydownEvent('tab', target: editor[0]) - expect(editor.getSelectedBufferRange()).toEqual [[1, 29], [1, 35]] - describe "when tab stops are nested", -> it "destroys the inner tab stop if the outer tab stop is modified", -> buffer.setText('') @@ -141,6 +131,14 @@ describe "Snippets extension", -> editor.trigger keydownEvent('tab', target: editor[0]) expect(editor.getSelectedBufferRange()).toEqual [[0, 5], [0, 10]] + describe "when tab stops are separated by blank lines", -> + it "correctly places the tab stops (regression)", -> + buffer.setText('') + editor.insertText 't7' + editor.trigger 'snippets:expand' + editor.trigger 'snippets:next-tab-stop' + expect(editSession.getCursorBufferPosition()).toEqual [3, 25] + describe "when the cursor is moved beyond the bounds of a tab stop", -> it "terminates the snippet", -> editor.setCursorScreenPosition([2, 0]) @@ -164,7 +162,24 @@ describe "Snippets extension", -> editor.trigger keydownEvent('tab', shiftKey: true, target: editor[0]) expect(editor.getCursorBufferPosition()).toEqual [4, 15] - describe "when a the start of the snippet is indented", -> + describe "when the snippet contains hard tabs", -> + describe "when the edit session is in soft-tabs mode", -> + it "translates hard tabs in the snippet to the appropriate number of spaces", -> + expect(editSession.softTabs).toBeTruthy() + editor.insertText("t3") + editor.trigger keydownEvent('tab', target: editor[0]) + expect(buffer.lineForRow(1)).toBe " line 2" + expect(editSession.getCursorBufferPosition()).toEqual [1, 8] + + describe "when the edit session is in hard-tabs mode", -> + it "inserts hard tabs in the snippet directly", -> + editSession.setSoftTabs(false) + editor.insertText("t3") + editor.trigger keydownEvent('tab', target: editor[0]) + expect(buffer.lineForRow(1)).toBe "\tline 2" + expect(editSession.getCursorBufferPosition()).toEqual [1, 7] + + describe "when the snippet prefix is indented", -> describe "when the snippet spans a single line", -> it "does not indent the next line", -> editor.setCursorScreenPosition([2, Infinity]) @@ -174,6 +189,7 @@ describe "Snippets extension", -> describe "when the snippet spans multiple lines", -> it "indents the subsequent lines of the snippet to be even with the start of the first line", -> + expect(editSession.softTabs).toBeTruthy() editor.setCursorScreenPosition([2, Infinity]) editor.insertText ' t3' editor.trigger 'snippets:expand' @@ -204,9 +220,10 @@ describe "Snippets extension", -> describe "when a snippet expansion is undone and redone", -> it "recreates the snippet's tab stops", -> editor.insertText ' t6\n' - editor.setCursorBufferPosition [0, 6] + editor.setCursorBufferPosition [0, Infinity] editor.trigger keydownEvent('tab', target: editor[0]) expect(buffer.lineForRow(0)).toBe " first line" + expect(editor.getCursorBufferPosition()).toEqual [0, 14] editor.undo() editor.redo() expect(editor.getCursorBufferPosition()).toEqual [0, 14] @@ -214,34 +231,68 @@ describe "Snippets extension", -> expect(editor.getSelectedBufferRange()).toEqual [[1, 6], [1, 36]] describe "snippet loading", -> + beforeEach -> + atom.packages = null + jasmine.unspy(LoadSnippetsTask.prototype, 'start') + spyOn(LoadSnippetsTask.prototype, 'loadAtomSnippets').andCallFake -> @snippetsLoaded({}) + spyOn(LoadSnippetsTask.prototype, 'loadTextMateSnippets').andCallFake -> @snippetsLoaded({}) + it "loads non-hidden snippet files from all atom packages with snippets directories, logging a warning if a file can't be parsed", -> - spyOn(console, 'warn').andCallThrough() - jasmine.unspy(AtomPackage.prototype, 'loadSnippets') + jasmine.unspy(LoadSnippetsTask.prototype, 'loadAtomSnippets') + spyOn(console, 'warn') + snippets.loaded = false snippets.loadAll() - expect(syntax.getProperty(['.test'], 'snippets.test')?.constructor).toBe Snippet + waitsFor "all snippets to load", 5000, -> snippets.loaded - # warn about junk-file, but don't even try to parse a hidden file - expect(console.warn).toHaveBeenCalled() - expect(console.warn.calls.length).toBeGreaterThan 0 + runs -> + expect(syntax.getProperty(['.test'], 'snippets.test')?.constructor).toBe Snippet + + # warn about junk-file, but don't even try to parse a hidden file + expect(console.warn).toHaveBeenCalled() + expect(console.warn.calls.length).toBe 1 it "loads snippets from all TextMate packages with snippets", -> - jasmine.unspy(TextMatePackage.prototype, 'loadSnippets') + jasmine.unspy(LoadSnippetsTask.prototype, 'loadTextMateSnippets') + spyOn(console, 'warn') + snippets.loaded = false snippets.loadAll() - snippet = syntax.getProperty(['.source.js'], 'snippets.fun') - expect(snippet.constructor).toBe Snippet - expect(snippet.prefix).toBe 'fun' - expect(snippet.name).toBe 'Function' - expect(snippet.body).toBe """ - function function_name (argument) { - \t// body... - } - """ + waitsFor "all snippets to load", 5000, -> snippets.loaded - describe "Snippets parser", -> + runs -> + snippet = syntax.getProperty(['.source.js'], 'snippets.fun') + expect(snippet.constructor).toBe Snippet + expect(snippet.prefix).toBe 'fun' + expect(snippet.name).toBe 'Function' + expect(snippet.body).toBe """ + function function_name (argument) { + \t// body... + } + """ + + # warn about junk-file, but don't even try to parse a hidden file + expect(console.warn).toHaveBeenCalled() + expect(console.warn.calls.length).toBe 1 + + it "terminates the worker when loading completes", -> + jasmine.unspy(LoadSnippetsTask.prototype, 'loadAtomSnippets') + spyOn(console, "warn") + spyOn(Worker.prototype, 'terminate').andCallThrough() + snippets.loaded = false + snippets.loadAll() + + waitsFor "all snippets to load", 5000, -> snippets.loaded + + runs -> + expect(console.warn).toHaveBeenCalled() + expect(console.warn.argsForCall[0]).toMatch /Error reading snippets file '.*?\/spec\/fixtures\/packages\/package-with-snippets\/snippets\/junk-file'/ + expect(Worker.prototype.terminate).toHaveBeenCalled() + expect(Worker.prototype.terminate.calls.length).toBe 1 + + describe "snippet body parser", -> it "breaks a snippet body into lines, with each line containing tab stops at the appropriate position", -> - bodyTree = snippets.parser.parse """ + bodyTree = snippets.getBodyParser().parse """ the quick brown $1fox ${2:jumped ${3:over} }the ${4:lazy} dog """ @@ -262,3 +313,16 @@ describe "Snippets extension", -> { index: 4, content: ["lazy"] }, " dog" ] + + it "removes interpolated variables in placeholder text (we don't currently support it)", -> + bodyTree = snippets.getBodyParser().parse """ + module ${1:ActiveRecord::${TM_FILENAME/(?:\\A|_)([A-Za-z0-9]+)(?:\\.rb)?/(?2::\\u$1)/g}} + """ + + expect(bodyTree).toEqual [ + "module ", + { + "index": 1, + "content": ["ActiveRecord::", ""] + } + ] diff --git a/src/packages/snippets/src/load-snippets-handler.coffee b/src/packages/snippets/src/load-snippets-handler.coffee new file mode 100644 index 000000000..ba4f16c13 --- /dev/null +++ b/src/packages/snippets/src/load-snippets-handler.coffee @@ -0,0 +1,53 @@ +fs = require 'fs' +TextMatePackage = require 'text-mate-package' +SnippetBodyParser = require './snippet-body-parser' + +module.exports = + snippetsLoaded: (snippets) -> + for snippet in snippets + for selector, snippetsByName of snippet + for name, attributes of snippetsByName + attributes.bodyTree = SnippetBodyParser.parse(attributes.body) + callTaskMethod('snippetsLoaded', snippets) + + loadTextMateSnippets: (path) -> + snippetsDirPath = fs.join(path, 'Snippets') + snippets = [] + + for snippetsPath in fs.list(snippetsDirPath) + logWarning = -> + console.warn "Error reading TextMate snippets file '#{snippetsPath}'" + + continue if fs.base(snippetsPath).indexOf('.') is 0 + try + if object = fs.readPlist(snippetsPath) + snippets.push(object) if object + else + logWarning() + catch e + logWarning() + + @snippetsLoaded(@translateTextmateSnippets(snippets)) + + loadAtomSnippets: (path) -> + snippetsDirPath = fs.join(path, 'snippets') + snippets = [] + for snippetsPath in fs.list(snippetsDirPath) + continue if fs.base(snippetsPath).indexOf('.') is 0 + try + snippets.push(fs.readObject(snippetsPath)) + catch e + console.warn "Error reading snippets file '#{snippetsPath}'" + @snippetsLoaded(snippets) + + translateTextmateSnippets: (tmSnippets) -> + atomSnippets = {} + for { scope, name, content, tabTrigger } in tmSnippets + if scope + scope = TextMatePackage.cssSelectorFromScopeSelector(scope) + else + scope = '*' + + snippetsForScope = (atomSnippets[scope] ?= {}) + snippetsForScope[name] = { prefix: tabTrigger, body: content } + [atomSnippets] diff --git a/src/packages/snippets/src/load-snippets-task.coffee b/src/packages/snippets/src/load-snippets-task.coffee new file mode 100644 index 000000000..e32d70dde --- /dev/null +++ b/src/packages/snippets/src/load-snippets-task.coffee @@ -0,0 +1,34 @@ +Task = require 'task' +TextMatePackage = require 'text-mate-package' + +module.exports = +class LoadSnippetsTask extends Task + constructor: (@snippets) -> + super('snippets/src/load-snippets-handler') + @packages = atom.getPackages() + @packages.push(path: config.configDirPath) + + started: -> + @loadNextPackageSnippets() + + loadNextPackageSnippets: -> + unless @packages.length + @terminate() + @snippets.loaded = true + return + + @packageBeingLoaded = @packages.shift() + if @packageBeingLoaded instanceof TextMatePackage + @loadTextMateSnippets(@packageBeingLoaded.path) + else + @loadAtomSnippets(@packageBeingLoaded.path) + + loadAtomSnippets: (path) -> + @callWorkerMethod('loadAtomSnippets', path) + + loadTextMateSnippets: (path) -> + @callWorkerMethod('loadTextMateSnippets', path) + + snippetsLoaded: (snippets) -> + @snippets.add(snippet) for snippet in snippets + @loadNextPackageSnippets() diff --git a/src/packages/snippets/src/package-extensions.coffee b/src/packages/snippets/src/package-extensions.coffee deleted file mode 100644 index 342d20ecd..000000000 --- a/src/packages/snippets/src/package-extensions.coffee +++ /dev/null @@ -1,25 +0,0 @@ -AtomPackage = require 'atom-package' -TextMatePackage = require 'text-mate-package' -fs = require 'fs' - -AtomPackage.prototype.loadSnippets = -> - snippetsDirPath = fs.join(@path, 'snippets') - snippets.loadDirectory(snippetsDirPath) if fs.exists(snippetsDirPath) - -TextMatePackage.prototype.loadSnippets = -> - snippetsDirPath = fs.join(@path, 'Snippets') - if fs.exists(snippetsDirPath) - tmSnippets = fs.list(snippetsDirPath).map (snippetPath) -> fs.readPlist(snippetPath) - snippets.add(@translateSnippets(tmSnippets)) - -TextMatePackage.prototype.translateSnippets = (tmSnippets) -> - atomSnippets = {} - for { scope, name, content, tabTrigger } in tmSnippets - if scope - scope = TextMatePackage.cssSelectorFromScopeSelector(scope) - else - scope = '*' - - snippetsForScope = (atomSnippets[scope] ?= {}) - snippetsForScope[name] = { prefix: tabTrigger, body: content } - atomSnippets diff --git a/src/packages/snippets/src/snippet-body-parser.coffee b/src/packages/snippets/src/snippet-body-parser.coffee new file mode 100644 index 000000000..0239fee3f --- /dev/null +++ b/src/packages/snippets/src/snippet-body-parser.coffee @@ -0,0 +1,4 @@ +PEG = require 'pegjs' +fs = require 'fs' +grammarSrc = fs.read(require.resolve('./snippet-body.pegjs')) +module.exports = PEG.buildParser(grammarSrc, trackLineAndColumn: true) diff --git a/src/packages/snippets/snippets.pegjs b/src/packages/snippets/src/snippet-body.pegjs similarity index 52% rename from src/packages/snippets/snippets.pegjs rename to src/packages/snippets/src/snippet-body.pegjs index 72ec9be48..ce4f992fb 100644 --- a/src/packages/snippets/snippets.pegjs +++ b/src/packages/snippets/src/snippet-body.pegjs @@ -2,10 +2,6 @@ bodyContent = content:(tabStop / bodyContentText)* { return content; } bodyContentText = text:bodyContentChar+ { return text.join(''); } bodyContentChar = !tabStop char:. { return char; } -placeholderContent = content:(tabStop / placeholderContentText)* { return content; } -placeholderContentText = text:placeholderContentChar+ { return text.join(''); } -placeholderContentChar = !tabStop char:[^}] { return char; } - tabStop = simpleTabStop / tabStopWithPlaceholder simpleTabStop = '$' index:[0-9]+ { return { index: parseInt(index), content: [] }; @@ -13,3 +9,13 @@ simpleTabStop = '$' index:[0-9]+ { tabStopWithPlaceholder = '${' index:[0-9]+ ':' content:placeholderContent '}' { return { index: parseInt(index), content: content }; } +placeholderContent = content:(tabStop / variable / placeholderContentText)* { return content; } +placeholderContentText = text:placeholderContentChar+ { return text.join(''); } +placeholderContentChar = !tabStop !variable char:[^}] { return char; } + +variable = '${' variableContent '}' { + return ''; // we eat variables and do nothing with them for now +} +variableContent = content:(variable / variableContentText)* { return content; } +variableContentText = text:variableContentChar+ { return text.join(''); } +variableContentChar = !variable char:[^}] { return char; } diff --git a/src/packages/snippets/src/snippet-expansion.coffee b/src/packages/snippets/src/snippet-expansion.coffee index b1a62e496..9c2187af9 100644 --- a/src/packages/snippets/src/snippet-expansion.coffee +++ b/src/packages/snippets/src/snippet-expansion.coffee @@ -11,7 +11,7 @@ class SnippetExpansion @editSession.selectToBeginningOfWord() startPosition = @editSession.getCursorBufferPosition() @editSession.transact => - @editSession.insertText(snippet.body, autoIndent: false) + [newRange] = @editSession.insertText(snippet.body, autoIndent: false) if snippet.tabStops.length > 0 editSession.pushOperation do: => @@ -19,6 +19,7 @@ class SnippetExpansion @placeTabStopAnchorRanges(startPosition, snippet.tabStops) @editSession.snippetExpansion = this undo: => @destroy() + @editSession.normalizeTabsInBufferRange(newRange) @indentSubsequentLines(startPosition.row, snippet) if snippet.lineCount > 1 cursorMoved: ({oldBufferPosition, newBufferPosition}) -> @@ -37,7 +38,6 @@ class SnippetExpansion anchorRange @setTabStopIndex(0) - indentSubsequentLines: (startRow, snippet) -> initialIndent = @editSession.lineForBufferRow(startRow).match(/^\s*/)[0] for row in [startRow + 1...startRow + snippet.lineCount] diff --git a/src/packages/snippets/src/snippet.coffee b/src/packages/snippets/src/snippet.coffee index ef7c3d0aa..6e0069862 100644 --- a/src/packages/snippets/src/snippet.coffee +++ b/src/packages/snippets/src/snippet.coffee @@ -30,7 +30,7 @@ class Snippet bodyText.push(segment) segmentLines = segment.split('\n') column += segmentLines.shift().length - while nextLine = segmentLines.shift() + while (nextLine = segmentLines.shift())? row += 1 column = nextLine.length diff --git a/src/packages/symbols-view/spec/symbols-view-spec.coffee b/src/packages/symbols-view/spec/symbols-view-spec.coffee index 89476c8d5..3e0724795 100644 --- a/src/packages/symbols-view/spec/symbols-view-spec.coffee +++ b/src/packages/symbols-view/spec/symbols-view-spec.coffee @@ -30,9 +30,9 @@ describe "SymbolsView", -> expect(symbolsView.find('.loading')).toBeEmpty() expect(rootView.find('.symbols-view')).toExist() expect(symbolsView.list.children('li').length).toBe 2 - expect(symbolsView.list.children('li:first').find('.function-name')).toHaveText 'quicksort' + expect(symbolsView.list.children('li:first').find('.label')).toHaveText 'quicksort' expect(symbolsView.list.children('li:first').find('.function-details')).toHaveText 'Line 1' - expect(symbolsView.list.children('li:last').find('.function-name')).toHaveText 'quicksort.sort' + expect(symbolsView.list.children('li:last').find('.label')).toHaveText 'quicksort.sort' expect(symbolsView.list.children('li:last').find('.function-details')).toHaveText 'Line 2' expect(symbolsView).not.toHaveClass "error" expect(symbolsView.error).not.toBeVisible() @@ -165,7 +165,7 @@ describe "SymbolsView", -> editor.setCursorBufferPosition([8,14]) editor.trigger 'symbols-view:go-to-declaration' expect(symbolsView.list.children('li').length).toBe 1 - expect(symbolsView.list.children('li:first').find('.function-name')).toHaveText 'tagged.js' + expect(symbolsView.list.children('li:first').find('.label')).toHaveText 'tagged.js' describe "project symbols", -> it "displays all tags", -> @@ -181,9 +181,9 @@ describe "SymbolsView", -> expect(symbolsView.find('.loading')).toBeEmpty() expect(rootView.find('.symbols-view')).toExist() expect(symbolsView.list.children('li').length).toBe 4 - expect(symbolsView.list.children('li:first').find('.function-name')).toHaveText 'callMeMaybe' + expect(symbolsView.list.children('li:first').find('.label')).toHaveText 'callMeMaybe' expect(symbolsView.list.children('li:first').find('.function-details')).toHaveText 'tagged.js' - expect(symbolsView.list.children('li:last').find('.function-name')).toHaveText 'thisIsCrazy' + expect(symbolsView.list.children('li:last').find('.label')).toHaveText 'thisIsCrazy' expect(symbolsView.list.children('li:last').find('.function-details')).toHaveText 'tagged.js' expect(symbolsView).not.toHaveClass "error" expect(symbolsView.error).not.toBeVisible() diff --git a/src/packages/symbols-view/src/symbols-view.coffee b/src/packages/symbols-view/src/symbols-view.coffee index c0d019132..acf72cb78 100644 --- a/src/packages/symbols-view/src/symbols-view.coffee +++ b/src/packages/symbols-view/src/symbols-view.coffee @@ -12,7 +12,7 @@ class SymbolsView extends SelectList @activate: (rootView) -> @instance = new SymbolsView(rootView) - @viewClass: -> "#{super} symbols-view" + @viewClass: -> "#{super} symbols-view overlay from-top" filterKey: 'name' @@ -22,14 +22,13 @@ class SymbolsView extends SelectList itemForElement: ({position, name, file}) -> $$ -> @li => - @div name, class: 'function-name' + @div name, class: 'label' @div class: 'right', => if position text = "Line #{position.row + 1}" else text = fs.base(file) @div text, class: 'function-details' - @div class: 'clear-float' toggleFileSymbols: -> if @hasParent() diff --git a/src/packages/symbols-view/src/tag-generator.coffee b/src/packages/symbols-view/src/tag-generator.coffee index bcc958bb4..7517d4dea 100644 --- a/src/packages/symbols-view/src/tag-generator.coffee +++ b/src/packages/symbols-view/src/tag-generator.coffee @@ -6,40 +6,13 @@ class TagGenerator constructor: (@path, @callback) -> - parsePrefix: (section = "") -> - if section.indexOf('class:') is 0 - section.substring(6) - else if section.indexOf('namespace:') is 0 - section.substring(10) - else if section.indexOf('file:') is 0 - section.substring(5) - else if section.indexOf('signature:') is 0 - section.substring(10) - else if section.indexOf('struct:') is 0 - section.substring(7) - else - section - parseTagLine: (line) -> sections = line.split('\t') - return null if sections.length < 4 - - label = sections[0] - line = parseInt(sections[2]) - 1 - if sections.length > 5 - if prefix = @parsePrefix(sections[4]) - label = "#{prefix}::#{label}" - if suffix = @parsePrefix(sections[5]) - label = "#{label}#{suffix}" + if sections.length > 3 + position: new Point(parseInt(sections[2]) - 1) + name: sections[0] else - if suffix = @parsePrefix(sections[4]) - label = "#{label}#{suffix}" - - tag = - position: new Point(line, 0) - name: label - - return tag + null generate: -> options = diff --git a/src/packages/tabs/spec/tabs-spec.coffee b/src/packages/tabs/spec/tabs-spec.coffee index c3055d081..e5b821204 100644 --- a/src/packages/tabs/spec/tabs-spec.coffee +++ b/src/packages/tabs/spec/tabs-spec.coffee @@ -5,7 +5,7 @@ Tabs = require 'tabs' fs = require 'fs' describe "Tabs", -> - [rootView, editor, statusBar, buffer, tabs] = [] + [rootView, editor, buffer, tabs] = [] beforeEach -> rootView = new RootView(require.resolve('fixtures/sample.js')) @@ -38,6 +38,10 @@ describe "Tabs", -> expect(editor.getActiveEditSessionIndex()).toBe 1 expect(tabs.find('.tab:eq(1)')).toHaveClass 'active' + 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 "when the active edit session changes", -> it "highlights the tab for the newly-active edit session", -> editor.setActiveEditSessionIndex(0) @@ -60,6 +64,11 @@ describe "Tabs", -> expect(tabs.find('.tab').length).toBe 3 expect(tabs.find('.tab:eq(2) .file-name').text()).toBe 'untitled' + 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 an edit session is removed", -> it "removes the tab for the removed edit session", -> editor.setActiveEditSessionIndex(0) @@ -118,3 +127,21 @@ describe "Tabs", -> 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] = [] + + beforeEach -> + tempPath = '/tmp/sample.js' + fs.write(tempPath, 'sample') + + afterEach -> + fs.remove(tempPath) if fs.exists(tempPath) + + 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' diff --git a/src/packages/tabs/src/tab.coffee b/src/packages/tabs/src/tab.coffee index 6a26a114f..6a8ba4338 100644 --- a/src/packages/tabs/src/tab.coffee +++ b/src/packages/tabs/src/tab.coffee @@ -1,4 +1,5 @@ {View} = require 'space-pen' +fs = require 'fs' module.exports = class Tab extends View @@ -7,12 +8,14 @@ class Tab extends View @span class: 'file-name', outlet: 'fileName' @span class: 'close-icon' - initialize: (@editSession) -> + initialize: (@editSession, @editor) -> @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() @@ -25,4 +28,14 @@ class Tab extends View @isModified = false updateFileName: -> - @fileName.text(@editSession.buffer.getBaseName() ? 'untitled') + 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/src/tabs-view.coffee b/src/packages/tabs/src/tabs-view.coffee index e7322dc64..d2b21fa7c 100644 --- a/src/packages/tabs/src/tabs-view.coffee +++ b/src/packages/tabs/src/tabs-view.coffee @@ -34,7 +34,7 @@ class Tabs extends View false addTabForEditSession: (editSession) -> - @append(new Tab(editSession)) + @append(new Tab(editSession, @editor)) setActiveTab: (index) -> @find(".tab.active").removeClass('active') diff --git a/src/packages/tabs/stylesheets/tabs.css b/src/packages/tabs/stylesheets/tabs.css index 0239bc734..ebdfad7f2 100644 --- a/src/packages/tabs/stylesheets/tabs.css +++ b/src/packages/tabs/stylesheets/tabs.css @@ -1,14 +1,20 @@ .tabs { -webkit-user-select: none; + display: -webkit-box; + -webkit-box-align: center; } .tab { - display: table-cell; + -webkit-box-flex: 2; position: relative; - width:175px; + width: 175px; + max-width: 175px; min-width: 40px; box-sizing: border-box; - height: 24px; +} + +.tab.active { + -webkit-box-flex: 1; } .tab.file-modified .close-icon { @@ -23,9 +29,5 @@ overflow: hidden; white-space: nowrap; text-overflow: ellipsis; - position: absolute; - left: 9px; - top:4px; - bottom:4px; - right: 21px; + padding: 3px 5px; } \ No newline at end of file diff --git a/src/packages/tree-view/spec/tree-view-spec.coffee b/src/packages/tree-view/spec/tree-view-spec.coffee index c788842bf..ab6d16d1a 100644 --- a/src/packages/tree-view/spec/tree-view-spec.coffee +++ b/src/packages/tree-view/spec/tree-view-spec.coffee @@ -770,6 +770,9 @@ describe "TreeView", -> treeView.trigger "tree-view:move" moveDialog = rootView.find(".tree-view-dialog").view() + afterEach -> + waits 50 # The move specs cause too many false positives because of their async nature, so wait a little bit before we cleanup + it "opens a move dialog with the file's current path (excluding extension) populated", -> extension = fs.extension(filePath) fileNameWithoutExtension = fs.base(filePath, extension) diff --git a/src/packages/tree-view/src/file-view.coffee b/src/packages/tree-view/src/file-view.coffee index ea75356da..142bc5eea 100644 --- a/src/packages/tree-view/src/file-view.coffee +++ b/src/packages/tree-view/src/file-view.coffee @@ -17,7 +17,9 @@ class FileView extends View @subscribe $(window), 'focus', => @updateStatus() extension = fs.extension(@getPath()) - if fs.isCompressedExtension(extension) + if fs.isReadme(@getPath()) + @fileName.addClass('readme-icon') + else if fs.isCompressedExtension(extension) @fileName.addClass('compressed-icon') else if fs.isImageExtension(extension) @fileName.addClass('image-icon') diff --git a/src/stdlib/cson.coffee b/src/stdlib/cson.coffee new file mode 100644 index 000000000..63158f006 --- /dev/null +++ b/src/stdlib/cson.coffee @@ -0,0 +1,79 @@ +_ = require 'underscore' + +module.exports = + stringifyIndent: (level=0) -> _.multiplyString(' ', Math.max(level, 0)) + + stringifyString: (string) -> + string = JSON.stringify(string) + string = string[1...-1] # Remove surrounding double quotes + string = string.replace(/\\"/g, '"') # Unescape escaped double quotes + string = string.replace(/'/g, '\\\'') # Escape single quotes + "'#{string}'" # Wrap in single quotes + + stringifyBoolean: (boolean) -> "#{boolean}" + + stringifyNumber: (number) -> "#{number}" + + stringifyNull: -> 'null' + + stringifyArray: (array, indentLevel=0) -> + return '[]' if array.length is 0 + + cson = '[\n' + for value in array + cson += @stringifyIndent(indentLevel + 2) + if _.isString(value) + cson += @stringifyString(value) + else if _.isBoolean(value) + cson += @stringifyBoolean(value) + else if _.isNumber(value) + cson += @stringifyNumber(value) + else if _.isNull(value) or value is undefined + cson += @stringifyNull(value) + else if _.isArray(value) + cson += @stringifyArray(value, indentLevel + 2) + else if _.isObject(value) + cson += @stringifyObject(value, indentLevel + 2) + else + throw new Error("Unrecognized type for array value: #{value}") + cson += '\n' + "#{cson}#{@stringifyIndent(indentLevel)}]" + + stringifyObject: (object, indentLevel=0) -> + cson = '' + prefix = '' + for key, value of object + continue if value is undefined + if _.isFunction(value) + throw new Error("Function specified as value to key: #{key}") + + cson += "#{prefix}#{@stringifyIndent(indentLevel)}'#{key}':" + if _.isString(value) + cson += " #{@stringifyString(value)}" + else if _.isBoolean(value) + cson += " #{@stringifyBoolean(value)}" + else if _.isNumber(value) + cson += " #{@stringifyNumber(value)}" + else if _.isNull(value) + cson += " #{@stringifyNull(value)}" + else if _.isArray(value) + cson += " #{@stringifyArray(value, indentLevel)}" + else if _.isObject(value) + cson += "\n#{@stringifyObject(value, indentLevel + 2)}" + else + throw new Error("Unrecognized value type for key: #{key} with value: #{value}") + prefix = '\n' + cson + + stringify: (object) -> + throw new Error("Cannot stringify undefined object") if object is undefined + throw new Error("Cannot stringify function: #{object}") if _.isFunction(object) + + return @stringifyString(object) if _.isString(object) + return @stringifyBoolean(object) if _.isBoolean(object) + return @stringifyNumber(object) if _.isNumber(object) + return @stringifyNull(object) if _.isNull(object) + return @stringifyArray(object) if _.isArray(object) + return @stringifyObject(object) if _.isObject(object) + + throw new Error("Unrecognized type to stringify: #{object}") diff --git a/src/stdlib/fs.coffee b/src/stdlib/fs.coffee index 39ae82334..35e9c5510 100644 --- a/src/stdlib/fs.coffee +++ b/src/stdlib/fs.coffee @@ -2,7 +2,6 @@ # http://ringojs.org/api/v0.8/fs/ _ = require 'underscore' -$ = require 'jquery' module.exports = # Make the given path absolute by resolving it against the @@ -60,11 +59,16 @@ module.exports = # Returns an array with all the names of files contained # in the directory path. - list: (rootPath) -> + list: (rootPath, extensions) -> paths = [] - onPath = (path) => - paths.push(@join(rootPath, path)) - false + if extensions + onPath = (path) => + paths.push(@join(rootPath, path)) if _.contains(extensions, @extension(path)) + false + else + onPath = (path) => + paths.push(@join(rootPath, path)) + false @traverseTree(rootPath, onPath, onPath) paths @@ -148,35 +152,39 @@ module.exports = undefined isCompressedExtension: (ext) -> - _.contains([ + _.indexOf([ '.gz' '.jar' '.tar' '.zip' - ], ext) + ], ext, true) >= 0 isImageExtension: (ext) -> - _.contains([ + _.indexOf([ '.gif' '.jpeg' '.jpg' '.png' '.tiff' - ], ext) + ], ext, true) >= 0 isPdfExtension: (ext) -> - _.contains([ - '.pdf' - ], ext) + ext is '.pdf' isMarkdownExtension: (ext) -> - _.contains([ + _.indexOf([ '.markdown' '.md' '.mkd' '.mkdown' '.ron' - ], ext) + ], ext, true) >= 0 + + + isReadme: (path) -> + extension = @extension(path) + base = @base(path, extension).toLowerCase() + base is 'readme' and (extension is '' or @isMarkdownExtension(extension)) readObject: (path) -> contents = @read(path) @@ -186,6 +194,14 @@ module.exports = else JSON.parse(contents) + writeObject: (path, object) -> + if @extension(path) is '.cson' + CSON = require 'cson' + content = CSON.stringify(object) + else + content = JSON.stringify(object, undefined, 2) + @write(path, "#{content}\n") + readPlist: (path) -> plist = require 'plist' object = null diff --git a/src/stdlib/git-repository.coffee b/src/stdlib/git-repository.coffee new file mode 100644 index 000000000..fc03e1368 --- /dev/null +++ b/src/stdlib/git-repository.coffee @@ -0,0 +1,18 @@ +module.exports = +class GitRepository + constructor: (path) -> + unless repo = $git.getRepository(path) + throw new Error("No Git repository found searching path: " + path) + repo.constructor = GitRepository + repo.__proto__ = GitRepository.prototype + return repo + + getHead: $git.getHead + getPath: $git.getPath + getStatus: $git.getStatus + isIgnored: $git.isIgnored + checkoutHead: $git.checkoutHead + getDiffStats: $git.getDiffStats + isSubmodule: $git.isSubmodule + refreshIndex: $git.refreshIndex + destroy: $git.destroy diff --git a/src/stdlib/jquery-extensions.coffee b/src/stdlib/jquery-extensions.coffee index 365c2b5a8..089363577 100644 --- a/src/stdlib/jquery-extensions.coffee +++ b/src/stdlib/jquery-extensions.coffee @@ -25,6 +25,9 @@ $.fn.pageUp = -> $.fn.pageDown = -> @scrollTop(@scrollTop() + @height()) +$.fn.isOnDom = -> + @closest(document.body).length is 1 + $.fn.containsElement = (element) -> (element[0].compareDocumentPosition(this[0]) & 8) == 8 diff --git a/src/stdlib/onig-reg-exp.coffee b/src/stdlib/onig-reg-exp.coffee new file mode 100644 index 000000000..93883da20 --- /dev/null +++ b/src/stdlib/onig-reg-exp.coffee @@ -0,0 +1,11 @@ +module.exports = +class OnigRegExp + constructor: (source) -> + regexp = $onigRegExp.buildOnigRegExp(source); + regexp.constructor = OnigRegExp + regexp.__proto__ = OnigRegExp.prototype + regexp.source = source + return regexp + + search: $onigRegExp.search + test: $onigRegExp.test diff --git a/src/stdlib/onig-scanner.coffee b/src/stdlib/onig-scanner.coffee new file mode 100644 index 000000000..fb26939ec --- /dev/null +++ b/src/stdlib/onig-scanner.coffee @@ -0,0 +1,10 @@ +module.exports = +class OnigScanner + constructor: (sources) -> + scanner = $onigScanner.buildScanner(sources) + scanner.constructor = OnigScanner + scanner.__proto__ = OnigScanner.prototype + scanner.sources = sources + return scanner + + findNextMatch: $onigScanner.findNextMatch diff --git a/src/stdlib/require.coffee b/src/stdlib/require.coffee index 1076bcc49..4a6f919aa 100644 --- a/src/stdlib/require.coffee +++ b/src/stdlib/require.coffee @@ -25,17 +25,16 @@ require = (path, cb) -> unless file = resolve(path) throw new Error("Require can't find file at path '#{path}'") - parts = file.split '.' - ext = parts[parts.length-1] + ext = file.split('.').pop() if __moduleExists file if not __modules.loaded[file.toLowerCase()]? - console.warn "Circular require: #{__filename} required #{file}" + console.warn "Circular require: #{window.__filename} required #{file}" return __modules[file] else if __modules.loaded[file.toLowerCase()] console.warn "Multiple requires (different cases) for #{file}" - [ previousFilename, window.__filename ] = [ __filename, file ] + [ previousFilename, window.__filename ] = [ window.__filename, file ] __modules[file] = {} # Fix for circular references __modules[file] = (exts[ext] or (file) -> __read file) file window.__filename = previousFilename @@ -43,33 +42,67 @@ require = (path, cb) -> define = (cb) -> __defines.push -> - exports = __modules[__filename] or {} + exports = __modules[window.__filename] or {} module = exports: exports cb.call exports, require, exports, module - __modules.loaded[__filename.toLowerCase()] = true + __modules.loaded[window.__filename.toLowerCase()] = true module.exports or exports exts = js: (file, code) -> code or= __read file - eval("define(function(require, exports, module) { 'use strict';" + code + "})\n//@ sourceURL=" + file) + eval("define(function(require, exports, module) { 'use strict';#{code}})\n//@ sourceURL=#{file}") __defines.pop()?.call() - coffee: (file) -> - exts.js(file, __coffeeCache(file)) + coffee: (file, retry=true) -> + cacheFilePath = getCacheFilePath(file) + if __exists(cacheFilePath) + compiled = __read(cacheFilePath) + writeToCache = false + else + {CoffeeScript} = require 'coffee-script' + compiled = CoffeeScript.compile(__read(file), filename: file) + writeToCache = true + + try + evaluated = exts.js(file, compiled) + $native.write(cacheFilePath, compiled) if writeToCache + evaluated + catch e + if retry + # Attempt a second compile to work around mysterious CEF/CoffeeScript + # timing issue where the CoffeeScript compiler generates invalid + # JavaScript such as [object Object]. + console.warn "Error evaluating #{file}. Trying again...", e.stack + exts.coffee(file, false) + else + throw e + +getPath = (path) -> + path = resolve(path) + return path unless path.split('.').pop() is 'coffee' + + cacheFilePath = getCacheFilePath(path) + unless __exists(cacheFilePath) + {CoffeeScript} = require 'coffee-script' + compiled = CoffeeScript.compile(__read(path), filename: path) + $native.write(cacheFilePath, compiled) + cacheFilePath + +getCacheFilePath = (path) -> + "/tmp/atom-compiled-scripts/#{$native.md5ForPath(path)}" resolve = (name, {verifyExistence}={}) -> verifyExistence ?= true file = name if /!/.test file - parts = file.split '!' - file = parts[parts.length-1] + file = file.split('!').pop() if file[0..1] is './' - prefix = __filename.split('/')[0..-2].join '/' + prefix = window.__filename.split('/')[0..-2].join '/' file = file.replace './', "#{prefix}/" if file[0..2] is '../' - prefix = __filename.split('/')[0..-3].join '/' + prefix = window.__filename.split('/')[0..-3].join '/' file = file.replace '../', "#{prefix}/" if file[0] isnt '/' @@ -130,18 +163,6 @@ __exists = (path) -> __isFile = (path) -> $native.isFile path -__coffeeCache = (filePath) -> - {CoffeeScript} = require 'coffee-script' - tmpPath = "/tmp/atom-compiled-scripts" - cacheFilePath = [tmpPath, $native.md5ForPath(filePath)].join("/") - - if __exists(cacheFilePath) - __read(cacheFilePath) - else - compiled = CoffeeScript.compile(__read(filePath), filename: filePath) - $native.write(cacheFilePath, compiled) - compiled - __read = (path) -> try $native.read(path) @@ -157,6 +178,7 @@ this.nakedLoad = nakedLoad this.define = define this.require.paths = paths +this.require.getPath = getPath this.require.exts = exts this.require.resolve = resolve diff --git a/src/stdlib/task-shell.coffee b/src/stdlib/task-shell.coffee new file mode 100644 index 000000000..458235e56 --- /dev/null +++ b/src/stdlib/task-shell.coffee @@ -0,0 +1,45 @@ +# This file is loaded within Task's worker thread. It will attempt to invoke +# any message with a 'method' and 'args' key on the global `handler` object. The +# initial `handler` object contains the `start` method, which is called by the +# task itself to relay information from the window thread and bootstrap the +# worker's environment. The `start` method then replaces the handler with an +# object required from the given `handlerPath`. + +self.window = {} +self.attachEvent = -> +self.console = + warn: -> callTaskMethod 'warn', arguments... + log: -> callTaskMethod 'log', arguments... + error: -> callTaskMethod 'error', arguments... + +window.document = + createElement: -> + setAttribute: -> + getElementsByTagName: -> [] + appendChild: -> + documentElement: + insertBefore: -> + removeChild: -> + getElementById: -> {} + createComment: -> {} + createDocumentFragment: -> {} +self.document = window.document + +# `callTaskMethod` can be used to invoke method's on the parent `Task` object +# back in the window thread. +self.callTaskMethod = (method, args...) -> + postMessage(method: method, args: args) + +# The worker's initial handler replaces itself when `start` is invoked +self.handler = + start: ({resourcePath, globals, requirePath, handlerPath}) -> + for key, value of globals + self[key] = value + window[key] = value + importScripts(requirePath) + require 'config' + self.handler = require(handlerPath) + callTaskMethod 'started' + +self.addEventListener 'message', ({data}) -> + handler[data.method]?(data.args...) if data.method diff --git a/src/stdlib/task.coffee b/src/stdlib/task.coffee new file mode 100644 index 000000000..4a66a7042 --- /dev/null +++ b/src/stdlib/task.coffee @@ -0,0 +1,38 @@ +module.exports = +class Task + constructor: (@path) -> + + start: -> + @worker = new Worker(require.getPath('task-shell')) + @worker.onmessage = ({data}) => + if data.method and this[data.method] + this[data.method](data.args...) + else + @onMessage(data) + @startWorker() + + log: -> console.log(arguments...) + warn: -> console.warn(arguments...) + error: -> console.error(arguments...) + + startWorker: -> + @callWorkerMethod 'start' + globals: + resourcePath: window.resourcePath + navigator: + userAgent: navigator.userAgent + requirePath: require.getPath('require') + handlerPath: @path + + started: -> + + onMessage: (message) -> + + callWorkerMethod: (method, args...) -> + @postMessage({method, args}) + + postMessage: (data) -> + @worker.postMessage(data) + + terminate: -> + @worker.terminate() diff --git a/src/window-bootstrap.coffee b/src/window-bootstrap.coffee index fa815e832..cca57c7d7 100644 --- a/src/window-bootstrap.coffee +++ b/src/window-bootstrap.coffee @@ -1,7 +1,9 @@ # Like sands through the hourglass, so are the days of our lives. +date = new Date().getTime() require 'atom' require 'window' pathToOpen = atom.getWindowState('pathToOpen') ? window.location.params.pathToOpen window.attachRootView(pathToOpen) atom.show() +console.log "Load time: #{new Date().getTime() - date}" diff --git a/static/atom.css b/static/atom.css index 4f70a0574..382352314 100644 --- a/static/atom.css +++ b/static/atom.css @@ -53,4 +53,11 @@ html, body { 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; } \ No newline at end of file diff --git a/themes/Atom - Light/command-panel.css b/static/command-panel.css similarity index 76% rename from themes/Atom - Light/command-panel.css rename to static/command-panel.css index 4bfff1d22..5f10228c4 100644 --- a/themes/Atom - Light/command-panel.css +++ b/static/command-panel.css @@ -1,35 +1,23 @@ .command-panel { - background-color: #f4f4f4; - border-top: 1px solid #979797; - color: #ededed; - padding: 10px; + padding: 5px; } .command-panel .preview-list { max-height: 300px; overflow: auto; - margin-bottom: 10px; + margin: 0 1px 5px 10px; position: relative; - background-color: #e7e7e7; - color: #222; cursor: default; - border: 1px solid #989898; } .command-panel .preview-count { font-size: 11px; - color: #333; text-align: right; padding-bottom: 1px; } -.command-panel .preview-list li.selected, .command-panel .preview-list li.operation:hover { - background-color: rgba(255, 255, 255, .6); -} - .command-panel .preview-list .path { padding-left: 5px; - color: #3D5075; } .command-panel .preview-list .path:before { @@ -44,6 +32,10 @@ top: 1px; } +.command-panel .preview-list .path.readme:before { + content: "\f007"; +} + .command-panel .preview-list .operation { padding-top: 2px; padding-bottom: 2px; @@ -51,7 +43,6 @@ .command-panel .preview-list .line-number { padding-left: 3px; - color: #3D5075; margin-right: 1ex; text-align: right; display: inline-block; @@ -59,7 +50,6 @@ .command-panel .preview-list .path-match-number { padding-left: 8px; - color: #3D5075; } .command-panel .preview-list .preview { @@ -67,21 +57,16 @@ } .command-panel .preview-list .preview .match { - background-color: #c8d4d7; -webkit-border-radius: 2px; padding: 1px; } -.command-panel .prompt-and-editor { - display: -webkit-box; -} - .command-panel .prompt-and-editor .prompt:before { color: #969696; content: '\f078'; font-family: 'Octicons Regular'; position: relative; - top: -4px; + top: 0; left: -5px; -webkit-font-smoothing: antialiased; } @@ -92,6 +77,10 @@ margin-right: -4px; } +.command-panel .prompt-and-editor { + display: -webkit-box; +} + .error-messages { padding: 5px 1em; color: white; diff --git a/static/editor.css b/static/editor.css index bdd9f2825..83ee6efb2 100644 --- a/static/editor.css +++ b/static/editor.css @@ -6,10 +6,23 @@ -webkit-box-flex: 1; position: relative; z-index: 0; + font-family: Inconsolata, Monaco, Courier; + line-height: 1.3; } .editor.mini { height: auto; + line-height: 25px; +} + +.editor.mini .cursor { + width: 2px; + line-height: 20px; + margin-top: 2px; +} + +.editor .gutter .line-number.cursor-line { + opacity: 1; } .editor .gutter { @@ -19,14 +32,58 @@ text-align: right; } +.editor .gutter .line-number { + padding-right: .5em; + min-width: 35px; + box-sizing: border-box; + text-align: right; + opacity: 0.6; +} + .editor .gutter .line-numbers { position: relative; } +.editor .gutter .line-number.fold.cursor-line { + opacity: 1; +} + +.editor .gutter .line-number.fold:after { + visibility: visible; +} + .editor.mini .gutter { display: none; } +.editor .gutter .line-number:after { + font-size: 0.8em; + content: '\f078'; + font-family: 'Octicons Regular'; + -webkit-font-smoothing: antialiased; + color: #fba0e3; + visibility: hidden; +} + +.editor .fold-marker:after { + content: '\2026'; + opacity: .8; + color: #fba0e3; + padding-left: .2em; +} + +.editor .line.cursor-line .fold-marker { + opacity: 1; +} + +.editor.is-blurred .line.cursor-line { + background: rgba(0, 0, 0, 0); +} + +.editor .invisible { + opacity: 0.2; +} + .editor .vertical-scrollbar { position: absolute; right: 0; @@ -56,7 +113,6 @@ overflow-x: hidden; } - .editor .underlayer, .editor .lines, .editor .overlayer { width: 100%; height: 100%; @@ -83,14 +139,18 @@ .editor .cursor { position: absolute; - border-left: 2px solid; + border-left: 1px solid; } -.editor:not(.focused) .cursor, -.editor.focused .cursor.blink-off { +.editor .cursor, +.editor.is-focused .cursor.blink-off { visibility: hidden; } +.editor.is-focused .cursor { + visibility: visible; +} + .editor .hidden-input { position: absolute; z-index: -1; @@ -103,4 +163,4 @@ position: absolute; pointer-events: none; z-index: -1; -} \ No newline at end of file +} diff --git a/themes/Atom - Light/fuzzy-finder.css b/static/fuzzy-finder.css similarity index 72% rename from themes/Atom - Light/fuzzy-finder.css rename to static/fuzzy-finder.css index 0cc921e02..f480ab0d3 100644 --- a/themes/Atom - Light/fuzzy-finder.css +++ b/static/fuzzy-finder.css @@ -1,17 +1,8 @@ -.fuzzy-finder ol { - overflow: hidden; - -webkit-user-select: none; - cursor: default; -} - -.fuzzy-finder ol:empty { - margin-bottom: 0; - border: none; -} - .fuzzy-finder .directory { - color: #b2b2b2; - padding-left: .5em; + color: rgba(0, 0, 0, 0.5); + word-break: break-word; + margin-left: 5px; + line-height: 150%; } .fuzzy-finder .file:before { @@ -39,4 +30,8 @@ .fuzzy-finder .file.pdf-name:before { content: "\f014"; -} \ No newline at end of file +} + +.fuzzy-finder .file.readme-name:before { + content: "\f007"; +} diff --git a/static/grammar-view.css b/static/grammar-view.css deleted file mode 100644 index 1cbeeba30..000000000 --- a/static/grammar-view.css +++ /dev/null @@ -1,8 +0,0 @@ -.grammar-view { - width: 50%; - margin-left: -25%; -} - -.grammar-view ol { - max-height: 300px; -} \ No newline at end of file diff --git a/static/jasmine.css b/static/jasmine.css index 6bf9eef9c..a1000e0fc 100644 --- a/static/jasmine.css +++ b/static/jasmine.css @@ -1,170 +1,80 @@ -.jasmine_reporter { - font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; - font-size: 16px; -} +body { background-color: #eeeeee; padding: 0; overflow-y: scroll; } -body { - background: #eee; -} +#HTMLReporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; } +#HTMLReporter a { text-decoration: none; } +#HTMLReporter a:hover { text-decoration: underline; } +#HTMLReporter p, #HTMLReporter h1, #HTMLReporter h2, #HTMLReporter h3, #HTMLReporter h4, #HTMLReporter h5, #HTMLReporter h6 { margin: 0; line-height: 14px; } +#HTMLReporter .banner, #HTMLReporter .symbolSummary, #HTMLReporter .summary, #HTMLReporter .resultMessage, #HTMLReporter .specDetail .description, #HTMLReporter .alert .bar, #HTMLReporter .stackTrace { padding-left: 9px; padding-right: 9px; } +#HTMLReporter #jasmine_content { position: fixed; right: 100%; } +#HTMLReporter .version { color: #aaaaaa; } +#HTMLReporter .banner { margin-top: 14px; } +#HTMLReporter .duration { color: #aaaaaa; float: right; } +#HTMLReporter .symbolSummary { overflow: hidden; *zoom: 1; margin: 14px 0; } +#HTMLReporter .symbolSummary li { display: block; float: left; height: 7px; width: 14px; margin-bottom: 7px; font-size: 16px; } +#HTMLReporter .symbolSummary li.passed { font-size: 14px; } +#HTMLReporter .symbolSummary li.passed:before { color: #5e7d00; content: "\02022"; } +#HTMLReporter .symbolSummary li.failed { line-height: 9px; } +#HTMLReporter .symbolSummary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; } +#HTMLReporter .symbolSummary li.skipped { font-size: 14px; } +#HTMLReporter .symbolSummary li.skipped:before { color: #bababa; content: "\02022"; } +#HTMLReporter .symbolSummary li.pending { line-height: 11px; } +#HTMLReporter .symbolSummary li.pending:before { color: #aaaaaa; content: "-"; } +#HTMLReporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; } +#HTMLReporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; } +#HTMLReporter .runningAlert { background-color: #666666; } +#HTMLReporter .skippedAlert { background-color: #aaaaaa; } +#HTMLReporter .skippedAlert:first-child { background-color: #333333; } +#HTMLReporter .passingAlert { background-color: #a6b779; } +#HTMLReporter .passingAlert:first-child { background-color: #5e7d00; } +#HTMLReporter .failingAlert { background-color: #cf867e; } +#HTMLReporter .failingAlert:first-child { background-color: #b03911; } +#HTMLReporter .results { margin-top: 5px } +#HTMLReporter #details { display: none; } +#HTMLReporter .resultsMenu, #HTMLReporter .resultsMenu a { background-color: #fff; color: #333333; } +#HTMLReporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; } +#HTMLReporter.showDetails .detailsMenuItem { font-weight: bold; } +#HTMLReporter.showDetails .summary { display: none; } +#HTMLReporter.showDetails #details { display: block; } +#HTMLReporter .summaryMenuItem { font-weight: bold; } +#HTMLReporter .summary { margin-top: 14px; } +#HTMLReporter .summary .suite .suite, #HTMLReporter .summary .specSummary { margin-left: 14px; } +#HTMLReporter .summary .specSummary.passed a { color: #5e7d00; } +#HTMLReporter .summary .specSummary.failed a { color: #b03911; } +#HTMLReporter .description + .suite { margin-top: 0; } +#HTMLReporter .suite { margin-top: 14px; } +#HTMLReporter .suite a { color: #333333; } +#HTMLReporter #details .specDetail { margin-bottom: 28px; } +#HTMLReporter #details .specDetail .description { display: block; color: white; background-color: #b03911; font-size: 14px; padding: 3px 9px } +#HTMLReporter .resultMessage { padding-top: 5px; color: #333333; } +#HTMLReporter .resultMessage span.result { display: block; } +#HTMLReporter .stackTrace { margin: 5px 0 0 0; max-height: 500px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; } -.jasmine_reporter a:visited, .jasmine_reporter a { - color: #303; -} - -.jasmine_reporter a:hover, .jasmine_reporter a:active { - color: blue; -} - -.run_spec { - float:right; - padding-right: 5px; - font-size: .8em; - text-decoration: none; -} - -.jasmine_reporter { - margin: 0 5px; -} - -.banner { - color: #303; - background-color: #fef; - padding: 5px; -} - -.logo { - float: left; - font-size: 1.1em; - padding-left: 5px; -} - -.logo .version { - font-size: .6em; - padding-left: 1em; -} - -.runner.running { - background-color: yellow; -} - - -.options { - text-align: right; - font-size: .8em; -} - - - - -.suite { - border: 1px outset gray; - margin: 5px 0; - padding-left: 1em; -} - -.suite .suite { - margin: 5px; -} - -.suite.passed { - background-color: #dfd; -} - -.suite.failed { - background-color: #fdd; -} - -.spec { - margin: 5px; - padding-left: 1em; - clear: both; -} - -.spec.failed, .spec.passed, .spec.skipped { - padding-bottom: 5px; - border: 1px solid gray; -} - -.spec.failed { - background-color: #fbb; - border-color: red; -} - -.spec.passed { - background-color: #bfb; - border-color: green; -} - -.spec.skipped { - background-color: #bbb; -} - -.messages { - border-left: 1px dashed gray; - padding-left: 1em; - padding-right: 1em; -} - -.passed { - background-color: #cfc; - display: none; -} - -.failed { - background-color: #fbb; -} - -.skipped { - color: #777; - background-color: #eee; - display: none; -} - - -/*.resultMessage {*/ - /*white-space: pre;*/ -/*}*/ - -.resultMessage span.result { - display: block; - line-height: 2em; - color: black; -} - -.resultMessage .mismatch { - color: black; -} - -.stackTrace { - white-space: pre; - font-size: .8em; - margin-left: 10px; - max-height: 5em; - overflow: auto; - border: 1px inset red; - padding: 1em; - background: #eef; -} - -.finished-at { - padding-left: 1em; - font-size: .6em; -} - -.show-passed .passed, -.show-skipped .skipped { - display: block; -} - - -#jasmine_content { - position:fixed; - right: 100%; -} - -.runner { - border: 1px solid gray; - display: block; - margin: 5px 0; - padding: 2px 0 2px 10px; -} +#TrivialReporter { padding: 8px 13px; position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow-y: scroll; background-color: white; font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; /*.resultMessage {*/ /*white-space: pre;*/ /*}*/ } +#TrivialReporter a:visited, #TrivialReporter a { color: #303; } +#TrivialReporter a:hover, #TrivialReporter a:active { color: blue; } +#TrivialReporter .run_spec { float: right; padding-right: 5px; font-size: .8em; text-decoration: none; } +#TrivialReporter .banner { color: #303; background-color: #fef; padding: 5px; } +#TrivialReporter .logo { float: left; font-size: 1.1em; padding-left: 5px; } +#TrivialReporter .logo .version { font-size: .6em; padding-left: 1em; } +#TrivialReporter .runner.running { background-color: yellow; } +#TrivialReporter .options { text-align: right; font-size: .8em; } +#TrivialReporter .suite { border: 1px outset gray; margin: 5px 0; padding-left: 1em; } +#TrivialReporter .suite .suite { margin: 5px; } +#TrivialReporter .suite.passed { background-color: #dfd; } +#TrivialReporter .suite.failed { background-color: #fdd; } +#TrivialReporter .spec { margin: 5px; padding-left: 1em; clear: both; } +#TrivialReporter .spec.failed, #TrivialReporter .spec.passed, #TrivialReporter .spec.skipped { padding-bottom: 5px; border: 1px solid gray; } +#TrivialReporter .spec.failed { background-color: #fbb; border-color: red; } +#TrivialReporter .spec.passed { background-color: #bfb; border-color: green; } +#TrivialReporter .spec.skipped { background-color: #bbb; } +#TrivialReporter .messages { border-left: 1px dashed gray; padding-left: 1em; padding-right: 1em; } +#TrivialReporter .passed { background-color: #cfc; display: none; } +#TrivialReporter .failed { background-color: #fbb; } +#TrivialReporter .skipped { color: #777; background-color: #eee; display: none; } +#TrivialReporter .resultMessage span.result { display: block; line-height: 2em; color: black; } +#TrivialReporter .resultMessage .mismatch { color: black; } +#TrivialReporter .stackTrace { white-space: pre; font-size: .8em; margin-left: 10px; max-height: 5em; overflow: auto; border: 1px inset red; padding: 1em; background: #eef; } +#TrivialReporter .finished-at { padding-left: 1em; font-size: .6em; } +#TrivialReporter.show-passed .passed, #TrivialReporter.show-skipped .skipped { display: block; } +#TrivialReporter #jasmine_content { position: fixed; right: 100%; } +#TrivialReporter .runner { border: 1px solid gray; display: block; margin: 5px 0; padding: 2px 0 2px 10px; } diff --git a/static/notification.css b/static/notification.css new file mode 100644 index 000000000..53ac32339 --- /dev/null +++ b/static/notification.css @@ -0,0 +1,57 @@ +.notification { + position: absolute; + top: 40px; + left: 50%; + margin-left: -5%; + z-index: 9999; + border: 2px solid rgba(0, 0, 0, 0.2); + border-radius: 5px; + box-shadow: + inset 1px 1px 0 rgba(255, 255, 255, 0.05), + 0 0 5px rgba(0, 0, 0, 0.5); + background: -webkit-linear-gradient( + rgba(20, 20, 20, 0.5), + rgba(0, 0, 0, 0.5)); + color: #eee; + width: 300px; +} + +.notification .content { + padding: 10px; + margin-left: 42px; + box-sizing: border-box; +} + +.notification .content:after { + content: "."; + display: block; + clear: both; + visibility: hidden; +} + +.notification .title { + font-size: 12px; + font-weight: bold; + margin-bottom: 3px; +} + +.notification .message { + font-size: 12px; + color: #777; +} + +.notification .icon { + display: inline-block; + float: left; + font-family: 'Octicons Regular'; + font-size: 32px; + width: 32px; + height: 32px; + margin-right: 5px; + -webkit-font-smoothing: antialiased; +} + +/* TODO: Full Octicon Support */ +.icon-gist { + content: "\f20e"; +} diff --git a/static/overlay.css b/static/overlay.css new file mode 100644 index 000000000..421b2f71f --- /dev/null +++ b/static/overlay.css @@ -0,0 +1,35 @@ +.overlay { + position: absolute; + top: 0; + left: 50%; + width: 500px; + margin-left: -250px; + background: #303030; + color: #eee; + padding: 10px; + z-index: 9999; + box-sizing: border-box; + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + border-radius: 3px; +} + +.overlay .editor.mini { + margin-bottom: 10px; +} + +.overlay .message { + padding-top: 5px; + font-size: 11px; +} + +.overlay.mini { + width: 200px; + margin-left: -100px; +} + +.overlay.from-top { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; +} \ No newline at end of file diff --git a/static/popover-list.css b/static/popover-list.css new file mode 100644 index 000000000..005b4a36a --- /dev/null +++ b/static/popover-list.css @@ -0,0 +1,17 @@ +.select-list.popover-list { + width: 200px; + border: 2px solid #222; + -webkit-box-shadow: 0 0 3px 3px rgba(0, 0, 0, .5); + margin-left: 0px; + position: relative; +} + +.select-list.popover-list ol { + position: relative; + overflow-y: scroll; + max-height: 200px; +} + +.select-list.popover-list ol li { + padding: 5px; +} \ No newline at end of file diff --git a/static/select-list.css b/static/select-list.css index f2472e3c6..847edb7ab 100644 --- a/static/select-list.css +++ b/static/select-list.css @@ -1,19 +1,45 @@ -.select-list { - position: absolute; - width: 600px; - top: 0; - left: 50%; - margin-left: -300px; - box-sizing: border-box; - z-index: 99; -} - -.select-list .editor { - box-sizing: border-box; - padding: 5px; -} - .select-list ol { + border: 1px solid #212121; position: relative; overflow-y: auto; + max-height: 312px; + margin: 0; + background: red; + padding: 0; + line-height: 100%; +} + +.select-list ol:empty { + border: none; +} + +.select-list ol li { + padding: 10px; + box-sizing: border-box; + display: block; +} + +.select-list li .label { + display: inline-block; +} + +.select-list li .right { + float: right; +} + +.select-list .key-binding { + border-radius: 2px; + margin-left: 5px; + padding: 3px; + font-size: 11px; +} + +.select-list ol li:last-child { + border-bottom: none; +} + +.select-list .error { + font-weight: bold; + color: white; + text-shadow: 0 1px 0 #4E0000; } \ No newline at end of file diff --git a/themes/Atom - Light/status-bar.css b/static/status-bar.css similarity index 74% rename from themes/Atom - Light/status-bar.css rename to static/status-bar.css index 88995cd78..fe46c8408 100644 --- a/themes/Atom - Light/status-bar.css +++ b/static/status-bar.css @@ -1,10 +1,7 @@ .status-bar { - background-color: #e5e5e5; - border-top: 1px solid #959595; padding: 4px 10px 3px; font-size: 11px; line-height: 14px; - color: #333; } .status-bar .cursor-position, @@ -42,20 +39,10 @@ content: "\f020"; } -.status-bar .git-status.octicons.modified-status-icon { - color: #f78a46; - display: inline-block; -} - .status-bar .modified-status-icon:before { content: "\f26d"; } -.status-bar .git-status.octicons.new-status-icon { - color: #5293d8; - display: inline-block; -} - .status-bar .new-status-icon:before { content: "\f26b"; } diff --git a/static/tabs.css b/static/tabs.css new file mode 100644 index 000000000..2a4faa887 --- /dev/null +++ b/static/tabs.css @@ -0,0 +1,66 @@ +.tabs { + font: caption; + margin-bottom: 1px; +} + +.tab { + cursor: default; + padding: 2px 21px 2px 9px; +} + +.tab.file-modified .close-icon { + border-radius: 10px; +} + +.tab.file-modified .close-icon:before { + content: ""; +} + +.tab.active:before, +.tab.active:after { + position: absolute; + bottom: -1px; + width: 4px; + height: 4px; + content: " "; + z-index: 3; +} + +.tab.active:before { + left: -4px; +} + +.tab.active:after { + right: -4px; + border-width: 0 0 1px 1px; +} + +.tab.active:first-child:before { + display: none; +} + +.tab .file-name { + font-size: 11px; + text-shadow: 0 -1px 1px black; +} + +.tab .close-icon { + font-family: 'Octicons Regular'; + font-size: 14px; + width: 14px; + height: 14px; + display: block; + cursor: pointer; + position: absolute; + right: 4px; + top: -1px; + -webkit-font-smoothing: antialiased; +} + +.tab .close-icon:before { + content: "\f081"; +} + +.tab .close-icon:hover { + color: white; +} \ No newline at end of file diff --git a/themes/Atom - Dark/tree-view.css b/static/tree-view.css similarity index 66% rename from themes/Atom - Dark/tree-view.css rename to static/tree-view.css index ea27fd587..2bf94d527 100644 --- a/themes/Atom - Dark/tree-view.css +++ b/static/tree-view.css @@ -1,12 +1,3 @@ -.tree-view { - background: #1e1e1e; - border-right: 2px solid #191919; -} - -.tree-view .entry { - text-shadow: 0 -1px 0 #000; -} - .tree-view .entries { margin-left: 12px; } @@ -15,39 +6,6 @@ margin-left: 20px; } -.tree-view .directory.selected > .header > .name, -.tree-view .selected > .name { - color: #d2d2d2; -} - -.tree-view .selected > .highlight { - background-image: -webkit-linear-gradient(#4e4e4e, #434343); -} - -.tree-view:focus .selected > .highlight { - background-image: -webkit-linear-gradient(#7e7e7e, #737373); -} - -.tree-view:focus .directory.selected > .header > .name, -.tree-view:focus .selected > .name { - color: #fff; - text-shadow: 0 -1px 0 #7E4521; -} - -.tree-view .directory .header { - color: #bebebe; -} - -.tree-view .file { - color: #7d7d7d; -} - -.tree-view .entry:hover, -.tree-view .directory .header:hover .name, -.tree-view .directory .header:hover .disclosure-arrow { - color: #ebebeb; -} - .tree-view .file .name, .tree-view .directory .header { padding-top: 4px; @@ -59,22 +17,7 @@ padding-left: 5px; } -.tree-view .ignored { - color: #555; -} - -.tree-view .modified { - color: #f78a46; -} - -.tree-view .new { - color: #5293d8; -} - .tree-view-dialog { - background-color: #333; - border-top: 1px solid #555; - -webkit-box-shadow: 0 0 3px 3px rgba(0, 0, 0, .5); padding: 5px; } @@ -82,7 +25,6 @@ padding-bottom: 3px; font-size: 12px; line-height: 16px; - color: #aaa; } .tree-view-dialog .prompt span { @@ -170,6 +112,11 @@ top: -2px; } +.tree-view .file .readme-icon:before { + content: "\f007"; + top: -2px; +} + .tree-view .directory > .header .disclosure-arrow:before { content: "\f05a"; } diff --git a/themes/Atom - Dark/atom.css b/themes/Atom - Dark/atom.css deleted file mode 100644 index 2e5a1355b..000000000 --- a/themes/Atom - Dark/atom.css +++ /dev/null @@ -1,40 +0,0 @@ -html, body, -#root-view { - font: caption; - background-color: #333333; -} - -#root-view #panes:before { - display: block; - content: "\f208"; - font-family: 'Octicons Regular'; - color: #303030; - -webkit-font-smoothing: antialiased; - font-size: 100vmin; - line-height: 100vmin; - text-align: center; -} - -#root-view #panes .row > * + * { - border-left: 5px solid #515151; -} - -#root-view #panes .column > * + * { - border-top: 5px solid #515151; -} - -.error { - background: #991212 !important; - -webkit-transition: background 300ms ease-out; -} - -.clear-float { - clear: both; -} - -@font-face { - font-family: 'Octicons Regular'; - src: url("octicons-regular-webfont.woff") format("woff"); - font-weight: normal; - font-style: normal; -} diff --git a/themes/Atom - Dark/autocomplete.css b/themes/Atom - Dark/autocomplete.css deleted file mode 100644 index 290713336..000000000 --- a/themes/Atom - Dark/autocomplete.css +++ /dev/null @@ -1,17 +0,0 @@ -.select-list.autocomplete { - min-width: 150px; - border: 2px solid #222; - webkit-box-shadow: 0 0 3px 3px rgba(0, 0, 0, .5); - margin-left: 0px; - width: auto; -} - -.autocomplete ol { - position: relative; - overflow-y: scroll; - max-height: 200px; -} - -.autocomplete ol li { - padding: 0.1em 0.2em; -} diff --git a/themes/Atom - Dark/command-palette.css b/themes/Atom - Dark/command-palette.css deleted file mode 100644 index 0cadfbcae..000000000 --- a/themes/Atom - Dark/command-palette.css +++ /dev/null @@ -1,37 +0,0 @@ -.command-palette { - width: 50%; - margin-left: -25%; -} - -.command-palette ol { - max-height: 300px; -} - -.command-palette ol .event-description { - float: left; - display: inline-block; - margin-right: .5em; - margin: 4px 0; -} - -.command-palette li .right { - float: right; -} - -.command-palette ol .event-name, .command-palette ol .key-binding { - display: inline-block; - margin: 4px 0; - margin-right: .5em; - font-size: 90%; - color: #ddd; - -webkit-border-radius: 3px; - padding: 0 4px; -} - -.command-palette ol .event-name { - background: rgba(0, 0, 0, .2); -} - -.command-palette ol .key-binding { - background: rgba(255, 255, 255, .1); -} diff --git a/themes/Atom - Dark/command-panel.css b/themes/Atom - Dark/command-panel.css deleted file mode 100644 index dfc7f225b..000000000 --- a/themes/Atom - Dark/command-panel.css +++ /dev/null @@ -1,101 +0,0 @@ -.command-panel { - background-color: #303030; - border: 1px solid #252525; - color: #ededed; - padding: 5px; -} - -.command-panel .preview-list { - max-height: 300px; - overflow: auto; - margin-bottom: 3px; - position: relative; - background-color: #000000; - color: #ededed; - cursor: default; -} - -.command-panel .preview-count { - font-size: 11px; - color: #969696; - text-align: right; - padding-bottom: 1px; -} - -.command-panel .preview-list li.selected, .command-panel .preview-list li.operation:hover { - background-color: rgba(255, 255, 255, .13); -} - -.command-panel .preview-list:focus li.selected { - background-color: rgba(255, 255, 255, .2); -} - -.command-panel .preview-list .path { - padding-left: 5px; - color: #00a693; -} - -.command-panel .preview-list .path: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 .operation { - padding-top: 2px; - padding-bottom: 2px; -} - -.command-panel .preview-list .line-number { - padding-left: 3px; - color: rgba(255, 255, 255, .3); - margin-right: 1ex; - text-align: right; - display: inline-block; -} - -.command-panel .preview-list .path-match-number { - padding-left: 8px; - color: rgba(255, 255, 255, .3); -} - -.command-panel .preview-list .preview { - word-break: break-all; -} - -.command-panel .preview-list .preview .match { - background-color: rgba(255, 255, 255, .2); - -webkit-border-radius: 2px; - padding: 1px; -} - -.command-panel .prompt-and-editor { - display: -webkit-box; -} - -.command-panel .prompt-and-editor .prompt:before { - color: #969696; - content: '\f078'; - font-family: 'Octicons Regular'; - position: relative; - top: -5px; - left: -5px; - -webkit-font-smoothing: antialiased; -} - -.command-panel .prompt-and-editor .editor { - position: relative; - left: -4px; - margin-right: -4px; -} - -.error-messages { - padding: 5px 1em; - color: white; -} diff --git a/themes/Atom - Dark/editor.css b/themes/Atom - Dark/editor.css deleted file mode 100644 index 6581aaa59..000000000 --- a/themes/Atom - Dark/editor.css +++ /dev/null @@ -1,46 +0,0 @@ -.editor { - font-family: Inconsolata, Monaco, Courier; -} - -.editor.mini { - height: auto; -} - -.editor .gutter .line-number { - padding-right: 1.3em; - min-width: 35px; - box-sizing: border-box; - text-align: right; - opacity: 0.6; -} - -.editor .gutter .line-number.cursor-line { - opacity: 1; -} - -.editor .gutter.drop-shadow { - -webkit-box-shadow: -2px 0px 10px 2px #222; -} - -@-webkit-keyframes highlight { - from { background-color: rgba(100, 255, 100, 0.7); } - to { background-color: null; } -} - -.editor .highlighted.selection .region { - -webkit-animation-name: highlight; - -webkit-animation-duration: 1s; - -webkit-animation-iteration-count: 1; -} - -.editor .fold { - background-color: #444; -} - -.editor .fold.selected { - background-color: #244; -} - -.editor .invisible { - opacity: 0.2; -} diff --git a/themes/Atom - Dark/fuzzy-finder.css b/themes/Atom - Dark/fuzzy-finder.css deleted file mode 100644 index 3d0218083..000000000 --- a/themes/Atom - Dark/fuzzy-finder.css +++ /dev/null @@ -1,42 +0,0 @@ -.fuzzy-finder ol { - overflow: hidden; - -webkit-user-select: none; - cursor: default; -} - -.fuzzy-finder ol:empty { - margin-bottom: 0; - border: none; -} - -.fuzzy-finder .directory { - color: #b2b2b2; - padding-left: .5em; -} - -.fuzzy-finder .file:before { - font-family: 'Octicons Regular'; - font-size: 16px; - width: 16px; - height: 16px; - margin-right: 5px; - margin-left: 5px; - -webkit-font-smoothing: antialiased; - color: #ccc; -} - -.fuzzy-finder .file.text-name:before { - content: "\f011"; -} - -.fuzzy-finder .file.image-name:before { - content: "\f012"; -} - -.fuzzy-finder .file.compressed-name:before { - content: "\f013"; -} - -.fuzzy-finder .file.pdf-name:before { - content: "\f014"; -} \ No newline at end of file diff --git a/themes/Atom - Dark/gists.css b/themes/Atom - Dark/gists.css deleted file mode 100644 index dde2dbd25..000000000 --- a/themes/Atom - Dark/gists.css +++ /dev/null @@ -1,35 +0,0 @@ -.gist-notification { - position: absolute; - top: 6px; - left: 50%; - margin-left: -5%; - z-index: 99; - padding-left: 5px; - padding-right: 10px; - -webkit-box-shadow: 0px 0px 5px 5px #222; - color: #BBB; - background-color: #333; -} - -.gist-notification .message-area { - float: right; - padding-top: 11px; -} - -.gist-notification .message { - font-size: 13px; -} - -.gist-notification .clipboard { - font-size: 11px; -} - -.gist-notification:before { - font-family: 'Octicons Regular'; - font-size: 32px; - width: 32px; - height: 32px; - margin-right: 5px; - -webkit-font-smoothing: antialiased; - content: "\f08c"; -} diff --git a/themes/Atom - Dark/go-to-line.css b/themes/Atom - Dark/go-to-line.css deleted file mode 100644 index 4ceec81aa..000000000 --- a/themes/Atom - Dark/go-to-line.css +++ /dev/null @@ -1,27 +0,0 @@ -.go-to-line { - position: absolute; - width: 200px; - top: 0; - left: 50%; - margin-left: -100px; - box-sizing: border-box; - z-index: 99; - background-color: #484848; - border: 1px solid #444; - color: #d2d2d2; - box-shadow: 0 0 10px #000; - border-bottom-left-radius: 3px; - border-bottom-right-radius: 3px; - padding: 5px; - cursor: pointer; -} - -.go-to-line .editor { - box-sizing: border-box; - padding: 5px; -} - -.go-to-line .message { - padding-top: 2px; - font-size: 11px; -} diff --git a/themes/Atom - Dark/grammar-view.css b/themes/Atom - Dark/grammar-view.css deleted file mode 100644 index 7be94f4c0..000000000 --- a/themes/Atom - Dark/grammar-view.css +++ /dev/null @@ -1,17 +0,0 @@ -.grammar-view ol li { - line-height: 16px; -} - -.grammar-view ol li.grammar { - padding-left: 21px; -} - -.grammar-view ol li.current-grammar:before { - font-family: 'Octicons Regular'; - width: 16px; - height: 16px; - margin-right: 5px; - -webkit-font-smoothing: antialiased; - color: #ccc; - content: '\f03a'; -} diff --git a/themes/Atom - Dark/select-list.css b/themes/Atom - Dark/select-list.css deleted file mode 100644 index 29aac3f03..000000000 --- a/themes/Atom - Dark/select-list.css +++ /dev/null @@ -1,42 +0,0 @@ -.select-list { - background-color: #484848; - border: 1px solid #444; - color: #d2d2d2; - box-shadow: 0 0 10px #000; - border-bottom-left-radius: 3px; - border-bottom-right-radius: 3px; - padding: 5px; - cursor: pointer; -} - -.select-list ol { - border: 1px solid #212121; -} - -.select-list ol li { - background-color: #292929; - border-bottom: 1px solid #212121; - padding: 5px; -} - -.select-list ol li:last-child { - border-bottom: none; -} - -.select-list .editor { - margin-bottom: 5px; -} - -.select-list li:hover { - background-color: #555; -} - -.select-list ol .selected { - background-image: -webkit-linear-gradient(#7e7e7e, #737373); -} - -.select-list .error { - font-weight: bold; - color: white; - text-shadow: 0 1px 0 #4E0000; -} \ No newline at end of file diff --git a/themes/Atom - Dark/status-bar.css b/themes/Atom - Dark/status-bar.css deleted file mode 100644 index f517760f4..000000000 --- a/themes/Atom - Dark/status-bar.css +++ /dev/null @@ -1,61 +0,0 @@ -.status-bar { - background-color: #303030; - border-top: 1px solid #454545; - padding: 4px 10px 3px; - font-size: 11px; - line-height: 14px; - color: #969696; -} - -.status-bar .cursor-position, -.status-bar .grammar-name { - padding-left: 10px; -} - -.status-bar .grammar-name { - cursor: pointer; -} - -.status-bar .branch-label { - vertical-align: baseline; -} - -.status-bar .git-status.octicons { - display: none; - padding-left: 10px; - margin-top:-2px; -} - -.status-bar .octicons:before { - font-family: 'Octicons Regular'; - font-size: 14px; - width: 14px; - height: 14px; - line-height: 14px; - -webkit-font-smoothing: antialiased; - display: inline-block; - vertical-align: middle; - margin-right: 5px; -} - -.status-bar .branch-icon:before { - content: "\f020"; -} - -.status-bar .git-status.octicons.modified-status-icon { - color: #f78a46; - display: inline-block; -} - -.status-bar .modified-status-icon:before { - content: "\f26d"; -} - -.status-bar .git-status.octicons.new-status-icon { - color: #5293d8; - display: inline-block; -} - -.status-bar .new-status-icon:before { - content: "\f26b"; -} diff --git a/themes/Atom - Dark/symbols-view.css b/themes/Atom - Dark/symbols-view.css deleted file mode 100644 index 0cbdbfa53..000000000 --- a/themes/Atom - Dark/symbols-view.css +++ /dev/null @@ -1,35 +0,0 @@ -.symbols-view { - width: 50%; - margin-left: -25%; -} - -.symbols-view ol { - max-height: 300px; -} - -.symbols-view ol li { - padding: 2px; - border-bottom: 1px solid rgba(255, 255, 255, .05); -} - -.symbols-view ol .function-name { - float: left; - display: inline-block; - margin-right: .5em; - margin: 4px 0; -} - -.symbols-view li .right { - float: right; -} - -.symbols-view ol .function-details { - display: inline-block; - margin: 4px 0; - margin-right: .5em; - font-size: 90%; - color: #ddd; - -webkit-border-radius: 3px; - padding: 0 4px; - background: rgba(0, 0, 0, .2); -} diff --git a/themes/Atom - Dark/wrap-guide.css b/themes/Atom - Dark/wrap-guide.css deleted file mode 100644 index 51660ea4c..000000000 --- a/themes/Atom - Dark/wrap-guide.css +++ /dev/null @@ -1,3 +0,0 @@ -.wrap-guide { - background: rgba(150, 150, 150, .30); -} diff --git a/themes/Atom - Light/atom.css b/themes/Atom - Light/atom.css deleted file mode 100644 index 772922301..000000000 --- a/themes/Atom - Light/atom.css +++ /dev/null @@ -1,29 +0,0 @@ -html, body, -#root-view { - font: caption; - background-color: #f4f4f4; -} - -#root-view #panes .row > * + * { - border-left: 1px solid #989898; -} - -#root-view #panes .column > * + * { - border-top: 1px solid #989898; -} - -.error { - background: #991212 !important; - -webkit-transition: background 300ms ease-out; -} - -.clear-float { - clear: both; -} - -@font-face { - font-family: 'Octicons Regular'; - src: url("octicons-regular-webfont.woff") format("woff"); - font-weight: normal; - font-style: normal; -} diff --git a/themes/Atom - Light/autocomplete.css b/themes/Atom - Light/autocomplete.css deleted file mode 100644 index c121407d0..000000000 --- a/themes/Atom - Light/autocomplete.css +++ /dev/null @@ -1,16 +0,0 @@ -.select-list.autocomplete { - min-width: 150px; - webkit-box-shadow: 0 0 3px rgba(0, 0, 0, .5); - margin-left: 0px; - width: auto; -} - -.autocomplete ol { - position: relative; - overflow-y: scroll; - max-height: 200px; -} - -.autocomplete ol li { - padding: 0.1em 0.2em; -} diff --git a/themes/Atom - Light/command-palette.css b/themes/Atom - Light/command-palette.css deleted file mode 100644 index 272b6b806..000000000 --- a/themes/Atom - Light/command-palette.css +++ /dev/null @@ -1,34 +0,0 @@ -.command-palette { - width: 50%; - margin-left: -25%; -} - -.command-palette ol { - max-height: 300px; -} - -.command-palette ol .event-description { - float: left; - display: inline-block; - margin-right: .5em; - margin: 4px 0; -} - -.command-palette li .right { - float: right; -} - -.command-palette ol .event-name, .command-palette ol .key-binding { - display: inline-block; - margin: 4px 0; - margin-right: .5em; - font-size: 90%; - color: #969696; - -webkit-border-radius: 3px; - padding: 0 4px; -} - -.command-palette ol .event-name { - background: rgba(0, 0, 0, .2); - color: #fff; -} \ No newline at end of file diff --git a/themes/Atom - Light/editor.css b/themes/Atom - Light/editor.css deleted file mode 100644 index ebdf4ad12..000000000 --- a/themes/Atom - Light/editor.css +++ /dev/null @@ -1,46 +0,0 @@ -.editor { - font-family: Inconsolata, Monaco, Courier; -} - -.editor.mini { - height: auto; -} - -.editor .gutter .line-number { - padding-right: 1.3em; - min-width: 35px; - box-sizing: border-box; - text-align: right; - opacity: 0.5; -} - -.editor .gutter .line-number.cursor-line { - opacity: 1; -} - -.editor .gutter.drop-shadow { - -webkit-box-shadow: -2px 0px 10px 2px #222; -} - -@-webkit-keyframes highlight { - from { background-color: rgba(100, 255, 100, 0.7); } - to { background-color: null; } -} - -.editor .highlighted.selection .region { - -webkit-animation-name: highlight; - -webkit-animation-duration: 1s; - -webkit-animation-iteration-count: 1; -} - -.editor .fold { - background-color: #444; -} - -.editor .fold.selected { - background-color: #244; -} - -.editor .invisible { - opacity: 0.2; -} diff --git a/themes/Atom - Light/gists.css b/themes/Atom - Light/gists.css deleted file mode 100644 index 0209ce3e3..000000000 --- a/themes/Atom - Light/gists.css +++ /dev/null @@ -1,35 +0,0 @@ -.gist-notification { - position: absolute; - top: 6px; - left: 50%; - margin-left: -5%; - z-index: 99; - padding-left: 5px; - padding-right: 10px; - -webkit-box-shadow: 0px 0px 5px 5px #222; - color: #BBB; - background-color: #444; -} - -.gist-notification .message-area { - float: right; - padding-top: 11px; -} - -.gist-notification .message { - font-size: 13px; -} - -.gist-notification .clipboard { - font-size: 11px; -} - -.gist-notification:before { - font-family: 'Octicons Regular'; - font-size: 32px; - width: 32px; - height: 32px; - margin-right: 5px; - -webkit-font-smoothing: antialiased; - content: "\f08c"; -} diff --git a/themes/Atom - Light/go-to-line.css b/themes/Atom - Light/go-to-line.css deleted file mode 100644 index 9f6c3ec8b..000000000 --- a/themes/Atom - Light/go-to-line.css +++ /dev/null @@ -1,27 +0,0 @@ -.go-to-line { - position: absolute; - width: 200px; - top: 0; - left: 50%; - margin-left: -100px; - box-sizing: border-box; - z-index: 99; - background-color: #eeeeee; - border: 1px solid #c6c6c6; - color: #323232; - box-shadow: 0 0 10px #555; - border-bottom-left-radius: 3px; - border-bottom-right-radius: 3px; - padding: 5px; - cursor: pointer; -} - -.go-to-line .editor { - box-sizing: border-box; - padding: 5px; -} - -.go-to-line .message { - padding-top: 2px; - font-size: 11px; -} diff --git a/themes/Atom - Light/grammar-view.css b/themes/Atom - Light/grammar-view.css deleted file mode 100644 index 7be94f4c0..000000000 --- a/themes/Atom - Light/grammar-view.css +++ /dev/null @@ -1,17 +0,0 @@ -.grammar-view ol li { - line-height: 16px; -} - -.grammar-view ol li.grammar { - padding-left: 21px; -} - -.grammar-view ol li.current-grammar:before { - font-family: 'Octicons Regular'; - width: 16px; - height: 16px; - margin-right: 5px; - -webkit-font-smoothing: antialiased; - color: #ccc; - content: '\f03a'; -} diff --git a/themes/Atom - Light/select-list.css b/themes/Atom - Light/select-list.css deleted file mode 100644 index 6142a4c13..000000000 --- a/themes/Atom - Light/select-list.css +++ /dev/null @@ -1,47 +0,0 @@ -.select-list { - background-color: #eeeeee; - border: 1px solid #c6c6c6; - color: #323232; - box-shadow: 0 0 10px #555; - border-bottom-left-radius: 3px; - border-bottom-right-radius: 3px; - padding: 5px; - cursor: pointer; -} - -.select-list .editor { - border: 1px solid #afafaf; - box-shadow: inset 0 0 2px #ccc; -} - -.select-list ol { - border: 1px solid #d2d2d2; -} - -.select-list ol li { - background-color: #f5f5f5; - border-bottom: 1px solid #e6e6e6; - padding: 5px; -} - -.select-list ol li:last-child { - border-bottom: none; -} - -.select-list .editor { - margin-bottom: 5px; -} - -.select-list li:hover { - background-color: #f9f9f9; -} - -.select-list ol .selected { - background-color: #e1e1e1; -} - -.select-list .error { - font-weight: bold; - color: white; - text-shadow: 0 1px 0 #4E0000; -} \ No newline at end of file diff --git a/themes/Atom - Light/symbols-view.css b/themes/Atom - Light/symbols-view.css deleted file mode 100644 index 0cbdbfa53..000000000 --- a/themes/Atom - Light/symbols-view.css +++ /dev/null @@ -1,35 +0,0 @@ -.symbols-view { - width: 50%; - margin-left: -25%; -} - -.symbols-view ol { - max-height: 300px; -} - -.symbols-view ol li { - padding: 2px; - border-bottom: 1px solid rgba(255, 255, 255, .05); -} - -.symbols-view ol .function-name { - float: left; - display: inline-block; - margin-right: .5em; - margin: 4px 0; -} - -.symbols-view li .right { - float: right; -} - -.symbols-view ol .function-details { - display: inline-block; - margin: 4px 0; - margin-right: .5em; - font-size: 90%; - color: #ddd; - -webkit-border-radius: 3px; - padding: 0 4px; - background: rgba(0, 0, 0, .2); -} diff --git a/themes/Atom - Light/tree-view.css b/themes/Atom - Light/tree-view.css deleted file mode 100644 index c0f599fc2..000000000 --- a/themes/Atom - Light/tree-view.css +++ /dev/null @@ -1,201 +0,0 @@ -.tree-view { - background: #dde3e8; - border-right: 1px solid #989898; -} - -.tree-view .entry { - text-shadow: 0 1px 0 #fff; -} - -.tree-view .entries { - margin-left: 12px; -} - -.tree-view .entries .file .name { - margin-left: 20px; -} - -.tree-view .directory.selected > .header > .name, -.tree-view .selected > .name { - color: #262626; -} - -.tree-view .selected > .highlight { - box-sizing: border-box; - border-top: 1px solid #97a4a7; - border-bottom: 1px solid #97a4a7; - box-shadow: 0 -1px 0 #dde4e6, 0 1px 0 #dde4e6; - background-image: -webkit-linear-gradient(#cad5d8, #bcccce); -} - -.tree-view:focus .selected > .highlight { - border-top: 1px solid #3D4552; - border-bottom: 1px solid #3D4552; - background-image: -webkit-linear-gradient(#7e868d, #69717b); -} - -.tree-view:focus .directory.selected > .header > .name, -.tree-view:focus .selected > .name, -.tree-view:focus .directory.selected > .header > .name:before, -.tree-view:focus .selected > .name:before { - color: #fff; - text-shadow: 0 1px 0 #000; -} - -.tree-view .directory .header { - color: #262626; -} - -.tree-view .file { - color: #262626; -} - -.tree-view .name:before { - color: #7e8692; -} - -.tree-view .entry:hover, -.tree-view .directory .header:hover .name, -.tree-view .directory .header:hover .disclosure-arrow { - color: #4e5666; -} - -.tree-view .directory .header .disclosure-arrow, -.tree-view .directory .header:hover .disclosure-arrow { - text-shadow: none; - color: #4e5666; -} - -.tree-view:focus .directory .header .disclosure-arrow, -.tree-view:focus .directory .header:hover .disclosure-arrow { - color: #3D4552; -} - -.tree-view .file .name, -.tree-view .directory .header { - padding-top: 4px; - padding-bottom: 4px; - padding-right: 10px; -} - -.tree-view .directory .header { - padding-left: 5px; -} - -.tree-view .ignored { - color: #555; -} - -.tree-view .modified { - color: #f78a46; -} - -.tree-view .new { - color: #5293d8; -} - -.tree-view-dialog { - background-color: #e7e7e7; - border-top: 1px solid #989898; - padding: 5px; -} - -.tree-view-dialog .prompt { - padding-bottom: 3px; - font-size: 12px; - line-height: 16px; - color: #333; -} - -.tree-view-dialog .prompt span { - position: relative; - top: -1px; -} - -.tree-view-dialog .prompt:before { - font-family: 'Octicons Regular'; - font-size: 16px; - width: 16px; - height: 16px; - margin-right: 3px; - -webkit-font-smoothing: antialiased; -} - -.tree-view-dialog .prompt.add:before { - content: "\f086"; -} - -.tree-view-dialog .prompt.move:before { - content: "\f03e"; -} - -.tree-view .directory .header .name, -.tree-view .file .name { - position: relative; - padding-left: 21px; -} - -.tree-view .directory .header .name:before, -.tree-view .file .name:before { - font-family: 'Octicons Regular'; - font-size: 16px; - width: 16px; - height: 16px; - margin-right: 5px; - -webkit-font-smoothing: antialiased; - position: absolute; - left: 0; -} - -.tree-view .disclosure-arrow:before { - font-family: 'Octicons Regular'; - font-size: 12px; - width: 12px; - height: 12px; - line-height: 16px; - margin-right: 3px; - -webkit-font-smoothing: antialiased; -} - -.tree-view .directory .header .directory-icon:before { - content: "\f016"; - top: -5px; -} - -.tree-view .directory .header .repository-icon:before { - content: "\f001"; - top: -4px; -} - -.tree-view .directory .header .submodule-icon:before { - content: "\f017"; - top: -5px; -} - -.tree-view .file .text-icon:before { - content: "\f011"; - top: -2px; -} - -.tree-view .file .image-icon:before { - content: "\f012"; - top: -2px; -} - -.tree-view .file .compressed-icon:before { - content: "\f013"; - top: -2px; -} - -.tree-view .file .pdf-icon:before { - content: "\f014"; - top: -2px; -} - -.tree-view .directory > .header .disclosure-arrow:before { - content: "\f05a"; -} - -.tree-view .directory.expanded > .header .disclosure-arrow:before { - content: "\f05b"; -} diff --git a/themes/Atom - Light/wrap-guide.css b/themes/Atom - Light/wrap-guide.css deleted file mode 100644 index 51660ea4c..000000000 --- a/themes/Atom - Light/wrap-guide.css +++ /dev/null @@ -1,3 +0,0 @@ -.wrap-guide { - background: rgba(150, 150, 150, .30); -} diff --git a/themes/atom-dark-syntax.css b/themes/atom-dark-syntax.css new file mode 100644 index 000000000..5f19316c2 --- /dev/null +++ b/themes/atom-dark-syntax.css @@ -0,0 +1,269 @@ +.editor, .editor .gutter { + background-color: #1d1f21; + color: #c5c8c6; +} + +.editor .cursor { + border-color: #FFFFFF; +} + +.editor .selection .region { + background-color: #333333; +} + +.editor .line-number.cursor-line-no-selection, .editor .line.cursor-line { + background-color: rgba(255, 255, 255, 0.14); +} + +.comment { + color: #7C7C7C; +} + +.entity { + color: #FFD2A7; +} + +.keyword { + color: #96CBFE; +} + +.keyword.control { + color: #96CBFE; +} + +.keyword.operator { + color: #EDEDED; +} + +.entity.name.type { + text-decoration: underline; + color: #FFFFB6; +} + +.support { + color: #FFFFB6; +} + +.storage { + color: #CFCB90; +} + +.storage.modifier { + color: #96CBFE; +} + +.constant { + color: #99CC99; +} + +.string { + color: #A8FF60; +} + +.constant.numeric { + color: #FF73FD; +} + +.punctuation { +} + +.variable { + color: #C6C5FE; +} + +.invalid.deprecated { + text-decoration: underline; + color: #FD5FF1; +} + +.invalid.illegal { + color: #FD5FF1; + background-color: rgba(86, 45, 86, 0.75); +} + +.text .source { + background-color: rgba(177, 179, 186, 0.03); +} + +.entity.other.inherited-class { + color: #9B5C2E; +} + +.source .string .source { + color: #EDEDED; +} + +.source .string .source .punctuation.section.embedded { + color: #00A0A0; +} + +.string .constant { + color: #00A0A0; +} + +.string.regexp { + color: #E9C062; +} + +.string.regexp .constant.character.escape, .string.regexp .source.ruby.embedded, .string.regexp .string.regexp.arbitrary-repitition { + color: #FF8000; +} + +.string.regexp.group { + color: #C6A24F; + background-color: rgba(255, 255, 255, 0.06); +} + +.string.regexp.character-class { + color: #B18A3D; +} + +.string .variable { + color: #8A9A95; +} + +.support.function { + color: #DAD085; +} + +.support.constant { + color: #FFD2A7; +} + +.meta.preprocessor.c { + color: #8996A8; +} + +.meta.preprocessor.c .keyword { + color: #AFC4DB; +} + +.meta.cast { + color: #676767; +} + +.meta.sgml.html .meta.doctype, .meta.sgml.html .meta.doctype .entity, .meta.sgml.html .meta.doctype .string, .meta.xml-processing, .meta.xml-processing .entity, .meta.xml-processing .string { + color: #494949; +} + +.meta.tag, .meta.tag .entity { + color: #96CBFE; +} + +.source .entity.name.tag, .source .entity.other.attribute-name, .meta.tag.inline, .meta.tag.inline .entity { + color: #96CBFE; +} + +.entity.other.attribute-name { + color: #FFD7B1; +} + +.entity.name.tag.namespace, .entity.other.attribute-name.namespace { + color: #E18964; +} + +.meta.selector.css .entity.name.tag { + text-decoration: underline; + color: #96CBFE; +} + +.meta.selector.css .entity.other.attribute-name.tag.pseudo-class { + color: #8F9D6A; +} + +.meta.selector.css .entity.other.attribute-name.id { + color: #8B98AB; +} + +.meta.selector.css .entity.other.attribute-name.class { + color: #62B1FE; +} + +.support.type.property-name.css { + color: #EDEDED; +} + +.meta.property-group .support.constant.property-value.css, .meta.property-value .support.constant.property-value.css { + color: #F9EE98; +} + +.meta.preprocessor.at-rule .keyword.control.at-rule { + color: #8693A5; +} + +.meta.property-value .support.constant.named-color.css, .meta.property-value .constant { + color: #87C38A; +} + +.meta.constructor.argument.css { + color: #8F9D6A; +} + +.meta.diff, .meta.diff.header { + color: #F8F8F8; + background-color: #0E2231; +} + +.markup.deleted { + color: #F8F8F8; + background-color: #420E09; +} + +.markup.changed { + color: #F8F8F8; + background-color: #4A410D; +} + +.markup.inserted { + color: #F8F8F8; + background-color: #253B22; +} + +.markup.italic { + color: #E9C062; +} + +.markup.bold { + color: #E9C062; +} + +.markup.underline { + text-decoration: underline; + color: #E18964; +} + +.markup.quote { + color: #E1D4B9; + background-color: rgba(254, 224, 156, 0.07); +} + +.markup.heading, .markup.heading .entity { + color: #FEDCC5; + background-color: #632D04; +} + +.markup.list { + color: #E1D4B9; +} + +.markup.raw { + color: #578BB3; + background-color: rgba(177, 179, 186, 0.03); +} + +.markup .comment { + color: #F67B37; +} + +.meta.separator { + color: #60A633; + background-color: #242424; +} + +.meta.line.entry.logfile, .meta.line.exit.logfile { + background-color: rgba(238, 238, 238, 0.16); +} + +.meta.line.error.logfile { + background-color: #751012; +} \ No newline at end of file diff --git a/themes/atom-dark-ui/atom.css b/themes/atom-dark-ui/atom.css new file mode 100644 index 000000000..22afd1c83 --- /dev/null +++ b/themes/atom-dark-ui/atom.css @@ -0,0 +1,26 @@ +html, body, +#root-view { + font: caption; + background-color: #333333; +} + +#root-view #panes .row > * + * { + -webkit-box-shadow: + -2px 0 rgba(0, 0, 0, 0.3), + -1px 0 rgba(255, 255, 255, 0.2); +} + +#root-view #panes .column > * + * { + -webkit-box-shadow: + 0 -2px rgba(0, 0, 0, 0.3), + 0 -1px rgba(255, 255, 255, 0.2); +} + +.error { + background: #991212 !important; + -webkit-transition: background 300ms ease-out; +} + +.wrap-guide { + background: rgba(150, 150, 150, 0.1); +} \ No newline at end of file diff --git a/themes/atom-dark-ui/blurred.css b/themes/atom-dark-ui/blurred.css new file mode 100644 index 000000000..cb9601b51 --- /dev/null +++ b/themes/atom-dark-ui/blurred.css @@ -0,0 +1,21 @@ +.is-blurred .tree-view { + background-color: #2a2a2a; +} + +.is-blurred .tabs { + opacity: 0.8; + border-bottom-color: #525252; +} + +.is-blurred .tab { + background-image: -webkit-linear-gradient(#444, #555); +} + +.is-blurred .tab.active { + border: 1px solid #525252 +} + +.is-blurred .tab.active:before { + box-shadow: 2px 2px 0 #525252; + border-color: #696969; +} \ No newline at end of file diff --git a/themes/atom-dark-ui/bracket-matcher.css b/themes/atom-dark-ui/bracket-matcher.css new file mode 100644 index 000000000..d579fdc35 --- /dev/null +++ b/themes/atom-dark-ui/bracket-matcher.css @@ -0,0 +1,5 @@ +.bracket-matcher { + border-bottom: 1px solid #f8de7e; + margin-top: -1px; + opacity: .7; +} diff --git a/themes/Atom - Dark/command-logger.css b/themes/atom-dark-ui/command-logger.css similarity index 100% rename from themes/Atom - Dark/command-logger.css rename to themes/atom-dark-ui/command-logger.css diff --git a/themes/atom-dark-ui/command-panel.css b/themes/atom-dark-ui/command-panel.css new file mode 100644 index 000000000..f3e8412cd --- /dev/null +++ b/themes/atom-dark-ui/command-panel.css @@ -0,0 +1,46 @@ +.command-panel { + background-color: #303030; + border: 1px solid #252525; + color: #dedede; +} + +.command-panel .preview-list { + background-color: #1e1e1e; + color: #C5C8C6; + border: 1px solid rgba(0, 0, 0, 0.5); + border-bottom: 1px solid rgba(180, 180, 180, 0.2); + border-right: 1px solid rgba(180, 180, 180, 0.2); +} + +.command-panel .preview-count { + color: #969696; +} + +.command-panel .preview-list li.selected, .command-panel .preview-list li.operation:hover { + background-color: rgba(255, 255, 255, .13); +} + +.command-panel .preview-list:focus li.selected { + background-color: rgba(255, 255, 255, .2); +} + +.command-panel .preview-list .path { + color: #fff; +} + +.command-panel .preview-list .line-number { + color: rgba(255, 255, 255, .3); +} + +.command-panel .preview-list .path-match-number { + color: rgba(255, 255, 255, .3); +} + +.command-panel .preview-list .preview { + word-break: break-all; +} + +.command-panel .preview-list .preview .match { + background-color: rgba(255, 255, 255, .2); + color: yellow; +} diff --git a/themes/atom-dark-ui/editor.css b/themes/atom-dark-ui/editor.css new file mode 100644 index 000000000..3006ed4ef --- /dev/null +++ b/themes/atom-dark-ui/editor.css @@ -0,0 +1,25 @@ +.editor.mini { + border: 1px solid rgba(0, 0, 0, 0.2); + border-bottom: 1px solid rgba(180, 180, 180, 0.2); + border-right: 1px solid rgba(180, 180, 180, 0.2); +} + +.editor .gutter.drop-shadow { + -webkit-box-shadow: -2px 0px 10px 2px #222; +} + +@-webkit-keyframes highlight { + from { background-color: rgba(100, 255, 100, 0.7); } + to { background-color: null; } +} + +.editor .highlighted.selection .region { + -webkit-animation-name: highlight; + -webkit-animation-duration: 1s; + -webkit-animation-iteration-count: 1; +} + +.editor .gutter .line-number.fold { + color: #fba0e3; + opacity: .8; +} diff --git a/themes/Atom - Dark/markdown-preview.css b/themes/atom-dark-ui/markdown-preview.css similarity index 99% rename from themes/Atom - Dark/markdown-preview.css rename to themes/atom-dark-ui/markdown-preview.css index f5274f2df..5d58e2de7 100644 --- a/themes/Atom - Dark/markdown-preview.css +++ b/themes/atom-dark-ui/markdown-preview.css @@ -29,6 +29,10 @@ font-family: Consolas, "Liberation Mono", Courier, monospace; } +.markdown-body a { + color: #4183c4; +} + .markdown-body ol > li { list-style-type: decimal; } diff --git a/themes/Atom - Light/package.json b/themes/atom-dark-ui/package.json similarity index 54% rename from themes/Atom - Light/package.json rename to themes/atom-dark-ui/package.json index 28f90e051..b223f8deb 100644 --- a/themes/Atom - Light/package.json +++ b/themes/atom-dark-ui/package.json @@ -2,20 +2,14 @@ "stylesheets":[ "atom.css", "editor.css", - "grammar-view.css", "select-list.css", "tree-view.css", "tabs.css", - "wrap-guide.css", "status-bar.css", - "symbols-view.css", "markdown-preview.css", - "fuzzy-finder.css", "command-panel.css", - "command-palette.css", "command-logger.css", - "autocomplete.css", - "gists.css", - "go-to-line.css" + "blurred.css", + "bracket-matcher.css" ] } diff --git a/themes/atom-dark-ui/select-list.css b/themes/atom-dark-ui/select-list.css new file mode 100644 index 000000000..235e604ff --- /dev/null +++ b/themes/atom-dark-ui/select-list.css @@ -0,0 +1,44 @@ +.select-list ol li { + background-color: #292929; + border-bottom: 1px solid #1e1e1e; +} + +.select-list li .label { + color: #ddd; +} + +.select-list li .right { + color: #999; + display: inline-block; + margin-top: -3px; +} + +.select-list .key-binding { + background: -webkit-linear-gradient( + rgba(100, 100, 100, 0.5), + rgba(70,70,70, 0.5)); + color: #ccc; + -webkit-box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.1); + display: inline-block; + line-height: 100%; +} + +.select-list li:hover { + background-color: #555; +} + +.select-list ol .selected { + background-image: -webkit-linear-gradient(#7e7e7e, #737373); +} + +.select-list .directory { + color: #777; +} + +.select-list ol .selected .label { + color: #fff; +} + +.select-list ol .selected .directory { + color: #ccc; +} \ No newline at end of file diff --git a/themes/atom-dark-ui/status-bar.css b/themes/atom-dark-ui/status-bar.css new file mode 100644 index 000000000..4ca4f2dc5 --- /dev/null +++ b/themes/atom-dark-ui/status-bar.css @@ -0,0 +1,15 @@ +.status-bar { + background-color: #303030; + border-top: 1px solid #454545; + color: #969696; +} + +.status-bar .git-status.octicons.modified-status-icon { + color: #f78a46; + display: inline-block; +} + +.status-bar .git-status.octicons.new-status-icon { + color: #5293d8; + display: inline-block; +} diff --git a/themes/Atom - Dark/tabs.css b/themes/atom-dark-ui/tabs.css similarity index 65% rename from themes/Atom - Dark/tabs.css rename to themes/atom-dark-ui/tabs.css index 732a5c3aa..81b52334e 100644 --- a/themes/Atom - Dark/tabs.css +++ b/themes/atom-dark-ui/tabs.css @@ -1,13 +1,10 @@ .tabs { background: #333333; border-bottom: 4px solid #424242; - font: caption; - box-shadow: inset 0 -1px 0 #2e2e2e; + box-shadow: inset 0 -1px 0 #2e2e2e, 0 1px 0 #191919; } .tab { - cursor: default; - padding: 2px 21px 2px 9px; background-image: -webkit-linear-gradient(#444, #3d3d3d); border-top: 1px solid #383838; border-right: 1px solid #2e2e2e; @@ -15,6 +12,18 @@ box-shadow: inset 0 0 5px #383838, 0 1px 0 #585858, inset -1px 0 0 #4a4a4a, inset 1px 0 0 #4a4a4a; } + .tab:first-child { + box-shadow: inset 0 0 5px #383838, 0 1px 0 #585858, inset -1px 0 0 #4a4a4a; +} + +.tab.active, +.tab.active:hover { + border-top: 1px solid #4a4a4a; + box-shadow: inset -1px 0 0 #595959, inset 1px 0 0 #595959; + border-bottom-color: #424242; + background-image: -webkit-linear-gradient(#555555, #424242); +} + .tab, .tab .close-icon { color: #aaa; @@ -41,15 +50,6 @@ .tab.file-modified .close-icon { border: 3px solid #777; - border-radius: 10px; -} - -.tab.file-modified .close-icon:before { - content: ""; -} - -.tab:first-child { - box-shadow: inset 0 0 5px #383838, 0 1px 0 #585858, inset -1px 0 0 #4a4a4a; } .tab.active:first-child, @@ -57,67 +57,24 @@ box-shadow: inset -1px 0 0 #595959; } -.tab.active, -.tab.active:hover { - border-top: 1px solid #4a4a4a; - box-shadow: inset -1px 0 0 #595959, inset 1px 0 0 #595959; - border-bottom: 0 none; - background-image: -webkit-linear-gradient(#555555, #424242); -} - .tab.active:before, .tab.active:after { - position: absolute; - bottom: -1px; - width: 4px; - height: 4px; - content: " "; - z-index: 3; border: 1px solid #595959; } + .tab.active:before { border-bottom-right-radius: 4px; border-width: 0 1px 1px 0; box-shadow: 2px 2px 0 #424242; - left: -4px; } + .tab.active:after { - right: -4px; border-bottom-left-radius: 4px; border-width: 0 0 1px 1px; box-shadow: -2px 2px 0 #424242; } -.tab.active:first-child:before { - display: none; -} .tab:hover { color: #c8c8c8; background-image: -webkit-linear-gradient(#474747, #444444); } - -.tab .file-name { - font-size: 11px; - text-shadow: 0 -1px 1px black; -} - -.tab .close-icon { - font-family: 'Octicons Regular'; - font-size: 14px; - width: 14px; - height: 14px; - display: block; - cursor: pointer; - position: absolute; - right: 4px; - top: -1px; - -webkit-font-smoothing: antialiased; -} - -.tab .close-icon:before { - content: "\f081"; -} - -.tab .close-icon:hover { - color: white; -} diff --git a/themes/atom-dark-ui/tree-view.css b/themes/atom-dark-ui/tree-view.css new file mode 100644 index 000000000..a51e58c44 --- /dev/null +++ b/themes/atom-dark-ui/tree-view.css @@ -0,0 +1,63 @@ +.tree-view { + background: #1e1e1e; + border-right: 1px solid #191919; +} + +.tree-view .entry { + text-shadow: 0 -1px 0 #000; +} + +.tree-view .directory.selected > .header > .name, +.tree-view .selected > .name { + color: #d2d2d2; +} + +.tree-view .selected > .highlight { + background-image: -webkit-linear-gradient(#4e4e4e, #434343); +} + +.tree-view:focus .selected > .highlight { + background-image: -webkit-linear-gradient(#7e7e7e, #737373); +} + +.tree-view:focus .directory.selected > .header > .name, +.tree-view:focus .selected > .name { + color: #fff; + text-shadow: 0 -1px 0 #7E4521; +} + +.tree-view .directory .header { + color: #bebebe; +} + +.tree-view .file { + color: #7d7d7d; +} + +.tree-view .entry:hover, +.tree-view .directory .header:hover .name, +.tree-view .directory .header:hover .disclosure-arrow { + color: #ebebeb; +} + +.tree-view .ignored { + color: #555; +} + +.tree-view .modified { + color: #f78a46; +} + +.tree-view .new { + color: #5293d8; +} + +.tree-view-dialog { + background-color: #333; + border-top: 1px solid #555; + -webkit-box-shadow: 0 0 3px 3px rgba(0, 0, 0, .5); +} + +.tree-view-dialog .prompt { + color: #aaa; +} diff --git a/themes/atom-light-syntax.css b/themes/atom-light-syntax.css new file mode 100644 index 000000000..c86205d69 --- /dev/null +++ b/themes/atom-light-syntax.css @@ -0,0 +1,168 @@ +.editor, .editor .gutter { + background-color: #FFFFFF; + color: #555; +} + +.editor.is-focused .cursor { + border-color: #000; +} + +.editor.is-focused .selection .region { + background-color: #afc4da; +} + +.editor.is-focused .line-number.cursor-line-no-selection, .editor.is-focused .line.cursor-line { + background-color: rgba(255, 255, 134, 0.34); +} + +.editor .comment { + color: #999988; + font-style: italic; +} + +.editor .string { + color: #D14; +} + +.editor .constant.numeric { + color: #D14; +} + +.editor .constant.language { + color: #606aa1; +} + +.editor .constant.character, .editor .constant.other { + color: #606aa1; +} + +.editor .constant.symbol { + color: #990073; +} + +.editor .variable { + color: #008080; +} + +/* Keywords */ +.editor .keyword { + color: #222; + font-weight: bold; +} + +.editor .keyword.unit { + color: #445588; +} + +.editor .keyword.special-method { + color: #0086B3; +} + +.editor .storage { + color: #222; +} + +.editor .storage.type { + color: #222; +} + +.editor .entity.name.class { + text-decoration: underline; + color: #606aa1; +} + +.editor .entity.other.inherited-class { + text-decoration: underline; + color: #606aa1; +} + +.editor .entity.name.function { + color: #900; +} + +.editor .variable.parameter { + color: #606aa1; +} + +.editor .entity.name.tag { + color: #008080; +} + +.editor .entity.other.attribute-name { + color: #458; + font-weight: bold; +} + +.editor .support.function { + color: #458; +} + +.editor .support.constant { + color: #458; +} + +.editor .support.type { + color: #458; +} + +.editor .support.class { + color: #008080; +} + +.editor .invalid { + color: #F8F8F0; + background-color: #00A8C6; +} + +.editor .invalid.deprecated { + color: #F8F8F0; + background-color: #8FBE00; +} + +.editor .meta.structure.dictionary.json > .string.quoted.double.json, +.editor .meta.structure.dictionary.json > .string.quoted.double.json .punctuation.string { + color: #000080; +} + +.editor .meta.structure.dictionary.value.json > .string.quoted.double.json { + color: #d14; +} + +.editor .meta.diff, .editor .meta.diff.header { + color: #75715E; +} + +.editor .meta.function span { + color: #990000; + font-weight: bold; +} + +.editor .markup.deleted { + color: #00A8C6; +} + +.editor .markup.inserted { + color: #A6E22E; +} + +.editor .markup.changed { + color: #E6DB74; +} + +.editor .constant.numeric.line-number.find-in-files .- .match { + color: rgba(143, 190, 0, 0.63); +} + +.editor .entity.name.filename.find-in-files { + color: #E6DB74; +} + +/* CSS Styles */ +.editor .css.support.property-name { + font-weight: bold; + color: #333; +} + +.editor .css.constant { + color: #099; +} \ No newline at end of file diff --git a/themes/atom-light-ui/atom.css b/themes/atom-light-ui/atom.css new file mode 100644 index 000000000..76f2e989b --- /dev/null +++ b/themes/atom-light-ui/atom.css @@ -0,0 +1,26 @@ +html, body, +#root-view { + font: caption; + background-color: #f4f4f4; +} + +#root-view #panes .row > * + * { + -webkit-box-shadow: + -2px 0 rgba(0, 0, 0, 0.3), + -1px 0 rgba(255, 255, 255, 0.2); +} + +#root-view #panes .column > * + * { + -webkit-box-shadow: + 0 -2px rgba(0, 0, 0, 0.3), + 0 -1px rgba(255, 255, 255, 0.2); +} + +.error { + background: #991212 !important; + -webkit-transition: background 300ms ease-out; +} + +.wrap-guide { + background: rgba(150, 150, 150, 0.1); +} \ No newline at end of file diff --git a/themes/atom-light-ui/blurred.css b/themes/atom-light-ui/blurred.css new file mode 100644 index 000000000..cc085ff29 --- /dev/null +++ b/themes/atom-light-ui/blurred.css @@ -0,0 +1,38 @@ +.is-blurred .tab { + background-image: none; + background-color: #e0e0e0); + border-top: none; + border-right: 1px solid #959595; + border-bottom: 1px solid #959595; + box-shadow: inset 0 0 5px #eee, 0 1px 0 #eee, inset -1px 0 0 #e0e0e0, inset 1px 0 0 #e0e0e0; + color: #323232; +} + +.is-blurred .tab.active { + border-bottom: 1px solid #e5e5e5; + box-shadow: inset 0 0 5px #eee, inset -1px 0 0 #e0e0e0, inset 1px 0 0 #e0e0e0; +} + +.is-blurred .tree-view { + background: #f3f3f3; + border-right: 1px solid #c5c5c5; +} + +.is-blurred .tree-view:focus .selected > .highlight { + border-top: 1px solid #3D4552; + border-bottom: 1px solid #3D4552; + background-image: none; + background-color: #69717b; +} + +.is-blurred .tree-view .selected > .highlight { + box-sizing: border-box; + border-top: 1px solid #97a4a7; + border-bottom: 1px solid #97a4a7; + background-image: none; + background-color: #DFDFDF; +} + +.is-blurred .tree-view .name:before { + color: #7e7e7e; +} diff --git a/themes/atom-light-ui/bracket-matcher.css b/themes/atom-light-ui/bracket-matcher.css new file mode 100644 index 000000000..d579fdc35 --- /dev/null +++ b/themes/atom-light-ui/bracket-matcher.css @@ -0,0 +1,5 @@ +.bracket-matcher { + border-bottom: 1px solid #f8de7e; + margin-top: -1px; + opacity: .7; +} diff --git a/themes/Atom - Light/command-logger.css b/themes/atom-light-ui/command-logger.css similarity index 100% rename from themes/Atom - Light/command-logger.css rename to themes/atom-light-ui/command-logger.css diff --git a/themes/atom-light-ui/command-panel.css b/themes/atom-light-ui/command-panel.css new file mode 100644 index 000000000..34ced33e7 --- /dev/null +++ b/themes/atom-light-ui/command-panel.css @@ -0,0 +1,35 @@ +.command-panel { + background-color: #f4f4f4; + border-top: 1px solid #979797; + color: #ededed; +} + +.command-panel .preview-list { + background-color: #e7e7e7; + color: #222; + border: 1px solid #989898; +} + +.command-panel .preview-count { + color: #333; +} + +.command-panel .preview-list li.selected, .command-panel .preview-list li.operation:hover { + background-color: rgba(255, 255, 255, .6); +} + +.command-panel .preview-list .path { + color: #3D5075; +} + +.command-panel .preview-list .line-number { + color: #3D5075; +} + +.command-panel .preview-list .path-match-number { + color: #3D5075; +} + +.command-panel .preview-list .preview .match { + background-color: #c8d4d7; +} \ No newline at end of file diff --git a/themes/atom-light-ui/editor.css b/themes/atom-light-ui/editor.css new file mode 100644 index 000000000..23d8a2eaf --- /dev/null +++ b/themes/atom-light-ui/editor.css @@ -0,0 +1,31 @@ +.editor.mini { + border: 1px solid rgba(0, 0, 0, 0.2); + border-bottom: 1px solid rgba(180, 180, 180, 0.3); + border-right: 1px solid rgba(180, 180, 180, 0.3); + background-color: #aaa; + color: #333; +} + +.editor.mini .cursor { + background: #333; +} + +.editor .gutter.drop-shadow { + -webkit-box-shadow: -2px 0px 10px 2px #222; +} + +@-webkit-keyframes highlight { + from { background-color: rgba(100, 255, 100, 0.7); } + to { background-color: null; } +} + +.editor .highlighted.selection .region { + -webkit-animation-name: highlight; + -webkit-animation-duration: 1s; + -webkit-animation-iteration-count: 1; +} + +.editor .gutter .line-number.fold { + color: #fba0e3; + opacity: .8; +} \ No newline at end of file diff --git a/themes/Atom - Light/markdown-preview.css b/themes/atom-light-ui/markdown-preview.css similarity index 99% rename from themes/Atom - Light/markdown-preview.css rename to themes/atom-light-ui/markdown-preview.css index f5274f2df..5d58e2de7 100644 --- a/themes/Atom - Light/markdown-preview.css +++ b/themes/atom-light-ui/markdown-preview.css @@ -29,6 +29,10 @@ font-family: Consolas, "Liberation Mono", Courier, monospace; } +.markdown-body a { + color: #4183c4; +} + .markdown-body ol > li { list-style-type: decimal; } diff --git a/themes/Atom - Dark/package.json b/themes/atom-light-ui/package.json similarity index 54% rename from themes/Atom - Dark/package.json rename to themes/atom-light-ui/package.json index 28f90e051..b223f8deb 100644 --- a/themes/Atom - Dark/package.json +++ b/themes/atom-light-ui/package.json @@ -2,20 +2,14 @@ "stylesheets":[ "atom.css", "editor.css", - "grammar-view.css", "select-list.css", "tree-view.css", "tabs.css", - "wrap-guide.css", "status-bar.css", - "symbols-view.css", "markdown-preview.css", - "fuzzy-finder.css", "command-panel.css", - "command-palette.css", "command-logger.css", - "autocomplete.css", - "gists.css", - "go-to-line.css" + "blurred.css", + "bracket-matcher.css" ] } diff --git a/themes/atom-light-ui/select-list.css b/themes/atom-light-ui/select-list.css new file mode 100644 index 000000000..f59cb7bce --- /dev/null +++ b/themes/atom-light-ui/select-list.css @@ -0,0 +1,23 @@ +.select-list { + background-color: #e5e5e5; + border: 1px solid #c6c6c6; + color: #323232; + box-shadow: 0 0 10px #555; +} + +.select-list ol { + border: 1px solid #d2d2d2; +} + +.select-list ol li { + background-color: #f5f5f5; + border-bottom: 1px solid #ccc; +} + +.select-list li:hover { + background-color: #f9f9f9; +} + +.select-list ol .selected { + background-color: #e0e0e0; +} \ No newline at end of file diff --git a/themes/atom-light-ui/status-bar.css b/themes/atom-light-ui/status-bar.css new file mode 100644 index 000000000..201be6e28 --- /dev/null +++ b/themes/atom-light-ui/status-bar.css @@ -0,0 +1,15 @@ +.status-bar { + background-color: #e5e5e5; + border-top: 1px solid #959595; + color: #333; +} + +.status-bar .git-status.octicons.modified-status-icon { + color: #f78a46; + display: inline-block; +} + +.status-bar .git-status.octicons.new-status-icon { + color: #5293d8; + display: inline-block; +} \ No newline at end of file diff --git a/themes/Atom - Light/tabs.css b/themes/atom-light-ui/tabs.css similarity index 70% rename from themes/Atom - Light/tabs.css rename to themes/atom-light-ui/tabs.css index 557b00b39..6c37140a7 100644 --- a/themes/Atom - Light/tabs.css +++ b/themes/atom-light-ui/tabs.css @@ -1,13 +1,10 @@ .tabs { background: #e3e3e3; border-bottom: 4px solid #e5e5e5; - font: caption; - box-shadow: inset 0 -1px 0 #959595; + box-shadow: inset 0 -1px 0 #959595, 0 1px 0 #989898; } .tab { - cursor: default; - padding: 2px 21px 2px 9px; background-image: -webkit-linear-gradient(#e0e0e0, #bfbfbf); border-top: none; border-right: 1px solid #959595; @@ -48,69 +45,40 @@ border-radius: 10px; } -.tab.file-modified .close-icon:before { - content: ""; -} - .tab.active, .tab.active:hover { - border-bottom: 0 none; + border-bottom-color: #e5e5e5; box-shadow: inset -1px 0 0 #e0e0e0, inset 1px 0 0 #e0e0e0; background-image: -webkit-linear-gradient(#fefefe, #e7e6e7); } .tab.active:before, .tab.active:after { - position: absolute; - bottom: 0; - width: 4px; - height: 4px; - content: " "; - z-index: 3; border: 1px solid #959595; } + .tab.active:before { + left: -5px; border-bottom-right-radius: 4px; border-width: 0 1px 1px 0; box-shadow: 2px 2px 0 #e5e5e5; - left: -5px; } + .tab.active:after { right: -5px; border-bottom-left-radius: 4px; border-width: 0 0 1px 1px; box-shadow: -2px 2px 0 #e5e5e5; } -.tab.active:first-child:before { - display: none; -} .tab:hover { background-image: -webkit-linear-gradient(#e2e2e2, #e0e0e0); } .tab .file-name { - font-size: 11px; text-shadow: 0 1px 0 #e0e0e0; } -.tab .close-icon { - font-family: 'Octicons Regular'; - font-size: 14px; - width: 14px; - height: 14px; - display: block; - cursor: pointer; - position: absolute; - right: 4px; - top: -1px; - -webkit-font-smoothing: antialiased; -} - -.tab .close-icon:before { - content: "\f081"; -} - .tab .close-icon:hover { color: #aaa; } diff --git a/themes/atom-light-ui/tree-view.css b/themes/atom-light-ui/tree-view.css new file mode 100644 index 000000000..ff18a5990 --- /dev/null +++ b/themes/atom-light-ui/tree-view.css @@ -0,0 +1,86 @@ +.tree-view { + background: #dde3e8; + border-right: 1px solid #989898; +} + +.tree-view .entry { + text-shadow: 0 1px 0 #fff; +} + +.tree-view .directory.selected > .header > .name, +.tree-view .selected > .name { + color: #262626; +} + +.tree-view .selected > .highlight { + box-sizing: border-box; + border-top: 1px solid #97a4a7; + border-bottom: 1px solid #97a4a7; + box-shadow: 0 -1px 0 #dde4e6, 0 1px 0 #dde4e6; + background-image: -webkit-linear-gradient(#cad5d8, #bcccce); +} + +.tree-view:focus .selected > .highlight { + border-top: 1px solid #3D4552; + border-bottom: 1px solid #3D4552; + background-image: -webkit-linear-gradient(#7e868d, #69717b); +} + +.tree-view:focus .directory.selected > .header > .name, +.tree-view:focus .selected > .name, +.tree-view:focus .directory.selected > .header > .name:before, +.tree-view:focus .selected > .name:before { + color: #fff; + text-shadow: 0 1px 0 #000; +} + +.tree-view .directory .header { + color: #262626; +} + +.tree-view .file { + color: #262626; +} + +.tree-view .name:before { + color: #7e8692; +} + + +.tree-view .entry:hover, +.tree-view .directory .header:hover .name, +.tree-view .directory .header:hover .disclosure-arrow { + color: #4e5666; +} + +.tree-view .directory .header .disclosure-arrow, +.tree-view .directory .header:hover .disclosure-arrow { + text-shadow: none; + color: #4e5666; +} + +.tree-view:focus .directory .header .disclosure-arrow, +.tree-view:focus .directory .header:hover .disclosure-arrow { + color: #3D4552; +} + +.tree-view .ignored { + color: #555; +} + +.tree-view .modified { + color: #f78a46; +} + +.tree-view .new { + color: #5293d8; +} + +.tree-view-dialog { + background-color: #e7e7e7; + border-top: 1px solid #989898; +} + +.tree-view-dialog .prompt { + color: #333; +} \ No newline at end of file diff --git a/vendor/jasmine-atom-reporter.js b/vendor/jasmine-atom-reporter.js index dd896b373..6feb1dd41 100644 --- a/vendor/jasmine-atom-reporter.js +++ b/vendor/jasmine-atom-reporter.js @@ -1,10 +1,6 @@ -jasmine.AtomReporter = function(doc) { - this.document = doc || document; - this.suiteDivs = {}; - this.logRunningSpecs = false; -}; +jasmine.AtomReporterHelpers = {}; -jasmine.AtomReporter.prototype.createDom = function(type, attrs, childrenVarArgs) { +jasmine.AtomReporterHelpers.createDom = function(type, attrs, childrenVarArgs) { var el = document.createElement(type); for (var i = 2; i < arguments.length; i++) { @@ -13,7 +9,9 @@ jasmine.AtomReporter.prototype.createDom = function(type, attrs, childrenVarArgs if (typeof child === 'string') { el.appendChild(document.createTextNode(child)); } else { - if (child) { el.appendChild(child); } + if (child) { + el.appendChild(child); + } } } @@ -28,117 +26,344 @@ jasmine.AtomReporter.prototype.createDom = function(type, attrs, childrenVarArgs return el; }; -jasmine.AtomReporter.prototype.reportRunnerStarting = function(runner) { - var showPassed, showSkipped; - - this.outerDiv = this.createDom('div', { className: 'jasmine_reporter' }, - this.createDom('div', { className: 'banner' }, - this.createDom('div', { className: 'logo' }, - this.createDom('span', { className: 'title' }, "Jasmine"), - this.createDom('span', { className: 'version' }, runner.env.versionString())), - this.createDom('div', { className: 'options' }, - "Show ", - showPassed = this.createDom('input', { id: "__jasmine_AtomReporter_showPassed__", type: 'checkbox' }), - this.createDom('label', { "for": "__jasmine_AtomReporter_showPassed__" }, " passed "), - showSkipped = this.createDom('input', { id: "__jasmine_AtomReporter_showSkipped__", type: 'checkbox' }), - this.createDom('label', { "for": "__jasmine_AtomReporter_showSkipped__" }, " skipped") - ) - ), - - this.runnerDiv = this.createDom('div', { className: 'runner running' }, - this.createDom('a', { className: 'run_spec', href: '?' }, "run all"), - this.runnerMessageSpan = this.createDom('span', {}, "Running..."), - this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, "")) - ); - - this.document.body.appendChild(this.outerDiv); - - var suites = runner.suites(); - for (var i = 0; i < suites.length; i++) { - var suite = suites[i]; - var suiteDiv = this.createDom('div', { className: 'suite' }, - this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"), - this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description)); - this.suiteDivs[suite.id] = suiteDiv; - var parentDiv = this.outerDiv; - if (suite.parentSuite) { - parentDiv = this.suiteDivs[suite.parentSuite.id]; - } - parentDiv.appendChild(suiteDiv); - } - - this.startedAt = new Date(); - - var self = this; - showPassed.onclick = function(evt) { - if (showPassed.checked) { - self.outerDiv.className += ' show-passed'; - } else { - self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, ''); - } - }; - - showSkipped.onclick = function(evt) { - if (showSkipped.checked) { - self.outerDiv.className += ' show-skipped'; - } else { - self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, ''); - } - }; -}; - -jasmine.AtomReporter.prototype.reportRunnerResults = function(runner) { - var results = runner.results(); - var className = (results.failedCount > 0) ? "runner failed" : "runner passed"; - this.runnerDiv.setAttribute("class", className); - //do it twice for IE - this.runnerDiv.setAttribute("className", className); - var specs = runner.specs(); - var specCount = 0; - for (var i = 0; i < specs.length; i++) { - if (this.specFilter(specs[i])) { - specCount++; - } - } - var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s"); - message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"; - this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild); - - this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString())); -}; - -jasmine.AtomReporter.prototype.reportSuiteResults = function(suite) { - var results = suite.results(); - var status = results.passed() ? 'passed' : 'failed'; - if (results.totalCount === 0) { // todo: change this to check results.skipped - status = 'skipped'; - } - this.suiteDivs[suite.id].className += " " + status; -}; - -jasmine.AtomReporter.prototype.reportSpecStarting = function(spec) { - if (this.logRunningSpecs) { - this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); - } -}; - -jasmine.AtomReporter.prototype.reportSpecResults = function(spec) { - var results = spec.results(); +jasmine.AtomReporterHelpers.getSpecStatus = function(child) { + var results = child.results(); var status = results.passed() ? 'passed' : 'failed'; if (results.skipped) { status = 'skipped'; } - var specDiv = this.createDom('div', { className: 'spec ' + status }, - this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"), + + return status; +}; + +jasmine.AtomReporterHelpers.appendToSummary = function(child, childElement) { + var parentDiv = this.dom.summary; + var parentSuite = (typeof child.parentSuite == 'undefined') ? 'suite' : 'parentSuite'; + var parent = child[parentSuite]; + + if (parent) { + if (typeof this.views.suites[parent.id] == 'undefined') { + this.views.suites[parent.id] = new jasmine.AtomReporter.SuiteView(parent, this.dom, this.views); + } + parentDiv = this.views.suites[parent.id].element; + } + + parentDiv.appendChild(childElement); +}; + + +jasmine.AtomReporterHelpers.addHelpers = function(ctor) { + for(var fn in jasmine.AtomReporterHelpers) { + ctor.prototype[fn] = jasmine.AtomReporterHelpers[fn]; + } +}; + +jasmine.AtomReporter = function(_doc) { + var self = this; + var doc = _doc || window.document; + + var reporterView; + + var dom = {}; + + // Jasmine Reporter Public Interface + self.logRunningSpecs = false; + + self.reportRunnerStarting = function(runner) { + var specs = runner.specs() || []; + + if (specs.length == 0) { + return; + } + + createReporterDom(); + doc.body.appendChild(dom.reporter); + + reporterView = new jasmine.AtomReporter.ReporterView(dom); + reporterView.addSpecs(specs, self.specFilter); + }; + + self.reportRunnerResults = function(runner) { + reporterView && reporterView.complete(); + }; + + self.reportSuiteResults = function(suite) { + reporterView.suiteComplete(suite); + }; + + self.reportSpecStarting = function(spec) { + if (self.logRunningSpecs) { + self.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); + } + + reporterView.specStarted(spec); + }; + + self.reportSpecResults = function(spec) { + reporterView.specComplete(spec); + }; + + self.log = function() { + var console = jasmine.getGlobal().console; + if (console && console.log) { + if (console.log.apply) { + console.log.apply(console, arguments); + } else { + console.log(arguments); // ie fix: console.log.apply doesn't exist on ie + } + } + }; + + self.specFilter = function(spec) { + return self.fSpecFilter(spec); + }; + + return self; + + function createReporterDom() { + dom.reporter = self.createDom('div', { id: 'HTMLReporter', className: 'jasmine_reporter' }, + dom.banner = self.createDom('div', { className: 'banner' }, + self.createDom('span', { className: 'title' }, "Atom "), + self.createDom('span', { className: 'version' }, "0.0.0")), + + dom.symbolSummary = self.createDom('ul', {className: 'symbolSummary'}), + dom.alert = self.createDom('div', {className: 'alert'}), + dom.results = self.createDom('div', {className: 'results'}, + dom.summary = self.createDom('div', { className: 'summary' }), + dom.details = self.createDom('div', { id: 'details' })) + ); + } + + function searchWithCatch() { + var params = jasmine.AtomReporter.parameters(window.document); + var removed = false; + var i = 0; + + while (!removed && i < params.length) { + if (params[i].match(/catch=/)) { + params.splice(i, 1); + removed = true; + } + i++; + } + if (jasmine.CATCH_EXCEPTIONS) { + params.push("catch=false"); + } + + return params.join("&"); + } +}; +jasmine.AtomReporter.parameters = function(doc) { + var paramStr = doc.location.search.substring(1); + var params = []; + + if (paramStr.length > 0) { + params = paramStr.split('&'); + } + return params; +} +jasmine.AtomReporter.sectionLink = function(sectionName) { + var link = '?'; + var params = []; + + if (sectionName) { + params.push('spec=' + encodeURIComponent(sectionName)); + } + if (!jasmine.CATCH_EXCEPTIONS) { + params.push("catch=false"); + } + if (params.length > 0) { + link += params.join("&"); + } + + return link; +}; +jasmine.AtomReporterHelpers.addHelpers(jasmine.AtomReporter); +jasmine.AtomReporter.ReporterView = function(dom) { + this.startedAt = new Date(); + this.runningSpecCount = 0; + this.completeSpecCount = 0; + this.passedCount = 0; + this.failedCount = 0; + this.skippedCount = 0; + + this.createResultsMenu = function() { + this.resultsMenu = this.createDom('span', {className: 'resultsMenu bar'}, + this.summaryMenuItem = this.createDom('span', {className: 'summaryMenuItem'}, '0 specs'), + ' | ', + this.detailsMenuItem = this.createDom('span', {className: 'detailsMenuItem'}, '0 failing'), + this.currentSpecMenuItem = this.createDom('div', {className: 'currentSpecMenuItem'}, '') + ); + + }; + + this.addSpecs = function(specs, specFilter) { + this.totalSpecCount = specs.length; + + this.views = { + specs: {}, + suites: {} + }; + + for (var i = 0; i < specs.length; i++) { + var spec = specs[i]; + this.views.specs[spec.id] = new jasmine.AtomReporter.SpecView(spec, dom, this.views); + if (specFilter(spec)) { + this.runningSpecCount++; + } + } + }; + + this.specStarted = function(spec) { + if (this.currentSpecMenuItem) { + this.currentSpecMenuItem.innerHTML = spec.getFullName() + } + } + + this.specComplete = function(spec) { + this.completeSpecCount++; + + if (isUndefined(this.views.specs[spec.id])) { + this.views.specs[spec.id] = new jasmine.AtomReporter.SpecView(spec, dom); + } + + var specView = this.views.specs[spec.id]; + + switch (specView.status()) { + case 'passed': + this.passedCount++; + break; + + case 'failed': + this.failedCount++; + break; + + case 'skipped': + this.skippedCount++; + break; + } + + specView.refresh(spec); + this.refresh(); + }; + + this.suiteComplete = function(suite) { + var suiteView = this.views.suites[suite.id]; + if (isUndefined(suiteView)) { + return; + } + suiteView.refresh(); + }; + + this.refresh = function() { + if (isUndefined(this.resultsMenu)) { + this.createResultsMenu(); + dom.alert.appendChild(this.resultsMenu); + } + + // summary info + this.summaryMenuItem.innerHTML = "" + specPluralizedFor(this.runningSpecCount); + this.detailsMenuItem.innerHTML = "" + this.failedCount + " failing"; + }; + + this.complete = function() { + dom.alert.removeChild(this.resultsMenu); + var skippedSpecs = this.skippedCount == 0 ? "" : " (" + this.skippedCount + " specs skipped)" + + if (this.failedCount === 0) { + dom.alert.appendChild(this.createDom('span', {className: 'passingAlert bar'}, this.passedCount + "/" + specPluralizedFor(this.totalSpecCount - this.skippedCount) + " passed" + skippedSpecs)); + } else { + dom.alert.appendChild(this.createDom('span', {className: 'failingAlert bar'}, specPluralizedFor(this.failedCount) + " failed" + skippedSpecs)); + showDetails(); + } + + dom.banner.appendChild(this.createDom('span', {className: 'duration'}, "finished in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s")); + }; + + return this; + + function showDetails() { + if (dom.reporter.className.search(/showDetails/) === -1) { + dom.reporter.className += " showDetails"; + } + } + + function isUndefined(obj) { + return typeof obj === 'undefined'; + } + + function isDefined(obj) { + return !isUndefined(obj); + } + + function specPluralizedFor(count) { + var str = count + " spec"; + if (count > 1) { + str += "s" + } + return str; + } + +}; + +jasmine.AtomReporterHelpers.addHelpers(jasmine.AtomReporter.ReporterView); + + +jasmine.AtomReporter.SpecView = function(spec, dom, views) { + this.spec = spec; + this.dom = dom; + this.views = views; + + this.symbol = this.createDom('li', { className: 'pending' }); + this.dom.symbolSummary.appendChild(this.symbol); + + this.summary = this.createDom('div', { className: 'specSummary' }, + this.createDom('a', { + className: 'description', + href: jasmine.AtomReporter.sectionLink(this.spec.getFullName()), + title: this.spec.getFullName() + }, this.spec.description) + ); + + this.detail = this.createDom('div', { className: 'specDetail' }, this.createDom('a', { className: 'description', - href: '?spec=' + encodeURIComponent(spec.getFullName()), - title: spec.getFullName() - }, spec.description)); + href: '?spec=' + encodeURIComponent(this.spec.getFullName()), + title: this.spec.getFullName() + }, this.spec.getFullName()) + ); +}; +jasmine.AtomReporter.SpecView.prototype.status = function() { + return this.getSpecStatus(this.spec); +}; - var resultItems = results.getItems(); +jasmine.AtomReporter.SpecView.prototype.refresh = function() { + this.symbol.className = this.status(); + + switch (this.status()) { + case 'skipped': + case 'passed': + break; + + case 'failed': + this.appendSummaryToSuiteDiv(); + this.appendFailureDetail(); + break; + } +}; + +jasmine.AtomReporter.SpecView.prototype.appendSummaryToSuiteDiv = function() { + this.summary.className += ' ' + this.status(); + this.appendToSummary(this.spec, this.summary); +}; + +jasmine.AtomReporter.SpecView.prototype.appendFailureDetail = function() { + this.detail.className += ' ' + this.status(); + + var resultItems = this.spec.results().getItems(); var messagesDiv = this.createDom('div', { className: 'messages' }); + for (var i = 0; i < resultItems.length; i++) { var result = resultItems[i]; @@ -154,37 +379,29 @@ jasmine.AtomReporter.prototype.reportSpecResults = function(spec) { } if (messagesDiv.childNodes.length > 0) { - specDiv.appendChild(messagesDiv); - } - - this.suiteDivs[spec.suite.id].appendChild(specDiv); -}; - -jasmine.AtomReporter.prototype.log = function() { - var console = jasmine.getGlobal().console; - if (console && console.log) { - if (console.log.apply) { - console.log.apply(console, arguments); - } else { - console.log(arguments); // ie fix: console.log.apply doesn't exist on ie - } + this.detail.appendChild(messagesDiv); + this.dom.details.appendChild(this.detail); } }; -jasmine.AtomReporter.prototype.getLocation = function() { - return this.document.location; +jasmine.AtomReporterHelpers.addHelpers(jasmine.AtomReporter.SpecView);jasmine.AtomReporter.SuiteView = function(suite, dom, views) { + this.suite = suite; + this.dom = dom; + this.views = views; + + this.element = this.createDom('div', { className: 'suite' }, + this.createDom('a', { className: 'description', href: jasmine.AtomReporter.sectionLink(this.suite.getFullName()) }, this.suite.description) + ); + + this.appendToSummary(this.suite, this.element); }; -jasmine.AtomReporter.prototype.specFilter = function(spec) { - var paramMap = {}; - var params = this.getLocation().search.substring(1).split('&'); - for (var i = 0; i < params.length; i++) { - var p = params[i].split('='); - paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); - } - - if (!paramMap.spec) { - return true; - } - return spec.getFullName().indexOf(paramMap.spec) === 0; +jasmine.AtomReporter.SuiteView.prototype.status = function() { + return this.getSpecStatus(this.suite); }; + +jasmine.AtomReporter.SuiteView.prototype.refresh = function() { + this.element.className += " " + this.status(); +}; + +jasmine.AtomReporterHelpers.addHelpers(jasmine.AtomReporter.SuiteView); \ No newline at end of file diff --git a/vendor/jasmine-console-reporter.js b/vendor/jasmine-console-reporter.js index fc6eb75d7..46fa75a29 100644 --- a/vendor/jasmine-console-reporter.js +++ b/vendor/jasmine-console-reporter.js @@ -31,7 +31,9 @@ jasmine.ConsoleReporter.prototype.reportSpecResults = function(spec) { for (var i = 0; i < resultItems.length; i++) { var result = resultItems[i]; if (this.logErrors && result.type == 'expect' && result.passed && !result.passed()) { - console.log("ERROR: " + spec.getFullName()) + message = spec.getFullName() + console.log("\n\n" + message) + console.log((new Array(message.length + 1)).join('-')) if (result.trace.stack) { console.log(result.trace.stack) } @@ -43,9 +45,5 @@ jasmine.ConsoleReporter.prototype.reportSpecResults = function(spec) { }; jasmine.ConsoleReporter.prototype.specFilter = function(spec) { - if (!jasmine.getEnv().focusPriority) { - return true; - } - - return fSpecFilter(spec); + return true; }; diff --git a/vendor/jasmine-focused.js b/vendor/jasmine-focused.js index c10b1b976..6ffb8b940 100644 --- a/vendor/jasmine-focused.js +++ b/vendor/jasmine-focused.js @@ -36,28 +36,12 @@ var fffit = function(description, specDefinitions) { fit(description, specDefinitions, 3); }; -var fSpecFilter = function(specOrSuite) { +jasmine.AtomReporter.prototype.fSpecFilter = function(specOrSuite) { globalFocusPriority = jasmine.getEnv().focusPriority; if (!globalFocusPriority) return true; if (specOrSuite.focusPriority >= globalFocusPriority) return true; var parent = specOrSuite.parentSuite || specOrSuite.suite; if (!parent) return false; - return fSpecFilter(parent); -} - -jasmine.AtomReporter.prototype.specFilter = function(spec) { - var paramMap = {}; - var params = this.getLocation().search.substring(1).split('&'); - for (var i = 0; i < params.length; i++) { - var p = params[i].split('='); - paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); - } - - if (!paramMap.spec && !jasmine.getEnv().focusPriority) { - return true; - } - - return (spec.getFullName().indexOf(paramMap.spec) === 0) || fSpecFilter(spec); -}; - + return this.fSpecFilter(parent); +}; \ No newline at end of file diff --git a/vendor/jasmine-helper.coffee b/vendor/jasmine-helper.coffee index 0defd1990..d20f96573 100644 --- a/vendor/jasmine-helper.coffee +++ b/vendor/jasmine-helper.coffee @@ -8,9 +8,6 @@ module.exports.runSpecSuite = (specSuite, logErrors=true) -> $ = require 'jquery' TimeReporter = require 'time-reporter' - $('body').append $$ -> - @div id: 'jasmine-content' - reporter = if atom.exitWhenDone new jasmine.ConsoleReporter(document, logErrors) else @@ -22,4 +19,8 @@ module.exports.runSpecSuite = (specSuite, logErrors=true) -> jasmineEnv.addReporter(new TimeReporter()) jasmineEnv.specFilter = (spec) -> reporter.specFilter(spec) + + $('body').append $$ -> + @div id: 'jasmine-content' + jasmineEnv.execute() diff --git a/vendor/jasmine.js b/vendor/jasmine.js index 2a5b5ad58..0da28abfa 100644 --- a/vendor/jasmine.js +++ b/vendor/jasmine.js @@ -1,8 +1,10 @@ -// Modified line 1769 -// - if (self.blocks[self.index].abort) { -// + if (self.blocks[self.index] && self.blocks[self.index].abort) { +// Modified line +// - var isCommonJS = typeof window == "undefined" && typeof exports == "object"; +// + +// +// Modified method jasmine.WaitsForBlock.prototype.execute -var isCommonJS = typeof window == "undefined"; +var isCommonJS = typeof window == "undefined" && typeof exports == "object"; /** * Top level namespace for Jasmine, a lightweight JavaScript BDD/spec/testing framework. @@ -38,11 +40,23 @@ jasmine.VERBOSE = false; */ jasmine.DEFAULT_UPDATE_INTERVAL = 250; +/** + * Maximum levels of nesting that will be included when an object is pretty-printed + */ +jasmine.MAX_PRETTY_PRINT_DEPTH = 40; + /** * Default timeout interval in milliseconds for waitsFor() blocks. */ jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; +/** + * By default exceptions thrown in the context of a test are caught by jasmine so that it can run the remaining tests in the suite. + * Set to false to let the exception bubble up in the browser. + * + */ +jasmine.CATCH_EXCEPTIONS = true; + jasmine.getGlobal = function() { function getGlobal() { return window; @@ -200,6 +214,21 @@ jasmine.any = function(clazz) { return new jasmine.Matchers.Any(clazz); }; +/** + * Returns a matchable subset of a JSON object. For use in expectations when you don't care about all of the + * attributes on the object. + * + * @example + * // don't care about any other attributes than foo. + * expect(mySpy).toHaveBeenCalledWith(jasmine.objectContaining({foo: "bar"}); + * + * @param sample {Object} sample + * @returns matchable object for the sample + */ +jasmine.objectContaining = function (sample) { + return new jasmine.Matchers.ObjectContaining(sample); +}; + /** * Jasmine Spies are test doubles that can act as stubs, spies, fakes or when used in an expecation, mocks. * @@ -452,7 +481,7 @@ jasmine.log = function() { * @see jasmine.createSpy * @param obj * @param methodName - * @returns a Jasmine spy that can be chained with all spy methods + * @return {jasmine.Spy} a Jasmine spy that can be chained with all spy methods */ var spyOn = function(obj, methodName) { return jasmine.getEnv().currentSpec.spyOn(obj, methodName); @@ -497,6 +526,7 @@ if (isCommonJS) exports.xit = xit; * jasmine.Matchers functions. * * @param {Object} actual Actual value to test against and expected value + * @return {jasmine.Matchers} */ var expect = function(actual) { return jasmine.getEnv().currentSpec.expect(actual); @@ -856,6 +886,25 @@ jasmine.Env.prototype.xit = function(desc, func) { }; }; +jasmine.Env.prototype.compareRegExps_ = function(a, b, mismatchKeys, mismatchValues) { + if (a.source != b.source) + mismatchValues.push("expected pattern /" + b.source + "/ is not equal to the pattern /" + a.source + "/"); + + if (a.ignoreCase != b.ignoreCase) + mismatchValues.push("expected modifier i was" + (b.ignoreCase ? " " : " not ") + "set and does not equal the origin modifier"); + + if (a.global != b.global) + mismatchValues.push("expected modifier g was" + (b.global ? " " : " not ") + "set and does not equal the origin modifier"); + + if (a.multiline != b.multiline) + mismatchValues.push("expected modifier m was" + (b.multiline ? " " : " not ") + "set and does not equal the origin modifier"); + + if (a.sticky != b.sticky) + mismatchValues.push("expected modifier y was" + (b.sticky ? " " : " not ") + "set and does not equal the origin modifier"); + + return (mismatchValues.length === 0); +}; + jasmine.Env.prototype.compareObjects_ = function(a, b, mismatchKeys, mismatchValues) { if (a.__Jasmine_been_here_before__ === b && b.__Jasmine_been_here_before__ === a) { return true; @@ -918,11 +967,19 @@ jasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) { return a.getTime() == b.getTime(); } - if (a instanceof jasmine.Matchers.Any) { + if (a.jasmineMatches) { + return a.jasmineMatches(b); + } + + if (b.jasmineMatches) { + return b.jasmineMatches(a); + } + + if (a instanceof jasmine.Matchers.ObjectContaining) { return a.matches(b); } - if (b instanceof jasmine.Matchers.Any) { + if (b instanceof jasmine.Matchers.ObjectContaining) { return b.matches(a); } @@ -934,6 +991,10 @@ jasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) { return (a == b); } + if (a instanceof RegExp && b instanceof RegExp) { + return this.compareRegExps_(a, b, mismatchKeys, mismatchValues); + } + if (typeof a === "object" && typeof b === "object") { return this.compareObjects_(a, b, mismatchKeys, mismatchValues); } @@ -1001,10 +1062,15 @@ jasmine.Block = function(env, func, spec) { }; jasmine.Block.prototype.execute = function(onComplete) { - try { + if (!jasmine.CATCH_EXCEPTIONS) { this.func.apply(this.spec); - } catch (e) { - this.spec.fail(e); + } + else { + try { + this.func.apply(this.spec); + } catch (e) { + this.spec.fail(e); + } } onComplete(); }; @@ -1216,7 +1282,7 @@ jasmine.Matchers.prototype.toEqual = function(expected) { /** * toNotEqual: compares the actual to the expected using the ! of jasmine.Matchers.toEqual * @param expected - * @deprecated as of 1.0. Use not.toNotEqual() instead. + * @deprecated as of 1.0. Use not.toEqual() instead. */ jasmine.Matchers.prototype.toNotEqual = function(expected) { return !this.env.equals_(this.actual, expected); @@ -1262,6 +1328,17 @@ jasmine.Matchers.prototype.toBeNull = function() { return (this.actual === null); }; +/** + * Matcher that compares the actual to NaN. + */ +jasmine.Matchers.prototype.toBeNaN = function() { + this.message = function() { + return [ "Expected " + jasmine.pp(this.actual) + " to be NaN." ]; + }; + + return (this.actual !== this.actual); +}; + /** * Matcher that boolean not-nots the actual. */ @@ -1339,18 +1416,14 @@ jasmine.Matchers.prototype.toHaveBeenCalledWith = function() { throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); } this.message = function() { + var invertedMessage = "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but it was."; + var positiveMessage = ""; if (this.actual.callCount === 0) { - // todo: what should the failure message for .not.toHaveBeenCalledWith() be? is this right? test better. [xw] - return [ - "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but it was never called.", - "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but it was." - ]; + positiveMessage = "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but it was never called."; } else { - return [ - "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall), - "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall) - ]; + positiveMessage = "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but actual calls were " + jasmine.pp(this.actual.argsForCall).replace(/^\[ | \]$/g, '') } + return [positiveMessage, invertedMessage]; }; return this.env.contains_(this.actual.argsForCall, expectedArgs); @@ -1389,7 +1462,7 @@ jasmine.Matchers.prototype.toContain = function(expected) { * Matcher that checks that the expected item is NOT an element in the actual Array. * * @param {Object} expected - * @deprecated as of 1.0. Use not.toNotContain() instead. + * @deprecated as of 1.0. Use not.toContain() instead. */ jasmine.Matchers.prototype.toNotContain = function(expected) { return !this.env.contains_(this.actual, expected); @@ -1408,22 +1481,19 @@ jasmine.Matchers.prototype.toBeGreaterThan = function(expected) { * up to a given level of decimal precision (default 2). * * @param {Number} expected - * @param {Number} precision + * @param {Number} precision, as number of decimal places */ jasmine.Matchers.prototype.toBeCloseTo = function(expected, precision) { if (!(precision === 0)) { precision = precision || 2; } - var multiplier = Math.pow(10, precision); - var actual = Math.round(this.actual * multiplier); - expected = Math.round(expected * multiplier); - return expected == actual; + return Math.abs(expected - this.actual) < (Math.pow(10, -precision) / 2); }; /** * Matcher that checks that the expected exception was thrown by the actual. * - * @param {String} expected + * @param {String} [expected] */ jasmine.Matchers.prototype.toThrow = function(expected) { var result = false; @@ -1457,7 +1527,7 @@ jasmine.Matchers.Any = function(expectedClass) { this.expectedClass = expectedClass; }; -jasmine.Matchers.Any.prototype.matches = function(other) { +jasmine.Matchers.Any.prototype.jasmineMatches = function(other) { if (this.expectedClass == String) { return typeof other == 'string' || other instanceof String; } @@ -1477,10 +1547,222 @@ jasmine.Matchers.Any.prototype.matches = function(other) { return other instanceof this.expectedClass; }; -jasmine.Matchers.Any.prototype.toString = function() { +jasmine.Matchers.Any.prototype.jasmineToString = function() { return ''; }; +jasmine.Matchers.ObjectContaining = function (sample) { + this.sample = sample; +}; + +jasmine.Matchers.ObjectContaining.prototype.jasmineMatches = function(other, mismatchKeys, mismatchValues) { + mismatchKeys = mismatchKeys || []; + mismatchValues = mismatchValues || []; + + var env = jasmine.getEnv(); + + var hasKey = function(obj, keyName) { + return obj != null && obj[keyName] !== jasmine.undefined; + }; + + for (var property in this.sample) { + if (!hasKey(other, property) && hasKey(this.sample, property)) { + mismatchKeys.push("expected has key '" + property + "', but missing from actual."); + } + else if (!env.equals_(this.sample[property], other[property], mismatchKeys, mismatchValues)) { + mismatchValues.push("'" + property + "' was '" + (other[property] ? jasmine.util.htmlEscape(other[property].toString()) : other[property]) + "' in expected, but was '" + (this.sample[property] ? jasmine.util.htmlEscape(this.sample[property].toString()) : this.sample[property]) + "' in actual."); + } + } + + return (mismatchKeys.length === 0 && mismatchValues.length === 0); +}; + +jasmine.Matchers.ObjectContaining.prototype.jasmineToString = function () { + return ""; +}; +// Mock setTimeout, clearTimeout +// Contributed by Pivotal Computer Systems, www.pivotalsf.com + +jasmine.FakeTimer = function() { + this.reset(); + + var self = this; + self.setTimeout = function(funcToCall, millis) { + self.timeoutsMade++; + self.scheduleFunction(self.timeoutsMade, funcToCall, millis, false); + return self.timeoutsMade; + }; + + self.setInterval = function(funcToCall, millis) { + self.timeoutsMade++; + self.scheduleFunction(self.timeoutsMade, funcToCall, millis, true); + return self.timeoutsMade; + }; + + self.clearTimeout = function(timeoutKey) { + self.scheduledFunctions[timeoutKey] = jasmine.undefined; + }; + + self.clearInterval = function(timeoutKey) { + self.scheduledFunctions[timeoutKey] = jasmine.undefined; + }; + +}; + +jasmine.FakeTimer.prototype.reset = function() { + this.timeoutsMade = 0; + this.scheduledFunctions = {}; + this.nowMillis = 0; +}; + +jasmine.FakeTimer.prototype.tick = function(millis) { + var oldMillis = this.nowMillis; + var newMillis = oldMillis + millis; + this.runFunctionsWithinRange(oldMillis, newMillis); + this.nowMillis = newMillis; +}; + +jasmine.FakeTimer.prototype.runFunctionsWithinRange = function(oldMillis, nowMillis) { + var scheduledFunc; + var funcsToRun = []; + for (var timeoutKey in this.scheduledFunctions) { + scheduledFunc = this.scheduledFunctions[timeoutKey]; + if (scheduledFunc != jasmine.undefined && + scheduledFunc.runAtMillis >= oldMillis && + scheduledFunc.runAtMillis <= nowMillis) { + funcsToRun.push(scheduledFunc); + this.scheduledFunctions[timeoutKey] = jasmine.undefined; + } + } + + if (funcsToRun.length > 0) { + funcsToRun.sort(function(a, b) { + return a.runAtMillis - b.runAtMillis; + }); + for (var i = 0; i < funcsToRun.length; ++i) { + try { + var funcToRun = funcsToRun[i]; + this.nowMillis = funcToRun.runAtMillis; + funcToRun.funcToCall(); + if (funcToRun.recurring) { + this.scheduleFunction(funcToRun.timeoutKey, + funcToRun.funcToCall, + funcToRun.millis, + true); + } + } catch(e) { + } + } + this.runFunctionsWithinRange(oldMillis, nowMillis); + } +}; + +jasmine.FakeTimer.prototype.scheduleFunction = function(timeoutKey, funcToCall, millis, recurring) { + this.scheduledFunctions[timeoutKey] = { + runAtMillis: this.nowMillis + millis, + funcToCall: funcToCall, + recurring: recurring, + timeoutKey: timeoutKey, + millis: millis + }; +}; + +/** + * @namespace + */ +jasmine.Clock = { + defaultFakeTimer: new jasmine.FakeTimer(), + + reset: function() { + jasmine.Clock.assertInstalled(); + jasmine.Clock.defaultFakeTimer.reset(); + }, + + tick: function(millis) { + jasmine.Clock.assertInstalled(); + jasmine.Clock.defaultFakeTimer.tick(millis); + }, + + runFunctionsWithinRange: function(oldMillis, nowMillis) { + jasmine.Clock.defaultFakeTimer.runFunctionsWithinRange(oldMillis, nowMillis); + }, + + scheduleFunction: function(timeoutKey, funcToCall, millis, recurring) { + jasmine.Clock.defaultFakeTimer.scheduleFunction(timeoutKey, funcToCall, millis, recurring); + }, + + useMock: function() { + if (!jasmine.Clock.isInstalled()) { + var spec = jasmine.getEnv().currentSpec; + spec.after(jasmine.Clock.uninstallMock); + + jasmine.Clock.installMock(); + } + }, + + installMock: function() { + jasmine.Clock.installed = jasmine.Clock.defaultFakeTimer; + }, + + uninstallMock: function() { + jasmine.Clock.assertInstalled(); + jasmine.Clock.installed = jasmine.Clock.real; + }, + + real: { + setTimeout: jasmine.getGlobal().setTimeout, + clearTimeout: jasmine.getGlobal().clearTimeout, + setInterval: jasmine.getGlobal().setInterval, + clearInterval: jasmine.getGlobal().clearInterval + }, + + assertInstalled: function() { + if (!jasmine.Clock.isInstalled()) { + throw new Error("Mock clock is not installed, use jasmine.Clock.useMock()"); + } + }, + + isInstalled: function() { + return jasmine.Clock.installed == jasmine.Clock.defaultFakeTimer; + }, + + installed: null +}; +jasmine.Clock.installed = jasmine.Clock.real; + +//else for IE support +jasmine.getGlobal().setTimeout = function(funcToCall, millis) { + if (jasmine.Clock.installed.setTimeout.apply) { + return jasmine.Clock.installed.setTimeout.apply(this, arguments); + } else { + return jasmine.Clock.installed.setTimeout(funcToCall, millis); + } +}; + +jasmine.getGlobal().setInterval = function(funcToCall, millis) { + if (jasmine.Clock.installed.setInterval.apply) { + return jasmine.Clock.installed.setInterval.apply(this, arguments); + } else { + return jasmine.Clock.installed.setInterval(funcToCall, millis); + } +}; + +jasmine.getGlobal().clearTimeout = function(timeoutKey) { + if (jasmine.Clock.installed.clearTimeout.apply) { + return jasmine.Clock.installed.clearTimeout.apply(this, arguments); + } else { + return jasmine.Clock.installed.clearTimeout(timeoutKey); + } +}; + +jasmine.getGlobal().clearInterval = function(timeoutKey) { + if (jasmine.Clock.installed.clearTimeout.apply) { + return jasmine.Clock.installed.clearInterval.apply(this, arguments); + } else { + return jasmine.Clock.installed.clearInterval(timeoutKey); + } +}; + /** * @constructor */ @@ -1609,10 +1891,6 @@ jasmine.PrettyPrinter = function() { * @param value */ jasmine.PrettyPrinter.prototype.format = function(value) { - if (this.ppNestLevel_ > 40) { - throw new Error('jasmine.PrettyPrinter: format() nested too deeply!'); - } - this.ppNestLevel_++; try { if (value === jasmine.undefined) { @@ -1621,8 +1899,8 @@ jasmine.PrettyPrinter.prototype.format = function(value) { this.emitScalar('null'); } else if (value === jasmine.getGlobal()) { this.emitScalar(''); - } else if (value instanceof jasmine.Matchers.Any) { - this.emitScalar(value.toString()); + } else if (value.jasmineToString) { + this.emitScalar(value.jasmineToString()); } else if (typeof value === 'string') { this.emitString(value); } else if (jasmine.isSpy(value)) { @@ -1655,6 +1933,7 @@ jasmine.PrettyPrinter.prototype.format = function(value) { jasmine.PrettyPrinter.prototype.iterateObject = function(obj, fn) { for (var property in obj) { + if (!obj.hasOwnProperty(property)) continue; if (property == '__Jasmine_been_here_before__') continue; fn(property, obj.__lookupGetter__ ? (obj.__lookupGetter__(property) !== jasmine.undefined && obj.__lookupGetter__(property) !== null) : false); @@ -1682,6 +1961,11 @@ jasmine.StringPrettyPrinter.prototype.emitString = function(value) { }; jasmine.StringPrettyPrinter.prototype.emitArray = function(array) { + if (this.ppNestLevel_ > jasmine.MAX_PRETTY_PRINT_DEPTH) { + this.append("Array"); + return; + } + this.append('[ '); for (var i = 0; i < array.length; i++) { if (i > 0) { @@ -1693,6 +1977,11 @@ jasmine.StringPrettyPrinter.prototype.emitArray = function(array) { }; jasmine.StringPrettyPrinter.prototype.emitObject = function(obj) { + if (this.ppNestLevel_ > jasmine.MAX_PRETTY_PRINT_DEPTH) { + this.append("Object"); + return; + } + var self = this; this.append('{ '); var first = true; @@ -1721,6 +2010,10 @@ jasmine.StringPrettyPrinter.prototype.append = function(value) { }; jasmine.Queue = function(env) { this.env = env; + + // parallel to blocks. each true value in this array means the block will + // get executed even if we abort + this.ensured = []; this.blocks = []; this.running = false; this.index = 0; @@ -1728,15 +2021,30 @@ jasmine.Queue = function(env) { this.abort = false; }; -jasmine.Queue.prototype.addBefore = function(block) { +jasmine.Queue.prototype.addBefore = function(block, ensure) { + if (ensure === jasmine.undefined) { + ensure = false; + } + this.blocks.unshift(block); + this.ensured.unshift(ensure); }; -jasmine.Queue.prototype.add = function(block) { +jasmine.Queue.prototype.add = function(block, ensure) { + if (ensure === jasmine.undefined) { + ensure = false; + } + this.blocks.push(block); + this.ensured.push(ensure); }; -jasmine.Queue.prototype.insertNext = function(block) { +jasmine.Queue.prototype.insertNext = function(block, ensure) { + if (ensure === jasmine.undefined) { + ensure = false; + } + + this.ensured.splice((this.index + this.offset + 1), 0, ensure); this.blocks.splice((this.index + this.offset + 1), 0, block); this.offset++; }; @@ -1760,7 +2068,7 @@ jasmine.Queue.prototype.next_ = function() { while (goAgain) { goAgain = false; - if (self.index < self.blocks.length && !this.abort) { + if (self.index < self.blocks.length && !(this.abort && !this.ensured[self.index])) { var calledSynchronously = true; var completedSynchronously = false; @@ -2051,7 +2359,7 @@ jasmine.Spec.prototype.finish = function(onComplete) { jasmine.Spec.prototype.after = function(doAfter) { if (this.queue.isRunning()) { - this.queue.add(new jasmine.Block(this.env, doAfter, this)); + this.queue.add(new jasmine.Block(this.env, doAfter, this), true); } else { this.afterCallbacks.unshift(doAfter); } @@ -2089,15 +2397,15 @@ jasmine.Spec.prototype.addBeforesAndAftersToQueue = function() { this.queue.addBefore(new jasmine.Block(this.env, runner.before_[i], this)); } for (i = 0; i < this.afterCallbacks.length; i++) { - this.queue.add(new jasmine.Block(this.env, this.afterCallbacks[i], this)); + this.queue.add(new jasmine.Block(this.env, this.afterCallbacks[i], this), true); } for (suite = this.suite; suite; suite = suite.parentSuite) { for (i = 0; i < suite.after_.length; i++) { - this.queue.add(new jasmine.Block(this.env, suite.after_[i], this)); + this.queue.add(new jasmine.Block(this.env, suite.after_[i], this), true); } } for (i = 0; i < runner.after_.length; i++) { - this.queue.add(new jasmine.Block(this.env, runner.after_[i], this)); + this.queue.add(new jasmine.Block(this.env, runner.after_[i], this), true); } }; @@ -2351,192 +2659,9 @@ jasmine.WaitsForBlock.MultiCompletion.prototype.buildCompletionFunction = functi }; }; -// Mock setTimeout, clearTimeout -// Contributed by Pivotal Computer Systems, www.pivotalsf.com - -jasmine.FakeTimer = function() { - this.reset(); - - var self = this; - self.setTimeout = function(funcToCall, millis) { - self.timeoutsMade++; - self.scheduleFunction(self.timeoutsMade, funcToCall, millis, false); - return self.timeoutsMade; - }; - - self.setInterval = function(funcToCall, millis) { - self.timeoutsMade++; - self.scheduleFunction(self.timeoutsMade, funcToCall, millis, true); - return self.timeoutsMade; - }; - - self.clearTimeout = function(timeoutKey) { - self.scheduledFunctions[timeoutKey] = jasmine.undefined; - }; - - self.clearInterval = function(timeoutKey) { - self.scheduledFunctions[timeoutKey] = jasmine.undefined; - }; - -}; - -jasmine.FakeTimer.prototype.reset = function() { - this.timeoutsMade = 0; - this.scheduledFunctions = {}; - this.nowMillis = 0; -}; - -jasmine.FakeTimer.prototype.tick = function(millis) { - var oldMillis = this.nowMillis; - var newMillis = oldMillis + millis; - this.runFunctionsWithinRange(oldMillis, newMillis); - this.nowMillis = newMillis; -}; - -jasmine.FakeTimer.prototype.runFunctionsWithinRange = function(oldMillis, nowMillis) { - var scheduledFunc; - var funcsToRun = []; - for (var timeoutKey in this.scheduledFunctions) { - scheduledFunc = this.scheduledFunctions[timeoutKey]; - if (scheduledFunc != jasmine.undefined && - scheduledFunc.runAtMillis >= oldMillis && - scheduledFunc.runAtMillis <= nowMillis) { - funcsToRun.push(scheduledFunc); - this.scheduledFunctions[timeoutKey] = jasmine.undefined; - } - } - - if (funcsToRun.length > 0) { - funcsToRun.sort(function(a, b) { - return a.runAtMillis - b.runAtMillis; - }); - for (var i = 0; i < funcsToRun.length; ++i) { - try { - var funcToRun = funcsToRun[i]; - this.nowMillis = funcToRun.runAtMillis; - funcToRun.funcToCall(); - if (funcToRun.recurring) { - this.scheduleFunction(funcToRun.timeoutKey, - funcToRun.funcToCall, - funcToRun.millis, - true); - } - } catch(e) { - } - } - this.runFunctionsWithinRange(oldMillis, nowMillis); - } -}; - -jasmine.FakeTimer.prototype.scheduleFunction = function(timeoutKey, funcToCall, millis, recurring) { - this.scheduledFunctions[timeoutKey] = { - runAtMillis: this.nowMillis + millis, - funcToCall: funcToCall, - recurring: recurring, - timeoutKey: timeoutKey, - millis: millis - }; -}; - -/** - * @namespace - */ -jasmine.Clock = { - defaultFakeTimer: new jasmine.FakeTimer(), - - reset: function() { - jasmine.Clock.assertInstalled(); - jasmine.Clock.defaultFakeTimer.reset(); - }, - - tick: function(millis) { - jasmine.Clock.assertInstalled(); - jasmine.Clock.defaultFakeTimer.tick(millis); - }, - - runFunctionsWithinRange: function(oldMillis, nowMillis) { - jasmine.Clock.defaultFakeTimer.runFunctionsWithinRange(oldMillis, nowMillis); - }, - - scheduleFunction: function(timeoutKey, funcToCall, millis, recurring) { - jasmine.Clock.defaultFakeTimer.scheduleFunction(timeoutKey, funcToCall, millis, recurring); - }, - - useMock: function() { - if (!jasmine.Clock.isInstalled()) { - var spec = jasmine.getEnv().currentSpec; - spec.after(jasmine.Clock.uninstallMock); - - jasmine.Clock.installMock(); - } - }, - - installMock: function() { - jasmine.Clock.installed = jasmine.Clock.defaultFakeTimer; - }, - - uninstallMock: function() { - jasmine.Clock.assertInstalled(); - jasmine.Clock.installed = jasmine.Clock.real; - }, - - real: { - setTimeout: jasmine.getGlobal().setTimeout, - clearTimeout: jasmine.getGlobal().clearTimeout, - setInterval: jasmine.getGlobal().setInterval, - clearInterval: jasmine.getGlobal().clearInterval - }, - - assertInstalled: function() { - if (!jasmine.Clock.isInstalled()) { - throw new Error("Mock clock is not installed, use jasmine.Clock.useMock()"); - } - }, - - isInstalled: function() { - return jasmine.Clock.installed == jasmine.Clock.defaultFakeTimer; - }, - - installed: null -}; -jasmine.Clock.installed = jasmine.Clock.real; - -//else for IE support -jasmine.getGlobal().setTimeout = function(funcToCall, millis) { - if (jasmine.Clock.installed.setTimeout.apply) { - return jasmine.Clock.installed.setTimeout.apply(this, arguments); - } else { - return jasmine.Clock.installed.setTimeout(funcToCall, millis); - } -}; - -jasmine.getGlobal().setInterval = function(funcToCall, millis) { - if (jasmine.Clock.installed.setInterval.apply) { - return jasmine.Clock.installed.setInterval.apply(this, arguments); - } else { - return jasmine.Clock.installed.setInterval(funcToCall, millis); - } -}; - -jasmine.getGlobal().clearTimeout = function(timeoutKey) { - if (jasmine.Clock.installed.clearTimeout.apply) { - return jasmine.Clock.installed.clearTimeout.apply(this, arguments); - } else { - return jasmine.Clock.installed.clearTimeout(timeoutKey); - } -}; - -jasmine.getGlobal().clearInterval = function(timeoutKey) { - if (jasmine.Clock.installed.clearTimeout.apply) { - return jasmine.Clock.installed.clearInterval.apply(this, arguments); - } else { - return jasmine.Clock.installed.clearInterval(timeoutKey); - } -}; - jasmine.version_= { "major": 1, - "minor": 1, - "build": 0, - "revision": 1315677058 + "minor": 3, + "build": 1, + "revision": 1354556913 }; diff --git a/vendor/packages/python.tmbundle b/vendor/packages/python.tmbundle new file mode 160000 index 000000000..3675c22ae --- /dev/null +++ b/vendor/packages/python.tmbundle @@ -0,0 +1 @@ +Subproject commit 3675c22ae891419b27a80c58001831d01e73d431