-
Notifications
You must be signed in to change notification settings - Fork 288
/
TRVSFormatter.m
328 lines (268 loc) · 12.1 KB
/
TRVSFormatter.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
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
//
// TRVSFormatter.m
// ClangFormat
//
// Created by Travis Jeffery on 1/9/14.
// Copyright (c) 2014 Travis Jeffery. All rights reserved.
//
#import "TRVSFormatter.h"
#import "TRVSXcode.h"
#import "TRVSCodeFragment.h"
#import "NSDocument+TRVSClangFormat.h"
@interface TRVSFormatter ()
@property (nonatomic, copy) NSSet *supportedFileTypes;
@end
@implementation TRVSFormatter
+ (instancetype)sharedFormatter {
static id sharedFormatter = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedFormatter = [[self alloc] initWithStyle:nil
executablePath:nil
useSystemClangFormat:NO];
});
return sharedFormatter;
}
- (instancetype)initWithStyle:(NSString *)style
executablePath:(NSString *)executablePath
useSystemClangFormat:(BOOL)useSystemClangFormat {
if (self = [self init]) {
self.style = style;
self.executablePath = executablePath;
self.useSystemClangFormat = useSystemClangFormat;
}
return self;
}
- (void)formatActiveFile {
[self formatRanges:
@[ [NSValue valueWithRange:[TRVSXcode wholeRangeOfTextView]] ]
inDocument:[TRVSXcode sourceCodeDocument]];
}
- (void)formatSelectedCharacters {
// Even if there is no selection, go one and perform a format with 0-length
// range. This will format the statement under cursor.
[self formatRanges:[[TRVSXcode textView] selectedRanges]
inDocument:[TRVSXcode sourceCodeDocument]];
}
- (void)formatSelectedFiles {
[[TRVSXcode selectedFileNavigableItems]
enumerateObjectsUsingBlock:^(IDEFileNavigableItem *fileNavigableItem,
NSUInteger idx,
BOOL *stop) {
NSDocument *document = [IDEDocumentController
retainedEditorDocumentForNavigableItem:fileNavigableItem
error:NULL];
if ([document
isKindOfClass:NSClassFromString(@"IDESourceCodeDocument")]) {
IDESourceCodeDocument *sourceCodeDocument =
(IDESourceCodeDocument *)document;
[self
formatRanges:
@[
[NSValue
valueWithRange:
NSMakeRange(
0, [[sourceCodeDocument textStorage] length])]
]
inDocument:sourceCodeDocument];
[document saveDocument:nil];
}
[IDEDocumentController releaseEditorDocument:document];
}];
}
- (void)formatDocument:(IDESourceCodeDocument *)document {
NSRect rect = [[TRVSXcode textView] visibleRect];
NSPoint containerOrigin = [[TRVSXcode textView] textContainerOrigin];
rect = NSOffsetRect(rect, -containerOrigin.x, -containerOrigin.y);
NSRange glyphRange = [[[TRVSXcode textView] layoutManager]
glyphRangeForBoundingRect:rect
inTextContainer:[[TRVSXcode textView] textContainer]];
NSRange charRange = [[[TRVSXcode textView] layoutManager]
characterRangeForGlyphRange:glyphRange
actualGlyphRange:NULL];
NSUInteger location = [[TRVSXcode textView] selectedRange].location;
NSUInteger length = [[document textStorage] length];
[self formatRanges:@[ [NSValue valueWithRange:NSMakeRange(0, length)] ]
inDocument:document];
NSUInteger textStorageLength = [[document textStorage] length];
NSUInteger diff = MAX(length, textStorageLength) - MIN(length, textStorageLength);
BOOL documentIsLongerAfterFormatting =
length > [[document textStorage] length];
if (documentIsLongerAfterFormatting && location > diff) {
location -= diff;
charRange.length -= diff;
} else if (!documentIsLongerAfterFormatting) {
location += diff;
charRange.length += diff;
}
NSRange range = NSMakeRange(location, 0);
[[TRVSXcode textView] setSelectedRange:range];
[[TRVSXcode textView] scrollRangeToVisible:charRange];
}
#pragma mark - Private
- (void)formatRanges:(NSArray *)ranges
inDocument:(IDESourceCodeDocument *)document {
if (![document trvs_shouldFormat])
return;
DVTSourceTextStorage *textStorage = [document textStorage];
NSArray *lineRanges =
[self lineRangesOfCharacterRanges:ranges usingTextStorage:textStorage];
NSArray *continuousLineRanges =
[self continuousLineRangesOfRanges:lineRanges];
[self
fragmentsOfContinuousLineRanges:continuousLineRanges
usingTextStorage:textStorage
withDocument:document
block:^(NSArray *fragments, NSArray *errors) {
if (errors.count == 0) {
NSArray *selectionRanges = [self
selectionRangesAfterReplacingFragments:
fragments usingTextStorage:
textStorage
withDocument:
document];
if (selectionRanges.count > 0)
[[TRVSXcode textView]
setSelectedRanges:selectionRanges];
} else {
NSAlert *alert = [NSAlert new];
alert.messageText =
[(NSError *)errors.firstObject
localizedDescription];
[alert runModal];
}
}];
}
- (NSArray *)
selectionRangesAfterReplacingFragments:(NSArray *)fragments
usingTextStorage:(DVTSourceTextStorage *)textStorage
withDocument:(IDESourceCodeDocument *)document {
NSMutableArray *selectionRanges = [[NSMutableArray alloc] init];
[fragments enumerateObjectsUsingBlock:^(TRVSCodeFragment *fragment,
NSUInteger idx,
BOOL *stop) {
[textStorage beginEditing];
[textStorage replaceCharactersInRange:fragment.rangeToReplace
withString:fragment.formattedString
withUndoManager:document.undoManager];
[self addSelectedRangeToSelectedRanges:selectionRanges
usingTextStorage:textStorage];
[textStorage endEditing];
}];
return selectionRanges;
}
- (void)addSelectedRangeToSelectedRanges:(NSMutableArray *)selectionRanges
usingTextStorage:(DVTSourceTextStorage *)textStorage {
if (selectionRanges.count > 0) {
NSUInteger i = 0;
while (i < selectionRanges.count) {
NSRange range = [[selectionRanges objectAtIndex:i] rangeValue];
range.location += [textStorage changeInLength];
[selectionRanges replaceObjectAtIndex:i
withObject:[NSValue valueWithRange:range]];
i++;
}
}
NSRange editedRange = textStorage.editedRange;
if (editedRange.location != NSNotFound) {
[selectionRanges addObject:[NSValue valueWithRange:editedRange]];
}
}
- (void)fragmentsOfContinuousLineRanges:(NSArray *)continuousLineRanges
usingTextStorage:(DVTSourceTextStorage *)textStorage
withDocument:(IDESourceCodeDocument *)document
block:(void (^)(NSArray *fragments,
NSArray *errors))block {
NSMutableArray *fragments = [[NSMutableArray alloc] init];
NSMutableArray *errors = [[NSMutableArray alloc] init];
NSString *executablePath = self.executablePath;
if (self.useSystemClangFormat) {
NSDictionary *environmentDict = [[NSProcessInfo processInfo] environment];
NSString *shellString =
[environmentDict objectForKey:@"SHELL"] ?: @"/bin/bash";
NSPipe *outputPipe = [NSPipe pipe];
NSPipe *errorPipe = [NSPipe pipe];
NSTask *task = [[NSTask alloc] init];
task.standardOutput = outputPipe;
task.standardError = errorPipe;
task.launchPath = shellString;
task.arguments = @[ @"-c", @"which clang-format" ];
[task launch];
[task waitUntilExit];
[errorPipe.fileHandleForReading readDataToEndOfFile];
NSData *outputData = [outputPipe.fileHandleForReading readDataToEndOfFile];
NSString *outputPath = [[NSString alloc] initWithData:outputData
encoding:NSUTF8StringEncoding];
outputPath = [outputPath
stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
BOOL isDirectory = NO;
if ([[NSFileManager defaultManager] fileExistsAtPath:outputPath isDirectory:&isDirectory] && !isDirectory) {
executablePath = outputPath;
}
}
[continuousLineRanges enumerateObjectsUsingBlock:^(NSValue *rangeValue,
NSUInteger idx,
BOOL *stop) {
NSRange characterRange =
[textStorage characterRangeForLineRange:[rangeValue rangeValue]];
if (characterRange.location == NSNotFound)
return;
NSString *string =
[[textStorage string] substringWithRange:characterRange];
if (!string.length)
return;
TRVSCodeFragment *fragment = [TRVSCodeFragment
fragmentUsingBlock:^(TRVSCodeFragmentBuilder *builder) {
builder.string = [textStorage string];
builder.textRange = characterRange;
builder.fileURL = document.fileURL;
}];
// clang-format doesn't support descrete ranges, so only the first range
// can be taken. This is fine because in practice, we have only one
// selected range in Xcode.
NSRange lineRange = [continuousLineRanges[0] rangeValue];
__weak typeof(fragment) weakFragment = fragment;
[fragment formatWithStyle:self.style
usingClangFormatAtLaunchPath:executablePath
lineRange:lineRange
block:^(NSString *formattedString,
NSError *error) {
__strong typeof(weakFragment)
strongFragment = weakFragment;
if (error) {
[errors addObject:error];
*stop = YES;
} else {
[fragments addObject:strongFragment];
}
}];
}];
block(fragments, errors);
}
- (NSArray *)lineRangesOfCharacterRanges:(NSArray *)characterRanges
usingTextStorage:(DVTSourceTextStorage *)textStorage {
NSMutableArray *lineRanges = [[NSMutableArray alloc] init];
[characterRanges enumerateObjectsUsingBlock:^(NSValue *rangeValue,
NSUInteger idx,
BOOL *stop) {
[lineRanges
addObject:[NSValue valueWithRange:[textStorage
lineRangeForCharacterRange:
[rangeValue rangeValue]]]];
}];
return lineRanges;
}
- (NSArray *)continuousLineRangesOfRanges:(NSArray *)ranges {
NSMutableIndexSet *indexSet = [NSMutableIndexSet indexSet];
[ranges enumerateObjectsUsingBlock:^(NSValue *rangeValue,
NSUInteger idx,
BOOL *stop) {
[indexSet addIndexesInRange:[rangeValue rangeValue]];
}];
NSMutableArray *continuousRanges = [[NSMutableArray alloc] init];
[indexSet enumerateRangesUsingBlock:^(NSRange range, BOOL *stop) {
[continuousRanges addObject:[NSValue valueWithRange:range]];
}];
return continuousRanges;
}
@end