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;