Skip to content

Commit 565a8a1

Browse files
committed
Fix Help menu searching Vim doc not working with special chars
Previously Help menu's Vim doc search did not work with special key documentation like `<Down>` because when sending the input to Vim, Vim interpreted `:h <Down>` as physically pressing down arrow instead of typing the literal text in. Fix this by a somewhat manual process of splitting the strings up and using `:execute` and string concatenation. It's not pretty but works. Otherwise we would need to define another IPC call to be able to pass commands to Vim without escaping commands. Also fix up XCText utility so we don't use a predicate to wait for Vim open/close which was previously kind of slow. The new method of just using swizzling is much faster.
1 parent 81d23bc commit 565a8a1

3 files changed

Lines changed: 135 additions & 21 deletions

File tree

src/MacVim/MMAppController.m

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1676,6 +1676,8 @@ - (NSArray *)serverList
16761676
return item;
16771677
}
16781678

1679+
/// Invoked when user typed on the help menu search bar. Will parse doc tags
1680+
/// and search among them for the search string and return the match items.
16791681
- (void)searchForItemsWithSearchString:(NSString *)searchString
16801682
resultLimit:(NSInteger)resultLimit
16811683
matchedItemHandler:(void (^)(NSArray *items))handleMatchedItems
@@ -1748,6 +1750,8 @@ - (void)searchForItemsWithSearchString:(NSString *)searchString
17481750
handleMatchedItems(ret);
17491751
}
17501752

1753+
/// Invoked when user clicked on a Help menu item for a documentation tag
1754+
/// previously returned by searchForItemsWithSearchString.
17511755
- (void)performActionForItem:(id)item
17521756
{
17531757
// When opening a help page, either open a new Vim instance, or reuse the
@@ -1758,8 +1762,27 @@ - (void)performActionForItem:(id)item
17581762
@":help %@", item[1]]];
17591763
return;
17601764
}
1761-
[vimController addVimInput:[NSString stringWithFormat:
1762-
@"<C-\\><C-N>:help %@<CR>", item[1]]];
1765+
1766+
// Vim is already open. We want to send it a message to open help. However,
1767+
// we're using `addVimInput`, which always treats input like "<Up>" as a key
1768+
// while we want to type it literally. The only way to do so is to manually
1769+
// split it up and concatenate the results together and pass it to :execute.
1770+
NSString *helpStr = item[1];
1771+
1772+
NSMutableString *cmd = [NSMutableString stringWithCapacity:40 + helpStr.length];
1773+
[cmd setString:@"<C-\\><C-N>:exe 'help "];
1774+
1775+
NSArray<NSString*> *splitComponents = [helpStr componentsSeparatedByString:@"<"];
1776+
for (NSUInteger i = 0; i < splitComponents.count; i++) {
1777+
if (i != 0) {
1778+
[cmd appendString:@"<'..'"];
1779+
}
1780+
NSString *component = splitComponents[i];
1781+
component = [component stringByReplacingOccurrencesOfString:@"'" withString:@"''"];
1782+
[cmd appendString:component];
1783+
}
1784+
[cmd appendString:@"'<CR>"];
1785+
[vimController addVimInput:cmd];
17631786
}
17641787
// End NSUserInterfaceItemSearching
17651788

src/MacVim/MacVim.xctestplan

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"argument" : "-MMUntitledWindow 0"
1818
}
1919
],
20+
"testExecutionOrdering" : "random",
2021
"testTimeoutsEnabled" : true
2122
},
2223
"testTargets" : [

src/MacVim/MacVimTests/MacVimTests.m

Lines changed: 109 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,44 @@ @interface MacVimTests : XCTestCase
4444

4545
@implementation MacVimTests
4646

47-
/// Wait for a Vim controller to be added/removed. By the time this is fulfilled
48-
/// the Vim window should be ready and visible.
49-
- (void)waitForVimController:(int)delta {
50-
NSArray *vimControllers = [MMAppController.sharedInstance vimControllers];
51-
const int desiredCount = (int)vimControllers.count + delta;
52-
[self waitForExpectations:@[[[XCTNSPredicateExpectation alloc]
53-
initWithPredicate:[NSPredicate predicateWithBlock:^(id vimControllers, NSDictionary<NSString *,id> *bindings) {
54-
return (BOOL)((int)[(NSArray*)vimControllers count] == desiredCount);
55-
}]
56-
object:vimControllers]]
57-
timeout:5];
47+
/// Wait for Vim window to open and is ready to go
48+
- (void)waitForVimOpen {
49+
XCTestExpectation *expectation = [self expectationWithDescription:@"VimOpen"];
50+
51+
SEL sel = @selector(windowControllerWillOpen:);
52+
Method method = class_getInstanceMethod([MMAppController class], sel);
53+
54+
IMP origIMP = method_getImplementation(method);
55+
IMP newIMP = imp_implementationWithBlock(^(id self, MMWindowController *w) {
56+
typedef void (*fn)(id,SEL,MMWindowController*);
57+
((fn)origIMP)(self, sel, w);
58+
[expectation fulfill];
59+
});
60+
61+
method_setImplementation(method, newIMP);
62+
[self waitForExpectations:@[expectation] timeout:10];
63+
method_setImplementation(method, origIMP);
64+
65+
[self waitForEventHandlingAndVimProcess];
66+
}
67+
68+
/// Wait for a Vim window to be closed
69+
- (void)waitForVimClose {
70+
XCTestExpectation *expectation = [self expectationWithDescription:@"VimClose"];
71+
72+
SEL sel = @selector(removeVimController:);
73+
Method method = class_getInstanceMethod([MMAppController class], sel);
74+
75+
IMP origIMP = method_getImplementation(method);
76+
IMP newIMP = imp_implementationWithBlock(^(id self, id controller) {
77+
typedef void (*fn)(id,SEL,id);
78+
((fn)origIMP)(self, sel, controller);
79+
[expectation fulfill];
80+
});
81+
82+
method_setImplementation(method, newIMP);
83+
[self waitForExpectations:@[expectation] timeout:10];
84+
method_setImplementation(method, origIMP);
5885
}
5986

6087
/// Wait for event handling to be finished at the main loop.
@@ -232,7 +259,7 @@ - (void)testVimTutor {
232259
// Adding a new window is necessary for the vimtutor menu to show up as it's
233260
// not part of the global menu
234261
[app openNewWindow:NewWindowClean activate:YES];
235-
[self waitForVimController:1];
262+
[self waitForVimOpen];
236263

237264
// Find the vimtutor menu and run it.
238265
NSMenu *mainMenu = [NSApp mainMenu];
@@ -248,16 +275,79 @@ - (void)testVimTutor {
248275
[[[app keyVimController] windowController] vimMenuItemAction:vimTutorMenu];
249276

250277
// Make sure the menu item actually opened a new window and point to a tutor buffer
251-
[self waitForVimController:1];
278+
// Note that `vimtutor` opens Vim twice. Once to copy the file. Another time to
279+
// actually open the copied file.
280+
[self waitForVimOpen];
281+
[self waitForVimOpen];
252282

253283
NSString *bufname = [[app keyVimController] evaluateVimExpression:@"bufname()"];
254284
XCTAssertTrue([bufname containsString:@"tutor"]);
255285

256286
// Clean up
257287
[[app keyVimController] sendMessage:VimShouldCloseMsgID data:nil];
258-
[self waitForVimController:-1];
288+
[self waitForVimClose];
259289
[[app keyVimController] sendMessage:VimShouldCloseMsgID data:nil];
260-
[self waitForVimController:-1];
290+
[self waitForVimClose];
291+
292+
XCTAssertEqual(0, [app vimControllers].count);
293+
}
294+
295+
/// Test that opening Vim documentation from Help menu works as expected even
296+
/// with odd characters.
297+
- (void)testHelpMenuDocumentationTag {
298+
MMAppController *app = MMAppController.sharedInstance;
299+
XCTAssertEqual(0, app.vimControllers.count);
300+
301+
[NSApp activateIgnoringOtherApps:YES];
302+
303+
// Test help menu when no window is shown
304+
[app performActionForItem:@[@"", @"m'"]];
305+
[self waitForVimOpen];
306+
MMVimController *vim = [app keyVimController];
307+
308+
XCTAssertEqualObjects(@"help", [vim evaluateVimExpression:@"&buftype"]);
309+
NSString *curLine = [vim evaluateVimExpression:@"getline('.')"];
310+
XCTAssertTrue([curLine containsString:@"*m'*"]);
311+
[vim sendMessage:VimShouldCloseMsgID data:nil];
312+
vim = nil;
313+
[self waitForVimClose];
314+
315+
// Test help menu when there's already a Vim window
316+
[app openNewWindow:NewWindowClean activate:YES];
317+
[self waitForVimOpen];
318+
vim = [app keyVimController];
319+
320+
#define ASSERT_HELP_PATTERN(pattern) \
321+
do { \
322+
[app performActionForItem:@[@"foobar.txt", @pattern]]; \
323+
[self waitForVimProcess]; \
324+
XCTAssertEqualObjects(@"help", [vim evaluateVimExpression:@"&buftype"]); \
325+
curLine = [vim evaluateVimExpression:@"getline('.')"]; \
326+
XCTAssertTrue([curLine containsString:@("*" pattern "*")]); \
327+
} while(0)
328+
329+
ASSERT_HELP_PATTERN("macvim-touchbar");
330+
ASSERT_HELP_PATTERN("++enc");
331+
ASSERT_HELP_PATTERN("v_CTRL-\\_CTRL-G");
332+
ASSERT_HELP_PATTERN("/\\%<v");
333+
334+
// '<' characters need to be concatenated to not be interpreted as keys
335+
ASSERT_HELP_PATTERN("c_<Down>");
336+
ASSERT_HELP_PATTERN("c_<C-R>_<C-W>");
337+
338+
// single-quote characters should be escaped properly when passed to help
339+
ASSERT_HELP_PATTERN("'display'");
340+
ASSERT_HELP_PATTERN("m'");
341+
342+
// Test both single-quote and '<'
343+
ASSERT_HELP_PATTERN("/\\%<'m");
344+
ASSERT_HELP_PATTERN("'<");
345+
346+
#undef ASSERT_HELP_PATTERN
347+
348+
// Clean up
349+
[vim sendMessage:VimShouldCloseMsgID data:nil];
350+
[self waitForVimClose];
261351
}
262352

263353
/// Test that cmdline row calculation (used by MMCmdLineAlignBottom) is correct.
@@ -268,19 +358,19 @@ - (void) testCmdlineRowCalculation {
268358
MMAppController *app = MMAppController.sharedInstance;
269359

270360
[app openNewWindow:NewWindowClean activate:YES];
271-
[self waitForVimController:1];
361+
[self waitForVimOpen];
272362

273363
MMTextView *textView = [[[[app keyVimController] windowController] vimView] textView];
274364
const int numLines = [textView maxRows];
275365
const int numCols = [textView maxColumns];
276366

277367
// Define convenience macro (don't use functions to preserve line numbers in callstack)
278368
#define ASSERT_NUM_CMDLINES(expected) \
279-
{ \
369+
do { \
280370
const int cmdlineRow = [[[app keyVimController] objectForVimStateKey:@"cmdline_row"] intValue]; \
281371
const int numBottomLines = numLines - cmdlineRow; \
282372
XCTAssertEqual(expected, numBottomLines); \
283-
}
373+
} while(0)
284374

285375
// Default value
286376
[self waitForEventHandlingAndVimProcess];
@@ -328,7 +418,7 @@ - (void) testCmdlineRowCalculation {
328418

329419
// Clean up
330420
[[app keyVimController] sendMessage:VimShouldCloseMsgID data:nil];
331-
[self waitForVimController:-1];
421+
[self waitForVimClose];
332422
}
333423

334424
@end

0 commit comments

Comments
 (0)