diff --git a/src/MacVim/Base.lproj/Preferences.xib b/src/MacVim/Base.lproj/Preferences.xib index c5ad95715a..c439432d4b 100644 --- a/src/MacVim/Base.lproj/Preferences.xib +++ b/src/MacVim/Base.lproj/Preferences.xib @@ -8,6 +8,7 @@ + @@ -24,11 +25,11 @@ - + - + @@ -77,7 +78,7 @@ - + @@ -156,7 +157,7 @@ - + @@ -189,7 +190,7 @@ - + + + + + + + + + + + + diff --git a/src/MacVim/MMAppController.m b/src/MacVim/MMAppController.m index 596dd4da4c..4692148080 100644 --- a/src/MacVim/MMAppController.m +++ b/src/MacVim/MMAppController.m @@ -233,6 +233,7 @@ + (void)registerDefaults @"", MMLastUsedBundleVersionKey, [NSNumber numberWithBool:YES], MMShowWhatsNewOnStartupKey, [NSNumber numberWithBool:0], MMScrollOneDirectionOnlyKey, + [NSNumber numberWithBool:NO], MMFindBarInlineKey, nil]; [ud registerDefaults:macvimDefaults]; diff --git a/src/MacVim/MMFindBarView.h b/src/MacVim/MMFindBarView.h new file mode 100644 index 0000000000..32a81857b9 --- /dev/null +++ b/src/MacVim/MMFindBarView.h @@ -0,0 +1,37 @@ +/* vi:set ts=8 sts=4 sw=4 ft=objc: + * + * VIM - Vi IMproved by Bram Moolenaar + * MacVim GUI port by Bjorn Winckler + * + * Do ":help uganda" in Vim to read copying and usage conditions. + * Do ":help credits" in Vim to see a list of people who contributed. + * See README.txt for an overview of the Vim source code. + */ + +#import + + +@class MMFindBarView; + +@protocol MMFindBarViewDelegate +- (void)findBarView:(MMFindBarView *)view findNext:(BOOL)forward; +- (void)findBarView:(MMFindBarView *)view replace:(BOOL)replaceAll; +- (void)findBarViewDidClose:(MMFindBarView *)view; +// Returns the rect (in MMFindBarView's superview coordinates) within which the +// bar may be dragged. Typically this is the text-view frame, excluding the +// tabline and scrollbars. +- (NSRect)findBarViewDraggableBounds:(MMFindBarView *)view; +@end + + +@interface MMFindBarView : NSView + +@property (nonatomic, assign) id delegate; + +- (void)showWithText:(NSString *)text flags:(int)flags; +- (NSString *)findString; +- (NSString *)replaceString; +- (BOOL)ignoreCase; +- (BOOL)matchWord; + +@end diff --git a/src/MacVim/MMFindBarView.m b/src/MacVim/MMFindBarView.m new file mode 100644 index 0000000000..fa21dcd481 --- /dev/null +++ b/src/MacVim/MMFindBarView.m @@ -0,0 +1,295 @@ +/* vi:set ts=8 sts=4 sw=4 ft=objc: + * + * VIM - Vi IMproved by Bram Moolenaar + * MacVim GUI port by Bjorn Winckler + * + * Do ":help uganda" in Vim to read copying and usage conditions. + * Do ":help credits" in Vim to see a list of people who contributed. + * See README.txt for an overview of the Vim source code. + */ +/* + * MMFindBarView - inline find/replace bar + * + * An NSView overlay anchored to the top-right corner of MMVimView that + * provides the same find/replace functionality as the floating + * MMFindReplaceController panel, but without leaving the editor window. + * The UI is built programmatically (no xib). + * + * Activation: controlled by the MMFindBarInlineKey user default. + * When enabled, ShowFindReplaceDialogMsgID routes here instead of to + * MMFindReplaceController. + */ + +#import "MMFindBarView.h" +#import "MMTabline/MMHoverButton.h" + + +// FRD flag bits (must match FRD_ defines in Vim's gui.h) +enum { + MMFRDForward = 0, + MMFRDBackward = 0x100, + MMFRDReplace = 0x03, + MMFRDReplaceAll= 0x04, + MMFRDMatchWord = 0x08, + MMFRDExactMatch= 0x10, // no ignore-case when set +}; + +static const CGFloat kBarWidth = 490; +static const CGFloat kBarHeight = 148; // 2*kMargin + 4*kFieldH + 3*kRowGap +static const CGFloat kMargin = 12; // ~3 mm outer padding on all four sides +static const CGFloat kLabelW = 90; +static const CGFloat kFieldH = 22; +static const CGFloat kRowH = 34; // kFieldH + kMargin, keeps row gap ~3 mm + + +@implementation MMFindBarView { + NSTextField *_findBox; + NSTextField *_replaceBox; + NSButton *_ignoreCaseButton; + NSButton *_matchWordButton; + NSButton *_replaceButton; + NSButton *_replaceAllButton; + NSButton *_prevButton; + NSButton *_nextButton; + NSButton *_closeButton; // MMHoverButton + NSPoint _dragOffset; // mouse-down offset for dragging +} + +- (instancetype)init { + self = [super initWithFrame:NSMakeRect(0, 0, kBarWidth, kBarHeight)]; + if (!self) return nil; + [self _buildUI]; + return self; +} + +- (instancetype)initWithFrame:(NSRect)frame { + self = [super initWithFrame:frame]; + if (!self) return nil; + [self _buildUI]; + return self; +} + +- (void)_buildUI { + self.wantsLayer = YES; + self.layer.zPosition = 100; + self.layer.borderWidth = 1; + self.layer.borderColor = [NSColor separatorColor].CGColor; + self.layer.cornerRadius = 4; + + // ── Step 1: Pre-create buttons and measure uniform width ───────────────── + // Must happen first so we can derive the right-alignment edge for fields. + _replaceAllButton = [NSButton buttonWithTitle:@"Replace All" target:self action:@selector(_replaceAll:)]; + _replaceButton = [NSButton buttonWithTitle:@"Replace" target:self action:@selector(_replace:)]; + _prevButton = [NSButton buttonWithTitle:@"Previous" target:self action:@selector(_findPrevious:)]; + _nextButton = [NSButton buttonWithTitle:@"Next" target:self action:@selector(_findNext:)]; + _nextButton.keyEquivalent = @"\r"; // primary / blue style + + NSArray *actionButtons = @[_replaceAllButton, _replaceButton, _prevButton, _nextButton]; + CGFloat maxBtnW = 0; + for (NSButton *btn in actionButtons) { + [btn sizeToFit]; + maxBtnW = MAX(maxBtnW, btn.frame.size.width); + } + maxBtnW += 8; + + // Right edge of the button row (4 buttons + 3 gaps, starting at kMargin) + CGFloat buttonsRight = kMargin + 4 * maxBtnW + 3 * 4; + + // ── Step 2: Field geometry — right edge aligned to buttonsRight ────────── + CGFloat fieldX = kMargin + kLabelW + 4; + CGFloat fieldW = buttonsRight - fieldX - 6; // both fields share this width + + // ── Close button: MMHoverButton (tab-style × with circular hover bg) ───── + // Positioned at bar's top-right corner with a tighter 6pt margin so it + // sits closer to the corner than the content rows (margin = 12pt). + MMHoverButton *closeBtn = [MMHoverButton new]; + closeBtn.imageTemplate = [MMHoverButton imageFromType:MMHoverButtonImageCloseTab]; + closeBtn.target = self; + closeBtn.action = @selector(_close:); + closeBtn.bordered = NO; + CGFloat closeSize = 15; + closeBtn.frame = NSMakeRect( + kBarWidth - 6 - closeSize, + kBarHeight - 6 - closeSize, + closeSize, closeSize); + _closeButton = closeBtn; + + // ── Row 1: Find ────────────────────────────────────────────────────────── + CGFloat y = kBarHeight - kMargin - kFieldH; + + NSTextField *findLabel = [NSTextField labelWithString:@"Find:"]; + findLabel.alignment = NSTextAlignmentRight; + findLabel.frame = NSMakeRect(kMargin, y, kLabelW, kFieldH); + [self addSubview:findLabel]; + + _findBox = [[NSTextField alloc] initWithFrame: + NSMakeRect(fieldX, y, fieldW, kFieldH)]; + _findBox.placeholderString = @"Search"; + _findBox.delegate = self; + _findBox.target = self; + _findBox.action = @selector(_findNext:); + [self addSubview:_findBox]; + // Close button floats above the find field + [self addSubview:_closeButton positioned:NSWindowAbove relativeTo:_findBox]; + + // ── Row 2: Replace with ────────────────────────────────────────────────── + y -= kRowH; + NSTextField *replaceLabel = [NSTextField labelWithString:@"Replace with:"]; + replaceLabel.alignment = NSTextAlignmentRight; + replaceLabel.frame = NSMakeRect(kMargin, y, kLabelW, kFieldH); + [self addSubview:replaceLabel]; + + _replaceBox = [[NSTextField alloc] initWithFrame: + NSMakeRect(fieldX, y, fieldW, kFieldH)]; + _replaceBox.placeholderString = @"Replace"; + _replaceBox.delegate = self; + [self addSubview:_replaceBox]; + + // ── Row 3: Checkboxes ──────────────────────────────────────────────────── + y -= kRowH; + _ignoreCaseButton = [NSButton checkboxWithTitle:@"Ignore case" + target:nil action:nil]; + _ignoreCaseButton.frame = NSMakeRect(fieldX, y, 110, kFieldH); + [self addSubview:_ignoreCaseButton]; + + _matchWordButton = [NSButton checkboxWithTitle:@"Match whole word" + target:nil action:nil]; + _matchWordButton.frame = NSMakeRect(fieldX + 114, y, 150, kFieldH); + [self addSubview:_matchWordButton]; + + // ── Row 4: Action buttons ──────────────────────────────────────────────── + y -= kRowH; + CGFloat bx = kMargin; + for (NSButton *btn in actionButtons) { + btn.frame = NSMakeRect(bx, y, maxBtnW, kFieldH); + [self addSubview:btn]; + bx += maxBtnW + 4; + } +} + +// ── Public API ─────────────────────────────────────────────────────────────── + +- (void)showWithText:(NSString *)text flags:(int)flags { + if (text && text.length > 0) + _findBox.stringValue = text; + + // Restore checkbox state from flags + _ignoreCaseButton.state = (flags & MMFRDExactMatch) ? NSControlStateValueOff + : NSControlStateValueOn; + _matchWordButton.state = (flags & MMFRDMatchWord) ? NSControlStateValueOn + : NSControlStateValueOff; + + self.hidden = NO; + [[self window] makeFirstResponder:_findBox]; +} + +- (NSString *)findString { return _findBox.stringValue; } +- (NSString *)replaceString { return _replaceBox.stringValue; } +- (BOOL)ignoreCase { return _ignoreCaseButton.state == NSControlStateValueOn; } +- (BOOL)matchWord { return _matchWordButton.state == NSControlStateValueOn; } + +// ── Background ─────────────────────────────────────────────────────────────── + +- (void)drawRect:(NSRect)dirtyRect { + [[NSColor windowBackgroundColor] setFill]; + NSRectFill(dirtyRect); + [super drawRect:dirtyRect]; +} + +- (void)updateLayer { + self.layer.borderColor = [NSColor separatorColor].CGColor; +} + +// ── Button actions ─────────────────────────────────────────────────────────── + +- (void)_findNext:(id)sender { + [_delegate findBarView:self findNext:YES]; +} + +- (void)_findPrevious:(id)sender { + [_delegate findBarView:self findNext:NO]; +} + +- (void)_replace:(id)sender { + [_delegate findBarView:self replace:NO]; +} + +- (void)_replaceAll:(id)sender { + [_delegate findBarView:self replace:YES]; +} + +- (void)_close:(id)sender { + self.hidden = YES; + [_delegate findBarViewDidClose:self]; +} + +// ── Dragging ────────────────────────────────────────────────────────────────── +// Allow the bar to be dragged anywhere within the text-editing area. +// The delegate supplies the allowed rect; if unavailable we fall back to the +// superview bounds so the bar can never leave the window. + +- (void)mouseDown:(NSEvent *)event { + // Record where inside the bar the user clicked so we can keep that point + // under the cursor during the drag. + NSPoint locInSelf = [self convertPoint:event.locationInWindow fromView:nil]; + _dragOffset = locInSelf; +} + +- (void)mouseDragged:(NSEvent *)event { + NSPoint locInSuper = [self.superview convertPoint:event.locationInWindow + fromView:nil]; + NSRect bounds = [_delegate findBarViewDraggableBounds:self]; + NSSize barSize = self.frame.size; + + CGFloat newX = locInSuper.x - _dragOffset.x; + CGFloat newY = locInSuper.y - _dragOffset.y; + + // Clamp so the bar stays fully inside the draggable bounds. + newX = MAX(NSMinX(bounds), MIN(newX, NSMaxX(bounds) - barSize.width)); + newY = MAX(NSMinY(bounds), MIN(newY, NSMaxY(bounds) - barSize.height)); + + [self setFrameOrigin:NSMakePoint(newX, newY)]; +} + +// ── NSControlTextEditingDelegate ───────────────────────────────────────────── +// Called by the field editor for each key command while a text field is active. +// This is the reliable way to intercept Escape and Return from NSTextField — +// overriding cancelOperation:/keyDown: on the text field itself is unreliable +// because the field editor (an NSTextView) is the actual first responder during +// editing and handles those events before the control sees them. + +- (BOOL)control:(NSControl *)control + textView:(NSTextView *)textView +doCommandBySelector:(SEL)cmd +{ + if (cmd == @selector(cancelOperation:)) { + // Escape → close the find bar + [self _close:nil]; + return YES; + } + if (cmd == @selector(insertNewline:)) { + // Return / Shift+Return → find next / previous + NSUInteger mods = [NSApp currentEvent].modifierFlags + & NSEventModifierFlagDeviceIndependentFlagsMask; + if (mods & NSEventModifierFlagShift) + [self _findPrevious:control]; + else + [self _findNext:control]; + return YES; + } + if (cmd == @selector(insertTab:)) { + // Tab → move focus: find → replace → find → … + NSTextField *next = (control == _findBox) ? _replaceBox : _findBox; + [[self window] makeFirstResponder:next]; + return YES; + } + if (cmd == @selector(insertBacktab:)) { + // Shift+Tab → move focus in reverse + NSTextField *prev = (control == _replaceBox) ? _findBox : _replaceBox; + [[self window] makeFirstResponder:prev]; + return YES; + } + return NO; +} + +@end diff --git a/src/MacVim/MMPreferenceController.h b/src/MacVim/MMPreferenceController.h index 5cc152616a..f721396f7a 100644 --- a/src/MacVim/MMPreferenceController.h +++ b/src/MacVim/MMPreferenceController.h @@ -21,6 +21,7 @@ IBOutlet NSPopUpButton *layoutPopUpButton; IBOutlet NSButton *autoInstallUpdateButton; IBOutlet NSView *sparkleUpdaterPane; + IBOutlet NSButton *findBarInlineButton; // Input pane IBOutlet NSButton *allowForceClickLookUpButton; @@ -36,6 +37,7 @@ - (IBAction)checkForUpdatesChanged:(id)sender; - (IBAction)appearanceChanged:(id)sender; - (IBAction)smoothResizeChanged:(id)sender; +- (IBAction)findBarModeChanged:(id)sender; // Appearance pane - (IBAction)fontPropertiesChanged:(id)sender; diff --git a/src/MacVim/MMPreferenceController.m b/src/MacVim/MMPreferenceController.m index 32f19ee84f..8e912372a9 100644 --- a/src/MacVim/MMPreferenceController.m +++ b/src/MacVim/MMPreferenceController.m @@ -177,6 +177,12 @@ - (IBAction)smoothResizeChanged:(id)sender [[MMAppController sharedInstance] refreshAllResizeConstraints]; } +- (IBAction)findBarModeChanged:(id)sender +{ + // Setting is read via NSUserDefaults each time a find dialog is shown, + // so no refresh of existing windows is required. +} + - (IBAction)cmdlineAlignBottomChanged:(id)sender { [[MMAppController sharedInstance] refreshAllTextViews]; diff --git a/src/MacVim/MMVimController.m b/src/MacVim/MMVimController.m index e48f55f411..af42c33cb9 100644 --- a/src/MacVim/MMVimController.m +++ b/src/MacVim/MMVimController.m @@ -1231,9 +1231,17 @@ - (void)handleMessage:(int)msgid data:(NSData *)data { NSDictionary *dict = [NSDictionary dictionaryWithData:data]; if (dict) { - [[MMFindReplaceController sharedInstance] - showWithText:[dict objectForKey:@"text"] - flags:[[dict objectForKey:@"flags"] intValue]]; + BOOL useInline = [[NSUserDefaults standardUserDefaults] + boolForKey:MMFindBarInlineKey]; + if (useInline) { + [[windowController vimView] + showFindBarWithText:[dict objectForKey:@"text"] + flags:[[dict objectForKey:@"flags"] intValue]]; + } else { + [[MMFindReplaceController sharedInstance] + showWithText:[dict objectForKey:@"text"] + flags:[[dict objectForKey:@"flags"] intValue]]; + } } } break; diff --git a/src/MacVim/MMVimView.h b/src/MacVim/MMVimView.h index f1a49009b3..ec00984ac8 100644 --- a/src/MacVim/MMVimView.h +++ b/src/MacVim/MMVimView.h @@ -9,6 +9,7 @@ */ #import +#import "MMFindBarView.h" @@ -19,13 +20,14 @@ @class MMVimController; -@interface MMVimView : NSView { +@interface MMVimView : NSView { /// The tab that has been requested to be closed and waiting on Vim to respond NSInteger pendingCloseTabID; MMTabline *tabline; MMVimController *vimController; MMTextView *textView; NSMutableArray *scrollbars; + MMFindBarView *findBarView; } @property BOOL pendingPlaceScrollbars; @@ -35,6 +37,7 @@ - (MMVimView *)initWithFrame:(NSRect)frame vimController:(MMVimController *)c; - (MMTextView *)textView; +- (MMFindBarView *)findBarView; - (void)cleanup; - (NSSize)desiredSize; @@ -70,4 +73,7 @@ - (void)setFrameSizeKeepGUISize:(NSSize)size; - (void)setFrame:(NSRect)frame; +- (void)showFindBarWithText:(NSString *)text flags:(int)flags; +- (void)hideFindBar; + @end diff --git a/src/MacVim/MMVimView.m b/src/MacVim/MMVimView.m index 6f8f4a3ded..a8cece62b5 100644 --- a/src/MacVim/MMVimView.m +++ b/src/MacVim/MMVimView.m @@ -20,6 +20,7 @@ #import "Miscellaneous.h" #import "MMCoreTextView.h" +#import "MMFindBarView.h" #import "MMTextView.h" #import "MMVimController.h" #import "MMVimView.h" @@ -84,6 +85,7 @@ - (void)liveResizeDidEnd; @implementation MMVimView { NSColor *tabColors[MMTabColorTypeCount]; + NSRect _lastTextRectForFindBar; // textView.frame at last find bar layout } - (MMVimView *)initWithFrame:(NSRect)frame @@ -135,7 +137,13 @@ - (MMVimView *)initWithFrame:(NSRect)frame tabline.addTabButton.action = @selector(addNewTab:); [tabline registerForDraggedTypes:@[getPasteboardFilenamesType()]]; [self addSubview:tabline]; - + + // Create the inline find bar (initially hidden, placed in top-right corner). + findBarView = [[MMFindBarView alloc] init]; + findBarView.hidden = YES; + findBarView.delegate = self; + [self addSubview:findBarView positioned:NSWindowAbove relativeTo:nil]; + return self; } @@ -224,6 +232,11 @@ - (MMTextView *)textView return textView; } +- (MMFindBarView *)findBarView +{ + return findBarView; +} + - (MMTabline *)tabline { return tabline; @@ -675,6 +688,62 @@ - (void)viewDidChangeEffectiveAppearance [winController performSelectorOnMainThread:@selector(refreshTabProperties) withObject:nil waitUntilDone:NO]; } } + +// ── Find bar public API ───────────────────────────────────────────────────── + +- (void)showFindBarWithText:(NSString *)text flags:(int)flags { + NSRect textRect = [textView frame]; + + // Only snap to the default top-right position when the bar is currently + // hidden. If it is already visible the user may have dragged it, so we + // keep its current position. + if (findBarView.hidden) { + NSSize barSize = findBarView.frame.size; + [findBarView setFrame:NSMakeRect( + NSMaxX(textRect) - barSize.width - 8, + NSMaxY(textRect) - barSize.height - 8, + barSize.width, + barSize.height)]; + } + _lastTextRectForFindBar = textRect; + + [findBarView showWithText:text flags:flags]; +} + +- (void)hideFindBar { + findBarView.hidden = YES; + if ([self window]) + [[self window] makeFirstResponder:textView]; +} + +// ── MMFindBarViewDelegate ──────────────────────────────────────────────────── + +- (void)findBarView:(MMFindBarView *)view findNext:(BOOL)forward { + [[vimController windowController] sendFindBarAction:forward ? 0 : 0x100 + findString:[view findString] + replaceString:[view replaceString] + ignoreCase:[view ignoreCase] + matchWord:[view matchWord]]; +} + +- (void)findBarView:(MMFindBarView *)view replace:(BOOL)replaceAll { + [[vimController windowController] sendFindBarAction:replaceAll ? 4 : 3 + findString:[view findString] + replaceString:[view replaceString] + ignoreCase:[view ignoreCase] + matchWord:[view matchWord]]; +} + +- (void)findBarViewDidClose:(MMFindBarView *)view { + [self hideFindBar]; +} + +- (NSRect)findBarViewDraggableBounds:(MMFindBarView *)view { + // Allow dragging anywhere within the text-editing area so the bar never + // overlaps the tabline or scrollbars. + return [textView frame]; +} + @end // MMVimView @@ -929,6 +998,28 @@ - (void)frameSizeMayHaveChanged:(BOOL)keepGUISize self.pendingPlaceScrollbars = NO; [self placeScrollbars]; + // Keep the find bar inside the text area when the window is resized. + // Use the delta of the text area's edges so that the bar tracks tabline + // show/hide and window resizes correctly, regardless of where the user + // may have dragged it. + if (findBarView && !findBarView.hidden) { + NSRect newTextRect = [textView frame]; + NSRect barFrame = findBarView.frame; + + // Shift bar by the same amount the text area's top-right corner moved. + CGFloat dx = NSMaxX(newTextRect) - NSMaxX(_lastTextRectForFindBar); + CGFloat dy = NSMaxY(newTextRect) - NSMaxY(_lastTextRectForFindBar); + CGFloat newX = barFrame.origin.x + dx; + CGFloat newY = barFrame.origin.y + dy; + + // Clamp so the bar stays fully inside the text area. + newX = MAX(NSMinX(newTextRect), MIN(newX, NSMaxX(newTextRect) - barFrame.size.width)); + newY = MAX(NSMinY(newTextRect), MIN(newY, NSMaxY(newTextRect) - barFrame.size.height)); + + [findBarView setFrameOrigin:NSMakePoint(newX, newY)]; + _lastTextRectForFindBar = newTextRect; + } + // It is possible that the current number of (rows,columns) is too big or // too small to fit the new frame. If so, notify Vim that the text // dimensions should change, but don't actually change the number of diff --git a/src/MacVim/MMWindowController.h b/src/MacVim/MMWindowController.h index e3c9cdfb10..fb5ed1c320 100644 --- a/src/MacVim/MMWindowController.h +++ b/src/MacVim/MMWindowController.h @@ -146,4 +146,10 @@ - (IBAction)joinAllStageManagerSets:(id)sender; - (IBAction)unjoinAllStageManagerSets:(id)sender; +- (void)sendFindBarAction:(int)tag + findString:(NSString *)findStr + replaceString:(NSString *)replStr + ignoreCase:(BOOL)ignoreCase + matchWord:(BOOL)matchWord; + @end diff --git a/src/MacVim/MMWindowController.m b/src/MacVim/MMWindowController.m index fc977f5ab1..b7aa5e7592 100644 --- a/src/MacVim/MMWindowController.m +++ b/src/MacVim/MMWindowController.m @@ -62,6 +62,7 @@ */ #import "MMAppController.h" +#import "MMFindBarView.h" #import "MMFindReplaceController.h" #import "MMFullScreenWindow.h" #import "MMTextView.h" @@ -1351,26 +1352,59 @@ - (IBAction)fontSizeDown:(id)sender - (IBAction)findAndReplace:(id)sender { NSInteger tag = [sender tag]; - MMFindReplaceController *fr = [MMFindReplaceController sharedInstance]; - int flags = 0; - - // NOTE: The 'flags' values must match the FRD_ defines in gui.h (except - // for 0x100 which we use to indicate a backward search). + NSString *findStr, *replStr; + BOOL ignoreCase, matchWord; + + if ([[NSUserDefaults standardUserDefaults] boolForKey:MMFindBarInlineKey]) { + MMFindBarView *fb = [vimView findBarView]; + findStr = [fb findString]; + replStr = [fb replaceString]; + ignoreCase = [fb ignoreCase]; + matchWord = [fb matchWord]; + } else { + MMFindReplaceController *frc = [MMFindReplaceController sharedInstance]; + findStr = [frc findString]; + replStr = [frc replaceString]; + ignoreCase = [frc ignoreCase]; + matchWord = [frc matchWord]; + } + + [self sendFindBarAction:(int)tag + findString:findStr + replaceString:replStr + ignoreCase:ignoreCase + matchWord:matchWord]; +} + +- (void)sendFindBarAction:(int)tag + findString:(NSString *)findStr + replaceString:(NSString *)replStr + ignoreCase:(BOOL)ignoreCase + matchWord:(BOOL)matchWord +{ + // Map IBAction sender tag values to FRD flag values (must match FRD_ + // defines in gui.h, except 0x100 which indicates a backward search): + // IBAction tag 0 → flags 0 (find forward) + // IBAction tag 1 → flags 0x100 (find backward) + // IBAction tag 2 → flags 3 (replace) + // IBAction tag 3 → flags 4 (replace all) + // Internal callers (MMFindBarViewDelegate) already pass the correct flag + // values directly (0, 0x100, 3, 4), so those pass through unchanged. + int flags; switch (tag) { case 1: flags = 0x100; break; - case 2: flags = 3; break; - case 3: flags = 4; break; + case 2: flags = 3; break; + case 3: flags = 4; break; + default: flags = tag; break; // 0=forward, or direct flag from delegate } - if ([fr matchWord]) - flags |= 0x08; - if (![fr ignoreCase]) - flags |= 0x10; + if (matchWord) flags |= 0x08; + if (!ignoreCase) flags |= 0x10; NSDictionary *args = [NSDictionary dictionaryWithObjectsAndKeys: - [fr findString], @"find", - [fr replaceString], @"replace", - [NSNumber numberWithInt:flags], @"flags", + findStr ?: @"", @"find", + replStr ?: @"", @"replace", + [NSNumber numberWithInt:flags], @"flags", nil]; [vimController sendMessage:FindReplaceMsgID data:[args dictionaryAsData]]; diff --git a/src/MacVim/MacVim.xcodeproj/project.pbxproj b/src/MacVim/MacVim.xcodeproj/project.pbxproj index e6eaa210e2..a503e08e45 100644 --- a/src/MacVim/MacVim.xcodeproj/project.pbxproj +++ b/src/MacVim/MacVim.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 1D384A0E100D671700D3C22F /* KeyBinding.plist in Resources */ = {isa = PBXBuildFile; fileRef = 1D384A0D100D671700D3C22F /* KeyBinding.plist */; }; 1D44972211FCA9B400B0630F /* MMCoreTextView+ToolTip.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D44972111FCA9B400B0630F /* MMCoreTextView+ToolTip.m */; }; 1D493D580C5247BF00AB718C /* Vim in Copy Executables */ = {isa = PBXBuildFile; fileRef = 1D493D570C5247BF00AB718C /* Vim */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + AA000003000000000000000C /* MMFindBarView.m in Sources */ = {isa = PBXBuildFile; fileRef = AA000002000000000000000B /* MMFindBarView.m */; }; 1D60088B0E96A0B2003763F0 /* MMFindReplaceController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D60088A0E96A0B2003763F0 /* MMFindReplaceController.m */; }; 1D80591F0E1185EA001699D1 /* Miscellaneous.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D80591D0E1185EA001699D1 /* Miscellaneous.m */; }; 1D80FBD40CBBD3B700102A1C /* MMFullScreenWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D80FBD00CBBD3B700102A1C /* MMFullScreenWindow.m */; }; @@ -189,6 +190,8 @@ 1D384A0D100D671700D3C22F /* KeyBinding.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = KeyBinding.plist; sourceTree = ""; }; 1D44972111FCA9B400B0630F /* MMCoreTextView+ToolTip.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MMCoreTextView+ToolTip.m"; sourceTree = ""; }; 1D493D570C5247BF00AB718C /* Vim */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = Vim; path = ../Vim; sourceTree = SOURCE_ROOT; }; + AA000001000000000000000A /* MMFindBarView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MMFindBarView.h; sourceTree = ""; }; + AA000002000000000000000B /* MMFindBarView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MMFindBarView.m; sourceTree = ""; }; 1D6008890E96A0B2003763F0 /* MMFindReplaceController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MMFindReplaceController.h; sourceTree = ""; }; 1D60088A0E96A0B2003763F0 /* MMFindReplaceController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MMFindReplaceController.m; sourceTree = ""; }; 1D80591D0E1185EA001699D1 /* Miscellaneous.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Miscellaneous.m; sourceTree = ""; }; @@ -515,6 +518,8 @@ 9098943B2A56ECF6007B84A3 /* MMWhatsNewController.m */, 925B55CB254B604A006B047E /* MMTabline */, 1D44972111FCA9B400B0630F /* MMCoreTextView+ToolTip.m */, + AA000001000000000000000A /* MMFindBarView.h */, + AA000002000000000000000B /* MMFindBarView.m */, 1D6008890E96A0B2003763F0 /* MMFindReplaceController.h */, 1D60088A0E96A0B2003763F0 /* MMFindReplaceController.m */, 1DE63FF90E71820F00959BDB /* MMCoreTextView.h */, @@ -1240,6 +1245,7 @@ 1D80591F0E1185EA001699D1 /* Miscellaneous.m in Sources */, 9098943C2A56ECF6007B84A3 /* MMWhatsNewController.m in Sources */, 1D145C7F0E5227CE00691AA0 /* MMTextViewHelper.m in Sources */, + AA000003000000000000000C /* MMFindBarView.m in Sources */, 1D60088B0E96A0B2003763F0 /* MMFindReplaceController.m in Sources */, 1DE63FFB0E71820F00959BDB /* MMCoreTextView.m in Sources */, 92C6F6E825587E1C007AE21E /* MMTab.m in Sources */, diff --git a/src/MacVim/MacVimTests/MacVimTests.m b/src/MacVim/MacVimTests/MacVimTests.m index 213c649e9f..b0dd542904 100644 --- a/src/MacVim/MacVimTests/MacVimTests.m +++ b/src/MacVim/MacVimTests/MacVimTests.m @@ -15,6 +15,7 @@ #import "Miscellaneous.h" #import "MMAppController.h" #import "MMApplication.h" +#import "MMFindBarView.h" #import "MMFullScreenWindow.h" #import "MMWindow.h" #import "MMTabline.h" @@ -1576,4 +1577,78 @@ - (void)testIPCSelectedText { [self waitForEventHandlingAndVimProcess]; } +// ── MMFindBarView tests ─────────────────────────────────────────────────────── + +/// Test that showWithText:flags: correctly restores the Ignore Case and Match +/// Word checkbox states from the flags bitmask. +- (void)testFindBarShowWithFlags { + MMFindBarView *bar = [[MMFindBarView alloc] init]; + + // flags = 0: ignoreCase ON (no ExactMatch bit), matchWord OFF + [bar showWithText:@"hello" flags:0]; + XCTAssertTrue([bar ignoreCase], @"ignoreCase should be ON when ExactMatch bit is clear"); + XCTAssertFalse([bar matchWord], @"matchWord should be OFF when MatchWord bit is clear"); + XCTAssertEqualObjects([bar findString], @"hello"); + + // flags = MMFRDExactMatch (0x10): ignoreCase OFF + [bar showWithText:@"world" flags:0x10]; + XCTAssertFalse([bar ignoreCase], @"ignoreCase should be OFF when ExactMatch bit (0x10) is set"); + XCTAssertFalse([bar matchWord], @"matchWord should still be OFF"); + XCTAssertEqualObjects([bar findString], @"world"); + + // flags = MMFRDMatchWord (0x08): matchWord ON + [bar showWithText:@"foo" flags:0x08]; + XCTAssertTrue([bar ignoreCase], @"ignoreCase should be ON (ExactMatch bit clear)"); + XCTAssertTrue([bar matchWord], @"matchWord should be ON when MatchWord bit (0x08) is set"); + + // flags = MMFRDExactMatch | MMFRDMatchWord (0x18): both set + [bar showWithText:@"bar" flags:0x18]; + XCTAssertFalse([bar ignoreCase], @"ignoreCase should be OFF"); + XCTAssertTrue([bar matchWord], @"matchWord should be ON"); + + // Passing nil text should not crash and should not clear existing text + NSString *prevText = [bar findString]; + [bar showWithText:nil flags:0]; + XCTAssertEqualObjects([bar findString], prevText, @"nil text should not clear the find field"); +} + +/// Test that calling showFindBarWithText:flags: on MMVimView snaps the bar to +/// the top-right corner only when the bar is hidden, and preserves the current +/// position when the bar is already visible. +- (void)testFindBarPositionPreservedOnReshow { + [self createTestVimWindow]; + + MMAppController *app = MMAppController.sharedInstance; + MMVimView *vimView = app.keyVimController.windowController.vimView; + MMFindBarView *bar = [vimView findBarView]; + + // Bar starts hidden; first show should snap to top-right. + XCTAssertTrue(bar.hidden, @"find bar should start hidden"); + [self setDefault:MMFindBarInlineKey toValue:@YES]; + [vimView showFindBarWithText:@"test" flags:0]; + + NSRect snapFrame = bar.frame; + NSRect textRect = vimView.textView.frame; + + // Verify snapped position is near the top-right corner (within 1pt tolerance). + XCTAssertEqualWithAccuracy(NSMaxX(snapFrame), NSMaxX(textRect) - 8, 1, + @"find bar right edge should be 8pt inside text area right edge on first show"); + XCTAssertEqualWithAccuracy(NSMaxY(snapFrame), NSMaxY(textRect) - 8, 1, + @"find bar top edge should be 8pt below text area top edge on first show"); + + // Move the bar to a different position. + NSPoint movedOrigin = NSMakePoint(snapFrame.origin.x - 50, snapFrame.origin.y - 30); + [bar setFrameOrigin:movedOrigin]; + + // Call showFindBarWithText: again while bar is already visible. + [vimView showFindBarWithText:@"test2" flags:0]; + + XCTAssertEqualWithAccuracy(bar.frame.origin.x, movedOrigin.x, 1, + @"X position should be preserved when bar is already visible"); + XCTAssertEqualWithAccuracy(bar.frame.origin.y, movedOrigin.y, 1, + @"Y position should be preserved when bar is already visible"); + XCTAssertEqualObjects([bar findString], @"test2", + @"find text should be updated even when position is preserved"); +} + @end diff --git a/src/MacVim/Miscellaneous.h b/src/MacVim/Miscellaneous.h index 0216de6f21..8e3905f289 100644 --- a/src/MacVim/Miscellaneous.h +++ b/src/MacVim/Miscellaneous.h @@ -75,6 +75,7 @@ extern NSString *MMUpdaterPrereleaseChannelKey; extern NSString *MMLastUsedBundleVersionKey; ///< The last used version of MacVim before this launch extern NSString *MMShowWhatsNewOnStartupKey; extern NSString *MMScrollOneDirectionOnlyKey; +extern NSString *MMFindBarInlineKey; // Enum for MMUntitledWindowKey diff --git a/src/MacVim/Miscellaneous.m b/src/MacVim/Miscellaneous.m index 3618db398e..dbe479a615 100644 --- a/src/MacVim/Miscellaneous.m +++ b/src/MacVim/Miscellaneous.m @@ -71,6 +71,7 @@ NSString *MMLastUsedBundleVersionKey = @"MMLastUsedBundleVersion"; NSString *MMShowWhatsNewOnStartupKey = @"MMShowWhatsNewOnStartup"; NSString *MMScrollOneDirectionOnlyKey = @"MMScrollOneDirectionOnly"; +NSString *MMFindBarInlineKey = @"MMFindBarInline"; @implementation NSIndexSet (MMExtras)