diff --git a/assets/highlighting-tests/csharp.cs b/assets/highlighting-tests/csharp.cs new file mode 100644 index 00000000000..9b139bf087a --- /dev/null +++ b/assets/highlighting-tests/csharp.cs @@ -0,0 +1,65 @@ +// Single-line comment +/* Block comment + spanning multiple lines */ + +using System; +using System.Collections.Generic; + +#nullable enable + +namespace Demo.App; + +[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; + + // Regular string + var a = "Hello, \"world\""; + + // Verbatim string + var b = @"C:\\Temp\\file.txt"; + + // Interpolated string + var c = $"Hello {_name}, count={count}"; + + // Interpolated string with alignment + format specifier + var c2 = $"Name={_name,-10} Count={count:D2}"; + + // Interpolated verbatim string + var d = $@"Name={_name}\nCount={count}"; + + // Escaped literal braces in interpolated string (should stay string-colored) + var d2 = $"Literal braces: {{ and }}"; + + return c; + } +} + +public record Person(string Name, int Age); + +public static class Program +{ + public static int Main(string[] args) + { + if (args.Length == 0) + { + return 1; + } + + var g = new Greeter(args[0]); + Console.WriteLine(g.SayHello()); + return 0; + } +} diff --git a/assets/highlighting-tests/markdown.md b/assets/highlighting-tests/markdown.md index 555b182f961..b0ecfbf1d1f 100644 --- a/assets/highlighting-tests/markdown.md +++ b/assets/highlighting-tests/markdown.md @@ -84,3 +84,10 @@ export function greet(name) { def greet(name: str) -> str: return f"hello {name}" ``` + +```CSharp +public string Greet(string name) +{ + return $"hello {name}"; +} +``` diff --git a/crates/edit/src/buffer/mod.rs b/crates/edit/src/buffer/mod.rs index 9277192e500..e1e86a63a0f 100644 --- a/crates/edit/src/buffer/mod.rs +++ b/crates/edit/src/buffer/mod.rs @@ -2187,6 +2187,7 @@ impl TextBuffer { HighlightKind::MarkupList => Some(IndexedColor::BrightBlue), HighlightKind::MarkupStrikethrough => None, HighlightKind::MetaHeader => Some(IndexedColor::BrightBlue), + HighlightKind::MetaPreprocessor => Some(IndexedColor::BrightBlue), }; let attr = match curr.kind { HighlightKind::MarkupBold => Some(Attributes::Bold), diff --git a/crates/lsh-bin/src/main.rs b/crates/lsh-bin/src/main.rs index 68547c431d5..cf341cbacac 100644 --- a/crates/lsh-bin/src/main.rs +++ b/crates/lsh-bin/src/main.rs @@ -136,6 +136,7 @@ fn run_render(generator: lsh::compiler::Generator, path: &Path) -> anyhow::Resul "markup.list" => "\x1b[94m", // Bright Blue "markup.strikethrough" => "\x1b[9m", // Strikethrough "meta.header" => "\x1b[94m", // Bright Blue + "meta.preprocessor" => "\x1b[94m", // Bright Blue _ => { 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..ce697b2252a --- /dev/null +++ b/crates/lsh/definitions/csharp.lsh @@ -0,0 +1,158 @@ +#[display_name = "C#"] +#[path = "**/*.cs"] +#[path = "**/*.csx"] +pub fn csharp() { + // Preprocessor directives (entire line) + if /\s*#\s*(?:if|elif|else|endif|define|undef|warning|error|line|nullable|region|endregion|pragma)\>.*/ { + yield meta.preprocessor; + return; + } + + until /$/ { + yield other; + + if /\/\/.*/ { + yield comment; + } else if /\/\*/ { + loop { + yield comment; + await input; + if /\*\// { + yield comment; + break; + } + } + } else if /(?:\$@|@\$)"/ { + // Interpolated verbatim string: $@"..." or @$"..." + loop { + yield string; + if /""/ { + // Escaped quote in verbatim strings + } else if /\{\{/ { + // Escaped literal '{' + } else if /\}\}/ { + // Escaped literal '}' + } else if /(\{)/ { + // Interpolation start: braces default, contents light blue. + yield $1 as other; + yield constant.language; + + loop { + if /(\})/ { + yield $1 as other; + break; + } else if /[^}:,]+/ { + // Interpolation contents (up to ':' or '}') + yield constant.language; + } else if /,\s*[^}:]+/ { + // Alignment specifier: , -10, ,10, etc. + yield constant.numeric; + yield constant.language; + } else if /:[^}]+/ { + // Format specifier: :D2, :X, :N0, etc. + yield constant.numeric; + yield constant.language; + } else { + break; + } + } + + // Resume string highlighting + yield string; + } else if /"/ { + yield string; + break; + } + await input; + } + } else if /@"/ { + // Verbatim string: @"..." + loop { + yield string; + if /""/ { + // Escaped quote in verbatim strings + } else if /"/ { + yield string; + break; + } + await input; + } + } else if /\$"/ { + // Interpolated string: $"..." + until /$/ { + yield string; + if /\\./ { + } else if /\{\{/ { + // Escaped literal '{' + } else if /\}\}/ { + // Escaped literal '}' + } else if /(\{)/ { + // Interpolation start: highlight braces as default, contents as light blue. + yield $1 as other; + yield constant.language; + + loop { + if /(\})/ { + yield $1 as other; + break; + } else if /[^}:,]+/ { + // Interpolation contents (up to ':' or '}') + yield constant.language; + } else if /,\s*[^}:]+/ { + // Alignment specifier: , -10, ,10, etc. + yield constant.numeric; + yield constant.language; + } else if /:[^}]+/ { + // Format specifier: :D2, :X, :N0, etc. + yield constant.numeric; + yield constant.language; + } else { + break; + } + } + + // Resume string highlighting + yield string; + } else if /"/ { + yield string; + break; + } + await input; + } + } else if /"/ { + until /$/ { + yield string; + if /\\./ {} + else if /"/ { yield string; break; } + await input; + } + } else if /'/ { + // Character literal + until /$/ { + yield string; + if /\\./ {} + else if /'/ { yield string; break; } + await input; + } + } else if /\[(?:assembly:|module:)?\s*([A-Z]\w*)(?:Attribute)?/ { + // Attribute names like [Obsolete] + yield $1 as markup.link; + } else if /(?:break|case|catch|continue|default|do|else|finally|for|foreach|goto|if|return|switch|throw|try|while|yield)\>/ { + yield keyword.control; + } else if /(?:abstract|as|await|base|bool|byte|char|checked|class|const|decimal|delegate|dynamic|enum|event|explicit|extern|false|fixed|float|from|get|global|group|implicit|in|int|interface|internal|into|is|join|let|lock|long|nameof|namespace|new|null|object|operator|orderby|out|override|params|partial|private|protected|public|readonly|record|ref|required|sbyte|sealed|select|set|short|sizeof|stackalloc|static|string|struct|this|true|typeof|uint|ulong|unmanaged|unchecked|unsafe|ushort|using|var|virtual|void|volatile|where|with)\>/ { + yield keyword.other; + } else if /(?i:-?(?:0x[\da-fA-F_]+|0b[01_]+|0o[0-7_]+|[\d_]+\.?[\d_]*|\.[\d_]+)(?:e[+-]?[\d_]+)?)/ { + 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; + } +}