diff --git a/ChangeLog b/ChangeLog index 01add603c..155959228 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,9 @@ +2000-11-13 Richard Frith-Macdonald + + * Source/NSGAttributedString.m: partial rewrite to cache/unique + dictionaries and to coalesce adjacent regions of string with the + same attributes when setting attributes. + 2000-11-12 Fred Kiefer * Source/NSArray.m: Corrected [NSMutableArray arrayWithObject:] to diff --git a/Source/NSGAttributedString.m b/Source/NSGAttributedString.m index fe84701ab..b9532ea0b 100644 --- a/Source/NSGAttributedString.m +++ b/Source/NSGAttributedString.m @@ -50,11 +50,86 @@ #include #include #include +#include +#include +#include #include #define SANITY_CHECKS 0 -@interface GSAttrInfo : NSObject +#define GSI_MAP_RETAIN_KEY(X) +#define GSI_MAP_RELEASE_KEY(X) +#define GSI_MAP_RETAIN_VAL(X) +#define GSI_MAP_RELEASE_VAL(X) +#define GSI_MAP_EQUAL(X,Y) [(X).obj isEqualToDictionary: (Y).obj] +#define GSI_MAP_KTYPES GSUNION_OBJ +#define GSI_MAP_VTYPES GSUNION_INT + +#include + +static NSLock *attrLock = nil; +static GSIMapTable_t attrMap; +static SEL lockSel; +static SEL unlockSel; +static IMP lockImp; +static IMP unlockImp; + +#define ALOCK() if (attrLock != nil) (*lockImp)(attrLock, lockSel) +#define AUNLOCK() if (attrLock != nil) (*unlockImp)(attrLock, unlockSel) + +/* + * Add a dictionary to the cache - if it was not already there, return + * the copy added to the cache, if it was, count it and return retained + * object that was there. + */ +static NSDictionary* +cacheAttributes(NSDictionary *attrs) +{ + GSIMapNode node; + + ALOCK(); + node = GSIMapNodeForKey(&attrMap, (GSIMapKey)attrs); + if (node == 0) + { + attrs = [attrs copy]; + GSIMapAddPair(&attrMap, (GSIMapKey)attrs, (GSIMapVal)(unsigned)1); + } + else + { + node->value.uint++; + attrs = RETAIN(node->key.obj); + } + AUNLOCK(); + return attrs; +} + +static void +unCacheAttributes(NSDictionary *attrs) +{ + GSIMapBucket bucket; + + ALOCK(); + bucket = GSIMapBucketForKey(&attrMap, (GSIMapKey)attrs); + if (bucket != 0) + { + GSIMapNode node; + + node = GSIMapNodeForKeyInBucket(bucket, (GSIMapKey)attrs); + if (node != 0) + { + if (--node->value.uint == 0) + { + GSIMapRemoveNodeFromMap(&attrMap, bucket, node); + GSIMapFreeNode(&attrMap, node); + } + } + } + AUNLOCK(); +} + + + +@interface GSAttrInfo : NSObject { @public unsigned loc; @@ -67,18 +142,23 @@ @implementation GSAttrInfo +/* + * Called to record attributes at a particular location - the given attributes + * dictionary must have been produced by 'cacheAttributes()' so that it is + * already copied/retained and this method doesn't need to do it. + */ + (GSAttrInfo*) newWithZone: (NSZone*)z value: (NSDictionary*)a at: (unsigned)l; { GSAttrInfo *info = (GSAttrInfo*)NSAllocateObject(self, 0, z); info->loc = l; - info->attrs = [a copyWithZone: z]; + info->attrs = a; return info; } - (void) dealloc { - RELEASE(attrs); + [self gcFinalize]; NSDeallocateObject(self); } @@ -88,6 +168,33 @@ loc, attrs]; } +- (void) encodeWithCoder: (NSCoder*)aCoder +{ + [aCoder encodeValueOfObjCType: @encode(unsigned) at: &loc]; + [aCoder encodeValueOfObjCType: @encode(id) at: &attrs]; +} + +- (void) gcFinalize +{ + unCacheAttributes(attrs); + DESTROY(attrs); +} + +- (id) initWithCoder: (NSCoder*)aCoder +{ + NSDictionary *a; + + [aCoder decodeValueOfObjCType: @encode(unsigned) at: &loc]; + a = [aCoder decodeObject]; + attrs = cacheAttributes(a); + return self; +} + +- (id) replacementObjectForPortCoder: (NSPortCoder*)aCoder +{ + return self; +} + @end @@ -122,6 +229,8 @@ static void _setup() { NSMutableArray *a; + GSIMapInitWithZoneAndCapacity(&attrMap, NSDefaultMallocZone(), 32); + infSel = @selector(newWithZone:value:at:); addSel = @selector(addObject:); cntSel = @selector(count); @@ -165,7 +274,8 @@ _setAttributesFrom( attr = [attributedString attributesAtIndex: aRange.location effectiveRange: &range]; - info = [GSAttrInfo newWithZone: z value: attr at: 0]; + attr = cacheAttributes(attr); + info = NEWINFO(z, attr, 0); ADDOBJECT(info); RELEASE(info); @@ -173,7 +283,8 @@ _setAttributesFrom( { attr = [attributedString attributesAtIndex: loc effectiveRange: &range]; - info = [GSAttrInfo newWithZone: z value: attr at: loc - aRange.location]; + attr = cacheAttributes(attr); + info = NEWINFO(z, attr, loc - aRange.location); ADDOBJECT(info); RELEASE(info); } @@ -263,9 +374,34 @@ _attributesAtIndexEffectiveRange( return nil; } +/* + * If we are multi-threaded, we must guard access to the uniquing set. + */ ++ (void) _becomeThreaded: (id)notification +{ + attrLock = [NSLock new]; + lockSel = @selector(lock); + unlockSel = @selector(unlock); + lockImp = [attrLock methodForSelector: lockSel]; + unlockImp = [attrLock methodForSelector: unlockSel]; +} + + (void) initialize { _setup(); + + if ([NSThread isMultiThreaded]) + { + [self _becomeThreaded: nil]; + } + else + { + [[NSNotificationCenter defaultCenter] + addObserver: self + selector: @selector(_becomeThreaded:) + name: NSWillBecomeMultiThreadedNotification + object: nil]; + } } - (id) initWithString: (NSString*)aString @@ -285,6 +421,11 @@ _attributesAtIndexEffectiveRange( { GSAttrInfo *info; + if (attributes == nil) + { + attributes = [NSDictionary dictionary]; + } + attributes = cacheAttributes(attributes); info = NEWINFO(z, attributes, 0); ADDOBJECT(info); RELEASE(info); @@ -349,7 +490,7 @@ _attributesAtIndexEffectiveRange( + (void) initialize { - _setup(); + [NSGAttributedString class]; // Ensure immutable class is initialised } - (id) initWithString: (NSString*)aString @@ -370,6 +511,11 @@ SANITY(); { GSAttrInfo *info; + if (attributes == nil) + { + attributes = [NSDictionary dictionary]; + } + attributes = cacheAttributes(attributes); info = NEWINFO(z, attributes, 0); ADDOBJECT(info); RELEASE(info); @@ -394,6 +540,17 @@ SANITY(); index, aRange, [_textChars length], _infoArray, &dummy); } +/* + * Primitive method! Sets attributes and values for a given range of + * characters, replacing any previous attributes and values for that + * range. + * + * Sets the attributes for the characters in aRange to attributes. + * These new attributes replace any attributes previously associated + * with the characters in aRange. Raises an NSRangeException if any + * part of aRange lies beyond the end of the receiver's characters. + * See also: - addAtributes: range: , - removeAttributes: range: + */ - (void) setAttributes: (NSDictionary*)attributes range: (NSRange)range { @@ -413,6 +570,7 @@ SANITY(); { attributes = [NSDictionary dictionary]; } + attributes = cacheAttributes(attributes); SANITY(); tmpLength = [_textChars length]; GS_RANGE_CHECK(range, tmpLength); @@ -426,7 +584,25 @@ SANITY(); */ attrs = _attributesAtIndexEffectiveRange( afterRangeLoc, &effectiveRange, tmpLength, _infoArray, &arrayIndex); - if (effectiveRange.location > beginRangeLoc) + if (attrs == attributes) + { + /* + * The located range has the same attributes as us - so we can + * extend our range to include it. + */ + if (effectiveRange.location < beginRangeLoc) + { + range.length += beginRangeLoc - effectiveRange.location; + range.location = effectiveRange.location; + beginRangeLoc = range.location; + } + if (NSMaxRange(effectiveRange) > afterRangeLoc) + { + range.length = NSMaxRange(effectiveRange) - range.location; + afterRangeLoc = NSMaxRange(range); + } + } + else if (effectiveRange.location > beginRangeLoc) { /* * The located range also starts at or after our range. @@ -435,13 +611,13 @@ SANITY(); info->loc = afterRangeLoc; arrayIndex--; } - else + else if (effectiveRange.location < beginRangeLoc) { /* * The located range starts before our range. * Create a subrange to go from our end to the end of the old range. */ - info = NEWINFO(z, attrs, afterRangeLoc); + info = NEWINFO(z, cacheAttributes(attrs), afterRangeLoc); arrayIndex++; INSOBJECT(info, arrayIndex); RELEASE(info); @@ -465,11 +641,17 @@ SANITY(); arrayIndex--; } + /* + * Use the location/attribute info in the current slot if possible, + * otherwise, add a new slot and use that. + */ info = OBJECTAT(arrayIndex); - if (info->loc >= beginRangeLoc) + if (info->loc >= beginRangeLoc || info->attrs == attributes) { info->loc = beginRangeLoc; - ASSIGNCOPY(info->attrs, attributes); + unCacheAttributes(info->attrs); + RELEASE(info->attrs); + info->attrs = attributes; } else { @@ -480,19 +662,6 @@ SANITY(); } SANITY(); - /* - * Primitive method! Sets attributes and values for a given range of - * characters, replacing any previous attributes and values for that - * range. - */ - - /* - * Sets the attributes for the characters in aRange to attributes. - * These new attributes replace any attributes previously associated - * with the characters in aRange. Raises an NSRangeException if any - * part of aRange lies beyond the end of the receiver's characters. - * See also: - addAtributes: range: , - removeAttributes: range: - */ } - (void) replaceCharactersInRange: (NSRange)range @@ -520,8 +689,7 @@ SANITY(); * simply appends the new string and attributes are inherited. */ [_textChars appendString: aString]; -SANITY(); - return; + goto finish; } arraySize = (*cntImp)(_infoArray, cntSel); @@ -532,8 +700,7 @@ SANITY(); * then the replacement characters will get them too. */ [_textChars replaceCharactersInRange: range withString: aString]; -SANITY(); - return; + goto finish; } /* @@ -552,7 +719,7 @@ SANITY(); arrayIndex++; if (NSMaxRange(effectiveRange) > NSMaxRange(range)) { - info = NEWINFO(z, attrs, NSMaxRange(range)); + info = NEWINFO(z, cacheAttributes(attrs), NSMaxRange(range)); INSOBJECT(info, arrayIndex); arraySize++; SANITY(); @@ -613,6 +780,7 @@ SANITY(); } SANITY(); [_textChars replaceCharactersInRange: range withString: aString]; +finish: SANITY(); }