Fix a wide character bug
[MacVim.git] / src / MacVim / MMTextStorage.m
blob7df3935085fedf8c55d28b415603d2dd3c21d050
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"
39 // Enable debug log messages for situations that should never occur.
40 #define MM_TS_PARANOIA_LOG 1
44 // TODO: What does DRAW_TRANSP flag do?  If the background isn't drawn when
45 // this flag is set, then sometimes the character after the cursor becomes
46 // blank.  Everything seems to work fine by just ignoring this flag.
47 #define DRAW_TRANSP               0x01    /* draw with transparant bg */
48 #define DRAW_BOLD                 0x02    /* draw bold text */
49 #define DRAW_UNDERL               0x04    /* draw underline text */
50 #define DRAW_UNDERC               0x08    /* draw undercurl text */
51 #define DRAW_ITALIC               0x10    /* draw italic text */
52 #define DRAW_CURSOR               0x20
55 static NSString *MMWideCharacterAttributeName = @"MMWideChar";
60 @interface MMTextStorage (Private)
61 - (void)lazyResize:(BOOL)force;
62 - (NSRange)charRangeForRow:(int)row column:(int*)col cells:(int*)cells;
63 - (void)fixInvalidCharactersInRange:(NSRange)range;
64 @end
68 @implementation MMTextStorage
70 - (id)init
72     if ((self = [super init])) {
73         attribString = [[NSMutableAttributedString alloc] initWithString:@""];
74         // NOTE!  It does not matter which font is set here, Vim will set its
75         // own font on startup anyway.  Just set some bogus values.
76         font = [[NSFont userFixedPitchFontOfSize:0] retain];
77         cellSize.height = [font pointSize];
78         cellSize.width = [font defaultLineHeightForFont];
79     }
81     return self;
84 - (void)dealloc
86 #if MM_USE_ROW_CACHE
87     if (rowCache) {
88         free(rowCache);
89         rowCache = NULL;
90     }
91 #endif
92     [emptyRowString release];
93     [boldItalicFontWide release];
94     [italicFontWide release];
95     [boldFontWide release];
96     [fontWide release];
97     [boldItalicFont release];
98     [italicFont release];
99     [boldFont release];
100     [font release];
101     [defaultBackgroundColor release];
102     [defaultForegroundColor release];
103     [attribString release];
104     [super dealloc];
107 - (NSString *)string
109     return [attribString string];
112 - (NSDictionary *)attributesAtIndex:(unsigned)index
113                      effectiveRange:(NSRangePointer)range
115     if (index >= [attribString length]) {
116         if (range)
117             *range = NSMakeRange(NSNotFound, 0);
119         return [NSDictionary dictionary];
120     }
122     return [attribString attributesAtIndex:index effectiveRange:range];
125 - (id)attribute:(NSString *)attrib atIndex:(unsigned)index
126         effectiveRange:(NSRangePointer)range
128     return [attribString attribute:attrib atIndex:index effectiveRange:range];
131 - (void)replaceCharactersInRange:(NSRange)range
132                       withString:(NSString *)string
134 #if MM_TS_PARANOIA_LOG
135     NSLog(@"WARNING: calling %s on MMTextStorage is unsupported", _cmd);
136 #endif
137     //[attribString replaceCharactersInRange:range withString:string];
140 - (void)setAttributes:(NSDictionary *)attributes range:(NSRange)range
142     //NSLog(@"%s%@", _cmd, NSStringFromRange(range));
144     // NOTE!  This method must be implemented since the text system calls it
145     // constantly to 'fix attributes', apply font substitution, etc.
146 #if 0
147     [attribString setAttributes:attributes range:range];
148 #elif 1
149     // HACK! If the font attribute is being modified, then ensure that the new
150     // font has a fixed advancement which is either the same as the current
151     // font or twice that, depending on whether it is a 'wide' character that
152     // is being fixed or not.
153     //
154     // TODO: This code assumes that the characters in 'range' all have the same
155     // width.
156     NSFont *newFont = [attributes objectForKey:NSFontAttributeName];
157     if (newFont) {
158         // Allow disabling of font substitution via a user default.  Not
159         // recommended since the typesetter hides the corresponding glyphs and
160         // the display gets messed up.
161         if ([[NSUserDefaults standardUserDefaults]
162                 boolForKey:MMNoFontSubstitutionKey])
163             return;
165         float adv = cellSize.width;
166         if ([attribString attribute:MMWideCharacterAttributeName
167                             atIndex:range.location
168                      effectiveRange:NULL])
169             adv += adv;
171         // Create a new font which has the 'fixed advance attribute' set.
172         NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
173             [NSNumber numberWithFloat:adv], NSFontFixedAdvanceAttribute, nil];
174         NSFontDescriptor *desc = [newFont fontDescriptor];
175         desc = [desc fontDescriptorByAddingAttributes:dict];
176         newFont = [NSFont fontWithDescriptor:desc size:[newFont pointSize]];
178         // Now modify the 'attributes' dictionary to hold the new font.
179         NSMutableDictionary *newAttr = [NSMutableDictionary
180             dictionaryWithDictionary:attributes];
181         [newAttr setObject:newFont forKey:NSFontAttributeName];
183         [attribString setAttributes:newAttr range:range];
184     } else {
185         //NSLog(@"NOT fixing font attribute!");
186         [attribString setAttributes:attributes range:range];
187     }
188 #endif
191 - (int)maxRows
193     return maxRows;
196 - (int)maxColumns
198     return maxColumns;
201 - (int)actualRows
203     return actualRows;
206 - (int)actualColumns
208     return actualColumns;
211 - (float)linespace
213     return linespace;
216 - (void)setLinespace:(float)newLinespace
218     NSLayoutManager *lm = [[self layoutManagers] objectAtIndex:0];
220     linespace = newLinespace;
222     // NOTE: The linespace is added to the cell height in order for a multiline
223     // selection not to have white (background color) gaps between lines.  Also
224     // this simplifies the code a lot because there is no need to check the
225     // linespace when calculating the size of the text view etc.  When the
226     // linespace is non-zero the baseline will be adjusted as well; check
227     // MMTypesetter.
228     cellSize.height = linespace + (lm ? [lm defaultLineHeightForFont:font]
229                                       : [font defaultLineHeightForFont]);
232 - (void)getMaxRows:(int*)rows columns:(int*)cols
234     if (rows) *rows = maxRows;
235     if (cols) *cols = maxColumns;
238 - (void)setMaxRows:(int)rows columns:(int)cols
240     // NOTE: Just remember the new values, the actual resizing is done lazily.
241     maxRows = rows;
242     maxColumns = cols;
245 - (void)drawString:(NSString *)string atRow:(int)row column:(int)col
246              cells:(int)cells withFlags:(int)flags
247    foregroundColor:(NSColor *)fg backgroundColor:(NSColor *)bg
248       specialColor:(NSColor *)sp
250     //NSLog(@"replaceString:atRow:%d column:%d withFlags:%d "
251     //          "foreground:%@ background:%@ special:%@",
252     //          row, col, flags, fg, bg, sp);
253     [self lazyResize:NO];
255     if (row < 0 || row >= maxRows || col < 0 || col >= maxColumns
256             || col+cells > maxColumns || !string || !(fg && bg && sp))
257         return;
259     // Find range of characters in text storage to replace.
260     int acol = col;
261     int acells = cells;
262     NSRange range = [self charRangeForRow:row column:&acol cells:&acells];
263     if (NSNotFound == range.location) {
264 #if MM_TS_PARANOIA_LOG
265         NSLog(@"INTERNAL ERROR [%s] Out of bounds", _cmd);
266 #endif
267         return;
268     }
270     // Create dictionary of attributes to apply to the new characters.
271     NSFont *theFont = font;
272     if (flags & DRAW_WIDE) {
273         if (flags & DRAW_BOLD)
274             theFont = flags & DRAW_ITALIC ? boldItalicFontWide : boldFontWide;
275         else if (flags & DRAW_ITALIC)
276             theFont = italicFontWide;
277         else
278             theFont = fontWide;
279     } else {
280         if (flags & DRAW_BOLD)
281             theFont = flags & DRAW_ITALIC ? boldItalicFont : boldFont;
282         else if (flags & DRAW_ITALIC)
283             theFont = italicFont;
284     }
286     NSMutableDictionary *attributes =
287         [NSMutableDictionary dictionaryWithObjectsAndKeys:
288             theFont, NSFontAttributeName,
289             bg, NSBackgroundColorAttributeName,
290             fg, NSForegroundColorAttributeName,
291             sp, NSUnderlineColorAttributeName,
292             nil];
294     if (flags & DRAW_UNDERL) {
295         NSNumber *value = [NSNumber numberWithInt:(NSUnderlineStyleSingle
296                 | NSUnderlinePatternSolid)]; // | NSUnderlineByWordMask
297         [attributes setObject:value forKey:NSUnderlineStyleAttributeName];
298     }
300     if (flags & DRAW_UNDERC) {
301         // TODO: figure out how do draw proper undercurls
302         NSNumber *value = [NSNumber numberWithInt:(NSUnderlineStyleThick
303                 | NSUnderlinePatternDot)]; // | NSUnderlineByWordMask
304         [attributes setObject:value forKey:NSUnderlineStyleAttributeName];
305     }
307     // Mark these characters as wide.  This attribute is subsequently checked
308     // when translating (row,col) pairs to offsets within 'attribString'.
309     if (flags & DRAW_WIDE)
310         [attributes setObject:[NSNull null]
311                        forKey:MMWideCharacterAttributeName];
313     // Replace characters in text storage and apply new attributes.
314     NSRange r = NSMakeRange(range.location, [string length]);
315     [attribString replaceCharactersInRange:range withString:string];
316     [attribString setAttributes:attributes range:r];
318     unsigned changeInLength = [string length] - range.length;
319     if (acells != cells || acol != col) {
320         if (acells == cells + 1) {
321             // NOTE: A normal width character replaced a double width
322             // character.  To maintain the invariant that each row covers the
323             // same amount of cells, we compensate by adding an empty column.
324             [attribString replaceCharactersInRange:NSMakeRange(NSMaxRange(r),0)
325                 withAttributedString:[emptyRowString
326                     attributedSubstringFromRange:NSMakeRange(0,1)]];
327             ++changeInLength;
328 #if 0
329         } else if (acol == col - 1) {
330             NSLog(@"acol == col - 1");
331             [attribString replaceCharactersInRange:NSMakeRange(r.location,0)
332                 withAttributedString:[emptyRowString
333                     attributedSubstringFromRange:NSMakeRange(0,1)]];
334             ++changeInLength;
335         } else if (acol == col + 1) {
336             NSLog(@"acol == col + 1");
337             [attribString replaceCharactersInRange:NSMakeRange(r.location-1,1)
338                 withAttributedString:[emptyRowString
339                     attributedSubstringFromRange:NSMakeRange(0,2)]];
340             ++changeInLength;
341 #endif
342         } else {
343             // NOTE: It seems that this never gets called.  If it ever does,
344             // then there is another case to treat.
345 #if MM_TS_PARANOIA_LOG
346             NSLog(@"row=%d col=%d acol=%d cells=%d acells=%d", row, col, acol,
347                     cells, acells);
348 #endif
349         }
350     }
352     if ((flags & DRAW_WIDE) || [string length] != cells)
353         characterEqualsColumn = NO;
355     [self fixInvalidCharactersInRange:r];
357     [self edited:(NSTextStorageEditedCharacters|NSTextStorageEditedAttributes)
358            range:range changeInLength:changeInLength];
360 #if MM_USE_ROW_CACHE
361     rowCache[row].length += changeInLength;
362 #endif
366  * Delete 'count' lines from 'row' and insert 'count' empty lines at the bottom
367  * of the scroll region.
368  */
369 - (void)deleteLinesFromRow:(int)row lineCount:(int)count
370               scrollBottom:(int)bottom left:(int)left right:(int)right
371                      color:(NSColor *)color
373     //NSLog(@"deleteLinesFromRow:%d lineCount:%d color:%@", row, count, color);
374     [self lazyResize:NO];
376     if (row < 0 || row+count > maxRows)
377         return;
379     int total = 1 + bottom - row;
380     int move = total - count;
381     int width = right - left + 1;
382     int destRow = row;
383     NSRange destRange, srcRange;
384     int i;
386     for (i = 0; i < move; ++i, ++destRow) {
387         int acol = left;
388         int acells = width;
389         destRange = [self charRangeForRow:destRow column:&acol cells:&acells];
390 #if MM_TS_PARANOIA_LOG
391         if (acells != width || acol != left)
392             NSLog(@"INTERNAL ERROR [%s]", _cmd);
393 #endif
395         acol = left; acells = width;
396         srcRange = [self charRangeForRow:(destRow+count) column:&acol
397                                    cells:&acells];
398 #if MM_TS_PARANOIA_LOG
399         if (acells != width || acol != left)
400             NSLog(@"INTERNAL ERROR [%s]", _cmd);
401 #endif
403         if (NSNotFound == destRange.location || NSNotFound == srcRange.location)
404         {
405 #if MM_TS_PARANOIA_LOG
406             NSLog(@"INTERNAL ERROR [%s] Out of bounds", _cmd);
407 #endif
408             return;
409         }
411         NSAttributedString *srcString = [attribString
412                 attributedSubstringFromRange:srcRange];
414         [attribString replaceCharactersInRange:destRange
415                           withAttributedString:srcString];
416         [self edited:(NSTextStorageEditedCharacters
417                 | NSTextStorageEditedAttributes) range:destRange
418                 changeInLength:([srcString length]-destRange.length)];
420 #if MM_USE_ROW_CACHE
421         rowCache[destRow].length += [srcString length] - destRange.length;
422 #endif
423     }
424     
425     NSRange emptyRange = {0,width};
426     NSAttributedString *emptyString =
427             [emptyRowString attributedSubstringFromRange:emptyRange];
428     NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
429             font, NSFontAttributeName,
430             color, NSBackgroundColorAttributeName, nil];
432     for (i = 0; i < count; ++i, ++destRow) {
433         int acol = left;
434         int acells = width;
435         destRange = [self charRangeForRow:destRow column:&acol cells:&acells];
436 #if MM_TS_PARANOIA_LOG
437         if (acells != width || acol != left)
438             NSLog(@"INTERNAL ERROR [%s]", _cmd);
439 #endif
440         if (NSNotFound == destRange.location) {
441 #if MM_TS_PARANOIA_LOG
442             NSLog(@"INTERNAL ERROR [%s] Out of bounds", _cmd);
443 #endif
444             return;
445         }
447         [attribString replaceCharactersInRange:destRange
448                           withAttributedString:emptyString];
449         [attribString setAttributes:attribs
450                               range:NSMakeRange(destRange.location, width)];
452         [self edited:(NSTextStorageEditedAttributes
453                 | NSTextStorageEditedCharacters) range:destRange
454                 changeInLength:([emptyString length]-destRange.length)];
456 #if MM_USE_ROW_CACHE
457         rowCache[destRow].length += [emptyString length] - destRange.length;
458 #endif
459     }
463  * Insert 'count' empty lines at 'row' and delete 'count' lines from the bottom
464  * of the scroll region.
465  */
466 - (void)insertLinesAtRow:(int)row lineCount:(int)count
467             scrollBottom:(int)bottom left:(int)left right:(int)right
468                    color:(NSColor *)color
470     //NSLog(@"insertLinesAtRow:%d lineCount:%d color:%@", row, count, color);
471     [self lazyResize:NO];
473     if (row < 0 || row+count > maxRows)
474         return;
476     int total = 1 + bottom - row;
477     int move = total - count;
478     int width = right - left + 1;
479     int destRow = bottom;
480     int srcRow = row + move - 1;
481     NSRange destRange, srcRange;
482     int i;
484     for (i = 0; i < move; ++i, --destRow, --srcRow) {
485         int acol = left;
486         int acells = width;
487         destRange = [self charRangeForRow:destRow column:&acol cells:&acells];
488 #if MM_TS_PARANOIA_LOG
489         if (acells != width || acol != left)
490             NSLog(@"INTERNAL ERROR [%s]", _cmd);
491 #endif
493         acol = left; acells = width;
494         srcRange = [self charRangeForRow:srcRow column:&acol cells:&acells];
495 #if MM_TS_PARANOIA_LOG
496         if (acells != width || acol != left)
497             NSLog(@"INTERNAL ERROR [%s]", _cmd);
498 #endif
499         if (NSNotFound == destRange.location || NSNotFound == srcRange.location)
500         {
501 #if MM_TS_PARANOIA_LOG
502             NSLog(@"INTERNAL ERROR [%s] Out of bounds", _cmd);
503 #endif
504             return;
505         }
507         NSAttributedString *srcString = [attribString
508                 attributedSubstringFromRange:srcRange];
509         [attribString replaceCharactersInRange:destRange
510                           withAttributedString:srcString];
511         [self edited:(NSTextStorageEditedCharacters
512                 | NSTextStorageEditedAttributes) range:destRange
513                 changeInLength:([srcString length]-destRange.length)];
515 #if MM_USE_ROW_CACHE
516         rowCache[destRow].length += [srcString length] - destRange.length;
517 #endif
518     }
519     
520     NSRange emptyRange = {0,width};
521     NSAttributedString *emptyString =
522             [emptyRowString attributedSubstringFromRange:emptyRange];
523     NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
524             font, NSFontAttributeName,
525             color, NSBackgroundColorAttributeName, nil];
526     
527     for (i = 0; i < count; ++i, --destRow) {
528         int acol = left;
529         int acells = width;
530         destRange = [self charRangeForRow:destRow column:&acol cells:&acells];
531 #if MM_TS_PARANOIA_LOG
532         if (acells != width || acol != left)
533             NSLog(@"INTERNAL ERROR [%s]", _cmd);
534 #endif
535         if (NSNotFound == destRange.location) {
536 #if MM_TS_PARANOIA_LOG
537             NSLog(@"INTERNAL ERROR [%s] Out of bounds", _cmd);
538 #endif
539             return;
540         }
542         [attribString replaceCharactersInRange:destRange
543                           withAttributedString:emptyString];
544         [attribString setAttributes:attribs
545                               range:NSMakeRange(destRange.location, width)];
547         [self edited:(NSTextStorageEditedAttributes
548                 | NSTextStorageEditedCharacters) range:destRange
549                 changeInLength:([emptyString length]-destRange.length)];
551 #if MM_USE_ROW_CACHE
552         rowCache[destRow].length += [emptyString length] - destRange.length;
553 #endif
554     }
557 - (void)clearBlockFromRow:(int)row1 column:(int)col1 toRow:(int)row2
558                    column:(int)col2 color:(NSColor *)color
560     //NSLog(@"clearBlockFromRow:%d column:%d toRow:%d column:%d color:%@",
561     //        row1, col1, row2, col2, color);
562     [self lazyResize:NO];
564     if (row1 < 0 || row2 >= maxRows || col1 < 0 || col2 > maxColumns)
565         return;
567     NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
568             font, NSFontAttributeName,
569             color, NSBackgroundColorAttributeName, nil];
570     int cells = col2 - col1 + 1;
571     NSRange range, emptyRange = {0, cells};
572     NSAttributedString *emptyString =
573             [emptyRowString attributedSubstringFromRange:emptyRange];
574     int r;
576     for (r=row1; r<=row2; ++r) {
577         int acol = col1;
578         int acells = cells;
579         range = [self charRangeForRow:r column:&acol cells:&acells];
580 #if MM_TS_PARANOIA_LOG
581         if (acells != cells || acol != col1)
582             NSLog(@"INTERNAL ERROR [%s]", _cmd);
583 #endif
584         if (NSNotFound == range.location) {
585 #if MM_TS_PARANOIA_LOG
586             NSLog(@"INTERNAL ERROR [%s] Out of bounds", _cmd);
587 #endif
588             return;
589         }
591         [attribString replaceCharactersInRange:range
592                           withAttributedString:emptyString];
593         [attribString setAttributes:attribs
594                               range:NSMakeRange(range.location, cells)];
596         [self edited:(NSTextStorageEditedAttributes
597                 | NSTextStorageEditedCharacters) range:range
598                                         changeInLength:cells-range.length];
600 #if MM_USE_ROW_CACHE
601         rowCache[r].length += cells - range.length;
602 #endif
603     }
606 - (void)clearAll
608     //NSLog(@"%s", _cmd);
609     [self lazyResize:YES];
612 - (void)setDefaultColorsBackground:(NSColor *)bgColor
613                         foreground:(NSColor *)fgColor
615     if (defaultBackgroundColor != bgColor) {
616         [defaultBackgroundColor release];
617         defaultBackgroundColor = bgColor ? [bgColor retain] : nil;
618     }
620     // NOTE: The default foreground color isn't actually used for anything, but
621     // other class instances might want to be able to access it so it is stored
622     // here.
623     if (defaultForegroundColor != fgColor) {
624         [defaultForegroundColor release];
625         defaultForegroundColor = fgColor ? [fgColor retain] : nil;
626     }
629 - (void)setFont:(NSFont*)newFont
631     if (newFont && font != newFont) {
632         [boldItalicFont release];
633         [italicFont release];
634         [boldFont release];
635         [font release];
637         // NOTE! When setting a new font we make sure that the advancement of
638         // each glyph is fixed.
640         float em = [newFont widthOfString:@"m"];
641         float cellWidthMultiplier = [[NSUserDefaults standardUserDefaults]
642                 floatForKey:MMCellWidthMultiplierKey];
644         // NOTE! Even though NSFontFixedAdvanceAttribute is a float, it will
645         // only render at integer sizes.  Hence, we restrict the cell width to
646         // an integer here, otherwise the window width and the actual text
647         // width will not match.
648         cellSize.width = ceilf(em * cellWidthMultiplier);
650         float pointSize = [newFont pointSize];
651         NSDictionary *dict = [NSDictionary
652             dictionaryWithObject:[NSNumber numberWithFloat:cellSize.width]
653                           forKey:NSFontFixedAdvanceAttribute];
655         NSFontDescriptor *desc = [newFont fontDescriptor];
656         desc = [desc fontDescriptorByAddingAttributes:dict];
657         font = [NSFont fontWithDescriptor:desc size:pointSize];
658         [font retain];
660         NSLayoutManager *lm = [[self layoutManagers] objectAtIndex:0];
661         cellSize.height = linespace + (lm ? [lm defaultLineHeightForFont:font]
662                                           : [font defaultLineHeightForFont]);
664         // NOTE: The font manager does not care about the 'font fixed advance'
665         // attribute, so after converting the font we have to add this
666         // attribute again.
667         boldFont = [[NSFontManager sharedFontManager]
668             convertFont:font toHaveTrait:NSBoldFontMask];
669         desc = [boldFont fontDescriptor];
670         desc = [desc fontDescriptorByAddingAttributes:dict];
671         boldFont = [NSFont fontWithDescriptor:desc size:pointSize];
672         [boldFont retain];
674         italicFont = [[NSFontManager sharedFontManager]
675             convertFont:font toHaveTrait:NSItalicFontMask];
676         desc = [italicFont fontDescriptor];
677         desc = [desc fontDescriptorByAddingAttributes:dict];
678         italicFont = [NSFont fontWithDescriptor:desc size:pointSize];
679         [italicFont retain];
681         boldItalicFont = [[NSFontManager sharedFontManager]
682             convertFont:italicFont toHaveTrait:NSBoldFontMask];
683         desc = [boldItalicFont fontDescriptor];
684         desc = [desc fontDescriptorByAddingAttributes:dict];
685         boldItalicFont = [NSFont fontWithDescriptor:desc size:pointSize];
686         [boldItalicFont retain];
687     }
690 - (void)setWideFont:(NSFont *)newFont
692     if (!newFont) {
693         // Use the normal font as the wide font (note that the normal font may
694         // very well include wide characters.)
695         if (font) [self setWideFont:font];
696     } else if (newFont != fontWide) {
697         [boldItalicFontWide release];
698         [italicFontWide release];
699         [boldFontWide release];
700         [fontWide release];
702         float pointSize = [newFont pointSize];
703         NSFontDescriptor *desc = [newFont fontDescriptor];
704         NSDictionary *dictWide = [NSDictionary
705             dictionaryWithObject:[NSNumber numberWithFloat:2*cellSize.width]
706                           forKey:NSFontFixedAdvanceAttribute];
708         desc = [desc fontDescriptorByAddingAttributes:dictWide];
709         fontWide = [NSFont fontWithDescriptor:desc size:pointSize];
710         [fontWide retain];
712         boldFontWide = [[NSFontManager sharedFontManager]
713             convertFont:fontWide toHaveTrait:NSBoldFontMask];
714         desc = [boldFontWide fontDescriptor];
715         desc = [desc fontDescriptorByAddingAttributes:dictWide];
716         boldFontWide = [NSFont fontWithDescriptor:desc size:pointSize];
717         [boldFontWide retain];
719         italicFontWide = [[NSFontManager sharedFontManager]
720             convertFont:fontWide toHaveTrait:NSItalicFontMask];
721         desc = [italicFontWide fontDescriptor];
722         desc = [desc fontDescriptorByAddingAttributes:dictWide];
723         italicFontWide = [NSFont fontWithDescriptor:desc size:pointSize];
724         [italicFontWide retain];
726         boldItalicFontWide = [[NSFontManager sharedFontManager]
727             convertFont:italicFontWide toHaveTrait:NSBoldFontMask];
728         desc = [boldItalicFontWide fontDescriptor];
729         desc = [desc fontDescriptorByAddingAttributes:dictWide];
730         boldItalicFontWide = [NSFont fontWithDescriptor:desc size:pointSize];
731         [boldItalicFontWide retain];
732     }
735 - (NSFont*)font
737     return font;
740 - (NSColor *)defaultBackgroundColor
742     return defaultBackgroundColor;
745 - (NSColor *)defaultForegroundColor
747     return defaultForegroundColor;
750 - (NSSize)size
752     return NSMakeSize(maxColumns*cellSize.width, maxRows*cellSize.height);
755 - (NSSize)cellSize
757     return cellSize;
760 - (NSRect)rectForRowsInRange:(NSRange)range
762     NSRect rect = { 0, 0, 0, 0 };
763     unsigned start = range.location > maxRows ? maxRows : range.location;
764     unsigned length = range.length;
766     if (start+length > maxRows)
767         length = maxRows - start;
769     rect.origin.y = cellSize.height * start;
770     rect.size.height = cellSize.height * length;
772     return rect;
775 - (NSRect)rectForColumnsInRange:(NSRange)range
777     NSRect rect = { 0, 0, 0, 0 };
778     unsigned start = range.location > maxColumns ? maxColumns : range.location;
779     unsigned length = range.length;
781     if (start+length > maxColumns)
782         length = maxColumns - start;
784     rect.origin.x = cellSize.width * start;
785     rect.size.width = cellSize.width * length;
787     return rect;
790 - (unsigned)characterIndexForRow:(int)row column:(int)col
792     int cells = 1;
793     NSRange range = [self charRangeForRow:row column:&col cells:&cells];
794     return range.location != NSNotFound ? range.location : 0;
797 // XXX: unused at the moment
798 - (BOOL)resizeToFitSize:(NSSize)size
800     int rows = maxRows, cols = maxColumns;
802     [self fitToSize:size rows:&rows columns:&cols];
803     if (rows != maxRows || cols != maxColumns) {
804         [self setMaxRows:rows columns:cols];
805         return YES;
806     }
808     // Return NO only if dimensions did not change.
809     return NO;
812 - (NSSize)fitToSize:(NSSize)size
814     return [self fitToSize:size rows:NULL columns:NULL];
817 - (NSSize)fitToSize:(NSSize)size rows:(int *)rows columns:(int *)columns
819     NSSize curSize = [self size];
820     NSSize fitSize = curSize;
821     int fitRows = maxRows;
822     int fitCols = maxColumns;
824     if (size.height < curSize.height) {
825         // Remove lines until the height of the text storage fits inside
826         // 'size'.  However, always make sure there are at least 3 lines in the
827         // text storage.  (Why 3? It seem Vim never allows less than 3 lines.)
828         //
829         // TODO: No need to search since line height is fixed, just calculate
830         // the new height.
831         int rowCount = maxRows;
832         int rowsToRemove;
833         for (rowsToRemove = 0; rowsToRemove < maxRows-3; ++rowsToRemove) {
834             float height = cellSize.height*rowCount;
836             if (height <= size.height) {
837                 fitSize.height = height;
838                 break;
839             }
841             --rowCount;
842         }
844         fitRows -= rowsToRemove;
845     } else if (size.height > curSize.height) {
846         float fh = cellSize.height;
847         if (fh < 1.0f) fh = 1.0f;
849         fitRows = floor(size.height/fh);
850         fitSize.height = fh*fitRows;
851     }
853     if (size.width != curSize.width) {
854         float fw = cellSize.width;
855         if (fw < 1.0f) fw = 1.0f;
857         fitCols = floor(size.width/fw);
858         fitSize.width = fw*fitCols;
859     }
861     if (rows) *rows = fitRows;
862     if (columns) *columns = fitCols;
864     return fitSize;
867 - (NSRect)boundingRectForCharacterAtRow:(int)row column:(int)col
869 #if 1
870     // This properly computes the position of where Vim expects the glyph to be
871     // drawn.  Had the typesetter actually computed the right position of each
872     // character and not hidden some, this code would be correct.
873     NSRect rect = NSZeroRect;
875     rect.origin.x = col*cellSize.width;
876     rect.origin.y = row*cellSize.height;
877     rect.size = cellSize;
879     // Wide character take up twice the width of a normal character.
880     int cells = 1;
881     NSRange r = [self charRangeForRow:row column:&col cells:&cells];
882     if (NSNotFound != r.location
883             && [attribString attribute:MMWideCharacterAttributeName
884                                atIndex:r.location
885                         effectiveRange:nil])
886         rect.size.width += rect.size.width;
888     return rect;
889 #else
890     // Use layout manager to compute bounding rect.  This works in situations
891     // where the layout manager decides to hide glyphs (Vim assumes all glyphs
892     // are drawn).
893     NSLayoutManager *lm = [[self layoutManagers] objectAtIndex:0];
894     NSTextContainer *tc = [[lm textContainers] objectAtIndex:0];
895     int cells = 1;
896     NSRange range = [self charRangeForRow:row column:&col cells:&cells];
897     NSRange glyphRange = [lm glyphRangeForCharacterRange:range
898                                     actualCharacterRange:NULL];
900     return [lm boundingRectForGlyphRange:glyphRange inTextContainer:tc];
901 #endif
904 #if MM_USE_ROW_CACHE
905 - (MMRowCacheEntry *)rowCache
907     return rowCache;
909 #endif
911 @end // MMTextStorage
916 @implementation MMTextStorage (Private)
918 - (void)lazyResize:(BOOL)force
920     // Do nothing if the dimensions are already right.
921     if (!force && actualRows == maxRows && actualColumns == maxColumns)
922         return;
924     NSRange oldRange = NSMakeRange(0, [attribString length]);
926     actualRows = maxRows;
927     actualColumns = maxColumns;
928     characterEqualsColumn = YES;
930 #if MM_USE_ROW_CACHE
931     free(rowCache);
932     rowCache = (MMRowCacheEntry*)calloc(actualRows, sizeof(MMRowCacheEntry));
933 #endif
935     NSDictionary *dict;
936     if (defaultBackgroundColor) {
937         dict = [NSDictionary dictionaryWithObjectsAndKeys:
938                 font, NSFontAttributeName,
939                 defaultBackgroundColor, NSBackgroundColorAttributeName, nil];
940     } else {
941         dict = [NSDictionary dictionaryWithObjectsAndKeys:
942                 font, NSFontAttributeName, nil];
943     }
944             
945     NSMutableString *rowString = [NSMutableString string];
946     int i;
947     for (i = 0; i < maxColumns; ++i) {
948         [rowString appendString:@" "];
949     }
950     [rowString appendString:@"\n"];
952     [emptyRowString release];
953     emptyRowString = [[NSAttributedString alloc] initWithString:rowString
954                                                      attributes:dict];
956     [attribString release];
957     attribString = [[NSMutableAttributedString alloc] init];
958     for (i=0; i<maxRows; ++i) {
959 #if MM_USE_ROW_CACHE
960         rowCache[i].length = actualColumns + 1;
961 #endif
962         [attribString appendAttributedString:emptyRowString];
963     }
965     NSRange fullRange = NSMakeRange(0, [attribString length]);
966     [self edited:(NSTextStorageEditedCharacters|NSTextStorageEditedAttributes)
967            range:oldRange changeInLength:fullRange.length-oldRange.length];
970 - (NSRange)charRangeForRow:(int)row column:(int*)pcol cells:(int*)pcells
972     int col = *pcol;
973     int cells = *pcells;
975     // If no wide chars are used and if every char has length 1 (no composing
976     // characters, no > 16 bit characters), then we can compute the range.
977     if (characterEqualsColumn)
978         return NSMakeRange(row*(actualColumns+1) + col, cells);
980     NSString *string = [attribString string];
981     NSRange r, range = { NSNotFound, 0 };
982     unsigned idx, rowEnd;
983     int i;
985     if (row < 0 || row >= actualRows || col < 0 || col >= actualColumns
986             || col+cells > actualColumns) {
987 #if MM_TS_PARANOIA_LOG
988         NSLog(@"%s row=%d col=%d cells=%d is out of range (length=%d)",
989                 _cmd, row, col, cells, [string length]);
990 #endif
991         return range;
992     }
994 #if MM_USE_ROW_CACHE
995     // Locate the beginning of the row
996     MMRowCacheEntry *cache = rowCache;
997     idx = 0;
998     for (i = 0; i < row; ++i, ++cache)
999         idx += cache->length;
1001     rowEnd = idx + cache->length;
1002 #else
1003     // Locate the beginning of the row by scanning for EOL characters.
1004     r.location = 0;
1005     for (i = 0; i < row; ++i) {
1006         r.length = [string length] - r.location;
1007         r = [string rangeOfString:@"\n" options:NSLiteralSearch range:r];
1008         if (NSNotFound == r.location)
1009             return range;
1010         ++r.location;
1011     }
1012 #endif
1014     // Locate the column
1015 #if MM_USE_ROW_CACHE
1016     cache = &rowCache[row];
1018     i = cache->col;
1019     if (col == i) {
1020         // Cache hit
1021         idx += cache->colOffset;
1022     } else {
1023         range.location = idx;
1025 #if 0  // Backward search seems to be broken...
1026         // Cache miss
1027         if (col < i - col) {
1028             // Search forward from beginning of line.
1029             i = 0;
1030         } else if (actualColumns - col < col - i) {
1031             // Search backward from end of line.
1032             i = actualColumns - 1;
1033             idx += cache->length - 2;
1034         } else {
1035             // Search from cache spot (forward or backward).
1036             idx += cache->colOffset;
1037         }
1039         if (col > i) {
1040             // Forward search
1041             while (col > i) {
1042                 r = [string rangeOfComposedCharacterSequenceAtIndex:idx];
1044                 // Wide chars take up two display cells.
1045                 if ([attribString attribute:MMWideCharacterAttributeName
1046                                     atIndex:idx
1047                              effectiveRange:nil])
1048                     ++i;
1050                 idx += r.length;
1051                 ++i;
1052             }
1053         } else if (col < i) {
1054             // Backward search
1055             while (col < i) {
1056                 r = [string rangeOfComposedCharacterSequenceAtIndex:idx-1];
1057                 idx -= r.length;
1058                 --i;
1060                 // Wide chars take up two display cells.
1061                 if ([attribString attribute:MMWideCharacterAttributeName
1062                                     atIndex:idx
1063                              effectiveRange:nil])
1064                     --i;
1065             }
1066         }
1068         *pcol = i;
1069         cache->col = i;
1070         cache->colOffset = idx - range.location;
1071 #else
1072         // Cache miss
1073         if (col < i) {
1074             // Search forward from beginning of line.
1075             i = 0;
1076         } else {
1077             // Search forward from cache spot.
1078             idx += cache->colOffset;
1079         }
1081         // Forward search
1082         while (col > i) {
1083             r = [string rangeOfComposedCharacterSequenceAtIndex:idx];
1085             // Wide chars take up two display cells.
1086             if ([attribString attribute:MMWideCharacterAttributeName
1087                                 atIndex:idx
1088                          effectiveRange:nil])
1089                 ++i;
1091             idx += r.length;
1092             ++i;
1093         }
1095         *pcol = i;
1096         cache->col = i;
1097         cache->colOffset = idx - range.location;
1098 #endif
1099     }
1100 #else
1101     idx = r.location;
1102     for (i = 0; i < col; ++i) {
1103         r = [string rangeOfComposedCharacterSequenceAtIndex:idx];
1105         // Wide chars take up two display cells.
1106         if ([attribString attribute:MMWideCharacterAttributeName
1107                             atIndex:idx
1108                      effectiveRange:nil])
1109             ++i;
1111         idx += r.length;
1112     }
1113 #endif
1115     // Count the number of characters that cover the cells.
1116     range.location = idx;
1117     for (i = 0; i < cells; ++i) {
1118         r = [string rangeOfComposedCharacterSequenceAtIndex:idx];
1120         // Wide chars take up two display cells.
1121         if ([attribString attribute:MMWideCharacterAttributeName
1122                             atIndex:idx
1123                      effectiveRange:nil])
1124             ++i;
1126         idx += r.length;
1127         range.length += r.length;
1128     }
1130     *pcells = i;
1132 #if MM_TS_PARANOIA_LOG
1133 #if MM_USE_ROW_CACHE
1134     if (range.location >= rowEnd-1) {
1135         NSLog(@"INTERNAL ERROR [%s] : row=%d col=%d cells=%d --> range=%@",
1136                 _cmd, row, col, cells, NSStringFromRange(range));
1137         range.location = rowEnd - 2;
1138         range.length = 1;
1139     } else if (NSMaxRange(range) >= rowEnd) {
1140         NSLog(@"INTERNAL ERROR [%s] : row=%d col=%d cells=%d --> range=%@",
1141                 _cmd, row, col, cells, NSStringFromRange(range));
1142         range.length = rowEnd - range.location - 1;
1143     }
1144 #endif
1146     if (NSMaxRange(range) > [string length]) {
1147         NSLog(@"INTERNAL ERROR [%s] : row=%d col=%d cells=%d --> range=%@",
1148                 _cmd, row, col, cells, NSStringFromRange(range));
1149         range.location = NSNotFound;
1150         range.length = 0;
1151     }
1152 #endif
1154     return range;
1157 - (void)fixInvalidCharactersInRange:(NSRange)range
1159     static NSCharacterSet *invalidCharacterSet = nil;
1160     NSRange invalidRange;
1161     unsigned end;
1163     if (!invalidCharacterSet)
1164         invalidCharacterSet = [[NSCharacterSet characterSetWithRange:
1165             NSMakeRange(0x2028, 2)] retain];
1167     // HACK! Replace characters that the text system can't handle (currently
1168     // LINE SEPARATOR U+2028 and PARAGRAPH SEPARATOR U+2029) with space.
1169     //
1170     // TODO: Treat these separately inside of Vim so we don't have to bother
1171     // here.
1172     while (range.length > 0) {
1173         invalidRange = [[attribString string]
1174             rangeOfCharacterFromSet:invalidCharacterSet
1175                             options:NSLiteralSearch
1176                               range:range];
1177         if (NSNotFound == invalidRange.location)
1178             break;
1180         [attribString replaceCharactersInRange:invalidRange withString:@" "];
1182         end = NSMaxRange(invalidRange);
1183         range.length -= end - range.location;
1184         range.location = end;
1185     }
1188 @end // MMTextStorage (Private)