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