1 /* vi:set ts=8 sts=4 sw=4 ft=objc:
3 * VIM - Vi IMproved by Bram Moolenaar
4 * MacVim GUI port by Bjorn Winckler
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.
13 * Text rendering related code.
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
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.
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
34 #import "MMTextStorage.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;
69 @implementation MMTextStorage
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;
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;
112 return [attribString string];
115 - (NSDictionary *)attributesAtIndex:(NSUInteger)index
116 effectiveRange:(NSRangePointer)range
118 if (index >= [attribString length]) {
120 *range = NSMakeRange(NSNotFound, 0);
122 return [NSDictionary dictionary];
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 ASLogWarn(@"Calling %s on MMTextStorage is unsupported", _cmd);
140 //[attribString replaceCharactersInRange:range withString:string];
143 - (void)setAttributes:(NSDictionary *)attributes range:(NSRange)range
145 // NOTE! This method must be implemented since the text system calls it
146 // constantly to 'fix attributes', apply font substitution, etc.
148 [attribString setAttributes:attributes range:range];
150 // HACK! If the font attribute is being modified, then ensure that the new
151 // font has a fixed advancement which is either the same as the current
152 // font or twice that, depending on whether it is a 'wide' character that
153 // is being fixed or not.
155 // TODO: This code assumes that the characters in 'range' all have the same
157 NSFont *newFont = [attributes objectForKey:NSFontAttributeName];
159 // Allow disabling of font substitution via a user default. Not
160 // recommended since the typesetter hides the corresponding glyphs and
161 // the display gets messed up.
162 if ([[NSUserDefaults standardUserDefaults]
163 boolForKey:MMNoFontSubstitutionKey])
166 float adv = cellSize.width;
167 if ([attribString attribute:MMWideCharacterAttributeName
168 atIndex:range.location
169 effectiveRange:NULL])
172 // Create a new font which has the 'fixed advance attribute' set.
173 NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
174 [NSNumber numberWithFloat:adv], NSFontFixedAdvanceAttribute, nil];
175 NSFontDescriptor *desc = [newFont fontDescriptor];
176 desc = [desc fontDescriptorByAddingAttributes:dict];
177 newFont = [NSFont fontWithDescriptor:desc size:[newFont pointSize]];
179 // Now modify the 'attributes' dictionary to hold the new font.
180 NSMutableDictionary *newAttr = [NSMutableDictionary
181 dictionaryWithDictionary:attributes];
182 [newAttr setObject:newFont forKey:NSFontAttributeName];
184 [attribString setAttributes:newAttr range:range];
186 [attribString setAttributes:attributes range:range];
208 return actualColumns;
216 - (void)setLinespace:(float)newLinespace
218 NSLayoutManager *lm = [[self layoutManagers] objectAtIndex:0];
220 ASLogWarn(@"No layout manager available");
224 linespace = newLinespace;
226 // NOTE: The linespace is added to the cell height in order for a multiline
227 // selection not to have white (background color) gaps between lines. Also
228 // this simplifies the code a lot because there is no need to check the
229 // linespace when calculating the size of the text view etc. When the
230 // linespace is non-zero the baseline will be adjusted as well; check
232 cellSize.height = linespace + [lm defaultLineHeightForFont:font];
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.
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 [self lazyResize:NO];
255 if (row < 0 || row >= maxRows || col < 0 || col >= maxColumns
256 || col+cells > maxColumns || !string || !(fg && bg && sp))
259 BOOL hasControlChars = [string rangeOfCharacterFromSet:
260 [NSCharacterSet controlCharacterSet]].location != NSNotFound;
261 if (hasControlChars) {
262 // HACK! If a string for some reason contains control characters, then
263 // draw blanks instead (otherwise charRangeForRow::: fails).
264 NSRange subRange = { 0, cells };
266 string = [[emptyRowString string] substringWithRange:subRange];
269 // Find range of characters in text storage to replace.
272 NSRange range = [self charRangeForRow:row column:&acol cells:&acells];
273 if (NSNotFound == range.location) {
274 #if MM_TS_PARANOIA_LOG
275 ASLogErr(@"INTERNAL ERROR: Out of bounds");
280 // Create dictionary of attributes to apply to the new characters.
281 NSFont *theFont = font;
282 if (flags & DRAW_WIDE) {
283 if (flags & DRAW_BOLD)
284 theFont = flags & DRAW_ITALIC ? boldItalicFontWide : boldFontWide;
285 else if (flags & DRAW_ITALIC)
286 theFont = italicFontWide;
290 if (flags & DRAW_BOLD)
291 theFont = flags & DRAW_ITALIC ? boldItalicFont : boldFont;
292 else if (flags & DRAW_ITALIC)
293 theFont = italicFont;
296 NSMutableDictionary *attributes =
297 [NSMutableDictionary dictionaryWithObjectsAndKeys:
298 theFont, NSFontAttributeName,
299 bg, NSBackgroundColorAttributeName,
300 fg, NSForegroundColorAttributeName,
301 sp, NSUnderlineColorAttributeName,
302 [NSNumber numberWithInt:0], NSLigatureAttributeName,
305 if (flags & DRAW_UNDERL) {
306 NSNumber *value = [NSNumber numberWithInt:(NSUnderlineStyleSingle
307 | NSUnderlinePatternSolid)]; // | NSUnderlineByWordMask
308 [attributes setObject:value forKey:NSUnderlineStyleAttributeName];
311 if (flags & DRAW_UNDERC) {
312 // TODO: figure out how do draw proper undercurls
313 NSNumber *value = [NSNumber numberWithInt:(NSUnderlineStyleThick
314 | NSUnderlinePatternDot)]; // | NSUnderlineByWordMask
315 [attributes setObject:value forKey:NSUnderlineStyleAttributeName];
318 // Mark these characters as wide. This attribute is subsequently checked
319 // when translating (row,col) pairs to offsets within 'attribString'.
320 if (flags & DRAW_WIDE)
321 [attributes setObject:[NSNull null]
322 forKey:MMWideCharacterAttributeName];
324 // Replace characters in text storage and apply new attributes.
325 NSRange r = NSMakeRange(range.location, [string length]);
326 [attribString replaceCharactersInRange:range withString:string];
327 [attribString setAttributes:attributes range:r];
329 unsigned changeInLength = [string length] - range.length;
330 if (acells != cells || acol != col) {
331 if (acells == cells + 1) {
332 // NOTE: A normal width character replaced a double width
333 // character. To maintain the invariant that each row covers the
334 // same amount of cells, we compensate by adding an empty column.
335 [attribString replaceCharactersInRange:NSMakeRange(NSMaxRange(r),0)
336 withAttributedString:[emptyRowString
337 attributedSubstringFromRange:NSMakeRange(0,1)]];
340 } else if (acol == col - 1) {
341 [attribString replaceCharactersInRange:NSMakeRange(r.location,0)
342 withAttributedString:[emptyRowString
343 attributedSubstringFromRange:NSMakeRange(0,1)]];
345 } else if (acol == col + 1) {
346 [attribString replaceCharactersInRange:NSMakeRange(r.location-1,1)
347 withAttributedString:[emptyRowString
348 attributedSubstringFromRange:NSMakeRange(0,2)]];
352 // NOTE: It seems that this never gets called. If it ever does,
353 // then there is another case to treat.
354 #if MM_TS_PARANOIA_LOG
355 ASLogWarn(@"row=%d col=%d acol=%d cells=%d acells=%d", row, col,
356 acol, cells, acells);
361 if ((flags & DRAW_WIDE) || [string length] != cells)
362 characterEqualsColumn = NO;
364 [self fixInvalidCharactersInRange:r];
366 [self edited:(NSTextStorageEditedCharacters|NSTextStorageEditedAttributes)
367 range:range changeInLength:changeInLength];
370 rowCache[row].length += changeInLength;
375 * Delete 'count' lines from 'row' and insert 'count' empty lines at the bottom
376 * of the scroll region.
378 - (void)deleteLinesFromRow:(int)row lineCount:(int)count
379 scrollBottom:(int)bottom left:(int)left right:(int)right
380 color:(NSColor *)color
382 [self lazyResize:NO];
384 if (row < 0 || row+count > maxRows || bottom > maxRows || left < 0
385 || right > maxColumns)
388 int total = 1 + bottom - row;
389 int move = total - count;
390 int width = right - left + 1;
392 NSRange destRange, srcRange;
395 for (i = 0; i < move; ++i, ++destRow) {
398 destRange = [self charRangeForRow:destRow column:&acol cells:&acells];
399 #if MM_TS_PARANOIA_LOG
400 if (acells != width || acol != left)
401 ASLogErr(@"INTERNAL ERROR");
404 acol = left; acells = width;
405 srcRange = [self charRangeForRow:(destRow+count) column:&acol
407 #if MM_TS_PARANOIA_LOG
408 if (acells != width || acol != left)
409 ASLogErr(@"INTERNAL ERROR");
412 if (NSNotFound == destRange.location || NSNotFound == srcRange.location)
414 #if MM_TS_PARANOIA_LOG
415 ASLogErr(@"INTERNAL ERROR: Out of bounds");
420 NSAttributedString *srcString = [attribString
421 attributedSubstringFromRange:srcRange];
423 [attribString replaceCharactersInRange:destRange
424 withAttributedString:srcString];
425 [self edited:(NSTextStorageEditedCharacters
426 | NSTextStorageEditedAttributes) range:destRange
427 changeInLength:([srcString length]-destRange.length)];
430 rowCache[destRow].length += [srcString length] - destRange.length;
434 NSRange emptyRange = {0,width};
435 NSAttributedString *emptyString =
436 [emptyRowString attributedSubstringFromRange:emptyRange];
437 NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
438 font, NSFontAttributeName,
439 color, NSBackgroundColorAttributeName, nil];
441 for (i = 0; i < count; ++i, ++destRow) {
444 destRange = [self charRangeForRow:destRow column:&acol cells:&acells];
445 #if MM_TS_PARANOIA_LOG
446 if (acells != width || acol != left)
447 ASLogErr(@"INTERNAL ERROR");
449 if (NSNotFound == destRange.location) {
450 #if MM_TS_PARANOIA_LOG
451 ASLogErr(@"INTERNAL ERROR: Out of bounds");
456 [attribString replaceCharactersInRange:destRange
457 withAttributedString:emptyString];
458 [attribString setAttributes:attribs
459 range:NSMakeRange(destRange.location, width)];
461 [self edited:(NSTextStorageEditedAttributes
462 | NSTextStorageEditedCharacters) range:destRange
463 changeInLength:([emptyString length]-destRange.length)];
466 rowCache[destRow].length += [emptyString length] - destRange.length;
472 * Insert 'count' empty lines at 'row' and delete 'count' lines from the bottom
473 * of the scroll region.
475 - (void)insertLinesAtRow:(int)row lineCount:(int)count
476 scrollBottom:(int)bottom left:(int)left right:(int)right
477 color:(NSColor *)color
479 [self lazyResize:NO];
481 if (row < 0 || row+count > maxRows || bottom > maxRows || left < 0
482 || right > maxColumns)
485 int total = 1 + bottom - row;
486 int move = total - count;
487 int width = right - left + 1;
488 int destRow = bottom;
489 int srcRow = row + move - 1;
490 NSRange destRange, srcRange;
493 for (i = 0; i < move; ++i, --destRow, --srcRow) {
496 destRange = [self charRangeForRow:destRow column:&acol cells:&acells];
497 #if MM_TS_PARANOIA_LOG
498 if (acells != width || acol != left)
499 ASLogErr(@"INTERNAL ERROR");
502 acol = left; acells = width;
503 srcRange = [self charRangeForRow:srcRow column:&acol cells:&acells];
504 #if MM_TS_PARANOIA_LOG
505 if (acells != width || acol != left)
506 ASLogErr(@"INTERNAL ERROR");
508 if (NSNotFound == destRange.location || NSNotFound == srcRange.location)
510 #if MM_TS_PARANOIA_LOG
511 ASLogErr(@"INTERNAL ERROR: Out of bounds");
516 NSAttributedString *srcString = [attribString
517 attributedSubstringFromRange:srcRange];
518 [attribString replaceCharactersInRange:destRange
519 withAttributedString:srcString];
520 [self edited:(NSTextStorageEditedCharacters
521 | NSTextStorageEditedAttributes) range:destRange
522 changeInLength:([srcString length]-destRange.length)];
525 rowCache[destRow].length += [srcString length] - destRange.length;
529 NSRange emptyRange = {0,width};
530 NSAttributedString *emptyString =
531 [emptyRowString attributedSubstringFromRange:emptyRange];
532 NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
533 font, NSFontAttributeName,
534 color, NSBackgroundColorAttributeName, nil];
536 for (i = 0; i < count; ++i, --destRow) {
539 destRange = [self charRangeForRow:destRow column:&acol cells:&acells];
540 #if MM_TS_PARANOIA_LOG
541 if (acells != width || acol != left)
542 ASLogErr(@"INTERNAL ERROR");
544 if (NSNotFound == destRange.location) {
545 #if MM_TS_PARANOIA_LOG
546 ASLogErr(@"INTERNAL ERROR: Out of bounds");
551 [attribString replaceCharactersInRange:destRange
552 withAttributedString:emptyString];
553 [attribString setAttributes:attribs
554 range:NSMakeRange(destRange.location, width)];
556 [self edited:(NSTextStorageEditedAttributes
557 | NSTextStorageEditedCharacters) range:destRange
558 changeInLength:([emptyString length]-destRange.length)];
561 rowCache[destRow].length += [emptyString length] - destRange.length;
566 - (void)clearBlockFromRow:(int)row1 column:(int)col1 toRow:(int)row2
567 column:(int)col2 color:(NSColor *)color
569 [self lazyResize:NO];
571 if (row1 < 0 || row2 >= maxRows || col1 < 0 || col2 > maxColumns)
574 NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
575 font, NSFontAttributeName,
576 color, NSBackgroundColorAttributeName, nil];
577 int cells = col2 - col1 + 1;
578 NSRange range, emptyRange = {0, cells};
579 NSAttributedString *emptyString =
580 [emptyRowString attributedSubstringFromRange:emptyRange];
583 for (r=row1; r<=row2; ++r) {
586 range = [self charRangeForRow:r column:&acol cells:&acells];
587 #if MM_TS_PARANOIA_LOG
588 if (acells != cells || acol != col1)
589 ASLogErr(@"INTERNAL ERROR");
591 if (NSNotFound == range.location) {
592 #if MM_TS_PARANOIA_LOG
593 ASLogErr(@"INTERNAL ERROR: Out of bounds");
598 [attribString replaceCharactersInRange:range
599 withAttributedString:emptyString];
600 [attribString setAttributes:attribs
601 range:NSMakeRange(range.location, cells)];
603 [self edited:(NSTextStorageEditedAttributes
604 | NSTextStorageEditedCharacters) range:range
605 changeInLength:cells-range.length];
608 rowCache[r].length += cells - range.length;
615 [self lazyResize:YES];
618 - (void)setDefaultColorsBackground:(NSColor *)bgColor
619 foreground:(NSColor *)fgColor
621 if (defaultBackgroundColor != bgColor) {
622 [defaultBackgroundColor release];
623 defaultBackgroundColor = bgColor ? [bgColor retain] : nil;
626 // NOTE: The default foreground color isn't actually used for anything, but
627 // other class instances might want to be able to access it so it is stored
629 if (defaultForegroundColor != fgColor) {
630 [defaultForegroundColor release];
631 defaultForegroundColor = fgColor ? [fgColor retain] : nil;
635 - (void)setFont:(NSFont*)newFont
637 if (newFont && font != newFont) {
638 [boldItalicFont release]; boldItalicFont = nil;
639 [italicFont release]; italicFont = nil;
640 [boldFont release]; boldFont = nil;
641 [font release]; font = nil;
643 // NOTE! When setting a new font we make sure that the advancement of
644 // each glyph is fixed.
646 float em = [@"m" sizeWithAttributes:
647 [NSDictionary dictionaryWithObject:newFont
648 forKey:NSFontAttributeName]].width;
649 float cellWidthMultiplier = [[NSUserDefaults standardUserDefaults]
650 floatForKey:MMCellWidthMultiplierKey];
652 // NOTE! Even though NSFontFixedAdvanceAttribute is a float, it will
653 // only render at integer sizes. Hence, we restrict the cell width to
654 // an integer here, otherwise the window width and the actual text
655 // width will not match.
656 cellSize.width = ceilf(em * cellWidthMultiplier);
658 float pointSize = [newFont pointSize];
659 NSDictionary *dict = [NSDictionary
660 dictionaryWithObject:[NSNumber numberWithFloat:cellSize.width]
661 forKey:NSFontFixedAdvanceAttribute];
663 NSFontDescriptor *desc = [newFont fontDescriptor];
664 desc = [desc fontDescriptorByAddingAttributes:dict];
665 font = [NSFont fontWithDescriptor:desc size:pointSize];
668 NSLayoutManager *lm = [[self layoutManagers] objectAtIndex:0];
670 cellSize.height = linespace + [lm defaultLineHeightForFont:font];
672 // Should never happen, set some bogus value for cell height.
673 ASLogWarn(@"No layout manager available");
674 cellSize.height = linespace + 16.0;
677 // NOTE: The font manager does not care about the 'font fixed advance'
678 // attribute, so after converting the font we have to add this
680 boldFont = [[NSFontManager sharedFontManager]
681 convertFont:font toHaveTrait:NSBoldFontMask];
682 desc = [boldFont fontDescriptor];
683 desc = [desc fontDescriptorByAddingAttributes:dict];
684 boldFont = [NSFont fontWithDescriptor:desc size:pointSize];
687 italicFont = [[NSFontManager sharedFontManager]
688 convertFont:font toHaveTrait:NSItalicFontMask];
689 desc = [italicFont fontDescriptor];
690 desc = [desc fontDescriptorByAddingAttributes:dict];
691 italicFont = [NSFont fontWithDescriptor:desc size:pointSize];
694 boldItalicFont = [[NSFontManager sharedFontManager]
695 convertFont:italicFont toHaveTrait:NSBoldFontMask];
696 desc = [boldItalicFont fontDescriptor];
697 desc = [desc fontDescriptorByAddingAttributes:dict];
698 boldItalicFont = [NSFont fontWithDescriptor:desc size:pointSize];
699 [boldItalicFont retain];
703 - (void)setWideFont:(NSFont *)newFont
706 // Use the normal font as the wide font (note that the normal font may
707 // very well include wide characters.)
708 if (font) [self setWideFont:font];
709 } else if (newFont != fontWide) {
710 [boldItalicFontWide release]; boldItalicFontWide = nil;
711 [italicFontWide release]; italicFontWide = nil;
712 [boldFontWide release]; boldFontWide = nil;
713 [fontWide release]; fontWide = nil;
715 float pointSize = [newFont pointSize];
716 NSFontDescriptor *desc = [newFont fontDescriptor];
717 NSDictionary *dictWide = [NSDictionary
718 dictionaryWithObject:[NSNumber numberWithFloat:2*cellSize.width]
719 forKey:NSFontFixedAdvanceAttribute];
721 desc = [desc fontDescriptorByAddingAttributes:dictWide];
722 fontWide = [NSFont fontWithDescriptor:desc size:pointSize];
725 boldFontWide = [[NSFontManager sharedFontManager]
726 convertFont:fontWide toHaveTrait:NSBoldFontMask];
727 desc = [boldFontWide fontDescriptor];
728 desc = [desc fontDescriptorByAddingAttributes:dictWide];
729 boldFontWide = [NSFont fontWithDescriptor:desc size:pointSize];
730 [boldFontWide retain];
732 italicFontWide = [[NSFontManager sharedFontManager]
733 convertFont:fontWide toHaveTrait:NSItalicFontMask];
734 desc = [italicFontWide fontDescriptor];
735 desc = [desc fontDescriptorByAddingAttributes:dictWide];
736 italicFontWide = [NSFont fontWithDescriptor:desc size:pointSize];
737 [italicFontWide retain];
739 boldItalicFontWide = [[NSFontManager sharedFontManager]
740 convertFont:italicFontWide toHaveTrait:NSBoldFontMask];
741 desc = [boldItalicFontWide fontDescriptor];
742 desc = [desc fontDescriptorByAddingAttributes:dictWide];
743 boldItalicFontWide = [NSFont fontWithDescriptor:desc size:pointSize];
744 [boldItalicFontWide retain];
758 - (NSColor *)defaultBackgroundColor
760 return defaultBackgroundColor;
763 - (NSColor *)defaultForegroundColor
765 return defaultForegroundColor;
770 return NSMakeSize(maxColumns*cellSize.width, maxRows*cellSize.height);
778 - (NSRect)rectForRowsInRange:(NSRange)range
780 NSRect rect = { {0, 0}, {0, 0} };
781 unsigned start = range.location > maxRows ? maxRows : range.location;
782 unsigned length = range.length;
784 if (start+length > maxRows)
785 length = maxRows - start;
787 rect.origin.y = cellSize.height * start;
788 rect.size.height = cellSize.height * length;
793 - (NSRect)rectForColumnsInRange:(NSRange)range
795 NSRect rect = { {0, 0}, {0, 0} };
796 unsigned start = range.location > maxColumns ? maxColumns : range.location;
797 unsigned length = range.length;
799 if (start+length > maxColumns)
800 length = maxColumns - start;
802 rect.origin.x = cellSize.width * start;
803 rect.size.width = cellSize.width * length;
808 - (NSUInteger)characterIndexForRow:(int)row column:(int)col
811 NSRange range = [self charRangeForRow:row column:&col cells:&cells];
812 return range.location != NSNotFound ? range.location : 0;
815 // XXX: unused at the moment
816 - (BOOL)resizeToFitSize:(NSSize)size
818 int rows = maxRows, cols = maxColumns;
820 [self fitToSize:size rows:&rows columns:&cols];
821 if (rows != maxRows || cols != maxColumns) {
822 [self setMaxRows:rows columns:cols];
826 // Return NO only if dimensions did not change.
830 - (NSSize)fitToSize:(NSSize)size
832 return [self fitToSize:size rows:NULL columns:NULL];
835 - (NSSize)fitToSize:(NSSize)size rows:(int *)rows columns:(int *)columns
837 NSSize curSize = [self size];
838 NSSize fitSize = curSize;
839 int fitRows = maxRows;
840 int fitCols = maxColumns;
842 if (size.height < curSize.height) {
843 // Remove lines until the height of the text storage fits inside
844 // 'size'. However, always make sure there are at least 3 lines in the
845 // text storage. (Why 3? It seem Vim never allows less than 3 lines.)
847 // TODO: No need to search since line height is fixed, just calculate
849 int rowCount = maxRows;
851 for (rowsToRemove = 0; rowsToRemove < maxRows-3; ++rowsToRemove) {
852 float height = cellSize.height*rowCount;
854 if (height <= size.height) {
855 fitSize.height = height;
862 fitRows -= rowsToRemove;
863 } else if (size.height > curSize.height) {
864 float fh = cellSize.height;
865 if (fh < 1.0f) fh = 1.0f;
867 fitRows = floor(size.height/fh);
868 fitSize.height = fh*fitRows;
871 if (size.width != curSize.width) {
872 float fw = cellSize.width;
873 if (fw < 1.0f) fw = 1.0f;
875 fitCols = floor(size.width/fw);
876 fitSize.width = fw*fitCols;
879 if (rows) *rows = fitRows;
880 if (columns) *columns = fitCols;
885 - (NSRect)boundingRectForCharacterAtRow:(int)row column:(int)col
888 // This properly computes the position of where Vim expects the glyph to be
889 // drawn. Had the typesetter actually computed the right position of each
890 // character and not hidden some, this code would be correct.
891 NSRect rect = NSZeroRect;
893 rect.origin.x = col*cellSize.width;
894 rect.origin.y = row*cellSize.height;
895 rect.size = cellSize;
897 // Wide character take up twice the width of a normal character.
899 NSRange r = [self charRangeForRow:row column:&col cells:&cells];
900 if (NSNotFound != r.location
901 && [attribString attribute:MMWideCharacterAttributeName
904 rect.size.width += rect.size.width;
908 // Use layout manager to compute bounding rect. This works in situations
909 // where the layout manager decides to hide glyphs (Vim assumes all glyphs
911 NSLayoutManager *lm = [[self layoutManagers] objectAtIndex:0];
912 NSTextContainer *tc = [[lm textContainers] objectAtIndex:0];
914 NSRange range = [self charRangeForRow:row column:&col cells:&cells];
915 NSRange glyphRange = [lm glyphRangeForCharacterRange:range
916 actualCharacterRange:NULL];
918 return [lm boundingRectForGlyphRange:glyphRange inTextContainer:tc];
923 - (MMRowCacheEntry *)rowCache
929 @end // MMTextStorage
934 @implementation MMTextStorage (Private)
936 - (void)lazyResize:(BOOL)force
938 // Do nothing if the dimensions are already right.
939 if (!force && actualRows == maxRows && actualColumns == maxColumns)
942 NSRange oldRange = NSMakeRange(0, [attribString length]);
944 actualRows = maxRows;
945 actualColumns = maxColumns;
946 characterEqualsColumn = YES;
950 rowCache = (MMRowCacheEntry*)calloc(actualRows, sizeof(MMRowCacheEntry));
954 if (defaultBackgroundColor) {
955 dict = [NSDictionary dictionaryWithObjectsAndKeys:
956 font, NSFontAttributeName,
957 defaultBackgroundColor, NSBackgroundColorAttributeName, nil];
959 dict = [NSDictionary dictionaryWithObjectsAndKeys:
960 font, NSFontAttributeName, nil];
963 NSMutableString *rowString = [NSMutableString string];
965 for (i = 0; i < maxColumns; ++i) {
966 [rowString appendString:@" "];
968 [rowString appendString:@"\n"];
970 [emptyRowString release];
971 emptyRowString = [[NSAttributedString alloc] initWithString:rowString
974 [attribString release];
975 attribString = [[NSMutableAttributedString alloc] init];
976 for (i=0; i<maxRows; ++i) {
978 rowCache[i].length = actualColumns + 1;
980 [attribString appendAttributedString:emptyRowString];
983 NSRange fullRange = NSMakeRange(0, [attribString length]);
984 [self edited:(NSTextStorageEditedCharacters|NSTextStorageEditedAttributes)
985 range:oldRange changeInLength:fullRange.length-oldRange.length];
988 - (NSRange)charRangeForRow:(int)row column:(int*)pcol cells:(int*)pcells
993 // If no wide chars are used and if every char has length 1 (no composing
994 // characters, no > 16 bit characters), then we can compute the range.
995 if (characterEqualsColumn)
996 return NSMakeRange(row*(actualColumns+1) + col, cells);
998 NSString *string = [attribString string];
999 unsigned stringLen = [string length];
1000 NSRange r, range = { NSNotFound, 0 };
1004 if (row < 0 || row >= actualRows || col < 0 || col >= actualColumns
1005 || col+cells > actualColumns) {
1006 #if MM_TS_PARANOIA_LOG
1007 ASLogErr(@"row=%d col=%d cells=%d is out of range (length=%d)",
1008 row, col, cells, stringLen);
1013 #if MM_USE_ROW_CACHE
1014 // Locate the beginning of the row
1015 MMRowCacheEntry *cache = rowCache;
1017 for (i = 0; i < row; ++i, ++cache)
1018 idx += cache->length;
1020 int rowEnd = idx + cache->length;
1022 // Locate the beginning of the row by scanning for EOL characters.
1024 for (i = 0; i < row; ++i) {
1025 r.length = stringLen - r.location;
1026 r = [string rangeOfString:@"\n" options:NSLiteralSearch range:r];
1027 if (NSNotFound == r.location)
1033 // Locate the column
1034 #if MM_USE_ROW_CACHE
1035 cache = &rowCache[row];
1040 idx += cache->colOffset;
1042 range.location = idx;
1044 #if 0 // Backward search seems to be broken...
1046 if (col < i - col) {
1047 // Search forward from beginning of line.
1049 } else if (actualColumns - col < col - i) {
1050 // Search backward from end of line.
1051 i = actualColumns - 1;
1052 idx += cache->length - 2;
1054 // Search from cache spot (forward or backward).
1055 idx += cache->colOffset;
1061 if (idx >= stringLen)
1062 return NSMakeRange(NSNotFound, 0);
1063 r = [string rangeOfComposedCharacterSequenceAtIndex:idx];
1065 // Wide chars take up two display cells.
1066 if ([attribString attribute:MMWideCharacterAttributeName
1068 effectiveRange:nil])
1074 } else if (col < i) {
1077 if (idx-1 >= stringLen)
1078 return NSMakeRange(NSNotFound, 0);
1079 r = [string rangeOfComposedCharacterSequenceAtIndex:idx-1];
1083 // Wide chars take up two display cells.
1084 if ([attribString attribute:MMWideCharacterAttributeName
1086 effectiveRange:nil])
1093 cache->colOffset = idx - range.location;
1097 // Search forward from beginning of line.
1100 // Search forward from cache spot.
1101 idx += cache->colOffset;
1106 if (idx >= stringLen)
1107 return NSMakeRange(NSNotFound, 0);
1108 r = [string rangeOfComposedCharacterSequenceAtIndex:idx];
1110 // Wide chars take up two display cells.
1111 if ([attribString attribute:MMWideCharacterAttributeName
1113 effectiveRange:nil])
1122 cache->colOffset = idx - range.location;
1127 for (i = 0; i < col; ++i) {
1128 if (idx >= stringLen)
1129 return NSMakeRange(NSNotFound, 0);
1130 r = [string rangeOfComposedCharacterSequenceAtIndex:idx];
1132 // Wide chars take up two display cells.
1133 if ([attribString attribute:MMWideCharacterAttributeName
1135 effectiveRange:nil])
1142 // Count the number of characters that cover the cells.
1143 range.location = idx;
1144 for (i = 0; i < cells; ++i) {
1145 if (idx >= stringLen)
1146 return NSMakeRange(NSNotFound, 0);
1147 r = [string rangeOfComposedCharacterSequenceAtIndex:idx];
1149 // Wide chars take up two display cells.
1150 if ([attribString attribute:MMWideCharacterAttributeName
1152 effectiveRange:nil])
1156 range.length += r.length;
1161 #if MM_TS_PARANOIA_LOG
1162 #if MM_USE_ROW_CACHE
1163 if (range.location >= rowEnd-1) {
1164 ASLogErr(@"INTERNAL ERROR: row=%d col=%d cells=%d --> range=%@",
1165 row, col, cells, NSStringFromRange(range));
1166 range.location = rowEnd - 2;
1168 } else if (NSMaxRange(range) >= rowEnd) {
1169 ASLogErr(@"INTERNAL ERROR: row=%d col=%d cells=%d --> range=%@",
1170 row, col, cells, NSStringFromRange(range));
1171 range.length = rowEnd - range.location - 1;
1175 if (NSMaxRange(range) > stringLen) {
1176 ASLogErr(@"INTERNAL ERROR: row=%d col=%d cells=%d --> range=%@",
1177 row, col, cells, NSStringFromRange(range));
1178 range.location = NSNotFound;
1186 - (void)fixInvalidCharactersInRange:(NSRange)range
1188 static NSCharacterSet *invalidCharacterSet = nil;
1189 NSRange invalidRange;
1192 if (!invalidCharacterSet)
1193 invalidCharacterSet = [[NSCharacterSet characterSetWithRange:
1194 NSMakeRange(0x2028, 2)] retain];
1196 // HACK! Replace characters that the text system can't handle (currently
1197 // LINE SEPARATOR U+2028 and PARAGRAPH SEPARATOR U+2029) with space.
1199 // TODO: Treat these separately inside of Vim so we don't have to bother
1201 while (range.length > 0) {
1202 invalidRange = [[attribString string]
1203 rangeOfCharacterFromSet:invalidCharacterSet
1204 options:NSLiteralSearch
1206 if (NSNotFound == invalidRange.location)
1209 [attribString replaceCharactersInRange:invalidRange withString:@" "];
1211 end = NSMaxRange(invalidRange);
1212 range.length -= end - range.location;
1213 range.location = end;
1217 @end // MMTextStorage (Private)