Skip to content

Commit

Permalink
Support dictionary/data lookups of text
Browse files Browse the repository at this point in the history
This adds support for looking up data under the mouse cursor. Usually it
will bring up a dictionary, but other times it could be a Wikipedia
article, Siri knowledge, etc. Apple doesn't really have a good name for
it, other than "looking up data", "quick look" (a confusingly similar
name with the other Quick Look OS feature), or "show definition". You
can activate this by doing Ctrl-Cmd-D when the mouse is over a cursor.
If you have a trackpad, you can also either activate this using Force
click or three-finger tap (depends on your system preference settings).

Note that for Force click, this could potentially make it impossible to
use the MacVim `<ForceClick>` mapping in Vim, which allows you to map a
force click to a Vim command (#716). This is handled by having a new
setting (under a new "Input" preference pane which will have more
populated later) that allows you to choose whether to use Force click
for data lookup or Vim's `<ForceClick>` mapping. If you have configured
to use three-finger taps though this setting wouldn't do anything, and
`<ForceClick>` is always send to the Vim mapping.

Also, this is lacking a lot of features that a normal macOS application
would get, e.g. looking up selected texts (e.g. if you have "ice cream",
you may want to select the whole thing to look up the phrase, rather
than just "ice" or "cream"), data detector, and much more (e.g. custom
API support). They will be done later as part of #1311.

Fix #1191
Part of Epic #1311, which contains other items to be implemented.

Technical details below:

The way the OS knows how to look up the data and present it is by
talking to the NSTextInput/NSTextInputClient. Previously MacVim
implemented NSTextInput partially, and didn't implement the critical
firstRectForCharacterRange:actualRange and characterIndexForPoint:
functions. First, in this change we change from NSTextInput to
NSTextInputClient (which is the newer non-deprecated version), and
implement those functions, which allows the OS to query the text
storage.

By default, the OS sends a quickLookWithEvent: call to us whenever the
lookup happens but for some odd reason this isn't automatic for Force
clicks, presumably because some apps want to handle Force clicks
manually (this is why some apps only work for three-finger taps but not
Force clicks for lookups). This isn't documented but I found references
in iTerm/Firefox, and basically we just need to manually handle it and
send off quickLookWithEvent: when handling Force clicks.

For implementing the NSTextInputClient properly, the main issue is
making sure that can work properly with input methods / marked texts,
which is the other primary purpose for this class (other than inputting
keys). For data lookups, I'm represending the grid as a row-major text
(with no newline/space in between) and expose that to the OS. This
already has some issue because it doesn't handle Vim vertical splits
well, as MacVim doesn't really have access to detailed Vim text buffers
easily (unless we do a lot of calls back-and-forth). This means wrapped
texts won't be looked up properly, which I think is ok. Also, the OS
APIs deal with UTF-8 indices, so we can't just convert row/column to raw
indices and have to do a lot of character length calculations
(especially for wide chars like CJK or emojis) to make sure the returned
ranges are consistent and valid. For marked texts though, this presents
a challenge because Vim doesn't really have a strong enough API to
communicate back-and-forth about the marked positions and whatnot (it
only let the GUI know where the current cursor is), and it's hard to
implement APIs like `markedRange` properly because some marked texts
could be hidden or wrapped (if you implement some of these functions
improperly Apple's input methods could start misbehaving especially when
you use arrow keys to navigate). In the end I kept the original
implementation for treating the marked texts as a range starting from 0,
*only* when we have marked text. Kind of a hack but this makes sure we
work both in marked text mode (i.e. when inputting texts) and when doing
lookups. For simplicity I made it so that you can't do data lookups when
in marked text mode now.

Data detection:

Note that the default implementation is quite bare, and lacks a lot of
smart data detection. For example, if you put your mouse over a URL, it
won't properly select the whole URL, and addresses and dates for example
also won't get grouped together properly. This is because these require
additional implementation (e.g. using NSDataDetector) instead of coming
"for free", and will be handled later. In fact, Apple's WebKit and
NSTextView cheats by calling an internal API framework called "Reveal"
(which you can find out by intercepting NSTextView's calls and/or
looking at WebKit's source code) which is much more powerful and
supports looking up package tracking, airline info, and more, but it's
not available to third-party software (that's why Safari's lookup is so
much better than Chrome/Firefox's).

This isn't tested right now. Future task needs to add XCTest support to
properly test this as there are a lot of edge cases involved here.
  • Loading branch information
ychin committed Oct 11, 2022
1 parent bb4c48a commit d6fc1d1
Showing 12 changed files with 558 additions and 55 deletions.
12 changes: 10 additions & 2 deletions runtime/doc/gui_mac.txt
Original file line number Diff line number Diff line change
@@ -259,6 +259,8 @@ as general information regarding macOS user defaults.
Here is a list of relevant dictionary entries:

KEY VALUE ~
*MMAllowForceClickLookUp* use Force click for data lookup instead of
<ForceClick> [bool]
*MMCellWidthMultiplier* width of a normal glyph in em units [float]
*MMCmdLineAlignBottom* Pin command-line to bottom of MacVim [bool]
*MMDialogsTrackPwd* open/save dialogs track the Vim pwd [bool]
@@ -783,11 +785,17 @@ Each gesture generates one of the following Vim pseudo keys:

*<SwipeUp>* *<SwipeDown>*
Generated when swiping three fingers across the trackpad in a
vertical direction. (Not supported by the Apple Magic Mouse.)
vertical direction. (Not supported by the Apple Magic Mouse)

*<ForceClick>*
Generated when doing a Force click by pressing hard on a trackpad.
(Only supported on trackpads that support Force Touch.)
(Only supported on trackpads that support Force Touch)

If you have configured to use Force click for "Look up & data
detectors" in the system settings, by default MacVim will do a
dictionary lookup instead of triggering this mapping. You can turn
this off in MacVim's Preference pane, or directly set
|MMAllowForceClickLookUp|.

You can map these keys like with any other key using the |:map| family of
commands. For example, the following commands map left/right swipe to change
1 change: 1 addition & 0 deletions runtime/doc/tags
Original file line number Diff line number Diff line change
@@ -5417,6 +5417,7 @@ LogiPat-flags pi_logipat.txt /*LogiPat-flags*
Lua if_lua.txt /*Lua*
M motion.txt /*M*
MDI starting.txt /*MDI*
MMAllowForceClickLookUp gui_mac.txt /*MMAllowForceClickLookUp*
MMAppearanceModeSelection gui_mac.txt /*MMAppearanceModeSelection*
MMCellWidthMultiplier gui_mac.txt /*MMCellWidthMultiplier*
MMCmdLineAlignBottom gui_mac.txt /*MMCmdLineAlignBottom*
36 changes: 36 additions & 0 deletions src/MacVim/Base.lproj/Preferences.xib
Original file line number Diff line number Diff line change
@@ -9,9 +9,11 @@
<customObject id="-2" userLabel="File's Owner" customClass="MMPreferenceController">
<connections>
<outlet property="advancedPreferences" destination="620" id="632"/>
<outlet property="allowForceClickLookUpButton" destination="rlt-zw-mfW" id="xCv-HS-zyJ"/>
<outlet property="appearancePreferences" destination="hr4-G4-3ZG" id="G54-DD-ACh"/>
<outlet property="autoInstallUpdateButton" destination="UYM-W0-Kgl" id="cX5-tk-9WJ"/>
<outlet property="generalPreferences" destination="115" id="143"/>
<outlet property="inputPreferences" destination="Bnq-Nx-GJH" id="FES-rQ-Fpa"/>
<outlet property="layoutPopUpButton" destination="427" id="596"/>
<outlet property="sparkleUpdaterPane" destination="0hT-y8-Hge" id="e0L-sv-OCW"/>
</connections>
@@ -475,6 +477,40 @@
</subviews>
<point key="canvasLocation" x="137.5" y="435"/>
</customView>
<customView id="Bnq-Nx-GJH" userLabel="Input">
<rect key="frame" x="0.0" y="0.0" width="483" height="58"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<customView id="DAP-Yi-QU0" userLabel="Trackpad">
<rect key="frame" x="20" y="20" width="433" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" id="f18-Wr-EgZ">
<rect key="frame" x="-2" y="0.0" width="187" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMinY="YES"/>
<textFieldCell key="cell" sendsActionOnEndEditing="YES" alignment="right" title="Trackpad:" id="jkp-Ls-ZhN">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button id="rlt-zw-mfW" userLabel="Force click option">
<rect key="frame" x="189" y="-1" width="213" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMinY="YES"/>
<string key="toolTip">Allow Force clicks to look up data when it is configured to do so (under "Trackpad" in System Settings). This will prevent &lt;ForceClick&gt; mappings in Vim from working. If you rely on &lt;ForceClick&gt; mappings, you may want to unset this option. This setting does not matter if you have configured to use three-finger taps to look up instead, as &lt;ForceClick&gt; will always work.</string>
<buttonCell key="cell" type="check" title="Use Force click to look up data" bezelStyle="regularSquare" imagePosition="left" alignment="left" inset="2" id="A5o-Il-XdJ" userLabel="Force click option">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<binding destination="58" name="value" keyPath="values.MMAllowForceClickLookUp" id="sef-c3-KyZ"/>
</connections>
</button>
</subviews>
</customView>
</subviews>
<point key="canvasLocation" x="137.5" y="679"/>
</customView>
<customView id="620" userLabel="Advanced">
<rect key="frame" x="0.0" y="0.0" width="483" height="264"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
1 change: 1 addition & 0 deletions src/MacVim/MMAppController.m
Original file line number Diff line number Diff line change
@@ -256,6 +256,7 @@ + (void)initialize
[NSNumber numberWithBool:YES], MMShareFindPboardKey,
[NSNumber numberWithBool:NO], MMSmoothResizeKey,
[NSNumber numberWithBool:NO], MMCmdLineAlignBottomKey,
[NSNumber numberWithBool:YES], MMAllowForceClickLookUpKey,
nil];

[[NSUserDefaults standardUserDefaults] registerDefaults:dict];
40 changes: 38 additions & 2 deletions src/MacVim/MMCoreTextView.h
Original file line number Diff line number Diff line change
@@ -13,8 +13,31 @@
@class MMTextViewHelper;


/// The main text view that manages drawing Vim's content using Core Text, and
/// handles input. We are using this instead of NSTextView because of the
/// custom needs in order to draw Vim's texts, as we don't have access to the
/// full contents of Vim, and works more like a smart terminal to Vim.
///
/// Currently the rendering is done in software via Core Text, but a future
/// extension will add support for Metal rendering which probably will require
/// splitting this class up.
///
/// Since this class implements text rendering/input using a custom view, it
/// implements NSTextInputClient, mostly for the following needs:
/// 1. Text input. This is done via insertText / doCommandBySelector.
/// 2. Input methods (e.g. for CJK). This is done via the marked text and the
/// other APIs like selectedRange/firstRectForCharacterRange/etc.
/// 3. Support native dictionary lookup (quickLookWithEvent:) when the user
/// wants to. This mostly involves implementing the attributeSubstring /
/// firstRectForCharacterRange / characterIndexForPoint APIs.
/// There is an inherent difficulty to implementing NSTextInputClient
/// 'correctly', because it assumes we have an entire text storage with
/// indexable ranges. However, we don't have full access to Vim's internal
/// storage, and we are represening the screen view instead in row-major
/// indexing, but this becomes complicated when we want to implement marked
/// texts. We the relevant parts for comments on how we hack around this.
@interface MMCoreTextView : NSView <
NSTextInput
NSTextInputClient
#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_14
, NSFontChanging
, NSMenuItemValidation
@@ -122,8 +145,21 @@
// NSTextView methods
//
- (void)keyDown:(NSEvent *)event;
- (void)insertText:(id)string;

//
// NSTextInputClient methods
//
- (void)insertText:(id)string replacementRange:(NSRange)replacementRange;
- (void)doCommandBySelector:(SEL)selector;
- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange;
- (void)unmarkText;
- (NSRange)selectedRange;
- (NSRange)markedRange;
- (BOOL)hasMarkedText;
- (nullable NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(nullable NSRangePointer)actualRange;
- (nonnull NSArray<NSAttributedStringKey> *)validAttributesForMarkedText;
- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(nullable NSRangePointer)actualRange;
- (NSUInteger)characterIndexForPoint:(NSPoint)point;

//
// NSTextContainer methods
Loading

0 comments on commit d6fc1d1

Please sign in to comment.