Skip to content

Commit

Permalink
Merge pull request macvim-dev#1313 from ychin/lookup-selected-text-da…
Browse files Browse the repository at this point in the history
…ta-detector

Support looking up selected texts, and also add data detector for URLs etc
  • Loading branch information
ychin committed Oct 14, 2022
2 parents 6500a0c + d8d7df8 commit 31efe8c
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 3 deletions.
104 changes: 104 additions & 0 deletions src/MacVim/MMBackend.m
Original file line number Diff line number Diff line change
Expand Up @@ -1486,6 +1486,110 @@ - (BOOL)selectedTextToPasteboard:(byref NSPasteboard *)pboard
return NO;
}

/// Returns the currently selected text. We should consolidate this with
/// selectedTextToPasteboard: above when we have time. (That function has a
/// fast path just to query whether selected text exists)
- (NSString *)selectedText
{
if (VIsual_active && (State & MODE_NORMAL)) {
char_u *str = extractSelectedText();
if (!str)
return nil;

if (output_conv.vc_type != CONV_NONE) {
char_u *conv_str = string_convert(&output_conv, str, NULL);
if (conv_str) {
vim_free(str);
str = conv_str;
}
}

NSString *string = [[NSString alloc] initWithUTF8String:(char*)str];
vim_free(str);
return [string autorelease];
}
return nil;
}

/// Returns whether the provided mouse screen position is on a visually
/// selected range of text.
///
/// If yes, also return the starting row/col of the selection.
- (BOOL)mouseScreenposIsSelection:(int)row column:(int)column selRow:(byref int *)startRow selCol:(byref int *)startCol
{
// The code here is adopted from mouse.c's handling of popup_setpos.
// Unfortunately this logic is a little tricky to do in pure Vim script
// because there isn't a function to allow you to query screen pos to
// window pos. Even getmousepos() doesn't work the way you expect it to if
// you click on the placeholder rows after the last line (they all return
// the same 'column').
if (!VIsual_active)
return NO;

// We set mouse_row / mouse_col without caching/restoring, because it
// hoenstly makes sense to update them. If in the future we want a version
// that isn't mouse-related, then we may want to resotre them at the end of
// the function.
mouse_row = row;
mouse_col = column;

pos_T m_pos;

if (mouse_row < curwin->w_winrow
|| mouse_row > (curwin->w_winrow + curwin->w_height))
{
return NO;
}
else if (get_fpos_of_mouse(&m_pos) != IN_BUFFER)
{
return NO;
}
else if (VIsual_mode == 'V')
{
if ((curwin->w_cursor.lnum <= VIsual.lnum
&& (m_pos.lnum < curwin->w_cursor.lnum
|| VIsual.lnum < m_pos.lnum))
|| (VIsual.lnum < curwin->w_cursor.lnum
&& (m_pos.lnum < VIsual.lnum
|| curwin->w_cursor.lnum < m_pos.lnum)))
{
return NO;
}
}
else if ((LTOREQ_POS(curwin->w_cursor, VIsual)
&& (LT_POS(m_pos, curwin->w_cursor)
|| LT_POS(VIsual, m_pos)))
|| (LT_POS(VIsual, curwin->w_cursor)
&& (LT_POS(m_pos, VIsual)
|| LT_POS(curwin->w_cursor, m_pos))))
{
return NO;
}
else if (VIsual_mode == Ctrl_V)
{
colnr_T leftcol, rightcol;
getvcols(curwin, &curwin->w_cursor, &VIsual,
&leftcol, &rightcol);
getvcol(curwin, &m_pos, NULL, &m_pos.col, NULL);
if (m_pos.col < leftcol || m_pos.col > rightcol)
return NO;
}

// Now, also return the selection's coordinates back to caller
pos_T* visualStart = LT_POS(curwin->w_cursor, VIsual) ? &curwin->w_cursor : &VIsual;
int srow = 0;
int scol = 0, ccol = 0, ecol = 0;
textpos2screenpos(curwin, visualStart, &srow, &scol, &ccol, &ecol);
srow = srow > 0 ? srow - 1 : 0; // convert from 1-indexed to 0-indexed.
scol = scol > 0 ? scol - 1 : 0;
if (VIsual_mode == 'V')
scol = 0;
*startRow = srow;
*startCol = scol;

return YES;
}

- (oneway void)addReply:(in bycopy NSString *)reply
server:(in byref id <MMVimServerProtocol>)server
{
Expand Down
5 changes: 4 additions & 1 deletion src/MacVim/MMCoreTextView.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
NSString* toolTip_;
}

- (id)initWithFrame:(NSRect)frame;
- (instancetype)initWithFrame:(NSRect)frame;

//
// NSFontChanging methods
Expand Down Expand Up @@ -145,6 +145,7 @@
// NSTextView methods
//
- (void)keyDown:(NSEvent *)event;
- (void)quickLookWithEvent:(NSEvent *)event;

//
// NSTextInputClient methods
Expand All @@ -161,6 +162,8 @@
- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(nullable NSRangePointer)actualRange;
- (NSUInteger)characterIndexForPoint:(NSPoint)point;

- (CGFloat)baselineDeltaForCharacterAtIndex:(NSUInteger)anIndex;

//
// NSTextContainer methods
//
Expand Down
152 changes: 151 additions & 1 deletion src/MacVim/MMCoreTextView.m
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ @implementation MMCoreTextView {
int cmdlineRow; ///< Row number (0-indexed) where the cmdline starts. Used for pinning it to the bottom if desired.
}

- (id)initWithFrame:(NSRect)frame
- (instancetype)initWithFrame:(NSRect)frame
{
if (!(self = [super initWithFrame:frame]))
return nil;
Expand Down Expand Up @@ -1597,6 +1597,12 @@ - (NSUInteger)characterIndexForPoint:(NSPoint)point
return utfCharIndexFromRowCol(&grid, row, col);
}

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

/// Optional function in text input client. Returns the proper baseline delta
/// for the returned rect. We need to do this because we take the ceil() of
/// fontDescent, which subtly changes the baseline relative to what the OS thinks,
/// and would have resulted in a slightly offset text under certain fonts/sizes.
- (CGFloat)baselineDeltaForCharacterAtIndex:(NSUInteger)anIndex
{
// Note that this function is calculated top-down, so we need to subtract from height.
return cellSize.height - fontDescent;
}

#pragma endregion // Text Input Client

/// Perform data lookup. This gets called by the OS when the user uses
/// Ctrl-Cmd-D or the trackpad to look up data.
///
/// This implementation will default to using the OS's implementation,
/// but also perform special checking for selected text, and perform data
/// detection for URLs, etc.
- (void)quickLookWithEvent:(NSEvent *)event
{
// The default implementation would query using the NSTextInputClient API
// which works fine.
//
// However, by default, if there are texts that are selected, *and* the
// user performs lookup when the mouse is on top of said selected text, the
// OS will use that for the lookup instead. E.g. if the user has selected
// "ice cream" and perform a lookup on it, the lookup will be "ice cream"
// instead of "ice" or "cream". We need to implement this in a custom
// fashion because our `selectedRange` implementation doesn't properly
// return the selected text (which we cannot do easily since our text
// storage isn't representative of the Vim's internal buffer, see above
// design notes), by querying Vim for the selected text manually.
//
// Another custom implementation we do is by first feeding the data through
// an NSDataDetector first. This helps us catch URLs, addresses, and so on.
// Otherwise for an URL, it will not include the whole https:// part and
// won't show a web page. Note that NSTextView/WebKit/etc all use an
// internal API called Reveal which does this for free and more powerful,
// but we don't have access to that as a third-party software that
// implements a custom text view.

const NSPoint pt = [self convertPoint:[event locationInWindow] fromView:nil];
int row = 0, col = 0;
if ([self convertPoint:pt toRow:&row column:&col]) {
// 1. If we have selected text. Proceed to see if the mouse is directly on
// top of said selection and if so, show definition of that instead.
MMVimController *vc = [self vimController];
id<MMBackendProtocol> backendProxy = [vc backendProxy];
if ([backendProxy selectedTextToPasteboard:nil]) {
int selRow = 0, selCol = 0;
const BOOL isMouseInSelection = [backendProxy mouseScreenposIsSelection:row column:col selRow:&selRow selCol:&selCol];

if (isMouseInSelection) {
NSString *selectedText = [backendProxy selectedText];
if (selectedText) {
NSAttributedString *attrText = [[[NSAttributedString alloc] initWithString:selectedText
attributes:@{NSFontAttributeName: font}
] autorelease];

const NSRect selRect = [self rectForRow:selRow
column:selCol
numRows:1
numColumns:1];

NSPoint baselinePt = selRect.origin;
baselinePt.y += fontDescent;

// We have everything we need. Just show the definition and return.
[self showDefinitionForAttributedString:attrText atPoint:baselinePt];
return;
}
}
}

// 2. Check if we have specialized data. Honestly the OS should really do this
// for us as we are just calling text input client APIs here.
const NSUInteger charIndex = utfCharIndexFromRowCol(&grid, row, col);
NSTextCheckingTypes checkingTypes = NSTextCheckingTypeAddress
| NSTextCheckingTypeLink
| NSTextCheckingTypePhoneNumber;
// | NSTextCheckingTypeDate // Date doesn't really work for showDefinition without private APIs
// | NSTextCheckingTypeTransitInformation // Flight info also doesn't work without private APIs
NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:checkingTypes error:nil];
if (detector != nil) {
// Just check [-100,100) around the mouse cursor. That should be more than enough to find interesting information.
const NSUInteger rangeSize = 100;
const NSUInteger rangeOffset = charIndex > rangeSize ? rangeSize : charIndex;
const NSRange checkRange = NSMakeRange(charIndex - rangeOffset, charIndex + rangeSize * 2);

NSAttributedString *attrStr = [self attributedSubstringForProposedRange:checkRange actualRange:nil];

__block NSUInteger count = 0;
__block NSRange foundRange = NSMakeRange(0, 0);
[detector enumerateMatchesInString:attrStr.string
options:0
range:NSMakeRange(0, attrStr.length)
usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){
if (++count >= 30) {
// Sanity checking
*stop = YES;
}

NSRange matchRange = [match range];
if (!NSLocationInRange(rangeOffset, matchRange)) {
// We found something interesting nearby, but it's not where the mouse cursor is, just move on.
return;
}
if (match.resultType == NSTextCheckingTypeLink) {
foundRange = matchRange;
*stop = YES; // URL is highest priority, so we always terminate.
} else if (match.resultType == NSTextCheckingTypePhoneNumber || match.resultType == NSTextCheckingTypeAddress) {
foundRange = matchRange;
}
}];

if (foundRange.length != 0) {
// We found something interesting! Show that instead of going through the default OS behavior.
NSUInteger startIndex = charIndex + foundRange.location - rangeOffset;

int row = 0, col = 0, firstLineNumCols = 0, firstLineUtf8Len = 0;
rowColFromUtfRange(&grid, NSMakeRange(startIndex, 0), &row, &col, &firstLineNumCols, &firstLineUtf8Len);
const NSRect rectToShow = [self rectForRow:row
column:col
numRows:1
numColumns:1];

NSPoint baselinePt = rectToShow.origin;
baselinePt.y += fontDescent;

[self showDefinitionForAttributedString:attrStr
range:foundRange
options:@{}
baselineOriginProvider:^NSPoint(NSRange adjustedRange) {
return baselinePt;
}];
return;
}
}
}

// Just call the default implementation, which will call misc
// NSTextInputClient methods on us and use that to determine what/where to
// show.
[super quickLookWithEvent:event];
}

@end // MMCoreTextView


Expand Down
2 changes: 2 additions & 0 deletions src/MacVim/MacVim.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@
- (id)evaluateExpressionCocoa:(in bycopy NSString *)expr
errorString:(out bycopy NSString **)errstr;
- (BOOL)selectedTextToPasteboard:(byref NSPasteboard *)pboard;
- (NSString *)selectedText;
- (BOOL)mouseScreenposIsSelection:(int)row column:(int)column selRow:(byref int *)startRow selCol:(byref int *)startCol;
- (oneway void)acknowledgeConnection;
@end

Expand Down
2 changes: 1 addition & 1 deletion src/mouse.c
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ find_end_of_word(pos_T *pos)
* Returns IN_BUFFER and sets "mpos->col" to the column when in buffer text.
* The column is one for the first column.
*/
static int
int
get_fpos_of_mouse(pos_T *mpos)
{
win_T *wp;
Expand Down
4 changes: 4 additions & 0 deletions src/proto/mouse.pro
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ int mouse_comp_pos(win_T *win, int *rowp, int *colp, linenr_T *lnump, int *pline
win_T *mouse_find_win(int *rowp, int *colp, mouse_find_T popup);
int vcol2col(win_T *wp, linenr_T lnum, int vcol);
void f_getmousepos(typval_T *argvars, typval_T *rettv);

// MacVim-only
int get_fpos_of_mouse(pos_T *mpos);

/* vim: set ft=c : */

0 comments on commit 31efe8c

Please sign in to comment.