From f449e0d0f5bf08c14c5181e3a403226ab79da3f7 Mon Sep 17 00:00:00 2001 From: Yee Cheng Chin Date: Thu, 7 Dec 2023 20:43:51 -0500 Subject: [PATCH] Support native macOS monospace font (SF Mono) 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. --- runtime/doc/gui.txt | 19 +++++++++--- runtime/doc/gui_mac.txt | 3 ++ runtime/doc/tags | 1 + src/MacVim/MMCoreTextView.h | 1 + src/MacVim/MMCoreTextView.m | 21 +++++++++++-- src/MacVim/MMVimController.m | 39 ++++++++++++++++++++++- src/MacVim/MacVim.h | 3 +- src/MacVim/MacVim.m | 2 ++ src/MacVim/MacVimTests/MacVimTests.m | 33 ++++++++++++++++++++ src/MacVim/Miscellaneous.h | 3 +- src/MacVim/gui_macvim.m | 46 ++++++++++++++++++++++++++-- src/testdir/test_gui.vim | 29 ++++++++++++++++++ 12 files changed, 188 insertions(+), 12 deletions(-) diff --git a/runtime/doc/gui.txt b/runtime/doc/gui.txt index ac3dc56ee7..f9b1d27797 100644 --- a/runtime/doc/gui.txt +++ b/runtime/doc/gui.txt @@ -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* diff --git a/runtime/doc/gui_mac.txt b/runtime/doc/gui_mac.txt index 869cec0f26..926da6e14a 100644 --- a/runtime/doc/gui_mac.txt +++ b/runtime/doc/gui_mac.txt @@ -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| diff --git a/runtime/doc/tags b/runtime/doc/tags index d1ce8e9840..c8541b5767 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -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* diff --git a/src/MacVim/MMCoreTextView.h b/src/MacVim/MMCoreTextView.h index f698d55bbd..9bdf59f7e4 100644 --- a/src/MacVim/MMCoreTextView.h +++ b/src/MacVim/MMCoreTextView.h @@ -87,6 +87,7 @@ NS_ASSUME_NONNULL_BEGIN // NSFontChanging methods // - (void)changeFont:(nullable id)sender; +- (NSFontPanelModeMask)validModesForFontPanel:(NSFontPanel *)fontPanel; // // NSMenuItemValidation diff --git a/src/MacVim/MMCoreTextView.m b/src/MacVim/MMCoreTextView.m index 38b508ee04..6c146e7e3f 100644 --- a/src/MacVim/MMCoreTextView.m +++ b/src/MacVim/MMCoreTextView.m @@ -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]; @@ -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 { diff --git a/src/MacVim/MMVimController.m b/src/MacVim/MMVimController.m index cfb236376a..ef23eec78b 100644 --- a/src/MacVim/MMVimController.m +++ b/src/MacVim/MMVimController.m @@ -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 diff --git a/src/MacVim/MacVim.h b/src/MacVim/MacVim.h index 0a9bd33378..bad6f8e09a 100644 --- a/src/MacVim/MacVim.h +++ b/src/MacVim/MacVim.h @@ -433,7 +433,8 @@ enum { extern NSString *VimFindPboardType; - +// Alias for system monospace font name +extern NSString *MMSystemFontAlias; @interface NSString (MMExtras) diff --git a/src/MacVim/MacVim.m b/src/MacVim/MacVim.m index 7cffc0b89f..127f18454a 100644 --- a/src/MacVim/MacVim.m +++ b/src/MacVim/MacVim.m @@ -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; diff --git a/src/MacVim/MacVimTests/MacVimTests.m b/src/MacVim/MacVimTests/MacVimTests.m index 1ba1999851..f74f8a7356 100644 --- a/src/MacVim/MacVimTests/MacVimTests.m +++ b/src/MacVim/MacVimTests/MacVimTests.m @@ -10,6 +10,8 @@ #import +#import + #import "Miscellaneous.h" #import "MMAppController.h" #import "MMApplication.h" @@ -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 diff --git a/src/MacVim/Miscellaneous.h b/src/MacVim/Miscellaneous.h index d245fbfa32..c3a9367caf 100644 --- a/src/MacVim/Miscellaneous.h +++ b/src/MacVim/Miscellaneous.h @@ -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 diff --git a/src/MacVim/gui_macvim.m b/src/MacVim/gui_macvim.m index ea30b75d9f..7704ccb978 100644 --- a/src/MacVim/gui_macvim.m +++ b/src/MacVim/gui_macvim.m @@ -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); @@ -1141,6 +1143,22 @@ 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; @@ -1148,6 +1166,7 @@ // 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]; } @@ -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; } @@ -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 *availableFonts; if (wide) diff --git a/src/testdir/test_gui.vim b/src/testdir/test_gui.vim index a8f44f75ac..a150146a2d 100644 --- a/src/testdir/test_gui.vim +++ b/src/testdir/test_gui.vim @@ -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 = '' @@ -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'))