Ensure valid scroll region when deleting/inserting rows
[MacVim.git] / src / MacVim / MMTextStorage.m
blobcb018e1d753dc2379c9d226fa2785f489c014731
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 = [font pointSize];
79         cellSize.width = [font defaultLineHeightForFont];
80     }
82     return self;
85 - (void)dealloc
87     //NSLog(@"MMTextStorage 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];
223     linespace = newLinespace;
225     // NOTE: The linespace is added to the cell height in order for a multiline
226     // selection not to have white (background color) gaps between lines.  Also
227     // this simplifies the code a lot because there is no need to check the
228     // linespace when calculating the size of the text view etc.  When the
229     // linespace is non-zero the baseline will be adjusted as well; check
230     // MMTypesetter.
231     cellSize.height = linespace + (lm ? [lm defaultLineHeightForFont:font]
232                                       : [font defaultLineHeightForFont]);
235 - (void)getMaxRows:(int*)rows columns:(int*)cols
237     if (rows) *rows = maxRows;
238     if (cols) *cols = maxColumns;
241 - (void)setMaxRows:(int)rows columns:(int)cols
243     // NOTE: Just remember the new values, the actual resizing is done lazily.
244     maxRows = rows;
245     maxColumns = cols;
248 - (void)drawString:(NSString *)string atRow:(int)row column:(int)col
249              cells:(int)cells withFlags:(int)flags
250    foregroundColor:(NSColor *)fg backgroundColor:(NSColor *)bg
251       specialColor:(NSColor *)sp
253     //NSLog(@"replaceString:atRow:%d column:%d withFlags:%d "
254     //          "foreground:%@ background:%@ special:%@",
255     //          row, col, flags, fg, bg, sp);
256     [self lazyResize:NO];
258     if (row < 0 || row >= maxRows || col < 0 || col >= maxColumns
259             || col+cells > maxColumns || !string || !(fg && bg && sp))
260         return;
262     BOOL hasControlChars = [string rangeOfCharacterFromSet:
263             [NSCharacterSet controlCharacterSet]].location != NSNotFound;
264     if (hasControlChars) {
265         // HACK! If a string for some reason contains control characters, then
266         // draw blanks instead (otherwise charRangeForRow::: fails).
267         NSRange subRange = { 0, cells };
268         flags &= ~DRAW_WIDE;
269         string = [[emptyRowString string] substringWithRange:subRange];
270     }
272     // Find range of characters in text storage to replace.
273     int acol = col;
274     int acells = cells;
275     NSRange range = [self charRangeForRow:row column:&acol cells:&acells];
276     if (NSNotFound == range.location) {
277 #if MM_TS_PARANOIA_LOG
278         NSLog(@"INTERNAL ERROR [%s] Out of bounds", _cmd);
279 #endif
280         return;
281     }
283     // Create dictionary of attributes to apply to the new characters.
284     NSFont *theFont = font;
285     if (flags & DRAW_WIDE) {
286         if (flags & DRAW_BOLD)
287             theFont = flags & DRAW_ITALIC ? boldItalicFontWide : boldFontWide;
288         else if (flags & DRAW_ITALIC)
289             theFont = italicFontWide;
290         else
291             theFont = fontWide;
292     } else {
293         if (flags & DRAW_BOLD)
294             theFont = flags & DRAW_ITALIC ? boldItalicFont : boldFont;
295         else if (flags & DRAW_ITALIC)
296             theFont = italicFont;
297     }
299     NSMutableDictionary *attributes =
300         [NSMutableDictionary dictionaryWithObjectsAndKeys:
301             theFont, NSFontAttributeName,
302             bg, NSBackgroundColorAttributeName,
303             fg, NSForegroundColorAttributeName,
304             sp, NSUnderlineColorAttributeName,
305             [NSNumber numberWithInt:0], NSLigatureAttributeName,
306             nil];
308     if (flags & DRAW_UNDERL) {
309         NSNumber *value = [NSNumber numberWithInt:(NSUnderlineStyleSingle
310                 | NSUnderlinePatternSolid)]; // | NSUnderlineByWordMask
311         [attributes setObject:value forKey:NSUnderlineStyleAttributeName];
312     }
314     if (flags & DRAW_UNDERC) {
315         // TODO: figure out how do draw proper undercurls
316         NSNumber *value = [NSNumber numberWithInt:(NSUnderlineStyleThick
317                 | NSUnderlinePatternDot)]; // | NSUnderlineByWordMask
318         [attributes setObject:value forKey:NSUnderlineStyleAttributeName];
319     }
321     // Mark these characters as wide.  This attribute is subsequently checked
322     // when translating (row,col) pairs to offsets within 'attribString'.
323     if (flags & DRAW_WIDE)
324         [attributes setObject:[NSNull null]
325                        forKey:MMWideCharacterAttributeName];
327     // Replace characters in text storage and apply new attributes.
328     NSRange r = NSMakeRange(range.location, [string length]);
329     [attribString replaceCharactersInRange:range withString:string];
330     [attribString setAttributes:attributes range:r];
332     unsigned changeInLength = [string length] - range.length;
333     if (acells != cells || acol != col) {
334         if (acells == cells + 1) {
335             // NOTE: A normal width character replaced a double width
336             // character.  To maintain the invariant that each row covers the
337             // same amount of cells, we compensate by adding an empty column.
338             [attribString replaceCharactersInRange:NSMakeRange(NSMaxRange(r),0)
339                 withAttributedString:[emptyRowString
340                     attributedSubstringFromRange:NSMakeRange(0,1)]];
341             ++changeInLength;
342 #if 0
343         } else if (acol == col - 1) {
344             NSLog(@"acol == col - 1");
345             [attribString replaceCharactersInRange:NSMakeRange(r.location,0)
346                 withAttributedString:[emptyRowString
347                     attributedSubstringFromRange:NSMakeRange(0,1)]];
348             ++changeInLength;
349         } else if (acol == col + 1) {
350             NSLog(@"acol == col + 1");
351             [attribString replaceCharactersInRange:NSMakeRange(r.location-1,1)
352                 withAttributedString:[emptyRowString
353                     attributedSubstringFromRange:NSMakeRange(0,2)]];
354             ++changeInLength;
355 #endif
356         } else {
357             // NOTE: It seems that this never gets called.  If it ever does,
358             // then there is another case to treat.
359 #if MM_TS_PARANOIA_LOG
360             NSLog(@"row=%d col=%d acol=%d cells=%d acells=%d", row, col, acol,
361                     cells, acells);
362 #endif
363         }
364     }
366     if ((flags & DRAW_WIDE) || [string length] != cells)
367         characterEqualsColumn = NO;
369     [self fixInvalidCharactersInRange:r];
371     [self edited:(NSTextStorageEditedCharacters|NSTextStorageEditedAttributes)
372            range:range changeInLength:changeInLength];
374 #if MM_USE_ROW_CACHE
375     rowCache[row].length += changeInLength;
376 #endif
380  * Delete 'count' lines from 'row' and insert 'count' empty lines at the bottom
381  * of the scroll region.
382  */
383 - (void)deleteLinesFromRow:(int)row lineCount:(int)count
384               scrollBottom:(int)bottom left:(int)left right:(int)right
385                      color:(NSColor *)color
387     //NSLog(@"deleteLinesFromRow:%d lineCount:%d color:%@", row, count, color);
388     [self lazyResize:NO];
390     if (row < 0 || row+count > maxRows || bottom > maxRows || left < 0
391             || right > maxColumns)
392         return;
394     int total = 1 + bottom - row;
395     int move = total - count;
396     int width = right - left + 1;
397     int destRow = row;
398     NSRange destRange, srcRange;
399     int i;
401     for (i = 0; i < move; ++i, ++destRow) {
402         int acol = left;
403         int acells = width;
404         destRange = [self charRangeForRow:destRow column:&acol cells:&acells];
405 #if MM_TS_PARANOIA_LOG
406         if (acells != width || acol != left)
407             NSLog(@"INTERNAL ERROR [%s]", _cmd);
408 #endif
410         acol = left; acells = width;
411         srcRange = [self charRangeForRow:(destRow+count) column:&acol
412                                    cells:&acells];
413 #if MM_TS_PARANOIA_LOG
414         if (acells != width || acol != left)
415             NSLog(@"INTERNAL ERROR [%s]", _cmd);
416 #endif
418         if (NSNotFound == destRange.location || NSNotFound == srcRange.location)
419         {
420 #if MM_TS_PARANOIA_LOG
421             NSLog(@"INTERNAL ERROR [%s] Out of bounds", _cmd);
422 #endif
423             return;
424         }
426         NSAttributedString *srcString = [attribString
427                 attributedSubstringFromRange:srcRange];
429         [attribString replaceCharactersInRange:destRange
430                           withAttributedString:srcString];
431         [self edited:(NSTextStorageEditedCharacters
432                 | NSTextStorageEditedAttributes) range:destRange
433                 changeInLength:([srcString length]-destRange.length)];
435 #if MM_USE_ROW_CACHE
436         rowCache[destRow].length += [srcString length] - destRange.length;
437 #endif
438     }
439     
440     NSRange emptyRange = {0,width};
441     NSAttributedString *emptyString =
442             [emptyRowString attributedSubstringFromRange:emptyRange];
443     NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
444             font, NSFontAttributeName,
445             color, NSBackgroundColorAttributeName, nil];
447     for (i = 0; i < count; ++i, ++destRow) {
448         int acol = left;
449         int acells = width;
450         destRange = [self charRangeForRow:destRow column:&acol cells:&acells];
451 #if MM_TS_PARANOIA_LOG
452         if (acells != width || acol != left)
453             NSLog(@"INTERNAL ERROR [%s]", _cmd);
454 #endif
455         if (NSNotFound == destRange.location) {
456 #if MM_TS_PARANOIA_LOG
457             NSLog(@"INTERNAL ERROR [%s] Out of bounds", _cmd);
458 #endif
459             return;
460         }
462         [attribString replaceCharactersInRange:destRange
463                           withAttributedString:emptyString];
464         [attribString setAttributes:attribs
465                               range:NSMakeRange(destRange.location, width)];
467         [self edited:(NSTextStorageEditedAttributes
468                 | NSTextStorageEditedCharacters) range:destRange
469                 changeInLength:([emptyString length]-destRange.length)];
471 #if MM_USE_ROW_CACHE
472         rowCache[destRow].length += [emptyString length] - destRange.length;
473 #endif
474     }
478  * Insert 'count' empty lines at 'row' and delete 'count' lines from the bottom
479  * of the scroll region.
480  */
481 - (void)insertLinesAtRow:(int)row lineCount:(int)count
482             scrollBottom:(int)bottom left:(int)left right:(int)right
483                    color:(NSColor *)color
485     //NSLog(@"insertLinesAtRow:%d lineCount:%d color:%@", row, count, color);
486     [self lazyResize:NO];
488     if (row < 0 || row+count > maxRows || bottom > maxRows || left < 0
489             || right > maxColumns)
490         return;
492     int total = 1 + bottom - row;
493     int move = total - count;
494     int width = right - left + 1;
495     int destRow = bottom;
496     int srcRow = row + move - 1;
497     NSRange destRange, srcRange;
498     int i;
500     for (i = 0; i < move; ++i, --destRow, --srcRow) {
501         int acol = left;
502         int acells = width;
503         destRange = [self charRangeForRow:destRow column:&acol cells:&acells];
504 #if MM_TS_PARANOIA_LOG
505         if (acells != width || acol != left)
506             NSLog(@"INTERNAL ERROR [%s]", _cmd);
507 #endif
509         acol = left; acells = width;
510         srcRange = [self charRangeForRow:srcRow column:&acol cells:&acells];
511 #if MM_TS_PARANOIA_LOG
512         if (acells != width || acol != left)
513             NSLog(@"INTERNAL ERROR [%s]", _cmd);
514 #endif
515         if (NSNotFound == destRange.location || NSNotFound == srcRange.location)
516         {
517 #if MM_TS_PARANOIA_LOG
518             NSLog(@"INTERNAL ERROR [%s] Out of bounds", _cmd);
519 #endif
520             return;
521         }
523         NSAttributedString *srcString = [attribString
524                 attributedSubstringFromRange:srcRange];
525         [attribString replaceCharactersInRange:destRange
526                           withAttributedString:srcString];
527         [self edited:(NSTextStorageEditedCharacters
528                 | NSTextStorageEditedAttributes) range:destRange
529                 changeInLength:([srcString length]-destRange.length)];
531 #if MM_USE_ROW_CACHE
532         rowCache[destRow].length += [srcString length] - destRange.length;
533 #endif
534     }
535     
536     NSRange emptyRange = {0,width};
537     NSAttributedString *emptyString =
538             [emptyRowString attributedSubstringFromRange:emptyRange];
539     NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
540             font, NSFontAttributeName,
541             color, NSBackgroundColorAttributeName, nil];
542     
543     for (i = 0; i < count; ++i, --destRow) {
544         int acol = left;
545         int acells = width;
546         destRange = [self charRangeForRow:destRow column:&acol cells:&acells];
547 #if MM_TS_PARANOIA_LOG
548         if (acells != width || acol != left)
549             NSLog(@"INTERNAL ERROR [%s]", _cmd);
550 #endif
551         if (NSNotFound == destRange.location) {
552 #if MM_TS_PARANOIA_LOG
553             NSLog(@"INTERNAL ERROR [%s] Out of bounds", _cmd);
554 #endif
555             return;
556         }
558         [attribString replaceCharactersInRange:destRange
559                           withAttributedString:emptyString];
560         [attribString setAttributes:attribs
561                               range:NSMakeRange(destRange.location, width)];
563         [self edited:(NSTextStorageEditedAttributes
564                 | NSTextStorageEditedCharacters) range:destRange
565                 changeInLength:([emptyString length]-destRange.length)];
567 #if MM_USE_ROW_CACHE
568         rowCache[destRow].length += [emptyString length] - destRange.length;
569 #endif
570     }
573 - (void)clearBlockFromRow:(int)row1 column:(int)col1 toRow:(int)row2
574                    column:(int)col2 color:(NSColor *)color
576     //NSLog(@"clearBlockFromRow:%d column:%d toRow:%d column:%d color:%@",
577     //        row1, col1, row2, col2, color);
578     [self lazyResize:NO];
580     if (row1 < 0 || row2 >= maxRows || col1 < 0 || col2 > maxColumns)
581         return;
583     NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
584             font, NSFontAttributeName,
585             color, NSBackgroundColorAttributeName, nil];
586     int cells = col2 - col1 + 1;
587     NSRange range, emptyRange = {0, cells};
588     NSAttributedString *emptyString =
589             [emptyRowString attributedSubstringFromRange:emptyRange];
590     int r;
592     for (r=row1; r<=row2; ++r) {
593         int acol = col1;
594         int acells = cells;
595         range = [self charRangeForRow:r column:&acol cells:&acells];
596 #if MM_TS_PARANOIA_LOG
597         if (acells != cells || acol != col1)
598             NSLog(@"INTERNAL ERROR [%s]", _cmd);
599 #endif
600         if (NSNotFound == range.location) {
601 #if MM_TS_PARANOIA_LOG
602             NSLog(@"INTERNAL ERROR [%s] Out of bounds", _cmd);
603 #endif
604             return;
605         }
607         [attribString replaceCharactersInRange:range
608                           withAttributedString:emptyString];
609         [attribString setAttributes:attribs
610                               range:NSMakeRange(range.location, cells)];
612         [self edited:(NSTextStorageEditedAttributes
613                 | NSTextStorageEditedCharacters) range:range
614                                         changeInLength:cells-range.length];
616 #if MM_USE_ROW_CACHE
617         rowCache[r].length += cells - range.length;
618 #endif
619     }
622 - (void)clearAll
624     //NSLog(@"%s", _cmd);
625     [self lazyResize:YES];
628 - (void)setDefaultColorsBackground:(NSColor *)bgColor
629                         foreground:(NSColor *)fgColor
631     if (defaultBackgroundColor != bgColor) {
632         [defaultBackgroundColor release];
633         defaultBackgroundColor = bgColor ? [bgColor retain] : nil;
634     }
636     // NOTE: The default foreground color isn't actually used for anything, but
637     // other class instances might want to be able to access it so it is stored
638     // here.
639     if (defaultForegroundColor != fgColor) {
640         [defaultForegroundColor release];
641         defaultForegroundColor = fgColor ? [fgColor retain] : nil;
642     }
645 - (void)setFont:(NSFont*)newFont
647     if (newFont && font != newFont) {
648         [boldItalicFont release];  boldItalicFont = nil;
649         [italicFont release];  italicFont = nil;
650         [boldFont release];  boldFont = nil;
651         [font release];  font = nil;
653         // NOTE! When setting a new font we make sure that the advancement of
654         // each glyph is fixed.
656         float em = [newFont widthOfString:@"m"];
657         float cellWidthMultiplier = [[NSUserDefaults standardUserDefaults]
658                 floatForKey:MMCellWidthMultiplierKey];
660         // NOTE! Even though NSFontFixedAdvanceAttribute is a float, it will
661         // only render at integer sizes.  Hence, we restrict the cell width to
662         // an integer here, otherwise the window width and the actual text
663         // width will not match.
664         cellSize.width = ceilf(em * cellWidthMultiplier);
666         float pointSize = [newFont pointSize];
667         NSDictionary *dict = [NSDictionary
668             dictionaryWithObject:[NSNumber numberWithFloat:cellSize.width]
669                           forKey:NSFontFixedAdvanceAttribute];
671         NSFontDescriptor *desc = [newFont fontDescriptor];
672         desc = [desc fontDescriptorByAddingAttributes:dict];
673         font = [NSFont fontWithDescriptor:desc size:pointSize];
674         [font retain];
676         NSLayoutManager *lm = [[self layoutManagers] objectAtIndex:0];
677         cellSize.height = linespace + (lm ? [lm defaultLineHeightForFont:font]
678                                           : [font defaultLineHeightForFont]);
680         // NOTE: The font manager does not care about the 'font fixed advance'
681         // attribute, so after converting the font we have to add this
682         // attribute again.
683         boldFont = [[NSFontManager sharedFontManager]
684             convertFont:font toHaveTrait:NSBoldFontMask];
685         desc = [boldFont fontDescriptor];
686         desc = [desc fontDescriptorByAddingAttributes:dict];
687         boldFont = [NSFont fontWithDescriptor:desc size:pointSize];
688         [boldFont retain];
690         italicFont = [[NSFontManager sharedFontManager]
691             convertFont:font toHaveTrait:NSItalicFontMask];
692         desc = [italicFont fontDescriptor];
693         desc = [desc fontDescriptorByAddingAttributes:dict];
694         italicFont = [NSFont fontWithDescriptor:desc size:pointSize];
695         [italicFont retain];
697         boldItalicFont = [[NSFontManager sharedFontManager]
698             convertFont:italicFont toHaveTrait:NSBoldFontMask];
699         desc = [boldItalicFont fontDescriptor];
700         desc = [desc fontDescriptorByAddingAttributes:dict];
701         boldItalicFont = [NSFont fontWithDescriptor:desc size:pointSize];
702         [boldItalicFont retain];
703     }
706 - (void)setWideFont:(NSFont *)newFont
708     if (!newFont) {
709         // Use the normal font as the wide font (note that the normal font may
710         // very well include wide characters.)
711         if (font) [self setWideFont:font];
712     } else if (newFont != fontWide) {
713         [boldItalicFontWide release];  boldItalicFontWide = nil;
714         [italicFontWide release];  italicFontWide = nil;
715         [boldFontWide release];  boldFontWide = nil;
716         [fontWide release];  fontWide = nil;
718         float pointSize = [newFont pointSize];
719         NSFontDescriptor *desc = [newFont fontDescriptor];
720         NSDictionary *dictWide = [NSDictionary
721             dictionaryWithObject:[NSNumber numberWithFloat:2*cellSize.width]
722                           forKey:NSFontFixedAdvanceAttribute];
724         desc = [desc fontDescriptorByAddingAttributes:dictWide];
725         fontWide = [NSFont fontWithDescriptor:desc size:pointSize];
726         [fontWide retain];
728         boldFontWide = [[NSFontManager sharedFontManager]
729             convertFont:fontWide toHaveTrait:NSBoldFontMask];
730         desc = [boldFontWide fontDescriptor];
731         desc = [desc fontDescriptorByAddingAttributes:dictWide];
732         boldFontWide = [NSFont fontWithDescriptor:desc size:pointSize];
733         [boldFontWide retain];
735         italicFontWide = [[NSFontManager sharedFontManager]
736             convertFont:fontWide toHaveTrait:NSItalicFontMask];
737         desc = [italicFontWide fontDescriptor];
738         desc = [desc fontDescriptorByAddingAttributes:dictWide];
739         italicFontWide = [NSFont fontWithDescriptor:desc size:pointSize];
740         [italicFontWide retain];
742         boldItalicFontWide = [[NSFontManager sharedFontManager]
743             convertFont:italicFontWide toHaveTrait:NSBoldFontMask];
744         desc = [boldItalicFontWide fontDescriptor];
745         desc = [desc fontDescriptorByAddingAttributes:dictWide];
746         boldItalicFontWide = [NSFont fontWithDescriptor:desc size:pointSize];
747         [boldItalicFontWide retain];
748     }
751 - (NSFont*)font
753     return font;
756 - (NSFont*)fontWide
758     return fontWide;
761 - (NSColor *)defaultBackgroundColor
763     return defaultBackgroundColor;
766 - (NSColor *)defaultForegroundColor
768     return defaultForegroundColor;
771 - (NSSize)size
773     return NSMakeSize(maxColumns*cellSize.width, maxRows*cellSize.height);
776 - (NSSize)cellSize
778     return cellSize;
781 - (NSRect)rectForRowsInRange:(NSRange)range
783     NSRect rect = { 0, 0, 0, 0 };
784     unsigned start = range.location > maxRows ? maxRows : range.location;
785     unsigned length = range.length;
787     if (start+length > maxRows)
788         length = maxRows - start;
790     rect.origin.y = cellSize.height * start;
791     rect.size.height = cellSize.height * length;
793     return rect;
796 - (NSRect)rectForColumnsInRange:(NSRange)range
798     NSRect rect = { 0, 0, 0, 0 };
799     unsigned start = range.location > maxColumns ? maxColumns : range.location;
800     unsigned length = range.length;
802     if (start+length > maxColumns)
803         length = maxColumns - start;
805     rect.origin.x = cellSize.width * start;
806     rect.size.width = cellSize.width * length;
808     return rect;
811 - (unsigned)characterIndexForRow:(int)row column:(int)col
813     int cells = 1;
814     NSRange range = [self charRangeForRow:row column:&col cells:&cells];
815     return range.location != NSNotFound ? range.location : 0;
818 // XXX: unused at the moment
819 - (BOOL)resizeToFitSize:(NSSize)size
821     int rows = maxRows, cols = maxColumns;
823     [self fitToSize:size rows:&rows columns:&cols];
824     if (rows != maxRows || cols != maxColumns) {
825         [self setMaxRows:rows columns:cols];
826         return YES;
827     }
829     // Return NO only if dimensions did not change.
830     return NO;
833 - (NSSize)fitToSize:(NSSize)size
835     return [self fitToSize:size rows:NULL columns:NULL];
838 - (NSSize)fitToSize:(NSSize)size rows:(int *)rows columns:(int *)columns
840     NSSize curSize = [self size];
841     NSSize fitSize = curSize;
842     int fitRows = maxRows;
843     int fitCols = maxColumns;
845     if (size.height < curSize.height) {
846         // Remove lines until the height of the text storage fits inside
847         // 'size'.  However, always make sure there are at least 3 lines in the
848         // text storage.  (Why 3? It seem Vim never allows less than 3 lines.)
849         //
850         // TODO: No need to search since line height is fixed, just calculate
851         // the new height.
852         int rowCount = maxRows;
853         int rowsToRemove;
854         for (rowsToRemove = 0; rowsToRemove < maxRows-3; ++rowsToRemove) {
855             float height = cellSize.height*rowCount;
857             if (height <= size.height) {
858                 fitSize.height = height;
859                 break;
860             }
862             --rowCount;
863         }
865         fitRows -= rowsToRemove;
866     } else if (size.height > curSize.height) {
867         float fh = cellSize.height;
868         if (fh < 1.0f) fh = 1.0f;
870         fitRows = floor(size.height/fh);
871         fitSize.height = fh*fitRows;
872     }
874     if (size.width != curSize.width) {
875         float fw = cellSize.width;
876         if (fw < 1.0f) fw = 1.0f;
878         fitCols = floor(size.width/fw);
879         fitSize.width = fw*fitCols;
880     }
882     if (rows) *rows = fitRows;
883     if (columns) *columns = fitCols;
885     return fitSize;
888 - (NSRect)boundingRectForCharacterAtRow:(int)row column:(int)col
890 #if 1
891     // This properly computes the position of where Vim expects the glyph to be
892     // drawn.  Had the typesetter actually computed the right position of each
893     // character and not hidden some, this code would be correct.
894     NSRect rect = NSZeroRect;
896     rect.origin.x = col*cellSize.width;
897     rect.origin.y = row*cellSize.height;
898     rect.size = cellSize;
900     // Wide character take up twice the width of a normal character.
901     int cells = 1;
902     NSRange r = [self charRangeForRow:row column:&col cells:&cells];
903     if (NSNotFound != r.location
904             && [attribString attribute:MMWideCharacterAttributeName
905                                atIndex:r.location
906                         effectiveRange:nil])
907         rect.size.width += rect.size.width;
909     return rect;
910 #else
911     // Use layout manager to compute bounding rect.  This works in situations
912     // where the layout manager decides to hide glyphs (Vim assumes all glyphs
913     // are drawn).
914     NSLayoutManager *lm = [[self layoutManagers] objectAtIndex:0];
915     NSTextContainer *tc = [[lm textContainers] objectAtIndex:0];
916     int cells = 1;
917     NSRange range = [self charRangeForRow:row column:&col cells:&cells];
918     NSRange glyphRange = [lm glyphRangeForCharacterRange:range
919                                     actualCharacterRange:NULL];
921     return [lm boundingRectForGlyphRange:glyphRange inTextContainer:tc];
922 #endif
925 #if MM_USE_ROW_CACHE
926 - (MMRowCacheEntry *)rowCache
928     return rowCache;
930 #endif
932 @end // MMTextStorage
937 @implementation MMTextStorage (Private)
939 - (void)lazyResize:(BOOL)force
941     // Do nothing if the dimensions are already right.
942     if (!force && actualRows == maxRows && actualColumns == maxColumns)
943         return;
945     NSRange oldRange = NSMakeRange(0, [attribString length]);
947     actualRows = maxRows;
948     actualColumns = maxColumns;
949     characterEqualsColumn = YES;
951 #if MM_USE_ROW_CACHE
952     free(rowCache);
953     rowCache = (MMRowCacheEntry*)calloc(actualRows, sizeof(MMRowCacheEntry));
954 #endif
956     NSDictionary *dict;
957     if (defaultBackgroundColor) {
958         dict = [NSDictionary dictionaryWithObjectsAndKeys:
959                 font, NSFontAttributeName,
960                 defaultBackgroundColor, NSBackgroundColorAttributeName, nil];
961     } else {
962         dict = [NSDictionary dictionaryWithObjectsAndKeys:
963                 font, NSFontAttributeName, nil];
964     }
965             
966     NSMutableString *rowString = [NSMutableString string];
967     int i;
968     for (i = 0; i < maxColumns; ++i) {
969         [rowString appendString:@" "];
970     }
971     [rowString appendString:@"\n"];
973     [emptyRowString release];
974     emptyRowString = [[NSAttributedString alloc] initWithString:rowString
975                                                      attributes:dict];
977     [attribString release];
978     attribString = [[NSMutableAttributedString alloc] init];
979     for (i=0; i<maxRows; ++i) {
980 #if MM_USE_ROW_CACHE
981         rowCache[i].length = actualColumns + 1;
982 #endif
983         [attribString appendAttributedString:emptyRowString];
984     }
986     NSRange fullRange = NSMakeRange(0, [attribString length]);
987     [self edited:(NSTextStorageEditedCharacters|NSTextStorageEditedAttributes)
988            range:oldRange changeInLength:fullRange.length-oldRange.length];
991 - (NSRange)charRangeForRow:(int)row column:(int*)pcol cells:(int*)pcells
993     int col = *pcol;
994     int cells = *pcells;
996     // If no wide chars are used and if every char has length 1 (no composing
997     // characters, no > 16 bit characters), then we can compute the range.
998     if (characterEqualsColumn)
999         return NSMakeRange(row*(actualColumns+1) + col, cells);
1001     NSString *string = [attribString string];
1002     NSRange r, range = { NSNotFound, 0 };
1003     unsigned idx, rowEnd;
1004     int i;
1006     if (row < 0 || row >= actualRows || col < 0 || col >= actualColumns
1007             || col+cells > actualColumns) {
1008 #if MM_TS_PARANOIA_LOG
1009         NSLog(@"%s row=%d col=%d cells=%d is out of range (length=%d)",
1010                 _cmd, row, col, cells, [string length]);
1011 #endif
1012         return range;
1013     }
1015 #if MM_USE_ROW_CACHE
1016     // Locate the beginning of the row
1017     MMRowCacheEntry *cache = rowCache;
1018     idx = 0;
1019     for (i = 0; i < row; ++i, ++cache)
1020         idx += cache->length;
1022     rowEnd = idx + cache->length;
1023 #else
1024     // Locate the beginning of the row by scanning for EOL characters.
1025     r.location = 0;
1026     for (i = 0; i < row; ++i) {
1027         r.length = [string length] - r.location;
1028         r = [string rangeOfString:@"\n" options:NSLiteralSearch range:r];
1029         if (NSNotFound == r.location)
1030             return range;
1031         ++r.location;
1032     }
1033 #endif
1035     // Locate the column
1036 #if MM_USE_ROW_CACHE
1037     cache = &rowCache[row];
1039     i = cache->col;
1040     if (col == i) {
1041         // Cache hit
1042         idx += cache->colOffset;
1043     } else {
1044         range.location = idx;
1046 #if 0  // Backward search seems to be broken...
1047         // Cache miss
1048         if (col < i - col) {
1049             // Search forward from beginning of line.
1050             i = 0;
1051         } else if (actualColumns - col < col - i) {
1052             // Search backward from end of line.
1053             i = actualColumns - 1;
1054             idx += cache->length - 2;
1055         } else {
1056             // Search from cache spot (forward or backward).
1057             idx += cache->colOffset;
1058         }
1060         if (col > i) {
1061             // Forward search
1062             while (col > i) {
1063                 r = [string rangeOfComposedCharacterSequenceAtIndex:idx];
1065                 // Wide chars take up two display cells.
1066                 if ([attribString attribute:MMWideCharacterAttributeName
1067                                     atIndex:idx
1068                              effectiveRange:nil])
1069                     ++i;
1071                 idx += r.length;
1072                 ++i;
1073             }
1074         } else if (col < i) {
1075             // Backward search
1076             while (col < i) {
1077                 r = [string rangeOfComposedCharacterSequenceAtIndex:idx-1];
1078                 idx -= r.length;
1079                 --i;
1081                 // Wide chars take up two display cells.
1082                 if ([attribString attribute:MMWideCharacterAttributeName
1083                                     atIndex:idx
1084                              effectiveRange:nil])
1085                     --i;
1086             }
1087         }
1089         *pcol = i;
1090         cache->col = i;
1091         cache->colOffset = idx - range.location;
1092 #else
1093         // Cache miss
1094         if (col < i) {
1095             // Search forward from beginning of line.
1096             i = 0;
1097         } else {
1098             // Search forward from cache spot.
1099             idx += cache->colOffset;
1100         }
1102         // Forward search
1103         while (col > i) {
1104             r = [string rangeOfComposedCharacterSequenceAtIndex:idx];
1106             // Wide chars take up two display cells.
1107             if ([attribString attribute:MMWideCharacterAttributeName
1108                                 atIndex:idx
1109                          effectiveRange:nil])
1110                 ++i;
1112             idx += r.length;
1113             ++i;
1114         }
1116         *pcol = i;
1117         cache->col = i;
1118         cache->colOffset = idx - range.location;
1119 #endif
1120     }
1121 #else
1122     idx = r.location;
1123     for (i = 0; i < col; ++i) {
1124         r = [string rangeOfComposedCharacterSequenceAtIndex:idx];
1126         // Wide chars take up two display cells.
1127         if ([attribString attribute:MMWideCharacterAttributeName
1128                             atIndex:idx
1129                      effectiveRange:nil])
1130             ++i;
1132         idx += r.length;
1133     }
1134 #endif
1136     // Count the number of characters that cover the cells.
1137     range.location = idx;
1138     for (i = 0; i < cells; ++i) {
1139         r = [string rangeOfComposedCharacterSequenceAtIndex:idx];
1141         // Wide chars take up two display cells.
1142         if ([attribString attribute:MMWideCharacterAttributeName
1143                             atIndex:idx
1144                      effectiveRange:nil])
1145             ++i;
1147         idx += r.length;
1148         range.length += r.length;
1149     }
1151     *pcells = i;
1153 #if MM_TS_PARANOIA_LOG
1154 #if MM_USE_ROW_CACHE
1155     if (range.location >= rowEnd-1) {
1156         NSLog(@"INTERNAL ERROR [%s] : row=%d col=%d cells=%d --> range=%@",
1157                 _cmd, row, col, cells, NSStringFromRange(range));
1158         range.location = rowEnd - 2;
1159         range.length = 1;
1160     } else if (NSMaxRange(range) >= rowEnd) {
1161         NSLog(@"INTERNAL ERROR [%s] : row=%d col=%d cells=%d --> range=%@",
1162                 _cmd, row, col, cells, NSStringFromRange(range));
1163         range.length = rowEnd - range.location - 1;
1164     }
1165 #endif
1167     if (NSMaxRange(range) > [string length]) {
1168         NSLog(@"INTERNAL ERROR [%s] : row=%d col=%d cells=%d --> range=%@",
1169                 _cmd, row, col, cells, NSStringFromRange(range));
1170         range.location = NSNotFound;
1171         range.length = 0;
1172     }
1173 #endif
1175     return range;
1178 - (void)fixInvalidCharactersInRange:(NSRange)range
1180     static NSCharacterSet *invalidCharacterSet = nil;
1181     NSRange invalidRange;
1182     unsigned end;
1184     if (!invalidCharacterSet)
1185         invalidCharacterSet = [[NSCharacterSet characterSetWithRange:
1186             NSMakeRange(0x2028, 2)] retain];
1188     // HACK! Replace characters that the text system can't handle (currently
1189     // LINE SEPARATOR U+2028 and PARAGRAPH SEPARATOR U+2029) with space.
1190     //
1191     // TODO: Treat these separately inside of Vim so we don't have to bother
1192     // here.
1193     while (range.length > 0) {
1194         invalidRange = [[attribString string]
1195             rangeOfCharacterFromSet:invalidCharacterSet
1196                             options:NSLiteralSearch
1197                               range:range];
1198         if (NSNotFound == invalidRange.location)
1199             break;
1201         [attribString replaceCharactersInRange:invalidRange withString:@" "];
1203         end = NSMaxRange(invalidRange);
1204         range.length -= end - range.location;
1205         range.location = end;
1206     }
1209 @end // MMTextStorage (Private)