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.
11 #import "MMTextStorage.h"
17 // TODO: support DRAW_TRANSP flag
18 #define DRAW_TRANSP 0x01 /* draw with transparant bg */
19 #define DRAW_BOLD 0x02 /* draw bold text */
20 #define DRAW_UNDERL 0x04 /* draw underline text */
21 #define DRAW_UNDERC 0x08 /* draw undercurl text */
22 #define DRAW_ITALIC 0x10 /* draw italic text */
27 @interface MMTextStorage (Private)
28 - (void)lazyResize:(BOOL)force;
33 @implementation MMTextStorage
37 if ((self = [super init])) {
38 attribString = [[NSMutableAttributedString alloc] initWithString:@""];
39 // NOTE! It does not matter which font is set here, Vim will set its
40 // own font on startup anyway. Just set some bogus values.
41 font = [[NSFont userFixedPitchFontOfSize:0] retain];
42 boldFont = [font retain];
43 italicFont = [font retain];
44 boldItalicFont = [font retain];
45 cellSize.height = [font pointSize];
46 cellSize.width = [font defaultLineHeightForFont];
54 //NSLog(@"%@ %s", [self className], _cmd);
56 [emptyRowString release];
57 [boldItalicFont release];
61 [defaultBackgroundColor release];
62 [defaultForegroundColor release];
63 [attribString release];
69 //NSLog(@"%s : attribString=%@", _cmd, attribString);
70 return [attribString string];
73 - (NSDictionary *)attributesAtIndex:(unsigned)index
74 effectiveRange:(NSRangePointer)range
77 if (index>=[attribString length]) {
78 //NSLog(@"%sWARNING: index (%d) out of bounds", _cmd, index);
80 *range = NSMakeRange(NSNotFound, 0);
82 return [NSDictionary dictionary];
85 return [attribString attributesAtIndex:index effectiveRange:range];
88 - (void)replaceCharactersInRange:(NSRange)range
89 withString:(NSString *)string
91 //NSLog(@"replaceCharactersInRange:(%d,%d) withString:%@", range.location,
92 // range.length, string);
93 NSLog(@"WARNING: calling %s on MMTextStorage is unsupported", _cmd);
94 //[attribString replaceCharactersInRange:range withString:string];
97 - (void)setAttributes:(NSDictionary *)attributes range:(NSRange)range
99 // NOTE! This method must be implemented since the text system calls it
100 // constantly to 'fix attributes', apply font substitution, etc.
102 [attribString setAttributes:attributes range:range];
104 // HACK! If the font attribute is being modified, then ensure that the new
105 // font has a fixed advancement which is either the same as the current
106 // font or twice that, depending on whether it is a 'wide' character that
107 // is being fixed or not. This code really only works if 'range' has
109 NSFont *newFont = [attributes objectForKey:NSFontAttributeName];
111 float adv = cellSize.width;
112 if ([attribString length] > range.location+1) {
113 // If the first char is followed by zero-width space, then it is a
114 // 'wide' character, so double the advancement.
115 NSString *string = [attribString string];
116 if ([string characterAtIndex:range.location+1] == 0x200b)
120 // Create a new font which has the 'fixed advance attribute' set.
121 NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
122 [NSNumber numberWithFloat:adv], NSFontFixedAdvanceAttribute, nil];
123 NSFontDescriptor *desc = [newFont fontDescriptor];
124 desc = [desc fontDescriptorByAddingAttributes:dict];
125 newFont = [NSFont fontWithDescriptor:desc size:[newFont pointSize]];
127 // Now modify the 'attributes' dictionary to hold the new font.
128 NSMutableDictionary *newAttr = [NSMutableDictionary
129 dictionaryWithDictionary:attributes];
130 [newAttr setObject:newFont forKey:NSFontAttributeName];
132 [attribString setAttributes:newAttr range:range];
134 [attribString setAttributes:attributes range:range];
156 return actualColumns;
164 - (void)setLinespace:(float)newLinespace
166 NSLayoutManager *lm = [[self layoutManagers] objectAtIndex:0];
168 linespace = newLinespace;
170 // NOTE: The linespace is added to the cell height in order for a multiline
171 // selection not to have white (background color) gaps between lines. Also
172 // this simplifies the code a lot because there is no need to check the
173 // linespace when calculating the size of the text view etc. When the
174 // linespace is non-zero the baseline will be adjusted as well; check
176 cellSize.height = linespace + (lm ? [lm defaultLineHeightForFont:font]
177 : [font defaultLineHeightForFont]);
180 - (void)getMaxRows:(int*)rows columns:(int*)cols
182 if (rows) *rows = maxRows;
183 if (cols) *cols = maxColumns;
186 - (void)setMaxRows:(int)rows columns:(int)cols
188 // NOTE: Just remember the new values, the actual resizing is done lazily.
193 - (void)replaceString:(NSString *)string atRow:(int)row column:(int)col
194 withFlags:(int)flags foregroundColor:(NSColor *)fg
195 backgroundColor:(NSColor *)bg specialColor:(NSColor *)sp
197 //NSLog(@"replaceString:atRow:%d column:%d withFlags:%d "
198 // "foreground:%@ background:%@ special:%@",
199 // row, col, flags, fg, bg, sp);
200 [self lazyResize:NO];
202 if (row < 0 || row >= maxRows || col < 0 || col >= maxColumns
203 || col+[string length] > maxColumns) {
204 //NSLog(@"[%s] WARNING : out of range, row=%d (%d) col=%d (%d) "
205 // "length=%d (%d)", _cmd, row, maxRows, col, maxColumns,
206 // [string length], [attribString length]);
210 // NOTE: If 'string' was initialized with bad data it might be nil; this
211 // may be due to 'enc' being set to an unsupported value, so don't print an
212 // error message or stdout will most likely get flooded.
215 if (!(fg && bg && sp)) {
216 NSLog(@"[%s] WARNING: background, foreground or special color not "
221 NSRange range = NSMakeRange(col+row*(maxColumns+1), [string length]);
222 [attribString replaceCharactersInRange:range withString:string];
224 NSFont *theFont = font;
225 if (flags & DRAW_BOLD)
226 theFont = flags & DRAW_ITALIC ? boldItalicFont : boldFont;
227 else if (flags & DRAW_ITALIC)
228 theFont = italicFont;
230 NSDictionary *attributes;
232 if (flags & DRAW_UNDERC) {
233 // move the undercurl down a bit so it is visible
234 attributes = [NSDictionary dictionaryWithObjectsAndKeys:
235 theFont, NSFontAttributeName,
236 bg, NSBackgroundColorAttributeName,
237 fg, NSForegroundColorAttributeName,
238 sp, NSUnderlineColorAttributeName,
239 [NSNumber numberWithFloat:2],NSBaselineOffsetAttributeName,
241 } else if (flags & DRAW_TRANSP) {
242 // Don't include background color when DRAW_TRANSP is set.
244 // NOTE: I'm not sure what the point of this is, but if DRAW_TRANSP
245 // calls are ignored, then cursor blinking in insert mode doesn't work.
246 attributes = [NSDictionary dictionaryWithObjectsAndKeys:
247 theFont, NSFontAttributeName,
248 fg, NSForegroundColorAttributeName,
249 sp, NSUnderlineColorAttributeName,
252 attributes = [NSDictionary dictionaryWithObjectsAndKeys:
253 theFont, NSFontAttributeName,
254 bg, NSBackgroundColorAttributeName,
255 fg, NSForegroundColorAttributeName,
256 sp, NSUnderlineColorAttributeName,
259 [attribString setAttributes:attributes range:range];
261 if (flags & DRAW_UNDERL) {
262 NSNumber *value = [NSNumber numberWithInt:(NSUnderlineStyleSingle
263 | NSUnderlinePatternSolid)]; // | NSUnderlineByWordMask
264 [attribString addAttribute:NSUnderlineStyleAttributeName
265 value:value range:range];
268 // TODO: figure out how do draw proper undercurls
269 if (flags & DRAW_UNDERC) {
270 NSNumber *value = [NSNumber numberWithInt:(NSUnderlineStyleThick
271 | NSUnderlinePatternDot)]; // | NSUnderlineByWordMask
272 [attribString addAttribute:NSUnderlineStyleAttributeName
273 value:value range:range];
276 [self edited:(NSTextStorageEditedCharacters|NSTextStorageEditedAttributes)
277 range:range changeInLength:0];
281 * Delete 'count' lines from 'row' and insert 'count' empty lines at the bottom
282 * of the scroll region.
284 - (void)deleteLinesFromRow:(int)row lineCount:(int)count
285 scrollBottom:(int)bottom left:(int)left right:(int)right
286 color:(NSColor *)color
288 //NSLog(@"deleteLinesFromRow:%d lineCount:%d color:%@", row, count, color);
289 [self lazyResize:NO];
291 if (row < 0 || row+count > maxRows) {
292 //NSLog(@"[%s] WARNING : out of range, row=%d (%d) count=%d", _cmd, row,
297 int total = 1 + bottom - row;
298 int move = total - count;
299 int width = right - left + 1;
300 NSRange destRange = { row*(maxColumns+1) + left, width };
301 NSRange srcRange = { (row+count)*(maxColumns+1) + left, width };
304 if (width != maxColumns) { // if this is the case, then left must be 0
305 for (i = 0; i < move; ++i) {
306 NSAttributedString *srcString = [attribString
307 attributedSubstringFromRange:srcRange];
308 [attribString replaceCharactersInRange:destRange
309 withAttributedString:srcString];
310 [self edited:(NSTextStorageEditedCharacters
311 | NSTextStorageEditedAttributes)
312 range:destRange changeInLength:0];
313 destRange.location += maxColumns+1;
314 srcRange.location += maxColumns+1;
317 NSRange emptyRange = {0,width};
318 NSAttributedString *emptyString =
319 [emptyRowString attributedSubstringFromRange: emptyRange];
320 NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
321 font, NSFontAttributeName,
322 color, NSBackgroundColorAttributeName, nil];
324 for (i = 0; i < count; ++i) {
325 [attribString replaceCharactersInRange:destRange
326 withAttributedString:emptyString];
327 [attribString setAttributes:attribs range:destRange];
328 [self edited:(NSTextStorageEditedAttributes
329 | NSTextStorageEditedCharacters) range:destRange
331 destRange.location += maxColumns+1;
334 NSRange delRange = {row*(maxColumns+1), count*(maxColumns+1)};
335 [attribString deleteCharactersInRange: delRange];
336 destRange.location += move*(maxColumns+1);
338 NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
339 font, NSFontAttributeName,
340 color, NSBackgroundColorAttributeName, nil];
341 destRange.length = maxColumns;
342 for (i = 0; i < count; ++i) {
343 [attribString insertAttributedString:emptyRowString
344 atIndex:destRange.location];
345 [attribString setAttributes:attribs range:destRange];
346 destRange.location += maxColumns+1;
348 NSRange editedRange = {row*(maxColumns+1),total*(maxColumns+1)};
349 [self edited:(NSTextStorageEditedAttributes
350 | NSTextStorageEditedCharacters) range:editedRange
356 * Insert 'count' empty lines at 'row' and delete 'count' lines from the bottom
357 * of the scroll region.
359 - (void)insertLinesAtRow:(int)row lineCount:(int)count
360 scrollBottom:(int)bottom left:(int)left right:(int)right
361 color:(NSColor *)color
363 //NSLog(@"insertLinesAtRow:%d lineCount:%d color:%@", row, count, color);
364 [self lazyResize:NO];
366 if (row < 0 || row+count > maxRows) {
367 //NSLog(@"[%s] WARNING : out of range, row=%d (%d) count=%d", _cmd, row,
372 int total = 1 + bottom - row;
373 int move = total - count;
374 int width = right - left + 1;
375 NSRange destRange = { bottom*(maxColumns+1) + left, width };
376 NSRange srcRange = { (row+move-1)*(maxColumns+1) + left, width };
379 if (width != maxColumns) { // if this is the case, then left must be 0
380 for (i = 0; i < move; ++i) {
381 NSAttributedString *srcString = [attribString
382 attributedSubstringFromRange:srcRange];
383 [attribString replaceCharactersInRange:destRange
384 withAttributedString:srcString];
385 [self edited:(NSTextStorageEditedCharacters
386 | NSTextStorageEditedAttributes)
387 range:destRange changeInLength:0];
388 destRange.location -= maxColumns+1;
389 srcRange.location -= maxColumns+1;
392 NSRange emptyRange = {0,width};
393 NSAttributedString *emptyString =
394 [emptyRowString attributedSubstringFromRange:emptyRange];
395 NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
396 font, NSFontAttributeName,
397 color, NSBackgroundColorAttributeName, nil];
399 for (i = 0; i < count; ++i) {
400 [attribString replaceCharactersInRange:destRange
401 withAttributedString:emptyString];
402 [attribString setAttributes:attribs range:destRange];
403 [self edited:(NSTextStorageEditedAttributes
404 | NSTextStorageEditedCharacters) range:destRange
406 destRange.location -= maxColumns+1;
409 NSRange delRange = {(row+move)*(maxColumns+1),count*(maxColumns+1)};
410 [attribString deleteCharactersInRange: delRange];
412 NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
413 font, NSFontAttributeName,
414 color, NSBackgroundColorAttributeName, nil];
416 destRange.location = row*(maxColumns+1);
417 for (i = 0; i < count; ++i) {
418 [attribString insertAttributedString:emptyRowString
419 atIndex:destRange.location];
420 [attribString setAttributes:attribs range:destRange];
422 NSRange editedRange = {row*(maxColumns+1),total*(maxColumns+1)};
423 [self edited:(NSTextStorageEditedAttributes
424 | NSTextStorageEditedCharacters) range:editedRange
429 - (void)clearBlockFromRow:(int)row1 column:(int)col1 toRow:(int)row2
430 column:(int)col2 color:(NSColor *)color
432 //NSLog(@"clearBlockFromRow:%d column:%d toRow:%d column:%d color:%@",
433 // row1, col1, row2, col2, color);
434 [self lazyResize:NO];
436 if (row1 < 0 || row2 >= maxRows || col1 < 0 || col2 > maxColumns) {
437 //NSLog(@"[%s] WARNING : out of range, row1=%d row2=%d (%d) col1=%d "
438 // "col2=%d (%d)", _cmd, row1, row2, maxRows, col1, col2,
443 NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
444 font, NSFontAttributeName,
445 color, NSBackgroundColorAttributeName, nil];
447 NSRange range = { row1*(maxColumns+1) + col1, col2-col1+1 };
449 NSRange emptyRange = {0,col2-col1+1};
450 NSAttributedString *emptyString =
451 [emptyRowString attributedSubstringFromRange:emptyRange];
453 for (r=row1; r<=row2; ++r) {
454 [attribString replaceCharactersInRange:range
455 withAttributedString:emptyString];
456 [attribString setAttributes:attribs range:range];
457 [self edited:(NSTextStorageEditedAttributes
458 | NSTextStorageEditedCharacters) range:range
460 range.location += maxColumns+1;
466 //NSLog(@"%s%@", _cmd, color);
467 [self lazyResize:YES];
470 - (void)setDefaultColorsBackground:(NSColor *)bgColor
471 foreground:(NSColor *)fgColor
473 //NSLog(@"setDefaultColorsBackground:%@ foreground:%@", bgColor, fgColor);
475 if (defaultBackgroundColor != bgColor) {
476 [defaultBackgroundColor release];
477 defaultBackgroundColor = bgColor ? [bgColor retain] : nil;
480 // NOTE: The default foreground color isn't actually used for anything, but
481 // other class instances might want to be able to access it so it is stored
483 if (defaultForegroundColor != fgColor) {
484 [defaultForegroundColor release];
485 defaultForegroundColor = fgColor ? [fgColor retain] : nil;
489 - (void)setFont:(NSFont*)newFont
491 if (newFont && font != newFont) {
494 // NOTE! When setting a new font we make sure that the advancement of
495 // each glyph is fixed.
497 float em = [newFont widthOfString:@"m"];
498 float cellWidthMultiplier = [[NSUserDefaults standardUserDefaults]
499 floatForKey:MMCellWidthMultiplierKey];
501 // NOTE! Even though NSFontFixedAdvanceAttribute is a float, it will
502 // only render at integer sizes. Hence, we restrict the cell width to
503 // an integer here, otherwise the window width and the actual text
504 // width will not match.
505 cellSize.width = ceilf(em * cellWidthMultiplier);
507 float pointSize = [newFont pointSize];
508 NSDictionary *dict = [NSDictionary
509 dictionaryWithObject:[NSNumber numberWithFloat:cellSize.width]
510 forKey:NSFontFixedAdvanceAttribute];
512 NSFontDescriptor *desc = [newFont fontDescriptor];
513 desc = [desc fontDescriptorByAddingAttributes:dict];
514 font = [NSFont fontWithDescriptor:desc size:pointSize];
517 NSLayoutManager *lm = [[self layoutManagers] objectAtIndex:0];
518 cellSize.height = linespace + (lm ? [lm defaultLineHeightForFont:font]
519 : [font defaultLineHeightForFont]);
521 // NOTE: The font manager does not care about the 'font fixed advance'
522 // attribute, so after converting the font we have to add this
524 boldFont = [[NSFontManager sharedFontManager]
525 convertFont:font toHaveTrait:NSBoldFontMask];
526 desc = [boldFont fontDescriptor];
527 desc = [desc fontDescriptorByAddingAttributes:dict];
528 boldFont = [NSFont fontWithDescriptor:desc size:pointSize];
531 italicFont = [[NSFontManager sharedFontManager]
532 convertFont:font toHaveTrait:NSItalicFontMask];
533 desc = [italicFont fontDescriptor];
534 desc = [desc fontDescriptorByAddingAttributes:dict];
535 italicFont = [NSFont fontWithDescriptor:desc size:pointSize];
538 boldItalicFont = [[NSFontManager sharedFontManager]
539 convertFont:italicFont toHaveTrait:NSBoldFontMask];
540 desc = [boldItalicFont fontDescriptor];
541 desc = [desc fontDescriptorByAddingAttributes:dict];
542 boldItalicFont = [NSFont fontWithDescriptor:desc size:pointSize];
543 [boldItalicFont retain];
552 - (NSColor *)defaultBackgroundColor
554 return defaultBackgroundColor;
557 - (NSColor *)defaultForegroundColor
559 return defaultForegroundColor;
564 return NSMakeSize(maxColumns*cellSize.width, maxRows*cellSize.height);
572 - (NSRect)rectForRowsInRange:(NSRange)range
574 NSRect rect = { 0, 0, 0, 0 };
575 unsigned start = range.location > maxRows ? maxRows : range.location;
576 unsigned length = range.length;
578 if (start+length > maxRows)
579 length = maxRows - start;
581 rect.origin.y = cellSize.height * start;
582 rect.size.height = cellSize.height * length;
587 - (NSRect)rectForColumnsInRange:(NSRange)range
589 NSRect rect = { 0, 0, 0, 0 };
590 unsigned start = range.location > maxColumns ? maxColumns : range.location;
591 unsigned length = range.length;
593 if (start+length > maxColumns)
594 length = maxColumns - start;
596 rect.origin.x = cellSize.width * start;
597 rect.size.width = cellSize.width * length;
602 - (unsigned)characterIndexForRow:(int)row column:(int)col
604 // Ensure the offset returned is valid.
605 // This code also works if maxRows and/or maxColumns is 0.
606 if (row >= maxRows) row = maxRows-1;
607 if (row < 0) row = 0;
608 if (col >= maxColumns) col = maxColumns-1;
609 if (col < 0) col = 0;
611 return (unsigned)(col + row*(maxColumns+1));
614 - (BOOL)resizeToFitSize:(NSSize)size
616 int rows = maxRows, cols = maxColumns;
618 [self fitToSize:size rows:&rows columns:&cols];
619 if (rows != maxRows || cols != maxColumns) {
620 [self setMaxRows:rows columns:cols];
624 // Return NO only if dimensions did not change.
628 - (NSSize)fitToSize:(NSSize)size
630 return [self fitToSize:size rows:NULL columns:NULL];
633 - (NSSize)fitToSize:(NSSize)size rows:(int *)rows columns:(int *)columns
635 NSSize curSize = [self size];
636 NSSize fitSize = curSize;
637 int fitRows = maxRows;
638 int fitCols = maxColumns;
640 if (size.height < curSize.height) {
641 // Remove lines until the height of the text storage fits inside
642 // 'size'. However, always make sure there are at least 3 lines in the
643 // text storage. (Why 3? It seem Vim never allows less than 3 lines.)
645 // TODO: No need to search since line height is fixed, just calculate
647 int rowCount = maxRows;
649 for (rowsToRemove = 0; rowsToRemove < maxRows-3; ++rowsToRemove) {
650 float height = cellSize.height*rowCount;
652 if (height <= size.height) {
653 fitSize.height = height;
660 fitRows -= rowsToRemove;
661 } else if (size.height > curSize.height) {
662 float fh = cellSize.height;
663 if (fh < 1.0f) fh = 1.0f;
665 fitRows = floor(size.height/fh);
666 fitSize.height = fh*fitRows;
669 if (size.width != curSize.width) {
670 float fw = cellSize.width;
671 if (fw < 1.0f) fw = 1.0f;
673 fitCols = floor(size.width/fw);
674 fitSize.width = fw*fitCols;
677 if (rows) *rows = fitRows;
678 if (columns) *columns = fitCols;
683 @end // MMTextStorage
688 @implementation MMTextStorage (Private)
689 - (void)lazyResize:(BOOL)force
693 // Do nothing if the dimensions are already right.
694 if (!force && actualRows == maxRows && actualColumns == maxColumns)
697 NSRange oldRange = NSMakeRange(0, actualRows*(actualColumns+1));
699 actualRows = maxRows;
700 actualColumns = maxColumns;
703 if (defaultBackgroundColor) {
704 dict = [NSDictionary dictionaryWithObjectsAndKeys:
705 font, NSFontAttributeName,
706 defaultBackgroundColor, NSBackgroundColorAttributeName, nil];
708 dict = [NSDictionary dictionaryWithObjectsAndKeys:
709 font, NSFontAttributeName, nil];
712 NSMutableString *rowString = [NSMutableString string];
713 for (i = 0; i < maxColumns; ++i) {
714 [rowString appendString:@" "];
716 [rowString appendString:@"\n"];
718 [emptyRowString release];
719 emptyRowString = [[NSAttributedString alloc] initWithString:rowString
722 [attribString release];
723 attribString = [[NSMutableAttributedString alloc] init];
724 for (i=0; i<maxRows; ++i) {
725 [attribString appendAttributedString:emptyRowString];
728 NSRange fullRange = NSMakeRange(0, [attribString length]);
729 [self edited:(NSTextStorageEditedCharacters|NSTextStorageEditedAttributes)
730 range:oldRange changeInLength:fullRange.length-oldRange.length];
733 @end // MMTextStorage (Private)