We should not keep the cacheURL local ivar. It could change during a run. E.g. after a Save As, the original cached index belongs to the file at the old location rather than the current document, and the cached index should be written to a different location. So there should be a map from database URL (or alias) to cached index URL. Possibly in a separate plist file. This way the old cached index can be found back or overwritten if required.
Christiaan On 17 Jan 2008, at 9:19 PM, [EMAIL PROTECTED] wrote: > Revision: 12540 > http://bibdesk.svn.sourceforge.net/bibdesk/? > rev=12540&view=rev > Author: amaxwell > Date: 2008-01-17 12:19:13 -0800 (Thu, 17 Jan 2008) > > Log Message: > ----------- > Changes to disk caching. Save to ~/Library/Caches, introspect > caches to find the correct document. Seems fast enough. > > Save index on index -cancel, which is sent when the window closes. > Also register for app terminate notifications. Use the condition > lock to save only after the worker thread exits. > > Modified Paths: > -------------- > trunk/bibdesk/BDSKFileContentSearchController.m > trunk/bibdesk/BDSKFileSearchIndex.h > trunk/bibdesk/BDSKFileSearchIndex.m > > Modified: trunk/bibdesk/BDSKFileContentSearchController.m > =================================================================== > --- trunk/bibdesk/BDSKFileContentSearchController.m 2008-01-17 > 19:19:57 UTC (rev 12539) > +++ trunk/bibdesk/BDSKFileContentSearchController.m 2008-01-17 > 20:19:13 UTC (rev 12540) > @@ -73,16 +73,7 @@ > NSParameterAssert([aDocument conformsToProtocol:@protocol > (BDSKSearchContentView)]); > [self setDocument:aDocument]; > > - // @@ temporary: this is just a temporary setup for testing > search index caching > - NSURL *cacheURL = nil; > - if ([[NSUserDefaults standardUserDefaults] > boolForKey:@"BDSKShouldCacheFileSearchIndexKey"]) { > - NSString *cachePath; > - cachePath = [[[[self document] fileName] > stringByDeletingPathExtension] > stringByAppendingPathExtension:@"bdskindex"]; > - if ([[NSFileManager defaultManager] > fileExistsAtPath:cachePath]) > - cacheURL = [NSURL fileURLWithPath:cachePath]; > - } > - > - searchIndex = [[BDSKFileSearchIndex alloc] > initWithDocument:aDocument cacheURL:cacheURL]; > + searchIndex = [[BDSKFileSearchIndex alloc] > initWithDocument:aDocument]; > search = [[BDSKFileSearch alloc] initWithIndex:searchIndex > delegate:self]; > searchFieldDidEndEditing = NO; > > @@ -415,16 +406,11 @@ > // extra safety here; make sure the index stops messaging the > search object now > [searchIndex setDelegate:nil]; > > - // @@ temporary: this is just a temporary setup for testing > search index caching > - NSString *cachePath = nil; > - if ([[NSUserDefaults standardUserDefaults] > boolForKey:@"BDSKShouldCacheFileSearchIndexKey"]) > - cachePath = [[[[self document] fileName] > stringByDeletingPathExtension] > stringByAppendingPathExtension:@"bdskindex"]; > + // let the index know the document's location so it can cache > the index to disk > + [searchIndex setDocumentURL:[[self document] fileURL]]; > > - // stops the search index runloop so it will release the document > - if (cachePath) > - [searchIndex closeIndexAndCacheToURL:[NSURL > fileURLWithPath:cachePath]]; > - else > - [searchIndex cancel]; > + // stops the search index runloop > + [searchIndex cancel]; > [searchIndex release]; > searchIndex = nil; > } > > Modified: trunk/bibdesk/BDSKFileSearchIndex.h > =================================================================== > --- trunk/bibdesk/BDSKFileSearchIndex.h 2008-01-17 19:19:57 UTC > (rev 12539) > +++ trunk/bibdesk/BDSKFileSearchIndex.h 2008-01-17 20:19:13 UTC > (rev 12540) > @@ -60,6 +60,8 @@ > CFMutableDataRef indexData; > NSMutableDictionary *itemInfos; > NSMutableDictionary *signatures; > + NSURL *documentURL; > + NSURL *indexCacheURL; > id delegate; > > BDSKThreadSafeMutableArray *notificationQueue; > @@ -72,7 +74,6 @@ > > // aDocument must respond to -publications; this should generally > be called on the main thread > - (id)initWithDocument:(id)aDocument; > -- (id)initWithDocument:(id)aDocument cacheURL:(NSURL *)cacheURL; > > // Warning: it is /not/ safe to write to this SKIndexRef > directly; use it only for reading. > - (SKIndexRef)index; > @@ -82,12 +83,11 @@ > - (BOOL)isIndexing; > - (void)setDelegate:(id <BDSKFileSearchIndexDelegate>)anObject; > - (NSDictionary *)itemInfoForURL:(NSURL *)theURL; > -- (NSData *)signatureForURL:(NSURL *)theURL; > > // Poll this for progress bar updates during indexing > - (double)progressValue; > > -- (BOOL)closeIndexAndCacheToURL:(NSURL *)cacheURL; > +- (void)setDocumentURL:(NSURL *)aURL; > > @end > > > Modified: trunk/bibdesk/BDSKFileSearchIndex.m > =================================================================== > --- trunk/bibdesk/BDSKFileSearchIndex.m 2008-01-17 19:19:57 UTC > (rev 12539) > +++ trunk/bibdesk/BDSKFileSearchIndex.m 2008-01-17 20:19:13 UTC > (rev 12540) > @@ -47,7 +47,7 @@ > > - (NSArray *)itemsToIndex:(NSArray *)items; > - (void)buildIndexForItems:(NSArray *)items; > -- (void)indexFilesForItems:(NSArray *)items; > +- (void)indexFilesForItems:(NSArray *)items > numberPreviouslyIndexed:(double)numberIndexed totalCount:(double) > totalObjectCount; > - (void)indexFilesForItem:(id)anItem; > - (void)runIndexThreadForItems:(NSArray *)items; > - (void)processNotification:(NSNotification *)note; > @@ -55,28 +55,57 @@ > - (void)handleDocDelItemNotification:(NSNotification *)note; > - (void)handleSearchIndexInfoChangedNotification:(NSNotification *) > note; > - (void)handleMachMessage:(void *)msg; > +- (void)writeIndexToDisk; > > @end > > +static NSString *cacheIndexFolder() > +{ > + static NSString *cacheFolder = nil; > + if (nil == cacheFolder) { > + cacheFolder = [NSSearchPathForDirectoriesInDomains > (NSCachesDirectory, NSUserDomainMask, YES) lastObject]; > + cacheFolder = [cacheFolder stringByAppendingPathComponent: > [[NSBundle mainBundle] bundleIdentifier]]; > + if (cacheFolder && [[NSFileManager defaultManager] > fileExistsAtPath:cacheFolder] == NO) > + [[NSFileManager defaultManager] > createDirectoryAtPath:cacheFolder attributes:nil]; > + cacheFolder = [cacheFolder > stringByAppendingPathComponent:@"Search indexes"]; > + if (cacheFolder && [[NSFileManager defaultManager] > fileExistsAtPath:cacheFolder] == NO) > + [[NSFileManager defaultManager] > createDirectoryAtPath:cacheFolder attributes:nil]; > + cacheFolder = [cacheFolder copy]; > + } > + return cacheFolder; > +} > + > +// Read each cache file and see which one has a matching > documentURL. If this gets too slow, we could save a plist mapping > URL -> UUID and use that instead. > +static NSURL *copyIndexCacheURLForDocumentURL(NSURL *documentURL) > +{ > + NSCParameterAssert(nil != documentURL); > + NSString *cacheFolder = cacheIndexFolder(); > + NSArray *existingIndexes = [[NSFileManager defaultManager] > directoryContentsAtPath:cacheFolder]; > + existingIndexes = [existingIndexes pathsMatchingExtensions: > [NSArray arrayWithObject:@"bdskindex"]]; > + > + NSEnumerator *indexEnum = [existingIndexes objectEnumerator]; > + NSString *path; > + NSURL *indexCacheURL = nil; > + > + while ((path = [indexEnum nextObject]) && nil == indexCacheURL) { > + path = [cacheFolder stringByAppendingPathComponent:path]; > + NSDictionary *cacheDict = [NSKeyedUnarchiver > unarchiveObjectWithFile:path]; > + if ([[cacheDict objectForKey:@"documentURL"] > isEqual:documentURL]) > + indexCacheURL = [[NSURL alloc] initFileURLWithPath:path]; > + } > + return indexCacheURL; > +} > + > + > @implementation BDSKFileSearchIndex > > #define INDEX_STARTUP 1 > #define INDEX_STARTUP_COMPLETE 2 > +#define INDEX_THREAD_WORKING 3 > +#define INDEX_THREAD_DONE 4 > > -static inline NSData *sha1SignatureForURL(NSURL *aURL) { > - NSData *data = [[NSData alloc] initWithContentsOfURL:aURL]; > - NSData *sha1Signature = [data sha1Signature]; > - [data release]; > - return sha1Signature; > -} > - > - (id)initWithDocument:(id)aDocument > { > - return [self initWithDocument:aDocument cacheURL:nil]; > -} > - > -- (id)initWithDocument:(id)aDocument cacheURL:(NSURL *)cacheURL > -{ > OBASSERT([NSThread inMainThread]); > > self = [super init]; > @@ -87,8 +116,10 @@ > > index = NULL; > > - if (cacheURL) { > - NSDictionary *cacheDict = [NSKeyedUnarchiver > unarchiveObjectWithFile:[cacheURL path]]; > + // new document won't have a URL, so we'll have to wait > for the controller to set it > + indexCacheURL = [aDocument fileURL] ? > copyIndexCacheURLForDocumentURL([aDocument fileURL]) : nil; > + if (indexCacheURL) { > + NSDictionary *cacheDict = [NSKeyedUnarchiver > unarchiveObjectWithFile:[indexCacheURL path]]; > indexData = (CFMutableDataRef)[[cacheDict > objectForKey:@"indexData"] mutableCopy]; > if (indexData != NULL) { > index = SKIndexOpenWithMutableData(indexData, NULL); > @@ -99,6 +130,11 @@ > indexData = NULL; > } > } > + } else { > + // Could use the doc name, but I keep different files > with the same name on shared and local volumes, and presumably > others do things that are just as weird. This guarantees a unique > name, so we'll just introspect the caches when loading them. > + NSString *cachePath = [cacheIndexFolder() > stringByAppendingPathComponent:[[NSProcessInfo processInfo] > globallyUniqueString]]; > + cachePath = [cachePath > stringByAppendingPathExtension:@"bdskindex"]; > + indexCacheURL = [[NSURL alloc] > initFileURLWithPath:cachePath]; > } > > if (index == NULL) { > @@ -113,6 +149,7 @@ > [nc addObserver:self selector:handler > name:BDSKFileSearchIndexInfoChangedNotification object:aDocument]; > [nc addObserver:self selector:handler > name:BDSKDocAddItemNotification object:aDocument]; > [nc addObserver:self selector:handler > name:BDSKDocDelItemNotification object:aDocument]; > + [nc addObserver:self selector:handler > name:NSApplicationWillTerminateNotification object:aDocument]; > > flags.isIndexing = 0; > flags.shouldKeepRunning = 1; > @@ -129,11 +166,8 @@ > > // block until the NSMachPort is set up to receive messages > [setupLock lockWhenCondition:INDEX_STARTUP_COMPLETE]; > - [setupLock unlock]; > - > - // done with this lock, so get rid of it now > - [setupLock release]; > - setupLock = nil; > + [setupLock unlockWithCondition:INDEX_THREAD_WORKING]; > + > } > > return self; > @@ -141,23 +175,31 @@ > > - (void)dealloc > { > + [indexCacheURL release]; > [notificationPort release]; > [notificationQueue release]; > [itemInfos release]; > [signatures release]; > if(index) CFRelease(index); > if(indexData) CFRelease(indexData); > + [setupLock release]; > [super dealloc]; > } > > -// cancel is usually sent from the main thread > +// cancel is always sent from the main thread > - (void)cancel > { > + NSParameterAssert([NSThread inMainThread]); > [[NSNotificationCenter defaultCenter] removeObserver:self]; > OSAtomicCompareAndSwap32(flags.shouldKeepRunning, 0, (int32_t > *)&flags.shouldKeepRunning); > > // wake the thread up so the runloop will exit > [notificationPort sendBeforeDate:[NSDate date] components:nil > from:nil reserved:0]; > + > + // wait until the thread exits, so we have exclusive access to > the ivars > + [setupLock lockWhenCondition:INDEX_THREAD_DONE]; > + [self writeIndexToDisk]; > + [setupLock unlock]; > } > > - (SKIndexRef)index > @@ -182,21 +224,12 @@ > - (NSDictionary *)itemInfoForURL:(NSURL *)theURL > { > NSDictionary *itemInfo = nil; > - @synchronized(itemInfos) { > + @synchronized(self) { > itemInfo = [[itemInfos objectForKey:theURL] retain]; > } > return [itemInfo autorelease]; > } > > -- (NSData *)signatureForURL:(NSURL *)theURL > -{ > - NSData *signature = nil; > - @synchronized(signature) { > - signature = [[signatures objectForKey:theURL] retain]; > - } > - return [signature autorelease]; > -} > - > - (double)progressValue > { > double theValue; > @@ -206,20 +239,10 @@ > return theValue; > } > > -- (BOOL)closeIndexAndCacheToURL:(NSURL *)cacheURL > +- (void)setDocumentURL:(NSURL *)aURL > { > - // I'm not sure if this is all safe > - [self cancel]; > - while ([self isIndexing]); > - if (index) { > - SKIndexClose(index); > - index = NULL; > - } > - NSDictionary *cacheDict = nil; > - @synchronized(signatures) { > - cacheDict = [NSDictionary dictionaryWithObjectsAndKeys: > [[(NSData *)indexData copy] autorelease], @"indexData", > [[signatures copy] autorelease], @"signatures", nil]; > - } > - return [NSKeyedArchiver archiveRootObject:cacheDict toFile: > [cacheURL path]]; > + [documentURL autorelease]; > + documentURL = [aURL copy]; > } > > @end > @@ -229,7 +252,7 @@ > SKIndexDocumentIteratorRef iterator = > SKIndexDocumentIteratorCreate (anIndex, inParentDocument); > SKDocumentRef skDocument; > CFURLRef aURL; > - Boolean isLeaf = true; > + bool isLeaf = true; > > while (skDocument = SKIndexDocumentIteratorCopyNext(iterator)) { > isLeaf = false; > @@ -246,6 +269,14 @@ > > @implementation BDSKFileSearchIndex (Private) > > +static inline NSData *sha1SignatureForURL(NSURL *aURL) { > + // using the mapped data options will cause a crash in the > sha1Signature method > + NSData *data = [[NSData alloc] initWithContentsOfURL:aURL]; > + NSData *sha1Signature = [data sha1Signature]; > + [data release]; > + return sha1Signature; > +} > + > - (NSArray *)itemsToIndex:(NSArray *)items > { > [items retain]; > @@ -262,9 +293,14 @@ > > NSEnumerator *itemEnum = [items objectEnumerator]; > id anItem = nil; > + NSMutableDictionary *previouslyIndexedInfos = > [NSMutableDictionary dictionary]; > + double numberIndexed = 0; > double totalObjectCount = [items count]; > - double numberIndexed = 0; > > + // see comment later; may need tuning here since this is much > faster, or else use a message queue instead of passing > waitUntilDone:YES > + const int32_t flushInterval = 20; > + int32_t countSinceLastFlush = flushInterval; > + > // update the itemInfos with the items, find items to add and > URLs to remove > OSMemoryBarrier(); > while(flags.shouldKeepRunning == 1 && (anItem = [itemEnum > nextObject])) { > @@ -275,15 +311,11 @@ > > while (url = [urlEnum nextObject]) { > if ([indexedURLs containsObject:url]) { > - if ([[self signatureForURL:url] > isEqual:sha1SignatureForURL(url)]) { > + if ([[signatures objectForKey:url] > isEqual:sha1SignatureForURL(url)]) { > [URLsToRemove removeObject:url]; > - @synchronized(itemInfos) { > - [itemInfos setObject:anItem forKey:url]; > - } > + [previouslyIndexedInfos setObject:anItem > forKey:url]; > } else { > - @synchronized(signatures) { > - [signatures removeObjectForKey:url]; > - } > + [signatures removeObjectForKey:url]; > needsIndexing = YES; > } > } else { > @@ -291,20 +323,26 @@ > } > } > > + // ??? is it worth updating progress here since it runs so > fast? we'd also need to send delegate messages periodically > + > if (needsIndexing) { > [itemsToAdd addObject:anItem]; > } else { > numberIndexed++; > @synchronized(self) { > progressValue = (numberIndexed / totalObjectCount) > * 100; > + [itemInfos > addEntriesFromDictionary:previouslyIndexedInfos]; > + [previouslyIndexedInfos removeAllObjects]; > } > + if (countSinceLastFlush-- == 0) { > + [delegate performSelectorOnMainThread:@selector > (searchIndexDidUpdate:) withObject:self waitUntilDone:YES]; > + countSinceLastFlush = flushInterval; > + } > } > + > OSMemoryBarrier(); > } > - > - OSMemoryBarrier(); > - if (flags.shouldKeepRunning == 1) > - [delegate performSelectorOnMainThread:@selector > (searchIndexDidUpdate:) withObject:self waitUntilDone:NO]; > + > > // remove URLs we could not find in the database > OSMemoryBarrier(); > @@ -312,27 +350,23 @@ > NSEnumerator *urlEnum = [URLsToRemove objectEnumerator]; > NSURL *url; > SKDocumentRef skDocument; > - volatile Boolean success; > > // loop through the array of URLs, create a new > SKDocumentRef, and try to remove it > while (url = [urlEnum nextObject]) { > > skDocument = SKDocumentCreateWithURL((CFURLRef)url); > - OBPOSTCONDITION(skDocument); > - if(!skDocument) continue; > - > - success = SKIndexRemoveDocument(index, skDocument); > - OBPOSTCONDITION(success); > - > - CFRelease(skDocument); > + if (skDocument) { > + SKIndexRemoveDocument(index, skDocument); > + CFRelease(skDocument); > + } > } > } > [URLsToRemove release]; > - > + [items release]; > + > OSMemoryBarrier(); > if (flags.shouldKeepRunning == 1) > [delegate performSelectorOnMainThread:@selector > (searchIndexDidUpdate:) withObject:self waitUntilDone:NO]; > - [items release]; > > return itemsToAdd; > } > @@ -343,31 +377,35 @@ > > OBPRECONDITION(items); > > + // normally set in the indexFilesForItem: method, but we need > to avoid returning NO for isIndexing here as well > + OSAtomicCompareAndSwap32Barrier(0, 1, (int32_t *) > &flags.isIndexing); > + > + double totalObjectCount = [items count]; > items = [self itemsToIndex:items]; > + double numberIndexed = totalObjectCount - [items count]; > > [items retain]; > > // add items that were not yet indexed > OSMemoryBarrier(); > if (flags.shouldKeepRunning == 1 && [items count]) { > - [self indexFilesForItems:items]; > + [self indexFilesForItems:items > numberPreviouslyIndexed:numberIndexed totalCount:totalObjectCount]; > } > > [items release]; > - > + OSAtomicCompareAndSwap32Barrier(1, 0, (int32_t *) > &flags.isIndexing); > + > OSMemoryBarrier(); > if (flags.shouldKeepRunning == 1) > [delegate performSelectorOnMainThread:@selector > (searchIndexDidFinishInitialIndexing:) withObject:self > waitUntilDone:NO]; > } > > -- (void)indexFilesForItems:(NSArray *)items > +- (void)indexFilesForItems:(NSArray *)items > numberPreviouslyIndexed:(double)numberIndexed totalCount:(double) > totalObjectCount > { > NSAssert2([[NSThread currentThread] > isEqual:notificationThread], @"-[%@ [EMAIL PROTECTED] must be called from the > > worker thread!", [self class], NSStringFromSelector(_cmd)); > > NSEnumerator *enumerator = [items objectEnumerator]; > id anObject = nil; > - double totalObjectCount = [items count]; > - double numberIndexed = 0; > > // This threshold is sort of arbitrary; for small batches, > frequent updates are better if the delegate has a progress > indicator, but for large batches (initial indexing), it can kill > performance to be continually flushing and searching while indexing. > const int32_t flushInterval = [items count] > 20 ? 5 : 1; > @@ -428,15 +466,13 @@ > > // SKIndexSetProperties is more generally useful, but is > really slow when creating the index > // SKIndexRenameDocument changes the URL, so it's not useful > - @synchronized(itemInfos) { > + @synchronized(self) { > [itemInfos setObject:anItem forKey:url]; > } > - @synchronized(signatures) { > - if (signature) > - [signatures setObject:signature forKey:url]; > - else > - [signatures removeObjectForKey:url]; > - } > + if (signature) > + [signatures setObject:signature forKey:url]; > + else > + [signatures removeObjectForKey:url]; > > success = SKIndexAddDocument(index, skDocument, NULL, TRUE); > OBPOSTCONDITION(success); > @@ -449,6 +485,22 @@ > // the caller is responsible for updating the delegate, so we > can throttle initial indexing > } > > +- (void)writeIndexToDisk > +{ > + NSParameterAssert([NSThread inMainThread]); > + NSParameterAssert([setupLock condition] == INDEX_THREAD_DONE); > + if (index && [[NSUserDefaults standardUserDefaults] > boolForKey:@"BDSKShouldCacheFileSearchIndexKey"]) { > + // flush all pending updates and compact the index as > needed before writing > + SKIndexCompact(index); > + CFRelease(index); > + index = NULL; > + > + NSDictionary *cacheDict = nil; > + cacheDict = [NSDictionary dictionaryWithObjectsAndKeys: > (NSData *)indexData, @"indexData", signatures, @"signatures", > documentURL, @"documentURL", nil]; > + [NSKeyedArchiver archiveRootObject:cacheDict toFile: > [indexCacheURL path]]; > + } > +} > + > - (void)runIndexThreadForItems:(NSArray *)items > { > NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; > @@ -465,6 +517,8 @@ > notificationQueue = [[BDSKThreadSafeMutableArray alloc] > initWithCapacity:5]; > [setupLock unlockWithCondition:INDEX_STARTUP_COMPLETE]; > > + [setupLock lockWhenCondition:INDEX_THREAD_WORKING]; > + > // an exception here can probably be ignored safely > @try{ > [self buildIndexForItems:items]; > @@ -490,6 +544,12 @@ > } > @catch(id localException){ > NSLog(@"Exception %@ raised in search index; exiting > thread run loop.", localException); > + > + // clean these up to make sure we have no chance of saving > it to disk > + if (index) CFRelease(index); > + index = NULL; > + if (indexData) CFRelease(indexData); > + indexData = NULL; > @throw; > } > @finally{ > @@ -498,15 +558,21 @@ > [notificationThread release]; > notificationThread = nil; > [notificationPort invalidate]; > + [setupLock unlockWithCondition:INDEX_THREAD_DONE]; > } > } > > - (void)processNotification:(NSNotification *)note > { > OBASSERT([NSThread inMainThread]); > - // Forward the notification to the correct thread > - [notificationQueue addObject:note]; > - [notificationPort sendBeforeDate:[NSDate date] components:nil > from:nil reserved:0]; > + if ([[note name] > isEqualToString:NSApplicationWillTerminateNotification]) { > + [self cancel]; > + > + } else { > + // Forward the notification to the correct thread > + [notificationQueue addObject:note]; > + [notificationPort sendBeforeDate:[NSDate date] > components:nil from:nil reserved:0]; > + } > } > > - (void)handleDocAddItemNotification:(NSNotification *)note > @@ -517,7 +583,7 @@ > OBPRECONDITION(searchIndexInfo); > > // this will update the delegate when all is complete > - [self indexFilesForItems:searchIndexInfo]; > + [self indexFilesForItems:searchIndexInfo > numberPreviouslyIndexed:0 totalCount:1]; > } > > - (void)handleDocDelItemNotification:(NSNotification *)note > @@ -549,12 +615,10 @@ > > url = [urls objectAtIndex:idx]; > > - @synchronized(itemInfos) { > + @synchronized(self) { > [itemInfos removeObjectForKey:url]; > } > - @synchronized(signatures) { > - [signatures removeObjectForKey:url]; > - } > + [signatures removeObjectForKey:url]; > > skDocument = SKDocumentCreateWithURL((CFURLRef)url); > OBPOSTCONDITION(skDocument); > > > This was sent by the SourceForge.net collaborative development > platform, the world's largest Open Source development site. > > ---------------------------------------------------------------------- > --- > This SF.net email is sponsored by: Microsoft > Defy all challenges. Microsoft(R) Visual Studio 2008. > http://clk.atdmt.com/MRT/go/vse0120000070mrt/direct/01/ > _______________________________________________ > Bibdesk-commit mailing list > [EMAIL PROTECTED] > https://lists.sourceforge.net/lists/listinfo/bibdesk-commit ------------------------------------------------------------------------- This SF.net email is sponsored by: Microsoft Defy all challenges. Microsoft(R) Visual Studio 2008. http://clk.atdmt.com/MRT/go/vse0120000070mrt/direct/01/ _______________________________________________ Bibdesk-develop mailing list Bibdesk-develop@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/bibdesk-develop