Skip to content

Fix macOS services no longer able to insert texts in non-Visual modes #1571

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 91 additions & 13 deletions src/MacVim/MMBackend.m
Original file line number Diff line number Diff line change
Expand Up @@ -1443,10 +1443,11 @@ - (NSString *)selectedText
return nil;
}

/// Replace the selected text in visual mode with the new suppiled one.
- (oneway void)replaceSelectedText:(in bycopy NSString *)text
/// Insert or replace text with the supplied text. Works in Normal / Visual /
/// Insert / Cmdline modes.
- (oneway void)insertOrReplaceSelectedText:(in bycopy NSString *)text
{
if (VIsual_active && (State & MODE_NORMAL)) {
if (State & MODE_NORMAL || State & MODE_INSERT) {
// 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
Expand Down Expand Up @@ -1474,17 +1475,27 @@ - (oneway void)replaceSelectedText:(in bycopy NSString *)text
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;
if (State & MODE_NORMAL || State & MODE_INSERT) {
oparg_T oap;
CLEAR_FIELD(oap);
oap.regname = '0';

cmdarg_T cap;
CLEAR_FIELD(cap);
cap.oap = &oap;
if (State & MODE_NORMAL) {
// Do 'P' or 'v_P' depending if we are in visual mode. They both do
// the correct behaviors, so no need to check for VIsual_active.
cap.cmdchar = 'P';
} else {
// Need 'gP' to leave the cursor at the right location.
cap.cmdchar = 'g';
cap.nchar = 'P';
}
cap.count1 = 1;

nv_put(&cap);
nv_put(&cap);
}

// Clean up the temporary register, and restore the old state.
yankreg_T *old_y_current = get_y_current();
Expand All @@ -1496,6 +1507,73 @@ - (oneway void)replaceSelectedText:(in bycopy NSString *)text
// 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];
} else if (State & MODE_CMDLINE) {
// This is basically doing the following:
// - let cmdline_str = getcmdline()
// - let cmdline_pos = getcmdpos() - 1
// - setcmdline(cmdline_str[0:cmdline_pos] .. text .. cmdline_str[cmdline_pos:], cmdline_pos + len(text) + 1)

typval_T cmdline_str;
f_getcmdline(NULL, &cmdline_str);

typval_T cmdline_pos;
f_getcmdpos(NULL, &cmdline_pos);

char_u *vimtext = [text vimStringSave];
for (char_u *c = vimtext; *c != NUL; c++) {
// Perform NL conversion due to Vim's internal usage
if (*c == '\n')
*c = '\r';
}

size_t new_size = STRLEN(vimtext);
varnumber_T pos = new_size + 1;

if (cmdline_str.vval.v_string != NULL && cmdline_pos.vval.v_number != 0) {
// Combine original string with new one
char_u *orig_str = cmdline_str.vval.v_string;
size_t pos_index = cmdline_pos.vval.v_number - 1;

size_t orig_size = STRLEN(cmdline_str.vval.v_string);

if (pos_index > orig_size)
pos_index = orig_size; // shouldn't really happen

char_u *newtext = alloc(orig_size + new_size + 1);
if (pos_index > 0)
memcpy(newtext, orig_str, pos_index);
memcpy(newtext + pos_index, vimtext, new_size);
if (pos_index < orig_size)
memcpy(newtext + pos_index + new_size, orig_str + pos_index, orig_size - pos_index);
newtext[orig_size + new_size] = '\0';

vim_free(vimtext);
vimtext = newtext;

pos += pos_index;
}

{
typval_T arg_cmdline_str_new;
init_tv(&arg_cmdline_str_new);
arg_cmdline_str_new.v_type = VAR_STRING;
arg_cmdline_str_new.vval.v_string = vimtext;

typval_T arg_cmdline_pos;
init_tv(&arg_cmdline_pos);
arg_cmdline_pos.v_type = VAR_NUMBER;
arg_cmdline_pos.vval.v_number = pos;

typval_T args[2] = { arg_cmdline_str_new, arg_cmdline_pos };

typval_T ret;
f_setcmdline(args, &ret);

vim_free(vimtext);
}

if (cmdline_str.vval.v_string != NULL)
vim_free(cmdline_str.vval.v_string);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/MacVim/MMTextViewHelper.m
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ - (void)insertText:(id)string replacementRange:(NSRange)replacementRange
// Only known way of this being called is Apple Intelligence Writing
// Tools.
MMVimController *vc = [self vimController];
[vc replaceSelectedText:string];
[vc insertOrReplaceSelectedText:string];
return;
}

Expand Down
2 changes: 1 addition & 1 deletion src/MacVim/MMVimController.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
errorString:(NSString **)errstr;
- (BOOL)hasSelectedText;
- (NSString *)selectedText;
- (void)replaceSelectedText:(NSString *)text;
- (void)insertOrReplaceSelectedText:(NSString *)text;
- (void)processInputQueue:(NSArray *)queue;
#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_12_2
- (NSTouchBar *)makeTouchBar;
Expand Down
6 changes: 3 additions & 3 deletions src/MacVim/MMVimController.m
Original file line number Diff line number Diff line change
Expand Up @@ -563,14 +563,14 @@ - (NSString *)selectedText
return selectedText;
}

- (void)replaceSelectedText:(NSString *)text
- (void)insertOrReplaceSelectedText:(NSString *)text
{
if (backendProxy) {
@try {
[backendProxy replaceSelectedText:text];
[backendProxy insertOrReplaceSelectedText:text];
}
@catch (NSException *ex) {
ASLogDebug(@"replaceSelectedText: failed: pid=%d reason=%@",
ASLogDebug(@"insertOrReplaceSelectedText: failed: pid=%d reason=%@",
pid, ex);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/MacVim/MMWindowController.m
Original file line number Diff line number Diff line change
Expand Up @@ -1679,7 +1679,7 @@ - (BOOL)readSelectionFromPasteboard:(NSPasteboard *)pboard
NSArray *types = [pboard types];
if ([types containsObject:NSPasteboardTypeString]) {
NSString *input = [pboard stringForType:NSPasteboardTypeString];
[vimController replaceSelectedText:input];
[vimController insertOrReplaceSelectedText:input];
return YES;
}

Expand Down
2 changes: 1 addition & 1 deletion src/MacVim/MacVim.h
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ typedef NSString* NSAttributedStringKey;
errorString:(out bycopy NSString **)errstr;
- (BOOL)hasSelectedText;
- (NSString *)selectedText;
- (oneway void)replaceSelectedText:(in bycopy NSString *)text;
- (oneway void)insertOrReplaceSelectedText:(in bycopy NSString *)text;
- (BOOL)mouseScreenposIsSelection:(int)row column:(int)column selRow:(byref int *)startRow selCol:(byref int *)startCol;
- (oneway void)acknowledgeConnection;
@end
Expand Down
73 changes: 63 additions & 10 deletions src/MacVim/MacVimTests/MacVimTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -1470,6 +1470,10 @@ - (void)testIPCSelectedText {
NSString *regcontents = [vc evaluateVimExpression:@"getreg()"];
XCTAssertEqualObjects(regcontents, @"abcd\n");

// Visual mode

NSString *changedtick1 = [vc evaluateVimExpression:@"b:changedtick"];

// Get selected texts in visual mode
XCTAssertFalse([vc hasSelectedText]);
XCTAssertNil([vc selectedText]);
Expand All @@ -1491,30 +1495,79 @@ - (void)testIPCSelectedText {
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"];
[vc insertOrReplaceSelectedText:@"xyz\n1234"];
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
[vc insertOrReplaceSelectedText:@"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");

// Normal mode

// When nothing is selected this will simply insert the text and not replace anything
[self sendStringToVim:@"ggll" withMods:0];
[self waitForEventHandlingAndVimProcess];
[vc insertOrReplaceSelectedText:@"_normtext_"];
XCTAssertEqualObjects([vc evaluateVimExpression:@"getline(1)"], @"ax_normtext_yz d");

// Insert mode

[self sendStringToVim:@"ggjja" withMods:0];
[self waitForEventHandlingAndVimProcess];
// Should insert the text at the cursor
[vc insertOrReplaceSelectedText:@"_inserttext_"];
XCTAssertEqualObjects([vc evaluateVimExpression:@"getline(3)"], @"x_inserttext_yz");
// Should leave the cursor past the inserted text
[self sendStringToVim:@"additional_text" withMods:0];
[self waitForEventHandlingAndVimProcess];
XCTAssertEqualObjects([vc evaluateVimExpression:@"getline(3)"], @"x_inserttext_additional_textyz");
[self sendKeyToVim:@"[" withMods:NSEventModifierFlagControl]; // escape insert mode
[self waitForEventHandlingAndVimProcess];

// Cmdline mode

NSString *changedtick2 = [vc evaluateVimExpression:@"b:changedtick"];
XCTAssertNotEqualObjects(changedtick1, changedtick2);

[self sendStringToVim:@":cnoremap z <Left>\n" withMods:0];
[self sendStringToVim:@":123" withMods:0];
[self waitForEventHandlingAndVimProcess];
[vc insertOrReplaceSelectedText:@"a\nb\n"];
XCTAssertEqualObjects([vc evaluateVimExpression:@"getcmdline()"], @"123a\rb\r"); // Vim does internal \n to \r conversion
XCTAssertEqualObjects([vc evaluateVimExpression:@"getcmdpos()"], @"8");
[self sendKeyToVim:@"[" withMods:NSEventModifierFlagControl]; // escape cmdline
[self waitForEventHandlingAndVimProcess];

[self sendStringToVim:@":123zzz" withMods:0];
[self waitForEventHandlingAndVimProcess];
[vc insertOrReplaceSelectedText:@"foobar"];
XCTAssertEqualObjects([vc evaluateVimExpression:@"getcmdline()"], @"foobar123");
XCTAssertEqualObjects([vc evaluateVimExpression:@"getcmdpos()"], @"7");
[self sendKeyToVim:@"[" withMods:NSEventModifierFlagControl]; // escape cmdline

[self waitForEventHandlingAndVimProcess];
[self sendStringToVim:@":123z" withMods:0];
[self waitForEventHandlingAndVimProcess];
[vc insertOrReplaceSelectedText:@"foobar"];
XCTAssertEqualObjects([vc evaluateVimExpression:@"getcmdline()"], @"12foobar3");
XCTAssertEqualObjects([vc evaluateVimExpression:@"getcmdpos()"], @"9");
[self sendKeyToVim:@"[" withMods:NSEventModifierFlagControl]; // escape cmdline
[self waitForEventHandlingAndVimProcess];

// Make sure that the actual buffer wasn't changed at all during these insertions as they all
// went to the cmdline.
NSString *changedtick3 = [vc evaluateVimExpression:@"b:changedtick"];
XCTAssertEqualObjects(changedtick2, changedtick3);

// 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");
Expand Down
Loading