// // CappuccinoProjectController.m // XcodeCapp // // Created by Alexandre Wilhelm on 5/7/15. // Copyright (c) 2015 cappuccino-project. All rights reserved. // #import "AppDelegate.h" #import "XCCCappuccinoProjectController.h" #import "XCCCappuccinoProject.h" #import "XCCFSEventLogUtils.h" #import "XCCSourcesFinderOperation.h" #import "XCCPPXOperation.h" #import "XCCSourceProcessingOperation.h" #import "XCCMainController.h" #import "XCCOperationDataView.h" #import "XCCOperationError.h" #import "XCCOperationErrorDataView.h" #import "XCCOperationErrorHeaderDataView.h" #import "XCCTaskLauncher.h" #import "XCCUserDefaults.h" #import "XcodeProjectCloser.h" #import "XCCOperationsViewController.h" #import "XCCErrorsViewController.h" static FSEventStreamCreateFlags const XCCProjectControllerFSEventFlags = kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagWatchRoot | kFSEventStreamCreateFlagIgnoreSelf | kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagFileEvents; typedef NS_ENUM(NSInteger, XCCLineSpecifier) { kLineSpecifierNone, kLineSpecifierColon, kLineSpecifierMinusL, kLineSpecifierPlus }; @interface XCCCappuccinoProjectController () - (void)_handleFSEventsWithPaths:(NSArray *)paths flags:(const FSEventStreamEventFlags[])eventFlags ids:(const FSEventStreamEventId[])eventIds; @end void fsevents_callback(ConstFSEventStreamRef streamRef, void *userData, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[]) { XCCCappuccinoProjectController *controller = (__bridge XCCCappuccinoProjectController *)userData; NSArray *paths = (__bridge NSArray *)eventPaths; [controller _handleFSEventsWithPaths:paths flags:eventFlags ids:eventIds]; } @implementation XCCCappuccinoProjectController @synthesize numberOfErrors = _numberOfErrors; #pragma mark - Init methods - (instancetype)initWithPath:(NSString*)aPath controller:(id)aController { if (self = [super init]) { [self _startListeningToOperationsNotifications]; self.cappuccinoProject = [[XCCCappuccinoProject alloc] initWithPath:aPath]; self.mainXcodeCappController = aController; self->sourceProcessingOperations = [@{} mutableCopy]; self->operationQueue = [((AppDelegate *)[NSApp delegate]) mainOperationQueue]; [self _reinitializeProjectController]; } return self; } - (void)_reinitializeProjectController { [self.cappuccinoProject reinitialize]; [self _reinitializeOperationsCounters]; [self _reinitializeOperationErrors]; [self _prepareXcodeSupport]; } - (void)_reinitializeTaskLauncher { NSArray *binaryPaths = @[]; if ([self.cappuccinoProject.binaryPaths count]) binaryPaths = [self.cappuccinoProject.binaryPaths valueForKeyPath:@"name"]; self->taskLauncher = [[XCCTaskLauncher alloc] initWithEnvironmentPaths:binaryPaths]; if (!self->taskLauncher.isValid) { [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; NSAlert *alert = [[NSAlert alloc] init]; [alert setAlertStyle:NSAlertStyleCritical]; NSString *title = self.cappuccinoProject.nickname; [alert setMessageText:title]; NSArray *informativeTextArray = @[ @"XcodeCapp was unable to find all necessary executables in current environment:", @"Please confirm the following executables are in current $PATH:\n", [self->taskLauncher.executables componentsJoinedByString:@", "] ]; [alert setInformativeText:[informativeTextArray componentsJoinedByString:@"\n"]]; [alert runModal]; [self _reinitializeProjectController]; } } #pragma mark - Project State Management - (void)_startListeningToProject { if (![self _projectPathExists] || self.cappuccinoProject.status == XCCCappuccinoProjectStatusListening) return; NSLog(@"Start to listen project: %@", self.cappuccinoProject.projectPath); [self _synchronizeXcodeSupport]; if (self->taskLauncher.isValid) { [self _startFSEventStream]; // this is needed in order to ensure we get a FS event, so we actually get a valid last event ID. [self->taskLauncher runTaskWithCommand:@"touch" arguments:@[self.cappuccinoProject.settingsPath] returnType:kTaskReturnTypeNone]; self.cappuccinoProject.status = XCCCappuccinoProjectStatusListening; } [self.cappuccinoProject saveSettings]; } - (void)_stopListeningToProject { if (self.cappuccinoProject.status == XCCCappuccinoProjectStatusStopped) return; [self _cancelAllProjectRelatedOperations]; [self _stopFSEventStream]; [self.mainXcodeCappController.errorsViewController cleanProjectErrors:self]; self.cappuccinoProject.status = XCCCappuccinoProjectStatusStopped; [self.cappuccinoProject saveSettings]; } #pragma mark - XcodeSupport Management - (BOOL)_projectPathExists { NSFileManager *fm = [NSFileManager defaultManager]; if (![fm fileExistsAtPath:self.cappuccinoProject.projectPath isDirectory:nil]) { NSAlert *alert = [[NSAlert alloc] init]; [alert setAlertStyle:NSAlertStyleCritical]; [alert setMessageText:@"The project can’t be located"]; NSString *informativeText = [NSString stringWithFormat:@"The project was either moved or deleted outside of the XcodeCapp environment.\nProject will be removed."]; [alert setInformativeText:informativeText]; [alert runModal]; [self.mainXcodeCappController unmanageCappuccinoProjectController:self]; return NO; } return YES; } - (void)_synchronizeXcodeSupport { NSLog(@"Synchronize project: %@", self.cappuccinoProject.projectPath); [self _reinitializeProjectController]; [self _reinitializeTaskLauncher]; if (!self->taskLauncher.isValid) return; [self _synchronizeXcodeSupportFromPath:@""]; } - (void)_prepareXcodeSupport { NSFileManager *fm = [NSFileManager defaultManager]; BOOL projectExists, projectIsDirectory; projectExists = [fm fileExistsAtPath:self.cappuccinoProject.XcodeProjectPath isDirectory:&projectIsDirectory]; BOOL supportExists, supportIsDirectory; supportExists = [fm fileExistsAtPath:self.cappuccinoProject.supportPath isDirectory:&supportIsDirectory]; if (!projectExists || !projectIsDirectory || !supportExists) [self _createXcodeProject]; if (!supportExists || !supportIsDirectory || !projectExists || ![self _isXcodeSupportCompatible]) [self _createXcodeSupportDirectory]; } - (BOOL)_isXcodeSupportCompatible { double appCompatibilityVersion = [[[NSBundle mainBundle] objectForInfoDictionaryKey:XCCCompatibilityVersionKey] doubleValue]; NSNumber *projectCompatibilityVersion = @([self.cappuccinoProject.version intValue]); if (projectCompatibilityVersion == nil) { NSLog(@"No compatibility version in project"); return NO; } NSLog(@"XcodeCapp/project compatibility version: %0.1f/%0.1f", projectCompatibilityVersion.doubleValue, appCompatibilityVersion); return projectCompatibilityVersion.doubleValue >= appCompatibilityVersion; } - (void)_createXcodeProject { [self _removeXcodeProject]; NSFileManager *fm = [NSFileManager defaultManager]; [fm createDirectoryAtPath:self.cappuccinoProject.XcodeProjectPath withIntermediateDirectories:YES attributes:nil error:nil]; NSString *pbxPath = [self.cappuccinoProject.XcodeProjectPath stringByAppendingPathComponent:@"project.pbxproj"]; [fm copyItemAtPath:[[NSBundle mainBundle] pathForResource:@"project" ofType:@"pbxproj"] toPath:pbxPath error:nil]; NSMutableString *content = [NSMutableString stringWithContentsOfFile:pbxPath encoding:NSUTF8StringEncoding error:nil]; [content writeToFile:pbxPath atomically:YES encoding:NSUTF8StringEncoding error:nil]; NSLog(@"Xcode support project created at: %@", self.cappuccinoProject.XcodeProjectPath); } - (void)_removeXcodeProject { NSFileManager *fm = [NSFileManager defaultManager]; if ([fm fileExistsAtPath:self.cappuccinoProject.XcodeProjectPath]) [fm removeItemAtPath:self.cappuccinoProject.XcodeProjectPath error:nil]; } - (void)_createXcodeSupportDirectory { [self _removeXcodeSupportDirectory]; [[NSFileManager defaultManager] createDirectoryAtPath:self.cappuccinoProject.supportPath withIntermediateDirectories:YES attributes:nil error:nil]; [self.cappuccinoProject saveSettings]; NSLog(@".XcodeSupport directory created at: %@", self.cappuccinoProject.supportPath); } - (void)_removeXcodeSupportDirectory { [XcodeProjectCloser closeXcodeProjectForProject:self.cappuccinoProject.projectPath]; NSFileManager *fm = [NSFileManager defaultManager]; if ([fm fileExistsAtPath:self.cappuccinoProject.supportPath]) [fm removeItemAtPath:self.cappuccinoProject.supportPath error:nil]; } - (void)_synchronizeXcodeSupportFromPath:(NSString *)path { self->operationQueue.suspended = YES; [self _cancelCurrentSourceFinderOperation]; [self _scheduleFinderSourceOperationForPath:path]; self->operationQueue.suspended = NO; } - (void)_removeXcodeSupportOrphanFiles { NSArray *subpaths = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.cappuccinoProject.supportPath error:nil]; NSMutableArray *orphanFiles = [@[] mutableCopy]; NSFileManager *fm = [NSFileManager defaultManager]; for (NSString *path in subpaths) { if ([XCCCappuccinoProject isHeaderFile:path] && ![path.lastPathComponent isEqualToString:@"xcc_general_include.h"]) { NSString *sourcePath = [self.cappuccinoProject sourcePathForShadowPath:path]; if (![fm fileExistsAtPath:sourcePath]) [orphanFiles addObject:sourcePath]; } } NSMutableArray *pathsToRemove = [@[] mutableCopy]; for (NSString *sourcePath in orphanFiles) { NSString *shadowBasePath = [self.cappuccinoProject shadowBasePathForProjectSourcePath:sourcePath]; NSString *shadowHeaderPath = [shadowBasePath stringByAppendingPathExtension:@"h"]; NSString *shadowImplementationPath = [shadowBasePath stringByAppendingPathExtension:@"m"]; [fm removeItemAtPath:shadowHeaderPath error:nil]; [fm removeItemAtPath:shadowImplementationPath error:nil]; [self removeOperationErrorsRelatedToSourcePath:sourcePath errorType:XCCDefaultOperationErrorType]; [pathsToRemove addObject:sourcePath]; } [self.mainXcodeCappController.errorsViewController reload]; if (self->pendingPBXOperation) [self->pendingPBXOperation registerPathsToRemoveFromPBX:pathsToRemove]; else [self _schedulePBXOperationWithPathsToAdd:nil pathsToRemove:pathsToRemove]; } - (void)_updateXcodeSupportFilesWithModifiedPaths:(NSArray *)modifiedPaths { NSLog(@"Modified files: %@", modifiedPaths); for (NSString *path in modifiedPaths) { if (![[NSFileManager defaultManager] fileExistsAtPath:path]) continue; [self _scheduleSourceProcessingOperationForPath:path]; } [self _removeXcodeSupportOrphanFiles]; [self _updateOperationsProgress]; } - (void)_updateXcodeSupportFilesWithRenamedDirectories:(NSArray *)directories { NSLog(@"Renamed directories: %@", directories); NSFileManager *fm = [NSFileManager defaultManager]; for (NSString *directory in directories) { if ([directory isEqualToString:self.cappuccinoProject.projectPath]) continue; if ([fm fileExistsAtPath:directory]) { if ([directory hasPrefix:self.cappuccinoProject.projectPath]) [self _synchronizeXcodeSupportFromPath:[self.cappuccinoProject projectRelativePathForPath:directory]]; else { [self _synchronizeXcodeSupportFromPath:@""]; break; } } } [self _removeXcodeSupportOrphanFiles]; } #pragma mark - Notifications - (void)_startListeningToOperationsNotifications { if (self->isListeningToOperationNotifications) return; self->isListeningToOperationNotifications = YES; NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(_didReceiveConversionDidStartNotification:) name:XCCConversionDidStartNotification object:nil]; [center addObserver:self selector:@selector(_didReceiveConversionDidEndNotification:) name:XCCConversionDidEndNotification object:nil]; [center addObserver:self selector:@selector(_didReceiveSourcesFinderOperationDidStartNotification:) name:XCCSourcesFinderOperationDidStartNotification object:nil]; [center addObserver:self selector:@selector(_didReceiveSourcesFinderOperationDidEndNotification:) name:XCCSourcesFinderOperationDidEndNotification object:nil]; [center addObserver:self selector:@selector(_didReceiveNeedSourceToProjectPathMappingNotification:) name:XCCNeedSourceToProjectPathMappingNotification object:nil]; [center addObserver:self selector:@selector(_didReceiveObjj2ObjcSkeletonDidStartNotification:) name:XCCObjj2ObjcSkeletonDidStartNotification object:nil]; [center addObserver:self selector:@selector(_didReceiveObjj2ObjcSeleketonDidGenerateErrorNotification:) name:XCCObjj2ObjcSkeletonDidGenerateErrorNotification object:nil]; [center addObserver:self selector:@selector(_didReceiveNib2CibDidStartNotifcation:) name:XCCNib2CibDidStartNotification object:nil]; [center addObserver:self selector:@selector(_didReceiveNib2CibDidGenerateErrorNotification:) name:XCCNib2CibDidGenerateErrorNotification object:nil]; [center addObserver:self selector:@selector(_didReceiveObjjDidStartNotification:) name:XCCObjjDidStartNotification object:nil]; [center addObserver:self selector:@selector(_didReceiveObjjDidGenerateErrorNotification:) name:XCCObjjDidGenerateErrorNotification object:nil]; [center addObserver:self selector:@selector(_didReceiveCappLintDidStartNotification:) name:XCCCappLintDidStartNotification object:nil]; [center addObserver:self selector:@selector(_didReceiveCappLintDidGenerateErrorNotification:) name:XCCCappLintDidGenerateErrorNotification object:nil]; [center addObserver:self selector:@selector(_didReceiveUpdatePbxFileDidStartNotification:) name:XCCPBXOperationDidStartNotification object:nil]; [center addObserver:self selector:@selector(_didReceiveUpdatePbxFileDidEndNotification:) name:XCCPBXOperationDidEndNotification object:nil]; } - (void)_stopListeningToOperationsNotifications { if (!self->isListeningToOperationNotifications) return; self->isListeningToOperationNotifications = NO; NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center removeObserver:self name:XCCConversionDidStartNotification object:nil]; [center removeObserver:self name:XCCConversionDidEndNotification object:nil]; [center removeObserver:self name:XCCSourcesFinderOperationDidStartNotification object:nil]; [center removeObserver:self name:XCCSourcesFinderOperationDidEndNotification object:nil]; [center removeObserver:self name:XCCNeedSourceToProjectPathMappingNotification object:nil]; [center removeObserver:self name:XCCObjj2ObjcSkeletonDidStartNotification object:nil]; [center removeObserver:self name:XCCObjj2ObjcSkeletonDidGenerateErrorNotification object:nil]; [center removeObserver:self name:XCCNib2CibDidStartNotification object:nil]; [center removeObserver:self name:XCCNib2CibDidGenerateErrorNotification object:nil]; [center removeObserver:self name:XCCObjjDidStartNotification object:nil]; [center removeObserver:self name:XCCObjjDidGenerateErrorNotification object:nil]; [center removeObserver:self name:XCCCappLintDidStartNotification object:nil]; [center removeObserver:self name:XCCCappLintDidGenerateErrorNotification object:nil]; [center removeObserver:self name:XCCPBXOperationDidStartNotification object:nil]; [center removeObserver:self name:XCCPBXOperationDidEndNotification object:nil]; } - (BOOL)_doesNotificationBelongToCurrentProject:(NSNotification *)note { return note.userInfo[@"cappuccinoProject"] == self.cappuccinoProject; } - (void)_didReceiveConversionDidStartNotification:(NSNotification *)note { if (![self _doesNotificationBelongToCurrentProject:note]) return; [self _schedulePBXOperation]; [self->pendingPBXOperation registerPathsToAddInPBX:@[note.userInfo[@"sourcePath"]]]; [self operationDidStart:note.object type:note.name userInfo:note.userInfo]; } - (void)_didReceiveConversionDidEndNotification:(NSNotification *)note { if (![self _doesNotificationBelongToCurrentProject:note]) return; [self.mainXcodeCappController reloadTotalNumberOfErrors]; [self operationDidEnd:note.object type:note.name userInfo:note.userInfo]; } - (void)_didReceiveSourcesFinderOperationDidStartNotification:(NSNotification *)note { if (![self _doesNotificationBelongToCurrentProject:note]) return; [self operationDidStart:note.object type:note.name userInfo:note.userInfo]; } - (void)_didReceiveSourcesFinderOperationDidEndNotification:(NSNotification *)note { if (![self _doesNotificationBelongToCurrentProject:note]) return; [self operationDidEnd:note.object type:note.name userInfo:note.userInfo]; } - (void)_didReceiveNeedSourceToProjectPathMappingNotification:(NSNotification *)note { if (![self _doesNotificationBelongToCurrentProject:note]) return; self.cappuccinoProject.projectPathsForSourcePaths[note.userInfo[@"sourcePath"]] = note.userInfo[@"projectPath"]; } - (void)_didReceiveObjj2ObjcSkeletonDidStartNotification:(NSNotification *)note { if (![self _doesNotificationBelongToCurrentProject:note]) return; [self removeOperationErrorsRelatedToSourcePath:note.userInfo[@"sourcePath"] errorType:XCCObjj2ObjcSkeletonOperationErrorType]; } - (void)_didReceiveObjj2ObjcSeleketonDidGenerateErrorNotification:(NSNotification *)note { if (![self _doesNotificationBelongToCurrentProject:note]) return; for (XCCOperationError *operationError in [XCCOperationError operationErrorsFromObjj2ObjcSkeletonInfo:note.userInfo]) [self addOperationError:operationError]; } - (void)_didReceiveNib2CibDidStartNotifcation:(NSNotification *)note { if (![self _doesNotificationBelongToCurrentProject:note]) return; [self removeOperationErrorsRelatedToSourcePath:note.userInfo[@"sourcePath"] errorType:XCCNib2CibOperationErrorType]; } - (void)_didReceiveNib2CibDidGenerateErrorNotification:(NSNotification *)note { if (![self _doesNotificationBelongToCurrentProject:note]) return; [self addOperationError:[XCCOperationError operationErrorFromNib2CibInfo:note.userInfo]]; } - (void)_didReceiveObjjDidStartNotification:(NSNotification *)note { if (![self _doesNotificationBelongToCurrentProject:note]) return; [self removeOperationErrorsRelatedToSourcePath:note.userInfo[@"sourcePath"] errorType:XCCObjjOperationErrorType]; } - (void)_didReceiveObjjDidGenerateErrorNotification:(NSNotification *)note { if (![self _doesNotificationBelongToCurrentProject:note]) return; for (XCCOperationError *operationError in [XCCOperationError operationErrorsFromObjjInfo:note.userInfo]) [self addOperationError:operationError]; } - (void)_didReceiveCappLintDidStartNotification:(NSNotification *)note { if (![self _doesNotificationBelongToCurrentProject:note]) return; [self removeOperationErrorsRelatedToSourcePath:note.userInfo[@"sourcePath"] errorType:XCCCappLintOperationErrorType]; } - (void)_didReceiveCappLintDidGenerateErrorNotification:(NSNotification *)note { if (![self _doesNotificationBelongToCurrentProject:note]) return; for (XCCOperationError *operationError in [XCCOperationError operationErrorsFromCappLintInfo:note.userInfo]) [self addOperationError:operationError]; } - (void)_didReceiveUpdatePbxFileDidStartNotification:(NSNotification*)note { if (![self _doesNotificationBelongToCurrentProject:note]) return; [self operationDidStart:note.object type:note.name userInfo:note.userInfo]; } - (void)_didReceiveUpdatePbxFileDidEndNotification:(NSNotification*)note { if (![self _doesNotificationBelongToCurrentProject:note]) return; [self operationDidEnd:note.object type:note.name userInfo:note.userInfo]; } #pragma mark - OperationError Management - (void)addOperationError:(XCCOperationError *)operationError { if (!operationError.fileName) operationError.fileName = @"/No Filename"; [self willChangeValueForKey:@"errors"]; if (!self.errors[operationError.fileName]) self.errors[operationError.fileName] = [@[] mutableCopy]; [self.errors[operationError.fileName] addObject:operationError]; [self didChangeValueForKey:@"errors"]; self.numberOfErrors++; [self _notifyUserWithOperationError:operationError]; } - (void)removeOperationError:(XCCOperationError *)operationError { if (!operationError.fileName) operationError.fileName = @"/No Filename"; [self willChangeValueForKey:@"errors"]; [self.errors[operationError.fileName] removeObject:operationError]; if (![self.errors[operationError.fileName] count]) [self.errors removeObjectForKey:operationError.fileName]; [self didChangeValueForKey:@"errors"]; self.numberOfErrors--; } - (void)removeAllOperationErrors { [self willChangeValueForKey:@"errors"]; [self.errors removeAllObjects]; [self didChangeValueForKey:@"errors"]; self.numberOfErrors = 0; } - (void)removeOperationErrorsRelatedToSourcePath:(NSString *)aPath errorType:(int)anErrorType { NSMutableArray *errorsToRemove = [@[] mutableCopy]; for (XCCOperationError *operationError in self.errors[aPath]) if ([operationError.fileName isEqualToString:aPath] && (operationError.errorType == anErrorType || anErrorType == XCCDefaultOperationErrorType)) [errorsToRemove addObject:operationError]; for (XCCOperationError *error in errorsToRemove) [self removeOperationError:error]; } - (NSInteger)numberOfErrors { return _numberOfErrors; } - (void)setNumberOfErrors:(NSInteger)numberOfErrors { if (numberOfErrors == _numberOfErrors) return; [self willChangeValueForKey:@"numberOfErrors"]; _numberOfErrors = numberOfErrors; [self didChangeValueForKey:@"numberOfErrors"]; if (!_numberOfErrors) self.errorsCountString = @""; else { NSString *plural1 = self.numberOfErrors > 1 ? @"s" : @""; NSString *plural2 = self.errors.count > 1 ? @"s" : @""; self.errorsCountString = [NSString stringWithFormat:@"%d issue%@ in %d file%@", (int)self.numberOfErrors, plural1, (int)self.errors.count, plural2]; } } - (void)_reinitializeOperationErrors { self.errors = [@{} mutableCopy]; self.numberOfErrors = 0; } #pragma mark - Operation Management - (void)operationDidStart:(XCCAbstractOperation*)anOperation type:(NSString *)aType userInfo:(NSDictionary*)userInfo { [self.mainXcodeCappController.operationsViewController reload]; [self.mainXcodeCappController.errorsViewController reload]; } - (void)operationDidEnd:(XCCAbstractOperation*)anOperation type:(NSString *)aType userInfo:(NSDictionary*)userInfo { [self _dequeueOperation:anOperation]; if ([aType isEqualToString:XCCSourcesFinderOperationDidEndNotification]) { self->currentFindSourceOperation = nil; [self _updateXcodeSupportFilesWithModifiedPaths:userInfo[@"sourcePaths"]]; } else if ([aType isEqualToString:XCCPBXOperationDidEndNotification]) { self->pendingPBXOperation = nil; } if (self->stream) self.cappuccinoProject.status = XCCCappuccinoProjectStatusListening; [self.mainXcodeCappController.operationsViewController reload]; [self.mainXcodeCappController.errorsViewController reload]; } - (void)_reinitializeOperationsCounters { self.operationsProgress = 1.0; self.operationsTotal = 0; self.operationsRemaining = 0; } - (void)_updateOperationsProgress { if (!self.operationsTotal) { self.operationsRemaining = 0; self.operationsProgress = 1.0; } else { self.operationsRemaining = (long)[[self projectRelatedOperations] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"cancelled = NO"]].count; self.operationsProgress = 1.0 - (float)self.operationsRemaining / (float)self.operationsTotal; } self.operationsRemainingString = [NSString stringWithFormat:@"%d total operations, %d remaining", (int)self.operationsTotal, (int)self.operationsRemaining]; if (self.operationsProgress == 1.0) [self _reinitializeOperationsCounters]; } - (void)_registerSourceProcessingOperation:(XCCSourceProcessingOperation *)sourceOperation { if (!self->sourceProcessingOperations[sourceOperation.sourcePath]) self->sourceProcessingOperations[sourceOperation.sourcePath] = [@[] mutableCopy]; NSMutableArray *operations = self->sourceProcessingOperations[sourceOperation.sourcePath]; if (![operations containsObject:sourceOperation]) [operations addObject:sourceOperation]; } - (void)_unregisterSourceProcessingOperation:(XCCSourceProcessingOperation *)sourceOperation { [self->sourceProcessingOperations[sourceOperation.sourcePath] removeObject:sourceOperation]; if (!self->sourceProcessingOperations[sourceOperation.sourcePath]) [self->sourceProcessingOperations removeObjectForKey:sourceOperation.sourcePath]; } - (NSArray *)_sourceProcessingOperationsForPath:(NSString *)path { return self->sourceProcessingOperations[path]; } - (void)_enqueueOperation:(NSOperation *)anOperation { if ([self->operationQueue.operations containsObject:anOperation]) return; [self->operationQueue addOperation:anOperation]; if ([anOperation isKindOfClass:[XCCSourceProcessingOperation class]]) [self _registerSourceProcessingOperation:(XCCSourceProcessingOperation *)anOperation]; self.operationsTotal++; [self _updateOperationsProgress]; } - (void)_dequeueOperation:(NSOperation *)anOperation { if ([anOperation isKindOfClass:[XCCSourceProcessingOperation class]]) [self _unregisterSourceProcessingOperation:(XCCSourceProcessingOperation *)anOperation]; [self _updateOperationsProgress]; } - (void)_cancelSourceOperationsForPath:(NSString *)path { NSArray *operations = [self _sourceProcessingOperationsForPath:path]; for (NSOperation * operation in operations) { [operation cancel]; [self _dequeueOperation:operation]; } } - (void)_cancelAllProjectRelatedOperations { [[self projectRelatedOperations] makeObjectsPerformSelector:@selector(cancel)]; self->sourceProcessingOperations = [@{} mutableCopy]; self->pendingPBXOperation = nil; } - (NSArray*)projectRelatedOperations { return [self->operationQueue.operations filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"cappuccinoProject.projectPath == %@", self.cappuccinoProject.projectPath]]; } - (NSArray *)projectRelatedSourceProcessingOperations { return [[self projectRelatedOperations] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self isKindOfClass: %@", [XCCSourceProcessingOperation class]]]; } - (void)_cancelCurrentSourceFinderOperation { if (!self->currentFindSourceOperation) return; [[self projectRelatedSourceProcessingOperations] makeObjectsPerformSelector:@selector(removeDependency:) withObject:self->currentFindSourceOperation]; [self->currentFindSourceOperation cancel]; [self _dequeueOperation:self->currentFindSourceOperation]; self->currentFindSourceOperation = nil; } - (void)_scheduleFinderSourceOperationForPath:(NSString *)path { if (self->currentFindSourceOperation) return; self->currentFindSourceOperation = [[XCCSourcesFinderOperation alloc] initWithCappuccinoProject:self.cappuccinoProject taskLauncher:self->taskLauncher sourcePath:path]; [[self projectRelatedSourceProcessingOperations] makeObjectsPerformSelector:@selector(addDependency:) withObject:self->currentFindSourceOperation]; [self _enqueueOperation:self->currentFindSourceOperation]; } - (void)_schedulePBXOperation { [self _schedulePBXOperationWithPathsToAdd:nil pathsToRemove:nil]; } - (void)_schedulePBXOperationWithPathsToAdd:(NSArray *)pathsToAdd pathsToRemove:(NSArray *)pathsToRemove; { BOOL needsEnqueue = NO; if (!self->pendingPBXOperation) { self->pendingPBXOperation = [[XCCPPXOperation alloc] initWithCappuccinoProject:self.cappuccinoProject taskLauncher:self->taskLauncher]; needsEnqueue = YES; } if (pathsToAdd) [self->pendingPBXOperation registerPathsToAddInPBX:pathsToAdd]; if (pathsToRemove) [self->pendingPBXOperation registerPathsToRemoveFromPBX:pathsToRemove]; for (XCCSourceProcessingOperation *operation in [self projectRelatedSourceProcessingOperations]) [self->pendingPBXOperation addDependency:operation]; if (needsEnqueue) [self _enqueueOperation:self->pendingPBXOperation]; } - (void)_scheduleSourceProcessingOperationForPath:(NSString *)path { [self _cancelSourceOperationsForPath:path]; XCCSourceProcessingOperation *operation = [[XCCSourceProcessingOperation alloc] initWithCappuccinoProject:self.cappuccinoProject taskLauncher:self->taskLauncher sourcePath:[self.cappuccinoProject projectPathForSourcePath:path]]; if (self->currentFindSourceOperation) [operation addDependency:self->currentFindSourceOperation]; [self _enqueueOperation:operation]; } #pragma mark - FS Event Management - (void)_startFSEventStream { if (self->stream) return; NSArray *pathsToWatch = [XCCCappuccinoProject getPathsToWatchForCappuccinoProject:self.cappuccinoProject]; void *appPointer = (__bridge void *)self; FSEventStreamContext context = { 0, appPointer, NULL, NULL, NULL }; CFTimeInterval latency = 0.5; UInt64 lastEventID = self.cappuccinoProject.lastEventID ? self.cappuccinoProject.lastEventID.unsignedLongLongValue : kFSEventStreamEventIdSinceNow; self->stream = FSEventStreamCreate(NULL, &fsevents_callback, &context, (__bridge CFArrayRef) pathsToWatch, lastEventID, latency, XCCProjectControllerFSEventFlags); FSEventStreamScheduleWithRunLoop(self->stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); FSEventStreamStart(self->stream); NSLog(@"FSEventStream started for paths: %@", pathsToWatch); } - (void)_stopFSEventStream { if (!self->stream) return; UInt64 lastEventID = FSEventStreamGetLatestEventId(self->stream); if (lastEventID != 0 && lastEventID != UINT64_MAX) { self.cappuccinoProject.lastEventID = @(lastEventID); [self.cappuccinoProject saveSettings]; } FSEventStreamStop(self->stream); FSEventStreamUnscheduleFromRunLoop(self->stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); FSEventStreamInvalidate(self->stream); FSEventStreamRelease(self->stream); self->stream = NULL; NSLog(@"FSEventStream stopped"); } - (void)_handleFSEventsWithPaths:(NSArray *)paths flags:(const FSEventStreamEventFlags[])eventFlags ids:(const FSEventStreamEventId[])eventIds { NSMutableArray *modifiedPaths = [@[] mutableCopy]; NSMutableArray *renamedDirectories = [@[] mutableCopy]; NSFileManager *fm = [NSFileManager defaultManager]; BOOL needUpdate = NO; for (size_t i = 0; i < paths.count; ++i) { FSEventStreamEventFlags flags = eventFlags[i]; NSString *path = [paths[i] stringByStandardizingPath]; BOOL rootChanged = (flags & kFSEventStreamEventFlagRootChanged) != 0; BOOL needRescan = (flags & kFSEventStreamEventFlagMustScanSubDirs) != 0; if (rootChanged || needRescan) { NSLog(@"Watched path changed: %@", path); [self _handleProjectPathChange:path]; return; } BOOL isHistoryDoneSentinalEvent = (flags & kFSEventStreamEventFlagHistoryDone) != 0; if (isHistoryDoneSentinalEvent) { NSLog(@"History done sentinal event"); continue; } BOOL isMountEvent = (flags & kFSEventStreamEventFlagMount) || (flags & kFSEventStreamEventFlagUnmount); if (isMountEvent) { NSLog(@"Volume %@: %@", (flags & kFSEventStreamEventFlagMount) ? @"mounted" : @"unmounted", path); continue; } BOOL inodeMetaModified = (flags & kFSEventStreamEventFlagItemInodeMetaMod) != 0; BOOL isFile = (flags & kFSEventStreamEventFlagItemIsFile) != 0; BOOL isSymlink = (flags & kFSEventStreamEventFlagItemIsSymlink) != 0; BOOL isDir = (flags & kFSEventStreamEventFlagItemIsDir) != 0; BOOL renamed = (flags & kFSEventStreamEventFlagItemRenamed) != 0; BOOL modified = (flags & kFSEventStreamEventFlagItemModified) != 0; BOOL created = (flags & kFSEventStreamEventFlagItemCreated) != 0; BOOL removed = (flags & kFSEventStreamEventFlagItemRemoved) != 0; if (isDir) { if (created && [path isEqualToString:self.cappuccinoProject.projectPath.stringByResolvingSymlinksInPath]) return; if ((renamed || created || removed) && ![XCCCappuccinoProject shouldIgnoreDirectoryNamed:path.lastPathComponent] && ![XCCCappuccinoProject pathMatchesIgnoredPaths:path cappuccinoProjectIgnoredPathPredicates:self.cappuccinoProject.ignoredPathPredicates]) { NSLog(@"Renamed directory: %@", path); [renamedDirectories addObject:path]; } continue; } else if ((isFile || isSymlink) && (created || modified || renamed || removed || inodeMetaModified) && [XCCCappuccinoProject isSourceFile:path cappuccinoProject:self.cappuccinoProject]) { NSLog(@"FSEvent accepted: %@ (%@)", path, [XCCFSEventLogUtils dumpFSEventFlags:flags]); if ([fm fileExistsAtPath:path]) { [modifiedPaths addObject:path]; } else if ([XCCCappuccinoProject isXibFile:path]) { // If a xib is deleted, delete its cib. There is no need to update when a xib is deleted, // it is inside a folder in Xcode, which updates automatically. if (![fm fileExistsAtPath:path]) { NSString *cibPath = [path.stringByDeletingPathExtension stringByAppendingPathExtension:@"cib"]; if ([fm fileExistsAtPath:cibPath]) [fm removeItemAtPath:cibPath error:nil]; continue; } } needUpdate = YES; } else if ((isFile || isSymlink) && (renamed || removed) && !(modified || created) && [XCCCappuccinoProject isCibFile:path]) { NSLog(@"FSEvent accepted: %@ (%@)", path, [XCCFSEventLogUtils dumpFSEventFlags:flags]); // If a cib is deleted, mark its xib as needing update so the cib is regenerated NSString *xibPath = [path.stringByDeletingPathExtension stringByAppendingPathExtension:@"xib"]; if ([fm fileExistsAtPath:xibPath]) { [modifiedPaths addObject:xibPath]; needUpdate = YES; } } else if ((isFile || isSymlink) && (created || modified || renamed || removed || inodeMetaModified) && [XCCCappuccinoProject isXCCIgnoreFile:path cappuccinoProject:self.cappuccinoProject]) { NSLog(@"FSEvent accepted: %@ (%@)", path, [XCCFSEventLogUtils dumpFSEventFlags:flags]); [self.cappuccinoProject reloadXcodeCappIgnoreFile]; } } // If directories were renamed, we take the easy way out and reset the project if (renamedDirectories.count) [self _updateXcodeSupportFilesWithRenamedDirectories:renamedDirectories]; else if (needUpdate) [self _updateXcodeSupportFilesWithModifiedPaths:modifiedPaths]; } - (void)_handleProjectPathChange:(NSString *)path { NSAlert *alert = [[NSAlert alloc] init]; [alert setAlertStyle:NSAlertStyleCritical]; [alert setMessageText:@"The project directory changed"]; NSString *informativeText = [NSString stringWithFormat:@"This project will be removed."]; [alert setInformativeText:informativeText]; [alert runModal]; [self.mainXcodeCappController unmanageCappuccinoProjectController:self]; } #pragma mark - Third Party Application Management - (NSString *)_managingApplicationIdenfierForFilePath:(NSString *)filePath { NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; NSString *app, *type; BOOL success = [workspace getInfoForFile:filePath application:&app type:&type]; return success ? app : nil; } - (void)launchEditorForPath:(NSString*)path line:(NSInteger)line { NSString *applicationIdentifier = [self _managingApplicationIdenfierForFilePath:path]; [self launchEditorForPath:path line:line applicationIdentifier:applicationIdentifier]; } - (void)launchEditorForPath:(NSString*)path line:(NSInteger)line applicationIdentifier:(NSString *)applicationIdentifier { if (!applicationIdentifier) return; NSFileManager *fm = [NSFileManager defaultManager]; NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; NSBundle *bundle = [NSBundle bundleWithPath:applicationIdentifier]; NSString *identifier = bundle.bundleIdentifier; NSString *executablePath = nil; XCCLineSpecifier lineSpecifier = kLineSpecifierNone; if ([identifier hasPrefix:@"com.sublimetext."]) { lineSpecifier = kLineSpecifierColon; executablePath = [[bundle sharedSupportPath] stringByAppendingPathComponent:@"bin/subl"]; } else if ([identifier isEqualToString:@"com.barebones.textwrangler"]) { lineSpecifier = kLineSpecifierColon; executablePath = [[bundle bundlePath] stringByAppendingPathComponent:@"Contents/Helpers/edit"]; } else if ([identifier isEqualToString:@"com.barebones.bbedit"]) { lineSpecifier = kLineSpecifierColon; executablePath = [[bundle bundlePath] stringByAppendingPathComponent:@"Contents/Helpers/bbedit"]; } else if ([identifier isEqualToString:@"com.macromates.textmate"]) // TextMate 1.x { lineSpecifier = kLineSpecifierMinusL; executablePath = [[bundle sharedSupportPath] stringByAppendingPathComponent:@"Support/bin/mate"]; } else if ([identifier hasPrefix:@"com.macromates.TextMate"]) // TextMate 2.x { lineSpecifier = kLineSpecifierMinusL; executablePath = [bundle pathForResource:@"mate" ofType:@""]; } else if ([identifier isEqualToString:@"com.chocolatapp.Chocolat"]) { lineSpecifier = kLineSpecifierMinusL; executablePath = [[bundle sharedSupportPath] stringByAppendingPathComponent:@"choc"]; } else if ([identifier isEqualToString:@"org.vim.MacVim"]) { lineSpecifier = kLineSpecifierPlus; executablePath = @"/usr/local/bin/mvim"; } else if ([identifier isEqualToString:@"org.gnu.Aquamacs"]) { if ([fm isExecutableFileAtPath:@"/usr/bin/aquamacs"]) executablePath = @"/usr/bin/aquamacs"; else if ([fm isExecutableFileAtPath:@"/usr/local/bin/aquamacs"]) executablePath = @"/usr/local/bin/aquamacs"; } else if ([identifier isEqualToString:@"com.apple.dt.Xcode"]) { executablePath = [[bundle bundlePath] stringByAppendingPathComponent:@"Contents/Developer/usr/bin/xed"]; } if (!executablePath || ![fm isExecutableFileAtPath:executablePath]) { [workspace openFile:path]; return; } NSArray *args; switch (lineSpecifier) { case kLineSpecifierNone: args = @[path]; break; case kLineSpecifierColon: args = @[[NSString stringWithFormat:@"%1$@:%2$ld", path, line]]; break; case kLineSpecifierMinusL: args = @[@"-l", [NSString stringWithFormat:@"%ld", line], path]; break; case kLineSpecifierPlus: args = @[[NSString stringWithFormat:@"+%ld", line], path]; break; } [self->taskLauncher runTaskWithCommand:executablePath arguments:args returnType:kTaskReturnTypeNone]; } #pragma mark - NSUserNotificationCenter Utilities - (void)_notifyUserWithOperationError:(XCCOperationError*)anOperationError { NSUserNotification *note = [NSUserNotification new]; note.title = [[anOperationError.fileName stringByReplacingOccurrencesOfString:self.cappuccinoProject.projectPath withString:@""] substringFromIndex:1]; note.informativeText = anOperationError.description; note.actionButtonTitle = @"Show"; note.otherButtonTitle = @"Close"; note.userInfo = @{@"cappuccinoProjectPath" : self.cappuccinoProject.projectPath, @"sourcePath": anOperationError.fileName}; [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:note]; } #pragma mark - Public Utilities - (void)reinitializeProjectFromSettings { NSLog(@"Saving Cappuccino configuration project %@", self.cappuccinoProject.projectPath); [self _cancelAllProjectRelatedOperations]; [self.cappuccinoProject saveSettings]; int previousStatus = self.cappuccinoProject.status; [self _stopListeningToProject]; [self _reinitializeTaskLauncher]; if (!self->taskLauncher.isValid) return; if (previousStatus == XCCCappuccinoProjectStatusListening) [self _startListeningToProject]; NSLog(@"Cappuccino configuration project %@ has been saved", self.cappuccinoProject.projectPath); } - (void)applicationIsClosing { [self _stopFSEventStream]; } - (void)cleanUpBeforeDeletion { [self cancelAllOperations:self]; [self _stopListeningToOperationsNotifications]; [self _stopListeningToProject]; [self _removeXcodeProject]; [self _removeXcodeSupportDirectory]; [self.mainXcodeCappController.operationsViewController reload]; [self.mainXcodeCappController.errorsViewController reload]; } #pragma mark - Actions - (IBAction)cancelAllOperations:(id)aSender { [self _cancelAllProjectRelatedOperations]; [self.mainXcodeCappController.operationsViewController reload]; } - (IBAction)resetProject:(id)aSender { [self _stopListeningToProject]; [self _removeXcodeSupportDirectory]; [self _removeXcodeProject]; [self _reinitializeProjectController]; [self _startListeningToProject]; } - (IBAction)openProjectInXcode:(id)aSender { BOOL isDirectory; BOOL isOpened = YES; NSFileManager *fm = [NSFileManager defaultManager]; BOOL exists = [fm fileExistsAtPath:self.cappuccinoProject.XcodeProjectPath isDirectory:&isDirectory]; if (exists && isDirectory) { NSLog(@"Opening Xcode project at: %@", self.cappuccinoProject.XcodeProjectPath); isOpened = [[NSWorkspace sharedWorkspace] openFile:self.cappuccinoProject.XcodeProjectPath]; } if (!exists || !isDirectory || !isOpened) { NSString *projectErrorDescription; if (!isOpened) projectErrorDescription = @"The project exists, but failed to open."; else projectErrorDescription = [NSString stringWithFormat:@"%@ %@.", self.cappuccinoProject.XcodeProjectPath, !exists ? @"does not exist" : @"is not an Xcode project"]; [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; NSAlert *alert = [[NSAlert alloc] init]; [alert setAlertStyle:NSAlertStyleCritical]; [alert setMessageText:@"The project could not be opened"]; NSArray *informativeTextArray = @[ projectErrorDescription, @"Should the project be regenerated?" ]; NSString *informativeText = [informativeTextArray componentsJoinedByString:@"\n"]; [alert setInformativeText:informativeText]; [alert addButtonWithTitle:@"Yes"]; [alert addButtonWithTitle:@"No"]; NSInteger response = [alert runModal]; if (response == NSAlertFirstButtonReturn) [self resetProject:self]; } } - (IBAction)openProjectInFinder:(id)sender { [[NSWorkspace sharedWorkspace] openFile:self.cappuccinoProject.projectPath]; } - (IBAction)openProjectInEditor:(id)sender { NSFileManager *fm = [NSFileManager defaultManager]; NSArray *contents = [fm contentsOfDirectoryAtPath:self.cappuccinoProject.projectPath error:nil]; NSString *firstObjjFile; for (NSString *file in contents) { if ([[file pathExtension] isEqualToString:@"j"]) { firstObjjFile = file; break; } } if (!firstObjjFile) return; NSString *applicationIdentifier = [self _managingApplicationIdenfierForFilePath:[self.cappuccinoProject.projectPath stringByAppendingPathComponent:firstObjjFile]]; if (!applicationIdentifier) return; [self launchEditorForPath:self.cappuccinoProject.projectPath line:0 applicationIdentifier:applicationIdentifier]; } - (IBAction)openProjectInTerminal:(id)sender; { NSString *s = [NSString stringWithFormat:@"tell application \"Terminal\"\n do script \"cd %@; clear\" \n activate\n end tell", self.cappuccinoProject.projectPath]; NSAppleScript *script = [[NSAppleScript alloc] initWithSource:s]; [script executeAndReturnError:nil]; } - (IBAction)switchProjectListeningStatus:(id)sender { if (self.cappuccinoProject.status == XCCCappuccinoProjectStatusStopped) [self _startListeningToProject]; else [self _stopListeningToProject]; } @end