Skip to content

Commit 5e5334a

Browse files
committed
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
1 parent 252a133 commit 5e5334a

15 files changed

+295
-185
lines changed

src/MacVim/MMBackend.m

Lines changed: 121 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1380,158 +1380,140 @@ - (NSString *)evaluateExpression:(in bycopy NSString *)expr
13801380
return eval;
13811381
}
13821382

1383-
/// Extracts the text currently selected in visual mode, and returns it.
1384-
///
1385-
/// @return the string representing the selected text, or NULL if failure.
1386-
static char_u *extractSelectedText(void)
1387-
{
1388-
// Note: Most of the functionality in Vim that allows for extracting useful
1389-
// text from a selection are in the register & clipboard utility functions.
1390-
// Unfortunately, most of those functions would actually send the text to
1391-
// the system clipboard, which we don't want (since we just want to extract
1392-
// the text instead of polluting the system clipboard). We don't want to
1393-
// refactor upstream Vim code too much to avoid merge pains later, so we
1394-
// duplicate a fair bit of the code from the functions below.
1395-
1396-
if (!(VIsual_active && (State & MODE_NORMAL))) {
1397-
// This only works when we are in visual mode and have stuff to select.
1398-
return NULL;
1399-
}
1400-
1401-
// Step 1: Find a register to yank the selection to. If we don't do this we
1402-
// have to duplicate a lot of code in op_yank(). We save it off to a backup
1403-
// first so we can restore it later to avoid polluting the registers.
1404-
1405-
// Just use the yank / '0' register as it makes sense, but it doesn't
1406-
// really matter.
1407-
yankreg_T *target_reg = get_y_register(0);
1408-
1409-
// Move the contents to the backup without doing memory allocs.
1410-
yankreg_T backup_reg = *target_reg;
1411-
target_reg->y_array = NULL;
1412-
target_reg->y_size = 0;
1413-
1414-
// Step 2: Preserve the local states, and then invoke yank.
1415-
// Note: These were copied from clip_get_selection() in clipboard.c
1416-
yankreg_T *old_y_previous, *old_y_current;
1417-
pos_T old_cursor;
1418-
pos_T old_visual;
1419-
int old_visual_mode;
1420-
colnr_T old_curswant;
1421-
int old_set_curswant;
1422-
pos_T old_op_start, old_op_end;
1423-
oparg_T oa;
1424-
cmdarg_T ca;
1425-
1426-
// Avoid triggering autocmds such as TextYankPost.
1427-
block_autocmds();
1428-
1429-
// Yank the selected text into the target register.
1430-
old_y_previous = get_y_previous();
1431-
old_y_current = get_y_current();
1432-
old_cursor = curwin->w_cursor;
1433-
old_curswant = curwin->w_curswant;
1434-
old_set_curswant = curwin->w_set_curswant;
1435-
old_op_start = curbuf->b_op_start;
1436-
old_op_end = curbuf->b_op_end;
1437-
old_visual = VIsual;
1438-
old_visual_mode = VIsual_mode;
1439-
clear_oparg(&oa);
1440-
oa.regname = '0'; // Use the '0' (yank) register. We will restore it later to avoid pollution.
1441-
oa.op_type = OP_YANK;
1442-
CLEAR_FIELD(ca);
1443-
ca.oap = &oa;
1444-
ca.cmdchar = 'y';
1445-
ca.count1 = 1;
1446-
ca.retval = CA_NO_ADJ_OP_END;
1447-
do_pending_operator(&ca, 0, TRUE);
1448-
1449-
// Step 3: Extract the text from the yank ('0') register.
1450-
char_u *str = get_reg_contents(0, 0);
1451-
1452-
// Step 4: Clean up the yank register, and restore it back.
1453-
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.
1454-
free_yank_all();
1455-
*target_reg = backup_reg;
1456-
1457-
// Step 5: Restore all the loose states that were modified during yank.
1458-
// Note: These were copied from clip_get_selection() in clipboard.c
1459-
set_y_previous(old_y_previous);
1460-
set_y_current(old_y_current);
1461-
curwin->w_cursor = old_cursor;
1462-
changed_cline_bef_curs(); // need to update w_virtcol et al
1463-
curwin->w_curswant = old_curswant;
1464-
curwin->w_set_curswant = old_set_curswant;
1465-
curbuf->b_op_start = old_op_start;
1466-
curbuf->b_op_end = old_op_end;
1467-
VIsual = old_visual;
1468-
VIsual_mode = old_visual_mode;
1469-
1470-
unblock_autocmds();
1471-
1472-
return str;
1473-
}
1474-
1475-
/// Extract the currently selected text (in visual mode) and send that to the
1476-
/// provided pasteboard.
1477-
- (BOOL)selectedTextToPasteboard:(byref NSPasteboard *)pboard
1383+
- (BOOL)hasSelectedText
14781384
{
1479-
if (VIsual_active && (State & MODE_NORMAL)) {
1480-
// If there is no pasteboard, just return YES to indicate that there is
1481-
// text to copy.
1482-
if (!pboard)
1483-
return YES;
1385+
return (VIsual_active && (State & MODE_NORMAL));
1386+
}
14841387

1485-
char_u *str = extractSelectedText();
1486-
if (!str)
1487-
return NO;
1388+
/// Returns the currently selected text.
1389+
- (NSString *)selectedText
1390+
{
1391+
if (VIsual_active && (State & MODE_NORMAL)) {
1392+
// This is basically doing the following:
1393+
// - join(getregion(getpos("."), getpos("v"), { type: visualmode() }),"\n")
1394+
// - Add extra "\n" if we have a linewise selection
14881395

1489-
if (output_conv.vc_type != CONV_NONE) {
1490-
char_u *conv_str = string_convert(&output_conv, str, NULL);
1491-
if (conv_str) {
1492-
vim_free(str);
1493-
str = conv_str;
1396+
// Call getpos()
1397+
typval_T pos1, pos2;
1398+
{
1399+
typval_T arg_posmark;
1400+
init_tv(&arg_posmark);
1401+
arg_posmark.v_type = VAR_STRING;
1402+
1403+
arg_posmark.vval.v_string = (char_u*)".";
1404+
typval_T args1[1] = { arg_posmark };
1405+
f_getpos(args1, &pos1);
1406+
if (pos1.v_type != VAR_LIST)
1407+
return nil;
1408+
1409+
arg_posmark.vval.v_string = (char_u*)"v";
1410+
typval_T args2[1] = { arg_posmark };
1411+
f_getpos(args2, &pos2);
1412+
if (pos2.v_type != VAR_LIST) {
1413+
list_unref(pos1.vval.v_list);
1414+
return nil;
14941415
}
14951416
}
14961417

1497-
NSString *string = [[NSString alloc] initWithUTF8String:(char*)str];
1498-
1499-
NSArray *types = [NSArray arrayWithObject:NSPasteboardTypeString];
1500-
[pboard declareTypes:types owner:nil];
1501-
BOOL ok = [pboard setString:string forType:NSPasteboardTypeString];
1502-
1503-
[string release];
1504-
vim_free(str);
1418+
// Call getregion()
1419+
typval_T arg_opts;
1420+
init_tv(&arg_opts);
1421+
arg_opts.v_type = VAR_DICT;
1422+
arg_opts.vval.v_dict = dict_alloc();
1423+
arg_opts.vval.v_dict->dv_refcount += 1;
1424+
1425+
char_u visualmode[2] = { VIsual_mode, '\0' };
1426+
dict_add_string(arg_opts.vval.v_dict, "type", visualmode);
1427+
1428+
typval_T args[3] = { pos1, pos2, arg_opts };
1429+
typval_T regionLines;
1430+
f_getregion(args, &regionLines);
1431+
1432+
// Join the results
1433+
NSMutableArray *returnLines = [NSMutableArray array];
1434+
if (regionLines.v_type == VAR_LIST) {
1435+
list_T *lines = regionLines.vval.v_list;
1436+
for (listitem_T *item = lines->lv_first; item != NULL; item = item->li_next) {
1437+
if (item->li_tv.v_type == VAR_STRING) {
1438+
char_u *str = item->li_tv.vval.v_string;
1439+
if (output_conv.vc_type != CONV_NONE) {
1440+
char_u *conv_str = string_convert(&output_conv, str, NULL);
1441+
if (conv_str) {
1442+
[returnLines addObject:[NSString stringWithUTF8String:(char*)conv_str]];
1443+
vim_free(conv_str);
1444+
}
1445+
} else {
1446+
[returnLines addObject:[NSString stringWithUTF8String:(char*)str]];
1447+
}
1448+
}
1449+
}
1450+
list_unref(lines);
1451+
}
1452+
dict_unref(arg_opts.vval.v_dict);
1453+
list_unref(pos1.vval.v_list);
1454+
list_unref(pos2.vval.v_list);
15051455

1506-
return ok;
1456+
if (VIsual_mode == 'V')
1457+
[returnLines addObject:@""]; // need trailing endline for linewise
1458+
return [returnLines componentsJoinedByString:@"\n"];
15071459
}
1508-
1509-
return NO;
1460+
return nil;
15101461
}
15111462

1512-
/// Returns the currently selected text. We should consolidate this with
1513-
/// selectedTextToPasteboard: above when we have time. (That function has a
1514-
/// fast path just to query whether selected text exists)
1515-
- (NSString *)selectedText
1463+
/// Replace the selected text in visual mode with the new suppiled one.
1464+
- (oneway void)replaceSelectedText:(in bycopy NSString *)text
15161465
{
15171466
if (VIsual_active && (State & MODE_NORMAL)) {
1518-
char_u *str = extractSelectedText();
1519-
if (!str)
1520-
return nil;
1521-
1522-
if (output_conv.vc_type != CONV_NONE) {
1523-
char_u *conv_str = string_convert(&output_conv, str, NULL);
1524-
if (conv_str) {
1525-
vim_free(str);
1526-
str = conv_str;
1527-
}
1528-
}
1529-
1530-
NSString *string = [[NSString alloc] initWithUTF8String:(char*)str];
1531-
vim_free(str);
1532-
return [string autorelease];
1467+
// The only real way Vim has in doing this consistently is to use the
1468+
// register put functionality as there is no generic API for this.
1469+
// We find an arbitrary register ('0'), back it up, replace it with our
1470+
// own content, paste it in, then restore the register to old value.
1471+
yankreg_T *target_reg = get_y_register(0);
1472+
yankreg_T backup_reg = *target_reg;
1473+
target_reg->y_array = NULL;
1474+
target_reg->y_size = 0;
1475+
1476+
// If selection is blockwise, we try to match it. Only do it if input
1477+
// and selected text have same number of lines, as otherwise it could
1478+
// be awkward.
1479+
int yank_type = MAUTO;
1480+
char_u *vimtext = [text vimStringSave];
1481+
if (VIsual_mode == Ctrl_V) {
1482+
long text_lines = string_count(vimtext, (char_u*)"\n", FALSE) + 1;
1483+
1484+
linenr_T v1 = VIsual.lnum;
1485+
linenr_T v2 = curwin->w_cursor.lnum;
1486+
long num_lines = v1 > v2 ? v1 - v2 + 1 : v2 - v1 + 1;
1487+
1488+
if (text_lines == num_lines)
1489+
yank_type = MBLOCK;
1490+
}
1491+
write_reg_contents_ex('0', vimtext, -1, FALSE, yank_type, -1);
1492+
vim_free(vimtext);
1493+
1494+
oparg_T oap;
1495+
CLEAR_FIELD(oap);
1496+
oap.regname = '0';
1497+
1498+
cmdarg_T cap;
1499+
CLEAR_FIELD(cap);
1500+
cap.oap = &oap;
1501+
cap.cmdchar = 'P';
1502+
cap.count1 = 1;
1503+
1504+
nv_put(&cap);
1505+
1506+
// Clean up the temporary register, and restore the old state.
1507+
yankreg_T *old_y_current = get_y_current();
1508+
set_y_current(target_reg);
1509+
free_yank_all();
1510+
set_y_current(old_y_current);
1511+
*target_reg = backup_reg;
1512+
1513+
// nv_put does not trigger a redraw command as it's done on a higher
1514+
// level, so just do a manual one here to make sure it's done.
1515+
[self redrawScreen];
15331516
}
1534-
return nil;
15351517
}
15361518

15371519
/// Returns whether the provided mouse screen position is on a visually

src/MacVim/MMCoreTextView.m

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -648,8 +648,7 @@ - (void)keyDown:(NSEvent *)event
648648

649649
- (void)insertText:(id)string replacementRange:(NSRange)replacementRange
650650
{
651-
// We are not currently replacementRange right now.
652-
[helper insertText:string];
651+
[helper insertText:string replacementRange:replacementRange];
653652
}
654653

655654
- (void)doCommandBySelector:(SEL)selector
@@ -1992,7 +1991,7 @@ - (void)quickLookWithEvent:(NSEvent *)event
19921991
// top of said selection and if so, show definition of that instead.
19931992
MMVimController *vc = [self vimController];
19941993
id<MMBackendProtocol> backendProxy = [vc backendProxy];
1995-
if ([backendProxy selectedTextToPasteboard:nil]) {
1994+
if ([backendProxy hasSelectedText]) {
19961995
int selRow = 0, selCol = 0;
19971996
const BOOL isMouseInSelection = [backendProxy mouseScreenposIsSelection:row column:col selRow:&selRow selCol:&selCol];
19981997

src/MacVim/MMTextView.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,7 @@ - (void)keyDown:(NSEvent *)event
724724

725725
- (void)insertText:(id)string
726726
{
727-
[helper insertText:string];
727+
[helper insertText:string replacementRange:NSMakeRange(0, 0)];
728728
}
729729

730730
- (void)doCommandBySelector:(SEL)selector

src/MacVim/MMTextViewHelper.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
- (NSColor *)insertionPointColor;
6666

6767
- (void)keyDown:(NSEvent *)event;
68-
- (void)insertText:(id)string;
68+
- (void)insertText:(id)string replacementRange:(NSRange)replacementRange;
6969
- (void)doCommandBySelector:(SEL)selector;
7070
- (void)scrollWheel:(NSEvent *)event;
7171
- (void)mouseDown:(NSEvent *)event;

src/MacVim/MMTextViewHelper.m

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ - (void)keyDown:(NSEvent *)event
221221
currentEvent = nil;
222222
}
223223

224-
- (void)insertText:(id)string
224+
- (void)insertText:(id)string replacementRange:(NSRange)replacementRange
225225
{
226226
if ([self hasMarkedText]) {
227227
[self sendMarkedText:nil position:0];
@@ -241,6 +241,20 @@ - (void)insertText:(id)string
241241
if ([string isKindOfClass:[NSAttributedString class]])
242242
string = [string string];
243243

244+
if (replacementRange.length > 0)
245+
{
246+
// Replacement range is a concept we don't really have a way to fulfill
247+
// as we don't have proper access to the underlying text storage. This
248+
// should usually be triggered when we have selected text though, and
249+
// so we simply ask Vim to replace the current selection with the new
250+
// text, and it should hopefully work.
251+
// Only known way of this being called is Apple Intelligence Writing
252+
// Tools.
253+
MMVimController *vc = [self vimController];
254+
[vc replaceSelectedText:string];
255+
return;
256+
}
257+
244258
//int len = [string length];
245259
//ASLogDebug(@"len=%d char[0]=%#x char[1]=%#x string='%@'", [string length],
246260
// [string characterAtIndex:0],

src/MacVim/MMVimController.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@
9292
- (NSString *)evaluateVimExpression:(NSString *)expr;
9393
- (id)evaluateVimExpressionCocoa:(NSString *)expr
9494
errorString:(NSString **)errstr;
95+
- (BOOL)hasSelectedText;
96+
- (NSString *)selectedText;
97+
- (void)replaceSelectedText:(NSString *)text;
9598
- (void)processInputQueue:(NSArray *)queue;
9699
#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_12_2
97100
- (NSTouchBar *)makeTouchBar;

0 commit comments

Comments
 (0)