From 6563c703f96dc9907c7ed700991069fc4ffe2f7c Mon Sep 17 00:00:00 2001 From: David Whetstone Date: Fri, 3 Jul 2026 07:43:26 -0700 Subject: [PATCH 1/3] Add GUI support for OpenType font feature settings Add a SetFontFeaturesMsgID message that carries a comma-separated list of OpenType feature settings ("tag" or "tag=N" entries). The Core Text renderer parses the list into a kCTFontFeatureSettingsAttribute array and applies it when deriving the fonts used for drawing, so the base, wide, bold and italic fonts all pick up the features. Requires macOS 10.13 for the OpenType feature tag API; older systems ignore the setting. The legacy renderer does not support font features. Signed-off-by: David Whetstone --- src/MacVim/MMCoreTextView.h | 3 ++ src/MacVim/MMCoreTextView.m | 57 ++++++++++++++++++++++++++++++++++++ src/MacVim/MMTextView.h | 1 + src/MacVim/MMTextView.m | 5 ++++ src/MacVim/MMVimController.m | 11 +++++++ src/MacVim/MacVim.h | 1 + 6 files changed, 78 insertions(+) diff --git a/src/MacVim/MMCoreTextView.h b/src/MacVim/MMCoreTextView.h index 15ec0b9c64..0e40314c9d 100644 --- a/src/MacVim/MMCoreTextView.h +++ b/src/MacVim/MMCoreTextView.h @@ -66,6 +66,8 @@ NS_ASSUME_NONNULL_BEGIN BOOL antialias; BOOL ligatures; BOOL thinStrokes; + NSString *fontFeatures; ///< Raw 'macfontfeatures' value + NSArray *fontFeatureSettings; ///< Parsed kCTFontFeatureSettingsAttribute array, nil if none BOOL forceRefreshFont; // when true, don't early out of setFont calls. @@ -138,6 +140,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)setPreEditRow:(int)row column:(int)col; - (void)setMouseShape:(int)shape; - (void)setAntialias:(BOOL)state; +- (void)setFontFeatures:(NSString *)features; - (void)setLigatures:(BOOL)state; - (void)setThinStrokes:(BOOL)state; - (void)setImControl:(BOOL)enable; diff --git a/src/MacVim/MMCoreTextView.m b/src/MacVim/MMCoreTextView.m index 7a44da201c..a047328bd4 100644 --- a/src/MacVim/MMCoreTextView.m +++ b/src/MacVim/MMCoreTextView.m @@ -136,6 +136,23 @@ - (void)invertBlockFromRow:(int)row column:(int)col numRows:(int)nrows return [@"m" sizeWithAttributes:a].width; } +/// Returns a font derived from the supplied one with the given OpenType +/// feature settings (a kCTFontFeatureSettingsAttribute-style array) applied. + static NSFont * +fontWithFeatureSettings(NSFont *base, NSArray *settings) +{ + if (settings == nil || base == nil) + return base; + CTFontDescriptorRef desc = CTFontDescriptorCreateWithAttributes( + (CFDictionaryRef)@{ (NSString *)kCTFontFeatureSettingsAttribute: settings }); + CTFontRef derived = CTFontCreateCopyWithAttributes( + (CTFontRef)base, 0.0, NULL, desc); + CFRelease(desc); + if (derived == NULL) + return base; + return [(NSFont *)derived autorelease]; +} + typedef struct { unsigned color; int shape; @@ -284,6 +301,8 @@ - (void)dealloc [characterStrings release]; characterStrings = nil; [fontVariants release]; fontVariants = nil; [characterLines release]; characterLines = nil; + [fontFeatures release]; fontFeatures = nil; + [fontFeatureSettings release]; fontFeatureSettings = nil; [helper setTextView:nil]; [helper release]; helper = nil; @@ -582,6 +601,43 @@ - (void)setAntialias:(BOOL)state antialias = state; } +- (void)setFontFeatures:(NSString *)features +{ + if (fontFeatures == features || [fontFeatures isEqualToString:features]) + return; + [fontFeatures release]; + fontFeatures = [features copy]; + [fontFeatureSettings release]; + fontFeatureSettings = nil; + +#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_13 + if (features.length > 0) { + if (@available(macos 10.13, *)) { + // Parse the 'macfontfeatures' value ("tag" or "tag=N" entries) + // into the array of dictionaries CoreText expects for + // kCTFontFeatureSettingsAttribute. + NSMutableArray *settings = [NSMutableArray array]; + for (NSString *entry in [features componentsSeparatedByString:@","]) { + NSArray *kv = [entry componentsSeparatedByString:@"="]; + NSString *tag = kv[0]; + if (tag.length != 4) + continue; // already validated by Vim; be defensive + int value = kv.count > 1 ? [kv[1] intValue] : 1; + [settings addObject:@{ + (NSString *)kCTFontOpenTypeFeatureTag: tag, + (NSString *)kCTFontOpenTypeFeatureValue: @(value), + }]; + } + if (settings.count > 0) + fontFeatureSettings = [settings retain]; + } + } +#endif + + [fontVariants removeAllObjects]; + [characterLines removeAllObjects]; +} + - (void)setLigatures:(BOOL)state { ligatures = state; @@ -2116,6 +2172,7 @@ - (NSFont *)fontVariantForTextFlags:(int)textFlags { fontRef = [NSFontManager.sharedFontManager convertFont:fontRef toHaveTrait:NSFontItalicTrait]; if (textFlags & DRAW_BOLD) fontRef = [NSFontManager.sharedFontManager convertFont:fontRef toHaveTrait:NSFontBoldTrait]; + fontRef = fontWithFeatureSettings(fontRef, fontFeatureSettings); fontVariants[cacheFlags] = fontRef; } diff --git a/src/MacVim/MMTextView.h b/src/MacVim/MMTextView.h index b18ee08203..c76af45f26 100644 --- a/src/MacVim/MMTextView.h +++ b/src/MacVim/MMTextView.h @@ -39,6 +39,7 @@ - (void)performBatchDrawWithData:(NSData *)data; - (void)setMouseShape:(int)shape; - (void)setAntialias:(BOOL)antialias; +- (void)setFontFeatures:(NSString *)features; - (void)setLigatures:(BOOL)ligatures; - (void)setThinStrokes:(BOOL)thinStrokes; - (void)setImControl:(BOOL)enable; diff --git a/src/MacVim/MMTextView.m b/src/MacVim/MMTextView.m index 6de0e56478..fc4318fc1b 100644 --- a/src/MacVim/MMTextView.m +++ b/src/MacVim/MMTextView.m @@ -307,6 +307,11 @@ - (void)setAntialias:(BOOL)state antialias = state; } +- (void)setFontFeatures:(NSString *)features +{ + // Not supported by this renderer; only MMCoreTextView handles this. +} + - (void)setLigatures:(BOOL)state { ligatures = state; diff --git a/src/MacVim/MMVimController.m b/src/MacVim/MMVimController.m index e48f55f411..09c6a0e94f 100644 --- a/src/MacVim/MMVimController.m +++ b/src/MacVim/MMVimController.m @@ -1185,6 +1185,17 @@ - (void)handleMessage:(int)msgid data:(NSData *)data [[[windowController vimView] textView] setAntialias:NO]; } break; + case SetFontFeaturesMsgID: + { + const void *bytes = [data bytes]; + int len = *((int*)bytes); bytes += sizeof(int); + NSString *features = len > 0 + ? [[[NSString alloc] initWithBytes:(void*)bytes length:len + encoding:NSUTF8StringEncoding] autorelease] + : @""; + [[[windowController vimView] textView] setFontFeatures:features]; + } + break; case EnableLigaturesMsgID: { [[[windowController vimView] textView] setLigatures:YES]; diff --git a/src/MacVim/MacVim.h b/src/MacVim/MacVim.h index 6291531677..90926cfa11 100644 --- a/src/MacVim/MacVim.h +++ b/src/MacVim/MacVim.h @@ -359,6 +359,7 @@ extern const char * const MMVimMsgIDStrings[]; MSG(EnableThinStrokesMsgID) \ MSG(DisableThinStrokesMsgID) \ MSG(ShowDefinitionMsgID) \ + MSG(SetFontFeaturesMsgID) \ MSG(LoopBackMsgID) /* Simple message that Vim will reflect back to MacVim */ \ MSG(LastMsgID) \ From 34daa74f5e9b7c4c6e03006c87c9f76be5e02c70 Mon Sep 17 00:00:00 2001 From: David Whetstone Date: Fri, 3 Jul 2026 07:43:36 -0700 Subject: [PATCH 2/3] Add macfontfeatures option The macfontfeatures option is a comma-separated list of OpenType font features to enable, e.g.: :set macfontfeatures=cv01,cv09,ss03=2 Each entry is a four-character feature tag optionally followed by "=" and a number; a bare tag is equivalent to "tag=1". This allows selecting character variants and stylistic sets in fonts that provide them, similar to font feature settings in other editors. Setting the option to an empty string restores the font's default features. Invalid values are rejected with E474. Includes tests for option existence, validation and round-tripping. Signed-off-by: David Whetstone --- runtime/optwin.vim | 2 ++ src/MacVim/MMBackend.h | 1 + src/MacVim/MMBackend.m | 13 ++++++++++ src/MacVim/gui_macvim.m | 8 +++++++ src/option.c | 40 +++++++++++++++++++++++++++++++ src/option.h | 1 + src/optiondefs.h | 9 +++++++ src/proto/gui_macvim.pro | 1 + src/proto/option.pro | 1 + src/testdir/test_macvim.vim | 14 +++++++++++ src/testdir/util/gen_opt_test.vim | 2 ++ 11 files changed, 92 insertions(+) diff --git a/runtime/optwin.vim b/runtime/optwin.vim index 606166da72..3743e9b091 100644 --- a/runtime/optwin.vim +++ b/runtime/optwin.vim @@ -756,6 +756,8 @@ if has("gui") call BinOptionG("fullscreen", &fullscreen) call append("$", "fuoptions\tcontrol how fullscreen mode should behave") call OptionG("fuoptions", &fuoptions) + call append("$", "macfontfeatures\tOpenType font features to enable") + call OptionG("macfontfeatures", &macfontfeatures) call append("$", "macligatures\tdisplay ligatures") call BinOptionG("macligatures", &macligatures) call append("$", "macmeta\tuse option as meta key") diff --git a/src/MacVim/MMBackend.h b/src/MacVim/MMBackend.h index 0ed690bd25..21338ac925 100644 --- a/src/MacVim/MMBackend.h +++ b/src/MacVim/MMBackend.h @@ -134,6 +134,7 @@ - (void)setFullScreenBackgroundColor:(int)color; - (void)setAntialias:(BOOL)antialias; +- (void)setFontFeatures:(NSString *)features; - (void)setLigatures:(BOOL)ligatures; - (void)setThinStrokes:(BOOL)thinStrokes; - (void)setBlurRadius:(int)radius; diff --git a/src/MacVim/MMBackend.m b/src/MacVim/MMBackend.m index d58eae8476..5872e947ad 100644 --- a/src/MacVim/MMBackend.m +++ b/src/MacVim/MMBackend.m @@ -1196,6 +1196,19 @@ - (void)setAntialias:(BOOL)antialias [self queueMessage:msgid data:nil]; } +- (void)setFontFeatures:(NSString *)features +{ + // NOTE: Unlike setFont:wide: an empty string must still be sent since it + // means "reset to the font's default features". + int len = (int)[features lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + NSMutableData *data = [NSMutableData data]; + [data appendBytes:&len length:sizeof(int)]; + if (len > 0) + [data appendBytes:[features UTF8String] length:len]; + + [self queueMessage:SetFontFeaturesMsgID data:data]; +} + - (void)setLigatures:(BOOL)ligatures { int msgid = ligatures ? EnableLigaturesMsgID : DisableLigaturesMsgID; diff --git a/src/MacVim/gui_macvim.m b/src/MacVim/gui_macvim.m index de46c923c9..911512e549 100644 --- a/src/MacVim/gui_macvim.m +++ b/src/MacVim/gui_macvim.m @@ -2134,6 +2134,14 @@ [[MMBackend sharedInstance] setAntialias:antialias]; } + void +gui_macvim_set_fontfeatures(char_u *features) +{ + NSString *s = features != NULL ? [NSString stringWithVimString:features] + : @""; + [[MMBackend sharedInstance] setFontFeatures:s]; +} + void gui_macvim_set_ligatures(int ligatures) { diff --git a/src/option.c b/src/option.c index 668a74fbbd..6189f785aa 100644 --- a/src/option.c +++ b/src/option.c @@ -5324,6 +5324,46 @@ did_set_fullscreen(optset_T *args) #endif #if defined(FEAT_GUI_MACVIM) || defined(PROTO) +/* + * Process the updated 'macfontfeatures' option value. + * The value is a comma-separated list of OpenType feature settings, each a + * four-character tag optionally followed by "=" and a non-negative number. + */ + char * +did_set_macfontfeatures(optset_T *args UNUSED) +{ + char_u *p = p_macfontfeatures; + + while (*p != NUL) + { + int i; + + // OpenType tag: exactly 4 printable ASCII chars (not ',' or '=') + for (i = 0; i < 4; ++i) + if (p[i] < 0x20 || p[i] > 0x7e || p[i] == ',' || p[i] == '=') + return e_invalid_argument; + p += 4; + if (*p == '=') + { + ++p; + if (!VIM_ISDIGIT(*p)) + return e_invalid_argument; + while (VIM_ISDIGIT(*p)) + ++p; + } + if (*p == ',') + { + if (*++p == NUL) + return e_invalid_argument; // trailing comma + } + else if (*p != NUL) + return e_invalid_argument; + } + + gui_macvim_set_fontfeatures(p_macfontfeatures); + return NULL; +} + /* * Process the updated 'macligatures' option value. */ diff --git a/src/option.h b/src/option.h index 023002739c..47e885cdea 100644 --- a/src/option.h +++ b/src/option.h @@ -795,6 +795,7 @@ EXTERN int p_lpl; // 'loadplugins' EXTERN char_u *p_luadll; // 'luadll' #endif #ifdef FEAT_GUI_MACVIM +EXTERN char_u *p_macfontfeatures; // 'macfontfeatures' EXTERN int p_macligatures; // 'macligatures' EXTERN int p_macthinstrokes; // 'macthinstrokes' EXTERN long p_columnspace; // 'columnspace' diff --git a/src/optiondefs.h b/src/optiondefs.h index adfbea22b2..ceb790648c 100644 --- a/src/optiondefs.h +++ b/src/optiondefs.h @@ -1775,6 +1775,15 @@ static struct vimoption options[] = (char_u *)NULL, PV_NONE, NULL, NULL, {(char_u *)"", (char_u *)0L} SCTX_INIT}, + {"macfontfeatures", NULL, P_STRING|P_VI_DEF|P_RCLR|P_ONECOMMA|P_NODUP, +#ifdef FEAT_GUI_MACVIM + (char_u *)&p_macfontfeatures, PV_NONE, did_set_macfontfeatures, NULL, + {(char_u *)"", (char_u *)0L} +#else + (char_u *)NULL, PV_NONE, NULL, NULL, + {(char_u *)NULL, (char_u *)0L} +#endif + SCTX_INIT}, {"macligatures", NULL, P_BOOL|P_VI_DEF|P_RCLR, #ifdef FEAT_GUI_MACVIM (char_u *)&p_macligatures, PV_NONE, did_set_macligatures, NULL, diff --git a/src/proto/gui_macvim.pro b/src/proto/gui_macvim.pro index eaa46b200f..8217823371 100644 --- a/src/proto/gui_macvim.pro +++ b/src/proto/gui_macvim.pro @@ -106,6 +106,7 @@ void gui_mch_fuopt_update(void); void gui_macvim_update_modified_flag(void); void gui_macvim_add_to_find_pboard(char_u *pat); void gui_macvim_set_antialias(int antialias); +void gui_macvim_set_fontfeatures(char_u *features); void gui_macvim_set_ligatures(int ligatures); void gui_macvim_set_thinstrokes(int thinStrokes); void gui_macvim_set_blur(int blur); diff --git a/src/proto/option.pro b/src/proto/option.pro index d327b59803..6a3738d82b 100644 --- a/src/proto/option.pro +++ b/src/proto/option.pro @@ -100,6 +100,7 @@ char *did_set_antialias(optset_T *args); char *did_set_blur(optset_T *args); char *did_set_columnspace(optset_T *args); char *did_set_fullscreen(optset_T *args); +char *did_set_macfontfeatures(optset_T *args); char *did_set_macligatures(optset_T *args); char *did_set_macthinstrokes(optset_T *args); char *did_set_transparency(optset_T *args); diff --git a/src/testdir/test_macvim.vim b/src/testdir/test_macvim.vim index 6feb090b16..315e87d9d2 100644 --- a/src/testdir/test_macvim.vim +++ b/src/testdir/test_macvim.vim @@ -10,6 +10,7 @@ func Test_macvim_options_commands_exist() call assert_true(exists('+blurradius'), 'Missing option "blurradius"') call assert_true(exists('+fullscreen'), 'Missing option "fullscreen"') call assert_true(exists('+fuoptions'), 'Missing option "fuoptions"') + call assert_true(exists('+macfontfeatures'), 'Missing option "macfontfeatures"') call assert_true(exists('+macligatures'), 'Missing option "macligatures"') call assert_true(exists('+macmeta'), 'Missing option "macmeta"') call assert_true(exists('+macthinstrokes'), 'Missing option "macthinstrokes"') @@ -75,4 +76,17 @@ func Test_macvim_invalid_options() call assert_fails("let &transparency=101", 'E474:') call assert_fails("let &fuoptions='abcdef'", 'E474:') + + call assert_fails("set macfontfeatures=abc", 'E474:') + call assert_fails("set macfontfeatures=abcde", 'E474:') + call assert_fails("set macfontfeatures=cv01=x", 'E474:') + call assert_fails("set macfontfeatures=cv01,", 'E474:') +endfunc + +" Test that 'macfontfeatures' accepts valid values and can be reset +func Test_macvim_macfontfeatures() + set macfontfeatures=cv01,ss03=2,zero=0 + call assert_equal('cv01,ss03=2,zero=0', &macfontfeatures) + set macfontfeatures= + call assert_equal('', &macfontfeatures) endfunc diff --git a/src/testdir/util/gen_opt_test.vim b/src/testdir/util/gen_opt_test.vim index 250ed669b6..e3ceac9d34 100644 --- a/src/testdir/util/gen_opt_test.vim +++ b/src/testdir/util/gen_opt_test.vim @@ -392,6 +392,8 @@ let test_values = { \ \ 'blurradius': [[], [-1]], \ 'fuoptions': [[], ['xxx']], + \ 'macfontfeatures': [['', 'cv01', 'cv01,cv09', 'ss03=2', 'liga=0'], + \ ['xxx', 'cv1', 'cv01=', 'cv01=x', 'cv01,,cv02']], \ 'transparency': [[], ['-1']], \ "\ default behaviours From 709ea31cdd119688f97d46a45a50de5cba626605 Mon Sep 17 00:00:00 2001 From: David Whetstone Date: Fri, 3 Jul 2026 07:43:47 -0700 Subject: [PATCH 3/3] Document macfontfeatures option Signed-off-by: David Whetstone --- runtime/doc/gui_mac.txt | 4 ++-- runtime/doc/options.txt | 22 ++++++++++++++++++++++ runtime/doc/quickref.txt | 1 + runtime/doc/tags | 1 + 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/runtime/doc/gui_mac.txt b/runtime/doc/gui_mac.txt index 5bf38e75e0..00ddde322f 100644 --- a/runtime/doc/gui_mac.txt +++ b/runtime/doc/gui_mac.txt @@ -115,8 +115,8 @@ They are only usable when GUI is active. *macvim-options* These are the non-standard options that MacVim supports: 'antialias' 'blurradius' 'fullscreen' - 'fuoptions' 'macligatures' 'macmeta' 'macthinstrokes' - 'toolbariconsize' 'transparency' + 'fuoptions' 'macfontfeatures' 'macligatures' 'macmeta' + 'macthinstrokes' 'toolbariconsize' 'transparency' These are GUI-related Vim options that have MacVim-specific behaviors: 'guifont' diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 97d8a409ac..6e7657a728 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -6026,6 +6026,28 @@ A jump table for the options with a short description can be found at |Q_op|. {not supported} No longer supported, as the Mac OS X GUI code was removed. + *'macfontfeatures'* +'macfontfeatures' string (default "") + global + {not in Vi} + {only available when compiled with GUI enabled on + Mac OS X} + Comma-separated list of OpenType font features to enable for the text + in the window. Each entry is a four-character OpenType feature tag, + optionally followed by "=" and a number. A bare tag is equivalent to + "tag=1"; use "tag=0" to disable a feature the font enables by + default. Example: > + :set macfontfeatures=cv01,cv09,ss03=2,zero +< This requires the selected 'guifont' to support the given features + (e.g. character variants "cv01"-"cv99" or stylistic sets + "ss01"-"ss20"). Features not supported by the font are ignored. + Set to an empty string to restore the font's default features. + Note: use 'macligatures' to control standard ligatures; that option + takes precedence over a "liga" entry here. + Note: Currently this option only has an effect if + 'Use Core Text renderer' is enabled in the GUI preferences pane, and + requires Mac OS X v10.13 or later. + *'macligatures'* *'nomacligatures'* 'macligatures' boolean (default off) global diff --git a/runtime/doc/quickref.txt b/runtime/doc/quickref.txt index 2400246e76..c68dc35f8f 100644 --- a/runtime/doc/quickref.txt +++ b/runtime/doc/quickref.txt @@ -810,6 +810,7 @@ Short explanation of each option: *option-list* 'loadplugins' 'lpl' load plugin scripts when starting up 'luadll' name of the Lua dynamic library 'macatsui' Mac GUI: use ATSUI text drawing +'macfontfeatures' OpenType font features to enable (MacVim GUI only) 'macligatures' display ligatures (MacVim GUI only) 'macmeta' 'mmta' use option as meta key (MacVim GUI only) 'macthinstrokes' render the text lighter by using thin strokes (MacVim GUI only) diff --git a/runtime/doc/tags b/runtime/doc/tags index edeb9941ff..d6bda3d1a2 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -513,6 +513,7 @@ $quote eval.txt /*$quote* 'lz' options.txt /*'lz'* 'ma' options.txt /*'ma'* 'macatsui' options.txt /*'macatsui'* +'macfontfeatures' options.txt /*'macfontfeatures'* 'macligatures' options.txt /*'macligatures'* 'macmeta' options.txt /*'macmeta'* 'macthinstrokes' options.txt /*'macthinstrokes'*