Merge remote-tracking branch 'origin/master' into vim-core-changes

Conflicts:
	src/app/editor.coffee
This commit is contained in:
Mutwin Kraus 2013-04-08 12:54:43 +02:00
commit a12c78100e
48 changed files with 542 additions and 478 deletions

2
.gitignore vendored
View File

@ -8,7 +8,7 @@ build
.xcodebuild-info
node_modules
npm-debug.log
/tags
tags
/cef/
/sources.gypi
/node/

View File

@ -251,8 +251,6 @@
'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/native.h',
@ -269,6 +267,13 @@
'native/mac/English.lproj/AtomWindow.xib',
'native/mac/English.lproj/MainMenu.xib',
],
'conditions': [
['CODE_SIGN', {
'defines': [
'CODE_SIGNING_ENABLED=1',
],
}],
],
'postbuilds': [
{
'postbuild_name': 'Copy Static Files',

View File

@ -143,12 +143,34 @@
}
- (void)open:(NSString *)path pidToKillWhenWindowCloses:(NSNumber *)pid {
for (NSWindow *window in [self windows]) {
if (![window isExcludedFromWindowsMenu]) {
AtomWindowController *controller = [window windowController];
if ([path isEqualToString:controller.pathToOpen]) {
[window makeKeyAndOrderFront:nil];
return;
BOOL openingDirectory = false;
[[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&openingDirectory];
if (!pid) {
for (NSWindow *window in [self windows]) {
if (![window isExcludedFromWindowsMenu]) {
AtomWindowController *controller = [window windowController];
if (!openingDirectory) {
BOOL openedPathIsDirectory = false;
[[NSFileManager defaultManager] fileExistsAtPath:controller.pathToOpen isDirectory:&openedPathIsDirectory];
NSString *projectPath = NULL;
if (openedPathIsDirectory) {
projectPath = [NSString stringWithFormat:@"%@/", controller.pathToOpen];
}
else {
projectPath = [controller.pathToOpen stringByDeletingLastPathComponent];
}
if ([path hasPrefix:projectPath]) {
[window makeKeyAndOrderFront:nil];
[controller openPath:path];
return;
}
}
if ([path isEqualToString:controller.pathToOpen]) {
[window makeKeyAndOrderFront:nil];
return;
}
}
}
}
@ -232,12 +254,14 @@
}
else {
_backgroundWindowController = [[AtomWindowController alloc] initInBackground];
if (![self.arguments objectForKey:@"dev"]) {
SUUpdater.sharedUpdater.delegate = self;
SUUpdater.sharedUpdater.automaticallyChecksForUpdates = YES;
SUUpdater.sharedUpdater.automaticallyDownloadsUpdates = YES;
[SUUpdater.sharedUpdater checkForUpdatesInBackground];
}
#if defined(CODE_SIGNING_ENABLED)
SUUpdater.sharedUpdater.delegate = self;
SUUpdater.sharedUpdater.automaticallyChecksForUpdates = YES;
SUUpdater.sharedUpdater.automaticallyDownloadsUpdates = YES;
[SUUpdater.sharedUpdater checkForUpdatesInBackground];
#endif
}
}

View File

@ -90,6 +90,9 @@ bool AtomCefClient::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
else if (name == "crash") {
__builtin_trap();
}
else if (name == "restartRendererProcess") {
RestartRendererProcess(browser);
}
else {
return false;
}
@ -252,3 +255,11 @@ bool AtomCefClient::Save(const std::string& path, const std::string& data) {
fclose(f);
return true;
}
void AtomCefClient::RestartRendererProcess(CefRefPtr<CefBrowser> browser) {
// Navigating to the same URL has the effect of restarting the renderer
// process, because cefode has overridden ContentBrowserClient's
// ShouldSwapProcessesForNavigation method.
CefRefPtr<CefFrame> frame = browser->GetFocusedFrame();
frame->LoadURL(frame->GetURL());
}

View File

@ -99,6 +99,7 @@ class AtomCefClient : public CefClient,
void EndTracing();
bool Save(const std::string& path, const std::string& data);
void RestartRendererProcess(CefRefPtr<CefBrowser> browser);
protected:
CefRefPtr<CefBrowser> m_Browser;

View File

@ -18,7 +18,6 @@ class AtomCefRenderProcessHandler : public CefRenderProcessHandler {
CefRefPtr<CefProcessMessage> message) OVERRIDE;
void Reload(CefRefPtr<CefBrowser> browser);
void Shutdown(CefRefPtr<CefBrowser> browser);
bool CallMessageReceivedHandler(CefRefPtr<CefV8Context> context, CefRefPtr<CefProcessMessage> message);
void InjectExtensionsIntoV8Context(CefRefPtr<CefV8Context> context);

View File

@ -2,7 +2,6 @@
#import "native/v8_extensions/atom.h"
#import "native/v8_extensions/native.h"
#import "native/message_translation.h"
#import "path_watcher.h"
#import "atom_cef_render_process_handler.h"
@ -18,7 +17,6 @@ void AtomCefRenderProcessHandler::OnContextCreated(CefRefPtr<CefBrowser> browser
void AtomCefRenderProcessHandler::OnContextReleased(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefV8Context> context) {
[PathWatcher removePathWatcherForContext:context];
}
bool AtomCefRenderProcessHandler::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
@ -30,10 +28,6 @@ bool AtomCefRenderProcessHandler::OnProcessMessageReceived(CefRefPtr<CefBrowser>
Reload(browser);
return true;
}
else if (name == "shutdown") {
Shutdown(browser);
return true;
}
else {
return CallMessageReceivedHandler(browser->GetMainFrame()->GetV8Context(), message);
}
@ -54,17 +48,6 @@ void AtomCefRenderProcessHandler::Reload(CefRefPtr<CefBrowser> browser) {
context->Exit();
}
void AtomCefRenderProcessHandler::Shutdown(CefRefPtr<CefBrowser> browser) {
CefRefPtr<CefV8Context> context = browser->GetMainFrame()->GetV8Context();
CefRefPtr<CefV8Value> global = context->GetGlobal();
context->Enter();
CefV8ValueList arguments;
CefRefPtr<CefV8Value> shutdownFunction = global->GetValue("shutdown");
shutdownFunction->ExecuteFunction(global, arguments);
context->Exit();
}
bool AtomCefRenderProcessHandler::CallMessageReceivedHandler(CefRefPtr<CefV8Context> context, CefRefPtr<CefProcessMessage> message) {
context->Enter();

View File

@ -34,5 +34,6 @@ class AtomCefClient;
- (void)toggleDevTools;
- (void)showDevTools;
- (void)openPath:(NSString*)path;
@end

View File

@ -215,6 +215,16 @@
_cefClient->GetBrowser()->GetHost()->SetFocus(true);
}
- (void)openPath:(NSString*)path {
if (_cefClient && _cefClient->GetBrowser()) {
CefRefPtr<CefProcessMessage> openMessage = CefProcessMessage::Create("openPath");
CefRefPtr<CefListValue> openArguments = openMessage->GetArgumentList();
openArguments->SetSize(1);
openArguments->SetString(0, [path UTF8String]);
_cefClient->GetBrowser()->SendProcessMessage(PID_RENDERER, openMessage);
}
}
- (void)setPidToKillOnClose:(NSNumber *)pid {
_pidToKillOnClose = [pid retain];
}
@ -236,7 +246,7 @@
- (BOOL)windowShouldClose:(NSNotification *)notification {
if (_cefClient && _cefClient->GetBrowser()) {
_cefClient->GetBrowser()->SendProcessMessage(PID_RENDERER, CefProcessMessage::Create("shutdown"));
_cefClient->GetBrowser()->GetHost()->CloseBrowser(false);
}
if (_pidToKillOnClose) kill([_pidToKillOnClose intValue], SIGQUIT);

View File

@ -1,25 +0,0 @@
#import "include/cef_base.h"
#import "include/cef_v8.h"
#import <Foundation/Foundation.h>
typedef void (^WatchCallback)(NSString *, NSString *);
@interface PathWatcher : NSObject {
int _kq;
CefRefPtr<CefV8Context> _context;
NSMutableDictionary *_callbacksByPath;
NSMutableDictionary *_fileDescriptorsByPath;
bool _keepWatching;
}
+ (PathWatcher *)pathWatcherForContext:(CefRefPtr<CefV8Context>)context;
+ (void)removePathWatcherForContext:(CefRefPtr<CefV8Context>)context;
- (id)initWithContext:(CefRefPtr<CefV8Context>)context;
- (NSString *)watchPath:(NSString *)path callback:(WatchCallback)callback;
- (void)unwatchPath:(NSString *)path callbackId:(NSString *)callbackId error:(NSError **)error;
- (void)unwatchAllPaths;
- (NSArray *)watchedPaths;
@end

View File

@ -1,273 +0,0 @@
#import <sys/event.h>
#import <sys/time.h>
#import <sys/param.h>
#import <fcntl.h>
#import "path_watcher.h"
static NSMutableArray *gPathWatchers;
@interface PathWatcher ()
- (bool)usesContext:(CefRefPtr<CefV8Context>)context;
- (NSString *)watchPath:(NSString *)path callback:(WatchCallback)callback callbackId:(NSString *)callbackId;
- (void)stopWatching;
- (bool)isAtomicWrite:(struct kevent)event;
@end
@implementation PathWatcher
+ (PathWatcher *)pathWatcherForContext:(CefRefPtr<CefV8Context>)context {
if (!gPathWatchers) gPathWatchers = [[NSMutableArray alloc] init];
PathWatcher *pathWatcher = nil;
for (PathWatcher *p in gPathWatchers) {
if ([p usesContext:context]) {
pathWatcher = p;
break;
}
}
if (!pathWatcher) {
pathWatcher = [[[PathWatcher alloc] initWithContext:context] autorelease];
[gPathWatchers addObject:pathWatcher];
}
return pathWatcher;
}
+ (void)removePathWatcherForContext:(CefRefPtr<CefV8Context>)context {
PathWatcher *pathWatcher = nil;
for (PathWatcher *p in gPathWatchers) {
if ([p usesContext:context]) {
pathWatcher = p;
break;
}
}
if (pathWatcher) {
[pathWatcher stopWatching];
[gPathWatchers removeObject:pathWatcher];
}
}
- (void)dealloc {
@synchronized(self) {
close(_kq);
for (NSString *path in [_callbacksByPath allKeys]) {
[self removeKeventForPath:path];
}
[_callbacksByPath release];
_context = nil;
_keepWatching = false;
}
[super dealloc];
}
- (id)initWithContext:(CefRefPtr<CefV8Context>)context {
self = [super init];
_keepWatching = YES;
_callbacksByPath = [[NSMutableDictionary alloc] init];
_fileDescriptorsByPath = [[NSMutableDictionary alloc] init];
_kq = kqueue();
_context = context;
if (_kq == -1) {
[NSException raise:@"PathWatcher" format:@"Could not create kqueue"];
}
[self performSelectorInBackground:@selector(watch) withObject:NULL];
return self;
}
- (bool)usesContext:(CefRefPtr<CefV8Context>)context {
return _context->IsSame(context);
}
- (void)stopWatching {
@synchronized(self) {
[self unwatchAllPaths];
_keepWatching = false;
}
}
- (NSString *)watchPath:(NSString *)path callback:(WatchCallback)callback {
NSString *callbackId = [[NSProcessInfo processInfo] globallyUniqueString];
return [self watchPath:path callback:callback callbackId:callbackId];
}
- (NSString *)watchPath:(NSString *)path callback:(WatchCallback)callback callbackId:(NSString *)callbackId {
@synchronized(self) {
if (![self createKeventForPath:path]) {
NSLog(@"WARNING: Failed to create kevent for path '%@'", path);
return nil;
}
NSMutableDictionary *callbacks = [_callbacksByPath objectForKey:path];
if (!callbacks) {
callbacks = [NSMutableDictionary dictionary];
[_callbacksByPath setObject:callbacks forKey:path];
}
[callbacks setObject:callback forKey:callbackId];
}
return callbackId;
}
- (void)unwatchPath:(NSString *)path callbackId:(NSString *)callbackId error:(NSError **)error {
@synchronized(self) {
NSMutableDictionary *callbacks = [_callbacksByPath objectForKey:path];
if (callbacks) {
if (callbackId) {
[callbacks removeObjectForKey:callbackId];
}
else {
[callbacks removeAllObjects];
}
if (callbacks.count == 0) {
[self removeKeventForPath:path];
[_callbacksByPath removeObjectForKey:path];
}
}
}
}
- (NSArray *)watchedPaths {
return [_callbacksByPath allKeys];
}
- (void)unwatchAllPaths {
@synchronized(self) {
NSArray *paths = [_callbacksByPath allKeys];
for (NSString *path in paths) {
[self unwatchPath:path callbackId:nil error:nil];
}
}
}
- (bool)createKeventForPath:(NSString *)path {
@synchronized(self) {
if ([_fileDescriptorsByPath objectForKey:path]) {
NSLog(@"we already have a kevent");
return YES;
}
int fd = open([path fileSystemRepresentation], O_EVTONLY, 0);
if (fd < 0) {
NSLog(@"WARNING: Could not create file descriptor for path '%@'. Error code %d.", path, errno);
return NO;
}
[_fileDescriptorsByPath setObject:[NSNumber numberWithInt:fd] forKey:path];
struct timespec timeout = { 0, 0 };
struct kevent event;
int filter = EVFILT_VNODE;
int flags = EV_ADD | EV_ENABLE | EV_CLEAR;
int filterFlags = NOTE_WRITE | NOTE_DELETE | NOTE_RENAME;
EV_SET(&event, fd, filter, flags, filterFlags, 0, path);
kevent(_kq, &event, 1, NULL, 0, &timeout);
return YES;
}
}
- (void)removeKeventForPath:(NSString *)path {
@synchronized(self) {
NSNumber *fdNumber = [_fileDescriptorsByPath objectForKey:path];
if (!fdNumber) {
NSLog(@"WARNING: Could not find file descriptor for path '%@'", path);
return;
}
close([fdNumber integerValue]);
[_fileDescriptorsByPath removeObjectForKey:path];
}
}
- (bool)isAtomicWrite:(struct kevent)event {
if (!event.fflags & NOTE_DELETE) return NO;
const char *path = [(NSString *)event.udata fileSystemRepresentation];
bool fileExists = access(path, F_OK) != -1;
return fileExists;
}
- (void)changePath:(NSString *)path toNewPath:(NSString *)newPath {
@synchronized(self) {
NSDictionary *callbacks = [NSDictionary dictionaryWithDictionary:[_callbacksByPath objectForKey:path]];
[self unwatchPath:path callbackId:nil error:nil];
for (NSString *callbackId in [callbacks allKeys]) {
[self watchPath:newPath callback:[callbacks objectForKey:callbackId] callbackId:callbackId];
}
}
}
- (void)watch {
struct kevent event;
struct timespec timeout = { 5, 0 }; // 5 seconds timeout.
while (_keepWatching) {
@autoreleasepool {
int numberOfEvents = kevent(_kq, NULL, 0, &event, 1, &timeout);
if (numberOfEvents == 0) {
continue;
}
NSString *eventFlag = nil;
NSString *newPath = nil;
NSString *path = [(NSString *)event.udata retain];
if (event.fflags & NOTE_WRITE) {
eventFlag = @"contents-change";
}
else if ([self isAtomicWrite:event]) {
eventFlag = @"contents-change";
// Atomic writes require the kqueue to be recreated
[self removeKeventForPath:path];
[self createKeventForPath:path];
}
else if (event.fflags & NOTE_DELETE) {
eventFlag = @"remove";
}
else if (event.fflags & NOTE_RENAME) {
eventFlag = @"move";
char pathBuffer[MAXPATHLEN];
fcntl((int)event.ident, F_GETPATH, &pathBuffer);
close(event.ident);
newPath = [NSString stringWithUTF8String:pathBuffer];
if (!newPath) {
NSLog(@"WARNING: Ignoring rename event for deleted file '%@'", path);
continue;
}
}
NSDictionary *callbacks;
@synchronized(self) {
callbacks = [NSDictionary dictionaryWithDictionary:[_callbacksByPath objectForKey:path]];
}
if ([eventFlag isEqual:@"move"]) {
[self changePath:path toNewPath:newPath];
}
if ([eventFlag isEqual:@"remove"]) {
[self unwatchPath:path callbackId:nil error:nil];
}
dispatch_sync(dispatch_get_main_queue(), ^{
for (NSString *key in callbacks) {
WatchCallback callback = [callbacks objectForKey:key];
callback(eventFlag, newPath ? newPath : path);
}
});
[path release];
}
}
}
@end

View File

@ -4,7 +4,6 @@
#import "atom_application.h"
#import "native.h"
#import "include/cef_base.h"
#import "path_watcher.h"
#import <iostream>
@ -22,8 +21,7 @@ namespace v8_extensions {
void Native::CreateContextBinding(CefRefPtr<CefV8Context> context) {
const char* methodNames[] = {
"writeToPasteboard", "readFromPasteboard", "quit", "watchPath",
"unwatchPath", "getWatchedPaths", "unwatchAllPaths", "moveToTrash",
"writeToPasteboard", "readFromPasteboard", "quit", "moveToTrash",
"reload", "setWindowState", "getWindowState", "beep", "crash"
};
@ -67,67 +65,6 @@ namespace v8_extensions {
[NSApp terminate:nil];
return true;
}
else if (name == "watchPath") {
NSString *path = stringFromCefV8Value(arguments[0]);
CefRefPtr<CefV8Value> function = arguments[1];
CefRefPtr<CefV8Context> context = CefV8Context::GetCurrentContext();
WatchCallback callback = ^(NSString *eventType, NSString *path) {
context->Enter();
CefV8ValueList args;
args.push_back(CefV8Value::CreateString(string([eventType UTF8String], [eventType lengthOfBytesUsingEncoding:NSUTF8StringEncoding])));
args.push_back(CefV8Value::CreateString(string([path UTF8String], [path lengthOfBytesUsingEncoding:NSUTF8StringEncoding])));
function->ExecuteFunction(function, args);
context->Exit();
};
PathWatcher *pathWatcher = [PathWatcher pathWatcherForContext:CefV8Context::GetCurrentContext()];
NSString *watchId = [pathWatcher watchPath:path callback:[[callback copy] autorelease]];
if (watchId) {
retval = CefV8Value::CreateString([watchId UTF8String]);
}
else {
exception = string("Failed to watch path '") + string([path UTF8String]) + string("' (it may not exist)");
}
return true;
}
else if (name == "unwatchPath") {
NSString *path = stringFromCefV8Value(arguments[0]);
NSString *callbackId = stringFromCefV8Value(arguments[1]);
NSError *error = nil;
PathWatcher *pathWatcher = [PathWatcher pathWatcherForContext:CefV8Context::GetCurrentContext()];
[pathWatcher unwatchPath:path callbackId:callbackId error:&error];
if (error) {
exception = [[error localizedDescription] UTF8String];
}
return true;
}
else if (name == "getWatchedPaths") {
PathWatcher *pathWatcher = [PathWatcher pathWatcherForContext:CefV8Context::GetCurrentContext()];
NSArray *paths = [pathWatcher watchedPaths];
CefRefPtr<CefV8Value> pathsArray = CefV8Value::CreateArray([paths count]);
for (int i = 0; i < [paths count]; i++) {
CefRefPtr<CefV8Value> path = CefV8Value::CreateString([[paths objectAtIndex:i] UTF8String]);
pathsArray->SetValue(i, path);
}
retval = pathsArray;
return true;
}
else if (name == "unwatchAllPaths") {
PathWatcher *pathWatcher = [PathWatcher pathWatcherForContext:CefV8Context::GetCurrentContext()];
[pathWatcher unwatchAllPaths];
return true;
}
else if (name == "moveToTrash") {
NSString *sourcePath = stringFromCefV8Value(arguments[0]);
bool success = [[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation

View File

@ -15,6 +15,7 @@
"async": "0.2.6",
"nak": "0.2.12",
"spellchecker": "0.2.0",
"pathwatcher": "0.1.5",
"plist": "git://github.com/nathansobo/node-plist.git",
"space-pen": "git://github.com/nathansobo/space-pen.git"
},

View File

@ -2,4 +2,4 @@
set -ex
rm -rf ~/.atom
CI_BUILD=true rake clean test
rake clean test

View File

@ -13,9 +13,7 @@ else
TARGET=$1
fi
DISTURL="https://gh-contractor-zcbenz.s3.amazonaws.com/cefode2/prebuilt-cef"
CEF_BASENAME="cef_binary_3.1423.1133_macosx"
CEF_SYMBOLS_BASENAME="${CEF_BASENAME}_symbols"
DISTURL="https://gh-contractor-zcbenz.s3.amazonaws.com/cefode3/prebuilt-cef"
TEMP_DIR=$(mktemp -d -t prebuilt-cef-download.XXXXXX)
trap "rm -rf \"${TEMP_DIR}\"" EXIT
@ -29,8 +27,8 @@ fi
CURRENT_VERSION=`cat cef/version 2>&1`
if [[ $LATEST_VERSION != $CURRENT_VERSION ]]; then
echo "Downloading/extracting cefode2 u${LATEST_VERSION}..."
if [ -z "$CI_BUILD" ]; then
echo "Downloading/extracting cefode3 u${LATEST_VERSION}..."
if [ -t 1 ] ; then # If run from the terminal
CURL_ARGS="--progress-bar"
else
CURL_ARGS="-fsS"
@ -38,7 +36,7 @@ if [[ $LATEST_VERSION != $CURRENT_VERSION ]]; then
curl $CURL_ARGS "${DISTURL}/cef_binary_latest.zip" > "${TEMP_DIR}/cef.zip"
unzip -q "${TEMP_DIR}/cef.zip" -d "${TEMP_DIR}"
[ -e "${TARGET}" ] && rm -rf "${TARGET}"
mv "${TEMP_DIR}/${CEF_BASENAME}" "${TARGET}"
mv "${TEMP_DIR}"/*_macosx "${TARGET}"
echo ${LATEST_VERSION} > 'cef/version'
fi
@ -46,7 +44,7 @@ if [[ "${SYMBOLS}" != "1" ]]; then
exit 0
fi
echo "Downloading/extracting symbols for cefode2 u${LATEST_VERSION}..."
echo "Downloading/extracting symbols for cefode3 u${LATEST_VERSION}..."
curl --progress-bar "${DISTURL}/cef_binary_latest_symbols.zip" > "${TEMP_DIR}/symbols.zip"
unzip -q "${TEMP_DIR}/symbols.zip" -d "${TEMP_DIR}"
mv "${TEMP_DIR}/${CEF_SYMBOLS_BASENAME}"/* "${TARGET}/Release"
mv "${TEMP_DIR}"/*_macosx_symbols/* "${TARGET}/Release"

View File

@ -708,6 +708,168 @@ describe "EditSession", ->
expect(editSession.selectMarker('bogus')).toBeFalsy()
expect(editSession.getSelectedBufferRange()).toEqual rangeBefore
describe ".addSelectionBelow()", ->
describe "when the selection is non-empty", ->
it "selects the same region of the line below current selections if possible", ->
editSession.setSelectedBufferRange([[3, 16], [3, 21]])
editSession.addSelectionForBufferRange([[3, 25], [3, 34]])
editSession.addSelectionBelow()
expect(editSession.getSelectedBufferRanges()).toEqual [
[[3, 16], [3, 21]]
[[3, 25], [3, 34]]
[[4, 16], [4, 21]]
[[4, 25], [4, 29]]
]
for cursor in editSession.getCursors()
expect(cursor.isVisible()).toBeFalsy()
it "skips lines that are too short to create a non-empty selection", ->
editSession.setSelectedBufferRange([[3, 31], [3, 38]])
editSession.addSelectionBelow()
expect(editSession.getSelectedBufferRanges()).toEqual [
[[3, 31], [3, 38]]
[[6, 31], [6, 38]]
]
it "honors the original selection's range (goal range) when adding across shorter lines", ->
editSession.setSelectedBufferRange([[3, 22], [3, 38]])
editSession.addSelectionBelow()
editSession.addSelectionBelow()
editSession.addSelectionBelow()
expect(editSession.getSelectedBufferRanges()).toEqual [
[[3, 22], [3, 38]]
[[4, 22], [4, 29]]
[[5, 22], [5, 30]]
[[6, 22], [6, 38]]
]
it "clears selection goal ranges when the selection changes", ->
editSession.setSelectedBufferRange([[3, 22], [3, 38]])
editSession.addSelectionBelow()
editSession.selectLeft()
editSession.addSelectionBelow()
expect(editSession.getSelectedBufferRanges()).toEqual [
[[3, 22], [3, 37]]
[[4, 22], [4, 29]]
[[5, 22], [5, 28]]
]
# goal range from previous add selection is honored next time
editSession.addSelectionBelow()
expect(editSession.getSelectedBufferRanges()).toEqual [
[[3, 22], [3, 37]]
[[4, 22], [4, 29]]
[[5, 22], [5, 30]] # select to end of line 5 because line 4's goal range was reset by line 3 previously
[[6, 22], [6, 28]]
]
describe "when the selection is empty", ->
it "does not skip lines that are shorter than the current column", ->
editSession.setCursorBufferPosition([3, 36])
editSession.addSelectionBelow()
editSession.addSelectionBelow()
editSession.addSelectionBelow()
expect(editSession.getSelectedBufferRanges()).toEqual [
[[3, 36], [3, 36]]
[[4, 29], [4, 29]]
[[5, 30], [5, 30]]
[[6, 36], [6, 36]]
]
it "skips empty lines when the column is non-zero", ->
editSession.setCursorBufferPosition([9, 4])
editSession.addSelectionBelow()
expect(editSession.getSelectedBufferRanges()).toEqual [
[[9, 4], [9, 4]]
[[11, 4], [11, 4]]
]
it "does not skip empty lines when the column is zero", ->
editSession.setCursorBufferPosition([9, 0])
editSession.addSelectionBelow()
expect(editSession.getSelectedBufferRanges()).toEqual [
[[9, 0], [9, 0]]
[[10, 0], [10, 0]]
]
describe ".addSelectionAbove()", ->
describe "when the selection is non-empty", ->
it "selects the same region of the line above current selections if possible", ->
editSession.setSelectedBufferRange([[3, 16], [3, 21]])
editSession.addSelectionForBufferRange([[3, 37], [3, 44]])
editSession.addSelectionAbove()
expect(editSession.getSelectedBufferRanges()).toEqual [
[[2, 16], [2, 21]]
[[2, 37], [2, 40]]
[[3, 16], [3, 21]]
[[3, 37], [3, 44]]
]
for cursor in editSession.getCursors()
expect(cursor.isVisible()).toBeFalsy()
it "skips lines that are too short to create a non-empty selection", ->
editSession.setSelectedBufferRange([[6, 31], [6, 38]])
editSession.addSelectionAbove()
expect(editSession.getSelectedBufferRanges()).toEqual [
[[3, 31], [3, 38]]
[[6, 31], [6, 38]]
]
it "honors the original selection's range (goal range) when adding across shorter lines", ->
editSession.setSelectedBufferRange([[6, 22], [6, 38]])
editSession.addSelectionAbove()
editSession.addSelectionAbove()
editSession.addSelectionAbove()
expect(editSession.getSelectedBufferRanges()).toEqual [
[[3, 22], [3, 38]]
[[4, 22], [4, 29]]
[[5, 22], [5, 30]]
[[6, 22], [6, 38]]
]
describe "when the selection is empty", ->
it "does not skip lines that are shorter than the current column", ->
editSession.setCursorBufferPosition([6, 36])
editSession.addSelectionAbove()
editSession.addSelectionAbove()
editSession.addSelectionAbove()
expect(editSession.getSelectedBufferRanges()).toEqual [
[[3, 36], [3, 36]]
[[4, 29], [4, 29]]
[[5, 30], [5, 30]]
[[6, 36], [6, 36]]
]
it "skips empty lines when the column is non-zero", ->
editSession.setCursorBufferPosition([11, 4])
editSession.addSelectionAbove()
expect(editSession.getSelectedBufferRanges()).toEqual [
[[9, 4], [9, 4]]
[[11, 4], [11, 4]]
]
it "does not skip empty lines when the column is zero", ->
editSession.setCursorBufferPosition([10, 0])
editSession.addSelectionAbove()
expect(editSession.getSelectedBufferRanges()).toEqual [
[[9, 0], [9, 0]]
[[10, 0], [10, 0]]
]
describe ".consolidateSelections()", ->
it "destroys all selections but the most recent, returning true if any selections were destroyed", ->
editSession.setSelectedBufferRange([[3, 16], [3, 21]])
selection1 = editSession.getSelection()
selection2 = editSession.addSelectionForBufferRange([[3, 25], [3, 34]])
selection3 = editSession.addSelectionForBufferRange([[8, 4], [8, 10]])
expect(editSession.getSelections()).toEqual [selection1, selection2, selection3]
expect(editSession.consolidateSelections()).toBeTruthy()
expect(editSession.getSelections()).toEqual [selection3]
expect(selection3.isEmpty()).toBeFalsy()
expect(editSession.consolidateSelections()).toBeFalsy()
expect(editSession.getSelections()).toEqual [selection3]
describe "when the cursor is moved while there is a selection", ->
makeSelection = -> selection.setBufferRange [[1, 2], [1, 5]]

View File

@ -765,6 +765,18 @@ describe "Editor", ->
expect(editor.getSelectionViews().length).toBe 1
expect(editor.find('.region').length).toBe 3
describe "when a selection is added and removed before the display is updated", ->
it "does not attempt to render the selection", ->
# don't update display until we request it
jasmine.unspy(editor, 'requestDisplayUpdate')
spyOn(editor, 'requestDisplayUpdate')
editSession = editor.activeEditSession
selection = editSession.addSelectionForBufferRange([[3, 0], [3, 4]])
selection.destroy()
editor.updateDisplay()
expect(editor.getSelectionViews().length).toBe 1
describe "when the selection is created with the selectAll event", ->
it "does not scroll to the end of the buffer", ->
editor.height(150)
@ -2460,3 +2472,19 @@ describe "Editor", ->
expect(fsUtils.write).toHaveBeenCalled()
expect(fsUtils.write.argsForCall[0][0]).toBe '/tmp/state'
expect(typeof fsUtils.write.argsForCall[0][1]).toBe 'string'
describe "when the escape key is pressed on the editor", ->
it "clears multiple selections if there are any, and otherwise allows other bindings to be handled", ->
keymap.bindKeys '.editor', 'escape': 'test-event'
testEventHandler = jasmine.createSpy("testEventHandler")
editor.on 'test-event', testEventHandler
editor.activeEditSession.addSelectionForBufferRange([[3, 0], [3, 0]])
expect(editor.activeEditSession.getSelections().length).toBe 2
editor.trigger(keydownEvent('escape'))
expect(editor.activeEditSession.getSelections().length).toBe 1
expect(testEventHandler).not.toHaveBeenCalled()
editor.trigger(keydownEvent('escape'))
expect(testEventHandler).toHaveBeenCalled()

View File

@ -54,6 +54,7 @@ describe 'File', ->
waitsFor "remove event", (done) -> file.on 'removed', done
it "it updates its path", ->
jasmine.unspy(window, "setTimeout")
moveHandler = null
moveHandler = jasmine.createSpy('moveHandler')
file.on 'moved', moveHandler
@ -67,6 +68,7 @@ describe 'File', ->
expect(file.getPath()).toBe newPath
it "maintains 'contents-changed' events set on previous path", ->
jasmine.unspy(window, "setTimeout")
moveHandler = null
moveHandler = jasmine.createSpy('moveHandler')
file.on 'moved', moveHandler

View File

@ -113,7 +113,8 @@ describe "Keymap", ->
describe "when the matching selectors differ in specificity", ->
it "triggers the binding for the most specific selector", ->
keymap.bindKeys 'div .child-node', 'x': 'foo'
keymap.bindKeys '.command-mode .child-node', 'x': 'baz'
keymap.bindKeys '.command-mode .child-node !important', 'x': 'baz'
keymap.bindKeys '.command-mode .child-node', 'x': 'quux'
keymap.bindKeys '.child-node', 'x': 'bar'
fooHandler = jasmine.createSpy 'fooHandler'

View File

@ -259,6 +259,32 @@ describe "Project", ->
match: 'aa'
range: [[1, 3], [1, 5]]
describe "when the core.excludeVcsIgnoredPaths config is truthy", ->
[projectPath, ignoredPath] = []
beforeEach ->
projectPath = fsUtils.resolveOnLoadPath('fixtures/git/working-dir')
ignoredPath = fsUtils.join(projectPath, 'ignored.txt')
fsUtils.write(ignoredPath, 'this match should not be included')
afterEach ->
fsUtils.remove(ignoredPath) if fsUtils.exists(ignoredPath)
it "excludes ignored files", ->
project.setPath(projectPath)
config.set('core.excludeVcsIgnoredPaths', true)
paths = []
matches = []
waitsForPromise ->
project.scan /match/, ({path, match, range}) ->
paths.push(path)
matches.push(match)
runs ->
expect(paths.length).toBe 0
expect(matches.length).toBe 0
describe "serialization", ->
it "restores the project path", ->
newProject = Project.deserialize(project.serialize())

View File

@ -32,10 +32,12 @@ describe 'Buffer', ->
expect(buffer.undoManager.undoHistory.length).toBe 0
describe "when no file exists for the path", ->
it "throws an exception", ->
it "is modified and is initially empty", ->
filePath = "does-not-exist.txt"
expect(fsUtils.exists(filePath)).toBeFalsy()
expect(-> project.bufferForPath(filePath)).toThrow()
buffer = project.bufferForPath(filePath)
expect(buffer.isModified()).toBeTruthy()
expect(buffer.getText()).toBe ''
describe "when no path is given", ->
it "creates an empty buffer", ->
@ -63,6 +65,8 @@ describe 'Buffer', ->
expect(eventHandler).toHaveBeenCalledWith(bufferToChange)
it "triggers a `path-changed` event when the file is moved", ->
jasmine.unspy(window, "setTimeout")
fsUtils.remove(newPath) if fsUtils.exists(newPath)
fsUtils.move(path, newPath)
@ -264,18 +268,44 @@ describe 'Buffer', ->
expect(modifiedHandler).toHaveBeenCalledWith(true)
expect(buffer.isModified()).toBe true
it "reports the modified status changing to false after a buffer to a non-existent file is saved", ->
filePath = "/tmp/atom-tmp-file"
fsUtils.remove(filePath) if fsUtils.exists(filePath)
expect(fsUtils.exists(filePath)).toBeFalsy()
buffer.release()
buffer = project.bufferForPath(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.save()
expect(fsUtils.exists(filePath)).toBeTruthy()
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()
buffer = project.bufferForPath(null)
expect(buffer.isModified()).toBeFalsy()
it "returns true for a non-empty buffer with no path", ->
buffer.release()
buffer = project.bufferForPath(null)
buffer.setText('a')
expect(buffer.isModified()).toBeTruthy()
buffer.setText('\n')
expect(buffer.isModified()).toBeTruthy()
buffer.release()
buffer = project.bufferForPath(null)
buffer.setText('a')
expect(buffer.isModified()).toBeTruthy()
buffer.setText('\n')
expect(buffer.isModified()).toBeTruthy()
describe ".getLines()", ->
it "returns an array of lines in the text contents", ->

View File

@ -13,6 +13,7 @@ File = require 'file'
Editor = require 'editor'
TokenizedBuffer = require 'tokenized-buffer'
fsUtils = require 'fs-utils'
pathwatcher = require 'pathwatcher'
RootView = require 'root-view'
Git = require 'git'
requireStylesheet "jasmine"
@ -95,8 +96,8 @@ afterEach ->
waits(0) # yield to ui thread to make screen update more frequently
ensureNoPathSubscriptions = ->
watchedPaths = $native.getWatchedPaths()
$native.unwatchAllPaths()
watchedPaths = pathwatcher.getWatchedPaths()
pathwatcher.closeAllWatchers()
if watchedPaths.length > 0
throw new Error("Leaking subscriptions for paths: " + watchedPaths.join(", "))

View File

@ -1,7 +1,6 @@
fsUtils = require 'fs-utils'
_ = require 'underscore'
Package = require 'package'
TextMatePackage = require 'text-mate-package'
Theme = require 'theme'
messageIdCounter = 1
@ -127,6 +126,9 @@ _.extend atom,
newWindow: (args...) ->
@sendMessageToBrowserProcess('newWindow', args)
restartRendererProcess: ->
@sendMessageToBrowserProcess('restartRendererProcess')
confirm: (message, detailedMessage, buttonLabelsAndCallbacks...) ->
wrapCallback = (callback) => => @dismissModal(callback)
@presentModal =>
@ -208,9 +210,13 @@ _.extend atom,
originalSendMessageToBrowserProcess(name, data)
receiveMessageFromBrowserProcess: (name, data) ->
if name is 'reply'
[messageId, callbackIndex] = data.shift()
@pendingBrowserProcessCallbacks[messageId]?[callbackIndex]?(data...)
switch name
when 'reply'
[messageId, callbackIndex] = data.shift()
@pendingBrowserProcessCallbacks[messageId]?[callbackIndex]?(data...)
when 'openPath'
path = data[0]
rootView?.open(path)
setWindowState: (keyPath, value) ->
windowState = @getWindowState()

View File

@ -16,9 +16,10 @@ class BindingSet
parser: null
name: null
constructor: (@selector, commandsByKeystrokes, @index, @name) ->
constructor: (selector, commandsByKeystrokes, @index, @name) ->
BindingSet.parser ?= PEG.buildParser(fsUtils.read(require.resolve 'keystroke-pattern.pegjs'))
@specificity = Specificity(@selector)
@specificity = Specificity(selector)
@selector = selector.replace(/!important/g, '')
@commandsByKeystrokes = @normalizeCommandsByKeystrokes(commandsByKeystrokes)
commandForEvent: (event) ->

View File

@ -27,6 +27,9 @@ class BufferMarker
isReversed: ->
@tailPosition? and @headPosition.isLessThan(@tailPosition)
isRangeEmpty: ->
@getHeadPosition().isEqual(@getTailPosition())
getRange: ->
if @tailPosition
new Range(@tailPosition, @headPosition)

View File

@ -12,9 +12,9 @@ class Cursor
needsAutoscroll: null
constructor: ({@editSession, @marker}) ->
@updateVisibility()
@editSession.observeMarker @marker, (e) =>
@setVisible(@selection.isEmpty())
@updateVisibility()
{oldHeadScreenPosition, newHeadScreenPosition} = e
{oldHeadBufferPosition, newHeadBufferPosition} = e
{bufferChanged} = e
@ -34,6 +34,7 @@ class Cursor
@needsAutoscroll = true
destroy: ->
@destroyed = true
@editSession.destroyMarker(@marker)
@editSession.removeCursor(this)
@trigger 'destroyed'
@ -59,6 +60,9 @@ class Cursor
unless fn()
@trigger 'autoscrolled' if @needsAutoscroll
updateVisibility: ->
@setVisible(@editSession.isMarkerRangeEmpty(@marker))
setVisible: (visible) ->
if @visible != visible
@visible = visible
@ -84,6 +88,7 @@ class Cursor
clearSelection: ->
if @selection
@selection.goalBufferRange = null
@selection.clear() unless @selection.retainSelection
getScreenRow: ->

View File

@ -1,6 +1,7 @@
_ = require 'underscore'
fs = require 'fs'
fsUtils = require 'fs-utils'
pathWatcher = require 'pathwatcher'
File = require 'file'
EventEmitter = require 'event-emitter'
@ -39,12 +40,12 @@ class Directory
@unsubscribeFromNativeChangeEvents() if @subscriptionCount() == 0
subscribeToNativeChangeEvents: ->
@watchSubscription = fsUtils.watchPath @path, (eventType) =>
@trigger "contents-changed" if eventType is "contents-change"
@watchSubscription = pathWatcher.watch @path, (eventType) =>
@trigger "contents-changed" if eventType is "change"
unsubscribeFromNativeChangeEvents: ->
if @watchSubscription?
@watchSubscription.unwatch()
@watchSubscription.close()
@watchSubscription = null
_.extend Directory.prototype, EventEmitter

View File

@ -400,6 +400,9 @@ class DisplayBuffer
isMarkerReversed: (id) ->
@buffer.isMarkerReversed(id)
isMarkerRangeEmpty: (id) ->
@buffer.isMarkerRangeEmpty(id)
observeMarker: (id, callback) ->
@getMarker(id).observe(callback)

View File

@ -125,8 +125,8 @@ class EditSession
getTabLength: -> @displayBuffer.getTabLength()
setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength)
clipBufferPosition: (bufferPosition) ->
@buffer.clipPosition(bufferPosition)
clipBufferPosition: (bufferPosition) -> @buffer.clipPosition(bufferPosition)
clipBufferRange: (range) -> @buffer.clipRange(range)
indentationForBufferRow: (bufferRow) ->
@indentLevelForLine(@lineForBufferRow(bufferRow))
@ -556,6 +556,9 @@ class EditSession
isMarkerReversed: (args...) ->
@displayBuffer.isMarkerReversed(args...)
isMarkerRangeEmpty: (args...) ->
@displayBuffer.isMarkerRangeEmpty(args...)
hasMultipleCursors: ->
@getCursors().length > 1
@ -585,10 +588,10 @@ class EditSession
unless options.preserveFolds
@destroyFoldsIntersectingBufferRange(@getMarkerBufferRange(marker))
cursor = @addCursor(marker)
selection = new Selection({editSession: this, marker, cursor})
selection = new Selection(_.extend({editSession: this, marker, cursor}, options))
@selections.push(selection)
selectionBufferRange = selection.getBufferRange()
@mergeIntersectingSelections()
@mergeIntersectingSelections() unless options.suppressMerge
if selection.destroyed
for selection in @getSelections()
if selection.intersectsBufferRange(selectionBufferRange)
@ -600,7 +603,7 @@ class EditSession
addSelectionForBufferRange: (bufferRange, options={}) ->
options = _.defaults({invalidationStrategy: 'never'}, options)
marker = @markBufferRange(bufferRange, options)
@addSelection(marker)
@addSelection(marker, options)
setSelectedBufferRange: (bufferRange, options) ->
@setSelectedBufferRanges([bufferRange], options)
@ -623,13 +626,16 @@ class EditSession
_.remove(@selections, selection)
clearSelections: ->
lastSelection = @getLastSelection()
for selection in @getSelections() when selection != lastSelection
selection.destroy()
lastSelection.clear()
@consolidateSelections()
@getSelection().clear()
clearAllSelections: ->
selection.destroy() for selection in @getSelections()
consolidateSelections: ->
selections = @getSelections()
if selections.length > 1
selection.destroy() for selection in selections[0...-1]
true
else
false
getSelections: -> new Array(@selections...)
@ -761,6 +767,12 @@ class EditSession
selectLine: ->
@expandSelectionsForward (selection) => selection.selectLine()
addSelectionBelow: ->
@expandSelectionsForward (selection) => selection.addSelectionBelow()
addSelectionAbove: ->
@expandSelectionsBackward (selection) => selection.addSelectionAbove()
transpose: ->
@mutateSelectedText (selection) =>
if selection.isEmpty()

View File

@ -60,7 +60,7 @@ class Editor extends View
if editSessionOrOptions instanceof EditSession
editSession = editSessionOrOptions
else
{editSession, @mini} = (editSessionOrOptions ? {})
{editSession, @mini} = editSessionOrOptions ? {}
requireStylesheet 'editor'
@ -104,6 +104,7 @@ class Editor extends View
'editor:move-to-previous-word': @moveCursorToPreviousWord
'editor:select-word': @selectWord
'editor:newline': @insertNewline
'editor:consolidate-selections': @consolidateSelections
'editor:indent': @indent
'editor:auto-indent': @autoIndent
'editor:indent-selected-rows': @indentSelectedRows
@ -124,6 +125,8 @@ class Editor extends View
'editor:select-to-end-of-word': @selectToEndOfWord
'editor:select-to-beginning-of-word': @selectToBeginningOfWord
'editor:select-to-beginning-of-next-word': @selectToBeginningOfNextWord
'editor:add-selection-below': @addSelectionBelow
'editor:add-selection-above': @addSelectionAbove
'editor:select-line': @selectLine
'editor:transpose': @transpose
'editor:upper-case': @upperCase
@ -165,7 +168,7 @@ class Editor extends View
documentation = {}
for name, method of editorBindings
do (name, method) =>
@command name, => method.call(this); false
@command name, (e) => method.call(this, e); false
getCursor: -> @activeEditSession.getCursor()
getCursors: -> @activeEditSession.getCursors()
@ -214,6 +217,8 @@ class Editor extends View
selectAll: -> @activeEditSession.selectAll()
selectToBeginningOfLine: -> @activeEditSession.selectToBeginningOfLine()
selectToEndOfLine: -> @activeEditSession.selectToEndOfLine()
addSelectionBelow: -> @activeEditSession.addSelectionBelow()
addSelectionAbove: -> @activeEditSession.addSelectionAbove()
selectToBeginningOfWord: -> @activeEditSession.selectToBeginningOfWord()
selectToEndOfWord: -> @activeEditSession.selectToEndOfWord()
selectToBeginningOfNextWord: -> @activeEditSession.selectToEndOfWord(); @activeEditSession.selectRight()
@ -234,6 +239,7 @@ class Editor extends View
cutToEndOfLine: -> @activeEditSession.cutToEndOfLine()
insertText: (text, options) -> @activeEditSession.insertText(text, options)
insertNewline: -> @activeEditSession.insertNewline()
consolidateSelections: (e) -> e.abortKeyBinding() unless @activeEditSession.consolidateSelections()
insertNewlineBelow: -> @activeEditSession.insertNewlineBelow()
insertNewlineAbove: -> @activeEditSession.insertNewlineAbove()
indent: (options) -> @activeEditSession.indent(options)
@ -782,7 +788,7 @@ class Editor extends View
updateCursorViews: ->
if @newCursors.length > 0
@addCursorView(cursor) for cursor in @newCursors
@addCursorView(cursor) for cursor in @newCursors when not cursor.destroyed
@syncCursorAnimations()
@newCursors = []
@ -794,11 +800,11 @@ class Editor extends View
updateSelectionViews: ->
if @newSelections.length > 0
@addSelectionView(selection) for selection in @newSelections
@addSelectionView(selection) for selection in @newSelections when not selection.destroyed
@newSelections = []
for selectionView in @getSelectionViews()
if selectionView.destroyed
if selectionView.needsRemoval
selectionView.remove()
else
selectionView.updateDisplay()

View File

@ -2,6 +2,7 @@ EventEmitter = require 'event-emitter'
fs = require 'fs'
fsUtils = require 'fs-utils'
pathWatcher = require 'pathwatcher'
_ = require 'underscore'
module.exports =
@ -45,12 +46,13 @@ class File
@unsubscribeFromNativeChangeEvents() if @subscriptionCount() == 0
handleNativeChangeEvent: (eventType, path) ->
if eventType is "remove"
if eventType is "delete"
@unsubscribeFromNativeChangeEvents()
@detectResurrectionAfterDelay()
else if eventType is "move"
else if eventType is "rename"
@setPath(path)
@trigger "moved"
else if eventType is "contents-change"
else if eventType is "change"
oldContents = @read()
newContents = @read(true)
return if oldContents == newContents
@ -62,19 +64,18 @@ class File
detectResurrection: ->
if @exists()
@subscribeToNativeChangeEvents()
@handleNativeChangeEvent("contents-change", @getPath())
@handleNativeChangeEvent("change", @getPath())
else
@cachedContents = null
@unsubscribeFromNativeChangeEvents()
@trigger "removed"
subscribeToNativeChangeEvents: ->
@watchSubscription = fsUtils.watchPath @path, (eventType, path) =>
@watchSubscription = pathWatcher.watch @path, (eventType, path) =>
@handleNativeChangeEvent(eventType, path)
unsubscribeFromNativeChangeEvents: ->
if @watchSubscription
@watchSubscription.unwatch()
@watchSubscription.close()
@watchSubscription = null
_.extend File.prototype, EventEmitter

View File

@ -9,6 +9,8 @@
'ctrl-]': 'editor:unfold-current-row'
'ctrl-{': 'editor:fold-all'
'ctrl-}': 'editor:unfold-all'
'alt-shift-down': 'editor:add-selection-below'
'alt-shift-up': 'editor:add-selection-above'
'alt-meta-ctrl-f': 'editor:fold-selection'
'shift-tab': 'editor:outdent-selected-rows'
'meta-[': 'editor:outdent-selected-rows'
@ -30,3 +32,6 @@
'enter': 'core:confirm',
'escape': 'core:cancel'
'meta-w': 'core:cancel'
'.editor !important, .editor.mini !important':
'escape': 'editor:consolidate-selections'

View File

@ -7,6 +7,8 @@
'ctrl-N': 'core:select-down'
'ctrl-F': 'core:select-right'
'ctrl-B': 'core:select-left'
'alt-ctrl-n': 'editor:add-selection-below'
'alt-ctrl-p': 'editor:add-selection-above'
'ctrl-h': 'core:backspace'
'ctrl-d': 'core:delete'

View File

@ -97,11 +97,8 @@ class RootView extends View
changeFocus = options.changeFocus ? true
path = project.resolve(path) if path?
if activePane = @getActivePane()
if editSession = activePane.itemForUri(path)
activePane.showItem(editSession)
else
editSession = project.buildEditSession(path)
activePane.showItem(editSession)
editSession = activePane.itemForUri(path) ? project.buildEditSession(path)
activePane.showItem(editSession)
else
editSession = project.buildEditSession(path)
activePane = new Pane(editSession)

View File

@ -8,13 +8,13 @@ class SelectionView extends View
@div class: 'selection'
regions: null
destroyed: false
needsRemoval: false
initialize: ({@editor, @selection} = {}) ->
@regions = []
@selection.on 'screen-range-changed', => @editor.requestDisplayUpdate()
@selection.on 'destroyed', =>
@destroyed = true
@needsRemoval = true
@editor.requestDisplayUpdate()
updateDisplay: ->

View File

@ -4,11 +4,15 @@ _ = require 'underscore'
module.exports =
class Selection
wordwise: false
cursor: null
marker: null
editSession: null
initialScreenRange: null
goalBufferRange: null
wordwise: false
needsAutoscroll: null
constructor: ({@cursor, @marker, @editSession}) ->
constructor: ({@cursor, @marker, @editSession, @goalBufferRange}) ->
@cursor.selection = this
@editSession.observeMarker @marker, => @screenRangeChanged()
@cursor.on 'destroyed.selection', =>
@ -148,6 +152,40 @@ class Selection
selectToEndOfWord: ->
@modifySelection => @cursor.moveToEndOfWord()
addSelectionBelow: ->
range = (@goalBufferRange ? @getBufferRange()).copy()
nextRow = range.end.row + 1
for row in [nextRow..@editSession.getLastBufferRow()]
range.start.row = row
range.end.row = row
clippedRange = @editSession.clipBufferRange(range)
if range.isEmpty()
continue if range.end.column > 0 and clippedRange.end.column is 0
else
continue if clippedRange.isEmpty()
@editSession.addSelectionForBufferRange(range, goalBufferRange: range, suppressMerge: true)
break
addSelectionAbove: ->
range = (@goalBufferRange ? @getBufferRange()).copy()
previousRow = range.end.row - 1
for row in [previousRow..0]
range.start.row = row
range.end.row = row
clippedRange = @editSession.clipBufferRange(range)
if range.isEmpty()
continue if range.end.column > 0 and clippedRange.end.column is 0
else
continue if clippedRange.isEmpty()
@editSession.addSelectionForBufferRange(range, goalBufferRange: range, suppressMerge: true)
break
insertText: (text, options={}) ->
oldBufferRange = @getBufferRange()
@editSession.destroyFoldsContainingBufferRow(oldBufferRange.end.row)
@ -369,10 +407,14 @@ class Selection
@getBufferRange().intersectsWith(bufferRange)
intersectsWith: (otherSelection) ->
@getScreenRange().intersectsWith(otherSelection.getScreenRange())
@getBufferRange().intersectsWith(otherSelection.getBufferRange())
merge: (otherSelection, options) ->
@setBufferRange(@getBufferRange().union(otherSelection.getBufferRange()), options)
if @goalBufferRange and otherSelection.goalBufferRange
@goalBufferRange = @goalBufferRange.union(otherSelection.goalBufferRange)
else if otherSelection.goalBufferRange
@goalBufferRange = otherSelection.goalBufferRange
otherSelection.destroy()
_.extend Selection.prototype, EventEmitter

View File

@ -37,17 +37,17 @@ class Buffer
@lineEndings = []
if path
throw "Path '#{path}' does not exist" unless fsUtils.exists(path)
@setPath(path)
if initialText?
@setText(initialText)
@updateCachedDiskContents()
else
else if fsUtils.exists(path)
@reload()
else
@setText('')
else
@setText(initialText ? '')
@undoManager = new UndoManager(this)
destroy: ->
@ -340,6 +340,9 @@ class Buffer
isMarkerReversed: (id) ->
@validMarkers[id]?.isReversed()
isMarkerRangeEmpty: (id) ->
@validMarkers[id]?.isRangeEmpty()
observeMarker: (id, callback) ->
@validMarkers[id]?.observe(callback)

View File

@ -25,6 +25,12 @@ window.setUpEnvironment = ->
$(document).on 'keydown', keymap.handleKeyEvent
keymap.bindDefaultKeys()
ignoreEvents = (e) ->
e.preventDefault()
e.stopPropagation()
$(document).on 'dragover', ignoreEvents
$(document).on 'drop', ignoreEvents
requireStylesheet 'reset'
requireStylesheet 'atom'
requireStylesheet 'overlay'
@ -148,7 +154,16 @@ window.applyStylesheet = (id, text, ttype = 'bundled') ->
$("head").append "<style class='#{ttype}' id='#{id}'>#{text}</style>"
window.reload = ->
$native.reload()
timesReloaded = process.global.timesReloaded ? 0
++timesReloaded
restartValue = if window.location.search.indexOf('spec-bootstrap') == -1 then 10 else 3
if timesReloaded > restartValue
atom.restartRendererProcess()
else
$native.reload()
process.global.timesReloaded = timesReloaded
window.onerror = ->
atom.showDevTools()

View File

@ -42,6 +42,7 @@ module.exports =
goToMatchingPair: (editor) ->
return unless @pairHighlighted
return unless underlayer = editor.getPane()?.find('.underlayer')
return unless underlayer.isVisible()
position = editor.getCursorBufferPosition()
previousPosition = position.translate([0, -1])

View File

@ -34,8 +34,12 @@
'name': 'markup.heading.gfm'
}
{
'match': '\\:[^\\:\\s]+\\:'
'match': '(\\:)([^\\:\\s]+)(\\:)'
'name': 'string.emoji.gfm'
'captures':
'1': 'name': 'string.emoji.start.gfm'
'2': 'name': 'string.emoji.word.gfm'
'3': 'name': 'string.emoji.end.gfm'
}
{
'match': '^\\s*[\\*]{3,}\\s*$'

View File

@ -78,7 +78,9 @@ describe "GitHub Flavored Markdown grammar", ->
it "tokenizies an :emoji:", ->
{tokens} = grammar.tokenizeLine("this is :no_good:")
expect(tokens[0]).toEqual value: "this is ", scopes: ["source.gfm"]
expect(tokens[1]).toEqual value: ":no_good:", scopes: ["source.gfm", "string.emoji.gfm"]
expect(tokens[1]).toEqual value: ":", scopes: ["source.gfm", "string.emoji.gfm", "string.emoji.start.gfm"]
expect(tokens[2]).toEqual value: "no_good", scopes: ["source.gfm", "string.emoji.gfm", "string.emoji.word.gfm"]
expect(tokens[3]).toEqual value: ":", scopes: ["source.gfm", "string.emoji.gfm", "string.emoji.end.gfm"]
{tokens} = grammar.tokenizeLine("this is :no good:")
expect(tokens[0]).toEqual value: "this is :no good:", scopes: ["source.gfm"]

View File

@ -18,7 +18,7 @@ class SpellCheckView extends View
@subscribeToBuffer()
subscribeToBuffer: ->
unsubscribeFromBuffer: ->
@destroyViews()
@task?.abort()
@ -26,6 +26,9 @@ class SpellCheckView extends View
@buffer.off '.spell-check'
@buffer = null
subscribeToBuffer: ->
@unsubscribeFromBuffer()
if @spellCheckCurrentGrammar()
@buffer = @editor.getBuffer()
@buffer.on 'contents-modified.spell-check', => @updateMisspellings()
@ -47,6 +50,10 @@ class SpellCheckView extends View
@append(view)
updateMisspellings: ->
unless @editor.activeEditSession?
@unsubscribeFromBuffer()
return
@task?.abort()
callback = (misspellings) =>

View File

@ -81,6 +81,8 @@ class TabBarView extends View
event.preventDefault()
return
event.originalEvent.dataTransfer.setData 'atom-event', 'true'
el = $(event.target).closest('.sortable')
el.addClass 'is-dragging'
event.originalEvent.dataTransfer.setData 'sortable-index', el.index()
@ -93,6 +95,11 @@ class TabBarView extends View
@find(".is-dragging").removeClass 'is-dragging'
onDragOver: (event) =>
unless event.originalEvent.dataTransfer.getData('atom-event') is 'true'
event.preventDefault()
event.stopPropagation()
return
event.preventDefault()
currentDropTargetIndex = @find(".is-drop-target").index()
newDropTargetIndex = @getDropTargetIndex(event)
@ -107,6 +114,11 @@ class TabBarView extends View
onDrop: (event) =>
unless event.originalEvent.dataTransfer.getData('atom-event') is 'true'
event.preventDefault()
event.stopPropagation()
return
event.stopPropagation()
@children('.is-drop-target').removeClass 'is-drop-target'
@children('.drop-target-is-after').removeClass 'drop-target-is-after'

View File

@ -278,3 +278,19 @@ describe "TabBarView", ->
expect(pane2.getItems()).toEqual [item2b, item1]
expect(pane2.activeItem).toBe item1
expect(pane2.focus).toHaveBeenCalled()
describe 'when a non-tab is dragged to pane', ->
it 'has no effect', ->
expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "sample.js", "Item 2"]
expect(pane.getItems()).toEqual [item1, editSession1, item2]
expect(pane.activeItem).toBe item2
spyOn(pane, 'focus')
[dragStartEvent, dropEvent] = buildDragEvents(tabBar.tabAtIndex(0), tabBar.tabAtIndex(0))
tabBar.onDrop(dropEvent)
expect(tabBar.getTabs().map (tab) -> tab.text()).toEqual ["Item 1", "sample.js", "Item 2"]
expect(pane.getItems()).toEqual [item1, editSession1, item2]
expect(pane.activeItem).toBe item2
expect(pane.focus).not.toHaveBeenCalled()

View File

@ -30,7 +30,7 @@ class DirectoryView extends View
iconClass = 'submodule-icon'
else
@subscribe git, 'status-changed', (path, status) =>
@updateStatus() if path.substring("#{@getPath()}/") is 0
@updateStatus() if path.indexOf("#{@getPath()}/") is 0
@subscribe git, 'statuses-changed', =>
@updateStatus()
@updateStatus()

View File

@ -312,11 +312,3 @@ module.exports =
cson.readObjectAsync(path, done)
else
@readPlistAsync(path, done)
watchPath: (path, callback) ->
path = @absolute(path)
watchCallback = (eventType, eventPath) =>
path = @absolute(eventPath) if eventType is 'move'
callback(arguments...)
id = $native.watchPath(path, watchCallback)
unwatch: -> $native.unwatchPath(path, id)

View File

@ -34,6 +34,9 @@ $.fn.pageDown = ->
$.fn.isOnDom = ->
@closest(document.body).length is 1
$.fn.isVisible = ->
@is(':visible')
$.fn.containsElement = (element) ->
(element[0].compareDocumentPosition(this[0]) & 8) == 8

View File

@ -11,7 +11,8 @@
}
.tree-view .directory.selected > .header > .name,
.tree-view .selected > .name {
.tree-view .selected > .name,
.tree-view .selected > .header > .disclosure-arrow {
color: #d2d2d2;
}
@ -39,7 +40,9 @@
.tree-view .entry:hover,
.tree-view .directory .header:hover .name,
.tree-view .directory .header:hover .disclosure-arrow {
.tree-view .directory .header:hover .disclosure-arrow,
.tree-view .selected > .directory > .header .disclosure-arrow,
.tree-view .selected > .directory > .header:hover .disclosure-arrow {
color: #ebebeb;
}