Skip to content

Commit

Permalink
Support native macOS monospace font (SF Mono)
Browse files Browse the repository at this point in the history
Can now set guifont to `-monospace-` to use the system default monospace
font, which is SF Mono on recent macOS versions (but could be updated to
other fonts in future macOS releases). The reason why this is necessary
instead of specifying the actual font name is that Apple does not expose
the SF Mono font for the user and instead only exposes an AppKit API
`monospacedSystemFontOfSize:weight:` to access it. The actual font name
(`.AppleSystemUIFontMonospaced` in macOS 14) is internal and subject to
change in different OS versions.

In older macOS versions, setting `-monospace-` will just use
`Menlo-Regular` just like the default font.

Also allow specifying the font weight for the font, e.g.
`-monospace-Semibold` / `-monospace-Light`. The list of weights follows
the NSFontWeight enum, but not all values yield unique fonts. E.g.
"UltraLight", "Thin", "Light" will all use the "Light" version of SF
Mono. The list of all font weights can be tab-completed but only if the
user has already filled in `-monospace-` in `:set guifont=`. This helps
prevents showing too many options when the user does tab completion just
to see the list of all fonts.

Note that SF Mono is currently available to be downloaded from Apple's
website as a standalone for testing. That font is mostly the same but
seems to have slightly different line spacing behavior, and when using
bold it uses the "Bold" font variant, whereas the system monospace font
uses "Semibold" variant instead.

Also make font panel not show misc formatting options like underline as
they aren't used by MacVim. Keep the background/foreground option just
so the font preview colors in the panel can be adjusted.

Also fix an existing potential buffer overflow issue in the Core Text
renderer in that `changeFont:` (when setting a new font using font panel
or using font size up/down) isn't setting `wideLen` which could cause an
unsafe memory access in Vim side.

Notes:
- Known issue: When using macaction `fontSizeUp:`/`fontSizeDown:` (Cmd
  +/-), `-monospace-` will get replaced by the internal font name (e.g.
  `.AppleSystemUIFontMonospaced-Regular`) instead due to how the
  `changeFont:` currently works. This could be fixed but it's low enough
  priority that it's ok for now.
- In the future, this may become the default font instead of Menlo, to
  make MacVim more consistent with Apple software like Terminal and
  Xcode.
  • Loading branch information
ychin committed Dec 7, 2023
1 parent 301f5b4 commit 8d68a01
Show file tree
Hide file tree
Showing 12 changed files with 188 additions and 12 deletions.
19 changes: 15 additions & 4 deletions runtime/doc/gui.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1129,11 +1129,22 @@ That's all. XLFDs are not used. For Chinese this is reported to work well: >
<
(Replace gui_gtk2 with gui_gtk3 for the GTK+ 3 GUI)

For Mac OSX you can use something like this: >
:set guifont=Monaco:h10
Also see 'macatsui', it can help fix display problems {not in MacVim}.
In MacVim, fonts with spaces are set like this: >
MacVim *macvim-guifont*

For MacVim you can use something like this: >
:set guifont=Menlo:h10
Fonts with spaces are set like this: >
:set guifont=DejaVu\ Sans\ Mono:h13
<To use bold/italic fonts, use the fully specified PostScript name of the
font, like so: >
:set guifont=Menlo-Bold:h13
<To use the system native monospace font (which is SF Mono in new macOS
versions), use the `-monospace-` keyword: >
:set guifont=-monospace-:h12
<You can also specify the font weight of the native monospace font (refer to
Apple documentation for `NSFontWeight` for possible values): >
:set guifont=-monospace-Light:h11
:set guifont=-monospace-Medium:h11
<
Mono-spaced fonts *E236*

Expand Down
3 changes: 3 additions & 0 deletions runtime/doc/gui_mac.txt
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ These are the non-standard options that MacVim supports:
'fuoptions' 'macligatures' 'macmeta' 'macthinstrokes'
'toolbariconsize' 'transparency'

These are GUI-related Vim options that have MacVim-specific behaviors:
'guifont'

*macvim-commands*
These are the non-standard commands that MacVim supports:
|:macaction| |:macmenu|
Expand Down
1 change: 1 addition & 0 deletions runtime/doc/tags
Original file line number Diff line number Diff line change
Expand Up @@ -8622,6 +8622,7 @@ macvim-encoding gui_mac.txt /*macvim-encoding*
macvim-find gui_mac.txt /*macvim-find*
macvim-full-screen gui_mac.txt /*macvim-full-screen*
macvim-gestures gui_mac.txt /*macvim-gestures*
macvim-guifont gui.txt /*macvim-guifont*
macvim-help-menu gui_mac.txt /*macvim-help-menu*
macvim-hints gui_mac.txt /*macvim-hints*
macvim-internal-variables gui_mac.txt /*macvim-internal-variables*
Expand Down
1 change: 1 addition & 0 deletions src/MacVim/MMCoreTextView.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ NS_ASSUME_NONNULL_BEGIN
// NSFontChanging methods
//
- (void)changeFont:(nullable id)sender;
- (NSFontPanelModeMask)validModesForFontPanel:(NSFontPanel *)fontPanel;

//
// NSMenuItemValidation
Expand Down
21 changes: 18 additions & 3 deletions src/MacVim/MMCoreTextView.m
Original file line number Diff line number Diff line change
Expand Up @@ -1299,9 +1299,10 @@ - (NSSize)minSize
MMMinRows * cellSize.height + insetSize.height + bot);
}

// Called when font panel selection has been made. Send the selected font to
// MMBackend so it would set guifont which will send a message back to MacVim to
// call MMWindowController::setFont.
// Called when font panel selection has been made or when adjusting font size
// using modifyFont/NSSizeUpFontAction. Send the selected font to MMBackend so
// it would set guifont which will send a message back to MacVim to call
// MMWindowController::setFont.
- (void)changeFont:(id)sender
{
NSFont *newFont = [sender convertFont:font];
Expand All @@ -1319,11 +1320,25 @@ - (void)changeFont:(id)sender
[data appendBytes:&len length:sizeof(unsigned)];
[data appendBytes:[name UTF8String] length:len];

// We don't update guifontwide for now, as panel font selection
// shouldn't affect them. This does mean Cmd +/- does not work for
// them for now.
const unsigned wideLen = 0;
[data appendBytes:&wideLen length:sizeof(unsigned)];

[[self vimController] sendMessage:SetFontMsgID data:data];
}
}
}

- (NSFontPanelModeMask)validModesForFontPanel:(NSFontPanel *)fontPanel
{
// Lets the user pick only the font face / size, as other properties as not
// useful. Still enable text/document colors as these affect the preview.
// Otherwise it could just be white text on white background in the preview.
return NSFontPanelModesMaskStandardModes & (~NSFontPanelModeMaskAllEffects | NSFontPanelModeMaskTextColorEffect | NSFontPanelModeMaskDocumentColorEffect);
}

/// Specifies whether the menu item should be enabled/disabled.
- (BOOL)validateMenuItem:(NSMenuItem *)item
{
Expand Down
39 changes: 38 additions & 1 deletion src/MacVim/MMVimController.m
Original file line number Diff line number Diff line change
Expand Up @@ -900,7 +900,44 @@ - (void)handleMessage:(int)msgid data:(NSData *)data
NSString *name = [[NSString alloc]
initWithBytes:(void*)bytes length:len
encoding:NSUTF8StringEncoding];
NSFont *font = [NSFont fontWithName:name size:size];
NSFont *font = nil;
if ([name hasPrefix:MMSystemFontAlias]) {
#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_15
if (@available(macos 10.15, *)) {
NSFontWeight fontWeight = NSFontWeightRegular;
if (name.length > MMSystemFontAlias.length) {
const NSRange cmpRange = NSMakeRange(MMSystemFontAlias.length, name.length - MMSystemFontAlias.length);
if ([name compare:@"UltraLight" options:NSCaseInsensitiveSearch range:cmpRange] == NSOrderedSame)
fontWeight = NSFontWeightUltraLight;
else if ([name compare:@"Thin" options:NSCaseInsensitiveSearch range:cmpRange] == NSOrderedSame)
fontWeight = NSFontWeightThin;
else if ([name compare:@"Light" options:NSCaseInsensitiveSearch range:cmpRange] == NSOrderedSame)
fontWeight = NSFontWeightLight;
else if ([name compare:@"Regular" options:NSCaseInsensitiveSearch range:cmpRange] == NSOrderedSame)
fontWeight = NSFontWeightRegular;
else if ([name compare:@"Medium" options:NSCaseInsensitiveSearch range:cmpRange] == NSOrderedSame)
fontWeight = NSFontWeightMedium;
else if ([name compare:@"Semibold" options:NSCaseInsensitiveSearch range:cmpRange] == NSOrderedSame)
fontWeight = NSFontWeightSemibold;
else if ([name compare:@"Bold" options:NSCaseInsensitiveSearch range:cmpRange] == NSOrderedSame)
fontWeight = NSFontWeightBold;
else if ([name compare:@"Heavy" options:NSCaseInsensitiveSearch range:cmpRange] == NSOrderedSame)
fontWeight = NSFontWeightHeavy;
else if ([name compare:@"Black" options:NSCaseInsensitiveSearch range:cmpRange] == NSOrderedSame)
fontWeight = NSFontWeightBlack;
}
font = [NSFont monospacedSystemFontOfSize:size weight:fontWeight];
}
else
#endif
{
// Fallback to Menlo on older macOS versions that don't support the system monospace font API
font = [NSFont fontWithName:@"Menlo-Regular" size:size];
}
}
else {
font = [NSFont fontWithName:name size:size];
}
if (!font) {
// This should only happen if the system default font has changed
// name since MacVim was compiled in which case we fall back on
Expand Down
3 changes: 2 additions & 1 deletion src/MacVim/MacVim.h
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,8 @@ enum {

extern NSString *VimFindPboardType;


// Alias for system monospace font name
extern NSString *MMSystemFontAlias;


@interface NSString (MMExtras)
Expand Down
2 changes: 2 additions & 0 deletions src/MacVim/MacVim.m
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
// Vim find pasteboard type (string contains Vim regex patterns)
NSString *VimFindPboardType = @"VimFindPboardType";

NSString *MMSystemFontAlias = @"-monospace-";

int ASLogLevel = MM_ASL_LEVEL_DEFAULT;


Expand Down
33 changes: 33 additions & 0 deletions src/MacVim/MacVimTests/MacVimTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

#import <objc/runtime.h>

#import <Cocoa/Cocoa.h>

#import "Miscellaneous.h"
#import "MMAppController.h"
#import "MMApplication.h"
Expand Down Expand Up @@ -425,4 +427,35 @@ - (void) testCmdlineRowCalculation {
[self waitForVimClose];
}

/// Test that using "-monospace-" for system default monospace font works.
- (void) testGuifontSystemMonospace {
MMAppController *app = MMAppController.sharedInstance;

[app openNewWindow:NewWindowClean activate:YES];
[self waitForVimOpenAndMessages];

MMTextView *textView = [[[[app keyVimController] windowController] vimView] textView];
XCTAssertEqualObjects(@"Menlo-Regular", [[textView font] fontName]);

[self sendStringToVim:@":set guifont=-monospace-\n" withMods:0];
[self waitForEventHandlingAndVimProcess];
XCTAssertEqualObjects([textView font], [NSFont monospacedSystemFontOfSize:11 weight:NSFontWeightRegular]);

[self sendStringToVim:@":set guifont=-monospace-Heavy:h12\n" withMods:0];
[self waitForEventHandlingAndVimProcess];
XCTAssertEqualObjects([textView font], [NSFont monospacedSystemFontOfSize:12 weight:NSFontWeightHeavy]);

[[[app keyVimController] windowController] fontSizeUp:nil];
[self waitForEventHandlingAndVimProcess];
XCTAssertEqualObjects([textView font], [NSFont monospacedSystemFontOfSize:13 weight:NSFontWeightHeavy]);

[[[app keyVimController] windowController] fontSizeDown:nil];
[self waitForEventHandlingAndVimProcess];
XCTAssertEqualObjects([textView font], [NSFont monospacedSystemFontOfSize:12 weight:NSFontWeightHeavy]);

// Clean up
[[app keyVimController] sendMessage:VimShouldCloseMsgID data:nil];
[self waitForVimClose];
}

@end
3 changes: 2 additions & 1 deletion src/MacVim/Miscellaneous.h
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ enum {


@interface NSNumber (MMExtras)
// HACK to allow font size to be changed via menu (bound to Cmd+/Cmd-)
// Used by modifyFont:/convertFont: to allow font size to be changed via menu
// (bound to Cmd+/Cmd-) or using macaction fontSizeUp:/fontSizeDown:.
- (NSInteger)tag;
@end

Expand Down
46 changes: 44 additions & 2 deletions src/MacVim/gui_macvim.m
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@

static NSString *MMDefaultFontName = @"Menlo-Regular";
static int MMDefaultFontSize = 11;
static char *MMDefaultFontStr = "Menlo-Regular:h11";
static char *MMDefaultFontSizeStr = "h11";
static int MMMinFontSize = 6;
static int MMMaxFontSize = 100;

// This is duplicated in MMVimController. Could consolidate in the future.
static NSString *(system_font_weights[]) = { @"UltraLight", @"Thin", @"Light", @"Regular", @"Medium", @"Semibold", @"Bold", @"Heavy", @"Black" };

static BOOL MMShareFindPboard = YES;

static GuiFont gui_macvim_font_with_name(char_u *name);
Expand Down Expand Up @@ -1141,13 +1143,30 @@
componentsJoinedByString:@" "];
}

const BOOL isSystemFont = [fontName hasPrefix:MMSystemFontAlias];
if (isSystemFont) {
if (fontName.length > MMSystemFontAlias.length) {
BOOL invalidWeight = YES;
const NSRange cmpRange = NSMakeRange(MMSystemFontAlias.length, fontName.length - MMSystemFontAlias.length);
for (size_t i = 0; i < ARRAY_LENGTH(system_font_weights); i++) {
if ([fontName compare:system_font_weights[i] options:NSCaseInsensitiveSearch range:cmpRange] == NSOrderedSame) {
invalidWeight = NO;
break;
}
}
if (invalidWeight)
return NOFONT;
}
}

if (!parseFailed && [fontName length] > 0) {
if (size < MMMinFontSize) size = MMMinFontSize;
if (size > MMMaxFontSize) size = MMMaxFontSize;

// If the default font is requested we don't need to check if NSFont
// can load it. Otherwise we ask NSFont if it can load it.
if ([fontName isEqualToString:MMDefaultFontName]
|| isSystemFont
|| [NSFont fontWithName:fontName size:size])
return [[NSString alloc] initWithFormat:@"%@:h%d", fontName, size];
}
Expand All @@ -1170,7 +1189,9 @@
{
// If guifont is empty, and we want to fill in the orig value, suggest
// the default so the user can modify it.
if (add_match((char_u *)MMDefaultFontStr) != OK)
NSString *defaultFontStr = [NSString stringWithFormat:@"%@:h%d",
MMDefaultFontName, MMDefaultFontSize];
if (add_match((char_u *)[defaultFontStr UTF8String]) != OK)
return;
}

Expand All @@ -1185,6 +1206,27 @@
return;
}

if (!wide) {
// Add system-native monospace font alias to completion.
char buf[40];
[MMSystemFontAlias getCString:buf maxLength:ARRAY_LENGTH(buf) encoding:NSASCIIStringEncoding];
if (add_match((char_u*)buf) != OK)
return;
const size_t fontAliasLen = STRLEN(buf);
if (STRNCMP(xp->xp_pattern, buf, fontAliasLen) == 0) {
// We additionally complete with font weights like "bold". We only
// do so if starting with "-monospace-" already to avoid spamming
// the user with too many variations on this.
for (size_t i = 0; i < ARRAY_LENGTH(system_font_weights); i++) {
[system_font_weights[i] getCString:buf+fontAliasLen
maxLength:ARRAY_LENGTH(buf)-fontAliasLen
encoding:NSASCIIStringEncoding];
if (add_match((char_u*)buf) != OK)
return;
}
}
}

NSFontManager *fontManager = [NSFontManager sharedFontManager];
NSArray<NSString *> *availableFonts;
if (wide)
Expand Down
29 changes: 29 additions & 0 deletions src/testdir/test_gui.vim
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,28 @@ func Test_set_guifont()
let &guifont = guifont_saved
endfunc

func Test_set_guifont_macvim()
CheckFeature gui_macvim
let guifont_saved = &guifont
let guifontwide_saved = &guifontwide

set guifont=-monospace-
call assert_equal('-monospace-:h11', getfontname())
set guifont=-monospace-Semibold
call assert_equal('-monospace-Semibold:h11', getfontname())

call assert_fails('set guifont=-monospace-SemiboldInvalidWeight', 'E596')

set guifont=Menlo\ Regular
call assert_equal('Menlo Regular:h11', getfontname())

set guifont=
call assert_equal('Menlo-Regular:h11', getfontname())

let &guifontwide = guifontwide_saved
let &guifont = guifont_saved
endfunc

func Test_set_guifontset()
CheckFeature xfontset
let skipped = ''
Expand Down Expand Up @@ -641,6 +663,13 @@ func Test_expand_guifont()
call assert_equal(['Menlo-Regular'], getcompletion('set guifont=Menl*lar$', 'cmdline'))
call assert_equal(['Menlo-Regular'], getcompletion('set guifontwide=Menl*lar$', 'cmdline'))

" Test system monospace font option. It's always the first option after
" the existing font.
call assert_equal('-monospace-', getcompletion('set guifont=', 'cmdline')[1])
call assert_equal('-monospace-', getcompletion('set guifont=-monospace-', 'cmdline')[0])
call assert_equal('-monospace-UltraLight', getcompletion('set guifont=-monospace-', 'cmdline')[1])
call assert_equal(['-monospace-Medium'], getcompletion('set guifont=-monospace-Med', 'cmdline'))

" Make sure non-monospace fonts are filtered out only in 'guifont'
call assert_equal([], getcompletion('set guifont=Hel*tica$', 'cmdline'))
call assert_equal(['Helvetica'], getcompletion('set guifontwide=Hel*tica$', 'cmdline'))
Expand Down

0 comments on commit 8d68a01

Please sign in to comment.