Merge remote-tracking branch 'origin/dev'

This commit is contained in:
Nathan Sobo
2013-03-14 19:08:06 -06:00
203 changed files with 10812 additions and 3755 deletions

2
.gitignore vendored
View File

@@ -2,7 +2,6 @@
.project
.svn
.nvm-version
atom-build
atom.xcodeproj
build
.xcodebuild-info
@@ -10,3 +9,4 @@ node_modules
npm-debug.log
/tags
/cef/
/sources.gypi

2
.gitmodules vendored
View File

@@ -51,7 +51,7 @@
url = https://github.com/mmcgrana/textmate-clojure
[submodule "prebuilt-cef"]
path = prebuilt-cef
url = git@github.com:github/prebuilt-cef.git
url = https://github.com/github/prebuilt-cef
[submodule "vendor/packages/yaml.tmbundle"]
path = vendor/packages/yaml.tmbundle
url = https://github.com/textmate/yaml.tmbundle.git

0
CHANGELOG.md Normal file
View File

View File

@@ -1,19 +1,19 @@
# Atom — Futuristic Text Editing
![atom](http://f.cl.ly/items/3h1L1O333p1d0W3D2K3r/atom-sketch.jpg)
![atom](https://f.cloud.github.com/assets/1300064/208230/4cefbca4-821a-11e2-8139-92c0328abf68.png)
Check out our [documentation on the docs tab](https://github.com/github/atom/docs).
## Building from source
Requirements
### Requirements
**Mountain Lion**
* Mountain Lion
* The Setup™ or Boxen
* Xcode (available in the App Store)
**The Setup™**
### Installation
**Xcode** (Get Xcode from the App Store (ugh, I know))
1. `gh-setup atom`
1. gh-setup atom
2. cd ~/github/atom && `rake install`
2. `cd ~/github/atom && rake install`

View File

@@ -1,9 +1,9 @@
ATOM_SRC_PATH = File.dirname(__FILE__)
BUILD_DIR = 'atom-build'
BUILD_DIR = '/tmp/atom-build'
desc "Build Atom via `xcodebuild`"
task :build => "create-xcode-project" do
command = "xcodebuild -target Atom -configuration Release SYMROOT=#{BUILD_DIR}"
command = "xcodebuild -target Atom SYMROOT=#{BUILD_DIR}"
output = `#{command}`
if $?.exitstatus != 0
$stderr.puts "Error #{$?.exitstatus}:\n#{output}"
@@ -14,6 +14,7 @@ end
desc "Create xcode project from gyp file"
task "create-xcode-project" => "update-cef" do
`rm -rf atom.xcodeproj`
`script/generate-sources-gypi`
`gyp --depth=. -D CODE_SIGN="#{ENV['CODE_SIGN']}" atom.gyp`
end
@@ -42,13 +43,17 @@ task :install => [:clean, :build] do
# Install Atom.app
dest_path = "/Applications/#{File.basename(path)}"
`rm -rf #{dest_path}`
`cp -r #{path} #{File.expand_path(dest_path)}`
`cp -a #{path} #{File.expand_path(dest_path)}`
# Install atom cli
if File.directory?("/opt/boxen")
cli_path = "/opt/boxen/bin/atom"
else
elsif File.directory?("/opt/github")
cli_path = "/opt/github/bin/atom"
elsif File.directory?("/usr/local")
cli_path = "/usr/local/bin/atom"
else
raise "Missing directory for `atom` binary"
end
FileUtils.cp("#{ATOM_SRC_PATH}/atom.sh", cli_path)
@@ -59,18 +64,6 @@ task :install => [:clean, :build] do
puts "\033[32mAtom is installed at `#{dest_path}`. Atom cli is installed at `#{cli_path}`\033[0m"
end
desc "Package up the app for speakeasy"
task :package => ["setup-codesigning", "build"] do
path = application_path()
exit 1 if not path
dest_path = '/tmp/atom-for-speakeasy/Atom.tar.bz2'
`mkdir -p $(dirname #{dest_path})`
`rm -rf #{dest_path}`
`tar --directory $(dirname #{path}) -jcf #{dest_path} $(basename #{path})`
`open $(dirname #{dest_path})`
end
task "setup-codesigning" do
ENV['CODE_SIGN'] = "Developer ID Application: GitHub"
end
@@ -90,7 +83,7 @@ task :clean do
end
desc "Run the specs"
task :test => ["update-cef", "clone-default-bundles", "build"] do
task :test => ["clean", "update-cef", "clone-default-bundles", "build"] do
`pkill Atom`
if path = application_path()
cmd = "#{path}/Contents/MacOS/Atom --test --resource-path=#{ATOM_SRC_PATH} 2> /dev/null"
@@ -107,7 +100,7 @@ task :benchmark do
end
task :nof do
system %{find . -name *spec.coffee | grep --invert-match --regexp "#{BUILD_DIR}\\|##package-name##" | xargs sed -E -i "" "s/f+(it|describe) +(['\\"])/\\1 \\2/g"}
system %{find . -name *spec.coffee | grep --invert-match --regexp "#{BUILD_DIR}\\|__package-name__" | xargs sed -E -i "" "s/f+(it|describe) +(['\\"])/\\1 \\2/g"}
end
task :tags do

288
atom.gyp
View File

@@ -16,13 +16,30 @@
'toolkit_uses_gtk%': 0,
}],
],
'fix_framework_link_command': [
'install_name_tool',
'-change',
'@executable_path/libcef.dylib',
'@rpath/Chromium Embedded Framework.framework/Libraries/libcef.dylib',
'-change',
'@executable_path/../Frameworks/CocoaOniguruma.framework/Versions/A/CocoaOniguruma',
'@rpath/CocoaOniguruma.framework/Versions/A/CocoaOniguruma',
'-change',
'@loader_path/../Frameworks/Sparkle.framework/Versions/A/Sparkle',
'@rpath/Sparkle.framework/Versions/A/Sparkle',
'-change',
'@executable_path/libgit2.0.17.0.dylib',
'@rpath/libgit2.framework/Libraries/libgit2.0.17.0.dylib',
'${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}'
],
},
'includes': [
'cef/cef_paths2.gypi',
'git2/libgit2.gypi',
'sources.gypi',
],
'target_defaults': {
'default_configuration': 'Debug',
'default_configuration': 'Release',
'configurations': {
'Debug': {
'defines': ['DEBUG=1'],
@@ -45,39 +62,20 @@
'mac_bundle': 1,
'msvs_guid': 'D22C6F51-AA2D-457C-B579-6C97A96C724D',
'dependencies': [
'libcef_dll_wrapper',
'atom_framework',
],
'defines': [
'USING_CEF_SHARED',
],
'include_dirs': [ '.', 'cef', 'git2' ],
'mac_framework_dirs': [ 'native/frameworks' ],
'libraries': [ 'native/frameworks/CocoaOniguruma.framework', 'native/frameworks/Sparkle.framework'],
'sources': [
'<@(includes_common)',
'<@(includes_wrapper)',
'native/main_mac.mm',
'native/atom_application.h',
'native/atom_application.mm',
'native/atom_cef_app.h',
'native/atom_window_controller.h',
'native/atom_window_controller.mm',
'native/atom_cef_client_mac.mm',
'native/atom_cef_client.cpp',
'native/atom_cef_client.h',
'native/message_translation.cpp',
'native/message_translation.h',
'native/main.cpp',
],
'mac_bundle_resources': [
'native/mac/atom.icns',
'native/mac/file.icns',
'native/mac/speakeasy.pem',
'native/mac/English.lproj/MainMenu.xib',
'native/mac/English.lproj/AtomWindow.xib',
],
'xcode_settings': {
'INFOPLIST_FILE': 'native/mac/info.plist',
'OTHER_LDFLAGS': ['-Wl,-headerpad_max_install_names'], # Necessary to avoid an "install_name_tool: changing install names or rpaths can't be redone" error.
'INFOPLIST_FILE': 'native/mac/Atom-Info.plist',
'LD_RUNPATH_SEARCH_PATHS': '@executable_path/../Frameworks',
},
'conditions': [
['CODE_SIGN' , {
@@ -142,6 +140,8 @@
{
'destination': '<(PRODUCT_DIR)/Atom.app/Contents/Frameworks',
'files': [
'<(PRODUCT_DIR)/Atom Helper.app',
'<(PRODUCT_DIR)/Atom.framework',
'native/frameworks/CocoaOniguruma.framework',
'native/frameworks/Sparkle.framework',
],
@@ -152,40 +152,18 @@
'git2/frameworks/libgit2.0.17.0.dylib',
],
},
{
'destination': '<(PRODUCT_DIR)/Atom.app/Contents/Frameworks/Chromium Embedded Framework.framework',
'files': [
'cef/Resources',
],
},
],
'postbuilds': [
{
'postbuild_name': 'Copy and Compile Static Files',
'action': [
'script/copy-files-to-bundle'
],
},
{
'postbuild_name': 'Copy Helper App',
'action': [
'cp',
'-r',
'${BUILT_PRODUCTS_DIR}/Atom Helper.app',
'${BUILT_PRODUCTS_DIR}/Atom.app/Contents/Frameworks',
],
},
{
'postbuild_name': 'Fix Framework Link',
'action': [
'install_name_tool',
'-change',
'@executable_path/libcef.dylib',
'@executable_path/../Frameworks/Chromium Embedded Framework.framework/Libraries/libcef.dylib',
'${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}'
],
},
{
'postbuild_name': 'Copy Framework Resources Directory',
'action': [
'cp',
'-r',
'cef/Resources',
'${BUILT_PRODUCTS_DIR}/Atom.app/Contents/Frameworks/Chromium Embedded Framework.framework/'
'<@(fix_framework_link_command)',
],
},
{
@@ -204,6 +182,12 @@
'Atom',
],
},
{
'postbuild_name': 'Print env for Constructicon',
'action': [
'env',
],
},
],
'link_settings': {
'libraries': [
@@ -231,6 +215,89 @@
}],
],
},
{
'target_name': 'atom_framework',
'product_name': 'Atom',
'type': 'shared_library',
'mac_bundle': 1,
'dependencies': [
'generated_sources',
'libcef_dll_wrapper',
],
'defines': [
'USING_CEF_SHARED',
],
'xcode_settings': {
'INFOPLIST_FILE': 'native/mac/framework-info.plist',
'LD_DYLIB_INSTALL_NAME': '@rpath/Atom.framework/Atom',
},
'include_dirs': [ '.', 'cef', 'git2' ],
'mac_framework_dirs': [ 'native/frameworks' ],
'sources': [
'<@(includes_common)',
'<@(includes_wrapper)',
'native/atom_application.h',
'native/atom_application.mm',
'native/atom_cef_app.h',
'native/atom_cef_app.h',
'native/atom_cef_client.cpp',
'native/atom_cef_client.h',
'native/atom_cef_client_mac.mm',
'native/atom_cef_render_process_handler.h',
'native/atom_cef_render_process_handler.mm',
'native/atom_window_controller.h',
'native/atom_window_controller.mm',
'native/atom_main.h',
'native/atom_main_mac.mm',
'native/message_translation.cpp',
'native/message_translation.cpp',
'native/message_translation.h',
'native/message_translation.h',
'native/path_watcher.h',
'native/path_watcher.mm',
'native/v8_extensions/atom.h',
'native/v8_extensions/atom.mm',
'native/v8_extensions/git.h',
'native/v8_extensions/git.mm',
'native/v8_extensions/native.h',
'native/v8_extensions/native.mm',
'native/v8_extensions/onig_reg_exp.h',
'native/v8_extensions/onig_reg_exp.mm',
'native/v8_extensions/onig_scanner.h',
'native/v8_extensions/onig_scanner.mm',
'native/v8_extensions/readtags.c',
'native/v8_extensions/readtags.h',
'native/v8_extensions/tags.h',
'native/v8_extensions/tags.mm',
],
'link_settings': {
'libraries': [
'$(SDKROOT)/System/Library/Frameworks/AppKit.framework',
'git2/frameworks/libgit2.0.17.0.dylib',
'native/frameworks/CocoaOniguruma.framework',
'native/frameworks/Sparkle.framework',
],
},
'mac_bundle_resources': [
'native/mac/English.lproj/AtomWindow.xib',
'native/mac/English.lproj/MainMenu.xib',
],
'postbuilds': [
{
'postbuild_name': 'Copy Static Files',
'action': [
'script/copy-files-to-bundle',
'<(compiled_sources_dir_xcode)',
],
},
{
'postbuild_name': 'Fix Framework Link',
'action': [
'<@(fix_framework_link_command)',
],
},
],
},
{
'target_name': 'libcef_dll_wrapper',
'type': 'static_library',
@@ -253,6 +320,48 @@
],
}
},
{
'target_name': 'generated_sources',
'type': 'none',
'sources': [
'<@(coffee_sources)',
'<@(cson_sources)',
],
'rules': [
{
'rule_name': 'coffee',
'extension': 'coffee',
'inputs': [
'script/compile-coffee',
],
'outputs': [
'<(compiled_sources_dir)/<(RULE_INPUT_DIRNAME)/<(RULE_INPUT_ROOT).js',
],
'action': [
'sh',
'script/compile-coffee',
'<(RULE_INPUT_PATH)',
'<(compiled_sources_dir)/<(RULE_INPUT_DIRNAME)/<(RULE_INPUT_ROOT).js',
],
},
{
'rule_name': 'cson2json',
'extension': 'cson',
'inputs': [
'script/compile-cson',
],
'outputs': [
'<(compiled_sources_dir)/<(RULE_INPUT_DIRNAME)/<(RULE_INPUT_ROOT).json',
],
'action': [
'sh',
'script/compile-cson',
'<(RULE_INPUT_PATH)',
'<(compiled_sources_dir)/<(RULE_INPUT_DIRNAME)/<(RULE_INPUT_ROOT).json',
],
},
],
},
],
'conditions': [
['os_posix==1 and OS!="mac" and OS!="android" and gcc_version==46', {
@@ -271,46 +380,15 @@
'product_name': 'Atom Helper',
'mac_bundle': 1,
'dependencies': [
'libcef_dll_wrapper',
'atom_framework',
],
'defines': [
'USING_CEF_SHARED',
'PROCESS_HELPER_APP',
],
'include_dirs': [ '.', 'cef', 'git2' ],
'mac_framework_dirs': [ 'native/frameworks' ],
'link_settings': {
'libraries': [
'$(SDKROOT)/System/Library/Frameworks/AppKit.framework',
],
},
'libraries': [
'native/frameworks/CocoaOniguruma.framework',
'git2/frameworks/libgit2.0.17.0.dylib',
],
'sources': [
'native/atom_cef_app.h',
'native/atom_cef_render_process_handler.h',
'native/atom_cef_render_process_handler.mm',
'native/message_translation.cpp',
'native/message_translation.h',
'native/path_watcher.mm',
'native/path_watcher.h',
'native/main_helper_mac.mm',
'native/v8_extensions/native.mm',
'native/v8_extensions/native.h',
'native/v8_extensions/onig_reg_exp.mm',
'native/v8_extensions/onig_reg_exp.h',
'native/v8_extensions/onig_scanner.mm',
'native/v8_extensions/onig_scanner.h',
'native/v8_extensions/atom.mm',
'native/v8_extensions/atom.h',
'native/v8_extensions/git.mm',
'native/v8_extensions/git.h',
'native/v8_extensions/readtags.h',
'native/v8_extensions/readtags.c',
'native/v8_extensions/tags.h',
'native/v8_extensions/tags.mm',
'native/main.cpp',
],
# TODO(mark): For now, don't put any resources into this app. Its
# resources directory will be a symbolic link to the browser app's
@@ -320,45 +398,13 @@
],
'xcode_settings': {
'INFOPLIST_FILE': 'native/mac/helper-info.plist',
'OTHER_LDFLAGS': ['-Wl,-headerpad_max_install_names'], # Necessary to avoid an "install_name_tool: changing install names or rpaths can't be redone" error.
'LD_RUNPATH_SEARCH_PATHS': '@executable_path/../../..',
},
'copies': [
{
'destination': '<(PRODUCT_DIR)/Atom Helper.app/Contents/Frameworks',
'files': [
'native/frameworks/CocoaOniguruma.framework',
],
},
],
'postbuilds': [
{
# The framework defines its load-time path
# (DYLIB_INSTALL_NAME_BASE) relative to the main executable
# (chrome). A different relative path needs to be used in
# atom_helper_app.
'postbuild_name': 'Fix CEF Framework Link',
'postbuild_name': 'Fix Framework Link',
'action': [
'install_name_tool',
'-change',
'@executable_path/libcef.dylib',
'@executable_path/../../../../Frameworks/Chromium Embedded Framework.framework/Libraries/libcef.dylib',
'${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}'
],
},
{
'postbuild_name': 'Fix libgit2 Framework Link',
'action': [
'install_name_tool',
'-change',
'@executable_path/libgit2.0.17.0.dylib',
'@executable_path/../../../../Frameworks/libgit2.framework/Libraries/libgit2.0.17.0.dylib',
'${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}'
],
},
{
'postbuild_name': 'Copy and Compile Static Files',
'action': [
'script/copy-files-to-bundle'
'<@(fix_framework_link_command)',
],
},
],

View File

@@ -7,7 +7,7 @@ Config = require 'config'
Project = require 'project'
require 'window'
requireStylesheet "jasmine.css"
requireStylesheet "jasmine.less"
# Load TextMate bundles, which specs rely on (but not other packages)
atom.loadTextMatePackages()
@@ -127,4 +127,3 @@ $.fn.textInput = (data) ->
event = document.createEvent 'TextEvent'
event.initTextEvent('textInput', true, true, window, data)
this.each -> this.dispatchEvent(event)

View File

@@ -107,7 +107,7 @@ describe "TokenizedBuffer.", ->
[languageMode, buffer] = []
beforeEach ->
editSession = benchmarkFixturesProject.buildEditSessionForPath('medium.coffee')
editSession = benchmarkFixturesProject.buildEditSession('medium.coffee')
{ languageMode, buffer } = editSession
benchmark "construction", 20, ->

View File

@@ -11,7 +11,7 @@ always hit `meta-p` to bring up a list of commands that are relevant to the
currently focused UI element. If there is a key binding for a given command, it
is also displayed. This is a great way to explore the system and get to know the
key commands interactively. If you'd like to add or change a binding for a
command, refer to the [keymaps](#keymaps) section to learn how.
command, refer to the [key bindings](#customizing-key-bindings) section to learn how.
![Command Palette](http://f.cl.ly/items/32041o3w471F3C0F0V2O/Screen%20Shot%202013-02-13%20at%207.27.41%20PM.png)
@@ -157,10 +157,10 @@ its own namespace.
- hideGitIgnoredFiles: Whether files in the .gitignore should be hidden
- ignoredNames: File names to ignore across all of atom (not fully implemented)
- themes: An array of theme names to load, in cascading order
- autosave: Save a resource when its view loses focus
- editor
- autoIndent: Enable/disable basic auto-indent (defaults to true)
- autoIndentOnPaste: Enable/disable auto-indented pasted text (defaults to false)
- autosave: Save a file when an editor loses focus
- nonWordCharacters: A string of non-word characters to define word boundaries
- fontSize
- fontFamily

View File

@@ -7,7 +7,7 @@ read config settings. You can read a value from `config` with `config.get`:
```coffeescript
# read a value with `config.get`
@autosave() if config.get "editor.autosave"
@autosave() if config.get "core.autosave"
```
Or you can use `observeConfig` to track changes from a view object.
@@ -47,7 +47,7 @@ the following way:
```coffeescript
# basic key update
config.set("editor.autosave", true)
config.set("core.autosave", true)
# if you mutate a config key, you'll need to call `config.update` to inform
# observers of the change

View File

@@ -0,0 +1,97 @@
## Serialization in Atom
When a window is refreshed or restored from a previous session, the view and its
associated objects are *deserialized* from a JSON representation that was stored
during the window's previous shutdown. For your own views and objects to be
compatible with refreshing, you'll need to make them play nicely with the
serializing and deserializing.
### Package Serialization Hook
Your package's main module can optionally include a `serialize` method, which
will be called before your package is deactivated. You should return JSON, which
will be handed back to you as an argument to `activate` next time it is called.
In the following example, the package keeps an instance of `MyObject` in the
same state across refreshes.
```coffee-script
module.exports =
activate: (state) ->
@myObject =
if state
deserialize(state)
else
new MyObject("Hello")
serialize: ->
@myObject.serialize()
```
### Serialization Methods
```coffee-script
class MyObject
registerDeserializer(this)
@deserialize: ({data}) -> new MyObject(data)
constructor: (@data) ->
serialize: -> { deserializer: 'MyObject', data: @data }
```
#### .serialize()
Objects that you want to serialize should implement `.serialize()`. This method
should return a serializable object, and it must contain a key named
`deserializer` whose value is the name of a registered deserializer that can
convert the rest of the data to an object. It's usually just the name of the
class itself.
#### @deserialize(data)
The other side of the coin is the `deserialize` method, which is usually a
class-level method on the same class that implements `serialize`. This method's
job is to convert a state object returned from a previous call `serialize` back
into a genuine object.
#### registerDeserializer(klass)
You need to call the global `registerDeserializer` method with your class in
order to make it available to the deserialization system. Now you can call the
global `deserialize` method with state returned from `serialize`, and your
class's `deserialize` method will be selected automatically.
### Versioning
```coffee-script
class MyObject
@version: 2
@deserialize: (state) -> ...
serialize: -> { version: MyObject.version, ... }
```
Your serializable class can optionally have a class-level `@version` property
and include a `version` key in its serialized state. When deserializing, Atom
will only attempt to call deserialize if the two versions match, and otherwise
return undefined. We plan on implementing a migration system in the future, but
this at least protects you from improperly deserializing old state. If you find
yourself in dire need of the migration system, let us know.
### Deferred Package Deserializers
If your package defers loading on startup with an `activationEvents` property in
its `package.cson`, your deserializers won't be loaded until your package is
activated. If you want to deserialize an object from your package on startup,
this could be a problem.
The solution is to also supply a `deferredDeserializers` array in your
`package.cson` with the names of all your deserializers. When Atom attempts to
deserialize some state whose `deserializer` matches one of these names, it will
load your package first so it can register any necessary deserializers before
proceeding.
For example, the markdown preview package doesn't fully load until a preview is
triggered. But if you refresh a window with a preview pane, it loads the
markdown package early so Atom can deserialize the view correctly.
```coffee-script
# markdown-preview/package.cson
'activationEvents': 'markdown-preview:toggle': '.editor'
'deferredDeserializers': ['MarkdownPreviewView']
...
```

Binary file not shown.

View File

@@ -18,6 +18,7 @@ class AtomCefClient;
+ (CefSettings)createCefSettings;
+ (NSDictionary *)parseArguments:(char **)argv count:(int)argc;
- (void)open:(NSString *)path;
- (void)openDev:(NSString *)path;
- (void)open:(NSString *)path pidToKillWhenWindowCloses:(NSNumber *)pid;
- (IBAction)runSpecs:(id)sender;
- (IBAction)runBenchmarks:(id)sender;

View File

@@ -4,17 +4,13 @@
#include "include/cef_app.h"
#ifdef PROCESS_HELPER_APP
#include "atom_cef_render_process_handler.h"
#endif
class AtomCefApp : public CefApp {
#ifdef PROCESS_HELPER_APP
virtual CefRefPtr<CefRenderProcessHandler> GetRenderProcessHandler() OVERRIDE {
return CefRefPtr<CefRenderProcessHandler>(new AtomCefRenderProcessHandler);
}
#endif
IMPLEMENT_REFCOUNTING(AtomCefApp);
};

View File

@@ -34,7 +34,6 @@ void AtomCefRenderProcessHandler::OnWorkerContextCreated(int worker_id,
void AtomCefRenderProcessHandler::OnWorkerContextReleased(int worker_id,
const CefString& url,
CefRefPtr<CefV8Context> context) {
NSLog(@"Web worker context released");
}
void AtomCefRenderProcessHandler::OnWorkerUncaughtException(int worker_id,

1
native/atom_main.h Normal file
View File

@@ -0,0 +1 @@
__attribute__((visibility("default"))) int AtomMain(int argc, char* argv[]);

View File

@@ -1,3 +1,5 @@
#import "atom_main.h"
#import "atom_cef_app.h"
#import "include/cef_application_mac.h"
#import "native/atom_application.h"
#include <sys/types.h>
@@ -10,7 +12,15 @@ void listenForPathToOpen(int fd, NSString *socketPath);
void activateOpenApp();
BOOL isAppAlreadyOpen();
int main(int argc, char* argv[]) {
int AtomMain(int argc, char* argv[]) {
// Check if we're being run as a secondary process.
CefMainArgs main_args(argc, argv);
CefRefPtr<CefApp> app(new AtomCefApp);
int exitCode = CefExecuteProcess(main_args, app);
if (exitCode >= 0)
return exitCode;
// We're the main process.
@autoreleasepool {
handleBeingOpenedAgain(argc, argv);
@@ -18,8 +28,8 @@ int main(int argc, char* argv[]) {
AtomApplication *application = [AtomApplication applicationWithArguments:argv count:argc];
NSString *mainNibName = [infoDictionary objectForKey:@"NSMainNibFile"];
NSNib *mainNib = [[NSNib alloc] initWithNibNamed:mainNibName bundle:[NSBundle mainBundle]];
[mainNib instantiateNibWithOwner:application topLevelObjects:nil];
NSNib *mainNib = [[NSNib alloc] initWithNibNamed:mainNibName bundle:[NSBundle bundleWithIdentifier:@"com.github.atom.framework"]];
[mainNib instantiateWithOwner:application topLevelObjects:nil];
CefRunMessageLoop();
}

View File

@@ -34,8 +34,7 @@
_resourcePath = [atomApplication.arguments objectForKey:@"resource-path"];
if (!alwaysUseBundleResourcePath && !_resourcePath) {
NSString *defaultRepositoryPath = @"~/github/atom";
defaultRepositoryPath = [defaultRepositoryPath stringByStandardizingPath];
NSString *defaultRepositoryPath = [@"~/github/atom" stringByStandardizingPath];
if ([defaultRepositoryPath characterAtIndex:0] == '/') {
BOOL isDir = false;
BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:defaultRepositoryPath isDirectory:&isDir];
@@ -45,8 +44,14 @@
}
if (alwaysUseBundleResourcePath || !_resourcePath) {
_resourcePath = [[NSBundle mainBundle] resourcePath];
_resourcePath = [[NSBundle bundleForClass:self.class] resourcePath];
}
if ([self isDevMode]) {
[self displayDevIcon];
}
_resourcePath = [_resourcePath stringByStandardizingPath];
[_resourcePath retain];
if (!background) {
@@ -69,7 +74,6 @@
- (id)initDevWithPath:(NSString *)path {
_pathToOpen = [path retain];
AtomApplication *atomApplication = (AtomApplication *)[AtomApplication sharedApplication];
return [self initWithBootstrapScript:@"window-bootstrap" background:NO alwaysUseBundleResourcePath:false];
}
@@ -119,11 +123,13 @@
// have the correct initial size based on the frame's last stored size.
// HACK: I hate this and want to place this code directly in windowDidLoad
- (void)attachWebView {
NSURL *url = [[NSBundle mainBundle] resourceURL];
NSURL *url = [[NSBundle bundleForClass:self.class] resourceURL];
NSMutableString *urlString = [NSMutableString string];
[urlString appendString:[[url URLByAppendingPathComponent:@"static/index.html"] absoluteString]];
[urlString appendFormat:@"?bootstrapScript=%@", [self encodeUrlParam:_bootstrapScript]];
[urlString appendFormat:@"&resourcePath=%@", [self encodeUrlParam:_resourcePath]];
if ([self isDevMode])
[urlString appendFormat:@"&devMode=1"];
if (_exitWhenDone)
[urlString appendString:@"&exitWhenDone=1"];
if (_pathToOpen)
@@ -202,6 +208,33 @@
return YES;
}
- (bool)isDevMode {
NSString *bundleResourcePath = [[NSBundle bundleForClass:self.class] resourcePath];
return ![_resourcePath isEqualToString:bundleResourcePath];
}
- (void)displayDevIcon {
NSView *themeFrame = [self.window.contentView superview];
NSButton *fullScreenButton = nil;
for (NSView *view in themeFrame.subviews) {
if (![view isKindOfClass:NSButton.class]) continue;
NSButton *button = (NSButton *)view;
if (button.action != @selector(toggleFullScreen:)) continue;
fullScreenButton = button;
break;
}
NSButton *devButton = [[NSButton alloc] init];
[devButton setTitle:@"\xF0\x9F\x92\x80"];
devButton.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin;
devButton.buttonType = NSMomentaryChangeButton;
devButton.bordered = NO;
[devButton sizeToFit];
devButton.frame = NSMakeRect(fullScreenButton.frame.origin.x - devButton.frame.size.width - 5, fullScreenButton.frame.origin.y, devButton.frame.size.width, devButton.frame.size.height);
[[self.window.contentView superview] addSubview:devButton];
}
- (void)populateBrowserSettings:(CefBrowserSettings &)settings {
CefString(&settings.default_encoding) = "UTF-8";
settings.remote_fonts_disabled = false;

View File

@@ -7,6 +7,8 @@
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleTypeIconFile</key>
<string>file.icns</string>
<key>LSItemContentTypes</key>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>com.github.atom.framework</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>

5
native/main.cpp Normal file
View File

@@ -0,0 +1,5 @@
#include "atom_main.h"
int main(int argc, char* argv[]) {
return AtomMain(argc, argv);
}

View File

@@ -1,8 +0,0 @@
#include "include/cef_app.h"
#include "atom_cef_app.h"
int main(int argc, char* argv[]) {
CefMainArgs main_args(argc, argv);
CefRefPtr<CefApp> app(new AtomCefApp);
return CefExecuteProcess(main_args, app); // Execute the secondary process.
}

View File

@@ -245,7 +245,7 @@ static NSMutableArray *gPathWatchers;
char pathBuffer[MAXPATHLEN];
fcntl((int)event.ident, F_GETPATH, &pathBuffer);
close(event.ident);
newPath = [NSString stringWithUTF8String:pathBuffer];
newPath = [[NSString stringWithUTF8String:pathBuffer] stringByStandardizingPath];
if (!newPath) {
NSLog(@"WARNING: Ignoring rename event for deleted file '%@'", path);
continue;

View File

@@ -22,24 +22,26 @@ namespace v8_extensions {
const CefV8ValueList& arguments,
CefRefPtr<CefV8Value>& retval,
CefString& exception) {
CefRefPtr<CefBrowser> browser = CefV8Context::GetCurrentContext()->GetBrowser();
@autoreleasepool {
CefRefPtr<CefBrowser> browser = CefV8Context::GetCurrentContext()->GetBrowser();
if (name == "sendMessageToBrowserProcess") {
if (arguments.size() == 0 || !arguments[0]->IsString()) {
exception = "You must supply a message name";
return false;
if (name == "sendMessageToBrowserProcess") {
if (arguments.size() == 0 || !arguments[0]->IsString()) {
exception = "You must supply a message name";
return false;
}
CefString name = arguments[0]->GetStringValue();
CefRefPtr<CefProcessMessage> message = CefProcessMessage::Create(name);
if (arguments.size() > 1 && arguments[1]->IsArray()) {
TranslateList(arguments[1], message->GetArgumentList());
}
browser->SendProcessMessage(PID_BROWSER, message);
return true;
}
CefString name = arguments[0]->GetStringValue();
CefRefPtr<CefProcessMessage> message = CefProcessMessage::Create(name);
if (arguments.size() > 1 && arguments[1]->IsArray()) {
TranslateList(arguments[1], message->GetArgumentList());
}
browser->SendProcessMessage(PID_BROWSER, message);
return true;
return false;
}
return false;
};
}

View File

@@ -8,6 +8,21 @@ namespace v8_extensions {
private:
git_repository *repo;
static int CollectStatus(const char *path, unsigned int status, void *payload) {
if ((status & GIT_STATUS_IGNORED) == 0) {
std::map<const char*, unsigned int> *statuses = (std::map<const char*, unsigned int> *) payload;
statuses->insert(std::pair<const char*, unsigned int>(path, status));
}
return 0;
}
static int CollectDiffHunk(const git_diff_delta *delta, const git_diff_range *range,
const char *header, size_t header_len, void *payload) {
std::vector<git_diff_range> *ranges = (std::vector<git_diff_range> *) payload;
ranges->push_back(*range);
return 0;
}
public:
GitRepository(const char *pathInRepo) {
if (git_repository_open_ext(&repo, pathInRepo, 0, NULL) != GIT_OK) {
@@ -55,6 +70,126 @@ namespace v8_extensions {
return CefV8Value::CreateNull();
}
CefRefPtr<CefV8Value> GetStatuses() {
std::map<const char*, unsigned int> statuses;
git_status_foreach(repo, CollectStatus, &statuses);
std::map<const char*, unsigned int>::iterator iter = statuses.begin();
CefRefPtr<CefV8Value> v8Statuses = CefV8Value::CreateObject(NULL);
for (; iter != statuses.end(); ++iter) {
v8Statuses->SetValue(iter->first, CefV8Value::CreateInt(iter->second), V8_PROPERTY_ATTRIBUTE_NONE);
}
return v8Statuses;
}
int GetCommitCount(const git_oid* fromCommit, const git_oid* toCommit) {
int count = 0;
git_revwalk *revWalk;
if (git_revwalk_new(&revWalk, repo) == GIT_OK) {
git_revwalk_push(revWalk, fromCommit);
git_revwalk_hide(revWalk, toCommit);
git_oid currentCommit;
while (git_revwalk_next(&currentCommit, revWalk) == GIT_OK)
count++;
git_revwalk_free(revWalk);
}
return count;
}
void GetShortBranchName(const char** out, const char* branchName) {
*out = NULL;
if (branchName == NULL)
return;
int branchNameLength = strlen(branchName);
if (branchNameLength < 12)
return;
if (strncmp("refs/heads/", branchName, 11) != 0)
return;
int shortNameLength = branchNameLength - 11;
char* shortName = (char*) malloc(sizeof(char) * (shortNameLength + 1));
shortName[shortNameLength] = '\0';
strncpy(shortName, &branchName[11], shortNameLength);
*out = shortName;
}
void GetUpstreamBranch(const char** out, git_reference *branch) {
*out = NULL;
const char* branchName = git_reference_name(branch);
const char* shortBranchName;
GetShortBranchName(&shortBranchName, branchName);
if (shortBranchName == NULL)
return;
int shortBranchNameLength = strlen(shortBranchName);
char* remoteKey = (char*) malloc(sizeof(char) * (shortBranchNameLength + 15));
sprintf(remoteKey, "branch.%s.remote", shortBranchName);
char* mergeKey = (char*) malloc(sizeof(char) * (shortBranchNameLength + 14));
sprintf(mergeKey, "branch.%s.merge", shortBranchName);
free((char*)shortBranchName);
git_config *config;
if (git_repository_config(&config, repo) != GIT_OK) {
free(remoteKey);
free(mergeKey);
return;
}
const char *remote;
const char *merge;
if (git_config_get_string(&remote, config, remoteKey) == GIT_OK
&& git_config_get_string(&merge, config, mergeKey) == GIT_OK) {
int remoteLength = strlen(remote);
if (remoteLength > 0) {
const char *shortMergeBranchName;
GetShortBranchName(&shortMergeBranchName, merge);
if (shortMergeBranchName != NULL) {
int updateBranchLength = remoteLength + strlen(shortMergeBranchName) + 14;
char* upstreamBranch = (char*) malloc(sizeof(char) * (updateBranchLength + 1));
sprintf(upstreamBranch, "refs/remotes/%s/%s", remote, shortMergeBranchName);
*out = upstreamBranch;
}
free((char*)shortMergeBranchName);
}
}
free(remoteKey);
free(mergeKey);
git_config_free(config);
}
CefRefPtr<CefV8Value> GetAheadBehindCounts() {
CefRefPtr<CefV8Value> result = NULL;
git_reference *head;
if (git_repository_head(&head, repo) == GIT_OK) {
const char* upstreamBranchName;
GetUpstreamBranch(&upstreamBranchName, head);
if (upstreamBranchName != NULL) {
git_reference *upstream;
if (git_reference_lookup(&upstream, repo, upstreamBranchName) == GIT_OK) {
const git_oid* headSha = git_reference_target(head);
const git_oid* upstreamSha = git_reference_target(upstream);
git_oid mergeBase;
if (git_merge_base(&mergeBase, repo, headSha, upstreamSha) == GIT_OK) {
result = CefV8Value::CreateObject(NULL);
int ahead = GetCommitCount(headSha, &mergeBase);
result->SetValue("ahead", CefV8Value::CreateInt(ahead), V8_PROPERTY_ATTRIBUTE_NONE);
int behind = GetCommitCount(upstreamSha, &mergeBase);
result->SetValue("behind", CefV8Value::CreateInt(behind), V8_PROPERTY_ATTRIBUTE_NONE);
}
git_reference_free(upstream);
}
free((char*)upstreamBranchName);
}
git_reference_free(head);
}
if (result != NULL)
return result;
else
return CefV8Value::CreateNull();
}
CefRefPtr<CefV8Value> IsIgnored(const char *path) {
int ignored;
if (git_ignore_path_is_ignored(&ignored, repo, path) == GIT_OK) {
@@ -123,6 +258,7 @@ namespace v8_extensions {
git_diff_list *diffs;
int diffStatus = git_diff_tree_to_workdir(&diffs, repo, tree, &options);
git_tree_free(tree);
free(copiedPath);
if (diffStatus != GIT_OK || git_diff_num_deltas(diffs) != 1) {
return CefV8Value::CreateNull();
@@ -162,6 +298,58 @@ namespace v8_extensions {
return result;
}
CefRefPtr<CefV8Value> GetLineDiffs(const char *path, const char *text) {
git_reference *head;
if (git_repository_head(&head, repo) != GIT_OK)
return CefV8Value::CreateNull();
const git_oid* sha = git_reference_target(head);
git_commit *commit;
int commitStatus = git_commit_lookup(&commit, repo, sha);
git_reference_free(head);
if (commitStatus != GIT_OK)
return CefV8Value::CreateNull();
git_tree *tree;
int treeStatus = git_commit_tree(&tree, commit);
git_commit_free(commit);
if (treeStatus != GIT_OK)
return CefV8Value::CreateNull();
git_tree_entry* treeEntry;
git_tree_entry_bypath(&treeEntry, tree, path);
git_blob *blob = NULL;
if (treeEntry != NULL) {
const git_oid *blobSha = git_tree_entry_id(treeEntry);
if (blobSha == NULL || git_blob_lookup(&blob, repo, blobSha) != GIT_OK)
blob = NULL;
}
git_tree_free(tree);
if (blob == NULL)
return CefV8Value::CreateNull();
int size = strlen(text);
std::vector<git_diff_range> ranges;
git_diff_options options = GIT_DIFF_OPTIONS_INIT;
options.context_lines = 1;
if (git_diff_blob_to_buffer(blob, text, size, &options, NULL, CollectDiffHunk, NULL, &ranges) == GIT_OK) {
CefRefPtr<CefV8Value> v8Ranges = CefV8Value::CreateArray(ranges.size());
for(int i = 0; i < ranges.size(); i++) {
CefRefPtr<CefV8Value> v8Range = CefV8Value::CreateObject(NULL);
v8Range->SetValue("oldStart", CefV8Value::CreateInt(ranges[i].old_start), V8_PROPERTY_ATTRIBUTE_NONE);
v8Range->SetValue("oldLines", CefV8Value::CreateInt(ranges[i].old_lines), V8_PROPERTY_ATTRIBUTE_NONE);
v8Range->SetValue("newStart", CefV8Value::CreateInt(ranges[i].new_start), V8_PROPERTY_ATTRIBUTE_NONE);
v8Range->SetValue("newLines", CefV8Value::CreateInt(ranges[i].new_lines), V8_PROPERTY_ATTRIBUTE_NONE);
v8Ranges->SetValue(i, v8Range);
}
git_blob_free(blob);
return v8Ranges;
} else {
git_blob_free(blob);
return CefV8Value::CreateNull();
}
}
CefRefPtr<CefV8Value> IsSubmodule(const char *path) {
BOOL isSubmodule = false;
git_index* index;
@@ -185,12 +373,14 @@ namespace v8_extensions {
};
Git::Git() : CefV8Handler() {
git_threads_init();
}
void Git::CreateContextBinding(CefRefPtr<CefV8Context> context) {
const char* methodNames[] = {
"getRepository", "getHead", "getPath", "isIgnored", "getStatus", "checkoutHead",
"getDiffStats", "isSubmodule", "refreshIndex", "destroy"
"getDiffStats", "isSubmodule", "refreshIndex", "destroy", "getStatuses",
"getAheadBehindCounts", "getLineDiffs"
};
CefRefPtr<CefV8Value> nativeObject = CefV8Value::CreateObject(NULL);
@@ -210,72 +400,94 @@ namespace v8_extensions {
const CefV8ValueList& arguments,
CefRefPtr<CefV8Value>& retval,
CefString& exception) {
if (name == "getRepository") {
GitRepository *repository = new GitRepository(arguments[0]->GetStringValue().ToString().c_str());
if (repository->Exists()) {
CefRefPtr<CefBase> userData = repository;
retval = CefV8Value::CreateObject(NULL);
retval->SetUserData(userData);
} else {
retval = CefV8Value::CreateNull();
@autoreleasepool {
if (name == "getRepository") {
GitRepository *repository = new GitRepository(arguments[0]->GetStringValue().ToString().c_str());
if (repository->Exists()) {
CefRefPtr<CefBase> userData = repository;
retval = CefV8Value::CreateObject(NULL);
retval->SetUserData(userData);
} else {
retval = CefV8Value::CreateNull();
}
return true;
}
return true;
}
if (name == "getHead") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
retval = userData->GetHead();
return true;
}
if (name == "getHead") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
retval = userData->GetHead();
return true;
}
if (name == "getPath") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
retval = userData->GetPath();
return true;
}
if (name == "getPath") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
retval = userData->GetPath();
return true;
}
if (name == "isIgnored") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
retval = userData->IsIgnored(arguments[0]->GetStringValue().ToString().c_str());
return true;
}
if (name == "isIgnored") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
retval = userData->IsIgnored(arguments[0]->GetStringValue().ToString().c_str());
return true;
}
if (name == "getStatus") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
retval = userData->GetStatus(arguments[0]->GetStringValue().ToString().c_str());
return true;
}
if (name == "getStatus") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
retval = userData->GetStatus(arguments[0]->GetStringValue().ToString().c_str());
return true;
}
if (name == "checkoutHead") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
retval = userData->CheckoutHead(arguments[0]->GetStringValue().ToString().c_str());
return true;
}
if (name == "checkoutHead") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
retval = userData->CheckoutHead(arguments[0]->GetStringValue().ToString().c_str());
return true;
}
if (name == "getDiffStats") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
retval = userData->GetDiffStats(arguments[0]->GetStringValue().ToString().c_str());
return true;
}
if (name == "getDiffStats") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
retval = userData->GetDiffStats(arguments[0]->GetStringValue().ToString().c_str());
return true;
}
if (name == "isSubmodule") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
retval = userData->IsSubmodule(arguments[0]->GetStringValue().ToString().c_str());
return true;
}
if (name == "isSubmodule") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
retval = userData->IsSubmodule(arguments[0]->GetStringValue().ToString().c_str());
return true;
}
if (name == "refreshIndex") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
userData->RefreshIndex();
return true;
}
if (name == "refreshIndex") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
userData->RefreshIndex();
return true;
}
if (name == "destroy") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
userData->Destroy();
return true;
}
if (name == "destroy") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
userData->Destroy();
return true;
}
return false;
if (name == "getStatuses") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
retval = userData->GetStatuses();
return true;
}
if (name == "getAheadBehindCounts") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
retval = userData->GetAheadBehindCounts();
return true;
}
if (name == "getLineDiffs") {
GitRepository *userData = (GitRepository *)object->GetUserData().get();
std::string path = arguments[0]->GetStringValue().ToString();
std::string text = arguments[1]->GetStringValue().ToString();
retval = userData->GetLineDiffs(path.c_str(), text.c_str());
return true;
}
return false;
}
}
}

View File

@@ -48,6 +48,7 @@ namespace v8_extensions {
const CefV8ValueList& arguments,
CefRefPtr<CefV8Value>& retval,
CefString& exception) {
@autoreleasepool {
if (name == "exists") {
std::string cc_value = arguments[0]->GetStringValue().ToString();
const char *path = cc_value.c_str();
@@ -165,8 +166,7 @@ namespace v8_extensions {
}
else if (name == "traverseTree") {
std::string argument = arguments[0]->GetStringValue().ToString();
int rootPathLength = argument.size() + 1;
char rootPath[rootPathLength];
char rootPath[argument.size() + 1];
strcpy(rootPath, argument.c_str());
char * const paths[] = {rootPath, NULL};
@@ -190,12 +190,8 @@ namespace v8_extensions {
continue;
}
int pathLength = entry->fts_pathlen - rootPathLength;
char relative[pathLength + 1];
relative[pathLength] = '\0';
strncpy(relative, entry->fts_path + rootPathLength, pathLength);
args.clear();
args.push_back(CefV8Value::CreateString(relative));
args.push_back(CefV8Value::CreateString(entry->fts_path));
if (isFile) {
onFile->ExecuteFunction(onFile, args);
}
@@ -526,10 +522,8 @@ namespace v8_extensions {
NSString *word = stringFromCefV8Value(arguments[0]);
NSSpellChecker *spellChecker = [NSSpellChecker sharedSpellChecker];
@synchronized(spellChecker) {
@autoreleasepool {
NSRange range = [spellChecker checkSpellingOfString:word startingAt:0];
retval = CefV8Value::CreateBool(range.length > 0);
}
NSRange range = [spellChecker checkSpellingOfString:word startingAt:0];
retval = CefV8Value::CreateBool(range.length > 0);
}
return true;
}
@@ -538,23 +532,22 @@ namespace v8_extensions {
NSString *misspelling = stringFromCefV8Value(arguments[0]);
NSSpellChecker *spellChecker = [NSSpellChecker sharedSpellChecker];
@synchronized(spellChecker) {
@autoreleasepool {
NSString *language = [spellChecker language];
NSRange range;
range.location = 0;
range.length = [misspelling length];
NSArray *guesses = [spellChecker guessesForWordRange:range inString:misspelling language:language inSpellDocumentWithTag:0];
CefRefPtr<CefV8Value> v8Guesses = CefV8Value::CreateArray([guesses count]);
for (int i = 0; i < [guesses count]; i++) {
v8Guesses->SetValue(i, CefV8Value::CreateString([[guesses objectAtIndex:i] UTF8String]));
}
retval = v8Guesses;
NSString *language = [spellChecker language];
NSRange range;
range.location = 0;
range.length = [misspelling length];
NSArray *guesses = [spellChecker guessesForWordRange:range inString:misspelling language:language inSpellDocumentWithTag:0];
CefRefPtr<CefV8Value> v8Guesses = CefV8Value::CreateArray([guesses count]);
for (int i = 0; i < [guesses count]; i++) {
v8Guesses->SetValue(i, CefV8Value::CreateString([[guesses objectAtIndex:i] UTF8String]));
}
retval = v8Guesses;
}
return true;
}
return false;
}
};
NSString *stringFromCefV8Value(const CefRefPtr<CefV8Value>& value) {

View File

@@ -73,32 +73,34 @@ bool OnigRegExp::Execute(const CefString& name,
CefRefPtr<CefV8Value>& retval,
CefString& exception) {
if (name == "search") {
CefRefPtr<CefV8Value> string = arguments[0];
CefRefPtr<CefV8Value> index = arguments.size() > 1 ? arguments[1] : CefV8Value::CreateInt(0);
OnigRegExpUserData *userData = (OnigRegExpUserData *)object->GetUserData().get();
retval = userData->Search(string, index);
return true;
}
else if (name == "test") {
CefRefPtr<CefV8Value> string = arguments[0];
CefRefPtr<CefV8Value> index = arguments.size() > 1 ? arguments[1] : CefV8Value::CreateInt(0);
OnigRegExpUserData *userData = (OnigRegExpUserData *)object->GetUserData().get();
retval = userData->Test(string, index);
return true;
}
else if (name == "buildOnigRegExp") {
CefRefPtr<CefV8Value> pattern = arguments[0];
CefRefPtr<OnigRegExpUserData> userData = new OnigRegExpUserData(pattern);
if (!userData->m_regex) {
exception = std::string("Failed to create OnigRegExp from pattern '") + pattern->GetStringValue().ToString() + "'";
@autoreleasepool {
if (name == "search") {
CefRefPtr<CefV8Value> string = arguments[0];
CefRefPtr<CefV8Value> index = arguments.size() > 1 ? arguments[1] : CefV8Value::CreateInt(0);
OnigRegExpUserData *userData = (OnigRegExpUserData *)object->GetUserData().get();
retval = userData->Search(string, index);
return true;
}
else if (name == "test") {
CefRefPtr<CefV8Value> string = arguments[0];
CefRefPtr<CefV8Value> index = arguments.size() > 1 ? arguments[1] : CefV8Value::CreateInt(0);
OnigRegExpUserData *userData = (OnigRegExpUserData *)object->GetUserData().get();
retval = userData->Test(string, index);
return true;
}
else if (name == "buildOnigRegExp") {
CefRefPtr<CefV8Value> pattern = arguments[0];
CefRefPtr<OnigRegExpUserData> userData = new OnigRegExpUserData(pattern);
if (!userData->m_regex) {
exception = std::string("Failed to create OnigRegExp from pattern '") + pattern->GetStringValue().ToString() + "'";
}
retval = CefV8Value::CreateObject(NULL);
retval->SetUserData((CefRefPtr<CefBase>)userData);
return true;
}
retval = CefV8Value::CreateObject(NULL);
retval->SetUserData((CefRefPtr<CefBase>)userData);
return true;
}
return false;
return false;
}
}
} // namespace v8_extensions

View File

@@ -152,18 +152,20 @@ bool OnigScanner::Execute(const CefString& name,
const CefV8ValueList& arguments,
CefRefPtr<CefV8Value>& retval,
CefString& exception) {
if (name == "findNextMatch") {
OnigScannerUserData *userData = (OnigScannerUserData *)object->GetUserData().get();
retval = userData->FindNextMatch(arguments[0], arguments[1]);
return true;
}
else if (name == "buildScanner") {
retval = CefV8Value::CreateObject(NULL);
retval->SetUserData(new OnigScannerUserData(arguments[0]));
return true;
}
@autoreleasepool {
if (name == "findNextMatch") {
OnigScannerUserData *userData = (OnigScannerUserData *)object->GetUserData().get();
retval = userData->FindNextMatch(arguments[0], arguments[1]);
return true;
}
else if (name == "buildScanner") {
retval = CefV8Value::CreateObject(NULL);
retval->SetUserData(new OnigScannerUserData(arguments[0]));
return true;
}
return false;
return false;
}
}
} // namespace v8_extensions

View File

@@ -37,78 +37,80 @@ namespace v8_extensions {
CefRefPtr<CefV8Value>& retval,
CefString& exception) {
if (name == "find") {
std::string path = arguments[0]->GetStringValue().ToString();
std::string tag = arguments[1]->GetStringValue().ToString();
tagFileInfo info;
tagFile* tagFile;
tagFile = tagsOpen(path.c_str(), &info);
if (info.status.opened) {
tagEntry entry;
std::vector<CefRefPtr<CefV8Value>> entries;
if (tagsFind(tagFile, &entry, tag.c_str(), TAG_FULLMATCH | TAG_OBSERVECASE) == TagSuccess) {
entries.push_back(ParseEntry(entry));
while (tagsFindNext(tagFile, &entry) == TagSuccess) {
entries.push_back(ParseEntry(entry));
}
}
retval = CefV8Value::CreateArray(entries.size());
for (int i = 0; i < entries.size(); i++) {
retval->SetValue(i, entries[i]);
}
tagsClose(tagFile);
}
return true;
}
if (name == "getAllTagsAsync") {
std::string path = arguments[0]->GetStringValue().ToString();
CefRefPtr<CefV8Value> callback = arguments[1];
CefRefPtr<CefV8Context> context = CefV8Context::GetCurrentContext();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
@autoreleasepool {
if (name == "find") {
std::string path = arguments[0]->GetStringValue().ToString();
std::string tag = arguments[1]->GetStringValue().ToString();
tagFileInfo info;
tagFile* tagFile;
tagFile = tagsOpen(path.c_str(), &info);
std::vector<tagEntry> 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<CefRefPtr<CefV8Value>> 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<CefV8Value> 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<CefV8Value> callback = arguments[1];
CefRefPtr<CefV8Context> 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<tagEntry> entries;
if (info.status.opened) {
tagEntry entry;
while (tagsNext(tagFile, &entry) == TagSuccess) {
entry.name = strdup(entry.name);
entry.file = strdup(entry.file);
if (entry.address.pattern) {
entry.address.pattern = strdup(entry.address.pattern);
}
entries.push_back(entry);
}
tagsClose(tagFile);
}
CefV8ValueList callbackArgs;
callbackArgs.push_back(v8Tags);
callback->ExecuteFunction(callback, callbackArgs);
context->Exit();
});
});
return true;
}
return false;
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(mainQueue, ^{
context->Enter();
CefRefPtr<CefV8Value> v8Tags = CefV8Value::CreateArray(entries.size());
for (int i = 0; i < entries.size(); i++) {
v8Tags->SetValue(i, ParseEntry(entries[i]));
free((void*)entries[i].name);
free((void*)entries[i].file);
if (entries[i].address.pattern) {
free((void*)entries[i].address.pattern);
}
}
CefV8ValueList callbackArgs;
callbackArgs.push_back(v8Tags);
callback->ExecuteFunction(callback, callbackArgs);
context->Exit();
});
});
return true;
}
return false;
}
}
}

View File

@@ -3,10 +3,11 @@
"version" : "0.0.0",
"dependencies": {
"coffee-script": "1.x",
"cson": "1.x"
"coffee-script": "1.5"
},
"private": true,
"scripts": {
"preinstall": "true"
}

21
script/compile-coffee Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/sh
set -e
# Because of the way xcodebuild invokes external scripts we need to load
# The Setup's environment ourselves. If this isn't done, things like the
# node shim won't be able to find the stuff they need.
node --version > /dev/null 2>&1 || {
if [ -e /opt/github/env.sh ]; then
source /opt/github/env.sh
else
# Try Constructicon's PATH.
export PATH="/usr/local/Cellar/node/0.8.21/bin:${PATH}"
fi
}
INPUT_FILE="${1}"
OUTPUT_FILE="${2}"
node_modules/.bin/coffee -c -p "${INPUT_FILE}" > "${OUTPUT_FILE}"

21
script/compile-cson Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/sh
set -e
# Because of the way xcodebuild invokes external scripts we need to load
# The Setup's environment ourselves. If this isn't done, things like the
# node shim won't be able to find the stuff they need.
node --version > /dev/null 2>&1 || {
if [ -e /opt/github/env.sh ]; then
source /opt/github/env.sh
else
# Try Constructicon's PATH.
export PATH="/usr/local/Cellar/node/0.8.21/bin:${PATH}"
fi
}
INPUT_FILE="${1}"
OUTPUT_FILE="${2}"
node_modules/.bin/coffee script/compile-cson.coffee -- "${INPUT_FILE}" "${OUTPUT_FILE}"

View File

@@ -0,0 +1,23 @@
fs = require 'fs'
{exec} = require 'child_process'
inputFile = process.argv[2]
unless inputFile?.length > 0
console.error("Input file must be first argument")
process.exit(1)
outputFile = process.argv[3]
unless outputFile?.length > 0
console.error("Output file must be second arguments")
process.exit(1)
contents = fs.readFileSync(inputFile)?.toString() ? ''
exec "node_modules/.bin/coffee -bcp #{inputFile}", (error, stdout, stderr) ->
if error
console.error(error)
process.exit(1)
json = eval(stdout.toString()) ? {}
if json isnt Object(json)
console.error("CSON file does not contain valid JSON")
process.exit(1)
fs.writeFileSync(outputFile, JSON.stringify(json, null, 2))

9
script/constructicon/prebuild Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/sh
set -ex
cd "$(dirname "$0")/../.."
export PATH="/usr/local/Cellar/node/0.8.21/bin:/usr/local/bin:${PATH}"
rake setup-codesigning create-xcode-project

View File

@@ -1,41 +1,10 @@
#!/bin/sh
# This can only be run by xcode or xcodebuild!
# Because of the way xcodebuild invokes external scripts we need to load
# The Setup's environment ourselves. If this isn't done, things like the
# node shim won't be able to find the stuff they need.
if [ -f /opt/github/env.sh ]; then
source /opt/github/env.sh
fi
set -e
COMPILED_SOURCES_DIR="${1}"
RESOUCES_PATH="$BUILT_PRODUCTS_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH"
DIRS="src static vendor"
# Compile .coffee files into bundle
COFFEE_FILES=$(find $DIRS -type file -name '*.coffee')
for COFFEE_FILE in $COFFEE_FILES; do
JS_FILE=$(echo "$RESOUCES_PATH/$COFFEE_FILE" | sed 's/.coffee/.js/' )
OUTPUT_PATH="$RESOUCES_PATH/$(dirname "$COFFEE_FILE")"
if [ $COFFEE_FILE -nt "$JS_FILE" ]; then
mkdir -p "$OUTPUT_PATH"
node_modules/.bin/coffee -c -o "$OUTPUT_PATH" "$COFFEE_FILE" || exit 1
fi
done;
# Compile .cson files into bundle
CSON_FILES=$(find $DIRS -type file -name '*.cson')
for CSON_FILE in $CSON_FILES; do
JSON_FILE=$(echo "$RESOUCES_PATH/$CSON_FILE" | sed 's/.cson/.json/' )
OUTPUT_PATH="$RESOUCES_PATH/$(dirname "$CSON_FILE")"
if [ $CSON_FILE -nt "$JSON_FILE" ]; then
mkdir -p "$OUTPUT_PATH"
node_modules/.bin/cson2json "$CSON_FILE" > "$JSON_FILE"
fi
done;
# Copy non-coffee files into bundle
rsync --archive --recursive --exclude="src/**/*.coffee" --exclude="src/**/*.cson" src static vendor spec benchmark themes dot-atom atom.sh "$RESOUCES_PATH"
rsync --archive --recursive --exclude="src/**/*.coffee" --exclude="src/**/*.cson" src static vendor spec benchmark themes dot-atom atom.sh "${COMPILED_SOURCES_DIR}/" "$RESOUCES_PATH"

32
script/generate-sources-gypi Executable file
View File

@@ -0,0 +1,32 @@
#!/bin/sh
set -e
cd "$(dirname $0)/.."
DIRS="src static vendor"
find_files() {
find ${DIRS} -type file -name ${1}
}
file_list() {
while read file; do
echo " '${file}',"
done
}
cat > sources.gypi <<EOF
{
'variables': {
'compiled_sources_dir': '<(INTERMEDIATE_DIR)/atom-resources',
'compiled_sources_dir_xcode': '\${INTERMEDIATE_DIR}/atom-resources',
'coffee_sources': [
$(find_files '*.coffee' | file_list)
],
'cson_sources': [
$(find_files '*.cson' | file_list)
],
},
}
EOF

View File

@@ -3,7 +3,8 @@
# From root of libgit2 repo:
# mkdir build
# cd build
# cmake .. -DCMAKE_INSTALL_PREFIX=~/repos/atom/git2 -DCMAKE_OSX_ARCHITECTURES="i386;x86_64" -DCMAKE_BUILD_TYPE=Release
# cmake .. -DCMAKE_INSTALL_PREFIX=~/github/atom/git2 -DCMAKE_OSX_ARCHITECTURES="i386;x86_64" -DCMAKE_BUILD_TYPE=Release -DTHREADSAFE=1 -DBUILD_CLAR=OFF
# cmake --build . --target install
#
# From root of atom repo:

View File

@@ -3,18 +3,31 @@ AtomPackage = require 'atom-package'
fs = require 'fs'
describe "AtomPackage", ->
[packageMainModule, pack] = []
beforeEach ->
pack = new AtomPackage(fs.resolve(config.packageDirPaths..., 'package-with-activation-events'))
pack.load()
describe ".load()", ->
describe "if the package's metadata has a `deferredDeserializers` array", ->
it "requires the package's main module attempting to use deserializers named in the array", ->
expect(pack.mainModule).toBeNull()
object = deserialize(deserializer: 'Foo', data: "Hello")
expect(object.constructor.name).toBe 'Foo'
expect(object.data).toBe 'Hello'
expect(pack.mainModule).toBeDefined()
expect(pack.mainModule.activateCallCount).toBe 0
describe ".activate()", ->
beforeEach ->
window.rootView = new RootView
packageMainModule = require 'fixtures/packages/package-with-activation-events/main'
spyOn(packageMainModule, 'activate').andCallThrough()
describe "when the package metadata includes activation events", ->
[packageMainModule, pack] = []
beforeEach ->
pack = new AtomPackage(fs.resolve(config.packageDirPaths..., 'package-with-activation-events'))
packageMainModule = require 'fixtures/packages/package-with-activation-events/main'
spyOn(packageMainModule, 'activate').andCallThrough()
pack.load()
pack.activate()
it "defers activating the package until an activation event bubbles to the root view", ->
expect(packageMainModule.activate).not.toHaveBeenCalled()
@@ -23,7 +36,7 @@ describe "AtomPackage", ->
it "triggers the activation event on all handlers registered during activation", ->
rootView.open('sample.js')
editor = rootView.getActiveEditor()
editor = rootView.getActiveView()
eventHandler = jasmine.createSpy("activation-event")
editor.command 'activation-event', eventHandler
editor.trigger 'activation-event'
@@ -44,6 +57,7 @@ describe "AtomPackage", ->
expect(packageMainModule.activate).not.toHaveBeenCalled()
pack.load()
pack.activate()
expect(packageMainModule.activate).toHaveBeenCalled()
describe "when the package doesn't have an index.coffee", ->

View File

@@ -84,26 +84,26 @@ describe "the `atom` global", ->
describe "activation", ->
it "calls activate on the package main with its previous state", ->
pack = window.loadPackage('package-with-module')
spyOn(pack.packageMain, 'activate')
spyOn(pack.mainModule, 'activate')
serializedState = rootView.serialize()
rootView.deactivate()
RootView.deserialize(serializedState)
window.loadPackage('package-with-module')
expect(pack.packageMain.activate).toHaveBeenCalledWith(someNumber: 1)
expect(pack.mainModule.activate).toHaveBeenCalledWith(someNumber: 1)
describe "deactivation", ->
it "deactivates and removes the package module from the package module map", ->
pack = window.loadPackage('package-with-module')
expect(atom.activatedAtomPackages.length).toBe 1
spyOn(pack.packageMain, "deactivate").andCallThrough()
spyOn(pack.mainModule, "deactivate").andCallThrough()
atom.deactivateAtomPackages()
expect(pack.packageMain.deactivate).toHaveBeenCalled()
expect(pack.mainModule.deactivate).toHaveBeenCalled()
expect(atom.activatedAtomPackages.length).toBe 0
describe "serialization", ->
it "uses previous serialization state on unactivated packages", ->
it "uses previous serialization state on packages whose activation has been deferred", ->
atom.atomPackageStates['package-with-activation-events'] = {previousData: 'exists'}
unactivatedPackage = window.loadPackage('package-with-activation-events')
activatedPackage = window.loadPackage('package-with-module')
@@ -115,7 +115,8 @@ describe "the `atom` global", ->
'previousData': 'exists'
# ensure serialization occurs when the packageis activated
unactivatedPackage.activatePackageMain()
unactivatedPackage.deferActivation = false
unactivatedPackage.activate()
expect(atom.serializeAtomPackages()).toEqual
'package-with-module':
'someNumber': 1
@@ -124,8 +125,8 @@ describe "the `atom` global", ->
it "absorbs exceptions that are thrown by the package module's serialize methods", ->
spyOn(console, 'error')
window.loadPackage('package-with-module')
window.loadPackage('package-with-serialize-error', activateImmediately: true)
window.loadPackage('package-with-module', activateImmediately: true)
window.loadPackage('package-with-serialize-error', activateImmediately: true)
packageStates = atom.serializeAtomPackages()
expect(packageStates['package-with-module']).toEqual someNumber: 1
@@ -141,3 +142,74 @@ describe "the `atom` global", ->
runs ->
expect(versionHandler.argsForCall[0][0]).toMatch /^\d+\.\d+(\.\d+)?$/
describe "modal native dialogs", ->
beforeEach ->
spyOn(atom, 'sendMessageToBrowserProcess')
atom.sendMessageToBrowserProcess.simulateConfirmation = (buttonText) ->
labels = @argsForCall[0][1][2...]
callbacks = @argsForCall[0][2]
@reset()
callbacks[labels.indexOf(buttonText)]()
advanceClock 50
atom.sendMessageToBrowserProcess.simulatePathSelection = (path) ->
callback = @argsForCall[0][2]
@reset()
callback(path)
advanceClock 50
it "only presents one native dialog at a time", ->
confirmHandler = jasmine.createSpy("confirmHandler")
selectPathHandler = jasmine.createSpy("selectPathHandler")
atom.confirm "Are you happy?", "really, truly happy?", "Yes", confirmHandler, "No"
atom.confirm "Are you happy?", "really, truly happy?", "Yes", confirmHandler, "No"
atom.showSaveDialog(selectPathHandler)
atom.showSaveDialog(selectPathHandler)
expect(atom.sendMessageToBrowserProcess.callCount).toBe 1
atom.sendMessageToBrowserProcess.simulateConfirmation("Yes")
expect(confirmHandler).toHaveBeenCalled()
expect(atom.sendMessageToBrowserProcess.callCount).toBe 1
atom.sendMessageToBrowserProcess.simulateConfirmation("No")
expect(atom.sendMessageToBrowserProcess.callCount).toBe 1
atom.sendMessageToBrowserProcess.simulatePathSelection('/selected/path')
expect(selectPathHandler).toHaveBeenCalledWith('/selected/path')
selectPathHandler.reset()
expect(atom.sendMessageToBrowserProcess.callCount).toBe 1
it "prioritizes dialogs presented as the result of dismissing other dialogs before any previously deferred dialogs", ->
atom.confirm "A1", "", "Next", ->
atom.confirm "B1", "", "Next", ->
atom.confirm "C1", "", "Next", ->
atom.confirm "C2", "", "Next", ->
atom.confirm "B2", "", "Next", ->
atom.confirm "A2", "", "Next", ->
expect(atom.sendMessageToBrowserProcess.callCount).toBe 1
expect(atom.sendMessageToBrowserProcess.argsForCall[0][1][0]).toBe "A1"
atom.sendMessageToBrowserProcess.simulateConfirmation('Next')
expect(atom.sendMessageToBrowserProcess.callCount).toBe 1
expect(atom.sendMessageToBrowserProcess.argsForCall[0][1][0]).toBe "B1"
atom.sendMessageToBrowserProcess.simulateConfirmation('Next')
expect(atom.sendMessageToBrowserProcess.callCount).toBe 1
expect(atom.sendMessageToBrowserProcess.argsForCall[0][1][0]).toBe "C1"
atom.sendMessageToBrowserProcess.simulateConfirmation('Next')
expect(atom.sendMessageToBrowserProcess.callCount).toBe 1
expect(atom.sendMessageToBrowserProcess.argsForCall[0][1][0]).toBe "C2"
atom.sendMessageToBrowserProcess.simulateConfirmation('Next')
expect(atom.sendMessageToBrowserProcess.callCount).toBe 1
expect(atom.sendMessageToBrowserProcess.argsForCall[0][1][0]).toBe "B2"
atom.sendMessageToBrowserProcess.simulateConfirmation('Next')
expect(atom.sendMessageToBrowserProcess.callCount).toBe 1
expect(atom.sendMessageToBrowserProcess.argsForCall[0][1][0]).toBe "A2"
atom.sendMessageToBrowserProcess.simulateConfirmation('Next')

View File

@@ -180,24 +180,88 @@ describe 'Buffer', ->
waitsFor 'change event', ->
changeHandler.callCount > 0
describe ".isModified()", ->
it "returns true when user changes buffer", ->
describe "modified status", ->
it "reports the modified status changing to true or false after the user changes buffer", ->
modifiedHandler = jasmine.createSpy("modifiedHandler")
buffer.on 'modified-status-changed', modifiedHandler
expect(buffer.isModified()).toBeFalsy()
buffer.insert([0,0], "hi")
expect(buffer.isModified()).toBe true
it "returns false after modified buffer is saved", ->
advanceClock(buffer.stoppedChangingDelay)
expect(modifiedHandler).toHaveBeenCalledWith(true)
modifiedHandler.reset()
buffer.insert([0,2], "ho")
advanceClock(buffer.stoppedChangingDelay)
expect(modifiedHandler).not.toHaveBeenCalled()
modifiedHandler.reset()
buffer.undo()
buffer.undo()
advanceClock(buffer.stoppedChangingDelay)
expect(modifiedHandler).toHaveBeenCalledWith(false)
it "reports the modified status changing to true after the underlying file is deleted", ->
buffer.release()
filePath = "/tmp/atom-tmp-file"
fs.write(filePath, 'delete me')
buffer = new Buffer(filePath)
modifiedHandler = jasmine.createSpy("modifiedHandler")
buffer.on 'modified-status-changed', modifiedHandler
fs.remove(filePath)
waitsFor "modified status to change", -> modifiedHandler.callCount
runs -> expect(buffer.isModified()).toBe true
it "reports the modified status changing to false after a modified buffer is saved", ->
filePath = "/tmp/atom-tmp-file"
fs.write(filePath, '')
buffer.release()
buffer = new Buffer(filePath)
expect(buffer.isModified()).toBe false
modifiedHandler = jasmine.createSpy("modifiedHandler")
buffer.on 'modified-status-changed', modifiedHandler
buffer.insert([0,0], "hi")
advanceClock(buffer.stoppedChangingDelay)
expect(buffer.isModified()).toBe true
modifiedHandler.reset()
buffer.save()
expect(modifiedHandler).toHaveBeenCalledWith(false)
expect(buffer.isModified()).toBe false
modifiedHandler.reset()
buffer.insert([0, 0], 'x')
advanceClock(buffer.stoppedChangingDelay)
expect(modifiedHandler).toHaveBeenCalledWith(true)
expect(buffer.isModified()).toBe true
it "reports the modified status changing to false after a modified buffer is reloaded", ->
filePath = "/tmp/atom-tmp-file"
fs.write(filePath, '')
buffer.release()
buffer = new Buffer(filePath)
modifiedHandler = jasmine.createSpy("modifiedHandler")
buffer.on 'modified-status-changed', modifiedHandler
buffer.insert([0,0], "hi")
advanceClock(buffer.stoppedChangingDelay)
expect(buffer.isModified()).toBe true
modifiedHandler.reset()
buffer.reload()
expect(modifiedHandler).toHaveBeenCalledWith(false)
expect(buffer.isModified()).toBe false
modifiedHandler.reset()
buffer.insert([0, 0], 'x')
advanceClock(buffer.stoppedChangingDelay)
expect(modifiedHandler).toHaveBeenCalledWith(true)
expect(buffer.isModified()).toBe true
it "returns false for an empty buffer with no path", ->
buffer.release()
@@ -1056,61 +1120,30 @@ describe 'Buffer', ->
expect(buffer.isEmpty()).toBeFalsy()
describe "'contents-modified' event", ->
describe "when the buffer is deleted", ->
it "triggers the contents-modified event", ->
delay = buffer.stoppedChangingDelay
path = "/tmp/atom-file-to-delete.txt"
fs.write(path, 'delete me')
bufferToDelete = new Buffer(path)
contentsModifiedHandler = jasmine.createSpy("contentsModifiedHandler")
bufferToDelete.on 'contents-modified', contentsModifiedHandler
it "triggers the 'contents-modified' event with the current modified status when the buffer changes, rate-limiting events with a delay", ->
delay = buffer.stoppedChangingDelay
contentsModifiedHandler = jasmine.createSpy("contentsModifiedHandler")
buffer.on 'contents-modified', contentsModifiedHandler
expect(bufferToDelete.getPath()).toBe path
expect(bufferToDelete.isModified()).toBeFalsy()
expect(contentsModifiedHandler).not.toHaveBeenCalled()
buffer.insert([0, 0], 'a')
expect(contentsModifiedHandler).not.toHaveBeenCalled()
removeHandler = jasmine.createSpy('removeHandler')
bufferToDelete.file.on 'removed', removeHandler
fs.remove(path)
waitsFor "file to be removed", ->
removeHandler.callCount > 0
advanceClock(delay / 2)
runs ->
expect(contentsModifiedHandler).toHaveBeenCalledWith(differsFromDisk:true)
bufferToDelete.destroy()
buffer.insert([0, 0], 'b')
expect(contentsModifiedHandler).not.toHaveBeenCalled()
describe "when the buffer text has been changed", ->
it "triggers the contents-modified event 'stoppedChangingDelay' ms after the last buffer change", ->
delay = buffer.stoppedChangingDelay
contentsModifiedHandler = jasmine.createSpy("contentsModifiedHandler")
buffer.on 'contents-modified', contentsModifiedHandler
advanceClock(delay / 2)
expect(contentsModifiedHandler).not.toHaveBeenCalled()
buffer.insert([0, 0], 'a')
expect(contentsModifiedHandler).not.toHaveBeenCalled()
advanceClock(delay / 2)
expect(contentsModifiedHandler).toHaveBeenCalledWith(true)
advanceClock(delay / 2)
buffer.insert([0, 0], 'b')
expect(contentsModifiedHandler).not.toHaveBeenCalled()
advanceClock(delay / 2)
expect(contentsModifiedHandler).not.toHaveBeenCalled()
advanceClock(delay / 2)
expect(contentsModifiedHandler).toHaveBeenCalled()
it "triggers the contents-modified event with data about whether its contents differ from the contents on disk", ->
delay = buffer.stoppedChangingDelay
contentsModifiedHandler = jasmine.createSpy("contentsModifiedHandler")
buffer.on 'contents-modified', contentsModifiedHandler
buffer.insert([0, 0], 'a')
advanceClock(delay)
expect(contentsModifiedHandler).toHaveBeenCalledWith(differsFromDisk:true)
buffer.delete([[0, 0], [0, 1]], '')
advanceClock(delay)
expect(contentsModifiedHandler).toHaveBeenCalledWith(differsFromDisk:false)
contentsModifiedHandler.reset()
buffer.undo()
buffer.undo()
advanceClock(delay)
expect(contentsModifiedHandler).toHaveBeenCalledWith(false)
describe ".append(text)", ->
it "adds text to the end of the buffer", ->

View File

@@ -1,3 +1,4 @@
Config = require 'config'
fs = require 'fs'
describe "Config", ->
@@ -133,3 +134,21 @@ describe "Config", ->
expect(fs.isFile(fs.join(config.configDirPath, 'themes/atom-light-ui/package.cson'))).toBeTruthy()
expect(fs.isFile(fs.join(config.configDirPath, 'themes/atom-dark-syntax.css'))).toBeTruthy()
expect(fs.isFile(fs.join(config.configDirPath, 'themes/atom-light-syntax.css'))).toBeTruthy()
describe "when the config file is not parseable", ->
beforeEach ->
config.configDirPath = '/tmp/dot-atom-dir'
config.configFilePath = fs.join(config.configDirPath, "config.cson")
expect(fs.exists(config.configDirPath)).toBeFalsy()
afterEach ->
fs.remove('/tmp/dot-atom-dir') if fs.exists('/tmp/dot-atom-dir')
it "logs an error to the console and does not overwrite the config file", ->
config.save.reset()
spyOn(console, 'error')
fs.write(config.configFilePath, "{{{{{")
config.loadUserConfig()
config.set("hair", "blonde") # trigger a save
expect(console.error).toHaveBeenCalled()
expect(config.save).not.toHaveBeenCalled()

View File

@@ -1,11 +1,12 @@
DisplayBuffer = require 'display-buffer'
Buffer = require 'buffer'
_ = require 'underscore'
describe "DisplayBuffer", ->
[editSession, displayBuffer, buffer, changeHandler, tabLength] = []
beforeEach ->
tabLength = 2
editSession = fixturesProject.buildEditSessionForPath('sample.js', { tabLength })
editSession = project.buildEditSession('sample.js', { tabLength })
{ buffer, displayBuffer } = editSession
changeHandler = jasmine.createSpy 'changeHandler'
displayBuffer.on 'changed', changeHandler
@@ -55,6 +56,13 @@ describe "DisplayBuffer", ->
describe "when the buffer changes", ->
describe "when buffer lines are updated", ->
describe "when whitespace is added after the max line length", ->
it "adds whitespace to the end of the current line and wraps an empty line", ->
fiftyCharacters = _.multiplyString("x", 50)
editSession.buffer.setText(fiftyCharacters)
editSession.setCursorBufferPosition([0, 51])
editSession.insertText(" ")
describe "when the update makes a soft-wrapped line shorter than the max line length", ->
it "rewraps the line and emits a change event", ->
buffer.delete([[6, 24], [6, 42]])
@@ -220,7 +228,7 @@ describe "DisplayBuffer", ->
editSession2 = null
beforeEach ->
editSession2 = fixturesProject.buildEditSessionForPath('two-hundred.txt')
editSession2 = project.buildEditSession('two-hundred.txt')
{ buffer, displayBuffer } = editSession2
displayBuffer.on 'changed', changeHandler

View File

@@ -9,12 +9,30 @@ describe "EditSession", ->
buffer.setText(buffer.getText().replace(/[ ]{2}/g, "\t"))
beforeEach ->
editSession = fixturesProject.buildEditSessionForPath('sample.js', autoIndent: false)
editSession = project.buildEditSession('sample.js', autoIndent: false)
buffer = editSession.buffer
lineLengths = buffer.getLines().map (line) -> line.length
afterEach ->
fixturesProject.destroy()
describe "title", ->
describe ".getTitle()", ->
it "uses the basename of the buffer's path as its title, or 'untitled' if the path is undefined", ->
expect(editSession.getTitle()).toBe 'sample.js'
buffer.setPath(undefined)
expect(editSession.getTitle()).toBe 'untitled'
describe ".getLongTitle()", ->
it "appends the name of the containing directory to the basename of the file", ->
expect(editSession.getLongTitle()).toBe 'sample.js - fixtures'
buffer.setPath(undefined)
expect(editSession.getLongTitle()).toBe 'untitled'
it "emits 'title-changed' events when the underlying buffer path", ->
titleChangedHandler = jasmine.createSpy("titleChangedHandler")
editSession.on 'title-changed', titleChangedHandler
buffer.setPath('/foo/bar/baz.txt')
buffer.setPath(undefined)
expect(titleChangedHandler.callCount).toBe 2
describe "cursor", ->
describe ".getCursor()", ->
@@ -793,12 +811,21 @@ describe "EditSession", ->
expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1)
describe "when the preceding does not match an auto-indent pattern", ->
it "auto-decreases the indentation of the line to be one level below that of the preceding line", ->
editSession.setCursorBufferPosition([3, Infinity])
editSession.insertText('\n', autoIndent: true)
expect(editSession.indentationForBufferRow(4)).toBe editSession.indentationForBufferRow(3)
editSession.insertText(' }', autoIndent: true)
expect(editSession.indentationForBufferRow(4)).toBe editSession.indentationForBufferRow(3) - 1
describe "when the inserted text is whitespace", ->
it "does not auto-decreases the indentation", ->
editSession.setCursorBufferPosition([12, 0])
editSession.insertText(' ', autoIndent: true)
expect(editSession.lineForBufferRow(12)).toBe ' };'
editSession.insertText('\t\t', autoIndent: true)
expect(editSession.lineForBufferRow(12)).toBe ' \t\t};'
describe "when the inserted text is non-whitespace", ->
it "auto-decreases the indentation of the line to be one level below that of the preceding line", ->
editSession.setCursorBufferPosition([3, Infinity])
editSession.insertText('\n', autoIndent: true)
expect(editSession.indentationForBufferRow(4)).toBe editSession.indentationForBufferRow(3)
editSession.insertText(' }', autoIndent: true)
expect(editSession.indentationForBufferRow(4)).toBe editSession.indentationForBufferRow(3) - 1
describe "when the current line does not match an auto-outdent pattern", ->
it "leaves the line unchanged", ->
@@ -1706,7 +1733,7 @@ describe "EditSession", ->
it "does not explode if the current language mode has no comment regex", ->
editSession.destroy()
editSession = fixturesProject.buildEditSessionForPath(null, autoIndent: false)
editSession = project.buildEditSession(null, autoIndent: false)
editSession.setSelectedBufferRange([[4, 5], [4, 5]])
editSession.toggleLineCommentsInSelection()
expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {"
@@ -1784,7 +1811,7 @@ describe "EditSession", ->
expect(editSession.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 18], [1, 18]]]
it "restores selected ranges even when the change occurred in another edit session", ->
otherEditSession = fixturesProject.buildEditSessionForPath(editSession.getPath())
otherEditSession = project.buildEditSession(editSession.getPath())
otherEditSession.setSelectedBufferRange([[2, 2], [3, 3]])
otherEditSession.delete()
@@ -1977,13 +2004,13 @@ describe "EditSession", ->
describe "soft-tabs detection", ->
it "assign soft / hard tabs based on the contents of the buffer, or uses the default if unknown", ->
editSession = fixturesProject.buildEditSessionForPath('sample.js', softTabs: false)
editSession = project.buildEditSession('sample.js', softTabs: false)
expect(editSession.softTabs).toBeTruthy()
editSession = fixturesProject.buildEditSessionForPath('sample-with-tabs.coffee', softTabs: true)
editSession = project.buildEditSession('sample-with-tabs.coffee', softTabs: true)
expect(editSession.softTabs).toBeFalsy()
editSession = fixturesProject.buildEditSessionForPath(null, softTabs: false)
editSession = project.buildEditSession(null, softTabs: false)
expect(editSession.softTabs).toBeFalsy()
describe ".indentLevelForLine(line)", ->
@@ -2006,6 +2033,19 @@ describe "EditSession", ->
editSession.buffer.reload()
expect(editSession.getCursorScreenPosition()).toEqual [0,1]
describe "when the 'grammars-loaded' event is triggered on the syntax global", ->
it "reloads the edit session's grammar and re-tokenizes the buffer if it changes", ->
editSession.destroy()
grammarToReturn = syntax.grammarByFileTypeSuffix('txt')
spyOn(syntax, 'grammarForFilePath').andCallFake -> grammarToReturn
editSession = project.buildEditSession('sample.js', autoIndent: false)
expect(editSession.lineForScreenRow(0).tokens.length).toBe 1
grammarToReturn = syntax.grammarByFileTypeSuffix('js')
syntax.trigger 'grammars-loaded'
expect(editSession.lineForScreenRow(0).tokens.length).toBeGreaterThan 1
describe "auto-indent", ->
describe "editor.autoIndent", ->
it "auto-indents newlines if editor.autoIndent is true", ->

View File

@@ -1,4 +1,3 @@
RootView = require 'root-view'
EditSession = require 'edit-session'
Buffer = require 'buffer'
Editor = require 'editor'
@@ -10,75 +9,41 @@ _ = require 'underscore'
fs = require 'fs'
describe "Editor", ->
[buffer, editor, cachedLineHeight] = []
[buffer, editor, editSession, cachedLineHeight, cachedCharWidth] = []
beforeEach ->
editSession = project.buildEditSession('sample.js')
buffer = editSession.buffer
editor = new Editor(editSession)
editor.lineOverdraw = 2
editor.isFocused = true
editor.enableKeymap()
editor.attachToDom = ({ heightInLines, widthInChars } = {}) ->
heightInLines ?= this.getBuffer().getLineCount()
this.height(getLineHeight() * heightInLines)
this.width(getCharWidth() * widthInChars) if widthInChars
$('#jasmine-content').append(this)
getLineHeight = ->
return cachedLineHeight if cachedLineHeight?
editorForMeasurement = new Editor(editSession: project.buildEditSessionForPath('sample.js'))
editorForMeasurement.attachToDom()
cachedLineHeight = editorForMeasurement.lineHeight
editorForMeasurement.remove()
calcDimensions()
cachedLineHeight
beforeEach ->
window.rootView = new RootView
rootView.open('sample.js')
editor = rootView.getActiveEditor()
buffer = editor.getBuffer()
getCharWidth = ->
return cachedCharWidth if cachedCharWidth?
calcDimensions()
cachedCharWidth
editor.attachToDom = ({ heightInLines } = {}) ->
heightInLines ?= this.getBuffer().getLineCount()
this.height(getLineHeight() * heightInLines)
$('#jasmine-content').append(this)
editor.lineOverdraw = 2
editor.enableKeymap()
editor.isFocused = true
calcDimensions = ->
editorForMeasurement = new Editor(editSession: project.buildEditSession('sample.js'))
editorForMeasurement.attachToDom()
cachedLineHeight = editorForMeasurement.lineHeight
cachedCharWidth = editorForMeasurement.charWidth
editorForMeasurement.remove()
describe "construction", ->
it "throws an error if no editor session is given unless deserializing", ->
it "throws an error if no edit session is given", ->
expect(-> new Editor).toThrow()
expect(-> new Editor(deserializing: true)).not.toThrow()
describe ".copy()", ->
it "builds a new editor with the same edit sessions, cursor position, and scroll position as the receiver", ->
rootView.attachToDom()
rootView.height(8 * editor.lineHeight)
rootView.width(50 * editor.charWidth)
editor.edit(project.buildEditSessionForPath('two-hundred.txt'))
editor.setCursorScreenPosition([5, 1])
editor.scrollTop(1.5 * editor.lineHeight)
editor.scrollView.scrollLeft(44)
# proves this test covers serialization and deserialization
spyOn(editor, 'serialize').andCallThrough()
spyOn(Editor, 'deserialize').andCallThrough()
newEditor = editor.copy()
expect(editor.serialize).toHaveBeenCalled()
expect(Editor.deserialize).toHaveBeenCalled()
expect(newEditor.getBuffer()).toBe editor.getBuffer()
expect(newEditor.getCursorScreenPosition()).toEqual editor.getCursorScreenPosition()
expect(newEditor.editSessions).toEqual(editor.editSessions)
expect(newEditor.activeEditSession).toEqual(editor.activeEditSession)
expect(newEditor.getActiveEditSessionIndex()).toEqual(editor.getActiveEditSessionIndex())
newEditor.height(editor.height())
newEditor.width(editor.width())
newEditor.attachToDom()
expect(newEditor.scrollTop()).toBe editor.scrollTop()
expect(newEditor.scrollView.scrollLeft()).toBe 44
it "does not blow up if no file exists for a previous edit session, but prints a warning", ->
spyOn(console, 'warn')
fs.write('/tmp/delete-me')
editor.edit(project.buildEditSessionForPath('/tmp/delete-me'))
fs.remove('/tmp/delete-me')
newEditor = editor.copy()
expect(console.warn).toHaveBeenCalled()
describe "when the editor is attached to the dom", ->
it "calculates line height and char width and updates the pixel position of the cursor", ->
@@ -117,7 +82,7 @@ describe "Editor", ->
it "triggers an alert", ->
path = "/tmp/atom-changed-file.txt"
fs.write(path, "")
editSession = project.buildEditSessionForPath(path)
editSession = project.buildEditSession(path)
editor.edit(editSession)
editor.insertText("now the buffer is modified")
@@ -135,272 +100,66 @@ describe "Editor", ->
expect(atom.confirm).toHaveBeenCalled()
describe ".remove()", ->
it "removes subscriptions from all edit session buffers", ->
previousEditSession = editor.activeEditSession
otherEditSession = project.buildEditSessionForPath(project.resolve('sample.txt'))
expect(previousEditSession.buffer.subscriptionCount()).toBeGreaterThan 1
editor.edit(otherEditSession)
expect(otherEditSession.buffer.subscriptionCount()).toBeGreaterThan 1
it "destroys the edit session", ->
editor.remove()
expect(previousEditSession.buffer.subscriptionCount()).toBe 0
expect(otherEditSession.buffer.subscriptionCount()).toBe 0
describe "when 'close' is triggered", ->
it "adds a closed session path to the array", ->
editor.edit(project.buildEditSessionForPath())
editSession = editor.activeEditSession
expect(editor.closedEditSessions.length).toBe 0
editor.trigger "core:close"
expect(editor.closedEditSessions.length).toBe 0
editor.edit(project.buildEditSessionForPath(project.resolve('sample.txt')))
editor.trigger "core:close"
expect(editor.closedEditSessions.length).toBe 1
it "closes the active edit session and loads next edit session", ->
editor.edit(project.buildEditSessionForPath())
editSession = editor.activeEditSession
spyOn(editSession.buffer, 'isModified').andReturn false
spyOn(editSession, 'destroy').andCallThrough()
spyOn(editor, "remove").andCallThrough()
editor.trigger "core:close"
expect(editSession.destroy).toHaveBeenCalled()
expect(editor.remove).not.toHaveBeenCalled()
expect(editor.getBuffer()).toBe buffer
it "triggers the 'editor:edit-session-removed' event with the edit session and its former index", ->
editor.edit(project.buildEditSessionForPath())
editSession = editor.activeEditSession
index = editor.getActiveEditSessionIndex()
spyOn(editSession.buffer, 'isModified').andReturn false
editSessionRemovedHandler = jasmine.createSpy('editSessionRemovedHandler')
editor.on 'editor:edit-session-removed', editSessionRemovedHandler
editor.trigger "core:close"
expect(editSessionRemovedHandler).toHaveBeenCalled()
expect(editSessionRemovedHandler.argsForCall[0][1..2]).toEqual [editSession, index]
it "calls remove on the editor if there is one edit session and mini is false", ->
editSession = editor.activeEditSession
expect(editor.mini).toBeFalsy()
expect(editor.editSessions.length).toBe 1
spyOn(editor, 'remove').andCallThrough()
editor.trigger 'core:close'
spyOn(editSession, 'destroy').andCallThrough()
expect(editor.remove).toHaveBeenCalled()
miniEditor = new Editor(mini: true)
spyOn(miniEditor, 'remove').andCallThrough()
miniEditor.trigger 'core:close'
expect(miniEditor.remove).not.toHaveBeenCalled()
describe "when buffer is modified", ->
it "triggers an alert and does not close the session", ->
spyOn(editor, 'remove').andCallThrough()
spyOn(atom, 'confirm')
editor.insertText("I AM CHANGED!")
editor.trigger "core:close"
expect(editor.remove).not.toHaveBeenCalled()
expect(atom.confirm).toHaveBeenCalled()
it "doesn't trigger an alert if the buffer is opened in multiple sessions", ->
spyOn(editor, 'remove').andCallThrough()
spyOn(atom, 'confirm')
editor.insertText("I AM CHANGED!")
editor.splitLeft()
editor.trigger "core:close"
expect(editor.remove).toHaveBeenCalled()
expect(atom.confirm).not.toHaveBeenCalled()
expect(editor.activeEditSession.destroyed).toBeTruthy()
describe ".edit(editSession)", ->
otherEditSession = null
[newEditSession, newBuffer] = []
beforeEach ->
otherEditSession = project.buildEditSessionForPath()
newEditSession = project.buildEditSession('two-hundred.txt')
newBuffer = newEditSession.buffer
describe "when the edit session wasn't previously assigned to this editor", ->
it "adds edit session to editor and triggers the 'editor:edit-session-added' event", ->
editSessionAddedHandler = jasmine.createSpy('editSessionAddedHandler')
editor.on 'editor:edit-session-added', editSessionAddedHandler
it "updates the rendered lines, cursors, selections, scroll position, and event subscriptions to match the given edit session", ->
editor.attachToDom(heightInLines: 5, widthInChars: 30)
editor.setCursorBufferPosition([3, 5])
editor.scrollToBottom()
editor.scrollView.scrollLeft(150)
previousScrollHeight = editor.verticalScrollbar.prop('scrollHeight')
previousScrollTop = editor.scrollTop()
previousScrollLeft = editor.scrollView.scrollLeft()
originalEditSessionCount = editor.editSessions.length
editor.edit(otherEditSession)
expect(editor.activeEditSession).toBe otherEditSession
expect(editor.editSessions.length).toBe originalEditSessionCount + 1
newEditSession.scrollTop = 120
newEditSession.setSelectedBufferRange([[40, 0], [43, 1]])
expect(editSessionAddedHandler).toHaveBeenCalled()
expect(editSessionAddedHandler.argsForCall[0][1..2]).toEqual [otherEditSession, originalEditSessionCount]
editor.edit(newEditSession)
{ firstRenderedScreenRow, lastRenderedScreenRow } = editor
expect(editor.lineElementForScreenRow(firstRenderedScreenRow).text()).toBe newBuffer.lineForRow(firstRenderedScreenRow)
expect(editor.lineElementForScreenRow(lastRenderedScreenRow).text()).toBe newBuffer.lineForRow(editor.lastRenderedScreenRow)
expect(editor.scrollTop()).toBe 120
expect(editor.getSelectionView().regions[0].position().top).toBe 40 * editor.lineHeight
editor.insertText("hello")
expect(editor.lineElementForScreenRow(40).text()).toBe "hello3"
describe "when the edit session was previously assigned to this editor", ->
it "restores the previous edit session associated with the editor", ->
previousEditSession = editor.activeEditSession
editor.edit(editSession)
{ firstRenderedScreenRow, lastRenderedScreenRow } = editor
expect(editor.lineElementForScreenRow(firstRenderedScreenRow).text()).toBe buffer.lineForRow(firstRenderedScreenRow)
expect(editor.lineElementForScreenRow(lastRenderedScreenRow).text()).toBe buffer.lineForRow(editor.lastRenderedScreenRow)
expect(editor.verticalScrollbar.prop('scrollHeight')).toBe previousScrollHeight
expect(editor.scrollTop()).toBe previousScrollTop
expect(editor.scrollView.scrollLeft()).toBe previousScrollLeft
expect(editor.getCursorView().position()).toEqual { top: 3 * editor.lineHeight, left: 5 * editor.charWidth }
editor.insertText("goodbye")
expect(editor.lineElementForScreenRow(3).text()).toMatch /^ vgoodbyear/
editor.edit(otherEditSession)
expect(editor.activeEditSession).not.toBe previousEditSession
it "triggers alert if edit session's buffer goes into conflict with changes on disk", ->
path = "/tmp/atom-changed-file.txt"
fs.write(path, "")
tempEditSession = project.buildEditSession(path)
editor.edit(tempEditSession)
tempEditSession.insertText("a buffer change")
editor.edit(previousEditSession)
expect(editor.activeEditSession).toBe previousEditSession
spyOn(atom, "confirm")
it "handles buffer manipulation correctly after switching to a new edit session", ->
editor.attachToDom()
editor.insertText("abc\n")
expect(editor.lineElementForScreenRow(0).text()).toBe 'abc'
contentsConflictedHandler = jasmine.createSpy("contentsConflictedHandler")
tempEditSession.on 'contents-conflicted', contentsConflictedHandler
fs.write(path, "a file change")
waitsFor ->
contentsConflictedHandler.callCount > 0
editor.edit(otherEditSession)
expect(editor.lineElementForScreenRow(0).html()).toBe '&nbsp;'
editor.insertText("def\n")
expect(editor.lineElementForScreenRow(0).text()).toBe 'def'
it "removes the opened session from the closed sessions array", ->
editor.edit(project.buildEditSessionForPath('sample.txt'))
expect(editor.closedEditSessions.length).toBe 0
editor.trigger "core:close"
expect(editor.closedEditSessions.length).toBe 1
editor.edit(project.buildEditSessionForPath('sample.txt'))
expect(editor.closedEditSessions.length).toBe 0
describe "switching edit sessions", ->
[session0, session1, session2] = []
beforeEach ->
session0 = editor.activeEditSession
editor.edit(project.buildEditSessionForPath('sample.txt'))
session1 = editor.activeEditSession
editor.edit(project.buildEditSessionForPath('two-hundred.txt'))
session2 = editor.activeEditSession
describe ".setActiveEditSessionIndex(index)", ->
it "restores the buffer, cursors, selections, and scroll position of the edit session associated with the index", ->
editor.attachToDom(heightInLines: 10)
editor.setSelectedBufferRange([[40, 0], [43, 1]])
expect(editor.getSelection().getScreenRange()).toEqual [[40, 0], [43, 1]]
previousScrollHeight = editor.verticalScrollbar.prop('scrollHeight')
editor.scrollTop(750)
expect(editor.scrollTop()).toBe 750
editor.setActiveEditSessionIndex(0)
expect(editor.getBuffer()).toBe session0.buffer
editor.setActiveEditSessionIndex(2)
expect(editor.getBuffer()).toBe session2.buffer
expect(editor.getCursorScreenPosition()).toEqual [43, 1]
expect(editor.verticalScrollbar.prop('scrollHeight')).toBe previousScrollHeight
expect(editor.scrollTop()).toBe 750
expect(editor.getSelection().getScreenRange()).toEqual [[40, 0], [43, 1]]
expect(editor.getSelectionView().find('.region')).toExist()
editor.setActiveEditSessionIndex(0)
editor.activeEditSession.selectToEndOfLine()
expect(editor.getSelectionView().find('.region')).toExist()
it "triggers alert if edit session's buffer goes into conflict with changes on disk", ->
path = "/tmp/atom-changed-file.txt"
fs.write(path, "")
editSession = project.buildEditSessionForPath(path)
editor.edit editSession
editSession.insertText("a buffer change")
spyOn(atom, "confirm")
contentsConflictedHandler = jasmine.createSpy("contentsConflictedHandler")
editSession.on 'contents-conflicted', contentsConflictedHandler
fs.write(path, "a file change")
waitsFor ->
contentsConflictedHandler.callCount > 0
runs ->
expect(atom.confirm).toHaveBeenCalled()
it "emits an editor:active-edit-session-changed event with the edit session and its index", ->
activeEditSessionChangeHandler = jasmine.createSpy('activeEditSessionChangeHandler')
editor.on 'editor:active-edit-session-changed', activeEditSessionChangeHandler
editor.setActiveEditSessionIndex(2)
expect(activeEditSessionChangeHandler).toHaveBeenCalled()
expect(activeEditSessionChangeHandler.argsForCall[0][1..2]).toEqual [editor.activeEditSession, 2]
activeEditSessionChangeHandler.reset()
editor.setActiveEditSessionIndex(0)
expect(activeEditSessionChangeHandler.argsForCall[0][1..2]).toEqual [editor.activeEditSession, 0]
activeEditSessionChangeHandler.reset()
describe ".loadNextEditSession()", ->
it "loads the next editor state and wraps to beginning when end is reached", ->
expect(editor.activeEditSession).toBe session2
editor.loadNextEditSession()
expect(editor.activeEditSession).toBe session0
editor.loadNextEditSession()
expect(editor.activeEditSession).toBe session1
editor.loadNextEditSession()
expect(editor.activeEditSession).toBe session2
describe ".loadPreviousEditSession()", ->
it "loads the next editor state and wraps to beginning when end is reached", ->
expect(editor.activeEditSession).toBe session2
editor.loadPreviousEditSession()
expect(editor.activeEditSession).toBe session1
editor.loadPreviousEditSession()
expect(editor.activeEditSession).toBe session0
editor.loadPreviousEditSession()
expect(editor.activeEditSession).toBe session2
describe ".save()", ->
describe "when the current buffer has a path", ->
tempFilePath = null
beforeEach ->
project.setPath('/tmp')
tempFilePath = '/tmp/atom-temp.txt'
fs.write(tempFilePath, "")
rootView.open(tempFilePath)
editor = rootView.getActiveEditor()
expect(editor.getPath()).toBe tempFilePath
afterEach ->
expect(fs.remove(tempFilePath))
it "saves the current buffer to disk", ->
editor.getBuffer().setText 'Edited!'
expect(fs.read(tempFilePath)).not.toBe "Edited!"
editor.save()
expect(fs.exists(tempFilePath)).toBeTruthy()
expect(fs.read(tempFilePath)).toBe 'Edited!'
describe "when the current buffer has no path", ->
selectedFilePath = null
beforeEach ->
editor.edit(project.buildEditSessionForPath())
expect(editor.getPath()).toBeUndefined()
editor.getBuffer().setText 'Save me to a new path'
spyOn(atom, 'showSaveDialog').andCallFake (callback) -> callback(selectedFilePath)
it "presents a 'save as' dialog", ->
editor.save()
expect(atom.showSaveDialog).toHaveBeenCalled()
describe "when a path is chosen", ->
it "saves the buffer to the chosen path", ->
selectedFilePath = '/tmp/temp.txt'
editor.save()
expect(fs.exists(selectedFilePath)).toBeTruthy()
expect(fs.read(selectedFilePath)).toBe 'Save me to a new path'
describe "when dialog is cancelled", ->
it "does not save the buffer", ->
selectedFilePath = null
editor.save()
expect(fs.exists(selectedFilePath)).toBeFalsy()
runs ->
expect(atom.confirm).toHaveBeenCalled()
describe ".scrollTop(n)", ->
beforeEach ->
@@ -449,29 +208,6 @@ describe "Editor", ->
editor.scrollTop(50)
expect(editor.scrollTop()).toBe 50
describe "split methods", ->
describe "when inside a pane", ->
fakePane = null
beforeEach ->
fakePane = { splitUp: jasmine.createSpy('splitUp').andReturn({}), remove: -> }
spyOn(editor, 'pane').andReturn(fakePane)
it "calls the corresponding split method on the containing pane with a new editor containing a copy of the active edit session", ->
editor.edit project.buildEditSessionForPath("sample.txt")
editor.splitUp()
expect(fakePane.splitUp).toHaveBeenCalled()
[newEditor] = fakePane.splitUp.argsForCall[0]
expect(newEditor.editSessions.length).toEqual 1
expect(newEditor.activeEditSession.buffer).toBe editor.activeEditSession.buffer
newEditor.remove()
describe "when not inside a pane", ->
it "does not split the editor, but doesn't throw an exception", ->
editor.splitUp().remove()
editor.splitDown().remove()
editor.splitLeft().remove()
editor.splitRight().remove()
describe "editor:attached event", ->
it 'only triggers an editor:attached event when it is first added to the DOM', ->
openHandler = jasmine.createSpy('openHandler')
@@ -486,7 +222,7 @@ describe "Editor", ->
editor.attachToDom()
expect(openHandler).not.toHaveBeenCalled()
describe "editor-path-changed event", ->
describe "editor:path-changed event", ->
path = null
beforeEach ->
path = "/tmp/something.txt"
@@ -504,7 +240,7 @@ describe "Editor", ->
it "emits event when editor receives a new buffer", ->
eventHandler = jasmine.createSpy('eventHandler')
editor.on 'editor:path-changed', eventHandler
editor.edit(project.buildEditSessionForPath(path))
editor.edit(project.buildEditSession(path))
expect(eventHandler).toHaveBeenCalled()
it "stops listening to events on previously set buffers", ->
@@ -512,7 +248,7 @@ describe "Editor", ->
oldBuffer = editor.getBuffer()
editor.on 'editor:path-changed', eventHandler
editor.edit(project.buildEditSessionForPath(path))
editor.edit(project.buildEditSession(path))
expect(eventHandler).toHaveBeenCalled()
eventHandler.reset()
@@ -539,30 +275,21 @@ describe "Editor", ->
afterEach ->
editor.clearFontFamily()
it "updates the font family on new and existing editors", ->
rootView.attachToDom()
rootView.height(200)
rootView.width(200)
config.set("editor.fontFamily", "Courier")
newEditor = editor.splitRight()
expect($("head style.editor-font-family").text()).toMatch "{font-family: Courier}"
expect(editor.css('font-family')).toBe 'Courier'
expect(newEditor.css('font-family')).toBe 'Courier'
it "updates the font family of editors and recalculates dimensions critical to cursor positioning", ->
rootView.attachToDom()
rootView.height(200)
rootView.width(200)
editor.attachToDom(12)
lineHeightBefore = editor.lineHeight
charWidthBefore = editor.charWidth
config.set("editor.fontFamily", "Courier")
editor.setCursorScreenPosition [5, 6]
config.set("editor.fontFamily", "PCMyungjo")
expect(editor.css('font-family')).toBe 'PCMyungjo'
expect($("head style.editor-font-family").text()).toMatch "{font-family: PCMyungjo}"
expect(editor.charWidth).not.toBe charWidthBefore
expect(editor.getCursorView().position()).toEqual { top: 5 * editor.lineHeight, left: 6 * editor.charWidth }
expect(editor.verticalScrollbarContent.height()).toBe buffer.getLineCount() * editor.lineHeight
newEditor = new Editor(editor.activeEditSession.copy())
newEditor.attachToDom()
expect(newEditor.css('font-family')).toBe 'PCMyungjo'
describe "font size", ->
beforeEach ->
@@ -574,24 +301,9 @@ describe "Editor", ->
expect($("head style.font-size").text()).toMatch "{font-size: #{config.get('editor.fontSize')}px}"
describe "when the font size changes", ->
it "updates the font family on new and existing editors", ->
rootView.attachToDom()
rootView.height(200)
rootView.width(200)
config.set("editor.fontSize", 20)
newEditor = editor.splitRight()
expect($("head style.font-size").text()).toMatch "{font-size: 20px}"
expect(editor.css('font-size')).toBe '20px'
expect(newEditor.css('font-size')).toBe '20px'
it "updates the font sizes of editors and recalculates dimensions critical to cursor positioning", ->
rootView.attachToDom()
rootView.height(200)
rootView.width(200)
config.set("editor.fontSize", 10)
editor.attachToDom()
lineHeightBefore = editor.lineHeight
charWidthBefore = editor.charWidth
editor.setCursorScreenPosition [5, 6]
@@ -604,10 +316,14 @@ describe "Editor", ->
expect(editor.renderedLines.outerHeight()).toBe buffer.getLineCount() * editor.lineHeight
expect(editor.verticalScrollbarContent.height()).toBe buffer.getLineCount() * editor.lineHeight
newEditor = new Editor(editor.activeEditSession.copy())
newEditor.attachToDom()
expect(editor.css('font-size')).toBe '30px'
it "updates the position and size of selection regions", ->
rootView.attachToDom()
config.set("editor.fontSize", 10)
editor.setSelectedBufferRange([[5, 2], [5, 7]])
editor.attachToDom()
config.set("editor.fontSize", 30)
selectionRegion = editor.find('.region')
@@ -617,10 +333,10 @@ describe "Editor", ->
expect(selectionRegion.width()).toBe 5 * editor.charWidth
it "updates the gutter width and font size", ->
rootView.attachToDom()
config.set("editor.fontSize", 16 * 4)
expect(editor.gutter.css('font-size')).toBe "#{16 * 4}px"
expect(editor.gutter.width()).toBe(64 + editor.gutter.calculateLineNumberPadding())
editor.attachToDom()
config.set("editor.fontSize", 20)
expect(editor.gutter.css('font-size')).toBe "20px"
expect(editor.gutter.width()).toBe(editor.charWidth * 2 + editor.gutter.calculateLineNumberPadding())
it "updates lines if there are unrendered lines", ->
editor.attachToDom(heightInLines: 5)
@@ -630,22 +346,25 @@ describe "Editor", ->
config.set("editor.fontSize", 10)
expect(editor.renderedLines.find(".line").length).toBeGreaterThan originalLineCount
describe "when the editor is detached", ->
describe "when the font size changes while editor is detached", ->
it "redraws the editor according to the new font size when it is reattached", ->
rootView.attachToDom()
rootView.height(200)
rootView.width(200)
editor.setCursorScreenPosition([4, 2])
editor.attachToDom()
initialLineHeight = editor.lineHeight
initialCharWidth = editor.charWidth
initialCursorPosition = editor.getCursorView().position()
initialScrollbarHeight = editor.verticalScrollbarContent.height()
editor.detach()
newEditor = editor.splitRight()
newEditorParent = newEditor.parent()
newEditor.detach()
config.set("editor.fontSize", 10)
newEditorParent.append(newEditor)
expect(editor.lineHeight).toBe initialLineHeight
expect(editor.charWidth).toBe initialCharWidth
expect(newEditor.lineHeight).toBe editor.lineHeight
expect(newEditor.charWidth).toBe editor.charWidth
expect(newEditor.getCursorView().position()).toEqual editor.getCursorView().position()
expect(newEditor.verticalScrollbarContent.height()).toBe editor.verticalScrollbarContent.height()
editor.attachToDom()
expect(editor.lineHeight).not.toBe initialLineHeight
expect(editor.charWidth).not.toBe initialCharWidth
expect(editor.getCursorView().position()).not.toEqual initialCursorPosition
expect(editor.verticalScrollbarContent.height()).not.toBe initialScrollbarHeight
describe "mouse events", ->
beforeEach ->
@@ -1373,7 +1092,7 @@ describe "Editor", ->
expect(editor.bufferPositionForScreenPosition(editor.getCursorScreenPosition())).toEqual [3, 60]
it "does not wrap the lines of any newly assigned buffers", ->
otherEditSession = project.buildEditSessionForPath()
otherEditSession = project.buildEditSession()
otherEditSession.buffer.setText([1..100].join(''))
editor.edit(otherEditSession)
expect(editor.renderedLines.find('.line').length).toBe(1)
@@ -1409,7 +1128,7 @@ describe "Editor", ->
expect(editor.getCursorScreenPosition()).toEqual [11, 0]
it "calls .setSoftWrapColumn() when the editor is attached because now its dimensions are available to calculate it", ->
otherEditor = new Editor(editSession: project.buildEditSessionForPath('sample.js'))
otherEditor = new Editor(editSession: project.buildEditSession('sample.js'))
spyOn(otherEditor, 'setSoftWrapColumn')
otherEditor.setSoftWrap(true)
@@ -1417,6 +1136,7 @@ describe "Editor", ->
otherEditor.simulateDomAttachment()
expect(otherEditor.setSoftWrapColumn).toHaveBeenCalled()
otherEditor.remove()
describe "when some lines at the end of the buffer are not visible on screen", ->
beforeEach ->
@@ -1705,7 +1425,7 @@ describe "Editor", ->
describe "when autoscrolling at the end of the document", ->
it "renders lines properly", ->
editor.edit(project.buildEditSessionForPath('two-hundred.txt'))
editor.edit(project.buildEditSession('two-hundred.txt'))
editor.attachToDom(heightInLines: 5.5)
expect(editor.renderedLines.find('.line').length).toBe 8
@@ -1716,7 +1436,7 @@ describe "Editor", ->
describe "when line has a character that could push it to be too tall (regression)", ->
it "does renders the line at a consistent height", ->
rootView.attachToDom()
editor.attachToDom()
buffer.insert([0, 0], "")
expect(editor.find('.line:eq(0)').outerHeight()).toBe editor.find('.line:eq(1)').outerHeight()
@@ -1747,21 +1467,11 @@ describe "Editor", ->
expect(editor.find('.line').html()).toBe '<span class="source js"><span class="storage modifier js">var</span></span><span class="invisible">¬</span>'
it "allows invisible glyphs to be customized via config.editor.invisibles", ->
rootView.height(200)
rootView.attachToDom()
rightEditor = rootView.getActiveEditor()
rightEditor.setText(" \t ")
leftEditor = rightEditor.splitLeft()
config.set "editor.showInvisibles", true
config.set "editor.invisibles",
eol: ";"
space: "_"
tab: "tab"
config.update()
expect(rightEditor.find(".line:first").text()).toBe "_tab _;"
expect(leftEditor.find(".line:first").text()).toBe "_tab _;"
editor.setText(" \t ")
editor.attachToDom()
config.set("editor.showInvisibles", true)
config.set("editor.invisibles", eol: ";", space: "_", tab: "tab")
expect(editor.find(".line:first").text()).toBe "_tab _;"
it "displays trailing carriage return using a visible non-empty value", ->
editor.setText "a line that ends with a carriage return\r\n"
@@ -1986,11 +1696,14 @@ describe "Editor", ->
describe "when the switching from an edit session for a long buffer to an edit session for a short buffer", ->
it "updates the line numbers to reflect the shorter buffer", ->
editor.edit(fixturesProject.buildEditSessionForPath(null))
emptyEditSession = project.buildEditSession(null)
editor.edit(emptyEditSession)
expect(editor.gutter.lineNumbers.find('.line-number').length).toBe 1
editor.setActiveEditSessionIndex(0)
editor.setActiveEditSessionIndex(1)
editor.edit(editSession)
expect(editor.gutter.lineNumbers.find('.line-number').length).toBeGreaterThan 1
editor.edit(emptyEditSession)
expect(editor.gutter.lineNumbers.find('.line-number').length).toBe 1
describe "when the editor is mini", ->
@@ -2120,7 +1833,7 @@ describe "Editor", ->
describe "folding", ->
beforeEach ->
editSession = project.buildEditSessionForPath('two-hundred.txt')
editSession = project.buildEditSession('two-hundred.txt')
buffer = editSession.buffer
editor.edit(editSession)
editor.attachToDom()
@@ -2209,14 +1922,6 @@ describe "Editor", ->
editor.scrollTop(0)
expect(editor.lineElementForScreenRow(2)).toMatchSelector('.fold.selected')
describe ".getOpenBufferPaths()", ->
it "returns the paths of all non-anonymous buffers with edit sessions on this editor", ->
editor.edit(project.buildEditSessionForPath('sample.txt'))
editor.edit(project.buildEditSessionForPath('two-hundred.txt'))
editor.edit(project.buildEditSessionForPath())
paths = editor.getOpenBufferPaths().map (path) -> project.relativize(path)
expect(paths).toEqual = ['sample.js', 'sample.txt', 'two-hundred.txt']
describe "paging up and down", ->
beforeEach ->
editor.attachToDom()
@@ -2258,37 +1963,20 @@ describe "Editor", ->
expect(editor.getCursor().getScreenPosition().row).toBe(0)
expect(editor.getFirstVisibleScreenRow()).toBe(0)
describe "when autosave is enabled", ->
it "autosaves the current buffer when the editor loses focus or switches edit sessions", ->
config.set "editor.autosave", true
rootView.attachToDom()
editor2 = editor.splitRight()
spyOn(editor2.activeEditSession, 'save')
editor.focus()
expect(editor2.activeEditSession.save).toHaveBeenCalled()
editSession = editor.activeEditSession
spyOn(editSession, 'save')
rootView.open('sample.txt')
expect(editSession.save).toHaveBeenCalled()
describe ".checkoutHead()", ->
[path, originalPathText] = []
beforeEach ->
path = require.resolve('fixtures/git/working-dir/file.txt')
path = project.resolve('git/working-dir/file.txt')
originalPathText = fs.read(path)
rootView.open(path)
editor = rootView.getActiveEditor()
editor.attachToDom()
editor.edit(project.buildEditSession(path))
afterEach ->
fs.write(path, originalPathText)
it "restores the contents of the editor to the HEAD revision", ->
editor.setText('')
editor.save()
editor.getBuffer().save()
fileChangeHandler = jasmine.createSpy('fileChange')
editor.getBuffer().file.on 'contents-changed', fileChangeHandler
@@ -2342,7 +2030,7 @@ describe "Editor", ->
describe "when clicking below the last line", ->
beforeEach ->
rootView.attachToDom()
editor.attachToDom()
it "move the cursor to the end of the file", ->
expect(editor.getCursorScreenPosition()).toEqual [0,0]
@@ -2359,68 +2047,19 @@ describe "Editor", ->
editor.underlayer.trigger event
expect(editor.getSelection().getScreenRange()).toEqual [[0,0], [12,2]]
describe ".destroyEditSessionIndex(index)", ->
it "prompts to save dirty buffers before closing", ->
editor.setText("I'm dirty")
rootView.open('sample.txt')
expect(editor.getEditSessions().length).toBe 2
spyOn(atom, "confirm")
editor.destroyEditSessionIndex(0)
expect(atom.confirm).toHaveBeenCalled()
expect(editor.getEditSessions().length).toBe 2
expect(editor.getEditSessions()[0].buffer.isModified()).toBeTruthy()
describe ".destroyInactiveEditSessions()", ->
it "destroys every edit session except the active one", ->
rootView.open('sample.txt')
cssSession = rootView.open('css.css')
rootView.open('coffee.coffee')
rootView.open('hello.rb')
expect(editor.getEditSessions().length).toBe 5
editor.setActiveEditSessionIndex(2)
editor.destroyInactiveEditSessions()
expect(editor.getActiveEditSessionIndex()).toBe 0
expect(editor.getEditSessions().length).toBe 1
expect(editor.getEditSessions()[0]).toBe cssSession
it "prompts to save dirty buffers before destroying", ->
editor.setText("I'm dirty")
dirtySession = editor.activeEditSession
rootView.open('sample.txt')
expect(editor.getEditSessions().length).toBe 2
spyOn(atom, "confirm")
editor.destroyInactiveEditSessions()
expect(atom.confirm).toHaveBeenCalled()
expect(editor.getEditSessions().length).toBe 2
expect(editor.getEditSessions()[0].buffer.isModified()).toBeTruthy()
describe ".destroyAllEditSessions()", ->
it "destroys every edit session", ->
rootView.open('sample.txt')
rootView.open('css.css')
rootView.open('coffee.coffee')
rootView.open('hello.rb')
expect(editor.getEditSessions().length).toBe 5
editor.setActiveEditSessionIndex(2)
editor.destroyAllEditSessions()
expect(editor.pane()).toBeUndefined()
expect(editor.getEditSessions().length).toBe 0
describe ".reloadGrammar()", ->
[path] = []
beforeEach ->
path = "/tmp/grammar-change.txt"
fs.write(path, "var i;")
rootView.attachToDom()
afterEach ->
project.removeGrammarOverrideForPath(path)
fs.remove(path) if fs.exists(path)
it "updates all the rendered lines when the grammar changes", ->
rootView.open(path)
editor = rootView.getActiveEditor()
editor.edit(project.buildEditSession(path))
expect(editor.getGrammar().name).toBe 'Plain Text'
jsGrammar = syntax.grammarForFilePath('/tmp/js.js')
expect(jsGrammar.name).toBe 'JavaScript'
@@ -2434,12 +2073,6 @@ describe "Editor", ->
expect(line0.tokens.length).toBe 3
expect(line0.tokens[0]).toEqual(value: 'var', scopes: ['source.js', 'storage.modifier.js'])
line0 = editor.renderedLines.find('.line:first')
span0 = line0.children('span:eq(0)')
expect(span0).toMatchSelector '.source.js'
expect(span0.children('span:eq(0)')).toMatchSelector '.storage.modifier.js'
expect(span0.children('span:eq(0)').text()).toBe 'var'
it "doesn't update the rendered lines when the grammar doesn't change", ->
expect(editor.getGrammar().name).toBe 'JavaScript'
spyOn(editor, 'updateDisplay').andCallThrough()
@@ -2449,8 +2082,8 @@ describe "Editor", ->
expect(editor.getGrammar().name).toBe 'JavaScript'
it "emits an editor:grammar-changed event when updated", ->
rootView.open(path)
editor = rootView.getActiveEditor()
editor.edit(project.buildEditSession(path))
eventHandler = jasmine.createSpy('eventHandler')
editor.on('editor:grammar-changed', eventHandler)
editor.reloadGrammar()
@@ -2768,104 +2401,6 @@ describe "Editor", ->
expect(buffer.lineForRow(15)).toBeUndefined()
expect(editor.getCursorBufferPosition()).toEqual [13, 0]
describe ".moveEditSessionToIndex(fromIndex, toIndex)", ->
describe "when the edit session moves to a later index", ->
it "updates the edit session order", ->
jsPath = editor.getPath()
rootView.open("sample.txt")
txtPath = editor.getPath()
expect(editor.editSessions[0].getPath()).toBe jsPath
expect(editor.editSessions[1].getPath()).toBe txtPath
editor.moveEditSessionToIndex(0, 1)
expect(editor.editSessions[0].getPath()).toBe txtPath
expect(editor.editSessions[1].getPath()).toBe jsPath
it "fires an editor:edit-session-order-changed event", ->
eventHandler = jasmine.createSpy("eventHandler")
rootView.open("sample.txt")
editor.on "editor:edit-session-order-changed", eventHandler
editor.moveEditSessionToIndex(0, 1)
expect(eventHandler).toHaveBeenCalled()
it "sets the moved session as the editor's active session", ->
jsPath = editor.getPath()
rootView.open("sample.txt")
txtPath = editor.getPath()
expect(editor.activeEditSession.getPath()).toBe txtPath
editor.moveEditSessionToIndex(0, 1)
expect(editor.activeEditSession.getPath()).toBe jsPath
describe "when the edit session moves to an earlier index", ->
it "updates the edit session order", ->
jsPath = editor.getPath()
rootView.open("sample.txt")
txtPath = editor.getPath()
expect(editor.editSessions[0].getPath()).toBe jsPath
expect(editor.editSessions[1].getPath()).toBe txtPath
editor.moveEditSessionToIndex(1, 0)
expect(editor.editSessions[0].getPath()).toBe txtPath
expect(editor.editSessions[1].getPath()).toBe jsPath
it "fires an editor:edit-session-order-changed event", ->
eventHandler = jasmine.createSpy("eventHandler")
rootView.open("sample.txt")
editor.on "editor:edit-session-order-changed", eventHandler
editor.moveEditSessionToIndex(1, 0)
expect(eventHandler).toHaveBeenCalled()
it "sets the moved session as the editor's active session", ->
jsPath = editor.getPath()
rootView.open("sample.txt")
txtPath = editor.getPath()
expect(editor.activeEditSession.getPath()).toBe txtPath
editor.moveEditSessionToIndex(1, 0)
expect(editor.activeEditSession.getPath()).toBe txtPath
describe ".moveEditSessionToEditor(fromIndex, toEditor, toIndex)", ->
it "closes the edit session in the source editor", ->
jsPath = editor.getPath()
rootView.open("sample.txt")
txtPath = editor.getPath()
rightEditor = editor.splitRight()
expect(editor.editSessions[0].getPath()).toBe jsPath
expect(editor.editSessions[1].getPath()).toBe txtPath
editor.moveEditSessionToEditor(0, rightEditor, 1)
expect(editor.editSessions[0].getPath()).toBe txtPath
expect(editor.editSessions[1]).toBeUndefined()
it "opens the edit session in the destination editor at the target index", ->
jsPath = editor.getPath()
rootView.open("sample.txt")
txtPath = editor.getPath()
rightEditor = editor.splitRight()
expect(rightEditor.editSessions[0].getPath()).toBe txtPath
expect(rightEditor.editSessions[1]).toBeUndefined()
editor.moveEditSessionToEditor(0, rightEditor, 0)
expect(rightEditor.editSessions[0].getPath()).toBe jsPath
expect(rightEditor.editSessions[1].getPath()).toBe txtPath
describe "when editor:undo-close-session is triggered", ->
describe "when an edit session is opened back up after it is closed", ->
it "is removed from the undo stack and not reopened when the event is triggered", ->
rootView.open('sample.txt')
expect(editor.getPath()).toBe fixturesProject.resolve('sample.txt')
editor.trigger "core:close"
expect(editor.closedEditSessions.length).toBe 1
rootView.open('sample.txt')
expect(editor.closedEditSessions.length).toBe 0
editor.trigger 'editor:undo-close-session'
expect(editor.getPath()).toBe fixturesProject.resolve('sample.txt')
it "opens the closed session back up at the previous index", ->
rootView.open('sample.txt')
editor.loadPreviousEditSession()
expect(editor.getPath()).toBe fixturesProject.resolve('sample.js')
editor.trigger "core:close"
expect(editor.getPath()).toBe fixturesProject.resolve('sample.txt')
editor.trigger 'editor:undo-close-session'
expect(editor.getPath()).toBe fixturesProject.resolve('sample.js')
expect(editor.getActiveEditSessionIndex()).toBe 0
describe "editor:save-debug-snapshot", ->
it "saves the state of the rendered lines, the display buffer, and the buffer to a file of the user's choosing", ->
saveDialogCallback = null

View File

@@ -1,5 +1,6 @@
Git = require 'git'
fs = require 'fs'
Task = require 'task'
describe "Git", ->
repo = null
@@ -21,11 +22,11 @@ describe "Git", ->
describe ".getPath()", ->
it "returns the repository path for a .git directory path", ->
repo = new Git(require.resolve('fixtures/git/master.git/HEAD'))
expect(repo.getPath()).toBe require.resolve('fixtures/git/master.git') + '/'
expect(repo.getPath()).toBe require.resolve('fixtures/git/master.git')
it "returns the repository path for a repository path", ->
repo = new Git(require.resolve('fixtures/git/master.git'))
expect(repo.getPath()).toBe require.resolve('fixtures/git/master.git') + '/'
expect(repo.getPath()).toBe require.resolve('fixtures/git/master.git')
describe ".getHead()", ->
it "returns a branch name for a non-empty repository", ->
@@ -126,6 +127,18 @@ describe "Git", ->
expect(fs.read(path2)).toBe('path 2 is edited')
expect(repo.isPathModified(path2)).toBeTruthy()
it "fires a status-changed event if the checkout completes successfully", ->
fs.write(path1, '')
repo.getPathStatus(path1)
statusHandler = jasmine.createSpy('statusHandler')
repo.on 'status-changed', statusHandler
repo.checkoutHead(path1)
expect(statusHandler.callCount).toBe 1
expect(statusHandler.argsForCall[0][0..1]).toEqual [path1, 0]
repo.checkoutHead(path1)
expect(statusHandler.callCount).toBe 1
describe ".destroy()", ->
it "throws an exception when any method is called after it is called", ->
repo = new Git(require.resolve('fixtures/git/master.git/HEAD'))
@@ -147,3 +160,75 @@ describe "Git", ->
expect(repo.getDiffStats(path)).toEqual {added: 0, deleted: 0}
fs.write(path, "#{originalPathText} edited line")
expect(repo.getDiffStats(path)).toEqual {added: 1, deleted: 1}
describe ".getPathStatus(path)", ->
[path, originalPathText] = []
beforeEach ->
repo = new Git(require.resolve('fixtures/git/working-dir'))
path = require.resolve('fixtures/git/working-dir/file.txt')
originalPathText = fs.read(path)
afterEach ->
fs.write(path, originalPathText)
it "trigger a status-changed event when the new status differs from the last cached one", ->
statusHandler = jasmine.createSpy("statusHandler")
repo.on 'status-changed', statusHandler
fs.write(path, '')
status = repo.getPathStatus(path)
expect(statusHandler.callCount).toBe 1
expect(statusHandler.argsForCall[0][0..1]).toEqual [path, status]
fs.write(path, 'abc')
status = repo.getPathStatus(path)
expect(statusHandler.callCount).toBe 1
describe ".refreshStatus()", ->
[newPath, modifiedPath, cleanPath, originalModifiedPathText] = []
beforeEach ->
repo = new Git(require.resolve('fixtures/git/working-dir'))
modifiedPath = project.resolve('git/working-dir/file.txt')
originalModifiedPathText = fs.read(modifiedPath)
newPath = project.resolve('git/working-dir/untracked.txt')
cleanPath = project.resolve('git/working-dir/other.txt')
fs.write(newPath, '')
afterEach ->
fs.write(modifiedPath, originalModifiedPathText)
fs.remove(newPath) if fs.exists(newPath)
it "returns status information for all new and modified files", ->
fs.write(modifiedPath, 'making this path modified')
statusHandler = jasmine.createSpy('statusHandler')
repo.on 'statuses-changed', statusHandler
repo.refreshStatus()
waitsFor ->
statusHandler.callCount > 0
runs ->
statuses = repo.statuses
expect(statuses[cleanPath]).toBeUndefined()
expect(repo.isStatusNew(statuses[newPath])).toBeTruthy()
expect(repo.isStatusModified(statuses[modifiedPath])).toBeTruthy()
it "only starts a single web worker at a time and schedules a restart if one is already running", =>
fs.write(modifiedPath, 'making this path modified')
statusHandler = jasmine.createSpy('statusHandler')
repo.on 'statuses-changed', statusHandler
spyOn(Task.prototype, "start").andCallThrough()
repo.refreshStatus()
expect(Task.prototype.start.callCount).toBe 1
repo.refreshStatus()
expect(Task.prototype.start.callCount).toBe 1
repo.refreshStatus()
expect(Task.prototype.start.callCount).toBe 1
waitsFor ->
statusHandler.callCount > 0
runs ->
expect(Task.prototype.start.callCount).toBe 2

View File

@@ -7,10 +7,8 @@ describe "GrammarView", ->
beforeEach ->
window.rootView = new RootView
project.removeGrammarOverrideForPath('sample.js')
rootView.open('sample.js')
editor = rootView.getActiveEditor()
rootView.attachToDom()
editor = rootView.getActiveView()
textGrammar = _.find syntax.grammars, (grammar) -> grammar.name is 'Plain Text'
expect(textGrammar).toBeTruthy()
jsGrammar = _.find syntax.grammars, (grammar) -> grammar.name is 'JavaScript'

View File

@@ -10,18 +10,18 @@ describe "LanguageMode", ->
describe "common behavior", ->
beforeEach ->
editSession = fixturesProject.buildEditSessionForPath('sample.js', autoIndent: false)
editSession = project.buildEditSession('sample.js', autoIndent: false)
{ buffer, languageMode } = editSession
describe "language detection", ->
it "uses the file name as the file type if it has no extension", ->
jsEditSession = fixturesProject.buildEditSessionForPath('js', autoIndent: false)
jsEditSession = project.buildEditSession('js', autoIndent: false)
expect(jsEditSession.languageMode.grammar.name).toBe "JavaScript"
jsEditSession.destroy()
describe "javascript", ->
beforeEach ->
editSession = fixturesProject.buildEditSessionForPath('sample.js', autoIndent: false)
editSession = project.buildEditSession('sample.js', autoIndent: false)
{ buffer, languageMode } = editSession
describe ".toggleLineCommentsForBufferRows(start, end)", ->
@@ -63,7 +63,7 @@ describe "LanguageMode", ->
describe "coffeescript", ->
beforeEach ->
editSession = fixturesProject.buildEditSessionForPath('coffee.coffee', autoIndent: false)
editSession = project.buildEditSession('coffee.coffee', autoIndent: false)
{ buffer, languageMode } = editSession
describe ".toggleLineCommentsForBufferRows(start, end)", ->
@@ -98,7 +98,7 @@ describe "LanguageMode", ->
describe "css", ->
beforeEach ->
editSession = fixturesProject.buildEditSessionForPath('css.css', autoIndent: false)
editSession = project.buildEditSession('css.css', autoIndent: false)
{ buffer, languageMode } = editSession
describe ".toggleLineCommentsForBufferRows(start, end)", ->

View File

@@ -0,0 +1,192 @@
PaneContainer = require 'pane-container'
Pane = require 'pane'
{View, $$} = require 'space-pen'
_ = require 'underscore'
$ = require 'jquery'
describe "PaneContainer", ->
[TestView, container, pane1, pane2, pane3] = []
beforeEach ->
class TestView extends View
registerDeserializer(this)
@deserialize: ({name}) -> new TestView(name)
@content: -> @div tabindex: -1
initialize: (@name) -> @text(@name)
serialize: -> { deserializer: 'TestView', @name }
getUri: -> "/tmp/#{@name}"
save: -> @saved = true
isEqual: (other) -> @name is other.name
container = new PaneContainer
pane1 = new Pane(new TestView('1'))
container.append(pane1)
pane2 = pane1.splitRight(new TestView('2'))
pane3 = pane2.splitDown(new TestView('3'))
afterEach ->
unregisterDeserializer(TestView)
describe ".focusNextPane()", ->
it "focuses the pane following the focused pane or the first pane if no pane has focus", ->
container.attachToDom()
container.focusNextPane()
expect(pane1.activeItem).toMatchSelector ':focus'
container.focusNextPane()
expect(pane2.activeItem).toMatchSelector ':focus'
container.focusNextPane()
expect(pane3.activeItem).toMatchSelector ':focus'
container.focusNextPane()
expect(pane1.activeItem).toMatchSelector ':focus'
describe ".getActivePane()", ->
it "returns the most-recently focused pane", ->
focusStealer = $$ -> @div tabindex: -1, "focus stealer"
focusStealer.attachToDom()
container.attachToDom()
pane2.focus()
expect(container.getFocusedPane()).toBe pane2
expect(container.getActivePane()).toBe pane2
focusStealer.focus()
expect(container.getFocusedPane()).toBeUndefined()
expect(container.getActivePane()).toBe pane2
pane3.focus()
expect(container.getFocusedPane()).toBe pane3
expect(container.getActivePane()).toBe pane3
# returns the first pane if none have been set to active
container.find('.pane.active').removeClass('active')
expect(container.getActivePane()).toBe pane1
describe ".eachPane(callback)", ->
it "runs the callback with all current and future panes until the subscription is cancelled", ->
panes = []
subscription = container.eachPane (pane) -> panes.push(pane)
expect(panes).toEqual [pane1, pane2, pane3]
panes = []
pane4 = pane3.splitRight()
expect(panes).toEqual [pane4]
panes = []
subscription.cancel()
pane4.splitDown()
expect(panes).toEqual []
describe ".reopenItem()", ->
describe "when there is an active pane", ->
it "reconstructs and shows the last-closed pane item", ->
expect(container.getActivePane()).toBe pane3
item3 = pane3.activeItem
item4 = new TestView('4')
pane3.showItem(item4)
pane3.destroyItem(item3)
pane3.destroyItem(item4)
expect(container.getActivePane()).toBe pane1
expect(container.reopenItem()).toBeTruthy()
expect(pane1.activeItem).toEqual item4
expect(container.reopenItem()).toBeTruthy()
expect(pane1.activeItem).toEqual item3
expect(container.reopenItem()).toBeFalsy()
expect(pane1.activeItem).toEqual item3
describe "when there is no active pane", ->
it "attaches a new pane with the reconstructed last pane item", ->
pane1.remove()
pane2.remove()
item3 = pane3.activeItem
pane3.destroyItem(item3)
expect(container.getActivePane()).toBeUndefined()
container.reopenItem()
expect(container.getActivePane().activeItem).toEqual item3
it "does not reopen an item that is already open", ->
item3 = pane3.activeItem
item4 = new TestView('4')
pane3.showItem(item4)
pane3.destroyItem(item3)
pane3.destroyItem(item4)
expect(container.getActivePane()).toBe pane1
pane1.showItem(new TestView('4'))
expect(container.reopenItem()).toBeTruthy()
expect(_.pluck(pane1.getItems(), 'name')).toEqual ['1', '4', '3']
expect(pane1.activeItem).toEqual item3
expect(container.reopenItem()).toBeFalsy()
expect(pane1.activeItem).toEqual item3
describe ".saveAll()", ->
it "saves all open pane items", ->
pane1.showItem(new TestView('4'))
container.saveAll()
for pane in container.getPanes()
for item in pane.getItems()
expect(item.saved).toBeTruthy()
describe ".confirmClose()", ->
it "resolves the returned promise after modified files are saved", ->
pane1.itemAtIndex(0).isModified = -> true
pane2.itemAtIndex(0).isModified = -> true
spyOn(atom, "confirm").andCallFake (a, b, c, d, e, f, g, noSaveFn) -> noSaveFn()
promiseHandler = jasmine.createSpy("promiseHandler")
failedPromiseHandler = jasmine.createSpy("failedPromiseHandler")
promise = container.confirmClose()
promise.done promiseHandler
promise.fail failedPromiseHandler
waitsFor ->
promiseHandler.wasCalled
runs ->
expect(failedPromiseHandler).not.toHaveBeenCalled()
expect(atom.confirm).toHaveBeenCalled()
it "rejects the returned promise if the user cancels saving", ->
pane1.itemAtIndex(0).isModified = -> true
pane2.itemAtIndex(0).isModified = -> true
spyOn(atom, "confirm").andCallFake (a, b, c, d, e, cancelFn, f, g) -> cancelFn()
promiseHandler = jasmine.createSpy("promiseHandler")
failedPromiseHandler = jasmine.createSpy("failedPromiseHandler")
promise = container.confirmClose()
promise.done promiseHandler
promise.fail failedPromiseHandler
waitsFor ->
failedPromiseHandler.wasCalled
runs ->
expect(promiseHandler).not.toHaveBeenCalled()
expect(atom.confirm).toHaveBeenCalled()
describe "serialization", ->
it "can be serialized and deserialized, and correctly adjusts dimensions of deserialized panes after attach", ->
newContainer = deserialize(container.serialize())
expect(newContainer.find('.row > :contains(1)')).toExist()
expect(newContainer.find('.row > .column > :contains(2)')).toExist()
expect(newContainer.find('.row > .column > :contains(3)')).toExist()
newContainer.height(200).width(300).attachToDom()
expect(newContainer.find('.row > :contains(1)').width()).toBe 150
expect(newContainer.find('.row > .column > :contains(2)').height()).toBe 100
it "removes empty panes on deserialization", ->
# only deserialize pane 1's view successfully
TestView.deserialize = ({name}) -> new TestView(name) if name is '1'
newContainer = deserialize(container.serialize())
expect(newContainer.find('.row, .column')).not.toExist()
expect(newContainer.find('> :contains(1)')).toExist()

701
spec/app/pane-spec.coffee Normal file
View File

@@ -0,0 +1,701 @@
PaneContainer = require 'pane-container'
Pane = require 'pane'
{$$} = require 'space-pen'
$ = require 'jquery'
describe "Pane", ->
[container, view1, view2, editSession1, editSession2, pane] = []
beforeEach ->
container = new PaneContainer
view1 = $$ -> @div id: 'view-1', tabindex: -1, 'View 1'
view2 = $$ -> @div id: 'view-2', tabindex: -1, 'View 2'
editSession1 = project.buildEditSession('sample.js')
editSession2 = project.buildEditSession('sample.txt')
pane = new Pane(view1, editSession1, view2, editSession2)
container.append(pane)
describe ".initialize(items...)", ->
it "displays the first item in the pane", ->
expect(pane.itemViews.find('#view-1')).toExist()
describe ".showItem(item)", ->
it "hides all item views except the one being shown and sets the activeItem", ->
expect(pane.activeItem).toBe view1
pane.showItem(view2)
expect(view1.css('display')).toBe 'none'
expect(view2.css('display')).toBe ''
expect(pane.activeItem).toBe view2
it "triggers 'pane:active-item-changed' if the item isn't already the activeItem", ->
pane.makeActive()
itemChangedHandler = jasmine.createSpy("itemChangedHandler")
container.on 'pane:active-item-changed', itemChangedHandler
expect(pane.activeItem).toBe view1
pane.showItem(view2)
pane.showItem(view2)
expect(itemChangedHandler.callCount).toBe 1
expect(itemChangedHandler.argsForCall[0][1]).toBe view2
itemChangedHandler.reset()
pane.showItem(editSession1)
expect(itemChangedHandler).toHaveBeenCalled()
expect(itemChangedHandler.argsForCall[0][1]).toBe editSession1
itemChangedHandler.reset()
describe "if the pane's active view is focused before calling showItem", ->
it "focuses the new active view", ->
container.attachToDom()
pane.focus()
expect(pane.activeView).not.toBe view2
expect(pane.activeView).toMatchSelector ':focus'
pane.showItem(view2)
expect(view2).toMatchSelector ':focus'
describe "when the given item isn't yet in the items list on the pane", ->
view3 = null
beforeEach ->
view3 = $$ -> @div id: 'view-3', "View 3"
pane.showItem(editSession1)
expect(pane.getActiveItemIndex()).toBe 1
it "adds it to the items list after the active item", ->
pane.showItem(view3)
expect(pane.getItems()).toEqual [view1, editSession1, view3, view2, editSession2]
expect(pane.activeItem).toBe view3
expect(pane.getActiveItemIndex()).toBe 2
it "triggers the 'item-added' event with the item and its index before the 'active-item-changed' event", ->
events = []
container.on 'pane:item-added', (e, item, index) -> events.push(['pane:item-added', item, index])
container.on 'pane:active-item-changed', (e, item) -> events.push(['pane:active-item-changed', item])
pane.showItem(view3)
expect(events).toEqual [['pane:item-added', view3, 2], ['pane:active-item-changed', view3]]
describe "when showing a model item", ->
describe "when no view has yet been appended for that item", ->
it "appends and shows a view to display the item based on its `.getViewClass` method", ->
pane.showItem(editSession1)
editor = pane.activeView
expect(editor.css('display')).toBe ''
expect(editor.activeEditSession).toBe editSession1
describe "when a valid view has already been appended for another item", ->
it "recycles the existing view by assigning the selected item to it", ->
pane.showItem(editSession1)
pane.showItem(editSession2)
expect(pane.itemViews.find('.editor').length).toBe 1
editor = pane.activeView
expect(editor.css('display')).toBe ''
expect(editor.activeEditSession).toBe editSession2
describe "when showing a view item", ->
it "appends it to the itemViews div if it hasn't already been appended and shows it", ->
expect(pane.itemViews.find('#view-2')).not.toExist()
pane.showItem(view2)
expect(pane.itemViews.find('#view-2')).toExist()
expect(pane.activeView).toBe view2
describe ".destroyItem(item)", ->
describe "if the item is not modified", ->
it "removes the item and tries to call destroy on it", ->
pane.destroyItem(editSession2)
expect(pane.getItems().indexOf(editSession2)).toBe -1
expect(editSession2.destroyed).toBeTruthy()
describe "if the item is modified", ->
beforeEach ->
spyOn(atom, 'confirm')
spyOn(atom, 'showSaveDialog')
spyOn(editSession2, 'save')
spyOn(editSession2, 'saveAs')
atom.confirm.selectOption = (buttonText) ->
for arg, i in @argsForCall[0] when arg is buttonText
@argsForCall[0][i + 1]?()
editSession2.insertText('a')
expect(editSession2.isModified()).toBeTruthy()
pane.destroyItem(editSession2)
it "presents a dialog with the option to save the item first", ->
expect(atom.confirm).toHaveBeenCalled()
expect(pane.getItems().indexOf(editSession2)).not.toBe -1
expect(editSession2.destroyed).toBeFalsy()
describe "if the [Save] option is selected", ->
describe "when the item has a uri", ->
it "saves the item before removing and destroying it", ->
atom.confirm.selectOption('Save')
expect(editSession2.save).toHaveBeenCalled()
expect(pane.getItems().indexOf(editSession2)).toBe -1
expect(editSession2.destroyed).toBeTruthy()
describe "when the item has no uri", ->
it "presents a save-as dialog, then saves the item with the given uri before removing and destroying it", ->
editSession2.buffer.setPath(undefined)
atom.confirm.selectOption('Save')
expect(atom.showSaveDialog).toHaveBeenCalled()
atom.showSaveDialog.argsForCall[0][0]("/selected/path")
expect(editSession2.saveAs).toHaveBeenCalledWith("/selected/path")
expect(pane.getItems().indexOf(editSession2)).toBe -1
expect(editSession2.destroyed).toBeTruthy()
describe "if the [Don't Save] option is selected", ->
it "removes and destroys the item without saving it", ->
atom.confirm.selectOption("Don't Save")
expect(editSession2.save).not.toHaveBeenCalled()
expect(pane.getItems().indexOf(editSession2)).toBe -1
expect(editSession2.destroyed).toBeTruthy()
describe "if the [Cancel] option is selected", ->
it "does not save, remove, or destroy the item", ->
atom.confirm.selectOption("Cancel")
expect(editSession2.save).not.toHaveBeenCalled()
expect(pane.getItems().indexOf(editSession2)).not.toBe -1
expect(editSession2.destroyed).toBeFalsy()
describe ".removeItem(item)", ->
it "removes the item from the items list and shows the next item if it was showing", ->
pane.removeItem(view1)
expect(pane.getItems()).toEqual [editSession1, view2, editSession2]
expect(pane.activeItem).toBe editSession1
pane.showItem(editSession2)
pane.removeItem(editSession2)
expect(pane.getItems()).toEqual [editSession1, view2]
expect(pane.activeItem).toBe editSession1
it "triggers 'pane:item-removed' with the item and its former index", ->
itemRemovedHandler = jasmine.createSpy("itemRemovedHandler")
pane.on 'pane:item-removed', itemRemovedHandler
pane.removeItem(editSession1)
expect(itemRemovedHandler).toHaveBeenCalled()
expect(itemRemovedHandler.argsForCall[0][1..2]).toEqual [editSession1, 1]
describe "when removing the last item", ->
it "removes the pane", ->
pane.removeItem(item) for item in pane.getItems()
expect(pane.hasParent()).toBeFalsy()
describe "when the pane is focused", ->
it "shifts focus to the next pane", ->
container.attachToDom()
pane2 = pane.splitRight($$ -> @div class: 'view-3', tabindex: -1, 'View 3')
pane.focus()
expect(pane).toMatchSelector(':has(:focus)')
pane.removeItem(item) for item in pane.getItems()
expect(pane2).toMatchSelector ':has(:focus)'
describe "when the item is a view", ->
it "removes the item from the 'item-views' div", ->
expect(view1.parent()).toMatchSelector pane.itemViews
pane.removeItem(view1)
expect(view1.parent()).not.toMatchSelector pane.itemViews
describe "when the item is a model", ->
it "removes the associated view only when all items that require it have been removed", ->
pane.showItem(editSession2)
pane.removeItem(editSession2)
expect(pane.itemViews.find('.editor')).toExist()
pane.removeItem(editSession1)
expect(pane.itemViews.find('.editor')).not.toExist()
describe ".moveItem(item, index)", ->
it "moves the item to the given index and emits a 'pane:item-moved' event with the item and the new index", ->
itemMovedHandler = jasmine.createSpy("itemMovedHandler")
pane.on 'pane:item-moved', itemMovedHandler
pane.moveItem(view1, 2)
expect(pane.getItems()).toEqual [editSession1, view2, view1, editSession2]
expect(itemMovedHandler).toHaveBeenCalled()
expect(itemMovedHandler.argsForCall[0][1..2]).toEqual [view1, 2]
itemMovedHandler.reset()
pane.moveItem(editSession1, 3)
expect(pane.getItems()).toEqual [view2, view1, editSession2, editSession1]
expect(itemMovedHandler).toHaveBeenCalled()
expect(itemMovedHandler.argsForCall[0][1..2]).toEqual [editSession1, 3]
itemMovedHandler.reset()
pane.moveItem(editSession1, 1)
expect(pane.getItems()).toEqual [view2, editSession1, view1, editSession2]
expect(itemMovedHandler).toHaveBeenCalled()
expect(itemMovedHandler.argsForCall[0][1..2]).toEqual [editSession1, 1]
itemMovedHandler.reset()
describe ".moveItemToPane(item, pane, index)", ->
[pane2, view3] = []
beforeEach ->
view3 = $$ -> @div id: 'view-3', "View 3"
pane2 = pane.splitRight(view3)
it "moves the item to the given pane at the given index", ->
pane.moveItemToPane(view1, pane2, 1)
expect(pane.getItems()).toEqual [editSession1, view2, editSession2]
expect(pane2.getItems()).toEqual [view3, view1]
describe "when it is the last item on the source pane", ->
it "removes the source pane, but does not destroy the item", ->
pane.removeItem(view1)
pane.removeItem(view2)
pane.removeItem(editSession2)
expect(pane.getItems()).toEqual [editSession1]
pane.moveItemToPane(editSession1, pane2, 1)
expect(pane.hasParent()).toBeFalsy()
expect(pane2.getItems()).toEqual [view3, editSession1]
expect(editSession1.destroyed).toBeFalsy()
describe "core:close", ->
it "destroys the active item and does not bubble the event", ->
containerCloseHandler = jasmine.createSpy("containerCloseHandler")
container.on 'core:close', containerCloseHandler
pane.showItem(editSession1)
initialItemCount = pane.getItems().length
pane.trigger 'core:close'
expect(pane.getItems().length).toBe initialItemCount - 1
expect(editSession1.destroyed).toBeTruthy()
expect(containerCloseHandler).not.toHaveBeenCalled()
describe "pane:close", ->
it "destroys all items and removes the pane", ->
pane.showItem(editSession1)
pane.trigger 'pane:close'
expect(pane.hasParent()).toBeFalsy()
expect(editSession2.destroyed).toBeTruthy()
expect(editSession1.destroyed).toBeTruthy()
describe "pane:close-other-items", ->
it "destroys all items except the current", ->
pane.showItem(editSession1)
pane.trigger 'pane:close-other-items'
expect(editSession2.destroyed).toBeTruthy()
expect(pane.getItems()).toEqual [editSession1]
describe "core:save", ->
describe "when the current item has a uri", ->
describe "when the current item has a save method", ->
it "saves the current item", ->
spyOn(editSession2, 'save')
pane.showItem(editSession2)
pane.trigger 'core:save'
expect(editSession2.save).toHaveBeenCalled()
describe "when the current item has no save method", ->
it "does nothing", ->
expect(pane.activeItem.save).toBeUndefined()
pane.trigger 'core:save'
describe "when the current item has no uri", ->
beforeEach ->
spyOn(atom, 'showSaveDialog')
describe "when the current item has a saveAs method", ->
it "opens a save dialog and saves the current item as the selected path", ->
spyOn(editSession2, 'saveAs')
editSession2.buffer.setPath(undefined)
pane.showItem(editSession2)
pane.trigger 'core:save'
expect(atom.showSaveDialog).toHaveBeenCalled()
atom.showSaveDialog.argsForCall[0][0]('/selected/path')
expect(editSession2.saveAs).toHaveBeenCalledWith('/selected/path')
describe "when the current item has no saveAs method", ->
it "does nothing", ->
expect(pane.activeItem.saveAs).toBeUndefined()
pane.trigger 'core:save'
expect(atom.showSaveDialog).not.toHaveBeenCalled()
describe "core:save-as", ->
beforeEach ->
spyOn(atom, 'showSaveDialog')
describe "when the current item has a saveAs method", ->
it "opens the save dialog and calls saveAs on the item with the selected path", ->
spyOn(editSession2, 'saveAs')
pane.showItem(editSession2)
pane.trigger 'core:save-as'
expect(atom.showSaveDialog).toHaveBeenCalled()
atom.showSaveDialog.argsForCall[0][0]('/selected/path')
expect(editSession2.saveAs).toHaveBeenCalledWith('/selected/path')
describe "when the current item does not have a saveAs method", ->
it "does nothing", ->
expect(pane.activeItem.saveAs).toBeUndefined()
pane.trigger 'core:save-as'
expect(atom.showSaveDialog).not.toHaveBeenCalled()
describe "pane:show-next-item and pane:show-previous-item", ->
it "advances forward/backward through the pane's items, looping around at either end", ->
expect(pane.activeItem).toBe view1
pane.trigger 'pane:show-previous-item'
expect(pane.activeItem).toBe editSession2
pane.trigger 'pane:show-previous-item'
expect(pane.activeItem).toBe view2
pane.trigger 'pane:show-next-item'
expect(pane.activeItem).toBe editSession2
pane.trigger 'pane:show-next-item'
expect(pane.activeItem).toBe view1
describe "pane:show-item-N events", ->
it "shows the (n-1)th item if it exists", ->
pane.trigger 'pane:show-item-2'
expect(pane.activeItem).toBe pane.itemAtIndex(1)
pane.trigger 'pane:show-item-1'
expect(pane.activeItem).toBe pane.itemAtIndex(0)
pane.trigger 'pane:show-item-9' # don't fail on out-of-bounds indices
expect(pane.activeItem).toBe pane.itemAtIndex(0)
describe "when the title of the active item changes", ->
it "emits pane:active-item-title-changed", ->
activeItemTitleChangedHandler = jasmine.createSpy("activeItemTitleChangedHandler")
pane.on 'pane:active-item-title-changed', activeItemTitleChangedHandler
expect(pane.activeItem).toBe view1
view2.trigger 'title-changed'
expect(activeItemTitleChangedHandler).not.toHaveBeenCalled()
view1.trigger 'title-changed'
expect(activeItemTitleChangedHandler).toHaveBeenCalled()
activeItemTitleChangedHandler.reset()
pane.showItem(view2)
view2.trigger 'title-changed'
expect(activeItemTitleChangedHandler).toHaveBeenCalled()
describe ".remove()", ->
it "destroys all the pane's items", ->
pane.remove()
expect(editSession1.destroyed).toBeTruthy()
expect(editSession2.destroyed).toBeTruthy()
it "triggers a 'pane:removed' event with the pane", ->
removedHandler = jasmine.createSpy("removedHandler")
container.on 'pane:removed', removedHandler
pane.remove()
expect(removedHandler).toHaveBeenCalled()
expect(removedHandler.argsForCall[0][1]).toBe pane
describe "when there are other panes", ->
[paneToLeft, paneToRight] = []
beforeEach ->
pane.showItem(editSession1)
paneToLeft = pane.splitLeft()
paneToRight = pane.splitRight()
container.attachToDom()
describe "when the removed pane is focused", ->
it "activates and focuses the next pane", ->
pane.focus()
pane.remove()
expect(paneToLeft.isActive()).toBeFalsy()
expect(paneToRight.isActive()).toBeTruthy()
expect(paneToRight).toMatchSelector ':has(:focus)'
describe "when the removed pane is active but not focused", ->
it "activates the next pane, but does not focus it", ->
$(document.activeElement).blur()
expect(pane).not.toMatchSelector ':has(:focus)'
pane.makeActive()
pane.remove()
expect(paneToLeft.isActive()).toBeFalsy()
expect(paneToRight.isActive()).toBeTruthy()
expect(paneToRight).not.toMatchSelector ':has(:focus)'
describe "when the removed pane is not active", ->
it "does not affect the active pane or the focus", ->
paneToLeft.focus()
expect(paneToLeft.isActive()).toBeTruthy()
expect(paneToRight.isActive()).toBeFalsy()
pane.remove()
expect(paneToLeft.isActive()).toBeTruthy()
expect(paneToRight.isActive()).toBeFalsy()
expect(paneToLeft).toMatchSelector ':has(:focus)'
describe "when it is the last pane", ->
beforeEach ->
expect(container.getPanes().length).toBe 1
window.rootView = focus: jasmine.createSpy("rootView.focus")
describe "when the removed pane is focused", ->
it "calls focus on rootView so we don't lose focus", ->
container.attachToDom()
pane.focus()
pane.remove()
expect(rootView.focus).toHaveBeenCalled()
describe "when the removed pane is not focused", ->
it "does not call focus on root view", ->
expect(pane).not.toMatchSelector ':has(:focus)'
pane.remove()
expect(rootView.focus).not.toHaveBeenCalled()
describe ".getNextPane()", ->
it "returns the next pane if one exists, wrapping around from the last pane to the first", ->
pane.showItem(editSession1)
expect(pane.getNextPane()).toBeUndefined
pane2 = pane.splitRight()
expect(pane.getNextPane()).toBe pane2
expect(pane2.getNextPane()).toBe pane
describe "when the pane is focused", ->
it "focuses the active item view", ->
focusHandler = jasmine.createSpy("focusHandler")
pane.activeItem.on 'focus', focusHandler
pane.focus()
expect(focusHandler).toHaveBeenCalled()
it "triggers 'pane:became-active' if it was not previously active", ->
becameActiveHandler = jasmine.createSpy("becameActiveHandler")
container.on 'pane:became-active', becameActiveHandler
expect(pane.isActive()).toBeFalsy()
pane.focusin()
expect(pane.isActive()).toBeTruthy()
pane.focusin()
expect(becameActiveHandler.callCount).toBe 1
describe "split methods", ->
[pane1, view3, view4] = []
beforeEach ->
pane1 = pane
pane.showItem(editSession1)
view3 = $$ -> @div id: 'view-3', 'View 3'
view4 = $$ -> @div id: 'view-4', 'View 4'
describe "splitRight(items...)", ->
it "builds a row if needed, then appends a new pane after itself", ->
# creates the new pane with a copy of the active item if none are given
pane2 = pane1.splitRight()
expect(container.find('.row .pane').toArray()).toEqual [pane1[0], pane2[0]]
expect(pane2.items).toEqual [editSession1]
expect(pane2.activeItem).not.toBe editSession1 # it's a copy
pane3 = pane2.splitRight(view3, view4)
expect(pane3.getItems()).toEqual [view3, view4]
expect(container.find('.row .pane').toArray()).toEqual [pane[0], pane2[0], pane3[0]]
describe "splitRight(items...)", ->
it "builds a row if needed, then appends a new pane before itself", ->
# creates the new pane with a copy of the active item if none are given
pane2 = pane.splitLeft()
expect(container.find('.row .pane').toArray()).toEqual [pane2[0], pane[0]]
expect(pane2.items).toEqual [editSession1]
expect(pane2.activeItem).not.toBe editSession1 # it's a copy
pane3 = pane2.splitLeft(view3, view4)
expect(pane3.getItems()).toEqual [view3, view4]
expect(container.find('.row .pane').toArray()).toEqual [pane3[0], pane2[0], pane[0]]
describe "splitDown(items...)", ->
it "builds a column if needed, then appends a new pane after itself", ->
# creates the new pane with a copy of the active item if none are given
pane2 = pane.splitDown()
expect(container.find('.column .pane').toArray()).toEqual [pane[0], pane2[0]]
expect(pane2.items).toEqual [editSession1]
expect(pane2.activeItem).not.toBe editSession1 # it's a copy
pane3 = pane2.splitDown(view3, view4)
expect(pane3.getItems()).toEqual [view3, view4]
expect(container.find('.column .pane').toArray()).toEqual [pane[0], pane2[0], pane3[0]]
describe "splitUp(items...)", ->
it "builds a column if needed, then appends a new pane before itself", ->
# creates the new pane with a copy of the active item if none are given
pane2 = pane.splitUp()
expect(container.find('.column .pane').toArray()).toEqual [pane2[0], pane[0]]
expect(pane2.items).toEqual [editSession1]
expect(pane2.activeItem).not.toBe editSession1 # it's a copy
pane3 = pane2.splitUp(view3, view4)
expect(pane3.getItems()).toEqual [view3, view4]
expect(container.find('.column .pane').toArray()).toEqual [pane3[0], pane2[0], pane[0]]
it "lays out nested panes by equally dividing their containing row / column", ->
container.width(520).height(240).attachToDom()
pane1.showItem($("1"))
pane1
.splitLeft($("2"))
.splitUp($("3"))
.splitLeft($("4"))
.splitDown($("5"))
row1 = container.children(':eq(0)')
expect(row1.children().length).toBe 2
column1 = row1.children(':eq(0)').view()
pane1 = row1.children(':eq(1)').view()
expect(column1.outerWidth()).toBe Math.round(2/3 * container.width())
expect(column1.outerHeight()).toBe container.height()
expect(pane1.outerWidth()).toBe Math.round(1/3 * container.width())
expect(pane1.outerHeight()).toBe container.height()
expect(Math.round(pane1.position().left)).toBe column1.outerWidth()
expect(column1.children().length).toBe 2
row2 = column1.children(':eq(0)').view()
pane2 = column1.children(':eq(1)').view()
expect(row2.outerWidth()).toBe column1.outerWidth()
expect(row2.height()).toBe 2/3 * container.height()
expect(pane2.outerWidth()).toBe column1.outerWidth()
expect(pane2.outerHeight()).toBe 1/3 * container.height()
expect(pane2.position().top).toBe row2.height()
expect(row2.children().length).toBe 2
column3 = row2.children(':eq(0)').view()
pane3 = row2.children(':eq(1)').view()
expect(column3.outerWidth()).toBe Math.round(1/3 * container.width())
expect(column3.outerHeight()).toBe row2.outerHeight()
# the built in rounding seems to be rounding x.5 down, but we need to go up. this sucks.
expect(Math.round(pane3.trueWidth())).toBe Math.round(1/3 * container.width())
expect(pane3.height()).toBe row2.outerHeight()
expect(Math.round(pane3.position().left)).toBe column3.width()
expect(column3.children().length).toBe 2
pane4 = column3.children(':eq(0)').view()
pane5 = column3.children(':eq(1)').view()
expect(pane4.outerWidth()).toBe column3.width()
expect(pane4.outerHeight()).toBe 1/3 * container.height()
expect(pane5.outerWidth()).toBe column3.width()
expect(pane5.position().top).toBe pane4.outerHeight()
expect(pane5.outerHeight()).toBe 1/3 * container.height()
pane5.remove()
expect(column3.parent()).not.toExist()
expect(pane2.outerHeight()).toBe Math.floor(1/2 * container.height())
expect(pane3.outerHeight()).toBe Math.floor(1/2 * container.height())
expect(pane4.outerHeight()).toBe Math.floor(1/2 * container.height())
pane4.remove()
expect(row2.parent()).not.toExist()
expect(pane1.outerWidth()).toBe Math.floor(1/2 * container.width())
expect(pane2.outerWidth()).toBe Math.floor(1/2 * container.width())
expect(pane3.outerWidth()).toBe Math.floor(1/2 * container.width())
pane3.remove()
expect(column1.parent()).not.toExist()
expect(pane2.outerHeight()).toBe container.height()
pane2.remove()
expect(row1.parent()).not.toExist()
expect(container.children().length).toBe 1
expect(container.children('.pane').length).toBe 1
expect(pane1.outerWidth()).toBe container.width()
describe "autosave", ->
[initialActiveItem, initialActiveItemUri] = []
beforeEach ->
initialActiveItem = pane.activeItem
initialActiveItemUri = null
pane.activeItem.getUri = -> initialActiveItemUri
pane.activeItem.save = jasmine.createSpy("activeItem.save")
spyOn(pane, 'saveItem').andCallThrough()
describe "when the active view loses focus", ->
it "saves the item if core.autosave is true and the item has a uri", ->
pane.activeView.trigger 'focusout'
expect(pane.saveItem).not.toHaveBeenCalled()
expect(pane.activeItem.save).not.toHaveBeenCalled()
config.set('core.autosave', true)
pane.activeView.trigger 'focusout'
expect(pane.saveItem).not.toHaveBeenCalled()
expect(pane.activeItem.save).not.toHaveBeenCalled()
initialActiveItemUri = '/tmp/hi'
pane.activeView.trigger 'focusout'
expect(pane.activeItem.save).toHaveBeenCalled()
describe "when an item becomes inactive", ->
it "saves the item if core.autosave is true and the item has a uri", ->
expect(view2).not.toBe pane.activeItem
expect(pane.saveItem).not.toHaveBeenCalled()
expect(initialActiveItem.save).not.toHaveBeenCalled()
pane.showItem(view2)
pane.showItem(initialActiveItem)
config.set('core.autosave', true)
pane.showItem(view2)
expect(pane.saveItem).not.toHaveBeenCalled()
expect(initialActiveItem.save).not.toHaveBeenCalled()
pane.showItem(initialActiveItem)
initialActiveItemUri = '/tmp/hi'
pane.showItem(view2)
expect(initialActiveItem.save).toHaveBeenCalled()
describe "when an item is destroyed", ->
it "saves the item if core.autosave is true and the item has a uri", ->
# doesn't have to be the active item
expect(view2).not.toBe pane.activeItem
pane.showItem(view2)
pane.destroyItem(editSession1)
expect(pane.saveItem).not.toHaveBeenCalled()
config.set("core.autosave", true)
view2.getUri = -> undefined
view2.save = ->
pane.destroyItem(view2)
expect(pane.saveItem).not.toHaveBeenCalled()
initialActiveItemUri = '/tmp/hi'
pane.destroyItem(initialActiveItem)
expect(initialActiveItem.save).toHaveBeenCalled()
describe ".itemForUri(uri)", ->
it "returns the item for which a call to .getUri() returns the given uri", ->
expect(pane.itemForUri(editSession1.getUri())).toBe editSession1
expect(pane.itemForUri(editSession2.getUri())).toBe editSession2
describe "serialization", ->
it "can serialize and deserialize the pane and all its serializable items", ->
newPane = deserialize(pane.serialize())
expect(newPane.getItems()).toEqual [editSession1, editSession2]
it "restores the active item on deserialization if it serializable", ->
pane.showItem(editSession2)
newPane = deserialize(pane.serialize())
expect(newPane.activeItem).toEqual editSession2
it "defaults to the first item on deserialization if the active item was not serializable", ->
expect(view2.serialize?()).toBeFalsy()
pane.showItem(view2)
newPane = deserialize(pane.serialize())
expect(newPane.activeItem).toEqual editSession1
it "focuses the pane after attach only if had focus when serialized", ->
container.attachToDom()
pane.focus()
state = pane.serialize()
pane.remove()
newPane = deserialize(state)
container.append(newPane)
expect(newPane).toMatchSelector(':has(:focus)')
$(document.activeElement).blur()
state = newPane.serialize()
newPane.remove()
newerPane = deserialize(state)
expect(newerPane).not.toMatchSelector(':has(:focus)')

View File

@@ -3,17 +3,13 @@ fs = require 'fs'
_ = require 'underscore'
describe "Project", ->
project = null
beforeEach ->
project = new Project(require.resolve('fixtures/dir'))
project.setPath(project.resolve('dir'))
afterEach ->
project.destroy()
describe "when editSession is destroyed", ->
describe "when an edit session is destroyed", ->
it "removes edit session and calls destroy on buffer (if buffer is not referenced by other edit sessions)", ->
editSession = project.buildEditSessionForPath("a")
anotherEditSession = project.buildEditSessionForPath("a")
editSession = project.buildEditSession("a")
anotherEditSession = project.buildEditSession("a")
expect(project.editSessions.length).toBe 2
expect(editSession.buffer).toBe anotherEditSession.buffer
@@ -24,7 +20,17 @@ describe "Project", ->
anotherEditSession.destroy()
expect(project.editSessions.length).toBe 0
describe ".buildEditSessionForPath(path)", ->
describe "when an edit session is saved and the project has no path", ->
it "sets the project's path to the saved file's parent directory", ->
path = project.resolve('a')
project.setPath(undefined)
expect(project.getPath()).toBeUndefined()
editSession = project.buildEditSession()
editSession.saveAs('/tmp/atom-test-save-sets-project-path')
expect(project.getPath()).toBe '/tmp'
fs.remove('/tmp/atom-test-save-sets-project-path')
describe ".buildEditSession(path)", ->
[absolutePath, newBufferHandler, newEditSessionHandler] = []
beforeEach ->
absolutePath = require.resolve('fixtures/dir/a')
@@ -35,30 +41,30 @@ describe "Project", ->
describe "when given an absolute path that hasn't been opened previously", ->
it "returns a new edit session for the given path and emits 'buffer-created' and 'edit-session-created' events", ->
editSession = project.buildEditSessionForPath(absolutePath)
editSession = project.buildEditSession(absolutePath)
expect(editSession.buffer.getPath()).toBe absolutePath
expect(newBufferHandler).toHaveBeenCalledWith editSession.buffer
expect(newEditSessionHandler).toHaveBeenCalledWith editSession
describe "when given a relative path that hasn't been opened previously", ->
it "returns a new edit session for the given path (relative to the project root) and emits 'buffer-created' and 'edit-session-created' events", ->
editSession = project.buildEditSessionForPath('a')
editSession = project.buildEditSession('a')
expect(editSession.buffer.getPath()).toBe absolutePath
expect(newBufferHandler).toHaveBeenCalledWith editSession.buffer
expect(newEditSessionHandler).toHaveBeenCalledWith editSession
describe "when passed the path to a buffer that has already been opened", ->
it "returns a new edit session containing previously opened buffer and emits a 'edit-session-created' event", ->
editSession = project.buildEditSessionForPath(absolutePath)
editSession = project.buildEditSession(absolutePath)
newBufferHandler.reset()
expect(project.buildEditSessionForPath(absolutePath).buffer).toBe editSession.buffer
expect(project.buildEditSessionForPath('a').buffer).toBe editSession.buffer
expect(project.buildEditSession(absolutePath).buffer).toBe editSession.buffer
expect(project.buildEditSession('a').buffer).toBe editSession.buffer
expect(newBufferHandler).not.toHaveBeenCalled()
expect(newEditSessionHandler).toHaveBeenCalledWith editSession
describe "when not passed a path", ->
it "returns a new edit session and emits 'buffer-created' and 'edit-session-created' events", ->
editSession = project.buildEditSessionForPath()
editSession = project.buildEditSession()
expect(editSession.buffer.getPath()).toBeUndefined()
expect(newBufferHandler).toHaveBeenCalledWith(editSession.buffer)
expect(newEditSessionHandler).toHaveBeenCalledWith editSession

View File

@@ -4,13 +4,13 @@ Project = require 'project'
RootView = require 'root-view'
Buffer = require 'buffer'
Editor = require 'editor'
Pane = require 'pane'
{View, $$} = require 'space-pen'
describe "RootView", ->
pathToOpen = null
beforeEach ->
project.destroy()
project.setPath(project.resolve('dir'))
pathToOpen = project.resolve('a')
window.rootView = new RootView
@@ -23,37 +23,39 @@ describe "RootView", ->
describe "when the serialized RootView has an unsaved buffer", ->
it "constructs the view with the same panes", ->
rootView.attachToDom()
rootView.open()
editor1 = rootView.getActiveEditor()
editor1 = rootView.getActiveView()
buffer = editor1.getBuffer()
editor1.splitRight()
viewState = rootView.serialize()
rootView.deactivate()
window.rootView = RootView.deserialize(viewState)
rootView.focus()
window.rootView = deserialize(viewState)
rootView.attachToDom()
expect(rootView.getEditors().length).toBe 2
expect(rootView.getActiveEditor().getText()).toBe buffer.getText()
expect(rootView.getTitle()).toBe "untitled #{project.getPath()}"
expect(rootView.getActiveView().getText()).toBe buffer.getText()
expect(rootView.title).toBe "untitled - #{project.getPath()}"
describe "when the serialized RootView has a project", ->
describe "when there are open editors", ->
it "constructs the view with the same panes", ->
editor1 = rootView.getActiveEditor()
editor2 = editor1.splitRight()
editor3 = editor2.splitRight()
editor4 = editor2.splitDown()
editor2.edit(project.buildEditSessionForPath('b'))
editor3.edit(project.buildEditSessionForPath('../sample.js'))
editor3.setCursorScreenPosition([2, 4])
editor4.edit(project.buildEditSessionForPath('../sample.txt'))
editor4.setCursorScreenPosition([0, 2])
rootView.attachToDom()
editor2.focus()
pane1 = rootView.getActivePane()
pane2 = pane1.splitRight()
pane3 = pane2.splitRight()
pane4 = pane2.splitDown()
pane2.showItem(project.buildEditSession('b'))
pane3.showItem(project.buildEditSession('../sample.js'))
pane3.activeItem.setCursorScreenPosition([2, 4])
pane4.showItem(project.buildEditSession('../sample.txt'))
pane4.activeItem.setCursorScreenPosition([0, 2])
pane2.focus()
viewState = rootView.serialize()
rootView.deactivate()
window.rootView = RootView.deserialize(viewState)
window.rootView = deserialize(viewState)
rootView.attachToDom()
expect(rootView.getEditors().length).toBe 4
@@ -81,276 +83,54 @@ describe "RootView", ->
expect(editor3.isFocused).toBeFalsy()
expect(editor4.isFocused).toBeFalsy()
expect(rootView.getTitle()).toBe "#{fs.base(editor2.getPath())} #{project.getPath()}"
expect(rootView.title).toBe "#{fs.base(editor2.getPath())} - #{project.getPath()}"
describe "where there are no open editors", ->
it "constructs the view with no open editors", ->
rootView.getActiveEditor().remove()
rootView.getActivePane().remove()
expect(rootView.getEditors().length).toBe 0
viewState = rootView.serialize()
rootView.deactivate()
window.rootView = RootView.deserialize(viewState)
window.rootView = deserialize(viewState)
rootView.attachToDom()
expect(rootView.getEditors().length).toBe 0
describe "when a pane's wrapped view cannot be deserialized", ->
it "renders an empty pane", ->
viewState =
panesViewState:
deserializer: "Pane",
wrappedView:
deserializer: "BogusView"
rootView.deactivate()
window.rootView = RootView.deserialize(viewState)
expect(rootView.find('.pane').length).toBe 1
expect(rootView.find('.pane').children().length).toBe 0
describe "focus", ->
describe "when there is an active editor", ->
it "hands off focus to the active editor", ->
rootView.attachToDom()
rootView.open() # create an editor
expect(rootView).not.toMatchSelector(':focus')
expect(rootView.getActiveEditor().isFocused).toBeTruthy()
describe "when there is an active view", ->
it "hands off focus to the active view", ->
editor = rootView.getActiveView()
editor.isFocused = false
rootView.focus()
expect(rootView).not.toMatchSelector(':focus')
expect(rootView.getActiveEditor().isFocused).toBeTruthy()
expect(editor.isFocused).toBeTruthy()
describe "when there is no active editor", ->
describe "when there is no active view", ->
beforeEach ->
rootView.getActiveEditor().remove()
rootView.getActivePane().remove()
expect(rootView.getActiveView()).toBeUndefined()
rootView.attachToDom()
expect(document.activeElement).toBe document.body
describe "when are visible focusable elements (with a -1 tabindex)", ->
it "passes focus to the first focusable element", ->
rootView.horizontal.append $$ ->
@div "One", id: 'one', tabindex: -1
@div "Two", id: 'two', tabindex: -1
focusable1 = $$ -> @div "One", id: 'one', tabindex: -1
focusable2 = $$ -> @div "Two", id: 'two', tabindex: -1
rootView.horizontal.append(focusable1, focusable2)
expect(document.activeElement).toBe document.body
rootView.focus()
expect(rootView).not.toMatchSelector(':focus')
expect(rootView.find('#one')).toMatchSelector(':focus')
expect(rootView.find('#two')).not.toMatchSelector(':focus')
expect(document.activeElement).toBe focusable1[0]
describe "when there are no visible focusable elements", ->
it "surrenders focus to the body", ->
expect(document.activeElement).toBe $('body')[0]
focusable = $$ -> @div "One", id: 'one', tabindex: -1
rootView.horizontal.append(focusable)
focusable.hide()
expect(document.activeElement).toBe document.body
describe "panes", ->
[pane1, newPaneContent] = []
beforeEach ->
rootView.attachToDom()
rootView.width(800)
rootView.height(600)
pane1 = rootView.find('.pane').view()
pane1.attr('id', 'pane-1')
newPaneContent = $("<div>New pane content</div>")
spyOn(newPaneContent, 'focus')
describe "vertical splits", ->
describe "when .splitRight(view) is called on a pane", ->
it "places a new pane to the right of the current pane in a .row div", ->
expect(rootView.panes.find('.row')).not.toExist()
pane2 = pane1.splitRight(newPaneContent)
expect(newPaneContent.focus).toHaveBeenCalled()
expect(rootView.panes.find('.row')).toExist()
expect(rootView.panes.find('.row .pane').length).toBe 2
[leftPane, rightPane] = rootView.panes.find('.row .pane').map -> $(this)
expect(rightPane[0]).toBe pane2[0]
expect(leftPane.attr('id')).toBe 'pane-1'
expect(rightPane.html()).toBe "<div>New pane content</div>"
expectedColumnWidth = Math.floor(rootView.panes.width() / 2)
expect(leftPane.outerWidth()).toBe expectedColumnWidth
expect(rightPane.position().left).toBe expectedColumnWidth
expect(rightPane.outerWidth()).toBe expectedColumnWidth
pane2.remove()
expect(rootView.panes.find('.row')).not.toExist()
expect(rootView.panes.find('.pane').length).toBe 1
expect(pane1.outerWidth()).toBe rootView.panes.width()
describe "when splitLeft(view) is called on a pane", ->
it "places a new pane to the left of the current pane in a .row div", ->
expect(rootView.find('.row')).not.toExist()
pane2 = pane1.splitLeft(newPaneContent)
expect(newPaneContent.focus).toHaveBeenCalled()
expect(rootView.find('.row')).toExist()
expect(rootView.find('.row .pane').length).toBe 2
[leftPane, rightPane] = rootView.find('.row .pane').map -> $(this)
expect(leftPane[0]).toBe pane2[0]
expect(rightPane.attr('id')).toBe 'pane-1'
expect(leftPane.html()).toBe "<div>New pane content</div>"
expectedColumnWidth = Math.floor(rootView.panes.width() / 2)
expect(leftPane.outerWidth()).toBe expectedColumnWidth
expect(rightPane.position().left).toBe expectedColumnWidth
expect(rightPane.outerWidth()).toBe expectedColumnWidth
pane2.remove()
expect(rootView.panes.find('.row')).not.toExist()
expect(rootView.panes.find('.pane').length).toBe 1
expect(pane1.outerWidth()).toBe rootView.panes.width()
expect(pane1.position().left).toBe 0
describe "horizontal splits", ->
describe "when splitUp(view) is called on a pane", ->
it "places a new pane above the current pane in a .column div", ->
expect(rootView.find('.column')).not.toExist()
pane2 = pane1.splitUp(newPaneContent)
expect(newPaneContent.focus).toHaveBeenCalled()
expect(rootView.find('.column')).toExist()
expect(rootView.find('.column .pane').length).toBe 2
[topPane, bottomPane] = rootView.find('.column .pane').map -> $(this)
expect(topPane[0]).toBe pane2[0]
expect(bottomPane.attr('id')).toBe 'pane-1'
expect(topPane.html()).toBe "<div>New pane content</div>"
expectedRowHeight = Math.floor(rootView.panes.height() / 2)
expect(topPane.outerHeight()).toBe expectedRowHeight
expect(bottomPane.position().top).toBe expectedRowHeight
expect(bottomPane.outerHeight()).toBe expectedRowHeight
pane2.remove()
expect(rootView.panes.find('.column')).not.toExist()
expect(rootView.panes.find('.pane').length).toBe 1
expect(pane1.outerHeight()).toBe rootView.panes.height()
expect(pane1.position().top).toBe 0
describe "when splitDown(view) is called on a pane", ->
it "places a new pane below the current pane in a .column div", ->
expect(rootView.find('.column')).not.toExist()
pane2 = pane1.splitDown(newPaneContent)
expect(newPaneContent.focus).toHaveBeenCalled()
expect(rootView.find('.column')).toExist()
expect(rootView.find('.column .pane').length).toBe 2
[topPane, bottomPane] = rootView.find('.column .pane').map -> $(this)
expect(bottomPane[0]).toBe pane2[0]
expect(topPane.attr('id')).toBe 'pane-1'
expect(bottomPane.html()).toBe "<div>New pane content</div>"
expectedRowHeight = Math.floor(rootView.panes.height() / 2)
expect(topPane.outerHeight()).toBe expectedRowHeight
expect(bottomPane.position().top).toBe expectedRowHeight
expect(bottomPane.outerHeight()).toBe expectedRowHeight
pane2.remove()
expect(rootView.panes.find('.column')).not.toExist()
expect(rootView.panes.find('.pane').length).toBe 1
expect(pane1.outerHeight()).toBe rootView.panes.height()
describe "layout of nested vertical and horizontal splits", ->
it "lays out rows and columns with a consistent width", ->
pane1.html("1")
pane1
.splitLeft("2")
.splitUp("3")
.splitLeft("4")
.splitDown("5")
row1 = rootView.panes.children(':eq(0)')
expect(row1.children().length).toBe 2
column1 = row1.children(':eq(0)').view()
pane1 = row1.children(':eq(1)').view()
expect(column1.outerWidth()).toBe Math.round(2/3 * rootView.panes.width())
expect(column1.outerHeight()).toBe rootView.height()
expect(pane1.outerWidth()).toBe Math.round(1/3 * rootView.panes.width())
expect(pane1.outerHeight()).toBe rootView.height()
expect(Math.round(pane1.position().left)).toBe column1.outerWidth()
expect(column1.children().length).toBe 2
row2 = column1.children(':eq(0)').view()
pane2 = column1.children(':eq(1)').view()
expect(row2.outerWidth()).toBe column1.outerWidth()
expect(row2.height()).toBe 2/3 * rootView.panes.height()
expect(pane2.outerWidth()).toBe column1.outerWidth()
expect(pane2.outerHeight()).toBe 1/3 * rootView.panes.height()
expect(pane2.position().top).toBe row2.height()
expect(row2.children().length).toBe 2
column3 = row2.children(':eq(0)').view()
pane3 = row2.children(':eq(1)').view()
expect(column3.outerWidth()).toBe Math.round(1/3 * rootView.panes.width())
expect(column3.outerHeight()).toBe row2.outerHeight()
# the built in rounding seems to be rounding x.5 down, but we need to go up. this sucks.
expect(Math.round(pane3.trueWidth())).toBe Math.round(1/3 * rootView.panes.width())
expect(pane3.height()).toBe row2.outerHeight()
expect(Math.round(pane3.position().left)).toBe column3.width()
expect(column3.children().length).toBe 2
pane4 = column3.children(':eq(0)').view()
pane5 = column3.children(':eq(1)').view()
expect(pane4.outerWidth()).toBe column3.width()
expect(pane4.outerHeight()).toBe 1/3 * rootView.panes.height()
expect(pane5.outerWidth()).toBe column3.width()
expect(pane5.position().top).toBe pane4.outerHeight()
expect(pane5.outerHeight()).toBe 1/3 * rootView.panes.height()
pane5.remove()
expect(column3.parent()).not.toExist()
expect(pane2.outerHeight()).toBe Math.floor(1/2 * rootView.panes.height())
expect(pane3.outerHeight()).toBe Math.floor(1/2 * rootView.panes.height())
expect(pane4.outerHeight()).toBe Math.floor(1/2 * rootView.panes.height())
pane4.remove()
expect(row2.parent()).not.toExist()
expect(pane1.outerWidth()).toBe Math.floor(1/2 * rootView.panes.width())
expect(pane2.outerWidth()).toBe Math.floor(1/2 * rootView.panes.width())
expect(pane3.outerWidth()).toBe Math.floor(1/2 * rootView.panes.width())
pane3.remove()
expect(column1.parent()).not.toExist()
expect(pane2.outerHeight()).toBe rootView.panes.height()
pane2.remove()
expect(row1.parent()).not.toExist()
expect(rootView.panes.children().length).toBe 1
expect(rootView.panes.children('.pane').length).toBe 1
expect(pane1.outerWidth()).toBe rootView.panes.width()
describe ".focusNextPane()", ->
it "focuses the wrapped view of the pane after the currently focused pane", ->
class DummyView extends View
@content: (number) -> @div(number, tabindex: -1)
view1 = pane1.wrappedView
view2 = new DummyView(2)
view3 = new DummyView(3)
pane2 = pane1.splitDown(view2)
pane3 = pane2.splitRight(view3)
rootView.attachToDom()
view1.focus()
spyOn(view1, 'focus').andCallThrough()
spyOn(view2, 'focus').andCallThrough()
spyOn(view3, 'focus').andCallThrough()
rootView.focusNextPane()
expect(view2.focus).toHaveBeenCalled()
rootView.focusNextPane()
expect(view3.focus).toHaveBeenCalled()
rootView.focusNextPane()
expect(view1.focus).toHaveBeenCalled()
rootView.focus()
expect(document.activeElement).toBe document.body
describe "keymap wiring", ->
commandHandler = null
@@ -360,249 +140,144 @@ describe "RootView", ->
window.keymap.bindKeys('*', 'x': 'foo-command')
describe "when a keydown event is triggered on the RootView (not originating from Ace)", ->
describe "when a keydown event is triggered on the RootView", ->
it "triggers matching keybindings for that event", ->
event = keydownEvent 'x', target: rootView[0]
rootView.trigger(event)
expect(commandHandler).toHaveBeenCalled()
describe ".activeKeybindings()", ->
originalKeymap = null
keymap = null
editor = null
describe "window title", ->
describe "when the project has no path", ->
it "sets the title to 'untitled'", ->
project.setPath(undefined)
expect(rootView.title).toBe 'untitled'
describe "when the project has a path", ->
beforeEach ->
rootView.attachToDom()
editor = rootView.getActiveEditor()
keymap = new (require 'keymap')
originalKeymap = window.keymap
window.keymap = keymap
rootView.open('b')
afterEach ->
window.keymap = originalKeymap
describe "when there is an active pane item", ->
it "sets the title to the pane item's title plus the project path", ->
item = rootView.getActivePaneItem()
expect(rootView.title).toBe "#{item.getTitle()} - #{project.getPath()}"
it "returns all keybindings available for focused element", ->
editor.on 'test-event-a', => # nothing
describe "when the title of the active pane item changes", ->
it "updates the window title based on the item's new title", ->
editSession = rootView.getActivePaneItem()
editSession.buffer.setPath('/tmp/hi')
expect(rootView.title).toBe "#{editSession.getTitle()} - #{project.getPath()}"
keymap.bindKeys ".editor",
"meta-a": "test-event-a"
"meta-b": "test-event-b"
describe "when the active pane's item changes", ->
it "updates the title to the new item's title plus the project path", ->
rootView.getActivePane().showNextItem()
item = rootView.getActivePaneItem()
expect(rootView.title).toBe "#{item.getTitle()} - #{project.getPath()}"
keybindings = rootView.activeKeybindings()
expect(Object.keys(keybindings).length).toBe 2
expect(keybindings["meta-a"]).toEqual "test-event-a"
describe "when the last pane item is removed", ->
it "sets the title to the project's path", ->
rootView.getActivePane().remove()
expect(rootView.getActivePaneItem()).toBeUndefined()
expect(rootView.title).toBe project.getPath()
describe "when the path of the active editor changes", ->
it "changes the title and emits an root-view:active-path-changed event", ->
pathChangeHandler = jasmine.createSpy 'pathChangeHandler'
rootView.on 'root-view:active-path-changed', pathChangeHandler
editor1 = rootView.getActiveEditor()
expect(rootView.getTitle()).toBe "#{fs.base(editor1.getPath())} #{project.getPath()}"
editor2 = rootView.getActiveEditor().splitLeft()
path = project.resolve('b')
editor2.edit(project.buildEditSessionForPath(path))
expect(pathChangeHandler).toHaveBeenCalled()
expect(rootView.getTitle()).toBe "#{fs.base(editor2.getPath())} #{project.getPath()}"
pathChangeHandler.reset()
editor1.getBuffer().saveAs("/tmp/should-not-be-title.txt")
expect(pathChangeHandler).not.toHaveBeenCalled()
expect(rootView.getTitle()).toBe "#{fs.base(editor2.getPath())} #{project.getPath()}"
it "sets the project path to the directory of the editor if it was previously unassigned", ->
project.setPath(undefined)
window.rootView = new RootView
rootView.open()
expect(project.getPath()?).toBeFalsy()
rootView.getActiveEditor().getBuffer().saveAs('/tmp/ignore-me')
expect(project.getPath()).toBe '/tmp'
describe "when editors are focused", ->
it "triggers 'root-view:active-path-changed' events if the path of the active editor actually changes", ->
pathChangeHandler = jasmine.createSpy 'pathChangeHandler'
rootView.on 'root-view:active-path-changed', pathChangeHandler
editor1 = rootView.getActiveEditor()
editor2 = rootView.getActiveEditor().splitLeft()
rootView.open(require.resolve('fixtures/sample.txt'))
expect(pathChangeHandler).toHaveBeenCalled()
pathChangeHandler.reset()
editor1.focus()
expect(pathChangeHandler).toHaveBeenCalled()
pathChangeHandler.reset()
rootView.focus()
expect(pathChangeHandler).not.toHaveBeenCalled()
editor2.edit(editor1.activeEditSession.copy())
editor2.focus()
expect(pathChangeHandler).not.toHaveBeenCalled()
describe "when the last editor is removed", ->
it "updates the title to the project path", ->
rootView.getEditors()[0].remove()
expect(rootView.getTitle()).toBe project.getPath()
describe "when an inactive pane's item changes", ->
it "does not update the title", ->
pane = rootView.getActivePane()
pane.splitRight()
initialTitle = rootView.title
pane.showNextItem()
expect(rootView.title).toBe initialTitle
describe "font size adjustment", ->
editor = null
beforeEach ->
editor = rootView.getActiveEditor()
editor.attachToDom()
it "increases/decreases font size when increase/decrease-font-size events are triggered", ->
fontSizeBefore = editor.getFontSize()
fontSizeBefore = config.get('editor.fontSize')
rootView.trigger 'window:increase-font-size'
expect(editor.getFontSize()).toBe fontSizeBefore + 1
expect(config.get('editor.fontSize')).toBe fontSizeBefore + 1
rootView.trigger 'window:increase-font-size'
expect(editor.getFontSize()).toBe fontSizeBefore + 2
expect(config.get('editor.fontSize')).toBe fontSizeBefore + 2
rootView.trigger 'window:decrease-font-size'
expect(editor.getFontSize()).toBe fontSizeBefore + 1
expect(config.get('editor.fontSize')).toBe fontSizeBefore + 1
rootView.trigger 'window:decrease-font-size'
expect(editor.getFontSize()).toBe fontSizeBefore
expect(config.get('editor.fontSize')).toBe fontSizeBefore
it "does not allow the font size to be less than 1", ->
config.set("editor.fontSize", 1)
rootView.trigger 'window:decrease-font-size'
expect(editor.getFontSize()).toBe 1
expect(config.get('editor.fontSize')).toBe 1
describe ".open(path, options)", ->
describe "when there is no active editor", ->
describe "when there is no active pane", ->
beforeEach ->
rootView.getActiveEditor().destroyActiveEditSession()
expect(rootView.getActiveEditor()).toBeUndefined()
spyOn(Pane.prototype, 'focus')
rootView.getActivePane().remove()
expect(rootView.getActivePane()).toBeUndefined()
describe "when called with no path", ->
it "opens / returns an edit session for an empty buffer in a new editor", ->
it "creates a empty edit session as an item on a new pane, and focuses the pane", ->
editSession = rootView.open()
expect(rootView.getActiveEditor()).toBeDefined()
expect(rootView.getActiveEditor().getPath()).toBeUndefined()
expect(editSession).toBe rootView.getActiveEditor().activeEditSession
expect(rootView.getActivePane().activeItem).toBe editSession
expect(editSession.getPath()).toBeUndefined()
expect(rootView.getActivePane().focus).toHaveBeenCalled()
describe "when called with a path", ->
it "opens a buffer with the given path in a new editor", ->
it "creates an edit session for the given path as an item on a new pane, and focuses the pane", ->
editSession = rootView.open('b')
expect(rootView.getActiveEditor()).toBeDefined()
expect(rootView.getActiveEditor().getPath()).toBe require.resolve('fixtures/dir/b')
expect(editSession).toBe rootView.getActiveEditor().activeEditSession
expect(rootView.getActivePane().activeItem).toBe editSession
expect(editSession.getPath()).toBe require.resolve('fixtures/dir/b')
expect(rootView.getActivePane().focus).toHaveBeenCalled()
describe "when there is an active editor", ->
describe "when the changeFocus option is false", ->
it "does not focus the new pane", ->
editSession = rootView.open('b', changeFocus: false)
expect(rootView.getActivePane().focus).not.toHaveBeenCalled()
describe "when there is an active pane", ->
[activePane, initialItemCount] = []
beforeEach ->
expect(rootView.getActiveEditor()).toBeDefined()
activePane = rootView.getActivePane()
spyOn(activePane, 'focus')
initialItemCount = activePane.getItems().length
describe "when called with no path", ->
it "opens an empty buffer in the active editor", ->
it "opens an edit session with an empty buffer as an item on the active pane and focuses it", ->
editSession = rootView.open()
expect(rootView.getActiveEditor().getPath()).toBeUndefined()
expect(editSession).toBe rootView.getActiveEditor().activeEditSession
expect(activePane.getItems().length).toBe initialItemCount + 1
expect(activePane.activeItem).toBe editSession
expect(editSession.getPath()).toBeUndefined()
expect(activePane.focus).toHaveBeenCalled()
describe "when called with a path", ->
[editor1, editor2] = []
beforeEach ->
rootView.attachToDom()
editor1 = rootView.getActiveEditor()
editor2 = editor1.splitRight()
rootView.open('b')
editor2.loadPreviousEditSession()
editor1.focus()
describe "when the active pane already has an edit session item for the path being opened", ->
it "shows the existing edit session on the pane", ->
previousEditSession = activePane.activeItem
describe "when allowActiveEditorChange is false (the default)", ->
activeEditor = null
beforeEach ->
activeEditor = rootView.getActiveEditor()
editSession = rootView.open('b')
expect(activePane.activeItem).toBe editSession
expect(editSession).not.toBe previousEditSession
describe "when the active editor has an edit session for the given path", ->
it "re-activates the existing edit session", ->
expect(activeEditor.getPath()).toBe require.resolve('fixtures/dir/a')
previousEditSession = activeEditor.activeEditSession
editSession = rootView.open(previousEditSession.getPath())
expect(editSession).toBe previousEditSession
expect(activePane.activeItem).toBe editSession
editSession = rootView.open('b')
expect(activeEditor.activeEditSession).not.toBe previousEditSession
expect(editSession).toBe rootView.getActiveEditor().activeEditSession
expect(activePane.focus).toHaveBeenCalled()
editSession = rootView.open('a')
expect(activeEditor.activeEditSession).toBe previousEditSession
expect(editSession).toBe previousEditSession
describe "when the active pane does not have an edit session item for the path being opened", ->
it "creates a new edit session for the given path in the active editor", ->
editSession = rootView.open('b')
expect(activePane.items.length).toBe 2
expect(activePane.activeItem).toBe editSession
expect(activePane.focus).toHaveBeenCalled()
describe "when the active editor does not have an edit session for the given path", ->
it "creates a new edit session for the given path in the active editor", ->
editSession = rootView.open('b')
expect(activeEditor.editSessions.length).toBe 2
expect(editSession).toBe rootView.getActiveEditor().activeEditSession
describe "when the 'allowActiveEditorChange' option is true", ->
describe "when the active editor has an edit session for the given path", ->
it "re-activates the existing edit session regardless of whether any other editor also has an edit session for the path", ->
activeEditor = rootView.getActiveEditor()
expect(activeEditor.getPath()).toBe require.resolve('fixtures/dir/a')
previousEditSession = activeEditor.activeEditSession
editSession = rootView.open('b')
expect(activeEditor.activeEditSession).not.toBe previousEditSession
expect(editSession).toBe activeEditor.activeEditSession
editSession = rootView.open('a', allowActiveEditorChange: true)
expect(activeEditor.activeEditSession).toBe previousEditSession
expect(editSession).toBe activeEditor.activeEditSession
describe "when the active editor does *not* have an edit session for the given path", ->
describe "when another editor has an edit session for the path", ->
it "focuses the other editor and activates its edit session for the path", ->
expect(rootView.getActiveEditor()).toBe editor1
editSession = rootView.open('b', allowActiveEditorChange: true)
expect(rootView.getActiveEditor()).toBe editor2
expect(editor2.getPath()).toBe require.resolve('fixtures/dir/b')
expect(editSession).toBe rootView.getActiveEditor().activeEditSession
describe "when no other editor has an edit session for the path either", ->
it "creates a new edit session for the path on the current active editor", ->
path = require.resolve('fixtures/sample.js')
editSession = rootView.open(path, allowActiveEditorChange: true)
expect(rootView.getActiveEditor()).toBe editor1
expect(editor1.getPath()).toBe path
expect(editSession).toBe rootView.getActiveEditor().activeEditSession
describe ".saveAll()", ->
it "saves all open editors", ->
project.setPath('/tmp')
file1 = '/tmp/atom-temp1.txt'
file2 = '/tmp/atom-temp2.txt'
fs.write(file1, "file1")
fs.write(file2, "file2")
rootView.open(file1)
editor1 = rootView.getActiveEditor()
buffer1 = editor1.activeEditSession.buffer
expect(buffer1.getText()).toBe("file1")
expect(buffer1.isModified()).toBe(false)
buffer1.setText('edited1')
expect(buffer1.isModified()).toBe(true)
editor2 = editor1.splitRight()
editor2.edit(project.buildEditSessionForPath('atom-temp2.txt'))
buffer2 = editor2.activeEditSession.buffer
expect(buffer2.getText()).toBe("file2")
expect(buffer2.isModified()).toBe(false)
buffer2.setText('edited2')
expect(buffer2.isModified()).toBe(true)
rootView.saveAll()
expect(buffer1.isModified()).toBe(false)
expect(fs.read(buffer1.getPath())).toBe("edited1")
expect(buffer2.isModified()).toBe(false)
expect(fs.read(buffer2.getPath())).toBe("edited2")
describe "when the changeFocus option is false", ->
it "does not focus the active pane", ->
editSession = rootView.open('b', changeFocus: false)
expect(activePane.focus).not.toHaveBeenCalled()
describe "window:toggle-invisibles event", ->
it "shows/hides invisibles in all open and future editors", ->
rootView.height(200)
rootView.attachToDom()
rightEditor = rootView.getActiveEditor()
rightEditor = rootView.getActiveView()
rightEditor.setText(" \t ")
leftEditor = rightEditor.splitLeft()
expect(rightEditor.find(".line:first").text()).toBe " "
@@ -636,7 +311,7 @@ describe "RootView", ->
count++
rootView.eachEditor(callback)
expect(count).toBe 1
expect(callbackEditor).toBe rootView.getActiveEditor()
expect(callbackEditor).toBe rootView.getActiveView()
it "invokes the callback for new editor", ->
count = 0
@@ -648,9 +323,9 @@ describe "RootView", ->
rootView.eachEditor(callback)
count = 0
callbackEditor = null
rootView.getActiveEditor().splitRight()
rootView.getActiveView().splitRight()
expect(count).toBe 1
expect(callbackEditor).toBe rootView.getActiveEditor()
expect(callbackEditor).toBe rootView.getActiveView()
describe ".eachBuffer(callback)", ->
beforeEach ->
@@ -664,7 +339,7 @@ describe "RootView", ->
count++
rootView.eachBuffer(callback)
expect(count).toBe 1
expect(callbackBuffer).toBe rootView.getActiveEditor().getBuffer()
expect(callbackBuffer).toBe rootView.getActiveView().getBuffer()
it "invokes the callback for new buffer", ->
count = 0
@@ -678,4 +353,4 @@ describe "RootView", ->
callbackBuffer = null
rootView.open(require.resolve('fixtures/sample.txt'))
expect(count).toBe 1
expect(callbackBuffer).toBe rootView.getActiveEditor().getBuffer()
expect(callbackBuffer).toBe rootView.getActiveView().getBuffer()

View File

@@ -262,7 +262,7 @@ describe "TextMateGrammar", ->
describe "when the grammar is CSON", ->
it "loads the grammar and correctly parses a keyword", ->
spyOn(syntax, 'addGrammar')
pack = new TextMatePackage(fixturesProject.resolve("packages/package-with-a-cson-grammar.tmbundle"))
pack = new TextMatePackage(project.resolve("packages/package-with-a-cson-grammar.tmbundle"))
pack.load()
grammar = pack.grammars[0]
expect(grammar).toBeTruthy()

View File

@@ -20,33 +20,38 @@ describe "@load(name)", ->
expect($(".editor").css("background-color")).toBe("rgb(20, 20, 20)")
describe "AtomTheme", ->
describe "when the theme is a file", ->
it "loads and applies css", ->
expect($(".editor").css("padding-bottom")).not.toBe "1234px"
themePath = project.resolve('themes/theme-stylesheet.css')
theme = Theme.load(themePath)
expect($(".editor").css("padding-top")).toBe "1234px"
it "parses, loads and applies less", ->
expect($(".editor").css("padding-bottom")).not.toBe "1234px"
themePath = project.resolve('themes/theme-stylesheet.less')
theme = Theme.load(themePath)
expect($(".editor").css("padding-top")).toBe "4321px"
describe "when the theme contains a package.json file", ->
it "loads and applies css from package.json in the correct order", ->
it "loads and applies stylesheets from package.json in the correct order", ->
expect($(".editor").css("padding-top")).not.toBe("101px")
expect($(".editor").css("padding-right")).not.toBe("102px")
expect($(".editor").css("padding-bottom")).not.toBe("103px")
themePath = fixturesProject.resolve('themes/theme-with-package-file')
themePath = project.resolve('themes/theme-with-package-file')
theme = Theme.load(themePath)
expect($(".editor").css("padding-top")).toBe("101px")
expect($(".editor").css("padding-right")).toBe("102px")
expect($(".editor").css("padding-bottom")).toBe("103px")
describe "when the theme is a CSS file", ->
it "loads and applies the stylesheet", ->
expect($(".editor").css("padding-bottom")).not.toBe "1234px"
themePath = fixturesProject.resolve('themes/theme-stylesheet.css')
theme = Theme.load(themePath)
expect($(".editor").css("padding-top")).toBe "1234px"
describe "when the theme does not contain a package.json file and is a directory", ->
it "loads all CSS files in the directory", ->
it "loads all stylesheet files in the directory", ->
expect($(".editor").css("padding-top")).not.toBe "10px"
expect($(".editor").css("padding-right")).not.toBe "20px"
expect($(".editor").css("padding-bottom")).not.toBe "30px"
themePath = fixturesProject.resolve('themes/theme-without-package-file')
themePath = project.resolve('themes/theme-without-package-file')
theme = Theme.load(themePath)
expect($(".editor").css("padding-top")).toBe "10px"
expect($(".editor").css("padding-right")).toBe "20px"

View File

@@ -18,7 +18,7 @@ describe "TokenizedBuffer", ->
describe "when the buffer contains soft-tabs", ->
beforeEach ->
editSession = fixturesProject.buildEditSessionForPath('sample.js', autoIndent: false)
editSession = project.buildEditSession('sample.js', autoIndent: false)
buffer = editSession.buffer
tokenizedBuffer = editSession.displayBuffer.tokenizedBuffer
editSession.setVisible(true)
@@ -299,7 +299,7 @@ describe "TokenizedBuffer", ->
describe "when the buffer contains hard-tabs", ->
beforeEach ->
tabLength = 2
editSession = fixturesProject.buildEditSessionForPath('sample-with-tabs.coffee', { tabLength })
editSession = project.buildEditSession('sample-with-tabs.coffee', { tabLength })
buffer = editSession.buffer
tokenizedBuffer = editSession.displayBuffer.tokenizedBuffer
editSession.setVisible(true)
@@ -328,7 +328,7 @@ describe "TokenizedBuffer", ->
describe "when a Git commit message file is tokenized", ->
beforeEach ->
editSession = fixturesProject.buildEditSessionForPath('COMMIT_EDITMSG', autoIndent: false)
editSession = project.buildEditSession('COMMIT_EDITMSG', autoIndent: false)
buffer = editSession.buffer
tokenizedBuffer = editSession.displayBuffer.tokenizedBuffer
editSession.setVisible(true)
@@ -355,7 +355,7 @@ describe "TokenizedBuffer", ->
describe "when a C++ source file is tokenized", ->
beforeEach ->
editSession = fixturesProject.buildEditSessionForPath('includes.cc', autoIndent: false)
editSession = project.buildEditSession('includes.cc', autoIndent: false)
buffer = editSession.buffer
tokenizedBuffer = editSession.displayBuffer.tokenizedBuffer
editSession.setVisible(true)
@@ -386,7 +386,7 @@ describe "TokenizedBuffer", ->
describe "when a Ruby source file is tokenized", ->
beforeEach ->
editSession = fixturesProject.buildEditSessionForPath('hello.rb', autoIndent: false)
editSession = project.buildEditSession('hello.rb', autoIndent: false)
buffer = editSession.buffer
tokenizedBuffer = editSession.displayBuffer.tokenizedBuffer
editSession.setVisible(true)
@@ -403,7 +403,7 @@ describe "TokenizedBuffer", ->
describe "when an Objective-C source file is tokenized", ->
beforeEach ->
editSession = fixturesProject.buildEditSessionForPath('function.mm', autoIndent: false)
editSession = project.buildEditSession('function.mm', autoIndent: false)
buffer = editSession.buffer
tokenizedBuffer = editSession.displayBuffer.tokenizedBuffer
editSession.setVisible(true)

View File

@@ -1,5 +1,6 @@
$ = require 'jquery'
fs = require 'fs'
{less} = require 'less'
describe "Window", ->
projectPath = null
@@ -34,49 +35,89 @@ describe "Window", ->
$(window).trigger 'focus'
expect($("body")).not.toHaveClass("is-blurred")
describe ".close()", ->
it "is triggered by the 'core:close' event", ->
spyOn window, 'close'
$(window).trigger 'core:close'
expect(window.close).toHaveBeenCalled()
describe "window:close event", ->
describe "when no pane items are modified", ->
it "calls window.close", ->
spyOn window, 'close'
$(window).trigger 'window:close'
expect(window.close).toHaveBeenCalled()
it "is triggered by the 'window:close event'", ->
spyOn window, 'close'
$(window).trigger 'window:close'
expect(window.close).toHaveBeenCalled()
describe "when pane items are are modified", ->
it "prompts user to save and and calls window.close", ->
spyOn(window, 'close')
spyOn(atom, "confirm").andCallFake (a, b, c, d, e, f, g, noSave) -> noSave()
editSession = rootView.open("sample.js")
editSession.insertText("I look different, I feel different.")
$(window).trigger 'window:close'
expect(window.close).toHaveBeenCalled()
expect(atom.confirm).toHaveBeenCalled()
it "prompts user to save and aborts if dialog is canceled", ->
spyOn(window, 'close')
spyOn(atom, "confirm").andCallFake (a, b, c, d, e, cancel) -> cancel()
editSession = rootView.open("sample.js")
editSession.insertText("I look different, I feel different.")
$(window).trigger 'window:close'
expect(window.close).not.toHaveBeenCalled()
expect(atom.confirm).toHaveBeenCalled()
describe ".reload()", ->
it "returns false when no buffers are modified", ->
beforeEach ->
spyOn($native, "reload")
it "returns false when no buffers are modified", ->
window.reload()
expect($native.reload).toHaveBeenCalled()
it "shows alert when a modifed buffer exists", ->
it "shows an alert when a modifed buffer exists", ->
rootView.open('sample.js')
rootView.getActiveEditor().insertText("hi")
rootView.getActiveView().insertText("hi")
spyOn(atom, "confirm")
spyOn($native, "reload")
window.reload()
expect($native.reload).not.toHaveBeenCalled()
expect(atom.confirm).toHaveBeenCalled()
describe "requireStylesheet(path)", ->
it "synchronously loads the stylesheet at the given path and installs a style tag for it in the head", ->
$('head style[id*="atom.css"]').remove()
it "synchronously loads css at the given path and installs a style tag for it in the head", ->
cssPath = project.resolve('css.css')
lengthBefore = $('head style').length
requireStylesheet('atom.css')
requireStylesheet(cssPath)
expect($('head style').length).toBe lengthBefore + 1
styleElt = $('head style[id*="atom.css"]')
fullPath = require.resolve('atom.css')
expect(styleElt.attr('id')).toBe fullPath
expect(styleElt.text()).toBe fs.read(fullPath)
element = $('head style[id*="css.css"]')
expect(element.attr('id')).toBe cssPath
expect(element.text()).toBe fs.read(cssPath)
# doesn't append twice
requireStylesheet('atom.css')
requireStylesheet(cssPath)
expect($('head style').length).toBe lengthBefore + 1
$('head style[id*="css.css"]').remove()
it "synchronously loads and parses less files at the given path and installs a style tag for it in the head", ->
lessPath = project.resolve('sample.less')
lengthBefore = $('head style').length
requireStylesheet(lessPath)
expect($('head style').length).toBe lengthBefore + 1
element = $('head style[id*="sample.less"]')
expect(element.attr('id')).toBe lessPath
expect(element.text()).toBe """
#header {
color: #4d926f;
}
h2 {
color: #4d926f;
}
"""
# doesn't append twice
requireStylesheet(lessPath)
expect($('head style').length).toBe lengthBefore + 1
$('head style[id*="sample.less"]').remove()
describe ".disableStyleSheet(path)", ->
it "removes styling applied by given stylesheet path", ->
cssPath = require.resolve(fs.join("fixtures", "css.css"))
@@ -103,13 +144,13 @@ describe "Window", ->
it "unsubscribes from all buffers", ->
rootView.open('sample.js')
editor1 = rootView.getActiveEditor()
editor2 = editor1.splitRight()
expect(window.rootView.getEditors().length).toBe 2
buffer = rootView.getActivePaneItem().buffer
rootView.getActivePane().splitRight()
expect(window.rootView.find('.editor').length).toBe 2
window.shutdown()
expect(editor1.getBuffer().subscriptionCount()).toBe 0
expect(buffer.subscriptionCount()).toBe 0
it "only serializes window state the first time it is called", ->
deactivateSpy = spyOn(atom, "setRootViewStateForPath").andCallThrough()
@@ -129,3 +170,34 @@ describe "Window", ->
window.installAtomCommand(commandPath)
expect(fs.exists(commandPath)).toBeTruthy()
expect(fs.read(commandPath).length).toBeGreaterThan 1
describe ".deserialize(state)", ->
class Foo
@deserialize: ({name}) -> new Foo(name)
constructor: (@name) ->
beforeEach ->
registerDeserializer(Foo)
afterEach ->
unregisterDeserializer(Foo)
it "calls deserialize on the deserializer for the given state object, or returns undefined if one can't be found", ->
object = deserialize({ deserializer: 'Foo', name: 'Bar' })
expect(object.name).toBe 'Bar'
expect(deserialize({ deserializer: 'Bogus' })).toBeUndefined()
describe "when the deserializer has a version", ->
beforeEach ->
Foo.version = 2
describe "when the deserialized state has a matching version", ->
it "attempts to deserialize the state", ->
object = deserialize({ deserializer: 'Foo', version: 2, name: 'Bar' })
expect(object.name).toBe 'Bar'
describe "when the deserialized state has a non-matching version", ->
it "returns undefined", ->
expect(deserialize({ deserializer: 'Foo', version: 3, name: 'Bar' })).toBeUndefined()
expect(deserialize({ deserializer: 'Foo', version: 1, name: 'Bar' })).toBeUndefined()
expect(deserialize({ deserializer: 'Foo', name: 'Bar' })).toBeUndefined()

View File

@@ -0,0 +1,3 @@
## File.markdown
:cool:

View File

@@ -1,8 +1,15 @@
class Foo
registerDeserializer(this)
@deserialize: ({data}) -> new Foo(data)
constructor: (@data) ->
module.exports =
activateCallCount: 0
activationEventCallCount: 0
activate: ->
rootView.getActiveEditor()?.command 'activation-event', =>
@activateCallCount++
rootView.getActiveView()?.command 'activation-event', =>
@activationEventCallCount++
serialize: ->

View File

@@ -1,2 +1,3 @@
'activationEvents': ['activation-event']
'deferredDeserializers': ['Foo']
'main': 'main'

1
spec/fixtures/sample-with-error.less vendored Normal file
View File

@@ -0,0 +1 @@
#header {

8
spec/fixtures/sample.less vendored Normal file
View File

@@ -0,0 +1,8 @@
@color: #4D926F;
#header {
color: @color;
}
h2 {
color: @color;
}

View File

@@ -0,0 +1,5 @@
@padding: 4321px;
.editor {
padding-top: @padding;
}

View File

@@ -1,3 +1,3 @@
{
"stylesheets": ["first.css", "second.css", "last.css"]
"stylesheets": ["first.css", "second.less", "last.css"]
}

View File

@@ -1,5 +0,0 @@
.editor {
/* padding-top: 102px;*/
padding-right: 102px;
padding-bottom: 102px;
}

View File

@@ -0,0 +1,7 @@
@number: 102px;
.editor {
/* padding-top: 102px;*/
padding-right: @number;
padding-bottom: @number;
}

View File

@@ -1,3 +0,0 @@
.editor {
padding-bottom: 30px;
}

View File

@@ -0,0 +1,5 @@
@number: 30px;
.editor {
padding-bottom: @number;
}

View File

@@ -14,7 +14,8 @@ Editor = require 'editor'
TokenizedBuffer = require 'tokenized-buffer'
fs = require 'fs'
RootView = require 'root-view'
requireStylesheet "jasmine.css"
Git = require 'git'
requireStylesheet "jasmine.less"
fixturePackagesPath = require.resolve('fixtures/packages')
require.paths.unshift(fixturePackagesPath)
keymap.loadBundledKeymaps()
@@ -29,8 +30,12 @@ jasmine.getEnv().defaultTimeoutInterval = 5000
beforeEach ->
jQuery.fx.off = true
window.fixturesProject = new Project(require.resolve('fixtures'))
window.project = fixturesProject
window.project = new Project(require.resolve('fixtures'))
window.git = Git.open(project.getPath())
window.project.on 'path-changed', ->
window.git?.destroy()
window.git = Git.open(window.project.getPath())
window.resetTimeouts()
atom.atomPackageStates = {}
atom.loadedPackages = []
@@ -43,13 +48,14 @@ beforeEach ->
window.config = new Config()
spyOn(config, 'load')
spyOn(config, 'save')
config.set "editor.fontFamily", "Courier"
config.set "editor.fontSize", 16
config.set "editor.autoIndent", false
config.set "core.disabledPackages", ["package-that-throws-an-exception"]
# make editor display updates synchronous
spyOn(Editor.prototype, 'requestDisplayUpdate').andCallFake -> @updateDisplay()
spyOn(RootView.prototype, 'updateWindowTitle').andCallFake ->
spyOn(RootView.prototype, 'setTitle').andCallFake (@title) ->
spyOn(window, "setTimeout").andCallFake window.fakeSetTimeout
spyOn(window, "clearTimeout").andCallFake window.fakeClearTimeout
spyOn(File.prototype, "detectResurrectionAfterDelay").andCallFake -> @detectResurrection()
@@ -62,25 +68,34 @@ beforeEach ->
spyOn($native, 'writeToPasteboard').andCallFake (text) -> pasteboardContent = text
spyOn($native, 'readFromPasteboard').andCallFake -> pasteboardContent
addCustomMatchers(this)
afterEach ->
keymap.bindingSets = bindingSetsToRestore
keymap.bindingSetsByFirstKeystrokeToRestore = bindingSetsByFirstKeystrokeToRestore
if rootView?
rootView.deactivate()
rootView.deactivate?()
window.rootView = null
if project?
project.destroy()
window.project = null
if git?
git.destroy()
window.git = null
$('#jasmine-content').empty()
ensureNoPathSubscriptions()
atom.pendingModals = [[]]
atom.presentingModal = false
waits(0) # yield to ui thread to make screen update more frequently
window.loadPackage = (name, options) ->
window.loadPackage = (name, options={}) ->
Package = require 'package'
packagePath = _.find atom.getPackagePaths(), (packagePath) -> fs.base(packagePath) == name
if pack = Package.build(packagePath)
pack.load(options)
atom.loadedPackages.push(pack)
pack.deferActivation = false if options.activateImmediately
pack.activate()
pack
# Specs rely on TextMate bundles (but not atom packages)
@@ -110,6 +125,23 @@ jasmine.unspy = (object, methodName) ->
throw new Error("Not a spy") unless object[methodName].originalValue?
object[methodName] = object[methodName].originalValue
addCustomMatchers = (spec) ->
spec.addMatchers
toBeInstanceOf: (expected) ->
notText = if @isNot then " not" else ""
this.message = => "Expected #{jasmine.pp(@actual)} to#{notText} be instance of #{expected.name} class"
@actual instanceof expected
toHaveLength: (expected) ->
notText = if @isNot then " not" else ""
this.message = => "Expected object with length #{@actual.length} to#{notText} have length #{expected}"
@actual.length == expected
toExistOnDisk: (expected) ->
notText = this.isNot and " not" or ""
@message = -> return "Expected path '" + @actual + "'" + notText + " to exist."
fs.exists(@actual)
window.keyIdentifierForKey = (key) ->
if key.length > 1 # named key
key

View File

@@ -134,11 +134,11 @@ describe 'Child Processes', ->
waitsForPromise ->
options =
cwd: fixturesProject.getPath()
cwd: "/Applications"
stdout: (data) -> output.push(data)
stderr: (data) ->
ChildProcess.exec("pwd", options)
runs ->
expect(output.join('')).toBe "#{fixturesProject.getPath()}\n"
expect(output.join('')).toBe "/Applications\n"

View File

@@ -62,10 +62,14 @@ describe "CSON", ->
it "formats the undefined value as null", ->
expect(CSON.stringify(['a', undefined, 'b'])).toBe "[\n 'a'\n null\n 'b'\n]"
describe "when the array contains an object", ->
it "wraps the object in {}", ->
expect(CSON.stringify([{a:'b', a1: 'b1'}, {c: 'd'}])).toBe "[\n {\n 'a': 'b'\n 'a1': 'b1'\n }\n {\n 'c': 'd'\n }\n]"
describe "when formatting an object", ->
describe "when the object is empty", ->
it "returns the empty string", ->
expect(CSON.stringify({})).toBe ""
it "returns {}", ->
expect(CSON.stringify({})).toBe "{}"
it "returns formatted CSON", ->
expect(CSON.stringify(a: {b: 'c'})).toBe "'a':\n 'b': 'c'"

View File

@@ -86,7 +86,7 @@ describe "fs", ->
it "calls fn for every path in the tree at the given path", ->
paths = []
onPath = (path) ->
paths.push(fs.join(fixturesDir, path))
paths.push(path)
true
fs.traverseTree fixturesDir, onPath, onPath
expect(paths).toEqual fs.listTree(fixturesDir)
@@ -106,14 +106,16 @@ describe "fs", ->
expect(path).not.toMatch /\/dir\//
it "returns entries if path is a symlink", ->
symlinkPath = fs.join(fixturesDir, 'symlink-to-dir')
symlinkPaths = []
onSymlinkPath = (path) -> symlinkPaths.push(path)
onSymlinkPath = (path) -> symlinkPaths.push(path.substring(symlinkPath.length + 1))
regularPath = fs.join(fixturesDir, 'dir')
paths = []
onPath = (path) -> paths.push(path)
onPath = (path) -> paths.push(path.substring(regularPath.length + 1))
fs.traverseTree(fs.join(fixturesDir, 'symlink-to-dir'), onSymlinkPath, onSymlinkPath)
fs.traverseTree(fs.join(fixturesDir, 'dir'), onPath, onPath)
fs.traverseTree(symlinkPath, onSymlinkPath, onSymlinkPath)
fs.traverseTree(regularPath, onPath, onPath)
expect(symlinkPaths).toEqual(paths)

View File

@@ -1,4 +1,5 @@
$ = require 'jquery'
_ = require 'underscore'
{View, $$} = require 'space-pen'
describe 'jQuery extensions', ->
@@ -41,7 +42,7 @@ describe 'jQuery extensions', ->
element.trigger 'foo'
expect(events).toEqual [2,1,3]
describe "$.fn.events() and $.fn.document", ->
describe "$.fn.events() and $.fn.document(...)", ->
it "returns a list of all events being listened for on the target node or its ancestors, along with their documentation string", ->
view = $$ ->
@div id: 'a', =>
@@ -49,20 +50,18 @@ describe 'jQuery extensions', ->
@div id: 'c'
@div id: 'd'
view.document
'a1': "This is event A2"
'b2': "This is event b2"
view.document 'a1', "This is event A2"
view.document 'b2', "This is event b2"
view.document 'a1': "A1: Waste perfectly-good steak"
view.document 'a1', "A1: Waste perfectly-good steak"
view.on 'a1', ->
view.on 'a2', ->
view.on 'b1', -> # should not appear as a duplicate
divB = view.find('#b')
divB.document
'b1': "B1: Super-sonic bomber"
'b2': "B2: Looks evil. Kinda is."
divB.document 'b1', "B1: Super-sonic bomber"
divB.document 'b2', "B2: Looks evil. Kinda is."
divB.on 'b1', ->
divB.on 'b2', ->
@@ -76,6 +75,80 @@ describe 'jQuery extensions', ->
'a1': "A1: Waste perfectly-good steak"
'a2': null
describe "$.fn.command(eventName, [selector, options,] handler)", ->
[view, handler] = []
beforeEach ->
view = $$ ->
@div class: 'a', =>
@div class: 'b'
@div class: 'c'
handler = jasmine.createSpy("commandHandler")
it "binds the handler to the given event / selector for all argument combinations", ->
view.command 'test:foo', handler
view.trigger 'test:foo'
expect(handler).toHaveBeenCalled()
handler.reset()
view.command 'test:bar', '.b', handler
view.find('.b').trigger 'test:bar'
view.find('.c').trigger 'test:bar'
expect(handler.callCount).toBe 1
handler.reset()
view.command 'test:baz', doc: 'Spaz', handler
view.trigger 'test:baz'
expect(handler).toHaveBeenCalled()
handler.reset()
view.command 'test:quux', '.c', doc: 'Lorem', handler
view.find('.b').trigger 'test:quux'
view.find('.c').trigger 'test:quux'
expect(handler.callCount).toBe 1
it "passes the 'data' option through when binding the event handler", ->
view.command 'test:foo', data: "bar", handler
view.trigger 'test:foo'
expect(handler.argsForCall[0][0].data).toBe 'bar'
it "sets a custom docstring if the 'doc' option is specified", ->
view.command 'test:foo', doc: "Foo!", handler
expect(view.events()).toEqual 'test:foo': 'Test: Foo!'
it "capitalizes the 'github' prefix how we like it", ->
view.command 'github:spelling', handler
expect(view.events()).toEqual 'github:spelling': 'GitHub: Spelling'
describe "$.fn.scrollUp/Down/ToTop/ToBottom", ->
it "scrolls the element in the specified way if possible", ->
view = $$ -> @div => _.times 20, => @div('A')
view.css(height: 100, width: 100, overflow: 'scroll')
view.attachToDom()
view.scrollUp()
expect(view.scrollTop()).toBe 0
view.scrollDown()
expect(view.scrollTop()).toBeGreaterThan 0
previousScrollTop = view.scrollTop()
view.scrollDown()
expect(view.scrollTop()).toBeGreaterThan previousScrollTop
view.scrollToBottom()
expect(view.scrollTop()).toBe view.prop('scrollHeight') - 100
previousScrollTop = view.scrollTop()
view.scrollDown()
expect(view.scrollTop()).toBe previousScrollTop
view.scrollUp()
expect(view.scrollTop()).toBeLessThan previousScrollTop
previousScrollTop = view.scrollTop()
view.scrollUp()
expect(view.scrollTop()).toBeLessThan previousScrollTop
view.scrollToTop()
expect(view.scrollTop()).toBe 0
describe "Event.prototype", ->
class GrandchildView extends View
@content: -> @div class: 'grandchild'

View File

@@ -6,67 +6,22 @@ $ = require 'jquery'
module.exports =
class AtomPackage extends Package
metadata: null
packageMain: null
mainModule: null
deferActivation: false
load: ({activateImmediately}={}) ->
load: ->
try
@loadMetadata()
@loadKeymaps()
@loadStylesheets()
if @metadata.activationEvents and not activateImmediately
@subscribeToActivationEvents()
if @deferActivation = @metadata.activationEvents?
@registerDeferredDeserializers()
else
@activatePackageMain()
@requireMainModule()
catch e
console.warn "Failed to load package named '#{@name}'", e.stack
this
disableEventHandlersOnBubblePath: (event) ->
bubblePathEventHandlers = []
disabledHandler = ->
element = $(event.target)
while element.length
if eventHandlers = element.data('events')?[event.type]
for eventHandler in eventHandlers
eventHandler.disabledHandler = eventHandler.handler
eventHandler.handler = disabledHandler
bubblePathEventHandlers.push(eventHandler)
element = element.parent()
bubblePathEventHandlers
restoreEventHandlersOnBubblePath: (eventHandlers) ->
for eventHandler in eventHandlers
eventHandler.handler = eventHandler.disabledHandler
delete eventHandler.disabledHandler
unsubscribeFromActivationEvents: (activateHandler) ->
if _.isArray(@metadata.activationEvents)
rootView.off(event, activateHandler) for event in @metadata.activationEvents
else
rootView.off(event, selector, activateHandler) for event, selector of @metadata.activationEvents
subscribeToActivationEvents: () ->
activateHandler = (event) =>
bubblePathEventHandlers = @disableEventHandlersOnBubblePath(event)
@activatePackageMain()
$(event.target).trigger(event)
@restoreEventHandlersOnBubblePath(bubblePathEventHandlers)
@unsubscribeFromActivationEvents(activateHandler)
if _.isArray(@metadata.activationEvents)
rootView.command(event, activateHandler) for event in @metadata.activationEvents
else
rootView.command(event, selector, activateHandler) for event, selector of @metadata.activationEvents
activatePackageMain: ->
mainPath = @path
mainPath = fs.join(mainPath, @metadata.main) if @metadata.main
mainPath = require.resolve(mainPath)
if fs.isFile(mainPath)
@packageMain = require(mainPath)
config.setDefaults(@name, @packageMain.configDefaults)
atom.activateAtomPackage(this)
loadMetadata: ->
if metadataPath = fs.resolveExtension(fs.join(@path, 'package'), ['cson', 'json'])
@metadata = fs.readObject(metadataPath)
@@ -86,3 +41,65 @@ class AtomPackage extends Package
stylesheetDirPath = fs.join(@path, 'stylesheets')
for stylesheetPath in fs.list(stylesheetDirPath)
requireStylesheet(stylesheetPath)
activate: ->
if @deferActivation
@subscribeToActivationEvents()
else
try
if @requireMainModule()
config.setDefaults(@name, @mainModule.configDefaults)
atom.activateAtomPackage(this)
catch e
console.warn "Failed to activate package named '#{@name}'", e.stack
requireMainModule: ->
return @mainModule if @mainModule
mainPath = @path
mainPath = fs.join(mainPath, @metadata.main) if @metadata.main
mainPath = require.resolve(mainPath)
@mainModule = require(mainPath) if fs.isFile(mainPath)
registerDeferredDeserializers: ->
for deserializerName in @metadata.deferredDeserializers ? []
registerDeferredDeserializer deserializerName, => @requireMainModule()
subscribeToActivationEvents: () ->
return unless @metadata.activationEvents?
activateHandler = (event) =>
bubblePathEventHandlers = @disableEventHandlersOnBubblePath(event)
@deferActivation = false
@activate()
$(event.target).trigger(event)
@restoreEventHandlersOnBubblePath(bubblePathEventHandlers)
@unsubscribeFromActivationEvents(activateHandler)
if _.isArray(@metadata.activationEvents)
rootView.command(event, activateHandler) for event in @metadata.activationEvents
else
rootView.command(event, selector, activateHandler) for event, selector of @metadata.activationEvents
unsubscribeFromActivationEvents: (activateHandler) ->
if _.isArray(@metadata.activationEvents)
rootView.off(event, activateHandler) for event in @metadata.activationEvents
else
rootView.off(event, selector, activateHandler) for event, selector of @metadata.activationEvents
disableEventHandlersOnBubblePath: (event) ->
bubblePathEventHandlers = []
disabledHandler = ->
element = $(event.target)
while element.length
if eventHandlers = element.data('events')?[event.type]
for eventHandler in eventHandlers
eventHandler.disabledHandler = eventHandler.handler
eventHandler.handler = disabledHandler
bubblePathEventHandlers.push(eventHandler)
element = element.parent()
bubblePathEventHandlers
restoreEventHandlersOnBubblePath: (eventHandlers) ->
for eventHandler in eventHandlers
eventHandler.handler = eventHandler.disabledHandler
delete eventHandler.disabledHandler

View File

@@ -5,10 +5,10 @@ module.exports =
class AtomTheme extends Theme
loadStylesheet: (stylesheetPath)->
@stylesheets[stylesheetPath] = fs.read(stylesheetPath)
@stylesheets[stylesheetPath] = window.loadStylesheet(stylesheetPath)
load: ->
if fs.extension(@path) is '.css'
if fs.extension(@path) in ['.css', '.less']
@loadStylesheet(@path)
else
metadataPath = fs.resolveExtension(fs.join(@path, 'package'), ['cson', 'json'])
@@ -17,6 +17,6 @@ class AtomTheme extends Theme
if stylesheetNames
@loadStylesheet(fs.join(@path, name)) for name in stylesheetNames
else
@loadStylesheet(stylesheetPath) for stylesheetPath in fs.list(@path, ['.css'])
@loadStylesheet(stylesheetPath) for stylesheetPath in fs.list(@path, ['.css', '.less'])
super

View File

@@ -10,21 +10,24 @@ originalSendMessageToBrowserProcess = atom.sendMessageToBrowserProcess
_.extend atom,
exitWhenDone: window.location.params.exitWhenDone
devMode: window.location.params.devMode
loadedThemes: []
pendingBrowserProcessCallbacks: {}
loadedPackages: []
activatedAtomPackages: []
atomPackageStates: {}
presentingModal: false
pendingModals: [[]]
getPathToOpen: ->
@getWindowState('pathToOpen') ? window.location.params.pathToOpen
activateAtomPackage: (pack) ->
@activatedAtomPackages.push(pack)
pack.packageMain.activate(@atomPackageStates[pack.name] ? {})
pack.mainModule.activate(@atomPackageStates[pack.name] ? {})
deactivateAtomPackages: ->
pack.packageMain.deactivate?() for pack in @activatedAtomPackages
pack.mainModule.deactivate?() for pack in @activatedAtomPackages
@activatedAtomPackages = []
serializeAtomPackages: ->
@@ -32,7 +35,7 @@ _.extend atom,
for pack in @loadedPackages
if pack in @activatedAtomPackages
try
packageStates[pack.name] = pack.packageMain.serialize?()
packageStates[pack.name] = pack.mainModule.serialize?()
catch e
console?.error("Exception serializing '#{pack.name}' package's module\n", e.stack)
else
@@ -58,6 +61,9 @@ _.extend atom,
new LoadTextMatePackagesTask(textMatePackages).start() if textMatePackages.length > 0
activatePackages: ->
pack.activate() for pack in @loadedPackages
getLoadedPackages: ->
_.clone(@loadedPackages)
@@ -101,15 +107,50 @@ _.extend atom,
@sendMessageToBrowserProcess('newWindow', args)
confirm: (message, detailedMessage, buttonLabelsAndCallbacks...) ->
args = [message, detailedMessage]
callbacks = []
while buttonLabelsAndCallbacks.length
args.push(buttonLabelsAndCallbacks.shift())
callbacks.push(buttonLabelsAndCallbacks.shift())
@sendMessageToBrowserProcess('confirm', args, callbacks)
wrapCallback = (callback) => => @dismissModal(callback)
@presentModal =>
args = [message, detailedMessage]
callbacks = []
while buttonLabelsAndCallbacks.length
do =>
buttonLabel = buttonLabelsAndCallbacks.shift()
buttonCallback = buttonLabelsAndCallbacks.shift()
args.push(buttonLabel)
callbacks.push(=> @dismissModal(buttonCallback))
@sendMessageToBrowserProcess('confirm', args, callbacks)
showSaveDialog: (callback) ->
@sendMessageToBrowserProcess('showSaveDialog', [], callback)
@presentModal =>
@sendMessageToBrowserProcess('showSaveDialog', [], (path) => @dismissModal(callback, path))
presentModal: (fn) ->
if @presentingModal
@pushPendingModal(fn)
else
@presentingModal = true
fn()
dismissModal: (fn, args...) ->
@pendingModals.push([]) # prioritize any modals presented during dismiss callback
fn?(args...)
@presentingModal = false
if fn = @shiftPendingModal()
_.delay (=> @presentModal(fn)), 50 # let view update before next dialog
pushPendingModal: (fn) ->
# pendingModals is a stack of queues. enqueue to top of stack.
stackSize = @pendingModals.length
@pendingModals[stackSize - 1].push(fn)
shiftPendingModal: ->
# pop pendingModals stack if its top queue is empty, otherwise shift off the topmost queue
stackSize = @pendingModals.length
currentQueueSize = @pendingModals[stackSize - 1].length
if stackSize > 1 and currentQueueSize == 0
@pendingModals.pop()
@shiftPendingModal()
else
@pendingModals[stackSize - 1].shift()
toggleDevTools: ->
@sendMessageToBrowserProcess('toggleDevTools')

View File

@@ -73,7 +73,7 @@ class BufferChangeOperation
event = { oldRange, newRange, oldText, newText }
@updateMarkers(event)
@buffer.trigger 'changed', event
@buffer.scheduleStoppedChangingEvent()
@buffer.scheduleModifiedEvents()
@resumeMarkerObservation()
@buffer.trigger 'markers-updated'

View File

@@ -69,7 +69,7 @@ class Buffer
@file.on "removed", =>
@updateCachedDiskContents()
@trigger "contents-modified", {differsFromDisk: true}
@triggerModifiedStatusChanged(@isModified())
@file.on "moved", =>
@trigger "path-changed", this
@@ -78,6 +78,7 @@ class Buffer
@trigger 'will-reload'
@updateCachedDiskContents()
@setText(@cachedDiskContents)
@triggerModifiedStatusChanged(false)
@trigger 'reloaded'
updateCachedDiskContents: ->
@@ -252,6 +253,7 @@ class Buffer
@setPath(path)
@cachedDiskContents = @getText()
@file.write(@getText())
@triggerModifiedStatusChanged(false)
@trigger 'saved'
isModified: ->
@@ -419,21 +421,25 @@ class Buffer
return match[0][0] != '\t'
undefined
getRepo: -> @project?.repo
checkoutHead: ->
path = @getPath()
return unless path
if @getRepo()?.checkoutHead(path)
@trigger 'git-status-changed'
git?.checkoutHead(path)
scheduleStoppedChangingEvent: ->
scheduleModifiedEvents: ->
clearTimeout(@stoppedChangingTimeout) if @stoppedChangingTimeout
stoppedChangingCallback = =>
@stoppedChangingTimeout = null
@trigger 'contents-modified', {differsFromDisk: @isModified()}
modifiedStatus = @isModified()
@trigger 'contents-modified', modifiedStatus
@triggerModifiedStatusChanged(modifiedStatus)
@stoppedChangingTimeout = setTimeout(stoppedChangingCallback, @stoppedChangingDelay)
triggerModifiedStatusChanged: (modifiedStatus) ->
return if modifiedStatus is @previousModifiedStatus
@previousModifiedStatus = modifiedStatus
@trigger 'modified-status-changed', modifiedStatus
fileExists: ->
@file.exists()

View File

@@ -20,6 +20,7 @@ class Config
userPackagesDirPath: userPackagesDirPath
defaultSettings: null
settings: null
configFileHasErrors: null
constructor: ->
@defaultSettings =
@@ -37,16 +38,16 @@ class Config
templateConfigDirPath = fs.resolve(window.resourcePath, 'dot-atom')
onConfigDirFile = (path) =>
templatePath = fs.join(templateConfigDirPath, path)
configPath = fs.join(@configDirPath, path)
fs.write(configPath, fs.read(templatePath))
relativePath = path.substring(templateConfigDirPath.length + 1)
configPath = fs.join(@configDirPath, relativePath)
fs.write(configPath, fs.read(path))
fs.traverseTree(templateConfigDirPath, onConfigDirFile, (path) -> true)
configThemeDirPath = fs.join(@configDirPath, 'themes')
onThemeDirFile = (path) ->
templatePath = fs.join(bundledThemesDirPath, path)
configPath = fs.join(configThemeDirPath, path)
fs.write(configPath, fs.read(templatePath))
relativePath = path.substring(bundledThemesDirPath.length + 1)
configPath = fs.join(configThemeDirPath, relativePath)
fs.write(configPath, fs.read(path))
fs.traverseTree(bundledThemesDirPath, onThemeDirFile, (path) -> true)
load: ->
@@ -55,8 +56,13 @@ class Config
loadUserConfig: ->
if fs.exists(@configFilePath)
userConfig = fs.readObject(@configFilePath)
_.extend(@settings, userConfig)
try
userConfig = fs.readObject(@configFilePath)
_.extend(@settings, userConfig)
catch e
@configFileHasErrors = true
console.error "Failed to load user config '#{@configFilePath}'", e.message
console.error e.stack
get: (keyPath) ->
_.valueForKeyPath(@settings, keyPath) ?
@@ -92,6 +98,7 @@ class Config
subscription
update: ->
return if @configFileHasErrors
@save()
@trigger 'updated'

View File

@@ -14,17 +14,22 @@ module.exports =
class EditSession
registerDeserializer(this)
@deserialize: (state, project) ->
@deserialize: (state) ->
if fs.exists(state.buffer)
session = project.buildEditSessionForPath(state.buffer)
session = project.buildEditSession(state.buffer)
else
console.warn "Could not build edit session for path '#{state.buffer}' because that file no longer exists" if state.buffer
session = project.buildEditSessionForPath(null)
session = project.buildEditSession(null)
session.setScrollTop(state.scrollTop)
session.setScrollLeft(state.scrollLeft)
session.setCursorScreenPosition(state.cursorScreenPosition)
session
@identifiedBy: 'path'
@deserializesToSameObject: (state, editSession) ->
state.path
scrollTop: 0
scrollLeft: 0
languageMode: null
@@ -43,17 +48,40 @@ class EditSession
@addCursorAtScreenPosition([0, 0])
@buffer.retain()
@subscribe @buffer, "path-changed", => @trigger "path-changed"
@subscribe @buffer, "path-changed", =>
@project.setPath(fs.directory(@getPath())) unless @project.getPath()?
@trigger "title-changed"
@trigger "path-changed"
@subscribe @buffer, "contents-conflicted", => @trigger "contents-conflicted"
@subscribe @buffer, "markers-updated", => @mergeCursors()
@subscribe @buffer, "modified-status-changed", => @trigger "modified-status-changed"
@preserveCursorPositionOnBufferReload()
@subscribe @displayBuffer, "changed", (e) =>
@trigger 'screen-lines-changed', e
@subscribe syntax, 'grammars-loaded', => @reloadGrammar()
getViewClass: ->
require 'editor'
getTitle: ->
if path = @getPath()
fs.base(path)
else
'untitled'
getLongTitle: ->
if path = @getPath()
fileName = fs.base(path)
directory = fs.base(fs.directory(path))
"#{fileName} - #{directory}"
else
'untitled'
destroy: ->
throw new Error("Edit session already destroyed") if @destroyed
return if @destroyed
@destroyed = true
@unsubscribe()
@buffer.release()
@@ -130,6 +158,7 @@ class EditSession
saveAs: (path) -> @buffer.saveAs(path)
getFileExtension: -> @buffer.getExtension()
getPath: -> @buffer.getPath()
getUri: -> @getPath()
isBufferRowBlank: (bufferRow) -> @buffer.isRowBlank(bufferRow)
nextNonBlankBufferRow: (bufferRow) -> @buffer.nextNonBlankRow(bufferRow)
getEofBufferPosition: -> @buffer.getEofPosition()
@@ -814,11 +843,10 @@ class EditSession
getGrammar: -> @languageMode.grammar
reloadGrammar: ->
grammarChanged = @languageMode.reloadGrammar()
if grammarChanged
if @languageMode.reloadGrammar()
@unfoldAll()
@displayBuffer.tokenizedBuffer.resetScreenLines()
grammarChanged
true
getDebugSnapshot: ->
[

View File

@@ -16,7 +16,6 @@ class Editor extends View
fontSize: 20
showInvisibles: false
showIndentGuide: false
autosave: false
autoIndent: true
autoIndentOnPaste: false
nonWordCharacters: "./\\()\"':,.;<>~!@#$%^&*|+=[]{}`~?-"
@@ -49,8 +48,6 @@ class Editor extends View
lineCache: null
isFocused: false
activeEditSession: null
closedEditSessions: null
editSessions: null
attached: false
lineOverdraw: 10
pendingChanges: null
@@ -58,16 +55,13 @@ class Editor extends View
newSelections: null
redrawOnReattach: false
@deserialize: (state) ->
editor = new Editor(mini: state.mini, deserializing: true)
editSessions = state.editSessions.map (state) -> EditSession.deserialize(state, project)
editor.pushEditSession(editSession) for editSession in editSessions
editor.setActiveEditSessionIndex(state.activeEditSessionIndex)
editor.isFocused = state.isFocused
editor
initialize: (editSessionOrOptions) ->
if editSessionOrOptions instanceof EditSession
editSession = editSessionOrOptions
else
{editSession, @mini} = (editSessionOrOptions ? {})
initialize: ({editSession, @mini, deserializing} = {}) ->
requireStylesheet 'editor.css'
requireStylesheet 'editor.less'
@id = Editor.nextEditorId++
@lineCache = []
@@ -76,8 +70,6 @@ class Editor extends View
@handleEvents()
@cursorViews = []
@selectionViews = []
@editSessions = []
@closedEditSessions = []
@pendingChanges = []
@newCursors = []
@newSelections = []
@@ -91,18 +83,8 @@ class Editor extends View
tabLength: 2
softTabs: true
)
else if not deserializing
throw new Error("Editor must be constructed with an 'editSession' or 'mini: true' param")
serialize: ->
@saveScrollPositionForActiveEditSession()
deserializer: "Editor"
editSessions: @editSessions.map (session) -> session.serialize()
activeEditSessionIndex: @getActiveEditSessionIndex()
isFocused: @isFocused
copy: ->
Editor.deserialize(@serialize(), rootView)
else
throw new Error("Must supply an EditSession or mini: true")
bindKeys: ->
editorBindings =
@@ -155,9 +137,6 @@ class Editor extends View
'core:select-down': @selectDown
'core:select-to-top': @selectToTop
'core:select-to-bottom': @selectToBottom
'core:close': @destroyActiveEditSession
'editor:save': @save
'editor:save-as': @saveAs
'editor:newline-below': @insertNewlineBelow
'editor:newline-above': @insertNewlineAbove
'editor:toggle-soft-tabs': @toggleSoftTabs
@@ -167,32 +146,14 @@ class Editor extends View
'editor:fold-current-row': @foldCurrentRow
'editor:unfold-current-row': @unfoldCurrentRow
'editor:fold-selection': @foldSelection
'editor:split-left': @splitLeft
'editor:split-right': @splitRight
'editor:split-up': @splitUp
'editor:split-down': @splitDown
'editor:show-next-buffer': @loadNextEditSession
'editor:show-buffer-1': => @setActiveEditSessionIndex(0) if @editSessions[0]
'editor:show-buffer-2': => @setActiveEditSessionIndex(1) if @editSessions[1]
'editor:show-buffer-3': => @setActiveEditSessionIndex(2) if @editSessions[2]
'editor:show-buffer-4': => @setActiveEditSessionIndex(3) if @editSessions[3]
'editor:show-buffer-5': => @setActiveEditSessionIndex(4) if @editSessions[4]
'editor:show-buffer-6': => @setActiveEditSessionIndex(5) if @editSessions[5]
'editor:show-buffer-7': => @setActiveEditSessionIndex(6) if @editSessions[6]
'editor:show-buffer-8': => @setActiveEditSessionIndex(7) if @editSessions[7]
'editor:show-buffer-9': => @setActiveEditSessionIndex(8) if @editSessions[8]
'editor:show-previous-buffer': @loadPreviousEditSession
'editor:toggle-line-comments': @toggleLineCommentsInSelection
'editor:log-cursor-scope': @logCursorScope
'editor:checkout-head-revision': @checkoutHead
'editor:close-other-edit-sessions': @destroyInactiveEditSessions
'editor:close-all-edit-sessions': @destroyAllEditSessions
'editor:select-grammar': @selectGrammar
'editor:copy-path': @copyPathToPasteboard
'editor:move-line-up': @moveLineUp
'editor:move-line-down': @moveLineDown
'editor:duplicate-line': @duplicateLine
'editor:undo-close-session': @undoDestroySession
'editor:toggle-indent-guide': => config.set('editor.showIndentGuide', !config.get('editor.showIndentGuide'))
'editor:save-debug-snapshot': @saveDebugSnapshot
@@ -343,7 +304,7 @@ class Editor extends View
checkoutHead: -> @getBuffer().checkoutHead()
setText: (text) -> @getBuffer().setText(text)
getText: -> @getBuffer().getText()
getPath: -> @getBuffer().getPath()
getPath: -> @activeEditSession?.getPath()
getLineCount: -> @getBuffer().getLineCount()
getLastBufferRow: -> @getBuffer().getLastRow()
getTextInRange: (range) -> @getBuffer().getTextInRange(range)
@@ -367,13 +328,11 @@ class Editor extends View
false
@hiddenInput.on 'focus', =>
rootView?.editorFocused(this)
@isFocused = true
@addClass 'is-focused'
@hiddenInput.on 'focusout', =>
@isFocused = false
@autosave() if config.get "editor.autosave"
@removeClass 'is-focused'
@underlayer.on 'click', (e) =>
@@ -436,10 +395,7 @@ class Editor extends View
e.pageX = @renderedLines.offset().left
onMouseDown(e)
@subscribe syntax, 'grammars-loaded', =>
@reloadGrammar()
for session in @editSessions
session.reloadGrammar() unless session is @activeEditSession
@subscribe syntax, 'grammars-loaded', => @reloadGrammar()
@scrollView.on 'scroll', =>
if @scrollView.scrollLeft() == 0
@@ -481,86 +437,16 @@ class Editor extends View
@trigger 'editor:attached', [this]
edit: (editSession) ->
index = @editSessions.indexOf(editSession)
index = @pushEditSession(editSession) if index == -1
@setActiveEditSessionIndex(index)
pushEditSession: (editSession) ->
index = @editSessions.length
@editSessions.push(editSession)
@closedEditSessions = @closedEditSessions.filter ({path})->
path isnt editSession.getPath()
editSession.on 'destroyed', => @editSessionDestroyed(editSession)
@trigger 'editor:edit-session-added', [editSession, index]
index
getBuffer: -> @activeEditSession.buffer
undoDestroySession: ->
return unless @closedEditSessions.length > 0
{path, index} = @closedEditSessions.pop()
rootView.open(path)
activeIndex = @getActiveEditSessionIndex()
@moveEditSessionToIndex(activeIndex, index) if index < activeIndex
destroyActiveEditSession: ->
@destroyEditSessionIndex(@getActiveEditSessionIndex())
destroyEditSessionIndex: (index, callback) ->
return if @mini
editSession = @editSessions[index]
destroySession = =>
path = editSession.getPath()
@closedEditSessions.push({path, index}) if path
editSession.destroy()
callback?(index)
if editSession.isModified() and not editSession.hasEditors()
@promptToSaveDirtySession(editSession, destroySession)
else
destroySession()
destroyInactiveEditSessions: ->
destroyIndex = (index) =>
index++ if index is @getActiveEditSessionIndex()
@destroyEditSessionIndex(index, destroyIndex) if @editSessions[index]
destroyIndex(0)
destroyAllEditSessions: ->
destroyIndex = (index) =>
@destroyEditSessionIndex(index, destroyIndex) if @editSessions[index]
destroyIndex(0)
editSessionDestroyed: (editSession) ->
index = @editSessions.indexOf(editSession)
@loadPreviousEditSession() if index is @getActiveEditSessionIndex() and @editSessions.length > 1
_.remove(@editSessions, editSession)
@trigger 'editor:edit-session-removed', [editSession, index]
@remove() if @editSessions.length is 0
loadNextEditSession: ->
nextIndex = (@getActiveEditSessionIndex() + 1) % @editSessions.length
@setActiveEditSessionIndex(nextIndex)
loadPreviousEditSession: ->
previousIndex = @getActiveEditSessionIndex() - 1
previousIndex = @editSessions.length - 1 if previousIndex < 0
@setActiveEditSessionIndex(previousIndex)
getActiveEditSessionIndex: ->
return index for session, index in @editSessions when session == @activeEditSession
setActiveEditSessionIndex: (index) ->
throw new Error("Edit session not found") unless @editSessions[index]
return if editSession is @activeEditSession
if @activeEditSession
@autosave() if config.get "editor.autosave"
@saveScrollPositionForActiveEditSession()
@activeEditSession.off(".editor")
@activeEditSession = @editSessions[index]
@activeEditSession = editSession
return unless @activeEditSession?
@activeEditSession.setVisible(true)
@activeEditSession.on "contents-conflicted.editor", =>
@@ -571,11 +457,18 @@ class Editor extends View
@trigger 'editor:path-changed'
@trigger 'editor:path-changed'
@trigger 'editor:active-edit-session-changed', [@activeEditSession, index]
@resetDisplay()
if @attached and @activeEditSession.buffer.isInConflict()
setTimeout(( =>@showBufferConflictAlert(@activeEditSession)), 0) # Display after editSession has a chance to display
_.defer => @showBufferConflictAlert(@activeEditSession) # Display after editSession has a chance to display
getModel: ->
@activeEditSession
setModel: (editSession) ->
@edit(editSession)
getBuffer: -> @activeEditSession.buffer
showBufferConflictAlert: (editSession) ->
atom.confirm(
@@ -585,30 +478,6 @@ class Editor extends View
"Cancel"
)
moveEditSessionToIndex: (fromIndex, toIndex) ->
return if fromIndex is toIndex
editSession = @editSessions.splice(fromIndex, 1)
@editSessions.splice(toIndex, 0, editSession[0])
@trigger 'editor:edit-session-order-changed', [editSession, fromIndex, toIndex]
@setActiveEditSessionIndex(toIndex)
moveEditSessionToEditor: (fromIndex, toEditor, toIndex) ->
fromEditSession = @editSessions[fromIndex]
toEditSession = fromEditSession.copy()
@destroyEditSessionIndex(fromIndex)
toEditor.edit(toEditSession)
toEditor.moveEditSessionToIndex(toEditor.getActiveEditSessionIndex(), toIndex)
activateEditSessionForPath: (path) ->
for editSession, index in @editSessions
if editSession.buffer.getPath() == path
@setActiveEditSessionIndex(index)
return @activeEditSession
false
getOpenBufferPaths: ->
editSession.buffer.getPath() for editSession in @editSessions when editSession.buffer.getPath()?
scrollTop: (scrollTop, options={}) ->
return @cachedScrollTop or 0 unless scrollTop?
maxScrollTop = @verticalScrollbar.prop('scrollHeight') - @verticalScrollbar.height()
@@ -723,22 +592,6 @@ class Editor extends View
@removeClass 'soft-wrap'
$(window).off 'resize', @_setSoftWrapColumn
save: (session=@activeEditSession, onSuccess) ->
if @getPath()
session.save()
onSuccess?()
else
@saveAs(session, onSuccess)
saveAs: (session=@activeEditSession, onSuccess) ->
atom.showSaveDialog (path) =>
if path
session.saveAs(path)
onSuccess?()
autosave: ->
@save() if @getPath()?
setFontSize: (fontSize) ->
headTag = $("head")
styleTag = headTag.find("style.font-size")
@@ -781,54 +634,33 @@ class Editor extends View
@updateLayerDimensions()
@requestDisplayUpdate()
newSplitEditor: (editSession) ->
new Editor { editSession: editSession ? @activeEditSession.copy() }
splitLeft: (items...) ->
@pane()?.splitLeft(items...).activeView
splitLeft: (editSession) ->
@pane()?.splitLeft(@newSplitEditor(editSession)).wrappedView
splitRight: (items...) ->
@pane()?.splitRight(items...).activeView
splitRight: (editSession) ->
@pane()?.splitRight(@newSplitEditor(editSession)).wrappedView
splitUp: (items...) ->
@pane()?.splitUp(items...).activeView
splitUp: (editSession) ->
@pane()?.splitUp(@newSplitEditor(editSession)).wrappedView
splitDown: (editSession) ->
@pane()?.splitDown(@newSplitEditor(editSession)).wrappedView
splitDown: (items...) ->
@pane()?.splitDown(items...).activeView
pane: ->
@parent('.pane').view()
promptToSaveDirtySession: (session, callback) ->
path = session.getPath()
filename = if path then fs.base(path) else "untitled buffer"
atom.confirm(
"'#{filename}' has changes, do you want to save them?"
"Your changes will be lost if you don't save them"
"Save", => @save(session, callback),
"Cancel", null
"Don't Save", callback
)
@closest('.pane').view()
remove: (selector, keepData) ->
return super if keepData or @removed
@trigger 'editor:will-be-removed'
if @pane() then @pane().remove() else super
super
rootView?.focus()
afterRemove: ->
@removed = true
@destroyEditSessions()
@activeEditSession?.destroy()
$(window).off(".editor-#{@id}")
$(document).off(".editor-#{@id}")
getEditSessions: ->
new Array(@editSessions...)
destroyEditSessions: ->
for session in @getEditSessions()
session.destroy()
getCursorView: (index) ->
index ?= @cursorViews.length - 1
@cursorViews[index]
@@ -933,7 +765,8 @@ class Editor extends View
@pendingDisplayUpdate = false
updateDisplay: (options={}) ->
return unless @attached
return unless @attached and @activeEditSession
return if @activeEditSession.destroyed
@updateRenderedLines()
@highlightCursorLine()
@updateCursorViews()

View File

@@ -83,4 +83,3 @@ module.exports =
for name, handlers of @eventHandlersByEventName
count += handlers.length
count

View File

@@ -1,11 +1,14 @@
_ = require 'underscore'
fs = require 'fs'
Subscriber = require 'subscriber'
EventEmitter = require 'event-emitter'
GitRepository = require 'git-repository'
RepositoryStatusTask = require 'repository-status-task'
module.exports =
class Git
@open: (path, options) ->
return null unless path
try
new Git(path, options)
catch e
@@ -23,12 +26,27 @@ class Git
working_dir_typechange: 1 << 10
ignore: 1 << 14
statuses: null
upstream: null
statusTask: null
constructor: (path, options={}) ->
@statuses = {}
@upstream = {ahead: 0, behind: 0}
@repo = GitRepository.open(path)
refreshIndexOnFocus = options.refreshIndexOnFocus ? true
if refreshIndexOnFocus
refreshOnWindowFocus = options.refreshOnWindowFocus ? true
if refreshOnWindowFocus
$ = require 'jquery'
@subscribe $(window), 'focus', => @refreshIndex()
@subscribe $(window), 'focus', =>
@refreshIndex()
@refreshStatus()
project?.eachBuffer this, (buffer) =>
bufferStatusHandler = =>
path = buffer.getPath()
@getPathStatus(path) if path
@subscribe buffer, 'saved', bufferStatusHandler
@subscribe buffer, 'reloaded', bufferStatusHandler
getRepo: ->
unless @repo?
@@ -37,27 +55,40 @@ class Git
refreshIndex: -> @getRepo().refreshIndex()
getPath: -> @getRepo().getPath()
getPath: ->
@path ?= fs.absolute(@getRepo().getPath())
destroy: ->
if @statusTask?
@statusTask.abort()
@statusTask.off()
@statusTask = null
@getRepo().destroy()
@repo = null
@unsubscribe()
getWorkingDirectory: ->
repoPath = @getPath()
repoPath?.substring(0, repoPath.length - 6)
@getPath()?.replace(/\/\.git\/?$/, '')
getHead: ->
@getRepo().getHead() ? ''
getPathStatus: (path) ->
pathStatus = @getRepo().getStatus(@relativize(path))
currentPathStatus = @statuses[path] ? 0
pathStatus = @getRepo().getStatus(@relativize(path)) ? 0
if pathStatus > 0
@statuses[path] = pathStatus
else
delete @statuses[path]
if currentPathStatus isnt pathStatus
@trigger 'status-changed', path, pathStatus
pathStatus
isPathIgnored: (path) ->
@getRepo().isIgnored(@relativize(path))
isStatusModified: (status) ->
isStatusModified: (status=0) ->
modifiedFlags = @statusFlags.working_dir_modified |
@statusFlags.working_dir_delete |
@statusFlags.working_dir_typechange |
@@ -69,7 +100,7 @@ class Git
isPathModified: (path) ->
@isStatusModified(@getPathStatus(path))
isStatusNew: (status) ->
isStatusNew: (status=0) ->
newFlags = @statusFlags.working_dir_new |
@statusFlags.index_new
(status & newFlags) > 0
@@ -93,7 +124,9 @@ class Git
return head
checkoutHead: (path) ->
@getRepo().checkoutHead(@relativize(path))
headCheckedOut = @getRepo().checkoutHead(@relativize(path))
@getPathStatus(path) if headCheckedOut
headCheckedOut
getDiffStats: (path) ->
@getRepo().getDiffStats(@relativize(path)) ? added: 0, deleted: 0
@@ -101,4 +134,30 @@ class Git
isSubmodule: (path) ->
@getRepo().isSubmodule(@relativize(path))
refreshStatus: ->
if @statusTask?
@statusTask.off()
@statusTask.one 'task-completed', =>
@statusTask = null
@refreshStatus()
else
@statusTask = new RepositoryStatusTask(this)
@statusTask.one 'task-completed', =>
@statusTask = null
@statusTask.start()
getDirectoryStatus: (directoryPath) ->
directoryPath = "#{directoryPath}/"
directoryStatus = 0
for path, status of @statuses
directoryStatus |= status if path.indexOf(directoryPath) is 0
directoryStatus
getAheadBehindCounts: ->
@getRepo().getAheadBehindCounts() ? ahead: 0, behind: 0
getLineDiffs: (path, text) ->
@getRepo().getLineDiffs(@relativize(path), text) ? []
_.extend Git.prototype, Subscriber
_.extend Git.prototype, EventEmitter

View File

@@ -1,9 +1,5 @@
{View, $$, $$$} = require 'space-pen'
$ = require 'jquery'
_ = require 'underscore'
Range = require 'range'
Point = require 'point'
module.exports =
class Gutter extends View

View File

@@ -1,4 +1,6 @@
'body':
'meta-s': 'core:save'
'meta-S': 'core:save-as'
'enter': 'core:confirm'
'escape': 'core:cancel'
'meta-w': 'core:close'
@@ -30,6 +32,26 @@
'ctrl-tab': 'window:focus-next-pane'
'ctrl-meta-f': 'window:toggle-full-screen'
'ctrl-|': 'pane:split-right'
'ctrl-w v': 'pane:split-right'
'ctrl--': 'pane:split-down'
'ctrl-w s': 'pane:split-down'
'meta-{': 'pane:show-previous-item'
'meta-}': 'pane:show-next-item'
'alt-meta-left': 'pane:show-previous-item'
'alt-meta-right': 'pane:show-next-item'
'meta-1': 'pane:show-item-1'
'meta-2': 'pane:show-item-2'
'meta-3': 'pane:show-item-3'
'meta-4': 'pane:show-item-4'
'meta-5': 'pane:show-item-5'
'meta-6': 'pane:show-item-6'
'meta-7': 'pane:show-item-7'
'meta-8': 'pane:show-item-8'
'meta-9': 'pane:show-item-9'
'meta-T': 'pane:reopen-closed-item'
'.tool-panel':
'meta-escape': 'tool-panel:unfocus'
'escape': 'core:close'

View File

@@ -1,9 +1,4 @@
'body':
'meta-T': 'editor:undo-close-session'
'.editor':
'meta-s': 'editor:save'
'meta-S': 'editor:save-as'
'enter': 'editor:newline'
'meta-enter': 'editor:newline-below'
'meta-shift-enter': 'editor:newline-above'
@@ -15,26 +10,9 @@
'ctrl-{': 'editor:fold-all'
'ctrl-}': 'editor:unfold-all'
'alt-meta-ctrl-f': 'editor:fold-selection'
'ctrl-|': 'editor:split-right'
'ctrl-w v': 'editor:split-right'
'ctrl--': 'editor:split-down'
'ctrl-w s': 'editor:split-down'
'shift-tab': 'editor:outdent-selected-rows'
'meta-[': 'editor:outdent-selected-rows'
'meta-]': 'editor:indent-selected-rows'
'meta-{': 'editor:show-previous-buffer'
'meta-}': 'editor:show-next-buffer'
'alt-meta-left': 'editor:show-previous-buffer'
'alt-meta-right': 'editor:show-next-buffer'
'meta-1': 'editor:show-buffer-1'
'meta-2': 'editor:show-buffer-2'
'meta-3': 'editor:show-buffer-3'
'meta-4': 'editor:show-buffer-4'
'meta-5': 'editor:show-buffer-5'
'meta-6': 'editor:show-buffer-6'
'meta-7': 'editor:show-buffer-7'
'meta-8': 'editor:show-buffer-8'
'meta-9': 'editor:show-buffer-9'
'meta-/': 'editor:toggle-line-comments'
'ctrl-W': 'editor:select-word'
'meta-alt-p': 'editor:log-cursor-scope'

View File

@@ -2,7 +2,7 @@ $ = require 'jquery'
{View} = require 'space-pen'
module.exports =
class PaneGrid extends View
class PaneAxis extends View
@deserialize: ({children}) ->
childViews = children.map (child) -> deserialize(child)
new this(childViews)

View File

@@ -1,9 +1,9 @@
$ = require 'jquery'
_ = require 'underscore'
PaneGrid = require 'pane-grid'
PaneAxis = require 'pane-axis'
module.exports =
class PaneColumn extends PaneGrid
class PaneColumn extends PaneAxis
@content: ->
@div class: 'column'

View File

@@ -0,0 +1,119 @@
{View} = require 'space-pen'
Pane = require 'pane'
$ = require 'jquery'
module.exports =
class PaneContainer extends View
registerDeserializer(this)
@deserialize: ({root}) ->
container = new PaneContainer
container.append(deserialize(root)) if root
container.removeEmptyPanes()
container
@content: ->
@div id: 'panes'
initialize: ->
@destroyedItemStates = []
serialize: ->
deserializer: 'PaneContainer'
root: @getRoot()?.serialize()
focusNextPane: ->
panes = @getPanes()
if panes.length > 1
currentIndex = panes.indexOf(@getFocusedPane())
nextIndex = (currentIndex + 1) % panes.length
panes[nextIndex].focus()
true
else
false
makeNextPaneActive: ->
panes = @getPanes()
currentIndex = panes.indexOf(@getActivePane())
nextIndex = (currentIndex + 1) % panes.length
panes[nextIndex].makeActive()
reopenItem: ->
if lastItemState = @destroyedItemStates.pop()
if activePane = @getActivePane()
activePane.showItem(deserialize(lastItemState))
true
else
@append(new Pane(deserialize(lastItemState)))
itemDestroyed: (item) ->
state = item.serialize?()
state.uri ?= item.getUri?()
@destroyedItemStates.push(state) if state?
itemAdded: (item) ->
itemUri = item.getUri?()
@destroyedItemStates = @destroyedItemStates.filter (itemState) ->
itemState.uri isnt itemUri
getRoot: ->
@children().first().view()
saveAll: ->
pane.saveItems() for pane in @getPanes()
confirmClose: ->
deferred = $.Deferred()
modifiedItems = []
for pane in @getPanes()
modifiedItems.push(item) for item in pane.getItems() when item.isModified?()
cancel = => deferred.reject()
saveNextModifiedItem = =>
if modifiedItems.length == 0
deferred.resolve()
else
item = modifiedItems.pop()
@paneAtIndex(0).promptToSaveItem item, saveNextModifiedItem, cancel
saveNextModifiedItem()
deferred.promise()
getPanes: ->
@find('.pane').views()
indexOfPane: (pane) ->
@getPanes().indexOf(pane.view())
paneAtIndex: (index) ->
@getPanes()[index]
eachPane: (callback) ->
callback(pane) for pane in @getPanes()
paneAttached = (e) -> callback($(e.target).view())
@on 'pane:attached', paneAttached
cancel: => @off 'pane:attached', paneAttached
getFocusedPane: ->
@find('.pane:has(:focus)').view()
getActivePane: ->
@find('.pane.active').view() ? @find('.pane:first').view()
getActivePaneItem: ->
@getActivePane()?.activeItem
getActiveView: ->
@getActivePane()?.activeView
adjustPaneDimensions: ->
if root = @getRoot()
root.css(width: '100%', height: '100%', top: 0, left: 0)
root.adjustDimensions()
removeEmptyPanes: ->
for pane in @getPanes() when pane.getItems().length == 0
pane.remove()
afterAttach: ->
@adjustPaneDimensions()

View File

@@ -1,9 +1,9 @@
$ = require 'jquery'
_ = require 'underscore'
PaneGrid = require 'pane-grid'
PaneAxis = require 'pane-axis'
module.exports =
class PaneRow extends PaneGrid
class PaneRow extends PaneAxis
@content: ->
@div class: 'row'

View File

@@ -1,4 +1,6 @@
{View} = require 'space-pen'
$ = require 'jquery'
_ = require 'underscore'
PaneRow = require 'pane-row'
PaneColumn = require 'pane-column'
@@ -6,58 +8,321 @@ module.exports =
class Pane extends View
@content: (wrappedView) ->
@div class: 'pane', =>
@subview 'wrappedView', wrappedView if wrappedView
@div class: 'item-views', outlet: 'itemViews'
@deserialize: ({wrappedView}) ->
new Pane(deserialize(wrappedView))
@deserialize: ({items, focused, activeItemUri}) ->
deserializedItems = _.compact(items.map((item) -> deserialize(item)))
pane = new Pane(deserializedItems...)
pane.showItemForUri(activeItemUri) if activeItemUri
pane.focusOnAttach = true if focused
pane
activeItem: null
items: null
initialize: (@items...) ->
@viewsByClassName = {}
@showItem(@items[0]) if @items.length > 0
@command 'core:close', @destroyActiveItem
@command 'core:save', @saveActiveItem
@command 'core:save-as', @saveActiveItemAs
@command 'pane:save-items', @saveItems
@command 'pane:show-next-item', @showNextItem
@command 'pane:show-previous-item', @showPreviousItem
@command 'pane:show-item-1', => @showItemAtIndex(0)
@command 'pane:show-item-2', => @showItemAtIndex(1)
@command 'pane:show-item-3', => @showItemAtIndex(2)
@command 'pane:show-item-4', => @showItemAtIndex(3)
@command 'pane:show-item-5', => @showItemAtIndex(4)
@command 'pane:show-item-6', => @showItemAtIndex(5)
@command 'pane:show-item-7', => @showItemAtIndex(6)
@command 'pane:show-item-8', => @showItemAtIndex(7)
@command 'pane:show-item-9', => @showItemAtIndex(8)
@command 'pane:split-left', => @splitLeft()
@command 'pane:split-right', => @splitRight()
@command 'pane:split-up', => @splitUp()
@command 'pane:split-down', => @splitDown()
@command 'pane:close', => @destroyItems()
@command 'pane:close-other-items', => @destroyInactiveItems()
@on 'focus', => @activeView?.focus(); false
@on 'focusin', => @makeActive()
@on 'focusout', => @autosaveActiveItem()
afterAttach: (onDom) ->
if @focusOnAttach and onDom
@focusOnAttach = null
@focus()
return if @attached
@attached = true
@trigger 'pane:attached'
makeActive: ->
for pane in @getContainer().getPanes() when pane isnt this
pane.makeInactive()
wasActive = @isActive()
@addClass('active')
@trigger 'pane:became-active' unless wasActive
makeInactive: ->
@removeClass('active')
isActive: ->
@hasClass('active')
getNextPane: ->
panes = @getContainer()?.getPanes()
return unless panes.length > 1
nextIndex = (panes.indexOf(this) + 1) % panes.length
panes[nextIndex]
getItems: ->
new Array(@items...)
showNextItem: =>
index = @getActiveItemIndex()
if index < @items.length - 1
@showItemAtIndex(index + 1)
else
@showItemAtIndex(0)
showPreviousItem: =>
index = @getActiveItemIndex()
if index > 0
@showItemAtIndex(index - 1)
else
@showItemAtIndex(@items.length - 1)
getActiveItemIndex: ->
@items.indexOf(@activeItem)
showItemAtIndex: (index) ->
@showItem(@itemAtIndex(index))
itemAtIndex: (index) ->
@items[index]
showItem: (item) ->
return if !item? or item is @activeItem
if @activeItem
@activeItem.off? 'title-changed', @activeItemTitleChanged
@autosaveActiveItem()
isFocused = @is(':has(:focus)')
@addItem(item)
item.on? 'title-changed', @activeItemTitleChanged
view = @viewForItem(item)
@itemViews.children().not(view).hide()
@itemViews.append(view) unless view.parent().is(@itemViews)
view.show()
view.focus() if isFocused
@activeItem = item
@activeView = view
@trigger 'pane:active-item-changed', [item]
activeItemTitleChanged: =>
@trigger 'pane:active-item-title-changed'
addItem: (item) ->
return if _.include(@items, item)
index = @getActiveItemIndex() + 1
@items.splice(index, 0, item)
@getContainer().itemAdded(item)
@trigger 'pane:item-added', [item, index]
item
destroyActiveItem: =>
@destroyItem(@activeItem)
false
destroyItem: (item) ->
container = @getContainer()
reallyDestroyItem = =>
@removeItem(item)
container.itemDestroyed(item)
item.destroy?()
@autosaveItem(item)
if item.isModified?()
@promptToSaveItem(item, reallyDestroyItem)
else
reallyDestroyItem()
destroyItems: ->
@destroyItem(item) for item in @getItems()
destroyInactiveItems: ->
@destroyItem(item) for item in @getItems() when item isnt @activeItem
promptToSaveItem: (item, nextAction, cancelAction) ->
uri = item.getUri()
atom.confirm(
"'#{item.getTitle?() ? item.getUri()}' has changes, do you want to save them?"
"Your changes will be lost if close this item without saving."
"Save", => @saveItem(item, nextAction)
"Cancel", cancelAction
"Don't Save", nextAction
)
saveActiveItem: =>
@saveItem(@activeItem)
saveActiveItemAs: =>
@saveItemAs(@activeItem)
saveItem: (item, nextAction) ->
if item.getUri?()
item.save()
nextAction?()
else
@saveItemAs(item, nextAction)
saveItemAs: (item, nextAction) ->
return unless item.saveAs?
atom.showSaveDialog (path) =>
if path
item.saveAs(path)
nextAction?()
saveItems: =>
@saveItem(item) for item in @getItems()
autosaveActiveItem: ->
@autosaveItem(@activeItem)
autosaveItem: (item) ->
@saveItem(item) if config.get('core.autosave') and item.getUri?()
removeItem: (item) ->
index = @items.indexOf(item)
return if index == -1
@showNextItem() if item is @activeItem and @items.length > 1
_.remove(@items, item)
@cleanupItemView(item)
@trigger 'pane:item-removed', [item, index]
moveItem: (item, newIndex) ->
oldIndex = @items.indexOf(item)
@items.splice(oldIndex, 1)
@items.splice(newIndex, 0, item)
@trigger 'pane:item-moved', [item, newIndex]
moveItemToPane: (item, pane, index) ->
@removeItem(item)
pane.addItem(item, index)
itemForUri: (uri) ->
_.detect @items, (item) -> item.getUri?() is uri
showItemForUri: (uri) ->
@showItem(@itemForUri(uri))
cleanupItemView: (item) ->
if item instanceof $
viewToRemove = item
else
viewClass = item.getViewClass()
otherItemsForView = @items.filter (i) -> i.getViewClass?() is viewClass
unless otherItemsForView.length
viewToRemove = @viewsByClassName[viewClass.name]
viewToRemove?.setModel(null)
delete @viewsByClassName[viewClass.name]
if @items.length > 0
viewToRemove?.remove()
else
@remove()
viewForItem: (item) ->
if item instanceof $
item
else
viewClass = item.getViewClass()
if view = @viewsByClassName[viewClass.name]
view.setModel(item)
else
view = @viewsByClassName[viewClass.name] = new viewClass(item)
view
viewForActiveItem: ->
@viewForItem(@activeItem)
serialize: ->
deserializer: "Pane"
wrappedView: @wrappedView?.serialize()
focused: @is(':has(:focus)')
activeItemUri: @activeItem.getUri?() if typeof @activeItem.serialize is 'function'
items: _.compact(@getItems().map (item) -> item.serialize?())
adjustDimensions: -> # do nothing
horizontalGridUnits: ->
1
horizontalGridUnits: -> 1
verticalGridUnits: ->
1
verticalGridUnits: -> 1
splitUp: (view) ->
@split(view, 'column', 'before')
splitUp: (items...) ->
@split(items, 'column', 'before')
splitDown: (view) ->
@split(view, 'column', 'after')
splitDown: (items...) ->
@split(items, 'column', 'after')
splitLeft: (view) ->
@split(view, 'row', 'before')
splitLeft: (items...) ->
@split(items, 'row', 'before')
splitRight: (view) ->
@split(view, 'row', 'after')
splitRight: (items...) ->
@split(items, 'row', 'after')
split: (view, axis, side) ->
split: (items, axis, side) ->
unless @parent().hasClass(axis)
@buildPaneAxis(axis)
.insertBefore(this)
.append(@detach())
pane = new Pane(view)
items = [@copyActiveItem()] unless items.length
pane = new Pane(items...)
this[side](pane)
rootView.adjustPaneDimensions()
view.focus?()
@getContainer().adjustPaneDimensions()
pane.focus()
pane
remove: (selector, keepData) ->
return super if keepData
# find parent elements before removing from dom
parentAxis = @parent('.row, .column')
super
if parentAxis.children().length == 1
sibling = parentAxis.children().detach()
parentAxis.replaceWith(sibling)
rootView.adjustPaneDimensions()
buildPaneAxis: (axis) ->
switch axis
when 'row' then new PaneRow
when 'column' then new PaneColumn
getContainer: ->
@closest('#panes').view()
copyActiveItem: ->
deserialize(@activeItem.serialize())
remove: (selector, keepData) ->
return super if keepData
# find parent elements before removing from dom
container = @getContainer()
parentAxis = @parent('.row, .column')
if @is(':has(:focus)')
container.focusNextPane() or rootView?.focus()
else if @isActive()
container.makeNextPaneActive()
super
if parentAxis.children().length == 1
sibling = parentAxis.children()
siblingFocused = sibling.is(':has(:focus)')
sibling.detach()
parentAxis.replaceWith(sibling)
sibling.focus() if siblingFocused
container.adjustPaneDimensions()
container.trigger 'pane:removed', [this]
afterRemove: ->
item.destroy?() for item in @getItems()

View File

@@ -7,7 +7,6 @@ EditSession = require 'edit-session'
EventEmitter = require 'event-emitter'
Directory = require 'directory'
ChildProcess = require 'child-process'
Git = require 'git'
module.exports =
class Project
@@ -35,8 +34,6 @@ class Project
grammarOverridesByPath: @grammarOverridesByPath
destroy: ->
@repo?.destroy()
@repo = null
editSession.destroy() for editSession in @getEditSessions()
addGrammarOverrideForPath: (path, grammar) ->
@@ -60,10 +57,8 @@ class Project
if path?
directory = if fs.isDirectory(path) then path else fs.directory(path)
@rootDirectory = new Directory(directory)
@repo = Git.open(path)
else
@rootDirectory = null
@repo = null
@trigger "path-changed"
@@ -85,7 +80,7 @@ class Project
@ignoreRepositoryPath(path)
ignoreRepositoryPath: (path) ->
config.get("core.hideGitIgnoredFiles") and @repo?.isPathIgnored(fs.join(@getPath(), path))
config.get("core.hideGitIgnoredFiles") and git?.isPathIgnored(fs.join(@getPath(), path))
resolve: (filePath) ->
filePath = fs.join(@getPath(), filePath) unless filePath[0] == '/'
@@ -100,10 +95,10 @@ class Project
getSoftWrap: -> @softWrap
setSoftWrap: (@softWrap) ->
buildEditSessionForPath: (filePath, editSessionOptions={}) ->
@buildEditSession(@bufferForPath(filePath), editSessionOptions)
buildEditSession: (filePath, editSessionOptions={}) ->
@buildEditSessionForBuffer(@bufferForPath(filePath), editSessionOptions)
buildEditSession: (buffer, editSessionOptions) ->
buildEditSessionForBuffer: (buffer, editSessionOptions) ->
options = _.extend(@defaultEditSessionOptions(), editSessionOptions)
options.project = this
options.buffer = buffer
@@ -133,9 +128,15 @@ class Project
buffers.push editSession.buffer
buffers
eachBuffer: (callback) ->
eachBuffer: (args...) ->
subscriber = args.shift() if args.length > 1
callback = args.shift()
callback(buffer) for buffer in @getBuffers()
@on 'buffer-created', (buffer) -> callback(buffer)
if subscriber
subscriber.subscribe this, 'buffer-created', (buffer) -> callback(buffer)
else
@on 'buffer-created', (buffer) -> callback(buffer)
bufferForPath: (filePath) ->
if filePath?

View File

@@ -0,0 +1,18 @@
Git = require 'git'
fs = require 'fs'
module.exports =
loadStatuses: (path) ->
repo = Git.open(path)
if repo?
workingDirectoryPath = repo.getWorkingDirectory()
statuses = {}
for path, status of repo.getRepo().getStatuses()
statuses[fs.join(workingDirectoryPath, path)] = status
upstream = repo.getAheadBehindCounts()
repo.destroy()
else
upstream = {}
statuses = {}
callTaskMethod('statusesLoaded', {statuses, upstream})

View File

@@ -0,0 +1,17 @@
Task = require 'task'
_ = require 'underscore'
module.exports =
class RepositoryStatusTask extends Task
constructor: (@repo) ->
super('repository-status-handler')
started: ->
@callWorkerMethod('loadStatuses', @repo.getPath())
statusesLoaded: ({statuses, upstream}) ->
@done()
statusesUnchanged = _.isEqual(statuses, @repo.statuses) and _.isEqual(upstream, @repo.upstream)
@repo.statuses = statuses
@repo.upstream = upstream
@repo.trigger 'statuses-changed' unless statusesUnchanged

View File

@@ -10,28 +10,29 @@ Project = require 'project'
Pane = require 'pane'
PaneColumn = require 'pane-column'
PaneRow = require 'pane-row'
PaneContainer = require 'pane-container'
EditSession = require 'edit-session'
module.exports =
class RootView extends View
registerDeserializers(this, Pane, PaneRow, PaneColumn, Editor)
@version: 1
@configDefaults:
ignoredNames: [".git", ".svn", ".DS_Store"]
disabledPackages: []
@content: ->
@content: ({panes}={}) ->
@div id: 'root-view', =>
@div id: 'horizontal', outlet: 'horizontal', =>
@div id: 'vertical', outlet: 'vertical', =>
@div id: 'panes', outlet: 'panes'
@subview 'panes', panes ? new PaneContainer
@deserialize: ({ panesViewState, packageStates, projectPath }) ->
atom.atomPackageStates = packageStates ? {}
rootView = new RootView
rootView.setRootPane(deserialize(panesViewState)) if panesViewState
rootView
title: null
@deserialize: ({ panes, packages, projectPath }) ->
atom.atomPackageStates = packages ? {}
panes = deserialize(panes) if panes?.deserializer is 'PaneContainer'
new RootView({panes})
initialize: ->
@command 'toggle-dev-tools', => atom.toggleDevTools()
@@ -39,12 +40,11 @@ class RootView extends View
@subscribe $(window), 'focus', (e) =>
@handleFocus(e) if document.activeElement is document.body
@on 'root-view:active-path-changed', (e, path) =>
if path
project.setPath(path) unless project.getRootDirectory()
@setTitle(fs.base(path))
else
@setTitle("untitled")
project.on 'path-changed', => @updateTitle()
@on 'pane:became-active', => @updateTitle()
@on 'pane:active-item-changed', '.active.pane', => @updateTitle()
@on 'pane:removed', => @updateTitle() unless @getActivePane()
@on 'pane:active-item-title-changed', '.active.pane', => @updateTitle()
@command 'window:increase-font-size', =>
config.set("editor.fontSize", config.get("editor.fontSize") + 1)
@@ -53,26 +53,34 @@ class RootView extends View
fontSize = config.get "editor.fontSize"
config.set("editor.fontSize", fontSize - 1) if fontSize > 1
@command 'window:focus-next-pane', => @focusNextPane()
@command 'window:save-all', => @saveAll()
@command 'window:toggle-invisibles', =>
config.set("editor.showInvisibles", !config.get("editor.showInvisibles"))
@command 'window:toggle-ignored-files', =>
config.set("core.hideGitIgnoredFiles", not config.core.hideGitIgnoredFiles)
@command 'window:toggle-auto-indent', =>
config.set("editor.autoIndent", !config.get("editor.autoIndent"))
@command 'window:toggle-auto-indent-on-paste', =>
config.set("editor.autoIndentOnPaste", !config.get("editor.autoIndentOnPaste"))
@command 'pane:reopen-closed-item', =>
@panes.reopenItem()
serialize: ->
version: RootView.version
deserializer: 'RootView'
panesViewState: @panes.children().view()?.serialize()
packageStates: atom.serializeAtomPackages()
panes: @panes.serialize()
packages: atom.serializeAtomPackages()
confirmClose: ->
@panes.confirmClose()
handleFocus: (e) ->
if @getActiveEditor()
@getActiveEditor().focus()
if @getActivePane()
@getActivePane().focus()
false
else
@setTitle(null)
@@ -92,118 +100,57 @@ class RootView extends View
open: (path, options = {}) ->
changeFocus = options.changeFocus ? true
allowActiveEditorChange = options.allowActiveEditorChange ? false
unless editSession = @openInExistingEditor(path, allowActiveEditorChange, changeFocus)
editSession = project.buildEditSessionForPath(path)
editor = new Editor({editSession})
pane = new Pane(editor)
@panes.append(pane)
if changeFocus
editor.focus()
path = project.resolve(path) if path?
if activePane = @getActivePane()
if editSession = activePane.itemForUri(path)
activePane.showItem(editSession)
else
@makeEditorActive(editor, changeFocus)
editSession = project.buildEditSession(path)
activePane.showItem(editSession)
else
editSession = project.buildEditSession(path)
activePane = new Pane(editSession)
@panes.append(activePane)
activePane.focus() if changeFocus
editSession
openInExistingEditor: (path, allowActiveEditorChange, changeFocus) ->
if activeEditor = @getActiveEditor()
activeEditor.focus() if changeFocus
path = project.resolve(path) if path
if editSession = activeEditor.activateEditSessionForPath(path)
return editSession
if allowActiveEditorChange
for editor in @getEditors()
if editSession = editor.activateEditSessionForPath(path)
@makeEditorActive(editor, changeFocus)
return editSession
editSession = project.buildEditSessionForPath(path)
activeEditor.edit(editSession)
editSession
editorFocused: (editor) ->
@makeEditorActive(editor) if @panes.containsElement(editor)
makeEditorActive: (editor, focus) ->
if focus
editor.focus()
return
previousActiveEditor = @panes.find('.editor.active').view()
previousActiveEditor?.removeClass('active').off('.root-view')
editor.addClass('active')
if not editor.mini
editor.on 'editor:path-changed.root-view', =>
@trigger 'root-view:active-path-changed', editor.getPath()
if not previousActiveEditor or editor.getPath() != previousActiveEditor.getPath()
@trigger 'root-view:active-path-changed', editor.getPath()
activeKeybindings: ->
keymap.bindingsForElement(document.activeElement)
getTitle: ->
@title or "untitled"
updateTitle: ->
if projectPath = project.getPath()
if item = @getActivePaneItem()
@setTitle("#{item.getTitle?() ? 'untitled'} - #{projectPath}")
else
@setTitle(projectPath)
else
@setTitle('untitled')
setTitle: (title) ->
projectPath = project.getPath()
if not projectPath
@title = "untitled"
else if title
@title = "#{title} #{projectPath}"
else
@title = projectPath
@updateWindowTitle()
updateWindowTitle: ->
document.title = @title
document.title = title
getEditors: ->
@panes.find('.pane > .editor').map(-> $(this).view()).toArray()
@panes.find('.pane > .item-views > .editor').map(-> $(this).view()).toArray()
getModifiedBuffers: ->
modifiedBuffers = []
for editor in @getEditors()
for session in editor.editSessions
modifiedBuffers.push session.buffer if session.buffer.isModified()
for pane in @getPanes()
for item in pane.getItems() when item instanceof EditSession
modifiedBuffers.push item.buffer if item.buffer.isModified()
modifiedBuffers
getOpenBufferPaths: ->
_.uniq(_.flatten(@getEditors().map (editor) -> editor.getOpenBufferPaths()))
getActiveEditor: ->
if (editor = @panes.find('.editor.active')).length
editor.view()
else
@panes.find('.editor:first').view()
getActivePane: ->
@panes.getActivePane()
getActiveEditSession: ->
@getActiveEditor()?.activeEditSession
getActivePaneItem: ->
@panes.getActivePaneItem()
focusNextPane: ->
panes = @panes.find('.pane')
currentIndex = panes.toArray().indexOf(@getFocusedPane()[0])
nextIndex = (currentIndex + 1) % panes.length
panes.eq(nextIndex).view().wrappedView.focus()
getActiveView: ->
@panes.getActiveView()
getFocusedPane: ->
@panes.find('.pane:has(:focus)')
setRootPane: (pane) ->
@panes.empty()
@panes.append(pane)
@adjustPaneDimensions()
adjustPaneDimensions: ->
rootPane = @panes.children().first().view()
rootPane?.css(width: '100%', height: '100%', top: 0, left: 0)
rootPane?.adjustDimensions()
focusNextPane: -> @panes.focusNextPane()
getFocusedPane: -> @panes.getFocusedPane()
remove: ->
editor.remove() for editor in @getEditors()
@@ -211,7 +158,16 @@ class RootView extends View
super
saveAll: ->
editor.save() for editor in @getEditors()
@panes.saveAll()
eachPane: (callback) ->
@panes.eachPane(callback)
getPanes: ->
@panes.getPanes()
indexOfPane: (pane) ->
@panes.indexOfPane(pane)
eachEditor: (callback) ->
callback(editor) for editor in @getEditors()
@@ -223,10 +179,3 @@ class RootView extends View
eachBuffer: (callback) ->
project.eachBuffer(callback)
indexOfPane: (pane) ->
index = -1
for p, idx in @panes.find('.pane')
if pane.is(p)
index = idx
break
index

View File

@@ -12,6 +12,8 @@ class ScreenLine
new ScreenLine({@tokens, @ruleStack, @bufferRows, @startBufferColumn, @fold})
clipScreenColumn: (column, options={}) ->
return 0 if @tokens.length == 0
{ skipAtomicTokens } = options
column = Math.min(column, @getMaxScreenColumn())

View File

@@ -20,7 +20,7 @@ class SelectList extends View
cancelling: false
initialize: ->
requireStylesheet 'select-list.css'
requireStylesheet 'select-list.less'
@miniEditor.getBuffer().on 'changed', => @schedulePopulateList()
@miniEditor.on 'focusout', => @cancel() unless @cancelling

View File

@@ -164,7 +164,7 @@ class Selection
if options.autoIndent
if text == '\n'
@editSession.autoIndentBufferRow(newBufferRange.end.row)
else
else if /\S/.test(text)
@editSession.autoDecreaseIndentForRow(newBufferRange.start.row)
newBufferRange

View File

@@ -14,7 +14,9 @@ class SortableList extends View
@on 'drop', '.sortable', @onDrop
onDragStart: (event) =>
return false if !@shouldAllowDrag(event)
unless @shouldAllowDrag(event)
event.preventDefault()
return
el = @getSortableElement(event)
el.addClass 'is-dragging'
@@ -45,9 +47,8 @@ class SortableList extends View
true
getDroppedElement: (event) ->
idx = event.originalEvent.dataTransfer.getData 'sortable-index'
@find ".sortable:eq(#{idx})"
index = event.originalEvent.dataTransfer.getData('sortable-index')
@find(".sortable:eq(#{index})")
getSortableElement: (event) ->
el = $(event.target)
if !el.hasClass('sortable') then el.closest('.sortable') else el
$(event.target).closest('.sortable')

View File

@@ -31,6 +31,8 @@ class TextMatePackage extends Package
console.warn "Failed to load package at '#{@path}'", e.stack
this
activate: -> # no-op
getGrammars: -> @grammars
readGrammars: ->

View File

@@ -11,7 +11,7 @@ class Theme
if fs.exists(name)
path = name
else
path = fs.resolve(config.themeDirPaths..., name, ['', '.tmTheme', '.css'])
path = fs.resolve(config.themeDirPaths..., name, ['', '.tmTheme', '.css', 'less'])
throw new Error("No theme exists named '#{name}'") unless path

Some files were not shown because too many files have changed in this diff Show More