Skip to content

Commit 31efe8c

Browse files
authored
Merge pull request #1313 from ychin/lookup-selected-text-data-detector
Support looking up selected texts, and also add data detector for URLs etc
2 parents 6500a0c + d8d7df8 commit 31efe8c

File tree

6 files changed

+266
-3
lines changed

6 files changed

+266
-3
lines changed

src/MacVim/MMBackend.m

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,6 +1486,110 @@ - (BOOL)selectedTextToPasteboard:(byref NSPasteboard *)pboard
14861486
return NO;
14871487
}
14881488

1489+
/// Returns the currently selected text. We should consolidate this with
1490+
/// selectedTextToPasteboard: above when we have time. (That function has a
1491+
/// fast path just to query whether selected text exists)
1492+
- (NSString *)selectedText
1493+
{
1494+
if (VIsual_active && (State & MODE_NORMAL)) {
1495+
char_u *str = extractSelectedText();
1496+
if (!str)
1497+
return nil;
1498+
1499+
if (output_conv.vc_type != CONV_NONE) {
1500+
char_u *conv_str = string_convert(&output_conv, str, NULL);
1501+
if (conv_str) {
1502+
vim_free(str);
1503+
str = conv_str;
1504+
}
1505+
}
1506+
1507+
NSString *string = [[NSString alloc] initWithUTF8String:(char*)str];
1508+
vim_free(str);
1509+
return [string autorelease];
1510+
}
1511+
return nil;
1512+
}
1513+
1514+
/// Returns whether the provided mouse screen position is on a visually
1515+
/// selected range of text.
1516+
///
1517+
/// If yes, also return the starting row/col of the selection.
1518+
- (BOOL)mouseScreenposIsSelection:(int)row column:(int)column selRow:(byref int *)startRow selCol:(byref int *)startCol
1519+
{
1520+
// The code here is adopted from mouse.c's handling of popup_setpos.
1521+
// Unfortunately this logic is a little tricky to do in pure Vim script
1522+
// because there isn't a function to allow you to query screen pos to
1523+
// window pos. Even getmousepos() doesn't work the way you expect it to if
1524+
// you click on the placeholder rows after the last line (they all return
1525+
// the same 'column').
1526+
if (!VIsual_active)
1527+
return NO;
1528+
1529+
// We set mouse_row / mouse_col without caching/restoring, because it
1530+
// hoenstly makes sense to update them. If in the future we want a version
1531+
// that isn't mouse-related, then we may want to resotre them at the end of
1532+
// the function.
1533+
mouse_row = row;
1534+
mouse_col = column;
1535+
1536+
pos_T m_pos;
1537+
1538+
if (mouse_row < curwin->w_winrow
1539+
|| mouse_row > (curwin->w_winrow + curwin->w_height))
1540+
{
1541+
return NO;
1542+
}
1543+
else if (get_fpos_of_mouse(&m_pos) != IN_BUFFER)
1544+
{
1545+
return NO;
1546+
}
1547+
else if (VIsual_mode == 'V')
1548+
{
1549+
if ((curwin->w_cursor.lnum <= VIsual.lnum
1550+
&& (m_pos.lnum < curwin->w_cursor.lnum
1551+
|| VIsual.lnum < m_pos.lnum))
1552+
|| (VIsual.lnum < curwin->w_cursor.lnum
1553+
&& (m_pos.lnum < VIsual.lnum
1554+
|| curwin->w_cursor.lnum < m_pos.lnum)))
1555+
{
1556+
return NO;
1557+
}
1558+
}
1559+
else if ((LTOREQ_POS(curwin->w_cursor, VIsual)
1560+
&& (LT_POS(m_pos, curwin->w_cursor)
1561+
|| LT_POS(VIsual, m_pos)))
1562+
|| (LT_POS(VIsual, curwin->w_cursor)
1563+
&& (LT_POS(m_pos, VIsual)
1564+
|| LT_POS(curwin->w_cursor, m_pos))))
1565+
{
1566+
return NO;
1567+
}
1568+
else if (VIsual_mode == Ctrl_V)
1569+
{
1570+
colnr_T leftcol, rightcol;
1571+
getvcols(curwin, &curwin->w_cursor, &VIsual,
1572+
&leftcol, &rightcol);
1573+
getvcol(curwin, &m_pos, NULL, &m_pos.col, NULL);
1574+
if (m_pos.col < leftcol || m_pos.col > rightcol)
1575+
return NO;
1576+
}
1577+
1578+
// Now, also return the selection's coordinates back to caller
1579+
pos_T* visualStart = LT_POS(curwin->w_cursor, VIsual) ? &curwin->w_cursor : &VIsual;
1580+
int srow = 0;
1581+
int scol = 0, ccol = 0, ecol = 0;
1582+
textpos2screenpos(curwin, visualStart, &srow, &scol, &ccol, &ecol);
1583+
srow = srow > 0 ? srow - 1 : 0; // convert from 1-indexed to 0-indexed.
1584+
scol = scol > 0 ? scol - 1 : 0;
1585+
if (VIsual_mode == 'V')
1586+
scol = 0;
1587+
*startRow = srow;
1588+
*startCol = scol;
1589+
1590+
return YES;
1591+
}
1592+
14891593
- (oneway void)addReply:(in bycopy NSString *)reply
14901594
server:(in byref id <MMVimServerProtocol>)server
14911595
{

src/MacVim/MMCoreTextView.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
NSString* toolTip_;
8080
}
8181

82-
- (id)initWithFrame:(NSRect)frame;
82+
- (instancetype)initWithFrame:(NSRect)frame;
8383

8484
//
8585
// NSFontChanging methods
@@ -145,6 +145,7 @@
145145
// NSTextView methods
146146
//
147147
- (void)keyDown:(NSEvent *)event;
148+
- (void)quickLookWithEvent:(NSEvent *)event;
148149

149150
//
150151
// NSTextInputClient methods
@@ -161,6 +162,8 @@
161162
- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(nullable NSRangePointer)actualRange;
162163
- (NSUInteger)characterIndexForPoint:(NSPoint)point;
163164

165+
- (CGFloat)baselineDeltaForCharacterAtIndex:(NSUInteger)anIndex;
166+
164167
//
165168
// NSTextContainer methods
166169
//

src/MacVim/MMCoreTextView.m

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ @implementation MMCoreTextView {
223223
int cmdlineRow; ///< Row number (0-indexed) where the cmdline starts. Used for pinning it to the bottom if desired.
224224
}
225225

226-
- (id)initWithFrame:(NSRect)frame
226+
- (instancetype)initWithFrame:(NSRect)frame
227227
{
228228
if (!(self = [super initWithFrame:frame]))
229229
return nil;
@@ -1597,6 +1597,12 @@ - (NSUInteger)characterIndexForPoint:(NSPoint)point
15971597
return utfCharIndexFromRowCol(&grid, row, col);
15981598
}
15991599

1600+
/// Returns the cursor location in the text storage. Note that the API is
1601+
/// supposed to return a range if there are selected texts, but since we don't
1602+
/// have access to the full text storage in MacVim (it requires IPC calls to
1603+
/// Vim), we just return the cursor with the range always having zero length.
1604+
/// This affects the quickLookWithEvent: implementation where we have to
1605+
/// manually handle the selected text case.
16001606
- (NSRange)selectedRange
16011607
{
16021608
if ([helper hasMarkedText]) {
@@ -1667,8 +1673,152 @@ - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(nullable NSRang
16671673
}
16681674
}
16691675

1676+
/// Optional function in text input client. Returns the proper baseline delta
1677+
/// for the returned rect. We need to do this because we take the ceil() of
1678+
/// fontDescent, which subtly changes the baseline relative to what the OS thinks,
1679+
/// and would have resulted in a slightly offset text under certain fonts/sizes.
1680+
- (CGFloat)baselineDeltaForCharacterAtIndex:(NSUInteger)anIndex
1681+
{
1682+
// Note that this function is calculated top-down, so we need to subtract from height.
1683+
return cellSize.height - fontDescent;
1684+
}
1685+
16701686
#pragma endregion // Text Input Client
16711687

1688+
/// Perform data lookup. This gets called by the OS when the user uses
1689+
/// Ctrl-Cmd-D or the trackpad to look up data.
1690+
///
1691+
/// This implementation will default to using the OS's implementation,
1692+
/// but also perform special checking for selected text, and perform data
1693+
/// detection for URLs, etc.
1694+
- (void)quickLookWithEvent:(NSEvent *)event
1695+
{
1696+
// The default implementation would query using the NSTextInputClient API
1697+
// which works fine.
1698+
//
1699+
// However, by default, if there are texts that are selected, *and* the
1700+
// user performs lookup when the mouse is on top of said selected text, the
1701+
// OS will use that for the lookup instead. E.g. if the user has selected
1702+
// "ice cream" and perform a lookup on it, the lookup will be "ice cream"
1703+
// instead of "ice" or "cream". We need to implement this in a custom
1704+
// fashion because our `selectedRange` implementation doesn't properly
1705+
// return the selected text (which we cannot do easily since our text
1706+
// storage isn't representative of the Vim's internal buffer, see above
1707+
// design notes), by querying Vim for the selected text manually.
1708+
//
1709+
// Another custom implementation we do is by first feeding the data through
1710+
// an NSDataDetector first. This helps us catch URLs, addresses, and so on.
1711+
// Otherwise for an URL, it will not include the whole https:// part and
1712+
// won't show a web page. Note that NSTextView/WebKit/etc all use an
1713+
// internal API called Reveal which does this for free and more powerful,
1714+
// but we don't have access to that as a third-party software that
1715+
// implements a custom text view.
1716+
1717+
const NSPoint pt = [self convertPoint:[event locationInWindow] fromView:nil];
1718+
int row = 0, col = 0;
1719+
if ([self convertPoint:pt toRow:&row column:&col]) {
1720+
// 1. If we have selected text. Proceed to see if the mouse is directly on
1721+
// top of said selection and if so, show definition of that instead.
1722+
MMVimController *vc = [self vimController];
1723+
id<MMBackendProtocol> backendProxy = [vc backendProxy];
1724+
if ([backendProxy selectedTextToPasteboard:nil]) {
1725+
int selRow = 0, selCol = 0;
1726+
const BOOL isMouseInSelection = [backendProxy mouseScreenposIsSelection:row column:col selRow:&selRow selCol:&selCol];
1727+
1728+
if (isMouseInSelection) {
1729+
NSString *selectedText = [backendProxy selectedText];
1730+
if (selectedText) {
1731+
NSAttributedString *attrText = [[[NSAttributedString alloc] initWithString:selectedText
1732+
attributes:@{NSFontAttributeName: font}
1733+
] autorelease];
1734+
1735+
const NSRect selRect = [self rectForRow:selRow
1736+
column:selCol
1737+
numRows:1
1738+
numColumns:1];
1739+
1740+
NSPoint baselinePt = selRect.origin;
1741+
baselinePt.y += fontDescent;
1742+
1743+
// We have everything we need. Just show the definition and return.
1744+
[self showDefinitionForAttributedString:attrText atPoint:baselinePt];
1745+
return;
1746+
}
1747+
}
1748+
}
1749+
1750+
// 2. Check if we have specialized data. Honestly the OS should really do this
1751+
// for us as we are just calling text input client APIs here.
1752+
const NSUInteger charIndex = utfCharIndexFromRowCol(&grid, row, col);
1753+
NSTextCheckingTypes checkingTypes = NSTextCheckingTypeAddress
1754+
| NSTextCheckingTypeLink
1755+
| NSTextCheckingTypePhoneNumber;
1756+
// | NSTextCheckingTypeDate // Date doesn't really work for showDefinition without private APIs
1757+
// | NSTextCheckingTypeTransitInformation // Flight info also doesn't work without private APIs
1758+
NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:checkingTypes error:nil];
1759+
if (detector != nil) {
1760+
// Just check [-100,100) around the mouse cursor. That should be more than enough to find interesting information.
1761+
const NSUInteger rangeSize = 100;
1762+
const NSUInteger rangeOffset = charIndex > rangeSize ? rangeSize : charIndex;
1763+
const NSRange checkRange = NSMakeRange(charIndex - rangeOffset, charIndex + rangeSize * 2);
1764+
1765+
NSAttributedString *attrStr = [self attributedSubstringForProposedRange:checkRange actualRange:nil];
1766+
1767+
__block NSUInteger count = 0;
1768+
__block NSRange foundRange = NSMakeRange(0, 0);
1769+
[detector enumerateMatchesInString:attrStr.string
1770+
options:0
1771+
range:NSMakeRange(0, attrStr.length)
1772+
usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){
1773+
if (++count >= 30) {
1774+
// Sanity checking
1775+
*stop = YES;
1776+
}
1777+
1778+
NSRange matchRange = [match range];
1779+
if (!NSLocationInRange(rangeOffset, matchRange)) {
1780+
// We found something interesting nearby, but it's not where the mouse cursor is, just move on.
1781+
return;
1782+
}
1783+
if (match.resultType == NSTextCheckingTypeLink) {
1784+
foundRange = matchRange;
1785+
*stop = YES; // URL is highest priority, so we always terminate.
1786+
} else if (match.resultType == NSTextCheckingTypePhoneNumber || match.resultType == NSTextCheckingTypeAddress) {
1787+
foundRange = matchRange;
1788+
}
1789+
}];
1790+
1791+
if (foundRange.length != 0) {
1792+
// We found something interesting! Show that instead of going through the default OS behavior.
1793+
NSUInteger startIndex = charIndex + foundRange.location - rangeOffset;
1794+
1795+
int row = 0, col = 0, firstLineNumCols = 0, firstLineUtf8Len = 0;
1796+
rowColFromUtfRange(&grid, NSMakeRange(startIndex, 0), &row, &col, &firstLineNumCols, &firstLineUtf8Len);
1797+
const NSRect rectToShow = [self rectForRow:row
1798+
column:col
1799+
numRows:1
1800+
numColumns:1];
1801+
1802+
NSPoint baselinePt = rectToShow.origin;
1803+
baselinePt.y += fontDescent;
1804+
1805+
[self showDefinitionForAttributedString:attrStr
1806+
range:foundRange
1807+
options:@{}
1808+
baselineOriginProvider:^NSPoint(NSRange adjustedRange) {
1809+
return baselinePt;
1810+
}];
1811+
return;
1812+
}
1813+
}
1814+
}
1815+
1816+
// Just call the default implementation, which will call misc
1817+
// NSTextInputClient methods on us and use that to determine what/where to
1818+
// show.
1819+
[super quickLookWithEvent:event];
1820+
}
1821+
16721822
@end // MMCoreTextView
16731823

16741824

src/MacVim/MacVim.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@
146146
- (id)evaluateExpressionCocoa:(in bycopy NSString *)expr
147147
errorString:(out bycopy NSString **)errstr;
148148
- (BOOL)selectedTextToPasteboard:(byref NSPasteboard *)pboard;
149+
- (NSString *)selectedText;
150+
- (BOOL)mouseScreenposIsSelection:(int)row column:(int)column selRow:(byref int *)startRow selCol:(byref int *)startCol;
149151
- (oneway void)acknowledgeConnection;
150152
@end
151153

src/mouse.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ find_end_of_word(pos_T *pos)
146146
* Returns IN_BUFFER and sets "mpos->col" to the column when in buffer text.
147147
* The column is one for the first column.
148148
*/
149-
static int
149+
int
150150
get_fpos_of_mouse(pos_T *mpos)
151151
{
152152
win_T *wp;

src/proto/mouse.pro

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@ int mouse_comp_pos(win_T *win, int *rowp, int *colp, linenr_T *lnump, int *pline
2121
win_T *mouse_find_win(int *rowp, int *colp, mouse_find_T popup);
2222
int vcol2col(win_T *wp, linenr_T lnum, int vcol);
2323
void f_getmousepos(typval_T *argvars, typval_T *rettv);
24+
25+
// MacVim-only
26+
int get_fpos_of_mouse(pos_T *mpos);
27+
2428
/* vim: set ft=c : */

0 commit comments

Comments
 (0)