-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathQSTextManipulationPlugIn.m
241 lines (200 loc) · 11.9 KB
/
QSTextManipulationPlugIn.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
//
// QSTextManipulationPlugIn.m
// QSTextManipulationPlugIn
//
// Created by Nicholas Jitkoff on 3/31/05.
// Copyright __MyCompanyName__ 2005. All rights reserved.
//
#import "QSTextManipulationPlugIn.h"
#define kQSTextAppendAction @"QSTextAppendAction"
#define kQSTextPrependAction @"QSTextPrependAction"
#define kQSLineRefEditAction @"QSLineRefEditAction"
#define kQSLineRefDeleteAction @"QSLineRefDeleteAction"
#define kQSTextAppendReverseAction @"QSTextAppendReverseAction"
#define kQSTextPrependReverseAction @"QSTextPrependReverseAction"
#define kQSTextAppendTimedAction @"QSTextAppendTimedAction"
#define kQSTextPrependTimedAction @"QSTextPrependTimedAction"
#define kQSTextAppendTimedReverseAction @"QSTextAppendTimedReverseAction"
#define kQSTextPrependTimedReverseAction @"QSTextPrependTimedReverseAction"
#define textTypes @[@"'TEXT'", @"txt", @"sh", @"pl", @"rb", @"html", @"htm",@"md",@"markdown", @"mdown", @"mkdn"]
#define richTextTypes @[@"rtf", @"doc", @"rtfd"]
@implementation QSTextManipulationPlugIn
- (id)init
{
self = [super init];
if (self) {
_dateFormatter = [[NSDateFormatter alloc] init];
[_dateFormatter setDateStyle:NSDateFormatterShortStyle];
[_dateFormatter setTimeStyle:NSDateFormatterShortStyle];
_reverseActions = @[kQSTextAppendReverseAction, kQSTextPrependReverseAction, kQSTextAppendTimedReverseAction, kQSTextPrependTimedReverseAction];
}
return self;
}
- (NSArray *)validIndirectObjectsForAction:(NSString *)action directObject:(QSObject *)dObject {
// for the Change To... select the current line to be changed
if ([action isEqualToString:kQSLineRefEditAction]) {
return [NSArray arrayWithObject:[QSObject textProxyObjectWithDefaultValue:[dObject stringValue]]];
} else if ([@[kQSTextPrependAction, kQSTextPrependTimedAction, kQSTextAppendAction, kQSTextAppendTimedAction] containsObject:action]) {
NSArray *fileObjects = [[QSLibrarian sharedInstance] arrayForType:QSFilePathType];
NSIndexSet *folderIndexes = [fileObjects indexesOfObjectsWithOptions:NSEnumerationConcurrent passingTest:^BOOL(QSObject *thisObject, NSUInteger i, BOOL *stop) {
QSObject *resolved = [thisObject resolvedAliasObject];
NSString *fileExtension = [[resolved fileExtension] lowercaseString];
return [resolved isFolder] || [textTypes containsObject:fileExtension] || [richTextTypes containsObject:fileExtension];
}];
return [@[[NSNull null]] arrayByAddingObjectsFromArray:[fileObjects objectsAtIndexes:folderIndexes]];
} else {
if ([self.reverseActions containsObject:action]) {
return @[[QSObject textProxyObjectWithDefaultValue:@""]];
}
}
return nil;
}
- (NSArray *)validActionsForDirectObject:(QSObject *)dObject indirectObject:(QSObject *)iObject {
// most methods are only set to work for 1 direct object
if ([dObject count] == 1) {
return [self.reverseActions arrayByAddingObjectsFromArray:@[kQSLineRefEditAction, kQSLineRefDeleteAction]];
// In the future, we may wish to make the Append Text... and Prepend Text... actions available for ALL files of type text
/* NSArray *validActions;
NSString *type = [[NSFileManager defaultManager] UTIOfFile:[dObject singleFilePath]];
if (UTTypeConformsTo((CFStringRef)type, kUTTypeText)) {
validActions = [NSArray arrayWithObjects:kQSTextAppendReverseAction,kQSTextPrependReverseAction,kQSLineRefEditAction,kQSLineRefDeleteAction,nil];
} else {
validActions = [NSArray arrayWithObjects:kQSLineRefDeleteAction,kQSLineRefEditAction,nil];
}
return validActions; */
}
return nil;
}
- (QSObject *)prependObject:(QSObject *)dObject toObject:(QSObject *)iObject {
return [self appendObject:(QSObject *)dObject toObject:(QSObject *)iObject withTimestamp:NO atBeginning:YES];
}
- (QSObject *)appendObject:(QSObject *)dObject toObject:(QSObject *)iObject {
return [self appendObject:(QSObject *)dObject toObject:(QSObject *)iObject withTimestamp:NO atBeginning:NO];
}
- (QSObject *)prependTimestampedObject:(QSObject *)dObject toObject:(QSObject *)iObject
{
return [self appendObject:(QSObject *)dObject toObject:(QSObject *)iObject withTimestamp:YES atBeginning:YES];
}
- (QSObject *)appendTimestampedObject:(QSObject *)dObject toObject:(QSObject *)iObject {
return [self appendObject:(QSObject *)dObject toObject:(QSObject *)iObject withTimestamp:YES atBeginning:NO];
}
- (QSObject *)appendObject:(QSObject *)dObject toObject:(QSObject *)iObject withTimestamp:(BOOL)includeTime atBeginning:(BOOL)atBeginning
{
NSMutableArray *lines = [NSMutableArray array];
NSString *newLine = nil;
NSString *timestamp = includeTime ? [self.dateFormatter stringFromDate:[NSDate date]] : nil;
for (QSObject *anObject in [dObject splitObjects]) {
// get the new line - file path for files or text (else string value - last case)
if ([[anObject primaryType] isEqualToString:QSFilePathType]) {
newLine = [[anObject singleFilePath] stringByAbbreviatingWithTildeInPath];
} else if([[anObject primaryType] isEqualToString:QSTextType]) {
newLine = [anObject objectForType:QSTextType];
} else {
newLine = [anObject stringValue];
}
// prepend a timestamp?
if (includeTime) {
newLine = [NSString stringWithFormat:@"%@ %@", timestamp, newLine];
}
[lines addObject:newLine];
}
newLine = [lines componentsJoinedByString:@"\n"];
// if we're within a file (QSLineReferenceType file)
if ([iObject containsType:@"QSLineReferenceType"]) {
NSDictionary *reference = [iObject objectForType:@"QSLineReferenceType"];
NSString *file = [[reference objectForKey:@"path"] stringByStandardizingPath];
NSStringEncoding encoding;
NSString *string = [NSString stringWithContentsOfFile:file usedEncoding:&encoding error:nil];
NSMutableArray *lines = [[string lines] mutableCopy];
NSUInteger lineIndex = [[reference objectForKey:@"line"] unsignedIntegerValue];
if (atBeginning) {
lineIndex--;
}
[lines insertObject:newLine atIndex:lineIndex];
[[lines componentsJoinedByString:@"\n"] writeToFile:file atomically:NO encoding:encoding error:nil];
} else {
NSString *path = [iObject singleFilePath];
NSString *type = [[NSFileManager defaultManager] typeOfFile:path];
// rich text
if (UTTypeConformsTo((__bridge CFStringRef)[iObject fileUTI], (__bridge CFStringRef)@"public.rtf") || [richTextTypes containsObject:type]) {
NSDictionary *docAttributes = nil;
NSError *error = nil;
NSMutableAttributedString *astring = [[NSMutableAttributedString alloc] initWithURL:[NSURL fileURLWithPath:path] options:@{} documentAttributes:&docAttributes error:&error];
NSDictionary *attributes = [astring attributesAtIndex:atBeginning ? 0 : [astring length]-1
effectiveRange:nil];
NSAttributedString *newlineString = [[NSAttributedString alloc] initWithString:@"\n" attributes:attributes];
NSAttributedString *appendString = [[NSAttributedString alloc] initWithString:newLine attributes:attributes];
if (atBeginning) {
[astring insertAttributedString:newlineString atIndex:0];
[astring insertAttributedString:appendString atIndex:0];
} else {
unichar lastChar = [[astring string] characterAtIndex:[astring length] - 1];
BOOL newlineAtEnd = lastChar == '\r' || lastChar == '\n';
if (!newlineAtEnd) [astring appendAttributedString:newlineString];
[astring appendAttributedString:appendString];
if (newlineAtEnd) [astring appendAttributedString:newlineString];
}
NSFileWrapper *wrapper = [astring fileWrapperFromRange:NSMakeRange(0, [astring length])
documentAttributes:docAttributes error:&error];
if (!error)
[wrapper writeToFile:path atomically:NO updateFilenames:YES];
} else if (UTTypeConformsTo((__bridge CFStringRef)[iObject fileUTI], (__bridge CFStringRef)@"public.text") || [textTypes containsObject:type]) {
NSStringEncoding encoding;
NSString *text = [NSString stringWithContentsOfFile:path usedEncoding:&encoding error:nil];
if (atBeginning || ![text length]) {
text = [NSString stringWithFormat:@"%@\n%@", newLine , text];
} else {
unichar lastChar = [text characterAtIndex:[text length] -1];
BOOL newlineAtEnd = lastChar == '\r' || lastChar == '\n';
text = [NSString stringWithFormat:newlineAtEnd?@"%@%@\n":@"%@\n%@", text, newLine];
}
[text writeToFile:path atomically:NO encoding:encoding error:nil];
} else {
QSShowAppNotifWithAttributes(@"QSTextManipulation", NSLocalizedStringFromTableInBundle(@"Error Appending Text", nil, [NSBundle bundleForClass:[self class]], @"Title for the error notif when appending text fails"), [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"Cannot append text to %@ files", nil, [NSBundle bundleForClass:[self class]], @"MEssage of the error notif when appending text fails"),type]);
}
}
if ([iObject containsType:@"QSLineReferenceType"]) {
// if it's a line type, return the original file (get it from the object's 'path' key)
return [QSObject fileObjectWithPath:[[[iObject objectForType:@"QSLineReferenceType"] objectForKey:@"path"] stringByStandardizingPath]];
} else {
//return the original file
return iObject;
}
}
- (QSObject *)deleteLineReference:(QSObject *)dObject {
NSString *file = [[[dObject objectForType:@"QSLineReferenceType"] objectForKey:@"path"] stringByStandardizingPath];
NSNumber *line = [[dObject objectForType:@"QSLineReferenceType"] objectForKey:@"line"];
NSUInteger lineNum = [line unsignedIntegerValue] -1;
NSStringEncoding encoding;
NSString *string = [NSString stringWithContentsOfFile:file usedEncoding:&encoding error:nil];
string = [string stringByReplacingOccurrencesOfString:@"\n" withString:@"\r"];
NSMutableArray *lines = [[string componentsSeparatedByString:@"\r"] mutableCopy];
NSString *fileLine = [lines objectAtIndex:lineNum];
if ([[dObject stringValue] isEqualToString:fileLine]) {
[lines removeObjectAtIndex:lineNum];
[[lines componentsJoinedByString:@"\n"] writeToFile:file atomically:NO encoding:encoding error:nil];
} else {
NSBeep();
QSShowNotifierWithAttributes([NSDictionary dictionaryWithObjectsAndKeys:@"QSTextManipulationNotification", QSNotifierType, [QSResourceManager imageNamed:@"com.blacktree.quicksilver"], QSNotifierIcon, @"Text Manipulation", QSNotifierTitle, @"Contents of file have changed. Line was not deleted.", QSNotifierText, nil]);
}
return [QSObject fileObjectWithPath:file];
}
- (QSObject *)changeLineReference:(QSObject *)dObject to:(QSObject *)iObject {
NSString *file = [[[dObject objectForType:@"QSLineReferenceType"] objectForKey:@"path"] stringByStandardizingPath];
NSNumber *line = [[dObject objectForType:@"QSLineReferenceType"] objectForKey:@"line"];
NSUInteger lineNum = [line unsignedIntegerValue] -1;
NSString *replacement = [iObject stringValue];
NSStringEncoding encoding;
NSString *string = [NSString stringWithContentsOfFile:file usedEncoding:&encoding error:nil];
NSMutableArray *lines = [[string lines] mutableCopy];
NSString *fileLine = [lines objectAtIndex:lineNum];
if ([[dObject stringValue] isEqualToString:fileLine]) {
[lines replaceObjectAtIndex:lineNum withObject:replacement];
[[lines componentsJoinedByString:@"\n"] writeToFile:file atomically:NO encoding:encoding error:nil];
} else {
NSBeep();
QSShowNotifierWithAttributes([NSDictionary dictionaryWithObjectsAndKeys:@"QSTextManipulationNotification", QSNotifierType, [QSResourceManager imageNamed:@"com.blacktree.quicksilver"], QSNotifierIcon, @"Text Manipulation", QSNotifierTitle, @"Contents of file have changed. Change was abandoned.", QSNotifierText, nil]);
}
return [QSObject fileObjectWithPath:file];
}
@end