3 Copyright (c) 1995-2001 by Apple Computer, Inc., all rights reserved.
6 Find and replace functionality with a minimal panel...
7 Would be nice to have the buttons in the panel validate; this would allow the
8 replace buttons to become disabled for readonly docs
11 IMPORTANT: This Apple software is supplied to you by Apple Computer, Inc. ("Apple") in
12 consideration of your agreement to the following terms, and your use, installation,
13 modification or redistribution of this Apple software constitutes acceptance of these
14 terms. If you do not agree with these terms, please do not use, install, modify or
15 redistribute this Apple software.
17 In consideration of your agreement to abide by the following terms, and subject to these
18 terms, Apple grants you a personal, non-exclusive license, under AppleĆs copyrights in
19 this original Apple software (the "Apple Software"), to use, reproduce, modify and
20 redistribute the Apple Software, with or without modifications, in source and/or binary
21 forms; provided that if you redistribute the Apple Software in its entirety and without
22 modifications, you must retain this notice and the following text and disclaimers in all
23 such redistributions of the Apple Software. Neither the name, trademarks, service marks
24 or logos of Apple Computer, Inc. may be used to endorse or promote products derived from
25 the Apple Software without specific prior written permission from Apple. Except as expressly
26 stated in this notice, no other rights or licenses, express or implied, are granted by Apple
27 herein, including but not limited to any patent rights that may be infringed by your
28 derivative works or by other works in which the Apple Software may be incorporated.
30 The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES,
31 EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT,
32 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS
33 USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
35 IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL
36 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
37 OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE,
38 REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND
39 WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR
40 OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
43 #import <Cocoa/Cocoa.h>
44 #import "TextFinder.h"
47 @interface NSString (NSStringTextFinding)
49 - (NSRange)findString:(NSString *)string selectedRange:(NSRange)selectedRange options:(unsigned)mask wrap:(BOOL)wrapFlag;
55 @implementation TextFinder
57 static id sharedFindObject = nil;
59 + (id)sharedInstance {
60 if (!sharedFindObject) {
61 [[self allocWithZone:[[NSApplication sharedApplication] zone]] init];
63 return sharedFindObject;
67 if (sharedFindObject) {
69 return sharedFindObject;
72 if (!(self = [super init])) return nil;
74 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidActivate:) name:NSApplicationDidBecomeActiveNotification object:[NSApplication sharedApplication]];
76 [self setFindString:@"" writeToPasteboard:NO];
77 [self setReplaceString:@""];
78 [self loadFindStringFromPasteboard];
80 sharedFindObject = self;
84 - (void)appDidActivate:(NSNotification *)notification {
85 [self loadFindStringFromPasteboard];
88 - (void)loadFindStringFromPasteboard {
89 NSPasteboard *pasteboard = [NSPasteboard pasteboardWithName:NSFindPboard];
90 if ([[pasteboard types] containsObject:NSStringPboardType]) {
91 NSString *string = [pasteboard stringForType:NSStringPboardType];
92 if (string && [string length]) {
93 [self setFindString:string writeToPasteboard:NO];
98 - (void)loadStringToPasteboard: (NSString*) string {
99 NSPasteboard *pasteboard = [NSPasteboard pasteboardWithName:NSFindPboard];
100 [pasteboard declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil];
101 [pasteboard setString: string forType:NSStringPboardType];
105 if (!findTextField) {
106 if (![NSBundle loadNibNamed:@"FindPanel" owner:self]) {
107 NSLog(@"Failed to load FindPanel.nib");
110 if (self == sharedFindObject) [[findTextField window] setFrameAutosaveName:@"Find"];
112 [findTextField setStringValue:[self findString]];
116 if (self != sharedFindObject) {
117 [[NSNotificationCenter defaultCenter] removeObserver:self];
118 [findString release];
123 - (NSString *)findString {
127 - (void)setFindString:(NSString *)string {
128 [self setFindString:string writeToPasteboard:YES];
131 - (void)setFindString:(NSString *)string writeToPasteboard:(BOOL)flag {
132 if ([string isEqualToString:findString]) return;
133 [findString autorelease];
134 findString = [string copyWithZone:[self zone]];
136 [findTextField setStringValue:string];
137 [findTextField selectText:nil];
139 if (flag) [self loadStringToPasteboard: findString];
142 - (NSString *)replaceString {
143 return replaceString;
146 - (void)setReplaceString:(NSString *)string {
147 if ([string isEqualToString: replaceString]) return;
148 [replaceString autorelease];
149 replaceString = [string copyWithZone:[self zone]];
150 if (replaceTextField) {
151 [replaceTextField setStringValue: string];
152 [replaceTextField selectText: nil];
156 - (NSTextView *)textObjectToSearchIn {
157 id obj = [[NSApp mainWindow] firstResponder];
158 return (obj && [obj isKindOfClass:[NSTextView class]]) ? obj : nil;
161 - (NSPanel *)findPanel {
162 if (!findTextField) [self loadUI];
163 return (NSPanel *)[findTextField window];
166 /* The primitive for finding; this ends up setting the status field (and beeping if necessary)...
168 - (BOOL)find:(BOOL)direction {
169 NSTextView *text = [self textObjectToSearchIn];
170 lastFindWasSuccessful = NO;
172 NSString *textContents = [text string];
174 if (textContents && (textLength = [textContents length])) {
176 unsigned options = 0;
177 if (direction == Backward) options |= NSBackwardsSearch;
178 if ([ignoreCaseButton state]) options |= NSCaseInsensitiveSearch;
179 range = [textContents findString:[self findString] selectedRange:[text selectedRange] options:options wrap:YES];
181 [text setSelectedRange:range];
182 [text scrollRangeToVisible:range];
183 lastFindWasSuccessful = YES;
187 if (!lastFindWasSuccessful) {
189 [statusField setStringValue:NSLocalizedStringFromTable(@"Not found", @"FindPanel", @"Status displayed in find panel when the find string is not found.")];
191 [statusField setStringValue:@""];
193 return lastFindWasSuccessful;
196 - (void)orderFrontFindPanel:(id)sender {
197 NSPanel *panel = [self findPanel];
198 [findTextField selectText:nil];
199 [panel makeKeyAndOrderFront:nil];
202 /**** Action methods for gadgets in the find panel; these should all end up setting or clearing the status field ****/
204 - (void)findNextAndOrderFindPanelOut:(id)sender {
205 [findNextButton performClick:nil];
206 if (lastFindWasSuccessful) {
207 [[self findPanel] orderOut:sender];
209 [findTextField selectText:nil];
213 - (void)findNext:(id)sender {
214 if (findTextField) [self setFindString:[findTextField stringValue]]; /* findTextField should be set */
215 (void)[self find:Forward];
218 - (void)findPrevious:(id)sender {
219 if (findTextField) [self setFindString:[findTextField stringValue]]; /* findTextField should be set */
220 (void)[self find:Backward];
223 - (void)replace:(id)sender {
224 NSTextView *text = [self textObjectToSearchIn];
225 // shouldChangeTextInRange:... should return NO if !isEditable, but doesn't...
226 if (replaceTextField) [self setReplaceString:[replaceTextField stringValue]];
227 if (text && [text isEditable] && [text shouldChangeTextInRange:[text selectedRange] replacementString: replaceString]) {
228 [[text textStorage] replaceCharactersInRange:[text selectedRange] withString: replaceString];
229 [text didChangeText];
233 [statusField setStringValue:@""];
236 - (void)replaceAndFind:(id)sender {
237 [self replace:sender];
238 [self findNext:sender];
241 #define ReplaceAllScopeEntireFile 42
242 #define ReplaceAllScopeSelection 43
244 /* The replaceAll: code is somewhat complex. One reason for this is to support undo well --- To play along with the undo mechanism in the text object, this method goes through the shouldChangeTextInRange:replacementString: mechanism. In order to do that, it precomputes the section of the string that is being updated. An alternative would be for this method to handle the undo for the replaceAll: operation itself, and register the appropriate changes. However, this is simpler...
246 Turns out this approach of building the new string and inserting it at the appropriate place in the actual text storage also has an added benefit of performance; it avoids copying the contents of the string around on every replace, which is significant in large files with many replacements. Of course there is the added cost of the temporary replacement string, but we try to compute that as tightly as possible beforehand to reduce the memory requirements.
248 - (void)replaceAll:(id)sender {
249 NSTextView *text = [self textObjectToSearchIn];
250 if (!text || ![text isEditable]) {
251 [statusField setStringValue:@""];
254 NSTextStorage *textStorage = [text textStorage];
255 NSString *textContents = [text string];
256 BOOL entireFile = replaceAllScopeMatrix ? ([replaceAllScopeMatrix selectedTag] == ReplaceAllScopeEntireFile) : YES;
257 NSRange replaceRange = entireFile ? NSMakeRange(0, [textStorage length]) : [text selectedRange];
258 unsigned searchOption = ([ignoreCaseButton state] ? NSCaseInsensitiveSearch : 0);
259 unsigned replaced = 0;
260 NSRange firstOccurence;
262 if (findTextField) [self setFindString:[findTextField stringValue]];
263 if (replaceTextField) [self setReplaceString:[replaceTextField stringValue]];
265 // Find the first occurence of the string being replaced; if not found, we're done!
266 firstOccurence = [textContents rangeOfString:[self findString] options:searchOption range:replaceRange];
267 if (firstOccurence.length > 0) {
268 NSAutoreleasePool *pool;
269 NSString *targetString = [self findString];
270 NSMutableAttributedString *temp; /* This is the temporary work string in which we will do the replacements... */
271 NSRange rangeInOriginalString; /* Range in the original string where we do the searches */
273 // Find the last occurence of the string and union it with the first occurence to compute the tightest range...
274 rangeInOriginalString = replaceRange = NSUnionRange(firstOccurence, [textContents rangeOfString:targetString options:NSBackwardsSearch|searchOption range:replaceRange]);
276 temp = [[NSMutableAttributedString alloc] init];
280 // The following loop can execute an unlimited number of times, and it could have autorelease activity.
281 // To keep things under control, we use a pool, but to be a bit efficient, instead of emptying everytime through
282 // the loop, we do it every so often. We can only do this as long as autoreleased items are not supposed to
283 // survive between the invocations of the pool!
285 pool = [[NSAutoreleasePool alloc] init];
287 while (rangeInOriginalString.length > 0) {
288 NSRange foundRange = [textContents rangeOfString:targetString options:searchOption range:rangeInOriginalString];
289 if (foundRange.length == 0) {
290 [temp appendAttributedString:[textStorage attributedSubstringFromRange:rangeInOriginalString]]; // Copy the remainder
291 rangeInOriginalString.length = 0; // And signal that we're done
293 NSRange rangeToCopy = NSMakeRange(rangeInOriginalString.location, foundRange.location - rangeInOriginalString.location + 1); // Copy upto the start of the found range plus one char (to maintain attributes with the overlap)...
294 [temp appendAttributedString:[textStorage attributedSubstringFromRange:rangeToCopy]];
295 [temp replaceCharactersInRange:NSMakeRange([temp length] - 1, 1) withString:replaceString];
296 rangeInOriginalString.length -= NSMaxRange(foundRange) - rangeInOriginalString.location;
297 rangeInOriginalString.location = NSMaxRange(foundRange);
299 if (replaced % 100 == 0) { // Refresh the pool... See warning above!
301 pool = [[NSAutoreleasePool alloc] init];
310 // Now modify the original string
311 if ([text shouldChangeTextInRange:replaceRange replacementString:[temp string]]) {
312 [textStorage replaceCharactersInRange:replaceRange withAttributedString:temp];
313 [text didChangeText];
314 } else { // For some reason the string didn't want to be modified. Bizarre...
322 [statusField setStringValue:NSLocalizedStringFromTable(@"Not found", @"FindPanel", @"Status displayed in find panel when the find string is not found.")];
324 [statusField setStringValue:[NSString localizedStringWithFormat:NSLocalizedStringFromTable(@"%d replaced", @"FindPanel", @"Status displayed in find panel when indicated number of matches are replaced."), replaced]];
329 - (void)takeFindStringFromSelection:(id)sender {
330 NSTextView *textView = [self textObjectToSearchIn];
332 NSString *selection = [[textView string] substringWithRange:[textView selectedRange]];
333 [self setFindString:selection];
337 - (void)takeReplaceStringFromSelection:(id)sender {
338 NSTextView *textView = [self textObjectToSearchIn];
340 NSString *selection = [[textView string] substringWithRange:[textView selectedRange]];
341 [self setReplaceString: selection];
345 - (void) jumpToSelection:sender {
346 NSTextView *textView = [self textObjectToSearchIn];
348 [textView scrollRangeToVisible:[textView selectedRange]];
355 @implementation NSString (NSStringTextFinding)
357 - (NSRange)findString:(NSString *)string selectedRange:(NSRange)selectedRange options:(unsigned)options wrap:(BOOL)wrap {
358 BOOL forwards = (options & NSBackwardsSearch) == 0;
359 unsigned length = [self length];
360 NSRange searchRange, range;
363 searchRange.location = NSMaxRange(selectedRange);
364 searchRange.length = length - searchRange.location;
365 range = [self rangeOfString:string options:options range:searchRange];
366 if ((range.length == 0) && wrap) { /* If not found look at the first part of the string */
367 searchRange.location = 0;
368 searchRange.length = selectedRange.location;
369 range = [self rangeOfString:string options:options range:searchRange];
372 searchRange.location = 0;
373 searchRange.length = selectedRange.location;
374 range = [self rangeOfString:string options:options range:searchRange];
375 if ((range.length == 0) && wrap) {
376 searchRange.location = NSMaxRange(selectedRange);
377 searchRange.length = length - searchRange.location;
378 range = [self rangeOfString:string options:options range:searchRange];