Modernize the source view classes.
[macgdbp.git] / Source / BSSourceView.m
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 BSSourceViewTextView* _textView;
30 BSLineNumberRulerView* _ruler;
31 NSScrollView* _scrollView;
32
33 // Line numbers to mark.
34 NSSet<NSNumber*>* _markers;
35
36 NSString* _file;
37 }
38
39 - (id)initWithFrame:(NSRect)frame
40 {
41 if (self = [super initWithFrame:frame]) {
42 [self setupViews];
43 }
44 return self;
45 }
46
47 - (void)setMarkers:(NSSet*)markers {
48 _markers = [markers copy];
49 [_ruler setNeedsDisplay:YES];
50 }
51
52 - (void)setMarkedLine:(NSUInteger)markedLine {
53 _markedLine = markedLine;
54 [_ruler setNeedsDisplay:YES];
55 [_textView setNeedsDisplay:YES];
56 }
57
58 /**
59 * Reads the contents of file at |f| and sets the source viewer and filename
60 * as such.
61 */
62 - (void)setFile:(NSString*)f
63 {
64 _file = [f copy];
65
66 if (![[NSFileManager defaultManager] fileExistsAtPath:f]) {
67 [_textView setString:@""];
68 return;
69 }
70
71 [self setSource:f completionHandler:nil];
72 }
73
74 /**
75 * Sets the contents of the SourceView to |source| representing the file at |path|.
76 */
77 - (void)setString:(NSString*)source asFile:(NSString*)path
78 {
79 _file = [path copy];
80
81 // Write the source out as a temporary file so it can be highlighted.
82 NSError* error = nil;
83 NSString* tmpPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"MacGDBpHighlighter"];
84 [source writeToFile:tmpPath atomically:NO encoding:NSUTF8StringEncoding error:&error];
85 if (error) {
86 [_textView setString:source];
87 return;
88 }
89
90 [self setSource:tmpPath completionHandler:^{
91 [[NSFileManager defaultManager] removeItemAtPath:tmpPath error:NULL];
92 }];
93 }
94
95 /**
96 * Flip the coordinates
97 */
98 - (BOOL)isFlipped
99 {
100 return YES;
101 }
102
103 /**
104 * Tells the text view to scroll to a certain line
105 */
106 - (void)scrollToLine:(NSUInteger)line
107 {
108 if ([[_textView textStorage] length] == 0)
109 return;
110
111 // go through the document until we find the NSRange for the line we want
112 NSUInteger rangeIndex = 0;
113 for (NSUInteger i = 0; i < line; i++) {
114 rangeIndex = NSMaxRange([[_textView string] lineRangeForRange:NSMakeRange(rangeIndex, 0)]);
115 }
116
117 // now get the true start/end markers for it
118 NSUInteger lineStart, lineEnd;
119 [[_textView string] getLineStart:&lineStart
120 end:NULL
121 contentsEnd:&lineEnd
122 forRange:NSMakeRange(rangeIndex - 1, 0)];
123 [_textView scrollRangeToVisible:[[_textView string]
124 lineRangeForRange:NSMakeRange(lineStart, lineEnd - lineStart)]];
125 }
126
127 /**
128 * Returns the preferred font for source views.
129 */
130 + (NSFont*)sourceFont
131 {
132 static NSFont* font = nil;
133 if (!font) {
134 font = [NSFont fontWithName:@"Menlo" size:12];
135 if (!font)
136 font = [NSFont fontWithName:@"Monaco" size:12.0];
137 }
138 return font;
139 }
140
141 /**
142 * Setup all the subviews for the source metaview
143 */
144 - (void)setupViews
145 {
146 // Create the scroll view.
147 _scrollView = [[NSScrollView alloc] initWithFrame:[self bounds]];
148 [_scrollView setHasHorizontalScroller:YES];
149 [_scrollView setHasVerticalScroller:YES];
150 [_scrollView setAutohidesScrollers:YES];
151 [_scrollView setBorderType:NSBezelBorder];
152 [_scrollView setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
153 [[_scrollView contentView] setAutoresizesSubviews:YES];
154 [self addSubview:_scrollView];
155
156 // add the text view to the scroll view
157 NSRect textFrame;
158 textFrame.origin = NSMakePoint(0.0, 0.0);
159 textFrame.size = [_scrollView contentSize];
160 _textView = [[BSSourceViewTextView alloc] initWithFrame:textFrame];
161 [_textView setSourceView:self];
162 [_textView setEditable:NO];
163 [_textView setFont:[[self class] sourceFont]];
164 [_textView setHorizontallyResizable:YES];
165 [_textView setVerticallyResizable:YES];
166 [_textView setMinSize:textFrame.size];
167 [_textView setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)];
168 [[_textView textContainer] setContainerSize:NSMakeSize(FLT_MAX, FLT_MAX)];
169 [[_textView textContainer] setWidthTracksTextView:NO];
170 [[_textView textContainer] setHeightTracksTextView:NO];
171 [_textView setAutoresizingMask:NSViewNotSizable];
172 [_scrollView setDocumentView:_textView];
173
174 // Set up the ruler.
175 _ruler = [[BSLineNumberRulerView alloc] initWithSourceView:self];
176 [_scrollView setVerticalRulerView:_ruler];
177 [_scrollView setHasHorizontalRuler:NO];
178 [_scrollView setHasVerticalRuler:YES];
179 [_scrollView setRulersVisible:YES];
180
181 [self registerForDraggedTypes:@[ NSFilenamesPboardType ]];
182 }
183
184 NSString* ColorHEXStringINIDirective(NSString* directive, NSColor* color) {
185 CGFloat red, green, blue, alpha;
186 color = [color colorUsingColorSpaceName:NSCalibratedRGBColorSpace];
187 [color getRed:&red green:&green blue:&blue alpha:&alpha];
188 return [NSString stringWithFormat:@"%@=\"#%02x%02x%02x\"", directive, (UInt8)(red * 255), (UInt8)(green * 255), (UInt8)(blue * 255)];
189 }
190
191 /**
192 * Reads the contents of |filePath| and sets it as the displayed text, after
193 * attempting to highlight it using the PHP binary.
194 */
195 - (void)setSource:(NSString*)filePath completionHandler:(void(^)(void))handler
196 {
197 @try {
198 // Attempt to use the PHP CLI to highlight the source file as HTML
199 NSPipe* outPipe = [NSPipe pipe];
200 NSPipe* errPipe = [NSPipe pipe];
201 NSTask* task = [[NSTask alloc] init];
202
203 [task setLaunchPath:@"/usr/bin/php"]; // This is the path to the default Leopard PHP executable
204 [task setArguments:@[
205 @"--syntax-highlight",
206 @"--define", ColorHEXStringINIDirective(@"highlight.string", [NSColor systemRedColor]),
207 @"--define", ColorHEXStringINIDirective(@"highlight.comment", [NSColor systemOrangeColor]),
208 @"--define", ColorHEXStringINIDirective(@"highlight.default", [NSColor systemBlueColor]),
209 @"--define", ColorHEXStringINIDirective(@"highlight.html", [NSColor systemGrayColor]),
210 filePath
211 ]];
212 [task setStandardOutput:outPipe];
213 [task setStandardError:errPipe];
214 [task setTerminationHandler:^(NSTask* taskBlock) {
215 if (task.terminationStatus != 0) {
216 NSLog(@"Failed to highlight PHP file %@. Termination status=%d. stderr: %@",
217 filePath, taskBlock.terminationStatus, [[errPipe fileHandleForReading] readDataToEndOfFile]);
218 }
219 }];
220 [task launch];
221
222 // Start reading the stdout pipe immediately. This is separate from the
223 // terminiationHandler, since a large file could be greater than
224 // the pipe buffer.
225 NSMutableAttributedString* source;
226 NSData* data = [[outPipe fileHandleForReading] readDataToEndOfFile];
227 if (data.length) {
228 source = [[NSMutableAttributedString alloc] initWithHTML:data
229 options:@{ NSCharacterEncodingDocumentAttribute : @(NSUTF8StringEncoding) }
230 documentAttributes:nil];
231
232 // PHP uses &nbsp; in the highlighted output, which should be converted
233 // back to normal spaces.
234 NSMutableString* stringData = [source mutableString];
235 [stringData replaceOccurrencesOfString:@"\u00A0" withString:@" " options:0 range:NSMakeRange(0, stringData.length)];
236
237 // Override the default font from Courier.
238 [source addAttributes:@{ NSFontAttributeName : [[self class] sourceFont] }
239 range:NSMakeRange(0, source.length)];
240 }
241
242 if (source) {
243 [[_textView textStorage] setAttributedString:source];
244 } else {
245 [self setPlainTextStringFromFile:filePath];
246 }
247
248 [_ruler performLayout];
249
250 if (handler) {
251 handler();
252 }
253 } @catch (NSException* exception) {
254 // If the PHP executable is not available then the NSTask will throw an exception
255 NSLog(@"Failed to highlight file: %@", exception);
256 [self setPlainTextStringFromFile:filePath];
257 }
258 }
259
260 /**
261 * Gets the plain-text representation of the file at |filePath| and sets the
262 * contents in the source view.
263 */
264 - (void)setPlainTextStringFromFile:(NSString*)filePath
265 {
266 NSError* error = nil;
267 NSString* contents = [NSString stringWithContentsOfFile:filePath
268 encoding:NSUTF8StringEncoding
269 error:&error];
270 if (error) {
271 NSLog(@"Error reading file at %@: %@", filePath, error);
272 if ([_delegate respondsToSelector:@selector(error:whileHighlightingFile:)]) {
273 [_delegate error:error whileHighlightingFile:filePath];
274 }
275 return;
276 }
277 [_textView setString:contents];
278 }
279
280 // Drag Handlers ///////////////////////////////////////////////////////////////
281
282 /**
283 * Validates an initiated drag operation.
284 */
285 - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender
286 {
287 return NSDragOperationCopy;
288 }
289
290 /**
291 * Performs a dragging operation of files to set the contents of the file.
292 */
293 - (BOOL)performDragOperation:(id<NSDraggingInfo>)sender
294 {
295 NSPasteboard* pboard = [sender draggingPasteboard];
296 if ([[pboard types] containsObject:NSFilenamesPboardType]) {
297 NSArray* files = [pboard propertyListForType:NSFilenamesPboardType];
298 if ([files count]) {
299 NSString* filename = [files objectAtIndex:0];
300 [self setFile:filename];
301 return YES;
302 }
303 }
304 return NO;
305 }
306
307 @end