1/*
2    PCEditorView.m
3
4    Implementation of the PCEditorView class for the
5    ProjectManager application.
6
7    Copyright (C) 2005-2014 Free Software Foundation
8      Saso Kiselkov
9      Serg Stoyan
10      Riccardo Mottola
11
12    This program is free software; you can redistribute it and/or modify
13    it under the terms of the GNU General Public License as published by
14    the Free Software Foundation; either version 2 of the License, or
15    (at your option) any later version.
16
17    This program is distributed in the hope that it will be useful,
18    but WITHOUT ANY WARRANTY; without even the implied warranty of
19    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20    GNU General Public License for more details.
21
22    You should have received a copy of the GNU General Public License
23    along with this program; if not, write to the Free Software
24    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
25*/
26
27#import "PCEditorView.h"
28
29#import <Foundation/NSString.h>
30#import <Foundation/NSUserDefaults.h>
31#import <Foundation/NSArchiver.h>
32
33#import <AppKit/PSOperators.h>
34#import <AppKit/NSColor.h>
35#import <AppKit/NSEvent.h>
36#import <AppKit/NSWindow.h>
37#import <AppKit/NSCursor.h>
38#import <AppKit/NSLayoutManager.h>
39#import <AppKit/NSFont.h>
40
41#import <ctype.h>
42
43#import "PCEditor.h"
44#import "SyntaxHighlighter.h"
45#import "LineJumper.h"
46
47static inline float my_abs(float aValue)
48{
49  if (aValue >= 0)
50    {
51      return aValue;
52    }
53  else
54    {
55      return -aValue;
56    }
57}
58
59/**
60 * Computes the indenting offset of the last line before the passed
61 * start offset containg text in the passed string, e.g.
62 *
63 * ComputeIndentingOffset(@"  Hello World", 12) = 2
64 * ComputeIndentingOffset(@"    Try this one out\n"
65 *                        @"      ", 27) = 4
66 *
67 * @argument string The string in which to do the computation.
68 * @argument start The start offset from which to start looking backwards.
69 * @return The ammount of spaces the last line containing text is offset
70 *      from it's start.
71 */
72static int ComputeIndentingOffset(NSString * string, unsigned int start)
73{
74  SEL sel = @selector(characterAtIndex:);
75  unichar (* charAtIndex)(NSString *, SEL, unsigned int) =
76    (unichar (*)(NSString *, SEL, unsigned int))
77    [string methodForSelector: sel];
78  unichar c;
79  int firstCharOffset = -1;
80  int offset;
81  int startOffsetFromLineStart = -1;
82
83  for (offset = start - 1; offset >= 0; offset--)
84    {
85      c = charAtIndex(string, sel, offset);
86
87      if (c == '\n')
88        {
89          if (startOffsetFromLineStart < 0)
90            {
91              startOffsetFromLineStart = start - offset - 1;
92            }
93
94          if (firstCharOffset >= 0)
95            {
96              firstCharOffset = firstCharOffset - offset - 1;
97              break;
98            }
99        }
100      else if (!isspace(c))
101        {
102          firstCharOffset = offset;
103        }
104    }
105
106  return firstCharOffset >= 0 ? firstCharOffset : 0;
107
108/*  if (firstCharOffset >= 0)
109    {
110      // if the indenting of the current line is lower than the indenting
111      // of the previous actual line, we return the lower indenting
112      if (startOffsetFromLineStart >= 0 &&
113        startOffsetFromLineStart < firstCharOffset)
114        {
115          return startOffsetFromLineStart;
116        }
117      // otherwise we return the actual indenting, so that any excess
118      // space is trimmed and the lines are aligned according the last
119      // indenting level
120      else
121        {
122          return firstCharOffset;
123        }
124    }
125   else
126    {
127      return 0;
128    }*/
129}
130
131@interface PCEditorView (Private)
132
133- (void)insertSpaceFillAlignedAtTabsOfSize:(unsigned int)tabSize;
134- (void)performIndentation;
135
136@end
137
138@implementation PCEditorView (Private)
139
140/**
141 * Makes the receiver insert as many spaces at the current insertion
142 * location as are required to reach the nearest tab-character boundary.
143 *
144 * @argument tabSize Specifies how many spaces represent one tab.
145 */
146- (void)insertSpaceFillAlignedAtTabsOfSize:(unsigned int)tabSize
147{
148  char buf[tabSize];
149  NSString * string = [self string];
150  unsigned int lineLength;
151  SEL sel = @selector(characterAtIndex:);
152  unichar (* charAtIndex)(NSString*, SEL, unsigned int) =
153    (unichar (*)(NSString*, SEL, unsigned int))
154    [string methodForSelector: sel];
155  int i;
156  int skip;
157
158  // computes the length of the current line
159  for (i = [self selectedRange].location - 1, lineLength = 0;
160       i >= 0;
161       i--, lineLength++)
162    {
163      if (charAtIndex(string, sel, i) == '\n')
164        {
165          break;
166        }
167    }
168
169  skip = tabSize - (lineLength % tabSize);
170  if (skip == 0)
171    {
172      skip = tabSize;
173    }
174
175  memset(buf, ' ', skip);
176  [super insertText: [NSString stringWithCString: buf length: skip]];
177}
178
179// Go backward to first '\n' char or start of file
180- (int)lineStartIndexForIndex:(int)index forString:(NSString *)string
181{
182  int line_start;
183
184  // Get line start index moving from index backwards
185  for (line_start = index;line_start > 0;line_start--)
186    {
187      if ([string characterAtIndex:line_start] == '\n' &&
188	  line_start != index)
189	{
190	  line_start++;
191	  break;
192	}
193    }
194
195  NSLog(@"index: %i start: %i", index, line_start);
196
197  return line_start > index ? index : line_start;
198}
199
200- (int)lineEndIndexForIndex:(int)index forString:(NSString *)string
201{
202  int line_end;
203  int string_length = [string length];
204
205  // Get line start index moving from index backwards
206  for (line_end = index;line_end < string_length;line_end++)
207    {
208      if ([string characterAtIndex:line_end] == '\n')
209	{
210	  break;
211	}
212    }
213
214  NSLog(@"index: %i end: %i", index, line_end);
215
216  return line_end < string_length ? line_end : string_length;
217}
218
219- (int)previousLineStartIndexForIndex:(int)index forString:(NSString *)string
220{
221  int cur_line_start;
222  int prev_line_start;
223
224  cur_line_start = [self lineStartIndexForIndex:index forString:string];
225  prev_line_start = [self lineStartIndexForIndex:cur_line_start-1
226 				       forString:string];
227
228  NSLog(@"index: %i prev_start: %i", index, prev_line_start);
229
230  return prev_line_start;
231}
232
233- (int)nextLineStartIndexForIndex:(int)index forString:(NSString *)string
234{
235  int cur_line_end;
236  int next_line_start;
237  int string_length = [string length];
238
239  cur_line_end = [self lineEndIndexForIndex:index forString:string];
240  next_line_start = cur_line_end + 1;
241
242  if (next_line_start < string_length)
243    {
244      return next_line_start;
245    }
246  else
247    {
248      return string_length;
249    }
250}
251
252- (unichar)firstCharOfLineForIndex:(int)index forString:(NSString *)string
253{
254  int line_start = [self lineStartIndexForIndex:index forString:string];
255  int i;
256  unichar c;
257
258  // Get leading whitespaces range
259  for (i = line_start; i >= 0; i++)
260    {
261      c = [string characterAtIndex:i];
262      if (!isspace(c))
263	{
264	  break;
265	}
266    }
267
268  fprintf(stderr, "First char: %c\n", c);
269
270  return c;
271}
272
273- (unichar)firstCharOfPrevLineForIndex:(int)index forString:(NSString *)string
274{
275  int line_start = [self previousLineStartIndexForIndex:index
276						   forString:string];
277
278  return [self firstCharOfLineForIndex:line_start forString:string];
279}
280
281- (unichar)firstCharOfNextLineForIndex:(int)index
282{
283  return 0;
284}
285
286- (void)performIndentation
287{
288  NSString *string = [self string];
289  int      location;
290  int      line_start;
291  int      offset;
292  unichar  c, plfc, clfc;
293  NSRange  wsRange;
294  NSMutableString *indentString;
295  NSCharacterSet  *wsCharSet = [NSCharacterSet whitespaceCharacterSet];
296  int i;
297//  int point;
298
299  location = [self selectedRange].location;
300
301//  point = [self nextLineStartIndexForIndex:location forString:string];
302//  [self setSelectedRange:NSMakeRange(point, 0)];
303
304  clfc = [self firstCharOfLineForIndex:location forString:string];
305  plfc = [self firstCharOfPrevLineForIndex:location forString:string];
306
307  // Get leading whitespaces range
308  line_start = [self lineStartIndexForIndex:location forString:string];
309  for (offset = line_start; offset >= 0; offset++)
310    {
311      c = [string characterAtIndex:offset];
312      if (![wsCharSet characterIsMember:c])
313	{
314	  wsRange = NSMakeRange(line_start, offset-line_start);
315	  break;
316	}
317    }
318
319  // Get indent
320  line_start = [self previousLineStartIndexForIndex:location forString:string];
321  for (offset = line_start; offset >= 0; offset++)
322    {
323      c = [string characterAtIndex:offset];
324      if (![wsCharSet characterIsMember:c])
325	{
326	  offset = offset - line_start;
327	  NSLog(@"offset: %i", offset);
328	  break;
329	}
330    }
331
332  NSLog (@"clfc: %c plfc: %c", clfc, plfc);
333  if (plfc == '{' || clfc == '{')
334    {
335      offset += 2;
336    }
337  else if (clfc == '}' && plfc != '{')
338    {
339      offset -= 2;
340    }
341
342  // Get offset from BOL of previous line
343//  offset = ComputeIndentingOffset([self string], line_start-1);
344  NSLog(@"Indent offset: %i", offset);
345
346  // Replace current line whitespaces with new ones
347  indentString = [[NSMutableString alloc] initWithString:@""];
348  for (i = offset; i > 0; i--)
349    {
350      [indentString appendString:@" "];
351    }
352
353  if ([self shouldChangeTextInRange:wsRange
354		  replacementString:indentString])
355    [[self textStorage] replaceCharactersInRange:wsRange
356				      withString:indentString];
357
358/*  if (location > line_start + offset)
359    {
360      point = location - offset;
361    }
362  else
363    {
364      point = location;
365    }
366  [self setSelectedRange:NSMakeRange(point, 0)];*/
367
368  [indentString release];
369}
370
371@end
372
373@implementation PCEditorView
374
375+ (NSFont *)defaultEditorFont
376{
377  NSUserDefaults *df = [NSUserDefaults standardUserDefaults];
378  NSString       *fontName;
379  float          fontSize;
380  NSFont         *font = nil;
381
382  fontName = [df objectForKey:@"EditorFont"];
383  fontSize = [df floatForKey:@"EditorFontSize"];
384
385  if (fontName != nil)
386    {
387      font = [NSFont fontWithName:fontName size:fontSize];
388    }
389  if (font == nil)
390    {
391      font = [NSFont userFixedPitchFontOfSize:fontSize];
392    }
393
394  return font;
395}
396
397+ (NSFont *)defaultEditorBoldFont
398{
399  NSFont *font = [self defaultEditorFont];
400
401  return [[NSFontManager sharedFontManager] convertFont:font
402                                            toHaveTrait:NSBoldFontMask];
403}
404
405+ (NSFont *)defaultEditorItalicFont
406{
407  NSFont *font = [self defaultEditorFont];
408
409  return [[NSFontManager sharedFontManager] convertFont:font
410                                            toHaveTrait:NSItalicFontMask];
411}
412
413+ (NSFont *)defaultEditorBoldItalicFont
414{
415  NSFont *font = [self defaultEditorFont];
416
417  return [[NSFontManager sharedFontManager] convertFont:font
418                                            toHaveTrait:NSBoldFontMask |
419                                                        NSItalicFontMask];
420}
421
422// ---
423- (BOOL)becomeFirstResponder
424{
425  return [editor becomeFirstResponder:self];
426}
427
428- (BOOL)resignFirstResponder
429{
430  return [editor resignFirstResponder:self];
431}
432
433- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
434{
435  return YES;
436}
437// ---
438
439- (void)dealloc
440{
441  TEST_RELEASE(highlighter);
442
443  [super dealloc];
444}
445
446- (void)setEditor:(PCEditor *)anEditor
447{
448  editor = anEditor;
449}
450
451- (PCEditor *)editor
452{
453  return editor;
454}
455
456- (void)awakeFromNib
457{
458/*  NSData * data;
459  NSUserDefaults * df = [NSUserDefaults standardUserDefaults];
460
461  drawCrosshairs = [df boolForKey: @"DrawCrosshairs"];
462  if (drawCrosshairs)
463    {
464      if ((data = [df dataForKey: @"CrosshairColor"]) == nil ||
465        (crosshairColor = [NSUnarchiver unarchiveObjectWithData: data]) == nil)
466        {
467          crosshairColor = [NSColor lightGrayColor];
468        }
469      [crosshairColor retain];
470    }
471
472  guides = [NSMutableArray new];*/
473}
474
475- (void)drawRect:(NSRect)r
476{
477  NSRange drawnRange;
478
479  if (highlighter)
480    {
481      drawnRange = [[self layoutManager]
482	glyphRangeForBoundingRect:r inTextContainer:[self textContainer]];
483      [highlighter highlightRange:drawnRange];
484    }
485
486  [super drawRect:r];
487}
488
489- (void)createSyntaxHighlighterForFileType:(NSString *)fileType
490{
491  ASSIGN(highlighter,
492	 [[[SyntaxHighlighter alloc] initWithFileType:fileType
493					  textStorage:[self textStorage]]
494					  autorelease]);
495}
496
497- (void)insertText:text
498{
499  /* NOTE: On Windows we ensure to get a string in UTF-8 encoding. The problem
500   * is the highlighter that don't use a consistent codification causing a
501   * problem on Windows platform. Anyway, the plugin for Gemas editor works
502   * better and don't show this problem.
503   */
504  if ([text isKindOfClass:[NSString class]])
505    {
506      NSString * string = text;
507
508      if ([text characterAtIndex:0] == 27)
509	{
510	  NSLog(@"ESC key pressed. Ignoring it");
511	  return;
512	}
513
514      if ([string isEqualToString:@"\n"])
515        {
516/*          if ([[NSUserDefaults standardUserDefaults]
517            boolForKey:@"ReturnDoesAutoindent"])
518            {*/
519	      int  location = [self selectedRange].location;
520              int  offset = ComputeIndentingOffset([self string], location);
521              char *buf;
522
523              buf = (char *) malloc((offset + 2) * sizeof(unichar));
524	      buf[0] = '\n';
525              memset(&buf[1], ' ', offset);
526              buf[offset+1] = '\0';
527
528#ifdef WIN32
529              [super insertText:[NSString stringWithCString: buf
530					  encoding: NSUTF8StringEncoding]];
531#else
532	      [super insertText:[NSString stringWithCString:buf]];
533#endif
534              free(buf);
535/*            }
536          else
537            {
538              [super insertText:text];
539            }*/
540        }
541      else if ([string isEqualToString:@"\t"])
542        {
543	  [self performIndentation];
544/*          switch ([[NSUserDefaults standardUserDefaults]
545            integerForKey:@"TabConversion"])
546            {
547            case 0:  // no conversion
548              [super insertText:text];
549              break;
550            case 1:  // 2 spaces
551              [super insertText:@"  "];
552              break;
553            case 2:  // 4 spaces
554              [super insertText:@"    "];
555              break;
556            case 3:  // 8 spaces
557              [super insertText:@"        "];
558              break;
559            case 4:  // aligned to tab boundaries of 2 spaces long tabs
560              [self insertSpaceFillAlignedAtTabsOfSize:2];
561              break;
562            case 5:  // aligned to tab boundaries of 4 spaces long tabs
563              [self insertSpaceFillAlignedAtTabsOfSize:4];
564              break;
565            case 6:  // aligned to tab boundaries of 8 spaces long tabs
566              [self insertSpaceFillAlignedAtTabsOfSize:8];
567              break;
568            }*/
569        }
570      else
571        {
572#ifdef WIN32
573	  [super insertText: [NSString stringWithCString: [text UTF8String]]];
574#else
575          [super insertText: text];
576#endif
577        }
578    }
579  else
580    {
581#ifdef WIN32
582      [super insertText: [NSString stringWithCString: [text UTF8String]]];
583#else
584      [super insertText: text];
585#endif
586    }
587}
588
589/* This extra change tracking is required in order to inform the document
590 * that the text is changing _before_ it actually changes. This is required
591 * so that the document can un-highlight any highlit characters before the
592 * change occurs and after the change recompute any new highlighting.
593 */
594- (void)keyDown:(NSEvent *)ev
595{
596  [editor editorTextViewWillPressKey:self];
597  [super keyDown:ev];
598  [editor editorTextViewDidPressKey:self];
599}
600
601- (void)paste:sender
602{
603  [editor editorTextViewWillPressKey:self];
604  [super paste:sender];
605  [editor editorTextViewDidPressKey:self];
606}
607
608- (void)mouseDown:(NSEvent *)ev
609{
610  [editor editorTextViewWillPressKey:self];
611  [super mouseDown:ev];
612  [editor editorTextViewDidPressKey:self];
613}
614
615- (NSRect)selectionRect
616{
617  return _insertionPointRect;
618}
619
620- (BOOL)usesFindPanel
621{
622  return YES;
623}
624
625- (void)performGoToLinePanelAction:(id)sender
626{
627  LineJumper *lj;
628
629  lj = [LineJumper sharedInstance];
630  [lj orderFrontLinePanel:self];
631}
632
633- (void)goToLineNumber:(NSUInteger)lineNumber
634{
635  NSUInteger   offset;
636  NSUInteger   i;
637  NSString     *line;
638  NSEnumerator *e;
639  NSArray      *lines;
640  NSRange      range;
641
642  lines = [[self string] componentsSeparatedByString: @"\n"];
643  e = [lines objectEnumerator];
644
645  for (offset = 0, i = 1;
646       (line = [e nextObject]) != nil && i < lineNumber;
647       i++, offset += [line length] + 1);
648
649  if (line != nil)
650    {
651      range = NSMakeRange(offset, [line length]);
652    }
653  else
654    {
655      range = NSMakeRange([[self string] length], 0);
656    }
657  [self setSelectedRange:range];
658  [self scrollRangeToVisible:range];
659}
660
661@end
662