diff --git a/CHANGELOG.md b/CHANGELOG.md index ad0e6ec38..6d0718ceb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## 0.55 (unreleased) +### Parsing + +Added support for Objective-C. + ## 0.54 (released 7th January 2024) ### Parsing diff --git a/build.rs b/build.rs index d0dd4434f..dc3321fa0 100644 --- a/build.rs +++ b/build.rs @@ -241,6 +241,11 @@ fn main() { src_dir: "vendored_parsers/tree-sitter-nix-src", extra_files: vec!["scanner.c"], }, + TreeSitterParser { + name: "tree-sitter-objc", + src_dir: "vendored_parsers/tree-sitter-objc-src", + extra_files: vec![], + }, TreeSitterParser { name: "tree-sitter-ocaml", src_dir: "vendored_parsers/tree-sitter-ocaml-src/ocaml/src", diff --git a/manual/src/languages_supported.md b/manual/src/languages_supported.md index 1e877c9ac..2505c0413 100644 --- a/manual/src/languages_supported.md +++ b/manual/src/languages_supported.md @@ -36,6 +36,7 @@ with `difft --list-languages`. | Lua | [nvim-treesitter/tree-sitter-lua](https://github.com/nvim-treesitter/tree-sitter-lua) | | Make | [alemuller/tree-sitter-make](https://github.com/alemuller/tree-sitter-make) | | Nix | [cstrahan/tree-sitter-nix](https://github.com/cstrahan/tree-sitter-nix) | +| Objective-C | [amaanq/tree-sitter-objc](https://github.com/amaanq/tree-sitter-objc) | | OCaml | [tree-sitter/tree-sitter-ocaml](https://github.com/tree-sitter/tree-sitter-ocaml) | | Perl | [ganezdragon/tree-sitter-perl](https://github.com/ganezdragon/tree-sitter-perl) | | PHP | [tree-sitter/tree-sitter-php](https://github.com/tree-sitter/tree-sitter-php) | diff --git a/sample_files/compare.expected b/sample_files/compare.expected index c8b7cb46a..6a64f1d42 100644 --- a/sample_files/compare.expected +++ b/sample_files/compare.expected @@ -163,6 +163,12 @@ e00b95a4cf3fa3edf994155d8656063f - sample_files/nullable_before.kt sample_files/nullable_after.kt 66da628a2c20e18059b8669aaa14a163 - +sample_files/objc_header_before.h sample_files/objc_header_after.h +f65feb21b25bb7f2ba31ee8f49977193 - + +sample_files/objc_module_before.m sample_files/objc_module_after.m +c04cdebd2da077cf28b53307ad0a3b02 - + sample_files/ocaml_before.ml sample_files/ocaml_after.ml 2113c6c7959b8099f678d13953f7f44a - diff --git a/sample_files/objc_header_after.h b/sample_files/objc_header_after.h new file mode 100644 index 000000000..da98fe5a3 --- /dev/null +++ b/sample_files/objc_header_after.h @@ -0,0 +1,32 @@ +// +// HttpServer.h after file +// +// Created by Nicholas Moore on 20/09/2022. +// A line added to the header. +// +// Added to difftastic test suite by the author. +// This source file is released by the author into the public domain. + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NSDictionary *_Nullable (^PopHttpRequestHandler)(NSURL *url, + NSString *method, + NSDictionary *headers, + NSData *body); + +@interface PopHttpServer : NSObjectqq +@property(readonly) uint16_t port; // comment +@property(readonly) NSString *host; +@property(readonly) NSString *lastError; +@property(readonly, getter=isListening) BOOL listening; +- (id)initWithPort:(uint16_t)port; +- (BOOL)start; +- (void)stop; +- (void)registerHandler:(NSString *_Nonnull)pathPrefix + block:(PopHttpRequestHandler)myblock + added:(BOOL *_Nullable)added; +@end + +NS_ASSUME_NONNULL_END diff --git a/sample_files/objc_header_before.h b/sample_files/objc_header_before.h new file mode 100644 index 000000000..537ce9c2b --- /dev/null +++ b/sample_files/objc_header_before.h @@ -0,0 +1,25 @@ +// +// HttpServer.h +// +// Created by Nicholas Moore on 20/09/2022. +// +// Added to difftastic test suite by the author. +// This source file is released by the author into the public domain. + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NSDictionary *_Nullable(^PopHttpRequestHandler)(NSURL *url, NSString *method, NSDictionary *headers, NSData *body); + +@interface PopHttpServer : NSObject +@property (readonly) uint16_t port; +@property (readonly) NSString *lastError; +@property (readonly, getter=isListening) BOOL listening; +- (id)initWithPort:(uint16_t)port; +- (BOOL)start; +- (void)stop; +- (void)registerHandler:(NSString *)pathPrefix block:(PopHttpRequestHandler)myblock; +@end + +NS_ASSUME_NONNULL_END diff --git a/sample_files/objc_module_after.m b/sample_files/objc_module_after.m new file mode 100644 index 000000000..e5172f818 --- /dev/null +++ b/sample_files/objc_module_after.m @@ -0,0 +1,346 @@ +// +// HttpServer.m +// Simple HTTP server. +// by Nicholas Moore 2023 +// inspired by http://www.cocoawithlove.com/2009/07/simple-extensible-http-server-in-cocoa.html +// +// Added to difftastic test suite by the author. +// This source file is released by the author into the public domain. + + +#import "PopHttpServer.h" +#import "NMKit.h" +#include +#include +#include + +@interface PopHttpServer () +@property (readonly) CFSocketRef socket; +@property (readonly) NSFileHandle *listenHandle; +@property (readonly) NSMapTable *httpMessages; +@property (readonly) NSMutableDictionary *handlers; +@property NSString *lastError; +@end + +const NSArray *expressions=@[@NO, @7, @(YES), @3.15, @(9), @-11, @"Goodbye"]; + +@implementation PopHttpServer + +- (id)initWithPort:(uint16_t)port +{ + self=[super init]; + if (self) { + self->_port=port; + self->_httpMessages=[NSMapTable weakToStrongObjectsMapTable]; + self->_handlers=[NSMutableDictionary dictionary]; + } + return self; +} + +// warning: handlers will be called on a background thread!! +- (void)registerHandler:(NSString *)pathPrefix block:(PopHttpRequestHandler)myblock +{ + [self.handlers setObject:myblock forKey:pathPrefix]; +} + +- (void)newHttpMessageForHandle:(NSFileHandle *const)connectionHandle +{ + [self.httpMessages setObject:CFBridgingRelease(CFHTTPMessageCreateEmpty(kCFAllocatorDefault, TRUE)) + forKey:connectionHandle]; +} + +- (BOOL)start +{ + NMLogFine(@"Attempting to start HTTP server on port %@", @(self.port)); + + // create socket + self->_socket = CFSocketCreate(kCFAllocatorDefault, + PF_INET, SOCK_STREAM, IPPROTO_TCP, 0, 6, NULL + ); + if (!self.socket) + { + self.lastError=@"Unable to create socket"; + return NO; + } + + // set reuse flag + // "This lets us reclaim the port if it is open but idle (a common occurrence if we restart the program immediately after a crash or killing the application)." + const int reuse = true; + const int fileDescriptor = CFSocketGetNative(self.socket); + if (setsockopt(fileDescriptor, SOL_SOCKET, SO_REUSEADDR, (void *)&reuse, sizeof(int)) != 0) + { + self.lastError=@"Unable to set socket options."; + [self stop]; + return NO; + } + + // create address and port + struct sockaddr_in address; + memset(&address, 0, sizeof(address)); + address.sin_len = sizeof(address); + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_ANY); + address.sin_port = htons(self.port); + NSData *const addressData = [NSData dataWithBytes:&address length:sizeof(address)]; + if (CFSocketSetAddress(self.socket, (__bridge CFDataRef)addressData) != kCFSocketSuccess) + { + self.lastError=@"Unable to bind socket."; + [self stop]; return NO; + } + + // add listener for connections + self->_listenHandle = [[NSFileHandle alloc] + initWithFileDescriptor:fileDescriptor + closeOnDealloc:YES]; + + // register for notifications + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(receiveIncomingConnectionNotification:) + name:NSFileHandleConnectionAcceptedNotification + object:self.listenHandle]; + [self.listenHandle acceptConnectionInBackgroundAndNotify]; + + NMLogFine(@"HTTP server started on port %@", @(self.port)); + return YES; + +} + +// Undo everything that was done in start +- (void)stop +{ + // close down all open connections + for (NSFileHandle *connectionHandle in [self.httpMessages keyEnumerator]) { + NMLogFine(@"Closing connection %@", connectionHandle); + [[NSNotificationCenter defaultCenter] removeObserver:self + name:NSFileHandleDataAvailableNotification + object:connectionHandle]; + [connectionHandle closeFile]; + } + + if (self.listenHandle) + { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:NSFileHandleConnectionAcceptedNotification + object:self.listenHandle]; + [self.listenHandle closeFile]; + self->_listenHandle=nil; + } + + if (self.socket) + { + CFSocketInvalidate(self.socket); + CFRelease(self.socket); + self->_socket=nil; + } + NMLogFine(@"HTTP server on port %@ stopped", @(self.port)); +} + +- (BOOL)isListening +{ + return self.socket; +} + +// notification sent by the listenHandle +- (void)receiveIncomingConnectionNotification:(NSNotification *)notification +{ + NSFileHandle *const connectionHandle=[[notification userInfo] objectForKey:NSFileHandleNotificationFileHandleItem]; + if(connectionHandle) + { + [self newHttpMessageForHandle:connectionHandle]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(receiveIncomingDataNotification:) + name:NSFileHandleDataAvailableNotification + object:connectionHandle]; + [connectionHandle waitForDataInBackgroundAndNotify]; + } + NMLogFine(@"Incoming connection %@", connectionHandle); + + // accept another connection + [self.listenHandle acceptConnectionInBackgroundAndNotify]; +} + +typedef NS_ENUM(NSUInteger, DataOutcome) { + DataOutcomeContinue, + DataOutcomeClose, + DataOutcomeKeepAlive, + DataOutcomeUnknown, +}; +- (void)receiveIncomingDataNotification:(NSNotification *)notification +{ + NSFileHandle *const connectionHandle=[notification object]; + + // get our stored partial http message for this connection + const CFHTTPMessageRef httpMessage=(__bridge CFHTTPMessageRef)[self.httpMessages objectForKey:connectionHandle]; + + // perform remaining processing in backgroumd thread + NMRunAsyncInBackground(^{ + const DataOutcome outcome=[self processHttpDataForMessage:httpMessage + connection:connectionHandle]; + NMRunAsyncOnMainThread(^{ + switch (outcome) { + case DataOutcomeClose: + [[NSNotificationCenter defaultCenter] removeObserver:self + name:NSFileHandleDataAvailableNotification + object:connectionHandle]; + [connectionHandle closeFile]; + NMLogFine(@"Closed connection %@; there are %@", connectionHandle, @(self.httpMessages.count)); + break; + case DataOutcomeKeepAlive: + [self newHttpMessageForHandle:connectionHandle]; + NMLogFine(@"Ready for new message on connection %@; there are %@", connectionHandle, @(self.httpMessages.count)); + // here we deliberately fall through to the Continue case + case DataOutcomeContinue: + default: + NMLogFine(@"Awaiting more data on connection %@", connectionHandle); + [connectionHandle waitForDataInBackgroundAndNotify]; + break; + } + }); + }); +} + +- (DataOutcome)processHttpDataForMessage:(CFHTTPMessageRef)httpMessage connection:(NSFileHandle *)connectionHandle +{ + // get the data (if any) + NSData *data=nil; + @try { + data=[connectionHandle availableData]; + } + @catch(NSException *e) { + // Ignore the exception, it normally just means the client + // closed the connection from the other end. + } + NMLogFine(@"Data: %@ bytes received on connection %@", @(data.length), connectionHandle); + + // close if EOF or error parsing into http message + if (!data.length || httpMessage || + !CFHTTPMessageAppendBytes(httpMessage, data.bytes, data.length)) { + return DataOutcomeClose; + } + + // continue if we don't have the full header yet + if (!CFHTTPMessageIsHeaderComplete(httpMessage)) { + return DataOutcomeContinue; + } + + // get the Content-Length value + NSInteger contentLength=0; + CFStringRef contentLengthStr = CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Content-Length")); + if (contentLengthStr) { + contentLength = CFStringGetIntValue(contentLengthStr); + CFRelease(contentLengthStr); + } + + // get the Connection header + BOOL closeWhenDone=NO; + CFStringRef connectionStr = CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Connection")); + if (connectionStr) { + closeWhenDone = (CFStringCompare(connectionStr, CFSTR("close"), kCFCompareCaseInsensitive)==kCFCompareEqualTo); + CFRelease(connectionStr); + } + + // get received data length + NSInteger receivedLength; + CFDataRef bodyData = CFHTTPMessageCopyBody(httpMessage); + if (bodyData) { + receivedLength = CFDataGetLength(bodyData); + CFRelease(bodyData); + } + + // check if full body is recieved + if (receivedLength>=contentLength) { + NMLogFine(@"Entire message received, responding on connection %@", connectionHandle); + [self respondToHttpMessage:httpMessage withConnection:connectionHandle]; + return closeWhenDone?DataOutcomeClose:DataOutcomeKeepAlive; + } else { + return DataOutcomeContinue; + } +} + +- (void)respondToHttpMessage:(CFHTTPMessageRef)httpMessage withConnection:(NSFileHandle *)connectionHandle +{ +#ifdef DEBUG + NSMutableArray *const logMessage=[NSMutableArray array]; + void(^log)(NSString *, ...) = ^(NSString *format, ...) { + va_list args; + va_start(args, format); + [logMessage addObject:[[NSString alloc] initWithFormat:format arguments:args]]; + va_end(args); + }; +#endif + + // get request info + NSURL *const url=CFBridgingRelease(CFHTTPMessageCopyRequestURL(httpMessage)); + NSString *const method=CFBridgingRelease(CFHTTPMessageCopyRequestMethod(httpMessage)); + NSDictionary *const headers=CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(httpMessage)); + NSData *const body=CFBridgingRelease(CFHTTPMessageCopyBody(httpMessage)); + +#ifdef DEBUG + log(@"%@ value is %@", method, url.absoluteURL); + log(@"Body: %@", body); +#endif + + // my very basic router + NSString *const firstPathPart=[[[url.path stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"/"]] componentsSeparatedByString:@"/"] safeFirstObject]; + NSDictionary *res=nil; + PopHttpRequestHandler handler=[self.handlers objectForKey:firstPathPart]; + if (handler) { +#ifdef DEBUG + log(@"Route: %@", firstPathPart); +#endif + res=handler(url, method, headers, body); + } +#ifdef DEBUG + log(@"Response: %@", res); +#endif + + NMLogFine(@"%@", [logMessage componentsJoinedByString:@"\n"]); + // note: prevously was wrapping this in WarpToMain, but seems unnecessary + if (res) { + [self respondWithBody:res[@"body"] status:res[@"status"] contentType:res[@"contentType"] headers:res[@"headers"] handle:connectionHandle]; + } else { + // redo + } +} + +- (void)respondWithBody:(NSObject *)bodyObj status:(NSNumber *)status contentType:(NSString *)contentType headers:(NSDictionary *)headers handle:(NSFileHandle *)connectionHandle +{ + NSData *bodyData=nil; + if ([bodyObj isKindOfClass:[NSString class]]) { + bodyData=[(NSString *)bodyObj dataUsingEncoding:NSUTF8StringEncoding]; + contentType=[contentType stringByAppendingString:@"; charset=utf-8"]; + } + else if ([bodyObj isKindOfClass:[NSData class]]) { + bodyData=(NSData *)bodyObj; + } + else { + NMLogError(@"Bad bodyObj: %@", bodyObj); + bodyData=[NSData data]; + } + + // create response message + CFHTTPMessageRef response=CFHTTPMessageCreateResponse(kCFAllocatorDefault, [status integerValue], NULL, kCFHTTPVersion1_1); + [headers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull val, BOOL * _Nonnull stop) { + CFHTTPMessageSetHeaderFieldValue(response, (__bridge CFStringRef)key, (__bridge CFStringRef)val); + }]; + CFHTTPMessageSetHeaderFieldValue(response, CFSTR("content-type"), (__bridge CFStringRef)contentType); + CFHTTPMessageSetHeaderFieldValue(response, CFSTR("content-length"), (__bridge CFStringRef)[NSString stringWithFormat:@"%@", @(bodyData.length)]); + CFHTTPMessageSetBody(response, (__bridge CFDataRef)bodyData); + CFDataRef messageData = CFHTTPMessageCopySerializedMessage(response); + @try + { + [connectionHandle writeData:(__bridge NSData *)messageData]; + } + @catch (NSException *exception) + { + // Ignore the exception, it normally just means the client + // closed the connection from the other end. + } + @finally + { + CFRelease(messageData); + CFRelease(response); + } +} + +@end diff --git a/sample_files/objc_module_before.m b/sample_files/objc_module_before.m new file mode 100644 index 000000000..7eae898cd --- /dev/null +++ b/sample_files/objc_module_before.m @@ -0,0 +1,341 @@ +// +// HttpServer.m +// Simple HTTP server. +// by Nicholas Moore 2023 +// inspired by http://www.cocoawithlove.com/2009/07/simple-extensible-http-server-in-cocoa.html +// +// Added to difftastic test suite by the author. +// This source file is released by the author into the public domain. + + +#import "PopHttpServer.h" +#import "NMKit.h" +#include +#include + +@interface PopHttpServer () +@property (readonly) CFSocketRef socket; +@property (readonly) NSFileHandle *listenHandle; +@property (readonly) NSMapTable *httpMessages; +@property (readonly) NSMutableDictionary *handlers; +@property NSString *lastError; +@end + +const NSArray *expressions=@[@YES, @6, @(NO), @3.14, @(-9), @-10, @"Hello"]; + +@implementation PopHttpServer + +- (id)initWithPort:(uint16_t)port +{ + self=[super init]; + if (self) { + self->_port=port; + self->_httpMessages=[NSMapTable weakToStrongObjectsMapTable]; + self->_handlers=[NSMutableDictionary dictionary]; + } + return self; +} + +// warning: handlers will be called on a background thread +- (void)registerHandler:(NSString *)pathPrefix block:(PopHttpRequestHandler)myblock +{ + [self.handlers setObject:myblock forKey:pathPrefix]; +} + +- (void)newHttpMessageForHandle:(NSFileHandle *)connectionHandle +{ + [self.httpMessages setObject:CFBridgingRelease(CFHTTPMessageCreateEmpty(kCFAllocatorDefault, TRUE)) forKey:connectionHandle]; +} + +- (BOOL)start +{ + NMLogFine(@"Attempting to start HTTP server on port %@", @(self.port)); + + // create socket + self->_socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCP, 0, NULL, NULL); + if (!self.socket) + { + self.lastError=@"Unable to create socket"; + return NO; + } + + // set reuse flag + // "This lets us reclaim the port if it is open but idle (a common occurrence if we restart the program immediately after a crash or killing the application)." + const int reuse = true; + const int fileDescriptor = CFSocketGetNative(self.socket); + if (setsockopt(fileDescriptor, SOL_SOCKET, SO_REUSEADDR, (void *)&reuse, sizeof(int)) != 0) + { + self.lastError=@"Unable to set socket options."; + [self stop]; + return NO; + } + + // create address and port + struct sockaddr_in address; + memset(&address, 0, sizeof(address)); + address.sin_len = sizeof(address); + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_ANY); + address.sin_port = htons(self.port); + NSData *const addressData = [NSData dataWithBytes:&address length:sizeof(address)]; + if (CFSocketSetAddress(self.socket, (__bridge CFDataRef)addressData) != kCFSocketSuccess) + { + self.lastError=@"Unable to bind socket to address."; + [self stop]; + return NO; + } + + // add listener for connections + self->_listenHandle = [[NSFileHandle alloc] + initWithFileDescriptor:fileDescriptor + closeOnDealloc:YES]; + + // register for notifications + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(receiveIncomingConnectionNotification:) + name:NSFileHandleConnectionAcceptedNotification + object:self.listenHandle]; + [self.listenHandle acceptConnectionInBackgroundAndNotify]; + + NMLogFine(@"HTTP server started on port %@", @(self.port)); + return YES; + +} + +// Undo everything in start +- (void)stop +{ + // close down all open connections + for (NSFileHandle *connectionHandle in [self.httpMessages keyEnumerator]) { + NMLogFine(@"Closing connection %@", connectionHandle); + [[NSNotificationCenter defaultCenter] removeObserver:self + name:NSFileHandleDataAvailableNotification + object:connectionHandle]; + [connectionHandle closeFile]; + } + + if (self.listenHandle) + { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:NSFileHandleConnectionAcceptedNotification + object:self.listenHandle]; + [self.listenHandle closeFile]; + self->_listenHandle=nil; + } + + if (self.socket) + { + CFSocketInvalidate(self.socket); + CFRelease(self.socket); + self->_socket=nil; + } + NMLogFine(@"HTTP server on port %@ stopped", @(self.port)); +} + +- (BOOL)isListening +{ + return self.socket; +} + +// notification sent by the listenHandle +- (void)receiveIncomingConnectionNotification:(NSNotification *)notification +{ + NSFileHandle *const connectionHandle=[[notification userInfo] objectForKey:NSFileHandleNotificationFileHandleItem]; + if(connectionHandle) + { + [self newHttpMessageForHandle:connectionHandle]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(receiveIncomingDataNotification:) + name:NSFileHandleDataAvailableNotification + object:connectionHandle]; + [connectionHandle waitForDataInBackgroundAndNotify]; + } + NMLogFine(@"Incoming connection %@", connectionHandle); + // accept another connection + [self.listenHandle acceptConnectionInBackgroundAndNotify]; +} + +typedef NS_ENUM(NSUInteger, DataOutcome) { + DataOutcomeContinue, + DataOutcomeClose, + DataOutcomeKeepAlive, +}; +- (void)receiveIncomingDataNotification:(NSNotification *)notification +{ + NSFileHandle *const connectionHandle=[notification object]; + + // get our stored partial http message for this connection + const CFHTTPMessageRef httpMessage=(__bridge CFHTTPMessageRef)[self.httpMessages objectForKey:connectionHandle]; + + // perform remaining processing in backgroumd thread + NMRunAsyncInBackground(^{ + const DataOutcome outcome=[self processHttpDataForMessage:httpMessage connection:connectionHandle]; + NMRunAsyncOnMainThread(^{ + switch (outcome) { + case DataOutcomeClose: + [[NSNotificationCenter defaultCenter] removeObserver:self + name:NSFileHandleDataAvailableNotification + object:connectionHandle]; + [connectionHandle closeFile]; + NMLogFine(@"Closed connection %@; there are %@", connectionHandle, @(self.httpMessages.count)); + break; + case DataOutcomeKeepAlive: + [self newHttpMessageForHandle:connectionHandle]; + NMLogFine(@"Ready for new message on connection %@; there are %@", connectionHandle, @(self.httpMessages.count)); + // here we deliberately fall through to the Continue case + case DataOutcomeContinue: + default: + NMLogFine(@"Awaiting more data on connection %@", connectionHandle); + [connectionHandle waitForDataInBackgroundAndNotify]; + break; + } + }); + }); +} + +- (DataOutcome)processHttpDataForMessage:(CFHTTPMessageRef)httpMessage connection:(NSFileHandle *)connectionHandle +{ + // get the data (if any) + NSData *data=nil; + @try { + data=[connectionHandle availableData]; + } + @catch(NSException *e) { + // Ignore the exception, it normally just means the client + // closed the connection from the other end. + } + NMLogFine(@"Data: %@ bytes received on connection %@", @(data.length), connectionHandle); + + // close if EOF or error parsing into http message + if (!data.length || + !httpMessage || + !CFHTTPMessageAppendBytes(httpMessage, data.bytes, data.length)) { + return DataOutcomeClose; + } + + // continue if we don't have the full header yet + if (!CFHTTPMessageIsHeaderComplete(httpMessage)) { + return DataOutcomeContinue; + } + + // get the Content-Length value + NSInteger contentLength=0; + CFStringRef contentLengthStr = CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Content-Length")); + if (contentLengthStr) { + contentLength = CFStringGetIntValue(contentLengthStr); + CFRelease(contentLengthStr); + } + + // get the Connection header + BOOL closeWhenDone=NO; + CFStringRef connectionStr = CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Connection")); + if (connectionStr) { + closeWhenDone = (CFStringCompare(connectionStr, CFSTR("close"), kCFCompareCaseInsensitive)==kCFCompareEqualTo); + CFRelease(connectionStr); + } + + // get received data length + NSInteger receivedLength=0; + CFDataRef bodyData = CFHTTPMessageCopyBody(httpMessage); + if (bodyData) { + receivedLength = CFDataGetLength(bodyData); + CFRelease(bodyData); + } + + // check if full body is recieved + if (receivedLength>=contentLength) { + NMLogFine(@"Entire message received, responding on connection %@", connectionHandle); + [self respondToHttpMessage:httpMessage withConnection:connectionHandle]; + return closeWhenDone?DataOutcomeClose:DataOutcomeKeepAlive; + } else { + return DataOutcomeContinue; + } +} + +- (void)respondToHttpMessage:(CFHTTPMessageRef)httpMessage withConnection:(NSFileHandle *)connectionHandle +{ +#ifdef DEBUG + NSMutableArray *const logMessage=[NSMutableArray array]; + void(^log)(NSString *, ...) = ^(NSString *format, ...) { + va_list args; + va_start(args, format); + [logMessage addObject:[[NSString alloc] initWithFormat:format arguments:args]]; + va_end(args); + }; +#endif + + // get request info + NSURL *const url=CFBridgingRelease(CFHTTPMessageCopyRequestURL(httpMessage)); + NSString *const method=CFBridgingRelease(CFHTTPMessageCopyRequestMethod(httpMessage)); + NSDictionary *const headers=CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(httpMessage)); + NSData *const body=CFBridgingRelease(CFHTTPMessageCopyBody(httpMessage)); + +#ifdef DEBUG + log(@"%@ %@", method, url.absoluteURL); + log(@"Body: %@", body); +#endif + + // my very basic router + NSString *const firstPathPart=[[[url.path stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"/"]] componentsSeparatedByString:@"/"] safeFirstObject]; + NSDictionary *res=nil; + PopHttpRequestHandler handler=[self.handlers objectForKey:firstPathPart]; + if (handler) { +#ifdef DEBUG + log(@"Route: %@", firstPathPart); +#endif + res=handler(url, method, headers, body); + } +#ifdef DEBUG + log(@"Response: %@", res); +#endif + + NMLogFine(@"%@", [logMessage componentsJoinedByString:@"\n"]); + // note: prevously was wrapping this in WarpToMain, but seems unnecessary + if (res) { + [self respondWithBody:res[@"body"] status:res[@"status"] contentType:res[@"contentType"] headers:res[@"headers"] handle:connectionHandle]; + } else { + [self respondWithBody:@"Not found\n" status:@(404) contentType:@"text/plain" headers:@{} handle:connectionHandle]; + } +} + +- (void)respondWithBody:(NSObject *)bodyObj status:(NSNumber *)status contentType:(NSString *)contentType headers:(NSDictionary *)headers handle:(NSFileHandle *)connectionHandle +{ + NSData *bodyData=nil; + if ([bodyObj isKindOfClass:[NSString class]]) { + bodyData=[(NSString *)bodyObj dataUsingEncoding:NSUTF8StringEncoding]; + contentType=[contentType stringByAppendingString:@"; charset=utf-8"]; + } + else if ([bodyObj isKindOfClass:[NSData class]]) { + bodyData=(NSData *)bodyObj; + } + else { + NMLogError(@"Bad bodyObj: %@", bodyObj); + bodyData=[NSData data]; + } + + // create response message + CFHTTPMessageRef response=CFHTTPMessageCreateResponse(kCFAllocatorDefault, [status integerValue], NULL, kCFHTTPVersion1_1); + [headers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull val, BOOL * _Nonnull stop) { + CFHTTPMessageSetHeaderFieldValue(response, (__bridge CFStringRef)key, (__bridge CFStringRef)val); + }]; + CFHTTPMessageSetHeaderFieldValue(response, CFSTR("Content-Type"), (__bridge CFStringRef)contentType); + CFHTTPMessageSetHeaderFieldValue(response, CFSTR("Content-Length"), (__bridge CFStringRef)[NSString stringWithFormat:@"%@", @(bodyData.length)]); + CFHTTPMessageSetBody(response, (__bridge CFDataRef)bodyData); + CFDataRef messageData = CFHTTPMessageCopySerializedMessage(response); + @try + { + [connectionHandle writeData:(__bridge NSData *)messageData]; + } + @catch (NSException *exception) + { + // Ignore the exception, it normally just means the client + // closed the connection from the other end. + } + @finally + { + CFRelease(messageData); + CFRelease(response); + } +} + +@end diff --git a/src/parse/guess_language.rs b/src/parse/guess_language.rs index ee8b3ea32..262fe4973 100644 --- a/src/parse/guess_language.rs +++ b/src/parse/guess_language.rs @@ -53,6 +53,7 @@ pub(crate) enum Language { Make, Newick, Nix, + ObjC, OCaml, OCamlInterface, Pascal, @@ -140,6 +141,7 @@ pub(crate) fn language_name(language: Language) -> &'static str { Make => "Make", Newick => "Newick", Nix => "Nix", + ObjC => "Objective-C", OCaml => "OCaml", OCamlInterface => "OCaml Interface", Pascal => "Pascal", @@ -317,6 +319,7 @@ pub(crate) fn language_globs(language: Language) -> Vec { ], Newick => &["*.nhx", "*.nwk", "*.nh"], Nix => &["*.nix"], + ObjC => &["*.m"], OCaml => &["*.ml"], OCamlInterface => &["*.mli"], Pascal => &["*.pas", "*.dfm", "*.dpr", "*.lpr", "*.pascal"], @@ -391,6 +394,24 @@ fn looks_like_hacklang(path: &Path, src: &str) -> bool { false } +/// Use a heuristic to determine if a '.h' file looks like Objective-C. +/// We look for a line starting with '#import', '@interface' or '@protocol' +/// near the top of the file. These keywords are not valid C or C++, so this +/// should not produce false positives. +fn looks_like_objc(path: &Path, src: &str) -> bool { + if let Some(extension) = path.extension() { + if extension == "h" { + return src.lines().take(100).any(|line| { + ["#import", "@interface", "@protocol"] + .iter() + .any(|keyword| line.starts_with(keyword)) + }); + } + } + + false +} + pub(crate) fn guess( path: &Path, src: &str, @@ -421,6 +442,9 @@ pub(crate) fn guess( if looks_like_hacklang(path, src) { return Some(Language::Hack); } + if looks_like_objc(path, src) { + return Some(Language::ObjC); + } if let Some(lang) = from_glob(path) { return Some(lang); } @@ -468,6 +492,7 @@ fn from_emacs_mode_header(src: &str) -> Option { "js" | "js2" => Some(JavaScript), "lisp" => Some(CommonLisp), "nxml" => Some(Xml), + "objc" => Some(ObjC), "perl" => Some(Perl), "python" => Some(Python), "racket" => Some(Racket), diff --git a/src/parse/tree_sitter_parser.rs b/src/parse/tree_sitter_parser.rs index 56d9b40c0..345f381b1 100644 --- a/src/parse/tree_sitter_parser.rs +++ b/src/parse/tree_sitter_parser.rs @@ -94,6 +94,7 @@ extern "C" { fn tree_sitter_make() -> ts::Language; fn tree_sitter_newick() -> ts::Language; fn tree_sitter_nix() -> ts::Language; + fn tree_sitter_objc() -> ts::Language; fn tree_sitter_ocaml() -> ts::Language; fn tree_sitter_ocaml_interface() -> ts::Language; fn tree_sitter_pascal() -> ts::Language; @@ -739,6 +740,27 @@ pub(crate) fn from_language(language: guess::Language) -> TreeSitterConfig { sub_languages: vec![], } } + ObjC => { + let language = unsafe { tree_sitter_objc() }; + TreeSitterConfig { + language, + atom_nodes: vec!["string_literal"].into_iter().collect(), + delimiter_tokens: vec![ + ("(", ")"), + ("{", "}"), + ("[", "]"), + ("@(", ")"), + ("@{", "}"), + ("@[", "]"), + ], + highlight_query: ts::Query::new( + language, + include_str!("../../vendored_parsers/highlights/objc.scm"), + ) + .unwrap(), + sub_languages: vec![], + } + } OCaml => { let language = unsafe { tree_sitter_ocaml() }; TreeSitterConfig { diff --git a/vendored_parsers/highlights/objc.scm b/vendored_parsers/highlights/objc.scm new file mode 120000 index 000000000..17560f1f2 --- /dev/null +++ b/vendored_parsers/highlights/objc.scm @@ -0,0 +1 @@ +../tree-sitter-objc/queries/highlights.scm \ No newline at end of file diff --git a/vendored_parsers/tree-sitter-objc-src b/vendored_parsers/tree-sitter-objc-src new file mode 120000 index 000000000..812b262c0 --- /dev/null +++ b/vendored_parsers/tree-sitter-objc-src @@ -0,0 +1 @@ +tree-sitter-objc/src \ No newline at end of file