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

Interactions with OS services that insert texts were improved in #1552
to make it work in a more integrated fashion instead of a hacky
injection of 's' followed by the text. However, it only accounted for
services that replaces selected texts in visual mode. In other modes,
MacVim would simply ignore the service. This was a regression as
previously it would work everywhere (albeit often times in a
non-intuitive manner since if used in insert mode the user would see an
's' in the beginning). This also affects Shortcuts that a user may have
made that could be invoked from the Services menu or bound to a hotkey.

Fix this properly by allowing this to be used in Normal / Insert /
Cmdline modes in addition to Visual mode.

Even with this fix, there is still a slight difference between the
new behavior and the old hacky solution - the old method would leave the
user in Insert mode, whereas the new method would stay in Normal mode. I
think this is an improvement so no need to fix.

Fix #1569
This commit is contained in:
Yee Cheng Chin
2025-03-27 23:59:39 -07:00
parent b2c10a6c0d
commit 7cb3ab5058
7 changed files with 160 additions and 29 deletions
+90 -12
View File
@@ -1443,10 +1443,11 @@ static struct specialkey
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
@@ -1474,17 +1475,27 @@ static struct specialkey
write_reg_contents_ex('0', vimtext, -1, FALSE, yank_type, -1);
vim_free(vimtext);
oparg_T oap;
CLEAR_FIELD(oap);
oap.regname = '0';
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;
cap.cmdchar = 'P';
cap.count1 = 1;
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();
@@ -1496,6 +1507,73 @@ static struct specialkey
// 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);
}
}
+1 -1
View File
@@ -251,7 +251,7 @@ KeyboardInputSourcesEqual(TISInputSourceRef a, TISInputSourceRef b)
// Only known way of this being called is Apple Intelligence Writing
// Tools.
MMVimController *vc = [self vimController];
[vc replaceSelectedText:string];
[vc insertOrReplaceSelectedText:string];
return;
}
+1 -1
View File
@@ -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;
+3 -3
View File
@@ -563,14 +563,14 @@ static BOOL isUnsafeMessage(int msgid);
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);
}
}
+1 -1
View File
@@ -1679,7 +1679,7 @@
NSArray *types = [pboard types];
if ([types containsObject:NSPasteboardTypeString]) {
NSString *input = [pboard stringForType:NSPasteboardTypeString];
[vimController replaceSelectedText:input];
[vimController insertOrReplaceSelectedText:input];
return YES;
}
+1 -1
View File
@@ -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
+63 -10
View File
@@ -1470,6 +1470,10 @@ do { \
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]);
@@ -1491,30 +1495,79 @@ do { \
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");