From f34fd7010126f76fca4d3fd890c7f860f3a0ab33 Mon Sep 17 00:00:00 2001 From: Sarath Francis Date: Sat, 18 Jul 2026 03:30:23 -0400 Subject: [PATCH 1/2] fix(csharp) support digit separators in binary literals and numeric type suffixes The C# number mode still carried a few C/C++ habits: it used ' as the digit separator, only allowed _ in decimal and hex literals, and only recognised the u/U/l/L/f/F suffixes on plain decimal literals. So 0b1010_1010 highlighted only up to the first _, 0xFFu and 1_000UL dropped their suffix, and the decimal/double suffixes m/M and d/D were missing entirely, which leaves 19.99m half-highlighted. Allowing [\d_]+ to start with an underscore also made _count (the usual private-field convention) render its leading _ as a number. Rewrote the three variants against the lexical grammar: separators sit between digits and may follow the 0x/0b prefix, integer literals take the U/L suffixes in either order, and real literals take F/D/M as well. --- CHANGES.md | 1 + src/languages/csharp.js | 12 +++++++++--- test/markup/csharp/numbers.expect.txt | 10 ++++++++++ test/markup/csharp/numbers.txt | 10 ++++++++++ 4 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 test/markup/csharp/numbers.expect.txt create mode 100644 test/markup/csharp/numbers.txt diff --git a/CHANGES.md b/CHANGES.md index f724dac486..7bebecb027 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Core Grammars: - fix(rust) recognize `\\` and `\"` char-literal escapes so highlighting doesn't leak, issue #4351 [Sarath Francis][] - fix(cmake) only highlight standalone numbers, not digits that begin an identifier (e.g. `3rdparty`), issue #4170 [MarkXian][] - fix(cpp) require a word boundary before numeric literals so digits inside identifiers aren't highlighted as numbers, issue #4231 [Mark Xian][] +- fix(csharp) support digit separators in binary literals and numeric type suffixes, and stop highlighting the leading `_` of an identifier, issue #4258 [Sarath Francis][] Documentation: diff --git a/src/languages/csharp.js b/src/languages/csharp.js index 5201903356..2af7ede983 100644 --- a/src/languages/csharp.js +++ b/src/languages/csharp.js @@ -161,12 +161,18 @@ export default function(hljs) { literal: LITERAL_KEYWORDS }; const TITLE_MODE = hljs.inherit(hljs.TITLE_MODE, { begin: '[a-zA-Z](\\.?\\w)*' }); + // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/integral-numeric-types + // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/floating-point-numeric-types + // `_` separators sit between digits, and may also follow the `0x`/`0b` prefix + const DIGITS = '\\d(_*\\d)*'; + const INTEGER_SUFFIX = '([uU][lL]?|[lL][uU]?)?'; + const REAL_SUFFIX = '([fFdDmM]|[uU][lL]?|[lL][uU]?)?'; const NUMBERS = { className: 'number', variants: [ - { begin: '\\b(0b[01\']+)' }, - { begin: '(-?)\\b([\\d\']+(\\.[\\d\']*)?|\\.[\\d\']+)(u|U|l|L|ul|UL|f|F|b|B)' }, - { begin: '(-?)(\\b0[xX][a-fA-F0-9\'_]+|(\\b[\\d\'_]+(\\.[\\d\'_]*)?|\\.[\\d\'_]+)([eE][-+]?[\\d\'_]+)?)' } + { begin: '\\b0[bB]_*[01](_*[01])*' + INTEGER_SUFFIX }, + { begin: '(-?)\\b0[xX]_*[a-fA-F0-9](_*[a-fA-F0-9])*' + INTEGER_SUFFIX }, + { begin: '(-?)(\\b' + DIGITS + '(\\.(' + DIGITS + ')?)?|\\.' + DIGITS + ')([eE][-+]?' + DIGITS + ')?' + REAL_SUFFIX } ], relevance: 0 }; diff --git a/test/markup/csharp/numbers.expect.txt b/test/markup/csharp/numbers.expect.txt new file mode 100644 index 0000000000..c9f5a67a0c --- /dev/null +++ b/test/markup/csharp/numbers.expect.txt @@ -0,0 +1,10 @@ +int bin = 0b1010_1010; +int binSeparatorFirst = 0b_1010_1010; +ulong hex = 0xFF_FFUL; +uint hexSuffix = 0xFFu; +ulong decSuffix = 1_000_000UL; +decimal price = 19.99m; +double ratio = 3.14d; +decimal big = 1.5e10M; +int _count = 0; +int index = point._1; diff --git a/test/markup/csharp/numbers.txt b/test/markup/csharp/numbers.txt new file mode 100644 index 0000000000..c1da6780a6 --- /dev/null +++ b/test/markup/csharp/numbers.txt @@ -0,0 +1,10 @@ +int bin = 0b1010_1010; +int binSeparatorFirst = 0b_1010_1010; +ulong hex = 0xFF_FFUL; +uint hexSuffix = 0xFFu; +ulong decSuffix = 1_000_000UL; +decimal price = 19.99m; +double ratio = 3.14d; +decimal big = 1.5e10M; +int _count = 0; +int index = point._1; From 7e26a031c75ecc268c020ed96c39be7e2d062872 Mon Sep 17 00:00:00 2001 From: Sarath Francis Date: Sun, 19 Jul 2026 00:20:30 -0400 Subject: [PATCH 2/2] test(csharp) spell out that the last two cases guard false positives --- test/markup/csharp/numbers.expect.txt | 1 + test/markup/csharp/numbers.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/test/markup/csharp/numbers.expect.txt b/test/markup/csharp/numbers.expect.txt index c9f5a67a0c..b5cdaec5b6 100644 --- a/test/markup/csharp/numbers.expect.txt +++ b/test/markup/csharp/numbers.expect.txt @@ -6,5 +6,6 @@ decimal price = 19.99m; double ratio = 3.14d; decimal big = 1.5e10M; +// _count and point._1 are identifiers, not numbers int _count = 0; int index = point._1; diff --git a/test/markup/csharp/numbers.txt b/test/markup/csharp/numbers.txt index c1da6780a6..3268d3a417 100644 --- a/test/markup/csharp/numbers.txt +++ b/test/markup/csharp/numbers.txt @@ -6,5 +6,6 @@ ulong decSuffix = 1_000_000UL; decimal price = 19.99m; double ratio = 3.14d; decimal big = 1.5e10M; +// _count and point._1 are identifiers, not numbers int _count = 0; int index = point._1;