diff --git a/assets/highlighting-tests/fsharp.fsx b/assets/highlighting-tests/fsharp.fsx new file mode 100644 index 00000000000..0998a432e1e --- /dev/null +++ b/assets/highlighting-tests/fsharp.fsx @@ -0,0 +1,74 @@ +// --- F# highlighting test --- +// Exercises: comments, block comments, strings, chars, numbers, directives, +// keywords/control flow, types/members, and backtick identifiers. + +#r "System.Runtime" +#r "nuget: Newtonsoft.Json, 13.0.3" +#load "some-script.fsx" + +(*** Block comment (not nested in this highlighter) ***) +(* Another block comment *) + +open System +open Newtonsoft.Json + +module ``My Module With Spaces`` = + let pi = 3.14159 + let hex = 0xDEAD_BEEF + let bin = 0b1010_1100 + let sci = 1.23e-4 + let mutable counter = 0 + + let name = "Edit" + let path = @"C:\Program Files\Edit\edit.exe" + let escaped = "tab:\t newline:\n quote:\" backslash:\\" + let interpolated = $"Hello, {name}!" + let interpolatedVerbatim = $@"Path: {path}" + + let triple = """ +This is a triple-quoted string. +It can span multiple lines without escapes. +""" + + let chA = 'a' + let chN = '\n' + + let rec fib n = + if n <= 1 then n + else fib (n - 1) + fib (n - 2) + + let classify x = + match x with + | 0 -> "zero" + | 1 | 2 -> "small" + | _ when x < 0 -> "negative" + | _ -> "other" + + let tryParseInt (s: string) = + try + let v = Int32.Parse(s) + Ok v + with + | :? FormatException -> Error "format" + | ex -> Error ex.Message + + type Person(name: string, age: int) = + member _.Name = name + member _.Age = age + override _.ToString() = $"{name} ({age})" + + let demoLoops () = + for i = 1 to 3 do + counter <- counter + i + + while counter < 20 do + counter <- counter + 1 + + // Method-call style tokenization + let p = Person("Ada", 37) + printfn "%s" (p.ToString()) + + // Single-backtick identifier fallback (not typical F#, but supported by lexer) + let `singleBacktickIdentifier` = 42 + + demoLoops () diff --git a/crates/lsh/definitions/fsharp.lsh b/crates/lsh/definitions/fsharp.lsh new file mode 100644 index 00000000000..82001a30c36 --- /dev/null +++ b/crates/lsh/definitions/fsharp.lsh @@ -0,0 +1,115 @@ +#[display_name = "F#"] +#[path = "**/*.fs"] +#[path = "**/*.fsi"] +#[path = "**/*.fsx"] +pub fn fsharp() { + until /$/ { + yield other; + + // Line comment + if /\/\/.*/ { + yield comment; + + // Preprocessor / script directives + } else if /#(?:if|else|endif|load|r|I|nowarn|light|time|help|quit)\>.*/ { + yield keyword.other; + + // Block comment: (* ... *) + } else if /\(\*/ { + loop { + yield comment; + if /\*\)/ { + yield comment; + break; + } + await input; + } + + // Triple-quoted string: """ ... """ + } else if /"""/ { + loop { + yield string; + if /"""/ { + yield string; + break; + } + await input; + } + + // Verbatim (optionally interpolated) string: @"..." or $@"..." + } else if /\$?@"/ { + loop { + yield string; + if /""/ { + // Escaped quote + } else if /"/ { + yield string; + break; + } + await input; + } + + // Interpolated string: $"..." + } else if /\$"/ { + until /$/ { + yield string; + if /\\./ { + // Escape sequences + } else if /"/ { + yield string; + break; + } + await input; + } + + // Regular string: "..." + } else if /"/ { + until /$/ { + yield string; + if /\\./ { + // Escape sequences + } else if /"/ { + yield string; + break; + } + await input; + } + + // Character literal: 'a', '\n', etc. + } else if /'(?:\\.|[^'\\])'/ { + yield string; + + // Backtick identifiers: ``identifier with spaces`` + } else if /``[^`]+``/ { + yield variable; + } else if /`[^`]+`/ { + yield variable; + + // Keywords + } else if /(?:if|then|else|elif|match|with|function|try|finally|for|to|downto|while|do|return|yield|begin|end|in)\>/ { + yield keyword.control; + } else if /(?:let|use|and|rec|mutable|open|module|namespace|type|member|interface|abstract|override|default|static|inline|extern|public|private|internal|new|inherit|as|of|when|exception|raise|lazy|assert|global)\>/ { + yield keyword.other; + + // Language constants + } else if /(?:true|false|null)\>/ { + yield constant.language; + + // Numbers (best-effort) + } else if /(?i:-?(?:0x[\da-f_]+|0b[01_]+|0o[0-7_]+|[\d_]+\.?[\d_]*|\.[\d_]+)(?:e[+-]?[\d_]+)?)(?:u(?:y|s|l|n)|(?:y|s|l|n)|[fFmM])?/ { + if /\w+/ { + // Not a number after all. + } else { + yield constant.numeric; + } + + // Method calls in .NET-style syntax: Foo.Bar(...) + } 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; + } +}