Skip to content

Support looking up selected texts, and also add data detector for URLs etc #1313

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 : */