Expand variables after an outline view item expands.
[macgdbp.git] / Source / BSSourceView.mm
1 /*
2 * MacGDBp
3 * Copyright (c) 2007 - 2011, Blue Static <http://www.bluestatic.org>
4 *
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
10 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 * General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
15 */
16
17 #import "BSSourceView.h"
18
19 #import "BSLineNumberRulerView.h"
20 #import "BSSourceViewTextView.h"
21
22 @interface BSSourceView (Private)
23 - (void)setupViews;
24 - (void)errorHighlightingFile:(NSNotification*)notif;
25 - (void)setPlainTextStringFromFile:(NSString*)filePath;
26 @end
27
28 @implementation BSSourceView
29
30 @synthesize textView = textView_;
31 @synthesize scrollView = scrollView_;
32 @synthesize markers = markers_;
33 @synthesize markedLine = markedLine_;
34 @synthesize delegate = delegate_;
35 @synthesize file = file_;
36
37 /**
38 * Initializes the source view with the path of a file
39 */
40 - (id)initWithFrame:(NSRect)frame
41 {
42 if (self = [super initWithFrame:frame]) {
43 [self setupViews];
44 [[NSNotificationCenter defaultCenter]
45 addObserver:self
46 selector:@selector(errorHighlightingFile:)
47 name:NSFileHandleReadToEndOfFileCompletionNotification
48 object:nil
49 ];
50 }
51 return self;
52 }
53
54 - (void)setMarkers:(NSSet*)markers {
55 markers_ = [markers copy];
56 [ruler_ setNeedsDisplay:YES];
57 }
58
59 - (void)setMarkedLine:(NSUInteger)markedLine {
60 markedLine_ = markedLine;
61 [ruler_ setNeedsDisplay:YES];
62 }
63
64 /**
65 * Reads the contents of file at |f| and sets the source viewer and filename
66 * as such.
67 */
68 - (void)setFile:(NSString*)f
69 {
70 file_ = f;
71
72 if (![[NSFileManager defaultManager] fileExistsAtPath:f]) {
73 [textView_ setString:@""];
74 return;
75 }
76
77 [self setSource:f completionHandler:nil];
78 }
79
80 /**
81 * Sets the contents of the SourceView to |source| representing the file at |path|.
82 */
83 - (void)setString:(NSString*)source asFile:(NSString*)path
84 {
85 file_ = path;
86
87 // Write the source out as a temporary file so it can be highlighted.
88 NSError* error = nil;
89 NSString* tmpPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"MacGDBpHighlighter"];
90 [source writeToFile:tmpPath atomically:NO encoding:NSUTF8StringEncoding error:&error];
91 if (error) {
92 [textView_ setString:source];
93 return;
94 }
95
96 [self setSource:tmpPath completionHandler:^{
97 [[NSFileManager defaultManager] removeItemAtPath:tmpPath error:NULL];
98 }];
99 }
100
101 /**
102 * Flip the coordinates
103 */
104 - (BOOL)isFlipped
105 {
106 return YES;
107 }
108
109 /**
110 * Tells the text view to scroll to a certain line
111 */
112 - (void)scrollToLine:(NSUInteger)line
113 {
114 if ([[textView_ textStorage] length] == 0)
115 return;
116
117 // go through the document until we find the NSRange for the line we want
118 NSUInteger rangeIndex = 0;
119 for (NSUInteger i = 0; i < line; i++) {
120 rangeIndex = NSMaxRange([[textView_ string] lineRangeForRange:NSMakeRange(rangeIndex, 0)]);
121 }
122
123 // now get the true start/end markers for it
124 NSUInteger lineStart, lineEnd;
125 [[textView_ string] getLineStart:&lineStart
126 end:NULL
127 contentsEnd:&lineEnd
128 forRange:NSMakeRange(rangeIndex - 1, 0)];
129 [textView_ scrollRangeToVisible:[[textView_ string]
130 lineRangeForRange:NSMakeRange(lineStart, lineEnd - lineStart)]];
131 }
132
133 /**
134 * Returns the preferred font for source views.
135 */
136 + (NSFont*)sourceFont
137 {
138 static NSFont* font = nil;
139 if (!font) {
140 font = [NSFont fontWithName:@"Menlo" size:12];
141 if (!font)
142 font = [NSFont fontWithName:@"Monaco" size:12.0];
143 }
144 return font;
145 }
146
147 /**
148 * Setup all the subviews for the source metaview
149 */
150 - (void)setupViews
151 {
152 // Create the scroll view.
153 scrollView_ = [[NSScrollView alloc] initWithFrame:[self bounds]];
154 [scrollView_ setHasHorizontalScroller:YES];
155 [scrollView_ setHasVerticalScroller:YES];
156 [scrollView_ setAutohidesScrollers:YES];
157 [scrollView_ setBorderType:NSBezelBorder];
158 [scrollView_ setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
159 [[scrollView_ contentView] setAutoresizesSubviews:YES];
160 [self addSubview:scrollView_];
161
162 // add the text view to the scroll view
163 NSRect textFrame;
164 textFrame.origin = NSMakePoint(0.0, 0.0);
165 textFrame.size = [scrollView_ contentSize];
166 textView_ = [[BSSourceViewTextView alloc] initWithFrame:textFrame];
167 [textView_ setSourceView:self];
168 [textView_ setEditable:NO];
169 [textView_ setFont:[[self class] sourceFont]];
170 [textView_ setHorizontallyResizable:YES];
171 [textView_ setVerticallyResizable:YES];
172 [textView_ setMinSize:textFrame.size];
173 [textView_ setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)];
174 [[textView_ textContainer] setContainerSize:NSMakeSize(FLT_MAX, FLT_MAX)];
175 [[textView_ textContainer] setWidthTracksTextView:NO];
176 [[textView_ textContainer] setHeightTracksTextView:NO];
177 [textView_ setAutoresizingMask:NSViewNotSizable];
178 [scrollView_ setDocumentView:textView_];
179
180 // Set up the ruler.
181 ruler_ = [[BSLineNumberRulerView alloc] initWithSourceView:self];
182 [scrollView_ setVerticalRulerView:ruler_];
183 [scrollView_ setHasHorizontalRuler:NO];
184 [scrollView_ setHasVerticalRuler:YES];
185 [scrollView_ setRulersVisible:YES];
186
187 NSArray* types = [NSArray arrayWithObject:NSFilenamesPboardType];
188 [self registerForDraggedTypes:types];
189 }
190
191 NSString* ColorHEXStringINIDirective(NSString* directive, NSColor* color) {
192 CGFloat red, green, blue, alpha;
193 color = [color colorUsingColorSpaceName:NSCalibratedRGBColorSpace];
194 [color getRed:&red green:&green blue:&blue alpha:&alpha];
195 return [NSString stringWithFormat:@"%@=\"#%02x%02x%02x\"", directive, (UInt8)(red * 255), (UInt8)(green * 255), (UInt8)(blue * 255)];
196 }
197
198 /**
199 * Reads the contents of |filePath| and sets it as the displayed text, after
200 * attempting to highlight it using the PHP binary.
201 */
202 - (void)setSource:(NSString*)filePath completionHandler:(void(^)(void))handler
203 {
204 @try {
205 // Attempt to use the PHP CLI to highlight the source file as HTML
206 NSPipe* outPipe = [NSPipe pipe];
207 NSPipe* errPipe = [NSPipe pipe];
208 NSTask* task = [[NSTask alloc] init];
209
210 [task setLaunchPath:@"/usr/bin/php"]; // This is the path to the default Leopard PHP executable
211 [task setArguments:@[
212 @"--syntax-highlight",
213 @"--define", ColorHEXStringINIDirective(@"highlight.string", [NSColor systemRedColor]),
214 @"--define", ColorHEXStringINIDirective(@"highlight.comment", [NSColor systemOrangeColor]),
215 @"--define", ColorHEXStringINIDirective(@"highlight.default", [NSColor systemBlueColor]),
216 @"--define", ColorHEXStringINIDirective(@"highlight.html", [NSColor systemGrayColor]),
217 filePath
218 ]];
219 [task setStandardOutput:outPipe];
220 [task setStandardError:errPipe];
221 [task setTerminationHandler:^(NSTask* taskBlock) {
222 if (task.terminationStatus != 0) {
223 NSLog(@"Failed to highlight PHP file %@. Termination status=%d. stderr: %@",
224 filePath, taskBlock.terminationStatus, [[errPipe fileHandleForReading] readDataToEndOfFile]);
225 }
226 }];
227 [task launch];
228
229 // Start reading the stdout pipe on a background queue. This is separate
230 // from the terminiationHandler, since a large file could be greater than
231 // the pipe buffer.
232 dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
233 NSMutableAttributedString* source;
234 NSData* data = [[outPipe fileHandleForReading] readDataToEndOfFile];
235 if (data.length) {
236 source = [[NSMutableAttributedString alloc] initWithHTML:data
237 options:@{ NSCharacterEncodingDocumentAttribute : @(NSUTF8StringEncoding) }
238 documentAttributes:nil];
239
240 // PHP uses &nbsp; in the highlighted output, which should be converted
241 // back to normal spaces.
242 NSMutableString* stringData = [source mutableString];
243 [stringData replaceOccurrencesOfString:@"\u00A0" withString:@" " options:0 range:NSMakeRange(0, stringData.length)];
244
245 // Override the default font from Courier.
246 [source addAttributes:@{ NSFontAttributeName : [[self class] sourceFont] }
247 range:NSMakeRange(0, source.length)];
248 }
249
250 dispatch_async(dispatch_get_main_queue(), ^{
251 if (source) {
252 [[self->textView_ textStorage] setAttributedString:source];
253 } else {
254 [self setPlainTextStringFromFile:filePath];
255 }
256
257 [self->ruler_ performLayout];
258
259 [self scrollToLine:self->markedLine_];
260
261 if (handler)
262 handler();
263 });
264 });
265 } @catch (NSException* exception) {
266 // If the PHP executable is not available then the NSTask will throw an exception
267 NSLog(@"Failed to highlight file: %@", exception);
268 [self setPlainTextStringFromFile:filePath];
269 }
270 }
271
272 /**
273 * Gets the plain-text representation of the file at |filePath| and sets the
274 * contents in the source view.
275 */
276 - (void)setPlainTextStringFromFile:(NSString*)filePath
277 {
278 NSError* error = nil;
279 NSString* contents = [NSString stringWithContentsOfFile:filePath
280 encoding:NSUTF8StringEncoding
281 error:&error];
282 if (error) {
283 NSLog(@"Error reading file at %@: %@", filePath, error);
284 if ([delegate_ respondsToSelector:@selector(error:whileHighlightingFile:)]) {
285 [delegate_ error:error whileHighlightingFile:filePath];
286 }
287 return;
288 }
289 [textView_ setString:contents];
290 }
291
292 // Drag Handlers ///////////////////////////////////////////////////////////////
293
294 /**
295 * Validates an initiated drag operation.
296 */
297 - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender
298 {
299 return NSDragOperationCopy;
300 }
301
302 /**
303 * Performs a dragging operation of files to set the contents of the file.
304 */
305 - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender
306 {
307 NSPasteboard* pboard = [sender draggingPasteboard];
308 if ([[pboard types] containsObject:NSFilenamesPboardType]) {
309 NSArray* files = [pboard propertyListForType:NSFilenamesPboardType];
310 if ([files count]) {
311 NSString* filename = [files objectAtIndex:0];
312 [self setFile:filename];
313 return YES;
314 }
315 }
316 return NO;
317 }
318
319 @end