Stop using deprecated Cocoa APIs
[MacVim.git] / src / MacVim / MMTextStorage.m
blob82ad1400e1b60fd094579abff296697f5f352620
1 /* vi:set ts=8 sts=4 sw=4 ft=objc:
2  *
3  * VIM - Vi IMproved            by Bram Moolenaar
4  *                              MacVim GUI port by Bjorn Winckler
5  *
6  * Do ":help uganda"  in Vim to read copying and usage conditions.
7  * Do ":help credits" in Vim to see a list of people who contributed.
8  * See README.txt for an overview of the Vim source code.
9  */
11  * MMTextStorage
12  *
13  * Text rendering related code.
14  *
15  * Note that:
16  * - There are exactly 'actualRows' number of rows
17  * - Each row is terminated by an EOL character ('\n')
18  * - Each row must cover exactly 'actualColumns' display cells
19  * - The attribute "MMWideChar" denotes a character that covers two cells, a
20  *   character without this attribute covers one cell
21  * - Unicode line (U+2028) and paragraph (U+2029) terminators are considered
22  *   invalid and are replaced by spaces
23  * - Spaces are used to fill out blank spaces
24  *
25  * In order to locate a (row,col) pair it is in general necessary to search one
26  * character at a time.  To speed things up we cache the length of each row, as
27  * well as the offset of the last column searched within each row.
28  *
29  * If each character in the text storage has length 1 and is not wide, then
30  * there is no need to search for a (row, col) pair since it can easily be
31  * computed.
32  */
34 #import "MMTextStorage.h"
35 #import "MacVim.h"
36 #import "Miscellaneous.h"
40 // Enable debug log messages for situations that should never occur.
41 #define MM_TS_PARANOIA_LOG 1
45 // TODO: What does DRAW_TRANSP flag do?  If the background isn't drawn when
46 // this flag is set, then sometimes the character after the cursor becomes
47 // blank.  Everything seems to work fine by just ignoring this flag.
48 #define DRAW_TRANSP               0x01    /* draw with transparant bg */
49 #define DRAW_BOLD                 0x02    /* draw bold text */
50 #define DRAW_UNDERL               0x04    /* draw underline text */
51 #define DRAW_UNDERC               0x08    /* draw undercurl text */
52 #define DRAW_ITALIC               0x10    /* draw italic text */
53 #define DRAW_CURSOR               0x20
56 static NSString *MMWideCharacterAttributeName = @"MMWideChar";
61 @interface MMTextStorage (Private)
62 - (void)lazyResize:(BOOL)force;
63 - (NSRange)charRangeForRow:(int)row column:(int*)col cells:(int*)cells;
64 - (void)fixInvalidCharactersInRange:(NSRange)range;
65 @end
69 @implementation MMTextStorage
71 - (id)init
73     if ((self = [super init])) {
74         attribString = [[NSMutableAttributedString alloc] initWithString:@""];
75         // NOTE!  It does not matter which font is set here, Vim will set its
76         // own font on startup anyway.  Just set some bogus values.
77         font = [[NSFont userFixedPitchFontOfSize:0] retain];
78         cellSize.height = 16.0;
79         cellSize.width = 6.0;
80     }
82     return self;
85 - (void)dealloc
87     LOG_DEALLOC
89 #if MM_USE_ROW_CACHE
90     if (rowCache) {
91         free(rowCache);
92         rowCache = NULL;
93     }
94 #endif
95     [emptyRowString release];  emptyRowString = nil;
96     [boldItalicFontWide release];  boldItalicFontWide = nil;
97     [italicFontWide release];  italicFontWide = nil;
98     [boldFontWide release];  boldFontWide = nil;
99     [fontWide release];  fontWide = nil;
100     [boldItalicFont release];  boldItalicFont = nil;
101     [italicFont release];  italicFont = nil;
102     [boldFont release];  boldFont = nil;
103     [font release];  font = nil;
104     [defaultBackgroundColor release];  defaultBackgroundColor = nil;
105     [defaultForegroundColor release];  defaultForegroundColor = nil;
106     [attribString release];  attribString = nil;
107     [super dealloc];
110 - (NSString *)string
112     return [attribString string];
115 - (NSDictionary *)attributesAtIndex:(unsigned)index
116                      effectiveRange:(NSRangePointer)range
118     if (index >= [attribString length]) {
119         if (range)
120             *range = NSMakeRange(NSNotFound, 0);
122         return [NSDictionary dictionary];
123     }
125     return [attribString attributesAtIndex:index effectiveRange:range];
128 - (id)attribute:(NSString *)attrib atIndex:(unsigned)index
129         effectiveRange:(NSRangePointer)range
131     return [attribString attribute:attrib atIndex:index effectiveRange:range];
134 - (void)replaceCharactersInRange:(NSRange)range
135                       withString:(NSString *)string
137 #if MM_TS_PARANOIA_LOG
138     NSLog(@"WARNING: calling %s on MMTextStorage is unsupported", _cmd);
139 #endif
140     //[attribString replaceCharactersInRange:range withString:string];
143 - (void)setAttributes:(NSDictionary *)attributes range:(NSRange)range
145     //NSLog(@"%s%@", _cmd, NSStringFromRange(range));
147     // NOTE!  This method must be implemented since the text system calls it
148     // constantly to 'fix attributes', apply font substitution, etc.
149 #if 0
150     [attribString setAttributes:attributes range:range];
151 #elif 1
152     // HACK! If the font attribute is being modified, then ensure that the new
153     // font has a fixed advancement which is either the same as the current
154     // font or twice that, depending on whether it is a 'wide' character that
155     // is being fixed or not.
156     //
157     // TODO: This code assumes that the characters in 'range' all have the same
158     // width.
159     NSFont *newFont = [attributes objectForKey:NSFontAttributeName];
160     if (newFont) {
161         // Allow disabling of font substitution via a user default.  Not
162         // recommended since the typesetter hides the corresponding glyphs and
163         // the display gets messed up.
164         if ([[NSUserDefaults standardUserDefaults]
165                 boolForKey:MMNoFontSubstitutionKey])
166             return;
168         float adv = cellSize.width;
169         if ([attribString attribute:MMWideCharacterAttributeName
170                             atIndex:range.location
171                      effectiveRange:NULL])
172             adv += adv;
174         // Create a new font which has the 'fixed advance attribute' set.
175         NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
176             [NSNumber numberWithFloat:adv], NSFontFixedAdvanceAttribute, nil];
177         NSFontDescriptor *desc = [newFont fontDescriptor];
178         desc = [desc fontDescriptorByAddingAttributes:dict];
179         newFont = [NSFont fontWithDescriptor:desc size:[newFont pointSize]];
181         // Now modify the 'attributes' dictionary to hold the new font.
182         NSMutableDictionary *newAttr = [NSMutableDictionary
183             dictionaryWithDictionary:attributes];
184         [newAttr setObject:newFont forKey:NSFontAttributeName];
186         [attribString setAttributes:newAttr range:range];
187     } else {
188         //NSLog(@"NOT fixing font attribute!");
189         [attribString setAttributes:attributes range:range];
190     }
191 #endif
194 - (int)maxRows
196     return maxRows;
199 - (int)maxColumns
201     return maxColumns;
204 - (int)actualRows
206     return actualRows;
209 - (int)actualColumns
211     return actualColumns;
214 - (float)linespace
216     return linespace;
219 - (void)setLinespace:(float)newLinespace
221     NSLayoutManager *lm = [[self layoutManagers] objectAtIndex:0];
222     if (!lm) {
223         NSLog(@"WARNING: No layout manager available in call to %s", _cmd);
224         return;
225     }
227     linespace = newLinespace;
229     // NOTE: The linespace is added to the cell height in order for a multiline
230     // selection not to have white (background color) gaps between lines.  Also
231     // this simplifies the code a lot because there is no need to check the
232     // linespace when calculating the size of the text view etc.  When the
233     // linespace is non-zero the baseline will be adjusted as well; check
234     // MMTypesetter.
235     cellSize.height = linespace + [lm defaultLineHeightForFont:font];
238 - (void)getMaxRows:(int*)rows columns:(int*)cols
240     if (rows) *rows = maxRows;
241     if (cols) *cols = maxColumns;
244 - (void)setMaxRows:(int)rows columns:(int)cols
246     // NOTE: Just remember the new values, the actual resizing is done lazily.
247     maxRows = rows;
248     maxColumns = cols;
251 - (void)drawString:(NSString *)string atRow:(int)row column:(int)col
252              cells:(int)cells withFlags:(int)flags
253    foregroundColor:(NSColor *)fg backgroundColor:(NSColor *)bg
254       specialColor:(NSColor *)sp
256     //NSLog(@"replaceString:atRow:%d column:%d withFlags:%d "
257     //          "foreground:%@ background:%@ special:%@",
258     //          row, col, flags, fg, bg, sp);
259     [self lazyResize:NO];
261     if (row < 0 || row >= maxRows || col < 0 || col >= maxColumns
262             || col+cells > maxColumns || !string || !(fg && bg && sp))
263         return;
265     BOOL hasControlChars = [string rangeOfCharacterFromSet:
266             [NSCharacterSet controlCharacterSet]].location != NSNotFound;
267     if (hasControlChars) {
268         // HACK! If a string for some reason contains control characters, then
269         // draw blanks instead (otherwise charRangeForRow::: fails).
270         NSRange subRange = { 0, cells };
271         flags &= ~DRAW_WIDE;
272         string = [[emptyRowString string] substringWithRange:subRange];
273     }
275     // Find range of characters in text storage to replace.
276     int acol = col;
277     int acells = cells;
278     NSRange range = [self charRangeForRow:row column:&acol cells:&acells];
279     if (NSNotFound == range.location) {
280 #if MM_TS_PARANOIA_LOG
281         NSLog(@"INTERNAL ERROR [%s] Out of bounds", _cmd);
282 #endif
283         return;
284     }
286     // Create dictionary of attributes to apply to the new characters.
287     NSFont *theFont = font;
288     if (flags & DRAW_WIDE) {
289         if (flags & DRAW_BOLD)
290             theFont = flags & DRAW_ITALIC ? boldItalicFontWide : boldFontWide;
291         else if (flags & DRAW_ITALIC)
292             theFont = italicFontWide;
293         else
294             theFont = fontWide;
295     } else {
296         if (flags & DRAW_BOLD)
297             theFont = flags & DRAW_ITALIC ? boldItalicFont : boldFont;
298         else if (flags & DRAW_ITALIC)
299             theFont = italicFont;
300     }
302     NSMutableDictionary *attributes =
303         [NSMutableDictionary dictionaryWithObjectsAndKeys:
304             theFont, NSFontAttributeName,
305             bg, NSBackgroundColorAttributeName,
306             fg, NSForegroundColorAttributeName,
307             sp, NSUnderlineColorAttributeName,
308             [NSNumber numberWithInt:0], NSLigatureAttributeName,
309             nil];
311     if (flags & DRAW_UNDERL) {
312         NSNumber *value = [NSNumber numberWithInt:(NSUnderlineStyleSingle
313                 | NSUnderlinePatternSolid)]; // | NSUnderlineByWordMask
314         [attributes setObject:value forKey:NSUnderlineStyleAttributeName];
315     }
317     if (flags & DRAW_UNDERC) {
318         // TODO: figure out how do draw proper undercurls
319         NSNumber *value = [NSNumber numberWithInt:(NSUnderlineStyleThick
320                 | NSUnderlinePatternDot)]; // | NSUnderlineByWordMask
321         [attributes setObject:value forKey:NSUnderlineStyleAttributeName];
322     }
324     // Mark these characters as wide.  This attribute is subsequently checked
325     // when translating (row,col) pairs to offsets within 'attribString'.
326     if (flags & DRAW_WIDE)
327         [attributes setObject:[NSNull null]
328                        forKey:MMWideCharacterAttributeName];
330     // Replace characters in text storage and apply new attributes.
331     NSRange r = NSMakeRange(range.location, [string length]);
332     [attribString replaceCharactersInRange:range withString:string];
333     [attribString setAttributes:attributes range:r];
335     unsigned changeInLength = [string length] - range.length;
336     if (acells != cells || acol != col) {
337         if (acells == cells + 1) {
338             // NOTE: A normal width character replaced a double width
339             // character.  To maintain the invariant that each row covers the
340             // same amount of cells, we compensate by adding an empty column.
341             [attribString replaceCharactersInRange:NSMakeRange(NSMaxRange(r),0)
342                 withAttributedString:[emptyRowString
343                     attributedSubstringFromRange:NSMakeRange(0,1)]];
344             ++changeInLength;
345 #if 0
346         } else if (acol == col - 1) {
347             NSLog(@"acol == col - 1");
348             [attribString replaceCharactersInRange:NSMakeRange(r.location,0)
349                 withAttributedString:[emptyRowString
350                     attributedSubstringFromRange:NSMakeRange(0,1)]];
351             ++changeInLength;
352         } else if (acol == col + 1) {
353             NSLog(@"acol == col + 1");
354             [attribString replaceCharactersInRange:NSMakeRange(r.location-1,1)
355                 withAttributedString:[emptyRowString
356                     attributedSubstringFromRange:NSMakeRange(0,2)]];
357             ++changeInLength;
358 #endif
359         } else {
360             // NOTE: It seems that this never gets called.  If it ever does,
361             // then there is another case to treat.
362 #if MM_TS_PARANOIA_LOG
363             NSLog(@"row=%d col=%d acol=%d cells=%d acells=%d", row, col, acol,
364                     cells, acells);
365 #endif
366         }
367     }
369     if ((flags & DRAW_WIDE) || [string length] != cells)
370         characterEqualsColumn = NO;
372     [self fixInvalidCharactersInRange:r];
374     [self edited:(NSTextStorageEditedCharacters|NSTextStorageEditedAttributes)
375            range:range changeInLength:changeInLength];
377 #if MM_USE_ROW_CACHE
378     rowCache[row].length += changeInLength;
379 #endif
383  * Delete 'count' lines from 'row' and insert 'count' empty lines at the bottom
384  * of the scroll region.
385  */
386 - (void)deleteLinesFromRow:(int)row lineCount:(int)count
387               scrollBottom:(int)bottom left:(int)left right:(int)right
388                      color:(NSColor *)color
390     //NSLog(@"deleteLinesFromRow:%d lineCount:%d color:%@", row, count, color);
391     [self lazyResize:NO];
393     if (row < 0 || row+count > maxRows || bottom > maxRows || left < 0
394             || right > maxColumns)
395         return;
397     int total = 1 + bottom - row;
398     int move = total - count;
399     int width = right - left + 1;
400     int destRow = row;
401     NSRange destRange, srcRange;
402     int i;
404     for (i = 0; i < move; ++i, ++destRow) {
405         int acol = left;
406         int acells = width;
407         destRange = [self charRangeForRow:destRow column:&acol cells:&acells];
408 #if MM_TS_PARANOIA_LOG
409         if (acells != width || acol != left)
410             NSLog(@"INTERNAL ERROR [%s]", _cmd);
411 #endif
413         acol = left; acells = width;
414         srcRange = [self charRangeForRow:(destRow+count) column:&acol
415                                    cells:&acells];
416 #if MM_TS_PARANOIA_LOG
417         if (acells != width || acol != left)
418             NSLog(@"INTERNAL ERROR [%s]", _cmd);
419 #endif
421         if (NSNotFound == destRange.location || NSNotFound == srcRange.location)
422         {
423 #if MM_TS_PARANOIA_LOG
424             NSLog(@"INTERNAL ERROR [%s] Out of bounds", _cmd);
425 #endif
426             return;
427         }
429         NSAttributedString *srcString = [attribString
430                 attributedSubstringFromRange:srcRange];
432         [attribString replaceCharactersInRange:destRange
433                           withAttributedString:srcString];
434         [self edited:(NSTextStorageEditedCharacters
435                 | NSTextStorageEditedAttributes) range:destRange
436                 changeInLength:([srcString length]-destRange.length)];
438 #if MM_USE_ROW_CACHE
439         rowCache[destRow].length += [srcString length] - destRange.length;
440 #endif
441     }
442     
443     NSRange emptyRange = {0,width};
444     NSAttributedString *emptyString =
445             [emptyRowString attributedSubstringFromRange:emptyRange];
446     NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
447             font, NSFontAttributeName,
448             color, NSBackgroundColorAttributeName, nil];
450     for (i = 0; i < count; ++i, ++destRow) {
451         int acol = left;
452         int acells = width;
453         destRange = [self charRangeForRow:destRow column:&acol cells:&acells];
454 #if MM_TS_PARANOIA_LOG
455         if (acells != width || acol != left)
456             NSLog(@"INTERNAL ERROR [%s]", _cmd);
457 #endif
458         if (NSNotFound == destRange.location) {
459 #if MM_TS_PARANOIA_LOG
460             NSLog(@"INTERNAL ERROR [%s] Out of bounds", _cmd);
461 #endif
462             return;
463         }
465         [attribString replaceCharactersInRange:destRange
466                           withAttributedString:emptyString];
467         [attribString setAttributes:attribs
468                               range:NSMakeRange(destRange.location, width)];
470         [self edited:(NSTextStorageEditedAttributes
471                 | NSTextStorageEditedCharacters) range:destRange
472                 changeInLength:([emptyString length]-destRange.length)];
474 #if MM_USE_ROW_CACHE
475         rowCache[destRow].length += [emptyString length] - destRange.length;
476 #endif
477     }
481  * Insert 'count' empty lines at 'row' and delete 'count' lines from the bottom
482  * of the scroll region.
483  */
484 - (void)insertLinesAtRow:(int)row lineCount:(int)count
485             scrollBottom:(int)bottom left:(int)left right:(int)right
486                    color:(NSColor *)color
488     //NSLog(@"insertLinesAtRow:%d lineCount:%d color:%@", row, count, color);
489     [self lazyResize:NO];
491     if (row < 0 || row+count > maxRows || bottom > maxRows || left < 0
492             || right > maxColumns)
493         return;
495     int total = 1 + bottom - row;
496     int move = total - count;
497     int width = right - left + 1;
498     int destRow = bottom;
499     int srcRow = row + move - 1;
500     NSRange destRange, srcRange;
501     int i;
503     for (i = 0; i < move; ++i, --destRow, --srcRow) {
504         int acol = left;
505         int acells = width;
506         destRange = [self charRangeForRow:destRow column:&acol cells:&acells];
507 #if MM_TS_PARANOIA_LOG
508         if (acells != width || acol != left)
509             NSLog(@"INTERNAL ERROR [%s]", _cmd);
510 #endif
512         acol = left; acells = width;
513         srcRange = [self charRangeForRow:srcRow column:&acol cells:&acells];
514 #if MM_TS_PARANOIA_LOG
515         if (acells != width || acol != left)
516             NSLog(@"INTERNAL ERROR [%s]", _cmd);
517 #endif
518         if (NSNotFound == destRange.location || NSNotFound == srcRange.location)
519         {
520 #if MM_TS_PARANOIA_LOG
521             NSLog(@"INTERNAL ERROR [%s] Out of bounds", _cmd);
522 #endif
523             return;
524         }
526         NSAttributedString *srcString = [attribString
527                 attributedSubstringFromRange:srcRange];
528         [attribString replaceCharactersInRange:destRange
529                           withAttributedString:srcString];
530         [self edited:(NSTextStorageEditedCharacters
531                 | NSTextStorageEditedAttributes) range:destRange
532                 changeInLength:([srcString length]-destRange.length)];
534 #if MM_USE_ROW_CACHE
535         rowCache[destRow].length += [srcString length] - destRange.length;
536 #endif
537     }
538     
539     NSRange emptyRange = {0,width};
540     NSAttributedString *emptyString =
541             [emptyRowString attributedSubstringFromRange:emptyRange];
542     NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
543             font, NSFontAttributeName,
544             color, NSBackgroundColorAttributeName, nil];
545     
546     for (i = 0; i < count; ++i, --destRow) {
547         int acol = left;
548         int acells = width;
549         destRange = [self charRangeForRow:destRow column:&acol cells:&acells];
550 #if MM_TS_PARANOIA_LOG
551         if (acells != width || acol != left)
552             NSLog(@"INTERNAL ERROR [%s]", _cmd);
553 #endif
554         if (NSNotFound == destRange.location) {
555 #if MM_TS_PARANOIA_LOG
556             NSLog(@"INTERNAL ERROR [%s] Out of bounds", _cmd);
557 #endif
558             return;
559         }
561         [attribString replaceCharactersInRange:destRange
562                           withAttributedString:emptyString];
563         [attribString setAttributes:attribs
564                               range:NSMakeRange(destRange.location, width)];
566         [self edited:(NSTextStorageEditedAttributes
567                 | NSTextStorageEditedCharacters) range:destRange
568                 changeInLength:([emptyString length]-destRange.length)];
570 #if MM_USE_ROW_CACHE
571         rowCache[destRow].length += [emptyString length] - destRange.length;
572 #endif
573     }
576 - (void)clearBlockFromRow:(int)row1 column:(int)col1 toRow:(int)row2
577                    column:(int)col2 color:(NSColor *)color
579     //NSLog(@"clearBlockFromRow:%d column:%d toRow:%d column:%d color:%@",
580     //        row1, col1, row2, col2, color);
581     [self lazyResize:NO];
583     if (row1 < 0 || row2 >= maxRows || col1 < 0 || col2 > maxColumns)
584         return;
586     NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
587             font, NSFontAttributeName,
588             color, NSBackgroundColorAttributeName, nil];
589     int cells = col2 - col1 + 1;
590     NSRange range, emptyRange = {0, cells};
591     NSAttributedString *emptyString =
592             [emptyRowString attributedSubstringFromRange:emptyRange];
593     int r;
595     for (r=row1; r<=row2; ++r) {
596         int acol = col1;
597         int acells = cells;
598         range = [self charRangeForRow:r column:&acol cells:&acells];
599 #if MM_TS_PARANOIA_LOG
600         if (acells != cells || acol != col1)
601             NSLog(@"INTERNAL ERROR [%s]", _cmd);
602 #endif
603         if (NSNotFound == range.location) {
604 #if MM_TS_PARANOIA_LOG
605             NSLog(@"INTERNAL ERROR [%s] Out of bounds", _cmd);
606 #endif
607             return;
608         }
610         [attribString replaceCharactersInRange:range
611                           withAttributedString:emptyString];
612         [attribString setAttributes:attribs
613                               range:NSMakeRange(range.location, cells)];
615         [self edited:(NSTextStorageEditedAttributes
616                 | NSTextStorageEditedCharacters) range:range
617                                         changeInLength:cells-range.length];
619 #if MM_USE_ROW_CACHE
620         rowCache[r].length += cells - range.length;
621 #endif
622     }
625 - (void)clearAll
627     //NSLog(@"%s", _cmd);
628     [self lazyResize:YES];
631 - (void)setDefaultColorsBackground:(NSColor *)bgColor
632                         foreground:(NSColor *)fgColor
634     if (defaultBackgroundColor != bgColor) {
635         [defaultBackgroundColor release];
636         defaultBackgroundColor = bgColor ? [bgColor retain] : nil;
637     }
639     // NOTE: The default foreground color isn't actually used for anything, but
640     // other class instances might want to be able to access it so it is stored
641     // here.
642     if (defaultForegroundColor != fgColor) {
643         [defaultForegroundColor release];
644         defaultForegroundColor = fgColor ? [fgColor retain] : nil;
645     }
648 - (void)setFont:(NSFont*)newFont
650     if (newFont && font != newFont) {
651         [boldItalicFont release];  boldItalicFont = nil;
652         [italicFont release];  italicFont = nil;
653         [boldFont release];  boldFont = nil;
654         [font release];  font = nil;
656         // NOTE! When setting a new font we make sure that the advancement of
657         // each glyph is fixed.
659         float em = [@"m" sizeWithAttributes:
660                 [NSDictionary dictionaryWithObject:newFont
661                                             forKey:NSFontAttributeName]].width;
662         float cellWidthMultiplier = [[NSUserDefaults standardUserDefaults]
663                 floatForKey:MMCellWidthMultiplierKey];
665         // NOTE! Even though NSFontFixedAdvanceAttribute is a float, it will
666         // only render at integer sizes.  Hence, we restrict the cell width to
667         // an integer here, otherwise the window width and the actual text
668         // width will not match.
669         cellSize.width = ceilf(em * cellWidthMultiplier);
671         float pointSize = [newFont pointSize];
672         NSDictionary *dict = [NSDictionary
673             dictionaryWithObject:[NSNumber numberWithFloat:cellSize.width]
674                           forKey:NSFontFixedAdvanceAttribute];
676         NSFontDescriptor *desc = [newFont fontDescriptor];
677         desc = [desc fontDescriptorByAddingAttributes:dict];
678         font = [NSFont fontWithDescriptor:desc size:pointSize];
679         [font retain];
681         NSLayoutManager *lm = [[self layoutManagers] objectAtIndex:0];
682         if (lm) {
683             cellSize.height = linespace + [lm defaultLineHeightForFont:font];
684         } else {
685             // Should never happen, set some bogus value for cell height.
686             NSLog(@"WARNING: No layout manager available in call to %s", _cmd);
687             cellSize.height = linespace + 16.0;
688         }
690         // NOTE: The font manager does not care about the 'font fixed advance'
691         // attribute, so after converting the font we have to add this
692         // attribute again.
693         boldFont = [[NSFontManager sharedFontManager]
694             convertFont:font toHaveTrait:NSBoldFontMask];
695         desc = [boldFont fontDescriptor];
696         desc = [desc fontDescriptorByAddingAttributes:dict];
697         boldFont = [NSFont fontWithDescriptor:desc size:pointSize];
698         [boldFont retain];
700         italicFont = [[NSFontManager sharedFontManager]
701             convertFont:font toHaveTrait:NSItalicFontMask];
702         desc = [italicFont fontDescriptor];
703         desc = [desc fontDescriptorByAddingAttributes:dict];
704         italicFont = [NSFont fontWithDescriptor:desc size:pointSize];
705         [italicFont retain];
707         boldItalicFont = [[NSFontManager sharedFontManager]
708             convertFont:italicFont toHaveTrait:NSBoldFontMask];
709         desc = [boldItalicFont fontDescriptor];
710         desc = [desc fontDescriptorByAddingAttributes:dict];
711         boldItalicFont = [NSFont fontWithDescriptor:desc size:pointSize];
712         [boldItalicFont retain];
713     }
716 - (void)setWideFont:(NSFont *)newFont
718     if (!newFont) {
719         // Use the normal font as the wide font (note that the normal font may
720         // very well include wide characters.)
721         if (font) [self setWideFont:font];
722     } else if (newFont != fontWide) {
723         [boldItalicFontWide release];  boldItalicFontWide = nil;
724         [italicFontWide release];  italicFontWide = nil;
725         [boldFontWide release];  boldFontWide = nil;
726         [fontWide release];  fontWide = nil;
728         float pointSize = [newFont pointSize];
729         NSFontDescriptor *desc = [newFont fontDescriptor];
730         NSDictionary *dictWide = [NSDictionary
731             dictionaryWithObject:[NSNumber numberWithFloat:2*cellSize.width]
732                           forKey:NSFontFixedAdvanceAttribute];
734         desc = [desc fontDescriptorByAddingAttributes:dictWide];
735         fontWide = [NSFont fontWithDescriptor:desc size:pointSize];
736         [fontWide retain];
738         boldFontWide = [[NSFontManager sharedFontManager]
739             convertFont:fontWide toHaveTrait:NSBoldFontMask];
740         desc = [boldFontWide fontDescriptor];
741         desc = [desc fontDescriptorByAddingAttributes:dictWide];
742         boldFontWide = [NSFont fontWithDescriptor:desc size:pointSize];
743         [boldFontWide retain];
745         italicFontWide = [[NSFontManager sharedFontManager]
746             convertFont:fontWide toHaveTrait:NSItalicFontMask];
747         desc = [italicFontWide fontDescriptor];
748         desc = [desc fontDescriptorByAddingAttributes:dictWide];
749         italicFontWide = [NSFont fontWithDescriptor:desc size:pointSize];
750         [italicFontWide retain];
752         boldItalicFontWide = [[NSFontManager sharedFontManager]
753             convertFont:italicFontWide toHaveTrait:NSBoldFontMask];
754         desc = [boldItalicFontWide fontDescriptor];
755         desc = [desc fontDescriptorByAddingAttributes:dictWide];
756         boldItalicFontWide = [NSFont fontWithDescriptor:desc size:pointSize];
757         [boldItalicFontWide retain];
758     }
761 - (NSFont*)font
763     return font;
766 - (NSFont*)fontWide
768     return fontWide;
771 - (NSColor *)defaultBackgroundColor
773     return defaultBackgroundColor;
776 - (NSColor *)defaultForegroundColor
778     return defaultForegroundColor;
781 - (NSSize)size
783     return NSMakeSize(maxColumns*cellSize.width, maxRows*cellSize.height);
786 - (NSSize)cellSize
788     return cellSize;
791 - (NSRect)rectForRowsInRange:(NSRange)range
793     NSRect rect = { 0, 0, 0, 0 };
794     unsigned start = range.location > maxRows ? maxRows : range.location;
795     unsigned length = range.length;
797     if (start+length > maxRows)
798         length = maxRows - start;
800     rect.origin.y = cellSize.height * start;
801     rect.size.height = cellSize.height * length;
803     return rect;
806 - (NSRect)rectForColumnsInRange:(NSRange)range
808     NSRect rect = { 0, 0, 0, 0 };
809     unsigned start = range.location > maxColumns ? maxColumns : range.location;
810     unsigned length = range.length;
812     if (start+length > maxColumns)
813         length = maxColumns - start;
815     rect.origin.x = cellSize.width * start;
816     rect.size.width = cellSize.width * length;
818     return rect;
821 - (unsigned)characterIndexForRow:(int)row column:(int)col
823     int cells = 1;
824     NSRange range = [self charRangeForRow:row column:&col cells:&cells];
825     return range.location != NSNotFound ? range.location : 0;
828 // XXX: unused at the moment
829 - (BOOL)resizeToFitSize:(NSSize)size
831     int rows = maxRows, cols = maxColumns;
833     [self fitToSize:size rows:&rows columns:&cols];
834     if (rows != maxRows || cols != maxColumns) {
835         [self setMaxRows:rows columns:cols];
836         return YES;
837     }
839     // Return NO only if dimensions did not change.
840     return NO;
843 - (NSSize)fitToSize:(NSSize)size
845     return [self fitToSize:size rows:NULL columns:NULL];
848 - (NSSize)fitToSize:(NSSize)size rows:(int *)rows columns:(int *)columns
850     NSSize curSize = [self size];
851     NSSize fitSize = curSize;
852     int fitRows = maxRows;
853     int fitCols = maxColumns;
855     if (size.height < curSize.height) {
856         // Remove lines until the height of the text storage fits inside
857         // 'size'.  However, always make sure there are at least 3 lines in the
858         // text storage.  (Why 3? It seem Vim never allows less than 3 lines.)
859         //
860         // TODO: No need to search since line height is fixed, just calculate
861         // the new height.
862         int rowCount = maxRows;
863         int rowsToRemove;
864         for (rowsToRemove = 0; rowsToRemove < maxRows-3; ++rowsToRemove) {
865             float height = cellSize.height*rowCount;
867             if (height <= size.height) {
868                 fitSize.height = height;
869                 break;
870             }
872             --rowCount;
873         }
875         fitRows -= rowsToRemove;
876     } else if (size.height > curSize.height) {
877         float fh = cellSize.height;
878         if (fh < 1.0f) fh = 1.0f;
880         fitRows = floor(size.height/fh);
881         fitSize.height = fh*fitRows;
882     }
884     if (size.width != curSize.width) {
885         float fw = cellSize.width;
886         if (fw < 1.0f) fw = 1.0f;
888         fitCols = floor(size.width/fw);
889         fitSize.width = fw*fitCols;
890     }
892     if (rows) *rows = fitRows;
893     if (columns) *columns = fitCols;
895     return fitSize;
898 - (NSRect)boundingRectForCharacterAtRow:(int)row column:(int)col
900 #if 1
901     // This properly computes the position of where Vim expects the glyph to be
902     // drawn.  Had the typesetter actually computed the right position of each
903     // character and not hidden some, this code would be correct.
904     NSRect rect = NSZeroRect;
906     rect.origin.x = col*cellSize.width;
907     rect.origin.y = row*cellSize.height;
908     rect.size = cellSize;
910     // Wide character take up twice the width of a normal character.
911     int cells = 1;
912     NSRange r = [self charRangeForRow:row column:&col cells:&cells];
913     if (NSNotFound != r.location
914             && [attribString attribute:MMWideCharacterAttributeName
915                                atIndex:r.location
916                         effectiveRange:nil])
917         rect.size.width += rect.size.width;
919     return rect;
920 #else
921     // Use layout manager to compute bounding rect.  This works in situations
922     // where the layout manager decides to hide glyphs (Vim assumes all glyphs
923     // are drawn).
924     NSLayoutManager *lm = [[self layoutManagers] objectAtIndex:0];
925     NSTextContainer *tc = [[lm textContainers] objectAtIndex:0];
926     int cells = 1;
927     NSRange range = [self charRangeForRow:row column:&col cells:&cells];
928     NSRange glyphRange = [lm glyphRangeForCharacterRange:range
929                                     actualCharacterRange:NULL];
931     return [lm boundingRectForGlyphRange:glyphRange inTextContainer:tc];
932 #endif
935 #if MM_USE_ROW_CACHE
936 - (MMRowCacheEntry *)rowCache
938     return rowCache;
940 #endif
942 @end // MMTextStorage
947 @implementation MMTextStorage (Private)
949 - (void)lazyResize:(BOOL)force
951     // Do nothing if the dimensions are already right.
952     if (!force && actualRows == maxRows && actualColumns == maxColumns)
953         return;
955     NSRange oldRange = NSMakeRange(0, [attribString length]);
957     actualRows = maxRows;
958     actualColumns = maxColumns;
959     characterEqualsColumn = YES;
961 #if MM_USE_ROW_CACHE
962     free(rowCache);
963     rowCache = (MMRowCacheEntry*)calloc(actualRows, sizeof(MMRowCacheEntry));
964 #endif
966     NSDictionary *dict;
967     if (defaultBackgroundColor) {
968         dict = [NSDictionary dictionaryWithObjectsAndKeys:
969                 font, NSFontAttributeName,
970                 defaultBackgroundColor, NSBackgroundColorAttributeName, nil];
971     } else {
972         dict = [NSDictionary dictionaryWithObjectsAndKeys:
973                 font, NSFontAttributeName, nil];
974     }
975             
976     NSMutableString *rowString = [NSMutableString string];
977     int i;
978     for (i = 0; i < maxColumns; ++i) {
979         [rowString appendString:@" "];
980     }
981     [rowString appendString:@"\n"];
983     [emptyRowString release];
984     emptyRowString = [[NSAttributedString alloc] initWithString:rowString
985                                                      attributes:dict];
987     [attribString release];
988     attribString = [[NSMutableAttributedString alloc] init];
989     for (i=0; i<maxRows; ++i) {
990 #if MM_USE_ROW_CACHE
991         rowCache[i].length = actualColumns + 1;
992 #endif
993         [attribString appendAttributedString:emptyRowString];
994     }
996     NSRange fullRange = NSMakeRange(0, [attribString length]);
997     [self edited:(NSTextStorageEditedCharacters|NSTextStorageEditedAttributes)
998            range:oldRange changeInLength:fullRange.length-oldRange.length];
1001 - (NSRange)charRangeForRow:(int)row column:(int*)pcol cells:(int*)pcells
1003     int col = *pcol;
1004     int cells = *pcells;
1006     // If no wide chars are used and if every char has length 1 (no composing
1007     // characters, no > 16 bit characters), then we can compute the range.
1008     if (characterEqualsColumn)
1009         return NSMakeRange(row*(actualColumns+1) + col, cells);
1011     NSString *string = [attribString string];
1012     NSRange r, range = { NSNotFound, 0 };
1013     unsigned idx, rowEnd;
1014     int i;
1016     if (row < 0 || row >= actualRows || col < 0 || col >= actualColumns
1017             || col+cells > actualColumns) {
1018 #if MM_TS_PARANOIA_LOG
1019         NSLog(@"%s row=%d col=%d cells=%d is out of range (length=%d)",
1020                 _cmd, row, col, cells, [string length]);
1021 #endif
1022         return range;
1023     }
1025 #if MM_USE_ROW_CACHE
1026     // Locate the beginning of the row
1027     MMRowCacheEntry *cache = rowCache;
1028     idx = 0;
1029     for (i = 0; i < row; ++i, ++cache)
1030         idx += cache->length;
1032     rowEnd = idx + cache->length;
1033 #else
1034     // Locate the beginning of the row by scanning for EOL characters.
1035     r.location = 0;
1036     for (i = 0; i < row; ++i) {
1037         r.length = [string length] - r.location;
1038         r = [string rangeOfString:@"\n" options:NSLiteralSearch range:r];
1039         if (NSNotFound == r.location)
1040             return range;
1041         ++r.location;
1042     }
1043 #endif
1045     // Locate the column
1046 #if MM_USE_ROW_CACHE
1047     cache = &rowCache[row];
1049     i = cache->col;
1050     if (col == i) {
1051         // Cache hit
1052         idx += cache->colOffset;
1053     } else {
1054         range.location = idx;
1056 #if 0  // Backward search seems to be broken...
1057         // Cache miss
1058         if (col < i - col) {
1059             // Search forward from beginning of line.
1060             i = 0;
1061         } else if (actualColumns - col < col - i) {
1062             // Search backward from end of line.
1063             i = actualColumns - 1;
1064             idx += cache->length - 2;
1065         } else {
1066             // Search from cache spot (forward or backward).
1067             idx += cache->colOffset;
1068         }
1070         if (col > i) {
1071             // Forward search
1072             while (col > i) {
1073                 r = [string rangeOfComposedCharacterSequenceAtIndex:idx];
1075                 // Wide chars take up two display cells.
1076                 if ([attribString attribute:MMWideCharacterAttributeName
1077                                     atIndex:idx
1078                              effectiveRange:nil])
1079                     ++i;
1081                 idx += r.length;
1082                 ++i;
1083             }
1084         } else if (col < i) {
1085             // Backward search
1086             while (col < i) {
1087                 r = [string rangeOfComposedCharacterSequenceAtIndex:idx-1];
1088                 idx -= r.length;
1089                 --i;
1091                 // Wide chars take up two display cells.
1092                 if ([attribString attribute:MMWideCharacterAttributeName
1093                                     atIndex:idx
1094                              effectiveRange:nil])
1095                     --i;
1096             }
1097         }
1099         *pcol = i;
1100         cache->col = i;
1101         cache->colOffset = idx - range.location;
1102 #else
1103         // Cache miss
1104         if (col < i) {
1105             // Search forward from beginning of line.
1106             i = 0;
1107         } else {
1108             // Search forward from cache spot.
1109             idx += cache->colOffset;
1110         }
1112         // Forward search
1113         while (col > i) {
1114             r = [string rangeOfComposedCharacterSequenceAtIndex:idx];
1116             // Wide chars take up two display cells.
1117             if ([attribString attribute:MMWideCharacterAttributeName
1118                                 atIndex:idx
1119                          effectiveRange:nil])
1120                 ++i;
1122             idx += r.length;
1123             ++i;
1124         }
1126         *pcol = i;
1127         cache->col = i;
1128         cache->colOffset = idx - range.location;
1129 #endif
1130     }
1131 #else
1132     idx = r.location;
1133     for (i = 0; i < col; ++i) {
1134         r = [string rangeOfComposedCharacterSequenceAtIndex:idx];
1136         // Wide chars take up two display cells.
1137         if ([attribString attribute:MMWideCharacterAttributeName
1138                             atIndex:idx
1139                      effectiveRange:nil])
1140             ++i;
1142         idx += r.length;
1143     }
1144 #endif
1146     // Count the number of characters that cover the cells.
1147     range.location = idx;
1148     for (i = 0; i < cells; ++i) {
1149         r = [string rangeOfComposedCharacterSequenceAtIndex:idx];
1151         // Wide chars take up two display cells.
1152         if ([attribString attribute:MMWideCharacterAttributeName
1153                             atIndex:idx
1154                      effectiveRange:nil])
1155             ++i;
1157         idx += r.length;
1158         range.length += r.length;
1159     }
1161     *pcells = i;
1163 #if MM_TS_PARANOIA_LOG
1164 #if MM_USE_ROW_CACHE
1165     if (range.location >= rowEnd-1) {
1166         NSLog(@"INTERNAL ERROR [%s] : row=%d col=%d cells=%d --> range=%@",
1167                 _cmd, row, col, cells, NSStringFromRange(range));
1168         range.location = rowEnd - 2;
1169         range.length = 1;
1170     } else if (NSMaxRange(range) >= rowEnd) {
1171         NSLog(@"INTERNAL ERROR [%s] : row=%d col=%d cells=%d --> range=%@",
1172                 _cmd, row, col, cells, NSStringFromRange(range));
1173         range.length = rowEnd - range.location - 1;
1174     }
1175 #endif
1177     if (NSMaxRange(range) > [string length]) {
1178         NSLog(@"INTERNAL ERROR [%s] : row=%d col=%d cells=%d --> range=%@",
1179                 _cmd, row, col, cells, NSStringFromRange(range));
1180         range.location = NSNotFound;
1181         range.length = 0;
1182     }
1183 #endif
1185     return range;
1188 - (void)fixInvalidCharactersInRange:(NSRange)range
1190     static NSCharacterSet *invalidCharacterSet = nil;
1191     NSRange invalidRange;
1192     unsigned end;
1194     if (!invalidCharacterSet)
1195         invalidCharacterSet = [[NSCharacterSet characterSetWithRange:
1196             NSMakeRange(0x2028, 2)] retain];
1198     // HACK! Replace characters that the text system can't handle (currently
1199     // LINE SEPARATOR U+2028 and PARAGRAPH SEPARATOR U+2029) with space.
1200     //
1201     // TODO: Treat these separately inside of Vim so we don't have to bother
1202     // here.
1203     while (range.length > 0) {
1204         invalidRange = [[attribString string]
1205             rangeOfCharacterFromSet:invalidCharacterSet
1206                             options:NSLiteralSearch
1207                               range:range];
1208         if (NSNotFound == invalidRange.location)
1209             break;
1211         [attribString replaceCharactersInRange:invalidRange withString:@" "];
1213         end = NSMaxRange(invalidRange);
1214         range.length -= end - range.location;
1215         range.location = end;
1216     }
1219 @end // MMTextStorage (Private)