From db26a3b1acaa9aa212685e92fc066b7320ea7905 Mon Sep 17 00:00:00 2001 From: Yee Cheng Chin Date: Tue, 11 Feb 2025 11:25:33 -0800 Subject: [PATCH] Fix Writing Tools integration and fix up Services integration with texts Apple "Intelligence" Writing Tools was previously not working correctly with MacVim. When the user chooses to replace the original selection with the updated texts, MacVim mistreats the input and treat them as commands instead of raw texts. The reason was that even though this service uses the NSServicesMenuRequestor API to obtain the selected texts, it does not use it to send over the replacement. Instead, it uses NSTextInput's `insertText:replacementRange` to do so instead, which we never implemented properly. The reason behind this choice was probably because Writing Tools first shows a UI with user interaction and has a delay between obtaining the texts and replacing them, like a regular Services menu. This means the selection may already be invalid by the time it requests a replacement. To fix this, add a new IPC API `replaceSelectedText` to replace the selected texts and redirect `insertText:replacementRange` to use it if the replacement range is non-empty. This isn't the most correct implementation of the protocol but should work in most cases. We don't have a way to implement it "correctly" as MacVim does not have easy access to Vim's internal text storage. Also make sure the Service menu uses this API (for things like "convert to full width" and Traditional/Simplified Chinese conversions). The old method of simple injecting a normal mode command `s` before the text was horribly buggy. It also works with visual block selection properly now. The implementation uses Vim's register put functionality because Vim doesn't have an API to simply replace a block of text, and everything has to go through registers. At the same time, replace the implementation for the old `selectedText` IPC API to not do this, because Vim *did* end an API to do so for obtaining texts (via `getregion()`) and it's more stable to use this than to manually cache/restore registers. Related: vim/vim#16596 (fixes `setreg()` which this uses) Fix #1512 --- src/MacVim/MMBackend.m | 260 +++++++++++++-------------- src/MacVim/MMCoreTextView.m | 5 +- src/MacVim/MMTextView.m | 2 +- src/MacVim/MMTextViewHelper.h | 2 +- src/MacVim/MMTextViewHelper.m | 16 +- src/MacVim/MMVimController.h | 3 + src/MacVim/MMVimController.m | 43 +++++ src/MacVim/MMWindowController.h | 2 +- src/MacVim/MMWindowController.m | 41 ++--- src/MacVim/MacVim.h | 3 +- src/MacVim/MacVimTests/MacVimTests.m | 79 +++++++- src/evalfunc.c | 6 +- src/normal.c | 3 +- src/proto/evalfunc.pro | 2 + src/proto/normal.pro | 4 + 15 files changed, 286 insertions(+), 185 deletions(-) diff --git a/src/MacVim/MMBackend.m b/src/MacVim/MMBackend.m index 66ea170252..21ade18abb 100644 --- a/src/MacVim/MMBackend.m +++ b/src/MacVim/MMBackend.m @@ -1380,158 +1380,140 @@ - (NSString *)evaluateExpression:(in bycopy NSString *)expr return eval; } -/// Extracts the text currently selected in visual mode, and returns it. -/// -/// @return the string representing the selected text, or NULL if failure. -static char_u *extractSelectedText(void) -{ - // Note: Most of the functionality in Vim that allows for extracting useful - // text from a selection are in the register & clipboard utility functions. - // Unfortunately, most of those functions would actually send the text to - // the system clipboard, which we don't want (since we just want to extract - // the text instead of polluting the system clipboard). We don't want to - // refactor upstream Vim code too much to avoid merge pains later, so we - // duplicate a fair bit of the code from the functions below. - - if (!(VIsual_active && (State & MODE_NORMAL))) { - // This only works when we are in visual mode and have stuff to select. - return NULL; - } - - // Step 1: Find a register to yank the selection to. If we don't do this we - // have to duplicate a lot of code in op_yank(). We save it off to a backup - // first so we can restore it later to avoid polluting the registers. - - // Just use the yank / '0' register as it makes sense, but it doesn't - // really matter. - yankreg_T *target_reg = get_y_register(0); - - // Move the contents to the backup without doing memory allocs. - yankreg_T backup_reg = *target_reg; - target_reg->y_array = NULL; - target_reg->y_size = 0; - - // Step 2: Preserve the local states, and then invoke yank. - // Note: These were copied from clip_get_selection() in clipboard.c - yankreg_T *old_y_previous, *old_y_current; - pos_T old_cursor; - pos_T old_visual; - int old_visual_mode; - colnr_T old_curswant; - int old_set_curswant; - pos_T old_op_start, old_op_end; - oparg_T oa; - cmdarg_T ca; - - // Avoid triggering autocmds such as TextYankPost. - block_autocmds(); - - // Yank the selected text into the target register. - old_y_previous = get_y_previous(); - old_y_current = get_y_current(); - old_cursor = curwin->w_cursor; - old_curswant = curwin->w_curswant; - old_set_curswant = curwin->w_set_curswant; - old_op_start = curbuf->b_op_start; - old_op_end = curbuf->b_op_end; - old_visual = VIsual; - old_visual_mode = VIsual_mode; - clear_oparg(&oa); - oa.regname = '0'; // Use the '0' (yank) register. We will restore it later to avoid pollution. - oa.op_type = OP_YANK; - CLEAR_FIELD(ca); - ca.oap = &oa; - ca.cmdchar = 'y'; - ca.count1 = 1; - ca.retval = CA_NO_ADJ_OP_END; - do_pending_operator(&ca, 0, TRUE); - - // Step 3: Extract the text from the yank ('0') register. - char_u *str = get_reg_contents(0, 0); - - // Step 4: Clean up the yank register, and restore it back. - set_y_current(target_reg); // should not be necessary as it's done in do_pending_operator above (since regname was set to 0), but just to be safe and verbose in intention. - free_yank_all(); - *target_reg = backup_reg; - - // Step 5: Restore all the loose states that were modified during yank. - // Note: These were copied from clip_get_selection() in clipboard.c - set_y_previous(old_y_previous); - set_y_current(old_y_current); - curwin->w_cursor = old_cursor; - changed_cline_bef_curs(); // need to update w_virtcol et al - curwin->w_curswant = old_curswant; - curwin->w_set_curswant = old_set_curswant; - curbuf->b_op_start = old_op_start; - curbuf->b_op_end = old_op_end; - VIsual = old_visual; - VIsual_mode = old_visual_mode; - - unblock_autocmds(); - - return str; -} - -/// Extract the currently selected text (in visual mode) and send that to the -/// provided pasteboard. -- (BOOL)selectedTextToPasteboard:(byref NSPasteboard *)pboard +- (BOOL)hasSelectedText { - if (VIsual_active && (State & MODE_NORMAL)) { - // If there is no pasteboard, just return YES to indicate that there is - // text to copy. - if (!pboard) - return YES; + return (VIsual_active && (State & MODE_NORMAL)); +} - char_u *str = extractSelectedText(); - if (!str) - return NO; +/// Returns the currently selected text. +- (NSString *)selectedText +{ + if (VIsual_active && (State & MODE_NORMAL)) { + // This is basically doing the following: + // - join(getregion(getpos("."), getpos("v"), { type: visualmode() }),"\n") + // - Add extra "\n" if we have a linewise selection - if (output_conv.vc_type != CONV_NONE) { - char_u *conv_str = string_convert(&output_conv, str, NULL); - if (conv_str) { - vim_free(str); - str = conv_str; + // Call getpos() + typval_T pos1, pos2; + { + typval_T arg_posmark; + init_tv(&arg_posmark); + arg_posmark.v_type = VAR_STRING; + + arg_posmark.vval.v_string = (char_u*)"."; + typval_T args1[1] = { arg_posmark }; + f_getpos(args1, &pos1); + if (pos1.v_type != VAR_LIST) + return nil; + + arg_posmark.vval.v_string = (char_u*)"v"; + typval_T args2[1] = { arg_posmark }; + f_getpos(args2, &pos2); + if (pos2.v_type != VAR_LIST) { + list_unref(pos1.vval.v_list); + return nil; } } - NSString *string = [[NSString alloc] initWithUTF8String:(char*)str]; - - NSArray *types = [NSArray arrayWithObject:NSPasteboardTypeString]; - [pboard declareTypes:types owner:nil]; - BOOL ok = [pboard setString:string forType:NSPasteboardTypeString]; - - [string release]; - vim_free(str); + // Call getregion() + typval_T arg_opts; + init_tv(&arg_opts); + arg_opts.v_type = VAR_DICT; + arg_opts.vval.v_dict = dict_alloc(); + arg_opts.vval.v_dict->dv_refcount += 1; + + char_u visualmode[2] = { VIsual_mode, '\0' }; + dict_add_string(arg_opts.vval.v_dict, "type", visualmode); + + typval_T args[3] = { pos1, pos2, arg_opts }; + typval_T regionLines; + f_getregion(args, ®ionLines); + + // Join the results + NSMutableArray *returnLines = [NSMutableArray array]; + if (regionLines.v_type == VAR_LIST) { + list_T *lines = regionLines.vval.v_list; + for (listitem_T *item = lines->lv_first; item != NULL; item = item->li_next) { + if (item->li_tv.v_type == VAR_STRING) { + char_u *str = item->li_tv.vval.v_string; + if (output_conv.vc_type != CONV_NONE) { + char_u *conv_str = string_convert(&output_conv, str, NULL); + if (conv_str) { + [returnLines addObject:[NSString stringWithUTF8String:(char*)conv_str]]; + vim_free(conv_str); + } + } else { + [returnLines addObject:[NSString stringWithUTF8String:(char*)str]]; + } + } + } + list_unref(lines); + } + dict_unref(arg_opts.vval.v_dict); + list_unref(pos1.vval.v_list); + list_unref(pos2.vval.v_list); - return ok; + if (VIsual_mode == 'V') + [returnLines addObject:@""]; // need trailing endline for linewise + return [returnLines componentsJoinedByString:@"\n"]; } - - return NO; + return nil; } -/// Returns the currently selected text. We should consolidate this with -/// selectedTextToPasteboard: above when we have time. (That function has a -/// fast path just to query whether selected text exists) -- (NSString *)selectedText +/// Replace the selected text in visual mode with the new suppiled one. +- (oneway void)replaceSelectedText:(in bycopy NSString *)text { if (VIsual_active && (State & MODE_NORMAL)) { - char_u *str = extractSelectedText(); - if (!str) - return nil; - - if (output_conv.vc_type != CONV_NONE) { - char_u *conv_str = string_convert(&output_conv, str, NULL); - if (conv_str) { - vim_free(str); - str = conv_str; - } - } - - NSString *string = [[NSString alloc] initWithUTF8String:(char*)str]; - vim_free(str); - return [string autorelease]; + // The only real way Vim has in doing this consistently is to use the + // register put functionality as there is no generic API for this. + // We find an arbitrary register ('0'), back it up, replace it with our + // own content, paste it in, then restore the register to old value. + yankreg_T *target_reg = get_y_register(0); + yankreg_T backup_reg = *target_reg; + target_reg->y_array = NULL; + target_reg->y_size = 0; + + // If selection is blockwise, we try to match it. Only do it if input + // and selected text have same number of lines, as otherwise it could + // be awkward. + int yank_type = MAUTO; + char_u *vimtext = [text vimStringSave]; + if (VIsual_mode == Ctrl_V) { + long text_lines = string_count(vimtext, (char_u*)"\n", FALSE) + 1; + + linenr_T v1 = VIsual.lnum; + linenr_T v2 = curwin->w_cursor.lnum; + long num_lines = v1 > v2 ? v1 - v2 + 1 : v2 - v1 + 1; + + if (text_lines == num_lines) + yank_type = MBLOCK; + } + write_reg_contents_ex('0', vimtext, -1, FALSE, yank_type, -1); + vim_free(vimtext); + + oparg_T oap; + CLEAR_FIELD(oap); + oap.regname = '0'; + + cmdarg_T cap; + CLEAR_FIELD(cap); + cap.oap = &oap; + cap.cmdchar = 'P'; + cap.count1 = 1; + + nv_put(&cap); + + // Clean up the temporary register, and restore the old state. + yankreg_T *old_y_current = get_y_current(); + set_y_current(target_reg); + free_yank_all(); + set_y_current(old_y_current); + *target_reg = backup_reg; + + // nv_put does not trigger a redraw command as it's done on a higher + // level, so just do a manual one here to make sure it's done. + [self redrawScreen]; } - return nil; } /// Returns whether the provided mouse screen position is on a visually diff --git a/src/MacVim/MMCoreTextView.m b/src/MacVim/MMCoreTextView.m index 480de9674f..7a44da201c 100644 --- a/src/MacVim/MMCoreTextView.m +++ b/src/MacVim/MMCoreTextView.m @@ -648,8 +648,7 @@ - (void)keyDown:(NSEvent *)event - (void)insertText:(id)string replacementRange:(NSRange)replacementRange { - // We are not currently replacementRange right now. - [helper insertText:string]; + [helper insertText:string replacementRange:replacementRange]; } - (void)doCommandBySelector:(SEL)selector @@ -1992,7 +1991,7 @@ - (void)quickLookWithEvent:(NSEvent *)event // top of said selection and if so, show definition of that instead. MMVimController *vc = [self vimController]; id backendProxy = [vc backendProxy]; - if ([backendProxy selectedTextToPasteboard:nil]) { + if ([backendProxy hasSelectedText]) { int selRow = 0, selCol = 0; const BOOL isMouseInSelection = [backendProxy mouseScreenposIsSelection:row column:col selRow:&selRow selCol:&selCol]; diff --git a/src/MacVim/MMTextView.m b/src/MacVim/MMTextView.m index e7c4923cf2..6de0e56478 100644 --- a/src/MacVim/MMTextView.m +++ b/src/MacVim/MMTextView.m @@ -724,7 +724,7 @@ - (void)keyDown:(NSEvent *)event - (void)insertText:(id)string { - [helper insertText:string]; + [helper insertText:string replacementRange:NSMakeRange(0, 0)]; } - (void)doCommandBySelector:(SEL)selector diff --git a/src/MacVim/MMTextViewHelper.h b/src/MacVim/MMTextViewHelper.h index 0aca93ea78..71983661fb 100644 --- a/src/MacVim/MMTextViewHelper.h +++ b/src/MacVim/MMTextViewHelper.h @@ -65,7 +65,7 @@ - (NSColor *)insertionPointColor; - (void)keyDown:(NSEvent *)event; -- (void)insertText:(id)string; +- (void)insertText:(id)string replacementRange:(NSRange)replacementRange; - (void)doCommandBySelector:(SEL)selector; - (void)scrollWheel:(NSEvent *)event; - (void)mouseDown:(NSEvent *)event; diff --git a/src/MacVim/MMTextViewHelper.m b/src/MacVim/MMTextViewHelper.m index b2834f7bd7..c854e95b87 100644 --- a/src/MacVim/MMTextViewHelper.m +++ b/src/MacVim/MMTextViewHelper.m @@ -221,7 +221,7 @@ - (void)keyDown:(NSEvent *)event currentEvent = nil; } -- (void)insertText:(id)string +- (void)insertText:(id)string replacementRange:(NSRange)replacementRange { if ([self hasMarkedText]) { [self sendMarkedText:nil position:0]; @@ -241,6 +241,20 @@ - (void)insertText:(id)string if ([string isKindOfClass:[NSAttributedString class]]) string = [string string]; + if (replacementRange.length > 0) + { + // Replacement range is a concept we don't really have a way to fulfill + // as we don't have proper access to the underlying text storage. This + // should usually be triggered when we have selected text though, and + // so we simply ask Vim to replace the current selection with the new + // text, and it should hopefully work. + // Only known way of this being called is Apple Intelligence Writing + // Tools. + MMVimController *vc = [self vimController]; + [vc replaceSelectedText:string]; + return; + } + //int len = [string length]; //ASLogDebug(@"len=%d char[0]=%#x char[1]=%#x string='%@'", [string length], // [string characterAtIndex:0], diff --git a/src/MacVim/MMVimController.h b/src/MacVim/MMVimController.h index 3656ea22be..ec4bc9505c 100644 --- a/src/MacVim/MMVimController.h +++ b/src/MacVim/MMVimController.h @@ -92,6 +92,9 @@ - (NSString *)evaluateVimExpression:(NSString *)expr; - (id)evaluateVimExpressionCocoa:(NSString *)expr errorString:(NSString **)errstr; +- (BOOL)hasSelectedText; +- (NSString *)selectedText; +- (void)replaceSelectedText:(NSString *)text; - (void)processInputQueue:(NSArray *)queue; #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_12_2 - (NSTouchBar *)makeTouchBar; diff --git a/src/MacVim/MMVimController.m b/src/MacVim/MMVimController.m index de6b9d0964..4d0a295722 100644 --- a/src/MacVim/MMVimController.m +++ b/src/MacVim/MMVimController.m @@ -533,6 +533,49 @@ - (id)evaluateVimExpressionCocoa:(NSString *)expr return eval; } +- (BOOL)hasSelectedText +{ + BOOL hasSelectedText = NO; + if (backendProxy) { + @try { + hasSelectedText = [backendProxy hasSelectedText]; + } + @catch (NSException *ex) { + ASLogDebug(@"hasSelectedText: failed: pid=%d reason=%@", + pid, ex); + } + } + return hasSelectedText; +} + +- (NSString *)selectedText +{ + NSString *selectedText = nil; + if (backendProxy) { + @try { + selectedText = [backendProxy selectedText]; + } + @catch (NSException *ex) { + ASLogDebug(@"selectedText: failed: pid=%d reason=%@", + pid, ex); + } + } + return selectedText; +} + +- (void)replaceSelectedText:(NSString *)text +{ + if (backendProxy) { + @try { + [backendProxy replaceSelectedText:text]; + } + @catch (NSException *ex) { + ASLogDebug(@"replaceSelectedText: failed: pid=%d reason=%@", + pid, ex); + } + } +} + - (id)backendProxy { return backendProxy; diff --git a/src/MacVim/MMWindowController.h b/src/MacVim/MMWindowController.h index 0ba7d1cc2d..e3c9cdfb10 100644 --- a/src/MacVim/MMWindowController.h +++ b/src/MacVim/MMWindowController.h @@ -18,7 +18,7 @@ @class MMVimView; @interface MMWindowController : NSWindowController< - NSWindowDelegate + NSWindowDelegate, NSServicesMenuRequestor #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_14 , NSMenuItemValidation #endif diff --git a/src/MacVim/MMWindowController.m b/src/MacVim/MMWindowController.m index 45f2c8c86f..c6cb16605f 100644 --- a/src/MacVim/MMWindowController.m +++ b/src/MacVim/MMWindowController.m @@ -86,7 +86,6 @@ - (void)resizeWindowToFitContentSize:(NSSize)contentSize keepOnScreen:(BOOL)onScreen; - (NSSize)constrainContentSizeToScreenSize:(NSSize)contentSize; - (NSRect)constrainFrame:(NSRect)frame; -- (BOOL)askBackendForSelectedText:(NSPasteboard *)pb; - (void)updateTablineSeparator; - (void)hideTablineSeparator:(BOOL)hide; - (void)doFindNext:(BOOL)next; @@ -1642,7 +1641,7 @@ - (IBAction)unjoinAllStageManagerSets:(id)sender - (id)validRequestorForSendType:(NSString *)sendType returnType:(NSString *)returnType { - const BOOL sendOk = (([sendType isEqual:NSPasteboardTypeString] && [self askBackendForSelectedText:nil]) + const BOOL sendOk = (([sendType isEqual:NSPasteboardTypeString] && [self.vimController hasSelectedText]) || [sendType length] == 0); const BOOL returnOk = ([returnType isEqual:NSPasteboardTypeString] || [returnType length] == 0); if (sendOk && returnOk) @@ -1662,7 +1661,14 @@ - (BOOL)writeSelectionToPasteboard:(NSPasteboard *)pboard // We should really be fine here since we already checked the types in // validRequestsForSendType: above. (void)types; - return [self askBackendForSelectedText:pboard]; + + NSString *string = [vimController selectedText]; + if (string != nil) { + NSArray *types = [NSArray arrayWithObject:NSPasteboardTypeString]; + [pboard declareTypes:types owner:nil]; + return [pboard setString:string forType:NSPasteboardTypeString]; + } + return NO; } /// Called by the OS when it tries to update the selection. This could happen @@ -1672,9 +1678,8 @@ - (BOOL)readSelectionFromPasteboard:(NSPasteboard *)pboard // Replace the current selection with the text on the pasteboard. NSArray *types = [pboard types]; if ([types containsObject:NSPasteboardTypeString]) { - NSString *input = [NSString stringWithFormat:@"s%@", - [pboard stringForType:NSPasteboardTypeString]]; - [vimController addVimInput:input]; + NSString *input = [pboard stringForType:NSPasteboardTypeString]; + [vimController replaceSelectedText:input]; return YES; } @@ -1986,30 +1991,6 @@ - (NSRect)constrainFrame:(NSRect)frame return [decoratedWindow frameRectForContentRect:contentRect]; } -/// Ask Vim to fill in the pasteboard with the currently selected text in visual mode. -- (BOOL)askBackendForSelectedText:(NSPasteboard *)pb -{ - // This could potentially be done via evaluateExpression by yanking the - // selection, then returning the results via getreg('@') and restoring the - // register. Using a dedicated API is probably a little safer (e.g. it - // prevents TextYankPost autocmd's from triggering) and efficient - // and hence this is what we use for now. - BOOL reply = NO; - id backendProxy = [vimController backendProxy]; - - if (backendProxy) { - @try { - reply = [backendProxy selectedTextToPasteboard:pb]; - } - @catch (NSException *ex) { - ASLogDebug(@"selectedTextToPasteboard: failed: pid=%d reason=%@", - [vimController pid], ex); - } - } - - return reply; -} - - (void)updateTablineSeparator { BOOL tablineVisible = ![[vimView tabline] isHidden]; diff --git a/src/MacVim/MacVim.h b/src/MacVim/MacVim.h index b863bad620..3d2b17425f 100644 --- a/src/MacVim/MacVim.h +++ b/src/MacVim/MacVim.h @@ -192,8 +192,9 @@ typedef NSString* NSAttributedStringKey; - (NSString *)evaluateExpression:(in bycopy NSString *)expr; - (id)evaluateExpressionCocoa:(in bycopy NSString *)expr errorString:(out bycopy NSString **)errstr; -- (BOOL)selectedTextToPasteboard:(byref NSPasteboard *)pboard; +- (BOOL)hasSelectedText; - (NSString *)selectedText; +- (oneway void)replaceSelectedText:(in bycopy NSString *)text; - (BOOL)mouseScreenposIsSelection:(int)row column:(int)column selRow:(byref int *)startRow selCol:(byref int *)startCol; - (oneway void)acknowledgeConnection; @end diff --git a/src/MacVim/MacVimTests/MacVimTests.m b/src/MacVim/MacVimTests/MacVimTests.m index 6cb8c5d294..463affa405 100644 --- a/src/MacVim/MacVimTests/MacVimTests.m +++ b/src/MacVim/MacVimTests/MacVimTests.m @@ -362,17 +362,22 @@ - (void)waitTimeout:(double)delaySecs { /// Send a single key to MacVim via event handling system. - (void)sendKeyToVim:(NSString*)chars withMods:(int)mods { NSApplication* app = [NSApplication sharedApplication]; + + NSString *modChars = chars; + if (mods & NSEventModifierFlagControl) { + unichar ch = [chars characterAtIndex:0] & ~0x60; + modChars = [NSString stringWithCharacters:&ch length:1]; + } NSEvent* keyEvent = [NSEvent keyEventWithType:NSEventTypeKeyDown location:NSMakePoint(50, 50) modifierFlags:mods timestamp:100 windowNumber:[[NSApp mainWindow] windowNumber] context:0 - characters:chars + characters:modChars charactersIgnoringModifiers:chars isARepeat:NO keyCode:0]; - [app postEvent:keyEvent atStart:NO]; } @@ -1384,4 +1389,74 @@ - (void) testFullScreenNonNativeMultiScreen { XCTAssertTrue(NSPointInRect(winController.window.frame.origin, NSScreen.screens[0].frame)); } +#pragma mark Vim IPC + +/// Test the selected text related IPC APIs +- (void)testIPCSelectedText { + [self createTestVimWindow]; + [self sendStringToVim:@":put =['abcd', 'efgh', 'ijkl']\nggdd" withMods:0]; + [self waitForEventHandlingAndVimProcess]; + + MMAppController *app = MMAppController.sharedInstance; + MMVimController *vc = app.keyVimController; + + // Set up register + [self sendStringToVim:@"ggyy" withMods:0]; + [self waitForEventHandlingAndVimProcess]; + NSString *regcontents = [vc evaluateVimExpression:@"getreg()"]; + XCTAssertEqualObjects(regcontents, @"abcd\n"); + + // Get selected texts in visual mode + XCTAssertFalse([vc hasSelectedText]); + XCTAssertNil([vc selectedText]); + [self sendStringToVim:@"lvjl" withMods:0]; + [self waitForEventHandlingAndVimProcess]; + XCTAssertTrue([vc hasSelectedText]); + XCTAssertEqualObjects([vc selectedText], @"bcd\nefg"); + + // Get selected texts in visual line mode + [self sendStringToVim:@"V" withMods:0]; + [self waitForEventHandlingAndVimProcess]; + XCTAssertTrue([vc hasSelectedText]); + XCTAssertEqualObjects([vc selectedText], @"abcd\nefgh\n"); + + // Get selected texts in visual block mode + [self sendKeyToVim:@"v" withMods:NSEventModifierFlagControl]; + [self waitForEventHandlingAndVimProcess]; + XCTAssertTrue([vc hasSelectedText]); + XCTAssertEqualObjects([vc selectedText], @"bc\nfg"); + + // Set selected texts in visual block mode + NSString *changedtick = [vc evaluateVimExpression:@"b:changedtick"]; + [vc replaceSelectedText:@"xyz\n1234"]; + NSString *changedtick2 = [vc evaluateVimExpression:@"b:changedtick"]; + XCTAssertEqualObjects([vc evaluateVimExpression:@"getline(1)"], @"axyz d"); + XCTAssertEqualObjects([vc evaluateVimExpression:@"getline(2)"], @"e1234h"); + XCTAssertEqualObjects([vc evaluateVimExpression:@"getline(3)"], @"ijkl"); + XCTAssertNotEqualObjects(changedtick, changedtick2); + + // Make sure replacing texts when nothing is selected won't set anything + [vc replaceSelectedText:@"foobar"]; + NSString *changedtick3 = [vc evaluateVimExpression:@"b:changedtick"]; + XCTAssertEqualObjects(changedtick2, changedtick3); + + // Select in visual block again but send a different number of lines, make sure we intentionaly won't treat it as block text + [self sendStringToVim:@"ggjjvll" withMods:0]; + [self sendKeyToVim:@"v" withMods:NSEventModifierFlagControl]; + [self waitForEventHandlingAndVimProcess]; + [vc replaceSelectedText:@"xyz\n1234\n"]; // ending in newline means it gets interpreted as line-wise + XCTAssertEqualObjects([vc evaluateVimExpression:@"getline(1)"], @"axyz d"); + XCTAssertEqualObjects([vc evaluateVimExpression:@"getline(2)"], @"e1234h"); + XCTAssertEqualObjects([vc evaluateVimExpression:@"getline(3)"], @"xyz"); + XCTAssertEqualObjects([vc evaluateVimExpression:@"getline(4)"], @"1234"); + XCTAssertEqualObjects([vc evaluateVimExpression:@"getline(5)"], @"l"); + + // Make sure registers didn't get stomped (internally the implementation uses register and manually restores it) + regcontents = [[app keyVimController] evaluateVimExpression:@"getreg()"]; + XCTAssertEqualObjects(regcontents, @"abcd\n"); + + [self sendStringToVim:@":set nomodified\n" withMods:0]; + [self waitForEventHandlingAndVimProcess]; +} + @end diff --git a/src/evalfunc.c b/src/evalfunc.c index 60dd4ddbf1..dd1441fde1 100644 --- a/src/evalfunc.c +++ b/src/evalfunc.c @@ -72,10 +72,8 @@ static void f_getenv(typval_T *argvars, typval_T *rettv); static void f_getfontname(typval_T *argvars, typval_T *rettv); static void f_getjumplist(typval_T *argvars, typval_T *rettv); static void f_getpid(typval_T *argvars, typval_T *rettv); -static void f_getpos(typval_T *argvars, typval_T *rettv); static void f_getreg(typval_T *argvars, typval_T *rettv); static void f_getreginfo(typval_T *argvars, typval_T *rettv); -static void f_getregion(typval_T *argvars, typval_T *rettv); static void f_getregionpos(typval_T *argvars, typval_T *rettv); static void f_getregtype(typval_T *argvars, typval_T *rettv); static void f_gettagstack(typval_T *argvars, typval_T *rettv); @@ -5758,7 +5756,7 @@ f_getcursorcharpos(typval_T *argvars, typval_T *rettv) /* * "getpos(string)" function */ - static void + void f_getpos(typval_T *argvars, typval_T *rettv) { if (in_vim9script() && check_for_string_arg(argvars, 0) == FAIL) @@ -5949,7 +5947,7 @@ getregionpos( /* * "getregion()" function */ - static void + void f_getregion(typval_T *argvars, typval_T *rettv) { pos_T p1, p2; diff --git a/src/normal.c b/src/normal.c index 1e02164ce2..fa27efceee 100644 --- a/src/normal.c +++ b/src/normal.c @@ -112,7 +112,6 @@ static void nv_record(cmdarg_T *cap); static void nv_at(cmdarg_T *cap); static void nv_halfpage(cmdarg_T *cap); static void nv_join(cmdarg_T *cap); -static void nv_put(cmdarg_T *cap); static void nv_put_opt(cmdarg_T *cap, int fix_indent); static void nv_open(cmdarg_T *cap); #ifdef FEAT_NETBEANS_INTG @@ -7339,7 +7338,7 @@ nv_join(cmdarg_T *cap) /* * "P", "gP", "p" and "gp" commands. */ - static void + void nv_put(cmdarg_T *cap) { nv_put_opt(cap, FALSE); diff --git a/src/proto/evalfunc.pro b/src/proto/evalfunc.pro index 3f17bfdad9..a581bb8aa1 100644 --- a/src/proto/evalfunc.pro +++ b/src/proto/evalfunc.pro @@ -30,5 +30,7 @@ long do_searchpair(char_u *spat, char_u *mpat, char_u *epat, int dir, typval_T * // MacVim only void f_getcurpos(typval_T *argvars, typval_T *rettv); +void f_getpos(typval_T *argvars, typval_T *rettv); +void f_getregion(typval_T *argvars, typval_T *rettv); /* vim: set ft=c : */ diff --git a/src/proto/normal.pro b/src/proto/normal.pro index 36a26ec480..ea74d6d7d0 100644 --- a/src/proto/normal.pro +++ b/src/proto/normal.pro @@ -33,4 +33,8 @@ void may_start_select(int c); int unadjust_for_sel(void); int unadjust_for_sel_inner(pos_T *pp); void set_cursor_for_append_to_line(void); + +// MacVim only +void nv_put(cmdarg_T *cap); + /* vim: set ft=c : */