diff --git a/assets/highlighting-tests/csharp.cs b/assets/highlighting-tests/csharp.cs new file mode 100644 index 00000000000..07c920e4611 --- /dev/null +++ b/assets/highlighting-tests/csharp.cs @@ -0,0 +1,158 @@ +// Single-line comment +/* Block comment + spanning multiple lines */ + +#nullable enable +#region Usings +global using static System.Console; +using System; +using System.Collections.Generic; +#endregion + +#if DEBUG +#pragma warning disable CS1591 +#endif + +namespace Demo.App; + +// Numbers +42; +3.14; +.5; +10_000_000; +1e10; +1.5e-3; +0xff; +0xffu; +0xffl; +0xfful; +0xFF; +0XFF; +0b1010; +0B1010; +42u; +42U; +42L; +42UL; +42ul; +42Ul; +42lu; +3.14f; +3.14d; +3.14m; +3.14M; +-.5_0_1e3_0d; +-.5_0_1E3_0D; + +// Constants +true; +false; +null; + +// Strings and characters +'a'; +'\n'; +"double quotes with escape: \" \n \t \\"; +$"double quotes with a value {1 + 1}"; +@"C:\Temp\file.txt"; +@"verbatim with ""escaped"" quotes"; +$@"interpolated verbatim {1}"; +@$"interpolated verbatim {1}"; +""" +raw string literal +"""; + +[Obsolete("Use NewThing instead")] +public sealed class Greeter +{ + private readonly string _name; + + public Greeter(string name) + { + _name = name; + } + + public string SayHello(int count = 3) + { + var list = new List { 1, 2, 3 }; + var pi = 3.14159; + var hex = 0xDEAD_BEEF; + return $"Hello {_name}, count={count}"; + } +} + +public record Person(string Name, int Age); + +class Animal +{ + private int _age; + public int Age { get => _age; init => _age = value; } + protected bool _isAlive; + public bool IsAlive => _isAlive; + + public virtual void Speak() { } +} + +class Dog : Animal +{ + private string _bark = "woof"; + + public override void Speak() + { + Console.WriteLine(_bark); + } +} + +public static class Program +{ + public static int Main(string[] args) + { + // Control flow keywords + if (args.Length == 0) + { + return 1; + } + else if (args.Length > 10) + { + return 2; + } + else + { + for (int i = 0; i < 10; i++) + { + if (i == 5) continue; + if (i == 8) break; + } + + foreach (var arg in args) + { + Console.WriteLine(arg); + } + + while (false) { } + do { } while (true); + + switch (args.Length) + { + case 1: break; + default: break; + } + } + + try + { + throw new Exception("oops"); + } + catch (Exception e) when (e is InvalidOperationException) + { + } + finally + { + } + + var query = from a in args where a is not null orderby a select a; + var g = new Greeter(args[0]); + Console.WriteLine(g.SayHello()); + return 0; + } +} diff --git a/crates/edit/src/buffer/mod.rs b/crates/edit/src/buffer/mod.rs index 468eb6898f1..f5dde4adb03 100644 --- a/crates/edit/src/buffer/mod.rs +++ b/crates/edit/src/buffer/mod.rs @@ -2177,6 +2177,7 @@ impl TextBuffer { HighlightKind::ConstantNumeric => Some(IndexedColor::BrightGreen), HighlightKind::KeywordControl => Some(IndexedColor::BrightMagenta), HighlightKind::KeywordOther => Some(IndexedColor::BrightBlue), + HighlightKind::KeywordPreprocessor => Some(IndexedColor::BrightBlue), HighlightKind::MarkupBold => None, HighlightKind::MarkupChanged => Some(IndexedColor::BrightBlue), HighlightKind::MarkupDeleted => Some(IndexedColor::BrightRed), diff --git a/crates/lsh-bin/src/main.rs b/crates/lsh-bin/src/main.rs index 68547c431d5..748483d9012 100644 --- a/crates/lsh-bin/src/main.rs +++ b/crates/lsh-bin/src/main.rs @@ -122,20 +122,23 @@ fn run_render(generator: lsh::compiler::Generator, path: &Path) -> anyhow::Resul "string" => "\x1b[91m", // Bright Red "variable" => "\x1b[96m", // Bright Cyan - "constant.language" => "\x1b[94m", // Bright Blue - "constant.numeric" => "\x1b[92m", // Bright Green - "keyword.control" => "\x1b[95m", // Bright Magenta - "keyword.other" => "\x1b[94m", // Bright Blue - "markup.bold" => "\x1b[1m", // Bold - "markup.changed" => "\x1b[94m", // Bright Blue - "markup.deleted" => "\x1b[91m", // Bright Red - "markup.heading" => "\x1b[94m", // Bright Blue - "markup.inserted" => "\x1b[92m", // Bright Green - "markup.italic" => "\x1b[3m", // Italic - "markup.link" => "\x1b[4m", // Underlined - "markup.list" => "\x1b[94m", // Bright Blue - "markup.strikethrough" => "\x1b[9m", // Strikethrough - "meta.header" => "\x1b[94m", // Bright Blue + "constant.language" => "\x1b[94m", // Bright Blue + "constant.numeric" => "\x1b[92m", // Bright Green + "keyword.control" => "\x1b[95m", // Bright Magenta + "keyword.other" => "\x1b[94m", // Bright Blue + "keyword.preprocessor" => "\x1b[94m", // Bright Blue + "markup.bold" => "\x1b[1m", // Bold + "markup.changed" => "\x1b[94m", // Bright Blue + "markup.deleted" => "\x1b[91m", // Bright Red + "markup.heading" => "\x1b[94m", // Bright Blue + "markup.inserted" => "\x1b[92m", // Bright Green + "markup.italic" => "\x1b[3m", // Italic + "markup.link" => "\x1b[4m", // Underlined + "markup.list" => "\x1b[94m", // Bright Blue + "markup.strikethrough" => "\x1b[9m", // Strikethrough + "meta.header" => "\x1b[94m", // Bright Blue + "storage.annotation" => "\x1b[36m", // Cyan + "storage.type" => "\x1b[36m", // Cyan _ => { unknown_kinds.push(hk.identifier.to_string()); diff --git a/crates/lsh/definitions/csharp.lsh b/crates/lsh/definitions/csharp.lsh new file mode 100644 index 00000000000..16e1d9b9bc1 --- /dev/null +++ b/crates/lsh/definitions/csharp.lsh @@ -0,0 +1,75 @@ +#[display_name = "C#"] +#[path = "**/*.cs"] +#[path = "**/*.csx"] +pub fn csharp() { + // Preprocessor directives always occupy the entire line. + if /\s*#\s*(?:define|elif|else|endif|endregion|error|if|line|nullable|pragma|region|undef|warning)\>.*/ { + yield keyword.preprocessor; + return; + } + + until /$/ { + yield other; + + if /\/\/.*/ { + yield comment; + } else if /\/\*/ { + loop { + yield comment; + await input; + if /\*\// { + yield comment; + break; + } + } + } else if /\$*"""/ { + // Raw string literal. It has no escape sequences and may span lines. + loop { + yield string; + if /"""/ { + yield string; + break; + } + await input; + } + } else if /\$?@\$?"/ { + // Verbatim string. `""` is the only escape sequence and it may span lines. + loop { + yield string; + if /""/ { + // Escaped quote + } else if /"/ { + yield string; + break; + } + await input; + } + } else if /\$?"/ { + double_quote_string(); + } else if /'/ { + single_quote_string(); + } else if /\[([A-Z]\w*)/ { + yield $1 as storage.annotation; + } else if /(?:break|case|catch|continue|default|do|else|finally|foreach|for|goto|if|return|switch|throw|try|while|yield)\>/ { + yield keyword.control; + } else if /(?:abstract|and|async|as|await|base|checked|class|const|delegate|dynamic|enum|event|explicit|extern|file|fixed|from|get|global|group|implicit|init|interface|internal|into|in|is|join|let|lock|nameof|namespace|new|notnull|not|operator|orderby|or|out|override|params|partial|private|protected|public|readonly|record|ref|required|scoped|sealed|select|set|sizeof|stackalloc|static|struct|this|typeof|unchecked|unmanaged|unsafe|using|var|virtual|volatile|when|where|with)\>/ { + yield keyword.other; + } else if /(?:true|false|null)\>/ { + yield constant.language; + } else if /(?:bool|byte|char|decimal|double|float|int|long|nint|nuint|object|sbyte|short|string|uint|ulong|ushort|void)\>/ { + yield storage.type; + } else if /(?i:-?(?:0x[\da-f_]+|0b[01_]+|[\d_]+\.?[\d_]*|\.[\d_]+)(?:e[+-]?[\d_]+)?[dflmu]{0,2})/ { + if /\w+/ { + // Invalid numeric literal + } else { + yield constant.numeric; + } + } else if /(\w+)\s*\(/ { + yield $1 as method; + } else if /\w+/ { + // Gobble word chars to align the next iteration on a word boundary. + } + + yield other; + } +} diff --git a/crates/lsh/src/compiler/charset.rs b/crates/lsh/src/compiler/charset.rs index e7e301e3253..50529672301 100644 --- a/crates/lsh/src/compiler/charset.rs +++ b/crates/lsh/src/compiler/charset.rs @@ -100,6 +100,17 @@ impl Charset { } } + /// Adds the opposite case of every ASCII letter in the set. + pub fn fold_case(&mut self) { + for lower in b'a'..=b'z' { + let upper = lower.to_ascii_uppercase(); + if self.get(lower) || self.get(upper) { + self.set(lower, true); + self.set(upper, true); + } + } + } + pub fn is_superset_of(&self, other: &Charset) -> bool { for (&s, &o) in self.bits.iter().zip(other.bits.iter()) { // For self to be a superset, every bit in other must be in self diff --git a/crates/lsh/src/compiler/regex.rs b/crates/lsh/src/compiler/regex.rs index 9cc3370e25a..fba4206df0a 100644 --- a/crates/lsh/src/compiler/regex.rs +++ b/crates/lsh/src/compiler/regex.rs @@ -27,6 +27,7 @@ //! | `foo` | `Prefix("foo")` - single prefix check | //! | `\+\+\+` | `Prefix("+++")` - escapes fused into literals | //! | `(?i:foo)` | `PrefixInsensitive("foo")` | +//! | `(?i:[a-f])` | `Charset` with both cases folded in | //! | `[a-z]+` | `Charset{cs, min=1, max=∞}` - greedy char class | //! | `[a-z]?` | `Charset{cs, min=0, max=1}` - optional char | //! | `$` | `EndOfLine` condition | @@ -441,6 +442,10 @@ impl<'a> RegexParser<'a> { } } + if self.case_insensitive { + charset.fold_case(); + } + if negated { charset.invert(); }