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
54 #define DRAW_WIDE 0x40 /* draw wide text */
57 static NSString *MMWideCharacterAttributeName = @"MMWideChar";
62 @interface MMTextStorage (Private)
63 - (void)lazyResize:(BOOL)force;
64 - (NSRange)charRangeForRow:(int)row column:(int*)col cells:(int*)cells;
65 - (void)fixInvalidCharactersInRange:(NSRange)range;
70 @implementation MMTextStorage
74 if ((self = [super init])) {
75 attribString = [[NSMutableAttributedString alloc] initWithString:@""];
76 // NOTE! It does not matter which font is set here, Vim will set its
77 // own font on startup anyway. Just set some bogus values.
78 font = [[NSFont userFixedPitchFontOfSize:0] retain];
79 cellSize.height = 16.0;
96 [emptyRowString release]; emptyRowString = nil;
97 [boldItalicFontWide release]; boldItalicFontWide = nil;
98 [italicFontWide release]; italicFontWide = nil;
99 [boldFontWide release]; boldFontWide = nil;
100 [fontWide release]; fontWide = nil;
101 [boldItalicFont release]; boldItalicFont = nil;
102 [italicFont release]; italicFont = nil;
103 [boldFont release]; boldFont = nil;
104 [font release]; font = nil;
105 [defaultBackgroundColor release]; defaultBackgroundColor = nil;
106 [defaultForegroundColor release]; defaultForegroundColor = nil;
107 [attribString release]; attribString = nil;
113 return [attribString string];
116 - (NSDictionary *)attributesAtIndex:(NSUInteger)index
117 effectiveRange:(NSRangePointer)range
119 return [attribString attributesAtIndex:index effectiveRange:range];
122 - (void)replaceCharactersInRange:(NSRange)range
123 withString:(NSString *)string
125 #if MM_TS_PARANOIA_LOG
126 ASLogWarn(@"Calling %s on MMTextStorage is unsupported", _cmd);
128 //[attribString replaceCharactersInRange:range withString:string];
131 - (void)setAttributes:(NSDictionary *)attributes range:(NSRange)range
133 // NOTE! This method must be implemented since the text system calls it
134 // constantly to 'fix attributes', apply font substitution, etc.
136 [attribString setAttributes:attributes range:range];
138 // HACK! If the font attribute is being modified, then ensure that the new
139 // font has a fixed advancement which is either the same as the current
140 // font or twice that, depending on whether it is a 'wide' character that
141 // is being fixed or not.
143 // TODO: This code assumes that the characters in 'range' all have the same
145 NSFont *newFont = [attributes objectForKey:NSFontAttributeName];
147 // Allow disabling of font substitution via a user default. Not
148 // recommended since the typesetter hides the corresponding glyphs and
149 // the display gets messed up.
150 if ([[NSUserDefaults standardUserDefaults]
151 boolForKey:MMNoFontSubstitutionKey])
154 float adv = cellSize.width;
155 if ([attribString attribute:MMWideCharacterAttributeName
156 atIndex:range.location
157 effectiveRange:NULL])
160 // Create a new font which has the 'fixed advance attribute' set.
161 NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
162 [NSNumber numberWithFloat:adv], NSFontFixedAdvanceAttribute, nil];
163 NSFontDescriptor *desc = [newFont fontDescriptor];
164 desc = [desc fontDescriptorByAddingAttributes:dict];
165 newFont = [NSFont fontWithDescriptor:desc size:[newFont pointSize]];
167 // Now modify the 'attributes' dictionary to hold the new font.
168 NSMutableDictionary *newAttr = [NSMutableDictionary
169 dictionaryWithDictionary:attributes];
170 [newAttr setObject:newFont forKey:NSFontAttributeName];
172 [attribString setAttributes:newAttr range:range];
174 [attribString setAttributes:attributes range:range];
196 return actualColumns;
204 - (void)setLinespace:(float)newLinespace
206 NSLayoutManager *lm = [[self layoutManagers] objectAtIndex:0];
208 ASLogWarn(@"No layout manager available");
212 linespace = newLinespace;
214 // NOTE: The linespace is added to the cell height in order for a multiline
215 // selection not to have white (background color) gaps between lines. Also
216 // this simplifies the code a lot because there is no need to check the
217 // linespace when calculating the size of the text view etc. When the
218 // linespace is non-zero the baseline will be adjusted as well; check
220 cellSize.height = linespace + [lm defaultLineHeightForFont:font];
223 - (void)getMaxRows:(int*)rows columns:(int*)cols
225 if (rows) *rows = maxRows;
226 if (cols) *cols = maxColumns;
229 - (void)setMaxRows:(int)rows columns:(int)cols
231 // NOTE: Just remember the new values, the actual resizing is done lazily.
236 - (void)drawString:(NSString *)string atRow:(int)row column:(int)col
237 cells:(int)cells withFlags:(int)flags
238 foregroundColor:(NSColor *)fg backgroundColor:(NSColor *)bg
239 specialColor:(NSColor *)sp
241 [self lazyResize:NO];
243 if (row < 0 || row >= maxRows || col < 0 || col >= maxColumns
244 || col+cells > maxColumns || !string || !(fg && bg && sp))
247 BOOL hasControlChars = [string rangeOfCharacterFromSet:
248 [NSCharacterSet controlCharacterSet]].location != NSNotFound;
249 if (hasControlChars) {
250 // HACK! If a string for some reason contains control characters, then
251 // draw blanks instead (otherwise charRangeForRow::: fails).
252 NSRange subRange = { 0, cells };
254 string = [[emptyRowString string] substringWithRange:subRange];
257 // Find range of characters in text storage to replace.
260 NSRange range = [self charRangeForRow:row column:&acol cells:&acells];
261 if (NSNotFound == range.location) {
262 #if MM_TS_PARANOIA_LOG
263 ASLogErr(@"INTERNAL ERROR: Out of bounds");
268 // Create dictionary of attributes to apply to the new characters.
269 NSFont *theFont = font;
270 if (flags & DRAW_WIDE) {
271 if (flags & DRAW_BOLD)
272 theFont = flags & DRAW_ITALIC ? boldItalicFontWide : boldFontWide;
273 else if (flags & DRAW_ITALIC)
274 theFont = italicFontWide;
278 if (flags & DRAW_BOLD)
279 theFont = flags & DRAW_ITALIC ? boldItalicFont : boldFont;
280 else if (flags & DRAW_ITALIC)
281 theFont = italicFont;
284 NSMutableDictionary *attributes =
285 [NSMutableDictionary dictionaryWithObjectsAndKeys:
286 theFont, NSFontAttributeName,
287 bg, NSBackgroundColorAttributeName,
288 fg, NSForegroundColorAttributeName,
289 sp, NSUnderlineColorAttributeName,
290 [NSNumber numberWithInt:0], NSLigatureAttributeName,
293 if (flags & DRAW_UNDERL) {
294 NSNumber *value = [NSNumber numberWithInt:(NSUnderlineStyleSingle
295 | NSUnderlinePatternSolid)]; // | NSUnderlineByWordMask
296 [attributes setObject:value forKey:NSUnderlineStyleAttributeName];
299 if (flags & DRAW_UNDERC) {
300 // TODO: figure out how do draw proper undercurls
301 NSNumber *value = [NSNumber numberWithInt:(NSUnderlineStyleThick
302 | NSUnderlinePatternDot)]; // | NSUnderlineByWordMask
303 [attributes setObject:value forKey:NSUnderlineStyleAttributeName];
306 // Mark these characters as wide. This attribute is subsequently checked
307 // when translating (row,col) pairs to offsets within 'attribString'.
308 if (flags & DRAW_WIDE)
309 [attributes setObject:[NSNull null]
310 forKey:MMWideCharacterAttributeName];
312 // Replace characters in text storage and apply new attributes.
313 NSRange r = NSMakeRange(range.location, [string length]);
314 [attribString replaceCharactersInRange:range withString:string];
315 [attribString setAttributes:attributes range:r];
317 NSInteger changeInLength = [string length] - range.length;
318 if (acells != cells || acol != col) {
319 if (acells == cells + 1) {
320 // NOTE: A normal width character replaced a double width
321 // character. To maintain the invariant that each row covers the
322 // same amount of cells, we compensate by adding an empty column.
323 [attribString replaceCharactersInRange:NSMakeRange(NSMaxRange(r),0)
324 withAttributedString:[emptyRowString
325 attributedSubstringFromRange:NSMakeRange(0,1)]];
328 } else if (acol == col - 1) {
329 [attribString replaceCharactersInRange:NSMakeRange(r.location,0)
330 withAttributedString:[emptyRowString
331 attributedSubstringFromRange:NSMakeRange(0,1)]];
333 } else if (acol == col + 1) {
334 [attribString replaceCharactersInRange:NSMakeRange(r.location-1,1)
335 withAttributedString:[emptyRowString
336 attributedSubstringFromRange:NSMakeRange(0,2)]];
340 // NOTE: It seems that this never gets called. If it ever does,
341 // then there is another case to treat.
342 #if MM_TS_PARANOIA_LOG
343 ASLogWarn(@"row=%d col=%d acol=%d cells=%d acells=%d", row, col,
344 acol, cells, acells);
349 if ((flags & DRAW_WIDE) || [string length] != cells)
350 characterEqualsColumn = NO;
352 [self fixInvalidCharactersInRange:r];
355 ASLogDebug(@"length=%d row=%d col=%d cells=%d replaceRange=%@ change=%d",
356 [string length], row, col, cells,
357 NSStringFromRange(r), changeInLength);
359 [self edited:(NSTextStorageEditedCharacters|NSTextStorageEditedAttributes)
360 range:range changeInLength:changeInLength];
363 rowCache[row].length += changeInLength;
368 * Delete 'count' lines from 'row' and insert 'count' empty lines at the bottom
369 * of the scroll region.
371 - (void)deleteLinesFromRow:(int)row lineCount:(int)count
372 scrollBottom:(int)bottom left:(int)left right:(int)right
373 color:(NSColor *)color
375 [self lazyResize:NO];
377 if (row < 0 || row+count > maxRows || bottom > maxRows || left < 0
378 || right > maxColumns)
381 int total = 1 + bottom - row;
382 int move = total - count;
383 int width = right - left + 1;
385 NSRange destRange, srcRange;
388 for (i = 0; i < move; ++i, ++destRow) {
391 destRange = [self charRangeForRow:destRow column:&acol cells:&acells];
392 #if MM_TS_PARANOIA_LOG
393 if (acells != width || acol != left)
394 ASLogErr(@"INTERNAL ERROR");
397 acol = left; acells = width;
398 srcRange = [self charRangeForRow:(destRow+count) column:&acol
400 #if MM_TS_PARANOIA_LOG
401 if (acells != width || acol != left)
402 ASLogErr(@"INTERNAL ERROR");
405 if (NSNotFound == destRange.location || NSNotFound == srcRange.location)
407 #if MM_TS_PARANOIA_LOG
408 ASLogErr(@"INTERNAL ERROR: Out of bounds");
413 NSAttributedString *srcString = [attribString
414 attributedSubstringFromRange:srcRange];
416 [attribString replaceCharactersInRange:destRange
417 withAttributedString:srcString];
418 [self edited:(NSTextStorageEditedCharacters
419 | NSTextStorageEditedAttributes) range:destRange
420 changeInLength:([srcString length]-destRange.length)];
423 rowCache[destRow].length += [srcString length] - destRange.length;
427 NSRange emptyRange = {0,width};
428 NSAttributedString *emptyString =
429 [emptyRowString attributedSubstringFromRange:emptyRange];
430 NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
431 font, NSFontAttributeName,
432 color, NSBackgroundColorAttributeName, nil];
434 for (i = 0; i < count; ++i, ++destRow) {
437 destRange = [self charRangeForRow:destRow column:&acol cells:&acells];
438 #if MM_TS_PARANOIA_LOG
439 if (acells != width || acol != left)
440 ASLogErr(@"INTERNAL ERROR");
442 if (NSNotFound == destRange.location) {
443 #if MM_TS_PARANOIA_LOG
444 ASLogErr(@"INTERNAL ERROR: Out of bounds");
449 [attribString replaceCharactersInRange:destRange
450 withAttributedString:emptyString];
451 [attribString setAttributes:attribs
452 range:NSMakeRange(destRange.location, width)];
454 [self edited:(NSTextStorageEditedAttributes
455 | NSTextStorageEditedCharacters) range:destRange
456 changeInLength:([emptyString length]-destRange.length)];
459 rowCache[destRow].length += [emptyString length] - destRange.length;
465 * Insert 'count' empty lines at 'row' and delete 'count' lines from the bottom
466 * of the scroll region.
468 - (void)insertLinesAtRow:(int)row lineCount:(int)count
469 scrollBottom:(int)bottom left:(int)left right:(int)right
470 color:(NSColor *)color
472 [self lazyResize:NO];
474 if (row < 0 || row+count > maxRows || bottom > maxRows || left < 0
475 || right > maxColumns)
478 int total = 1 + bottom - row;
479 int move = total - count;
480 int width = right - left + 1;
481 int destRow = bottom;
482 int srcRow = row + move - 1;
483 NSRange destRange, srcRange;
486 for (i = 0; i < move; ++i, --destRow, --srcRow) {
489 destRange = [self charRangeForRow:destRow column:&acol cells:&acells];
490 #if MM_TS_PARANOIA_LOG
491 if (acells != width || acol != left)
492 ASLogErr(@"INTERNAL ERROR");
495 acol = left; acells = width;
496 srcRange = [self charRangeForRow:srcRow column:&acol cells:&acells];
497 #if MM_TS_PARANOIA_LOG
498 if (acells != width || acol != left)
499 ASLogErr(@"INTERNAL ERROR");
501 if (NSNotFound == destRange.location || NSNotFound == srcRange.location)
503 #if MM_TS_PARANOIA_LOG
504 ASLogErr(@"INTERNAL ERROR: Out of bounds");
509 NSAttributedString *srcString = [attribString
510 attributedSubstringFromRange:srcRange];
511 [attribString replaceCharactersInRange:destRange
512 withAttributedString:srcString];
513 [self edited:(NSTextStorageEditedCharacters
514 | NSTextStorageEditedAttributes) range:destRange
515 changeInLength:([srcString length]-destRange.length)];
518 rowCache[destRow].length += [srcString length] - destRange.length;
522 NSRange emptyRange = {0,width};
523 NSAttributedString *emptyString =
524 [emptyRowString attributedSubstringFromRange:emptyRange];
525 NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
526 font, NSFontAttributeName,
527 color, NSBackgroundColorAttributeName, nil];
529 for (i = 0; i < count; ++i, --destRow) {
532 destRange = [self charRangeForRow:destRow column:&acol cells:&acells];
533 #if MM_TS_PARANOIA_LOG
534 if (acells != width || acol != left)
535 ASLogErr(@"INTERNAL ERROR");
537 if (NSNotFound == destRange.location) {
538 #if MM_TS_PARANOIA_LOG
539 ASLogErr(@"INTERNAL ERROR: Out of bounds");
544 [attribString replaceCharactersInRange:destRange
545 withAttributedString:emptyString];
546 [attribString setAttributes:attribs
547 range:NSMakeRange(destRange.location, width)];
549 [self edited:(NSTextStorageEditedAttributes
550 | NSTextStorageEditedCharacters) range:destRange
551 changeInLength:([emptyString length]-destRange.length)];
554 rowCache[destRow].length += [emptyString length] - destRange.length;
559 - (void)clearBlockFromRow:(int)row1 column:(int)col1 toRow:(int)row2
560 column:(int)col2 color:(NSColor *)color
562 [self lazyResize:NO];
564 if (row1 < 0 || row2 >= maxRows || col1 < 0 || col2 > maxColumns)
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];
576 for (r=row1; r<=row2; ++r) {
579 range = [self charRangeForRow:r column:&acol cells:&acells];
580 #if MM_TS_PARANOIA_LOG
581 if (acells != cells || acol != col1)
582 ASLogErr(@"INTERNAL ERROR");
584 if (NSNotFound == range.location) {
585 #if MM_TS_PARANOIA_LOG
586 ASLogErr(@"INTERNAL ERROR: Out of bounds");
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];
601 rowCache[r].length += cells - range.length;
608 [self lazyResize:YES];
611 - (void)setDefaultColorsBackground:(NSColor *)bgColor
612 foreground:(NSColor *)fgColor
614 if (defaultBackgroundColor != bgColor) {
615 [defaultBackgroundColor release];
616 defaultBackgroundColor = bgColor ? [bgColor retain] : nil;
619 // NOTE: The default foreground color isn't actually used for anything, but
620 // other class instances might want to be able to access it so it is stored
622 if (defaultForegroundColor != fgColor) {
623 [defaultForegroundColor release];
624 defaultForegroundColor = fgColor ? [fgColor retain] : nil;
628 - (void)setFont:(NSFont*)newFont
630 if (newFont && font != newFont) {
631 [boldItalicFont release]; boldItalicFont = nil;
632 [italicFont release]; italicFont = nil;
633 [boldFont release]; boldFont = nil;
634 [font release]; font = nil;
636 // NOTE! When setting a new font we make sure that the advancement of
637 // each glyph is fixed.
639 float em = [@"m" sizeWithAttributes:
640 [NSDictionary dictionaryWithObject:newFont
641 forKey:NSFontAttributeName]].width;
642 float cellWidthMultiplier = [[NSUserDefaults standardUserDefaults]
643 floatForKey:MMCellWidthMultiplierKey];
645 // NOTE! Even though NSFontFixedAdvanceAttribute is a float, it will
646 // only render at integer sizes. Hence, we restrict the cell width to
647 // an integer here, otherwise the window width and the actual text
648 // width will not match.
649 cellSize.width = ceilf(em * cellWidthMultiplier);
651 float pointSize = [newFont pointSize];
652 NSDictionary *dict = [NSDictionary
653 dictionaryWithObject:[NSNumber numberWithFloat:cellSize.width]
654 forKey:NSFontFixedAdvanceAttribute];
656 NSFontDescriptor *desc = [newFont fontDescriptor];
657 desc = [desc fontDescriptorByAddingAttributes:dict];
658 font = [NSFont fontWithDescriptor:desc size:pointSize];
661 NSLayoutManager *lm = [[self layoutManagers] objectAtIndex:0];
663 cellSize.height = linespace + [lm defaultLineHeightForFont:font];
665 // Should never happen, set some bogus value for cell height.
666 ASLogWarn(@"No layout manager available");
667 cellSize.height = linespace + 16.0;
670 // NOTE: The font manager does not care about the 'font fixed advance'
671 // attribute, so after converting the font we have to add this
673 boldFont = [[NSFontManager sharedFontManager]
674 convertFont:font toHaveTrait:NSBoldFontMask];
675 desc = [boldFont fontDescriptor];
676 desc = [desc fontDescriptorByAddingAttributes:dict];
677 boldFont = [NSFont fontWithDescriptor:desc size:pointSize];
680 italicFont = [[NSFontManager sharedFontManager]
681 convertFont:font toHaveTrait:NSItalicFontMask];
682 desc = [italicFont fontDescriptor];
683 desc = [desc fontDescriptorByAddingAttributes:dict];
684 italicFont = [NSFont fontWithDescriptor:desc size:pointSize];
687 boldItalicFont = [[NSFontManager sharedFontManager]
688 convertFont:italicFont toHaveTrait:NSBoldFontMask];
689 desc = [boldItalicFont fontDescriptor];
690 desc = [desc fontDescriptorByAddingAttributes:dict];
691 boldItalicFont = [NSFont fontWithDescriptor:desc size:pointSize];
692 [boldItalicFont retain];
696 - (void)setWideFont:(NSFont *)newFont
699 // Use the normal font as the wide font (note that the normal font may
700 // very well include wide characters.)
701 if (font) [self setWideFont:font];
702 } else if (newFont != fontWide) {
703 [boldItalicFontWide release]; boldItalicFontWide = nil;
704 [italicFontWide release]; italicFontWide = nil;
705 [boldFontWide release]; boldFontWide = nil;
706 [fontWide release]; fontWide = nil;
708 float pointSize = [newFont pointSize];
709 NSFontDescriptor *desc = [newFont fontDescriptor];
710 NSDictionary *dictWide = [NSDictionary
711 dictionaryWithObject:[NSNumber numberWithFloat:2*cellSize.width]
712 forKey:NSFontFixedAdvanceAttribute];
714 desc = [desc fontDescriptorByAddingAttributes:dictWide];
715 fontWide = [NSFont fontWithDescriptor:desc size:pointSize];
718 boldFontWide = [[NSFontManager sharedFontManager]
719 convertFont:fontWide toHaveTrait:NSBoldFontMask];
720 desc = [boldFontWide fontDescriptor];
721 desc = [desc fontDescriptorByAddingAttributes:dictWide];
722 boldFontWide = [NSFont fontWithDescriptor:desc size:pointSize];
723 [boldFontWide retain];
725 italicFontWide = [[NSFontManager sharedFontManager]
726 convertFont:fontWide toHaveTrait:NSItalicFontMask];
727 desc = [italicFontWide fontDescriptor];
728 desc = [desc fontDescriptorByAddingAttributes:dictWide];
729 italicFontWide = [NSFont fontWithDescriptor:desc size:pointSize];
730 [italicFontWide retain];
732 boldItalicFontWide = [[NSFontManager sharedFontManager]
733 convertFont:italicFontWide toHaveTrait:NSBoldFontMask];
734 desc = [boldItalicFontWide fontDescriptor];
735 desc = [desc fontDescriptorByAddingAttributes:dictWide];
736 boldItalicFontWide = [NSFont fontWithDescriptor:desc size:pointSize];
737 [boldItalicFontWide retain];
751 - (NSColor *)defaultBackgroundColor
753 return defaultBackgroundColor;
756 - (NSColor *)defaultForegroundColor
758 return defaultForegroundColor;
763 return NSMakeSize(maxColumns*cellSize.width, maxRows*cellSize.height);
771 - (NSRect)rectForRowsInRange:(NSRange)range
773 NSRect rect = { {0, 0}, {0, 0} };
774 unsigned start = range.location > maxRows ? maxRows : range.location;
775 unsigned length = range.length;
777 if (start+length > maxRows)
778 length = maxRows - start;
780 rect.origin.y = cellSize.height * start;
781 rect.size.height = cellSize.height * length;
786 - (NSRect)rectForColumnsInRange:(NSRange)range
788 NSRect rect = { {0, 0}, {0, 0} };
789 unsigned start = range.location > maxColumns ? maxColumns : range.location;
790 unsigned length = range.length;
792 if (start+length > maxColumns)
793 length = maxColumns - start;
795 rect.origin.x = cellSize.width * start;
796 rect.size.width = cellSize.width * length;
801 - (NSUInteger)characterIndexForRow:(int)row column:(int)col
804 NSRange range = [self charRangeForRow:row column:&col cells:&cells];
805 return range.location != NSNotFound ? range.location : 0;
808 // XXX: unused at the moment
809 - (BOOL)resizeToFitSize:(NSSize)size
811 int rows = maxRows, cols = maxColumns;
813 [self fitToSize:size rows:&rows columns:&cols];
814 if (rows != maxRows || cols != maxColumns) {
815 [self setMaxRows:rows columns:cols];
819 // Return NO only if dimensions did not change.
823 - (NSSize)fitToSize:(NSSize)size
825 return [self fitToSize:size rows:NULL columns:NULL];
828 - (NSSize)fitToSize:(NSSize)size rows:(int *)rows columns:(int *)columns
830 NSSize curSize = [self size];
831 NSSize fitSize = curSize;
832 int fitRows = maxRows;
833 int fitCols = maxColumns;
835 if (size.height < curSize.height) {
836 // Remove lines until the height of the text storage fits inside
837 // 'size'. However, always make sure there are at least 3 lines in the
838 // text storage. (Why 3? It seem Vim never allows less than 3 lines.)
840 // TODO: No need to search since line height is fixed, just calculate
842 int rowCount = maxRows;
844 for (rowsToRemove = 0; rowsToRemove < maxRows-3; ++rowsToRemove) {
845 float height = cellSize.height*rowCount;
847 if (height <= size.height) {
848 fitSize.height = height;
855 fitRows -= rowsToRemove;
856 } else if (size.height > curSize.height) {
857 float fh = cellSize.height;
858 if (fh < 1.0f) fh = 1.0f;
860 fitRows = floor(size.height/fh);
861 fitSize.height = fh*fitRows;
864 if (size.width != curSize.width) {
865 float fw = cellSize.width;
866 if (fw < 1.0f) fw = 1.0f;
868 fitCols = floor(size.width/fw);
869 fitSize.width = fw*fitCols;
872 if (rows) *rows = fitRows;
873 if (columns) *columns = fitCols;
878 - (NSRect)boundingRectForCharacterAtRow:(int)row column:(int)col
881 // This properly computes the position of where Vim expects the glyph to be
882 // drawn. Had the typesetter actually computed the right position of each
883 // character and not hidden some, this code would be correct.
884 NSRect rect = NSZeroRect;
886 rect.origin.x = col*cellSize.width;
887 rect.origin.y = row*cellSize.height;
888 rect.size = cellSize;
890 // Wide character take up twice the width of a normal character.
892 NSRange r = [self charRangeForRow:row column:&col cells:&cells];
893 if (NSNotFound != r.location
894 && [attribString attribute:MMWideCharacterAttributeName
897 rect.size.width += rect.size.width;
901 // Use layout manager to compute bounding rect. This works in situations
902 // where the layout manager decides to hide glyphs (Vim assumes all glyphs
904 NSLayoutManager *lm = [[self layoutManagers] objectAtIndex:0];
905 NSTextContainer *tc = [[lm textContainers] objectAtIndex:0];
907 NSRange range = [self charRangeForRow:row column:&col cells:&cells];
908 NSRange glyphRange = [lm glyphRangeForCharacterRange:range
909 actualCharacterRange:NULL];
911 return [lm boundingRectForGlyphRange:glyphRange inTextContainer:tc];
916 - (MMRowCacheEntry *)rowCache
922 @end // MMTextStorage
927 @implementation MMTextStorage (Private)
929 - (void)lazyResize:(BOOL)force
931 // Do nothing if the dimensions are already right.
932 if (!force && actualRows == maxRows && actualColumns == maxColumns)
935 NSRange oldRange = NSMakeRange(0, [attribString length]);
937 actualRows = maxRows;
938 actualColumns = maxColumns;
939 characterEqualsColumn = YES;
943 rowCache = (MMRowCacheEntry*)calloc(actualRows, sizeof(MMRowCacheEntry));
947 if (defaultBackgroundColor) {
948 dict = [NSDictionary dictionaryWithObjectsAndKeys:
949 font, NSFontAttributeName,
950 defaultBackgroundColor, NSBackgroundColorAttributeName, nil];
952 dict = [NSDictionary dictionaryWithObjectsAndKeys:
953 font, NSFontAttributeName, nil];
956 NSMutableString *rowString = [NSMutableString string];
958 for (i = 0; i < maxColumns; ++i) {
959 [rowString appendString:@" "];
961 [rowString appendString:@"\n"];
963 [emptyRowString release];
964 emptyRowString = [[NSAttributedString alloc] initWithString:rowString
967 [attribString release];
968 attribString = [[NSMutableAttributedString alloc] init];
969 for (i=0; i<maxRows; ++i) {
971 rowCache[i].length = actualColumns + 1;
973 [attribString appendAttributedString:emptyRowString];
976 NSRange fullRange = NSMakeRange(0, [attribString length]);
977 [self edited:(NSTextStorageEditedCharacters|NSTextStorageEditedAttributes)
978 range:oldRange changeInLength:fullRange.length-oldRange.length];
981 - (NSRange)charRangeForRow:(int)row column:(int*)pcol cells:(int*)pcells
986 // If no wide chars are used and if every char has length 1 (no composing
987 // characters, no > 16 bit characters), then we can compute the range.
988 if (characterEqualsColumn)
989 return NSMakeRange(row*(actualColumns+1) + col, cells);
991 NSString *string = [attribString string];
992 unsigned stringLen = [string length];
993 NSRange r, range = { NSNotFound, 0 };
997 if (row < 0 || row >= actualRows || col < 0 || col >= actualColumns
998 || col+cells > actualColumns) {
999 #if MM_TS_PARANOIA_LOG
1000 ASLogErr(@"row=%d col=%d cells=%d is out of range (length=%d)",
1001 row, col, cells, stringLen);
1006 #if MM_USE_ROW_CACHE
1007 // Locate the beginning of the row
1008 MMRowCacheEntry *cache = rowCache;
1010 for (i = 0; i < row; ++i, ++cache)
1011 idx += cache->length;
1013 int rowEnd = idx + cache->length;
1015 // Locate the beginning of the row by scanning for EOL characters.
1017 for (i = 0; i < row; ++i) {
1018 r.length = stringLen - r.location;
1019 r = [string rangeOfString:@"\n" options:NSLiteralSearch range:r];
1020 if (NSNotFound == r.location)
1026 // Locate the column
1027 #if MM_USE_ROW_CACHE
1028 cache = &rowCache[row];
1033 idx += cache->colOffset;
1035 range.location = idx;
1037 #if 0 // Backward search seems to be broken...
1039 if (col < i - col) {
1040 // Search forward from beginning of line.
1042 } else if (actualColumns - col < col - i) {
1043 // Search backward from end of line.
1044 i = actualColumns - 1;
1045 idx += cache->length - 2;
1047 // Search from cache spot (forward or backward).
1048 idx += cache->colOffset;
1054 if (idx >= stringLen)
1055 return NSMakeRange(NSNotFound, 0);
1056 r = [string rangeOfComposedCharacterSequenceAtIndex:idx];
1058 // Wide chars take up two display cells.
1059 if ([attribString attribute:MMWideCharacterAttributeName
1061 effectiveRange:nil])
1067 } else if (col < i) {
1070 if (idx-1 >= stringLen)
1071 return NSMakeRange(NSNotFound, 0);
1072 r = [string rangeOfComposedCharacterSequenceAtIndex:idx-1];
1076 // Wide chars take up two display cells.
1077 if ([attribString attribute:MMWideCharacterAttributeName
1079 effectiveRange:nil])
1086 cache->colOffset = idx - range.location;
1090 // Search forward from beginning of line.
1093 // Search forward from cache spot.
1094 idx += cache->colOffset;
1099 if (idx >= stringLen)
1100 return NSMakeRange(NSNotFound, 0);
1101 r = [string rangeOfComposedCharacterSequenceAtIndex:idx];
1103 // Wide chars take up two display cells.
1104 if ([attribString attribute:MMWideCharacterAttributeName
1106 effectiveRange:nil])
1115 cache->colOffset = idx - range.location;
1120 for (i = 0; i < col; ++i) {
1121 if (idx >= stringLen)
1122 return NSMakeRange(NSNotFound, 0);
1123 r = [string rangeOfComposedCharacterSequenceAtIndex:idx];
1125 // Wide chars take up two display cells.
1126 if ([attribString attribute:MMWideCharacterAttributeName
1128 effectiveRange:nil])
1135 // Count the number of characters that cover the cells.
1136 range.location = idx;
1137 for (i = 0; i < cells; ++i) {
1138 if (idx >= stringLen)
1139 return NSMakeRange(NSNotFound, 0);
1140 r = [string rangeOfComposedCharacterSequenceAtIndex:idx];
1142 // Wide chars take up two display cells.
1143 if ([attribString attribute:MMWideCharacterAttributeName
1145 effectiveRange:nil])
1149 range.length += r.length;
1154 #if MM_TS_PARANOIA_LOG
1155 #if MM_USE_ROW_CACHE
1156 if (range.location >= rowEnd-1) {
1157 ASLogErr(@"INTERNAL ERROR: row=%d col=%d cells=%d --> range=%@",
1158 row, col, cells, NSStringFromRange(range));
1159 range.location = rowEnd - 2;
1161 } else if (NSMaxRange(range) >= rowEnd) {
1162 ASLogErr(@"INTERNAL ERROR: row=%d col=%d cells=%d --> range=%@",
1163 row, col, cells, NSStringFromRange(range));
1164 range.length = rowEnd - range.location - 1;
1168 if (NSMaxRange(range) > stringLen) {
1169 ASLogErr(@"INTERNAL ERROR: row=%d col=%d cells=%d --> range=%@",
1170 row, col, cells, NSStringFromRange(range));
1171 range.location = NSNotFound;
1179 - (void)fixInvalidCharactersInRange:(NSRange)range
1181 static NSCharacterSet *invalidCharacterSet = nil;
1182 NSRange invalidRange;
1185 if (!invalidCharacterSet)
1186 invalidCharacterSet = [[NSCharacterSet characterSetWithRange:
1187 NSMakeRange(0x2028, 2)] retain];
1189 // HACK! Replace characters that the text system can't handle (currently
1190 // LINE SEPARATOR U+2028 and PARAGRAPH SEPARATOR U+2029) with space.
1192 // TODO: Treat these separately inside of Vim so we don't have to bother
1194 while (range.length > 0) {
1195 invalidRange = [[attribString string]
1196 rangeOfCharacterFromSet:invalidCharacterSet
1197 options:NSLiteralSearch
1199 if (NSNotFound == invalidRange.location)
1202 [attribString replaceCharactersInRange:invalidRange withString:@" "];
1204 end = NSMaxRange(invalidRange);
1205 range.length -= end - range.location;
1206 range.location = end;
1210 @end // MMTextStorage (Private)