From 28c4b5c0f15bf3ab43e97086b1139584cd396ee6 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Wed, 6 Apr 2022 20:47:31 -0500 Subject: [PATCH] Use System.CommandLine for CLI parsing (#888) --- paket.dependencies | 4 +- paket.lock | 16 +- src/Directory.Build.props | 2 + .../FsAutoComplete.Core.fsproj | 1 + src/FsAutoComplete/FsAutoComplete.fsproj | 3 +- src/FsAutoComplete/Options.fs | 85 ------ src/FsAutoComplete/Parser.fs | 244 ++++++++++++++++++ src/FsAutoComplete/Program.fs | 75 +----- src/FsAutoComplete/paket.references | 8 +- 9 files changed, 266 insertions(+), 172 deletions(-) delete mode 100644 src/FsAutoComplete/Options.fs create mode 100644 src/FsAutoComplete/Parser.fs diff --git a/paket.dependencies b/paket.dependencies index 27dfc2973..6fd2c8b24 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -10,7 +10,6 @@ storage: none github TheAngryByrd/FsLibLog:f81cba440bf0476bb4e2262b57a067a0d6ab78a7 src/FsLibLog/FsLibLog.fs -nuget Argu nuget Fantomas.Client nuget FSharp.Compiler.Service nuget Ionide.ProjInfo @@ -27,7 +26,7 @@ nuget Mono.Cecil >= 0.10.0-beta7 nuget Newtonsoft.Json # nuget Fake.Runtime prerelease nuget FSharpLint.Core -nuget FSharp.Core +nuget FSharp.Core content: none nuget Dapper nuget Microsoft.Data.Sqlite 2.2.4 nuget Microsoft.Data.Sqlite.Core 2.2.4 @@ -44,6 +43,7 @@ nuget FSharp.Formatting nuget FsToolkit.ErrorHandling nuget FSharpx.Async nuget CliWrap +nuget System.CommandLine prerelease nuget Microsoft.NET.Test.Sdk nuget Dotnet.ReproducibleBuilds copy_local:true diff --git a/paket.lock b/paket.lock index 50a1e2a1c..5b154c200 100644 --- a/paket.lock +++ b/paket.lock @@ -3,9 +3,6 @@ RESTRICTION: || (== net6.0) (== netstandard2.0) NUGET remote: https://api.nuget.org/v3/index.json altcover (8.2.837) - Argu (6.1.1) - FSharp.Core (>= 4.3.2) - System.Configuration.ConfigurationManager (>= 4.4) CliWrap (3.4.2) Microsoft.Bcl.AsyncInterfaces (>= 6.0) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netstandard2.1)) (== netstandard2.0) System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netstandard2.1)) (== netstandard2.0) @@ -67,7 +64,7 @@ NUGET FSharp.Control.Reactive (5.0.2) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) FSharp.Core (>= 4.7.2) System.Reactive (>= 5.0) - FSharp.Core (6.0.3) + FSharp.Core (6.0.3) - content: none FSharp.Formatting (15.0) FSharp.Compiler.Service (41.0.3) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netstandard2.1)) FSharp.UMX (1.1) @@ -92,7 +89,8 @@ NUGET FSharp.Core (>= 4.7.2) GitHubActionsTestLogger (1.3) Microsoft.TestPlatform.ObjectModel (>= 17.0) - ICSharpCode.Decompiler (7.2.0.6844) + ICSharpCode.Decompiler (7.2.1.6856) + Microsoft.Win32.Registry (>= 5.0) System.Collections.Immutable (>= 5.0) System.Reflection.Metadata (>= 5.0) Ionide.KeepAChangelog.Tasks (0.1.6) - copy_local: true @@ -224,7 +222,9 @@ NUGET Microsoft.NETCore.Platforms (>= 1.1) Microsoft.NETCore.Targets (>= 1.1) System.Runtime (>= 4.3) - Microsoft.Win32.Registry (5.0) - copy_local: false + Microsoft.Win32.Registry (5.0) + System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= monoandroid) (< netstandard1.3)) (&& (== net6.0) (>= monotouch)) (&& (== net6.0) (< netcoreapp2.0)) (&& (== net6.0) (>= xamarinios)) (&& (== net6.0) (>= xamarinmac)) (&& (== net6.0) (>= xamarintvos)) (&& (== net6.0) (>= xamarinwatchos)) (== netstandard2.0) + System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (< netcoreapp2.0)) (&& (== net6.0) (< netcoreapp2.1)) (&& (== net6.0) (>= uap10.1)) (== netstandard2.0) System.Security.AccessControl (>= 5.0) System.Security.Principal.Windows (>= 5.0) Microsoft.Win32.SystemEvents (6.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) @@ -340,6 +340,8 @@ NUGET System.Collections.Immutable (6.0) System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (== netstandard2.0) System.Runtime.CompilerServices.Unsafe (>= 6.0) + System.CommandLine (2.0.0-beta3.22114.1) + System.Memory (>= 4.5.4) - restriction: == netstandard2.0 System.Configuration.ConfigurationManager (6.0) System.Security.Cryptography.ProtectedData (>= 6.0) System.Security.Permissions (>= 6.0) @@ -550,7 +552,7 @@ NUGET System.Resources.ResourceManager (>= 4.3) System.Runtime (>= 4.3) System.Threading.Tasks (>= 4.3) - System.Numerics.Vectors (4.5) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp2.0) (< netstandard2.1)) (== netstandard2.0) + System.Numerics.Vectors (4.5) - restriction: || (&& (== net6.0) (< netcoreapp2.0)) (== netstandard2.0) System.ObjectModel (4.3) System.Collections (>= 4.3) System.Diagnostics.Debug (>= 4.3) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 913ad3ed4..0c92968a9 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,5 +5,7 @@ true $(NoWarn);FS2003 + + $(NoWarn);NU5104 diff --git a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj index 8bd947cb3..aa46eab79 100644 --- a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj +++ b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj @@ -5,6 +5,7 @@ + diff --git a/src/FsAutoComplete/FsAutoComplete.fsproj b/src/FsAutoComplete/FsAutoComplete.fsproj index 9968bfd24..3994a9f4f 100644 --- a/src/FsAutoComplete/FsAutoComplete.fsproj +++ b/src/FsAutoComplete/FsAutoComplete.fsproj @@ -12,18 +12,17 @@ logo.png $(RepositoryUrl) FsAutoComplete contributors - false - + diff --git a/src/FsAutoComplete/Options.fs b/src/FsAutoComplete/Options.fs deleted file mode 100644 index dd098d6af..000000000 --- a/src/FsAutoComplete/Options.fs +++ /dev/null @@ -1,85 +0,0 @@ -// -------------------------------------------------------------------------------------- -// (c) Robin Neatherway -// -------------------------------------------------------------------------------------- -namespace FsAutoComplete - -open System -open Serilog -open Serilog.Core -open Serilog.Events - -module Options = - open Argu - - type CLIArguments = - | Version - | [] Verbose - | AttachDebugger - | [] Logfile of path:string - | Filter of filter: string list - | [] WaitForDebugger - | [] HostPID of pid:int - | [] BackgroundServiceEnabled - | [] ProjectGraphEnabled - with - interface IArgParserTemplate with - member s.Usage = - match s with - | Version -> "display versioning information" - | AttachDebugger -> "launch the system debugger and break." - | Verbose -> "enable verbose mode" - | Logfile _ -> "send verbose output to specified log file" - | Filter _ -> "filter out messages that match the provided category" - | WaitForDebugger _ -> "wait for a debugger to attach to the process" - | HostPID _ -> "the Host process ID." - | BackgroundServiceEnabled -> "enable background service" - | ProjectGraphEnabled -> "enable MsBuild ProjectGraph for workspace loading. Experimental." - - let isCategory (category: string) (e: LogEvent) = - match e.Properties.TryGetValue "SourceContext" with - | true, loggerName -> - match loggerName with - | :? ScalarValue as v -> - match v.Value with - | :? string as s when s = category -> true - | _ -> false - | _ -> false - | false, _ -> false - - let hasMinLevel (minLevel: LogEventLevel) (e: LogEvent) = - e.Level >= minLevel - - // will use later when a mapping-style config of { "category": "minLevel" } is established - let excludeByLevelWhenCategory category level event = isCategory category event || not (hasMinLevel level event) - - let apply (levelSwitch: LoggingLevelSwitch) (logConfig: Serilog.LoggerConfiguration) (args: ParseResults) = - - let applyArg arg = - match arg with - | Verbose -> - levelSwitch.MinimumLevel <- LogEventLevel.Verbose - () - | AttachDebugger -> - System.Diagnostics.Debugger.Launch() |> ignore - | Logfile s -> - try - logConfig.WriteTo.Async(fun c -> c.File(path = s, levelSwitch = levelSwitch) |> ignore) |> ignore - with - | e -> - eprintfn "Bad log file: %s" e.Message - exit 1 - | Filter categories -> - categories - |> List.iter (fun category -> - // category is encoded in the SourceContext property, so we filter messages based on that property's value - logConfig.Filter.ByExcluding(Func<_,_>(isCategory category)) |> ignore - ) - | Version - | WaitForDebugger - | BackgroundServiceEnabled - | ProjectGraphEnabled - | HostPID _ -> - () - - args.GetAllResults() - |> List.iter applyArg diff --git a/src/FsAutoComplete/Parser.fs b/src/FsAutoComplete/Parser.fs new file mode 100644 index 000000000..a880b3f71 --- /dev/null +++ b/src/FsAutoComplete/Parser.fs @@ -0,0 +1,244 @@ +namespace FsAutoComplete + +open System +open Serilog +open Serilog.Core +open Serilog.Events +open System.CommandLine +open System.CommandLine.Parsing +open System.CommandLine.Builder +open Serilog.Filters + +module Parser = + open System.Threading.Tasks + [] + type Pos = { Line: int; Column: int } + + [] + type Rng = + { File: string + Start: FSharp.Compiler.Text.pos + End: FSharp.Compiler.Text.pos } + + let private setArity arity (o: #Option) = + o.Arity <- arity + o + + /// set option to expect no arguments (e.g a flag-style argument: `--verbose`) + let inline private zero x = setArity ArgumentArity.Zero x + /// set option to expect one argument (e.g a single value: `--foo bar) + let inline private one x = setArity ArgumentArity.ExactlyOne x + + /// set option to expect multiple arguments + /// (e.g a list of values: `--foo bar baz` or `--foo bar --foo baz` depending on the style) + let inline private many x = setArity ArgumentArity.OneOrMore x + + /// set option to allow multiple arguments per use of the option flag + /// (e.g. `--foo bar baz` is equivalent to `--foo bar --foo baz`) + let inline private multipleArgs (x: #Option) = + x.AllowMultipleArgumentsPerToken <- true + x + + let verboseOption = + Option([| "--verbose"; "-v"; "--debug" |], "Enable verbose logging. This is equivalent to --log-level debug.") + |> setArity ArgumentArity.Zero + + let logLevelOption = + Option("--log-level", "Set the log verbosity to a specific level.") + + let attachOption = + Option("--attach-debugger", "Launch the system debugger and break immediately") + |> zero + + let logFileOption = + Option([| "--logfile"; "-l"; "--log-file" |], "Send log output to specified file.") + |> one + + let logFilterOption = + Option( + [| "--filter"; "--log-filter" |], + "Filter logs by category. The category can be seen in the logs inside []. For example: [Compiler]." + ) + |> many + |> fun o -> + o.AllowMultipleArgumentsPerToken <- true + o + + let waitForDebuggerOption = + Option( + "--wait-for-debugger", + "Stop execution on startup until an external debugger to attach to this process" + ) + |> zero + + let backgroundServiceOption = + Option( + "--background-service-enabled", + "Enable running typechecking services in a background process. Enables various performance optimizations." + ) + |> zero + + let projectGraphOption = + Option( + "--project-graph-enabled", + "Enable MSBuild Graph workspace loading. Should be faster than the default, but is experimental." + ) + |> zero + + let rootCommand = + let rootCommand = RootCommand("An F# LSP server implementation") + + rootCommand.AddOption verboseOption + rootCommand.AddOption attachOption + rootCommand.AddOption logFileOption + rootCommand.AddOption logFilterOption + rootCommand.AddOption waitForDebuggerOption + rootCommand.AddOption backgroundServiceOption + rootCommand.AddOption projectGraphOption + rootCommand.AddOption logLevelOption + rootCommand.SetHandler( + Func<_,_,Task>(fun backgroundServiceEnabled projectGraphEnabled -> + let workspaceLoaderFactory = + if projectGraphEnabled then Ionide.ProjInfo.WorkspaceLoaderViaProjectGraph.Create + else Ionide.ProjInfo.WorkspaceLoader.Create + + let toolsPath = Ionide.ProjInfo.Init.init (IO.DirectoryInfo Environment.CurrentDirectory) None + use _compilerEventListener = new Debug.FSharpCompilerEventLogger.Listener() + let result = Lsp.start backgroundServiceEnabled toolsPath workspaceLoaderFactory + + Task.FromResult result + ), backgroundServiceOption, projectGraphOption) + rootCommand + + let waitForDebugger = + Invocation.InvocationMiddleware (fun ctx next -> + let waitForDebugger = ctx.ParseResult.HasOption waitForDebuggerOption + + if waitForDebugger then + Debug.waitForDebugger () + + next.Invoke(ctx)) + + let immediateAttach = + Invocation.InvocationMiddleware (fun ctx next -> + let attachDebugger = ctx.ParseResult.HasOption attachOption + + if attachDebugger then + Diagnostics.Debugger.Launch() + |> ignore + + next.Invoke(ctx)) + + let configureLogging = + Invocation.InvocationMiddleware (fun ctx next -> + let isCategory (category: string) (e: LogEvent) = + match e.Properties.TryGetValue "SourceContext" with + | true, loggerName -> + match loggerName with + | :? ScalarValue as v -> + match v.Value with + | :? string as s when s = category -> true + | _ -> false + | _ -> false + | false, _ -> false + + let hasMinLevel (minLevel: LogEventLevel) (e: LogEvent) = e.Level >= minLevel + + // will use later when a mapping-style config of { "category": "minLevel" } is established + let excludeByLevelWhenCategory category level event = + isCategory category event + || not (hasMinLevel level event) + + let args = ctx.ParseResult + + let logLevel = + if args.HasOption verboseOption then + LogEventLevel.Debug + else if args.HasOption logLevelOption then + args.GetValueForOption logLevelOption + else + LogEventLevel.Warning + + let logSourcesToExclude = + if args.HasOption logFilterOption then + args.GetValueForOption logFilterOption + else + [||] + + let sourcesToExclude = + Matching.WithProperty( + Constants.SourceContextPropertyName, + fun s -> s <> null && Array.contains s logSourcesToExclude + ) + + let verbositySwitch = LoggingLevelSwitch(logLevel) + + let outputTemplate = + "[{Timestamp:HH:mm:ss.fff} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}" + + let logConf = + LoggerConfiguration() + .MinimumLevel.ControlledBy(verbositySwitch) + .Filter.ByExcluding(Matching.FromSource("FileSystem")) + .Filter.ByExcluding(sourcesToExclude) + .Enrich.FromLogContext() + .Destructure.FSharpTypes() + .Destructure + .ByTransforming(fun r -> + { File = r.FileName + Start = r.Start + End = r.End }) + .Destructure.ByTransforming(fun r -> { Line = r.Line; Column = r.Column }) + .Destructure.ByTransforming(fun tok -> tok.ToString() |> box) + .Destructure.ByTransforming(fun di -> box di.FullName) + .WriteTo + .Async(fun c -> + c.Console( + outputTemplate = outputTemplate, + standardErrorFromLevel = Nullable<_>(LogEventLevel.Verbose), + theme = Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme.Code + ) + |> ignore) // make it so that every console log is logged to stderr so that we don't interfere with LSP stdio + + if args.HasOption logFileOption then + let logFile = args.GetValueForOption logFileOption + + try + logConf.WriteTo.Async (fun c -> + c.File(path = logFile, levelSwitch = verbositySwitch) + |> ignore) + |> ignore + with + | e -> + eprintfn "Bad log file: %s" e.Message + exit 1 + + if args.HasOption logFilterOption then + let categories = args.GetValueForOption logFilterOption + + categories + |> Array.iter (fun category -> + // category is encoded in the SourceContext property, so we filter messages based on that property's value + logConf.Filter.ByExcluding(Func<_, _>(isCategory category)) + |> ignore) + + let logger = logConf.CreateLogger() + Serilog.Log.Logger <- logger + Logging.LogProvider.setLoggerProvider (Logging.Providers.SerilogProvider.create ()) + next.Invoke(ctx)) + + let serilogFlush = + Invocation.InvocationMiddleware (fun ctx next -> + task { + do! next.Invoke ctx + Serilog.Log.CloseAndFlush() + }) + + let parser = + CommandLineBuilder(rootCommand) + .UseDefaults() + .AddMiddleware(waitForDebugger) + .AddMiddleware(immediateAttach) + .AddMiddleware(serilogFlush) + .AddMiddleware(configureLogging) + .Build() diff --git a/src/FsAutoComplete/Program.fs b/src/FsAutoComplete/Program.fs index 9611415d9..3b28e4aeb 100644 --- a/src/FsAutoComplete/Program.fs +++ b/src/FsAutoComplete/Program.fs @@ -1,77 +1,8 @@ module FsAutoComplete.Program -open System -open FsAutoComplete.JsonSerializer -open Argu -open Serilog -open Serilog.Core -open Serilog.Events -open FsAutoComplete.Logging +open System.CommandLine.Parsing [] let entry args = - - - try - let parser = ArgumentParser.Create(programName = "fsautocomplete") - - let results = parser.Parse args - - System.Threading.ThreadPool.SetMinThreads(16, 16) |> ignore - - // default the verbosity to warning - let verbositySwitch = LoggingLevelSwitch(LogEventLevel.Warning) - let outputTemplate = "[{Timestamp:HH:mm:ss.fff} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}" - let logConf = - LoggerConfiguration() - .MinimumLevel.ControlledBy(verbositySwitch) - .Enrich.FromLogContext() - .Destructure.FSharpTypes() - .Destructure.ByTransforming(fun r -> box {| FileName = r.FileName; Start = r.Start; End = r.End |}) - .Destructure.ByTransforming(fun r -> box {| Line = r.Line; Column = r.Column |}) - .Destructure.ByTransforming(fun tok -> tok.ToString() |> box) - .Destructure.ByTransforming(fun di -> box di.FullName) - .WriteTo.Async( - fun c -> c.Console(outputTemplate = outputTemplate, standardErrorFromLevel = Nullable<_>(LogEventLevel.Verbose), theme = Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme.Code) |> ignore - ) // make it so that every console log is logged to stderr - - - results.TryGetResult(<@ Options.CLIArguments.WaitForDebugger @>) - |> Option.iter (ignore >> Debug.waitForDebugger) - - results.TryGetResult(<@ Options.CLIArguments.Version @>) - |> Option.iter (fun _ -> - let version = Version.info () - printfn "FsAutoComplete %s (git sha %s)" (version.Version) (version.GitSha) - exit 0 ) - - Options.apply verbositySwitch logConf results - - let logger = logConf.CreateLogger() - Serilog.Log.Logger <- logger - LogProvider.setLoggerProvider (Providers.SerilogProvider.create()) - - let backgroundServiceEnabled = - results.Contains <@ Options.CLIArguments.BackgroundServiceEnabled @> - - let projectGraphEnabled = - results.Contains <@ Options.CLIArguments.ProjectGraphEnabled @> - - let workspaceLoaderFactory = - if projectGraphEnabled then Ionide.ProjInfo.WorkspaceLoaderViaProjectGraph.Create - else Ionide.ProjInfo.WorkspaceLoader.Create - - let toolsPath = Ionide.ProjInfo.Init.init (System.IO.DirectoryInfo Environment.CurrentDirectory) None - use _compilerEventListener = new Debug.FSharpCompilerEventLogger.Listener() - let result = FsAutoComplete.Lsp.start backgroundServiceEnabled toolsPath workspaceLoaderFactory - Serilog.Log.CloseAndFlush() - result - with - | :? ArguParseException as ex -> - printfn "%s" ex.Message - match ex.ErrorCode with - | ErrorCode.HelpText -> 0 - | _ -> 1 // Unrecognised arguments - | e -> - printfn "Server crashing error - %s \n %s" e.Message e.StackTrace - 3 + let results = Parser.parser.Invoke args + results diff --git a/src/FsAutoComplete/paket.references b/src/FsAutoComplete/paket.references index 4194033ed..f25dd059c 100644 --- a/src/FsAutoComplete/paket.references +++ b/src/FsAutoComplete/paket.references @@ -1,4 +1,4 @@ -Argu +#FSharpLint.Core CliWrap Dapper Destructurama.FSharp @@ -7,9 +7,10 @@ FSharp.Analyzers.SDK FSharp.Compiler.Service FSharp.Core FSharp.UMX -#FSharpLint.Core FsToolkit.ErrorHandling ICSharpCode.Decompiler +Ionide.KeepAChangelog.Tasks +Ionide.LanguageServerProtocol Ionide.ProjInfo Ionide.ProjInfo.ProjectSystem Microsoft.Data.Sqlite @@ -20,6 +21,5 @@ Serilog Serilog.Sinks.Async Serilog.Sinks.Console Serilog.Sinks.File +System.CommandLine System.Configuration.ConfigurationManager -Ionide.LanguageServerProtocol -Ionide.KeepAChangelog.Tasks