1/*
2    HKSyntaxHighlighter.m
3
4    Implementation of the HKSyntaxHighlighter class for the HighlighterKit
5    framework.
6
7    Copyright (C) 2005, 2006  Saso Kiselkov
8
9    This program is free software; you can redistribute it and/or modify
10    it under the terms of the GNU General Public License as published by
11    the Free Software Foundation; either version 2 of the License, or
12    (at your option) any later version.
13
14    This program is distributed in the hope that it will be useful,
15    but WITHOUT ANY WARRANTY; without even the implied warranty of
16    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17    GNU General Public License for more details.
18
19    You should have received a copy of the GNU General Public License
20    along with this program; if not, write to the Free Software
21    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
22*/
23
24#import "HKSyntaxHighlighter.h"
25
26#import <Foundation/NSArray.h>
27#import <Foundation/NSAutoreleasePool.h>
28#import <Foundation/NSBundle.h>
29#import <Foundation/NSCharacterSet.h>
30#import <Foundation/NSDebug.h>
31#import <Foundation/NSDictionary.h>
32#import <Foundation/NSException.h>
33#import <Foundation/NSLock.h>
34#import <Foundation/NSNotification.h>
35#import <Foundation/NSNull.h>
36#import <Foundation/NSString.h>
37#import <Foundation/NSUserDefaults.h>
38#import <Foundation/NSValue.h>
39
40#import <AppKit/NSAttributedString.h>
41#import <AppKit/NSTextStorage.h>
42
43#import "HKSyntaxDefinition.h"
44
45static NSString * const KeywordsNotFixedAttributeName = @"KNF";
46static NSString * const ContextAttributeName = @"C";
47
48static inline BOOL
49my_isspace(unichar c)
50{
51  if (c == ' ' || c == '\t' || c == '\f')
52    {
53      return YES;
54    }
55  else
56    {
57      return NO;
58    }
59}
60
61/**
62 * This function looks ahead and after `startRange' in `string' and
63 * tries to return the range of a whitespace delimited word at the
64 * specified range. E.g. string = @"abc def ghi" and startRange = {5, 1},
65 * then {4, 3} is returned, because the word "def" lies within the range.
66 * Please note that even when the range points to a whitespace area
67 * (e.g. string = @"abc def" and startRange = {3, 1}), the lookup
68 * will occur and not return `not found' (e.g. in the above example it
69 * would return {0, 7}). When the range is also surrounded by whitespace
70 * (e.g. @"   " and startRange = {1, 1}) the startRange itself is returned.
71 */
72static NSRange
73RangeOfWordInString(NSString * string, NSRange startRange)
74{
75  SEL sel = @selector(characterAtIndex:);
76  unichar (*characterAtIndex)(id, SEL, unsigned int) =
77    (unichar (*)(id, SEL, unsigned int)) [string methodForSelector: sel];
78  int ahead, after;
79  unsigned int length = [string length];
80
81  for (ahead = 1; ahead <= (int) startRange.location; ahead++)
82    {
83      if (my_isspace(characterAtIndex(string,
84                                      sel,
85                                      startRange.location - ahead)))
86        {
87          break;
88        }
89    }
90  ahead--;
91
92  for (after = 0; (after + NSMaxRange(startRange)) < length; after++)
93    {
94      if (my_isspace(characterAtIndex(string,
95                                      sel,
96                                      (after + NSMaxRange(startRange)))))
97        {
98          break;
99        }
100    }
101
102  {
103    unsigned int start = startRange.location - ahead,
104                 length = startRange.length + ahead + after;
105
106    if (start > 0)
107      {
108        start--;
109        length++;
110      }
111    if (length + 1 < length)
112      {
113        length++;
114      }
115
116    return NSMakeRange(start, length);
117  }
118}
119
120static inline BOOL
121LocateString(NSString * str,
122             unichar * buf,
123             unsigned int length,
124             unsigned int offset)
125{
126  unsigned int i, n;
127
128  for (i = 0, n = [str length]; i < n; i++)
129    {
130      if (i >= length)
131        {
132          return NO;
133        }
134
135      if (buf[i + offset] != [str characterAtIndex: i])
136        {
137          return NO;
138        }
139    }
140
141  return YES;
142}
143
144@interface HKSyntaxHighlighter (Private)
145
146- (void) fixUpContextsInRange: (NSRange) r;
147
148- (void) fixUpKeywordsInRange: (NSRange) r;
149- (void) lazilyFixUpKeywordsInRange: (NSRange) r;
150
151- (void) assignGraphicalAttributesOfContext: (unsigned int) context
152                                    toRange: (NSRange) r;
153
154- (void) assignGraphicalAttributesOfKeyword: (unsigned int) keyword
155                                  inContext: (unsigned int) context
156                                    toRange: (NSRange) r;
157
158- (int) contextBeforeRange: (NSRange) r;
159- (int) contextAfterRange: (NSRange) r;
160- (int) contextAtEndOfRange: (NSRange) r;
161
162- (void) beginEditingIfNeeded;
163- (void) endEditingIfNeeded;
164
165@end
166
167@implementation HKSyntaxHighlighter (Private)
168
169/**
170 * Fixes up the contexts inside the text storage in range `r'. A context
171 * is recognized by the "Context" attribute which holds the number of
172 * the context. This method also applies graphical attributes of the
173 * corresponding contexts to the context ranges.
174 */
175- (void) fixUpContextsInRange: (NSRange) r
176{
177  HKTextPattern ** beginnings = [syntax contextBeginnings];
178  const char * beginningChars = [syntax contextBeginningCharacters];
179  unsigned numBeginningChars = [syntax numberOfContextBeginningCharacters];
180
181  unsigned int i;
182  unichar * string;
183  unsigned int context;
184
185  string = (unichar *) malloc(r.length * sizeof(unichar));
186  [[textStorage string] getCharacters: string range: r];
187
188  i = 0;
189  context = [self contextBeforeRange: r];
190  while (i < r.length)
191    {
192       // marks the beginning of the currently processed range
193      unsigned int mark = i;
194
195      // default context - look for beginning symbols
196      if (context == 0)
197        {
198          unsigned int j = 0;
199          HKTextPattern * pattern = NULL;
200          NSRange ctxtRange;
201          int l = 0;
202          HKTextPattern ** skips = [syntax contextSkipsForContext: 0];
203          const char * skipChars = [syntax contextSkipCharactersForContext: 0];
204          unsigned int numSkipChars = [syntax
205            numberOfContextSkipCharactersForContext: 0];
206
207          for (;i < r.length; i++)
208            {
209              unichar c = string[i];
210
211              // Optimize - look into the skip characters array if the
212              // character could be the beginning of a skip sequence.
213              // If not, don't perform skip sequence recognition at all.
214              if (c < numSkipChars && skipChars[c])
215                {
216                  for (j = 0; (pattern = skips[j]) != NULL; j++)
217                    {
218                      l = HKCheckTextPatternPresenceInString (pattern,
219                                                              string,
220                                                              r.length,
221                                                              i);
222                      if (l > 0)
223                        {
224                          break;
225                        }
226                    }
227
228                  if (l > 0)
229                    {
230                      i += l - 1;
231                      continue;
232                    }
233                }
234
235              // optimize - skip unneeded characters
236              if (c < numBeginningChars && !beginningChars[c])
237                {
238                  continue;
239                }
240
241              for (j = 0; (pattern = beginnings[j]) != NULL; j++)
242                {
243                  l = HKCheckTextPatternPresenceInString(pattern, string,
244                                                       r.length, i);
245                  if (l > 0)
246                    {
247                      break;
248                    }
249                }
250
251              if (l > 0)
252                {
253                  break;
254                }
255            }
256
257          // non-default contexts begin with number 1, not zero
258          j++;
259
260          ctxtRange = NSMakeRange(r.location + mark, i - mark);
261          if (ctxtRange.length > 0)
262            {
263              // add an attribute telling the context into the text storage
264              [textStorage addAttribute: ContextAttributeName
265                                  value: [NSNumber numberWithInt: 0]
266                                  range: ctxtRange];
267              [self assignGraphicalAttributesOfContext: 0 toRange: ctxtRange];
268            }
269
270          ctxtRange = NSMakeRange(r.location + i, l);
271          if (ctxtRange.length > 0)
272            {
273              [textStorage addAttribute: ContextAttributeName
274                                  value: [NSNumber numberWithInt: j]
275                                  range: ctxtRange];
276              [self assignGraphicalAttributesOfContext: j
277                                               toRange: ctxtRange];
278            }
279          i += l;
280
281          // switch to the found context again
282          context = j;
283        }
284      // specific context - look for it's terminator, but skip it's
285      // exceptions
286      else
287        {
288          int l = 0;
289          HKTextPattern * ending = [syntax contextEndingForContext: context - 1];
290          NSRange ctxtRange;
291          HKTextPattern ** skips = [syntax contextSkipsForContext: context];
292          const char * skipChars = [syntax contextSkipCharactersForContext:
293            context];
294          unsigned int numSkipChars = [syntax
295            numberOfContextSkipCharactersForContext: context];
296
297          for (;i < r.length; i++)
298            {
299              unichar c = string[i];
300              unsigned int j;
301
302              if (c < numSkipChars && skipChars[c])
303                {
304                  unsigned int j;
305                  HKTextPattern * pattern;
306
307                  for (j = 0; (pattern = skips[j]) != NULL; j++)
308                    {
309                      l = HKCheckTextPatternPresenceInString(pattern, string,
310                                                           r.length, i);
311                      if (l > 0)
312                        {
313                          break;
314                        }
315                    }
316
317                  if (l > 0)
318                    {
319                      i += l - 1;
320                      continue;
321                    }
322                }
323
324              l = HKCheckTextPatternPresenceInString(ending, string,
325                                                   r.length, i);
326              if (l > 0)
327                {
328                  break;
329                }
330            }
331
332          ctxtRange = NSMakeRange(r.location + mark, i - mark);
333          if (ctxtRange.length > 0)
334            {
335              // add an attribute telling the context into the
336              // text storage
337              [textStorage addAttribute: ContextAttributeName
338                                  value: [NSNumber numberWithInt: context]
339                                  range: ctxtRange];
340              [self assignGraphicalAttributesOfContext: context
341                                               toRange: ctxtRange];
342            }
343
344          ctxtRange = NSMakeRange(r.location + i, l);
345          if (ctxtRange.length > 0)
346            {
347              [textStorage addAttribute: ContextAttributeName
348                                  value: [NSNumber numberWithInt: 0]
349                                  range: ctxtRange];
350              [self assignGraphicalAttributesOfContext: context
351                                               toRange: ctxtRange];
352            }
353          i += l;
354
355          // switch to the default context again
356          context = 0;
357        }
358    }
359
360  free(string);
361}
362
363- (void) fixUpKeywordsInRange: (NSRange) r
364{
365  unichar * string;
366  unsigned int i;
367
368  string = malloc(r.length * sizeof(unichar));
369  [[textStorage string] getCharacters: string range: r];
370
371  for (i = 0; i < r.length;)
372    {
373      NSRange contextRange;
374      HKTextPattern ** patterns;
375      int context;
376
377      context = [[textStorage attribute: ContextAttributeName
378                                atIndex: i + r.location
379                         effectiveRange: &contextRange] intValue];
380
381      contextRange = NSIntersectionRange(r, contextRange);
382      contextRange.location -= r.location;
383
384      patterns = [syntax keywordsInContext: context];
385
386      while (i < NSMaxRange(contextRange))
387        {
388          unichar c = string[i];
389          unsigned int l = 0;
390          unsigned int j;
391          HKTextPattern * pattern;
392
393          // skip whitespace - it can't start a keyword
394          if (my_isspace(c) || c == '\r' || c == '\n')
395            {
396              i++;
397              continue;
398            }
399
400          for (j = 0; (pattern = patterns[j]) != NULL; j++)
401            {
402              l = HKCheckTextPatternPresenceInString (pattern,
403                                                      string,
404                                                      r.length,
405                                                      i);
406              if (l > 0)
407                {
408                  break;
409                }
410            }
411
412          // found a pattern?
413          if (pattern != NULL)
414            {
415              NSRange keywordRange = NSMakeRange(i + r.location, l);
416
417              [self assignGraphicalAttributesOfKeyword: j
418                                             inContext: context
419                                               toRange: keywordRange];
420              i += l;
421            }
422          else
423            {
424              i++;
425            }
426        }
427    }
428
429  free(string);
430}
431
432- (void) lazilyFixUpKeywordsInRange: (NSRange) r
433{
434  unsigned int i;
435  BOOL localDidBeginEditing = NO;
436
437  for (i = r.location; i < NSMaxRange(r);)
438    {
439      NSRange effectiveRange;
440
441      // locate non-fixed areas and fix them up
442      if ([textStorage attribute: KeywordsNotFixedAttributeName
443                         atIndex: i
444           longestEffectiveRange: &effectiveRange
445                         inRange: r] != nil)
446        {
447          if (localDidBeginEditing == NO)
448            {
449              localDidBeginEditing = YES;
450              [textStorage beginEditing];
451            }
452          effectiveRange = NSIntersectionRange(effectiveRange, r);
453          [self fixUpKeywordsInRange: effectiveRange];
454          [textStorage removeAttribute: KeywordsNotFixedAttributeName
455                                 range: effectiveRange];
456          i += effectiveRange.length;
457        }
458
459      // skip over fixed areas
460      else
461        {
462          i += effectiveRange.length;
463        }
464    }
465
466  if (localDidBeginEditing == YES)
467    {
468      [textStorage endEditing];
469    }
470}
471
472- (void) assignGraphicalAttributesOfContext: (unsigned int) ctxt
473                                    toRange: (NSRange) r
474{
475  BOOL bold, italic;
476  NSColor * color;
477
478  color = [syntax foregroundColorForContext: ctxt];
479  if (color != nil)
480    {
481      [textStorage addAttribute: NSForegroundColorAttributeName
482                          value: color
483                          range: r];
484    }
485  else if (defaultTextColor != nil)
486    {
487      [textStorage addAttribute: NSForegroundColorAttributeName
488                          value: defaultTextColor
489                          range: r];
490    }
491  else
492    {
493      [textStorage removeAttribute: NSForegroundColorAttributeName range: r];
494    }
495
496  color = [syntax backgroundColorForContext: ctxt];
497  if (color != nil)
498    {
499      [textStorage addAttribute: NSBackgroundColorAttributeName
500                          value: color
501                          range: r];
502    }
503  else
504    {
505      [textStorage removeAttribute: NSBackgroundColorAttributeName range: r];
506    }
507
508  bold = [syntax isBoldFontForContext: ctxt];
509  italic = [syntax isItalicFontForContext: ctxt];
510  if (bold && italic)
511    {
512      [textStorage addAttribute: NSFontAttributeName
513                          value: boldItalicFont
514                          range: r];
515    }
516  else if (bold)
517    {
518      [textStorage addAttribute: NSFontAttributeName
519                          value: boldFont
520                          range: r];
521    }
522  else if (italic)
523    {
524      [textStorage addAttribute: NSFontAttributeName
525                          value: italicFont
526                          range: r];
527    }
528  else
529    {
530      [textStorage addAttribute: NSFontAttributeName
531                          value: normalFont
532                          range: r];
533    }
534}
535
536- (void) assignGraphicalAttributesOfKeyword: (unsigned int) keyword
537                                  inContext: (unsigned int) context
538                                    toRange: (NSRange) r
539{
540  BOOL bold, italic;
541  NSColor * color;
542
543  color = [syntax foregroundColorForKeyword: keyword inContext: context];
544  if (color != nil)
545    {
546      [textStorage addAttribute: NSForegroundColorAttributeName
547                          value: color
548                          range: r];
549    }
550  else
551    {
552      color = [syntax foregroundColorForContext: context];
553
554      if (color != nil)
555        {
556          [textStorage addAttribute: NSForegroundColorAttributeName
557                              value: color
558                              range: r];
559        }
560      else if (defaultTextColor != nil)
561        {
562          [textStorage addAttribute: NSForegroundColorAttributeName
563                              value: defaultTextColor
564                              range: r];
565        }
566      else
567        {
568          [textStorage removeAttribute: NSForegroundColorAttributeName
569                                 range: r];
570        }
571    }
572
573  color = [syntax backgroundColorForKeyword: keyword inContext: context];
574  if (color != nil)
575    {
576      [textStorage addAttribute: NSBackgroundColorAttributeName
577                          value: color
578                          range: r];
579    }
580  else
581    {
582      color = [syntax backgroundColorForContext: context];
583
584      if (color != nil)
585        {
586          [textStorage addAttribute: NSBackgroundColorAttributeName
587                              value: color
588                              range: r];
589        }
590      else
591        {
592          [textStorage removeAttribute: NSBackgroundColorAttributeName
593                                 range: r];
594        }
595    }
596
597  bold = [syntax isBoldFontForKeyword: keyword inContext: context];
598  italic = [syntax isItalicFontForKeyword: keyword inContext: context];
599  if (bold && italic)
600    {
601      [textStorage addAttribute: NSFontAttributeName
602                          value: boldItalicFont
603                          range: r];
604    }
605  else if (bold)
606    {
607      [textStorage addAttribute: NSFontAttributeName
608                          value: boldFont
609                          range: r];
610    }
611  else if (italic)
612    {
613      [textStorage addAttribute: NSFontAttributeName
614                          value: italicFont
615                          range: r];
616    }
617  else
618    {
619      [textStorage addAttribute: NSFontAttributeName
620                          value: normalFont
621                          range: r];
622    }
623}
624
625- (int) contextBeforeRange: (NSRange) r
626{
627  NSRange tmp;
628
629  if (r.location == 0)
630    {
631      return 0;
632    }
633  else
634    {
635      return [[textStorage attribute: ContextAttributeName
636                             atIndex: r.location - 1
637                      effectiveRange: &tmp] intValue];
638    }
639}
640
641- (int) contextAfterRange: (NSRange) r
642{
643  NSRange tmp;
644  unsigned int i, length;
645
646  i = NSMaxRange(r);
647  length = [textStorage length];
648
649  if (length == 0)
650    {
651      return 0;
652    }
653  else if (i < length)
654    {
655      return [[textStorage attribute: ContextAttributeName
656                             atIndex: i
657                      effectiveRange: &tmp] intValue];
658    }
659  else
660    {
661      return 0;
662    }
663}
664
665- (int) contextAtEndOfRange: (NSRange) r
666{
667  NSRange tmp;
668  int i = (int) NSMaxRange(r) - 1;
669
670  if (i < 0)
671    {
672      return 0;
673    }
674  else
675    {
676      return [[textStorage attribute: ContextAttributeName
677                             atIndex: i
678                      effectiveRange: &tmp] intValue];
679    }
680}
681
682- (void) beginEditingIfNeeded
683{
684  if (didBeginEditing == NO)
685    {
686      didBeginEditing = YES;
687      [textStorage beginEditing];
688    }
689}
690
691- (void) endEditingIfNeeded
692{
693  if (didBeginEditing == YES)
694    {
695      didBeginEditing = NO;
696      [textStorage endEditing];
697    }
698}
699
700@end
701
702@implementation HKSyntaxHighlighter
703
704+ (NSFont *) defaultFont
705{
706  NSUserDefaults * df = [NSUserDefaults standardUserDefaults];
707  NSString * fontName;
708  float fontSize;
709  NSFont * font = nil;
710
711  fontName = [df objectForKey: @"HKFont"];
712  fontSize = [df floatForKey: @"HKFontSize"];
713
714  if (fontName != nil)
715    {
716      font = [NSFont fontWithName: fontName size: fontSize];
717    }
718  if (font == nil)
719    {
720      font = [NSFont userFixedPitchFontOfSize: fontSize];
721    }
722
723  return font;
724}
725
726+ (NSFont *) defaultBoldFont
727{
728  NSFont * font = [self defaultFont];
729
730  return [[NSFontManager sharedFontManager] convertFont: font
731                                            toHaveTrait: NSBoldFontMask];
732}
733
734+ (NSFont *) defaultItalicFont
735{
736  NSFont * font = [self defaultFont];
737
738  return [[NSFontManager sharedFontManager] convertFont: font
739                                            toHaveTrait: NSItalicFontMask];
740}
741
742+ (NSFont *) defaultBoldItalicFont
743{
744  NSFont * font = [self defaultFont];
745
746  return [[NSFontManager sharedFontManager] convertFont: font
747                                            toHaveTrait: NSBoldFontMask |
748                                                         NSItalicFontMask];
749}
750
751
752- initWithHighlighterType: (NSString *) type
753              textStorage: (NSTextStorage *) aStorage
754         defaultTextColor: (NSColor *) aColor
755{
756  HKSyntaxDefinition * def = [HKSyntaxDefinition
757    syntaxDefinitionForType: type];
758
759  return [self initWithSyntaxDefinition: def
760                            textStorage: aStorage
761                       defaultTextColor: aColor];
762}
763
764- initWithSyntaxDefinition: (HKSyntaxDefinition *) aSyntaxDefinition
765               textStorage: (NSTextStorage *) aStorage
766          defaultTextColor: (NSColor *) aColor
767{
768  if ((self = [self init]) != nil)
769    {
770      NSRange r;
771
772      ASSIGN (textStorage, aStorage);
773      ASSIGN (syntax, aSyntaxDefinition);
774
775      // no syntax definition - no highlighting possible
776      if (syntax == nil)
777        {
778          [self release];
779          return nil;
780        }
781
782      // mark all of the text storage as requiring keyword fixing
783      r = NSMakeRange(0, [textStorage length]);
784      [textStorage addAttribute: KeywordsNotFixedAttributeName
785                          value: [NSNull null]
786                          range: r];
787
788      [[NSNotificationCenter defaultCenter]
789        addObserver: self
790           selector: @selector(textStorageWillProcessEditing:)
791               name: NSTextStorageWillProcessEditingNotification
792             object: textStorage];
793
794      ASSIGN (normalFont, [[self class] defaultFont]);
795      ASSIGN (boldFont, [[self class] defaultBoldFont]);
796      ASSIGN (italicFont, [[self class] defaultItalicFont]);
797      ASSIGN (boldItalicFont, [[self class] defaultBoldItalicFont]);
798
799      ASSIGN (defaultTextColor, aColor);
800
801      return self;
802    }
803  else
804    {
805      return nil;
806    }
807}
808
809- (void) dealloc
810{
811  NSDebugLLog(@"HKSyntaxHighlighter", @"HKSyntaxHighlighter: dealloc");
812
813  [[NSNotificationCenter defaultCenter] removeObserver: self];
814
815  TEST_RELEASE (textStorage);
816  TEST_RELEASE (syntax);
817  TEST_RELEASE (normalFont);
818  TEST_RELEASE (boldFont);
819  TEST_RELEASE (italicFont);
820  TEST_RELEASE (boldItalicFont);
821
822  TEST_RELEASE (defaultTextColor);
823
824  [super dealloc];
825}
826
827- (void) highlightRange: (NSRange) r
828{
829  if (delayedProcessedRange.length > 0)
830    {
831      [self beginEditingIfNeeded];
832      [self fixUpContextsInRange: delayedProcessedRange];
833      [self fixUpKeywordsInRange: delayedProcessedRange];
834
835      if ([self contextAtEndOfRange: delayedProcessedRange] !=
836          [self contextAfterRange: delayedProcessedRange])
837        {
838          NSRange invalidatedRange;
839
840          lastProcessedContextIndex = NSMaxRange(delayedProcessedRange);
841
842          invalidatedRange = NSMakeRange(NSMaxRange(delayedProcessedRange),
843            [textStorage length] - NSMaxRange(delayedProcessedRange));
844          [textStorage addAttribute: KeywordsNotFixedAttributeName
845                              value: [NSNull null]
846                              range: invalidatedRange];
847        }
848    }
849  else
850    {
851      if (delayedProcessedRange.location > 0 &&
852        [self contextBeforeRange: delayedProcessedRange] !=
853        [self contextAfterRange: delayedProcessedRange])
854        {
855          NSRange invalidatedRange;
856
857          lastProcessedContextIndex = NSMaxRange(delayedProcessedRange);
858
859          [self beginEditingIfNeeded];
860
861          invalidatedRange = NSMakeRange(NSMaxRange(delayedProcessedRange),
862            [textStorage length] - NSMaxRange(delayedProcessedRange));
863          [textStorage addAttribute: KeywordsNotFixedAttributeName
864                              value: [NSNull null]
865                              range: invalidatedRange];
866        }
867    }
868
869  delayedProcessedRange = NSMakeRange(0, 0);
870
871  r = RangeOfWordInString([textStorage string], r);
872
873  // need to fixup contexts?
874  if (NSMaxRange(r) > lastProcessedContextIndex)
875    {
876      unsigned int prevContext;
877      NSRange fixupRange;
878
879      fixupRange = NSMakeRange(lastProcessedContextIndex,
880                               NSMaxRange(r) - lastProcessedContextIndex);
881
882      [self beginEditingIfNeeded];
883      [self fixUpContextsInRange: fixupRange];
884
885      lastProcessedContextIndex = NSMaxRange(r);
886    }
887
888  [self lazilyFixUpKeywordsInRange: r];
889
890  [self endEditingIfNeeded];
891}
892
893- (void) textStorageWillProcessEditing: (NSNotification *) notif
894{
895  if ([textStorage editedMask] & NSTextStorageEditedCharacters)
896    {
897      NSRange editedRange = [textStorage editedRange];
898
899      delayedProcessedRange = RangeOfWordInString([textStorage string],
900                                                  editedRange);
901
902      if (lastProcessedContextIndex > editedRange.location)
903        {
904          lastProcessedContextIndex += [textStorage changeInLength];
905        }
906    }
907}
908
909@end
910