diff --git a/src/MacVim/MMBackend.m b/src/MacVim/MMBackend.m index 8e1b6f2953..a891c81491 100644 --- a/src/MacVim/MMBackend.m +++ b/src/MacVim/MMBackend.m @@ -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 )server { diff --git a/src/MacVim/MMCoreTextView.h b/src/MacVim/MMCoreTextView.h index 5f1aab2367..eb57b45e94 100644 --- a/src/MacVim/MMCoreTextView.h +++ b/src/MacVim/MMCoreTextView.h @@ -79,7 +79,7 @@ NSString* toolTip_; } -- (id)initWithFrame:(NSRect)frame; +- (instancetype)initWithFrame:(NSRect)frame; // // NSFontChanging methods @@ -145,6 +145,7 @@ // NSTextView methods // - (void)keyDown:(NSEvent *)event; +- (void)quickLookWithEvent:(NSEvent *)event; // // NSTextInputClient methods @@ -161,6 +162,8 @@ - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(nullable NSRangePointer)actualRange; - (NSUInteger)characterIndexForPoint:(NSPoint)point; +- (CGFloat)baselineDeltaForCharacterAtIndex:(NSUInteger)anIndex; + // // NSTextContainer methods // diff --git a/src/MacVim/MMCoreTextView.m b/src/MacVim/MMCoreTextView.m index 21475608f2..425d96672f 100644 --- a/src/MacVim/MMCoreTextView.m +++ b/src/MacVim/MMCoreTextView.m @@ -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; @@ -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]) { @@ -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 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 diff --git a/src/MacVim/MacVim.h b/src/MacVim/MacVim.h index e549b436e3..e3a99baed6 100644 --- a/src/MacVim/MacVim.h +++ b/src/MacVim/MacVim.h @@ -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 diff --git a/src/mouse.c b/src/mouse.c index 5481b73d8c..9e773fa668 100644 --- a/src/mouse.c +++ b/src/mouse.c @@ -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; diff --git a/src/proto/mouse.pro b/src/proto/mouse.pro index 249e7f2408..76c067dcbb 100644 --- a/src/proto/mouse.pro +++ b/src/proto/mouse.pro @@ -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 : */